:root {
    --bg: #12151a;
    --grid-bg: #1c2027;

    --panel-bg: rgba(24, 28, 35, 0.88);
    --panel-border: rgba(255, 255, 255, 0.07);
    --text-primary: #eef1f5;
    --text-muted: #8b93a3;

    --admin-accent: #35d0c0;
    --admin-accent-soft: rgba(53, 208, 192, 0.14);
    --admin-accent-strong: rgba(53, 208, 192, 0.35);

    --player-accent: #e6832e;
    --player-accent-soft: rgba(230, 131, 46, 0.14);
    --player-accent-strong: rgba(230, 131, 46, 0.35);

    --danger: #e0554f;
    --danger-soft: rgba(224, 85, 79, 0.14);

    --ok: #43c17a;
    --bad: #e0554f;

    --tax: #e0b93d;
    --tax-soft: rgba(224, 185, 61, 0.14);

    --font-display: 'Space Grotesk', 'Segoe UI', sans-serif;
    --font-body: 'Inter', 'Segoe UI', sans-serif;

    /* Per-category accent colors - drive both the build-rail (left icon
       dock) and each category's flyout panel. */
    --cat-buildings: #e6832e;
    --cat-plots: #5bc26a;
    --cat-taxes: #e0b93d;
    --cat-bank: #4f9de6;
    --cat-storage: #a878e6;
    --cat-resource: #35d0c0;
    --cat-stock: #e6547a;
    --cat-global: #6f7ae6;
}

* { box-sizing: border-box; margin: 0; padding: 0; user-select: none; }

body {
    background-color: var(--bg);
    background-image: radial-gradient(circle at 15% 10%, rgba(53, 208, 192, 0.05), transparent 45%),
                       radial-gradient(circle at 85% 90%, rgba(230, 131, 46, 0.05), transparent 45%);
    overflow: hidden;
    width: 100vw;
    height: 100vh;
    font-family: var(--font-body);
    color: var(--text-primary);
}

/* ---------- Logged-in user bar ---------- */

#user-bar {
    position: absolute;
    top: 16px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 110;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 14px;
    background: var(--panel-bg);
    border: 1px solid var(--panel-border);
    border-radius: 999px;
    backdrop-filter: blur(10px);
    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
    font-size: 13px;
}
.user-bar-name { font-weight: 600; }
.user-bar-role {
    font-family: var(--font-display);
    font-size: 10.5px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
    background: rgba(255, 255, 255, 0.06);
    padding: 2px 8px;
    border-radius: 999px;
}
.balance-menu {
    position: relative;
    flex-shrink: 0;
}

.user-bar-balance {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 13px;
    font-variant-numeric: tabular-nums;
    color: var(--ok);
    background: rgba(67, 193, 122, 0.12);
    padding: 3px 10px;
    border-radius: 999px;
    border: 1px solid rgba(67, 193, 122, 0.3);
    cursor: pointer;
}
.user-bar-balance:hover { background: rgba(67, 193, 122, 0.2); }

.balance-caret {
    color: var(--ok);
    opacity: 0.75;
    transition: transform 0.15s ease;
}
.balance-menu.is-open .balance-caret { transform: rotate(180deg); }

/* ---------- Income breakdown dropdown ---------- */

.balance-dropdown {
    /* Same accent-driven badge/border pattern as the .build-flyout panels
       (see .flyout-badge and #build-flyout etc.) - just scoped locally
       instead of by category id. */
    --player-accent: var(--ok);

    position: absolute;
    top: calc(100% + 10px);
    left: 50%;
    transform: translateX(-50%);
    z-index: 120;
    width: 250px;
    padding: 14px;
    background: var(--panel-bg);
    border: 2px solid var(--player-accent);
    border-radius: 14px;
    backdrop-filter: blur(10px);
    box-shadow: 0 16px 38px rgba(0, 0, 0, 0.45);
    display: flex;
    flex-direction: column;
    gap: 10px;
    user-select: text;
}
.balance-dropdown[hidden] { display: none; }

.income-breakdown-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.income-row {
    padding: 10px 10px 8px;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 10px;
}

.income-row-header {
    display: flex;
    align-items: center;
    gap: 7px;
    font-size: 12.5px;
}

.income-row-label {
    flex: 1;
    color: var(--text-primary);
    font-weight: 500;
}

.income-row-gross {
    font-family: var(--font-display);
    font-weight: 600;
    color: var(--ok);
}
.income-row-gross--cost { color: var(--danger); }

.income-row-detail {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 4px 2px 0 19px;
    font-size: 11.5px;
    color: var(--text-muted);
}
.income-row-detail span:last-child { font-family: var(--font-display); font-weight: 600; }
.income-row-detail--loan span:last-child { color: var(--danger); }
.income-row-detail--tax span:last-child { color: var(--tax); }

.income-net-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding-top: 10px;
    border-top: 1px solid rgba(255, 255, 255, 0.08);
    font-size: 13px;
    font-weight: 600;
}
.income-net-row span:last-child {
    font-family: var(--font-display);
    color: var(--ok);
    font-size: 15px;
}
.online-pill {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 12px;
    font-variant-numeric: tabular-nums;
    color: var(--ok);
    background: rgba(67, 193, 122, 0.12);
    padding: 3px 10px;
    border-radius: 999px;
    border: 1px solid rgba(67, 193, 122, 0.3);
    flex-shrink: 0;
    white-space: nowrap;
}
.online-pill-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--ok);
    box-shadow: 0 0 0 2px rgba(67, 193, 122, 0.25);
    flex-shrink: 0;
    animation: online-pill-pulse 2s ease-in-out infinite;
}
@keyframes online-pill-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.4; }
}
.user-bar-logout {
    color: var(--text-muted);
    text-decoration: none;
    font-size: 12.5px;
    border-left: 1px solid var(--panel-border);
    padding-left: 10px;
    flex-shrink: 0;
}
.user-bar-logout:hover { color: var(--text-primary); }

/* ---------- Username dropdown (change password) ---------- */

.user-menu {
    position: relative;
    flex-shrink: 0;
}

.user-menu-trigger {
    display: flex;
    align-items: center;
    gap: 8px;
    background: none;
    border: none;
    padding: 2px 2px;
    color: var(--text-primary);
    font-family: var(--font-body);
    cursor: pointer;
    border-radius: 999px;
    flex-shrink: 0;
}

.user-menu-trigger:hover .user-bar-name { color: var(--text-primary); }

.user-menu-caret {
    color: var(--text-muted);
    transition: transform 0.15s ease;
}

.user-menu.is-open .user-menu-caret { transform: rotate(180deg); }

.user-menu-dropdown {
    /* Same accent-driven badge/border pattern as the .build-flyout panels
       (see .flyout-badge and #build-flyout etc.) - just scoped locally
       instead of by category id. */
    --player-accent: var(--admin-accent);

    position: absolute;
    top: calc(100% + 10px);
    left: 0;
    z-index: 120;
    width: 240px;
    padding: 14px;
    background: var(--panel-bg);
    border: 2px solid var(--player-accent);
    border-radius: 14px;
    backdrop-filter: blur(10px);
    box-shadow: 0 16px 38px rgba(0, 0, 0, 0.45);
    display: flex;
    flex-direction: column;
    gap: 10px;
    user-select: text;
}
.user-menu-dropdown[hidden] { display: none; }

.user-menu-section-label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-muted);
}

.user-menu-form {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.auth-input--sm {
    font-size: 12.5px;
    padding: 8px 10px;
}

.user-menu-submit {
    justify-content: center;
    margin-top: 2px;
    border-color: var(--admin-accent-strong);
    background: var(--admin-accent-soft);
}

/* ---------- Inbox ---------- */

.inbox-menu {
    position: relative;
    flex-shrink: 0;
}

.inbox-btn {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    flex-shrink: 0;
    border-radius: 50%;
    border: 1px solid rgba(224, 185, 61, 0.4);
    background: rgba(224, 185, 61, 0.14);
    color: var(--tax);
    cursor: pointer;
    transition: background 0.15s ease, transform 0.1s ease;
}
.inbox-btn svg { flex-shrink: 0; }
.inbox-btn:hover { background: rgba(224, 185, 61, 0.26); }
.inbox-btn:active { transform: scale(0.92); }
.inbox-menu.is-open .inbox-btn { background: rgba(224, 185, 61, 0.26); }

.inbox-badge {
    position: absolute;
    top: -5px;
    right: -5px;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: var(--font-display);
    font-size: 9.5px;
    font-weight: 700;
    line-height: 1;
    color: #1a1300;
    background: var(--tax);
    border: 1.5px solid var(--panel-bg);
    border-radius: 999px;
}
.inbox-badge[hidden] { display: none; }

.inbox-dropdown {
    /* Same accent-driven badge/border pattern as the .build-flyout panels
       (see .flyout-badge and #build-flyout etc.) - just scoped locally
       instead of by category id. */
    --player-accent: var(--tax);

    position: absolute;
    top: calc(100% + 10px);
    right: 0;
    z-index: 120;
    width: 280px;
    max-height: 360px;
    padding: 16px;
    background: var(--panel-bg);
    border: 2px solid var(--player-accent);
    border-radius: 14px;
    backdrop-filter: blur(10px);
    box-shadow: 0 16px 38px rgba(0, 0, 0, 0.45);
    display: flex;
    flex-direction: column;
    gap: 12px;
    user-select: text;
}
.inbox-dropdown[hidden] { display: none; }

.inbox-message-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
    overflow-y: auto;
    max-height: 260px;

    /* Firefox: cienki suwak w kolorze motywu, ten sam wzorzec co .restock-panel */
    scrollbar-width: thin;
    scrollbar-color: rgba(224, 185, 61, 0.45) transparent;
}

/* Chrome / Edge / Safari: stylizacja suwaka dla listy wiadomości inboksu */
.inbox-message-list::-webkit-scrollbar {
    width: 8px;
}
.inbox-message-list::-webkit-scrollbar-track {
    background: transparent;
    margin: 4px 0;
}
.inbox-message-list::-webkit-scrollbar-thumb {
    background-color: rgba(224, 185, 61, 0.45);
    border-radius: 8px;
    border: 2px solid transparent;
    background-clip: padding-box;
}
.inbox-message-list::-webkit-scrollbar-thumb:hover {
    background-color: var(--tax);
}

.inbox-message {
    padding: 9px 10px;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 10px;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.inbox-message-type {
    font-family: var(--font-display);
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
}
.inbox-message--transfer .inbox-message-type { color: var(--ok); }
.inbox-message--global .inbox-message-type { color: var(--admin-accent); }

.inbox-message-body {
    font-size: 12.5px;
    color: var(--text-primary);
    line-height: 1.4;
    word-break: break-word;
}

/* ---------- Transfer money ---------- */

.transfer-menu {
    position: relative;
    flex-shrink: 0;
}

.transfer-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    flex-shrink: 0;
    border-radius: 50%;
    border: 1px solid rgba(87, 157, 230, 0.4);
    background: rgba(87, 157, 230, 0.16);
    color: #6fb3ea;
    cursor: pointer;
    transition: background 0.15s ease, transform 0.1s ease;
}
.transfer-btn svg { flex-shrink: 0; }
.transfer-btn:hover { background: rgba(87, 157, 230, 0.28); }
.transfer-btn:active { transform: scale(0.92); }
.transfer-menu.is-open .transfer-btn { background: rgba(87, 157, 230, 0.28); }

.transfer-dropdown {
    /* Same accent-driven badge/border pattern as the .build-flyout panels
       (see .flyout-badge and #build-flyout etc.) - just scoped locally
       instead of by category id. */
    --player-accent: #6fb3ea;

    position: absolute;
    top: calc(100% + 10px);
    right: 0;
    z-index: 120;
    width: 260px;
    padding: 16px;
    background: var(--panel-bg);
    border: 2px solid var(--player-accent);
    border-radius: 14px;
    backdrop-filter: blur(10px);
    box-shadow: 0 16px 38px rgba(0, 0, 0, 0.45);
    display: flex;
    flex-direction: column;
    gap: 12px;
    user-select: text;
}
.transfer-dropdown[hidden] { display: none; }

.transfer-submit {
    justify-content: center;
    width: 100%;
    margin-top: 2px;
    border-color: rgba(87, 157, 230, 0.4);
    background: rgba(87, 157, 230, 0.16);
}

.transfer-tax-preview {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 10px 12px;
    background: rgba(255, 184, 76, 0.12);
    border: 1px solid rgba(255, 184, 76, 0.35);
    border-radius: 9px;
    font-size: 12.5px;
}

.transfer-tax-line {
    color: #ffcd7a;
    font-weight: 600;
}

.transfer-receive-line {
    color: var(--text-muted);
}
.transfer-receive-line strong { color: var(--ok); }

.own-color-dot {
    display: inline-block;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    vertical-align: middle;
}

/* ---------- Side panels ---------- */

.side-panel {
    position: absolute;
    top: 16px;
    z-index: 100;
    width: 240px;
    padding: 18px 18px 20px;
    background: var(--panel-bg);
    border: 1px solid var(--panel-border);
    border-radius: 14px;
    backdrop-filter: blur(10px);
    box-shadow: 0 12px 30px rgba(0, 0, 0, 0.35);
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.side-panel--admin {
    right: 16px;
    border-top: 2px solid var(--admin-accent);
    max-height: calc(100vh - 32px);
    overflow-y: auto;

    /* Firefox: cienki suwak w kolorze motywu zamiast domyślnego */
    scrollbar-width: thin;
    scrollbar-color: var(--admin-accent-strong) transparent;
}

/* Chrome / Edge / Safari: stylizacja suwaka dla panelu administratora */
.side-panel--admin::-webkit-scrollbar {
    width: 8px;
}
.side-panel--admin::-webkit-scrollbar-track {
    background: transparent;
}
.side-panel--admin::-webkit-scrollbar-thumb {
    background-color: var(--admin-accent-strong);
    border-radius: 8px;
    border: 2px solid transparent;
    background-clip: padding-box;
}
.side-panel--admin::-webkit-scrollbar-thumb:hover {
    background-color: var(--admin-accent);
}

/* ---------- Player build dock (narrow category rail + expandable panel) ---------- */

.build-dock {
    position: absolute;
    top: 50%;
    left: 16px;
    transform: translateY(-50%);
    z-index: 100;
}

.build-rail {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 10px 8px;
    width: 60px;
    background: linear-gradient(180deg, rgba(30, 34, 42, 0.94), rgba(18, 21, 26, 0.94));
    border: 1px solid rgba(255, 255, 255, 0.09);
    border-radius: 16px;
    backdrop-filter: blur(12px);
    box-shadow:
        0 16px 36px rgba(0, 0, 0, 0.45),
        inset 0 1px 0 rgba(255, 255, 255, 0.06),
        inset 0 0 0 1px rgba(0, 0, 0, 0.25);
}
/* Thin machined-metal edge down the left side of the dock, like a physical
   panel rail rather than a flat rounded card - part of what keeps this from
   reading as a generic glassmorphism block. */
.build-rail::before {
    content: '';
    position: absolute;
    left: 3px;
    top: 10px;
    bottom: 10px;
    width: 2px;
    border-radius: 2px;
    background: linear-gradient(180deg,
        var(--cat-buildings), var(--cat-plots), var(--cat-taxes),
        var(--cat-bank), var(--cat-storage), var(--cat-resource),
        var(--cat-stock), var(--cat-global));
    opacity: 0.55;
}

.rail-cat {
    --rail-accent: var(--text-muted);
    position: relative;
    width: 42px;
    height: 42px;
    padding: 0;
    margin-left: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 11px;
    color: var(--text-muted);
    cursor: pointer;
    overflow: visible;
    transition: background 0.16s ease, border-color 0.16s ease, color 0.16s ease,
                transform 0.12s ease, box-shadow 0.16s ease;
}
.rail-cat svg { width: 20px; height: 20px; position: relative; z-index: 1; }

/* Small color-coded tab that sits flush against the left rail edge - the
   "which category is this" cue reads even with the icon alone. */
.rail-cat::before {
    content: '';
    position: absolute;
    left: -4px;
    top: 50%;
    transform: translateY(-50%);
    width: 3px;
    height: 14px;
    border-radius: 3px;
    background: var(--rail-accent);
    opacity: 0.5;
    transition: height 0.16s ease, opacity 0.16s ease;
}

.rail-cat:hover {
    background: color-mix(in srgb, var(--rail-accent) 14%, rgba(255,255,255,0.03));
    border-color: color-mix(in srgb, var(--rail-accent) 45%, rgba(255,255,255,0.08));
    color: var(--rail-accent);
    transform: translateX(1px);
}
.rail-cat:hover::before { height: 22px; opacity: 0.9; }
.rail-cat:active { transform: scale(0.94); }

.rail-cat.is-active {
    background: color-mix(in srgb, var(--rail-accent) 20%, rgba(255,255,255,0.02));
    border-color: color-mix(in srgb, var(--rail-accent) 65%, transparent);
    color: var(--rail-accent);
    box-shadow: 0 0 0 1px color-mix(in srgb, var(--rail-accent) 35%, transparent),
                0 6px 16px color-mix(in srgb, var(--rail-accent) 30%, transparent);
}
.rail-cat.is-active::before { height: 26px; opacity: 1; }

.rail-cat[data-category="buildings"]           { --rail-accent: var(--cat-buildings); }
.rail-cat[data-category="plots"]               { --rail-accent: var(--cat-plots); }
.rail-cat[data-category="taxes"]               { --rail-accent: var(--cat-taxes); }
.rail-cat[data-category="bank"]                { --rail-accent: var(--cat-bank); }
.rail-cat[data-category="storage"]             { --rail-accent: var(--cat-storage); }
.rail-cat[data-category="resource-production"] { --rail-accent: var(--cat-resource); }
.rail-cat[data-category="stock-market"]        { --rail-accent: var(--cat-stock); }
.rail-cat[data-category="global-stats"]        { --rail-accent: var(--cat-global); }

/* Custom flyout label - slides out to the right of the rail on hover. Reads
   as a HUD tooltip rather than a stock browser title="" bubble. */
.rail-cat[data-label]::after {
    content: attr(data-label);
    position: absolute;
    left: calc(100% + 10px);
    top: 50%;
    transform: translateY(-50%) translateX(-6px);
    white-space: nowrap;
    padding: 6px 12px;
    font-family: var(--font-display);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--text-primary);
    background: rgba(16, 18, 23, 0.95);
    border: 1px solid color-mix(in srgb, var(--rail-accent) 45%, rgba(255,255,255,0.1));
    border-left: 2px solid var(--rail-accent);
    border-radius: 7px;
    box-shadow: 0 8px 18px rgba(0, 0, 0, 0.4);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.14s ease, transform 0.14s ease;
    z-index: 200;
}
.rail-cat[data-label]:hover::after {
    opacity: 1;
    transform: translateY(-50%) translateX(0);
}

/* Flyouts (and the stock market modal) pick up the same accent color as the
   rail button that opens them, by locally overriding --player-accent(-soft/
   -strong) within each panel - every rule elsewhere that reads those three
   variables (border-top, badge, active tab, scrollbar, stat values, etc.)
   then themes itself automatically without needing its own per-category
   rule. Scoped to each panel's own element so nothing outside it (the
   global confirm modal, rotate-hint, side-panel eyebrow, ...) is affected. */
#build-flyout {
    --player-accent: var(--cat-buildings);
    --player-accent-soft: color-mix(in srgb, var(--cat-buildings) 14%, transparent);
    --player-accent-strong: color-mix(in srgb, var(--cat-buildings) 35%, transparent);
}
#plot-flyout {
    --player-accent: var(--cat-plots);
    --player-accent-soft: color-mix(in srgb, var(--cat-plots) 14%, transparent);
    --player-accent-strong: color-mix(in srgb, var(--cat-plots) 35%, transparent);
}
#tax-flyout {
    --player-accent: var(--cat-taxes);
    --player-accent-soft: color-mix(in srgb, var(--cat-taxes) 14%, transparent);
    --player-accent-strong: color-mix(in srgb, var(--cat-taxes) 35%, transparent);
}
#bank-flyout {
    --player-accent: var(--cat-bank);
    --player-accent-soft: color-mix(in srgb, var(--cat-bank) 14%, transparent);
    --player-accent-strong: color-mix(in srgb, var(--cat-bank) 35%, transparent);
}
#storage-flyout {
    --player-accent: var(--cat-storage);
    --player-accent-soft: color-mix(in srgb, var(--cat-storage) 14%, transparent);
    --player-accent-strong: color-mix(in srgb, var(--cat-storage) 35%, transparent);
}
#resource-production-flyout {
    --player-accent: var(--cat-resource);
    --player-accent-soft: color-mix(in srgb, var(--cat-resource) 14%, transparent);
    --player-accent-strong: color-mix(in srgb, var(--cat-resource) 35%, transparent);
}
#global-stats-flyout {
    --player-accent: var(--cat-global);
    --player-accent-soft: color-mix(in srgb, var(--cat-global) 14%, transparent);
    --player-accent-strong: color-mix(in srgb, var(--cat-global) 35%, transparent);
}
/* Restock panel picks up the same badge treatment as the rail flyouts above,
   themed with the tax/gold accent it already uses for its border-top/title
   (--tax === --cat-taxes), so the badge reads as belonging to the same
   category-badge language as #build-flyout etc. - see .flyout-badge and
   .has-badge below. */
.restock-panel {
    --player-accent: var(--tax);
    --player-accent-soft: color-mix(in srgb, var(--tax) 14%, transparent);
    --player-accent-strong: color-mix(in srgb, var(--tax) 35%, transparent);
}
.stock-modal-card {
    --player-accent: var(--cat-stock);
    --player-accent-soft: color-mix(in srgb, var(--cat-stock) 14%, transparent);
    --player-accent-strong: color-mix(in srgb, var(--cat-stock) 35%, transparent);
}

.build-flyout {
    position: fixed;
    z-index: 100;
    width: max-content;
    min-width: 240px;
    max-width: 420px;
    padding: 18px 18px 20px;
    background: var(--panel-bg);
    border: 2px solid var(--player-accent);
    border-radius: 14px;
    backdrop-filter: blur(10px);
    box-shadow: 0 12px 30px rgba(0, 0, 0, 0.35);
    display: none;
    flex-direction: column;
    gap: 12px;
    transform: translateY(-50%);
}
.build-flyout.is-open { display: flex; }

/* Category badge: a circular icon badge centered on the card's top edge.
   It straddles the border-top accent line half above/half below, so that
   line reads as if it grows outward from the badge's ring on either side
   rather than running straight across. Shared by the anchored flyouts AND
   the stock market modal card (.has-badge on whichever wrapper needs it). */
.build-flyout.has-badge,
.stock-modal-card.has-badge,
.inbox-dropdown.has-badge,
.transfer-dropdown.has-badge,
.balance-dropdown.has-badge,
.user-menu-dropdown.has-badge,
.restock-panel.has-badge {
    padding-top: 34px;
}
.flyout-badge {
    position: absolute;
    top: -22px;
    left: 50%;
    transform: translateX(-50%);
    width: 44px;
    height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--panel-bg);
    backdrop-filter: blur(10px);
    border: 2px solid var(--player-accent);
    border-radius: 50%;
    color: var(--player-accent);
    box-shadow: 0 8px 18px rgba(0, 0, 0, 0.35);
    z-index: 1;
}
.flyout-badge svg {
    width: 22px;
    height: 22px;
}

#global-stats-flyout {
    width: 300px;
}

/* ---------- Stock market modal ----------
   Deliberately NOT a .build-flyout: this needs to be a large, centered
   "tablet" rather than a small panel anchored to a rail button, since it
   holds a full tabbed layout (Market / Portfolio / My company / IPO). */
.stock-modal {
    position: fixed;
    inset: 0;
    z-index: 500;
    display: flex;
    align-items: center;
    justify-content: center;
}
.stock-modal[hidden] { display: none; }
.stock-modal-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(6, 8, 12, 0.6);
    backdrop-filter: blur(2px);
}
.stock-modal-card {
    position: relative;
    width: min(880px, 90vw);
    height: min(720px, 85vh);
    display: flex;
    flex-direction: column;
    gap: 14px;
    padding: 22px 26px 26px;
    background: var(--panel-bg);
    border: 2px solid var(--player-accent);
    border-radius: 16px;
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
    overflow: visible;
}
.stock-modal-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
}
/* min-width: 0 lets a long "Globalnomics Exchange"-style title actually
   shrink/ellipsize inside the flex row instead of forcing the row wider
   than the card and pushing the balance pill/close button out over it -
   the fix for the header elements riding on top of each other. */
.stock-modal-header-title {
    min-width: 0;
}
.stock-modal-header-title .panel-title {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.stock-modal-header-actions {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-shrink: 0;
}
.stock-modal-balance {
    display: inline-flex;
    align-items: center;
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 13px;
    font-variant-numeric: tabular-nums;
    color: var(--ok);
    background: rgba(67, 193, 122, 0.12);
    padding: 5px 12px;
    border-radius: 999px;
    border: 1px solid rgba(67, 193, 122, 0.3);
    white-space: nowrap;
}
.stock-modal-close {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    flex-shrink: 0;
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid rgba(255, 255, 255, 0.08);
    color: var(--text-muted);
    cursor: pointer;
}
.stock-modal-close:hover { color: var(--text-primary); background: rgba(255, 255, 255, 0.08); }

.stock-tabs {
    display: flex;
    gap: 6px;
    border-bottom: 1px solid var(--panel-border);
    padding-bottom: 10px;
    flex-wrap: wrap;
    flex-shrink: 0;
}
.stock-tab {
    padding: 6px 14px;
    border-radius: 8px;
    background: transparent;
    border: 1px solid transparent;
    color: var(--text-muted);
    font-family: var(--font-display);
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
}
.stock-tab:hover { color: var(--text-primary); }
.stock-tab.is-active {
    color: var(--player-accent);
    background: var(--player-accent-soft);
    border-color: var(--player-accent-strong);
}
.stock-tab[hidden] { display: none; }

/* Centered "not loaded yet" indicator - see showStockLoadingSpinner()/
   hideStockLoadingSpinner() in the script below. Absolutely positioned
   against .stock-modal-card (which is already position: relative), so it
   centers on the card regardless of which tab panel happens to be
   underneath - only one is ever visible at a time anyway. pointer-events:
   none so it never blocks clicks on whatever's rendered behind/around it. */
.stock-loading-spinner {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    color: var(--player-accent);
    opacity: 0.75;
    pointer-events: none;
}
.stock-loading-spinner[hidden] { display: none; }
.stock-loading-spinner-icon {
    display: block;
    animation: stock-loading-spin 0.8s linear infinite;
    transform-box: fill-box;
    transform-origin: center;
}
@keyframes stock-loading-spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
}

.stock-tab-panel {
    display: none;
    flex-direction: column;
    gap: 12px;
    overflow-y: auto;
    flex: 1;
    /* Flex items default to min-height: auto, which refuses to shrink
       below the content's natural size - so with a long company list this
       panel would just grow past .stock-modal-card's fixed height instead
       of respecting it, and since that card has overflow: hidden, the
       overflow was silently clipped rather than reachable via scroll.
       min-height: 0 lets this item actually shrink to its allotted flex
       space so overflow-y: auto above can kick in and scroll internally. */
    min-height: 0;
    /* .panel-title/.panel-hint both carry a -6px margin-top (see their
       rules further down) to sit snug under whatever's above them. That's
       fine everywhere else, but here they can be the very FIRST child -
       the negative margin then pulls their text up past this container's
       own top edge, and since this container scrolls (overflow-y: auto),
       that edge clips the top sliver of the first line instead of just
       tightening a gap. This padding gives that -6px somewhere to land
       without biting into the scrollable box itself. */
    padding-top: 6px;

    /* Firefox: cienki suwak w kolorze akcentu modala giełdy zamiast domyślnego */
    scrollbar-width: thin;
    scrollbar-color: var(--player-accent-strong) transparent;
}
.stock-tab-panel.is-active { display: flex; }

/* Chrome / Edge / Safari: stylizacja suwaka dla zakładek giełdy (Market /
   Portfolio / My company / IPO) - ten sam wzorzec co .side-panel--admin i
   .restock-panel powyżej. */
.stock-tab-panel::-webkit-scrollbar {
    width: 8px;
}
.stock-tab-panel::-webkit-scrollbar-track {
    background: transparent;
    margin: 4px 0;
}
.stock-tab-panel::-webkit-scrollbar-thumb {
    background-color: var(--player-accent-strong);
    border-radius: 8px;
    border: 2px solid transparent;
    background-clip: padding-box;
}
.stock-tab-panel::-webkit-scrollbar-thumb:hover {
    background-color: var(--player-accent);
}

/* Unlike .stock-tab-panel (which is display:flex + gap, so any child's own
   negative margin is safely absorbed by the gap instead of biting into the
   element above it), these inner views were plain block containers -
   children stacked via bare margins with no gap buffer. That's exactly why
   .panel-hint's shared -6px margin-top (its base rule further down) ate
   into the element above it instead of just tightening the gap, e.g.
   #stock-trade-preview overlapping the qty input/Buy/Sell row above it - and,
   in #stock-my-company-body specifically, the same collision misplaced the
   .stat-row separator lines (their border-top landing hard against the
   section label above instead of sitting evenly between rows). Giving all
   three the same flex-column+gap treatment as the rest of the modal fixes
   that at the source, the same way it already works everywhere else,
   rather than patching one -6px collision at a time. */
#stock-market-list-view,
#stock-company-detail-view,
#stock-my-company-body {
    display: flex;
    flex-direction: column;
    gap: 12px;
}
/* The ID rule above beats the browser's default [hidden]{display:none} on
   specificity alone, so without this override, setting .hidden = true on
   either view from JS (openStockCompanyDetail()/showStockCompanyList())
   had no visual effect - both views stayed visible on top of each other. */
#stock-market-list-view[hidden],
#stock-company-detail-view[hidden] {
    display: none;
}

.stock-company-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 10px 12px;
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid rgba(255, 255, 255, 0.06);
    cursor: pointer;
}
/* #stock-company-list is a plain block container - without this, its
   .stock-company-row children (and the portfolio list's rows, which reuse
   the same markup/id pattern) just stack flush against each other with no
   breathing room between rows. */
#stock-company-list,
#stock-portfolio-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.stock-company-row:hover { background: rgba(255, 255, 255, 0.06); }
/* min-width: 0 + ellipsis on the name/owner side keeps a long company name
   or username from overflowing into the price on the right - without it,
   a flex child's default min-width:auto refuses to shrink below its
   content's natural width, so the row just overflows/overlaps instead. */
.stock-company-row-text {
    min-width: 0;
}
.stock-company-row-name {
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.stock-company-row-owner {
    font-size: 12px;
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.stock-company-row-price {
    font-family: var(--font-display);
    font-weight: 600;
    flex-shrink: 0;
    white-space: nowrap;
}
.stock-company-row-price-block {
    flex-shrink: 0;
    text-align: right;
}
.stock-company-row-change {
    font-family: var(--font-display);
    font-size: 11px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    margin-top: 2px;
}
.stock-company-row-change.is-up { color: var(--ok); }
.stock-company-row-change.is-down { color: var(--bad); }
.stock-company-row-change.is-flat { color: var(--text-muted); }

.stock-portfolio-summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 8px 12px;
    margin-bottom: 8px;
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid rgba(255, 255, 255, 0.06);
}
.stock-portfolio-summary-label {
    font-size: 12px;
    color: var(--text-muted);
}
.stock-portfolio-summary .stock-company-row-change {
    font-size: 13px;
}

.stock-company-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
}
.stock-company-header > div:first-child {
    min-width: 0;
}
.stock-company-header .panel-title {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.stock-company-header .stat-block {
    flex-shrink: 0;
}

.stock-trade-form {
    display: flex;
    flex-direction: row;
    align-items: flex-start;
    justify-content: center;
    gap: 16px;
}
.stock-trade-col {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    flex: 0 1 auto;
    min-width: 0;
}
.stock-trade-row {
    display: flex;
    gap: 8px;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
}
.stock-trade-row .loan-input { flex: 1; min-width: 120px; }
/* Quantity fields in the buy/sell rows: fixed, compact width (well under
   half of the old 120px min-width) instead of growing to fill the row. */
.stock-trade-row .stock-qty-input {
    flex: 0 0 144px;
    min-width: 144px;
    max-width: 144px;
    width: 144px;
    padding-left: 8px;
    padding-right: 4px;
    text-align: center;
}
.btn--max {
    padding: 8px 10px;
    font-size: 0.78rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    flex-shrink: 0;
}

/* Now that #stock-company-detail-view is a flex-column+gap container (see
   above), .panel-hint's shared -6px margin-top is safely absorbed by the
   12px gap (net ~6px) instead of overlapping the row above it - no ID-
   specific override needed here anymore. */
#stock-trade-preview {
    font-family: var(--font-display);
    font-weight: 600;
    color: var(--text-primary);
}

.stock-delist-btn {
    color: var(--danger);
    border-color: rgba(224, 85, 79, 0.35);
    align-self: flex-start;
}
.stock-delist-btn:hover { background: var(--danger-soft); }

/* ---------- "My company" tab: wraps each panel-section in a visible card
   (background + border) instead of plain unbounded text, so financials,
   shareholders, and the delist action read as distinct blocks rather than
   blending into one long stack. Mirrors .stat-block's card look, just
   sized for a section with a label + several stat-rows instead of one
   big number. ---------- */
.stock-card {
    padding: 12px 14px 14px;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 10px;
}
/* .stat-row's border-top (meant to separate it from the row above it)
   would otherwise also draw along the card's own top edge for the first
   row in each card, right under the label - redundant with the card's own
   border. Only rows after the first need that separator. */
.stock-card .stat-row:first-of-type { border-top: none; }
/* A .panel-hint used as a closing note inside a card (the redemption
   warning, the delist explainer) - drop its shared -6px margin-top since
   the card's own padding-top already provides that breathing room, and
   negative margin here would just pull it up against the label/rows above. */
.stock-card-footnote { margin-top: 0; }

.stock-card--danger {
    border-color: rgba(224, 85, 79, 0.3);
    background: rgba(224, 85, 79, 0.05);
}
.stock-card--danger .panel-section-label { color: var(--danger); }

/* ---------- Generic confirm dialog ----------
   Same fixed+centered+backdrop shape as .stock-modal, but z-index'd above
   it (600 > 500) so it can be triggered from inside another modal (e.g.
   the Delist button in the Stock market modal's My Company tab) without
   being trapped behind it. Deliberately much smaller/fixed-width rather
   than .stock-modal-card's min(880px, 90vw) - this only ever holds a
   short title + message + two buttons, never a scrolling body. */
.confirm-modal {
    position: fixed;
    inset: 0;
    z-index: 600;
    display: flex;
    align-items: center;
    justify-content: center;
}
.confirm-modal[hidden] { display: none; }
.confirm-modal-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(6, 8, 12, 0.6);
    backdrop-filter: blur(2px);
}
.confirm-modal-card {
    position: relative;
    width: min(380px, 90vw);
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 22px 24px 24px;
    background: var(--panel-bg);
    border: 1px solid var(--panel-border);
    border-top: 2px solid var(--player-accent);
    border-radius: 16px;
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
}
.confirm-modal-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    margin-top: 4px;
}
.confirm-modal-actions .btn { flex: 0 0 auto; }

.btn-label {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px 8px;
}

/* ---------- Rotation key hint bubble (shown under the stall while placing it) ---------- */

.rotate-hint {
    position: fixed;
    z-index: 400;
    transform: translateX(-50%);
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 5px 10px;
    background: var(--panel-bg);
    border: 1px solid var(--player-accent-strong);
    border-radius: 8px;
    backdrop-filter: blur(10px);
    box-shadow: 0 8px 18px rgba(0, 0, 0, 0.35);
    font-size: 12px;
    font-weight: 500;
    color: var(--text-primary);
    pointer-events: none;
    white-space: nowrap;
}
.rotate-hint[hidden] { display: none; }
.rotate-hint-key {
    font-family: var(--font-display);
    font-size: 10.5px;
    font-weight: 600;
    color: var(--player-accent);
    border: 1px solid var(--player-accent-strong);
    border-radius: 5px;
    padding: 1px 6px;
}

.panel-eyebrow {
    font-family: "Silkscreen", sans-serif;
    font-size: 15px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--text-muted);
    text-align: center;
}

.side-panel--admin .panel-eyebrow { color: var(--admin-accent); }
.side-panel--player .panel-eyebrow { color: var(--player-accent); }

.panel-title {
    font-family: var(--font-display);
    font-size: 20px;
    font-weight: 600;
    margin-top: -6px;
}

.panel-hint {
    font-size: 12px;
    line-height: 1.5;
    color: var(--text-muted);
    margin-top: -6px;
}

/* The build/tax/bank flyouts size themselves to their widest content
   (width: max-content) - the hint text now stretches to fill that width
   instead of being pinned to a narrower fixed column, so it uses the full
   panel width rather than wrapping early and leaving empty space beside it.
   Other places .panel-hint is used (the side panels, the transfer dropdown)
   size correctly on their own and must NOT get this rule. */
.build-flyout .panel-hint {
    width: 100%;
}

.panel-section {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.panel-section-label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-muted);
    margin-bottom: -2px;
}

/* ---------- Admin panel categories (accordion) ----------
   Each admin tool group (Zones / Routes / Plots / Map / Messages) lives in
   its own <details>, so only the category someone is actually using takes
   up space - the rest collapse down to a single header row. Native
   <details>/<summary> is used instead of a JS-driven tab system: no extra
   event wiring, keyboard/click toggling works for free, and it can't get
   out of sync with anything. */
.admin-cat {
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.02);
    overflow: hidden;

    /* .side-panel--admin is a flex column container. Without this,
       overflow:hidden here gives the browser an automatic min-height of 0
       for this flex item, so instead of the panel scrolling, each category
       gets silently squashed to fit inside the panel's max-height - clipping
       content (e.g. the "Create business" button) with no way to reach it.
       flex-shrink:0 keeps every category at its natural content height, so
       the panel actually overflows and the scrollbar/wheel-scroll added
       above has something real to scroll. */
    flex-shrink: 0;
}

.admin-cat + .admin-cat {
    margin-top: 2px;
}

.admin-cat-summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 10px 12px;
    cursor: pointer;
    list-style: none;
    font-family: var(--font-display);
    font-size: 13px;
    font-weight: 600;
    color: var(--text-primary);
    user-select: none;
    transition: background 0.15s ease;
}
.admin-cat-summary::-webkit-details-marker { display: none; }
.admin-cat-summary:hover { background: rgba(255, 255, 255, 0.04); }

.admin-cat-chevron {
    flex: none;
    color: var(--text-muted);
    transition: transform 0.15s ease;
}
.admin-cat[open] > .admin-cat-summary .admin-cat-chevron {
    transform: rotate(180deg);
}
.admin-cat[open] > .admin-cat-summary {
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}

.admin-cat-body {
    display: flex;
    flex-direction: column;
    gap: 12px;
    padding: 14px 12px 16px;
}

/* ---------- Bank panel (net worth) ---------- */

.stat-block {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 14px 14px 16px;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 10px;
}

.stat-label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-muted);
}

.stat-value {
    font-family: var(--font-display);
    font-size: 26px;
    font-weight: 700;
    color: var(--player-accent);
    white-space: nowrap;
}

.stat-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 8px 2px;
    border-top: 1px solid rgba(255, 255, 255, 0.06);
    font-size: 13px;
}

.stat-row-label { color: var(--text-muted); }
.stat-row-value { font-family: var(--font-display); font-weight: 600; }
.stat-row-value--negative { color: var(--danger); }

/* ---------- Global stats panel: hourly World GDP chart ---------- */
.gdp-chart {
    width: 100%;
    height: 64px;
    margin-top: 8px;
    display: block;
}

.gdp-chart-axis {
    display: flex;
    justify-content: space-between;
    margin-top: 2px;
    font-size: 10px;
    color: var(--text-muted);
}

/* ---------- Taxes panel debt meter ---------- */
/* Same 30%/90% breakpoints as DEBT_RATIO_BUILD_LOCK/DEBT_RATIO_LIQUIDATION
   in config.php - green under 30%, amber 30-90% (build/rent lock band),
   red at 90%+ (forced-liquidation band). See debt_ratio_class() (PHP,
   initial render) and getDebtRatioClass() (JS, live poll updates). */
.debt-ratio-section {
    gap: 6px;
}

.debt-ratio-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
}

.debt-ratio-value {
    font-family: var(--font-display);
    font-size: 13px;
    font-weight: 700;
    color: var(--text-primary);
}

.debt-ratio-track {
    width: 100%;
    height: 8px;
    border-radius: 5px;
    background: rgba(255, 255, 255, 0.08);
    overflow: hidden;
}

.debt-ratio-fill {
    height: 100%;
    border-radius: 5px;
    transition: width 0.25s ease, background 0.25s ease;
}

.debt-ratio-fill--ok { background: var(--ok); }
.debt-ratio-fill--warning { background: var(--tax); }
.debt-ratio-fill--danger { background: var(--danger); }

.debt-ratio-hint {
    margin-top: 0;
}

.loan-form {
    display: flex;
    gap: 8px;
}
.loan-form[hidden] { display: none; }

.loan-input {
    flex: 1;
    min-width: 0;
    width: 100%;
}

#paint-tools { display: none; }
body.painting-mode #paint-tools { display: flex; }

#route-tools {
    display: none;
    flex-direction: column;
    gap: 12px;
}
body.routing-mode #route-tools { display: flex; }

#plot-tools {
    display: none;
    flex-direction: column;
    gap: 12px;
}
body.plotting-mode #plot-tools { display: flex; }

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

.btn {
    font-family: var(--font-body);
    font-size: 13.5px;
    font-weight: 500;
    color: var(--text-primary);
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 9px;
    padding: 10px 12px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 8px;
    transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
}

.btn:hover { background: rgba(255, 255, 255, 0.07); }
.btn:active { transform: scale(0.98); }

.btn--zone.is-active {
    border-color: var(--admin-accent-strong);
    background: var(--admin-accent-soft);
}

.btn--tool.is-active {
    border-color: var(--player-accent-strong);
    background: var(--player-accent-soft);
}

.btn--danger.is-active {
    border-color: var(--danger);
    background: var(--danger-soft);
}

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

/* Stock market trade buttons: buy = green (--ok), sell = red (--danger) */
.btn--buy {
    color: var(--ok);
    border-color: rgba(67, 193, 122, 0.35);
    background: rgba(67, 193, 122, 0.14);
}
.btn--buy:hover { background: rgba(67, 193, 122, 0.24); }
.btn--buy:active { background: rgba(67, 193, 122, 0.3); }

.btn--sell {
    color: var(--danger);
    border-color: rgba(224, 85, 79, 0.35);
    background: var(--danger-soft);
}
.btn--sell:hover { background: rgba(224, 85, 79, 0.24); }
.btn--sell:active { background: rgba(224, 85, 79, 0.3); }

.btn-pill {
    font-family: var(--font-display);
    font-size: 10.5px;
    font-weight: 600;
    background: rgba(255, 255, 255, 0.08);
    padding: 2px 6px;
    border-radius: 999px;
    color: var(--text-muted);
}

.btn--tool.is-active .btn-pill {
    background: var(--player-accent);
    color: #1a1207;
}

.btn-key {
    font-family: var(--font-display);
    font-size: 10.5px;
    color: var(--text-muted);
    border: 1px solid rgba(255, 255, 255, 0.15);
    border-radius: 5px;
    padding: 1px 6px;
}

.btn-swatch {
    width: 12px;
    height: 12px;
    border-radius: 3px;
    display: inline-block;
    margin-right: 6px;
    flex-shrink: 0;
}
.btn-swatch--grass { background: #2ecc71; }
.btn-swatch--water { background: #2e86de; }
.btn-swatch--default {
    background: #333333;
    border: 1px solid rgba(255, 255, 255, 0.15);
}
.btn-swatch--white {
    background: #ffffff;
    border: 1px solid rgba(255, 255, 255, 0.25);
}
.btn-swatch--street {
    background: #55595f;
}
.btn-swatch--plots {
    background: #8e44ad;
}
.btn-swatch--resource-sales {
    background: #16a085;
}
.btn-swatch--loan {
    background: var(--danger);
}
.btn--zone { justify-content: flex-start; }

/* Toggle switch button */
.btn--toggle {
    justify-content: flex-start;
}
.toggle-track {
    width: 34px;
    height: 18px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.12);
    position: relative;
    flex-shrink: 0;
    transition: background 0.15s ease;
}
.toggle-thumb {
    position: absolute;
    top: 2px;
    left: 2px;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: var(--text-muted);
    transition: transform 0.15s ease, background 0.15s ease;
}
.btn--toggle.is-active .toggle-track { background: var(--admin-accent-soft); }
.btn--toggle.is-active .toggle-thumb {
    background: var(--admin-accent);
    transform: translateX(16px);
}
.toggle-label { font-size: 13.5px; }

/* ---------- Viewport / grid ---------- */

#viewport {
    width: 100vw;
    height: 100vh;
    cursor: grab;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative; /* anchors #buildings-3d-viewport (absolute, inset:0) below */
}
#viewport.is-dragging { cursor: grabbing; }
body.tool-stall_item #viewport,
body.tool-remove_stall #viewport,
body.tool-foodtruck_item #viewport,
body.tool-barbershop_item #viewport { cursor: crosshair; }

/* CHECKPOINT 7.8 CUTOVER: the real DOM tile grid is retired in favor of
   #map-canvas-static (see drawStaticTileBackground() in index.php), which
   now paints every tile's background/zone-tint/plot-edge/flat-sprite
   unconditionally, not just behind the "C" debug toggle - same treatment
   #buildings-3d-viewport got in Checkpoint 7.7. display: none (not just
   opacity/visibility) so it's fully out of layout and paints nothing.

   CHECKPOINT 7.9: PHP no longer emits any .tile elements at all - #map-grid
   is now permanently empty (see index.php). Tile data lives in plain JS
   objects (tileByCoord/tileById), kept current by applyTileData(), instead
   of DOM dataset attributes. This rule (and the .tile rule below) stays
   only so mapGrid.style.transform - still written every frame by
   updateTransform() for parity with the buildings layer - has a live,
   correctly-transformed (if childless) element to write to; nothing here
   paints or receives pointer events. Clicks/hovers over the map fall
   through to #viewport itself (pointer-events: auto by default, nothing
   above it claims the hit - every canvas layer is pointer-events: none)
   and resolve to a tile via tileFromEvent()/screenToTile() (Checkpoint 2). */
#map-grid {
    display: none;
    grid-template-columns: repeat(30, 40px);
    grid-template-rows: repeat(30, 40px);
    gap: 2px;
    transform: rotateX(60deg) rotateZ(45deg);
    /* Promote to its own compositor layer so panning/zooming only updates
       a GPU transform instead of repainting hundreds of tiles every frame.
       (transform-style: preserve-3d was removed on purpose - nothing here
       uses translateZ, so it bought nothing but forced every one of the
       900+ tile elements into a real 3D rendering context, which is what
       was causing the panning lag.) Moot now that the layer is display:
       none and never painted, kept only so this rule is a minimal diff to
       revert if 7.8 needs rolling back. */
    will-change: transform;
    backface-visibility: hidden;
}

.tile {
    width: 40px;
    height: 40px;
    background-color: var(--grid-bg);
    border: 1px solid rgba(255, 255, 255, 0.06);
    position: relative;
    transition: outline-color 0.1s ease;
    /* On a full map this is ~900 of these. Without containment, the browser
       has to assume any layout/style change inside ONE tile (a restock
       countdown's text ticking every second, a zone repaint while drawing,
       a walker's position updating) *might* affect its neighbors too, and
       re-checks the whole grid to be safe. `layout` + `style` tell it a
       tile's own internal layout/counters can't leak out and affect
       anything else (true here), so a change to one tile only ever costs
       that one tile, not a pass over all 900 - matters for any per-tile
       update, not just panning/zooming. (Deliberately NOT `contain: paint`
       here - that would additionally clip .stall-obj's box-shadow right at
       the tile edge, a small but real visible change to how stalls look.) */
    contain: layout style;
}

/* ---------- 3D buildings overlay ----------
   A sibling of #map-grid, not a descendant - see render_business_model_mount()
   in config.php / updateTransform() in index.php for why. #map-grid has no
   transform-style: preserve-3d on purpose (perf - see the note on #map-grid
   above), so any real translateZ()'d geometry nested inside it would get
   flattened flat-on before the isometric rotation ever reached it. This
   layer used to get its OWN copy of that same rotateX/rotateZ tilt (plus its
   own preserve-3d) for exactly that reason - now that every placed
   business's model is pre-flattened into plain .cube-face-2d divs (see the
   "2D-flattened building faces" section further down), the tilt is baked
   into each face's own position instead (projectIso() in index.php), so
   this layer only ever needs the pan/zoom part - translate/scale, same
   numbers as #map-grid, just without the isometric part - see
   updateTransform() in index.php. */
/* CHECKPOINT 7.7 CUTOVER: this whole layer - the old DOM-based 3D model
   system (.building-anchor-cell / .building-mount / .cube-face-2d, built by
   hydrateBuildingMounts()/syncBuildingMount() in index.php) - is retired in
   favor of #map-canvas-buildings (see drawBuildingModelsOnCanvas()/
   redrawBuildingsCanvas() in index.php), which now paints every placed
   business unconditionally, not just behind the "C" debug toggle. display:
   none (not just opacity/visibility) so it's fully out of layout and paints
   nothing, same treatment #map-grid itself will get in Checkpoint 7.8.
   pointer-events was already none (see below) so this was never
   interactive to begin with - nothing here was depended on by any click/
   hover handler.

   Deliberately NOT deleted yet, along with every rule below that only ever
   targeted this layer (.is-dragging fade, tool-dimming) and the JS that
   builds/maintains it (hydrateBuildingMounts()/mountAt()/syncBuildingMount()
   and friends, still called from index.php) - per the migration plan, that
   ~700-line system stays as inert dead code until a couple of regression-free
   weeks have passed post-cutover (Checkpoint 7.9), specifically so a
   regression here can be fixed by reverting this one display:none instead of
   reconstructing anything. */
#buildings-3d-viewport {
    display: none; /* CHECKPOINT 7.7 cutover - see the comment above */
    position: absolute;
    inset: 0;
    align-items: center;
    justify-content: center;
    pointer-events: none; /* clicks fall through to #map-grid beneath */
    /* Fades back in once dragging stops - see the .is-dragging rule below
       for why it's hidden in the first place. transition only applies here
       (the "resting" state), not on the way out - see that rule for why. */
    transition: opacity 0.15s ease;
}

/* #buildings-3d-layer used to be a REAL preserve-3d rendering context (see
   #map-grid's own note above for why it, unlike this layer, was always kept
   flat): every placed business with a saved model contributed 5-10 real
   .cube-face divs per block, composited as genuine 3D geometry, which is
   what made panning choppier the more buildings were on the map. It's now a
   plain flat layer instead - see the big "2D-flattened building faces" note
   further down - so those divs (now .cube-face-2d) are ordinary
   absolutely-positioned elements with a precomputed 2D matrix() each, no
   different in kind from any other absolutely-positioned div, but a full
   map can still have thousands of them, and updateTransform() in index.php
   still rewrites this layer's own translate/scale on every animation frame
   while panning. Hiding the whole layer for the ~instant duration of an
   active drag (instant, not faded - no `transition` on THIS rule - so the
   hide itself costs nothing extra mid-drag) and letting it fade back in via
   the base rule above the moment mouseup fires is a cheap, conservative way
   to keep that frame-by-frame cost off the hot path regardless. Buildings
   never move relative to the grid, so re-appearing a frame or two after the
   drag settles is imperceptible in practice. */
#viewport.is-dragging #buildings-3d-viewport {
    opacity: 0;
}

/* Same idea as the drag-hide above, but for placing/demolishing: with
   buildings rendered as flattened 2D blocks (see the comment on
   #buildings-3d-layer below), a tall existing model right next to an empty
   spot can visually cover the tiles you're trying to aim at - the zone
   coloring and hover preview (.tile--preview-ok/.tile--preview-bad) live on
   #map-grid, underneath this layer. Dimming buildings (not fully hiding them
   - see the outline below for why) for the whole duration of the stall/food
   truck/barbershop placement tool or the demolish tool keeps the grid clear
   enough to aim at, the same way it already goes fully invisible mid-drag,
   while still leaving existing footprints visible as a faint reference so
   you can see what's already built without a tall model blocking the tile
   right behind it. Transitions via the base #buildings-3d-viewport rule
   above, so it fades rather than popping. */
body.tool-stall_item #buildings-3d-viewport,
body.tool-foodtruck_item #buildings-3d-viewport,
body.tool-barbershop_item #buildings-3d-viewport,
body.tool-remove_stall #buildings-3d-viewport {
    opacity: 0.25;
}

/* ---------- CHECKPOINT 0/3: canvas migration ----------
   A third overlay, same pattern as #buildings-3d-viewport above: absolute,
   inset:0, flex-centered, so its content centers exactly where #map-grid's
   own box sits. Both canvases are sized in JS to #map-grid's own local box
   (GRID_W*42-2 x GRID_H*42-2) and given the SAME isoTransform
   updateTransform() already applies to #map-grid, so they land
   pixel-identical to the live DOM map without any separate per-tile
   projection math - the whole grid is flat (no translateZ anywhere), so
   rotating the finished 2D bitmap in CSS is exact, the same reasoning
   #map-grid's own comment above gives for doing this live instead of
   pre-projecting every tile.

   The container itself is no longer hidden (checkpoint 3): the DYNAMIC
   canvas carries real, always-on gameplay content (walkers - see
   renderWalkers()/drawWalkers() in index.php). CHECKPOINT 7.8: the STATIC
   canvas is no longer gated behind the "C" debug toggle either - it's now
   the real, permanently-visible tile layer (#map-grid is retired, see its
   own comment above). "C" still exists but only toggles the leftover
   debug affordances noted at its call sites (canvas-debug-on is mostly
   vestigial past this point). z-index puts the whole layer above
   #buildings-3d-viewport, matching where the old .walker divs (z-index:5)
   used to sit relative to buildings. */
#map-canvas-viewport {
    position: absolute;
    inset: 0;
    z-index: 5;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
}
/* CHECKPOINT 7.6: #map-canvas-buildings split off from #map-canvas-static
   as its own layer (see drawStaticCanvas()/drawBuildingModelsOnCanvas() in
   index.php). CHECKPOINT 7.7: #map-canvas-buildings cut over for real
   (permanently visible, replacing #buildings-3d-viewport above).
   CHECKPOINT 7.8: #map-canvas-static cut over too now, replacing #map-grid
   itself - both canvases are permanently visible below, no more "C"-gated
   debug state for either.

   BUGFIX (walkers-vs-buildings occlusion): DOM order inside
   #map-canvas-sizer is now static, WALKERS, buildings, dynamic (see
   index.php) - not the original 3-canvas static/buildings/dynamic layout
   from Checkpoint 7.6. That original order put #map-canvas-dynamic
   (walkers + drawToolOverlay()'s build/demolish previews, existing-
   building hover highlight, route/plot draft points, debug wireframe - see
   drawWalkers() in index.php) entirely above #map-canvas-buildings, so
   every walker showed in front of every building regardless of position -
   visibly wrong, since buildings should occlude walkers passing behind/
   under them.

   Simply swapping buildings and dynamic would have fixed walkers but broken
   the tool-overlay half of that same canvas: the demolish-hover highlight
   and debug wireframe on an existing building would then render underneath
   that building's own sprite. So walkers got their own canvas instead,
   #map-canvas-walkers, inserted between static and buildings (so buildings
   occlude walkers, as intended) - #map-canvas-dynamic keeps every other
   piece of tool-overlay content and stays last/topmost, same position over
   buildings it always had. Static (tile backgrounds) stays first/
   bottommost either way. */
#map-canvas-static, #map-canvas-walkers, #map-canvas-buildings, #map-canvas-dynamic {
    position: absolute;
    top: 0;
    left: 0;
    backface-visibility: hidden;
}
/* CHECKPOINT 7.8: always on now, same cutover #map-canvas-buildings got in
   7.7 - drawStaticTileBackground() (via refreshMapTiles() in index.php)
   keeps this current on every real map-data change unconditionally, not
   just while canvas-debug-on. #map-grid is permanently display:none above,
   so this is now the ONLY thing rendering tile backgrounds/zone tints/
   plot edges/flat sprites. No transition here (unlike before): it's never
   toggled at runtime anymore, so there's nothing to fade between. */
#map-canvas-static {
    opacity: 1;
}
/* Walkers-vs-buildings occlusion bugfix: always on, same as every other
   canvas layer below it - drawWalkers() (via walkerRafLoop() in index.php)
   keeps this current every animation frame unconditionally. Sits directly
   above #map-canvas-static and below #map-canvas-buildings in DOM order so
   buildings paint over walkers, not the other way around. */
#map-canvas-walkers {
    pointer-events: none;
}
/* CHECKPOINT 7.7: always on now - drawBuildingModelsOnCanvas() (via
   redrawBuildingsCanvas() in index.php) keeps this current on every real
   map-data change unconditionally, not just while canvas-debug-on. No
   transition here (unlike the static canvas above): it's never toggled at
   runtime anymore, so there's nothing to fade between. */
#map-canvas-buildings {
    opacity: 1;
}
#map-canvas-dynamic {
    pointer-events: none;
}
/* Sized in JS (width/height) to exactly #map-grid's own local box
   (GRID_W*42-2 x GRID_H*42-2), so it's the thing #map-canvas-viewport's
   flex centering actually centers - the two canvases inside then stack via
   plain position:absolute/top:0/left:0 against THIS box, not the (much
   bigger, window-sized) #map-canvas-viewport.

   BUGFIX (route markers / hit-testing drift): #map-canvas-viewport is
   display:flex, and this sizer is a normal flex item with the browser
   default flex-shrink:1. On any real map the true pixel width (CANVAS_W =
   GRID_W*42-2, ~3948px for a full 94x94 map) exceeds the actual window
   width, so flexbox was silently shrinking the sizer's rendered width to
   fit the viewport - height (the cross axis in a row-flex container) was
   left untouched, since align-items:center doesn't force-stretch it. The
   three canvases squish together and stay internally consistent with each
   other (all canvas-drawn bitmaps), so tiles/buildings/walkers always
   agreed - but tileCenterToScreen()/screenToCanvasLocalXY() (route
   markers, mouse hit-testing) do their math against the JS constant
   CANVAS_W, which has no idea the box actually rendered narrower. That put
   DOM-based .route-marker elements (and hover hit-testing) where the
   canvas SHOULD have been, not where it actually was once squished - a
   horizontal-only drift that grows with map width relative to window
   width. flex-shrink:0 below stops the sizer from ever shrinking, so its
   rendered box always matches CANVAS_W/CANVAS_H exactly, same as the JS
   math assumes. */
#map-canvas-sizer {
    position: relative;
    flex-shrink: 0;
}

#buildings-3d-layer {
    /* USED to be display:grid with an explicit grid-template-columns/rows:
       repeat($gridW/$gridH, 40px) (94x94 on a full map = 8836 tracks),
       matching #map-grid's own track layout so each .building-anchor-cell
       could be placed via grid-column/grid-row. That placement was always
       thrown away immediately: every anchor cell already carries its real
       screen position baked into its own faces by projectIso() (see the
       comment on .building-anchor-cell below and pinAnchorCellToOrigin() /
       hydrateBuildingMounts() in index.php), so the grid-column/grid-row
       PHP gave each cell was dead weight from the moment JS ran, and the
       94x94 track grid itself was dead weight even before that - nothing
       ever actually needed it sized. Removing display:grid here (and the
       matching inline grid-template on this element / grid-column/grid-row
       per cell in index.php) drops that track computation entirely without
       changing anything on screen: cells are positioned via
       .building-anchor-cell's own position:absolute below regardless. */
    will-change: transform;
    pointer-events: none;
}

.building-anchor-cell {
    /* Pulled out of any layout flow entirely and pinned flush against
       #buildings-3d-layer's own top-left corner - every face this cell's
       .building-mount contains is already positioned in final screen space
       by projectIso() (index.php), which folds the anchor's real (x, y)
       tile coordinates in BEFORE rotating. So the cell itself never needs
       its own per-anchor offset (grid or otherwise); it just needs to get
       out of the way, for every anchor alike, and let its own content carry
       the real per-anchor position. Setting this directly here (rather than
       only via JS, as before) means it's correct from the very first paint,
       with no dependency on pinAnchorCellToOrigin() having run yet. */
    position: absolute;
    inset: 0;
    pointer-events: none;
}

.building-mount {
    position: relative;
    width: 100%;
    height: 100%;
    pointer-events: none;
}

.zone-layer {
    position: absolute;
    top: 0; left: 0;
    width: 100%; height: 100%;
    display: none;
    pointer-events: none;
}

.building-layer {
    position: absolute;
    top: 0; left: 0;
    width: 100%; height: 100%;
    pointer-events: none;
}

/* A stall occupies 2 tiles - each one draws its own full-size square, but
   only on the side(s) that face OUTSIDE the stall's 2-tile footprint
   (stall-edge-* classes, computed in get_stall_edge_classes() / mirrored in
   getStallEdgeClasses()) so the pair reads as one outlined shape instead of
   two squares with a seam down the middle. stall-flat-* classes square off
   the two corners next to the internal (partner-facing) side, so rounding
   only happens on the stall's true outer corners. The background color is
   overridden inline in PHP/JS depending on the owner; the border color
   differs by ownership too - see stall-obj--own / stall-obj--other below. */
.stall-obj {
    position: absolute;
    top: 0; left: 0;
    width: 100%;
    height: 100%;
    background: linear-gradient(135deg, #f0a04b, #d9691f);
    border-radius: 4px;
    box-shadow: 0 3px 6px rgba(0, 0, 0, 0.4);
}
.stall-edge-top { border-top: 2px solid var(--stall-edge-color, #3b1d0a); }
.stall-edge-right { border-right: 2px solid var(--stall-edge-color, #3b1d0a); }
.stall-edge-bottom { border-bottom: 2px solid var(--stall-edge-color, #3b1d0a); }
.stall-edge-left { border-left: 2px solid var(--stall-edge-color, #3b1d0a); }
.stall-flat-tl { border-top-left-radius: 0; }
.stall-flat-tr { border-top-right-radius: 0; }
.stall-flat-bl { border-bottom-left-radius: 0; }
.stall-flat-br { border-bottom-right-radius: 0; }

/* Other players' stalls: dark-brown edge with a light amber outer ring, so
   it reads clearly against any tile color underneath (grass, dirt, plot
   violet, other stalls). */
.stall-obj--other {
    --stall-edge-color: #3b1d0a;
}
.stall-obj--other.stall-edge-top,
.stall-obj--other.stall-edge-right,
.stall-obj--other.stall-edge-bottom,
.stall-obj--other.stall-edge-left,
.stall-obj--other.stall-edge-interior {
    box-shadow: 0 0 0 1px rgba(255, 214, 153, 0.85), 0 3px 6px rgba(0, 0, 0, 0.4);
}

/* Your own stalls: navy edge instead of the amber/brown one above, so your
   own stalls are instantly distinguishable from everyone else's at a
   glance. */
.stall-obj--own {
    --stall-edge-color: #1b2a4a;
}
.stall-obj--own.stall-edge-top,
.stall-obj--own.stall-edge-right,
.stall-obj--own.stall-edge-bottom,
.stall-obj--own.stall-edge-left,
.stall-obj--own.stall-edge-interior {
    box-shadow: 0 0 0 1px rgba(173, 202, 255, 0.85), 0 3px 6px rgba(0, 0, 0, 0.4);
}

/* Food trucks (2x3, 6 tiles) reuse the exact same .stall-obj structure -
   per-tile squares joined by stall-edge-*/stall-flat-* classes, computed by
   get_foodtruck_edge_classes() / getFoodtruckEdgeClasses() for a rectangular
   grid position instead of a fixed 2-tile anchor/part pair. This modifier
   only overrides the background/border-radius so food trucks read visually
   distinct from stalls; ownership border colors (own = navy, other = brown)
   still come from stall-obj--own / stall-obj--other above. */
.foodtruck-obj {
    /* Fallback only - the real color (own = bordowy/maroon, others =
       czerwony/red) is set inline per-tile from FOODTRUCK_OWN_*/FOODTRUCK_OTHER_*
       in index.php, see render_building() / renderBuildingHTML(). */
    background: linear-gradient(135deg, #e0574a, #a8291d);
    border-radius: 6px;
}

/* Barbershops (3x3, 9 tiles) reuse the same .stall-obj structure too, via
   get_barbershop_edge_classes() / getBarbershopEdgeClasses(). Only the
   background/border-radius is overridden here; ownership border colors
   (own = navy, other = brown) still come from stall-obj--own/--other above. */
.barbershop-obj {
    /* Fallback only - the real color (own = coral, others = yellow) is set
       inline per-tile from BARBERSHOP_OWN_*/BARBERSHOP_OTHER_* in index.php,
       see render_building() / renderBuildingHTML(). */
    background: linear-gradient(135deg, #f4d03f, #b7950b);
    border-radius: 6px;
}

/* Admin-defined ("generic") business types (any shape_rows x shape_cols,
   built through the 'build_business' action) reuse the same .stall-obj
   structure too, via get_generic_business_edge_classes() /
   getGenericBusinessEdgeClasses(). Only the background/border-radius is
   overridden here; ownership border colors (own = navy, other = brown)
   still come from stall-obj--own/--other above. */
.generic-biz-obj {
    /* Fallback only - the real color is derived from the business type's own
       single `color` (own = that color, others = grayed-out) via
       hex_darken()/hex_mix_gray() in config.php, mirrored by hexDarken()/
       hexMixGray() in index.php, and set inline per-tile - see
       render_building() / renderBuildingHTML(). */
    background: linear-gradient(135deg, #9aa0a6, #5f6368);
    border-radius: 6px;
}

#map-grid { position: relative; }

/* ---------- Route drawing (admin) ---------- */

/* CHECKPOINT 7.1: the point outline + numbered badge (.tile--route-point,
   .route-point-badge, including the erase-tool :hover recolor) moved to
   #map-canvas-dynamic, drawn every frame by drawToolOverlay() in
   index.php from routeDraftPoints/hoveredRoutePointCoord instead - see
   the comment there. Only the tool cursors stay here. */
body.tool-draw_route #viewport { cursor: crosshair; }
body.tool-erase_route #viewport { cursor: cell; }

/* ---------- Saved route markers (admin: click to select/edit) ---------- */

.route-marker {
    /* CHECKPOINT 7.2: fixed instead of absolute, and no longer a DOM
       child of #map-grid - it used to inherit its isometric screen
       position for free from #map-grid's own CSS transform (rotateX/
       rotateZ/translate/scale); now index.php computes real
       clientX/clientY-space coordinates itself via tileCenterToScreen()
       and writes them here directly, same left/top pattern as
       .owner-tooltip/.rotate-hint below. z-index dropped from 4 (which
       only mattered within #map-grid's own local stacking context) to 50 -
       enough to sit above the map/canvas layers (z-index up to 6) but
       below every UI panel/dropdown (z-index 100+) and the tooltip/hint
       bubbles (400), so a marker never floats over chrome it shouldn't. */
    position: fixed;
    width: 12px;
    height: 12px;
    margin-left: -6px;
    margin-top: -6px;
    border-radius: 50%;
    background: rgba(255, 212, 121, 0.35);
    border: 1.5px solid rgba(255, 212, 121, 0.75);
    z-index: 50;
    cursor: pointer;
    transition: transform 0.12s ease, background 0.12s ease, border-color 0.12s ease;
}
.route-marker:hover {
    background: rgba(255, 212, 121, 0.6);
    transform: scale(1.2);
}
.route-marker--selected {
    background: rgba(53, 208, 192, 0.55);
    border-color: var(--admin-accent);
    transform: scale(1.25);
}
/* Route-markers-visibility toggle (Routes panel "Hide markers"/"Show
   markers" button, see toggleRouteMarkersVisibility() in index.php): hides
   every marker via display:none instead of touching each one's own style
   individually - one class toggle on <body> vs N element writes. The real
   perf win isn't this rule itself (toggling it is a rare, one-off action)
   but that repositionRouteMarkers() in index.php skips its whole per-marker
   loop while routeMarkersVisible is false, which is what removes the
   per-pan-frame cost that made dragging feel slow on an admin account with
   many saved routes. */
body.route-markers-hidden .route-marker {
    display: none;
}

/* ---------- Plots ("dzialki") ---------- */

/* Local-only highlight for tiles painted but not yet saved by the admin -
   the plot doesn't actually exist on the map (no zone_type change, no color)
   until "Save plot" is clicked. */
.tile--plot-draft {
    outline: 2px dashed #8e44ad !important;
    outline-offset: -2px;
    z-index: 2;
}
body.tool-plot_draw #viewport { cursor: crosshair; }
body.tool-plot_erase #viewport { cursor: cell; }
body.tool-plot_erase .tile--plot-draft:hover {
    outline-color: var(--danger) !important;
}

/* Plot tiles are transparent by default - always, whether or not they're
   currently rented - and only get their solid violet fill (set server-side
   via building_color, same mechanism as grass/water/street) back while the
   player is actively using the Rent/Release plot tool (body.tool-rent_plot /
   body.tool-release_plot, toggled by setTool() below). The !important is
   required because the fill is applied inline (both in the initial PHP
   render and in the live JS tile updater), and inline styles otherwise beat
   stylesheet rules. #8e44ad is hardcoded rather than "unset" because every
   plot tile's building_color is always that same constant (PLOT_ZONE_COLOR
   in index.php) - there's nothing else it could be. */
.tile[data-zone="plot"] {
    background-color: transparent !important;
}
body.tool-rent_plot .tile[data-zone="plot"],
body.tool-release_plot .tile[data-zone="plot"] {
    background-color: #8e44ad !important;
}

/* Outlines the outer boundary of a whole (possibly multi-tile) plot shape,
   not every individual tile - plot-edge-* classes are computed server-side
   in get_plot_edge_classes() (index.php) and mirrored client-side in
   getPlotEdgeClasses() for live map updates, marking only the side(s) of
   each tile that actually touch a non-plot neighbor or the map edge. */
.tile.plot-edge-top { border-top: 2px solid rgba(142, 68, 173, 0.9); }
.tile.plot-edge-right { border-right: 2px solid rgba(142, 68, 173, 0.9); }
.tile.plot-edge-bottom { border-bottom: 2px solid rgba(142, 68, 173, 0.9); }
.tile.plot-edge-left { border-left: 2px solid rgba(142, 68, 173, 0.9); }

/* Rental-state overlay drawn on top of a saved plot tile's violet base
   color (set server-side via building_color, same mechanism as
   grass/water/street). Dashed = free to rent, solid tinted = rented
   (blue if it's yours, wood if someone else's - same convention as stalls). */
.plot-layer {
    position: absolute;
    top: 0; left: 0;
    width: 100%; height: 100%;
    pointer-events: none;
}
.plot-obj {
    position: absolute;
    top: 3px; left: 3px; right: 3px; bottom: 3px;
    border-radius: 4px;
}
.plot-obj--available {
    border: 2px dashed rgba(255, 255, 255, 0.45);
}
.plot-obj--rented {
    border: 3px solid;
    box-shadow: 0 0 6px rgba(0, 0, 0, 0.45) inset;
}

body.tool-rent_plot #viewport { cursor: pointer; }
body.tool-rent_plot .tile[data-zone="plot"][data-owner-id=""]:hover {
    outline: 2px solid var(--ok) !important;
    outline-offset: -2px;
    z-index: 2;
}
body.tool-release_plot #viewport { cursor: pointer; }
body.tool-release_plot .tile[data-zone="plot"]:not([data-owner-id=""]):hover {
    outline: 2px solid var(--danger) !important;
    outline-offset: -2px;
    z-index: 2;
}

/* ---------- Business restock ---------- */
/* The old per-tile restock badge/button and its "out of stock" dimming
   filter have been fully retired - #restock-panel (top-left corner) is the
   only restock UI now; see handleRestockCategoryClick() in index.php. */

/* ---------- Restock panel (top-left) ---------- */
/* The single visible restock control, replacing the old per-tile badge
   above - see #restock-panel in index.php / handleRestockCategoryClick(). */

.restock-panel {
    position: absolute;
    /* A few px lower than a bare 16px so the .flyout-badge (which protrudes
       22px above the panel's own top edge, same as #build-flyout etc.)
       doesn't get clipped by the viewport edge. */
    top: 24px;
    left: 16px;
    z-index: 100;
    width: 340px;
    padding: 14px 16px 16px;
    background: var(--panel-bg);
    /* Full accent-colored outline, same as #build-flyout/.stock-modal-card/
       etc. (border: 2px solid var(--player-accent)) - previously this was
       a plain 1px panel-border with only a colored border-top, which is why
       the panel looked like it had no outline compared to the category
       flyouts. --player-accent is scoped to var(--tax) above. */
    border: 2px solid var(--player-accent);
    border-radius: 14px;
    backdrop-filter: blur(10px);
    box-shadow: 0 12px 30px rgba(0, 0, 0, 0.35);
    display: flex;
    flex-direction: column;
    gap: 10px;
    /* Deliberately NOT overflow-y:auto here anymore - that used to clip the
       .flyout-badge (which protrudes above this panel's own top edge) in
       half, since a scroll container clips its own overflowing children.
       Only .restock-panel-rows below scrolls now, so the badge (and the
       header above the row list) stay fully visible while the row list
       itself gets long. */
}

.restock-panel-header {
    display: flex;
    align-items: center;
}

.restock-panel-title {
    font-family: "Silkscreen", sans-serif;;
    font-size: 15px;
    font-weight: 600;
    text-transform: uppercase;
    color: var(--tax);
    text-align: center;
}

.restock-panel-rows {
    display: flex;
    flex-direction: column;
    gap: 8px;
    /* Fallback until JS (updateRestockPanelMaxHeight() in index.php) measures
       #build-dock's real position and sets an inline max-height that stops
       exactly above it - see that function for why a fixed vh value alone
       isn't enough. */
    max-height: calc(50vh - 32px);
    overflow-y: auto;

    /* Firefox: cienki suwak w kolorze motywu, ten sam wzorzec co .side-panel--admin */
    scrollbar-width: thin;
    scrollbar-color: rgba(224, 185, 61, 0.45) transparent;
}

/* Chrome / Edge / Safari: stylizacja suwaka dla listy wierszy restocku */
.restock-panel-rows::-webkit-scrollbar {
    width: 8px;
}
.restock-panel-rows::-webkit-scrollbar-track {
    background: transparent;
    margin: 4px 0;
}
.restock-panel-rows::-webkit-scrollbar-thumb {
    background-color: rgba(224, 185, 61, 0.45);
    border-radius: 8px;
    border: 2px solid transparent;
    background-clip: padding-box;
}
.restock-panel-rows::-webkit-scrollbar-thumb:hover {
    background-color: var(--tax);
}

.restock-row {
    display: flex;
    align-items: center;
    gap: 0;
    padding: 9px 11px;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid transparent;
    border-radius: 10px;
    transition: background 0.15s ease, border-color 0.15s ease;
}
/* .restock-row's own "display: flex" above has the same specificity as the
   browser's default "[hidden] { display: none }" rule and comes later in
   the cascade, so it was winning - rows stayed visible even after JS set
   row.hidden = true for a category the player doesn't own yet. Same fix
   pattern as .balance-dropdown[hidden] / .loan-form[hidden] /
   #loading-overlay[hidden] elsewhere in this file. */
.restock-row[hidden] { display: none; }

.restock-row--needed {
    background: var(--danger-soft);
    border-color: rgba(224, 85, 79, 0.35);
}

.restock-row-info {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.restock-row-label {
    font-size: 12.5px;
    font-weight: 600;
    color: var(--text-primary);
}

.restock-row-meta {
    display: flex;
    align-items: center;
    gap: 5px;
    font-size: 11px;
    color: var(--text-muted);
}

.restock-row-sep { opacity: 0.6; }

.restock-row-timer {
    font-family: var(--font-display);
    font-variant-numeric: tabular-nums;
    color: var(--ok);
}

.restock-row-timer--depleted {
    color: var(--danger);
    font-weight: 700;
    animation: restock-pulse 1s ease-in-out infinite;
}

.restock-row-btn {
    flex-shrink: 0;
    margin-left: 8px;
    font-family: var(--font-display);
    font-size: 10.5px;
    font-weight: 700;
    color: var(--tax);
    background: var(--tax-soft);
    border: 1px solid rgba(224, 185, 61, 0.4);
    border-radius: 8px;
    padding: 7px 10px;
    cursor: pointer;
    white-space: nowrap;
}
.restock-row-btn:hover {
    background: rgba(224, 185, 61, 0.28);
    border-color: rgba(224, 185, 61, 0.65);
}
.restock-row--needed .restock-row-btn {
    color: var(--danger);
    background: var(--danger-soft);
    border-color: rgba(224, 85, 79, 0.5);
}
.restock-row--needed .restock-row-btn:hover {
    background: rgba(224, 85, 79, 0.28);
    border-color: rgba(224, 85, 79, 0.7);
}

.route-name-input {
    font-family: var(--font-body);
    font-size: 13px;
    color: var(--text-primary);
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 8px;
    padding: 8px 10px;
    user-select: text;
}
.route-speed-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-size: 12px;
    color: var(--text-muted);
}
.route-speed-row input {
    width: 60px;
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 6px;
    color: var(--text-primary);
    padding: 4px 6px;
    font-family: var(--font-body);
    font-size: 12.5px;
    user-select: text;
}

.route-income-estimate {
    display: flex;
    flex-direction: column;
    gap: 2px;
    color: var(--admin-accent);
    font-weight: 600;
    margin-top: -2px;
}

.route-list-item {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 8px 10px;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 9px;
    font-size: 12.5px;
}
.route-list-item-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-weight: 600;
    gap: 8px;
}
.route-list-item-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}
.route-mini-btn {
    flex: 1;
    min-width: 64px;
    font-family: var(--font-body);
    font-size: 11.5px;
    padding: 5px 6px;
    border-radius: 6px;
    background: rgba(255, 255, 255, 0.05);
    border: 1px solid rgba(255, 255, 255, 0.1);
    color: var(--text-primary);
    cursor: pointer;
}
.route-mini-btn:hover { background: rgba(255, 255, 255, 0.1); }
.route-mini-btn--danger { color: var(--danger); border-color: rgba(224, 85, 79, 0.35); }
.route-mini-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.route-mini-btn:disabled:hover { background: rgba(255, 255, 255, 0.05); }

/* ---------- Admin: business type creation form ---------- */

.biz-form-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
}

.biz-color-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
}

.biz-color-label {
    flex-direction: row !important;
    align-items: center;
    gap: 8px !important;
}

.biz-color-input {
    width: 36px;
    height: 28px;
    padding: 2px;
    border-radius: 6px;
    border: 1px solid rgba(255, 255, 255, 0.15);
    background: transparent;
    cursor: pointer;
}

.biz-checkbox-label {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 12px;
    color: var(--text-muted);
    cursor: pointer;
}
.biz-checkbox-label input { cursor: pointer; }

.biz-form-actions {
    display: flex;
    gap: 8px;
}
.biz-form-actions .btn { flex: 1; }

/* ---------- Stall owner name tooltip ---------- */

.owner-tooltip {
    position: fixed;
    z-index: 400;
    padding: 5px 10px;
    background: var(--panel-bg);
    border: 1px solid var(--panel-border);
    border-radius: 8px;
    backdrop-filter: blur(10px);
    box-shadow: 0 8px 18px rgba(0, 0, 0, 0.35);
    font-size: 12px;
    font-weight: 500;
    color: var(--text-primary);
    pointer-events: none;
    white-space: nowrap;
}

/* ---------- Candle-hover tooltip (Stock market chart) ----------
   Reuses .owner-tooltip's box styling, but needs its own higher z-index:
   the stock market panel itself is a modal (.stock-modal, z-index: 500),
   so the default .owner-tooltip z-index (400, tuned for tooltips floating
   over the map) would render this tooltip BEHIND the panel it belongs to. */
#stock-chart-tooltip {
    z-index: 700;
}

/* Candle OHLC breakdown inside #stock-chart-tooltip (reuses .owner-tooltip
   for position/box styling above) - colors each price line to match the
   candle's up/down color used on the chart itself. */
#stock-chart-tooltip .is-up { color: var(--ok); }
#stock-chart-tooltip .is-down { color: var(--bad); }

/* ---------- Login / registration screen ---------- */

.auth-body {
    position: relative;
    overflow: auto;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Just the two corner glows here now - the drafting-table grid moved to
       .auth-body::before below so it can drift on its own without dragging
       these glows along with it (see that rule for why). */
    background-image:
        radial-gradient(circle at 15% 10%, rgba(53, 208, 192, 0.05), transparent 45%),
        radial-gradient(circle at 85% 90%, rgba(230, 131, 46, 0.05), transparent 45%);
}

/* Faint drafting-table grid behind the whole login screen, tying the
   "blueprint" build-in effect (below) to a permanent, slowly-alive backdrop
   rather than a one-off intro gimmick. Two pitches (small + large square)
   layered for a proper graph-paper feel. Isolated into its own pseudo-
   element (rather than living on .auth-body directly, as it used to) so
   bp-grid-drift below can animate background-position on just the grid,
   leaving the two radial corner glows on .auth-body itself perfectly still.
   z-index: -1 (with .auth-body's own position: relative above establishing
   the stacking context) keeps it behind .auth-wrap's normal-flow content. */
.auth-body::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    background-image:
        repeating-linear-gradient(to right, rgba(53, 208, 192, 0.05) 0 1px, transparent 1px 88px),
        repeating-linear-gradient(to bottom, rgba(53, 208, 192, 0.05) 0 1px, transparent 1px 88px),
        repeating-linear-gradient(to right, rgba(53, 208, 192, 0.09) 0 1px, transparent 1px 22px),
        repeating-linear-gradient(to bottom, rgba(53, 208, 192, 0.09) 0 1px, transparent 1px 22px);
    animation: bp-grid-drift 72s linear infinite;
}

/* Drifts by exact multiples of both grid pitches (88px and 22px) so the
   loop point is seamless - no visible jump when it wraps. */
@keyframes bp-grid-drift {
    from { background-position: 0 0, 0 0, 0 0, 0 0; }
    to   { background-position: 176px 176px, 176px 176px, 44px 44px, 44px 44px; }
}

.auth-wrap {
    width: 100%;
    height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 20px;
}

/* ---------- Login screen logo ---------- */
/* Sits above .blueprint-stage inside .auth-wrap (now a column flex, so both
   are centered together as one group). The source file (logo.jpg) is a flat
   4096x4096 square with an opaque #121212 backdrop baked in - close to but
   not identical to --bg (#12151a), and definitely not identical to whatever
   happens to sit under .auth-card at any given moment (radial glows, the
   drifting drafting-table grid). Two things do the blending together:
   mix-blend-mode: screen treats near-black pixels as if they weren't there
   at all (screen-blending black with whatever's underneath just reproduces
   whatever's underneath unchanged), so the flat #121212 backdrop baked into
   the jpg effectively dissolves into the page and only the actual bright
   logo artwork stays visible/opaque - while mask-image still fades the
   remaining rectangular edge into a soft circle on top of that, so even any
   lighter pixels right at the image's border (which screen alone wouldn't
   hide) taper off gently instead of ending in a hard edge. */
.auth-logo-wrap {
    width: 108px;
    height: 108px;
    flex-shrink: 0;
    opacity: 0;
    animation: auth-logo-in 0.6s ease-out 0.15s forwards;
}

.auth-logo {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    -webkit-mask-image: radial-gradient(circle at center, #000 42%, transparent 68%);
    mask-image: radial-gradient(circle at center, #000 42%, transparent 68%);
    mix-blend-mode: screen;
    filter: contrast(1.12) saturate(1.1) drop-shadow(0 0 20px rgba(53, 208, 192, 0.3));
}

@keyframes auth-logo-in {
    from { opacity: 0; transform: translateY(-8px) scale(0.92); }
    to   { opacity: 1; transform: translateY(0) scale(1); }
}

/* Wraps .auth-card so the blueprint overlay (drawn border + corner marks)
   can sit exactly on top of it, absolutely positioned, without affecting
   the card's own layout/sizing. */
.blueprint-stage {
    position: relative;
    width: 320px;
}

.auth-card {
    width: 320px;
    padding: 26px 26px 28px;
    border-radius: 0;
    display: flex;
    flex-direction: column;
    gap: 14px;
    position: relative;
    /* Starts fully transparent/borderless - it "hardens" into the real
       glass panel once the blueprint outline finishes drawing itself.
       See @keyframes bp-harden below for the final values (these match
       what used to be static: var(--panel-bg)/blur(10px)/the same
       box-shadow). No border here on purpose: the card's outline is the
       drawn .bp-overlay (dashed edges + corner marks) below, kept alive
       permanently instead of hardening into a normal solid border - and
       with no border added to this box, it stays pixel-for-pixel the same
       size as .blueprint-stage/.bp-overlay, so the dashed outline and the
       CAD cursor's patrol path line up exactly on the card's real edge. */
    background: rgba(24, 28, 35, 0);
    border: none;
    box-shadow: 0 20px 45px rgba(0, 0, 0, 0);
    backdrop-filter: blur(0px);
    animation: bp-harden 0.55s ease-out 1.25s forwards;
}

@keyframes bp-harden {
    to {
        background: rgba(24, 28, 35, 0.88);
        box-shadow: 0 20px 45px rgba(0, 0, 0, 0.45);
        backdrop-filter: blur(10px);
    }
}

/* ---------- Blueprint build-in overlay ---------- */
/* Sits absolutely over .blueprint-stage, exactly matching .auth-card's
   box, and stacked above it (z-index) so the dashed lines/corner marks
   stay visible on top of the card's opaque glass fill instead of being
   painted over by it. Draws a dashed outline clockwise (top -> right ->
   bottom -> left) with L-shaped corner marks popping in as each edge
   completes, like a plan being drafted - then, rather than fading away
   once .auth-card hardens, it settles into a permanent ambient "breathe"
   (see bp-overlay-breathe below) so the blueprint outline IS the card's
   border for as long as the login screen is open. */
.bp-overlay {
    position: absolute;
    inset: 0;
    border-radius: 0;
    pointer-events: none;
    z-index: 2;
    opacity: 1;
    animation: bp-overlay-breathe 3.6s ease-in-out 1.8s infinite;
}

@keyframes bp-overlay-breathe {
    0%, 100% { opacity: 0.55; }
    50% { opacity: 0.85; }
}

.bp-edge {
    position: absolute;
    background-repeat: repeat;
}
.bp-edge-top, .bp-edge-bottom {
    left: 0;
    width: 100%;
    height: 2px;
    background-image: repeating-linear-gradient(to right, var(--admin-accent) 0 6px, transparent 6px 11px);
    transform: scaleX(0);
}
.bp-edge-left, .bp-edge-right {
    top: 0;
    width: 2px;
    height: 100%;
    background-image: repeating-linear-gradient(to bottom, var(--admin-accent) 0 6px, transparent 6px 11px);
    transform: scaleY(0);
}
.bp-edge-top { top: 0; transform-origin: left center; animation: bp-draw-x 0.35s ease-out 0s forwards; }
.bp-edge-right { right: 0; transform-origin: top center; animation: bp-draw-y 0.35s ease-out 0.3s forwards; }
.bp-edge-bottom { bottom: 0; transform-origin: right center; animation: bp-draw-x 0.35s ease-out 0.6s forwards; }
.bp-edge-left { left: 0; transform-origin: bottom center; animation: bp-draw-y 0.35s ease-out 0.9s forwards; }

@keyframes bp-draw-x { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes bp-draw-y { from { transform: scaleY(0); } to { transform: scaleY(1); } }

.bp-corner {
    position: absolute;
    width: 14px;
    height: 14px;
    opacity: 0;
    transform: scale(0.4);
}
.bp-corner-tl { top: -1px; left: -1px; border-top: 2px solid var(--admin-accent); border-left: 2px solid var(--admin-accent); animation: bp-corner-pop 0.25s ease-out 0s forwards; }
.bp-corner-tr { top: -1px; right: -1px; border-top: 2px solid var(--admin-accent); border-right: 2px solid var(--admin-accent); animation: bp-corner-pop 0.25s ease-out 0.6s forwards; }
.bp-corner-br { bottom: -1px; right: -1px; border-bottom: 2px solid var(--admin-accent); border-right: 2px solid var(--admin-accent); animation: bp-corner-pop 0.25s ease-out 0.9s forwards; }
.bp-corner-bl { bottom: -1px; left: -1px; border-bottom: 2px solid var(--admin-accent); border-left: 2px solid var(--admin-accent); animation: bp-corner-pop 0.25s ease-out 1.2s forwards; }

@keyframes bp-corner-pop {
    0% { opacity: 0; transform: scale(0.4); }
    65% { opacity: 1; transform: scale(1.2); }
    100% { opacity: 1; transform: scale(1); }
}

.bp-tag {
    position: absolute;
    top: -9px;
    left: 16px;
    padding: 1px 7px;
    background: var(--grid-bg);
    font-family: var(--font-display);
    font-size: 9px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--admin-accent);
    opacity: 0;
    animation: bp-tag-in 0.3s ease-out 1.0s forwards;
}
@keyframes bp-tag-in {
    from { opacity: 0; transform: translateY(2px); }
    to { opacity: 0.85; transform: translateY(0); }
}

/* Small CAD-style crosshair that continuously patrols the card's edge - a
   plus-shaped marker (two thin bars via ::before/::after) rather than a dot,
   to read as "a drafting cursor" rather than a loading spinner. Lives as a
   direct sibling of .bp-overlay/.auth-card inside .blueprint-stage (not
   inside .bp-overlay itself) so it keeps running for as long as the login
   screen is open, instead of fading out with the one-off intro drawing.
   Positioned in %, so it tracks the card's actual edges whether the
   register form (taller, 3 fields) or the login form (shorter, 2 fields)
   is showing - no fixed pixel path needed. */
.bp-cad-cursor {
    position: absolute;
    width: 9px;
    height: 9px;
    margin: -4.5px;
    pointer-events: none;
    z-index: 3;
    opacity: 0;
    animation:
        bp-cad-fade-in 0.4s ease-out 1.8s forwards,
        bp-cad-orbit 15s linear 2.2s infinite;
}
.bp-cad-cursor::before,
.bp-cad-cursor::after {
    content: '';
    position: absolute;
    background: var(--admin-accent);
}
.bp-cad-cursor::before { left: 50%; top: 0; width: 1px; height: 100%; transform: translateX(-50%); }
.bp-cad-cursor::after { top: 50%; left: 0; width: 100%; height: 1px; transform: translateY(-50%); }

@keyframes bp-cad-fade-in {
    to { opacity: 0.8; }
}

/* Travels clockwise (top-left -> top-right -> bottom-right -> bottom-left),
   dwelling briefly at each corner (like a plotter pausing before changing
   direction) rather than gliding past it. */
@keyframes bp-cad-orbit {
    0%, 6%    { left: 0%;   top: 0%; }
    19%       { left: 100%; top: 0%; }
    25%, 31%  { left: 100%; top: 0%; }
    44%       { left: 100%; top: 100%; }
    50%, 56%  { left: 100%; top: 100%; }
    69%       { left: 0%;   top: 100%; }
    75%, 81%  { left: 0%;   top: 100%; }
    94%       { left: 0%;   top: 0%; }
    100%      { left: 0%;   top: 0%; }
}

/* Content inside the card fills in after the structure "hardens" - each
   .bp-item rises and fades in, staggered via the --i custom property set
   inline on each element (0, 1, 2, 3...). */
.bp-item {
    opacity: 0;
    transform: translateY(8px);
    animation: bp-item-in 0.4s ease-out forwards;
    animation-delay: calc(1.45s + (var(--i, 0) * 70ms));
}
@keyframes bp-item-in {
    to { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
    .auth-card { animation: none; background: rgba(24, 28, 35, 0.88); box-shadow: 0 20px 45px rgba(0, 0, 0, 0.45); backdrop-filter: blur(10px); }
    .bp-overlay { animation: none; opacity: 0.7; }
    .bp-item { opacity: 1; transform: none; animation: none; }
    .auth-body::before { animation: none; }
    .bp-cad-cursor { display: none; }
    .auth-tab.is-active::after { animation: none; transform: scaleX(1); }
    .auth-logo-wrap { opacity: 1; animation: none; }
}

.auth-tabs {
    display: flex;
    gap: 6px;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid var(--panel-border);
    border-radius: 10px;
    padding: 4px;
}
.auth-tab {
    position: relative;
    flex: 1;
    text-align: center;
    padding: 8px 10px;
    border-radius: 7px;
    font-size: 13px;
    font-weight: 500;
    color: var(--text-muted);
    text-decoration: none;
}
.auth-tab.is-active {
    background: var(--admin-accent-soft);
    color: var(--text-primary);
}
/* Log in <-> Register is a real link (full page reload, not a client-side
   toggle), so there's no "sliding" motion possible between the two states -
   what we can do instead is have the underline draw itself in on load,
   same beat as the rest of the card's content (bp-item's own stagger runs
   1.45s+ - this starts right after). Reuses bp-draw-x (already defined
   above for the card's own top/bottom edges) so it's the same "line drawing
   itself left-to-right" motion as everywhere else on this screen. */
.auth-tab.is-active::after {
    content: '';
    position: absolute;
    left: 10px;
    right: 10px;
    bottom: 3px;
    height: 2px;
    border-radius: 2px;
    background: var(--admin-accent);
    transform: scaleX(0);
    transform-origin: left center;
    animation: bp-draw-x 0.35s ease-out 1.55s forwards;
}

.auth-error {
    font-size: 12.5px;
    color: var(--bad);
    background: var(--danger-soft);
    border: 1px solid rgba(224, 85, 79, 0.35);
    border-radius: 8px;
    padding: 8px 10px;
    /* Overrides .bp-item's own fade+rise animation (rather than adding a
       second animation alongside it) - both would animate `transform`, and
       stacking two animations on the same property means one just clobbers
       the other's output instead of combining, so the rise-in and the
       shake are folded into a single keyframe list here. animation-delay
       is kept identical to .bp-item's own formula so it still lands in the
       same stagger slot as every other .bp-item. */
    animation: bp-error-in 0.5s ease-out forwards;
    animation-delay: calc(1.45s + (var(--i, 0) * 70ms));
}
@keyframes bp-error-in {
    0%   { opacity: 0; transform: translate(0, 8px); }
    40%  { opacity: 1; transform: translate(0, 0); }
    55%  { transform: translate(-6px, 0); }
    70%  { transform: translate(5px, 0); }
    85%  { transform: translate(-3px, 0); }
    100% { opacity: 1; transform: translate(0, 0); }
}

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

.auth-label {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 6px;
    font-size: 12px;
    color: var(--text-muted);
}

/* Blueprint-flavored dimension marker - appears next to a field on
   hover/focus like the arrow+number call-outs on a technical drawing.
   Sits in the 12px gap .auth-form already leaves between fields (bottom:
   -12px), so it never overlaps the next label's own text. Purely
   decorative - every field reports the same "268mm" since they're all the
   same rendered width, in keeping with .bp-tag's own "Scale 1:1" conceit. */
.auth-label::after {
    content: '\21A4 user_data \21A6';
    position: absolute;
    right: 0;
    bottom: -12px;
    font-family: var(--font-display);
    font-size: 9px;
    letter-spacing: 0.03em;
    color: var(--admin-accent);
    opacity: 0;
    pointer-events: none;
    transform: translateY(-2px);
    transition: opacity 0.15s ease, transform 0.15s ease;
}
.auth-label:hover::after,
.auth-label:focus-within::after {
    opacity: 0.75;
    transform: translateY(0);
}

.auth-input {
    font-family: var(--font-body);
    font-size: 13.5px;
    color: var(--text-primary);
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 8px;
    padding: 10px 12px;
    user-select: text;
    transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
}
.auth-input:focus {
    outline: none;
    border-color: var(--admin-accent-strong);
    box-shadow: 0 0 0 3px var(--admin-accent-soft), 0 0 14px var(--admin-accent-soft);
    transform: scale(1.01);
}

textarea.auth-input {
    font-family: inherit;
    resize: vertical;
    min-height: 60px;
}

.auth-submit {
    position: relative;
    justify-content: center;
    margin-top: 4px;
    border-color: var(--admin-accent-strong);
    background: var(--admin-accent-soft);
}
/* Hover-only outline that "draws itself" left-to-right, same idea as
   bp-edge-top/bp-draw-x on the card above but on a :hover trigger instead
   of a page-load delay, and quicker (0.25s vs 0.35s) since it needs to feel
   responsive to the cursor rather than part of the slow intro reveal.
   Dashed to match the blueprint's dashed card edges rather than plain
   solid. transform-origin: left + scaleX means it grows left-to-right on
   hover-in and shrinks back the same way on hover-out. */
.auth-submit::after {
    content: '';
    position: absolute;
    inset: -3px;
    border: 1.5px dashed var(--admin-accent);
    border-radius: inherit;
    opacity: 0;
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform 0.25s ease-out, opacity 0.1s ease-out;
    pointer-events: none;
}
.auth-submit:hover::after {
    opacity: 0.9;
    transform: scaleX(1);
}

/* ---------- Loading overlay (login.php + index.php) ---------- */
/* index.php also inlines a copy of these same rules directly in <head>, so
   the overlay is already styled correctly the instant it paints, before
   this stylesheet has necessarily finished downloading - see the comment
   there. Keep the two in sync if this changes. */
#loading-overlay {
    position: fixed;
    inset: 0;
    z-index: 9999;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg);
    transition: opacity 0.35s ease;
}
/* Needed because the #loading-overlay ID rule above (display: flex) is more
   specific than the browser's default [hidden] { display: none } rule, so
   without this, the hidden attribute login.php uses before the user submits
   a form would otherwise be silently overridden and the overlay would show
   immediately instead of only on submit. */
#loading-overlay[hidden] { display: none; }
#loading-overlay.is-hidden { opacity: 0; pointer-events: none; }
.loading-box {
    width: 280px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
}
.loading-title {
    font-family: var(--font-display);
    font-size: 15px;
    font-weight: 600;
    color: var(--text-primary);
    letter-spacing: 0.02em;
}
.loading-bar-track {
    width: 100%;
    height: 8px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.08);
    overflow: hidden;
}
.loading-bar-fill {
    width: 0%;
    height: 100%;
    border-radius: 999px;
    background: var(--admin-accent);
    transition: width 0.25s ease;
}
.loading-percent {
    font-family: var(--font-body);
    font-size: 12.5px;
    color: var(--text-muted);
}

/* ---------- Notification toasts (index.php) ----------
   Only rendered by showNotification(msg, 'error') - i.e. when the player
   tried to do something and couldn't (not enough money, not their
   building, a failed request, invalid input, ...). 'success'/'info' calls
   stay silent on purpose, see the JS comment above showNotification(). */
.notification-stack {
    position: fixed;
    right: 18px;
    bottom: 18px;
    z-index: 10000;
    display: flex;
    flex-direction: column-reverse;
    gap: 8px;
    pointer-events: none;
}

.notification-toast {
    max-width: 360px;
    font-family: var(--font-body);
    font-size: 13px;
    line-height: 1.4;
    color: var(--text-primary);
    background: var(--panel-bg);
    border: 1px solid rgba(224, 85, 79, 0.35);
    border-left: 3px solid var(--danger);
    border-radius: 8px;
    padding: 10px 14px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
    backdrop-filter: blur(6px);
    opacity: 0;
    transform: translateY(8px);
    transition: opacity 0.2s ease, transform 0.2s ease;
    pointer-events: auto;
    cursor: pointer;
}

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

.notification-toast--error::before {
    content: '⚠ ';
    color: var(--danger);
}

/* ==========================================================================
   3D model shape geometry
   ==========================================================================
   Used by model-editor.php (building/editing custom models) for its own
   real preserve-3d/rotateX/rotateY/translateZ preview - see BUSINESS_SHAPES
   / getFacesForShape() in model-editor.php for how the face divs are
   generated and which faces each shape actually uses. Every shape is built
   out of 90px "cube-face" divs inside a 90px .custom-tile container
   representing one 1x1x1 grid cell.

   The map (index.php) USED to share this same geometry for rendering saved
   models on placed businesses, but no longer does - it now pre-flattens
   every model into plain 2D matrix()-transformed ".cube-face-2d" divs
   instead (see the "2D-flattened building faces" section further down),
   so none of the classes below are ever created on the map anymore. Kept
   editor-only on purpose: model-editor.php's free-orbit camera still needs
   genuine 3D geometry, which is exactly what these rules provide.
   Editor-only concerns (ghost preview, eraser hover state, the floor grid
   palette, the orbit camera stage/rig) stay in model-editor.php's own
   <style> block as before - only the shape geometry itself lives here. */

.custom-tile {
    position: absolute;
    width: 90px;
    height: 90px;
    box-sizing: border-box;
    transform-style: preserve-3d;
}

.cube-face {
    position: absolute;
    box-sizing: border-box;
    background: #808080;
    border: 1px solid #4a4a4a;
    transition: background 0.15s;
}

/* --- PRECYZYJNA GEOMETRIA KSZTAŁTÓW 3D --- */

/* Color custom properties: unset by default, so model-editor.php (which never
   sets them) keeps rendering its neutral grey preview exactly as before. The
   map sets these inline on .building-mount (see render_business_model_mount()
   in config.php) to tint the whole model per-owner instead of grey - top/mid
   faces get the lighter shade, side/bottom faces the darker one, matching the
   old linear-gradient(135deg, light, dark) flat sprite's convention. */

/* 1. Cube */
.shape-cube .cube-face-top { width: 90px; height: 90px; transform: translateZ(90px); background: var(--face-top, #9e9e9e); }
.shape-cube .cube-face-bottom { width: 90px; height: 90px; transform: translateZ(0px); background: var(--face-dark, #555555); }
.shape-cube .cube-face-front { width: 90px; height: 90px; transform-origin: bottom center; transform: rotateX(-90deg); bottom: 0; background: var(--face-mid, #808080); }
.shape-cube .cube-face-back { width: 90px; height: 90px; transform-origin: top center; transform: rotateX(90deg); top: 0; background: var(--face-mid, #808080); }
.shape-cube .cube-face-left { width: 90px; height: 90px; transform-origin: left center; transform: rotateY(-90deg); left: 0; background: var(--face-side, #666666); }
.shape-cube .cube-face-right { width: 90px; height: 90px; transform-origin: right center; transform: rotateY(90deg); right: 0; background: var(--face-side, #666666); }

/* 2. Slab (Płytka 30px) */
.shape-slab .cube-face-top { width: 90px; height: 90px; transform: translateZ(30px); background: var(--face-top, #a8a8a8); }
.shape-slab .cube-face-bottom { width: 90px; height: 90px; transform: translateZ(0px); background: var(--face-dark, #555555); }
.shape-slab .cube-face-front { width: 90px; height: 30px; transform-origin: bottom center; transform: rotateX(-90deg); bottom: 0; background: var(--face-mid, #888888); }
.shape-slab .cube-face-back { width: 90px; height: 30px; transform-origin: top center; transform: rotateX(90deg); top: 0; background: var(--face-mid, #888888); }
.shape-slab .cube-face-left { width: 30px; height: 90px; transform-origin: left center; transform: rotateY(-90deg); left: 0; background: var(--face-side, #6e6e6e); }
.shape-slab .cube-face-right { width: 30px; height: 90px; transform-origin: right center; transform: rotateY(90deg); right: 0; background: var(--face-side, #6e6e6e); }

/* 3. Post (Słupek 0.25x0.25x1 -> 22.5px x 22.5px x 90px) */
.shape-post .cube-face-top { width: 22.5px; height: 22.5px; left: 33.75px; top: 33.75px; transform: translateZ(90px); background: var(--face-top, #b0b0b0); }
.shape-post .cube-face-bottom { width: 22.5px; height: 22.5px; left: 33.75px; top: 33.75px; transform: translateZ(0px); background: var(--face-dark, #555555); }
.shape-post .cube-face-front { width: 22.5px; height: 90px; left: 33.75px; bottom: 33.75px; transform-origin: bottom center; transform: rotateX(-90deg); background: var(--face-mid, #888888); }
.shape-post .cube-face-back { width: 22.5px; height: 90px; left: 33.75px; top: 33.75px; transform-origin: top center; transform: rotateX(90deg); background: var(--face-mid, #888888); }
.shape-post .cube-face-left { width: 90px; height: 22.5px; left: 33.75px; top: 33.75px; transform-origin: left center; transform: rotateY(-90deg); background: var(--face-side, #6e6e6e); }
.shape-post .cube-face-right { width: 90px; height: 22.5px; right: 33.75px; top: 33.75px; transform-origin: right center; transform: rotateY(90deg); background: var(--face-side, #6e6e6e); }

/* 4. Wedge (Rampa) */
.shape-wedge .cube-face-top { width: 90px; height: 127.28px; top: 0; transform-origin: top center; transform: translateZ(90px) rotateX(-45deg); background: var(--face-top, #a0a0a0); }
.shape-wedge .cube-face-bottom { width: 90px; height: 90px; transform: translateZ(0px); background: var(--face-dark, #555555); }
.shape-wedge .cube-face-back { width: 90px; height: 90px; top: 0; transform-origin: top center; transform: rotateX(90deg); background: var(--face-mid, #7b7b7b); }
.shape-wedge .cube-face-left { width: 90px; height: 90px; left: 0; top: 0; transform-origin: left center; transform: rotateY(-90deg); clip-path: polygon(0% 0%, 100% 0%, 0% 100%); background: var(--face-side, #666666); }
.shape-wedge .cube-face-right { width: 90px; height: 90px; right: 0; top: 0; transform-origin: right center; transform: rotateY(90deg); clip-path: polygon(0% 0%, 100% 0%, 100% 100%); background: var(--face-side, #666666); }

/* 5. Pyramid (Piramida) */
.shape-pyramid .cube-face-bottom { width: 90px; height: 90px; transform: translateZ(0px); background: var(--face-dark, #555555); }
.shape-pyramid .cube-face-front { width: 90px; height: 100.62px; bottom: 0; transform-origin: bottom center; transform: rotateX(-63.435deg); clip-path: polygon(50% 0%, 0% 100%, 100% 100%); background: var(--face-top, #969696); }
.shape-pyramid .cube-face-back { width: 90px; height: 100.62px; top: 0; transform-origin: top center; transform: rotateX(63.435deg); clip-path: polygon(50% 100%, 0% 0%, 100% 0%); background: var(--face-mid, #888888); }
.shape-pyramid .cube-face-left { width: 100.62px; height: 90px; left: 0; top: 0; transform-origin: left center; transform: rotateY(-63.435deg); clip-path: polygon(0% 0%, 100% 50%, 0% 100%); background: var(--face-side, #707070); }
.shape-pyramid .cube-face-right { width: 100.62px; height: 90px; right: 0; top: 0; transform-origin: right center; transform: rotateY(63.435deg); clip-path: polygon(100% 0%, 0% 50%, 100% 100%); background: var(--face-dark, #606060); }

/* 6. Window (Okno) - same 1x1x1 box as Cube (geometry-wise it collides,
   rotates and stacks exactly like a Cube); only the front face differs,
   painted with an inset "glass" rectangle plus a cross of mullions on top
   of the normal owner-tinted wall color. The glass tone itself is a fixed
   light blue rather than var(--face-mid) - unlike the wall it sits in, real
   glass doesn't take on the owner's paint color, and a fixed tone keeps it
   readable as "a window" regardless of what color the building is. */
.shape-window .cube-face-top { width: 90px; height: 90px; transform: translateZ(90px); background: var(--face-top, #9e9e9e); }
.shape-window .cube-face-bottom { width: 90px; height: 90px; transform: translateZ(0px); background: var(--face-dark, #555555); }
.shape-window .cube-face-front {
    width: 90px; height: 90px; transform-origin: bottom center; transform: rotateX(-90deg); bottom: 0;
    background:
        linear-gradient(rgba(35, 35, 40, 0.55), rgba(35, 35, 40, 0.55)) center / 100% 5px no-repeat,
        linear-gradient(rgba(35, 35, 40, 0.55), rgba(35, 35, 40, 0.55)) center / 5px 100% no-repeat,
        linear-gradient(160deg, #c3e6ff, #6fa8d6) 10px 10px / calc(100% - 20px) calc(100% - 20px) no-repeat,
        var(--face-mid, #808080);
}
.shape-window .cube-face-back { width: 90px; height: 90px; transform-origin: top center; transform: rotateX(90deg); top: 0; background: var(--face-mid, #808080); }
.shape-window .cube-face-left { width: 90px; height: 90px; transform-origin: left center; transform: rotateY(-90deg); left: 0; background: var(--face-side, #666666); }
.shape-window .cube-face-right { width: 90px; height: 90px; transform-origin: right center; transform: rotateY(90deg); right: 0; background: var(--face-side, #666666); }

/* 7. Door (Drzwi) - same 1x1x1 box as Cube; the other 5 faces stay
   owner-tinted exactly like Cube's, but the front face is a fixed
   wood-brown door with two inset panels and a knob, deliberately NOT using
   var(--face-mid) so it always reads as "a door" no matter the building's
   own color, same reasoning as Window's fixed glass tone above. */
.shape-door .cube-face-top { width: 90px; height: 90px; transform: translateZ(90px); background: var(--face-top, #9e9e9e); }
.shape-door .cube-face-bottom { width: 90px; height: 90px; transform: translateZ(0px); background: var(--face-dark, #555555); }
.shape-door .cube-face-front {
    width: 90px; height: 90px; transform-origin: bottom center; transform: rotateX(-90deg); bottom: 0;
    background:
        radial-gradient(circle 4px at 74px 48px, #2b1c10, #2b1c10 4px, transparent 4.5px),
        linear-gradient(rgba(0, 0, 0, 0.22), rgba(0, 0, 0, 0.22)) 14px 12px / calc(100% - 28px) 34px no-repeat,
        linear-gradient(rgba(0, 0, 0, 0.22), rgba(0, 0, 0, 0.22)) 14px 54px / calc(100% - 28px) 24px no-repeat,
        linear-gradient(160deg, #8a5a34, #6b4423);
}
.shape-door .cube-face-back { width: 90px; height: 90px; transform-origin: top center; transform: rotateX(90deg); top: 0; background: var(--face-mid, #808080); }
.shape-door .cube-face-left { width: 90px; height: 90px; transform-origin: left center; transform: rotateY(-90deg); left: 0; background: var(--face-side, #666666); }
.shape-door .cube-face-right { width: 90px; height: 90px; transform-origin: right center; transform: rotateY(90deg); right: 0; background: var(--face-side, #666666); }

/* 8. Quarter (0.5x0.5x0.5 sub-block) - geometrically identical to Cube's own
   rules, just at half scale (45px instead of 90px) and always anchored at
   this shape's own top-left-front-bottom corner. Unlike Post (which insets
   its faces to stay CENTERED within the 90px cell), Quarter does NOT inset
   here - it always renders as if it were the qx=0/qy=0/zh=0 corner of the
   cell. Its qx/qy/zh sub-position instead shifts the whole .custom-tile
   container via inline left/top/translateZ, set in model-editor.php's
   renderPlacedTiles()/updateGhost() alongside the existing row/col/level
   placement - keeping this file to pure per-shape face geometry, same as
   every other shape here, with placement itself staying in JS. */
.shape-quarter .cube-face-top { width: 45px; height: 45px; transform: translateZ(45px); background: var(--face-top, #9e9e9e); }
.shape-quarter .cube-face-bottom { width: 45px; height: 45px; transform: translateZ(0px); background: var(--face-dark, #555555); }
.shape-quarter .cube-face-front { width: 45px; height: 45px; transform-origin: bottom center; transform: rotateX(-90deg); top: 0; background: var(--face-mid, #808080); }
.shape-quarter .cube-face-back { width: 45px; height: 45px; transform-origin: top center; transform: rotateX(90deg); top: 0; background: var(--face-mid, #808080); }
.shape-quarter .cube-face-left { width: 45px; height: 45px; transform-origin: left center; transform: rotateY(-90deg); left: 0; background: var(--face-side, #666666); }
.shape-quarter .cube-face-right { width: 45px; height: 45px; transform-origin: right center; transform: rotateY(90deg); left: 0; background: var(--face-side, #666666); }

/* ==========================================================================
   2D-flattened building faces (map only) - 2/4
   ==========================================================================
   Map-side counterpart to the real-3D ".cube-face"/".shape-<shape>
   .cube-face-<face>" rules above, which stay exactly as they are for
   model-editor.php's own free-orbit preview (still real preserve-3d there -
   untouched by anything in this section). The map itself no longer builds
   any .custom-tile/.cube-face/.building-model-rig hierarchy at all - see
   buildFlatBuildingMount() in index.php, which now creates one flat,
   absolutely-positioned ".cube-face-2d.cube-face-2d-<face>" div per face
   directly inside .building-mount, with its geometry (left/top/width/height
   and a 2D matrix() standing in for the old rotateX/rotateY/translateZ)
   already computed and set inline by projectIso()/faceMatrixFromCorners() -
   so unlike the real-3D rules above, none of THIS section needs width,
   height, position offsets, transform, or transform-origin: JS already did
   that math once at build time instead of the browser compositing it live
   every frame. All that's left for CSS to supply is the box model + the
   per-face/per-shape shading, i.e. exactly what's below.

   DONE (as of this quarter): the now-dead transform-style:preserve-3d and
   .building-model-rig/.is-rotated-v rotateZ/translateX rules that
   #buildings-3d-layer/.building-anchor-cell/.building-mount/
   .building-model-rig used to carry from the real-3D era are gone - see
   those rules further up, which no longer mention preserve-3d at all, and
   .building-model-rig itself, which no longer exists as a class anywhere in
   this file (nothing builds one on the map anymore; model-editor.php never
   used that particular class name to begin with - its own rig lives under
   #model-rig in its own <style> block). */


.cube-face-2d {
    position: absolute;
    box-sizing: border-box;
    border: 1px solid #4a4a4a;
    /* REQUIRED to match faceMatrixFromCorners() in index.php: that function
       returns matrix(a,b,c,d,0,0) - a pure linear map with NO translation -
       on the assumption that the transform pivots around the box's own
       (0,0) local origin (its top-left corner, i.e. exactly the point
       `left/top: p00.x/p00.y` already places at the correct screen
       position). CSS's actual default transform-origin is 50% 50% (the
       box's center), not its top-left corner - left uncorrected, every
       face's matrix rotates/shears around the wrong pivot, throwing each
       one off by roughly half its own (transformed) width/height instead
       of landing where p00 put it. That's what "scattered" faces are: each
       one individually shaped/shaded correctly, just pivoted from the wrong
       point. top left (== 0 0) makes the pivot match what index.php already
       assumes.
       No `transition: background` here (unlike .cube-face above) - that
       existed for model-editor.php's paint-mode hover feedback, which flat
       map faces never get; skipping it avoids an unnecessary transition
       property on what can be thousands of these divs on a full map. */
    transform-origin: top left;
}

/* Generic per-face shading - correct for every shape except pyramid (see
   its override below): top gets the lightest shade, bottom the darkest,
   front/back the mid shade, left/right the side shade - exactly the same
   convention .shape-cube/.shape-slab/.shape-post/.shape-wedge's own
   .cube-face-<face> rules above already use. The real-3D rules had to
   repeat this per shape because they ALSO carried shape-specific
   width/height/transform on the same selector; the flat version keys
   shading off the face name alone since geometry is handled entirely via
   inline styles now (see the big comment above). */
.cube-face-2d-top { background: var(--face-top, #9e9e9e); }
.cube-face-2d-bottom { background: var(--face-dark, #555555); }
.cube-face-2d-front, .cube-face-2d-back { background: var(--face-mid, #808080); }
.cube-face-2d-left, .cube-face-2d-right { background: var(--face-side, #666666); }

/* Pyramid is the one shape whose real-3D shading (see .shape-pyramid above)
   breaks from that generic mapping: its front face reuses the TOP shade
   (the pyramid has no true top face - its slanted front is what catches the
   most light instead) and its right face reuses the DARK shade rather than
   side. buildFlatBuildingMount() sets data-shape on every face div (mirrors
   block.shape) specifically so this override can target pyramid alone
   without touching cube/slab/post/wedge, which all already match the
   generic rule above as-is. */
[data-shape="pyramid"].cube-face-2d-front { background: var(--face-top, #969696); }
[data-shape="pyramid"].cube-face-2d-right { background: var(--face-dark, #606060); }

/* Window/Door mirror their own real-3D front-face overrides above (same
   fixed glass/wood tones, not owner-tinted) - only the front face differs
   from the generic per-face shading; their other 5 faces already match the
   generic rule same as Cube's. */
[data-shape="window"].cube-face-2d-front {
    background:
        linear-gradient(rgba(35, 35, 40, 0.55), rgba(35, 35, 40, 0.55)) center / 100% 5px no-repeat,
        linear-gradient(rgba(35, 35, 40, 0.55), rgba(35, 35, 40, 0.55)) center / 5px 100% no-repeat,
        linear-gradient(160deg, #c3e6ff, #6fa8d6) 10px 10px / calc(100% - 20px) calc(100% - 20px) no-repeat,
        var(--face-mid, #808080);
}
[data-shape="door"].cube-face-2d-front {
    background:
        radial-gradient(circle 4px at 74px 48px, #2b1c10, #2b1c10 4px, transparent 4.5px),
        linear-gradient(rgba(0, 0, 0, 0.22), rgba(0, 0, 0, 0.22)) 14px 12px / calc(100% - 28px) 34px no-repeat,
        linear-gradient(rgba(0, 0, 0, 0.22), rgba(0, 0, 0, 0.22)) 14px 54px / calc(100% - 28px) 24px no-repeat,
        linear-gradient(160deg, #8a5a34, #6b4423);
}