/* =========================================
   GLOBAL RESET & VARIABLES
   ========================================= */
:root {
    --bg-graph-line: #e0e0e0;
    --header-bg: #2c3e50;
    --wood-border: #b87b41;
    --wood-shadow: #8a572a;
    --tile-bg: #f4d399;
    --card-bg: #ffffff;
    --blue-header: #bde0fe;
    --row-highlight: #ffe066;
    --text-dark: #333333;
    
    /* Premium Square Colors */
    --tw-color: #ff4d4d;
    --dw-color: #ff9999;
    --tl-color: #3399ff;
    --dl-color: #99ccff;
}

* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

html {
    overflow-x: hidden;
    height: 100%;
    /* Lives here (inherited by body and everything below it), NOT on body
       itself - a direct declaration on an element always wins over an
       INHERITED value regardless of specificity, and cosmetics.js's
       applyCosmetics() sets [data-font="X"] on this same <html> element.
       [data-font="X"] (an attribute selector) outranks this plain-element
       rule on specificity, so a font cosmetic correctly overrides this
       default - but only because this default lives on the SAME element
       being overridden, not on a descendant the override can't out-rank. */
    font-family: 'Helvetica Neue', Arial, sans-serif;
}

/* Buttons/inputs/selects/textareas do NOT inherit the page's font-family by
   default in any browser - they use the OS's UI font instead, regardless of
   what's set on <html>/body/etc. Without this, a font cosmetic advertised as
   changing "type across the whole interface" would visibly change body text
   and headings but leave every button/dropdown in the browser's plain
   default font. */
button, input, select, textarea, optgroup {
    font: inherit;
}

body {
    background-color: #f8f9fa;
    background-image:
        linear-gradient(var(--bg-graph-line) 1px, transparent 1px),
        linear-gradient(90deg, var(--bg-graph-line) 1px, transparent 1px);
    background-size: 20px 20px;
    color: var(--text-dark);
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 100%;
    overflow-x: hidden;
}

/* =========================================
   HEADER & ADS
   ========================================= */
.site-header {
    width: 100%;
    background-color: var(--header-bg);
    color: white;
    padding: 10px 20px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    box-shadow: 0 2px 5px rgba(0,0,0,0.2);
    z-index: 10;
}

.logo {
    font-size: 1.5rem;
    font-weight: bold;
}

.header-info {
    font-size: 1rem;
    text-transform: uppercase;
    letter-spacing: 1px;
}

.ad-container {
    background-color: #1a365d;
    color: white;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    box-shadow: 0 4px 6px rgba(0,0,0,0.1);
    position: relative;
    /* AdSense's "auto"/full-width-responsive format picks whatever size it
       wants for the impression, and flexbox does not clip an oversized
       child by default - an ad wider than this container (e.g. a 600x280
       rectangle inside the 160px-wide sidebar slot) used to spill straight
       out over the board and action buttons, silently eating every click
       aimed at Submit/Clear/Shuffle/Swap. This guarantees the ad can never
       render (or capture clicks) outside its own designated box. */
    overflow: hidden;
}

.ad-label {
    position: absolute;
    top: -15px;
    font-size: 0.6rem;
    color: #666;
    background: #f8f9fa;
    padding: 2px 5px;
    border-radius: 3px;
    border: 1px solid #ccc;
}

/* Now that this ad lives below the action buttons inside .center-column
   (rather than spanning the full page above everything, like it used to),
   it's capped to the same width as the board/rack above it so it doesn't
   stretch wider than the play area and look misaligned. */
.horizontal-ad { width: 728px; max-width: 600px; height: 90px; margin: 10px 0; }
.vertical-ad { width: 160px; height: 250px; margin: 0 auto 15px auto; }

/* Desktop ad rails: bracket the whole 3-column layout on screens wide
   enough to hold them without cramping anything (see the min-width media
   query further down) - .ad-slot-left/.ad-slot-right reuse the existing
   .vertical-ad sizing/styling via the .ad-rail modifier below, they just
   live outside .game-layout now instead of inside the left/right sidebars. */
.page-shell {
    display: flex;
    flex-direction: row;
    align-items: flex-start;
    justify-content: center;
    width: 100%;
}
.ad-rail {
    display: none;
    margin: 20px 0 0 0;
    flex-shrink: 0;
}
/* .game-layout caps out at 1300px (250 + 1fr + 250, plus gaps) - a rail on
   each side (160px) plus breathing room only actually has room without
   cramping the game itself once the viewport clears that by a solid
   margin. Below this, both rails simply stay hidden (the base rule above). */
@media screen and (min-width: 1650px) {
    .ad-rail { display: flex; }
}

/* Capped Ad Cadence / Overnight Mode / Ad-Free Token: any one of these
   collapses the slot to nothing rather than just visually hiding it, so it
   doesn't leave a dead gap in the layout. */
.ad-container.ads-suppressed { display: none !important; }

/* =========================================
   MAIN 3-COLUMN LAYOUT
   ========================================= */
.game-layout {
    display: grid;
    grid-template-columns: 250px 1fr 250px;
    gap: 30px;
    width: 100%;
    max-width: 1300px;
    padding: 0 20px 40px 20px;
}

/* =========================================
   LEFT & RIGHT COLUMNS (Cards)
   ========================================= */
.hand-tracker-card, .leaderboard-card {
    background: var(--card-bg);
    border-radius: 8px;
    box-shadow: 0 4px 10px rgba(0,0,0,0.1);
    border: 1px solid #ddd;
    overflow: hidden;
    margin-bottom: 20px;
}

.tracker-header, .leaderboard-header {
    background: var(--blue-header);
    padding: 10px;
    text-align: center;
    font-weight: bold;
    border-bottom: 1px solid #ddd;
}

.tracker-row, .leaderboard-row {
    display: flex;
    justify-content: space-between;
    padding: 10px 15px;
    border-bottom: 1px solid #eee;
    font-size: 0.9rem;
}

.tracker-row.active, .leaderboard-row.highlight {
    background-color: var(--row-highlight);
    font-weight: bold;
}

.tracker-row.completed span:first-child { color: #2e7d32; }

.tracker-footer {
    display: flex;
    justify-content: space-between;
    padding: 10px 15px;
    font-weight: bold;
    background-color: #f9f9f9;
}

/* =========================================
   CENTER COLUMN (Board & Rack)
   ========================================= */
.center-column {
    display: flex;
    flex-direction: column;
    align-items: center;
}

.board-header {
    display: flex;
    justify-content: space-between;
    width: 100%;
    max-width: 600px;
    align-items: center;
    margin-bottom: 10px;
}

.ghost-pacing {
    font-size: 0.9rem;
    color: #666;
    background: white;
    padding: 5px 10px;
    border-radius: 20px;
    border: 1px solid #ddd;
    box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}

/* Persistent Rivalry Ghosting */
.rival-pace-metric { border-color: #e74c3c; color: #b3311d; font-weight: bold; }
.rival-pace-metric.rival-pace-pulse { animation: rivalPacePulse 500ms ease-out; }
@keyframes rivalPacePulse {
    0% { transform: scale(1); }
    40% { transform: scale(1.12); background: #fdecea; }
    100% { transform: scale(1); }
}

/* NEW: Live Score UI Visibility Toggle */
.live-score-visible {
    opacity: 1 !important;
}

.hidden { display: none !important; }

.board {
    display: grid;
    grid-template-columns: repeat(15, 1fr);
    grid-template-rows: repeat(15, 1fr);
    width: 100%;
    max-width: 600px;
    aspect-ratio: 1;
    /* Faint grain on the free default board's own play surface, same
       "every paid theme sets the full background/background-color shorthand
       at higher specificity" safety as the default tile above - equipped
       themes are completely unaffected. */
    background:
        repeating-linear-gradient(2deg, rgba(120,90,40,0.05) 0 2px, transparent 2px 8px),
        #e4dfd1;
    border: 12px solid var(--wood-border);
    border-radius: 8px;
    box-shadow: 0 10px 20px rgba(0,0,0,0.3), inset 0 0 10px rgba(0,0,0,0.2);
    padding: 2px;
    gap: 2px;
}

/* BOARD CELLS & MULTIPLIERS */
.cell {
    background-color: #f0ebd8;
    border: 1px solid #d3cbb5;
    display: flex;
    justify-content: center;
    align-items: center;
    position: relative;
    aspect-ratio: 1; 
}

.cell[data-multiplier]::before {
    content: attr(data-multiplier);
    position: absolute;
    font-size: 0.6rem;
    font-weight: bold;
    color: rgba(0, 0, 0, 0.4);
    z-index: 1;
}

/* Global Community Placement Heatmap: a ::after overlay (not box-shadow,
   which every board theme's own premium-square styling already uses and
   this would otherwise clobber) - z-index 0 keeps it below the multiplier
   label (z-index 1 above) and any placed tile (z-index 10), so both stay
   fully readable/usable with the heatmap toggled on. */
.cell[data-heat]::after {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    z-index: 0;
    background: rgba(231, 76, 60, 0.15);
}
.cell[data-heat="2"]::after { background: rgba(231, 76, 60, 0.32); }
.cell[data-heat="3"]::after { background: rgba(231, 76, 60, 0.48); }
.cell[data-heat="4"]::after { background: rgba(231, 76, 60, 0.64); }
.cell[data-heat="5"]::after { background: rgba(231, 76, 60, 0.8); }

.cell.center-star {
    background-color: #ff9f43 !important; 
}

.cell.center-star::before {
    content: '★';
    position: absolute;
    font-size: 1.5rem;
    color: white;
    z-index: 1;
}

/* =========================================
   ANIMATIONS & EFFECTS (FLASHY SCORES)
   ========================================= */
#floating-score-overlay {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    z-index: 500;
}

.floating-score {
    position: absolute;
    top: 40%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-size: 4rem;
    font-weight: bold;
    color: #ff9f43; 
    text-shadow: 3px 3px 0px #000, 0 0 20px rgba(255, 159, 67, 0.8);
    animation: scorePop 2s cubic-bezier(0.25, 1, 0.5, 1) forwards;
    pointer-events: none;
    z-index: 1000;
}

@keyframes scorePop {
    0% { opacity: 0; transform: translate(-50%, 20px) scale(0.5); }
    15% { opacity: 1; transform: translate(-50%, -30px) scale(1.2); }
    80% { opacity: 1; transform: translate(-50%, -60px) scale(1); }
    100% { opacity: 0; transform: translate(-50%, -100px) scale(0.8); }
}

/* =========================================
   TILES & RACK
   ========================================= */
.rack {
    display: flex;
    gap: 5px;
    background-color: var(--wood-shadow);
    padding: 10px;
    border-radius: 8px;
    margin-top: 20px;
    width: 100%;
    max-width: 450px;
    height: 70px;
    box-shadow: inset 0 5px 10px rgba(0,0,0,0.5), 0 5px 10px rgba(0,0,0,0.2);
    justify-content: center;
}

.tile {
    width: 100%;
    height: 100%;
    aspect-ratio: 1;
    /* A faint grain + corner sheen even on the free default tile - every
       PAID finish below sets the full `background` shorthand at higher
       specificity, which fully replaces this regardless, so equipped
       finishes are untouched by this. Without it the free tile was the
       only completely flat, textureless surface in the whole cosmetic set. */
    background:
        repeating-linear-gradient(3deg, rgba(120,80,30,0.06) 0 2px, transparent 2px 7px),
        linear-gradient(135deg, rgba(255,255,255,0.35) 0%, transparent 30%),
        var(--tile-bg);
    border-radius: 4px;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 1.8rem;
    font-weight: bold;
    color: #222;
    cursor: grab;
    box-shadow: 1px 3px 2px rgba(0,0,0,0.3), inset 1px 1px 1px rgba(255,255,255,0.6);
    position: relative;
    user-select: none;
    transition: transform 0.1s;
    z-index: 10;
    /* Let JS own touch gestures on tiles (drag-to-place) instead of the
       browser's default scroll/zoom handling stealing the gesture. */
    touch-action: none;
    -webkit-touch-callout: none;
}

.tile.locked {
    box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
    cursor: default;
}

.tile:active:not(.locked) {
    cursor: grabbing;
    transform: scale(1.05);
    box-shadow: 5px 10px 10px rgba(0,0,0,0.3);
    z-index: 100;
}

.tile::after {
    content: attr(data-points);
    position: absolute;
    bottom: 2px;
    right: 3px;
    font-size: 0.7rem;
    color: #444;
}

.rack .tile { width: 50px; height: 50px; }

/* THE BURN MECHANIC HIGHLIGHT */
.tile.burning {
    background-color: #ff6b6b;
    color: white;
    box-shadow: 0 0 10px #ff0000;
    transform: translateY(-5px);
}

/* NEW: THE OPTIMAL WORD FLASHING MECHANIC */
.tile.optimal-flash {
    background-color: #27ae60 !important;
    color: white;
    border: 2px solid #2ecc71;
    animation: pulseGreen 1s infinite alternate;
}

@keyframes pulseGreen {
    from { box-shadow: 0 0 5px #27ae60; transform: scale(1); }
    to { box-shadow: 0 0 15px #2ecc71; transform: scale(1.05); }
}

/* =========================================
   BUTTONS
   ========================================= */
.action-buttons { display: flex; gap: 15px; margin-top: 20px; }

.btn {
    padding: 10px 25px;
    font-size: 1rem;
    font-weight: bold;
    border: none;
    border-radius: 25px;
    color: white;
    cursor: pointer;
    box-shadow: 0 4px 6px rgba(0,0,0,0.2);
    transition: transform 0.1s, box-shadow 0.1s;
}

.btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 6px 12px rgba(0,0,0,0.25); }
.btn:active { transform: translateY(2px); box-shadow: 0 2px 3px rgba(0,0,0,0.2); }
.btn:disabled { background-color: #95a5a6 !important; cursor: not-allowed; }

.btn-green { background-color: #27ae60; }
.btn-red { background-color: #e74c3c; }
.btn-blue { background-color: #3498db; }
.btn-purple { background-color: #8e44ad; } 
.btn-oauth { background-color: #4285F4; width: 100%; margin-top: 15px; }

#btn-rewarded-optimal, #btn-rewarded-sandbox {
    width: 100%;
    margin-top: 10px;
}

/* =========================================
   OVERLAYS & MODALS
   ========================================= */
.overlay-container {
    position: fixed;
    top: 0; left: 0; width: 100%; height: 100%;
    background: rgba(0,0,0,0.7);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 1000;
    /* Content taller than the viewport (e.g. the item shop on a phone) must
       still be reachable - without this, align-items:center clips both the
       top AND bottom of tall modals with no way to scroll to the rest. */
    overflow-y: auto;
    padding: 20px 0;
    box-sizing: border-box;
}

/* Ensure Custom Alert is above everything */
#custom-alert-overlay {
    z-index: 1100;
}

.tutorial-box, .modal-box {
    /* Anchors .modal-close-x's position:absolute to THIS box - without it,
       an absolutely-positioned child falls back to the nearest positioned
       ancestor, which is .overlay-container (position:fixed, covers the
       whole viewport), landing the X in the corner of the SCREEN instead of
       the corner of the actual card. .wide-modal already sets this
       redundantly for Store/Wardrobe/Clan/Duels; Settings and Recap only
       use plain .modal-box, which is what was missing it. */
    position: relative;
    background: white;
    padding: 30px;
    border-radius: 12px;
    max-width: 450px;
    width: 90%;
    text-align: center;
    box-shadow: 0 10px 30px rgba(0,0,0,0.3);
    /* Belt-and-suspenders with the scrollable overlay above: even very tall
       modal content (long item lists, etc.) stays reachable via its own
       internal scroll rather than ever growing past the viewport. */
    max-height: calc(100vh - 40px);
    overflow-y: auto;
    margin: auto;
    flex-shrink: 0;
}

.tutorial-box h3, .modal-box h2 { margin-bottom: 20px; color: var(--header-bg); }
.tutorial-box p { text-align: left; margin-bottom: 15px; line-height: 1.5; }
.final-score-text { font-size: 1.5rem; font-weight: bold; margin-bottom: 20px; }

/* "One-Tile" Regret Engine */
.regret-swap-box {
    background: linear-gradient(135deg, #fff5e6, #ffe8cc);
    border: 1px solid #f0b862;
    border-radius: 10px;
    padding: 12px 14px;
    margin-bottom: 18px;
    text-align: left;
}
.regret-swap-label { font-weight: bold; color: #b3660a; font-size: 0.85rem; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
.regret-swap-text { font-size: 0.85rem; color: #5c4a2e; line-height: 1.5; }
.regret-swap-text b { color: #b3660a; }

.interstitial-ad { width: 300px; height: 250px; margin: 0 auto 20px auto; }
hr { margin: 20px 0; border: none; border-top: 1px solid #ddd; }

/* =========================================
   NEW: COSMETICS & LOBBY CONTROLS
   ========================================= */
.cosmetic-tracker-card {
    background: var(--card-bg);
    border-radius: 8px;
    box-shadow: 0 4px 10px rgba(0,0,0,0.1);
    border: 1px solid #ddd;
    overflow: hidden;
    margin-bottom: 20px;
}

.cosmetic-header {
    background: #9b59b6;
    color: white;
    padding: 10px;
    text-align: center;
    font-weight: bold;
}

.cosmetic-body {
    padding: 15px;
}

.cosmetic-text {
    font-size: 0.85rem;
    margin-bottom: 5px;
    text-align: center;
}

.progress-bar-container {
    width: 100%;
    height: 15px;
    background-color: #eee;
    border-radius: 10px;
    overflow: hidden;
    margin-bottom: 5px;
    border: 1px solid #ccc;
}

.progress-bar-fill {
    height: 100%;
    background-color: #8e44ad;
    transition: width 0.5s ease-out;
}

/* Parallel Achievement Pass Progression: amber/gold, deliberately distinct
   from Monthly Prestige's purple - this is a SEPARATE track (tiles placed,
   not score), and sharing the same color would visually imply they're the
   same progress bar. */
.achievement-pass-header { background: #d68910; }
.achievement-pass-fill { background-color: #f39c12; }

/* Achievement Pass title badge, shown next to a username wherever that
   entry's flair also shows (leaderboard rows, shop/wardrobe previews). */
.title-badge {
    display: inline-block;
    background: linear-gradient(135deg, #fff3c4, #f4c430);
    color: #5c4300;
    font-size: 0.7rem;
    font-weight: bold;
    padding: 1px 6px;
    border-radius: 8px;
    border: 1px solid #d68910;
    white-space: nowrap;
}

/* 8-Bit Champion: the one PURCHASED title (see shop_items/titles.py) among
   otherwise-free achievement badges - it earns its own distinct look
   instead of sharing the plain gold pill every achievement title gets.
   Same CRT language as the matching board/tile: near-black glass, sharp
   (non-blurred) pixel-stepped border instead of a soft radius, and real
   phosphor-green glow via Press Start 2P, already loaded for font_pixel. */
.title-badge[data-title="title_crt"] {
    background: #0a0f0a;
    color: #39ff14;
    font-family: 'Press Start 2P', 'Courier New', monospace;
    font-size: 0.6rem;
    letter-spacing: 0.5px;
    padding: 3px 7px;
    border-radius: 3px;
    border: 1px solid #39ff14;
    box-shadow: 0 0 4px #39ff14, 0 0 9px rgba(57,255,20,0.6), inset 0 0 4px rgba(57,255,20,0.35);
    text-shadow: 0 0 4px #39ff14, 0 0 9px rgba(57,255,20,0.7);
    animation: crtBadgeFlicker 3.2s ease-in-out infinite;
}
@keyframes crtBadgeFlicker {
    0%, 96%, 100% { opacity: 1; }
    97% { opacity: 0.7; }
    98% { opacity: 1; }
    99% { opacity: 0.85; }
}
/* At its normal (leaderboard-context) size this badge measured ~181px wide
   - comfortably fine next to a username in a leaderboard row, but the
   shop/wardrobe preview box is only 136px wide total. Shrunk specifically
   in that context rather than shrinking the badge everywhere. */
.shop-item-preview .title-badge[data-title="title_crt"] {
    font-size: 0.42rem;
    padding: 2px 5px;
    letter-spacing: 0;
}

/* Tilt Master: a polished-chrome plaque (same steel gradient family as the
   Chrome Rimmed Tiles finish) with bold backglass-red lettering - the
   exact look of a pinball machine's lit scoreboard/high-score plate, and
   the same DW-bumper red (#ff2d3c) as the matching board for a set that
   visibly reads as one family. flashTiltMaster is a single quick double-
   blink (not a slow loop like the CRT badge's flicker) - a scoreboard
   lighting up on a big score, not a screen artifact. */
.title-badge[data-title="title_pinball"] {
    background: linear-gradient(160deg, #eef1f6 0%, #9aa2ae 30%, #4a5058 65%, #1c1e22 100%);
    color: #ff2d3c;
    font-weight: 900;
    letter-spacing: 0.3px;
    padding: 2px 7px;
    border-radius: 4px;
    border: 1px solid #f0f3f8;
    box-shadow: inset 0 1px 2px rgba(255,255,255,0.7), inset 0 -2px 3px rgba(0,0,0,0.4), 0 0 5px rgba(255,45,60,0.4);
    text-shadow: 0 0 4px rgba(255,45,60,0.85), 0 1px 0 rgba(0,0,0,0.5);
    animation: tiltMasterFlash 4.5s ease-in-out infinite;
}
@keyframes tiltMasterFlash {
    0%, 90%, 100% { text-shadow: 0 0 4px rgba(255,45,60,0.85), 0 1px 0 rgba(0,0,0,0.5); }
    92% { text-shadow: 0 0 9px rgba(255,45,60,1), 0 0 4px #fff, 0 1px 0 rgba(0,0,0,0.5); }
    94% { text-shadow: 0 0 4px rgba(255,45,60,0.85), 0 1px 0 rgba(0,0,0,0.5); }
    96% { text-shadow: 0 0 9px rgba(255,45,60,1), 0 0 4px #fff, 0 1px 0 rgba(0,0,0,0.5); }
    98% { text-shadow: 0 0 4px rgba(255,45,60,0.85), 0 1px 0 rgba(0,0,0,0.5); }
}
.shop-item-preview .title-badge[data-title="title_pinball"] {
    font-size: 0.68rem;
    padding: 2px 6px;
}

/* Caffeinated Wordsmith: a warm cream card-stock badge with an espresso-
   ink Caveat script, like a name handwritten on a coffee sleeve - the
   longest title name in this shop (21 characters), so the preview-context
   size below is tuned tighter than the CRT/pinball badges needed. */
.title-badge[data-title="title_coffee"] {
    background: linear-gradient(160deg, #f7ecda 0%, #e8d4b0 100%);
    color: #4a2c17;
    font-family: 'Caveat', cursive;
    font-weight: 700;
    font-size: 1rem;
    letter-spacing: 0.2px;
    padding: 0px 8px 2px;
    border-radius: 6px;
    border: 1px solid #b5895a;
    box-shadow: inset 0 1px 2px rgba(255,255,255,0.6), inset 0 -2px 3px rgba(139,94,52,0.2), 0 0 4px rgba(184,134,74,0.35);
    text-shadow: 0 1px 0 rgba(255,255,255,0.4);
}
.shop-item-preview .title-badge[data-title="title_coffee"] {
    font-size: 0.62rem;
    padding: 0px 5px 1px;
}

.lobby-controls {
    padding: 10px 15px;
    background-color: #f9f9f9;
    border-top: 1px solid #eee;
}

.lobby-controls input {
    width: 100%;
    padding: 8px;
    border: 1px solid #ccc;
    border-radius: 4px;
    font-size: 0.9rem;
}

/* =========================================
   RESPONSIVE DESIGN (Tablets & Mobile)
   ========================================= */
@media screen and (max-width: 1050px) {
    .game-layout {
        display: flex;
        flex-direction: column;
        padding: 15px;
        gap: 20px;
    }

    .center-column {
        order: -1;
        width: 100%;
    }

    .left-column, .right-column {
        width: 100%;
        max-width: 600px;
        margin: 0 auto;
    }

    .horizontal-ad {
        width: 100%;
        max-width: 728px;
    }

    .vertical-ad {
        width: 100%;
        max-width: 600px;
        height: 100px; 
    }
}

@media screen and (max-width: 600px) {
    /* Keep the header a single compact row instead of stacking to a column -
       stacking was eating ~60px of vertical space that pushed the rack
       below the fold on common phone screen heights (e.g. 375x812). */
    .site-header {
        flex-wrap: wrap;
        gap: 4px 10px;
        padding: 6px 12px;
        justify-content: center;
    }
    .logo { font-size: 1.1rem; }
    .header-info { font-size: 0.75rem; }
    /* .hub-btn, .streak-countdown, .community-goal-bar and
       .community-goal-label overrides used to live here, but the
       HEADER HUB / COMMUNITY GOAL BAR base rules further down the file
       declare those same selectors at equal specificity, so being later
       in source order those base rules always won the cascade tie and
       silently cancelled these overrides on every mobile viewport. Moved
       to a dedicated trailing block right after those base sections
       instead - see "MOBILE OVERRIDES (must stay after HEADER HUB /
       COMMUNITY GOAL BAR base rules)" below. */

    /* AdSense responsive units adapt to whatever height the container
       allows - a slim mobile banner (320x50 class) instead of the desktop
       leaderboard size (728x90) reclaims real gameplay space on phones. */
    .horizontal-ad { height: 50px; margin: 6px 0; }
    .vertical-ad { height: 90px; margin-bottom: 10px; }

    /* This is the actual fix for "the board feels tiny to play on" - the
       board's width is 100% of .game-layout minus its padding, so shaving
       padding down here is what actually grows every cell and makes tile
       dragging/dropping precise instead of fiddly on a phone screen. */
    .game-layout {
        padding: 6px;
        gap: 12px;
    }

    .board-header {
        flex-wrap: wrap;
        gap: 4px 10px;
        margin-bottom: 6px;
    }
    .board-header h2 { font-size: 1.1rem; }

    .tile {
        font-size: 1.5rem;
    }

    .tile::after {
        font-size: 0.55rem;
    }

    /* Rack tiles specifically are what a thumb actually has to grab and
       drag - Apple/Google's ~44px touch-target guideline is the floor here,
       not just a nice-to-have, given the previous 40px (and 34px below
       400px) sizing was genuinely fiddly to pick up precisely. */
    .rack {
        height: 68px;
        padding: 6px;
        margin-top: 12px;
        max-width: 100%;
    }

    .rack .tile {
        width: 52px;
        height: 52px;
    }

    .action-buttons {
        width: 100%;
        justify-content: center;
        flex-wrap: wrap;
        gap: 10px;
        margin-top: 14px;
    }

    #already-played-banner {
        width: 100%;
        max-width: 100%;
        padding: 20px !important;
    }

    .btn {
        padding: 12px 18px;
        font-size: 0.95rem;
    }
}

@media screen and (max-width: 400px) {
    .game-layout { padding: 4px; }

    /* Still comfortably above the ~44px touch-target guideline even on the
       narrowest common phones, rather than the previous 34px (below it). */
    .rack .tile {
        width: 46px;
        height: 46px;
    }

    .tile {
        font-size: 1.3rem;
    }

    .cosmetic-text, .tracker-row, .leaderboard-row {
        font-size: 0.8rem;
    }
}

/* =========================================
   HEADER HUB & MAINTENANCE BANNER
   ========================================= */
.header-hub {
    display: flex;
    align-items: center;
    gap: 8px;
}

.hub-btn {
    background: rgba(255,255,255,0.15);
    border: none;
    color: white;
    font-size: 1.1rem;
    padding: 6px 10px;
    border-radius: 8px;
    cursor: pointer;
}
.hub-btn:hover { background: rgba(255,255,255,0.3); }

.streak-countdown {
    font-size: 0.75rem;
    color: #ffe066;
    font-weight: bold;
}
.streak-countdown.streak-urgent-glow {
    animation: urgentPulse 1.5s infinite;
}
@keyframes urgentPulse {
    0%, 100% { text-shadow: 0 0 4px #ff4d4d; }
    50% { text-shadow: 0 0 12px #ff4d4d; }
}

#maintenance-banner {
    width: 100%;
    background: #b91c1c;
    color: white;
    text-align: center;
    padding: 8px;
    font-size: 0.85rem;
    font-weight: bold;
}

/* =========================================
   COMMUNITY GOAL BAR
   ========================================= */
.community-goal-bar {
    width: 100%;
    max-width: 1300px;
    padding: 8px 20px;
}
.community-goal-label { font-size: 0.8rem; margin-bottom: 4px; text-align: center; }
.community-goal-track {
    width: 100%;
    height: 10px;
    background: #eee;
    border-radius: 6px;
    overflow: hidden;
    border: 1px solid #ccc;
}
.community-goal-fill {
    height: 100%;
    background: linear-gradient(90deg, #3498db, #8e44ad);
    width: 0%;
    transition: width 0.6s ease-out;
}

/* =========================================
   MOBILE OVERRIDES (must stay after HEADER HUB / COMMUNITY GOAL BAR base
   rules): .hub-btn, .streak-countdown, .community-goal-bar and
   .community-goal-label all share the exact same specificity as their
   base-rule counterparts above, and CSS resolves equal-specificity ties by
   source order - so this block has to come after them to actually apply
   on mobile widths instead of being silently overridden.
   ========================================= */
@media screen and (max-width: 600px) {
    .hub-btn { padding: 4px 8px; font-size: 0.95rem; }
    .streak-countdown { font-size: 0.65rem; }
    .community-goal-bar { padding: 4px 12px; }
    .community-goal-label { font-size: 0.7rem; margin-bottom: 2px; }
}

/* =========================================
   WIDE MODALS (Store / Wardrobe / Clan)
   ========================================= */
.wide-modal { max-width: 700px !important; text-align: left; position: relative; }
.modal-close-x {
    position: absolute; top: 15px; right: 20px; background: none; border: none;
    font-size: 1.5rem; font-weight: bold; cursor: pointer; color: #95a5a6;
}

.store-category { margin-bottom: 16px; }
.store-category h4 { color: #8e44ad; margin-bottom: 8px; font-size: 0.85rem; }
.store-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }
.store-item-card {
    background: #f9f9f9; border: 1px solid #ddd; border-radius: 8px; padding: 10px; text-align: center;
}
.store-item-name { font-weight: bold; font-size: 0.85rem; margin-bottom: 4px; }
.store-item-price { font-size: 0.75rem; color: #666; margin-bottom: 8px; }
.store-owned-badge { background: #27ae60; color: white; padding: 3px 8px; border-radius: 10px; font-size: 0.7rem; }
.store-buy-btn { width: 100%; padding: 6px; font-size: 0.8rem; }

.badge-chip {
    display: inline-block; background: #fff3cd; border: 1px solid #ffe066; border-radius: 20px;
    padding: 5px 12px; margin: 3px; font-size: 0.8rem;
}

.share-preview {
    background: #f5f5f5; border-radius: 8px; padding: 12px; margin-top: 10px;
    font-family: monospace; white-space: pre-wrap; font-size: 0.85rem; text-align: center;
}

/* Annual "Year in Review" Data Recap: deliberately "splashy" (gradient,
   glow, bold stat tiles) - the whole point is to look good in a screenshot
   someone shares, not to match the rest of the app's utilitarian modal look. */
.recap-card {
    background: linear-gradient(160deg, #2c1a4d 0%, #4a2a7a 55%, #6a3aa0 100%);
    color: #fff;
    border-radius: 14px;
    padding: 20px;
    box-shadow: 0 0 30px rgba(123, 58, 191, 0.5);
}
.recap-card-header { text-align: center; margin-bottom: 16px; }
.recap-card-title {
    font-size: 1.3rem;
    font-weight: bold;
    letter-spacing: 1px;
    text-shadow: 0 0 12px rgba(255, 224, 130, 0.7);
    color: #ffe082;
}
.recap-card-username { font-size: 0.9rem; color: #d8c6f0; margin-top: 4px; }
.recap-stat-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 10px;
}
.recap-stat {
    background: rgba(255, 255, 255, 0.08);
    border: 1px solid rgba(255, 255, 255, 0.15);
    border-radius: 10px;
    padding: 10px;
    text-align: center;
}
.recap-stat-value { font-size: 1.3rem; font-weight: bold; color: #ffe082; }
.recap-stat-label { font-size: 0.65rem; color: #d8c6f0; text-transform: uppercase; letter-spacing: 0.5px; margin-top: 2px; }
.recap-favorite-word {
    text-align: center;
    margin-top: 14px;
    font-size: 0.9rem;
    color: #d8c6f0;
}
.recap-favorite-word b { color: #ffe082; }

/* =========================================
   COSMETIC THEMES
   Every selector is SELF-scoped (attribute lives directly on the element
   it styles: .board[data-board-theme], .center-column[data-tile-finish])
   rather than a global-descendant pattern like [data-x] .board. That
   matters because cosmetics.js sets these attributes on real, specific
   elements (#game-board, .center-column) - NOT on <html> - so a shop
   preview card for theme A never leaks the ancestor's REAL equipped theme
   B onto itself just because both happen to share <html> as an ancestor.
   Each preview wrapper carries its own independent attribute and is
   completely isolated from whatever the player has actually equipped.
   ========================================= */

/* --- Tile Finishes: live scope is .center-column (wraps board + rack),
   preview scope is the shop card's own .shop-item-preview wrapper.
   Deliberately NOT excluding .locked tiles (only .burning) - every placed
   tile gets .locked the instant its word is accepted, which is the
   PERMANENT state a tile spends the rest of the game in. An earlier
   version excluded .locked here too, which meant every tile finish and
   rare-letter emboss visibly reverted to the plain default tile the moment
   a word was submitted - the cosmetic was only ever visible for the few
   seconds before locking. Cosmetic selectors here already out-specificity
   .tile.locked's own box-shadow (more classes/attributes in the selector),
   so nothing about the locked dimming effect is lost by including it. --- */
.center-column[data-tile-finish="tile_obsidian"] .tile:not(.burning),
.shop-item-preview[data-tile-finish="tile_obsidian"] .tile {
    background:
        /* "Snowflake obsidian" mineral inclusions - real volcanic glass
           often carries tiny pale cristobalite flecks, not a perfectly
           uniform matte black. */
        radial-gradient(1px 1px at 30% 70%, rgba(220,210,255,0.55), transparent),
        radial-gradient(0.8px 0.8px at 70% 35%, rgba(220,210,255,0.4), transparent),
        radial-gradient(0.7px 0.7px at 55% 80%, rgba(220,210,255,0.35), transparent),
        linear-gradient(115deg, transparent 32%, rgba(255,255,255,0.22) 47%, rgba(150,110,255,0.16) 53%, transparent 68%),
        linear-gradient(145deg, #333333 0%, #0a0a0a 55%, #1a1024 100%);
    color: #f0f0f0;
    box-shadow: 1px 3px 2px rgba(0,0,0,0.5), inset 0 0 10px rgba(140,100,220,0.25);
}

/* --- Neon-Glowing Letters: glossy black glass base (distinct from Obsidian's
   MATTE black - this one has a sharp specular highlight sweep) with a glow
   that continuously cycles through every color, the same technique as the
   board_neon theme's rotating ring: an @property-registered custom value
   gets animated, and both the pulse keyframes AND the base rule read its
   CURRENT value via var() every frame, so the glow's color keeps shifting
   independently of its pulse. --- */
@property --tile-glow-hue {
    syntax: '<number>';
    inherits: false;
    initial-value: 280;
}
.center-column[data-tile-finish="tile_glow"] .tile:not(.burning),
.shop-item-preview[data-tile-finish="tile_glow"] .tile {
    background:
        linear-gradient(115deg, transparent 42%, rgba(255,255,255,0.55) 48%, transparent 54%),
        linear-gradient(120deg, rgba(255,255,255,0.32) 4%, transparent 22%, transparent 74%, rgba(255,255,255,0.14) 94%),
        linear-gradient(160deg, #202024 0%, #0a0a0c 55%, #050506 100%);
    background-size: 250% 250%, 100% 100%, 100% 100%;
    background-position: 0% 0%, 0 0, 0 0;
    background-repeat: no-repeat;
    color: #ffffff;
    text-shadow: 0 0 5px rgba(255,255,255,0.75);
    animation: tileGlowHueShift 6s linear infinite, tileGlowPulse 2.4s ease-in-out infinite, tileGlassSweep 3.5s ease-in-out infinite;
}
@keyframes tileGlowHueShift {
    to { --tile-glow-hue: 640; }
}
/* A genuine light sweep across the glass surface - the same "premium reveal"
   beat loot-box/legendary-card UIs use (Overwatch's skin unlock screen,
   CS:GO's case-opening glint) - not just a static diagonal highlight. */
@keyframes tileGlassSweep {
    0%, 15% { background-position: -120% -120%, 0 0, 0 0; }
    55%, 100% { background-position: 120% 120%, 0 0, 0 0; }
}
@keyframes tileGlowPulse {
    0%, 100% { box-shadow: 1px 3px 2px rgba(0,0,0,0.5), 0 0 6px 1px hsl(var(--tile-glow-hue) 100% 60% / 0.55); }
    50% { box-shadow: 1px 3px 2px rgba(0,0,0,0.5), 0 0 18px 5px hsl(var(--tile-glow-hue) 100% 65% / 0.9); }
}

/* --- Rose Gold Cascade: real rose gold reference values (canonical rose
   gold #B76E79, its lighter blush highlight #D8ABB1/#F6DFE1, and its darker
   shadow #A85D6E). A fine repeating micro-streak layer runs across the
   whole tile at the SAME angle as the diagonal sheen - real brushed metal
   isn't a smooth gradient, it's thousands of parallel abrasion lines that
   catch light unevenly, which is what actually reads as "metal" instead of
   "painted pink plastic". --- */
.center-column[data-tile-finish="tile_rosegold"] .tile:not(.burning),
.shop-item-preview[data-tile-finish="tile_rosegold"] .tile {
    background:
        repeating-linear-gradient(120deg, rgba(255,255,255,0.12) 0 0.5px, transparent 0.5px 2px),
        linear-gradient(120deg, rgba(255,255,255,0.6) 6%, transparent 24%, transparent 68%, rgba(255,255,255,0.3) 90%),
        linear-gradient(145deg, #F6DFE1 0%, #D8ABB1 32%, #B76E79 62%, #A85D6E 100%);
    color: #4a2530;
    text-shadow: 0 1px 0 rgba(255,255,255,0.45);
    box-shadow: 1px 3px 2px rgba(0,0,0,0.3), inset 0 0 6px rgba(255,255,255,0.55), 0 0 6px rgba(183,110,121,0.4);
}

/* --- Carrara Marble: creamy white base (#F3F4EF, the real Carrara White
   reference) with irregular soft blue-grey vein streaks at varying angles -
   real Carrara's veins run "irregularly" rather than in a repeating
   pattern, so each layer uses a different angle/position instead of one
   repeating-gradient. Two thinner "branch" veins split off the main streaks
   at shallower angles (real marble veining forks rather than running as
   clean parallel lines), plus a soft polish sheen for that ground-and-
   buffed stone look rather than raw rock. --- */
.center-column[data-tile-finish="tile_marble"] .tile:not(.burning),
.shop-item-preview[data-tile-finish="tile_marble"] .tile {
    background:
        linear-gradient(115deg, transparent 22%, rgba(255,255,255,0.5) 26%, transparent 30%),
        linear-gradient(112deg, transparent 24%, rgba(150,155,158,0.3) 25.5%, transparent 27%),
        linear-gradient(105deg, transparent 21%, rgba(139,145,150,0.55) 23%, transparent 26%),
        linear-gradient(19deg, transparent 58%, rgba(150,155,158,0.28) 59.5%, transparent 61%),
        linear-gradient(15deg, transparent 54%, rgba(168,172,174,0.4) 57%, transparent 61%),
        linear-gradient(82deg, transparent 71%, rgba(150,155,158,0.25) 72.5%, transparent 74%),
        linear-gradient(78deg, transparent 67%, rgba(139,145,150,0.35) 70%, transparent 74%),
        linear-gradient(160deg, #ffffff 0%, #F3F4EF 55%, #e7e6df 100%);
    color: #3a3a38;
    box-shadow: 1px 3px 2px rgba(0,0,0,0.25), inset 0 0 8px rgba(255,255,255,0.6);
}

/* --- Black Walnut Burl: real black-walnut heartwood range (#774E37 down
   to #31241A), a faint burl swirl (off-center radial highlight, mimicking
   the wavy figure that gives burl wood its name) plus fine angled grain
   lines. --- */
.center-column[data-tile-finish="tile_walnut"] .tile:not(.burning),
.shop-item-preview[data-tile-finish="tile_walnut"] .tile {
    background:
        /* A soft satin polish highlight - real furniture-grade walnut is
           lacquered/oiled, never left raw, so it always carries a gentle
           sheen on top of the grain. */
        linear-gradient(120deg, rgba(255,220,180,0.16) 8%, transparent 26%),
        radial-gradient(ellipse at 32% 38%, rgba(119,78,55,0.55), transparent 55%),
        repeating-linear-gradient(4deg, rgba(0,0,0,0.16) 0 2px, transparent 2px 6px),
        linear-gradient(160deg, #774E37 0%, #56301d 45%, #31241A 100%);
    color: #ecd9c3;
    box-shadow: 1px 3px 2px rgba(0,0,0,0.5), inset 0 0 6px rgba(0,0,0,0.4);
}

/* --- Stardust (matches Celestial Nebula board): deep space-black glass
   scattered with tiny star specks (small radial-gradients at varied sizes/
   positions, the same technique real CSS starfields use) over soft nebula
   cloud color pooling (magenta #B3298E and teal #2EC4B6, the same hues the
   Celestial Nebula board's clouds use, so the set reads as one family). --- */
.center-column[data-tile-finish="tile_nebula"] .tile:not(.burning),
.shop-item-preview[data-tile-finish="tile_nebula"] .tile {
    background:
        radial-gradient(1.4px 1.4px at 18% 22%, #fff, transparent),
        radial-gradient(1px 1px at 68% 18%, #fff, transparent),
        radial-gradient(1.4px 1.4px at 82% 62%, #e0d4ff, transparent),
        radial-gradient(1px 1px at 32% 78%, #fff, transparent),
        radial-gradient(1px 1px at 52% 48%, #cbb8ff, transparent),
        radial-gradient(0.8px 0.8px at 12% 55%, rgba(255,255,255,0.6), transparent),
        radial-gradient(0.8px 0.8px at 90% 40%, rgba(255,255,255,0.6), transparent),
        radial-gradient(ellipse at 28% 72%, rgba(179,41,142,0.4), transparent 55%),
        radial-gradient(ellipse at 76% 28%, rgba(46,196,182,0.3), transparent 50%),
        linear-gradient(150deg, #2a1245 0%, #150a2e 55%, #0a0518 100%);
    color: #f0e8ff;
    text-shadow: 0 0 6px rgba(203,184,255,0.85);
    box-shadow: 1px 3px 2px rgba(0,0,0,0.5), inset 0 0 8px rgba(179,41,142,0.3), 0 0 6px rgba(46,196,182,0.3);
    animation: tileStarTwinkle 3.5s ease-in-out infinite;
}
@keyframes tileStarTwinkle {
    0%, 100% { filter: brightness(1); }
    50% { filter: brightness(1.18); }
}

/* --- Sapphire & Gold (matches Royal Sapphire & Gold board): real sapphire
   gemstone blue (#0F52BA, the standard reference for the gem) with a
   faceted diagonal sheen (mimicking how a gem-cut surface catches light at
   an angle) and a gold rim (#D4AF37, standard "metallic gold") instead of
   the usual plain border. --- */
.center-column[data-tile-finish="tile_sapphire"] .tile:not(.burning),
.shop-item-preview[data-tile-finish="tile_sapphire"] .tile {
    background:
        linear-gradient(115deg, transparent 18%, rgba(255,255,255,0.55) 26%, transparent 34%),
        linear-gradient(65deg, transparent 58%, rgba(255,255,255,0.28) 65%, transparent 73%),
        linear-gradient(120deg, transparent 26%, rgba(255,255,255,0.42) 38%, transparent 50%, transparent 68%, rgba(255,255,255,0.16) 84%),
        linear-gradient(155deg, #3a8fe0 0%, #0F52BA 42%, #0a3a85 75%, #062554 100%);
    color: #ffe9a8;
    text-shadow: 0 1px 0 rgba(0,0,0,0.4);
    border: 2px solid #d4af37;
    box-shadow: 1px 3px 2px rgba(0,0,0,0.4), inset 0 0 6px rgba(255,255,255,0.3), 0 0 6px rgba(212,175,55,0.5);
    animation: sapphireFacetGleam 3.2s ease-in-out infinite;
}
@keyframes sapphireFacetGleam {
    0%, 100% { box-shadow: 1px 3px 2px rgba(0,0,0,0.4), inset 0 0 6px rgba(255,255,255,0.3), 0 0 6px rgba(212,175,55,0.5); }
    50% { box-shadow: 1px 3px 2px rgba(0,0,0,0.4), inset 0 0 8px rgba(255,255,255,0.45), 0 0 9px rgba(212,175,55,0.7); }
}

/* --- Molten Obsidian (matches Molten Obsidian board): true obsidian black
   volcanic glass (#0b0a09-ish base) with a single glowing lava-crack sheen
   running through it (real molten-rock orange #FF4500/#FFAA00) and a slow
   pulsing glow, like a fissure of magma still cooling. --- */
.center-column[data-tile-finish="tile_molten"] .tile:not(.burning),
.shop-item-preview[data-tile-finish="tile_molten"] .tile {
    background:
        linear-gradient(115deg, transparent 40%, rgba(255,107,0,0.65) 47%, rgba(255,170,0,0.85) 50%, rgba(255,107,0,0.65) 53%, transparent 60%),
        linear-gradient(25deg, transparent 68%, rgba(255,69,0,0.5) 73%, rgba(255,140,0,0.65) 75%, rgba(255,69,0,0.5) 77%, transparent 82%),
        linear-gradient(150deg, #232022 0%, #0f0d0e 55%, #050405 100%);
    color: #ffd8b0;
    text-shadow: 0 0 6px rgba(255,107,0,0.7);
    animation: moltenTilePulse 2.6s ease-in-out infinite;
}
@keyframes moltenTilePulse {
    0%, 100% { box-shadow: 1px 3px 2px rgba(0,0,0,0.5), inset 0 0 4px rgba(255,107,0,0.35), 0 0 5px rgba(255,69,0,0.45); }
    50% { box-shadow: 1px 3px 2px rgba(0,0,0,0.5), inset 0 0 8px rgba(255,107,0,0.55), 0 0 12px rgba(255,69,0,0.75); }
}

/* --- 8-Bit Pixel Wood (matches the CRT Grid Terminal board): real honey-
   oak wood grain - warm and golden (#C68A3D range), lighter and more amber
   than Black Walnut's deep chocolate-brown, so the two "wood" finishes
   stay clearly distinct - overlaid with two things a real CRT does to
   whatever's behind its glass: a fine pixel dither grid (paired
   repeating-gradients at true single-digit-pixel scale, not a big blocky
   checker) and horizontal scanline GROOVES - each line pairs a dark top
   edge with a lighter bottom edge, the actual light/shadow pair a carved
   etch throws, not a flat tint band. The letter itself renders in Press
   Start 2P with a phosphor-green glow, the same signature color as the
   matching board's premium squares and the 8-Bit Champion title, so the
   whole set reads as one family instead of three separate items. --- */
.center-column[data-tile-finish="tile_crt"] .tile:not(.burning),
.shop-item-preview[data-tile-finish="tile_crt"] .tile {
    background:
        /* etched horizontal scanline grooves */
        repeating-linear-gradient(0deg, rgba(0,0,0,0.22) 0 1px, rgba(255,255,255,0.05) 1px 2px, transparent 2px 3px),
        /* fine pixel dither grid */
        repeating-linear-gradient(90deg, rgba(0,0,0,0.05) 0 1px, transparent 1px 3px),
        repeating-linear-gradient(0deg, rgba(0,0,0,0.05) 0 1px, transparent 1px 3px),
        /* satin sheen + honey-oak grain, same layering technique as the
           other wood/metal finishes in this shop */
        linear-gradient(120deg, rgba(255,220,150,0.22) 6%, transparent 26%),
        repeating-linear-gradient(3deg, rgba(0,0,0,0.14) 0 2px, transparent 2px 6px),
        linear-gradient(160deg, #C68A3D 0%, #9c6626 45%, #5c3a1a 100%);
    color: #eafff0;
    text-shadow: 0 0 4px #39ff14, 0 0 9px rgba(57,255,20,0.65);
    /* Press Start 2P renders noticeably wider per-glyph than the tile's
       default font at the same nominal size (the same reason font_pixel
       tightens letter-spacing globally) - sized down from the base 1.8rem
       so a single letter stays comfortably inside the tile instead of
       crowding its edges. */
    font-family: 'Press Start 2P', 'Courier New', monospace;
    font-size: 1.3rem;
    box-shadow: 1px 3px 2px rgba(0,0,0,0.5), inset 0 0 5px rgba(0,0,0,0.35), 0 0 6px rgba(57,255,20,0.3);
}

/* --- Chrome Rimmed Tiles (matches the Pinball Wizard Playfield board):
   POLISHED chrome, not brushed - the board's rail frame uses many thin
   alternating bands (real brushed metal), this uses one dominant bright
   highlight band on a dark steel base (real POLISHED/mirror chrome, like
   a pinball's own steel ball, has a single sharp specular highlight, not
   a scatter of parallel scratches - the two pieces are deliberately
   different metal finishes so the set doesn't feel like the same texture
   copy-pasted twice). @property-registered custom position lets the
   highlight genuinely SWEEP across the tile on a loop (the same
   "premium reveal" glass-sweep beat Neon-Glowing Letters uses) rather
   than sitting static, plus a bright chrome RIM ring (outer + inner
   border pair) distinct from the tile's own surface gradient - the
   literal "rimmed" the name promises, not just an all-over metal tint. --- */
@property --chrome-sweep {
    syntax: '<percentage>';
    inherits: false;
    initial-value: -30%;
}
.center-column[data-tile-finish="tile_pinball"] .tile:not(.burning),
.shop-item-preview[data-tile-finish="tile_pinball"] .tile {
    background:
        linear-gradient(115deg,
            transparent calc(var(--chrome-sweep) - 18%),
            rgba(255,255,255,0.85) var(--chrome-sweep),
            transparent calc(var(--chrome-sweep) + 18%)),
        linear-gradient(160deg, #e8ecf2 0%, #9aa2ae 22%, #4a5058 48%, #262a30 74%, #14161a 100%);
    color: #eef2f8;
    text-shadow: 0 1px 0 rgba(255,255,255,0.5), 0 -1px 0 rgba(0,0,0,0.6);
    border: 3px solid #f2f5fa;
    box-shadow:
        1px 3px 2px rgba(0,0,0,0.5),
        inset 0 0 0 2px rgba(20,22,26,0.7),
        inset 0 2px 3px rgba(255,255,255,0.6),
        0 0 5px rgba(230,236,244,0.5);
    animation: chromeSweepMove 4s linear infinite;
}
@keyframes chromeSweepMove {
    0% { --chrome-sweep: -30%; }
    100% { --chrome-sweep: 130%; }
}

/* --- Latte Foam Script (matches Coffee-Stained Parchment): a conic-
   gradient off-centered near one corner is what actually gives this a
   hand-poured SWIRL rather than a flat tint - real latte art is milk
   folded into foam in a spiral, not an even wash of cream. Letters render
   in Caveat (a genuine handwritten script webfont, also the base of the
   font_coffee cosmetic) at bold weight for legibility at tile size, in a
   warm espresso ink color with a soft warm glow standing in for "written
   in foam" rather than the sharp neon/chrome glows elsewhere in this
   shop. --- */
.center-column[data-tile-finish="tile_coffee"] .tile:not(.burning),
.shop-item-preview[data-tile-finish="tile_coffee"] .tile {
    background:
        conic-gradient(from 45deg at 66% 32%,
            rgba(210,180,140,0.5) 0deg, transparent 55deg,
            rgba(210,180,140,0.3) 130deg, transparent 190deg,
            rgba(210,180,140,0.45) 270deg, transparent 330deg),
        radial-gradient(ellipse at 28% 72%, rgba(255,252,245,0.5), transparent 55%),
        linear-gradient(120deg, rgba(255,255,255,0.3) 4%, transparent 24%),
        linear-gradient(160deg, #f7ecda 0%, #ecdab8 45%, #d8bd8e 100%);
    color: #4a2c17;
    font-family: 'Caveat', cursive;
    font-weight: 700;
    font-size: 2.1rem;
    text-shadow: 0 0 3px rgba(216,161,74,0.55), 0 1px 0 rgba(255,255,255,0.4);
    box-shadow: 1px 3px 2px rgba(0,0,0,0.35), inset 0 0 6px rgba(139,94,52,0.25);
}

/* --- Prestige Letter Embossing: layered on TOP of whichever tile_finish is
   equipped, but only for the rare Q/Z/X tiles - live scope is .center-column
   like tile_finish, preview scope is .shop-item-preview. Selector order
   matters here: these rules are declared after the tile_finish block above,
   so on a tile that's both a finish AND a rare letter, the emboss texture
   wins (later same-specificity rule beats the earlier one). --- */
.center-column[data-rare-emboss="emboss_carbon"] .tile[data-letter="Q"]:not(.burning),
.center-column[data-rare-emboss="emboss_carbon"] .tile[data-letter="Z"]:not(.burning),
.center-column[data-rare-emboss="emboss_carbon"] .tile[data-letter="X"]:not(.burning),
.shop-item-preview[data-rare-emboss="emboss_carbon"] .tile {
    background:
        repeating-linear-gradient(45deg, rgba(255,255,255,0.05) 0 2px, transparent 2px 4px),
        repeating-linear-gradient(-45deg, rgba(0,0,0,0.35) 0 2px, transparent 2px 4px),
        linear-gradient(150deg, #2a2a2e 0%, #0a0a0c 55%, #1c1c20 100%);
    color: #eaeaea;
    box-shadow: 1px 3px 2px rgba(0,0,0,0.6), inset 0 0 6px rgba(255,255,255,0.08);
    border: 1px solid #3a3a3e;
}

.center-column[data-rare-emboss="emboss_gold"] .tile[data-letter="Q"]:not(.burning),
.center-column[data-rare-emboss="emboss_gold"] .tile[data-letter="Z"]:not(.burning),
.center-column[data-rare-emboss="emboss_gold"] .tile[data-letter="X"]:not(.burning),
.shop-item-preview[data-rare-emboss="emboss_gold"] .tile {
    background:
        repeating-linear-gradient(100grad, rgba(255,255,255,0.25) 0 1px, transparent 1px 3px),
        linear-gradient(135deg, #fff3c4 0%, #e0a83a 35%, #8a5c14 70%, #d4af37 100%);
    color: #3a2400;
    text-shadow: 0 1px 0 rgba(255,255,255,0.35);
    box-shadow: 1px 3px 2px rgba(0,0,0,0.4), inset 0 0 5px rgba(255,255,255,0.5), 0 0 8px rgba(212,175,55,0.4);
    border: 1px solid #8a5c14;
}

.center-column[data-rare-emboss="emboss_glass"] .tile[data-letter="Q"]:not(.burning),
.center-column[data-rare-emboss="emboss_glass"] .tile[data-letter="Z"]:not(.burning),
.center-column[data-rare-emboss="emboss_glass"] .tile[data-letter="X"]:not(.burning),
.shop-item-preview[data-rare-emboss="emboss_glass"] .tile {
    background:
        linear-gradient(120deg, rgba(255,255,255,0.35) 8%, transparent 22%, transparent 78%, rgba(255,255,255,0.12) 92%),
        linear-gradient(150deg, #3a2e5c 0%, #0d0a17 55%, #1e1730 100%);
    color: #f4f0ff;
    box-shadow: 1px 3px 2px rgba(0,0,0,0.6), inset 0 0 12px rgba(180,140,255,0.35), 0 0 10px rgba(120,80,220,0.3);
    border: 1px solid #4a3a70;
}

/* --- Board Theme: Neon Board - a rotating rainbow neon TUBE glowing
   around the board edge only. The ring shape comes from padding + a
   content-box mask (exclude the inner area), so the gradient is only ever
   visible along a thin outline - never as a filled square. Critically, the
   ring's BOX is never transformed/rotated (a rotated square's corners sweep
   outside its own bounding box at off-axis angles, which is what made the
   old version spike a giant diamond past the board edges) - only the
   conic-gradient's start angle animates, via an animatable custom property,
   so the ring geometry stays perfectly static and the colors sweep in
   place. Dead-black cells so the light actually reads as light. --- */
@property --neon-angle {
    syntax: '<angle>';
    inherits: false;
    initial-value: 0deg;
}
.board[data-board-theme="board_neon"] {
    background-color: #050508;
    border-color: #0a0a0f;
    position: relative;
    isolation: isolate;
    box-shadow: 0 10px 20px rgba(0,0,0,0.5);
}
.board[data-board-theme="board_neon"]::before {
    content: '';
    position: absolute;
    inset: -8px;
    border-radius: 16px;
    padding: 8px;
    z-index: -1;
    background: conic-gradient(from var(--neon-angle), #ff005c, #ff9900, #fff700, #00ff6a, #00e5ff, #7000ff, #ff005c);
    -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
    -webkit-mask-composite: xor;
    mask-composite: exclude;
    filter: blur(1.5px) saturate(1.3);
    animation: neonBoardRotate 7s linear infinite;
}
.board[data-board-theme="board_neon"]::after {
    content: '';
    position: absolute;
    inset: -2px;
    border-radius: 10px;
    z-index: -1;
    border: 2px solid rgba(255,255,255,0.8);
    filter: blur(0.5px);
}
@keyframes neonBoardRotate { to { --neon-angle: 360deg; } }
.board[data-board-theme="board_neon"] .cell {
    background-color: #0a0a0f !important;
    border-color: #17172a;
}
.board[data-board-theme="board_neon"] .cell.center-star { background-color: #0a0a0f !important; }
.board[data-board-theme="board_neon"] .cell.center-star::before { color: #fff700; text-shadow: 0 0 8px #fff700, 0 0 18px #fff700; }
.board[data-board-theme="board_neon"] .cell[data-multiplier]::before { text-shadow: 0 0 6px currentColor; }
/* A shared brightness-only breathing pulse (never touches color, so one
   keyframe covers all four multiplier hues without duplicating it four
   times) - the ring already rotates and the cells already glow statically,
   but nothing on the board itself had any independent motion of its own. */
.board[data-board-theme="board_neon"] .cell[data-multiplier] { animation: neonCellBreathe 2.6s ease-in-out infinite; }
@keyframes neonCellBreathe {
    0%, 100% { filter: brightness(1); }
    50% { filter: brightness(1.4); }
}
.board[data-board-theme="board_neon"] .cell[data-multiplier="DL"] { border: 2px solid #00e5ff; box-shadow: inset 0 0 10px rgba(0,229,255,0.55), 0 0 8px rgba(0,229,255,0.5); }
.board[data-board-theme="board_neon"] .cell[data-multiplier="DL"]::before { color: #00e5ff; }
.board[data-board-theme="board_neon"] .cell[data-multiplier="TL"] { border: 2px solid #7000ff; box-shadow: inset 0 0 10px rgba(112,0,255,0.55), 0 0 8px rgba(112,0,255,0.5); }
.board[data-board-theme="board_neon"] .cell[data-multiplier="TL"]::before { color: #b378ff; }
.board[data-board-theme="board_neon"] .cell[data-multiplier="DW"] { border: 2px solid #ff00c8; box-shadow: inset 0 0 10px rgba(255,0,200,0.55), 0 0 8px rgba(255,0,200,0.5); }
.board[data-board-theme="board_neon"] .cell[data-multiplier="DW"]::before { color: #ff6de3; }
.board[data-board-theme="board_neon"] .cell[data-multiplier="TW"] { border: 2px solid #ff2e2e; box-shadow: inset 0 0 10px rgba(255,46,46,0.55), 0 0 8px rgba(255,46,46,0.5); }
.board[data-board-theme="board_neon"] .cell[data-multiplier="TW"]::before { color: #ff7676; }

/* --- Board Theme: Synthwave Grid - Outrun horizon, animated perspective
   grid floor scrolling toward the viewer, glowing sun. The grid briefly
   flares brighter/faster when a tile lands (.synth-pulse, toggled by
   board.js), so the "sound wave" floor visibly reacts to play. The sky AND
   sun (with the genre's single most recognizable detail baked in -
   horizontal "vinyl record" bands cutting through its lower half, the way
   every Kavinsky/Hotline Miami/Kung Fury sun has them, rendered at real
   pixel-level quality no CSS radial-gradient banding can match - see
   server/_gen_textures.py) now come from one procedurally generated image
   instead of a flat CSS gradient plus a separate glowing-circle pseudo-
   element, so the ::before sun layer this used to need is gone entirely. --- */
.board[data-board-theme="board_synthwave"] {
    background: url('/textures/board_synthwave.png') center / cover no-repeat;
    border-color: #ff2e88;
    position: relative;
    overflow: hidden;
    box-shadow: 0 0 25px 2px rgba(255, 46, 136, 0.4), 0 10px 20px rgba(0,0,0,0.3);
}
.board[data-board-theme="board_synthwave"]::after {
    content: '';
    position: absolute;
    left: -60%; right: -60%; bottom: -5%; height: 60%;
    background-image:
        repeating-linear-gradient(90deg, rgba(0,229,255,0.55) 0 2px, transparent 2px 8%),
        repeating-linear-gradient(0deg, rgba(0,229,255,0.55) 0 2px, transparent 2px 12px);
    background-size: 100% 100%, 100% 24px;
    transform: perspective(180px) rotateX(62deg);
    transform-origin: bottom;
    animation: synthGridMove 1.2s linear infinite;
    z-index: 0;
    transition: filter 0.15s ease-out;
}
@keyframes synthGridMove { from { background-position: 0 0, 0 0; } to { background-position: 0 0, 0 24px; } }
.board[data-board-theme="board_synthwave"].synth-pulse::after {
    animation: synthGridMove 0.35s linear infinite;
    filter: brightness(1.8) saturate(1.4);
}
.board[data-board-theme="board_synthwave"] .cell { background-color: rgba(20, 5, 35, 0.6); border-color: rgba(255, 46, 136, 0.45); position: relative; z-index: 1; }
.board[data-board-theme="board_synthwave"] .cell.center-star { background-color: rgba(255, 153, 0, 0.4) !important; }

/* --- Board Theme: Dark Medieval Scroll - a real stone castle, built from
   the actual anatomy of a medieval fortification rather than a flat brown
   fill with a decorative strip:
     - Round corner towers (castle towers evolved square -> D-shaped ->
       round specifically to remove blind corners and give defenders a
       wider firing arc), each with a battlement rim and an arrow slit -
       a narrow vertical opening, true to how they were actually built:
       narrow on the outside face, splayed wider on the inside for the
       archer's firing angle.
     - Alternating merlons (solid) and crenels (gaps) - a true battlement,
       not just a single decorative row - along BOTH the top and bottom
       edges.
     - A pointed-arch portcullis gate at the bottom, with the vertical
       bars a real portcullis grille has.
     - Dressed stone coursing (coursed masonry) instead of a uniform tint,
       plus mortar lines, so the wall reads as fitted stone blocks.
   All of this is composited as layered backgrounds/pseudo-elements, no
   image assets, so it stays crisp and theme-recolorable at any board size. --- */
.board[data-board-theme="board_medieval"] {
    --stone-hi: #948d7d;
    --stone-mid: #6b6558;
    --stone-lo: #4a453b;
    --stone-dark: #332f28;
    --mortar: #241f1a;
    background:
        /* Battlements: merlons (solid teeth) top and bottom, crenels
           (gaps) let the layer beneath show through. */
        repeating-linear-gradient(90deg, var(--stone-dark) 0 18px, transparent 18px 36px) top / 36px 16px repeat-x,
        repeating-linear-gradient(90deg, var(--stone-dark) 0 18px, transparent 18px 36px) bottom / 36px 16px repeat-x,
        /* Four round corner towers, each with a battlement rim ring and a
           highlight to read as a turret top seen from above. */
        radial-gradient(circle at center, var(--stone-hi) 0%, var(--stone-mid) 42%, var(--stone-lo) 60%, var(--mortar) 67%, var(--mortar) 74%, transparent 76%) top left / 50px 50px no-repeat,
        radial-gradient(circle at center, var(--stone-hi) 0%, var(--stone-mid) 42%, var(--stone-lo) 60%, var(--mortar) 67%, var(--mortar) 74%, transparent 76%) top right / 50px 50px no-repeat,
        radial-gradient(circle at center, var(--stone-hi) 0%, var(--stone-mid) 42%, var(--stone-lo) 60%, var(--mortar) 67%, var(--mortar) 74%, transparent 76%) bottom left / 50px 50px no-repeat,
        radial-gradient(circle at center, var(--stone-hi) 0%, var(--stone-mid) 42%, var(--stone-lo) 60%, var(--mortar) 67%, var(--mortar) 74%, transparent 76%) bottom right / 50px 50px no-repeat,
        /* Arrow slits on each tower - narrow, dark, vertical. */
        linear-gradient(#0a0806, #0a0806) 17px 12px / 4px 22px no-repeat,
        linear-gradient(#0a0806, #0a0806) calc(100% - 17px) 12px / 4px 22px no-repeat,
        linear-gradient(#0a0806, #0a0806) 17px calc(100% - 12px) / 4px 22px no-repeat,
        linear-gradient(#0a0806, #0a0806) calc(100% - 17px) calc(100% - 12px) / 4px 22px no-repeat,
        /* Torch flame cores: small, bright ellipses give the warm pooling
           below an actual light SOURCE to radiate from, instead of just a
           vague warm patch on the wall. */
        radial-gradient(ellipse 7px 13px at 24% 22%, rgba(255,225,140,0.95), rgba(255,140,20,0.55) 55%, transparent 78%),
        radial-gradient(ellipse 7px 13px at 78% 76%, rgba(255,225,140,0.95), rgba(255,140,20,0.55) 55%, transparent 78%),
        /* Warm torchlight pooling near two of the towers. */
        radial-gradient(ellipse at 24% 22%, rgba(255,150,40,0.16), transparent 45%),
        radial-gradient(ellipse at 78% 76%, rgba(255,150,40,0.13), transparent 45%),
        /* The wall material itself: a procedurally generated, height-mapped
           dressed-stone texture (real per-pixel rock variation + directional
           lighting + genuine coursed mortar joints, rendered offline - see
           server/_gen_textures.py) instead of CSS repeating-gradients trying
           to fake masonry with flat mortar lines. */
        url('/textures/board_medieval.png') center / cover no-repeat;
    border-color: var(--mortar);
    border-width: 14px;
    border-style: solid;
    position: relative;
    isolation: isolate;
    animation: torchFlicker 3.2s ease-in-out infinite;
}
.board[data-board-theme="board_medieval"]::before {
    /* The gatehouse: a pointed-arch portcullis set into the bottom wall. */
    content: '';
    position: absolute;
    bottom: -14px;
    left: 50%;
    transform: translateX(-50%);
    width: 15%;
    min-width: 42px;
    max-width: 64px;
    height: 30px;
    background:
        repeating-linear-gradient(90deg, #17140f 0 4px, #0a0806 4px 8px);
    border: 2px solid var(--mortar);
    border-bottom: none;
    border-radius: 50% 50% 0 0 / 65% 65% 0 0;
    box-shadow: inset 0 0 8px rgba(0,0,0,0.7);
    z-index: 1;
}
.board[data-board-theme="board_medieval"]::after {
    content: '';
    position: absolute; inset: 0;
    box-shadow: inset 0 0 3px 1px rgba(0,0,0,0.5);
    pointer-events: none;
}
@keyframes torchFlicker {
    0%, 100% { box-shadow: 0 10px 20px rgba(0,0,0,0.35), inset 0 0 40px rgba(255,140,20,0.08); }
    45% { box-shadow: 0 10px 20px rgba(0,0,0,0.35), inset 0 0 55px rgba(255,140,20,0.2); }
    60% { box-shadow: 0 10px 20px rgba(0,0,0,0.35), inset 0 0 45px rgba(255,140,20,0.12); }
}
.board[data-board-theme="board_medieval"] .cell { background-color: #362f26; border-color: #4a3f30; }
.board[data-board-theme="board_medieval"] .cell.center-star { background: radial-gradient(circle, #ff9f43, #8a4a10) !important; }

/* --- Board Theme: Cybernetic Terminal - circuit-trace grid, a scanning
   line that sweeps top-to-bottom like an active hacking readout. Real PCBs
   aren't one uniform grid - they have thin signal traces AND thicker power/
   bus traces layered together, plus copper via-node dots at junctions -
   this now has both, so it reads as an actual circuit board rather than
   graph paper. --- */
.board[data-board-theme="board_cyber"] {
    /* An actual randomized-orthogonal-walk circuit trace layout (real PCB
       auto-routers use a far more complex version of the same idea) baked
       into the base texture, with these five brighter via-node dots layered
       on top as accent highlights - see server/_gen_textures.py. */
    background:
        radial-gradient(circle 1.8px, rgba(0,255,224,0.6), transparent) 25% 33% / auto no-repeat,
        radial-gradient(circle 1.8px, rgba(0,255,224,0.6), transparent) 58% 66% / auto no-repeat,
        radial-gradient(circle 1.8px, rgba(0,255,224,0.6), transparent) 75% 25% / auto no-repeat,
        radial-gradient(circle 1.8px, rgba(0,255,224,0.6), transparent) 33% 83% / auto no-repeat,
        radial-gradient(circle 1.8px, rgba(0,255,224,0.6), transparent) 91% 50% / auto no-repeat,
        url('/textures/board_cyber.png') center / cover no-repeat;
    border-color: #00ffe0;
    position: relative;
    overflow: hidden;
    box-shadow: 0 0 25px 2px rgba(0,255,224,0.35), 0 10px 20px rgba(0,0,0,0.3);
}
.board[data-board-theme="board_cyber"]::before {
    content: '';
    position: absolute; inset: 0;
    background-image:
        linear-gradient(rgba(0,255,224,0.2) 1.5px, transparent 1.5px),
        linear-gradient(90deg, rgba(0,255,224,0.2) 1.5px, transparent 1.5px),
        linear-gradient(rgba(0,255,224,0.09) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0,255,224,0.09) 1px, transparent 1px);
    background-size: 33.2% 33.2%, 33.2% 33.2%, 8.3% 8.3%, 8.3% 8.3%;
    z-index: 0;
}
.board[data-board-theme="board_cyber"]::after {
    content: '';
    position: absolute; left: 0; right: 0; height: 3px;
    background: linear-gradient(90deg, transparent, #00ffe0, transparent);
    box-shadow: 0 0 10px 3px #00ffe0;
    animation: cyberScan 3.4s linear infinite;
    z-index: 2;
}
@keyframes cyberScan { 0% { top: -2%; } 100% { top: 100%; } }
.board[data-board-theme="board_cyber"] .cell { background-color: rgba(0, 36, 36, 0.72); border-color: #00b3a4; position: relative; z-index: 1; }
.board[data-board-theme="board_cyber"] .cell.center-star { background-color: rgba(0, 255, 224, 0.35) !important; }

/* --- Board Theme: Celestial Nebula - a real domain-warped turbulence-noise
   nebula (the actual procedural-art technique for convincing gas/cloud
   rendering: warp fbm noise's own sample coordinates with a second noise
   field) in genuine Hubble false-color hues (magenta #B3298E, teal #2EC4B6),
   scattered with a real varied-size/brightness starfield - rendered offline
   at full pixel detail no CSS radial-gradient stack could match (see
   server/_gen_textures.py) - plus a periodic shooting star streaking across
   on its own independent timer. Cells go dead-space-black (!important, same
   technique as Neon board - see its comment) so multiplier squares read as
   glowing jewel-tone borders against the void instead of plain pastel squares. --- */
.board[data-board-theme="board_nebula"] {
    background: url('/textures/board_nebula.png') center / cover no-repeat;
    border-color: #3a1a5c;
    position: relative;
    overflow: hidden;
    box-shadow: 0 0 25px 2px rgba(107,26,143,0.4), 0 10px 20px rgba(0,0,0,0.4);
    animation: nebulaTwinkle 4s ease-in-out infinite;
}
@keyframes nebulaTwinkle {
    0%, 100% { filter: brightness(1); }
    50% { filter: brightness(1.14); }
}
.board[data-board-theme="board_nebula"]::after {
    content: '';
    position: absolute;
    top: 14%;
    left: -12%;
    width: 16%;
    height: 2px;
    background: linear-gradient(90deg, transparent, #fff 70%, #fff);
    border-radius: 2px;
    box-shadow: 0 0 6px 1px #fff, 0 0 12px 2px rgba(203,184,255,0.6);
    transform: rotate(-18deg);
    opacity: 0;
    z-index: 2;
    pointer-events: none;
    animation: nebulaShootingStar 9s ease-in infinite;
}
@keyframes nebulaShootingStar {
    0%, 68% { opacity: 0; transform: translateX(0) rotate(-18deg); }
    70% { opacity: 1; }
    82% { opacity: 0.9; transform: translateX(560%) rotate(-18deg); }
    84%, 100% { opacity: 0; transform: translateX(560%) rotate(-18deg); }
}
.board[data-board-theme="board_nebula"] .cell { background-color: rgba(10, 5, 20, 0.55) !important; border-color: rgba(179,41,142,0.35); position: relative; z-index: 1; }
.board[data-board-theme="board_nebula"] .cell.center-star { background-color: rgba(46,196,182,0.4) !important; }
.board[data-board-theme="board_nebula"] .cell.center-star::before { color: #fff; text-shadow: 0 0 8px #fff, 0 0 16px #cbb8ff; }
.board[data-board-theme="board_nebula"] .cell[data-multiplier]::before { text-shadow: 0 0 5px currentColor; }
.board[data-board-theme="board_nebula"] .cell[data-multiplier="DL"] { border: 2px solid #2ec4b6; box-shadow: inset 0 0 8px rgba(46,196,182,0.45); }
.board[data-board-theme="board_nebula"] .cell[data-multiplier="DL"]::before { color: #2ec4b6; }
.board[data-board-theme="board_nebula"] .cell[data-multiplier="TL"] { border: 2px solid #7b5fd6; box-shadow: inset 0 0 8px rgba(123,95,214,0.45); }
.board[data-board-theme="board_nebula"] .cell[data-multiplier="TL"]::before { color: #b39ce0; }
.board[data-board-theme="board_nebula"] .cell[data-multiplier="DW"] { border: 2px solid #b3298e; box-shadow: inset 0 0 8px rgba(179,41,142,0.45); }
.board[data-board-theme="board_nebula"] .cell[data-multiplier="DW"]::before { color: #e07dc7; }
.board[data-board-theme="board_nebula"] .cell[data-multiplier="TW"] { border: 2px solid #ff6b6b; box-shadow: inset 0 0 8px rgba(255,107,107,0.45); }
.board[data-board-theme="board_nebula"] .cell[data-multiplier="TW"]::before { color: #ff9e9e; }

/* --- Board Theme: Royal Sapphire & Gold - a real woven-velvet base (large-
   scale directional noise for the pile's uneven light-catching, low-
   amplitude fine grain for actual weave texture) in genuine sapphire-gem
   blue (#0F52BA), overlaid with an actual repeating gold brocade/damask
   medallion motif - the real texture language of royal regalia, rendered
   offline at full pixel detail (see server/_gen_textures.py) instead of a
   flat radial gradient. Framed by a gold filigree ring (#D4AF37, standard
   "metallic gold") with four corner medallions - same ring technique as the
   Neon board (negative-inset pseudo-element in the border zone, never
   overlapping the cell grid, so nothing fights the cells for paint order).
   A second pseudo-element (::after) adds a travelling glint circling the
   gold frame - the same animated-conic-angle trick as the ring itself, just
   masked to a narrow arc instead of the whole ring, so it reads as light
   catching polished gold as it turns (real jewelry macro photography does
   exactly this - a hard, moving specular highlight, not a static shine). --- */
.board[data-board-theme="board_royal"] {
    background: url('/textures/board_royal.png') center / cover no-repeat;
    border-color: #0a2040;
    border-width: 10px;
    border-style: solid;
    position: relative;
    isolation: isolate;
    box-shadow: 0 0 20px 2px rgba(212,175,55,0.4), 0 10px 20px rgba(0,0,0,0.4);
}
.board[data-board-theme="board_royal"]::before {
    content: '';
    position: absolute;
    inset: -10px;
    border: 3px solid #d4af37;
    border-radius: 4px;
    box-shadow: inset 0 0 0 1px rgba(246,230,168,0.5), 0 0 8px rgba(212,175,55,0.5);
    background:
        radial-gradient(circle at center, #f6e6a8 0%, #d4af37 45%, #8a6a1a 68%, transparent 74%) top left / 34px 34px no-repeat,
        radial-gradient(circle at center, #f6e6a8 0%, #d4af37 45%, #8a6a1a 68%, transparent 74%) top right / 34px 34px no-repeat,
        radial-gradient(circle at center, #f6e6a8 0%, #d4af37 45%, #8a6a1a 68%, transparent 74%) bottom left / 34px 34px no-repeat,
        radial-gradient(circle at center, #f6e6a8 0%, #d4af37 45%, #8a6a1a 68%, transparent 74%) bottom right / 34px 34px no-repeat;
    z-index: -1;
    pointer-events: none;
}
@property --royal-shine-angle {
    syntax: '<angle>';
    inherits: false;
    initial-value: 0deg;
}
.board[data-board-theme="board_royal"]::after {
    content: '';
    position: absolute;
    inset: -10px;
    border-radius: 4px;
    padding: 3px;
    background: conic-gradient(from var(--royal-shine-angle), transparent 0deg, transparent 328deg, rgba(255,255,255,0.95) 344deg, rgba(255,255,255,0.3) 352deg, transparent 360deg);
    -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
    -webkit-mask-composite: xor;
    mask-composite: exclude;
    z-index: -1;
    pointer-events: none;
    animation: royalShineRotate 6s linear infinite;
}
@keyframes royalShineRotate { to { --royal-shine-angle: 360deg; } }
.board[data-board-theme="board_royal"] .cell {
    background-color: #0f2f5c !important;
    background-image: linear-gradient(115deg, rgba(255,255,255,0.1) 0%, transparent 35%, transparent 65%, rgba(255,255,255,0.05) 100%);
    border-color: #1e5fa8;
    position: relative;
    z-index: 1;
}
.board[data-board-theme="board_royal"] .cell.center-star { background: radial-gradient(circle, #f6e6a8, #d4af37) !important; }
.board[data-board-theme="board_royal"] .cell.center-star::before { color: #4a2f00; text-shadow: none; }
.board[data-board-theme="board_royal"] .cell[data-multiplier]::before { text-shadow: 0 1px 0 rgba(0,0,0,0.5); }
.board[data-board-theme="board_royal"] .cell[data-multiplier="DL"] { border: 2px solid #6fa8dc; box-shadow: inset 0 0 8px rgba(111,168,220,0.5); }
.board[data-board-theme="board_royal"] .cell[data-multiplier="DL"]::before { color: #a8c8ec; }
.board[data-board-theme="board_royal"] .cell[data-multiplier="TL"] { border: 2px solid #d4af37; box-shadow: inset 0 0 8px rgba(212,175,55,0.5); }
.board[data-board-theme="board_royal"] .cell[data-multiplier="TL"]::before { color: #f6e6a8; }
.board[data-board-theme="board_royal"] .cell[data-multiplier="DW"] { border: 2px solid #c0392b; box-shadow: inset 0 0 8px rgba(192,57,43,0.45); }
.board[data-board-theme="board_royal"] .cell[data-multiplier="DW"]::before { color: #e8a99f; }
.board[data-board-theme="board_royal"] .cell[data-multiplier="TW"] { border: 2px solid #f6e6a8; box-shadow: inset 0 0 10px rgba(246,230,168,0.6), 0 0 6px rgba(212,175,55,0.5); }
.board[data-board-theme="board_royal"] .cell[data-multiplier="TW"]::before { color: #fff7d6; }

/* --- Board Theme: Molten Obsidian - a real basalt height-field (Perlin
   noise -> directional lighting, the same technique as the Medieval wall)
   split by an actual random-walk fracture network - straight segments
   jittering around a slowly-drifting propagation direction, branches
   peeling off at real angles, so cracks read as genuine rock fractures
   radiating outward instead of CSS diagonal bars - rendered offline at full
   pixel detail (see server/_gen_textures.py). The whole board pulses in
   brightness (volcanicEmber, extended below) so the crack glow baked into
   the texture visibly breathes, plus real embers (small glowing dots)
   drifting upward off the board and fading, the same visual beat real
   lava-field footage and God of War's Muspelheim use. --- */
.board[data-board-theme="board_volcanic"] {
    background: url('/textures/board_volcanic.png') center / cover no-repeat;
    border-color: #ff4500;
    position: relative;
    overflow: hidden;
    isolation: isolate;
    box-shadow: 0 0 22px 2px rgba(255,69,0,0.4), 0 10px 20px rgba(0,0,0,0.5);
    animation: volcanicEmber 3s ease-in-out infinite;
}
.board[data-board-theme="board_volcanic"]::after {
    content: '';
    position: absolute; inset: 0;
    background-image:
        radial-gradient(circle 1.4px, #ffcc66, transparent),
        radial-gradient(circle 1px, #ff8a00, transparent),
        radial-gradient(circle 1.6px, #ffaa00, transparent),
        radial-gradient(circle 1px, #ff6b00, transparent),
        radial-gradient(circle 1.2px, #ffcc66, transparent);
    background-position: 18% 96%, 42% 94%, 63% 98%, 80% 93%, 30% 97%;
    background-repeat: no-repeat;
    opacity: 0;
    z-index: 2;
    pointer-events: none;
    animation: volcanicEmberRise 5s ease-in infinite;
}
@keyframes volcanicEmber {
    0%, 100% { box-shadow: 0 0 22px 2px rgba(255,69,0,0.4), 0 10px 20px rgba(0,0,0,0.5); filter: brightness(1) saturate(1); }
    50% { box-shadow: 0 0 32px 6px rgba(255,107,0,0.6), 0 10px 20px rgba(0,0,0,0.5); filter: brightness(1.12) saturate(1.15); }
}
@keyframes volcanicEmberRise {
    0% { opacity: 0; background-position: 18% 98%, 42% 96%, 63% 100%, 80% 95%, 30% 99%; }
    15% { opacity: 1; }
    75% { opacity: 0.5; }
    100% { opacity: 0; background-position: 18% 5%, 42% 0%, 63% 8%, 80% -5%, 30% 3%; }
}
.board[data-board-theme="board_volcanic"] .cell { background-color: rgba(20,15,13,0.75) !important; border-color: #3a2a20; position: relative; z-index: 1; }
.board[data-board-theme="board_volcanic"] .cell.center-star { background: radial-gradient(circle, #ffaa00, #ff4500) !important; }
.board[data-board-theme="board_volcanic"] .cell.center-star::before { color: #1a0800; text-shadow: none; }
.board[data-board-theme="board_volcanic"] .cell[data-multiplier]::before { text-shadow: 0 0 5px currentColor; }
.board[data-board-theme="board_volcanic"] .cell[data-multiplier="DL"] { border: 2px solid #ffaa00; box-shadow: inset 0 0 8px rgba(255,170,0,0.4); }
.board[data-board-theme="board_volcanic"] .cell[data-multiplier="DL"]::before { color: #ffcc66; }
.board[data-board-theme="board_volcanic"] .cell[data-multiplier="TL"] { border: 2px solid #ff6b00; box-shadow: inset 0 0 8px rgba(255,107,0,0.45); }
.board[data-board-theme="board_volcanic"] .cell[data-multiplier="TL"]::before { color: #ff9e4d; }
.board[data-board-theme="board_volcanic"] .cell[data-multiplier="DW"] { border: 2px solid #ff4500; box-shadow: inset 0 0 8px rgba(255,69,0,0.5); }
.board[data-board-theme="board_volcanic"] .cell[data-multiplier="DW"]::before { color: #ff8a5c; }
.board[data-board-theme="board_volcanic"] .cell[data-multiplier="TW"] { border: 2px solid #ffe066; box-shadow: inset 0 0 10px rgba(255,224,102,0.55), 0 0 6px rgba(255,170,0,0.5); }
.board[data-board-theme="board_volcanic"] .cell[data-multiplier="TW"]::before { color: #fff2b8; }

/* --- Board Theme: CRT Grid Terminal - a powered-on CRT monitor viewport,
   not just "another dark neon board" (deliberately distinct from Neon
   Board's soft rotating-ring glow and Cybernetic Terminal's photo-real
   panel): a phosphor wireframe grid baked into the background, genuine
   horizontal scanlines (a repeating-gradient ::after overlay, the actual
   CRT refresh-line artifact, at real ~2px density), a dark bezel frame
   with a glass highlight line, and an irregular flicker - CRTs don't pulse
   smoothly, they stutter for one or two frames every few seconds, which is
   what crtFlicker's uneven keyframe spacing (96/97/98/99%) reproduces
   instead of a smooth sine pulse. Multiplier squares use a real 8-bit/
   arcade primary palette (cyan/magenta/yellow/phosphor-green) with hard,
   non-blurred pixel-stepped borders instead of soft glow blur - "blocky,
   not blurry" is the entire visual thesis that separates 8-bit from every
   other neon theme in this shop. Labels and the center star render in
   Press Start 2P (already loaded for the font_pixel cosmetic), so the
   "glowing bit font" is a real period-accurate arcade typeface, not an
   approximation. --- */
.board[data-board-theme="board_crt"] {
    background:
        repeating-linear-gradient(90deg, rgba(57,255,20,0.07) 0 1px, transparent 1px 20px),
        repeating-linear-gradient(0deg, rgba(57,255,20,0.07) 0 1px, transparent 1px 20px),
        radial-gradient(ellipse at center, #0c1a10 0%, #050a06 70%, #020402 100%);
    border-color: #1a1a1a;
    position: relative;
    overflow: hidden;
    isolation: isolate;
    box-shadow: 0 0 24px 2px rgba(57,255,20,0.25), 0 10px 20px rgba(0,0,0,0.6), inset 0 0 40px rgba(0,0,0,0.6);
    animation: crtFlicker 5s ease-in-out infinite;
}
/* Scanlines - the actual CRT artifact this whole theme is named for. Sits
   above every cell (z-index 2, cells are 1) so it reads as glass in front
   of the board, not a texture painted under it; pointer-events:none so it
   never intercepts tile drag/drop. */
.board[data-board-theme="board_crt"]::after {
    content: '';
    position: absolute; inset: 0;
    background: repeating-linear-gradient(0deg, rgba(0,0,0,0.55) 0 1px, transparent 1px 3px);
    z-index: 2;
    pointer-events: none;
    mix-blend-mode: multiply;
}
/* The monitor bezel's glass highlight - a thin bright line hugging the
   inner edge, exactly how light catches the inside lip of a real CRT's
   curved glass. */
.board[data-board-theme="board_crt"]::before {
    content: '';
    position: absolute; inset: 3px;
    border-radius: 4px;
    border: 1px solid rgba(57,255,20,0.35);
    box-shadow: inset 0 0 12px rgba(57,255,20,0.15);
    z-index: 3;
    pointer-events: none;
}
@keyframes crtFlicker {
    0%, 92%, 100% { filter: brightness(1); }
    93% { filter: brightness(0.85); }
    94% { filter: brightness(1.05); }
    95% { filter: brightness(0.92); }
    96% { filter: brightness(1); }
}
.board[data-board-theme="board_crt"] .cell {
    background-color: rgba(6,14,8,0.82) !important;
    border-color: rgba(57,255,20,0.25);
    position: relative;
    z-index: 1;
}
.board[data-board-theme="board_crt"] .cell.center-star { background-color: rgba(6,14,8,0.82) !important; }
.board[data-board-theme="board_crt"] .cell.center-star::before {
    color: #39ff14;
    text-shadow: 0 0 6px #39ff14, 0 0 14px #39ff14;
}
.board[data-board-theme="board_crt"] .cell[data-multiplier]::before {
    font-family: 'Press Start 2P', 'Courier New', monospace;
    font-size: 0.42rem;
    text-shadow: 0 0 4px currentColor, 0 0 8px currentColor;
}
.board[data-board-theme="board_crt"] .cell[data-multiplier] { animation: crtCellBreathe 2.4s ease-in-out infinite; }
@keyframes crtCellBreathe {
    0%, 100% { filter: brightness(1); }
    50% { filter: brightness(1.3); }
}
/* Hard-stepped double border (no blur radius) is the "pixel," not a soft
   box-shadow glow - the technique every other theme's premium squares use
   for smooth neon, deliberately swapped out here for a blocky sprite edge. */
.board[data-board-theme="board_crt"] .cell[data-multiplier="DL"] { border: 2px solid #00fff2; box-shadow: 0 0 0 1px #0a2e2c, inset 0 0 6px rgba(0,255,242,0.4); }
.board[data-board-theme="board_crt"] .cell[data-multiplier="DL"]::before { color: #00fff2; }
.board[data-board-theme="board_crt"] .cell[data-multiplier="TL"] { border: 2px solid #ff2e97; box-shadow: 0 0 0 1px #2e0a1e, inset 0 0 6px rgba(255,46,151,0.4); }
.board[data-board-theme="board_crt"] .cell[data-multiplier="TL"]::before { color: #ff2e97; }
.board[data-board-theme="board_crt"] .cell[data-multiplier="DW"] { border: 2px solid #ffe600; box-shadow: 0 0 0 1px #2e2a0a, inset 0 0 6px rgba(255,230,0,0.4); }
.board[data-board-theme="board_crt"] .cell[data-multiplier="DW"]::before { color: #ffe600; }
.board[data-board-theme="board_crt"] .cell[data-multiplier="TW"] { border: 2px solid #39ff14; box-shadow: 0 0 0 1px #0a2e0f, inset 0 0 8px rgba(57,255,20,0.5); }
.board[data-board-theme="board_crt"] .cell[data-multiplier="TW"]::before { color: #39ff14; }

/* --- Board Theme: Pinball Wizard Playfield - a real pinball machine's
   playfield, not just "a shiny board": a chrome rail frame (the same
   mask-composite ring technique board_neon's rotating ring uses, here a
   static multi-band brushed-metal gradient instead of a rotating rainbow -
   real brushed chrome IS a series of light/dark bands catching light at
   different angles, not one smooth silver gradient), a glossy lacquer
   sheen sweeping slowly across the whole playfield the way cabinet
   floodlights catch a glass-coated surface, and premium squares
   reimagined as round pinball BUMPERS (border-radius:50%, the one theme
   in this shop where the premium squares change shape, not just color -
   because that shape change is literally what makes them read as bumpers
   instead of glowing tiles) that flash bright on a staggered per-bumper
   cycle, exactly like a real cabinet's chase-lit bumpers never firing in
   perfect unison. --- */
.board[data-board-theme="board_pinball"] {
    background:
        linear-gradient(115deg, transparent 28%, rgba(255,255,255,0.16) 42%, rgba(255,255,255,0.05) 48%, transparent 62%),
        radial-gradient(ellipse at 28% 18%, rgba(255,255,255,0.10), transparent 55%),
        linear-gradient(160deg, #241820 0%, #150e16 50%, #0a060a 100%);
    background-size: 220% 220%, 100% 100%, 100% 100%;
    border-color: #2a2228;
    position: relative;
    overflow: hidden;
    isolation: isolate;
    box-shadow: 0 10px 22px rgba(0,0,0,0.55), inset 0 0 30px rgba(0,0,0,0.5);
    animation: pinballGlossSweep 9s ease-in-out infinite;
}
@keyframes pinballGlossSweep {
    0%, 100% { background-position: 0% 0%, 0 0, 0 0; }
    50% { background-position: 100% 100%, 0 0, 0 0; }
}
/* Chrome rail frame: alternating light/dark bands at a diagonal, the
   actual visual signature of brushed metal catching light unevenly. */
.board[data-board-theme="board_pinball"]::before {
    content: '';
    position: absolute;
    inset: -9px;
    border-radius: 13px;
    padding: 9px;
    z-index: -1;
    background: linear-gradient(125deg,
        #38383e 0%, #c8c8ce 9%, #6a6a72 18%, #eeeef2 27%,
        #52525a 36%, #d8d8dc 45%, #3a3a40 54%, #b0b0b6 63%,
        #46464c 72%, #dcdce0 81%, #2c2c32 90%, #8a8a92 100%);
    -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
    -webkit-mask-composite: xor;
    mask-composite: exclude;
    box-shadow: 0 0 10px rgba(0,0,0,0.4);
}
.board[data-board-theme="board_pinball"] .cell {
    background-color: rgba(15,10,14,0.78) !important;
    border-color: #3a2e36;
    position: relative;
    z-index: 1;
}
.board[data-board-theme="board_pinball"] .cell.center-star {
    background: radial-gradient(circle, #fff6d8, #ffcc33 55%, #b8860b) !important;
    border-radius: 50%;
}
.board[data-board-theme="board_pinball"] .cell.center-star::before { color: #3a2200; text-shadow: 0 1px 0 rgba(255,255,255,0.4); }
/* Bumpers: round, glossy (an inset highlight arc near the top mimics a
   real bumper cap's curved specular reflection), with a bright staggered
   flash instead of a smooth breathing pulse - real bumper lights snap on,
   they don't fade in. */
.board[data-board-theme="board_pinball"] .cell[data-multiplier] {
    border-radius: 50%;
    box-sizing: border-box;
}
.board[data-board-theme="board_pinball"] .cell[data-multiplier]::before {
    text-shadow: 0 1px 1px rgba(0,0,0,0.6);
    font-weight: 900;
}
.board[data-board-theme="board_pinball"] .cell[data-multiplier="DL"] {
    border: 3px solid #ffd400;
    box-shadow: inset 0 3px 5px rgba(255,255,255,0.5), inset 0 -4px 6px rgba(0,0,0,0.35), 0 0 7px rgba(255,212,0,0.55);
    animation: pinballBumperFlash 2.8s ease-in-out infinite;
}
.board[data-board-theme="board_pinball"] .cell[data-multiplier="DL"]::before { color: #4a3800; }
.board[data-board-theme="board_pinball"] .cell[data-multiplier="TL"] {
    border: 3px solid #ff8c1a;
    box-shadow: inset 0 3px 5px rgba(255,255,255,0.5), inset 0 -4px 6px rgba(0,0,0,0.35), 0 0 7px rgba(255,140,26,0.55);
    animation: pinballBumperFlash 3.1s ease-in-out infinite 0.6s;
}
.board[data-board-theme="board_pinball"] .cell[data-multiplier="TL"]::before { color: #4a2600; }
.board[data-board-theme="board_pinball"] .cell[data-multiplier="DW"] {
    border: 3px solid #ff2d3c;
    box-shadow: inset 0 3px 5px rgba(255,255,255,0.5), inset 0 -4px 6px rgba(0,0,0,0.35), 0 0 8px rgba(255,45,60,0.6);
    animation: pinballBumperFlash 2.5s ease-in-out infinite 1.1s;
}
.board[data-board-theme="board_pinball"] .cell[data-multiplier="DW"]::before { color: #4a0004; }
.board[data-board-theme="board_pinball"] .cell[data-multiplier="TW"] {
    border: 3px solid #f0f4ff;
    box-shadow: inset 0 3px 6px rgba(255,255,255,0.7), inset 0 -4px 6px rgba(0,0,0,0.3), 0 0 10px rgba(240,244,255,0.7);
    animation: pinballBumperFlash 2.2s ease-in-out infinite 1.7s;
}
.board[data-board-theme="board_pinball"] .cell[data-multiplier="TW"]::before { color: #202840; }
@keyframes pinballBumperFlash {
    0%, 78%, 100% { filter: brightness(1); }
    82% { filter: brightness(1.9); }
    86% { filter: brightness(1.1); }
    90% { filter: brightness(1.7); }
    94% { filter: brightness(1); }
}

/* --- Board Theme: Coffee-Stained Parchment - real coffee-ring physics,
   not just a brown tint: an actual coffee stain dries with its residue
   concentrated at the EDGE of the spill (the "coffee ring effect"), so
   each stain below is a darker annulus around a much fainter center, not
   a solid blob - five stains at varied position/size/strength so it reads
   as accumulated everyday use, not one graphic pasted in the corner. Warm
   diagonal paper-fiber grain (two angles, the same technique the free
   board's own grain uses, just amber-toned here) plus a soft vignette
   toward the edges - old paper is never evenly lit, it's always a little
   darker where it's been handled most. Premium squares are warm cafe
   pen-ink colors with a soft AMBIENT glow rather than the sharp neon
   every other dark theme uses - this is a cozy morning theme, not an
   electric one, so "glow" here means candlelight, not a bulb. --- */
.board[data-board-theme="board_coffee"] {
    background:
        /* coffee rings: dark rim, faint center, each independent so they
           can overlap at different strengths like real repeated use */
        radial-gradient(circle at 18% 22%, transparent 38%, rgba(101,67,33,0.4) 43%, rgba(101,67,33,0.12) 47%, transparent 52%),
        radial-gradient(circle at 78% 15%, transparent 30%, rgba(90,58,28,0.3) 35%, rgba(90,58,28,0.1) 39%, transparent 44%),
        radial-gradient(circle at 85% 72%, transparent 42%, rgba(101,67,33,0.35) 47%, rgba(101,67,33,0.1) 51%, transparent 56%),
        radial-gradient(circle at 12% 80%, transparent 34%, rgba(120,80,40,0.28) 39%, rgba(120,80,40,0.08) 43%, transparent 48%),
        radial-gradient(circle at 48% 50%, transparent 46%, rgba(101,67,33,0.18) 50%, rgba(101,67,33,0.06) 54%, transparent 58%),
        /* warm diagonal paper fiber, two crossing angles */
        repeating-linear-gradient(4deg, rgba(120,80,30,0.05) 0 2px, transparent 2px 9px),
        repeating-linear-gradient(-11deg, rgba(90,60,20,0.04) 0 1px, transparent 1px 11px),
        /* handled-edges vignette */
        radial-gradient(ellipse at center, #ede0c4 0%, #ded0ac 55%, #c8b788 100%);
    border-color: #8a6f45;
    box-shadow: 0 10px 20px rgba(0,0,0,0.3), inset 0 0 40px rgba(101,67,33,0.25);
}
.board[data-board-theme="board_coffee"] .cell {
    background-color: rgba(237,224,196,0.6) !important;
    border-color: rgba(138,111,69,0.4);
}
.board[data-board-theme="board_coffee"] .cell.center-star {
    background: radial-gradient(circle, #d8a84a, #8a5a1e) !important;
}
.board[data-board-theme="board_coffee"] .cell.center-star::before { color: #3a2508; text-shadow: 0 1px 0 rgba(255,235,200,0.4); }
.board[data-board-theme="board_coffee"] .cell[data-multiplier]::before { font-family: 'Caveat', cursive; font-size: 0.85rem; font-weight: 700; }
.board[data-board-theme="board_coffee"] .cell[data-multiplier] { animation: coffeeCellBreathe 3.4s ease-in-out infinite; }
@keyframes coffeeCellBreathe {
    0%, 100% { filter: brightness(1); }
    50% { filter: brightness(1.12); }
}
.board[data-board-theme="board_coffee"] .cell[data-multiplier="DL"] { border: 2px solid #c68a3d; box-shadow: inset 0 0 8px rgba(198,138,61,0.4), 0 0 6px rgba(198,138,61,0.35); }
.board[data-board-theme="board_coffee"] .cell[data-multiplier="DL"]::before { color: #7a4f1c; }
.board[data-board-theme="board_coffee"] .cell[data-multiplier="TL"] { border: 2px solid #a0522d; box-shadow: inset 0 0 8px rgba(160,82,45,0.4), 0 0 6px rgba(160,82,45,0.35); }
.board[data-board-theme="board_coffee"] .cell[data-multiplier="TL"]::before { color: #5c2e14; }
.board[data-board-theme="board_coffee"] .cell[data-multiplier="DW"] { border: 2px solid #6b4423; box-shadow: inset 0 0 8px rgba(107,68,35,0.45), 0 0 6px rgba(107,68,35,0.4); }
.board[data-board-theme="board_coffee"] .cell[data-multiplier="DW"]::before { color: #3a2410; }
.board[data-board-theme="board_coffee"] .cell[data-multiplier="TW"] { border: 2px solid #e6c068; box-shadow: inset 0 0 10px rgba(230,192,104,0.55), 0 0 8px rgba(230,192,104,0.5); }
.board[data-board-theme="board_coffee"] .cell[data-multiplier="TW"]::before { color: #6b4a10; }

/* --- Username Flair: applied per-element (leaderboard rows, share cards,
   shop previews) rather than globally, so every viewer sees YOUR flair on
   YOUR name, not just their own selection reflected back at them. --- */
.flair-flair_gold {
    background: linear-gradient(90deg, #b8860b, #ffe066, #f6d365, #ffe066, #b8860b);
    background-size: 300% 100%;
    -webkit-background-clip: text; background-clip: text; color: transparent;
    font-weight: bold;
    text-shadow: 0 0 6px rgba(246, 211, 101, 0.55);
    animation: flairGoldShimmer 3s linear infinite;
}
@keyframes flairGoldShimmer { to { background-position: 300% 0; } }

/* Real neon tubes glow white-hot at the glass itself, with the COLOR
   living in the outer halo, not the letterforms - that's why real neon
   signage (Vegas marquees, Drive's title card, Blade Runner) reads as
   near-white text with a colored glow, not solid-green text. */
.flair-flair_neon {
    color: #eafff0;
    font-weight: bold;
    text-shadow: 0 0 2px #fff, 0 0 5px #39ff14, 0 0 11px #39ff14, 0 0 22px #0f0, 0 0 36px rgba(57,255,20,0.5);
    animation: flairNeonFlicker 2.6s infinite;
}
@keyframes flairNeonFlicker {
    0%, 3%, 6%, 7%, 9%, 90%, 100% { opacity: 1; }
    4%, 8%, 91% { opacity: 0.4; }
    5% { opacity: 0.85; }
}

/* --- Fonts: real distinct Google Fonts (loaded in index.html), self-scoped
   the same way as everything else above so shop previews never inherit the
   page's actual equipped font. --- */
/* Real 8-bit game UI text (NES/Game Boy era) almost always carries a hard,
   UNBLURRED drop shadow - that crisp offset-with-zero-blur look is the
   actual signature of the era, distinct from every soft/blurred shadow
   used elsewhere in this UI. */
[data-font="font_pixel"] { font-family: 'Press Start 2P', 'Courier New', monospace; letter-spacing: -0.5px; }
[data-font="font_pixel"] .btn, [data-font="font_pixel"] h2, [data-font="font_pixel"] h3 { letter-spacing: 0; }
[data-font="font_pixel"] h2, [data-font="font_pixel"] h3 { text-shadow: 2px 2px 0 rgba(0,0,0,0.3); }
/* Generous, precise letter-spacing on headings is the actual tell of
   "expensive minimalist" type systems (Apple/Stripe/Linear-style product
   pages) - body copy stays tight and readable, headings get the room to
   breathe. */
[data-font="font_minimal"] { font-family: 'Inter', 'Segoe UI', sans-serif; letter-spacing: 0.2px; font-weight: 500; }
[data-font="font_minimal"] h2, [data-font="font_minimal"] h3 { letter-spacing: 1px; font-weight: 600; }
/* A faint engraved/carved-stone bevel (light edge above, dark edge below)
   on headings - the same beat illuminated manuscripts and fantasy-game
   title cards (Skyrim, Diablo, the Game of Thrones opening titles) use for
   lettering that's meant to read as chiseled or cast, not printed. */
[data-font="font_fantasy"] { font-family: 'Cinzel', Georgia, serif; letter-spacing: 0.4px; }
[data-font="font_fantasy"] h2, [data-font="font_fantasy"] h3 { font-weight: 700; text-shadow: 0 1px 0 rgba(255,255,255,0.18), 0 -1px 0 rgba(0,0,0,0.35); }

/* Caveat reads noticeably LIGHTER/smaller than a normal sans-serif at the
   same nominal size (the opposite problem font_pixel has, hence its
   negative letter-spacing) - sized up and given a touch of extra spacing
   here instead, plus a bump to headings specifically so they don't get
   lost against body copy the way a plain cursive upscale would. */
[data-font="font_coffee"] { font-family: 'Caveat', cursive; font-size: 1.12em; letter-spacing: 0.3px; font-weight: 600; }
[data-font="font_coffee"] .btn { letter-spacing: 0.2px; }
[data-font="font_coffee"] h2, [data-font="font_coffee"] h3 { font-weight: 700; font-size: 1.15em; }

/* =========================================
   HIGH-CONTRAST ACCESSIBILITY MODE
   ========================================= */
:root[data-high-contrast="true"] body { background: #000 !important; color: #fff !important; }
:root[data-high-contrast="true"] .cell { background-color: #000 !important; border: 2px solid #fff !important; }
:root[data-high-contrast="true"] .tile { background-color: #fff !important; color: #000 !important; border: 2px solid #000; }
:root[data-high-contrast="true"] .cell.center-star { background-color: #ff0 !important; }
:root[data-high-contrast="true"] .cell[data-multiplier="DL"] { background-color: #00f !important; }
:root[data-high-contrast="true"] .cell[data-multiplier="TL"] { background-color: #0ff !important; }
:root[data-high-contrast="true"] .cell[data-multiplier="DW"] { background-color: #f0f !important; }
:root[data-high-contrast="true"] .cell[data-multiplier="TW"] { background-color: #f00 !important; }

/* =========================================
   DYSLEXIA-FRIENDLY FONT & TACTILE PATTERN OVERLAYS
   Both free (see index.html's Settings markup) - not gated behind the
   paid Font/cosmetic shop, and !important specifically because either one
   needs to reliably win even when a paid font/board cosmetic is equipped -
   accessibility shouldn't lose a specificity fight against a purchase.
   ========================================= */
:root[data-dyslexia-font="true"] {
    font-family: 'Lexend', 'Helvetica Neue', Arial, sans-serif !important;
}

/* Distinct geometric pattern per premium-square type, layered on top of
   the existing color coding - so DL/TL/DW/TW stay distinguishable by shape
   alone for colorblind players, not just by (JS-set) background-color.
   .cell's background-color is set inline by board.js per square, so a
   background-IMAGE layer here composites on top of it rather than
   replacing it. */
:root[data-a11y-patterns="true"] .cell[data-multiplier="DL"] {
    background-image: repeating-linear-gradient(45deg, rgba(0,0,0,0.28) 0 2px, transparent 2px 8px) !important;
}
:root[data-a11y-patterns="true"] .cell[data-multiplier="TL"] {
    background-image: radial-gradient(rgba(0,0,0,0.35) 1.4px, transparent 1.4px) !important;
    background-size: 6px 6px !important;
}
:root[data-a11y-patterns="true"] .cell[data-multiplier="DW"] {
    background-image:
        repeating-linear-gradient(45deg, rgba(0,0,0,0.25) 0 2px, transparent 2px 6px),
        repeating-linear-gradient(-45deg, rgba(0,0,0,0.25) 0 2px, transparent 2px 6px) !important;
}
:root[data-a11y-patterns="true"] .cell[data-multiplier="TW"] {
    background-image: repeating-linear-gradient(90deg, rgba(0,0,0,0.32) 0 3px, transparent 3px 9px) !important;
}

/* =========================================
   SENSORY JUICE EFFECTS
   ========================================= */
.board.board-ready-glow {
    border-color: #a020f0 !important;
    box-shadow: 0 0 25px 5px rgba(160, 32, 240, 0.6), 0 10px 20px rgba(0,0,0,0.3);
    transition: border-color 0.3s, box-shadow 0.3s;
}

.tile-glint {
    position: relative;
    overflow: hidden;
}
.tile-glint::before {
    content: '';
    position: absolute; top: -50%; left: -50%; width: 200%; height: 200%;
    background: linear-gradient(45deg, transparent 40%, rgba(255,255,255,0.8) 50%, transparent 60%);
    animation: glintSweep 3s infinite;
}
@keyframes glintSweep {
    0% { transform: translate(-30%, -30%) rotate(0deg); }
    100% { transform: translate(30%, 30%) rotate(0deg); }
}

.cell-ignite {
    animation: igniteFlash 0.7s ease-out;
}
@keyframes igniteFlash {
    0% { box-shadow: 0 0 0px 0px #ffcc00; }
    30% { box-shadow: 0 0 20px 8px #ffcc00; }
    100% { box-shadow: 0 0 0px 0px transparent; }
}

.screen-shimmer {
    position: fixed; top: 0; left: 0; width: 100%; height: 100%;
    pointer-events: none; z-index: 2000;
    background: linear-gradient(120deg, transparent 30%, rgba(255,105,180,0.25) 45%, rgba(120,200,255,0.25) 55%, transparent 70%);
    background-size: 300% 300%;
    animation: shimmerFlow 1.8s ease-in-out;
}
@keyframes shimmerFlow {
    0% { background-position: 0% 0%; opacity: 0; }
    30% { opacity: 1; }
    100% { background-position: 100% 100%; opacity: 0; }
}

.milestone-burst {
    position: fixed; top: 0; left: 0; width: 100%; height: 100%;
    pointer-events: none; z-index: 2100;
    display: flex; align-items: center; justify-content: center;
}
.milestone-text {
    font-size: 2rem; font-weight: bold; color: #ff9f43;
    text-shadow: 2px 2px 0 #000; animation: scorePop 2.5s ease-out;
}
.confetti-piece {
    position: absolute; top: -10px; left: calc(var(--i) * 2.5%);
    width: 8px; height: 14px;
    background: hsl(var(--hue), 90%, 60%);
    animation: confettiFall 2.5s ease-in forwards;
    animation-delay: calc(var(--i) * 0.02s);
}
@keyframes confettiFall {
    to { transform: translateY(100vh) rotate(360deg); opacity: 0; }
}

.score-recap { text-align: center; padding: 10px; }
.recap-word { background: #eee; padding: 3px 8px; border-radius: 6px; font-weight: bold; margin: 2px; display: inline-block; }
.score-recap-total { margin-top: 8px; font-size: 1.2rem; font-weight: bold; color: #27ae60; }

/* =========================================
   BOARD.JS WIRING: MAGNETIC SNAP / RIPPLE / KINETIC RACK / RECALL SWEEP
   ========================================= */
.cell.magnetic-target {
    box-shadow: inset 0 0 0 3px #a020f0, 0 0 10px 2px rgba(160, 32, 240, 0.5);
    transition: box-shadow 0.1s ease-out;
    z-index: 5;
}

.tile-drop-ripple {
    position: absolute;
    top: 50%; left: 50%;
    width: 10px; height: 10px;
    margin: -5px 0 0 -5px;
    border-radius: 50%;
    border: 2px solid rgba(160, 32, 240, 0.8);
    pointer-events: none;
    z-index: 50;
    animation: ripplePulse 0.5s ease-out forwards;
}
@keyframes ripplePulse {
    0% { width: 10px; height: 10px; margin: -5px 0 0 -5px; opacity: 1; }
    100% { width: 90px; height: 90px; margin: -45px 0 0 -45px; opacity: 0; }
}

.cell.ripple-neighbor-pulse {
    animation: rippleNeighbor 0.5s ease-out;
}
@keyframes rippleNeighbor {
    0% { background-color: rgba(160, 32, 240, 0.35); }
    100% { background-color: transparent; }
}

/* --------------------------------------------------------------------------
   Milestone Grid Resonance: fires once, mid-game, the instant a hand's
   running total crosses the player's own personal-best pace - a board-wide
   shockwave ring plus a brief golden pulse on every high-value tile
   currently placed, landing right as the moment happens instead of waiting
   for the end-of-game recap.
   -------------------------------------------------------------------------- */
.grid-resonance-shockwave {
    position: absolute;
    inset: 0;
    border-radius: 8px;
    border: 3px solid rgba(255, 215, 0, 0.9);
    pointer-events: none;
    z-index: 40;
    animation: gridResonanceRing 900ms ease-out forwards;
}
@keyframes gridResonanceRing {
    0% { transform: scale(0.85); opacity: 0; box-shadow: 0 0 0 0 rgba(255, 215, 0, 0.5); }
    35% { opacity: 1; box-shadow: 0 0 40px 10px rgba(255, 215, 0, 0.5); }
    100% { transform: scale(1.08); opacity: 0; box-shadow: 0 0 0 0 rgba(255, 215, 0, 0); }
}
.tile.resonance-lit {
    animation: resonanceTileLit 900ms ease-out;
}
@keyframes resonanceTileLit {
    0%, 100% { filter: brightness(1); }
    40% { box-shadow: 1px 3px 2px rgba(0,0,0,0.4), 0 0 18px 6px rgba(255, 215, 0, 0.85); filter: brightness(1.35); }
}

/* Kinetic Session Resume Playback: a quick tile-by-tile materialize-in
   instead of a resumed board just appearing all at once - each tile's
   animation-delay is staggered in JS so the whole board "replays" itself
   into place over roughly half a second. */
.tile.resume-materialize {
    animation: resumeMaterialize 260ms ease-out backwards;
}
@keyframes resumeMaterialize {
    0% { transform: scale(0.3); opacity: 0; }
    70% { transform: scale(1.08); opacity: 1; }
    100% { transform: scale(1); opacity: 1; }
}

@keyframes kineticBounce {
    0% { transform: translateY(-14px) rotate(0deg) scale(0.9); opacity: 0.6; }
    55% { transform: translateY(3px) rotate(var(--kinetic-rot, 4deg)) scale(1.04); opacity: 1; }
    80% { transform: translateY(-1px) rotate(calc(var(--kinetic-rot, 4deg) * -0.4)) scale(1); }
    100% { transform: translateY(0) rotate(0deg) scale(1); }
}
.tile.kinetic-bounce {
    /* .tile's own `transition: transform 0.1s` targets the same property
       this animation drives - suppressing it here removes any chance of the
       transition fighting the animation's keyframes (e.g. easing into the
       first frame instead of the animation owning the value outright). */
    transition: none;
    animation: kineticBounce 0.45s ease-out both;
    animation-delay: var(--kinetic-delay, 0ms);
}

@keyframes recallSweep {
    0% { transform: translateY(0) scale(1); opacity: 1; }
    100% { transform: translateY(-30px) scale(0.4); opacity: 0; }
}
.tile.recall-sweep {
    animation: recallSweep 0.22s ease-in forwards;
    animation-delay: var(--sweep-delay, 0ms);
    pointer-events: none;
}

/* Touch drag ghost: follows the finger since touch devices have no native
   drag image. Sized to match a rack tile regardless of where it's dragged. */
.tile.drag-ghost {
    position: fixed;
    width: 48px;
    height: 48px;
    margin: -24px 0 0 -24px;
    pointer-events: none;
    z-index: 1500;
    opacity: 0.9;
    transform: scale(1.1);
    box-shadow: 0 8px 16px rgba(0,0,0,0.4);
}

/* =========================================
   MORE JUICE: VARIABLE REWARD / COMBO / LUCKY DRAW / TRANSITIONS / VORTEX /
   DAMPENING / DRAG TRAIL / ZEIGARNIK / GOAL-GRADIENT / AMBIENT GLOW
   ========================================= */
.variable-reward-flash {
    position: absolute; top: 0; left: 0; width: 100%; height: 100%;
    pointer-events: none; z-index: 600;
    background: radial-gradient(circle at 50% 40%, rgba(255,215,0,0.5), transparent 60%);
    animation: variableRewardPop 0.9s ease-out forwards;
}
@keyframes variableRewardPop {
    0% { opacity: 0; transform: scale(0.6); }
    30% { opacity: 1; transform: scale(1.1); }
    100% { opacity: 0; transform: scale(1.4); }
}

.combo-streak-badge {
    display: inline-block;
    font-size: 0.85rem;
    font-weight: bold;
    color: #ff6b35;
    margin-left: 8px;
}
.combo-streak-badge.combo-pulse { animation: comboPulse 0.9s infinite; }
@keyframes comboPulse {
    0%, 100% { transform: scale(1); }
    50% { transform: scale(1.15); }
}

.lucky-draw-overlay {
    position: absolute; top: 0; left: 0; width: 100%; height: 100%;
    display: flex; align-items: center; justify-content: center;
    background: rgba(0,0,0,0.55); z-index: 700; border-radius: 8px;
}
.lucky-draw-bag {
    font-size: 3rem; text-align: center; color: white;
    animation: luckyDrawSway 0.9s ease-in-out infinite;
}
.lucky-draw-label { display: block; font-size: 0.9rem; margin-top: 8px; }
@keyframes luckyDrawSway {
    0%, 100% { transform: rotate(-6deg); }
    50% { transform: rotate(6deg); }
}

@keyframes handTransitionSwipe {
    0% { transform: translateX(0); opacity: 1; }
    40% { transform: translateX(-14px); opacity: 0.7; }
    41% { transform: translateX(14px); }
    100% { transform: translateX(0); opacity: 1; }
}
.hand-transition-swipe { animation: handTransitionSwipe 0.35s ease-out; }

.tile.burn-vortex {
    animation: burnVortex 0.5s ease-in forwards;
    animation-delay: var(--vortex-delay, 0ms);
    pointer-events: none;
    z-index: 200;
}
@keyframes burnVortex {
    0% { transform: translate(0, 0) scale(1) rotate(0deg); opacity: 1; }
    100% { transform: translate(var(--vortex-dx, 0), var(--vortex-dy, 0)) scale(0.1) rotate(360deg); opacity: 0; }
}

.board.sound-dampen {
    animation: soundDampen 0.4s ease-out;
}
@keyframes soundDampen {
    0% { filter: brightness(1) saturate(1); }
    40% { filter: brightness(0.75) saturate(0.4); }
    100% { filter: brightness(1) saturate(1); }
}

.drag-trail-particle {
    position: fixed;
    width: 6px; height: 6px;
    background: radial-gradient(circle, #a020f0, transparent 70%);
    border-radius: 50%;
    pointer-events: none;
    z-index: 900;
    transform: translate(-50%, -50%);
    animation: dragTrailFade 0.4s ease-out forwards;
}
@keyframes dragTrailFade {
    0% { opacity: 0.8; transform: translate(-50%, -50%) scale(1); }
    100% { opacity: 0; transform: translate(-50%, -50%) scale(0.2); }
}

.tracker-row.zeigarnik-anchor {
    border: 1px dashed #e67e22;
    animation: zeigarnikPulse 2.2s infinite;
}
@keyframes zeigarnikPulse {
    0%, 100% { background-color: transparent; }
    50% { background-color: rgba(230, 126, 34, 0.08); }
}

.progress-bar-fill.progress-near-goal {
    background: linear-gradient(90deg, #8e44ad, #e74c3c);
    box-shadow: 0 0 8px 2px rgba(231, 76, 60, 0.6);
    transition: width 0.25s ease-out;
}

.board { position: relative; }
.board::after {
    content: '';
    position: absolute; inset: -20px;
    z-index: -1;
    border-radius: 16px;
    background: radial-gradient(circle, var(--ambient-glow-color, rgba(160, 32, 240, 0.18)), transparent 70%);
    animation: ambientBreathe 4.5s ease-in-out infinite;
    pointer-events: none;
    transition: background 1.2s ease;
}
@keyframes ambientBreathe {
    0%, 100% { opacity: 0.4; transform: scale(0.97); }
    50% { opacity: 0.8; transform: scale(1.03); }
}

/* --- Streak-Linked Ambient Shift: this "resting-heart-rate" glow starts a
   calm oceanic teal and evolves toward a deep cosmic violet as a player's
   login streak crosses the same milestones the streak_7/30/100 badges use
   (see database.DEFAULT_BADGES), so the color and the badge unlock together.
   data-streak-tier is set once per login in main.js from user_data.
   current_streak - untiered (logged out, or streak 0-6) keeps the board's
   original default purple-violet glow via the fallback above. --- */
:root[data-streak-tier="teal"] .board::after {
    --ambient-glow-color: rgba(20, 181, 175, 0.20);
}
:root[data-streak-tier="blue"] .board::after {
    --ambient-glow-color: rgba(30, 130, 210, 0.20);
}
:root[data-streak-tier="indigo"] .board::after {
    --ambient-glow-color: rgba(90, 70, 220, 0.21);
}
:root[data-streak-tier="violet"] .board::after {
    --ambient-glow-color: rgba(160, 32, 240, 0.24);
}

.board.streak-tier-flash::after {
    animation: ambientBreathe 4.5s ease-in-out infinite, streakTierFlash 1.4s ease-out;
}
@keyframes streakTierFlash {
    0% { opacity: 0.4; transform: scale(0.97); }
    30% { opacity: 1; transform: scale(1.18); }
    100% { opacity: 0.4; transform: scale(0.97); }
}

/* =========================================
   ASYNCHRONOUS LOBBY EMOTE SPARKS
   ========================================= */
.emote-spark-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 4px;
    padding: 8px 10px;
    border-top: 1px solid #eee;
    border-bottom: 1px solid #eee;
}
.emote-spark-label { font-size: 0.7rem; color: #888; width: 100%; margin-bottom: 2px; }
.emote-spark-btn {
    background: none;
    border: 1px solid transparent;
    border-radius: 6px;
    font-size: 1.1rem;
    padding: 2px 5px;
    cursor: pointer;
    transition: transform 0.12s ease, background 0.12s ease;
}
.emote-spark-btn:hover { background: #f0f0f0; transform: scale(1.15); }
.emote-spark-btn:active { transform: scale(0.95); }
.emote-spark-btn:disabled { opacity: 0.4; cursor: default; transform: none; }

/* Social Peer Nudges */
.nudge-list {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 8px 10px;
    border-top: 1px solid #eee;
    border-bottom: 1px solid #eee;
}
.nudge-list-label { font-size: 0.7rem; color: #888; margin-bottom: 2px; }
.nudge-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-size: 0.8rem;
}
.nudge-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-right: 8px; }
.nudge-btn {
    padding: 3px 10px;
    font-size: 0.72rem;
    flex-shrink: 0;
}
.nudge-btn:disabled { opacity: 0.6; }

/* Fixed to a corner, small, and self-dismissing - this must never compete
   with the board for attention, since the whole point is "non-disruptive". */
.emote-burst-container {
    position: fixed;
    bottom: 16px;
    right: 16px;
    z-index: 950;
    display: flex;
    flex-direction: column-reverse;
    gap: 6px;
    pointer-events: none;
}
.emote-burst-chip {
    display: flex;
    align-items: center;
    gap: 6px;
    background: rgba(20, 20, 30, 0.82);
    color: #fff;
    border-radius: 20px;
    padding: 6px 12px 6px 8px;
    font-size: 0.78rem;
    box-shadow: 0 4px 14px rgba(0,0,0,0.35);
    animation: emoteBurstPop 3.2s ease-out forwards;
}
.emote-burst-chip .emote-burst-emoji {
    font-size: 1.3rem;
    animation: emoteBurstBounce 0.6s ease-in-out 2;
}
@keyframes emoteBurstPop {
    0% { opacity: 0; transform: translateY(10px) scale(0.85); }
    10% { opacity: 1; transform: translateY(0) scale(1); }
    80% { opacity: 1; transform: translateY(0) scale(1); }
    100% { opacity: 0; transform: translateY(-6px) scale(0.95); }
}
@keyframes emoteBurstBounce {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(-4px); }
}

/* =========================================
   SOCIAL COMPARISON / COLLECTION REMINDERS / PRESTIGE
   ========================================= */
.social-comparison-panel {
    padding: 8px 15px;
    background: #fdf6e3;
    border-top: 1px solid #eee;
    border-bottom: 1px solid #eee;
}
.social-comparison-row {
    display: flex; justify-content: space-between;
    font-size: 0.8rem; padding: 3px 0;
}
.social-comparison-gap { color: #e67e22; font-weight: bold; }

.collection-locked-card { opacity: 0.55; filter: grayscale(0.6); }
.collection-silhouette {
    font-size: 1.5rem; font-weight: bold; color: #999;
    background: #ddd; border-radius: 50%;
    width: 36px; height: 36px; line-height: 36px;
    margin: 0 auto 6px auto;
}

.prestige-section { margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee; text-align: center; }
.prestige-tier-label { font-weight: bold; color: #8e44ad; margin-bottom: 6px; }

/* =========================================
   FORTNITE-STYLE ITEM SHOP
   ========================================= */
.modal-box.item-shop {
    background: linear-gradient(160deg, #0f0c29, #1a1440 45%, #24123f 100%);
    color: #f0eaff;
    border: 1px solid #4a2f7a;
    box-shadow: 0 0 40px rgba(123, 44, 191, 0.5), 0 10px 30px rgba(0,0,0,0.5);
}
.item-shop-title {
    font-size: 1.4rem;
    letter-spacing: 1px;
    color: #fff;
    text-shadow: 0 0 12px rgba(155, 89, 182, 0.8);
}
.item-shop-title span { color: #ffe066; }
.item-shop-subtitle { font-size: 0.8rem; color: #c9bfe0 !important; margin-bottom: 14px; }
.item-shop .modal-close-x { color: #c9bfe0; }

.store-featured-label {
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 1.5px;
    color: #ffe066;
    margin-bottom: 8px;
    font-weight: bold;
}
.store-featured { margin-bottom: 16px; }

.shop-tabs, .store-tabs { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; }
.shop-tab-btn {
    background: rgba(255,255,255,0.08);
    border: 1px solid rgba(255,255,255,0.15);
    color: #e5dcff;
    padding: 5px 14px;
    border-radius: 20px;
    font-size: 0.75rem;
    text-transform: capitalize;
    cursor: pointer;
}
.shop-tab-btn.active { background: #8e44ad; border-color: #b370d6; color: white; font-weight: bold; }
.shop-tab-btn:hover { background: rgba(142, 68, 173, 0.5); }

.item-shop .store-category h4 { color: #b892e8; font-size: 0.75rem; letter-spacing: 1px; margin-top: 10px; }

/* =========================================
   SHARED DARK-MODAL POLISH
   Settings/Clans/Duels/Post-Game all now use the same .item-shop theme as
   Store/Wardrobe (see the "FORTNITE-STYLE ITEM SHOP" block above) rather
   than a plain white modal-box - these rules make their plain HTML form
   controls, section labels, and list rows match that theme instead of
   rendering as default-white-on-dark (illegible) or plain white boxes
   (inconsistent). Scoped under .item-shop so Store/Wardrobe's own already-
   correct styling is untouched.
   ========================================= */
.item-shop p { color: #d8cfee; }
.item-shop label { color: #e5dcff; }
.item-shop hr { border-top: 1px solid rgba(255,255,255,0.15); }

.item-shop select,
.item-shop input[type="text"],
.item-shop input[type="number"] {
    background: rgba(255,255,255,0.08);
    border: 1px solid rgba(255,255,255,0.25);
    color: #f0eaff;
    border-radius: 6px;
    padding: 7px 10px;
    font-size: 0.85rem;
}
.item-shop select:focus,
.item-shop input:focus {
    outline: none;
    border-color: #8e44ad;
}
.item-shop input::placeholder { color: rgba(240,234,255,0.45); }
/* The dropdown list itself is OS-rendered and mostly ignores <select>'s own
   background in Chrome/Edge - forcing dark text keeps it legible against
   that native (usually white) popup regardless of browser support level. */
.item-shop select option { color: #1a1440; }

.settings-section { margin-top: 18px; }
.settings-section:first-child { margin-top: 0; }
.settings-hint { font-size: 0.72rem; color: #a89bc9; margin-top: 4px; }
.archive-msg, #rival-msg, #clan-msg, #duel-msg { font-size: 0.8rem; margin-top: 6px; min-height: 1em; }
.item-shop .prestige-section { border-top-color: rgba(255,255,255,0.15); }

/* Clan leaderboard / duel list rows: same dark card language as the shop's
   item cards, not the plain white-bordered rows those classes render as
   everywhere else (the in-game sidebar leaderboard, tracker rows). */
.item-shop .leaderboard-row,
.item-shop .duel-list-row {
    background: rgba(255,255,255,0.05);
    border: 1px solid rgba(255,255,255,0.12);
    border-radius: 8px;
    margin-bottom: 6px;
    color: #f0eaff;
}
.item-shop .leaderboard-row:hover,
.item-shop .duel-list-row:hover {
    background: rgba(255,255,255,0.1);
}

/* Horizontal scrolling row (Fortnite item-shop style) instead of a
   vertically-wrapping grid - cards keep a fixed width and the row scrolls
   sideways, on desktop AND touch (swipe) alike. */
.shop-grid {
    display: flex;
    gap: 14px;
    overflow-x: auto;
    overflow-y: hidden;
    padding-bottom: 10px;
    scroll-snap-type: x proximity;
    scrollbar-width: thin;
    scrollbar-color: #8e44ad rgba(255,255,255,0.08);
}
.shop-grid::-webkit-scrollbar { height: 8px; }
.shop-grid::-webkit-scrollbar-track { background: rgba(255,255,255,0.06); border-radius: 4px; }
.shop-grid::-webkit-scrollbar-thumb { background: #8e44ad; border-radius: 4px; }

.shop-item-card {
    position: relative;
    flex: 0 0 160px;
    scroll-snap-align: start;
    background: linear-gradient(160deg, #241b3f, #1a1330);
    border-radius: 10px;
    padding: 14px 10px;
    text-align: center;
    border: 2px solid #3a2a5c;
    overflow: hidden;
    transition: transform 0.15s ease-out, box-shadow 0.15s ease-out;
}
.shop-item-card:hover { transform: translateY(-4px); }

.shop-item-art {
    font-size: 2.2rem;
    margin-bottom: 6px;
    filter: drop-shadow(0 0 6px rgba(255,255,255,0.25));
}

/* --------------------------------------------------------------------
   Live cosmetic previews: these reuse the EXACT same classes/attributes
   the real game uses (.board/.cell/.tile, data-board-theme etc.), just
   scaled down inside a small wrapper - so a preview is never an
   approximation, it's the real CSS rendered small.
   -------------------------------------------------------------------- */
.shop-item-preview {
    width: 100%;
    height: 92px;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Every other preview type is one fixed-size element (a mini board, a
       tile row, one flair/font sample) that never needs this - only the
       title preview pairs a badge with a variable-length "PlayerName"
       sample in the same row, and a long title name/badge combination
       doesn't reliably fit on one line at this width. Wrapping instead of
       clipping means a long title still reads completely (two lines) at
       the cost of vertical centering looking slightly less tidy, which
       beats silently cutting text off under overflow:hidden below. */
    flex-wrap: wrap;
    row-gap: 2px;
    margin-bottom: 8px;
    border-radius: 6px;
    overflow: hidden;
    background: rgba(0,0,0,0.25);
}

.board.mini-board-preview {
    width: 82px !important;
    max-width: 82px !important;
    height: 82px !important;
    aspect-ratio: unset;
    grid-template-columns: repeat(5, 1fr) !important;
    grid-template-rows: repeat(5, 1fr) !important;
    border-width: 4px;
    padding: 1px;
    gap: 1px;
    pointer-events: none;
    box-shadow: none;
}
.board.mini-board-preview::before { inset: -4px; filter: blur(1px) saturate(1.3); }
.board.mini-board-preview::after { inset: -1px; }
.board.mini-board-preview .cell::before { font-size: 0; } /* DL/TL labels unreadable at this size - hide them */
.board.mini-board-preview .cell.center-star::before { font-size: 0.5rem; }
/* Neon ring thickness scaled down to match the smaller inset above */
.board.mini-board-preview[data-board-theme="board_neon"]::before { padding: 4px; }
/* Medieval castle: towers/crenellations/arrow-slits live in `background`
   itself now (see the main rule), so they already show up here - this just
   shrinks those accent layers to match the smaller border, in the exact
   same layer order as the main rule (position/repeat unchanged, only size
   shrunk, except the arrow slits which also need closer-in offsets). */
.board.mini-board-preview[data-board-theme="board_medieval"] {
    border-width: 6px;
    background-size: 16px 7px, 16px 7px, 22px 22px, 22px 22px, 22px 22px, 22px 22px,
                      2px 9px, 2px 9px, 2px 9px, 2px 9px, auto, auto, auto, auto, auto, auto;
    background-position: top, bottom, top left, top right, bottom left, bottom right,
                          8px 5px, calc(100% - 8px) 5px, 8px calc(100% - 5px), calc(100% - 8px) calc(100% - 5px),
                          0 0, 0 0, 0 0, 0 0, 0 0, 0 0;
}
/* The generic .board.mini-board-preview::before rule above (inset: -4px,
   for the neon ring) would otherwise override the gate's own bottom/left
   positioning - re-anchor it here, scaled to the smaller board. */
.board.mini-board-preview[data-board-theme="board_medieval"]::before {
    inset: auto;
    bottom: -6px;
    left: 50%;
    transform: translateX(-50%);
    width: 18%;
    min-width: 12px;
    max-width: 20px;
    height: 12px;
    filter: none;
}

.mini-tile-row { display: flex; gap: 5px; }
.mini-tile-row .tile { width: 34px; height: 34px; font-size: 1.05rem; box-shadow: 1px 2px 2px rgba(0,0,0,0.3), inset 1px 1px 1px rgba(255,255,255,0.6); }

.flair-preview-text, .font-preview-text {
    font-size: 1.3rem;
    font-weight: bold;
}
/* Font previews have no color-setting cosmetic class of their own, so they
   need this default; flair previews always pair with a .flair-* class that
   must win instead (hence no color set here - it would tie in specificity
   with .flair-flair_gold/.flair-flair_neon and clobber them via source order). */
.font-preview-text { color: #fff; }
/* The TITLE preview (buildPreviewHtml's "title" case) reuses this same
   class for its "PlayerName" half, but there it sits in a row NEXT TO a
   title-badge inside a ~144px-wide preview box, not alone - 1.3rem was
   sized for a lone flair/font sample and doesn't leave room for a badge
   beside it, clipping both under .shop-item-preview's overflow:hidden.
   This was already true for any long achievement title (e.g. "Grandmaster
   of the Rack") shown in the Wardrobe; just never surfaced as a purchasable
   card until title_crt made the Item Shop render a title preview for the
   first time. Scoped to only the badge+name pairing, so a standalone flair
   preview elsewhere keeps its full 1.3rem. */
.shop-item-preview .title-badge + .flair-preview-text {
    font-size: 0.85rem;
}

.audio-preview-btn {
    background: rgba(255,255,255,0.12);
    border: 1px solid rgba(255,255,255,0.3);
    color: #fff;
    border-radius: 50%;
    width: 44px; height: 44px;
    font-size: 1.1rem;
    cursor: pointer;
}
.audio-preview-btn:hover { background: rgba(255,255,255,0.25); }
.shop-rarity-label {
    font-size: 0.65rem;
    text-transform: uppercase;
    letter-spacing: 1px;
    font-weight: bold;
    margin-bottom: 4px;
}
.shop-item-name { font-weight: bold; font-size: 0.85rem; margin-bottom: 4px; color: #fff; }
.shop-item-price { font-size: 0.75rem; color: #c9bfe0; margin-bottom: 8px; }
.shop-bundle-contents { font-size: 0.68rem; color: #a89bc9; margin: -2px 0 6px; line-height: 1.3; }

.shop-featured-ribbon {
    position: absolute;
    top: 8px; right: -26px;
    background: #ffe066;
    color: #241b3f;
    font-size: 0.6rem;
    font-weight: bold;
    padding: 2px 30px;
    transform: rotate(35deg);
}

/* Rarity tiers: border + glow color scales with price, mirroring the
   familiar gray/blue/purple/gold rarity language of live-service shops. */
.shop-item-card.rarity-standard { border-color: #5a5a6a; }
.shop-item-card.rarity-rare { border-color: #3399ff; box-shadow: 0 0 12px rgba(51, 153, 255, 0.35); }
.shop-item-card.rarity-epic { border-color: #a020f0; box-shadow: 0 0 14px rgba(160, 32, 240, 0.45); }
.shop-item-card.rarity-legendary {
    border-color: #ffb703;
    box-shadow: 0 0 18px rgba(255, 183, 3, 0.55);
    background: linear-gradient(160deg, #3a2a10, #241b3f);
}
/* Mythical: the tier above Legendary - a shifting prismatic gradient
   (magenta/gold/cyan, echoing the Celestial Nebula/Royal/Molten Obsidian
   trio's own palettes) instead of one fixed color, so it visibly outranks
   every other tier at a glance rather than just being "gold but more". */
.shop-item-card.rarity-mythical {
    border-color: #ff2e88;
    box-shadow: 0 0 22px rgba(255, 46, 136, 0.55), 0 0 10px rgba(0, 229, 255, 0.35);
    background: linear-gradient(160deg, #2a0a3d, #0a1a3a, #3a0a2a);
    background-size: 200% 200%;
    animation: mythicalShimmer 4s ease-in-out infinite;
}
@keyframes mythicalShimmer {
    0%, 100% { background-position: 0% 50%; }
    50% { background-position: 100% 50%; }
}
.shop-item-card.rarity-standard .shop-rarity-label { color: #9a9aa8; }
.shop-item-card.rarity-rare .shop-rarity-label { color: #3399ff; }
.shop-item-card.rarity-epic .shop-rarity-label { color: #c77dff; }
.shop-item-card.rarity-legendary .shop-rarity-label { color: #ffb703; }
.shop-item-card.rarity-mythical .shop-rarity-label {
    background: linear-gradient(90deg, #ff2e88, #ffb703, #00e5ff, #ff2e88);
    background-size: 300% 100%;
    -webkit-background-clip: text; background-clip: text; color: transparent;
    animation: mythicalShimmer 3s linear infinite;
    font-weight: bold;
}

.shop-item-card.shop-item-featured { border-width: 2px; flex-basis: 190px; }

.item-shop .store-buy-btn { width: 100%; padding: 6px; font-size: 0.8rem; }
.item-shop .store-owned-badge { background: #27ae60; color: white; padding: 3px 8px; border-radius: 10px; font-size: 0.7rem; }

.shop-item-actions { display: flex; flex-direction: column; gap: 6px; }
.shop-item-actions .store-buy-btn, .shop-item-actions .store-buy-money-btn { width: 100%; padding: 6px; font-size: 0.75rem; }

/* =========================================
   WARDROBE (shares the item-shop dark theme + card/preview language)
   ========================================= */
.wardrobe-section-heading { color: #b892e8; font-size: 0.85rem; letter-spacing: 1px; margin-top: 18px; margin-bottom: 8px; }
.wardrobe-category { margin-bottom: 14px; }
.wardrobe-category h4 { color: #b892e8; font-size: 0.75rem; letter-spacing: 1px; margin-bottom: 8px; }

.wardrobe-card { cursor: pointer; transition: transform 0.15s ease-out, box-shadow 0.15s ease-out; }
.wardrobe-card:hover { transform: translateY(-4px); }
/* outline, not border-color/box-shadow: those properties are already
   claimed by the .rarity-* tiers above (same specificity, so whichever
   rule happened to win the cascade tie would fully replace the other's
   border-color/box-shadow) - a legendary/mythical item's whole reason for
   costing more is its distinctive glow, and the equipped indicator
   shouldn't be able to flatten that into a plain green box no matter
   which rule wins. outline paints as its own separate box, so it layers
   on top of any rarity glow instead of competing with it. */
.wardrobe-card.wardrobe-card-selected {
    outline: 3px solid #2ecc71;
    outline-offset: 2px;
}
.wardrobe-equipped-badge {
    display: inline-block; background: #27ae60; color: white;
    padding: 3px 8px; border-radius: 10px; font-size: 0.7rem; font-weight: bold;
}

/* The "None" pseudo-card (categories with no free item to click back to -
   see cosmetics.js's buildUnequipCard) - dashed border and dimmer art so
   it reads as a reset control, not just another item in the row. */
.wardrobe-unequip-card {
    background: rgba(255,255,255,0.03);
    border-style: dashed !important;
    box-shadow: none !important;
}
.wardrobe-unequip-card .shop-item-art { opacity: 0.6; }
.wardrobe-equip-hint {
    display: inline-block; color: #a08fc4; font-size: 0.7rem;
}

/* Customizable Letter Points Overlay */
:root[data-hide-points="true"] .tile::after { display: none; }

/* =========================================
   DESKTOP NO-SCROLL LAYOUT
   Placed at the very end of the file (after every rule it overrides -
   .board, .game-layout, .center-column, .site-header, .horizontal-ad,
   .community-goal-bar) deliberately: every selector below has the exact
   same specificity as its base-rule counterpart earlier in the file, and
   CSS resolves equal-specificity ties by source order, so this block has
   to come after all of them to actually win.
   ========================================= */
/* On screens tall/wide enough to plausibly fit the whole game (see the
   min-height/min-width guard - a short or narrow viewport falls through to
   normal page scrolling instead, which stays strictly more usable than a
   game area squeezed into an impossibly small box), the page itself never
   scrolls. The header/ad/community-bar stack is fixed-height chrome;
   .game-layout takes exactly what's left via flex:1 and JS (see
   fitBoardToViewport in main.js) sizes the board to whatever fits inside
   that. Each sidebar column keeps its own overflow-y:auto as a safety net
   so a long leaderboard/ad stack scrolls internally rather than ever
   pushing the page taller than the viewport. */
@media screen and (min-height: 700px) and (min-width: 1051px) {
    html, body {
        height: 100dvh;
        overflow-y: hidden;
    }
    body { justify-content: flex-start; }

    .site-header { padding: 6px 20px; }
    .community-goal-bar { padding: 4px 20px; }
    .community-goal-label { font-size: 0.75rem; margin-bottom: 2px; }
    /* Compact, since it now competes with the board/rack/buttons for the
       same constrained .center-column space rather than sitting in the
       fixed chrome above everything. */
    .horizontal-ad { height: 50px; margin: 6px 0 0 0; }

    /* .page-shell (ad-rail | .game-layout | ad-rail) is body's actual flex
       child now, not .game-layout directly - it's the one that needs
       flex:1 to fill whatever's left below the header/community-bar, with
       .game-layout then filling 100% of THAT. Same auto-row-track trap as
       before applies at this level too (see the .game-layout comment
       below), so page-shell needs the same minmax(0,1fr) treatment via its
       own display:flex (already row-direction, so align-items:stretch - the
       default - already makes children match its height; the min-height:0
       here is what stops a tall ad-rail from inflating that height instead
       of being capped and scrolling internally). */
    .page-shell {
        flex: 1;
        min-height: 0;
    }
    .ad-rail {
        max-height: 100%;
        overflow: hidden;
    }

    .game-layout {
        height: 100%;
        /* Without an explicit row track, CSS Grid's default auto-sized row
           grows to fit whatever the center column's content wants (the
           board's own default max-width) instead of being capped by the
           space it was actually given - that circularity is what let the
           board render past the viewport with nothing (not even a
           scrollbar) to reach it. Pinning the row to the container's own
           resolved height breaks the loop: now the row - and every grid
           item's height:100% - is genuinely capped, so JS can measure the
           real available space instead of an inflated one. */
        grid-template-rows: minmax(0, 1fr);
        padding-top: 10px;
        padding-bottom: 10px;
        gap: 20px;
    }
    .left-column, .right-column {
        height: 100%;
        overflow-y: auto;
        scrollbar-width: thin;
    }
    .center-column {
        height: 100%;
        /* flex-start, not center: the board caps at 600px (see
           fitBoardToViewport in main.js), so on a tall viewport there's
           often leftover height in this column after that cap - centering
           split that leftover evenly above AND below the content, pushing
           the board down away from the header/community-bar with a big gap
           above it. Anchoring to the top puts all the leftover space below
           instead, right where a taller board would have used it anyway. */
        justify-content: flex-start;
        overflow: hidden;
    }

    /* The board's actual pixel size is set inline via --board-fit-size,
       computed by fitBoardToViewport() in main.js from real measured space
       (ads/community-bar/header can all reflow, e.g. once AdSense actually
       fills a slot, so a static CSS guess would drift out of sync) - this
       is just the ceiling so it never re-grows past the normal desktop max. */
    .board {
        max-width: var(--board-fit-size, 600px);
        max-height: var(--board-fit-size, 600px);
        width: var(--board-fit-size, 600px);
    }
}

/* Asynchronous Two-Player Grid Duels */
.duel-hud {
    background: #2c3e50;
    color: #fff;
    border-radius: 10px;
    padding: 10px 14px;
    margin-bottom: 12px;
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.duel-hud-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-size: 0.85rem;
}
.duel-turn-indicator {
    font-weight: bold;
    padding: 2px 8px;
    border-radius: 6px;
    background: #7f8c8d;
}
.duel-turn-indicator.my-turn { background: #27ae60; }
.duel-turn-indicator.their-turn { background: #7f8c8d; }
.duel-turn-indicator.duel-over { background: #f39c12; }
.duel-hud-btn { padding: 4px 10px; font-size: 0.75rem; }
.duel-status-msg { font-size: 0.75rem; color: #f1c40f; min-height: 1em; }

#duel-create-section { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.duel-created-code {
    font-family: monospace;
    font-size: 1rem;
    background: #eef2f7;
    padding: 4px 10px;
    border-radius: 6px;
    letter-spacing: 1px;
}
#duel-join-section { display: flex; gap: 8px; }
#duel-join-section input { flex: 1; }
#duel-msg { font-size: 0.8rem; margin-top: 8px; min-height: 1em; }

#duel-list-body { display: flex; flex-direction: column; gap: 8px; }
.duel-list-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 8px 10px;
    border: 1px solid #eee;
    border-radius: 8px;
    font-size: 0.82rem;
}
.duel-list-row .duel-list-status { font-size: 0.72rem; color: #888; }
.duel-list-row .duel-list-status.duel-your-turn { color: #27ae60; font-weight: bold; }
/* #888 reads fine on the old white row background but is low-contrast
   against the new dark card rows - matches the same muted lavender used
   for hints/subtitles throughout the item-shop theme instead. */
.item-shop .duel-list-row .duel-list-status { color: #a89bc9; }