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

    /* CSS Custom Properties for responsive scaling */
    :root {
      /* Mobile-first defaults */
      --container-max-width: 520px;
      --tile-size: 56px;
      --font-xs: 7px;
      --font-sm: 8px;
      --font-base: 10px;
      --font-md: 12px;
      --font-lg: 14px;
      --font-xl: 16px;
      --font-2xl: 20px;
      --spacing-xs: 4px;
      --spacing-sm: 8px;
      --spacing-md: 12px;
      --spacing-lg: 16px;
      --spacing-xl: 24px;
      --button-size: 36px;
      --battle-height: 120px;
      --sprite-scale: 1;
    }

    /* Tablet - medium screens */
    @media (min-width: 768px) {
      :root {
        --container-max-width: 680px;
        --tile-size: 60px;
        --font-xs: 9px;
        --font-sm: 10px;
        --font-base: 12px;
        --font-md: 14px;
        --font-lg: 16px;
        --font-xl: 20px;
        --font-2xl: 24px;
        --spacing-sm: 10px;
        --spacing-md: 16px;
        --spacing-lg: 20px;
        --spacing-xl: 32px;
        --button-size: 44px;
        --battle-height: 150px;
        --sprite-scale: 1.2;
      }
    }

    /* Desktop - large screens */
    @media (min-width: 1024px) {
      :root {
        --container-max-width: 900px;
        --tile-size: 64px;
        --font-xs: 11px;
        --font-sm: 12px;
        --font-base: 14px;
        --font-md: 16px;
        --font-lg: 20px;
        --font-xl: 24px;
        --font-2xl: 32px;
        --spacing-sm: 12px;
        --spacing-md: 20px;
        --spacing-lg: 24px;
        --spacing-xl: 40px;
        --button-size: 48px;
        --battle-height: 180px;
        --sprite-scale: 1.5;
      }
    }

    /* Large desktop - 1440p+ */
    @media (min-width: 1440px) {
      :root {
        --container-max-width: 1000px;
        --tile-size: 72px;
        --font-xs: 12px;
        --font-sm: 14px;
        --font-base: 16px;
        --font-md: 18px;
        --font-lg: 22px;
        --font-xl: 28px;
        --font-2xl: 36px;
        --spacing-sm: 14px;
        --spacing-md: 24px;
        --spacing-lg: 28px;
        --spacing-xl: 48px;
        --button-size: 52px;
        --battle-height: 200px;
        --sprite-scale: 1.8;
      }
    }

    body {
      background-color: #0f0f1a;
      image-rendering: pixelated;
      image-rendering: crisp-edges;
    }
    #root {
      max-width: var(--container-max-width);
      margin: 0 auto;
    }

    /* Bonfire animation */
    @keyframes flicker {
      0%, 100% { opacity: 1; transform: scale(1); }
      25% { opacity: 0.9; transform: scale(1.02, 0.98); }
      50% { opacity: 1; transform: scale(0.98, 1.02); }
      75% { opacity: 0.85; transform: scale(1.01, 0.99); }
    }

    @keyframes ember {
      0% { transform: translateY(0) scale(1); opacity: 1; }
      100% { transform: translateY(-8px) scale(0.5); opacity: 0; }
    }

    /* Grass sway animation */
    @keyframes grassSway {
      0%, 100% { transform: skewX(0deg); }
      50% { transform: skewX(2deg); }
    }

    /* Combat juice animations */
    @keyframes damageNumber {
      0% { opacity: 1; transform: translateY(0); }
      100% { opacity: 0; transform: translateY(-30px); }
    }

    @keyframes screenShake {
      0%, 100% { transform: translateX(0); }
      20% { transform: translateX(-4px); }
      40% { transform: translateX(4px); }
      60% { transform: translateX(-3px); }
      80% { transform: translateX(3px); }
    }

    @keyframes criticalFlash {
      0%, 100% { background-color: transparent; }
      50% { background-color: rgba(255, 255, 255, 0.4); }
    }

    @keyframes hpPulse {
      0%, 100% { box-shadow: 0 0 0 0 rgba(252, 129, 129, 0); }
      50% { box-shadow: 0 0 8px 2px rgba(252, 129, 129, 0.6); }
    }

    @keyframes staminaWarning {
      0%, 100% { opacity: 1; }
      50% { opacity: 0.6; }
    }

    /* Torch flicker animation - Fallen Keep */
    @keyframes torchFlicker {
      0%, 100% { opacity: 1; filter: brightness(1); }
      15% { opacity: 0.85; filter: brightness(0.9); }
      30% { opacity: 1; filter: brightness(1.1); }
      45% { opacity: 0.9; filter: brightness(0.95); }
      60% { opacity: 1; filter: brightness(1.05); }
      75% { opacity: 0.8; filter: brightness(0.85); }
      90% { opacity: 0.95; filter: brightness(1); }
    }

    /* Void pulse animation - Hollow Deep */
    @keyframes voidPulse {
      0%, 100% {
        box-shadow: inset 0 0 15px rgba(159, 122, 234, 0.3);
        filter: brightness(1);
      }
      50% {
        box-shadow: inset 0 0 25px rgba(159, 122, 234, 0.6);
        filter: brightness(1.1);
      }
    }

    /* Maze ambient glow - Labyrinth */
    @keyframes mazeGlow {
      0%, 100% {
        box-shadow: inset 0 0 8px rgba(246, 224, 94, 0.2);
      }
      50% {
        box-shadow: inset 0 0 16px rgba(246, 224, 94, 0.4);
      }
    }

    /* Secret door shimmer */
    @keyframes secretShimmer {
      0%, 100% {
        opacity: 0;
        background: linear-gradient(90deg, transparent, rgba(246, 224, 94, 0.3), transparent);
      }
      50% {
        opacity: 1;
        background: linear-gradient(90deg, transparent, rgba(246, 224, 94, 0.5), transparent);
      }
    }

    /* Ashen wind effect - Ashen Path */
    @keyframes ashenWind {
      0%, 100% { transform: translateX(0) rotate(0deg); opacity: 0.6; }
      25% { transform: translateX(2px) rotate(1deg); opacity: 0.4; }
      50% { transform: translateX(-1px) rotate(-1deg); opacity: 0.7; }
      75% { transform: translateX(1px) rotate(0.5deg); opacity: 0.5; }
    }

    /* Lore indicator pulse */
    @keyframes lorePulse {
      0%, 100% { transform: scale(1); opacity: 0.7; }
      50% { transform: scale(1.2); opacity: 1; }
    }

    /* Gathering node pulse: slower gold beckon than lorePulse */
    @keyframes gatherPulse {
      0%, 100% { transform: scale(1); opacity: 0.6; }
      50% { transform: scale(1.3); opacity: 1; }
    }

    /* Subtle shimmer on available (not depleted) gathering nodes */
    @keyframes gather-shimmer {
      0%, 100% { filter: brightness(1); }
      50% { filter: brightness(1.15); }
    }

    .gather-available {
      animation: gather-shimmer 3s ease-in-out infinite;
    }

    /* FOE aura: amber while patrolling, faster and red once it has your
       scent. Drop-shadow only, so it reads through fog dim without
       repainting the tile underneath. */
    @keyframes foePulsePatrol {
      0%, 100% { filter: drop-shadow(0 0 3px rgba(240, 160, 72, 0.55)); }
      50% { filter: drop-shadow(0 0 8px rgba(240, 160, 72, 0.9)); }
    }

    @keyframes foePulseChase {
      0%, 100% { filter: drop-shadow(0 0 4px rgba(220, 60, 60, 0.7)); }
      50% { filter: drop-shadow(0 0 11px rgba(255, 90, 90, 1)); }
    }

    .foe-aura-patrol { animation: foePulsePatrol 2.2s ease-in-out infinite; }
    .foe-aura-chase { animation: foePulseChase 0.9s ease-in-out infinite; }

    /* Destination flash for tap-to-path */
    @keyframes destinationFlash {
      0%, 20% { box-shadow: inset 0 0 0 2px rgba(246, 224, 94, 0.7); }
      100% { box-shadow: inset 0 0 0 2px rgba(246, 224, 94, 0); }
    }

    /* Gentle breathing for idle sprites (player, NPCs) */
    @keyframes idleBob {
      0%, 100% { transform: translateY(0); }
      50% { transform: translateY(-1.5px); }
    }

    /* Footstep dust kicked up on the tile just left behind */
    @keyframes dustPuff {
      0% { opacity: 0.7; transform: scale(0.5) translateY(0); }
      100% { opacity: 0; transform: scale(1.5) translateY(-6px); }
    }

    /* White flash on battle entry */
    @keyframes battle-flash {
      0% { opacity: 1; }
      100% { opacity: 0; }
    }

    .battle-flash-overlay {
      position: fixed;
      top: 0; left: 0; right: 0; bottom: 0;
      background: rgba(255, 255, 255, 0.3);
      z-index: 3000;
      pointer-events: none;
      animation: battle-flash 0.4s ease-out forwards;
    }

    /* --- Battle juice --- */

    /* Attacker lunges at the foe and snaps back */
    @keyframes lungeRight {
      0%, 100% { transform: translateX(0); }
      35% { transform: translateX(14px) scale(1.06); }
      55% { transform: translateX(10px); }
    }

    @keyframes lungeLeft {
      0%, 100% { transform: translateX(0); }
      35% { transform: translateX(-14px) scale(1.06); }
      55% { transform: translateX(-10px); }
    }

    /* Defender takes the hit: white blowout + knockback */
    @keyframes hitFlashRight {
      0%, 100% { transform: translateX(0); filter: none; }
      10% { transform: translateX(8px); filter: brightness(4) saturate(0); }
      30% { transform: translateX(4px); filter: brightness(2) saturate(0.4); }
      60% { transform: translateX(-2px); filter: none; }
    }

    @keyframes hitFlashLeft {
      0%, 100% { transform: translateX(0); filter: none; }
      10% { transform: translateX(-8px); filter: brightness(4) saturate(0); }
      30% { transform: translateX(-4px); filter: brightness(2) saturate(0.4); }
      60% { transform: translateX(2px); filter: none; }
    }

    /* Boss winding up a telegraphed blow */
    @keyframes telegraphCharge {
      0%, 100% { filter: brightness(1); transform: scale(1); }
      50% { filter: brightness(1.6) drop-shadow(0 0 8px rgba(246, 224, 94, 0.9)); transform: scale(1.08); }
    }

    /* GBC-style encounter wipe */
    @keyframes battleWipe {
      0% { clip-path: inset(0 0 100% 0); opacity: 1; }
      45% { clip-path: inset(0 0 0 0); opacity: 1; }
      100% { clip-path: inset(0 0 0 0); opacity: 0; }
    }

    /* Drifting ash (Ashen Path). Travel is in px because the motes are tiny:
       percentage translate is relative to the mote's own box, not the map. */
    @keyframes ashFall {
      0% { transform: translate(0, -10px); opacity: 0; }
      15% { opacity: 0.7; }
      85% { opacity: 0.5; }
      100% { transform: translate(14px, 640px); opacity: 0; }
    }

    /* Embers climbing from the torchlit dark (Fallen Keep) */
    @keyframes emberRise {
      0% { transform: translate(0, 10px); opacity: 0; }
      10% { opacity: 0.8; }
      60% { opacity: 0.6; }
      100% { transform: translate(8px, -640px); opacity: 0; }
    }

    /* Void motes drifting up slow (Hollow Deep) */
    @keyframes voidDrift {
      0% { transform: translate(0, 10px); opacity: 0; }
      20% { opacity: 0.6; }
      80% { opacity: 0.4; }
      100% { transform: translate(-12px, -380px); opacity: 0; }
    }

    /* Spores tumbling down through the green (Labyrinth) */
    @keyframes sporeFall {
      0% { transform: translate(0, -8px) rotate(0deg); opacity: 0; }
      15% { opacity: 0.55; }
      50% { transform: translate(10px, 300px) rotate(120deg); opacity: 0.5; }
      100% { transform: translate(-4px, 640px) rotate(240deg); opacity: 0; }
    }

    /* A tile charting out of the fog: brief bright desaturated settle */
    @keyframes tileReveal {
      0% { opacity: 0; filter: brightness(2.4) saturate(0); }
      60% { opacity: 1; filter: brightness(1.3) saturate(0.6); }
      100% { opacity: 1; filter: none; }
    }

    /* Opacity-only pulse for area overlays (static shadow underneath, so the
       compositor handles it; the old voidPulse/mazeGlow animated box-shadow
       across whole grass fields and forced paint every frame) */
    @keyframes pulseFade {
      0%, 100% { opacity: 0.25; }
      50% { opacity: 0.6; }
    }

    .ambient-mote { position: absolute; pointer-events: none; }
    .tile-reveal { animation: tileReveal 0.45s ease-out; }

    /* Fog cover fading off a tile that was just charted */
    @keyframes fogLift {
      0% { opacity: 1; }
      100% { opacity: 0; }
    }
    .fog-lift {
      animation: fogLift 0.45s ease-out forwards;
      pointer-events: none;
    }
    .pulse-overlay-void {
      position: absolute;
      inset: 0;
      pointer-events: none;
      box-shadow: inset 0 0 18px rgba(167, 139, 250, 0.6);
      animation: pulseFade 3s ease-in-out infinite;
    }
    .pulse-overlay-maze {
      position: absolute;
      inset: 0;
      pointer-events: none;
      box-shadow: inset 0 0 14px rgba(232, 200, 74, 0.5);
      animation: pulseFade 4s ease-in-out infinite;
    }

    .lunge-right { animation: lungeRight 0.45s ease-out; }
    .lunge-left { animation: lungeLeft 0.45s ease-out; }
    .hit-flash-right { animation: hitFlashRight 0.4s ease-out; }
    .hit-flash-left { animation: hitFlashLeft 0.4s ease-out; }
    .telegraph-charge { animation: telegraphCharge 0.7s ease-in-out infinite; }
    .battle-wipe {
      position: fixed;
      inset: 0;
      background: #0f0f1a;
      z-index: 9999;
      pointer-events: none;
      animation: battleWipe 0.55s ease-in forwards;
    }

    .screen-shake {
      animation: screenShake 0.2s ease-in-out;
    }

    .critical-flash {
      animation: criticalFlash 0.1s ease-in-out;
    }

    /* Respect users who asked for less motion */
    @media (prefers-reduced-motion: reduce) {
      .lunge-right, .lunge-left, .hit-flash-right, .hit-flash-left,
      .telegraph-charge, .screen-shake, .critical-flash,
      .ambient-mote, .tile-reveal, .pulse-overlay-void, .pulse-overlay-maze {
        animation: none !important;
      }
      /* FOE auras keep a static glow: the threat cue must survive
         reduced-motion, it is gameplay information, not decoration */
      .foe-aura-patrol {
        animation: none !important;
        filter: drop-shadow(0 0 6px rgba(240, 160, 72, 0.8)) !important;
      }
      .foe-aura-chase {
        animation: none !important;
        filter: drop-shadow(0 0 9px rgba(255, 90, 90, 0.95)) !important;
      }
      /* Killing the wipe's animation alone would leave an opaque panel sitting
         on screen until React removes it. Hide it outright. */
      .battle-wipe {
        animation: none !important;
        display: none !important;
      }
      .screen-transition {
        animation: none !important;
        opacity: 0 !important;
      }
      .ridge-drift {
        animation: none !important;
      }
      /* Same trap: fog-lift fills forward to transparent, so with its
         animation removed it would sit opaque on the tile forever */
      .fog-lift {
        animation: none !important;
        opacity: 0 !important;
      }
      .gather-available {
        animation: none !important;
      }
      /* Flash fills forward to opacity 0, so killing the animation
         would leave an opaque white sheet. Hide it outright. */
      .battle-flash-overlay {
        animation: none !important;
        opacity: 0 !important;
      }
    }

    /* Slow horizontal drift for parallax battle backdrop layers */
    @keyframes ridgeDriftSlow {
      0% { transform: translateX(0); }
      100% { transform: translateX(-8px); }
    }
    @keyframes ridgeDriftMid {
      0% { transform: translateX(0); }
      100% { transform: translateX(5px); }
    }

    /* Screen transition: fade through a color */
    @keyframes screenFadeIn {
      0% { opacity: 1; }
      100% { opacity: 0; }
    }
    @keyframes screenFadeOut {
      0% { opacity: 0; }
      100% { opacity: 1; }
    }

    .screen-transition {
      position: fixed;
      inset: 0;
      z-index: 9998;
      pointer-events: none;
      animation: screenFadeIn 0.4s ease-out forwards;
    }

    .hp-low {
      animation: hpPulse 1s ease-in-out infinite;
    }

    .stamina-warning {
      animation: staminaWarning 0.5s ease-in-out infinite;
    }

    /* Focus styles for accessibility */
    button:focus-visible,
    input:focus-visible {
      outline: 2px solid #f6e05e;
      outline-offset: 2px;
    }
