/* A game's theme/theme.css loads after this file, so it can restyle anything,
   but only the names below are a promise: renaming or removing one breaks
   every theme written against it, the way a wire change breaks clients.
   Everything else here may change in any release. Their colours, borders and
   corners are set in this file, not inline, so a theme's rule wins over them.
   styles.test.ts fails if one of them disappears from the client.

   Theme contract:
     login screen  .auth-layout .auth-card .auth-heading .auth-form
                   .engine-brand .engine-brand[data-engine-mark]
                   .password-toggle
     game screen   #ui #canvasFrame #sidePanel #map-name .player-controls
                   .zone-banner .low-health-vignette
                   #gameShell[data-layout="right|left|bottom|overlay"]
     player card   #hud #hud-portrait[data-sprite] #hud-name #hud-level
                   #hud-row[data-tone] #xp-bar-container
     side panel    .panel .action-button .chat-expand
     windows       .editor-window .module-panel .game-window
                   .window-titlebar .window-counter .window-content
                   .window-footer .inventory-panel .empty-state
     buttons       .button-primary .button-danger
     notices       .module-toasts .notice[data-tone] .notice-label
                   .notice-detail
     hotbar        .hotbar .hotbar-slot[data-state] .hotbar-tooltip
     player menu   .player-menu .player-menu-title
     icons         .icon
   and the variables in :root, except the --z-* stacking order. */

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

/* Latin subsets (Portuguese and Western European included), OFL, licences
   beside the files. A script outside the subset falls back to system-ui. */
@font-face {
  font-family: 'Chakra Petch';
  font-weight: 400;
  font-display: swap;
  src: url(fonts/chakra-petch-400.woff2) format('woff2');
}
@font-face {
  font-family: 'Chakra Petch';
  font-weight: 500;
  font-display: swap;
  src: url(fonts/chakra-petch-500.woff2) format('woff2');
}
@font-face {
  font-family: 'Chakra Petch';
  font-weight: 600;
  font-display: swap;
  src: url(fonts/chakra-petch-600.woff2) format('woff2');
}
@font-face {
  font-family: 'Chakra Petch';
  font-weight: 700 900;
  font-display: swap;
  src: url(fonts/chakra-petch-700.woff2) format('woff2');
}
@font-face {
  font-family: 'Pixelify Sans';
  font-weight: 400 700;
  font-display: swap;
  src: url(fonts/pixelify-sans.woff2) format('woff2');
}

:root {
  color-scheme: dark;
  /* primary fills a button under white text, so it stays deep enough to read
     on; accent is the bright highlight (a selected slot, a gold rule, a focus
     ring) that nothing is written on. */
  --color-primary: #2a78ae;
  --color-primary-hover: #3387c0;
  --color-accent: #4ea8de;
  --color-success: #5fbf6a;
  --color-danger: #cf4555;
  --color-danger-text: #ff8b86;
  --color-danger-dark: #b8383a;
  --color-danger-muted: #2e1d22;
  --color-info: #6fa8dc;
  --color-warning: #e0a336;
  --color-surface: #1e262f;
  --color-surface-muted: #141a20;
  --color-text: #e8eef4;
  --color-text-muted: #94a3b0;
  --color-border: #34424f;
  --color-overlay: rgba(6, 9, 12, 0.72);
  /* What the world darkens to behind a conversation, around whoever is
     talking; transparent leaves it lit. */
  --dialog-dim: rgba(0, 0, 0, 0.6);
  --color-selected: #1e3a4f;
  --color-app-bg: #0c1014;
  /* The names drawn over the world: a player's by access, an NPC's by
     behavior, and the tag a script sets under a name. Hex, so the canvas can
     weigh them: a light name is outlined in black, a dark one gets a light
     halo instead. */
  --color-name-user: #ffff00;
  --color-name-mod: #0000ff;
  --color-name-admin: #000000;
  --color-name-aggressive: #ef4444;
  --color-name-hostile: #f97316;
  --color-name-friendly: #22c55e;
  --color-name-shopkeeper: #3b82f6;
  --color-name-guard: #a855f7;
  --color-name-neutral: #e5e7eb;
  --color-name-tag: #ffd24d;
  /* A picture behind the login card, as background layers. */
  --auth-backdrop: none;
  /* ui is everything read at a glance; display names a thing (a panel, a
     window, the player); world is the game's own numbers and keys (levels,
     bars, hotkeys), drawn in the art's pixel style. */
  --font-ui: 'Chakra Petch', system-ui, sans-serif;
  --font-display: var(--font-ui);
  --font-world: 'Pixelify Sans', ui-monospace, monospace;
  --color-panel-bg: var(--color-surface-muted);
  --color-panel-border: var(--color-border);
  --color-panel-text: var(--color-text);
  --surface-control: #26303b;
  --edge-light: rgba(255, 255, 255, 0.06);
  --edge-strong: rgba(255, 255, 255, 0.12);
  --shadow-panel: 4px 4px 0 rgba(0, 0, 0, 0.45);
  --shadow-control: 0 1px 2px rgba(0, 0, 0, 0.2);
  --focus-ring: 0 0 0 2px var(--color-accent);
  /* The UI's two shapes: what a hand presses or a bar fills, and what holds
     them. theme.ts hands the same two to the React panels' inline styles. */
  --radius-control: 0px;
  --radius-panel: 0px;
  --z-canvas-base: 1;
  --z-canvas-mask: 2;
  --z-canvas-items: 3;
  --z-canvas-actors: 4;
  --z-canvas-fringe: 5;
  --z-canvas-text: 6;
  /* Editor markers (B, W, N, ...) sit above every world layer: a tile hidden
     under a fringe roof is exactly the one whose blocking you need to see. */
  --z-canvas-grid: 7;
  --z-game-overlay: 50;
  --z-hud: 100;
  --z-panel: 150;
  --z-editor: 500;
  --z-dialog: 2000;
  --z-toast: 10000;
}

body {
  font-family: var(--font-ui);
  color: var(--color-text);
  font-size: 13px;
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
  height: 100dvh;
  width: 100vw;
  margin: 0;
  background: var(--color-app-bg);
}

#ui {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  background: var(--color-app-bg);
}

#root {
  position: relative;
  width: 100%;
  height: 100%;
}

#errorMessage {
  color: red;
  margin-top: 15px;
  font-size: 0.9rem;
  display: block;
}

#gameShell {
  position: relative;
  container-type: size;
  width: 100%;
  flex: 1;
  min-height: 0;
  display: grid;
  /* The world is 1280x896, so a landscape screen runs out of height first:
     the player card and menu take a column beside it, out of width that the
     world could not have used, and nothing is drawn over the map. */
  grid-template-columns: minmax(0, 1fr) 320px;
  grid-template-rows: auto minmax(0, 1fr);
  grid-template-areas:
    'main bar'
    'main panel';
  gap: 6px 10px;
  padding: max(10px, env(safe-area-inset-top))
    max(10px, env(safe-area-inset-right)) max(10px, env(safe-area-inset-bottom))
    max(10px, env(safe-area-inset-left));
}

#gameMain {
  min-width: 0;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

#canvasFrame {
  position: relative;
  /* Named so the thumb pad inside can size itself against the world it covers
     rather than against the window, which is a different number on a phone in
     landscape than the world it letterboxes into. */
  container: gameframe / size;
  width: min(100cqw, calc(100cqh * 1280 / 896));
  aspect-ratio: 1280 / 896;
  border: 1px solid rgba(0, 0, 0, 0.6);
  border-radius: var(--radius-panel);
  overflow: hidden;
  background: #31363c;
  box-shadow:
    0 0 0 1px var(--edge-light),
    0 24px 60px rgba(0, 0, 0, 0.5);
}

#canvasStack {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

#canvasStack > canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  background: transparent;
  /* The frame is almost never 1:1 with the 1280x896 buffer - 1.16x on a 1080p
     screen, 0.78x on a laptop - and the browser's default filtering turns
     pixel art into a smear at both. Nearest-neighbour keeps the edges the
     artist drew; compared side by side at both scales it is the better of the
     two, which is the only reason it is here. */
  image-rendering: pixelated;
}

#foregroundCanvas,
#itemCanvas,
#gridCanvas,
#playerCanvas,
#fringeCanvas,
#textCanvas {
  width: 100%;
  height: 100%;
}

#foregroundCanvas {
  z-index: var(--z-canvas-base);
  background-color: #666;
}
#maskCanvas {
  z-index: var(--z-canvas-mask);
  pointer-events: none;
  background: transparent;
}
#itemCanvas {
  z-index: var(--z-canvas-items);
  pointer-events: none;
  background: transparent;
}
#gridCanvas {
  z-index: var(--z-canvas-grid);
  pointer-events: none;
  background: transparent;
  position: absolute;
  display: none;
}
#playerCanvas {
  z-index: var(--z-canvas-actors);
  pointer-events: none;
  background: transparent;
}
#fringeCanvas {
  z-index: var(--z-canvas-fringe);
  pointer-events: none;
  background: transparent;
}
/* Drawn at the screen's resolution (graphics/canvas.ts); what is left of the
   scale is filtered, which a line of text survives and pixel art does not. */
#canvasStack > #textCanvas {
  z-index: var(--z-canvas-text);
  pointer-events: none;
  background: transparent;
  image-rendering: auto;
}

#npcSpritePreview,
#itemSpritePreview {
  display: none;
  width: 64px;
  height: 64px;
  z-index: var(--z-game-overlay);
}

#tilesetContainer {
  position: fixed;
  top: 0;
  left: 0;
  z-index: var(--z-editor);
  /* 512 is the tileset's own width, not a layout choice - on a phone it hung
     half off the screen, and a tileset that is scrolled inside its box beats
     one that is scrolled by dragging the whole editor around. */
  width: min(512px, calc(100vw - 16px));
  height: min(100dvh, 1200px);
  padding: 0;
  box-sizing: border-box;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 10px;
  cursor: default;
}

/* One framed block of the side column: the player card, the menu, the chat.
   A theme frames every one of them with this selector. */
.panel {
  padding: 8px;
  color: var(--color-panel-text);
  background: var(--color-panel-bg);
  border: 2px solid var(--color-panel-border);
  border-radius: var(--radius-panel);
}

/* The portrait beside the stats; the status module's badges wrap onto a line
   of their own underneath. */
#hud {
  flex: 1;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 6px 10px;
  min-width: 0;
  font-size: 13px;
}

/* Every bar is a row of the same three columns (label, bar, amount), so the
   bars line up whatever the numbers. */
.hud-stats {
  flex: 1;
  min-width: 0;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 5px 6px;
}

#hud-who {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  min-width: 0;
}

#hud-row,
.hud-resource-row,
#xp-bar-container {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  align-items: center;
}

/* data-sprite is the character's sprite id, so a theme can put a picture in
   place of the sprite for one class, or for everyone:
     #hud-portrait[data-sprite="12"] { background-image: url(portraits/mage.png); }
     #hud-portrait[data-sprite="12"] > canvas { visibility: hidden; } */
#hud-portrait {
  flex: none;
  width: 52px;
  height: 52px;
  overflow: hidden;
  border: 2px solid var(--color-accent);
  border-radius: var(--radius-control);
  background: var(--color-surface-muted) center / cover no-repeat;
}

#hud-name {
  font-family: var(--font-display);
  font-size: 16px;
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A badge, so "Level 12" cannot fold onto two lines and push the row taller. */
#hud-level {
  flex: none;
  font-family: var(--font-world);
  white-space: nowrap;
  padding: 0 6px;
  border: 2px solid var(--color-accent);
  border-radius: var(--radius-control);
  background: var(--color-surface-muted);
  color: var(--color-accent);
  font-size: 12px;
  line-height: 16px;
}

#hud-hp-label,
.hud-resource-label,
#hud-xp-label,
#hud-hp-values,
.hud-resource-values,
#xp-bar-text {
  font-family: var(--font-world);
  font-size: 12px;
  line-height: 1;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
#hud-hp-label,
.hud-resource-label,
#hud-xp-label {
  color: var(--color-text-muted);
}
#hud-hp-values,
.hud-resource-values,
#xp-bar-text {
  text-align: right;
}

#health-bar-container,
.mana-track,
.xp-track {
  position: relative;
  min-width: 0;
  height: 14px;
  overflow: hidden;
  border: 2px solid var(--color-border);
  border-radius: var(--radius-control);
  background: var(--color-surface-muted);
}
#health-bar,
.mana-fill,
#xp-bar {
  height: 100%;
  transition: width 0.3s ease;
}
#health-bar {
  background: var(--color-success);
}
#hud-row[data-tone='warning'] #health-bar {
  background: var(--color-warning);
}
#hud-row[data-tone='danger'] #health-bar {
  background: var(--color-danger);
}
/* Stepped rather than eased, a blink more than a glow, so it reads as an
   alarm in the corner of the eye while the player watches the fight. */
@media (prefers-reduced-motion: no-preference) {
  #hud-row[data-tone='danger'] #health-bar {
    animation: hp-pulse 0.9s steps(2, end) infinite;
  }
}
@keyframes hp-pulse {
  50% {
    filter: brightness(1.7);
  }
}
.mana-fill {
  background: var(--color-info);
}

.xp-track {
  height: 8px;
  border-width: 1px;
}
#xp-bar {
  width: 0%;
  background: var(--color-warning);
}

#connection-status {
  width: auto;
  max-width: min(500px, calc(100vw - 32px));
  color: white;
  text-align: center;
}

/* A column of panels on the app's own background, each framed by .panel. */
#sidePanel {
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
  overflow-y: auto;
  color: var(--color-panel-text);
}

#actionPanel {
  flex: none;
  padding: 6px;
}

/* Three across: the longest labels ("Achievements", "Online players") fit a
   tile a third of the column wide, where four across they would not. */
.action-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 4px;
}

.action-button {
  position: relative;
  min-height: 60px;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding: 10px 4px 6px;
  color: var(--color-panel-text);
  background: var(--color-surface);
  border: 2px solid var(--color-panel-border);
  border-radius: var(--radius-control);
  cursor: pointer;
  font-size: 12px;
  line-height: 1.1;
  font-weight: 600;
  text-align: center;
  transition:
    background-color 0.12s ease,
    border-color 0.12s ease,
    transform 0.06s ease;
}

.action-button:hover:not(:disabled) {
  background: var(--color-surface-muted);
  border-color: var(--color-accent);
}

/* Pressed, and reachable by keyboard: the panel is the one thing every player
   uses every session, so it answers to both. */
.action-button:active:not(:disabled) {
  transform: translateY(1px);
}

.action-button:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

.action-button:disabled {
  cursor: wait;
  opacity: 0.65;
}

.action-button .icon {
  width: 24px;
  height: 24px;
}

/* A module tile without a picture wears its label's first letter. */
.action-monogram {
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  border: 2px solid currentColor;
  font-family: var(--font-display);
  font-size: 13px;
  font-weight: 700;
}

.action-label {
  color: var(--color-text-muted);
}
.action-button:hover .action-label {
  color: var(--color-panel-text);
}

.action-key {
  font-family: var(--font-world);
  flex: none;
  padding: 0 4px;
  color: var(--color-text-muted);
  background: var(--color-surface-muted);
  border-radius: var(--radius-control);
  font-size: 11px;
  line-height: 14px;
}
.action-grid .action-key {
  position: absolute;
  top: 0;
  left: 0;
}

#chatContainer {
  flex: 1;
  min-height: 210px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.chat-tabs {
  display: flex;
  gap: 2px;
}
.chat-tab {
  padding: 3px 8px;
  color: var(--color-text-muted);
  background: var(--color-surface-muted);
  border: 2px solid var(--color-border);
  border-radius: var(--radius-control);
  font-family: var(--font-ui);
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  cursor: pointer;
}
.chat-tab:hover {
  color: var(--color-panel-text);
}
.chat-tab[aria-selected='true'] {
  color: var(--color-panel-text);
  background: var(--color-surface);
  border-color: var(--color-accent);
  font-weight: 700;
}

.chat-box {
  flex: 1;
  min-height: 0;
  min-width: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 4px 6px;
  color: var(--color-text);
  background: var(--color-surface-muted);
  border: 2px solid var(--color-border);
  border-radius: var(--radius-control);
  font-size: 13px;
  line-height: 1.45;
}

/* A line someone said reads in the text colour with their name in the
   channel's; a line nobody signed (a command's reply, a death) is the
   channel's colour throughout. */
#chatBox > div,
.chat-peek > span {
  margin-bottom: 2px;
  word-break: break-word;
}
.chat-map {
  --chat-tone: var(--color-info);
}
.chat-global {
  --chat-tone: var(--color-success);
}
.chat-player {
  --chat-tone: var(--color-warning);
}
.chat-death {
  --chat-tone: var(--color-danger);
}
.chat-who {
  font-weight: 700;
}
.chat-who,
.chat-box > .chat-notice {
  color: var(--chat-tone);
}

/* Says the panel is working rather than leaving a void the eye reads as a
   loading failure. */
.chat-empty {
  margin: 0;
  color: var(--color-text-muted);
  font-size: 12.5px;
  font-style: italic;
}
.chat-box::-webkit-scrollbar {
  width: 6px;
}
.chat-box::-webkit-scrollbar-track {
  background: transparent;
}
.chat-box::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.16);
  border-radius: var(--radius-control);
}
.chat-box::-webkit-scrollbar-thumb:hover {
  background: rgba(255, 255, 255, 0.28);
}

#chatInputContainer {
  display: flex;
  gap: 4px;
}
#chatInput,
#chatScope {
  padding: 6px 8px;
  font-size: 13px;
  font-family: inherit;
  border-radius: var(--radius-control);
  border: 2px solid var(--color-panel-border);
  background: var(--color-surface-muted);
  color: var(--color-panel-text);
  transition:
    border-color 0.12s ease,
    box-shadow 0.12s ease;
}

#chatInput {
  flex: 1;
  min-width: 0;
}

#chatInput::placeholder {
  color: var(--color-text-muted);
}

#chatInput:focus,
#chatScope:focus {
  outline: none;
  border-color: var(--color-accent);
}

#chatScope {
  cursor: pointer;
}

.editor-menu {
  background: var(--color-surface);
  border: 2px solid var(--color-primary);
  border-radius: var(--radius-panel);
  box-shadow: 0 5px 25px rgba(0, 0, 0, 0.3);
  padding: 25px 20px;
  width: 400px;
  max-width: 95vw;
  max-height: 90vh;
  overflow-y: auto;
  display: none;
  flex-direction: column;
  gap: 20px;
  position: absolute;
  z-index: var(--z-panel);
  color: var(--color-text);
}

#modContainer {
  top: 15px;
  right: 15px;
}

#contentEditorContainer {
  top: 15px;
  right: 15px;
}

/* The keyboard a phone does not have, drawn over the bottom of the frame.
   Hidden unless the primary pointer is a finger: a mouse already has the
   arrow keys, and a pad sitting over the world would only cover it.
   Every button is a hold, like the key it stands for. */
#touchControls {
  display: none;
}

@media (pointer: coarse) {
  #touchControls {
    --touch-button: clamp(40px, 10dvh, 46px);
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    padding: 8px;
    z-index: var(--z-hud);
    /* Only the buttons take the touch; the gap between them is still world. */
    pointer-events: none;
  }

  .touch-dpad {
    pointer-events: auto;
    touch-action: none;
    user-select: none;
    display: grid;
    grid-template-columns: repeat(3, var(--touch-button));
    grid-template-rows: repeat(3, var(--touch-button));
    gap: 4px;
  }

  .touch-up {
    grid-area: 1 / 2;
  }
  .touch-left {
    grid-area: 2 / 1;
  }
  .touch-right {
    grid-area: 2 / 3;
  }
  .touch-down {
    grid-area: 3 / 2;
  }

  .touch-actions {
    display: grid;
    grid-template-columns: repeat(2, var(--touch-button));
    gap: 8px;
  }

  .touch-button {
    pointer-events: auto;
    /* A thumb holding a direction must not scroll the page, pull to refresh
       or zoom on the second tap. */
    touch-action: none;
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
    width: var(--touch-button);
    height: var(--touch-button);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    font-size: 18px;
    line-height: 1;
    color: var(--color-panel-text);
    background: color-mix(in srgb, var(--color-surface) 50%, transparent);
    border: 1px solid var(--edge-strong);
    border-radius: 12px;
    box-shadow: var(--shadow-control);
    cursor: pointer;
  }

  .touch-button:active,
  .touch-button[aria-pressed='true'] {
    background: color-mix(in srgb, var(--color-primary) 40%, transparent);
  }

  .touch-attack {
    background: color-mix(in srgb, var(--color-primary) 28%, transparent);
    border-color: color-mix(in srgb, var(--color-primary) 50%, transparent);
  }

  /* The button the prompt beside the player is about: a phone has no key for
     the prompt to name. */
  .touch-button[data-ready] {
    color: var(--color-accent);
    border: 2px solid var(--color-accent);
    box-shadow: 0 0 10px color-mix(in srgb, var(--color-accent) 55%, transparent);
  }
}

/* Low-specificity defaults also style controls contributed by game modules. */
:where(button, input, select, textarea) {
  font: inherit;
  color: var(--color-text);
  accent-color: var(--color-primary);
}
:where(button) {
  padding: 7px 10px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  background: var(--surface-control);
  cursor: pointer;
}
:where(
  input:not([type='checkbox'], [type='radio'], [type='range']),
  select,
  textarea
) {
  min-width: 0;
  max-width: 100%;
  padding: 8px 10px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  background: var(--color-surface-muted);
}
:where(textarea) {
  resize: vertical;
}
:where(input, textarea)::placeholder {
  color: var(--color-text-muted);
  opacity: 1;
}
:where(button):hover:not(:disabled) {
  filter: brightness(1.12);
}
/* The action a window leads with, and one that takes something away. Core and
   module windows mark theirs with the same class, so a theme restyles both. */
.button-primary,
.button-danger {
  border-color: transparent;
  color: #fff;
  font-weight: 600;
}
.button-primary {
  background: var(--color-primary);
}
.button-danger {
  background: var(--color-danger);
}
:where(button, input, select, textarea):disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
:where(button, input, select, textarea, a, [tabindex]):focus-visible {
  outline: 2px solid var(--color-accent) !important;
  outline-offset: 2px;
}
:where(*) {
  scrollbar-width: thin;
  scrollbar-color: var(--color-border) transparent;
}
:where(select) {
  width: 100%;
}
#chatScope {
  width: auto;
}
:where(a) {
  color: var(--color-info);
}

/* UI icons (Icon.tsx) are masks filled with the text colour, so they follow
   the palette. A theme swaps one with --icon-<name>, where only the picture's
   alpha counts, and writes the address from the root,
   url(/theme/icons/hand.svg): a relative url inside a variable resolves where
   the variable is used, here, not in theme.css. To show them as pictures in
   their own colours a theme adds
   `.icon { mask: none; background: var(--icon) center / contain no-repeat; }`. */
:root {
  --icon-up: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='black' stroke='black' stroke-width='2' stroke-linejoin='round'><path d='M12 5 20 18H4z'/></svg>");
  --icon-down: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='black' stroke='black' stroke-width='2' stroke-linejoin='round'><path d='M12 19 4 6h16z'/></svg>");
  --icon-left: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='black' stroke='black' stroke-width='2' stroke-linejoin='round'><path d='M5 12 18 4v16z'/></svg>");
  --icon-right: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='black' stroke='black' stroke-width='2' stroke-linejoin='round'><path d='M19 12 6 20V4z'/></svg>");
  --icon-attack: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'><path d='M4 4l10 10m-2.5 2.5 5-5M14 14l5 5M20 4 10 14m-2.5-2.5 5 5M10 14l-5 5'/></svg>");
  --icon-interact: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M8 13V5a1.5 1.5 0 0 1 3 0v6M11 11V3.5a1.5 1.5 0 0 1 3 0V11M14 11V5a1.5 1.5 0 0 1 3 0v6'/><path d='M17 11V8a1.5 1.5 0 0 1 3 0v6a7 7 0 0 1-7 7h-1a6 6 0 0 1-5-2.7l-2.7-3.6a1.5 1.5 0 0 1 2.4-1.8L8 14.5V13'/></svg>");
  --icon-pickup: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M12 4v11m-5-5 5 5 5-5M5 20h14'/></svg>");
  --icon-run: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='m6 7 5 5-5 5m7-10 5 5-5 5'/></svg>");
  --icon-settings: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2'><circle cx='12' cy='12' r='6'/><circle cx='12' cy='12' r='2'/><path d='M12 2v4m0 12v4M2 12h4m12 0h4M5 5l3 3m8 8 3 3M5 19l3-3m8-8 3-3'/></svg>");
  --icon-menu: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'><path d='M4 7h16M4 12h16M4 17h16'/></svg>");
  --icon-close: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'><path d='m6 6 12 12M18 6 6 18'/></svg>");
  --icon-eye: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'><path d='M2 12s3.6-6.5 10-6.5S22 12 22 12s-3.6 6.5-10 6.5S2 12 2 12z'/><circle cx='12' cy='12' r='3'/></svg>");
  --icon-eye-off: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round' stroke-linecap='round'><path d='M2 12s3.6-6.5 10-6.5S22 12 22 12s-3.6 6.5-10 6.5S2 12 2 12z'/><circle cx='12' cy='12' r='3'/><path d='M4 3l16 18'/></svg>");
  /* The menu's panels: filled silhouettes, which hold up at the size of a
     tile better than outlines do. */
  --icon-bag: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill-rule='evenodd'><path d='M9 2.5h6l-1.6 3.2h-2.8zM8.2 7h7.6c2.9 2 5.2 5.8 5.2 9.3 0 3.3-2.6 4.7-9 4.7s-9-1.4-9-4.7C3 12.8 5.3 9 8.2 7z'/></svg>");
  --icon-stats: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill-rule='evenodd'><path d='M3 14h4.5v7H3zM9.75 9h4.5v12h-4.5zM16.5 3h4.5v18h-4.5z'/></svg>");
  --icon-players: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill-rule='evenodd'><path d='M9 4a3.6 3.6 0 1 1 0 7.2A3.6 3.6 0 0 1 9 4zM1.8 20.5c0-4.2 3.2-7.4 7.2-7.4s7.2 3.2 7.2 7.4zM16.5 3.7a2.8 2.8 0 1 1 0 5.6 2.8 2.8 0 0 1 0-5.6zM16.9 12.6c3 .5 5.3 3.3 5.3 6.8V20.5h-4.6c0-3.1-1-5.8-3-7.6.7-.2 1.5-.3 2.3-.3z'/></svg>");
  --icon-quest: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill-rule='evenodd'><path d='M7 2.5h12a2.5 2.5 0 0 1 0 5h-1v11a3 3 0 0 1-3 3H5a2.5 2.5 0 0 1 0-5h1v-11a3 3 0 0 1 1-3zM9 9.5v1.6h6V9.5zM9 13v1.6h6V13z'/></svg>");
  --icon-friends: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill-rule='evenodd'><path d='M12 21 3.4 12.6A5.2 5.2 0 0 1 12 5.9a5.2 5.2 0 0 1 8.6 6.7z'/></svg>");
  --icon-trade: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill-rule='evenodd'><path d='M3 6.5h12V3.5l6 4.5-6 4.5v-3H3zM21 17.5H9v3l-6-4.5 6-4.5v3h12z'/></svg>");
  --icon-guild: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill-rule='evenodd'><path d='M12 2 20.5 5v6.2c0 5-3.6 9-8.5 10.8C7.1 20.2 3.5 16.2 3.5 11.2V5zM12 7.4 10.6 10.4 7.4 10.6 9.9 12.7 9 15.9 12 14.1 15 15.9 14.1 12.7 16.6 10.6 13.4 10.4z'/></svg>");
  --icon-trophy: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill-rule='evenodd'><path d='M6.5 2.5h11v3h4v2.3a5.5 5.5 0 0 1-4.8 5.4A5.3 5.3 0 0 1 13.2 16v2.2h3.3v3.3h-9v-3.3h3.3V16a5.3 5.3 0 0 1-3.5-2.8A5.5 5.5 0 0 1 2.5 7.8V5.5h4zM5 8a3 3 0 0 0 1.7 2.7C6.6 9.9 6.5 9 6.5 8zM19 8h-1.5c0 1-.1 1.9-.2 2.7A3 3 0 0 0 19 8z'/></svg>");
  --icon-mail: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill-rule='evenodd'><path d='M2 4.5h20v15H2zM4 7v2.2l8 5.4 8-5.4V7l-8 5.3z'/></svg>");
  --icon-ranking: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill-rule='evenodd'><path d='M9 11h6v10.5H9zM2 14h6v7.5H2zM16 16.5h6v5h-6zM12.0 2.2 13.1 5.1 16.1 5.2 13.7 7.1 14.5 10 12.0 8.4 9.5 10 10.3 7.1 7.9 5.2 10.9 5.1z'/></svg>");
  --icon-spellbook: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill-rule='evenodd'><path d='M4 2.5h13.5a2.5 2.5 0 0 1 2.5 2.5v16.5H7a3 3 0 0 1-3-3zM6.8 18.4c0 .6.5 1 1.1 1H18v-2.1H7.9c-.6 0-1.1.5-1.1 1.1zM12 5.6l1.3 3.2 3.2 1.3-3.2 1.3L12 14.6l-1.3-3.2-3.2-1.3 3.2-1.3z'/></svg>");
  --icon-party: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill-rule='evenodd'><path d='M4.5 2h2.2v20H4.5zM8 3h12l-3 4.2 3 4.3H8z'/></svg>");
  --icon-auction: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill-rule='evenodd'><path d='M14.3 6.2 10.8 2.7 3.7 9.8 7.2 13.3zM19.3 20.1 21 18.4 12.4 9.8 10.7 11.5zM2.5 19h10v3h-10z'/></svg>");
}
.icon {
  display: inline-block;
  flex: none;
  width: 20px;
  height: 20px;
  background: currentColor;
  mask: var(--icon) center / contain no-repeat;
}
/* Centred by the box rather than by the line, which sits an icon a pixel or
   two low. :where() so a rule that hides one of these, .panel-toggle's, still
   wins. */
:where(.window-close, .player-controls button) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.player-controls .icon {
  width: 18px;
  height: 18px;
}
.window-close .icon {
  width: 14px;
  height: 14px;
}

.engine-brand {
  font-family: var(--font-display);
  display: flex;
  align-items: center;
  gap: 9px;
  /* The logo's height, held while the name is on its way. */
  min-height: 24px;
  font-size: 12px;
  font-weight: 750;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.engine-brand > img {
  display: block;
  width: auto;
  height: 24px;
}
/* A game with no logo of its own shows the engine's mark as the emblem over
   its name, not as an icon beside it. */
.engine-brand[data-engine-mark] {
  flex-direction: column;
  align-items: flex-start;
  gap: 14px;
  min-height: 72px;
}
.engine-brand[data-engine-mark] > img {
  height: 56px;
}
/* What the page shows in place of the client: until the server behind it
   answers, or after a render threw (main.tsx, ErrorBoundary). */
.server-wait {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 13px;
  color: var(--color-text-muted);
}
.engine-credit {
  margin-top: 20px;
  font-size: 11px;
  color: var(--color-text-muted);
  text-align: center;
}
.engine-credit a {
  color: inherit;
}
#gameMain {
  grid-area: main;
  position: relative;
  flex-direction: column;
  border-radius: var(--radius-panel);
  background: radial-gradient(
    ellipse at center,
    var(--color-surface),
    var(--color-app-bg) 80%
  );
}
.viewport-stage {
  container-type: size;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
  width: 100%;
  flex: 1;
  overflow: hidden;
}
#canvasFrame {
  box-shadow: 0 2px 24px #0008;
  border-radius: var(--radius-panel);
}

.window-overlay {
  position: fixed;
  inset: 0;
  background: var(--color-overlay);
  z-index: 1000;
}
.editor-window {
  position: fixed;
  display: flex;
  flex-direction: column;
  max-width: calc(100vw - 24px);
  max-height: 85dvh;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-panel);
  background: var(--color-surface);
  color: var(--color-text);
  box-shadow: var(--shadow-panel);
  z-index: 1001;
  overflow: hidden;
}
.window-titlebar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  min-height: 42px;
  padding: 6px 10px 6px 16px;
  border-bottom: 1px solid var(--color-border);
  background: var(--color-surface-muted);
  cursor: move;
  user-select: none;
  touch-action: none;
  flex-shrink: 0;
}
.window-titlebar h2 {
  font-family: var(--font-display);
  margin: 0;
  font-size: 13px;
  font-weight: 600;
}
.window-close {
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--color-text-muted);
  font-size: 20px;
}
.window-close:hover {
  background: var(--color-danger-muted);
  color: var(--color-text);
}
.window-content {
  overflow: auto;
  min-height: 0;
  padding: 16px;
  overscroll-behavior: contain;
}
/* A module window floats over the world without dimming it, above any open
   Modal, and stacks whatever the module hands setBody(). */
.module-panel {
  min-width: 220px;
  z-index: 1002;
}
.module-panel > .window-content {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
/* A window the player uses (size="narrow|regular|wide" on Modal, or a width
   name on engine.ui.Window): the same bar on every one, its title in the
   accent, and a body set to be read at 14px. */
.game-window {
  font-size: 14px;
}
.game-window .window-titlebar {
  min-height: 38px;
  padding: 4px 4px 4px 12px;
  gap: 8px;
  border-bottom-width: 2px;
}
.game-window .window-titlebar h2 {
  flex: 1;
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--color-accent);
}
.window-counter {
  font-family: var(--font-ui);
  font-size: 15px;
  font-variant-numeric: tabular-nums;
}
.game-window .window-close {
  width: 30px;
  height: 30px;
  background: var(--color-surface);
  border: 2px solid var(--color-border);
}
.game-window .window-close:hover,
.game-window .window-close:focus-visible {
  border-color: var(--color-accent);
  background: var(--color-surface);
}
.game-window .window-content {
  padding: 10px;
  gap: 10px;
}

.empty-state {
  display: grid;
  justify-items: center;
  gap: 2px;
  padding: 14px 10px;
  text-align: center;
  border: 2px dashed var(--color-border);
  background: var(--color-surface-muted);
}
.empty-state .icon {
  width: 28px;
  height: 28px;
  margin-bottom: 4px;
  color: var(--color-text-muted);
}
.empty-state-title {
  font-size: 14px;
  font-weight: 700;
}
.empty-state-hint {
  font-size: 12px;
  color: var(--color-text-muted);
}
/* Under the body and out of its scroll, so what the window does stays in
   reach however long the list above it grows. */
.window-footer {
  flex-shrink: 0;
  padding: 8px 10px;
  border-top: 2px solid var(--color-border);
  background: var(--color-surface-muted);
}
.window-footer .empty-state {
  padding: 8px;
  border: 0;
}
.window-actions {
  display: flex;
  gap: 8px;
}
.window-actions .button-primary {
  flex: 1;
}
.window-error {
  color: var(--color-danger-text);
  font-size: 13px;
}
/* :where keeps these under .button-primary and whatever a module gives its
   own buttons. */
:where(.game-window .window-content, .window-footer) :where(button) {
  min-height: 32px;
  padding: 4px 12px;
  border-width: 2px;
  font-weight: 700;
}
.step-button {
  width: 32px;
  padding: 0;
  font-size: 18px;
  line-height: 1;
}
/* What a window's body is written in, the engine's and a module's alike: a
   heading over a section, a small line that labels or explains, a list of
   rows with what can be done to each at its end, and a side column. */
.window-heading {
  font-family: var(--font-display);
  font-size: 14px;
  letter-spacing: 0.04em;
  color: var(--color-accent);
}
.window-label {
  font-size: 12px;
  color: var(--color-text-muted);
}
.window-aside {
  padding-left: 10px;
  border-left: 2px solid var(--color-border);
}
/* A list beside what is picked in it. */
.window-split {
  display: flex;
  gap: 10px;
}
.window-split > :first-child {
  flex: 0 0 200px;
}
.window-split > .window-aside {
  flex: 1;
  min-width: 0;
}
/* Too narrow for both: what was beside goes under, ruled off above. */
@media (max-width: 480px) {
  .window-split {
    flex-direction: column;
  }
  .window-split > :first-child {
    flex: none;
  }
  .window-aside {
    min-width: 100%;
    padding: 10px 0 0;
    border-left: 0;
    border-top: 2px solid var(--color-border);
  }
}
.list {
  display: grid;
  gap: 3px;
  list-style: none;
}
.list-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  min-height: 40px;
  padding: 4px 6px;
  border: 2px solid var(--color-border);
  background: var(--color-surface-muted);
}
.list-row-actions {
  display: flex;
  gap: 4px;
  margin-left: auto;
}
.list-row-actions > button {
  min-height: 28px;
  padding: 2px 8px;
}
/* A row's name over the line that says more about it. */
.list-row-text {
  display: grid;
  flex: 1;
  gap: 1px;
  min-width: 0;
}
.list-row-text small {
  font-size: 12px;
  font-weight: 400;
  color: var(--color-text-muted);
}
.list-row[data-self] {
  border-color: var(--color-accent);
}
/* There, and not to be acted on: a party member out of sight, an achievement
   still locked. */
.list-row[data-dim] {
  opacity: 0.7;
}
/* Who a row is: an initial or an icon in a box, with a corner that says
   whether they are in the game when the list knows (data-online). */
.avatar {
  position: relative;
  display: grid;
  flex: 0 0 auto;
  place-items: center;
  width: 32px;
  height: 32px;
  border: 2px solid var(--color-border);
  background: var(--color-surface);
  font: 700 16px/1 var(--font-display);
  text-transform: uppercase;
}
.avatar[data-online]::after {
  content: '';
  position: absolute;
  right: -4px;
  bottom: -4px;
  width: 10px;
  height: 10px;
  border: 2px solid var(--color-app-bg);
  background: var(--color-text-muted);
}
.avatar[data-online='true']::after {
  background: var(--color-success);
}
.avatar[data-earned] {
  border-color: var(--color-accent);
  color: var(--color-accent);
}
/* How far along, as a thin bar: its fill is the child's width. */
.meter {
  display: block;
  height: 6px;
  border: 1px solid var(--color-border);
  background: var(--color-app-bg);
}
.meter > span {
  display: block;
  height: 100%;
  background: var(--color-accent);
}
.meter[data-tone='health'] > span {
  background: var(--color-success);
}
.meter[data-tone='low'] > span {
  background: var(--color-danger);
}
/* A field with the button that sends it. */
.window-field {
  display: flex;
  gap: 6px;
}
.window-field > input {
  flex: 1;
  min-width: 0;
}
.tag {
  margin-left: 6px;
  padding: 0 4px;
  font-size: 12px;
  color: var(--color-app-bg);
  background: var(--color-accent);
}
/* First, second and third on a board. */
.medal {
  display: inline-grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border: 2px solid #1a1206;
  color: #1a1206;
  background: #e8c24a;
}
.medal[data-place='2'] {
  background: #c9d1d8;
}
.medal[data-place='3'] {
  background: #c98a4a;
}
.stack-count {
  font-family: var(--font-world);
  font-size: 16px;
  font-variant-numeric: tabular-nums;
}
/* One of a few to choose between, where a list of tabs would be too much. */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}
.chip {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  min-height: 0;
  padding: 4px 8px;
  background: var(--color-surface-muted);
}
.chip > small {
  font-size: 12px;
  font-weight: 400;
  color: var(--color-text-muted);
}
.chip[aria-checked='true'],
.chip[aria-current='true'] {
  border-color: var(--color-accent);
  background: var(--color-surface);
}
.link-button {
  min-height: 0;
  padding: 0;
  border: 0;
  background: none;
  font-size: 12px;
  font-weight: 400;
  color: var(--color-text-muted);
  text-decoration: underline;
}
/* The slot the next picked item goes to. */
.offer-slot {
  padding: 6px;
  border: 2px solid transparent;
}
.offer-slot[data-active] {
  border-color: var(--color-accent);
  background: var(--color-surface-muted);
}
/* A window's sections, as tabs standing on the rule over what they show. */
.window-tabs {
  display: flex;
  gap: 4px;
  border-bottom: 2px solid var(--color-border);
}
.window-tabs > button {
  min-height: 0;
  margin-bottom: -2px;
  padding: 6px 14px;
  border-color: transparent;
  border-radius: 0;
  background: transparent;
  color: var(--color-text-muted);
}
.window-tabs > [aria-selected='true'] {
  border-color: var(--color-border) var(--color-border) var(--color-surface);
  background: var(--color-surface);
  color: var(--color-text);
}
.stepper {
  display: inline-flex;
  align-items: center;
  gap: 2px;
}
.stepper > button {
  min-height: 26px;
  padding: 0 6px;
  font-size: 12px;
}
.stepper > .step-button {
  width: 26px;
  font-size: 16px;
}
.stepper > output {
  min-width: 34px;
  text-align: center;
  font-family: var(--font-world);
  font-size: 16px;
  font-variant-numeric: tabular-nums;
}

/* An item in a box: the bag, what is worn, an offer. The frame is drawn over
   the picture, so the box is the 64px its art is and five of them fit a
   window whatever frame a theme puts round it. */
.item-slot {
  position: relative;
  display: grid;
  place-items: center;
  width: 64px;
  height: 64px;
  min-height: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: var(--color-surface-muted);
}
.item-slot::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 2px solid var(--color-border);
  pointer-events: none;
}
button.item-slot:hover::after {
  border-color: color-mix(
    in srgb,
    var(--color-accent) 60%,
    var(--color-border)
  );
}
.item-slot[aria-pressed='true'] {
  background: var(--color-surface);
}
.item-slot[aria-pressed='true']::after {
  border-color: var(--color-accent);
  box-shadow: inset 0 0 0 2px var(--color-app-bg);
}
.item-slot[data-empty] {
  background: color-mix(in srgb, var(--color-surface-muted) 70%, transparent);
}
.item-slot[data-empty]::after {
  border-style: dashed;
}
.item-slot-quantity {
  position: absolute;
  right: 4px;
  bottom: 2px;
  font-family: var(--font-world);
  font-size: 16px;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: #fff;
  text-shadow:
    1px 0 #000,
    -1px 0 #000,
    0 1px #000,
    0 -1px #000;
}
/* The hotbar's tooltip, placed on the screen by the slot it is about: a
   window clips what is inside it. */
.hotbar-tooltip.item-tooltip {
  position: fixed;
  bottom: auto;
  display: flex;
  transform: translate(-50%, -100%);
  z-index: 1;
}
@media (hover: none) {
  .hotbar-tooltip.item-tooltip {
    display: none;
  }
}
.bag-grid {
  display: grid;
  grid-template-columns: repeat(5, 64px);
  gap: 3px;
  justify-content: center;
}
.item-detail {
  display: grid;
  gap: 8px;
}
.item-detail-hint {
  color: var(--color-text-muted);
  text-align: center;
}
.item-detail-head {
  display: grid;
  grid-template-columns: 64px 1fr;
  gap: 10px;
  align-items: start;
}
.item-detail-head h3 {
  font-family: var(--font-display);
  font-size: 16px;
}
.item-detail-quantity {
  font-family: var(--font-world);
  color: var(--color-text-muted);
}
.item-detail-type,
.item-detail-note {
  font-size: 12px;
  color: var(--color-text-muted);
}
.item-detail-effect,
.item-compare {
  font-weight: 700;
}
.item-compare {
  color: var(--color-text-muted);
}
.item-compare[data-change='up'] {
  color: var(--color-success);
}
.item-compare[data-change='down'] {
  color: var(--color-danger-text);
}
/* A thing for sale: what it is, as the bag's detail pane says it, and what
   it costs over the button that buys it. */
.shop-row {
  flex-wrap: nowrap;
  align-items: start;
  gap: 10px;
}
.shop-row > .item-detail-head {
  flex: 1;
  min-width: 0;
}
.shop-buy {
  display: grid;
  flex: 0 0 132px;
  gap: 4px;
  justify-items: end;
}
.shop-buy > button {
  width: 100%;
}
.shop-buy > [aria-disabled='true'],
.spell-row[aria-disabled='true'] {
  opacity: 0.5;
  filter: none;
  cursor: not-allowed;
}
.shop-buy > .stack-count,
.shop-wallet,
.shop-wallet > span {
  display: flex;
  align-items: center;
  gap: 4px;
}
.shop-wallet {
  gap: 14px;
}
.shop-wallet > .window-label {
  margin-left: auto;
}
.item-drop {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}
.item-drop > span:first-child {
  flex-basis: 100%;
}
/* What is worn, laid out round the character it is worn by. */
.equip-doll {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  grid-template-areas:
    '. helmet .'
    'weapon body chestplate'
    'gloves body .'
    '. boots .';
  justify-items: center;
  gap: 4px 0;
}
.equip-body {
  grid-area: body;
  align-self: center;
}
.equip-slot {
  display: grid;
  justify-items: center;
  gap: 2px;
}
.equip-label {
  font-size: 12px;
  color: var(--color-text-muted);
}
.equip-totals {
  display: flex;
  justify-content: center;
  gap: 24px;
  padding-top: 6px;
  border-top: 2px solid var(--color-border);
}
.equip-totals > div {
  display: flex;
  align-items: baseline;
  gap: 6px;
}
.equip-totals dt {
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.equip-totals dd {
  font-family: var(--font-world);
  font-size: 20px;
  font-variant-numeric: tabular-nums;
}

.attr-points {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 8px;
  padding: 4px 0;
}
.attr-points-number {
  font-family: var(--font-world);
  font-size: 40px;
  line-height: 1;
  color: var(--color-accent);
  font-variant-numeric: tabular-nums;
}
.attr-points-label {
  color: var(--color-text-muted);
}
.attr-list {
  display: grid;
  gap: 4px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.attr-row {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border: 2px solid var(--color-border);
  background: var(--color-surface-muted);
}
.attr-row[data-pending] {
  border-color: var(--color-accent);
}
.attr-row h3 {
  margin: 0;
  font-family: var(--font-display);
  font-size: 15px;
}
.attr-row p {
  margin: 0;
  font-size: 12px;
  color: var(--color-text-muted);
}
.attr-value {
  display: flex;
  align-items: baseline;
  justify-content: flex-end;
  gap: 4px;
  min-width: 40px;
  font-family: var(--font-world);
  font-size: 20px;
  font-variant-numeric: tabular-nums;
}
.attr-value span {
  font-size: 16px;
  color: var(--color-text-muted);
}
.attr-row[data-pending] .attr-value b {
  color: var(--color-success);
}
.attr-buttons {
  display: flex;
  gap: 4px;
}
/* Notices over the world, stacked under the map name. .notice is the look
   engine.ui.notice gives them; a module that styles its own (ui.toast) only
   borrows the stack and the fade. */
.module-toasts {
  position: fixed;
  top: 44px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  pointer-events: none;
  z-index: var(--z-toast);
}
.module-toasts > * {
  transition: opacity 0.4s;
}
/* One look for every tone: a plate in the panel colours, the tone carried by
   a stripe down its left edge, its frame and its label, so a quest and an
   achievement landing together read as one family in two colours. */
.notice {
  --notice-tone: var(--color-accent);
  padding: 6px 18px 6px 22px;
  border: 2px solid
    color-mix(in srgb, var(--notice-tone) 55%, var(--color-border));
  border-radius: var(--radius-control);
  background: var(--color-surface-muted);
  box-shadow:
    inset 4px 0 0 var(--notice-tone),
    var(--shadow-panel);
  color: var(--color-text);
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 600;
  text-align: center;
  animation: notice-in 0.25s ease-out;
}
.notice[data-tone='primary'] {
  --notice-tone: var(--color-primary);
}
.notice[data-tone='danger'] {
  --notice-tone: var(--color-danger);
}
.notice[data-tone='warning'],
.notice[data-tone='highlight'] {
  --notice-tone: var(--color-warning);
}
.notice[data-tone='highlight'] {
  font-size: 16px;
}
.notice-label {
  margin-bottom: 2px;
  color: var(--notice-tone);
  font-family: var(--font-ui);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
.notice-detail {
  margin-top: 2px;
  color: var(--color-text-muted);
  font-family: var(--font-ui);
  font-size: 12px;
  font-weight: normal;
}
@keyframes notice-in {
  from {
    transform: translateY(-8px);
    opacity: 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  .notice {
    animation: none;
  }
}
/* The status module's buffs and debuffs, a line of their own under the
   player card. */
#status-bar {
  flex-basis: 100%;
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}
#status-bar:empty {
  display: none;
}
.status-badge {
  padding: 0 5px;
  border: 2px solid var(--status-color, var(--color-warning));
  border-radius: var(--radius-control);
  background: var(--color-surface-muted);
  color: var(--color-text);
  font-family: var(--font-world);
  font-size: 12px;
  line-height: 16px;
  white-space: nowrap;
}
/* What a module offers on another player (engine.registerPlayerAction),
   opened by clicking them in the world. */
.player-menu {
  position: fixed;
  z-index: 1002;
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 160px;
  padding: 6px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  background: var(--color-surface);
  box-shadow: var(--shadow-panel);
  font-size: 13px;
}
.player-menu-title {
  font-family: var(--font-display);
  margin-bottom: 2px;
  padding: 2px 4px;
  border-bottom: 1px solid var(--color-border);
  font-weight: bold;
}
.player-menu button {
  width: 100%;
  padding: 5px 8px;
  border: none;
  background: transparent;
  text-align: left;
}
.player-menu button:hover {
  background: var(--color-surface-muted);
}
.content-workspace {
  display: grid;
  grid-template-columns: 148px minmax(0, 1fr);
  gap: 20px;
  min-height: 440px;
}
.content-navigation {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding-right: 12px;
  border-right: 1px solid var(--color-border);
}
.content-tab {
  text-align: left;
  padding: 9px 12px;
  border: 1px solid transparent;
  background: transparent;
  color: var(--color-text-muted);
  font-size: 13px;
}
.content-tab[aria-pressed='true'] {
  color: var(--color-text);
  background: var(--color-selected);
  border-color: var(--color-primary);
}
.content-inspector {
  min-width: 0;
}
.inspector-title {
  margin: 0 0 18px;
  font-size: 12px;
  font-weight: 600;
  color: var(--color-info);
  padding-bottom: 12px;
  border-bottom: 1px solid var(--color-border);
}
.auth-layout {
  position: absolute;
  inset: 0;
  display: flex;
  overflow: auto;
  /* A card taller than the window scrolls, and the scrollbar would push it
     off centre: the same gutter on both sides keeps it in the middle. */
  scrollbar-gutter: stable both-edges;
  padding: 32px 16px;
  background:
    radial-gradient(ellipse at 50% 20%, var(--color-selected), transparent 65%),
    var(--auth-backdrop),
    var(--color-app-bg);
}
.auth-card {
  width: 380px;
  max-width: 100%;
  margin: auto;
  padding: 28px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-panel);
  background: var(--color-surface);
  box-shadow: var(--shadow-panel);
}
.auth-heading {
  margin-bottom: 24px;
}
.auth-heading h1 {
  font-family: var(--font-display);
  font-size: 21px;
  font-weight: 600;
  margin-top: 24px;
}
.auth-form {
  text-align: left;
}
.auth-form input {
  font-size: 14px;
}
.auth-form button[type='submit'] {
  font-size: 14px;
}
/* Between a new account and the login screen it hands over to. */
.auth-handoff {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 24px 0 12px;
  text-align: center;
}
.auth-handoff .auth-spinner {
  margin-bottom: 12px;
}
.auth-handoff-title {
  font-family: var(--font-display);
  font-size: 17px;
  font-weight: 600;
  color: var(--color-success);
}
.auth-handoff-note {
  font-size: 13px;
  color: var(--color-text-muted);
}
.auth-spinner {
  width: 36px;
  height: 36px;
  flex: none;
  border: 3px solid var(--color-border);
  border-top-color: var(--color-primary);
  border-radius: 50%;
  animation: auth-spin 0.8s linear infinite;
}
/* On a primary button: the track and the arc in the button's own text color. */
.auth-spinner.small {
  width: 14px;
  height: 14px;
  border-width: 2px;
  border-color: color-mix(in srgb, currentColor 35%, transparent);
  border-top-color: currentColor;
}
@keyframes auth-spin {
  to {
    transform: rotate(360deg);
  }
}
@media (prefers-reduced-motion: reduce) {
  .auth-spinner {
    animation-duration: 2.4s;
  }
}
/* The eye sits inside the field. Edge draws its own reveal button there too,
   and two eyes read as a glitch. */
.password-field {
  position: relative;
  display: block;
}
.password-field > input::-ms-reveal {
  display: none;
}
.password-toggle {
  position: absolute;
  top: 50%;
  right: 6px;
  display: inline-flex;
  padding: 6px;
  border: 0;
  background: none;
  color: var(--color-text-muted);
  transform: translateY(-50%);
}
.password-toggle[aria-pressed='true'] {
  color: var(--color-accent);
}
.admin-header {
  flex-wrap: wrap;
  gap: 12px;
}
.admin-header-actions {
  flex-wrap: wrap;
}
/* One line at any width: a phone scrolls it sideways. */
.admin-nav {
  display: flex;
  gap: 2px;
  overflow-x: auto;
  scrollbar-width: none;
}
.admin-nav a {
  padding: 6px 10px;
  border-radius: var(--radius-control);
  color: var(--color-text-muted);
  font-size: 13px;
  white-space: nowrap;
  text-decoration: none;
}
.admin-nav a:hover,
.admin-nav a:focus-visible {
  background: var(--surface-control);
  color: var(--color-text);
}
/* A link in the sticky header lands the section's title below it; on a
   phone the header wraps to a line more. */
.admin-section {
  scroll-margin-top: 110px;
}
@media (max-width: 600px) {
  .admin-section {
    scroll-margin-top: 140px;
  }
}

/* The Appearance page of the admin panel: what is chosen on one side, the
   game's own classes under those choices on the other. */
.appearance {
  display: grid;
  grid-template-columns: minmax(260px, 340px) minmax(0, 1fr);
  gap: 16px;
  align-items: start;
}
.appearance-form,
.appearance-form fieldset {
  display: grid;
  gap: 8px;
}
.appearance-form {
  gap: 14px;
}
.appearance-form fieldset {
  border: 0;
}
.appearance-form legend {
  margin-bottom: 6px;
  font-weight: 600;
}
.appearance-row,
.appearance-row > label,
.appearance-check,
.appearance-actions {
  display: flex;
  align-items: center;
  gap: 8px;
}
.appearance-row {
  justify-content: space-between;
  flex-wrap: wrap;
}
.appearance-row input[type='color'] {
  width: 36px;
  height: 28px;
  padding: 0;
}
/* Only its button: a picture is sent as it is picked, so there is never a
   chosen file waiting for the name of one to be worth showing. */
.appearance-row input[type='file'] {
  width: auto;
  min-height: 0;
  padding: 0;
  border: 0;
  background: none;
  font-size: 0;
}
.appearance-row input[type='file']::file-selector-button {
  margin: 0;
  padding: 4px 8px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  background: var(--surface-control);
  color: var(--color-text);
  font: 12px var(--font-ui);
  cursor: pointer;
}
.appearance-note,
.appearance-row > small {
  font-size: 12px;
  color: var(--color-text-muted);
}
.appearance-note[data-tone='warning'] {
  padding: 8px;
  border: 1px solid var(--color-warning);
  color: var(--color-text);
}
.appearance-note[data-tone='danger'] {
  color: var(--color-danger-text);
}
.art {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr));
  gap: 24px;
  border: 0;
}
.art > p {
  grid-column: 1 / -1;
}
.art-sheet {
  display: flex;
  gap: 14px;
  align-items: start;
}
.art-sheet > img {
  flex: none;
  width: 96px;
  height: 96px;
  object-fit: contain;
  image-rendering: pixelated;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  background: var(--surface-control);
}
.art-sheet-body {
  display: grid;
  gap: 8px;
  min-width: 0;
}
.art-field {
  display: grid;
  gap: 4px;
  font-size: 13px;
}
/* As wide as the card, not as its longest option. */
.art-field select {
  width: 100%;
  min-width: 0;
}
.art-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.art-action {
  position: relative;
  padding: 5px 12px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  background: var(--surface-control);
  color: var(--color-text);
  font-size: 12px;
  text-decoration: none;
  cursor: pointer;
}
.art-action:hover {
  border-color: var(--color-accent);
}
.art-action:focus-visible,
.art-action:focus-within {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}
/* The label is the button and opens the picker; the input stays in the tab
   order, unseen, so the keyboard reaches it too. */
.art-action > input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.appearance-swatches {
  display: inline-flex;
  align-self: center;
}
.appearance-swatches > i {
  width: 10px;
  height: 14px;
}
/* The variables arrive inline, the engine's under the draft's; what the body
   sets from them is set again here, for the inherited value would be the
   page's own. */
.appearance-preview {
  /* In view while the form beside it scrolls, under the page's own header. */
  position: sticky;
  top: 76px;
  display: grid;
  gap: 10px;
  padding: 10px;
  border: 1px solid var(--color-border);
  font-family: var(--font-ui);
  color: var(--color-text);
  background: var(--color-app-bg);
}
/* As tall as its card, where the real one fills the screen. The card keeps
   its real width, a theme sizes the name and the logo for it, so it is
   scaled whole to leave the game below it in view. */
.appearance-login > .auth-layout {
  position: static;
  padding: 24px 16px;
  zoom: 0.7;
}
.appearance-caption {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.appearance-game {
  display: flex;
  gap: 6px;
}
.appearance-game[data-layout='left'] {
  flex-direction: row-reverse;
}
.appearance-game[data-layout='bottom'] {
  flex-direction: column;
}
.appearance-side {
  flex: 0 0 250px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
/* The game's #hud grows along the bar it sits in, not down a column. */
.appearance-side > #hud {
  flex: none;
}
.appearance-game[data-layout='bottom'] > .appearance-side {
  flex: none;
  flex-direction: row;
}
.appearance-game[data-layout='bottom'] > .appearance-side > .panel {
  flex: 1;
}
.appearance-game[data-layout='overlay'] {
  position: relative;
}
.appearance-game[data-layout='overlay'] > .appearance-side {
  position: absolute;
  inset: 40px auto auto 8px;
  z-index: 1;
  width: 250px;
}
/* The world as the editor's grid draws it, under the map's name and a window
   the player opened. */
.appearance-world {
  position: relative;
  flex: 1;
  min-height: 280px;
  border: 2px solid var(--color-border);
  background:
    linear-gradient(
        color-mix(in srgb, var(--color-border) 45%, transparent) 1px,
        transparent 1px
      )
      0 0 / 32px 32px,
    linear-gradient(
        90deg,
        color-mix(in srgb, var(--color-border) 45%, transparent) 1px,
        transparent 1px
      )
      0 0 / 32px 32px,
    var(--color-surface-muted);
}
.appearance-game[data-layout='overlay'] > .appearance-world {
  min-height: 300px;
}
.appearance-world .window-content {
  display: grid;
  gap: 10px;
}
.appearance-world > .editor-window {
  position: absolute;
  left: 50%;
  bottom: 16px;
  z-index: 1;
  width: min(260px, 90%);
  max-height: none;
  transform: translateX(-50%);
}
/* Save in reach while the form above it scrolls, with what it answered. */
.appearance-savebar {
  position: sticky;
  bottom: 0;
  display: grid;
  gap: 6px;
  padding: 10px 0;
  border-top: 1px solid var(--color-border);
  background: var(--color-surface);
}
@media (max-width: 760px) {
  .appearance {
    grid-template-columns: minmax(0, 1fr);
  }
  .appearance-preview {
    position: static;
  }
}

@media (max-width: 640px) {
  .content-workspace {
    grid-template-columns: minmax(0, 1fr);
    gap: 14px;
    min-height: 0;
  }
  .content-navigation {
    flex-direction: row;
    flex-wrap: wrap;
    border-right: 0;
    border-bottom: 1px solid var(--color-border);
    padding: 0 0 10px;
  }
  .content-tab {
    padding: 7px 9px;
  }
  .window-content {
    padding: 12px;
  }
  .viewport-stage {
    padding: 0;
  }
  .auth-layout {
    padding: 16px;
  }
  .auth-card {
    padding: 24px;
  }
}
#sidePanel {
  grid-area: panel;
}
#playerBar {
  grid-area: bar;
  display: flex;
  align-items: flex-start;
  gap: 8px;
  min-width: 0;
}
#map-name {
  font-family: var(--font-display);
  position: absolute;
  top: 8px;
  left: 50%;
  transform: translateX(-50%);
  z-index: var(--z-game-overlay);
  max-width: 40%;
  padding: 3px 12px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.45);
  color: rgba(255, 255, 255, 0.9);
  font-size: 12px;
  font-weight: 600;
  text-shadow: 1px 1px 2px #000;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  pointer-events: none;
}
#map-name.map-name-pvp {
  color: var(--color-warning);
}

/* The map's name across the world as it is entered: in, held, out, and left
   in place transparent until the next map replaces it. */
.zone-banner {
  position: absolute;
  top: 14%;
  left: 50%;
  transform: translateX(-50%);
  z-index: var(--z-game-overlay);
  max-width: 90%;
  padding: 4px 48px;
  border-block: 2px solid var(--color-accent);
  background: linear-gradient(
    90deg,
    transparent,
    rgba(0, 0, 0, 0.45) 20%,
    rgba(0, 0, 0, 0.45) 80%,
    transparent
  );
  color: var(--color-text);
  font-family: var(--font-display);
  font-size: clamp(20px, 4.2cqw, 56px);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-shadow:
    2px 2px 0 #000,
    -1px -1px 0 #000,
    1px -1px 0 #000,
    -1px 1px 0 #000;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
  opacity: 0;
  animation: zone-banner 3.8s ease-in-out forwards;
}
@keyframes zone-banner {
  0% {
    opacity: 0;
  }
  12%,
  88% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

.low-health-vignette {
  position: absolute;
  inset: 0;
  z-index: var(--z-game-overlay);
  pointer-events: none;
  box-shadow: inset 0 0 9cqw 2.4cqw
    color-mix(in srgb, var(--color-danger) 88%, transparent);
  opacity: 0.45;
  animation: low-health-heartbeat 1.2s ease-in-out infinite;
}
/* Two beats and a rest, like the thing it stands for. */
@keyframes low-health-heartbeat {
  0%,
  100% {
    opacity: 0.4;
  }
  10% {
    opacity: 0.8;
  }
  20% {
    opacity: 0.45;
  }
  32% {
    opacity: 0.65;
  }
  48% {
    opacity: 0.4;
  }
}

@media (prefers-reduced-motion: reduce) {
  .zone-banner {
    animation-timing-function: step-end;
  }
  .low-health-vignette {
    animation: none;
  }
}

/* Slots along the bottom of the world (engine.ui.hotbar). Sized against the
   frame rather than the window, so eight still fit across a phone held
   upright; the icon is one tile of the spell sheet at its own 32px. */
.hotbar {
  --slot: clamp(38px, 6.5cqw, 56px);
  position: absolute;
  left: 50%;
  bottom: 8px;
  transform: translateX(-50%);
  z-index: var(--z-game-overlay);
  display: flex;
  gap: 4px;
  padding: 4px;
  background: color-mix(in srgb, var(--color-surface-muted) 80%, transparent);
  border: 2px solid var(--color-border);
  border-radius: var(--radius-panel);
}
.hotbar-tip {
  position: relative;
  display: flex;
}
.hotbar-slot {
  position: relative;
  width: var(--slot);
  height: var(--slot);
  padding: 0;
  display: grid;
  place-items: center;
  overflow: hidden;
  background: var(--color-surface);
  border: 2px solid var(--color-border);
  border-radius: var(--radius-control);
}
.hotbar-slot[data-state='ready'] {
  border-color: var(--color-accent);
}
.hotbar-slot[data-state='ready'][data-friendly] {
  border-color: var(--color-success);
}
.hotbar-slot:active {
  transform: translateY(1px);
}
.hotbar-slot[data-state='nomana'] > :is(canvas, .spell-monogram) {
  filter: grayscale(0.85);
  opacity: 0.6;
}
.spell-monogram {
  font-family: var(--font-world);
  font-size: 20px;
}
.hotbar-key,
.hotbar-cost,
.hotbar-mark {
  position: absolute;
  font-family: var(--font-world);
  font-size: 11px;
  line-height: 1;
  text-shadow: 1px 1px 0 #000;
}
.hotbar-key {
  top: 2px;
  left: 3px;
  color: var(--color-text-muted);
}
.hotbar-cost {
  bottom: 2px;
  right: 3px;
  color: var(--color-info);
}
.hotbar-mark {
  top: 2px;
  right: 3px;
  color: var(--color-success);
}
.hotbar-slot[data-state='nomana'] :is(.hotbar-cost, .hotbar-mark) {
  color: var(--color-danger-text);
}
/* The dark wedge is the share of the cooldown still owed, shrinking
   clockwise as it runs out. */
.hotbar-cooldown {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: var(--font-world);
  font-size: 18px;
  color: #fff;
  text-shadow:
    1px 1px 0 #000,
    -1px -1px 0 #000;
  background:
    conic-gradient(
      transparent 0 calc(100% - var(--cd)),
      rgba(0, 0, 0, 0.72) 0
    ),
    rgba(0, 0, 0, 0.25);
}
.hotbar-tooltip {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 8px);
  transform: translateX(-50%);
  display: none;
  flex-direction: column;
  gap: 2px;
  width: max-content;
  max-width: 220px;
  padding: 6px 10px;
  font-size: 12px;
  color: var(--color-text-muted);
  background: var(--color-surface-muted);
  border: 2px solid var(--color-warning);
  box-shadow: var(--shadow-panel);
  pointer-events: none;
}
.hotbar-tooltip > strong {
  font-family: var(--font-display);
  font-size: 13px;
  color: var(--color-text);
}
/* On hover only where there is one: a tap on a phone is the cast, and a
   tooltip left standing over the world after it would only cover it. */
@media (hover: hover) {
  .hotbar-tip:hover > .hotbar-tooltip {
    display: flex;
  }
}
.hotbar-tip:has(> :focus-visible) > .hotbar-tooltip {
  display: flex;
}
/* A conversation. It holds the page while it is open, so .dialog-screen takes
   every click. .dialog-layer is laid over the world from outside - the frame's
   size containment lets nothing out of it - and the box stands at its foot,
   over the hotbar, which takes no key while someone is talking. */
.dialog-screen {
  position: fixed;
  inset: 0;
  z-index: var(--z-dialog);
}
.dialog-layer {
  position: absolute;
}
.npc-dialog {
  position: absolute;
  left: 50%;
  bottom: 8px;
  transform: translateX(-50%);
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 12px;
  width: min(680px, 96%);
  max-height: 80%;
  padding: 10px 12px;
  overflow: auto;
  box-shadow: var(--shadow-panel);
  font-size: 15px;
  line-height: 1.35;
}
/* Where the bar is docked outside the world, and so still in sight. */
body:has(.npc-dialog) .hotbar {
  opacity: 0.45;
  filter: saturate(0.5);
}
/* data-sprite is the speaker's sprite id, as on #hud-portrait, and a theme
   puts a drawn portrait in its place the same way:
     .dialog-portrait[data-sprite="7"] { background-image: url(portraits/smith.png); }
     .dialog-portrait[data-sprite="7"] > canvas { visibility: hidden; } */
.dialog-portrait {
  display: grid;
  place-items: center;
  width: 72px;
  height: 72px;
  border: 2px solid var(--color-accent);
  background: var(--color-surface-muted) center / cover no-repeat;
}
.dialog-body {
  grid-column: 2;
  display: grid;
  gap: 6px;
  align-content: start;
  min-width: 0;
  /* Clear of the button that leaves the conversation. */
  padding-right: 28px;
}
.npc-dialog > .window-close {
  position: absolute;
  top: 4px;
  right: 4px;
}
.dialog-line {
  white-space: pre-wrap;
}
/* What has not been said yet, holding the room it will take so the box does
   not grow under the reader. */
.dialog-line > span {
  opacity: 0;
}
.dialog-choices {
  display: grid;
  gap: 2px;
  list-style: none;
}
.dialog-choices[data-waiting] {
  visibility: hidden;
}
.dialog-choice {
  display: grid;
  grid-template-columns: 20px minmax(0, 1fr);
  gap: 8px;
  align-items: start;
  width: 100%;
  min-height: 0;
  padding: 3px 6px;
  border: 0;
  background: transparent;
  font-weight: 400;
  text-align: left;
}
.dialog-choice:hover,
.dialog-choice:focus-visible {
  background: var(--color-surface);
}
/* It leads somewhere this conversation has already been. */
.dialog-choice[data-read] {
  color: var(--color-text-muted);
}
.dialog-choice > b {
  display: grid;
  place-items: center;
  height: 18px;
  margin-top: 1px;
  font-family: var(--font-world);
  font-size: 12px;
  color: var(--color-app-bg);
  background: var(--color-accent);
}
/* Upright on a phone the world is a strip at the top, and a box inside it
   would cover all of it: the conversation goes under the world, over the pad
   it holds still anyway. */
@media (max-width: 700px) and (orientation: portrait) {
  .npc-dialog {
    top: calc(100% + 8px);
    bottom: auto;
    max-height: 55dvh;
  }
}
.player-controls {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
}
.player-controls button {
  min-height: 44px;
  min-width: 44px;
  border-radius: var(--radius-control);
}
.panel-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  gap: 8px;
}
.panel-toggle[aria-expanded='true'] {
  background: var(--color-selected);
  border-color: var(--color-info);
}
/* The owner's strip, shown to staff only. It stays outside .panel so that a
   theme's frame does not give it the weight of something the player uses. */
.creator-tools {
  flex: none;
  border: 2px solid var(--color-border);
  border-radius: var(--radius-panel);
}
.creator-tools summary {
  padding: 5px 8px;
  color: var(--color-text-muted);
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  cursor: pointer;
}
.creator-tools-body {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  padding: 0 8px 8px;
}
.creator-tools .action-button {
  flex-direction: row;
  width: auto;
  min-height: 30px;
  padding: 4px 8px 4px 4px;
  gap: 6px;
  text-decoration: none;
}

/* A small screen has no width to spare for the column: the menu becomes a
   sheet over the world, opened on demand. */
@media (max-width: 900px), (max-height: 500px) {
  .panel-toggle {
    display: flex;
  }
  #sidePanel {
    position: fixed;
    z-index: var(--z-panel);
    top: max(108px, env(safe-area-inset-top));
    right: max(10px, env(safe-area-inset-right));
    bottom: max(10px, env(safe-area-inset-bottom));
    width: min(340px, calc(100vw - 20px));
    padding: 6px;
    background: var(--color-app-bg);
    border: 2px solid var(--color-border);
    box-shadow: 0 12px 48px #000a;
  }
  #gameShell.panel-collapsed #sidePanel {
    display: none;
  }
  .action-button {
    min-height: 44px;
  }
}
/* Upright, the height is what is spare: the card sits in a bar above the
   world, which starts right under it. */
@media (max-width: 900px) {
  #gameShell {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      'bar'
      'main';
  }
  #playerBar {
    align-items: center;
    justify-content: space-between;
    gap: 16px;
  }
  #playerBar #hud {
    flex: 0 1 480px;
  }
  .viewport-stage {
    align-items: flex-start;
  }
}
@media (max-width: 540px) {
  #gameShell {
    padding: max(6px, env(safe-area-inset-top)) 6px
      max(6px, env(safe-area-inset-bottom));
    gap: 6px;
  }
  #playerBar {
    gap: 6px;
  }
  #playerBar #hud {
    padding: 6px;
  }
  #hud-portrait {
    display: none;
  }
  #hud-name {
    font-size: 12px;
  }
  #hud-level {
    font-size: 10px;
    padding: 0 3px;
  }
  .player-controls {
    flex-direction: column;
    gap: 4px;
  }
  .player-controls button {
    padding: 4px 8px;
  }
  .panel-toggle > span:last-child {
    display: none;
  }
}
/* A phone on its side: no height for a bar and no width for a column, but
   the world leaves a band either side of it. The card takes the left band and
   the buttons the right, over the thumb pad's corners, and the map stays
   clear. --band is that width, from the same min() that sizes #canvasFrame. */
@media (max-height: 500px) and (orientation: landscape) {
  #gameShell {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr);
    grid-template-areas: 'main';
  }
  #playerBar {
    --band: calc((100cqw - min(100cqw, 100cqh * 1280 / 896)) / 2 - 8px);
    position: absolute;
    z-index: var(--z-hud);
    top: max(10px, env(safe-area-inset-top));
    left: max(10px, env(safe-area-inset-left));
    width: 100cqw;
    justify-content: space-between;
    pointer-events: none;
  }
  #playerBar > * {
    pointer-events: auto;
  }
  #playerBar #hud {
    flex: 0 0 auto;
    width: max(120px, var(--band));
    padding: 6px;
  }
  #hud-portrait,
  #hud-hp-values,
  .hud-resource-values,
  #xp-bar-text {
    display: none;
  }
  .player-controls {
    flex-direction: column;
  }
  .panel-toggle > span:last-child {
    display: none;
  }
  /* No height for the chat under the menu, where it would be a scroll away
     with nothing to say so: they go side by side, in a sheet beside the
     buttons that open it. */
  #sidePanel {
    top: max(10px, env(safe-area-inset-top));
    right: calc(max(10px, env(safe-area-inset-right)) + 52px);
    width: min(580px, calc(100vw - 84px));
    flex-flow: row wrap;
    align-content: flex-start;
  }
  #actionPanel {
    flex: 0 0 300px;
  }
  #chatContainer {
    flex: 1 1 160px;
    min-width: 0;
    min-height: 0;
  }
  .creator-tools {
    flex: 0 0 100%;
  }
}
@media (pointer: coarse) and (orientation: portrait) {
  #touchControls {
    position: static;
    width: 100%;
    flex-shrink: 0;
    padding: 12px;
  }
}
/* The last lines said (ChatPeek), in what an upright phone has left between
   the world and the thumb pad. Anywhere else the chat itself is on screen, or
   there is no such gap. */
.chat-peek {
  display: none;
}
@media (pointer: coarse) and (orientation: portrait) and (max-width: 900px) {
  .viewport-stage {
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    gap: 6px;
  }
  #canvasFrame {
    flex: none;
  }
  .chat-peek {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    width: 100%;
    overflow: hidden;
    font-family: var(--font-ui);
    text-align: left;
    cursor: pointer;
  }
  .chat-peek > span {
    flex: none;
  }
  /* A conversation is laid over the same gap. */
  body:has(.npc-dialog) .chat-peek {
    visibility: hidden;
  }
}
/* Where the menu sits is the game's choice (theme/theme.json, sent by /game as
   data-layout on the shell). The column on the right is the base above; these
   are the others, for a screen with room for them. A phone keeps the
   arrangements above whichever the game picked. */
@media (min-width: 901px) and (min-height: 501px) {
  #gameShell[data-layout='left'] {
    grid-template-columns: 320px minmax(0, 1fr);
    grid-template-areas:
      'bar main'
      'panel main';
  }

  /* One bar under the world: the card, the hotbar and the menu side by side,
     and the chat above them folded to its last two lines. #sidePanel gives up
     its own box so the menu and the chat are placed on the shell's grid. */
  #gameShell[data-layout='bottom'] {
    grid-template-columns: minmax(230px, 290px) minmax(0, 1fr) auto auto;
    grid-template-rows: minmax(0, 1fr) 58px auto;
    grid-template-areas:
      'main main main main'
      'chat chat chat chat'
      'bar hotbar menu tools';
    align-items: stretch;
  }
  :is([data-layout='bottom'], [data-layout='overlay']) #sidePanel {
    display: contents;
  }
  :is([data-layout='bottom'], [data-layout='overlay']) #playerBar {
    align-items: stretch;
  }
  :is([data-layout='bottom'], [data-layout='overlay']) .player-controls {
    align-self: center;
  }
  :is([data-layout='bottom'], [data-layout='overlay']) #hud {
    flex-wrap: nowrap;
    align-items: center;
    gap: 8px;
    padding: 6px;
  }
  :is([data-layout='bottom'], [data-layout='overlay']) #hud-portrait {
    width: 44px;
    height: 44px;
  }
  :is([data-layout='bottom'], [data-layout='overlay']) .hud-stats {
    gap: 3px 6px;
  }
  :is([data-layout='bottom'], [data-layout='overlay']) #xp-bar-text {
    display: none;
  }

  /* The hotbar is a block of the bar here, not an overlay of the world. It
     keeps a z-index of its own for its tooltips, which open upward over the
     chat. */
  #gameShell:is([data-layout='bottom'], [data-layout='overlay']) > .hotbar {
    --slot: 46px;
    grid-area: hotbar;
    position: relative;
    left: auto;
    bottom: auto;
    transform: none;
    align-self: stretch;
    justify-self: center;
    align-items: center;
    z-index: var(--z-hud);
  }

  /* Two rows of icons, as many columns as the modules add. The label stays for
     a screen reader and for the tooltip; the icon and the key carry the tile. */
  :is([data-layout='bottom'], [data-layout='overlay']) #actionPanel {
    grid-area: menu;
    align-self: stretch;
    padding: 4px;
  }
  :is([data-layout='bottom'], [data-layout='overlay']) .action-grid {
    grid-auto-flow: column;
    grid-template-columns: none;
    grid-template-rows: repeat(2, 44px);
    grid-auto-columns: 40px;
    gap: 2px;
  }
  :is([data-layout='bottom'], [data-layout='overlay']) .action-grid .action-button {
    min-height: 0;
    padding: 4px 0 0;
  }
  :is([data-layout='bottom'], [data-layout='overlay']) .action-label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
  }

  :is([data-layout='bottom'], [data-layout='overlay']) .creator-tools {
    grid-area: tools;
    position: relative;
    align-self: end;
  }
  :is([data-layout='bottom'], [data-layout='overlay']) .creator-tools-body {
    position: absolute;
    right: 0;
    bottom: 100%;
    z-index: var(--z-panel);
    width: max-content;
    max-width: 320px;
    padding: 8px;
    background: var(--color-panel-bg);
    border: 2px solid var(--color-border);
    border-radius: var(--radius-panel);
  }

  /* Folded: the tabs are gone and the log shows the last two lines, the input
     and the button beside it. Unfolded, the panel rises out of its row over the
     world - an absolutely placed box on a grid takes the grid area as its
     containing block - so the bar does not change height. */
  :is([data-layout='bottom'], [data-layout='overlay']) #chatContainer {
    grid-area: chat;
    min-height: 0;
    flex-direction: row;
    align-items: stretch;
    padding: 4px;
  }
  :is([data-layout='bottom'], [data-layout='overlay']) .chat-tabs {
    display: none;
  }
  :is([data-layout='bottom'], [data-layout='overlay']) #chatContainer .chat-box {
    overflow-y: hidden;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    padding: 2px 6px;
    line-height: 18px;
  }
  :is([data-layout='bottom'], [data-layout='overlay']) #chatBox > div {
    flex: none;
    margin: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  :is([data-layout='bottom'], [data-layout='overlay']) #chatInputContainer {
    flex: 0 0 300px;
    align-self: center;
  }
  :is([data-layout='bottom'], [data-layout='overlay']) .chat-expand {
    display: block;
  }
  :is([data-layout='bottom'], [data-layout='overlay']) #chatContainer.is-expanded {
    position: absolute;
    inset: auto 0 0;
    z-index: var(--z-panel);
    height: 340px;
    flex-direction: column;
    gap: 4px;
    padding: 8px;
    border-color: var(--color-accent);
  }
  :is([data-layout='bottom'], [data-layout='overlay']) .is-expanded .chat-tabs {
    display: flex;
  }
  :is([data-layout='bottom'], [data-layout='overlay']) .is-expanded .chat-box {
    overflow-y: auto;
    justify-content: flex-start;
  }
  :is([data-layout='bottom'], [data-layout='overlay']) .is-expanded #chatBox > div {
    overflow: visible;
    white-space: normal;
  }
  :is([data-layout='bottom'], [data-layout='overlay']) .is-expanded #chatInputContainer {
    flex: none;
    align-self: stretch;
  }
  :is([data-layout='bottom'], [data-layout='overlay']) .is-expanded .chat-expand {
    align-self: flex-end;
  }
  /* The world covers the window and the controls float over it, on a grid of
     their own: the card and the creator tools under it at the top left, the
     chat, the hotbar and the menu along the bottom. */
  #gameShell[data-layout='overlay'] {
    grid-template-columns: min(420px, 40%) minmax(0, 1fr) auto;
    grid-template-rows: auto auto minmax(0, 1fr) auto;
    grid-template-areas:
      'bar . .'
      'tools . .'
      '. . .'
      'chat hotbar menu';
    gap: 8px;
  }
  [data-layout='overlay'] #gameMain {
    position: absolute;
    inset: 0;
  }
  [data-layout='overlay'] #canvasFrame {
    width: 100%;
    height: 100%;
    aspect-ratio: auto;
    border: 0;
    border-radius: 0;
    background: var(--color-app-bg);
  }
  /* Its logical aspect ratio is kept and the frame clips the spare edges, but
     never past a quarter of the world either way: an ultrawide or a tall
     window gets bars at its sides rather than a strip of the map. */
  [data-layout='overlay'] #canvasStack {
    inset: auto;
    left: 50%;
    top: 50%;
    width: min(
      max(100cqw, calc(100cqh * 1280 / 896)),
      calc(100cqw * 4 / 3),
      calc(100cqh * 1280 / 896 * 4 / 3)
    );
    height: auto;
    aspect-ratio: 1280 / 896;
    transform: translate(-50%, -50%);
  }
  [data-layout='overlay'] :is(#playerBar, #actionPanel, #chatContainer, .creator-tools) {
    position: relative;
    z-index: var(--z-hud);
  }
  [data-layout='overlay'] #playerBar {
    justify-self: start;
    width: 320px;
  }
  [data-layout='overlay'] :is(#actionPanel, #gameShell > .hotbar) {
    align-self: end;
  }
  [data-layout='overlay'] .creator-tools {
    justify-self: start;
    align-self: start;
    background: var(--color-panel-bg);
  }
  [data-layout='overlay'] .creator-tools-body {
    right: auto;
    bottom: auto;
    top: 100%;
  }
  /* Folded: the log, its last lines, over a row of the input and the button.
     A grid: a wrapping flex row sizes each line to its content, so the log
     would grow a line at a time and push the input out of the window. */
  [data-layout='overlay'] #chatContainer {
    height: 100px;
  }
  [data-layout='overlay'] #chatContainer:not(.is-expanded) {
    display: grid;
    grid-template: minmax(0, 1fr) auto / minmax(0, 1fr) auto;
  }
  [data-layout='overlay'] #chatContainer:not(.is-expanded) .chat-box {
    grid-column: 1 / -1;
  }
  [data-layout='overlay'] #chatInputContainer {
    min-width: 0;
  }
  [data-layout='overlay'] #chatContainer.is-expanded {
    height: min(340px, calc(100cqh - 150px));
  }
}

.chat-expand {
  display: none;
  flex: none;
  padding: 0 12px;
  color: var(--color-text);
  background: var(--color-surface);
  border: 2px solid var(--color-border);
  border-radius: var(--radius-control);
  font-family: var(--font-ui);
  font-size: 12px;
  font-weight: 700;
  cursor: pointer;
}
.chat-expand:hover {
  border-color: var(--color-accent);
}

/* Inline geometry belongs to desktop dragging; mobile surfaces are anchored. */
@media (max-width: 700px), (pointer: coarse) and (max-height: 600px) {
  .editor-window {
    inset: 0 !important;
    width: 100% !important;
    max-width: none !important;
    height: 100dvh !important;
    max-height: 100dvh !important;
    border-radius: 0 !important;
    padding: env(safe-area-inset-top) env(safe-area-inset-right)
      env(safe-area-inset-bottom) env(safe-area-inset-left);
    background: var(--color-surface);
  }
  .window-titlebar {
    cursor: default !important;
    touch-action: auto !important;
  }
  /* Over a window that is the whole screen a notice lands on its title bar,
     short of the close button: the one strip the player is not reading. A
     module's panel stays in the document while shut, hidden inline. */
  body:has(.editor-window:not([style*='display: none'])) .module-toasts {
    top: calc(env(safe-area-inset-top) + 2px) !important;
    left: 8px !important;
    right: 56px;
    transform: none;
    align-items: stretch;
  }
  .window-close {
    min-width: 44px;
    min-height: 44px;
    flex-shrink: 0;
  }
  /* The window is the whole screen: what it holds takes the height, and its
     actions stay at the foot, where the thumb is. */
  .window-content {
    flex: 1;
    padding: 16px;
  }
  .editor-window input,
  .editor-window select,
  #chatInput,
  #chatScope {
    font-size: 16px !important;
  }
  .ui-dialog-overlay {
    align-items: flex-end !important;
    padding: 0 !important;
  }
  #ui-dialog {
    width: 100% !important;
    max-height: 85dvh;
    overflow: auto;
    border-radius: 16px 16px 0 0 !important;
    padding-bottom: max(20px, env(safe-area-inset-bottom)) !important;
  }
  #ui-dialog button {
    min-height: 44px;
  }
}

/* A finger needs more to hit than a cursor does. */
@media (pointer: coarse) {
  :where(.game-window .window-content, .window-footer)
    :where(button, select, input:not([type='checkbox'], [type='radio'])),
  .list-row-actions > button,
  .window-tabs > button,
  .stepper > button,
  .chip,
  .dialog-choice {
    min-height: 40px;
  }
  .step-button {
    width: 40px;
  }
  .window-close {
    min-width: 40px;
    min-height: 40px;
  }
  .dialog-body {
    padding-right: 40px;
  }
  .chip,
  .dialog-choice {
    align-items: center;
  }
  /* The key that does the same, which a touch screen does not have. */
  .action-key,
  .hotbar-key {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    scroll-behavior: auto !important;
    transition: none !important;
  }
}

#gameShell[data-art='smooth'] #canvasStack > canvas {
  image-rendering: auto;
}
