/* Page styles that used to live in the cshtml files.
   Loaded after elementa-glass.css, so a page rule still wins over the sheet.
   Keep the per-view sections in place: later sections rely on nothing earlier,
   but the order mirrors the order these pages were migrated. */

/* ── _Layout.cshtml ───────────────────────────────────────────────────────────── */
    @keyframes fadeIn {
        from {
            opacity: 0;
            transform: translateY(-10px);
        }

        to {
            opacity: 1;
            transform: translateY(0);
        }
    }

    #LastBids .alert {
        transition: all 0.3s ease;
    }

        #LastBids .alert:hover {
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
        }

    /* ── The shell is a column one screen tall, minimum ────────────────────────────
       A short page used to end wherever its own content ended, which left a band of
       bare background under the footer and put the footer's bottom edge above the
       fold. The document is now a flex column at least as tall as the screen and the
       leftover space is handed to .main, so the content and the footer TOGETHER
       measure 100% of the screen at minimum on every page and the footer always
       closes the viewport.

       min-height, not height: a page longer than the screen (Register, the Auction
       list) still grows and scrolls exactly as before - a flex item is only stretched
       when there is space left over, never shortened when there is not.

       The header is `position: sticky`, so it is a real item in this column and its
       own height is counted here rather than assumed anywhere else. */
    .body {
        display: flex;
        flex-direction: column;
        min-height: 100vh;
    }

    .body > .main {
        flex: 1 0 auto;
    }

    /* Neither stretched nor shrunk: a footer is its own height, always. Both footer
       shapes are named - the full bar and the two-link strip the layout renders in
       its place when `HideFooter` is false - because whichever one is present is the
       last item in the column. */
    .body > #footer,
    .body > .footer-strip {
        flex: 0 0 auto;
    }

    .footer-strip {
        background: #f4f4f4;
    }

    /* The page's ground is the theme's own now (--eg-bg / --eg-bg-soft in elementa-glass.css):
       it was drawn HERE for one round, as a `body.member-portal` override applied to /Member/*
       only, and then taken site-wide on request - so the two values in the theme token block
       are the single copy and nothing is scoped by URL any more. Do not add a second one. */

    /* ── Dialog titles ────────────────────────────────────────────────────────────
       The icon and the words share one centre line. The markup carried the old theme's
       `float: left` on a 42px glyph beside 20px text, which left the glyph's box hanging
       below the text's baseline - and every dialog repeated that inline style. Both come
       from here now, so a dialog title cannot drift again. */
    .modal-title {
        display: flex;
        align-items: center;
        gap: 10px;
    }

    .modal-title > .material-symbols-outlined {
        font-size: 42px;
    }

    /* ── The wordmark ─────────────────────────────────────────────────────────────
       Built exactly the way Elementa builds its own brand lockup (`.brand-name` in its
       sidebar): the display face at 20px, weight 500, -0.01em tracking, and the gradient
       painted INTO the text with background-clip plus a transparent fill.

       The gradient runs purple -> orange, the two ends of our own logo, and both stops are
       palette tokens (purple #a855f7, orange #f97316) rather than values picked by eye.
       Elementa's lockup runs the same two colours the other way round and adds blue.

       @supports guards it: without the clip, a gradient would paint the whole box and the
       transparent fill would leave nothing to read, so the wordmark falls back to a plain
       token purple.

       The face is loaded in _Layout.cshtml. The old theme asked for Barlow Semi Condensed
       all over without ever loading it, which is why this element had been rendering in the
       body face. line-height 1.2 keeps the span the same 24px it already was, so the brand
       lockup's height does not move. */
    .header-text {
        color: #a855f7;
    }

    @supports ((-webkit-background-clip: text) or (background-clip: text)) {
        .header-text {
            font-family: "Barlow Semi Condensed", "Noto Sans Thai", Arial, sans-serif;
            font-size: 20px;
            font-weight: 500;
            letter-spacing: -0.01em;
            line-height: 1.2;
            background-image: linear-gradient(90deg, #a855f7 0%, #f97316 100%);
            -webkit-background-clip: text;
            background-clip: text;
            -webkit-text-fill-color: transparent;
            color: transparent;
        }
    }

/* ── Auction.cshtml ───────────────────────────────────────────────────────────── */
    .glass-card {
        position: relative;
        border: 1px solid var(--eg-border);
        border-radius: 14px;
        background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(244, 250, 255, 0.82));
        backdrop-filter: blur(10px) saturate(125%);
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72), 0 18px 40px -28px rgba(31, 41, 55, 0.55);
        transition: box-shadow 0.2s ease;
    }

        .glass-card::before {
            content: "";
            position: absolute;
            inset: 0;
            border-radius: inherit;
            pointer-events: none;
            background: linear-gradient(145deg, rgba(255, 255, 255, 0.54), rgba(255, 255, 255, 0) 42%);
            opacity: 0.8;
        }

    .bento-tile {
        min-width: 0;
    }

    .kpi {
        display: grid;
        gap: 10px;
        padding: 16px;
    }

    /* The card used to open with a head strip — the words "AUCTION LOT" and a grid
       icon in the corner. Neither said anything the page had not already said, and
       together they pushed the lot name 30px down the card. The name is now the
       first thing on it. */

    /* One line, always: the title must not push the countdown panel down. The
       ellipsis lives on the NAME span, not on the <h4>, for a reason that only
       shows up at hover time: the bubble carrying the full name is anchored to the
       title, and a clipping <h4> would cut that bubble off. */
    .auc-lot-title {
        position: relative;
        display: flex;
        align-items: center;
        gap: 8px;
        margin: 0 0 8px;
        line-height: 1.4;
    }

    .auc-lot-title .no-1 {
        margin-right: 0;   /* the flex gap is what spaces the pill from the name */
        flex: none;
    }

    .auc-lot-title .no3 {
        min-width: 0;      /* a flex item must be allowed to shrink, to ellipsise */
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /* The full name, in the same bubble language as the rest of the sheet. It sits
       BELOW the title rather than above it: this card has two clipping ancestors —
       the .overflow-hidden wrapper and the column's `content-visibility: auto`,
       which implies paint containment — so anything leaving the card's box would be
       cut off. Below the title there is room inside the card. */
    .auc-lot-pop {
        position: absolute;
        left: 0;
        top: calc(100% + 4px);
        z-index: 3;
        max-width: 100%;
        padding: 6px 9px;
        border: 1px solid var(--eg-border);
        border-radius: var(--radius);
        background: #fff;
        box-shadow: var(--eg-pop-shadow);
        color: var(--eg-ink);
        font: 400 12px/1.35 "Noto Sans Thai", "Open Sans", Arial, sans-serif;
        white-space: normal;
        opacity: 0;
        visibility: hidden;
        pointer-events: none;
        transition: opacity 0.12s ease, visibility 0.12s;
    }

    /* :focus-visible on the card link keeps this reachable by keyboard as well as
       by pointer — the native title attribute it replaces was not. */
    .auc-lot-title:hover .auc-lot-pop,
    .item-x:focus-visible .auc-lot-pop {
        opacity: 1;
        visibility: visible;
    }

    /* …and it stays out of the way when the name is not cut off at all. */
    .auc-lot-title.no-trunc .auc-lot-pop {
        display: none;
    }

    /* …but a nowrap child makes its ancestors as wide as the whole name: the card
       (.kpi) is a GRID and this wrapper is its only item, and a grid item's
       automatic minimum is its MIN-CONTENT width. Measured: the wrapper grew to
       375px inside a 288px card, so the title was clipped by the .overflow-hidden
       above it — no ellipsis, just a cut-off word — and it also pushed the
       countdown panel out of shape. Releasing that minimum is what lets the
       truncation above happen at the card's own edge. */
    .auction-kpi-tile > .rounded-3 {
        min-width: 0;
        /* Above the card's watermark (see ::after below): the card's own content
           must stay on top of its own icon. */
        position: relative;
        z-index: 1;
    }

    /* The count sits on the circles' centre line. It used to carry an inline
       `padding-top: 4px`, which measured 2px BELOW that line; `line-height: 1`
       makes the span box exactly as tall as its own glyphs, so the row's
       align-items: center has nothing left to misplace. */
    .auc-pigeon-count {
        color: #433e27;
        /* The card's normal secondary size — the same 12px/400 its Starts and
           Ends rows use. At 15px/500 this label shouted louder than the panel
           above it. `line-height: 1` stays: it is what puts the box, and so the
           type, on the circles' centre line. */
        font-size: 12px;
        font-weight: 400;
        line-height: 1;
    }

    /* One height for the tag strip on EVERY card. It used to be whatever its
       tallest child made it — 20px when the "+N" chip was present, 15px when it
       was not (the label is line-height:1 and the plain circle is 10px) — so the
       strip, and everything above it, sat at a different height from one card to
       the next. 20px is the chip's own height in the sheet (`min-width: 26px;
       height: 20px`), so no row can ever need more than this. */
    .auc-pigeon-row {
        min-height: 20px;
    }

    /* One typeface across the card: the tag row ("12 Pigeons") carried `.tec-f2`,
       which was a second family then, so the card mixed two faces. The
       icons are deliberately not listed here — .material-symbols-outlined needs
       its own ligature font to draw a glyph at all. */
    .auction-kpi-tile,
    .auction-kpi-tile .tec-f2 {
        font-family: "Noto Sans Thai", "Open Sans", Arial, sans-serif;
    }

    /* The card takes the sheet's two shadows: the resting one at rest and the
       lifted one under the pointer, exactly like every other card family. It used
       to wear ONE shadow in both states — the hover on this list cancelled the
       sheet's own hover rule — which left a grid of tiles that never answered the
       cursor.
       overflow: hidden crops the ::after watermark at the card's own rounded corner
       — the glyph is positioned past `right`/`bottom` so the corner is what gives it
       its edge. It is `!important` because the theme's own `.glass-card, .box-a`
       rule sets `overflow: visible`, and it is safe for the title's hover bubble:
       that bubble is placed INSIDE the card (below the title, left: 0, max-width:
       100%), and the `.p-relative.overflow-hidden` wrapper above it already clips. */
    .auction-kpi-tile {
        background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(244, 250, 255, 0.82)) !important;
        border: 1px solid var(--eg-border) !important;
        border-radius: 14px !important;
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.74), var(--eg-shadow) !important;
        overflow: hidden !important;
    }

    .auction-kpi-tile:hover {
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.78), var(--eg-shadow-hover) !important;
    }

    /* A column in this grid used to carry `content-visibility: auto` inline, to let
       the browser skip 70 off-screen tiles. That value also implies PAINT
       containment, which clips everything a card paints OUTSIDE its own box — and a
       raised card's shadow is exactly that. The tile fills the column, so the bottom
       shadow simply vanished, and the side shadow was cut at the column's 12px
       padding. Dropping the containment only while the pointer was inside the column
       brought the shadow back but left a window of its own: the moment the pointer
       left, the clip returned and the shadow's 200ms fade-out was cut on the left and
       right — visible exactly while moving between two cards.

       `contain` alone is the whole idea the original value was after, and layout and
       style containment do NOT clip painting: the long grid keeps its isolation and
       the card keeps its shadow, in every state. */
    .tile-col {
        contain: layout style;
    }

    /* The lot's own state icon, blown up at the CARD's bottom-right corner and set
       UNDER the whole card — the same glyph the countdown line leads with, 121px
       against that line's 15px, so the card reads as one object at two scales. A
       pseudo-element and not markup: a decoration must not be able to change the
       card's height. The glyph follows the state the countdown engine writes on its
       panel, matched here with :has(), so no script has to know it exists.
       The glyph is a CODEPOINT (\e425 and friends, read from Google's icon metadata)
       rather than the icon's name: a name would depend on the font's ligature and,
       worse, would put the word "timer" into the card's accessible name, where a
       screen reader would read it out in the middle of the lot's details.
       right/bottom are NEGATIVE on purpose: the corner of the glyph is meant to run
       off the card and show through the gutter. The card has to keep
       `overflow: visible` for the title's hover bubble, so that part is painted on
       the page instead of being cropped — which is the overlap. */
    .auction-kpi-tile::after {
        content: "\e425";
        position: absolute;
        right: -30px;
        bottom: -34px;
        z-index: 0;
        font-family: 'Material Symbols Outlined';
        -webkit-font-smoothing: antialiased;
        font-size: 121px;
        font-weight: 800;
        font-style: normal;
        line-height: 1;
        letter-spacing: normal;
        color: #64748b;
        opacity: 0.1;
        pointer-events: none;
    }

    .auction-kpi-tile:has(.countdown-bg-wrapper[data-cd-state="pending"])::after,
    .auction-kpi-tile:has(.countdown-bg-wrapper[data-cd-state="starting"])::after {
        content: "\f238";
        color: #1d4ed8;
    }

    .auction-kpi-tile:has(.countdown-bg-wrapper[data-cd-state="today"])::after {
        content: "\e425";
        color: #b45309;
    }

    .auction-kpi-tile:has(.countdown-bg-wrapper[data-cd-state="soon"])::after {
        content: "\ef55";
        color: #ef4444;
    }

    .auction-kpi-tile:has(.countdown-bg-wrapper[data-cd-state="ended"])::after {
        content: "\e86c";
        color: #94a3b8;
    }

    .no3 {
        color: #002b41 !important;
        font-size: 18px !important;
        font-weight: 500 !important;
    }

    /* Ordinal badge in front of the lot name.
       Was a white 30px tab pulled left with margin-left:-20px, so the card's
       .overflow-hidden wrapper clipped most of it and the digit came out
       half-cut on the card edge.  Now a self-contained pill: full ring, tinted
       fill, tabular figures, no negative offsets. */
    .no-1 {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-width: 24px;
        height: 24px;
        padding: 0 8px;
        margin-right: 6px;
        border: 1px solid var(--eg-border);
        border-radius: 9999px;
        background: var(--surface-100);
        color: #33547d;
        font-family: "Noto Sans Thai", "Open Sans", Arial, sans-serif;
        font-size: 12px;
        font-weight: 600;
        line-height: 1;
        font-variant-numeric: tabular-nums;
        vertical-align: middle;
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
    }

    /* --- Search: one field, with its controls inside it ---------------------
       Search / Reset used to be three separate boxes in a row, which read as
       three unrelated things. The controls now live IN the field: a quiet
       magnifier on the left, then the clear ✕ and the Search button on the
       right, so the whole control is a single object centred on the page. */
    .auc-search {
        position: relative;
        display: flex;
        align-items: center;
        width: min(100%, 380px);
    }

    /* The right padding is what keeps the typed keyword clear of the two
       controls: 5px margin + the button + a gap + the 26px ✕ + 8px.
       The field — and everything sitting inside it — uses the theme's button
       radius, the same --radius the View button on the home page carries, rather
       than the 10px the shared .form-control rule gives an input. */
    .auc-search .auc-search-input {
        padding-left: 36px;
        padding-right: 142px;
        height: 40px;
        border-radius: var(--radius);
    }

    .auc-search-ico {
        position: absolute;
        left: 11px;
        font-size: 18px;
        color: #94a3b8;
        pointer-events: none;
    }

    .auc-search .auc-search-clear,
    .auc-search .auc-search-go {
        position: absolute;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        cursor: pointer;
    }

    /* Clear: a quiet icon that only stands out under the pointer. It is square in
       the theme's button radius like the field it sits in, not a circle. */
    .auc-search .auc-search-clear {
        right: 108px;
        width: 26px;
        height: 26px;
        border: 0;
        border-radius: var(--radius);
        background: transparent;
        color: #94a3b8;
    }

    .auc-search .auc-search-clear:hover {
        background: var(--hover-bg);
        color: #475569;
    }

    .auc-search-clear .material-symbols-outlined {
        font-size: 18px;
    }

    /* Search: the theme's own gradient button, sized to sit in the field. Its
       corners are the View button's on the home page — the same --radius — rather
       than the 8px it was drawn with, so field and button read as one object. */
    .auc-search .auc-search-go {
        right: 5px;
        height: 30px;
        padding: 0 12px;
        gap: 5px;
        border-radius: var(--radius);
        font-family: "Noto Sans Thai", "Open Sans", Arial, sans-serif;
        font-size: 15px;
        letter-spacing: 0.01em;
    }

    .auc-search-go .material-symbols-outlined {
        font-size: 16px;
    }

    /* The field and the cards share one left edge and one right edge. On a phone the
       field is full width, so it has to carry the same inset the card grid does —
       the container's 12px PLUS the grid row's own 0.75rem, which is the 24px the
       cards start at. Above the phone breakpoint the field is capped at 380px and
       merely centred, so the padding has nothing to change there: one rule, both
       cases. */
    .auc-search-row {
        padding-left: 0.75rem;
        padding-right: 0.75rem;
    }

    /* --- The item search (Search.cshtml) --------------------------------------
       The field above is the auction page's, rule for rule; only the ANSWER differs,
       because the question does: one row per ITEM rather than one tile per lot. The
       list is the shape the member pages use - one bordered box, rows that own
       nothing but a hairline and a hover tint, so the box reads as a single object. */
    .search-list {
        border: 1px solid var(--eg-border);
        border-radius: var(--radius);
        background: #fff;
        overflow: hidden;
        margin-bottom: 1.5rem;
    }

    .search-item {
        display: flex;
        align-items: center;
        gap: 12px;
        padding: 10px 12px;
        border-bottom: 1px solid var(--eg-line);
        color: inherit;
        text-decoration: none;
        transition: background 150ms ease;
    }

    .search-item:last-child {
        border-bottom: 0;
    }

    .search-item:hover {
        background: rgba(86, 127, 189, 0.06);
        color: inherit;
    }

    .search-item-thumb {
        flex: none;
        width: 44px;
        height: 44px;
        border: 1px solid var(--eg-line);
        border-radius: var(--radius);
        background: #eef2f7 center/cover no-repeat;
        overflow: hidden;
    }

    /* The photo is a CHILD of the chip rather than a background on it, because a background
       image cannot be lazy-loaded and every row of a hundred-row list would fetch one at once. */
    .search-item-thumb img {
        display: block;
        width: 100%;
        height: 100%;
        object-fit: cover;
        border-radius: inherit;
    }

    /* min-width: 0 is what lets the two text lines ellipsis instead of pushing the
       price off the row - a flex item refuses to shrink below its content without it. */
    .search-item-main {
        flex: 1 1 auto;
        min-width: 0;
        display: flex;
        flex-direction: column;
        gap: 1px;
    }

    .search-item-ring {
        font-size: 15px;
        font-weight: 600;
        color: var(--eg-ink);
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .search-item-sub {
        font-size: 12.5px;
        line-height: 1.35;
        color: #64748b;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .search-item-lot .material-symbols-outlined {
        font-size: 14px;
        vertical-align: -3px;
        margin-right: 3px;
        color: #94a3b8;
    }

    .search-item-facts {
        flex: none;
        display: flex;
        flex-direction: column;
        align-items: flex-end;
        gap: 1px;
    }

    .search-item-price {
        font-size: 15px;
        font-weight: 600;
        color: var(--eg-ink);
        white-space: nowrap;
    }

    .search-item-cur {
        font-size: 11px;
        font-weight: 400;
        color: #94a3b8;
    }

    @media (max-width: 575.98px) {
        .search-item {
            gap: 10px;
            padding: 9px 10px;
        }

        .search-item-thumb {
            width: 38px;
            height: 38px;
        }

        /* The price keeps the row; the end date is the detail that can go. */
        .search-item-facts .search-item-sub {
            display: none;
        }
    }

    /* --- The row's switches ---------------------------------------------------
       Elementa's own switch control (.switch / .switch-track / .switch-thumb, from its
       "Checkbox / Radio / Switch" component), which the app's sheet did not carry yet.
       The label IS the control: the checkbox stays in the markup for the keyboard and for
       auction.js but is invisible, and the two-element track draws the state. Geometry is
       Elementa's own - 38x20 track, 16px thumb inset 2px, 18px of travel - and only the
       type follows this row, 13px in the row's own colour, because it is a line of small
       print under the search field.

       These replaced 15px native checkboxes. A checkbox says "select this item"; a switch
       says "turn this on" - and all three are filters that stay on between visits, which
       is what a switch means. */
    .auc-toggles .switch {
        gap: 8px;
        font-size: 13px;
        color: #475569;
    }

    .switch {
        display: inline-flex;
        align-items: center;
        gap: 10px;
        color: #475569;
        cursor: pointer;
        position: relative;
        user-select: none;
    }

    /* The control the pointer and the keyboard actually hit: it covers the track, so a
       click on the pill toggles it, and it is still focusable and announceable. */
    .switch input {
        position: absolute;
        opacity: 0;
        width: 38px;
        height: 20px;
        margin: 0;
        cursor: pointer;
    }

    .switch-track {
        width: 38px;
        height: 20px;
        flex-shrink: 0;
        border-radius: 9999px;
        background: var(--eg-border);
        position: relative;
        transition: background-color 200ms ease;
    }

    .switch-thumb {
        position: absolute;
        top: 2px;
        left: 2px;
        width: 16px;
        height: 16px;
        border-radius: 9999px;
        background: #fff;
        box-shadow: 0 1px 2px rgba(15, 23, 42, 0.3);
        transition: transform 200ms ease;
    }

    .switch input:checked + .switch-track {
        background: var(--accent-600);
    }

    .switch input:checked + .switch-track .switch-thumb {
        transform: translateX(18px);
    }

    /* The same 3px accent halo the sheet uses elsewhere, so a keyboard user can see where
       they are without the component inventing a second focus style. */
    .switch input:focus-visible + .switch-track {
        box-shadow: 0 0 0 3px rgba(0, 115, 199, 0.14);
    }

    .switch input:disabled + .switch-track {
        opacity: 0.5;
    }

    @media (prefers-reduced-motion: reduce) {
        .switch-track,
        .switch-thumb {
            transition: none;
        }
    }

    /* The switch hides the two instants and nothing else: the status line stays, so every card
       still answers "how long left" — one row shorter. The class is written by the view, from
       the filter the URL describes, so the first paint is already correct: nothing is shown and
       then taken away. */
    .auc-grid.hide-date-details .pc-cd-sub {
        display: none;
    }

    /* The grid keeps the page gutter it always had (this used to be an inline style on the row,
       next to the container's own 12px). */
    .auc-grid {
        padding-left: 0.75rem;
        padding-right: 0.75rem;
    }

    /* The three switches share one centred row and wrap together on a phone: 8px between
       the words of a pair, 20px between the pairs. */
    .auc-toggles {
        gap: 8px 20px;
    }

    /* --- The results region ---------------------------------------------------
       Controls and grid arrive together, from the server, so the region is replaced as one
       piece. While it is being fetched the region is dimmed and stops taking input, so a
       second click cannot race the first; the spinner is server-rendered and this class is
       the only thing that shows it, which keeps the browser from building markup of its own. */
    .auc-results {
        position: relative;
        transition: opacity 0.15s ease;
    }

    .auc-results.is-loading {
        opacity: 0.55;
        pointer-events: none;
    }

    .auc-busy {
        display: none;
    }

    .auc-results.is-loading > .auc-busy {
        display: block;
        position: absolute;
        top: 1px;
        right: 10px;
        color: var(--accent-600);
    }

    @keyframes auc-spin {
        to { transform: rotate(360deg); }
    }

    .auc-results.is-loading > .auc-busy .material-symbols-outlined {
        font-size: 20px;
        animation: auc-spin 0.9s linear infinite;
    }

    @media (prefers-reduced-motion: reduce) {
        .auc-results.is-loading > .auc-busy .material-symbols-outlined {
            animation: none;
        }
    }

    /* Nothing matched. On this page an empty grid is always the reader's own filters - a lot
       list is never empty without them - so the message points at the filters rather than
       apologising in the abstract. */
    .auc-empty {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 4px;
        /* Sits under the grid's own 16px top padding when there is no result line, and under
           the result line when there is (14px + its 8px) - so it never hangs low in one case
           and reads cramped in the other. */
        padding: 22px 16px 14px;
        text-align: center;
    }

    .auc-empty > .material-symbols-outlined {
        font-size: 34px;
        color: #94a3b8;
    }

    .auc-empty-title {
        margin: 0;
        font-size: 15px;
        font-weight: 600;
        color: #475569;
    }

    .auc-empty-hint {
        margin: 0;
        font-size: 13px;
        color: #94a3b8;
    }

    @media (max-width: 575.98px) {
        /* Phones: the label goes, the magnifier stays, and the field keeps its room. */
        .auc-search .auc-search-input {
            padding-right: 82px;
        }

        .auc-search .auc-search-clear {
            right: 46px;
        }

        .auc-search .auc-search-go {
            width: 30px;
            padding: 0;
        }

        .auc-search-go-label {
            display: none;
        }
    }

    /* --- Search result line -------------------------------------------------
       Was #d8c6ff, a lavender chosen for the old dark page header; on this glass
       background it left the line almost invisible. It now speaks the same
       language as the cards: an ink-grey sentence with the keyword as a pill. */
    .auc-result {
        display: flex;
        align-items: center;
        justify-content: center;
        flex-wrap: wrap;
        gap: 8px;
        /* Room from the switch row above: the two used to touch (0px), which read as one
           control block rather than a sentence about the results below. */
        margin-top: 14px;
        font-size: 14px;
        color: #61708a;
    }

    .auc-result > .material-symbols-outlined {
        font-size: 17px;
        color: #94a3b8;
    }

    .auc-result-key {
        display: inline-flex;
        align-items: center;
        height: 24px;
        padding: 0 10px;
        border: 1px solid var(--eg-border);
        border-radius: 9999px;
        background: var(--surface-100);
        color: #33547d;
        font-weight: 600;
    }

    .auc-result-count {
        color: #94a3b8;
    }

/* ── AuctionLot.cshtml ───────────────────────────────────────────────────────────── */
    /* This page opened with a `page-header-md` band. That class belongs to the legacy
       theme the app no longer loads, so the band had no fill of its own and its
       `text-light` labels painted near-white straight onto the light page — the lot
       name was invisible. The grid below it sat on a hard-coded #152739 navy strip.
       Both are replaced by the sheet's own light language: a glass header panel on
       the page background.

       The panel is a direct child of .container, not a row: a .row cancels its
       column's 12px gutter with a -12px margin, so the grid's cards start at the
       container's content edge. Keeping the panel in the same box is what makes the
       header share one left and one right edge with the first and last card. */

    .lot-hero {
        padding: 18px 0 4px;
    }

    .lot-hero-panel {
        padding: 18px 20px 20px;
        text-align: center;
    }

    /* One shadow, in both states: this panel does not change under the pointer. It is
       the page's own heading - the lot name and its countdown - not one of the cards in
       the grid below it, so the lift the .glass-card family uses as a "you can click
       this" cue would read as a promise it does not keep. (It was unlocked for one
       round on 2026-10-04 and re-locked the same day on request.)

       Both states are stated, and both are needed: the sheet's `.glass-card:hover`
       carries one more class-equivalent than this rule, so only a later sheet that
       names the hover state itself can hold the shadow still. */
    .lot-hero-panel,
    .lot-hero-panel:hover {
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.74), var(--eg-shadow) !important;
        z-index: auto;
    }

    .lot-crumb {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        font-size: 12px;
        font-weight: 600;
        letter-spacing: 0.1em;
        text-transform: uppercase;
    }

    .lot-crumb a {
        display: inline-flex;
        align-items: center;
        gap: 4px;
        color: var(--accent-600);
    }

    .lot-crumb a:hover {
        color: #005ba1;
        text-decoration: underline;
    }

    .lot-crumb .material-symbols-outlined {
        font-size: 15px;
    }

    .lot-hero-title {
        margin: 8px 0 0;
        font-family: "Noto Sans Thai", "Open Sans", Arial, sans-serif;
        font-size: 26px;
        font-weight: 600;
        line-height: 1.2;
        color: var(--eg-ink);
    }

    .lot-hero-count {
        display: inline-flex;
        align-items: center;
        gap: 8px;
        margin-top: 10px;
        font-size: 14px;
        color: var(--eg-subtle);
    }

    /* Vertical space only — the horizontal edges belong to .container. */
    .lot-band {
        padding: 20px 0 34px;
    }

    /* A light stand-in for the legacy btn-gradient-secondary bar: going back is the
       quietest control on the page, so it is a bordered glass surface with ink text
       rather than a filled gradient. */
    .lot-back {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        gap: 8px;
        width: 100%;
        height: 42px;
        border: 1px solid var(--eg-border);
        border-radius: var(--radius);
        background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(244, 250, 255, 0.84));
        color: var(--eg-ink);
        font-weight: 500;
        letter-spacing: 0.02em;
    }

    .lot-back:hover {
        background: var(--hover-bg);
        color: var(--eg-ink);
    }

    .lot-back .material-symbols-outlined {
        font-size: 18px;
    }

    @media (max-width: 575.98px) {
        .lot-hero {
            padding-top: 12px;
        }

        .lot-hero-panel {
            padding: 16px 14px 18px;
        }

        .lot-hero-title {
            font-size: 21px;
        }
    }

/* ── AuctionLotItem.cshtml ───────────────────────────────────────────── */
        /* The detail card keeps one shadow in both states. It is a whole page in a card -
           the photo, the price, the countdown and the bid history all live inside it - so
           the lift that tells a member "you can pick this up" is a promise this one does not
           keep, the same reasoning as .info-panel and .lot-hero-panel.

           BOTH states have to be named, and it has to be here: `.box-a:hover` in
           elementa-glass.css carries `!important` and the same specificity, and a sheet that
           loads later is the only thing that can hold it still. z-index stays auto because
           this card is alone in its column - the families in a grid need the raised card to
           sit above its neighbours, and there are none here. */
        .lot-card,
        .lot-card:hover {
            box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.74), var(--eg-shadow) !important;
            z-index: auto;
        }

        /* ── Pigeon list modal ──
           A card like any other: the theme's ink on the theme's surface and hover / active
           tints taken from --hover-bg and the accent. The dark navy header (#1b3349) and the
           #f0f4ff / #e8f0fe rows were the legacy palette. */
        .pigeon-list-row { transition: background .15s; color: inherit; }
        .pigeon-list-row:hover { background: var(--hover-bg); }
        .pigeon-list-row--active { background: rgba(0, 115, 199, 0.08) !important; }
        .pigeon-list-row--active:hover { background: rgba(0, 115, 199, 0.13) !important; }
        /* A row is `d-flex`, and Bootstrap's utilities are !important, so the browser's own
           [hidden] loses to display:flex - without this line a filtered-out row stays on
           screen. */
        .pigeon-list-row[hidden] { display: none !important; }
        /* The card is the one User → Change Password wears (`box-a`), with ONE difference: a
           fixed height. The list is filtered client-side, and a content-sized dialog shrank
           from 18 rows to 1 while the member typed. min() keeps it inside a short window.
           The clipping is the only thing `box-a`'s own rules cannot do - its header has to be
           cut to the card's corners. */
        #pigeonModal .modal-content {
            height: min(470px, 84vh);
            overflow: hidden !important;
        }
        .box-a, .box-a:hover { background: #fff !important; }

        /* ── Header bar ── */
        .lot-header { background: #f8f9fa; border-bottom: 1px solid #e9ecef; }

        /* ── Main image ── */
        .lot-main-img-wrap { background: #eee; min-height: 200px; }
        .lot-main-img { object-fit: cover; max-height: 380px; border-radius: 8px; cursor: zoom-in; }
        /* The card's two photo chips at this page's scale, in the card's own corners:
           the card insets them 18px because its photo panel carries 16px of padding,
           while this wrap has none, so they take a plain 12px. */
        .lot-main-img-wrap .pc-zoom { top: 12px; right: 12px; }
        .lot-main-img-wrap .pc-status { top: 12px; left: 12px; }

        /* Thumbnail strip on the lot detail page.
           These rules lived in pigeon.css, which this app stopped loading when it moved to
           Elementa + Glass, so .img-thumb was an UNDEFINED class: the strip drew every photo
           at its natural size instead of a 64px square. Width and height are stated here (as
           well as on the element) because a box the browser can size before the bytes arrive
           is a row that does not move when they do. */
        .img-thumb {
            width: 64px;
            height: 64px;
            object-fit: cover;
            border-radius: 6px;
            cursor: pointer;
            border: 2px solid #e9ecef;
            transition: border-color .15s;
        }
        .img-thumb--active,
        .img-thumb:hover { border-color: #1b3349; }

        /* ── Nav buttons ──
           One segmented bar rather than three grey slabs. Every value is the theme's: the
           surface is the bordered glass .lot-back wears, --radius sits on the outer corners
           only, and the colours are var(--eg-border) / var(--eg-ink) with the accent on
           hover - the previous #e8edf2 / #dbe8f8 / #2c5282 came from the legacy theme and
           were the only off-palette surfaces left on this page.
           The pieces share ONE border: the centre keeps its two side strokes, the arrows drop
           the stroke they would otherwise double. */
        .lot-nav { gap: 0; }
        .lot-nav-prev, .lot-nav-next, .lot-nav-center {
            height: 42px;
            border: 1px solid var(--eg-border);
            background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(244, 250, 255, 0.84));
            color: var(--eg-ink);
        }
        .lot-nav-prev, .lot-nav-next { width: 42px; flex: none; }
        .lot-nav-prev { border-radius: var(--radius) 0 0 var(--radius); border-right: none; }
        .lot-nav-next { border-radius: 0 var(--radius) var(--radius) 0; border-left: none; }
        .lot-nav-center { border-radius: 0; gap: 1px; }
        .lot-nav-prev:hover, .lot-nav-next:hover, .lot-nav-center:hover {
            background: var(--hover-bg);
            /* The border has to be re-stated or the frame vanishes under the pointer:
               Bootstrap's `.btn:hover` sets `border-color: var(--bs-btn-hover-border-color)`,
               and that token is `transparent` in this theme. The bar's frame is not decoration -
               it is what holds the three pieces together as one control - so it is put back.
               Same specificity as `.btn:hover` (one class + :hover) and this sheet loads last,
               so naming it here is enough; no !important needed. */
            border-color: var(--eg-border);
            color: var(--eg-ink);
        }
        /* The arrows drop the stroke they share with the centre, and a hover that put it back
           would draw a double line: those two stay width-less on hover too. */
        .lot-nav-prev:hover { border-right: none; }
        .lot-nav-next:hover { border-left: none; }
        .lot-nav-icon { font-size: 24px; color: rgba(100, 116, 139, 0.9); }
        /* The arrows are the only part that does something to the page itself, so they are
           what takes the accent on hover; the centre opens the list and keeps the ink. */
        .lot-nav-prev:hover .lot-nav-icon, .lot-nav-next:hover .lot-nav-icon { color: var(--accent-600); }
        .lot-nav-ring { font-size: 17px; }
        .lot-nav-pos { font-size: 11px; opacity: .7; }

        /* ── Bid panel ──
           These five one-liners predate the panel's own block further down (see "Place a
           Bid"): every property they set is stated there, in a rule that also carries the
           theme tokens, and a duplicate that only half-covers the same classes is how a
           later reader ends up changing the wrong one. */

        /* ── Price block ── */
        /* The price and the countdown share this row as 40/60, not 50/50: the price is one
           figure, the countdown carries a status line and two labelled rows under it, so the
           wider half belongs to the countdown. Bootstrap's grid cannot say 40/60 (4.8 and 7.2
           of twelve) and both columns stay `col-md-6` because the phone stacks them - width
           rather than flex-basis, because that is what the grid sets on them (`.col-md-6`
           is `flex: 0 0 auto; width: 50%`, so an override that only moved flex-basis would
           change nothing). Only from the breakpoint where the two become a pair, which is
           the same one the grid uses. `lot-split` is the marker the row carries in the
           markup, the way `lot-cd` marks the column. */
        @media (min-width: 768px) {
            .lot-split > .col-md-6:first-child { width: 40%; max-width: 40%; }
            .lot-split > .col-md-6:last-child { width: 60%; max-width: 60%; }
        }

        /* The price is the .lot-price-box panel; its surface is in elementa-glass.css.
           With the status chip and the bid count gone the box carries the price alone,
           so its content is centred in the panel the way the countdown's lines are. */
        .lot-price-box { display: flex; flex-direction: column; justify-content: center; }

        /* ── Countdown ── */
        /* The panel below the price is the shared one: the same rules the bidding item
           card and the Auction Lot card wear, in elementa-glass.css, scoped by the
           `lot-cd` marker on this column. */

        /* ── Info rows ── */
        /* Each row leads with its icon, the way the rest of the app's rows do, and reads
           as caption + value: the caption and the icon keep the row's grey while the
           value takes the app's ink, so the data is what the eye lands on. The row
           carries the grey itself now (.text-grey is !important and would have beaten
           any colour this file sets on the value). The icon is a flex item, so a value
           long enough to wrap keeps its text in a column of its own instead of sliding
           back under the icon. */
        /* The row aligns its items to the TOP: a value long enough to wrap (a long
           fancier on a phone) used to push its caption to the middle of the two-line
           row, so the caption floated between the lines instead of opening the first
           one. The icon takes the row's own line-height further down, which is what
           keeps "top" meaning the centre of the first line the words sit on. */
        .lot-info { display: flex; align-items: flex-start; gap: 6px; color: #61708a; }
        /* The rows are a table in disguise: the icon and the caption are fixed columns,
           so every value starts on the same vertical line however long its own caption
           is. "Ring No.:" is the widest the fields can print - 65.55px measured at 15px
           in Noto Sans Thai with the block's 0.01em tracking - so the caption column is
           66px, and the row's own 6px gap is what separates it from the value. */
        .lot-info-cap { flex: none; min-width: 66px; }        .lot-info-value { min-width: 0; color: var(--eg-ink); }
        /* The icon takes the row's OWN line box: font-size 15px, the caption's size, so the
           inherited 1.5 leading gives both a 22.5px box with the same top. That is what
           keeps a highlighted row even - the selection band follows the line box, so a
           taller icon box (17px gave 25.5px) or a 1px nudge drew a band over the icon
           that rode above the band over the words. The glyph then sits on the row's
           baseline rather than 1px higher; the row reads as one line either way, and an
           even highlight is worth more than that pixel.

           user-select: none is the half that actually evens the band out: the selection
           over an icon is painted on the ICON FONT's own box (ascent 1.12em + descent
           0.12em = 18.5px at 15px) while the words carry Noto Sans Thai's (23px), so no
           icon size makes the two bands coincide. Out of the selection, the icon is not
           painted at all and the highlight runs unbroken over the words - and the row no
           longer copies "person" or "description" out as text. */
        .lot-info .material-symbols-outlined { flex: none; font-size: 15px; line-height: inherit; color: rgba(100, 116, 139, 0.75); user-select: none; -webkit-user-select: none; }

        /* The one row whose value is an ACTION rather than a value (the family tree button under
           Gender). It keeps the row's ink and only underlines the words, so the table still reads
           as a table. It is a real <button>, so the browser's own button chrome is taken off it -
           `font: inherit` first, then the weight, because the shorthand would reset it. */
        .lot-info-link { background: none; border: 0; padding: 0; font: inherit; color: #0073c7; font-weight: 600; text-decoration: underline; text-underline-offset: 2px; cursor: pointer; }
        .lot-info-link:hover { color: #005fa3; }

        /* ── Description ── */
        /* The block reads as caption + value like the info rows above it: the caption
           stays the row's grey and the member's own text takes the same ink a value
           does. The grey is set here rather than with .text-grey on the wrapper, whose
           !important would beat any colour this file sets on the text. */
        .lot-desc { color: #61708a; }
        /* display: block, not inline: an inline box wears its left padding on the first
           line only, so a wrapped line would slide back under the icon. 21px is the
           heading's own left edge - the 15px icon plus the row's 6px gap - so every line
           of the member's text starts under the word "Description". */
        .lot-desc .tec-html { display: block; padding-left: 21px; color: var(--eg-ink); }

        /* The description is HTML the member typed, and their editor starts a new <p>
           on Enter, so a list of short results arrives as six paragraphs - each one
           carrying Bootstrap's 1rem bottom margin, which reads as a gap between lines
           that belong together. The old theme carried exactly this rule (.tec-html p)
           with margin and padding 0; it went with the sheets that are no longer loaded,
           so it is ported back here and scoped to this block. */
        .lot-desc .tec-html p { margin: 0; padding: 0; }

        /* ── Place a Bid ──
           The panel worn the old bundle's class names, none of which this sheet carries,
           so it rendered as loose labels lying on the card. It is a compact box now - a
           tool, not a page: a filled surface a step darker than the card (the "different
           background" that says "this one is for acting on"), on the same hairline and
           radius as the panels beside it, with the bid and the ceiling beside each other
           on one line rather than stacked in three. */
        .bid-panel {
            position: relative;
            padding: 8px 12px 10px;
            border: 1px solid var(--eg-border);
            border-radius: var(--radius);
            background: var(--surface-100);
        }

        /* ── The panel's covers ──
           Two states make the panel unusable - a bid in flight, and a lot that has closed -
           and both say so the same way: a blurred sheet over the fields with the reason
           written on it, rather than fields that quietly do nothing. (--surface-100 at 55%,
           so the sheet is the panel's own colour made translucent.) */
        .bid-panel__overlay {
            position: absolute;
            inset: 0;
            z-index: 1;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            gap: 6px;
            border-radius: var(--radius);
            background: rgba(241, 245, 249, 0.55);
            -webkit-backdrop-filter: blur(2.5px);
            backdrop-filter: blur(2.5px);
            font-size: 13px;
            font-weight: 600;
            line-height: 1.3;
            color: var(--eg-ink);
            text-align: center;
        }

        .bid-panel__overlay-text { max-width: 100%; }

        /* [hidden] has to be said out loud: the rule above sets a display, and a display
           beats the attribute's own. */
        .bid-panel__overlay[hidden] { display: none; }

        .bid-panel__overlay-icon {
            flex: none;
            font-size: 22px;
            line-height: 1;
            color: var(--accent-600);
        }

        /* ── While a bid is in flight ──
           The theme's warning pair, not the panel's own slate: amber is the colour this
           sheet already uses for "something is in hand" (the check-countdown ring, the
           "soon" bar), and warning.strong #d97706 / alertTones.warning text #92400e are the
           values it defines. The icon is a refresh, turned by a keyframe - so it reads as
           work in progress before the sentence is read. */
        .bid-panel__overlay--busy {
            border: 1px solid rgba(245, 158, 11, 0.45);
            background: rgba(245, 158, 11, 0.16);
            color: #92400e;
        }

        .bid-panel__overlay--busy .bid-panel__overlay-icon {
            color: #d97706;
            animation: bid-spin 0.9s linear infinite;
        }

        @keyframes bid-spin {
            to { transform: rotate(360deg); }
        }

        @media (prefers-reduced-motion: reduce) {
            .bid-panel__overlay--busy .bid-panel__overlay-icon { animation: none; }
        }

        /* The two covers the SERVER draws - a lot that has closed, and one whose window has
           not opened yet - are the same stacked icon-over-line, only still and quiet: both
           are facts rather than states to act on, and the difference between them is the
           icon and the line, not the skin. (The busy sheet above is the third, and the only
           one that animates.) */
        .bid-panel__overlay--ended .bid-panel__overlay-icon,
        .bid-panel__overlay--upcoming .bid-panel__overlay-icon { color: var(--eg-subtle); }

        .bid-panel__header {
            display: flex;
            align-items: center;
            gap: 7px;
            margin-bottom: 6px;
            font-size: 14px;
            font-weight: 600;
            line-height: 1.3;
            color: var(--eg-ink);
        }

        .bid-panel__icon {
            flex: none;
            font-size: 16px;
            line-height: 1;
            color: var(--accent-600);
            user-select: none;
            -webkit-user-select: none;
        }

        .bid-panel__body {
            display: flex;
            flex-direction: column;
            gap: 6px;
        }

        /* The line of controls, in the order the decision happens: your bid, the ceiling
           behind it, then the button. The button carries no label, so flex-end puts its
           bottom on the fields' bottom line. */
        .bid-panel__fields {
            display: flex;
            flex-wrap: wrap;
            align-items: flex-end;
            gap: 8px 14px;
        }

        .bid-panel__field {
            display: flex;
            flex-direction: column;
            min-width: 0;
        }

        .bid-panel__field--bid { flex: 1 1 180px; }
        /* The three bases decide one thing only - whether they share a line. 180 + 14 + 140 +
           14 + 111 is 459, which fits the 486px of content this panel has when the column is
           512 wide; at 220 the same three wrapped the button onto a line of its own. The
           actual widths still come from the bid group growing to fill and, below that, from
           the ceiling group shrinking. */
        .bid-panel__field--max { flex: 0 1 140px; }

        .bid-panel__row { min-width: 0; }

        .bid-panel__label {
            display: block;
            margin-bottom: 3px;
            font-size: 11px;
            font-weight: 500;
            color: var(--eg-subtle);
        }

        .bid-opt-label {
            font-size: 10px;
            font-weight: 400;
            color: var(--eg-subtle);
        }

        .bid-panel__select {
            min-width: 0;
            height: 36px;
            padding: 0 9px;
            border: 1px solid var(--eg-border);
            border-radius: var(--radius);
            background: #fff;
            color: var(--eg-ink);
            font-size: 13px;
        }

        /* One height for the two dropdowns and the button, so the line of three shares one
           top and one bottom edge. Each field fills the group that sizes it - the group,
           not the control, is the item on the line.

           The control draws its own chevron: the native arrow is pinned to the right edge
           of the box, which on the 140px Max Bid select leaves it almost touching the
           border. `appearance: none` takes the native one away, the chevron is an inline
           SVG in --eg-subtle (#66748f - a data URI is its own document and cannot read a
           custom property, so the value is written out), and the 30px right padding keeps
           the text from running under it. */
        select.bid-panel__select {
            width: 100%;
            padding-right: 30px;
            appearance: none;
            -webkit-appearance: none;
            background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none' stroke='%2366748f' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 4.5 6 8l3.5-3.5'/%3E%3C/svg%3E");
            background-repeat: no-repeat;
            background-position: right 10px center;
            background-size: 12px 12px;
        }

        .bid-panel__select:focus {
            outline: none;
            border-color: var(--accent-600);
            box-shadow: 0 0 0 3px rgba(0, 115, 199, 0.14);
        }

        .bid-panel__btn {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            gap: 6px;
            flex: none;
            height: 36px;
            padding: 0 12px;
            white-space: nowrap;
        }

        .bid-btn-icon {
            font-size: 16px;
            line-height: 1;
        }

        .bid-hint {
            font-size: 12px;
            line-height: 1.45;
            color: var(--eg-subtle);
        }

        .bid-hint strong {
            font-weight: 600;
            color: var(--eg-ink);
        }

        /* What the bid answers with. The theme's own success and error pairs, rather than
           the default paint of an unstyled div. */
        .bid-panel__msg {
            padding: 7px 10px;
            border: 1px solid var(--eg-border);
            border-radius: var(--radius);
            background: #fff;
            font-size: 12px;
        }

        .bid-panel__msg--ok {
            border-color: #bbf7d0;
            background: #f0fdf4;
            color: #166534;
        }

        .bid-panel__msg--err {
            border-color: #fecaca;
            background: #fef2f2;
            color: #991b1b;
        }

        /* Signed out: the box stays, the form goes. */
        .bid-lock-body {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 6px;
            padding: 2px 0;
            font-size: 13px;
            color: var(--eg-subtle);
        }

        .bid-lock-icon {
            flex: none;
            font-size: 18px;
            line-height: 1;
        }

        /* ── The price moved ──
           A bid lands through SignalR, the handler re-reads the history and the price box
           takes the lot's new figure and says so: the panel fades the theme's amber (the
           same warning pair the in-flight cover wears) IN over a second, and the class comes
           off at the peak so it fades back OUT over the next one - two seconds, no hold in
           the middle. A transition rather than a keyframe, so the way out is the sheet's own
           frosted fill and not a copy of it written here. */
        .lot-price-box {
            transition: background-color 1s ease-in-out, border-color 1s ease-in-out;
        }

        /* The panel's own fill is set by the sheet through
           `:is(..., .lot-cd) .lot-price-box` - two classes plus one - so a single-class rule
           here loses to it even though it comes later. Matching that weight is what makes the
           flash paint at all. */
        .lot-cd .lot-price-box.is-price-updated {
            background-color: rgba(245, 158, 11, 0.42);
            border-color: rgba(245, 158, 11, 0.8);
        }

        /* ── The member's own ceiling ──
           The price box centres its stack (justify-content: center in the sheet), so the
           line can come and go without the box growing lopsided - see the measurement in
           the note on .lot-price-box. Colour is the theme's error red (status.error.strong):
           the sheet defines no danger variable, and this is the red it defines that still
           holds contrast at 11px on the frosted panel. */
        .lot-maxbid {
            color: #dc2626;
        }

        /* ── Bid history ──
           The table wore the old theme's chrome: a solid navy header bar and Bootstrap's
           default cells, with no padding on the cells at all (only the <th>s carried
           px-3 py-2), so the columns were cramped and the date wrapped onto two lines.
           It is the sheet's quiet list now - a hairline box on the theme's border, a
           small uppercase header, one bid per line separated by a hairline rather than
           striped, and the standing bid tinted with its price in the accent.

           Every row in it is written by this page's JS, three ways: the first page, the
           next page for "Load More", and the whole first page again whenever SignalR
           says a bid landed. So the classes below are the ones that JS already writes
           (none of them is new), and the reveal is the transition that JS has always set
           up - it fades and lifts each row in on a staggered timeout. */
        .bid-history-badge {
            padding: 3px 10px;
            border: 1px solid var(--eg-border);
            border-radius: 9999px;
            background: #fff;
            color: var(--eg-subtle);
            font-size: 12px;
            font-weight: 500;
            font-variant-numeric: tabular-nums;
        }

        .bid-table-wrap {
            border: 1px solid var(--eg-border);
            border-radius: var(--radius);
            overflow: hidden;
        }

        #MemberBidding {
            border-collapse: collapse;
            font-size: 13px;
        }

        .bid-thead-row {
            background: var(--surface-100);
            color: var(--eg-subtle);
        }

        .bid-th {
            padding: 7px 10px;
            border-bottom: 1px solid var(--eg-border);
            font-size: 11px;
            font-weight: 600;
            letter-spacing: 0.06em;
            text-transform: uppercase;
            white-space: nowrap;
        }

        .bid-th--num { width: 40px; }
        .bid-th--price { text-align: right; }

        .bid-row td {
            padding: 7px 10px;
            border-top: 1px solid var(--eg-border);
            vertical-align: middle;
        }

        /* No line under the header's own: it already draws one. */
        .bid-row:first-child td { border-top: 0; }

        .bid-cell-num,
        .bid-cell-price {
            white-space: nowrap;
            font-variant-numeric: tabular-nums;
        }

        .bid-cell-num {
            width: 40px;
            color: var(--eg-subtle);
        }

        .bid-cell-date {
            color: var(--eg-ink);
            white-space: nowrap;
            font-variant-numeric: tabular-nums;
        }

        .bid-cell-date-time {
            margin-left: 1px;
            color: var(--eg-subtle);
            font-size: 12px;
        }

        .bid-cell-bidder {
            max-width: 150px;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }

        /* The pair, for the width with room for only one column: the name, then the country
           under it. It is written for every row and shown only where the desktop's two
           separate columns are not. */
        .bid-cell-bidder-merged {
            display: none;
            max-width: none;
            overflow: visible;
            text-overflow: clip;
            white-space: normal;
        }

        /* Each of the two lines ellipsises on its own - a table cell cannot do that for
           two lines at once. */
        .bid-cell-bidder-merged .bid-cell-name,
        .bid-cell-country-line {
            display: block;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }

        .bid-cell-country-line {
            margin-top: 1px;
            color: var(--eg-subtle);
            font-size: 12px;
        }

        .bid-cell-flag {
            width: 16px;
            height: 11px;
            margin-right: 6px;
            border-radius: 2px;
            vertical-align: -1px;
        }

        .bid-cell-price {
            text-align: right;
            font-weight: 600;
            color: var(--eg-ink);
        }

        /* The price cell is the one cell that carries two things: the amount, which belongs
           on the column's right edge like every other amount down the table, and - when the
           house bid on its own behalf - a badge, which belongs on its left. The cell itself
           stays a table cell (display: flex would take it out of the table) and the pair
           inside it is laid out by a flex span: badge left, amount pushed to the right. */
        .bid-cell-price-inner {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 8px;
        }

        .bid-cell-amount {
            margin-left: auto;
        }

        /* A fact about the row, not a warning, so it wears the same quiet pill as the bid
           count above (white, hairline, subtle text) rather than a status colour. It is
           written with the tokens the sheet actually defines: --surface-200 and
           --surface-700 are Elementa's own, not this port's, and a rule that used them
           came out transparent and inherited. */
        .bid-bo {
            flex: none;
            padding: 1px 6px;
            border: 1px solid var(--eg-border);
            border-radius: 9999px;
            background: #fff;
            color: var(--eg-subtle);
            font-size: 10px;
            font-weight: 600;
            letter-spacing: 0.04em;
        }

        /* The bid that stands: tinted row, price in the accent - what the eye should land
           on first after a realtime re-render. */
        .bid-row--top td { background: rgba(0, 115, 199, 0.06); }
        .bid-cell-price--top { color: var(--accent-600); }

        .bid-row:hover td { background: var(--hover-bg); }

        /* The reveal the JS drives, for all three ways rows arrive. */
        .bid-row {
            opacity: 0;
            transform: translateY(4px);
            transition: opacity 0.28s ease, transform 0.28s ease;
        }

        .load-more-row td {
            padding: 9px 10px;
            border-top: 1px solid var(--eg-border);
        }

        .load-more-row .btn {
            padding: 5px 12px;
            border: 1px solid var(--eg-border);
            background: #fff;
            color: var(--accent-600);
            font-size: 13px;
            font-weight: 500;
        }

        .load-more-row .btn:hover { background: var(--hover-bg); }
        .load-more-row .btn:disabled { opacity: 0.6; cursor: default; }

        @media (prefers-reduced-motion: reduce) {
            .bid-row { opacity: 1; transform: none; transition: none; }
        }

        @media (max-width: 575.98px) {
            /* Five columns do not fit a phone. The ordinal goes; the clock drops back under
               its date; and the bidder and the country stop being two columns and become one
               - the name, then the country under it - which is the shape that survives a
               narrow screen. (The JS writes both arrangements for exactly this.)

               The two narrow columns are held open by their HEADINGS, not their data, so
               the country heading and the currency unit come off too, or the table still
               overflows the card by a word's worth. */
            .bid-th--num,
            .bid-cell-num,
            .bid-th--country,
            .bid-th-unit,
            .bid-cell-bidder-name,
            .bid-cell-bidder-country-cell { display: none; }

            .bid-cell-bidder-merged { display: table-cell; }

            .bid-th,
            .bid-row td { padding-left: 7px; padding-right: 7px; }

            .bid-cell-bidder { max-width: 110px; }

            .bid-cell-date-time {
                display: block;
                margin-left: 0;
            }
        }

        /* ── Modal dialog ─────────────────────────────────────────────────────────────
           The chrome is the change-password dialog's: the same `.modal-content box-a
           border-0` card, the same white header carrying a 42px glyph (from the global
           .modal-title rule) beside a 20px title. Only the height is a decision of its own,
           and it is taken above. */
        .pigeon-modal-dialog { max-width: 580px; }
        /* A long lot name may not push the close button off the header. */
        #pigeonModal .modal-title { min-width: 0; }
        .pigeon-list-count {
            flex: 0 0 auto;
            margin-left: 10px;
            padding: 2px 9px;
            border: 1px solid var(--eg-border);
            border-radius: 999px;
            font-size: 12px;
            font-weight: 500;
            color: #64748b;
            white-space: nowrap;
        }

        /* ── Search row ──
           The list is every pigeon in the lot - 18 here, a hundred on a big one - so it
           filters as the member types. It is the first row of the card's own column rather
           than part of the scroll area, so the field keeps its place while the rows move
           under it, and it takes the header's 16px gutter so the field lines up with the
           title above it. */
        .pigeon-search {
            position: relative;
            flex: 0 0 auto;
            padding: 12px 16px;
            border-bottom: 1px solid var(--bs-border-color);
            background: #fff;
        }
        .pigeon-search > .material-symbols-outlined {
            position: absolute;
            left: 28px;
            top: 50%;
            transform: translateY(-50%);
            font-size: 20px;
            color: #94a3b8;
            pointer-events: none;
            user-select: none;
        }
        /* A form-control, like the fields in the change-password dialog: same 38px height,
           same 16px text, the theme's 4px corners. The padding reserves the two glyphs. */
        .pigeon-search-input {
            padding-left: 40px;
            padding-right: 40px;
            border-radius: var(--radius);
        }
        .pigeon-search-input:focus {
            border-color: var(--accent-600);
            box-shadow: 0 0 0 3px rgba(0, 115, 199, 0.14);
        }
        /* `type="search"` brings WebKit's own clear glyph, which would sit beside ours. */
        .pigeon-search-input::-webkit-search-cancel-button { -webkit-appearance: none; display: none; }
        /* The clear button reuses the icon font, so it is a glyph and not an image. */
        .pigeon-search-clear {
            position: absolute;
            right: 26px;
            top: 50%;
            transform: translateY(-50%);
            border: 0;
            background: none;
            padding: 2px;
            font-size: 20px;
            line-height: 1;
            color: #94a3b8;
            cursor: pointer;
        }
        .pigeon-search-clear:hover { color: var(--eg-ink); }
        .pigeon-search-clear[hidden] { display: none; }

        /* The list takes whatever is left of the fixed card and scrolls; the empty message is
           its own last line, so "no match" reads as the end of the list rather than a second
           panel pinned under the field. */
        #pigeonModal .modal-body {
            display: flex;
            flex-direction: column;
            min-height: 0;
        }
        .pigeon-list-wrap {
            flex: 1 1 auto;
            min-height: 0;
            overflow-y: auto;
            overscroll-behavior: contain;
        }
        .pigeon-list-num { min-width: 28px; color: #94a3b8; font-size: 14px; }
        .pigeon-list-ring { font-size: 14px; color: var(--eg-ink); }
        .pigeon-list-title { font-size: 14px; color: var(--eg-ink); }
        .pigeon-list-check { font-size: 18px; color: var(--accent-600); }
        .pigeon-list-empty {
            padding: 22px 16px;
            text-align: center;
            font-size: 14px;
            color: #64748b;
        }
        .pigeon-list-empty[hidden] { display: none; }
    

/* ── MyAuction.cshtml ─────────────────────────────────────────────────────────────
    The page opened with a `page-header-modern bg-color-primary` band carrying its title. That
    class belongs to the legacy theme the app no longer loads, so the band had no fill of its own
    and its `text-light` title painted white on the light page - the page looked headless. It was
    briefly a glass heading panel; on request that is gone too, so the page now starts on its
    first list, like every other member page. `py-5` on the wrapper is what holds its distance
    from the header.

    The tables STAY tables: five of these columns are figures to compare down a column, which is
    what a table is for. What they lose is Bootstrap's chrome - the solid header band, the
    wrapped dates, the solid badges - and what they gain is this sheet's own list: a hairline box
    on the theme's border, a small uppercase head, one row per bid separated by a hairline, the
    figures in tabular numerals, and the state in the same pill the invoice cards use.

    (The cards on this page hold their shadow under the pointer like every read-only panel in the
    app - that is said once at the end of this file, not here.) */
    .member-table-wrap {
        border: 1px solid var(--eg-border);
        border-radius: var(--radius);
        overflow: hidden;
    }

    .member-table {
        width: 100%;
        margin: 0;
        border-collapse: collapse;
        font-size: 13px;
    }

    /* The head WRAPS and the figures below it do not. "Current Price (THB)" on one line is
       149px of a 724px table, and that 149px is taken straight out of the title column - which
       then breaks a pigeon's name over three lines. The head is two short lines instead, and
       the columns below it keep their widths. */
    .member-table thead th {
        padding: 8px 10px;
        border-bottom: 1px solid var(--eg-border);
        background: var(--surface-100);
        color: var(--eg-subtle);
        font-size: 11px;
        font-weight: 600;
        letter-spacing: 0.06em;
        text-transform: uppercase;
    }

    .member-table tbody td {
        padding: 8px 10px;
        border-top: 1px solid var(--eg-border);
        vertical-align: middle;
    }

    /* The head already draws one, so the first row would double it. */
    .member-table tbody tr:first-child td { border-top: none; }

    .member-table tbody tr:hover td { background: var(--hover-bg); }

    /* Figures are read down a column, so they are not allowed to wrap: "8 Oct 26 17:49" on two
       lines is a date nobody can scan, and tabular digits keep the amounts in one column. */
    .member-table .col-nowrap { white-space: nowrap; }
    .member-table .col-num { font-variant-numeric: tabular-nums; }

    .member-table .col-idx {
        width: 44px;
        color: var(--eg-subtle);
    }

    /* The one link in a row, in the accent the rest of the app links in. */
    .member-table .title-cell a {
        color: var(--accent-600);
        font-weight: 500;
    }

    .member-table .title-cell a:hover { text-decoration: underline; }

    /* ── The member area's section heading ──
       Every member page is a stack of panels, and each panel opens with a heading and - when the
       section has one - that section's action at the right end of the same line (Change Password
       for the personal block, Upload Payment for the invoices, Mark all as read for the notices).

       The band, not just the heading: 45px is the tallest section action plus the 8px padding (a
       full-size .btn beside a heading measures 36.8px in this theme, and a .btn-sm at 32px simply
       centres inside), and the box is border-box, so the padding is inside the 45px. Because the
       band is a fixed height, a panel whose section has NO action keeps it as well - which is
       what puts the heading's line, and the first row of content under it (the panel's 24px
       padding + 45px + 16px), at the same height in every panel of every member page.

       There was a 1px rule under this band for one round - var(--bs-border-color), the line the
       auction panels used to draw on their own <h6> - so that a member page read as ruled
       sections. Removed on request: the panel is already bounded by its own border, and a second
       line, a heading's width inside it, was one line too many. Do not put it back. */
    .section-head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 8px;
        min-height: 45px;
        padding-bottom: 8px;
        margin-bottom: 16px;
    }


/* ── Profile.cshtml ───────────────────────────────────────────────────────────── */
    /* The page's cards wear the invoice items' corner (--radius, 4px). This page asked for it
       first, and it is now the corner every `.box-a` carries - stated once, globally, in the
       last section of this sheet. The line stays here because the reason is this page's: a card
       here is a list of labelled rows, so the same radius the rows and the fields use is what
       makes the card and its content read as one system. (The dialogs are not inside
       .profile-page - the global rule is what reaches those.) */
    .profile-page .box-a {
        border-radius: var(--radius);
    }

    /* The member's own fields take the radius the login card uses (the theme's --radius, 4px)
       rather than the 10px the global .form-control rule carries. Same kind of card, same
       background, same job - and next to the 4px buttons a 10px field is what made this page
       read as a different app.

       The two dialogs are named as well: they are modal siblings of this container, not
       children of it, and both are filled in from this page (the payment slip and the change
       of password). */
    .profile-page .form-control,
    .profile-page .form-select,
    #uploadPaymentModal .form-control,
    #changePasswordModal .form-control {
        border-radius: var(--radius);
    }

    /* ── Invoices, as cards ──
       One card per invoice. The table this replaces put five columns into a 486px column:
       the description wrapped onto three lines, "Price (THB)" broke into two, and the number
       and the amount were squeezed against them. A card gives the description a line of its
       own - which is what multi-line needs - and puts the three things a member scans for
       (which invoice, how much, due when) on its edges. Quiet tint rather than a second
       white panel: the card it sits in is white, and the app's own nested panels (the bid
       panel) are exactly this pair - --surface-100 on --eg-border. */
    .invoice-list {
        display: flex;
        flex-direction: column;
        gap: 10px;
        margin: 0;
        padding: 0;
        list-style: none;
    }

    .invoice-card {
        padding: 12px 14px;
        border: 1px solid var(--eg-border);
        border-radius: var(--radius);
        background: var(--surface-100);
    }

    /* `flex-start` and not `space-between`: the status pill takes the right edge with its own
       `margin-left: auto`, so anything else on the line - the Upload Payment button - sits
       right after the invoice number where the eye already is, instead of floating mid-card. */
    .invoice-card__head {
        display: flex;
        align-items: center;
        gap: 10px;
        margin-bottom: 5px;
    }

    /* Whatever sits beside it, the state owns the right edge of the card's first line. */
    .invoice-card__head .invoice-card__status { margin-left: auto; }

    .invoice-card__no {
        font-size: 14px;
        font-weight: 600;
        font-variant-numeric: tabular-nums;
    }

    /* State pills, one rule for two names: the invoice card's status and the member tables'
       Leading / Result column. A state should not have two colours on one page, and writing the
       geometry and the tones once is what keeps them from drifting apart. */
    .invoice-card__status,
    .member-pill {
        display: inline-block;
        flex: none;
        padding: 2px 10px;
        border: 1px solid transparent;
        border-radius: 9999px;
        font-size: 11px;
        font-weight: 600;
        letter-spacing: 0.02em;
        line-height: 1.6;
        white-space: nowrap;
    }

    /* The tones the sheet already speaks: amber is the "something is in hand" pair the busy
       bid cover uses (#92400e on rgba(245,158,11,.16)), green the winner's, red the
       maximum-bid mark, and slate every quiet mark in the app. A solid amber badge was the
       loudest thing on a page whose job is reading a list. */
    .invoice-card__status.is-paid,
    .member-pill.is-win {
        border-color: rgba(22, 163, 74, 0.35);
        background: rgba(22, 163, 74, 0.12);
        color: #15803d;
    }

    .invoice-card__status.is-pending {
        border-color: rgba(245, 158, 11, 0.45);
        background: rgba(245, 158, 11, 0.16);
        color: #92400e;
    }

    .invoice-card__status.is-overdue {
        border-color: rgba(220, 38, 38, 0.35);
        background: rgba(220, 38, 38, 0.12);
        color: #b91c1c;
    }

    .invoice-card__status.is-neutral,
    .member-pill.is-neutral {
        border-color: rgba(148, 163, 184, 0.35);
        background: rgba(148, 163, 184, 0.14);
        color: var(--eg-subtle);
    }

    /* The description is the one thing on the card that may be long, so it takes the card's
       full width and wraps - `anywhere` because the "&gt;&gt;" runs and ring numbers it carries
       have no space to break at. */
    .invoice-card__desc {
        margin: 0 0 8px;
        font-size: 13px;
        line-height: 1.45;
        color: var(--eg-subtle);
        overflow-wrap: anywhere;
    }

    .invoice-card__foot {
        display: flex;
        align-items: baseline;
        justify-content: space-between;
        gap: 10px;
        flex-wrap: wrap;
    }

    /* What the card can be asked to do. It is the quiet sibling of the page's own button - a
       hairline box on the theme's border, the accent as ink - and it rides on the card's first
       line, after the invoice number, because "pay this one" is decided by reading which
       invoice it is. It is only on the invoices that still expect a payment (see the view). */
    .invoice-card__pay {
        display: inline-flex;
        align-items: center;
        gap: 5px;
        padding: 3px 10px;
        border: 1px solid var(--eg-border);
        border-radius: var(--radius);
        background: #fff;
        color: var(--accent-600);
        font-size: 12px;
        font-weight: 500;
        line-height: 1.5;
        cursor: pointer;
    }

    .invoice-card__pay:hover {
        border-color: var(--accent-600);
        background: rgba(0, 115, 199, 0.06);
    }

    .invoice-card__pay .material-symbols-outlined {
        font-size: 15px;
        line-height: 1;
    }

    .invoice-card__amount {
        font-size: 15px;
        font-weight: 600;
        font-variant-numeric: tabular-nums;
    }

    .invoice-card__cur {
        font-size: 11px;
        font-weight: 500;
        color: var(--eg-subtle);
    }

    .invoice-card__due {
        font-size: 12px;
        color: var(--eg-subtle);
        font-variant-numeric: tabular-nums;
        white-space: nowrap;
    }

    /* ── Configuration: the email option, as rows you can click anywhere on ──
       The same card an invoice item wears - white, 1px of --eg-border, --radius - so the panel
       reads as one list of things to choose instead of as a form. Every row is a <label>, so
       the target is the whole card and not the 16px circle, and the chosen one is marked with
       the accent rather than with a second control: `:has(input:checked)` keeps that mark in the
       stylesheet, where no script can let it disagree with the radio it describes. */
    .email-option {
        display: flex;
        align-items: flex-start;
        gap: 10px;
        padding: 10px 12px;
        border: 1px solid var(--eg-border);
        border-radius: var(--radius);
        background: #fff;
        cursor: pointer;
        transition: background 0.15s ease, border-color 0.15s ease;
    }

    .email-option + .email-option {
        margin-top: 10px;
    }

    .email-option:hover {
        background: var(--hover-bg);
        border-color: #94a3b8;
    }

    .email-option:has(input:checked) {
        border-color: var(--accent-600);
        box-shadow: inset 0 0 0 1px var(--accent-600);
    }

    /* The browser's own radio, in the sheet's accent: a member has to be able to SEE which of
       the three is on, and a control drawn by hand is one more thing that can be out of date. */
    .email-option input[type="radio"] {
        flex: none;
        margin: 2px 0 0;
        accent-color: var(--accent-600);
    }

    .email-option__body {
        display: grid;
        gap: 2px;
        min-width: 0;
    }

    .email-option__title {
        font-size: 14px;
        font-weight: 600;
        color: var(--eg-ink);
    }

    .email-option__desc {
        font-size: 13px;
        color: var(--eg-subtle);
    }

    /* Upload Payment used to sit in an action row above the first box (.profile-actions).
       That row is gone: the button rides on the Invoices heading line now, so the page has one
       layout - a section heading with the section's own action on its right - instead of a row
       of page-level buttons up top and a button on a heading line below. Nothing replaced the
       class; a button in a `.d-flex ... gap-2` heading row needs no helper. */

    /* --- Upload payment: a zone, not a native "Choose File" ---------------------
       The zone is the visible control and the file input lives inside it: kept as the input
       so the form still posts one, moved out of sight with the standard clip recipe rather
       than `display: none`, because a control nobody can focus is a control nobody can use
       from the keyboard. Focus rings therefore come from the zone's own :focus-within, in the
       same 3px accent halo the rest of the app uses. */
    .upload-zone {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 2px;
        padding: 22px 16px;
        border: 1px dashed var(--eg-border);
        border-radius: var(--radius);
        background: rgba(248, 250, 252, 0.7);
        color: #64748b;
        text-align: center;
        cursor: pointer;
        transition: border-color 0.15s ease, background-color 0.15s ease;
    }

    .upload-zone input[type="file"] {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
        border: 0;
    }

    .upload-zone:hover {
        border-color: #94a3b8;
        background: var(--hover-bg);
    }

    .upload-zone:focus-within {
        border-color: rgba(86, 127, 189, 0.62);
        box-shadow: 0 0 0 3px rgba(0, 115, 199, 0.14);
    }

    /* The file is over the zone. Same idea as the hover, one step louder. */
    .upload-zone.is-dragover {
        border-color: var(--accent-600);
        background: rgba(0, 115, 199, 0.06);
    }

    .upload-zone-ico {
        font-size: 26px;
        color: #94a3b8;
    }

    .upload-zone-title {
        font-size: 14px;
        color: #475569;
    }

    .upload-zone-hint {
        font-size: 12px;
        color: #94a3b8;
    }

    /* `[hidden]` is a UA rule and loses to the `display` above, so both halves say it out
       loud - profile.js swaps the two with the hidden attribute alone. */
    .upload-zone[hidden],
    .upload-picked[hidden] {
        display: none;
    }

    /* What was browsed: the file's name leads, its size follows, and the ✕ is the way back
       to the zone. */
    .upload-picked {
        display: flex;
        align-items: center;
        gap: 10px;
        padding: 10px 12px;
        border: 1px solid var(--eg-border);
        border-radius: var(--radius);
        background: #fff;
    }

    .upload-picked-ico {
        font-size: 20px;
        color: var(--accent-600);
    }

    /* The chosen image, shown as itself. Fixed box so the row does not move between an image
       and a PDF, and `[hidden]` is repeated because a UA rule loses to the display above. */
    .upload-picked-thumb {
        display: block;
        width: 44px;
        height: 44px;
        flex-shrink: 0;
        border: 1px solid var(--eg-border);
        border-radius: var(--radius);
        background: #f1f5f9;
        object-fit: cover;
    }

    .upload-picked-thumb[hidden],
    .upload-picked-ico[hidden] {
        display: none;
    }

    .upload-picked-text {
        display: flex;
        flex-direction: column;
        min-width: 0;
        line-height: 1.3;
    }

    .upload-picked-name {
        font-size: 13px;
        color: #334155;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .upload-picked-size {
        font-size: 12px;
        color: #94a3b8;
    }

    .upload-picked-clear {
        margin-left: auto;
        display: inline-flex;
        padding: 3px;
        border: 0;
        border-radius: 9999px;
        background: transparent;
        color: #94a3b8;
        cursor: pointer;
        transition: background-color 0.15s ease, color 0.15s ease;
    }

    .upload-picked-clear:hover {
        background: #fee2e2;
        color: #b91c1c;
    }

    .upload-picked-clear .material-symbols-outlined {
        font-size: 18px;
    }

/* ── Register.cshtml ───────────────────────────────────────────────────────────── */
        /* A hand-rolled .modal block used to sit here: `display:none / position:fixed /
           z-index: 1050`, its own rgba(0,0,0,.4) background and `.modal-dialog { margin: 10%
           auto }`. It was written for the two popups layout.js used to open by hand, but an
           UNSCOPED `.modal` also matched every Bootstrap dialog in the app - and since it
           carried the same 1050 as `.modal-backdrop`, the backdrop (which Bootstrap appends
           after the dialog) painted straight over the dialog it belonged to. The helpers go
           through bootstrap.Modal now, so the block is gone and all dialogs keep Bootstrap's
           own 1055 over the 1050 backdrop, their centring, ESC and focus trap. */

        #countrySelect {
            background-repeat: no-repeat;
            background-position: 0.5em center;
            padding-left: 2.5em;
        }
    

/* ── StaticInformation.cshtml ───────────────────────────────────────────────────────────── */
    /* This page is one column of prose and has no title band of its own: the nav
       already names the section, and a heading would only repeat it.

       The text is Noto Sans Thai — the body face — at 14px. It used to sit inside
       `.tec-f2`, which is the sheet's condensed DISPLAY face, so a page of running
       text was set in a heading font at the body's 16px. */

    /* The shell of every static page (.info-body / .info-panel / the icon-led
       section headings / .info-logo) is shared with /Privacy-Policy and
       /Disclaimer and lives in elementa-glass.css, so the three pages cannot
       drift apart. Only the payment screenshot is this page's own. */

    .info-panel .upload-shot {
        width: 100%;
        margin: 10px 0;
        border: 1px solid var(--eg-border);
        border-radius: var(--radius);
    }

/* ── Member area: Login / Register / Forgot Password ─────────────────────────────
   The three member forms share one shell class (.auth-box) so the card width and the
   field radius are stated once instead of three times. The column they sit in is still
   the one the markup asks for; the max-width is what keeps a two-field form from
   stretching across a wide screen. */
    .auth-box {
        max-width: 440px;
        margin-inline: auto;
    }

    /* The member cards hold the resting shadow under the pointer as well. They are form
       panels, not tiles in a grid: nothing on them opens or navigates, so the lift every
       other card family uses as a "you can click this" cue would read as a promise the
       card does not keep. Both states are stated, and BOTH are needed — the sheet's own
       `.box-a:hover` carries the same specificity and, being `!important` on both sides,
       only this later sheet can hold the shadow still. */
    .auth-box,
    .auth-box:hover {
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.74), var(--eg-shadow) !important;
        z-index: auto;
    }

    /* The same radius the Auction page's search field uses: the theme's button radius
       (--radius) rather than the 10px the global .form-control rule carries - a field in
       a small form reads better squared off, exactly as that search field does. */
    .auth-box .form-control,
    .auth-box .form-select {
        border-radius: var(--radius);
    }

    /* One size inside the card: the body's own 16px, for every element including the icon
       glyphs - the icon font renders at 24px by default, and that was the only text on these
       pages that was not the primary size.

       Only the SIZE is set here. An earlier version also forced `font-family: inherit`, which
       stripped Material Symbols off the icon spans and made them print their ligature names
       ("app_registration", "lock_reset") as words. The sheet already sets the primary face
       everywhere, so the family needs no help. */
    .auth-box,
    .auth-box * {
        font-size: 1rem;
    }

    /* The card sits in the middle of the screen: the band between the fixed header (72px) and
       the footer (45px) is filled, so the card is centred in what the visitor can actually see
       while the footer stays on screen. The 28px is .main's own bottom padding, which sits
       between this band and the footer - without it the footer lands just below the fold.
       min-height rather than height, so a long form (Register, or a validation message) makes
       the page taller instead of being clipped. */
    .auth-stage {
        display: flex;
        align-items: center;
        justify-content: center;
        min-height: calc(100vh - 72px - 45px - 28px);
    }

/* ══ Cards hold their shadow ══════════════════════════════════════════════════════════
   A card that is read is not a card that can be clicked. `elementa-glass.css` gives
   `.box-a:hover` and `.glass-card:hover` the raised shadow, and it was being handed out by
   every panel in the app whether or not the panel could be clicked - the member pages' cards,
   a dialog's own content, the info pages' panels, the KPI tiles, the announcements and last
   bids panels. Asked for page by page across 2026-10-04, then globally; this is the one place
   it is said, and the per-surface locks written during those rounds are now redundant but
   were left where they document a decision.

   Both states are named, and both are needed: elementa-glass's hover rule is `!important` and
   carries the same specificity, so only a later sheet that names the state it is holding still
   can win. `z-index` rides along because the raised card also lifts itself above its
   neighbours in a grid, and a card that is not raised must not move there either. */
.box-a,
.box-a:hover,
.glass-card,
.glass-card:hover {
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.74), var(--eg-shadow) !important;
    z-index: auto;
}

/* The exception, and the reason the rule above has to name the hover state at all: the one
   card that IS a link keeps the cue that says so. The auction results grid's tile is a whole
   <a>, so it lifts exactly as it did before - `a.item-x` and not `.item-x`, because the same
   wrapper on the pigeon grid is a div with a link inside it, and that card does not navigate
   as a whole. */
a.item-x .glass-card:hover,
a.item-x .auction-kpi-tile:hover {
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.78), var(--eg-shadow-hover) !important;
    z-index: 2;
}

/* ══ One corner for every box ══════════════════════════════════════════════════════════
   `elementa-glass.css` gives `.card`, `.box-a` and `.glass-card` one 14px corner, and the
   legacy `.box-shadow-4` / `.box-shadow-10` sections a second one. The member cards were the
   exception: Profile asked for the theme's --radius (4px) - the corner the rows and the fields
   inside it use - because on that page a card is a list of labelled rows rather than a tile.
   Asked for on Profile first, then for every box of every page, so it is stated once here, at
   the last sheet, where it wins over the theme's 14px and over the legacy sections' corner.

   Scoped to `.box-a`, which is what a page's content box is: the member pages' cards, the
   info pages' panels, the three member forms, the lot card, a dialog's own content. The TILE
   families keep their own corner - `.card` / `.glass-card` are the auction grid's and the home
   page's tiles, not boxes - and the pigeon grid's tile keeps the 8px Bootstrap's own
   `rounded-3` asks it for (that utility is `!important`, so it wins here regardless). */
.box-a {
    border-radius: var(--radius);
}

/* ── Message.cshtml ───────────────────────────────────────────────────────────── */
/* The member's mail reader: the list is a rail on the left, the opened message takes the rest.
   Two panes in ONE box rather than two boxes side by side, because a list of e-mails and one
   e-mail are read together - and because the member area's content width (776px) is what the
   other three member pages already use, so this page is drawn on the same axis as they are.

   The rail is fixed and the message is `minmax(0, 1fr)`: `1fr` alone would let a wide element
   inside the message (these bodies are e-mail tables) push the rail narrower. */

/* ── One screen, and the scrolling is inside the box ─────────────────────────────────────
   Message.cshtml is the only page that is a WINDOW rather than a document. The shell
   (`.body` in the section above) is a flex column at least one screen tall and it lets a long
   page grow and scroll - which is right everywhere else and wrong here: a mail reader whose
   whole box slides up when the page is scrolled is a page with a list in it, not a reader.

   So on this page only, the column is exactly one screen (`height`, not `min-height`) and the
   space left over after the header and the footer is handed down the chain - page, container,
   row, column, box, pane - until the box fills it. `min-height: 0` at every link is what lets
   those items actually shrink to the space available; without it a flex item refuses to go
   below its content and the page scrolls again, which is the bug this section exists to fix.

   Every rule here is keyed off `message-fill`, a class no other page has, and the phone
   breakpoint at the bottom of this section puts the page back to normal scrolling. */
.body:has(.message-fill) {
    height: 100vh;
    overflow: hidden;
}

/* The header and the footer keep their own height (`flex: 0 0 auto` is already on the footer);
   it is .main that gives up the room - including the 28px of padding the shell gives a page whose
   content ends in loose text. Below this box that padding is not air, it is a gap: the box is
   meant to reach the footer. */
.body:has(.message-fill) > .main {
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
    padding-bottom: 0;
}

.message-fill {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
    /* py-5's 3rem of ground is left to the other pages: the reader spends its pixels on the
      reader. The bottom is smaller than the top on purpose - 24 + the shell's own 28 was 52px of
      dead space under a box that is supposed to reach the footer, which reads as a gap rather
      than as air. */
    padding-top: 24px;
    padding-bottom: 16px;
}

.message-fill > .container {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

.message-fill > .container > .row {
    flex: 1 1 auto;
    min-height: 0;
    /* Column, not row. A flex item in a ROW is sized on the cross axis by its LINE, and the line
       is as tall as the tallest item in it - here the whole message list - so the column below
       stayed 4179px tall inside a 627px row and the list was clipped instead of scrolled. Read as
       a column, the same class pair is simply one item in a fixed-height box and `min-height: 0`
       does its job. `align-items: center` is what `justify-content-center` was doing for the
       column's width, which the row no longer owns. */
    flex-direction: column;
    align-items: center;
}

.message-fill > .container > .row > [class*="col-"] {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

.message-fill .box-a {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

.message-page .message-pane {
    display: grid;
    grid-template-columns: 250px minmax(0, 1fr);
    gap: 16px;
    /* The box decides the height here, not a formula: it is already exactly the space between
      the header and the footer. */
    flex: 1 1 auto;
    min-height: 0;
}

/* Scrolled rather than paginated: this is a reader, and the newest rows are the ones that are
   read. The scrollbar is the site's own (see elementa-glass.css).

   The rail is a BOX of its own now - the same white card with the same edge as the message pane
   beside it - and the rows inside it are separated by lines. Before that the rows were loose text
   on the panel, which read as one paragraph of dates rather than as a list of things to open. */
.message-page .message-list {
    display: flex;
    flex-direction: column;
    height: 100%;
    overflow-y: auto;
    border: 1px solid var(--eg-border);
    border-radius: var(--radius);
    background: #fff;
}

/* The loading and "no messages" lines are the only children that are not rows, so they carry the
   padding the rows carry themselves. */
.message-page .message-list > .text-muted {
    padding: 12px;
}

/* A row is a button, so it can be reached and opened from the keyboard - a mail list that only
   responds to a mouse is a mail list half the members cannot read. The line under it is what
   separates one message from the next; the last row keeps none, because there is nothing after
   it to separate from. */
.message-page .message-item {
    display: flex;
    flex-direction: column;
    gap: 3px;
    width: 100%;
    padding: 9px 12px;
    text-align: left;
    border: 0;
    border-bottom: 1px solid var(--eg-line);
    border-radius: 0;
    background: transparent;
    transition: background 150ms;
}

.message-page .message-item:hover {
    background: rgba(86, 127, 189, 0.06);
}

.message-page .message-item:last-child {
    border-bottom: 0;
}

/* The opened one: the theme's blue as a tint across the whole row, and the accent bar down its
   left edge. It used to be a white card with a shadow, which worked while the rail had no box of
   its own - inside a bordered list of divided rows a floating card reads as a mistake, while a
   tinted band with the subject in semibold is what every list does. */
.message-page .message-item.is-active {
    background: rgba(86, 127, 189, 0.12);
    box-shadow: inset 3px 0 0 var(--eg-primary-b);
}

.message-page .message-item.is-active .message-item-subject {
    font-weight: 600;
}

.message-page .message-item-line {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}

.message-page .message-item-date {
    font-size: 11.5px;
    color: var(--eg-subtle);
    font-variant-numeric: tabular-nums;
}

/* Sent or not sent, as a light and not as a word. The words are the same on every row and a
   member reading this list is scanning dates and titles, so a column of "Sent / Not sent" is a
   column of noise; the light says the same thing in 9px.

   It is wrapped so that it can be pointed at - a bare 9px dot is a target you miss - and the
   wrapper is 20px, the height of the badge it replaced, so the rows keep the height they had. */
.message-page .message-item-status {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 20px;
    height: 20px;
}

.message-page .message-item-dot {
    display: block;
    width: 9px;
    height: 9px;
    border-radius: 50%;
}

/* The theme's success green, and a slate grey for "not sent" - not the danger red: a message the
   member's own setting held back is not a failure, it is the answer to "why was I not told?" and
   it is kept here so they can still read it. The ring is what makes the light read as a light. */
.message-page .message-item-dot.is-sent {
    background: #198754;
    box-shadow: 0 0 0 2.5px rgba(25, 135, 84, 0.16);
}

.message-page .message-item-dot.is-unsent {
    background: #94a3b8;
    box-shadow: 0 0 0 2.5px rgba(148, 163, 184, 0.22);
}

/* What the light means. Two sentences long, so it is a popover and not a title attribute - and
   message.js appends it to the BODY and not to the rail: the rail scrolls, and a box wider than
   the rail is clipped by it (or gives the rail a horizontal scrollbar).

   `pointer-events: none` because there is nothing in it to point at: it is text, and the pointer
   leaving the dot must be the only thing that dismisses it. Unscoped, like the detail rules
   below, and for the same reason - it is not rendered inside `.message-page`. */
.message-tip {
    position: fixed;
    z-index: 1080;
    max-width: 300px;
    padding: 10px 12px;
    border: 1px solid var(--eg-border);
    border-radius: var(--radius);
    background: #fff;
    box-shadow: 0 10px 30px rgba(15, 23, 42, 0.14);
    font-size: 12.5px;
    line-height: 1.45;
    pointer-events: none;
}

.message-tip-title {
    display: block;
    font-weight: 600;
    color: var(--eg-ink);
}

.message-tip-text {
    color: var(--eg-subtle);
}

/* Subjects here are the app's own long ones ("You are the leading bidder - 7,500 THB - #1147
   LOFT ALK..."), so two lines with the overflow cut, rather than one line and an ellipsis that
   hides the lot number at the end. */
.message-page .message-item-subject {
    font-size: 13.5px;
    line-height: 1.35;
    color: var(--eg-ink);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* The page's one action, at the right end of the heading line - the place the member pages put
   theirs. It searches the subject AND the body, which is why the list is asked for rather than
   written into the page: a list already drawn cannot be asked for anything. */
.message-page .message-tools {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
}

.message-page .message-count {
    white-space: nowrap;
}

/* The same radius the Auction page's search field uses: the theme's button radius (--radius),
   which is what the sign-in forms take too, rather than the 10px the shared .form-control rule
   gives an input - a field in a small box reads better squared off, exactly as that search
   field does (see `.auc-search .auc-search-input` and `.auth-box .form-control`). */
.message-page .message-search {
    width: 210px;
    max-width: 100%;
    border-radius: var(--radius);
}

/* A row that is in the list because of its BODY and not its title wears this tag. The member is
   looking at a list of subjects, so without it the row looks like a mistake. */
.message-page .message-item-where {
    margin-left: 6px;
    padding: 0 5px;
    border: 1px solid var(--eg-line);
    border-radius: 9999px;
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--eg-subtle);
}

/* The rail's own scrollbar, in Elementa's terms. Its tokens are the SIZE OF THE VISIBLE BAR -
   --scrollbar-size (4px) at rest, --scrollbar-size-active (8px) while it is being used.

   The gutter of the scroller is the ACTIVE size and never changes; the resting pill is drawn
   inside it by insetting the thumb with a transparent half-difference border, which is the same
   trick the sheet's global rule uses (a 6px gutter insets by 1px to show 4px). Widening the
   gutter instead - the obvious reading of the two tokens - moves every row 4px sideways the
   moment the pointer touches the list, and a list that shifts while it is being read is worse
   than a bar that is 4px wider than it needs to be. */
.message-page .message-list::-webkit-scrollbar {
    width: var(--scrollbar-size-active);
    height: var(--scrollbar-size-active);
}

.message-page .message-list::-webkit-scrollbar-track,
.message-page .message-list::-webkit-scrollbar-corner {
    background: transparent;
}

.message-page .message-list::-webkit-scrollbar-button {
    display: none;
    width: 0;
    height: 0;
}

.message-page .message-list::-webkit-scrollbar-thumb {
    /* (8px - 4px) / 2: at rest the pill is 4px and centred in its lane. */
    border: calc((var(--scrollbar-size-active) - var(--scrollbar-size)) / 2) solid transparent;
    border-radius: 9999px;
    background: var(--scrollbar-thumb);
    background-clip: padding-box;
    transition: border-width 150ms, background 200ms;
}

/* In use: the inset goes and the pill fills the lane it already had, which changes nothing but
   the bar. The whole rail answers to the pointer, because a 4px target is not one to hit. */
.message-page .message-list:hover::-webkit-scrollbar-thumb,
.message-page .message-list::-webkit-scrollbar-thumb:hover {
    border-width: 0;
    background: var(--scrollbar-thumb-active);
}

/* Firefox has no ::-webkit-scrollbar: the same thin bar and the same two colours. It sits behind
   the same guard the sheet uses, and for the same reason - in Chromium `scrollbar-width` and
   `scrollbar-color` WIN over ::-webkit-scrollbar, so an unguarded pair here would silently throw
   away the 4px/8px bar above and put the browser's own ~10px thin bar in its place. */
@supports not selector(::-webkit-scrollbar) {
    .message-page .message-list {
        scrollbar-width: thin;
        scrollbar-color: var(--scrollbar-thumb) transparent;
    }
}

/* The message side of the same fixed pane: the head stays with the message, and anything that
   does not fit (a long note, a long address) scrolls here rather than pushing the box taller. */
.message-page .message-detail {
    display: flex;
    flex-direction: column;
    gap: 10px;
    height: 100%;
    min-height: 0;
    overflow-y: auto;
    padding: 14px 16px;
    border: 1px solid var(--eg-border);
    border-radius: var(--radius);
    background: #fff;
}

/* Everything the OPENED message is made of, from here down, is written unscoped - and that is
   on purpose. These class names exist on this page and nowhere else, and the same markup is
   rendered into two different hosts: the pane beside the list on a desktop, and the dialog that
   opens over the list on a phone (the `.message-modal-body` rule below). A rule scoped to
   `.message-page` would style one of them and silently skip the other. */
.message-detail-empty {
    font-size: 13.5px;
    color: var(--eg-subtle);
}

.message-detail-subject {
    font-size: 15px;
    font-weight: 600;
    line-height: 1.4;
    color: var(--eg-ink);
}

.message-detail-meta {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin-top: 6px;
    padding-bottom: 10px;
    border-bottom: 1px solid var(--eg-line);
    font-size: 12.5px;
    color: var(--eg-subtle);
}

.message-detail-row {
    display: flex;
    gap: 8px;
}

/* A status badge carries Bootstrap's `line-height: 1`, and as a flex item in the meta row it was
   STRETCHED to the row's height - so the words sat against the top of it with 4px of empty badge
   underneath (measured: 0.28px above, 4.47px below). A badge is not a stretchy thing and its text
   is not at the top of it: centred in the row, and centred in itself.

   It also holds 9.4px text (it inherits the meta's 12.5px and Bootstrap takes 0.75em) inside a
   0.35em box: a 15.9px pill for a 14px font box, which leaves no slack for the face's own
   ascent/descent asymmetry to cancel out, so the words still read high (1.22px above the text,
   2.59px below). The 0.45em below brings the pill to 17.8px and the text back within 1.4px of
   centre.

   The LIST has no badge any more: it says sent / not sent with a light (`.message-item-dot`
   above), because those are the same two words on every row. */
.message-detail-row > .badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    align-self: center;
    line-height: 1;
    padding: 0.45em 0.7em;
}

.message-detail-label {
    flex: none;
    width: 48px;
}

.message-detail-value {
    color: var(--eg-ink);
    overflow-wrap: anywhere;
}

/* "This message was not sent", said to the member who is reading it: the ledger shows the
   messages that did not go out on purpose, and this is where that is explained. */
.message-detail-note {
    margin-top: 8px;
    padding: 8px 10px;
    border: 1px solid rgba(138, 90, 18, 0.28);
    border-radius: var(--radius);
    background: rgba(138, 90, 18, 0.06);
    color: #6d4a12;
    font-size: 12.5px;
    line-height: 1.5;
}

.message-detail-link {
    margin-top: 8px;
    font-size: 12.5px;
}

/* The body is a stored HTML document, so it is rendered in its own frame instead of being
   injected into this page: the templates in wwwroot/Templates carry their own styles, and a
   mail's <style> block must not meet the site's. `sandbox` with no allowances makes it inert.

   It takes the space the head leaves, and scrolls its own contents - the mail is the thing that
   grows, so the mail is the thing that scrolls. `min-height: 0` is what lets it shrink into a
   short pane instead of forcing the box back to a fixed 400px. */
.message-body {
    flex: 1 1 auto;
    width: 100%;
    min-height: 200px;
    border: 0;
    border-radius: var(--radius);
    background: #fff;
}

/* The dialog's half of the same rendering, on a phone. The dialog itself is the member area's
   own shape - `.box-a` content, a close button, `modal-dialog-scrollable` so a long head
   scrolls - and this is only the inside of it: the same padding and column the pane has. */
.message-modal-body {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 14px 16px;
}

/* A phone gets the SAME fixed box, one pane wide. It was briefly given back its page: the box
   went to `height: auto` and the page scrolled - which on a phone meant a 2,200px panel sliding
   under a 65px header, and the list the member was reading moving whenever the page did. The
   pane below is hidden there, so there is only one column to fill and nothing needs to be
   stacked: the shell stays exactly as it is on a desktop and the list scrolls inside it. */
@media (max-width: 767.98px) {
    .message-fill .message-pane {
        grid-template-columns: 1fr;
    }

    /* The count and the search box cannot share a phone's heading line: the search is the one
       that is used, so the count goes. */
    .message-page .message-count {
        display: none;
    }

    /* No second pane here: a message opens over the list, in the dialog below the page, and a
       pane that is display:none would hold the rendering invisibly. Unscoped on purpose (see
       the note above the message-detail rules). */
    .message-page .message-detail {
        display: none;
    }

    .message-body {
        flex: none;
        height: 56vh;
        min-height: 300px;
    }
}

