/* ============================================================================
   THE COMBAT VIEW, COPIED FROM THE WIKI'S SIMULATE WINDOW.

   Lifted from ApogeaWiki/source/src/styles/atlas.css -- the `.pl-sim*` rules
   that draw openSim()'s arena -- with the class names kept.  Keeping them is
   the point: a rule here and the rule it came from can be diffed, and when the
   wiki restyles its simulator the change is findable rather than archaeology.

   THREE ADAPTATIONS, and they are the only ones.

   1. NOT A MODAL.  The wiki's `.pl-simback` is a full-screen overlay over the
      planner, because there the fight is a thing you open and close.  Here the
      fight IS the screen -- it replaces the tab strip for as long as the
      character is out -- so the overlay is dropped and `.pl-sim` sits in the
      page.  Every rule inside it is untouched.

   2. NO GRID.  The wiki's simulator moves six fighters around a tiled field;
      this server has no positions, so the `.pl-grid*` half is not copied.  What
      is copied is the two-column arena the wiki uses when the grid is off,
      which is the layout the idle fight actually has: you on the left, what you
      are hitting on the right.

   3. THE SPARKS ARE A RING, not the sprite's outline.  The wiki reads the
      figure's pixels with `contourOf()` and throws thirty sparks off the shape
      itself.  It cannot do that here: our creatures are drawn as a cell of a
      spritesheet through `background-position`, not as an `<img>` with pixels
      to sample.  A ring is the honest approximation and it is marked as one.

   The palette is the wiki's own, so the colours are the same colours.
   ========================================================================== */
.pl-sim {
  --ink: #0d1017;
  --panel: #151a24;
  --line: #2a3242;
  --text: #d6dde8;
  --dim: #7c8698;
  --torch: #f0a53c;
  --torch-d: #a56c1f;
  --pl-simfig: 230px;

  position: relative;
  display: flex;
  flex-direction: column;
  /* NO BOX.  The arena used to be a bordered card with its own copy of the
     scenery sitting inside the page. Now the PAGE carries the ground -- see
     setScene() in app.mjs -- and the fight is drawn straight onto it, so a
     border and a second background would be a window into the place you are
     already standing in. */
  overflow: hidden;
  color: var(--text);
  background: transparent;
}

/* NO GRADIENT.  The header used to sit on a black-to-transparent wash so the
   title read over any background. It reads perfectly well over its own text
   shadow, and the wash was a dark band across the top of a picture that is now
   the thing you are looking at. */
.pl-simhead {
  display: flex; align-items: center; gap: 10px;
  flex: none;
  /* OVER THE SCENE, NOT ABOVE IT. The arena is the whole screen now (see
     `.pl-simarena`), so the header is furniture floating on the picture rather
     than a band that pushes it down. Its own text shadow is what makes it
     readable over whatever it lands on. */
  position: relative; z-index: 3;
  /* ── AND IT STEPS INSIDE AN OPEN WINDOW ───────────────────────────────────
     > *"also make that panel slide off the inventory, just like the button to
     > return to town does"*

     The plate sits at the right-hand end of this row with `margin-left: auto`,
     which is the frame's right edge -- and the bag is a 276px window you keep
     open at that edge for the whole fight. So the one panel that answers "how
     is this trip going" spent every hunt underneath the one panel you opened
     to watch it fill.

     `--winR` is the open window's own footprint, published on `#game` by
     paintWindows(), and it is the SAME number `.walkwrap` reads one screen
     lower -- which is what Victoria's "just like the button to return to town"
     names. One fact, three readers, and no second guess at how wide a bag is.

     BOTH SIDES, because the title is furniture too and the equipment window
     opens on the left. The transition is the overlay's own .14s: with an open
     window the room changes shape, and a plate that teleported would read as a
     glitch rather than as the bag arriving. */
  padding: 10px calc(14px + var(--winR)) 10px calc(14px + var(--winL));
  transition: padding .14s ease;
}
.pl-simtitle { text-shadow: 0 2px 8px rgba(0,0,0,.95), 0 0 3px rgba(0,0,0,.9); }
/* NOT flex:1 any more -- the trip plate takes the slack with its own auto
   margin, so the title is as wide as the ground's name and no wider. */
.pl-simtitle { color: var(--torch); font-size: 20px; letter-spacing: .06em;
               text-transform: uppercase; white-space: nowrap; }

/* ── THE ARENA STOPS WHERE THE HUD STARTS ────────────────────────────────────
   The HUD used to be a ROW under the stage, so the arena's floor and the top of
   the hotbar were the same line and everything worked out. It floats over the
   art now -- which is what let the picture reach the bottom of the screen -- and
   the arena quietly grew a hundred and thirty pixels taller underneath it. Three
   things live on that floor and all three went under the hotbar: the fighters
   stand on it, the distance strip is pinned to it, and the log sits above that.

   THE INSET IS HERE, ON THE BODY, AND NOT ON THE ARENA. Padding the arena would
   move the figures and leave the strip and the log where they were: they are
   absolutely positioned, and `bottom: 14px` resolves against the arena's PADDING
   box, which padding does not move. Shortening the arena itself moves all three
   together, because all three are measured from its floor.

   `--hudh` is published by placeChat() -- see the note there. The fallback is a
   fight-sized cluster, so a first paint before the measurement lands is low
   rather than wrong. The extra 22px is air: a fighter whose head touches the
   hotbar is a fighter standing in it. */
/* ── AND THE INSET MOVED FROM THE BOX TO THE CAMERA ──────────────────────────
   > *"push the hunting scene all the way to the top and bottom, to cover the
   > same area that background picture once covered."*

   It used to be a padding on this element: the arena was a shorter box inside
   the page, and everything that stands on its floor -- the fighters, the plates,
   the log -- was kept clear of the hotbar by the box simply not reaching it.
   That is also what put a band of nothing across the top and bottom of the
   scene, which is the thing being complained about.

   So the arena fills the screen and the SAME inset is applied to the CAMERA
   instead: `A3.safe(top, bottom)` aims it at the middle of the band that is not
   spoken for, so the two fighters stand exactly where they used to while the
   ground reaches every edge. The numbers are measured from the header and the
   HUD rather than written twice -- see `armSafeBand` in app.mjs. */
.pl-simbody { flex: 1; min-height: 0; display: flex; flex-direction: column; }

/* THE BOTTOM BAND BELONGS TO THE GROUND STRIP. The figures used to stand in it:
   the strip is absolutely positioned at the arena's foot and the feet of both
   sprites were inside it, so the reach bands were drawn across somebody's
   boots. The padding is the strip's room, and nothing else is allowed in it. */
/* ── ONE BOX, WITH A GRID IN IT ─────────────────────────────────────────────
   It was `grid-template-columns: 1fr 1fr` -- the player pinned to the left half
   and the creature to the right, which is what a fight looks like when the only
   thing you can draw is two pictures side by side. The field is a canvas now
   and the two of them stand wherever the server says they are, so there are no
   columns to be pinned to and there is no bottom band to keep clear: the
   distance strip that owned those 54px is gone and its information is on the
   floor. */
.pl-simarena {
  /* THE WHOLE SCREEN. Absolute against `.pl-sim`, under the header and the HUD
     -- so the canvas, and every overlay that is placed by projecting a world
     position into it, share one box that is the size of the picture. */
  position: absolute; inset: 0; z-index: 0;
}
/* The canvas fills the arena and sits under everything. It is not the
   background -- the ground's photograph is still behind it and the grid is
   drawn with alpha so you can see the place you walked to. */
.pl-stagebox { position: absolute; inset: 0; z-index: 0; }
.pl-stage { display: block; width: 100%; height: 100%; }

/* ── A POINT, AND NOTHING ELSE ──────────────────────────────────────────────
   Where a fighter is, in screen pixels, for the effects that need somewhere to
   be thrown from. Zero-sized on purpose: `sparks` appends its ring inside one
   and `attackFx` measures one with getBoundingClientRect, and both want a
   centre rather than a box.

   NO ANIMATION AND NO OPACITY EVER GO ON THESE. The figure they replaced wore
   the lunge, the flinch and the death fade, and a spark appended inside
   something animating to `opacity: 0` played invisibly -- which is a bug that
   took weeks to find once. The models carry all of that now. */
.pl-anchor { position: absolute; left: 0; top: 0; width: 0; height: 0;
             pointer-events: none; z-index: 4; }

/* The nameplate, moved to its owner's head once a frame by follow(). */
.pl-plate { position: absolute; left: 0; top: 0; pointer-events: none;
            z-index: 5; will-change: transform; }

/* ── THE FIFTEEN WITH NO MODEL ──────────────────────────────────────────────
   Props, effects and unreleased things the extraction has no rig for. The
   wiki's flat picture stands on the tile instead, and it topples when it dies
   the way every creature used to -- which is still the right answer for a
   picture with no bones. */
/* ── AND THE PLACEMENT IS NOT THE PICTURE ───────────────────────────────────
   > *"it teleports to the top left corner of the screen on stagger/damaged"*

   TWO ELEMENTS, ONE TRANSFORM EACH. The outer box is where the creature IS --
   written as a transform by `follow()` thirty times a second -- and it carries
   no animation at all. The inner box is what the creature DOES: the flinch, the
   hop and the topple all live here.

   They cannot share an element. A running CSS animation outranks an inline
   style, so a flinch whose first keyframe says `transform: none` threw the
   placement away for as long as it ran and dropped the picture at 0,0. That is
   the whole of the reported bug, and it is why nothing below ever sets a
   transform on `.pl-billboard` itself. */
.pl-billboard { position: absolute; left: 0; top: 0; z-index: 3;
                pointer-events: none; }
.pl-billboard.hidden { display: none; }
.bbart { transform-origin: 50% 100%; }

/* ── A PICTURE WITH NO LEGS ─────────────────────────────────────────────────
   > *"it looks like a sliding sprite while walking"*

   Fifteen creatures have no rig, so there is no walk cycle to play and a flat
   picture dragged across the ground looks dragged. The hop is what a thing with
   no legs has instead: a beat and a half per second, a squash at the bottom of
   it, and it runs only while the arena says this creature is moving. Small on
   purpose -- it is meant to read as alive, not as a pogo stick. */
@keyframes plBillboardWalk {
  0%   { transform: translateY(0)     scaleY(1); }
  30%  { transform: translateY(-9px)  scaleY(1.04); }
  55%  { transform: translateY(0)     scaleY(0.95); }
  75%  { transform: translateY(-3px)  scaleY(1.02); }
  100% { transform: translateY(0)     scaleY(1); }
}
.bbart.bbwalk { animation: plBillboardWalk .62s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) { .bbart.bbwalk { animation: none } }

/* ── AND ONE ELEMENT ANIMATES ONE THING AT A TIME ───────────────────────────
   Only one `animation` declaration can win the cascade, so the hop, the flinch
   and the topple are in a queue rather than in a pile -- and the order they are
   written in IS the priority. All three weigh the same (two class names), so
   the last one declared wins, which is why the topple is at the bottom: a
   creature that dies mid-stride should fall over, not keep hopping.

   The flinch rules exist ONLY to outweigh the hop. Without them `.bbart.bbwalk`
   at two names beats `.pl-hurt-r` at one, and a staggered creature bobs
   politely instead of rocking. */
.bbart.pl-hurt-r { animation: plHurtR .32s ease-out; }
.bbart.pl-hurt-l { animation: plHurtL .32s ease-out; }
.bbart.dead { animation: plSimDie .7s ease-in forwards; }
/* ── THE PLATE STANDS WITH THE FIGHTER ───────────────────────────────────────
   The column used to let the figure take all the slack, which pinned the plate
   to the TOP of the arena -- the name, the creature's health and the two range
   stats floating at the top of the screen, a long way from the thing they were
   about. Bottom-aligned instead: the sprite sits on the ground and its plate is
   directly above its head, which is where a nameplate goes. */
.pl-simside { display: flex; flex-direction: column; gap: 6px; min-width: 0;
              align-items: center; justify-content: flex-end; }
/* The plate hangs above the row and must not be clipped by it. */
.pl-simrow { overflow: visible; }
/* ── A NAMEPLATE, NOT A PANEL ────────────────────────────────────────────────
   Three shapes, and the third is the one a game uses.

   It was two dashboards -- name, Health, Mana and two stats each, in bordered
   boxes over the top corners of the fight. Then it was two smaller bordered
   boxes standing above the fighters. Both are PANELS: a rectangle of interface
   sitting on top of a picture, with its own background so its text can be read
   against whatever is behind it.

   A GAME DOES NOT DO THAT. It writes the name straight onto the world and makes
   the LETTERS carry their own contrast -- bold, and outlined, so they read over
   grass, over stonework and over a lit roof without a slab behind them. That is
   what is here now: no background, no border, no blur. The bar keeps a frame,
   because a bar without a track is not a bar; everything else is text on the
   world.

   AND IT IS INSIDE THE ROW. `.pl-simrow` is the element that carries `--walk`,
   so a plate parented to it goes exactly where its fighter goes -- no second
   thing to keep in step, and no name floating over ground its owner left. */
/* STATIC INSIDE THE PLATE. It used to place itself against the row it hung
   over; the plate is the thing that is placed now, so the bars simply stack
   inside it. */
.pl-simbars {
  width: max-content; max-width: 300px;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  pointer-events: none;
}
/* THE OUTLINE IS THE BORDER SHE ASKED FOR, drawn on the glyphs rather than
   around a box: four hard shadows make a one-pixel keyline on every letter, and
   the two soft ones underneath sit it on the world. `paint-order` puts the
   stroke behind the fill so the letterforms stay their own weight instead of
   being eaten from the inside. */
.pl-simnm {
  color: #fff; font-size: 15px; font-weight: 800; line-height: 1.2;
  letter-spacing: .01em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-align: center;
  -webkit-text-stroke: 2.5px #05070b; paint-order: stroke fill;
  text-shadow: 0 1px 3px rgba(0,0,0,.95), 0 0 8px rgba(0,0,0,.85);
}
/* Whose name it is, said in colour rather than in a word: yours is the torch
   the rest of your interface is lit with, and the creature's is the red every
   other hostile number in this arena is written in. */
.pl-simP .pl-simnm { color: #ffd79a; }
.pl-simM .pl-simnm { color: #ff9d9d; }

/* ── AND EVERY OTHER BODY ON THE FIELD ───────────────────────────────────────
   > *"when a new entity enters the combat, it should have it's own health bar
   > above it, with it's stats, like a normal creatures, no health bars under
   > the player."*

   Your summon and the two the creature can bring. The same plate as the two
   above, in the same place -- over the head, moved by `follow()` -- and SMALLER
   ON PURPOSE: there can be three of these on screen at once and they are not
   the fight. The two that matter stay the two that are the biggest.

   THE COLOUR IS THE SIDE, which is the one thing a glance has to answer when
   four health bars are on screen: green is fighting for you, red is not. That
   rule was already written down for the row of bars this replaced; it is worth
   keeping in the place the bars actually moved to.

   THE SECONDS SIT AFTER THE BAR. A summon is the only body here with a clock,
   and how long it has left is what you would spend the mana again over. */
.pl-simE .pl-simbars { gap: 2px; }
.pl-simE .pl-simnm { font-size: 12px; -webkit-text-stroke: 2px #05070b; }
.pl-simE .pl-simbar { width: 104px; height: 10px; }
.pl-simE .pl-simbar b { font-size: 9px; line-height: 10px; }
.pl-simE .pl-simleft {
  font-variant-numeric: tabular-nums; font-size: 10px; font-weight: 700;
  text-align: center; color: #cfd6e4;
  -webkit-text-stroke: 2px #05070b; paint-order: stroke fill;
  text-shadow: 0 1px 2px rgba(0,0,0,.95);
}
.pl-simS .pl-simnm { color: #9fe3a8; }
.pl-simPack .pl-simnm { color: #ff9d9d; }
/* IT IS THE ONE BEING HIT. A ring rather than a flash: the plate says which of
   you the creature is angry at, which is the whole of the aggro rule made
   visible, and a plate that pulsed would be a plate you look at instead of the
   fight. */
.pl-simS.angry .pl-simbar { border-color: #d8a24a; box-shadow: 0 0 0 1px #d8a24a; }


.pl-simbar { position: relative; width: 150px; height: 13px;
             background: #0a0d13; border: 1px solid #05070b; border-radius: 3px;
             box-shadow: inset 0 1px 0 rgba(255,255,255,.09),
                         inset 0 -1px 0 rgba(0,0,0,.8);
             overflow: hidden; }
.pl-simbar i { position: absolute; inset: 1px auto 1px 1px; display: block;
               border-radius: 2px; transition: width .12s linear; }
.pl-simbar b { position: relative; display: block; text-align: center;
               font-size: 10.5px; line-height: 13px; font-weight: 700; color: #fff;
               text-shadow: 0 1px 2px rgba(0, 0, 0, .95);
               font-variant-numeric: tabular-nums; }
/* Same ramp as the HUD's -- see the note there. A creature's bar reads green at
   the top too: what the colour says is "how much of this is left", and that is
   the same question whichever side of the fight is asking it. */
.pl-simhp i { background: linear-gradient(var(--f0, #c4494f), var(--f1, #8e2b30)); }
.pl-simmp i { background: linear-gradient(#4a7fd0, #2a4f8e); }
.pl-simxp i { background: linear-gradient(#6ec06a, #3e7c3c); }

/* The figure is the burst's containing block. width:100% because the column
   centres its children: with the artwork missing the cell has no content and
   collapses to zero width, which put the fallback spark ring in the arena's
   corner instead of on the fighter. */
.pl-simfig { position: relative; flex: 1; width: 100%;
             display: flex; align-items: flex-end; justify-content: center; }
/* ── STANDING WHERE THE FIGHT SAYS YOU ARE ───────────────────────────────────
   The row carries the WALK and the figure inside it carries the lunge and the
   death, and that division is not tidiness: both of those animate `transform`,
   so a walk written on the figure would be overwritten by whichever animation
   ran last -- a creature that stopped mid-approach every time it swung.

   The transition is a little longer than the server's push interval (twice a
   second), so a step that lands slightly late glides rather than snapping. */
.pl-simrow { position: relative; flex: none; width: 100%;
             height: var(--pl-simfig);
             display: flex; align-items: flex-end; justify-content: center;
             transform: translateX(var(--walk, 0%));
             transition: transform .55s linear; }

/* ── AND WALKS WHILE IT IS MOVING ────────────────────────────────────────────
   A bob and a lean, on the SPRITE -- the one element in the stack that owns no
   other animation. Backing away leans the other way, which is the whole of the
   difference between advancing and retreating when you cannot see the feet.

   TWO SELECTORS, because the two sides are two kinds of element: your class art
   is an <img class="pl-simsprite"> and a creature is a <span class="sprite mob">
   positioned into the atlas. Styling only the first animated the player and left
   the creature sliding along like a chess piece. */
@keyframes plStep {
  0%   { transform: translateY(0)    rotate(0deg); }
  25%  { transform: translateY(-3px) rotate(-1.4deg); }
  50%  { transform: translateY(0)    rotate(0deg); }
  75%  { transform: translateY(-3px) rotate(1.4deg); }
  100% { transform: translateY(0)    rotate(0deg); }
}
.pl-simside.walking .pl-simsprite,
.pl-simside.walking .pl-simfig > .sprite {
  animation: plStep .52s ease-in-out infinite;
}
.pl-simside.walking.backward .pl-simsprite,
.pl-simside.walking.backward .pl-simfig > .sprite { animation-duration: .72s; }
/* The monster art is mirrored to face the fight, and an animation that sets
   `transform` would throw that mirroring away mid-step. So the flip is folded
   into the keyframes for that side rather than fought with. */
@keyframes plStepFlip {
  0%   { transform: scaleX(-1) translateY(0)    rotate(0deg); }
  25%  { transform: scaleX(-1) translateY(-3px) rotate(-1.4deg); }
  50%  { transform: scaleX(-1) translateY(0)    rotate(0deg); }
  75%  { transform: scaleX(-1) translateY(-3px) rotate(1.4deg); }
  100% { transform: scaleX(-1) translateY(0)    rotate(0deg); }
}
.pl-simside.walking .pl-simflip { animation-name: plStepFlip; }
@media (prefers-reduced-motion: reduce) {
  .pl-simside.walking .pl-simsprite,
  .pl-simside.walking .pl-simfig > .sprite { animation: none; }
  .pl-simrow { transition: none; }
}

.pl-simsprite { max-width: 100%; image-rendering: pixelated;
                filter: drop-shadow(0 6px 10px rgba(0, 0, 0, .55)); }
.pl-simP .pl-simsprite { max-height: var(--pl-simfig); }
.pl-simM .pl-simsprite { height: var(--pl-simfig); width: auto; object-fit: contain; }
/* Every piece of monster artwork is drawn facing right, which in an arena laid
   out player-left is facing away from the fight. */
.pl-simflip { transform: scaleX(-1); }
.pl-simnoart { font-size: 64px; color: var(--dim); opacity: .5; }

.pl-simP .pl-simfig.hit { animation: plSimLungeR .26s ease-out; }
.pl-simM .pl-simfig.hit { animation: plSimLungeL .26s ease-out; }
@keyframes plSimLungeR { 0% { transform: translateX(0) } 45% { transform: translateX(26px) } 100% { transform: translateX(0) } }
@keyframes plSimLungeL { 0% { transform: translateX(0) } 45% { transform: translateX(-26px) } 100% { transform: translateX(0) } }

/* ── floating numbers ────────────────────────────────────────────────────── */
/* ── ABOVE EVERYTHING IT IS ABOUT ────────────────────────────────────────────
   > *"the floating damage numbers are getting behind some creature models,
   > like the cube of doom."*

   They were, and only behind SOME: a rigged creature is drawn in the canvas
   (`z-index: 0`) and can never occlude anything, but the fourteen with no rig
   keep the wiki's flat sprite, which is DOM -- `.pl-billboard`, `z-index: 3`.
   `.pl-simnum` carries no z-index at all, and a positioned element at `auto`
   paints BELOW every positioned element with a positive one, whatever the DOM
   order says. So every number thrown at a Cube Of Doom went behind it.

   THE LAYER CARRIES IT AND NOT THE NUMBER. One rule, and everything that rises
   off a fighter rises together -- the damage, the words, the yell. Six, which
   is above the billboard (3), the sparks (4) and the nameplates (5): a number
   is the one thing on this screen that is worth reading over anything else, and
   it is gone in 1.6 seconds. */
.pl-simfloat { position: absolute; inset: 0; pointer-events: none; overflow: hidden;
               z-index: 6; }
/* THEY RISE FROM THE FIGHTER, NOT FROM THE MIDDLE OF THE SKY. 52% was the
   right height while the nameplates were pinned to the top of the arena; now
   that a plate sits above each fighter's head, a number starting there landed
   on top of the creature's name. Lower, so it leaves the body and passes the
   plate on the way up. */
/* TWENTY-SIX AND NOT THIRTY, and the element icon beside it is why. That icon
   is a 12px file and 24 is the nearest size it is sharp at (see `elementIcon`);
   a 30px number puts 21px of digit next to 18px of drawing, and a 26px one puts
   18 next to 18. Coming down was the cheaper half of the match -- the icon
   could not go to 30 without being resampled, and the numbers lose nothing at
   26, which is the size the heal, mana and xp floats have always been. */
.pl-simnum { position: absolute; top: 66%; font-size: 26px; font-weight: 700;
             text-shadow: 0 2px 3px rgba(0, 0, 0, .95);
             animation: plSimFloat 1.6s ease-out forwards; }
.pl-simnum.left { left: 26%; }
.pl-simnum.right { right: 26%; }
/* PINNED TO WHOEVER IT HAPPENED TO. `place()` writes left/top from the
   projected position of that fighter, so the percentage rules above are the
   fallback for a float thrown when nobody is on the field.

   CENTRED WITH `translate`, NOT WITH `transform`. The rise animation owns
   `transform` for the whole 1.6 seconds -- writing a translateX here would be
   overwritten on the first keyframe and the number would hang off to the right
   of its owner. `translate` is its own property and composes with it. */
.pl-simnum.pinned { right: auto; translate: -50% 0; }
.pl-simnum.dmg { color: #ff6b6b; }
.pl-simnum.out { color: #ffd9a0; }
.pl-simnum.spell { color: #b07fd9; }
.pl-simnum.true { color: #c79bff; }
.pl-simnum.heal { color: #6ee7a0; font-size: 26px; animation: plSimRise 1.8s ease-out forwards; }
.pl-simnum.mana { color: #7fb4f0; font-size: 26px; animation: plSimRise 1.8s ease-out forwards; }

@keyframes plSimFloat {
  0%   { opacity: 0; transform: translate(0, calc(var(--dy, 0px) + 10px)) scale(.8) }
  12%  { opacity: 1; transform: translate(var(--dx, 0), var(--dy, 0px)) scale(1.1) }
  62%  { opacity: 1 }
  100% { opacity: 0; transform: translate(var(--dx, 0), calc(var(--dy, 0px) - 64px)) scale(1) }
}
@keyframes plSimRise {
  0%   { opacity: 0; transform: translate(var(--dx, 0), var(--dy, 0px)) }
  15%  { opacity: 1 }
  60%  { opacity: .85 }
  100% { opacity: 0; transform: translate(var(--dx, 0), calc(var(--dy, 0px) - 96px)) }
}

/* ── sparks ──────────────────────────────────────────────────────────────── */
.pl-simspk { position: absolute; inset: 0; pointer-events: none; z-index: 4; }
.pl-simspk i {
  position: absolute; left: 50%; top: 50%; width: var(--s); height: var(--s);
  image-rendering: pixelated; border-radius: 1px;
  /* "both", not "forwards": the sparks are staggered by up to 130ms, and with
     forwards-only fill a delayed one sits raw at the box's origin until its
     turn rather than waiting where it was born. */
  animation: plSimSpark var(--t) steps(9, end) both; animation-delay: var(--d);
}
/* Bright, and glowing twice -- a tight core and a wide halo. At one soft shadow
   these read as dust against the blurred backdrop; the point is that they are
   the loudest thing in the arena for half a second. */
/* ── the arrival ─────────────────────────────────────────────────────────────
   A creature fades up and settles rather than blinking into existence. The
   duration comes from the fight itself (`spawnIn`) through a custom property,
   so the animation and the beat the monster's swing clock is held for are the
   same number rather than two that drift. */
@keyframes plSimSpawn {
  0%   { opacity: 0; transform: translateY(-14px) scale(.82); filter: brightness(2.4); }
  55%  { opacity: 1; transform: translateY(2px) scale(1.04);  filter: brightness(1.5); }
  100% { opacity: 1; transform: none;                          filter: none; }
}
.pl-simspawn { animation: plSimSpawn var(--spawn-in, .6s) ease-out both; }
@media (prefers-reduced-motion: reduce) { .pl-simspawn { animation: none } }

.pl-simspk-hurt i { background: #ffc9be; box-shadow: 0 0 5px 2px #ff7b6b, 0 0 20px 7px rgba(255, 90, 70, .9); }
.pl-simspk-heal i { background: #b9ffd4; box-shadow: 0 0 5px 2px #6ee7a0, 0 0 20px 7px rgba(110, 231, 160, .9); }
.pl-simspk-spawn i { background: #dbe6ff; box-shadow: 0 0 5px 2px #8fa9ff, 0 0 22px 8px rgba(143, 169, 255, .8); }
@keyframes plSimSpark {
  0%   { opacity: 0; transform: translate(var(--x0, 0), var(--y0, 0)) }
  10%  { opacity: 1 }
  62%  { opacity: 1 }
  100% { opacity: 0; transform: translate(var(--x1, 0), var(--y1, 0)) }
}
@media (prefers-reduced-motion: reduce) { .pl-simspk { display: none } }

/* ── the yell ────────────────────────────────────────────────────────────── */
.pl-simyell {
  position: absolute; left: 50%; bottom: calc(10px + var(--pl-simfig) + 6px);
  transform: translateX(-50%); white-space: nowrap; pointer-events: none; z-index: 5;
  font-size: 20px; line-height: 1; color: #ffe6a7;
  text-shadow: 0 2px 0 rgba(0, 0, 0, .92), 0 0 8px rgba(0, 0, 0, .95);
  animation: plSimYell 1.5s ease-out forwards;
}
.pl-simM .pl-simyell { color: #ffb383; }
@keyframes plSimYell {
  0%   { opacity: 0; transform: translate(-50%, 7px) scale(.86) }
  14%  { opacity: 1; transform: translate(-50%, 0) scale(1) }
  70%  { opacity: 1; transform: translate(-50%, -6px) scale(1) }
  100% { opacity: 0; transform: translate(-50%, -15px) scale(1) }
}

/* ── the spell strip ─────────────────────────────────────────────────────── */
.pl-simspells { flex: none; padding: 0 18px 4px; }
.pl-simsplist { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
                gap: 5px; max-height: 76px; overflow: auto; align-content: start; }
.pl-simsp { display: flex; align-items: center; gap: 7px; height: 30px; padding: 0 8px;
            font-size: 15px; background: rgba(21, 26, 36, .9); color: var(--text);
            border: 1px solid var(--line); border-radius: 6px; text-align: left; }
.pl-simsp.off { opacity: .55; }
.pl-simspn { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pl-simspc { color: #6fa8ff; font-size: 13px; flex: none; }
.pl-simspc.hp { color: #d98f8f; }
.pl-simspcd { color: var(--torch); font-size: 13px; flex: none; min-width: 30px; text-align: right; }

/* ── the foot: tally and log ─────────────────────────────────────────────── */
/* A SCRIM UNDER THE FOOTER, and it is the price of the sharp background. The
   arena used to sit on a blurred picture, which is a flat surface to write on;
   an unblurred one has grass, roof tiles and stonework behind the labels, and
   "THIS TRIP" over a lit roof is unreadable. The panels carry their own dark
   backgrounds -- what needed help is the space between and around them. */
/* ── ARRIVING SOMEWHERE ───────────────────────────────────────────────────
   > *"show a nice animation with something like 'You arrived at <hunting
   > ground>!' that slides in and out, before the first fight starts."*

   IN AND OUT THE SAME WAY YOU WALKED. The card enters from the left, holds
   still long enough to be read at a glance -- about a second and a half of a
   three-and-a-half second life, which lands inside the six seconds the fight
   holds the first creature back for -- and leaves to the right. The letters
   arrive spaced wide and tighten as it lands, which is what makes it read as a
   title card rather than as a tooltip that appeared.

   ONE TRANSFORM, WRITTEN ONCE. The centring is part of every keyframe rather
   than a separate declaration, for the same reason the damage numbers use
   `translate` instead: a running animation owns `transform` outright, and a
   centring written anywhere else is a centring that is thrown away the moment
   this starts. */
.pl-arrive {
  position: absolute; left: 50%; top: 24%; z-index: 6;
  pointer-events: none; white-space: nowrap;
  font-size: 24px; letter-spacing: .12em; text-transform: uppercase;
  color: #f3e7cf;
  text-shadow: 0 2px 10px rgba(0, 0, 0, .95), 0 0 4px rgba(0, 0, 0, .9);
  animation: plArrive 3.4s cubic-bezier(.2, .7, .25, 1) forwards;
}
/* THE PLACE IS THE POINT. "You arrived at" is the sentence and the ground is
   the news, so the news is bigger, brighter and in the colour the header uses
   for the same name. */
.pl-arrive .ark { font-size: 15px; opacity: .78; margin-right: .5em;
                  letter-spacing: .18em; }
.pl-arrive b { color: var(--torch); font-weight: 800; }
@keyframes plArrive {
  0%   { opacity: 0; transform: translateX(-50%) translateX(-70px);
         letter-spacing: .34em; }
  14%  { opacity: 1; transform: translateX(-50%) translateX(0);
         letter-spacing: .12em; }
  72%  { opacity: 1; transform: translateX(-50%) translateX(0);
         letter-spacing: .12em; }
  100% { opacity: 0; transform: translateX(-50%) translateX(70px);
         letter-spacing: .20em; }
}
/* Somebody who asked their machine to stop moving things still needs to be
   told where they are -- so it stops sliding, not appearing. It still fades
   out, which is also what keeps `animationend` firing: the card is removed on
   it, and an animation of `none` never ends. */
@media (prefers-reduced-motion: reduce) {
  .pl-arrive { animation: plArriveStill 3.4s linear forwards; }
  @keyframes plArriveStill {
    0%, 84% { opacity: 1; transform: translateX(-50%); }
    100%    { opacity: 0; transform: translateX(-50%); }
  }
}

/* ── THE COMBAT LOG, WHERE A GAME PUTS ITS CHAT ──────────────────────────────
   Bottom-left, over the ground, with no panel around it. It was a bordered box
   in a column of its own, which made a running commentary look like a document
   and cost the arena a third of its width to say so.

   NO SCROLLBAR AND NO BOX. The newest line is at the bottom, the oldest fade
   out at the top under a mask, and you read the last few and ignore the rest --
   which is the whole of what a combat log is for. The text carries its own
   shadow because it is written on grass and roof tiles, not on a panel. */
.huntlog {
  /* Lifted clear of the distance strip, which is `bottom: 14px` and about 40px
     tall with its pill. It used to be lifted to clear the CHAT TAB as well --
     the chat is a row of its own below the arena now and hangs over nothing --
     but the strip is still down there, and a log line long enough to reach the
     middle of the column would otherwise be written across it. */
  /* CLEAR OF THE HOTBAR BY ITS OWN MEASUREMENT, now that the arena reaches the
     bottom of the screen and no longer stops short of it for everybody. */
  position: absolute; left: 14px; bottom: calc(var(--hudh, 130px) + 34px); z-index: 4;
  transition: bottom .22s ease;
  width: min(340px, 40%); max-height: 132px; overflow: hidden;
  display: flex; flex-direction: column; justify-content: flex-end;
  pointer-events: none;
  font-size: 13px; line-height: 1.35;
  -webkit-mask-image: linear-gradient(transparent, #000 34%);
  mask-image: linear-gradient(transparent, #000 34%);
}
/* ── AND IT STEPS ASIDE FOR THE CHAT ─────────────────────────────────────────
   The chat is pinned to this column's bottom-left corner and the log is written
   in the same corner, so an open chat would be standing on the last two lines of
   it. Rather than making the chat smaller for a case that is not always true,
   the log moves for exactly as long as the chat is open: `:has` on the column
   asks the one question that decides it, and the transition matches the fold so
   the two move as one gesture. Both of them together still clear the distance
   strip at the arena's foot. */
.middle:has(.chat:not(.shut)) .huntlog {
  bottom: calc(var(--hudh, 130px) + var(--chatlift, 96px) - 62px);
}

.pl-simline { color: var(--text); padding: 0;
              text-shadow: 0 1px 2px #000, 0 0 5px rgba(0,0,0,.95); }
.pl-simline.in { color: #e0a0a0; }
.pl-simline.spell { color: #c39ce0; }
.pl-simline.regen { color: #7fbf8f; opacity: .85; }
.pl-simline.good { color: #5fbf6f; }
.pl-simline.bad { color: #e06a6a; }

/* ── HOW THE HUNT IS GOING, IN THREE NUMBERS ─────────────────────────────────
   Top-right of the arena, opposite the ground's name. It was a ten-row roster
   with a footer in a permanent column; the roster answers "what lives here",
   which you asked before you set off, and these three answer "how is it going",
   which is the question you keep asking. The roster is on the hover. */
/* In the header, between the ground's name and the clock -- see the note in
   app.mjs. Floated over the arena it landed on the creature's nameplate, which
   is the corner the creature's own column already owns. */
/* THE CLOCK IS ONE OF THE CELLS NOW. `.pl-simclock` was a node of its own
   pinned past the plate's right edge; it is `.hp-cell` number one, because
   elapsed is what the two rates on the far end are divided BY. Nothing here
   styles it specially -- a cell is a cell -- and `.pl-simclock` is gone from
   this file as well as from app.mjs, because a rule that styles nothing is the
   quietest kind of wrong. */
.huntplate {
  display: flex; align-items: center; gap: 14px; padding: 3px 12px;
  background: rgba(9,11,16,.55); border: 1px solid var(--line);
  border-radius: 7px; margin-left: auto;
}
.huntplate .hp-cell { text-align: center; }
.huntplate .hp-cell b { display: block; color: var(--text); font-size: 16px;
                        font-weight: 600; font-variant-numeric: tabular-nums; }
.huntplate .hp-cell span { color: var(--muted); font-size: 10px;
                           text-transform: uppercase; letter-spacing: .08em; }
.huntplate .hp-rate { color: var(--torch); font-size: 11px; max-width: 150px;
                      border-left: 1px solid var(--line); padding-left: 12px;
                      font-variant-numeric: tabular-nums; }
.rosterlist { display: grid; gap: 1px; margin-top: 4px; min-width: 170px; }
.rrow { display: flex; gap: 10px; font-size: 12px; }
.rrow .grow { flex: 1; min-width: 0; overflow: hidden;
              text-overflow: ellipsis; white-space: nowrap; }
.rrow .n { font-variant-numeric: tabular-nums; }
.rrow.none { color: var(--muted); }

@media (max-width: 760px) {
  .pl-sim { --pl-simfig: 150px; }
  /* The arena is `position: absolute; inset: 0` at every width now, so there is
     no padding here to shrink -- only the log, which still has to clear a
     hotbar that is shorter on a narrow screen. */
  .huntlog { width: 55%; bottom: calc(var(--hudh, 130px) + 18px); }
  .huntplate { gap: 9px; padding: 5px 8px; }
  .huntplate .hp-rate { display: none; }
}

/* ── ATTACK EFFECTS ──────────────────────────────────────────────────────────
   Three shapes, drawn between the figures. The STYLE is the server's answer
   (`attack.style`, from the weapon actually held), so a bow never slices. */
.pl-fx { position: absolute; pointer-events: none; z-index: 5; transform-origin: 0 50%; }

/* A blade arc, swept across the target. Two crossed strokes rather than one,
   because a single line reads as a scratch and a cut has a shape. */
.pl-fx-slice { width: 92px; height: 92px; transform-origin: 50% 50%; }
.pl-fx-slice::before, .pl-fx-slice::after {
  content: ""; position: absolute; inset: 0; border-radius: 50%;
  border: 3px solid transparent; border-top-color: #fdfbf2;
  filter: drop-shadow(0 0 6px rgba(255, 255, 255, .85));
  animation: plFxSlice .26s ease-out both;
}
.pl-fx-slice::after { animation-delay: .05s; opacity: .55; }
@keyframes plFxSlice {
  0%   { transform: rotate(-58deg) scale(.72); opacity: 0; }
  35%  { opacity: 1; }
  100% { transform: rotate(48deg) scale(1.12); opacity: 0; }
}

/* ── A BEAM: A THIN BRIGHT LINE THE WHOLE DISTANCE, THERE AND GONE ───────────
   > *"the attack animations (projectiles and the magic laser beam) should
   > always comes from the source to the target, right now it seems like it's
   > always shooting to the right"*

   AND THEY DID, AND IT IS THE SAME BUG AS THE ANCHORS. `attackFx` sets the
   angle as an INLINE transform, and a running CSS animation whose keyframes
   mention `transform` OVERRIDES the inline one for as long as it runs -- so a
   beam aimed at forty degrees was drawn by keyframes that said
   `rotate(var(--a, 0deg))`, `--a` was never set by anything, and every beam in
   the game came out flat and pointing right whoever fired it at whatever.

   The comment that used to sit here said the keyframes "must not fight it --
   they scale a child instead", which was the correct intention and not what the
   CSS did. So now NOTHING on this element animates `transform`: the parent
   fades, the child scales, and the inline rotation is the only transform on the
   line itself. That is the rule, and it is the second time it has been learnt:
   an element positioned by script must not be animated on the same property. */
.pl-fx-beam { height: 4px; margin-top: -2px; border-radius: 2px;
              background: linear-gradient(90deg, rgba(160,200,255,0), #cfe3ff 18%, #ffffff 55%, rgba(160,200,255,0));
              box-shadow: 0 0 10px 3px rgba(140, 190, 255, .8);
              will-change: opacity;
              animation: plFxBeamFade .22s ease-out both; }
@keyframes plFxBeamFade { 0% { opacity: 0 } 25% { opacity: 1 } 100% { opacity: 0 } }
.pl-fx-beam::before {
  content: ""; position: absolute; inset: 0; border-radius: 2px; background: inherit;
  animation: plFxBeamPulse .22s ease-out both;
}
@keyframes plFxBeamPulse {
  0%   { opacity: 0; transform: scaleY(.3); }
  30%  { opacity: 1; transform: scaleY(1.4); }
  100% { opacity: 0; transform: scaleY(.6); }
}

/* An arrow that crosses the gap. The shaft is the element line; the head is a
   small triangle riding it. */
.pl-fx-shoot { height: 2px; margin-top: -1px; }
.pl-fx-shoot::before {
  content: ""; position: absolute; top: -4px; left: 0; width: 26px; height: 10px;
  background: linear-gradient(90deg, rgba(255,255,255,0), #e8dcc0 60%, #fff);
  clip-path: polygon(0 45%, 70% 45%, 70% 0, 100% 50%, 70% 100%, 70% 55%, 0 55%);
  filter: drop-shadow(0 0 3px rgba(0, 0, 0, .8));
  animation: plFxShoot .4s linear both;
}
/* THE WHOLE GAP, not a hundred per cent of the arrowhead. `100%` of a 26px
   glyph is 26px whatever the distance was, which is why this used to twitch at
   the shooter's hand and stop. The length is written inline by `attackFx`,
   because only it knows how far apart they are.

   This is the FALLBACK now -- a real arrow flies in the scene for anything with
   a model behind it (see `shoot` in arena3d.mjs) -- and it is still the right
   answer for a shot with no arrow to fly. */
@keyframes plFxShoot {
  from { transform: translateX(0) }
  to   { transform: translateX(var(--reach, 26px)) }
}

@media (prefers-reduced-motion: reduce) { .pl-fx { display: none } }

/* ── GETTING HIT ─────────────────────────────────────────────────────────────
   A red blink and a knock away from the blow. Without it a hit is a number
   appearing, which reads as a spreadsheet rather than a fight. */
@keyframes plHurtR {
  0%   { filter: none; transform: none; }
  18%  { filter: brightness(1.6) sepia(1) saturate(6) hue-rotate(-28deg);
         transform: translateX(13px) rotate(3deg); }
  55%  { filter: brightness(1.2) sepia(.5) saturate(3) hue-rotate(-28deg);
         transform: translateX(-3px); }
  100% { filter: none; transform: none; }
}
@keyframes plHurtL {
  0%   { filter: none; transform: none; }
  18%  { filter: brightness(1.6) sepia(1) saturate(6) hue-rotate(-28deg);
         transform: translateX(-13px) rotate(-3deg); }
  55%  { filter: brightness(1.2) sepia(.5) saturate(3) hue-rotate(-28deg);
         transform: translateX(3px); }
  100% { filter: none; transform: none; }
}
.pl-hurt-r { animation: plHurtR .32s ease-out; }
.pl-hurt-l { animation: plHurtL .32s ease-out; }
@media (prefers-reduced-motion: reduce) { .pl-hurt-r, .pl-hurt-l { animation: none } }

/* A block is a shield taking a blade: white sparks, not red ones. */
.pl-simspk-block i { background: #fffdf2; box-shadow: 0 0 4px 2px #ffe9a8, 0 0 18px 6px rgba(255, 220, 130, .85); }

/* ── the element icon riding a damage number ─────────────────────────────── */
/* CENTRED ON THE DIGITS, not sat on the baseline. The icon is a 24px box with
   about three transparent pixels top and bottom, so the drawing is centred in
   it; `middle` puts that centre against the middle of the x-height, which is
   where the middle of a row of digits is. A baseline alignment would stand the
   whole box on the baseline and hang it a third of its height above the number.
   No width or height here on purpose -- `elementIcon` sets them, because it is
   the thing that knows what sizes the file is sharp at. */
.pl-simnum .elico { vertical-align: middle; margin-right: 5px;
                    image-rendering: pixelated;
                    filter: drop-shadow(0 1px 2px rgba(0,0,0,.9)); }
.pl-simnum.block { color: #ffe9a8; }
/* XP is not damage, not healing and not an element -- so it is the one
   float with no colour of its own. */
.pl-simnum.xp { color: #ffffff; font-size: 26px;
                animation: plSimRise 1.8s ease-out forwards; }

/* ── STAGGERED ──────────────────────────────────────────────────────────────
   > *"When a monster is staggered, show a floating word 'Staggered'."*

   A WORD, so it is built like one: smaller than a damage number, spaced out and
   in small capitals, so a glance separates it from the figures flying off the
   same body without having to read it. Amber because it is neither damage nor
   healing -- it is a THING THAT HAPPENED TO the creature, which is the same
   family as a block, and it shares that colour deliberately.

   It rises straight rather than arcing (`plSimRise`): the arc is what makes a
   damage number feel thrown off a body, and this is a state the creature is in
   rather than something that flew off it. */
.pl-simnum.stagger { color: #ffe9a8; font-size: 20px; font-weight: 800;
                     letter-spacing: .10em; text-transform: uppercase;
                     text-shadow: 0 0 10px rgba(255, 200, 90, .5),
                                  0 2px 3px rgba(0, 0, 0, .95);
                     animation: plSimRise 1.5s ease-out forwards; }

/* ── the log's icons ─────────────────────────────────────────────────────── */
.pl-simline .elico { vertical-align: -2px; margin-right: 5px; image-rendering: pixelated; }
.logico { display: inline-block; width: 13px; margin-right: 5px; text-align: center;
          font-size: 11px; vertical-align: 1px; }
.logico.xp { color: #7fd18c; }
.logico.loot { color: var(--torch); }

/* ── loot cards ──────────────────────────────────────────────────────────────
   Slide down, hold, fade. One card per item so three drops are three things
   you can read rather than three things in one place. */
/* ── AND THEY STEP ASIDE FOR AN OPEN BAG ─────────────────────────────────────
   > *"The loot tooltip that appears when a monster is defeated, should slide
   > out behind the inventory when the inventory is open, otherwise the player
   > will never see it."*

   The rail sits fourteen pixels from the right edge and the window column sits
   ten from it, so with the bag open the cards were drawn underneath it -- and a
   card that says what you just picked up, hidden behind the thing you picked it
   up into, is the one place it is guaranteed to be useless.

   `--winR` IS ALREADY THE ANSWER AND IT WAS ALREADY BEING SET. `paintWindows`
   writes it on `#game` whenever a window opens or closes -- the column's
   footprint, or `0px` -- and the panels, the chat and the hotbar all inset by
   it already. This is the fourth reader of one fact rather than a fifth copy of
   the pane width.

   TRANSITIONED, because the bag opens with a click and a card that teleported
   sideways mid-slide would read as a glitch rather than as making room. */
.lootrail { position: absolute; top: 84px; z-index: 6;
            right: calc(14px + var(--winR, 0px));
            transition: right .18s ease;
            display: flex; flex-direction: column; gap: 6px;
            pointer-events: none; align-items: flex-end; }
@media (prefers-reduced-motion: reduce) { .lootrail { transition: none; } }
.lootcard {
  display: flex; align-items: center; gap: 8px; padding: 5px 10px 5px 6px;
  background: rgba(13, 16, 23, .92); border: 1px solid var(--torch-d);
  border-radius: 7px; font-size: 13px; color: var(--text);
  box-shadow: 0 6px 20px rgba(0, 0, 0, .6);
  animation: plLoot 2.6s ease-out forwards;
}
.lootcard .lt b { font-weight: 500; }
.lootcard .q { color: var(--dim); }
@keyframes plLoot {
  0%   { opacity: 0; transform: translateY(-16px); }
  12%  { opacity: 1; transform: none; }
  76%  { opacity: 1; transform: none; }
  100% { opacity: 0; transform: translateY(10px); }
}
@media (prefers-reduced-motion: reduce) {
  .lootcard { animation: plLootStill 2.6s steps(1) forwards; }
  @keyframes plLootStill { 0%, 92% { opacity: 1 } 100% { opacity: 0 } }
}

/* ── the respawn clock ───────────────────────────────────────────────────────
   The gap between creatures used to be an empty arena, which reads as broken.
   A bar counting the wait down says "something is coming". */
/* Parented to the creature's ROW now, so it stands over the spot the last one
   died on and the next one will appear on -- rather than in the middle of the
   arena, which is the one place nothing is happening. Same offset the nameplate
   uses, because it is standing in for the nameplate while there is nobody to
   name. */
.pl-respawn { position: absolute; left: 0; top: 0; z-index: 5;
              width: 170px; text-align: center; pointer-events: none; z-index: 4; }
.pl-respawn .lbl { font-size: 12px; color: #d7dee9; letter-spacing: .1em;
                   font-weight: 700; text-transform: uppercase; margin-bottom: 4px;
                   text-shadow: 0 1px 3px #000, 0 0 7px rgba(0,0,0,.95); }
.pl-respawn .track { height: 6px; background: rgba(13,16,23,.85);
                     border: 1px solid var(--line); border-radius: 4px; overflow: hidden; }
.pl-respawn .track i { display: block; height: 100%; width: 0;
                       background: linear-gradient(90deg, var(--torch-d), var(--torch)); }

/* ── the gap between you ─────────────────────────────────────────────────────
   Eighty units apart is where a pull starts, and until the distance is inside
   somebody's Range nobody swings. Drawn rather than left to be inferred: two
   figures standing still and not attacking is the single most confusing thing
   a fight can show you.

   The two marks are the two reaches on the same scale, so "it can hit me and I
   cannot hit it" -- which the wiki allows on purpose, no slack added to Range
   -- is a picture instead of a surprise. */
/* ── THE GROUND BETWEEN YOU ──────────────────────────────────────────────────
   This used to be a slider -- a track, two tick marks and a dot -- which was
   correct and read as a form control. The marks had to be explained, and having
   been explained they still left you comparing two positions on a line to
   answer "can it hit me".

   It is the FLOOR now, seen from the side: your feet at the left edge, the
   creature standing at the gap, and each side's Range laid on the ground as a
   band. Where a band covers the other side's feet, that side can swing -- so
   the wiki's no-slack Range rule ("it can reach you and you cannot reach it")
   is a red band touching your toes while the green one falls short, rather than
   two numbers and an inference. */
/* ── HOW FAR APART, IN WORDS ────────────────────────────────────────────────
   All that survives of the strip. The floor says far or close by being a ruler;
   this says twenty-three, which is the number you compare against the Range on
   your sheet. It floats at the midpoint of the two of you, on the line the walk
   happens along, and it is lit in whichever colour is currently true: torch
   when you can reach it, red when it can reach you, both when both. */
.gpill {
  position: absolute; left: 0; top: 0; z-index: 5; pointer-events: none;
  display: flex; align-items: baseline; gap: 3px; padding: 2px 9px;
  background: var(--ink-2); border: 1px solid var(--line); border-radius: 999px;
  font-size: 11px; color: var(--dim); white-space: nowrap;
  will-change: transform;
}
.gpill.hidden { display: none; }
.gpill b { color: var(--text); font-size: 14px;
           font-variant-numeric: tabular-nums; }
.gpill.mine   { border-color: rgba(255,196,106,.75);
                box-shadow: 0 0 10px rgba(255,196,106,.28); }
.gpill.theirs { border-color: rgba(255,107,107,.75);
                box-shadow: 0 0 10px rgba(255,107,107,.28); }
.gpill.mine.theirs { border-color: rgba(255,150,90,.85); }
/* AND WHEN YOU ARE RUNNING, NEITHER OF THOSE IS THE STORY. `back` stops your
   attack, so a lit "you can reach it" border over a character who is not going
   to swing is the picture contradicting itself. Backing away wins. */
.gpill.fleeing, .gpill.fleeing.mine, .gpill.fleeing.mine.theirs {
  border-color: rgba(196,214,255,.55); box-shadow: none;
}

/* ── RUNNING ─────────────────────────────────────────────────────────────────
   On the plate, under the name, only while it is true. Same treatment as the
   name above it -- letters carrying their own contrast, no slab -- because it
   is a word about the person and not a control. */
.plflee {
  font: 400 9px var(--loud); letter-spacing: .16em; text-transform: uppercase;
  color: #cdd8ea;
  -webkit-text-stroke: 2.5px #05070b; paint-order: stroke fill;
  text-shadow: 0 1px 3px rgba(0,0,0,.95);
}
.plflee.hidden { display: none; }

/* ── RANGE AND MOVE SPEED, ON BOTH SIDES ─────────────────────────────────────
   The two stats the whole distance system runs on. Until now the only place
   either appeared was the attributes panel in town, so the answer to "why is it
   hitting me and I am not hitting it" lived on a different screen from the
   fight that raised the question. */
/* Inside the plate now, so it needs no shadow of its own -- the plate is the
   dark surface these were previously trying to be legible without. */
/* ON THE WORLD, like the name above it -- the plate has no background to be
   legible against any more, so these carry their own. */
.figstats { display: flex; gap: 10px; align-items: center; font-size: 11.5px;
            color: #cbd4e2;
            text-shadow: 0 1px 2px #000, 0 0 5px rgba(0,0,0,.95); }
.figstats .fs { display: flex; align-items: center; gap: 3px; }
.figstats .fs b { color: #fff; font-weight: 700;
                  font-variant-numeric: tabular-nums; }

/* THE MOVEMENT STRIP MOVED TO THE HUD. It used to sit above the Log in a
   column under the arena; it is the line above the action bar now, where the
   game keeps its second and smaller row of buttons. The rules live in
   client/css/app.css beside the rest of the HUD, because they are the same
   object as the slots under them and splitting one object across two files is
   how the two halves come to disagree about a border colour. */

/* ── the word beside the shield ──────────────────────────────────────────────
   A shield over a fighter says "something defensive happened"; the word says
   which. Still no number, because there is no number. */
.pl-simnum .blbl { font-size: 20px; margin-left: 4px; letter-spacing: .02em; }

/* ── dying ───────────────────────────────────────────────────────────────────
   A creature that dies topples and is gone, rather than turning translucent and
   standing in the arena for the six seconds until the next one. A quarter turn,
   a slump, a fade -- and `forwards` so it stays gone. */
@keyframes plSimDie {
  0%   { transform: rotate(0deg) translateY(0); opacity: 1; }
  35%  { transform: rotate(-14deg) translateY(4px); opacity: 1; }
  100% { transform: rotate(-82deg) translateY(26px); opacity: 0; }
}
/* SPECIFIC ENOUGH TO WIN.  `.pl-simM .pl-simfig.hit` is the lunge, at three
   classes; a bare `.pl-simdie` at one lost to it, so a creature killed on a
   swing kept lunging and never fell. Same weight, declared later. */
.pl-simM .pl-simfig.pl-simdie, .pl-simP .pl-simfig.pl-simdie {
  animation: plSimDie .75s cubic-bezier(.4, .05, .6, 1) forwards;
  transform-origin: 50% 92%;
}
/* Somebody who asked their machine to stop moving things still needs the
   creature to leave. */
@media (prefers-reduced-motion: reduce) {
  .pl-simM .pl-simfig.pl-simdie, .pl-simP .pl-simfig.pl-simdie {
    animation: none; opacity: 0;
  }
}

/* ── WHAT IS LEFT WHEN A CREATURE DIES, WHICH IS NOTHING ─────────────────────
   > *"by the end of the death animation the monster should be gone, alongside
   > with their health bar and stats."*

   The model dissolves over the last part of its own death clip and the plate
   goes with it, on the same delay and the same duration -- both handed to the
   class from the clip's own length, which is why there is no timing in here.
   A `transition` set inline is the whole mechanism; this is only the end
   state, and `pointer-events` so a name nobody can see cannot be hovered. */
.pl-plate.dying, .pl-billboard.dying { opacity: 0; pointer-events: none; }

/* ── WHAT IS RUNNING ON A FIGHTER ────────────────────────────────────────────
   > *"Let's add the fight buffs icons that we have on the simulator: If have a
   > movement speed buff icon, if have a attack speed buff icon, etc."*

   The wiki simulator's own strip, at its own numbers: 11x12 sprites drawn at a
   WHOLE multiple of themselves (22x24), which is the same rule the cooldown
   clock and the element icons follow -- a pixel sprite is only sharp where each
   source pixel becomes exactly four. `image-rendering: pixelated` finishes it.

   IT WRAPS. Ten seconds of a summoner's opening can put five marks on one
   fighter, and a strip that scrolled sideways under a nameplate would be a row
   whose last mark is the one you cannot see. */
.fxstrip { display: flex; flex-wrap: wrap; gap: 3px; margin-top: 4px;
           align-items: center; }
.fxstrip:empty { display: none; }
.fxi { position: relative; display: inline-flex; line-height: 0; cursor: help; }
.fxi img { image-rendering: pixelated; display: block; }
/* THE SECONDS SIT IN THE CORNER, over the art rather than beside it: a row of
   icons each with a number after it is twice as wide and reads as a list. The
   shadow is what keeps a digit legible over eleven pixels of anything. */
.fxi b { position: absolute; right: -2px; bottom: -3px;
         font-size: 11px; font-weight: 700; line-height: 1;
         color: var(--text); text-shadow: 0 1px 2px rgba(0,0,0,.95); }
/* A DEBUFF'S CLOCK IS RED, which is the wiki's own tell and the only thing
   separating "I have four seconds of Haste" from "it has four seconds of
   Slow" at a glance -- the arrows differ, but not at 22 pixels. */
.fxi.down b { color: #e08a8a; }
/* HUNGRY IS DIMMED rather than coloured: it is the absence of a buff, and a
   bright mark for nothing happening is the loudest thing on the plate. */
.fxi.hungry { opacity: .7; }
