/* ============================================================
   WOOL ONLINE — Modernised Design System
   ============================================================ */

/* --- Reset & Base --- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* The platform's tokens.css and base.css load first. wolx keeps its own look:
   light only, headings without base.css's margin, and .page (below) a block. */
:root { color-scheme: normal; }
h1 { margin: 0; }

:root {
    /* Brand */

    /* Neutrals */
    --gray-50:  #F9FAFB;
    --gray-100: #F3F4F6;
    --gray-200: #E5E7EB;
    --gray-300: #D1D5DB;
    --gray-400: #9CA3AF;
    --gray-500: #6B7280;
    --gray-600: #4B5563;
    --gray-700: #374151;
    --gray-800: #1F2937;
    --gray-900: #111827;

    /* Status */
    --status-red:        #DC2626;
    --status-amber:      #D97706;
    --status-blue:       #2563EB;

    /* Spacing */
    --space-xs:  0.25rem;
    --space-sm:  0.5rem;
    --space-md:  0.75rem;
    --space-lg:  1rem;
    --space-xl:  1.5rem;
    --space-2xl: 2rem;
    --space-3xl: 3rem;

    /* Typography */
    --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
    --font-mono: 'Red Hat Mono', 'JetBrains Mono', ui-monospace, monospace;

    --text-xs:   0.75rem;
    --text-sm:   0.8125rem;
    --text-base: 0.875rem;
    --text-xl:   1.25rem;

    /* Layout */
    --max-width: 1280px;
    --footer-height: 2.5rem;

    /* Misc */
    --radius-md: 6px;
    --radius-lg: 8px;

    /* ── Redesign (April 2026): ink / paper / sage palette ── */
    --ink:         #16202E;
    --ink-2:       #243148;
    --ink-3:       #455168;
    --ink-4:       #6B7285;
    --ink-5:       #9A9FAC;
    --line:        #E4DCC9;
    --line-soft:   #EFE8D6;
    --paper-light: #FDFBF7;
    --paper:       #FAF6EE;
    --paper-2:     #F4EEDF;
    --paper-3:     #EDE5D0;
    --card:        #FFFFFF;

    --win:         oklch(55% 0.10 148);
    --win-bg:      oklch(96% 0.03 148);
    --win-border:  oklch(85% 0.05 148);
    --lose:        oklch(54% 0.15 30);
    --lose-bg:     oklch(96% 0.03 30);
    --lose-border: oklch(85% 0.06 30);
    --pass:        oklch(60% 0.13 70);
    --pass-bg:     oklch(96% 0.04 70);
    --live:        oklch(58% 0.15 250);
    --live-bg:     oklch(96% 0.03 250);

    --sans: 'Inter Tight', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
    --mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;

    --shadow-wol-sm: 0 1px 2px rgba(22,32,46,0.04);
    --shadow-wol:    0 1px 3px rgba(22,32,46,0.06), 0 8px 24px -8px rgba(22,32,46,0.10);
    --shadow-wol-lg: 0 20px 60px -20px rgba(22,32,46,0.25);
}

html {
    font-size: 16px;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

body {
    font-family: var(--font-sans);
    font-size: var(--text-base);
    color: var(--gray-800);
    background: var(--gray-50);
    line-height: 1.5;
    min-height: 100vh;
}

.session-expired-notice {
    position: fixed;
    z-index: 10000;
    bottom: 1rem;
    left: 1rem;
    right: 1rem;
    width: fit-content;
    max-width: calc(100vw - 2rem);
    padding: 0.85rem 1rem;
    border: 2px solid #991B1B;
    border-radius: 6px;
    background: #FEF2F2;
    color: #7F1D1D;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
}

.session-expired-notice a { color: #7F1D1D; font-weight: 700; text-decoration: underline; }

a { color: var(--status-blue); text-decoration: none; }
a:hover { text-decoration: underline; }

/* ============================================================
   NAVIGATION
   ============================================================ */.nav-link.active {
    background: rgba(255, 255, 255, 0.2);
    color: #FFFFFF;
}

.page {
    display: block;
    gap: normal;
    max-width: var(--max-width);
    margin: 0 auto;
    padding: var(--space-xl) var(--space-xl) calc(var(--footer-height) + var(--space-xl));
}

/* ============================================================
   PAGE HEADER (Sale title area)
   ============================================================ */
.page-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: var(--space-2xl);
    gap: var(--space-lg);
}

.page-header-left h1 {
    font-size: var(--text-xl);
    font-weight: 700;
    color: var(--gray-900);
    line-height: 1.3;
}

.page-header-left .subtitle {
    font-size: var(--text-sm);
    color: var(--gray-500);
    margin-top: var(--space-xs);
}

.row-number {
    position: absolute;
    left: -3rem;
    top: 50%;
    transform: translateY(-50%);
    font-size: var(--text-xs);
    color: var(--gray-400);
    font-weight: 400;
    font-variant-numeric: tabular-nums;
    text-align: right;
    width: 2rem;
}

/* ============================================================
   STATUS LABELS
   ============================================================ */
.price {
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--gray-900);
}

.countdown {
    font-size: var(--text-xl);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--status-red);
}

.countdown.warning {
    color: var(--status-amber);
}

/* ============================================================
   LOGIN PAGE
   ============================================================ */
.login-page {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    background: var(--paper);
    font-family: var(--sans);
    color: var(--ink);
    line-height: 1.45;
}
.login-main {
    flex: 1 0 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 20px 24px 14px;
}

.login-page .brand-lockup {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    margin-bottom: 16px;
}
.login-page .brand-mark {
    width: clamp(128px, 18vh, 160px);
    height: auto;
}
.login-page .brand-lockup h1 {
    margin-top: 4px;
    font-size: 36px;
    font-weight: 700;
    letter-spacing: -0.03em;
    line-height: 1.05;
    color: var(--ink);
}
.login-page .brand-tag {
    margin-top: 8px;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--ink-4);
}

.login-card {
    width: 100%;
    max-width: 440px;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: 14px;
    box-shadow: var(--shadow-wol);
    padding: 22px 32px 12px;
}

.login-site-link {
    margin-top: 14px;
    text-align: center;
    font-size: 14px;
    color: var(--ink-3);
}
.login-site-link a {
    font-weight: 600;
    color: var(--ink-2);
    text-decoration: underline;
    text-underline-offset: 3px;
}
.login-site-link a:hover { color: var(--ink); }

.login-footer {
    padding: 0 24px 14px;
    text-align: center;
    font-size: 11.5px;
    line-height: 1.7;
    letter-spacing: 0.02em;
    color: var(--ink-5);
}
.login-footer a {
    color: var(--ink-4);
    text-decoration: none;
}
.login-footer a:hover {
    color: var(--ink);
    text-decoration: underline;
}
.login-footer .build-version {
    font-family: var(--mono);
    font-size: 10.5px;
}

/* The photo takes most of the height the content leaves, between its min and max,
   so it is tallest where there is room and never pushes the page into scrolling. */
.login-banner {
    --banner-min: clamp(80px, 11vh, 160px);
    position: relative;
    flex: 3 0 var(--banner-min);
    min-height: var(--banner-min);
    max-height: clamp(112px, 15.5vh, 224px);
    overflow: hidden;
    background: var(--paper-3);
}
.login-banner img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}
/* Backend system message above the sign-in card (pre-auth announcements,
   maintenance notices, and the TEST-server identity stamp). Red emphasis
   mirrors the legacy login banner; width-matched to the card. */
.login-system-message {
    width: 100%;
    max-width: 440px;
    margin-bottom: 20px;
    text-align: center;
    font-size: 14px;
    font-weight: 700;
    color: var(--lose);
    background: var(--lose-bg);
    border: 1px solid var(--lose-border);
    border-radius: 8px;
    padding: 12px 16px;
    line-height: 1.45;
}
.login-card h3 {
    font-size: 26px;
    font-weight: 700;
    letter-spacing: -0.02em;
    color: var(--ink);
    margin-bottom: 24px;
    line-height: 1.2;
}
.login-card .tagline {
    font-size: 14px;
    color: var(--ink-3);
    margin-bottom: 28px;
    line-height: 1.5;
}
.login-card .form-error {
    font-size: 13px;
    color: var(--lose);
    background: var(--lose-bg);
    border: 1px solid var(--lose-border);
    border-radius: 8px;
    padding: 10px 14px;
    margin-bottom: 20px;
}

.login-tab.active {
    background: var(--card);
    color: var(--ink);
    box-shadow: var(--shadow-wol-sm);
    font-weight: 600;
}
.login-tab:not(.active):hover {
    color: var(--ink);
}

.login-page .form-group { margin-bottom: 16px; }
.login-page .form-label {
    display: block;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-4);
    margin-bottom: 6px;
}
.login-page .form-input {
    width: 100%;
    padding: 11px 14px;
    font-family: var(--sans);
    font-size: 14px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: var(--card);
    color: var(--ink);
    outline: none;
    transition: all 0.15s;
}
.login-page .form-input:focus {
    border-color: var(--ink);
    box-shadow: 0 0 0 3px rgba(22,32,46,0.08);
}
.login-page .form-input::placeholder {
    color: var(--ink-5);
}
.login-page .form-check {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 13px;
    color: var(--ink-3);
    margin-top: 4px;
    margin-bottom: 20px;
    cursor: pointer;
}
.login-page .form-check input {
    width: 16px;
    height: 16px;
    accent-color: var(--ink);
    cursor: pointer;
}

.login-card .terms-footer {
    font-size: 12px;
    line-height: 1.5;
    color: var(--ink-3);
    margin: 4px 0 18px;
}
.login-card .terms-footer a {
    color: var(--ink-2);
    text-decoration: underline;
}
.login-card .terms-footer a:hover {
    color: var(--ink);
}

/* Primary login-style button. Lifted out of the .login-card scope
   so the forgot-password dialog (which lives outside the login card
   in the DOM) picks up the same dark ink treatment. The login form
   keeps width:100%; the dialog overrides to width:auto in its
   action row below. */
.btn-login {
    width: 100%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 12px 20px;
    font-family: var(--sans);
    font-size: 14px;
    font-weight: 600;
    border-radius: 8px;
    border: 1px solid var(--ink);
    background: var(--ink);
    color: var(--paper);
    cursor: pointer;
    transition: all 0.15s;
    text-decoration: none;
    letter-spacing: 0.01em;
}
.btn-login:hover {
    background: var(--ink-2);
    border-color: var(--ink-2);
}
.btn-login:disabled {
    opacity: 0.65;
    cursor: progress;
}

.forgot-link {
    display: block;
    width: 100%;
    text-align: center;
    font-size: 13px;
    color: var(--ink-3);
    margin-top: 12px;
    background: none;
    border: none;
    padding: 4px;
    cursor: pointer;
    font-family: inherit;
    text-decoration: none;
}
.forgot-link:hover {
    color: var(--ink);
    text-decoration: underline;
}

/* ============================================================
   FORGOT-PASSWORD DIALOG (login page)
   ============================================================ */
.forgot-dialog-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(22, 32, 46, 0.55);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
    z-index: 100;
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
}
.forgot-dialog-backdrop[hidden] { display: none; }

.forgot-dialog-card {
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: 14px;
    padding: 24px 24px 20px;
    width: 100%;
    max-width: 420px;
    box-shadow: var(--shadow-wol);
    color: var(--ink);
    font-family: var(--sans);
}
.forgot-dialog-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 8px;
}
.forgot-dialog-header h3 {
    font-size: 18px;
    font-weight: 700;
    letter-spacing: -0.015em;
    color: var(--ink);
    line-height: 1.2;
}
.forgot-dialog-close {
    background: none;
    border: none;
    color: var(--ink-4);
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
    padding: 0 4px;
    margin: -4px -4px 0 0;
}
.forgot-dialog-close:hover { color: var(--ink); }
.forgot-dialog-blurb {
    font-size: 13px;
    color: var(--ink-3);
    margin-bottom: 18px;
    line-height: 1.5;
}
.forgot-dialog-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    margin-top: 18px;
}
.forgot-dialog-actions .btn-secondary {
    padding: 10px 16px;
    font-size: 14px;
    font-weight: 500;
    border-radius: 8px;
    border: 1px solid var(--line);
    background: var(--card);
    color: var(--ink-2);
    cursor: pointer;
    font-family: inherit;
}
.forgot-dialog-actions .btn-secondary:hover {
    border-color: var(--ink-4);
    color: var(--ink);
}
.forgot-dialog-actions .btn-login {
    width: auto;
    padding: 10px 18px;
}

/* In-flight indicator on the forgot-password submit. htmx adds the
   .htmx-request class to the form for the duration of the POST;
   we swap the visible label between "Send reset email" and
   "Sending…" + a small spinner so the user has clear feedback the
   click was received. The button is disabled via hx-disabled-elt
   on the form so a second click can't double-submit. */
.btn-login .btn-spinner { display: none; }
form.htmx-request .btn-login .btn-label { display: none; }
form.htmx-request .btn-login .btn-spinner {
    display: inline-flex;
    align-items: center;
    gap: 8px;
}
.btn-spinner::before {
    content: '';
    width: 12px;
    height: 12px;
    border-radius: 50%;
    border: 2px solid currentColor;
    border-top-color: transparent;
    animation: btn-spin 0.7s linear infinite;
    display: inline-block;
}
@keyframes btn-spin {
    to { transform: rotate(360deg); }
}

/* Success / error fragment swapped in by htmx after the POST. Sits
   below the form actions. */
#forgot-result.forgot-result {
    margin-top: 14px;
    padding: 10px 14px;
    border-radius: 8px;
    font-size: 13px;
    line-height: 1.45;
}
#forgot-result.forgot-result.success {
    background: var(--win-bg);
    border: 1px solid var(--win-border);
    color: var(--win);
}
#forgot-result.forgot-result.error {
    background: var(--lose-bg);
    border: 1px solid var(--lose-border);
    color: var(--lose);
}

/* Shorter windows (a 1080p laptop at 125% leaves about 730px): a tighter lockup
   and card keep the photo in view without scrolling. */
@media (max-height: 880px) {
    .login-main { padding: 14px 24px 10px; }
    .login-page .brand-lockup { margin-bottom: 12px; }
}
@media (max-height: 810px) {
    .login-main { padding: 12px 24px 8px; }
    .login-page .brand-lockup { margin-bottom: 10px; }
    .login-page .brand-mark { width: 110px; }
    .login-page .brand-lockup h1 { margin-top: 2px; font-size: 28px; }
    .login-page .brand-tag { margin-top: 4px; }
    .login-card { padding: 20px 28px 12px; }
    .login-page .login-card h3 { font-size: 22px; }
    .login-page .login-card-head { margin-bottom: 16px; }
    .login-banner {
        --banner-min: clamp(64px, 11vh, 160px);
        max-height: clamp(90px, 15.5vh, 224px);
    }
}
@media (max-width: 560px) {
    .login-main { padding: 20px 16px; }
    .login-page .brand-mark { width: 120px; }
    .login-page .brand-lockup h1 { font-size: 30px; }
    .login-card { padding: 22px 20px 16px; }
    .login-banner { --banner-min: 112px; max-height: 112px; }
}

/* ============================================================
   LEGAL PAGES (/terms, /privacy)
   ============================================================
   Generalised from the old .terms-page rules so both documents share one
   set of type styles. Each renders inside the normal .page shell when the
   reader is signed in, and standalone (no sidebar / topbar) when not. */

/* Standalone header: brand + a way back to sign-in, since there is no
   chrome around the document for a signed-out reader. */
.legal-page .legal-standalone-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
    padding-bottom: 18px;
    margin-bottom: 22px;
    border-bottom: 1px solid var(--line);
}
.legal-standalone-brand {
    display: flex;
    align-items: center;
    gap: 10px;
    font-family: var(--sans);
    font-weight: 700;
    color: var(--ink);
    text-decoration: none;
}
.legal-standalone-brand .ram img {
    display: block;
    width: 34px;
    height: 34px;
    object-fit: contain;
}
.legal-standalone-name {
    font-size: 16px;
    letter-spacing: -0.01em;
}
.legal-standalone-back {
    font-size: 13px;
    color: var(--ink-3);
    text-decoration: underline;
}
.legal-standalone-back:hover {
    color: var(--ink);
}

/* Signed out there is no .app wrapper, so the document sits straight on the
   body: give it the paper background and centre the measure. */
body > .page.legal-page {
    max-width: none;
    min-height: 100vh;
    background: var(--paper);
}
body > .page.legal-page .legal-standalone-head,
body > .page.legal-page .page-header,
body > .page.legal-page .legal-body {
    max-width: 74ch;
    margin-left: auto;
    margin-right: auto;
}

.legal-page .legal-body {
    max-width: 74ch;
    font-size: 15px;
    line-height: 1.7;
    color: var(--ink-2);
}
.legal-page .legal-body p {
    margin: 0 0 14px;
}
.legal-page .legal-body ul,
.legal-page .legal-body ol {
    margin: 0 0 16px;
    padding-left: 22px;
}
.legal-page .legal-body li {
    margin-bottom: 10px;
}
.legal-page .legal-body li:last-child {
    margin-bottom: 0;
}
.legal-page .legal-body strong {
    color: var(--ink);
    font-weight: 600;
}
.legal-page .legal-body a {
    color: var(--ink);
    text-decoration: underline;
    text-underline-offset: 2px;
}
.legal-page .legal-body a:hover {
    color: var(--ink-3);
}
.legal-page .legal-body a:focus-visible,
.legal-page .legal-standalone-head a:focus-visible {
    outline: 2px solid var(--live);
    outline-offset: 2px;
    border-radius: 2px;
}
.legal-page .legal-lead {
    font-size: 16px;
}
.legal-page .legal-section {
    margin-top: 30px;
}
.legal-page .legal-section h2 {
    font-family: var(--sans);
    font-size: 18px;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--ink);
    margin: 0 0 10px;
    /* Clears the sticky topbar when an in-page #anchor is followed. */
    scroll-margin-top: 80px;
}
.legal-page .legal-section h3 {
    font-size: 15px;
    font-weight: 600;
    color: var(--ink);
    margin: 18px 0 8px;
}

/* "Last updated" line at the top of each document. */
.legal-updated {
    font-family: var(--mono);
    font-size: 11.5px;
    letter-spacing: 0.04em;
    color: var(--ink-3);
    margin: 0 0 18px;
}

/* In-page contents. */
.legal-toc {
    margin: 20px 0 4px;
    padding: 14px 16px;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
}
.legal-toc ul {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 6px;
}
/* Beats the .legal-body li spacing — contents entries sit on the grid gap. */
.legal-page .legal-body .legal-toc li {
    margin: 0;
}
.legal-toc a {
    font-size: 13.5px;
    color: var(--ink-2);
    text-decoration: none;
}
.legal-toc a:hover {
    color: var(--ink);
    text-decoration: underline;
}

/* The legal-link group: both documents plus the contact address, at the
   foot of each page. The current document is named but not linked. */
.legal-links {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 18px;
    margin-top: 34px;
    padding-top: 16px;
    border-top: 1px solid var(--line);
    font-size: 13px;
}
.legal-links-current {
    color: var(--ink);
    font-weight: 600;
}
.legal-links .legal-links-link {
    color: var(--ink-3);
    text-decoration: underline;
}
.legal-links .legal-links-link:hover {
    color: var(--ink);
}

@media (max-width: 560px) {
    .legal-page .legal-body {
        font-size: 14.5px;
    }
    .legal-page .legal-lead {
        font-size: 15px;
    }
    .legal-page .legal-section h2 {
        font-size: 16.5px;
    }
    .legal-page .legal-body ul,
    .legal-page .legal-body ol {
        padding-left: 18px;
    }
}

/* ============================================================
   HELP / DOCUMENTATION PAGE
   ============================================================ */
/* Card stack shares the 900px cap the sales list uses so the two
   pages read at the same scale. Each card is a single <a> that opens
   the PDF in a new tab. */
.help-page .page-header {
    max-width: 900px;
    margin: 0 auto 20px;
    width: 100%;
}
.help-page .help-cards {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: 14px;
    max-width: 900px;
    margin: 0 auto;
}
.help-card {
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: 5px;
    padding: 18px;
    display: flex;
    flex-direction: column;
    gap: 12px;
    color: inherit;
    text-decoration: none;
    transition: border-color 0.15s, transform 0.15s, box-shadow 0.15s;
}
.help-card:hover {
    border-color: var(--ink-4);
    transform: translateY(-1px);
    box-shadow: var(--shadow-wol);
    text-decoration: none;
}
.help-card-icon {
    width: 36px;
    height: 36px;
    border-radius: 6px;
    background: var(--paper-2);
    color: var(--status-blue);
    display: flex;
    align-items: center;
    justify-content: center;
}
.help-card-icon svg { width: 20px; height: 20px; }
.help-card-body { flex: 1; }
.help-card-body h3 {
    font-size: 16px;
    font-weight: 600;
    letter-spacing: -0.015em;
    line-height: 1.25;
    color: var(--ink);
    margin: 0 0 6px;
}
.help-card-body p {
    font-size: 13.5px;
    line-height: 1.55;
    color: var(--ink-3);
    margin: 0;
}
.help-card-cta {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--status-blue);
}
.help-card-cta svg { width: 14px; height: 14px; }
.help-page .help-contact {
    max-width: 900px;
    margin: 28px auto 0;
    font-size: 13px;
    color: var(--ink-3);
}
/* Unauthenticated (no sidebar) — give the bare page some breathing
   room so it doesn't sit hard against the top of the viewport. */
body > .help-page {
    max-width: 960px;
    padding: 40px 24px 60px;
}

/* ============================================================
   WATCHLIST BUTTON (inline in table)
   ============================================================ */.watchlist-btn.on {
    color: var(--status-blue);
}

/* ============================================================
   BUYER CODE
   ============================================================ */

/* Flash on update — brand color fade */
@keyframes row-flash {
    0%   { background-color: rgba(38, 53, 72, 0.12); }
    100% { background-color: transparent; }
}

.row-updated {
    animation: row-flash 1.5s ease-out;
}

/* ============================================================
   LOT CARD — div-based two-row lot display
   ============================================================ */

.lot-list {
    max-width: 900px;
    margin: 0 auto;
}

/*  LotNo  Bales  Clean  Y-Z  S/D  AFLW  VM  |gap|  Type  Desc  Price/Status  */
.lot-grid {
    display: grid;
    grid-template-columns:
        /* Wider LotNo track so the watch star + note flag + lot number sit on
           one line (was 4rem — both flags pushed the number to a 2nd line),
           and so row 2's farm brand + NZFAP badge fit under them. That last
           1.5rem was taken back off the numeric block (Bales 3.5→3, the four
           test columns 3→2.75) rather than off the Description — every one of
           those tracks still runs well clear of its 4–5 char values. */
        6.25rem 3rem 3rem 2.75rem 2.75rem 2.75rem 2.75rem
        0.5rem
        /* Type is 6.25rem so Description opens on the row-2 cluster's "T"
           sub-column: the cluster lays six 2rem cells on a 2.25rem pitch
           from the Type edge, T sits 6.75rem in, less the 0.5rem grid gap.
           Wool types rarely run past that line. The 2.25rem this frees
           (vs opening on "M") goes to Price/Status at 10.25rem, so an
           "XXX OFFER $00.00" price or the inline bid widget (AUTO/OFFER
           label + field) stays on one line without narrowing Description. */
        6.25rem minmax(0, 1fr)
        10.25rem;
    gap: 0 var(--space-sm);
    align-items: baseline;
}

/* The gap column is just a spacer */
.lot-grid > .col-gap { }

/* The packed MM/CV/NKT/T/M/B test cluster spans the (empty) Type
   column plus the Description column so the monospace 6-value string
   fits on one line instead of wrapping or shoving the Status column
   off the right edge. white-space:nowrap keeps it single-line; the
   minmax(0,1fr) above lets the Description track shrink rather than
   widen the whole row. Shared by the header, the lot rows and the
   live hero so all three stay column-aligned. */
.lot-grid .lot-cluster {
    /* The MM/CV/NKT/T/M/B cluster spans the (empty) Type + Description
       columns and lays its six values out as a real 6-column sub-grid, so
       each value gets its own evenly-spaced cell (matching the mobile
       per-cell layout) instead of one cramped packed string. */
    grid-column: span 2;
    display: grid;
    /* FIXED, START-packed sub-cells: MM opens where the Type column starts
       and the six columns keep a constant width at every window size (a
       minmax here made them breathe with the flexible Description track
       while every other value column stayed put). 2rem comfortably fits
       the nbsp-padded 4-char "XX.X" values. */
    grid-template-columns: repeat(6, 2rem);
    justify-content: start;
    /* Tighter than the grid's own 0.5rem: the resulting 2.25rem pitch is what
       lets Description line up with the "T" sub-column (see the track comment
       above). */
    gap: 0 0.25rem;
    align-items: baseline;
}
/* Table: header labels + row values LEFT-aligned within each sub-cell —
   the values are nbsp-padded to the 4-char "XX.X" shape server-side, so
   columns line up and MM opens exactly at the Type column's start. */
.lot-header .lot-cluster > span,
.lot-card-row2 .lot-cluster > span {
    text-align: left;
}

/* Type / description read as words rather than figures, so ease their
   tracking from the dense numeric -1px back to a gentler -0.5px. Covers the
   desktop lot rows, the desktop live hero, and the mobile cards/header.
   Higher-specificity than the per-row/-cell -1px so it overrides cleanly. */
.lot-card-row1 > span:not([class]),
.live-hero-info-desktop .c:not(.num):not(.lot-cluster) .v,
.m-lot-row .c.type-col,
.m-lot-row .c.desc-col {
    letter-spacing: -0.5px;
}

.lot-header {
    padding: var(--space-xs) var(--space-sm);
    padding-left: var(--space-lg);
    background: linear-gradient(135deg, var(--ink) 0%, var(--ink-2) 100%);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: 600;
    color: rgba(255, 255, 255, 0.9);
    text-transform: uppercase;
    letter-spacing: -1px;
}

.lot-header:first-child {
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    padding-top: var(--space-sm);
    /* Overlap the row below by 1px (padding gives the space back, so the
       rows look unchanged). The two rows are separate boxes, so at a
       fractional layout position — which is exactly what a sticky header
       parked under the top chrome sits at — the sub-pixel seam between them
       lets the page background through as a white hairline. */
    padding-bottom: calc(var(--space-xs) + 1px);
    margin-bottom: -1px;
}

.lot-header:nth-child(2) {
    padding-bottom: var(--space-sm);
    font-weight: 500;
    color: rgba(255, 255, 255, 0.55);
    text-transform: none;
    letter-spacing: -1px;
    font-size: var(--text-xs);
}

.lot-header > span.num {
    text-align: right;
}

.lot-header .status {
    text-align: right;
    white-space: nowrap;
}

.lot-card {
    position: relative;
    border: 1px solid var(--gray-200);
    border-radius: 3px;
    background: #FFFFFF;
    margin-top: 4px;
}

.lot-card-row1 {
    padding: var(--space-sm) var(--space-sm);
    padding-left: var(--space-lg);
    padding-bottom: 2px;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: -1px;
    color: var(--gray-900);
}

.lot-card-row2 {
    padding: 0 var(--space-sm) var(--space-sm);
    padding-left: var(--space-lg);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: -1px;
    color: var(--gray-400);
}

.lot-card .lot-no {
    font-weight: 600;
    font-size: 14px;
    font-family: var(--font-mono);
    letter-spacing: -1px;
    color: var(--gray-900);
    /* Keep the star + note flag + number on one line even at the rare 8-char
       fallback lot number; the wider track above gives them room. */
    white-space: nowrap;
}

.lot-card .lot-no .test-lab {
    font-weight: 400;
    font-size: 0.6875rem;
    color: var(--gray-400);
}

/* NZFAP badge — small subscript next to / under the lot number on
   every surface (desktop list, mobile card, live hero). Matches
   legacy `lots-tbody.html:14`: thin weight, ~80% font-size, muted
   colour so it reads as a tag, not data. On the desktop lot list
   it lives in the secondary row's first column, sitting on the
   same baseline as Test / Net Wgt / etc. */
.nzfap {
    font-family: var(--font-sans);
    font-weight: 500;
    font-size: 80%;
    color: var(--gray-500);
    letter-spacing: 0.04em;
}
.lot-card-row2 .nzfap {
    color: var(--gray-500);
    font-family: var(--font-mono);
    letter-spacing: -1px;
    font-size: var(--text-xs);
    font-weight: 500;
}

/* Row-2 first column: the farm brand ([Lots]Notes — the brand painted on the
   bales) directly under the lot number, with the NZFAP badge beside it. The
   assurance travels with the farm, so the two read as one unit. The brand
   ellipsises (full text on the title attribute) rather than pushing the badge
   out of the fixed-width track. */
.lot-card-row2 .lot-brand {
    display: flex;
    align-items: baseline;
    gap: 4px;
    min-width: 0;
}
.lot-card-row2 .lot-brand .brand {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.lot-card-row2 .lot-brand .nzfap {
    flex: none;
}
.live-hero .nzfap {
    color: rgba(250, 246, 238, 0.55);
}

.lot-card .num {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.lot-card .price {
    text-align: right;
    font-weight: 700;
    font-size: 14px;
    font-variant-numeric: tabular-nums;
    font-family: var(--font-mono);
    letter-spacing: -1px;
}

.lot-card .status {
    font-family: var(--font-sans);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    text-align: right;
    white-space: nowrap;
}

.lot-card .row-number {
    left: -3rem;
    font-size: var(--text-xs);
    line-height: inherit;
    color: var(--gray-600);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    text-align: right;
    width: 2rem;
    font-family: var(--font-mono);
    letter-spacing: -1px;
    opacity: 0.85;
}

/* ============================================================
   IN-ROW WATCHED FLAG
   A small live-blue star just left of the lot number on watched lots —
   display only; toggling lives in the lot panel's watch button, which
   paints these optimistically by data-lot (ws diffs don't re-render
   rows on overlay-only changes). Always in the DOM, hidden unless
   .watching, so the toggle has something to paint.
   ============================================================ */
.lot-watch-flag { display: none; }
.lot-watch-flag.watching {
    display: inline-block;
    /* Hang into the cell's left edge so the star reads as attached to the
       lot number without pushing it too far right. */
    margin-left: -10px;
    margin-right: 3px;
}
.lot-watch-flag svg {
    width: 16px;
    height: 16px;
    fill: var(--live);
    vertical-align: -3px;
}

/* In-row "has a note" flag — sits just right of the watch star, before the lot
   number, shown only when the buyer has a note on the lot. Same live-blue as the
   watch star, sized just under it (15px) so the two align and read as one
   buyer-context cluster. Painted optimistically by data-lot on note save and
   over the ws diff. */
.lot-note-flag { display: none; }
.lot-note-flag.has-note {
    display: inline-block;
    margin-right: 3px;
}
.lot-note-flag svg {
    width: 15px;
    height: 15px;
    color: var(--live);
    vertical-align: -3px;
}

/* In-row "has a sample photo" flag — after the watch/note pair, before the
   lot number. Shared lot data, and VERIFIED: the NZWTA resolution is cached
   server-side, and the camera appears via the ws diff once the background
   check lands (so it's rendered only when it applies — no hidden placeholder,
   no data-lot repaint hook). Muted rather than live-blue — the blue pair means
   "your stuff" (watched / noted), and once photos flow for most lots a blue
   camera on every row would drown that signal. */
.lot-photo-flag {
    display: inline-block;
    margin-right: 3px;
}
.lot-photo-flag svg {
    width: 15px;
    height: 15px;
    color: var(--ink-4);
    vertical-align: -3px;
}

/* In-row padlock on a lot guests can't see, for the sale's admins and brokers.
   Darker than the camera so a private lot isn't missed; not a status colour. */
.lot-private-flag {
    display: inline-block;
    margin-right: 3px;
}
.lot-private-flag svg {
    width: 14px;
    height: 14px;
    color: var(--ink-2);
    vertical-align: -2px;
}
.live-hero-lot-no .v .lot-private-flag { margin-right: 7px; }
.live-hero-lot-no .v .lot-private-flag svg {
    width: 20px;
    height: 20px;
}

/* ============================================================
   LOT-ACTIONS PANEL (accordion)
   Lazy fragment inserted as a SIBLING directly beneath the tapped lot row
   (desktop + mobile lists) and expanded with a grid-rows transition — the
   sibling-with-own-id shape is what lets the ws diffs swap/append/delete
   row elements without touching an open panel. No header: the lot number
   and specs are the row it hangs off.
   ============================================================ */
.lot-panel-wrap {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows 0.15s ease;
}
.lot-panel-wrap.open { grid-template-rows: 1fr; }
.lot-panel-inner {
    overflow: hidden;
    min-height: 0;
}
/* The open panel hangs directly off its row as one piece (like a sale card on
   its lots table): no gap, square corners where the two meet, the row's
   gray-200 border and 3px outer radius carried round the panel. The row's
   squared corners live with the stacked-sections rules further down (they
   must follow the status-accent rules to win). */
.lot-panel {
    margin: 0 0 10px;
    padding: 12px 14px;
    background: var(--paper-light);
    border: 1px solid var(--gray-200);
    border-top: none;
    border-radius: 0 0 3px 3px;
    color: var(--ink);
    font-family: var(--sans);
    cursor: default;
}
/* The mobile table spaces its children with a 3px flex gap — pull the panel
   back up onto its entry. */
.m-lot-table > .lot-panel-wrap {
    margin-top: -3px;
}
.lot-panel-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 12px;
}
/* The watch toggle's standalone bottom margin is the strip's job here. */
.lot-panel-top .lot-dialog-watch { margin-bottom: 0; }
.lot-panel-error-title {
    font-size: 13px;
    font-weight: 700;
    color: var(--ink-2);
}
.lot-panel-close {
    background: none;
    border: none;
    color: var(--ink-4);
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
    padding: 0 4px;
    align-self: flex-start;
}
.lot-panel-close:hover { color: var(--ink); }
/* Tighter rhythm than the old modal — the panel sits inside the list flow. */
.lot-panel .lot-dialog-panel { margin: 0 0 12px; padding: 12px; }

/* Full-width admin tender offers sit above the notes/photo grid. */
.lot-offers-scroll { overflow-x: auto; }
.lot-offers-table { width: 100%; min-width: 590px; border-collapse: collapse; font-size: 13px; }
.lot-offers-table th { color: var(--ink-4); font-size: 11px; font-weight: 600; }
.lot-offers-table th,
.lot-offers-table td { padding: 7px 10px; text-align: left; border-bottom: 1px solid var(--gray-200); }
.lot-offers-table th:first-child,
.lot-offers-table td:first-child { padding-left: 0; }
.lot-offers-table th:last-child,
.lot-offers-table td:last-child { text-align: right; padding-right: 0; }
.lot-offers-date { white-space: nowrap; font-variant-numeric: tabular-nums; }
.lot-offers-table tbody tr:last-child td { border-bottom: 0; }
.lot-offers-amount { font-family: var(--font-mono); font-size: 14px; font-weight: 700; font-variant-numeric: tabular-nums; }
.lot-offers-count,
.lot-offers-empty,
.lot-offers-refresh-status { font-size: 12px; color: var(--ink-4); }
.lot-offers-refresh-status { margin-left: auto; }

/* Panel body: desktop splits notes (left) and the NZWTA sample photo (right)
   into even halves; mobile stacks them in DOM order (notes first). The
   :only-child rule keeps a lone notes editor full-width, which is what the
   /preview gallery's static panel markup relies on. */
.lot-panel-body {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
    /* Stretch (not start) so the notes editor and the photo beside it always
       come out the same height — the photo crops to whatever the row is. */
    align-items: stretch;
}
.lot-panel-body > .lot-dialog-panel {
    grid-column: 2;
    grid-row: 1;
    margin: 0;
    height: 100%;
    box-sizing: border-box;
}
/* The notes editor is itself a .lot-dialog-panel (same shell, so the two
   boxes sit flush) — this later rule re-homes it to the left column. */
.lot-panel-body > .lot-dialog-notes { grid-column: 1; grid-row: 1; margin: 0; }
.lot-panel-body > .lot-dialog-notes:only-child { grid-column: 1 / -1; }
@media (max-width: 875px) {
    .lot-panel-body { grid-template-columns: 1fr; }
    .lot-panel-body > .lot-dialog-panel { grid-column: 1; grid-row: auto; }
    .lot-panel-body > .lot-dialog-notes { grid-column: 1; grid-row: auto; }
}
/* Desktop only: the photo beside it sets a taller row, so grow the textarea
   into the spare height rather than leaving a gap under it. `flex: 1 0 auto`
   grows but never shrinks, so the box can't end up smaller than its rows="3"
   size. Mobile keeps the natural height — there's no row to fill. */
@media (min-width: 876px) {
    .lot-panel-body > .lot-dialog-notes {
        display: flex;
        flex-direction: column;
    }
    .lot-panel-body > .lot-dialog-notes .lot-dialog-panel-head { flex: 0 0 auto; }
    .lot-panel-body > .lot-dialog-notes .lot-dialog-notes-form {
        flex: 1 1 auto;
        display: flex;
        flex-direction: column;
        min-height: 0;
    }
    .lot-panel-body > .lot-dialog-notes textarea { flex: 1 0 auto; }
}

/* NZWTA grab-sample photo. The proxy 404s when there's no photo (or NZWTA is
   unreachable), so the img's inline onload/onerror flag the wrapper and these
   rules swap spinner → photo → message. The server renders the wrapper
   pre-`.failed`, with no img at all, when the lot has no cert pair. */
.lot-dialog-sample {
    display: flex;
    flex-direction: column;
}
.lot-dialog-sample .lot-dialog-panel-head { flex: 0 0 auto; }
/* Fills the rest of the panel so the crop matches the notes height on desktop;
   the min-height is the floor for mobile, where the row imposes nothing. */
.lot-sample-shot {
    position: relative;
    flex: 1 1 auto;
    min-height: 150px;
    border-radius: 8px;
    overflow: hidden;
    /* A faint wash so the loading crop area still reads on the white panel. */
    background: var(--paper-light);
}
.lot-sample-shot img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    /* Fill the slot both ways — a slight top/bottom crop of the 4:3 sample,
       never letterboxing. Full framing is one click away in the viewer. */
    object-fit: cover;
    cursor: zoom-in;
    opacity: 0;
    transition: opacity 0.2s ease;
}
.lot-sample-shot.loaded img { opacity: 1; }
.lot-sample-shot.failed img { display: none; }
/* Same spinner idiom as .btn-spinner, centred over the empty crop area. */
.lot-sample-shot::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 16px;
    height: 16px;
    margin: -8px 0 0 -8px;
    border-radius: 50%;
    border: 2px solid var(--ink-5);
    border-top-color: transparent;
    animation: btn-spin 0.7s linear infinite;
}
.lot-sample-shot.loaded::after,
.lot-sample-shot.failed::after { content: none; }
.lot-sample-none {
    display: none;
    position: absolute;
    inset: 0;
    margin: 0;
    padding: 12px;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    text-align: center;
    font-size: 12px;
    color: var(--ink-4);
}
.lot-sample-none svg {
    width: 28px;
    height: 28px;
    opacity: 0.55;
}
.lot-sample-shot.failed .lot-sample-none { display: flex; }
/* No-image state: the outer panel card stays (same shell as the notes box),
   but the inner white crop-area rectangle drops away so the slashed-frame
   icon + message sit subtly on the panel's own background. */
.lot-sample-shot.failed { background: none; }

/* ============================================================
   FULL-SIZE SAMPLE-PHOTO VIEWER
   Built by lot-image-viewer.js on a thumbnail tap and appended to <body>, so it
   escapes the lot panel's overflow. Transform-based pan/zoom lives on the img;
   everything here is layout + the scrim.
   ============================================================ */
body.lot-image-viewer-open { overflow: hidden; }
.lot-image-viewer {
    position: fixed;
    inset: 0;
    z-index: 200;
    background: rgba(10, 14, 22, 0.9);
    /* The stage handles every gesture itself — no browser scroll/zoom. */
    touch-action: none;
    overscroll-behavior: contain;
}
/* A real scroll container, not a transform surface: scrollbars, keyboard
   scrolling and one-finger touch panning all come free, and the zoom maths
   reduces to sizing the img and setting scrollLeft/Top. */
.lot-image-viewer-stage {
    position: absolute;
    /* Only the top bar keeps reserved space — the title and zoom cluster
       float OVER the photo (inside the stage rect), so the image runs
       nearly to the bottom of the screen. */
    inset: 52px 16px 16px;
    overflow: auto;
    overscroll-behavior: contain;
    /* Pans natively; the two-finger pinch is handled in JS. */
    touch-action: pan-x pan-y;
    scrollbar-color: rgba(255, 255, 255, 0.32) transparent;
    scrollbar-width: thin;
}
.lot-image-viewer-stage::-webkit-scrollbar { width: 11px; height: 11px; }
.lot-image-viewer-stage::-webkit-scrollbar-track,
.lot-image-viewer-stage::-webkit-scrollbar-corner { background: transparent; }
.lot-image-viewer-stage::-webkit-scrollbar-thumb {
    background: rgba(255, 255, 255, 0.32);
    background-clip: content-box;
    border: 3px solid transparent;
    border-radius: 999px;
}
.lot-image-viewer-stage::-webkit-scrollbar-thumb:hover {
    background: rgba(255, 255, 255, 0.55);
    background-clip: content-box;
}
/* max-content over a 100% floor: the canvas is exactly max(stage, image), so a
   zoomed-in photo scrolls without the flex centring clipping its top-left. */
.lot-image-viewer-canvas {
    width: max-content;
    min-width: 100%;
    height: max-content;
    min-height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
}
.lot-image-viewer-img {
    display: block;
    max-width: none;
    max-height: none;
    cursor: grab;
    opacity: 0;
    transition: opacity 0.2s ease;
    user-select: none;
    -webkit-user-drag: none;
}
.lot-image-viewer.loaded .lot-image-viewer-img { opacity: 1; }
.lot-image-viewer.dragging .lot-image-viewer-img {
    cursor: grabbing;
    transition: none;
}
.lot-image-viewer-bar {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 12px;
    padding: 10px 14px;
    font-family: var(--sans);
}
/* Lot number, leading the top-right cluster. :empty keeps it out of the
   layout when the thumbnail carried no number. */
/* Floating two-line title — sale name over lot number, top-centre INSIDE the
   image area (10px below the stage's top edge, under the top bar), on a
   scrimmed pill so it stands out whatever the photo behind it. Decorative
   only (pointer-events: none): a click on it falls through to the backdrop
   like any other letterbox click. Width-capped so a long sale name stays a
   tidy pill on small screens. */
.lot-image-viewer-title {
    position: absolute;
    top: 62px;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1px;
    max-width: 55vw;
    padding: 6px 16px;
    border: 1px solid rgba(255, 255, 255, 0.28);
    border-radius: 10px;
    background: rgba(10, 14, 22, 0.72);
    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.45);
    font-family: var(--sans);
    text-align: center;
    pointer-events: none;
}
.lot-image-viewer-title-sale {
    max-width: 100%;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    font-size: 11px;
    font-weight: 600;
    color: rgba(255, 255, 255, 0.78);
}
.lot-image-viewer-title-lot {
    font-size: 15px;
    font-weight: 700;
    color: #fff;
}
.lot-image-viewer-title-sale:empty,
.lot-image-viewer-title-lot:empty { display: none; }
.lot-image-viewer-tab {
    font-size: 12px;
    font-weight: 600;
    text-decoration: none;
    color: rgba(255, 255, 255, 0.82);
    border: 1px solid rgba(255, 255, 255, 0.32);
    border-radius: 999px;
    padding: 5px 12px;
}
.lot-image-viewer-tab:hover { color: #fff; border-color: #fff; }
.lot-image-viewer-close {
    background: none;
    border: none;
    color: rgba(255, 255, 255, 0.82);
    font-size: 26px;
    line-height: 1;
    padding: 0 4px;
    cursor: pointer;
}
.lot-image-viewer-close:hover { color: #fff; }
.lot-image-viewer-spinner {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 26px;
    height: 26px;
    margin: -13px 0 0 -13px;
    border-radius: 50%;
    border: 2px solid rgba(255, 255, 255, 0.55);
    border-top-color: transparent;
    animation: btn-spin 0.7s linear infinite;
}
.lot-image-viewer.loaded .lot-image-viewer-spinner,
.lot-image-viewer.failed .lot-image-viewer-spinner { display: none; }
.lot-image-viewer-error {
    display: none;
    position: absolute;
    inset: 0;
    align-items: center;
    justify-content: center;
    margin: 0;
    padding: 24px;
    text-align: center;
    font-family: var(--sans);
    font-size: 13px;
    color: rgba(255, 255, 255, 0.8);
}
.lot-image-viewer.failed .lot-image-viewer-error { display: flex; }
/* Zoom cluster, floating bottom-centre over the stage. Hidden until the photo
   is in — there's nothing to zoom before that. */
/* Floats over the photo just inside the stage's bottom edge — 26px keeps it
   clear of the stage's horizontal scrollbar (stage bottom is 16px). */
.lot-image-viewer-zoom {
    display: none;
    position: absolute;
    left: 50%;
    bottom: 26px;
    transform: translateX(-50%);
    align-items: center;
    gap: 2px;
    padding: 4px;
    border-radius: 999px;
    background: rgba(20, 26, 38, 0.88);
    border: 1px solid rgba(255, 255, 255, 0.22);
    font-family: var(--sans);
}
.lot-image-viewer.loaded .lot-image-viewer-zoom { display: flex; }
.lot-image-viewer-zoom button {
    min-width: 30px;
    height: 28px;
    padding: 0 9px;
    background: none;
    border: none;
    border-radius: 999px;
    color: rgba(255, 255, 255, 0.85);
    font-family: inherit;
    font-size: 15px;
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
}
.lot-image-viewer-zoom button:hover { background: rgba(255, 255, 255, 0.16); color: #fff; }
.lot-image-viewer-preset { font-size: 12px; }
.lot-image-viewer-level {
    min-width: 54px;
    text-align: center;
    font-size: 12px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: rgba(255, 255, 255, 0.85);
}

/* Live-hero per-buyer context: read-only watched flag + private note for
   the lot on the block (editing lives in the lot lists' panels). */
.hero-user-meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px 12px;
    margin-top: 8px;
    min-width: 0;
}
/* Note chip — icon + text. A clear leading glyph plus text sized + coloured to
   match the hero's testing-detail values (.live-hero-info .c .v), so a note reads
   at the same weight as the lot data while bidding. Single-line with an ellipsis. */
.hero-user-meta .hero-note {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    min-width: 0;
    font-size: 15px;
    color: var(--ink-3);
}
.hero-user-meta .hero-note svg {
    width: 13px;
    height: 13px;
    flex: 0 0 auto;
    /* Same live-blue as the watch star (and the in-row note flag) — reads clearly
       on the navy hero just like the hero star does. */
    color: var(--live);
}
.hero-user-meta .hero-note .hero-note-text {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* The chip sits on the dark navy hero — the text takes the same paper ink the
   testing-detail values use, so the note reads as first-class lot context. */
.live-hero .hero-note { color: var(--paper); }
/* Active auto-bid pill + one-tap cancel, shown on the live hero for the lot
   the buyer has a standing auto-bid on. */
.hero-autobid {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.hero-autobid-pill {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 9px;
    border-radius: 999px;
    background: var(--win-bg);
    color: var(--win);
    border: 1px solid var(--win-border);
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.02em;
    white-space: nowrap;
}
.hero-autobid-pill svg {
    width: 12px;
    height: 12px;
    fill: currentColor;
}
.hero-autobid-cancel {
    appearance: none;
    border: 1px solid var(--line);
    background: var(--card);
    color: var(--ink-3);
    font-size: 11px;
    font-weight: 600;
    padding: 2px 9px;
    border-radius: 6px;
    cursor: pointer;
}
.hero-autobid-cancel:hover {
    color: var(--lose);
    border-color: var(--lose-border);
}
.hero-autobid-cancel:disabled {
    opacity: 0.5;
    cursor: default;
}
/* Watched flag beside the hero's centred lot number — same in-row star as
   the lot lists, sized up to sit with the large lot-number type. */
.live-hero-lot-no .v .lot-watch-flag.watching {
    margin-left: 0;
    margin-right: 7px;
}
.live-hero-lot-no .v .lot-watch-flag svg {
    width: 20px;
    height: 20px;
    vertical-align: -2px;
}
.lot-dialog-guest { font-size: 13px; color: var(--ink-3); line-height: 1.5; }

/* In-dialog watchlist toggle — mirrors the gutter star (live-blue when on). */
.lot-dialog-watch {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 8px 14px;
    margin-bottom: 18px;
    border: 1px solid var(--gray-200);
    border-radius: 8px;
    background: var(--card);
    color: var(--ink-2);
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.lot-dialog-watch svg {
    width: 16px;
    height: 16px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
    stroke-linejoin: round;
}
.lot-dialog-watch:hover { border-color: var(--live); color: var(--live); }
.lot-dialog-watch.watching { background: var(--live); border-color: var(--live); color: #fff; }
.lot-dialog-watch.watching svg { fill: #fff; stroke: #fff; }

/* Notes editor (shares the .lot-dialog-panel shell) */
.lot-dialog-notes textarea {
    width: 100%;
    box-sizing: border-box;
    border: 1px solid var(--gray-200);
    border-radius: 8px;
    padding: 10px 12px;
    background: var(--card);
    font-family: var(--sans);
    font-size: 14px;
    line-height: 1.5;
    color: var(--ink);
    resize: none;
}
.lot-dialog-notes textarea::placeholder { color: var(--ink-5); }
.lot-dialog-notes textarea:focus {
    outline: none;
    border-color: var(--ink);
    box-shadow: 0 0 0 3px rgba(22, 32, 46, 0.08);
}
/* Notes auto-save status — sits at the head strip's right edge, opposite
   the "Your notes" title. Spinner while a debounce/save is pending, then
   "Saved" (or an error) once the request settles. */
.lot-dialog-note-status {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: 5px;
    min-height: 12px;
    font-size: 11px;
    font-weight: 600;
    color: var(--ink-4);
}
.lot-dialog-note-status.saved { color: var(--win); }
.lot-dialog-note-status.error { color: var(--lose); }
.lot-dialog-note-status.pending::before {
    content: '';
    width: 10px;
    height: 10px;
    border-radius: 50%;
    border: 2px solid rgba(22, 32, 46, 0.35);
    border-top-color: transparent;
    animation: btn-spin 0.7s linear infinite;
}

/* ── Per-row note strip ─────────────────────────────────────────────────
   A slim row under each lot — its watch star, its note, the save status —
   for buyers who want to read their notes without opening every lot. Off by
   default: the markup always ships (it rides the ws row renders), and the
   .wolx-notes-on class on <html> is the only thing that reveals it, so
   toggling costs no re-render.

   It is a SIBLING of the row, never a child: row swaps replace the row whole,
   and a note being typed would go with it. */
.lot-note-strip { display: none; }
:root.wolx-notes-on .lot-note-strip {
    display: flex;
    align-items: center;
    gap: 8px;
    max-width: 900px;
    margin: 0 auto;
    padding: 5px 10px 6px 14px;
    border: 1px solid var(--gray-200);
    border-top: none;
    /* Paper-light, not card white — the field inside is the white thing, so
       the strip reads as the row's own margin rather than another row. */
    background: var(--paper-light);
    border-radius: 0 0 3px 3px;
    cursor: default;
}
/* An open detail panel already has the full notes editor in it, so the strip
   for that lot stands down rather than showing the same note twice. The panel
   is inserted directly after its row, ahead of the strip. */
:root.wolx-notes-on .lot-panel-wrap + .lot-note-strip { display: none; }

/* With the strip showing, the row's flags say what the strip says an inch
   below, so all three stand down — watch star, note glyph and camera. The
   strip carries each of them as a control rather than a marker, which is
   strictly more than the flags were telling you. */
:root.wolx-notes-on .lot-card :is(.lot-watch-flag, .lot-note-flag, .lot-photo-flag),
:root.wolx-notes-on .m-lot-entry :is(.lot-watch-flag, .lot-note-flag, .lot-photo-flag) {
    display: none;
}

/* Watch star — the lot panel's watch button, compressed to a row: bordered,
   outlined star when the lot isn't watched, filled on live-blue when it is.
   The wording is the panel's job; here the star carries it alone. */
.lot-note-star {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 24px;
    padding: 0;
    border: 1px solid var(--gray-200);
    border-radius: 6px;
    background: var(--card);
    color: var(--ink-2);
    cursor: pointer;
    transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.lot-note-star svg {
    width: 13px;
    height: 13px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
    stroke-linejoin: round;
}
.lot-note-star:hover { border-color: var(--live); color: var(--live); }
.lot-note-star.watching { background: var(--live); border-color: var(--live); color: #fff; }
.lot-note-star.watching svg { fill: #fff; stroke: #fff; }
.lot-note-star:focus-visible,
.lot-note-photo:focus-visible {
    outline: 2px solid var(--ink);
    outline-offset: 1px;
}

/* Sample photo — same button shape as the star beside it. Live when NZWTA
   has a photo (opens the same pan/zoom viewer the lot panel's thumbnail
   does); disabled and wearing the broken-image glyph when it hasn't, so the
   strip answers "is there a photo?" without anyone having to open the lot. */
.lot-note-photo {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 24px;
    padding: 0;
    border: 1px solid var(--gray-200);
    border-radius: 6px;
    background: var(--card);
    color: var(--ink-2);
    cursor: pointer;
    transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.lot-note-photo svg { width: 13px; height: 13px; }
.lot-note-photo:hover { border-color: var(--live); color: var(--live); }
.lot-note-photo.none {
    cursor: default;
    color: var(--ink-5);
    opacity: 0.55;
}
.lot-note-photo.none:hover {
    border-color: var(--gray-200);
    color: var(--ink-5);
}

.lot-note-strip-form {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    min-width: 0;
}
/* The field carries its own save status inside its right edge, so a list of
   strips keeps one clean right margin instead of a ragged status column. */
.lot-note-strip-field {
    position: relative;
    flex: 1 1 auto;
    min-width: 0;
    display: block;
}
/* Same treatment as the panel's notes textarea, sized for one line: a real
   bordered field on pure white, so the two editors read as one control. */
.lot-note-strip-input {
    width: 100%;
    box-sizing: border-box;
    padding: 4px 66px 4px 10px;
    border: 1px solid var(--gray-200);
    border-radius: 6px;
    background: var(--card);
    font-family: var(--sans);
    font-size: 13px;
    line-height: 1.4;
    color: var(--ink);
}
.lot-note-strip-input::placeholder { color: var(--ink-5); }
.lot-note-strip-input:focus {
    outline: none;
    border-color: var(--ink);
    box-shadow: 0 0 0 3px rgba(22, 32, 46, 0.08);
}
.lot-note-strip .lot-dialog-note-status {
    position: absolute;
    right: 9px;
    top: 50%;
    transform: translateY(-50%);
    margin-left: 0;
    min-height: 0;
    font-size: 10px;
    pointer-events: none;
}

/* ── Rows and strips as one run ─────────────────────────────────────────
   A strip sits between consecutive rows, which breaks the `.lot-card +
   .lot-card` adjacency the tight-table rules rely on. These restore it
   THROUGH the strip, so with notes off the table looks exactly as it did. */
.lot-note-strip + .lot-card,
.lot-note-strip + .m-lot-entry {
    margin-top: 0;
    border-top: none;
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}
.lot-card:has(+ .lot-note-strip + .lot-card),
.m-lot-entry:has(+ .lot-note-strip + .m-lot-entry) {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}
/* The mobile table is a flex column with a 3px gap — pull each run member
   back onto the one above, strip included. */
.lot-note-strip + .m-lot-entry,
.m-lot-entry + .lot-note-strip {
    margin-top: -3px;
}

/* With notes ON, each row and its strip become one block, and blocks are
   held apart by the same 4px a plain row run used before the strips existed. */
:root.wolx-notes-on .lot-card:has(+ .lot-note-strip),
:root.wolx-notes-on .m-lot-entry:has(+ .lot-note-strip) {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}
:root.wolx-notes-on .lot-note-strip + .lot-card {
    margin-top: 4px;
    border-top: 1px solid var(--gray-200);
    border-top-left-radius: 3px;
    border-top-right-radius: 3px;
}
:root.wolx-notes-on .lot-note-strip + .m-lot-entry {
    /* 3px flex gap already sits here; add 1px to land on 4px. */
    margin-top: 1px;
    border-top: 1px solid var(--gray-200);
    border-top-left-radius: 3px;
    border-top-right-radius: 3px;
}
/* A strip whose row is hidden behind an open panel leaves the run joined. */
:root.wolx-notes-on .lot-panel-wrap + .lot-note-strip + .lot-card {
    margin-top: 0;
}

/* ── Edit lots strip ─────────────────────────────────────────────────────
   The sale's own admins get a second strip after the note strip: the lot's
   type, description and reserve as inputs. Hidden until the Sale admin
   panel's "Edit lots" puts the page in edit mode, then drawn like the note
   strip but in the admin panel's warm colours, so it reads as an admin tool. */
.lot-edit-strip { display: none; }
body.lot-edit-mode .lot-edit-strip {
    display: flex;
    align-items: center;
    max-width: 900px;
    margin: 0 auto;
    padding: 6px 10px 7px 14px;
    border: 1px solid #FED7AA;
    border-top: none;
    background: #FFF7ED;
    border-radius: 0 0 3px 3px;
    cursor: default;
}
.lot-edit-form {
    flex: 1 1 auto;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 12px;
    min-width: 0;
}
.lot-edit-field { display: flex; align-items: center; gap: 6px; min-width: 0; }
.lot-edit-field.type { flex: 0 1 150px; }
.lot-edit-field.description { flex: 1 1 220px; }
.lot-edit-field.reserve { flex: 0 0 auto; }
.lot-edit-label {
    font-size: 11px;
    font-weight: 600;
    color: #9A3412;
    white-space: nowrap;
}
.lot-edit-input {
    width: 100%;
    min-width: 0;
    box-sizing: border-box;
    padding: 4px 8px;
    border: 1px solid #E8A080;
    border-radius: 6px;
    background: #FFF;
    font-family: var(--sans);
    font-size: 13px;
    line-height: 1.4;
    color: var(--ink);
}
.lot-edit-input:focus { outline: none; border-color: #C2410C; box-shadow: 0 0 0 3px rgba(194, 65, 12, 0.10); }
.lot-edit-money { display: inline-flex; align-items: center; gap: 3px; }
.lot-edit-currency { font-size: 13px; color: var(--ink-3); }
.lot-edit-field.reserve .lot-edit-input { width: 84px; text-align: right; font-variant-numeric: tabular-nums; }
.lot-edit-status { min-width: 48px; font-size: 11px; font-weight: 600; color: var(--ink-4); }
.lot-edit-status.dirty { color: oklch(52% 0.13 70); }
.lot-edit-status.saved { color: var(--win); }
.lot-edit-status.error { color: var(--lose); }
.lot-edit-locked { font-size: 12px; color: var(--ink-4); }
/* Desktop: the row's own padding and .lot-grid tracks, so each field sits
   under the value it edits — Type and Description under theirs, the reserve
   in the Price column — with the save status under the numbers. The inputs
   take the row's monospace type, and are pulled out by their padding and
   border so the text inside starts exactly where the row's text does. The
   column headers name Type and Description; only Reserve needs its label. */
body.lot-edit-mode .lot-list .lot-edit-strip { padding: 6px var(--space-sm) 7px var(--space-lg); }
.lot-edit-form.lot-grid { display: grid; align-items: center; gap: 0 var(--space-sm); }
.lot-edit-form.lot-grid .lot-edit-status { grid-column: 1 / 9; }
.lot-edit-form.lot-grid .lot-edit-field.type { grid-column: 9; }
.lot-edit-form.lot-grid .lot-edit-field.description { grid-column: 10; }
.lot-edit-form.lot-grid .lot-edit-field.reserve { grid-column: 11; justify-content: flex-end; }
.lot-edit-form.lot-grid :is(.type, .description) .lot-edit-label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
}
.lot-edit-form.lot-grid .lot-edit-input {
    padding: 3px 4px;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: -0.5px;
}
.lot-edit-form.lot-grid :is(.type, .description) .lot-edit-input {
    width: calc(100% + 5px);
    margin-left: -5px;
}
.lot-edit-form.lot-grid .reserve .lot-edit-input { width: 4.5rem; margin-right: -5px; }
/* The mobile strip wraps, with the status after the fields. */
.lot-edit-form:not(.lot-grid) .lot-edit-status { order: 9; }
/* The Edit lots switch: the notes switch's pencil twin, one header row down
   in the same gutter; the admin panel's orange when on. */
.lot-header:nth-child(2) { position: relative; }
.m-lot-header .lot-edit-mode-toggle { top: 39px; }
.lot-edit-mode-toggle svg { width: 17px; height: 17px; margin-right: -4px; }
.lot-edit-mode-toggle.on { color: #C2410C; opacity: 1; }

/* The edit strip joins the row run the same way the note strip does,
   whether or not a (hidden or shown) note strip sits between. */
.lot-edit-strip + .lot-card,
.lot-edit-strip + .m-lot-entry {
    margin-top: 0;
    border-top: none;
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}
.lot-card:has(+ .lot-note-strip + .lot-edit-strip + .lot-card),
.m-lot-entry:has(+ .lot-note-strip + .lot-edit-strip + .m-lot-entry),
.lot-card:has(+ .lot-edit-strip + .lot-card),
.m-lot-entry:has(+ .lot-edit-strip + .m-lot-entry) {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}
/* The mobile table's 3px flex gap, pulled back as for the note strip. */
.lot-edit-strip + .m-lot-entry,
.m-lot-entry + .lot-edit-strip,
.m-lot-list .lot-note-strip + .lot-edit-strip { margin-top: -3px; }
/* In edit mode each row with its strips is one block, 4px from the next. */
body.lot-edit-mode .lot-card:has(+ .lot-edit-strip),
body.lot-edit-mode .lot-card:has(+ .lot-note-strip + .lot-edit-strip),
body.lot-edit-mode .m-lot-entry:has(+ .lot-edit-strip),
body.lot-edit-mode .m-lot-entry:has(+ .lot-note-strip + .lot-edit-strip) {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}
body.lot-edit-mode .lot-edit-strip + .lot-card {
    margin-top: 4px;
    border-top: 1px solid var(--gray-200);
    border-top-left-radius: 3px;
    border-top-right-radius: 3px;
}
body.lot-edit-mode .lot-edit-strip + .m-lot-entry {
    margin-top: 1px;
    border-top: 1px solid var(--gray-200);
    border-top-left-radius: 3px;
    border-top-right-radius: 3px;
}
:root.wolx-notes-on .lot-note-strip + .lot-edit-strip + .lot-card {
    margin-top: 4px;
    border-top: 1px solid var(--gray-200);
    border-top-left-radius: 3px;
    border-top-right-radius: 3px;
}
/* With notes on and edit mode off, the hidden edit strip mustn't undo the
   gap the note strip rule above puts before the next row. */
:root.wolx-notes-on .lot-note-strip + .lot-edit-strip + .m-lot-entry {
    margin-top: 1px;
    border-top: 1px solid var(--gray-200);
    border-top-left-radius: 3px;
    border-top-right-radius: 3px;
}
@media (max-width: 640px) {
    .lot-edit-field.type, .lot-edit-field.description { flex: 1 1 100%; }
}

/* The switch: a quiet glyph in the left gutter beside the column headers,
   sitting in the same column as the rows' index numbers and level with the
   "Lot No" label. Outside the dark header block, so it reads as a control
   over the list rather than another column. */
.lot-header:first-child,
.m-lot-header { position: relative; }
.lot-notes-toggle {
    position: absolute;
    display: inline-flex;
    align-items: center;
    /* Right-aligned in the gutter, as the index numbers are. */
    justify-content: flex-end;
    padding: 0;
    border: none;
    background: none;
    cursor: pointer;
    color: var(--gray-400);
    opacity: 0.55;
    transition: opacity 0.12s, color 0.12s;
    /* Matches .row-number's gutter box. */
    left: -3rem;
    width: 2rem;
    top: 50%;
    transform: translateY(-50%);
}
/* Matches .m-lot-entry .index's gutter box. */
.m-lot-header .lot-notes-toggle {
    left: -28px;
    width: 22px;
    top: 11px;
    transform: none;
}
.lot-notes-toggle svg { width: 20px; height: 20px; margin-right: -6px; }
.lot-notes-toggle:hover { opacity: 0.9; }
.lot-notes-toggle.on {
    color: var(--live);
    opacity: 1;
}
.lot-notes-toggle:focus-visible {
    outline: 2px solid var(--ink);
    outline-offset: 2px;
    opacity: 1;
}


/* Panel shell — now home only to the private-notes editor (the auto-bid /
   tender controls moved inline onto the lot row; see .lot-row-bid). Notes is
   itself a .lot-dialog-panel so it keeps this shell. */
.lot-dialog-panel {
    margin: 4px 0 18px;
    padding: 14px;
    border: 1px solid var(--gray-200);
    border-radius: 10px;
    background: var(--card);
}
.lot-dialog-panel-head {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 8px;
    /* Reserve the note-status glyph's height so the strip doesn't jump when
       the spinner/tick appears. */
    min-height: 21px;
}
.lot-dialog-panel-title {
    font-size: 10.5px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ink-4);
    font-weight: 700;
}

/* Lot-list AUTO indicator — the buyer's own auto-bid, shown in the status
   slot on still-biddable lots (replaces the status word; see
   lotAutobidIndicator). Row tint still conveys any won/lost outcome. The
   mobile selector is deliberately specific enough to beat the base
   `.m-lot-entry .m-lot-row .c.price .sub` (sans/uppercase/grey) so the
   indicator reads the same mono on both surfaces. */
.lot-card .status .auto,
.m-lot-entry .m-lot-row .c.price .sub.auto {
    font-family: var(--mono);
    font-variant-numeric: tabular-nums;
    color: var(--ink-3);
    font-weight: 600;
    letter-spacing: -0.2px;
    text-transform: none;
    white-space: nowrap;
}

/* Read-only tender amounts inherit the regular outcome price typography.
   Only the offer label uses the input label's smaller sans font. */
.lot-offer-price {
    display: inline-flex;
    align-items: baseline;
    justify-content: flex-end;
    flex-wrap: wrap;
    max-width: 100%;
    gap: 4px 12px;
    white-space: nowrap;
}
.lot-offer-amount {
    font: inherit;
    letter-spacing: inherit;
    font-variant-numeric: tabular-nums;
    text-align: right;
}
.lot-offer-price .lot-row-bid-label { color: var(--ink-4); white-space: normal; }

/* ============================================================
   INLINE ROW BID / TENDER INPUT (buttonless)
   A small `0.00` money field on a biddable lot row: the auto-bid max
   sits UNDER the price (auction/rolling — in the status slot, where the
   AUTO text used to render); the sealed tender offer takes the PRICE slot
   (in place of the hidden sealed price). The pre-filled value IS the
   standing indicator, so it replaces the AUTO/TENDER text. Saves on a
   ~1.2 s debounce + blur/Enter, and blur reformats to canonical "x.xx"
   (wolxRowBid* in lot-dialog.js). The status glyph (pending spinner / saved
   tick) sits LEFT of the field so nothing occupies its right edge, and
   the wrapper right-bleeds by the field's padding+border so the DIGITS
   inside line up flush with the right-aligned price digits. Failed saves
   red-outline the field and show a compact one-line "Error ⓘ" block beneath
   it (.lot-row-bid-err); the server's own explanation opens in the anchored
   .lot-row-bid-pop on hover, keyboard focus or tap, and is announced once
   through the visually-hidden .lot-row-bid-msg live region.
   type=text + inputmode=decimal keeps the mobile decimal keypad while
   letting the focus-survival caret restore work (selectionStart throws
   on number inputs).
   ============================================================ */
.lot-row-bid {
    /* Column: the controls row, then the (usually hidden) compact error
       block beneath it. */
    display: inline-flex;
    flex-direction: column;
    align-items: flex-end;
    /* Right-bleed by the field's padding (4px) + border (1px) so the digits
       inside the box align with the price digits above/beside it. The
       price/status cells carry an 8px right inset (below), so the box edge
       lands 3px INSIDE the cell rather than kissing the card edge. */
    margin-right: -5px;
    /* Not a row-tap target (also stopPropagation-guarded in the markup). */
    cursor: auto;
    letter-spacing: 0;
}
/* Label + glyph + field on one unbreakable line ("OFFER" must never wrap
   away from its box). The label leads the flex order so the widget's
   baseline — which the grid row aligns cells by — is real text. */
.lot-row-bid-row {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    white-space: nowrap;
}
/* Breathing room between the right-aligned price/bid column and the card
   edge: inset every cell of the column (rows, status slot, both header
   rows) so digits stay aligned while nothing sits flush against the edge. */
.lot-card .price,
.lot-card .status,
.lot-header > span:last-child {
    padding-right: 8px;
}
.m-lot-entry .m-lot-row .c.price,
.m-lot-header .m-lot-row .c.price {
    padding-right: 9px;
}
/* Mirror the price column's 9px inset on the other right-rail cells
   (Clean/Nett on the id row, B on the tests row) so the whole right edge
   stays column-aligned. */
.m-lot-entry .m-lot-row.row-id > .c:last-child,
.m-lot-header .m-lot-row.row-id > .c:last-child,
.m-lot-entry .m-lot-row.row-tests > .c:last-child,
.m-lot-header .m-lot-row.row-tests > .c:last-child {
    padding-right: 9px;
}
/* AUTO / OFFER kind label — reads as part of the field ("AUTO [ 0.00 ]"),
   placeholder-washed while the field is empty, inked once a value stands so
   an active auto-bid / offer announces itself. */
.lot-row-bid-label {
    font-family: var(--font-sans);
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--ink-5);
}
.lot-row-bid.has-value .lot-row-bid-label { color: var(--gray-900); }  /* match the price */
.lot-row-bid-input {
    font-family: var(--mono);
    font-variant-numeric: tabular-nums;
    font-size: 14px;
    font-weight: 400;
    letter-spacing: -0.3px;
    text-transform: none;
}
.lot-row-bid-input {
    /* "99.99" is five mono chars; the negative tracking below buys the
       caret its sliver of room at this width. */
    width: 5ch;
    box-sizing: content-box;
    border: 1px solid var(--line);
    border-radius: 6px;
    background: var(--card);
    /* Slim chrome: 4px horizontal padding, and the vertical padding+border
       pulled back out with negative margins so the box doesn't inflate the
       secondary row's height. */
    padding: 2px 4px;
    margin: -2px 0;
    line-height: 1;
    color: var(--ink);
    text-align: right;
    -moz-appearance: textfield;
    appearance: textfield;
}
.lot-row-bid-input::placeholder { color: var(--ink-5); font-weight: 400; }
.lot-row-bid-input:focus {
    outline: none;
    border-color: var(--ink);
    box-shadow: 0 0 0 3px rgba(22, 32, 46, 0.08);
}
.lot-row-bid-input:disabled {
    background: var(--paper-2);
    color: var(--ink-5);
    border-style: dashed;
    cursor: not-allowed;
}
/* Failed save: louder than the glyph alone — red outline on the field, with
   the compact "Error ⓘ" block beneath carrying the reason (a native title
   tooltip would never reach touch, which is why the popover replaced it). */
.lot-row-bid-input.error {
    border-color: var(--lose);
}
.lot-row-bid-input.error:focus {
    border-color: var(--lose);
    box-shadow: 0 0 0 3px oklch(54% 0.15 30 / 0.12);
}
/* "Too low": an auto-bid the reserve has overtaken, so it won't bid. The
   error block's shape in amber rather than red — the buyer hasn't done
   anything wrong, the lot has moved. */
.lot-row-bid-input.warn { border-color: oklch(62% 0.14 70); }
.lot-row-bid-err.warn .lot-row-bid-err-word { color: oklch(52% 0.13 70); }
.lot-row-bid-err.warn .lot-row-bid-info { border-color: oklch(52% 0.13 70); color: oklch(52% 0.13 70); }
/* Compact failure block: "Error ⓘ" on ONE line. The prose that used to sit
   here wrapped to two or three lines and shoved the row apart; the full
   reason now lives in the popover the ⓘ anchors. Hidden until the controller
   adds .visible, nowrap so it can never reflow the row, and inset by the
   wrapper's right-bleed so it can't overhang the price column. */
.lot-row-bid-err {
    position: relative;
    display: none;
    align-items: center;
    gap: 4px;
    white-space: nowrap;
    line-height: 1;
    margin-top: 2px;
    padding-right: 5px;   /* cancel the wrapper's digit-alignment bleed */
}
.lot-row-bid-err.visible { display: inline-flex; }
.lot-row-bid-err-word {
    font-family: var(--font-sans);
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--lose);
}
/* Small round ⓘ. 14px visually, but the ::before pad grows the hit area to
   the mobile minimum without adding a pixel of layout. */
.lot-row-bid-info {
    position: relative;
    width: 14px;
    height: 14px;
    flex: none;
    padding: 0;
    border: 1px solid var(--lose);
    border-radius: 50%;
    background: transparent;
    color: var(--lose);
    font-family: var(--font-sans);
    font-size: 9px;
    font-weight: 800;
    font-style: italic;
    line-height: 1;
    text-transform: none;
    letter-spacing: 0;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.lot-row-bid-info::before {
    content: '';
    position: absolute;
    top: -8px; right: -8px; bottom: -8px; left: -8px;
}
.lot-row-bid-info:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px oklch(54% 0.15 30 / 0.20);
}
/* The detail itself: anchored to the ⓘ, right-aligned to the column so it
   opens INTO the card rather than off the edge, and out of flow so opening
   it never changes the row's height. */
.lot-row-bid-pop {
    position: absolute;
    right: 0;
    bottom: calc(100% + 6px);
    z-index: 40;
    display: none;
    width: max-content;
    max-width: min(240px, 70vw);
    padding: 6px 8px;
    border: 1px solid var(--line);
    border-radius: 6px;
    background: var(--card);
    box-shadow: 0 6px 18px rgba(22, 32, 46, 0.16);
    font-family: var(--font-sans);
    font-size: 11.5px;
    font-weight: 500;
    line-height: 1.35;
    letter-spacing: 0;
    text-transform: none;
    text-align: left;
    white-space: normal;
    color: var(--ink);
}
/* Pointer hover AND keyboard focus open it; tap/click latches it open via
   .open (hover alone leaves touch devices with no way to read the reason). */
.lot-row-bid-err:hover .lot-row-bid-pop,
.lot-row-bid-info:focus-visible + .lot-row-bid-pop,
.lot-row-bid-err.open .lot-row-bid-pop { display: block; }
/* Announced, not shown — the compact block is the visible surface. */
.lot-row-bid-msg {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}
/* Per-lot save-status glyph — a fixed 12px box (no reflow as it changes),
   sitting LEFT of the field so the field's right edge stays flush with the
   price column: spinner (pending) → sage tick (saved) → terracotta ! (failed,
   alongside the red outline + message line). */
.lot-row-bid-status {
    width: 12px;
    height: 12px;
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-family: var(--sans);
    font-size: 12px;
    font-weight: 800;
    line-height: 1;
    box-sizing: border-box;
}
.lot-row-bid-status.pending {
    border-radius: 50%;
    border: 2px solid rgba(22, 32, 46, 0.35);
    border-top-color: transparent;
    animation: btn-spin 0.7s linear infinite;
}
.lot-row-bid-status.saved::after { content: '\2713'; color: var(--win); }
.lot-row-bid-status.error::after { content: '!'; color: var(--lose); }

/* Mobile: font-size ≥16px so iOS Safari doesn't auto-zoom the field on
   focus (which would wreck the tab-through flow), plus a ≥44px tap target —
   the wrapper (which focus-forwards taps to the field) provides the height
   around the compact input. */
.m-lot-entry .lot-row-bid-input { font-size: 16px; }
.m-lot-entry .lot-row-bid-input { padding: 5px 7px; }
/* Mobile field padding is 7px (+1px border), so the digit-alignment bleed
   is 8px here; the column centres its controls row inside the 44px target. */
.m-lot-entry .lot-row-bid { padding: 4px 0; min-height: 44px; margin-right: -8px; justify-content: center; }
/* Mobile bleeds 8px, so the error block has to cancel 8px to stay inside the
   card's right rail; the compact single line fits the 44px target as-is. */
.m-lot-entry .lot-row-bid-err { padding-right: 8px; }

/* Legacy "View all closed lots" link styling. No longer used by the
   auction room — its closed section now groups each sale under its own card
   and headers, with a .lot-section-footer link at the foot — but
   preview.html's component gallery still references this class, so the rule
   stays until that page is updated too. */
.closed-view-all {
    display: block;
    text-align: center;
    padding: 10px 12px;
    font-size: 12.5px;
    font-weight: 600;
    color: var(--ink-3);
    text-decoration: none;
}
.closed-view-all:hover { color: var(--ink); }
.closed-view-all .action { display: block; }
.closed-view-all .sale {
    display: block;
    margin-top: 2px;
    font-size: 11.5px;
    font-weight: 500;
    color: var(--ink-4);
}
/* A live page's placeholder until its first websocket frame replaces it. */
.ws-connecting {
    padding: 48px;
    text-align: center;
    color: var(--ink-4);
    font-size: 13px;
}
/* ============================================================
   CONNECTION-HEALTH BANNER
   Slim strip under the topbar, driven by connection-health.js: red when
   the websocket is down (htmx reconnecting), amber when frames stop
   arriving (watchdog) or the heartbeat flags the Fleece feed stale.
   ============================================================ */
.conn-banner {
    /* Sticky so it follows the scroll — connection-health.js sets `top`
       to the visible topbar's bottom edge so it rides just beneath it. */
    position: sticky;
    top: 0;
    z-index: 19;
    text-align: center;
    font-size: 12.5px;
    font-weight: 600;
    padding: 7px 12px;
    border-bottom: 1px solid var(--line);
}
.conn-banner[hidden] { display: none; }
.conn-banner.amber {
    background: #FFFBEB;
    border-bottom-color: #FACC6B;
    color: #B45309;
}
.conn-banner.red {
    background: #FEF2F2;
    border-bottom-color: #FCA5A5;
    color: #DC2626;
}

/* App-wide backend sales message (WoolOnline_SalesMessage). Sticky under
   the topbar like the conn-banner (site-banner.js sets `top`); sits above the
   conn-banner in the stack. Orange palette, matching the per-sale message. */
.site-banner {
    position: sticky;
    top: 0;
    z-index: 18;
    text-align: center;
    font-size: 12.5px;
    font-weight: 600;
    padding: 7px 12px;
    background: #FFF7ED;
    border-bottom: 1px solid #FED7AA;
    color: #C2410C;
}
.site-banner[hidden] { display: none; }

/* Per-sale backend message (Sale.message) — the broker's notice for this
   sale, auto-derived Fleece-side (e.g. the Rolling reminder, a trial-sale
   warning). It sits on a row of its own at the foot of the sale card, so it
   travels with the sale rather than floating at the top of one page. Keeps
   its own bordered amber box inside the card: it is the broker talking, not
   part of the sale's own data. */
.sale-card-message {
    padding: 4px 8px;
    border-radius: 4px;
    background: #FFF7ED;
    border: 1px solid #FED7AA;
    color: #C2410C;
    /* Matches the indicator / type label on the card's first line. */
    font-size: 11px;
    font-weight: 600;
    line-height: 1.35;
    /* Staff type the message in a textarea; keep their line breaks. */
    white-space: pre-line;
    /* Above the stretched cover link on a card that has one, so the message
       is selectable rather than swallowed by the card's own navigation. */
    position: relative;
    z-index: 1;
}

/* ── Sale overflow menu (catalogue download) ── */
.sale-actions {
    max-width: 900px;
    margin: 0 auto 12px;
    padding: 0 4px;
    display: flex;
    justify-content: flex-end;
    position: relative;
}
.sale-actions-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    background: none;
    color: var(--ink-4);
    cursor: pointer;
}
.sale-actions-toggle:hover,
.sale-actions.open .sale-actions-toggle {
    color: var(--ink-2);
    background: var(--paper-2);
    border-color: var(--line-soft);
}
.sale-actions-toggle svg { width: 16px; height: 16px; }
.sale-actions-menu {
    display: none;
    position: absolute;
    top: calc(100% + 4px);
    right: 4px;
    min-width: 210px;
    padding: 4px;
    background: #FFFFFF;
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-wol);
    z-index: 30;
}
.sale-actions.open .sale-actions-menu { display: block; }
.sale-actions-item {
    display: flex;
    align-items: center;
    gap: 6px;
    width: 100%;
    padding: 7px 10px;
    border: none;
    border-radius: var(--radius-md);
    background: none;
    color: var(--ink-3);
    font-family: inherit;
    font-size: 12px;
    font-weight: 500;
    text-align: left;
    text-decoration: none;
    cursor: pointer;
}
.sale-actions-item:hover { background: var(--paper-2); color: var(--ink-2); }
.sale-actions-item svg { width: 14px; height: 14px; flex-shrink: 0; }
.sale-actions-substep + .sale-actions-item { margin-top: 4px; }
/* Include-notes second step: hidden until "Download catalogue" is clicked
   (JS adds .notes-step to the .sale-actions wrapper). */
.sale-actions-substep {
    display: none;
    margin-top: 4px;
    padding: 8px 10px 6px;
    border-top: 1px solid var(--line-soft);
}
.sale-actions.notes-step .sale-actions-substep { display: block; }
.sale-actions.notes-step button.sale-actions-item { color: var(--ink-2); background: var(--paper-2); }
.catalogue-notes-toggle {
    display: flex;
    align-items: center;
    gap: 4px;
    color: var(--ink-5);
    cursor: pointer;
    font-size: 11px;
    user-select: none;
}
.catalogue-notes-toggle input { margin: 0; accent-color: var(--ink-4); }
.catalogue-notes-toggle + .catalogue-notes-toggle { margin-top: 6px; }
.sale-actions-download {
    display: block;
    margin-top: 8px;
    padding: 6px 10px;
    border-radius: var(--radius-md);
    background: var(--ink-2);
    color: #FFFFFF;
    font-size: 12px;
    font-weight: 600;
    text-align: center;
    text-decoration: none;
}
.sale-actions-download:hover { background: var(--ink); }

/* Status row highlight styles */
.lot-card.status-won { background: #F7FDF9; }
.lot-card.status-lost { background: #FEF2F2; }
.lot-card.status-passed { background: #FFFBEB; }
.lot-card.status-live { background: #EDF1F9; }

/* Subtle row-hover highlight. A faint ink wash laid over the card via a
   pseudo-element, so it DEEPENS whatever background is underneath — plain
   white or any of the won/lost/passed/live status washes — rather than
   replacing it. Helps the eye track a single row across the wide test-data
   table. The data rows are lifted above the wash so their text stays crisp;
   the wash sits between the card background/accent and the content. */
.lot-card::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: transparent;
    transition: background 0.12s ease;
    pointer-events: none;
}
.lot-card-row1,
.lot-card-row2 {
    position: relative;
    z-index: 1;
}
.lot-card:hover::after {
    background: rgba(22, 32, 46, 0.05);
}
/* The whole row opens the lot dialog on click. */
.lot-card { cursor: pointer; }

/* Full-height left accent. The bar spans the whole row; its left corners
   INHERIT the row's, so they round with a rounded row and go square wherever
   the row's corners are squared (tight table runs, an open detail panel) —
   no overflow:hidden, which would clip the floating .row-number sitting in
   the gutter. */
.lot-card.status-won::before,
.lot-card.status-lost::before,
.lot-card.status-passed::before,
.lot-card.status-live::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 3px;
    border-radius: 0;
    border-top-left-radius: inherit;
    border-bottom-left-radius: inherit;
}
.lot-card.status-won::before    { background: #16A34A; }
.lot-card.status-lost::before   { background: #DC2626; }
.lot-card.status-passed::before { background: #D97706; }
.lot-card.status-live::before   { background: #4B73D1; }

.lot-card.status-won .price { color: #16A34A; }
.lot-card.status-lost .price { color: #DC2626; }
.lot-card.status-passed .price { color: #D97706; }
.lot-card.status-live .price { color: #4B73D1; }

.lot-card.status-won .status { color: #16A34A; }
.lot-card.status-lost .status { color: #DC2626; }
.lot-card.status-other .status { color: var(--gray-600); }
.lot-card.status-passed .status { color: #D97706; }
.lot-card.status-inactive .status { color: var(--gray-500); }
.lot-card.status-live .status { color: #4B73D1; }

/* Live badge (pulsing dot + label) used in the status cell for the
   currently-selling lot in the auction room. */
.lot-card .status .live-badge {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.lot-card .status .live-badge .d {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: #4B73D1;
    animation: pulse-live 1.5s infinite;
    box-shadow: 0 0 0 3px rgba(75, 115, 209, 0.25);
}

/* Section bar used inside .lot-list to separate groups (Upcoming /
   Live now / Closed) on the auction room. Visually a subtle wash
   with a top/bottom border so it reads as a divider rather than a
   new header row. */
.lot-list .lot-section-bar {
    padding: var(--space-xs) var(--space-md);
    padding-left: var(--space-lg);
    background: #F9FAFB;
    border-top: 1px solid var(--gray-200);
    border-bottom: 1px solid var(--gray-200);
    font-family: var(--font-sans);
    font-size: var(--text-xs);
    font-weight: 600;
    color: var(--gray-600);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    display: flex;
    align-items: center;
    gap: 10px;
}
.lot-list .lot-section-bar.live-sec {
    background: #EDF1F9;
    color: #4B73D1;
}
.lot-list .lot-section-bar.live-sec .d {
    display: inline-block;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: #4B73D1;
    animation: pulse-live 1.5s infinite;
    box-shadow: 0 0 0 3px rgba(75, 115, 209, 0.25);
}

/* Section bars on the auction room page sit OUTSIDE their .lot-list
   so they read as the section heading (above the table titles), not
   as a row inside the table. Drop the grey wash + borders, give them
   plenty of breathing room above so each section reads as its own
   block. Hides bars that have no content (empty section). */
.lot-section-bar.lot-section-outer,
.m-lot-section-bar.lot-section-outer {
    background: transparent;
    border: none;
    padding: 0;
    max-width: 900px;
    margin: 32px auto 8px;
    font-family: var(--font-sans);
    font-size: 14px;
    font-weight: 700;
    color: var(--ink);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    display: flex;
    align-items: center;
    gap: 12px;
}
.lot-section-bar.lot-section-outer:empty,
.m-lot-section-bar.lot-section-outer:empty {
    display: none;
}
.lot-section-bar.lot-section-outer::after,
.m-lot-section-bar.lot-section-outer::after {
    content: '';
    flex: 1;
    height: 1px;
    background: var(--line);
}

/* Vertical breathing room above the live hero so it sits with the
   same gap below Upcoming as the Closed section bar uses. The hero
   container is .lot-list (desktop) / .m-lot-table.auction-hero-block
   (mobile); both pick this up via the `#auction-hero-X` child. */
.lot-list:has(> #auction-hero-desktop:not(:empty)) {
    margin-top: 32px;
}
.m-lot-table.auction-hero-block:has(> #auction-hero-mobile:not(:empty)) {
    margin-top: 32px;
}

/* Hero-slot fallback shown when no lot is actively selling. Three
   states use the same card chrome:
     - "Sale in progress" (live sale, between lots)
     - "Next sale starts in HH:MM:SS" (upcoming within the threshold,
        wrapped in <a> linking into the sale detail page)
     - "No sale currently live" (no sale or further-out upcoming)
   Same 900px max-width and centring as the surrounding lot list,
   slightly more vertical breathing room so the page doesn't feel
   collapsed when there's nothing live. */
.auction-hero-fallback {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    gap: 8px;
    max-width: 900px;
    margin: 0 auto;
    padding: 36px 24px;
    border: none;
    border-radius: var(--radius-lg);
    /* Pre-start / between-lots card mirrors the live hero's dark
       gradient + size so the Selling Area looks the same before a
       sale starts as while it's running — only the countdown/text is
       inverted to read white-on-dark. */
    background: linear-gradient(135deg, var(--ink) 0%, var(--ink-2) 100%);
    color: var(--paper);
    text-decoration: none;
}
.auction-hero-fallback .title {
    font-family: var(--font-sans);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: rgba(250, 246, 238, 0.55);
}
.auction-hero-fallback .countdown-line {
    margin: 4px 0 2px;
}
.auction-hero-fallback .countdown {
    font-family: var(--mono);
    font-size: 36px;
    font-weight: 700;
    color: var(--paper);
    font-variant-numeric: tabular-nums;
    letter-spacing: -1px;
    line-height: 1;
}
.auction-hero-fallback .subtitle {
    font-size: 13px;
    color: rgba(250, 246, 238, 0.7);
}

/* Empty-state placeholder for the Closed section (and any other
   section that opts in via emptyText). Sits inside the lot-list
   body slot so the section header + column titles still render
   above it; reads as "no content" rather than as a missing block. */
.auction-section-empty {
    max-width: 900px;
    margin: 12px auto;
    padding: 24px;
    text-align: center;
    color: var(--ink-4);
    font-size: 13px;
    font-style: italic;
    border: 1px dashed var(--line);
    border-radius: var(--radius-lg);
    background: transparent;
}

/* ============================================================
   AUCTION STAGE — live lot display with bid controls
   ============================================================ */.auction-stage .lot-card {
    border: none;
    border-radius: 0;
}

.auction-stage-price .countdown {
    font-family: var(--font-mono);
    letter-spacing: -1px;
    font-size: 2rem;
    font-weight: 700;
    color: var(--status-red);
    font-variant-numeric: tabular-nums;
}

.auction-stage-bid .bid-btn {
    padding: 8px 16px;
    font-family: var(--font-sans);
    font-size: var(--text-sm);
    font-weight: 600;
    border: 1px solid var(--gray-300);
    border-radius: var(--radius-md);
    background: #FFFFFF;
    color: var(--gray-700);
    cursor: pointer;
    transition: background 0.15s;
}

.auction-stage-bid .bid-btn:hover {
    background: var(--gray-100);
}

@keyframes pulse-live {
    0%, 100% { box-shadow: 0 0 0 3px oklch(55% 0.10 148 / 0.3); }
    50%      { box-shadow: 0 0 0 5px oklch(55% 0.10 148 / 0.0); }
}
@keyframes shine {
    0%, 100% { transform: translateX(-20%); }
    50%      { transform: translateX(20%); }
}

body {
    font-family: var(--sans);
    color: var(--ink);
    background: var(--paper);
    line-height: 1.45;
    font-feature-settings: 'ss01', 'cv11';
}

/* ── App shell ── */
.app {
    display: grid;
    grid-template-columns: 260px 1fr;
    min-height: 100vh;
    background: var(--paper);
}

.sidebar {
    background: var(--ink);
    color: var(--paper);
    padding: 22px 18px;
    display: flex;
    flex-direction: column;
    gap: 28px;
    position: sticky;
    top: 0;
    height: 100vh;
    overflow-y: auto;
}
.sidebar-brand {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 0 4px;
    text-decoration: none;
    color: inherit;
}
.sidebar-brand:hover { text-decoration: none; }
.sidebar-brand .ram {
    width: 32px;
    height: 32px;
    background: var(--paper);
    border-radius: 8px;
    display: grid;
    place-items: center;
    flex-shrink: 0;
}
.sidebar-brand .ram img {
    width: 24px;
    height: 24px;
    object-fit: contain;
}
.sidebar-brand h1 {
    font-family: var(--sans);
    font-size: 15px;
    font-weight: 700;
    letter-spacing: -0.02em;
    color: var(--paper);
    line-height: 1.1;
}
.sidebar-brand .sub {
    font-size: 10.5px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: rgba(250,246,238,0.55);
    margin-top: 2px;
}

.sidebar-section {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.nav-section-label {
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: rgba(250,246,238,0.45);
    padding: 0 10px;
    margin-bottom: 6px;
}

.nav-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 10px;
    border-radius: 8px;
    color: rgba(250,246,238,0.75);
    font-size: 13.5px;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
    text-decoration: none;
    background: none;
    border: none;
    width: 100%;
    text-align: left;
    font-family: inherit;
}
.nav-item:hover {
    background: rgba(250,246,238,0.06);
    color: var(--paper);
    text-decoration: none;
}
.nav-item.active {
    background: rgba(250,246,238,0.10);
    color: var(--paper);
}
/* Indented "current sale" child under All Sales on a sale detail page. No icon;
   its text aligns under the parent label (10px pad + 16px icon + 10px gap), long
   sale names truncate rather than wrap. Rendered with .active so it — not the
   parent All Sales — carries the single highlight. */
.nav-item.child {
    display: block;
    padding: 6px 10px 6px 36px;
    margin-top: 1px;
    font-size: 12.5px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.nav-item .count {
    margin-left: auto;
    font-size: 11px;
    font-variant-numeric: tabular-nums;
    color: rgba(250,246,238,0.5);
    background: rgba(250,246,238,0.08);
    padding: 1px 6px;
    border-radius: 100px;
    font-family: var(--mono);
    letter-spacing: -1px;
}
.nav-item.active .count {
    background: rgba(250,246,238,0.18);
    color: var(--paper);
}
/* The dot lives in a stable #auction-room-nav-dot slot (so ws handlers can
   OOB-swap it). Make THAT slot the flex item pinned hard-right; its dot child
   then blockifies so width/height/border-radius apply — as a plain inline span
   the dot ignored them and rendered as a tiny square box-shadow blob. Matches
   the 7px / pulse-live 1.5s solid-dot-with-pulsing-ring used on the sale cards
   and live hero. */
#auction-room-nav-dot {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
}
.nav-item .dot-live {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--win);
    box-shadow: 0 0 0 3px oklch(55% 0.10 148 / 0.3);
    animation: pulse-live 1.5s infinite;
}
.nav-item .dot-upcoming {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    /* Soft warm-amber (matches the upcoming sale-card accents) so
       the "starting soon" signal reads as friendly, not alarming. */
    background: oklch(80% 0.13 80);
    box-shadow: 0 0 0 3px oklch(80% 0.13 80 / 0.25);
}
.nav-item.disabled {
    opacity: 0.5;
    cursor: not-allowed;
    pointer-events: none;
}

.sidebar-footer {
    margin-top: auto;
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding-top: 14px;
    border-top: 1px solid rgba(250,246,238,0.08);
}
.sidebar-user {
    /* Match the nav-item padding (8px 10px) and gap (10px) so the
       avatar lines up with the Profile / Help / Log out icons below
       and the user-info text shares its left edge with the nav-item
       labels. */
    padding: 8px 10px;
    display: flex;
    align-items: flex-start;
    gap: 10px;
}
.sidebar-user .avatar {
    /* Same 16px footprint as the nav-item icons so the column of
       avatar + Profile / Help / Log out icons reads as one
       consistent left rail. */
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--paper-3);
    color: var(--ink);
    display: grid;
    place-items: center;
    font-weight: 700;
    /* Initials are clamped down to fit; tighter letter-spacing so
       two characters still read inside the 16px circle. */
    font-size: 8px;
    letter-spacing: -0.02em;
    flex-shrink: 0;
    /* Align with the first line of u-info text (the user name). */
    margin-top: 2px;
}
.sidebar-user .u-info {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
}
.sidebar-user .u-name {
    font-size: 13px;
    font-weight: 600;
    color: var(--paper);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    margin-bottom: 2px;
}
/* Each detail line under the user name (code, organisation, role)
   sits on its own row. Mono only for the code so it reads as an
   identifier; the others stay sans for legibility. */
.sidebar-user .u-line {
    font-size: 11px;
    color: rgba(250,246,238,0.55);
    line-height: 1.3;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.sidebar-user .u-code {
    font-family: var(--mono);
    letter-spacing: -1px;
}
.sidebar-user .u-access {
    text-transform: uppercase;
    letter-spacing: 0.08em;
    font-size: 10px;
    color: rgba(250,246,238,0.45);
}
.sidebar-actions {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.sidebar-actions .nav-item.logout-item {
    color: rgba(250,246,238,0.85);
}
.sidebar-actions .nav-item.logout-item:hover {
    background: rgba(250,246,238,0.10);
    color: var(--paper);
}
/* Terms + Privacy as one quiet group at the foot of the sidebar. */
.sidebar-legal {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 10px 2px;
    font-size: 11px;
    letter-spacing: 0.04em;
}
.sidebar-legal-link {
    color: rgba(250,246,238,0.4);
    text-decoration: none;
}
.sidebar-legal-link:hover {
    color: rgba(250,246,238,0.7);
    text-decoration: underline;
}
.sidebar-legal-link:focus-visible {
    outline: 2px solid rgba(250,246,238,0.75);
    outline-offset: 3px;
    border-radius: 3px;
    color: var(--paper);
}
.sidebar-legal-sep {
    color: rgba(250,246,238,0.25);
}

/* ── Main area ── */
.main {
    min-width: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
}
.topbar {
    position: sticky;
    top: 0;
    z-index: 20;
    background: rgba(250,246,238,0.85);
    backdrop-filter: blur(12px) saturate(140%);
    -webkit-backdrop-filter: blur(12px) saturate(140%);
    border-bottom: 1px solid var(--line);
    padding: 14px 32px;
    display: flex;
    align-items: center;
    gap: 16px;
}
.topbar .breadcrumb {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 13px;
    color: var(--ink-3);
}
.topbar .breadcrumb strong {
    color: var(--ink);
    font-weight: 600;
}
/* Non-terminal breadcrumb segments are links back up the tree — same muted ink
   as the plain segments, with an underline on hover as the affordance. */
.topbar .breadcrumb a {
    color: var(--ink-3);
    text-decoration: none;
    transition: color 0.15s;
}
.topbar .breadcrumb a:hover {
    color: var(--ink);
    text-decoration: underline;
    text-underline-offset: 2px;
}
.topbar .breadcrumb .sep { color: var(--ink-5); }
.topbar-actions {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 14px;
}
.legacy-switch-form {
    margin: 0;
}
.legacy-switch-link {
    appearance: none;
    border: 0;
    border-right: 1px solid var(--line);
    padding: 2px 14px 2px 0;
    background: transparent;
    color: var(--ink-4);
    font: 600 11px/1 var(--sans);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    cursor: pointer;
    transition: color 0.15s;
}
.legacy-switch-link:hover {
    color: var(--ink);
}
.topbar .clock {
    margin-left: 0;
    font-family: var(--mono);
    letter-spacing: -1px;
    font-size: 13px;
    color: var(--ink-3);
    display: flex;
    align-items: center;
    gap: 8px;
    font-variant-numeric: tabular-nums;
}
.topbar .clock .live-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--win);
    box-shadow: 0 0 0 3px oklch(55% 0.10 148 / 0.25);
    animation: pulse-live 2s infinite;
}

/* ── Page layout override ── */
.page {
    padding: 28px 32px 60px;
    flex: 1;
    max-width: 1440px;
    width: 100%;
    margin: 0 auto;
}
.page-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    margin-bottom: 24px;
    gap: 16px;
    flex-wrap: wrap;
}
.page-header-left { flex: 1; min-width: 0; }
.page-title {
    font-size: 30px;
    font-weight: 700;
    letter-spacing: -0.025em;
    color: var(--ink);
    line-height: 1.1;
}
.page-subtitle {
    font-size: 14px;
    color: var(--ink-3);
    margin-top: 6px;
}
.page-header-left h1 {
    font-size: 30px;
    font-weight: 700;
    letter-spacing: -0.025em;
    color: var(--ink);
    line-height: 1.1;
}
.page-actions {
    display: flex;
    gap: 8px;
    align-items: center;
}

/* ── Buttons override ── */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 8px 14px;
    font-family: var(--sans);
    font-size: 13px;
    font-weight: 500;
    line-height: 1.4;
    border-radius: 8px;
    border: 1px solid var(--line);
    background: var(--card);
    color: var(--ink);
    cursor: pointer;
    transition: all 0.15s;
    white-space: nowrap;
    text-decoration: none;
}
.btn:hover {
    background: var(--paper-2);
    border-color: var(--ink-5);
    text-decoration: none;
}
.btn-primary {
    background: var(--ink);
    color: var(--paper);
    border-color: var(--ink);
}
.btn-primary:hover {
    background: var(--ink-2);
    border-color: var(--ink-2);
}
.btn-ghost {
    background: transparent;
    border-color: transparent;
    color: var(--ink-3);
}
.btn-ghost:hover {
    background: var(--paper-2);
    color: var(--ink);
}
.btn-sm {
    padding: 5px 10px;
    font-size: 12px;
    border-radius: 6px;
}
.btn-lg {
    padding: 12px 20px;
    font-size: 14px;
    font-weight: 600;
}

/* ── Legend ── */
.legend {
    display: flex;
    gap: 18px;
    flex-wrap: wrap;
    font-size: 12px;
    color: var(--ink-3);
    padding: 0;
    background: transparent;
    border: none;
    border-radius: 0;
}
.legend-item {
    display: flex;
    align-items: center;
    gap: 6px;
}
.legend-swatch {
    width: 12px;
    height: 12px;
    border-radius: 3px;
    border: 1px solid currentColor;
}
.legend-swatch.win  { background: var(--win-bg);  color: var(--win); }
.legend-swatch.lose { background: var(--lose-bg); color: var(--lose); }
.legend-swatch.pass { background: var(--pass-bg); color: var(--pass); }
.legend-swatch.live { background: var(--live-bg); color: var(--live); }

/* ── Sales dashboard ── */
/* Align the page header (and the auction-room CTA banner) with the
   centred 900px sale-card-list / sales-section-label below, so the
   "Sales" heading sits flush with the cards rather than the page
   gutter. Scoped via .page.sales / .auction / .lot-sections so other pages
   keep the default full-width header. */
.page.sales .page-header,
.page.auction .page-header,
.page.lot-sections .page-header {
    max-width: 900px;
    margin: 0 auto 20px;
    width: 100%;
}
.page.sales .auction-room-cta,
.page.auction .auction-room-cta {
    max-width: 900px;
    margin-left: auto;
    margin-right: auto;
}

/* Floats bottom-centre over the content column, under the drawer scrim (40).
   back-to-selling-lot.js adds .visible only while the hero is off-screen, so
   it never sits over the hero's bid buttons. The show delay rides out
   hero-auto-focus.js's arrival scroll so the button doesn't flash. */
.page.auction {
    padding-bottom: 96px;
}
.back-to-selling-lot {
    position: fixed;
    z-index: 24;
    bottom: calc(16px + env(safe-area-inset-bottom, 0px));
    left: calc(50% + 130px);
    transform: translateX(-50%);
    max-width: calc(100vw - 32px);
    min-height: 44px;
    padding: 10px 20px;
    border: 0;
    border-radius: 999px;
    background: var(--ink);
    color: var(--paper);
    font: inherit;
    font-size: 14px;
    font-weight: 600;
    white-space: nowrap;
    box-shadow: 0 6px 20px rgba(22, 32, 46, 0.3);
    cursor: pointer;
    visibility: hidden;
    opacity: 0;
    transition: opacity 0.15s ease-out, visibility 0s linear 0.15s;
}
.back-to-selling-lot.visible {
    visibility: visible;
    opacity: 1;
    transition: opacity 0.15s ease-out 0.6s, visibility 0s linear 0.6s;
}
.back-to-selling-lot:hover {
    background: var(--gray-800);
}
.back-to-selling-lot:focus-visible {
    outline: 2px solid var(--ink);
    outline-offset: 3px;
}
#auction-hero-desktop:focus,
#auction-hero-mobile:focus {
    outline: none;
}
@media (max-width: 1140px) {
    .back-to-selling-lot { left: calc(50% + 100px); }
}
@media (max-width: 1075px) {
    .back-to-selling-lot { left: 50%; }
}
@media (prefers-reduced-motion: reduce) {
    .back-to-selling-lot,
    .back-to-selling-lot.visible {
        transition-property: visibility;
    }
}

.sales-section-label {
    font-size: 14px;
    font-weight: 700;
    letter-spacing: 0.02em;
    color: var(--ink);
    margin: 28px auto 12px;
    max-width: 900px;
    display: flex;
    align-items: center;
    gap: 12px;
}
.sales-section-label::after {
    content: '';
    flex: 1;
    height: 1px;
    background: var(--line);
}

/* Sales list: a simple vertical stack of sale cards, one per row,
   capped at the same 900px width the sale-detail lot list uses so
   the two views read at the same scale. */
.sale-card-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
    max-width: 900px;
    margin: 0 auto;
}
.sale-card-grid {
    display: flex;
    flex-direction: column;
    gap: 10px;
    max-width: 900px;
    margin: 0 auto;
}
.sale-card-grid.sale-card-solo {
    max-width: 900px;
    margin: 0 auto 24px;
}

/* Auction-room CTA banner that sits above the sales list when
   there's a currently live auction or one starting within the next
   hour. Live variant picks up the same green pulse the live hero
   uses; upcoming variant is intentionally softer — informational,
   not urgent. */
.auction-room-cta {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 16px;
    padding: 12px 18px;
    /* Match the sale cards' 5px corners (was 14px) so the live/upcoming
       banner reads as part of the same surface family. */
    border-radius: 5px;
    text-decoration: none;
    color: inherit;
    font-family: var(--sans);
    max-width: 900px;
    margin: 0 auto 20px;
    transition: transform 0.15s, box-shadow 0.15s, border-color 0.15s;
    border: 1px solid transparent;
}
.auction-room-cta:hover {
    transform: translateY(-1px);
    box-shadow: var(--shadow-wol);
    text-decoration: none;
}
.auction-room-cta.live {
    background: var(--win-bg);
    border-color: var(--win-border);
    color: var(--ink);
    /* Same expanding-ring pulse the live status dots use (pulse-live),
       so the banner's attention cue matches the dots across the app —
       a crisp 3→5px ring rather than a soft halo. box-shadow only so
       it doesn't fight the hover lift transform. */
    animation: pulse-live 1.5s infinite;
}
.auction-room-cta.upcoming {
    /* Soft warm-amber tint matching the upcoming sale-card accents
       so the same "starting soon" signal is consistent across the
       page. Uses the dot-style ring pulse in honey (pulse-upcoming)
       so it shares the live banner's animation while keeping the
       upcoming colour identity. */
    background: oklch(98% 0.02 85);
    border-color: oklch(88% 0.07 80);
    color: var(--ink);
    animation: pulse-upcoming 1.5s infinite;
}

/* Honey counterpart to pulse-live (styles.css :root section) — same
   expanding-ring shape, warm-amber hue, for the upcoming banner. */
@keyframes pulse-upcoming {
    0%, 100% { box-shadow: 0 0 0 3px oklch(80% 0.13 80 / 0.3); }
    50%      { box-shadow: 0 0 0 5px oklch(80% 0.13 80 / 0.0); }
}
.auction-room-cta .cta-main {
    display: flex;
    align-items: center;
    gap: 10px;
    /* Match the sale-card name size so the banner reads as the
       same hierarchy level. */
    font-size: 16px;
    line-height: 1.35;
    flex: 1 1 auto;
    min-width: 0;
}
.auction-room-cta .cta-main strong {
    font-weight: 600;
    color: var(--ink);
}
.auction-room-cta .cta-main .pulse {
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--win);
    box-shadow: 0 0 0 3px oklch(55% 0.10 148 / 0.22);
    animation: pulse-live 1.5s infinite;
    flex-shrink: 0;
}
/* The action reads as a button wherever the banner appears — the All Sales
   page, the sale-detail page, the sale room — and matches the sale card's own
   .sale-card-room-cta, so "go to the room" looks the same everywhere. */
.auction-room-cta .cta-action {
    font-weight: 600;
    font-size: 13px;
    padding: 7px 12px;
    border-radius: 4px;
    background: var(--win);
    color: var(--paper);
    line-height: 1.2;
    white-space: nowrap;
}
/* The upcoming banner is informational, not a call to act now, so its button
   stays an outline rather than competing with a live sale's filled one. */
.auction-room-cta.upcoming .cta-action {
    background: transparent;
    color: var(--ink);
    box-shadow: inset 0 0 0 1px oklch(80% 0.07 80);
}
/* Inline countdown ticker inside the upcoming-banner. Same data-end-ms
   contract as the per-card .countdown widget, so the two readouts
   tick in lockstep (item 5.9). Sized to match the surrounding banner
   text — the banner is already a deliberate call-out, so the
   countdown doesn't need to scream a second time. */
.auction-room-cta.upcoming .cta-main .countdown {
    font-family: var(--mono);
    letter-spacing: -1px;
    font-weight: 600;
    font-size: inherit;
    color: var(--ink-2);
    font-variant-numeric: tabular-nums;
}

/* ── All Sales pinned live banner ───────────────────────────────────
   .sales-cta-slot wraps the live banner at the top of #sales-content,
   which spans the whole list. It is the ONE place this banner still
   appears: every sale card now carries its own .sale-card-room-cta
   button, so the sale-detail page needs no banner, but the All Sales
   list has no single focal card to hang a button on. */
.sales-cta-slot.sticky {
    position: sticky;
    /* Sits below every visible top chrome layer. sticky-chrome.js keeps
       --wolx-chrome-bottom at the bottom edge of the lowest visible
       topbar / site-banner / conn-banner (all of which have dynamic
       heights and a mobile bar that scrolls away); the literal
       fallback covers the first paint before the script runs. The
       extra 12px keeps the pinned strip from sitting hard against
       the topbar's bottom edge. */
    top: calc(var(--wolx-chrome-bottom, 56px) + 12px);
    /* Under the topbar (20), site-banner (18) and conn-banner (19) —
       a connection warning must never be covered by this — but above
       the lot rows scrolling beneath it. */
    z-index: 17;
    max-width: 900px;
    margin: 0 auto 16px;
}
/* Compact while pinned: this is a persistent control, not a hero
   banner. It gives back vertical space and drops the ring pulse,
   which would otherwise animate under the reader's eye for the whole
   sale. Opaque background + a shadow so lot rows passing underneath
   never bleed through. */
.sales-cta-slot.sticky .auction-room-cta {
    margin: 0;
    padding: 8px 14px;
    animation: none;
    background: var(--win-bg);
    box-shadow: 0 2px 10px oklch(0% 0 0 / 0.10);
}
.sales-cta-slot.sticky .auction-room-cta:hover {
    transform: none;
    box-shadow: 0 2px 10px oklch(0% 0 0 / 0.16);
}
.sales-cta-slot.sticky .auction-room-cta .cta-main {
    font-size: 14px;
}
/* Keyboard focus must stay obvious on a control that floats over
   content. */
.sales-cta-slot .auction-room-cta:focus-visible {
    outline: 2px solid var(--ink);
    outline-offset: 2px;
}

/* Sale state indicator ("Live now" / "Upcoming") rendered inside
   the card header above the sale name. Same markup / styles on
   both the sales page and the auction room's solo card so the
   visual language is consistent. */
.sale-indicator {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-family: var(--sans);
    /* Matches .type-label — the two share the card's first line, so they
       have to be the same size or the line reads as crooked. */
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    margin-bottom: 6px;
    line-height: 1;
}
.sale-indicator.live { color: var(--win); }
.sale-indicator.live .pulse {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--win);
    box-shadow: 0 0 0 3px oklch(55% 0.10 148 / 0.22);
    animation: pulse-live 1.5s infinite;
}
.sale-indicator.upcoming { color: var(--ink-3); }
.sale-indicator.closed { color: var(--ink-4); }
/* A draft (Unavailable) sale: the ordinary card, not quite solid yet — a
   half-white fill that lets the page show through, clearer grey dashes and a
   slightly softened name, so it reads as "not real yet" without shouting. */
.sale-indicator.draft { color: var(--ink-4); }
.sale-card.draft {
    border-style: dashed;
    border-color: var(--ink-5);
    background: rgba(255, 255, 255, 0.5);
}
.sale-card.draft h3 { color: var(--ink-2); }

/* Closed sale cards: the same card anatomy as the open sections, visibly
   "over" — a crisp grey CLOSED chip with the name/type/meta faded. No
   accent stripe or wash; hover/click still work (the catalogue stays
   browsable after close). */
.sale-card.closed h3,
.sale-card.closed .id,
.sale-card.closed .type-label,
.sale-card.closed .sale-when { opacity: 0.55; }

/* Live + upcoming cards stand out from the rest of the list with a
   chunkier left stripe, a subtle wash background, and a heavier
   border so they read as the focal sale at a glance (item 5.1).
   Live = green; upcoming = warm orange to match the countdown's
   "starting soon" colour from item 5.7. Hover stacks the inset
   stripe with the drop shadow so the accent stays visible while the
   card lifts. */
.sale-card.live {
    background: var(--win-bg);
    border-color: var(--win-border);
}
.sale-card.upcoming {
    background: oklch(98% 0.02 85);
    border-color: oklch(88% 0.07 80);
}
/* Flat, straight left accent (pseudo-element bar instead of an inset
   box-shadow, so it doesn't curl around the corners). The bar itself is
   square: the card's overflow:hidden clips it to the card's exact inner
   corner curve, so it follows whatever corners the card has (rounded when
   free-floating, square where it sits on a lots table). The base card's
   hover lift shadow still applies. */
.sale-card.live::before,
.sale-card.upcoming::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0px;
    bottom: 0px;
    width: 5px;
}
.sale-card.live::before     { background: var(--win); }
.sale-card.upcoming::before { background: oklch(80% 0.13 80); }
/* The called-out cards are marked by their stripe, wash and border, not by
   type size: a sale name is a sale name whatever state it is in, and a list
   whose names change size as sales go live reads as ragged rather than
   hierarchical. Colour is the one thing that still varies here. */
.sale-card.upcoming .sale-indicator {
    color: oklch(58% 0.14 70);
}
.sale-card {
    background: var(--card);
    /* gray-200, like the lot rows — one border colour for sale cards on
       every surface. */
    border: 1px solid var(--gray-200);
    border-radius: 5px;
    /* Tightened bottom padding — the broker block that used to sit at
       the foot of the card has moved up onto the id line, so the card
       can be a touch shorter. Even top and bottom now, and tighter
       horizontally: the id lines below the name run to three, so the
       card carries its height in content rather than padding. */
    padding: 10px 14px;
    display: flex;
    flex-direction: column;
    gap: 10px;
    cursor: pointer;
    transition: border-color 0.15s, transform 0.15s, box-shadow 0.15s;
    position: relative;
    overflow: hidden;
    color: inherit;
    text-decoration: none;
    font-family: var(--sans);
}
/* Hover marks the SALE NAME, not the card. A card can now hold a button of
   its own, and lifting or outlining the whole card claimed the hover for the
   card while the pointer sat on the button — saying "you are about to open
   the sale" when the click would enter the room. Underlining the name says
   exactly what the click does, and where. */
.sale-card:hover { text-decoration: none; }
.sale-card:not(.static):hover h3 {
    text-decoration: underline;
    text-underline-offset: 2px;
}
/* ...and drops away while the pointer is on the button, which goes somewhere
   else entirely. */
.sale-card:not(.static):has(.sale-card-room-cta:hover) h3 {
    text-decoration: none;
}
.sale-card.static { cursor: default; }
/* The card's first line: the sale type on the left, the state indicator
   (Live now, Upcoming, Closed, Draft) on the right. */
.sale-card .card-top {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 8px;
    /* 6px to the sale name below, not the card's own 10px stack gap: this
       line labels the header under it, so it should sit with it rather than
       float between. Both labels are set solid (line-height 1) so the gap is
       what it says, not padded out by their leading. */
    margin-bottom: -4px;
    line-height: 1;
}
.sale-card .card-top :is(.sale-indicator, .type-label) { margin-bottom: 0; line-height: 1; }
/* The two columns stretch to a common height, so the right one can hang its
   readout (and the sale-room button) at the foot rather than up beside the
   sale name. */
.sale-card .header {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    align-items: stretch;
}
.sale-card .header-left { min-width: 0; }
/* Lots, location and broker/sale codes, one per line, in the same rhythm as
   the status readout opposite (1px gaps, 1.15 leading) so the two columns
   come out about the same height. */
.sale-card .id-lines {
    display: flex;
    flex-direction: column;
    gap: 1px;
    line-height: 1.15;
    margin-top: 5px;
}
/* Right column of the card header: at most one status readout — a countdown,
   a "starts after" line or a date/time — plus the type label when the card
   has no indicator to pair it with. */
.sale-card .header-right {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 6px;
    flex-shrink: 0;
}
/* The readout hangs at the foot of the column, level with the id lines
   opposite, instead of riding up beside the sale name. */
.sale-card .header-right > :is(.sale-when, .sale-countdown, .starts-after) {
    margin-top: auto;
}
.sale-card .type-label {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ink-4);
    flex-shrink: 0;
    white-space: nowrap;
}
.sale-card .sale-countdown {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 1px;
    line-height: 1.1;
    text-align: right;
}
.sale-card .sale-countdown .k {
    font-family: var(--sans);
    font-size: 9px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ink-4);
    font-weight: 500;
    /* Both labels here ("Starts in", "Scheduled start") are short enough to
       hold one line, and a wrapped label reads as two readouts rather than
       one. The column is shrink-to-fit, so the sale name takes the wrapping
       instead — which is what it is there for. */
    white-space: nowrap;
}
.sale-card .sale-countdown .countdown {
    font-family: var(--mono);
    letter-spacing: -1px;
    font-size: 16px;
    font-weight: 700;
    /* 5.7: countdown only renders inside the final hour. Coloured a
       softer warm-amber rather than red — reads as "starting soon"
       without looking like an error / urgent state. */
    color: oklch(58% 0.14 70);
    font-variant-numeric: tabular-nums;
}
.sale-card.live .sale-countdown .countdown,
.sale-card.upcoming .sale-countdown .countdown {
    font-size: 18px;
}
.sale-card .sale-countdown .k {
    color: oklch(58% 0.14 70);
    font-weight: 600;
}
/* Queue-aware "Starts after <sale>" line — shown in the header-right
   countdown slot on an overdue sale still waiting behind an unfinished sale
   ahead of it in the queue. A quiet status readout, not a ticking clock:
   it won't start on a timer, it starts when the named sale finishes. */
.sale-card .starts-after {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 1px;
    line-height: 1.15;
    text-align: right;
    max-width: 150px;
}
.sale-card .starts-after .k {
    font-family: var(--sans);
    font-size: 9px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ink-4);
    font-weight: 500;
}
.sale-card .starts-after .v {
    font-size: 12px;
    font-weight: 600;
    color: var(--ink-3);
    line-height: 1.2;
}
.sale-card h3 {
    font-size: 16px;
    font-weight: 600;
    letter-spacing: -0.015em;
    line-height: 1.25;
    color: var(--ink);
}
.sale-card .id {
    font-family: var(--mono);
    letter-spacing: -1px;
    font-size: 12px;
    color: var(--ink-4);
}
/* The one relevant sale time (Starts / Ends / Ended) in the header-right
   status slot, where the countdown sits when there is one — and built to the
   same shape, so the slot reads the same whichever is showing: the label, the
   date under it, then the figure people actually look for. */
.sale-card .sale-when {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 1px;
    line-height: 1.15;
    text-align: right;
}
.sale-card .sale-when .k {
    font-family: var(--sans);
    font-size: 9px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ink-4);
    font-weight: 500;
    white-space: nowrap;
}
.sale-card .sale-when .date {
    font-family: var(--mono);
    letter-spacing: -1px;
    font-size: 11.5px;
    color: var(--ink-4);
    white-space: nowrap;
}
/* The time of day carries the readout, so it takes the size the countdown's
   value takes — a step under it, because a date/time is reference, not a
   clock running out. */
.sale-card .sale-when .time {
    font-family: var(--mono);
    letter-spacing: -1px;
    font-size: 14px;
    font-weight: 600;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* "Enter sale room" on the sale-detail card. The card is pinned above the lot
   list, so this is the standing way into the room — it replaces the banner
   that used to pin itself. A solid button, unlike anything else on the card,
   because it is the only thing here you act on rather than read.

   margin-top:auto drops it to the foot of the header's right column, which
   stretches to the card's full height — so the button lands in the card's
   bottom-right corner rather than tucked under the type label. */
/* Stretched link. A card carrying the sale-room button can't be an anchor
   itself, so it's a div with this transparent cover laid over it: the whole
   card still navigates to the sale, and the button — positioned above the
   cover — stays its own control. */
.sale-card-cover {
    position: absolute;
    inset: 0;
    border-radius: inherit;
    z-index: 0;
}
.sale-card-room-cta {
    display: inline-block;
    position: relative;
    z-index: 1;
    margin-top: auto;
    padding: 7px 12px;
    border-radius: 4px;
    background: var(--win);
    color: var(--paper);
    font-family: var(--sans);
    font-size: 13px;
    font-weight: 600;
    line-height: 1.2;
    white-space: nowrap;
    text-decoration: none;
    transition: background 0.15s, box-shadow 0.15s;
}
.sale-card-room-cta:hover {
    text-decoration: none;
    color: var(--paper);
    box-shadow: 0 2px 8px oklch(0% 0 0 / 0.18);
}
/* An upcoming sale's room is open to watch, not to bid in yet, so its button
   is an outline rather than a filled one — the same restraint the upcoming
   banner shows on the All Sales page. Only the live sale gets the solid
   green, so on a list of sales the one you can act in stands out. */
.sale-card.upcoming .sale-card-room-cta {
    background: transparent;
    color: var(--ink);
    box-shadow: inset 0 0 0 1px oklch(80% 0.07 80);
}
.sale-card.upcoming .sale-card-room-cta:hover {
    color: var(--ink);
    background: oklch(80% 0.13 80 / 0.10);
    box-shadow: inset 0 0 0 1px oklch(72% 0.10 80);
}
.sale-card-room-cta:focus-visible {
    outline: 2px solid var(--ink);
    outline-offset: 2px;
}
.sales-table {
    width: 100%;
    max-width: 900px;
    margin: 0 auto;
    border-collapse: collapse;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: 16px;
    overflow: hidden;
    font-size: 13px;
    font-family: var(--sans);
}
.sales-table thead {
    background: linear-gradient(135deg, var(--ink) 0%, var(--ink-2) 100%);
}
.sales-table th {
    text-align: left;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: -1px;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.9);
    padding: var(--space-sm) var(--space-md);
    background: transparent;
    border-bottom: none;
}
.sales-table th.num { text-align: right; }
.sales-table td {
    padding: 12px 14px;
    border-bottom: 1px solid var(--line-soft);
    vertical-align: middle;
    color: var(--ink-2);
}
.sales-table td.num {
    text-align: right;
    font-variant-numeric: tabular-nums;
    letter-spacing: -1px;
}
.sales-table td.id {
    font-family: var(--mono);
    font-size: 12px;
    letter-spacing: -1px;
    color: var(--ink-3);
}
.sales-table td.name {
    font-weight: 500;
    color: var(--ink);
}
.sales-table td.mono {
    font-family: var(--mono);
    font-size: 12px;
    letter-spacing: -1px;
}
.sales-table tr { transition: background 0.12s; cursor: pointer; }
.sales-table tr:last-child td { border-bottom: none; }
.sales-table tr:hover td { background: var(--paper-2); }

/* ── Live auction ── */
/* Currently-selling lot card. Single-column layout: hero label,
   then a mobile-card-style three-row lot info block with inline
   k/v cells, then a prominent current-bid + countdown row, then
   the bid-increment buttons. */
/* Wrapper hosts the absolute-positioned .row-number index that sits
   in the left margin of the hero card, mirroring the lot-card
   numbering. The wrapper owns the 900px width + centering so the
   index can escape the hero's overflow:hidden. */
.live-hero-wrap {
    position: relative;
    max-width: 900px;
    margin: 0 auto;
}
.live-hero-wrap > .row-number {
    position: absolute;
    left: -3rem;
    top: 50%;
    transform: translateY(-50%);
    font-family: var(--font-mono);
    letter-spacing: -1px;
    font-size: 13px;
    /* Same ink + weight + opacity as the .lot-card row index so the hero's
       gutter number reads as clearly as the rows above and below — the old
       gray-400 @ 0.6 washed out to near-invisible on the paper background. */
    font-weight: 600;
    color: var(--gray-600);
    font-variant-numeric: tabular-nums;
    text-align: right;
    width: 2rem;
    opacity: 0.85;
}

.live-hero {
    background: linear-gradient(135deg, var(--ink) 0%, var(--ink-2) 100%);
    color: var(--paper);
    border-radius: var(--radius-lg);
    padding: 15px 18px;
    display: flex;
    flex-direction: column;
    gap: 11px;
    position: relative;
    overflow: hidden;
}
/* Neutral splash by default; the .leading / .outbid / .pending
   state variants tint this in colour when the user has a bid. */
.live-hero::before {
    content: '';
    position: absolute;
    inset: -1px;
    border-radius: var(--radius-lg);
    background: linear-gradient(120deg, transparent 20%, rgba(250, 246, 238, 0.08) 50%, transparent 80%);
    pointer-events: none;
}
.live-hero > * { position: relative; z-index: 1; }

.live-hero .hero-label {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: oklch(70% 0.10 148);
    display: flex;
    align-items: center;
    gap: 8px;
}
.live-hero .hero-label .d {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--win);
    animation: pulse-live 1.5s infinite;
    box-shadow: 0 0 0 3px oklch(55% 0.10 148 / 0.35);
}
/* Status eyebrow + sale name stacked tightly at the top of the hero. */
.live-hero .hero-head {
    display: flex;
    flex-direction: column;
    gap: 3px;
}
.live-hero .hero-sale-name {
    font-family: var(--font-sans);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.04em;
    color: rgba(250, 246, 238, 0.7);
}

/* The three-row lot info block inside the live hero uses the same
   .m-lot-row grid templates as the mobile lot cards, but each cell
   contains an inline label (k) and value (v) so the field names
   stay visible like the stat blocks did before. */
.live-hero-info {
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.live-hero-info .m-lot-row .c {
    font-family: var(--font-mono);
    letter-spacing: -1px;
    color: var(--paper);
    text-align: left;
    font-size: 12px;
}
.live-hero-info .m-lot-row .c .k {
    display: block;
    font-family: var(--font-sans);
    font-size: 9.5px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: rgba(250,246,238,0.5);
    font-weight: 500;
    margin-bottom: 2px;
}
.live-hero-info .m-lot-row .c .v {
    font-family: var(--font-mono);
    letter-spacing: -1px;
    font-size: 15px;
    font-weight: 600;
    color: var(--paper);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Secondary value appearing beneath the primary value in a hero
   cell — e.g. Test under Bales, Net Wgt under Clean, Col Y under
   Y-Z. Rendered as a labelled k/v pair (sub-k = uppercase label,
   sub-v = monospace value) so the secondary reads as its own
   titled field, matching the primary .k/.v rhythm at a smaller
   scale. */
.live-hero-info .m-lot-row .c .sub {
    display: block;
    margin-top: 4px;
    line-height: 1.2;
}
.live-hero-info .m-lot-row .c .sub .sub-k {
    display: block;
    font-family: var(--font-sans);
    font-size: 9px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: rgba(250, 246, 238, 0.4);
    font-weight: 500;
    margin-bottom: 1px;
}
.live-hero-info .m-lot-row .c .sub .sub-v {
    display: block;
    font-family: var(--font-mono);
    letter-spacing: -1px;
    font-size: 12.5px;
    font-weight: 500;
    color: rgba(250, 246, 238, 0.7);
    font-variant-numeric: tabular-nums;
}
/* row-tests in the hero bottom-aligns its cells so the primary
   cells' secondary lines sit on the same baseline as the sub-only
   cells (MM / CV / NTK / T / M / B), no matter how tall the
   primary row becomes. Also drops the primary value size down to
   match the secondary's, so the whole row reads at one scale —
   avoids clipping on narrow screens where 15px was tight. */
.live-hero-info .m-lot-row.row-tests {
    align-items: end;
}
.live-hero-info .m-lot-row.row-tests .c .v {
    font-size: 12.5px;
    font-weight: 500;
}

/* Desktop hero info — sits in `.lot-grid` columns so each cell
   aligns with the equivalent column in the lot table below.
   LotNo / Price columns are intentionally rendered empty (lot
   number lives in the centred header, the bid lives in the bid
   section). Cell values are sized down vs the original 22px
   "lot-col" treatment because the columns are narrower. */
.live-hero-info-desktop .live-hero-info-row {
    align-items: end;
    column-gap: var(--space-sm, 8px);
    padding: 6px 0;
}
.live-hero-info-desktop .live-hero-info-row + .live-hero-info-row {
    border-top: 1px dashed rgba(250, 246, 238, 0.10);
}
.live-hero-info-desktop .c {
    color: var(--paper);
    min-width: 0;
}
.live-hero-info-desktop .c .k {
    display: block;
    font-family: var(--font-sans);
    font-size: 9.5px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: rgba(250, 246, 238, 0.5);
    font-weight: 500;
    margin-bottom: 2px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.live-hero-info-desktop .c .v {
    font-family: var(--font-mono);
    letter-spacing: -1px;
    font-size: 13px;
    font-weight: 600;
    color: var(--paper);
    font-variant-numeric: tabular-nums;
    line-height: 1.2;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.live-hero-info-desktop .c.num .v {
    text-align: right;
}
.live-hero-info-desktop .c.num .k {
    text-align: right;
}
/* The MM/CV/NKT/T/M/B cluster cell pairs a packed monospace label
   with a packed monospace value. Render the label in the same mono
   face + size as the value (it would otherwise be a small sans
   micro-label) so the six space-padded tokens line up column-for-
   column with the values directly below. The muted colour + lighter
   weight inherited from .c .k keep it reading as a label, not data. */
/* Hero cluster: six stacked label+value sub-cells, right-aligned, matching
   the other hero data cells rather than one packed monospace string. */
.live-hero-info-desktop .lot-cluster > .num {
    text-align: right;
    min-width: 0;
}
.live-hero-info-desktop .c.lot-no-col,
.live-hero-info-desktop .c.price-col {
    /* Reserved-but-empty slots that keep the rest of the row in
       sync with the lot table below. Rendered as plain spans so
       no padding / decoration leaks. */
}

/* Centred lot-number title block, sits between the lot info grid
   and the bid section. The label is the same monospace small-caps
   the other live-hero `.k` values use; the value is large and bold
   so the change-pulse below reads clearly when a new lot becomes
   the focal lot. */
/* Hairline divider between the lot detail grid and the lot-no /
   bid section. Visible separator so the centred lot-no header
   reads as the start of the bid area, not a stray label between
   the lot-info rows. */
.live-hero .live-hero-divider {
    border-top: 1px solid rgba(250, 246, 238, 0.18);
    margin: 11px 0 0;
}
.live-hero .live-hero-lot-no {
    text-align: center;
    /* Sits just below the divider, before the bid block. */
    margin: 8px auto 7px;
    transform-origin: center center;
}
/* Pulse only when the server explicitly tags the lot-no element
   as freshly-changed (server-side: prev.hero.lotNo != curr lotNo
   across consecutive ws frames). Same lot reappearing — close,
   linger, reopen-at-lower-price, state-class flips — re-renders
   the hero but does NOT carry .lot-changed, so the keyframe stays
   dormant. */
.live-hero .live-hero-lot-no.lot-changed {
    animation: live-hero-lot-pulse 0.7s ease-out;
}
.live-hero .live-hero-lot-no .k {
    font-family: var(--font-sans);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: rgba(250, 246, 238, 0.55);
    line-height: 1;
}
.live-hero .live-hero-lot-no .v {
    font-family: var(--font-mono);
    font-size: 24px;
    font-weight: 700;
    letter-spacing: -1px;
    color: var(--paper);
    line-height: 1.05;
    margin-top: 4px;
}
/* Hero Brand cell (desktop lot-no-col / mobile lot-col): farm brand with the
   NZFAP badge beside it, same pairing as the lot table's row 2. Kept out of
   the centred Lot No block above so the label/number stay on one axis. The
   brand ellipsises rather than pushing the badge out of the cell. */
.live-hero-info .c .v.lot-brand {
    display: flex;
    align-items: baseline;
    gap: 5px;
    min-width: 0;
}
.live-hero-info .c .v.lot-brand .brand {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.live-hero-info .c .v.lot-brand .nzfap {
    flex: none;
    color: rgba(250, 246, 238, 0.55);
    font-size: 60%;
    font-weight: 500;
    letter-spacing: 0.04em;
}
@keyframes live-hero-lot-pulse {
    0%   { transform: scale(1);    filter: brightness(1); }
    35%  { transform: scale(1.30); filter: brightness(1.6); }
    100% { transform: scale(1);    filter: brightness(1); }
}
/* Mobile hero info — keep the 24-unit row-desc split that the
   mobile lot card uses (Type 8u | Description 8u | Price 8u) so
   the hero cells line up under the table headers. The Price slot
   renders empty in the hero (bid info lives in the bid section). */

/* Current bid | countdown row laid out in two equal halves so the
   price's right edge + the countdown's left edge both sit against
   the hero's vertical midline (with a gap between). The .bid-status
   row spans both columns underneath, hosting the centered leader
   line + reserve indicator. */
.live-hero-bid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 32px;
    row-gap: 10px;
    align-items: start;
    padding-top: 2px;
    /* No top border — the .live-hero-divider above the lot-no is
       the only divider in the hero now; this section reads as part
       of the same block as the lot-no header above it. */
}
.live-hero-bid .bid-current { text-align: right; }
.live-hero-bid .bid-countdown { text-align: left; }
.live-hero-bid .bid-current .k,
.live-hero-bid .bid-countdown .k {
    display: block;
    font-family: var(--font-sans);
    font-size: 10px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: rgba(250,246,238,0.55);
    font-weight: 500;
    margin-bottom: 4px;
}
.live-hero-bid .bid-current .v {
    font-family: var(--font-mono);
    letter-spacing: -1px;
    font-size: 27px;
    font-weight: 700;
    color: var(--paper);
    line-height: 1;
    font-variant-numeric: tabular-nums;
}
/* Price digits sit in .amt so the currency prefix (.cur) doesn't
   scale with them. inline-block lets transform apply; origin centre
   keeps the pulse symmetrical rather than pushing out sideways. */
.live-hero-bid .bid-current .v .amt {
    display: inline-block;
    transform-origin: center center;
}
/* One-shot pulse when the server renders the hero with a fresh
   (higher) price — the element is freshly-swapped each time, so
   the animation runs on entrance automatically. */
@keyframes live-hero-price-bump {
    0%   { transform: scale(1);    filter: brightness(1); }
    12%  { transform: scale(1.18); filter: brightness(1.6); }
    100% { transform: scale(1);    filter: brightness(1); }
}
.live-hero-bid .bid-current .v .amt.bumped {
    animation: live-hero-price-bump 0.3s ease-out;
}
/* Inverse pulse for the rare case where a passed-in rolling lot
   reopens at a lower starting price for a second round. Scale
   shrinks instead of expanding and the brightness dips, so the
   visual reads as "the price just got smaller" rather than the
   energetic upward bump. */
@keyframes live-hero-price-drop {
    0%   { transform: scale(1);    filter: brightness(1); }
    12%  { transform: scale(0.85); filter: brightness(0.65); }
    100% { transform: scale(1);    filter: brightness(1); }
}
.live-hero-bid .bid-current .v .amt.dropped {
    animation: live-hero-price-drop 0.3s ease-out;
}
.live-hero-bid .bid-current .v .cur {
    font-size: 14px;
    color: rgba(250,246,238,0.6);
    margin-right: 6px;
}
/* Centered leader + reserve indicator row that sits under the
   price/countdown. Spans both columns. Holds its space with
   min-height so the buttons below don't jump when the first bid
   lands. */
.live-hero-bid .bid-status {
    grid-column: 1 / -1;
    text-align: center;
    min-height: 44px;
    padding-top: 2px;
}
.live-hero-bid .bid-status .bidder {
    font-family: var(--font-mono);
    letter-spacing: -1px;
    font-size: 17px;
    font-weight: 600;
    line-height: 1.25;
    color: rgba(250, 246, 238, 0.85);
    min-height: 22px;
}
.live-hero-bid .bid-status .reserve-indicator {
    font-family: var(--font-sans);
    font-size: 15px;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    line-height: 1.2;
    min-height: 18px;
    margin-top: 6px;
    color: rgba(250, 246, 238, 0.65);
}
.live-hero-bid .bid-status .reserve-indicator.reserve-met { color: oklch(72% 0.14 148); }
.live-hero-bid .bid-status .reserve-indicator.lot-withdrawn,
.live-hero-bid .bid-status .reserve-indicator.below-reserve { color: oklch(78% 0.13 65); }
.live-hero-bid .bid-countdown .countdown {
    font-family: var(--font-mono);
    font-size: 27px;
    font-weight: 700;
    color: oklch(70% 0.13 30);
    letter-spacing: -1px;
    text-align: left;
    line-height: 1;
    font-variant-numeric: tabular-nums;
}

/* Live-hero state variants, driven by whether the user has a bid on
   the currently-selling lot:
     .leading → user is leading and reserve met  (green)
     .outbid  → user has bid but is not leading  (red)
     .pending → user is leading, reserve not met (honey)
   Each variant recolours the sage splash on the hero, plus the
   current-bid price and the Leading: line. */
/* State-tinted text colours pulled from the same softer oklch
   palette that drives the reserve-met / below-reserve indicators
   and the "closes in" countdown. Keeps the hero's status language
   (price + leader line) visually coherent with the reserve
   indicator directly beneath it, instead of clashing brighter
   hex reds/greens against that softer palette. */
.live-hero.leading::before {
    background: linear-gradient(120deg, transparent 20%, rgba(22, 163, 74, 0.22) 50%, transparent 80%);
}
.live-hero.leading .bid-current .v,
.live-hero.leading .bid-status .bidder { color: oklch(72% 0.14 148); }

.live-hero.outbid::before {
    background: linear-gradient(120deg, transparent 20%, rgba(220, 38, 38, 0.22) 50%, transparent 80%);
}
.live-hero.outbid .bid-current .v,
.live-hero.outbid .bid-status .bidder { color: oklch(70% 0.13 30); }

.live-hero.pending::before {
    background: linear-gradient(120deg, transparent 20%, rgba(217, 119, 6, 0.26) 50%, transparent 80%);
}
.live-hero.pending .bid-current .v,
.live-hero.pending .bid-status .bidder { color: oklch(78% 0.13 65); }

/* Lingering closed state — used between the moment the selling lot
   closes and the next live lot taking over, so the hero doesn't flash
   empty. A coloured overlay (same ink as the background, 0.15 alpha)
   sits above the content to mute its contrast — reads as "darker,
   less active" without touching opacity (the previous opacity-out
   actually washed the card brighter against the page background).
   Buttons stay disabled / un-clickable; the pulsing hero-label dot
   freezes. */
.live-hero.is-closed::after {
    content: '';
    position: absolute;
    inset: 0;
    background: var(--ink);
    opacity: 0.30;
    border-radius: var(--radius-lg);
    pointer-events: none;
    /* Above .live-hero > * (z-index: 1) so the tint covers all the
       hero content; below interactive layers if any are introduced
       above this in future. */
    z-index: 2;
}
.live-hero.is-closed .hero-label .d {
    animation: none;
    background: var(--ink-4);
    box-shadow: none;
}
.live-hero.is-closed .bid-btn {
    opacity: 0.4;
    pointer-events: none;
    cursor: not-allowed;
}

/* Bid-increment buttons: one big +0.01 up top, then a row of
   +0.02 / +0.05 / +0.10 underneath. Clicking any button posts a
   bid for the current price + increment. */
.live-hero-actions {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-top: 4px;
    /* Anchors the transient .hero-bid-notice strip above the buttons. */
    position: relative;
}

/* Bid-outcome notice (wolxShowBidNotice): a transient strip floated just
   above the bid buttons, overlaying the Leading/Reserve line for a few
   seconds. Amber = race loss (normal auction flow, buttons stay live);
   red = failure (network, sale/lot state, permissions). Client-owned —
   re-painted across hero swaps, removed on expiry. */
.hero-bid-notice {
    position: absolute;
    bottom: 100%;
    left: 0;
    right: 0;
    margin-bottom: 6px;
    padding: 8px 12px;
    border-radius: 8px;
    font-size: 12.5px;
    font-weight: 600;
    line-height: 1.35;
    text-align: center;
    z-index: 3;
    pointer-events: none;
}
.hero-bid-notice.amber {
    background: #FFFBEB;
    border: 1px solid #FACC6B;
    color: #B45309;
}
.hero-bid-notice.red {
    background: #FEF2F2;
    border: 1px solid #FCA5A5;
    color: #DC2626;
}
.live-hero-actions .bid-btn {
    font-family: var(--font-sans);
    font-weight: 700;
    border: none;
    border-radius: 10px;
    cursor: pointer;
    letter-spacing: 0.02em;
    transition: background 0.15s, opacity 0.125s ease-out, filter 0.125s ease-out;
}
/* Disabled state: used while a bid request is in flight (wolxSubmitBid
   toggles `disabled` on every hero bid button until the ws replaces
   the hero or the request fails). Short fade both ways so the dip
   feels like feedback rather than a sudden change. */
/* Snap invisibly to 0 when a bid is in flight — no fade-out, so the
   lockout reads instantly. Transition only applies coming back, so
   the button fades back in once the ws update lands. */
.live-hero-actions .bid-btn:disabled,
.live-hero-actions .bid-btn[disabled] {
    opacity: 0.35;
    filter: saturate(0.6);
    cursor: not-allowed;
    transition: none;
}
.live-hero-actions .bid-btn-main {
    background: var(--win);
    color: var(--ink);
    padding: 18px 20px;
    font-size: 22px;
}
.live-hero-actions .bid-btn-main:hover {
    background: oklch(60% 0.12 148);
}
.live-hero-actions .bid-quick-row {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 6px;
}
.live-hero-actions .bid-btn-quick {
    background: rgba(250,246,238,0.08);
    border: 1px solid rgba(250,246,238,0.18);
    color: var(--paper);
    padding: 10px 12px;
    font-size: 14px;
}
.live-hero-actions .bid-btn-quick:hover {
    background: rgba(250,246,238,0.14);
}

/* Sale-start countdown banner between the sale card and the lots
   list, shown when the auction hasn't started yet. */
.sale-start-banner {
    max-width: 900px;
    margin: 0 auto 16px;
    padding: 10px 14px;
    background: #EDF1F9;
    border: 1px solid rgba(75, 115, 209, 0.35);
    border-radius: var(--radius-lg);
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    color: #1E3A8A;
}
.sale-start-banner .k {
    font-family: var(--font-sans);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}
.sale-start-banner .countdown {
    font-family: var(--font-mono);
    font-size: 20px;
    font-weight: 700;
    letter-spacing: -1px;
    color: #1E3A8A;
    font-variant-numeric: tabular-nums;
}

.section-bar-row td { padding: 0 !important; background: transparent !important; }
.section-bar {
    padding: 10px 16px;
    background: var(--paper-2);
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ink-3);
    border-top: 1px solid var(--line);
    border-bottom: 1px solid var(--line);
    display: flex;
    align-items: center;
    gap: 10px;
}
.section-bar .count {
    font-family: var(--mono);
    font-weight: 500;
    letter-spacing: -1px;
    color: var(--ink-4);
}
.section-bar.live-sec {
    background: linear-gradient(90deg, var(--win-bg) 0%, var(--paper-2) 60%);
    color: var(--win);
}
.section-bar.live-sec .d {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--win);
    animation: pulse-live 1.5s infinite;
    box-shadow: 0 0 0 3px oklch(55% 0.10 148 / 0.3);
}

.buyer-code {
    font-weight: 700;
    margin-left: 2px;
    font-family: var(--mono);
    letter-spacing: -1px;
}

.live-pill {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: var(--win);
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.1em;
    margin-left: 8px;
}
.live-pill .d {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--win);
    animation: pulse-live 1.5s infinite;
    box-shadow: 0 0 0 3px oklch(55% 0.10 148 / 0.3);
}

/* Empty states */
/* ============================================================
   MOBILE LOT "TABLE" — connected multi-row entries that mirror
   the desktop lot-table columns. Visible at ≤820px only.
   ============================================================ */
.m-lot-list { display: none; }

.m-lot-table {
    /* Transparent container so the page's paper background shows
       through the 3px gaps between the header, section bars and
       entries. Each child renders as its own bordered card. */
    background: transparent;
    border: none;
    overflow: visible;
    font-family: var(--font-sans);
    max-width: 450px;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    gap: 3px;
}

/* Mobile header is its own bordered card sitting above the lot
   entries. gray-200 border + full radius match the entry cards. */
.m-lot-header {
    background: linear-gradient(135deg, var(--ink) 0%, var(--ink-2) 100%);
    color: rgba(255,255,255,0.9);
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-lg);
    padding: 12px 12px 10px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.m-lot-header .m-lot-row + .m-lot-row {
    padding-top: 6px;
    border-top: 1px solid rgba(255,255,255,0.08);
}
.m-lot-header .m-lot-row .c {
    font-family: var(--font-mono);
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: -1px;
    text-transform: uppercase;
    color: rgba(255,255,255,0.9);
    font-variant-numeric: normal;
}
.m-lot-header .m-lot-row .c .sub {
    font-family: var(--font-mono);
    font-size: 9.5px;
    letter-spacing: -1px;
    text-transform: none;
    color: rgba(255,255,255,0.55);
    font-weight: 500;
}
/* Price + Status header right-aligns to match the Price / Status
   column's right alignment in the data rows below. */
.m-lot-header .m-lot-row .c.price,
.m-lot-header .m-lot-row .c.price .sub {
    text-align: right;
}

/* Section bar between Upcoming / Live now / Closed groups on mobile.
   Own card — full border + radius — so it visually matches the
   per-row card treatment of the entries and header. */
.m-lot-section-bar {
    padding: var(--space-xs) var(--space-md);
    padding-left: var(--space-lg);
    background: #F9FAFB;
    font-family: var(--font-sans);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--gray-600);
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-lg);
    display: flex;
    align-items: center;
    gap: 10px;
}
.m-lot-section-bar.live-sec {
    background: #EDF1F9;
    color: #4B73D1;
}
.m-lot-section-bar.live-sec .d {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: #4B73D1;
    animation: pulse-live 1.5s infinite;
    box-shadow: 0 0 0 3px rgba(75, 115, 209, 0.25);
}

/* The auction room's ws-diff path keeps the section-bar element in
   the DOM even when the section has no lots, so the hx-swap-oob
   target ids are always addressable. `:empty` hides the bar when
   the server blanks out its inner text on a zero-count tick. */
.lot-list .lot-section-bar:empty,
.m-lot-section-bar:empty {
    display: none;
}

/* The auction room's desktop Upcoming / Closed bodies. Each holds one
   .lot-section per sale (card + pinned headers + rows), so unlike the tables
   they replace they are NOT caught by `.lot-list { display: none }` at the
   mobile breakpoint — they have to be hidden by name, or every sale card
   would render twice on a phone. The mobile bodies need no such rule: they
   live inside .m-lot-list, which the breakpoint already switches. */
.auction-sections {
    display: block;
}
@media (max-width: 875px) {
    .auction-sections { display: none; }
}

/* Quiet last line of a closed sale's group. The room only ever shows a
   sale's most recent closed lots, so this is the way through to the rest —
   deliberately small: it closes the group rather than competing with it. */
.lot-section-footer {
    display: block;
    max-width: 900px;
    margin: 0 auto;
    padding: 6px 14px 7px;
    border: 1px solid var(--gray-200);
    border-top: none;
    border-radius: 0 0 5px 5px;
    background: var(--card);
    font-family: var(--sans);
    font-size: 11.5px;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--ink-4);
    text-align: right;
    text-decoration: none;
}
.lot-section-footer:hover {
    color: var(--ink);
    text-decoration: none;
}
/* The row above it is the run's last, so square the corners where they meet. */
.lot-section:has(> .lot-section-footer) :is(.lot-card, .m-lot-entry):last-child {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}

.m-lot-entry {
    padding: 10px 12px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    position: relative;
    transition: background 0.12s;
    background: #FFFFFF;
    border: 1px solid var(--gray-200);
    border-radius: 3px;
    cursor: pointer;
}
/* Hover wash on the mobile entry, mirroring the desktop .lot-card: the
   ::after deepens whatever status tint is underneath rather than replacing
   it, and the row content sits above the wash. */
.m-lot-entry::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: transparent;
    transition: background 0.12s ease;
    pointer-events: none;
}
.m-lot-entry .m-lot-row { position: relative; z-index: 1; }
.m-lot-entry:hover::after { background: rgba(22, 32, 46, 0.05); }
/* Lot index floats in the gutter to the left of the entire table.
   The .page's horizontal padding is what reserves the gutter space
   on the mobile layout — see the @media block below. */
.m-lot-entry .index {
    position: absolute;
    left: -28px;
    top: 10px;
    width: 22px;
    text-align: right;
    font-family: var(--font-mono);
    letter-spacing: -1px;
    font-size: var(--text-xs);
    color: var(--gray-600);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    opacity: 0.85;
}

/* Row groups within an entry (or header). 24 equal "unit" tracks so
   every row lives on the same vertical grid:
     Row 1 (row-id):    [Lot No 12u] [Bales 6u] [Net Wgt 6u]
     Row 2 (row-tests): [Y-Z 3u][S/D 3u][Airflow 3u][VM 3u]
                        [MM 2u][CV 2u][NTK 2u][T 2u][M 2u][B 2u]
     Row 3 (row-desc):  [Type 8u] [Description 8u] [Price+Status 8u]
   The gap is tight (4px) because 24 tracks leaves little slack. */
.m-lot-row {
    display: grid;
    grid-template-columns: repeat(24, minmax(0, 1fr));
    gap: 4px;
    align-items: baseline;
}
.m-lot-row.row-id .c.lot-col { grid-column: span 12; }
.m-lot-row.row-id > .c:not(.lot-col) { grid-column: span 6; }

.m-lot-row.row-tests > .c:nth-child(-n+4) { grid-column: span 3; }
.m-lot-row.row-tests > .c:nth-child(n+5) { grid-column: span 2; }

/* Type over Description (both primary), price on the right spanning both. */
.m-lot-row.row-desc { row-gap: 2px; }
.m-lot-row.row-desc .c.type-col { grid-column: 1 / span 16; grid-row: 1; }
.m-lot-row.row-desc .c.desc-col { grid-column: 1 / span 16; grid-row: 2; }
.m-lot-row.row-desc .c.price { grid-column: 17 / span 8; grid-row: 1 / span 2; align-self: start; }

/* Row-tests alignment: cells 1-4 (Y-Z / S/D / Airflow / VM) left-
   align under the left-aligned Type / Description. Cells 5-10 (the
   six sub-only MM / CV / NTK / T / M / B) right-align so they sit
   under the right-aligned Price / Status. Higher specificity than
   `.m-lot-row .c.num` so it wins regardless of rule ordering. */
.m-lot-row.row-tests > .c,
.m-lot-row.row-tests > .c.num {
    text-align: left;
}
.m-lot-row.row-tests > .c:nth-child(n+5),
.m-lot-row.row-tests > .c.num:nth-child(n+5) {
    text-align: right;
}

/* Really narrow phones (iPhone SE ~375px): ten cells on the tests row
   crush the MM–B values into an unreadable run ("82.013.052.0…"), and
   the inline offer widget outgrows its 8-column price cell. Below 450px
   the tests block reflows onto a shared SIX-column rhythm: the four
   primary tests take the first four slots (two empty slots on the
   right), the six cluster values fill the next line, everything
   left-aligned with no row gap — it reads as one block of stacked value
   rows, with MM sitting directly under Y-Z. All values are nbsp-padded
   to the 4-char "XX.X" shape so the columns decimal-align. Type/
   Description vs Price also rebalances 16:8 → 14:10 so the OFFER/AUTO
   widget fits inside the card. Header rows share the selectors, so the
   column labels re-flow identically. */
@media (max-width: 449px) {
    .m-lot-row.row-tests { row-gap: 0; }
    .m-lot-row.row-tests > .c:nth-child(-n+4) { grid-column: span 4; }
    .m-lot-row.row-tests > .c:nth-child(n+5),
    .m-lot-row.row-tests > .c.num:nth-child(n+5) {
        grid-column: span 4;
        text-align: left;
    }
    /* MM explicitly opens the cluster line at column 1 — without this,
       auto-placement tucks MM + CV into the primary line's two empty
       end slots (leaving them baseline-aligned against the primary
       values, floating above the rest of the cluster). Cells 6-10 then
       flow after it, keeping all six on one line. */
    .m-lot-row.row-tests > .c:nth-child(5),
    .m-lot-row.row-tests > .c.num:nth-child(5) {
        grid-column: 1 / span 4;
    }
    .m-lot-row.row-desc .c.type-col { grid-column: 1 / span 14; }
    .m-lot-row.row-desc .c.desc-col { grid-column: 1 / span 14; }
    .m-lot-row.row-desc .c.price { grid-column: 15 / span 10; }
}

.m-lot-entry .m-lot-row + .m-lot-row {
    padding-top: 6px;
    border-top: 1px dashed var(--gray-200);
}

/* Mobile data rows inherit the same font + colours as the desktop
   .lot-card rows (Red Hat Mono at 12px) so the two views read as
   the same design. */
.m-lot-row .c {
    font-family: var(--font-mono);
    letter-spacing: -1px;
    font-size: 12px;
    color: var(--gray-900);
    min-width: 0;
    font-variant-numeric: tabular-nums;
    line-height: 1.25;
}
.m-lot-row .c.num {
    text-align: right;
}
.m-lot-row .c .sub {
    display: block;
    font-family: var(--font-mono);
    font-size: 12px;
    color: var(--gray-400);
    font-weight: 400;
    margin-top: 1px;
    letter-spacing: -1px;
    text-transform: none;
}
/* Header keeps its smaller, uppercase sub labels — they're column
   descriptors rather than data. */
.m-lot-header .m-lot-row .c .sub {
    font-family: var(--font-mono);
    font-size: 9.5px;
    color: rgba(255,255,255,0.55);
    font-weight: 500;
    letter-spacing: -1px;
    text-transform: none;
}

/* Entry-specific cell colours — scoped to .m-lot-entry so they don't
   fight the dark-header cell colours above. */
.m-lot-entry .m-lot-row .c.lot-col {
    font-weight: 600;
    font-size: 14px;
    color: var(--gray-900);
}
/* Type / Description cells inherit font-family: var(--font-mono) from
   .m-lot-row .c (matching the desktop .lot-card-row1 spans). */
.m-lot-entry .m-lot-row.row-desc .c.type-col,
.m-lot-entry .m-lot-row.row-desc .c.desc-col {
    font-size: 12px;
    color: var(--gray-900);
    /* Long types/descriptions BREAK to a new line rather than ellipsising
       away data; tight leading keeps a wrapped value reading as one item
       (Type on line 1, Description below it). */
    white-space: normal;
    line-height: 1.15;
}
.m-lot-entry .m-lot-row .c.price {
    font-weight: 700;
    font-size: 14px;
    color: var(--gray-900);
    text-align: right;
    white-space: nowrap;
}
.m-lot-entry .m-lot-row .c.price.empty::after {
    content: '—';
    color: var(--gray-400);
}
/* Status lives as a sub under Price. Matches the desktop .lot-card
   .status styling — var(--font-sans), uppercase, 600 weight, right-
   aligned. */
.m-lot-entry .m-lot-row .c.price .sub {
    font-family: var(--font-sans);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    text-align: right;
    color: var(--gray-500);
    font-variant-numeric: normal;
    white-space: normal;
}

/* Sub-only cells (row 2, cols 5 and 6) have no primary value. A
   ::before placeholder occupies the primary line so the sub line
   drops to the second row and aligns vertically with the sub values
   in the neighbouring cells (Col Y / 16% / L/S / OFDA). */
.m-lot-entry .m-lot-row .c.only-sub::before,
.m-lot-header .m-lot-row .c.only-sub::before {
    content: '\A0';
    display: block;
}
.m-lot-entry .m-lot-row .c.only-sub .sub,
.m-lot-header .m-lot-row .c.only-sub .sub {
    white-space: nowrap;
}
/* …but on the narrow-phone layout the cluster owns a whole line (see the
   449px block up by .m-lot-row.row-tests), so the placeholder would just
   inject a 16px void above every value — drop it there. Lives HERE, after
   the base rule, because the same-specificity override must come later in
   the file. */
@media (max-width: 449px) {
    .m-lot-entry .m-lot-row .c.only-sub::before,
    .m-lot-header .m-lot-row .c.only-sub::before {
        content: none;
    }
}

/* Entry state variants — same hex colours the desktop .lot-card uses,
   so both views produce identical state washes. Includes the 3px
   inset accent stripe on the left. */
.m-lot-entry.live {
    background: #EDF1F9;
}
.m-lot-entry.live .c.lot-col,
.m-lot-entry.live .c.price,
.m-lot-entry.live .c.price .sub { color: #4B73D1; }

.m-lot-entry.won {
    background: #F7FDF9;
}
.m-lot-entry.won .c.price,
.m-lot-entry.won .c.price .sub { color: #16A34A; }

.m-lot-entry.lost {
    background: #FEF2F2;
}
.m-lot-entry.lost .c.price,
.m-lot-entry.lost .c.price .sub { color: #DC2626; }

.m-lot-entry.passed {
    background: #FFFBEB;
}
.m-lot-entry.passed .c.price,
.m-lot-entry.passed .c.price .sub { color: #D97706; }

/* Full-height left accent — see note on .lot-card. */
.m-lot-entry.live::before,
.m-lot-entry.won::before,
.m-lot-entry.lost::before,
.m-lot-entry.passed::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 3px;
    border-radius: 0;
    border-top-left-radius: inherit;
    border-bottom-left-radius: inherit;
}
.m-lot-entry.live::before   { background: #4B73D1; }
.m-lot-entry.won::before    { background: #16A34A; }
.m-lot-entry.lost::before   { background: #DC2626; }
.m-lot-entry.passed::before { background: #D97706; }

.m-lot-entry.withdrawn {
    opacity: 0.6;
}
.m-lot-entry.withdrawn .c.price .sub {
    color: var(--gray-500);
}

/* Live badge sits inside the Price sub. Matches the desktop
   .lot-card .status .live-badge exactly. */
.m-lot-row .c.price .sub .live-badge {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: #4B73D1;
}
.m-lot-row .c.price .sub .live-badge .d {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: #4B73D1;
    animation: pulse-live 1.5s infinite;
    box-shadow: 0 0 0 3px rgba(75, 115, 209, 0.25);
}

/* ============================================================
   MOBILE TOPBAR + SIDEBAR DRAWER
   Desktop: topbar is hidden, sidebar is the fixed 240px column.
   Mobile (≤820px): sidebar becomes an off-canvas drawer, a slim
   ink topbar surfaces a hamburger to toggle it.
   ============================================================ */
.mobile-topbar {
    display: none;
    position: sticky;
    top: 0;
    z-index: 25;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    background: var(--ink);
    color: var(--paper);
}
.mobile-menu-btn {
    display: grid;
    place-items: center;
    width: 36px;
    height: 36px;
    border-radius: 8px;
    background: rgba(250,246,238,0.08);
    color: var(--paper);
    border: none;
    cursor: pointer;
    flex-shrink: 0;
    font-family: inherit;
}
.mobile-menu-btn:hover {
    background: rgba(250,246,238,0.14);
}
.mobile-topbar-brand {
    font-size: 14px;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--paper);
    flex: 1;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.mobile-topbar .legacy-switch-link {
    border-right-color: rgba(250,246,238,0.2);
    color: rgba(250,246,238,0.65);
    padding-right: 10px;
}
.mobile-topbar .legacy-switch-link:hover {
    color: var(--paper);
}
.mobile-topbar-clock {
    display: flex;
    align-items: center;
    gap: 6px;
    font-family: var(--mono);
    letter-spacing: -1px;
    font-size: 12px;
    color: rgba(250,246,238,0.75);
    font-variant-numeric: tabular-nums;
    flex-shrink: 0;
}
.mobile-topbar-clock .live-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--win);
    box-shadow: 0 0 0 3px oklch(55% 0.10 148 / 0.25);
    animation: pulse-live 2s infinite;
}

.sidebar-scrim {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(22,32,46,0.55);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
    z-index: 40;
    opacity: 0;
    transition: opacity 0.2s ease-out;
    pointer-events: none;
}

/* ============================================================
   RESPONSIVE — staged across four breakpoints
     >1140px:  desktop shell (260px sidebar + desktop lot table)
     ≤1140px:  sidebar shrinks to 200px; rest of the layout unchanged
     ≤1075px:  sidebar becomes an off-canvas drawer; mobile topbar
               appears with a hamburger button (full viewport for
               page content)
     ≤875px:   switch to the mobile lot table; tighten page density
     ≤560px:   extra-narrow phone tweaks
   ============================================================ */

/* Stage 1 — sidebar shrinks from 260px to 200px, but everything
   else stays at its desktop layout (table view, full-size page
   layout). Content area at 1140px viewport is 1140-200-64=876, which
   still comfortably holds the 900px-max lot table. */
@media (max-width: 1140px) {
    .app {
        grid-template-columns: 200px 1fr;
    }
    .sidebar {
        padding: 18px 14px;
        gap: 22px;
    }
}

/* Stage 2 — sidebar becomes an off-canvas drawer; mobile topbar
   replaces it. Page content now has the full viewport to work with,
   so the desktop lot table stays in view until the ≤875px block
   below (~875 = 1075 − the 200px the sidebar used to occupy). */
@media (max-width: 1075px) {
    .app {
        grid-template-columns: 1fr;
    }
    .sidebar {
        position: fixed;
        top: 0;
        left: 0;
        height: 100dvh;
        width: 260px;
        max-width: 82vw;
        padding: 22px 18px;
        transform: translateX(-100%);
        transition: transform 0.22s ease-out;
        z-index: 50;
        box-shadow: none;
    }
    body.sidebar-open .sidebar {
        transform: translateX(0);
        box-shadow: var(--shadow-wol-lg);
    }
    body.sidebar-open .sidebar-scrim {
        display: block;
        opacity: 1;
        pointer-events: auto;
    }
    body.sidebar-open {
        overflow: hidden;
    }

    .mobile-topbar { display: flex; }
    .topbar { display: none; }
}

/* Stage 3 — switch to the mobile lot table and tighten page density.
   This used to fire at ≤1075px; now that the sidebar is hidden at
   that breakpoint we get ~200px of extra room back and can keep the
   desktop table until ≤875px. */
@media (max-width: 875px) {
    /* Lot table switch: hide the desktop grid variants, show the
       connected mobile lot table (.m-lot-list). */
    .lot-list { display: none; }
    .m-lot-list { display: block; }
    /* A mobile lot entry is a three-line card, so the pinned head has to
       give back more of it before the last row could be hidden. */
    .lot-section { --lot-section-release: 84px; }
    /* Auction-room section bars (Upcoming / Closed) are rendered
       once for desktop and once inside .m-lot-list for mobile. The
       desktop pair sits outside .lot-list so the .lot-list { display:
       none } above doesn't catch them — hide them explicitly here so
       only the mobile bars show. */
    .lot-section-bar.lot-section-outer { display: none; }

    /* Sale card on the sale detail page: cap at the same 450px the
       mobile lot table uses and centre it so the two line up. */
    .sale-card-grid.sale-card-solo {
        max-width: 450px;
        margin: 0 auto 16px;
    }

    /* One step down with the rest of the card's small type. */
    .sale-card-message { font-size: 10px; }
    .sale-actions {
        max-width: 450px;
    }

    /* Mirror page padding on both sides so the floating lot-index
       gutter is balanced — the .index sits in the left gutter via
       absolute positioning relative to the entry. */
    .page {
        padding: 18px 32px 40px;
    }
    .page-header {
        flex-direction: column;
        align-items: stretch;
        gap: 12px;
        margin-bottom: 18px;
    }
    .page-title,
    .page-header-left h1 {
        font-size: 24px;
    }
    .page-subtitle {
        font-size: 13px;
    }
    .page-actions {
        flex-wrap: wrap;
    }

    /* Sales dashboard */
    .sale-card-list,
    .sale-card-grid {
        gap: 10px;
    }
    .auction-room-cta {
        flex-direction: column;
        align-items: stretch;
        gap: 8px;
        padding: 12px 14px;
    }
    /* Stacked banner: the action is a filled button, so it hugs its text at
       the right rather than stretching to a full-width slab. */
    .auction-room-cta .cta-action {
        align-self: flex-end;
        text-align: center;
    }
    /* The pinned sale-detail CTA stays a SINGLE row on narrow screens —
       the stacked variant above would eat two lines of the lot list for
       the whole scroll. The sale name truncates instead. */
    .sales-cta-slot.sticky .auction-room-cta {
        flex-direction: row;
        align-items: center;
        gap: 10px;
        padding: 7px 10px;
    }
    .sales-cta-slot.sticky .auction-room-cta .cta-main {
        font-size: 13px;
        overflow: hidden;
    }
    .sales-cta-slot.sticky .auction-room-cta .cta-main > span:last-child {
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    .sales-cta-slot.sticky .auction-room-cta .cta-action {
        text-align: center;
        flex: 0 0 auto;
        font-size: 12.5px;
    }
    .sales-section-label {
        margin: 16px 0 8px;
    }
    .sales-table {
        font-size: 12px;
    }
    .sales-table th,
    .sales-table td {
        padding: 10px 10px;
    }

    /* Auction legend compact */
    .legend {
        gap: 10px 14px;
        font-size: 11px;
    }

    /* Live hero stacks. Drop max-width + auto-margins so the hero
       stretches to match the 450px .m-lot-table it now sits inside,
       giving consistent left/right edges down the page. */
    .live-hero-wrap {
        max-width: none;
        margin: 0;
        width: 100%;
    }
    .live-hero-wrap > .row-number {
        left: -28px;
        width: 22px;
    }
    .live-hero {
        padding: 13px;
        gap: 10px;
        max-width: none;
        margin: 0;
        width: 100%;
    }
    /* Slightly smaller hero numerals on the narrow mobile card. */
    .live-hero .live-hero-lot-no .v { font-size: 22px; }
    .live-hero-bid .bid-current .v { font-size: 24px; }
    .live-hero-bid .bid-countdown .countdown { font-size: 24px; }
    /* Note text tracks the testing-detail values' mobile size (12.5px). */
    .hero-user-meta .hero-note { font-size: 12.5px; }

    /* One step down on a phone, for the sale name and for the pair that
       shares the card's first line — which stay matched to each other. */
    .sale-card h3 { font-size: 14px; }
    .sale-card .sale-indicator,
    .sale-card .type-label { font-size: 10px; }
}

@media (max-width: 560px) {
    .page {
        padding: 14px 28px 40px;
    }
    .page-title,
    .page-header-left h1 {
        font-size: 22px;
    }

    /* Closed sales table: hide type + location columns on very narrow */
    .sales-table th:nth-child(3),
    .sales-table td:nth-child(3),
    .sales-table th:nth-child(4),
    .sales-table td:nth-child(4) {
        display: none;
    }

    /* Shrink legend further */
    .legend {
        gap: 8px 10px;
        font-size: 10.5px;
    }
    .legend-swatch {
        width: 10px;
        height: 10px;
    }
}

/* Small phones (e.g. iPhone SE, 375×667): compact the live hero VERTICALLY —
   horizontal spacing is already good, but the selling area ran too tall to see
   the bid controls without scrolling. Trims padding, gaps and vertical margins
   and steps the big numerals down ~2px. No horizontal changes; last in source
   order so it wins over the 875px hero rules at these widths. */
@media (max-width: 480px) {
    .live-hero {
        padding: 10px 12px;
        gap: 7px;
    }
    .live-hero .hero-head { gap: 2px; }
    .hero-user-meta { margin-top: 5px; gap: 6px 10px; }
    .live-hero-info { gap: 5px; }
    /* Tighten the label/value leading (not the font sizes) so each two-value
       info cell packs down — the biggest vertical block on the card. */
    .live-hero-info .m-lot-row .c .k { margin-bottom: 1px; line-height: 1.1; }
    .live-hero-info .m-lot-row .c .v { line-height: 1.1; }
    .live-hero-info .m-lot-row .c .sub { margin-top: 2px; line-height: 1.15; }
    .live-hero .live-hero-divider { margin-top: 8px; }
    .live-hero .live-hero-lot-no { margin: 6px auto 5px; }
    .live-hero .live-hero-lot-no .v { font-size: 20px; margin-top: 2px; }
    .live-hero-bid { row-gap: 7px; }
    .live-hero-bid .bid-current .v { font-size: 22px; }
    .live-hero-bid .bid-countdown .countdown { font-size: 22px; }
    .live-hero-bid .bid-status { min-height: 30px; }
    .live-hero-bid .bid-status .bidder { min-height: 18px; }
    .live-hero-bid .bid-status .reserve-indicator { margin-top: 4px; }
    .live-hero-actions { gap: 6px; margin-top: 2px; }
    .live-hero-actions .bid-btn-main { padding: 13px 18px; font-size: 20px; }
    .live-hero-actions .bid-btn-quick { padding: 8px 12px; }
}

/* ── QA round 2 · Package 3 (tenders) ─────────────────────────────────────
   Appended at end of file to keep merges with Package 2 trivial. */

/* (Pre-open tender: the disabled offer input now lives inline on the lot row
   — see .lot-row-bid-input:disabled — so the old .lot-dialog-tender-pending
   panel CSS was removed with the accordion bid panels.) */

/* Stacked sale sections (Open Tenders, Watch List, Bid List, Search, and the
   single-sale page): one section per sale — its sale card then its lots
   table inline — open sales first, then closed sales (no group labels; the
   card's CLOSED state marks them). The card sits directly on its table (no gap, bottom edge squared
   onto the dark header), so a card can only
   read as belonging to the lots below it. */
/* The same 10px pitch the All Sales list uses. Sections used to be held well
   apart so a card read as belonging to the table under it rather than the one
   above; the pinned head now says which sale you are in at all times, so that
   separation has nothing left to do and the list can close up. */
.lot-section {
    margin: 0 0 10px;
}
.lot-section .sale-card-grid.sale-card-solo {
    margin-bottom: 0;
}
/* Square the card's foot onto whatever it heads — but ONLY when it heads
   something. A card-only group (a closed sale whose lots fell outside the
   room's window) has nothing below it to join, so it keeps all four corners
   and its bottom border, like any free-standing card. */
.lot-section:has(:is(.lot-card, .m-lot-entry, .lot-section-footer)) .sale-card {
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    border-bottom: none;
}
.lot-section .lot-header:first-child,
.lot-section .m-lot-header {
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}
/* Pinned section head: the sale card and its column headers ride under the
   top chrome while their section scrolls by, so on a long list the buyer
   always knows which sale a row is in and what its columns mean. The head is
   bounded by its .lot-section, so it scrolls off with the section's last row
   and the next section's head takes over. Under the top chrome (topbar 20,
   banners 18/19) but above the rows passing beneath it.

   The paper background, plus two offset shadows reaching 32px into the page
   gutters (shadows never add scroll overflow), hide the rows AND their
   floating index numbers as they pass under — the head reads as solid from
   gutter to gutter. */
.lot-section-head {
    position: sticky;
    top: var(--wolx-chrome-bottom, 56px);
    z-index: 16;
    background: var(--paper);
    box-shadow: -32px 0 0 var(--paper), 32px 0 0 var(--paper);
    container-type: scroll-state;
}
/* Release the pin one row early. A sticky box is held inside its containing
   block by its MARGIN box, so this bottom margin makes the head start
   scrolling away while a row of the section is still below it — otherwise you
   can reach a position where the head has eaten the last lot and the section
   shows nothing but its own header. The margin is empty space; the rows are
   pulled back over it below, so nothing moves.

   Only for a section that actually has rows: a card-only group (a closed sale
   whose lots fell outside the room's window, a sale still loading) has nothing
   to keep in view, and the pull would drag its footer up over the card. */
.lot-section:has(:is(.lot-card, .m-lot-entry)) > .lot-section-head {
    margin-bottom: var(--lot-section-release);
}
/* Both tables are named: they are siblings of the head, and whichever one the
   breakpoint hides is out of layout, so only the visible table's pull counts. */
.lot-section:has(:is(.lot-card, .m-lot-entry)) > .lot-section-head + *,
.lot-section:has(:is(.lot-card, .m-lot-entry)) > .lot-section-head ~ .m-lot-list {
    margin-top: calc(var(--lot-section-release) * -1);
}
/* Roughly one desktop lot row (two ~16px lines plus padding); the mobile
   entry is a three-line card, so it claims more further down. */
.lot-section {
    --lot-section-release: 50px;
}
/* The headers live in the head, apart from the rows, so re-join them: square
   header foot, first row flush on it. (Body tables carry no headers inside a
   .lot-section, so a first body row is the first child.) */
.lot-section-head .m-lot-header {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}
.lot-section .lot-list > div:first-child > .lot-card:first-child,
.lot-section .m-lot-table > .m-lot-entry:first-child {
    margin-top: 0;
    border-top: none;
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}
/* No rows yet (the sale page's "Loading lots…", an empty sale): no headers. */
.lot-section:has(.lot-section-empty) > .lot-section-head :is(.lot-list, .m-lot-list) {
    display: none;
}
/* While pinned, the header foot casts a shadow over the rows sliding under
   it. Progressive: browsers without scroll-state queries just skip it. */
@container scroll-state(stuck: top) {
    .lot-header:nth-child(2),
    .m-lot-header {
        box-shadow: 0 8px 12px -8px rgba(0, 0, 0, 0.35);
    }
    /* On a phone the pinned head is already eating a big share of the screen,
       and a long broker message can run to several lines. Once the head is
       actually stuck, clip the message to one line — the buyer has read it by
       then, and the ellipsis says there is more. It opens back up as soon as
       the head unpins. Desktop has the room, so it keeps the full message. */
    @media (max-width: 875px) {
        .lot-section-head .sale-card-message {
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
    }
}
/* Tight lot tables: consecutive lot rows (desktop .lot-card, mobile
   .m-lot-entry) sit flush — no gap, one shared 1px rule between them, square
   corners in the middle of a run. Only a run's first row keeps its subtle
   top corners and its last row its bottom corners (a run ends at a section
   bar, a sale divider, an open detail panel or the end of the list). Status
   accent bars inherit the row's left corners, so they follow suit. */
.lot-card + .lot-card,
.m-lot-entry + .m-lot-entry {
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}
.lot-card:has(+ .lot-card),
.m-lot-entry:has(+ .m-lot-entry) {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}
.lot-card + .lot-card {
    margin-top: 0;
    border-top: none;
}
/* The mobile table is a flex column with a 3px gap — pull each following
   entry back up onto the one above. */
.m-lot-entry + .m-lot-entry {
    margin-top: -3px;
    border-top: none;
}
/* ...and the first row sits flush on the dark column header (desktop rows
   live in a body div right after the headers; mobile entries follow the
   header directly, or inside an auction-room body wrapper). */
.lot-header + div > .lot-card:first-child,
.m-lot-header + .m-lot-entry,
.m-lot-header + div > .m-lot-entry:first-child {
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}
.lot-header + div > .lot-card:first-child {
    margin-top: 0;
    border-top: none;
}
.m-lot-header + .m-lot-entry,
.m-lot-header + div > .m-lot-entry:first-child {
    margin-top: -3px;
    border-top: none;
}
.m-lot-header:has(+ .m-lot-entry),
.m-lot-header:has(+ div > .m-lot-entry:first-child) {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}
/* A lot row with its detail panel open below it: square bottom corners (the
   status accent inherits them) where the panel joins on. Keyed off the panel
   wrap's presence via a sibling selector, so a live OOB swap of the row keeps
   the join. */
.lot-card:has(+ .lot-panel-wrap),
.m-lot-entry:has(+ .lot-panel-wrap) {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}
/* With no lots table (loading / empty), the note closes the card off. */
.lot-section .lot-section-empty {
    max-width: 900px;
    margin: 0 auto;
    box-sizing: border-box;
    background: var(--card);
    border: 1px solid var(--gray-200);
    border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}
.lot-section:last-child {
    margin-bottom: 0;
}
.lot-sections-empty {
    padding: 48px;
    text-align: center;
    color: var(--ink-4);
    font-size: 13px;
}
.lot-section-empty {
    padding: 20px 24px;
    text-align: center;
    color: var(--ink-4);
    font-size: 13px;
}
/* A sale with no lots: the card and column headers stay joined as usual, the
   headers close off with rounded corners, and the Live Sales Room's dashed
   empty state (.auction-section-empty) sits under them. */
.lot-section-none {
    max-width: 900px;
    margin: 12px auto 0;
    padding: 24px;
    box-sizing: border-box;
    text-align: center;
    color: var(--ink-4);
    font-size: 13px;
    font-style: italic;
    border: 1px dashed var(--line);
    border-radius: var(--radius-lg);
    background: transparent;
}
.lot-section:has(.lot-section-none) .sale-card {
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    border-bottom: none;
}
.lot-section:has(.lot-section-none) .lot-header:nth-child(2) {
    border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}
.lot-sections-footnote {
    max-width: 900px;
    margin: 24px auto 0;
    text-align: center;
    color: var(--ink-4);
    font-size: 13px;
}
/* The quiet "how far back" note under the list titles. */
.page-subtitle.lot-window-note {
    font-size: 13px;
    color: var(--ink-4);
}

/* Bid List All / Won filter chips. */
.list-chips {
    display: flex;
    gap: 8px;
    max-width: 900px;
    margin: -4px auto 20px;
}
.list-chip {
    padding: 5px 14px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--card);
    color: var(--ink-3);
    font-size: 13px;
    font-weight: 500;
    text-decoration: none;
    transition: all 0.15s;
}
.list-chip:hover {
    border-color: var(--ink-5);
    color: var(--ink);
    text-decoration: none;
}
.list-chip.active {
    background: var(--ink);
    border-color: var(--ink);
    color: var(--paper);
}

/* Search form: text criteria, min–max ranges, sale filters. */
.lot-search {
    max-width: 900px;
    margin: -4px auto 28px;
    padding: 16px 18px;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: 10px;
}
.lot-search-grid {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 12px 16px;
}
.lot-search-field {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}
.lot-search-field.wide {
    grid-column: span 2;
}
.lot-search-label {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--ink-4);
}
.lot-search-input {
    width: 100%;
    min-width: 0;
    box-sizing: border-box;
    padding: 7px 10px;
    border: 1px solid var(--line);
    border-radius: 6px;
    background: var(--card);
    color: var(--ink);
    font-family: var(--sans);
    font-size: 14px;
    line-height: 1.3;
}
.lot-search-input[type="number"] {
    font-family: var(--mono);
    font-variant-numeric: tabular-nums;
    -moz-appearance: textfield;
    appearance: textfield;
}
.lot-search-input[type="number"]::-webkit-outer-spin-button,
.lot-search-input[type="number"]::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}
.lot-search-input::placeholder { color: var(--ink-5); }
.lot-search-input:focus {
    outline: none;
    border-color: var(--ink);
    box-shadow: 0 0 0 3px rgba(22, 32, 46, 0.08);
}
.lot-search-range {
    display: flex;
    align-items: center;
    gap: 6px;
}
.lot-search-dash {
    color: var(--ink-5);
    flex: none;
}
.lot-search-actions {
    display: flex;
    gap: 8px;
    margin-top: 14px;
}

/* Sale page: the overflow (⋯) menu sits on the title row, at its right. */
.page-header.with-actions {
    align-items: center;
}
.page-header .sale-actions {
    max-width: none;
    margin: 0;
    padding: 0;
    flex-shrink: 0;
}

@media (max-width: 875px) {
    /* Line the list controls up with the 450px mobile cards + lot table. */
    .page.lot-sections .page-header,
    .list-chips,
    .lot-search,
    .lot-sections-footnote,
    .lot-section .lot-section-empty {
        max-width: 450px;
    }
    /* Keep the ⋯ beside the title rather than stacked under it. */
    .page-header.with-actions {
        flex-direction: row;
        align-items: center;
    }
    .lot-search-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
    .lot-search-input {
        font-size: 16px; /* no iOS zoom-on-focus */
    }
}

/* ============================================================
   ADMIN: lot-panel withdraw section
   ============================================================ */
.lot-admin-section { margin-top: 12px; }
.lot-admin-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 12px;
    background: #FFF7ED;
    border: 1px solid #FED7AA;
    border-radius: 6px;
    font-size: 13px;
}
.lot-admin-state {
    display: flex;
    align-items: center;
    gap: 6px;
    flex: 1 1 auto;
}
.lot-admin-label {
    color: var(--ink-3);
    font-weight: 600;
}
.lot-admin-value {
    font-weight: 700;
    color: var(--ink);
}
/* The button contains a label span and a spinner span; only one is
   visible at a time. Both render inline so the button keeps its
   natural size when the spinner shows (the spinner is sized to
   match the label's line height, preventing reflow). */
.lot-admin-btn {
    padding: 6px 14px;
    border: 1px solid #C2410C;
    border-radius: 6px;
    background: #FFF;
    color: #C2410C;
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 0;
}
.lot-admin-btn:hover { background: #FFF7ED; }
.lot-admin-btn:disabled { opacity: 0.6; cursor: default; }
.lot-admin-btn-spinner { display: none; }
.lot-admin-btn.busy .lot-admin-btn-label { display: none; }
.lot-admin-btn.busy .lot-admin-btn-spinner {
    display: inline-block;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    border: 2px solid currentColor;
    border-top-color: transparent;
    animation: btn-spin 0.7s linear infinite;
}
/* Error area below the admin row — hidden until JS adds .visible;
   uses a fixed small height so it never pushes the button around. */
.lot-admin-error {
    font-size: 12px;
    font-weight: 600;
    color: #DC2626;
    padding: 0 12px;
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.15s ease, padding 0.15s ease;
}
.lot-admin-error.visible {
    max-height: 2em;
    padding: 4px 12px 0;
}

/* ============================================================
   ADMIN: sale-level admin section (resequence) — collapsible
   ============================================================ */
.sale-admin {
    max-width: 900px;
    margin: 0 auto 16px;
    border-radius: 8px;
    background: #FFF7ED;
    border: 1px solid #FED7AA;
    font-size: 13px;
}
/* Toggle button: the clickable title bar. Always visible. */
.sale-admin-toggle {
    display: flex;
    align-items: center;
    gap: 6px;
    width: 100%;
    padding: 10px 14px;
    background: none;
    border: none;
    cursor: pointer;
    font-size: 13px;
    font-weight: 700;
    color: #C2410C;
    text-align: left;
}
.sale-admin-toggle:hover { background: rgba(194, 65, 12, 0.04); border-radius: 8px; }
.sale-admin-toggle svg {
    width: 14px;
    height: 14px;
    flex-shrink: 0;
    transition: transform 0.15s ease;
}
.sale-admin.open .sale-admin-toggle svg { transform: rotate(90deg); }
/* Body: collapsed by default, expanded when .open is on the wrapper. */
.sale-admin-body {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows 0.15s ease;
}
.sale-admin.open .sale-admin-body { grid-template-rows: 1fr; }
.sale-admin-inner {
    overflow: hidden;
    min-height: 0;
}
/* Each tool (Privacy, Message, Resequence) is its own section, ruled off
   from the one above and headed by an uppercase label. */
.sale-admin-group {
    padding: 12px 14px;
    border-top: 1px solid #FED7AA;
}
/* A sub-section within a group (Resequence, under Lots): a smaller label,
   set a little apart from the tools above it. */
.sale-admin-subsection { margin-top: 14px; }
.sale-admin-sublabel {
    display: block;
    margin-bottom: 6px;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: #9A3412;
    opacity: 0.8;
}
.sale-admin-label {
    display: block;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: #C2410C;
    margin-bottom: 8px;
}
.sale-admin-buttons {
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.sale-admin-buttons.inline { flex-direction: row; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
/* The Details form's guest visibility row: the dropdown, then how many lots
   guests can see, level with the field. */
.sale-form-visibility-row { margin-top: 12px; }
.sale-details-form > .sale-form-visibility-row:first-child { margin-top: 0; }
.sale-form-visibility-state {
    justify-content: flex-end;
    padding-bottom: 8px;
    font-size: 12px;
    color: var(--ink-3);
}
.sale-admin-privacy-error {
    font-size: 12px;
    font-weight: 600;
    color: #DC2626;
    display: none;
    margin-top: 6px;
}
.sale-admin-privacy-error.visible { display: block; }
/* Guest privacy dropdowns (sale admin panel and lot panel), in the warm
   admin palette. */
.privacy-select {
    padding: 4px 8px;
    border: 1px solid #E8A080;
    border-radius: 4px;
    background: #FFF;
    color: var(--ink);
    font-family: inherit;
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
}
.privacy-select:focus { outline: none; border-color: #C2410C; box-shadow: 0 0 0 3px rgba(194, 65, 12, 0.10); }
.privacy-select:disabled { color: var(--ink-4); background: var(--paper-2); border-color: #FED7AA; cursor: default; }
.lot-admin-note {
    margin: 6px 12px 0;
    font-size: 12px;
    color: var(--ink-3);
}
/* Low-prominence action buttons — small, minimal padding, subdued border.
   Contains a label span + spinner span; .busy swaps visibility. */
.sale-admin-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 3px 10px;
    border: 1px solid #E8A080;
    border-radius: 4px;
    background: #FFF;
    color: #C2410C;
    font-size: 11px;
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
    align-self: flex-start;
}
.sale-admin-btn:hover { background: #FFF7ED; }
.sale-admin-btn:disabled { opacity: 0.5; cursor: default; }
.sale-admin-btn .sale-admin-btn-spinner { display: none; }
.sale-admin-btn.busy .sale-admin-btn-label { display: none; }
.sale-admin-btn.busy .sale-admin-btn-spinner {
    display: inline-block;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    border: 1.5px solid currentColor;
    border-top-color: transparent;
    animation: btn-spin 0.7s linear infinite;
}
.sale-admin-disabled {
    color: var(--ink-3);
    font-size: 11px;
    margin: 0;
}
.sale-admin-error {
    font-size: 12px;
    font-weight: 600;
    color: #DC2626;
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.15s ease, padding 0.15s ease;
}
.sale-admin-error.visible {
    max-height: 2em;
    padding-top: 6px;
}

/* ── Sale details form ───────────────────────────────────────────────────
   Shared by the Manage Sales create panel and the Sale admin panel's
   Details section: a wrapping grid of labelled fields (the guest-code
   form's inputs), the name taking a full row. */
.sale-form-grid {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 10px 12px;
}
.sale-form-field { display: flex; flex-direction: column; min-width: 0; }
.sale-form-field.wide { grid-column: 1 / -1; }
.sale-form-field.span3 { grid-column: span 3; }
.sale-form-field.span2 { grid-column: span 2; }
/* Start and end each begin a row of their own: date, then time. */
.sale-form-field.row-start { grid-column-start: 1; }
/* A Rolling sale ends the day after it starts, so it has no end fields. */
.sale-form-grid.rolling .sale-form-end { display: none; }
@media (max-width: 640px) {
    .sale-form-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .sale-form-field.span3 { grid-column: 1 / -1; }
}
.sale-form-label {
    margin: 0 0 4px;
    font-size: 12px;
    font-weight: 600;
    color: var(--ink-3);
}
.sale-form-input {
    width: 100%;
    box-sizing: border-box;
    padding: 7px 10px;
    border: 1px solid var(--gray-200);
    border-radius: 6px;
    background: var(--card);
    font-family: var(--sans);
    font-size: 13px;
    line-height: 1.4;
    color: var(--ink);
}
.sale-form-input:focus {
    outline: none;
    border-color: var(--ink);
    box-shadow: 0 0 0 3px rgba(22, 32, 46, 0.08);
}
/* A value the form shows but doesn't take (the sale week): an input's box,
   without the input. */
.sale-form-readonly {
    border-style: dashed;
    background: transparent;
    color: var(--ink-3);
    font-variant-numeric: tabular-nums;
}
.sale-form-message { resize: vertical; line-height: 1.5; }
.sale-form-message-row { margin-top: 12px; }
.sale-form-actions {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 10px;
    margin-top: 12px;
}

/* Manage Sales: the create dialog, floated over a scrim like the login
   page's forgot-password dialog. Scrolls itself when taller than the window. */
.manage-sales-dialog, .admin-dialog {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 100;
    align-items: flex-start;
    justify-content: center;
    padding: 48px 16px;
    overflow-y: auto;
    background: rgba(22, 32, 46, 0.55);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
}
.manage-sales-dialog.open, .admin-dialog.open { display: flex; }
body.dialog-open { overflow: hidden; }
.manage-sales-dialog-card, .admin-dialog-card {
    width: 100%;
    max-width: 680px;
    padding: 22px 24px 20px;
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: 14px;
    box-shadow: var(--shadow-wol);
}
.manage-sales-dialog-head, .admin-dialog-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 16px;
}
.manage-sales-dialog-head h2, .admin-dialog-head h2 {
    margin: 0;
    font-size: 18px;
    font-weight: 700;
    letter-spacing: -0.015em;
    color: var(--ink);
}
.manage-sales-dialog-close, .admin-dialog-close {
    padding: 0 4px;
    margin: -4px -4px 0 0;
    border: none;
    background: none;
    color: var(--ink-4);
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
}
.manage-sales-dialog-close:hover, .admin-dialog-close:hover { color: var(--ink); }
/* The sale page's catalogue upload: the same dialog, wider for its preview. */
.admin-dialog-card.wide { max-width: 760px; }
.admin-dialog-text { margin: 0 0 14px; font-size: 13px; line-height: 1.5; color: var(--ink-3); }
.catalogue-upload-file { display: block; width: 100%; font-size: 13px; }
.catalogue-upload-error { margin: 10px 0 0; font-size: 12px; font-weight: 600; color: var(--lose); }
.catalogue-upload-error:empty { display: none; }
.catalogue-preview-summary { margin: 0 0 12px; font-size: 13px; color: var(--ink-3); }
.catalogue-preview-file { font-weight: 600; color: var(--ink); }
.catalogue-preview-errors {
    margin: 0 0 12px;
    padding: 10px 14px;
    border-radius: 6px;
    background: var(--lose-bg);
    color: var(--lose);
    font-size: 13px;
}
.catalogue-preview-errors h3 { margin: 0 0 4px; font-size: 13px; font-weight: 700; }
.catalogue-preview-errors ul,
.catalogue-preview-warnings ul { margin: 0; padding-left: 18px; }
.catalogue-preview-table {
    max-height: 46vh;
    overflow: auto;
    margin: 0 0 12px;
    border: 1px solid var(--line);
    border-radius: 6px;
    background: var(--card);
}
.catalogue-preview-table table { width: 100%; border-collapse: collapse; font-size: 13px; }
.catalogue-preview-table th {
    position: sticky;
    top: 0;
    padding: 6px 10px;
    background: var(--paper-light);
    border-bottom: 1px solid var(--line);
    text-align: left;
    font-size: 11px;
    font-weight: 600;
    color: var(--ink-3);
}
.catalogue-preview-table td { padding: 5px 10px; vertical-align: top; color: var(--ink); }
.catalogue-preview-table tr.first td { border-top: 1px solid var(--gray-200); }
.catalogue-preview-table tbody tr:first-child td { border-top: none; }
.catalogue-preview-table td.lot { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
.catalogue-preview-table td.from { color: var(--ink-4); text-decoration: line-through; }
.catalogue-preview-table td.to { font-weight: 600; }
.catalogue-preview-warnings { margin: 0 0 12px; font-size: 12px; color: var(--ink-3); }
.catalogue-preview-warnings summary { cursor: pointer; font-weight: 600; margin-bottom: 4px; }
.catalogue-preview-done p { margin: 0; font-size: 14px; line-height: 1.5; color: var(--ink); white-space: pre-line; }

/* Move lots: where to, then a ticked list of the lots that can move. */
.move-lots-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin: 14px 0 6px;
    font-size: 13px;
}
.move-lots-all { display: inline-flex; align-items: center; gap: 6px; font-weight: 600; color: var(--ink); cursor: pointer; }
.move-lots-count { font-size: 12px; color: var(--ink-4); }
.move-lots-list {
    max-height: 44vh;
    overflow: auto;
    border: 1px solid var(--line);
    border-radius: 6px;
    background: var(--card);
}
.move-lots-lot {
    display: grid;
    grid-template-columns: auto 56px 72px 90px minmax(0, 1fr);
    align-items: center;
    gap: 10px;
    padding: 6px 10px;
    border-top: 1px solid var(--gray-200);
    font-size: 13px;
    color: var(--ink);
    cursor: pointer;
}
.move-lots-lot:first-child { border-top: none; }
.move-lots-lot:has(input:checked) { background: #FFF7ED; }
.move-lots-no { font-weight: 600; font-variant-numeric: tabular-nums; }
.move-lots-bales { color: var(--ink-3); font-variant-numeric: tabular-nums; }
.move-lots-desc { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink-3); }
.move-lots-note { margin: 8px 0 0; font-size: 12px; color: var(--ink-4); }
@media (max-width: 640px) {
    .move-lots-lot { grid-template-columns: auto 48px minmax(0, 1fr); }
    .move-lots-bales, .move-lots-desc { display: none; }
}

.manage-sales-create-error {
    margin: 0 0 12px;
    padding: 8px 12px;
    border-radius: 6px;
    background: var(--lose-bg);
    color: var(--lose);
    font-size: 13px;
    font-weight: 600;
}

/* The same form inside the warm Sale admin panel. */
.sale-admin .sale-form-label { color: var(--ink-3); }
.sale-admin .sale-form-input { border-color: #E8A080; background: #FFF; }
.sale-admin .sale-form-input:focus { border-color: #C2410C; box-shadow: 0 0 0 3px rgba(194, 65, 12, 0.10); }
.sale-details-status { font-size: 11px; font-weight: 600; color: var(--ink-4); }
.sale-details-status.dirty { color: oklch(52% 0.13 70); }
.sale-details-status.saved { color: var(--win); }
.sale-details-status.error { color: var(--lose); }
/* A refused save's message, on its own line above the buttons so the
   "Unsaved" status can't cover it. */
.sale-details-error {
    margin: 10px 0 0;
    font-size: 12px;
    font-weight: 600;
    color: var(--lose);
}
.sale-details-error:empty { display: none; }
/* Delete sits at the far left, away from Save. */
.sale-details-delete { margin-right: auto; }
.sale-admin .sale-form-readonly { background: transparent; border-color: #E8A080; }
.sale-admin-created {
    margin: 0;
    padding: 10px 14px;
    border-top: 1px solid #FED7AA;
    font-size: 12px;
    font-weight: 600;
    color: #C2410C;
}

/* ── Admin page: presence panels ─────────────────────────────────────────
   Two stacked panels, WOLX then Legacy, both pinned to the same 900px
   column as the sale pages so the whole page reads as one measure. Panels
   are paper-light; each person sits on their own white card inside, so a
   row is a thing rather than a table line. The status tag above each name
   is the only place the two panels differ in meaning — wolx can say
   "Active", Fleece can only ever say when someone was last seen. */

.admin-page .page-header {
    max-width: 900px;
    margin: 0 auto 24px;
    width: 100%;
}

.presence-panel {
    max-width: 900px;
    margin: 0 auto 16px;
    background: var(--paper-light);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    padding: 14px 16px 16px;
}

.presence-panel-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 12px;
}

.presence-panel-head h2 {
    font-size: 14px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-3);
    margin: 0;
}

.presence-count {
    font-family: var(--mono);
    font-size: 12px;
    color: var(--ink-3);
    background: var(--paper-2);
    border-radius: 999px;
    padding: 2px 10px;
}

.presence-rows {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.presence-card {
    background: var(--card);
    border: 1px solid var(--line-soft);
    border-radius: var(--radius-md);
    padding: 10px 14px 12px;
}

/* An absence, not a row — dotted outline, no fill. */
.presence-empty {
    border: 1px dashed var(--line);
    border-radius: var(--radius-md);
    padding: 22px 14px;
    text-align: center;
    color: var(--ink-4);
    font-size: 13px;
}

.presence-loading {
    max-width: 900px;
    margin: 0 auto;
    padding: 28px 0;
    text-align: center;
    color: var(--ink-4);
    font-size: 13px;
}

/* Status tag sits above the name: dot + words, same green as a live sale. */
.presence-status {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    margin-bottom: 5px;
}

.presence-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    flex: 0 0 auto;
}

.presence-status.live { color: var(--win); }
.presence-status.live .presence-dot {
    background: var(--win);
    /* The same expanding-ring pulse the live sale dots use, so "someone is
       here now" reads identically wherever it appears. */
    animation: pulse-live 1.5s infinite;
}

.presence-status.idle { color: oklch(52% 0.13 70); }
.presence-status.idle .presence-dot { background: oklch(70% 0.14 75); }

.presence-status.stale { color: var(--ink-5); }
.presence-status.stale .presence-dot { background: var(--ink-5); }

.presence-name {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 14px;
    font-weight: 600;
    color: var(--ink);
}

.presence-sub {
    color: var(--ink-4);
    font-size: 11.5px;
    margin-top: 2px;
}

.presence-tag {
    font-size: 9px;
    font-weight: 600;
    letter-spacing: 0.08em;
    border-radius: 3px;
    padding: 1px 4px;
    background: var(--paper-3);
    color: var(--ink-3);
}
/* Access-type chips. Text is the tint darkened toward ink so every pair
   clears 4.5:1 on its background. */
.presence-tag.admin    { background: var(--live-bg); color: color-mix(in srgb, var(--live) 70%, var(--ink-2)); }
.presence-tag.broker   { background: var(--lose-bg); color: color-mix(in srgb, var(--lose) 70%, var(--ink-2)); }
.presence-tag.buyer    { background: var(--win-bg);  color: color-mix(in srgb, var(--win) 70%, var(--ink-2)); }
.presence-tag.merchant { background: var(--pass-bg); color: color-mix(in srgb, var(--pass) 70%, var(--ink-2)); }
.presence-tag.guest    { background: var(--paper-2); color: var(--ink-3); }
.presence-tag.other    { background: var(--card); color: var(--ink-3); box-shadow: inset 0 0 0 1px var(--line); }
.presence-tag.muted { background: var(--paper-2); color: var(--ink-5); }

/* A person's card. Narrow: status, then chip + name, then
   organisation. Once the panel is wide: one row whose fixed columns line up
   from card to card, with the status on the right. */
.presence-rows { container-type: inline-size; }

.presence-user .presence-tag {
    display: inline-block;
    white-space: nowrap;
    vertical-align: 2px;
}
.presence-user .presence-type { margin-right: 6px; }
.presence-user .presence-name { display: inline; overflow-wrap: anywhere; }
.presence-user .presence-name .presence-tag { margin-left: 6px; }
.presence-user .presence-sub { overflow-wrap: anywhere; }
.presence-user .presence-sub:empty { display: none; }

@container (min-width: 800px) {
    .presence-user {
        display: grid;
        grid-template-columns: 5rem minmax(0, 3fr) minmax(0, 2fr) 14rem;
        grid-template-areas: "type name sub status";
        align-items: center;
        column-gap: 12px;
        padding: 9px 14px;
    }
    .presence-user .presence-type {
        grid-area: type;
        justify-self: start;
        max-width: 100%;
        margin: 0;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    .presence-user .presence-name { grid-area: name; display: flex; gap: 0; min-width: 0; }
    .presence-user .presence-label,
    .presence-user .presence-sub {
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    .presence-user .presence-name .presence-tag { flex: 0 0 auto; }
    .presence-user .presence-sub { grid-area: sub; display: block; margin: 0; }
    .presence-user .presence-status {
        grid-area: status;
        justify-self: end;
        margin: 0;
        white-space: nowrap;
    }
}

/* ── Admin sections ──────────────────────────────────────────────────────
   /admin groups its cards under section titles — a step down from the
   page title, same measure as the cards beneath. */
.admin-section {
    max-width: 900px;
    margin: 0 auto;
}
.admin-section + .admin-section { margin-top: 32px; }
/* The whole title row toggles the section; hovering underlines the title
   and the Collapse / Expand label to show it's clickable. */
.admin-section-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 12px;
    cursor: pointer;
    user-select: none;
}
.admin-section-head:hover .admin-section-title,
.admin-section-head:hover .admin-section-toggle {
    text-decoration: underline;
    text-underline-offset: 4px;
    text-decoration-thickness: 2px;
}
.admin-section-head:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; border-radius: 4px; }
.admin-section.collapsed .admin-section-head { margin-bottom: 0; }
.admin-section.collapsed .admin-section-body { display: none; }
.admin-section-title {
    margin: 0;
    font-size: 20px;
    font-weight: 700;
    letter-spacing: -0.015em;
    color: var(--ink);
    line-height: 1.2;
}
.admin-section-toggle {
    flex: 0 0 auto;
    font-size: 13px;
    font-weight: 600;
    color: var(--ink-3);
}

/* Each site message is its own top-level card, dressed as a presence panel
   (paper, border, uppercase label) so the page reads as one column. */
.admin-messages .msg-editor {
    max-width: 900px;
    margin: 0 auto 16px;
    background: var(--paper-light);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    padding: 14px 16px 16px;
}
.admin-messages .msg-editor-head { margin-bottom: 12px; }
.admin-messages .msg-editor-label {
    font-size: 14px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-3);
}

/* ── Admin message editor ────────────────────────────────────────────────
   One staff-authored message: label + status, textarea, Save. Shared by the
   two site messages on /admin and the per-sale message in the Sale admin
   panel, which is why the shell carries no colour of its own — the sale
   panel re-tints it below.

   Explicit Save, unlike the per-lot notes field it otherwise resembles:
   these are on screen for every user within a poll cycle, so half-typed
   text must not publish itself. */
.msg-editor {
    background: var(--card);
    border: 1px solid var(--line-soft);
    border-radius: var(--radius-md);
    padding: 10px 14px 12px;
}

.msg-editor-head {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 6px;
}

.msg-editor-label {
    font-size: 12px;
    font-weight: 700;
    color: var(--ink-2);
}

/* Save state, opposite the label. Mirrors .lot-dialog-note-status so the
   two editors report themselves the same way. */
.msg-editor-status {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: 5px;
    min-height: 12px;
    font-size: 11px;
    font-weight: 600;
    color: var(--ink-4);
}
.msg-editor-status.dirty { color: oklch(52% 0.13 70); }
.msg-editor-status.saved { color: var(--win); }
.msg-editor-status.error { color: var(--lose); }
.msg-editor-status.pending::before {
    content: '';
    width: 10px;
    height: 10px;
    border-radius: 50%;
    border: 2px solid rgba(22, 32, 46, 0.35);
    border-top-color: transparent;
    animation: btn-spin 0.7s linear infinite;
}

.msg-editor-field {
    width: 100%;
    box-sizing: border-box;
    border: 1px solid var(--gray-200);
    border-radius: 8px;
    padding: 10px 12px;
    background: var(--card);
    font-family: var(--sans);
    font-size: 14px;
    line-height: 1.5;
    color: var(--ink);
    resize: vertical;
}
.msg-editor-field::placeholder { color: var(--ink-5); }
.msg-editor-field:focus {
    outline: none;
    border-color: var(--ink);
    box-shadow: 0 0 0 3px rgba(22, 32, 46, 0.08);
}
.msg-editor-field[readonly] {
    background: var(--paper-2);
    color: var(--ink-4);
    resize: none;
}

.msg-editor-actions {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 8px;
}

.msg-editor-locked {
    margin: 0;
    font-size: 11px;
    color: var(--ink-4);
}
.msg-editor-locked { color: oklch(52% 0.13 70); font-weight: 600; }

.msg-editor-btn {
    margin-left: auto;
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 62px;
    padding: 5px 14px;
    border: 1px solid var(--ink);
    border-radius: 6px;
    background: var(--ink);
    color: var(--paper);
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
}
.msg-editor-btn:hover { opacity: 0.9; }
.msg-editor-btn:disabled { opacity: 0.5; cursor: default; }
.msg-editor-btn .msg-editor-btn-spinner { display: none; }
.msg-editor-btn.busy .msg-editor-btn-label { display: none; }
.msg-editor-btn.busy .msg-editor-btn-spinner {
    display: inline-block;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    border: 1.5px solid currentColor;
    border-top-color: transparent;
    animation: btn-spin 0.7s linear infinite;
}

/* The same editor inside the warm Sale admin panel: drop the nested card
   and borrow the panel's colours so it doesn't look pasted in. */
.sale-admin .msg-editor {
    background: none;
    border: none;
    border-radius: 0;
    padding: 0;
}
.sale-admin .msg-editor-head { margin-bottom: 8px; }
/* The editor's label is the section's heading, styled as .sale-admin-label. */
.sale-admin .msg-editor-label {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: #C2410C;
}
.sale-admin .msg-editor-field {
    border-color: #E8A080;
    font-size: 13px;
}
.sale-admin .msg-editor-field:focus {
    border-color: #C2410C;
    box-shadow: 0 0 0 3px rgba(194, 65, 12, 0.10);
}
.sale-admin .msg-editor-btn {
    min-width: 0;
    padding: 3px 10px;
    border-color: #E8A080;
    background: #FFF;
    color: #C2410C;
    font-size: 11px;
}
.sale-admin .msg-editor-btn:hover { background: #FFF7ED; opacity: 1; }

/* Guest lot placeholders: the server has already replaced every private value. */
.guest-blur,
.guest-private-secondary > :not(.status) {
    filter: blur(5px);
    user-select: none;
    pointer-events: none;
}
span.guest-blur { display: inline-block; }
/* Floats over the lot number column: the desktop row's LotNo track opens at
   the row's --space-lg padding, the mobile card's at its 12px padding. */
.private-lot-label {
    position: absolute;
    top: 7px;
    left: calc(var(--space-lg) - 6px);
    /* Mobile lot rows sit at z-index: 1; keep their blur behind this card. */
    z-index: 3;
    padding: 3px 12px;
    border: 1px solid var(--line);
    border-radius: 6px;
    background: #fff;
    color: var(--ink-2);
    font-size: 12px;
    font-weight: 600;
    text-align: center;
    white-space: nowrap;
    pointer-events: none;
}
.m-lot-entry:has(.private-lot-label) { position: relative; }
.m-lot-entry .private-lot-label { top: 14px; left: 6px; }
.private-hero-label { font-size: 13px; font-weight: 600; margin-bottom: 8px; }

/* Keep the OOB status target without reserving space for an empty guest status. */
.live-hero-bid .bid-status:empty { display: none; }

/* Login page: account and guest sign-in are two modes of the one card. */
.login-card .login-pane-guest,
.login-card.guest-mode .login-pane-account { display: none; }
.login-card.guest-mode .login-pane-guest { display: block; }
.login-card-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 20px;
}
.login-card .login-card-head h3 { margin-bottom: 0; }
.login-mode-switch {
    padding: 4px 2px;
    border: none;
    background: none;
    font-family: inherit;
    font-size: 14px;
    font-weight: 600;
    color: var(--ink-3);
    cursor: pointer;
}
.login-mode-switch:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
.guest-code-input {
    letter-spacing: 0.3em;
    font-variant-numeric: tabular-nums;
}
/* How to ask for a code, tucked under the Guest Sign In heading. */
.login-card .guest-request {
    font-size: 14px;
    line-height: 1.5;
    color: var(--ink-3);
    margin: -12px 0 22px;
}
.login-card .guest-request a {
    color: var(--ink-2);
    text-decoration: underline;
}
.login-card .guest-request a:hover { color: var(--ink); }
/* The short Guest Access Notice, then the box accepting the full terms. */
.login-card .guest-notice {
    font-size: 12px;
    line-height: 1.5;
    color: var(--ink-3);
    background: var(--paper-2);
    border: 1px solid var(--line);
    border-radius: 8px;
    padding: 10px 12px;
    margin: 4px 0 14px;
}
.login-card .guest-notice strong { color: var(--ink-2); }
.login-page .guest-terms-check {
    align-items: flex-start;
    line-height: 1.4;
}
.login-page .guest-terms-check input { flex: none; margin-top: 1px; }
.login-page .guest-terms-check a {
    color: var(--ink-2);
    text-decoration: underline;
}
.login-page .guest-terms-check a:hover { color: var(--ink); }

/* ── Admin: guest codes ──────────────────────────────────────────────────
   The create form (fields stacked), then one card per code (same card as a
   presence row). */
.guest-code-form {
    display: flex;
    flex-direction: column;
    align-items: stretch;
}
.guest-code-field {
    margin: 0 0 4px;
    font-size: 12px;
    font-weight: 600;
    color: var(--ink-3);
}
/* The lot notes strip's field (.lot-note-strip-input), at a comfortable
   single-line height for a form. */
.guest-code-input-strip {
    width: 100%;
    box-sizing: border-box;
    margin-bottom: 12px;
    padding: 7px 10px;
    border: 1px solid var(--gray-200);
    border-radius: 6px;
    background: var(--card);
    font-family: var(--sans);
    font-size: 13px;
    line-height: 1.4;
    color: var(--ink);
}
.guest-code-input-strip::placeholder { color: var(--ink-5); }
.guest-code-input-strip:focus {
    outline: none;
    border-color: var(--ink);
    box-shadow: 0 0 0 3px rgba(22, 32, 46, 0.08);
}
.guest-code-date,
.guest-code-select { max-width: 220px; }
.guest-code-actions {
    display: flex;
    justify-content: flex-end;
    margin-top: 4px;
}
.guest-code-filter { display: flex; gap: 6px; }
.guest-code-filter .list-chip {
    padding: 3px 12px;
    font-family: inherit;
    font-size: 12px;
    cursor: pointer;
}
/* Active filter (the default): revoked and expired codes are hidden. */
.guest-code-list:not(.show-all) .guest-code-card.inactive { display: none; }
.guest-code-list.show-all .guest-code-none-active { display: none; }
.guest-code-meta > div + div { margin-top: 2px; }
.guest-code-card .presence-name { margin-bottom: 2px; }
.guest-code-main {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
}
.guest-code-digits {
    font-family: var(--mono);
    font-size: 22px;
    font-weight: 700;
    letter-spacing: 0.15em;
    color: var(--ink);
    user-select: all;
}
.guest-code-card.new { border-color: var(--win); box-shadow: 0 0 0 2px var(--live-bg); }
.guest-code-card.inactive .guest-code-digits { color: var(--ink-5); text-decoration: line-through; }
.guest-code-status {
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.06em;
}
.guest-code-status.active { color: var(--win); }
.guest-code-status.expired,
.guest-code-status.revoked { color: var(--ink-5); }
.guest-code-notes {
    margin-top: 6px;
    font-size: 13px;
    color: var(--ink-2);
}

/* ── Sale report (/sale/{id}/report): one dense table, A4 landscape in print ──
   The card and table are greyscale only, on screen as on paper; the toolbar
   keeps the app's own styling. */
.sale-report-toolbar {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 14px;
}
.sale-report-back {
    color: var(--ink-3);
    font-size: 13px;
    text-decoration: none;
}
.sale-report-back:hover { color: var(--ink); text-decoration: underline; }
.sale-report-actions {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 6px;
}
.sale-report-columns {
    display: flex;
    align-items: center;
    gap: 6px;
    color: var(--ink-3);
    font-size: 12px;
}
.sale-report-columns label { display: flex; align-items: center; gap: 5px; cursor: pointer; user-select: none; }
.sale-report-columns input { margin: 0; accent-color: var(--ink-2); }
.sale-report-columns-apply {
    padding: 1px 8px;
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    background: none;
    color: var(--ink-3);
    font-family: inherit;
    font-size: 11px;
    cursor: pointer;
}
.sale-report-js .sale-report-columns-apply { display: none; }
.sale-report-print {
    min-width: 96px;
    padding: 6px 16px;
    border: none;
    border-radius: var(--radius-md);
    background: var(--ink-2);
    color: #FFFFFF;
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
}
.sale-report-print:hover { background: var(--ink); }
/* The sale card, docked onto the table: the sale page's card layout, square
   foot, no bottom border, the table's header row flush beneath it. */
.sale-report-card {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 10px 14px;
    border: 1px solid #000000;
    border-bottom: none;
    background: #FFFFFF;
    color: #000000;
}
.sale-report-card .card-top {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: -4px;
    line-height: 1;
}
.sale-report-card :is(.type-label, .state) {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}
.sale-report-card .header {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    gap: 8px;
}
.sale-report-card h1 {
    margin: 0;
    font-size: 16px;
    font-weight: 600;
    letter-spacing: -0.015em;
    line-height: 1.25;
    color: #000000;
}
.sale-report-card .id-lines {
    display: flex;
    flex-direction: column;
    gap: 1px;
    margin-top: 5px;
    line-height: 1.15;
}
.sale-report-card .id {
    font-family: var(--mono);
    font-size: 12px;
    letter-spacing: -1px;
}
.sale-report-card .sale-when {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 1px;
    line-height: 1.15;
    white-space: nowrap;
}
.sale-report-card .sale-when .k {
    font-size: 9px;
    font-weight: 500;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}
.sale-report-card .sale-when .date {
    font-family: var(--mono);
    font-size: 11.5px;
    letter-spacing: -1px;
}
.sale-report-card .sale-when .time {
    font-family: var(--mono);
    font-size: 14px;
    font-weight: 600;
    letter-spacing: -1px;
}
.sale-report-card .message {
    padding-top: 8px;
    border-top: 1px solid #000000;
    font-size: 12px;
}
.sale-report-table {
    width: 100%;
    border: 1px solid #000000;
    border-collapse: collapse;
    background: #FFFFFF;
    font-size: 12px;
    color: #000000;
}
.sale-report-table.all-columns { font-size: 11px; }
.sale-report-table th,
.sale-report-table td {
    padding: 3px 6px;
    text-align: left;
    white-space: nowrap;
}
.sale-report-table th {
    background: #000000;
    color: #FFFFFF;
    font-size: 10.5px;
    font-weight: 700;
}
.sale-report-table tbody tr:nth-child(even) { background: #EBEBEB; }
.sale-report-table .num { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; }
.sale-report-table td.text:nth-last-child(3) { white-space: normal; min-width: 12em; }
.sale-report-table .with-buyer { display: flex; justify-content: space-between; gap: 1em; }
.sale-report-table tr.private td { font-style: italic; }
.sale-report-empty,
.sale-report-printed { margin-top: 12px; font-size: 11px; color: #555555; }
.sale-report-printed { display: none; }

@media print {
    @page { size: A4 landscape; margin: 10mm; }
    body:has(.sale-report) { background: #FFFFFF; }
    .app:has(.sale-report) { display: block; background: #FFFFFF; min-height: 0; }
    .app:has(.sale-report) > .sidebar,
    .app:has(.sale-report) > .sidebar-scrim,
    .app:has(.sale-report) .mobile-topbar,
    .app:has(.sale-report) .topbar,
    .app:has(.sale-report) .site-banner,
    .app:has(.sale-report) .conn-banner,
    .sale-report-toolbar { display: none !important; }
    .page.sale-report { max-width: none; padding: 0; }
    .sale-report-table th,
    .sale-report-table tbody tr { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
    .sale-report-table { font-size: 8.5pt; line-height: 1.25; }
    .sale-report-table.all-columns { font-size: 7pt; }
    .sale-report-table th,
    .sale-report-table td { padding: 1.5pt 3pt; }
    .sale-report-table thead { display: table-header-group; }
    .sale-report-table tr { break-inside: avoid; }
    .sale-report-printed { display: block; }
}

.guest-print-notice { display: none; }
@media print {
    body:has(> .guest-print-notice) > :not(.guest-print-notice) { display: none !important; }
    .guest-print-notice { display: block; padding: 20mm 0; font-size: 14pt; text-align: center; color: #000000; }
}
