/* ==========================================================================
   Pokemon Browser Game — app shell styles
   Mobile-first. Fixed header/footer, scrollable content, touch-sized
   controls. On wider viewports the shell stays phone-width and centers,
   so the game reads as "an app" rather than a stretched website.
   ========================================================================== */

:root {
    --color-bg:        #e5e8e8;
    --color-surface:   #f2f3f4;
    --color-surface-banner: #eaeded;
    --color-panel:     #f8f9f9;
    --color-primary:   #e3350d;
    --color-primary-dark: #b8280a;
    --color-accent:    #3b6ce0;
    --color-activity-accent: #3498db;
    --color-link-hover: #1abc9c;
    --color-heading:   #2c3e50;
    --color-text:      #34495e;
    --color-text-muted: #6b7c8f;
    --color-border:    #d5d8dc;
    --color-border-light: #abb2b9;
    --color-danger:    #d92d20;
    --color-success:   #1a7f37;

    --color-warning:   #f1c40f;

    /* Low-alpha tints derived from --color-primary/--color-danger, used for
       hover/active nav highlights and error banners — kept as their own
       variables (rather than hardcoded hex) so the 3 themes below can each
       supply a tint that actually reads against their own --color-surface,
       instead of every theme inheriting the light theme's pale pink. */
    --color-primary-tint: #feecec;
    --color-danger-tint:  #fdecea;

    --header-height: 56px;
    --footer-height: 68px;
    --sidebar-width: 200px;
    --radius: 8px;
    /* Smaller radius for content nested one level inside an outer container
       card (.app-header-group/.app-sidebar-left/.app-sidebar-right/.app-main
       all use --radius; .card and the party/box grid cells living inside
       one of those use this instead) — matches the reference mockup's
       two-tier 8px/6px rounding rather than one radius everywhere. */
    --radius-sm: 6px;
    --shell-max-width: 1600px;
}

/* Dark and Fire Red themes — selected via the sidebar's Theme switcher
   (assets/js/app.js persists the choice to localStorage as
   data-theme on <html>; includes/layout_header.php applies it before first
   paint via an inline head script, so there's no flash of the light theme).
   Every other rule in this file already reads color through the
   --color-* variables above, so overriding them here is enough to reskin
   the whole app — only a few rules elsewhere used a hardcoded hex instead
   of a variable (nav hover/active, .form-error, .item-count) and those were
   changed to reference --color-primary-tint/--color-danger-tint or a
   theme-neutral fixed color so they don't go pale-pink-on-black.

   --color-surface (outer header/sidebar/main card), --color-surface-banner
   (sidebar-group-title banners, one shade lighter/more saturated) and
   --color-panel (nested .card sections within .app-main) are 3 distinct
   depth layers, not 3 names for the same grey — matching the reference
   mockup's --bg-header/--bg-menu-header/--bg-panel exactly. */
:root[data-theme="dark"] {
    --color-bg:        #0a0a0a;
    --color-surface:   #1a1a1a;
    --color-surface-banner: #242424;
    --color-panel:     #181818;
    --color-primary:   #e3350d;
    --color-primary-dark: #ff6b4a;
    --color-accent:    #6d93ff;
    --color-activity-accent: #888888;
    --color-link-hover: #ffffff;
    --color-heading:   #ffffff;
    --color-text:      #cccccc;
    --color-text-muted: #888888;
    --color-border:    #333333;
    --color-border-light: #444444;
    --color-danger:    #ff6b6b;
    --color-success:   #3ddc73;
    --color-warning:   #f1c40f;
    --color-primary-tint: rgba(227, 53, 13, 0.18);
    --color-danger-tint:  rgba(255, 107, 107, 0.18);
}

/* Fire Red is the boldest of the 3 — the reference mockup paints the outer
   header/sidebar/main cards themselves a deep red (not just a red accent on
   a neutral dark card like the other two themes), with an even darker red
   banner shade for group-titles/the stats ribbon. */
:root[data-theme="fire"] {
    --color-bg:        #0a0a0a;
    --color-surface:   #b30000;
    --color-surface-banner: #800000;
    --color-panel:     #1a1515;
    --color-primary:   #ff6600;
    --color-primary-dark: #ff9999;
    --color-accent:    #ff9999;
    --color-activity-accent: #ff6600;
    --color-link-hover: #ff9999;
    --color-heading:   #ffffff;
    --color-text:      #dddddd;
    --color-text-muted: #cc9999;
    --color-border:    #590000;
    --color-border-light: #ff4d4d;
    --color-danger:    #ff4d4d;
    --color-success:   #3ddc73;
    --color-warning:   #f1c40f;
    --color-primary-tint: rgba(255, 102, 0, 0.18);
    --color-danger-tint:  rgba(255, 77, 77, 0.18);
}

/* PokeLegacy — the warm amber/cream palette from login.php/register.php's
   "bento grid" design (assets/css/auth.css), offered here as a 4th selectable
   theme for the logged-in app too. Light like the default theme (white/cream
   surfaces, dark text) rather than dark like Fire Red/Dark, so --color-bg is
   the cream (not black) and --color-surface is white, matching those pages'
   actual colors 1:1 instead of just borrowing the accent hue. */
:root[data-theme="legacy"] {
    --color-bg:        #fffbeb;
    --color-surface:   #ffffff;
    --color-surface-banner: #fef3c7;
    --color-panel:     #fffbeb;
    --color-primary:   #fbbf24;
    --color-primary-dark: #f59e0b;
    --color-accent:    #78350f;
    --color-activity-accent: #b45309;
    --color-link-hover: #f59e0b;
    --color-heading:   #78350f;
    --color-text:      #44403c;
    --color-text-muted: #6b7280;
    --color-border:    #fef3c7;
    --color-border-light: #fde68a;
    --color-danger:    #dc2626;
    --color-success:   #16a34a;
    --color-warning:   #f1c40f;
    --color-primary-tint: rgba(251, 191, 36, 0.16);
    --color-danger-tint:  rgba(220, 38, 38, 0.12);
}

/* "Fresh" — the visual refresh built for the public homepage (2026-07-22),
   offered here as a 5th selectable theme for the logged-in app instead of
   replacing the existing default outright. A real Pokemon red/gold pairing
   (unrelated blue accent swapped for a legible gold-amber that works as both
   text AND a fill/border color — see every var(--color-accent) consumer
   across this file) plus a warm neutral background instead of the default
   theme's cold grey-blue. Deliberately reversible: switching back to any
   other theme (including "Light", the pre-refresh default) instantly
   reverts everything, nothing is deleted — see settings.php's Theme card. */
:root[data-theme="refresh"] {
    --color-bg:        #f7f4ee;
    --color-surface:   #ffffff;
    --color-surface-banner: #fff1d6;
    --color-panel:     #fbf9f5;
    --color-primary:   #ee1c25;
    --color-primary-dark: #b3121a;
    --color-accent:    #c98a00;
    --color-activity-accent: #c98a00;
    --color-link-hover: #ee1c25;
    --color-heading:   #1f2a44;
    --color-text:      #3c4657;
    --color-text-muted: #6b7280;
    --color-border:    #e7e2d8;
    --color-border-light: #d8d0bd;
    --color-danger:    #d92d20;
    --color-success:   #1a7f37;
    --color-warning:   #f1c40f;
    --color-primary-tint: rgba(238, 28, 37, 0.08);
    --color-danger-tint:  #fdecea;
}

/* Display font, "Fresh" theme only (assets/js/app.js sets [data-theme] on
   <html>, same mechanism the other 4 themes use) — the logged-in app's
   .section-title stays "Noto Sans" for every other theme. Loaded via
   includes/layout_header.php's <head> (same Google Fonts link the public
   pages use); text-transform/letter-spacing left alone (still uppercase +
   tracked like every other theme's section titles) so only the typeface
   itself changes, not the whole heading treatment. */
:root[data-theme="refresh"] .section-title {
    font-family: 'Baloo 2', 'Noto Sans', sans-serif;
    font-weight: 700;
}

* {
    box-sizing: border-box;
}

html, body {
    height: 100%;
    margin: 0;
    padding: 0;
}

/* .app-shell below is horizontally centered (max-width + margin:0 auto)
   within the full window width — on a desktop browser with a classic
   (non-overlay) scrollbar, a page whose content fits the viewport (no
   scrollbar) has ~15-17px more width to center within than a page whose
   content needs one, so navigating between a short page and a tall page
   visibly shifts the whole centered shell sideways (reported: map.php,
   which usually needs to scroll, to battle.php, which usually doesn't).
   Reserving the scrollbar's gutter unconditionally — whether or not this
   particular page actually needs to scroll — keeps the available width
   identical across every page, so nothing shifts. overflow-y: scroll
   (rather than the newer scrollbar-gutter: stable) since it needs to work
   on every browser this project already targets, not just recent ones. */
html {
    overflow-y: scroll;
}

body {
    font-family: "Noto Sans", sans-serif;
    font-size: 16px;
    color: var(--color-text);
    background: var(--color-bg);
    -webkit-tap-highlight-color: transparent;
    overscroll-behavior-y: none;
}

a {
    color: var(--color-accent);
    transition: color 0.2s ease;
}

a:hover {
    color: var(--color-link-hover);
}

/* Desktop-only 80% zoom — the player found the site "much cleaner" at 80%
   browser zoom, so this reproduces that directly for every visitor rather
   than a per-element font-size/icon-size chase (which can't reach every
   padding/gap the way real zoom does, and stacks badly with it). `zoom`
   actually reflows the subtree at the smaller scale (unlike
   `transform: scale()`, which just visually shrinks the box without
   updating layout or JS's getBoundingClientRect() reads — that would
   desync assets/js/app.js's tooltip positioning math), so this is the same
   mechanism as manually zooming, just applied automatically. Scoped to
   desktop only — mobile already has its own carefully-tuned separate
   sizing via the max-width:768px breakpoints throughout this file and
   shouldn't get any smaller. */
@media (min-width: 769px) {
    body {
        zoom: 0.8;
    }
}

/* ---------------------------------------------------------------------- */
/* App shell                                                               */
/* ---------------------------------------------------------------------- */

/* .has-sidebar (set on .app-shell by layout_header.php) marks the pages
   that actually render the sidebar/bottom nav — auth pages (login/register)
   and the pre-starter shell reuse plain .app-shell/.app-main (no flex, no
   padding) without a nav, so they must NOT get pulled into this layout. */
.app-shell {
    max-width: var(--shell-max-width);
    margin: 0 auto;
    min-height: 100vh;
    min-height: 100dvh;
    background: var(--color-bg);
}

.app-shell.has-sidebar {
    display: flex;
    flex-direction: column;
    gap: 20px;
    padding: 20px 15px;
}

/* The left sidebar / main content / right sidebar row — a genuine flexbox
   row (not the old position:fixed-sidebar + margin-offset-content hack), so
   the 3 columns lay out exactly like the reference mockup: same document
   flow, same "sidebar height stretches to match main" default flex
   behavior, no manual margin math to keep in sync with --sidebar-width. */
.app-content-row {
    display: flex;
    flex: 1;
    gap: 20px;
    min-height: 0;
}

/* Groups the logo, Stats/Headlines tabs, and Quick Actions into one
   cohesive bar (same "one bordered container" language as
   .app-sidebar-left/.app-sidebar-right below) — includes/layout_header.php
   wraps all three in this div. The 3 sections are divided by a border-left
   between them (see .header-box below) rather than each being its own
   separately-boxed card; overflow:hidden keeps the top accent gradient
   (::before) and each section's content from poking past the rounded
   corners. */
.app-header-group {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 0;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.08);
    overflow: hidden;
    color: var(--color-heading);
}

/* Thin brand-gradient accent along the header's top edge — the single
   "this app has a personality" touch on an otherwise neutral shell.
   overflow:hidden on the parent clips it to the outer radius. */
.app-header-group::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 3px;
    background: linear-gradient(90deg, var(--color-primary), var(--color-accent));
}

/* The only child of .app-header-group now (no more stats ribbon below it),
   so it doesn't need its own border-bottom divider. Three sections side by
   side: Logo / Stats+Headlines tabs / Quick Actions — see includes/
   layout_header.php's header-box-logo/-stats/-links. Each is divided from
   the next by a border-left rather than being its own separately-boxed
   card, so the header reads as one cohesive bar instead of 3 nested boxes. */
.app-header {
    display: flex;
    align-items: stretch;
    gap: 0;
    padding: 15px 8px;
    z-index: 10;
}

.header-box {
    padding-left: 22px;
    padding-right: 22px;
}

.header-box + .header-box {
    border-left: 1px solid var(--color-border);
}

.header-box-logo {
    flex: 0 0 240px;
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 0;
}

.app-header-logo {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 0;
    width: 100%;
}

.app-header-logo img {
    height: 176px;
    width: auto;
    max-width: 100%;
    object-fit: contain;
}

.header-box-stats {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    padding-top: 10px;
    padding-bottom: 10px;
    gap: 8px;
}

/* Grid instead of flex so .header-tabs can sit truly centered in the row
   (grid-column 2 of 3, justify-self:center) while .header-tab-badges stays
   pinned to the right (grid-column 3, justify-self:end) independent of
   each group's own width — a plain flex row can't do "one group centered,
   the other pinned to an edge" without one pushing the other off-center. */
.header-tab-row {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: 12px;
}

/* Segmented pill control rather than an underlined-tab look — the active
   tab gets its own filled pill (matching .theme-btn.active's language
   elsewhere) instead of just a bottom border, which reads as more of a
   deliberate switch and less like an afterthought row of text links. */
.header-tabs {
    grid-column: 2;
    justify-self: center;
    display: flex;
    gap: 2px;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: 999px;
    padding: 3px;
}

.header-tab {
    display: flex;
    align-items: center;
    gap: 6px;
    background: none;
    border: none;
    border-radius: 999px;
    padding: 5px 14px;
    font-size: 0.85rem;
    font-weight: 700;
    color: var(--color-text-muted);
    cursor: pointer;
    transition: background-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
}

.header-tab:hover {
    color: var(--color-heading);
}

.header-tab.active {
    color: var(--color-primary-dark);
    background: var(--color-surface);
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}

.header-tab-badges {
    grid-column: 3;
    justify-self: end;
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
}

.header-badge {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: 0.8rem;
    font-weight: 700;
    color: var(--color-text-muted);
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: 999px;
    padding: 3px 9px;
    white-space: nowrap;
    text-decoration: none;
    transition: border-color 0.15s ease, color 0.15s ease;
}

a.header-badge:hover {
    border-color: var(--color-primary);
    color: var(--color-primary-dark);
}

.header-badge-admin {
    color: var(--color-primary-dark);
}

/* Currency-flavored accents (2026-07-22, player feedback: the badges were
   all the same flat grey pill regardless of what they represented) — low-
   alpha tinted background + a matching border, text color left alone
   (still var(--color-text-muted) from the base rule) so contrast stays
   safe across all 5 themes rather than hardcoding a text color that might
   only work against a light surface. */
.header-badge-silver {
    background: rgba(148, 163, 184, 0.14);
    border-color: rgba(148, 163, 184, 0.55);
}

.header-badge-gold {
    background: rgba(234, 179, 8, 0.14);
    border-color: rgba(234, 179, 8, 0.55);
}

.header-badge-mail {
    background: var(--color-primary-tint);
    border-color: var(--color-primary);
}

.header-badge-daynight {
    background: rgba(129, 140, 248, 0.12);
    border-color: rgba(129, 140, 248, 0.45);
}

#header-clock {
    font-variant-numeric: tabular-nums;
}

/* Reserves the real time's width up front (2026-07-27 perf pass) — the
   server-rendered placeholder is "--:--:--" (assets/js/app.js replaces it
   on load), and a hyphen is narrower than a tabular digit in this font even
   though tabular-nums above already keeps digits equal-width to each
   other. Without this, swapping the placeholder for a real time widens the
   pill by ~15-20px, which (in the Modern header's auto-sized badge grid,
   see .header-modern-badge-grid below) slides every badge after it
   sideways one frame after paint. */
#header-clock-text {
    display: inline-block;
    min-width: 8ch;
    text-align: center;
}

.header-tab-panel[hidden] {
    display: none;
}

/* Both tabs share one floor height (tall enough for 7 Headlines rows, the
   most either tab needs) so switching tabs never changes the header's
   height. Both are flex columns with justify-content:center, so a shorter
   block of content (e.g. fewer than 7 real headlines) sits vertically in
   the middle of the box instead of stuck at the top — this centers the
   BLOCK as a whole, not each row's own text (see .global-activity-item
   below, which stays left-aligned so every row starts at the same
   position). */
.header-tab-panel {
    min-height: 225px;
    display: flex;
    flex-direction: column;
    justify-content: center;
}

.header-tab-panel[data-header-panel="headlines"] .global-activity-list,
.header-tab-panel[data-header-panel="headlines"] .global-activity-empty {
    flex: 1;
    /* No scrollbar — 7 rows are sized to fit within the shared min-height
       above; the list just grows to fit them instead of clipping/scrolling. */
    overflow-y: visible;
}

/* Headlines rows read a little cramped at the base .global-activity-item
   size (shared with activity_log.php's full page and pvp.php's History
   card, which stay untouched) — scoped up slightly just for this panel.
   Deliberately NOT centered — every row's icon/text starts at the same
   left position regardless of how long its text is. */
.header-tab-panel[data-header-panel="headlines"] .global-activity-item {
    font-size: 0.82rem;
    padding: 4px 0;
}

.header-tab-panel[data-header-panel="headlines"] .global-activity-time {
    font-size: 0.75rem;
}

/* Bars (left) and the Silver/Gold/Messages/Status table (right) split the
   row 50/50 and center as a group, rather than stacking. */
.header-stats-row {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 16px;
}

.header-stat-bars {
    flex: 0 0 50%;
    min-width: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 6px;
}

.header-stat-bar-label {
    display: flex;
    justify-content: space-between;
    font-size: 0.82rem;
    font-weight: 700;
    color: var(--color-text-muted);
}

.header-stat-bar-label span:first-child {
    display: inline-flex;
    align-items: center;
    gap: 5px;
}

.header-stat-bar-label .ui-icon {
    height: 1em;
    width: 1em;
}

.header-stat-bar-track {
    height: 8px;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: 999px;
    overflow: hidden;
}

.header-stat-bar-fill {
    height: 100%;
    border-radius: 999px;
    background: var(--color-accent);
    transition: width 0.4s ease;
}

.header-stat-bar-fill--hp {
    background: var(--color-success);
}

.header-stat-bar-fill--dex {
    background: var(--color-primary);
}

/* 2x2 grid of clickable stat chips, replacing what used to be a plain
   <table> — same 4 destinations (Silver/Gold/Messages/Status), but each
   reads as a small tile with its own icon rather than a spreadsheet row. */
.header-stat-chips {
    flex: 0 0 50%;
    min-width: 0;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 6px;
}

.header-stat-chip {
    display: flex;
    align-items: center;
    gap: 8px;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    padding: 6px 10px;
    cursor: pointer;
    transition: border-color 0.15s ease, transform 0.15s ease;
}

.header-stat-chip:hover {
    border-color: var(--color-primary);
    transform: translateY(-1px);
}

.header-stat-chip-icon {
    flex: 0 0 auto;
    width: 20px;
    height: 20px;
    object-fit: contain;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.header-stat-chip-icon .ui-icon {
    height: 20px;
    width: 20px;
}

.header-stat-chip-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
    line-height: 1.15;
}

.header-stat-chip-value {
    font-size: 0.9rem;
    font-weight: 700;
    color: var(--color-heading);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.header-stat-chip-label {
    font-size: 0.7rem;
    color: var(--color-text-muted);
}

.header-more-headlines {
    align-self: flex-end;
    font-size: 0.8rem;
    font-weight: 700;
    color: var(--color-primary-dark);
    text-decoration: none;
    margin-top: 4px;
}

/* Inside .header-modern-ticker's flex ROW (unlike the Classic layout's
   Headlines panel above, a flex COLUMN, where this already sits alone on
   its own line) this is a shrinkable flex child by default — at mobile
   widths it was being squeezed narrow enough that "More →" wrapped onto two
   lines. */
.header-modern-ticker .header-more-headlines {
    flex-shrink: 0;
    white-space: nowrap;
    align-self: center;
}

.header-more-headlines:hover {
    text-decoration: underline;
}

.header-box-links {
    flex: 0 0 300px;
    min-width: 0;
    padding-top: 10px;
    padding-bottom: 10px;
    display: flex;
    flex-direction: column;
}

.header-box-links h3 {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 0 0 8px;
    padding-bottom: 6px;
    border-bottom: 1px solid var(--color-border);
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-heading);
}

.header-box-links h3 .ui-icon {
    height: 1em;
    width: 1em;
}

/* Icon-first "app launcher" tiles rather than a 2-column list of bordered
   text buttons — the same destinations, but the icon carries the row now
   (label shrinks underneath) so the box reads as inviting shortcuts rather
   than a cramped spreadsheet of links. 5 columns keeps the box roughly the
   same height as the Stats column next to it. */
.quick-links-grid {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: 6px;
    flex: 1;
}

.quick-link-tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    padding: 8px 2px;
    text-decoration: none;
    color: var(--color-text);
    text-align: center;
    transition: transform 0.15s ease, border-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
}

.quick-link-icon {
    display: inline-flex;
}

.quick-link-icon .ui-icon,
.quick-link-icon .nav-icon-img,
.quick-link-icon .nav-avatar-icon {
    width: 22px;
    height: 22px;
}

.quick-link-label {
    font-size: 0.66rem;
    font-weight: 700;
    line-height: 1.1;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}

.quick-link-tile:hover {
    transform: translateY(-2px);
    border-color: var(--color-primary);
    color: var(--color-primary-dark);
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
}

/* ---------------------------------------------------------------------- */
/* Modern header (admin-selectable alternative — Admin > Game Settings >   */
/* Layout > "Use Modern Header Layout", see includes/layout_header.php's   */
/* $useModernHeader). One compact row: brand, an always-visible Live       */
/* ticker (instead of the Classic layout's Stats/Headlines tab switch),    */
/* then a mini Active Party grid paired with the currency/clock/admin      */
/* badges. Shares .app-header/.header-badge/.currency-icon/.ui-icon/       */
/* .global-activity-time with the Classic rules above rather than          */
/* redefining them.                                                        */
/* ---------------------------------------------------------------------- */

.app-header--modern {
    display: flex;
    flex-direction: column;
    gap: 10px;
    width: 100%;
    /* No left padding (2026-07-22) — .app-header-group and .app-content-row
       start at the exact same x (both direct children of .app-shell), but
       .app-sidebar-left/.app-main sit flush against .app-content-row's own
       edge with no extra inset before them. Padding here would offset
       .header-modern-row's grid columns (below) from that shared start
       point, undoing the alignment the grid is for. .header-modern-brand
       gets its own left padding instead, for breathing room that doesn't
       move the actual column boundary. */
    padding: 18px 24px 18px 0;
}

/* Real 2-column grid (2026-07-22, player request), matching
   .app-content-row's own sidebar/main split exactly rather than
   approximating it with padding on a single flex row. Column 1 is
   var(--sidebar-width) — the SAME variable .app-sidebar-left itself uses,
   so the logo column's right edge is always exactly the sidebar's right
   edge, not just visually close. Column 2 (.header-modern-content) picks
   up everything the logo column doesn't need, and its own left edge lands
   exactly on .app-main's left edge for the same reason. A padding-based
   approach was tried first and rejected: it moved the ROW's start point,
   but the logo's own rendered width still ate into where the ticker
   visually began, so the ticker itself never actually lined up with
   .app-main — only the row's outer edge did. */
.header-modern-row {
    display: grid;
    grid-template-columns: var(--sidebar-width) 1fr;
    align-items: center;
    gap: 20px;
}

.header-modern-brand {
    display: flex;
    align-items: center;
    padding-left: 24px;
}

.header-modern-content {
    display: flex;
    align-items: center;
    gap: 20px;
    flex-wrap: wrap;
    min-width: 0;
}

/* Real logo size (2026-07-16, was 42px — read as an icon, not a logo),
   roughly matching the Classic layout's own 88px .app-header-logo img so
   the two layouts carry the same brand presence. */
.header-modern-brand img {
    height: 76px;
    width: auto;
    max-width: 220px;
    object-fit: contain;
}

/* Mini Active Party (left) and the currency/time badges (right) as a single
   matched pair of small "tables" — both grids are 3 columns wide so a full
   6-Pokemon party (2 rows of 3) and the 5-6 badges (also 2 rows of 3) line
   up at the same height, divided by one rule instead of reading as two
   unrelated clusters. */
.header-modern-side {
    display: flex;
    align-items: center;
    gap: 18px;
}

.header-modern-side > * + * {
    border-left: 1px solid var(--color-border);
    padding-left: 18px;
}

.header-modern-badge-grid {
    display: grid;
    grid-template-columns: repeat(3, auto);
    gap: 8px;
}

/* Scoped bump over the base .header-badge (shared with the Classic layout,
   left untouched there) — matches the bigger logo/party sizing here without
   resizing Classic's own currency/clock/admin badges. */
.header-modern-badge-grid .header-badge {
    justify-content: center;
    font-size: 0.88rem;
    padding: 5px 11px;
}

.header-modern-badge-grid .currency-icon {
    width: 18px;
    height: 18px;
}

/* Mini Active Party grid — replaces what used to be a Quick Actions button
   row in this spot (removed 2026-07-16: every destination it linked to is
   already reachable from the sidebars/mobile bottom bar, so it wasn't worth
   the space next to something more useful). Each slot is a real sprite +
   level + a slim HP bar, using the same lightweight per-Pokemon query as
   the Classic layout's aggregate Team HP bar (see header_party_snapshot()
   in includes/header_stats.php) rather than the heavier full-Pokemon
   pipeline index.php's own Active Party card uses. */
.header-modern-party {
    display: grid;
    grid-template-columns: repeat(3, auto);
    gap: 8px;
}

.header-modern-party-slot {
    display: flex;
    align-items: center;
    gap: 6px;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    /* Primary-type accent (includes/layout_header.php sets --type-accent
       inline via pokemon_type_color()) — same idea as .party-mini-card's
       top accent, a left bar here instead since these slots are wide and
       short rather than tall, 2026-07-22 player feedback that the header's
       own party slots read as an afterthought next to everything else. */
    border-left: 3px solid var(--type-accent, var(--color-border));
    border-radius: var(--radius-sm);
    padding: 5px 12px 5px 8px;
    text-decoration: none;
    color: var(--color-text);
    transition: border-color 0.15s ease, transform 0.15s ease;
}

.header-modern-party-slot:hover {
    border-color: var(--color-primary);
    transform: translateY(-1px);
}

.header-modern-party-sprite {
    width: 42px;
    height: 42px;
    object-fit: contain;
    image-rendering: pixelated;
    flex: 0 0 auto;
}

.header-modern-party-info {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 46px;
}

.header-modern-party-level {
    font-size: 0.7rem;
    font-weight: 700;
    color: var(--color-text-muted);
    line-height: 1;
}

.header-modern-party-hp-track {
    width: 100%;
    height: 5px;
    background: var(--color-surface);
    border-radius: 999px;
    overflow: hidden;
}

.header-modern-party-hp-fill {
    height: 100%;
    background: var(--color-success);
}

.header-modern-party-hp-fill--warning {
    background: var(--color-warning);
}

.header-modern-party-hp-fill--danger {
    background: var(--color-danger);
}

/* Sits between the logo and the party/badges panel in .header-modern-row
   now (moved 2026-07-16, previously a full-width block below the row) —
   flex:1 1 auto + min-width:0 lets it both claim the row's remaining space
   and actually shrink, so its own overflow-y scroll (not the row) is what
   handles a long Headlines list. */
.header-modern-ticker {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    flex: 1 1 auto;
    min-width: 0;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    /* A thin accent instead of the same flat grey border every other box
       here uses — was previously the single largest, plainest element in
       the header (2026-07-22 feedback); this and the shrunk max-height
       below give it a bit less visual dominance without hiding anything. */
    border-left: 3px solid var(--color-activity-accent);
    border-radius: var(--radius-sm);
    padding: 8px 14px;
    font-size: 0.85rem;
}

.header-modern-ticker-label {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding-top: 2px;
    font-weight: 700;
    color: var(--color-text-muted);
    text-transform: uppercase;
    font-size: 0.7rem;
    letter-spacing: 0.03em;
}

/* Row by row (one Headline per line), same as the Classic layout's
   Headlines tab — NOT the left-to-right scrolling ticker this briefly
   became. Height-capped and vertically scrollable so a long list doesn't
   balloon the header; the default thin scrollbar (see this file's
   ::-webkit-scrollbar rules near the top) is left visible rather than
   hidden, since a vertical scrollbar is a familiar, expected affordance
   here (unlike a horizontal one). Capped taller than before (92px, up from
   76px) to match the row's now-bigger overall height. */
.header-modern-ticker-track {
    flex: 1 1 auto;
    min-width: 0;
    max-height: 78px;
    display: flex;
    flex-direction: column;
    gap: 4px;
    overflow-y: auto;
}

.header-modern-ticker-item {
    flex: 0 0 auto;
    width: 100%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    color: var(--color-text-muted);
}

.header-modern-ticker-item .global-activity-time {
    color: var(--color-activity-accent);
    font-variant-numeric: tabular-nums;
    margin-right: 2px;
}

@media (max-width: 768px) {
    /* No sidebar exists on mobile (.app-content-row itself stacks to a
       single column here), so the desktop 2-column grid has nothing left
       to align with — collapse back to a single stacked column. */
    /* The desktop padding is deliberately asymmetric (`18px 24px 18px 0`):
       there, the logo grid column supplies the left inset. On mobile that
       column is gone, so the ticker/party/badges started flush against the
       left edge with a 24px gap only on the right — the header read as
       shoved to the left (player-reported 2026-09-20). Symmetric padding
       here instead. */
    .app-header--modern {
        padding: 14px;
    }

    .header-modern-row {
        display: flex;
        flex-direction: column;
        align-items: stretch;
    }

    /* The collapse strip above the header (.app-header-mobile-toggle) already
       shows the logo on mobile, so this in-header copy made the same logo
       appear twice, one above the other (player-reported 2026-09-20 — easy
       to miss before the 2026-09-19 design pass gave dark themes a light
       plate behind it). The toggle's copy is the one that stays: it's the
       tap target that collapses/expands the header. Classic header's own
       logo box (.header-box-logo) is hidden for the same reason. */
    .header-modern-brand,
    .header-box-logo {
        display: none;
    }

    .header-modern-brand img {
        height: 56px;
    }

    .header-modern-content {
        flex-direction: column;
        align-items: stretch;
    }

    .header-modern-ticker {
        width: 100%;
    }

    /* The party/badges divider reads as a rule between two side-by-side
       columns at desktop widths; stacked on mobile, a left border no longer
       makes sense — swap it for a top border between the two rows instead. */
    .header-modern-side {
        flex-direction: column;
        align-items: stretch;
    }

    .header-modern-side > * + * {
        border-left: none;
        border-top: 1px solid var(--color-border);
        padding-left: 0;
        padding-top: 10px;
    }

    /* Wrapping flex rows rather than a 3-column grid (2026-09-20): a grid
       left-aligns whatever is left over on its last row, so a 5-Pokemon
       party (or 5 badges) read as "stuck to the left" of the header on a
       phone (player-reported). Flex keeps the same 3-up sizing but centers
       the leftover row. The explicit flex-basis + min-width:0 below carry
       over the old minmax(0, 1fr) "grid blowout" fix (same problem
       .pc-box-grid's own comment documents elsewhere in this file): a track
       that won't shrink below its content's min-content width made the 3rd
       column of fixed-size party-slot/badge content overflow its card at
       ~320-360px and get silently clipped by .card's overflow:hidden. */
    .header-modern-party,
    .header-modern-badge-grid {
        display: flex;
        flex-wrap: wrap;
        justify-content: center;
        gap: 8px;
    }

    .header-modern-party > *,
    .header-modern-badge-grid > * {
        flex: 0 1 calc((100% - 16px) / 3);
        min-width: 0;
    }

    /* The slot itself must also be allowed to shrink (a flex child's
       default min-width:auto would otherwise re-introduce the same
       overflow one level down) and its fixed-size children shrunk to
       actually fit a minmax(0,1fr) column at these widths. */
    .header-modern-party-slot {
        min-width: 0;
        padding: 5px 6px;
        gap: 4px;
    }

    .header-modern-party-sprite {
        width: 30px;
        height: 30px;
    }

    .header-modern-party-info {
        min-width: 0;
        flex: 1 1 auto;
    }

    .header-modern-badge-grid .header-badge {
        padding: 5px 6px;
        font-size: 0.78rem;
    }
}

/* Mobile-only bar that replaces the full header (the 3 header-box columns)
   with a single collapsible strip — desktop never shows this at all (see
   .app-header above, which stays visible unconditionally on desktop
   widths). Hidden by default here; .app-header itself is what actually
   gets hidden/shown on mobile, driven by the .header-collapsed class
   assets/js/app.js toggles on .app-header-group when this button is
   clicked. */
.app-header-mobile-toggle {
    display: none;
    width: 100%;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    background: none;
    border: none;
    padding: 10px 16px;
    cursor: pointer;
    color: var(--color-heading);
}

.app-header-mobile-toggle-logo {
    height: 48px;
    width: auto;
    max-width: 220px;
    object-fit: contain;
}

.app-header-mobile-toggle-caret {
    font-size: 0.9rem;
    opacity: 0.8;
    transition: transform 0.2s ease;
}

.app-header-group.header-collapsed .app-header-mobile-toggle-caret {
    transform: rotate(180deg);
}

/* Reusable standalone version of the same icon (empty states, etc.) */
.pokeball-icon {
    display: inline-block;
    width: 1em;
    height: 1em;
    border-radius: 50%;
    background:
        radial-gradient(circle at center, #fff 0 16%, #1c1c1e 16% 22%, transparent 22%),
        linear-gradient(to bottom, #e3350d 0 47%, #1c1c1e 47% 53%, #fff 53% 100%);
    border: 2px solid #1c1c1e;
    vertical-align: middle;
}

/* Bill's PC — the red storage-system console from the games, drawn in CSS
   (same technique as .pokeball-icon above) rather than hotlinked, since
   PokeAPI's item sprite set only covers bag items and has no PC/console
   sprite to pull from. */
.pc-icon {
    display: inline-block;
    width: 1em;
    height: 1em;
    position: relative;
    vertical-align: middle;
    background: #1c1c1e;
    border-radius: 1px;
}
.pc-icon::before {
    content: "";
    position: absolute;
    top: 0;
    left: 6%;
    right: 6%;
    height: 70%;
    background: #e3350d;
    border: 2px solid #1c1c1e;
    border-radius: 2px;
    box-sizing: border-box;
}
.pc-icon::after {
    content: "";
    position: absolute;
    top: 16%;
    left: 20%;
    right: 20%;
    height: 34%;
    background: #1c1c1e;
    border-radius: 1px;
    box-shadow: inset 0 0 0 1px #6be675;
}

/* activity_log.php's full list (and any other standalone reuse of
   .global-activity-list, e.g. pvp.php's History card) — .global-activity-list
   defaults to display:none since the header's own Headlines tab panel
   (includes/layout_header.php) sets display:block inline itself rather than
   via a class, so this modifier is what makes it visible everywhere else.
   Also unlike the header's tab panel, this one isn't height-clamped — the
   page itself scrolls.
   Compound selector (not just .activity-log-page-list) deliberately, so this
   always wins over .global-activity-list's own display:none below regardless
   of which one ends up later in the file — two single-class selectors of
   equal specificity silently depend on source order, and .global-activity-list's
   base rule being the one that happened to come later here is exactly what
   made this list invisible on activity_log.php despite real rows existing
   in the markup (2026-07-14). */
.global-activity-list.activity-log-page-list {
    display: block;
    max-height: none;
}

.global-activity-text a {
    color: inherit;
    font-weight: 700;
    text-decoration: none;
}

.global-activity-text a:hover {
    text-decoration: underline;
}

.global-activity-empty {
    display: none;
    width: 100%;
    margin: 2px 0 0;
    font-size: 0.85rem;
}

.global-activity-list {
    display: none;
    width: 100%;
    list-style: none;
    margin: 4px 0 0;
    padding: 0 5px 0 0;
    flex: 1 1 auto;
    min-width: 0;
    overflow-y: auto;
}

.global-activity-item {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 2px 0;
    font-size: 0.6875rem;
    line-height: 1.2;
    white-space: nowrap;
    overflow: hidden;
}

.global-activity-time {
    flex: 0 0 auto;
    color: var(--color-activity-accent);
    font-variant-numeric: tabular-nums;
    font-size: 0.625rem;
}

.global-activity-glyph {
    flex: 0 0 auto;
}

.global-activity-text {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.currency-icon {
    width: 16px;
    height: 16px;
    object-fit: contain;
    vertical-align: middle;
    image-rendering: pixelated;
}

/* Generic inline replacement for what used to be a raw Unicode emoji (see
   includes/sanitize.php's ui_icon()) — em-sized so it scales with whatever
   font-size it's dropped into (button, heading, badge, ...) rather than a
   fixed pixel size like .currency-icon above, which only ever appears in
   one spot at one size. */
.ui-icon {
    height: 1.1em;
    width: 1.1em;
    object-fit: contain;
    vertical-align: -0.2em;
    image-rendering: pixelated;
}

/* Cosmetics Shop's equipped Username Flair (2026-07-27) — a real Pokemon
   item icon (see includes/cosmetics.php's username_flair_icon_url()) shown
   next to a username on profile.php and the players.php rank list. Bumped
   up from .ui-icon's own 1.1em default — this is meant to read as a
   deliberate cosmetic flourish, not a small UI glyph like the rest of that
   class's uses. */
.username-flair-icon {
    height: 1.3em;
    width: 1.3em;
    vertical-align: -0.25em;
    margin-right: 2px;
}

/* Same "one bordered container" card treatment as .app-header-group/the two
   sidebars, rather than a plain unstyled content area — order:0 keeps it
   visually between the two sidebars in .app-content-row regardless of
   where it falls in the DOM (see .app-sidebar-left's own comment). */
.app-main {
    order: 0;
    flex: 1;
    min-width: 0;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.08);
    padding: 20px;
}

.app-main h2 {
    margin-top: 0;
    color: var(--color-heading);
}

/* ---------------------------------------------------------------------- */
/* Bottom nav (mobile only, flat/ungrouped) vs. the two categorized        */
/* sidebars (desktop only, see .app-sidebar-left/.app-sidebar-right        */
/* below) — three separate elements now, all built from the same          */
/* $navOrder/$allNavItems in includes/layout_footer.php.                   */
/* ---------------------------------------------------------------------- */

.app-footer {
    position: fixed;
    top: auto;
    bottom: 0;
    left: 0;
    width: 100%;
    height: var(--footer-height);
    display: flex;
    flex-direction: row;
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    background: var(--color-surface);
    border-top: 1px solid var(--color-border);
    box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08);
    z-index: 100;
}

.app-footer .nav-item,
.app-sidebar-left .nav-item,
.app-sidebar-right .nav-item,
.mobile-nav-more-grid .nav-item {
    flex: 0 0 auto;
    width: auto;
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    gap: 14px;
    padding: 10px 14px;
    border-radius: var(--radius);
    font-size: 0.92rem;
    font-weight: 400;
    color: var(--color-text-muted);
    text-decoration: none;
    background: none;
    border: none;
    min-height: 44px;
    cursor: pointer;
    font-family: inherit;
}

.app-footer .nav-item:hover,
.app-sidebar-left .nav-item:hover,
.app-sidebar-right .nav-item:hover {
    background: var(--color-primary-tint);
    color: var(--color-primary);
}

.app-footer .nav-item.active,
.app-sidebar-left .nav-item.active,
.app-sidebar-right .nav-item.active {
    background: var(--color-primary-tint);
    color: var(--color-primary);
    font-weight: 700;
}

/* Desktop sidebars only (not the mobile footer bar, which keeps its own
   centered icon-over-label pill look): rows read as a divided list rather
   than a stack of rounded pills, with a small hover/active slide-right —
   same visual language as .sidebar-group-title's banner below. Same
   selector specificity as the shared .app-footer/.app-sidebar-* rule above
   (two classes each), so source order alone decides the winner — this must
   stay after it to actually override border-radius/transition per side. */
.app-sidebar-left .nav-item,
.app-sidebar-right .nav-item {
    border-radius: 0;
    border-left: 3px solid transparent;
    border-bottom: 1px solid var(--color-border);
    color: var(--color-text);
    transition: background-color 0.2s ease, color 0.2s ease, border-left-color 0.2s ease;
}

.app-sidebar-left .sidebar-group:last-child .nav-item:last-child,
.app-sidebar-right .sidebar-group:last-child .nav-item:last-child {
    border-bottom: none;
}

.app-sidebar-left .nav-item:hover,
.app-sidebar-left .nav-item.active,
.app-sidebar-right .nav-item:hover,
.app-sidebar-right .nav-item.active {
    /* --region-accent — set inline per .sidebar-group wrapper (see
       sidebar_group_style() in includes/layout_footer.php): Explore gets a
       dynamic per-region color (region_accent_colors()), every other group
       gets its own fixed color (nav_group_accent_colors()), both in
       includes/sanitize.php. A group with neither falls back to the
       normal theme accent. */
    border-left-color: var(--region-accent, var(--color-primary));
}

/* Visual hierarchy for the ~30-item combined sidebar list (2026-07-22,
   player feedback: it reads as a flat wall of same-weight text) — a
   player's own pinned 5 (same set the mobile bar already prioritizes, see
   nav_class()'s $primaryKeys param in layout_footer.php) get full-contrast
   bold text; everything else steps down to a quieter muted weight so the
   handful of things actually used every session stand out without hiding
   or removing any of the rest. */
.app-sidebar-left .nav-item:not(.nav-item-primary),
.app-sidebar-right .nav-item:not(.nav-item-primary) {
    color: var(--color-text-muted);
    font-weight: 500;
}

.app-sidebar-left .nav-item.nav-item-primary,
.app-sidebar-right .nav-item.nav-item-primary {
    color: var(--color-heading);
    font-weight: 700;
}

.app-footer .nav-item .icon,
.app-sidebar-left .nav-item .icon,
.app-sidebar-right .nav-item .icon {
    font-size: 1.2rem;
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 1.25em;
    flex: 0 0 auto;
}

/* Both sidebars (desktop only) — categorized destinations grouped under a
   heading per includes/nav_items.php's NAV_LEFT_SIDEBAR_GROUPS/
   NAV_RIGHT_SIDEBAR_GROUPS. Hidden by default; mobile keeps every item in
   the single flat .app-footer bottom bar instead (hidden from desktop
   below so the two don't both show at once). */
.app-sidebar-left,
.app-sidebar-right {
    display: none;
}

/* Desktop-only site footer (includes/layout_footer.php) — hidden by
   default since mobile's bottom nav bar (.app-footer) already owns that
   screen real estate, same convention as the sidebars above. The "Install
   App!" column (.site-footer-col-new below) now lives INSIDE this footer's
   normal column grid rather than as its own always-visible row (2026-07-21:
   moved next to the Info column, under a "New" heading, at the player's
   request) — that also means it's desktop-only-visible again, same as
   Game/Community/Info. A logged-in mobile player still installs via
   Chrome's own address-bar/menu install affordance; a logged-out visitor
   on any device still sees the button via .public-footer below, which
   (unlike this footer) has no bottom nav bar to fall back to and so was
   never hidden on mobile in the first place. */
.site-footer {
    display: none;
}

@media (min-width: 769px) {
    .app-footer {
        display: none;
    }

    /* Safety net only — the trigger button lives inside .app-footer above
       (already hidden here), so this sheet has no way to open on desktop in
       normal use. Guards just the edge case of it being left open on a
       resize down from mobile. */
    .mobile-nav-more-overlay {
        display: none;
    }

    .site-footer {
        display: block;
    }

    /* Normal flex children of .app-content-row now (not position:fixed) —
       matches the reference mockup exactly: a real 3-column flex row, so
       the sidebar's own height/scroll is whatever the row's tallest column
       naturally is, rather than independently pinned/scrolled against the
       viewport. .app-sidebar-left renders after .app-main in the DOM (see
       includes/layout_footer.php) since the page's own content has to sit
       inside <main>, between layout_header.php and layout_footer.php) — the
       order values below put it back on the visual left regardless. */
    .app-sidebar-left,
    .app-sidebar-right {
        display: flex;
        flex-direction: column;
        gap: 0;
        width: var(--sidebar-width);
        flex-shrink: 0;
        background: var(--color-surface);
        border: 1px solid var(--color-border);
        border-radius: var(--radius);
        box-shadow: 0 4px 18px rgba(0, 0, 0, 0.08);
        padding: 20px 12px;
        overflow: hidden;
    }

    .app-sidebar-left {
        order: -1;
    }

    .app-sidebar-right {
        order: 1;
    }

    /* Per the .sidebar-group-icon comment below: on desktop the one group
       heading icon is enough context for the whole section, so individual
       row icons are redundant clutter — hide them here (desktop only; the
       mobile .app-footer bar is untouched and keeps every item's icon). */
    .app-sidebar-left .nav-item .icon,
    .app-sidebar-right .nav-item .icon {
        display: none;
    }
}

/* Desktop-only site footer (includes/layout_footer.php) — same "bordered
   surface card" language as .app-header-group/.app-sidebar-left above
   (border + radius + shadow), sitting as its own row below
   .app-content-row in .app-shell.has-sidebar's flex column (picks up that
   parent's 20px gap automatically, no margin needed here). Informational
   links only (About/Rules/Privacy, Forum/Dev Reports, Changelog/Rankings/
   Activity Log) — none of this duplicates the reorderable player nav in
   the sidebars/mobile bar above, so it stays out of nav_items.php entirely. */
.site-footer {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.08);
    padding: 20px 24px;
    color: var(--color-heading);
}

.site-footer-columns {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 28px;
}

.site-footer-col {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    min-width: 160px;
    text-align: center;
}

.site-footer-col-title {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-heading);
    margin-bottom: 4px;
}

.site-footer-col a {
    font-size: 0.85rem;
    color: var(--color-text-muted);
}

.site-footer-col a:hover {
    color: var(--color-link-hover);
}

.site-footer-bottom {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 6px 16px;
    margin-top: 18px;
    padding-top: 14px;
    border-top: 1px solid var(--color-border);
    font-size: 0.78rem;
    color: var(--color-text-muted);
    text-align: center;
}

/* "Install App!" PWA button — a normal .site-footer-col like Game/
   Community/Info, sitting right next to the Info column (see
   includes/layout_footer.php / includes/public_layout_footer.php), just
   with a button in place of that column's usual list of links. Shown via
   assets/js/pwa-install.js only once the browser actually fires
   beforeinstallprompt (Firefox/Safari, an already-installed Chrome, and a
   page already running in standalone/installed display mode never do, so
   this stays hidden there rather than offering a button that does
   nothing or nothing-to-do). */
.pwa-install-btn {
    display: none;
    width: auto;
    gap: 8px;
}

.pwa-install-btn img {
    width: 20px;
    height: 20px;
}

/* index.php's homepage banner for the PWA-install Master Ball reward — a
   distinct accent border/background so it reads as a promo, not just
   another info card, matching the "really visible" ask it was built for. */
.pwa-reward-banner {
    border: 1px solid var(--color-accent);
    background: rgba(59, 108, 224, 0.08);
}

/* index.php's "Follow Us" section — same auto-fit card-grid convention as
   .info-grid/.admin-hub-grid, just narrower cells since each card is only
   an icon badge + one word. */
.social-links-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: 12px;
}

.social-link-card {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    text-decoration: none;
    color: inherit;
    transition: border-color 0.15s ease;
}

.social-link-card:hover {
    border-color: var(--color-accent);
}

.social-link-mono {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    flex: 0 0 auto;
    color: #fff;
    font-weight: 700;
    font-size: 0.85rem;
}

.social-link-mono-discord {
    background: #5865f2;
}

.social-link-mono-instagram {
    background: radial-gradient(circle at 30% 110%, #fdf497, #fd5949 45%, #d6249f 60%, #285aeb 90%);
}

.social-link-mono-facebook {
    background: #1877f2;
}

.social-link-mono-tiktok {
    background: #010101;
}

.sidebar-group {
    display: flex;
    flex-direction: column;
    gap: 0;
    margin-bottom: 12px;
}

/* Bleeds flush to the sidebar's own edges (the sidebar's 12px side padding
   is cancelled with a matching negative margin) so each group reads as a
   shaded banner spanning the full sidebar width, like a menu section
   header, rather than an indented label floating over the plain sidebar
   background. */
.sidebar-group-title {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-heading);
    /* --region-tint — set inline per .sidebar-group wrapper, same source as
       --region-accent above (see includes/layout_footer.php's
       sidebar_group_style()). */
    background: var(--region-tint, var(--color-surface-banner));
    border-bottom: 1px solid var(--color-border);
    margin: 0 -12px 4px;
    padding: 8px 14px;
}

/* The one icon per desktop sidebar group heading (includes/nav_items.php's
   nav_group_icon_img()) — individual nav-item rows below it are text-only
   on desktop now; the mobile bottom bar (.app-footer) is untouched and
   still shows .nav-icon-img per item. */
.sidebar-group-icon {
    width: 20px;
    height: 20px;
    object-fit: contain;
    image-rendering: pixelated;
    flex: 0 0 auto;
}

/* Theme switcher (settings.php) — sets data-theme on <html> and persists
   it via localStorage (see assets/js/app.js); .theme-btn.active marks
   whichever theme is currently applied, kept in sync on click and on
   page load. */
.theme-switcher {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.theme-switcher .theme-btn {
    flex: 1 1 100px;
}

.theme-btn {
    background: var(--color-panel);
    border: 1px solid var(--color-border);
    color: var(--color-text-muted);
    padding: 8px 6px;
    cursor: pointer;
    font-size: 0.7rem;
    text-transform: uppercase;
    font-weight: 700;
    letter-spacing: 0.03em;
    border-radius: 6px;
    transition: transform 0.1s ease, background-color 0.2s ease, color 0.2s ease;
}

.theme-btn:hover {
    color: var(--color-text);
}

.theme-btn.active {
    background: var(--color-primary-tint);
    border-color: var(--color-primary);
    color: var(--color-primary);
}

.nav-icon-img {
    width: 20px;
    height: 20px;
    object-fit: contain;
    image-rendering: pixelated;
}

.nav-avatar-icon {
    width: 20px;
    height: 20px;
    border-radius: 50%;
    object-fit: cover;
}

/* ---------------------------------------------------------------------- */
/* Cards / layout helpers                                                  */
/* ---------------------------------------------------------------------- */

.card {
    position: relative;
    overflow: hidden;
    background: var(--color-panel);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    padding: 16px;
    margin-bottom: 16px;
}

.stack {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

/* Lets a hover tooltip (.poke-tooltip) escape the card's own overflow:hidden
   clipping (needed to round off the top accent bar) — used on any card
   containing .poke-hoverable rows. */
.overflow-visible {
    overflow: visible;
}

.text-muted {
    color: var(--color-text-muted);
}

/* ---------------------------------------------------------------------- */
/* Buttons                                                                 */
/* ---------------------------------------------------------------------- */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    padding: 0 20px;
    border-radius: var(--radius);
    border: none;
    font-size: 1rem;
    font-weight: 600;
    cursor: pointer;
    background: var(--color-surface);
    color: var(--color-text);
    border: 1px solid var(--color-border);
    transition: transform 0.05s ease, opacity 0.15s ease;
    width: 100%;
    text-decoration: none;
}

.btn:active {
    transform: scale(0.98);
}

.btn:disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

.btn-primary {
    background: var(--color-primary);
    border-color: var(--color-primary);
    color: #fff;
}

.btn-primary:hover:not(:disabled) {
    background: var(--color-primary-dark);
}

.btn-link {
    background: none;
    border: none;
    color: var(--color-accent);
    width: auto;
    min-height: auto;
    padding: 4px;
    font-weight: 600;
}

.form-field {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin-bottom: 16px;
}

.form-field label {
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--color-text-muted);
}

.form-field input {
    min-height: 44px;
    padding: 0 14px;
    font-size: 1rem;
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    background: var(--color-surface);
    color: var(--color-text);
}

.form-field input:focus {
    outline: none;
    border-color: var(--color-accent);
}

.form-error {
    background: var(--color-danger-tint);
    color: var(--color-danger);
    border-radius: var(--radius);
    padding: 10px 14px;
    font-size: 0.9rem;
    margin-bottom: 16px;
    display: none;
}

.form-error.visible {
    display: block;
}

/* Shared toast notifications (assets/js/app.js's window.showToast()) —
   bottom-right on desktop, top of the screen on mobile so it doesn't sit
   under a thumb or the bottom nav bar. */
.toast-container {
    position: fixed;
    z-index: 1000;
    bottom: 20px;
    right: 20px;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 10px;
    pointer-events: none;
}

.toast {
    pointer-events: auto;
    cursor: pointer;
    max-width: 320px;
    background: var(--color-surface);
    color: var(--color-text);
    border: 1px solid var(--color-border);
    border-left: 4px solid var(--color-success);
    border-radius: var(--radius);
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
    padding: 12px 16px;
    font-size: 0.88rem;
    font-weight: 600;
    line-height: 1.35;
    opacity: 0;
    transform: translateY(12px);
    transition: opacity 0.3s ease, transform 0.3s ease;
}

.toast.visible {
    opacity: 1;
    transform: translateY(0);
}

.toast.toast-error {
    border-left-color: var(--color-danger);
}

@media (max-width: 768px) {
    .toast-container {
        top: 12px;
        bottom: auto;
        right: 12px;
        left: 12px;
        align-items: stretch;
    }

    .toast {
        max-width: none;
        transform: translateY(-12px);
    }

    .toast.visible {
        transform: translateY(0);
    }
}

/* Centered blocking alert (assets/js/app.js's window.showCenterAlerts()) —
   used for things worth actually stopping the player for, like an
   evolution notice, rather than a corner toast that's easy to miss. */
.center-alert-overlay {
    position: fixed;
    inset: 0;
    z-index: 1100;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.55);
    opacity: 0;
    transition: opacity 0.2s ease;
    padding: 20px;
}

.center-alert-overlay.visible {
    opacity: 1;
}

.center-alert-box {
    background: var(--color-surface);
    color: var(--color-text);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
    padding: 24px;
    max-width: 340px;
    width: 100%;
    text-align: center;
    display: flex;
    flex-direction: column;
    gap: 16px;
}

.center-alert-box p {
    margin: 0;
    font-size: 1.05rem;
    font-weight: 600;
    line-height: 1.4;
}

/* Wider variant for the mid-battle move-learn popup (battle.php's
   #pending-move-picker) — needs room for a 4-move stat comparison table,
   unlike .center-alert-box's plain one-line message + OK button shape. */
.pending-move-box {
    max-width: 480px;
    text-align: left;
}

.pending-move-compare {
    display: flex;
    flex-direction: column;
    gap: 6px;
    font-size: 0.82rem;
}

.pending-move-row {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 6px 8px;
    border-radius: 6px;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
}

.pending-move-row.pending-move-new {
    border-color: #e6a817;
    background: rgba(230, 168, 23, 0.1);
}

.pending-move-row .move-name {
    font-weight: 700;
    flex: 1 1 auto;
    min-width: 0;
}

.pending-move-row .move-stat-line {
    display: flex;
    gap: 10px;
    color: var(--color-text-muted);
    flex: 0 0 auto;
    font-size: 0.78rem;
}

/* ==========================================================================
   Evolution/hatch popup (assets/js/app.js's window.showEvolutionPopup() —
   builds its own DOM via document.createElement, no page markup needed, so
   these classes are all it needs to style) — a full-screen blocking scene
   shown whenever an action reports an evolution or an egg hatching,
   whether from a battle win, a mid-battle KO against a trainer with more
   roster left, a Rare Candy, an Evolution Stone, or the playtime heartbeat
   ticking an egg's hatch progress to completion from any page. Same
   fixed-overlay convention as .center-alert-overlay above, but built as
   its own dedicated overlay rather than reused through that helper: this
   needs a real animated scene (accelerating sprite flash, scale pulse,
   screen-shake, white-flash reveal), not a one-line message + OK button.
   z-index sits above .center-alert-overlay (1100) and .stats-popover
   (1150, battle.css) since this is the single most "stop everything"
   moment any of those callers can show.
   ========================================================================== */
.evolution-popup-overlay {
    position: fixed;
    inset: 0;
    z-index: 1200;
    display: none;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.7);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    opacity: 0;
    transition: opacity 0.2s ease;
    padding: 20px;
}

.evolution-popup-overlay.visible {
    opacity: 1;
}

.evolution-scene {
    position: relative;
    width: 100%;
    max-width: 360px;
    background: var(--color-surface);
    color: var(--color-text);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
    padding: 24px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 18px;
}

@keyframes evolution-screen-shake {
    0% { transform: translate(0, 0); }
    20% { transform: translate(-4px, 4px); }
    40% { transform: translate(4px, -4px); }
    60% { transform: translate(-4px, -4px); }
    80% { transform: translate(4px, 4px); }
    100% { transform: translate(0, 0); }
}

.evolution-scene.shake {
    animation: evolution-screen-shake 0.4s ease-in-out infinite;
}

.evolution-status-text {
    margin: 0;
    font-size: 1.05rem;
    font-weight: 700;
    text-align: center;
    line-height: 1.4;
    min-height: 28px;
}

.evolution-display-window {
    position: relative;
    width: 100%;
    height: 200px;
    background: radial-gradient(circle, var(--color-primary-tint) 0%, var(--color-bg) 72%);
    border-radius: var(--radius-sm);
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
}

.evolution-sprite-wrapper {
    position: relative;
    width: 140px;
    height: 140px;
    transition: transform 0.15s ease-in-out;
}

.evolution-sprite {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
    opacity: 0;
    transition: opacity 0.05s linear;
}

.evolution-sprite.active {
    opacity: 1;
}

/* Pure white flashing silhouette — the classic games' own evolution
   effect — applied regardless of theme (unlike everything else on this
   card, deliberately NOT theme-tokenized: the point is stark white against
   whatever backdrop). */
.evolution-scene.evolving .evolution-sprite {
    filter: brightness(0) invert(1) drop-shadow(0 0 8px rgba(255, 255, 255, 0.85));
}

.evolution-scene.pulse-up .evolution-sprite-wrapper {
    transform: scale(1.25);
}

.evolution-scene.pulse-down .evolution-sprite-wrapper {
    transform: scale(0.75);
}

.evolution-flash-overlay {
    position: absolute;
    inset: 0;
    background-color: #ffffff;
    opacity: 0;
    pointer-events: none;
    z-index: 3;
}

.evolution-flash-overlay.trigger {
    opacity: 1;
    transition: opacity 0.05s ease-out;
}

.evolution-flash-overlay.fade-out {
    opacity: 0;
    transition: opacity 0.8s ease-in;
}

.evolution-action-btn {
    min-width: 140px;
}

@media (max-width: 480px) {
    .evolution-scene {
        padding: 18px;
        gap: 14px;
    }

    .evolution-display-window {
        height: 160px;
    }

    .evolution-sprite-wrapper {
        width: 110px;
        height: 110px;
    }

    .evolution-status-text {
        font-size: 0.92rem;
    }
}

/* ---------------------------------------------------------------------- */
/* Starter picker                                                          */
/* ---------------------------------------------------------------------- */

.starter-card {
    display: flex;
    gap: 12px;
    align-items: center;
}

.starter-sprite {
    width: 72px;
    height: 72px;
    object-fit: contain;
    flex: 0 0 auto;
    background: var(--color-bg);
    border-radius: var(--radius);
}

.starter-info {
    flex: 1 1 auto;
    min-width: 0;
}

/* ---------------------------------------------------------------------- */
/* Bag / party                                                             */
/* ---------------------------------------------------------------------- */

.bag-pokemon-row,
.bag-item-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    padding: 8px 0;
    border-bottom: 1px solid var(--color-border);
}

.bag-pokemon-row:last-child,
.bag-item-row:last-child {
    border-bottom: none;
}

.bag-pokemon-sprite {
    width: 44px;
    height: 44px;
    object-fit: contain;
    flex: 0 0 auto;
}

.bag-pokemon-row .btn {
    flex: 0 0 auto;
    width: auto;
    min-height: 36px;
    padding: 0 16px;
    font-size: 0.85rem;
}

.bag-pokemon-info,
.bag-item-info {
    flex: 1 1 auto;
    min-width: 0;
}

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

/* Party reordering (pc.php + assets/js/pc.js) — press-and-drag the handle,
   works with both mouse and touch via the Pointer Events API. */
/* CSS-drawn grip (3 bars via box-shadow) instead of a Unicode "hamburger"
   glyph — same "abstract UI chrome gets a CSS shape, not emoji/item art"
   rule .pokeball-icon/.pc-icon already follow for nav chrome. */
.party-drag-handle {
    flex: 0 0 auto;
    cursor: grab;
    width: 16px;
    height: 12px;
    padding: 4px 6px;
    touch-action: none;
    user-select: none;
    display: flex;
    align-items: center;
    justify-content: center;
}

.party-drag-handle::before {
    content: "";
    display: block;
    width: 14px;
    height: 2px;
    background: var(--color-text-muted);
    box-shadow: 0 -4px 0 var(--color-text-muted), 0 4px 0 var(--color-text-muted);
}

.party-row-dragging {
    opacity: 0.4;
}

.party-row-drop-target {
    outline: 2px dashed var(--color-primary, #4a90d9);
    outline-offset: -2px;
    border-radius: 8px;
}

.bag-item-actions select {
    min-height: 36px;
    border-radius: var(--radius);
    border: 1px solid var(--color-border);
    padding: 0 8px;
}

.item-icon {
    width: 32px;
    height: 32px;
    object-fit: contain;
    flex: 0 0 auto;
    image-rendering: pixelated;
}

/* ---------------------------------------------------------------------- */
/* PC Box storage grid (pc.php) — an actual 5x5-per-page grid rather than  */
/* the .bag-pokemon-row list Party still uses; blank cells pad out a       */
/* partial last page so the "storage box" shape stays 5x5 regardless.     */
/* assets/js/pc.js rebuilds #pc-box-grid's contents wholesale on every     */
/* search/filter/page change, so nothing here depends on static markup.   */
/* ---------------------------------------------------------------------- */

.pc-box-filters {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
}

.pc-box-filter-input {
    flex: 1 1 120px;
    min-width: 0;
    min-height: 40px;
    padding: 8px 10px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    font-size: 0.85rem;
    background: var(--color-surface);
    color: var(--color-text);
}

.pc-box-filter-checkbox {
    display: flex;
    align-items: center;
    gap: 5px;
    min-height: 40px;
    padding: 0 4px;
    font-size: 0.85rem;
    white-space: nowrap;
    cursor: pointer;
}

.pc-box-grid {
    display: grid;
    /* minmax(0, 1fr) instead of a bare 1fr — a plain 1fr track still won't
       shrink below its content's min-content width (a CSS Grid default),
       so a cell with a long, unbroken label (achievements.php's names run
       longer than Bag/Box item names) could force the whole grid wider
       than the viewport on mobile despite .pc-box-name's own text-overflow
       ellipsis. minmax(0, ...) is a no-op wherever content already fits. */
    grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: 8px;
}

.pc-box-cell {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    gap: 2px;
    background: rgba(128, 128, 128, 0.05);
    border: 1px dashed var(--color-border-light);
    border-radius: var(--radius-sm);
    padding: 6px 2px;
    min-height: 92px;
}

.pc-box-cell-empty {
    background: transparent;
    border-style: dashed;
    opacity: 0.35;
}

.pc-box-sprite {
    width: 40px;
    height: 40px;
    object-fit: contain;
}

.pc-box-name {
    font-size: 0.66rem;
    font-weight: 700;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}

.pc-box-level {
    font-size: 0.6rem;
}

.pc-box-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 4px;
    margin-top: 2px;
}

/* Bigger than the base .btn-icon (22px) — scoped to the PC Box grid's
   action row only, so achievements/marketplace/bag/profile's own .btn-icon
   uses are unaffected. .party-mini-actions below gets the same override
   for the Party tab's cards. Comfortably fits .pc-box-cell's grid column
   at normal widths; the mobile media query further down shrinks it again
   so 4 icons still wrap 2x2 without crowding a narrow phone-width cell. */
.pc-box-actions .btn-icon {
    width: 30px;
    height: 30px;
    font-size: 0.95rem;
}

/* Shop grid (shop.php) reuses .pc-box-grid/.pc-box-cell/.pc-box-sprite/
   .pc-box-name/.pc-box-actions directly, same component as the Bag/PC Box —
   only the price line and the locked-cell treatment are shop-specific. */
.shop-cell-price {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1px;
    font-size: 0.64rem;
    font-weight: 700;
}

.shop-cell-price-old {
    font-size: 0.58rem;
    font-weight: 400;
    color: var(--color-text-muted);
    text-decoration: line-through;
}

/* A level/egg-slot/weekly-limit-locked item stays visible (so the full
   category is still browsable) but reads as unavailable at a glance — same
   dimmed treatment as .achievement-locked below, applied to the whole cell
   rather than just the sprite since the price/name are equally "not
   available right now" here. The actual reason is one tap away via the
   info popover (assets/js/shop.js's data-lock-reason). */
.pc-box-cell-locked {
    opacity: 0.55;
    filter: grayscale(0.5);
}

/* Highlights the one "you are here" tile in an overview picker grid — e.g.
   gym.php's League Progress grid (2026-07-22, player request: "follow the
   same approach as legacy_starters" — that page's picker grid doesn't need
   this since only one line is ever pursuable and it gets its own full chain
   card instead; gym.php's grid shows all 8 badges + Elite Four + Champion
   at once, so the currently-challengeable one needs to stand out from the
   completed/locked ones around it). A solid accent border + tinted
   background, distinct from .pc-box-cell-locked's dimmed treatment below. */
.pc-box-cell-current {
    border: 1px solid var(--region-accent, var(--color-primary));
    background: var(--region-tint, var(--color-primary-tint));
}

/* A real (non-.btn-icon) button inside a grid cell's action row — shrunk
   down from .btn's normal full-width/44px-tall page-button sizing to
   actually fit a ~110px+ cell alongside the sprite/name/status above it. */
.pc-box-actions .btn {
    width: auto;
    min-height: 30px;
    padding: 0 12px;
    font-size: 0.72rem;
}

/* Buy quantity popup, anchored next to whichever cell's Buy button opened
   it (assets/js/shop.js) instead of sitting in normal page flow at the
   bottom — a player reported having to scroll all the way down to the old
   static card after clicking Buy on a cell near the top of a long category.
   Same "fixed position, faded in/out, positioned via JS" shape as
   .item-info-popover above, just wide enough to hold a real form (quantity
   input + Buy/Cancel) rather than a couple lines of text — .card's own
   position:relative/overflow:hidden/margin-bottom are overridden here since
   this cascades after .card in the stylesheet. */
.shop-buy-popover {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 1150;
    width: 240px;
    max-width: calc(100vw - 16px);
    margin: 0;
    overflow: visible;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.15s ease;
}

.shop-buy-popover.visible {
    opacity: 1;
    visibility: visible;
}

/* Achievements grid (achievements.php) reuses .pc-box-grid/.pc-box-cell/
   .pc-box-sprite directly — each achievement's icon is a real sprite image
   (achievement_icon_url(), same item_icon_url()/currency_icon_url() lookup
   Bag/Shop/PC already use), not the raw achievements.icon emoji column. A
   locked achievement is dimmed/grayscaled rather than removed, so the full
   25-item shape stays visible as a checklist regardless of progress. */
.achievement-locked {
    opacity: 0.45;
    filter: grayscale(1);
}

/* Section header for each condition_type group, inline within the same
   grid (not a separate list) — grid-column: 1/-1 spans the full row so it
   reads as a divider rather than squeezing into a single 1fr cell. A real
   <button> (not a div) so it's keyboard/screen-reader operable; collapsed
   by default (see assets/js/achievements.js) — every .achievement-card in
   its group is server-rendered with display:none already, toggled by
   aria-expanded on click. */
.achievement-category-header {
    grid-column: 1 / -1;
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    margin: 0;
    padding: 10px 4px 8px;
    background: none;
    border: none;
    border-bottom: 1px solid var(--color-border-light);
    font-family: inherit;
    font-size: 0.72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-text-muted);
    text-align: left;
    cursor: pointer;
}

.achievement-category-header:first-child {
    padding-top: 0;
}

.achievement-category-chevron {
    display: inline-block;
    transition: transform 0.15s ease;
}

.achievement-category-header[aria-expanded="true"] .achievement-category-chevron {
    transform: rotate(90deg);
}

.achievement-category-count {
    margin-left: auto;
    font-weight: 700;
}

.btn-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    border-radius: 6px;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    color: var(--color-text);
    font-size: 0.7rem;
    line-height: 1;
    cursor: pointer;
    text-decoration: none;
    padding: 0;
}

.btn-icon:disabled {
    opacity: 0.35;
    cursor: not-allowed;
}

@media (max-width: 480px) {
    .pc-box-grid {
        gap: 4px;
    }

    .pc-box-cell {
        padding: 4px 1px;
        min-height: 92px;
    }

    .pc-box-sprite {
        width: 32px;
        height: 32px;
    }

    /* 4 action icons (Stats/Move/Daycare/Release) no longer fit in one row
       at this width — .pc-box-actions already wraps, this just shrinks the
       icons themselves so a 2x2 wrap looks intentional, not cramped. Still
       bigger than the pre-enlargement 19px base, just scaled down less
       aggressively than the 30px desktop size above. */
    .pc-box-actions .btn-icon {
        width: 24px;
        height: 24px;
        font-size: 0.76rem;
    }

    /* .shop-table is now ONLY the Respect Shop's Power-Ups table (Boost/
       Effect/Duration/Cost/Buy) — the regular/VIP Shop moved onto a sprite
       grid 2026-07-18 (see .pc-box-grid below) and stopped using this class
       entirely. This block was written for that OLD table's Item/Price/Qty/
       Buy column shape and never updated when the columns actually in use
       changed — "Effect" (long text like "Shiny Encounter Chance ×2.00")
       was being squeezed into a 64px column originally sized for a short
       Price value, wrapping into an illegible, cramped mess (player-
       reported, 2026-08-03). Widths below match the table's REAL current
       columns instead: Boost (unset, absorbs remaining space), Effect
       (widest of the fixed columns — it's the one genuinely long text
       column now), Duration ("30m"-style, always short), Cost (icon +
       number), Buy/Refresh button. */
    .shop-table {
        font-size: 0.72rem;
        table-layout: fixed;
    }

    .shop-table th,
    .shop-table td {
        padding: 6px 3px;
    }

    .shop-table td {
        white-space: normal;
        vertical-align: top;
    }

    .shop-table th:nth-child(2),
    .shop-table td:nth-child(2) {
        width: 96px;
    }

    .shop-table th:nth-child(3),
    .shop-table td:nth-child(3) {
        width: 34px;
    }

    .shop-table th:nth-child(4),
    .shop-table td:nth-child(4) {
        width: 44px;
    }

    .shop-table th:nth-child(5),
    .shop-table td:nth-child(5) {
        width: 52px;
    }

    /* Cost cell (icon + number) — inline-flex, not the old block's
       flex-direction:column (that stacked the icon ABOVE the number
       instead of beside it, the same "every direct child becomes its own
       row" flex-column trap fixed on players.php's username flair earlier
       the same day). .ui-icon (ui_icon()'s own class) targeted alongside
       .currency-icon since the Respect Shop uses ui_icon('respect'), not
       the old shop's currency_icon_url(). */
    .shop-table-price {
        display: inline-flex;
        align-items: center;
        gap: 2px;
        white-space: normal;
    }

    .shop-table-price .currency-icon,
    .shop-table-price .ui-icon {
        width: 12px;
        height: 12px;
    }

    .shop-table-item {
        min-width: 0;
        gap: 6px;
    }

    .shop-table-item .item-icon {
        width: 24px;
        height: 24px;
    }

    .shop-item-qty {
        width: 100%;
        min-height: 28px;
        padding: 0 2px;
    }

    .shop-table .btn {
        width: 100%;
        padding: 0 2px;
        font-size: 0.62rem;
        min-height: 28px;
        white-space: nowrap;
    }
}

/* ---------------------------------------------------------------------- */
/* Section titles + cards                                                 */
/* ---------------------------------------------------------------------- */

.card::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 3px;
    background: linear-gradient(90deg, var(--color-primary), var(--color-accent));
    opacity: 0.85;
}

/* Accent top-bar (2026-07-22, player request), two flavors sharing one
   class/pair of variables — a page's own header card adds this class +
   inline --region-accent/--region-accent-dark, either:
   - dynamic per-region (map.php/travel.php/gym.php, includes/sanitize.php's
     region_accent_colors()) when the player is standing in Johto/Hoenn/
     Sinnoh — same values the zone banner and Explore sidebar group use; or
   - fixed per nav group (every other content page, nav_group_accent_colors())
     matching that page's own sidebar section — see includes/nav_items.php's
     'group' key for which page uses which.
   Two classes beats .card::before's one on specificity, so this wins
   regardless of source order; a page with neither (Kanto with no group
   color, or a card nobody bothered to tag) just keeps the normal look. */
.card.region-card::before {
    background: linear-gradient(90deg, var(--region-accent, var(--color-primary)), var(--region-accent-dark, var(--color-accent)));
}

.section-title {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 0.95rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--color-primary-dark);
}

/* ---------------------------------------------------------------------- */
/* Shop                                                                    */
/* ---------------------------------------------------------------------- */

.shop-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.85rem;
}

.shop-table th {
    text-align: left;
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--color-text-muted);
    padding: 4px 8px;
    border-bottom: 1px solid var(--color-border);
    white-space: nowrap;
}

.shop-table td {
    padding: 8px;
    vertical-align: middle;
    border-bottom: 1px solid var(--color-border);
    white-space: nowrap;
}

.shop-table tr:last-child td {
    border-bottom: none;
}

.shop-table-item {
    display: flex;
    align-items: center;
    gap: 8px;
    white-space: normal;
    min-width: 160px;
}

.shop-table-price {
    font-weight: 700;
    color: var(--color-primary-dark);
}

.shop-item-price-gold {
    color: #b8860b;
}

.shop-item-qty {
    width: 52px;
    min-height: 32px;
    text-align: center;
    border-radius: var(--radius);
    border: 1px solid var(--color-border);
}

.shop-table .btn {
    min-height: 32px;
    padding: 0 14px;
    font-size: 0.8rem;
    width: auto;
}

/* ---------------------------------------------------------------------- */
/* Type badges (shared by pokemon.php + battle screens)                    */
/* ---------------------------------------------------------------------- */

.type-badges {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 4px;
    margin-top: 4px;
    max-width: 100%;
}

.type-badge {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 0.65rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: #fff;
    background: #777;
    white-space: nowrap;
}

.type-normal   { background: #A8A878; }
.type-fire     { background: #F08030; }
.type-water    { background: #6890F0; }
.type-electric { background: #F8D030; color: #1c1c1e; }
.type-grass    { background: #78C850; }
.type-ice      { background: #98D8D8; color: #1c1c1e; }
.type-fighting { background: #C03028; }
.type-poison   { background: #A040A0; }
.type-ground   { background: #E0C068; color: #1c1c1e; }
.type-flying   { background: #A890F0; }
.type-psychic  { background: #F85888; }
.type-bug      { background: #A8B820; }
.type-rock     { background: #B8A038; }
.type-ghost    { background: #705898; }
.type-dragon   { background: #7038F8; }
.type-dark     { background: #705848; }
.type-steel    { background: #B8B8D0; color: #1c1c1e; }
.type-fairy    { background: #EE99AC; color: #1c1c1e; }

/* ---------------------------------------------------------------------- */
/* Pokemon stat sheet (pokemon.php)                                        */
/* ---------------------------------------------------------------------- */

.pokemon-detail-header {
    display: flex;
    align-items: center;
    gap: 14px;
}

.pokemon-detail-sprite {
    width: 88px;
    height: 88px;
    object-fit: contain;
    flex: 0 0 auto;
    background: var(--color-bg);
    border-radius: var(--radius);
}

.shiny-badge {
    position: absolute;
    top: 14px;
    right: 14px;
    font-size: 1.1rem;
}

/* Smaller variant for compact cards (PC Box/Party, Profile team row) —
   absolutely positioned so it overlays without taking a row in whichever
   grid-row-pinned card layout it's dropped into (see the long comment on
   .bag-pokemon-row.party-mini-card above for why those are pinned by row). */
.shiny-badge-mini {
    position: absolute;
    top: 4px;
    right: 4px;
    font-size: 0.85rem;
    line-height: 1;
    z-index: 2;
}

/* Prestige Star count (profile.php's Current Team grid, 2026-08-01, player
   request: "make the prestige stars visible on users profile") — opposite
   corner from .shiny-badge-mini above so a shiny AND prestiged Pokemon
   never overlaps its own badges. Shows the real count (not just a bare
   icon like the shiny badge) since 1 star and 5 stars are meaningfully
   different at a glance. */
.prestige-badge-mini {
    position: absolute;
    top: 4px;
    left: 4px;
    display: inline-flex;
    align-items: center;
    gap: 2px;
    font-size: 0.68rem;
    font-weight: 700;
    line-height: 1;
    color: #fff;
    background: var(--color-warning);
    border-radius: 999px;
    padding: 2px 5px;
    z-index: 2;
}

/* Same pill as .prestige-badge-mini, but sat inline next to a Pokemon's
   name (profile.php's Current Team/Showcase cards, 2026-08-02 — moved off
   the sprite corner per player request) instead of pinned over the sprite. */
.prestige-badge-inline {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    font-size: 0.68rem;
    font-weight: 700;
    line-height: 1;
    color: #fff;
    background: var(--color-warning);
    border-radius: 999px;
    padding: 2px 5px;
    vertical-align: middle;
}

.gender-symbol {
    font-weight: 700;
    font-size: 0.85em;
}

.gender-male {
    color: #4a90d9;
}

.gender-female {
    color: #e0629c;
}

.xp-bar-track {
    width: 100%;
    height: 8px;
    background: #d8d8d8;
    border-radius: 999px;
    overflow: hidden;
}

.xp-bar-fill {
    height: 100%;
    background: var(--color-accent);
}

.stat-row {
    display: grid;
    grid-template-columns: 90px 1fr 34px auto;
    align-items: center;
    gap: 8px;
    font-size: 0.85rem;
}

.stat-label {
    font-weight: 600;
    white-space: nowrap;
}

.stat-arrow-up {
    color: var(--color-danger);
}

.stat-arrow-down {
    color: var(--color-accent);
}

.stat-bar-track {
    width: 100%;
    height: 7px;
    background: #d8d8d8;
    border-radius: 999px;
    overflow: hidden;
}

.stat-bar-fill {
    height: 100%;
    background: linear-gradient(90deg, var(--color-accent), var(--color-primary));
}

.stat-value {
    font-weight: 700;
    text-align: right;
}

/* .stat-extra stacks IV above EV (2026-08-01, player request: "make the EV
   display") in the same .stat-row grid cell that used to hold IV alone —
   keeps the row's fixed grid-template-columns untouched, the cell just
   grows a little taller instead of the row growing wider. */
.stat-extra {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
}

.stat-iv,
.stat-ev {
    font-size: 0.72rem;
    white-space: nowrap;
}

.pokemon-move-row {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 0;
    border-bottom: 1px solid var(--color-border);
}

.pokemon-move-row:last-child {
    border-bottom: none;
}

/* Move reordering (pokemon.php + assets/js/pokemon_detail.js) — same
   press-and-drag-the-handle gesture as .party-drag-handle on pc.php, kept as
   its own class since it applies to .pokemon-move-row instead of a party
   row, but visually identical (CSS-drawn grip, not emoji/item art). */
.move-drag-handle {
    flex: 0 0 auto;
    cursor: grab;
    width: 16px;
    height: 12px;
    padding: 4px 6px;
    touch-action: none;
    user-select: none;
    display: flex;
    align-items: center;
    justify-content: center;
}

.move-drag-handle::before {
    content: "";
    display: block;
    width: 14px;
    height: 2px;
    background: var(--color-text-muted);
    box-shadow: 0 -4px 0 var(--color-text-muted), 0 4px 0 var(--color-text-muted);
}

.move-row-dragging {
    opacity: 0.4;
}

.move-row-drop-target {
    outline: 2px dashed var(--color-primary, #4a90d9);
    outline-offset: -2px;
    border-radius: 8px;
}

/* ---------------------------------------------------------------------- */
/* Work (work.php)                                                         */
/* ---------------------------------------------------------------------- */


#job-countdown {
    font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------------------- */
/* HP bar — shared by battle.php (battle.css sets a fixed width override)  */
/* and profile.php's mini party cards                                     */
/* ---------------------------------------------------------------------- */

.hp-bar-track {
    width: 100%;
    height: 8px;
    background: #d8d8d8;
    border-radius: 999px;
    overflow: hidden;
}

.hp-bar-fill {
    height: 100%;
    background: var(--color-success);
    width: 100%;
    transition: width 0.35s ease, background-color 0.35s ease;
}

.hp-bar-fill.low {
    background: #e0a800;
}

.hp-bar-fill.critical {
    background: var(--color-danger);
}

.bag-capacity-bar {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.bag-capacity-label {
    display: flex;
    justify-content: space-between;
    font-size: 0.85rem;
    font-weight: 600;
}

/* ---------------------------------------------------------------------- */
/* Hover stat tooltip — wrap a card/row in .poke-hoverable and drop        */
/* includes/battle_engine.php::poke_stat_tooltip_html()'s markup inside    */
/* ---------------------------------------------------------------------- */

.poke-hoverable {
    position: relative;
}

/* position:fixed with top/left set inline by assets/js/app.js's mouseenter
   handler (positioned next to whichever sprite was actually hovered) —
   NOT position:absolute centered on .poke-hoverable itself, which used to
   put this in the middle of wide rows (pc.php's party/box rows span the
   whole card) instead of next to the Pokemon. CSS here only owns the
   fade-in/out; positioning is entirely the JS handler's job. */
.poke-tooltip {
    position: fixed;
    top: 0;
    left: 0;
    background: rgba(28, 28, 30, 0.95);
    color: #fff;
    border-radius: 8px;
    padding: 8px 12px;
    font-size: 0.72rem;
    font-weight: 600;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 2px 14px;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.15s ease;
    pointer-events: none;
    z-index: 200;
    white-space: nowrap;
}

.poke-hoverable:hover .poke-tooltip {
    opacity: 1;
    visibility: visible;
}

.poke-tooltip-row {
    display: flex;
    justify-content: space-between;
    gap: 10px;
}

.poke-tooltip-row span:first-child {
    color: #aaa;
}

/* Click-to-toggle description popover — unlike .poke-tooltip above, this is
   click-triggered rather than hover-only, since hover doesn't exist on
   mobile. One shared element per page (assets/js/bag.js's item descriptions,
   assets/js/achievements.js's achievement descriptions, assets/js/admin_icons.js's
   "where does this show up" descriptions each create/reposition their own),
   not one per row/cell. */
.item-info-popover {
    position: fixed;
    top: 0;
    left: 0;
    background: rgba(28, 28, 30, 0.97);
    color: #fff;
    border-radius: 8px;
    padding: 10px 12px;
    font-size: 0.75rem;
    line-height: 1.4;
    max-width: 220px;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.15s ease;
    /* Above .center-alert-overlay (1100) — this popover is also opened from
       move-info buttons INSIDE that overlay (battle.php's
       pending-move-picker), and at its old value (300) rendered entirely
       behind the overlay's own backdrop/box there: readable-but-obscured on
       a wide desktop viewport, fully invisible on mobile where the overlay
       covers nearly the whole screen. */
    z-index: 1150;
}

.item-info-popover.visible {
    opacity: 1;
    visibility: visible;
}

.item-info-popover strong {
    display: block;
    margin-bottom: 4px;
    font-size: 0.8rem;
}

/* ---------------------------------------------------------------------- */
/* Profile (profile.php)                                                   */
/* ---------------------------------------------------------------------- */

.profile-hero {
    padding: 0 0 20px;
}

/* Cover photo — a plain radial-dot gradient by default (nothing uploaded
   yet), or an uploaded image (profile.php inlines background-image via
   style="" when users.banner_path is set). The box is a fixed size (per
   breakpoint below); background-size is `contain` (not `cover` or a
   stretched 100% 100%) so an uploaded photo is always shown in full,
   uncropped AND undistorted — any letterbox gap around it is filled by
   background-color rather than stretching the image to hide the gap.
   Generated gradients (the no-banner default) have no intrinsic size, so
   `contain` still computes to 100% 100% for them — same look as before.
   position:relative anchors the upload/remove buttons in the corner. */
.profile-hero-banner {
    position: relative;
    height: 190px;
    margin: -16px -16px 0;
    background:
        radial-gradient(circle at 15% 30%, rgba(255, 255, 255, 0.18) 0 6px, transparent 7px),
        radial-gradient(circle at 40% 70%, rgba(255, 255, 255, 0.14) 0 4px, transparent 5px),
        radial-gradient(circle at 70% 25%, rgba(255, 255, 255, 0.16) 0 5px, transparent 6px),
        radial-gradient(circle at 88% 65%, rgba(255, 255, 255, 0.14) 0 4px, transparent 5px),
        linear-gradient(135deg, var(--color-primary), var(--color-accent));
    background-color: #111214;
    background-size: contain;
    background-repeat: no-repeat;
    background-position: center;
}

@media (min-width: 769px) {
    /* Wider cards on desktop crop a fixed-height cover photo far more
       aggressively (cover scales to fill width first) — a taller box here
       keeps roughly the same crop ratio the mobile height already reads as. */
    .profile-hero-banner {
        height: 260px;
    }
}

.profile-hero-banner-actions {
    position: absolute;
    right: 10px;
    bottom: 10px;
    display: flex;
    gap: 6px;
}

.profile-hero-banner-btn {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.45);
    border: 1px solid rgba(255, 255, 255, 0.5);
    color: #fff;
    font-size: 0.95rem;
}

.profile-hero-banner-btn:hover {
    background: rgba(0, 0, 0, 0.65);
}

/* Everything below the banner reads as one row: a square avatar
   (hero-avatar-wrap) next to the name/meta/chips/actions block
   (hero-info-wrap) — a left-aligned "dashboard hero" layout. Sits fully
   below the banner (no overlap) so the name/info block never paints over
   the picture itself. */
.profile-hero-body {
    display: flex;
    align-items: flex-end;
    gap: 16px;
    padding: 16px 20px 20px;
}

.hero-avatar-wrap {
    position: relative;
    z-index: 2;
    flex: 0 0 auto;
    /* Overlap the banner (2026-07-22, player request) — previously sat
       fully below it in its own row, reading as a lot of empty gradient
       space disconnected from the name/info block. .profile-hero (a
       .card) has overflow:hidden, but that only clips content escaping
       the CARD's own box — this pulls the avatar up into its sibling
       .profile-hero-banner above, still well within the card, so nothing
       gets cut off. -60px against the 104px avatar overlaps a bit more
       than a third of it into the banner; the mobile media query below
       scales this down to match the smaller 80px avatar there. */
    margin-top: -60px;
}

.profile-avatar {
    width: 104px;
    height: 104px;
    border-radius: 18px;
    object-fit: cover;
    display: block;
    background: var(--color-surface);
    border: 5px solid var(--color-panel);
    /* The panel-colored border alone can vanish against a similarly-toned
       uploaded cover photo (the avatar sits half over the banner) — a thin
       dark ring just outside it keeps it readable against any photo, not
       just the card background it's designed to blend with below. */
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25), 0 2px 10px rgba(0, 0, 0, 0.35);
}

.profile-avatar-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 2.4rem;
    color: var(--color-text-muted);
    background: var(--color-bg);
}

/* The avatar's own edit/remove buttons — same corner-overlay pattern as
   .profile-hero-banner-actions/-btn below, anchored to the avatar instead. */
.hero-avatar-actions {
    position: absolute;
    right: -4px;
    bottom: -4px;
    display: flex;
    gap: 4px;
}

.hero-avatar-btn {
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.45);
    border: 1px solid rgba(255, 255, 255, 0.5);
    color: #fff;
    font-size: 0.85rem;
}

.hero-avatar-btn:hover {
    background: rgba(0, 0, 0, 0.65);
}

.hero-info-wrap {
    flex: 1;
    min-width: 0;
    padding-bottom: 6px;
}

.hero-info-wrap h2 {
    margin: 0 0 4px;
    font-size: 1.35rem;
    font-weight: 800;
    line-height: 1.15;
}

.profile-hero-meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    margin: 0;
    color: var(--color-text-muted);
    font-size: 0.82rem;
}

.profile-hero-rank {
    font-weight: 700;
    color: var(--color-primary-dark);
}

.profile-hero-chips {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
    justify-content: flex-start;
    margin-top: 10px;
}

.profile-hero-chip {
    font-size: 0.72rem;
    font-weight: 700;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    padding: 3px 9px;
    border-radius: 999px;
    white-space: nowrap;
}

/* Shown next to the username on profile.php only while includes/vip.php's
   is_vip_active() is true for the profile being viewed. */
.profile-vip-badge {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    font-size: 0.55em;
    font-weight: 700;
    vertical-align: middle;
    background: linear-gradient(135deg, #f6c453, #e8a13a);
    color: #4a2f00;
    padding: 3px 8px;
    border-radius: 999px;
}

.profile-hero-actions {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    justify-content: flex-start;
    margin-top: 10px;
}

.profile-hero-actions .btn {
    width: auto;
    flex: 0 0 auto;
}

@media (max-width: 480px) {
    .profile-hero-body {
        gap: 12px;
        padding: 12px 14px 16px;
    }

    .profile-avatar,
    .profile-avatar-placeholder {
        width: 80px;
        height: 80px;
        border-radius: 14px;
    }

    /* Scaled down to match the 80px avatar above — the desktop -60px
       would overlap roughly 3/4 of a smaller avatar, too much. */
    .hero-avatar-wrap {
        margin-top: -44px;
    }

    .hero-info-wrap h2 {
        font-size: 1.1rem;
    }
}

.profile-bio-input {
    width: 100%;
    padding: 8px 10px;
    border-radius: var(--radius);
    border: 1px solid var(--color-border);
    font-family: inherit;
    font-size: 0.95rem;
    resize: vertical;
}

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

.profile-stat {
    display: flex;
    flex-direction: column;
    gap: 2px;
    background: var(--color-bg);
    border-radius: var(--radius-sm);
    padding: 10px 12px;
}

.profile-stat-label {
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--color-text-muted);
}

.profile-stat-icon {
    font-size: 0.85rem;
    text-transform: none;
}

.profile-stat-value {
    font-weight: 700;
}

.party-row-scroll {
    display: flex;
    justify-content: center;
    align-items: stretch;
    flex-wrap: wrap;
    gap: 10px;
    overflow-x: auto;
    overflow-y: hidden;
    padding-bottom: 6px;
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
}

/* Centering + wrapping only reads well once there's room to actually center
   within — on narrow screens a full 6-card party needs to scroll left-
   aligned instead, or the first cards become unreachable. */
@media (max-width: 700px) {
    .party-row-scroll {
        justify-content: flex-start;
        flex-wrap: nowrap;
    }
}

/* width:160px (up from the previous 132px) so a full 6-Pokemon party sits
   side by side across .app-main's width without scrolling on desktop —
   .app-main is wide enough for this given --shell-max-width/--sidebar-width
   (see app-shell/app-content-row above), and .party-row-scroll still falls
   back to horizontal scrolling below 700px regardless of this width. */
.party-mini-card {
    position: relative;
    flex: 0 0 auto;
    width: 160px;
    min-height: 150px;
    scroll-snap-align: start;
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    text-decoration: none;
    color: var(--color-text);
    background: rgba(128, 128, 128, 0.05);
    border: 1px solid var(--color-border-light);
    /* Primary-type accent (index.php/pc.php set --type-accent inline per
       card via pokemon_type_color(), includes/sanitize.php) — falls back to
       the plain border color for anything that doesn't set it (empty-slot
       cards, other .party-mini-card contexts like daycare/pokecenter that
       intentionally don't opt in). */
    border-top: 3px solid var(--type-accent, var(--color-border-light));
    border-radius: var(--radius-sm);
    padding: 10px 8px;
    overflow: visible;
}

/* pc.php's Party tab reuses this same card shape (see .bag-pokemon-row.party-mini-card
   below) but, unlike profile.php's read-only version, needs a drag handle and a
   row of icon actions — kept as a plain <div>, not an <a>, since it has multiple
   click targets of its own.

   .bag-pokemon-row (the plain list-row rule, further up this file) also
   matches this same element and sets flex properties meant for a horizontal
   row, not this vertical card — since both rules share equal specificity,
   source order alone decided which won. Explicitly restated here as a real
   CSS grid instead of relying on cascade order.

   Each semantic piece (sprite/name/level/types/hp bar/hp text/held item &
   pending-move notice/action icons) is pinned to its own fixed-height grid
   row below, keyed by class rather than DOM order. That's what actually
   fixes the "cards don't line up" bug: an egg card skips the level and
   type-badges elements entirely, and a healthy Pokemon skips the held-item/
   pending-move line — with plain flex stacking, each card's later elements
   (hp bar, action icons) shift up or down depending on which optional
   elements that ONE card happens to render. Pinning every class to an
   explicit row number means a row a given card doesn't use just stays
   blank space instead of letting a sibling's elements collapse upward, so
   the hp bar/icons land on the same row for every card regardless of which
   optional lines are present. Row heights are hard pixel values (not
   auto/minmax) — each card is its own independent grid (the row isn't a
   shared grid across siblings), so only fixed values guarantee identical
   heights card-to-card; auto-sized rows would let one card's longer status
   text grow just that card's row, undoing the alignment. */
.bag-pokemon-row.party-mini-card {
    position: relative;
    display: grid;
    grid-template-rows: 56px 18px 16px 24px 10px 28px 32px auto;
    row-gap: 4px;
    justify-items: center;
    padding: 10px 8px;
}

.bag-pokemon-row.party-mini-card .party-mini-sprite  { grid-row: 1; }
.bag-pokemon-row.party-mini-card .party-mini-name     { grid-row: 2; align-self: center; }
.bag-pokemon-row.party-mini-card .party-mini-level    { grid-row: 3; align-self: center; }
.bag-pokemon-row.party-mini-card .type-badges         { grid-row: 4; align-self: center; margin-top: 0; }
.bag-pokemon-row.party-mini-card .hp-bar-track        { grid-row: 5; align-self: center; }
.bag-pokemon-row.party-mini-card .party-mini-hp       { grid-row: 6; align-self: start; width: 100%; overflow: hidden; }
.bag-pokemon-row.party-mini-card .party-mini-extra    { grid-row: 7; align-self: start; width: 100%; overflow: hidden; }
.bag-pokemon-row.party-mini-card .party-mini-actions  { grid-row: 8; }

.bag-pokemon-row.party-mini-card .party-drag-handle {
    position: absolute;
    top: 2px;
    left: 2px;
}

.party-mini-extra {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1px;
    font-size: 0.68rem;
    text-align: center;
}

/* Icons grid at most 4 across (5th+ action wraps to its own row below)
   instead of ad-hoc flex-wrap, so the icon block itself stays a tidy
   rectangle no matter how many actions a given card shows (egg cards show
   as few as 2, a held-item Pokemon shows as many as 5). Columns sized to
   30px (see .party-mini-actions .btn-icon below) — 4x30px + 3x5px gap =
   135px, still comfortably inside .party-mini-card's 144px content width
   (160px card minus its own 8px side padding). */
.party-mini-actions {
    display: grid;
    grid-template-columns: repeat(4, 30px);
    justify-content: center;
    gap: 5px;
    margin-top: 8px;
}

/* Same enlargement as .pc-box-actions .btn-icon above, applied to the
   Party tab's cards — scoped so the rest of the site's .btn-icon uses
   (achievements, marketplace, bag, profile) stay their original size. */
.party-mini-actions .btn-icon {
    width: 30px;
    height: 30px;
    font-size: 0.95rem;
}

.party-mini-sprite {
    width: 56px;
    height: 56px;
    object-fit: contain;
}

.party-mini-name {
    font-weight: 700;
    font-size: 0.85rem;
    margin-top: 4px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}

.party-mini-level {
    font-size: 0.72rem;
    color: var(--color-text-muted);
}

.party-mini-hp {
    font-size: 0.68rem;
    color: var(--color-text-muted);
    margin-top: 2px;
}

/* index.php's Active Party, mobile only (2026-07-21, player request) — the
   full pc.php-matching card (.bag-pokemon-row.party-mini-card, 160px wide,
   8-row grid) is desktop-appropriate but reads as way too large on a phone
   for what's meant to be a quick glance. .home-party-row scopes this to
   index.php specifically (see that file) so pc.php's own real Party tab is
   completely unaffected. Shrunk down toward the same compact sprite+level+
   HP-bar density as the header's own .header-modern-party-slot (36px
   sprite) rather than inventing a new visual language — type badges and
   the held-item/pending-move line are dropped entirely (no room, and
   that's already visible from pc.php if wanted); the single Stats action
   icon stays so tapping a card still opens that Pokemon's page. */
@media (max-width: 768px) {
    /* .home-party-section overrides .overflow-visible's shorthand (which
       sets both axes to visible, needed so poke-hoverable's tooltip can
       escape the card vertically) — this only reclaims the horizontal
       axis, since a grid item (.dashboard-grid .full-width) with overflow
       fully visible lets a wide scrolling descendant push the whole page
       wider instead of scrolling within its own box. min-width:0 overrides
       the grid-item default of min-width:auto, which otherwise refuses to
       shrink the column below its content's natural width. */
    .home-party-section {
        overflow-x: hidden;
        min-width: 0;
    }

    .home-party-row {
        max-width: 100%;
    }

    .home-party-row .bag-pokemon-row.party-mini-card {
        width: 100px;
        grid-template-rows: 40px 14px 12px 0 8px 12px 0 26px;
        row-gap: 3px;
        padding: 6px 4px;
    }

    .home-party-row .party-mini-sprite {
        width: 40px;
        height: 40px;
    }

    .home-party-row .type-badges,
    .home-party-row .party-mini-extra {
        display: none;
    }

    .home-party-row .party-mini-name {
        font-size: 0.7rem;
        margin-top: 0;
    }

    .home-party-row .party-mini-level {
        font-size: 0.65rem;
    }

    .home-party-row .party-mini-hp {
        font-size: 0.6rem;
        margin-top: 0;
    }

    .home-party-row .party-mini-actions {
        grid-template-columns: 26px;
        margin-top: 0;
    }

    .home-party-row .party-mini-actions .btn-icon {
        width: 26px;
        height: 26px;
        font-size: 0.8rem;
    }
}

.party-mini-stats {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 2px 8px;
    margin-top: 6px;
    font-size: 0.68rem;
    color: var(--color-text-muted);
    width: 100%;
}

/* profile.php's read-only "Current Team" cards — same fixed-row-grid fix as
   pc.php's .bag-pokemon-row.party-mini-card above (see the long comment
   there for why), scoped to this modifier class rather than the bare
   .party-mini-card so daycare.js/pokecenter.php/requests.php's own (simpler,
   differently-shaped) cards that also use .party-mini-card are unaffected.
   No held-item/pending-move/action rows here (this view is read-only), but
   the same egg-vs-non-egg gap applies: an egg skips level + type-badges, so
   without explicit row pinning its hp bar would sit two rows higher than a
   healthy Pokemon's. Empty-slot cards (.party-mini-empty) deliberately don't
   get this class — they're not real Pokemon cards and stay the simple
   centered flex box they always were. */
.party-mini-card--team {
    display: grid;
    grid-template-rows: 56px 18px 16px 24px 10px 18px 46px;
    row-gap: 4px;
    justify-items: center;
}

.party-mini-card--team .party-mini-sprite { grid-row: 1; }
.party-mini-card--team .party-mini-name   { grid-row: 2; align-self: center; margin-top: 0; }
.party-mini-card--team .party-mini-level  { grid-row: 3; align-self: center; }
.party-mini-card--team .type-badges       { grid-row: 4; align-self: center; margin-top: 0; }
.party-mini-card--team .hp-bar-track      { grid-row: 5; align-self: center; }
.party-mini-card--team .party-mini-hp     { grid-row: 6; align-self: start; width: 100%; margin-top: 0; overflow: hidden; }
.party-mini-card--team .party-mini-stats  { grid-row: 7; align-self: start; margin-top: 0; }

.party-mini-empty {
    justify-content: center;
    min-height: 150px;
    border-style: dashed;
    color: var(--color-text-muted);
}

.party-mini-empty-icon {
    font-size: 1.5rem;
    line-height: 1;
    margin-bottom: 6px;
}

/* Day Care's locked (not-yet-purchased) slot cards — same shape as
   .party-mini-empty, just with a buy action instead of just being empty. */
.party-mini-locked {
    justify-content: center;
    min-height: 150px;
    border-style: dashed;
    color: var(--color-text-muted);
    gap: 6px;
}

.party-mini-locked-icon {
    font-size: 1.5rem;
    line-height: 1;
}

/* ---------------------------------------------------------------------- */
/* Pokedex (pokedex.php)                                                   */
/* ---------------------------------------------------------------------- */

.pokedex-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
    gap: 8px;
}

.pokedex-entry {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    padding: 8px 4px;
    overflow: hidden;
}

.pokedex-entry-caught {
    border-color: var(--color-success);
}

.pokedex-entry-unseen {
    opacity: 0.5;
    filter: grayscale(100%);
}

.pokedex-caught-badge {
    position: absolute;
    top: 4px;
    right: 6px;
    color: var(--color-success);
    font-weight: 700;
}

.pokedex-sprite {
    width: 48px;
    height: 48px;
    object-fit: contain;
}

.pokedex-silhouette {
    width: 48px;
    height: 48px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.3rem;
    font-weight: 700;
    color: var(--color-text-muted);
}

.pokedex-number {
    font-size: 0.68rem;
    color: var(--color-text-muted);
    margin-top: 2px;
}

.pokedex-name {
    font-size: 0.78rem;
    font-weight: 700;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}

.pokedex-seen-at {
    font-size: 0.66rem;
    color: var(--color-text-muted);
    margin-top: 4px;
}

.rarity-badge {
    display: inline-block;
    font-size: 0.6rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    padding: 1px 6px;
    border-radius: 999px;
    margin-top: 2px;
    color: #fff;
}

.rarity-common     { background: #8a8a8a; }
.rarity-uncommon    { background: #4caf7d; }
.rarity-rare        { background: #3d7de0; }
.rarity-epic        { background: #a24fe0; }
.rarity-legendary   { background: #e0a83d; }
.rarity-mythical    { background: #e0473d; }

/* Collapsible per-region pokedex cards (assets/js/pokedex.js drives the
   expand/collapse — see there for why max-height is toggled between an
   explicit pixel value and 'none' rather than just 0/auto). Also reused
   as-is for the Set Bonuses card (pokedex.php's .pokedex-set-bonuses,
   2026-08-03) — same collapsed-by-default component (.pokedex-region-body's
   max-height:0 below IS the default, no JS needed to start collapsed),
   wired as its own standalone toggle rather than joining the region list
   below, so it stays untouched by the region search filter. */
.pokedex-region-toggle {
    display: flex;
    width: 100%;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    background: none;
    border: none;
    padding: 0;
    margin: 0;
    font: inherit;
    color: inherit;
    cursor: pointer;
    text-align: left;
}

.pokedex-region-title {
    margin: 0;
}

.pokedex-region-meta {
    display: flex;
    align-items: center;
    gap: 10px;
    flex: 0 0 auto;
    font-size: 0.8rem;
    white-space: nowrap;
}

.pokedex-region-chevron {
    display: inline-block;
    transition: transform 0.25s ease;
}

.pokedex-region.expanded .pokedex-region-chevron {
    transform: rotate(180deg);
}

.pokedex-region-body {
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.3s ease;
}

.pokedex-region.expanded .pokedex-region-body {
    max-height: none;
}

.pokedex-region-body .pokedex-grid {
    padding-top: 14px;
}

.pokedex-entry-clickable {
    cursor: pointer;
}

.pokedex-entry-clickable:hover,
.pokedex-entry-clickable:focus-visible {
    border-color: var(--color-primary);
    outline: none;
}

/* Search (2026-08-01, player request) — assets/js/pokedex.js toggles
   [hidden] on a whole .pokedex-region card (zero matches) or an individual
   .pokedex-entry (not a match, including every un-searchable "???" entry)
   once a query is active. Both classes set their own 'display' elsewhere in
   this file, so the bare [hidden] UA default needs this explicit override —
   same reasoning as .admin-settings-group[hidden]/.admin-setting-row[hidden]
   further down, whose .admin-settings-search/.admin-settings-controls/
   .admin-settings-count classes this page's search bar reuses as-is rather
   than duplicating a near-identical block for one more page.  */
.pokedex-region[hidden],
.pokedex-entry[hidden] {
    display: none;
}

/* Prestige keep/re-roll choice (pokemon.php, 2026-08-01) — same centered-
   card-over-backdrop shape as .pokedex-modal-overlay below/.icon-picker-overlay,
   own class since it's a small fixed 3-button choice, not a rich detail view. */
.prestige-modal-overlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    align-items: center;
    justify-content: center;
    padding: 20px;
    z-index: 1000;
}

.prestige-modal-overlay.visible {
    display: flex;
}

.prestige-modal {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    max-width: 420px;
    width: 100%;
    padding: 24px;
}

/* Showcase slot picker (pokemon.php, 2026-08-01) — a row of numbered slot
   buttons; .active marks whichever slot this Pokemon currently occupies
   (see assets/js/showcase.js — clicking .active clears it instead). */
.showcase-slot-picker {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.showcase-slot-btn {
    width: 36px;
    height: 36px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-bg);
    color: var(--color-text);
    font-weight: 700;
    cursor: pointer;
    transition: transform 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}

.showcase-slot-btn:hover {
    border-color: var(--color-primary);
    transform: translateY(-1px);
}

.showcase-slot-btn.active {
    background: var(--color-primary);
    border-color: var(--color-primary);
    color: #fff;
}

/* Detail popup — a centered card over a dark backdrop, opened by
   assets/js/pokedex.js on click of any seen/caught .pokedex-entry-clickable
   (see api/game/pokedex_detail.php for what fills #pokedex-modal-body). */
.pokedex-modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    z-index: 1000;
}

.pokedex-modal {
    position: relative;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    max-width: 560px;
    width: 100%;
    max-height: 85vh;
    overflow-y: auto;
    padding: 24px;
}

.pokedex-modal-close {
    position: absolute;
    top: 10px;
    right: 12px;
    background: none;
    border: none;
    font-size: 1.4rem;
    line-height: 1;
    cursor: pointer;
    color: var(--color-text-muted);
}

.pokedex-modal-close:hover {
    color: var(--color-heading);
}

/* Push-notification opt-in nudge (assets/js/push_prompt.js) — same centered
   card-over-backdrop pattern as .pokedex-modal-overlay above, just its own
   dedicated classes since the two popups are unrelated features. */
.push-prompt-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    z-index: 1000;
}

.push-prompt-modal {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    max-width: 400px;
    width: 100%;
    padding: 24px;
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.push-prompt-text {
    margin: 0;
    font-size: 0.9rem;
}

.push-prompt-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
}

.pokedex-modal-header {
    display: flex;
    align-items: center;
    gap: 16px;
    margin-bottom: 8px;
}

.pokedex-modal-sprite {
    width: 80px;
    height: 80px;
    object-fit: contain;
    flex: 0 0 auto;
}

.pokedex-modal-title {
    margin: 0;
    font-size: 1.2rem;
}

.pokedex-modal-section {
    margin-top: 18px;
}

.pokedex-modal-section-title {
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--color-text-muted);
    margin: 0 0 8px;
    display: block;
}

.pokedex-stat-row {
    display: grid;
    grid-template-columns: 90px 32px 1fr;
    align-items: center;
    gap: 8px;
    font-size: 0.8rem;
    margin-bottom: 4px;
}

.pokedex-location-row {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    font-size: 0.85rem;
    padding: 6px 0;
    border-bottom: 1px solid var(--color-border);
}

.pokedex-location-row:last-child {
    border-bottom: none;
}

.pokedex-move-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.78rem;
}

.pokedex-move-table th,
.pokedex-move-table td {
    text-align: left;
    padding: 4px 6px;
    border-bottom: 1px solid var(--color-border);
}

.pokedex-evo-row {
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 0.85rem;
}

.pokedex-evo-sprite {
    width: 40px;
    height: 40px;
    object-fit: contain;
}

/* ---------------------------------------------------------------------- */
/* Wild Trainers (battle.php's no-active-battle empty state)               */
/* ---------------------------------------------------------------------- */

.random-trainer-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 12px;
}

.random-trainer-card {
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    padding: 12px;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.random-trainer-name {
    font-weight: 700;
}

.random-trainer-reward {
    display: flex;
    align-items: center;
    gap: 4px;
    font-weight: 700;
    font-size: 0.85rem;
    color: var(--color-primary-dark);
    margin: 8px 0;
}

.random-trainer-card .btn {
    margin-top: auto;
}

/* ---------------------------------------------------------------------- */
/* Quests (quests.php) — Daily/Weekly/Event quests share one grid of card  */
/* tiles, same "auto-fill grid of bordered .color-bg tiles" language as    */
/* .random-trainer-grid/.random-trainer-card above.                        */
/* ---------------------------------------------------------------------- */

.quest-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: 12px;
}

.quest-card {
    display: flex;
    flex-direction: column;
    gap: 6px;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    padding: 14px;
}

.quest-card-done {
    border-color: var(--color-success);
}

.quest-card-eyebrow {
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-text-muted);
}

.quest-card-title {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 1rem;
}

.quest-card-desc {
    margin: 0;
    font-size: 0.8rem;
    color: var(--color-text-muted);
}

.quest-card-progress-label {
    font-size: 0.82rem;
    color: var(--color-text-muted);
}

.quest-card-done-tag {
    display: flex;
    align-items: center;
    gap: 6px;
    font-weight: 700;
    font-size: 0.85rem;
    color: var(--color-success);
}

.quest-card-reward {
    display: flex;
    align-items: center;
    gap: 4px;
    font-weight: 700;
    font-size: 0.78rem;
    color: var(--color-primary-dark);
    margin-top: auto;
    padding-top: 8px;
    border-top: 1px solid var(--color-border-light);
}

/* ---------------------------------------------------------------------- */
/* Public login.php/register.php (includes/public_layout_header.php shell) */
/* ---------------------------------------------------------------------- */
.landing-stats-row {
    display: flex;
    gap: 20px;
    flex-wrap: wrap;
}

.landing-stat {
    flex: 1;
    min-width: 90px;
    text-align: center;
}

.landing-stat-value {
    margin: 0;
    font-size: 1.4rem;
    font-weight: 700;
    color: var(--color-primary-dark);
}

.landing-stat-label {
    margin: 0;
    font-size: 0.72rem;
    color: var(--color-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* login.php/register.php's species showcase + form cards sit in the same
   .info-grid as the boxes below them (Top Trainer / New Trainer), so all
   the box widths on the page line up rather than the showcase+form pair
   using a bespoke, differently-sized grid of their own. Reuses
   .landing-stats-row's flat, borderless flex-row look (no boxed/
   dashed card around each item) instead of the PC Box's dashed-border
   .pc-box-cell — matches the stats row directly above it in the Welcome
   card rather than introducing a second, visually different grid style. */
.landing-sprite-item {
    flex: 1;
    min-width: 90px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    text-align: center;
}

.landing-sprite-icon {
    width: 56px;
    height: 56px;
    object-fit: contain;
}

/* Box-grid layout for the public shell's secondary content cards
   (about.php/rules.php/privacy.php's logged-out branch) — same
   auto-fill/minmax pattern as .admin-hub-grid, just a wider minimum since
   these hold paragraphs of text rather than a short label. */
.info-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: 16px;
}

/* ---------------------------------------------------------------------- */
/* Store (real-money purchases — store.php, vip.php's Buy VIP section)    */
/* ---------------------------------------------------------------------- */
/* The one deliberate departure from this app shell's usual bordered-card
   look, same reasoning as login.php's public .landing-hero — a real-money
   purchase page is a pitch, not a tool, so it earns the gradient band the
   rest of the logged-in app intentionally avoids. */
.store-hero {
    background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
    border-radius: var(--radius);
    text-align: center;
    padding: 28px 24px;
}

.store-hero-title {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    font-family: var(--font-display, inherit);
    font-size: 1.4rem;
    font-weight: 800;
    color: #fff;
}

.store-hero-subtitle {
    margin: 8px 0 0;
    color: rgba(255, 255, 255, 0.92);
    font-size: 0.92rem;
}

.store-hero-trust {
    margin: 10px 0 0;
    color: rgba(255, 255, 255, 0.75);
    font-size: 0.78rem;
}

.store-hero-trust a {
    color: #fff;
}

.store-vip-banner {
    display: flex;
    align-items: center;
    gap: 12px;
    border-left: 4px solid var(--color-primary);
}

.store-vip-banner-icon {
    font-size: 1.6rem;
    flex-shrink: 0;
}

.store-package-card {
    text-align: center;
    transition: transform 0.15s ease, box-shadow 0.15s ease;
}

.store-package-card:hover {
    transform: translateY(-3px);
    box-shadow: 0 8px 22px rgba(0, 0, 0, 0.14);
}

.store-package-icon {
    display: block;
    width: 56px;
    height: 56px;
    margin: 0 auto;
    object-fit: contain;
    image-rendering: pixelated;
}

.store-package-price {
    margin: 4px 0;
    font-size: 1.5rem;
    font-weight: 800;
    color: var(--color-primary-dark);
}

/* ---------------------------------------------------------------------- */
/* Trainer rankings (players.php)                                          */
/* ---------------------------------------------------------------------- */

/* .player-rank-row is the whole-row container (2026-08-02 redesign — was
   previously the <a> itself; a row grew per-row Challenge/Trade buttons
   that day, and a <button> can't validly sit inside an <a>, so the
   clickable "go to profile" part moved to its own nested .player-rank-link
   while this became a plain flex wrapper around that link plus
   .player-rank-actions). */
.player-rank-row {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 10px 0;
    border-bottom: 1px solid var(--color-border);
}

.player-rank-row:last-child {
    border-bottom: none;
}

.player-rank-row-you {
    background: rgba(59, 108, 224, 0.08);
    border-radius: var(--radius);
    padding-left: 8px;
    padding-right: 8px;
}

.player-rank-link {
    display: flex;
    align-items: center;
    gap: 10px;
    flex: 1 1 auto;
    min-width: 0;
    text-decoration: none;
    color: var(--color-text);
}

.player-rank-actions {
    display: flex;
    gap: 6px;
    flex: 0 0 auto;
}

/* Same touch-friendly enlargement as .pc-box-actions/.party-mini-actions'
   own .btn-icon overrides elsewhere in this file. */
.player-rank-actions .btn-icon {
    width: 30px;
    height: 30px;
    font-size: 0.95rem;
}

/* Mobile fix (player-reported, 2026-08-03): 3 action buttons added
   2026-08-02 alongside rank/avatar/name/CP left too little width on a
   narrow phone for the name to render at all — badly truncated rather than
   a clean ellipsis. Actions drop to their own full-width, right-aligned row
   below the link instead of fighting it for horizontal space; the link row
   above is then free to give .player-rank-name (flex:1 1 auto/min-width:0)
   the room it needs. */
@media (max-width: 480px) {
    .player-rank-row {
        flex-wrap: wrap;
        row-gap: 6px;
    }
    .player-rank-link {
        flex: 1 1 100%;
    }
    .player-rank-actions {
        flex: 1 1 100%;
        justify-content: flex-end;
    }
}

.player-rank-number {
    font-weight: 700;
    color: var(--color-text-muted);
    width: 32px;
    flex: 0 0 auto;
    text-align: center;
}

/* Podium treatment (2026-07-22, player request: "it looks plain and
   boring") — ranks 1-3 get a round gold/silver/bronze medal instead of
   plain muted "#N" text, so the top of the leaderboard reads as a podium
   at a glance instead of every row carrying identical visual weight. Rank
   4+ keeps the original plain .player-rank-number look, untouched. */
.player-rank-number-medal {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    color: #fff;
    font-size: 0.9rem;
    font-weight: 700;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
}

.player-rank-number-medal-1 {
    background: linear-gradient(135deg, #ffdf7e, #d4a017);
}

.player-rank-number-medal-2 {
    background: linear-gradient(135deg, #e6ecf2, #9aa7b5);
    color: #2b3440;
}

.player-rank-number-medal-3 {
    background: linear-gradient(135deg, #e3a76f, #a8642a);
}

.player-rank-avatar,
.player-rank-avatar-placeholder {
    /* Bumped from 36px (2026-07-22, same request) — the whole row reads
       more like a compact spreadsheet than a leaderboard at the old size. */
    width: 44px;
    height: 44px;
    border-radius: 50%;
    flex: 0 0 auto;
}

.player-rank-avatar {
    object-fit: cover;
    background: var(--color-bg);
}

.player-rank-avatar-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.3rem;
    color: var(--color-text-muted);
}

/* #1 gets a slightly bigger, gold-ringed avatar on top of the medal above
   — same "make the podium actually look like one" reasoning. */
.player-rank-row-top1 .player-rank-avatar,
.player-rank-row-top1 .player-rank-avatar-placeholder {
    width: 56px;
    height: 56px;
    box-sizing: border-box;
    border: 2px solid #d4a017;
}

.player-rank-name {
    flex: 1 1 auto;
    min-width: 0;
    font-weight: 600;
    display: flex;
    flex-direction: column;
}

/* Flair icon + username, side by side on one line (2026-08-03 fix —
   .player-rank-name's own flex-direction:column above was stacking EVERY
   direct child on its own row, including the flair <img>, so it rendered
   above the username instead of next to it). Truncation lives on the inner
   .player-rank-name-text specifically, not this wrapper — text-overflow:
   ellipsis doesn't reliably apply to a flex container with multiple flex
   items (icon + text), only to a single overflowing text box. */
.player-rank-name-line {
    display: flex;
    align-items: center;
    gap: 2px;
    min-width: 0;
}

.player-rank-name-text {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
}

.player-rank-title {
    font-size: 0.68rem;
    font-weight: 600;
}

.player-rank-cp {
    flex: 0 0 auto;
    font-weight: 700;
    font-size: 0.85rem;
    color: var(--color-primary-dark);
}

/* .username-flair-icon's shared 1.3em size (app.css, "Cosmetics Shop's
   equipped Username Flair") is tuned for profile.php's big <h2> — at this
   row's own compact ~1rem/tight line-height, that came out oversized and
   vertically misaligned against the username text (player-reported
   "doesn't quite fit", 2026-08-02). Scoped down to actually match this
   row's own type scale instead. */
.player-rank-name .username-flair-icon {
    height: 1em;
    width: 1em;
    vertical-align: -0.15em;
    flex: 0 0 auto;
}

/* Stats overview strip (2026-08-02 redesign, player: "still looks empty")
   — 4 quick server-wide facts above the leaderboard itself. */
.players-stats-grid {
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}

.players-stat-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    text-align: center;
    padding: 16px 12px;
}

.players-stat-value {
    font-size: 1.5rem;
    font-weight: 800;
    color: var(--color-primary-dark);
    font-family: var(--font-display, inherit);
}

.players-stat-label {
    display: flex;
    align-items: center;
    gap: 4px;
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--color-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* Row for VIP/custom-tag pills, under the name — kept separate from
   .player-rank-title (level/rank text) so a player with several tags wraps
   onto its own line instead of fighting that text for space. */
.player-rank-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin-top: 2px;
}

/* Custom player tag pills (2026-07-22 — admin-assignable titles, see
   includes/player_tags.php) — a generic colored-pill component reused on
   players.php (leaderboard rows), profile.php (next to the VIP badge in
   the hero <h2>), and admin_players.php (search table + detail header).
   Unlike every other badge family in this file (.type-badge, .rarity-badge,
   .header-badge, .admin-badge-flag), a tag's color is admin-chosen per row
   rather than one of a small fixed set, so it's set inline as
   --tag-bg/--tag-text (player_tag_badge_html()) instead of a modifier class
   per value. */
.player-tag-badge {
    display: inline-flex;
    align-items: center;
    background: var(--tag-bg, var(--color-primary));
    color: var(--tag-text, #fff);
    border-radius: 999px;
    padding: 2px 8px;
    font-size: 0.62rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    line-height: 1.5;
    white-space: nowrap;
    vertical-align: middle;
}

/* VIP itself stays a fixed, non-admin-editable color (the same gold
   gradient .profile-vip-badge already uses in its one single-context h2
   use) rather than living in the player_tags table — it's a real account
   attribute (includes/vip.php), not an admin-assignable title. */
.player-tag-badge-vip {
    background: linear-gradient(135deg, #f6c453, #e8a13a);
    color: #4a2f00;
}

/* Sized to match .profile-vip-badge's own em-relative sizing so a custom
   tag pill sitting right next to it in profile.php's <h2> doesn't look
   mismatched — the fixed 0.62rem above suits players.php's smaller row
   text, but would look tiny/inconsistent against a big heading. */
.hero-info-wrap h2 .player-tag-badge {
    font-size: 0.55em;
    vertical-align: middle;
}

/* ---------------------------------------------------------------------- */
/* Settings (settings.php)                                                 */
/* ---------------------------------------------------------------------- */

.settings-nav-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 0;
    border-bottom: 1px solid var(--color-border);
}

.settings-nav-row:last-child {
    border-bottom: none;
}

/* Drag-and-drop reordering (2026-07-21, assets/js/settings.js) — same
   press-and-drag Pointer Events pattern and CSS-drawn grip (3 bars via
   box-shadow, not a Unicode glyph) as .party-drag-handle (pc.php/pc.js)
   already uses for party-slot reordering. */
.settings-nav-drag-handle {
    flex: 0 0 auto;
    cursor: grab;
    width: 16px;
    height: 12px;
    padding: 4px 6px;
    touch-action: none;
    user-select: none;
    display: flex;
    align-items: center;
    justify-content: center;
}

.settings-nav-drag-handle::before {
    content: "";
    display: block;
    width: 14px;
    height: 2px;
    background: var(--color-text-muted);
    box-shadow: 0 -4px 0 var(--color-text-muted), 0 4px 0 var(--color-text-muted);
}

.settings-nav-row-dragging {
    opacity: 0.4;
}

.settings-nav-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    font-size: 1.1rem;
}

.settings-nav-label {
    flex: 1 1 auto;
    font-weight: 600;
}

.settings-nav-actions {
    display: flex;
    gap: 2px;
}

.settings-nav-actions .btn {
    width: auto;
    min-height: 36px;
    padding: 0 10px;
}

.settings-account-details {
    display: flex;
    flex-direction: column;
}

.settings-account-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 8px 0;
    border-bottom: 1px solid var(--color-border);
    font-size: 0.9rem;
}

.settings-account-row:last-child {
    border-bottom: none;
}

.settings-checkbox-row {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 0.9rem;
    cursor: pointer;
}

.settings-checkbox-row input[type="checkbox"] {
    width: 16px;
    height: 16px;
    flex-shrink: 0;
}

.settings-rarity-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
    gap: 8px 12px;
}

.settings-alert-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 0;
    border-bottom: 1px solid var(--color-border);
}

.settings-alert-row:last-child {
    border-bottom: none;
}

.settings-alert-summary {
    flex: 1 1 auto;
    font-weight: 600;
}

.settings-alert-form {
    display: grid;
    grid-template-columns: 2fr 1fr 1fr auto;
    gap: 10px;
    align-items: end;
}

@media (max-width: 600px) {
    .settings-alert-form {
        grid-template-columns: 1fr;
    }
}

/* Collapsible settings cards (e.g. Menu Order) — collapsed by default,
   same pattern as .global-activity-bar/.global-activity-toggle above:
   assets/js/settings.js toggles the .expanded class and remembers the
   choice in localStorage so it survives page loads. */
.settings-collapsible-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    /* The whole header is the click target now, not just the small caret
       icon (2026-07-27 fix, see assets/js/settings.js's comment) — cursor
       makes that discoverable instead of looking like plain label text. */
    cursor: pointer;
}

.settings-collapsible-toggle {
    background: none;
    border: none;
    cursor: pointer;
    color: var(--color-text-muted);
    font-size: 0.9rem;
    padding: 4px;
    transition: transform 0.2s ease;
}

.settings-collapsible.expanded .settings-collapsible-toggle {
    transform: rotate(180deg);
}

.settings-collapsible-body {
    display: none;
}

.settings-collapsible.expanded .settings-collapsible-body {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

/* ---------------------------------------------------------------------- */
/* Dashboard (index.php)                                                   */
/* ---------------------------------------------------------------------- */

.dashboard-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 20px;
    margin-top: 16px;
}

.dashboard-grid .full-width {
    grid-column: 1 / -1;
}

/* Region-tinted (2026-07-22) — index.php sets --region-accent/
   --region-accent-dark inline per player (includes/sanitize.php's
   region_accent_colors()) so the zone banner reads as "you are somewhere
   specific" instead of a fixed color regardless of region; Kanto (and
   anywhere the helper doesn't recognize) simply never sets the variables,
   falling back to the original --color-primary look unchanged. The
   repeating-linear-gradient layer is a plain CSS diagonal hatch — texture
   without a new image asset. */
.action-area {
    background-image:
        repeating-linear-gradient(135deg, rgba(255, 255, 255, 0.06) 0px, rgba(255, 255, 255, 0.06) 2px, transparent 2px, transparent 14px),
        linear-gradient(135deg, var(--region-accent, var(--color-primary)), var(--region-accent-dark, #ff6b6b));
    color: #fff;
    padding: 32px;
    border-radius: var(--radius);
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 20px;
    flex-wrap: wrap;
}

.action-area h2 {
    margin: 0 0 6px;
    font-size: 1.6rem;
}

.action-area p {
    margin: 0;
    opacity: 0.92;
}

.btn-explore {
    background: #fff;
    color: var(--color-primary);
    border: none;
    padding: 14px 26px;
    border-radius: 999px;
    font-size: 1rem;
    font-weight: 700;
    cursor: pointer;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
    display: inline-flex;
    align-items: center;
    gap: 8px;
    text-decoration: none;
    white-space: nowrap;
}

.party-list {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.pokemon-card {
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: 10px;
    padding: 12px;
    display: flex;
    align-items: center;
    gap: 14px;
    text-decoration: none;
    color: var(--color-text);
}

.poke-sprite {
    width: 56px;
    height: 56px;
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    background: radial-gradient(circle, #ffffff 40%, transparent 70%);
    border-radius: 50%;
}

.poke-sprite img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}

.poke-details {
    flex: 1 1 auto;
    min-width: 0;
}

.poke-header {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 4px;
    font-weight: 700;
    font-size: 0.92rem;
}

.poke-header .text-muted {
    font-weight: 600;
}

.inventory-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
    gap: 12px;
}

.item-slot {
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: 10px;
    aspect-ratio: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
}

.item-slot img {
    width: 34px;
    height: 34px;
    object-fit: contain;
    image-rendering: pixelated;
}

.item-count {
    position: absolute;
    bottom: -4px;
    right: -4px;
    /* Always a fixed dark badge with white text, regardless of theme — using
       var(--color-text) here would give a pale badge with unreadable white
       text once --color-text turns light in the dark/fire themes. */
    background: rgba(20, 20, 20, 0.85);
    color: #fff;
    font-size: 0.68rem;
    font-weight: 700;
    padding: 1px 5px;
    border-radius: 10px;
    border: 2px solid var(--color-surface);
}

.badge-case {
    display: flex;
    gap: 14px;
    flex-wrap: wrap;
    padding: 10px;
    background: var(--color-bg);
    border-radius: 10px;
    border: 1px solid var(--color-border);
}

.gym-badge {
    width: 48px;
    height: 48px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.4rem;
    border-radius: 50%;
    background: var(--color-bg);
    transition: transform 0.15s ease;
}

.gym-badge:hover {
    transform: scale(1.08);
}

.gym-badge img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.2));
}

.gym-badge.locked {
    opacity: 0.35;
    filter: grayscale(100%);
}

.activity-log {
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin: 0;
    padding: 0;
}

.activity-item {
    display: flex;
    align-items: baseline;
    gap: 12px;
    font-size: 0.9rem;
    padding-bottom: 10px;
    border-bottom: 1px solid var(--color-border);
}

.activity-item:last-child {
    border-bottom: none;
    padding-bottom: 0;
}

.activity-time {
    color: var(--color-text-muted);
    font-size: 0.78rem;
    min-width: 62px;
    flex: 0 0 auto;
}

.activity-icon {
    flex: 0 0 auto;
}

.activity-icon.combat-win { color: var(--color-success); }
.activity-icon.combat-loss { color: var(--color-danger); }
.activity-icon.loot { color: var(--color-warning); }

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

/* ---------------------------------------------------------------------- */
/* Mobile: compact icon-over-label nav-item layout for the bottom bar      */
/* (.app-footer's own fixed/flex-row positioning is already its default). */
/* ---------------------------------------------------------------------- */

@media (max-width: 768px) {
    .app-footer .nav-item {
        flex: 0 0 auto;
        flex-direction: column;
        width: 72px;
        gap: 2px;
        padding: 8px 5px;
        border-radius: 0;
        font-size: 0.68rem;
        justify-content: center;
    }

    /* "More" sheet (includes/layout_footer.php) — everything past
       MOBILE_NAV_PINNED_COUNT, reached via the bottom bar's last slot
       instead of an endless horizontal scroll (player-reported, 2026-07-21).
       Slides up from the bottom, grouped exactly like the desktop sidebars'
       own .sidebar-group/.sidebar-group-title (assets/js/app.js toggles the
       [hidden] attribute). */
    .mobile-nav-more-overlay {
        position: fixed;
        inset: 0;
        background: rgba(0, 0, 0, 0.5);
        display: flex;
        align-items: flex-end;
        justify-content: center;
        z-index: 200;
    }

    /* Bug fix (2026-07-21, player-reported: "expanded and I can't close
       it") — an AUTHOR stylesheet rule always wins over the browser's own
       [hidden] rule regardless of matching specificity (user-agent styles
       lose to author styles by cascade origin, not just specificity), so
       the plain `display: flex` above was permanently overriding the
       [hidden] attribute assets/js/app.js toggles — the sheet rendered
       open from the very first page load and neither the close button nor
       the backdrop click could ever actually hide it (they were setting
       the attribute correctly; nothing was ever reading it). This explicit,
       higher-specificity [hidden] rule restores that. */
    .mobile-nav-more-overlay[hidden] {
        display: none;
    }

    .mobile-nav-more-sheet {
        width: 100%;
        max-height: 75vh;
        overflow-y: auto;
        background: var(--color-surface);
        border-radius: 16px 16px 0 0;
        padding: 16px;
        box-shadow: 0 -4px 18px rgba(0, 0, 0, 0.2);
    }

    .mobile-nav-more-header {
        display: flex;
        align-items: center;
        justify-content: space-between;
        margin-bottom: 12px;
    }

    .mobile-nav-more-close {
        background: none;
        border: none;
        font-size: 1.4rem;
        line-height: 1;
        cursor: pointer;
        color: var(--color-text-muted);
        padding: 4px 8px;
    }

    .mobile-nav-more-group {
        margin-bottom: 16px;
    }

    .mobile-nav-more-group:last-child {
        margin-bottom: 0;
    }

    .mobile-nav-more-group-title {
        display: flex;
        align-items: center;
        gap: 8px;
        font-size: 0.75rem;
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: 0.04em;
        color: var(--color-text-muted);
        margin: 0 0 8px;
    }

    .mobile-nav-more-grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
        gap: 4px;
    }

    .mobile-nav-more-grid .nav-item {
        flex-direction: column;
        gap: 2px;
        padding: 8px 4px;
        font-size: 0.68rem;
        text-align: center;
        justify-content: flex-start;
        min-height: 0;
    }

    .mobile-nav-more-grid .nav-item:hover,
    .mobile-nav-more-grid .nav-item.active {
        background: var(--color-primary-tint);
        color: var(--color-primary);
    }

    .app-shell.has-sidebar {
        gap: 12px;
        padding: 12px;
    }

    .app-content-row {
        flex-direction: column;
        gap: 12px;
    }

    /* Centered logo with the collapse caret pinned to the right edge
       (2026-09-20) — with the header's own duplicate logo gone (see
       .header-modern-brand above), a space-between row left the single
       remaining logo hanging off the left of the header. The caret is
       centered vertically with a margin rather than translateY(-50%) so
       .header-collapsed's rotate(180deg) on it still works untouched. */
    .app-header-mobile-toggle {
        display: flex;
        justify-content: center;
        position: relative;
    }

    .app-header-mobile-toggle-caret {
        position: absolute;
        right: 16px;
        top: 50%;
        margin-top: -0.45em;
    }

    .app-header {
        flex-direction: column;
        align-items: stretch;
        text-align: center;
        gap: 12px;
    }

    /* Collapsed = the whole rest of the header (all 3 header-box columns)
       disappears, leaving only the toggle strip above — assets/js/app.js
       adds/removes this class on .app-header-group and remembers the
       choice in localStorage across page loads. */
    .app-header-group.header-collapsed .app-header {
        display: none;
    }

    /* Pre-paint equivalent of the rule above (2026-07-27 perf pass) — the
       [data-header-collapsed] attribute is set on <html> by an inline
       <head> script (includes/layout_header.php, same technique the theme
       preference right above it already uses) BEFORE first paint, since
       waiting for assets/js/app.js's own class-toggle (a body-end script)
       meant the full header painted once, then vanished a frame later —
       the single largest layout shift found on the battle screen, since
       everything in main.app-main jumps up by the header's whole height.
       app.js still adds the .header-collapsed class too on load, which is
       harmless/redundant once this rule already applies. */
    html[data-header-collapsed] .app-header {
        display: none;
    }

    .header-box-logo {
        flex: 0 0 auto;
        padding: 6px;
    }

    .app-header-logo img {
        height: 104px;
    }

    .header-box-stats {
        padding: 10px 12px;
    }

    .header-tab-row {
        justify-content: center;
        text-align: center;
    }

    .header-tab-badges {
        justify-content: center;
        width: 100%;
    }

    /* The 60/40 bars-next-to-table split only works at desktop widths —
       narrow viewports stack them instead so neither gets squeezed. */
    .header-stats-row {
        flex-direction: column;
        align-items: stretch;
        gap: 10px;
    }

    .header-stat-bars,
    .header-stat-chips {
        flex: 1 1 auto;
    }

    /* Mobile already has icon-based quick actions (.app-footer's bottom
       nav bar) — the header's own Quick Actions box is a desktop-only
       convenience shortcut, not needed a second time here. */
    .header-box-links {
        display: none;
    }

    .app-main {
        padding: 16px 12px calc(var(--footer-height) + 20px);
    }

    .action-area {
        flex-direction: column;
        text-align: center;
        padding: 24px 20px;
    }
}

/* ---------------------------------------------------------------------- */
/* Social: Inbox + Forum                                                   */
/* ---------------------------------------------------------------------- */

.mail-row {
    border-bottom: 1px solid var(--color-border);
    padding: 10px 0;
}

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

.mail-row-header {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    cursor: pointer;
}

.mail-row.unread .mail-subject {
    font-weight: 700;
    color: var(--color-text);
}

.mail-sender {
    font-weight: 600;
    flex: 0 0 auto;
}

.mail-subject {
    flex: 1 1 auto;
    color: var(--color-text-muted);
}

.mail-time {
    font-size: 0.8rem;
    flex: 0 0 auto;
}

.mail-body {
    margin-top: 8px;
    padding: 10px 12px;
    background: var(--color-bg);
    border-radius: var(--radius);
    font-size: 0.9rem;
}

.pagination {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    justify-content: center;
    margin: 16px 0;
}

.activity-log-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

/* shop.php's Shop / VIP Shop tab switcher (assets/js/shop.js) — same
   underlying idea as the header's own .header-tab-row, just a plain flex
   row since there's no second badge group needing independent alignment
   here. */
.shop-tab-row {
    display: flex;
    gap: 4px;
}

.shop-tab {
    display: flex;
    align-items: center;
    gap: 6px;
    background: none;
    border: none;
    border-bottom: 2px solid transparent;
    padding: 4px 2px;
    font-size: 0.95rem;
    font-weight: 700;
    color: var(--color-text-muted);
    cursor: pointer;
    margin-right: 16px;
}

.shop-tab.active {
    color: var(--color-heading);
    border-bottom-color: var(--color-primary);
}

/* vip.php's "What VIP Includes" list. */
.vip-perk-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.vip-perk-row {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    font-size: 0.9rem;
}

.vip-perk-icon {
    flex: 0 0 auto;
    width: 1.3em;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.pagination .btn {
    width: auto;
    min-width: 40px;
    padding: 8px 12px;
}

.forum-category-tabs {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 16px;
}

/* .btn's own base rule sets width:100% (right for a full-width page button,
   wrong for a row of tabs) — .forum-category-tabs' flex-wrap row used to get
   fought into 3 full-width stacked buttons instead of sitting side by side.
   .forum-tab overrides just the sizing; .btn/.btn-primary still supply the
   actual colors (the active tab's inline --region-accent override, set in
   forum.php, wins over .btn-primary's plain --color-primary). */
.forum-tab {
    width: auto;
    display: inline-flex;
    align-items: center;
    gap: 6px;
}

.forum-tab-count {
    font-size: 0.7rem;
    font-weight: 700;
    background: rgba(0, 0, 0, 0.12);
    padding: 1px 7px;
    border-radius: 999px;
}

.forum-tab-active .forum-tab-count {
    background: rgba(255, 255, 255, 0.25);
    color: #fff;
}

/* The "New Topic" compose form (forum.php) — previously bare, unclassed
   <input>/<textarea> sat directly in the header card with zero styling
   (default browser border/font). .form-control now covers the fields
   themselves; this just gives the whole form a contained "you're composing
   something" box instead of floating loose inside the region-tinted header
   card above it. */
.forum-compose-box {
    padding: 12px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    background: var(--color-bg);
}

.forum-topic-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 10px;
    padding: 12px 0;
    border-bottom: 1px solid var(--color-border);
}

.forum-topic-row:last-child {
    border-bottom: none;
}

/* forum.php's topic rows are real <a> links (the whole row is now a click
   target, not just the title text) — support.php/admin_support.php still
   reuse plain .forum-topic-row on a <div>, so this modifier (and the two
   rules below scoped to it) leave those completely unaffected. */
.forum-topic-row-link {
    text-decoration: none;
    color: inherit;
    padding: 10px 8px;
    margin: 0 -8px;
    border-radius: var(--radius-sm);
    transition: background 0.12s ease;
}

.forum-topic-row-link:hover {
    background: rgba(128, 128, 128, 0.07);
}

.forum-topic-row.forum-topic-row-link {
    justify-content: flex-start;
}

.forum-topic-avatar,
.forum-topic-avatar-placeholder {
    width: 38px;
    height: 38px;
    border-radius: 50%;
    flex: 0 0 auto;
}

.forum-topic-avatar {
    object-fit: cover;
    background: var(--color-bg);
}

.forum-topic-avatar-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.05rem;
    color: var(--color-text-muted);
    background: rgba(128, 128, 128, 0.08);
}

.forum-topic-info {
    flex: 1 1 auto;
    min-width: 0;
}

.forum-topic-title {
    font-weight: 700;
    color: var(--color-text);
    text-decoration: none;
    display: block;
}

.forum-topic-hot-dot {
    display: inline-block;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: #22c55e;
    margin-right: 5px;
}

.forum-topic-meta {
    font-size: 0.8rem;
    color: var(--color-text-muted);
}

.forum-topic-stats {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 3px;
    flex: 0 0 auto;
}

.forum-topic-reply-count {
    font-size: 0.72rem;
    font-weight: 700;
    color: var(--color-primary-dark);
    background: var(--color-primary-tint);
    padding: 2px 9px;
    border-radius: 999px;
    white-space: nowrap;
}

.forum-topic-last-activity {
    font-size: 0.7rem;
    white-space: nowrap;
}

.forum-post {
    padding: 14px 0;
    border-bottom: 1px solid var(--color-border);
}

.forum-post:last-child {
    border-bottom: none;
}

.forum-post-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-size: 0.85rem;
    color: var(--color-text-muted);
    margin-bottom: 6px;
}

/* Wraps the avatar + author name (forum_topic.php) — .forum-post-header's
   own justify-content:space-between still puts this flush left and the
   timestamp flush right, same as before this pair existed; support_ticket.php
   doesn't render this wrapper at all, so its plain author-name-only header
   is completely unaffected. */
.forum-post-identity {
    display: flex;
    align-items: center;
    gap: 8px;
}

.forum-post-avatar,
.forum-post-avatar-placeholder {
    width: 30px;
    height: 30px;
    border-radius: 50%;
    flex: 0 0 auto;
}

.forum-post-avatar {
    object-fit: cover;
    background: var(--color-bg);
}

.forum-post-avatar-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.85rem;
    color: var(--color-text-muted);
    background: rgba(128, 128, 128, 0.08);
}

.forum-post-author {
    font-weight: 700;
    color: var(--color-text);
}

/* Same shape as .badge-staff (support_ticket.php's "Staff" reply tag) —
   the original post's author gets an "OP" tag instead. */
.forum-op-badge {
    background: var(--color-primary-tint);
    color: var(--color-primary);
    margin: 0 0 0 6px;
    vertical-align: middle;
}

.forum-category-badge {
    display: inline-block;
    padding: 3px 10px;
    font-size: 0.7rem;
    font-weight: 700;
    border-radius: 12px;
}

.forum-post-body {
    white-space: pre-wrap;
    font-size: 0.92rem;
}

/* ---------------------------------------------------------------------- */
/* Marketplace (marketplace.php)                                          */
/* ---------------------------------------------------------------------- */

/* The 3 "List a..." forms, side by side once there's room instead of
   stacked full-width cards. */
.listing-forms-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: 16px;
    margin-top: 4px;
}

.form-group {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.form-group label {
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--color-text-muted);
}

.form-control {
    padding: 8px 10px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    font-size: 0.9rem;
    background: var(--color-surface);
    color: var(--color-text);
    width: 100%;
    font-family: inherit;
}

.form-text-muted {
    font-size: 0.72rem;
    color: var(--color-text-muted);
}

/* Layered onto .pc-box-cell (not replacing it — that class is shared with
   the Bag/Box/Achievements grids) so a listing card gets its own richer
   treatment without changing those other grids. */
.marketplace-listing-card {
    justify-content: space-between;
    padding: 14px 10px;
    transition: transform 0.15s ease, box-shadow 0.15s ease;
}

.marketplace-listing-card:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}

.marketplace-listing-card .pc-box-name {
    font-size: 0.85rem;
    font-weight: 700;
    margin-top: 4px;
}

.price-tag {
    font-weight: 700;
    color: var(--color-warning);
    font-size: 0.95rem;
    margin: 6px 0;
}

.badge {
    display: inline-block;
    padding: 3px 10px;
    font-size: 0.66rem;
    font-weight: 700;
    border-radius: 12px;
    margin: 0 auto 4px;
}

.badge-buy-now {
    background: rgba(26, 127, 55, 0.15);
    color: var(--color-success);
}

.badge-auction {
    background: rgba(59, 108, 224, 0.15);
    color: var(--color-accent);
}

/* Layered onto .pc-box-cell (2026-08-01, player request: "same layout as
   the shop") — same relocation .marketplace-listing-card above did, moving
   crafting.php off a stacked list of full-width rows onto a card grid. A
   recipe's ingredient list runs longer than the shop's single price line,
   so this gets extra vertical room + a left-aligned ingredient block
   instead of .pc-box-cell's default centered/compact treatment. */
.crafting-recipe-card {
    justify-content: space-between;
    align-items: stretch;
    padding: 12px 10px;
    min-height: 180px;
}

.crafting-recipe-card .pc-box-sprite {
    display: block;
    margin: 0 auto;
}

.crafting-recipe-card .pc-box-name {
    font-size: 0.78rem;
    white-space: normal;
    margin-top: 4px;
}

.crafting-recipe-card .pc-box-level {
    font-size: 0.68rem;
    font-weight: 600;
    margin-top: 2px;
}

.crafting-ingredient-list {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin-top: 8px;
    font-size: 0.7rem;
    text-align: left;
}

.crafting-ingredient-ok {
    color: var(--color-success);
}

.crafting-ingredient-missing {
    color: var(--color-danger);
}

/* ---------------------------------------------------------------------- */
/* Dev Reports (admin_dev_reports.php, admin-only)                         */
/* ---------------------------------------------------------------------- */

.dev-report-section-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
}

.dev-report-row {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 12px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
}

.dev-report-row-done {
    opacity: 0.7;
}

.dev-report-row-header {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}

.dev-report-row-header .badge {
    margin: 0;
}

.dev-report-description {
    margin: 0;
    font-size: 0.9rem;
    white-space: pre-wrap;
}

.dev-report-meta {
    font-size: 0.75rem;
}

.dev-report-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.btn-compact {
    width: auto;
    min-height: 34px;
    padding: 0 12px;
    font-size: 0.8rem;
}

.btn-danger {
    background: var(--color-danger-tint);
    border-color: var(--color-danger);
    color: var(--color-danger);
}

.badge-bug {
    background: var(--color-danger-tint);
    color: var(--color-danger);
}

.badge-feature {
    background: rgba(59, 108, 224, 0.15);
    color: var(--color-accent);
}

/* ---------------------------------------------------------------------- */
/* Support (support.php/support_ticket.php/admin_support.php) — category   */
/* and status badges. Reuses .badge-bug above for category=bug so a "Bug"  */
/* pill reads identically whether it came from a support ticket or the old */
/* dev-report list. Rows/threads themselves reuse .forum-topic-row/        */
/* .forum-post wholesale (see forum.php) rather than new markup.           */
/* ---------------------------------------------------------------------- */

.badge-idea {
    background: rgba(241, 196, 15, 0.18);
    color: #a3790a;
}

.badge-question {
    background: rgba(59, 108, 224, 0.15);
    color: var(--color-accent);
}

.badge-status-open {
    background: rgba(241, 196, 15, 0.18);
    color: #a3790a;
}

.badge-status-answered {
    background: rgba(26, 127, 55, 0.15);
    color: var(--color-success);
}

.badge-status-closed {
    background: var(--color-panel);
    color: var(--color-text-muted);
}

.forum-topic-title .badge {
    margin: 0 6px 0 0;
    vertical-align: middle;
}

/* Quick-scan status accent (2026-07-23, support ticket list redesign) — a
   thin left border in the same color as that row's status badge, so a
   ticket queue reads at a glance without having to read every badge text.
   Layered onto .forum-topic-row-link (support.php/admin_support.php only —
   forum.php's own topic rows never carry a status, so they never get one
   of these classes and stay exactly as before). */
.forum-topic-row-status-open {
    border-left: 3px solid #a3790a;
}

.forum-topic-row-status-answered {
    border-left: 3px solid var(--color-success);
}

.forum-topic-row-status-closed {
    border-left: 3px solid var(--color-text-muted);
    opacity: 0.75;
}

.badge-staff {
    background: var(--color-primary-tint);
    color: var(--color-primary);
    margin: 0 0 0 6px;
    vertical-align: middle;
}

/* ---------------------------------------------------------------------- */
/* Admin panel (admin.php, admin_settings.php, admin_table.php,            */
/* admin_players.php) — reuses .card/.stack/.btn/.form-error throughout,   */
/* this block is only the admin-specific layout on top of those.           */
/* ---------------------------------------------------------------------- */

.admin-badge-link {
    text-decoration: none;
    font-weight: 600;
}

.admin-hub-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 12px;
}

.admin-hub-card {
    display: flex;
    flex-direction: column;
    gap: 6px;
    text-decoration: none;
    color: inherit;
    transition: border-color 0.15s ease;
}

.admin-hub-card:hover {
    border-color: var(--color-accent);
}

.admin-hub-icon {
    font-size: 1.5rem;
}

.admin-hub-title {
    font-size: 1rem;
}

.admin-hub-desc {
    font-size: 0.8rem;
}

.admin-setting-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 8px 0;
    border-top: 1px solid var(--color-border);
}

.admin-setting-row:first-of-type {
    border-top: none;
}

.admin-setting-label {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.admin-setting-help {
    font-size: 0.75rem;
}

.admin-setting-input {
    width: 120px;
    flex-shrink: 0;
    padding: 8px 10px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--color-border);
    background: var(--color-bg);
    color: var(--color-text);
    font-family: inherit;
    font-size: 0.9rem;
}

/* Game Settings page: search/jump-nav/collapsible-groups shell added
   2026-08-01 once the registry grew past 120 settings across 24 groups —
   see admin_settings.php. Kept intentionally simple (no position:sticky
   anywhere — no existing precedent elsewhere in this codebase to match
   against, and a wrapping chip-list top-of-page ToC solves the same
   "too much to scroll through blindly" problem without it). */
.admin-settings-controls {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
}

.admin-settings-search {
    flex: 1 1 240px;
    min-width: 0;
    padding: 8px 12px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--color-border);
    background: var(--color-bg);
    color: var(--color-text);
    font-family: inherit;
    font-size: 0.9rem;
}

.admin-settings-count {
    font-size: 0.8rem;
    white-space: nowrap;
}

.admin-settings-jumpnav {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

.admin-settings-jumpnav a {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 4px 10px;
    border-radius: 999px;
    border: 1px solid var(--color-border);
    background: var(--color-bg);
    color: var(--color-text);
    text-decoration: none;
    font-size: 0.78rem;
    transition: border-color 0.15s ease;
}

.admin-settings-jumpnav a:hover {
    border-color: var(--color-accent);
}

.admin-settings-jumpnav a[hidden] {
    display: none;
}

.admin-settings-jumpnav .count {
    opacity: 0.6;
    font-variant-numeric: tabular-nums;
}

.admin-settings-group > summary {
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    list-style: none;
}

.admin-settings-group > summary::-webkit-details-marker {
    display: none;
}

.admin-settings-group > summary::after {
    content: '\25BE';
    transition: transform 0.15s ease;
    opacity: 0.6;
    flex-shrink: 0;
}

.admin-settings-group:not([open]) > summary::after {
    transform: rotate(-90deg);
}

.admin-settings-group-count {
    font-size: 0.75rem;
    font-weight: 400;
    opacity: 0.6;
    margin-left: 6px;
}

.admin-settings-group[hidden],
.admin-setting-row[hidden] {
    display: none;
}

.admin-settings-empty {
    text-align: center;
    padding: 24px 0;
}

.admin-table-wrap {
    overflow-x: auto;
}

.admin-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.85rem;
}

.admin-table th,
.admin-table td {
    padding: 8px 10px;
    text-align: left;
    border-bottom: 1px solid var(--color-border);
    white-space: nowrap;
}

.admin-table th {
    color: var(--color-text-muted);
    font-weight: 600;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
}

.admin-table-actions {
    display: flex;
    gap: 6px;
}

.admin-form-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 12px;
}

.admin-form-field {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.admin-form-field label {
    font-size: 0.75rem;
    color: var(--color-text-muted);
}

.admin-form-field input,
.admin-form-field select,
.admin-form-field textarea {
    padding: 8px 10px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--color-border);
    background: var(--color-bg);
    color: var(--color-text);
    font-family: inherit;
    font-size: 0.9rem;
}

.admin-stat-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
    gap: 12px;
}

.admin-stat-tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 10px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--color-border);
    background: var(--color-bg);
}

.admin-stat-value {
    font-size: 1.3rem;
    font-weight: 700;
    color: var(--color-text);
}

.admin-stat-label {
    font-size: 0.7rem;
    color: var(--color-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.03em;
}

.admin-chart-wrap {
    width: 100%;
    overflow-x: auto;
}

.admin-chart {
    width: 100%;
    height: auto;
    display: block;
}

.admin-chart .admin-chart-bar {
    fill: var(--color-accent);
}

.admin-chart .admin-chart-line {
    fill: none;
    stroke: var(--color-accent);
    stroke-width: 2;
}

.admin-chart .admin-chart-point {
    fill: var(--color-accent);
}

.admin-chart .admin-chart-axis-label {
    fill: var(--color-text-muted);
    font-size: 9px;
}

.admin-player-search {
    display: flex;
    gap: 8px;
}

.admin-player-search input {
    flex: 1;
    padding: 10px 14px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--color-border);
    background: var(--color-bg);
    color: var(--color-text);
    font-family: inherit;
    font-size: 0.9rem;
}

.admin-badge-flag {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 0.7rem;
    font-weight: 700;
    margin-left: 6px;
}

.admin-badge-admin {
    background: rgba(59, 108, 224, 0.15);
    color: var(--color-accent);
}

.admin-badge-banned {
    background: var(--color-danger-tint);
    color: var(--color-danger);
}

.admin-badge-vip {
    background: rgba(232, 161, 58, 0.18);
    color: #a9701a;
}

/* Player Tags card (admin_players.php) — each assigned tag renders as its
   normal .player-tag-badge (app.css's Trainer rankings section) plus a
   small revoke "x" right next to it. */
.admin-player-tags-list {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.admin-player-tag-row {
    display: inline-flex;
    align-items: center;
    gap: 4px;
}

.admin-icon-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 0;
    border-top: 1px solid var(--color-border);
}

.admin-icon-row:first-of-type {
    border-top: none;
}

.admin-icon-preview {
    flex-shrink: 0;
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
}

.admin-icon-preview img {
    width: 24px;
    height: 24px;
    object-fit: contain;
}

.admin-icon-label {
    flex: 0 0 160px;
    font-size: 0.85rem;
    font-family: monospace;
}

.admin-icon-input {
    flex: 1;
    min-width: 0;
    padding: 8px 10px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--color-border);
    background: var(--color-bg);
    color: var(--color-text);
    font-family: inherit;
    font-size: 0.85rem;
}

/* Icon Picker modal (assets/js/admin_icon_picker.js) — browses images/items/
   for admin_icons.php's rows and admin_table.php's Items Sprite Path field.
   Built dynamically by JS rather than server-rendered per-page, but styled
   to match the existing .pokedex-modal-overlay pattern. */
.icon-picker-field {
    display: flex;
    gap: 6px;
    align-items: center;
    flex: 1;
    min-width: 0;
}

.icon-picker-field .icon-picker-input {
    flex: 1;
    min-width: 0;
}

.icon-picker-overlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    z-index: 1000;
    align-items: center;
    justify-content: center;
    padding: 20px;
}

.icon-picker-overlay.visible {
    display: flex;
}

.icon-picker-modal {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    width: min(880px, 100%);
    height: min(600px, 100%);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.icon-picker-header {
    display: flex;
    gap: 8px;
    align-items: center;
    padding: 12px;
    border-bottom: 1px solid var(--color-border);
    flex-shrink: 0;
}

.icon-picker-search {
    flex: 1;
    padding: 8px 10px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--color-border);
    background: var(--color-bg);
    color: var(--color-text);
    font-family: inherit;
    font-size: 0.85rem;
}

.icon-picker-close {
    background: none;
    border: none;
    font-size: 1.4rem;
    line-height: 1;
    cursor: pointer;
    color: var(--color-text);
    padding: 4px 8px;
    flex-shrink: 0;
}

.icon-picker-close:hover {
    color: var(--color-heading);
}

.icon-picker-body {
    display: flex;
    flex: 1;
    min-height: 0;
}

.icon-picker-folders {
    width: 190px;
    flex-shrink: 0;
    overflow-y: auto;
    border-right: 1px solid var(--color-border);
    padding: 8px;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.icon-picker-folder-btn {
    text-align: left;
    background: none;
    border: none;
    padding: 6px 8px;
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-size: 0.8rem;
    color: var(--color-text);
}

.icon-picker-folder-btn:hover {
    background: var(--color-panel);
}

.icon-picker-folder-btn.active {
    background: var(--color-primary);
    color: #fff;
}

.icon-picker-grid {
    flex: 1;
    overflow-y: auto;
    padding: 12px;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
    gap: 10px;
    align-content: start;
}

.icon-picker-thumb {
    background: none;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    padding: 6px;
    cursor: pointer;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
}

.icon-picker-thumb:hover {
    border-color: var(--color-primary);
    background: var(--color-panel);
}

.icon-picker-thumb img {
    width: 40px;
    height: 40px;
    object-fit: contain;
}

.icon-picker-thumb span {
    font-size: 0.65rem;
    text-align: center;
    word-break: break-all;
    color: var(--color-text-muted);
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.admin-run-result-body {
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    padding: 12px;
    font-family: monospace;
    font-size: 0.8rem;
    white-space: pre-wrap;
    word-break: break-word;
    max-height: 320px;
    overflow-y: auto;
    color: var(--color-text);
}

/* ---------------------------------------------------------------------- */
/* Mosaic exploration grid (map.php) — 7 clickable maps reproducing a      */
/* supplied reference image map's exact tile proportions. Each             */
/* .mosaic-tile's flex-grow/aspect-ratio come inline from PHP (see         */
/* includes/mosaic_zones.php's width/height per tile) — .mosaic-row's job  */
/* is just to lay those out edge-to-edge at the right relative sizes.      */
/* ---------------------------------------------------------------------- */

.mosaic-grid {
    display: flex;
    flex-direction: column;
    gap: 0;
    max-width: 580px;
    margin: 12px auto 0;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    overflow: hidden;
}

.mosaic-row {
    display: flex;
    gap: 0;
}

.mosaic-tile {
    position: relative;
    overflow: hidden;
    flex: 0 1 0%;
    min-width: 0;
    display: block;
    padding: 0;
    margin: 0;
    border: none;
    cursor: pointer;
    background: var(--color-panel);
}

.mosaic-tile::after {
    content: "";
    position: absolute;
    inset: 0;
    background: transparent;
    transition: background-color 0.15s ease;
    pointer-events: none;
}

.mosaic-tile:hover::after {
    background: rgba(255, 255, 255, 0.12);
}

.mosaic-tile:disabled {
    opacity: 0.6;
    cursor: not-allowed;
}

/* A locked tile (missing Fishing Rod/Cave Suit) still shows its image and
   overlay (so the theme/species list stays visible as a preview) but reads
   as unavailable — dimmed image, no hover tint, not-allowed cursor. The
   click handler (assets/js/map.js) still intercepts and shows an error
   rather than relying on CSS alone to block the request. */
.mosaic-tile.locked {
    cursor: not-allowed;
}

.mosaic-tile.locked .mosaic-tile-img {
    filter: grayscale(0.6) brightness(0.6);
}

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

.mosaic-tile-overlay {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
    padding: 6px 8px;
    background: linear-gradient(to top, rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0));
    text-align: left;
    color: #fff;
    line-height: 1.3;
}

.mosaic-tile-label {
    font-weight: 700;
    font-size: 0.9rem;
}

.mosaic-tile-subtitle {
    font-size: 0.72rem;
    opacity: 0.85;
}

.mosaic-tile-types {
    font-size: 0.62rem;
    opacity: 0.75;
}

@media (max-width: 480px) {
    .mosaic-tile-types {
        display: none;
    }

    .mosaic-tile-label {
        font-size: 0.75rem;
    }

    .mosaic-tile-subtitle {
        font-size: 0.62rem;
    }
}

/* ==========================================================================
   Challenge chain — a "beat these N things in sequence, then unlock a
   final prize/fight" stepper. Originally legacy_starters.php-only (as
   .legacy-starter-chain-*); generalized 2026-07-22 (player request: gym.php
   "follow the same approach as legacy_starters") since gym.php's own gauntlet
   -> Leader progression is the exact same shape (beat 5 trainers in order,
   then unlock one final fight) — both pages share this component now
   instead of gym.php growing a near-duplicate of its own.
   Same "bordered card" language as the rest of the app. Mobile-first:
   stacked vertically (arrows rotated) by default. Desktop (min-width:769px,
   same breakpoint every other desktop-only layout in this file uses) locks
   to a single non-wrapping row — narrower steps + a smaller final sprite so
   all steps + the final box genuinely fit side by side instead of the
   final box wrapping onto its own row; .challenge-chain-scroll gives a
   horizontal scrollbar as a safety net on a genuinely narrow desktop
   window rather than silently wrapping again.
   ========================================================================== */
.challenge-chain-scroll {
    overflow-x: auto;
}

.challenge-chain {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
}

.challenge-chain-step {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 6px;
    background: rgba(128, 128, 128, 0.05);
    border: 1px dashed var(--color-border-light);
    border-radius: var(--radius-sm);
    padding: 10px 10px;
}

.challenge-chain-step-done {
    border-style: solid;
    border-color: var(--color-success);
    background: rgba(26, 127, 55, 0.08);
}

.challenge-chain-step-locked {
    opacity: 0.55;
}

.challenge-chain-step-badge {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    flex-shrink: 0;
    border-radius: 50%;
    background: var(--color-panel);
    border: 1px solid var(--color-border);
    font-weight: 700;
    font-size: 0.85rem;
    color: var(--color-heading);
}

.challenge-chain-step-done .challenge-chain-step-badge {
    background: var(--color-success);
    border-color: var(--color-success);
    color: #fff;
}

.challenge-chain-step-desc {
    font-size: 0.78rem;
    color: var(--color-text);
}

.challenge-chain-arrow {
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--color-text-muted);
    font-size: 1.2rem;
    transform: rotate(90deg);
}

.challenge-chain-final {
    justify-content: center;
}

.challenge-chain-sprite {
    width: 56px;
    height: 56px;
    object-fit: contain;
    image-rendering: pixelated;
}

@media (min-width: 769px) {
    .challenge-chain {
        flex-direction: row;
        flex-wrap: nowrap;
    }

    .challenge-chain-step {
        flex: 1 1 0;
        min-width: 108px;
    }

    .challenge-chain-arrow {
        flex: 0 0 auto;
        transform: none;
    }
}
