/* Pre-keyed offline: these used to be three 1.2 MB PNGs fetched at boot,
   scaled to 384x256 on a canvas, keyed, and handed back as base64 data
   URLs. The files below are that result, already done. */
:root {
  --pot-sheet: url("assets/pot-sheet-v1.webp");
  --town-bird-sheet: url("assets/town-birds-keyed-v1.webp");
  --forest-bird-sheet: url("assets/forest-birds-keyed-v1.webp");
}
:root {
  --shell: #c8c6b2;
  --shell-dark: #9b9b8c;
  --ink: #152619;
  --screen: #9bbc0f;
  --screen-dark: #306230;
  --button: #8d2953;
  color-scheme: dark;
  font-family: "Courier New", monospace;
  -webkit-tap-highlight-color: transparent;
}

* { box-sizing: border-box; }
html, body { min-height: 100%; margin: 0; overflow: hidden; }
body {
  display: grid;
  place-items: center;
  background: radial-gradient(circle at 50% 10%, #405c46, #142018 70%);
  touch-action: manipulation;
  /* The webkit pair is what iOS honours; without it a long press on any
   * label raised the selection handles and the copy callout mid-game. */
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

button, input { font: inherit; }
button {
  cursor: pointer;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  touch-action: manipulation;
}
.controls, .controls label, .system-buttons, .system-buttons label {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
.handheld {
  --control-unit: clamp(44px, 12.3vw, 52px);
  position: relative;
  display: grid;
  grid-template-rows: 24px minmax(0, 1fr) clamp(154px, 21dvh, 190px) 22px;
  gap: 4px;
  width: min(100vw, 450px);
  height: 100dvh;
  padding: max(8px, env(safe-area-inset-top)) max(8px, env(safe-area-inset-right)) max(8px, env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left));
  overflow: hidden;
  color: #30322f;
  background:
    linear-gradient(105deg, transparent 48%, #fff4 49%, transparent 51%) 0 0 / 8px 100%,
    linear-gradient(160deg, #dad8c7, var(--shell) 58%, #aaa99a);
  border-radius: 18px 18px 56px 18px;
  box-shadow: inset 5px 0 #ffffff55, inset -7px -7px #77786b55, 0 24px 80px #0009;
}

.brand { min-height: 0; display: flex; align-items: center; gap: 5px; padding: 0 6px; letter-spacing: -1px; }
.brand span { color: #314174; font-weight: 900; font-style: italic; }
.brand strong { color: #8d2953; font-size: 20px; font-style: italic; }
.brand i { margin-left: auto; font-size: 8px; letter-spacing: 2px; color: #686b64; }

.bezel {
  min-height: 0;
  padding: 10px 10px 5px;
  background: linear-gradient(145deg, #50535b, #262a32);
  border-radius: 16px 16px 42px 16px;
  box-shadow: inset 3px 3px #777b84, inset -4px -5px #17191d, 0 3px #efedda;
}
.screen {
  position: relative;
  height: calc(100% - 16px);
  overflow: hidden;
  color: var(--ink);
  background: var(--screen);
  border: 4px solid #161b18;
  box-shadow: inset 0 0 18px #0f380f99, 0 0 0 2px #7c7f6d;
  image-rendering: pixelated;
}
.scanlines { position: absolute; inset: 0; z-index: 20; pointer-events: none; opacity: .14; background: repeating-linear-gradient(#173518 0 1px, transparent 1px 4px); }
.hud { position: absolute; inset: 0 0 auto; z-index: 5; height: 49px; padding: 7px 9px; display: flex; justify-content: space-between; color: #fff4d2; background: #101927f2; border-bottom: 2px solid #79dbea; text-shadow: 1px 1px #000; }
/* Tight leading on purpose: the affects row below has to fit inside the
 * header's fixed height, or the experience strip pinned to the bottom edge
 * is painted through it. */
.hud b { display: block; max-width: 145px; overflow: hidden; text-overflow: ellipsis; font-size: 14px; line-height: 1.05; }
.hud small { font-size: 10px; line-height: 1.05; }
/* Affects are chips rather than a truncated sentence: every one is visible,
   colour says whether it helps or hurts, and the number is hours remaining. */
.status-effects { display: flex; flex-wrap: wrap; gap: 2px; max-width: 168px; color: #9cecff; font-size: 7px; font-weight: 900; }
.affect { display: inline-flex; align-items: center; gap: 2px; padding: 0 3px; border: 1px solid currentColor; border-radius: 2px; font-style: normal; line-height: 1.5; white-space: nowrap; }
.affect.good { color: #8ddc9a; }
.affect.bad { color: #ff8f8f; }
.affect.resting { color: #9cecff; }
.affect b { padding-left: 2px; font-weight: 400; opacity: .75; }
/* About to lapse: the one thing a player needs to notice without reading. */
.affect.expiring { animation: affect-blink .9s steps(2) infinite; }
@keyframes affect-blink { 50% { opacity: .35; } }

/* A few affects are worth seeing on the character, not only in the list. */
/* Sanctuary breathes. The ground shadow is repeated in both keyframes
   because animating `filter` replaces the property outright -- omit it and
   the character loses its shadow for the duration of the blessing. */
.player-sprite[data-aura="sanctuary"], .remote-player[data-aura="sanctuary"] { animation: aura-sanctuary 1.7s ease-in-out infinite; }
@keyframes aura-sanctuary {
  0%, 100% { filter: drop-shadow(0 5px 1px #102a1766) drop-shadow(0 0 8px #fff2c4) drop-shadow(0 0 16px #ffd25eaa); }
  50%      { filter: drop-shadow(0 5px 1px #102a1766) drop-shadow(0 0 16px #fffbe8) drop-shadow(0 0 32px #ffd25e) drop-shadow(0 0 46px #ffb43c77); }
}
.player-sprite[data-aura="invisible"], .remote-player[data-aura="invisible"] { opacity: .42; }
/* Camouflaged. Faded rather than gone on your own screen, so you can still see
   where you are; another player's client is told nothing at all, so to them the
   room is simply empty. */
.player-sprite[data-aura="hidden"] { opacity: .5; filter: saturate(.55) brightness(.9); }
.remote-player[data-aura="hidden"] { opacity: .5; filter: saturate(.55) brightness(.9); }
.player-sprite[data-aura="poison"], .remote-player[data-aura="poison"] { filter: hue-rotate(58deg) saturate(1.5) brightness(.92); }
.player-sprite[data-aura="flying"], .remote-player[data-aura="flying"] { animation: aura-hover 2.4s ease-in-out infinite; }
.player-sprite[data-aura="blind"], .remote-player[data-aura="blind"] { filter: grayscale(.75) brightness(.65); }
.player-sprite[data-aura="infravision"], .remote-player[data-aura="infravision"] { filter: drop-shadow(0 0 5px #ff7a5e99); }
@keyframes aura-hover { 50% { translate: 0 -4px; } }
@media (prefers-reduced-motion: reduce) {
  .affect.expiring, .player-sprite[data-aura="flying"],
  .player-sprite[data-aura="sanctuary"] { animation: none; }
}
.meters { width: 126px; }
.meters label { display: grid; grid-template-columns: 20px 1fr 32px; gap: 3px; align-items: center; font-size: 10px; font-weight: bold; }
.meters i { height: 6px; background: #05080d; border: 1px solid #dffaff; }
.meters i span { display: block; height: 100%; background: #61d9eb; transition: width .25s; }
/* Three bars now, so they read apart at a glance: health warm, mana the
   existing cyan, stamina green. */
.meters label:nth-of-type(1) i span { background: #ff6b7a; }
.meters label:nth-of-type(3) i span { background: #7ee081; }
.meters em { font-style: normal; text-align: right; }

.world { --scene-image: url("assets/forest.png"); --scene-size: cover; --scene-position: center; --scene-filter: none; --scene-flip: 1; --scene-zoom: 1; position: absolute; inset: 49px 0 0; overflow: hidden; background: #6f8f18; }
.world::before { content: ""; position: absolute; z-index: 0; inset: -3%; background-image: var(--scene-image); background-position: var(--scene-position); background-size: var(--scene-size); background-repeat: no-repeat; filter: var(--scene-filter); image-rendering: pixelated; transform: scaleX(var(--scene-flip)) scale(var(--scene-zoom)); transition: filter .25s; }
.world::after { content: ""; position: absolute; z-index: 1; inset: 0; background: linear-gradient(transparent 45%, #183d1744); pointer-events: none; }
.world[class*="transition-"] .room-card, .world[class*="transition-"] .exit-lights,
.world[class*="transition-"] .room-entities, .world[class*="transition-"] .player-sprite,
.world[class*="transition-"] .enemy { animation-duration: .36s; animation-timing-function: ease-out; animation-fill-mode: both; }
.world.transition-east > :not(.message), .world.transition-east .enemy { animation-name: room-enter-east; }
.world.transition-west > :not(.message), .world.transition-west .enemy { animation-name: room-enter-west; }
.world.transition-north > :not(.message), .world.transition-north .enemy { animation-name: room-enter-north; }
.world.transition-south > :not(.message), .world.transition-south .enemy { animation-name: room-enter-south; }
/* Teleports (goto, trans, recall, waystone) have no direction to slide from. */
.world.transition-fade > :not(.message), .world.transition-fade .enemy { animation-name: room-enter-fade; }
@keyframes room-enter-fade { from { opacity: 0; transform: scale(.985); } to { opacity: 1; transform: none; } }
@keyframes room-enter-east { from { opacity: .15; transform: translateX(38%); } }
@keyframes room-enter-west { from { opacity: .15; transform: translateX(-38%); } }
@keyframes room-enter-north { from { opacity: .15; transform: translateY(-32%); } }
@keyframes room-enter-south { from { opacity: .15; transform: translateY(32%); } }
.room-card { position: absolute; z-index: 3; top: 6px; left: 8px; right: 8px; padding: 3px 7px; color: #fff4d2; background: #101927e8; border: 1px solid #79dbea; border-radius: 4px; box-shadow: 0 2px 0 #0007; text-shadow: 1px 1px #000; }
.room-card small, .room-card b { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* The two feeds, directly under the room card.
 *
 * World news and world chat are both global and both arrive unprompted, but
 * they are different things to read -- one is what the world did, the other is
 * what people said -- so they get a line each rather than being interleaved
 * into one stream you have to parse. Both scroll, because a phone-width strip
 * cannot hold a sentence, and neither takes pointer events: this sits over the
 * play area and must never eat a tap meant for the world behind it. */
.world-feed { position: absolute; z-index: 3; top: var(--feed-top, 41px); left: 8px; right: 8px; pointer-events: none; display: grid; gap: 2px; }
/* An explicit display beats the browser's own [hidden] rule, so before this the
 * empty strip sat on screen from the moment the page loaded. */
.world-feed[hidden] { display: none; }
.feed-line { display: flex; align-items: center; gap: 5px; width: 100%; height: 13px; padding: 0 5px; color: inherit; background: #101927cc; border: 1px solid #ffffff1f; border-radius: 3px; pointer-events: auto; text-align: left; font: 700 7px/13px system-ui, sans-serif; text-shadow: 1px 1px #000; }
.feed-line:active, .feed-line:focus-visible { filter: brightness(1.45); outline: 1px solid #fff8; }
.feed-line > b { flex: none; letter-spacing: .06em; opacity: .75; }
.feed-line > i { position: relative; flex: 1; overflow: hidden; height: 13px; font-style: normal; }
.feed-line > i > span { position: absolute; left: 0; white-space: nowrap; will-change: transform; }
/* Only what overflows is animated. A short line that fits should sit still and
 * be read, not crawl past for no reason. */
.feed-line.rolling > i > span { animation: feed-roll var(--feed-ms, 14s) linear infinite; }
.feed-line.news { color: #ffd98a; border-color: #d89b3559; }
.feed-line.news > b { color: #ff9d5c; }
.feed-line.chat { color: #b9e6ff; border-color: #79dbea44; }
.feed-line.chat > b { color: #79dbea; }
@keyframes feed-roll {
  from { transform: translateX(var(--feed-from, 100%)); }
  to { transform: translateX(-100%); }
}
@media (prefers-reduced-motion: reduce) {
  .feed-line.rolling > i > span { animation: none; }
}
/* The quest tracker shares this band. Rather than one hiding the other, they
 * stack: the feed's top and the north exit light below it are both measured in
 * script, because the tracker's height depends on whether it is showing a
 * one-line objective or a full briefing. */
.room-card small { display: flex; justify-content: space-between; gap: 8px; font-size: 7px; line-height: 1.1; opacity: .82; }
.room-card small span { overflow: hidden; text-overflow: ellipsis; }
.room-card small em { flex: none; font-style: normal; }
.room-card b { font-size: 11px; line-height: 1.15; }
.room-card.region-on-level { border-color: #f0cb68; box-shadow: 0 2px 0 #0008, inset 0 0 8px #f0cb6822; }
.room-card.region-danger { border-color: #ff6874; box-shadow: 0 2px 0 #0008, inset 0 0 10px #ff304b44; }
.room-card.region-danger #area-name { color: #ffaaa0; }
.room-card.region-mastered { border-color: #79dbea99; }
.room-card.region-mastered #area-name { color: #b9d9df; }
.room-card.region-capital #area-name { color: #ffe39a; }
.tutorial-objective { position: absolute; z-index: 7; top: 39px; left: 8px; right: 8px; min-height: 0; padding: 3px 7px 3px 8px; overflow: hidden; color: #f8fafc; background: linear-gradient(100deg, #111827ed, #1f2937e8); border: 1px solid #e7c66b; border-left: 3px solid #f5c451; border-radius: 4px; text-align: left; box-shadow: 0 2px 6px #02061788; text-shadow: 0 1px 1px #000; }
.tutorial-objective[hidden] { display: none; }
.tutorial-objective small, .tutorial-objective b, .tutorial-objective span { display: block; overflow: hidden; text-overflow: ellipsis; }
.tutorial-objective small { color: #f8d77d; font-size: 6px; line-height: 1; letter-spacing: .7px; white-space: nowrap; }
.tutorial-objective b { margin-top: 1px; color: #fff; font-size: 9px; line-height: 1.1; white-space: nowrap; }
.tutorial-objective span { margin-top: 1px; color: #dbeafe; font-family: system-ui, sans-serif; font-size: 8px; font-weight: 700; line-height: 1.1; white-space: nowrap; }
.tutorial-objective.reward { color: #fff7d1; background: linear-gradient(100deg, #3f1d0df5, #713f12f2); border-color: #ffcf56; border-left-color: #fff0a6; animation: reward-pulse .8s steps(2) infinite; }
.tutorial-objective.live-event { background: linear-gradient(100deg, #10283ff2, #143c4cf2); border-color: #66e5ff; border-left-color: #b7f5ff; box-shadow: 0 2px 7px #02061799, 0 0 12px #22d3ee55; }
.tutorial-objective.live-event small { color: #8ff3ff; }
.tutorial-objective.live-event span { color: #d5f8ff; }
.tutorial-objective.compact { min-height: 0; display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 6px; align-items: center; }
.tutorial-objective.compact small { font-size: 7px; line-height: 1; }
.tutorial-objective.compact b { min-width: 0; margin: 0; font-size: 9px; line-height: 1; white-space: nowrap; }
.tutorial-objective.compact span { margin: 0; color: #f8d77d; font-size: 7px; line-height: 1; white-space: nowrap; }
@keyframes reward-pulse { 50% { filter: brightness(1.35); } }
.world:has(.tutorial-objective:not([hidden])) .exit-lights .north { top: var(--exit-north-top, 78px); }
.world:has(.tutorial-objective.compact:not([hidden])) .message { bottom: 42px; }
.exit-lights { position: absolute; z-index: 15; inset: 0; pointer-events: none; }
.exit-lights button { position: absolute; display: grid; place-items: center; width: 30px; height: 30px; padding: 0; border: 2px solid #5a6674; border-radius: 50%; color: #74808a; background: radial-gradient(circle, #29313a 0 42%, #111820 46% 100%); box-shadow: inset 0 0 0 2px #080c11, 0 2px 3px #0009; opacity: .2; pointer-events: none; font: 900 10px/1 system-ui, sans-serif; text-shadow: 0 1px #000; touch-action: manipulation; user-select: none; -webkit-user-select: none; }
/* The lit state puts a pale-yellow glow behind the glyph, so near-white text
 * on it was unreadable. A dark glyph with a warm halo reads against the glow. */
.exit-lights button.on { color: #2b1b05; text-shadow: 0 1px #ffe9a8, 0 0 3px #ffe9a8; background: radial-gradient(circle, #fff3a1 0 22%, #d89b35 25% 48%, #202b36 52% 100%); border-color: #79dbea; box-shadow: 0 0 5px #fff3a1, 0 0 13px #79dbea, inset 0 0 0 2px #6e421c, 0 2px 3px #000b; opacity: 1; pointer-events: auto; animation: exit-glow 1.1s ease-in-out infinite alternate; }
.exit-lights button.on:active { filter: brightness(1.35); transform: scale(.9); }
/* The way to the objective. Green against the amber of an ordinary exit, and
 * pulsing on its own slower beat, so which door to take reads at a glance
 * without having to open the quest journal and work it out from the briefing. */
.exit-lights button.on.quest-way { color: #06240f; text-shadow: 0 1px #c8ffd4, 0 0 3px #8dffa8; background: radial-gradient(circle, #ddffe4 0 22%, #46c46a 25% 48%, #14301c 52% 100%); border-color: #8dffa8; box-shadow: 0 0 6px #b6ffc6, 0 0 16px #46c46a, inset 0 0 0 2px #1c5c2c, 0 2px 3px #000b; animation: quest-way-glow 1.5s ease-in-out infinite alternate; }
@keyframes quest-way-glow {
  from { box-shadow: 0 0 5px #b6ffc6, 0 0 10px #46c46a, inset 0 0 0 2px #1c5c2c, 0 2px 3px #000b; }
  to { box-shadow: 0 0 9px #ddffe4, 0 0 22px #6fe08c, inset 0 0 0 2px #1c5c2c, 0 2px 3px #000b; }
}
/* Pinned to the walkable bounds rather than to fixed pixels, so a marker sits
 * where the floor actually stops in THIS room. */
.exit-lights .north { top: calc(var(--grid-top, 22) * 1% - 15px); left: calc(50% - 15px); }
.exit-lights .south { top: calc(var(--grid-bottom, 86) * 1% - 15px); left: calc(50% - 15px); bottom: auto; }
/* The right-hand ability column occupies right 7-47px, so the east/up/down
 * lights sit inboard of it, mirroring how west clears the left column. */
/* Nudged inward by the width of the ability column, which overlaps the world:
 * a marker directly under it cannot be tapped. That was the real reason for the
 * old 52px, and it is the only part of it worth keeping. */
.exit-lights .east { top: calc(50% - 15px); left: auto; right: calc((100 - var(--grid-right, 93)) * 1% + 6px); }
.exit-lights .west { top: calc(50% - 15px); left: calc(var(--grid-left, 7) * 1% + 6px); }
.exit-lights .up { top: calc(var(--grid-top, 22) * 1% - 15px); right: calc((100 - var(--grid-right, 93)) * 1% + 6px); }
.exit-lights .down { top: calc(var(--grid-bottom, 86) * 1% - 15px); right: calc((100 - var(--grid-right, 93)) * 1% + 6px); bottom: auto; }
@keyframes exit-glow { to { box-shadow: 0 0 8px #fff3a1, 0 0 19px #79dbea, inset 0 0 0 2px #6e421c, 0 2px 3px #000b; } }
.tap-destination { position: absolute; width: 18px; height: 10px; border: 2px solid #fff3a1; border-radius: 50%; box-shadow: 0 0 8px #79dbea, inset 0 0 4px #d89b35; pointer-events: none; transform: translate(-50%, -50%); animation: tap-destination .65s ease-out both; }
@keyframes tap-destination { from { opacity: 1; transform: translate(-50%, -50%) scale(.35); } to { opacity: 0; transform: translate(-50%, -50%) scale(1.5); } }
/* Do not give this wrapper a z-index. A numbered z-index creates a stacking
 * context and traps every prop below the player, defeating gridPosition's
 * per-tile depth values. With auto, its positioned children share the world's
 * stacking context with players and mobs, so crossing a prop's foot changes
 * which one is in front. */
.room-entities { position: absolute; inset: 0; z-index: auto; pointer-events: none; }
.travel-camp-prop { position: absolute; display: block; translate: -50% -82%; pointer-events: none; image-rendering: pixelated; filter: drop-shadow(0 3px 2px #07120b99); }
.travel-camp-tent { width: 82px; height: 82px; background: url("assets/travelers-camp-tent-64.png") 0 0 / 200% 100% no-repeat; animation: camp-tent-breathe 1.8s steps(1) infinite; }
.travel-camp-fire { width: 48px; height: 48px; background: url("assets/travelers-camp-32.png") 0 0 / 500% 300% no-repeat; filter: drop-shadow(0 0 8px #ff9b38) drop-shadow(0 3px 2px #07120b99); animation: camp-fire-frames .55s steps(4) infinite; }
@keyframes camp-fire-frames { to { background-position-x: 100%; } }
@keyframes camp-tent-breathe { 50% { background-position-x: 100%; } }
.ambient-life { position: absolute; inset: 0; z-index: 1; overflow: hidden; pointer-events: none; }
.ambient-bird { position: absolute; width: 30px; height: 40px; opacity: var(--bird-opacity, 1); image-rendering: pixelated; filter: drop-shadow(0 3px 1px #102a1766); transform-origin: 50% 80%; }
.ambient-bird::before { content: ""; position: absolute; inset: 0; background-image: var(--town-bird-sheet); background-repeat: no-repeat; background-size: 400% 200%; background-position-y: 0%; transform: scaleX(var(--idle-facing, 1)); animation: bird-idle 1.15s steps(1) infinite; animation-delay: var(--bird-delay); }
.ambient-bird.forest::before { background-image: var(--forest-bird-sheet); }
.ambient-bird.flying { z-index: 2; animation: bird-flight var(--flight-duration, 9.6s) infinite; animation-delay: var(--bird-delay); }
.ambient-bird.flying::before { background-position-y: 100%; animation: bird-flap .48s steps(1) infinite, bird-turn var(--flight-duration, 9.6s) linear infinite; animation-delay: var(--bird-delay); }
@keyframes bird-idle { 0%, 24% { background-position-x: 0%; } 25%, 49% { background-position-x: 33.333%; } 50%, 74% { background-position-x: 66.667%; } 75%, 100% { background-position-x: 100%; } }
@keyframes bird-flap { 0%, 24% { background-position-x: 0%; } 25%, 49% { background-position-x: 33.333%; } 50%, 74% { background-position-x: 66.667%; } 75%, 100% { background-position-x: 100%; } }
@keyframes bird-flight { 0% { translate: var(--flight-start) 5px; animation-timing-function: ease-in-out; } 50% { translate: var(--flight-end) -8px; animation-timing-function: ease-in-out; } 100% { translate: var(--flight-start) 5px; } }
@keyframes bird-turn { 0%, 49.99% { transform: scaleX(var(--flight-facing)); } 50%, 100% { transform: scaleX(var(--return-facing)); } }
.ambient-particle { position: absolute; width: var(--particle-size); height: var(--particle-size); border-radius: 50%; opacity: 0; background: #d7f6bd; box-shadow: 0 0 5px currentColor; animation: ambient-rise var(--particle-duration) ease-in-out infinite; animation-delay: var(--particle-delay); }
.ambient-particle.leaf { color: #9bcb58; width: 7px; height: 4px; border-radius: 80% 15% 80% 15%; background: #80ad45; animation-name: ambient-leaf; }
.ambient-particle.sparkle { color: #fff0a0; background: #fff8cf; animation-name: ambient-sparkle; }
.ambient-particle.mist { color: #d7f7ff; width: 14px; height: 2px; background: #d7f7ff99; box-shadow: 0 0 8px #b8efff; }
.ambient-particle.spore { color: #a8f08a; background: #d2ff9a; box-shadow: 0 0 6px #74db8d; }
.ambient-particle.ember { color: #ff9a48; background: #ffd06a; box-shadow: 0 0 7px #ff542f; }
.ambient-particle.dust { color: #e6c58d; background: #e6c58dbb; box-shadow: none; }
.ambient-particle.snow { color: #eaf8ff; background: #fff; box-shadow: 0 0 3px #bde8ff; animation-name: ambient-fall; }
.ambient-particle.rain { color: #83d7ff; width: 1px; height: 12px; border-radius: 1px; background: #b9ebffbb; box-shadow: none; animation-name: ambient-rain; }
.ambient-particle.shadow { color: #7d6bb4; background: #66528aaa; box-shadow: 0 0 7px #34245d; }
.ambient-particle.neon { color: #41fff2; background: #bafff8; box-shadow: 0 0 6px #00d9ff, 0 0 10px #f248ff; animation-name: ambient-sparkle; }
.ambient-particle.star { color: #fff7cf; background: #fff; box-shadow: 0 0 8px #a7d5ff; animation-name: ambient-sparkle; }
.ambient-particle.ash { color: #a8a4aa; background: #b9b2bcaa; box-shadow: none; animation-name: ambient-fall; }
.ambient-particle.chrono { color: #d9a4ff; background: #f0d4ff; box-shadow: 0 0 7px #9c5cff; animation-name: ambient-sparkle; }
.ambient-particle.wisp { color: #82ffbd; width: 7px; height: 3px; background: #b5ffd0; box-shadow: 0 0 8px #38da91; }
@keyframes ambient-rise { 0%, 100% { opacity: 0; transform: translate(0, 12px) scale(.5); } 25%, 70% { opacity: .8; } 80% { opacity: .35; } 99% { transform: translate(var(--particle-drift), -34px) scale(1.15); } }
@keyframes ambient-fall { 0% { opacity: 0; transform: translate(0, -25px); } 15%, 80% { opacity: .85; } 100% { opacity: 0; transform: translate(var(--particle-drift), 48px); } }
@keyframes ambient-rain { 0% { opacity: 0; transform: translate(0, -35px) rotate(10deg); } 12%, 82% { opacity: .7; } 100% { opacity: 0; transform: translate(var(--particle-drift), 62px) rotate(10deg); } }
@keyframes ambient-leaf { 0%, 100% { opacity: 0; transform: translate(0, -20px) rotate(0deg); } 15%, 85% { opacity: .8; } 99% { transform: translate(var(--particle-drift), 45px) rotate(320deg); } }
@keyframes ambient-sparkle { 0%, 100% { opacity: 0; transform: scale(.2) rotate(0deg); } 35% { opacity: .9; transform: scale(1.25) rotate(90deg); } 65% { opacity: .35; transform: scale(.65) rotate(180deg); } }
.player-sprite { position: absolute; z-index: 6; left: 50%; top: 58%; width: 54px; height: 54px; transform: translate(-50%, -82%) scale(1.25); transform-origin: 50% 82%; transition: left .024s linear, top .024s linear; filter: drop-shadow(0 5px 1px #102a1766); }
.world.dedicated-scene .player-sprite { width: 46px; height: 46px; }
.farmer-layer { position: absolute; inset: -5px; z-index: 1; background-size: 1600% 1600%; background-repeat: no-repeat; background-position: 0 0; image-rendering: pixelated; transform: translateY(11px); }
.farmer-under { z-index: 0; }.farmer-body { z-index: 1; }.farmer-race { z-index: 2; }
.farmer-lower { z-index: 3; }.farmer-shirt { z-index: 4; }
/* 06lwr2/08lwr3: overalls, skirts and dresses sit over the shirt. */
.farmer-outer { z-index: 5; }
/* 10outr vest/suspenders, then 11neck cloaks -- the cape is equipment,
   not a barber choice, so this layer follows the about-body slot. */
.farmer-vest { z-index: 6; }.farmer-neck { z-index: 7; }
.farmer-eyes { z-index: 8; }
.farmer-hair { z-index: 9; }.farmer-head { z-index: 10; }.farmer-over { z-index: 11; }
.player-sprite > .sprite-layer, .remote-player > .sprite-layer { visibility: hidden; }
.player-sprite.attacking:not(.farmer-combat-active) > .sprite-layer.sword,
.remote-player.event-attacking:not(.farmer-combat-active) > .sprite-layer.sword,
.remote-player.event-casting > .sprite-layer.sword { visibility: visible; }
.player-sprite[data-facing="north"] .farmer-layer, .remote-player[data-facing="north"] .farmer-layer { background-position: 0 6.6667%; }
.player-sprite[data-facing="east"] .farmer-layer, .remote-player[data-facing="east"] .farmer-layer { background-position: 0 13.3333%; }
.player-sprite[data-facing="west"] .farmer-layer, .remote-player[data-facing="west"] .farmer-layer { background-position: 0 13.3333%; transform: translateY(11px) scaleX(-1); }
.player-sprite[data-position="sitting"] .farmer-layer, .remote-player[data-position="sitting"] .farmer-layer { background-position: 20% 80%; animation: farmer-seated 1.8s steps(2, end) infinite; }
@keyframes farmer-seated { 50% { translate: 0 1px; } }
.player-sprite.walking:not(.attacking)[data-facing="south"] .farmer-layer,
.player-sprite.continuous-walk:not(.attacking)[data-facing="south"] .farmer-layer,
.remote-player.walking[data-facing="south"] .farmer-layer { animation: farmer-walk-south .81s steps(1) infinite; }
.player-sprite.walking:not(.attacking)[data-facing="north"] .farmer-layer,
.player-sprite.continuous-walk:not(.attacking)[data-facing="north"] .farmer-layer,
.remote-player.walking[data-facing="north"] .farmer-layer { animation: farmer-walk-north .81s steps(1) infinite; }
.player-sprite.walking:not(.attacking)[data-facing="east"] .farmer-layer,
.player-sprite.continuous-walk:not(.attacking)[data-facing="east"] .farmer-layer,
.remote-player.walking[data-facing="east"] .farmer-layer { animation: farmer-walk-east .81s steps(1) infinite; }
.player-sprite.walking:not(.attacking)[data-facing="west"] .farmer-layer,
.player-sprite.continuous-walk:not(.attacking)[data-facing="west"] .farmer-layer,
.remote-player.walking[data-facing="west"] .farmer-layer { animation: farmer-walk-west .81s steps(1) infinite; }
@keyframes farmer-walk-south {
  0%, 16.66% { background-position: 0 20%; transform: translateY(11px) scaleX(1); }
  16.67%, 33.33% { background-position: 6.6667% 20%; transform: translateY(11px) scaleX(1); }
  33.34%, 50% { background-position: 13.3333% 20%; transform: translateY(11px) scaleX(1); }
  50.01%, 66.66% { background-position: 0 20%; transform: translateY(11px) scaleX(-1); }
  66.67%, 83.33% { background-position: 6.6667% 20%; transform: translateY(11px) scaleX(-1); }
  83.34%, 100% { background-position: 13.3333% 20%; transform: translateY(11px) scaleX(-1); }
}
@keyframes farmer-walk-north {
  0%, 16.66% { background-position: 26.6667% 20%; transform: translateY(11px) scaleX(1); }
  16.67%, 33.33% { background-position: 33.3333% 20%; transform: translateY(11px) scaleX(1); }
  33.34%, 50% { background-position: 40% 20%; transform: translateY(11px) scaleX(1); }
  50.01%, 66.66% { background-position: 26.6667% 20%; transform: translateY(11px) scaleX(-1); }
  66.67%, 83.33% { background-position: 33.3333% 20%; transform: translateY(11px) scaleX(-1); }
  83.34%, 100% { background-position: 40% 20%; transform: translateY(11px) scaleX(-1); }
}
@keyframes farmer-walk-east {
  0%, 16.66% { background-position: 0 26.6667%; transform: translateY(11px) scaleX(1); }
  16.67%, 33.33% { background-position: 6.6667% 26.6667%; transform: translateY(11px) scaleX(1); }
  33.34%, 50% { background-position: 13.3333% 26.6667%; transform: translateY(11px) scaleX(1); }
  50.01%, 66.66% { background-position: 20% 26.6667%; transform: translateY(11px) scaleX(1); }
  66.67%, 83.33% { background-position: 26.6667% 26.6667%; transform: translateY(11px) scaleX(1); }
  83.34%, 100% { background-position: 33.3333% 26.6667%; transform: translateY(11px) scaleX(1); }
}
@keyframes farmer-walk-west {
  0%, 16.66% { background-position: 0 26.6667%; transform: translateY(11px) scaleX(-1); }
  16.67%, 33.33% { background-position: 6.6667% 26.6667%; transform: translateY(11px) scaleX(-1); }
  33.34%, 50% { background-position: 13.3333% 26.6667%; transform: translateY(11px) scaleX(-1); }
  50.01%, 66.66% { background-position: 20% 26.6667%; transform: translateY(11px) scaleX(-1); }
  66.67%, 83.33% { background-position: 26.6667% 26.6667%; transform: translateY(11px) scaleX(-1); }
  83.34%, 100% { background-position: 33.3333% 26.6667%; transform: translateY(11px) scaleX(-1); }
}
.sprite-layer { position: absolute; inset: 0; background-size: 800% 800%; background-repeat: no-repeat; background-position-x: 0; image-rendering: pixelated; }
.sprite-layer.base { background-image: url("assets/player-base.png"); }
.sprite-layer.outfit { background-image: url("assets/player-outfit.png"); }
.sprite-layer.hair { background-image: url("assets/player-hair.png"); }
.sprite-layer.hat { background-image: url("assets/player-hat.png"); }
.sprite-layer.sword { display: none; }

/* Elemental weapons.
 *
 * The blade sprites are near-greyscale steel, and hue-rotate does nothing to a
 * colour with no saturation -- it would leave every element looking identical.
 * sepia() puts saturation in first and hue-rotate then moves it somewhere,
 * which is what actually tints pixel art. The drop-shadow is the element's own
 * light thrown onto the swing. */
[data-element] > .sprite-layer.sword {
  filter: sepia(.9) saturate(var(--el-sat, 2.4)) hue-rotate(var(--el-hue, 0deg))
          brightness(var(--el-bright, 1.05)) drop-shadow(0 0 3px var(--el-glow, transparent));
}
[data-element="fire"]    { --el-hue: -30deg; --el-sat: 4;   --el-bright: 1.15; --el-glow: #ff8a2e;  --orb-row: 87.5000%; }
[data-element="frost"]   { --el-hue: 165deg; --el-sat: 3.2; --el-bright: 1.2;  --el-glow: #7fe6ff;  --orb-row: 25.0000%; }
[data-element="arcane"]  { --el-hue: 240deg; --el-sat: 3.4; --el-bright: 1.1;  --el-glow: #c48cff;  --orb-row: 12.5000%; }
[data-element="shadow"]  { --el-hue: 215deg; --el-sat: 1.6; --el-bright: .72;  --el-glow: #7a5bb0;  --orb-row: 100.0000%; }
[data-element="toxic"]   { --el-hue: 55deg;  --el-sat: 3.6; --el-bright: 1.05; --el-glow: #b6f24a;  --orb-row: 37.5000%; }
[data-element="verdant"] { --el-hue: 75deg;  --el-sat: 2.8; --el-bright: 1;    --el-glow: #6fdc7a;  --orb-row: 37.5000%; }
[data-element="tide"]    { --el-hue: 180deg; --el-sat: 3;   --el-bright: 1.05; --el-glow: #46b8ff;  --orb-row: 25.0000%; }
[data-element="volt"]    { --el-hue: 10deg;  --el-sat: 4.2; --el-bright: 1.3;  --el-glow: #ffe14a;  --orb-row: 50.0000%; }
[data-element="sand"]    { --el-hue: -10deg; --el-sat: 2.2; --el-bright: 1.12; --el-glow: #e5b45c;  --orb-row: 75.0000%; }
[data-element="stone"]   { --el-hue: 0deg;   --el-sat: .35; --el-bright: .95;  --el-glow: #9aa4ad;  --orb-row: 62.5000%; }

/* The element only flares on the swing, so an idle blade stays a weapon rather
 * than a light source. */
.player-sprite.attacking[data-element] > .sprite-layer.sword,
.remote-player.event-attacking[data-element] > .sprite-layer.sword {
  animation: sword-slash .52s steps(1) 1, element-flare .52s ease-out 1;
}
@keyframes element-flare {
  0%   { filter: sepia(.9) saturate(var(--el-sat, 2.4)) hue-rotate(var(--el-hue, 0deg)) brightness(var(--el-bright, 1.05)) drop-shadow(0 0 2px var(--el-glow, transparent)); }
  35%  { filter: sepia(.9) saturate(calc(var(--el-sat, 2.4) * 1.4)) hue-rotate(var(--el-hue, 0deg)) brightness(calc(var(--el-bright, 1.05) * 1.5)) drop-shadow(0 0 11px var(--el-glow, transparent)); }
  100% { filter: sepia(.9) saturate(var(--el-sat, 2.4)) hue-rotate(var(--el-hue, 0deg)) brightness(var(--el-bright, 1.05)) drop-shadow(0 0 3px var(--el-glow, transparent)); }
}
@media (prefers-reduced-motion: reduce) {
  .player-sprite.attacking[data-element] > .sprite-layer.sword,
  .remote-player.event-attacking[data-element] > .sprite-layer.sword { animation: sword-slash .52s steps(1) 1; }
}
.player-sprite[data-sex="female"] .sprite-layer.base { background-image: url("assets/player-female-base.png"); }
.player-sprite[data-sex="female"] .sprite-layer.outfit { background-image: url("assets/player-female-outfit.png"); }
.player-sprite[data-sex="female"] .sprite-layer.hair { background-image: url("assets/player-female-hair.png"); }
.player-sprite[data-sex="female"] .sprite-layer.hat { background-image: url("assets/player-female-hat.png"); }
.player-sprite .sprite-layer.hair, .remote-player .sprite-layer.hair { background-image: var(--player-hair-image, url("assets/player-hair.png")); }
.player-sprite.attacking:not(.farmer-combat-active) .sprite-layer { animation: sword-slash .52s steps(1) 1; }
.player-sprite.attacking:not(.farmer-combat-active) .sprite-layer.base { background-image: url("assets/player-attack-base.png"); }
.player-sprite.attacking:not(.farmer-combat-active) .sprite-layer.outfit { background-image: url("assets/player-attack-outfit.png"); }
.player-sprite.attacking:not(.farmer-combat-active) .sprite-layer.hair { background-image: var(--player-attack-hair-image, url("assets/player-attack-hair.png")); }
.player-sprite.attacking:not(.farmer-combat-active) .sprite-layer.hat { background-image: url("assets/player-attack-hat.png"); }
.player-sprite.attacking:not(.farmer-combat-active) { animation: farmer-strike .52s ease-out; }
.player-sprite.attacking:not(.farmer-combat-active) .sprite-layer.sword { display: block; z-index: 10; background-image: url("assets/player-attack-sword.png"); }
.player-sprite.attacking:not(.farmer-combat-active)[data-sex="female"] .sprite-layer.base { background-image: url("assets/player-female-attack-base.png"); }
.player-sprite.attacking:not(.farmer-combat-active)[data-sex="female"] .sprite-layer.outfit { background-image: url("assets/player-female-attack-outfit.png"); }
.player-sprite.attacking:not(.farmer-combat-active)[data-sex="female"] .sprite-layer.hair { background-image: var(--player-attack-hair-image, url("assets/player-female-attack-hair.png")); }
.player-sprite.attacking:not(.farmer-combat-active)[data-sex="female"] .sprite-layer.hat { background-image: url("assets/player-female-attack-hat.png"); }
.player-sprite.attacking:not(.farmer-combat-active)[data-weapon="wand"] .sprite-layer.sword { background-image: url("assets/player-attack-wand.png"); }
.player-sprite.attacking:not(.farmer-combat-active)[data-weapon="axe"] .sprite-layer.sword { background-image: url("assets/player-attack-axe.png"); }
.player-sprite.attacking:not(.farmer-combat-active)[data-weapon="mace"] .sprite-layer.sword { background-image: url("assets/player-attack-mace.png"); }
.player-sprite.attacking:not(.farmer-combat-active)[data-weapon="twinblade"] .sprite-layer.sword { background-image: url("assets/player-attack-twinblade.png"); }
.player-sprite.attacking:not(.farmer-combat-active)[data-weapon="spear"] .sprite-layer.base { background-image: url("assets/player-spear-base.png"); }
.player-sprite.attacking:not(.farmer-combat-active)[data-weapon="spear"] .sprite-layer.outfit { background-image: url("assets/player-spear-outfit.png"); }
.player-sprite.attacking:not(.farmer-combat-active)[data-weapon="spear"] .sprite-layer.hair { background-image: var(--player-spear-hair-image, url("assets/player-spear-hair.png")); }
.player-sprite.attacking:not(.farmer-combat-active)[data-weapon="spear"] .sprite-layer.hat { background-image: url("assets/player-spear-hat.png"); }
.player-sprite.attacking:not(.farmer-combat-active)[data-weapon="spear"] .sprite-layer.sword { background-image: url("assets/player-attack-spear.png"); }
.player-sprite.attacking:not(.farmer-combat-active)[data-weapon="spear"][data-sex="female"] .sprite-layer.base { background-image: url("assets/player-female-spear-base.png"); }
.player-sprite.attacking:not(.farmer-combat-active)[data-weapon="spear"][data-sex="female"] .sprite-layer.outfit { background-image: url("assets/player-female-spear-outfit.png"); }
.player-sprite.attacking:not(.farmer-combat-active)[data-weapon="spear"][data-sex="female"] .sprite-layer.hair { background-image: var(--player-spear-hair-image, url("assets/player-female-spear-hair.png")); }
.player-sprite.attacking:not(.farmer-combat-active)[data-weapon="spear"][data-sex="female"] .sprite-layer.hat { background-image: url("assets/player-female-spear-hat.png"); }
.player-sprite.attacking:not(.farmer-combat-active)[data-weapon="spear"][data-sex="female"] .sprite-layer.sword { background-image: url("assets/player-female-attack-spear.png"); }
/* Spear is always handled by the Farmer polearm routine now. Never flash the
   retired char_a spear between the routine clearing and `attacking` ending. */
.player-sprite.attacking:not(.farmer-combat-active)[data-weapon="spear"] > .sprite-layer.sword,
.remote-player.event-attacking:not(.farmer-combat-active)[data-weapon="spear"] > .sprite-layer.sword { display: none; }
.player-sprite.attacking:not(.farmer-combat-active)[data-weapon="unarmed"] .sprite-layer.sword { display: none; }
.player-sprite[data-weapon="sword"], .remote-player[data-weapon="sword"] { --farmer-weapon-image: url("assets/farmer/farmer-1hwpn-001.png"); }
.player-sprite[data-weapon="axe"], .remote-player[data-weapon="axe"] { --farmer-weapon-image: url("assets/farmer/farmer-1hwpn-002.png"); }
.player-sprite[data-weapon="metalstaff"], .remote-player[data-weapon="metalstaff"] { --farmer-weapon-image: url("assets/farmer/farmer-1hwpn-003.png"); }
.player-sprite[data-weapon="knife"], .remote-player[data-weapon="knife"] { --farmer-weapon-image: url("assets/farmer/farmer-1hwpn-001.png"); }
.player-sprite[data-weapon="mace"], .remote-player[data-weapon="mace"] { --farmer-weapon-image: url("assets/farmer/farmer-1hwpn-006.png"); }
.player-sprite[data-weapon="club"], .remote-player[data-weapon="club"] { --farmer-weapon-image: url("assets/farmer/farmer-1hwpn-007.png"); }
.player-sprite[data-weapon="twinblade"], .remote-player[data-weapon="twinblade"] { --farmer-weapon-image: url("assets/farmer/farmer-1hwpn-005.png"); }
/* "wand" covers a mage's rod and an alchemist's bone totem, and the pack ships
   no prop for either. This briefly borrowed the wooden staff as the closest
   silhouette, which just meant an alchemist visibly swung a plank -- a clearly
   wrong weapon reads worse than no weapon. Left unset deliberately: the layer
   draws var(--farmer-weapon-image), so with nothing bound the hand stays
   empty until there is real art for a totem. */
/* The Farmer System's own bow prop, for its own BOW SHOT animation. Four cells
   in one row rather than the 1hwpn sheets' four-by-two -- playFarmerAttack
   reads that grid from the routine. */
.player-sprite[data-weapon="bow"], .remote-player[data-weapon="bow"] { --farmer-weapon-image: url("assets/farmer/farmer-bow-002.png"); }
.player-sprite[data-weapon="spear"], .remote-player[data-weapon="spear"] { --farmer-weapon-image: url("assets/farmer/farmer-spear-001.png"); }
.player-sprite[data-weapon="staff"], .remote-player[data-weapon="staff"] { --farmer-weapon-image: url("assets/farmer/farmer-staff-001.png"); }
.player-sprite.farmer-combat-active[data-weapon="bow"] > .sprite-layer.sword,
.remote-player.farmer-combat-active[data-weapon="bow"] > .sprite-layer.sword { background-size: 400% 100%; }
.remote-player[data-sex="female"] .sprite-layer.base { background-image: url("assets/player-female-base.png"); }
.remote-player[data-sex="female"] .sprite-layer.outfit { background-image: url("assets/player-female-outfit.png"); }
.remote-player[data-sex="female"] .sprite-layer.hair { background-image: url("assets/player-female-hair.png"); }
.remote-player[data-sex="female"] .sprite-layer.hat { background-image: url("assets/player-female-hat.png"); }
.player-sprite[data-position="sitting"], .remote-player[data-position="sitting"] { transform: translate(-50%, -72%) scale(1.25); transform-origin: 50% 100%; }
.player-sprite[data-position="resting"], .remote-player[data-position="resting"] { transform: translate(-50%, -72%) scale(1.125, .775) rotate(-5deg); transform-origin: 50% 100%; }
.player-sprite[data-position="sleeping"], .remote-player[data-position="sleeping"] { transform: translate(-50%, -62%) rotate(82deg) scale(1.025); transform-origin: 50% 50%; }
.player-sprite[data-position="stunned"], .remote-player[data-position="stunned"],
.player-sprite[data-position="incapacitated"], .remote-player[data-position="incapacitated"],
.player-sprite[data-position="mortally-wounded"], .remote-player[data-position="mortally-wounded"] { transform: translate(-50%, -68%) rotate(68deg) scale(1.025); opacity: .7; }
.player-sprite[data-facing="south"] .sprite-layer { background-position-y: 0%; }
.player-sprite[data-facing="north"] .sprite-layer { background-position-y: 14.2857%; }
.player-sprite[data-facing="east"] .sprite-layer { background-position-y: 28.5714%; }
.player-sprite[data-facing="west"] .sprite-layer { background-position-y: 42.8571%; }
.player-sprite.walking[data-facing="south"] .sprite-layer { background-position-y: 57.1428%; }
.player-sprite.walking[data-facing="north"] .sprite-layer { background-position-y: 71.4285%; }
.player-sprite.walking[data-facing="east"] .sprite-layer { background-position-y: 85.7142%; }
.player-sprite.walking[data-facing="west"] .sprite-layer { background-position-y: 100%; }
.player-sprite.continuous-walk[data-facing="south"] .sprite-layer { background-position-y: 57.1428%; }
.player-sprite.continuous-walk[data-facing="north"] .sprite-layer { background-position-y: 71.4285%; }
.player-sprite.continuous-walk[data-facing="east"] .sprite-layer { background-position-y: 85.7142%; }
.player-sprite.continuous-walk[data-facing="west"] .sprite-layer { background-position-y: 100%; }
/* Combat sheets use their first four rows.  Keep these after every walking
 * selector so a held direction cannot display a walk/push row during a hit. */
.player-sprite.attacking:not(.farmer-combat-active)[data-facing="south"] .sprite-layer { background-position-y: 0%; }
.player-sprite.attacking:not(.farmer-combat-active)[data-facing="north"] .sprite-layer { background-position-y: 14.2857%; }
.player-sprite.attacking:not(.farmer-combat-active)[data-facing="east"] .sprite-layer { background-position-y: 28.5714%; }
.player-sprite.attacking:not(.farmer-combat-active)[data-facing="west"] .sprite-layer { background-position-y: 42.8571%; }
.remote-player[data-facing="south"] .sprite-layer { background-position-y: 0%; }
.remote-player[data-facing="north"] .sprite-layer { background-position-y: 14.2857%; }
.remote-player[data-facing="east"] .sprite-layer { background-position-y: 28.5714%; }
.remote-player[data-facing="west"] .sprite-layer { background-position-y: 42.8571%; }
.remote-player.walking[data-facing="south"] .sprite-layer { background-position-y: 57.1428%; }
.remote-player.walking[data-facing="north"] .sprite-layer { background-position-y: 71.4285%; }
.remote-player.walking[data-facing="east"] .sprite-layer { background-position-y: 85.7142%; }
.remote-player.walking[data-facing="west"] .sprite-layer { background-position-y: 100%; }
/* Mana Seed page-one column six is the directional crouched pose.  It reads
 * as a proper seated character and keeps every paper-doll layer aligned. */
.player-sprite[data-position="sitting"] .sprite-layer,
.remote-player[data-position="sitting"] .sprite-layer {
  background-position-x: 71.4285%;
  animation: seated-idle 1.8s steps(2, end) infinite;
}
@keyframes seated-idle { 0%, 70%, 100% { transform: translateY(0); } 75%, 95% { transform: translateY(1px); } }
.player-sprite.walking:not(.attacking) .sprite-layer, .remote-player.walking .sprite-layer { animation: walk-frame .38s steps(1) 1; }
@keyframes sword-slash {
  0%, 30% { background-position-x: 0%; }
  31%, 43% { background-position-x: 14.2857%; }
  44%, 56% { background-position-x: 28.5714%; }
  57%, 100% { background-position-x: 42.8571%; }
}
@keyframes farmer-strike { 45% { scale: 1.08; } }
@keyframes farmer-strike-south {
  0%, 27% { background-position: 20% 53.3333%; }
  28%, 42% { background-position: 26.6667% 53.3333%; }
  43%, 100% { background-position: 33.3333% 53.3333%; }
}
@keyframes farmer-strike-north {
  0%, 27% { background-position: 20% 60%; }
  28%, 42% { background-position: 26.6667% 60%; }
  43%, 100% { background-position: 33.3333% 60%; }
}
@keyframes farmer-strike-side {
  0%, 27% { background-position: 20% 66.6667%; }
  28%, 42% { background-position: 26.6667% 66.6667%; }
  43%, 100% { background-position: 33.3333% 66.6667%; }
}
@keyframes farmer-strike-side-west {
  0%, 27% { background-position: 26.6667% 66.6667%; }
  28%, 100% { background-position: 33.3333% 66.6667%; }
}
@keyframes walk-frame {
  0%, 16% { background-position-x: 0%; }
  17%, 32% { background-position-x: 14.2857%; }
  33%, 49% { background-position-x: 28.5714%; }
  50%, 66% { background-position-x: 42.8571%; }
  67%, 83% { background-position-x: 57.1428%; }
  84%, 100% { background-position-x: 71.4285%; }
}

.enemy { position: absolute; left: 70%; top: 52%; z-index: 4; padding: 0; color: inherit; background: transparent; border: 0; text-align: center; transform: translate(-50%, -66%); transition: left .12s linear, top .12s linear; }
.enemy > i { display: block; width: 58px; height: 58px; margin: auto; background: url("assets/monsters.png") 0 0 / 800% 300%; image-rendering: pixelated; animation: monster-idle 1.15s steps(2) infinite; transform-origin: 50% 85%; transform: scaleX(var(--npc-flip, 1)) scale(var(--mob-scale, 1)); }

/* A world boss: its own three-frame idle rather than the shared two-step
   bob, and no scaleY breathing on top of it -- the frames already animate.
   The origin sits at the feet so growing makes it loom over the tile instead
   of floating above it. */
.enemy.world-boss > i {
  /* The base rule sets `background` as a shorthand, which resets repeat to
     `repeat`. If the grid and the sheet ever disagree the cell no longer fills
     the box and the boss tiles into a wall of itself, so this says it once
     rather than relying on the two staying in step. */
  background-repeat: no-repeat;
  animation: boss-idle 0.62s steps(1) infinite;
  transform-origin: 50% 100%;
  filter: drop-shadow(0 0 5px #ff6a3c66) drop-shadow(0 3px 2px #0009);
}
/* Three frames across a single row, so the steps are 0, 50 and 100 percent. */
/* Three idle frames along the top row of a seven-wide sheet, so the steps are
   0, 1/6 and 2/6 of the way across. */
@keyframes boss-idle {
  0%   { background-position: 0% 0%; }
  33%  { background-position: 16.6667% 0%; }
  66%  { background-position: 33.3333% 0%; }
}
/* While a move is winding up the frames are driven from script, so the idle
   loop has to stand down or the two fight over background-position. */
.enemy.world-boss.boss-casting > i { animation: none; }
/*
 * A world boss stacks: sprite, name, then health.
 *
 * Every other creature hangs its health bar above its head, which works when
 * the sprite is 58px. On something nearly twice that the bar floated far above
 * the boss with the name stranded below it, reading as two separate things.
 * Laying the marker out as a column and ordering the parts keeps the bar with
 * the name it belongs to, and makes it wide enough to be worth reading.
 */
.enemy.world-boss { display: flex; flex-direction: column; align-items: center; }
.enemy.world-boss > .quest-marker { order: 0; }
.enemy.world-boss > i { order: 1; }
.enemy.world-boss > b { order: 2; color: #ffb37a; text-shadow: 0 0 6px #ff5a2a, 1px 1px #000; }
.enemy.world-boss > .enemy-health { order: 3; position: static; width: 104px; height: 8px; margin: 1px 0 0; transform: none; border-color: #ff9a5a; box-shadow: 0 0 6px #ff5a2a66; }
.enemy.world-boss > .enemy-health em { background: linear-gradient(90deg, #ff7a3c, #ffd08a); }
.enemy.world-boss > small { order: 4; }
.enemy.champion > i { filter: drop-shadow(0 0 4px #d7e894); }
.enemy.boss > i { width: 72px; height: 72px; filter: drop-shadow(0 0 7px #ffcf56); }
.enemy.world-boss { z-index: 8; }
.enemy.world-boss > i {
  width: 112px;
  height: 112px;
  filter: saturate(1.35) drop-shadow(0 0 12px #ffcf56) drop-shadow(0 8px 3px #0008);
  animation-duration: .9s;
}
/* Humanoid NPCs share the player's 54px cell but were missing the 1.25 scale
 * the player and remote players carry, so questmasters and townsfolk rendered
 * a fifth shorter than everyone else. */
.enemy.humanoid { width: 54px; height: 54px; pointer-events: none; transform: translate(-50%, -69%) scale(1.25); transform-origin: 50% 69%; }
.enemy.humanoid::before { content: ""; position: absolute; left: 50%; top: 35px; z-index: 0; width: 18px; height: 4px; border-radius: 50%; background: #102a1788; transform: translateX(-50%); filter: blur(.5px); pointer-events: none; }
.enemy.humanoid > i { position: relative; top: 0; z-index: 1; width: 54px; height: 54px; }
.enemy.humanoid > i:first-of-type { pointer-events: auto; clip-path: inset(9px 15px 7px); }
.enemy > i.npc-outfit, .enemy > i.npc-hair { position: absolute; left: 50%; top: 0; z-index: 2; display: block; width: 54px; height: 54px; margin: 0; background-size: 800% 800%; background-position: 0 0; transform: translateX(-50%) scaleX(var(--npc-flip, 1)); animation: npc-layer-idle 1.15s steps(2) infinite; pointer-events: none; }
.enemy > i.npc-hair { z-index: 3; }
.enemy.humanoid > i { animation: none; }
.enemy.npc-ryan > i.npc-hair { filter: sepia(1) saturate(1.8) brightness(.55); }
.enemy.npc-leah > i.npc-hair { filter: sepia(.35) saturate(1.3) brightness(1.2); }
.enemy.npc-lilly > i.npc-hair { filter: saturate(1.35) contrast(1.1); }
.enemy[data-npc="mira"] > i.npc-hair { filter: hue-rotate(165deg) saturate(.75) brightness(1.1); }
.enemy[data-npc="oren"] > i.npc-hair { filter: sepia(1) saturate(1.5) brightness(.65); }
.enemy[data-npc="tamsin"] > i.npc-hair { filter: sepia(.8) saturate(2) brightness(.7); }
.enemy[data-npc="puck"] > i.npc-hair { filter: sepia(1) saturate(2.2) hue-rotate(335deg) brightness(.75); }
.enemy[data-npc="elis"] > i.npc-hair { filter: grayscale(.75) brightness(1.05); }
.enemy.protected .enemy-health { display: none; }
.enemy.protected b { color: #ffe39a; background: linear-gradient(90deg, transparent, #101927c9 18%, #101927c9 82%, transparent); }
.mob-marker { pointer-events: auto; }
.mob-marker b { width: max-content; max-width: 170px; overflow: visible; font-size: 8px; white-space: normal; text-overflow: clip; }
.mob-marker small { font-size: 7px; }
.enemy .quest-marker { position: absolute; left: 50%; top: -28px; z-index: 6; display: none; width: 20px; height: 24px; place-items: center; color: #fff07a; font: 900 23px/1 system-ui, sans-serif; text-shadow: 0 2px #8b541e, 0 0 6px #ffe75a; transform: translateX(-50%); pointer-events: none; animation: quest-marker-bob .8s ease-in-out infinite alternate; }
.enemy.questmaster .quest-marker { display: grid; }
.enemy.questmaster.targeted .quest-marker { left: calc(50% + 18px); }
.enemy.questmaster.quest-complete .quest-marker { color: #a9afb3; text-shadow: 0 2px #343b40, 0 0 4px #d8dde0; animation: none; opacity: .78; }
/* Green: you are on this one's quest right now.
 *
 * Last, so it wins over both the gold "there is work here" and the grey
 * "you have done everything here" -- being mid-quest is the more useful thing
 * to know, and it is what tells you which of a town's questmasters is the one
 * actually waiting on you. Brighter and bobbing faster than the rest so it
 * stands out in a crowd of them. */
.enemy.questmaster.quest-active .quest-marker { color: #8dffa8; text-shadow: 0 2px #14532a, 0 0 8px #46f07a; opacity: 1; animation: quest-marker-bob .55s ease-in-out infinite alternate; }
@keyframes quest-marker-bob { to { translate: 0 -4px; filter: brightness(1.25); } }
.enemy .enemy-health { position: absolute; left: 50%; top: -8px; display: block; width: 48px; height: 6px; padding: 1px; overflow: hidden; background: #0f380f; border: 1px solid #d7e894; transform: translateX(-50%); box-shadow: 0 1px 2px #0f380f99; }
.enemy .enemy-health em { display: block; width: 100%; height: 100%; background: #8bac0f; transition: width .18s linear; }
.enemy .enemy-health em.low { background: #d7e894; animation: danger-pulse .45s steps(2) infinite; }
.enemy b { display: block; width: max-content; max-width: 170px; margin: 0 auto; padding: 3px 10px; overflow: visible; color: #fff4d2; background: linear-gradient(90deg, transparent, #101927c9 18%, #101927c9 82%, transparent); font-size: 10px; line-height: 1.15; text-shadow: 1px 1px #000, -1px 0 #000; white-space: normal; text-overflow: clip; }
.enemy small { display: block; padding: 1px 5px; color: #f2cc73; background: transparent; font-size: 8px; text-shadow: 1px 1px #000, -1px 0 #000; }
.enemy.humanoid > b { position: absolute; left: 50%; top: 45px; pointer-events: none; transform: translateX(-50%); }
.enemy.humanoid > small { position: absolute; left: 50%; top: 63px; pointer-events: none; transform: translateX(-50%); white-space: nowrap; }
/* Townspeople are readable when relevant without carpeting a busy square in
 * nameplates. Quest punctuation remains visible, and hostile labels are not
 * affected. Walking within about one tile adds .near-player in the renderer. */
.mob-marker.townsperson > b,
.mob-marker.townsperson > small { opacity: 0; visibility: hidden; transition: opacity .14s linear, visibility .14s linear; }
.mob-marker.townsperson.near-player > b,
.mob-marker.townsperson.near-player > small,
.mob-marker.townsperson.targeted > b,
.mob-marker.townsperson.targeted > small,
.mob-marker.townsperson:hover > b,
.mob-marker.townsperson:hover > small,
.mob-marker.townsperson:focus-visible > b,
.mob-marker.townsperson:focus-visible > small { opacity: 1; visibility: visible; }
.enemy.targeted > i { filter: drop-shadow(0 0 5px #d7e894); }
.mob-marker.targeted::after { content: "▼"; position: absolute; left: 50%; top: -28px; color: #fff279; font-size: 15px; line-height: 1; text-shadow: 1px 0 #8d2953, -1px 0 #8d2953, 0 1px #8d2953, 0 -1px #8d2953; transform: translateX(-50%); pointer-events: none; animation: target-chevron .7s steps(2) infinite; }
.mob-marker.targeted b { color: #fff8b8; text-shadow: 0 0 4px #fff279, 1px 1px #000, -1px 0 #000; }
.mob-marker.dangerous.targeted::after { color: #ff6c65; text-shadow: 0 0 5px #ff2e47, 1px 1px #4a0714; }
.mob-marker.dangerous.targeted b,
.mob-marker.dangerous.targeted small { color: #ffaaa0; text-shadow: 0 0 4px #ff344d, 1px 1px #26040a; }
@keyframes target-chevron { 50% { translate: 0 4px; filter: brightness(1.45); } }
.enemy.hurt { animation: monster-hit .28s steps(2); }
.enemy.hurt > i { animation: monster-hit-flash .28s steps(2); }
.enemy.attacking { animation: monster-lunge .36s ease-out; }
.enemy.mob-departure-ghost { z-index: 12 !important; pointer-events: none !important; transition: none !important; animation: mob-departure .68s linear forwards !important; }
.enemy.mob-departure-ghost > b,
.enemy.mob-departure-ghost > small,
.enemy.mob-departure-ghost > .enemy-health,
.enemy.mob-departure-ghost > .quest-marker,
.enemy.mob-departure-ghost::after { display: none !important; }
.enemy.mob-departure-ghost > i { animation-duration: .24s; }
@keyframes mob-departure { 0% { opacity: 1; } 72% { opacity: 1; } to { opacity: 0; translate: var(--depart-x, 90px) var(--depart-y, 0); } }
.enemy.mob-arriving { animation: mob-arrival .72s ease-out both !important; }
.enemy.mob-arriving > i { animation-duration: .22s; }
@keyframes mob-arrival { from { opacity: 0; translate: var(--arrive-x, -100px) var(--arrive-y, 0); } 22% { opacity: 1; } to { opacity: 1; translate: 0 0; } }
.enemy.mob-dashing { animation: mob-dash .38s ease-out !important; z-index: 13; }
.enemy.mob-dashing > i { animation-duration: .14s; }
@keyframes mob-dash { 35% { translate: 0 -13px; scale: 1.08 .92; } 70% { translate: 0 2px; scale: .96 1.05; } }
/* Carries --mob-scale through: a keyframe replaces the element's whole
   transform while it runs, so leaving it out would shrink an oversized
   creature back to normal on every other beat of its own idle. */
@keyframes monster-idle { 50% { transform: scaleX(var(--npc-flip, 1)) scale(var(--mob-scale, 1)) translateY(-2px) scaleY(1.025); } }
@keyframes npc-layer-idle { 50% { transform: translateX(-50%) scaleX(var(--npc-flip, 1)) translateY(-2px) scaleY(1.025); } }
@keyframes monster-hit { 25% { translate: 7px 0; } 55% { translate: -5px 0; } }
@keyframes monster-hit-flash { 20%, 65% { filter: brightness(3) saturate(.2) drop-shadow(0 0 7px #ff304f); } 45% { opacity: .35; } }
@keyframes monster-lunge { 45% { translate: -18px 7px; scale: 1.12; } }
@keyframes danger-pulse { 50% { opacity: .45; } }

/* --- a pointer that belongs to the game ------------------------------------
 *
 * Desktop only. A touch screen has no pointer to restyle, and setting one
 * there can suppress the tap highlight that tells a finger it landed.
 *
 * The arrow keeps its point at the very top-left pixel, which is where a
 * cursor is expected to bite; the diamond is the mark the interface already
 * puts beside an object, so anything wearing it is a thing you can act on.
 * `auto` and `pointer` stay as the fallbacks, so a browser that refuses the
 * image still gets the right SHAPE of cursor rather than none.
 */
@media (hover: hover) and (pointer: fine) {
  body,
  .world,
  .screen {
    cursor: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 16 16' shape-rendering='crispEdges'><path d='M2 1 L2 13.2 L5.4 10 L7.6 14.8 L10.2 13.6 L8.1 9.2 L12 9.2 Z' fill='%230f380f'/><path d='M3.2 3.1 L3.2 10.8 L5.7 8.4 L7.9 13 L8.9 12.5 L6.8 8.1 L9.8 8.1 Z' fill='%23f4f7dc'/></svg>") 4 2, auto;
  }
  button,
  a,
  summary,
  [role="button"],
  .room-entity,
  .room-structure,
  .enemy.mob-marker,
  .remote-player,
  .cache-action,
  .menu-command,
  .atlas-node {
    cursor: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 16 16' shape-rendering='crispEdges'><path d='M8 0.6 L10.1 5.9 L15.4 8 L10.1 10.1 L8 15.4 L5.9 10.1 L0.6 8 L5.9 5.9 Z' fill='%230f380f'/><path d='M8 2.9 L9.5 6.5 L13.1 8 L9.5 9.5 L8 13.1 L6.5 9.5 L2.9 8 L6.5 6.5 Z' fill='%23ffd76a'/></svg>") 16 16, pointer;
  }
  /* Dragging in the builder keeps the system's own grab hands: they say
   * something these two do not. */
  .world.edit-mode .room-entity.object,
  .world.edit-mode .enemy.mob-marker { cursor: grab; }
  .world.edit-mode .room-entity.object.dragging,
  .world.edit-mode .enemy.mob-marker.dragging { cursor: grabbing; }
  .cache-action.disabled { cursor: not-allowed; }
}

/* --- the shield on the off hand ---------------------------------------------
 *
 * Fifty-five shields have been wearable, and worth armour, for as long as the
 * slot has existed; none of them was ever drawn. The art comes from the sword
 * and shield pack at 32px a cell, which is the size the farmer rig already
 * uses for the weapon in the other hand, so it composites the same way rather
 * than needing a rig of its own.
 *
 * Eight cells per design: 1-4 the face of the shield at decreasing angles,
 * 5-7 its inside. Facing you, you see the face; walking away, the inside;
 * side on, the narrow view. The row picks the design -- wood, steel, kite.
 */
.shield-prop {
  position: absolute; display: none;
  left: calc(50% - 16px); top: calc(50% - 16px); width: 32px; height: 32px;
  /* One sheet carries all three designs; the row picks which. The
   * variable stays so a future coloured set can override it. */
  background-image: var(--shield-image, url("assets/shield-v01.png"));
  background-size: 800% 800%; background-repeat: no-repeat;
  image-rendering: pixelated; pointer-events: none;
  /* Between the body and anything worn over it, and always under the mount's
   * top half so a rider's shield cannot float over the animal's neck. */
  z-index: 4;
  transform: translateY(11px);
}
.player-sprite[data-shield] .shield-prop,
.remote-player[data-shield] .shield-prop { display: block; }
.player-sprite[data-shield=""] .shield-prop,
.remote-player[data-shield=""] .shield-prop { display: none; }

/* Design: the row. Wood, steel, kite. */
.player-sprite[data-shield="1"] .shield-prop,
.remote-player[data-shield="1"] .shield-prop { background-position-y: 0%; }
.player-sprite[data-shield="2"] .shield-prop,
.remote-player[data-shield="2"] .shield-prop { background-position-y: 14.2857%; }
.player-sprite[data-shield="3"] .shield-prop,
.remote-player[data-shield="3"] .shield-prop { background-position-y: 28.5714%; }

/* Which way it is turned: the column. Eight columns, so each is a seventh. */
/* Facing the viewer, a character's left arm -- the shield arm -- is on the
 * VIEWER's right, and facing away it is on their left. */
.player-sprite[data-facing="south"] .shield-prop,
.remote-player[data-facing="south"] .shield-prop {
  background-position-x: 14.2857%;                       /* cell 1, the face */
  transform: translate(7px, 12px);
}
.player-sprite[data-facing="north"] .shield-prop,
.remote-player[data-facing="north"] .shield-prop {
  background-position-x: 71.4286%;                       /* cell 5, the inside */
  transform: translate(-7px, 12px);
}
/* Sideways, only one of the two has the shield on the far arm. Going east it
 * is the far one and belongs behind the body; going west it is the near one
 * and belongs in front, which is also why west is the view that reads. */
.player-sprite[data-facing="east"] .shield-prop,
.remote-player[data-facing="east"] .shield-prop {
  background-position-x: 28.5714%;                       /* cell 2, turned */
  transform: translate(-4px, 11px);
  z-index: 0;
}
.player-sprite[data-facing="west"] .shield-prop,
.remote-player[data-facing="west"] .shield-prop {
  background-position-x: 28.5714%;
  transform: translate(4px, 11px) scaleX(-1);
  z-index: 4;
}

@media (prefers-reduced-motion: reduce) { .shield-prop { transition: none; } }

/* --- a house standing in a room ---------------------------------------------
 *
 * Capped at a quarter of the view in both directions, so the biggest tier in
 * the smallest window still leaves the room readable. The art keeps its own
 * proportions inside that box rather than being squashed to fit it, and the
 * button is anchored at the foot of the building so it sits ON the ground it
 * occupies rather than being centred through its roof.
 */
.room-structure {
  position: absolute; z-index: 4; padding: 0; border: 0; background: transparent;
  pointer-events: auto; transform: translate(-50%, -100%);
  display: flex; flex-direction: column; align-items: center;
  /* A loose ceiling only. The real size is the per-tier box below, in vh,
   * because a share of the screen is what the quarter-screen rule meant --
   * a percentage here is a share of the room layer, which is far shorter,
   * and it flattened every tier to one height. */
  max-width: 46%;
}
.room-structure > i {
  display: block; background-repeat: no-repeat; background-position: bottom center;
  background-size: contain; image-rendering: pixelated;
}
/* A box per tier, sized to the tallest building offered at that tier, so every
 * design draws at 1:1. Downscaling by a ratio like 0.3 is what turns a roof of
 * shingles into grey soup, and it is avoided rather than smoothed over.
 *
 * The vw/vh terms stay as the ceiling: on a screen too small to give a
 * building its full size it shrinks rather than dominating the room, which is
 * the quarter-of-the-screen promise. It is only there that scaling happens. */
.room-structure[data-tier="1"] > i { width: 25vw; max-width:  90px; height: 21vh; max-height: 166px; }
.room-structure[data-tier="2"] > i { width: 25vw; max-width: 128px; height: 24vh; max-height: 186px; }
.room-structure[data-tier="3"] > i { width: 25vw; max-width: 160px; height: 32vh; max-height: 251px; }
.room-structure > span {
  margin-top: 1px; padding: 1px 4px; font-size: 7px; letter-spacing: .4px;
  color: #d7e894; background: #0f380fcc; border: 1px solid #8bac0f;
  white-space: nowrap;
}
.room-structure.locked > span { color: #ffd0d0; border-color: #b06a6a; }
.room-structure:active > i { filter: brightness(1.2); }
@media (prefers-reduced-motion: reduce) { .room-structure { transition: none; } }

/* Names wrap rather than being cut off. A label reading "the singing ways..."
 * names nothing -- the whole point of putting it on screen is that you can
 * read what the thing is, and mob labels have wrapped like this all along. */
.room-entity { position: absolute; z-index: 3; width: max-content; max-width: 170px; padding: 3px 4px; pointer-events: auto; color: #d7e894; background: #0f380fcc; border: 1px solid #8bac0f; transform: translate(-50%, -50%); font-size: 9px; line-height: 1.15; white-space: normal; overflow: visible; text-overflow: clip; text-align: center; }
.room-entity.object::before { content: "◆ "; color: #9bbc0f; }
.room-entity.object > span { display: block; max-width: 162px; overflow: visible; text-overflow: clip; white-space: normal; }
.room-entity.object > small { display: block; margin-top: 2px; color: #fff0b5; font-size: 7px; letter-spacing: .4px; }
.room-entity.object.selected-object { outline: 2px solid #fff3a1; filter: brightness(1.35); }
.room-entity.object.decor-object { overflow: visible; background: transparent; border: 0; }
.room-entity.object.decor-object::before { content: none; }
.room-entity.object.decor-object > span,
.room-entity.object.decor-object > small { opacity: 0; transition: opacity .1s steps(1); }
.room-entity.object.decor-object:hover > span,
.room-entity.object.decor-object:hover > small,
.room-entity.object.decor-object:focus-visible > span,
.room-entity.object.decor-object:focus-visible > small,
.room-entity.object.decor-object.selected-object > span,
.room-entity.object.decor-object.selected-object > small { opacity: 1; }
/* pointer-events: none -- this art is drawn wider than the one tile its
 * button occupies (overflow: visible above), so without this a bench's own
 * artwork steals clicks meant for whatever is sitting in the tile next to it.
 * The button itself, anchored at the furniture's real tile, stays clickable. */
.room-entity.object.decor-object .decor-sprite { position: absolute; left: 50%; top: 0; display: block; pointer-events: none; background-image: var(--decor-image, url("assets/homestead-furnishings-v1.png")); background-size: var(--decor-sheet-size, 480px 160px); background-repeat: no-repeat; image-rendering: pixelated; transform: translateX(-50%) scaleX(var(--decor-flip, 1)); filter: drop-shadow(0 3px 2px #0008); }
.room-entity.object.decor-object > span, .room-entity.object.decor-object > small { width: max-content; max-width: 112px; margin-inline: auto; padding: 2px 4px; color: #fff0c1; background: #17120dcc; border: 1px solid #785b35; text-shadow: 1px 1px #000; }
.room-entity.object.decor-object > small { margin-top: 1px; color: #d4b879; }
.room-entity.object.decor-object > .cache-actions {
  display: flex;
  width: max-content;
  max-width: none;
  gap: 3px;
  padding: 2px 0 0;
  background: transparent;
  border: 0;
}
.cache-action {
  padding: 3px 5px;
  color: #fff4bd;
  background: #38260de8;
  border: 1px solid #d29b36;
  border-radius: 3px;
  box-shadow: 0 1px 2px #0009;
  font-size: 7px;
  font-weight: 800;
  letter-spacing: .25px;
  cursor: pointer;
}
.cache-action:active { translate: 0 1px; filter: brightness(1.35); }
.cache-action.disabled { color: #b8ab81; border-color: #6d624a; cursor: default; }
.room-entity.object.legendary-cache .decor-sprite {
  filter: sepia(.25) saturate(2.4) brightness(1.2) drop-shadow(0 0 8px #ffd34d);
  animation: legendary-cache-glow 1.15s ease-in-out infinite;
}
.room-entity.object.legendary-cache > span {
  color: #fff3a6;
  border-color: #f0bd38;
  box-shadow: 0 0 7px #ffc62e88;
}
@keyframes legendary-cache-glow {
  50% { filter: sepia(.15) saturate(3.2) brightness(1.55) drop-shadow(0 0 14px #ffe16a); }
}
/* Furniture is identifiable from its sprite and shares the tile of whoever
 * sits on it, so its name label is suppressed entirely. */
.room-entity.object.furniture-object > span,
.room-entity.object.furniture-object > small { display: none; }
.room-entity.object.decor-object .decor-sprite.lamp { animation: decor-lamp-glow 1.3s steps(2) infinite; }

/* Speech bubbles. Positioned from the speaker's own box, so the transform lifts
 * them clear of the sprite's head rather than centring on it. Never interactive
 * -- a bubble must not swallow a tap meant for the person under it. */
.speech-bubble {
  position: absolute;
  max-width: 148px;
  padding: 4px 7px;
  pointer-events: none;
  color: #16210f;
  background: #e8f5c8;
  border: 2px solid #2b3a1c;
  border-radius: 7px;
  transform: translate(-50%, calc(-100% - 9px));
  font-size: 9px;
  line-height: 1.35;
  text-align: center;
  overflow-wrap: anywhere;
  box-shadow: 0 2px 0 #0000004d;
  animation: bubble-pop .17s cubic-bezier(.2, 1.5, .4, 1) both;
}
.speech-bubble::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -7px;
  width: 0;
  height: 0;
  border: 5px solid transparent;
  border-top-color: #2b3a1c;
  transform: translateX(-50%);
}
.speech-bubble.fading { animation: bubble-fade .4s ease-in both; }
@keyframes bubble-pop { from { opacity: 0; transform: translate(-50%, calc(-100% - 2px)) scale(.8); } }
@keyframes bubble-fade { to { opacity: 0; transform: translate(-50%, calc(-100% - 16px)); } }
@media (prefers-reduced-motion: reduce) {
  .speech-bubble { animation: none; }
  .speech-bubble.fading { animation: bubble-fade .4s linear both; }
}
/* Ambient scenery. Never interactive: pointer-events stays off so a tree can
 * neither be tapped nor swallow a tap meant for the ground behind it, and the
 * walk-to solver keeps treating every tile as clear. The +11px matches where a
 * character's feet land relative to their own tile anchor, so props stand on
 * the same ground line rather than hovering above it. */
.ambient-decor { position: absolute; display: block; background-repeat: no-repeat; image-rendering: pixelated; pointer-events: none; transform: translate(-50%, calc(-100% + 11px)) scaleX(var(--decor-flip, 1)); filter: drop-shadow(0 3px 2px #0006); }
.ambient-decor.off-floor { opacity: .25; }
/* Anything leafy gets a slow sway, pivoting at its base. This uses the
 * standalone "rotate" property rather than a transform, because the transform
 * above is carrying the tile anchoring and the flip. */
.ambient-decor.grass, .ambient-decor.fern, .ambient-decor.reeds,
.ambient-decor.kelp, .ambient-decor.bush, .ambient-decor.scrub {
  transform-origin: 50% 100%;
  animation: decor-sway 3.6s ease-in-out infinite alternate;
}
/* Offset by prop so a whole room does not sway in lockstep. */
.ambient-decor.fern { animation-duration: 4.3s; animation-delay: -1.1s; }
.ambient-decor.reeds { animation-duration: 3.1s; animation-delay: -2.2s; }
.ambient-decor.kelp { animation-duration: 5.2s; animation-delay: -.7s; }
.ambient-decor.bush { animation-duration: 4.8s; animation-delay: -3.4s; }
.ambient-decor.scrub { animation-duration: 4.1s; animation-delay: -1.9s; }
.ambient-decor.crystal, .ambient-decor.obsidian { animation: decor-glimmer 2.8s ease-in-out infinite; }
@keyframes decor-sway { from { rotate: -1.4deg; } to { rotate: 1.4deg; } }
@keyframes decor-glimmer { 50% { filter: drop-shadow(0 3px 2px #0006) brightness(1.3); } }
@keyframes decor-lamp-glow { 50% { filter: brightness(1.35) drop-shadow(0 0 7px #74e8ff); } }
.room-entity.breakable-pot { min-width: 34px; max-width: 72px; padding: 34px 0 0; overflow: visible; color: #ffe6a6; background: transparent; border: 0; }
.room-entity.breakable-pot::before { content: ""; position: absolute; left: 50%; top: 0; width: 28px; height: 36px; background-image: var(--pot-sheet); background-repeat: no-repeat; background-size: 400% 200%; background-position: 0 0; image-rendering: pixelated; transform: translateX(-50%); filter: drop-shadow(0 2px 1px #102a1766); animation: pot-idle-sheet 1.4s steps(1) infinite; }
.room-entity.breakable-pot::after { content: none; }
.room-entity.breakable-pot > span { max-width: 72px; padding: 1px 3px; color: #ffe09a; background: #17120dbb; border: 1px solid #785b35; text-shadow: 1px 1px #000; }
.room-entity.breakable-pot > small { color: #ffe09a; }
.room-entity.breakable-pot.event-breaking { animation: pot-break-fade .62s linear both; }
.room-entity.breakable-pot.event-breaking::before { background-position-y: 100%; animation: pot-smash-sheet .58s steps(1) both; }
@keyframes pot-idle-sheet { 0%, 24% { background-position-x: 0%; } 25%, 49% { background-position-x: 33.333%; } 50%, 74% { background-position-x: 66.667%; } 75%, 100% { background-position-x: 100%; } }
@keyframes pot-smash-sheet { 0%, 24% { background-position-x: 0%; } 25%, 49% { background-position-x: 33.333%; } 50%, 74% { background-position-x: 66.667%; } 75%, 100% { background-position-x: 100%; } }
@keyframes pot-break-fade { 0%, 78% { opacity: 1; } to { opacity: 0; } }
/* Exactly the size of .player-sprite above, not 48/43.
 *
 * Another player is the same rig you are, so drawing them at 89% made everyone
 * else look like a child standing next to you. It also mattered past the
 * silhouette: the farmer and mount layers are inset -5px, so a 54px body gives
 * them the 64px cell their sheets are cut to. At 48px those layers were 58px
 * and every remote rider's mount was sampled a little short. */
/* Interpolated across the 200ms roster cadence rather than the old 20ms one:
   other people's positions now arrive five times a second, and a transition
   shorter than that gap would show the step instead of hiding it. */
.remote-player { position: absolute; z-index: 5; width: 54px; height: 54px; pointer-events: auto; transform: translate(-50%, -82%) scale(1.25); transform-origin: 50% 82%; transition: left .21s linear, top .21s linear; filter: drop-shadow(0 4px 1px #102a1766); }
.world.dedicated-scene .remote-player { width: 46px; height: 46px; }
/* Compact plate: name over thin HP and mana bars. The previous wide
 * "Name · 87%" strip crowded neighbouring entities. */
.remote-player b { position: absolute; left: 50%; top: 45px; display: grid; gap: 1px; width: max-content; min-width: 44px; max-width: 104px; padding: 2px 5px; color: #eaf6b4; background: #0f380fe6; border: 1px solid #4b7a3a; border-radius: 2px; transform: translateX(-50%); font-size: 9px; font-weight: 700; line-height: 1.2; letter-spacing: .2px; text-shadow: 1px 1px 0 #06170a, -1px 0 0 #06170a, 0 -1px 0 #06170a; }
.remote-player b span { overflow: hidden; text-align: center; white-space: nowrap; text-overflow: ellipsis; }
.remote-player b i { display: block; height: 3px; background: #08210b; border-radius: 1px; }
.remote-player b i em { display: block; height: 100%; transition: width .2s linear; }
.remote-player b .hp em { background: #7ada6a; }
.remote-player b .mp em { background: #6bb8ff; }
.room-entity.off-floor, .remote-player.off-floor, .enemy.off-floor { opacity: .25; pointer-events: none; }
.player-sprite.hurt { animation: player-hit .4s steps(2); }
.player-sprite.event-hurt, .remote-player.event-hurt { animation: player-hit .4s steps(2); }
.remote-player.event-attacking:not(.farmer-combat-active) { animation: remote-attack .43s ease-out; }
.remote-player.event-casting { animation: remote-cast .62s ease-out; }
.remote-player.event-attacking:not(.farmer-combat-active) .sprite-layer,
.remote-player.event-casting .sprite-layer { animation: sword-slash .52s steps(1) 1; }
.remote-player.event-attacking:not(.farmer-combat-active) .sprite-layer.base,
.remote-player.event-casting .sprite-layer.base { background-image: url("assets/player-attack-base.png"); }
.remote-player.event-attacking:not(.farmer-combat-active) .sprite-layer.outfit,
.remote-player.event-casting .sprite-layer.outfit { background-image: url("assets/player-attack-outfit.png"); }
.remote-player.event-attacking:not(.farmer-combat-active) .sprite-layer.hair,
.remote-player.event-casting .sprite-layer.hair { background-image: var(--player-attack-hair-image, url("assets/player-attack-hair.png")); }
.remote-player.event-attacking:not(.farmer-combat-active) .sprite-layer.hat,
.remote-player.event-casting .sprite-layer.hat { background-image: url("assets/player-attack-hat.png"); }
.remote-player.event-attacking:not(.farmer-combat-active) .sprite-layer.sword,
.remote-player.event-casting .sprite-layer.sword { display: block; z-index: 10; background-image: url("assets/player-attack-sword.png"); }
.remote-player.event-attacking:not(.farmer-combat-active)[data-sex="female"] .sprite-layer.base,
.remote-player.event-casting[data-sex="female"] .sprite-layer.base { background-image: url("assets/player-female-attack-base.png"); }
.remote-player.event-attacking:not(.farmer-combat-active)[data-sex="female"] .sprite-layer.outfit,
.remote-player.event-casting[data-sex="female"] .sprite-layer.outfit { background-image: url("assets/player-female-attack-outfit.png"); }
.remote-player.event-attacking:not(.farmer-combat-active)[data-sex="female"] .sprite-layer.hair,
.remote-player.event-casting[data-sex="female"] .sprite-layer.hair { background-image: var(--player-attack-hair-image, url("assets/player-female-attack-hair.png")); }
.remote-player.event-attacking:not(.farmer-combat-active)[data-sex="female"] .sprite-layer.hat,
.remote-player.event-casting[data-sex="female"] .sprite-layer.hat { background-image: url("assets/player-female-attack-hat.png"); }
.remote-player.event-attacking:not(.farmer-combat-active)[data-weapon="wand"] .sprite-layer.sword,
.remote-player.event-casting .sprite-layer.sword { background-image: url("assets/player-attack-wand.png"); }
.remote-player.event-attacking:not(.farmer-combat-active)[data-weapon="axe"] .sprite-layer.sword { background-image: url("assets/player-attack-axe.png"); }
.remote-player.event-attacking:not(.farmer-combat-active)[data-weapon="mace"] .sprite-layer.sword { background-image: url("assets/player-attack-mace.png"); }
.remote-player.event-attacking:not(.farmer-combat-active)[data-weapon="twinblade"] .sprite-layer.sword { background-image: url("assets/player-attack-twinblade.png"); }
.remote-player.event-attacking:not(.farmer-combat-active)[data-weapon="spear"] .sprite-layer.base { background-image: url("assets/player-spear-base.png"); }
.remote-player.event-attacking:not(.farmer-combat-active)[data-weapon="spear"] .sprite-layer.outfit { background-image: url("assets/player-spear-outfit.png"); }
.remote-player.event-attacking:not(.farmer-combat-active)[data-weapon="spear"] .sprite-layer.hair { background-image: var(--player-spear-hair-image, url("assets/player-spear-hair.png")); }
.remote-player.event-attacking:not(.farmer-combat-active)[data-weapon="spear"] .sprite-layer.hat { background-image: url("assets/player-spear-hat.png"); }
.remote-player.event-attacking:not(.farmer-combat-active)[data-weapon="spear"] .sprite-layer.sword { background-image: url("assets/player-attack-spear.png"); }
.remote-player.event-attacking:not(.farmer-combat-active)[data-weapon="spear"][data-sex="female"] .sprite-layer.base { background-image: url("assets/player-female-spear-base.png"); }
.remote-player.event-attacking:not(.farmer-combat-active)[data-weapon="spear"][data-sex="female"] .sprite-layer.outfit { background-image: url("assets/player-female-spear-outfit.png"); }
.remote-player.event-attacking:not(.farmer-combat-active)[data-weapon="spear"][data-sex="female"] .sprite-layer.hair { background-image: var(--player-spear-hair-image, url("assets/player-female-spear-hair.png")); }
.remote-player.event-attacking:not(.farmer-combat-active)[data-weapon="spear"][data-sex="female"] .sprite-layer.hat { background-image: url("assets/player-female-spear-hat.png"); }
.remote-player.event-attacking:not(.farmer-combat-active)[data-weapon="spear"][data-sex="female"] .sprite-layer.sword { background-image: url("assets/player-female-attack-spear.png"); }
.remote-player.event-attacking:not(.farmer-combat-active)[data-weapon="unarmed"] .sprite-layer.sword { display: none; }
.player-sprite.farmer-combat-active,
.remote-player.farmer-combat-active { animation: farmer-strike .72s ease-out; }
.player-sprite.farmer-combat-active .farmer-layer,
.remote-player.farmer-combat-active .farmer-layer { animation: none !important; }
.player-sprite.farmer-combat-active > .sprite-layer.sword,
.remote-player.farmer-combat-active > .sprite-layer.sword {
  display: block !important;
  visibility: visible;
  position: absolute;
  inset: auto;
  left: calc(50% - 16px);
  top: calc(50% - 16px);
  width: 32px;
  height: 32px;
  z-index: 10;
  background-image: var(--farmer-weapon-image);
  background-size: 400% 200%;
  background-repeat: no-repeat;
  image-rendering: pixelated;
  animation: none !important;
  scale: 1 1 !important;
  transform-origin: 50% 50%;
}
.remote-player.event-casting::before { content: "✦"; position: absolute; z-index: 8; left: 50%; top: -8px; color: #f2dcff; font-size: 19px; text-shadow: 0 0 5px #8d4dff, 0 0 10px #5be9ff; transform: translateX(-50%); animation: caster-spark .62s ease-out both; }
.enemy.event-attacking { animation: monster-lunge .43s ease-out; }
.player-sprite.healed, .remote-player.healed, .enemy.healed { animation: healing-glow .65s steps(3); }
.player-sprite.event-death, .remote-player.event-death, .enemy.event-death { animation: event-death .8s ease-in both; }
.player-sprite.posture-change, .remote-player.posture-change { animation: posture-pop .36s ease-out; }
.player-sprite.event-flee, .remote-player.event-flee { animation: flee-dash .5s ease-in both; }
.room-entity.interaction-pulse { animation: interaction-pulse .45s steps(2); }
.room-entity.event-picked-up { animation: item-pickup .38s ease-in both; }
.room-entity.event-sacrificed { animation: sacrifice-rise .7s ease-in both; }
.world.event-sacrifice-glow { animation: sacrifice-glow .7s ease-out; }
.game-event-fx.sacrifice { color: #fff2a3; text-shadow: 0 0 5px #f5c451, 2px 0 #63351b, -2px 0 #63351b, 0 2px #63351b, 0 -2px #63351b; }
.screen.player-hurt { animation: screen-hit .4s steps(2); }
.damage-fx { position: absolute; z-index: 12 !important; width: 1px; height: 1px; pointer-events: none; transform: translate(-50%, -50%); }
.damage-fx b { position: absolute; left: -22px; top: -35px; min-width: 44px; color: #fff1dc; text-align: center; font-size: 15px; font-weight: 900; text-shadow: 2px 0 #8e1025, -2px 0 #8e1025, 0 2px #8e1025, 0 -2px #8e1025; animation: damage-number .7s ease-out both; }
.damage-fx b small { display: block; margin-top: -2px; color: #ffc5ac; font-size: 7px; line-height: 8px; text-transform: uppercase; white-space: nowrap; }
.damage-fx i { position: absolute; width: 5px; height: 5px; background: #cf1738; box-shadow: 0 0 0 1px #68101f; animation: blood-pixel .55s ease-out both; }
.damage-fx i:nth-of-type(1) { --dx: -25px; --dy: -22px; }.damage-fx i:nth-of-type(2) { --dx: 22px; --dy: -28px; animation-delay: .03s; }
.damage-fx i:nth-of-type(3) { --dx: -17px; --dy: 12px; animation-delay: .05s; }.damage-fx i:nth-of-type(4) { --dx: 28px; --dy: 8px; }
.damage-fx i:nth-of-type(5) { --dx: -5px; --dy: -34px; }.damage-fx i:nth-of-type(6) { --dx: 10px; --dy: 20px; animation-delay: .04s; }
.damage-fx i:nth-of-type(7) { --dx: 31px; --dy: -10px; animation-delay: .07s; }
.damage-fx.player-damage i { background: #ff5b45; }
.damage-fx.magic-damage b { color: #e6dcff; text-shadow: 2px 0 #50309c, -2px 0 #50309c, 0 2px #50309c, 0 -2px #50309c; }
.damage-fx.magic-damage b small { color: #bda9ff; }
.damage-fx.magic-damage i { background: #926cff; box-shadow: 0 0 0 1px #43278b, 0 0 5px #bba8ff; transform: rotate(45deg); }
@keyframes damage-number { from { opacity: 0; transform: translateY(5px) scale(.7); } 25% { opacity: 1; transform: translateY(-7px) scale(1.18); } to { opacity: 0; transform: translateY(-25px); } }
@keyframes blood-pixel { from { opacity: 1; transform: translate(0, 0) scale(1.4); } to { opacity: 0; transform: translate(var(--dx), var(--dy)) scale(.4); } }
@keyframes player-hit { 20% { translate: -5px 0; filter: brightness(2) sepia(1) saturate(6) hue-rotate(315deg); } 45% { translate: 6px 0; opacity: .5; } }
@keyframes screen-hit { 20%, 55% { box-shadow: inset 0 0 55px 16px #d91635cc, 0 0 0 2px #7c7f6d; } }
@keyframes remote-attack { 45% { translate: 18px -5px; scale: 1.12; } }
@keyframes healing-glow { 35%, 70% { filter: brightness(1.9) drop-shadow(0 0 9px #d7ff83); } }
@keyframes event-death { 35% { filter: grayscale(1) brightness(2); } to { opacity: 0; translate: 0 18px; rotate: 16deg; scale: .65; } }
@keyframes posture-pop { 45% { scale: 1.14 .82; } }
@keyframes flee-dash { 35% { opacity: 1; scale: .84 1.16; } to { opacity: .15; translate: 28px 0; scale: 1.25 .72; } }
@keyframes interaction-pulse { 50% { color: #0f380f; background: #d7e894; scale: 1.13; } }
@keyframes item-pickup { to { opacity: 0; translate: 0 -28px; scale: .4; } }
.game-event-fx { position: absolute; z-index: 14 !important; width: 70px; margin-left: -35px; pointer-events: none; color: #fff1dc; text-align: center; font-size: 14px; font-weight: 900; text-shadow: 2px 0 #0f380f, -2px 0 #0f380f, 0 2px #0f380f, 0 -2px #0f380f; animation: game-event-rise .82s ease-out both; }
.game-event-fx.heal { color: #d7ff83; font-size: 17px; }
.game-event-fx.miss { color: #e9e6ba; font-size: 12px; }
.game-event-fx.death { color: #ff5064; font-size: 27px; }
.game-event-fx.pickup, .game-event-fx.drop { color: #ffe97a; font-size: 11px; }
.game-event-fx.coin { color: #ffe06a; font-size: 13px; text-shadow: 2px 0 #70401e, -2px 0 #70401e, 0 2px #70401e, 0 -2px #70401e, 0 0 7px #fff2a1; }
.game-event-fx.break { color: #ffd17c; font-size: 15px; }
.game-event-fx.interaction { color: #bff4ff; font-size: 10px; }
.game-event-fx.flee { color: #ffe97a; font-size: 13px; }
.game-event-fx.boss-phase { color: #ffcf56; font-size: 15px; }
.game-event-fx.enemy-special { color: #ff795c; font-size: 13px; }
.game-event-fx.enemy-interrupt { color: #9fffd2; font-size: 12px; }
@keyframes game-event-rise { from { opacity: 0; translate: 0 8px; scale: .7; } 22% { opacity: 1; scale: 1.2; } to { opacity: 0; translate: 0 -34px; scale: .9; } }
.world.event-shake { animation: world-shake .14s steps(2) infinite; }
.world.event-flash::after { animation: world-flash .52s steps(3); }
.enemy.boss-phase, .remote-player.boss-phase { animation: boss-phase .9s steps(4); }
.enemy.enemy-casting { animation: enemy-casting .38s steps(2) infinite; }
.enemy.enemy-interrupted { animation: enemy-interrupted .5s steps(3); }
@keyframes world-shake { 25% { translate: -4px 2px; } 50% { translate: 4px -2px; } 75% { translate: -2px -3px; } }
@keyframes world-flash { 25%, 60% { background: #e8ffb4bb; } }
@keyframes boss-phase { 25%, 65% { filter: brightness(2.5) saturate(3) drop-shadow(0 0 12px #ffcf56); scale: 1.18; } }
@keyframes enemy-casting { 50% { filter: brightness(1.75) saturate(2.2) drop-shadow(0 0 9px #ff593f); } }
@keyframes enemy-interrupted { 45% { filter: brightness(2.2) sepia(1) hue-rotate(80deg); translate: -3px 0; } 70% { translate: 3px 0; } }

/* Server-authoritative danger areas. Their geometry is calculated from the
 * same room grid as movement, so getting outside the painted shape really is
 * getting outside the hit test. */
.enemy-telegraph { position: absolute; z-index: 7; pointer-events: none; box-sizing: border-box; color: #ffe2a3; opacity: .92; }
.enemy-telegraph > i { position: absolute; inset: 0; display: block; box-sizing: border-box; border: 3px solid #ff553f; background: #ff3b272b; box-shadow: inset 0 0 14px #ffbd4a80, 0 0 9px #ff382fcc; animation: enemy-warning var(--cast-ms, 1s) steps(5) both; }
.enemy-telegraph > b { position: absolute; z-index: 1; top: 50%; left: 50%; color: #fff0b8; font-size: 9px; letter-spacing: .08em; text-shadow: 1px 1px #6b1010, -1px -1px #6b1010; translate: -50% -50%; animation: enemy-warning-label .38s steps(2) infinite; }
.enemy-telegraph.circle { translate: -50% -50%; }
.enemy-telegraph.circle > i { border-radius: 50%; background: repeating-radial-gradient(circle, #ff4a2433 0 7px, #ffb52b22 8px 13px); }
.enemy-telegraph.line { translate: 0 -50%; transform-origin: left center; }
.enemy-telegraph.line > i { border-radius: 999px; background: repeating-linear-gradient(90deg, #ff3d2533 0 10px, #ffb22925 11px 18px); }
@keyframes enemy-warning { from { opacity: .35; filter: saturate(.7); } 70% { opacity: .8; } to { opacity: 1; filter: saturate(2) brightness(1.5); box-shadow: inset 0 0 20px #fff09a99, 0 0 15px #ff2828; } }
@keyframes enemy-warning-label { 50% { opacity: .42; } }
.action-buttons button.cooldown { filter: grayscale(.8); transform: translateY(3px); }
.message { position: absolute; z-index: 10; bottom: 9px; left: 50%; width: max-content; max-width: calc(100% - 28px); padding: 5px 11px; overflow: hidden; color: #fff4d2; background: #101927dc; border: 1px solid #d8ad5a99; border-radius: 12px; box-shadow: 0 3px 8px #0009; opacity: 0; text-align: center; text-shadow: 1px 1px #000; transform: translate(-50%, 8px); transition: opacity .2s, transform .2s; pointer-events: none; font-size: 10px; line-height: 1.25; }
.message.visible { opacity: 1; transform: translate(-50%, 0); }
.message.world-news { max-width: calc(100% - 48px); color: #fff1a8; background: #281c0aeb; border-color: #f2c75e; border-radius: 5px; box-shadow: 0 3px 8px #0009, inset 0 0 8px #f2c7522; font-size: 9px; letter-spacing: .02em; }

.game-shortcuts { position: absolute; /* Above the room entities, which compute their own z from their tile and
   * reach about 20. This is a stacking context, so nothing inside it can
   * rise past this number however high its own z-index is set. */
  z-index: 24; inset: 0; display: block; pointer-events: none; }
.game-shortcuts[hidden] { display: none; }
.game-shortcuts button { position: absolute; width: 34px; height: 34px; min-width: 0; padding: 2px 1px; overflow: hidden; color: #fff4d2; background: #101927ee; border: 2px solid #79dbea; border-radius: 4px; box-shadow: 0 2px 0 #0009, inset 0 0 7px #79dbea22; text-shadow: 1px 1px #000; pointer-events: auto; }
.game-shortcuts button:active { translate: 0 2px; }
.quick-slot { left: 7px; }
.quick-slot[data-slot="0"] { top: 52px; }.quick-slot[data-slot="1"] { top: 90px; }
.quick-slot[data-slot="2"] { top: 128px; }.quick-slot[data-slot="3"] { top: 166px; }
.quick-slot[data-slot="4"] { top: 52px; }.quick-slot[data-slot="5"] { top: 90px; }
.quick-slot[data-slot="6"] { top: 128px; }.quick-slot[data-slot="7"] { top: 166px; }
.quick-slot small { position: absolute; top: 1px; left: 3px; color: #79dbea; font-size: 7px; }
.quick-slot .ability-icon { display: grid; width: 26px; height: 26px; margin: 1px auto 0; place-items: center; border: 1px solid currentColor; border-radius: 50%; font-style: normal; font-size: 20px; line-height: 1; box-shadow: inset 0 0 7px currentColor, 0 0 4px #000; }
/* A drawn icon fills the slot rather than sitting inside a glyph ring: the
 * ring exists to give a bare character an edge, and a 32px illustration has
 * its own. Square, because these are painted to the corners of their cell. */
.quick-slot .ability-icon.drawn { background-repeat: no-repeat; border-radius: 4px; border-color: #ffffff2e; box-shadow: 0 0 4px #000; image-rendering: pixelated; }
.quick-slot .ability-icon.arcane { color: #d9b9ff; }.quick-slot .ability-icon.frost { color: #bcefff; }
.quick-slot .ability-icon.ward { color: #8fd8ff; }.quick-slot .ability-icon.fire { color: #ff9d62; }
.quick-slot .ability-icon.sleep, .quick-slot .ability-icon.shadow { color: #b7a7e9; }
.quick-slot .ability-icon.lightning { color: #fff279; }.quick-slot .ability-icon.heal { color: #8dff9d; }
.quick-slot .ability-icon.holy { color: #fff2a1; }.quick-slot .ability-icon.earth { color: #e3bb7d; }
.quick-slot .ability-icon.utility { color: #b9ebdd; }.quick-slot .ability-icon.blade { color: #e6edf7; }
.quick-slot .ability-icon.strike { color: #ffbd8a; }.quick-slot .ability-icon.empty { color: #79dbea; }
.quick-slot.empty { color: #79dbea; border-style: dashed; }
.quick-slot.locked, .quick-slot.recovering { opacity: .55; filter: grayscale(.7); }
.backpack-button { right: 7px; bottom: 50px; display: grid; grid-template-columns: 1fr; justify-items: center; align-content: center; width: 38px !important; height: 36px !important; }
.backpack-button i { position: relative; width: 18px; height: 18px; background: #b9773e; border: 2px solid #ffe0a3; border-radius: 3px 3px 5px 5px; box-shadow: inset 0 -4px #7b4028; }
.backpack-button i::before { content: ""; position: absolute; top: -7px; left: 3px; width: 8px; height: 7px; border: 2px solid #ffe0a3; border-bottom: 0; border-radius: 5px 5px 0 0; }
.backpack-button small { margin-top: 1px; font-size: 7px; font-weight: 900; }
.score-button { right: 49px; bottom: 50px; display: grid; grid-template-columns: 1fr; justify-items: center; align-content: center; width: 38px !important; height: 36px !important; }
.score-button i { position: relative; width: 20px; height: 22px; background: #f2d897; border: 2px solid #8b572a; border-radius: 2px; box-shadow: inset -3px 0 #c99850; }
.score-button i::before { content: ""; position: absolute; top: 4px; left: 4px; width: 8px; height: 3px; border-top: 2px solid #70401e; border-bottom: 2px solid #70401e; box-shadow: 0 5px #70401e; }
.score-button small { margin-top: 1px; font-size: 7px; font-weight: 900; }
/* Bottom-left, mirroring the backpack on the right. Only shown while a potion
 * is actually carried, the same way STAND and FLEE appear on demand. */
.potion-button { left: 7px; bottom: 50px; display: grid; grid-template-columns: 1fr; justify-items: center; align-content: center; background: #1b1030ee !important; border-color: #c58bff !important; box-shadow: 0 0 9px #a862ff, inset 0 0 8px #c58bff33 !important; }
.potion-button[hidden] { display: none; }
.potion-button i { position: relative; width: 11px; height: 14px; background: linear-gradient(#f6a2ff 0 38%, #d94bd6 38% 100%); border: 2px solid #ffe0a3; border-radius: 2px 2px 6px 6px; box-shadow: inset 0 -3px #8d2a8f; }
.potion-button i::before { content: ""; position: absolute; top: -6px; left: 2px; width: 5px; height: 5px; background: #c9b48f; border: 2px solid #ffe0a3; border-bottom: 0; border-radius: 2px 2px 0 0; }
.potion-button em { position: absolute; top: 1px; right: 3px; color: #ffe9a8; font-style: normal; font-size: 8px; font-weight: 900; text-shadow: 1px 1px #000; }
.potion-button small { margin-top: 2px; font-size: 7px; font-weight: 900; }
.social-button { left: 7px; bottom: 90px; display: grid; place-items: center; align-content: center; background: #1c3320ee !important; border-color: #8ddc9a !important; box-shadow: 0 0 9px #4fbf68, inset 0 0 8px #8ddc9a33 !important; }
.social-button b { color: #c9ffd4; font-size: 17px; line-height: 14px; text-shadow: 0 0 5px #4fbf68; }
.social-button small { font-size: 7px; font-weight: 900; }
/* World edit mode (implementors only). */
.edit-button { left: 7px; bottom: 170px; display: grid; place-items: center; align-content: center; background: #33240fee !important; border-color: #f0cb68 !important; box-shadow: 0 0 9px #d89b35, inset 0 0 8px #f0cb6833 !important; }
.recall-button { left: 7px; bottom: 130px; display: grid; place-items: center; align-content: center; background: #10222eee !important; border-color: #6fd0f0 !important; box-shadow: 0 0 9px #38a8d8, inset 0 0 8px #6fd0f033 !important; }
.recall-button b { color: #cdf1ff; font-size: 15px; line-height: 14px; text-shadow: 0 0 5px #38a8d8; }
.recall-button small { font-size: 7px; font-weight: 900; }
.recall-button[disabled] { opacity: .45; filter: grayscale(.5); }
.edit-button[hidden] { display: none; }
.edit-button b { color: #ffe9a8; font-size: 17px; line-height: 14px; text-shadow: 0 0 5px #d89b35; }
.edit-button small { font-size: 7px; font-weight: 900; }
.edit-button.active { background: #6d4a0fee !important; animation: exit-glow 1.1s ease-in-out infinite alternate; }
.world.edit-mode .room-entity.object { cursor: grab; outline: 1px dashed #f0cb6899; touch-action: none; }
/* Furniture normally hides its label, which makes a piece standing on your own
 * tile invisible behind your sprite. In edit mode you need to see what is
 * there, so labels come back for everything. */
.world.edit-mode .room-entity.object.furniture-object > span { display: block; }
.world.edit-mode .room-entity.object.dragging { cursor: grabbing; outline: 2px dashed #fff3a1; opacity: .8; filter: brightness(1.3); }
/* NPCs are draggable too. Humanoids normally set pointer-events:none on the
 * container so taps fall through to the sprite; edit mode needs the whole
 * marker grabbable. */
.world.edit-mode .enemy.mob-marker { pointer-events: auto !important; cursor: grab; touch-action: none; }
.world.edit-mode .enemy.mob-marker.dragging { cursor: grabbing; opacity: .8; filter: brightness(1.3); }
/* Second bank of ability slots, mirroring the left column. */
.quick-slot.right { left: auto; right: 7px; }
/* A pose drives the farmer layers frame by frame from JS, so the idle and walk
 * keyframes must not fight it -- same arrangement as farmer-combat-active. */
.player-sprite.farmer-pose-active .farmer-layer,
.remote-player.farmer-pose-active .farmer-layer { animation: none !important; }
/* Clear of the icon row below it (which tops out at 523) and of the down
 * exit beside it. It used to sit at bottom:90px, directly on top of the
 * help button, where the tap went to help and STAND was never seen. */
.stand-button { right: 7px; bottom: 136px; display: grid; width: 38px !important; height: 36px !important; place-items: center; align-content: center; color: #fff6c9 !important; background: #24485aee !important; border-color: #f0cb68 !important; box-shadow: 0 0 9px #79dbea, inset 0 0 8px #f0cb6833 !important; }
.stand-button[hidden] { display: none; }
/* Sits above STAND rather than beside it: the two can both be showing (seated
   on a bench while mounted is possible) and FLEE already owns the 90px slot. */
.dismount-button { right: 7px; bottom: 130px; display: grid; width: 38px !important; height: 36px !important; place-items: center; align-content: center; color: #fff6c9 !important; background: #3b2a52ee !important; border-color: #c9a2f0 !important; box-shadow: 0 0 9px #a97ce8, inset 0 0 8px #c9a2f033 !important; }
.dismount-button[hidden] { display: none; }
.dismount-button b { color: #e6d2ff; font-size: 20px; line-height: 17px; text-shadow: 0 0 5px #a97ce8; }
.dismount-button small { font-size: 7px; font-weight: 900; }
.stand-button b { color: #fff3a1; font-size: 20px; line-height: 17px; text-shadow: 0 0 5px #79dbea; }
.stand-button small { font-size: 7px; font-weight: 900; }
.panic-flee { right: 7px; bottom: 90px; display: grid; width: 38px !important; height: 36px !important; place-items: center; align-content: center; color: #fff4dc !important; background: #8e1730ee !important; border-color: #ff7185 !important; box-shadow: 0 0 9px #ff3855, inset 0 0 8px #ff718577 !important; animation: panic-flee-pulse .55s steps(2) infinite; }
.panic-flee[hidden] { display: none; }
/* The live event here, offered rather than hidden behind the quest banner.
   Same shelf as STAND and GET OFF, in the shrine's own gold so it reads as an
   opportunity rather than a warning. */
.event-button { right: 7px; bottom: 170px; display: grid; width: 38px !important; height: 36px !important; place-items: center; align-content: center; color: #fff6c9 !important; background: #4a3a12ee !important; border-color: #f0cb68 !important; box-shadow: 0 0 9px #ffcf56, inset 0 0 8px #f0cb6844 !important; animation: event-button-pulse 1.4s ease-in-out infinite alternate; }
.event-button[hidden] { display: none; }
.event-button b { color: #ffe9a8; font-size: 18px; line-height: 16px; text-shadow: 0 0 6px #ffcf56; }
.event-button small { max-width: 36px; overflow: hidden; font-size: 6px; font-weight: 900; text-overflow: ellipsis; white-space: nowrap; }
@keyframes event-button-pulse {
  from { box-shadow: 0 0 6px #ffcf56, inset 0 0 8px #f0cb6844; }
  to   { box-shadow: 0 0 13px #ffe9a8, inset 0 0 8px #f0cb6866; }
}
@media (prefers-reduced-motion: reduce) { .event-button { animation: none; } }
.panic-flee b { font-size: 19px; line-height: 17px; }.panic-flee small { font-size: 8px; font-weight: 900; }
@keyframes panic-flee-pulse { 50% { scale: 1.08; filter: brightness(1.45); } }

.quick-menu { position: absolute; z-index: 15; left: 4px; right: 4px; bottom: 4px; height: min(72%, 420px); display: grid; grid-template-rows: 38px 1fr; padding: 8px; color: #38220f; background: linear-gradient(90deg, #a56d34, #efd18a 6%, #f7e2aa 50%, #e5bf70 94%, #915925); border: 3px double #63351b; border-radius: 8px 8px 4px 4px; box-shadow: 0 -5px 12px #10090599, inset 0 0 18px #8b572a55; }
.quick-menu[hidden], .terminal[hidden] { display: none; }
.quick-menu button { min-height: 44px; color: #38220f; background: #e5c37b; border: 2px solid #815126; border-radius: 4px; box-shadow: inset 0 0 6px #fff3bf55, 0 2px 1px #5b321955; font-weight: bold; touch-action: manipulation; }
.quick-menu button:active { color: #fff0c2; background: #63351b; }
.menu-header { display: grid; grid-template-columns: 42px 1fr 42px; gap: 4px; align-items: center; color: #38220f; text-align: center; }
.menu-header button { min-height: 34px; padding: 0; font-size: 15px; }
.menu-header b { overflow: hidden; font-size: 11px; letter-spacing: .5px; white-space: nowrap; text-overflow: ellipsis; }
.menu-grid { display: grid; grid-template-columns: 1fr 1fr; grid-auto-rows: minmax(46px, auto); gap: 6px; align-content: start; padding-top: 6px; overflow-y: auto; overscroll-behavior: contain; }
.menu-guide { grid-column: 1 / -1; margin: 0; padding: 6px 8px; color: #5c3518; background: #fff1c166; border-left: 3px solid #815126; font-size: 9px; line-height: 1.35; }
.menu-filter { grid-column: 1 / -1; min-height: 38px; padding: 0 10px; color: #38220f; background: #fff1c1; border: 2px solid #815126; border-radius: 4px; outline: none; font: 800 10px system-ui, sans-serif; letter-spacing: .04em; }
.menu-filter:focus { border-color: #3f7080; box-shadow: 0 0 0 2px #79dbea66; }
.menu-filter::placeholder { color: #7e6345; opacity: 1; }
.menu-grid button { padding: 7px 6px; font-size: 11px; line-height: 1.15; }
.menu-grid button.selected { color: #fff0c2; background: #63351b; outline: 2px solid #f4d98e; outline-offset: -4px; }
.menu-grid button small { display: block; margin-top: 3px; opacity: .78; font-size: 8px; }
.menu-grid button .item-icon { float: left; width: 34px; height: 34px; margin: -2px 6px -2px -1px; background-repeat: no-repeat; image-rendering: pixelated; border: 1px solid #815126; }
.menu-grid button.has-object-icon { min-height: 54px; text-align: left; }
.menu-grid button.has-scene-icon { min-height: 64px; text-align: left; }
.menu-grid button .item-icon.scene-preview { width: 52px; height: 52px; margin-top: -3px; background-position: center; background-size: cover; }
.menu-grid button .item-icon.object-preview { position: relative; width: 44px; height: 44px; margin-top: -3px; overflow: hidden; background: #21190f55; }
.menu-grid button .item-icon.object-preview > i { position: absolute; left: 50%; bottom: 1px; display: block; background-repeat: no-repeat; image-rendering: pixelated; transform: translateX(-50%); }
.menu-grid .wide { grid-column: 1 / -1; }
.menu-grid .shop-action { color: #3b240d; background: #e0ad50; border-color: #8d5720; }
/* Catalogue entries you cannot yet afford stay legible and stay tappable --
 * the server refuses the purchase and says how many questpoints are short, so
 * dimming is a hint rather than a lock. */
.menu-grid .shop-action.unaffordable { color: #6b6257; background: #cbb9a0; border-color: #7d7264; }
.menu-grid .class-action { color: #fff0d2; background: #62405f; border-color: #b77eb1; }
.menu-grid button.locked { opacity: .48; filter: grayscale(1); }
.menu-grid button.recovering { opacity: .5; filter: grayscale(.8); }
.screen.ability-magic-user { animation: mage-cast .52s ease-out; }
.screen.ability-cleric { animation: cleric-cast .52s ease-out; }
.screen.ability-thief { animation: thief-skill .42s ease-out; }
.screen.ability-warrior { animation: warrior-skill .42s ease-out; }
.game-event-fx.ability { color: #fff0a6; font-size: 10px; }
@keyframes mage-cast { 35% { box-shadow: inset 0 0 42px 12px #714cffaa, 0 0 0 2px #7c7f6d; } }
@keyframes remote-cast { 35% { filter: brightness(1.65) drop-shadow(0 0 8px #8b63ff); transform: translate(-50%, -86%) scale(1.06); } }
@keyframes caster-spark { 0% { opacity: 0; scale: .35; } 45% { opacity: 1; scale: 1.35; rotate: 35deg; } 100% { opacity: 0; translate: 0 -18px; rotate: 90deg; } }
@keyframes sacrifice-rise { 0% { opacity: 1; } 55% { opacity: 1; translate: 0 -13px; filter: brightness(2) sepia(1); } 100% { opacity: 0; translate: 0 -28px; scale: .35; } }
@keyframes sacrifice-glow { 35% { box-shadow: inset 0 0 48px 10px #ffd96799; } }
@keyframes cleric-cast { 35% { box-shadow: inset 0 0 42px 12px #ffe66caa, 0 0 0 2px #7c7f6d; } }
@keyframes thief-skill { 35% { filter: brightness(.55) contrast(1.5); } }
@keyframes warrior-skill { 25%, 60% { transform: translateX(-3px); filter: sepia(1) saturate(2); } 45% { transform: translateX(3px); } }
.menu-grid button.rarity-class { color: #d9f5ff; border-color: #73d9ff; }
.menu-grid button.rarity-relic { color: #bfffc0; border-color: #73df79; }
.menu-grid button.rarity-legendary { color: #fff0a6; border-color: #ffcf56; box-shadow: inset 0 0 8px #ffcf5633; }
.menu-grid button.rarity-mythic { color: #ffd8ff; border-color: #f188ff; box-shadow: inset 0 0 10px #db43ff55; }
.menu-grid button.item-upgrade { border-color: #60e58b; box-shadow: inset 0 0 9px #35dd7040; }
.menu-grid button.item-upgrade small { color: #85f5aa; }
.room-entity.object.vendor-item { filter: drop-shadow(0 0 5px #ffd966); }
.room-entity.object.vendor-item span { color: #ffe58a; border-color: #b88a2b; background: #21180ee8; }
.menu-grid button.item-downgrade small { color: #ff8e8e; }
.menu-grid button.item-match small { color: #aab5c5; }
.menu-grid button.item-open-slot { border-color: #67c9ff; }
.menu-grid button.item-open-slot small { color: #8bd8ff; }
.menu-empty { grid-column: 1 / -1; padding: 18px 8px; color: #63351b; border: 1px dashed #815126; text-align: center; font-size: 11px; }
.menu-no-matches { padding: 12px 8px; }
.menu-prompt { position: absolute; z-index: 2; inset: 40px 7px 7px; display: grid; grid-template-rows: auto 42px auto; gap: 8px; align-content: center; padding: 12px; color: #38220f; background: linear-gradient(120deg, #f5dda0, #dfb96d); border: 2px solid #63351b; }
.menu-prompt[hidden] { display: none; }
.menu-prompt label { font-size: 11px; text-align: center; }
.menu-prompt input { width: 100%; padding: 8px; color: #2f1b0d; background: #fff1c1; border: 2px solid #815126; font-size: 12px; outline: none; }
.menu-prompt div { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.quest-scroll { position: absolute; z-index: 17; inset: 0; display: grid; place-items: center; padding: 14px; background: #100b07bb; backdrop-filter: blur(2px); }
.quest-scroll[hidden] { display: none; }
.quest-scroll article { position: relative; width: min(100%, 340px); height: min(88%, 470px); display: grid; grid-template-rows: auto minmax(0, 1fr) auto; gap: 9px; padding: 24px 20px 20px; color: #38220f; background: linear-gradient(90deg, #b27a3b, #f0d58c 8%, #f8e5ad 50%, #e8c678 92%, #9c632d); border: 3px solid #6c3c1d; border-radius: 12px 12px 8px 8px; box-shadow: 0 8px 18px #000b, inset 0 0 20px #8b572a66; text-shadow: none; }
.quest-scroll article::before, .quest-scroll article::after { content: ""; position: absolute; z-index: -1; right: -8px; left: -8px; height: 18px; background: linear-gradient(#8f5929, #dcae62 45%, #77421e); border: 2px solid #5a3018; border-radius: 9px; box-shadow: 0 3px 4px #0007; }
.quest-scroll article::before { top: -8px; }.quest-scroll article::after { bottom: -8px; }
.quest-scroll header { padding-bottom: 8px; border-bottom: 2px solid #815126; text-align: center; }
.quest-scroll header small { display: block; color: #79431d; font-size: 9px; font-weight: 900; letter-spacing: 2px; }
.quest-scroll h2 { margin: 3px 0 0; color: #38200e; font-family: Georgia, serif; font-size: 19px; line-height: 1.15; }
.quest-scroll-text { min-height: 0; padding: 4px 7px; overflow-y: auto; overscroll-behavior: contain; color: #2f2115; font-family: Georgia, serif; font-size: 15px; font-weight: 700; line-height: 1.48; white-space: pre-wrap; user-select: text; touch-action: pan-y; outline: none; scrollbar-color: #875126 #e7c77f; }
.quest-scroll footer { display: grid; grid-template-columns: 1fr 92px; gap: 8px; align-items: center; padding-top: 8px; border-top: 2px solid #815126; }
.quest-scroll footer span { font-size: 10px; font-weight: 900; }
.quest-scroll button { min-height: 40px; color: #f8e5ad; background: #63351b; border: 2px solid #38200e; font-size: 10px; font-weight: 900; }
.mob-inspect { position: absolute; inset: 0; z-index: 18; display: grid; padding: 10px; place-items: center; background: #06090dcc; }
.mob-inspect[hidden] { display: none; }
.mob-inspect article { width: min(100%, 350px); max-height: 94%; display: grid; grid-template: auto 126px auto minmax(90px, 1fr) auto / 1fr; gap: 7px; padding: 12px; overflow: hidden; color: #38220f; background: linear-gradient(90deg, #a56d34, #f4dda1 7%, #fae9b8 50%, #edce86 93%, #915925); border: 4px double #63351b; border-radius: 10px; box-shadow: 0 8px 22px #000c, inset 0 0 20px #8b572a55; }
.mob-inspect header { display: grid; grid-template-columns: 1fr auto; align-items: center; border-bottom: 2px solid #9b662e; }
.mob-inspect header small { color: #89521f; font-size: 8px; letter-spacing: 2px; }
.mob-inspect h2 { grid-column: 1; margin: 1px 0 5px; font: bold 19px Georgia, serif; }
.mob-inspect header button { grid-column: 2; grid-row: 1 / 3; width: 38px; height: 38px; color: #f8e5ad; background: #63351b; border: 2px solid #38200e; font-size: 18px; }
.mob-inspect-portrait { position: relative; width: 136px; height: 126px; margin: auto; overflow: hidden; background: radial-gradient(ellipse at 50% 78%, #92a86666 0 17%, transparent 18%), linear-gradient(#b9d29a55, #75905b55); border: 3px double #815126; image-rendering: pixelated; }
.mob-inspect-portrait > i { position: absolute !important; left: 50% !important; top: 7px !important; width: 112px !important; height: 112px !important; margin: 0 !important; background-repeat: no-repeat; transform: translateX(-50%) !important; animation: none !important; image-rendering: pixelated; }
.mob-inspect-portrait > i.npc-outfit { z-index: 2; background-size: 800% 800%; }
.mob-inspect-portrait > i.npc-hair { z-index: 3; background-size: 800% 800%; }
.mob-inspect-portrait > b { display: grid; height: 100%; place-items: center; font-size: 48px; }
.mob-inspect-portrait.questmaster::after { content: "!"; position: absolute; top: 3px; right: 9px; z-index: 5; color: #fff07a; font: 900 28px/1 system-ui, sans-serif; text-shadow: 0 2px #8b541e, 0 0 6px #ffe75a; }
.mob-inspect-portrait.questmaster.quest-complete::after { color: #a9afb3; text-shadow: 0 2px #343b40, 0 0 4px #d8dde0; }
.mob-inspect > article > b { text-align: center; color: #63351b; font-size: 10px; letter-spacing: 1px; }
.mob-inspect-description { min-height: 90px; padding: 9px; overflow-y: auto; color: #2f2115; background: #fff0c655; border: 1px solid #a66e31; font: 700 15px/1.45 Georgia, serif; white-space: pre-wrap; outline: none; }
.mob-inspect footer { color: #75451e; text-align: center; font-size: 8px; letter-spacing: 1px; }
.score-sheet { position: absolute; inset: 0; z-index: 18; display: grid; padding: 9px; place-items: center; background: #06090ddd; }
.score-sheet[hidden] { display: none; }
.score-sheet article { width: min(100%, 360px); max-height: 95%; display: grid; grid-template-rows: auto minmax(0, 1fr) auto; gap: 8px; padding: 12px; overflow: hidden; color: #38220f; background: linear-gradient(90deg, #95602e, #f0d18b 6%, #f9e7b5 50%, #e7c376 94%, #885326); border: 4px double #63351b; border-radius: 9px; box-shadow: 0 8px 22px #000c, inset 0 0 20px #8b572a55; }
.score-sheet header { display: grid; grid-template-columns: 1fr 40px; align-items: center; padding-bottom: 7px; border-bottom: 2px solid #9b662e; }
.score-sheet header small { color: #89521f; font-size: 8px; letter-spacing: 2px; }
.score-sheet h2 { margin: 2px 0 1px; font: bold 20px Georgia, serif; }
.score-sheet header b { color: #70401e; font-size: 9px; letter-spacing: 1px; }
.score-sheet header button { grid-column: 2; grid-row: 1; width: 38px; height: 38px; color: #f8e5ad; background: #63351b; border: 2px solid #38200e; font-size: 18px; }
.score-sheet-body { min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
.score-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 5px; }
.score-grid div { min-width: 0; padding: 5px 3px 7px; color: #fff1c8; background: linear-gradient(#463120ee, #291c14ee); border: 1px solid #9d713c; border-radius: 4px; text-align: center; box-shadow: inset 0 0 9px #0008, 0 1px #fff2b833; }
.score-grid small, .score-grid b { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.score-grid small { color: #e5bd69; font-size: 7px; letter-spacing: .5px; }
.score-grid b { margin-top: 2px; font-size: 10px; }
.score-stat-icon { display: grid; width: 22px; height: 22px; margin: 0 auto 2px; place-items: center; color: #f6d37c; border: 1px solid currentColor; border-radius: 50%; font: normal 15px/1 system-ui, sans-serif; text-shadow: 0 1px #000; box-shadow: inset 0 0 7px currentColor, 0 0 4px #0008; }
.score-stat.vital .score-stat-icon { color: #ff7185; }.score-stat.magic .score-stat-icon { color: #9edcff; }
.score-stat.defense .score-stat-icon { color: #cbd5e1; }.score-stat.spirit .score-stat-icon { color: #d9b9ff; }
.score-stat.might .score-stat-icon { color: #ffad72; }.score-stat.agility .score-stat-icon { color: #8dffb2; }
.score-stat.charm .score-stat-icon { color: #ff9fd0; }.score-stat.progress .score-stat-icon { color: #fff279; }
.score-stat.gold .score-stat-icon { color: #ffc84f; }.score-stat.quest .score-stat-icon { color: #79dbea; }
.score-stat.time .score-stat-icon { color: #d8ad8a; }
.score-sheet footer { padding-top: 6px; color: #70401e; border-top: 1px solid #a56d34; text-align: center; font-size: 8px; font-weight: 900; letter-spacing: .7px; }
.world-atlas { position: absolute; inset: 0; z-index: 18; display: grid; padding: 8px; place-items: center; background: #06090de8; }
.world-atlas[hidden] { display: none; }
.world-atlas article { width: min(100%, 370px); max-height: 97%; display: grid; grid-template-rows: auto minmax(0, 1fr) auto auto; gap: 7px; padding: 11px; overflow: hidden; color: #38220f; background: linear-gradient(115deg, #8b5528, #e9c778 7%, #f7e3a8 50%, #dfb966 93%, #79451f); border: 4px double #63351b; border-radius: 9px; box-shadow: 0 9px 24px #000d, inset 0 0 24px #77451f66; }
.world-atlas header { display: grid; grid-template-columns: 1fr 40px; align-items: center; padding-bottom: 6px; border-bottom: 2px solid #8b572a; }
.world-atlas header small, .world-atlas header b { display: block; color: #79471f; font-size: 7px; letter-spacing: 1.2px; }
.world-atlas h2 { margin: 1px 0 2px; color: #38200e; font: bold 18px Georgia, serif; letter-spacing: .5px; }
.world-atlas header button { grid-column: 2; grid-row: 1; width: 38px; height: 38px; color: #f8e5ad; background: #63351b; border: 2px solid #38200e; font-size: 18px; }
.atlas-route { position: relative; min-height: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-rows: repeat(6, minmax(43px, auto)); gap: 5px; padding: 5px; overflow-y: auto; overscroll-behavior: contain; background: radial-gradient(circle at 50% 0, #ffeeba99, transparent 35%), repeating-linear-gradient(35deg, #6a43251b 0 1px, transparent 1px 12px), #d8b66f; border: 2px solid #7b4c24; box-shadow: inset 0 0 12px #6d3d2266; }
.atlas-node { position: relative; min-width: 0; min-height: 43px; display: grid; grid-template: auto auto / 22px minmax(0, 1fr); align-content: center; align-items: center; gap: 0 3px; padding: 3px; color: #f9e9b7; background: linear-gradient(145deg, hsl(var(--atlas-hue) 34% 30%), hsl(var(--atlas-hue) 38% 18%)); border: 2px solid #805326; border-radius: 5px; box-shadow: 0 2px 3px #3a210c77, inset 0 0 7px #0006; text-align: left; touch-action: manipulation; }
.atlas-node.capital { grid-column: 1 / -1; grid-row: 1; width: 58%; justify-self: center; background: linear-gradient(145deg, #69431f, #302117); border-color: #d0a344; }
.atlas-node i { grid-row: 1 / 3; display: grid; width: 21px; height: 21px; place-items: center; color: hsl(var(--atlas-hue) 80% 76%); border: 1px solid currentColor; border-radius: 50%; font: normal 11px/1 system-ui, sans-serif; box-shadow: 0 0 6px currentColor; }
.atlas-node b, .atlas-node small { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.atlas-node b { align-self: end; font-size: 7px; line-height: 1.05; white-space: nowrap; }
.atlas-node small { align-self: start; color: #dfbd75; font-size: 6px; white-space: nowrap; }
.atlas-node.unknown { color: #9f927c; background: linear-gradient(145deg, #4d493f, #272723); border-color: #716957; filter: saturate(.35); }
.atlas-node.unknown i { color: #a39c8c; box-shadow: none; }
.atlas-node.current::after { content: "YOU"; position: absolute; top: -5px; right: -3px; padding: 1px 3px; color: #2c1c0d; background: #fff195; border: 1px solid #6c441f; border-radius: 3px; font-size: 6px; font-weight: 900; box-shadow: 0 0 7px #fff09b; }
.atlas-node.selected { outline: 3px solid #fff1a0; outline-offset: 1px; filter: brightness(1.22); }
.atlas-node.progress-danger { border-color: #b83b44; }
.atlas-node.progress-danger i, .atlas-node.progress-danger small { color: #ffaaa0; }
.atlas-node.progress-on-level { border-color: #e1b956; }
.atlas-node.progress-on-level i { color: #fff1a0; }
.atlas-node.progress-mastered { border-color: #648e98; filter: saturate(.72); }
.atlas-detail { min-height: 44px; display: grid; align-content: center; gap: 2px; padding: 6px 9px; color: #f9e9b7; background: linear-gradient(#49301f, #2c1d14); border: 2px solid #9a692e; box-shadow: inset 0 0 9px #0008; }
.atlas-detail b { font: bold 12px Georgia, serif; }.atlas-detail span { color: #dfbd75; font-size: 7px; line-height: 1.3; }
.world-atlas footer { color: #6d411d; text-align: center; font-size: 6px; font-weight: 900; letter-spacing: .4px; }
.terminal { position: absolute; z-index: 16; inset: 49px 0 0; display: grid; grid-template-rows: 1fr 35px; padding: 8px; background: #101927f7; color: #ffe8ae; }
#terminal-log { overflow-y: auto; white-space: pre-wrap; user-select: text; font-size: 12px; line-height: 1.4; }
#command-form { display: grid; grid-template-columns: 1fr 36px; border-top: 1px solid #d4a85b; padding-top: 5px; }
#command-form input { min-width: 0; color: #38220f; background: #f4dda0; border: 0; padding: 5px; outline: 0; }
#command-form button { border: 0; color: #fff0c2; background: #63351b; }
.death-screen { position: absolute; z-index: 19; inset: 0; display: grid; place-content: center; justify-items: center; gap: 12px; padding: 28px; color: #fff0dc; background: radial-gradient(circle, #621326dd, #16070bf5 72%); text-align: center; animation: death-arrive .55s ease-out; }
.death-screen[hidden] { display: none; }
.auth-screen { position: absolute; z-index: 18; inset: 0; display: grid; grid-template-columns: 1fr; align-content: center; justify-items: stretch; gap: 8px; padding: 18px; color: #ffe8ae; background: radial-gradient(circle at 50% 20%, #4d321d, #101927 76%); text-align: center; }
.auth-screen[hidden] { display: none; }
.auth-mark { color: #f4cf72; font-size: 34px; line-height: 1; text-shadow: 0 0 10px #e9b84e; }
.auth-screen > small { font-size: 8px; letter-spacing: 1px; }
.auth-screen h2 { margin: 0; font-size: 22px; letter-spacing: 2px; }
.auth-screen p { min-height: 32px; margin: 0; font-size: 11px; line-height: 1.4; }
.auth-screen form { display: grid; gap: 6px; }
.auth-screen form[hidden] { display: none; }
.auth-screen label { font-size: 9px; font-weight: 900; letter-spacing: 1px; }
.auth-screen input { min-width: 0; width: 100%; height: 44px; padding: 8px; color: #38220f; background: #f6e2a9; border: 3px solid #b47a32; border-radius: 2px; outline: none; text-align: center; font-size: 16px; }
.auth-screen input[type="hidden"] { display: none; }
.auth-remember { display: flex; align-items: center; justify-content: center; gap: 7px; min-height: 28px; color: #bcefff; font-size: 8px !important; letter-spacing: 0 !important; }
.auth-remember input { appearance: none; width: 18px; height: 18px; min-width: 18px; padding: 0; background: #101927; border: 2px solid #d8ad5a; }
.auth-remember input:checked::after { content: "✓"; display: block; color: #fff4d2; font-size: 14px; line-height: 14px; }
.auth-screen button:disabled, .auth-screen input:disabled { opacity: .65; pointer-events: none; }
.auth-screen button { min-height: 44px; padding: 7px; color: #fff0c2; background: #63351b; border: 2px solid #d8ad5a; font-size: 11px; font-weight: 900; touch-action: manipulation; }
.auth-screen button:active { color: #38220f; background: #f4dda0; }
.auth-choices { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; }
/*
 * Two choices stack instead of splitting the row.
 *
 * MALE / FEMALE as two half-width boxes read as an accident -- narrow, short,
 * and adrift in an empty panel. A stage with exactly two answers gets two
 * full-width rows, which is the same shape as every other stage.
 */
.auth-choices:has(> button:nth-child(2):last-child) { grid-template-columns: 1fr; }
.auth-choices > button { min-height: 44px; }
.auth-choices:empty { display: none; }
.auth-choices button.wide { grid-column: 1 / -1; }
.auth-choices button strong, .auth-choices button small { display: block; }
.auth-choices button strong { font-size: 12px; }
.auth-choices button small { margin-top: 3px; opacity: .78; font-size: 8px; font-weight: normal; }
.auth-fallback { min-height: 30px !important; margin-top: 3px; opacity: .7; border-style: dashed !important; font-size: 8px !important; }
.auth-fallback[hidden] { display: none; }
.death-screen .death-mark { font-size: 54px; filter: drop-shadow(0 4px #000); animation: death-pulse 1.2s ease-in-out infinite; }
.death-screen h2 { margin: 0; color: #ff3855; font-size: 28px; letter-spacing: 3px; text-shadow: 3px 3px #26040b; }
.death-screen p { max-width: 310px; margin: 0; font-size: 12px; line-height: 1.45; }
.death-screen button { min-height: 48px; padding: 9px 16px; color: #fff0dc; background: #8e1730; border: 3px double #ff7185; font-size: 12px; font-weight: 900; }
@keyframes death-arrive { from { opacity: 0; transform: scale(1.12); } }
@keyframes death-pulse { 50% { transform: translateY(-4px) scale(1.06); } }
.screen-label { height: 15px; display: flex; align-items: center; gap: 8px; color: #9da1a7; font-size: 7px; letter-spacing: 2px; }
.screen-label i { width: 7px; height: 7px; margin-left: auto; border-radius: 50%; background: #4b272b; box-shadow: 0 0 0 1px #17191d; }
.screen-label i.on { background: #ef3054; box-shadow: 0 0 7px #ff284f; }

.controls { min-height: 0; display: flex; align-items: center; justify-content: space-between; padding: 0 clamp(3px, 2vw, 12px); }
.dpad { position: relative; width: calc(var(--control-unit) * 3); height: calc(var(--control-unit) * 3); flex: none; transform: rotate(-2deg); }
.dpad button { position: absolute; width: var(--control-unit); height: var(--control-unit); border: 0; color: #555; background: linear-gradient(145deg, #343638, #151718); box-shadow: inset 2px 2px #5e6061, inset -3px -3px #090a0a, 0 3px 2px #888; }
.dpad button:active { transform: translateY(2px); filter: brightness(.75); }
.dpad button { touch-action: none; user-select: none; -webkit-user-select: none; }
.dpad button.held-direction { transform: translateY(2px); filter: brightness(.72); box-shadow: inset 3px 3px #090a0a, inset -1px -1px #5e6061; }
.player-sprite.continuous-walk:not(.attacking) .sprite-layer { animation: walk-frame .48s steps(1) infinite; }
.dpad .up { left: var(--control-unit); top: 0; border-radius: 6px 6px 0 0; }.dpad .left { left: 0; top: var(--control-unit); border-radius: 6px 0 0 6px; }.dpad .center { left: var(--control-unit); top: var(--control-unit); border-radius: 0; }.dpad .right { left: calc(var(--control-unit) * 2); top: var(--control-unit); border-radius: 0 6px 6px 0; }.dpad .down { left: var(--control-unit); top: calc(var(--control-unit) * 2); border-radius: 0 0 6px 6px; }
.action-buttons { position: relative; width: calc(var(--control-unit) * 3); height: calc(var(--control-unit) * 3); flex: none; transform: rotate(-8deg); }
.action-buttons label { position: absolute; display: grid; gap: 3px; justify-items: center; font-size: 7px; font-weight: bold; color: #4d4860; }
.action-buttons label.x { left: var(--control-unit); top: 0; }.action-buttons label.y { left: 0; top: var(--control-unit); }
.action-buttons label.a { right: 0; top: var(--control-unit); }.action-buttons label.b { left: var(--control-unit); top: calc(var(--control-unit) * 2); }
.action-buttons button { width: var(--control-unit); height: var(--control-unit); border: 0; border-radius: 50%; color: #eac9d6; background: radial-gradient(circle at 35% 30%, #b74271, var(--button) 65%); box-shadow: inset 2px 2px #d06991, inset -3px -4px #56142f, 0 4px 1px #8d8b7f; font-size: 17px; font-weight: bold; }
.action-buttons button:active { transform: translateY(3px); box-shadow: inset 2px 2px #56142f; }
.system-buttons { min-height: 0; display: flex; justify-content: center; gap: 28px; transform: rotate(-2deg); }
.system-buttons label { display: grid; justify-items: center; gap: 4px; font-size: 7px; font-weight: bold; color: #55566a; }
.system-buttons button { width: 52px; height: 13px; border: 0; border-radius: 10px; background: linear-gradient(#777a75, #3f4140); box-shadow: inset 1px 1px #999, 0 2px #888; }

@media (max-height: 720px) {
  .handheld { --control-unit: clamp(40px, 11.5vw, 46px); grid-template-rows: 20px minmax(0, 1fr) 142px 18px; gap: 2px; }
  .brand strong { font-size: 17px; }
  .controls { align-items: start; padding-top: 2px; }
  .system-buttons label { gap: 1px; }
}
@media (max-width: 500px) {
  .handheld { border-radius: 0; box-shadow: inset 4px 0 #ffffff44, inset -4px 0 #77786b44; }
}
@media (min-width: 600px) {
  body::after { content: "MOBILE PORTRAIT CLIENT"; position: fixed; bottom: 12px; color: #ffffff55; font-size: 10px; letter-spacing: 3px; }
}
@media (prefers-reduced-motion: reduce) {
  .ambient-particle { display: none; }
  .ambient-bird, .ambient-bird::before { animation: none !important; }
}

/* World entities use depth-based z-indexes. Keep every modal surface above them. */
.quick-menu, .terminal { z-index: 30; }
.quest-scroll, .mob-inspect, .score-sheet, .world-atlas { z-index: 32; }
.auth-screen { z-index: 34; }
.death-screen { z-index: 35; }

/* Draughts board. The grid is eight squares wide and sized off the menu width,
 * so it scales with the panel rather than assuming a device. */
.menu-grid .checkers-holder { grid-column: 1 / -1; display: grid; justify-items: center;
  gap: 8px; padding: 8px 0 10px; background: none; border: 0;
  /* Its own containing block, so the board can size against the panel rather
     than the viewport, and nothing from the next row can overlap it. */
  container-type: inline-size; position: relative; z-index: 1; }
.checkers-board { display: grid; grid-template-columns: repeat(8, 1fr); gap: 0;
  /* The height term is what stops the seat buttons falling below the fold
     on a tall narrow phone: sized off width alone this was 268px tall in a
     panel with less than that to give. dvh, not vh -- mobile chrome moves. */
  /* Square by construction rather than by aspect-ratio alone: the holder is
     a grid row, and a row that does not reserve the height lets the seat
     buttons ride up over the board. Both terms use the same expression. */
  width: min(264px, 86cqw, 42dvh); height: min(264px, 86cqw, 42dvh);
  border: 2px solid #2b3a1c; box-shadow: 0 2px 0 #0009; }
.checkers-cell { display: grid; place-items: center; padding: 0; min-width: 0;
  border: 0; border-radius: 0; background: #e8d9b0; }
.checkers-cell.dark { background: #6b4b2a; }
.checkers-cell.light { pointer-events: none; }
.checkers-cell.picked { box-shadow: inset 0 0 0 2px #ffe14a; }
/*
 * A hint, not a rule: the server still decides what is legal. When a capture
 * is on the board it marks only the pieces that can take, because that is the
 * move you are obliged to make; otherwise it marks everything that can move.
 */
.checkers-cell.can-move:not(.picked) .checkers-piece { animation: checkers-nudge 1.6s ease-in-out infinite; }
.checkers-cell.must-take:not(.picked) { box-shadow: inset 0 0 0 2px #ff9a3c; }
.checkers-cell.must-take:not(.picked) .checkers-piece { animation: checkers-urge .9s ease-in-out infinite; }
@keyframes checkers-nudge { 0%, 100% { transform: none; } 50% { transform: translateY(-2px); } }
@keyframes checkers-urge  { 0%, 100% { transform: none; } 50% { transform: scale(1.12); } }
@media (prefers-reduced-motion: reduce) {
  .checkers-cell.can-move .checkers-piece, .checkers-cell.must-take .checkers-piece { animation: none; }
  .checkers-cell.can-move:not(.picked) { box-shadow: inset 0 0 0 2px #9fd15a88; }
}
.checkers-cell:focus-visible { outline: 2px solid #ffe14a; outline-offset: -2px; }
.checkers-piece { display: block; width: 72%; aspect-ratio: 1; border-radius: 50%;
  box-shadow: inset 0 -2px 0 #0006, 0 1px 1px #0008; }
.checkers-piece.light { background: #f2e6c8; border: 1px solid #b09a62; }
.checkers-piece.dark { background: #3a2a18; border: 1px solid #14100a; }
/* A king is marked with a ring rather than a crown sprite: legible at 24px. */
.checkers-piece.king { box-shadow: inset 0 0 0 3px #e0ad50, inset 0 -2px 0 #0006, 0 1px 1px #0008; }
.checkers-status { margin: 0; font: 500 9px/1.4 "Courier New", monospace; letter-spacing: .06em;
  color: #d7e894; text-align: center; }

/* An object standing on furniture keeps full opacity: it is raised, not on
   another floor. gridPosition lifts it by its z, which reads as sitting on
   the surface. */
.room-entity.on-furniture { opacity: 1; }

/* Threat banding on the mob nameplate. The level is always visible; the colour
   says whether it is worth attacking without doing the subtraction. */
.enemy[data-threat="trivial"] > small { color: #8a9280; }
.enemy[data-threat="even"] > small { color: #d7e894; }
.enemy[data-threat="dangerous"] > small { color: #ffc25e; }
.enemy[data-threat="deadly"] > small { color: #ff7a7a; font-weight: 900; }
.enemy[data-threat="friendly"] > small { color: #79c6ea; }

/* Ambient critters. Same sheet geometry as .ambient-bird: 4x2, facing left. */
.ambient-critter { position: absolute; image-rendering: pixelated; z-index: 1; pointer-events: none; opacity: var(--critter-opacity, 1); transform: translate(-50%, -100%); filter: drop-shadow(0 2px 1px #0a1a0f55); transition: translate var(--roam-ms, 1400ms) linear; }
.ambient-critter::before { content: ""; position: absolute; inset: 0; background-image: var(--critter-sheet); background-repeat: no-repeat; background-size: 400% 200%; background-position-y: 0%; transform: scaleX(var(--critter-facing, 1)); animation: critter-frames var(--critter-idle, 1.2s) steps(1) infinite; }
.ambient-critter.moving::before { background-position-y: 100%; animation-duration: var(--critter-rate, .4s); }
/* A butterfly is never still, and never simply slid along the ground: the bob
   rides on the wander, on the pseudo-element so it does not fight it. */
.ambient-critter.flutter::before { animation: critter-frames var(--critter-rate, .16s) steps(1) infinite, butterfly-bob 1.5s ease-in-out infinite; }
@keyframes butterfly-bob { 0%, 100% { translate: 0 0; } 50% { translate: 0 -6px; } }
@keyframes critter-frames { 0%, 24% { background-position-x: 0%; } 25%, 49% { background-position-x: 33.333%; } 50%, 74% { background-position-x: 66.667%; } 75%, 100% { background-position-x: 100%; } }
@media (prefers-reduced-motion: reduce) { .ambient-critter { transition: none; } .ambient-critter::before { animation: none; } }

/* The minipet. Same 4x2 sheet geometry as the critters, and a transition three
   times slower than the player's so it arrives a beat behind them. */
.minipet { position: absolute; z-index: 5; image-rendering: pixelated; pointer-events: none; transform: translate(calc(-50% - 13px), -100%); transition: left .46s ease-out, top .46s ease-out; filter: drop-shadow(0 2px 1px #102a1766); }
.minipet::before { content: ""; position: absolute; inset: 0; background-image: var(--pet-sheet); background-repeat: no-repeat; background-size: 400% 200%; background-position-y: 0%; transform: scaleX(var(--pet-facing, 1)); animation: critter-frames 1.1s steps(1) infinite; }
.minipet.moving::before { background-position-y: 100%; animation-duration: .34s; }
@media (prefers-reduced-motion: reduce) { .minipet { transition: none; } .minipet::before { animation: none; } }

/* Household animals: a mob drawn from a 4x2 critter sheet rather than the
   player sheet or the monster atlas. */
/* Mobs are .enemy, not .room-entity -- that is the object class. And .enemy > i
   sets `background` as a shorthand, so this has to use the shorthand too or
   monsters.png reasserts itself underneath. */
.enemy.pet-npc > i { background: var(--pet-sheet) no-repeat 0 0 / 400% 200%; image-rendering: pixelated; animation: critter-frames 1.2s steps(1) infinite; }

/* A photographed pet fills its portrait frame; sprite-drawn mobs do not. */
.mob-inspect-portrait.photo { overflow: hidden; border-radius: 9px; border: 2px solid #6e5a33; box-shadow: 0 3px 0 #0006; }
.mob-inspect-portrait.photo img { display: block; width: 100%; height: 100%; object-fit: cover; image-rendering: auto; }
/* Wandering. The destination is chosen in script and carried by a
   transition, so each animal takes its own path at its own moment.
   translate rather than transform, because .enemy already carries its
   tile anchoring in transform. left/top stay in the list or the
   entity's own positioning loses its easing. */
.enemy.pet-npc { transition: left .12s linear, top .12s linear, translate var(--stroll-ms, 1400ms) linear; }
/* Its own flip variable: renderRoomEntities rewrites --npc-flip for every
   mob several times a second and would undo the turn. */
.enemy.pet-npc > i { transform: scaleX(var(--pet-flip, 1)); }
.enemy.pet-npc.strolling > i { background-position-y: 100%; animation: critter-frames .34s steps(1) infinite; }
@media (prefers-reduced-motion: reduce) { .enemy.pet-npc { transition: none; } }
/* Where selection is deliberate: the log and the long descriptions are
   there to be read and quoted, and an input is unusable on a phone if you
   cannot put the caret in it. */
.terminal, .terminal *, .mob-inspect-description, .quest-scroll,
input, textarea {
  -webkit-user-select: text;
  user-select: text;
  -webkit-touch-callout: default;
}
/* Audio switches, sat between the name and the bars. min-height is reset
   because the shared button rule sizes for thumbs and these are chips. */
.audio-toggles { display: flex; flex-direction: column; gap: 3px; justify-content: center; margin: 0 6px; }
.audio-toggles button { min-height: 0; padding: 2px 6px; color: #79dbea; background: #0d1620; border: 1px solid #35505e; border-radius: 3px; font: 900 8px/1.3 "Courier New", monospace; letter-spacing: .09em; text-shadow: none; }
.audio-toggles button[aria-pressed="false"] { color: #4a5a66; border-color: #22303a; opacity: .7; }
.audio-toggles button[aria-pressed="false"]::after { content: " " attr(data-off); }
/* Experience, as a strip along the bottom of the HUD. A third row in the
   meters column would not fit: the HUD is a fixed 49px and .terminal is
   positioned at inset 49px to sit beneath it. */
/* A gold hairline along the bottom of the header meant nothing to anybody who
 * had not been told what it was. It says so now: the label sits just above
 * the bar, in the gap under the portrait, and costs the header no height. */
.xp-strip { position: absolute; left: 0; right: 0; bottom: 0; height: 3px; background: #05080d; border-top: 1px solid #0a1119; overflow: visible; }
.xp-strip::before { content: 'XP'; position: absolute; right: 4px; bottom: 3px; color: #ffd25e; font: 900 6px/1 system-ui, sans-serif; letter-spacing: .12em; text-shadow: 0 1px 0 #000, 0 0 4px #00000099; pointer-events: none; }
.xp-strip span { display: block; width: 0; height: 100%; background: linear-gradient(90deg, #b8862c, #ffe9a8); box-shadow: 0 0 4px #ffd25e88; transition: width .35s ease-out; }
/* Dye palette. Twelve swatches in two rows beats twelve rows of words,
   and shows the shading rather than naming it. */
.menu-grid .tint-holder { grid-column: 1 / -1; display: grid; justify-items: center; gap: 4px; padding: 5px 0 2px; background: none; border: 0; }
.tint-title { font: 700 9px/1 "Courier New", monospace; letter-spacing: .12em; color: #5b3a18; }
.tint-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 5px; width: min(238px, 92%); }
.tint-swatch { min-height: 0; aspect-ratio: 1; padding: 0; border: 2px solid #63351b; border-radius: 4px; box-shadow: inset 0 0 0 1px #ffffff40, 0 1px 1px #0006; }
.tint-swatch:active { outline: 2px solid #ffe14a; outline-offset: -1px; filter: brightness(1.2); }
.world:has(.game-shortcuts[hidden]) .tutorial-objective { left: 8px; right: 8px; }
/* A quest arriving. Three seconds of obvious, then it settles down and
   behaves like a normal panel -- something that pulses for ever is worse
   than something nobody notices. */
.tutorial-objective.arriving { animation: quest-arrive .55s cubic-bezier(.2,1.4,.4,1) both, quest-attention 1s ease-in-out 3 .55s; }
.tutorial-objective.arriving::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: linear-gradient(105deg, transparent 30%, #ffe9a866 48%, transparent 66%); animation: quest-sweep 1.1s ease-out .2s 2 both; }
@keyframes quest-arrive { from { opacity: 0; transform: translateY(-14px) scale(.94); } }
@keyframes quest-attention {
  50% { border-color: #ffe9a8; box-shadow: 0 3px 10px #020617aa, 0 0 0 3px #f5c45166, inset 0 0 0 1px #ffffff26; }
}
@keyframes quest-sweep { from { transform: translateX(-110%); } to { transform: translateX(110%); } }
@media (prefers-reduced-motion: reduce) {
  .tutorial-objective.arriving, .tutorial-objective.arriving::after { animation: none; }
  .tutorial-objective.arriving { border-color: #ffe9a8; }
}

/* --- spell slots as a bottom bar ------------------------------------------
 *
 * They used to run down both edges, four a side, which spent the left and
 * right of a portrait screen on chrome -- exactly the width the world wanted.
 * Eight across the bottom is one row of thumb travel, keeps them all reachable
 * without moving your hand off the d-pad side, and gives the play area back
 * about 90px of usable width.
 *
 * Positioned by index rather than with flexbox because the buttons are
 * absolutely positioned siblings of the rest of the HUD, and changing that
 * would mean restructuring the markup for a layout that is fixed at eight.
 * 34px wide on a 38px pitch, 300px total, centred.
 */
.quick-slot,
.quick-slot.right {
  top: auto;
  right: auto;
  bottom: 6px;
}

.quick-slot[data-slot="0"] { left: calc(50% - 150px); }
.quick-slot[data-slot="1"] { left: calc(50% - 112px); }
.quick-slot[data-slot="2"] { left: calc(50% -  74px); }
.quick-slot[data-slot="3"] { left: calc(50% -  36px); }
.quick-slot[data-slot="4"] { left: calc(50% +   2px); }
.quick-slot[data-slot="5"] { left: calc(50% +  40px); }
.quick-slot[data-slot="6"] { left: calc(50% +  78px); }
.quick-slot[data-slot="7"] { left: calc(50% + 116px); }

/* On a narrow phone 300px is most of the width, so tighten the pitch rather
 * than letting the row run off the edges. */
@media (max-width: 340px) {
  .quick-slot { width: 30px !important; }
  .quick-slot[data-slot="0"] { left: calc(50% - 134px); }
  .quick-slot[data-slot="1"] { left: calc(50% - 100px); }
  .quick-slot[data-slot="2"] { left: calc(50% -  66px); }
  .quick-slot[data-slot="3"] { left: calc(50% -  32px); }
  .quick-slot[data-slot="4"] { left: calc(50% +   2px); }
  .quick-slot[data-slot="5"] { left: calc(50% +  36px); }
  .quick-slot[data-slot="6"] { left: calc(50% +  70px); }
  .quick-slot[data-slot="7"] { left: calc(50% + 104px); }
  .quick-slot .ability-icon { width: 22px; height: 22px; font-size: 17px; }
}

/* The toast sat at the very bottom, which is now the bar. */
.message { bottom: 44px; }

/* --- the death sequence ---------------------------------------------------
 *
 * A hole rather than a veil: a small element with an enormous spread shadow
 * paints everything outside itself dark, so shrinking the element closes the
 * screen down to a point. Centred on the hero, which is why its position is set
 * from script rather than here.
 */
/* The waystone portal. One 64px cell of fx-portal-v1, scaled per style and
 * anchored on the traveller's feet like every other tile-placed sprite. */
.portal-fx {
  position: absolute;
  width: 64px;
  height: 64px;
  background-image: url("assets/fx-portal-v1.png");
  background-repeat: no-repeat;
  image-rendering: pixelated;
  transform: translate(-50%, -62%) scale(var(--portal-scale, 1.5));
  transform-origin: 50% 62%;
  pointer-events: none;
  filter: drop-shadow(0 0 6px #79dbea66);
}

.death-iris {
  position: absolute;
  z-index: 18;
  width: 0;
  height: 0;
  border-radius: 50%;
  box-shadow: 0 0 0 9999px #07040b;
  transform: translate(-50%, -50%);
  pointer-events: none;
  animation: death-iris-close .9s cubic-bezier(.55, 0, .8, .35) forwards;
}

@keyframes death-iris-close {
  from { width: 760px; height: 760px; }
  to   { width: 0; height: 0; }
}

/* The panel arrives after the iris has already closed, so it fades up out of
 * black rather than sliding over a lit room. */
.death-screen { animation: death-arrive .5s ease-out both; }

.death-screen .death-mark {
  animation: death-pulse 1.2s ease-in-out infinite, death-mark-drop .45s ease-out both;
}

@keyframes death-mark-drop {
  from { opacity: 0; transform: translateY(-14px) scale(.7); }
  to   { opacity: 1; transform: none; }
}

/* Letter-spaced and struck through with its own shadow: this is the one piece
 * of type in the game allowed to shout. */
.death-screen h2 {
  animation: death-title .6s .18s ease-out both;
}

@keyframes death-title {
  from { opacity: 0; letter-spacing: 14px; filter: blur(3px); }
  to   { opacity: 1; letter-spacing: 3px; filter: none; }
}

.death-screen p,
.death-screen button { animation: death-arrive .5s .36s ease-out both; }

@media (prefers-reduced-motion: reduce) {
  .death-iris { animation-duration: .01s; }
  .death-screen,
  .death-screen h2,
  .death-screen p,
  .death-screen button,
  .death-screen .death-mark { animation: none; }
}

/* The original per-slot rules are `.quick-slot[data-slot="0"]`, specificity
 * (0,2,0), so the bare `.quick-slot { top: auto }` above never applied and the
 * row stayed pinned to the top of the screen. Matched at the same specificity
 * and later in the file, so it actually wins. */
.quick-slot[data-slot] { top: auto; bottom: 6px; }

/* --- the bottom bar -------------------------------------------------------
 *
 * The ten slots own the bar. HOME, SOCIAL, PACK and HERO sit in a quiet utility
 * strip just above its right edge so combat controls retain the widest targets.
 *
 * Flex rather than more pixel arithmetic: the five fixed buttons keep their
 * size and the rail takes whatever is left, so this fits a 320px phone and a
 * 560px one without a breakpoint for each. These rules come after the absolute
 * positioning above and are written at matching or higher specificity, because
 * the per-slot `[data-slot="N"]` rules are (0,2,0) and a bare class would lose
 * to them -- which is exactly how the first version of this silently did
 * nothing.
 */
.bottom-bar {
  position: absolute;
  z-index: 11;
  right: 5px;
  bottom: 5px;
  left: 5px;
  display: flex;
  gap: 3px;
  padding: 3px;
  align-items: flex-end;
  justify-content: space-between;
  background: #08111ccc;
  border: 1px solid #79dbea55;
  border-radius: 7px;
  box-shadow: 0 2px 7px #0008;
  pointer-events: none;
}

.bottom-bar > * { pointer-events: auto; }

.slot-rail {
  display: flex;
  flex: 1 1 auto;
  gap: 3px;
  justify-content: center;
  min-width: 0;
}

/* Override the old absolute placement the spell slots carried. */
.game-shortcuts .bottom-bar .quick-slot,
.game-shortcuts .bottom-bar .quick-slot[data-slot] {
  position: static;
  inset: auto;
  flex: 0 0 auto;
}

/* The tray is navigation, not a second light show. Keep combat/context
 * controls vivid and quiet the always-present utility buttons. */
.bottom-bar > button,
.bottom-bar .quick-slot {
  border-width: 1px;
  box-shadow: inset 0 0 4px #79dbea18 !important;
  text-shadow: 1px 1px #000;
}
.utility-actions { position: absolute; right: 0; bottom: calc(100% + 4px); display: flex; gap: 2px; padding: 2px; background: #08111cb8; border: 1px solid #79dbea44; border-radius: 5px; pointer-events: auto; }
.game-shortcuts .utility-actions > button { position: relative; inset: auto; display: grid; width: 25px !important; height: 25px !important; padding: 1px !important; place-items: center; align-content: center; border-width: 1px; box-shadow: inset 0 0 3px #79dbea18 !important; filter: saturate(.72); }
.utility-actions > button small { display: none; }
.utility-actions .recall-button b { font-size: 12px; line-height: 1; }
.utility-actions .social-button b { font-size: 13px; line-height: 1; }
.utility-actions .backpack-button i { width: 12px; height: 12px; border-width: 1px; }
.utility-actions .backpack-button i::before { top: -5px; left: 2px; width: 6px; height: 5px; border-width: 1px; }
.utility-actions .score-button i { width: 13px; height: 16px; border-width: 1px; }
.utility-actions .score-button i::before { top: 3px; left: 3px; width: 6px; height: 2px; border-width: 1px; box-shadow: 0 4px #70401e; }

/* The rail's slots share the leftover width instead of being a fixed size. */
.game-shortcuts .bottom-bar .quick-slot,
.game-shortcuts .bottom-bar .quick-slot[data-slot] {
  flex: 1 1 0;
  width: auto;
  min-width: 0;
  max-width: 38px;
  height: 34px;
}

/* The slot number, which is now also its keyboard shortcut. */
.quick-slot small {
  top: 0;
  left: 2px;
  font-size: 8px;
  font-weight: 900;
  opacity: .85;
}

.quick-slot .ability-icon {
  width: 22px;
  height: 22px;
  margin: 2px auto 0;
  font-size: 16px;
}

/* An item on the bar reads differently from a spell: square, warmer, and it
 * shows how many are left rather than a cooldown. */
.quick-slot .ability-icon.item {
  color: #ffd48a;
  border-radius: 4px;
}

.quick-slot em.count {
  position: absolute;
  right: 2px;
  bottom: 1px;
  color: #ffe9a8;
  font-style: normal;
  font-size: 8px;
  font-weight: 900;
  text-shadow: 1px 1px #000;
}

.quick-slot em.cooldown-count {
  position: absolute;
  z-index: 3;
  inset: 50% auto auto 50%;
  min-width: 24px;
  min-height: 24px;
  display: grid;
  padding: 3px;
  place-items: center;
  translate: -50% -50%;
  color: #fff7d1;
  background: #07101de8;
  border: 2px solid #bcefff;
  border-radius: 50%;
  font: 900 13px/1 system-ui;
  text-align: center;
  text-shadow: 1px 1px #000;
  box-shadow: 0 0 8px #79dbea, 0 0 5px #000;
}
.quick-slot em.cooldown-count[hidden] { display: none; }
.quick-slot.recovering .ability-icon { filter: grayscale(.85) brightness(.55); }

/* Backpack rows favour scanning. Full stats and actions live one tap deeper;
 * the green upgrade edge remains the strongest signal on this screen. */
.menu-grid button.inventory-card { min-height: 39px; padding: 4px 5px; font-size: 10px; }
.menu-grid button.inventory-card small { margin-top: 2px; font-size: 7px; line-height: 1.12; }
.menu-grid button.inventory-card .item-icon { width: 28px; height: 28px; margin: -1px 5px -1px 0; }
.menu-grid button.pack-meter { min-height: 34px; padding: 4px 7px; }
.menu-grid button.pack-full { color: #6f1717; border-color: #b52d2d; background: #e4b07c; }
.menu-grid button.pack-full small { color: #7c1b1b; }

/* The toast sat where the bar now is. */
.message { bottom: 46px; }

@media (max-width: 360px) {
  .bottom-bar { gap: 2px; }
  .slot-rail { gap: 2px; }
  .quick-slot .ability-icon { width: 18px; height: 18px; font-size: 13px; }
}

@media (max-width: 470px) {
  .game-shortcuts .bottom-bar .quick-slot,
  .game-shortcuts .bottom-bar .quick-slot[data-slot] { max-width: 44px; height: 36px; }
}

/* The media query above keys off the VIEWPORT, but the handheld screen is a
 * responsive element inside a much larger page -- 414px wide in a 1280px
 * window -- so it never matched where it mattered. A container query asks the
 * right question: how wide is the screen the bar is actually in. */
.screen { container-type: inline-size; container-name: handheld; }

@container handheld (max-width: 470px) {
  .game-shortcuts .bottom-bar .quick-slot,
  .game-shortcuts .bottom-bar .quick-slot[data-slot] { max-width: 44px; height: 36px; }
  .potion-button, .stand-button, .panic-flee { bottom: 50px; }
  /* STAND alone is raised: it shares the right-hand corner with the down
   * exit and the help button, and at 50px it sat exactly on top of help --
   * invisible, and the tap went to help instead. The potion and flee
   * buttons are on the left and have no such quarrel. */
  .stand-button { bottom: 120px; }
}

/* --- menu detail text, readable on the panel it is actually on -------------
 *
 * The comparison colours (#85f5aa green, #8bd8ff blue, #ff8e8e red) were
 * picked for a dark background, but the menu panel is light tan (#e5c37b).
 * Pale mint on pale tan is close to invisible -- the quest-point catalogue was
 * unreadable, which is a poor place for it since every line there is a price.
 *
 * Same meanings, same hues, darkened to sit on a light ground. Rarity colours
 * on the button label get the same treatment for the same reason.
 */
.menu-grid button.item-upgrade small { color: #0d6b33; }
.menu-grid button.item-downgrade small { color: #9a1f1f; }
.menu-grid button.item-match small { color: #4a4033; }
.menu-grid button.item-open-slot small { color: #14556f; }

/* The base detail line was inheriting the button colour at 78% opacity, which
 * on some rarities left it washed out. Pin it dark and lift the opacity. */
.menu-grid button small { color: #4a3316; opacity: .92; font-weight: 600; }

/* Rarity tints were also chosen for a dark ground. */
.menu-grid button.rarity-legendary { color: #7a5300; }
.menu-grid button.rarity-mythic { color: #6c2076; }

/* A selected row goes dark, so its text has to go light again. */
.menu-grid button.selected small,
.menu-grid button.selected.item-upgrade small,
.menu-grid button.selected.item-downgrade small,
.menu-grid button.selected.item-match small,
.menu-grid button.selected.item-open-slot small { color: #ffe9b8; }

/* And the unaffordable rows stay legible rather than merely dim. */
.menu-grid .shop-action.unaffordable { color: #4a4238; }
.menu-grid .shop-action.unaffordable small { color: #6a3b3b; }

/* --- quest board state ------------------------------------------------------
 * A finished lesson reads as finished at a glance: green edge, a tick in the
 * detail line, and the BEGIN button gone. The one in progress is amber.
 */
.menu-grid button.quest-done { color: #12491f; background: #cfe6c2; border-color: #4e8a49; }
.menu-grid button.quest-done small { color: #1c6b2c; font-weight: 700; opacity: 1; }
.menu-grid button.quest-active { color: #4a3000; background: #f3d089; border-color: #b6812a; }
.menu-grid button.quest-active small { color: #7a4f00; font-weight: 700; opacity: 1; }
.menu-grid button.quest-done.selected,
.menu-grid button.quest-active.selected { color: #fff0c2; background: #63351b; }

/* --- reclaimed top space ---------------------------------------------------
 * The logo row is gone, so the grid loses its first track and the screen grows
 * by 28px. The live connection state is still on the strip under the screen.
 */
.handheld { grid-template-rows: minmax(0, 1fr) clamp(154px, 21dvh, 190px) 22px; }
.brand { display: none; }

/* --- the creation mirror ---------------------------------------------------
 * A sprite standing on the auth screen while you build it. Sized to the same
 * 64px cell the world uses, scaled up so the pixels read on a phone.
 */
.auth-preview {
  display: grid;
  place-items: center;
  width: 96px;
  height: 96px;
  margin: 0 auto 2px;
  background: radial-gradient(circle at 50% 62%, #2b3d52 0%, #16202c 70%);
  border: 2px solid #79dbea;
  border-radius: 6px;
  box-shadow: inset 0 0 14px #79dbea33;
}
.auth-preview[hidden] { display: none; }
.auth-preview .player-sprite {
  position: relative;
  inset: auto;
  transform: scale(1.4);
  transform-origin: center;
}

/* --- the class picker has to fit seven -------------------------------------
 * Two columns make seven callings four rows instead of seven. The odd final
 * card spans the row so it is not left as a cramped half-button by itself.
 */
#auth-choices { max-height: 62vh; overflow-y: auto; overscroll-behavior: contain; }
#auth-choices button.class-pick { min-width: 0; min-height: 40px; padding: 4px 5px; }
#auth-choices button.class-pick:nth-last-child(1):nth-child(odd) { grid-column: 1 / -1; }
#auth-choices button.class-pick strong { font-size: 11px; letter-spacing: .7px; }
#auth-choices button.class-pick small {
  display: block;
  margin-top: 1px;
  overflow: hidden;
  font-size: 7px;
  line-height: 1.15;
  letter-spacing: .15px;
  text-overflow: ellipsis;
  white-space: nowrap;
  opacity: .85;
}
/* The heading block above it can give up some room too. */
.auth-screen h2 { margin: 2px 0; }
.auth-screen > p { margin: 0 0 4px; }

/* --- the auth screen has to fit its own form -------------------------------
 * The registry panel is a fixed-height screen with a heading, a mark, copy, a
 * two-field form and a button. On a short screen the button fell off the
 * bottom, which is the one control that has to be reachable.
 */
.auth-screen {
  overflow-y: auto;
  overscroll-behavior: contain;
  padding-bottom: 10px;
}
.auth-mark { font-size: 22px; margin-bottom: 0; }
.auth-screen > small { font-size: 8px; }
#auth-form { gap: 3px; }
#auth-form label { font-size: 8px; }
#auth-form input { min-height: 34px; }
.auth-remember { font-size: 8px; }
#auth-submit { min-height: 38px; }

/* --- the creation mirror step ----------------------------------------------
 * Ten slot rows plus a preview plus a confirm button did not fit. The slots
 * pair up two to a row, the mirror shrinks, and the whole panel scrolls if a
 * shorter phone still cannot take it.
 */
#auth-choices { display: flex; flex-wrap: wrap; gap: 4px; justify-content: center; }
#auth-choices button.look-slot {
  flex: 0 0 calc(50% - 4px);
  min-height: 0;
  padding: 4px 6px;
}
#auth-choices button.look-slot strong { font-size: 10px; letter-spacing: .5px; }
#auth-choices button.look-slot small { font-size: 8px; opacity: .8; }
#auth-choices button.wide { flex: 0 0 100%; }

/* A smaller mirror on the look step, since the rows below it need the room. */
.auth-preview { width: 78px; height: 78px; }
.auth-preview .player-sprite { transform: scale(1.15); }

/* --- the mirror step earns its space ---------------------------------------
 * The registry mark and strapline are scene-setting for the login screen and
 * pure overhead once you are dressing a character. Dropping them on this step
 * buys about 60px, which goes to a bigger mirror -- the thing actually worth
 * looking at here.
 */
.auth-screen[data-stage="look"] .auth-mark,
.auth-screen[data-stage="look"] > small { display: none; }

.auth-screen[data-stage="look"] h2 {
  margin: 0 0 1px;
  font-size: 15px;
  letter-spacing: 2px;
}
.auth-screen[data-stage="look"] > p {
  margin: 0 0 5px;
  font-size: 9px;
  opacity: .8;
}
.auth-screen[data-stage="look"] .auth-preview {
  width: 112px;
  height: 112px;
  margin-bottom: 5px;
}
.auth-screen[data-stage="look"] .auth-preview .player-sprite { transform: scale(1.7); }


/* --- mounts ---------------------------------------------------------------
 *
 * Sheets are 16 columns by 8 rows of 64x64, so the background is sized 1600%
 * by 800% and a column step is 100/15 = 6.6667%, a row step 100/7 = 14.2857%.
 * Columns 0-3 face south, 4-7 east, 8-11 north, 12-15 west -- west has its own
 * frames, so unlike the farmer rig nothing is mirrored.
 */
.mount-layer {
  position: absolute; inset: -5px; display: none;
  background-size: 1600% 800%; background-repeat: no-repeat;
  background-position: 0 0; image-rendering: pixelated;
  transform: translateY(11px); pointer-events: none;
}
.player-sprite.mounted .mount-layer, .remote-player.mounted .mount-layer { display: block; }

/* Behind the rider, in front of the rider, and on the ground beneath both. */
.mount-bottom { z-index: 0; background-image: var(--mount-bottom, none); }
.mount-top    { z-index: 5; background-image: var(--mount-top, none); }
/* The shadow sheet is four 32x32 cells, and the pack puts that art in the
 * bottom-middle QUARTER of the 64x64 mount cell rather than across the whole
 * of it. Stretching it over the full layer drew it at double size, which is
 * the outsized smudge that showed under a standing mount. */
.mount-shadow {
  z-index: -1; background-image: var(--mount-shadow, none);
  inset: auto; left: 25%; top: 50%; width: 50%; height: 50%;
  background-size: 400% 100%; opacity: .5; transform: translateY(11px);
}

/* Sitting in the saddle, not standing on top of it.
 *
 * Hardy Horse's animation guide names the rider's pose for every frame, and
 * the saddle poses are farmer cells 056 (south), 072 (north) and 088 (east,
 * with west the same cell flipped) -- column 8, rows 3 to 5 of the 16x16
 * farmer sheet. The guide aligns the farmer's 64x64 cell with the horse's and
 * then applies a per-frame X/Y, so these keep .farmer-layer's own 11px and add
 * that nudge. (The -3px this used to carry was tuned to fake a seat out of the
 * standing pose; with the real saddle cell it just lifts the rider off the
 * horse.)
 *
 * Only the cell was missing before, so a mounted character kept its standing
 * pose and appeared to stand upright on the horse's back. The farmer walk
 * cycle is killed for the same reason: the mount's legs carry the movement,
 * and stepping the rider would walk them out of the saddle. */
/*
 * --ride-lift is how far the rider drops from the horse's seat height.
 *
 * These offsets were measured against Hardy Horse, whose back is high and
 * long. Every generated animal is a different shape -- a rhino is low and
 * broad, a beetle lower still -- so reusing the horse's number left the rider
 * hovering above the animal rather than sitting on it. The mount sets its own
 * lift and these stay the baseline it is measured from.
 */
.player-sprite.mounted .farmer-layer, .remote-player.mounted .farmer-layer {
  animation: none !important;
  background-position: 53.3333% 20%;
  transform: translateY(calc(10px + var(--ride-lift, 0px)));
}
.player-sprite.mounted[data-facing="north"] .farmer-layer,
.remote-player.mounted[data-facing="north"] .farmer-layer {
  background-position: 53.3333% 26.6667%;
  transform: translateY(calc(11px + var(--ride-lift, 0px)));
}
.player-sprite.mounted[data-facing="east"] .farmer-layer,
.remote-player.mounted[data-facing="east"] .farmer-layer {
  background-position: 53.3333% 33.3333%;
  transform: translate(1px, calc(11px + var(--ride-lift, 0px)));
}
.player-sprite.mounted[data-facing="west"] .farmer-layer,
.remote-player.mounted[data-facing="west"] .farmer-layer {
  background-position: 53.3333% 33.3333%;
  transform: translate(-1px, calc(11px + var(--ride-lift, 0px))) scaleX(-1);
}

/* Riding the gait.
 *
 * Through the walk the guide holds the same saddle cell and alternates the
 * rider's Y by one pixel per frame -- that is the posting bob, and without it
 * the rider is welded to the horse. Timed to the mount's own .62s four-frame
 * cycle so the two rise and fall together. */
.player-sprite.mounted.walking .farmer-layer,
.player-sprite.mounted.continuous-walk .farmer-layer,
.remote-player.mounted.walking .farmer-layer { animation: ride-post-south .62s steps(1) infinite !important; }
.player-sprite.mounted.walking[data-facing="north"] .farmer-layer,
.player-sprite.mounted.continuous-walk[data-facing="north"] .farmer-layer,
.remote-player.mounted.walking[data-facing="north"] .farmer-layer { animation-name: ride-post-north !important; }
.player-sprite.mounted.walking[data-facing="east"] .farmer-layer,
.player-sprite.mounted.continuous-walk[data-facing="east"] .farmer-layer,
.remote-player.mounted.walking[data-facing="east"] .farmer-layer { animation-name: ride-post-east !important; }
.player-sprite.mounted.walking[data-facing="west"] .farmer-layer,
.player-sprite.mounted.continuous-walk[data-facing="west"] .farmer-layer,
.remote-player.mounted.walking[data-facing="west"] .farmer-layer { animation-name: ride-post-west !important; }
@keyframes ride-post-south {
  0%   { transform: translateY(calc(10px + var(--ride-lift, 0px))); }
  25%  { transform: translateY(calc(11px + var(--ride-lift, 0px))); }
  50%  { transform: translateY(calc(10px + var(--ride-lift, 0px))); }
  75%  { transform: translateY(calc(11px + var(--ride-lift, 0px))); }
}
@keyframes ride-post-north {
  0%   { transform: translateY(calc(10px + var(--ride-lift, 0px))); }
  25%  { transform: translateY(calc(11px + var(--ride-lift, 0px))); }
  50%  { transform: translateY(calc(10px + var(--ride-lift, 0px))); }
  75%  { transform: translateY(calc(11px + var(--ride-lift, 0px))); }
}
@keyframes ride-post-east {
  0%   { transform: translate(1px, calc(10px + var(--ride-lift, 0px))); }
  25%  { transform: translate(1px, calc(11px + var(--ride-lift, 0px))); }
  50%  { transform: translate(1px, calc(10px + var(--ride-lift, 0px))); }
  75%  { transform: translate(1px, calc(11px + var(--ride-lift, 0px))); }
}
@keyframes ride-post-west {
  0%   { transform: translate(-1px, calc(10px + var(--ride-lift, 0px))) scaleX(-1); }
  25%  { transform: translate(-1px, calc(11px + var(--ride-lift, 0px))) scaleX(-1); }
  50%  { transform: translate(-1px, calc(10px + var(--ride-lift, 0px))) scaleX(-1); }
  75%  { transform: translate(-1px, calc(11px + var(--ride-lift, 0px))) scaleX(-1); }
}

.player-sprite.mounted[data-facing="south"] .mount-layer, .remote-player.mounted[data-facing="south"] .mount-layer { background-position-x: 0%; }
.player-sprite.mounted[data-facing="east"]  .mount-layer, .remote-player.mounted[data-facing="east"]  .mount-layer { background-position-x: 26.6667%; }
.player-sprite.mounted[data-facing="north"] .mount-layer, .remote-player.mounted[data-facing="north"] .mount-layer { background-position-x: 53.3333%; }
.player-sprite.mounted[data-facing="west"]  .mount-layer, .remote-player.mounted[data-facing="west"]  .mount-layer { background-position-x: 80%; }

/*
 * Some generated animals have a spoiled west strip.
 *
 * The stable normally draws all four facings from its own frames, which is
 * better than mirroring -- a mirrored animal has its markings on the wrong
 * side. But the giant fox's west frames are clipped through the face, and a
 * fox with half a head is worse than a fox whose blaze is reversed, so those
 * mounts borrow the east frames and flip them instead. Only the body layers:
 * the shadow is symmetrical and has its own column rule.
 */
.player-sprite.mounted.mirror-west[data-facing="west"] .mount-bottom,
.player-sprite.mounted.mirror-west[data-facing="west"] .mount-top,
.remote-player.mounted.mirror-west[data-facing="west"] .mount-bottom,
.remote-player.mounted.mirror-west[data-facing="west"] .mount-top {
  background-position-x: 26.6667%;
  transform: translateY(11px) scaleX(-1);
}
/* Those four offsets are 16-column maths for the body sheets. The shadow sheet
 * has only four cells, so the same percentages land between two of them and
 * blend a pair of shadows together. One cell, every facing -- a shadow does
 * not need to turn. */
.player-sprite.mounted[data-facing] .mount-shadow,
.remote-player.mounted[data-facing] .mount-shadow { background-position-x: 0%; }

/* Row 1 is the walk cycle; four frames per direction. */
.player-sprite.mounted.walking .mount-bottom, .player-sprite.mounted.walking .mount-top,
.player-sprite.mounted.continuous-walk .mount-bottom, .player-sprite.mounted.continuous-walk .mount-top,
.remote-player.mounted.walking .mount-bottom, .remote-player.mounted.walking .mount-top {
  background-position-y: 14.2857%;
}
.player-sprite.mounted.walking[data-facing="south"] .mount-bottom,
.player-sprite.mounted.walking[data-facing="south"] .mount-top,
.player-sprite.mounted.continuous-walk[data-facing="south"] .mount-bottom,
.player-sprite.mounted.continuous-walk[data-facing="south"] .mount-top { animation: mount-walk-south .62s steps(4) infinite; }
.player-sprite.mounted.walking[data-facing="east"] .mount-bottom,
.player-sprite.mounted.walking[data-facing="east"] .mount-top,
.player-sprite.mounted.continuous-walk[data-facing="east"] .mount-bottom,
.player-sprite.mounted.continuous-walk[data-facing="east"] .mount-top { animation: mount-walk-east .62s steps(4) infinite; }
.player-sprite.mounted.walking[data-facing="north"] .mount-bottom,
.player-sprite.mounted.walking[data-facing="north"] .mount-top,
.player-sprite.mounted.continuous-walk[data-facing="north"] .mount-bottom,
.player-sprite.mounted.continuous-walk[data-facing="north"] .mount-top { animation: mount-walk-north .62s steps(4) infinite; }
.player-sprite.mounted.walking[data-facing="west"] .mount-bottom,
.player-sprite.mounted.walking[data-facing="west"] .mount-top,
.player-sprite.mounted.continuous-walk[data-facing="west"] .mount-bottom,
.player-sprite.mounted.continuous-walk[data-facing="west"] .mount-top { animation: mount-walk-west .62s steps(4) infinite; }

/*
 * A mirrored mount has to walk on the east strip as well as stand on it.
 *
 * The standing rule below parks these on the east frames and flips them, but
 * the walk cycle animates background-position-x, and an animation beats a
 * plain declaration -- so a mirrored fox walking west stepped back onto its
 * own damaged west frames while still carrying the flip, and came out facing
 * right. Walking the east strip instead keeps the flip meaningful.
 */
.player-sprite.mounted.mirror-west.walking[data-facing="west"] .mount-bottom,
.player-sprite.mounted.mirror-west.walking[data-facing="west"] .mount-top,
.player-sprite.mounted.mirror-west.continuous-walk[data-facing="west"] .mount-bottom,
.player-sprite.mounted.mirror-west.continuous-walk[data-facing="west"] .mount-top,
.remote-player.mounted.mirror-west.walking[data-facing="west"] .mount-bottom,
.remote-player.mounted.mirror-west.walking[data-facing="west"] .mount-top {
  animation-name: mount-walk-east !important;
}

/* --- a mount that hops ------------------------------------------------------
 *
 * The generated animal sheets give a frog two walk poses and no vertical
 * travel, so it shuffles along the ground; and its idle row is four copies of
 * one drawing, so standing still it is a photograph. What is missing is the
 * lift, and that can be added without new art.
 *
 * Three parts have to agree or it reads as a glitch: the body rises, the
 * shadow stays on the ground and shrinks -- that is what says "off the ground"
 * rather than "drawn higher" -- and the rider goes up with it instead of
 * hovering while the frog drops away beneath.
 *
 * Written to the independent `translate` property rather than `transform`,
 * which the rider's posting bob and the mirror flip already own; animating
 * transform here would clobber both.
 */
@keyframes mount-hop {
  0%   { translate: 0 1px; }
  18%  { translate: 0 -4px; }
  42%  { translate: 0 -5px; }
  68%  { translate: 0 0; }
  78%  { translate: 0 1px; }
  100% { translate: 0 0; }
}
@keyframes mount-hop-shadow {
  0%, 100% { scale: 1 1;     opacity: .5; }
  42%      { scale: .72 .72; opacity: .26; }
}
/* Still breathing when standing still. */
@keyframes mount-idle-breathe {
  0%, 100% { translate: 0 0; }
  50%      { translate: 0 -1px; }
}

.player-sprite.mounted.mount-hops.walking[data-facing="south"] .mount-bottom,
.player-sprite.mounted.mount-hops.walking[data-facing="south"] .mount-top,
.player-sprite.mounted.mount-hops.continuous-walk[data-facing="south"] .mount-bottom,
.player-sprite.mounted.mount-hops.continuous-walk[data-facing="south"] .mount-top {
  animation: mount-walk-south .62s steps(4) infinite, mount-hop .62s ease-in-out infinite;
}
.player-sprite.mounted.mount-hops.walking[data-facing="east"] .mount-bottom,
.player-sprite.mounted.mount-hops.walking[data-facing="east"] .mount-top,
.player-sprite.mounted.mount-hops.continuous-walk[data-facing="east"] .mount-bottom,
.player-sprite.mounted.mount-hops.continuous-walk[data-facing="east"] .mount-top {
  animation: mount-walk-east .62s steps(4) infinite, mount-hop .62s ease-in-out infinite;
}
.player-sprite.mounted.mount-hops.walking[data-facing="north"] .mount-bottom,
.player-sprite.mounted.mount-hops.walking[data-facing="north"] .mount-top,
.player-sprite.mounted.mount-hops.continuous-walk[data-facing="north"] .mount-bottom,
.player-sprite.mounted.mount-hops.continuous-walk[data-facing="north"] .mount-top {
  animation: mount-walk-north .62s steps(4) infinite, mount-hop .62s ease-in-out infinite;
}
.player-sprite.mounted.mount-hops.walking[data-facing="west"] .mount-bottom,
.player-sprite.mounted.mount-hops.walking[data-facing="west"] .mount-top,
.player-sprite.mounted.mount-hops.continuous-walk[data-facing="west"] .mount-bottom,
.player-sprite.mounted.mount-hops.continuous-walk[data-facing="west"] .mount-top {
  animation: mount-walk-west .62s steps(4) infinite, mount-hop .62s ease-in-out infinite;
}
/* Other people's mounts never stepped their columns, so the hop is the only
 * motion they get -- and it is the half that carries the most. */
.remote-player.mounted.mount-hops.walking .mount-bottom,
.remote-player.mounted.mount-hops.walking .mount-top {
  animation: mount-hop .62s ease-in-out infinite;
}
/* The shadow does not travel with the body; it reacts to it. */
.player-sprite.mounted.mount-hops.walking .mount-shadow,
.player-sprite.mounted.mount-hops.continuous-walk .mount-shadow,
.remote-player.mounted.mount-hops.walking .mount-shadow {
  animation: mount-hop-shadow .62s ease-in-out infinite;
}
/* The rider rises with the frog. Each facing needs its own shorthand: the
 * posting keyframes differ per direction, and naming two animations while
 * inheriting steps(1) would make the hop stutter. */
.player-sprite.mounted.mount-hops.walking .farmer-layer,
.player-sprite.mounted.mount-hops.continuous-walk .farmer-layer,
.remote-player.mounted.mount-hops.walking .farmer-layer {
  animation: ride-post-south .62s steps(1) infinite, mount-hop .62s ease-in-out infinite !important;
}
.player-sprite.mounted.mount-hops.walking[data-facing="north"] .farmer-layer,
.player-sprite.mounted.mount-hops.continuous-walk[data-facing="north"] .farmer-layer,
.remote-player.mounted.mount-hops.walking[data-facing="north"] .farmer-layer {
  animation: ride-post-north .62s steps(1) infinite, mount-hop .62s ease-in-out infinite !important;
}
.player-sprite.mounted.mount-hops.walking[data-facing="east"] .farmer-layer,
.player-sprite.mounted.mount-hops.continuous-walk[data-facing="east"] .farmer-layer,
.remote-player.mounted.mount-hops.walking[data-facing="east"] .farmer-layer {
  animation: ride-post-east .62s steps(1) infinite, mount-hop .62s ease-in-out infinite !important;
}
.player-sprite.mounted.mount-hops.walking[data-facing="west"] .farmer-layer,
.player-sprite.mounted.mount-hops.continuous-walk[data-facing="west"] .farmer-layer,
.remote-player.mounted.mount-hops.walking[data-facing="west"] .farmer-layer {
  animation: ride-post-west .62s steps(1) infinite, mount-hop .62s ease-in-out infinite !important;
}
/* Standing: a slow breath, on the mount only. One pixel, so the rider staying
 * put reads as weight rather than as a seam. */
.player-sprite.mounted.mount-hops:not(.walking):not(.continuous-walk) .mount-bottom,
.player-sprite.mounted.mount-hops:not(.walking):not(.continuous-walk) .mount-top,
.remote-player.mounted.mount-hops:not(.walking) .mount-bottom,
.remote-player.mounted.mount-hops:not(.walking) .mount-top {
  animation: mount-idle-breathe 2.6s ease-in-out infinite;
}

@keyframes mount-walk-south { from { background-position-x: 0%; }       to { background-position-x: 26.6667%; } }
@keyframes mount-walk-east  { from { background-position-x: 26.6667%; } to { background-position-x: 53.3333%; } }
@keyframes mount-walk-north { from { background-position-x: 53.3333%; } to { background-position-x: 80%; } }
@keyframes mount-walk-west  { from { background-position-x: 80%; }      to { background-position-x: 106.6667%; } }

@media (prefers-reduced-motion: reduce) {
  .mount-layer { animation: none !important; }
}

/* --- the arrow ------------------------------------------------------------
 *
 * A shot is over in a moment, so the arrow is one element that crosses the
 * room and removes itself rather than anything retained.
 */
/*
 * Two elements, because the outer one is animated with a transform and a
 * mirrored west arrow needs a transform of its own. One element cannot carry
 * both -- the keyframe would overwrite the flip.
 */
.loosed-projectile {
  position: absolute; z-index: 7; width: 24px; height: 24px;
  margin: -12px 0 0 -12px; pointer-events: none;
  animation: projectile-fly .34s linear forwards;
}
/* The cell is chosen from the flight angle in JS -- the sheet is a quarter
 * turn in ten steps -- and mirrored into the other three quadrants here. */
.loosed-arrow > i {
  display: block; width: 100%; height: 100%;
  background-image: url("assets/effects/arrow.png");
  background-size: 500% 200%;
  background-position: var(--arrow-col, 0) var(--arrow-row, 0);
  transform: scale(var(--arrow-flip-x, 1), var(--arrow-flip-y, 1));
  image-rendering: pixelated;
}
.loosed-projectile[data-element] > i {
  filter: hue-rotate(var(--el-hue, 0deg)) saturate(var(--el-sat, 1)) brightness(var(--el-bright, 1));
}
.loosed-arrow { filter: drop-shadow(0 0 3px var(--el-glow, #f5dfad)); }
/* The old per-facing cells are gone: they picked three of the ten drawn angles
 * and picked them wrongly, which is what sent arrows sideways. */
/* The alchemist throws a visible volatile potion. The bone totem remains in
 * hand as the focus that mixes it; it is never the projectile. */
/*
 * What an alchemist throws.
 *
 * The sphere from fx-portal-v1.png rather than a bottle drawn in CSS: the
 * art was already in the assets folder and unused, and it is pixel art like
 * everything else in the room. Nine colours in the sheet, nine elements in
 * the table above -- so the orb is tinted by what is being thrown.
 *
 * A sphere reads the same from every angle, which is why this carries none
 * of the per-frame rotation the other weapon props need.
 */
.thrown-flask { width: 34px; height: 34px; margin: -17px 0 0 -17px; filter: drop-shadow(0 0 5px var(--el-glow, #ffb22e)); }
.thrown-flask > i {
  display: block; width: 34px; height: 34px;
  background-image: url("assets/fx-portal-v1.png");
  background-repeat: no-repeat;
  /* Sized to the whole sheet and shifted by cell, so the orb stays crisp. */
  background-size: 1500% 900%;
  background-position: 57.1429% var(--orb-row, 0%);
  image-rendering: pixelated;
}
.thrown-flask[data-element="fire"] > i { --orb-row: 87.5000%; }
.thrown-flask[data-element="frost"] > i { --orb-row: 25.0000%; }
.thrown-flask[data-element="arcane"] > i { --orb-row: 12.5000%; }
.thrown-flask[data-element="shadow"] > i { --orb-row: 100.0000%; }
.thrown-flask[data-element="toxic"] > i { --orb-row: 37.5000%; }
.thrown-flask[data-element="verdant"] > i { --orb-row: 37.5000%; }
.thrown-flask[data-element="tide"] > i { --orb-row: 25.0000%; }
.thrown-flask[data-element="volt"] > i { --orb-row: 50.0000%; }
.thrown-flask[data-element="sand"] > i { --orb-row: 75.0000%; }
.thrown-flask[data-element="stone"] > i { --orb-row: 62.5000%; }
@keyframes projectile-fly {
  from { opacity: 1; }
  to   { opacity: 0; transform: translate(var(--projectile-dx, 0), var(--projectile-dy, -34px)); }
}
/* No spin: a sphere turning looks like nothing turning, and the sheet's own
 * shading would smear if it did. It pulses instead, which reads as something
 * alive being thrown. */
.thrown-flask > i { animation: orb-pulse .34s ease-out; }
@keyframes orb-pulse { from { transform: scale(.72); } to { transform: scale(1.08); } }
.potion-burst {
  position: absolute; width: 18px; height: 18px; margin: -9px;
  z-index: 8; pointer-events: none; border-radius: 50%;
  background: var(--el-glow, #ff8b39);
  box-shadow: 0 0 5px 2px var(--el-glow, #ff8b39);
  animation: potion-burst .38s ease-out forwards;
}
@keyframes potion-burst {
  from { opacity: 1; transform: scale(.35); }
  to { opacity: 0; transform: scale(3.2); }
}
@media (prefers-reduced-motion: reduce) { .loosed-projectile { animation-duration: .01s; } }


/* --- the held bow ---------------------------------------------------------
 *
 * Its own element, because .sprite-layer is visibility:hidden by default and
 * .sprite-layer.sword only appears mid-swing. Four 32x32 cells in one row, so
 * the background is 400% wide and a cell step is 100/3 = 33.3333%.
 */
.bow-prop {
  position: absolute; display: none; z-index: 4;
  width: 32px; height: 32px; left: 50%; top: 50%;
  margin: -2px 0 0 -10px;
  background-image: url("assets/effects/bow.png");
  background-size: 400% 100%; background-repeat: no-repeat;
  background-position: 0 0; image-rendering: pixelated;
  pointer-events: none;
}
/* Retired: this drew a bow beside an archer at all times, so that wielding one
 * was visible on the idle and walk frames, which have no bow of their own. It
 * was the only weapon in the game to do that -- a sword is invisible until it
 * swings -- and once the farmer rig started drawing a real bow for the shot,
 * the icon was simply a second, floating bow that never sat in the hand. The
 * element stays in the markup; only the shot draws a bow now. */
.player-sprite[data-weapon="bow"] .bow-prop,
.remote-player[data-weapon="bow"] .bow-prop { display: none; }
/* The first cell (0%) for every direction -- the only one of the four that reads as a held bow; the others are a mid-draw diamond shape that looked broken. */
.bow-prop { background-position-x: 0%; }
.player-sprite[data-facing="west"] .bow-prop,
.remote-player[data-facing="west"] .bow-prop { transform: scaleX(-1); }

/* Drawing the string: a short pull back as the shot goes off, swapping to the
 * fully-drawn cell at the peak and back to the relaxed cell as the arrow
 * releases -- resting between shots always shows the relaxed cell (0%). */
.player-sprite.loosing .bow-prop, .remote-player.loosing .bow-prop { animation: bow-draw .34s ease-out; }
@keyframes bow-draw {
  0%   { margin-left: -10px; background-position-x: 0%; }
  40%  { margin-left: -13px; background-position-x: 33.3333%; }
  100% { margin-left: -10px; background-position-x: 0%; }
}
@media (prefers-reduced-motion: reduce) { .bow-prop { animation: none !important; } }


/* --- a bow is not a sword -------------------------------------------------
 *
 * The bow is animated by the farmer combat rig now (farmerBowShot in app.js),
 * which drives this layer with inline styles and the Farmer System's own bow
 * prop. Outside that routine the layer must stay dark: `attacking` is set by
 * anything that swings -- a practice swing, smashing a pot -- and its rule
 * points at the sword sheet, so without this an archer draws a blade.
 */
.player-sprite[data-weapon="bow"]:not(.farmer-combat-active) > .sprite-layer.sword,
.remote-player[data-weapon="bow"]:not(.farmer-combat-active) > .sprite-layer.sword { display: none !important; }
/* Held in front of the body, not lost behind it. */
.bow-prop { z-index: 8; }

/* One bow at a time.
 *
 * .bow-prop is the carried bow, drawn for the idle and walking frames that have
 * no bow of their own. The moment the farmer rig takes over it draws the real
 * bow on the weapon layer, so leaving this one up put two bows on screen. */
.player-sprite.farmer-combat-active[data-weapon="bow"] .bow-prop,
.remote-player.farmer-combat-active[data-weapon="bow"] .bow-prop { display: none !important; }

/*
 * Facing away, the bow is on the far side of you.
 *
 * One cell serves every direction because only that pose reads as a bow, but
 * depth still has to be right: drawn over the character back it looks stuck to
 * them rather than carried. Behind the body when facing north, in front the
 * rest of the time.
 */
.player-sprite[data-facing="north"] .bow-prop,
.remote-player[data-facing="north"] .bow-prop { z-index: -1; }

/* The stall keeper: a player sprite that happens to be a button. */
.stall-keeper { padding: 0; border: 0; background: none; cursor: pointer; pointer-events: auto; }
.stall-keeper > b { position: absolute; left: 50%; top: 45px; width: max-content; max-width: 116px; padding: 2px 5px; overflow: hidden; color: #ffe9a8; background: #0f380fe6; border: 1px solid #d89b35; border-radius: 2px; transform: translateX(-50%); white-space: nowrap; text-overflow: ellipsis; font: 700 9px/1.2 system-ui, sans-serif; letter-spacing: .2px; text-shadow: 1px 1px 0 #06170a; }
.stall-keeper:hover > b, .stall-keeper:focus-visible > b { color: #fff3a1; border-color: #fff3a1; }

/* Shown when the link drops with somebody already in the world. Above the
 * death screen deliberately: being disconnected outranks anything the game
 * was in the middle of telling you. */
/* safe centring plus overflow: centred while it fits, reachable when it does
 * not. Without this the reconnect button lands below the screen on a short
 * one, which is the only control here that does anything. */
.offline-screen { position: absolute; inset: 0; z-index: 36; display: grid; align-content: safe center; justify-items: center; gap: 4px; padding: 10px; overflow: auto; text-align: center; color: #eaf6b4; background: #06121ef2; }
.offline-screen[hidden] { display: none; }
.offline-screen .offline-mark { font-size: 20px; line-height: 1; filter: drop-shadow(0 0 8px #79dbea); }
.offline-screen h2 { margin: 0; color: #fff3a1; font: 900 12px/1.2 system-ui, sans-serif; letter-spacing: .08em; }
.offline-screen p { margin: 0; max-width: 36ch; color: #b9d4a8; font: 500 9px/1.35 system-ui, sans-serif; }
.offline-screen .offline-countdown { color: #79dbea; font-weight: 700; }
.offline-screen button { margin-top: 3px; padding: 6px 12px; color: #06121e; background: linear-gradient(#fff3a1, #d89b35); border: 1px solid #fff3a1; border-radius: 4px; cursor: pointer; font: 900 9px/1 system-ui, sans-serif; letter-spacing: .06em; }
.offline-screen button:active { transform: translateY(1px); }

/* The affects row shares the feed strips' shape so the band reads as one
 * thing, but is not interactive -- there is nothing behind it to open. */
.feed-line.affects { cursor: default; pointer-events: none; border-color: #79dbea55; }
.feed-line.affects > b { color: #9cecff; }
.feed-line.affects .status-effects { max-width: none; }

/* The face in the corner, and whatever is hung around it. */
.identity { display: flex; align-items: center; gap: 7px; min-width: 0; }
.portrait { position: relative; flex: none; width: 34px; height: 34px; overflow: hidden; border-radius: 3px; background: linear-gradient(#16202b, #0a1017); box-shadow: inset 0 0 0 1px #ffffff17; }
/* The rig places itself for standing in a room. Here it is cropped to the head
 * and shoulders, so its own positioning and idle animation are overridden
 * rather than fought with. */
.portrait .player-sprite { position: absolute; left: 50%; top: 50%; width: 54px; height: 54px; transform: translate(-50%, -40%) scale(1.5); transform-origin: 50% 50%; transition: none; filter: none; animation: none; }
.portrait-frame { position: absolute; inset: 0; z-index: 3; pointer-events: none; background-repeat: no-repeat; image-rendering: pixelated; }
/* Nought is bare, and must claim no image: a missing sheet would otherwise
 * paint a broken-image box over somebody's face. */
.portrait[data-frame="0"] .portrait-frame { background-image: none; }

/* [hidden] is a browser default of display:none, and .feed-line sets display
 * to flex, which beats it. Without this the rows take the attribute, report
 * themselves hidden, and go on occupying the screen -- so every strip that
 * stood down was still charging rent for its row. */
.feed-line[hidden], .room-card[hidden] { display: none !important; }

/* The waystone basin is painted into the room, so its light is a layer of its
 * own. Click-through: a glow in the middle of the square must never take a tap
 * meant for the world behind it. */
.fountain-glow { position: absolute; z-index: 0; width: 132px; height: 104px; pointer-events: none; border-radius: 50%; transform: translate(-50%, -58%); background: radial-gradient(ellipse at center, #8feaff66 0%, #4fc8ff2e 46%, transparent 72%); mix-blend-mode: screen; animation: fountain-breathe 3.6s ease-in-out infinite; }
.fountain-glow.alarmed { background: radial-gradient(ellipse at center, #ff6a52dd 0%, #ff2a1466 46%, transparent 74%); animation: fountain-alarm 1.15s ease-in-out infinite; }
@keyframes fountain-breathe { 0%, 100% { opacity: .5; } 50% { opacity: .92; } }
/* Swings brightness AND size: the old version moved opacity from .55 to 1
 * through a screen blend, which at handheld size read as one pulse and
 * then nothing. An alarm has to be visible from across the square. */
@keyframes fountain-alarm {
  0%, 100% { opacity: .3; transform: translate(-50%, -58%) scale(.86); }
  50% { opacity: 1; transform: translate(-50%, -58%) scale(1.14); }
}
@media (prefers-reduced-motion: reduce) {
  .fountain-glow, .fountain-glow.alarmed { animation: none; opacity: .7; }
}

/* Something described in the artwork. Small, and quiet until asked: the room
 * has enough labels in it, and a marker that announced itself would be one
 * more thing between the player and the picture. */
.lore-mark { position: absolute; width: 15px; height: 15px; padding: 0; overflow: visible; color: #ffe9a8; background: #101927b0; border: 1px solid #d89b3588; border-radius: 50%; box-shadow: 0 0 5px #00000066; cursor: pointer; pointer-events: auto; font: 900 9px/13px system-ui, sans-serif; text-align: center; transform: translate(-50%, -50%); animation: lore-breathe 2.8s ease-in-out infinite; }
.lore-mark::before { content: "?"; }
.lore-mark:hover, .lore-mark:focus-visible { color: #fff3a1; border-color: #fff3a1; background: #16233acc; animation: none; }
@keyframes lore-breathe { 0%, 100% { opacity: .55; } 50% { opacity: .95; } }
@media (prefers-reduced-motion: reduce) { .lore-mark { animation: none; opacity: .8; } }

/* A practice target. It wears whatever creature the yard rolled, so this is
 * the only thing telling it apart from a fight you are meant to win. */
.enemy.training-target > b { color: #9cecff; }
.enemy.training-target > b::after { content: " · PRACTICE"; color: #79dbea; font-size: 7px; letter-spacing: .06em; }

/* An orb carried in the hand.
 *
 * Built like .shield-prop, which is the same kind of thing: worn rather than
 * swung, shown by an attribute and hidden by the empty string. The row comes
 * from the element variables, so one place decides that fire is the red one
 * and both the held orb and the thrown one read it. */
.held-orb {
  position: absolute; display: none;
  /* The same distance from the body on every facing, found by magnifying
   * the sprite and looking rather than by arithmetic on a 64px cell.
   * Higher put the orb over the face and the shield; further in drew it
   * through the chest. */
  left: calc(50% - 14px); top: calc(50% - 2px); width: 22px; height: 22px;
  background-image: url("assets/fx-portal-v1.png");
  background-size: 1500% 900%; background-repeat: no-repeat;
  background-position: 57.1429% var(--orb-row, 0%);
  image-rendering: pixelated; pointer-events: none;
  z-index: 11; transform: translateY(4px);
  filter: drop-shadow(0 0 6px var(--el-glow, #ffb22e));
  animation: held-orb-bob 2.4s ease-in-out infinite;
}
.player-sprite[data-orb] .held-orb, .remote-player[data-orb] .held-orb { display: block; }
.player-sprite[data-orb=""] .held-orb, .remote-player[data-orb=""] .held-orb { display: none; }
/* Carried on the far side when facing away, and behind the body when the back
 * is turned -- otherwise it floats in front of a shoulder. */
/* Mirrored: carried on the far side so it is not drawn through the chest. */
.player-sprite[data-facing="west"] .held-orb, .remote-player[data-facing="west"] .held-orb
  { left: calc(50% - 18px); }
/* East is not the mirror of west by the numbers: the character sits off
 * centre in its own box, so an offset equal and opposite to west leaves a
 * wider gap on this side. Pulled in to match what west looks like. */
.player-sprite[data-facing="east"] .held-orb, .remote-player[data-facing="east"] .held-orb
  { left: calc(50% - 4px); z-index: 2; }
/* Back turned: the hand sits higher. It stays in front, which is how it
 * looked when it was working -- putting it behind was a fix for a
 * problem that had not been reported. */
.player-sprite[data-facing="north"] .held-orb, .remote-player[data-facing="north"] .held-orb
  { left: calc(50% - 4px); top: calc(50% - 12px); }
/* It is thrown during the swing, so it is not still in the hand. */
.player-sprite.attacking .held-orb, .remote-player.event-attacking .held-orb { display: none; }
@keyframes held-orb-bob { 0%, 100% { transform: translateY(4px); } 50% { transform: translateY(2px); } }
@media (prefers-reduced-motion: reduce) { .held-orb { animation: none; } }

/* Every creature stands on something.
 *
 * Only humanoids had a shadow, so everything drawn from the monster atlas
 * floated a few pixels above the floor -- which reads as a sprite pasted over
 * a picture rather than a thing standing in a room. The humanoid rule below
 * is more specific and still wins, so the people keep the smaller one cut to
 * their feet. */
.enemy::before { content: ""; position: absolute; left: 50%; top: 78%; z-index: 0; width: 60%; height: 7px; border-radius: 50%; background: #0a1c1080; transform: translateX(-50%); filter: blur(1px); pointer-events: none; }

/* Ground a builder has closed. Edit mode only -- a player has no use for it. */
.blocked-tile { position: absolute; width: 26px; height: 18px; pointer-events: none; background: repeating-linear-gradient(45deg, #ff5a4a3d 0 4px, transparent 4px 8px); border: 1px solid #ff5a4a66; border-radius: 2px; transform: translate(-50%, -50%); }
/* The region being dragged out. Sized in tiles so it grows by the same step the
 * server will close. */
.wall-band { position: absolute; width: calc(var(--band-tiles-x, 1) * 26px); height: calc(var(--band-tiles-y, 1) * 18px); pointer-events: none; background: #ff5a4a2e; border: 1px dashed #ff8b7a; border-radius: 2px; transform: translate(-50%, -50%); }
.wall-band.opening { background: #7ada6a2e; border-color: #9cf08c; }

/* The HELP button, and the state it puts the screen into. */
.help-button b { font-size: 15px; font-weight: 900; line-height: 1; }
.help-button[aria-pressed="true"] { color: #06121e; background: linear-gradient(#fff3a1, #d89b35); border-color: #fff3a1; }
/* While asking, everything is a target: the pointer says so and the world
 * stops inviting a walk. */
.world.help-asking, .world.help-asking * { cursor: help !important; }
.help-asking-hint { position: absolute; left: 8px; right: 8px; bottom: 8px; z-index: 33; padding: 5px 8px; color: #06121e; background: linear-gradient(#fff3a1, #ecc46a); border: 1px solid #fff3a1; border-radius: 4px; text-align: center; font: 900 9px/1.3 system-ui, sans-serif; letter-spacing: .04em; pointer-events: none; }

/* A placed prop that runs through cells of its sheet -- the camp fire, and
 * anything else a pack ships as frames. Only background-position-x moves: the
 * renderer has already chosen the row, and this must not argue with it. The
 * animation itself is written inline, since the step count belongs to the prop
 * rather than to the stylesheet. */
@keyframes decor-frame-cycle {
  from { background-position-x: var(--decor-frame-from, 0px); }
  to { background-position-x: var(--decor-frame-to, 0px); }
}

/* While wall paint is armed the ground must not offer the drag to the browser
 * as a scroll: it is claimed as a pan mid-gesture and arrives back as
 * pointercancel, so the rectangle never starts. Objects already do this for
 * themselves, which is why dragging one always worked. */
.world.wall-painting { touch-action: none; }

/* Something a named NPC carries. Shares the clothing layers' box and their bob
 * so the weapon travels with the body -- given its own timing it would drift
 * out of the hands it was drawn into. Above the hair, because these sheets are
 * drawn to sit in front for every facing the client uses. */
.enemy > i.npc-weapon { position: absolute; left: 50%; top: 0; z-index: 4; display: block; width: 54px; height: 54px; margin: 0; background-size: 800% 800%; background-position: 0 0; background-repeat: no-repeat; image-rendering: pixelated; transform: translateX(-50%) scaleX(var(--npc-flip, 1)); animation: npc-layer-idle 1.15s steps(2) infinite; pointer-events: none; }

/* The travel network's marker. Twenty of these stand in the world as the only
 * visible sign of a Rest, so it pulses rather than sitting there -- the same
 * trick the lamp uses, at a slower beat because a stone is not a flame. */
.room-entity.object.decor-object .decor-sprite.waystone { animation: decor-waystone-pulse 3.4s ease-in-out infinite; }
@keyframes decor-waystone-pulse { 50% { filter: brightness(1.22) drop-shadow(0 0 9px #5fe3d0cc); } }

/* The level-up flourish. Covers the screen because it is the one moment the
 * game should interrupt you; dismisses itself because a reward you have to
 * close is a chore.
 *
 * Health, mana and stamina wear the colours of the meters they raise, so the
 * box that lit up tells you what improved before you read a word of it. */
.level-screen { position: absolute; inset: 0; z-index: 40; display: grid; place-items: center; padding: 18px; overflow: hidden; background: radial-gradient(circle at 50% 40%, #6b4aa8 0%, #3a2470 26%, #1b1140 52%, #0b0820 100%); animation: level-screen-in .32s ease-out both; }
.level-screen[hidden] { display: none; }
/* A pinwheel of light behind the number. Slow, so it reads as radiance and not
 * as something spinning. */
.level-screen::before { content: ""; position: absolute; left: 50%; top: 40%; width: 620px; height: 620px; background: conic-gradient(from 0deg, #ffd98a55 0deg, transparent 18deg, #ffd98a44 36deg, transparent 54deg, #ffb45c44 72deg, transparent 90deg, #ffd98a55 108deg, transparent 126deg, #ffd98a44 144deg, transparent 162deg, #ffb45c44 180deg, transparent 198deg, #ffd98a55 216deg, transparent 234deg, #ffd98a44 252deg, transparent 270deg, #ffb45c44 288deg, transparent 306deg, #ffd98a55 324deg, transparent 342deg); border-radius: 50%; transform: translate(-50%, -50%); mask-image: radial-gradient(circle, #000 0%, #0008 42%, transparent 70%); -webkit-mask-image: radial-gradient(circle, #000 0%, #0008 42%, transparent 70%); animation: level-rays 22s linear infinite; pointer-events: none; }
.level-burst { position: relative; z-index: 1; max-width: 310px; text-align: center; }
.level-eyebrow { margin: 0; color: #fff3c4; font: 900 13px/1 system-ui, sans-serif; letter-spacing: .44em; text-indent: .44em; text-shadow: 0 0 12px #ffcf6e, 0 2px 5px #2a1550; }
.level-number { margin: 0 0 14px; background: linear-gradient(180deg, #fffbe8 0%, #ffe08a 42%, #ffab3d 100%); -webkit-background-clip: text; background-clip: text; color: transparent; font: 900 88px/1 Georgia, serif; filter: drop-shadow(0 0 26px #ffc35eaa) drop-shadow(0 4px 0 #7a3d12); animation: level-number-pop .5s cubic-bezier(.2, 1.6, .4, 1) both; }
.level-gains { display: flex; justify-content: center; gap: 10px; margin: 0 0 15px; padding: 0; list-style: none; }
.level-gains li { --tint: #ffd98a; min-width: 80px; padding: 9px 7px; background: linear-gradient(180deg, #ffffff1f, #00000033), #1d1440ee; border: 1.5px solid var(--tint); border-radius: 7px; box-shadow: 0 0 16px color-mix(in srgb, var(--tint) 45%, transparent), inset 0 0 12px color-mix(in srgb, var(--tint) 16%, transparent); }
.level-gains li.health { --tint: #ff6b7a; }
.level-gains li.mana { --tint: #61d9eb; }
.level-gains li.stamina { --tint: #7ee081; }
.level-gains b { display: block; color: var(--tint); font: 900 23px/1.1 Georgia, serif; text-shadow: 0 0 14px var(--tint); }
.level-gains span { color: #efe6ff; font: 700 9px/1.5 system-ui, sans-serif; letter-spacing: .12em; }
.level-unlocks { margin: 0 0 13px; padding: 12px 14px; background: linear-gradient(180deg, #4a2f0aee, #2a1a06ee); border: 2px solid #ffc357; border-radius: 9px; box-shadow: 0 0 26px #ffb43c66, inset 0 0 16px #ffd98a22; }
.level-unlocked-label { margin: 0 0 8px; color: #ffe9a8; font: 900 10px/1 system-ui, sans-serif; letter-spacing: .3em; text-indent: .3em; text-shadow: 0 0 10px #ffc357; }
.level-unlocks ul { display: grid; gap: 7px; margin: 0; padding: 0; list-style: none; }
.level-unlocks li { display: flex; align-items: baseline; justify-content: center; gap: 9px; }
.level-unlocks b { color: #fff7d8; font: 900 18px/1.2 Georgia, serif; text-shadow: 0 0 16px #ffc357, 0 2px 0 #6b3d09; }
.level-unlocks span { padding: 1px 6px; color: #2a1a06; background: #ffc357; border-radius: 3px; font: 900 8px/1.5 system-ui, sans-serif; letter-spacing: .12em; }
.level-dismiss { margin: 0; color: #b9a7e6; font: 700 9px/1 system-ui, sans-serif; letter-spacing: .22em; }
@keyframes level-screen-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes level-number-pop { from { transform: scale(.4); opacity: 0; } to { transform: scale(1); opacity: 1; } }
@keyframes level-rays { to { transform: translate(-50%, -50%) rotate(360deg); } }

/* A waystone waking up for the first time: a gold flare on the stone itself,
 * long enough to notice and short enough not to be in the way. */
.room-entity.object.event-waystone .decor-sprite {
  animation: waystone-lit 1.6s ease-out;
}
@keyframes waystone-lit {
  0% { filter: brightness(2.6) drop-shadow(0 0 22px #ffe9a8); }
  40% { filter: brightness(1.7) drop-shadow(0 0 16px #7fe8d8); }
  100% { filter: none; }
}
.game-event-fx.waystone-lit { color: #ffe9a8; font-size: 22px; text-shadow: 0 0 12px #7fe8d8, 0 2px 4px #000; }

/* A builder's own size for one placed piece, set with objscale. Anchored at the
 * foot so a bigger piece grows upward from the tile it stands on. */
.room-entity.object.decor-object .decor-sprite {
  transform-origin: 50% 100%;
  transform: translateX(-50%) scaleX(var(--decor-flip, 1)) scale(var(--decor-size, 1));
}

/* The waystone keeper. Marked like a questmaster -- he sells travel, which is
 * a service nobody would guess at from a townsman standing by a fountain. */
.enemy.waystone-keeper .quest-marker { display: grid; color: #8ff3ff; text-shadow: 0 2px #10283f, 0 0 6px #66e5ff; }
.enemy.waystone-keeper .quest-marker::after { content: "\2726"; }
.enemy.waystone-keeper.targeted .quest-marker { left: calc(50% + 18px); }

/* --- Tapping the art, not the label --------------------------------------- */
/* A prop's hitbox was its label. The sprite is absolutely positioned, so it
 * contributes nothing to the container's size; the name and price underneath
 * do, and they stay in the layout even at opacity 0. A 32px lamp answered to
 * taps across 120px, which is why tapping one thing so often opened whatever
 * stood beside it. Pointers land on the art now. Clicks still reach the
 * container's own handler by bubbling, and :hover still lights the label,
 * because both travel up from the child. */
.room-entity.object.decor-object { pointer-events: none; }
.room-entity.object.decor-object > span,
.room-entity.object.decor-object > small { pointer-events: none; }
.room-entity.object.decor-object .decor-sprite { pointer-events: auto; }
.room-entity.object.decor-object > .cache-actions { pointer-events: auto; }
/* Small art still needs a thumb-sized target, so the sprite carries an
 * invisible pad. On the sprite rather than on the container, so it stays
 * centred on the art and cannot reach across a neighbour. */
.room-entity.object.decor-object .decor-sprite::after {
  content: ""; position: absolute; left: 50%; top: 50%;
  width: max(100%, 34px); height: max(100%, 34px);
  transform: translate(-50%, -50%);
}
/* Edit mode drags the whole marker, label included. */
.world.edit-mode .room-entity.object.decor-object { pointer-events: auto; }

/* --- Pets on the ground ---------------------------------------------------- */
/* They stood about twelve pixels up. A pet's name and species sit in the
 * layout beneath a 20px sprite, making the marker 48px tall, and the anchor is
 * a percentage of that height -- so the labels lifted the animal. Out of flow,
 * the way a humanoid's labels already are, and the box is the sprite again. */
.enemy.pet-npc > b {
  position: absolute; left: 50%; top: calc(100% + 2px);
  transform: translateX(-50%); white-space: nowrap; pointer-events: none;
}
.enemy.pet-npc > small {
  position: absolute; left: 50%; top: calc(100% + 15px);
  transform: translateX(-50%); white-space: nowrap; pointer-events: none;
}

/* A person's feet land 21px below the tile anchor; the -66% in `.enemy` was
 * measured against the 58px monster sprite and drops a 20px animal only 6.8px,
 * which left every pet standing a finger's width above the floorboards.
 * Anchored by the feet instead of by a fraction of the height, so it keeps
 * holding if a pet is ever drawn at a different size. */
.enemy.pet-npc { transform: translate(-50%, calc(-100% + 21px)); }

/* The toast used to sit at z-index 10 under a full-room nav at 24, so a
 * two-line message lost its first line behind the round button row and a
 * three-line message lost its middle. One above the nav. It is pointer-events:
 * none, so it cannot take a tap that was meant for a button, and being above
 * the menu as well means a reply to something done FROM a menu is finally
 * visible -- "You take a red beginner's remedy" used to be painted behind the
 * menu that asked for it. */
.message { z-index: 25; }

/* A nameplate hangs below its owner, and the bottom of the room is under the
 * controls, so anyone standing on the last two rows had their name and the
 * TALK line beneath it painted over by the buttons. Down there the plate goes
 * above the head instead -- the name on top, the prompt just over the hair. */
.enemy.label-above > b { top: auto; bottom: calc(100% + 12px); }
.enemy.label-above > small { top: auto; bottom: calc(100% + 1px); }

/* --- A menu you can see past ----------------------------------------------- */
/* Dressing is the one thing done to something standing behind the menu, so
 * those pages give the room back the top half of the screen. */
.quick-menu.peek { height: min(46%, 300px); }

/* --- Townsfolk who walk ----------------------------------------------------- */
/* The character base carries a six-frame walk in rows 4-7, one row per facing,
 * and nothing has ever played it: every NPC in the world slid between tiles in
 * its standing pose, column 0, for years.
 *
 * Safe where the old idle bob was not. That bob moved each layer on its own
 * transform, so a shirt drifted off the body it belonged to and it had to be
 * switched off for humanoids. Stepping the COLUMN moves body, clothes, hair and
 * weapon to the same cell on the same tick -- they cannot come apart.
 *
 * steps(6) across 0% to 85.714% lands on columns 0,1,2,3,4,5. Columns 6 and 7
 * are a run, dust and all, waiting for something that runs. */
@keyframes npc-walk { from { background-position-x: 0%; } to { background-position-x: 85.714%; } }
.enemy.humanoid.walking > i { animation: npc-walk .66s steps(6) infinite; }
