/* theme-nethack.css — glyph color classes, status line, message log, panel chrome */

/* Glyph / text color classes — drive every color decision in the UI */
.g-fg      { color: var(--fg); }
.g-dim     { color: var(--dim); }
.g-gold    { color: var(--gold); }
.g-green   { color: var(--green); }
.g-red     { color: var(--red); }
.g-cyan    { color: var(--cyan); }
.g-yellow  { color: var(--yellow); font-weight: bold; }
.g-magenta { color: var(--magenta); }
.g-blue    { color: var(--blue); }

/* Hotkey letters — always yellow + bold */
.key, .hot { color: var(--yellow); font-weight: bold; }

/* Hotkey hint rendered as a tappable button (`<button class="hot hotkey-btn">`).
   Visually identical to the inline `<span class="hot">[X]</span>` it replaces,
   so the desktop terminal aesthetic doesn't shift. On touch, it gives the
   player a real ~24px tap target for the in-dialog command. The global
   click-dispatch (app-adventurer.js bindClickDispatch) reads `data-key` and
   synthesizes a keydown, so the dialog's existing keyboard handler picks it
   up — no per-dialog click wiring needed. */
button.hotkey-btn {
  background: transparent;
  border: 0;
  padding: 2px 1px;
  margin: 0;
  font: inherit;
  color: var(--yellow);
  font-weight: bold;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  text-transform: none;
  letter-spacing: 0;
}
button.hotkey-btn:active {
  background: var(--selected-bg);
}
/* Row container that wraps hotkey buttons — small line-height adjustment
   so tapped buttons don't overlap on touch zoom. */
.hotkey-row {
  line-height: 1.6;
}
@media (pointer: coarse) {
  button.hotkey-btn {
    padding: 4px 4px;
    margin: 0 1px;
    border: 1px solid var(--dim);
    background: rgba(20, 20, 20, 0.85);
  }
}

/* Frame chars (box-drawing) — dim by default */
.frame { color: var(--dim); }

/* "--More--" tag */
.more { color: var(--dim); }

/* Sold-out / expired suffix */
.sold { color: var(--red); }

/* Selected row */
.selected { background: var(--selected-bg); }

/* Cursor-highlighted row (arrow-key navigation). Distinct from `.selected`
   so a hover and a cursor highlight can co-exist visually. */
.row.highlighted {
  background: var(--selected-bg);
  outline: 1px solid var(--yellow);
}

/* Status line at the top of every page — two-row HUD with HP/MP
   bars and grouped sections. The 78ch column is set on the parent in
   layout.css so the status mirrors the dungeon grid width below. */
.status-line {
  color: var(--fg);
  margin-bottom: 6px;
  border-bottom: 1px solid var(--dim);
  padding-bottom: 4px;
  width: 78ch;
  max-width: 100%;
}
.status-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px;
  padding: 1px 4px;
  line-height: 1.4;
}
.status-row-main  { font-weight: bold; }
.status-row-world { font-size: 0.92em; color: var(--fg); }

.status-id { display: inline-flex; align-items: baseline; gap: 6px; }
.status-id .player { color: var(--yellow); font-weight: bold; font-size: 1.1em; }
.status-id .status-name { color: var(--fg); }
.status-id .status-role { color: var(--dim); font-weight: normal; }
.status-id .status-lvl  { color: var(--dim); font-weight: normal; }

.status-vitals { display: inline-flex; align-items: center; gap: 10px; flex: 1 1 auto; min-width: 0; }
.vital { display: inline-flex; align-items: center; gap: 4px; min-width: 0; }
.vital-label { color: var(--dim); font-size: 0.85em; font-weight: normal; letter-spacing: 1px; }
.vital-bar {
  position: relative;
  display: inline-block;
  width: 12ch;
  height: 1.1em;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--dim);
  overflow: hidden;
  vertical-align: middle;
}
.hp-bar-fill, .mp-bar-fill {
  position: absolute;
  left: 0; top: 0; bottom: 0;
  transition: width 180ms ease-out;
}
.hp-bar-fill          { background: rgba(0, 192, 96, 0.42); }
.hp-bar-fill.hp-wounded { background: rgba(224, 192, 64, 0.45); }
.hp-bar-fill.hp-critical {
  background: rgba(224, 64, 48, 0.55);
  animation: hp-pulse 1.1s ease-in-out infinite;
}
@keyframes hp-pulse {
  0%, 100% { opacity: 1.0; }
  50%      { opacity: 0.55; }
}

/* Low-HP vignette — radial darkening that intensifies as HP drops
   below ~40%. Opacity is set dynamically by `ui.renderStatusLine` from
   the current HP/maxHp ratio so the effect is data-driven (no extra
   subscriber). Pinned to viewport, pointer-events none so it never
   eats a click. Sits below the hit-flash (z-index 50) but above the
   FPS overlay so it darkens both the dungeon and the touch controls
   when the player is genuinely in danger. */
.hp-vignette {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 45;
  background: radial-gradient(ellipse at center,
              rgba(224, 64, 48, 0) 40%,
              rgba(224, 64, 48, 0.45) 80%,
              rgba(160, 24, 16, 0.65) 100%);
  opacity: 0;
}
.hp-vignette.hp-vignette--critical {
  animation: hp-vignette-pulse 1.4s ease-in-out infinite;
}
@keyframes hp-vignette-pulse {
  0%, 100% { filter: brightness(1.0); }
  50%      { filter: brightness(1.25); }
}
.mp-bar-fill { background: rgba(64, 96, 192, 0.50); }
.vital-num {
  position: relative;
  display: block;
  text-align: center;
  font-weight: bold;
  font-size: 0.92em;
  color: var(--fg);
  text-shadow: 0 0 2px rgba(0, 0, 0, 0.9);
  z-index: 1;
  line-height: 1.1em;
}

.status-extras { color: var(--fg); font-weight: normal; }
.status-tags { color: var(--yellow); }
.status-tags .tag { color: var(--yellow); }

.status-world  { display: inline-flex; align-items: baseline; gap: 8px; flex: 1 1 auto; }
.status-combat { display: inline-flex; align-items: baseline; gap: 10px; color: var(--dim); }
.status-money  { color: var(--gold); font-weight: bold; }
.biome-name    { color: var(--cyan); }

.status-line .gold-num  { color: var(--gold); }
.status-line .floor-num { color: var(--cyan); font-weight: bold; }
.status-line .delta     { color: var(--gold); }

/* Message log at the bottom of every page. The body is scrollable so users
   can read back through the persisted history (capped at 1024 entries in
   localStorage). New entries auto-scroll the viewport to the bottom.
   Head and foot are static; only `.log-mid` scrolls so the box-drawing
   border stays in place. Width is fixed to PANEL_WIDTH (78ch) so the box
   lines up exactly with `.panel` rows above; the scrollbar lives inside
   that column. */
.message-log {
  white-space: pre;
  margin: 0;
  padding: 0;
  width: 78ch;
}
.log-head, .log-foot { white-space: pre; line-height: 1; }
/* Mid: long messages wrap onto continuation rows. The left border is
   a CSS rule on `.log-mid` itself so the vertical edge is continuous
   across wrap rows (and visually connects the `┌` and `└` corners
   drawn by the head/foot rows). Interior padding gives the text some
   breathing room from the border characters above, below, and on
   the left. `overflow-wrap: break-word` handles pathological cases
   like a giant unbroken token. Height auto-scales with the viewport. */
.log-mid {
  overflow-y: auto;
  overflow-x: hidden;
  border-left: 1px solid var(--dim);
  padding: 4px 10px 4px 10px;
  scrollbar-width: thin;
  scrollbar-color: var(--dim) var(--bg);
  transition: max-height 120ms ease-out;
}
/* Collapsed: a 2-line ticker. Players can still scroll within the
   2-line window to see the most recent few messages; the expand
   chip surfaces the full scrollback when needed. */
.log-mid--collapsed { max-height: 2.6em; }
/* Expanded: the previous full-height log. Same calc as before so
   players who used to rely on the expanded view get the exact same
   feel after clicking the chip. */
.log-mid--expanded { max-height: min(14em, max(4em, calc(100vh - 540px))); }
.log-line {
  white-space: pre-wrap;
  overflow-wrap: break-word;
  line-height: 1.2;
}
.log-mid::-webkit-scrollbar { width: 8px; }
.log-mid::-webkit-scrollbar-track { background: var(--bg); }
.log-mid::-webkit-scrollbar-thumb { background: var(--dim); }
.log-mid::-webkit-scrollbar-thumb:hover { background: var(--fg); }

/* Expand / collapse chip in the log header. Sits at the right edge
   of the title row so it doesn't overlap any log content. The button
   inherits the monospace font + neutral colors of the dialog system. */
.log-head { position: relative; }
.log-toggle {
  position: absolute;
  right: 4px;
  top: 0;
  background: #000;
  color: var(--yellow);
  border: 1px solid var(--dim);
  font: inherit;
  padding: 0 6px;
  cursor: pointer;
  line-height: 1.1;
}
.log-toggle:hover { color: var(--fg); border-color: var(--fg); }

/* Panels — content rendered as monospace pre-formatted text */
.panel {
  white-space: pre;
  margin-bottom: 14px;
}
.panel.active .frame { color: var(--fg); } /* active uses brighter frames */

/* A row in a letter-keyed list — hover/select cue.
   `display: inline` is required because rows live inside a `white-space: pre`
   panel where the surrounding "\n" characters provide the line breaks. Forcing
   block here would split each panelLine into three rendered lines and orphan
   the leading/trailing border characters. */
.row {
  display: inline;
  cursor: pointer;
}
.row:hover { background: var(--selected-bg); }
.row.disabled { cursor: not-allowed; }
.row.disabled:hover { background: transparent; }

/* Inline letter-keyed action button (e.g., [+] [-] [d]rop) */
.act {
  color: var(--yellow);
  font-weight: bold;
  cursor: pointer;
  user-select: none;
}
.act:hover { background: var(--selected-bg); }

/* Receipt — bordered active panel */
.receipt {
  white-space: pre;
  margin: 12px 0;
}

/* Toolbar (filters / nav links) above panels */
.toolbar {
  white-space: pre;
  margin-bottom: 10px;
}
.toolbar select {
  margin: 0 2px;
}

/* Empty-state line */
.empty {
  color: var(--dim);
  white-space: pre;
}
.empty .slime { color: var(--green); }

/* Dungeon grid — pre-formatted block of monospace cells, one char each.
   A subtle dim border + inner padding makes the viewport read as a
   discrete "scrying window" rather than a bare wall of text. The
   border color picks up the room's torchlight a touch via the inset
   shadow. */
.dungeon-grid {
  white-space: pre;
  line-height: 1;
  margin: 0 auto 10px;
  width: 60ch;
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
  user-select: none;
  border: 1px solid var(--dim);
  padding: 8px 10px;
  background: #050505;
  box-shadow: inset 0 0 12px rgba(255, 208, 64, 0.04);
  /* `manipulation` keeps single-tap (move) + flick-pan working but
     disables double-tap-to-zoom — mobile players were accidentally
     zooming the page when tapping fast on adjacent cells. */
  touch-action: manipulation;
}

/* Optional WebGPU 3D viewport — same column as the 2D grid. */
.view3d-canvas {
  display: block;
  margin: 0 auto 14px;
  width: 78ch;
  height: 460px;
  background: #000;
  border: 1px solid var(--dim);
  touch-action: manipulation;
}

/* Hit-flash overlay + screen shake on player damage. The veil is full-screen
   fixed-position; the shake animates the page wrapper briefly. */
@keyframes dss-hitflash {
  0%   { background: rgba(224, 64, 48, 0.30); opacity: 1; }
  100% { background: rgba(224, 64, 48, 0); opacity: 0; }
}
.hit-flash {
  position: fixed; inset: 0;
  pointer-events: none;
  z-index: 50;
  animation: dss-hitflash 320ms ease-out forwards;
}
@keyframes dss-shake {
  0%   { transform: translate(0, 0); }
  20%  { transform: translate(-3px, 2px); }
  40%  { transform: translate(2px, -3px); }
  60%  { transform: translate(-2px, -1px); }
  80%  { transform: translate(2px, 2px); }
  100% { transform: translate(0, 0); }
}
@keyframes dss-shake-mild {
  0%   { transform: translate(0, 0); }
  25%  { transform: translate(-1px, 1px); }
  50%  { transform: translate(1px, -1px); }
  75%  { transform: translate(-1px, 0); }
  100% { transform: translate(0, 0); }
}
@keyframes dss-shake-heavy {
  0%   { transform: translate(0, 0); }
  10%  { transform: translate(-6px, 4px); }
  20%  { transform: translate(5px, -5px); }
  30%  { transform: translate(-4px, 6px); }
  40%  { transform: translate(6px, 3px); }
  55%  { transform: translate(-5px, -4px); }
  70%  { transform: translate(4px, 5px); }
  85%  { transform: translate(-3px, -2px); }
  100% { transform: translate(0, 0); }
}
.shake          { animation: dss-shake       240ms ease-in-out; }
.shake.shake--mild  { animation: dss-shake-mild  180ms ease-in-out; }
.shake.shake--heavy { animation: dss-shake-heavy 360ms ease-in-out; }

/* Hitstop pulse on the 2D ASCII grid. A brief flashbulb desaturation +
   inverse contrast holds the frame visually for ~90ms, mirroring the
   3D camera's peak-hold shake during the same window. Triggered by
   `ui.hitFlash` for heavy hits + kills. */
@keyframes dss-hitstop {
  0%   { filter: brightness(2.4) saturate(0); }
  60%  { filter: brightness(1.8) saturate(0.2); }
  100% { filter: brightness(1) saturate(1); }
}
.dungeon-grid.hitstop {
  animation: dss-hitstop 130ms ease-out;
}

/* Threat telegraph — an awake non-friendly monster adjacent to the
   player gets a subtle red ring/background so new players see "this
   thing will hit me next turn" without reading AI state. Pulses gently
   so it reads as a warning rather than a solid block of red that would
   compete with the actual damage flash. Applied alongside the regular
   color class so the glyph keeps its species color (yellow kobold etc.)
   over the red threat aura. */
@keyframes dss-threat-pulse {
  0%   { background: rgba(224, 64, 48, 0.18); }
  50%  { background: rgba(224, 64, 48, 0.32); }
  100% { background: rgba(224, 64, 48, 0.18); }
}
.dungeon-grid span.threat {
  animation: dss-threat-pulse 1100ms ease-in-out infinite;
  outline: 1px solid rgba(224, 64, 48, 0.45);
  outline-offset: -1px;
}

/* Torch flicker. Mounted-torch glyphs in 2D get this class so the
   wall-light reads as living fire rather than a static `i`. The
   animation cycles through three brightness frames at ~360 ms total
   so the flicker is visible but not seizure-inducing. Per-glyph
   `animation-delay` (set inline from a hash of the torch id) keeps
   neighboring sconces out of sync. */
@keyframes dss-torch-flicker {
  0%   { color: var(--gold);   text-shadow: 0 0 2px var(--gold); }
  50%  { color: var(--yellow); text-shadow: 0 0 4px var(--yellow); }
  100% { color: var(--gold);   text-shadow: 0 0 2px var(--gold); }
}
@keyframes dss-torch-flicker-red {
  0%   { color: var(--red);    text-shadow: 0 0 2px var(--red); }
  50%  { color: var(--gold);   text-shadow: 0 0 4px var(--gold); }
  100% { color: var(--red);    text-shadow: 0 0 2px var(--red); }
}
@keyframes dss-torch-flicker-cyan {
  0%   { color: var(--cyan);   text-shadow: 0 0 2px var(--cyan); }
  50%  { color: var(--fg);     text-shadow: 0 0 4px var(--fg); }
  100% { color: var(--cyan);   text-shadow: 0 0 2px var(--cyan); }
}
@keyframes dss-torch-flicker-magenta {
  0%   { color: var(--magenta); text-shadow: 0 0 2px var(--magenta); }
  50%  { color: var(--fg);      text-shadow: 0 0 4px var(--fg); }
  100% { color: var(--magenta); text-shadow: 0 0 2px var(--magenta); }
}
.torch-flicker         { animation: dss-torch-flicker         360ms infinite ease-in-out; }
.torch-flicker-red     { animation: dss-torch-flicker-red     360ms infinite ease-in-out; }
.torch-flicker-cyan    { animation: dss-torch-flicker-cyan    360ms infinite ease-in-out; }
.torch-flicker-magenta { animation: dss-torch-flicker-magenta 360ms infinite ease-in-out; }

/* Torch-lit floor/wall cells: subtle brightness pulse so the LIGHT cast by
   a wall torch shimmers in place, not just the torch glyph itself. Pulled
   over the terrain color class via opacity so the underlying color stays
   intact. Per-cell `animation-delay` (set inline from a torch+cell hash)
   keeps neighboring cells out of sync. */
@keyframes dss-torch-lit {
  0%   { opacity: 1.00; }
  25%  { opacity: 0.86; }
  50%  { opacity: 0.96; }
  75%  { opacity: 0.80; }
  100% { opacity: 1.00; }
}
.torch-lit-cell { animation: dss-torch-lit 700ms infinite ease-in-out; }
/* Low-fuel light warning: pulse the player's `@` between yellow and
   red so the player notices their torch is about to die. Applied by
   dungeon-view when the equipped light is in its final 25% of fuel. */
@keyframes dss-light-low {
  0%   { color: var(--yellow); }
  50%  { color: var(--red);    }
  100% { color: var(--yellow); }
}
.light-low { animation: dss-light-low 600ms infinite ease-in-out; }

/* Floating damage numbers — spawned on every hit, drift up + fade.
   Anchored absolutely to body so 2D / 3D both work. The CSS animation
   handles motion, the JS removes the node after ~800ms. */
@keyframes dss-dmg-float {
  0%   { transform: translate(-50%, -10%) scale(1.10); opacity: 0; }
  10%  { transform: translate(-50%, -20%) scale(1.20); opacity: 1; }
  100% { transform: translate(-50%, -180%) scale(0.90); opacity: 0; }
}
.damage-number {
  position: fixed;
  z-index: 9;
  font: bold 16px 'IBM Plex Mono', 'Cascadia Mono', 'Consolas', ui-monospace, monospace;
  pointer-events: none;
  user-select: none;
  text-shadow: 0 0 4px #000, 0 0 4px #000;
  animation: dss-dmg-float 800ms ease-out forwards;
  white-space: nowrap;
}
.dmg-damage { color: var(--red); }
.dmg-crit   { color: var(--yellow); font-size: 22px; }
.dmg-heal   { color: var(--green); }
.dmg-miss   { color: var(--dim); font-style: italic; }
.dmg-mp     { color: var(--blue); }

/* ----- Player-state overlays ----------------------------------------
   Two full-screen fixed divs:
   - .state-fx-veil — flat-colored tint for status effects (poison =
     green, burn = orange, frozen = cyan, etc.). Multiple statuses stack
     (poison + burn → green-orange wash) but each is low-opacity so the
     game stays readable.
   - .state-fx-vignette — radial gradient pinned to the screen edges
     that darkens / colors them as HP drops.

   Both elements always exist; CSS classes are toggled to drive the
   effect. Idle classes produce 0-alpha results so the divs stay
   invisible until something happens.
*/
.state-fx-veil {
  position: fixed;
  inset: 0;
  z-index: 7;
  pointer-events: none;
  background: transparent;
  transition: background 360ms ease-out;
}
.state-fx-poison  { background: rgba(0, 200, 80, 0.13); }
.state-fx-burn    { background: rgba(220, 90, 20, 0.16); }
.state-fx-frozen  { background: rgba(120, 200, 255, 0.18); }
.state-fx-bleed   { background: rgba(220, 30, 30, 0.10); }
.state-fx-cursed  { background: rgba(60, 0, 60, 0.18); }
.state-fx-blessed { background: rgba(255, 230, 120, 0.10); }
.state-fx-blind   { background: rgba(0, 0, 0, 0.45); }
/* Multi-status combinations dominate — burn beats poison, frozen beats both. */
.state-fx-poison.state-fx-burn   { background: rgba(180, 140, 30, 0.16); }
.state-fx-frozen.state-fx-poison { background: rgba(80, 200, 200, 0.18); }

.state-fx-vignette {
  position: fixed;
  inset: 0;
  z-index: 8;
  pointer-events: none;
  background: transparent;
  transition: background 220ms ease-out;
}
@keyframes dss-vignette-pulse {
  0%, 100% { box-shadow: inset 0 0 80px 30px rgba(220, 30, 30, 0.55); }
  50%      { box-shadow: inset 0 0 120px 50px rgba(220, 30, 30, 0.75); }
}
.state-fx-low-hp {
  /* Soft red edge — "wounded". */
  box-shadow: inset 0 0 60px 18px rgba(220, 30, 30, 0.30);
}
.state-fx-near-death {
  /* Pulsing heavier red — "near death". */
  animation: dss-vignette-pulse 1.2s ease-in-out infinite;
}

/* Color-swatch radio group — used in the edit / add forms instead of a <select>.
   Each radio is hidden; the colored block character (█) acts as the visible
   target. The currently-selected swatch gets a yellow outline. */
.color-swatches {
  display: inline-flex;
  gap: 2px;
  vertical-align: middle;
}
.color-pick {
  cursor: pointer;
  padding: 2px 4px;
  user-select: none;
  display: inline-block;
  line-height: 1;
}
.color-pick input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
  width: 0;
  height: 0;
}
.color-pick .swatch {
  display: inline-block;
  font-weight: bold;
  outline: 1px solid transparent;
  padding: 0 1px;
}
.color-pick:hover .swatch {
  outline-color: var(--dim);
}
.color-pick input:checked + .swatch {
  outline-color: var(--yellow);
}
