/* ===========================================================================
   KNAPSACK — the warm-stone tokens, and how a fight is drawn.

   Shared by the game at /play/ and the landing page at /, because the landing
   page shows a real bag on the real grid and loops a real duel through the
   same playFight() the game uses. It is not a picture of the game.

   The tokens come with it rather than living in a third file: nothing below
   renders without them, and a page that linked the fight rendering without
   the palette would draw grey rectangles.

   This file exists for the reason ui.js exists. The two pages need the same
   hundred lines of geometry and the same eleven keyframes, and a second copy
   would drift — the item descriptions were kept in two places and four of the
   seventeen had quietly diverged before anybody noticed.

   NOT shared with the 1997 skin at /90s/, which draws the same elements its
   own way, in bevels and a 216-colour palette. That is a different visual
   world rather than a second copy of this one, and tools/skin-harness.mjs
   reads this file as part of the classic sheet when it checks that the skin
   covers every class.

   The geometry here is load-bearing and explained where it sits: .it is
   positioned by calc against .cells' padding, border and gaps, and .items is
   deliberately outside .cells. See CLAUDE.md's invariants before changing a
   number in it.
   =========================================================================== */
/* Paths in a stylesheet resolve against the STYLESHEET, not the page that
   linked it — so this stays ./fonts/ and is correct from both / and /play/. */
@font-face{
  font-family:"UnifrakturMaguntia";
  src:url("./fonts/unifrakturmaguntia-latin.woff2") format("woff2");
  font-weight:400;font-style:normal;font-display:swap;
}
:root{
  /* Warm stone and brass. The palette is the whole restyle: a keep at night,
     lit by fire, rather than a dashboard. Everything below is derived from
     these — no component invents its own colour. */
  --bg:#150F0A; --p1:#2A2117; --p2:#342819; --p3:#453522;
  --line:#6B5436; --line2:#42331F;
  --ink:#F1E6D2; --ink2:#C0A987; --ink3:#B69A72;
  --brass:#D9A441; --brass-hi:#F5CE7A; --brass-dim:#7A5C23;
  --cyan:#4FB3C4; --red:#E8796B; --green:#63B37B; --violet:#9A7BD0;
  /* carving: one highlight along the top edge, one shadow under it, and the
     drop shadow that lifts a slab off the wall behind it */
  --bevel:inset 0 1px 0 rgba(255,232,190,.13),inset 0 -1px 0 rgba(0,0,0,.55);
  --slab:linear-gradient(180deg,#4A3A28,#35291B);
  --lift:0 14px 30px -12px rgba(0,0,0,.75);
  /* a recess cut into the slab: dark, with the light catching its top lip */
  --recess:#1B140C;
  --carve:inset 0 3px 9px rgba(0,0,0,.75),inset 0 0 0 1px rgba(0,0,0,.55),0 1px 0 rgba(255,232,190,.12);
  --disp:"Archivo","Helvetica Neue",Arial,sans-serif;
  /* the wordmark only. Swap this one line for a vendored @font-face if you
     license a different blackletter. */
  --brand:"UnifrakturMaguntia","Palatino Linotype",Palatino,Georgia,serif;
  --text:"Source Serif 4",Georgia,serif;
  --mono:"IBM Plex Mono",Menlo,Consolas,monospace;
  color-scheme:dark;
}

/* ---- arena ---- */
.arena{position:relative;display:grid;grid-template-columns:1fr auto 1fr;gap:14px;padding:16px;align-items:start;
  margin:12px;border-radius:8px;background:var(--recess);box-shadow:var(--carve)}
@media (max-width:700px){.arena{grid-template-columns:1fr}}
.side{min-width:0}
.side h4{font-family:var(--disp);font-size:11.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink3);margin-bottom:7px}
.side.foe h4{color:var(--cyan)}
.hp{height:13px;background:#0C0805;border:1px solid var(--line);border-radius:7px;overflow:hidden;position:relative;
  margin-bottom:4px;box-shadow:inset 0 2px 4px rgba(0,0,0,.8),0 1px 0 rgba(255,232,190,.07)}
/* a sheen across the top half, so the bar reads as glass over a gem */
.hp::after{content:"";position:absolute;left:0;right:0;top:0;height:46%;pointer-events:none;z-index:2;
  background:linear-gradient(180deg,rgba(255,255,255,.22),transparent);border-radius:7px 7px 0 0}
.hp u{position:absolute;inset:0;transform-origin:left;background:rgba(255,255,255,.34);transition:transform .55s ease .12s}
.hp i{position:absolute;inset:0;transform-origin:left;transition:transform .1s linear;
  background:linear-gradient(180deg,#8ED39F,#4E9B66)}
.hp.foe i{background:linear-gradient(180deg,#7FD3E0,#3C8E9C)}
.hp.low{box-shadow:0 0 0 1px var(--red),0 0 12px rgba(208,87,74,.5);animation:lowp 1s ease-in-out infinite}
@keyframes lowp{50%{box-shadow:0 0 0 1px var(--red),0 0 20px rgba(208,87,74,.85)}}
.hp.low i{background:var(--red)!important}
.hpn{font-family:var(--mono);font-size:11px;color:var(--ink2);font-variant-numeric:tabular-nums;margin-bottom:9px}
/* the medallion between the two bags — a brass disc with the letters struck
   into it. Decoration only; it says nothing the layout does not. */
.vs{align-self:center;margin-top:52px;width:52px;height:52px;border-radius:50%;flex:none;
  display:grid;place-items:center;font-family:var(--disp);font-weight:800;font-size:13px;letter-spacing:.08em;
  color:#2A1D08;background:radial-gradient(circle at 50% 30%,#F5CE7A,#B9821F 62%,#7A5C23);
  border:1px solid #6B4A15;text-shadow:0 1px 0 rgba(255,242,214,.45);
  box-shadow:inset 0 2px 3px rgba(255,245,220,.5),inset 0 -3px 5px rgba(0,0,0,.45),0 6px 16px -6px rgba(0,0,0,.9)}
/* After .vs, not before it: a media query adds no specificity, so the
   narrow-screen rule placed up with .arena never won against the
   display:grid above, and the medallion stayed on screen on a phone —
   floating in the gap between the two stacked bags. */
@media (max-width:700px){.vs{display:none}}

.grid{position:relative;aspect-ratio:6/5;touch-action:manipulation;overflow:hidden;contain:layout paint}
.cells{position:absolute;inset:0;display:grid;gap:3px;padding:4px;background:#1C140C;border-radius:7px;
  border:2px solid #6B5436;grid-template-columns:repeat(6,1fr);grid-template-rows:repeat(5,1fr);
  box-shadow:inset 0 3px 12px rgba(0,0,0,.8),inset 0 0 0 1px rgba(0,0,0,.6),
    0 0 0 1px rgba(255,232,190,.08),0 6px 16px -8px rgba(0,0,0,.9)}
.items{position:absolute;inset:0;pointer-events:none}
.cell{background:linear-gradient(180deg,#3A2D1D,#241B11);border-radius:3px;min-width:0;min-height:0;
  box-shadow:inset 0 1px 0 rgba(255,232,190,.14),inset 0 -2px 5px rgba(0,0,0,.5)}
.cell.ok{background:#1D3826;box-shadow:inset 0 0 0 1px #3E7A52,0 0 10px -2px rgba(99,179,123,.6)}
.cell.bad{background:#3A1A1C;box-shadow:inset 0 0 0 1px #6E2F2F}
.it{position:absolute;pointer-events:auto;border-radius:5px;border:1px solid var(--rar,rgba(0,0,0,.55));
  display:flex;align-items:center;justify-content:center;cursor:pointer;
  box-shadow:inset 0 2px 0 rgba(255,255,255,.28),inset 0 -3px 6px rgba(0,0,0,.42),0 3px 8px -3px rgba(0,0,0,.8);
  /* padding 4 + border 2 = 6px inset; 5 gaps of 3px across, 4 down */
  left:calc(6px + var(--x) * ((100% - 27px)/6 + 3px));
  top:calc(6px + var(--y) * ((100% - 24px)/5 + 3px));
  width:calc(var(--w) * (100% - 27px)/6 + (var(--w) - 1) * 3px);
  height:calc(var(--h) * (100% - 24px)/5 + (var(--h) - 1) * 3px)}
.it svg{width:62%;height:62%;max-width:38px;max-height:38px;opacity:1;
  filter:drop-shadow(0 2px 2px rgba(0,0,0,.6))}
/* No blanket stroke: every icon is drawn over its own ink silhouette, and a
   CSS stroke on top would thicken each internal detail with it. */
.it::before{content:"";position:absolute;inset:0;border-radius:4px;pointer-events:none;
  background:linear-gradient(180deg,rgba(255,255,255,.3),rgba(255,255,255,.05) 46%,rgba(0,0,0,.24));
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.22),inset 0 0 0 2px rgba(32,24,11,.35)}
/* Every frame of a fight moves this bar on every tile. Without a layer of
   its own, that repaints the whole tile — gloss overlay, rarity frame and the
   drop-shadowed SVG inside it — and a deep wave fields thirty tiles doing it
   at once. will-change puts it on the compositor, so the bar moves and
   nothing repaints. This is the single biggest cost in a long hunt. */
.it .cd{position:absolute;left:0;right:0;bottom:0;height:3px;border-radius:0 0 2px 2px;background:rgba(255,255,255,.85);transform-origin:left;transform:scaleX(0);will-change:transform}
.it.ready .cd{background:#fff;box-shadow:0 0 6px rgba(255,255,255,.9)}
.it.pop{animation:pop .24s cubic-bezier(.34,1.7,.5,1)}
@keyframes pop{0%{transform:scale(.8)}62%{transform:scale(1.07)}100%{transform:scale(1)}}
/* The hit flash used to animate filter:brightness on the tile itself. A
   filter animation repaints the tile every frame, drop-shadowed SVG and all.
   A white overlay whose opacity animates is the same flash to look at, and
   the compositor does it without a repaint. */
.it.zap{animation:zappop .3s ease;z-index:5}
/* promoted only for the 300ms it flashes: will-change on every tile all the
   time would hold a compositor layer per tile, and a full grid is thirty */
.it.zap::after{animation:zapflash .3s ease;will-change:opacity}
.it::after{content:"";position:absolute;inset:0;border-radius:4px;pointer-events:none;
  background:#fff;opacity:0}
@keyframes zappop{0%,100%{transform:scale(1)}22%{transform:scale(1.08)}}
@keyframes zapflash{0%,100%{opacity:0}22%{opacity:.5}}
.it.buffed{box-shadow:inset 0 0 0 2px rgba(255,255,255,.55),0 0 10px rgba(217,164,65,.35)}
.it.link{box-shadow:0 0 0 2px var(--brass),0 0 16px rgba(217,164,65,.65);z-index:3}
.it.src{box-shadow:0 0 0 2px #fff,0 0 18px rgba(255,255,255,.4);z-index:4}
.it.dim{opacity:.34}
.buffnum{position:absolute;top:2px;right:2px;font-family:var(--mono);font-size:10.5px;font-weight:600;
  background:var(--brass);color:#140F04;border-radius:2px;padding:1px 4px;z-index:6;pointer-events:none;animation:pop .24s cubic-bezier(.34,1.7,.5,1)}
.grid.bad{animation:nope .22s}
@keyframes nope{25%{transform:translateX(-6px)}75%{transform:translateX(6px)}100%{transform:none}}
/* promoted only while it runs: a permanent will-change on a container this
   big would cost memory for the 99% of the time it is still */
.arena.shake{animation:shk .2s;will-change:transform}
@keyframes shk{20%{transform:translate(-4px,2px)}60%{transform:translate(4px,-2px)}100%{transform:none}}
.fnum{position:absolute;font-family:var(--disp);font-weight:800;font-size:26px;pointer-events:none;
  animation:fn .9s ease-out forwards;text-shadow:0 2px 6px #000;z-index:8;will-change:transform,opacity}
@keyframes fn{0%{opacity:0;transform:translateY(6px) scale(.7)}18%{opacity:1;transform:translateY(0) scale(1.12)}100%{opacity:0;transform:translateY(-44px) scale(1)}}
.fnum.crit{animation:fnc 1.1s ease-out forwards}
@keyframes fnc{0%{opacity:0;transform:translateY(8px) scale(.6) rotate(-6deg)}14%{opacity:1;transform:translateY(-2px) scale(1.5) rotate(3deg)}30%{transform:scale(1.2) rotate(0)}100%{opacity:0;transform:translateY(-58px) scale(1.1)}}
.fnum.tick{font-family:var(--mono);font-weight:600;font-size:14px;animation-duration:.7s}
.burst{position:absolute;width:90px;height:90px;margin:-45px 0 0 -45px;pointer-events:none;border-radius:50%;will-change:transform,opacity;
  background:radial-gradient(circle,rgba(240,193,105,.75),transparent 62%);animation:burst .42s ease-out forwards;z-index:7}
@keyframes burst{0%{transform:scale(.2);opacity:1}100%{transform:scale(1.5);opacity:0}}
/* The end of a fight used to be two lines of sans-serif floating over a lit
   arena, competing with thirty item tiles for attention. It now dims what it
   is sitting on and is set in the wordmark's blackletter — the one display
   face this game owns, and until now used on exactly one element.
   The scrim is its own layer so it can fade independently of the lettering,
   and it is tinted by outcome: warm for a win, cold for a loss, which is the
   fastest read on the screen and needs no reading at all. */
.result{position:absolute;inset:0;display:grid;place-items:center;pointer-events:none;z-index:20}
.result::before{content:"";position:absolute;inset:0;
  background:radial-gradient(120% 90% at 50% 45%,rgba(14,9,4,.80),rgba(6,4,2,.94));
  animation:dim .28s ease-out both}
.result.win::before{background:radial-gradient(120% 90% at 50% 45%,rgba(48,32,8,.80),rgba(6,4,2,.94))}
.result.lose::before{background:radial-gradient(120% 90% at 50% 45%,rgba(46,13,11,.80),rgba(5,2,2,.95))}
@keyframes dim{from{opacity:0}to{opacity:1}}
.result>div{position:relative;text-align:center;padding:0 16px}
/* Blackletter capitals are close to unreadable as a short all-caps word —
   the first pass set LOST in them and it read as LOGT. The face is fine and
   the lowercase is where its legibility lives, so the caps the callers send
   are folded down and only the first letter is raised. The 1997 skin leaves
   them as capitals, because its own face is a grotesque where caps are right.
   The callers' strings are untouched either way. */
.result b{display:block;font-family:var(--brand);font-weight:400;
  text-transform:lowercase;
  font-size:clamp(52px,10vw,96px);line-height:.94;letter-spacing:.01em;
  /* a struck edge rather than a blur: one lit pixel above, one dark below,
     then a wide soft shadow to lift it off whatever tile it lands on */
  text-shadow:0 -2px 0 rgba(0,0,0,.85),0 2px 0 rgba(0,0,0,.85),
              -2px 0 0 rgba(0,0,0,.85),2px 0 0 rgba(0,0,0,.85),
              0 3px 0 rgba(0,0,0,.6),0 10px 34px rgba(0,0,0,.95);
  animation:pop .4s cubic-bezier(.34,1.7,.5,1)}
.result b::first-letter{text-transform:uppercase}
/* the sub-line carries numbers, so it gets a recess to sit in rather than
   floating on the scrim */
.result small{display:inline-block;font-family:var(--mono);font-size:12.5px;
  margin-top:12px;padding:5px 12px;color:var(--ink);
  background:rgba(10,6,3,.72);border:1px solid var(--line2);border-radius:3px;
  box-shadow:inset 0 1px 0 rgba(255,232,190,.08)}
.flashfx{position:absolute;inset:0;background:#fff;pointer-events:none;z-index:19;animation:fl .26s ease-out forwards}
@keyframes fl{0%{opacity:.45}100%{opacity:0}}

/* ---- rarity on a tile ---- */
.it .pip{position:absolute;top:0;left:0;width:0;height:0;border-top:8px solid var(--rar);
  border-right:8px solid transparent;opacity:0;pointer-events:none}
.it.r-rare .pip,.it.r-fabled .pip{opacity:1}
.it.r-rare{filter:drop-shadow(0 0 4px rgba(240,193,105,.45))}
.it.r-fabled{filter:drop-shadow(0 0 7px rgba(201,167,255,.6))}
