/* Self-hosted rather than a CDN link, to keep the client free of a
   third-party request on every page load. Only the two weights actually
   used in this file are included — regular for body text, bold for room
   titles and headings — no italic, no variable-font axes. font-display:
   swap shows the fallback monospace font immediately and swaps once
   JetBrains Mono loads, rather than leaving text invisible while it
   loads. */
@font-face {
  font-family: 'JetBrains Mono';
  src: url('../fonts/JetBrainsMono-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'JetBrains Mono';
  src: url('../fonts/JetBrainsMono-Bold.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* height (not min-height) is what actually caps the page at the viewport —
   min-height is only a floor, so without it a tall #event-log could grow
   the whole page past the viewport and push #command-input below the
   fold. box-sizing: border-box keeps this element's own padding inside
   that height rather than adding to it. Declaring height twice, 100vh then
   100dvh, means a browser without dvh support still gets a usable height
   instead of falling through to the default. */
body {
  font-family: 'JetBrains Mono', monospace;
  font-size: 14px;
  margin: 0;
  padding: 1rem;
  background: var(--bg, #0b0b0b);
  box-sizing: border-box;
  height: 100vh;
  height: 100dvh;
  display: flex;
  flex-direction: column;
}

/* box-sizing: border-box keeps this element's own padding and border
   inside its flex-basis rather than adding to it. flex: 1 with
   min-height: 0 lets it fill whatever height its flex parent leaves
   available, rather than claiming a separate fixed height of its own. */
#app {
  max-width: 800px;
  margin: 0 auto;
  padding: 1rem;
  border: 1px solid var(--border, #333333);
  box-sizing: border-box;
  flex: 1;
  min-height: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
}

/* :not([hidden]) — without it, this rule's display: flex would override
   the browser's default [hidden] { display: none } on a hidden form, since
   an author-stylesheet rule beats a user-agent rule regardless of selector
   specificity. Scoping the selector this way means the rule simply doesn't
   match a hidden form, leaving the browser's own [hidden] handling
   unopposed. */
form:not([hidden]) {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  max-width: 300px;
}

/* Inline validation message below a form. :empty collapses it out of the
   flex flow entirely until there's a message to show. */
.form-error {
  margin: 0;
  font-size: 0.85rem;
  color: var(--hp-critical, #e0685c);
}

.form-error:empty {
  display: none;
}

#character-list {
  list-style: none;
  padding: 0;
}

#character-list li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* :not([hidden]) keeps this rule from overriding the browser's default
   [hidden] { display: none }. flex: 1 fills whatever space its flex parent
   leaves available, rather than this element claiming a separate
   full-viewport height of its own. min-height: 0 overrides flex's default
   min-height: auto, which would otherwise let this element grow to fit its
   content instead of respecting the space its parent gives it. */
#game-screen:not([hidden]) {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

/* position: relative makes this the positioning context for the absolutely
   positioned scroll indicator inside it, so the indicator anchors to the
   log's own box regardless of how much room the command input beneath it
   takes up. */
#log-panel {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  margin: 1rem 0;
}

/* Explicit scrollbar styling — a default OS-themed scrollbar (especially
   macOS's auto-hiding, trackpad-only style) can be nearly invisible
   against this dark background until actively scrolling, giving no cue
   that older entries exist above the fold. scrollbar-width/scrollbar-color
   (the standard properties) and the ::-webkit-scrollbar rules below (for
   browsers that don't support them) both point at the same --dim token, so
   neither path needs to be kept in sync by hand. */
#event-log {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  scrollbar-width: thin;
  scrollbar-color: var(--dim, #6b6b6b) transparent;
}

#event-log::-webkit-scrollbar {
  width: 8px;
}

#event-log::-webkit-scrollbar-track {
  background: transparent;
}

#event-log::-webkit-scrollbar-thumb {
  background: var(--dim, #6b6b6b);
  border-radius: 4px;
}

#event-log::-webkit-scrollbar-thumb:hover {
  background: var(--accent, #7fdbca);
}

#event-log li {
  line-height: 1.5;
  padding: 0.35rem 0;
}

/* Anchors the whole log to the bottom edge while it's sparse, via
   margin-top: auto on the first entry — not justify-content: flex-end on
   the list, which has a real cross-browser bug: once content overflows,
   the browser can leave the overflowed start-edge content completely
   unreachable, so the mouse wheel does nothing and no scrollbar styling
   fixes it. margin-top: auto gets the same bottom-anchored look while the
   log is sparse, but simply resolves to 0 once it overflows, leaving
   ordinary top-anchored scrolling in place. Re-targets automatically as
   entries are appended or the log is cleared, since :first-child is
   evaluated against current DOM state, not fixed at some point in time. */
#event-log li:first-child {
  margin-top: auto;
}

/* Room title inside a fused room entry — bold and accented so it reads as
   a heading, not just another line of description. */
.entry-room .room-title {
  color: var(--accent, #7fdbca);
  font-weight: bold;
}

.help-command {
  font-weight: bold;
}

/* Category heading in a help listing (e.g. "Movement", "Communication") —
   the same accent color as a room title, so it reads as a section break
   rather than just another line. Uppercase and letter-spacing are the only
   cues distinguishing it from a room title, since both share that color.
   Gets no bullet prefix, unlike an ordinary help line. */
.help-category {
  color: var(--accent, #7fdbca);
  font-weight: bold;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

/* Bullet prefix generated via ::before rather than a real list marker, so
   the underlying element doesn't need to be a list item. */
.help-line::before {
  content: '• ';
  color: var(--dim, #6b6b6b);
}

/* Compact multi-column command index. 3 columns keeps a bare `help`
   listing scannable as a reference grid instead of one long column. */
.help-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.15rem 1rem;
}

.help-grid-item::before {
  content: '• ';
  color: var(--dim, #6b6b6b);
}

/* Shared base color for bracketed nouns in the log — players, npcs, mobs,
   items, set dressing, and exits. The five presence categories below
   override this with their own theme token; exits keep this default. */
.entity {
  color: var(--accent, #7fdbca);
}

.entity-mob {
  color: var(--entity-mob, var(--accent, #7fdbca));
}

.entity-npc {
  color: var(--entity-npc, var(--accent, #7fdbca));
}

.entity-player {
  color: var(--entity-player, var(--accent, #7fdbca));
}

.entity-item {
  color: var(--entity-item, var(--accent, #7fdbca));
}

.entity-dressing {
  color: var(--entity-dressing, var(--accent, #7fdbca));
}

.entity-corpse {
  color: var(--entity-corpse, var(--accent, #7fdbca));
}

/* Combat log line coloring — reuses the HP bar's red/yellow/green tokens
   rather than new ones, since the same danger/caution/good semantics
   apply: taking damage or dying reads as --hp-critical, a miss or failed
   flee as --dim (nothing happened), and an xp/cash/level gain as
   --hp-good. A crit gets bold on top of --hp-critical so it stands out
   from ordinary damage in a fast-scrolling fight. */
.combat-damage {
  color: var(--hp-critical, #e0685c);
}
.combat-critical {
  color: var(--hp-critical, #e0685c);
  font-weight: bold;
}
.combat-miss {
  color: var(--dim, #6b6b6b);
}
.combat-gain {
  color: var(--hp-good, #5bbf7a);
}
.combat-death {
  color: var(--hp-critical, #e0685c);
  font-weight: bold;
}
.combat-engage {
  color: var(--hp-warn, #e0c15c);
}

/* The player's own typed input, echoed back before its result arrives —
   dimmed and caret-prefixed so it reads as "what I said," not "what the
   game said back." */
.entry-command {
  color: var(--dim, #6b6b6b);
}
.entry-command::before {
  content: '> ';
}

.entry-result {
  color: var(--fg, #d0d0d0);
}

/* A message personally addressed to this player (a received tell, a gift
   offer) — a left accent border does the work of standing out, so the text
   itself stays dim and doesn't compete with the border for attention. */
.entry-notice {
  border-left: 3px solid var(--accent, #7fdbca);
  padding-left: 0.5rem;
  color: var(--dim, #6b6b6b);
  font-size: 0.9em;
}

/* Ambient/background log lines (bystander chat, presence, bystander
   combat, bystander item actions) — dim and slightly smaller than ordinary
   text, so they read as background noise rather than a direct event. */
.entry-ambient {
  color: var(--dim, #6b6b6b);
  font-size: 0.9em;
}

.entry-ambient .entity-player {
  color: var(--dim);
}

/* Marks an actual room change — not shown for a same-room refresh. */
.entry-divider {
  border-top: 1px dashed var(--border, #333333);
  margin: 0.4rem 0;
}

/* :not([hidden]) keeps this rule from overriding the browser's default
   [hidden] { display: none }. */
#scroll-indicator:not([hidden]) {
  position: absolute;
  bottom: 0.5rem;
  left: 50%;
  transform: translateX(-50%);
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.8rem;
  letter-spacing: 0.03em;
  background: var(--accent, #7fdbca);
  color: var(--bg, #0b0b0b);
  border: none;
  border-radius: 999px;
  padding: 0.3rem 0.9rem;
  cursor: pointer;
}

/* A bare <input> can't reliably render ::before content since it's a
   replaced element, so the "> " prompt glyph lives on this wrapper instead
   of on the input itself.

   Focus indication is a background-color swap (--panel-bg to
   --panel-bg-focus), not an outline or ring — WCAG 2.4.7 requires a
   visible focus indicator, not specifically a border, and a deliberately
   subtle resting background leaves room for an obvious, high-contrast jump
   on focus. There is no border anywhere in this component: border: 0 and
   border-radius: 0 below are the resting-state visual, not an
   accessibility fallback — the background swap alone carries that job. */
#command-line {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0.75rem;
  background: var(--panel-bg, #0e1211);
  border-radius: 0px;
  transition: background-color 0.15s ease;
}

#command-line:focus-within {
  background: var(--panel-bg-focus, #14302c);
}

#command-line::before {
  content: '> ';
  color: var(--accent, #7fdbca);
  font-family: 'JetBrains Mono', monospace;
}

/* Persistent character HUD beneath the input, not in the scrolling log.
   flex-wrap: wrap rather than truncating or hiding fields on narrow
   viewports — every field here is a live gameplay stat, not decoration, so
   on a narrow screen it drops to a second line rather than losing a
   field. */
#status-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem 1rem;
  padding: 0.4rem 0.75rem 0;
  color: var(--dim, #6b6b6b);
  font-size: 0.85rem;
}

.status-name {
  color: var(--accent, #7fdbca);
  font-weight: bold;
}

/* Both names can be arbitrarily long (a player's own character name, or a
   mob's authored name) and share the status bar with several other live
   stats, so an unusually long one would otherwise push everything else
   around or wrap the bar unpredictably. inline-block is required for
   max-width to apply to a span at all; overflow/text-overflow turn the
   clip into a readable ellipsis instead of a hard cut. */
.status-name,
.status-target-name {
  display: inline-block;
  max-width: 100px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  vertical-align: bottom;
}

/* Bracket glyphs around the bar are plain text, not a bordered box. */
.status-hp-bar,
.status-target-hp-bar,
.status-xp-bar {
  letter-spacing: -1px; /* block glyphs have built-in side bearing; tightens the bar back to a solid strip */
}

/* Threshold-based coloring, deliberately theme-invariant like most game
   HUDs. Only the filled portion of the bar is colored; the unfilled
   portion is left at the surrounding --dim color, same as the bracket
   glyphs either side, so the eye reads "colored = filled" at a glance. */
.status-hp-fill.hp-good {
  color: var(--hp-good, #5bbf7a);
}
.status-hp-fill.hp-warn {
  color: var(--hp-warn, #e0c15c);
}
.status-hp-fill.hp-critical {
  color: var(--hp-critical, #e0685c);
}

/* Deliberately one constant color, not a good/warn/critical threshold swap
   like the HP fill above — XP progress isn't a danger signal, so there's
   no "bad" reading to warn about. */
.status-xp-fill {
  color: var(--xp-fill, #6c8ebf);
}

/* Wraps two mutually exclusive sub-states — an active combat opponent's
   name and HP, or this character's own death/respawn countdown — as
   separate sub-elements rather than one span whose text gets overwritten,
   so switching between the two can't leave a stray bracket or label behind
   from the other. --hp-warn rather than --dim/--accent, since this is an
   active, attention-worthy state regardless of which sub-state is
   showing. */
.status-target:not([hidden]) {
  color: var(--hp-warn, #e0c15c);
}

#command-input {
  font-family: 'JetBrains Mono', monospace;
  padding: 0;
  width: 100%;
  /* Without this, the padding/border below would add to width: 100% rather
     than counting inside it, rendering wider than the containing block. */
  box-sizing: border-box;
  /* 16px is the threshold below which iOS Safari auto-zooms an input on
     focus. Pinned explicitly rather than inherited, since body's own
     font-size is smaller and would trigger that zoom. */
  font-size: 16px;
  background: transparent;
  color: var(--fg, #d0d0d0);
  border: none;
  outline: none;
  /* Colors the blinking text-insertion caret itself, not just selection —
     the one part of native <input> cursor styling a browser exposes. */
  caret-color: var(--accent, #7fdbca);
}

/* Close button for a native <dialog>, anchored to its top-right corner. */
.modal-close-form {
  position: absolute;
  top: 0.75rem;
  right: 0.75rem;
  margin: 0;
}

.modal-close {
  width: 1.75rem;
  height: 1.75rem;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  font-size: 1.1rem;
  line-height: 1;
  background: transparent;
  border: 1px solid var(--border, #333333);
  border-radius: 2px;
  color: var(--fg, #d0d0d0);
  cursor: pointer;
}

.modal-close:hover,
.modal-close:focus-visible {
  border-color: var(--accent, #7fdbca);
  color: var(--accent, #7fdbca);
  outline: none;
}

/* Narrow viewports (phones, small tablets in portrait) — #app's 1rem
   padding plus a 1px border already fits down to ~360px content width
   without overflow, so this is a tightening/comfort pass, not a fix for
   actual clipping. */
@media (max-width: 480px) {
  body {
    padding: 0.5rem;
  }

  #app {
    padding: 0.5rem;
  }

  form:not([hidden]) {
    max-width: 100%;
  }

  .help-grid {
    grid-template-columns: 1fr;
  }
}
