        :root {
            /* Native controls, scrollbars and the date picker follow the page
               instead of rendering light-on-dark (Android Chrome, iOS
               standalone, a Windows light theme). */
            color-scheme: light;
            /* The gallery: warm white, one serif, almost no chrome. The card
               is the art and the app is the placard beside it. */
            --bg: #f8f7f4;
            --panel: #ffffff;
            --panel-2: #f1efea;
            --line: #dcd9d1;
            --line-strong: #bdb9b0;
            --ink: #1c1c1c;
            --ink-dim: #6b6b66;
            --ink-faint: #8a8a86;
            /* Orange is the only colour in the building. Two grades of it: the
               text-safe one (5:1 on white) for words and rules, and the
               fluorescent dealer-sticker dot for the things a dealer would
               put a dot on — a price, a status, the one button. */
            --accent: #c2410c;
            --accent-dim: #f0b797;
            --dot: #ff5c00;
            /* Status trio chosen by measurement, not eye: the old green /
               orange / red sat at one lightness and were dE 9 apart under a
               deuteranopia simulation — the same colour. These three stay
               >= 20 apart from each other AND from the dim label grey under
               normal, protan, deutan and tritan vision (Machado 2009), and the
               warn yellow is deliberately lighter than the gold accent so a
               warning does not read as a highlight. Every status still prints
               a word beside the colour; see docs/UI-REVIEW-2026-08-22.md. */
            --good: #1f7a3e;
            --warn: #8f6400;
            --bad: #b3261e;
            /* The dot grades of the trio: the price-dot stickers every card
               dealer already uses. Dots never appear without the word. */
            --good-dot: #c6ff00;
            --warn-dot: #ffe600;
            --bad-dot: #ff2f92;
            --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
            --sans: "Instrument Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
            /* Chrome — labels, tab names, table heads — is the sans in small
               caps now, not the monospace; mono goes back to being data. */
            --chrome: var(--sans);
            /* The title tier. Login.html set the voice (Fraunces); the app now
               speaks it too — titles in serif, chrome stays mono. */
            --serif: "Instrument Serif", Georgia, "Times New Roman", serif;
            /* The reading size and tracking for chrome — section headings,
               table heads, field labels, tab names. 10px uppercase monospace
               was the single biggest legibility complaint; one knob now. */
            --label: 11.5px;
            --track: .12em;
        }

        * {
            box-sizing: border-box;
        }

        body {
            margin: 0;
            background: var(--bg);
            color: var(--ink);
            font: 15px/1.5 var(--sans);
        }

        .wrap {
            max-width: 1100px;
            margin: 0 auto;
            /* Top inset: under viewport-fit=cover the installed app paints
               behind the status bar, so the header steps below it. */
            padding: calc(28px + env(safe-area-inset-top, 0px)) 20px 60px;
        }

        header {
            display: flex;
            align-items: baseline;
            justify-content: space-between;
            gap: 12px;
            flex-wrap: wrap;
            margin-bottom: 6px;
        }

        h1 {
            font-family: var(--serif);
            font-size: 26px;
            font-weight: 400;
            margin: 0;
            letter-spacing: .01em;
        }

            h1 .tag {
                color: var(--ink);
            }

            /* The mark: the dealer's dot, as the wordmark's full stop. */
            h1 .mark {
                display: inline-block;
                width: 9px;
                height: 9px;
                margin-left: 3px;
                border-radius: 50%;
                background: var(--dot);
                vertical-align: baseline;
            }

        .sub {
            font-family: var(--chrome);
            font-size: var(--label);
            text-transform: uppercase;
            letter-spacing: var(--track);
            color: var(--ink-faint);
        }

            /* Daily AI allowance. Quiet until it starts to matter. */
            .sub .quota-low {
                color: var(--warn);
            }

            .sub .quota-out {
                color: var(--bad);
            }

        .build {
            text-align: center;
            font-family: var(--mono);
            font-size: 10px;
            letter-spacing: .12em;
            color: var(--ink-dim);
            opacity: .55;
            padding: 0 20px 26px;
        }

        .banner {
            display: none;
            margin: 14px 0 0;
            padding: 10px 14px;
            border: 1px solid var(--line);
            border-left: 3px solid var(--ink);
            background: var(--panel);
            border-radius: 2px;
            font-size: 14px;
        }

            .banner.show {
                display: block;
            }

        .grid {
            display: grid;
            grid-template-columns: 340px 1fr;
            gap: 18px;
            margin-top: 18px;
        }

        /* The scan bench stands on its own until there is a result to sit
           beside it. Two columns with nothing in the second is the same empty
           column the old layout had, just moved - and the bench is the only
           thing on this screen until you photograph something.

           :has() rather than a class toggled from JS, because three different
           places show or hide #result and a fourth would eventually forget. */
        #scanGrid:not(:has(#result.show)) {
            grid-template-columns: minmax(0, 460px);
            justify-content: center;
        }

        /* ------------------------------------------------------------------
           Views.

           Everything used to live in one column in the order features were
           added, so the page was a list of everything the app can do rather
           than what you are doing right now. Each view is one job; the nav
           switches between them and the URL hash records which, so the back
           button and a bookmark both work.

           Only the scan bench keeps the two-column grid - a camera on the left
           and its result on the right is the one place that pairing earns its
           keep. Every other view is full width, which is what reclaims the
           340px of empty air the old layout left below the bench.
           ------------------------------------------------------------------ */
        .view {
            display: none;
        }

            .view.on {
                display: block;
            }

        .views {
            display: flex;
            gap: 4px;
            margin-top: 16px;
            border-bottom: 1px solid var(--line);
        }

            .views button {
                appearance: none;
                background: none;
                border: none;
                border-bottom: 2px solid transparent;
                margin-bottom: -1px;
                padding: 9px 14px;
                font-family: var(--chrome);
                font-size: 12px;
                font-weight: 600;
                text-transform: uppercase;
                letter-spacing: var(--track);
                color: var(--ink-dim);
                cursor: pointer;
            }

                .views button:hover {
                    color: var(--ink);
                }

                .views button.on {
                    color: var(--ink);
                    border-bottom-color: var(--ink);
                }

                /* A count that wants attention - friend requests waiting on you. */
                .views button .pip {
                    display: inline-block;
                    margin-left: 6px;
                    padding: 1px 5px;
                    border-radius: 8px;
                    background: var(--dot);
                    color: var(--ink);
                    font-size: 10px;
                    letter-spacing: .04em;
                }

        /* "More" is the fifth slot in the phone bar and nothing on a desktop,
           where every view fits in the strip. The sheet it opens is built by
           buildMoreSheet(); both are hidden until the narrow layout below. */
        .views button.more {
            display: none;
        }

        .moresheet {
            display: none;
            position: fixed;
            inset: 0;
            z-index: 29;            /* over the page, under the bar (30) */
            background: rgba(28, 28, 28, .38);
        }

            .moresheet.on {
                display: block;
            }

            .moresheet .sheet {
                position: absolute;
                left: 0;
                right: 0;
                bottom: 0;
                /* Runs under the bar; the bottom padding is the bar's height
                   plus the home-indicator inset, so the last item clears it. */
                padding: 8px 10px calc(62px + env(safe-area-inset-bottom, 0px));
                background: var(--panel);
                border-top: 1px solid var(--line);
                border-radius: 14px 14px 0 0;
            }

                .moresheet .sheet button {
                    display: flex;
                    align-items: center;
                    justify-content: space-between;
                    width: 100%;
                    appearance: none;
                    background: none;
                    border: none;
                    border-bottom: 1px solid var(--line);
                    padding: 14px 8px;
                    font: 500 15px var(--sans);
                    color: var(--ink);
                    text-align: left;
                    cursor: pointer;
                }

                    .moresheet .sheet button:last-child {
                        border-bottom: none;
                    }

                    .moresheet .sheet button.on {
                        color: var(--ink);
                        font-weight: 600;
                    }

                    .moresheet .sheet button .pip {
                        padding: 1px 7px;
                        border-radius: 9px;
                        background: var(--dot);
                        color: var(--ink);
                        font-family: var(--mono);
                        font-size: 11px;
                    }

        /* ------------------------------------------------------------------
           Toasts. The page's own voice for what toast() used to say: a short
           notice that names the outcome and gets out of the way. Errors keep
           the red edge and stay longer; anything can be dismissed by tapping.
           Fixed above the phone bar, centred on a desktop.
           ------------------------------------------------------------------ */
        .toasts {
            position: fixed;
            left: 50%;
            bottom: 20px;
            transform: translateX(-50%);
            z-index: 60;
            display: flex;
            flex-direction: column;
            gap: 8px;
            width: min(520px, calc(100vw - 32px));
            pointer-events: none;
        }

            .toast {
                pointer-events: auto;
                position: relative;
                background: var(--panel);
                border: 1px solid var(--line);
                border-radius: 2px;
                padding: 10px 14px 10px 32px;
                font-size: 13.5px;
                line-height: 1.45;
                color: var(--ink);
                box-shadow: 0 8px 28px rgba(0, 0, 0, .14);
                cursor: pointer;
                white-space: pre-wrap;
                word-break: break-word;
            }

                /* The sticker, not a coloured edge: a dot in the margin says
                   what kind of notice this is, the words say the rest. */
                .toast::before {
                    content: "";
                    position: absolute;
                    left: 12px;
                    top: 14px;
                    width: 11px;
                    height: 11px;
                    border-radius: 50%;
                    background: var(--dot);
                    box-shadow: inset 0 -1px 2px rgba(0, 0, 0, .15);
                }

                .toast.bad::before {
                    background: var(--bad-dot);
                }

                .toast.warn::before {
                    background: var(--warn-dot);
                }

                .toast.good::before {
                    background: var(--good-dot);
                }

        /* ------------------------------------------------------------------
           Home.

           What opening the app means now: the collection's number, what is
           waiting on you, the sets you are closest to finishing, what just
           landed, what is coming up. Nothing here is new data — every figure
           arrives through the refreshers the other views already run — it is
           the one screen where they are read together.
           ------------------------------------------------------------------ */
        .homehero {
            margin-top: 18px;
            padding: 22px 20px 20px;
            background: var(--panel);
            border-top: 2px solid var(--ink);
        }

            .homehero .eyebrow {
                font-family: var(--chrome);
                font-size: var(--label);
                text-transform: uppercase;
                letter-spacing: var(--track);
                color: var(--ink-faint);
            }

        /* The one number a collector wants first deserves the display face,
           not the dashboard's. Serif numerals, like a ledger total. */
        .homeval {
            margin-top: 6px;
            font-family: var(--serif);
            font-size: 48px;
            font-weight: 400;
            line-height: 1.1;
            letter-spacing: 0;
        }

        .homesub {
            margin-top: 6px;
            font-size: 14px;
            color: var(--ink-dim);
        }

        /* The value and its last ninety days, side by side. */
        .homeval-row {
            display: flex;
            align-items: center;
            gap: 18px;
            flex-wrap: wrap;
        }

        svg.spark {
            width: 180px;
            height: 40px;
            overflow: visible;
        }

            svg.spark path.area {
                fill: var(--dot);
                opacity: .12;
            }

            svg.spark polyline {
                fill: none;
                stroke: var(--dot);
                stroke-width: 2;
                stroke-linejoin: round;
                stroke-linecap: round;
                vector-effect: non-scaling-stroke;
            }

        /* Up, down, or flat — a glyph and a sign as well as a colour. */
        .homedelta {
            margin-top: 6px;
            font-size: 13px;
            color: var(--ink-dim);
        }

            .homedelta b {
                font-weight: 600;
                color: var(--ink);
            }

            .homedelta b.up {
                color: var(--good);
            }

            .homedelta b.down {
                color: var(--bad);
            }

        .homeacts {
            margin-top: 16px;
        }

            .homeacts a.btn-accent {
                background: var(--ink);
                color: var(--bg);
                border-color: var(--ink);
                font-weight: 600;
            }

        .homegrid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
            gap: 18px;
            margin-top: 18px;
        }

            /* Rows reuse .setrow (name, optional bar, value) so Home reads
               like the views it points at. */
            .homegrid .setrow:last-child {
                border-bottom: none;
            }

            .homegrid .setrow .val {
                font-size: 13.5px;
                font-variant-numeric: tabular-nums;
                white-space: nowrap;
                color: var(--ink);
            }

            .homegrid .quiet {
                margin: 2px 0 0;
                font-size: 13.5px;
                color: var(--ink-dim);
            }

            .homegrid p.more {
                margin: 8px 0 0;
                font-size: 13px;
            }

                .homegrid .quiet a, .homegrid p.more a {
                    color: var(--accent);
                    text-decoration: none;
                }

        /* ------------------------------------------------------------------
           The binder page.

           A checklist is a spreadsheet of a thing people keep in nine-pocket
           sheets, and the sheet is the form the hobby actually thinks in.
           Same rows, same filter, same sort as the table — only the shape
           changes. Nine to a page on purpose: the page break is what makes it
           a binder rather than one more grid, and it makes each page a
           screenshot that explains itself.
           ------------------------------------------------------------------ */
        .binder-page {
            display: grid;
            grid-template-columns: repeat(3, minmax(0, 1fr));
            gap: 10px;
            background: var(--panel);
            border: 1px solid var(--line);
            border-radius: 8px;
            padding: 12px;
            margin: 10px auto 0;
            /* Capped, and centred in whatever is left. Three 5:7 pockets across
               a full desktop column make a sheet half again as tall as the
               screen — the ninth pocket and the page controls end up below the
               fold, which forfeits the one thing nine-to-a-page buys: a whole
               page you can see, and screenshot, at once. A real sheet is
               portrait and sits in the middle of the table; so does this. */
            max-width: 540px;
        }

        .pocket {
            position: relative;
            aspect-ratio: 5 / 7;
            border: 1px solid var(--line);
            border-radius: 6px;
            overflow: hidden;
            background: var(--panel-2);
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            gap: 3px;
            padding: 6px;
            text-align: center;
            min-width: 0;
        }

            /* An empty sleeve at the end of the last page: the sheet keeps its
               nine pockets, the way the real one does. */
            .pocket.blank {
                border-style: dashed;
                opacity: .35;
                background: none;
            }

            .pocket.need {
                border-style: dashed;
            }

            .pocket.have {
                border-color: var(--line-strong);
                cursor: pointer;
            }

                .pocket.have:hover {
                    border-color: var(--ink);
                }

            .pocket img {
                position: absolute;
                inset: 0;
                width: 100%;
                height: 100%;
                object-fit: cover;
            }

            /* Sits under any photo, and IS the pocket when there is none. */
            .pocket .pnum {
                font-family: var(--chrome);
                font-size: var(--label);
                letter-spacing: var(--track);
                color: var(--ink-dim);
            }

            .pocket .pname {
                font-size: 12px;
                line-height: 1.25;
                color: var(--ink-dim);
                overflow: hidden;
                display: -webkit-box;
                -webkit-line-clamp: 3;
                -webkit-box-orient: vertical;
            }

            .pocket.have .pname {
                color: var(--ink);
            }

            /* On a photographed pocket the number rides on top of the image. */
            .pocket .chip {
                position: absolute;
                left: 5px;
                top: 5px;
                z-index: 1;
                /* "#12 Photo Variation SP" must stay a chip, not become a
                   paragraph laid over the card. */
                max-width: calc(100% - 34px);
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
                padding: 1px 5px;
                border-radius: 4px;
                background: rgba(11, 16, 23, .82);
                font-family: var(--mono);
                font-size: 10.5px;
                letter-spacing: .04em;
                color: var(--ink);
            }

            .pocket .dupe {
                position: absolute;
                right: 5px;
                top: 5px;
                z-index: 1;
                padding: 1px 5px;
                border-radius: 4px;
                background: rgba(11, 16, 23, .82);
                font-family: var(--mono);
                font-size: 10.5px;
                color: var(--accent);
            }

        /* While a checklist is open, the set list above it folds away — you
           arrived from a row you already found, and scrolling past the whole
           library to reach the binder was the reported pain. The breadcrumb
           in #checklistView is the way back. */
        #v-sets .panel.cl-open > .vaulthead,
        #v-sets .panel.cl-open > .row,
        #v-sets .panel.cl-open > #setList {
            display: none;
        }

        #v-sets .panel.cl-open > #checklistView {
            border-top: none;
            margin-top: 0;
            padding-top: 0;
        }

        .binder-nav {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 14px;
            margin-top: 10px;
        }

            .binder-nav .page {
                font-family: var(--chrome);
                font-size: var(--label);
                letter-spacing: var(--track);
                text-transform: uppercase;
                color: var(--ink-dim);
                white-space: nowrap;
            }

        /* ------------------------------------------------------------------
           The vault as a binder.

           The nine-pocket vinyl sheet is the shape this hobby has thought in
           since the eighties — the checklist's binder view already proved it
           here — so the vault's visual mode is the collection in that object:
           smoky welded pockets, punched holes, rings. A desktop shows the
           binder lying open, two sheets on the rings; a phone holds one
           sheet. Same rows, same sort, same selection as the list — only the
           shape changes.
           ------------------------------------------------------------------ */
        .vgrid {
            margin-top: 10px;
        }

        /* A horizontal touch drag on a binder is the page-turn swipe;
           vertical stays the page's own scroll, pinch stays the browser's
           zoom. Without this line the browser may claim a sloppy diagonal
           for scrolling and cancel the pointer mid-swipe. */
        .vgrid, #checklistBinder {
            touch-action: pan-y pinch-zoom;
        }

        /* A finger that rests before it slides is still a page turn. Without
           these, iOS hands that touch to its long-press recogniser (the image
           callout, the selection loupe on a pocket's text) and cancels it
           before the drag can lock — "put your finger down and nothing
           happens". Spreads only: the list views keep selectable text. */
        .bspread, .binder-spread {
            -webkit-user-select: none;
            user-select: none;
            -webkit-touch-callout: none;
        }

        .vtile img, .pocket img {
            -webkit-user-drag: none;
        }

        .bspread {
            display: flex;
            align-items: stretch;
            justify-content: center;
        }

        /* The checklist binder's spread: two sheets side by side at desktop,
           one on a phone — the vault binder's rule, so the two binders agree
           on what "a binder" shows. Each sheet splits the row evenly. */
        .binder-spread {
            display: flex;
            gap: 18px;
            align-items: flex-start;
        }

            .binder-spread .binder-page {
                flex: 1 1 0;
                min-width: 0;
            }

        /* The vinyl. The checklist's sheet is the same object, so it shares
           the material; layout (columns, cap, gap) stays each view's own. */
        .bsheet, .binder-page {
            position: relative;
            /* Clear vinyl over white paper: the same sheet, lit from a window
               instead of a desk lamp. */
            background: linear-gradient(172deg, #ffffff 0%, #faf9f6 46%, #f4f2ed 100%);
            border: 1px solid var(--line);
            border-top-color: #ffffff;
            border-radius: 10px;
            box-shadow: 0 12px 30px rgba(0, 0, 0, .10), 0 1px 2px rgba(0, 0, 0, .06);
        }

            /* Light across the plastic. Pointer-events off: it lies over the
               pockets, and clicks belong to them. */
            .bsheet::after, .binder-page::after {
                content: "";
                position: absolute;
                inset: 0;
                border-radius: 10px;
                background: linear-gradient(118deg, rgba(255, 255, 255, .7) 0%, rgba(255, 255, 255, .25) 34%, transparent 52%);
                pointer-events: none;
            }

            /* The punched holes, on whichever edge faces the rings. */
            .bsheet::before, .binder-page::before {
                content: "";
                position: absolute;
                top: 0;
                bottom: 0;
                width: 16px;
                background-image:
                    radial-gradient(circle 5px at 8px 12%, #dcd9d1 98%, transparent),
                    radial-gradient(circle 5px at 8px 50%, #dcd9d1 98%, transparent),
                    radial-gradient(circle 5px at 8px 88%, #dcd9d1 98%, transparent);
            }

        .bsheet {
            width: min(430px, 100%);
            display: grid;
            grid-template-columns: repeat(3, minmax(0, 1fr));
            gap: 9px;
            padding: 14px 16px;
        }

            .bsheet.lp::before {
                right: 0;
            }

            .bsheet.rp::before {
                left: 0;
            }

        /* A checklist page stands alone, holes to the left, like a sheet
           lifted out of the binder. */
        .binder-page::before {
            left: 0;
        }

        /* The rings. Stretched to the spread's height so the three line up
           with the holes; the negative margin threads them over both edges. */
        .brings {
            flex: 0 0 34px;
            position: relative;
            z-index: 2;
            margin: 0 -8px;
        }

            .brings i {
                position: absolute;
                left: 50%;
                width: 30px;
                height: 30px;
                margin-left: -15px;
                border: 3px solid #b9bec7;
                border-radius: 50%;
                box-shadow: inset 0 1px 1px rgba(255, 255, 255, .8), 0 2px 3px rgba(0, 0, 0, .25);
            }

                .brings i:nth-child(1) { top: calc(12% - 15px); }
                .brings i:nth-child(2) { top: calc(50% - 15px); }
                .brings i:nth-child(3) { top: calc(88% - 15px); }

        /* One pocket of the vault's sheet. The card sits INSIDE the sleeve —
           inset, behind the plastic — rather than filling a tile. */
        .vtile {
            position: relative;
            aspect-ratio: 5 / 7;
            background: rgba(28, 28, 28, .05);
            border: 1px solid rgba(28, 28, 28, .09);
            border-top-color: rgba(28, 28, 28, .16);  /* the opening catches light */
            border-radius: 7px;
            overflow: hidden;
            cursor: pointer;
        }

            /* The plastic in front of the card. */
            .vtile::after, .pocket::after {
                content: "";
                position: absolute;
                inset: 0;
                pointer-events: none;
                background: linear-gradient(160deg, rgba(255, 255, 255, .45) 0%, rgba(255, 255, 255, .12) 26%, rgba(255, 255, 255, 0) 46%, rgba(28, 28, 28, .06) 100%);
            }

            .vtile:hover {
                border-color: var(--line-strong);
            }

            .vtile.selected {
                outline: 2px solid var(--ink);
                outline-offset: -2px;
            }

            /* The card open in the editor — the pocket equivalent of tr.selected. */
            .vtile.editing {
                border-color: var(--ink);
                box-shadow: 0 0 0 1px var(--ink);
            }

            /* An empty sleeve past the end of the collection. NOT dashed —
               dashed is the checklist's word for "need", and the vault makes
               no such claim. */
            .vtile.sleeve {
                background: rgba(28, 28, 28, .03);
                box-shadow: inset 0 2px 9px rgba(0, 0, 0, .08);
                cursor: default;
            }

            /* The dealer's dot on a sleeve: only on the cards that need a
               look — a finish the scanner could not call, a card it doubts.
               A green dot on every ordinary card would be noise, so
               "linked and fine" is the absence of a dot. The word lives in
               the editor and the list; the dot is the glance. */
            .vtile .vdot {
                position: absolute;
                right: -5px;
                top: -5px;
                z-index: 3;
                width: 15px;
                height: 15px;
                border-radius: 50%;
                box-shadow: 0 1px 2px rgba(0, 0, 0, .28), inset 0 -1px 2px rgba(0, 0, 0, .12);
            }

                .vtile .vdot.warn {
                    background: var(--warn-dot);
                }

                .vtile .vdot.bad {
                    background: var(--bad-dot);
                }

            .vtile img {
                position: absolute;
                inset: 4px;
                width: calc(100% - 8px);
                height: calc(100% - 8px);
                object-fit: cover;
                border-radius: 4px;
            }

            /* No photo: the card is in the pocket face down. Cardboard, not
               chrome — the one warm thing on the sheet, like the real thing. */
            .vtile .bback {
                position: absolute;
                inset: 4px;
                border-radius: 4px;
                background: linear-gradient(173deg, #c7b48d 0%, #b8a37a 100%);
                box-shadow: inset 0 0 0 3px rgba(96, 76, 44, .28);
                display: flex;
                align-items: center;
                justify-content: center;
            }

                .vtile .bback span {
                    font-family: var(--mono);
                    font-size: 10.5px;
                    letter-spacing: .06em;
                    text-transform: uppercase;
                    color: #6d5b39;
                }

            /* Quiet until it matters: nothing on a real sheet sits in that
               corner, so the box fades up on hover, focus, or once checked. */
            .vtile .tsel {
                position: absolute;
                top: 7px;
                left: 7px;
                z-index: 2;
                width: 18px;
                height: 18px;
                margin: 0;
                accent-color: var(--ink);
                cursor: pointer;
                opacity: .45;
            }

                .vtile:hover .tsel, .vtile .tsel:checked, .vtile .tsel:focus {
                    opacity: 1;
                }

            /* What the eye can't read off a photo alone: RC / AUTO / grade,
               in the same chip the checklist pockets wear. */
            .vtile .bflag {
                position: absolute;
                right: 6px;
                top: 6px;
                z-index: 2;
                max-width: calc(100% - 34px);
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
                padding: 1px 5px;
                border-radius: 4px;
                background: rgba(11, 16, 23, .82);
                font-family: var(--mono);
                font-size: 10.5px;
                letter-spacing: .04em;
                color: var(--ink);
            }

            /* The price, as the little sticker a dealer would leave on the
               sleeve. Cream, square corners, sits ON the plastic. */
            .vtile .btag {
                position: absolute;
                right: 6px;
                bottom: 6px;
                z-index: 2;
                padding: 1px 6px 2px;
                border-radius: 2px;
                background: #f0e9d8;
                color: #37301e;
                font-family: var(--mono);
                font-size: 10.5px;
                font-variant-numeric: tabular-nums;
                box-shadow: 0 1px 2px rgba(0, 0, 0, .4);
            }

        /* Turning a page hinges the new sheets in at the rings; one page on
           a phone just settles. Skipped entirely under reduced motion. */
        @media (prefers-reduced-motion: no-preference) {
            .bspread.turn .bsheet.rp {
                transform-origin: left center;
                animation: bflipR .24s ease-out;
            }

            .bspread.turn .bsheet.lp {
                transform-origin: right center;
                animation: bflipL .24s ease-out;
            }

            /* The checklist binder's turn. Its sheets draw the punched holes
               on the LEFT of every page and there are no rings between them
               (see .binder-page::before), so every sheet hinges at its left
               edge — the desktop pair and a lone phone sheet alike. The
               vault's mirrored left-page rule was copied here once and
               swung the left sheet on its hole-less free edge. */
            .binder-spread.turn .binder-page {
                transform-origin: left center;
                animation: bflipR .24s ease-out;
            }
        }

        /* On a phone each binder holds ONE sheet with the punched holes on
           the left — both binders hinge there, and hinge the SAME way. The
           vault's lone sheet is .lp, whose desktop rule hinges at the right;
           this override keeps the twins from animating mirror-imaged. */
        @media (prefers-reduced-motion: no-preference) and (max-width: 760px) {
            .bspread.turn .bsheet.lp {
                transform-origin: left center;
                animation: bflipR .24s ease-out;
            }
        }

        @keyframes bflipR {
            from {
                transform: perspective(1200px) rotateY(-14deg);
                opacity: .35;
            }
        }

        @keyframes bflipL {
            from {
                transform: perspective(1200px) rotateY(9deg);
                opacity: .55;
            }
        }

        /* One sheet in the hand below 760px: the right page and the rings go,
           and the lone sheet's holes move to its left edge. */
        @media (max-width: 760px) {
            .bsheet.rp, .brings {
                display: none;
            }

            .bsheet.lp::before {
                right: auto;
                left: 0;
            }
        }

        /* The grid has no column heads, so the sort select that the phone
           layout shows is the sort control here too. */
        .vault-grid .onnarrow {
            display: inline;
        }

        /* A small photo on a Home row — in a sleeve, like everything else.
           The padding IS the pocket: vinyl showing around the card. */
        .setrow img.rthumb {
            flex: 0 0 auto;
            width: 38px;
            aspect-ratio: 5 / 7;
            object-fit: cover;
            padding: 2px;
            border-radius: 4px;
            border: 1px solid rgba(28, 28, 28, .12);
            border-top-color: rgba(28, 28, 28, .2);
            background: rgba(28, 28, 28, .05);
        }

        /* The search / filter / sort row stays put while the list scrolls
           under it — a long vault is the one place this page scrolls far. */
        #vaultTools {
            position: sticky;
            top: 0;
            z-index: 5;
            background: var(--panel);
            padding-top: 6px;
            padding-bottom: 6px;
        }

        #vaultSearch {
            flex: 1;
        }

        /* ------------------------------------------------------------------
           Desktop: the strip becomes a rail.

           At 1200px and up the tab strip moves to a left rail and the view
           takes the rest — one column of panels left two-thirds of a wide
           screen as air. Same buttons, same ids, same router: CSS on the
           markup that was already there. The header and banners span both
           columns; anything else in .wrap lands in the content column, so a
           dialog or overlay added later cannot fall into the rail by accident.
           ------------------------------------------------------------------ */
        @media (min-width: 1200px) {
            .wrap {
                max-width: 1340px;
                display: grid;
                grid-template-columns: 164px minmax(0, 1fr);
                column-gap: 28px;
                align-items: start;
            }

                .wrap > header, .wrap > .banner {
                    grid-column: 1 / -1;
                }

                .wrap > :not(header):not(.banner):not(.views) {
                    grid-column: 2;
                }

                .wrap > .views {
                    grid-column: 1;
                    position: sticky;
                    top: 20px;
                    flex-direction: column;
                    gap: 2px;
                    margin-top: 18px;
                    padding-right: 10px;
                    border-bottom: none;
                    border-right: 1px solid var(--line);
                }

                    .wrap > .views button {
                        text-align: left;
                        padding: 9px 10px;
                        margin-bottom: 0;
                        border-bottom: none;
                        border-left: 2px solid transparent;
                        border-radius: 0 6px 6px 0;
                    }

                        .wrap > .views button.on {
                            border-left-color: var(--ink);
                            background: var(--panel);
                        }

                .wrap > .view {
                    margin-top: 18px;
                }

                    /* These carried the gap the rail now provides. */
                    .wrap > .view > .grid, .wrap > .view > .homehero {
                        margin-top: 0;
                    }
        }

        /* ------------------------------------------------------------------
           The scan queue. Sits under the bench, shows only what is unfinished,
           and disappears entirely when there is nothing in flight — the vault
           is where finished cards live.
           ------------------------------------------------------------------ */
        .queue {
            margin-top: 14px;
            border-top: 1px solid var(--line);
            padding-top: 10px;
        }

            .queue h3 {
                margin: 0 0 6px;
                font-family: var(--chrome);
                font-size: var(--label);
                font-weight: 600;
                text-transform: uppercase;
                letter-spacing: var(--track);
                color: var(--ink-dim);
            }

        .qrow {
            display: flex;
            align-items: center;
            gap: 9px;
            padding: 5px 0;
            border-bottom: 1px solid var(--line);
            font-size: 13px;
        }

            .qrow img {
                width: 26px;
                height: 36px;
                object-fit: cover;
                border-radius: 3px;
                border: 1px solid var(--line);
                flex: 0 0 auto;
            }

            .qrow .qlabel {
                flex: 1;
                min-width: 0;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
                color: var(--ink-dim);
            }

            .qrow .qstate {
                font-family: var(--chrome);
                font-size: 11px;
                text-transform: uppercase;
                letter-spacing: .06em;
                color: var(--ink-dim);
                white-space: nowrap;
            }

                .qrow .qstate::before {
                    content: "";
                    display: inline-block;
                    width: 9px;
                    height: 9px;
                    margin-right: 6px;
                    border-radius: 50%;
                    background: var(--line-strong);
                    vertical-align: -1px;
                }

                .qrow.working .qstate::before {
                    background: var(--dot);
                }

                .qrow.done .qstate::before {
                    background: var(--good-dot);
                }

                .qrow.failed .qstate::before {
                    background: var(--bad-dot);
                }

            .qrow .qactions {
                font-family: var(--mono);
                font-size: 11px;
                white-space: nowrap;
            }

            .qrow.working .qstate {
                color: var(--accent);
            }

            .qrow.done .qlabel {
                color: var(--ink);
            }

            .qrow.done .qstate {
                color: var(--good);
            }

            .qrow.failed .qstate {
                color: var(--bad);
            }

        /* ------------------------------------------------------------------
           The photo viewer.

           Tapping a thumbnail used to open a new browser tab, which loses the
           page you were on and, on a phone, dumps you in a tab you then have to
           close. It shows the image over the card instead — and the new-tab
           behaviour stays, as a deliberate choice rather than the only one:
           the thumbnail is still a real <a href>, so cmd/ctrl/middle-click and
           long-press-open-in-new-tab all keep working untouched.
           ------------------------------------------------------------------ */
        .lightbox {
            display: none;
            position: fixed;
            inset: 0;
            z-index: 100;
            color: #f2f2f4;
            /* Opaque, not a 95% scrim. At 95% the editor's own labels ghosted
               through hard enough that "FRONT" landed on top of "EDIT CARD" and
               the nav bar was still legible underneath — a see-through overlay
               reads as a rendering fault, not as depth. */
            background: #0b1017;
            flex-direction: column;
            /* A dark viewing room inside a white gallery. The tokens are
               re-declared here so the chrome inside stays legible on black. */
            --ink: #f2f2f4;
            --ink-dim: #a0a3ad;
            --line: #26282e;
            --panel-2: #17181c;
            --accent: #ff8a4c;
            --accent-dim: #c4460a;
        }

            /* The photo eases in instead of snapping — the one-frame jump was
               most of the "clunky" in the report. A keyframe, not a
               transition: the box itself goes display:none → flex, and only
               an animation runs from nothing. On the STAGE, not the img: a
               running animation overrides inline transforms, and the img's
               transform now belongs to the zoom/pan gestures. */
            .lightbox.on .lb-stage {
                animation: lbin .18s ease-out;
            }

            /* When the photo FLIES from its thumbnail (0.65.0), the stage
               must hold still — the flight measures real rects, and the img
               carries the motion. The backdrop and bar fade in around it. */
            .lightbox.on.flying {
                animation: lbbg .22s ease-out;
            }

                .lightbox.on.flying .lb-stage {
                    animation: none;
                }

                .lightbox.on.flying .lb-bar {
                    animation: lbfade .22s ease-out;
                }

            .lb-stage img.flying {
                transition: transform .22s cubic-bezier(.2, .7, .3, 1);
            }

        @keyframes lbin {
            from { opacity: 0; transform: scale(.965); }
            to { opacity: 1; transform: scale(1); }
        }

        @keyframes lbbg {
            from { background: rgba(11, 16, 23, 0); }
            to { background: rgba(11, 16, 23, 1); }
        }

        @keyframes lbfade {
            from { opacity: 0; }
            to { opacity: 1; }
        }

        @media (prefers-reduced-motion: reduce) {
            .lightbox.on .lb-stage,
            .lightbox.on.flying,
            .lightbox.on.flying .lb-bar {
                animation: none;
            }
        }

            .lightbox.on {
                display: flex;
            }

        .lb-bar {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 12px;
            padding: 10px 14px;
            border-bottom: 1px solid var(--line);
            font-family: var(--chrome);
            font-size: 12px;
            text-transform: uppercase;
            letter-spacing: var(--track);
            color: var(--ink-dim);
            /* Notch and status bar on a phone. */
            padding-top: calc(10px + env(safe-area-inset-top, 0px));
        }

        .lb-actions {
            display: flex;
            align-items: center;
            gap: 6px;
        }

            .lb-actions button, .lb-actions a {
                appearance: none;
                background: var(--panel-2);
                border: 1px solid var(--line);
                border-radius: 0;
                color: var(--ink);
                font: inherit;
                text-transform: uppercase;
                text-decoration: none;
                /* 34px tall: a thumb target, not a mouse target. */
                padding: 8px 12px;
                cursor: pointer;
            }

                .lb-actions button:hover, .lb-actions a:hover {
                    border-color: var(--accent-dim);
                    color: var(--accent);
                }

        .lb-stage {
            flex: 1;
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: 0;          /* or the image can push the flex item past the viewport */
            padding: 12px;
            padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
            cursor: zoom-out;
            /* Our pinch, not the browser's page-zoom; our swipe, not a
               scroll. The page behind is frozen anyway (body overflow). */
            touch-action: none;
            overflow: hidden;       /* a zoomed image pans inside the stage, never over the bar */
        }

            .lb-stage img {
                max-width: 100%;
                max-height: 100%;
                object-fit: contain;
                border-radius: 6px;
                /* zoom-in, not default: the image is a GESTURE target now —
                   still never a close target, which stays the space around
                   it. Grab cursors say "this pans" once zoomed. */
                cursor: zoom-in;
                will-change: transform;
            }

                .lb-stage img.zoomed {
                    cursor: grab;
                }

                    .lb-stage img.zoomed:active {
                        cursor: grabbing;
                    }

        /* The thumbnails say they can be enlarged. */
        .cardphotos a {
            cursor: zoom-in;
        }

        /* What a view says when it has nothing to show. Roomy on purpose — the
           point is to fill space that would otherwise read as a dead end, so
           cramping it back down would defeat the exercise. */
        .emptystate {
            padding: 22px 2px 26px;
            max-width: 46em;
        }

            .emptystate p {
                margin: 0;
                font-size: 14px;
                line-height: 1.6;
                color: var(--ink-dim);
            }

            .emptystate a {
                display: inline-block;
                margin-top: 10px;
                font-family: var(--chrome);
                font-size: 12px;
                text-transform: uppercase;
                letter-spacing: var(--track);
            }

        /* Shown only when the vault folds to two lines. Declared BEFORE the
           media query on purpose: same specificity, so whichever rule comes
           last wins, and a base rule sitting after the query would silently
           beat it at every width. */
        .onnarrow {
            display: none;
        }

        @media (max-width: 760px) {
            .grid {
                grid-template-columns: 1fr;
            }

            /* The tab strip becomes a thumb-reachable bar pinned to the bottom.
               Scan and Vault are the two you actually open on a phone, so they
               lead; the rest stay one tap away instead of a scroll away. */
            .views {
                position: fixed;
                left: 0;
                right: 0;
                bottom: 0;
                z-index: 30;
                margin: 0;
                gap: 0;
                background: var(--panel);
                border-top: 1px solid var(--line);
                border-bottom: none;
                padding-bottom: env(safe-area-inset-bottom, 0);
            }

                /* Five slots — Home, Scan, Vault, Sets, More — at ~78px each in
                   390px, which is room for a label a thumb can read. The other
                   views are not gone: they are the More sheet, which is how every
                   phone platform handles a bar with more jobs than slots. Nowrap
                   keeps a long label a clipped label rather than a second line
                   that pushes the bar over the padding the page reserves. */
                .views button {
                    flex: 1;
                    min-width: 0;
                    white-space: nowrap;
                    overflow: hidden;
                    margin-bottom: 0;
                    padding: 12px 1px;
                    min-height: 46px;
                    font-size: 10.5px;
                    letter-spacing: .02em;
                    border-bottom: none;
                    border-top: 2px solid transparent;
                }

                    .views button.overflow {
                        display: none;
                    }

                    .views button.more {
                        display: block;
                    }

            /* Toasts sit above the bar, not under it. */
            .toasts {
                bottom: calc(58px + env(safe-area-inset-bottom, 0px));
            }

                    .views button.on {
                        border-top-color: var(--ink);
                    }

                    .views button .pip {
                        margin-left: 4px;
                        padding: 0 4px;
                    }

            /* The accounts table is four columns of mostly numbers, and on a
               390px screen "0 / 100000" wrapping is what pushes a row to four
               lines. Keeping the numbers on one line each and letting only the
               actions wrap holds every row to the height of the name beside it.
               Admin is a desk job; this is the version that stays READABLE on a
               phone rather than the version that pretends it's comfortable. */
            #adminUserRows td.num {
                white-space: nowrap;
                font-size: 11.5px;
            }

            /* Clear the fixed bar, or the last row of anything — and the build
               stamp, which sits outside .wrap — is under it. On the body, so
               both clear it; the bar is 46px plus the home-indicator inset. */
            body {
                padding-bottom: calc(66px + env(safe-area-inset-bottom, 0px));
            }

            .wrap {
                padding-bottom: 20px;
            }

            /* --------------------------------------------------------------
               The vault, folded.

               Five columns in 390px meant "Topps Series 1" and "Team 14" each
               wrapped, so every row was double height and you read a
               spreadsheet through a mail slot. Same cells, same click target,
               same selection checkbox — laid out as two lines instead:

                   [x]  1999 Player 14 · RC PSA 9        $518.00
                        Topps Series 1 · Team 14          listed

               Done in CSS rather than by rendering different markup, so there
               is one row builder to keep correct and rotating the phone can't
               desync anything.
               -------------------------------------------------------------- */
            #vaultRows tr {
                display: grid;
                grid-template-columns: 26px minmax(0, 1fr) auto;
                grid-template-areas:
                    "sel name value"
                    "sel meta status";
                align-items: baseline;
                column-gap: 8px;
                padding: 8px 0;
                border-bottom: 1px solid var(--line);
            }

                #vaultRows td {
                    border: none;
                    padding: 0;
                    white-space: normal;
                }

                    #vaultRows td.selcol {
                        grid-area: sel;
                        align-self: center;
                    }

                    #vaultRows td.c-name {
                        grid-area: name;
                        font-weight: 600;
                    }

                    #vaultRows td.c-set {
                        grid-area: meta;
                        color: var(--ink-dim);
                        font-size: 12.5px;
                    }

                    /* Both folded into .c-set above — there is no column for
                       them once the row is two lines. */
                    #vaultRows td.c-team,
                    #vaultRows td.c-added {
                        display: none;
                    }

                    #vaultRows td.num {
                        grid-area: value;
                        text-align: right;
                    }

                    #vaultRows td.c-status {
                        grid-area: status;
                        text-align: right;
                        color: var(--ink-dim);
                        font-size: 12.5px;
                    }

                    /* The empty-state row spans the lot rather than landing in
                       whichever grid cell it fell into. */
                    #vaultRows td.empty {
                        grid-area: name / name / status / status;
                        text-align: left;
                    }

            /* One thing at a time. With the editor open there is no room for the
               list as well, and leaving it below means scrolling past a form to
               reach rows you can't act on while a card is open. Close brings it
               straight back. */
            #v-vault.editing > .panel:not(#editorPanel) {
                display: none;
            }

            /* Column headings describe a table that isn't on screen any more.
               Sorting moves to the select beside the search box. */
            #vaultTable thead {
                display: none;
            }

            /* Search, status and sort don't fit on one 390px line — sharing it
               squeezed the search box down to "Sear". Search takes the row and
               the two selects share the next. */
            .row {
                flex-wrap: wrap;
            }

            #vaultSearch {
                flex: 1 0 100%;
            }

            #vaultStatus, #vaultSort {
                flex: 1;
                min-width: 0;
            }

            /* --------------------------------------------------------------
               The rip tables, folded — the vault treatment for the same
               reason: six and seven columns in 390px pushed "2026 Topps
               Flagship Football · 87%" clean off the screen edge. Same
               cells, same renderer, laid out as short lines instead:

                   C&S Hobbies · Mega Box            196 · 149m
                   8/29/2026 4:21 PM                    46s/card
                   2026 Topps Series 2 · 92%
                   183 of 196 finish unknown             delete
               -------------------------------------------------------------- */
            #v-rips thead {
                display: none;
            }

            #ripRecRows tr, #ripRows tr {
                display: grid;
                grid-template-columns: minmax(0, 1fr) auto;
                grid-template-areas:
                    "name  cards"
                    "sub   pace"
                    "val   val"
                    "most  most"
                    "unk   act";
                align-items: baseline;
                column-gap: 8px;
                padding: 8px 0;
                border-bottom: 1px solid var(--line);
            }

                #ripRecRows td, #ripRows td {
                    border: none;
                    padding: 0;
                }

                    #ripRecRows td.c-name, #ripRows td.c-name {
                        grid-area: name;
                        font-weight: 600;
                    }

                    #ripRecRows td.c-sub, #ripRows td.c-sub {
                        grid-area: sub;
                    }

                    /* The value line gets its own full-width row: it is the
                       number the fold must not bury, and it can run long once
                       at-rip and cost ride along. A sitting with no value at
                       all drops the line entirely here — a floating "—" with
                       no column heading above it explains nothing on a phone,
                       while the desktop table keeps it for column alignment. */
                    #ripRecRows td.c-val, #ripRows td.c-val {
                        grid-area: val;
                        text-align: left;
                    }

                        #ripRecRows td.c-val.novalue, #ripRows td.c-val.novalue {
                            display: none;
                        }

                    #ripRecRows td.c-cards, #ripRows td.c-cards {
                        grid-area: cards;
                    }

                    #ripRecRows td.c-pace, #ripRows td.c-pace {
                        grid-area: pace;
                        color: var(--ink-dim);
                        font-size: 12.5px;
                    }

                    #ripRecRows td.c-mostly, #ripRows td.c-mostly {
                        grid-area: most;
                        color: var(--ink-dim);
                        font-size: 12.5px;
                    }

                    /* The column heading that named this count is display:none
                       up there, so the count carries its own label down here. */
                    #ripRecRows td.c-unk, #ripRows td.c-unk {
                        grid-area: unk;
                        text-align: left;
                        font-size: 12.5px;
                    }

                        #ripRecRows td.c-unk::after, #ripRows td.c-unk::after {
                            content: " finish unknown";
                        }

                    #ripRecRows td.c-act, #ripRows td.c-act {
                        grid-area: act;
                        text-align: right;
                    }

                    /* On the fold the two buttons share one right-aligned
                       cluster — the gap below keeps a thumb aimed at the cost
                       from landing on delete. */

                    /* Empty-state and error rows span the whole fold. */
                    #ripRecRows td[colspan], #ripRows td[colspan] {
                        grid-column: 1 / -1;
                    }

            .onnarrow {
                display: inline;
            }
        }

        /* How many of this card you own. Silent at 1; loud enough to notice
           when it isn't, because that is the whole point of tracking it. */
        .qty {
            display: inline-block;
            margin-left: 7px;
            padding: 0 5px;
            border: 1px solid var(--line-strong);
            border-radius: 4px;
            font-family: var(--mono);
            font-size: 11px;
            color: var(--ink);
            vertical-align: 1px;
        }

        .panel {
            background: var(--panel);
            border: 1px solid var(--line);
            border-radius: 2px;
            padding: 16px;
            box-shadow: 0 1px 2px rgba(0, 0, 0, .04);
        }

        /* The right column owns the spacing between its panels. Flex gaps skip
           hidden children, so the top panel always sits flush with the left
           column no matter which panels happen to be visible. */
        .rightcol {
            display: flex;
            flex-direction: column;
            gap: 18px;
        }

        /* Panel titles carried the same whisper as every label — 12.5px mono
           uppercase — so hierarchy inside a panel was position alone. Titles
           now hold the serif tier; mono goes back to being data-chrome. */
        .panel h2 {
            margin: 0 0 12px;
            font-family: var(--serif);
            font-size: 21px;
            font-weight: 400;
            letter-spacing: 0;
            color: var(--ink);
        }

        .slots {
            display: flex;
            gap: 12px;
        }

        .slot {
            flex: 1;
        }

            .slot label {
                display: block;
                font-family: var(--chrome);
                font-size: var(--label);
                text-transform: uppercase;
                letter-spacing: var(--track);
                color: var(--ink-dim);
                margin-bottom: 6px;
            }

        .drop {
            position: relative;
            aspect-ratio: 5 / 7;
            border: 1px dashed var(--line);
            border-radius: 6px;
            background: var(--panel-2);
            display: flex;
            align-items: center;
            justify-content: center;
            overflow: hidden;
            cursor: pointer;
            color: var(--ink-dim);
            font-size: 14px;
            text-align: center;
            padding: 8px;
        }

            .drop:hover, .drop:focus-within {
                border-color: var(--ink);
            }

            .drop img {
                position: absolute;
                inset: 0;
                width: 100%;
                height: 100%;
                object-fit: cover;
            }

            .drop input {
                position: absolute;
                inset: 0;
                opacity: 0;
                cursor: pointer;
            }

        .slotctl {
            display: flex;
            gap: 10px;
        }

        /* Text buttons. 10px with zero padding was a mouse target on a page
           used by thumbs; the hit box is now 32px tall without the label
           getting any louder. */
        .clear {
            margin-top: 2px;
            background: none;
            border: none;
            color: var(--ink-dim);
            font-family: var(--chrome);
            font-size: var(--label);
            text-transform: uppercase;
            letter-spacing: var(--track);
            cursor: pointer;
            padding: 6px 2px;
            min-height: 32px;
            display: none;
        }

            .clear.show {
                display: inline-block;
            }

            .clear:hover {
                color: var(--ink);
            }

        .row {
            display: flex;
            align-items: center;
            gap: 10px;
            margin-top: 14px;
        }

        select {
            background: var(--panel);
            color: var(--ink);
            border: 1px solid var(--line-strong);
            border-radius: 3px;
            padding: 8px 10px;
            font: inherit;
        }

        /* The one button. Black, square, small caps — and the dealer's
           orange dot on its corner, which is the only colour it has. Shared
           by every primary action so the dot means "this is the thing to
           press" everywhere. */
        button.primary, button.save, .homeacts a.btn-accent {
            position: relative;
            background: var(--ink);
            color: var(--bg);
            border: 1px solid var(--ink);
            border-radius: 0;
            padding: 11px 16px;
            font: 600 12.5px var(--sans);
            letter-spacing: .12em;
            text-transform: uppercase;
            cursor: pointer;
        }

        button.primary {
            flex: 1;
        }

            button.primary::after, button.save::after, .homeacts a.btn-accent::after {
                content: "";
                position: absolute;
                right: -6px;
                top: -6px;
                width: 13px;
                height: 13px;
                border-radius: 50%;
                background: var(--dot);
                box-shadow: 0 1px 2px rgba(0, 0, 0, .25);
            }

            button.primary:hover, button.save:hover, .homeacts a.btn-accent:hover {
                background: #000;
                border-color: #000;
            }

            button.primary:disabled, button.save:disabled {
                background: var(--ink-dim);
                border-color: var(--ink-dim);
                cursor: wait;
                color: var(--bg);
            }

                button.primary:disabled::after, button.save:disabled::after {
                    background: var(--line-strong);
                }

        .hint {
            margin-top: 10px;
            font-size: 13px;
            color: var(--ink-dim);
        }

        /* A panel's standing explanation, folded. Open the first time a panel
           is seen and remembered closed once dismissed — prose that is read
           once and skimmed forever should not push the controls down a phone
           screen for ever. */
        details.about {
            margin: 10px 0 0;
        }

            details.about summary {
                cursor: pointer;
                list-style: none;
                font-family: var(--chrome);
                font-size: var(--label);
                text-transform: uppercase;
                letter-spacing: var(--track);
                color: var(--ink-dim);
                padding: 4px 0;
            }

                details.about summary::-webkit-details-marker {
                    display: none;
                }

                details.about summary::before {
                    content: "\25B8  ";
                    color: var(--ink-faint);
                }

                details.about[open] summary::before {
                    content: "\25BE  ";
                }

                details.about summary:hover {
                    color: var(--ink);
                }

            details.about .hint {
                margin: 4px 0 6px;
            }

            /* The invite how-to. Numbered rather than bulleted because it is a
               sequence you perform, not a set of facts you read. */
            ol.hint {
                margin: 10px 0 0;
                padding-left: 20px;
                line-height: 1.55;
            }

                ol.hint li {
                    margin-bottom: 5px;
                }

                ol.hint b {
                    color: var(--ink);
                    font-weight: 500;
                }

        .error {
            margin-top: 10px;
            font-size: 13px;
            color: var(--bad);
            display: none;
            white-space: pre-wrap;
        }

            .error.show {
                display: block;
            }

        /* Result — the "slab flip label" */
        #result {
            display: none;
        }

            #result.show {
                display: block;
            }

        .slabline {
            font-family: var(--chrome);
            font-size: var(--label);
            text-transform: uppercase;
            letter-spacing: var(--track);
            color: var(--ink-faint);
            border-bottom: 1px solid var(--ink);
            padding-bottom: 8px;
            margin-bottom: 10px;
        }

        .cardname {
            font-family: var(--serif);
            font-size: 26px;
            font-weight: 400;
            line-height: 1.15;
            margin: 0 0 2px;
        }

        .setline {
            color: var(--ink-dim);
            font-size: 14px;
            margin: 0 0 10px;
        }

        .badges {
            display: flex;
            flex-wrap: wrap;
            gap: 6px;
            margin: 0 0 12px;
        }

        .badge {
            font-family: var(--chrome);
            font-size: 11px;
            text-transform: uppercase;
            letter-spacing: var(--track);
            border: 1px solid var(--line);
            border-radius: 4px;
            padding: 3px 8px;
            color: var(--ink-dim);
        }

            .badge.hot {
                border-color: var(--ink);
                color: var(--ink);
            }

        .value {
            font-family: var(--serif);
            font-size: 30px;
            font-weight: 400;
        }

            .value small {
                font-size: 12px;
                font-weight: 400;
                color: var(--ink-dim);
                margin-left: 8px;
            }

        .notes {
            font-size: 13px;
            color: var(--ink-dim);
            margin-top: 8px;
        }

        .actions {
            display: flex;
            gap: 10px;
            margin-top: 14px;
            flex-wrap: wrap;
        }

            .actions a, .actions button {
                font: 500 13px var(--sans);
                background: var(--panel);
                color: var(--ink);
                border: 1px solid var(--line-strong);
                border-radius: 0;
                padding: 8px 12px;
                cursor: pointer;
                text-decoration: none;
            }

                .actions a:hover, .actions button:hover {
                    border-color: var(--ink);
                }

        .alert {
            border: 1px solid var(--bad);
            border-left: 3px solid var(--bad);
            border-radius: 2px;
            background: rgba(179, 38, 30, .05);
            padding: 10px 12px;
            margin: 0 0 12px;
            font-size: 13.5px;
            line-height: 1.45;
            color: #7a1a14;
        }

            .alert.warn {
                border-color: var(--warn);
                border-left-color: var(--warn);
                color: #5c4200;
                background: rgba(143, 100, 0, .06);
            }

            .alert b {
                display: block;
                font-family: var(--chrome);
                font-size: 10px;
                letter-spacing: .14em;
                text-transform: uppercase;
                margin-bottom: 3px;
            }

        td .flag {
            color: var(--bad);
            font-weight: 700;
            margin-left: 6px;
        }

            /* Shared, not wrong — so it reads as information rather than alarm. */
            td .flag.share {
                color: var(--accent);
                font-weight: 400;
            }

            /* The word beside the "!" — a title tooltip says nothing on a phone. */
            td .flagword {
                margin-left: 4px;
                font-size: 11.5px;
                font-weight: 500;
                color: var(--bad);
                white-space: nowrap;
            }

                /* The dot beside the word — the sticker a dealer puts on a
                   card that needs a second look. */
                td .flagword::before {
                    content: "";
                    display: inline-block;
                    width: 9px;
                    height: 9px;
                    margin-right: 4px;
                    border-radius: 50%;
                    background: var(--bad-dot);
                    vertical-align: -1px;
                }

        .pricehist {
            margin-top: 14px;
            border-top: 1px solid var(--line);
            padding-top: 10px;
        }

            .pricehist h3 {
                margin: 0 0 6px;
                font-family: var(--chrome);
                font-size: var(--label);
                font-weight: 600;
                text-transform: uppercase;
                letter-spacing: var(--track);
                color: var(--ink-dim);
            }

            .pricehist div.row2 {
                display: flex;
                justify-content: space-between;
                gap: 10px;
                font-size: 13px;
                padding: 3px 0;
                border-bottom: 1px solid var(--line);
            }

            .pricehist span.src {
                color: var(--ink-dim);
                font-family: var(--mono);
                font-size: 11px;
            }

        /* ------------------------------------------------------------------
           "Why this price?" — the estimate showing its work.

           The value is the most opaque thing on a card: a model produced it and
           the collector has to take it on faith. This is the affordance that
           turns it into something you can interrogate; the answer is built only
           from records the vault already holds.
           ------------------------------------------------------------------ */
        .explain {
            margin: 0 0 14px;
        }

            .explain button {
                appearance: none;
                background: var(--panel-2);
                border: 1px solid var(--line);
                border-radius: 6px;
                color: var(--accent);
                font: 500 12.5px var(--sans);
                padding: 7px 11px;
                cursor: pointer;
            }

                .explain button:hover:not(:disabled) {
                    border-color: var(--accent-dim);
                }

                .explain button:disabled {
                    color: var(--ink-dim);
                    cursor: default;
                }

            .explain .answer {
                display: none;
                margin-top: 9px;
                background: var(--panel-2);
                border-left: 2px solid var(--ink);
                border-radius: 0 6px 6px 0;
                padding: 10px 12px;
                font-size: 13px;
                line-height: 1.55;
                white-space: pre-wrap;
            }

                .explain .answer.show {
                    display: block;
                }

                .explain .answer .src {
                    display: block;
                    margin-top: 7px;
                    font-family: var(--chrome);
                    font-size: var(--label);
                    letter-spacing: var(--track);
                    text-transform: uppercase;
                    color: var(--ink-dim);
                }

        .basisnote {
            font-size: 12.5px;
            line-height: 1.45;
            color: var(--ink-dim);
            border-left: 2px solid var(--line);
            padding: 2px 0 2px 10px;
            margin-bottom: 14px;
        }

        #whoami a {
            color: var(--accent);
            text-decoration: none;
            margin-left: 10px;
        }

            #whoami a:hover {
                text-decoration: underline;
            }

        button.ghost {
            background: var(--panel);
            color: var(--ink-dim);
            border: 1px solid var(--line-strong);
            border-radius: 0;
            padding: 9px 12px;
            font: 500 13px var(--sans);
            cursor: pointer;
            white-space: nowrap;
        }

            button.ghost:hover {
                color: var(--ink);
                border-color: var(--ink);
            }

        th[data-sort] {
            cursor: pointer;
            user-select: none;
        }

            th[data-sort]:hover {
                color: var(--ink);
            }

            th[data-sort] span.arrow {
                color: var(--accent);
                margin-left: 4px;
            }

        .setrow {
            display: flex;
            align-items: center;
            gap: 10px;
            padding: 9px 0;
            border-bottom: 1px solid var(--line);
            cursor: pointer;
        }

            .setrow:hover .setname {
                text-decoration: underline;
                text-underline-offset: 3px;
            }

        .setname {
            flex: 1;
            font-size: 14px;
        }

            .setname small {
                display: block;
                color: var(--ink-dim);
                font-size: 12px;
            }

        .bar {
            width: 110px;
            height: 6px;
            background: var(--panel-2);
            border-radius: 3px;
            overflow: hidden;
        }

            .bar i {
                display: block;
                height: 100%;
                background: var(--dot);
            }

        .pct {
            font-family: var(--mono);
            font-size: 12px;
            color: var(--ink-dim);
            width: 78px;
            text-align: right;
            font-variant-numeric: tabular-nums;
        }

        tr.need td {
            color: var(--ink-dim);
        }

        tr.have td:first-child {
            box-shadow: inset 3px 0 0 var(--good-dot);
        }

        .cardphotos {
            display: flex;
            gap: 10px;
            margin-bottom: 14px;
        }

            /* 150px, up from 92: "people really care about the item/card most
               of all" — the photos lead the editor, so they get to read as the
               card rather than as icons. The WRAPS (photo + its retake label)
               are the flex children, so the size lives on them and the photo
               fills its wrap — a percentage on the anchor resolves against the
               wrap and quietly shrank the tile to half itself. Two still fit
               a narrow phone (flex splits the row), .missing holds the same
               footprint as a real side, and the lightbox remains the
               full-size path. */
            .cardphotos > div {
                flex: 1 1 0;
                max-width: 150px;
                min-width: 0;
            }

            .cardphotos a, .cardphotos .missing {
                display: block;
                width: 100%;
                aspect-ratio: 5 / 7;
                border: 1px solid var(--line);
                border-radius: 5px;
                overflow: hidden;
                background: var(--panel-2);
            }

                .cardphotos a:hover {
                    border-color: var(--ink);
                }

            .cardphotos img {
                width: 100%;
                height: 100%;
                object-fit: cover;
                display: block;
            }

            .cardphotos .missing {
                display: flex;
                align-items: center;
                justify-content: center;
                text-align: center;
                font-family: var(--chrome);
                font-size: 9px;
                letter-spacing: .1em;
                text-transform: uppercase;
                color: var(--ink-dim);
                padding: 6px;
            }

        /* Copies of the same checklist row, opened from a ✓ ×N cell. A
           horizontal strip — native scroll IS the swipe on a phone. */
        #copyStrip {
            display: none;
            align-items: center;
            gap: 8px;
            margin: -6px 0 14px;
            overflow-x: auto;
            padding-bottom: 4px;
        }

            #copyStrip .striplabel {
                font-family: var(--chrome);
                font-size: 10px;
                letter-spacing: .08em;
                text-transform: uppercase;
                color: var(--ink-dim);
                white-space: nowrap;
                flex: 0 0 auto;
            }

            #copyStrip button {
                flex: 0 0 auto;
                width: 46px;
                aspect-ratio: 5 / 7;
                padding: 0;
                border: 2px solid var(--line);
                border-radius: 5px;
                overflow: hidden;
                background: var(--panel-2);
                cursor: pointer;
                color: var(--ink-dim);
                font-family: var(--mono);
                font-size: 10px;
            }

                #copyStrip button:hover {
                    border-color: var(--accent-dim);
                }

                #copyStrip button.current {
                    border-color: var(--accent);
                    cursor: default;
                }

                #copyStrip button img {
                    width: 100%;
                    height: 100%;
                    object-fit: cover;
                    display: block;
                }

        .editgrid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
            gap: 10px;
        }

            .editgrid label, label.fullwidth {
                display: block;
                font-family: var(--chrome);
                font-size: 10px;
                text-transform: uppercase;
                letter-spacing: .12em;
                color: var(--ink-dim);
            }

        label.fullwidth {
            margin-top: 10px;
        }

            .editgrid input, .editgrid select, label.fullwidth textarea, #vaultSearch, #setSearch, #friendSearch {
                display: block;
                width: 100%;
                margin-top: 4px;
                background: var(--panel);
                color: var(--ink);
                border: 1px solid var(--line-strong);
                border-radius: 3px;
                padding: 7px 9px;
                font: 13px var(--sans);
                letter-spacing: 0;
                text-transform: none;
            }

            /* Inputs and selects apply vertical padding differently (Windows
               especially), so sharing padding still leaves a select's text
               floating off-center next to its input neighbours. Identical
               fixed boxes make the grid line up; a select centers its text
               within its height on its own. Textareas keep their auto height. */
            .editgrid input, .editgrid select {
                box-sizing: border-box;
                height: 32px;
            }

            /* The other half of "off center", measured from a problem report's
               snapshot: a control whose label wraps to two lines sits 15px
               lower than a neighbour whose label fits on one — the row's
               controls didn't share a bottom edge. Each cell is a flex column
               and the control takes the slack ABOVE itself (margin-top auto),
               so label text stays at the top, controls align at the bottom,
               and the flex gap keeps the 4px breathing room the old margin
               provided in the tallest cell. */
            .editgrid label {
                display: flex;
                flex-direction: column;
                gap: 4px;
            }

            .editgrid label > input, .editgrid label > select {
                margin-top: auto;
            }

            /* Money fields read as money. The input stays type=number — a
               browser rejects "$11.00" in one, and the save path parses it as a
               number — so the currency mark sits beside the field rather than
               inside its value. Reported as "can we have est value as currency?"
               against a field showing a bare 11. */
            .editgrid label > .money {
                display: flex;
                align-items: center;
                gap: 6px;
                margin-top: auto;
            }

            .editgrid label > .money::before {
                content: attr(data-cur);
                color: var(--muted);
                font-variant-numeric: tabular-nums;
            }

            .editgrid label > .money > input {
                flex: 1;
                min-width: 0;
                margin-top: 0;
            }

            label.fullwidth textarea {
                resize: vertical;
                font-family: inherit;
            }

        .checkrow {
            display: flex;
            gap: 18px;
            margin-top: 12px;
        }

        label.check {
            display: flex;
            align-items: center;
            gap: 6px;
            font-family: var(--chrome);
            font-size: 10px;
            text-transform: uppercase;
            letter-spacing: .12em;
            color: var(--ink-dim);
            cursor: pointer;
        }

            label.check input {
                accent-color: var(--ink);
            }

        /* The rips rows' delete: a ghost, but a visibly DESTRUCTIVE ghost,
           held a full gap away from the box-cost button beside it. */
        button.ghost.ripdel {
            color: var(--bad);
            margin-left: 14px;
        }

        button.danger {
            color: var(--bad);
        }

            button.danger:hover {
                border-color: var(--bad);
            }

        tr.clickable {
            cursor: pointer;
        }

            tr.clickable:hover td {
                background: var(--panel-2);
            }

        /* The row you have open in the editor. Was a 2px inset line, which on a
           dark table is close to invisible — and the whole job of this state is
           to answer "which of these am I editing?" from across the page. */
        tr.selected td {
            background: var(--panel-2);
            box-shadow: inset 0 1px 0 var(--line-strong), inset 0 -1px 0 var(--line-strong);
        }

            tr.selected td:first-child {
                box-shadow: inset 4px 0 0 var(--dot), inset 0 1px 0 var(--line-strong), inset 0 -1px 0 var(--line-strong);
            }

            tr.selected td.c-name {
                color: var(--ink);
                font-weight: 600;
            }

        /* ------------------------------------------------------------------
           The editor is not a peer of the list it came from.

           Both were the same panel — same fill, same border, same weight — so
           opening a card gave you two identical slabs and no signal about which
           one you were working in. The accent edge and the lifted fill say "this
           is the thing you are in"; the list below stays a list.
           ------------------------------------------------------------------ */
        #editorPanel {
            border-color: var(--line);
            border-top: 2px solid var(--ink);
            background: var(--panel);
        }

        /* ------------------------------------------------------------------
           D3: rail | vault | inspector. At rail widths the editor stops being
           a panel that shoves the list away and becomes a persistent right-hand
           inspector: the list stays where you were, the row stays highlighted,
           and closing is no longer a navigation. Same markup, moved by CSS —
           the 0.35.0 rail trick one level down. The gate matches the rail's
           (1200px) so "desktop" means one thing. Below it, nothing changes:
           the phone's full-width editor is right, and the tablet keeps the
           stacked panels it has today.

           The inspector scrolls INSIDE itself (sticky + max-height): a card's
           form plus price history outruns any viewport, and the page scrollbar
           must keep belonging to the list. overscroll-behavior keeps a wheel
           at the inspector's end from grabbing the page.
           ------------------------------------------------------------------ */
        @media (min-width: 1200px) {
            #v-vault.editing.on {
                display: grid;
                grid-template-columns: minmax(0, 1fr) 380px;
                gap: 18px;
                align-items: start;
            }

                #v-vault.editing.on > #editorPanel {
                    order: 2;
                    position: sticky;
                    top: 16px;
                    max-height: calc(100vh - 32px);
                    overflow-y: auto;
                    overscroll-behavior: contain;
                }
        }

        /* The card being edited, named in the heading. "Edit card" is true of
           every card and therefore tells you nothing. */
        #editorTitle {
            font-family: inherit;
            font-size: 15px;
            font-weight: 600;
            letter-spacing: 0;
            text-transform: none;
            color: var(--ink);
        }

            #editorTitle .eyebrow {
                display: block;
                font-family: var(--chrome);
                font-size: 11px;
                font-weight: 600;
                text-transform: uppercase;
                letter-spacing: .14em;
                color: var(--ink-faint);
                margin-bottom: 3px;
            }

        th.selcol, td.selcol {
            width: 22px;
            padding-right: 0;
        }

            th.selcol input, td.selcol input {
                accent-color: var(--ink);
                cursor: pointer;
                display: block;
                margin: 0;
            }

        #batchOut {
            display: none;
            margin-top: 12px;
            border: 1px solid var(--line);
            border-left: 3px solid var(--ink);
            border-radius: 2px;
            padding: 10px 12px;
            font-size: 12.5px;
            line-height: 1.6;
        }

            #batchOut b {
                display: block;
                font-family: var(--chrome);
                font-size: var(--label);
                letter-spacing: var(--track);
                text-transform: uppercase;
                color: var(--ink-dim);
                margin-bottom: 4px;
            }

            #batchOut .fail {
                color: var(--bad);
            }

            #batchOut .note {
                color: var(--ink-dim);
                margin-top: 4px;
            }

        .listingbox {
            display: none;
            margin-top: 16px;
            border-top: 1px solid var(--line);
            padding-top: 14px;
        }

            .listingbox.show {
                display: block;
            }

        .fieldrow {
            margin-bottom: 14px;
        }

        .fieldlabel {
            font-family: var(--chrome);
            font-size: var(--label);
            text-transform: uppercase;
            letter-spacing: var(--track);
            color: var(--ink-dim);
            margin-bottom: 5px;
        }

        .fieldval {
            background: var(--panel-2);
            border: 1px solid var(--line);
            border-radius: 6px;
            padding: 9px 11px;
            font: 13px/1.5 var(--mono);
            white-space: pre-wrap;
            word-break: break-word;
        }

        #titleCount {
            color: var(--accent);
            letter-spacing: 0;
        }

            #titleCount.over {
                color: var(--bad);
            }

        table.specifics {
            width: 100%;
            border-collapse: collapse;
            font-size: 13px;
        }

            table.specifics td {
                padding: 4px 8px;
                border-bottom: 1px solid var(--line);
                vertical-align: top;
            }

                table.specifics td:first-child {
                    color: var(--ink-dim);
                    width: 45%;
                }

        .warnings {
            margin-bottom: 14px;
        }

            .warnings div {
                font-size: 13px;
                color: var(--warn);
                border-left: 2px solid var(--warn);
                padding: 4px 10px;
                margin-bottom: 6px;
            }

        #listingPreview {
            width: 100%;
            height: 620px;
            margin-top: 10px;
            border: 1px solid var(--line);
            border-radius: 6px;
            background: #fff;
        }

        pre.listing {
            display: none;
            margin: 12px 0 0;
            padding: 12px;
            background: var(--panel-2);
            border: 1px solid var(--line);
            border-radius: 6px;
            font: 12px/1.5 var(--mono);
            white-space: pre-wrap;
            color: var(--ink);
        }

            pre.listing.show {
                display: block;
            }

        /* Vault table */
        .vaulthead {
            display: flex;
            align-items: baseline;
            justify-content: space-between;
            gap: 10px;
            flex-wrap: wrap;
        }

        .stats {
            font-family: var(--chrome);
            font-size: 12px;
            text-transform: uppercase;
            letter-spacing: var(--track);
            color: var(--ink-dim);
        }

            .stats b {
                color: var(--ink);
                font-weight: 600;
            }

        /* Help view: reference prose. Measure capped where lines stay readable;
           section heads reuse the app's eyebrow voice so Help looks like the
           app explaining itself, not a pasted-in document. */
        #v-help .helpbody {
            max-width: 68ch;
        }

            #v-help .helpbody h3 {
                font-family: var(--serif);
                font-size: 20px;
                font-weight: 400;
                text-transform: none;
                letter-spacing: 0;
                color: var(--ink);
                margin: 26px 0 8px;
            }

            #v-help .helpbody p {
                font-size: 14.5px;
                line-height: 1.55;
                margin: 0 0 10px;
            }

        .helptable {
            border-collapse: collapse;
            width: 100%;
            margin: 8px 0 10px;
            font-size: 13.5px;
        }

            .helptable th, .helptable td {
                text-align: left;
                padding: 6px 12px 6px 0;
                border-bottom: 1px solid var(--line);
                vertical-align: top;
            }

            .helptable th {
                font-family: var(--chrome);
                font-size: 11px;
                text-transform: uppercase;
                letter-spacing: .1em;
                color: var(--ink-dim);
            }

        table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 10px;
            font-size: 13.5px;
        }

        th {
            text-align: left;
            font-family: var(--chrome);
            font-size: var(--label);
            text-transform: uppercase;
            letter-spacing: var(--track);
            color: var(--ink-dim);
            font-weight: 600;
            padding: 6px 8px;
            border-bottom: 1px solid var(--line);
        }

        td {
            padding: 7px 8px;
            border-bottom: 1px solid var(--line);
        }

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

        .empty {
            color: var(--ink-dim);
            font-size: 13.5px;
            padding: 10px 8px;
        }

        a.plain {
            color: var(--accent);
            text-decoration: none;
        }

            a.plain:hover {
                text-decoration: underline;
            }

        :focus-visible {
            outline: 2px solid var(--dot);
            outline-offset: 2px;
        }

        /* ---- Command palette (D4) ----
           Top-anchored, not centered: results grow and shrink as you type, and
           a centered box would bounce its own input around. Above the issue
           overlay (210) — the palette can be summoned from anywhere and must
           never open UNDER something. */

        .pal-overlay {
            position: fixed;
            inset: 0;
            z-index: 220;
            background: rgba(28, 28, 28, .42);
            padding-top: 12vh;
        }

        .pal {
            width: min(560px, calc(100vw - 32px));
            margin: 0 auto;
            background: var(--panel);
            border: 1px solid var(--ink);
            border-radius: 2px;
            box-shadow: 0 24px 80px rgba(0, 0, 0, .28);
            overflow: hidden;
        }

            .pal input {
                width: 100%;
                border: none;
                border-bottom: 1px solid var(--line);
                border-radius: 0;
                background: transparent;
                padding: 14px 16px;
                font-size: 16px;   /* ≥16px: iOS zooms smaller inputs on focus */
            }

                .pal input:focus {
                    outline: none;
                }

        .pal-results {
            max-height: min(48vh, 420px);
            overflow-y: auto;
            overscroll-behavior: contain;
        }

            .pal-results .pal-head {
                padding: 8px 16px 2px;
                font-family: var(--chrome);
                font-size: 10.5px;
                font-weight: 600;
                text-transform: uppercase;
                letter-spacing: .14em;
                color: var(--ink-dim);
            }

            .pal-results .pal-row {
                display: flex;
                align-items: baseline;
                gap: 10px;
                padding: 8px 16px;
                cursor: pointer;
            }

                .pal-results .pal-row .t {
                    flex: 0 1 auto;
                    min-width: 0;
                    white-space: nowrap;
                    overflow: hidden;
                    text-overflow: ellipsis;
                }

                .pal-results .pal-row .s {
                    flex: 1 1 auto;
                    min-width: 0;
                    white-space: nowrap;
                    overflow: hidden;
                    text-overflow: ellipsis;
                    color: var(--ink-dim);
                    font-size: 12.5px;
                    text-align: right;
                }

                .pal-results .pal-row.sel {
                    background: var(--panel-2);
                    box-shadow: inset 3px 0 0 var(--dot);
                }

            .pal-results .pal-none {
                padding: 14px 16px;
                color: var(--ink-dim);
            }

        .pal-hint {
            padding: 8px 16px;
            border-top: 1px solid var(--line);
            font-family: var(--mono);
            font-size: 10.5px;
            letter-spacing: .08em;
            color: var(--ink-dim);
        }

        /* The discoverable half of Ctrl-K, next to "report a problem". Hidden
           on touch-narrow screens: no hardware keyboard, no palette. */
        #palBtn {
            display: none;
        }

        @media (min-width: 1200px) {
            #palBtn {
                display: inline-block;
            }
        }

        /* ---- Sub-tabs (Settings, Admin) ----
           One group of panels on screen at a time — seven same-looking
           sections in one scroll was the report, three times over. The row
           borrows the rail's voice (mono, uppercase, accent for the active
           one) so orientation is typographic, not positional. .taboff hides
           with !important on purpose: gated panels keep their own inline
           display for "does this section exist for this account", and the
           tab's class must win over an inline "block" when the group is
           inactive. */

        .subnav {
            display: flex;
            gap: 6px;
            flex-wrap: wrap;
            margin-bottom: 14px;
        }

            .subnav button {
                background: none;
                border: 1px solid var(--line);
                border-radius: 999px;
                padding: 5px 14px;
                font-family: var(--chrome);
                font-size: 11px;
                font-weight: 600;
                text-transform: uppercase;
                letter-spacing: .1em;
                color: var(--ink-dim);
                cursor: pointer;
            }

                .subnav button:hover {
                    color: var(--ink);
                    border-color: var(--ink);
                }

                .subnav button.on {
                    color: var(--bg);
                    border-color: var(--ink);
                    background: var(--ink);
                }

        .panel.taboff {
            display: none !important;
        }

        /* ---- One question, one answer (askText) ----
           The styled replacement for prompt(): the native dialog is unthemed,
           blocking, and in an installed app titled with the origin — "the
           Report a Problem popup is more consistent to what I'd expect" was
           the report, and it was right. Sits under the palette (220), above
           the issue overlay (210): a question asked from a form must never
           open beneath the thing that asked it. */

        .ask-overlay {
            position: fixed;
            inset: 0;
            z-index: 215;
            display: flex;
            align-items: flex-start;
            justify-content: center;
            padding-top: 16vh;
            background: rgba(28, 28, 28, .42);
        }

        .ask-card {
            background: var(--panel);
            border: 1px solid var(--ink);
            border-radius: 2px;
            padding: 20px 22px;
            width: min(460px, calc(100vw - 32px));
            box-shadow: 0 24px 80px rgba(0, 0, 0, .28);
        }

            .ask-card h3 {
                margin: 0;
                font-size: 15px;
                font-weight: 600;
            }

            .ask-card .hint {
                margin: 8px 0 0;
            }

            .ask-card input {
                width: 100%;
                margin-top: 12px;
                font-size: 16px;   /* ≥16px: iOS zooms smaller inputs on focus */
            }

            .ask-card .actions {
                margin-top: 14px;
            }

        /* ---- Report a problem (see reportIssue) ---- */

        .issue-overlay {
            position: fixed;
            inset: 0;
            z-index: 210;
            display: flex;
            align-items: center;
            justify-content: center;
            background: rgba(28, 28, 28, .42);
        }

        .issue-card {
            background: var(--panel);
            border: 1px solid var(--ink);
            border-radius: 2px;
            padding: 22px 26px;
            max-width: 480px;
            width: calc(100% - 48px);
            box-shadow: 0 18px 60px rgba(0, 0, 0, .28);
        }

            .issue-card h3 {
                margin: 0 0 6px;
                font-size: 16px;
            }

            .issue-card textarea {
                width: 100%;
                margin-top: 10px;
            }

        #reportIssueBtn, #palBtn {
            background: none;
            border: none;
            color: var(--ink-dim);
            font-size: 11.5px;
            cursor: pointer;
            padding: 2px 4px;
        }

            #reportIssueBtn:hover, #palBtn:hover {
                color: var(--accent);
                text-decoration: underline;
            }

        .issue-snap-frame {
            width: 100%;
            height: 420px;
            border: 1px solid var(--line);
            border-radius: 8px;
            background: #fff;
        }

        /* ---- Life list: the "new card!" party (see showLifer) ---- */

        .lifer-overlay {
            position: fixed;
            inset: 0;
            z-index: 200;
            display: flex;
            align-items: center;
            justify-content: center;
            background: rgba(28, 28, 28, .42);
            animation: liferFade .25s ease-out;
            cursor: pointer;
            overflow: hidden;
        }

        .lifer-card {
            position: relative;
            background: var(--panel);
            border: 1px solid var(--ink);
            border-radius: 2px;
            padding: 28px 36px 24px;
            max-width: 420px;
            width: calc(100% - 48px);
            text-align: center;
            box-shadow: 0 18px 60px rgba(0, 0, 0, .28);
            animation: liferPop .45s cubic-bezier(.2, 1.6, .4, 1);
        }

        .lifer-kicker {
            font-family: var(--chrome);
            font-size: 12px;
            letter-spacing: .35em;
            color: var(--accent);
            margin-bottom: 10px;
        }

            .lifer-kicker.complete {
                color: var(--good);
            }

            /* The "new card" party: the confetti is the dealer's dots. */

        .lifer-title {
            font-family: var(--serif);
            font-size: 28px;
            font-weight: 400;
            margin-bottom: 4px;
        }

        .lifer-sub {
            color: var(--ink-dim);
            font-size: 13.5px;
            margin-bottom: 16px;
        }

        .lifer-bar {
            height: 6px;
            border-radius: 3px;
            background: var(--panel-2);
            overflow: hidden;
            margin-bottom: 8px;
        }

            .lifer-bar > i {
                display: block;
                height: 100%;
                border-radius: 3px;
                background: var(--dot);
                width: 0;
                transition: width .9s cubic-bezier(.25, .1, .25, 1) .35s;
            }

            .lifer-bar.complete > i {
                background: var(--good-dot);
            }

        .lifer-count {
            font-family: var(--mono);
            font-size: 12.5px;
            color: var(--ink-dim);
        }

            .lifer-count b {
                color: var(--ink);
            }

        .lifer-dismiss {
            margin-top: 14px;
            font-size: 11.5px;
            color: var(--ink-dim);
            opacity: .7;
        }

        .lifer-confetti {
            position: absolute;
            top: -8px;
            width: 7px;
            height: 11px;
            border-radius: 1px;
            opacity: 0;
            animation: liferDrop 2.6s ease-in forwards;
        }

        @keyframes liferFade {
            from { opacity: 0; }
        }

        @keyframes liferPop {
            from { transform: scale(.7); opacity: 0; }
        }

        @keyframes liferDrop {
            0% { opacity: 0; transform: translateY(-10px) rotate(0deg); }
            8% { opacity: 1; }
            100% { opacity: 0; transform: translateY(105vh) rotate(660deg); }
        }

        @media (prefers-reduced-motion: reduce) {
            .lifer-overlay, .lifer-card { animation: none; }
            .lifer-confetti { display: none; }
            .lifer-bar > i { transition: none; }
        }
        /* ------------------------------------------------------------------
           Rip party (0.72.0). The join code is the one thing on the screen
           that must be readable across a table, so it gets the display face
           at a size nothing else in the app uses. The feed reuses the vault's
           sleeve (.vtile) so a pull looks like a pull, with the owner's handle
           on it and the dealer's sticker for the price.
           ------------------------------------------------------------------ */
        .partyline {
            margin-top: 6px;
            font-size: 13px;
        }

            .partyline b {
                font-weight: 600;
            }

        .partycode {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 14px;
            flex-wrap: wrap;
            margin-top: 12px;
            padding: 14px 16px;
            border: 1px solid var(--line);
            background: var(--panel-2);
        }

            .partycode .code {
                font-family: var(--serif);
                font-size: 46px;
                letter-spacing: .18em;
                line-height: 1;
                margin-top: 4px;
            }

        .partytotals {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
            gap: 14px;
            margin-top: 16px;
        }

            .partytotals .n {
                font-family: var(--serif);
                font-size: 30px;
                line-height: 1;
            }

            .partytotals .l {
                margin-top: 5px;
                font-family: var(--chrome);
                font-size: var(--label);
                letter-spacing: var(--track);
                text-transform: uppercase;
                color: var(--ink-faint);
            }

        .partymembers {
            margin-top: 16px;
            border-top: 1px solid var(--line);
        }

            .partymembers .who {
                display: flex;
                align-items: baseline;
                gap: 10px;
                padding: 8px 0;
                border-bottom: 1px solid var(--line);
                font-size: 13.5px;
            }

                .partymembers .who .h {
                    flex: 1;
                    min-width: 0;
                    overflow: hidden;
                    text-overflow: ellipsis;
                    white-space: nowrap;
                }

                .partymembers .who .box {
                    color: var(--ink-dim);
                    font-size: 12.5px;
                }

                .partymembers .who .v {
                    font-variant-numeric: tabular-nums;
                    white-space: nowrap;
                }

                .partymembers .who.gone {
                    opacity: .55;
                }

        .partyh3 {
            margin: 18px 0 8px;
            font-family: var(--chrome);
            font-size: var(--label);
            letter-spacing: var(--track);
            text-transform: uppercase;
            color: var(--ink-faint);
        }

        .pfeed {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(118px, 1fr));
            gap: 10px;
        }

            .pfeed .vtile {
                cursor: default;
            }

                .pfeed .vtile.mine {
                    cursor: pointer;
                }

                /* Landed in the last minute: the dealer's orange, so the
                   newest pull on the table is the one every eye goes to. */
                .pfeed .vtile.fresh {
                    outline: 2px solid var(--dot);
                    outline-offset: -2px;
                }

            .pfeed .pwho {
                position: absolute;
                left: 6px;
                bottom: 6px;
                z-index: 2;
                max-width: calc(100% - 12px);
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
                padding: 1px 5px;
                border-radius: 4px;
                background: rgba(11, 16, 23, .82);
                color: #fff;
                font-family: var(--chrome);
                font-size: 10px;
                letter-spacing: .06em;
            }

            /* The price sticker moves up when the handle takes the corner. */
            .pfeed .vtile .btag {
                bottom: 26px;
            }

        /* A photo the scanner judged to be a different physical card from its
           partner — kept, but never shown without saying so. */
        a.mismatched img { outline: 2px solid var(--bad); outline-offset: -2px; }

