/* The Activity's own look, and the first time this project has had one. The bot
   inherited Discord's typography and was budgeted to 28 columns inside a
   codeblock; none of that applies here. What carries over is the palette
   convention — RARITY_COLOR's white/green/blue/purple/orange — because a player
   arriving from anywhere else already knows what blue means. */

/* SAFE AREAS COME FROM DISCORD FIRST AND `env()` SECOND, AND BOTH HALVES ARE
   LOAD-BEARING. Discord injects `--discord-safe-area-inset-*` into the frame on
   mobile, which is the only source that knows about ITS chrome — the drag handle
   above the activity and the system nav below it — where `env()` knows only about
   the notch. `env()` is the fallback for iOS and for opening the page in a plain
   browser during development, and 0px is the fallback for a desktop client that
   injects neither. Written once here as four variables so no rule has to repeat
   the three-deep fallback. */
:root {
  --safe-top: var(--discord-safe-area-inset-top, env(safe-area-inset-top, 0px));
  --safe-right: var(--discord-safe-area-inset-right, env(safe-area-inset-right, 0px));
  --safe-bottom: var(--discord-safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
  --safe-left: var(--discord-safe-area-inset-left, env(safe-area-inset-left, 0px));

  color-scheme: dark;

  --bg: #16161c;
  --panel: #1f1f27;
  --panel-raised: #272733;
  --line: #34343f;
  --ink: #e8e8ef;
  --ink-dim: #9a9aab;

  --gold: #d8a54a;
  --hp: #c34a4a;
  --mana: #4a7fc3;
  --xp: #7a5fc3;

  /* The rarity ladder, unchanged from combat/ansi.js. */
  --common: #e8e8ef;
  --uncommon: #4fbf6a;
  --rare: #4a8fd8;
  --epic: #a05fd8;
  --legendary: #d8842a;

  --warrior: #d8a54a;
  --rogue: #4fbf6a;
  --mage: #4a8fd8;
}

* {
  box-sizing: border-box;
}

html,
body {
  /* The frame is the viewport on mobile and the page must not rubber-band past
     it: an overscroll on iOS detaches the content from Discord's own scroll and
     reads as the Activity coming loose. */
  overscroll-behavior: none;
}

body {
  margin: 0;
  /* No webfont. The iframe is on a cold cache every launch and a system stack
     costs nothing; a display face can come later behind a measured budget. */
  font-family:
    'Inter',
    system-ui,
    -apple-system,
    'Segoe UI',
    sans-serif;
  background: var(--bg);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;

  /* A LONG-PRESS ON A PHONE MUST NOT SELECT TEXT OR FLASH A GREY BOX. Both are
     browser defaults that read as a broken tap rather than as a deliberate
     gesture. Re-enabled for the one element where selecting text is the point. */
  -webkit-tap-highlight-color: transparent;
  -webkit-user-select: none;
  user-select: none;
  /* iOS inflates font sizes on rotation unless told not to. */
  -webkit-text-size-adjust: 100%;
}

.notice,
.stats {
  -webkit-user-select: text;
  user-select: text;
}

#app {
  max-width: 900px;
  margin: 0 auto;
  /* THE GUTTERS ARE THE SAFE AREA PLUS THE DESIGN'S OWN 16px, NOT THE LARGER OF
     THE TWO. A landscape phone puts the notch inside the left inset, so a
     max() would leave the first column of text against it. The bottom is the
     generous one: it clears the home-gesture strip AND gives the last card room
     to sit above Discord's own edge. */
  padding: calc(20px + var(--safe-top)) calc(16px + var(--safe-right))
    calc(48px + var(--safe-bottom)) calc(16px + var(--safe-left));
}

.status {
  color: var(--ink-dim);
  text-align: center;
  padding: 48px 16px;
  font-size: 15px;
}

.status.error {
  color: var(--hp);
}

header.account {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding-bottom: 14px;
  margin-bottom: 18px;
  border-bottom: 1px solid var(--line);
}

header.account h1 {
  font-size: 19px;
  font-weight: 650;
  margin: 0;
  letter-spacing: -0.01em;
}

header.account .purse {
  color: var(--gold);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  font-size: 15px;
}

.roster {
  display: grid;
  gap: 14px;
  /* One column on a phone, two where there is room. An Activity is played in a
     voice channel on both. */
  grid-template-columns: 1fr;
}

@media (min-width: 620px) {
  .roster {
    grid-template-columns: 1fr 1fr;
  }
}

.card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 16px;
}

.card header {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 14px;
}

.card .name {
  font-size: 17px;
  font-weight: 650;
  letter-spacing: -0.01em;
}

.card .klass {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 600;
}

.card .klass[data-class='warrior'] {
  color: var(--warrior);
}
.card .klass[data-class='rogue'] {
  color: var(--rogue);
}
.card .klass[data-class='mage'] {
  color: var(--mage);
}

.card .doing {
  margin-left: auto;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-dim);
  background: var(--panel-raised);
  border-radius: 999px;
  padding: 3px 9px;
}

/* The bars the PNG pipeline used to draw. Here they are two divs, which is the
   single clearest illustration of what the port buys: no palette grid, no dirty
   rectangles, no re-encode per beat. */
.meter {
  margin-bottom: 9px;
}

.meter .row {
  display: flex;
  justify-content: space-between;
  font-size: 11px;
  color: var(--ink-dim);
  margin-bottom: 4px;
  font-variant-numeric: tabular-nums;
}

.meter .track {
  height: 7px;
  border-radius: 4px;
  background: var(--panel-raised);
  overflow: hidden;
}

.meter .fill {
  height: 100%;
  border-radius: 4px;
  transition: width 220ms ease-out;
}

.meter[data-kind='hp'] .fill {
  background: var(--hp);
}
.meter[data-kind='mana'] .fill {
  background: var(--mana);
}
.meter[data-kind='xp'] .fill {
  background: var(--xp);
}

/* Every stat for every class, unconditionally — `/character info`'s rule, and
   the reason is unchanged: a zero that is shown reads as "cannot have it", a
   zero that is hidden reads as "the class lacks the stat". */
.stats {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2px 16px;
  margin-top: 14px;
  padding-top: 13px;
  border-top: 1px solid var(--line);
  font-size: 12.5px;
}

.stats div {
  display: flex;
  justify-content: space-between;
  padding: 2.5px 0;
}

.stats .label {
  color: var(--ink-dim);
}

.stats .value {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

.empty {
  color: var(--ink-dim);
  font-size: 14px;
  line-height: 1.55;
}

/* The hunt panel: what a character is doing, how close it is to the cap, and
   the one thing to do about it. */
.hunt {
  margin-top: 13px;
  padding-top: 13px;
  border-top: 1px solid var(--line);
}

.hunt .row {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  font-size: 12px;
  color: var(--ink-dim);
  margin-bottom: 6px;
}

.hunt .track {
  height: 7px;
  border-radius: 4px;
  background: var(--panel-raised);
  overflow: hidden;
  margin-bottom: 11px;
}

.hunt .fill {
  height: 100%;
  border-radius: 4px;
  background: var(--uncommon);
  transition: width 220ms ease-out;
}

/* PAST THE CAP NOTHING MORE IS BANKED, so a capped hunt is waste that has
   already started rather than an activity still running. Red says that in the
   one glance the panel gets. */
.hunt.capped .fill {
  background: var(--hp);
}

.hunt.capped .row {
  color: var(--hp);
}

button.primary {
  width: 100%;
  padding: 9px 14px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel-raised);
  color: var(--ink);
  font: inherit;
  font-size: 13.5px;
  font-weight: 600;
  cursor: pointer;
  /* 44px is Apple's floor for a comfortable touch target and Android's is 48dp;
     44 clears both once the row's own padding is counted, and an Activity is
     played on a phone as often as not. */
  min-height: 44px;
  /* No double-tap-to-zoom delay: a 300ms wait on the only button on the card
     reads as the tap not having registered. */
  touch-action: manipulation;
  transition:
    background 120ms ease,
    border-color 120ms ease;
}

/* HOVER ONLY WHERE THERE IS A POINTER. On a touch screen `:hover` sticks after
   a tap, so the last button pressed stays lit as though still focused — which is
   exactly the "controls work as expected for the platform" failure. The pressed
   state below is what a phone gets instead, and it is immediate. */
@media (hover: hover) and (pointer: fine) {
  button.primary:hover:not([disabled]) {
    background: #313140;
    border-color: #454555;
  }
}

button.primary:active:not([disabled]) {
  background: #3a3a4a;
  transform: translateY(1px);
}

button.primary[disabled] {
  opacity: 0.55;
  cursor: default;
}

/* What the last write said. The server composes these sentences — they are the
   same strings the slash commands reply with — so the client never writes its
   own account of what happened. */
.notice {
  margin: 18px 0 0;
  padding: 11px 14px;
  border-radius: 9px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-left: 3px solid var(--uncommon);
  font-size: 13px;
  line-height: 1.5;
  white-space: pre-wrap;
}

.notice.bad {
  border-left-color: var(--hp);
}

/* ── The fight ────────────────────────────────────────────────────────────────
   What replaced the PNG pipeline. The bot composes a palette image per beat —
   hand-rolled colour-type-3 encoder, cached base plane, reported dirty
   rectangles, a bitmap font for the damage numbers — because uploading a picture
   is its only way to show one. Here it is divs, repainted for free. */
.fight {
  margin-top: 13px;
  padding-top: 13px;
  border-top: 1px solid var(--line);
}

.fight .sides {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
  margin-bottom: 10px;
}

.fight .who {
  font-size: 12px;
  font-weight: 600;
  color: var(--ink-dim);
  margin-bottom: 5px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.fight .bar {
  height: 8px;
  border-radius: 4px;
  background: var(--panel-raised);
  overflow: hidden;
}

.fight .bar .fill {
  height: 100%;
  background: var(--uncommon);
  /* Faster than the beat period, so the bar has settled before the next event
     lands. A transition longer than a beat reads as lag rather than motion. */
  transition: width 180ms linear;
}

.fight .bar.foe .fill {
  background: var(--hp);
}

.fight .num {
  margin-top: 4px;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--ink-dim);
}

/* THE FLINCH. A scene shows both figures on one frame, which is why the bot
   draws the blow and the flinch together; with no figures to draw, the side that
   took the hit is what moves. */
.fight .side.hit {
  animation: flinch 200ms ease-out;
}

@keyframes flinch {
  0% {
    transform: translateX(0);
  }
  40% {
    transform: translateX(3px);
  }
  100% {
    transform: translateX(0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .fight .side.hit {
    animation: none;
  }
  .fight .bar .fill {
    transition: none;
  }
}

/* ── The scene ───────────────────────────────────────────────────────────── */

/* THE SAME ART THE BOT DRAWS, AT THE SAME PROPORTIONS. `aspect-ratio` pins the
   stage to the bot's 720x380 canvas, which is what lets `scene.js` place every
   figure as a FRACTION of that canvas using the manifest's own anchor and scale.
   Sized by percentage rather than by pixels, so the picture is whatever width the
   phone gives the card and the geometry never has to be re-derived. */
.stage {
  position: relative;
  aspect-ratio: 720 / 380;
  width: 100%;
  overflow: hidden;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: #80c2ce; /* layer5's own top row, so a slow backdrop is not black */
  margin-bottom: 9px;
}

/* Pixel art, scaled up. Smoothing softens every edge in it, which is the same
   reason the bot's compositor places layers by corner instead of resampling. */
.stage img {
  image-rendering: pixelated;
  -webkit-user-select: none;
  user-select: none;
  pointer-events: none;
}

/* A FLOATING NUMBER, AND IT IS THE ONE THING HERE THE BOT CANNOT DO CHEAPLY:
   its version is a 13x15 bitmap font recoloured by an exact three-colour palette
   swap, with a FIXED advance so a number does not slide sideways when the damage
   crosses 99. `tabular-nums` buys the same property for free. The palettes are
   the scene's own: orange for a crit, purple for poison (never green — green is
   what every game a player arrived from uses for healing), blue for a ward
   absorb, and the coolest, flattest one for a dodge, which is the only popup
   that is a word. */
.stage .popup {
  position: absolute;
  top: 12%;
  transform: translateX(-50%);
  font-size: clamp(16px, 5.5vw, 30px);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  color: var(--ink);
  text-shadow:
    0 2px 0 rgba(0, 0, 0, 0.55),
    0 0 6px rgba(0, 0, 0, 0.4);
  animation: rise 420ms ease-out both;
}

.stage .popup.crit {
  color: var(--legendary);
  font-size: clamp(20px, 7vw, 38px);
}
.stage .popup.poison {
  color: #bb66dd;
}
.stage .popup.absorbed {
  color: #66aaee;
}
.stage .popup.burn {
  color: #ee8844;
}
.stage .popup.dodge {
  color: #cdd6e0;
  font-size: clamp(13px, 4vw, 22px);
  letter-spacing: 0.08em;
}

@keyframes rise {
  from {
    opacity: 0;
    transform: translate(-50%, 10px);
  }
  to {
    opacity: 1;
    transform: translate(-50%, -6px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .stage .popup {
    animation: none;
  }
}

.fight .narration {
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ink-dim);
  /* Fixed height, so the card does not jump as lines arrive and leave — the
     same reason the bot's damage numbers use a FIXED advance rather than a
     proportional one. */
  min-height: 74px;
}

.fight .payout {
  margin-top: 8px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--gold);
}

.fight .tally {
  margin: 9px 0 11px;
  font-size: 11.5px;
  color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
}

/* ── Choosing a creature ─────────────────────────────────────────────────── */
.picker {
  margin-top: 13px;
  padding-top: 13px;
  border-top: 1px solid var(--line);
}

.picker .row {
  font-size: 12px;
  color: var(--ink-dim);
  margin-bottom: 8px;
}

.picker .monsters {
  display: grid;
  gap: 6px;
  margin-bottom: 10px;
}

button.monster {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  padding: 8px 12px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel-raised);
  color: var(--ink);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  touch-action: manipulation;
}

/* LOCKED IS SHOWN, NOT HIDDEN. A ladder with holes in it does not read as a
   ladder: a player should see what is next rather than wonder if it exists. */
button.monster[disabled] {
  opacity: 0.4;
  cursor: default;
}

button.monster .mlevel {
  font-variant-numeric: tabular-nums;
  color: var(--ink-dim);
  font-size: 12px;
}

button.primary.subtle {
  background: transparent;
  font-weight: 500;
  color: var(--ink-dim);
  margin-top: 6px;
}
