/* Ninefall — the look.

   A dark stage the hero stands on, with everything else floating over it: an uppercase eyebrow, a
   serif name, rarity told by a pill, and a glass panel for whatever the screen lets you choose. The
   direction is the Mythic Model Lab tester: vignette and grain over a lit ground, gold on deep blue,
   micro-labels in wide uppercase, one characterful serif against a plain interface sans.

   Three rules hold it together:
     · one shape - a rounded rectangle at one of four radii; nothing is a circle or a pill
     · one control height - every button is --control tall, whatever it says
     · one spacing and tracking scale - the space between letters and parts comes from the tokens
       below and nowhere else, which is what makes it read as made rather than assembled.

   Nothing scrolls: every screen is sized to the display (see fit() in the sprite module). */

@font-face{
  font-family:"Playfair Display";
  src:url("/fonts/playfair-display.woff2") format("woff2-variations");
  font-weight:400 900;font-style:normal;font-display:swap;
}

:root{
  --bg:#070912;
  --ink:#f8f4ea;
  --muted:#9a9cab;
  --line:#ffffff1c;
  --line-soft:#ffffff0e;
  /* Glass, and it has to actually be glass. This was #111625df - 87% opaque - which is a painted
     panel with a blur behind it that nobody can see. The stage, its stars and the hero's own light
     are supposed to come through; the blur is what makes it readable, not the fill. */
  --panel:linear-gradient(180deg,#151b2d4d,#0a0e1a59);
  --panel-line:#ffffff24;
  /* No blur anywhere. backdrop-filter is the most expensive thing an interface can ask for - every
     glass surface re-samples what is behind it EVERY frame, and during the Spritefall nine tiles do
     that at once over a live 3D stage. Measured: it doubled the worst frame of the ceremony, 300ms to
     617ms. The panels stay translucent and lean on opacity alone (scratchpad/fall-perf.mjs). */
  --gold:#d9bd75;
  --gold-hi:#f0d58d;
  --grey:#9aa0ae;
  --green:#7cc08d;
  --blue:#91b4ff;
  --violet:#9277ff;
  --rarity:var(--grey);

  --serif:"Playfair Display",Georgia,"Times New Roman",serif;
  --sans:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
  /* type: three roles, and the tracking each one wants. One scale, every screen - the interface is
     not the thing you came to look at, so it stays out of the hero's way and the serif never becomes
     a billboard. A desk is not a reason to make the chrome bigger; it is a reason to leave more dark
     around it, which is what --column below does. */
  --t-display:clamp(23px,6.2vw,30px);
  --t-body:11.5px;
  --t-label:9.5px;
  --t-micro:9px;
  --track-display:-.015em;
  --track-label:.16em;
  --track-micro:.22em;
  --track-caption:.1em;
  --lead-display:.95;
  --lead-body:1.62;
  --lead-label:1.25;

  /* space: one scale, for every gap and every padding */
  --s1:6px; --s2:10px; --s3:14px; --s4:18px; --s5:24px;
  /* shape: rounded rectangles only */
  --r-panel:18px;
  --r-tile:14px;
  --r-control:12px;
  --r-pill:9px;
  --control:34px;        /* every button on a page, whatever it says */
  --dock-control:40px;   /* the dock is not a page control and keeps its own */
  /* The game is one column wide, on every screen. A desk gets more room around it, never a wider
     interface - so the dock and the page are the same width and nothing has to be sized twice. */
  --column:420px;

  --ease:cubic-bezier(.22,1,.36,1);
  --ease-inout:ease-in-out;
  --move:300ms;
  --travel:44px;
  color-scheme:dark;
}

*{box-sizing:border-box}
html,body{height:100%}
body{
  margin:0;overflow:hidden;
  background:var(--bg);color:var(--ink);
  font-family:var(--sans);font-size:var(--t-body);line-height:var(--lead-body);
  -webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;
  user-select:none;-webkit-user-select:none;
  touch-action:manipulation;overscroll-behavior:none;
  text-rendering:optimizeLegibility;
}
[hidden]{display:none!important}
button{font:inherit;color:inherit;cursor:pointer}
img{display:block;max-width:100%}

/* ---------- the stage: the world behind everything ---------- */
.stage{position:fixed;inset:0;z-index:0;overflow:hidden;background:var(--bg)}
.sky{position:absolute;inset:0;width:100%;height:100%;display:block;opacity:0;transition:opacity .5s var(--ease)}
.stage[data-ready="true"] .sky{opacity:1}
.vignette{position:absolute;inset:0;pointer-events:none;background:radial-gradient(circle at 50% 42%,transparent 38%,#03040a55 74%,#010207e6 112%)}
.grain{position:absolute;inset:0;pointer-events:none;opacity:.05;background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.55'/%3E%3C/svg%3E")}

/* ---------- the screen ---------- */
#view{
  position:relative;z-index:1;
  height:100dvh;width:100%;max-width:calc(var(--column) + 2 * var(--s4));margin:0 auto;
  padding:max(var(--s4),env(safe-area-inset-top)) max(var(--s4),env(safe-area-inset-left)) calc(env(safe-area-inset-bottom) + 78px);
  display:flex;flex-direction:column;gap:var(--s3);
}
#view[data-slack] .grid,#view[data-slack] .frame,#view[data-slack] .body,#view[data-slack] .state{margin-block:auto}
/* One view gives way to the next: the incoming screen arrives a short way off its place and eases in.
   Transforms only, so the glass over the stage never flattens mid-move. */
[data-view]{visibility:hidden;animation:var(--move) var(--ease-inout) both paused}
[data-view][data-run="true"]{visibility:visible;animation-play-state:running}
[data-view="push"]{animation-name:view-push}
[data-view="pop"]{animation-name:view-pop}
[data-view="cover"]{animation-name:view-cover}
[data-view="dismiss"]{animation-name:view-dismiss}
@keyframes view-push{from{transform:translateX(var(--travel))}to{transform:none}}
@keyframes view-pop{from{transform:translateX(calc(-1 * var(--travel)))}to{transform:none}}
@keyframes view-cover{from{transform:translateY(var(--travel)) scale(.982)}to{transform:none}}
@keyframes view-dismiss{from{transform:translateY(calc(-1 * var(--travel))) scale(.982)}to{transform:none}}
@media (prefers-reduced-motion:reduce){[data-view]{animation:none!important;visibility:visible}}

/* ---------- the head: what this is, whose it is, how rare ---------- */
.head{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--s3);flex:0 0 auto}
.brand{min-width:0;flex:1 1 auto;text-align:left}
.tools{flex:0 0 auto;display:flex;gap:var(--s1)}
.eyebrow{margin:0;font-size:var(--t-micro);letter-spacing:var(--track-micro);text-transform:uppercase;color:#b9b2a2;line-height:var(--lead-label)}
.title{
  margin:var(--s1) 0 0;font-family:var(--serif);font-weight:500;
  font-size:var(--t-display);line-height:var(--lead-display);letter-spacing:var(--track-display);
  text-wrap:balance;overflow-wrap:anywhere;
}
.pills{display:flex;flex-wrap:wrap;justify-content:flex-start;gap:var(--s1);margin-top:var(--s2)}
.pill{
  min-height:26px;padding:0 var(--s2);display:inline-flex;align-items:center;
  border:1px solid var(--line);border-radius:var(--r-pill);
  background:#090c1499;
  font-size:var(--t-micro);letter-spacing:var(--track-label);text-transform:uppercase;color:#d6d2c9;white-space:nowrap;line-height:1;
}
.pill[data-rarity]{border-color:color-mix(in srgb,var(--rarity) 42%,transparent);color:var(--rarity);box-shadow:inset 0 0 18px color-mix(in srgb,var(--rarity) 9%,transparent)}
.pill[data-rarity="0"]{--rarity:var(--grey)}
.pill[data-rarity="1"]{--rarity:var(--green)}
.pill[data-rarity="2"]{--rarity:var(--blue)}
.pill[data-rarity="3"]{--rarity:var(--violet)}
.pill[data-rarity="4"]{--rarity:var(--gold)}

/* ---------- controls: one height, one shape ---------- */
.icon,.util,.btn,.seg button{
  min-height:var(--control);
  border-radius:var(--r-control);
  font-size:var(--t-label);letter-spacing:var(--track-label);text-transform:uppercase;line-height:1.2;
  transition:color .2s ease,border-color .2s ease,background-color .2s ease,transform .16s var(--ease);
}
.icon,.util{
  display:inline-flex;align-items:center;justify-content:center;gap:var(--s1);
  border:1px solid var(--line);background:#ffffff06;color:#c9cbd4;
}
.icon{width:var(--control);flex:0 0 auto}
.util{padding:0 var(--s3)}
.icon:active,.util:active,.btn:active,.seg button:active,.dock button:active{transform:scale(.96)}
.icon:hover,.util:hover{color:var(--ink)}
.icon svg,.util svg,.btn svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
.icon:focus-visible,.util:focus-visible,.seg button:focus-visible,.dock button:focus-visible,.btn:focus-visible,.tile:focus-visible,.frame:focus-visible{outline:2px solid var(--gold);outline-offset:2px}
.btn{
  padding:0 var(--s4);display:inline-flex;align-items:center;justify-content:center;gap:var(--s2);
  border:1px solid color-mix(in srgb,var(--gold) 34%,transparent);
  background:linear-gradient(180deg,#d9bd7524,#d9bd750a);color:#f6e6bf;
}
.btn[data-tone="quiet"]{border-color:var(--line);background:#090c1440;color:#c9cbd4}

/* ---------- the glass panel, and the segmented rows it holds ---------- */
.panel{
  flex:0 0 auto;padding:var(--s2);
  border:1px solid var(--panel-line);border-radius:var(--r-panel);
  background:var(--panel);
  box-shadow:0 18px 70px #0007;
  display:grid;gap:var(--s1);
}
.seg{display:grid;grid-auto-flow:column;grid-auto-columns:1fr;gap:var(--s1)}
.seg[data-cols="3"]{grid-auto-flow:row;grid-template-columns:repeat(3,1fr)}
.seg[data-cols="4"]{grid-auto-flow:row;grid-template-columns:repeat(4,1fr)}
.seg button{
  padding:0 var(--s1);min-width:0;
  border:1px solid var(--line);background:#090c1459;color:#b4b7c1;
  font-size:var(--t-micro);
}
.seg button[aria-pressed="true"],.seg button[aria-current]{
  color:#fff4d9;border-color:color-mix(in srgb,var(--gold) 38%,transparent);
  background:linear-gradient(180deg,#d9bd7520,#d9bd750a);box-shadow:inset 0 0 18px #d9bd7512;
}
.seg button:disabled,.btn:disabled{opacity:.34;transform:none;cursor:default}
.pair{display:grid;grid-template-columns:1fr 1fr;gap:var(--s1)}

/* ---------- where a hero stands ----------
   The shot is the hero's frame: the sprite is scaled so its frame lands here. Anything that reaches
   past it - a wide weapon, one that hangs below the feet - keeps its full size and hangs outside,
   over whatever is around it, rather than being cropped or shrunk to fit. */
.frame,.tile{overflow:visible}
/* the sprite is art, not a target: a weapon hanging over a button never swallows the tap */
.shot{position:relative;width:100%;height:var(--shot-h,0px);pointer-events:none}
.shot canvas{
  position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  width:calc(100% * var(--spill,1));max-width:none;height:auto;
  image-rendering:pixelated;
}
.frame{position:relative;flex:0 0 auto;margin:0}
.frame .shot canvas{filter:drop-shadow(0 22px 34px #0006)}

/* ---------- grids of heroes ---------- */
.grid{flex:0 0 auto;display:grid;gap:var(--s3)}
.grid[data-cols="2"]{grid-template-columns:repeat(2,1fr)}
.grid[data-cols="3"]{grid-template-columns:repeat(3,1fr)}
.tile{
  position:relative;margin:0;padding:var(--s2) var(--s1) var(--s1);display:grid;gap:var(--s1);
  border:1px solid var(--line-soft);border-radius:var(--r-tile);
  background:linear-gradient(180deg,#ffffff12,#ffffff06);
  box-shadow:0 10px 30px #0004;
  transition:border-color .2s ease,background-color .2s ease,box-shadow .2s ease,transform .14s var(--ease);
}
.tile:active{transform:scale(.965);border-color:#ffffff3d}
/* A hero's art spills over its own caption on purpose - nothing is cropped to fit a frame - but the
   name still has to be readable, and a sprite's gold aura is a terrible thing to set grey text on.
   Measured before this: tile captions ran as low as 2.41:1 against a hero's own glow. So the words
   get their own ground, and the art keeps spilling over everything except them. This is also why the
   names looked overdrawn on the band. */
.tile figcaption,.tile .seatskill{position:relative;z-index:1}
.tile::after{
  /* only as tall as the words it is there for. Measured: the caption and skill line occupy the lowest
     23% of a tile while the hero's art runs to 88%, so a 46% scrim was covering the model's lower
     third to no purpose (scratchpad/scrim.mjs). Lighter, too - it has to carry the text, not hide the
     hero behind it. */
  content:"";position:absolute;left:0;right:0;bottom:0;height:27%;pointer-events:none;
  background:linear-gradient(180deg,#07091200,#07091299 44%,#070912cc);
  border-radius:0 0 var(--r-tile) var(--r-tile);
}
@keyframes seat-take{0%{transform:scale(1)}38%{transform:scale(1.07)}100%{transform:scale(1)}}
.seat[data-took]{animation:seat-take .36s var(--ease)}
/* A card says its own tier. At 24% on a dark panel over a lit world the rarity edge was invisible -
   the only thing telling you a hero was legendary was the letters "T3" in grey - so the edge does the
   work instead. It stays an EDGE rather than tinting the panel: the glass is a window onto the world
   behind it, and colouring it in would quietly undo that. The sprite already carries rarity in its
   floor ring and its rays; this is the chrome agreeing with the art rather than a second opinion. */
.tile[data-rarity]{border-color:color-mix(in srgb,var(--rarity) 78%,transparent)}
.tile[data-rarity="0"]{border-color:color-mix(in srgb,var(--rarity) 42%,transparent)}
.tile[data-rarity="3"],.tile[data-rarity="4"]{
  box-shadow:inset 0 1px 0 color-mix(in srgb,var(--rarity) 60%,transparent),0 10px 30px #0004}
.tile[data-rarity="4"]{border-width:2px}
.tile[data-rarity="0"]{--rarity:var(--grey)}
.tile[data-rarity="1"]{--rarity:var(--green)}
.tile[data-rarity="2"]{--rarity:var(--blue)}
.tile[data-rarity="3"]{--rarity:var(--violet)}
.tile[data-rarity="4"]{--rarity:var(--gold);box-shadow:inset 0 -30px 40px -30px var(--gold)}
.tile figcaption{font-size:var(--t-micro);letter-spacing:var(--track-caption);text-transform:uppercase;color:#c3c5cd;text-align:center;line-height:var(--lead-label);overflow-wrap:anywhere}

/* ---------- numbers ---------- */
.stats{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--s3)}
.stat{display:grid;gap:var(--s1);min-width:0}
.stat span{font-size:var(--t-micro);letter-spacing:var(--track-micro);text-transform:uppercase;color:var(--muted);line-height:1}
.stat strong{font-size:21px;font-weight:600;line-height:1;letter-spacing:-.01em;font-variant-numeric:tabular-nums}
.stat small{font-size:var(--t-micro);letter-spacing:var(--track-caption);color:var(--muted);font-variant-numeric:tabular-nums;line-height:1.2}
.bar{height:3px;border-radius:2px;background:#ffffff14;overflow:hidden}
.bar i{display:block;height:100%;background:linear-gradient(90deg,color-mix(in srgb,var(--gold) 60%,transparent),var(--gold));transform-origin:left center;transform:scaleX(var(--fill,0));transition:transform .9s var(--ease)}
.facts{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--s2)}
.fact{display:grid;gap:4px;min-width:0}
.fact span{font-size:var(--t-micro);letter-spacing:var(--track-micro);text-transform:uppercase;color:var(--muted);line-height:1}
.fact b{font-weight:500;font-size:var(--t-body);line-height:var(--lead-label);overflow-wrap:anywhere}

/* ---------- states, hints, toast ---------- */
.state{
  padding:var(--s5);display:grid;gap:var(--s2);justify-items:start;
  border:1px solid var(--line);border-radius:var(--r-panel);background:var(--panel);
}
.state p{margin:0}
.state h2{margin:0;font-family:var(--serif);font-weight:500;font-size:18px;line-height:1.15;letter-spacing:-.01em}
.state small{color:var(--muted);font-size:var(--t-body);line-height:var(--lead-body);max-width:46ch}
.state .btn{margin-top:var(--s1)}
.hint{flex:0 0 auto;margin:0;text-align:center;font-size:var(--t-micro);letter-spacing:var(--track-micro);text-transform:uppercase;color:#8e929f}
.toast{
  position:fixed;z-index:40;left:50%;transform:translateX(-50%);
  bottom:calc(env(safe-area-inset-bottom) + 86px);
  max-width:min(92vw,420px);padding:var(--s2) var(--s3);
  display:flex;align-items:center;gap:var(--s3);
  border:1px solid var(--panel-line);border-radius:var(--r-control);background:var(--panel);box-shadow:0 14px 50px #0008;
  font-size:var(--t-body);color:#dcdde3;
}

/* ---------- the dock ---------- */
.dock{
  position:fixed;z-index:20;left:50%;transform:translateX(-50%);
  bottom:calc(env(safe-area-inset-bottom) + 12px);
  padding:var(--s1);display:grid;grid-auto-flow:column;grid-auto-columns:1fr;gap:var(--s1);
  width:min(var(--column),calc(100vw - 28px));
  border:1px solid var(--panel-line);border-radius:var(--r-panel);background:var(--panel);
  box-shadow:0 16px 50px #0008;
}
.dock button{
  min-height:var(--dock-control);border-radius:var(--r-control);border:0;background:transparent;color:#9497a3;
  font-size:var(--t-micro);letter-spacing:var(--track-label);text-transform:uppercase;line-height:1.2;
  transition:color .2s ease,background-color .2s ease,transform .16s var(--ease);
  display:flex;align-items:center;justify-content:center;gap:var(--s1);
  font-size:var(--t-micro);
}
.dock button svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
.dock button[aria-current="page"]{color:#fff4d9;background:linear-gradient(180deg,#d9bd751f,#d9bd7508)}


/* ---------- the band: nine seats, and the lines drawn across them ----------
   The grid takes the pointer itself (touch-action:none) so drawing a line is never read as a scroll.
   The strokes sit in an overlay above the seats and below nothing: they are not interactive. */
.band{position:relative;flex:0 0 auto;display:grid}
.band .grid{touch-action:none;-webkit-user-select:none;user-select:none}
.band .lines{position:absolute;inset:0;pointer-events:none;overflow:visible}
.band .lines line{stroke-linecap:round;fill:none}
.band .lines .drawn{stroke:var(--gold);stroke-width:3;opacity:.85}
.band .lines .drawn.n1{stroke:var(--blue)}
.band .lines .drawn.n2{stroke:var(--green)}
.band .lines .drawing{stroke:#ffffff8c;stroke-width:2;stroke-dasharray:2 6}

.seat{cursor:pointer}
.seat[data-in]{border-color:color-mix(in srgb,var(--gold) 55%,transparent)}
.seat[data-in="2"]{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--gold) 30%,transparent)}
.seat[data-in="3"]{box-shadow:inset 0 0 0 2px color-mix(in srgb,var(--gold) 42%,transparent)}
.seat[data-first]{border-color:#ffffff6b}
.seat .seatskill{display:block;text-align:center;font-size:var(--t-micro);letter-spacing:var(--track-caption);
  color:var(--muted);line-height:var(--lead-label);overflow-wrap:anywhere}
.seat figcaption{display:flex;align-items:center;justify-content:center;gap:5px}

/* a sign is a square of its own colour - the five never mix, so the shape stays the same */
.sign{width:8px;height:8px;border-radius:3px;flex:none;background:var(--sign,var(--grey))}
.sign[data-sign="lightning"]{--sign:#e0c84a}
.sign[data-sign="water"]{--sign:#6f9ce8}
.sign[data-sign="fire"]{--sign:#e08a52}
.sign[data-sign="earth"]{--sign:#7fbb7a}
.sign[data-sign="wind"]{--sign:#74c2cc}

/* the three lines you have drawn, named */
.lineup-chosen{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--s2)}
.chosen{display:grid;gap:2px;padding:var(--s2) var(--s1);border:1px solid var(--line-soft);
  border-radius:var(--r-control);text-align:center;min-width:0;background:#090c1440}
.chosen[data-on]{border-color:color-mix(in srgb,var(--gold) 45%,transparent)}
.chosen span{font-size:var(--t-micro);letter-spacing:var(--track-micro);text-transform:uppercase;color:var(--muted);line-height:1}
.chosen b{font-size:var(--t-label);font-weight:600;line-height:1.2;overflow-wrap:anywhere}

/* A short screen cannot hold nine seats, their names, their skills and a panel. The skill line is the
   first thing to go - it is a reminder, not a decision - and the chosen lines stack tighter. */
/* A title sized only by width grows on a wide-but-short screen until it pushes the page off the
   bottom. Height gets a say: the display face is the largest thing on any screen, so it is the first
   thing that should notice the screen is short. */

/* ---------- bare: the model gets the room ----------
   On a screen whose subject is one figure, the head stays where it is and everything you can press is
   reduced to one slim strip, so the figure has the rest. The art is then fitted to the HEIGHT it was
   given rather than the width - the no-crop rule solved the other way round - and whatever reaches past
   the frame sideways hangs outside it as it always did. */
#view[data-bare="true"]{gap:var(--s2)}
#view[data-bare="true"] > .body{flex:1 1 auto;min-height:0;display:grid;place-items:center}
#view[data-bare="true"] .frame{margin:0;height:100%;width:100%;display:grid;place-items:center}
#view[data-bare="true"] .shot{height:calc(100% / var(--spill-y,1))}
#view[data-bare="true"] .shot canvas{width:auto;height:calc(100% * var(--spill-y,1))}
#view[data-bare="true"] .frame{cursor:pointer}
.float{display:grid;gap:var(--s1)}
.float .hint{margin:0}
.float .panel{padding:var(--s1);gap:var(--s1)}
/* A row that is nothing but buttons does not get a box round it - that is a frame containing only
   other frames. The store and the archive are both one segmented row, so they stand in the stack at
   the same height as every other button and the panel is kept for things that hold actual content. */
.float .panel[data-part="stores"],.float .panel[data-part="archive"]{
  padding:0;border:0;background:none;box-shadow:none;}

/* the smallest a control can be and still be worth aiming at */
.mini{display:grid;grid-template-columns:auto 1fr;gap:var(--s1);align-items:center}

/* an icon that is currently true wears the gold */
.icon[data-on]{color:var(--gold);border-color:color-mix(in srgb,var(--gold) 45%,transparent);background:#d9bd7514}

/* A narrow screen tightens the smallest type rather than taking another row: a row costs the model
   more height than the letters cost legibility. */
@media (max-width:370px){
  .seg button{font-size:8.5px;letter-spacing:.03em;padding:0 3px}
}

/* ---------- the field ----------
   Two sides, the far one above the near one, each fighter a sprite with its life under it. The rows
   share the room the rest of the screen leaves, and the fallen stay where they fell, dimmed. */
/* Two columns facing each other: the band on the left, what it is facing on the right. Each side is a
   stack, so a fighter's whole height is its own and the two lines look across the gap between them. */
.battle{flex:1 1 auto;min-height:0;display:grid;grid-template-columns:1fr 1fr;gap:var(--s2)}
.side{display:grid;grid-auto-flow:row;grid-auto-rows:1fr;gap:var(--s2);justify-items:center;align-items:stretch;min-height:0}
.fighter{position:relative;margin:0;padding:0;width:100%;min-width:0;height:100%;
  display:grid;grid-template-rows:minmax(0,1fr) auto;gap:4px;
  transition:transform .22s var(--ease),opacity .4s ease}
/* the art is fitted to the row's HEIGHT, and whatever spills sideways passes behind its neighbours */
.battle .shot{height:100%;min-height:0;z-index:1}
.battle .shot canvas{width:auto;height:calc(100% * var(--spill-y,1));max-width:none}
/* A boss stands alone on its side and does not get to eat the screen with it. Everything else keeps
   the rule - art may exceed its frame and nothing is cropped - but a boss frame is already 400x300
   against a normal foe's 256, and with the width unbounded an Anchor covered the header, the band and
   the log. It is SCALED to the room it has rather than cropped to it, because the band opposite has to
   stay readable and that is the entire point of the screen. */
.battle .fighter[data-rank="boss"] .shot canvas{
  width:auto;height:auto;max-width:100%;max-height:100%;
}
/* and it is given more of the field to be big IN. Contained inside half a screen a boss came out
   smaller than the heroes it was fighting, which is its own kind of wrong - so when one is present the
   field stops being two equal columns. It still cannot reach the band, the header or the log. */
.battle:has(.fighter[data-rank="boss"]){grid-template-columns:.66fr 1.34fr}
.vitals{position:relative;z-index:2}
.fighter[data-acting]{transform:translateY(-6px) scale(1.04)}
.fighter[data-down]{opacity:.26;filter:grayscale(1)}
.vitals{display:grid;gap:2px;min-width:0;justify-items:center}

/* one pip per mark, in the mark's own colour */
.marks{display:flex;flex-wrap:wrap;justify-content:center;gap:3px;min-height:9px}
.mark{width:9px;height:9px;border-radius:3px;background:color-mix(in srgb,var(--tint) 78%,transparent);
  display:grid;place-items:center;font-size:7px;font-style:normal;font-weight:700;color:#0b0d16;line-height:1}
.mark[data-hard]{box-shadow:0 0 0 1px var(--tint),0 0 8px var(--tint)}
.fighter[data-frozen] .shot canvas{filter:saturate(.45) brightness(1.15) drop-shadow(0 0 6px #74c2cc)}
.battle .side{align-items:stretch}
.hp{height:2px;border-radius:2px;background:#ffffff14;overflow:hidden}
.battle .hp{width:62%;justify-self:center;margin:0 auto}
.hp i{display:block;height:100%;border-radius:2px;background:linear-gradient(90deg,#b2553f,var(--gold));
  transform-origin:left center;transform:scaleX(var(--fill,1));transition:transform .38s var(--ease)}
.fighter figcaption{display:flex;align-items:center;justify-content:center;gap:4px;
  font-size:var(--t-micro);letter-spacing:var(--track-caption);text-transform:uppercase;color:#c3c5cd;
  line-height:var(--lead-label);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* a number that leaves the fighter it landed on */
@keyframes rise{from{transform:translate(-50%,0);opacity:0}
  18%{opacity:1}to{transform:translate(-50%,-46px);opacity:0}}
.num{position:absolute;left:50%;top:22%;z-index:4;pointer-events:none;
  font-size:15px;font-weight:600;font-variant-numeric:tabular-nums;letter-spacing:-.01em;
  text-shadow:0 2px 6px #000a;animation:rise 1.05s var(--ease) forwards;color:#f3ede0}
.num[data-tone="edge"]{color:var(--gold);font-size:18px}
.num[data-tone="weak"]{color:#9aa0ae;font-size:13px}
.num[data-tone="mend"]{color:var(--green)}
.num[data-tone="buff"]{color:var(--blue);font-size:11px;letter-spacing:var(--track-caption)}

#view[data-over] .hint{color:var(--gold)}

/* ---------- what a player has ---------- */
.tally{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--s1);
  padding:var(--s2);border:1px solid var(--panel-line);border-radius:var(--r-panel);
  background:var(--panel);}
.tell{display:grid;gap:2px;justify-items:center;text-align:center;min-width:0}
.tell span{font-size:var(--t-micro);letter-spacing:var(--track-micro);text-transform:uppercase;color:var(--muted);line-height:1}
.shard{width:1.05em;height:1.05em;display:inline-block;vertical-align:-.17em;margin-right:.34em;flex:0 0 auto}
.tell strong{font-size:15px;font-weight:600;line-height:1.1;font-variant-numeric:tabular-nums;color:var(--gold)}
.tell small{font-size:var(--t-micro);letter-spacing:var(--track-caption);color:var(--muted);line-height:1.2}

/* the daily three */
.deeds{display:grid;gap:var(--s1);padding:var(--s2);border:1px solid var(--panel-line);
  border-radius:var(--r-panel);background:var(--panel);}
.deed{display:grid;grid-template-columns:1fr auto;gap:var(--s2);align-items:center;min-height:var(--control)}
.deed-say{display:grid;gap:2px;min-width:0}
.deed-say b{font-size:var(--t-label);font-weight:600;letter-spacing:var(--track-caption);
  text-transform:uppercase;line-height:1.2;overflow-wrap:anywhere}
.deed-say small{font-size:var(--t-micro);letter-spacing:var(--track-caption);color:var(--muted);font-variant-numeric:tabular-nums}
.deed .hp{width:76px}
.deed[data-done] .deed-say b{color:var(--gold)}
.deed[data-paid]{opacity:.5}

/* the ground: nine fields to an area */
.nodes{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--s2);flex:0 0 auto}
.node{display:grid;gap:2px;place-items:center;min-height:56px;padding:var(--s2) var(--s1);
  border:1px solid var(--line);border-radius:var(--r-tile);background:#090c1459;color:#c9cbd4;
  transition:border-color .2s ease,background-color .2s ease,transform .14s var(--ease)}
.node:active{transform:scale(.965)}
.node span{font-family:var(--serif);font-size:19px;line-height:1;font-variant-numeric:tabular-nums}
.node small{font-size:var(--t-micro);letter-spacing:var(--track-caption);text-transform:uppercase;color:var(--muted)}
.node[data-done]{border-color:color-mix(in srgb,var(--green) 40%,transparent);color:var(--green)}
.node[data-here]{border-color:color-mix(in srgb,var(--gold) 55%,transparent);color:var(--gold);
  background:linear-gradient(180deg,#d9bd7520,#d9bd7508)}
.node:disabled{opacity:.3;transform:none}
.node:focus-visible{outline:2px solid var(--gold);outline-offset:2px}

/* an empty seat in the frame: the same box as a filled one, so nothing reflows when it is taken */
.seat[data-empty]{border-style:dashed;border-color:#ffffff1c;background:
  repeating-linear-gradient(135deg,#ffffff05 0 6px,transparent 6px 12px)}
.seat[data-empty] figcaption{color:var(--muted)}

/* the ground takes the room the rest leaves rather than a fixed nine rows of its own */
#view .nodes{flex:1 1 auto;min-height:0;grid-auto-rows:1fr}
.campaign-body{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;gap:var(--s2)}

/* ---------- a fall ----------
   Each shape comes down out of the dark a moment after the one before it, and settles when it is
   touched. Transform and filter only: the tiles themselves are glass. */
/* The ceremony, in four beats. A Spritefall is the one moment the game asks you to wait, so nothing
   in it snaps and no two things happen at once that could happen in order.

     1  an empty stage and one button, so the fall is something you DO rather than something that has
        already happened by the time the screen arrives
     2  the cards come down out of the dark, one at a time, beginning at the bottom right
     3  a silhouette dissolves - it does not swap
     4  and only then does the hero itself come down, out of the same sky the card did

   The canvas inside a shot is centred with translate(-50%,-50%), so every keyframe here carries that
   with it; dropping it is how an animated sprite jumps half its own width on the first frame. */
@keyframes descend{
  from{transform:translateY(-62vh) scale(.90);opacity:0;filter:blur(3px)}
  50%{opacity:1}
  74%{transform:translateY(7px) scale(1.03);filter:blur(0)}
  88%{transform:translateY(-2px) scale(.995)}
  to{transform:none;opacity:1;filter:blur(0)}
}
/* what the landing throws off, in the colour of how intact the seed was */
@keyframes land{
  from{box-shadow:0 0 0 0 color-mix(in srgb,var(--rarity) 70%,transparent)}
  to{box-shadow:0 0 40px 14px transparent}
}
/* the grain letting go */
@keyframes dissolve{
  from{opacity:1;transform:translate(-50%,-50%) scale(1);filter:brightness(1)}
  45%{opacity:.9;filter:brightness(2.6) saturate(1.4)}
  to{opacity:0;transform:translate(-50%,-50%) scale(1.14);filter:brightness(3)}
}
/* and the hero itself, arriving the way the card did */
@keyframes arrive{
  from{opacity:0;transform:translate(-50%,-50%) translateY(-44vh) scale(.92);filter:blur(2px)}
  54%{opacity:1;filter:blur(0)}
  80%{transform:translate(-50%,-50%) translateY(5px) scale(1.02)}
  to{opacity:1;transform:translate(-50%,-50%);filter:blur(0)}
}
.fell[data-fell]{
  animation:descend 1.05s var(--ease) both, land .8s var(--ease) both;
  animation-delay:calc(var(--i,0) * 240ms), calc(var(--i,0) * 240ms + .74s);
}
.fell[data-dissolving] .shot canvas{animation:dissolve .34s var(--ease) both}
.fell[data-arriving] .shot canvas{animation:arrive .66s var(--ease) both}
.fell[data-veil]{cursor:pointer}
.fell[data-veil] .shot canvas{filter:drop-shadow(0 0 14px color-mix(in srgb,var(--rarity) 55%,transparent))}
.fell[data-veil] .seatskill{color:var(--rarity);letter-spacing:.4em}
/* the untouched stage: one thing to press, and a long way to fall into */
.throw{flex:1 1 auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--s3);min-height:0}
.throw .hint{max-width:34ch}
.throw .btn{min-width:min(260px,72vw)}
@media (prefers-reduced-motion:reduce){.fell[data-fell],.fell[data-dissolving] .shot canvas,.fell[data-arriving] .shot canvas{animation:none}}

/* ---------- what a hero carries ---------- */
.slots{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--s1)}
.slot{display:grid;gap:3px;justify-items:center;text-align:center;min-width:0;padding:var(--s2) var(--s1);
  border:1px solid var(--line-soft);border-radius:var(--r-tile);background:#ffffff05}
.slot[data-rarity]{border-color:color-mix(in srgb,var(--rarity) 42%,transparent)}
.slot[data-rarity="0"]{--rarity:var(--grey)} .slot[data-rarity="1"]{--rarity:var(--green)}
.slot[data-rarity="2"]{--rarity:var(--blue)} .slot[data-rarity="3"]{--rarity:var(--violet)}
.slot[data-rarity="4"]{--rarity:var(--gold)}
.slot span{font-size:var(--t-micro);letter-spacing:var(--track-micro);text-transform:uppercase;color:var(--muted);line-height:1}
.slot b{font-size:var(--t-label);font-weight:600;line-height:1.2;overflow-wrap:anywhere}
.slot b.dim{color:var(--muted);font-weight:400}
.slot small{font-size:var(--t-micro);letter-spacing:var(--track-caption);color:var(--muted);line-height:1.2}
.slot .btn{margin-top:4px;padding:0 var(--s2);width:100%}

.relics{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--s1)}
.relic{display:grid;gap:2px;justify-items:start;text-align:left;padding:var(--s2);min-height:var(--control);
  border:1px solid var(--line);border-radius:var(--r-control);background:#ffffff07;color:var(--ink);
  transition:border-color .2s ease,transform .14s var(--ease)}
.relic:active{transform:scale(.97)}
.relic:disabled{opacity:.4}
.relic[data-rarity]{border-color:color-mix(in srgb,var(--rarity) 38%,transparent)}
.relic[data-rarity="0"]{--rarity:var(--grey)} .relic[data-rarity="1"]{--rarity:var(--green)}
.relic[data-rarity="2"]{--rarity:var(--blue)} .relic[data-rarity="3"]{--rarity:var(--violet)}
.relic[data-rarity="4"]{--rarity:var(--gold)}
.relic b{font-size:var(--t-label);font-weight:600;letter-spacing:var(--track-caption);text-transform:uppercase;line-height:1.2}
.relic small{font-size:var(--t-micro);letter-spacing:var(--track-caption);color:var(--muted)}
.relic:focus-visible{outline:2px solid var(--gold);outline-offset:2px}

/* today's ground and the week's Anchor sit above the nine, because they are not part of the nine */
.special{display:grid;gap:var(--s2);flex:0 0 auto}
.node.wide{grid-auto-flow:row;min-height:46px;padding:var(--s2) var(--s3);justify-items:start;text-align:left;gap:3px}
.node.wide span{font-family:var(--sans);font-size:var(--t-label);font-weight:600;letter-spacing:var(--track-caption);text-transform:uppercase}
.node.wide .hp{width:100%;margin-top:3px}


/* ---------- the wider screen ----------
   Last, for the same reason the short-screen blocks are: a media query carries no extra
   specificity, so this has to sit after the base rules it raises. It stays ABOVE the
   short-screen blocks, which are allowed to take the title back down on a wide-but-short one. */
/* A bigger screen gets more ROOM AROUND the game, never a bigger game. The page is the same column
   the dock is, centred, so every size and gap measured on a phone holds on a desk without being
   worked out a second time - which is also why there is no longer a min-width:760px block here
   raising the type, the gaps and the padding. One column, one set of numbers. */


/* ---------- short screens ----------
   These sit at the END of the sheet on purpose. A media query adds no specificity, so a block
   written earlier loses to an identical selector defined later in the file - which is exactly what
   had happened: six rules here (.tally, .tell strong, .tell small, .deeds, .deed) were dead the
   whole time, beaten by their own base rules further down. Keep these last. */
/* A title sized only by width grows on a WIDE-but-short screen until it pushes the page off the
   bottom - a laptop, not a phone. Scoped to match: a phone is short by nature and already has its own
   smaller scale, and this rule was quietly undoing it. */
/* (There was a rule here that shrank the title on a wide-but-short screen, back when the display face
   ran to 58px and pushed the page off the bottom. The scale tops out at 30px now, so the same rule had
   turned into the opposite of itself - it was INFLATING the title to 44px on exactly those screens.
   Deleted rather than adjusted: a smaller face never needed rescuing from its own size.) */
@media (max-height:860px){
  /* The archive is a thing to look at, so a short screen may keep it folded away. The store is NOT -
     provisions are bought, and a laptop is about 800px tall, so hiding it here meant a player on a
     MacBook could not buy a poultice at all. Content you act on does not get to be the thing that
     gives way; the display face above is what makes the room. */
  [data-part="archive"]{display:none}
}
@media (max-height:700px){
  /* the archive is a thing to look at, so it is the first thing a short screen gives up */
  [data-part="archive"]{display:none}
  [data-part="dev"] .hint{display:none}
  [data-part="signin"] .hint{display:none}
  .tally{padding:var(--s1)}
  .tell strong{font-size:15px}
  .tell small{display:none}
  /* The deeds close up rather than lose a row. They were crushed flat when the type was bigger; the
     smaller phone scale paid that back, so this is now a gentle tightening with about 30px still in
     hand rather than a squeeze with 6 (scratchpad/relax.mjs). */
  .deeds{gap:3px;padding:var(--s1)}
  .deed{min-height:30px}
  .tally{gap:0}
  .state{padding:var(--s3);gap:var(--s1)}
  .state h2{font-size:19px}
  .state small{display:none}
}
@media (max-height:640px){
  .seat .seatskill{display:none}
  .band .grid{gap:var(--s2)}
  .chosen{padding:var(--s1) 4px}
  .lineup-chosen{gap:var(--s1)}
}

/* ---------- what a blow looks like from the other end ----------
   The caster already plays its skill. What was missing was everything that happens to whoever it
   landed on: a hit that only prints a number is arithmetic, not a fight. So a struck fighter recoils
   and flashes in the colour of what hit it, a mended one lifts and glows, and anything carrying a
   mark keeps showing it for as long as it has it rather than only in a chip. */
@keyframes struck{
  0%{transform:none;filter:none}
  14%{transform:translate(-4px,1px);filter:brightness(2.4) saturate(.4)}
  30%{transform:translate(3px,-1px);filter:brightness(1.5)}
  52%{transform:translate(-2px,0)}
  74%{transform:translate(1px,0)}
  to{transform:none;filter:none}
}
@keyframes struck-hard{
  0%{transform:none;filter:none}
  10%{transform:translate(-7px,2px) scale(1.04);filter:brightness(3) saturate(.3)}
  26%{transform:translate(6px,-2px) scale(.985);filter:brightness(1.8)}
  46%{transform:translate(-4px,1px)}
  68%{transform:translate(2px,0)}
  to{transform:none;filter:none}
}
@keyframes mended{
  0%{transform:none;filter:none}
  40%{transform:translateY(-5px);filter:brightness(1.5) saturate(1.35)}
  to{transform:none;filter:none}
}
.fighter[data-struck] .shot{animation:struck .42s var(--ease)}
.fighter[data-struck="hard"] .shot{animation:struck-hard .52s var(--ease)}
.fighter[data-mended] .shot{animation:mended .5s var(--ease)}
/* a burst where the blow landed, in the colour the blow was */
.fighter .blow{
  position:absolute;left:50%;top:42%;width:54px;height:54px;margin:-27px 0 0 -27px;
  border-radius:50%;pointer-events:none;z-index:2;
  background:radial-gradient(circle,color-mix(in srgb,var(--tint,#fff) 85%,transparent) 0%,transparent 62%);
  animation:blow .44s var(--ease) forwards;
}
@keyframes blow{from{opacity:.95;transform:scale(.25)}to{opacity:0;transform:scale(1.9)}}

/* a mark is not only a chip: whoever carries one is tinted by it the whole time it lasts */
.fighter[data-marked]{position:relative}
.fighter[data-marked] .shot::after{
  content:"";position:absolute;inset:0;pointer-events:none;border-radius:var(--r-tile);
  background:radial-gradient(ellipse at 50% 72%,color-mix(in srgb,var(--marktint) 26%,transparent),transparent 70%);
  animation:marked 2.1s ease-in-out infinite;
}
@keyframes marked{0%,100%{opacity:.45}50%{opacity:.95}}

/* ---------- the skill, named ----------
   A skill that only shows its light asks the player to guess what happened. The name arrives with it
   and leaves before the next one, so the fight reads as a sequence of decisions rather than flashes. */
.skillname{
  /* the middle of the field, not the top of it: at 8% it sat under the header and behind anything tall */
  position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:6;pointer-events:none;
  display:grid;justify-items:center;gap:2px;text-align:center;white-space:nowrap;
  animation:skillname 1.5s var(--ease) both;
}
.skillname b{
  font-family:var(--serif);font-size:clamp(17px,4.6vw,23px);font-weight:500;letter-spacing:-.01em;
  color:var(--ink);text-shadow:0 2px 18px #000,0 0 34px color-mix(in srgb,var(--tint,var(--gold)) 70%,transparent);
}
.skillname span{
  font-size:var(--t-micro);letter-spacing:var(--track-micro);text-transform:uppercase;
  color:color-mix(in srgb,var(--tint,var(--gold)) 85%,var(--ink));
}
@keyframes skillname{
  0%{opacity:0;transform:translate(-50%,-50%) translateY(10px) scale(.94)}
  14%{opacity:1;transform:translate(-50%,-50%) translateY(0) scale(1)}
  72%{opacity:1;transform:translate(-50%,-50%) translateY(0)}
  to{opacity:0;transform:translate(-50%,-50%) translateY(-8px)}
}

/* ---------- before, and after ----------
   A fight used to start the moment the screen arrived and end as an attribute on the host. Both
   moments are worth standing still for: one says what you are walking into, the other says what it
   cost and asks where you want to go next. */
.verdict{
  position:fixed;inset:0;z-index:30;display:grid;place-items:center;padding:var(--s4);
  background:radial-gradient(ellipse at 50% 45%,#070912cc,#070912f2 70%);
  animation:verdict-in .34s var(--ease) both;
}
@keyframes verdict-in{from{opacity:0}to{opacity:1}}
.verdict[data-going]{animation:verdict-out .3s var(--ease) both}
@keyframes verdict-out{from{opacity:1}to{opacity:0}}
.verdict .card{
  width:min(var(--column),100%);display:grid;gap:var(--s3);justify-items:center;text-align:center;
  padding:var(--s5) var(--s4);border:1px solid var(--panel-line);border-radius:var(--r-panel);
  background:var(--panel);box-shadow:0 24px 80px #000a;
}
.verdict h2{
  margin:0;font-family:var(--serif);font-weight:500;font-size:clamp(26px,7vw,34px);
  line-height:1.05;letter-spacing:var(--track-display)
}
.verdict .eyebrow{color:color-mix(in srgb,var(--tint,var(--gold)) 80%,var(--ink))}
.verdict .lines{display:grid;gap:4px;width:100%}
.verdict .lines div{
  display:flex;justify-content:space-between;gap:var(--s3);
  font-size:var(--t-label);letter-spacing:var(--track-caption);text-transform:uppercase;color:var(--muted)
}
.verdict .lines b{color:var(--ink);font-weight:600;font-variant-numeric:tabular-nums}
.verdict .pair{display:grid;grid-template-columns:1fr 1fr;gap:var(--s2);width:100%}
.verdict .pair .btn{width:100%}
@media (prefers-reduced-motion:reduce){
  .fighter[data-struck] .shot,.fighter[data-struck="hard"] .shot,.fighter[data-mended] .shot,
  .fighter .blow,.skillname,.verdict,.fighter[data-marked] .shot::after{animation:none}
}

/* the nine on Home: one row, nine across, right above the dock */
.lineup{
  display:grid;grid-template-columns:repeat(9,1fr);gap:3px;align-items:end;
  flex:0 0 auto;cursor:pointer;padding:2px;border-radius:var(--r-control);
  transition:background-color .2s ease;
}
.lineup:hover{background:#ffffff05}
.lineup:active{transform:scale(.99)}
.lineup:focus-visible{outline:2px solid var(--gold);outline-offset:2px}
.pip{margin:0;position:relative;min-width:0;border-radius:6px}
.pip[data-rarity]{box-shadow:inset 0 -2px 0 color-mix(in srgb,var(--rarity) 85%,transparent)}
.pip[data-rarity="0"]{--rarity:var(--grey)}
.pip[data-rarity="1"]{--rarity:var(--green)}
.pip[data-rarity="2"]{--rarity:var(--blue)}
.pip[data-rarity="3"]{--rarity:var(--violet)}
.pip[data-rarity="4"]{--rarity:var(--gold)}
/* an empty seat stays visibly empty: it is the most useful thing Home can say */
/* an empty seat stands as tall as a filled one, or the row reads as two rows */
.pip[data-empty]{
  align-self:stretch;min-height:44px;border:1px dashed var(--line);border-radius:6px;background:#ffffff04;
}
.lineup{align-items:stretch}
