/* Layout is the shared .page (main.css); this only adds a local custom prop. */
.admin-page {
    /* .admin-cards-mode-info .admin-pricing-layout's first track width (see
       further down) — the list item itself has no explicit width of its
       own, it just stretches to fill whatever this track resolves to. */
    --admin-pid-info-list-width: 480px;
}

.admin-subnav {
    display: flex;
    align-items: center;
    gap: 4px;
}

.admin-subnav-btn {
    font-family: var(--font-mono);
    font-size: 0.78rem;
    font-weight: 400;
    color: var(--text-muted);
    background: transparent;
    border: none;
    border-bottom: 2px solid transparent;
    padding: 8px 16px;
    cursor: pointer;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    transition: color 0.2s, border-color 0.2s;
}

.admin-subnav-btn:hover,
.admin-subnav-btn.active {
    color: var(--text);
    border-bottom-color: var(--accent);
}

.admin-content {
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    min-height: 0;
    padding: 32px;
}

/* .fade-out / .fade-in are shared (main.css). */

.admin-section {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
}

.admin-section-title {
    display: block;
    font-family: var(--font-mono);
    font-size: 0.78rem;
    font-weight: 500;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--accent);
    margin-bottom: 16px;
}

.admin-section-placeholder {
    font-size: 0.9rem;
    color: var(--text-muted);
}

/* Centered like a standalone options menu, rather than left-anchored like
   the Cards/Users sections' full-width working areas — System has no list
   or grid to fill the space, just the settings-cards column (.admin-system-
   options-col) and, once Use JSON is off, the Sync panel beside it — see
   .admin-system-sync-panel below. justify-content:center keeps the pair
   centered as a unit the same way the settings card alone used to center
   itself via margin:auto; align-items:flex-start so the shorter panel
   doesn't stretch to match the taller one's height. admin.js's
   animateSystemPanelsForUseJson (via flipSlide / animateHeightWipe in
   animation.js) animates the re-centre and the Sync panel's reveal.

   No overflow clip here: flipSlide only ever moves the settings card between
   two in-bounds positions, and animateHeightWipe clips the Sync panel on the
   panel itself — so nothing escapes this row. Clipping here instead cut off
   the SSL field's dropdown where it extended past the Sync panel's bottom. */
.admin-system-panels {
    display: flex;
    gap: 16px;
    justify-content: center;
    align-items: flex-start;
    width: 100%;
    margin-top: 32px;
}

/* The left stack in .admin-system-panels — the main settings card plus the
   separate Local DB card below it, kept the same width so they line up. */
.admin-system-options-col {
    display: flex;
    flex-direction: column;
    gap: 16px;
    width: 100%;
    max-width: 420px;
}

/* One bordered card holds every settings row (divided by border-bottom
   below), rather than each row being its own floating card. */
.admin-system-options {
    display: flex;
    flex-direction: column;
    width: 100%;
    max-width: 420px;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    overflow: hidden;
}

.admin-system-option-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 14px 16px;
    border-bottom: 1px solid var(--border);
}

.admin-system-option-row:last-child {
    border-bottom: none;
}

.admin-system-option-name {
    display: inline-flex;
    align-items: center;
    font-size: 0.9rem;
    color: var(--text);
}

/* ⓘ affordance beside a setting's name — its explainer text lives in the
   button's data-tip and is shown as a floating bubble on hover/focus by
   admin.js (appended to <body>, so it isn't clipped by the cards'
   overflow:hidden). */
.admin-system-info {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 15px;
    height: 15px;
    margin-left: 7px;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: 50%;
    background: transparent;
    font: italic 600 0.66rem/1 Georgia, 'Times New Roman', serif;
    color: var(--text-muted);
    cursor: help;
    transition: color 0.12s ease, border-color 0.12s ease;
}

.admin-system-info:hover,
.admin-system-info:focus-visible {
    color: var(--text);
    border-color: var(--border-focus);
    outline: none;
}

.admin-system-tip {
    position: fixed;
    z-index: 1000;
    max-width: 260px;
    padding: 9px 11px;
    background: var(--bg-hover);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-size: 0.74rem;
    line-height: 1.5;
    color: var(--text-muted);
    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.12s ease;
}

.admin-system-tip.visible {
    opacity: 1;
}

/* The System On/Off setting toggles carry the shared .pill-toggle plus
   .admin-pid-source-toggle (the tighter Regular/Discord track); this class only
   adds the data-setting hook loadAdminSystemSettings/updateAdminSystemSetting
   (admin.js) use to find the right pill. */
.admin-system-option-toggle {
    flex-shrink: 0;
}

/* Appears beside .admin-system-options-col once Use JSON is off (see
   renderSyncPanelVisibility / animateSystemPanelsForUseJson in admin.js),
   and also while a switch is being staged so the connection can be set up
   first. Holds the Database Connection editor plus the Sync / Wipe actions.
   Same bordered-card look as the settings options card. */
.admin-system-sync-panel {
    display: flex;
    flex-direction: column;
    gap: 10px;
    width: 100%;
    max-width: 320px;
    padding: 16px;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
}

.admin-system-sync-title {
    font-size: 0.9rem;
    color: var(--text);
}

.admin-system-sync-desc {
    font-size: 0.78rem;
    line-height: 1.45;
    color: var(--text-muted);
}

.admin-system-sync-btn {
    align-self: flex-start;
    flex-shrink: 0;
}

/* Sync / Wipe now sit in the Local DB card as .admin-system-option-row items
   (right-aligned action button, ⓘ hover on the label — see admin.html). The
   result log ("Wiped N tables…" / the sync counts) is the card's final child
   and reads as part of the same menu: no inset box, no border, and NO divider
   line above it — the Wipe row (last .admin-system-option-row) drops its
   bottom border so nothing separates it from the log. The card animates its
   own height around the log appearing / changing via animateBoxResize (see
   runAdminSystemSync / runAdminSystemWipe in admin.js). */
.admin-system-local-db-card .admin-system-option-row:last-of-type {
    border-bottom: none;
}

.admin-system-local-db-card .admin-system-sync-log {
    margin: 0;
    padding: 12px 16px 14px;
    background: transparent;
    border: none;
    border-radius: 0;
}

/* Wipe Database — same shape as .admin-system-sync-btn (inherits
   .admin-pid-refresh-btn's base look) but in --error instead of --accent,
   the one deliberately alarming-looking control in this card. Actual
   confirmation happens in JS (runAdminSystemWipe opens the shared type-
   "confirm" appConfirm modal) — this is just the visual warning, not the
   safeguard itself. */
.admin-system-wipe-btn {
    align-self: flex-start;
    flex-shrink: 0;
    background: var(--error);
}

/* Sync Now's result log — the same output CLI users of
   scripts/migrate_json_to_pg.py see, just rendered in the panel instead of
   a terminal (see runAdminSystemSync in admin.js). Monospace + pre-wrap so
   the per-domain row counts stay readable without a horizontal scrollbar.
   Caps at 240px and scrolls with the shared .scroll-thin bar (main.css) —
   the class is on the element in admin.html. */
.admin-system-sync-log {
    max-height: 240px;
    overflow-y: auto;
    margin: 0;
    padding: 10px;
    font-family: var(--font-mono);
    font-size: 0.72rem;
    line-height: 1.5;
    white-space: pre-wrap;
    word-break: break-word;
    color: var(--text-muted);
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
}

.admin-system-sync-log.admin-system-sync-log-error {
    color: var(--error);
    border-color: var(--error);
}

/* Data Import / Export card — one row per store, Export + Import buttons
   right-aligned like the Sync / Wipe rows. Its result log keeps the bordered
   inset box (unlike the Local DB card's borderless variant) but is inset from
   the card edges so the box reads as sitting inside the row group. */
.admin-system-io-actions {
    display: flex;
    gap: 8px;
    flex-shrink: 0;
}

#admin-system-io-card .admin-system-option-row:last-of-type {
    border-bottom: none;
}

#admin-system-io-card .admin-system-sync-log {
    margin: 0 16px 14px;
}

/* Database Connection lives at the top of .admin-system-sync-panel (above
   Sync / Wipe), so it wipes in with that panel's phase-2 vertical reveal
   for free — see animateHeightWipe (animation.js) /
   animateSystemPanelsForUseJson (admin.js). The fields sit in a 2-up grid to keep the panel from getting
   tall. */
.admin-system-db-fields {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px 10px;
}

.admin-system-db-field {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}

.admin-system-db-label {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 6px;
    font-size: 0.72rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* Per-field auto-save state dot (see _setAdminDbPip in admin.js). Invisible
   when clean; amber while an edit is pending / in flight, green on save,
   red on failure. */
.admin-system-db-pip {
    flex-shrink: 0;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: transparent;
    transition: background-color 0.2s ease;
}

.admin-system-db-pip[data-state="dirty"],
.admin-system-db-pip[data-state="saving"] {
    background: var(--warning);
}

.admin-system-db-pip[data-state="saving"] {
    animation: adminDbPipPulse 1s ease-in-out infinite;
}

.admin-system-db-pip[data-state="saved"] {
    background: var(--success);
}

.admin-system-db-pip[data-state="error"] {
    background: var(--error);
}

@keyframes adminDbPipPulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.3; }
}

/* Base input styling is global (components.css); overrides only. */
.admin-system-db-input {
    padding: 7px 9px;
    font-size: 0.82rem;
}

.admin-system-db-input:focus {
    border-color: var(--accent);
}

/* The SSL field is an .admin-pid-dropdown widget (rotating arrow +
   revealDown menu, same look as the Cards page filters — see admin.html).
   Nudge its button padding to sit level with the sibling text inputs in this
   row; the menu is already left:0/right:0 so it tracks the narrow grid cell,
   and opens downward past the Sync panel's edge now that .admin-system-panels
   no longer clips it. */
.admin-system-db-ssl-wrap .admin-pid-dropdown-btn {
    padding: 7px 9px;
}

/* Scroll the inner list, never the .menu box itself. A rounded element's own
   scrollbar isn't clipped by its border-radius and spills past the corner;
   nesting the scroll one level in lets .menu's overflow:hidden clip the bar to
   the rounded edge — the same split the Cards page uses for #set-dropdown-options
   inside its .menu. */
.admin-system-db-ssl-wrap .admin-pid-dropdown-menu {
    overflow: hidden;
    max-height: none;
}

.admin-system-db-ssl-wrap .admin-pid-dropdown-options {
    max-height: 200px;
    overflow-y: auto;
}

/* Scrollbar matches #set-dropdown-options on the Cards page exactly. */
.admin-system-db-ssl-wrap .admin-pid-dropdown-options::-webkit-scrollbar {
    width: 6px;
}

.admin-system-db-ssl-wrap .admin-pid-dropdown-options::-webkit-scrollbar-track {
    background: var(--bg-card);
}

.admin-system-db-ssl-wrap .admin-pid-dropdown-options::-webkit-scrollbar-thumb {
    background: var(--border);
    border-radius: 3px;
}

.admin-system-db-ssl-wrap .admin-pid-dropdown-options::-webkit-scrollbar-thumb:hover {
    background: var(--border-focus);
}

.admin-system-db-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 2px;
}

/* Test / Set Up Database result line. Caps + scrolls (shared .scroll-thin bar,
   main.css — class is on the <p> in admin.html) so a multi-line Postgres error
   doesn't balloon the panel; #admin-system-sync-panel animates its own height
   around this appearing (animateBoxResize in testAdminSystemDatabaseConnection
   / setupAdminDatabase). */
.admin-system-db-status {
    max-height: 120px;
    overflow-y: auto;
    margin: 0;
    font-size: 0.75rem;
    line-height: 1.4;
    word-break: break-word;
    color: var(--text-muted);
}

.admin-system-db-status.admin-system-db-status-error {
    color: var(--error);
}

.admin-system-db-status.admin-system-db-status-ok {
    color: var(--accent);
}

/* ── Use JSON → database-mode staged confirmation ────────────────────────
   Shown above .admin-system-panels while the Off switch is pending (see
   beginUseJsonStaging in admin.js) — nothing is written until Confirm. Same
   bordered-card language as the panels below it, with an --accent left edge
   so it reads as an active blocking step rather than a passive notice. */

/* Height-wipe clipper for the bar's open/close animation (animateHeightWipe in
   animation.js animates this element's height 0 ↔ natural). overflow:hidden so
   the bar is revealed/hidden as a vertical wipe; flex-shrink:0 because
   #admin-section-system is a definite-height flex column and its
   .admin-system-panels child can make total content overflow — without this,
   an overflow:hidden flex item's automatic minimum size is 0 and the bar
   would collapse under that shrink pressure instead of holding its height. */
.admin-system-staging-wrap {
    flex-shrink: 0;
    overflow: hidden;
}

.admin-system-staging-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: 16px 24px;
    width: 100%;
    max-width: 760px;
    margin: 24px auto 0;
    padding: 16px 18px;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius-sm);
}

.admin-system-staging-main {
    display: flex;
    flex-direction: column;
    gap: 8px;
    flex: 1 1 340px;
    min-width: 0;
}

.admin-system-staging-title {
    font-size: 0.9rem;
    color: var(--text);
}

.admin-system-staging-msg {
    margin: 0;
    font-size: 0.78rem;
    line-height: 1.45;
    color: var(--text-muted);
}

.admin-system-staging-checklist {
    list-style: none;
    margin: 4px 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.admin-system-staging-checklist li {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 0.8rem;
    color: var(--text-muted);
}

.admin-system-staging-checklist li[data-state="ok"] {
    color: var(--text);
}

.admin-system-staging-checklist li[data-state="fail"] {
    color: var(--error);
}

.admin-system-staging-mark {
    position: relative;
    flex-shrink: 0;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    border: 1px solid var(--border);
    background: var(--bg);
}

.admin-system-staging-checklist li[data-state="pending"] .admin-system-staging-mark {
    border-style: dashed;
}

.admin-system-staging-checklist li[data-state="ok"] .admin-system-staging-mark {
    background: var(--accent);
    border-color: var(--accent);
}

.admin-system-staging-checklist li[data-state="ok"] .admin-system-staging-mark::after {
    content: "";
    position: absolute;
    left: 4px;
    top: 1px;
    width: 3px;
    height: 7px;
    border: solid var(--bg);
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
}

.admin-system-staging-checklist li[data-state="fail"] .admin-system-staging-mark {
    border-color: var(--error);
}

.admin-system-staging-error {
    margin: 4px 0 0;
    font-size: 0.76rem;
    line-height: 1.4;
    color: var(--error);
    word-break: break-word;
}

.admin-system-staging-actions {
    display: flex;
    flex-direction: column;
    gap: 8px;
    flex-shrink: 0;
}

.admin-system-staging-actions .admin-pid-refresh-btn {
    white-space: nowrap;
}

/* Sits inline to the left of the card search box (in .admin-cards-header-row
   below) rather than spanning its own full-width row — it's a small mode
   switch, not a second nav bar. */
.admin-cards-header-row {
    margin-bottom: 16px;
    flex-shrink: 0;
}

/* Cards sub-view switch (Info / Pricing) — the shared .pill-toggle /
   .pill-toggle-btn (components.css) with a monospace uppercase label, so it
   reads as a mode switch inside Cards rather than a second top-level nav. */
.admin-cards-subnav-btn {
    font-family: var(--font-mono);
    font-size: 0.78rem;
    font-weight: 400;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    padding: 6px 16px;
}

.admin-cards-view {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
}

/* Info sub-view: switchAdminCardsView toggles admin-cards-mode-info on
   #admin-section-pricing rather than swapping in a separate panel — the
   same list + card info window Pricing uses stays mounted, just morphed to
   hide the pricing-only parts. The controls bar above the grid (TCG button,
   Regular/Discord toggle, refresh buttons) collapses via max-height/opacity
   since none of it applies outside Pricing. */
.admin-cards-mode-info .admin-pid-controls {
    opacity: 0;
    max-height: 0;
    margin-bottom: 0;
    pointer-events: none;
}

/* The list's own width comes from .admin-pricing-layout's first track below
   (it's a plain stretch-to-fill grid item, no explicit width of its own) —
   animating THAT track (via animateGridColumns, see switchAdminCardsView in
   admin.js) rather than the list item directly is what makes the card info
   window next to it (.admin-pricing-image-col, in the fixed 320px track)
   visibly slide along as a normal side effect of the grid recalculating each
   frame, instead of needing its own separate position animation. The third
   and fourth tracks keep real width too, same as Pricing's third — Info mode
   uses them for the card's raw identifiers (renderAdminInfoIdsPanel) and a
   per-set card count breakdown (renderAdminInfoSetsPanel) instead of
   Sales/Listings, not nothing. The list's Check/Product ID/Sales/Listings
   columns still swap to Info's reduced set instantly rather than animating
   live (see .admin-pid-row-info below) — doing THAT live, on every row at
   once, would force a full grid layout recalculation on every visible row
   on every frame with thousands potentially loaded, which is what made an
   earlier version of this transition glitchy; the outer track above only
   affects its 4 direct children, so animating it doesn't have that same
   cost. */
.admin-cards-mode-info .admin-pricing-layout {
    grid-template-columns: var(--admin-pid-info-list-width) 320px minmax(0, 1fr) 560px;
    /* Neutralizes the base rule's margin-right:-24px (see there) — that
       compensates for the 4th track's always-reserved gap while it's 0px
       (Pricing mode); here the track is real (560px) content and that gap
       is wanted, not a phantom to cancel out. */
    margin-right: 0;
}

/* Info mode's reduced row markup (see renderAdminPidHeader/renderAdminPidRows
   in admin.js) — just Card/Rarity/Set, no Check/Product ID/Sales/Listings
   tracks at all, rather than the Pricing row's 7-column
   grid-template-columns collapsed down to fewer visible ones. Selector is
   .admin-pid-row.admin-pid-row-info (not just .admin-pid-row-info) so its
   specificity actually beats .admin-pid-row's own grid-template-columns
   further down this file — same specificity plus later source order there
   would otherwise win. */
.admin-pid-row.admin-pid-row-info {
    grid-template-columns: 2fr 0.6fr 0.8fr;
}

.admin-pid-controls {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin-bottom: 16px;
    flex-shrink: 0;
    overflow: hidden;
    /* Just a big-enough open target for the Info<->Pricing collapse transition
       below (there's no content here to size against — see the comment on
       .admin-cards-mode-info's max-height:0 counterpart) — not tuned tightly
       to .admin-pid-controls-grid's usual (much shorter) content height. Sized
       with real headroom past that content's normal ~76px so the row wrapping
       .admin-pid-controls-cell-detail can fall into on narrower screens (see
       its own comment below) never gets clipped by this cap. */
    max-height: 240px;
    transition: opacity 0.25s ease, max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
        margin-bottom 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.admin-pid-controls-row {
    display: flex;
    align-items: center;
    gap: 16px;
}

/* Mirrors .admin-pricing-layout's exact column widths (1.4fr / fixed 320px /
   1fr / 0px, same 24px gap) so the TCG button (in the list cell) and refresh
   buttons (in the detail cell) land flush with the list/detail columns
   below, even though this is a separate row above them. */
.admin-pid-controls-grid {
    display: grid;
    /* minmax(0, …) instead of a bare fr: without it, each fr track's implicit
       minimum is its cell's min-content width, which differs between this row's
       (mostly empty) cells and .admin-pricing-layout's (list/detail content)
       cells — silently breaking the column-matching this grid exists for.
       The trailing 0px track has to be here too, even though nothing ever
       sits in it — .admin-pricing-layout gained a 4th (Info-mode Sets panel)
       track without this one following, and the resulting mismatched gap
       count (3 gaps reserved here vs. 4 there) was enough on its own to
       shift every track after the first one out of alignment. */
    grid-template-columns: minmax(0, 1.4fr) 320px minmax(0, 1fr) 0px;
    gap: 24px;
    align-items: center;
    /* The 0px 4th track still costs one real `gap` of space (CSS reserves a
       gap between every pair of tracks regardless of their size — there's no
       way to skip just this one boundary), which otherwise shows up as
       ~24px of unclaimed space past the third track's right edge that has
       no counterpart on the left. Pulling the whole grid 24px further right
       via a negative margin — rather than shrinking the fr tracks to fit —
       cancels that out so the visible content (not just the grid's own box)
       lines up flush on both sides again. Only correct while the 4th track
       is actually 0px, i.e. outside Info mode — see the override below. */
    margin-right: -24px;
}

.admin-cards-mode-info .admin-pid-controls-grid {
    /* Moot in practice (.admin-pid-controls fades out entirely in Info
       mode), but keeps this grid's own box honest if that ever changes —
       the 4th track is real content width there, not 0px, so the negative
       margin above no longer corresponds to a phantom gap to cancel out. */
    margin-right: 0;
}

.admin-pid-controls-cell-list {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    min-width: 0;
}

.admin-pid-controls-cell-image {
    min-width: 0;
}

/* The Regular/Discord pill's left edge lands on this cell's left edge, which
   — since this cell shares .admin-pricing-layout's own grid-template-columns
   (see .admin-pid-controls-grid) — is the same pixel as the detail column's
   left edge below, i.e. flush with the Sales/Listings area. At common desktop
   widths the detail column isn't wide enough to also fit all three refresh
   buttons on the same line, so this wraps rather than overflowing off-page:
   .admin-pid-refresh-group's own margin-left:auto (not justify-content here,
   which would misbehave once refresh-group is alone on its own wrapped line)
   keeps it pinned to the right whether it's sharing the first line or has
   wrapped to a second one. */
.admin-pid-controls-cell-detail {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    row-gap: 8px;
    column-gap: 12px;
    min-width: 0;
}

/* Regular/Discord pill (also reused for the System section's On/Off setting
   toggles) — the shared .pill-toggle / .pill-toggle-btn (components.css); the
   only tweak is a tighter track so it matches .admin-pid-refresh-btn's height
   exactly, since it sits directly beside it. */
.admin-pid-source-toggle {
    padding: 2px;
}

.admin-pid-search {
    flex: 1;
    max-width: 320px;
    font-family: var(--font-sans);
    font-size: 0.85rem;
    color: var(--text);
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 8px 12px;
}

.admin-pid-search:focus {
    outline: none;
    border-color: var(--border-focus);
}

.admin-pid-col-check input[type="checkbox"],
.admin-pid-curio-select-all {
    appearance: none;
    -webkit-appearance: none;
    width: 16px;
    height: 16px;
    margin: 0;
    flex-shrink: 0;
    background: transparent;
    border: 1px solid var(--border);
    border-radius: 4px;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
}

.admin-pid-col-check input[type="checkbox"]:focus-visible,
.admin-pid-curio-select-all:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 25%, transparent);
}

.admin-pid-col-check input[type="checkbox"]:checked,
.admin-pid-curio-select-all:checked {
    background: var(--accent);
    border-color: var(--accent);
}

.admin-pid-col-check input[type="checkbox"]:indeterminate,
.admin-pid-curio-select-all:indeterminate {
    background: color-mix(in srgb, var(--accent) 45%, transparent);
    border-color: var(--accent);
}

/* Sits in the header's Product ID column, positioned exactly like the
   per-row .admin-pid-curio-toggle buttons below it (see the comment on
   that rule) — outside the column, flush with its left edge plus the same
   6px gap — so the "select all" checkbox and the row toggles it controls
   line up in one column down the page. Sized to match those same toggle
   buttons (26px) rather than the smaller 16px checkboxes elsewhere in this
   table, since it's standing in for a whole column of them.
   The explicit padding:0 below isn't decorative — login.css's global
   `input { padding: 10px 14px }` rule (unscoped by type, so it reaches
   every input on the page including this checkbox) inflates a checkbox's
   box past its declared width/height despite box-sizing:border-box, since
   a checkbox is a replaced form control rather than ordinary box content;
   overriding padding back to 0 is what actually makes width/height stick. */
.admin-pid-curio-select-all {
    position: absolute;
    right: 100%;
    margin-right: 6px;
    top: 50%;
    transform: translateY(-50%);
    width: 26px;
    height: 26px;
    padding: 0;
    /* Rides the Product ID column collapse alongside the row ✨ toggles (see
       .admin-pid-hide-pid) — overrides the shared checkbox transition above. */
    transition: background 0.15s, border-color 0.15s,
        margin-right 300ms cubic-bezier(0.4, 0, 0.2, 1);
}

/* Card-select checkboxes (both the per-row ones and their "select all" in
   the header) match the Curio Foil select-all checkbox's size (26px) rather
   than the smaller 16px base size declared for them above. Same login.css
   `input { padding }` override as .admin-pid-curio-select-all above (see its
   comment) is needed here too — without it these were already rendering
   larger than their declared 16px, just not next to anything to notice it
   against. */
.admin-pid-col-check input[type="checkbox"] {
    width: 26px;
    height: 26px;
    padding: 0;
}

.admin-pid-curio-select-all:disabled {
    cursor: not-allowed;
    opacity: 0.4;
}

.admin-pid-refresh-group {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 8px;
    /* Fade + horizontal wipe on show/hide (marketplace pill / Local Database),
       via .admin-pid-collapsed below. Both this and the Link button sit at the
       trailing edge of their flex row, so collapsing max-width to 0 only eats
       invisible slack — the row's own gap needs no special handling. */
    overflow: hidden;
    max-width: 700px;
    transition: opacity 0.2s ease, max-width 0.28s cubic-bezier(0.4, 0, 0.2, 1);
}

.admin-pid-link-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 32px;
    height: 32px;
    min-width: 0;
    max-width: 32px;
    padding: 0;
    overflow: hidden;
    color: var(--text);
    background: transparent;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: border-color 0.15s, opacity 0.2s ease,
        max-width 0.28s cubic-bezier(0.4, 0, 0.2, 1);
}

.admin-pid-link-btn:hover:not(:disabled) {
    border-color: var(--border-focus);
}

.admin-pid-link-btn:disabled {
    color: var(--text-muted);
    cursor: not-allowed;
    opacity: 0.6;
}

/* Collapsed (faded + wiped to zero width) state for the Link button and the
   Refresh group — toggled by updateAdminPidLinkButton / updateAdminPidRefreshButton
   instead of .hidden so the change animates. Comes after :disabled above so its
   opacity:0 wins for a Link button that's both disabled and collapsed. */
.admin-pid-link-btn.admin-pid-collapsed,
.admin-pid-refresh-group.admin-pid-collapsed {
    max-width: 0;
    opacity: 0;
    pointer-events: none;
}

.admin-pid-link-icon {
    width: 22px;
    height: 22px;
    object-fit: contain;
    flex-shrink: 0;
}

/* Right cluster of the list controls cell: the Link button plus the
   marketplace scope pill. Sits as .admin-pid-controls-cell-list's second flex
   child, so space-between pins it to the cell's right edge — which shares
   .admin-pricing-layout's first track, i.e. flush with the Card list's right
   edge below. */
.admin-pid-list-actions {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-shrink: 0;
}

/* Marketplace scope pill (TCGPlayer / CoreTCG / Manual) — the shared
   .pill-toggle / .pill-toggle-btn (components.css), tightened to match
   .admin-pid-link-btn's height the same way .admin-pid-source-toggle is. */
.admin-pid-marketplace-toggle {
    padding: 2px;
}

.admin-pid-marketplace-toggle-btn {
    padding: 5px 12px;
    font-size: 0.8rem;
}

.admin-pid-refresh-btn {
    font-family: var(--font-sans);
    font-size: 0.82rem;
    font-weight: 500;
    color: var(--bg);
    background: var(--accent);
    border: none;
    border-radius: var(--radius-sm);
    padding: 8px 16px;
    cursor: pointer;
    white-space: nowrap;
    transition: opacity 0.15s;
}

.admin-pid-refresh-btn:hover:not(:disabled) {
    opacity: 0.85;
}

/* Selection count on "Refresh Selected" (updateAdminPidRefreshButton in
   admin.js) — a small fixed-size badge instead of growing the button's own
   inline text, so the button's width barely changes as the count changes.
   currentColor keeps it readable in both the accent (enabled) and muted
   (disabled) button states above/below without a separate override for each. */
.admin-pid-refresh-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    margin-left: 2px;
    border-radius: 999px;
    font-size: 0.72rem;
    font-weight: 600;
    line-height: 1;
    background: color-mix(in srgb, currentColor 22%, transparent);
}

.admin-pid-refresh-btn:disabled {
    background: var(--border);
    color: var(--text-muted);
    cursor: not-allowed;
}

.admin-pid-refresh-btn-secondary {
    color: var(--text);
    background: transparent;
    border: 1px solid var(--border);
}

.admin-pid-refresh-btn-secondary:hover:not(:disabled) {
    background: var(--bg-hover);
    opacity: 1;
}

.admin-pid-refresh-btn-secondary:disabled {
    background: transparent;
    border-color: var(--border);
    color: var(--text-muted);
}

.admin-pid-status-group {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
}

.admin-pid-summary {
    font-size: 0.85rem;
    color: var(--text-muted);
    flex-shrink: 0;
}

.admin-pid-progress {
    font-size: 0.82rem;
    color: var(--accent);
    flex-shrink: 0;
}

.admin-pid-progress::before {
    content: "— ";
}

/* Same grid-template-columns/gap as .admin-pid-controls-grid above (including
   the minmax(0, …) — see the comment there for why a bare fr isn't enough) —
   kept as two separate grid declarations rather than one shared element
   (there's a controls block between them), but identical track definitions
   guarantee their column edges land on the same pixels in every case. */
/* Fourth track (.admin-pricing-sets) is 0px here — it's Info-only (see
   .admin-cards-mode-info below), same idea as the third track being 0px in
   Info mode: kept as a real track (not just display:none-d) so
   animateGridColumns has a consistent 4-track shape to interpolate between
   in both directions, rather than the track count itself changing. */
.admin-pricing-layout {
    flex: 1;
    display: grid;
    grid-template-columns: minmax(0, 1.4fr) 320px minmax(0, 1fr) 0px;
    gap: 24px;
    min-height: 0;
    overflow: hidden;
    /* Cancels out the 4th track's always-reserved leading gap while it's
       0px wide (Pricing mode) — see the matching comment on
       .admin-pid-controls-grid, which this mirrors exactly. Neutralized by
       .admin-cards-mode-info .admin-pricing-layout above, where that same
       track is real (560px) content and the gap in front of it is wanted. */
    margin-right: -24px;
}

.admin-pricing-list {
    min-width: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
}

.admin-pid-table-header {
    flex-shrink: 0;
    border-bottom: 1px solid var(--border);
    /* Matches .admin-pid-row-header's own background — the header row is
       padded narrower than this container to offset the scrollbar (see
       syncAdminPidHeaderScrollbarOffset() in admin.js), and without this the
       reserved strip on the right shows the plain page background instead,
       reading as a visibly separate patch. */
    background: var(--bg-hover);
}

.admin-pid-table-scroll {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
}

.admin-pricing-image-col {
    min-width: 0;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    gap: 14px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 16px;
    /* Lets .admin-pid-detail-pid-row animate max-height to/from `max-content`
       (its collapse wipe). Inherited; scoped here rather than global :root. */
    interpolate-size: allow-keywords;
}

.admin-pid-scrape-stats {
    flex-shrink: 0;
    grid-template-columns: 1fr 1fr;
}

/* Anchors the absolutely-positioned .admin-pid-clear-last-btn (below) to this
   one badge — .drawer-stat itself (drawer.css, shared with the public
   pricing drawer) isn't given position:relative there, so it's set here
   instead, scoped to just the Last Sales/Last Listings badges. */
.admin-pid-scrape-stats .drawer-stat {
    position: relative;
}

.admin-pid-clear-last-btn {
    position: absolute;
    top: 4px;
    right: 4px;
    width: 16px;
    height: 16px;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.6rem;
    line-height: 1;
    background: transparent;
    border: none;
    cursor: pointer;
    opacity: 0.6;
    transition: opacity 0.15s;
}

.admin-pid-clear-last-btn:hover:not(:disabled) {
    opacity: 1;
}

.admin-pid-clear-last-btn:disabled {
    cursor: not-allowed;
    opacity: 0.2;
}

.admin-pricing-detail {
    min-width: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: 16px;
    overflow: hidden;
    padding-left: 4px;
}

/* Info mode's fourth panel (see renderAdminInfoSetsPanel) — a per-set card
   count, always the same content regardless of which card (if any) is
   selected, so unlike .admin-pricing-detail it's rendered once when
   adminPidData loads rather than per-selection. Its track is 0px outside
   Info mode (see .admin-cards-mode-info .admin-pricing-layout above) so it
   just needs to sit here ready to be revealed as that track grows — no fade
   of its own to worry about. */
.admin-pricing-sets {
    min-width: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 14px;
}

.admin-pricing-sets-header {
    flex-shrink: 0;
    display: flex;
    align-items: stretch;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 10px;
}

/* Title on top, the message slot (Search-Set progress bar / post-action
   status line) pinned to the bottom so it lines up with the icon button row
   opposite it — filling the header's dead space instead of adding a row of
   its own below. */
.admin-pricing-sets-header-left {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: 6px;
}

/* Height matched to .admin-pricing-sets-action-btn so this slot spans the
   icon button row exactly; centering keeps the progress bar / status text on
   that row's vertical midline rather than dropping to the header's baseline. */
.admin-pricing-sets-header-msg {
    min-width: 0;
    min-height: 24px;
    display: flex;
    flex-direction: column;
    justify-content: center;
}

/* Two right-aligned rows: "Check Featured" on top, the four icon actions
   (import / search / clear / tcgcsv link) below. */
.admin-pricing-sets-header-actions {
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 6px;
}

.admin-pricing-sets-action-row {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 6px;
}

.admin-pricing-sets-title {
    font-family: var(--font-mono);
    font-size: 0.7rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--accent);
}

/* Smaller than .admin-pid-refresh-btn's usual padding — this panel is
   narrower than where that class is normally used, and "Check Featured"
   sits on its own compact row above the icon actions. */
.admin-pricing-sets-featured-btn {
    padding: 4px 10px;
    font-size: 0.72rem;
}

/* Shares the header's left gutter with the progress bar (only one is ever
   non-empty at a time). One line, clipped to the gutter width with the full
   text on the title attr, and zero height while empty — so a status message
   never grows the fixed-height header or pushes the list down. */
.admin-pricing-sets-status {
    font-size: 0.72rem;
    color: var(--text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.admin-pricing-sets-status-error {
    color: var(--error);
}

/* "Search Set" download progress — same visual language as the Cards page's
   own set-search progress bar (.cards-set-progress-* in cards.css), just
   scoped to this panel's own compact padding/font-size. Lives in the header's
   left gutter (.admin-pricing-sets-header-msg), so no outer margin — the
   header's own margin-bottom already spaces it from the list. */
.admin-pricing-sets-progress-wrap {
    flex-shrink: 0;
}

.admin-pricing-sets-progress-label {
    font-family: var(--font-mono);
    font-size: 0.68rem;
    color: var(--text-muted);
    margin-bottom: 4px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.admin-pricing-sets-progress-track {
    width: 100%;
    height: 4px;
    background: var(--border);
    border-radius: 2px;
    overflow: hidden;
}

.admin-pricing-sets-progress-bar {
    height: 100%;
    background: var(--accent);
    border-radius: 2px;
    transition: width 0.2s ease;
}

/* Splits .admin-pricing-sets-list into Featured/Other (see
   renderAdminInfoSetsPanel) — plain section headers within the same
   scrolling list rather than two separate boxes. */
/* Shared .label .label--muted (main.css) + smaller size / tighter tracking. */
.admin-pricing-sets-group-label {
    font-size: 0.68rem;
    letter-spacing: 0.06em;
    padding: 10px 0 4px;
}

.admin-pricing-sets-group-label:first-child {
    padding-top: 0;
}

.admin-pricing-sets-group-label-featured {
    color: var(--accent);
}

.admin-pricing-sets-list {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    /* No scrollbar-gutter: stable here — the .scroll-thin bar is a classic
       (space-consuming) scrollbar, so when it's present it already takes its
       own 8px from the content box and .admin-pricing-sets-row-actions'
       margin-right keeps the count/button off it; when it's absent that width
       returns to the rows instead of sitting reserved and empty. */
    display: flex;
    flex-direction: column;
    gap: 2px;
}

/* Clickable — selects this set (see selectAdminSet in admin.js), like a card
   list row selecting a card. */
.admin-pricing-sets-row {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 5px 0;
    border-bottom: 1px solid var(--border);
    cursor: pointer;
}

.admin-pricing-sets-row:last-child {
    border-bottom: none;
}

.admin-pricing-sets-row:hover {
    background: var(--bg-hover);
}

.admin-pricing-sets-row-active {
    background: color-mix(in srgb, var(--accent) 12%, transparent);
}

.admin-pricing-sets-row-active:hover {
    background: color-mix(in srgb, var(--accent) 16%, transparent);
}

/* flex:1 is what pushes .admin-pricing-sets-row-actions to the row's right
   edge — plain space-between on the row would work just as well with these
   two children, but flex:1 also lets this ellipsis correctly against the
   row's actual available width rather than just its own content width. */
.admin-pricing-sets-name {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 0.8rem;
    color: var(--text);
}

.admin-pricing-sets-prefix {
    font-family: var(--font-mono);
    font-size: 0.72rem;
    color: var(--text-muted);
}

/* margin-right nudges the whole group off the list's right edge — and, when
   the list is scrolling, off its (space-consuming) scrollbar too. */
.admin-pricing-sets-row-actions {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    gap: 8px;
    margin-right: 6px;
}

/* Admin-entered tcgcsv.com Group ID (see saveAdminSetGroupId in admin.js) —
   sits ahead of the count/search button in the same actions group, small and
   plain since this panel's rows are compact and one sits on every row. */
.admin-pricing-sets-groupid {
    width: 72px;
    flex-shrink: 0;
    font-family: var(--font-mono);
    font-size: 0.72rem;
    color: var(--text);
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 3px 6px;
}

.admin-pricing-sets-groupid:focus {
    outline: none;
    border-color: var(--border-focus);
}

.admin-pricing-sets-groupid:disabled {
    opacity: 0.6;
}

.admin-pricing-sets-groupid-error {
    border-color: var(--error);
}

/* Small non-interactive "already searched" badge (see adminSetSearches in
   admin.js) — replaced the old clickable per-row Search Set button now that
   searching lives in the header's bulk actions instead; this just reports
   status. */
.admin-pricing-sets-searched {
    flex-shrink: 0;
    width: 14px;
    text-align: center;
    font-family: var(--font-mono);
    font-size: 0.72rem;
    color: var(--text-muted);
}

.admin-pricing-sets-searched-done {
    color: var(--accent);
}

.admin-pricing-sets-count {
    flex-shrink: 0;
    /* Fixed width (rather than sizing to its own digits) so the Group ID
       input before it — same fixed width on every row — lands at the same
       x-position down the whole list instead of drifting with how many
       digits each row's count happens to have. */
    min-width: 28px;
    text-align: right;
    font-family: var(--font-mono);
    font-size: 0.8rem;
    color: var(--text);
}

/* Header action buttons (Search/Clear/Sync — see searchSelectedAdminSet/
   clearSelectedAdminSetIds/syncSelectedAdminSetTcgcsv in admin.js), acting
   on whichever one set is currently selected (see selectAdminSet) rather
   than living on every row. Emoji glyphs sized up a bit since they read
   smaller than text at that size. */
/* Shared by both <button> action buttons and the tcgcsv.com <a> link below —
   text-decoration/color reset since browsers underline and blue-tint anchor
   text by default, which an emoji glyph would otherwise still pick up. */
.admin-pricing-sets-action-btn {
    flex-shrink: 0;
    min-width: 26px;
    height: 24px;
    padding: 0 5px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.85rem;
    line-height: 1;
    background: transparent;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: inherit;
    text-decoration: none;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
}

.admin-pricing-sets-action-btn:hover:not(:disabled) {
    background: var(--bg-hover);
    border-color: var(--border-focus);
}

.admin-pricing-sets-action-btn:disabled {
    cursor: not-allowed;
    opacity: 0.35;
}

/* Clear button reads as destructive even while otherwise styled like its
   Search/Sync siblings — a red-tinted border is enough at this size without
   needing a filled/high-contrast treatment. */
.admin-pricing-sets-action-btn-danger {
    border-color: color-mix(in srgb, var(--error) 45%, var(--border));
}

.admin-pricing-sets-action-btn-danger:hover:not(:disabled) {
    background: color-mix(in srgb, var(--error) 12%, transparent);
    border-color: var(--error);
}

/* The panels above use the shared .fade-out / .fade-in (main.css). */

/* The .admin-pricing-detail panel's own fade — used when its Sales/Listings
   content is swapped in place without the image column also changing: the
   Curio Foil toggle (toggleAdminPidCurioView) and the marketplace scope pill
   (switchAdminPidMarketplace). Same look as .fade-out/.fade-in above, just
   slower. Switching cards fades both the image column AND this panel together,
   which reads as a bigger, slower motion; fading only this one (smaller) panel
   at .fade-out/.fade-in's exact timing looked noticeably quicker despite being
   the same kind of data swap — this gives it its own pace, leaving
   card-switching's untouched. */
.admin-pricing-detail.curio-fade-out {
    opacity: 0;
    transform: translateY(8px);
    transition: opacity 0.3s ease, transform 0.3s ease;
}

.admin-pricing-detail.curio-fade-in {
    opacity: 1;
    transform: translateY(0);
    transition: opacity 0.35s ease, transform 0.35s ease;
}

/* Used instead of curio-fade-out/in above when the toggle came from the
   header's "select all" checkbox (see toggleAllAdminPidCurioView) rather
   than a single row's button — slower still, since a bulk action reads as
   rushed at the single-row pace even though it's animating the exact same
   one panel (whichever card happens to be open). */
.admin-pricing-detail.curio-fade-out-bulk {
    opacity: 0;
    transform: translateY(8px);
    transition: opacity 0.45s ease, transform 0.45s ease;
}

.admin-pricing-detail.curio-fade-in-bulk {
    opacity: 1;
    transform: translateY(0);
    transition: opacity 0.5s ease, transform 0.5s ease;
}

/* The admin table/list scroll containers use the shared .scroll-thin (main.css). */

.admin-pid-table {
    display: flex;
    flex-direction: column;
}

.admin-pid-row {
    display: grid;
    grid-template-columns: 28px 2fr 0.6fr 0.8fr 130px 100px 100px;
    align-items: center;
    gap: 16px;
    padding: 10px 16px;
    border-bottom: 1px solid var(--border);
}

.admin-pid-row:not(.admin-pid-row-header) {
    cursor: pointer;
}

.admin-pid-col-check {
    display: flex;
    align-items: center;
    justify-content: center;
}

.admin-pid-row:last-child {
    border-bottom: none;
}

.admin-pid-row-header {
    font-family: var(--font-mono);
    font-size: 0.7rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
    background: var(--bg-hover);
}

/* Each column's own cell class (.admin-pid-col-rarity etc., defined further
   down for the DATA rows) sets its own font-size/family/color and, being an
   equal-specificity class selector that comes later in the file, would
   otherwise win the cascade over .admin-pid-row-header above and make each
   header label render in its data row's font instead of the header's own —
   this pins every header cell but Card back to one shared look. */
.admin-pid-row-header .admin-pid-col-rarity,
.admin-pid-row-header .admin-pid-col-set,
.admin-pid-row-header .admin-pid-col-status,
.admin-pid-row-header .admin-pid-col-sales,
.admin-pid-row-header .admin-pid-col-listings {
    font-family: inherit;
    font-size: inherit;
    letter-spacing: inherit;
    text-transform: inherit;
    color: inherit;
}

/* "Card" is the one header styled to match its own column's data instead of
   the shared uppercase header look above — same font/size/case as the card
   names underneath it. */
.admin-pid-row-header .admin-pid-col-name {
    font-family: var(--font-sans);
    font-size: 0.85rem;
    letter-spacing: normal;
    text-transform: none;
    color: var(--text);
}

/* The Rarity and Set header cells host filter dropdowns; the data rows'
   overflow:hidden truncation below would otherwise clip the dropdown menus. */
.admin-pid-row-header .admin-pid-col-rarity,
.admin-pid-row-header .admin-pid-col-set,
.admin-pid-row-header .admin-pid-col-status,
.admin-pid-row-header .admin-pid-col-sales,
.admin-pid-row-header .admin-pid-col-listings {
    overflow: visible;
}

/* Header labels (Product ID / Sales / Listings) sit left-aligned above their
   column. Product ID also carries the curio select-all checkbox, itself
   positioned outside this cell to its left (see its own comment further up). */
.admin-pid-row-header .admin-pid-col-status,
.admin-pid-row-header .admin-pid-col-sales,
.admin-pid-row-header .admin-pid-col-listings {
    display: flex;
    align-items: center;
    gap: 6px;
}

/* Card/Sales/Listings header labels double as sort controls (setAdminPidSort
   in admin.js) — the active column's own arrow uses --accent so it reads at
   a glance which of the three (if any) the list is currently sorted by. */
.admin-pid-sort-header {
    cursor: pointer;
    user-select: none;
}

.admin-pid-sort-header:hover {
    color: var(--text);
}

.admin-pid-sort-arrow {
    color: var(--accent);
}

.admin-pid-row:not(.admin-pid-row-header):hover {
    background: var(--bg-hover);
}

.admin-pid-row-active {
    background: color-mix(in srgb, var(--accent) 12%, transparent);
}

.admin-pid-row-active:hover {
    background: color-mix(in srgb, var(--accent) 16%, transparent);
}

.admin-pid-col-name,
.admin-pid-col-rarity,
.admin-pid-col-set {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.admin-pid-col-name {
    font-size: 0.85rem;
    color: var(--text);
}

.admin-pid-col-rarity {
    font-family: var(--font-mono);
    font-size: 0.78rem;
    color: var(--text-muted);
}

.admin-pid-col-set {
    font-family: var(--font-mono);
    font-size: 0.78rem;
    color: var(--text-muted);
}

/* Pulls each of these in from the row's usual 16px gap so the table reads
   tighter — headers and their column's data share the same grid track, so
   both move together and stay lined up on the left. */
.admin-pid-col-rarity,
.admin-pid-col-set,
.admin-pid-col-status,
.admin-pid-col-sales,
.admin-pid-col-listings {
    margin-left: -6px;
}

.admin-pid-input {
    width: 100%;
    font-family: var(--font-mono);
    font-size: 0.8rem;
    color: var(--text);
    background: var(--bg-card);
    border: 1px solid var(--error);
    border-radius: var(--radius-sm);
    /* Left padding is trimmed well below the other sides — a full 10px would
       leave the typed digits visibly indented past the "Product ID" header
       above them, since the header label itself has no such padding. */
    padding: 5px 10px 5px 4px;
    transition: border-color 0.15s;
}

.admin-pid-input::placeholder {
    color: var(--error);
    opacity: 0.8;
}

.admin-pid-input:focus {
    outline: none;
    border-color: var(--border-focus);
}

.admin-pid-input-filled {
    border-color: var(--series-sales);
}

/* Cards deliberately marked "~" (confirmed to have no TCGPlayer listings at
   all) get a neutral/muted look — distinct from both the red "missing, not
   yet looked up" default and the green "filled with a real ID" state. */
.admin-pid-input-no-listings {
    border-color: var(--border);
    color: var(--text-muted);
    font-style: italic;
}

.admin-pid-input-error {
    border-color: var(--error);
    background: color-mix(in srgb, var(--error) 12%, var(--bg-card));
}

/* A product ID currently assigned to more than one card (see
   computeAdminPidDuplicateProductIds/applyAdminPidDuplicateHighlights in
   admin.js) — unlike the transient .admin-pid-input-error flash above, this
   stays applied until the collision is actually resolved. The extra
   .admin-pid-input in the selector outranks .admin-pid-input-filled's own
   green border so a duplicate reads as red even though it's also "filled". */
.admin-pid-input.admin-pid-input-duplicate {
    border-color: var(--error);
    background: color-mix(in srgb, var(--error) 16%, var(--bg-card));
}

/* Anchors the absolutely-positioned .admin-pid-curio-toggle (below) so its
   right edge lands exactly on this column's own left edge — entirely outside
   the Product ID column, not sharing its width. Putting the toggle in-flow
   inside the column (tried first) meant either the input got narrower on
   curio rows specifically, or — widening the column to compensate — every
   OTHER row's input got wider than curio rows' input, since all rows share
   one grid track. Keeping the column at its original width and positioning
   the toggle outside it keeps every row's input pixel-identical regardless
   of whether that row has a toggle. */
.admin-pid-col-status {
    position: relative;
}

.admin-pid-pid-wrap {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 6px;
}

/* Only rendered for editions with a Curio Foil variant (see e.curio). Instead
   of sitting in-flow it's pinned outside the Product ID column — right edge
   flush with the column's own left edge (see the comment on
   .admin-pid-col-status) — so it never takes width away from the input. */
.admin-pid-curio-toggle {
    position: absolute;
    right: 100%;
    /* Same 6px gap .admin-pid-pid-wrap uses for its own children, mirrored
       here so the toggle reads as evenly spaced from the column edge instead
       of sitting flush against it. */
    margin-right: 6px;
    top: 50%;
    transform: translateY(-50%);
    width: 26px;
    height: 26px;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.75rem;
    line-height: 1;
    color: var(--text-muted);
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    cursor: pointer;
    /* margin-right eases with the Product ID column collapse (see
       .admin-pid-hide-pid) — same 300ms / curve as --admin-pid-status-col. */
    transition: border-color 0.15s, color 0.15s, background 0.15s,
        margin-right 300ms cubic-bezier(0.4, 0, 0.2, 1);
}

.admin-pid-curio-toggle:hover {
    border-color: var(--accent);
    color: var(--accent);
}

.admin-pid-curio-toggle.active {
    border-color: var(--accent);
    background: color-mix(in srgb, var(--accent) 16%, var(--bg-card));
    color: var(--accent);
}

.admin-pid-pid-wrap .admin-pid-input {
    flex: 1;
    min-width: 0;
}

/* Detail-panel-only (see adminPidFoilSwapToggleHtml) — unlike
   .admin-pid-curio-toggle this never appears in the row list, so it sits
   in-flow as a normal flex sibling of the Product ID field rather than
   needing the absolute-positioning trick that keeps that one from taking
   width away from every row's input. */
.admin-pid-swap-toggle {
    flex-shrink: 0;
    width: 26px;
    height: 26px;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.75rem;
    line-height: 1;
    color: var(--text-muted);
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: border-color 0.15s, color 0.15s, background 0.15s;
}

.admin-pid-swap-toggle:hover {
    border-color: var(--accent);
    color: var(--accent);
}

.admin-pid-swap-toggle.active {
    border-color: var(--accent);
    background: color-mix(in srgb, var(--accent) 16%, var(--bg-card));
    color: var(--accent);
}

.admin-pid-swap-toggle:disabled {
    opacity: 0.6;
    cursor: default;
}

/* ── Non-TCGPlayer marketplace: collapsed Product ID column ──────────────
   Only TCGPlayer has scraper automation, so nothing stores product IDs for
   CoreTCG / Manual — the list's Product ID column shrinks to just its
   curio-foil ✨ toggles (row toggles + the header select-all), and the
   input and header label both hide.
   Driven by .admin-pid-hide-pid on #admin-section-pricing (see
   updateAdminPidProductIdVisibility in admin.js) — which also eases the
   grid-template-columns change here via animateGridColumnsGroup (animation.js)
   on the header row plus the on-screen data rows; off-screen rows just take
   the new rule directly.

   Every selector is guarded with :not(.admin-cards-mode-info): Info mode
   swaps in its own reduced 3-column row template (.admin-pid-row-info) and
   fades the marketplace pill out entirely, so this must never touch it.
   The compound-class specificity ((0,3,x)) also clears
   .admin-pid-row.admin-pid-row-info's grid-template-columns for free. */
.admin-pid-hide-pid:not(.admin-cards-mode-info) .admin-pid-row {
    grid-template-columns: 28px 2fr 0.6fr 0.8fr 34px 100px 100px;
}

.admin-pid-hide-pid:not(.admin-cards-mode-info) .admin-pid-col-status .admin-pid-input,
.admin-pid-hide-pid:not(.admin-cards-mode-info) .admin-pid-row-header .admin-pid-col-status-label {
    display: none;
}

/* The ✨ toggles normally hang off the Product ID column's left edge (see
   .admin-pid-curio-toggle / .admin-pid-curio-select-all). With the column
   collapsed there's nothing to hang off of, so slide them in over the track
   itself — a negative margin-right rather than a position swap, so it stays
   anchored to the (moving, shrinking) column edge and eases across with it
   instead of jumping. */
.admin-pid-hide-pid:not(.admin-cards-mode-info) .admin-pid-curio-toggle,
.admin-pid-hide-pid:not(.admin-cards-mode-info) .admin-pid-curio-select-all {
    margin-right: -28px;
}

.admin-pid-empty {
    padding: 20px 16px;
    text-align: center;
    font-size: 0.85rem;
    color: var(--text-muted);
}

.admin-user-controls {
    display: flex;
    align-items: center;
    gap: 16px;
    margin-bottom: 16px;
    flex-shrink: 0;
}

.admin-user-role-actions {
    display: flex;
    align-items: center;
    gap: 8px;
}

.admin-user-summary {
    font-size: 0.85rem;
    color: var(--text-muted);
}

.admin-user-layout {
    flex: 1;
    display: flex;
    gap: 24px;
    min-height: 0;
    overflow: hidden;
}

/* Kept narrow — just wide enough for a username, an Omni ID and a short role
   label — so the profile/inventory/decks panels (which repeat the same info in
   full) still get the bulk of the space. The +130px covers the three text
   columns beyond the percentage baseline. */
.admin-user-list {
    flex: 0 0 calc(16% + 130px);
    min-width: 260px;
    max-width: 380px;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
}

/* Mirrors .admin-pid-table-header — lives outside the scrollable body so its
   own background covers the strip the header reserves via JS
   (syncAdminUserHeaderScrollbarOffset()) to stay aligned with the scrollbar
   that appears in the body once there are enough rows. */
.admin-user-table-header {
    flex-shrink: 0;
    border-bottom: 1px solid var(--border);
    background: var(--bg-hover);
}

.admin-user-table-scroll {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    /* No scrollbar-gutter here (unlike .admin-user-tile-grid) — the header's
       padding is instead synced reactively to whatever the scrollbar
       actually measures via syncAdminUserHeaderScrollbarOffset(), which is
       0 until a real scrollbar appears. That keeps the list at full width
       whenever scrolling isn't needed, only reserving space once it is. */
}

.admin-user-table {
    display: flex;
    flex-direction: column;
}

.admin-user-row {
    display: grid;
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr) 92px 20px;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
    border-bottom: 1px solid var(--border);
}

.admin-user-row:not(.admin-user-row-header) {
    cursor: pointer;
}

.admin-user-row:not(.admin-user-row-header):hover {
    background: var(--bg-hover);
}

.admin-user-row.admin-user-row-active {
    background: var(--bg-hover);
}

.admin-user-row:last-child {
    border-bottom: none;
}

.admin-user-row-header {
    font-family: var(--font-mono);
    font-size: 0.7rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
    background: var(--bg-hover);
    border-bottom: 1px solid var(--border);
}

/* Narrow profile column — pinned to the same width as the pricing tab's
   card-image column (.admin-pricing-image-col) so the two admin tabs read
   as one consistent master-detail layout. */
.admin-user-profile-col {
    flex: 0 0 320px;
    min-width: 0;
    overflow: hidden auto;
    display: flex;
    flex-direction: column;
    gap: 14px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 16px;
}

.admin-user-profile-name {
    font-size: 1.1rem;
    font-weight: 500;
    color: var(--text);
}

.admin-user-profile-role {
    font-family: var(--font-mono);
    font-size: 0.72rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--accent);
}

/* Omnidex ID — a small number trailing the username in the profile header,
   floored so the digits sit level with the bottom of the username. */
.admin-user-omnidex {
    font-family: var(--font-mono);
    font-size: 0.72rem;
    font-weight: 400;
    letter-spacing: 0.02em;
    color: var(--text-muted);
    vertical-align: text-bottom;
}

/* Member-since value under its label — normal case, not the label caps. */
.admin-user-since-date {
    font-family: var(--font-mono);
    font-size: 0.85rem;
    color: var(--text);
    letter-spacing: 0.02em;
}

/* Bins / Cards / Decks badges pinned to the bottom of the profile panel. */
.admin-user-stats {
    margin-top: auto;
}

/* Bio block below the header. */
.admin-user-meta {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding-top: 12px;
    border-top: 1px solid var(--border);
}

.admin-user-meta-bio {
    margin: 0;
    font-size: 0.85rem;
    line-height: 1.5;
    color: var(--text);
    white-space: pre-wrap;
    overflow-wrap: break-word;
}

.admin-user-meta-empty {
    color: var(--text-muted);
    font-style: italic;
}

/* Admin notes textarea in the user profile panel — fixed size, not resizable. */
.admin-user-note-input {
    width: 100%;
    height: 84px;
    resize: none;
    font-family: var(--font-sans);
    font-size: 0.85rem;
    line-height: 1.5;
    color: var(--text);
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 8px 10px;
}

.admin-user-note-input:focus {
    outline: none;
    border-color: var(--border-focus);
}

.admin-user-note-actions {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 10px;
}

.admin-user-note-hint {
    margin-right: auto;
    font-family: var(--font-mono);
    font-size: 0.7rem;
    color: var(--text-muted);
}

.admin-user-note-hint-error {
    color: var(--error);
}

/* ── Per-row action menu (Reset Omni / Reset Password / Delete) ── */
.admin-user-row-menu-btn {
    width: 20px;
    height: 20px;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    color: var(--text-muted);
    cursor: pointer;
    transition: color 0.15s;
}

.admin-user-row-menu-btn:hover,
.admin-user-row-menu-btn.open {
    color: var(--text);
}

/* Left-facing ‹ at rest, rotating down to a v when the menu is open (overrides
   the shared .dropdown-arrow rotation, which points down at rest). */
.admin-user-row-menu-btn .dropdown-arrow {
    transform: rotate(0deg);
    transition: transform 0.2s ease;
}

.admin-user-row-menu-btn.open .dropdown-arrow {
    transform: rotate(-90deg);
}

/* The list clips its overflow, so the menu is position:fixed and JS-placed.
   Skip the shared .menu reveal animation — it fights the fixed positioning. */
.admin-user-row-menu {
    position: fixed;
    top: auto;
    min-width: 150px;
    display: flex;
    flex-direction: column;
    padding: 4px;
    z-index: 400;
    animation: none;
    opacity: 1;
    transform: none;
}

.admin-user-row-menu-item {
    display: block;
    width: 100%;
    text-align: left;
    font-family: var(--font-mono);
    font-size: 0.75rem;
    letter-spacing: 0.03em;
    color: var(--text-muted);
    background: none;
    border: none;
    border-radius: calc(var(--radius) - 2px);
    padding: 8px 10px;
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
}

.admin-user-row-menu-item:hover {
    background: var(--bg-hover);
    color: var(--text);
}

.admin-user-row-menu-item-danger:hover {
    background: color-mix(in srgb, var(--error) 14%, transparent);
    color: var(--error);
}

.admin-user-detail {
    flex: 1;
    min-width: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: 16px;
    overflow: hidden;
    padding-left: 4px;
}

/* Inventory/decks tile grids inside the admin user-detail sections — reuses
   the same .inv-bin-tile / .dga-deck-tile look as the actual Inventory and
   My Decks pages so the admin overview reads as the same object, just at a
   glance. .admin-user-tile strips the click affordance those tiles normally
   carry, since there's no per-bin/per-deck admin drill-down (yet) to link to. */
/* CSS Grid, same as .inv-bins-grid / .dga-deck-grid — minmax(280px, 1fr)
   columns stretch to fill the row's full width, same as the real pages, and
   because grid keeps one shared column structure across every row (unlike
   flex-wrap, where each wrapped line is sized independently), a half-empty
   last row just leaves blank cells instead of one tile ballooning to fill
   the whole line the way it did under flexbox.
   The row height is a fixed, literal 150px rather than "auto" — this
   browser measures "auto" wrong for this specific tile (a column flexbox
   with a flex:1 child, nested inside overflow:auto ancestors): it was
   using only the tile's min-height regardless of its real content,
   collapsing rows short and bleeding tiles into each other. A definite
   value needs no measurement at all, so that bug doesn't apply. */
.admin-user-tile-grid {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    /* No scrollbar-gutter here — same "only when necessary" choice as
       .admin-user-table-scroll. The tradeoff is that crossing the
       threshold into needing a scrollbar nudges the available width by a
       few px, which can reflow how many 280px tiles fit per row; that's
       accepted in exchange for tiles using the full row width the rest of
       the time instead of always leaving a reserved sliver unused. */
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    grid-auto-rows: 150px;
    gap: 14px;
    align-content: start;
}

/* overflow:hidden is a safety net for a name/description too long to fit
   the fixed 150px row height. */
.inv-bin-tile.admin-user-tile,
.dga-deck-tile.admin-user-tile {
    cursor: default;
    overflow: hidden;
}

.inv-bin-tile.admin-user-tile:hover,
.dga-deck-tile.admin-user-tile:hover {
    transform: none;
    border-color: var(--border);
    background: var(--bg-card);
}

.inv-bin-tile.admin-user-tile.default-bin:hover {
    border-color: rgba(200, 169, 110, 0.3);
}

.admin-user-col-name {
    font-size: 0.85rem;
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.admin-user-col-role {
    font-family: var(--font-mono);
    font-size: 0.78rem;
    color: var(--text-muted);
    text-transform: capitalize;
    text-align: left;
}

.admin-user-col-omnidex {
    font-family: var(--font-mono);
    font-size: 0.78rem;
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Dimmer dash for a user who hasn't set an Omni ID yet. */
.admin-user-col-omnidex.is-empty {
    opacity: 0.5;
}

.admin-user-col-delete {
    display: flex;
    align-items: center;
    justify-content: center;
}

.admin-pid-col-sales,
.admin-pid-col-listings {
    font-family: var(--font-mono);
    font-size: 0.78rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.admin-pid-updated-idle {
    color: var(--text-muted);
}

.admin-pid-updated-running {
    color: var(--accent);
}

.admin-pid-updated-done {
    color: var(--series-sales);
}

.admin-pid-updated-error {
    color: var(--error);
}

.admin-pid-detail-empty,
.admin-pid-detail-empty-small,
.admin-pid-detail-loading {
    font-size: 0.85rem;
    color: var(--text-muted);
    padding: 12px 0;
}

.admin-pid-detail-image-wrap {
    position: relative;
    flex: 1;
    min-height: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    border-radius: 18px;
    transition: transform 0.2s;
}

.admin-pid-detail-image-wrap:hover {
    transform: scale(1.03);
}

.admin-pid-detail-image-wrap .tile-img-spinner {
    border: none;
}

.admin-pid-detail-image {
    max-width: 100%;
    max-height: 100%;
    width: auto;
    height: auto;
    display: block;
    border-radius: 18px;
    cursor: pointer;
    opacity: 0;
    transition: opacity 0.2s ease;
}

.admin-pid-detail-image.tile-img-loaded {
    opacity: 1;
}

.admin-pid-detail-header {
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.admin-pid-detail-name {
    font-size: 1.1rem;
    font-weight: 500;
    color: var(--text);
}

.admin-pid-detail-pid-row {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    gap: 12px;
    /* Wipes closed on non-TCGPlayer marketplaces (Product ID is TCGPlayer-only)
       — see .admin-pid-detail-pid-row-collapsed, toggled by
       syncAdminPricingImageColForMarketplace. interpolate-size (inherited from
       .admin-pricing-image-col) lets the grid-height keyword animate; the
       negative margin cancels the parent's 14px flex gap so a collapsed row
       leaves no dead space. */
    overflow: hidden;
    max-height: max-content;
    transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
        opacity 0.2s ease, margin-top 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.admin-pid-detail-pid-row-collapsed {
    max-height: 0;
    opacity: 0;
    margin-top: -14px;
    pointer-events: none;
}

/* Shared .label .label--muted (main.css) + tighter tracking. */
.admin-pid-detail-label {
    letter-spacing: 0.06em;
    white-space: nowrap;
}

/* Info mode's third panel (renderAdminInfoIdsPanel) — Card ID/Edition
   ID/Foil IDs as a tree (Card → Edition → each Foil), reflecting how they
   actually nest in the data, rather than a flat label/value list. */
.admin-pid-id-tree-list {
    list-style: none;
    margin: 0;
    padding: 0;
    flex: 1;
    min-height: 0;
    overflow-y: auto;
}

.admin-pid-id-tree-list .admin-pid-id-tree-list {
    flex: none;
    overflow: visible;
    margin-left: 8px;
}

/* Each node's own connector lines are drawn via ::before (the vertical line
   down to the NEXT sibling, so a node with siblings below it stays
   connected) and ::after (the horizontal branch into this node's own row) —
   both positioned off the node's own box, not the row inside it, so they
   stay put regardless of how tall the row's content wraps to. */
.admin-pid-id-tree-node {
    position: relative;
    padding: 6px 0 6px 20px;
    /* Where THIS node's own branch meets its row, measured from the node's
       own top edge — fixed rather than a 50% split of the node's full height
       so every node's branch lands at the same consistent spot (roughly its
       own row's vertical center) regardless of how much is nested below it.
       A parent node (Edition, with its meta block and a whole foils list
       under it) is far taller than a leaf (a bare foil row) — 50% would put
       the parent's branch deep into its children instead of at its own
       label, which is what made Card → Edition → Foils read as inconsistent
       branching depending on which level you were looking at. */
    --admin-pid-id-tree-branch-y: 16px;
}

.admin-pid-id-tree-node::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    border-left: 1px solid var(--border);
}

/* The last child has nothing below it to connect to — its vertical line
   stops at its own branch instead of trailing past it. */
.admin-pid-id-tree-node:last-child::before {
    bottom: calc(100% - var(--admin-pid-id-tree-branch-y));
}

.admin-pid-id-tree-node::after {
    content: '';
    position: absolute;
    left: 0;
    top: var(--admin-pid-id-tree-branch-y);
    width: 12px;
    border-top: 1px solid var(--border);
}

/* The root (Card) has no parent of its own to branch off of. */
.admin-pid-id-tree-node-root {
    padding-left: 0;
}

.admin-pid-id-tree-node-root::before,
.admin-pid-id-tree-node-root::after {
    content: none;
}

.admin-pid-id-tree-row {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 8px;
}

/* Rarity/dates/illustrator on the Edition node (adminInfoTreeMetaChip in
   admin.js) — a sibling block under that node's own row, not a tree branch,
   so no connector lines of its own; relies on .admin-pid-id-tree-node's own
   padding-left for indentation instead of a separate margin. Population uses
   the same chip inline within each foil leaf's own row instead. */
.admin-pid-id-tree-meta {
    display: flex;
    flex-wrap: wrap;
    row-gap: 2px;
    column-gap: 12px;
    margin-top: 2px;
}

.admin-pid-id-tree-meta-item {
    font-size: 0.72rem;
    color: var(--text-muted);
    white-space: nowrap;
}

.admin-pid-id-tree-meta-label {
    font-family: var(--font-mono);
    font-size: 0.66rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0.75;
}

/* Sits below .admin-pid-id-tree-list, pinned to the bottom of the
   Identifiers panel — .admin-pid-detail-section is a flex column and the
   tree list above this is the only sibling with flex:1 eating leftover
   space, so this footer naturally lands at the panel's bottom edge without
   an explicit margin-top:auto. Last Synced stat on the left, styled like the
   card info window's own Released/Last Updated stats (see .drawer-stat in
   drawer.css — reused directly here rather than a separate admin-only style)
   so the two panels read consistently; Refresh Card on the right. */
.admin-pid-id-tree-footer {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 10px;
    margin-top: 10px;
    padding-top: 10px;
    border-top: 1px solid var(--border);
}

/* .drawer-stat is normally one of several boxes in a .drawer-stats grid
   (see admin-pid-scrape-stats) — used standalone here, so it just sizes to
   its own content as a plain flex child of the footer instead. */
.admin-pid-id-tree-synced-stat {
    flex-shrink: 0;
}

.admin-pid-id-tree-refresh-btn {
    flex-shrink: 0;
    padding: 4px 10px;
    font-size: 0.72rem;
}

/* Shared by every tree row's own ID — user-select:all so a single click
   selects the whole UUID, since it's not editable here (see .admin-pid-input
   elsewhere for the one ID field that is: Product ID, in Pricing mode's own
   version of this panel). */
.admin-pid-detail-id-value {
    font-family: var(--font-mono);
    font-size: 0.8rem;
    color: var(--text);
    word-break: break-all;
    user-select: all;
}

.admin-pid-detail-section {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 14px;
    overflow: hidden;
}

.admin-pid-detail-section-title {
    display: block;
    font-family: var(--font-mono);
    font-size: 0.7rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--accent);
}

.admin-pid-detail-section-header {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 8px;
}

.admin-pid-section-actions {
    display: flex;
    align-items: center;
    gap: 6px;
}

.admin-pid-add-entry-wrap {
    position: relative;
}

.admin-pid-add-entry-toggle {
    width: 20px;
    height: 20px;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: var(--font-sans);
    font-size: 1rem;
    font-weight: 500;
    line-height: 1;
    color: var(--text-muted);
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: border-color 0.2s, color 0.2s;
}

.admin-pid-add-entry-toggle:hover,
.admin-pid-add-entry-toggle.open {
    border-color: var(--accent);
    color: var(--accent);
}

/* Import (↓) / export (↑) arrow glyphs — monochrome, so they pick up the
   hover/open accent `color` like the "+". Bumped up and bolder since a lone
   arrow reads narrower than the "+" at the same size. */
.admin-pid-add-entry-toggle-io {
    font-size: 1.15rem;
    font-weight: 700;
}

.admin-pid-add-entry-menu {
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    left: auto;
    width: 240px;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-menu);
    z-index: 300;
    padding: 12px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    animation: revealDown 0.2s ease forwards;
}

/* Import (↓) popover — TCGPlayer-paste / GAL-JSON textarea. */
.admin-pid-import-menu {
    width: 340px;
    z-index: 1000;
}

/* Sales import action row: [Import] [paste/GAL pill]. The status wraps onto its
   own line below once it has a message; while empty it's removed entirely so it
   adds no trailing gap under the button. */
.admin-pid-import-actions {
    gap: 8px;
    flex-wrap: wrap;
}
.admin-pid-import-actions .admin-pid-add-entry-status {
    flex: 1 1 100%;
}
.admin-pid-import-actions .admin-pid-add-entry-status:empty {
    display: none;
}

/* Paste / GAL mode switch — a compact segmented control (shared .pill-toggle /
   .pill-toggle-btn, components.css) sitting next to the Import button. */
.admin-pid-import-mode-toggle {
    display: inline-flex;
    padding: 2px;
    flex-shrink: 0;
}
.admin-pid-import-mode-toggle .pill-toggle-btn {
    padding: 3px 9px;
    font-size: 0.7rem;
}

/* GAL export popover — same card, a touch wider for the two action buttons
   (Copy / Download). Its textarea reads rather than accepts input, and is
   shorter (exports are usually a handful of entries; it scrolls if longer) to
   keep the popover from clipping the viewport. */
.admin-pid-export-menu {
    width: 340px;
    z-index: 1000;
}

.admin-pid-export-menu textarea {
    height: 200px;
    cursor: text;
    color: var(--text-muted);
}

/* Flips a section-action popover to open above its trigger instead of below —
   toggled by renderAdminPricingDetail when opening downward would clip the
   viewport bottom. */
.admin-pid-add-entry-menu.admin-pid-menu-flip-up {
    top: auto;
    bottom: calc(100% + 6px);
    animation-name: revealUp;
}

/* Temporarily lifts the panel's own scroll-clipping while a section-action
   popup (import / export / add-entry) is open, so it isn't cut off — restored
   the instant it closes. Kept as a plain CSS override (position/top/right/left still come
   from the shared .admin-pid-add-entry-menu rule) rather than JS-computed
   positioning, since this page's global `zoom` scale doesn't compose
   correctly with manually-set position values. .admin-pricing-layout is the
   third ancestor in the clipping chain (list/image/detail columns, its own
   overflow:hidden exists so the list/detail columns can scroll internally
   instead of growing the page) — without lifting it too, a popup nested
   deep enough (e.g. the marketplace dropdown inside the add-entry popup)
   still gets clipped even though the two closer ancestors are already lifted. */
.admin-pricing-layout.admin-pid-popover-open,
.admin-pricing-detail.admin-pid-popover-open,
.admin-pid-detail-section.admin-pid-popover-open {
    overflow: visible;
}

.admin-pid-import-hint {
    font-family: var(--font-sans);
    font-size: 0.72rem;
    line-height: 1.5;
    color: var(--text-muted);
}

.admin-pid-import-textarea {
    width: 100%;
    height: 200px;
    resize: none;
    font-family: var(--font-mono);
    font-size: 0.75rem;
    color: var(--text);
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 8px 10px;
    overflow-y: auto;
}

/* Scrollbar hidden via the shared .scroll-none (main.css). */

.admin-pid-import-textarea:focus {
    outline: none;
    border-color: var(--border-focus);
}

/* ── Add-entry dropdowns (foil, condition) — mirrors .dga-fmt-dropdown-btn / .set-dropdown-btn rotating-arrow style ── */
.admin-pid-dropdown-wrap {
    position: relative;
}

/* Shared .btn .btn--ghost .btn--mono (components.css). */
.admin-pid-dropdown-btn {
    width: 100%;
    font-size: 0.78rem;
    background: var(--bg);
    padding: 6px 10px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}

.admin-pid-dropdown-btn:hover,
.admin-pid-dropdown-btn.open {
    border-color: var(--border-focus);
}

.admin-pid-dropdown-btn:disabled {
    cursor: not-allowed;
    color: var(--text-muted);
}

/* Shared .dropdown-arrow (components.css) + muted colour. */
.admin-pid-dropdown-arrow {
    color: var(--text-muted);
}

/* Shared .menu (components.css) — full-width, scrolls when tall, above the drawer. */
.admin-pid-dropdown-menu {
    left: 0;
    right: 0;
    z-index: 301;
    max-height: 200px;
    overflow-y: auto;
}

.admin-pid-dropdown-option {
    padding: 8px 12px;
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
    font-family: var(--font-mono);
    font-size: 0.78rem;
    color: var(--text-muted);
    letter-spacing: 0.04em;
}

.admin-pid-dropdown-option:hover {
    background: var(--bg-hover);
    color: var(--text);
}

.admin-pid-dropdown-option.selected {
    color: var(--accent);
}

/* The Rarity- and Set-column header filters both reuse cards.css's
   .set-dropdown-wrap / .set-dropdown-btn / .set-dropdown-menu /
   .set-dropdown-option / .set-toggle directly (loaded globally) for the
   dropdown menu itself, so both match the cards page's set filter exactly.
   The trigger button, though, should look like the plain "Card" / "Product ID"
   header labels, not a boxed button — this strips its chrome down to match
   while keeping the arrow. */
#admin-pid-table-header .set-dropdown-btn {
    background: none;
    border: none;
    padding: 0;
    margin: 0;
    gap: 4px;
    font: inherit;
    /* Browsers reset text-transform (and letter-spacing) to normal on
       <button> by default, so `font: inherit` above — which only covers
       font-family/size/weight/line-height — isn't enough to match the
       other header labels' uppercase letter-spaced style; state both
       explicitly instead of relying on inheritance for them. */
    text-transform: uppercase;
    letter-spacing: inherit;
    color: inherit;
    cursor: pointer;
}

#admin-pid-table-header .set-dropdown-btn:hover,
#admin-pid-table-header .set-dropdown-btn.open {
    color: var(--accent);
}

/* The header row is uppercase/letter-spaced, but the menu's own set names
   shouldn't inherit that, and the list is capped with a scrollbar instead of
   growing tall enough to clip off-screen when there are many sets. */
#admin-pid-table-header .set-dropdown-menu {
    text-transform: none;
    letter-spacing: normal;
    max-height: 240px;
    overflow: auto;
}

/* Info mode's list panel is a fixed, narrow track (--admin-pid-info-list-width)
   and its ancestor (.admin-pricing-list) clips overflow — the Rarity/Set
   menus' default left:0 (cards.css .set-dropdown-menu) grows them rightward
   from the column's left edge, which is fine in Pricing mode's much wider
   list but pushes them past Info mode's panel edge and gets them clipped.
   Growing leftward from the column's right edge instead keeps both menus
   inside the panel, since neither column sits near its left edge. */
.admin-cards-mode-info #admin-pid-table-header .set-dropdown-menu {
    left: auto;
    right: 0;
}

/* The Product ID filter sits close to the row's right edge (Sales/Listings
   still follow it) — same overflow risk and same fix as Info mode's panel
   above, just scoped to this one column instead of the whole header. */
.admin-pid-col-status .set-dropdown-menu {
    left: auto;
    right: 0;
}

/* Matches #set-dropdown-options's scrollbar on the cards page exactly. */
#admin-pid-table-header .set-dropdown-menu::-webkit-scrollbar {
    width: 6px;
}

#admin-pid-table-header .set-dropdown-menu::-webkit-scrollbar-track {
    background: var(--bg-card);
}

#admin-pid-table-header .set-dropdown-menu::-webkit-scrollbar-thumb {
    background: var(--border);
    border-radius: 3px;
}

#admin-pid-table-header .set-dropdown-menu::-webkit-scrollbar-thumb:hover {
    background: var(--border-focus);
}

.admin-pid-add-entry-row {
    display: flex;
    gap: 8px;
}

.admin-pid-add-entry-input {
    flex: 1;
    min-width: 0;
    font-family: var(--font-sans);
    font-size: 0.8rem;
    color: var(--text);
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 6px 10px;
    appearance: none;
    -webkit-appearance: none;
    color-scheme: dark;
}

.admin-pid-add-entry-input:focus {
    outline: none;
    border-color: var(--border-focus);
}

.admin-pid-add-entry-input::-webkit-inner-spin-button,
.admin-pid-add-entry-input::-webkit-outer-spin-button {
    -webkit-appearance: none;
}

.admin-pid-add-entry-qty {
    flex: 0 0 70px;
}

.admin-pid-add-entry-actions {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-top: 2px;
}

.admin-pid-add-entry-status {
    font-size: 0.78rem;
}

.admin-pid-refresh-done {
    color: var(--series-sales);
}

.admin-pid-refresh-error {
    color: var(--error);
}

.admin-pid-detail-table-scroll {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
}

.admin-pid-detail-table {
    display: flex;
    flex-direction: column;
}

.admin-pid-detail-row {
    display: grid;
    grid-template-columns: 1fr 1.4fr 0.8fr 0.6fr 20px;
    align-items: center;
    gap: 8px;
    padding: 6px 12px;
    font-size: 0.78rem;
    border-bottom: 1px solid var(--border);
}

.admin-pid-detail-row:last-child {
    border-bottom: none;
}

.admin-pid-detail-delete-btn {
    width: 20px;
    height: 20px;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.95rem;
    line-height: 1;
    color: var(--text-muted);
    background: transparent;
    border: none;
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: color 0.15s, background 0.15s;
}

.admin-pid-detail-delete-btn:hover {
    color: var(--error);
    background: color-mix(in srgb, var(--error) 12%, transparent);
}

.admin-pid-detail-delete-btn:disabled {
    cursor: not-allowed;
    opacity: 0.5;
}

.admin-pid-detail-row-header {
    flex-shrink: 0;
    font-family: var(--font-mono);
    font-size: 0.68rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
    background: var(--bg-hover);
}
