/* Layout + component styling. Bootstrap 5 supplies the grid/utilities; this
   is the custom design system layered on top (spec §54 — not plain Bootstrap). */

#app-shell {
    display: flex;
    min-height: 100vh;
}

#live-news-ticker {
    --live-news-height: 2.5rem;
    --live-news-duration: 30000ms;
    --live-news-passes: 3;
    position: fixed;
    inset: 0 0 auto 0;
    height: var(--live-news-height);
    z-index: 1100;
    display: flex;
    align-items: center;
    overflow: hidden;
    color: #fff;
    background: linear-gradient(90deg, #5b21b6, #9d174d, #5b21b6);
    box-shadow: 0 2px 10px hsl(var(--shadow-color) / .28);
    font-weight: 700;
    font-size: .9rem;
}
#live-news-ticker.d-none { display: none !important; }
.live-news-viewport { flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; }
[data-live-news-text] { display: inline-block; padding-inline: 1.5rem; }
[data-live-news-text].live-news-run {
    animation: live-news-scroll var(--live-news-duration) linear var(--live-news-passes) both;
}
@keyframes live-news-scroll {
    from { transform: translateX(100vw); }
    to { transform: translateX(-100%); }
}
/* Batch 12 follow-up: the repeat-count badge sits outside the scrolling
   text (a flex sibling, not nested inside [data-live-news-text]) so it
   never affects the marquee's own scrollWidth-based travel distance, and
   stays put — legible — while the message scrolls behind/past it. */
.live-news-repeat-badge {
    flex-shrink: 0;
    margin-inline: .5rem 1rem;
    padding: .15rem .55rem;
    border-radius: 999px;
    background: rgba(255, 255, 255, .25);
    font-size: .78rem;
    white-space: nowrap;
}
body.live-news-visible #app-shell { padding-top: 2.5rem; }
body.live-news-visible #wallet-bar { top: 2.5rem; }
/* #wallet-bar (and the balances inside it) shift down by the ticker's height
   when it's visible — the toast stack must shift down by the same amount so
   toasts keep landing below the balances instead of overlapping/appearing
   above them. */
body.live-news-visible #toast-stack { top: calc(4rem + 2.5rem + env(safe-area-inset-top)); }
@media (min-width: 768px) {
    body.live-news-visible #sidebar { top: 2.5rem; height: calc(100vh - 2.5rem); }
}
/* Batch 12 follow-up 8: #sidebar is a fixed-position Bootstrap offcanvas
   drawer below 768px (see the #sidebar comment further down), independent
   of #app-shell's normal document flow — the padding-top above doesn't
   reach it. Without this, opening the mobile drawer while the ticker is
   visible let the fixed ticker (z-index 1100) render on top of the
   drawer's own header, covering the Binder logo and close button. Mirrors
   the desktop rule above; bottom stays Bootstrap's own 0, so the drawer
   simply gets shorter instead of needing an explicit height override. */
@media (max-width: 767.98px) {
    body.live-news-visible #sidebar { top: 2.5rem; }
}
@media (prefers-reduced-motion: reduce) {
    [data-live-news-text].live-news-run {
        display: block;
        width: 100%;
        overflow: hidden;
        text-align: center;
        text-overflow: ellipsis;
        animation: none;
    }
}

/* Login/register: no sidebar (there's nowhere to navigate to
   yet), content centered in the full viewport instead of left-aligned next
   to a nav column. !important on the sidebar hide because .offcanvas-md's
   own responsive rules already fight for width/visibility at this
   breakpoint (see the #sidebar rule below) — this needs to win outright,
   on mobile too where it would otherwise still be reachable via the
   hamburger toggle despite there being no nav to show while logged out. */
#app-shell.auth-mode #sidebar { display: none !important; }
#app-shell.auth-mode #view-root {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
}

/* #sidebar is a Bootstrap RESPONSIVE offcanvas: real slide-in drawer below
   the md breakpoint, normal static sidebar at md+. The element's class list
   is deliberately `offcanvas-md offcanvas-start` WITHOUT the bare
   `offcanvas` class — combining them (an earlier version of this markup
   did) drags in .offcanvas's own always-on, non-responsive rule
   (`.offcanvas.offcanvas-start { position:fixed; ...; transform:
   translateX(-100%) }` plus `.offcanvas { visibility:hidden }`), which
   permanently hides the element at *every* viewport width — nothing in the
   `offcanvas-md` breakpoint variant ever undoes it, since Bootstrap's own
   docs never combine the two. That was the actual bug behind "sidebar is
   completely invisible on desktop": it wasn't a styling/specificity
   problem, the element was `visibility:hidden` and translated off-screen
   unconditionally. With the bare class removed, `.offcanvas-md`'s own
   compound rules (scoped to `max-width:767.98px`) fully own the mobile
   drawer's show/hide, and nothing hides the element at md+ at all.
   Bootstrap's `min-width:768px` `.offcanvas-md` rule still forces
   `background-color: transparent !important` and un-!important-ed
   `.offcanvas-header { display:none }` / `.offcanvas-body` padding — those
   still need overriding below. */
#sidebar {
    --bs-offcanvas-width: 260px;
    background: var(--bg-elevated);
    color: var(--text-primary);
}
@media (min-width: 768px) {
    #sidebar {
        display: flex;
        flex-direction: column;
        width: 230px;
        flex-shrink: 0;
        background: var(--bg-elevated) !important;
        border-right: 1px solid var(--surface-border);
        position: sticky;
        top: 0;
        height: 100vh;
    }
    #sidebar .offcanvas-header { display: flex !important; }
    #sidebar .offcanvas-body { min-height: 0; overflow: hidden; padding: 1.25rem 1rem; }
    #sidebar .nav-links {
        scrollbar-width: thin;
        scrollbar-color: var(--surface-border) transparent;
    }
    #sidebar .nav-links::-webkit-scrollbar { width: 5px; }
    #sidebar .nav-links::-webkit-scrollbar-track { background: transparent; }
    #sidebar .nav-links::-webkit-scrollbar-thumb { background: var(--surface-border); border-radius: 999px; }
    .nav-group-toggle { padding: .48rem .58rem; }
    .nav-group-items .nav-link-item { padding: .48rem .58rem .48rem .9rem; min-height: 38px; }
}
.brand {
    display: flex;
    align-items: center;
    gap: .5rem;
    font-weight: 800;
    font-size: 1.1rem;
    color: var(--accent);
    text-decoration: none;
}
.brand:hover { color: var(--accent); }
/* Pin the mobile drawer's close button to the far right of the header with a
   bit of breathing room, regardless of whatever else sits in that flex row. */
#sidebar .offcanvas-header .theme-toggle[data-bs-dismiss] {
    margin-left: auto;
    margin-right: .25rem;
}
.nav-links {
    display: flex;
    flex-direction: column;
    gap: .4rem;
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding-right: .15rem;
}
.nav-group { display: flex; flex-direction: column; gap: .15rem; }
.nav-group-toggle {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .6rem;
    width: 100%;
    padding: .55rem .65rem;
    border: 0;
    border-radius: var(--radius-sm);
    color: var(--text-primary);
    background: transparent;
    font-size: .78rem;
    font-weight: 800;
    letter-spacing: .035em;
    text-align: left;
    text-transform: uppercase;
}
/* A dedicated, non-wrapping row for icon + label + badge + chevron so a
   longer group label (e.g. "ADMINISTRATION" vs "ACCOUNT") can never push the
   chevron onto its own wrapped line — width:100% makes this the sole flex
   item on its line, leaving .nav-group-summary (flex-basis:100% below) free
   to still wrap onto its own second line as intended. */
.nav-group-toggle-main {
    display: flex;
    align-items: center;
    gap: .6rem;
    width: 100%;
    min-width: 0;
}
.nav-group-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-group-toggle:hover { background: var(--bg-sunken); }
.nav-group-chevron {
    margin-left: auto;
    transition: transform .18s ease;
}
.nav-group.open .nav-group-chevron { transform: rotate(180deg); }
.nav-group.open .nav-group-summary { display: none; }
.nav-group-summary {
    display: flex;
    align-items: center;
    flex-basis: 100%;
    gap: .35rem;
    min-width: 0;
    padding-left: 1.6rem;
    padding-top: .1rem;
}
.nav-group-quiet {
    display: inline-flex;
    align-items: center;
    gap: .2rem;
    min-width: 0;
    color: var(--text-secondary);
    font-size: .66rem;
    font-weight: 700;
    letter-spacing: 0;
    text-transform: none;
}
.nav-group-items { display: flex; flex-direction: column; gap: .2rem; }
.nav-group-items[hidden] { display: none; }
.nav-group-items .nav-link-item { padding-left: 1rem; }
.nav-link-item {
    display: flex;
    align-items: center;
    gap: .65rem;
    padding: .6rem .75rem;
    border-radius: var(--radius-sm);
    color: var(--text-secondary);
    font-weight: 600;
    cursor: pointer;
    text-decoration: none;
    border: none;
    background: none;
    text-align: left;
    width: 100%;
    font-size: .88rem;
}
.nav-link-item > .bi { flex-shrink: 0; }
.nav-link-label { min-width: 0; }
.nav-link-meta {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    margin-left: auto;
    flex-shrink: 0;
}
.nav-quiet-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.45rem;
    padding: .08rem .32rem;
    border: 1px solid var(--surface-border);
    border-radius: 999px;
    background: var(--bg-sunken);
    color: var(--text-secondary);
    font-size: .68rem;
    font-weight: 700;
    line-height: 1.2;
    white-space: nowrap;
}
.nav-link-item.active .nav-quiet-count {
    border-color: rgb(255 255 255 / .45);
    background: rgb(255 255 255 / .16);
    color: #fff;
}
.nav-player-identity {
    display: flex;
    align-items: center;
    gap: .6rem;
    margin: .1rem .35rem .35rem;
    padding: .55rem;
    border: 1px solid var(--surface-border);
    border-radius: var(--radius-sm);
    color: var(--text-primary);
    text-decoration: none;
    background: var(--bg-sunken);
}
.nav-player-identity:hover { border-color: var(--accent); color: var(--text-primary); }
.nav-player-copy { display: flex; flex-direction: column; min-width: 0; line-height: 1.15; }
.nav-player-name { overflow: hidden; font-size: .82rem; font-weight: 800; text-overflow: ellipsis; white-space: nowrap; }
.nav-player-username { overflow: hidden; color: var(--text-secondary); font-size: .7rem; text-overflow: ellipsis; white-space: nowrap; }
.nav-link-item:hover { background: var(--accent-soft); color: var(--text-primary); }
.nav-link-item.active { background: var(--accent); color: #fff; }
.sidebar-footer { flex-shrink: 0; padding-top: 1rem; border-top: 1px solid var(--surface-border); }
.theme-toggle {
    background: var(--bg-sunken);
    border: 1px solid var(--surface-border);
    color: var(--text-primary);
    border-radius: 999px;
    width: 40px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
}

.notification-badge {
    position: absolute;
    top: -4px;
    right: -4px;
    background: var(--danger);
    color: #fff;
    font-size: .65rem;
    font-weight: 700;
    min-width: 17px;
    height: 17px;
    border-radius: 999px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 3px;
}
.notification-badge.notification-badge-muted {
    background: var(--success);
}
/* Second, independently-shown badge (marketplace/auctions) sitting beside the
   main urgent one — offset left so both can be visible at once instead of
   one color masking the other. */
.notification-badge.notification-badge-secondary {
    right: 14px;
}

@media (max-width: 767.98px) {
    .nav-group-toggle,
    .nav-link-item { min-height: 44px; }
}

.content-column {
    flex: 1;
    display: flex;
    flex-direction: column;
    min-width: 0;
}

#wallet-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    padding: .6rem 1.5rem;
    border-bottom: 1px solid var(--surface-border);
    background: var(--bg-elevated);
    position: sticky;
    top: 0;
    z-index: 30;
}

#tutorial-banner {
    padding: .55rem 1.5rem;
    background: var(--accent-soft);
    border-bottom: 1px solid var(--surface-border);
    font-size: .85rem;
    text-align: center;
}
#tutorial-banner a { font-weight: 700; }

#view-root {
    flex: 1;
    padding: 1.5rem;
    max-width: 1100px;
    margin: 0 auto;
    width: 100%;
}

.page-header { margin-bottom: 1.25rem; }
.page-header h1 { font-size: 1.4rem; font-weight: 800; margin: 0; }

/* Global, page-level footer (Batch 8) — a sibling of #view-root inside
   .content-column, not the sidebar's own .sidebar-footer (kept theme-toggle
   only). flex:1 on #view-root pushes this to the bottom of the viewport on
   short pages and lets it follow content normally on tall ones. */
.app-footer {
    flex-shrink: 0;
    padding: 1rem 1.5rem 1.25rem;
    border-top: 1px solid var(--surface-border);
    text-align: center;
}
.app-footer-links {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: .4rem 1.1rem;
    margin-bottom: .5rem;
}
.app-footer-link {
    background: none;
    border: none;
    padding: 0;
    color: var(--text-secondary);
    font-size: .82rem;
    font-weight: 600;
    text-decoration: none;
}
.app-footer-link:hover { color: var(--accent); text-decoration: underline; }
.app-footer-disclaimer,
.app-footer-contact {
    max-width: 720px;
    margin: 0 auto;
    color: var(--text-secondary);
    font-size: .72rem;
    line-height: 1.4;
}
.app-footer-contact { margin-top: .25rem; }
.app-footer-contact a { color: var(--text-secondary); }

.stat-pill {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    background: var(--bg-sunken);
    border: 1px solid var(--surface-border);
    border-radius: 999px;
    padding: .4rem .85rem;
    font-weight: 700;
}

/* Exact-amount tooltip for k/m-formatted balances: no `transition` anywhere
   here on purpose, so hover shows it instantly instead of waiting on the
   browser's native title-attribute delay. Hover-only (desktop) — tapping on
   touch devices shows the same value via a toast instead (app.js), since
   this tooltip sits inside the sticky top wallet bar with no room above it
   to render into on a tap. */
.exact-value-pill { position: relative; cursor: default; }
.exact-value-tooltip {
    position: absolute;
    bottom: 100%;
    left: 50%;
    transform: translateX(-50%);
    margin-bottom: .4rem;
    background: var(--bg-elevated);
    border: 1px solid var(--surface-border);
    border-radius: var(--radius-sm);
    padding: .3rem .6rem;
    font-size: .78rem;
    font-weight: 700;
    white-space: nowrap;
    box-shadow: 0 8px 20px hsl(var(--shadow-color) / .3);
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    z-index: 50;
}
.exact-value-pill:hover .exact-value-tooltip {
    opacity: 1;
    visibility: visible;
}

/* VIP badge click popover (badges.js) — a chart-tooltip-style bubble
   (matches Admin Analytics' chart tooltips) rather than a full modal, since
   this is a quick glance at one badge's effects, not a task needing a
   dedicated screen. Fixed-positioned and appended to <body> (not nested
   inside the badge) so it can never get clipped by a scrollable ancestor
   (chat messages, notification lists, etc.) the way a relative/absolute
   pair nested in place would be. The arrow is a rotated square clipped by
   the bubble's own overflow, same construction as a typical chat bubble
   tail. */
.vip-badge-popover {
    position: fixed;
    z-index: 1060;
    background: var(--bg-elevated);
    border: 1px solid var(--surface-border);
    border-radius: var(--radius-sm);
    padding: .55rem .75rem;
    font-size: .82rem;
    box-shadow: 0 10px 30px hsl(var(--shadow-color) / .3);
    max-width: 240px;
}
.vip-badge-popover-arrow {
    position: absolute;
    width: 10px;
    height: 10px;
    background: var(--bg-elevated);
    transform: rotate(45deg);
}
.vip-badge-popover[data-placement="top"] .vip-badge-popover-arrow {
    bottom: -5px;
    border-right: 1px solid var(--surface-border);
    border-bottom: 1px solid var(--surface-border);
}
.vip-badge-popover[data-placement="bottom"] .vip-badge-popover-arrow {
    top: -5px;
    border-left: 1px solid var(--surface-border);
    border-top: 1px solid var(--surface-border);
}
.vip-badge-popover ul { margin: 0; padding-left: 1.1rem; }
.vip-badge-popover li { white-space: nowrap; }

.equip-slot {
    border: 1px dashed var(--surface-border);
    border-radius: var(--radius-md);
    padding: .75rem;
    background: var(--bg-elevated);
    display: flex;
    flex-direction: column;
    gap: .5rem;
    min-height: 220px;
    /* Bootstrap's .row stretches every .col to the row's tallest sibling by
       default, but that only stretches the column itself — this element
       still needs height:100% to actually fill it, otherwise a short-content
       slot (e.g. "empty") just sizes to min-height while equipped slots
       (badge + name + CPS + pending + buttons) grow taller than their peers. */
    height: 100%;
}
.equip-slot.locked { align-items: center; justify-content: center; text-align: center; color: var(--text-muted); background: var(--bg-sunken); }
.equip-slot.empty { align-items: center; justify-content: center; text-align: center; color: var(--text-muted); cursor: pointer; }
.equip-slot.empty:hover { border-color: var(--accent); color: var(--accent); }

.card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
    gap: 1rem;
}

/* Binder page: fixed 2x2 spread (Collection + profile browsing), same at
   every viewport width — unlike .card-grid's auto-fill, this never
   collapses to a single column on mobile. */
.binder-page-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 1rem;
    max-width: 420px;
    margin-left: auto;
    margin-right: auto;
}

.binder-pocket-empty {
    border: 1px dashed var(--surface-border);
    background: transparent;
    box-shadow: none;
}

/* Edit Badges modal — the real badge component, made selectable (Batch 17
   follow-up: render actual badges instead of a checkbox-and-text list). */
.badge-select-option {
    display: inline-flex;
    cursor: pointer;
    border: 2px solid transparent;
    border-radius: 999px;
    padding: 2px;
    transition: border-color .12s ease, box-shadow .12s ease;
}
.badge-select-option:hover { border-color: var(--surface-border); }
.badge-select-option.badge-select-selected {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
}

/* Inventory card action row: favorite icon + main equip button + kebab menu,
   sized to fit exactly within one .card-grid column with no overlap. */
.card-action-row {
    display: flex;
    align-items: center;
    gap: .35rem;
}
.icon-btn {
    flex: 0 0 auto;
    width: 32px;
    height: 32px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    background: var(--bg-sunken);
    color: var(--text-primary);
    border: 1px solid var(--surface-border);
    border-radius: var(--radius-sm);
    font-size: .95rem;
    line-height: 1;
}
.icon-btn:hover { background: var(--accent-soft); }
.icon-btn.active-fav { color: #ff5c8d; border-color: #ff5c8d; }

.card-action-row .btn-accent {
    flex: 1 1 auto;
    min-width: 0;
    padding: .4rem .5rem;
    font-size: .78rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.dropdown-menu {
    background: var(--bg-elevated);
    border: 1px solid var(--surface-border);
    border-radius: var(--radius-md);
    box-shadow: 0 12px 32px hsl(var(--shadow-color) / .3);
    min-width: 190px;
    padding: .35rem;
}
.dropdown-item {
    display: flex;
    align-items: center;
    gap: .55rem;
    color: var(--text-primary);
    font-size: .85rem;
    font-weight: 500;
    padding: .5rem .6rem;
    border-radius: var(--radius-sm);
}
.dropdown-item:hover, .dropdown-item:focus {
    background: var(--accent-soft);
    color: var(--text-primary);
}
.dropdown-item.active-lock { color: var(--accent); }

.skeleton {
    background: linear-gradient(90deg, var(--bg-sunken) 25%, var(--surface-border) 37%, var(--bg-sunken) 63%);
    background-size: 400% 100%;
    border-radius: var(--radius-sm);
    animation: skeleton-loading 1.4s ease infinite;
}
@keyframes skeleton-loading { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }

/* Browse Players: the viewer's own card, pinned to the front of the default
   (unsearched) list — a pulsing accent glow makes it unmistakable at a glance
   rather than just a background tint. */
.self-glow {
    border-color: var(--accent) !important;
    animation: self-glow-pulse 2.2s ease-in-out infinite;
}
@keyframes self-glow-pulse {
    0%, 100% { box-shadow: 0 0 0 1px var(--accent), 0 0 10px 1px var(--accent-soft); }
    50% { box-shadow: 0 0 0 1px var(--accent), 0 0 20px 6px var(--accent-soft); }
}
@media (prefers-reduced-motion: reduce) {
    .self-glow { animation: none; box-shadow: 0 0 0 1px var(--accent), 0 0 14px 3px var(--accent-soft); }
}
@media (prefers-reduced-motion: reduce) {
    .skeleton { animation: none; }
}

/* Batch 19: runPending()'s fallback for a non-<button> interactive element
   (no native .disabled) — blocks interaction and dims it the same way a
   disabled button reads, while its own spinner/text swap communicates why. */
.pending-disabled {
    pointer-events: none;
    opacity: .65;
}

/* Batch 16: briefly highlights a Chat Room message row that was sent since
   the player's previous read boundary, on entering the room — a one-shot
   ~10s fade rather than .self-glow's infinite pulse, since this should mark
   a moment in time and then get out of the way, never a permanent style. */
.chat-msg-unread-glow {
    animation: chat-unread-glow-fade 10s ease-out forwards;
}
@keyframes chat-unread-glow-fade {
    0% { background-color: var(--accent-soft); box-shadow: 0 0 0 1px var(--accent), 0 0 14px 3px var(--accent-soft); border-radius: var(--radius-sm); }
    100% { background-color: transparent; box-shadow: none; }
}
@media (prefers-reduced-motion: reduce) {
    .chat-msg-unread-glow { animation: none; background-color: var(--accent-soft); border-radius: var(--radius-sm); }
}

/* A quick one-off flash used to draw the eye to a message jumped to via a
   reply-quote click — same visual language as the glow above, much shorter. */
.chat-msg-flash {
    animation: chat-unread-glow-fade 1.5s ease-out forwards;
}
@media (prefers-reduced-motion: reduce) {
    .chat-msg-flash { animation: none; background-color: var(--accent-soft); border-radius: var(--radius-sm); }
}

.chat-mention {
    color: var(--accent);
    font-weight: 600;
}

.attachment-drop-active {
    border-color: var(--accent) !important;
    background: var(--accent-soft);
}

.chat-room-mention {
    color: var(--accent);
    font-weight: 600;
    text-decoration: none;
}
.chat-room-mention:hover {
    text-decoration: underline;
}

.chat-reply-quote {
    display: block;
    background: none;
    border: none;
    border-left: 2px solid var(--accent);
    padding: 2px 8px;
    margin: 2px 0 4px;
    color: var(--text-muted);
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
button.chat-reply-quote {
    cursor: pointer;
}

.mention-option.active,
.mention-option:hover {
    background: var(--bg-sunken);
}

.chat-reaction-bar {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 4px;
    margin-top: 4px;
}
.chat-reaction-pills {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin-top: 2px;
}
.chat-reaction-pills:empty {
    margin-top: 0;
}
.chat-reaction-pill {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    background: var(--bg-sunken);
    border: 1px solid var(--surface-border);
    border-radius: 999px;
    padding: 1px 7px;
    font-size: .82rem;
    line-height: 1.6;
    cursor: pointer;
    color: var(--text-primary);
}
.chat-reaction-pill:hover {
    border-color: var(--accent);
}
.chat-reaction-pill.active {
    background: var(--accent-soft);
    border-color: var(--accent);
}
.chat-reaction-count {
    font-size: .72rem;
    color: var(--text-secondary);
}
.presence-dot {
    display: inline-block;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    margin-left: 2px;
}
.presence-dot-online { background: var(--success); }
.presence-dot-offline { background: var(--text-secondary); opacity: .5; }
.chat-reaction-add-wrap {
    position: relative;
    display: inline-block;
}
.chat-reaction-add-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    background: none;
    border: 1px solid var(--surface-border);
    border-radius: 999px;
    color: var(--text-secondary);
    cursor: pointer;
    font-size: .8rem;
    padding: 0;
}
.chat-reaction-add-btn:hover {
    border-color: var(--accent);
    color: var(--accent);
}
.chat-reaction-picker {
    position: absolute;
    bottom: 100%;
    left: 0;
    margin-bottom: 4px;
    display: flex;
    gap: 2px;
    background: var(--bg-sunken);
    border: 1px solid var(--surface-border);
    border-radius: 999px;
    padding: 4px;
    box-shadow: 0 4px 16px rgba(0, 0, 0, .25);
    z-index: 15;
}
.chat-reaction-picker-btn {
    background: none;
    border: none;
    font-size: 1.1rem;
    line-height: 1;
    padding: 4px 6px;
    border-radius: 999px;
    cursor: pointer;
}
.chat-reaction-picker-btn:hover {
    background: var(--accent-soft);
}

.empty-state {
    text-align: center;
    padding: 3rem 1rem;
    color: var(--text-muted);
}
.empty-state i { font-size: 2.2rem; opacity: .6; margin-bottom: .5rem; display: block; }

#toast-stack {
    position: fixed;
    /* Top, below the sticky #wallet-bar (~3.25rem tall) so toasts never sit
       above/under the balances — see the body.live-news-visible override
       below, which pushes this down further to stay below the wallet bar
       once the ticker has also pushed the wallet bar down. right side —
       Netlify's dev-only Heads-up Display overlay (visible only to
       teammates logged into Netlify, never real players) sits in the
       bottom-right corner, so it never overlaps this top-right stack. */
    top: calc(4rem + env(safe-area-inset-top));
    right: 1rem;
    z-index: 100;
    display: flex;
    flex-direction: column;
    gap: .5rem;
    max-width: 320px;
}
.toast-item {
    display: flex;
    align-items: flex-start;
    gap: .5rem;
    background: var(--bg-elevated);
    border: 1px solid var(--surface-border);
    border-left: 4px solid var(--accent);
    border-radius: var(--radius-sm);
    padding: .7rem .6rem .7rem .9rem;
    box-shadow: 0 10px 30px hsl(var(--shadow-color) / .25);
    font-size: .88rem;
    animation: toast-in .18s ease;
    /* Manual swipe-to-close (toast.js) drags this via inline transform/
       opacity/transition — touch-action:pan-y lets the browser still treat
       a vertical drag as page scroll instead of fighting the gesture. */
    touch-action: pan-y;
}
.toast-item.success { border-left-color: var(--success); }
.toast-item.danger { border-left-color: var(--danger); }
.toast-item-text { flex: 1 1 auto; min-width: 0; }
.toast-item-close {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.5rem;
    padding: 0;
    margin-top: -.1rem;
    border: none;
    background: none;
    color: var(--text-muted);
    font-size: .8rem;
    border-radius: var(--radius-sm);
    cursor: pointer;
}
.toast-item-close:hover, .toast-item-close:focus-visible { color: var(--text-primary); background: var(--bg-sunken); }
@keyframes toast-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: translateY(0); } }

/* Batch 8: Binder-themed page-turn flourish, applied by pagination.js's
   bindPager() to whichever content region a paginated view opts in with.
   A lightweight 2D-content-with-3D-transform illusion (perspective()+
   rotateY on the region itself, plus a brief lifted box-shadow to read as
   a page tilting up off the surface) rather than a true two-sided flip,
   which would need duplicated front/back content per view — sequenced as
   two separate one-directional animations (out, then in) on the same
   element since only one page's content is ever in the DOM at a time.
   Durations here must match pagination.js's TURN_OUT_MS/TURN_IN_MS exactly
   — that's what the swap is actually sequenced against. */
.page-turn-out-next, .page-turn-out-prev, .page-turn-in-next, .page-turn-in-prev {
    animation-duration: .15s;
    animation-fill-mode: forwards;
}
.page-turn-out-next { animation-name: page-turn-out-next; animation-timing-function: ease-in; transform-origin: left center; }
.page-turn-out-prev { animation-name: page-turn-out-prev; animation-timing-function: ease-in; transform-origin: right center; }
.page-turn-in-next { animation-name: page-turn-in-next; animation-duration: .22s; animation-timing-function: ease-out; transform-origin: right center; }
.page-turn-in-prev { animation-name: page-turn-in-prev; animation-duration: .22s; animation-timing-function: ease-out; transform-origin: left center; }
@keyframes page-turn-out-next {
    from { transform: perspective(900px) rotateY(0deg) scale(1); opacity: 1; box-shadow: none; }
    to { transform: perspective(900px) rotateY(-55deg) scale(.96); opacity: 0; box-shadow: -12px 0 24px hsl(var(--shadow-color) / .35); }
}
@keyframes page-turn-out-prev {
    from { transform: perspective(900px) rotateY(0deg) scale(1); opacity: 1; box-shadow: none; }
    to { transform: perspective(900px) rotateY(55deg) scale(.96); opacity: 0; box-shadow: 12px 0 24px hsl(var(--shadow-color) / .35); }
}
@keyframes page-turn-in-next {
    from { transform: perspective(900px) rotateY(55deg) scale(.96); opacity: 0; box-shadow: 12px 0 24px hsl(var(--shadow-color) / .35); }
    to { transform: perspective(900px) rotateY(0deg) scale(1); opacity: 1; box-shadow: none; }
}
@keyframes page-turn-in-prev {
    from { transform: perspective(900px) rotateY(-55deg) scale(.96); opacity: 0; box-shadow: -12px 0 24px hsl(var(--shadow-color) / .35); }
    to { transform: perspective(900px) rotateY(0deg) scale(1); opacity: 1; box-shadow: none; }
}
@media (prefers-reduced-motion: reduce) {
    .page-turn-out-next, .page-turn-out-prev, .page-turn-in-next, .page-turn-in-prev { animation: none; }
}

.form-field label { font-weight: 600; font-size: .85rem; color: var(--text-secondary); }
.form-control, .form-select {
    background: var(--bg-sunken);
    border: 1px solid var(--surface-border);
    color: var(--text-primary);
}
.form-control::placeholder {
    color: var(--text-secondary);
    opacity: 1; /* Firefox applies its own lower default opacity otherwise */
}
.form-control:focus, .form-select:focus {
    background: var(--bg-sunken);
    color: var(--text-primary);
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
}

.auth-shell {
    max-width: 400px;
    margin: 3rem auto;
    padding: 2rem;
}

.modal-backdrop-custom {
    position: fixed;
    inset: 0;
    background: rgba(10, 8, 20, .55);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 80;
    padding: 1rem;
}
.modal-panel {
    background: var(--bg-elevated);
    border: 1px solid var(--surface-border);
    border-radius: var(--radius-lg);
    max-width: 440px;
    width: 100%;
    padding: 1.75rem;
    max-height: calc(100vh - 2rem);
    display: flex;
    flex-direction: column;
}
.modal-body-content {
    overflow-y: auto;
    flex: 1 1 auto;
    min-height: 0;
}
.badge-streak {
    display: inline-flex;
    align-items: center;
    gap: .2rem;
    padding: .22rem .48rem;
    border-radius: 999px;
    background: linear-gradient(135deg, #ff7a18, #e7372f);
    color: #fff;
    font-size: .72rem;
    font-weight: 800;
    line-height: 1;
    box-shadow: 0 2px 8px rgba(231, 55, 47, .24);
    vertical-align: middle;
}

.badge-streak-milestone {
    display: inline-flex;
    align-items: center;
    gap: .2rem;
    padding: .22rem .48rem;
    border: 1px solid rgba(255, 210, 77, .72);
    border-radius: 999px;
    background: linear-gradient(135deg, #9a5d00, #f2b705);
    color: #fff;
    font-size: .72rem;
    font-weight: 800;
    line-height: 1;
    box-shadow: 0 2px 8px rgba(242, 183, 5, .24);
    vertical-align: middle;
}

.badge-achievement {
    display: inline-flex;
    align-items: center;
    gap: .2rem;
    padding: .22rem .48rem;
    border-radius: 999px;
    background: linear-gradient(135deg, #6a3093, #a044ff);
    color: #fff;
    font-size: .72rem;
    font-weight: 800;
    line-height: 1;
    box-shadow: 0 2px 8px rgba(160, 68, 255, .24);
    vertical-align: middle;
}

.login-calendar-grid {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: .4rem;
}

.login-calendar-weekday {
    padding: .35rem 0;
    color: var(--text-secondary);
    font-size: .72rem;
    font-weight: 700;
    text-align: center;
    text-transform: uppercase;
}

.login-calendar-day {
    width: 100%;
    position: relative;
    min-height: 4rem;
    padding: .45rem;
    border: 1px solid var(--surface-border);
    border-radius: .75rem;
    background: var(--bg-sunken);
    color: var(--text-primary);
    font-family: inherit;
    font-weight: 700;
    text-align: left;
}

.login-calendar-day.logged-in {
    border-color: #ff7a18;
    background: linear-gradient(145deg, rgba(255, 122, 24, .2), rgba(231, 55, 47, .1));
    color: #c94816;
}

.login-calendar-day.today::after {
    content: 'Today';
    position: absolute;
    right: .35rem;
    bottom: .3rem;
    color: var(--accent);
    font-size: .6rem;
    text-transform: uppercase;
}

.login-calendar-day.empty {
    visibility: hidden;
}

.login-calendar-box-marker {
    position: absolute;
    top: 1.45rem;
    right: .35rem;
    display: inline-flex;
    align-items: center;
    gap: .15rem;
    color: var(--accent);
    font-size: .68rem;
    cursor: pointer;
}

.login-reward-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(165px, 1fr));
    gap: .65rem;
}

.login-reward-card {
    padding: .75rem;
    border: 1px solid var(--surface-border);
    border-radius: .75rem;
    background: var(--bg-sunken);
}

.login-cycle-progress {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
    gap: .6rem;
}

.login-cycle-day {
    padding: .65rem;
    text-align: center;
    border: 1px solid var(--surface-border);
    border-radius: .75rem;
    background: var(--bg-sunken);
}

.login-cycle-day-check {
    font-size: 1.2rem;
    color: var(--text-muted);
    margin-bottom: .2rem;
}

.login-cycle-day.claimed {
    border-color: #ff7a18;
    background: linear-gradient(145deg, rgba(255, 122, 24, .16), rgba(231, 55, 47, .08));
}

.login-cycle-day.claimed .login-cycle-day-check { color: #ff7a18; }

.login-cycle-day.is-today {
    border-color: var(--accent);
    box-shadow: 0 0 0 .15rem var(--accent-soft);
}

@media (max-width: 575.98px) {
    .login-calendar-grid { gap: .22rem; }
    .login-calendar-day { min-height: 2.8rem; padding: .3rem; border-radius: .5rem; font-size: .8rem; }
    .login-calendar-day > span:first-child { font-size: .62rem; line-height: 1; }
    .login-calendar-box-marker { top: 1rem; right: .2rem; font-size: .56rem; }
    .login-calendar-day.today::after { content: ''; width: .35rem; height: .35rem; border-radius: 50%; background: var(--accent); }
}

/* Admin drag-to-reorder lists (services/dragSort.js) — Store Rotation
   Tracks, Chat Rooms, Announcements. */
.drag-sort-item { cursor: grab; }
.drag-sort-item.drag-sorting { opacity: .4; }
.drag-sort-handle { color: var(--text-muted); cursor: grab; padding-right: .3rem; }
/* Blocks further drags/clicks while a reorder save is in flight (Exclusive
   Boxes' own optimistic, no-refetch reorder — see admin.js renderBoxes()) so
   a second drag can't race the first one's still-pending confirm/revert. */
.drag-sort-saving { opacity: .6; pointer-events: none; }

/* Admin Dashboard tiles with a drill-down (Diamonds leaderboard, Coins
   Removed breakdown). */
.admin-tile-clickable { cursor: pointer; transition: border-color .12s ease; }
.admin-tile-clickable:hover { border-color: var(--accent); }
