/* Titanus, visual system
   ---------------------------------------------------------------------------------------------
   The seed: a war table in an iron keep. Cold light through a high slit window, gunmetal and
   riveted plate, and one blood-red banner. Everything follows from that one image. The body of
   the thing is cold iron; every frame, rule and rim is the same iron polished; and the single hot
   color on any screen is the thing you are meant to press.

   Three rules, and the reason each one matters:

   1. THE NEUTRALS CARRY ONE HUE. Every surface step is the same cold gunmetal at a different
      lightness. A hue-free gray ramp is what makes a dark interface read as a code editor rather
      than as an object; a ramp with two or three hues in it reads as unfinished.
   2. IRON IS A MATERIAL, NOT A COLOR. A flat #8A94A6 fill is a gray rectangle. Metal needs a ramp
      with a cold-white specular, a hard jump into it and a long fall out of it, and a stop darker
      than whatever sits behind it, because the eye reads occlusion as reflectivity.
   3. BLOOD IS RATIONED. One filled crimson element per screen. Crimson as TEXT uses the light stop
      (--bl-light, 5.3:1); the body stop is 2.4:1 and is for fills, rules and glows only.

   Radii are 3, 6 or round. Values are deliberately quiet: structural bevels are 1px at 6-16%
   opacity and the page bloom is 6% white. Loud versions of these read as 2010 gloss.
   --------------------------------------------------------------------------------------------- */
:root {
  /* ---- surface: cold gunmetal, one hue (~215), low chroma, 4% to 16% lightness ---- */
  --ink:      #0B0D10;   /* the page itself */
  --sunken:   #0E1115;   /* wells, tracks, inset plaques */
  --panel:    #12161B;   /* the default panel */
  --panel2:   #1A1E24;   /* a raised panel: cards, boxes */
  --panel3:   #232830;   /* hover, modal, the nearest surface */
  --line:     #2C333C;   /* quiet division */
  --line2:    #3E4753;   /* a stated edge */

  /* ---- text: bone for content, a cooler ash ramp for chrome ---- */
  --text:     #E8E4DA;   /* 15.6:1 on --panel */
  --text-body:#C9C4B8;   /* 11.2:1, the size most of the game is read at */
  --muted:    #98A0AA;   /* 6.2:1, still passes AA, so usable for real content */
  --faint:    #646C76;   /* disabled only, never information */

  /* ---- iron, as an eight stop material ramp. This is every frame, rule and rim. ---- */
  --st-void:   #07090B;  /* inside an engraved cut, darker than any panel */
  --st-shadow: #1E242B;
  --st-dark:   #3A434E;
  --st-mid:    #5C6673;
  --st-body:   #8A94A6;  /* the local color of the metal */
  --st-light:  #B4BDC8;
  --st-bright: #D9DFE6;
  --st-spec:   #F6F8FA;  /* specular: cold white */

  /* ---- blood, as a ramp. This is the ONE thing on a screen you are meant to press. ---- */
  --bl-void:   #1A0306;
  --bl-shadow: #4A0B12;
  --bl-deep:   #6E1019;
  --bl-mid:    #8A1420;
  --bl-body:   #B01B2A;  /* the local color */
  --bl-warm:   #D0313F;
  --bl-light:  #E8697A;  /* blood as TEXT: 5.3:1 on --panel. --bl-body is 2.4:1 and is fills only */
  --bl-spec:   #FFD9DE;

  /* ---- what used to be blood, as TEXT (Dom, 2026-09-17: "we need to remove all red text") ----
     Red stays everywhere it is a surface: the primary button's field, the danger ring, the wash
     behind your own row on the leaderboard, every glow. Only letters changed, and they changed to
     the metal's own light, so a heading still sits a tier above body text and the hierarchy the
     red was carrying survives without it. Two steps, because the sheet had two: --bl-light for a
     heading and --gold-bright, which aliases onto it, for the brighter hover. */
  --ink-accent:    var(--st-light);   /* #B4BDC8 */
  --ink-accent-hi: var(--st-bright);  /* #D9DFE6 */

  /* Semantic aliases. The rest of the sheet speaks in these, so the accent is one swap. */
  --gold:        var(--bl-body);
  --gold-bright: var(--bl-light);
  --gold-dim:    var(--bl-shadow);
  /* The old brass names, kept as aliases onto iron so nothing dangles. Prefer --st-* in new code. */
  --au-void: var(--st-void); --au-shadow: var(--st-shadow); --au-olive: var(--st-dark);
  --au-mid: var(--st-mid); --au-body: var(--st-body); --au-warm: var(--st-light);
  --au-light: var(--st-bright); --au-spec: var(--st-spec);

  /* ---- the accents the game data already speaks in ---- */
  --violet: #A46BFF;  --cyan: #57D8E8;  --magenta: #FF5FB0;
  --amber:  #F0B94A;  --lime: #8FE07A;  --red:  #FF6A48;

  /* ---- type ---- */
  --font:    'Alegreya Sans', 'Segoe UI', system-ui, sans-serif;
  --display: 'Cinzel', Georgia, 'Times New Roman', serif;
  --serif:   'Alegreya', Georgia, serif;

  /* ---- radius: four values, nothing between ---- */
  /* "Square corners throughout, no radius anywhere in the system" (Buttons handoff, section 02),
     and every panel in every handoff file is drawn as a hard 1px rule with no rounding. The two
     surface tokens are therefore zero. --r-pill survives for progress bars and gauges only: a
     capsule that fills is a different object from a plate you press. */
  --r-sm: 0;    --r-md: 0;    --r-pill: 999px;
  --cut: 0px;

  /* ---- motion ---- */
  --dur-instant: 70ms; --dur-fast: 120ms; --dur-base: 200ms; --dur-panel: 300ms;
  /* How quickly the light comes up under the pointer, and how slowly it lets go (Dom, 2026-09-18:
     "lets have the hover fade in and how quickly"). In fast enough to feel like a response to the
     pointer, out slower so the button does not snap dark the moment you leave it. Both numbers are
     here rather than in the twenty rules that use them. */
  --dur-hover-in: 140ms; --dur-hover-out: 260ms;
  --ease-standard: cubic-bezier(.2, 0, .38, .9);
  --ease-entrance: cubic-bezier(0, 0, .38, .9);
  --ease-exit:     cubic-bezier(.2, 0, 1, .9);
  --ease-settle:   cubic-bezier(.22, 1, .36, 1);

  /* The sidebar reads mark-then-name in a row, so it holds the longest label, COLLECTION, beside
     a 20px mark. Measured, that needs 125px at the insets below; 136 leaves a little air without
     taking more off the content column than it has to. */
  --nav-w: 136px;
  --hdr-h: calc(58px + env(safe-area-inset-top, 0px));
  --nav-h: 0px;              /* on desktop the nav is down the side, so nothing sits at the bottom */
  --vh: 100vh;               /* replaced with the dynamic viewport below wherever it is supported */
}@supports (height: 100dvh) { :root { --vh: 100dvh; }}

/* ---- the material layer ----------------------------------------------------------------------
   Four recipes the whole sheet draws on. Every panel in the game is one of these plus content. */

/* Iron. The 177deg rather than 180 keeps the bands off-parallel to the box edges, which is what
   stops a wide element reading as "a CSS gradient". The jump into the specular is hard (2%) and
   the fall out of it is long (15%); a symmetric ramp reads as plastic. */
.mat-iron, .mat-gold {
  background-image: linear-gradient(177deg,
    var(--au-void) 0%, var(--au-shadow) 4%, var(--au-mid) 11%, var(--au-warm) 22%,
    var(--au-light) 33%, var(--au-spec) 37%, var(--au-spec) 39%, var(--au-light) 43%,
    var(--au-body) 52%, var(--au-olive) 62%, var(--au-mid) 70%, var(--au-warm) 80%,
    var(--au-light) 87%, var(--au-body) 93%, var(--au-shadow) 98%, var(--au-void) 100%);
}

/* A raised surface. Seven layers, only two of which exceed 1px. The two doing the real work are
   the wide insets with NEGATIVE spread: without it a large-blur inset bleeds around all four
   sides and turns the panel into a soft blob, and the outer ambient haloes past the element
   instead of tucking under it. That negative spread is the whole difference between this and a
   2010 bevel. */
.surface {
  position: relative;
  background: linear-gradient(180deg, var(--panel2) 0%, var(--panel) 58%, var(--sunken) 100%);
  box-shadow:
    inset 0 1px 0 rgba(214, 224, 236, .13),
    inset 0 -1px 0 rgba(0, 0, 0, .70),
    inset 1px 0 0 rgba(214, 224, 236, .04),
    inset -1px 0 0 rgba(0, 0, 0, .32),
    inset 0 28px 34px -30px rgba(214, 224, 236, .09),
    inset 0 -44px 48px -44px rgba(0, 0, 0, .55),
    0 0 0 1px rgba(0, 0, 0, .60),
    0 1px 2px rgba(0, 0, 0, .55),
    0 22px 44px -20px rgba(0, 0, 0, .60);
}
/* The same lighting, inverted: a cut into the surface rather than a plate laid on it. */
.well {
  background: linear-gradient(180deg, #080A0C, var(--sunken));
  box-shadow:
    inset 0 1px 0 rgba(0, 0, 0, .90),
    inset 0 2px 4px rgba(0, 0, 0, .60),
    inset 0 -1px 0 rgba(214, 224, 236, .07),
    0 1px 0 rgba(214, 224, 236, .05);
}

/* A brass rim that follows border-radius. border-image cannot do this: it ignores border-radius
   outright, which is spec rather than a bug. The conic source is what makes all four edges catch
   light differently, the way a real beveled frame does; a linear source lights two and flattens
   the other two. Write the WebKit mask line first and the standard one second: Firefox reads only
   the standard property and legacy WebKit only understands `xor`. */
.rim { position: relative; isolation: isolate; }
.rim::before {
  content: ''; position: absolute; inset: 0; padding: 1px; border-radius: inherit;
  pointer-events: none; z-index: 2;
  background: conic-gradient(from 218deg,
    var(--au-shadow) 0deg, var(--au-light) 38deg, var(--au-spec) 52deg, var(--au-body) 96deg,
    var(--au-olive) 156deg, var(--au-void) 194deg, var(--au-mid) 238deg, var(--au-spec) 292deg,
    var(--au-shadow) 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
}

/* Lettering cut into brass rather than printed on it. On a surface lit from above the TOP inner
   wall of a cut is in shadow and the BOTTOM wall catches the light, so the dark goes up and the
   highlight goes down. Getting that backwards is what makes engraved text look embossed. */
.engraved {
  color: var(--au-mid);
  text-shadow:
    0 -1px 0 rgba(0, 0, 0, .90), 0 -2px 3px rgba(0, 0, 0, .50),
    0 1px 0 var(--au-light), 0 1px 3px rgba(138, 148, 166, .30);
}
/* Brass lettering at display size. text-shadow does not work under background-clip: text, so the
   depth has to come from a filter: drop-shadow() stack. */
.gilt {
  background-image: linear-gradient(178deg,
    var(--au-shadow) 0%, var(--au-mid) 12%, var(--au-warm) 28%, var(--au-light) 40%,
    var(--au-spec) 47%, var(--au-spec) 50%, var(--au-light) 54%, var(--au-body) 62%,
    var(--au-olive) 72%, var(--au-mid) 82%, var(--au-warm) 92%, var(--au-shadow) 100%);
  -webkit-background-clip: text; background-clip: text;
  color: transparent; -webkit-text-fill-color: transparent;
  filter: drop-shadow(0 1px 0 rgba(0, 0, 0, .9)) drop-shadow(0 2px 1px rgba(0, 0, 0, .5))
          drop-shadow(0 6px 14px rgba(0, 0, 0, .5));
}

* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
body {
  font-family: var(--font); color: var(--text-body); background: var(--ink);
  background-image:
    radial-gradient(ellipse 78% 52% at 50% -12%, rgba(200, 214, 232, .06), transparent 62%),
    radial-gradient(ellipse 62% 46% at 50% 112%, rgba(4, 6, 9, .6), transparent);
  background-attachment: fixed;
  -webkit-font-smoothing: antialiased; overflow-x: hidden; touch-action: manipulation;
  font-size: 16.5px; line-height: 1.5;
}
/* One grain layer for the whole page, rasterized once into a 200px tile and repeated. Never
   filter: url(#noise) on a live element: feTurbulence is per-pixel Perlin noise on the CPU, and
   at 2x DPR across a full viewport that is tens of millions of evaluations and a visible stall.
   fractalNoise, not the default turbulence, which takes the absolute value and gives a hard
   veined ripple rather than grain. The feColorMatrix is the step most recipes omit: raw
   turbulence writes independent noise into every channel, so without desaturating it you get
   colored speckle. screen, not overlay: overlay resolves to multiply below 50% luminance and is
   a no-op on a near-black ground, which is exactly where this sits. */
body::after {
  content: ''; position: fixed; inset: 0; pointer-events: none; z-index: 100;
  opacity: .05; mix-blend-mode: screen;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='200'%20height='200'%3E%3Cfilter%20id='n'%20x='0'%20y='0'%20width='100%25'%20height='100%25'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.8'%20numOctaves='4'%20stitchTiles='stitch'%20seed='7'/%3E%3CfeColorMatrix%20type='saturate'%20values='0'/%3E%3C/filter%3E%3Crect%20width='200'%20height='200'%20filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 200px 200px;
}
button, input, select { font: inherit; color: inherit; }
button { cursor: pointer; }
h1, h2, h3 { margin: 0; font-family: var(--display); font-weight: 600; color: var(--text); }
/* Tracking is inversely proportional to size: display goes to zero, small caps labels get .15em
   and up. Uppercase is the trigger for tracking; mixed case never gets it. */
h1 { font-size: 2.05rem; letter-spacing: .01em; text-transform: uppercase; line-height: 1.1;
     font-weight: 700; }
h2 { font-size: 1.35rem; letter-spacing: .02em; line-height: 1.15; }
h3 { font-size: 1.02rem; margin-top: .9rem; letter-spacing: .04em; }
p { margin: .35rem 0; }
.muted { color: var(--muted); }
.small { font-size: .82rem; }
.good { color: var(--st-bright); }
.bad { color: var(--red); }
code { font-family: ui-monospace, monospace; }
/* THE red line (Dom, 2026-09-18: "remove the red glow or hover line from all buttons", with a
   picture of one skill button ringed in red among its neighbours). It was the global focus ring:
   `--gold` aliases onto --bl-body in this palette, so every focusable thing in the game took a
   blood outline, and focus SURVIVES the click that gave it, which is why it read as something the
   pointer was doing. `.btn` had overridden it with steel long ago and nothing else had.
   Steel everywhere now, and still 2px at 2px of offset: a focus ring is how the keyboard sees, and
   removing it is not on the table. */
:focus-visible { outline: 2px solid var(--st-bright); outline-offset: 2px; }
canvas#fx { position: fixed; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 50; }

/* header + nav */
/* Chrome is flush to its edge and reads as furniture. The brass hairline underneath is the one
   rule in the game that runs the full width, so it has to be a real edge: a dark line with a
   bright one immediately under it, which is how a metal band meets stone. */
/* THE CHROME'S OWN SCRIM, set by portraits.js beside the plate so it exists only when a texture
   does. 52% rather than the shared 30%, and the number is measured rather than chosen: the plate
   averages 0.16, the flat ramps it covers are 0.065 and 0.075, and 0.16 x 0.48 is 0.077. At the
   shared 30% the header ran at 0.134, near double the tone it was replacing, which is what Dom
   was looking at when he said it needs to be darker. The grain survives: the plate's spread is 23
   and 48% of that is still 11 levels, which reads. */
/* The lintel (Dom, 2026-09-19: "we need background materials for all other UI elements").
   `cover` on a 4:1 plate in a 58px bar shows a thin horizontal slice of it, magnified, which is
   exactly what the card sill does and is why the plate is asked for without bolts or seams:
   a feature that lands half in and half out of a 12% crop reads as damage.
   The blur goes when a texture arrives, because a backdrop blur behind an opaque photograph is
   work done for nothing; without one the header keeps the translucency it shipped with. */
.hdr { position: fixed; top: 0; left: 0; right: 0; height: var(--hdr-h); display: flex;
  align-items: center; justify-content: space-between; padding: env(safe-area-inset-top, 0px) 18px 0;
  background-color: #0D1013;
  background-image: var(--ui-chrome-scrim, none), var(--ui-chrome, none), linear-gradient(180deg, #15191EF2, #0D1013F2);
  background-size: auto, cover, auto; background-position: center; background-repeat: no-repeat;
  backdrop-filter: blur(10px);
  box-shadow: 0 1px 0 rgba(0, 0, 0, .8), 0 2px 0 rgba(138, 148, 166, .22), 0 10px 26px -12px #000;
  z-index: 20; }
.brand { font-family: var(--display); font-weight: 700; font-size: 1.28rem; letter-spacing: .2em;
  text-decoration: none; background-image: linear-gradient(178deg, var(--au-mid) 0%,
    var(--au-warm) 26%, var(--au-spec) 46%, var(--au-light) 54%, var(--au-body) 68%,
    var(--au-olive) 86%, var(--au-shadow) 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  -webkit-text-fill-color: transparent;
  filter: drop-shadow(0 1px 0 rgba(0, 0, 0, .9)) drop-shadow(0 0 16px rgba(138, 148, 166, .3)); }
.hdr-stats { display: flex; gap: 8px; align-items: center; }
.hdr-pill { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px;
  border-radius: 0; font-size: .85rem; color: var(--text-body);
  background: linear-gradient(180deg, #080A0C, var(--sunken));
  box-shadow: inset 0 1px 0 rgba(0, 0, 0, .9), inset 0 2px 4px rgba(0, 0, 0, .55),
              inset 0 -1px 0 rgba(214, 224, 236, .07), 0 1px 0 rgba(214, 224, 236, .05); }
.hdr-pill b { color: var(--text); font-weight: 700; }
.hdr-btn { background: none; border: 1px solid var(--line); border-radius: 0;
  padding: 4px 10px; font-size: .8rem; color: var(--muted);
  transition: color var(--dur-fast) var(--ease-standard), border-color var(--dur-fast) var(--ease-standard); }
.hdr-btn:hover { color: var(--ink-accent-hi); border-color: var(--st-dark); }
/* The rail, and the bottom bar on a phone: one element at two shapes ninety degrees apart, which
   is why its plate is cast rather than brushed and is shot square. `cover` then crops a different
   part of the same square for each, and neither is wrong, because a cast surface has no direction
   to be wrong about. */
.nav { position: fixed; top: var(--hdr-h); bottom: 0; left: 0; width: var(--nav-w); display: flex;
  flex-direction: column; gap: 2px; padding: 14px 0; z-index: 20;
  background-color: #0D1013;
  background-image: var(--ui-chrome-scrim, none), var(--ui-chrome, none), linear-gradient(90deg, #111418, #0D1013);
  background-size: auto, cover, auto; background-position: center; background-repeat: no-repeat;
  box-shadow: 1px 0 0 rgba(0, 0, 0, .8), 2px 0 0 rgba(138, 148, 166, .13), 14px 0 30px -18px #000; }

/* ONE PIECE OF FURNITURE ON DESKTOP (Dom, 2026-09-20: "The nav and desktop needs to be one
   cohesive piece when on desktop view").
   The header and the rail already wear the same plate, and that is exactly why they read as two
   things rather than one: each `cover`-fits it to its OWN box. Measured on an 837x915 window,
   the header lays the 1024x768 plate out at 826x620 and shows the middle 58px of it; the rail
   lays the same plate out at 1143x857 and shows the middle 136px. The rail's material is 38%
   larger than the header's and comes from an unrelated part of the picture, so the grain cannot
   meet at the corner and the two average to different tones. Same fault as the card crest's
   doubled rivets, same fix: REGISTER BOTH TO ONE GEOMETRY rather than letting each fit its own.
   The geometry is the viewport. The plate is sized to cover it once, anchored at the top left,
   and the rail simply shifts its copy up by the header's height, so the two elements are windows
   onto one continuous slab and the material runs round the corner unbroken.
   `background-attachment: fixed` would do the same in one word. It is not used because it is the
   classic scroll-repaint hazard and iOS treats it inconsistently, and this arithmetic costs
   nothing at paint time. 1.334 is the plate's own 1024x768; `auto` then takes the real aspect, so
   a regenerated plate of the same shape needs no change here.
   Only on desktop, because below 761px the rail becomes a bar across the foot of the screen and
   the two are not touching at all. */
@media (min-width: 761px) {
  :root { --chrome-w: max(100vw, calc(100vh * 1.334)); }
  /* `background-origin` MUST be the border box here. It defaults to the padding box, and these
     two have different padding (the header 18px at the sides, the rail 14px top and bottom), so
     with the default the header's copy of the plate starts 18px in from the window edge and the
     rail's starts 14px below its own top. Same size, same stated position, still eighteen pixels
     out of register: the kind of miss that looks like the texture simply not matching. */
  .hdr {
    background-origin: border-box;
    background-size: auto, var(--chrome-w) auto, auto;
    background-position: center, left top, center; }
  .nav {
    background-origin: border-box;
    background-size: auto, var(--chrome-w) auto, auto;
    background-position: center, left calc(-1 * var(--hdr-h)), center; }

  /* And ONE inner edge, which turns the corner instead of crossing it. The header's lit lip ran
     the full width, so it cut straight across the top of the rail, and the two lips were not even
     the same brightness (.22 against .13): a single piece of metal has one edge where it meets
     the room, and here that edge runs along the underside of the lintel and then turns down the
     side of the rail. The header's own edge is redrawn as a strip that starts where the rail
     ends. It is absolutely positioned, so it is out of flow and does not become a flex item in
     the header's `space-between` row. */
  .hdr { box-shadow: 0 10px 26px -12px #000; }
  .hdr::after {
    content: ''; position: absolute; left: var(--nav-w); right: 0; top: 100%; height: 2px;
    background: linear-gradient(180deg, rgba(0, 0, 0, .8) 0 1px, rgba(138, 148, 166, .2) 1px 2px);
    pointer-events: none; }
  .nav { box-shadow: 1px 0 0 rgba(0, 0, 0, .8), 2px 0 0 rgba(138, 148, 166, .2),
                     14px 0 30px -18px #000; }
}

/* The sidebar reads left to right like a list: mark, then name. */
.nav-btn { background: none; border: 0; border-radius: 0; color: var(--muted); overflow: hidden;
  display: flex; flex-direction: row; align-items: center; justify-content: flex-start;
  gap: 10px; padding: 11px 11px; position: relative;
  font-family: var(--font); font-size: 12px; font-weight: 500;
  letter-spacing: .1em; text-transform: uppercase;
  transition: color 90ms linear, background 90ms linear; }
@media (hover: hover) {
  .nav-btn:hover { color: var(--text-body); background: rgba(255, 255, 255, .035); }
}
.nav-btn .nav-ic-old { font-size: 1.3rem; }

/* THE BLOOD RAIL. Three parts and the order matters: a 3px bar hard against the edge carrying its
   own ramp and a 1px specular on its inner face; a single linear wash running off it, 30% to
   zero across about three quarters of the item, which is the light it throws; then the mark and
   the label on the blood-light ramp.
   What made the old one muddy was the glow blurring in every direction at once. Light has a
   source: anchoring it to the bar and letting it fall one way turns a smear into an object with a
   lamp beside it. Still inside the blood rule, because a rail and a wash are not a filled
   element, so the screen's one blood fill stays free for the primary action. */
.nav-btn { z-index: 1; }
.nav-btn.active { color: var(--ink-accent); font-weight: 700; }

/* The rail is ONE object for the whole nav, not a state each item owns, so changing tabs moves
   the lamp instead of switching one off and another on. It carries both parts: the wash is its
   background, the 3px bar is its leading edge. */
.nav-rail { position: absolute; left: 0; top: 0; width: 100%; height: 0; z-index: 0;
  pointer-events: none; opacity: 0; }
.nav-rail.on { opacity: 1; }
/* no transition until it has been placed once, or it slides in from the top corner on load */
.nav-rail.ready { transition: transform 260ms cubic-bezier(.22, 1, .36, 1),
                              height 260ms cubic-bezier(.22, 1, .36, 1),
                              width 260ms cubic-bezier(.22, 1, .36, 1),
                              opacity 140ms linear; }
/* The specular sits on the OUTER face. On the inner face it put a pale line between the bar and
   the icon, so the item read as two stripes, a yellow one and a red one, instead of one object.
   Red is now the edge closest to the mark. */
.nav-rail::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
  background: linear-gradient(180deg, #6E1019 0%, #B01B2A 34%, #D6414F 50%, #B01B2A 66%, #6E1019 100%);
  box-shadow: inset 1px 0 0 rgba(255, 217, 222, .5); }
/* The lamp swelling and settling as the rail arrives. It is not a fade-out: the glow comes UP
   and goes back DOWN, which is what a light does when something is set down beside it. The
   transform scales the rendered shadow too, so the red pushes out from the bar and draws back in
   rather than only changing opacity.
   Timed to the landing, not the departure. The rail's own curve is heavily front-loaded and is
   effectively home by about 140ms, so a 40ms delay with the peak at 28% of 500ms puts the
   brightest moment at ~180ms: as it arrives. */
/* Two layers, because the red and the white do different work.

   The WASH is the red gradient ARRIVING. It starts at nothing, throws outward off the bar past
   where it settles, and comes to rest fully revealed: that is the active state, not a pulse, so
   the animation holds its last frame and the resting rule is opacity 0 underneath it. Moving to
   another tab drops the class, which puts it back to nothing, and it reveals again on the new one.

   The CORE is the white strike on the bar itself. That one IS transient: it swells and is gone. */
.rail-wash { position: absolute; inset: 0; opacity: 0; pointer-events: none;
  background-repeat: no-repeat;
  background-image: linear-gradient(90deg, rgba(214, 65, 79, .78) 0%, rgba(176, 27, 42, .42) 22%, rgba(176, 27, 42, 0) 80%); }
.nav-rail.flare .rail-wash { animation: railWash 560ms cubic-bezier(.16, 1, .3, 1) 110ms forwards; }
@keyframes railWash {
   0%  { opacity: 0; background-size:   0% 100%; }
  18%  { opacity: 1; background-size:  46% 100%; }
  62%  { opacity: 1; background-size: 116% 100%; }
 100%  { opacity: 1; background-size: 100% 100%; }
}
.nav-rail::after { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
  opacity: 0; background: #FFD9DE; transform-origin: left center;
  box-shadow: 0 0 18px 4px rgba(232, 105, 122, .9); }
.nav-rail.flare::after { animation: railFlare 500ms cubic-bezier(.33, 0, .2, 1) 40ms; }
@keyframes railFlare {
   0%  { opacity: 0;   transform: scaleX(1); }
  28%  { opacity: .95; transform: scaleX(2.4); }
 100%  { opacity: 0;   transform: scaleX(1); }
}
@media (prefers-reduced-motion: reduce) {
  .nav-rail.ready { transition: none; }
  .nav-rail.flare::after, .nav-rail.flare .rail-wash { animation: none; }
}
/* Full width on the desktop. The cap was 1320px INCLUDING the rail and the padding (box-sizing is
   border-box globally), so a 2000px window drew a 1150px column of cards and left the rest of the
   screen empty (Dom, 2026-09-13). Every card grid here is auto-fill/minmax, so removing the cap
   adds columns rather than stretching anything. Prose does not get wider: .page-head is already
   held at 70ch, which is what a line length cap is actually for. */
.main { padding: calc(var(--hdr-h) + 20px) 24px 40px calc(var(--nav-w) + 24px); max-width: none; }
.screen { display: none; }
.screen.active { display: block; }
.page-head { margin-bottom: 20px; max-width: 70ch; }
.page-head h1 {
  background-image: linear-gradient(178deg, var(--au-mid) 0%, var(--au-warm) 22%,
    var(--au-light) 40%, var(--au-spec) 48%, var(--au-light) 56%, var(--au-body) 70%,
    var(--au-olive) 88%, var(--au-shadow) 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  -webkit-text-fill-color: transparent;
  filter: drop-shadow(0 1px 0 rgba(0, 0, 0, .9)) drop-shadow(0 2px 1px rgba(0, 0, 0, .5))
          drop-shadow(0 4px 18px rgba(138, 148, 166, .18)); }
.page-head p { color: var(--muted); }
/* The rule under a page title: a hairline that fades at both ends with a small brass lozenge set
   into the middle of it. Two pseudo-elements, no assets. */
.page-head::after { content: ''; display: block; height: 1px; margin: 12px 0 0;
  background: linear-gradient(90deg, transparent, rgba(138, 148, 166, .45) 12%,
    rgba(138, 148, 166, .45) 88%, transparent); }

/* ---------------------------------------------------------------- buttons --
   From the Buttons handoff. Its section 02 is treatment-independent ("geometry, type and sizing
   are identical across all five; only the surface changes"), so those numbers are absolute here:
   44px tall (40 in dialogs, 34 segmented), SQUARE, 26px of horizontal padding (18 for ghost),
   Alegreya Sans 700 at 15px, uppercase, .1em tracking, and 90ms linear on everything.

   Four of the five surfaces are in use, one job each (Dom's call, 2026-09-11):
     1a struck plate  -> ACT      the default and .primary, raised above the panel
     1b keyline       -> SELECT   .flat, level with it, all the information in the edge
     1c cast iron     -> INERT    .sunken and :disabled, sitting below its own frame
     1d enamel inlay  -> PREMIUM  .enamel, the paid thing, and nothing else
     1e etched        -> unused: it runs bright plates, which breaks both "cold and desaturated
                        by default" and the blood rule, since a light plate outshouts the fill.

   The one deliberate deviation: the handoff paints grain with filter:url(#grain) on the live
   element. feTurbulence is per-pixel Perlin noise on the CPU and this codebase already banned it
   once for exactly that reason (see the page grain layer above, and the phone-heat fix). Same
   look, rasterized once into a 120px tile and repeated. */

.btn { position: relative; overflow: hidden; cursor: pointer;
  height: 44px; padding: 0 22px;
  border: 4px solid transparent; border-radius: 0;
  /* The frame is the border itself, drawn with the steel ramp, so no call site needs an inner
     wrapper element. border-image slices the vertical gradient so the top edge takes the light,
     the bottom takes the shade and the sides carry the fall between them: a struck steel surround
     with the field set into it. background-clip stops the field painting under that surround. */
  border-image: linear-gradient(180deg, #6E7885 0%, #39414D 50%, #242B34 100%) 1;
  /* The field is generated rock over the painted ramp (Dom, 2026-09-17: "metal buttons with black
     volcanic rock backgrounds that the text overlays onto"). `--ui-btn` is set by portraits.js from
     the manifest, so the gradient underneath is not a fallback that never runs: it is what every
     button looks like until the texture is generated, and what it keeps looking like on a build
     with no art at all. The ramp stays under the rock either way and does the shading, which is
     why one flat-lit texture reads as a lit control at every size.
     `cover` and not `repeat`: a button is between 90 and 900 pixels wide and the rock is one slab,
     so the crop moves rather than the scale, and nothing tiles visibly. */
  /* Dom, 2026-09-17: "change the overlay to the main button to a 30% opacity black." The rock came
     back lighter and busier than the field it replaced, so a flat 30% black sits over it and holds
     it down under the label.
     `--ui-scrim` is set by portraits.js only when there is actually a texture, which is what keeps
     the scrim off the painted ramp: with no art both layers are `none` and the button is exactly
     what it was. */
  /* A color under the images: any frame in which they had not painted showed the browser's own
     light-gray button face, so a row of action buttons flashed pale with unreadable labels (blind
     playtest round 1, 2026-09-23). */
  background-color: #1C212A;
  background-image: var(--ui-scrim, none), var(--ui-btn, none), linear-gradient(180deg, #1C212A, #2C3441);
  background-size: auto, cover, auto;
  background-position: center, center, center;
  background-clip: padding-box;
  font-family: var(--font); font-size: 15px; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center; gap: 12px;
  color: #F6F8FA;
  /* The label sits on stone rather than on a flat field, so it carries its own shadow: without it
     a bright crack running under a letter takes the letter with it. Costs the untextured button a
     pixel of depth it can afford. */
  text-shadow: 0 1px 2px rgba(0, 0, 0, .85);
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .7), inset 0 -1px 0 rgba(217, 223, 230, .16),
              0 0 0 1px #0B0D10, 0 2px 6px rgba(0, 0, 0, .6);
  transition: background 90ms linear, box-shadow 90ms linear, transform 90ms linear, color 90ms linear; }
.btn > * { position: relative; }

/* Hover is a LIGHT, not a different button (Dom, 2026-09-18: "I don't like button hover,
   background change. Lets use gradients to show hover"). The ramp and the rock stay exactly as
   they are and a gradient is laid over the top: bright where a light above the screen would catch
   the upper edge, gone by halfway down. Nothing under it moves, so the control does not read as
   two objects, one of which you only ever see for a moment.
   It is a LAYER and not a fourth background image, because background-image does not interpolate:
   swapping the stack is a switch, and Dom asked for a fade. An element's opacity does interpolate,
   so the light is a pseudo-element that comes up and goes down.
   inset: 0 on a relatively positioned button is its PADDING box, so the light falls on the field
   and stops at the frame, which is where a light on a piece of metal in a frame would stop. */
.btn::after {
  content: ''; position: absolute; inset: 0; pointer-events: none; opacity: 0;
  background: linear-gradient(0deg, rgba(232, 240, 250, .17) 0%, rgba(232, 240, 250, .05) 38%, rgba(232, 240, 250, 0) 72%);
  transition: opacity var(--dur-hover-out) var(--ease-standard); }
.btn:hover:not(:disabled)::after, .btn:focus-visible:not(:disabled)::after {
  opacity: 1; transition-duration: var(--dur-hover-in); }
/* The blood field takes a warmer light than the iron does. */
.btn.primary::after {
  background: linear-gradient(0deg, rgba(255, 228, 232, .2) 0%, rgba(255, 228, 232, .06) 38%, rgba(255, 228, 232, 0) 72%); }
/* A press puts the light out: the face is tipped away from it. */
.btn:active:not(:disabled)::after { opacity: 0; transition-duration: var(--dur-instant); }
/* press: the field sinks further into its frame and the whole piece drops a pixel */
.btn:active:not(:disabled) { transform: translateY(1px);
  box-shadow: inset 0 3px 7px rgba(0, 0, 0, .85), 0 0 0 1px #0B0D10; }
.btn:focus-visible { outline: 2px solid var(--st-bright); outline-offset: 2px; }

/* the screen's one filled blood element: the enamel field runs blood, the metal never changes */
.btn.primary { color: var(--st-spec);
  /* The one button on a screen that is filled rather than framed, so this is the one place the
     rock does not simply cover what is under it: soft-light keeps the blood ramp's color and takes
     only the stone's light and shade, so the primary stays the loud one and gains a surface. Laid
     over the rock at `normal` it went black, and a black primary is not a primary. */
  background-image: var(--ui-btn-primary, none), linear-gradient(180deg, #7E1420, #A61E2C);
  background-size: cover, auto; background-position: center, center;
  background-blend-mode: soft-light, normal;
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .6), inset 0 -1px 0 rgba(232, 105, 122, .3),
              0 0 0 1px #0B0D10, 0 2px 6px rgba(0, 0, 0, .6); }

.btn.primary:active:not(:disabled) {
  box-shadow: inset 0 3px 7px rgba(0, 0, 0, .8), 0 0 0 1px #0B0D10; }

/* Destructive keeps the blood EDGE and never the fill, so the field stays ink and takes a blood
   ring inside the frame. The surround darkens rather than brightening. */
.btn.danger { color: var(--ink-accent);
  border-image: linear-gradient(180deg, #4A5460 0%, #2A313B 50%, #1A1E24 100%) 1;
  background-image: var(--ui-scrim, none), var(--ui-btn, none), linear-gradient(180deg, #14181E, #14181E);
  background-size: auto, cover, auto; background-position: center, center, center;
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .7), inset 0 0 0 1px #6E1019, 0 0 0 1px #0B0D10; }
.btn.danger:hover:not(:disabled) { color: var(--st-spec); background: #191E26;
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .7), inset 0 0 0 1px #B01B2A, 0 0 0 1px #0B0D10,
              0 0 12px rgba(176, 27, 42, .35); }

/* Premium: the same inlay, in polished metal. The field is what the enamel says; the frame is
   what the money says. */
.btn.premium { border-image: linear-gradient(180deg, #AEBBD0 0%, #6E7885 50%, #39414D 100%) 1;
  background: linear-gradient(180deg, #7E1420, #A61E2C); color: var(--st-spec);
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .6), inset 0 -1px 0 rgba(232, 105, 122, .3),
              0 0 0 1px #0B0D10, 0 2px 8px rgba(0, 0, 0, .6); }
.btn.premium:hover:not(:disabled) { background: linear-gradient(180deg, #8A1622, #B42432); }

/* ghost: no inlay at all, a rule that arrives on hover */
.btn.ghost { padding: 0 18px; font-weight: 500; letter-spacing: .08em; color: #8A94A6;
  border: 0; background: none; box-shadow: inset 0 -1px 0 rgba(138, 148, 166, 0); }
.btn.ghost:hover:not(:disabled) { color: #D9DFE6; background: none; box-shadow: inset 0 -1px 0 #8A94A6; }
.btn.ghost:active:not(:disabled) { color: #F6F8FA; transform: none; box-shadow: inset 0 -1px 0 #8A94A6; }

/* 1b keyline: SELECT. Flat ink face, everything in the edge, no frame. */
.btn.flat { border: 0; background: #15181D; color: #D9DFE6;
  box-shadow: inset 0 0 0 1px #5B6472, 0 0 0 1px #0B0D10; }
.btn.flat:hover:not(:disabled) { background: #1A1E24; box-shadow: inset 0 0 0 1px #8A94A6, 0 0 0 1px #0B0D10; }
.btn.flat.on { color: var(--st-spec);
  box-shadow: inset 0 0 0 1px #B01B2A, 0 0 0 1px #0B0D10, inset 0 0 18px rgba(176, 27, 42, .22); }

/* 1c cast iron: INERT. */
.btn.sunken { border: 0; background: #39414D; color: #D9DFE6;
  box-shadow: inset 0 3px 7px rgba(0, 0, 0, .55), inset 0 -1px 0 rgba(217, 223, 230, .14), 0 0 0 1px #0B0D10; }

/* "a plate with the light switched off, not a faded one": the frame goes too */
/* `background` and not `background-image`, on purpose: the shorthand clears the texture too, so a
   disabled control is flat and dead rather than a piece of stone that happens not to respond. */
.btn:disabled { cursor: not-allowed; transform: none; color: #5B6472; text-shadow: none;
  border: 0; background: #171C22; box-shadow: inset 0 0 0 1px #2A313B; }

/* a count carried inside a button, on its own recessed field */
.btn .cost { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px;
  background: #0F1318; box-shadow: inset 0 0 0 1px #4A5460; color: var(--ink-accent-hi);
  font-size: 14px; font-weight: 700; letter-spacing: .04em;
  font-variant-numeric: lining-nums tabular-nums; }
.btn.primary .cost, .btn.premium .cost { box-shadow: inset 0 0 0 1px rgba(232, 105, 122, .45); }

/* held, with a specular crossing the field */
.btn.loading { cursor: wait; pointer-events: none;
  background: linear-gradient(180deg, #5A0C14, #7E1420); }
/* This takes over the same ::after the hover light lives on, so it has to turn it back on: the
   light rests at opacity 0 and a loading button has pointer-events: none, so without this the
   sweep would have been invisible from the moment hover became a fading layer. */
.btn.loading::after { content: ''; position: absolute; top: 0; bottom: 0; width: 40%;
  pointer-events: none; opacity: 1; transition: none; animation: btnSweep 1.4s linear infinite;
  background: linear-gradient(100deg, rgba(255, 255, 255, 0), rgba(255, 255, 255, .22), rgba(255, 255, 255, 0)); }
.btn .spin { width: 14px; height: 14px; border-radius: 50%; flex: 0 0 auto;
  border: 2px solid rgba(255, 217, 222, .3); border-top-color: var(--st-spec);
  animation: btnSpin .8s linear infinite; }
@keyframes btnSweep { 0% { transform: translateX(-120%); } 100% { transform: translateX(320%); } }
@keyframes btnSpin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .btn.loading::after, .btn .spin { animation: none; } }

/* the three heights the system has */
.btn.small { height: 34px; padding: 0 14px; font-size: 13px; gap: 8px; border-width: 3px; }
.btn.small .cost { padding: 2px 7px; font-size: 12px; }
.btn.xl { height: 44px; padding: 0 32px; }
.modal-actions .btn { height: 40px; padding: 0 18px; font-size: 14px; }
.modal-actions .btn.ghost { padding: 0 18px; }

/* brass keeps its name and its job: a call to action that must not read as THE primary. It is
   the iron plate with gold lettering, which is the blood rule holding. */
/* brass keeps its name from the old palette but the color token now aliases onto blood, which
   is correct for its job: a call to action carrying the blood SIGNAL without the blood FILL, so
   it can sit on a screen that already has its one primary. Destructive is still distinct, because
   destructive adds the full blood ring. The hover used to jump to gold, which belonged to a
   palette this game does not have any more. */
.btn.brass { color: var(--ink-accent); }
.btn.brass:hover:not(:disabled) { color: var(--st-spec);
  box-shadow: inset 0 1px 0 #8A94A6, inset 0 -2px 0 #0B0D10,
              inset 1px 0 0 rgba(232, 105, 122, .22), inset -1px 0 0 rgba(0, 0, 0, .5),
              0 0 0 1px #0B0D10, 0 2px 6px rgba(0, 0, 0, .6), 0 0 12px -2px rgba(176, 27, 42, .4); }
.btn small { display: block; font-weight: 400; font-size: 11px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted); text-shadow: none; }
.btn.net { color: var(--ink-accent-hi); }
.btn.net.pulse { color: var(--magenta); }

/* ------------------------------------------------- roles: select, and inert --
   1b keyline is SELECT. These four families are all "pick one of these" controls, and they were
   each carrying their own border color, radius and tinted fill. One idiom now: a flat ink face,
   square, and a single rule that carries the whole state. Chosen takes the blood rule, which is
   the same signal the primary plate uses without the fill, so a grid of options can never read as
   louder than the action button under it.
   1c cast iron is INERT: a locked or claimed tile sits BELOW its frame rather than fading out,
   which is the same reasoning the handoff gives for the disabled plate. */
.conj-opt, .build-chip, .equip-opt, .conj-form {
  border: 0; border-radius: 0; background: #15181D;
  box-shadow: inset 0 0 0 1px #2A313B, 0 0 0 1px #0B0D10;
  transition: background 90ms linear, box-shadow 90ms linear, color 90ms linear; }
.conj-opt:hover:not(:disabled), .build-chip:hover:not(.active),
.equip-opt:hover, .conj-form:hover {
  background: #1A1E24; box-shadow: inset 0 0 0 1px #5B6472, 0 0 0 1px #0B0D10; transform: none; }
.conj-opt.on, .build-chip.active, .conj-form.on {
  background: #15181D; color: var(--text);
  box-shadow: inset 0 0 0 1px #B01B2A, 0 0 0 1px #0B0D10, inset 0 0 18px rgba(176, 27, 42, .22); }
.conj-opt.short { opacity: 1; color: var(--muted);
  box-shadow: inset 0 0 0 1px #1E242B, 0 0 0 1px #0B0D10; }

/* the season track's tiles: square, and inert rather than faded */
.season-pip { border: 0; border-radius: 0;
  box-shadow: inset 0 0 0 1px #2A313B, 0 0 0 1px #0B0D10; }
.season-pip.can { box-shadow: inset 0 0 0 1px #B01B2A, 0 0 0 1px #0B0D10, inset 0 0 18px rgba(176, 27, 42, .22); }
.season-pip.claimed, .season-pip.locked { opacity: 1; background: #1C212A; color: #5B6472;
  box-shadow: inset 0 3px 7px rgba(0, 0, 0, .55), inset 0 -1px 0 rgba(217, 223, 230, .08), 0 0 0 1px #0B0D10; }
.season-pip.claimed i, .season-pip.locked i { color: #5B6472; }

/* -------------------------------------------- the rest of the controls --
   Squaring the radius tokens was only half of it. Most of what reads as a button in this game
   does not carry .btn: the nav, a zone's Enter, the battle skills, the team toggles, the header
   sound switch. They kept their own borders and mixed-case labels, so a screen could be entirely
   on the new system and still look untouched. These carry the system's TYPE (Alegreya Sans,
   uppercase, .1em) and its two surfaces: keyline for select, plate for act. */

/* select: a rule carries the state, and the chosen one takes the blood rule without the fill */
.skill, .team-tog, .ts-btn, .ts-lead, .more-btn, .lead-opt, .speed-btn, .hdr-btn, .track-arrow {
  border: 0; border-radius: 0; background: #15181D; color: #D9DFE6;
  box-shadow: inset 0 0 0 1px #2A313B, 0 0 0 1px #0B0D10;
  transition: background 90ms linear, box-shadow 90ms linear, color 90ms linear; }
/* `.skill:not(.big)` throughout, because the battle button left this system (Dom, 2026-09-18:
   "the skill button hover still does not work right and I am seeing red"). These rules are
   (0,3,0) with their :not, and the battle button's own rules are (0,2,0), so on hover this one
   was winning: `background` is a SHORTHAND, so it wiped the lava rock back to a flat #1A1E24 and
   took the tier's glow with it, and `.skill.active` was painting a blood ring and a blood inner
   glow on top. The loadout picker still wears all of it; the thing in a fight does not. */
.skill:not(.big):hover:not(:disabled), .team-tog:hover:not(:disabled), .ts-btn:hover:not(:disabled),
.ts-lead:hover, .more-btn:hover, .lead-opt:hover, .speed-btn:hover, .hdr-btn:hover,
.track-arrow:hover { background: #1A1E24; box-shadow: inset 0 0 0 1px #5B6472, 0 0 0 1px #0B0D10; }
.skill:not(.big).active, .team-tog.on, .lead-opt.on, .speed-btn.on {
  color: var(--st-spec); box-shadow: inset 0 0 0 1px #B01B2A, 0 0 0 1px #0B0D10, inset 0 0 18px rgba(176, 27, 42, .22); }
.skill:not(.big):disabled, .team-tog:disabled, .ts-btn:disabled {
  background: #171C22; color: #5B6472; box-shadow: inset 0 0 0 1px #2A313B; }

/* the battle skills keep their element rule down the left edge, which is the one thing they
   carry that a plain selector does not */
.skill { border-left: 3px solid var(--c); }

/* the type the system uses, on the controls that were still mixed case */
.team-tog, .ts-btn, .ts-lead, .speed-btn, .depth b, .more-btn {
  font-family: var(--font); text-transform: uppercase; letter-spacing: .1em; font-weight: 700; }

/* ---- hunt depths ----
   Three per zone, stacked, because the zones column is 230px at its narrowest and three of these
   side by side would break every name onto its own line. The row IS the button: the depth's name
   on the left, the band it rolls on the right, its odds under both. A shut one is an iron plate
   with the light switched off rather than a faded copy of an open one, which is the same rule the
   rest of the button system follows. */
.zone-depths { margin-top: auto; display: flex; flex-direction: column; gap: 6px; }
.depth { position: relative; display: grid; grid-template-columns: 1fr auto; column-gap: 10px;
  align-items: baseline; text-align: left; cursor: pointer; padding: 8px 11px;
  border: 0; border-radius: 0; color: var(--text-body); font-family: var(--font);
  background: linear-gradient(180deg, #1C212A, #151A21);
  box-shadow: inset 0 0 0 1px #39414D, inset 0 1px 0 rgba(217, 223, 230, .07), 0 1px 2px rgba(0, 0, 0, .5);
  transition: background 90ms linear, box-shadow 90ms linear, color 90ms linear; }
.depth b { grid-column: 1; grid-row: 1; font-size: .8rem; color: var(--e); }
.depth i { grid-column: 2; grid-row: 1; justify-self: end; font-style: normal; font-size: .8rem;
  color: var(--text); font-variant-numeric: lining-nums tabular-nums; }
.depth u { grid-column: 1; grid-row: 2; text-decoration: none; font-size: .72rem; color: var(--muted); }
.depth em { grid-column: 1 / -1; grid-row: 3; font-style: normal; font-size: .71rem;
  letter-spacing: .05em; text-transform: uppercase; color: var(--ink-accent); margin-top: 2px; }
.depth .depth-lock { grid-column: 2; grid-row: 2; justify-self: end; align-self: center;
  font-size: .78rem; color: var(--st-light); }
.depth:hover:not(:disabled), .depth:focus-visible {
  background: linear-gradient(180deg, #262D38, #1B212A);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--e) 55%, #39414D),
              inset 0 1px 0 rgba(217, 223, 230, .13), 0 2px 6px rgba(0, 0, 0, .6); }
.depth:focus-visible { outline: none; }
.depth.shut { cursor: not-allowed;
  background: linear-gradient(180deg, #14181E, #10141A);
  box-shadow: inset 0 0 0 1px #262C35, inset 0 2px 4px rgba(0, 0, 0, .7); }
.depth.shut b { color: #5B6472; }
.depth.shut i { color: var(--st-light); }

/* nav: not a plate, because a persistent bar of plates would fight every screen for attention.
   It takes the system's type and its active rule, and nothing else. */
/* nav lives with the rest of the nav, above */

/* the test-mode banner: a blood edge on an iron plate, which is the system's way of saying
   "this is not normal" without spending the one blood fill a screen gets */
.testbar { display: flex; flex-direction: column; gap: 3px; margin: 0 0 16px; padding: 11px 14px;
  background: #15181D; box-shadow: inset 0 0 0 1px #6E1019, 0 0 0 1px #0B0D10;
  border-left: 3px solid var(--bl-body); }
.testbar b { color: var(--ink-accent); font-size: .84rem; letter-spacing: .1em; text-transform: uppercase; }
.testbar span { color: var(--muted); font-size: .84rem; line-height: 1.45; }
/* the grant button inside the banner. It sits on the banner's own plate rather than carrying a
   plate of its own, so it never reads as one of the shop's buy buttons. */
.testbar-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-top: 8px; }
.testbar-row .btn { flex: 0 0 auto; }
.testbar-row span { flex: 1 1 220px; font-size: .78rem; }
@media (max-width: 760px) {
  .testbar-row { display: grid; grid-template-columns: 1fr; gap: 6px; }
  .testbar-row .btn { width: 100%; }
}

/* The result screen's card slot. One card at a time: the foe you just beat, then each Titan that
   leveled, sliding through in its place. Fixed height so the modal does not jump as they swap. */
.result-stage { position: relative; display: flex; align-items: center; justify-content: center;
  min-height: 210px; width: 100%; overflow: hidden; }
.stage-card { position: relative; display: flex; flex-direction: column; align-items: center; }
.stage-card.stage-in  { animation: stageIn 380ms cubic-bezier(.16, 1, .3, 1) both; }
.stage-card.stage-out { animation: stageOut 360ms cubic-bezier(.5, 0, .75, 0) both; }
@keyframes stageIn  { 0% { opacity: 0; transform: translateX(46%) scale(.92); } 100% { opacity: 1; transform: none; } }
@keyframes stageOut { 0% { opacity: 1; transform: none; } 100% { opacity: 0; transform: translateX(-46%) scale(.92); } }

/* A level landing on a card. Bigger than the row version was, because this is now the thing the
   screen is showing rather than a note beside a bar. */
.stage-card.growing .tcard { animation: cardSwell 900ms cubic-bezier(.16, 1, .3, 1) both; }
@keyframes cardSwell {
  0%   { transform: none; }
  32%  { transform: translateY(-10px) scale(1.075); }
  100% { transform: none; }
}
.stage-card.growing::before { content: ''; position: absolute; inset: -14px -20px; z-index: 0; pointer-events: none;
  background: radial-gradient(ellipse at 50% 55%, rgba(255, 214, 100, .5), transparent 70%);
  animation: growGlow 900ms ease-out both; }
/* Rays off the card, the flourish proper. Two crossed bars that sweep out and fade. */
.stage-card.growing::after { content: ''; position: absolute; left: 50%; top: 50%; z-index: 0; pointer-events: none;
  width: 190px; height: 190px; margin: -95px 0 0 -95px; border-radius: 50%;
  /* A shine, not blood (Dom, 2026-09-23: "make the long red lines White and yellow? like a
     shine"): white and gold rays in turn, each fading from a hot core to nothing. */
  background: conic-gradient(from 0deg, transparent 0 6%, rgba(255, 255, 255, .85) 7%, transparent 8.5% 18%,
    rgba(255, 211, 90, .8) 19%, transparent 20.5% 31%,
    rgba(255, 255, 255, .85) 32%, transparent 33.5% 43%,
    rgba(255, 211, 90, .8) 44%, transparent 45.5% 56%,
    rgba(255, 255, 255, .85) 57%, transparent 58.5% 68%,
    rgba(255, 211, 90, .8) 69%, transparent 70.5% 81%,
    rgba(255, 255, 255, .85) 82%, transparent 83.5% 93%,
    rgba(255, 211, 90, .8) 94%, transparent 95.5%);
  mask-image: radial-gradient(closest-side, #000 20%, transparent 100%);
  -webkit-mask-image: radial-gradient(closest-side, #000 20%, transparent 100%);
  animation: growRays 950ms cubic-bezier(.2, .8, .3, 1) both; }
@keyframes growRays {
  0%   { opacity: 0; transform: scale(.3) rotate(0deg); }
  35%  { opacity: .85; }
  100% { opacity: 0; transform: scale(1.5) rotate(42deg); }
}
.stage-card .grow-stack { bottom: auto; top: -6px; }

/* The level-up particles (Dom, 2026-09-22). A zero-size layer pinned to the page at the card's
   center, above the result modal; every particle is positioned from that point by its own
   custom properties, set in levelBurst(). Gold and white, the one color here that means "more". */
.lvl-burst { position: fixed; width: 0; height: 0; z-index: 1000; pointer-events: none;
  /* One base hue, gold unless sparkBurst() is given one (a conjured form's element); the light
     and glow are mixed from it so a colored burst keeps its white-hot core. */
  --spk: #FFC940;
  --spk-light: color-mix(in srgb, var(--spk) 45%, #FFFFFF);
  --spk-glow: color-mix(in srgb, var(--spk) 80%, transparent); }
.lvl-burst i { position: absolute; left: 0; top: 0; display: block; opacity: 0; will-change: transform, opacity; }
/* ring: a bright band that opens off the card and thins as it goes */
.lvl-ring { width: 40px; height: 40px; margin: -20px 0 0 -20px; border-radius: 50%;
  border: 3px solid var(--spk-light); box-shadow: 0 0 22px 6px var(--spk-glow), inset 0 0 18px 4px var(--spk-glow);
  animation: lvlRing 700ms cubic-bezier(.16, 1, .3, 1) var(--d) both; }
@keyframes lvlRing {
  0%   { opacity: 0; transform: scale(.3); border-width: 6px; }
  15%  { opacity: 1; }
  100% { opacity: 0; transform: scale(7); border-width: 1px; }
}
/* flash: a white-hot wash the size of the card, gone in a blink */
.lvl-flash { width: var(--w); height: var(--h); margin: calc(var(--h) / -2) 0 0 calc(var(--w) / -2); border-radius: 6px;
  background: radial-gradient(ellipse at center, #FFFFFF 0 30%, var(--spk-light) 55%, transparent 80%);
  mix-blend-mode: screen; animation: lvlFlash 520ms ease-out var(--d) both; }
@keyframes lvlFlash {
  0%   { opacity: 0; transform: scale(.85); }
  18%  { opacity: .95; transform: scale(1.04); }
  100% { opacity: 0; transform: scale(1.18); }
}
/* sparks: bright streaks thrown out along their own angle, holding their light most of the way */
.lvl-spark { width: 4px; height: var(--len); margin: calc(var(--len) / -2) 0 0 -2px; border-radius: 2px;
  background: linear-gradient(#FFFFFF, var(--spk-light) 35%, var(--spk) 75%, transparent);
  box-shadow: 0 0 10px 2px var(--spk-glow);
  animation: lvlSpark var(--t) cubic-bezier(.12, .7, .3, 1) var(--d) both; }
@keyframes lvlSpark {
  0%   { opacity: 0; transform: translate(0, 0) rotate(var(--rot)) scaleY(.4); }
  8%   { opacity: 1; }
  60%  { opacity: 1; }
  100% { opacity: 0; transform: translate(var(--dx), var(--dy)) rotate(var(--rot)) scaleY(.7); }
}
/* embers: round motes that climb the card and sway as they go */
.lvl-ember { width: var(--s); height: var(--s); margin: calc(var(--s) / -2) 0 0 calc(var(--s) / -2); border-radius: 50%;
  background: radial-gradient(circle, #FFFFFF 0 25%, var(--spk-light) 55%, transparent 100%);
  box-shadow: 0 0 12px 3px var(--spk-glow);
  animation: lvlEmber var(--t) ease-out var(--d) both; }
@keyframes lvlEmber {
  0%   { opacity: 0; transform: translate(var(--x), var(--y0)) scale(.6); }
  12%  { opacity: 1; }
  65%  { opacity: 1; transform: translate(calc(var(--x) + var(--sway)), calc(var(--y0) + var(--rise) * .65)) scale(1); }
  100% { opacity: 0; transform: translate(calc(var(--x) - var(--sway) * .5), calc(var(--y0) + var(--rise))) scale(.5); }
}
/* glints: four-pointed stars that flare and hang */
.lvl-star { width: var(--s); height: var(--s); margin: calc(var(--s) / -2) 0 0 calc(var(--s) / -2);
  background: radial-gradient(circle, #FFFFFF 0 16%, var(--spk-light) 30%, transparent 70%);
  clip-path: polygon(50% 0, 57% 43%, 100% 50%, 57% 57%, 50% 100%, 43% 57%, 0 50%, 43% 43%);
  animation: lvlStar var(--t) ease-in-out var(--d) both; }
@keyframes lvlStar {
  0%   { opacity: 0; transform: translate(var(--x), var(--y)) scale(.2) rotate(0deg); }
  40%  { opacity: 1; transform: translate(var(--x), var(--y)) scale(1.3) rotate(25deg); }
  100% { opacity: 0; transform: translate(var(--x), calc(var(--y) - 14px)) scale(.4) rotate(60deg); }
}
@media (prefers-reduced-motion: reduce) { .lvl-burst { display: none; } }

/* Battle speed in the shop: rungs, not storefront cards, because these are account switches
   rather than things with art. */
.speed-block { margin-top: 18px; }
.speed-block h3 { margin: 0 0 2px; color: var(--st-light); font-size: 1.05rem; letter-spacing: .04em; }
.speed-block > p { margin: 0 0 10px; }
.speed-rung.owned b { color: var(--st-bright); }
.speed-rung.owned { opacity: .78; }
@media (max-width: 760px) {
  .speed-rung { flex-wrap: wrap; }
  .speed-rung > .btn { width: 100%; }
}

/* A level landing on the result screen. The row swells and lifts, light comes off its edges, and
   what changed is said one line at a time above it. Everything is transform and opacity so it
   stays cheap on a phone mid-teardown of a battle. */
/* growSwell moved to .stage-card when the flourish moved onto the card; kept only because
   the keyframes are shared with it. */
.stage-card.growing { }
@keyframes growSwell {
  0%   { transform: none; }
  35%  { transform: translateY(-7px) scale(1.045); }
  100% { transform: none; }
}
.xp-row.growing::before { content: ''; position: absolute; inset: -6px -10px; pointer-events: none; z-index: 0;
  background: radial-gradient(ellipse at 50% 60%, color-mix(in srgb, var(--bl-warm) 42%, transparent), transparent 68%);
  animation: growGlow 900ms ease-out both; }
@keyframes growGlow { 0% { opacity: 0; } 30% { opacity: 1; } 100% { opacity: 0; } }
/* The statements. Each rises, holds, and clears before the next, so they are read rather than
   stacked into a wall. */
.grow-stack { position: absolute; left: 0; right: 0; bottom: 100%; z-index: 4;
  display: flex; flex-direction: column-reverse; align-items: center; gap: 2px; pointer-events: none; }
.grow-line { font-size: .78rem; letter-spacing: .08em; color: var(--st-bright);
  text-shadow: 0 1px 0 #000, 0 0 14px rgba(0, 0, 0, .9);
  animation: growLine 320ms cubic-bezier(.16, 1, .3, 1) both; }
.grow-line.grow-shout { font-family: var(--display); font-size: 1.1rem; font-weight: 700;
  text-transform: uppercase;
  /* Yellow with a 3px white border (Dom, 2026-09-23). The stroke is painted UNDER the fill
     (paint-order), so 3px of it shows outside the letters instead of eating into them. */
  color: #FFD23F; -webkit-text-stroke: 3px #FFFFFF; paint-order: stroke fill;
  text-shadow: 0 2px 8px rgba(0, 0, 0, .6), 0 0 18px rgba(255, 210, 63, .55); }
.grow-line.out { animation: growLineOut 480ms ease-in both; }
@keyframes growLine { 0% { opacity: 0; transform: translateY(10px) scale(.9); } 100% { opacity: 1; transform: none; } }
@keyframes growLineOut { 0% { opacity: 1; } 100% { opacity: 0; transform: translateY(-12px); } }
.xp-row { position: relative; }
@media (prefers-reduced-motion: reduce) {
  /* The stage cards, the growth flourish and the swell rings. The old rule here named
     .xp-row.growing, which stopped matching when the flourish moved onto the card. */
  .grow-line, .stage-card.stage-in, .stage-card.stage-out,
  .stage-card.growing .tcard, .stage-card.growing::before, .stage-card.growing::after,
  .conj-swell.go .swell-ring, .conj-swell.t2::before { animation: none; }
  .stage-card.growing::after, .stage-card.growing::before { display: none; }
  .conj-swell.go .swell-ring { opacity: .35; }
}

/* The word struck over the beaten Titan on the result screen. It falls from above at three times
   its size, lands on one hard frame, sits still while it is read, then tears off to the side.
   The two shadows are what keep it legible over any card art; a single glow disappears on a pale
   one. It waits for .go so it lands after the modal has settled instead of arriving with it. */
.slam { position: absolute; left: 50%; top: 50%; z-index: 6; pointer-events: none; white-space: nowrap;
  font-family: var(--display); font-size: clamp(1.15rem, 5.2vw, 1.85rem); font-weight: 700;
  letter-spacing: .05em; color: var(--st-spec); opacity: 0;
  transform: translate(-50%, -180%) scale(3.1) rotate(-6deg);
  text-shadow: 0 2px 0 #000, 0 0 22px rgba(0, 0, 0, .95), 0 0 34px color-mix(in srgb, var(--bl-body) 70%, transparent); }
.slam.go { animation: slamDown 2.1s cubic-bezier(.2, .9, .25, 1) both; }
/* A struck line under the word, thrown outward on the frame it lands. */
.slam::after { content: ''; position: absolute; left: 50%; top: 50%; width: 130%; height: 3px;
  transform: translate(-50%, -50%) scaleX(0); background: linear-gradient(90deg, transparent, var(--bl-warm), transparent); }
.slam.go::after { animation: slamBar 2.1s cubic-bezier(.2, .9, .25, 1) both; }
@keyframes slamDown {
  0%   { opacity: 0; transform: translate(-50%, -180%) scale(3.1) rotate(-6deg); }
  13%  { opacity: 1; transform: translate(-50%, -50%) scale(.86) rotate(1.5deg); }   /* the hit */
  19%  { transform: translate(-50%, -50%) scale(1.06) rotate(0deg); }                /* the bounce */
  25%  { transform: translate(-50%, -50%) scale(1); }
  76%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }                    /* held, to be read */
  100% { opacity: 0; transform: translate(-50%, -50%) scale(1.45) rotate(-3deg) translateX(26%); }
}
@keyframes slamBar {
  0%, 12% { opacity: 0; transform: translate(-50%, -50%) scaleX(0); }
  17%     { opacity: .95; transform: translate(-50%, -50%) scaleX(1); }
  64%     { opacity: .45; transform: translate(-50%, -50%) scaleX(1); }
  100%    { opacity: 0; transform: translate(-50%, -50%) scaleX(1.3); }
}
.result-foe .foe-card { position: relative; }
@media (prefers-reduced-motion: reduce) {
  .slam { opacity: 1; transform: translate(-50%, -50%); }
  .slam.go, .slam.go::after { animation: none; }
}

/* Being taken back by the void, and the next one coming out of the same tear.
   The body is pulled IN toward a point rather than dropped: it narrows, tips toward the rift and
   the color drains as it goes, which is the difference between something dying and something
   being reclaimed. */
.sprite.banish { animation: voidBanish 620ms cubic-bezier(.55, 0, .85, .3) both; transform-origin: 50% 92%; }
@keyframes voidBanish {
  0%   { opacity: 1; transform: none; filter: none; }
  40%  { opacity: 1; transform: translateY(6px) scale(1.04, .92); filter: grayscale(.5) brightness(1.25); }
  100% { opacity: 0; transform: translateY(26px) scaleX(.06) scaleY(.34); filter: grayscale(1) brightness(.4); }
}
.sprite.arrive { animation: voidArrive 640ms cubic-bezier(.2, 1.25, .4, 1) both; transform-origin: 50% 92%; }
@keyframes voidArrive {
  0%   { opacity: 0; transform: translateY(22px) scaleX(.05) scaleY(.3); filter: brightness(2.4); }
  45%  { opacity: 1; transform: translateY(0) scaleX(1.08) scaleY(1.02); filter: brightness(1.5); }
  100% { opacity: 1; transform: none; filter: none; }
}
/* The tear itself: a vertical slit of light at the Titan's feet that opens, then shuts. */
.void-rift { position: absolute; left: 50%; bottom: 6%; z-index: 0; pointer-events: none;
  width: 5px; height: 62%; margin-left: -2.5px; border-radius: 50%;
  background: linear-gradient(180deg, transparent, var(--st-spec) 22%, #C9A6FF 55%, transparent);
  box-shadow: 0 0 22px 6px rgba(160, 120, 255, .55), 0 0 60px 18px rgba(90, 50, 160, .35);
  mix-blend-mode: screen; opacity: 0; }
.void-rift.rift-close { animation: riftClose 780ms cubic-bezier(.4, 0, .5, 1) both; }
.void-rift.rift-open  { animation: riftOpen 700ms cubic-bezier(.2, 1, .3, 1) both; }
@keyframes riftClose {
  0%   { opacity: 0; transform: scaleY(.2) scaleX(.4); }
  30%  { opacity: 1; transform: scaleY(1) scaleX(1); }
  70%  { opacity: 1; transform: scaleY(1) scaleX(2.2); }
  100% { opacity: 0; transform: scaleY(.05) scaleX(.3); }
}
@keyframes riftOpen {
  0%   { opacity: 0; transform: scaleY(.05) scaleX(.3); }
  25%  { opacity: 1; transform: scaleY(1) scaleX(2.6); }
  60%  { opacity: .9; transform: scaleY(1) scaleX(1); }
  100% { opacity: 0; transform: scaleY(.9) scaleX(.2); }
}
@media (prefers-reduced-motion: reduce) {
  .sprite.banish { animation: none; opacity: 0; }
  .sprite.arrive { animation: none; }
  .void-rift { display: none; }
}

/* The Breach button carries its guarantee on a second line. Without this the small runs straight
   on from the price and reads as one broken sentence. */
.buy-row .btn small { display: block; margin-top: 2px; font-size: .68rem; font-weight: 400;
  letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
/* Two lines need the room for two lines. At 375px the button kept its one-line height and sliced
   the top off BREACH and the digits off the price (round 4 review). */
.buy-row .btn:has(small) { height: auto; min-height: 52px; padding-top: 8px; padding-bottom: 8px;
  line-height: 1.25; white-space: normal;
  /* Stacked, whatever display a button inherits: a flex button laid the price BESIDE the name,
     which cut "OBSIDIAN EDGE" off at 390px (2026-09-29). */
  display: flex; flex-direction: column; align-items: center; justify-content: center; }
.buy-row .btn:disabled small { opacity: .7; }

/* A Breach deals one card at a time. One slot, fixed size so the modal does not jump: the card
   arrives, is held long enough to look at, then swipes off and the next arrives behind it. Five
   side by side was the first attempt and only two fitted, which is also the all-at-once problem
   the deal order exists to avoid. */
.breach-stage { position: relative; display: flex; align-items: center; justify-content: center;
  min-height: 250px; width: 100%; overflow: hidden; }
.breach-cell { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.breach-cell .tcard { width: 140px; box-shadow: 0 16px 30px -16px rgba(0, 0, 0, .9); }
.breach-cell.in  { animation: breachIn 380ms cubic-bezier(.16, 1, .3, 1) both; }
.breach-cell.out { animation: breachOut 260ms cubic-bezier(.5, 0, .75, 0) both; }
@keyframes breachIn  { 0% { opacity: 0; transform: translateX(42%) scale(.9); } 100% { opacity: 1; transform: none; } }
@keyframes breachOut { 0% { opacity: 1; transform: none; } 100% { opacity: 0; transform: translateX(-42%) scale(.9); } }
.breach-rar { font-family: var(--display); font-size: .74rem; letter-spacing: .1em;
  text-transform: uppercase; color: var(--c); text-shadow: 0 0 12px color-mix(in srgb, var(--c) 45%, transparent); }
.breach-tally { letter-spacing: .1em; text-transform: uppercase; margin-bottom: 4px; }
.breach-tally.breach-best { color: var(--c); font-family: var(--display); font-size: .95rem; }
.breach-skip { margin-top: 8px; align-self: center; }

/* Then the whole haul, scrollable, every card with its own price on it. A Breach is where
   duplicates arrive five at a time, so the moment you are looking at them is the moment to be
   able to do something about them. */
.breach-haul { display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
  gap: 12px; width: 100%; max-height: min(52vh, 420px); overflow-y: auto; padding: 4px 2px 8px;
  scrollbar-width: thin; }
.haul-cell { display: flex; flex-direction: column; align-items: center; gap: 5px;
  animation: breachIn 300ms cubic-bezier(.16, 1, .3, 1) both; }
.haul-cell .tcard { width: 100%; max-width: 132px; }
.haul-cell .btn.small { width: 100%; }
.haul-cell.sold { opacity: .5; }
.haul-cell.sold .tcard { filter: grayscale(1) brightness(.7); }
/* An armed sell button. Blood, because the next press cannot be taken back. */
.haul-cell .btn.arm { color: var(--ink-accent); box-shadow: inset 0 0 0 1px var(--bl-body), 0 0 16px -4px var(--bl-body);
  font-size: .64rem; letter-spacing: .02em; }
.modal-card.breach-modal { max-width: 660px; }
.reveal.breach { min-height: 280px; }
@media (max-width: 760px) {
  .breach-cell .tcard { width: 118px; }
  .breach-haul { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 8px; max-height: 46vh; }
  .modal-card.breach-modal { max-width: none; }
}
@media (prefers-reduced-motion: reduce) {
  .breach-cell.in, .breach-cell.out, .haul-cell { animation: none; }
  .breach-cell.out { display: none; }
}

/* Sell mode in the Collection. The cards stay the cards; what changes is that picking one is the
   action, and every one wears its own price so the total in the bar is checkable. */
.grid.selling .cwrap { cursor: pointer; }
.grid.selling .cwrap.locked { opacity: .45; cursor: not-allowed; }
.pick-tag { display: block; margin-top: 4px; text-align: center; font-size: .72rem;
  letter-spacing: .04em; color: var(--muted); font-variant-numeric: tabular-nums; }
.cwrap.picked .pick-tag { color: var(--lime); }
.cwrap.locked .pick-tag { color: var(--st-mid); }

/* The bar sits above the grid and stays put: the count and the total are the two things you are
   deciding on, so they must not be somewhere you have to scroll back to. */
.sell-bar { position: sticky; top: var(--hdr-h); z-index: 4; display: flex; flex-wrap: wrap; gap: 10px;
  align-items: center; justify-content: space-between; margin: 0 0 12px; padding: 10px 14px;
  background: #15181D; box-shadow: inset 0 0 0 1px var(--line2), 0 10px 22px -14px rgba(0, 0, 0, .9);
  border-left: 3px solid var(--bl-body); }
.sell-count { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.sell-count b { color: var(--st-bright); letter-spacing: .04em; }
.sell-acts { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.btn.sell-go:not(:disabled) { color: var(--ink-accent); box-shadow: inset 0 0 0 1px var(--bl-body); }
@media (max-width: 760px) {
  .sell-bar { flex-direction: column; align-items: stretch; }
  .sell-acts { display: grid; grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); }
  .sell-acts > .btn { width: 100%; }
}

/* Claim all, above the three dailies. */
.quest-all { align-self: flex-start; margin-bottom: 4px; }
@media (max-width: 760px) { .quest-all { width: 100%; } }

/* The ground changing hands. A wash of the new element's color across the whole arena, once,
   because this is the only event in a fight that changes what every later card is worth on both
   sides at the same time. */
.screen.battle.ground-turn .arena::after { animation: groundTurn 900ms ease-out; }
@keyframes groundTurn {
  0%   { box-shadow: inset 0 0 0 0 transparent; }
  35%  { box-shadow: inset 0 0 120px -10px var(--ground, var(--st-body)); }
  100% { box-shadow: inset 0 0 0 0 transparent; }
}
.screen.battle.ground-turn .field-tag { animation: fieldPop 620ms cubic-bezier(.2, 1.3, .3, 1) both; }
@keyframes fieldPop {
  0%   { transform: scale(.7); opacity: .4; }
  55%  { transform: scale(1.14); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .screen.battle.ground-turn .arena::after, .screen.battle.ground-turn .field-tag { animation: none; }
}

/* chips */
.chip { display: inline-block; padding: 2px 8px; border-radius: 0; font-size: .74rem;
  font-weight: 700; color: var(--c); background: color-mix(in srgb, var(--c) 13%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--c) 42%, transparent); }
.rarity { font-family: var(--display); letter-spacing: .1em; display: inline-block;
  padding: 2px 9px; border-radius: 0; font-size: .7rem; font-weight: 700;
  text-transform: uppercase; color: var(--st-light); background: linear-gradient(180deg, var(--panel3), var(--panel2));
  box-shadow: inset 0 1px 0 rgba(214, 224, 236, .12), inset 0 -1px 0 rgba(0, 0, 0, .55), 0 0 0 1px color-mix(in srgb, var(--c) 45%, #000),
              0 1px 2px rgba(0, 0, 0, .5); }

/* titan card */
/* ---------- the card ----------
   The card IS the face. There used to be a second construction around it: a 7px plate with its
   own background, its own drop shadow and a conic-gradient bevel on .tcard::before, left over
   from the version before the Inverted Shield. The shield rebuild already zeroed the plate's
   padding and background further down the file, but the bevel pseudo-element survived and went
   on drawing a 1px ring around the CARD box while the face inside it lifted 4px on hover: a
   frame that did not line up with the card it was framing (Dom, 2026-09-11). It is gone. The
   face draws its own edge, per rarity, in .tcard.t2-t5 .tc-face. */

/* Hover lifts the face. Only transform and opacity move, which keeps a 200-card collection on
   the compositor instead of asking the browser to repaint a box-shadow per card per frame. */
.tcard.clickable { cursor: pointer; }

/* Entrance choreography. Only transform and opacity, which are the two properties the compositor
   can handle without layout or paint, so a hundred-card collection still lands in one frame's
   work. Opacity runs linear and the movement runs eased: a fading element on an ease-out looks
   like it hesitates. The stagger is 40ms and stops after eight, because past that the last items
   are waiting on an animation nobody is watching, and the whole set should be readable at once.
   Text is opaque and stationary by the time it arrives; nothing here animates a card's contents. */
@media (prefers-reduced-motion: no-preference) {
  .grid > *, .zones > *, .boxes > *, .hero-team > * {
    animation: riseIn 260ms cubic-bezier(0, 0, .38, .9) backwards;
  }
  .grid > :nth-child(1), .zones > :nth-child(1), .boxes > :nth-child(1), .hero-team > :nth-child(1) { animation-delay: 0ms; }
  .grid > :nth-child(2), .zones > :nth-child(2), .boxes > :nth-child(2), .hero-team > :nth-child(2) { animation-delay: 40ms; }
  .grid > :nth-child(3), .zones > :nth-child(3), .boxes > :nth-child(3), .hero-team > :nth-child(3) { animation-delay: 80ms; }
  .grid > :nth-child(4), .zones > :nth-child(4), .boxes > :nth-child(4) { animation-delay: 120ms; }
  .grid > :nth-child(5), .zones > :nth-child(5), .boxes > :nth-child(5) { animation-delay: 160ms; }
  .grid > :nth-child(6), .zones > :nth-child(6), .boxes > :nth-child(6) { animation-delay: 200ms; }
  .grid > :nth-child(7), .zones > :nth-child(7), .boxes > :nth-child(7) { animation-delay: 240ms; }
  .grid > :nth-child(n+8), .zones > :nth-child(n+8), .boxes > :nth-child(n+8) { animation-delay: 280ms; }
}
@keyframes riseIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* --card-size is the player's own preference, written on :root by the slider on Collection and
   remembered per device. min() keeps a large setting from overflowing a narrow screen: the track
   falls back to the full width rather than pushing the page sideways. */
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(var(--card-size, 150px), 100%), 1fr)); gap: 12px; }
.modal .grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
.mini-hp, .listing-foot { margin-top: 2px; }
.mini-hp { height: 5px; border-radius: var(--r-pill); overflow: hidden; background: #080A0C;
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, .7); }
.mini-hp i { display: block; height: 100%; border-radius: inherit;
  background: linear-gradient(180deg, #A8E89A, #6FCB5C); }
.listing-foot { display: flex; justify-content: space-between; align-items: baseline; }
.empty { padding: 30px; text-align: center; color: var(--muted); border: 1px dashed var(--line); }

/* home */
.home-hero { position: relative; isolation: isolate; overflow: hidden;
  display: grid; grid-template-columns: minmax(260px, .9fr) minmax(0, 1.6fr); gap: 30px; align-items: center;
  padding: 40px 34px; border-radius: 0;
  background-image: radial-gradient(88% 68% at 50% -14%, rgba(200, 214, 232, .09), transparent 62%),
    radial-gradient(120% 90% at 50% 118%, rgba(4, 6, 9, .55), transparent 58%),
    linear-gradient(168deg, var(--panel3), var(--panel) 56%, var(--ink));
  background-position: 0 0, 0 0, 0 0;
  background-size: auto, auto, auto;
  background-repeat: no-repeat;
  box-shadow:
    inset 0 1px 0 rgba(214, 224, 236, .12),
    inset 0 -2px 0 rgba(176, 27, 42, .2),
    inset 0 -1px 0 rgba(0, 0, 0, .7),
    0 0 0 1px rgba(0, 0, 0, .6),
    0 26px 52px -26px rgba(0, 0, 0, .75); }
/* Elliptical, focused ABOVE center to match a light hanging over the table, not a centered
   circle. This is the layer that makes the panel read as a lit space rather than a colored box. */
.home-hero::after { content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 0;
  background: radial-gradient(128% 96% at 50% 30%, transparent 0%, transparent 44%,
    rgba(0, 0, 0, .26) 74%, rgba(0, 0, 0, .58) 100%); }
.home-hero > * { position: relative; z-index: 1; min-width: 0; }
.home-hero::before { content: ''; position: absolute; inset: 0; padding: 1px; border-radius: inherit;
  pointer-events: none; z-index: 2;
  background: conic-gradient(from 218deg,
    /* The peak used to reach --st-spec, the cold white. On a rounded panel that read as a rim;
       square, the same arc lands along a flat edge and draws a hard white rectangle. The
       handoff's own panels are a single steel rule, so the specular tops out at --st-light. */
    var(--st-shadow) 0deg, var(--st-body) 38deg, var(--st-light) 52deg, var(--st-dark) 96deg,
    var(--st-shadow) 156deg, var(--st-void) 194deg, var(--st-dark) 238deg, var(--st-body) 292deg,
    var(--st-shadow) 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude; }
.hero-copy h1 { font-family: var(--display); font-weight: 700; font-size: 3.4rem;
  letter-spacing: .1em; line-height: 1.05;
  background-image: linear-gradient(176deg, var(--au-mid) 0%, var(--au-warm) 20%,
    var(--au-light) 38%, var(--au-spec) 47%, var(--au-light) 55%, var(--au-body) 68%,
    var(--au-olive) 86%, var(--au-shadow) 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  -webkit-text-fill-color: transparent;
  filter: drop-shadow(0 2px 0 rgba(0, 0, 0, .85)) drop-shadow(0 3px 2px rgba(0, 0, 0, .5))
          drop-shadow(0 0 34px rgba(138, 148, 166, .3)); }
.hero-copy p { color: var(--text-body); font-size: 1.02rem; max-width: min(44ch, 100%); }
.hero-actions { display: flex; gap: 10px; margin-top: 16px; flex-wrap: wrap; }
.hero-team { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
.home-stats { display: flex; gap: 10px; margin-top: 16px; flex-wrap: wrap; }
.hstat { border-radius: 0; padding: 10px 16px; display: flex; flex-direction: column;
  min-width: 120px; background: linear-gradient(180deg, var(--panel2), var(--panel));
  box-shadow: inset 0 1px 0 rgba(214, 224, 236, .1), inset 0 -1px 0 rgba(0, 0, 0, .6),
              0 0 0 1px rgba(0, 0, 0, .5), 0 1px 2px rgba(0, 0, 0, .45); }
.hstat b { font-size: 1.4rem; color: var(--amber); }
.hstat span { color: var(--muted); font-size: .8rem; }

/* hunt */
.zones { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 12px; }@media (min-width: 1100px) { .zones { grid-template-columns: repeat(3, 1fr); }}
   /* nine zones, three by three, no orphan */
.zone { --e: var(--st-body); position: relative; text-align: left; border: 0;
  border-radius: 0; padding: 17px; display: flex; flex-direction: column; gap: 6px;
  min-height: 132px; color: var(--text-body);
  background-image: radial-gradient(120% 70% at 50% -10%, color-mix(in srgb, var(--e) 20%, transparent), transparent 62%),
    linear-gradient(180deg, var(--panel2), var(--panel) 60%, var(--sunken));
  background-position: 0 0, 0 0;
  background-size: auto, auto;
  background-repeat: no-repeat;
  box-shadow:
    inset 0 2px 0 color-mix(in srgb, var(--e) 75%, transparent),
    inset 0 3px 0 rgba(0, 0, 0, .5),
    inset 0 -1px 0 rgba(0, 0, 0, .7),
    inset 0 -44px 48px -44px rgba(0, 0, 0, .5),
    0 0 0 1px rgba(0, 0, 0, .55), 0 1px 2px rgba(0, 0, 0, .5),
    0 18px 36px -22px rgba(0, 0, 0, .65);
  transition: transform var(--dur-fast) var(--ease-standard),
              box-shadow var(--dur-fast) var(--ease-standard); }
.zone:not(.locked):hover {
  box-shadow:
    inset 0 2px 0 color-mix(in srgb, var(--e) 95%, transparent),
    inset 0 3px 0 rgba(0, 0, 0, .5), inset 0 -1px 0 rgba(0, 0, 0, .7),
    inset 0 -44px 48px -44px rgba(0, 0, 0, .5),
    0 0 0 1px color-mix(in srgb, var(--e) 40%, transparent),
    0 2px 4px rgba(0, 0, 0, .5), 0 22px 44px -20px rgba(0, 0, 0, .7); }
.zone.locked { opacity: .6; cursor: not-allowed; filter: saturate(.25) brightness(.8); }
.zone-lock { display: flex; align-items: center; gap: 6px; }
.zone-lock svg { flex: 0 0 auto; color: var(--st-light); }
.zone-name { font-family: var(--display); font-size: 1.16rem; font-weight: 700;
  letter-spacing: .05em; color: var(--text); }
.zone-sub { color: var(--muted); font-size: .85rem; }
.zone-lock { margin-top: auto; color: var(--muted); font-size: .8rem; flex-wrap: wrap; }
/* The count is the part a player checks on the way past, so it is the part that is lit. */
.zone-lock-n { color: var(--st-light); font-weight: 600;
  font-variant-numeric: lining-nums tabular-nums; }

/* ranked */
.ladder { display: flex; gap: 8px; margin: 12px 0 20px; flex-wrap: wrap; }
.rung { flex: 1; min-width: 110px; padding: 12px; border: 1px solid var(--line); background: var(--panel); display: flex; flex-direction: column; }
.rung.here { border-color: var(--amber); background: #FFC53D18; }
.rung span { color: var(--muted); font-size: .8rem; }
.ranked-actions { display: flex; gap: 10px; flex-wrap: wrap; }

/* collection */
.team-row { display: flex; gap: 10px; align-items: center; margin-bottom: 14px; flex-wrap: wrap; }
.team-label { font-family: var(--display); font-weight: 700; color: var(--ink-accent); margin-right: 6px; letter-spacing: .06em; }
.team-slot { display: flex; align-items: center; gap: 8px; background: var(--panel); border: 1px solid var(--line); padding: 4px 10px 4px 4px; font-size: .85rem; }
.team-slot img { width: 44px; height: 44px; }
.team-slot.empty { color: var(--muted); padding: 12px 16px; border-style: dashed; }
.filters { display: flex; gap: 12px; margin-bottom: 14px; flex-wrap: wrap; }
.sel { display: flex; align-items: center; gap: 6px; font-size: .85rem; color: var(--muted); }
select {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--gold) 50%), linear-gradient(135deg, var(--gold) 50%, transparent 50%);
  background-position: right 14px center, right 9px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: 28px;
}
select, textarea, .input, input[type=text], input[type=password], input[type=search], input[type=number] {
  border: 0; border-radius: 0; padding: 9px 11px; color: var(--text);
  background: linear-gradient(180deg, #08090B, var(--sunken));
  /* the last inset is a stated edge: without it a select on a dark panel read as static text */
  box-shadow: inset 0 1px 0 rgba(0, 0, 0, .9), inset 0 2px 4px rgba(0, 0, 0, .55),
              inset 0 -1px 0 rgba(214, 224, 236, .07), inset 0 0 0 1px var(--line2);
  transition: box-shadow var(--dur-fast) var(--ease-standard); }
select:focus, textarea:focus, .input:focus, input:focus {
  outline: none;
  /* Steel, for the same reason every other focus ring is: a red one on a text field is the same
     complaint in a different place. */
  box-shadow: inset 0 1px 0 rgba(0, 0, 0, .9), inset 0 2px 4px rgba(0, 0, 0, .55),
              inset 0 0 0 1px rgba(214, 224, 236, .5), 0 0 0 3px rgba(214, 224, 236, .12); }
::placeholder { color: var(--faint); }
.input { width: 100%; }
.detail { display: grid; grid-template-columns: 420px 1fr; gap: 24px; align-items: start; }
/* A lit pedestal under the big card: cold key light from above and the card sitting a little
   proud of it. The element-colored floor glow and the halo around the card edge are gone: the
   card's own frame carries the element, and a colored wash around it read as a glow effect
   rather than as a printed card sitting on a surface (Dom, 2026-09-10). */
.pedestal { position: relative; padding: 26px 18px 30px; border-radius: 0; isolation: isolate;
  background:
    radial-gradient(90% 60% at 50% -10%, rgba(214, 224, 236, .08), transparent 60%),
    linear-gradient(180deg, var(--panel2), var(--sunken));
  box-shadow: inset 0 1px 0 rgba(214, 224, 236, .1), inset 0 -1px 0 rgba(0, 0, 0, .7), 0 0 0 1px rgba(0, 0, 0, .6); }
.pedestal .tcard { max-width: 380px; margin: 0 auto; box-shadow: 0 0 0 1px rgba(0, 0, 0, .65), 0 22px 44px -18px rgba(0, 0, 0, .9); }
.pedestal::after { content: ''; position: absolute; left: 18%; right: 18%; bottom: 18px; height: 10px; border-radius: 50%;
  background: radial-gradient(closest-side, rgba(0, 0, 0, .75), transparent); filter: blur(4px); z-index: -1; }@media (max-width: 900px) { .pedestal { padding: 18px 12px 22px; } .pedestal .tcard { max-width: 280px; }}

.detail-art img { width: 100%; height: auto; border-radius: 0;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .8), 0 0 0 1px rgba(176, 27, 42, .18),
              0 18px 36px -18px rgba(0, 0, 0, .75); }
/* ---- the three tags under the card ----
   Element, rarity and build were three different components that happened to sit in a row: the
   element and build chips in the body face at .74rem, the rarity in Cinzel at .7rem with its own
   tracking, each inline-block with its own padding, so no two shared a height and none of them was
   centered in its own box (Dom, 2026-09-12). They are one thing here. Same face, same size, same
   height, contents centered on both axes by flex rather than left to fall where a baseline puts
   them. The face is the body one, because these are labels in the control system, not display
   titles: it is the same type the buttons and the nav use. The rarity keeps Cinzel everywhere
   else it appears, which is where it is a heading rather than a tag. */
.detail-head { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin-bottom: 8px; }
.detail-head .chip, .detail-head .rarity {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  min-height: 30px; padding: 0 12px; line-height: 1;
  font-family: var(--font); font-size: .74rem; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; }
/* The rarity's ring is drawn OUTSIDE its box (`0 0 0 1px`) while the other two are drawn inside
   (`inset 0 0 0 1px`), so at the same 30px it painted 32 and read as the bigger box of the three
   (Dom, 2026-09-12). Inside here it is one of a set, so the ring comes in and the drop shadow
   goes: three tags on the same plane, all three exactly the box they measure. */
.detail-head .rarity {
  background: color-mix(in srgb, var(--c) 13%, transparent);
  color: var(--c);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--c) 42%, transparent); }
/* Under flex the diamond is an item, so the gap spaces it; its own margin would double that, and
   the vertical-align nudge it needed as an inline box now does nothing but confuse. */
.detail-head .rarity::before { margin-right: 0; vertical-align: 0; }
.detail-head .build-sym { font-size: 1.05rem; }
.detail-level { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.xp { display: flex; align-items: center; gap: 8px; flex: 1; font-size: .78rem; color: var(--muted); }
.bar, .quest-bar, .codex-bar, .season-bar, .pity-bar, .budget-bar {
  position: relative; border-radius: var(--r-pill); overflow: hidden;
  background: linear-gradient(180deg, #08090A, #101216);
  box-shadow: inset 0 1px 0 rgba(0, 0, 0, .9), inset 0 2px 4px rgba(0, 0, 0, .6),
              inset 0 -1px 0 rgba(214, 224, 236, .07); }
.bar { height: 10px; flex: 1; }
.bar.thin { height: 6px; }
/* A bar whose color is set per instance still gets the lit channel: the hue comes from --c and
   the light comes from the same place as everywhere else. */
.bar > i.tinted {
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--c) 72%, #fff),
    var(--c) 48%,
    color-mix(in srgb, var(--c) 62%, #000));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .45), inset 0 -1px 0 rgba(0, 0, 0, .35),
              0 0 10px -3px var(--c); }
.bar > i, .quest-bar > i, .codex-bar > i, .season-bar > i, .pity-bar > i, .budget-bar > i {
  display: block; height: 100%; border-radius: inherit;
  background: linear-gradient(180deg, var(--st-light), var(--st-body) 46%, var(--st-dark));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .3), inset 0 -1px 0 rgba(0, 0, 0, .5),
              0 0 10px -3px rgba(138, 148, 166, .5);
  transition: width var(--dur-panel) var(--ease-standard); }
.stats { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; }
.stat { position: relative; display: grid; grid-template-columns: 40px 1fr 60px; align-items: center; gap: 8px; font-size: .85rem; }
.stat-l { color: var(--muted); }
.stat-v { text-align: right; font-weight: 600; }
.buy-row { display: flex; gap: 8px; flex-wrap: wrap; }
/* A row of choices, wrapped to the space on a wide screen. On a phone it is one column: see the
   mobile rule further down, where these all go full width at one height. */
.pills { display: flex; gap: 6px; flex-wrap: wrap; margin: 6px 0; }
.skill { --c: var(--muted); background: var(--panel2); border: 1px solid var(--line); border-left: 3px solid var(--c); padding: 6px 10px; text-align: left; display: flex; flex-direction: column; gap: 1px; min-width: 120px; }
/* The element mark sits top-right, out of the way of the name and the key number. */
.skill-ic { position: absolute; top: 6px; right: 8px; opacity: .8; }
.skill-gem { width: 14px; height: 14px; color: var(--c); }
.skill.big { position: relative; }
/* What the skill DOES. Two lines at most: it is a reminder on a card being scanned mid-turn, not
   documentation, and a card that grows with its longest description breaks the grid. */
.skill-desc { color: var(--muted); font-size: .72rem; line-height: 1.28; margin: 1px 0 2px;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.skill:disabled .skill-desc { color: #4A525E; }
/* Equipped used to be an element-tinted FIELD: a pink Strike beside a green Quake beside a tan
   Lumen, three different colored blocks in one row (Dom, 2026-09-12). The element belongs on the
   edge, which is where the battle buttons already carry it, and the field stays iron. Equipped is
   the blood ring every selected control in this game uses, so it reads as chosen rather than as a
   fourth color. */
.skill.active { background: #15181D; border-color: #2A313B; border-left-color: var(--c);
  box-shadow: inset 0 0 0 1px #B01B2A, inset 0 0 18px rgba(176, 27, 42, .16); }
.skill.active .skill-name { color: var(--st-spec); }
.skill.active .skill-sub { color: #C98F98; }
.skill:disabled { opacity: 1; cursor: default; background: var(--panel); border-color: var(--line); }
.skill:disabled .skill-name, .skill:disabled .skill-key { opacity: .45; }
.skill:disabled .skill-sub { opacity: .85; }
.skill-name { font-weight: 600; font-size: .9rem; }
/* Room for the two corner controls. The name and the stat line are full-width blocks, so without
   this the name runs under the detail control and the stat line's badges run under the shortcut. */
.skill.big .skill-name, .skill.big .skill-desc { padding-right: 103px; }
.skill.big .skill-sub { padding-right: 26px; }
.skill-sub { font-size: .72rem; color: var(--muted); }
.learn { display: flex; flex-direction: column; gap: 4px; }

/* shop */
.boxes { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; margin-bottom: 20px; }
/* The workhorse panel. It is a plate, not a div with a border: lit on the top edge, occluded at
   the bottom, with a wide soft falloff at each end that a negative spread keeps from bleeding
   round the sides. The accent is a 2px inlay at the top, not a 4px stripe. */
.box { --c: var(--st-body); position: relative; border-radius: 0; padding: 18px;
  display: flex; flex-direction: column; gap: 8px;
  /* NO RIVETS (Dom, 2026-09-22: "These place holder rivets need to be removed from all UI"). They
     were four drawn dots in the corners; the plate and its scrim sit over the painted ramp that is
     the fallback when no texture exists. */
  background-image: var(--ui-scrim, none), var(--ui-box, none),
    linear-gradient(180deg, var(--panel2) 0%, var(--panel) 58%, var(--sunken) 100%);
  background-position: 0 0, center, 0 0;
  background-size: auto, cover, auto;
  background-repeat: no-repeat;
  box-shadow:
    inset 0 -1px 0 rgba(0, 0, 0, .7),
    inset 0 30px 36px -32px rgba(214, 224, 236, .10),
    inset 0 -44px 48px -44px rgba(0, 0, 0, .55),
    0 0 0 1px rgba(0, 0, 0, .55),
    0 1px 2px rgba(0, 0, 0, .5),
    0 20px 40px -22px rgba(0, 0, 0, .65); }
/* THE ACCENT IS A PIECE OF METAL, NOT A BAR OF COLOR (Dom, 2026-09-20: "The color strip should
   be a colored metal texture"). It was `inset 0 2px 0` of the rarity color at 70%, which is a
   flat swatch, and next to three textured plates that is the one thing on the card that looks
   printed on.
   The recipe is the skill socket's, which already solved this exact problem for the nine element
   colors, including its trap: `color` takes hue and saturation from the tint but LUMINOSITY FROM
   WHAT IS BELOW, and what is below is a photograph of dark iron, so a bright accent comes back
   muddy unless the iron's midtones are lifted first. Hence the white `soft-light` layer between
   the tint and the metal. Soft-light rather than screen because it raises the middle without
   flattening the tool marks, which are the only reason to use a photograph in this strip.
   FIVE PIXELS, against the 2px this file argued for when the accent was a flat swatch. That note
   ("a 2px inlay at the top, not a 4px stripe") was about restraint and it still holds, but a
   material needs somewhere to happen: measured at 2px the metal contributed nothing the eye
   could find and the strip was the flat bar it had always been. Five is the least that reads.
   The bottom layer is the flat color again, so a build with no generated art gets what it had. */
.box::before {
  content: ''; position: absolute; left: 0; right: 0; top: 0; height: 5px; pointer-events: none;
  background-image:
    linear-gradient(180deg, rgba(255, 255, 255, .30), rgba(255, 255, 255, .04) 55%, rgba(0, 0, 0, .40)),
    linear-gradient(var(--c), var(--c)),
    linear-gradient(rgba(255, 255, 255, .46), rgba(255, 255, 255, .46)),
    var(--ui-metal, none),
    linear-gradient(var(--c), var(--c));
  background-blend-mode: normal, color, soft-light, normal, normal;
  background-size: auto, auto, auto, cover, auto;
  background-position: center;
  background-repeat: no-repeat;
  box-shadow: 0 1px 0 rgba(0, 0, 0, .55); }

/* THE THREE TIERS ARE ONE SET AND HAVE TO MEASURE THE SAME (Dom, 2026-09-20: "We need these all
   to be uniform sized"). They did not: the descriptions are different lengths and the Mythic card
   grows a second button as soon as you hold a token, so a player with tokens saw two cards at one
   height and a third at another. `1fr` auto-rows in a grid of indefinite height resolves every
   row to the tallest one's content, and pushing the first button down with `margin-top: auto`
   spends the slack above the buttons instead of stretching the text, so the three Begins line up
   across the row as well. Scoped to `.tiers` because `.boxes` also carries the shop's Gates, which
   are a different shape of card and were not what was being looked at. */
.boxes.tiers { grid-auto-rows: 1fr; }
.boxes.tiers .box > .btn:first-of-type { margin-top: auto; }

.box h3 { margin: 0; color: var(--st-light); font-size: 1.2rem; letter-spacing: .05em; }
.odds { display: flex; gap: 6px; flex-wrap: wrap; }
.odds span { color: var(--text-body); font-size: .78rem; padding: 2px 6px; border: 1px solid var(--line2); border-left: 3px solid var(--c); }
.price-list { color: var(--muted); font-size: .88rem; display: flex; flex-direction: column;
  gap: 0; margin-top: 26px; max-width: 60ch; }
.price-list h3 { margin: 0 0 2px; }
.price-list p { margin: 0 0 10px; }
/* Every number in the game: lining and tabular, zero tracking. Costs, stats and damage sit in
   fixed gems and right-aligned columns and must not change width as they count. Cinzel has
   neither lining nor tabular figures, which is why it never carries a number anywhere here.
   (The card's own numbers are set on .tc-s and .tc-lv in the card block at the end of this file.) */
.num, .hdr-pill b, .hstat b, .fighter-sub {
  font-variant-numeric: lining-nums tabular-nums; letter-spacing: 0; }
.price-row { display: flex; justify-content: space-between; align-items: baseline; gap: 16px;
  padding: 7px 2px; border-bottom: 1px solid rgba(255, 255, 255, .05); }
.price-row:last-child { border-bottom: 0; }
.price-row b { color: var(--text-body); font-weight: 600; text-align: right;
  font-variant-numeric: lining-nums tabular-nums; }
.reveal { --c: var(--st-body); min-height: 260px; display: flex; align-items: center; justify-content: center; }
.reveal-box { width: 120px; height: 120px; background: var(--c); animation: boxspin .9s ease-in forwards; box-shadow: 0 0 60px var(--c); }
@keyframes boxspin { 0% { transform: scale(.6) rotate(0); } 70% { transform: scale(1.15) rotate(340deg); } 100% { transform: scale(0) rotate(360deg); opacity: 0; } }
.reveal.shown .tcard { width: 220px; box-shadow: 0 22px 44px -18px rgba(0, 0, 0, .9); }
.reveal { flex-direction: column; gap: 10px; position: relative; }
@keyframes pop { from { transform: scale(.3); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.market-tools { display: flex; gap: 10px; margin-bottom: 14px; }

/* leaderboard */
.lb-row { display: grid; grid-template-columns: 40px 1fr 110px 90px 70px 80px; padding: 10px 12px; border-bottom: 1px solid var(--line); }
.lb-row.head { color: var(--muted); font-size: .78rem; }
.lb-row.me { background: rgba(176, 27, 42, 0.08); color: var(--ink-accent); }

/* account */
.form, .account { max-width: 440px; display: flex; flex-direction: column; gap: 12px; }
.field { display: flex; flex-direction: column; gap: 4px; font-size: .85rem; color: var(--muted); }
.form-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.acct-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--line); }
.secret { display: block; font-size: 1.2rem; letter-spacing: .12em; padding: 10px; background: var(--sunken); word-break: break-all; margin: 8px 0; }
.danger-zone { margin-top: 30px; }

/* modal + toast */
.modal { position: fixed; inset: 0; background: #0D0F12B8; display: none; align-items: center; justify-content: center; padding: 16px; z-index: 60; }
.modal.open { display: flex; }
.modal-card { background: linear-gradient(180deg, var(--panel2), var(--panel)); border: 1px solid var(--line2); box-shadow: 0 0 0 1px #00000060, 0 24px 60px #00000090; padding: 24px; width: min(560px, 100%); max-height: 94vh; overflow: hidden; display: flex; flex-direction: column; animation: rise .18s ease-out; position: relative; }
.modal-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
/* A scroll container cannot be scrolled ABOVE its origin, so anything at the top that pulls
   itself up with a negative top margin is cut off with no way to reach it. Worth guarding at the
   container, but note it only reaches DIRECT children: a negative margin on a deeper first child
   collapses up through its wrapper and lands in the same place, which is what .result-sub did. */
.modal-body > :first-child { margin-top: 0; }
.modal-title, .modal-sub, .modal-actions { flex: 0 0 auto; }
.modal-card::before { content: ''; position: absolute; inset: 4px; border: 1px solid rgba(176, 27, 42, 0.15); pointer-events: none; }
.modal-card.wide { width: min(980px, 100%); }
@keyframes rise { from { transform: translateY(12px); opacity: 0; } }
/* A card redrawn in place (a purchase on a Titan's card) is already on screen: it does not rise again. */
.modal-card.still { animation: none; }
.modal-title { font-family: var(--display); font-size: 1.4rem; letter-spacing: .06em; margin-bottom: 14px; color: var(--ink-accent); }
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 18px; flex-wrap: wrap; }
.toasts { position: fixed; top: calc(var(--hdr-h) + 12px); right: 16px; display: flex; flex-direction: column; align-items: flex-end; gap: 8px; z-index: 70; pointer-events: none; }
.toast { background: var(--panel2); border: 1px solid var(--line2); border-left: 3px solid var(--gold); padding: 10px 16px; opacity: 0; transform: translateY(-8px); transition: .25s; max-width: min(360px, 90vw); box-shadow: 0 12px 28px -12px rgba(0, 0, 0, .8); }
.toast.in { opacity: 1; transform: none; }
.toast-error { border-left-color: var(--magenta); }
.toast-good { border-left-color: var(--st-light); }

/* battle */
/* The battle screen is exactly one viewport tall and the action row is pinned to the bottom of
   it. This treatment existed only inside the mobile media query, so on a 1280x800 desktop the
   arena took its 420px minimum, the HUD was pushed after it, and Switch, Wait, Net and Run sat
   from y790 to y844 in an 800px window: off screen, on the screen the game is mostly played on.
   A primary action below the fold is indistinguishable from a broken button. */
.screen.battle { position: relative; }
.screen.battle.active { display: flex; flex-direction: column; min-height: 0;
  height: calc(var(--vh) - var(--hdr-h) - 34px); }
.screen.battle.active .arena { flex: 1 1 auto; min-height: 200px; overflow-y: auto; overflow-x: hidden; }
.screen.battle.active .battle-hud { flex: 0 0 auto; }
/* The page's top padding clears the fixed header and leaves 14px over the turn row, which on a
   phone is 14px spent on nothing above a 24px row. Pulled up to 6, and the row sits 2px off the
   arena below it (Dom, 2026-09-15: "tighten the padding above and below them"). Scoped to the
   battle screen, because every other screen wants that breathing room. */
.screen.battle.active .battle-top { flex: 0 0 auto; margin-top: -8px; }
/* `align-items: center` rather than the default stretch. The row's height is set by the longest
   text in it ("Turn 4 · next: Fulgurax" wraps to two lines on a phone), and the field tag and the
   speed button were stretching to match it, which is why a row of three short labels was 44px of
   mostly empty boxes ("the field section at the top can also be smaller", Dom, 2026-09-15). */
.battle-top { display: flex; justify-content: space-between; align-items: center; color: var(--muted); font-size: .85rem; margin-bottom: 6px; }
.round { color: var(--amber); font-weight: 700; }
/* align-content on a grid defaults to stretch, and the battle screen is a flex column that hands
   the arena whatever height is left. On a tall window that spare height went INTO the three auto
   rows: at 1600x1350 they measured 374 / 199 / 374, which pushed the two fighters to the top and
   bottom edges and inflated the turn strip from its natural 49px into a 199px empty box with the
   chips floating in the middle of it. The fight belongs in the middle of the ground it is fought
   on, so the rows keep their own height and the whole composition centers. */
.arena { position: relative; display: grid; align-content: center; grid-template-columns: 1fr 1fr; grid-template-rows: auto minmax(36px, auto) auto; gap: 8px; padding: 14px; min-height: 200px; background: radial-gradient(ellipse at 50% 60%, #303843, #14171C 70%); border: 1px solid var(--line); overflow: hidden; }
/* The horizon. It is the brightest line in the frame and the thing that makes the rest of it read
   as distance rather than as a two-tone box. */
.arena::before { content: ''; position: absolute; left: 0; right: 0; top: 30%; height: 1px; z-index: 0;
  pointer-events: none;
  background: linear-gradient(90deg, transparent 0%,
    color-mix(in srgb, var(--board-line, #8A94A6) 55%, transparent) 18%,
    color-mix(in srgb, var(--board-line, #8A94A6) 80%, #F6F8FA) 50%,
    color-mix(in srgb, var(--board-line, #8A94A6) 55%, transparent) 82%, transparent 100%);
  box-shadow: 0 0 30px 7px color-mix(in srgb, var(--board-line, #8A94A6) 22%, transparent); }
/* The .fighter box is dissolved with `display: contents` further down, at every width, so its own
   grid never runs; what is left here is the element color the whole side inherits. The three
   columns it used to declare (info, a 260px sprite, a 90px bench, stacked one side above the
   other) went with the old desktop layout, and the `order` rules with them: with the box gone its
   children are grid items of the arena itself, where `order` would have changed which of them
   paints over the other. */
.fighter { --e: var(--st-body); display: grid; gap: 10px; align-items: center; }
.fighter-info { background: #14171CC0; border: 1px solid var(--line); border-left: 4px solid var(--e); padding: 8px 12px; display: flex; flex-direction: column; gap: 4px; }
.fighter-name { font-weight: 700; font-size: 1.1rem; }
.fighter-lvl { color: var(--muted); font-weight: 400; font-size: .85rem; }
.hpbar { height: 12px; border-radius: var(--r-pill); overflow: hidden;
  background: linear-gradient(180deg, #060708, #0D1013);
  box-shadow: inset 0 1px 0 rgba(0, 0, 0, .9), inset 0 2px 4px rgba(0, 0, 0, .6),
              inset 0 -1px 0 rgba(214, 224, 236, .07); }
.hpfill { display: block; height: 100%; border-radius: inherit;
  background: linear-gradient(180deg, var(--st-spec), var(--st-light) 48%, var(--st-body));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .35), inset 0 -1px 0 rgba(0, 0, 0, .4),
              0 0 12px -3px rgba(217, 223, 230, .45);
  transition: width var(--dur-panel) var(--ease-standard); }
.hpfill[data-low="1"] { background: linear-gradient(180deg, #FFA88E, #FF6A48 48%, #B23A22);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .4), inset 0 -1px 0 rgba(0, 0, 0, .35),
              0 0 14px -3px rgba(255, 106, 72, .8); }
.fighter-readout { display: contents; }
.fighter-sub { display: flex; gap: 10px; font-size: .8rem; color: var(--muted); align-items: center; }
.status-tag { color: var(--c); border: 1px solid var(--c); padding: 0 6px; font-size: .72rem; }
.stage-tag { color: var(--amber); font-size: .72rem; }
.energy { display: flex; gap: 3px; }
.energy i { width: 22px; height: 10px; border-radius: 2px;
  background: linear-gradient(180deg, #090A0C, #20252C);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, .8), inset 0 -1px 0 rgba(214, 224, 236, .06); }
.energy i.on { background: linear-gradient(180deg, #FFDE9A, var(--amber) 48%, #A87418);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .55), 0 0 8px -1px var(--amber); }
.sprite-wrap { position: relative; display: flex; justify-content: center; }
/* `--sprite-cap` is the room the band actually has, measured after layout by fitSprites() in
   battle.js. It applies at every width, because a desktop window can be short too. */
/* `--lvl` is how big this Titan is for its level, set by levelScale() in battle.js on every
   fighter change and on every draw. It scales the size the sprite has ALREADY been cut down to,
   which is the way round that works: multiplying what it wants and then clamping to
   `--sprite-cap` looked right on a wide desktop and did nothing at all anywhere else, because the
   cap was the binding constraint from about level 20 up and every Titan above that came out the
   same height. levelScale() tops out at exactly 1, so scaling the capped value can never push the
   sprite out of the band, and the difference is visible at every window size. */
.sprite { width: auto; aspect-ratio: 1; height: calc(min(240px, var(--sprite-cap, 240px)) * var(--lvl, 1));
  position: relative; transition: transform .2s, opacity .3s, filter .3s; }
/* The generated field for a zone, under everything else in the arena.
   Two layers, and the scrim is not optional: a painted landscape is busy, and two cut-out creatures
   standing on one need it held back or they stop reading. Darker at the top so the fighter panels
   sit on something quiet, and lifted a little through the middle where the creatures stand. */
.arena.fielded::before {
  content: ''; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  /* `height: auto` is load-bearing and was the whole bug. `.arena::before` is the HORIZON LINE and
     declares `height: 1px`. This rule is more specific and wins every property it names, but it
     never named height, so the 1px came through: with top, bottom AND height all set, CSS honours
     height and ignores bottom. The field was painting correctly into a one-pixel strip, which is
     why every check said it was applied and nothing was on screen (Dom, 2026-09-17: "no
     battlegrounds"). Width likewise, against `left`/`right`. */
  height: auto; width: auto;
  /* Lighter than the first pass, which buried the field entirely: the prompt already asks for a
     muted place held back in value, so the scrim does not have to do that job twice. Heaviest at
     the top, where the fighter panels sit, and thinnest through the band the creatures stand in. */
  background-image: linear-gradient(180deg, rgba(11, 13, 16, .58) 0%, rgba(11, 13, 16, .10) 42%, rgba(11, 13, 16, .34) 100%), var(--field);
  background-size: cover, var(--field-size, cover); background-position: center, var(--field-pos, center); }
/* Everything in the arena stands above the field. Without this the fighter panels, the turn order
   and the sprites are all painted underneath it. */
.arena.fielded > * { position: relative; z-index: 1; }
/* THE FLOOR LAYER (Dom, 2026-09-29: Overgrowth's grass "should be behind all the other effects", his pick "Behind the
   Titans too"): skill effects marked `back` (ground cover, frost floors, slabs) draw on this canvas, above the painted field
   and under the fighters. battle.js fxBack() adds it as the arena's first child. The fighters are raised with z-index alone
   (grid items take it without being positioned), so nothing that is absolutely placed in the arena moves. */
.arena > canvas.fx-back, .arena.fielded > canvas.fx-back { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 0; pointer-events: none; }
.arena > :not(canvas.fx-back) { z-index: 1; }

/* Grounding. A cutout carries no shadow of its own, so every fighter hovered over the arena floor
   like a sticker laid on a backdrop. Two ellipses under the feet fix it: a tight contact shadow,
   and a wider pool of the element's own light so the creature sits IN the scene rather than on it.
   Dom picked this over both the ungrounded cutouts and showing the cards themselves in battle
   (2026-09-17: "I like the grounded").
   On `.sprite` rather than `.sprite-wrap` so both scale with `--lvl` along with the creature: a
   level 60 Titan draws bigger and its shadow has to grow with it or it looks pinned to a smaller
   animal. `--e` is the element color, inherited from `.fighter`. */
.sprite { position: relative; }
.sprite::before, .sprite::after {
  content: ''; position: absolute; left: 50%; translate: -50% 0;
  border-radius: 50%; pointer-events: none; z-index: 0; }
.sprite::before { bottom: 0; width: 70%; height: 12%;
  background: radial-gradient(ellipse at 50% 50%, rgba(0, 0, 0, .72), rgba(0, 0, 0, 0) 70%); }
.sprite::after { bottom: -4%; width: 94%; height: 24%;
  background: radial-gradient(ellipse at 50% 50%, rgba(0, 0, 0, .38), rgba(0, 0, 0, 0) 72%); }
/* NO GLOW, BLACK GROUND (Dom, 2026-09-22: "remove the glow behind the titans in the battlefield.
   However I want to keep a black grounding shadow below them"). The wide ellipse was a pool of the
   element's own light and the image carried an 18px drop-shadow in the element color, which
   together haloed every creature. The pool is a soft black one now, under the tight contact shadow,
   and the image carries no shadow at all. The action flashes (.lunge, .netted, .flash) are left as
   they are: they are moments, not a standing halo. */
/* Above both ellipses. Without this the light pool, being ::after, paints over the creature. */
.sprite img { position: relative; z-index: 1; }

/* `object-fit` was never set here, so it defaulted to `fill`: a square box stretching a cutout that
   is taller than it is wide, which squashes the creature rather than cropping it. Same root cause
   as .tc-art img.cut above, different symptom. Bottom aligned so it stands on the ground line. */
.sprite img { width: 100%; height: 100%; object-fit: contain; object-position: 50% 100%; }
/* Facing. Side 1 is mirrored so the two face each other, which assumes the art faces the viewer's
   right (see STYLE_ALWAYS in shared/artPrompt.js). A Titan whose art came out the other way carries
   `flip`, and the two cancel: flipped on side 0 is mirrored, flipped on side 1 is not. */
/* Mirrored when the art has to be turned to face the enemy from the side it is standing on.
   This used to be `.side-1 .sprite img`, mirroring the right-hand side unconditionally, with a
   `.flipped` class to cancel it. That hard-coded the assumption that every picture faces right,
   and a species had no way to say otherwise: kilnhound's left-facing art turned its back on the
   fight. battle.js computes this from the stored facing and the side now, so one class says
   everything and neither side is special. */
.sprite.mirror img { transform: scaleX(-1); }
.sprite.lunge { transform: translate(-30px, 16px) scale(1.06); }
.side-1 .sprite.lunge { transform: translate(30px, -16px) scale(1.06); }
.sprite.shake { animation: shake .4s; }
@keyframes shake { 0%,100% { transform: none; } 20% { transform: translateX(-10px) rotate(-3deg); } 40% { transform: translateX(10px) rotate(3deg); } 60% { transform: translateX(-6px); } 80% { transform: translateX(6px); } }
/* Superseded by .banish for a Titan the void takes back; kept for any state that still
   needs a plainly downed sprite. */
.sprite.faint { opacity: 0; transform: translateY(40px) scale(.7); filter: grayscale(1); }
.sprite.exit { opacity: 0; transform: scale(.5); }
/* Afterimages (afterimage() in battle.js): a copy of the Titan's own picture left where it stood,
   sliding back and fading. `translate` rather than `transform`, so a mirrored picture stays mirrored. */
.sprite img.ghost-img { position: absolute; inset: 0; z-index: -1; pointer-events: none; opacity: 0;
  animation: ghost-slide .95s cubic-bezier(.2,.7,.3,1) calc(var(--gi, 0) * 110ms) forwards; }
@keyframes ghost-slide { 0% { opacity: .6; translate: 0 0; } 100% { opacity: 0; translate: calc(var(--dir, -1) * (18% + var(--gi, 0) * 10%)) 0; } }
.sprite img.ghost-phase { filter: brightness(.55) sepia(1) hue-rotate(225deg) saturate(3.2) drop-shadow(0 0 12px #B06BFF); animation-duration: 1.2s; }
.sprite img.ghost-flash { filter: brightness(1.5) sepia(1) hue-rotate(5deg) saturate(4) drop-shadow(0 0 10px #FFD23D); animation-duration: .5s; }
.sprite img.ghost-dawn { filter: brightness(1.7) sepia(1) hue-rotate(350deg) saturate(2.2) drop-shadow(0 0 12px #FFE3A0); animation-duration: .7s; }
.sprite img.ghost-quick { filter: brightness(1.3) grayscale(1) drop-shadow(0 0 6px #E8EEF6); animation-duration: .5s; }
.sprite img.ghost-cinder { filter: brightness(1.2) sepia(1) hue-rotate(335deg) saturate(4) drop-shadow(0 0 10px #FF6A1A); animation-duration: .6s; }
@media (prefers-reduced-motion: reduce) { .sprite img.ghost-img { display: none; } }
/* Aging (ageSprite() in battle.js): the Titan's own picture drains and dims at the hit and comes
   back. The class is removed when it is done, so the animation never holds a filter afterwards. */
/* No 0% frame and no backwards fill: it starts FROM whatever filter is already on the picture (the
   white hit flash lands at the same moment), where forcing `none` through the delay cancelled it. */
.sprite.aging img:not(.ghost-img) { animation: age-out 1.3s ease-in-out var(--age-delay, 0ms) 1; }
@keyframes age-out { 35% { filter: grayscale(.85) sepia(.35) brightness(.8); } 70% { filter: grayscale(.6) sepia(.2) brightness(.9); } 100% { filter: none; } }
@media (prefers-reduced-motion: reduce) { .sprite.aging img { animation: none; } }
.sprite.enter { animation: enter .45s cubic-bezier(.2,1.3,.4,1); }
@keyframes enter { from { transform: scale(.2); opacity: 0; } }
.sprite.netted img { filter: brightness(1.6) saturate(.5) drop-shadow(0 0 30px var(--cyan)); }
.sprite.wiggle { transform: rotate(6deg) scale(.92); }
.dmg-host { position: absolute; inset: 0; pointer-events: none; }
.dmg { position: absolute; left: 50%; top: 30%; transform: translateX(-50%); font-weight: 700; font-size: 1.5rem; color: #fff; text-shadow: 0 2px 8px #000; animation: floatup .9s ease-out forwards; }
.dmg.crit { color: var(--amber); font-size: 2.1rem; }
.dmg.super { color: var(--red); font-size: 1.8rem; }
.dmg.weak { color: var(--muted); font-size: 1.2rem; }
/* the source of a heal, when a status paid for it ("+42 Drain", Dom 2026-09-30) */
.dmg small { font-size: .55em; margin-left: 3px; letter-spacing: .04em; opacity: .9; }
.dmg.heal { color: var(--st-bright); }
.dmg.status { color: var(--st-bright); font-size: 1.1rem; }
/* ---- a status burns on the body, not beside it (Dom, 2026-09-12) ----
   The overlay is masked with the portrait's own image, so the thing that lights up is the
   creature's silhouette rather than a colored rectangle over a drawing. `screen` blending means
   the effect adds light to the body instead of painting over it, which keeps the creature legible
   underneath the whole time it is burning.
   Two readings, because statuses do two different things: one that eats at you CLIMBS, one that
   holds you still BREATHES. Every hue comes from the status's own color. */
.sprite .fx-status { position: absolute; inset: 0; z-index: 2; pointer-events: none;
  mask-size: 100% 100%; -webkit-mask-size: 100% 100%;
  mask-repeat: no-repeat; -webkit-mask-repeat: no-repeat;
  mask-position: center; -webkit-mask-position: center;
  mix-blend-mode: screen; }
.sprite .fx-status[data-kind="burn"] {
  /* lighter: at 92% at the feet a Burn hid a third to half of the Titan for the whole turn (review
     round 14); a climbing tint the painting shows through */
  background-image: linear-gradient(0deg,
    color-mix(in srgb, var(--c) 55%, transparent) 0%,
    color-mix(in srgb, var(--c) 24%, transparent) 26%,
    color-mix(in srgb, var(--c) 5%, transparent) 46%, transparent 64%);
  background-size: 100% 210%; background-repeat: no-repeat;
  animation: fxClimb 1.45s linear infinite; }
@keyframes fxClimb { from { background-position: 50% 118%; } to { background-position: 50% -96%; } }
.sprite .fx-status[data-kind="hold"] {
  background-image: radial-gradient(62% 58% at 50% 44%,
    color-mix(in srgb, var(--c) 42%, transparent) 0%, color-mix(in srgb, var(--c) 10%, transparent) 52%, transparent 74%);
  animation: fxBreathe 2.3s ease-in-out infinite; }
/* A tint the painting shows through, never a coat over it: at .92 a held status (Drain, Chill) turned
   a spiky Titan into a colored crystal cage for the whole of its turns (effects review round 13). */
@keyframes fxBreathe { 0%, 100% { opacity: .18; } 50% { opacity: .45; } }
/* The moment it takes its bite, the whole silhouette flares. */
.sprite .fx-status.bite { animation: fxBite 420ms ease-out; }
@keyframes fxBite {
  0%   { background-image: none; background-color: color-mix(in srgb, var(--c) 96%, transparent); opacity: 1; }
  100% { background-image: none; background-color: transparent; opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .sprite .fx-status { animation: none !important; opacity: .55; }
  .sprite .fx-status[data-kind="burn"] { background-size: 100% 100%; background-position: 50% 0; } }

/* A status tag carries its cost: what it takes a turn and how many turns are left. The damage is
   in the status's own color so it reads as that effect's doing, the counter is quiet. */
.status-tag .st-dmg { margin-left: 5px; color: var(--c, var(--ink-accent)); font-weight: 700;
  font-variant-numeric: lining-nums tabular-nums; }
.status-tag .st-turns { margin-left: 5px; font-style: normal; color: var(--muted);
  font-variant-numeric: lining-nums tabular-nums; }
/* The tick itself floats in the status's color rather than in iron, so a burn and a heal are not
   the same white number over the same creature. */
.dmg.status { color: var(--c, var(--st-bright)); }
.dmg.miss { color: var(--muted); font-size: 1.1rem; }
@keyframes floatup { to { transform: translate(-50%, -60px); opacity: 0; } }
.bench { display: flex; flex-direction: column; gap: 6px; }
.bench-slot { width: 56px; background: var(--panel); border: 1px solid var(--line); padding: 3px; }
.bench-slot img { width: 100%; height: auto; display: block; }
/* A cutout is trimmed to the creature, so it is rarely square: glaciowl is 272x691. Left on
   `height: auto` a tall one makes its bench slot two and a half times the height of its neighbours
   and the row goes ragged. Square slot, creature contained inside it, standing on the floor of it.
   Spelled out at this specificity rather than relying on coming after `.bench-slot img`, which is
   the same specificity and would be decided by source order alone. */
.bench-slot img.cut { aspect-ratio: 1; height: auto; object-fit: contain; object-position: 50% 100%; }
.bench-slot i { display: block; height: 3px; background: var(--lime); margin-top: 2px; }
.bench-slot.out { opacity: .35; filter: grayscale(1); }
.battle-hud { display: grid; grid-template-columns: 1fr 1.3fr; gap: 12px; margin-top: 12px; }
.screen.battle.active .coach { flex: 1 1 auto; min-height: 0; overflow-y: auto;;
  box-shadow: inset 0 1px 0 rgba(214, 224, 236, .1), inset 0 -1px 0 rgba(0, 0, 0, .6), 0 0 0 1px rgba(0, 0, 0, .55); }
.battle-log { background: var(--panel); border: 1px solid var(--line); padding: 10px 14px; font-size: .9rem; height: 110px; min-height: 110px; max-height: 110px; overflow-y: auto; display: flex; flex-direction: column; justify-content: flex-start; gap: 2px; scrollbar-width: thin; }
.battle-log > :first-child { margin-top: auto; }
.log-line { color: var(--muted); }
.log-line:last-child { color: var(--text); }
.log-line.crit { color: var(--amber); } .log-line.super { color: var(--lime); } .log-line.weak { color: var(--muted); } .log-line.warn { color: var(--red); } .log-line.status { color: var(--st-light); }
.actions { display: flex; flex-direction: column; gap: 8px; }
/* Two to a row, always: a third skill sits at half width under the first two rather than spanning
   the row (Dom, 2026-09-12). The battle strip is where vertical space is scarcest and these are a
   list of choices rather than a footer of actions, so they stay the size of each other and let the
   fight keep the room. A single skill still spans, because half a row with nothing beside it is
   not a column, it is a gap. */
/* A scrolling move list (Dom, 2026-09-17). Two columns that SCROLL inside a fixed band, rather
   than a grid that grows until it pushes the action row off the bottom of the screen. That was a
   real problem and the file already records it: at 375x812 with five skills the action bar
   measured 928px in an 812px viewport, so every turn needed a scroll to reach Switch or Run.
   A capped, scrolling list means the number of skills stops deciding whether the buttons are
   reachable. `overscroll-behavior` keeps a flick inside the list from dragging the page. */
.skill-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
  /* `.actions` is already a flex column holding this and the action row. These three lines are
     what let the list SHRINK inside it rather than push the row below it down the page: without
     `min-height: 0` a flex item refuses to go under its content height and the cap does nothing. */
  flex: 0 1 auto; min-height: 0;
  max-height: min(30vh, 260px); overflow-y: auto; overscroll-behavior: contain;
  scrollbar-width: thin; padding-right: 2px; }
.actions { min-height: 0; }
.action-row { flex: 0 0 auto; }
/* A fade at the bottom edge so a cut-off card reads as "there is more below" rather than as a
   card that happens to be clipped. */
.skill-grid { mask-image: linear-gradient(#000 0, #000 calc(100% - 14px), transparent 100%);
  -webkit-mask-image: linear-gradient(#000 0, #000 calc(100% - 14px), transparent 100%); }
.skill-grid:not(:hover) { scrollbar-color: #2A313B transparent; }
.skill-grid:not(:has(> :nth-child(2))) { grid-template-columns: 1fr; }
.skill.big { padding: 10px 12px 10px 34px; position: relative; min-height: 54px; }
/* Super effective is text now, not an edge; resisted stays dimmed, which reads as "this one is
   worth less" without claiming a frame. */
.skill.big.weak { opacity: .8; }
.skill-key { position: absolute; left: 10px; top: 10px; width: 18px; height: 18px; display: grid; place-items: center; font-size: .7rem; background: var(--line); }
/* Equal columns, not content-sized boxes in a wrapping row: it is one strip of controls and they
   are all the same kind of thing, so they are all the same size. auto-fit with a floor rather than
   a fixed five, because on a wide screen this strip sits in a ~300px column beside the log and
   five hard columns squeezed "Wait for energy" onto three lines. Below 700px the phone rule takes
   over and pins it to five across. */
.action-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(88px, 1fr)); gap: 8px; }
/* The battle strip is five controls in one row, and two of them carry a cost under the label.
   The button system is a single 44px row by default, so the strip takes the one local exception
   it needs: the label and its cost stack, and the plate grows rather than clipping them. 44px
   stays the floor, so the touch target is unchanged. */
.action-row .btn { flex-direction: column; justify-content: center; gap: 2px;
  height: auto; min-height: 44px; padding: 6px 12px; line-height: 1.1; text-align: center; }
.action-row .btn small { font-size: 10px; letter-spacing: .06em; }
.result-rows { display: flex; flex-direction: column; gap: 6px; font-size: 1.05rem; }
.result-catch .tcard { width: 220px; margin: 0 auto 14px; }@media (max-width: 900px) {
  .home-hero { grid-template-columns: 1fr; }
  .detail { grid-template-columns: 1fr; }
  .detail-art img { max-width: 240px; }
  .lb-row { grid-template-columns: 30px 1fr 80px 70px; }
  .lb-row span:nth-child(5), .lb-row span:nth-child(6) { display: none; }}
@media (max-width: 760px) {
  /* The bar height plus whatever the home indicator claims, used by everything that has to sit
     above it rather than each place guessing a number. */
  :root { --nav-w: 0px; --nav-h: calc(58px + env(safe-area-inset-bottom, 0px)); }
  .main { padding: calc(var(--hdr-h) + 14px) 12px calc(var(--nav-h) + 20px) 12px; }
  .toasts { top: auto; bottom: calc(var(--nav-h) + 12px); right: 10px; left: 10px; align-items: stretch; }
  .toast { max-width: none; }
  .nav { top: auto; bottom: 0; right: 0; width: 100%; flex-direction: row; padding: 4px 4px calc(4px + env(safe-area-inset-bottom, 0px)); border-right: 0; border-top: 1px solid var(--line); overflow-x: auto; }
  .nav-btn { flex: 1; min-width: 60px; padding: 9px 2px 7px; border-left: 0; border-top: 0;
    flex-direction: column; justify-content: center; gap: 4px; font-size: 10px; letter-spacing: .08em; }
  .nav-rail { width: 0; height: 100%;
    background: linear-gradient(0deg, rgba(176, 27, 42, .30) 0%, rgba(176, 27, 42, 0) 75%); }
  .nav-rail::before { left: 0; right: 0; bottom: 0; top: auto; width: auto; height: 3px;
    background: linear-gradient(90deg, #6E1019 0%, #B01B2A 34%, #D6414F 50%, #B01B2A 66%, #6E1019 100%);
    box-shadow: inset 0 -1px 0 rgba(255, 217, 222, .5); }
  .rail-wash { background-image: linear-gradient(0deg, rgba(214, 65, 79, .78) 0%, rgba(176, 27, 42, .42) 22%, rgba(176, 27, 42, 0) 80%);
    background-position: bottom left; }
  .nav-rail::after { left: 0; right: 0; bottom: 0; top: auto; width: auto; height: 3px;
    transform-origin: center bottom; }
  @keyframes railWash {
     0%  { opacity: 0; background-size: 100%   0%; }
    18%  { opacity: 1; background-size: 100%  46%; }
    62%  { opacity: 1; background-size: 100% 116%; }
   100%  { opacity: 1; background-size: 100% 100%; }
  }
  @keyframes railFlare {
     0%  { opacity: 0;   transform: scaleY(1); }
    28%  { opacity: .95; transform: scaleY(2.4); }
   100%  { opacity: 0;   transform: scaleY(1); }
  }
  /* `#mute` carries .hdr-btn, and hiding it left a phone with no way to mute the game: it is the
     only sound control in the app. The rating pill also ran 28px past the right edge, cutting the
     number mid-digit. The header shrinks now instead of hiding its only control. */
  .hdr-pill#hdr-user { display: none; }
  .hdr { gap: 5px; padding-left: 10px; padding-right: 10px; }
  .hdr-pill { padding: 4px 7px; font-size: .74rem; gap: 4px; }
  .hdr-btn { padding: 4px 7px; font-size: 0; line-height: 0; }
  /* The label is the icon on a narrow screen: two glyphs, and it still says which state it is in. */
  .hdr-btn#mute::before { content: '\1F50A'; font-size: .95rem; line-height: 1.2; }
  .hdr-btn#mute.muted::before { content: '\1F507'; }
  #hdr-rating { max-width: 30vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .home-hero { padding: 26px 18px; gap: 20px; }
  .hero-copy h1 { font-size: 2.4rem; letter-spacing: .06em; }
  .hero-team { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
  /* At three across on a phone a card is about 100px wide, and the level chip was taking enough
     of the title bar to truncate every name to its first letter. The level is already in the
     line above the team, so the name wins the row. */
  /* ...and three across was 101px cards with 7px print. The team is a horizontal shelf of
     152px cards now; the level pill comes back. */
  /* The shelf runs the full width of the panel rather than stopping inside its padding. It was
     inset by 18px on both sides, so the third card was sliced off by the panel's own edge and
     read as a clipped layout instead of as a shelf with more on it (Dom, 2026-09-12). Bleeding it
     to the edges puts the cut at the edge of the screen, which is where a cut means "keep going".
     The inner padding puts the first card back where the text above it starts. */
  .hero-team { display: flex; gap: 10px; overflow-x: auto; scroll-snap-type: x mandatory;
    padding: 2px 18px 8px; margin: 0 -18px; scroll-padding: 0 18px; scrollbar-width: none; }
  .hero-team::-webkit-scrollbar { display: none; }
  .hero-team > * { flex: 0 0 152px; scroll-snap-align: start; }
  .arena { padding: 10px; min-height: 0; gap: 6px; }
  .fighter.side-1, .fighter.side-0 { grid-template-columns: 1fr 130px; }
  .fighter.side-0 .fighter-info { order: 1; } .fighter.side-0 .sprite-wrap { order: 2; }
  .bench { flex-direction: row; grid-column: 1 / -1; }
  .fighter.side-0 .bench { order: 3; }
  .bench-slot { width: 40px; }
  .sprite { width: 120px; height: 120px; }
  .fighter-name { font-size: .95rem; }
  .battle-hud { grid-template-columns: 1fr; }
  .battle-log { min-height: 70px; font-size: .8rem; }
  /* ONE PER ROW on a phone (Dom, 2026-09-18: "we need to make skills full width"). This overturns
     "two to a row, always" from 2026-09-12, and the reason that rule existed still holds on a wide
     screen, so it keeps two there. In portrait it was making 180px boxes: the description truncated
     mid-word, the damage line wrapped, and "RESISTED x0.67" took two lines of its own. The list
     already scrolls inside a capped band, so a taller row costs nothing but a shorter list. */
  .skill-grid { grid-template-columns: 1fr; }
  .skill.big { min-height: 48px; }
  .modal .grid { grid-template-columns: repeat(2, 1fr); }}

/* Reduced motion, done as an opt-out of MOVEMENT rather than of feedback. Killing every
   transition also kills the state change that tells a player their click registered, which makes
   the interface worse for exactly the people the setting is for. Color, opacity and shadow
   changes that do not alter perceived size, shape or position are explicitly not motion under
   WCAG 2.3.3, so they stay. Transforms and looping animations go. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-property: opacity, color, background-color, box-shadow, border-color, fill, stroke !important;
    transition-duration: 120ms !important;
    scroll-behavior: auto !important;
  }
  .tcard.clickable:hover, .tcard.clickable:focus-visible,
  .btn:active:not(:disabled) { transform: none !important; }
}

/* build (class) chips, aura ranks */
.build-row { display: flex; gap: 8px; flex-wrap: wrap; margin: 6px 0 4px; }
.build-chip { display: flex; flex-direction: column; gap: 2px; align-items: flex-start; padding: 8px 12px; background: var(--panel); border: 1px solid var(--line); color: var(--text); cursor: pointer; font: inherit; min-width: 96px; }
.build-chip:hover:not(.active) { background: var(--panel3); }
.build-chip b { color: var(--text); font-size: .9rem; }
.build-chip span { font-size: .7rem; color: var(--muted); }
.build-chip.active { background: color-mix(in srgb, var(--gold) 12%, transparent); border-color: var(--gold); }
.build-chip.active .build-sym { color: var(--st-bright); }
.aura-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 8px; margin: 6px 0; }
.aura { display: flex; flex-direction: column; gap: 6px; padding: 10px; background: var(--panel); border: 1px solid var(--line); border-left: 3px solid var(--c); }
.aura-name { font-size: .82rem; color: var(--st-light); font-weight: 600; }
.aura-pips { display: flex; gap: 3px; }
.aura-pips i { width: 10px; height: 6px; background: var(--line); }
.aura-pips i.on { background: var(--c); }

/* skill store */
.skgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 10px; }
.skcard { display: flex; flex-direction: column; gap: 6px; padding: 12px; background: var(--panel); border: 1px solid var(--line); border-top: 3px solid var(--c); }
.skcard.locked { opacity: .62; }
/* The nine signatures are the only skills that cannot be bought at any price, so they should not
   look like the ones that are merely out of reach for now (Dom, 2026-09-15). */
.skcard.is-mythic { border-top-color: var(--m); opacity: 1; background:
  linear-gradient(180deg, color-mix(in srgb, var(--m) 9%, transparent), transparent 42%), var(--panel); }
.skcard.is-mythic .skcard-why { color: var(--m); }
.chip.sk-myth { --c: var(--m); color: var(--m); border-color: color-mix(in srgb, var(--m) 55%, transparent); letter-spacing: .06em; }
.fxlab-row.is-mythic { border-left-color: var(--m); }
.fxlab-myth { font-size: .62rem; letter-spacing: .1em; text-transform: uppercase; color: var(--m);
  border: 1px solid color-mix(in srgb, var(--m) 55%, transparent); border-radius: 2px; padding: 1px 5px; }
.skcard-top { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
.skcard-top b { color: var(--c); font-size: .95rem; }
.skcard-cost { font-size: .8rem; color: var(--muted); }
.unit { white-space: nowrap; }
.unit-ic { font-size: .85em; margin-left: 2px; color: var(--st-body); }
.unit-ic.ether { color: var(--violet); }
.skcard-meta { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.skcard p { margin: 0; font-size: .78rem; line-height: 1.45; }
.skcard-block { color: var(--amber) !important; }   /* was the same blood as the Buy CTA on the same card */
.skcard-foot { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-top: auto; padding-top: 6px; }
.skcard-own { font-size: .76rem; color: var(--text); display: flex; gap: 6px; align-items: baseline; }
.skcard-own i { font-style: normal; color: var(--st-light); font-size: .7rem; }
.skcard-why { font-size: .72rem; color: var(--muted); text-align: right; }
.filters input[type=search] { min-width: 190px; }

.energy-label { font-size: .8rem; color: var(--muted); letter-spacing: .04em; margin-right: 2px; font-variant-numeric: lining-nums tabular-nums; }
.energy.short .energy-label { color: var(--amber); font-weight: 700; }
.skill-sub .need { color: var(--amber); font-weight: 700; letter-spacing: .04em; }

/* conjuring */
.screen.conjure-screen { padding: 0; }
.conjure { position: relative; height: calc(var(--vh) - var(--hdr-h) - 60px); display: flex; flex-direction: column; align-items: center; justify-content: center; overflow: hidden; padding-top: 8px; }
.conjure-void { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.conjure-inner { position: relative; z-index: 1; width: min(880px, 100%); max-height: 100%; overflow-y: auto; overscroll-behavior: contain;
  padding: 24px 16px 0; text-align: center; display: flex; flex-direction: column; gap: 14px; align-items: center; }
/* A hint that there is more below, on the rare window where there is. */
.conjure-inner::-webkit-scrollbar { width: 6px; }
.conjure-inner::-webkit-scrollbar-thumb { background: rgba(176, 27, 42, 0.27); border-radius: 3px; }
.conjure-title { font-family: var(--display); font-weight: 700; font-size: clamp(1.6rem, 4.2vw, 2.8rem); letter-spacing: .1em; text-transform: uppercase;
  margin: 0; padding: 6px 0 2px; line-height: 1.2;
  /* Was blood on gunmetal: 2.4:1, and the rule says blood is for the thing you press. Struck in
     iron like every other title in the game. */
  background-image: linear-gradient(178deg, var(--st-mid) 0%, var(--st-light) 22%, var(--st-bright) 40%, var(--st-spec) 48%,
    var(--st-bright) 56%, var(--st-body) 70%, var(--st-dark) 88%, var(--st-shadow) 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent; -webkit-text-fill-color: transparent;
  filter: drop-shadow(0 1px 0 rgba(0, 0, 0, .9)) drop-shadow(0 2px 1px rgba(0, 0, 0, .5)); }
.conjure-sub { color: var(--muted); max-width: 60ch; margin: 0 auto; }
/* The void's rings are drawn behind the step and cross its text, so every line of copy on it
   carries its own dark halo (blind playtest round 1: the rings cut across "Your words shape..."). */
.conjure-sub, .conjure-inner .conj-note, .conjure-inner .muted, .conj-picks, .conj-forms-note {
  text-shadow: 0 1px 2px rgba(0, 0, 0, .95), 0 0 12px rgba(0, 0, 0, .9), 0 0 24px rgba(0, 0, 0, .75); }
/* The titles are struck steel: a gradient clipped to the letters with transparent text, and a text
   shadow paints OVER a text-clipped background, which turned "SHAPE THE FORM" dark gray with a
   light rim (blind playtest round 2, 2026-09-23). A drop-shadow filter sits behind the letters
   instead, so they keep their steel and still stand off the rings. */
.conjure-title, .conj-h { filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .95)) drop-shadow(0 0 10px rgba(0, 0, 0, .85)); }
.conj-h { align-self: flex-start; margin: 10px 0 0; font-size: .85rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
.conj-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px; width: 100%; }
/* The style picker. Wider tiles than an element or a build, because the sample image IS the
   choice: the name and the blurb are captions on it, not the other way round. */
.conj-styles { display: grid; grid-template-columns: repeat(auto-fit, minmax(128px, 1fr)); gap: 8px; width: 100%; }
.conj-style { display: flex; flex-direction: column; gap: 4px; padding: 0 0 8px; text-align: left;
  background: color-mix(in srgb, var(--panel) 82%, transparent); border: 1px solid var(--line);
  color: var(--text); font: inherit; cursor: pointer; overflow: hidden; }
.conj-style img, .conj-style .conj-style-none { width: 100%; aspect-ratio: 1; object-fit: cover; display: block; background: #06070a; }
.conj-style .conj-style-none { display: grid; place-items: center; color: var(--muted); font-size: .7rem; }
.conj-style b { color: var(--text); font-size: .85rem; padding: 0 8px; }
.conj-style span { font-size: .7rem; color: var(--muted); line-height: 1.35; padding: 0 8px; }
.conj-style:hover { background: var(--panel3); }
.conj-style.on { border-color: var(--ink-accent); box-shadow: 0 0 0 1px var(--bl-light) inset; }
.conj-opt { display: flex; flex-direction: column; gap: 4px; align-items: flex-start; text-align: left; padding: 10px 12px; background: color-mix(in srgb, var(--panel) 82%, transparent); border: 1px solid var(--line); border-left: 3px solid var(--c); color: var(--text); font: inherit; cursor: pointer; }
.conj-opt:hover { background: var(--panel3); }
.conj-opt.on { background: color-mix(in srgb, var(--c) 18%, transparent); border-color: var(--c); }
.conj-opt b { color: var(--c); font-size: .95rem; }
/* ---- the element list is a stack that opens ----
   Nine colored names and a "Strong against" line told you who beats whom and nothing about what
   playing one is like (Dom, 2026-09-17). What each element IS and the mark it leaves are three
   more lines, and three lines on nine tiles is a page nobody reads. So the list stacks and only
   the one you have chosen is open (Dom: "it might be easier to stack the elements and have them
   expand once you click them").
   The open and close are animated with grid-template-rows 0fr to 1fr, which is the one way to
   transition to a height the content decides. It needs exactly this shape: a grid body with one
   child that can be clipped. `height: auto` does not interpolate and a max-height guess either
   clips the long elements (Chill and Soak run to three lines) or makes the short ones drift open
   at a speed that does not match. */
.conj-grid.els { display: flex; flex-direction: column; gap: 6px; }
.conj-grid.els > * { width: 100%; }
.conj-opt .conj-el-head { display: flex; align-items: center; justify-content: space-between;
  gap: 10px; width: 100%; }
/* The recommendation chip is `display: block` where it sits on a tile, which put it on a line of
   its own under the name and made every closed row 69px for two words. Beside the name. */
.conj-opt .conj-el-head b { display: flex; align-items: center; gap: 9px; }
.conj-opt .conj-el-head .conj-rec { display: inline-flex; align-items: center; }
.conj-opt .conj-el-body { display: grid; grid-template-rows: 0fr; width: 100%; opacity: 0;
  transition: grid-template-rows var(--dur-panel, 220ms) var(--ease-standard, ease),
              opacity 160ms linear; }
.conj-opt .conj-el-inner { min-height: 0; overflow: hidden; display: flex; flex-direction: column;
  gap: 4px; padding-top: 0; transition: padding-top var(--dur-panel, 220ms) var(--ease-standard, ease); }
.conj-opt.on .conj-el-body { grid-template-rows: 1fr; opacity: 1; }
.conj-opt.on .conj-el-inner { padding-top: 7px; }
.conj-opt span.conj-el-lore { color: var(--text-body); font-size: .76rem; }
/* The mark is the mechanical half, so it is in the element's own color and reads as the thing
   the row is telling you rather than as more flavor. */
.conj-opt span.conj-el-mark { color: var(--c); font-size: .71rem; opacity: .9; }
.conj-opt span.conj-el-why { color: var(--st-light); }
/* The shard exchange offered under an element that is short only of its own shards (store.exchangeShards). */
.conj-convert { justify-self: stretch; margin-top: -2px; border-color: color-mix(in srgb, var(--c) 55%, transparent); color: var(--c); background: color-mix(in srgb, var(--c) 10%, transparent); white-space: normal; text-align: center; }
@media (prefers-reduced-motion: reduce) {
  .conj-opt .conj-el-body, .conj-opt .conj-el-inner { transition: none; }
}
.conj-opt span { font-size: .72rem; color: var(--muted); line-height: 1.4; }
.conj-describe { display: flex; gap: 8px; width: 100%; flex-wrap: wrap; justify-content: center; }
.conj-input { flex: 1; min-width: 240px; text-align: center; }
.conj-forms { display: flex; flex-direction: column; gap: 8px; width: 100%; min-height: 60px;
  /* the slide leaves sideways, and it has to leave the frame rather than the page */
  position: relative; overflow: hidden; }
.conj-slide { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px;
  width: 100%; place-items: center; }
/* the one on its way out is taken out of the flow so the one coming in does not wait for it */
.conj-slide.out-left, .conj-slide.out-right { position: absolute; left: 0; right: 0; bottom: 0; }
.conj-form { background: color-mix(in srgb, var(--panel) 70%, transparent); border: 1px solid var(--line); padding: 8px; cursor: pointer; color: var(--muted); font: inherit; display: flex; flex-direction: column; align-items: center; gap: 4px; transition: transform .15s, border-color .15s; }
.conj-form img { width: 100%; height: auto; max-width: 280px; }
/* A generated form is a square painting rather than a transparent sprite, so it gets the frame the
   drawing does not need. Same footprint either way, so a set that comes back half generated and
   half drawn (a provider can refuse one image) does not step up and down the row. */
.conj-art { aspect-ratio: 1; object-fit: cover; border: 1px solid var(--line); background: #06070a; }
.conj-form:hover { transform: translateY(-3px); border-color: var(--st-body); }
.conj-form.on { border-color: var(--st-light); box-shadow: 0 0 0 2px var(--st-light) inset; color: var(--text); }
.conj-final img { max-width: 340px; }
.conj-actions { display: flex; gap: 10px; justify-content: center; align-items: center; flex-wrap: wrap; margin-top: auto; position: relative; z-index: 2;
  width: 100%; padding: 10px 0 14px; }
/* Not pinned any more (blind playtest round 1, 2026-09-23). The ink bar was what let a pinned row
   float over the step's text; with the bar gone (Dom's call, the day before) the buttons sat on top
   of body copy on Shape the Form and Name It. They end the step instead. */
/* No ground behind the buttons on any conjure step (Dom, 2026-09-22: "Remove the gradient behind
   the back button on all conjure pages"). The buttons are solid plates of their own and still pin
   to the foot of a long step; only the ink slab under them is gone. The hint gets a shadow to
   stand on instead, as it already did on the describe step. */
.conj-actions .conj-need { text-shadow: 0 1px 2px rgba(0, 0, 0, .95), 0 0 10px rgba(0, 0, 0, .8); }
/* The describe step's row is the exception: it sits under the field rather than at the foot of
   the screen (Dom, 2026-09-16: "remove the faded black box behind the buttons, move the buttons
   to below the input form"). The bottom-pinned bar and the ink gradient behind it exist to keep
   the buttons reachable while a long list scrolls past them; a row that is not pinned needs
   neither, and the gradient was reading as a dark panel floating in the middle of the void.
   Scoped to `.inline` so the Name it and reveal screens keep the bar they were built around. */
.conj-actions.inline { margin-top: 0; position: static; background: none; padding: 0; }
/* Same on the reveal: the Titan has arrived and the screen is one object against the void, so the
   ink bar under the only button on it reads as a slab floating behind it (Dom, 2026-09-16:
   "remove the black behind take it with you"). */
.conj-reveal .conj-actions { margin-top: 0; position: static; background: none; padding: 0;
  /* the phone rule below also gives the sticky bar a hairline and a shadow, which without the bar
     behind them are just a stray line floating under the button */
  border-top: 0; box-shadow: none; }
/* The hint under those buttons used to sit on the solid bar. On the open void it is a dark red on
   a starfield, so it needs its own ground: a shadow rather than a panel, which is the thing that
   was asked to go. */
.conj-actions.inline .conj-need { text-shadow: 0 1px 2px rgba(0, 0, 0, .95), 0 0 10px rgba(0, 0, 0, .8); }

/* `flex-basis` and `order` here were for living inside .conj-actions, which is a flex ROW. This
   line sits in .conjure-inner now, which is a flex COLUMN: there, flex-basis is a HEIGHT and
   `order: 2` would move the line away from the three forms it is about. Both are scoped back to
   the row they were written for. */
.conj-need { text-align: center; font-size: .78rem; color: var(--ink-accent); letter-spacing: .04em; min-height: 1.2em; }
.conj-actions .conj-need { flex-basis: 100%; order: 2; }
.conj-need[hidden] { display: block; visibility: hidden; }@media (max-width: 760px) {
  .conjure { height: calc(var(--vh) - var(--hdr-h) - var(--nav-h) - 34px); }
  .conj-actions { padding-bottom: 14px; }
  .conj-opt { padding: 8px 10px; }
  .conj-opt span { font-size: .68rem; }
  .conj-opt span.conj-el-lore { font-size: .72rem; }
  .conj-opt span.conj-el-mark { font-size: .68rem; }}

/* builds read by symbol, so color stays the language of elements */
.build-sym { font-size: 1.35rem; line-height: 1; color: var(--text); opacity: .85; }
.build-chip { align-items: center; }
.build-chip .build-sym { font-size: 1.6rem; margin-bottom: 2px; }
.conj-opt.build b .build-sym { font-size: 1.25rem; }
.build-chip-tag { --c: var(--muted); }
.conj-opt.build { border-left-color: var(--line); }
.conj-opt.build.on { background: color-mix(in srgb, var(--st-light) 16%, transparent); border-color: var(--st-light); }
.conj-opt.build b { color: var(--text); display: flex; align-items: center; gap: 6px; }
.conj-opt.build.on .build-sym { color: var(--st-bright); }

/* action gauge */
.gauge { height: 5px; border-radius: var(--r-pill); margin-top: 6px; overflow: hidden;
  background: linear-gradient(180deg, #08090A, #111418);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, .8), inset 0 -1px 0 rgba(214, 224, 236, .06); }
.gaugefill { display: block; height: 100%; width: 0; border-radius: inherit;
  background: linear-gradient(180deg, #7E77A0, #525F71);
  transition: width var(--dur-base) linear; }
.gaugefill[data-ready="1"] { background: linear-gradient(180deg, var(--st-spec), var(--st-light) 50%, var(--st-mid));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .5), 0 0 10px -1px rgba(217, 223, 230, .8); }
.fighter.acting .gauge { background: color-mix(in srgb, var(--st-light) 25%, var(--line)); }

.energy i.part { background: color-mix(in srgb, var(--amber) 35%, var(--line)); }
.dmg.energy { color: var(--amber); font-size: 1rem; }

/* attack animations */
@keyframes bighit { 0%,100% { transform: translate(0,0) scale(1); } 20% { transform: translate(-10px,4px) scale(1.06); } 45% { transform: translate(9px,-3px) scale(.96); } 70% { transform: translate(-5px,2px) scale(1.02); } }
.sprite.bighit { animation: bighit .5s ease-out; }
@keyframes quake { 0%,100% { transform: translate(0,0); } 25% { transform: translate(-4px,2px); } 50% { transform: translate(3px,-2px); } 75% { transform: translate(-2px,1px); } }
.screen.battle.quake { animation: quake .42s ease-out; }
.sprite.lunge { filter: drop-shadow(0 0 12px var(--e)); }

/* conjure section */
.mat-head { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; margin-bottom: 10px; }
.mat-essence { display: flex; align-items: baseline; gap: 6px; padding: 8px 14px; background: var(--panel); border: 1px solid var(--line); border-left: 3px solid var(--violet); }
.mat-essence b { font-size: 1.4rem; color: var(--st-bright); }
.mat-essence span { font-size: .78rem; color: var(--muted); }
.mat-head p { margin: 0; flex: 1; min-width: 220px; }
.shard-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(84px, 1fr)); gap: 6px; margin-bottom: 14px; }
.shard { display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 8px 4px; background: var(--panel); border: 1px solid var(--line); border-top: 2px solid var(--c); }
.shard b { color: var(--c); font-size: 1.05rem; }
.shard span { font-size: .64rem; color: var(--muted); letter-spacing: .04em; }
.conj-opt.short { opacity: .78; cursor: not-allowed; }
/* An element you cannot afford yet still has to be READ: at .45 over a translucent row the rings
   showed straight through its "Need N more shards" line (blind playtest round 2). The row is
   solid behind its text, and the muted color is what says "not yet". */
.conjure-inner .conj-opt.short { background-color: color-mix(in srgb, var(--panel) 96%, #000); }
.conj-shards { font-size: .66rem !important; color: var(--muted); }
.conj-reveal { display: flex; flex-direction: column; align-items: center; gap: 12px; min-height: 220px; justify-content: center;
  flex-shrink: 0; /* it shrank to 220px inside the scrolling column and its content spilled up over the title */ }
.conj-reveal .tcard { width: clamp(180px, 32vh, 240px); }   /* the card fits the viewport; the button stays above the fold */
.reveal-rarity { font-size: 1.6rem; letter-spacing: .12em; text-transform: uppercase; color: var(--st-bright); text-shadow: 0 0 22px color-mix(in srgb, var(--c) 60%, transparent), 0 1px 0 #000; }
.reveal-rarity::before { content: ''; display: inline-block; width: 11px; height: 11px; margin-right: 12px; transform: rotate(45deg); border-radius: 2px; background: linear-gradient(135deg, #ffffffd0, var(--c) 55%, color-mix(in srgb, var(--c) 45%, #000)); box-shadow: 0 0 10px -1px var(--c); vertical-align: 2px; }

.reveal-spread { display: flex; gap: 10px; }
.reveal-spread .rs { display: flex; flex-direction: column; align-items: center; padding: 6px 12px; background: var(--panel); border: 1px solid var(--line); }
.reveal-spread .rs b { font-size: 1.1rem; }
.reveal-spread .rs span { font-size: .62rem; color: var(--muted); letter-spacing: .08em; }

.reveal-sig { display: flex; flex-direction: column; align-items: center; gap: 3px; padding: 10px 18px; background: var(--panel); border: 1px solid var(--line); max-width: 420px; text-align: center; }
.reveal-sig b { font-size: 1.05rem; }
.rs-label { font-size: .64rem; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
.reveal-sig.mythic { border-color: #FF3DA6; box-shadow: 0 0 18px rgba(255,61,166,.35); }
.reveal-sig.mythic b { color: #FF3DA6; }
.reveal-sig.mythic .rs-label { color: #FF3DA6; }

/* action gauge, made legible */
.gauge-row { display: flex; align-items: center; gap: 6px; margin-top: 6px; }
.gauge-row .gauge { flex: 1; margin-top: 0; }
.gauge-label { font-size: .6rem; letter-spacing: .08em; color: var(--muted); white-space: nowrap; }
.gauge-label b { color: var(--text); font-size: .68rem; }
.next-tag { font-size: .64rem; letter-spacing: .1em; padding: 1px 5px; background: var(--st-bright); color: var(--ink); font-weight: 700; }
.next-tag[hidden] { display: none; }

/* turn order preview */
.order { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: var(--panel); border: 1px solid var(--line); margin-bottom: 8px;
  overflow-x: auto; scrollbar-width: none; }
.order::-webkit-scrollbar { height: 0; }
.order-label { font-size: .64rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); white-space: nowrap; }
.order-list { display: flex; gap: 5px; align-items: center; }
.order-chip { font-size: .68rem; padding: 3px 8px; white-space: nowrap; border: 1px solid var(--line); background: var(--panel3); color: var(--muted); }
.order-chip.side-0 { border-left: 3px solid var(--st-light); color: var(--text); }
.order-chip.side-1 { border-left: 3px solid var(--red); }
.order-chip.now { background: color-mix(in srgb, var(--st-light) 18%, transparent); color: var(--text); font-weight: 600; }@media (max-width: 760px) { .order-chip { font-size: .6rem; padding: 2px 6px; }}

/* element and build, on the fighter in battle */
/* ---- the tags beside a fighter's name ----
   Element, build, matchup and rarity were four separate things: three different paddings, two
   different font sizes, two different icon sizes and two different ways of drawing the outline,
   one with a real border and one with an inset ring. On a phone the words are hidden and the
   element and build tags become icon-only boxes, which is where it showed worst: two squares
   next to each other at two different sizes (Dom, 2026-09-12). One component, one box, one icon
   size; only the hue changes, and it comes from --c in every case. */
.fighter-el, .fighter-build, .fighter-match, .fighter-rar, .fighter-codex {
  --c: var(--st-body);
  display: inline-flex; align-items: center; justify-content: center; gap: 4px;
  min-height: 20px; min-width: 20px; padding: 0 7px; margin-left: 6px;
  border: 0; border-radius: 0; vertical-align: middle; white-space: nowrap;
  font-size: .64rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; line-height: 1;
  color: var(--c); background: color-mix(in srgb, var(--c) 12%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--c) 45%, transparent); }
.fighter-el .el-sym, .fighter-build .build-sym, .fighter-codex .codex-sym { font-size: .95rem; color: currentColor; opacity: 1; }
@media (max-width: 760px) { .fighter-el, .fighter-build, .fighter-match, .fighter-rar, .fighter-codex { margin-left: 4px; padding: 0 5px; }}

/* drawn nav icons, one grid and one stroke weight */
svg.nav-ic { width: 20px; height: 20px; display: block; margin: 0; flex: 0 0 auto; opacity: .85; }
.nav-btn.active svg.nav-ic { opacity: 1; }@media (max-width: 760px) { svg.nav-ic { width: 20px; height: 20px; margin: 0 auto; }}

/* ---------- the conjuring ceremony ---------- */
.conj-swell { position: relative; display: grid; place-items: center; min-height: 240px; width: 100%; }
.swell-ring { position: absolute; width: 60px; height: 60px; border: 2px solid var(--c); border-radius: 50%; opacity: 0; }
.conj-swell.go .swell-ring { animation: swell var(--dur, 1.2s) cubic-bezier(.3,.7,.4,1) infinite; }
.conj-swell.go .swell-ring.d2 { animation-delay: .18s; }
.conj-swell.go .swell-ring.d3 { animation-delay: .36s; }
@keyframes swell { 0% { transform: scale(.3); opacity: .9; } 100% { transform: scale(5.5); opacity: 0; } }
.swell-text { color: var(--muted); font-family: var(--serif); font-style: italic; letter-spacing: .04em; animation: pulse 1.6s ease-in-out infinite; }
@keyframes pulse { 0%,100% { opacity: .45; } 50% { opacity: 1; } }
.conj-reveal { position: relative; }
/* .conj-reveal.shown used to run one `arrive` on the whole container; the pieces sequence themselves now (see .rv-*) */
@keyframes arrive { 0% { transform: scale(.6); opacity: 0; filter: brightness(3); } 60% { filter: brightness(1.6); } 100% { transform: scale(1); opacity: 1; } }
.conj-reveal[data-tier="4"], .conj-reveal[data-tier="5"] { }
/* The mythic glow lives with the arrival now, as one two-item list, and the rule carries every
   animation longhand it needs rather than relying on a shorthand.
   What it replaces: `animation: mythicPulse 1.4s ease-in-out infinite` as a SHORTHAND, with the
   generic `.conj-reveal.shown .rv-2` rule further down the file at the same specificity. That rule
   won animation-name and animation-duration, because it is later, but it never declares
   animation-iteration-count, so the `infinite` from the rule it beat stayed. The ARRIVAL ran
   forever: rvStrike scales from 1.18 and re-spreads its letters every 420ms, which is what Dom
   watched loop (2026-09-16: "the mythic text keeps resizing in a loop"). mythicPulse itself never
   ran once. Specificity here is (0,4,0) against that rule's (0,3,0), so it no longer depends on
   which came first. */
.conj-reveal.shown[data-tier="5"] .reveal-rarity,
.reveal.shown[data-tier="5"] .reveal-rarity {
  animation-name: rvStrike, mythicPulse;
  animation-duration: 420ms, 1.4s;
  animation-delay: 380ms, 800ms;
  animation-iteration-count: 1, infinite;
  animation-fill-mode: both, none;
  animation-timing-function: cubic-bezier(.22, 1, .36, 1), ease-in-out;
}
@keyframes mythicPulse { 0%,100% { text-shadow: 0 0 18px var(--c); } 50% { text-shadow: 0 0 46px var(--c), 0 0 80px var(--c); } }
.reveal-rarity { font-family: var(--display); }

/* ---------- impact ---------- */
.sprite.flash img { filter: brightness(4) saturate(0); }
.dmg.ko { color: #FF6A48; font-family: var(--display); font-size: 1.5rem; letter-spacing: .14em; }

/* ---------- a result that looks like a result ---------- */
.modal-card.result-win { border-color: var(--gold); box-shadow: 0 0 0 1px rgba(176, 27, 42, 0.27), 0 0 80px rgba(176, 27, 42, 0.13), 0 24px 60px #00000090; }
.modal-card.result-win .modal-title { color: var(--ink-accent); font-size: 2.2rem; letter-spacing: .16em; text-shadow: 0 0 34px rgba(176, 27, 42, 0.40); text-align: center; }
.modal-card.result-loss .modal-title { color: var(--muted); font-size: 1.8rem; letter-spacing: .14em; text-align: center; }
.modal-card.result-win .result-rows, .modal-card.result-loss .result-rows { align-items: center; }

/* ---------- rewards, season, storefront ---------- */
.hdr-pill.ether b { color: var(--violet); }
.ic-ether::before { content: '⧫'; color: var(--violet); margin-right: 4px; }

.rewards-grid { display: grid; grid-template-columns: 1.4fr 1fr; gap: 14px; align-items: start; }
.quest-card, .streak-card, .season-card { background: var(--sunken); border: 1px solid var(--line); border-radius: 0; padding: 16px; }
.quest-card h3, .streak-card h3, .season-card h3 { font-family: var(--display); color: var(--ink-accent); margin: 0 0 4px; letter-spacing: .04em; }
.quest { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-top: 1px solid var(--line); }
.quest:first-of-type { border-top: 0; }
.quest-main { flex: 1; min-width: 0; }
.quest-main b { display: block; margin-bottom: 6px; }
.quest-bar { height: 6px; margin-bottom: 5px; }
.quest.done .quest-bar > i, .quest.claimed .quest-bar > i {
  background: linear-gradient(180deg, var(--st-bright), var(--st-light) 50%, var(--st-mid)); }
.quest.claimed .quest-main b, .quest.claimed .quest-main span { opacity: .5; }
.quest-tick { color: var(--ink-accent); font-size: .8rem; white-space: nowrap; }

.streak-row { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; margin: 12px 0 14px; }
.streak-pip { background: #ffffff08; border: 1px solid var(--line); border-radius: 0; padding: 8px 3px; text-align: center; font-size: .64rem; color: var(--muted); }
.streak-pip b { display: block; font-size: .7rem; color: var(--text); margin-bottom: 3px; }
.streak-pip.done { border-color: rgba(176, 27, 42, 0.33); background: rgba(176, 27, 42, 0.10); color: var(--ink-accent); }
.streak-pip.now { border-color: var(--gold); box-shadow: 0 0 14px rgba(176, 27, 42, 0.27); }

.season-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 10px; }
.season-bar { height: 8px; margin-bottom: 8px; }
.season-legend { display: flex; gap: 14px; font-size: .7rem; color: var(--muted); margin-bottom: 6px; }
.season-legend .gold { color: var(--ink-accent); }
.season-track { display: flex; gap: 4px; overflow-x: auto; padding: 6px 2px 10px; }
.season-col { display: flex; flex-direction: column; gap: 4px; align-items: center; flex: 0 0 auto; }
.season-n { font-size: .64rem; color: var(--muted); }
.season-col.next .season-n { color: var(--ink-accent); }
.season-pip { width: 58px; height: 46px; border-radius: 0; border: 1px solid var(--line); background: #ffffff08; color: var(--muted);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px; padding: 0 3px; font-family: var(--font); }
.season-pip b { font-size: .84rem; font-weight: 700; color: var(--text); line-height: 1.1; font-variant-numeric: lining-nums tabular-nums; }
.season-pip i { font-style: normal; font-size: .64rem; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); line-height: 1.1; white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
.season-pip.prem { border-color: rgba(176, 27, 42, 0.32); background: rgba(176, 27, 42, 0.08); }
.season-pip.prem b { color: var(--ink-accent); }
.season-pip.can { border-color: var(--gold); box-shadow: 0 0 12px rgba(176, 27, 42, 0.33); cursor: pointer; }
.season-pip.can b { color: var(--ink-accent); }
.season-pip.claimed { opacity: .4; }
.season-pip.locked { opacity: .55; }
.season-pip.locked .pip-lock { display: inline-flex; color: var(--st-light); }
.season-pip.locked i { color: var(--st-light); }
.season-col.milestone .season-n { color: var(--st-bright); font-weight: 700; }
.season-col.milestone .season-pip { border-color: var(--st-body); box-shadow: inset 0 1px 0 rgba(214, 224, 236, .12); }
.season-col.got .season-pip { background: rgba(138, 148, 166, .12); }

/* Segmented, per the handoff: a raised plate in a recessed track. It was an underlined text row
   set in Cinzel, which the spec rules out on a control ("Never Cinzel: it has no tabular figures
   and buttons carry counts"). 34px tall, Alegreya Sans, uppercase, .1em. */
.tabs { display: inline-flex; padding: 3px; margin: 0 0 16px; gap: 0;
  background: #0B0D10; box-shadow: inset 0 2px 5px rgba(0, 0, 0, .9), inset 0 0 0 1px #2A313B; }
.tab { position: relative; overflow: hidden; border: 0; border-radius: 0; cursor: pointer;
  height: 34px; padding: 0 20px; background: transparent;
  font-family: var(--font); font-size: 13px; font-weight: 500; letter-spacing: .1em;
  text-transform: uppercase; line-height: 1; color: #8A94A6;
  transition: background 90ms linear, box-shadow 90ms linear, color 90ms linear; }
.tab:hover:not(.on) { color: #D9DFE6; }
.tab:focus-visible { outline: 2px solid var(--st-bright); outline-offset: -2px; }
.tab.on { font-weight: 700; color: #F6F8FA;
  background: linear-gradient(180deg, #4E5866 0%, #5E6875 14%, #3A424E 48%, #242B34 78%, #191F26 100%);
  box-shadow: inset 0 1px 0 #8A94A6, inset 0 -1px 0 #0B0D10,
              inset 1px 0 0 rgba(217, 223, 230, .22), inset -1px 0 0 rgba(0, 0, 0, .5),
              0 1px 3px rgba(0, 0, 0, .7); }
h2.sec { font-family: var(--display); color: var(--ink-accent-hi); letter-spacing: .14em;
  font-size: .95rem; text-transform: uppercase; margin: 26px 0 12px;
  display: flex; align-items: center; gap: 12px; }
h2.sec::after { content: ''; flex: 1; height: 1px;
  background: linear-gradient(90deg, rgba(138, 148, 166, .4), transparent); }

.promise { background: linear-gradient(160deg, rgba(138, 148, 166, .08), transparent); border: 0; border-radius: 0; padding: 16px 18px; margin: 18px 0;
  box-shadow: inset 0 1px 0 rgba(214, 224, 236, .1), 0 0 0 1px rgba(0, 0, 0, .5); }
.promise h3 { font-family: var(--display); color: var(--st-light); margin: 0 0 6px; letter-spacing: .05em; }
.never { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.never span { font-size: .68rem; border: 1px solid var(--line); border-radius: 0; padding: 3px 10px; color: var(--muted); }

.skus { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 12px; }
.sku, .pack { position: relative; background: var(--sunken); border: 1px solid var(--line); border-radius: 0; padding: 16px; display: flex; flex-direction: column; gap: 8px; }
.sku.best, .pack.best { border-color: rgba(176, 27, 42, 0.40); }
.sku h3 { font-family: var(--display); color: var(--st-light); margin: 0; letter-spacing: .04em; }
.sku-flag { position: absolute; top: -9px; right: 12px; background: var(--gold); color: var(--st-spec); font-size: .7rem; font-weight: 700; padding: 2px 9px; border-radius: 0; letter-spacing: .04em; box-shadow: 0 0 0 1px rgba(0, 0, 0, .5); }
.sku-list { margin: 0; padding-left: 18px; font-size: .82rem; color: var(--muted); display: flex; flex-direction: column; gap: 4px; }
.sku-buy { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: auto; }
.packs { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
.pack { align-items: flex-start; }
.pack-amt { font-size: .75rem; color: var(--muted); }
.pill.gold { border: 1px solid var(--gold); color: var(--ink-accent); border-radius: 0; padding: 3px 12px; font-size: .72rem; }

.todo { position: relative; display: flex; justify-content: space-between; align-items: center;
  gap: 16px; border-radius: 0; padding: 15px 18px; margin: 18px 0 14px; flex-wrap: wrap;
  background: linear-gradient(120deg, rgba(176, 27, 42, .1), transparent 62%),
              linear-gradient(180deg, var(--panel2), var(--panel));
  box-shadow: inset 0 1px 0 rgba(214, 224, 236, .11), inset 0 -1px 0 rgba(0, 0, 0, .6),
              0 0 0 1px rgba(0, 0, 0, .5), 0 1px 2px rgba(0, 0, 0, .45); }
/* The brass stop on the leading edge, the same object as the active nav tab. */
.todo::before { content: ''; position: absolute; left: 0; top: 8px; bottom: 8px; width: 3px;
  border-radius: 0;
  background: linear-gradient(180deg, var(--bl-warm), var(--bl-body) 45%, var(--bl-deep));
  box-shadow: 0 0 12px -1px rgba(176, 27, 42, .7); }
.todo-main b { font-family: var(--display); font-size: 1.12rem; letter-spacing: .04em;
  color: var(--text); display: block; margin: 2px 0 5px; }
.todo-main p { margin: 0; max-width: 62ch; }
.todo-label { font-size: .64rem; letter-spacing: .2em; text-transform: uppercase;
  color: var(--ink-accent-hi); font-weight: 700; }
.daily-strip { border-radius: 0; padding: 13px 15px; margin-bottom: 16px; cursor: pointer;
  background: linear-gradient(180deg, #080A0C, var(--sunken));
  box-shadow: inset 0 1px 0 rgba(0, 0, 0, .85), inset 0 2px 4px rgba(0, 0, 0, .5),
              inset 0 -1px 0 rgba(214, 224, 236, .06); }
.ds-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.ds-flag { font-size: .68rem; color: var(--st-light); letter-spacing: .04em; }
.ds-quests { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; font-size: .76rem; }
.ds-q { display: flex; justify-content: space-between; gap: 8px; background: #ffffff08; border-radius: 0; padding: 7px 10px; color: var(--muted); }
.ds-q i { font-style: normal; color: var(--text); }
.ds-q.done { color: var(--ink-accent); background: rgba(176, 27, 42, 0.08); }
.ds-q.claimed { opacity: .45; }

.more-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
.more-btn { position: relative; background: #ffffff0a; border: 1px solid var(--line); border-radius: 0; padding: 16px 10px; color: var(--text); font-family: var(--display); letter-spacing: .04em; }
.more-btn .dot { position: absolute; top: 7px; right: 9px; background: var(--gold); color: #14100A; font-style: normal; font-size: .6rem; font-weight: 700; min-width: 16px; height: 16px; border-radius: 8px; display: grid; place-items: center; padding: 0 4px; }
.nav-more { display: none; }@media (max-width: 900px) { .rewards-grid { grid-template-columns: 1fr; }}
@media (max-width: 760px) {
  /* Ten tabs at 375px scrolled sideways and hid the last four. Five fit; the rest go in More. */
  .nav { overflow: visible; }
  .nav-btn { display: none; }
  .nav-btn[data-tier="1"], .nav-more { display: flex; }
  .todo { flex-direction: column; align-items: stretch; }
  .todo .btn { width: 100%; }
  .ds-quests { grid-template-columns: 1fr; }
  .streak-row { grid-template-columns: repeat(4, 1fr); }
  .skus, .packs { grid-template-columns: 1fr; }}

/* ---------- squad budget and the element ring ---------- */
.budget { background: var(--sunken); border: 1px solid var(--line); border-left: 3px solid var(--gold); border-radius: 0; padding: 14px 16px; margin: 14px 0; }
.budget.over { border-left-color: #FF5C7A; }
.budget-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; margin-bottom: 8px; }
.budget-head b { font-family: var(--display); font-size: 1.05rem; }
.budget-bar { height: 7px; margin-bottom: 10px; }
.budget.over .budget-bar > i { background: linear-gradient(180deg, #FF9DB2, #FF5C7A 50%, #B22F49);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.4), 0 0 12px -3px #FF5C7A; }
.budget-list { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.budget-chip { border: 1px solid var(--c); color: var(--c); border-radius: 0; padding: 3px 6px 3px 11px; font-size: .74rem; display: inline-flex; align-items: center; gap: 7px; }
.budget-chip i { font-style: normal; background: var(--c); color: #0C0E11; border-radius: 10px; min-width: 18px; text-align: center; font-weight: 700; padding: 1px 5px; }

.ec { display: flex; flex-direction: column; gap: 12px; }
.ec-grid { overflow-x: auto; }
.ec-row { display: flex; gap: 2px; margin-bottom: 2px; }
.ec-h { font-size: .68rem; color: var(--c, var(--muted)); display: grid; place-items: center; letter-spacing: .04em; }
.ec-head .ec-h { width: 44px; height: 24px; }
.ec-side { width: 86px; justify-content: flex-start; text-align: left; flex: 0 0 86px; padding-left: 2px; }
.ec-corner { width: 86px; flex: 0 0 86px; }
.ec-cell { width: 44px; height: 30px; flex: 0 0 44px; display: grid; place-items: center; font-size: .7rem; border-radius: 0; background: #ffffff08; color: var(--muted); }
.ec-cell.up { background: #3DF2FF1f; color: #7FE9FF; }
.ec-cell.down { background: #FF5C7A1a; color: #FF8FA3; }
.ec-cell.mine { outline: 1px solid rgba(176, 27, 42, 0.33); }
.ec-cover { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; border-top: 1px solid var(--line); padding-top: 12px; }
.ec-cover b { font-family: var(--display); color: var(--ink-accent); font-size: .85rem; letter-spacing: .04em; }
.ec-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; }
.ec-tags span { font-size: .68rem; border: 1px solid var(--c, var(--line)); color: var(--c, var(--muted)); border-radius: 0; padding: 2px 9px; }
.ec-tags span.bad { border-style: dashed; }@media (max-width: 760px) { .ec-cover { grid-template-columns: 1fr; } .ec-side, .ec-corner { width: 62px; flex: 0 0 62px; font-size: .55rem; }}

.equip-pick { display: flex; flex-direction: column; gap: 7px; margin-top: 10px; }
.equip-opt { text-align: left; background: #ffffff08; border: 1px solid var(--line); border-radius: 0; padding: 11px 13px; color: var(--text); display: flex; flex-direction: column; gap: 3px; }
.equip-opt b { font-family: var(--display); letter-spacing: .03em; }
.equip-opt:not(.full):hover { border-color: var(--st-body); }
.equip-opt.full { opacity: .45; }

.coach { display: flex; gap: 12px; align-items: center; border: 0; border-radius: 0; padding: 12px 14px; margin-bottom: 10px;
  background: linear-gradient(120deg, #272E37, #1A1F25); box-shadow: 0 10px 30px -18px #000; }
.coach-body { flex: 1; min-width: 0; }
.coach-body b { font-family: var(--display); color: var(--st-light); letter-spacing: .03em; display: block; margin-bottom: 3px; }
.coach-body p { margin: 0; font-size: .84rem; color: var(--muted); }@media (max-width: 760px) { .coach { flex-direction: column; align-items: stretch; gap: 9px; } .coach .btn { width: 100%; }}

.field-tag { font-size: .68rem; letter-spacing: .1em; text-transform: uppercase; border: 1px solid var(--c, var(--line)); color: var(--c, var(--muted)); border-radius: 0; padding: 2px 11px; }
.field-tag.plain { opacity: .55; }
.skill.big.onfield { box-shadow: inset 0 0 0 1px var(--c), 0 0 16px -6px var(--c); }

/* ---------- the arena as a place, and a phone that can reach its own buttons ---------- */
/* Two info panels on the page background is a stat readout, not a fight. A horizon, a ground
   plane and a far wall cost three gradients and turn it into somewhere the fight happens. */
.arena {
  background:
    radial-gradient(ellipse 60% 40% at 50% 92%, #38414D 0%, transparent 70%),
    linear-gradient(#14171C 0%, #1E2329 29.6%, #252B33 30%, #111317 100%);
  border-radius: 0;
}
/* The ground, actually receding. It used to be a flat grid: vertical lines that stayed vertical
   and horizontal lines evenly spaced, which is a graph, not a field, and it was the thing that
   kept the arena reading as a box with a line across it (Dom, 2026-09-12).
   One plane, laid down. rotateX with the origin ON the horizon converges the lines to a vanishing
   point there and bunches the cross lines as they recede, which is the whole of the effect. It is
   drawn wider than the arena on both sides because perspective pulls the near edge outward, and
   the mask fades it out before the grid gets so fine it aliases. */
.arena::after {
  content: ''; position: absolute; left: -60%; right: -60%; top: 30%; bottom: -40%;
  pointer-events: none; z-index: 0;
  background-image:
    repeating-linear-gradient(90deg, transparent 0 62px, rgba(138, 148, 166, .13) 62px 64px),
    repeating-linear-gradient(0deg, transparent 0 62px, rgba(138, 148, 166, .10) 62px 64px);
  transform: perspective(300px) rotateX(64deg);
  transform-origin: 50% 0%;
  mask-image: linear-gradient(#000 0%, #000 26%, transparent 88%);
  -webkit-mask-image: linear-gradient(#000 0%, #000 26%, transparent 88%);
}
.arena .fighter { position: relative; z-index: 1; }@media (max-width: 760px) {
  /* The action bar used to sit below the fold: at 375x812 with five skills its bottom measured
     928px in an 812px viewport, so every turn needed a scroll and a mis-tap hit the nav and
     silently abandoned the match. The battle screen is now a fixed column: the arena takes what
     is left and the buttons are always on screen. */
  .screen.battle.active { display: flex; flex-direction: column; height: calc(var(--vh) - var(--hdr-h) - var(--nav-h) - 34px); min-height: 0; }
  .arena { flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; padding: 8px; }
  .battle-hud { display: flex; flex-direction: column; margin-top: 8px; flex: 0 0 auto; }
  .battle-log { min-height: 44px; max-height: 62px; overflow-y: auto; font-size: .78rem; padding: 6px 10px; }
  /* The log is pinned to its newest line, and its height is a whole number of lines, so whatever
     is left over lands on the line at the TOP: it was being sliced horizontally through the
     glyphs and read as a rendering fault rather than as "there is more above". A fade says the
     same thing and says it on purpose. */
  .battle-log { -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 14px);
                mask-image: linear-gradient(180deg, transparent 0, #000 14px); }
  .actions { max-height: 42vh; overflow-y: auto; }
  .skill.big { min-height: 44px; padding: 7px 10px 7px 30px; }
  .skill.big .skill-sub { font-size: .68rem; }
  .coach { position: relative; margin: 0 0 8px; padding: 10px 12px; }
  .coach-body p { font-size: .78rem; line-height: 1.35; }
  .coach-body b { font-size: .9rem; }
  .sprite { width: 96px; height: 96px; }
  .battle-top { font-size: .7rem; gap: 6px; }
  /* The field tag and the speed button are a matched pair sitting side by side, so they are one
     height and one vertical rhythm rather than 19px next to 24px (Dom, 2026-09-15). */
  .battle-top .field-tag, .battle-top .speed-btn {
    display: inline-flex; align-items: center; justify-content: center;
    height: 24px; min-height: 24px; padding-top: 0; padding-bottom: 0; line-height: 1; }
  .battle-top .kind { font-size: .66rem; }}
@media (max-width: 760px) {
  /* Both fighter panels have to fit the arena, or the one that gets clipped is yours and you are
     playing without your own health bar. Everything here is compression, nothing is hidden. */
  .arena { grid-template-rows: 1fr minmax(30px, auto) 1fr; gap: 4px; padding: 6px; }
  .fighter { padding: 4px 6px; gap: 6px; align-items: center; }
  /* The panel was five stacked rows: name, health bar, health text, energy pips, and then the
     action gauge alone on the bottom with nothing beside it. A naked full-width bar under a full
     width health bar reads as a second, broken health bar, and the gap above it made the block
     look like it had come apart. The readout is one row now: health text, pips and gauge side by
     side, which reads as one instrument and gives two rows back to the fighters. */
  .fighter-info { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
  .fighter-readout { display: flex; flex-wrap: nowrap; align-items: center; gap: 6px; min-width: 0; }
  .fighter-sub { flex: 0 0 auto; }
  /* The pips are the only part of this row that can give width back, so they are the part that
     gives it: eight of them at 22px is the desktop size and would take the whole row on a phone. */
  .energy { flex: 0 1 auto; min-width: 0; }
  /* The action gauge keeps a readable length even when a status tag joins the row: it was being
     squeezed to 43px by a "Drain -37 3t", and a 43px bar cannot show you how close you are to
     moving. The pips give up the width instead, because eight of them still read at 5px each. */
  .gauge-row { flex: 1 1 68px; min-width: 68px; }
  .fighter-name { font-size: .82rem; line-height: 1.15; }
  /* The arrow already says which way it points and the panel it sits on says who it is about, so
     the words are what goes when the row will not fit. */
  .fighter-match .fm-t { display: none; }
  .fighter-build .fb-t, .fighter-el .fe-t, .fighter-codex .fc-t {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
  .fighter-el .el-sym { color: var(--c); }
  .fighter-sub { font-size: .62rem; }
  .hpbar { height: 8px; }
  .energy { transform: none; height: auto; gap: 2px; margin-bottom: 0; }
  .energy i { width: 10px; height: 7px; flex: 0 1 auto; min-width: 4px; }
  .energy-label { font-size: .7rem; }
  .gauge-row { font-size: .58rem; gap: 5px; margin-top: 0; }
  .gauge { height: 5px; }
  .sprite { width: 84px; height: 84px; }
  /* End, not center: centered in a tall band, every creature floated half a band above the ground
     it was meant to stand on (Dom, 2026-09-24: "the titans sit too high in the field"). */
  .sprite-wrap { display: grid; place-items: end center; }
  .fighter.side-1, .fighter.side-0 { grid-template-columns: 1fr 96px; }
  .bench-slot { width: 30px; }
  .order-chip { font-size: .64rem; padding: 1px 5px; }}
@media (max-width: 760px) {
  /* Last 60 pixels: two fighter panels at 133 each did not fit a 225 pixel arena. The log gives
     back 16, the sprite 12, and the rest comes out of the panel's own padding and line heights. */
  .battle-log { max-height: 46px; min-height: 34px; }
  .actions { gap: 6px; }
  .sprite { width: 72px; height: 72px; }
  .fighter { padding: 2px 6px; gap: 4px; }
  .fighter-name { font-size: .76rem; }
  .fighter-info { gap: 2px; }
  .energy i { width: 9px; height: 6px; }
  .next-tag { font-size: .55rem; padding: 1px 4px; }
  .gauge-label { display: none; }
  .fighter.side-1, .fighter.side-0 { grid-template-columns: 1fr 82px; }
  .battle-top { margin-bottom: 4px; }
  .order { margin-bottom: 4px; }}

/* ---------- what a cosmetic actually looks like ---------- */
.cos-frame { border: 2px solid var(--edge); border-radius: 0; background: var(--inner); padding: 5px; box-shadow: 0 0 18px -8px var(--edge); }
.cos-frame-art { display: grid; place-items: center; border: 1px solid var(--accent); border-radius: 0; background: #00000055; }
.cos-frame-bar { font-family: var(--display); font-size: .6rem; color: var(--accent); text-align: center; padding-top: 4px; letter-spacing: .05em; }
.cos-board { position: relative; aspect-ratio: 4 / 3; border-radius: 0; border: 1px solid var(--line); overflow: hidden;
  background:
    radial-gradient(ellipse 60% 40% at 50% 92%, var(--floor, #38414D) 0%, transparent 70%),
    repeating-linear-gradient(90deg, transparent 0 22px, color-mix(in srgb, var(--line, #8A94A6) 26%, transparent) 22px 23px),
    repeating-linear-gradient(#ffffff07 0 1px, transparent 1px 15px),
    linear-gradient(var(--sky, #14171C) 0%, var(--sky, #1E2329) 48%, var(--floor, #252B33) 48.5%, #111317 100%); }
/* Stood on the floor line, not floating in the middle of the swatch. */
.cos-board-fig { position: absolute; left: 50%; bottom: 16%; transform: translateX(-50%);
  display: block; line-height: 0; filter: drop-shadow(0 4px 6px rgba(0, 0, 0, .8)); }
.cos-card { line-height: 0; }
.cos-title { font-family: var(--display); font-size: .74rem; color: var(--ink-accent); border: 1px solid rgba(176, 27, 42, 0.33); border-radius: 0; padding: 8px 10px; text-align: center; background: rgba(176, 27, 42, 0.06); }
.cos-skin { display: grid; place-items: center; background: #ffffff06; border: 1px solid var(--line); border-radius: 0; padding: 4px; }
.skin-row { display: flex; gap: 10px; flex-wrap: wrap; }
.skin-card { text-align: center; width: 104px; display: flex; flex-direction: column; gap: 4px; }
.skin-card b { font-family: var(--display); font-size: .72rem; }
.sku-art { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; margin: 2px 0 4px; }
.sku-art.tokens { gap: 8px; }
.token-orb { display: grid; place-items: center; width: 38px; height: 38px; border-radius: 50%; border: 1px solid var(--st-body); color: var(--st-body); background: #A46BFF14; font-size: 17px; }
.pip-skin { display: block; transform: scale(.9); }
.season-pip .pip-skin .cos-skin { border: 0; background: none; padding: 0; }
.season-prize { display: flex; gap: 14px; align-items: center; border-top: 1px solid var(--line); padding-top: 12px; margin-top: 6px; }
.season-prize b { font-family: var(--display); color: var(--ink-accent); }
.season-prize p { margin: 4px 0 0; max-width: 58ch; }@media (max-width: 760px) { .season-prize { flex-direction: column; align-items: flex-start; }}

/* This panel is dropped into the market and leaderboard GRIDS, which are sized for listing
   cards, so it inherited a single narrow column and wrapped to one word a line. It spans the
   whole row now, and keeps a reading measure rather than a fixed cell. */
.offline { grid-column: 1 / -1; border-radius: 0; padding: 18px 20px; max-width: 62ch;
  position: relative;
  background: linear-gradient(180deg, #080A0C, var(--sunken));
  box-shadow: inset 0 1px 0 rgba(0, 0, 0, .85), inset 0 2px 4px rgba(0, 0, 0, .5),
              inset 0 -1px 0 rgba(214, 224, 236, .06); }
.offline::before { content: ''; position: absolute; left: 0; top: 10px; bottom: 10px; width: 3px;
  border-radius: 0; background: linear-gradient(180deg, var(--st-light), var(--st-body) 50%, var(--st-dark));
  box-shadow: 0 0 12px -2px rgba(138, 148, 166, .6); }
.offline b { font-family: var(--display); color: var(--st-light); display: block; margin-bottom: 6px; letter-spacing: .04em; }
.offline p { margin: 0 0 12px; }

.acct-val { color: var(--muted); font-size: .85rem; }
.acct-val i { font-style: italic; }

/* ---------- the lead picker's matchup tags (on the Square off) ---------- */
.lead-tags { display: flex; gap: 6px; flex-wrap: wrap; font-size: .66rem; }
.lead-tags span { border-radius: 0; padding: 2px 8px; border: 1px solid currentColor; }
.lead-tags .good { color: #7FE9FF; }
.lead-tags .bad { color: #FF8FA3; }
.lead-tags .field { color: var(--ink-accent); }

.pity { background: var(--sunken); border: 1px solid var(--line); border-radius: 0; padding: 16px 18px; margin-top: 18px; }
.pity h3 { font-family: var(--display); color: var(--ink-accent); margin: 0 0 4px; letter-spacing: .04em; }
.pity p { margin: 0 0 12px; max-width: 78ch; }
.pity-row { display: grid; grid-template-columns: 96px 1fr auto; gap: 12px; align-items: center; margin-top: 8px; font-size: .8rem; }
.pity-label { color: var(--muted); }
.pity-bar { height: 7px; }
.pity-row.due b { color: var(--ink-accent); }
.pity-row.due .pity-bar > i { background: linear-gradient(180deg, var(--bl-light), var(--bl-body) 50%, var(--bl-deep));
  box-shadow: inset 0 1px 0 rgba(255,248,220,.5), 0 0 14px -2px rgba(176,27,42,.8); }@media (max-width: 760px) { .pity-row { grid-template-columns: 74px 1fr; } .pity-row b { grid-column: 1 / -1; }}

/* A card wearing a bought frame, and an arena wearing a bought board. */
/* On the FACE, not on the card box: the face is what lifts on hover, and a glow left on the box
   behind it separates from the card the moment you point at it. */
/* The framed edge glow was a `filter: drop-shadow`, and a filter is its own rendering layer: put
   a transform on an ancestor and WebKit composites that layer separately, where a soft glow can
   come back as a hard pale rectangle around the card (Dom, 2026-09-16: "the glow around the frame
   is broken"). `.tc-face` is a rectangle carrying inset shadows already, so the same glow is an
   outer box-shadow with no layer of its own. It has to be folded into the framed rule below
   rather than declared here: box-shadow does not merge, a second declaration replaces the first
   and would take the frame's own edge with it. */
body.has-board .arena {
  background:
    radial-gradient(ellipse 60% 40% at 50% 88%, var(--board-floor) 0%, transparent 70%),
    linear-gradient(var(--board-sky) 0%, var(--board-sky) 29.6%, var(--board-floor) 30%, #070809 100%);
}
body.has-board .arena::after {
  background:
    repeating-linear-gradient(90deg, transparent 0 38px, color-mix(in srgb, var(--board-line) 34%, transparent) 38px 39px),
    repeating-linear-gradient(#ffffff08 0 1px, transparent 1px 26px);
}
.pack.worn { border-color: var(--gold); box-shadow: 0 0 20px -12px var(--gold); }
.exception { border-top: 1px solid rgba(138, 148, 166, .25); margin-top: 14px; padding-top: 12px; }
.exception b { display: block; font-family: var(--display); color: var(--ink-accent); letter-spacing: .04em; margin-bottom: 5px; }
.exception p { margin: 0 0 5px; }

.budget.under { border-left-color: var(--st-mid); }
.budget.under .budget-bar > i { background: linear-gradient(180deg, var(--st-light), var(--st-body) 50%, var(--st-dark));
  box-shadow: inset 0 1px 0 rgba(255,255,255,.3), 0 0 10px -3px rgba(138, 148, 166, .6); }

.fighter-side { color: var(--muted); font-size: .74em; font-weight: 400; }
.order-chip.incoming { opacity: .72; border-style: dashed; }

/* ---------- the codex ---------- */
.codex { margin-top: 22px; background: var(--sunken); border: 1px solid var(--line); border-radius: 0; overflow: hidden; }
.codex-head { display: grid; grid-template-columns: auto auto 1fr auto; gap: 14px; align-items: center; width: 100%; background: none; border: 0; color: var(--text); padding: 14px 18px; text-align: left; }
.codex-head b { font-family: var(--display); color: var(--ink-accent); letter-spacing: .05em; }
.codex-bar { height: 6px; }
.codex-caret { font-size: .72rem; color: var(--muted); }
.codex-body { padding: 4px 18px 18px; display: flex; flex-direction: column; gap: 14px; }
.codex-el-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 6px; }
.codex-el-head b { font-family: var(--display); color: var(--c); font-size: .88rem; letter-spacing: .04em; }
.codex-row { display: flex; flex-wrap: wrap; gap: 6px; }
/* A slot is art over a name now. The name is what makes the row a checklist instead of a score:
   a fixed width keeps the grid even, and the name is clamped to two lines rather than truncated
   so a longer species still reads. */
.codex-slot { width: 76px; display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 4px 3px 5px; border: 1px solid var(--line); border-radius: 0; background: #00000044; }
.codex-art { position: relative; width: 52px; height: 52px; display: grid; place-items: center; overflow: hidden; }
/* The codex is an INDEX of creatures rather than a gallery of pictures, so it shows the cutout
   (Dom, 2026-09-17: "cutouts are also good for codex"). At 52px a painting is its scene and almost
   none of its creature. A cutout is trimmed to the creature and rarely square, so it is contained
   in the square cell rather than stretched to it, and stands on the floor of the cell so a row of
   them shares a ground line.
   The `.codex-slot.saw` filter below silhouettes a merely-seen species, and that still works: it
   acts on whatever image is there. */
/* ABSOLUTE, not `height: 100%`. This cell is a grid whose row is content-sized, so a percentage
   height has no definite track to resolve against and silently falls back to auto: the image then
   took its aspect-derived height and a 319x538 cinderling rendered 52x88, spilling out of the cell
   and over the name. Measured, not guessed: computed height came back 87.6875px. Positioning
   against the 52px box gives the percentage something definite to mean.
   This is the same failure as the art lab card earlier, and it is silent every time. */
.codex-art img.cut { position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: contain; object-position: 50% 100%; }
.codex-name { font-size: .58rem; line-height: 1.15; text-align: center; letter-spacing: .02em;
  color: var(--muted); width: 100%; overflow: hidden;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.codex-slot.got .codex-name { color: var(--text); }
.codex-slot:not(.got):not(.saw) .codex-name { opacity: .5; }
.codex-slot.got { border-color: var(--r); box-shadow: inset 0 0 14px -8px var(--r); background: #ffffff06; }
.codex-slot.saw { border-style: dashed; border-color: color-mix(in srgb, var(--r) 45%, transparent); }
/* A set member whose art is not finished: present, named, and plainly not ready. Dimmed, not
   faded out: at .45 over the name's own .5 the names were unreadable (blind playtest round 2). */
.codex-slot.soon { opacity: .72; border-style: dotted; }
.codex-slot.soon .codex-name { opacity: .9; }
.codex-slot.saw img, .codex-slot.saw svg, .codex-slot.saw canvas {
  filter: brightness(0) invert(38%); opacity: .75; }
.codex-q { color: var(--muted); opacity: .45; font-size: 1.1rem; }
@media (max-width: 760px) { .codex-head { grid-template-columns: 1fr auto; gap: 6px 10px; } .codex-bar { grid-column: 1 / -1; }
  .codex-slot { width: 62px; } .codex-art { width: 42px; height: 42px; } .codex-name { font-size: .54rem; }}

/* The default arena, tinted by the ground the fight is actually on. A bought battleground overrides
   it, because that is what you paid for. */
/* The ground used to be a band of element color along the floor under a flat gunmetal sky, so on a
   phone the Infernum field read as a red stripe across the middle of a black box rather than as
   somewhere you are standing. The land is the whole field now (Dom, 2026-09-12): the sky carries
   the same hue at a fraction of the floor's strength, the horizon is still the brightest line in
   the frame, and the bottom falls to a deep tint of the element instead of to near-black. */
.arena.themed {
  background:
    radial-gradient(ellipse 66% 44% at 50% 90%, var(--board-floor) 0%, transparent 72%),
    radial-gradient(ellipse 120% 58% at 50% 4%, color-mix(in srgb, var(--board-line) 13%, transparent) 0%, transparent 72%),
    linear-gradient(var(--board-deep) 0%, var(--board-sky) 22%,
                    color-mix(in srgb, var(--board-line) 34%, var(--board-sky)) 29.6%,
                    var(--board-floor) 30%, var(--board-deep) 100%);
}
.arena.themed::after {
  background-image:
    repeating-linear-gradient(90deg, transparent 0 62px, color-mix(in srgb, var(--board-line) 34%, transparent) 62px 64px),
    repeating-linear-gradient(0deg, transparent 0 62px, color-mix(in srgb, var(--board-line) 24%, transparent) 62px 64px);
}

/* Once a zone has a real painted field, the two things that were IMITATING one have to stop.
   `.arena` paints a horizon-and-ground gradient and `.arena::after` lays a perspective grid over
   it, and both exist precisely because there was no picture of the place. With a picture they are
   a colored haze and a wireframe on top of a photograph, which is how Ember Flats, a genuinely
   good image of a cracked slag plain under a red sunset, rendered as a dark red blur (Dom,
   2026-09-17: "no battlegrounds").
   After `.arena.themed` on purpose: equal specificity, so source order decides. */
.arena.fielded { background: var(--ink); }
.arena.fielded::after { opacity: 0; }

.speed-btn { background: none; border: 1px solid var(--line); color: var(--muted); border-radius: 0; padding: 2px 12px; font-size: .68rem; letter-spacing: .1em; text-transform: uppercase; }
.speed-btn:hover { border-color: var(--st-body); color: var(--ink-accent-hi); }
.skill-sub .lethal { color: #FF5C7A; font-weight: 700; letter-spacing: .06em; }

/* While a teaching card is up the arena stands down: the card is 250 pixels tall and the arena on
   a phone is 230, so in the flow it clipped the player's own fighter panel in half. The turn order
   strip, the field tag and the skill buttons all stay where they are, which is what the cards
   actually point at. */
@media (max-width: 760px) {
  /* (moved out of the media query: the coach hides the arena at every width now) */
}

/* ==== battle readability: the matchup, said out loud ==== */
.modal-sub { margin: -6px 0 12px; font-size: .8rem; color: #9E97BC; }
/* Only the hue: the box, the type and the padding all come from the shared rule, and the old
   letter-spacing and vertical-align overrides here were the last two things keeping this tag a
   different shape from the three beside it. */
/* Codex state on the enemy. "New" is deliberately the brightest of the three: it is the only one
   of them that should change whether you spend a Net. */
.fighter-codex.is-caught { --c: #6FE39A; }
.fighter-codex.is-seen { --c: #B4BDC8; }
.fighter-codex.is-new { --c: #FFC53D; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--c) 70%, transparent); }
.fighter-match.up { --c: #6FE39A; }
.fighter-match.down { --c: #F0798B; }
.switch-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; }
.sw-foot { display: flex; flex-direction: column; gap: 4px; }
.sw-hp { display: flex; justify-content: space-between; gap: 6px; font-size: .58rem; color: #9E97BC;
  letter-spacing: .04em; }
.sw-hp b { color: #C7C0E0; font-weight: 600; }
.sw-tags { display: flex; flex-wrap: wrap; gap: 3px; }
.sw-tags span { padding: 1px 5px; border-radius: 0; font-size: .56rem; font-weight: 700; letter-spacing: .03em; }
.sw-up { color: #6FE39A; background: #6FE39A18; }
.sw-down { color: #F0798B; background: #F0798B18; }
.sw-field { color: var(--ink-accent); background: #E8B44618; }
.sw-even { color: #8E88A8; background: #ffffff0d; }@media (max-width: 720px) {
  .fighter-match { margin-left: 4px; }
  .switch-grid { grid-template-columns: repeat(auto-fill, minmax(128px, 1fr)); gap: 8px; }}

/* ---- one edge treatment for every remaining panel -------------------------------------------
   Thirty-five surfaces still carried `border: 1px solid var(--line)`: the same hue-neutral
   hairline on a stat plaque, a quest card, a battle log and a skill button, regardless of what
   any of them are. A flat mid-value line on a dark ground reads as a wireframe.
   The border stays in the box model as `transparent` so nothing reflows, and the edge is drawn
   with shadow instead: a warm rim along the top where the lamp catches it, an occluded line
   along the bottom, and a hairline key line to separate the whole plate from the page. */
.rung, .team-slot, .skill, .fighter-info, .bench-slot, .battle-log, .build-chip, .aura, .skcard,
.conj-note { margin: -2px 0 10px; }
/* The one instruction on the style row that changes what you do with it (Dom, 2026-09-22: "Bold
   and highlight PICK UP TO THREE"): bold, capitals, and a band of amber behind it. */
/* A form still being painted: a square the size of the picture it is waiting for, a ring turning in
   the element's color, and the word. It replaces the drawings that used to stand in first. */
.conj-pending { position: relative; display: grid; place-items: center; width: 100%; max-width: 280px; aspect-ratio: 1;
  background: radial-gradient(circle at 50% 45%, color-mix(in srgb, var(--c) 18%, transparent), transparent 70%); }
.conj-pending i { width: 38%; aspect-ratio: 1; border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--c) 25%, transparent); border-top-color: var(--c);
  animation: conjPaint 1.1s linear infinite; }
.conj-pending em { position: absolute; bottom: 14%; font-style: normal; font-size: .75rem; letter-spacing: .12em;
  text-transform: uppercase; color: color-mix(in srgb, var(--c) 70%, #FFFFFF); }
@keyframes conjPaint { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .conj-pending i { animation: none; } }
.conj-form.pending { cursor: progress; }
.conj-pick3 { font-weight: 800; text-transform: uppercase; letter-spacing: .06em; color: #FFE39A;
  background: color-mix(in srgb, #FFC53D 22%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, #FFC53D 55%, transparent);
  padding: 1px 6px; white-space: nowrap; }
/* ...except where the container already owns the spacing. That -2px is a nudge for these plates
   sitting in a column of their own; a skill is a cell in a grid with a gap, and the -2px lifted it
   OUT of its container. `.actions` scrolls on a phone, so the two pixels the button stuck up were
   clipped and the top edge of every attack button was cut off (Dom, 2026-09-12). The 10px below it
   was landing on top of the grid's own gap as well, which is why a 38px button sat in a 46px row. */
.skill-grid > .skill, .skill-head > .skill, .pills > .skill, .learn > .skill { margin: 0; }
/* An element tile is about 100px across in the three-column grid on a phone, which the name
   already fills, so the tag takes its own line rather than being clipped off the edge. */
.conj-rec { display: block; width: fit-content; font-style: normal; font-size: .62rem;
  letter-spacing: .1em; text-transform: uppercase; margin: 4px 0 0; padding: 1px 6px;
  border-radius: 0; color: var(--st-bright); border: 1px solid var(--line2); white-space: nowrap;
  max-width: 100%; overflow: hidden; }
.conj-rec.good { color: var(--st-spec); border-color: var(--st-body); }
/* good / hard / even were told apart by color: bright, red, muted. With the red gone they are told
   apart by VALUE instead, or "this element is against you" and "this one suits you" would have been
   the same chip with a slightly different border. The border keeps its red, because a border is a
   surface. */
.conj-rec.hard { color: var(--st-body); border-color: rgba(176, 27, 42, .45); }
.conj-opt, .conj-form, .mat-essence, .shard, .reveal-sig, .order, .quest-card, .streak-card,
.season-card, .sku, .pack, .more-btn, .budget, .equip-opt, .cos-skin {
  border-color: transparent;
  border-radius: 0;
  background-image: linear-gradient(180deg, rgba(255, 255, 255, .022), rgba(0, 0, 0, .16));
  box-shadow:
    inset 0 1px 0 rgba(214, 224, 236, .09),
    inset 0 -1px 0 rgba(0, 0, 0, .55),
    0 0 0 1px rgba(0, 0, 0, .45),
    0 1px 2px rgba(0, 0, 0, .35);
}
/* The interactive ones lift toward the light on hover rather than swapping a flat fill.
   THIS was the red line on hover (Dom, four times, most recently 2026-09-18 with a Quake
   screenshot: "the red mouse over is still there. Whats the deal?"). Three passes looking for it
   missed it because they were grepping for `outline` and for the blood TOKENS: this one is a raw
   rgba(176, 27, 42, .3) sitting fourth in a box-shadow list, and it is a 1px spread ring, which is
   exactly the shape of an outline. Steel now, like every other hover in the game.
   Big skill buttons are scoped out entirely. `.skill:hover:not(:disabled)` is (0,3,0) and
   `.skill.big` is (0,2,0), so this rule was also wiping a mythic's or a legendary's own resting
   shadow the moment the pointer touched it. A big skill's hover is the light that sweeps across
   the plate, and it does not need a ring at all. */
.skill:not(.big):hover:not(:disabled), .build-chip:hover:not(.active), .conj-opt:hover,
.conj-form:hover, .more-btn:hover, .cos-skin:hover, .team-slot:hover {
  box-shadow:
    inset 0 1px 0 rgba(214, 224, 236, .16),
    inset 0 -1px 0 rgba(0, 0, 0, .55),
    0 0 0 1px rgba(214, 224, 236, .22),
    0 2px 6px -1px rgba(0, 0, 0, .5);
}
/* Selected states keep their own accent, drawn as a brass rim rather than a border color. */
.build-chip.active, .conj-opt.on, .conj-form.on {
  box-shadow:
    inset 0 1px 0 rgba(214, 224, 236, .2),
    inset 0 0 0 1px rgba(176, 27, 42, .55),
    0 0 0 1px rgba(0, 0, 0, .5),
    0 0 18px -6px rgba(176, 27, 42, .4);
}

/* Scrollbars. The season track scrolls horizontally and its bar rendered as a bright unstyled
   strip across the bottom of the panel, which is the one piece of default browser chrome loud
   enough to break the whole surface. Firefox gets the standard properties; WebKit gets its
   pseudo-elements. Width stays at 10px so it is still a comfortable drag target. */
* { scrollbar-width: thin; scrollbar-color: var(--line2) transparent; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: rgba(0, 0, 0, .35); border-radius: var(--r-pill); }
::-webkit-scrollbar-thumb {
  border-radius: var(--r-pill);
  background: linear-gradient(180deg, var(--line2), var(--line));
  box-shadow: inset 0 1px 0 rgba(214, 224, 236, .12), inset 0 -1px 0 rgba(0, 0, 0, .5);
}
::-webkit-scrollbar-thumb:hover { background: linear-gradient(180deg, #5A697D, var(--line2)); }
::-webkit-scrollbar-corner { background: transparent; }

/* ---- modal: a plate lifted off the table ---- */
.modal-card { border-radius: 0; border: 0;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .8), 0 30px 70px -20px rgba(0, 0, 0, .9); }
.modal-card::before { content: ''; position: absolute; inset: 0; padding: 1px; border-radius: inherit;
  border: 0; pointer-events: none; z-index: 2;
  background: conic-gradient(from 218deg,
    /* The peak used to reach --st-spec, the cold white. On a rounded panel that read as a rim;
       square, the same arc lands along a flat edge and draws a hard white rectangle. The
       handoff's own panels are a single steel rule, so the specular tops out at --st-light. */
    var(--st-shadow) 0deg, var(--st-body) 38deg, var(--st-light) 52deg, var(--st-dark) 96deg,
    var(--st-shadow) 156deg, var(--st-void) 194deg, var(--st-dark) 238deg, var(--st-body) 292deg,
    var(--st-shadow) 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude; }
.modal-title { letter-spacing: .06em;
  background-image: linear-gradient(178deg, var(--st-mid) 0%, var(--st-light) 26%, var(--st-spec) 46%,
    var(--st-bright) 54%, var(--st-body) 68%, var(--st-dark) 86%, var(--st-shadow) 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent; -webkit-text-fill-color: transparent;
  filter: drop-shadow(0 1px 0 rgba(0, 0, 0, .9)); }

/* ---- coaching: a strip that points at the board, with the board still there ------------------
   Hiding the arena while coaching meant the card titled "The fuller bar is not the next to move"
   was shown while the bars were display:none. The coach is a compact strip above the actions
   now, the arena stays, and the element the step names gets a pulsing iron outline. */
/* the log stays up while the coach is up; hiding it was a first-session blackout */
/* The card FLOATS over the fight and costs no layout at all. In the HUD it took 133px out of the
   one screen with no spare height, and the arena paid: the fighters' row collapsed and both
   creatures vanished (Dom, 2026-09-13). Fixed, it cannot take anything from anyone.
   Above the effects canvas at 50, below a dialog at 60, so a result screen still covers it. */
.coach { position: fixed; z-index: 54; left: 50%; transform: translateX(-50%);
  top: calc(var(--hdr-h) + 14px); width: min(430px, calc(100vw - 24px)); margin: 0;
  padding: 12px 14px;
  background: linear-gradient(180deg, var(--panel3), var(--panel2));
  box-shadow: inset 0 1px 0 rgba(214, 224, 236, .12), inset 0 -1px 0 rgba(0, 0, 0, .6),
              0 0 0 1px rgba(0, 0, 0, .55), 0 18px 44px -18px rgba(0, 0, 0, .95); }
/* The exact y is measured in showCoach: there is no slot on a phone that covers nothing, so it
   picks the cheapest one and never crosses the lit band. */
/* On a short phone the only slot that covers no readout is the strip of ground between the two
   of them, about 142px. A 141px card needed its margins too, so it always cost a readout; capped,
   it fits cleanly and the body scrolls if a step's text is long. */
@media (max-height: 740px) {
  .coach { max-height: 118px; }
  .coach-body { overflow-y: auto; }
}
/* A dialog owns the screen; the card and its dimming stand aside until it closes. */
.coach.hidden-by-modal, .coach-veil.hidden-by-modal { visibility: hidden; pointer-events: none; }

/* The dark. Four strips around the target's rectangle, not one scrim with the target lifted
   above it: `.arena` clips its contents and `.fighter-info` carries a z-index, so anything inside
   either is trapped under a full-screen overlay whatever z-index you give it. Four strips leave a
   real hole, and the hole stays clickable, which is what you want when the card is pointing at a
   button and telling you to press it. */
.coach-veil { position: fixed; z-index: 52; background: rgba(6, 8, 10, .74);
  backdrop-filter: saturate(.45) brightness(.8); -webkit-backdrop-filter: saturate(.45) brightness(.8); }
@media (prefers-reduced-motion: reduce) { .coach-veil { backdrop-filter: none; -webkit-backdrop-filter: none; } }
.coach-body b { font-size: .84rem; margin-bottom: 1px; }
.coach-body p { font-size: .78rem; line-height: 1.4; }
.coach-hi { outline: 2px solid var(--st-bright); outline-offset: 3px; border-radius: 0; animation: coachPulse 1.3s ease-in-out infinite; }
@keyframes coachPulse { 0%, 100% { outline-color: var(--st-bright); } 50% { outline-color: color-mix(in srgb, var(--st-bright) 35%, transparent); } }   /* outline color only: the old box-shadow pulse repainted every frame */
@media (prefers-reduced-motion: reduce) { .coach-hi { animation: none; } }

/* ---- the reveal, as a sequence -------------------------------------------------------------
   Card first, because it is the thing; the rarity struck over it a beat later; the numbers
   counting up as their plaques land; the signature; then the button. Each piece arrives on its
   own delay and STOPS, so nothing is read while it is moving. Opacity runs linear and the
   movement is eased; a fading element on an ease-out reads as hesitating. */
.conj-reveal.shown .rv, .reveal.shown .rv { animation-fill-mode: both; animation-timing-function: cubic-bezier(.22, 1, .36, 1); }
.conj-reveal.shown .rv-1, .reveal.shown .rv-1 { animation-name: rvCard; animation-duration: 620ms; }
.conj-reveal.shown .rv-2, .reveal.shown .rv-2 { animation-name: rvStrike; animation-duration: 420ms; animation-delay: 380ms; }
.conj-reveal.shown .rv-3 { animation-name: rvRise; animation-duration: 380ms; animation-delay: 560ms; }
.conj-reveal.shown .rv-3 .rs { animation: rvRise 340ms cubic-bezier(.22, 1, .36, 1) both; animation-delay: calc(560ms + var(--i, 0) * 60ms); }
.conj-reveal.shown .rv-4 { animation-name: rvRise; animation-duration: 380ms; animation-delay: 900ms; }
.conj-reveal.shown .rv-5 { animation-name: rvFade; animation-duration: 300ms; animation-timing-function: linear; animation-delay: 1200ms; }
@keyframes rvCard { 0% { opacity: 0; transform: translateY(26px) scale(.82) rotate(-2.5deg); filter: brightness(2.2); }
                    55% { filter: brightness(1.25); } 100% { opacity: 1; transform: none; filter: brightness(1); } }
@keyframes rvStrike { 0% { opacity: 0; transform: translateY(-22px) scale(1.18); letter-spacing: .4em; }
                      100% { opacity: 1; transform: none; letter-spacing: .12em; } }
@keyframes rvRise { 0% { opacity: 0; transform: translateY(14px); } 100% { opacity: 1; transform: none; } }
@keyframes rvFade { 0% { opacity: 0; } 100% { opacity: 1; } }
/* The flash: a radial burst in the rarity's color that peaks as the card lands and is gone by the
   time the rarity is struck. It sits behind the pieces. */
.conj-reveal.shown::before, .reveal.shown::before { content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 0;
  background: radial-gradient(closest-side, color-mix(in srgb, var(--c, var(--st-light)) 55%, transparent), transparent 70%);
  animation: rvFlash 900ms ease-out both; }
.conj-reveal.shown > *, .reveal.shown > * { position: relative; z-index: 1; }
/* ends at scale 1: with fill-mode both, a 1.6 final frame left the glow 878px wide and grew a scrollbar under the card */
@keyframes rvFlash { 0% { opacity: 0; transform: scale(.4); } 30% { opacity: .9; } 85% { opacity: 0; transform: scale(1.6); } 100% { opacity: 0; transform: scale(1); } }
/* Legendary and Mythic get a shockwave ring on top of the flash. Loud moments are the only place
   for this, and there are two of them in the game. */
.conj-reveal[data-tier="4"].shown::after, .conj-reveal[data-tier="5"].shown::after,
.reveal[data-tier="4"].shown::after, .reveal[data-tier="5"].shown::after { content: ''; position: absolute; left: 50%; top: 42%;
  width: 120px; height: 120px; margin: -60px 0 0 -60px; border-radius: 50%; pointer-events: none; z-index: 0;
  border: 2px solid var(--c); box-shadow: 0 0 24px -4px var(--c), inset 0 0 24px -8px var(--c);
  animation: rvRing 1100ms cubic-bezier(.2, .7, .3, 1) both; animation-delay: 120ms; }
@keyframes rvRing { 0% { opacity: .95; transform: scale(.3); } 85% { opacity: 0; transform: scale(7); } 100% { opacity: 0; transform: scale(1); } }
.conj-reveal { --c: var(--st-light); }
.conj-reveal[data-tier="2"] { --c: #4AA3F0; } .conj-reveal[data-tier="3"] { --c: #C07DFA; }
.conj-reveal[data-tier="4"] { --c: #FF8000; } .conj-reveal[data-tier="5"] { --c: #E6CC80; }@media (prefers-reduced-motion: reduce) {
  .conj-reveal.shown .rv, .reveal.shown .rv, .conj-reveal.shown .rv-3 .rs,
  .conj-reveal.shown[data-tier="5"] .reveal-rarity, .reveal.shown[data-tier="5"] .reveal-rarity { animation: rvFade 120ms linear both; animation-delay: 0ms !important; }
  .conj-reveal.shown::before, .reveal.shown::before, .conj-reveal.shown::after { display: none; }}

/* ---- once the card has landed, the title steps back ---- */
.conjure.revealed .conjure-title { font-size: clamp(1rem, 2.4vw, 1.5rem); padding: 0; letter-spacing: .14em; }
.conjure.revealed .conjure-sub { display: none; }
.conj-reveal { gap: 8px; }

/* ---- rank crests: five shields in five metals, so the ladder is not five text tiles ---- */
.rung { position: relative; padding-left: 48px; --c: #B07A3E; }
.rung:nth-child(2) { --c: #B4BDC8; } .rung:nth-child(3) { --c: #D4A63A; } .rung:nth-child(4) { --c: #9FD3E8; } .rung:nth-child(5) { --c: #E8697A; }
.rung::before { content: ''; position: absolute; left: 12px; top: 50%; width: 26px; height: 30px; margin-top: -15px;
  clip-path: polygon(50% 0, 100% 16%, 100% 58%, 50% 100%, 0 58%, 0 16%);
  background: linear-gradient(160deg, color-mix(in srgb, var(--c) 55%, #fff) 0%, var(--c) 42%, color-mix(in srgb, var(--c) 50%, #000) 100%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .35); }
.rung.here { border-color: transparent; background: linear-gradient(180deg, var(--panel3), var(--panel2));
  box-shadow: inset 0 1px 0 rgba(214, 224, 236, .16), inset 0 0 0 1px color-mix(in srgb, var(--c) 55%, transparent), 0 0 0 1px rgba(0, 0, 0, .5), 0 0 18px -6px var(--c); }
.rung.here::before { filter: drop-shadow(0 0 6px var(--c)); }

/* ---- zones: a place, not a plate. An element watermark and an atmospheric horizon until the
   painted key art lands through the same pipeline as the portraits (.has-art). ---- */
.zone { overflow: hidden; }
.zone::before { content: ''; position: absolute; right: -18%; bottom: -40%; width: 70%; aspect-ratio: 1; border-radius: 50%; pointer-events: none; z-index: 0;
  background: radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--e) 42%, transparent) 0, color-mix(in srgb, var(--e) 18%, transparent) 38%, transparent 70%); }
.zone-flavor { margin: 2px 0 0; font-family: var(--serif); font-style: italic; font-size: .84rem; line-height: 1.35; color: var(--text-body); max-width: 34ch; }
/* The panel is `display: contents` on a phone, so its children are the card's own children and
   `.zone > *` no longer reaches them. Without this they paint in the background layer, UNDER the
   watermark and the bottom wash, both of which are positioned at z-index 0. */
.zone > *, .zone-panel > * { position: relative; z-index: 1; }
/* A phone keeps the single column it has always had: the panel dissolves and the art's half is
   not drawn at all. The desktop banner below turns both back on. */
.zone-panel { display: contents; }
.zone-art-side { display: none; }
.zone::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 46%; pointer-events: none; z-index: 0;
  background: linear-gradient(180deg, transparent, color-mix(in srgb, var(--e) 14%, transparent)); }
.zone.has-art { background-image: linear-gradient(180deg, rgba(11, 13, 16, .35), rgba(11, 13, 16, .92) 70%), var(--zone-art); background-size: cover; background-position: center; }
.zone.has-art::before { display: none; }
.zone-name { font-size: 1.22rem; }

/* ---- a zone is a banner on a desktop (Dom, 2026-09-17) ----
   "For desktop I would like to have the buttons fill 1/3 of the zone box with a black gradient
   behind it starting from the left to right and the right side of the box is the image of the
   zone uncovered."
   Three 407px cards across scrimmed the painted zone top to bottom at 92% black to keep the text
   legible, which meant the art was paid for and then covered up. One zone per row instead: the
   panel takes the left third, the scrim runs ACROSS and is gone by the time the art starts, and
   the zone is a place you are looking at rather than a texture behind a list.
   Below 761 this does not apply at all and the phone keeps its single column: a third of a 350px
   card is 116px, which does not hold "Levels 7 to 18". */
@media (min-width: 761px) {
  /* Later than the 1100px rule above it on purpose: same specificity, so this is the one that
     wins. Written as a media query rather than a wider selector for exactly that reason. */
  /* Capped and centered (Dom, 2026-09-17). Unbounded, a third of the box grew with the window:
     at 1990 the card was 1795, the panel 598 and each depth button 562 wide with the name pinned
     left and the level band pinned right, so all three carried about 400px of empty middle. The
     cap holds the panel near 460 and gives every pixel past it to the art. */
  .zones { grid-template-columns: 1fr; gap: 14px; max-width: 1400px; margin-inline: auto; }
  /* The page head lines up with the zones it introduces (Dom, 2026-09-22: "the Hunt and
     descriptions should be left justified to where the Zones start"). */
  #screen-hunt .page-head { max-width: 1400px; margin-inline: auto; }
  /* The height is for the art. A locked zone carries a name, a line of flavor and the rating it
     opens at, which came to 134px, and 134px of a 1245px box is a 9:1 slot no painted landscape
     survives being cropped into; an unlocked one set its own height from the three buttons and
     stopped at 293, which is still 4.8:1. The floor drives both now, so the zone is a picture with
     a panel on it rather than a strip (Dom, 2026-09-17: "lets add more height as well"). The
     panel puts its text at the top and its buttons at the bottom of whatever that comes to. */
  /* A third plus the panel's own two 18px gutters, so it is the BUTTONS that measure a third of
     the box and not the panel around them (Dom, 2026-09-17: "the level buttons can increase the
     text size, height and width to be 1/3 of the zone box"). They were 431 of a 1400 box. */
  /* The box was briefly shaped by the picture instead: aspect-ratio 2.73, which is the height at
     which a 1344x768 zone stands full height in the space the panel leaves, so nothing was
     cropped. 513px tall at the cap. Dom looked at it and said no (2026-09-17), so the height is
     back to the floor and the art is cropped to fit, anchored bottom right. */
  .zone { display: grid; grid-template-columns: calc(33.3333% + 36px) 1fr; gap: 0; padding: 0;
    min-height: clamp(240px, 26vw, 380px); }
  .zone-art-side { display: block; }
  .zone-panel { display: flex; flex-direction: column; gap: 5px; min-width: 0;
    padding: 15px 18px; position: relative; z-index: 1; }
  .zone-art-side { position: relative; z-index: 0; }
  .zone-depths { margin-top: auto; padding-top: 9px; gap: 8px; }
  /* Sized for a 407px card in a three-across grid, which is what these were. On a banner they are
     the thing you came to the screen to press, so they carry the weight: 62px tall against 48. */
  .depth { padding: 13px 16px; }
  .depth b { font-size: 1rem; letter-spacing: .06em; }
  .depth i { font-size: .95rem; }
  .depth u { font-size: .8rem; }
  .depth em { font-size: .76rem; }
  .depth .depth-lock { font-size: .9rem; }
  /* The column is 420px at 1440 and the flavor was set to 34ch for a 407px card, so it kept its
     own narrower measure inside a panel that is wider than it. */
  .zone-flavor { max-width: none; }
  /* Solid under the panel, gone by 70%. The two stops in the middle are what keeps the fade off
     the panel's own right edge: a single stop from 33% put visible haze over the first third of
     the art and read as a smudge rather than as a scrim. */
  .zone.has-art {
    background-image:
      linear-gradient(90deg, rgba(8, 9, 11, .97) 0%, rgba(8, 9, 11, .95) 34%,
        rgba(8, 9, 11, .62) 48%, rgba(8, 9, 11, .12) 62%, transparent 74%),
      var(--zone-art);
    /* Anchored bottom right (Dom, 2026-09-17). `cover` on a 3.7:1 box crops a 1.75:1 painting
       hard, and where that crop falls is the whole of what you see: centered it took a band out
       of the middle and cut the ground off, so nine zones all read as sky. From the corner the
       foreground survives, which is the half of a zone that says what standing in it is like,
       and the part that is lost is the top of the sky, which is the part every zone shares. */
    background-size: cover, cover; background-position: center, right bottom; }
  /* The bottom wash is a vertical device and this box is horizontal: over the art it drew a band
     of element color across the bottom of the picture. */
  .zone.has-art::after { display: none; }
}

/* ---- box reveal: one modal, the actions arrive when the card has ---- */
.reveal-modal .modal-actions { opacity: 0; pointer-events: none; transition: opacity 300ms linear; }
/* Belt and braces for browsers without inert: a hidden action must not be reachable by Tab. */
.reveal-modal:not(.ready) .modal-actions .btn { visibility: hidden; }
.reveal-modal.ready .modal-actions { opacity: 1; pointer-events: auto; }

/* ---- a paid or token conjuring says so on every step ---- */
.conj-tier { display: inline-flex; align-items: center; gap: 8px; margin: 0 auto 10px; padding: 4px 12px; border-radius: var(--r-pill);
  font-size: .72rem; letter-spacing: .12em; text-transform: uppercase; color: var(--st-light);
  background: linear-gradient(180deg, #06050E, var(--sunken));
  box-shadow: inset 0 1px 0 rgba(0, 0, 0, .9), inset 0 -1px 0 rgba(214, 224, 236, .08), 0 0 0 1px rgba(0, 0, 0, .5); }
/* The choices from the Shape step, read back on Describe it: one quiet line of label and value,
   not three more buttons, because nothing here is pressed. */
.conj-picks { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px 18px; margin: 2px auto 0; font-size: .86rem; }
.conj-picks:empty { display: none; }
.conj-pick { display: inline-flex; align-items: center; gap: 6px; text-shadow: 0 1px 2px rgba(0, 0, 0, .95); }
.conj-pick em { font-style: normal; font-size: .68rem; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
.conj-pick b { color: var(--text); font-weight: 600; }
.conj-pick .build-sym { font-size: 1rem; }
.conj-tier b { color: var(--text); font-weight: 700; letter-spacing: .04em; text-transform: none; font-size: .78rem; }

/* ---- the reveal composes as a pack-opening on a wide screen: the card, and its details beside
   it. Stacked, at 1280x800 the button landed at y 848; side by side the whole reveal is the height
   of the card and nothing needs a scrollbar to reach it. Phones keep the stack. ---- */
@media (min-width: 900px) {
  .conj-reveal.shown { display: grid; grid-template-columns: auto minmax(240px, 360px); grid-template-areas: "card rarity" "card sig" "card actions";
    column-gap: 28px; row-gap: 14px; align-items: center; justify-content: center; text-align: left; }
  .conj-reveal.shown .tcard { grid-area: card; }
  .conj-reveal.shown .reveal-rarity { grid-area: rarity; align-self: end; }
  .conj-reveal.shown .reveal-sig { grid-area: sig; align-items: flex-start; text-align: left; }
  .conj-reveal.shown .conj-actions { grid-area: actions; align-self: start; justify-content: flex-start; }
  .conj-reveal.shown::before { inset: 0; }
}

.rarity::before { content: ''; display: inline-block; width: 7px; height: 7px; margin-right: 7px; transform: rotate(45deg); border-radius: 1px;
  background: linear-gradient(135deg, #ffffffd0, var(--c) 55%, color-mix(in srgb, var(--c) 45%, #000)); box-shadow: 0 0 6px -1px var(--c); vertical-align: 1px; }

/* ---- round 4: result ledger and rank bar, currency legend, header essence, audio details ---- */
.result-grid { display: grid; grid-template-columns: 1fr auto; gap: 6px 18px; padding: 10px 14px; margin: 0 0 10px;
  background: var(--panel); box-shadow: inset 0 1px 0 rgba(214, 224, 236, .08), 0 0 0 1px rgba(0, 0, 0, .5); border-radius: 0; }
.result-grid .rg-k { color: var(--muted); font-size: .9rem; }
.result-grid .rg-v { font-variant-numeric: lining-nums tabular-nums; text-align: right; }
.result-grid .rg-v.good { color: var(--lime); } .result-grid .rg-v.bad { color: var(--ink-accent); }   /* the one number a ladder player looks for */
.result-rows:empty { display: none; }
.rank-strip { margin: 0 0 12px; padding: 10px 14px; background: var(--panel2); border-radius: 0; box-shadow: 0 0 0 1px rgba(0, 0, 0, .5), inset 0 1px 0 rgba(214, 224, 236, .1); }
.rank-strip-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px; }
.rank-strip-head b { font-family: var(--display); letter-spacing: .06em; color: var(--st-light); }
.rank-strip-head span { font-size: .82rem; color: var(--muted); font-variant-numeric: lining-nums tabular-nums; }
.rank-bar { height: 8px; border-radius: 4px; background: var(--sunken); box-shadow: inset 0 1px 2px rgba(0, 0, 0, .7); overflow: hidden; }
.rank-bar i { display: block; height: 100%; background: linear-gradient(90deg, var(--bl-dark), var(--bl-body), var(--bl-light)); transition: width 900ms cubic-bezier(.22, 1, .36, 1); }
.rank-next { display: block; margin-top: 6px; }
.cur-legend { display: flex; flex-wrap: wrap; gap: 6px 18px; margin: -6px 0 14px; font-size: .74rem; color: var(--muted); }
.cur-legend span { display: inline-flex; align-items: center; gap: 5px; }
.cur-legend em { font-style: normal; }
.cur-legend .lg-glyph { font-style: normal; color: var(--st-light); font-size: .9rem; line-height: 1; }
.cur-legend .lg-glyph.ether { color: var(--violet); }
.cur-legend .lg-glyph.credit { color: var(--amber); }
.ic-essence::before { content: '\2727'; color: var(--st-light); margin-right: 4px; }
.hdr-pill.essence b { color: var(--st-bright); }
/* phones show credits and Essence (Rewards and Conjure spend it); Ether waits for a wider screen */
@media (max-width: 700px) { .hdr-pill.ether { display: none; } }
/* What you have to spend, on the screen that spends it. The header drops the Ether chip on a
   phone and the Cosmetics tab prices everything in Ether, so without this you could not see
   your balance while shopping for the one thing it buys. */
.purse { display: flex; flex-wrap: wrap; gap: 8px 18px; margin: 0 0 14px; }
.purse > span { display: inline-flex; align-items: center; gap: 6px; font-size: .86rem; }
.purse b { color: var(--text); font-weight: 700; font-variant-numeric: lining-nums tabular-nums; }
.purse em { font-style: normal; color: var(--muted); }
.purse-ic { font-size: .95rem; color: var(--st-light); }
.purse-ic.credit { color: var(--amber); }
.purse-ic.ether { color: var(--violet); }
.ds-flag { background: none; border: 0; padding: 0; cursor: pointer; font: inherit; font-size: .68rem; text-decoration: underline; text-underline-offset: 3px; text-decoration-color: color-mix(in srgb, var(--st-light) 50%, transparent); }
.ds-flag:hover { color: var(--st-bright); }
details.audio-row { display: block; }
details.audio-row summary { display: flex; justify-content: space-between; align-items: center; gap: 12px; cursor: pointer; list-style: none; }
details.audio-row summary::-webkit-details-marker { display: none; }
details.audio-row summary > span:last-child { text-decoration: underline; text-underline-offset: 3px; }
.audio-diag { margin-top: 10px; padding: 10px 12px; background: var(--panel); border-radius: 0; font-size: .82rem; display: flex; flex-direction: column; gap: 6px; cursor: pointer; }
.audio-diag > div { display: flex; justify-content: space-between; gap: 12px; }
.audio-diag > div span { color: var(--muted); flex: 0 0 auto; }
.audio-diag > div b { font-weight: 500; text-align: right; word-break: break-word; }

/* ---- team builder (Collection). One tap adds, one tap removes, one tap swaps on a full team. ---- */
.team-bench { display: flex; flex-wrap: wrap; gap: 10px; align-items: stretch; margin-bottom: 16px; padding: 12px 14px 14px;
  background: linear-gradient(180deg, var(--panel2), var(--panel)); border-radius: 0;
  box-shadow: inset 0 1px 0 rgba(214, 224, 236, .1), inset 0 -1px 0 rgba(0, 0, 0, .6), 0 0 0 1px rgba(0, 0, 0, .55); }
.team-bench.over { box-shadow: inset 0 1px 0 rgba(214, 224, 236, .1), inset 0 -2px 0 var(--bl-body), 0 0 0 1px rgba(0, 0, 0, .55); }
.team-head { flex-basis: 100%; display: flex; justify-content: space-between; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.team-cost { font-size: .8rem; color: var(--muted); font-variant-numeric: lining-nums tabular-nums; }
.team-cost.over { color: var(--ink-accent); }
.team-bench .team-slot { flex: 1 1 220px; min-width: 0; display: flex; align-items: center; gap: 6px; padding: 5px 6px 5px 5px; border-radius: 0;
  background: var(--sunken); border: 1px solid var(--line); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--c, var(--st-body)) 22%, transparent); }
.team-bench .team-slot.lead { border-color: color-mix(in srgb, var(--st-light) 60%, var(--line)); }
.team-bench .team-slot.empty { justify-content: center; color: var(--muted); font-size: .82rem; border-style: dashed; padding: 14px 12px; box-shadow: none; }
.ts-open { flex: 1; min-width: 0; display: flex; align-items: center; gap: 9px; background: none; border: 0; padding: 0; color: inherit; font: inherit; text-align: left; cursor: pointer; border-radius: 0; }
.ts-open:hover .ts-name, .ts-open:focus-visible .ts-name { color: var(--st-bright); }
.ts-open img { width: 46px; height: 46px; flex: 0 0 auto; border-radius: 0; background: #0E1013; }
.ts-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.2; }
.ts-name { font-family: var(--display); font-size: .86rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ts-sub { font-size: .7rem; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ts-lead { font-size: .64rem; letter-spacing: .1em; text-transform: uppercase; color: var(--st-bright); padding: 3px 7px; border-radius: 0; background: rgba(217, 223, 230, .08); box-shadow: inset 0 0 0 1px var(--st-dark); flex: 0 0 auto; }
.ts-btn { flex: 0 0 auto; height: 28px; min-width: 28px; padding: 0 9px; border-radius: 0; border: 1px solid var(--line2); background: var(--panel3); color: var(--text-body); font: inherit; font-size: .72rem; letter-spacing: .04em; cursor: pointer; }
.ts-btn:hover, .ts-btn:focus-visible { color: var(--text); border-color: var(--st-body); }
.ts-x { font-size: 1.05rem; line-height: 1; padding: 0; width: 28px; }
.ts-x:hover, .ts-x:focus-visible { color: var(--ink-accent-hi); border-color: var(--st-body); }
.cwrap { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.team-tog { width: 100%; height: 30px; border-radius: 0; border: 1px solid var(--line2); background: var(--panel2); color: var(--text-body); font: inherit; font-size: .78rem; font-weight: 700; letter-spacing: .03em; cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(214, 224, 236, .08); transition: border-color var(--dur-fast), color var(--dur-fast), background var(--dur-fast); }
.team-tog:hover, .team-tog:focus-visible { border-color: var(--st-body); color: var(--text); }
.team-tog.on { background: linear-gradient(180deg, var(--st-dark), var(--st-shadow)); border-color: var(--st-body); color: var(--st-bright); }
.team-tog.on:hover, .team-tog.on:focus-visible { color: var(--ink-accent-hi); border-color: var(--st-body); }

/* ---- the team's own controls, in the same material as everything else (Dom, 2026-09-17) ----
   "The team buttons need to change." They were flat plates with a 1px line, which is what every
   button in the game looked like a week ago; with the rock and the steel surround on .btn, the team
   strip and the card toggles were the one place still wearing the old interface.
   Same stone, same frame, cut thinner: these are 28 and 30 pixels tall against a .btn's 44, and a
   4px surround on a 28px control is a third of the control. Two.
   `.ts-lead` is deliberately NOT in here. It is a span reading "Leads" on the first slot, a label
   for a state rather than a thing you press, and giving a label the material of a button is how you
   teach someone to click something that does nothing. */
.ts-btn, .team-tog {
  border: 2px solid transparent; border-radius: 0;
  border-image: linear-gradient(180deg, #6E7885 0%, #39414D 50%, #242B34 100%) 1;
  background-image: var(--ui-scrim, none), var(--ui-btn, none), linear-gradient(180deg, #1C212A, #2C3441);
  background-size: auto, cover, auto;
  background-position: center, center, center;
  background-clip: padding-box;
  color: var(--text); text-shadow: 0 1px 2px rgba(0, 0, 0, .85);
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .7), inset 0 -1px 0 rgba(217, 223, 230, .16),
              0 0 0 1px #0B0D10, 0 2px 6px rgba(0, 0, 0, .6);
  transition: background 90ms linear, box-shadow 90ms linear, transform 90ms linear, color 90ms linear; }
/* The ramp under the rock brightens, exactly as a .btn does. The material never changes on hover:
   a control that swaps what it is made of when you point at it reads as two different objects. */
.ts-btn, .team-tog { position: relative; }
.ts-btn::after, .team-tog::after {
  content: ''; position: absolute; inset: 0; pointer-events: none; opacity: 0;
  background: linear-gradient(0deg, rgba(232, 240, 250, .17) 0%, rgba(232, 240, 250, .05) 38%, rgba(232, 240, 250, 0) 72%);
  transition: opacity var(--dur-hover-out) var(--ease-standard); }
.ts-btn:hover::after, .ts-btn:focus-visible::after,
.team-tog:hover::after, .team-tog:focus-visible::after {
  opacity: 1; transition-duration: var(--dur-hover-in); }
.ts-btn:hover, .ts-btn:focus-visible, .team-tog:hover, .team-tog:focus-visible {
  color: var(--st-spec); }
.ts-btn:active, .team-tog:active { transform: translateY(1px);
  box-shadow: inset 0 3px 7px rgba(0, 0, 0, .85), 0 0 0 1px #0B0D10; }
/* On the team is the RED button (Dom, 2026-09-18: "if a titan is on a team, lets change it to the
   red button"). It was a seated dark plate, which said "pressed" but not "this one is in". The
   blood field is the game's one filled surface and it is doing the job the badge on the card used
   to do, in the place you would press to undo it. Same material as .btn.primary: the hot rock
   blended soft-light over the blood ramp, so the field keeps its color and takes the stone's
   light and shade. */
.team-tog.on {
  background-image: var(--ui-btn-primary, none), linear-gradient(180deg, #7E1420, #A61E2C);
  background-size: cover, auto; background-position: center, center;
  background-blend-mode: soft-light, normal;
  color: var(--st-spec);
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .6), inset 0 -1px 0 rgba(232, 105, 122, .3),
              0 0 0 1px #0B0D10, 0 2px 6px rgba(0, 0, 0, .6); }
/* A warm light on a warm field, the same one the primary button takes. */
.team-tog.on::after {
  background: linear-gradient(0deg, rgba(255, 228, 232, .2) 0%, rgba(255, 228, 232, .06) 38%, rgba(255, 228, 232, 0) 72%); }
.team-tog.on:hover, .team-tog.on:focus-visible {
  color: var(--st-spec);
  /* The field is already red, so hover says nothing about color: the light comes up and the ramp
     brightens a shade, exactly as the primary button does. */
  background-image: var(--ui-btn-primary, none), linear-gradient(180deg, #8A1622, #B42432);
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .6), inset 0 -1px 0 rgba(232, 105, 122, .38),
              0 0 0 1px #0B0D10, 0 2px 6px rgba(0, 0, 0, .6); }
/* The remove button says the same thing the same way. */
.ts-x:hover, .ts-x:focus-visible { color: var(--st-spec);
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .7), inset 0 0 0 1px var(--st-body), 0 0 0 1px #0B0D10; }
.swap-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }@media (max-width: 700px) {
  .team-bench { padding: 10px; gap: 8px; }
  .team-bench .team-slot { flex-basis: 100%; }
  .swap-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); }}

/* ---- the turn order lives in the split between the fighters ---- */
.arena .order { grid-column: 1 / -1; justify-self: center; align-self: center; max-width: 100%; min-height: 36px; height: auto; margin: 0; padding: 4px 8px 4px 12px; border-radius: 0;
  background: linear-gradient(180deg, rgba(20, 23, 28, .92), rgba(11, 13, 16, .92)); border: 1px solid var(--line2);
  box-shadow: inset 0 1px 0 rgba(214, 224, 236, .1), 0 8px 20px -10px rgba(0, 0, 0, .9); position: relative; z-index: 2; }
.arena .order::before, .arena .order::after { content: ''; position: absolute; top: 50%; width: 22vw; max-width: 260px; height: 1px; pointer-events: none;
  background: linear-gradient(90deg, transparent, var(--line2)); }
.arena .order::before { right: 100%; margin-right: 8px; }
.arena .order::after { left: 100%; margin-left: 8px; transform: scaleX(-1); }
.arena .order-chip { border-radius: 0; padding: 3px 9px; }
.arena .order-chip.now { box-shadow: 0 0 0 1px var(--st-body); }@media (max-width: 760px) {
  .arena .order { padding: 2px 6px 2px 8px; gap: 5px; }
  .arena .order::before, .arena .order::after { display: none; }
  .arena .order-label { display: none; }}

/* ---- round 5 ---- */
/* detail stat bars in one steel tone; the inline --c from statBar() loses to the important */
.stat .bar .tinted { --c: var(--st-light) !important; }
/* swap picker: the newcomer beside the three who could step down */
.swap-body { display: grid; grid-template-columns: minmax(140px, 180px) 1fr; gap: 18px; align-items: start; }
.swap-label { display: block; font-size: .7rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); margin-bottom: 8px; }
.swap-in .tcard { box-shadow: 0 0 0 1px var(--st-body), 0 0 22px -6px rgba(217, 223, 230, .5); }@media (max-width: 700px) { .swap-body { grid-template-columns: 1fr; } .swap-in .tcard { width: 160px; }}

/* the battle screen owns its height; the page under it must not scroll */
body.in-battle .main { padding-bottom: 12px; }
/* capture and result modals fit an 800px window */
.result-catch .tcard { width: 180px; margin: 0 auto 10px; }
.modal-card.result-win .modal-title { font-size: clamp(1.4rem, 3vw, 2.2rem); }
/* ---- modal footers on a phone ----
   One column: every action full width, the same size, the same gap between them. They used to be
   a two-column grid with the primary spanning, which left whatever was beside it stranded at half
   width, and the Titan detail's three actions are ALL `ghost` - and ghost strips the plate - so
   the bottom of that dialog was three links rather than three buttons (Dom, 2026-09-12).
   Ghost keeps its quiet type and its lighter weight here; what it gets back is a plate, because a
   control at the bottom of a phone dialog is a thing you press with a thumb and it has to look
   like one. Destructive confirms are left as two halves: see the override further down. */
@media (max-width: 700px) {
  .modal-actions { display: grid; grid-template-columns: 1fr; }
  /* The confirm is moved to the front of the DOM by modal() itself, so no `order` here: a
     CSS reorder would leave Tab and a screen reader reading the stack bottom to top. */
  .modal-actions .btn.primary { grid-column: 1 / -1; }
  .modal-actions .btn.ghost {
    padding: 0 18px; border: 4px solid transparent;
    border-image: linear-gradient(180deg, #6E7885 0%, #39414D 50%, #242B34 100%) 1;
    background: linear-gradient(180deg, #1C212A, #2C3441); background-clip: padding-box;
    box-shadow: inset 0 2px 5px rgba(0, 0, 0, .7), inset 0 -1px 0 rgba(217, 223, 230, .16),
                0 0 0 1px #0B0D10, 0 2px 6px rgba(0, 0, 0, .6); }
  .modal-actions .btn.ghost:hover:not(:disabled) {
    background: linear-gradient(180deg, #242B36, #36404E); box-shadow: inset 0 2px 5px rgba(0, 0, 0, .7),
                inset 0 -1px 0 rgba(217, 223, 230, .22), 0 0 0 1px #0B0D10, 0 2px 8px rgba(0, 0, 0, .6); }
}

/* ---- the beat before the card, tiered by what is coming. Common: three thin rings. Rare: a
   brighter ring and a low pulse of the ground. Epic and up: the screen edge flashes in the rarity
   color and the rings run heavier. Mythic: all of it, faster, and the text shakes. ---- */
.conj-swell.t2 .swell-ring { border-width: 3px; }
.conj-swell.t2::before { content: ''; position: absolute; inset: 20% 30%; border-radius: 50%; background: radial-gradient(circle, color-mix(in srgb, var(--c) 28%, transparent), transparent 70%); animation: pulse 1.1s ease-in-out infinite; }
.conj-swell.t3 .swell-ring, .conj-swell.t4 .swell-ring, .conj-swell.t5 .swell-ring { border-width: 3px; box-shadow: 0 0 18px -4px var(--c), inset 0 0 18px -6px var(--c); }
.conj-swell.t3::after, .conj-swell.t4::after, .conj-swell.t5::after { content: ''; position: fixed; inset: 0; pointer-events: none; z-index: 55;
  box-shadow: inset 0 0 140px -30px var(--c); animation: edgeFlash 1.1s ease-in-out infinite; }
.conj-swell.t4::after { box-shadow: inset 0 0 180px -20px var(--c); }
.conj-swell.t5::after { box-shadow: inset 0 0 240px 0 var(--c); animation-duration: .6s; }
.conj-swell.t5 .swell-text { animation: pulse .7s ease-in-out infinite, strain .12s linear infinite; color: var(--text); }
@keyframes edgeFlash { 0%, 100% { opacity: .15; } 50% { opacity: .85; } }
@keyframes strain { 0% { transform: translate(0, 0); } 25% { transform: translate(1px, -1px); } 50% { transform: translate(-1px, 1px); } 75% { transform: translate(1px, 1px); } 100% { transform: translate(0, 0); } }

/* ---- round 6 ---- */
.btn.brass.soon small { display: inline-block; margin-left: 6px; font-size: .7rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); font-weight: 400; }
.btn.brass.soon { opacity: .8; }@media (max-width: 760px) {
  .arena .order-list { overflow-x: auto; scrollbar-width: none; -webkit-mask-image: linear-gradient(90deg, #000 86%, transparent); mask-image: linear-gradient(90deg, #000 86%, transparent); padding-right: 24px; }
  .arena .order-list::-webkit-scrollbar { display: none; }
  .battle-top .field-tag, .battle-top .round { white-space: nowrap; }
  .battle-top .field-tag { font-size: .62rem; }}

/* the card column is short and the management column is long; the card follows the scroll so the
   left half of a tall detail modal is never an empty column under a rule */
.detail-art { position: sticky; top: 0; align-self: start; }@media (max-width: 900px) { .detail-art { position: static; }}
@media (max-width: 760px) {
  .battle-top .kind { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1 1 auto; min-width: 0; text-align: right; }
  .speed-btn { white-space: nowrap; }}

/* the action row of a tall modal stays reachable: it sticks to the bottom of the card */
.modal-card .modal-actions { position: static; margin: 14px -24px -24px; padding: 12px 24px 18px; background: var(--panel); border-top: 1px solid var(--line); }

/* ---- round 8 ---- */
/* the strip clips at .order, so the fade lives on .order */
@media (max-width: 760px) {
  .arena .order { overflow: hidden; -webkit-mask-image: linear-gradient(90deg, #000 84%, transparent); mask-image: linear-gradient(90deg, #000 84%, transparent); }
  .arena .order-list { -webkit-mask-image: none; mask-image: none; padding-right: 30px; }
  .ds-flag { padding: 8px 12px; min-height: 36px; border: 1px solid var(--line2); border-radius: 0; text-decoration: none; font-size: .74rem; }
}

/* ---- round 9 ---- */
.xp-rows { display: flex; flex-direction: column; gap: 8px; margin: 0 0 12px; }
.xp-row { position: relative; display: grid; grid-template-columns: minmax(80px, 1fr) 2fr auto; gap: 10px; align-items: center; font-size: .9rem; }
.xp-row .bar.thin i { transition: width 900ms cubic-bezier(.22, 1, .36, 1); }
.xp-row .xp-lvl { font-variant-numeric: lining-nums tabular-nums; color: var(--muted); }
.xp-row.up .xp-lvl { color: var(--st-bright); animation: lvlFlash 1.2s ease-out 500ms both; }
@keyframes lvlFlash { 0% { text-shadow: none; } 40% { text-shadow: 0 0 18px rgba(217, 223, 230, .9); color: #fff; } 100% { text-shadow: none; } }
.cost-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 4px 0 8px; }
.cost-chip { display: inline-flex; align-items: center; gap: 7px; padding: 3px 6px 3px 10px; border-radius: 0; border: 1px solid color-mix(in srgb, var(--c) 50%, transparent); color: var(--text-body); font-size: .76rem; }
.cost-chip i { font-style: normal; min-width: 18px; text-align: center; padding: 1px 6px; border-radius: 10px; background: color-mix(in srgb, var(--c) 30%, var(--panel3)); color: var(--text); font-weight: 700; }
/* Same story: written for the inside of the forms grid, now a line of its own under them. */
.conj-forms-note { margin: 0; text-align: center; max-width: 62ch; margin-inline: auto; }
/* Shape the form fits an 800px window without an inner scrollbar */
@media (max-height: 820px) {
  .conjure-title { font-size: clamp(1.3rem, 2.8vw, 1.9rem); margin-bottom: 2px; }
  .conjure-sub { font-size: .84rem; margin-bottom: 8px; }
  .conj-h { margin: 8px 0 4px; }
  .conj-opt { padding: 8px 10px; gap: 2px; }
  .conj-opt span { font-size: .74rem; line-height: 1.3; }
}@media (max-width: 760px) {
  .action-row { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 6px; }
  .action-row .btn { padding: 6px 4px; font-size: 11px; letter-spacing: .04em; min-width: 0; }
  .action-row .btn small { font-size: 9px; }}

/* the box reveal borrows the conjure swell; the box spins in the middle of the rings */
.reveal .conj-swell { min-height: 220px; width: 100%; }
.reveal .conj-swell .reveal-box { position: relative; z-index: 1; }

/* ---- round 10 ---- */
/* 2: a 1280x800 desktop showed 421px of a 540px arena and your own panel sat below the fold */
@media (min-width: 761px) and (max-height: 860px) {
  .arena { padding: 12px 16px; gap: 8px; }
  /* `min-height: min-content` used to live here. It was the right answer to the OLD desktop
     layout, where the arena held two stacked info-sprite-bench rows whose own content overflowed
     a short window however small the creature was drawn, so the band was made unsqueezable and
     the page allowed to scroll. Under one shared layout it is the wrong answer and an active bug:
     the band is the only flexible row, fitSprites() sizes the creature to whatever the band comes
     out at, and refusing to squeeze pushed the arena to 426px in a 608px column at 1280x700 and
     put Switch, Wait, Net and Run 92px below the fold (measured 2026-09-17). The arena shrinks
     again; the creature follows it down.
     The `.fighter.side-*` column templates that were also here went with the old layout: the box
     they sized is dissolved by `display: contents` at every width now. */
  .fighter-info { padding: 6px 11px; gap: 3px; }
  .fighter-name { font-size: 1rem; }
  .arena .order { min-height: 32px; padding: 3px 8px 3px 12px; }
  .battle-log { min-height: 60px; max-height: 72px; }   /* gives the band back what it needs */
}
/* 7: the strip fades only when it actually clips */
.arena .order.clipped { -webkit-mask-image: linear-gradient(90deg, #000 88%, transparent); mask-image: linear-gradient(90deg, #000 88%, transparent); }
/* 8, 9: a lone card or a matrix sits centered in a wide modal */
.modal-body .grid { grid-template-columns: repeat(auto-fit, minmax(150px, 220px)); justify-content: center; }
.ec-grid { width: fit-content; max-width: 100%; margin: 0 auto; }
.ec-cover { max-width: 720px; margin: 0 auto; }
/* 10: last matches */
.history { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 12px; }
.history-label { font-size: .68rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); margin-right: 4px; }
.hist { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px 4px 6px; border-radius: 0; border: 1px solid var(--line2); background: var(--panel2); font-size: .8rem; color: var(--text-body); font-variant-numeric: lining-nums tabular-nums; }
.hist b { width: 18px; height: 18px; border-radius: 50%; display: grid; place-items: center; font-size: .68rem; color: var(--ink); background: var(--st-light); }
.hist.lost b { background: var(--bl-light); }
.hist i { font-style: normal; color: var(--muted); }
.hist.won i { color: var(--st-bright); }
.rule-more { margin-top: 8px; }
.rule-more summary { cursor: pointer; color: var(--st-light); font-size: .84rem; text-decoration: underline; text-underline-offset: 3px; list-style: none; }
.rule-more summary::-webkit-details-marker { display: none; }
.rule-more[open] summary { margin-bottom: 10px; }
/* the result modal fits an 800px window: the last XP row was 6px under the sticky footer */
.modal-body { padding-bottom: 8px; }
.modal-card.result-win .result-catch .tcard, .modal-card.result-loss .result-catch .tcard { width: 160px; margin-bottom: 8px; }
.modal-card.result-win .rank-strip, .modal-card.result-loss .rank-strip { padding: 8px 12px; margin-bottom: 8px; }
.xp-rows { margin-bottom: 6px; }
/* ---- the detail modal's rows of choices ----
   Skills, spare copies and skins were all content-sized in a wrapping row, so a column of them was
   every width at once: Strike beside Quake, Lumen alone underneath at half the row, then five skin
   buttons in a 2-2-1 stagger (Dom, 2026-09-12). Two across, every cell the same size, and an ODD
   count fills the row it is on so nothing is ever left stranded at half width. Same rule the
   battle skill grid uses, for the same reason. */
.modal-card .pills { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.modal-card .pills:not(:has(> :nth-child(2))) { grid-template-columns: 1fr; }
.modal-card .pills:has(> :nth-child(3)):not(:has(> :nth-child(4))) > :last-child,
.modal-card .pills:has(> :nth-child(5)):not(:has(> :nth-child(6))) > :last-child,
.modal-card .pills:has(> :nth-child(7)):not(:has(> :nth-child(8))) > :last-child { grid-column: 1 / -1; }
.modal-card .pills > .skill, .modal-card .pills > .btn, .modal-card .pills > .learn { width: 100%; min-width: 0; }
.modal-card .pills > .btn { min-height: 44px; }
.modal-card .pills > .btn.small { height: auto; min-height: 44px; }
/* A skill carries two lines, so its own height is 54 and everything in a skill row matches it
   rather than the 44 a one-line control needs. */
.modal-card .pills > .skill, .modal-card .learn > .skill { min-height: 54px; }
.modal-card .learn { flex-direction: row; align-items: stretch; gap: 8px; }
/* A floor on the skill itself: beside a long effect line it was squeezed to 81px and "Shard Rain"
   broke over two lines (blind playtest round 2, the market listing popup). */
.modal-card .learn > .skill { flex: 1 1 auto; min-width: min(170px, 48%); }
/* Both halves are told the one height. Left to stretch they disagreed by 8px, because the plate's
   own border sits outside the height the button was given and the skill's does not. */
.modal-card .learn > .btn { flex: 0 0 92px; height: 54px; min-height: 0; }

/* ---- round 11 ---- */
/* 6: the coach takes its own row, so the log and the buttons never move when it closes */
.battle-hud .coach { grid-column: 1 / -1; }
/* 9: the Ether tab keeps blood for the flags and the primary only */
h2.sec, .sec { color: var(--st-light); }
/* 10: thumb-sized team controls on phones */
@media (max-width: 768px) {
  .ts-btn, .ts-x { height: 40px; min-width: 40px; }
  .team-tog { height: 40px; }
}
/* B7: no scrollbar for one pixel */
.modal-card.result-win, .modal-card.result-loss { max-height: 96vh; }

/* ---- round 12 ---- */
/* 1: the reveal glow pseudo-elements extend past the card; they must not grow a scrollbar */
.modal-card.reveal-modal { overflow-x: hidden; }
.reveal, .conj-reveal { overflow: visible; }
.modal-card.reveal-modal .modal-body { overflow: hidden; }
/* 4, 5: who it was */
/* Was margin-top -6px, to sit tight under the title. It is PREPENDED into the result body, so
   that pull always landed above the scroll container's origin and sliced the top off the line:
   "Over Torque - 1,034 rating" lost the tops of its capitals on every single result screen. */
.result-sub { margin: 0 0 10px; text-align: center; color: var(--muted); font-size: .9rem; }
/* Was 140px so a third card would peek. A card's face scales its type from its own width, and
   at 140px the type line under the portrait fell to 5.8px and was held at a 7px floor, which
   is smaller than anything else in the game (round 5 review). 190px is the width at which
   that line reaches 8px on its own. Two cards and a cut third still say "keep going", and the
   shelf snaps, so nothing is lost but the peek. */
@media (max-width: 760px) { .hero-team > * { flex: 0 0 190px; } }

/* ---- round 12b: nothing in a reveal may grow a scrollbar ---- */
.modal-card.reveal-modal .modal-body { overflow: hidden; }
.conjure-inner { overflow-x: hidden; }

/* ---- battery (Dom's phone ran hot, 2026-09-10) ---- */
/* the fixed grain layer with a blend mode forced a full-screen composite on every frame on phones */
@media (max-width: 760px) { body::after { display: none; } }@media (max-width: 760px) {
  .conj-swell.t3::after, .conj-swell.t4::after, .conj-swell.t5::after { box-shadow: inset 0 0 60px -10px var(--c); }
  .conj-swell.t5 .swell-text { animation: pulse .7s ease-in-out infinite; }}

/* ---- conjure on a phone: the action bar is solid, and the last row of forms scrolls clear of it ---- */
@media (max-width: 760px) {
  .conj-actions { padding: 10px 0 12px; }
  .conj-actions.inline { position: static; padding: 0; }
  .conj-forms { padding-bottom: 8px; }
  .conjure-inner > .conj-actions:last-child { margin-top: 8px; }
  .conj-describe + .conj-forms-note, .conj-forms { scroll-margin-bottom: 120px; }
}

/* ---- the three forms: staggered arrival and a walking highlight until one is picked ---- */
/* A form coming through. rvRise (fade up eight pixels) is what a list row does when it loads;
   this is a thing arriving out of the void, so it comes through small and overbright, overshoots,
   and settles. It runs against a particle break fired at the same card, so the first 12% is
   spent hidden inside the flash. */
.conj-form.rv-form { animation: conjForm 560ms cubic-bezier(.22, 1.2, .36, 1) both; }
@keyframes conjForm {
  0%   { opacity: 0; transform: scale(.42) translateY(14px); filter: brightness(2.6) saturate(.5); }
  12%  { opacity: 1; filter: brightness(2.2) saturate(.7); }
  62%  { transform: scale(1.06) translateY(-3px); filter: brightness(1.14) saturate(1); }
  100% { opacity: 1; transform: none; filter: none; }
}
.conj-form.lit { border-color: var(--st-body); box-shadow: 0 0 24px -6px rgba(217, 223, 230, .45); color: var(--text); }
.conj-form.lit img { filter: brightness(1.12); }
.conj-form { transition: transform .15s, border-color .3s, box-shadow .3s, color .3s; }
.conj-form.on { border-color: var(--ink-accent); box-shadow: 0 0 0 2px var(--bl-light) inset, 0 0 26px -6px rgba(232, 105, 122, .5); color: var(--text); }@media (prefers-reduced-motion: reduce) { .conj-form.rv-form { animation: none; }}

/* the panel is a flex column: nothing in it may shrink, or the forms grid overflows under the
   action bar (Dom's phone screenshot, 2026-09-10: Back / Name it sitting on top of Form 3) */
.conjure-inner > * { flex-shrink: 0; }

/* ---- round 13 ---- */
@media (max-width: 760px) {
  .conj-slide { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
  .conj-form { padding: 6px; }
  .conj-form img { max-width: 100%; }
  .conj-form span { font-size: .74rem; }
}
/* 4: two big bundles on the first row, the three token bundles on the second */
@media (min-width: 900px) {
  .skus { grid-template-columns: repeat(6, 1fr); align-items: start; }
  .sku { grid-column: span 2; }
  .sku.kind-bundle, .sku.kind-pass { grid-column: span 3; }
}
.sku-art-cap { display: block; font-size: .7rem; color: var(--muted); margin-top: 6px; text-align: center; }
/* 10: cosmetics in sections */
.cos-sec { font-family: var(--display); font-size: .82rem; letter-spacing: .12em; text-transform: uppercase; color: var(--st-light); margin: 18px 0 8px; }
.cos-sec:first-child { margin-top: 0; }
.packs { align-items: start; }
/* 11: three buttons share one row on a phone */
@media (max-width: 700px) {
  .modal-actions .btn { min-width: 0; }
}
/* 12: More sheet close spans the row */
.more-grid + .modal-actions .btn, .modal-card:has(.more-grid) .modal-actions { display: flex; }
.modal-card:has(.more-grid) .modal-actions .btn { flex: 1; }

/* ---- round 14 ---- */
/* 2: phone modal bodies keep their last glyph clear of the overlay scrollbar */
@media (max-width: 760px) { .modal-body { padding-right: 10px; scrollbar-gutter: stable; } }
/* 3: an empty strip is no strip */
.arena .order[hidden] { display: none; }
/* 8: Rewards keeps one blood fill */
.streak-pip.now { border-color: var(--st-light); box-shadow: 0 0 14px rgba(217, 223, 230, .18); }
.season-prize b, .pill.gold { color: var(--st-light); }
.pill.gold { border-color: var(--st-body); }
/* 9: desktop Credits tab: boxes beside the reference prices */
@media (min-width: 1100px) {
  .shop-credits { display: grid; grid-template-columns: 1fr 360px; gap: 18px 24px; align-items: start; }
  .shop-credits > .boxes { grid-column: 1; }
  .shop-credits > :not(.boxes) { grid-column: 2; }
  .shop-credits > h2.sec, .shop-credits > .sec { grid-column: 1 / -1; }
}
/* 10: a finished coach keeps its slot on desktop so nothing below it moves */
.coach.done { visibility: hidden; }
/* 11: cosmetic kickers */
.pack-amt.kicker { font-size: .66rem; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
/* 12: short phones: the last row of forms scrolls clear of the sticky bar */
@media (max-height: 700px) and (max-width: 760px) { .conj-forms { padding-bottom: 110px; } }

/* ---- round 15 ---- */
/* 1: the crate */
/* A gate is lit from inside, so the art carries its own glow as well as a drop shadow. */
/* The painted Gate. It wears the vector's own two class names so `gateBreathe`, `gateOpen` and
   the hover scale all reach it untouched; what changes is that the core is an image and the glow
   is a wash behind it rather than an SVG blur. Sized to the 120x96 the drawing occupied, so
   nothing around it moves when a build gains its first generated gate. */
.gate-art.painted { position: relative; }
/* 120x96 was the vector's box and a vector does not care. A painting does: the plates come back
   around 800px and were being thrown away down to 120, which is most of why Dom called them
   pixelated. The box has the room. */
.gate-art.painted .gate-core { display: block; width: 168px; height: 134px; object-fit: contain;
  transform-origin: 50% 80%; }
@media (min-width: 761px) { .gate-art.painted .gate-core { width: 208px; height: 166px; } }
.gate-art.painted .gate-glow { position: absolute; left: 50%; top: 58%; width: 130px; height: 96px;
  transform: translate(-50%, -50%); transform-origin: 50% 50%; pointer-events: none;
  background: radial-gradient(50% 50% at 50% 50%, color-mix(in srgb, var(--c) 85%, transparent) 0%, transparent 72%);
  filter: blur(9px); }
.gate-art { display: flex; justify-content: center; margin: -4px 0 6px;
  filter: drop-shadow(0 10px 14px rgba(0, 0, 0, .55)) drop-shadow(0 0 10px color-mix(in srgb, var(--c) 22%, transparent)); }
/* A crate opened by hinging a lid off it. A gate has no lid: what opens is the Void in the
   opening, so hovering widens the aperture rather than lifting anything. */
.gate-art .gate-core { transition: transform .3s var(--ease-standard), opacity .3s var(--ease-standard);
  transform-origin: 60px 63px; }
.box:hover .gate-art .gate-core { transform: scaleX(1.08) scaleY(1.03); opacity: .92; }
/* A gate that is standing open is not a still image. The floor spill breathes so the card has one
   slow moving thing on it; the Void behind the arch is deliberately NOT animated, because a
   shifting aperture at this size reads as a rendering fault rather than as atmosphere. */
.gate-art .gate-glow { animation: gateBreathe 5.5s ease-in-out infinite; transform-origin: 60px 86px; }
@keyframes gateBreathe {
  0%, 100% { opacity: var(--gs, .2); transform: scaleX(1); }
  50%      { opacity: calc(var(--gs, .2) * 1.7); transform: scaleX(1.12); } }
@media (prefers-reduced-motion: reduce) { .gate-art .gate-glow { animation: none; } }
/* 2: the reveal darkens the room and pulses the rarity where the card will land */
.modal:has(.modal-card.reveal-modal) { background: rgba(4, 5, 7, .86); }
.reveal .conj-swell::before { content: ''; position: absolute; inset: 22% 30%; border-radius: 50%; pointer-events: none;
  background: radial-gradient(circle, color-mix(in srgb, var(--c) 30%, transparent), transparent 70%); animation: pulse 1s ease-in-out infinite; }
/* On a phone the card comes FIRST and small. It was moved below the management UI to get the
   Level up button above the fold, which worked until the card became a full 5:7 face: it then sat
   1,400px down a 600px panel and you could open a Titan without ever seeing its card. At 190px the
   card is 266 tall, so it and the primary action both land in the first screen. */
@media (max-width: 900px) {
  .detail { display: flex; flex-direction: column; gap: 14px; }
  .detail-art { order: 1; }
  .detail-main { order: 2; }
  /* .detail-art and .pedestal are given an explicit full width first: .pedestal sizes to its
     content, so a percentage width on the card alone resolved against a parent whose width
     depended on the card, and collapsed it to zero */
  .detail-art, .detail-art .pedestal { width: 100%; }
  .detail-art .tcard { width: min(240px, 66%); margin: 0 auto; }
  .detail-art .pedestal { padding: 0; background: none; box-shadow: none; }
}
/* 4: a quiet sell button */
.btn.ghost.sell { color: var(--ink-accent); }
.btn.ghost.sell:hover { border-color: var(--st-body); }
/* 6: the fallen opponent beside the ledger */
/* =================================================================================================
   THE RESULT SCREEN (Dom, 2026-09-12)
   Winning was a card sitting intact beside the word "Fell", four numbers that moved at the same
   instant, and a level showing up as a color change on a line of text. Three things happen now:
   the one you beat takes the cut, the ledger tallies row by row, and a level is struck over the bar
   that earned it.
   ============================================================================================== */
.result-foe { display: flex; align-items: center; gap: 12px; margin: 0 0 10px; }
.result-foe .tcard { width: 120px; flex: 0 0 auto; }

/* The cut: one bright line travelling across the card and gone, before the color goes out of it. */
.foe-slash { position: absolute; inset: -10% -18%; z-index: 5; pointer-events: none; opacity: 0;
  background: linear-gradient(74deg, transparent 43%, rgba(246, 248, 250, .95) 49%,
              rgba(240, 121, 139, .9) 51.5%, transparent 58%);
  animation: foeSlash 520ms cubic-bezier(.2, 0, .1, 1) 140ms both; }
@keyframes foeSlash {
  0%   { opacity: 0; transform: translate(-55%, 38%); }
  28%  { opacity: 1; }
  100% { opacity: 0; transform: translate(55%, -38%); } }
/* Then it goes out: a flinch on the hit, and it settles grayed and leaning. `both` holds the last
   frame, so the card STAYS fallen rather than springing back up when the animation ends. */
.result-foe.fell .foe-card { animation: foeFell 900ms cubic-bezier(.3, 0, .2, 1) 140ms both; }
@keyframes foeFell {
  0%   { transform: none; filter: none; }
  14%  { transform: translateX(-3px) rotate(-1.5deg); filter: brightness(1.7) saturate(.35); }
  100% { transform: translateY(7px) rotate(3.5deg); filter: grayscale(.85) brightness(.5); } }
/* The tally: each row arrives in turn and its figure flashes as it lands.
   The delay is handed in now rather than derived from --i alone: the ledger waits for the word on
   the card to land and tear away, so the takings are read after the outcome instead of underneath
   it. --delay falls back to the old formula for any row built without one. */
.result-grid .rg-k, .result-grid .rg-v {
  animation: rgRow 320ms var(--ease-standard) var(--delay, calc(260ms + var(--i, 0) * 230ms)) both; }
@keyframes rgRow { 0% { opacity: 0; transform: translateY(5px); } 100% { opacity: 1; transform: none; } }
.result-grid .rg-v.landed { animation: rgLand 420ms var(--ease-standard) both; }
@keyframes rgLand {
  0%   { text-shadow: 0 0 16px rgba(255, 197, 61, .85); transform: scale(1.16); }
  100% { text-shadow: none; transform: scale(1); } }

/* A level lands ON the bar that earned it, as the bar finishes filling. */
.xp-row .xp-burst { position: absolute; inset: -12px -8px; z-index: 1; pointer-events: none; opacity: 0;
  background: radial-gradient(closest-side, rgba(255, 197, 61, .38), transparent 74%);
  /* `backwards`, not `both`: held at its last frame the invisible burst stayed scaled to 1.3 and
     gave the result popup a horizontal scrollbar after every level-up (blind playtest round 2). */
  animation: xpBurst 1100ms ease-out var(--xp-delay, 820ms) backwards; }
@keyframes xpBurst {
  0%   { opacity: 0; transform: scale(.5); }
  24%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.3); } }
.xp-row .xp-up { position: absolute; left: 50%; top: 50%; z-index: 2; pointer-events: none;
  font-family: var(--display); font-weight: 700; letter-spacing: .18em;
  text-transform: uppercase; white-space: nowrap; padding: 3px 12px;
  /* The same yellow and white border as the level-up shout on the card (Dom, 2026-09-23), on no
     plate: a gold word on a gold pill would not read. */
  font-size: .9rem; color: #FFD23F; -webkit-text-stroke: 3px #FFFFFF; paint-order: stroke fill;
  text-shadow: 0 2px 6px rgba(0, 0, 0, .7), 0 0 16px rgba(255, 210, 63, .6);
  animation: xpUp 2000ms cubic-bezier(.2, 1, .3, 1) var(--xp-delay, 820ms) both; }
@keyframes xpUp {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(.55); }
  12%  { opacity: 1; transform: translate(-50%, -50%) scale(1.07); }
  22%  { transform: translate(-50%, -50%) scale(1); }
  70%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -165%) scale(.94); } }

@media (prefers-reduced-motion: reduce) {
  .foe-slash, .xp-row .xp-burst, .xp-row .xp-up { animation: none; }
  .xp-row .xp-up { opacity: 1; transform: translate(-50%, -50%); }
  .result-foe.fell .foe-card { animation: none; filter: grayscale(.85) brightness(.5); }
  .result-grid .rg-k, .result-grid .rg-v { animation: none; } }

.modal-card.result-win .result-foe .foe-card .tcard { filter: grayscale(.6) brightness(.8); }
/* 7: board preview */
.lb-preview { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 12px; }
/* 9: phone hero: the primary spans the row */
/* Hunt spans the row; the two under it are halves of it. They used to be content-sized in a
   wrapping flex row, so Ranked match came out at 192px and Edit team at 153 and the pair sat
   lopsided under a full-width primary (Dom, 2026-09-12). */
@media (max-width: 760px) {
  .hero-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
  /* Half of 366 is 161, and "Ranked match" at the system's .1em tracking wants 170 of it, so the
     two secondaries tighten rather than break onto two lines under a one-line primary. */
  .hero-actions .btn { width: 100%; min-width: 0; padding: 0 8px; letter-spacing: .05em; }
  .hero-actions .btn.primary { letter-spacing: .1em; }
  .hero-actions .btn.primary { grid-column: 1 / -1; }
}
/* 10: phone arena: the enemy panel hugs the strip */
@media (max-width: 760px) { .fighter.side-1 { align-self: end; } }
/* 11: the rule disclosure says it opens */
.rule-more summary::before { content: '\25B8'; display: inline-block; margin-right: 6px; transition: transform .2s; }
.rule-more[open] summary::before { transform: rotate(90deg); }

/* ---- round 16 ---- */
/* 3: the crate opens where the card will land */
.reveal-gate { position: relative; z-index: 1; animation: gateRise 900ms cubic-bezier(.22, 1, .36, 1) both; }
.reveal-gate .gate-art { margin: 0; transform: scale(1.35); }
/* The gate opening: the Void in the arch swells, floods the opening, then blows past the stone
   just as the card lands. The masonry never moves, which is the point of a gate. */
.reveal-gate .gate-core { animation: gateOpen 900ms cubic-bezier(.22, 1, .36, 1) both;
  transform-origin: 60px 63px; transform-box: fill-box; }
@keyframes gateOpen {
  0%   { transform: scale(1); opacity: .85; }
  45%  { transform: scale(1.1, 1.04); opacity: 1; }
  100% { transform: scale(2.6, 1.5); opacity: 0; } }

/* THE PAINTED GATE OPENS FROM A BIG RASTER, NOT UP FROM A SMALL ONE (Dom, 2026-09-19: the gates
   look pixelated, and specifically "when a box opens").
   `gateOpen` takes the gate to 2.6 times its width. That is free for the vector it was written
   for and ruinous for a photograph: WebKit rasterises a transformed layer once at its
   untransformed size and stretches the result, so the gate went soft exactly at the moment you
   are looking straight at it. Nothing about the motion changes here. The image is LAID OUT at
   the size the animation ends on and shown scaled down, so every frame is a downscale of a
   540px raster instead of an upscale of a 208px one.
   The numbers are the same numbers: 540 x 0.385 is the 208 it rests at, 540 x 1 is the 2.6 it
   ends on, and 430 x 0.577 is that end height. It is absolutely positioned so the oversized box
   overflows its slot instead of pushing the modal open. */
.reveal-gate .gate-art.painted { width: 208px; height: 166px; }
.reveal-gate .gate-art.painted .gate-core {
  position: absolute; left: 50%; top: 50%; width: 540px; height: 430px;
  transform-origin: 50% 62%; transform-box: view-box;
  animation-name: gateOpenPainted; }
@keyframes gateOpenPainted {
  0%   { transform: translate(-50%, -50%) scale(0.385); opacity: .85; }
  45%  { transform: translate(-50%, -50%) scale(0.4235, 0.4004); opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(1, 0.577); opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .reveal-gate .gate-art.painted .gate-core { animation: none; transform: translate(-50%, -50%) scale(0.385); opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .reveal-gate .gate-core { animation: none; opacity: 1; }
  .box:hover .gate-art .gate-core { transform: none; } }
@keyframes gateRise { 0% { transform: scale(.86); opacity: 0; } 30% { opacity: 1; } 100% { transform: scale(1); opacity: 1; } }
/* 5: the home page uses its width instead of ending at 460px of 800 */
@media (min-width: 1100px) {
  .home-cols { display: grid; grid-template-columns: 1.4fr 1fr; gap: 16px; align-items: start; }
  .home-cols > .todo { grid-column: 1 / -1; }
}
/* 6: the fallen card and the ledger share the row */
@media (min-width: 560px) {
  .modal-card.result-win .result, .modal-card.result-loss .result { display: grid; grid-template-columns: auto 1fr; gap: 0 18px; align-items: start; }
  .result-foe { grid-row: span 3; flex-direction: column; margin: 0; }
  .result > .rank-strip, .result > .result-grid, .result > .xp-rows, .result > .result-rows, .result > .result-sub { grid-column: 2; }
  .result-catch { grid-row: span 3; }
}
/* 7: five tiles center their last row instead of orphaning one */
@media (max-width: 760px) {
  .conj-grid { display: flex; flex-wrap: wrap; justify-content: center; }
  .conj-grid > * { flex: 1 1 calc(33.333% - 8px); min-width: 96px; }
  .rungs { justify-content: center; }
  /* The element rows are not the build tiles. Thirds are right for five tiles that are a name
     and a symbol; the element list is a stack that opens, so its rows take the width. This has to
     sit here rather than with the other element rules 1,500 lines up, because the rule above it is
     later in the file and sets the flex basis these rows would otherwise inherit. */
  .conj-grid.els > * { flex: 0 0 auto; min-width: 0; }
}
/* 9: a toast clears the battle's own header row */
.screen.battle.active ~ * .toasts, body.in-battle .toasts { top: calc(var(--hdr-h) + 58px); }
/* 12: the arena starts right under the header on phones */
@media (max-width: 760px) { .arena { padding-top: 4px; } .battle-top { margin-bottom: 2px; } }

/* ---- round 17 ---- */
/* 1: the loser's name was clipped at six pixels on every result screen */
.result-foe .stage-card .tcard { width: 146px; }
/* 3: on a phone the caption sits under the card, not beside it */
@media (max-width: 560px) {
  .result-foe { flex-direction: column; align-items: center; gap: 8px; }

  .modal-actions .btn { width: 100%; }
  /* A destructive confirm stays two halves. It is exactly two buttons, the labels are short, and
     side by side is how a yes/no question reads; stacking them would put the irreversible one
     under your thumb. */
  .modal-actions:has(> :nth-child(2)):not(:has(> :nth-child(3))):not(:has(> .btn.primary)) { grid-template-columns: 1fr 1fr; }
}
/* 5: the season track says it scrolls */
.track-wrap { position: relative; }
.track-wrap .season-track { -webkit-mask-image: linear-gradient(90deg, transparent, #000 18px, #000 calc(100% - 26px), transparent);
  mask-image: linear-gradient(90deg, transparent, #000 18px, #000 calc(100% - 26px), transparent); scrollbar-width: thin; scroll-behavior: smooth; }
.track-arrow { position: absolute; top: 50%; transform: translateY(-50%); z-index: 2; width: 28px; height: 44px; padding: 0;
  border: 1px solid var(--line2); border-radius: 0; background: linear-gradient(180deg, var(--panel3), var(--panel2));
  color: var(--st-light); font-size: 1rem; line-height: 1; cursor: pointer; box-shadow: 0 6px 14px -8px rgba(0, 0, 0, .9); }
.track-arrow:hover { color: var(--st-bright); border-color: var(--st-body); }
.track-arrow.left { left: -6px; } .track-arrow.right { right: -6px; }@media (max-width: 700px) { .track-arrow { display: none; }}

/* 7: six tiles, two columns, no orphan */
@media (max-width: 760px) {
  .home-stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
}
/* 9: the player's bench fits inside the arena on a phone */
@media (max-width: 480px) {
  .battle-log { height: 58px; min-height: 58px; max-height: 58px; }
  .bench-slot { width: 26px; }
}
/* 12: the strategy key is worth the width it has */
@media (min-width: 900px) {
  .ec-head .ec-h { width: 62px; height: 28px; }
  .ec-cell { width: 62px; height: 38px; flex: 0 0 62px; font-size: .8rem; }
  .ec-h { font-size: .76rem; }
  .ec-side, .ec-corner { width: 104px; flex: 0 0 104px; }
}
/* an input that is being asked for says so where the input is */
.conj-input.bad, .input.bad { box-shadow: inset 0 1px 0 rgba(0,0,0,.9), inset 0 2px 4px rgba(0,0,0,.55), inset 0 0 0 1px var(--bl-body), 0 0 18px -8px var(--bl-body); }
.conj-input.bad::placeholder { color: var(--ink-accent); }

/* =================================================================================================
   THE CARD: "Inverted Shield", from the Titanus design handoff (2026-09-10)
   -------------------------------------------------------------------------------------------------
   Every measurement below is the handoff's own pixel number against a 300px-wide card. `--u` is one
   of those pixels in container units, so calc(18 * var(--u)) is literally the doc's 18px and one
   implementation covers the 150px thumbnail, the 218px card and the 340px detail with no
   size-specific arithmetic. Geometry is identical across all five rarities: the ladder is carried
   by material and layer count only, which the handoff flags as the thing that must not drift.
   ================================================================================================= */
.tcard { display: flex; flex-direction: column; gap: 8px; width: 100%; background: none; border: 0; padding: 0; box-shadow: none; border-radius: 0; }
.tc-face { container-type: inline-size; --u: .3333cqw;
  position: relative; width: 100%; aspect-ratio: 300 / 420; overflow: hidden;
  background: var(--ink); font-family: var(--font);
  box-shadow: inset 0 0 0 1px #2A313B; }

/* the portrait sits behind everything, lit by its own element */
/* The art box stays full-bleed so its glow and vignette light the whole face, but the CREATURE is
   fitted to the clear band between the crest point and the sill. Full-bleed cover put every head
   behind the escutcheon. */
.tc-art { position: absolute; inset: 0; }
/* Full bleed across the card, as the handoff has it, but dropped 100px so the head clears the
   crest point: the portraits are square and centered, so at inset 0 every face sat behind the
   escutcheon. The band above is under the crest and the scrim anyway, and the feet run behind the
   sill, which is how the handoff's own cards sit. */
.tc-art img, .tc-art canvas, .tc-art svg {
  position: absolute; left: calc(-15 * var(--u)); top: calc(36 * var(--u)); width: calc(330 * var(--u)); height: auto;
  aspect-ratio: 1; object-fit: cover; display: block;
  /* A 40u FADE (Dom, 2026-09-22: "Lets increase the fade between the sections"). Over 10u the art
     rose beside the plate as a line. It starts at 36u now and is solid by 76u, so under the plate
     nothing changes and beside it the picture rises out of the dark. A 300u square starting at 36u
     would stop 29u short of the sill, so it is drawn 330u and centered, which crops 15u (4.5%) off
     each side: 36 + 330 = 366 against a sill at 365. */
  /* THE FADE HAPPENS BEHIND THE PLATE (Dom, 2026-09-22: "is that from within the image or is the
     card cutting it off"). It was the card. When the shield went square and stopped at 76u, the
     60u fade below and the scrim to 120u were left out in the open: the top fifth of every
     painting faded out under a dark band, which took the whole arch of branches off Barkscale and
     read as a crop. The picture now starts 10u higher and is solid by 76u, so the plate's edge is
     the top of the picture, and it still reaches the sill (66 + 300 = 366 against a sill at 365).
     The notes below are the history of the fade's SHAPE, which is kept: the same eased stops,
     squeezed into the first 10u.
     the art's own top edge was a hard line across the card; it emerges out of the black instead,
     with the fade finishing just below the crest point.
     76 and 12%, from 92 and 24% (Dom, 2026-09-18: "can we shorten the mask and move them up?").
     A square portrait in a square box is never cropped left or right, so what was hiding the
     creature was these two numbers: a quarter of it faded at the top and a tenth of it behind the
     sill at the bottom. Two thirds of the picture was fully visible; it is 84% now.
     The pair has to move together. Shortening the mask alone leaves the fade ending 60 units past
     the crest point, which is a hard edge across open card; raising the image alone pushes more of
     it under a fade that is too long. At 76 and 12% the fade finishes at 112u against a crest
     point at 104u, which is what the line above always claimed and never did. */
  /* EASED, not linear (Dom, 2026-09-18: "there is a weird black line across two cards"). A two
     stop fade is opaque at 12% and flat after it, and that is a sudden change of slope: over a
     smooth sky the eye reads a slope discontinuity as an edge, which is a Mach band, and it drew a
     line straight across the card. At 24% the same kink existed and was spread over twice the
     distance, usually landing in busier paint, so it never showed.
     The stops below rise fast and then ease: 85% opaque by a twelfth of the way down, so the
     creature is out of the dark as early as the short fade intended, and the last 15% spread over
     another twelfth so there is no step anywhere for the eye to catch. */
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, .55) 2.4%, rgba(0, 0, 0, .85) 4.8%, rgba(0, 0, 0, .97) 7.9%, #000 12.1%);
          mask-image: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, .55) 2.4%, rgba(0, 0, 0, .85) 4.8%, rgba(0, 0, 0, .97) 7.9%, #000 12.1%); }
/* A CUTOUT is not a painting and must not be laid out like one.
   A painting is 1024x1024, so `aspect-ratio: 1` and `object-fit: cover` above are a no-op on it.
   A cutout has been trimmed to the creature's own bounding box by png.js, precisely so the creature
   draws bigger, and that makes it TALL: measured on the first ten approved species, glaciowl is
   272x691 and solaryx 326x909. Cover in a square box then keeps only the middle 39% and 36% of
   them, which throws away the head and the feet. Glaciowl rendered as a pair of wings with no owl.
   Contain, bottom aligned, so the whole creature is present and stands where a painted one stands.
   The top mask goes too: it exists to melt a painting's hard top edge into the black, and a cutout
   has no edge to melt, only a head to erase. */
.tc-art img.cut {
  /* Stays at 76u: bottom aligned, this is what stands the creature on the ground line behind the
     sill, and raising paintings to fill the band under the plate says nothing about cut-outs. */
  top: calc(76 * var(--u)); left: 0; width: 100%;
  aspect-ratio: auto; height: calc(300 * var(--u));
  object-fit: contain; object-position: 50% 100%;
  -webkit-mask-image: none; mask-image: none; }

.tc-art::after { content: ''; position: absolute; inset: 0;
  background: radial-gradient(90% 50% at 50% 74%, color-mix(in srgb, var(--e) 10%, transparent), transparent 70%);
  box-shadow: inset 0 0 calc(40 * var(--u)) calc(10 * var(--u)) rgba(0, 0, 0, .7); }
/* Ends at the plate. Down to 120u it was darkening the top of every painting now that the plate
   no longer covers it; what it still does is sit behind the side labels beside the plate. */
.tc-scrim { position: absolute; left: 0; right: 0; top: 0; height: calc(76 * var(--u));
  background: linear-gradient(180deg, rgba(11, 13, 16, .95) 50%, rgba(11, 13, 16, 0)); }

/* ---- the rarity glow, inside the card's own edge (Dom, 2026-09-11) ----
   The rarity is not the shield, not a frame and not a rule: it is the color of the light in the
   room the card is standing in. It burns hotter up the ladder, and it is the ONLY thing on the
   card the rarity token touches now.
   Two things are deliberately kept out of it, because both of them are objects rather than air:
   the shield and the description box. They are lifted above this layer rather than having the
   glow stop at their edges, because an inset shadow that ends at a boundary draws a bright line
   along it, where occluding it lets the light run behind the plate the way light behind a plate
   actually does. */
.tc-face::after { content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 2;
  box-shadow: inset 0 0 calc(30 * var(--u)) calc(-9 * var(--u)) color-mix(in srgb, var(--r) 21%, transparent); }
.tcard.t2 .tc-face::after { box-shadow: inset 0 0 calc(30 * var(--u)) calc(-9 * var(--u)) color-mix(in srgb, var(--r) 29%, transparent); }
.tcard.t3 .tc-face::after { box-shadow: inset 0 0 calc(30 * var(--u)) calc(-9 * var(--u)) color-mix(in srgb, var(--r) 37%, transparent); }
.tcard.t4 .tc-face::after { box-shadow: inset 0 0 calc(30 * var(--u)) calc(-9 * var(--u)) color-mix(in srgb, var(--r) 45%, transparent); }
.tcard.t5 .tc-face::after { box-shadow: inset 0 0 calc(30 * var(--u)) calc(-9 * var(--u)) color-mix(in srgb, var(--r) 53%, transparent); }
.tc-crest, .tc-gem, .tc-sill { z-index: 3; }

/* ---- the shield plate ---- */
/* A SQUARE PLATE WITH CUT CORNERS (Dom, 2026-09-22: "I think we need to change the shield on the
   card. It keeps covering up or cutting of creatures heads").
   It was the handoff's escutcheon, 220 x 104 with a point: the point ran 28u into the art, which
   starts at 76u, and landed the gem on every forehead in the set. A flat plate can only be short,
   because a rectangle carried down to 104u would cover the head across the whole width, so it
   stops at 76u, exactly where the art starts: the plate and the creature share no card at all.
   Both bottom corners are cut at 70 degrees, 16u across and 44u up, so the cut leaves the side at
   32u, which is where the divider sits (Dom picked 70 over 45 and 60).
   --cx/--cy are the cut's width and height. Every nested plate carries its own, because a plate
   inset by d only stays parallel to the one outside it if its cut is the same line moved in by d.
   One geometry for all five rarities, which is the rule the handoff's README is emphatic about. */
.tc-crest, .tc-recess, .tc-inner, .tc-inner2 {
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--cy)), calc(100% - var(--cx)) 100%,
                     var(--cx) 100%, 0 calc(100% - var(--cy))); }
.tc-crest { position: absolute; left: 50%; top: 0; transform: translateX(-50%);
  width: calc(220 * var(--u)); height: calc(76 * var(--u));
  --cx: calc(16 * var(--u)); --cy: calc(44 * var(--u));
  background: linear-gradient(122deg, rgba(255,255,255,.07) 0%, rgba(255,255,255,0) 22%),
              linear-gradient(180deg, #333B47 0%, #1B222A 46%, #252D37 100%);
  --rim: #3E4753; }
/* THE RIM FOLLOWS THE CUTS (Dom, 2026-09-22: "there are purple lines around all the shields. They
   do not look right"). It was an inset box-shadow, which is a rectangle: the cut corners clipped it
   away, so the line ran along the top, the sides and the bottom and stopped dead at each cut. An
   equipped frame colors it, which is why it showed in purple. It is a 1px ring now, the plate's
   outline with the same outline 1px inside it cut out (evenodd), in --rim, which each rarity and
   the frame set. The inner outline's cut is the 70 degree line moved in by 1px. */
.tc-crest::after { content: ''; position: absolute; inset: 0; pointer-events: none; background: var(--rim);
  clip-path: polygon(evenodd,
    0 0, 100% 0, 100% calc(100% - var(--cy)), calc(100% - var(--cx)) 100%, var(--cx) 100%, 0 calc(100% - var(--cy)), 0 0,
    1px 1px, calc(100% - 1px) 1px, calc(100% - 1px) calc(100% - var(--cy) - .18px),
    calc(100% - var(--cx) - .7px) calc(100% - 1px), calc(var(--cx) + .7px) calc(100% - 1px),
    1px calc(100% - var(--cy) - .18px), 1px 1px); }
/* The handoff applies the grain as a plain overlay at .6 (crest) and .5 (sill), NOT with a blend
   mode. `mix-blend-mode: overlay` was lightening and darkening the metal unevenly and taking the
   forged look off the plate. */
.tc-grain { position: absolute; inset: 0; pointer-events: none; opacity: .6;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='120'%20height='120'%3E%3Cfilter%20id='g'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.9'%20numOctaves='2'%20stitchTiles='stitch'/%3E%3CfeColorMatrix%20values='0%200%200%200%200.55%20%200%200%200%200%200.6%20%200%200%200%200%200.66%20%200%200%200%200.16%200'/%3E%3C/filter%3E%3Crect%20width='120'%20height='120'%20filter='url(%23g)'/%3E%3C/svg%3E"); }
/* The plates step in 3u at a time at every rarity. They were 3/7/11 (Mythic 4/9/11) on the 104u
   escutcheon; on a 76u plate an 11u inset put the innermost edge at 65u, straight under the stat
   numbers, and Dom called it cramped. The cut sizes are that 70 degree line moved in by 3, 6, 9u.
   EVERY EDGE DRAWN, the cut ones included. The hairline used to be an inset box-shadow, which is a
   rectangle: the clip removed it along every diagonal (Dom: "the 2nd inner edge line is not coming
   through"), and the three fills all fade to the same near-black, so nothing else showed the step.
   Each plate IS its edge color now, and its fill sits 1px inside it on ::after, cut to the same
   shape less the 1px (.3px off the cut's width and .82px off its height, at 70 degrees). */
.tc-recess, .tc-inner, .tc-inner2 { position: absolute; background: #0B0D10; }
.tc-recess { inset: calc(3 * var(--u)); --cx: calc(15.1 * var(--u)); --cy: calc(41.53 * var(--u)); }
.tc-inner  { inset: calc(6 * var(--u)); --cx: calc(14.2 * var(--u)); --cy: calc(39.06 * var(--u)); }
.tc-inner2 { inset: calc(9 * var(--u)); --cx: calc(13.3 * var(--u)); --cy: calc(36.59 * var(--u)); }
.tc-recess::after, .tc-inner::after, .tc-inner2::after { content: ''; position: absolute; inset: 1px;
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--cy) + .82px), calc(100% - var(--cx) + .3px) 100%,
                     calc(var(--cx) - .3px) 100%, 0 calc(100% - var(--cy) + .82px)); }
/* ---- the struck steel, on every plate of the shield (Dom, 2026-09-18: "can we generate metal
   textures of the shield and then the bottom section of the cards?") ----
   The photograph is generated in NEUTRAL GRAY and blended soft-light, never laid on as itself. The
   shield is struck in five different materials, one per rarity, from a dark steel at Common to a
   violet at Mythic, and that ladder is most of what the card says about what it is holding. A
   picture with a color of its own would flatten all five into one. Soft-light contributes the
   hammer marks and the patchy sheen and nothing else, so every rarity keeps its own metal.
   Blended and not stacked, which also means the usual trap does not apply: a `normal` photograph is
   opaque and erases the ramp under it, and these do not.
   The three recessed plates carry it on their fills (::after), because each is an opaque layer over
   the one beneath. The outer plate carries it on a pseudo-element at z-index -1, which paints above
   the crest's background and below every child, so the recesses still sit on top of it.
   Where nothing has been generated, `none` resolves and none of this paints: a build with no art
   gets the plates exactly as they were drawn. */
/* ONE PHOTO ACROSS ALL FOUR PLATES (Dom, 2026-09-19: "there are rivits in it that are double
   stacking"). Every plate used to fit the same picture with `cover` against its OWN box, and a
   box inset by 3, 7 and 11 units fits it at a different scale and lands everything in it a few
   units off. The generated plate has a rivet near each corner, so each rivet was drawn once per
   plate, two or three units apart: a smear rather than a rivet. It got worse with rarity, because
   the plate count IS the rarity ladder, one at Common and four at Legendary, which is why a
   Legendary card is where it showed.
   The fix is registration, not removal. All four layers take the same size and each one is
   offset by its own inset, so they show one continuous photo with the step edges cutting across
   it: one rivet where there is one rivet. The size is written against the crest's own width with
   `auto` for the height, so it stays right whatever aspect the plate is generated at. */
/* The fills carry the photo now, since the plate itself is only its edge. Offset by inset + 1px. */
.tc-recess::after, .tc-inner::after, .tc-inner2::after { background-size: calc(220 * var(--u)) auto, auto;
  background-repeat: no-repeat, repeat; background-blend-mode: soft-light, normal; }
.tc-crest::before { background-position: 0 0; }
.tc-recess::after { background-image: var(--ui-card-crest, none), linear-gradient(180deg, #12161B, #151A20);
  background-position: calc(-3 * var(--u) - 1px) calc(-3 * var(--u) - 1px), center; }
.tc-inner::after  { background-image: var(--ui-card-crest, none), linear-gradient(180deg, #1E252E, #12161B);
  background-position: calc(-6 * var(--u) - 1px) calc(-6 * var(--u) - 1px), center; }
.tc-inner2::after { background-image: var(--ui-card-crest, none), linear-gradient(180deg, #252D37, #12161B);
  background-position: calc(-9 * var(--u) - 1px) calc(-9 * var(--u) - 1px), center; }
.tc-crest, .tc-sill { isolation: isolate; }
.tc-crest::before, .tc-sill::before {
  content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background-size: cover; background-position: center; background-repeat: no-repeat;
  mix-blend-mode: soft-light; }
.tc-crest::before { background-image: var(--ui-card-crest, none);
  background-size: calc(220 * var(--u)) auto; }
/* The sill plate is lifted before it blends, and this is arithmetic rather than taste.
   `soft-light` pivots on mid-gray: lighter than half lightens, darker darkens, half does nothing.
   The generated sill is blackened steel averaging 0.17 and never reaching 0.63 anywhere, so the
   only thing it could do was darken, and it was halving the sill's own ramp (0.172 / 0.115 /
   0.081 went to 0.080 / 0.049 / 0.033) while contributing about 5 of grain. The plate was being
   applied the whole time and looked like nothing had happened.
   Measured against the real image, these two numbers put the sill back within 0.006 of the tone
   it was designed at, raise the grain to 14-21, and clip nothing. Brightness alone gets there too
   but blows a fraction of a percent of the pixels to white, which soft-light turns into sparkle;
   the contrast pulls those back under. Dom kept the art and asked for the overlay to change. */
.tc-sill::before { background-image: var(--ui-card-sill, none);
  filter: brightness(2.6) contrast(0.9); }

/* The element inlay: lit metal, hot center, dark ends, seated on a hairline. The rarity rule that
   used to run beneath it is gone (Dom, 2026-09-11): the shield itself is struck in the rarity's
   color now, so a second colored line was saying the same thing twice and the two bars stacked at
   the top of the crest read as a colored frame around the card. */
/* Cut where the edges meet (Dom, 2026-09-22): it fills the band inside the first plate's own
   hairline on the top and both sides and ends exactly where the next plate starts at 6u, so the
   rim and every plate edge run unbroken all the way round and the line reads as inlaid. */
.tc-ebar { position: absolute;
  left: calc(3 * var(--u) + 1px); right: calc(3 * var(--u) + 1px);
  top: calc(3 * var(--u) + 1px); height: calc(3 * var(--u) - 1px);
  background: linear-gradient(90deg, color-mix(in srgb, var(--e) 30%, #000), var(--e) 20%, color-mix(in srgb, var(--e) 55%, #fff) 50%, var(--e) 80%, color-mix(in srgb, var(--e) 30%, #000));
  box-shadow: 0 0 calc(8 * var(--u)) 0 color-mix(in srgb, var(--e) 45%, transparent), 0 1px 0 rgba(0, 0, 0, .8); }

.tc-nmrow { position: absolute; left: calc(12 * var(--u)); right: calc(12 * var(--u)); top: calc(9 * var(--u));
  display: flex; align-items: center; justify-content: center; gap: calc(10 * var(--u)); min-width: 0; }
.tc-nm { font-family: var(--display); font-weight: 700;
  font-size: calc(18 * var(--u) * var(--fit, 1));
  letter-spacing: calc(.14em * var(--fit, 1)); line-height: 1.05; color: #F6F8FA; text-transform: uppercase;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.tc-div { position: absolute; left: calc(26 * var(--u)); right: calc(26 * var(--u)); top: calc(32 * var(--u)); height: 1px; background: #3E4753; }
/* Five tracks: the gem takes the middle one, between ATK and DEF (Dom, 2026-09-22). */
.tc-st { position: absolute; left: calc(20 * var(--u)); right: calc(20 * var(--u)); top: calc(44 * var(--u));
  display: grid; grid-template-columns: 1fr 1fr calc(40 * var(--u)) 1fr 1fr; }
.tc-st > :nth-child(3) { grid-column: 4; }
.tc-st > :nth-child(4) { grid-column: 5; }
.tc-s { display: flex; flex-direction: column; align-items: center; line-height: 1; min-width: 0; }
.tc-s i { font-style: normal; font-size: calc(7 * var(--u)); letter-spacing: .14em; color: #8A94A6; }
/* All four stats at one size. The handoff sets HP three pixels larger because it is the number
   read every turn in a fight; Dom's call (2026-09-10) is that the row should read as one block. */
.tc-s b { font-weight: 700; font-size: calc(13 * var(--u)); color: #F6F8FA; font-variant-numeric: lining-nums tabular-nums; }

/* ---- the gem, in the stat row ----
   It straddled the crest point until the point went. It sits between ATK and DEF now, centered on
   the stat row (44u to 64u, so 54u): each rarity's size hangs half its height above that. */
.tc-gem { position: absolute; left: 50%; top: calc(41 * var(--u)); transform: translateX(-50%);
  width: calc(26 * var(--u)); height: calc(26 * var(--u)); border-radius: 50%;
  display: grid; place-items: center; color: var(--e);
  /* The face is set at the bottom of this file, with the skill button's, because they are the same
     stone. What stays here is the setting: a light caught in the top of the cut, the shadow under
     it, and the metal collar the stone sits in. */
  box-shadow: inset 0 calc(1 * var(--u)) calc(2 * var(--u)) rgba(255, 255, 255, .45),
              inset 0 calc(-2 * var(--u)) calc(6 * var(--u)) rgba(0, 0, 0, .5),
              0 0 0 calc(1 * var(--u)) #0B0D10,
              0 0 0 calc(3 * var(--u)) #1A1E24,
              0 0 0 calc(4 * var(--u)) #8A94A6,
              0 0 calc(10 * var(--u)) calc(-2 * var(--u)) var(--e); }
.tc-gem svg { width: calc(14 * var(--u)); height: calc(14 * var(--u)); }

/* The level sits beside the name inside the crest, the way the handoff's cards have it. The name
   auto-fits around it rather than the pill being moved out of the row. */
.tc-lv { flex: 0 0 auto; padding: calc(1 * var(--u)) calc(6 * var(--u)); background: #1A1E24; color: #F6F8FA;
  font-size: calc(9 * var(--u)); font-weight: 700; letter-spacing: .08em; white-space: nowrap;
  font-variant-numeric: lining-nums tabular-nums; box-shadow: inset 0 0 0 1px var(--e); }
/* Common carries no pill: the ladder gives it plain text and no element rule. */
.tcard.t1 .tc-lv { background: none; box-shadow: none; color: #8A94A6; padding: 0; }

/* ---- edges ---- */
.tc-rar, .tc-prov { position: absolute; top: calc(14 * var(--u)); writing-mode: vertical-rl;
  font-size: calc(9 * var(--u)); letter-spacing: .22em; text-transform: uppercase; white-space: nowrap;
  /* They run past the plate into the art, which can be white paper (Barkscale), so they carry
     their own dark halo now that the scrim stops at the plate. */
  text-shadow: 0 0 calc(3 * var(--u)) #0B0D10, 0 0 calc(6 * var(--u)) #0B0D10; }
.tc-rar { left: calc(12 * var(--u)); transform: rotate(180deg); color: var(--r); }
.tc-prov { right: calc(12 * var(--u)); color: #8A94A6; font-family: ui-monospace, Menlo, monospace; letter-spacing: .14em; }
.tc-brk { position: absolute; top: 0; width: calc(22 * var(--u)); height: calc(22 * var(--u)); border-top: 2px solid #AEBBD0; }
.tc-brk.l { left: 0; border-left: 2px solid #AEBBD0; }
.tc-brk.r { right: 0; border-right: 2px solid #AEBBD0; }

/* ---- the sill ---- */
/* 57 units, down from 68 (Dom, 2026-09-18: "can we make the sill smaller on the card so we can see
   the full image?"). Everything it holds stays: the type line, the matchup dots, the signature and
   the flavour. The eleven units came out of padding and a unit off each type size, because the
   text was already using 34 of the 35 it had and there was nothing else to give.
   Worth knowing what this buys, which is not much: 11 of 420 is 2.6% more picture. The sill is not
   what is covering the art, it is a band at the foot of it, and the only way to show materially
   more would be to let the picture run under the text behind a scrim instead of behind a plate,
   which costs the brushed steel that was generated for it. */
/* ONE STACK, not two boxes (Dom, 2026-09-18: "can you put equal padding on all the text ... in
   the sill"). The sides were already even at 10 units and so were the top and bottom at 4, but the
   type row and the text block were positioned separately, so the gaps BETWEEN the three lines were
   not: the row and the signature overlapped by about two units while the signature and the flavour
   sat two apart. A flex column with one gap makes every space between them the same by
   construction rather than by three numbers agreeing. */
/* 55, measured rather than guessed. The type row is 15 units tall once the matchup dots are in
   it, and under the old absolute positioning it sat INSIDE the text block's box: that overlap was
   exactly the uneven spacing being complained about, and removing it costs about three units back.
   At 52 the stack overflowed and flex squeezed the bottom padding to 1.3 against 4 at the top,
   which is the same inequality in a new place. */
.tc-sill { position: absolute; left: 0; right: 0; bottom: 0; height: calc(55 * var(--u));
  display: flex; flex-direction: column; justify-content: center;
  gap: calc(2 * var(--u)); padding: calc(4 * var(--u)) calc(10 * var(--u));
  background: linear-gradient(180deg, #2A313B, #1A1E24 30%, #12161B);
  box-shadow: inset 0 1px 0 #6B7585, inset 0 2px 0 #0B0D10; }
.tc-sill .tc-grain { opacity: .5; }
.tc-srow { position: static; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: space-between; gap: calc(8 * var(--u)); }
.tc-ty { font-size: max(7px, calc(10 * var(--u))); letter-spacing: .14em; color: #D9DFE6; text-transform: uppercase;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.tc-tx { position: static; flex: 0 1 auto; min-height: 0; overflow: hidden;
  display: flex; flex-direction: column; gap: calc(2 * var(--u)); }
/* One line, cut at a word with an ellipsis, not through the middle of the letters.
   `-webkit-line-clamp` needs `display: -webkit-box`, and .tc-tx is a flex column, so the browser
   BLOCKIFIES its children: the computed display came out `flow-root`, the clamp was inert, and the
   box was simply one half-height line with the glyphs sliced across the middle. On every card, at
   every size, on the only line that says what the Titan does (found 2026-09-12).
   A nowrap line with text-overflow does the same job with no display trickery to be blockified. */
.tc-tx .tc-rule { margin: 0; font-size: max(8px, calc(11 * var(--u))); line-height: 1.32; color: #D9DFE6;
  flex: 0 0 auto; min-height: 0;
  display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tc-tx .tc-rule b { color: var(--e); font-weight: 700; }
.tc-tx .tc-flavor { margin: 0; font-family: var(--serif); font-style: italic; font-size: max(8px, calc(10 * var(--u)));
  line-height: 1.32; color: #8A94A6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  flex: 0 0 auto; }

/* affinity marks, read-in-play matchup data: present on every rarity including Common */
.tc-face .tc-match { display: flex; align-items: center; gap: calc(3 * var(--u)); opacity: .85; flex: 0 0 auto; }
.tc-face .tc-mg { display: inline-flex; align-items: center; gap: calc(3 * var(--u)); }
.tc-face .tc-mg.dn { margin-left: calc(5 * var(--u)); }
.tc-face .tc-mk { font-style: normal; font-size: calc(7 * var(--u)); line-height: 1; color: #8A94A6; }
.tc-face .tc-md { display: block; width: calc(5 * var(--u)); height: calc(5 * var(--u)); border-radius: 50%;
  background: var(--c); text-decoration: none; opacity: .8; }
/* The orb in place of the dot: the painted stone at 180%, which is what fills the circle with the
   stone and puts its dark rim just outside (see .tc-gem), a size up so it reads as a stone. */
.tc-face .tc-md.orb { width: calc(9 * var(--u)); height: calc(9 * var(--u)); opacity: 1;
  background-color: var(--c); background-size: 180%; background-position: center; background-repeat: no-repeat;
  box-shadow: 0 0 0 calc(.6 * var(--u)) rgba(0, 0, 0, .7), 0 0 calc(3 * var(--u)) color-mix(in srgb, var(--c) 45%, transparent); }

/* ---- the rarity ladder: material and layer count, never shape or brightness ---- */
.tcard.t2 .tc-face { box-shadow: inset 0 0 0 1px #3E4753; }
.tcard.t2 .tc-crest { background: linear-gradient(122deg, rgba(255,255,255,.11) 0%, rgba(255,255,255,0) 24%),
                                  linear-gradient(180deg, #48525F 0%, #1E252E 42%, #333B47 100%);
  --rim: #8A94A6; }
.tcard.t3 .tc-face { box-shadow: inset 0 0 0 1px #5B6472, inset 0 0 0 2px #0B0D10; }
.tcard.t3 .tc-crest { background: linear-gradient(122deg, rgba(255,255,255,.13) 0%, rgba(255,255,255,0) 26%),
                                  linear-gradient(180deg, #79838F 0%, #1E252E 44%, #48525F 100%);
  --rim: #AEBBD0; }
.tcard.t3 .tc-gem { top: calc(40 * var(--u)); width: calc(28 * var(--u)); height: calc(28 * var(--u));
  box-shadow: inset 0 0 0 1px var(--e), 0 0 0 calc(3 * var(--u)) #1A1E24, 0 0 0 calc(4 * var(--u)) #AEBBD0, 0 0 0 calc(6 * var(--u)) #0B0D10; }
.tcard.t4 .tc-face { box-shadow: inset 0 0 0 1px #AEBBD0, inset 0 0 0 2px #0B0D10, inset 0 0 0 4px #3E4753; }
.tcard.t4 .tc-crest { background: radial-gradient(120% 90% at 32% 8%, rgba(255,255,255,.22) 0%, rgba(255,255,255,.06) 34%, rgba(255,255,255,0) 62%),
                                  linear-gradient(180deg, #A9B3C0 0%, #79838F 30%, #4A5361 62%, #2B323C 100%);
  --rim: #F6F8FA; }
.tcard.t4 .tc-gem { top: calc(38 * var(--u)); width: calc(32 * var(--u)); height: calc(32 * var(--u));
  box-shadow: inset 0 0 0 1px var(--e), 0 0 0 calc(3 * var(--u)) #1A1E24, 0 0 0 calc(4 * var(--u)) #F6F8FA,
              0 0 calc(14 * var(--u)) calc(2 * var(--u)) color-mix(in srgb, var(--e) 55%, transparent); }
/* The card edge is metal at every tier. Rarity is carried by the crest material, the layer
   count, the gem and now the glow inside the card's edge; a colored frame made the rarity the
   loudest thing on the card (Dom, 2026-09-10). */
.tcard.t5 .tc-face { box-shadow: inset 0 0 0 1px #D9DFE6, inset 0 0 0 2px #0B0D10, inset 0 0 0 4px #3E4753; }
/* Mythic keeps the plate it was originally designed with: violet, not steel, and not mixed from
   the rarity token either (Dom, 2026-09-11). It is the one card in the set allowed to be a
   different material. */
.tcard.t5 .tc-crest { background: linear-gradient(122deg, rgba(255,255,255,.15) 0%, rgba(255,255,255,0) 26%),
                                  linear-gradient(180deg, #9E6AD2 0%, #1D1429 32%, #7A44AC 62%, #111419 100%);
  --rim: #F6F8FA; }
/* Mythic is the one rule-break the handoff allows: a tri-hue rule and a chromatic split on the
   name. Kept subtle on purpose; wider read as print misregistration rather than as distortion.
   The rule bar exists on Mythic ALONE. Every other rarity lost its colored rule when the shield
   stopped being tinted, and Mythic keeps it because Mythic keeps its original design.
   It takes the element bar's slot, cut to the same edges, and the element bar is not drawn on a
   Mythic (Dom, 2026-09-22: "there is a double color line for Titanus Prime"). */
.tcard.t5 .tc-rrule { position: absolute;
  left: calc(3 * var(--u) + 1px); right: calc(3 * var(--u) + 1px);
  top: calc(3 * var(--u) + 1px); height: calc(3 * var(--u) - 1px);
  background: linear-gradient(90deg, #C46BFF, #FF3DA6 40%, #3DF2FF 75%, #C46BFF);
  box-shadow: 0 0 calc(10 * var(--u)) calc(2 * var(--u)) rgba(196, 107, 255, .6), 0 1px 0 rgba(0, 0, 0, .8); }
.tcard.t5 .tc-nm { font-size: calc(17 * var(--u) * var(--fit, 1));
  text-shadow: .7px 0 0 rgba(255, 61, 166, .5), -.7px 0 0 rgba(61, 242, 255, .4), 0 1px 2px #000; }
/* Mythic's stone is mythic violet whatever element the Titan is, which is the one place rarity
   outranks element on this card. It takes the same cut as the rest now: the tier sets --gem and
   the shared face does the rest, rather than carrying a hand-mixed gradient that would have been
   the only flat ball left in the set. */
.tcard.t5 .tc-gem { top: calc(38 * var(--u)); width: calc(32 * var(--u)); height: calc(32 * var(--u));
  --gem: #C46BFF;
  box-shadow: inset 0 0 0 1px #C46BFF, 0 0 0 calc(3 * var(--u)) #1A1E24, 0 0 0 calc(4 * var(--u)) #C46BFF,
              0 0 calc(18 * var(--u)) calc(3 * var(--u)) rgba(196, 107, 255, .55); }
/* the element bar sits on its own innermost layer, so it reads as inlaid rather than stuck on */
/* The inlay does NOT slide down with the frames. It used to take its vertical offset from the
   frame inset, which walked the rule from 10u on a Rare to 21u on a Legendary while the name stayed
   put: the color bars were printed straight across the names on Epic, Legendary and Mythic. It sits
   in the first plate at every rarity now, at the same width, so there is nothing to step. */

/* ---- sizes ----
   Everything scales from one width. No per-size type floors: the handoff's 105px test is that the
   small print becomes texture at thumbnail size and that the card should read by silhouette,
   rarity and color there rather than by reading it. Width comes from CONTEXT (a grid cell, or a
   rule like `.conj-reveal .tcard`), never from the size class: a `.tcard.md { width: 100% }`
   matched `.conj-reveal .tcard`'s specificity, won on order, and collapsed the card to zero
   inside an auto-sized grid column. */

/* selection, focus and the equipped cosmetic frame all live on the face */
/* A team member used to carry a white glow of its own, and the hover rule set box-shadow on the
   same element, so pointing at a team member REPLACED its glow with a plain drop shadow: the one
   card you touched was the one that went dark (Dom, 2026-09-11). The backlight belongs to the
   pointer alone now. Selected keeps a hairline, in the rarity rather than in white, and hover no
   longer writes over the face at all, so the tier edge and that ring both survive it.
   The glow is on the CARD box rather than on the face, because --u is a container unit defined on
   the face and the glow has to sit outside it. */
.tcard.selected .tc-face { box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--r) 80%, #F6F8FA); }
.tcard.clickable { cursor: pointer; transition: box-shadow var(--dur-fast) var(--ease-standard); }
.tcard.clickable .tc-face { transition: transform var(--dur-fast) var(--ease-standard), box-shadow var(--dur-fast) var(--ease-standard); }
.tcard.clickable:hover, .tcard.clickable:focus-visible {
  box-shadow: 0 0 34px -6px color-mix(in srgb, var(--r) 78%, transparent), 0 10px 24px -14px rgba(0, 0, 0, .9); }
/* ---- the spotlight follows the pointer (Dom, 2026-09-11) ----
   The pool of light behind a creature is drawn INTO the portrait by art.js, so no CSS could turn
   it off: every card in the grid sat in its own spotlight and the one you pointed at looked no
   different. The grid asks titanCard for `spotlight: false`, which renders the creature without
   that baked background, and the pool becomes this layer instead: the rarity's color, off until
   you point at the card. The starfield and the nebula are part of the same baked background and
   go with it, which is the price of the light being ours to switch.
   `:active` is here for touch, where there is no hover at all. */
.tcard.nospot .tc-art::before { content: ''; position: absolute; inset: 0; opacity: 0; pointer-events: none;
  transition: opacity 170ms linear;
  background: radial-gradient(58% 44% at 50% 62%, color-mix(in srgb, var(--r) 38%, transparent) 0%,
                              color-mix(in srgb, var(--r) 12%, transparent) 46%, transparent 74%); }
/* Not on a foil card: its sheen is this same layer, and the spotlight's background and full
   opacity used to replace the foil the moment you pointed at it in the grid (Dom, 2026-09-29:
   "sparkle is on when I mouse over in collection, but foil is not"). The foil is the light there. */
.tcard.nospot:not(.is-foil):hover .tc-art::before, .tcard.nospot:not(.is-foil):focus-visible .tc-art::before,
.tcard.nospot:not(.is-foil):active .tc-art::before { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .tcard.nospot .tc-art::before { transition: none; } }
.tcard.clickable:hover .tc-face, .tcard.clickable:focus-visible .tc-face { transform: translateY(-4px); }

/* ---- the card as an object in the light (Dom, 2026-09-11) ----
   perspective() is written into the transform list rather than set as a property on the parent,
   because a card's parent is a different element on every screen: .cwrap in the collection, a
   grid cell on Home, .pedestal in the detail view. This way the card owns its own perspective and
   needs nothing from whatever is holding it.
   The whole CARD leans, not just the face, so the backlight behind it leans with it. A halo that
   stays flat while the card in front of it turns reads as a sticker on glass. */
.tcard.tiltable { transform-origin: 50% 50%; }
.tcard.tiltable.tilting {
  transform: perspective(900px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)) translateY(-4px) scale(1.02);
  /* Short and linear: the lean is TRACKING a pointer, so it only wants enough smoothing to hide
     the gap between frames. An ease here reads as lag. */
  transition: transform 90ms linear, box-shadow var(--dur-fast) var(--ease-standard); }
/* the face already lifted on hover; the card is doing it now, so it must not do it twice */
.tcard.tiltable.tilting .tc-face { transform: none;
  /* The press below eases both ways off this rule: on release the face is back in the
     leaning state, and a transition animates on whichever state is being entered. */
  transition: transform 90ms ease-out; }
/* The specular: a soft white bloom sitting where the light hits, which is wherever the pointer is.
   Above everything, including the shield and the sill, because it is ON the card rather than in
   it, and it is what makes the lean read as a surface catching light rather than a skewed image. */
.tcard.tiltable .tc-face::before { content: ''; position: absolute; inset: 0; z-index: 4;
  pointer-events: none; opacity: 0; transition: opacity 180ms linear;
  background: radial-gradient(40% 30% at var(--px, 50%) var(--py, 50%),
    rgba(246, 248, 250, .17) 0%, rgba(246, 248, 250, .06) 38%, rgba(246, 248, 250, 0) 70%); }
.tcard.tiltable.tilting .tc-face::before { opacity: 1; }
/* The spotlight is a light source, so it moves with the pointer too, but at a third of the travel:
   at full travel the pool slides out from under the creature and stops reading as light on it. */
.tcard.nospot.tilting:not(.is-foil) .tc-art::before {
  background: radial-gradient(58% 44% at calc(50% + (var(--px, 50%) - 50%) / 3) calc(62% + (var(--py, 50%) - 50%) / 3),
    color-mix(in srgb, var(--r) 38%, transparent) 0%, color-mix(in srgb, var(--r) 12%, transparent) 46%, transparent 74%); }
@media (prefers-reduced-motion: reduce) {
  .tcard.tiltable.tilting { transform: none; }
  .tcard.tiltable .tc-face::before { display: none; } }
.tcard.clickable:focus-visible { outline: none; }
.tcard.clickable:focus-visible .tc-face { box-shadow: inset 0 0 0 2px var(--st-bright); }
.tcard.framed .tc-face { box-shadow: 0 0 12px color-mix(in srgb, var(--fedge) 45%, transparent),
  inset 0 0 0 1px var(--fedge), inset 0 0 0 3px var(--finner); }
.tcard.framed .tc-crest { --rim: var(--faccent); }
/* Clear of the provenance rail. At right:8u the badge sat on top of the vertical collector
   line and cut the set code off it: badge 258.7-292, rail 274.5-288. */
.tcard-badge { position: absolute; top: calc(8 * var(--u)); right: calc(28 * var(--u)); z-index: 3;
  background: var(--bl-body); color: var(--st-spec); padding: calc(2 * var(--u)) calc(7 * var(--u));
  font-size: calc(9 * var(--u)); font-weight: 700; letter-spacing: .06em; border-radius: 0; }
.tc-extra { width: 100%; }
@media (prefers-reduced-motion: reduce) { .tcard.clickable:hover .tc-face { transform: none; } }

/* ---- leveling up ----
   Buying a level used to be a coin sound and four bars that snapped to new numbers between one
   frame and the next, which is nothing for the most expensive button on the screen. The card
   takes the light, the level counts, and every bar runs UP to its new value carrying the number
   it just gained. */
/* Named for what it is. It was `lvlFlash`, which is also the name of the text glow on the result
   screen's level readout defined 600 lines earlier, so this one won on order and the result screen
   has been putting a brightness filter on a word that wanted a glow. */
.detail-art.levelled { animation: cardLevelFlash 900ms var(--ease-standard); }
@keyframes cardLevelFlash { 0% { filter: none; } 14% { filter: brightness(1.45) saturate(1.12); } 100% { filter: none; } }
.detail-art.levelled .tc-face { animation: lvlLift 900ms cubic-bezier(.22, 1, .36, 1); }
@keyframes lvlLift { 0% { transform: none; } 22% { transform: translateY(-8px) scale(1.025); } 100% { transform: none; } }
.detail-level b.lv-pop { display: inline-block; animation: lvlPop 620ms cubic-bezier(.22, 1, .36, 1); }
@keyframes lvlPop { 0% { transform: scale(1); color: inherit; } 30% { transform: scale(1.18); color: var(--st-bright); } 100% { transform: scale(1); color: inherit; } }
/* Rises out of the bar's right end rather than out of the value, which it would otherwise print
   straight over: the value column is 60px and the gap 8px. */
.stat-gain { position: absolute; right: 68px; top: 50%; z-index: 2; pointer-events: none;
  font-style: normal; font-weight: 700; font-size: .8rem; color: #7CFF6B;
  text-shadow: 0 0 10px rgba(124, 255, 107, .45);
  animation: statGain 1200ms cubic-bezier(.16, 1, .3, 1) forwards; }
@keyframes statGain {
  0%   { opacity: 0; transform: translateY(2px); }
  18%  { opacity: 1; transform: translateY(-6px); }
  70%  { opacity: 1; transform: translateY(-13px); }
  100% { opacity: 0; transform: translateY(-19px); } }
@media (prefers-reduced-motion: reduce) {
  .detail-art.levelled, .detail-art.levelled .tc-face, .detail-level b.lv-pop, .stat-gain { animation: none; } }

/* ---- iconography, direction 1b "Grit" (2026-09-10) ----
   24x24 viewBox, stroke 2.4 in currentColor, mitre joins, butt caps. The interface decides the
   hue; the file never does. Sized per surface, judged at each one's smallest. */
.ic-cur { width: 13px; height: 13px; display: inline-block; vertical-align: -2px; margin-right: 5px; flex: 0 0 auto; }
.hdr-pill .ic-cur { color: var(--amber); }
.hdr-pill.essence .ic-cur { color: var(--st-light); }
.hdr-pill.ether .ic-cur { color: var(--violet); }
/* Every sprite icon is 1em square, so the font-size already set on each surface sizes it: the
   build marks, the status tags and the currency legend all inherit the size their old glyphs had. */
.ic { width: 1em; height: 1em; display: inline-block; vertical-align: -.12em; flex: 0 0 auto; }
.status-ic { font-size: .78rem; margin-right: 4px; }

/* =================================================================================================
   THE BUTTON SWEEP (Dom, 2026-09-12)
   Every row of actions in the game was its own arrangement: a flex row that wrapped when it ran
   out of space, so each button came out whatever width its label happened to need. Across a phone
   that reads as a different decision on every screen: Find a match 181 next to Element chart 176
   next to Leaderboard 164; List a Titan 221 next to Refresh 120; Sign in 111 next to Create
   account 255.
   On a phone they are all one thing now: equal columns across the full width, and where there is a
   primary among three or more, it takes the whole row above them.
   ============================================================================================== */
@media (max-width: 760px) {
  .ranked-actions, .market-tools, .form-actions, .buy-row {
    display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
  .ranked-actions > .btn, .market-tools > .btn, .form-actions > .btn, .buy-row > .btn {
    width: 100%; min-width: 0; padding: 0 8px; letter-spacing: .05em; }
  /* Three or more, and the one you came to press takes the row; two of a kind stay halves, because
     "Level up" and "Level up x10" are the same action at two sizes and belong side by side. */
  .ranked-actions:has(> :nth-child(3)) > .btn.primary { grid-column: 1 / -1; letter-spacing: .1em; }

  /* The segmented control spans the screen and its segments are equal. It was an inline track of
     content-sized tabs: Credits 95, Ether 82, Cosmetics 113. */
  .tabs { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); width: 100%; }
  .tabs .tab { padding: 0 6px; letter-spacing: .05em; }

  /* The More sheet: eight tiles, one size. repeat(2, 1fr) is not equal when a label is wider than
     the track, because 1fr floors at min-content; minmax(0, 1fr) is. And a two-line label used to
     make its own row 84px against its neighbour's 59. */
  .more-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-rows: 1fr; }
  /* LEADERBOARD in the display face is 146px in a 137px tile at 375 (blind playtest round 1,
     2026-09-23). A little smaller and allowed to break, rather than spilling out of its tile. */
  .more-btn { padding: 14px 6px; font-size: .86rem; letter-spacing: .02em; overflow-wrap: anywhere; min-width: 0; }

  /* The team row's two controls are a matched pair: Lead had a plate and the X beside it had none,
     so one of the two read as a button and the other as a mark on the panel. */
  .team-slot .ts-btn, .team-slot .ts-lead { height: 30px; min-width: 30px; display: inline-flex;
    align-items: center; justify-content: center; }

  /* A panel's one action spans it. Content-sized and left-aligned under a paragraph, these read as
     links tucked into the text rather than as the way out of the section they close, and neither
     lined up with anything around it: Change your team sat at 255 in a 330 panel, Open the Skill
     Store at 222 in a 366 one. */
  .budget > .btn, .skills-block > .btn, .offline > .btn { display: flex; width: 100%; margin-top: 4px; }
}

/* ---- the element chart fits the phone it is read on (Dom, 2026-09-12) ----
   Nine columns and a row label at 62 + nine 44s + gaps came to 476px inside a 300px dialog, so a
   third of the ring ran off the right edge and the last two elements were a horizontal scroll
   away. That is the one screen whose whole job is showing you the ring at once.
   The cells share what is left of the row instead of carrying fixed widths, so the chart fits 360,
   390 and 430 without a breakpoint each, and it still reads: the numbers are only ever ".67" or
   "1.5" and the headers are three letters. */
@media (max-width: 760px) {
  .ec-grid { overflow-x: visible; width: 100%; }
  .ec-row { gap: 1px; margin-bottom: 1px; }
  .ec-side, .ec-corner { width: 58px; flex: 0 0 58px; font-size: .52rem; letter-spacing: 0; padding-left: 0; }
  .ec-head .ec-h { width: auto; flex: 1 1 0; min-width: 0; height: 20px; font-size: .54rem; letter-spacing: 0; }
  .ec-cell { width: auto; flex: 1 1 0; min-width: 0; height: 26px; font-size: .6rem; }
}

/* ---- half-height skills on a phone (Dom, 2026-09-12) ----
   Three skills at 54 plus the five-button strip under them was most of the space below the arena,
   and the fight is the thing worth the room. This sits at the end of the file on purpose: two
   earlier phone blocks set .skill.big to 48 and then 44, and a rule written next to the base one
   loses to both of them however right it is. */
@media (max-width: 760px) {
  .skill-grid { gap: 6px; }
  .skill.big { min-height: 38px; padding: 4px 8px 4px 25px; }
  .skill.big .skill-name { font-size: .8rem; line-height: 1.2; }
  .skill.big .skill-sub { font-size: .63rem; line-height: 1.2; }
  .skill-key { left: 5px; top: 5px; width: 14px; height: 14px; font-size: .56rem; }
}

/* =================================================================================================
   THE ARENA: ONE GROUND, TWO FIGHTERS (Dom, 2026-09-12, promoted to every width 2026-09-17)
   Both fighters were laid out the same way, info left and sprite right, one stacked
   above the other. So the foe stood in the SKY above the horizon and the player stood on the
   ground below it, which is two planes, and half the arena was empty because nothing was ever
   placed in it.
   `display: contents` dissolves each .fighter box so its info, sprite and bench become items of the
   arena's own grid and can be placed where they belong rather than where their wrapper sits. The
   two creatures share one band and one baseline, facing each other across it, and they are the
   biggest thing on the screen now because the space the empty sky was holding went to them. The
   turn order moves to the top: in the middle it was a 300px strip standing between two fighters
   who wanted the width.
   This was a phone block until Dom asked for it everywhere ("for desktop battle, yes I want the
   Titans as large as they can be with the same layout as mobile", 2026-09-17). Desktop used to be
   two stacked rows of info-sprite-bench, which put the two creatures in different halves of the
   frame, 260px wide each, with the whole middle of a 1400px window empty. One layout now; the
   only thing that changes with width is how much room the creatures are allowed to take.
   ============================================================================================== */
/* Four rows: turn order, their readout, THE GROUND, your readout. It was six, because each
   side's bench had a row of its own, and those two rows cost 90px that the ground needed. The
   benches are drawn as standing further back on the field, so that is where they go: into the
   band, at the back of their own half. The band carries a real minimum, because a 1fr row with
   nothing left to claim collapses to zero and takes both fighters with it. */
.arena {
  grid-template-columns: 1fr 1fr;
  grid-template-rows: auto auto minmax(clamp(80px, 13vh, 104px), 1fr) auto;
  align-content: stretch; }
.fighter { display: contents; }

/* `.arena .fighter { z-index: 1 }` kept the fight above the ground plane, and `display: contents`
   made that rule land on a box that no longer exists: the ::after grid paints later in the same
   layer, so the field's lines were drawn straight over the creatures and they looked
   semi-transparent (Dom, 2026-09-12). The pieces carry the z-index themselves now. */
.fighter .fighter-info, .fighter .sprite-wrap, .fighter .bench, .arena .order { position: relative; z-index: 1; }

.arena .order { grid-column: 1 / -1; grid-row: 1; align-self: start; margin: 0; }
.fighter.side-1 .fighter-info { grid-column: 1 / -1; grid-row: 2; }
.fighter.side-0 .fighter-info { grid-column: 1 / -1; grid-row: 4; }

/* ---- the readout is one row, at every width ----
   `.fighter-readout { display: contents }` let the health figure, the energy meter and the action
   gauge fall into the panel's own column as three more stacked rows, so each panel measured 133px
   on a desktop and the two of them took 267 of the arena's 549. That is the band the creatures
   stand in, spent on five rows of instrument. Measured at 1440x900 before this: sprite 136px,
   the phone at 375 drawing them bigger relative to its screen than the desktop did.
   The phone has read as one row since 2026-09-12 and it was always the better composition; it was
   written inside a phone media query because at the time only the phone had run out of room. */
.fighter-readout { display: flex; flex-wrap: nowrap; align-items: center; gap: 14px; min-width: 0; }
.fighter-sub { flex: 0 0 auto; }
.energy { flex: 0 1 auto; min-width: 0; }
/* The gauge takes a readable length and stops. `flex: 1 1` is right on a phone, where the row is
   300px and the gauge should have what the pips do not want; across a 1226px panel it drew a
   thousand-pixel bar for a value that is always a fraction of one turn. */
.gauge-row { flex: 0 1 300px; min-width: 68px; }

/* The ground. Both sprites stand on the bottom edge of this band, which is the shared baseline.
   Centered in their own half rather than pinned to the outside edges: at the edges they were
   fighting the sides of the frame instead of each other (Dom, 2026-09-12). */
/* The baseline sits off the bottom of the band rather than on it, so the two of them stand IN
   the field with ground in front of them instead of balancing on the edge of the readout below
   (Dom, 2026-09-12). */
/* overflow: hidden is the guarantee. Whatever a creature's height computes to, it cannot paint
   over a health bar; the clip stops at the band. */
/* A COLUMN flex box, so the creature inside it can be shrunk by it. `height: 100%` does not
   work here: the row is not a definite containing block, so the percentage is dropped and the
   literal height wins, which is exactly what left a 165px creature standing in a 114px row and
   getting its head and feet cut off (Dom, 2026-09-15). Flex shrink has no such problem. */
/* The padding is the drop-shadow's landing room, and it has to be at least the 18px blur on
   `.sprite img`. `overflow: hidden` clips at the PADDING box, so with the creature flush to the
   edge its glow was sliced off square and read as a hard rectangle around every fighter (Dom,
   2026-09-17: "there is a hard glow edge"). Measured at phone width: a 150px image in a 150px
   wrap, gap 0 on every side, against an 18px shadow.
   Latent until the cutouts arrived. A procedural drawing never reached its own edges, so the
   glow fell inside the box; a cutout is trimmed to the creature and does reach them.
   The sprite shrinks to fit rather than overflowing, which is what the flex column is for. */
.fighter .sprite-wrap { align-self: stretch; min-height: 0; overflow: hidden;
  flex-direction: column; justify-content: flex-end; align-items: center;
  padding: 20px 20px 20px; }
.fighter.side-0 .sprite-wrap { grid-column: 1; grid-row: 3; justify-self: stretch; }
.fighter.side-1 .sprite-wrap { grid-column: 2; grid-row: 3; justify-self: stretch; }
/* ---- the rest of the team stands ON the field, further back ----
   They were tiles in a tray under the arena: a box, a border, a portrait and a green bar, which
   is a roster row, not a squad (Dom, 2026-09-12). They stand at the back of your own half now,
   small because they are further away, dimmed because they are not the one fighting, each on its
   own little HP line. A fainted one grays out where it fell. */
/* Each side's reserves stand with their own readout: the wild one's under its bar at the top,
   yours above yours at the bottom, rather than both floating in the middle of the field. */
.fighter.side-1 .bench { grid-column: 1 / -1; grid-row: 3; align-self: start; justify-self: end;
  flex-direction: row; gap: 8px; margin: 0; padding: 2px 0 0; }
.fighter.side-0 .bench { grid-column: 1 / -1; grid-row: 3; align-self: end; justify-self: start;
  flex-direction: row; gap: 8px; margin: 0; padding: 0 0 2px; }
/* Dimmed with BRIGHTNESS, not opacity. Opacity made them see-through and the field's grid lines
   ran straight through their bodies, which is the one thing a creature standing on a field must
   never do (Dom, 2026-09-12). Brightness pushes them back without making them ghosts. */
.arena .bench-slot { width: clamp(46px, 5vw, 78px); background: none; border: 0; padding: 0; margin: 0;
  opacity: 1; filter: brightness(.68) saturate(.8); }
.arena .bench-slot i { height: 2px; margin-top: 1px; border-radius: 0;
  box-shadow: 0 0 6px -1px var(--lime); }
.arena .bench-slot.out { opacity: 1; filter: grayscale(1) brightness(.42); }

/* The creatures take the room the sky was wasting, and no more than it. Fixed at 150 they
   overflowed the band whenever the arena was short and stood on top of the readouts. Filling the
   band up to 150 means they are as big as the space allows and never bigger. */
/* `min(150px, 100%)` was written to cap the creature to the band and never did: the percentage
   has no definite height to resolve against here, so it was dropped and the literal won. The
   creature stood at full size whatever the band measured, which on a phone put its legs over
   your own name and health bar, and showed nothing at all when the band collapsed (Dom,
   2026-09-13: "Battle is broken"). A viewport unit is always definite, so this one binds, and
   the arena's minimum below keeps the band big enough for it. */
/* 14vh was a floor set when the band was capped at 292px, and the band is not capped: it is
   `flex: 1 1 auto` and on a 950px phone it grows to 554, so the creature stayed at 114px inside
   a band with 259px of empty floor above it. 22vh spends that. It stays a viewport unit rather
   than a pixel count, which is what keeps it from ever outgrowing the screen and putting its
   legs across your own health bar again. (Dom picked this, 2026-09-15: everything else on the
   screen stays exactly where it is, the slack comes out.) */
/* Fill the row, and never be taller than it. A viewport fraction alone is wrong because the
   band is not a fraction of the viewport: it is whatever is left after the header, the panels,
   the log and the ACTIONS, and a six-skill ranked loadout makes that last one 194px where a
   two-skill wild fight makes it 94. At 460x785 with six skills the row is 114px while 22vh is
   173, and the creature overflowed its row and got cut top and bottom (Dom, 2026-09-15).
   `height: 100%` takes the row; the clamp is only a ceiling for when the row is generous. */
/* The clamp is what it WANTS to be. `--sprite-cap` is the room it actually has, measured after
   layout by fitSprites() in battle.js, because CSS cannot see how tall a grid row came out. */
.sprite { width: auto; aspect-ratio: 1; min-height: 0;
  height: calc(min(clamp(120px, 46vh, 620px), var(--sprite-cap, 100vh)) * var(--lvl, 1)); }

/* What is left here is compression, not layout: the same arena, drawn for a screen that has to
   fit an action row under it. (Dom, 2026-09-17: "for desktop battle, yes I want the Titans as
   large as they can be with the same layout as mobile.") */
@media (max-width: 900px) {
  .arena { gap: 4px; }
  .arena .bench-slot { width: 46px; }
  .fighter-readout { gap: 6px; }
  .gauge-row { flex: 1 1 68px; }
  /* The phone's ceiling on a Titan. Above this width the same line is 46vh, because a desktop
     window has the height to spend and nothing else on the screen wants it. */
  .sprite { height: calc(min(clamp(88px, 22vh, 210px), var(--sprite-cap, 100vh)) * var(--lvl, 1)); }
  /* The instruments were sized for a band with nothing else in it. */
  .fighter-info { padding: 6px 10px; }
  .arena .order { min-height: 28px; }
  .order-label { display: none; }
  /* A teaching card used to need the creatures to shrink and the log to disappear to make room
     for it. It floats now and takes no layout, so neither is necessary. */
  /* Tall enough for its four rows, but never so tall that the action buttons leave the screen.
     A flat 200px (set when this was a simpler layout) starved the band; a flat 292px fixed a tall
     phone and pushed Switch, Wait, Net and Run below the fold on a 667px one. The column is
     roughly 100vh minus the header, the nav, the top bar and the 287px HUD, so the arena takes
     what is left of that, between 200 and 292. */
  .screen.battle.active .arena { min-height: clamp(200px, calc(100vh - 443px), 292px); }
}

/* A SHORT window, where the four rows and a 324px HUD cannot both fit (a wild fight carries five
   action buttons, not three). Measured at 375x667: the turn strip goes, the log gives up two
   lines, the readouts tighten, and what that buys is a band the fighters still stand in with
   Switch, Wait, Net, Pulse and Run all still on screen. The alternative, and what shipped before,
   was a band of 2px with both creatures drawn across the health bars.
   The three rows are not a phone thing: a 1440x720 desktop window has the same problem, and now
   that both run the same layout it gets the same answer. The compressions below it, which are
   about a narrow screen rather than a short one, stay on the phone. */
@media (max-height: 740px) {
  .arena .order { display: none; }
  .battle-log { max-height: 46px; }
  .arena { grid-template-rows: auto minmax(clamp(64px, 11vh, 104px), 1fr) auto; }
  .fighter.side-1 .fighter-info { grid-row: 1; }
  .fighter.side-1 .sprite-wrap, .fighter.side-0 .sprite-wrap,
  .fighter.side-1 .bench, .fighter.side-0 .bench { grid-row: 2; }
  .fighter.side-0 .fighter-info { grid-row: 3; }
}

@media (max-width: 900px) and (max-height: 740px) {
  .fighter-info { padding: 3px 7px; gap: 2px; }
  .fighter-name { font-size: .76rem; }
  .screen.battle.active .arena { min-height: 200px; }

  /* ---- the meters get out of the creatures' way ----
     The readout was a 10px health bar, 22px energy pips and a 5px gauge in a panel with 10px of
     padding, which is a lot of instrument for a strip that sits under a fighter. */
  .fighter-info { padding: 5px 8px; gap: 3px; }
  .hpbar { height: 6px; }
  .energy i { width: 9px; height: 5px; }
  .gauge { height: 4px; }
  /* ---- the readout is three instruments, not one strip of marks ----
     Health, energy and the action gauge sat 5px apart, so the figure, the pips and the bar ran
     together and the eye had to work out where each one ended (Dom, 2026-09-12). Twelve between
     the three, two inside the energy meter, six between the meter and the number that labels it:
     the spacing says what belongs to what. */
  .fighter-readout { gap: 12px; align-items: center; }
  .fighter-sub { font-size: .58rem; }
  .energy { align-items: center; gap: 2px; }
  .energy-label { font-size: .62rem; margin-right: 6px; display: inline-flex; align-items: center; gap: 3px; }
  /* An empty pip is a socket with nothing in it rather than a flat dark bar, so eight of them read
     as one meter that is partly full. */
  .energy i { border-radius: 0;
    background: linear-gradient(180deg, #080A0C, #161A20);
    box-shadow: inset 0 1px 2px rgba(0, 0, 0, .9), inset 0 0 0 1px rgba(214, 224, 236, .05); }
  .energy i.on { box-shadow: inset 0 1px 0 rgba(255, 255, 255, .5), 0 0 7px -2px var(--amber); }
  .gauge-row { gap: 6px; }

}
/* ---- a short desktop window buys the band the same way a short phone does ----
   At 1280x700 the two readouts measured 87px each out of a 349px arena and the creatures came out
   at 93, smaller than the same fight draws on a 375px phone. The instruments give it back: this is
   the phone's own short-screen tightening, at desktop sizes. The type is left alone, because a
   desktop has the width for it and it is the meters, not the names, that are over-scaled here. */
@media (min-width: 901px) and (max-height: 740px) {
  .fighter-info { padding: 5px 10px; gap: 3px; }
  .hpbar { height: 8px; }
  .energy i { width: 14px; height: 7px; }
  .gauge { height: 5px; }
  .battle-log { min-height: 44px; max-height: 52px; }
}

/* The phone-arena block above owns the creature's size. These two used to restate it with the
   percentage cap that does not bind, and being narrower they cascaded AFTER it and won. */
/* The narrowest phones. This is the LAST rule that sizes a creature, so it is the one that decides
   on a 460px screen whatever the four before it say: raising any of those without this one changes
   nothing at all, which is exactly the trap that ate the first two attempts at this. */
@media (max-width: 480px) { .sprite { height: calc(min(clamp(84px, 21vh, 200px), var(--sprite-cap, 100vh)) * var(--lvl, 1)); }
  .arena .bench-slot { width: 40px; } }

/* The Skill Store pointed at one Titan. The element color comes in on --c so the bar reads as
   belonging to that Titan rather than to the store. */
.skfocus { display: flex; gap: 14px; align-items: center; justify-content: space-between; flex-wrap: wrap;
  padding: 12px 14px; margin-bottom: 14px; background: var(--panel);
  border: 1px solid var(--line); border-left: 3px solid var(--c, var(--gold)); }
.skfocus > div { min-width: 220px; flex: 1; }
.skfocus b { font-family: var(--display); letter-spacing: .04em; color: var(--c, var(--ink-accent)); }
.skfocus p { margin: 3px 0 0; }

/* What a skill actually does, generated from its effect object. The written line above it says
   what the skill is for; this says what happens. */
.skfx { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 3px; }
.skfx li { position: relative; padding-left: 12px; font-size: .78rem; color: var(--text); opacity: .86; line-height: 1.35; }
.skfx li::before { content: ''; position: absolute; left: 2px; top: .52em; width: 4px; height: 4px;
  background: var(--c, var(--gold)); transform: rotate(45deg); }
/* The status's own cost, in the status's own color so it reads as that effect speaking. */
.skfx-status { margin: 7px 0 0; padding: 6px 8px; font-size: .76rem; line-height: 1.4;
  color: var(--text); background: color-mix(in srgb, var(--c, var(--gold)) 12%, transparent);
  border-left: 2px solid var(--c, var(--gold)); }

/* Session backups on the Account screen. The date leads each row because the date is the thing
   being chosen between. */
.backups { margin-top: 16px; border: 1px solid var(--line); background: var(--panel); }
.backups-head { display: flex; gap: 12px; align-items: baseline; justify-content: space-between;
  flex-wrap: wrap; padding: 11px 14px; border-bottom: 1px solid var(--line); }
.backups-head b { font-family: var(--display); letter-spacing: .06em; color: var(--ink-accent); }
.bkrow { display: flex; gap: 14px; align-items: center; justify-content: space-between;
  flex-wrap: wrap; padding: 11px 14px; border-bottom: 1px solid var(--line2); }
.bkrow:last-child { border-bottom: 0; }
.bkrow > div { flex: 1; min-width: 220px; }
.bkrow b { font-variant-numeric: tabular-nums; }
.bkrow p { margin: 2px 0 0; }


/* Card size slider. Lives in the Collection filter row; the value it writes is global, so the
   Shop and Market card grids follow the same preference. */
.sz { display: flex; align-items: center; gap: 8px; font-size: .85rem; color: var(--muted); }
.sz input[type=range] {
  appearance: none; -webkit-appearance: none; width: 132px; height: 20px;
  background: transparent; box-shadow: none; padding: 0; cursor: ew-resize; }
.sz input[type=range]::-webkit-slider-runnable-track {
  height: 4px; background: linear-gradient(180deg, #08090B, var(--sunken));
  box-shadow: inset 0 0 0 1px var(--line2); }
.sz input[type=range]::-moz-range-track {
  height: 4px; background: linear-gradient(180deg, #08090B, var(--sunken));
  box-shadow: inset 0 0 0 1px var(--line2); }
.sz input[type=range]::-webkit-slider-thumb {
  appearance: none; -webkit-appearance: none; width: 12px; height: 16px; margin-top: -6px;
  background: linear-gradient(180deg, var(--st-light), var(--st-body)); border: 0;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .8); }
.sz input[type=range]::-moz-range-thumb {
  width: 12px; height: 16px; border: 0;
  background: linear-gradient(180deg, var(--st-light), var(--st-body));
  box-shadow: 0 1px 2px rgba(0, 0, 0, .8); }
.sz input[type=range]:focus-visible { outline: 2px solid var(--st-bright); outline-offset: 3px; }
.sz output { min-width: 5.2em; font-variant-numeric: tabular-nums; color: var(--text); opacity: .8; }
@media (max-width: 760px) { .sz input[type=range] { width: 100px; } .sz output { min-width: 4.6em; } }

/* What an element is worth finishing, said on its own row in the Codex. */
.codex-prize { margin-left: auto; font-size: .7rem; color: var(--muted); letter-spacing: .02em; }
.codex-prize.won { color: var(--c); font-weight: 700; }
.codex-goal { margin: 0 18px 6px; padding: 8px 12px; font-size: .76rem; line-height: 1.45;
  color: var(--text); background: #ffffff05; border-left: 2px solid var(--gold); }
.codex-goal.won { border-left-color: var(--ink-accent); }
/* The completion dialog. One title, one number, nothing else competing with them. */
.codex-won { text-align: center; padding: 6px 0 2px; }
.codex-won b { display: block; font-family: var(--display); font-size: 1.5rem; letter-spacing: .05em;
  color: var(--c, var(--ink-accent)); margin-bottom: 8px; }
.codex-won p { margin: 4px 0; }
.codex-won-pay { color: var(--ink-accent); font-weight: 700; }

/* ---------- player banners ----------
   Icon, name, title, rank on a background. The same component on Home, Ranked, the lead pick and
   the leaderboard, so a player looks like the same player everywhere. --a/--b are the wash,
   --line its edge; the sm size is a leaderboard row and drops the rank line. */
.pbanner { --a: #262C36; --b: #0A0C10; --line: #AEBBD0;
  position: relative; display: flex; align-items: center; gap: 14px; padding: 10px 14px; min-width: 0;
  background: linear-gradient(100deg, var(--a) 0%, var(--b) 70%), var(--b);
  border: 1px solid color-mix(in srgb, var(--line) 45%, transparent);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .6), inset 0 1px 0 color-mix(in srgb, var(--line) 22%, transparent),
              0 10px 24px -16px rgba(0, 0, 0, .9); }
.pbanner::after { content: ''; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(60% 120% at 0% 50%, color-mix(in srgb, var(--line) 16%, transparent), transparent 70%); }
.pbanner > * { position: relative; z-index: 1; }
.pbanner-icon { flex: 0 0 auto; width: 64px; height: 64px; display: grid; place-items: center;
  background: #00000055; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--line) 50%, transparent); }
.pbanner-icon img, .pbanner-icon canvas, .pbanner-icon svg:not(.banner-sigil) { width: 58px; height: 58px; }
svg.banner-sigil { width: 32px; height: 32px; color: var(--line); }
.pbanner-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.pbanner-name { font-family: var(--display); font-size: 1.05rem; letter-spacing: .06em; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pbanner-title { font-family: var(--serif); font-style: italic; font-size: .86rem; color: var(--line); }
.pbanner-rank { font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); font-variant-numeric: tabular-nums; }
.pbanner-edit { margin-left: auto; align-self: center; }
.pbanner.sm { gap: 8px; padding: 4px 8px; border: 0; box-shadow: none; background: linear-gradient(100deg, var(--a) 0%, transparent 85%); }
.pbanner.sm::after { display: none; }
.pbanner.sm .pbanner-icon { width: 32px; height: 32px; }
.pbanner.sm .pbanner-icon img, .pbanner.sm .pbanner-icon canvas { width: 30px; height: 30px; }
.pbanner.sm svg.banner-sigil { width: 18px; height: 18px; }
.pbanner.sm .pbanner-name { font-size: .88rem; }
.pbanner.sm .pbanner-title { font-size: .74rem; }
.pbanner.sm .pbanner-rank { display: none; }
.home-hero .pbanner { margin-bottom: 14px; }
#screen-ranked .pbanner { margin-bottom: 16px; max-width: 560px; }
/* The editor. */
.pbanner-preview { margin-bottom: 14px; }
.pb-pick h4 { margin: 12px 0 6px; font-size: .72rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
.pb-row { display: flex; flex-wrap: wrap; gap: 6px; }
.pb-opt { width: 48px; height: 48px; display: grid; place-items: center; padding: 0; border: 1px solid var(--line); background: #00000044; color: var(--text); cursor: pointer; }
.pb-opt img, .pb-opt canvas { width: 42px; height: 42px; }
.pb-opt svg.banner-sigil { width: 24px; height: 24px; color: var(--st-light); }
.pb-opt.on { border-color: var(--ink-accent); box-shadow: 0 0 0 1px var(--bl-light) inset, 0 0 14px -6px var(--bl-light); }
.pb-opt.pb-bg { width: 84px; height: 40px; background: linear-gradient(100deg, var(--a), var(--b)); border-color: color-mix(in srgb, var(--line) 55%, transparent); }
.pb-opt.pb-title { width: auto; height: auto; padding: 6px 10px; font-family: var(--serif); font-style: italic; font-size: .84rem; }
.cos-banner .pbanner { width: 100%; }
.lb-row .pbanner { min-width: 0; }
@media (max-width: 760px) {
  .pbanner { gap: 10px; padding: 8px 10px; }
  .pbanner-icon { width: 48px; height: 48px; }
  .pbanner-icon img, .pbanner-icon canvas { width: 44px; height: 44px; }
  .pbanner-edit { position: absolute; right: 8px; top: 8px; margin: 0; }
}
/* What an equipped skill does, printed under its pill on the card. */
.learn .skill-fx { display: block; margin: 3px 0 6px 2px; font-size: .74rem; line-height: 1.35; max-width: 42ch; }
/* A market listing, read before being asked to sign in. */
.listing-peek { display: grid; grid-template-columns: 200px 1fr; gap: 16px; align-items: start; }
@media (max-width: 760px) { .listing-peek { grid-template-columns: 1fr; } .listing-peek .tcard { max-width: 220px; margin: 0 auto; } }
/* The bench's kit, one line under each switch option. */
.sw-kit { font-size: .72rem; line-height: 1.3; margin-bottom: 4px; }
/* A finger needs 44px; the speed toggle was 39x22 on a phone. */
/* The speed button was the ONLY reason the top row was 44px: the turn text, the field tag and the
   encounter name are all 17 to 19px, and this was a 44px tap target holding the row open at more
   than twice their height ("the field section at the top can also be smaller", Dom, 2026-09-15).
   It keeps a comfortable target by going wide instead of tall: 32 by 64 is still an easy thumb
   press, and it is the one place in the game where the 44px guideline is traded for space, which
   is a deliberate call rather than an oversight. */
@media (max-width: 760px) { .speed-btn { min-height: 24px; min-width: 76px; padding: 2px 10px; } }

/* ---------- the kit on a Titan's card ----------
   One row per skill: name, numbers, what it does, one button. Full width, so the name never wraps
   one word per line, and the button is a fixed column rather than half the screen. */
.kit { display: flex; flex-direction: column; gap: 6px; margin: 6px 0 12px; }
.kit-row { --c: var(--muted); display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px; align-items: center;
  padding: 9px 12px; background: var(--panel2); border: 1px solid var(--line); border-left: 3px solid var(--c); }
.kit-row.active { background: #15181D; border-color: #2A313B; border-left-color: var(--c); }
.kit-main { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.kit-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; min-width: 0; }
.kit-name { font-family: var(--display); letter-spacing: .04em; font-size: .98rem; color: var(--text); }
.kit-count { font-size: .72rem; font-weight: 700; letter-spacing: .06em; color: var(--ink-accent); padding: 1px 6px;
  background: color-mix(in srgb, var(--bl-body) 22%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--bl-light) 40%, transparent); }
.kit-meta { font-size: .74rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.kit-fx { font-size: .76rem; line-height: 1.35; color: var(--text); opacity: .82; }
.kit-row > .btn { min-height: 44px; min-width: 84px; }
.modal-card .kit-row > .btn.small { height: auto; min-height: 44px; }
@media (max-width: 760px) { .kit-row { padding: 8px 10px; gap: 8px; } .kit-row > .btn { min-width: 76px; padding-left: 8px; padding-right: 8px; } }

/* ---------- the animation lab ----------
   A stage with two markers standing in for the two Titans, and one row per skill: what it is,
   what its animation is made of, a Play button and somewhere to write down what is wrong. */
.fxlab-stage { position: sticky; top: var(--hdr-h); z-index: 12;
  height: 210px; margin-bottom: 14px; overflow: hidden;
  background: radial-gradient(ellipse 60% 40% at 50% 92%, #38414D 0%, transparent 70%),
              linear-gradient(#14171C 0%, #1E2329 29.6%, #252B33 30%, #111317 100%);
  border: 1px solid var(--line); }
.fxlab-mark { position: absolute; bottom: 42px; width: 64px; height: 64px;
  border: 1px dashed color-mix(in srgb, var(--st-body) 55%, transparent); border-radius: 50%;
  display: grid; place-items: center; }
.fxlab-mark span { font-size: .6rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
.fxlab-mark.from { left: 12%; } .fxlab-mark.to { right: 12%; }
.fxlab-now { position: absolute; left: 12px; right: 12px; top: 10px; margin: 0; line-height: 1.35;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.fxlab-list { display: flex; flex-direction: column; gap: 8px; }
.fxlab-row { --c: var(--muted); padding: 10px 12px; background: var(--panel2);
  border: 1px solid var(--line); border-left: 3px solid var(--c); }
.fxlab-row.has-note { border-left-color: var(--ink-accent); background: #1A1418; }
.fxlab-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.fxlab-head b { font-family: var(--display); letter-spacing: .04em; }
.fxlab-head .btn { margin-left: 0; }
/* The tick pushes itself and the Play button to the right end of the head row. */
.fxlab-act { margin-left: auto; display: flex; align-items: center; gap: 8px; }
.fxlab-ok { display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  font-size: .74rem; letter-spacing: .06em; text-transform: uppercase; color: var(--muted);
  padding: 4px 8px; box-shadow: inset 0 0 0 1px var(--line2); background: var(--sunken); }
.fxlab-ok input { accent-color: #4FA96B; width: 16px; height: 16px; margin: 0; }
.fxlab-row.is-ok .fxlab-ok { color: #8FD3A4; box-shadow: inset 0 0 0 1px rgba(79, 169, 107, .55);
  background: rgba(79, 169, 107, .12); }
.fxlab-row.is-ok { border-left-color: #4FA96B; }
.fxlab-row.is-ok.has-note { border-left-color: #4FA96B; }
@media (pointer: coarse) { .fxlab-ok { min-height: 44px; } }
.fxlab-meta { font-size: .74rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.fxlab-spec { display: flex; gap: 6px; flex-wrap: wrap; margin: 6px 0 4px; }
.fxlab-chip { font-size: .64rem; letter-spacing: .08em; text-transform: uppercase; padding: 2px 7px;
  color: var(--c); background: color-mix(in srgb, var(--c) 12%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--c) 40%, transparent); }
.fxlab-chip.on-self { color: var(--ink-accent); background: color-mix(in srgb, var(--bl-body) 18%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--bl-light) 40%, transparent); }
.fxlab-note { width: 100%; margin-top: 6px; resize: vertical; font-family: var(--font); font-size: .82rem; }
.fxlab-dump { max-height: 50vh; overflow: auto; white-space: pre-wrap; font-size: .78rem; }
@media (max-width: 760px) { .fxlab-stage { height: 150px; } .fxlab-mark { width: 52px; height: 52px; bottom: 26px; } }
/* The lab is a tool you use one-handed while watching the stage, so its two buttons get a real
   thumb target rather than the small-button height the rest of the page uses. */
@media (pointer: coarse) { .fxlab-head .btn, #fxlab-copy { min-height: 44px; } }

/* ---------- thumb targets ----------
   Apple and Google both put the smallest comfortable touch target at around 44px, and the two
   controls people press most on a phone were under it: a dialog's buttons at 40px and a battle
   skill at 38px. The skill row grows by 6px, which the arena has room for: at 375x667 the action
   block ended 78px above the bottom of the screen before this. */
@media (max-width: 700px) { .modal-actions .btn { height: auto; min-height: 44px; padding-block: 8px; } }
@media (pointer: coarse) {
  /* .skill.big is the battle button, and three phone blocks above walk it down 48, 44, 38 to
     win back vertical space. Two classes beat one, so this has to be as specific as they are. */
  .skill, .skill.big { min-height: 44px; justify-content: center; }
  .team-tog, .ts-btn, .more-btn { min-height: 44px; }
  /* The speed button is deliberately not in that list. It sits in a row of 17px labels at the top
     of a fight, and a 44px minimum there holds the whole row open at more than twice the height of
     everything in it. It keeps a comfortable target by going wide instead of tall. This rule comes
     AFTER the one that sizes it, and a broader selector later in the file silently winning is the
     single most common way a change to this stylesheet does nothing at all. */
  .speed-btn { min-height: 24px; min-width: 76px; }
}

/* The status tag became a button so a phone can ask what Chill or Root is actually doing. It has
   to keep looking like the tag it was: no plate, no padding of its own beyond what it had. */
.status-tag { background: none; border-radius: 0; font-family: inherit; line-height: 1.5;
  cursor: help; box-shadow: none; }
.status-tag:hover { background: color-mix(in srgb, var(--c) 14%, transparent); }
@media (pointer: coarse) { .status-tag { min-height: 24px; } }
/* "reworked": this animation has changed since you wrote your note, so it is worth another look. */
.fxlab-new { font-size: .6rem; letter-spacing: .1em; text-transform: uppercase; padding: 2px 7px;
  color: #0B0D10; background: var(--st-light); font-weight: 700; }
.fxlab-row.is-new { border-left-color: var(--st-light); }
/* Retiring a note that has been acted on. Quiet: it sits under the box and only matters once. */
.fxlab-clear { margin-top: 6px; font-size: .7rem; opacity: .75; }
.fxlab-row:not(.has-note) .fxlab-clear { display: none; }

/* The settle after a Titan falls. Its half of the field empties and the skills clear at the same
   moment, and the arena, being the only element that flexes, took both: the survivor inflated to
   fill a band it was never measured against and the top third of the screen went to empty sky
   (Dom, 2026-09-15). The band is pinned in JS to the height the fight was already using, so
   nothing moves, and the log takes the room the skills gave up, which is the one thing worth
   reading while the result comes up. Specificity, not position, is what makes this hold: the
   `.battle-log` height is re-declared in four later media queries.
   Lives at the end of the file because every earlier attempt to size this band was undone by a
   broader rule further down. */
.screen.battle.active.settling .arena { flex: 0 1 var(--arena-hold, auto); min-height: 0; }
.screen.battle.active.settling .battle-hud { flex: 1 1 auto; min-height: 0; grid-template-columns: 1fr; }
.screen.battle.active.settling .battle-log { flex: 1 1 auto; height: auto; min-height: 0; max-height: none; }
/* The panel of the one that fell. It keeps its numbers (0 HP is the point) but stops competing
   with the Titan still standing. */
.arena .fighter.down .fighter-info { opacity: .45; filter: grayscale(.75);
  transition: opacity 260ms ease-out, filter 260ms ease-out; }
@media (prefers-reduced-motion: reduce) { .arena .fighter.down .fighter-info { transition: none; } }

/* The particle canvas normally sits under a dialog. An effect fired INSIDE one (a level landing
   on the card, a Breach opening) has to be above it, or it runs perfectly and is never seen.
   fxstage.js puts this on for exactly as long as the stage is pointed at something in a modal. */
canvas#fx.fx-over { z-index: 70; }

/* The conjuring's own rings live on the canvas behind the page now, so the swell is just the
   line of text and the place the burst is aimed at. The CSS rings are still used by the Breach,
   which has no scene of its own. */
.conj-swell.bare .swell-ring { display: none; }
.conj-swell.bare { min-height: 200px; }

/* The card coming OUT of the portal, rather than rising into place. rvCard is the Breach's
   arrival and stays the Breach's; a conjuring has a hole in the middle of the screen for it to
   come through, so it starts small and bright at that point and grows out of it. Same specificity
   as the shared rule and later in the file, which is what lets it win. */
.conj-reveal.shown .rv-1 { animation-name: conjThrough; animation-duration: 760ms; }
@keyframes conjThrough {
  0%   { opacity: 0; transform: scale(.16) translateY(8px); filter: brightness(3.2) saturate(.4); }
  16%  { opacity: 1; filter: brightness(2.4) saturate(.6); }
  64%  { transform: scale(1.05) translateY(-5px); filter: brightness(1.18) saturate(1); }
  100% { opacity: 1; transform: none; filter: none; }
}
@media (prefers-reduced-motion: reduce) { .conj-reveal.shown .rv-1 { animation: none; } }

/* The description has done its job by the time the rite starts, and for the next ten seconds the
   screen belongs to the void. It is left in the document rather than removed so the layout under
   it does not jump, and it comes back when the step is re-rendered. */
.conj-describe.spent { opacity: 0; transform: translateY(-6px) scale(.98); pointer-events: none;
  transition: opacity 620ms ease-out, transform 620ms ease-out; }
@media (prefers-reduced-motion: reduce) { .conj-describe.spent { transition: none; opacity: 1; transform: none; } }
/* The line that holds the place while it happens. */
.conj-wait { animation: conjWait 2.6s ease-in-out infinite; }
@keyframes conjWait { 0%, 100% { opacity: .45; } 50% { opacity: .95; } }
@media (prefers-reduced-motion: reduce) { .conj-wait { animation: none; } }

/* While the void is winding up and tearing open, the screen belongs to it: the header, the nav,
   the title, the price, the buttons, the line of text, all of it goes (Dom, 2026-09-15: "when the
   spin and portal sequence is happening all the text and other UI must fade to nothing, then come
   back in after they have been revealed").
   Two things are exempt, because they are what the rite is FOR: the three forms as they arrive,
   and the reveal the Titan comes through into. Everything is left in the document and faded
   rather than removed, so nothing reflows underneath and the burst still has an element to aim
   at. */
.hdr, .nav, .conjure-inner > *, .conj-wait { transition: opacity 700ms ease-out; }
body.rite .hdr,
body.rite .nav,
body.rite .conjure-inner > *:not(.conj-forms):not(.conj-reveal),
body.rite .conj-wait { opacity: 0; pointer-events: none; }
/* The exemption above is for the FORMS, not for everything that happens to sit in the same box as
   them. The note explaining the three lives inside .conj-forms, so it inherited the exemption and
   was the one piece of furniture still standing while the rest of the screen was gone: it came
   back the instant the first form landed, a paragraph of small print beside a Titan arriving out
   of a portal (Dom, 2026-09-16: "still does not fade away"). It needs its own transition as well
   as its own rule, because the one above only reaches direct children of .conjure-inner. */
.conj-forms-note { transition: opacity 700ms ease-out; }
body.rite .conj-forms-note { opacity: 0; pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
  .hdr, .nav, .conjure-inner > *, .conj-wait, .conj-forms-note { transition: none; }
  body.rite .hdr, body.rite .nav, body.rite .conjure-inner > *, body.rite .conj-wait,
  body.rite .conj-forms-note { opacity: 1; }
}

/* The card you have just pulled out of the void is the only thing on that screen, so it behaves
   like an object you are holding rather than a picture on a page: it leans to your finger (see
   .tcard.tiltable), and when nobody is touching it it drifts, very slightly, so it never sits
   dead (Dom, 2026-09-15: "add the hover effect to the card at the end, give it some slight
   automated movement").
   The arrival is the only CSS animation here. The idle movement is not a keyframe at all: it is
   driftTilt in ui.js walking --rx/--ry, the same custom properties the pointer writes, so the card
   is doing one thing in two ways rather than two things that fight over `transform`. */
.conj-reveal.shown .rv-1 {
  /* Only the arrival. The card's idle movement is the LEAN now, driven through --rx/--ry by
     driftTilt along the same path a pointer takes. The keyframe that replaced it was three
     problems at once: a different kind of motion from the hover, a 3D transform running forever
     which promoted the card to its own layer and rasterized the frame's bevel and glow flat, and
     a transform the tilt had to swap out in one frame the moment you pressed it, which read as
     the card jumping size (Dom, 2026-09-16). */
  animation-name: conjThrough;
  animation-duration: 760ms;
  /* `backwards`, not `both`. A finished animation with `forwards` or `both` keeps holding its last
     keyframe, and an animation's held value outranks any normal declaration: conjThrough ends on
     `transform: none`, which meant that from the moment the card landed it could never lean again,
     not on hover and not on its own. `backwards` still hides it before it starts and lets go the
     instant it is over. The end frame IS the resting state, so nothing is lost by letting go. */
  animation-fill-mode: backwards;
  animation-timing-function: cubic-bezier(.22, 1, .36, 1);
}
/* The press. A card you push should give, and the FACE takes it rather than the card: the card is
   carrying the lean, and a scale there would be swapped out from under the tilt transform in the
   same frame, which is the jump this replaces. */
.tcard.tiltable:active .tc-face { transform: scale(.975); transition: transform 90ms ease-out; }
@media (prefers-reduced-motion: reduce) {
  .conj-reveal.shown .rv-1 { animation-name: none; }
}

/* The one line of text that would not go. `.conj-wait` pulses, and a running animation beats a
   plain `opacity: 0` declaration however specific it is, so the only words on screen during the
   rite stayed on screen (Dom, 2026-09-15: "the text did not fade out"). The animation has to be
   turned off, not outranked. */
body.rite .conj-wait { animation: none; opacity: 0; }

/* The three options are what the step is for, so the column they sit in is centered in the void
   rather than stacked down from the title. `safe` centring, because a window short enough for the
   content to overflow would otherwise clip its first line with no way to scroll back to it. */
.conjure-inner:has(.conj-form) { justify-content: safe center; }
/* Wider for bigger forms (Dom, 2026-09-22: "make the forms larger when conjuring"), and wider
   again (2026-09-30: "they need to be larger so they can see what they are buying"). */
.conj-forms { max-width: 1160px; margin-inline: auto; }
/* The column is 880px everywhere else; on the step with the three paintings it widens so they can
   too, and the description box keeps the width it had. */
.conjure-inner:has(.conj-forms) { width: min(1180px, 100%); }
.conjure-inner:has(.conj-forms) .conj-describe { max-width: 850px; margin-inline: auto; }
@media (min-width: 761px) {
  .conj-forms .conj-slide { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 18px; }
  .conj-forms .conj-form img { max-width: 360px; }
}
/* Each painted form carries a way to see it full size. */
.conj-cell { position: relative; width: 100%; display: flex; justify-content: center; }
.conj-cell > .conj-form { width: 100%; }
.conj-zoom-btn { position: absolute; top: 12px; right: 12px; z-index: 2; width: 34px; height: 34px; display: grid; place-items: center;
  background: rgba(6, 7, 10, .72); border: 1px solid var(--line); color: var(--text); font-size: 1.1rem; line-height: 1; cursor: zoom-in; }
.conj-zoom-btn:hover, .conj-zoom-btn:focus-visible { border-color: var(--st-light); background: rgba(6, 7, 10, .9); }
@media (max-width: 760px) { .conj-zoom-btn { top: 8px; right: 8px; width: 30px; height: 30px; font-size: 1rem; } }
/* The full-size look. */
.conj-zoom .modal-body > div { display: flex; flex-direction: column; align-items: center; gap: 10px; width: 100%; }
.conj-zoom-stage { position: relative; display: flex; align-items: center; justify-content: center; width: 100%; touch-action: pan-y; }
.conj-zoom-img { display: block; width: min(100%, 78vh, 900px); aspect-ratio: 1; object-fit: contain; border: 1px solid var(--line); background: #06070a; }
.conj-zoom-nav { position: absolute; top: 50%; transform: translateY(-50%); z-index: 2; min-width: 44px; height: 56px; font-size: 1.8rem; line-height: 1; padding: 0 10px; background: rgba(6, 7, 10, .7); }
.conj-zoom-nav.prev { left: 6px; } .conj-zoom-nav.next { right: 6px; }
.conj-zoom-label { font-family: var(--display); letter-spacing: .12em; text-transform: uppercase; font-size: .8rem; color: var(--muted); }
@media (max-width: 760px) { .modal-card.conj-zoom { padding: 12px 8px; } .conj-zoom-img { width: 100%; } .conj-zoom-nav { height: 48px; min-width: 38px; } }

/* Paging between the sets of three you have pulled. Quiet: it is only there once there is more
   than one set, and it is a way back rather than an action. */
.conj-pager { display: flex; align-items: center; justify-content: center; gap: 10px; margin-top: 2px; }
.conj-pager .btn { min-width: 40px; padding: 4px 10px; line-height: 1; }
.conj-pager .btn:disabled { opacity: .3; }
.conj-pager span { min-width: 84px; text-align: center; font-variant-numeric: tabular-nums; }

/* The three you were choosing between, kept on the naming screen so the choice stays open. The
   slide inside it is a plain row here: no paging, no clipping, nothing to leave sideways. */
.conj-reselect { width: 100%; display: flex; flex-direction: column; gap: 6px; align-items: center; }
.conj-reselect p { margin: 0; letter-spacing: .04em; }
.conj-reselect .conj-slide { grid-template-columns: repeat(3, minmax(90px, 1fr)); max-width: 600px; gap: 10px; }
.conj-reselect .conj-form { padding: 6px; }

/* The three arrive big and in the middle of the column, then travel to their seat under the
   description (Dom, 2026-09-16). A transform, so they never leave the layout: `presenting` is put
   on with no transition anywhere, which makes it a jump to the center, and the move back is the
   `travelling` transition taking the transform away again. The box that holds them stops clipping
   for the duration, because a set scaled up past its own frame would be cut off by the
   `overflow: hidden` that lets a page turn leave sideways. */
.conj-forms.presenting-host { overflow: visible; }
.conj-slide.presenting { position: relative; z-index: 4;
  transform: translate(var(--pdx, 0px), var(--pdy, 0px)) scale(var(--pscale, 1)); }
.conj-slide.travelling { position: relative; z-index: 4;
  transition: transform 900ms cubic-bezier(.22, 1, .36, 1); }
@media (prefers-reduced-motion: reduce) {
  .conj-slide.presenting, .conj-slide.travelling { transform: none; transition: none; }
}

/* Turning between the sets of three. The one you are leaving goes off the side of the frame and
   the one you are turning to comes in from the other, which is the only way a swipe reads as
   moving between pages rather than as a redraw.
   One after the other, not at the same time. These two used to run together, and the outgoing set
   was still at 85% opacity while the incoming one was already at 86%: two sets of three stacked
   on the same spot, the old one showing straight through the new one, because a .conj-form is a
   semi-transparent panel (Dom, 2026-09-16: "the previous options are sitting behind the new
   options, they should fade out and then transition to the next set"). The one you are leaving
   goes first, and the new one is held at its start frame by the `backwards` half of `both` until
   the way is clear. OUT_MS below and the in-delay are the same number on purpose. */
.conj-slide.out-left  { animation: setOutLeft 190ms cubic-bezier(.4, 0, 1, 1) both; }
.conj-slide.out-right { animation: setOutRight 190ms cubic-bezier(.4, 0, 1, 1) both; }
.conj-slide.in-right  { animation: setInRight 280ms cubic-bezier(.16, 1, .3, 1) 190ms both; }
.conj-slide.in-left   { animation: setInLeft 280ms cubic-bezier(.16, 1, .3, 1) 190ms both; }
@keyframes setOutLeft  { to { opacity: 0; transform: translateX(-46%); } }
@keyframes setOutRight { to { opacity: 0; transform: translateX(46%); } }
@keyframes setInRight  { from { opacity: 0; transform: translateX(46%); } }
@keyframes setInLeft   { from { opacity: 0; transform: translateX(-46%); } }
/* Swiping at the end of the run: it gives, and comes back. */
.conj-forms.nudge .conj-slide { animation: setNudge 240ms ease-out; }
@keyframes setNudge { 50% { transform: translateX(-10px); } }
@media (prefers-reduced-motion: reduce) {
  .conj-slide.out-left, .conj-slide.out-right, .conj-slide.in-right, .conj-slide.in-left,
  .conj-forms.nudge .conj-slide { animation: none; }
}

/* A class that sets `display` beats the user agent's `[hidden] { display: none }`, so a hidden
   flex row is still on screen: the set pager rendered its two arrows into the starfield before
   any set existed, enabled and doing nothing (round 1, 2026-09-16). This is the general fix, and
   it is deliberately NOT !important: `.conj-need[hidden]` keeps its layout space on purpose
   (display: block, visibility: hidden), and an important here would collapse it and make the
   action row jump every time the hint came and went. Plain, and last in the file, so it outranks
   a single class and loses to a class that names it. */
[hidden] { display: none; }

/* Ether is hidden in the header on a phone to keep the bar from crowding, which is right
   everywhere except the one screen that spends it: the conjuring's reroll asks for Ether and a
   phone player had to leave the rite and open the Shop to find out whether they had any
   (round 1, 2026-09-16). Shown for as long as that screen is the one you are on.
   Adding a fourth pill then overflowed the bar at 375 and 390 and pushed the mute button half off
   the edge (round 2, 2026-09-16), on the screen where the game is loudest. The rating pill
   stands down for it: on the conjuring what you can spend matters and which division you are in
   does not, and one in one out keeps the bar the width it already fitted at. */
@media (max-width: 700px) {
  body[data-screen="conjure"] .hdr-pill.ether { display: inline-flex; }
  body[data-screen="conjure"] #hdr-rating { display: none; }
  /* and in the Shop, where Ether is what most of it costs, so a purchase can be seen leaving it */
  body[data-screen="shop"] .hdr-pill.ether { display: inline-flex; }
  body[data-screen="shop"] #hdr-rating { display: none; }
}

/* The hero stacks below 900px and sits in two columns above it, and between about 1000 and 1100
   the left column lands at 263px: the game's own logotype needs 289 and `overflow: hidden` took
   the last letter off, while the player banner squeezed its name to 32px and rendered "Guest" as
   "G…" (round 1, 2026-09-16, measured at 1024x768: a laptop and an iPad in landscape).
   The column is asked for the width the title actually needs, and the banner is allowed to put
   its Customize button on a second line rather than eating the name. */
.home-hero { grid-template-columns: minmax(310px, .9fr) minmax(0, 1.6fr); }
@media (max-width: 900px) { .home-hero { grid-template-columns: 1fr; } }
.pbanner { flex-wrap: wrap; row-gap: 8px; }
.pbanner-body { flex: 1 1 140px; }
.pbanner-edit { margin-left: auto; }

/* The season track's arrows sit 6px OUTSIDE the scroller, which is only outside if the page has
   room there. On a laptop the left arrow landed on top of tier 1 and hit-testing the word
   "credits" returned the arrow, so it read as "edits" (round 1, 2026-09-16). The scroller keeps
   a gutter the width of an arrow at each end, and the fade mask starts past it, so the arrows
   have their own ground and the first and last tier are never underneath one. */
.track-wrap .season-track { padding-inline: 34px;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 40px, #000 calc(100% - 42px), transparent);
  mask-image: linear-gradient(90deg, transparent, #000 40px, #000 calc(100% - 42px), transparent); }
.track-arrow.left { left: 0; } .track-arrow.right { right: 0; }
@media (max-width: 700px) { .track-wrap .season-track { padding-inline: 0;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 18px, #000 calc(100% - 26px), transparent);
  mask-image: linear-gradient(90deg, transparent, #000 18px, #000 calc(100% - 26px), transparent); } }

/* `.btn.net` sets its color AFTER `.btn:disabled` sets the gray, so a Net you cannot afford was
   drawn in full gold and a Pulse in full magenta: the brightest controls on the bar were the two
   that did nothing, and a player tapped a 91% catch twice and got silence (round 2, 2026-09-16).
   Named here, after both, so the disabled state actually reads as disabled. */
.btn.net:disabled, .btn.net.pulse:disabled { color: #5B6472; background: #171C22;
  border: 0; box-shadow: inset 0 0 0 1px #2A313B; cursor: not-allowed; }
.btn.net:disabled small { color: var(--ink-accent); opacity: 1; letter-spacing: .04em; }

/* NOT FIXED: the level pill and the corner badge overlap at small sizes ("LV 13" under "Team" at
   375px, "LV 12" under "BEST" in the switch panel). Reported round 2 with exact rects; I could not
   get a reliable measurement of either element in the test harness, and this stylesheet's standing
   failure mode is a rule that silently does nothing or quietly overrides something else. It is
   left alone on purpose rather than guessed at.
   The even footing badge: Umbra is neutral against both opening territories, which is an answer
   and not an absence. */
.conj-rec.even { color: var(--muted); }

/* Nothing behind a dialog scrolls while it is open. `overflow: hidden` on the body alone is not
   enough in Safari, which will still scroll the document; the height pin is what actually holds
   it. The scroll position is untouched, so closing the dialog puts the page back where it was. */
/* On `body` alone this does nothing: the scrolling element here is `html`, so the page went on
   scrolling under the dialog exactly as before. Both, and the html rule is what does the work.
   The body is held at the offset it was scrolled to, so the page behind the dialog still SHOWS
   where you were rather than snapping to the top the moment a dialog opens. closeModal puts the
   real scroll back before the next paint. */
/* No :has() on the root: it restyles the whole page on every change (see go() in app.js). */
html.modal-open, body.modal-open { overflow: hidden; }
body.modal-open { position: fixed; top: var(--locked-y, 0); left: 0; right: 0; width: 100%; }

/* =================================================================================================
   THE ELEMENT GEM, AND THE METAL IT IS SET INTO (Dom, 2026-09-17)
   "I want them to be pieces of metal with element colored gems at the left end of them. Any skill
   above a 5 energy would get an elemental colored metal." And: "I also want elemental gems with
   the symbol in the center for the middle of the cards."
   One stone, two homes. The card's boss and the skill button's mark were different objects doing
   the same job: the card's was a dark steel ball with the element's symbol drawn on it in the
   element's color, and the skill's was a 14px glyph in the top right corner. They are the same
   cut stone now, at two sizes, so a Titan's gem and the gem on the skill it is about to throw are
   recognizably the same thing.
   The face is two gradients: a specular highlight up and left of center, and the body of the stone
   running from a whitened tint through the element's own color into near-black at the rim. That
   third stop is what keeps a gem from reading as a flat colored disc, and the reason the symbol is
   cut dark INTO the stone rather than drawn light on top of it: a bright glyph on a bright stone
   has nothing to sit against on the nine elements that are pale.
   ============================================================================================== */
.tc-gem { --gem: var(--e); }
.skill.big .skill-ic { --gem: var(--c); }
.tc-gem, .skill.big .skill-ic {
  /* The painted stone first, where one exists, and the drawn one under it. Two layers rather than a
     swap, so a build with no generated art, a first paint before the manifest lands and an element
     nobody has cut yet all still show a gem. `cover` on a square image inside a circle crops the
     corners, which is what the corners of a square photograph of a round stone are for. */
  /* 180%, not `cover`. Every stone came back floating in black: measured across the set, the stone
     itself is about 57% of the frame and centered, so `cover` put a small gem in the middle of a
     black disc. Scaling to 180% makes 57% into 103% and the stone fills the circle with its dark
     rim just outside it. Regenerating would not fix this reliably; the prompt already says the
     stone fills the frame and the model framed it as a product shot anyway. */
  background-size: 180%, auto, auto; background-position: center, center, center;
  background-repeat: no-repeat;
  background-image: var(--gem-img, none),
    radial-gradient(circle at 34% 24%, rgba(255, 255, 255, .88) 0 6%, rgba(255, 255, 255, 0) 34%),
    radial-gradient(circle at 50% 56%,
      color-mix(in srgb, var(--gem) 86%, #FFFFFF) 0%,
      var(--gem) 34%,
      color-mix(in srgb, var(--gem) 52%, #000000) 76%,
      color-mix(in srgb, var(--gem) 20%, #000000) 100%); }
/* Cut into the DRAWN stone, which is a bright even field with nothing in it to get lost against.
   Card only: a skill button's stone carries no symbol, because the stone is the element there. */
.tc-gem .gem-ic {
  color: rgba(6, 8, 11, .82);
  filter: drop-shadow(0 1px 0 rgba(255, 255, 255, .38)); }
/* A painted stone is the other way round: a photograph with a lit center, dark facets and a dark
   rim, and a dark glyph disappears into it. Laid OVER it in white, with two shadows so it holds its
   own edge whether it lands on the highlight or in a facet. */
.tc-gem.painted .gem-ic {
  color: rgba(255, 255, 255, .94);
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .95)) drop-shadow(0 0 5px rgba(0, 0, 0, .8)); }
/* Common's orb: plain steel. No element color, no glow and a darker collar, so it reads as the
   bottom rung; the element symbol stays cut into it. */
.tc-gem.steel { --gem: #6E7887;
  box-shadow: inset 0 calc(1 * var(--u)) calc(2 * var(--u)) rgba(255, 255, 255, .35),
              inset 0 calc(-2 * var(--u)) calc(6 * var(--u)) rgba(0, 0, 0, .55),
              0 0 0 calc(1 * var(--u)) #0B0D10,
              0 0 0 calc(3 * var(--u)) #1A1E24,
              0 0 0 calc(4 * var(--u)) #5B6472; }

/* ---- the skill button ----
   A piece of metal: the same steel surround and the same rock field as every other control, cut to
   2px because these sit six to a screen in a fight.
   The gem takes the left end and the keyboard number moves to the top right corner. They were both
   on the left before, which is where the gem has to be, and a number under a stone is a number
   nobody reads. */
/* Dom, 2026-09-17: "let's make the skill buttons colored metal. Frames. With the plate looking like
   black lava rock."
   So the FRAME carries the element and the plate is the black basalt, at every cost. The frame is
   the element's color rolled as metal rather than painted on: lit along the top where it catches
   the light, the element's own color through the middle where the metal's local color lives, and
   the element taken down to near-black along the bottom where it is in its own shadow. The same
   three-stop shape as the steel surround everywhere else, in a different metal.
   The plate is the generated basalt with its own scrim, deepening downward, so it reads as set into
   the frame rather than flush with it. That scrim is written here rather than taken from --ui-scrim
   because a skill button wants a blacker plate than a Back button does: this is lava rock in a
   socket, not a slab you press. */
.skill.big {
  position: relative;
  border: 2px solid transparent; border-radius: 0;
  /* Dark metal (Dom, 2026-09-17: "let's try making the frame dark metal and then the section
     surrounding the crystals the colored metal but darker"). The element moved inward, to the band
     the stone sits in: six of these are on screen at once in a fight and six colored frames made
     the HUD a row of colored rectangles, where six dark frames with a colored socket in each reads
     as one rack of six tools.
     The border itself is transparent and 3px wide; what is actually drawn is the ring below, which
     is the same forged iron the socket wears. A border-image cannot be blended, and the metal is
     generated GRAY so the socket can colorize it, so a frame cut from it straight would have been
     a mid-gray band. The ring can carry a black cover; a border-image cannot. */
  border-image: none; border-width: 3px;
  /* `--ui-skill` is the plate cut for this size and `--ui-btn` is the button's slab, which stands
     in until one has been. Either way the gradient underneath is what a build with no art gets.
     The cover is a flat 30% black, the same one every other button wears (Dom, 2026-09-17). It was
     a gradient from 28% to 55% for a while, on the theory that a plate darkening downward reads as
     set into its frame; with fine pitting it read as a plate that was dirty at the bottom. */
  /* Its own cover rather than --ui-scrim's 30%: a skill plate wants to be darker than a Back
     button (Dom, 2026-09-18: "make the button background darker"). */
  background-image: linear-gradient(rgba(0, 0, 0, .46), rgba(0, 0, 0, .46)),
                    var(--ui-skill, var(--ui-btn, none)), linear-gradient(180deg, #14181E, #0A0D11);
  background-size: auto, cover, auto; background-position: center, center, center;
  background-clip: padding-box;
  padding: 10px 12px 10px 46px; min-height: 54px;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .85);
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .7), inset 0 -1px 0 rgba(217, 223, 230, .1),
              0 0 0 1px #0B0D10, 0 2px 6px rgba(0, 0, 0, .6); }
/* The plate catches a light, and it fades in like every other button's. The cover does NOT thin:
   changing it changed how dark the rock was, which is a change of material, and a skill you are
   only pointing at should not look like a different skill.
   A span rather than a pseudo-element, because this button's two are already spoken for: ::before
   is the socket and ::after is the frame. It sits at z-index 2, above the plate and the socket and
   under the frame, so the light crosses the rock and the element's metal and stops at the edge. */
.skill.big .skill-lite {
  position: absolute; inset: 0; z-index: 2; pointer-events: none; opacity: 0;
  background: linear-gradient(0deg, rgba(232, 240, 250, .16) 0%, rgba(232, 240, 250, .04) 42%, rgba(232, 240, 250, 0) 75%);
  transition: opacity var(--dur-hover-out) var(--ease-standard); }
.skill.big:hover:not(:disabled) .skill-lite, .skill.big:focus-visible:not(:disabled) .skill-lite {
  opacity: 1; transition-duration: var(--dur-hover-in); }
.skill.big:active:not(:disabled) .skill-lite { opacity: 0; transition-duration: var(--dur-instant); }
/* A warm light on the seven, because gold under a cold one goes green. */
.skill.big.forged.hot .skill-lite {
  background: linear-gradient(0deg, rgba(255, 233, 168, .22) 0%, rgba(255, 233, 168, .06) 42%, rgba(255, 233, 168, 0) 75%); }
.skill.big:active:not(:disabled) { transform: translateY(1px);
  box-shadow: inset 0 3px 7px rgba(0, 0, 0, .85), 0 0 0 1px #0B0D10; }
/* Not affordable yet is the light switched off, not the material taken away (Dom, 2026-09-18:
   "not affordable buttons should still have a background texture on the button"). It used to drop
   to a flat #171C22, which made a skill you cannot pay for the one button on the screen that was
   not cut from anything: a hole in a rack of tools. Same lava rock, under a heavier cover and with
   the color pulled out of it, so it reads as unlit stone. Still legible, because you are reading
   it to decide what to save energy FOR. */
.skill.big:disabled {
  background-image: linear-gradient(rgba(0, 0, 0, .66), rgba(0, 0, 0, .66)),
                    var(--ui-skill, var(--ui-btn, none)), linear-gradient(180deg, #171C22, #0C1014);
  background-size: auto, cover, auto; background-position: center, center, center;
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .55), 0 0 0 1px #0B0D10; }
/* The stone goes out (Dom, 2026-09-18: "lets make the gems black on not affordible"). Grayscale
   first and then down to a fifth of its brightness, so what is left is the CUT of the stone in
   black rather than a dim version of its color: a gem with no light in it. The collar keeps its
   metal, because the setting is still there. */
.skill.big:disabled .skill-ic { filter: grayscale(1) brightness(.22) contrast(1.1); }

/* ---- above five energy, the metal itself is cast in the element ----
   Cost 5 is the bridge tier and 6 through 8 are the top of the ladder, so the four heaviest things
   a Titan can do are the four that look like they were forged out of their own element rather than
   out of steel. The ramp, the surround and the glow all take the tint; the rock and the frame's
   shape do not change, so it still reads as the same object. */
/* The top of the ladder is told apart by the socket running hot rather than by the frame: the
   element reaches the plate's edge as an inner ring and the piece throws a glow, and the frame
   stays the dark metal every other skill wears. */
/* Legendary is a step up in the SAME language, not a different one (Dom, 2026-09-18, with a
   picture of Epoch Shift: "it's not working right for the skill").
   It had an element-colored ring drawn all the way around the inside of the plate and a wide glow
   thrown outside it, and between them the whole button read as a green rectangle: the lava rock
   went green, the frame went green, and the one piece that is supposed to carry the element, the
   socket, was lost inside it. The ring is gone and the glow is tight and dim. What says legendary
   is the three pieces every skill already has, each turned up a little: a brighter frame, a socket
   with less black over it, and some light at the edge.
   19 skills are legendary, which is most of a real loadout, and they cannot all be shouting. The
   loud tiers are mythic and the seven, and there are sixteen of those in the whole game. */
.skill.big.forged::before, .skill.big.legendary::before {
  background-image:
    linear-gradient(rgba(0, 0, 0, .34), rgba(0, 0, 0, .34)),
    linear-gradient(var(--socket-tint, var(--c)), var(--socket-tint, var(--c))),
    var(--ui-metal, none),
    linear-gradient(180deg,
      color-mix(in srgb, var(--c) 46%, #2A323C) 0%,
      color-mix(in srgb, var(--c) 52%, #10161C) 54%,
      color-mix(in srgb, var(--c) 30%, #06080B) 100%); }
.skill.big.forged, .skill.big.legendary {
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .7),
              0 0 0 1px #0B0D10,
              0 1px 9px -5px color-mix(in srgb, var(--c) 60%, transparent); }

/* Super effective used to take the frame: the same forged iron, rolled white. It is gone (Dom,
   2026-09-18: "for super effective, lets remove the white frame"). The frame belongs to rarity
   alone now, which is the only thing about a skill that never changes. A matchup changes with
   whoever is standing opposite, so it is written in the stat line where the damage it modifies
   already is, and it says how much. It also could not be seen on any of the 35 skills that carry a
   rarity edge, because every tier selector outranks it.
   The old white-metal ::after and its white halo were here. Nothing replaces them. */

/* ---- the frame, cut from the same iron (Dom, 2026-09-17: "let's use that material as the frame
   as well") ----
   A ring rather than a border, because a border-image takes one image and cannot be darkened, and
   the metal is generated gray. Two masks composited to leave only the band between the border box
   and the padding box: the middle is punched out, so this is a frame and not a pane over the
   button. 3px rather than 2, because a photograph of hammered iron needs a pixel or two to be
   anything at all; at 2 it averaged out to a flat gray line.
   It sits above the socket and the plate and below nothing: it is the edge of the object. */
.skill.big::after {
  content: ''; position: absolute; inset: 0; z-index: 3; pointer-events: none;
  padding: 3px;
  /* .72 and not .52. The cover was set before the iron was generated, against a painted ramp that
     was already dark; a photograph of gray metal under a half-strength cover came out mid-gray, so
     every frame in the game read as a bright edge and super effective had nothing left to be
     brighter THAN. Dark metal is dark. */
  background-image: linear-gradient(rgba(0, 0, 0, .72), rgba(0, 0, 0, .72)),
                    var(--ui-metal, none),
                    linear-gradient(180deg, #4C5561 0%, #262D36 48%, #11151A 100%);
  background-size: auto, cover, auto; background-position: center, center, center;
  background-repeat: no-repeat;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude; }
/* The top of the ladder polishes its edge: less black over the same iron. */
.skill.big.forged::after, .skill.big.legendary::after {
  background-image: linear-gradient(rgba(0, 0, 0, .52), rgba(0, 0, 0, .52)),
                    var(--ui-metal, none),
                    linear-gradient(180deg, #8792A0 0%, #49525F 48%, #1B2026 100%); }
/* ---- standing on your own ground takes the FRAME (Dom, 2026-09-18) ----
   "We change the on its own field to a colored metal frame edge, similar to what we are doing with
   mythic and the gold moves."
   It was a ring and a glow, which is the same language the glow layer now speaks for every skill,
   so the one state worth a fifth more damage was saying it the same way as everything else. The
   element takes the iron instead, and travels: the same five-stop sweep and the same keyframes as
   the mythic rule, in one color rather than three, so the frame reads as the element running
   around the edge of the tool.
   Mythic and the seven keep their own frames, because their rules come after this one: a mythic
   standing on its field is still mythic. */
.skill.big.onfield::after {
  background-image: linear-gradient(rgba(0, 0, 0, .28), rgba(0, 0, 0, .28)),
                    var(--ui-metal, none),
                    linear-gradient(90deg,
                      color-mix(in srgb, var(--c) 72%, #0B0D10) 0%,
                      var(--c) 24%,
                      color-mix(in srgb, var(--c) 55%, #FFFFFF) 42%,
                      var(--c) 62%,
                      color-mix(in srgb, var(--c) 72%, #0B0D10) 100%),
                    linear-gradient(180deg, var(--c) 0%, color-mix(in srgb, var(--c) 60%, #0B0D10) 100%);
  background-blend-mode: normal, soft-light, normal, normal;
  /* Its own keyframes now: the mythic rule moved to pixels when its bands went diagonal, and a
     half-box tile driven by a percentage is still the right loop for a single color running
     straight round an edge. */
  background-repeat: no-repeat, no-repeat, repeat-x, no-repeat;
  background-size: auto, cover, 50% 100%, auto;
  animation: fieldRule 5.5s linear infinite; }
@keyframes fieldRule {
  from { background-position: center, center, 0% 0, center; }
  to   { background-position: center, center, 100% 0, center; } }
.skill.big.onfield {
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--c) 60%, transparent),
              inset 0 2px 5px rgba(0, 0, 0, .6),
              0 0 0 1px #0B0D10; }
@media (prefers-reduced-motion: reduce) { .skill.big.onfield::after { animation: none; } }

.skill.big.forged.hot::after {
  /* Gold, not white (Dom, 2026-09-18: "give these a gold frame and a gold glow"). Literal values
     and not var(--gold), which aliases onto the blood ramp in this palette and would have made the
     seven biggest cards in the game red. The tint is blended `color` so the iron's hammer marks
     survive it, exactly as the mythic tri-hue does. */
  background-image: linear-gradient(rgba(255, 255, 255, .12), rgba(255, 255, 255, .12)),
                    var(--ui-metal, none),
                    linear-gradient(180deg, #FFE9A8 0%, #D9A441 46%, #5C4318 100%);
  background-blend-mode: normal, soft-light, normal;
  background-size: auto, cover, auto; }
/* A skill you cannot pay for has a dead edge, the same way its plate goes flat, and the edge stops
   moving with it (Dom, 2026-09-18: "no glow or animation when mythics and legendarys are not
   affordable"). `animation: none` matters beyond taste here: the flat edge is three layers and the
   travelling rules are written for four, so a disabled frame was still running keyframes whose
   positions no longer lined up with the layers underneath them. */
.skill.big:disabled::after {
  animation: none;
  background-image: linear-gradient(rgba(0, 0, 0, .84), rgba(0, 0, 0, .84)),
                    var(--ui-metal, none),
                    linear-gradient(180deg, #3A424D 0%, #232A33 50%, #171C22 100%); }

/* ---- mythic: the nine signatures ----
   A mythic skill is the one thing on a Titan nobody can buy, and it is the only place in the
   interface where rarity outranks element: the card's mythic gem is violet whatever the Titan is,
   and so is this edge. The socket stays the element's own metal, because the skill still belongs to
   an element and the stone in it is that element's crystal; what violet does is ring the piece.
   Two glows, one tight and one wide: a tight one alone reads as a border and a wide one alone as a
   smudge, and the pair reads as something lit from inside.
   The sheen is the only animation on a skill button. It is slow, it is a single pass of light down
   the plate every six seconds, and it is off entirely for anyone who asked for less motion. */
/* The mythic card's tri-hue rule, on the frame, travelling (Dom, 2026-09-18: "can mythic have the
   gradient glow that we put on the card shield, but have it animate?"). Same four stops as
   .tcard.t5 .tc-rrule: violet into magenta into cyan and back to violet, which is the one
   rule-break the card system allows itself.
   It moves by sliding its own background, not by moving anything on the page: one composited
   property, no layout, no paint of the plate underneath. Slow enough to read as a sheen on metal
   rather than as a marquee, and the gradient ends where it begins so the loop has no seam. */
.skill.big.mythic::after {
  /* The tri-hue sits ON the iron, blended `color`, not under it. Under it was the bug (Dom,
     2026-09-18: "the mythic gradient glow is not coming through, it just looks purple. I also do
     not see any movement"): a photograph is opaque and covers every layer below it, so the
     gradient was never drawn and the animation was sliding something nobody could see. `color`
     takes the hue from the gradient and the light and shade from the iron, the same trick the
     socket uses, and it is the hue that travels. */
  /* The gradient is the COLOR and the iron is a texture laid over it, not the other way round.
     Blended `color` onto gray metal, every hue came back at the metal's own mid luminance: cyan
     went gray-teal, violet went muted, and the frame read as one flat purple (Dom, 2026-09-18:
     "the mythic glow is great but it doesnt look correct"). This way the four stops keep their
     saturation and the hammer marks ride on top at soft-light. */
  /* The hues are ANODIZED, not neon (Dom, 2026-09-18: "the mythic glow is now a bit to bright, it
     also still does not look correctly"). The card's stops are #C46BFF, #FF3DA6 and #3DF2FF at
     full saturation, which is right on a 4px bar seen once; run all the way round a frame they
     read as an LED strip, and no amount of black over them fixes that, because the problem is
     saturation and not brightness. These are the same three hues pulled back toward metal, so the
     frame reads as oil on steel. Violet holds three of the five stops and the other two pass
     through it, rather than four hues in even thirds. */
  /* Three quarters of the way back to the card's neon (Dom, 2026-09-18: "maybe we go back to neon
     but like 75% of what it was"). Each stop is the anodized value walked 75% toward the card's
     own #C46BFF / #FF3DA6 / #3DF2FF, which keeps the frame reading as lit metal rather than as an
     LED strip while putting most of the color back. The cover came down with it, .2 to .12. */
  background-image: linear-gradient(rgba(0, 0, 0, .12), rgba(0, 0, 0, .12)),
                    var(--ui-metal, none),
                    repeating-linear-gradient(140deg,
                      #B769EC 0, #E9439E 99.63px, #B769EC 199.26px, #43DAE7 298.9px, #B769EC 398.53px),
                    linear-gradient(180deg, #DCC3F2 0%, #B769EC 44%, #4A2570 100%);
  background-blend-mode: normal, soft-light, normal, normal;
  /* WIDE BANDS ON A DIAGONAL (Dom, 2026-09-18: "can we widen the bands and put them on an angel so
     the top doesnt match the bottom"). At 90 degrees every column of the frame was one color, so
     the top edge and the bottom edge were always showing the same thing; on a lean they are reading
     different parts of the sweep, and the bands are twice as wide with it.
     The loop is exact rather than approximate, and the arithmetic is the whole trick. A repeating
     gradient at angle A repeats every L along its own axis, and shifting the background by dx
     shifts its phase by dx * sin(A). With A = 140deg, sin A = 0.64279, so a 620px tile carries
     exactly 398.53px of gradient: tile edges meet in phase, and moving one whole tile per cycle
     lands on the identical pattern. Nothing to see at the wrap and nothing to see at the joins.
     The lean is what makes the top edge and the bottom edge disagree, and by how much is
     height * cot(A): 67px across a 56px ring, so the two are more than half a band out of step.
     Each band is 100px measured across the sweep and 155px along the top edge.
     The cycle is 10s rather than 7 only to hold the speed: a wider tile covered in the same time
     would travel half again as fast, and 620px over 10s is 62px a second against the 60 it was.
     Pixels and not percentages here, because a percentage position is proportional to the BOX and
     the buttons are not all one width. */
  background-repeat: no-repeat, no-repeat, repeat, no-repeat;
  background-size: auto, cover, 620px 100%, auto;
  animation: mythicRule 10s linear infinite; }
@keyframes mythicRule {
  from { background-position: center, center, 0 0, center; }
  to   { background-position: center, center, 620px 0, center; } }
/* The tier rules are all more specific than the generic disabled edge, so stopping the travel takes
   saying so tier by tier. An unaffordable mythic keeps its ring, because that is the card you are
   banking three turns for and you should be able to see what you are banking for, but it holds
   still: motion is the part that reads as "cast me". */
.skill.big.mythic:disabled::after,
.skill.big.onfield:disabled::after,
.skill.big.forged.hot:disabled::after,
.skill.big.legendary:disabled::after { animation: none; }
@media (prefers-reduced-motion: reduce) { .skill.big.mythic::after { animation: none; } }
.skill.big.mythic::before {
  background-image:
    linear-gradient(rgba(0, 0, 0, .28), rgba(0, 0, 0, .28)),
    linear-gradient(var(--socket-tint, var(--c)), var(--socket-tint, var(--c))),
    var(--ui-metal, none),
    linear-gradient(180deg,
      color-mix(in srgb, var(--c) 46%, #2A323C) 0%,
      color-mix(in srgb, var(--c) 52%, #10161C) 54%,
      color-mix(in srgb, var(--c) 30%, #06080B) 100%); }
/* Mythic keeps two glows, because it is the one button allowed to be the loudest thing on the
   screen, but it lost the inside ring for the same reason legendary did: a line all the way around
   the plate reads as a colored button rather than as a lit one. The tight glow hugs the frame, the
   wide one carries past it. */
.skill.big.mythic {
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .7),
              0 0 0 1px #0B0D10,
              0 0 9px -2px rgba(196, 107, 255, .7),
              0 2px 24px -6px rgba(196, 107, 255, .5); }
.skill.big.mythic .skill-name { color: #EFE3F8; }
/* The sheen was here: one slow pass of light down a mythic plate every six seconds. Dom, 2026-09-18:
   "I don't like the sheen." Nothing on a skill button moves now. What a mythic has instead is two
   violet glows, and the seven have a white one; both are still, and still louder than anything
   else on the screen. */
/* A mythic or one of the seven that you cannot pay for keeps its plate and its frame and loses
   every light: no violet halo, no gold one, nothing breathing. The glow is what says "you can cast
   this", so an unaffordable card that still glowed was making the wrong promise, and the two loudest
   tiers were making it loudest. Dark and still, then, until the energy is there. */
.skill.big.mythic:disabled, .skill.big.forged.hot:disabled, .skill.big.legendary:disabled {
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, .55), 0 0 0 1px #0B0D10; }
.skill.big.mythic:disabled .skill-glow,
.skill.big.forged.hot:disabled .skill-glow,
.skill.big.legendary:disabled .skill-glow { animation: none; opacity: 0; }
/* The two loud tiers keep their own rock as well, one shade less buried, because these are the
   cards you are actually saving for and they should still look like something worth the wait. */
.skill.big.forged.hot:disabled, .skill.big.mythic:disabled {
  background-image: linear-gradient(rgba(0, 0, 0, .58), rgba(0, 0, 0, .58)),
                    var(--ui-skill, var(--ui-btn, none)), linear-gradient(180deg, #1B2028, #10141A);
  background-size: auto, cover, auto; background-position: center, center, center; }

/* ---- two metals that were the same metal ----
   The socket blends the element's color onto the iron with `color`, which carries HUE and
   SATURATION and nothing else. Tempest is #FFC53D and Empyrean is #FFEFA8: 45 degrees of hue
   against 48, which is three degrees apart, so the two came out the same dull gold and the only
   thing telling them apart was the crystal (Dom, 2026-09-18: "tempest needs to be a brighter
   yellow metal, its too close to empyrean").
   `--socket-tint` is the socket's tint and nothing else. The element's own color is untouched
   everywhere it means something: the card, the gem, the chart, the battle. This is one metal being
   corrected, not a palette change. */
.skill.big[data-el="tempest"]  { --socket-tint: #FFE01A; }   /* a true bright yellow, fully saturated */
.skill.big[data-el="empyrean"] { --socket-tint: #FFF4CF; }   /* pale, nearly white gold */

/* ---- the socket: the element's metal, taken down ----
   A band down the left end of the plate, the width of the padding the stone already needed, in the
   element's color mixed into iron and then darkened: bright enough to name the element at a glance
   in a list of six, dark enough that it is a piece of the button and not a swatch on it. A shadow
   down its inner edge and a hairline of light on the outside is what makes it read as a separate
   piece of metal set into the plate rather than as paint. */
.skill.big::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 42px; z-index: 0;
  pointer-events: none;
  /* Four layers, top to bottom: a black cover that takes the whole thing down, the element as a
     flat color blended `color` (which keeps the hue and saturation of the element and the LIGHT AND
     SHADE of what is under it, so hammer marks and scratches survive being tinted), the photograph
     of gray iron, and the old gradient as what a build with no art still gets.
     One image in gray, colorized nine ways, rather than nine images: the tint has to follow --c,
     which is set per button at render time, and nine files would be nine things to keep in step. */
  /* TWO DARKENERS WERE STACKING (Dom, 2026-09-18: "the yellow metal and the crystal seem dark").
     The cover was a flat 46% black over the whole band, and under it `color` blending takes hue and
     saturation from the element but LUMINOSITY FROM WHAT IS BELOW, which is a photograph of dark
     forged iron. So a fully saturated #FFE01A inherited dark iron's brightness, came out olive, and
     then had nearly half its remaining light taken off the top.
     The cover is 18% now, and a white layer blended `soft-light` sits between the tint and the iron
     to lift the iron's midtones before the tint reads them. Soft-light rather than `screen` because
     it raises the middle without flattening the hammer marks, which were the point of using a
     photograph at all. */
  background-image:
    linear-gradient(rgba(0, 0, 0, .18), rgba(0, 0, 0, .18)),
    linear-gradient(var(--socket-tint, var(--c)), var(--socket-tint, var(--c))),
    linear-gradient(rgba(255, 255, 255, .42), rgba(255, 255, 255, .42)),
    var(--ui-metal, none),
    linear-gradient(180deg,
      color-mix(in srgb, var(--c) 46%, #333D49) 0%,
      color-mix(in srgb, var(--c) 54%, #1A222C) 54%,
      color-mix(in srgb, var(--c) 30%, #0A0E14) 100%);
  background-blend-mode: normal, color, soft-light, normal, normal;
  background-size: auto, auto, auto, cover, auto;
  background-position: center, center, center, center, center;
  background-repeat: no-repeat;
  box-shadow: inset -1px 0 2px rgba(0, 0, 0, .75), inset 0 1px 0 rgba(255, 255, 255, .12); }
/* The stone rides on the socket, so it has to be above it. */
.skill.big .skill-ic, .skill.big .skill-key { z-index: 1; }

/* ---- every skill burns in its own element, by level (Dom, 2026-09-18) ----
   "Each skill has the element glow, and the brightness is based on its skill level."
   Its own layer, so what varies is OPACITY and nothing else: one --glow per button, set in
   battle.js off the skill's level, and the same number drives both the still glow and the two
   ends of the breathing one. An animated box-shadow would re-rasterize the button's shadow every
   frame; opacity on a layer is carried by the compositor.
   The layer sits under everything at z-index 0 and throws its shadow OUTSIDE the button, which is
   why a skill button must never clip. */
.skill.big .skill-glow {
  position: absolute; inset: 0; z-index: 0; pointer-events: none; border-radius: inherit;
  opacity: var(--glow, .3);
  /* Two shadows, not one (Dom, 2026-09-18: "we gotta make the glow brighter"). A single soft halo
     spends all its light at the blur's outer end, where it is thinnest; a wide one for the haze and
     a tight one hard against the edge give the same color somewhere to be solid. Raising the
     opacity alone could not do this, because the top of the ladder was already near 1. */
  box-shadow: 0 0 24px 3px color-mix(in srgb, var(--c) 92%, transparent),
              0 0 9px 0 color-mix(in srgb, var(--c) 80%, transparent); }
/* The two loud tiers burn in their own color rather than their element's, and breathe. Their
   --glow is pinned high in battle.js, because a mythic signature is level 1 and would otherwise be
   the faintest thing on the screen. */
.skill.big.forged.hot .skill-glow {
  box-shadow: 0 0 24px 3px rgba(232, 180, 70, .92), 0 0 9px 0 rgba(232, 180, 70, .8);
  animation: skillGlow 3.6s ease-in-out infinite; }
.skill.big.mythic .skill-glow {
  box-shadow: 0 0 27px 4px rgba(196, 107, 255, .92), 0 0 10px 0 rgba(196, 107, 255, .8);
  animation: skillGlow 3.6s ease-in-out infinite; }
@keyframes skillGlow {
  0%, 100% { opacity: calc(var(--glow, .5) * .42); }
  50%      { opacity: var(--glow, .5); } }
/* A skill you cannot pay for keeps its edge but stops breathing, and burns lower: the glow is an
   invitation and the pulse is the loudest part of it. */
.skill.big:disabled .skill-glow { animation: none; opacity: calc(var(--glow, .3) * .4); }
@media (prefers-reduced-motion: reduce) {
  .skill.big .skill-glow { animation: none; opacity: calc(var(--glow, .5) * .8); } }

/* ---- the stone at the left end ---- */
.skill.big .skill-ic {
  position: absolute; left: 10px; top: 50%; right: auto; transform: translateY(-50%);
  width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center; opacity: 1;
  /* The stone is 26px across and the inner shadow was reaching 6px up from the bottom of it, so a
     quarter of the face was in shade before the picture had a chance. Softer, and the whole stone
     comes up a little, which a generated crystal needs more than a drawn gradient did. */
  filter: brightness(1.14) saturate(1.06);
  box-shadow: inset 0 1px 2px rgba(255, 255, 255, .45), inset 0 -2px 5px rgba(0, 0, 0, .38),
              0 0 0 1px #0B0D10, 0 0 0 2px #8A94A6, 0 0 10px -2px var(--gem); }
/* The keyboard number, out of the stone's way, and now in the BOTTOM corner: it traded places with
   the detail control (Dom, 2026-09-18: "lets swap the keyboard short cut and the ? positions").
   The control is the thing you read before you know the button, so it takes the corner your eye
   reaches first; the shortcut is the thing you use once you already know it. */
.skill.big .skill-key { left: auto; right: 8px; top: auto; bottom: 8px; width: 16px; height: 16px;
  font-size: .64rem; background: rgba(11, 13, 16, .72); color: var(--st-body);
  box-shadow: inset 0 0 0 1px rgba(138, 148, 166, .35); }

@media (max-width: 760px) {
  .skill.big { padding: 7px 10px 7px 40px; }
  .skill.big::before { width: 37px; }
  .skill.big .skill-ic { width: 23px; height: 23px; left: 7px; }
  .skill.big .skill-key { right: 6px; top: 5px; width: 14px; height: 14px; font-size: .56rem; }
}

/* ---- the seven at the top of the ladder ----
   "Any skill above a 5 energy would get an elemental colored metal" turned out to name exactly
   seven skills, and every one of them is neutral: Rally, Upheaval, Sunder, Turn the Tide,
   Ruination, Last Word, World's End. There is no element to cast them in, and tinting them with
   the neutral gray would have made the seven biggest cards in the game the least remarkable
   plates on the screen. Dom's call (2026-09-17): keep the threshold, give the seven their own
   look. White hot steel, and a white stone, because these are the ones that belong to no element
   and answer to all nine.
   The colors are literals rather than var(--c): --c is set inline on the button from the skill's
   element, and an inline custom property beats a stylesheet rule every time. Only .skill-ic's
   --gem can be reached from here, which is why the stone is set there and the plate is not. */
/* GOLD, not white (Dom, 2026-09-18: "give these a gold frame and a gold glow"). The seven answer
   to no element, so gold stands in for the element they do not have. The tint is blended `color`
   over the iron so the hammer marks survive it, the same way the mythic tri-hue and the element
   socket do.
   Every gold here is a literal: `var(--gold)` aliases onto the blood ramp in this palette, and the
   seven biggest cards in the game are not red. */
.skill.big.forged.hot::before {
  background-image:
    linear-gradient(rgba(0, 0, 0, .22), rgba(0, 0, 0, .22)),
    var(--ui-metal, none),
    linear-gradient(180deg, #FFE9A8 0%, #D9A441 50%, #4A3614 100%);
  background-blend-mode: normal, soft-light, normal; }
.skill.big.forged.hot {
  background-image: linear-gradient(rgba(0, 0, 0, .46), rgba(0, 0, 0, .46)),
                    var(--ui-skill, var(--ui-btn, none)), linear-gradient(180deg, #1B2028, #10141A);
  box-shadow: inset 0 1px 0 rgba(255, 233, 168, .3), inset 0 2px 5px rgba(0, 0, 0, .55),
              0 0 0 1px #0B0D10,
              0 0 9px -2px rgba(232, 180, 70, .75),
              0 2px 22px -6px rgba(232, 180, 70, .5); }

/* A gold stone, for the same reason: no element to cut it from. */
.skill.big.forged.hot .skill-ic { --gem: #F7D98A;
  box-shadow: inset 0 1px 2px rgba(255, 255, 255, .55), inset 0 -2px 6px rgba(0, 0, 0, .45),
              0 0 0 1px #0B0D10, 0 0 0 2px #D9A441, 0 0 12px -2px rgba(232, 180, 70, .8); }

/* ---- the team strip, on a desktop (Dom, 2026-09-18) ----
   "The team section needs to be bigger on desktop, and sticky when you are scrolling."
   It was a 46px portrait in a 100px strip on a 2000px screen, which is the same strip a phone
   gets, and it scrolled away the moment you started looking at cards: the one panel on the screen
   you are comparing everything AGAINST was the first thing to leave.
   Sticky under the header rather than fixed, so it still belongs to the page: it scrolls up with
   the heading, comes to rest below the header, and the cards pass underneath. `--hdr-h` already
   carries the safe-area inset, so this needs no extra arithmetic.
   z-index 4 clears the card frame's inlays, which sit at 3 in the root stacking context because a
   position: relative card with z-index auto does not make one of its own. The header is far above
   both and stays there. */
@media (min-width: 761px) {
  .team-bench {
    /* Flush under the header: the 6px it used to sit below it was a strip the cards showed
       through as they scrolled (Dom, 2026-09-22: "There is a gap at the top of the team frame"). */
    position: sticky; top: var(--hdr-h); z-index: 4;
    padding: 16px 18px 18px; gap: 14px;
    /* A sticky panel has to be opaque, and the shadow under it is what makes a card pass BEHIND
       rather than through. */
    box-shadow: inset 0 1px 0 rgba(214, 224, 236, .1), inset 0 -1px 0 rgba(0, 0, 0, .6),
                0 0 0 1px rgba(0, 0, 0, .55), 0 16px 26px -16px rgba(0, 0, 0, .95); }
  .team-bench.over {
    box-shadow: inset 0 1px 0 rgba(214, 224, 236, .1), inset 0 -2px 0 var(--bl-body),
                0 0 0 1px rgba(0, 0, 0, .55), 0 16px 26px -16px rgba(0, 0, 0, .95); }
  .team-bench .team-slot { padding: 8px 10px 8px 8px; }
  .team-bench .team-slot.empty { padding: 22px 12px; font-size: .9rem; }
  .ts-open { gap: 12px; }
  .ts-open img { width: 72px; height: 72px; }
  .ts-name { font-size: 1.04rem; }
  .ts-sub { font-size: .78rem; }
  .team-label { font-size: 1.04rem; }
  .team-bench .ts-btn { height: 34px; min-width: 34px; font-size: .78rem; padding: 0 12px; }
  .team-bench .ts-x { width: 34px; font-size: 1.2rem; padding: 0; }
  .ts-lead { font-size: .7rem; padding: 4px 10px; }
}
/* Sticky only where there is room for it. Three full cards make the panel about 420px tall, and on
   a 768px-high window that pinned panel covered most of the collection it was meant to sit above
   (blind playtest round 1, 2026-09-23). Below 860px of height it scrolls away like the rest. */
@media (min-width: 761px) and (max-height: 859px) {
  .team-bench { position: relative; }
}

/* ---- a fight gets the screen to itself ----
   Dom, 2026-09-18, with a phone screenshot: "when you are in a battle the top UI with credits and
   what not, and bottom nav do not need to be there." Credits, essence, ether and rating are
   nothing you can act on mid-fight, and on a phone they and the tab bar were taking a fifth of the
   height off an arena that is the whole point of the screen.
   Done by zeroing the two variables rather than by writing a second set of rules: everything in
   this stylesheet that positions itself against --hdr-h or --nav-h, including the arena's own
   height calc and the toasts, reclaims the space on its own.
   The nav goes only on a phone, where it is a bar across the bottom. On a desktop it is a rail
   down the side taking nothing from the arena, and removing the only way out of a screen to gain
   space that was never contested would be a bad trade. */
body.in-battle { --hdr-h: 0px; }
body.in-battle .hdr { display: none; }
@media (max-width: 760px) {
  body.in-battle { --nav-h: 0px; }
  body.in-battle .nav { display: none; }
}
/* The battle's own mute, standing in for the header's. */
.battle-top .mute-btn { margin-left: 6px; }

/* ---- the slot: a skill, and the whole truth folded under it ----
   Dom, 2026-09-18: "I also want to add a full description button to all the skills", opening in
   place rather than in a modal. The detail cannot live INSIDE the skill button, because a button
   inside a button is not valid HTML and the inner one stops being clickable, so each skill now sits
   in a slot: the button, a "?" over its corner, and the panel beneath. The slot is what the grid
   lays out, which is why the margin rule below moved onto it. */
.skill-slot { min-width: 0; display: flex; flex-direction: column; }
/* The "?" is anchored to the BUTTON and not to the slot. Anchored to the slot it rode the slot's
   bottom edge, which is the bottom of the OPEN PANEL once one is open, and in a stretched grid row
   it is the bottom of whatever the tallest neighbour happens to be. */
.skill-head { position: relative; display: flex; min-width: 0; container-type: inline-size; }
.skill-head > .skill.big { width: 100%; }
/* Rows size to their own content now. They used to stretch every button in a row to the tallest
   one, which was tidy until a panel opened: the row grew by the whole panel and every button
   beside it grew with it. */
.skill-grid { align-items: start; }
/* It says what it does (Dom, 2026-09-18: "lets make the ? see full detail"). A question mark is a
   guess until you press it once, and this is the only control on the button that is not the skill
   itself, so it can afford to be spelled out. Top corner, above the frame ring at z-index 3. */
.skill-more { position: absolute; right: 12px; top: 12px; z-index: 4;
  height: 18px; padding: 0 8px; border: 0; border-radius: 9px; cursor: pointer; white-space: nowrap;
  display: grid; place-items: center; line-height: 1; font-size: .56rem; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--st-body); background: rgba(11, 13, 16, .72);
  box-shadow: inset 0 0 0 1px rgba(138, 148, 166, .35);
  transition: color var(--dur-hover-out) var(--ease-standard, ease),
              background-color var(--dur-hover-out) var(--ease-standard, ease),
              box-shadow var(--dur-hover-out) var(--ease-standard, ease); }
.skill-more:hover, .skill-more:focus-visible {
  color: var(--st-bright); background: rgba(11, 13, 16, .9);
  box-shadow: inset 0 0 0 1px rgba(214, 224, 236, .6);
  transition-duration: var(--dur-hover-in); }
.skill-slot.open .skill-more { color: var(--st-bright); background: rgba(11, 13, 16, .92);
  box-shadow: inset 0 0 0 1px rgba(214, 224, 236, .55); }
/* Spelled out wherever there is room for it, and back to a question mark where there is not. The
   battle HUD puts these in two columns of a narrow side panel and a phone is narrower still; at
   150px a 90px label would have left the skill's own name about four characters. The query is on
   the BUTTON's width rather than the viewport's, because the same button is wide on the bench, in
   the Skill Store and on a desktop battle, and narrow on a phone. */
/* Three widths, because the battle HUD's own columns land at about 194px and the full label needs
   270. Rather than let a fight never see the words at all, the middle band keeps one of them. */
@container (max-width: 270px) {
  .skill-more { font-size: 0; padding: 0 7px; }
  .skill-more::before { content: 'Detail'; font-size: .56rem; letter-spacing: .08em; }
  .skill.big .skill-name, .skill.big .skill-desc { padding-right: 63px; } }
@container (max-width: 178px) {
  .skill-more { width: 20px; height: 20px; padding: 0; border-radius: 50%; }
  .skill-more::before { content: '?'; font-size: .72rem; letter-spacing: 0; }
  .skill.big .skill-name, .skill.big .skill-desc { padding-right: 33px; } }
/* Same fold the conjuring elements use: 0fr to 1fr is the only transition that can run to a height
   the content decides, and the inner box has to be able to clip while it does. */
/* The panel is a piece of the button's own plate carried on below it, with the element's color down
   the left edge, so it reads as part of the skill rather than as loose text in the grid. */
.skill-detail { display: grid; grid-template-rows: 0fr; opacity: 0; border-radius: 0 0 6px 6px;
  transition: grid-template-rows var(--dur-panel, 300ms) var(--ease-standard, ease),
              opacity 160ms linear; }
.skill-detail > .sd-in { min-height: 0; overflow: hidden; display: flex; flex-direction: column;
  gap: 6px; padding: 0 12px; transition: padding var(--dur-panel, 300ms) var(--ease-standard, ease); }
.skill-slot.open .skill-detail { grid-template-rows: 1fr; opacity: 1;
  background: linear-gradient(180deg, rgba(20, 24, 30, .96), rgba(10, 13, 17, .96));
  box-shadow: inset 0 0 0 1px rgba(138, 148, 166, .22),
              inset 3px 0 0 color-mix(in srgb, var(--c, #8A94A6) 70%, transparent); }
.skill-slot.open .skill-detail > .sd-in { padding: 10px 12px 12px; }
.sd-row { font-size: .76rem; line-height: 1.45; color: var(--text-body); }
.sd-row b { color: var(--st-bright); font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; font-size: .66rem; }
.sd-list { margin: 0; padding-left: 15px; display: flex; flex-direction: column; gap: 3px;
  font-size: .76rem; line-height: 1.45; color: var(--text-body); }
.sd-list li::marker { color: var(--c, var(--st-light)); }
@media (prefers-reduced-motion: reduce) {
  .skill-detail, .skill-detail > .sd-in { transition: none; } }

/* The matchup, said rather than drawn. A frame could be outranked by a rarity frame and could
   never say how much; two words and a number can do both, and they sit in the line that already
   carries the damage they are modifying. */
.skill-sub .mx { font-weight: 700; letter-spacing: .06em; margin-left: 9px; }
.skill-sub .mx.super { color: var(--st-bright); }
.skill-sub .mx.weak { color: #6E7887; }

/* ---- the fight, with everything that is not the fight taken out ----
   Dom, 2026-09-18, from a landscape phone: "that nav on the left should go away. I also want to
   get rid of the log box everywhere. Let's make skills full width. The quick keys can go away on
   mobile." Last in the file on purpose: .battle-log and .battle-hud are each set by half a dozen
   rules inside media queries further up, and this has to win against all of them without a
   specificity fight in every one. */

/* The log is GONE FROM THE LAYOUT and still there for a screen reader. It carries aria-live, so
   `display: none` would take the running commentary away from the people who rely on it most;
   clipped to a pixel, it still announces and takes no room. The code that writes to it needs no
   change and the panel is one rule away from coming back. */
.battle-log {
  position: absolute; width: 1px; height: 1px; min-height: 0; max-height: none;
  padding: 0; margin: -1px; border: 0; background: none; overflow: hidden; clip-path: inset(50%);
  -webkit-mask-image: none; mask-image: none;
}
/* Which leaves the skills the whole width, on every screen: the HUD was two columns with the log
   in the first one. */
.battle-hud { grid-template-columns: 1fr; }

/* The side rail is a desktop idea. On a phone held sideways it was taking a fifth of the width of
   a fight, which is the worst place in the game to spend it. Touch or a short viewport, either
   one: a landscape phone is wide enough to miss a width query and is unmistakable by height. */
@media (max-width: 760px), (pointer: coarse), (max-height: 620px) {
  body.in-battle { --nav-h: 0px; --nav-w: 0px; }
  body.in-battle .nav { display: none; }
}

/* Nothing to press a number ON. The keyboard shortcut badge is meaningless on a touch device, and
   the space it reserves in the corner of every skill is not. */
@media (pointer: coarse) {
  .skill.big .skill-key { display: none; }
  .skill.big .skill-sub { padding-right: 0; }
}

/* ---- the zone map (Dom, 2026-09-19: "I want to progress on an actual map for each zone") ----
   A zone's twenty-seven fights are one road across one picture: the marks are absolutely
   positioned in percentages and the road is an SVG stretched over the same box, so the two hold
   register at every size. Nothing here is laid out by the grid; the geometry decides. ---- */
.zmap-head { display: grid; grid-template-columns: auto 1fr auto; align-items: center;
  gap: 10px 14px; margin-bottom: 10px; }
.zmap-back { grid-row: 1; display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  padding: 7px 11px; border: 0; border-radius: 0; font-family: var(--font); font-size: .76rem;
  letter-spacing: .08em; text-transform: uppercase; color: var(--muted);
  background: linear-gradient(180deg, #1C212A, #151A21);
  box-shadow: inset 0 0 0 1px #39414D, inset 0 1px 0 rgba(217, 223, 230, .07); }
.zmap-back:hover, .zmap-back:focus-visible { color: var(--text); outline: none;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--e, var(--st-light)) 50%, #39414D), inset 0 1px 0 rgba(217, 223, 230, .13); }
.zmap-title h1 { margin: 0; font-size: 1.3rem; }
.zmap-title p { margin: 0; font-size: .8rem; }
.zmap-prog { text-align: right; line-height: 1.15; }
.zmap-prog b { display: block; font-size: 1.15rem; color: var(--st-light);
  font-variant-numeric: lining-nums tabular-nums; }
.zmap-prog span { font-size: .68rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }

/* The board. It is sized to the room that is left rather than to a ratio, because the whole
   point of a map is seeing the road at a glance: an aspect-ratio box was 820px tall in a 814px
   window and the last third of every zone was below the fold. A phone therefore gets a tallish
   box and a desktop a wide one, and the road is generated for whichever shape it turns out to
   be rather than drawn once and stretched into it. */
/* LOCKED TO THE PLATE'S OWN 3:2 (Dom, 2026-09-19). The roads are read off the pictures now, so
   their coordinates are fractions of the plate: any other box shape would crop the art with
   `cover` and slide every stop off the part of it you can see. The width is capped by the room
   left below the chrome rather than the height being capped, so the ratio is never broken. */
.zmap { position: relative; overflow: hidden; margin-inline: auto; aspect-ratio: 3 / 2;
  container-type: inline-size;
  width: min(100%, calc((100dvh - var(--hdr-h, 56px) - var(--nav-h, 62px) - 178px) * 1.5));
  min-width: min(100%, 280px);
  background: radial-gradient(120% 90% at 50% 20%, color-mix(in srgb, var(--e) 16%, transparent), transparent 68%),
    linear-gradient(180deg, var(--panel2), var(--panel) 55%, var(--sunken));
  box-shadow: inset 0 2px 0 color-mix(in srgb, var(--e) 75%, transparent), inset 0 3px 0 rgba(0, 0, 0, .5),
              inset 0 0 0 1px rgba(0, 0, 0, .55), 0 18px 36px -22px rgba(0, 0, 0, .65); }
/* The plate, the road and the marks are one stage, scaled and moved as a whole by the map's
   camera (app.js screens.zonemap), so they cannot slip out of register when you zoom. */
.zmap-stage { position: absolute; inset: 0; transform-origin: 0 0; will-change: transform; }
.zmap.has-art .zmap-stage { background-image:
  linear-gradient(180deg, rgba(8, 9, 11, .34), rgba(8, 9, 11, .1) 32%, rgba(8, 9, 11, .5)),
  var(--zone-map);
  background-size: cover, cover; background-position: center, center; }
.zmap.zoomed { cursor: grab; touch-action: none; }
.zmap.zoomed.dragging { cursor: grabbing; }
.zmap.dragging .zspot { pointer-events: none; }
/* Zoom controls, top right of the board and outside the stage, so they stay put. */
.zmap-zoom { position: absolute; top: 10px; right: 10px; z-index: 6; display: flex; flex-direction: column; gap: 6px; }
.zmap-zbtn { width: 36px; height: 36px; display: grid; place-items: center; padding: 0; cursor: pointer;
  font: 700 1.2rem/1 var(--font); color: #FFFFFF; background: rgba(8, 9, 11, .72);
  border: 1px solid color-mix(in srgb, var(--e) 45%, rgba(255, 255, 255, .18)); }
.zmap-zbtn:hover:not(:disabled), .zmap-zbtn:focus-visible { background: rgba(8, 9, 11, .9); border-color: var(--e); outline: none; }
.zmap-zbtn:disabled { opacity: .35; cursor: default; }
.zmap-zdot { width: 12px; height: 12px; border-radius: 50%; background: var(--e); box-shadow: 0 0 0 3px rgba(8, 9, 11, .8), 0 0 10px var(--e); }
.zmap-tools:empty { display: none; }
.zmap-tools { display: flex; justify-content: flex-end; margin-top: 8px; }
.zmap-tools .zmap-zoom { position: static; flex-direction: row; gap: 6px; }
@media (max-width: 760px) { .zmap-zbtn { width: 38px; height: 34px; font-size: 1.1rem; } }
/* The zone's name is drawn on the board, not the plate, so zoomed in it would sit over stops. */
.zmap-label { transition: opacity .35s; }
.zmap.zoomed .zmap-label { opacity: 0; }

/* Three lines, one on top of the other, in this order: a dark casing so the road has an edge
   against whatever the painting puts under it, the unwalked road dashed over that, and the
   walked road solid and lit over both. Without the casing the line disappears every time it
   crosses something dark, which on a painted map is most of the way round. */
.zmap-road { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.zmap-road path { fill: none; stroke-linecap: round; stroke-linejoin: round; }
/* STRUCK METAL, NOT A PHOTOGRAPH (Dom, 2026-09-19: "keep the dashes, just metal them"). The
   dashes are three pixels wide, and three pixels is the exact width at which a texture cannot
   work: that is the lesson the conjure hoops cost. What reads as metal at this size is the same
   trick the card's element bar uses, which is a dark casing, a body, and a narrow brighter core
   along the middle of it. Four strokes rather than three, all on the same path. */
/* The casing is off (Dom, 2026-09-21: "remove the black outline around the trail"). The path is
   still drawn, because the road is four strokes on one path and dropping one from the JS would
   make the other three renumber; it simply paints nothing. The studs and the lit core carry the
   metal on their own against the plate. */
.road-case { stroke: none; }
/* Unwalked: cold iron studs set into the casing. Wider and round-capped so each dash is a stud
   rather than a tick, and a lighter top makes them sit proud. */
.road-dim { stroke: rgba(150, 163, 182, .42); stroke-width: 3.4; stroke-dasharray: 2.6 9; }
.road-dim-top { stroke: rgba(226, 234, 245, .30); stroke-width: 1.3; stroke-dasharray: 2.6 9; }
/* Walked: the same iron with the zone's light on it, and a near-white core down the middle,
   which is what turns a colored line into a lit edge. */
.road-lit { stroke: color-mix(in srgb, var(--e) 70%, #2B3038 30%); stroke-width: 5; }
.road-shine { stroke: color-mix(in srgb, var(--e) 34%, #FFF6EC 66%); stroke-width: 1.6;
  filter: drop-shadow(0 0 5px color-mix(in srgb, var(--e) 80%, transparent)); }

/* One mark per fight. Centered on its own point, which is why the translate is here and not in
   the positioning: `left` and `top` are the point, the mark hangs off it. */
/* THE MARKS SCALE WITH THE BOARD, and they have to. The roads are read off the plates now, so
   the tightest pair on any of the nine is 46 thousandths of the width apart, which is 42px on a
   906px desktop board and 17px on a 366px phone one. Fixed pixels were fine when the road was
   generated to fit whatever box it got; against a fixed road they overlap on a phone and float
   on a desktop. In container units the clearance is the same everywhere. */
/* `scale` rather than transform, so the hover grow and the camera's counter-scale compose. */
.zspot { scale: var(--ms, 1); transition: scale var(--glide, 0ms) cubic-bezier(.45, 0, .2, 1); }
.zspot { position: absolute; transform: translate(-50%, -50%); z-index: 2; display: grid;
  place-items: center; width: clamp(13px, 3.3cqw, 30px); height: clamp(14px, 3.5cqw, 32px);
  padding: 0; border: 0; border-radius: 0;
  cursor: default; background: none; color: var(--st-light); font-family: var(--font); }
/* The stops are struck metal (Dom, 2026-09-19: "can you use a metal material and color it for
   the stops"). Same recipe the skill sockets use and for the same reason: one gray photograph of
   worked iron, laid on as itself, with the zone's color soft-lit over it. Hammer marks and an
   uneven sheen are things a gradient cannot have, and one plate tinted nine ways is one image to
   keep in step instead of nine. `--e` is already on the map, so each zone strikes its own. */
/* A finger needs more than the 13px mark a phone draws (blind playtest round 1, 2026-09-23): the
   tap target reaches past the mark on touch screens without the mark itself growing and crowding
   the trail. */
@media (pointer: coarse) { .zspot::before { content: ''; position: absolute; inset: -12px; } }
.zspot .zmark { display: grid; place-items: center; width: 100%; height: 100%;
  clip-path: polygon(50% 0, 100% 16%, 100% 58%, 50% 100%, 0 58%, 0 16%);
  background-image: linear-gradient(rgba(0, 0, 0, .42), rgba(0, 0, 0, .42)),
                    linear-gradient(color-mix(in srgb, var(--e) 40%, #6C7482), color-mix(in srgb, var(--e) 40%, #6C7482)),
                    var(--ui-metal, none),
                    linear-gradient(180deg, #1A1F27, #11151B);
  background-size: auto, auto, cover, auto; background-position: center;
  background-blend-mode: normal, soft-light, normal, normal;
  box-shadow: inset 0 1px 0 rgba(217, 223, 230, .06); }
/* WHITE, IN EVERY STATE (Dom, 2026-09-21: "make the shield number and symbols white"). The mark
   sits on a painted plate whose brightness is not ours to predict, so the number carries its own
   dark halo rather than trusting the fill behind it. `color` covers the icons too: they are
   drawn with currentColor. */
.zspot { color: #FFFFFF; }
.zspot .znum { font-size: clamp(7px, 1.15cqw, .8rem); font-weight: 700; color: #FFFFFF;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .75), 0 0 1px rgba(0, 0, 0, .9);
  font-variant-numeric: lining-nums tabular-nums; }
.zspot .ic { filter: drop-shadow(0 1px 1.5px rgba(0, 0, 0, .7)); }
.zspot .ic { width: .72em; height: .72em; font-size: 1rem; }
.zspot.big { width: clamp(18px, 4.5cqw, 42px); height: clamp(19px, 4.8cqw, 45px); }
.zspot.big .ic { font-size: 1.4rem; }
/* A boss carries its name under it. It is the landmark the stretch is walked toward, and a map
   whose landmarks are unlabeled is a pattern. */
/* Bigger and on a plate of their own (Dom, 2026-09-30: "the warden and boss names need to be
   bigger and more legible"). They were muted gray letters straight on the painting, which vanished
   over anything pale; now they are white on a dark tab edged in the zone's color. */
.zspot-name { position: absolute; top: calc(100% + 4px); left: 50%; transform: translateX(-50%);
  white-space: nowrap; font-size: clamp(9px, 1.45cqw, .92rem); font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: #FFFFFF; background: rgba(8, 9, 11, .78); padding: 2px 7px 1px; line-height: 1.25;
  border: 1px solid color-mix(in srgb, var(--e) 55%, transparent);
  text-shadow: 0 1px 2px #000; pointer-events: none; }
.zspot.big.done .zspot-name { color: color-mix(in srgb, var(--e) 45%, #FFFFFF); }
.zspot.final .zspot-name { color: #FFD9DE; border-color: var(--bl-light); font-size: clamp(10px, 1.7cqw, 1.05rem); }

.zspot.done .zmark { background-image: linear-gradient(rgba(0, 0, 0, .06), rgba(0, 0, 0, .06)),
                    linear-gradient(var(--e), var(--e)),
                    var(--ui-metal, none),
                    linear-gradient(180deg, color-mix(in srgb, var(--e) 66%, #0B0D10), color-mix(in srgb, var(--e) 36%, #0B0D10)); }
.zspot.done { color: #FFFFFF; cursor: pointer; }
/* A cleared mark can be hunted again, so it lifts and lights under the pointer the way the live one
   does (Dom, 2026-09-22), in the zone's own color and without the live one's pulse. */
/* The ring is a drop-shadow on the BUTTON: the mark is clip-pathed to its shield, which clips any
   shadow drawn on the mark itself, so a glow there would never show. */
.zspot.done:hover, .zspot.done:focus-visible { transform: translate(-50%, -50%) scale(1.18); outline: none; z-index: 4;
  filter: drop-shadow(0 0 1.5px #FFFFFF) drop-shadow(0 0 8px color-mix(in srgb, var(--e) 90%, transparent)); }
.zspot.done:hover .zmark, .zspot.done:focus-visible .zmark { filter: brightness(1.3); }
.zspot.done .znum { color: #FFFFFF; }
.zspot.far .zmark { background-image: linear-gradient(rgba(0, 0, 0, .62), rgba(0, 0, 0, .62)),
                    linear-gradient(#5A626E, #5A626E),
                    var(--ui-metal, none),
                    linear-gradient(180deg, #171B22, #0E1116); }
.zspot.far { opacity: .78; }
/* Where you are. The only mark on the map that takes a pointer, and the only one that moves. */
.zspot.now { cursor: pointer; color: #FFFFFF; z-index: 3; }
.zspot.now .zmark { background-image: linear-gradient(rgba(0, 0, 0, .10), rgba(0, 0, 0, .10)),
                    linear-gradient(color-mix(in srgb, var(--e) 55%, #FFFFFF), color-mix(in srgb, var(--e) 55%, #FFFFFF)),
                    var(--ui-metal, none),
                    linear-gradient(180deg, #3B4453, #1F252F);
  animation: zspotPulse 2.2s var(--ease-standard) infinite; }
.zspot.now .znum { color: #FFFFFF; }
.zspot.now:hover, .zspot.now:focus-visible { transform: translate(-50%, -50%) scale(1.14); outline: none; }
.zspot.now:hover .zmark, .zspot.now:focus-visible .zmark { animation: none;
  background: linear-gradient(180deg, #4E5867, #272F3A);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .22), 0 0 0 1px color-mix(in srgb, var(--e) 95%, transparent), 0 0 22px 0 color-mix(in srgb, var(--e) 70%, transparent); }
.zspot.final .zmark { box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12), 0 0 0 1px var(--bl-body); }
.zspot.final.done .zmark { background: linear-gradient(180deg, #C4364A, #7E121F); }
@keyframes zspotPulse {
  0%, 100% { box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18), 0 0 0 1px color-mix(in srgb, var(--e) 70%, transparent), 0 0 10px -3px color-mix(in srgb, var(--e) 70%, transparent); }
  50%      { box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18), 0 0 0 1px color-mix(in srgb, var(--e) 95%, transparent), 0 0 22px 0 color-mix(in srgb, var(--e) 85%, transparent); }
}
@media (prefers-reduced-motion: reduce) { .zspot.now .zmark { animation: none; } }

/* The zone's name, struck into the bottom right of the plate. Set in the display face in wide
   caps with a hard shadow, because it sits on a painting and has to hold against anything the
   painting puts under it. The road is laid out to keep clear of this corner. */
.zmap-label { position: absolute; right: 14px; bottom: 11px; z-index: 4; text-align: right;
  pointer-events: none; display: flex; flex-direction: column; align-items: flex-end; gap: 1px; }
.zmap-label b { font-family: var(--display); font-weight: 700; font-size: clamp(15px, 4.4vw, 30px);
  letter-spacing: .13em; text-transform: uppercase; color: #EEF2F7;
  text-shadow: 0 2px 10px #000, 0 0 3px rgba(0, 0, 0, .95), 0 1px 0 rgba(0, 0, 0, .8); }
.zmap-label i { font-style: normal; font-size: clamp(9px, 2.2vw, 12px); letter-spacing: .16em;
  text-transform: uppercase; color: color-mix(in srgb, var(--e) 55%, #E6ECF4);
  text-shadow: 0 1px 6px #000, 0 0 3px rgba(0, 0, 0, .95); }
@media (min-width: 761px) { .zmap-label { right: 20px; bottom: 15px; } }

.zmap-foot { margin-top: 10px; }
/* The frontier plate is the one control on this screen, so it takes the width it is given
   rather than shrinking to its own text in the corner. */
.zmap-foot .depth { width: 100%; }
/* The mark sizes that used to live here are container units now, so they follow the board at
   every width instead of stepping once at 761px. */
@media (min-width: 761px) { .zmap-foot { max-width: 520px; } }

/* ---- a zone on the Hunt list ----
   The card says where the zone is up to and opens its map. The bar is the only place a number
   this small needs a shape as well: "12/27" alone does not read as most of the way. */
.zone-prog { display: flex; align-items: center; gap: 8px; }
.zone-bar { flex: 1 1 auto; height: 5px; min-width: 0;
  background: linear-gradient(180deg, #0E1116, #151A21); box-shadow: inset 0 0 0 1px #262C35; }
.zone-bar i { display: block; height: 100%; background: linear-gradient(90deg, color-mix(in srgb, var(--e) 55%, #000), var(--e)); }
.zone-prog b { font-size: .72rem; color: var(--st-light); font-variant-numeric: lining-nums tabular-nums; }
.zone-go { cursor: pointer; }
.zone-go u { color: var(--e); }

/* The frontier, written out. It wears `.depth` because it IS the button the three depth buttons
   used to be: one fight, its level, and what it is. */
.depth.trail-go { cursor: pointer; }
.depth.boss { background: linear-gradient(180deg, rgba(176, 27, 42, .22), rgba(176, 27, 42, .06));
  box-shadow: inset 0 0 0 1px var(--bl-body), inset 0 1px 0 rgba(217, 223, 230, .07), 0 1px 2px rgba(0, 0, 0, .5); }
.depth.boss b { color: var(--st-spec); letter-spacing: .1em; }
.depth.boss u { color: var(--bl-light); text-decoration: none; }
.depth.boss:hover:not(:disabled), .depth.boss:focus-visible {
  background: linear-gradient(180deg, rgba(176, 27, 42, .34), rgba(176, 27, 42, .1));
  box-shadow: inset 0 0 0 1px var(--bl-light), inset 0 1px 0 rgba(217, 223, 230, .13), 0 2px 6px rgba(0, 0, 0, .6); }
.depth.done { background: linear-gradient(180deg, #1C212A, #151A21);
  box-shadow: inset 0 0 0 1px #39414D, inset 0 1px 0 rgba(217, 223, 230, .07), 0 1px 2px rgba(0, 0, 0, .5); }
.depth.done u { color: var(--muted); }

/* ---- a phone held sideways ----
   Every rule that made a battle fit the screen was behind `max-width: 760px`, and a phone in
   landscape is 812px WIDE. So the one posture people actually play a battler in got the desktop
   layout: measured at 812x375, the page ran to 409px in a 375px viewport, the five action
   buttons sat at y 360 with their bottom 34px off the screen, and both creatures rendered 24px
   tall in a 200px arena. You had to scroll to press your own buttons.
   Keyed on HEIGHT, because that is the thing that is actually short. Width tells you nothing
   here: this viewport is wider than the desktop breakpoint.
   Specificity matters in this block. `.arena { min-height: 0 }` loses to
   `.screen.battle.active .arena { min-height: 200px }` above, and a media query adds nothing, so
   every selector here carries the same prefix. The first attempt at this fixed 13px of the 34
   for exactly that reason. */
@media (min-width: 761px) and (max-height: 560px) {
  .screen.battle.active { height: calc(var(--vh) - var(--hdr-h) - var(--nav-h) - 34px); min-height: 0; }
  /* Each panel goes ABOVE ITS OWN CREATURE rather than the two of them stacking top and bottom.
     Stacked, they took 136px of a 209px arena and left the creatures 35px to share. Side by side
     they cost one row, and it is also the arrangement that answers "which one is mine": the
     sprites were laid out left and right while the panels were stacked up and down, so the panel
     at the top of the screen described the creature on the right. */
  .screen.battle.active .arena { flex: 1 1 auto; min-height: 0; overflow: hidden; padding: 8px;
    grid-template-rows: auto 1fr; }
  .screen.battle.active .fighter.side-0 .fighter-info { grid-column: 1; grid-row: 1; }
  .screen.battle.active .fighter.side-1 .fighter-info { grid-column: 2; grid-row: 1; }
  .screen.battle.active .fighter.side-0 .sprite-wrap { grid-column: 1; grid-row: 2; }
  .screen.battle.active .fighter.side-1 .sprite-wrap { grid-column: 2; grid-row: 2; }
  /* THE TURN STRIP COMES BACK. `@media (max-height: 740px)` hides it outright, and that was the
     right call when the panels were stacked and there was genuinely no row for it: it buys the
     creatures their band. But it is the readout for the whole clock, and the coach step that
     teaches the clock is gated on it being on screen, so a landscape player lost the mechanic
     and the lesson about it. Un-stacking the panels freed the row, and floating the strip over
     the ground it used to sit on costs nothing at all: out of flow, it takes no grid track, so
     the creatures keep every pixel. */
  .screen.battle.active .arena .order { display: flex; position: absolute; left: 50%; bottom: 2px;
    transform: translateX(-50%); z-index: 4; min-height: 0; margin: 0; padding: 1px 8px;
    max-width: calc(100% - 16px); background: rgba(9, 11, 14, .72); border-color: rgba(0, 0, 0, .6); }
  .screen.battle.active .arena .order-chip { padding: 1px 6px; font-size: .62rem; }
  .screen.battle.active .fighter .sprite-wrap { padding: 2px 8px; }
  .screen.battle.active .battle-hud { display: flex; flex-direction: column; margin-top: 6px;
    flex: 0 0 auto; gap: 4px; }
  /* The band and the skill list are the two things competing for what is left. Capping the list
     and letting it scroll is what gives the creatures their height back: uncapped it took 150px
     against the arena's 165. */
  .screen.battle.active .actions { max-height: 116px; overflow-y: auto; }
  /* THE SKILL CARD LOSES ITS PROSE, NOT ITS NUMBERS. Capping the row above put 99px of card in a
     47px window, and because the card is taller than the window the cut landed MID TEXT LINE,
     which reads as a rendering fault rather than as "there is more below". Worse, the half it
     kept was the name and the half it lost was `~60 · 1`, the live damage estimate against this
     opponent, which is the whole point of the card.
     The description is the 29px that does not fit, and it is the one part already reachable from
     See full detail. Dropping it brings the card to 54px, which FITS: no clip, no scroll, and
     the name and the numbers are on screen together. */
  .screen.battle.active .skill.big .skill-desc { display: none; }
  .screen.battle.active .skill.big { min-height: 0; padding-top: 4px; padding-bottom: 4px; }
  .screen.battle.active .coach { margin: 0 0 6px; padding: 8px 10px; }
}

/* ---- the trail's stops are 13px, and a finger is not ----
   Measured on the Ember Flats map at 375px: a spot is clamp(13px, 3.3cqw, 30px), so on a phone
   it bottoms out at 13 wide and 14 tall, carrying a 16.5px numeral. The glyph is bigger than the
   target it sits in, and it is under a third of the 44px both Apple and Google ask for.
   The fix is hit area, NOT size: growing the marks would crowd a map that already has 27 of them
   on it and would change a look that was dialled in deliberately. An ::after pad centred on the
   mark takes the touch without drawing anything, so the stop stays 13px and the finger gets 44.
   `z-index: 3` over the mark's own 2, because the pad has to be what receives the tap.
   ONLY THE LIVE STOP GETS ONE. The other twenty-six are `disabled` in the markup, and a 44px pad
   on each would put twenty-six dead hit areas over the one stop you are allowed to press: a
   wider target that swallows the tap is worse than a small one that misses. Padding `.now`
   alone also guarantees no two pads can overlap. */
.zspot.now::after {
  content: ''; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: max(44px, 100%); height: max(44px, 100%); z-index: 3; }

/* ---- the battle header could not fit a phone, and one of its labels paid ----
   Measured at 375: `.battle-top` is 351px wide holding 386px of children. `.round`, the field tag
   and BOTH speed buttons are `nowrap`, and the two buttons carry `min-width: 76px`, so none of
   the four can give. The only flexible child is `.kind`, which duly shrank to WIDTH ZERO: the
   page scrolled sideways by 24px and "Wild encounter, Ember Flats" was not on screen at all.
   A row that cannot fit should wrap, not push. The encounter label takes its own line, where it
   is legible for the first time, and the sideways scroll goes with it.
   Portrait only. A phone held sideways is 812px wide, does not match this query, and already
   fits the row on one line. */
@media (max-width: 760px) {
  .screen.battle.active .battle-top { flex-wrap: wrap; row-gap: 4px; }
  .screen.battle.active .battle-top .round { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  .screen.battle.active .battle-top .kind { flex: 1 0 100%; text-align: left; order: 5; }
}

/* The legal and support links at the foot of the Account screen. Quiet: they are the sort of
   thing somebody goes looking for rather than the sort of thing that should compete with the
   sign-in form above them. */
.acct-legal { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 18px;
  margin-top: 26px; padding-top: 14px; border-top: 1px solid var(--line);
  font-size: .86rem; }
.acct-legal a { color: var(--muted); text-decoration: none; border-bottom: 1px solid var(--line2); }
.acct-legal a:hover { color: var(--text-body); border-bottom-color: var(--st-dark); }
.acct-legal .muted { margin-left: auto; }
@media (max-width: 760px) { .acct-legal .muted { margin-left: 0; flex-basis: 100%; } }

/* THE FIGHT IS OVER BUT THE BOARD HAS NOT MOVED. Set on the action row, the skill grid and the
   turn strip the moment a match settles, so they stop responding and read as spent while keeping
   every pixel of their height. Clearing them instead let the arena grow into the gap and slid
   both fighters across the screen in the pause before the result (Dom, 2026-09-21). */
.screen.battle .actions.spent,
.screen.battle .skill-grid.spent,
.screen.battle .order-list.spent {
  opacity: .38;
  pointer-events: none;
  transition: opacity 260ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
  .screen.battle .actions.spent,
  .screen.battle .skill-grid.spent,
  .screen.battle .order-list.spent { transition: none; }
}

/* ---- facing in battle, on the Titan's detail (Dom, 2026-09-22) ----
   The battle cut-out as it stands on the player's side, with the enemy marked to its right. */
.facing-row { display: flex; align-items: center; gap: 14px; margin: 14px 0 4px; flex-wrap: wrap; }
.facing-stage { position: relative; width: 150px; height: 120px; flex: 0 0 auto; display: flex; align-items: flex-end; justify-content: flex-start;
  background: radial-gradient(ellipse 60% 20% at 40% 92%, rgba(0, 0, 0, .55), transparent 70%), linear-gradient(180deg, #151A21, #0E1216);
  box-shadow: inset 0 0 0 1px var(--line); overflow: hidden; padding: 6px 10px; }
.facing-pic { width: 96px; height: 104px; display: flex; align-items: flex-end; justify-content: center; }
.facing-pic img, .facing-pic canvas { max-width: 100%; max-height: 100%; object-fit: contain; object-position: 50% 100%; }
.facing-pic.mirror img, .facing-pic.mirror canvas { transform: scaleX(-1); }
.facing-foe { position: absolute; right: 8px; top: 8px; font-size: .65rem; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
.facing-say { flex: 1 1 200px; min-width: 0; }
.facing-say h3 { margin: 0 0 4px; }
.facing-say p { margin: 0 0 8px; }
.rename-row { margin: 12px 0 4px; }
.rename-row h3 { margin: 0 0 6px; }
.rename-ctl { display: flex; gap: 8px; align-items: center; max-width: 360px; }
.rename-in { flex: 1 1 auto; min-width: 0; padding: 8px 10px; background: #0E1216; border: 1px solid var(--line); color: var(--text); font: inherit; }

/* ---- the team, as three full cards (Dom, 2026-09-22) ---- */
.team-bench { display: grid; grid-template-columns: repeat(3, minmax(0, 210px)); justify-content: start; }
.team-bench .team-head { grid-column: 1 / -1; }
.team-bench .team-slot.card { display: flex; flex-direction: column; gap: 8px; padding: 0; background: none; border: 0; box-shadow: none; min-width: 0; }
.team-bench .team-slot.card .tcard { width: 100%; }
.team-bench .team-slot.card.empty { aspect-ratio: 5 / 7; display: grid; place-items: center; padding: 14px; text-align: center;
  border: 1px dashed var(--line); color: var(--muted); font-size: .8rem; }
.ts-acts { display: flex; align-items: center; gap: 6px; }
.ts-leadbtn { flex: 1 1 auto; }
.ts-leadbtn.on { color: #0B0D10; background: var(--gold, #E8C167); border-color: var(--gold, #E8C167); font-weight: 700; cursor: default; }
.ts-pts { font-size: .7rem; color: var(--muted); white-space: nowrap; }
@media (max-width: 560px) { .team-bench { grid-template-columns: repeat(3, minmax(0, 1fr)); } .ts-pts { display: none; } }

/* ---- picked for sale: a bright red border (Dom, 2026-09-22) ---- */
.grid.selling .cwrap.picked .tcard { outline: 3px solid #FF2E3F; outline-offset: 2px; box-shadow: 0 0 18px rgba(255, 46, 63, .55); }
.grid.selling .cwrap.picked .pick-tag { color: #FF6B78; font-weight: 700; }

/* ---- every section frame is a metal plate (Dom, 2026-09-22: "Any section frame needs to be a
   metal texture background"): the shop panel's recipe, the generated plate under its scrim over the
   painted ramp that is the fallback when no plate exists. ---- */
.team-bench, .sell-bar, .codex, .quest-card, .streak-card, .season-card, .rank-strip, .modal-card, .hstat, .conj-tier {
  background-image: var(--ui-scrim, none), var(--ui-box, none), linear-gradient(180deg, var(--panel2) 0%, var(--panel) 58%, var(--sunken) 100%);
  background-size: auto, cover, auto; background-position: 0 0, center, 0 0; background-repeat: no-repeat; }

/* ---- aura totals instead of pips ---- */
.aura-total { font-size: .95rem; font-weight: 700; color: var(--muted); font-variant-numeric: lining-nums tabular-nums; }
.aura-total.on { color: var(--c); }
/* the slots beside the total (Dom, 2026-09-25) */
.aura-slots { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-width: 0; }
/* All ten pips on one line (Dom, 2026-09-28): they used to wrap to a second row in a narrow card.
   Each pip gives up width before the row gives up the line, and the total never breaks in two. */
.aura-slots .aura-pips { flex-wrap: nowrap; flex: 1 1 auto; min-width: 0; }
.aura-slots .aura-pips i { flex: 1 1 0; min-width: 4px; max-width: 12px; height: 7px; border-radius: 1px; }
.aura-slots .aura-total { flex: none; white-space: nowrap; }
.aura-reset-row { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin: 2px 0 8px; }
.aura-reset.armed { color: #fff; background: #8E1B24; border-color: #C8202E; }

/* ---- season reward previews ---- */
.season-pip { width: 74px; height: 74px; position: relative; justify-content: center; }
.rp { display: block; margin: 0 auto 3px; }
.rp-cur { display: flex; flex-direction: column; align-items: center; gap: 1px; }
.rp-cur .ic { width: 20px; height: 20px; }
.rp-cur b { font-size: .82rem; }
.rp-frame { width: 30px; height: 30px; border: 3px solid var(--fe); background: var(--fi); box-shadow: inset 0 0 0 1px var(--fa), 0 0 8px color-mix(in srgb, var(--fe) 50%, transparent); }
.rp-board { width: 40px; height: 28px; background: linear-gradient(180deg, var(--bs) 0 52%, var(--bl) 52% 57%, var(--bf) 57%); box-shadow: 0 0 0 1px rgba(0, 0, 0, .6); }
.rp-title { font-size: .58rem; font-style: italic; line-height: 1.15; color: var(--gold, #E8C167); padding: 0 3px; white-space: normal; text-align: center; max-height: 3.4em; overflow: hidden; }
.rp-badge { position: absolute; top: 2px; right: 2px; font-style: normal; display: inline-flex; }
.rp-badge .ic { width: 12px; height: 12px; }

/* Selling: the team stays where it is on the page, so the sell bar is the only thing that follows
   you down (Dom, 2026-09-22: "When selling is enabled, do not let the team section continue to be
   sticky"). */
.team-bench.selling { position: static; }

/* ---- a card with a footer glows on the CARD, not the whole box (Dom, 2026-09-22: "Hover is
   broken") ----
   The hover glow lives on .tcard, which is the face AND anything hung under it. In the switch dialog
   that is the skills, the HP bar and the matchup tags, so the light drew a big glowing rectangle
   round all of it. With a footer, the glow goes on the face, which is the card. */
.tcard.clickable:has(> .tc-extra):hover, .tcard.clickable:has(> .tc-extra):focus-visible { box-shadow: none; }
.tcard.clickable:has(> .tc-extra):hover .tc-face, .tcard.clickable:has(> .tc-extra):focus-visible .tc-face {
  box-shadow: 0 0 30px -4px color-mix(in srgb, var(--r) 78%, transparent), 0 10px 24px -14px rgba(0, 0, 0, .9); }
/* and room above the cards: the lift on hover and the crest's top edge were cut by the dialog's
   scroll box */
.switch-grid { padding-top: 10px; }

/* ---- battle callouts (Dom, 2026-09-23) ----
   One line over the arena for the moments that change a fight; see callout() in battle.js. It hangs
   just under the enemy's info box (callout() measures where that is), on a dark plate so it reads
   over any painted field. Two selectors so it beats `.arena.fielded > *`, which puts every
   other arena child back into the flow. */
.arena > .callout, .arena.fielded > .callout {
  position: absolute; left: var(--co-left, 50%); top: var(--co-top, 50%); z-index: 6; pointer-events: none;
  transform: translate(-50%, -6px); opacity: 0; max-width: min(92%, 520px); width: max-content;
  padding: 6px 16px; text-align: center; white-space: normal; text-wrap: balance;
  font-family: var(--display); font-size: .92rem; font-weight: 700; letter-spacing: .04em; color: var(--text);
  background: rgba(8, 10, 13, .84); box-shadow: inset 0 0 0 1px rgba(214, 224, 236, .14), 0 6px 18px rgba(0, 0, 0, .6);
  transition: opacity 180ms ease-out, transform 180ms ease-out; }
.arena > .callout.show { opacity: 1; transform: translate(-50%, 0); }
/* Announcements speak in the game's blood red (Dom, 2026-09-24: "update the main color for the
   announcing text to be red"): the plain line, the matchup and the ground. A crit keeps its gold,
   and a status, a good turn and a weak hit keep their own colors, so they still read apart. */
.arena > .callout, .arena.fielded > .callout, .arena > .callout.super, .arena > .callout.swap,
.arena > .callout.field, .arena > .callout.warn { color: #F0454F; text-shadow: 0 0 10px rgba(176, 27, 42, .45); }
.arena > .callout.crit { color: #FFD35A; }
.arena > .callout.weak { color: #9AA4B2; }
.arena > .callout.good { color: #9BE58A; }
.arena > .callout.status { color: #E6B8FF; }
@media (max-width: 760px), (max-height: 560px) {
  .arena > .callout, .arena.fielded > .callout { font-size: .8rem; padding: 4px 10px; }
}
@media (prefers-reduced-motion: reduce) { .arena > .callout { transition: none; } }

/* ---- a boss on the trail (Dom, 2026-09-23: "wardens as real bosses") ----
   The tag beside its name, and a slow red breath around the edge of the arena for the whole fight,
   so a warden is known to be one before its first move and not only on the result screen. */
.boss-tag { display: inline-block; margin-left: 6px; padding: 1px 7px; vertical-align: middle;
  font-family: var(--display); font-size: .62rem; font-weight: 700; letter-spacing: .14em; text-transform: uppercase;
  color: #FFE9E9; background: #7A1320; box-shadow: inset 0 0 0 1px #C2263A; }
/* Its own element, not ::after: the arena's ::after is the receding floor grid. Two selectors so it
   beats `.arena.fielded > *`, which would put it back in the flow. */
.arena > .boss-edge, .arena.fielded > .boss-edge { position: absolute; inset: 0; z-index: 3; pointer-events: none;
  box-shadow: inset 0 0 0 1px rgba(194, 38, 58, .55), inset 0 0 60px rgba(160, 20, 36, .38);
  animation: bossBreath 3.2s ease-in-out infinite; }
.arena.boss-fight.final > .boss-edge { box-shadow: inset 0 0 0 1px rgba(255, 201, 64, .6), inset 0 0 80px rgba(255, 160, 40, .32); }
@keyframes bossBreath { 0%, 100% { opacity: .65; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .arena > .boss-edge { animation: none; } }

/* An element you can afford on a paid conjuring: a frame in its own color and a slow breath of
   glow, so the ones you could take right now read before a single line is (Dom, 2026-09-24). Here
   at the end and doubled in specificity because the panel rule above resets every .conj-opt's
   border and shadow. */
.conjure-inner .conj-opt.can {
  border-color: color-mix(in srgb, var(--c) 70%, transparent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--c) 55%, transparent), 0 0 14px 0 color-mix(in srgb, var(--c) 38%, transparent),
    inset 0 0 18px -6px color-mix(in srgb, var(--c) 45%, transparent);
  animation: conjCan 2.8s ease-in-out infinite;
}
.conjure-inner .conj-opt.can.on { animation: none;
  box-shadow: 0 0 0 2px var(--c), 0 0 22px 2px color-mix(in srgb, var(--c) 50%, transparent), inset 0 0 22px -6px color-mix(in srgb, var(--c) 55%, transparent); }
@keyframes conjCan {
  0%, 100% { box-shadow: 0 0 0 1px color-mix(in srgb, var(--c) 45%, transparent), 0 0 8px 0 color-mix(in srgb, var(--c) 25%, transparent),
    inset 0 0 14px -6px color-mix(in srgb, var(--c) 35%, transparent); }
  50% { box-shadow: 0 0 0 1px color-mix(in srgb, var(--c) 75%, transparent), 0 0 18px 1px color-mix(in srgb, var(--c) 45%, transparent),
    inset 0 0 20px -6px color-mix(in srgb, var(--c) 50%, transparent); }
}
@media (prefers-reduced-motion: reduce) { .conjure-inner .conj-opt.can { animation: none; } }
.box .climb { color: var(--muted); }

/* ---------- purchase flourishes (flourish.js) ----------
   Currency in flight between a header counter and the thing it bought, the counter nudged as it
   leaves or lands, a band of polish, and the teaching moment. */
.fl-layer { position: fixed; inset: 0; pointer-events: none; z-index: 1001; }
.fl-layer i { position: absolute; left: 0; top: 0; display: block; will-change: transform, opacity; }
/* a coin: gold with a lit edge, turning as it flies */
.fl-coin { width: 14px; height: 14px; margin: -7px 0 0 -7px; }
.fl-coin::before { content: ''; display: block; width: 100%; height: 100%; border-radius: 50%;
  background: radial-gradient(circle at 34% 30%, #FFF7D0 0 16%, #FFD54A 42%, #C98F17 78%, #8A5A0C 100%);
  box-shadow: 0 0 8px 2px rgba(255, 196, 64, .55), inset 0 0 0 1px rgba(255, 240, 190, .6);
  animation: flTurn 380ms linear infinite; }
/* Ether: a violet cut gem */
.fl-gem { width: 13px; height: 16px; margin: -8px 0 0 -6px; }
.fl-gem::before { content: ''; display: block; width: 100%; height: 100%;
  clip-path: polygon(50% 0, 100% 38%, 50% 100%, 0 38%);
  background: linear-gradient(160deg, #F1E4FF 0 18%, #B98CFF 45%, #6A2FD0 100%);
  filter: drop-shadow(0 0 6px rgba(164, 107, 255, .9)); animation: flTurn 520ms linear infinite; }
/* Essence: a pale mote; shards: a small diamond in the element's color */
.fl-mote { width: 10px; height: 10px; margin: -5px 0 0 -5px; border-radius: 50%;
  background: radial-gradient(circle, #FFFFFF 0 30%, var(--c, #D9DFE6) 60%, transparent 100%);
  box-shadow: 0 0 10px 3px color-mix(in srgb, var(--c, #D9DFE6) 60%, transparent); }
.fl-shard { width: 10px; height: 14px; margin: -7px 0 0 -5px; }
.fl-shard::before { content: ''; display: block; width: 100%; height: 100%; clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
  background: linear-gradient(160deg, #FFFFFF 0 15%, var(--c, #D9DFE6) 55%); filter: drop-shadow(0 0 5px var(--c, #D9DFE6)); }
.fl-layer i.still::before { animation: none; }
@keyframes flTurn { 0%, 100% { transform: scaleX(1); } 50% { transform: scaleX(.18); } }
/* the counters: a dip as money leaves, a bright swell as something arrives */
.hdr-pill.fl-spent { animation: flSpent 520ms ease-out; }
.hdr-pill.fl-got { animation: flGot 620ms cubic-bezier(.16, 1, .3, 1); }
@keyframes flSpent { 0% { transform: scale(1); } 30% { transform: scale(.9); filter: brightness(.8); } 100% { transform: none; } }
@keyframes flGot { 0% { transform: scale(1); } 35% { transform: scale(1.18); filter: brightness(1.6);
  box-shadow: 0 0 18px 4px rgba(255, 214, 110, .45); } 100% { transform: none; } }
/* the thing bought lights once */
.fl-lit { animation: flLit 700ms ease-out; }
@keyframes flLit { 0% { filter: brightness(1); } 25% { filter: brightness(1.45) saturate(1.2); } 100% { filter: none; } }
/* polish: one band of light drawn across, in the cosmetic's own accent */
.fl-shine { position: absolute; inset: 0; pointer-events: none; overflow: hidden; z-index: 5; display: block; }
.fl-shine::before { content: ''; position: absolute; top: -20%; bottom: -20%; left: -60%; width: 45%;
  background: linear-gradient(100deg, transparent, color-mix(in srgb, var(--c, #FFE9A8) 35%, transparent) 35%,
    rgba(255, 255, 255, .75) 50%, color-mix(in srgb, var(--c, #FFE9A8) 35%, transparent) 65%, transparent);
  transform: skewX(-18deg); mix-blend-mode: screen; animation: flShine 900ms cubic-bezier(.3, 0, .2, 1) both; }
@keyframes flShine { 0% { left: -60%; } 100% { left: 130%; } }

/* teaching: the Titan in the middle of a dimmed screen, its name line under it, the rune rising
   from below into it */
.teach-fx { position: fixed; inset: 0; z-index: 65; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 14px; padding: 0 16px 10vh; cursor: pointer;
  /* near-opaque: a Titan's card or the Skill Store behind it showing through made the moment read
     as something drawn over a page rather than a stage of its own */
  background: radial-gradient(ellipse at 50% 42%, color-mix(in srgb, var(--c) 20%, #07080B) 0, #050608 72%);
  background-color: rgba(5, 6, 8, .94);
  animation: teachIn 260ms ease-out both; }
.teach-fx.out { animation: teachOut 320ms ease-in both; }
@keyframes teachIn { 0% { opacity: 0; } 100% { opacity: 1; } }
@keyframes teachOut { 0% { opacity: 1; } 100% { opacity: 0; } }
.teach-titan { position: relative; width: min(58vw, 300px, 42vh); aspect-ratio: 1; display: grid; place-items: center;
  transition: filter 500ms ease-out, transform 600ms cubic-bezier(.16, 1, .3, 1); }
.teach-titan::before { content: ''; position: absolute; inset: -22%; border-radius: 50%; z-index: -1;
  background: radial-gradient(circle, color-mix(in srgb, var(--c) 40%, transparent) 0, transparent 65%);
  opacity: .5; transition: opacity 400ms ease-out, transform 600ms cubic-bezier(.16, 1, .3, 1); }
.teach-titan img { width: 100%; height: 100%; object-fit: contain; filter: drop-shadow(0 10px 24px rgba(0, 0, 0, .7)); }
.teach-rune { position: absolute; left: 50%; bottom: 9vh; translate: -50% 0; display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 10px 20px; border: 1px solid color-mix(in srgb, var(--c) 70%, transparent); white-space: nowrap;
  background: linear-gradient(180deg, color-mix(in srgb, var(--c) 26%, #0B0D10), #0B0D10);
  box-shadow: 0 0 24px 2px color-mix(in srgb, var(--c) 45%, transparent), inset 0 0 18px -4px var(--c);
  animation: teachRune 720ms cubic-bezier(.4, 0, .2, 1) both; }
.teach-rune b { font-family: var(--display); font-size: 1.15rem; color: #FFFFFF; letter-spacing: .06em; }
.teach-rune span { font-size: .7rem; letter-spacing: .14em; text-transform: uppercase; color: var(--c); }
@keyframes teachRune {
  0%   { opacity: 0; transform: translateY(40px) scale(.9); }
  28%  { opacity: 1; transform: translateY(0) scale(1); }
  80%  { opacity: 1; transform: translateY(calc(var(--rise, -200px) * .8)) scale(.7); filter: brightness(1.6); }
  100% { opacity: 0; transform: translateY(var(--rise, -200px)) scale(.3); filter: brightness(2.4); }
}
.teach-fx.taken .teach-titan { transform: scale(1.06);
  filter: brightness(1.35) drop-shadow(0 0 22px var(--c)) drop-shadow(0 0 6px #FFFFFF); }
/* the jolt of taking it, on the Titan's own picture: the camera shake it had moved the whole page and,
   with it, this fixed overlay (see cameraShake) */
.teach-fx.taken .teach-titan img { animation: teachJolt 260ms ease-out; }
@keyframes teachJolt { 0% { translate: 0 0; } 20% { translate: 4px -3px; } 45% { translate: -4px 2px; } 70% { translate: 2px -1px; } 100% { translate: 0 0; } }
.teach-fx.taken .teach-titan::before { opacity: 1; transform: scale(1.15); }
.teach-say { display: flex; flex-direction: column; align-items: center; gap: 2px; opacity: 0; max-width: 100%; }
.teach-fx.taken .teach-say { animation: teachSay 520ms cubic-bezier(.16, 1, .3, 1) both; }
.teach-say span { font-size: .78rem; letter-spacing: .3em; text-transform: uppercase; color: var(--st-bright); }
.teach-say b { font-family: var(--display); font-size: clamp(1.4rem, 5vw, 2.3rem); font-weight: 700; text-transform: uppercase;
  color: var(--c); -webkit-text-stroke: 3px #FFFFFF; paint-order: stroke fill; letter-spacing: .04em; text-align: center; line-height: 1.15;
  text-shadow: 0 3px 10px rgba(0, 0, 0, .7), 0 0 24px color-mix(in srgb, var(--c) 60%, transparent); }
@keyframes teachSay { 0% { opacity: 0; transform: scale(1.5); } 100% { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
  .teach-fx, .teach-fx.out, .teach-rune, .teach-fx.taken .teach-say, .teach-fx.taken .teach-titan img, .fl-shine::before, .fl-lit,
  .hdr-pill.fl-spent, .hdr-pill.fl-got { animation: none; }
}
/* a Titan bought, shown as itself */
.arrival { display: grid; place-items: center; padding: 8px 0 4px; }
.arrival .tcard { animation: arrivalIn 520ms cubic-bezier(.16, 1, .3, 1) both; }
@keyframes arrivalIn { 0% { opacity: 0; transform: scale(.82) translateY(14px); } 100% { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .arrival .tcard { animation: none; } }
/* A warden or protector fight: one wide, quiet button where the two Nets would be, so the row
   keeps its five columns and says why there is nothing to throw. */
.action-row .btn.no-catch { grid-column: span 2; color: #6B7482; background: #14181E; cursor: not-allowed; }
.action-row .btn.no-catch small { color: var(--ink-accent); opacity: .9; }
/* On a phone the sprite column is a grid (see the 760px block), and `.fighter .sprite-wrap` above
   sets align-items: center, which in a grid is the VERTICAL axis: every creature was centered in a
   band taller than it and hung over the ground (Dom, 2026-09-24). Here, later and as specific, it
   stands at the bottom of the band, on the ground the field is aligned to. */
@media (max-width: 760px) {
  .arena .fighter .sprite-wrap { align-items: end; justify-items: center; }
}

/* A Titan going down lets the log take the room the skills gave up, and when that pushed the page
   past the window the scrollbar arrived and took 11px off the width: the whole arena, both panels
   and the survivor slid sideways at the moment of the knockout (Dom, 2026-09-24). The gutter is
   reserved for the whole fight, so the width never changes. Overlay scrollbars (phones) take none. */
html.in-battle, body.in-battle { scrollbar-gutter: stable; }   /* the page scroll is the viewport's */

/* A disabled plate drops its 4px frame (.btn:disabled and .btn.net:disabled, border: 0), and with
   the battle strip's height set by its content that took 6px off it at the start of every turn, so
   the arena grew and shrank and both Titans bobbed with it (2026-09-24). Same size, the frame
   painted over by the dead fill. After and above both rules that zero it. */
.battle-hud .action-row .btn:disabled, .battle-hud .action-row .btn.net:disabled {
  border: 4px solid transparent; border-image: none; background-clip: border-box; }

/* Coming in. The arena is built hidden and shown once the field, both Titans and their effects are
   in hand (arrive() in battle.js), so the ground, the creatures and their panels arrive as one
   picture instead of the painting dropping in under creatures already standing there. */
.screen.battle .arena.arriving { opacity: 0; }
.screen.battle .arena.arrived { transition: opacity 320ms ease-out; }
@media (prefers-reduced-motion: reduce) { .screen.battle .arena.arrived { transition: none; } }

/* The generated wordmark in place of the typed title on the home screen: as wide as the copy column
   allows, never taller than a large heading, and no wider than the screen on a phone. */
.home-wordmark { margin: 0; line-height: 0; }
.home-wordmark img { display: block; width: 100%; max-width: 520px; max-height: 150px; height: auto; object-fit: contain;
  object-position: left center; filter: drop-shadow(0 2px 6px rgba(0, 0, 0, .7)); }

/* The generated wordmark in the header (portraits.js applyUiSkin), in place of the gold typed name:
   the gradient text treatment is switched off so it cannot tint or clip the picture. */
.brand.has-mark { background: none; -webkit-text-fill-color: initial; color: inherit; filter: none;
  display: inline-flex; align-items: center; line-height: 0; }
.brand .brand-mark { display: block; height: 42px; width: auto; max-width: 42vw; object-fit: contain;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .8)); }
@media (max-width: 600px) { .brand .brand-mark { height: 30px; } }

/* The home hero on DARK METAL (Dom, 2026-09-25: "This section needs a dark metal background
   texture"): the same cast-iron plate as the header and nav, under a heavy scrim so the cards and the
   wordmark stay the brightest things on it. Without the plate the old gradient shows, as before. */
.home-hero {
  background-image: radial-gradient(88% 68% at 50% -14%, rgba(200, 214, 232, .09), transparent 62%),
    radial-gradient(120% 90% at 50% 118%, rgba(4, 6, 9, .55), transparent 58%),
    linear-gradient(rgba(6, 8, 11, .5), rgba(6, 8, 11, .62)),
    var(--ui-chrome, none),
    linear-gradient(168deg, var(--panel3), var(--panel) 56%, var(--ink));
  background-size: auto, auto, auto, cover, auto;
  background-position: 0 0, 0 0, 0 0, center, 0 0;
  background-repeat: no-repeat; }
/* the stat tiles' number and label centered in their metal squares (Dom, 2026-09-25) */
.hstat { align-items: center; justify-content: center; text-align: center; }

/* ---- the intro (conjure.js stepIntro): a fresh game opens on the wordmark over the void ---- */
body.conj-intro .hdr, body.conj-intro .nav { visibility: hidden; pointer-events: none; }
/* height auto: the step panel's own height (the page minus the header and nav) left the void short
   of the bottom of the screen */
.conjure.is-intro { position: fixed; inset: 0; z-index: 300; height: auto; }
/* The panel the conjure steps sit in is 880px wide and hides what overflows it, which cut the logo's
   black glow off in a hard rectangle (Dom, 2026-09-25: "The glow is clipping on the edges"). The
   intro takes the whole screen, and its glow is allowed past the panel. */
.conjure.is-intro .conjure-inner { display: flex; align-items: center; justify-content: center; min-height: 100%; padding: 24px 16px;
  width: 100%; max-height: none; overflow: visible; }
.intro { --wm-w: min(760px, 88vw); display: flex; flex-direction: column; align-items: center; text-align: center; gap: 18px;
  animation: intro-rise 1.4s var(--ease-standard, ease-out) both; }
/* held back, animations and all, until the wordmark has loaded (conjure.js stepIntro) */
.intro.intro-wait { visibility: hidden; }
.intro.intro-wait, .intro.intro-wait * { animation: none !important; }
/* black shadows behind it, not red (Dom, 2026-09-25), and animated in: it may land after the intro
   is already up, when the art list arrives on a first visit */
.intro-mark { display: block; width: var(--wm-w); height: auto;
  /* ONE shadow, and it never animates (Dom, 2026-09-29: "When I sign out, it takes a super long
     time to see the game logo", with a white box where it should be). Three stacked drop-shadows
     and an animated blur on a 760px image were re-rasterized every frame of the fade, and an
     iPhone shows a blank white layer until it catches up. The fade is opacity and transform only,
     which the compositor does without redrawing the picture. */
  filter: drop-shadow(0 4px 28px rgba(0, 0, 0, .95));
  animation: intro-mark-in 1.1s cubic-bezier(.2, .7, .2, 1) both; }
@keyframes intro-mark-in { 0% { opacity: 0; transform: scale(.94); } 100% { opacity: 1; transform: none; } }
.intro-word { font-family: var(--display); font-size: clamp(3rem, 12vw, 7rem); letter-spacing: .14em; color: #E8E2DC;
  text-shadow: 0 0 26px rgba(0, 0, 0, .95), 0 6px 18px rgba(0, 0, 0, .9); }
.intro-sub { margin: 0; font-family: var(--display); text-transform: uppercase; letter-spacing: .34em;
  font-size: clamp(.8rem, 2.4vw, 1.1rem); color: #C9CED8; text-shadow: 0 0 14px rgba(0, 0, 0, .9), 0 0 24px rgba(196, 107, 255, .35); }
/* right under the letters (Dom, 2026-09-25), not under the picture: the letters end about 70% of the
   way down the wordmark and only the T's spike hangs below them, at the left, clear of the line */
.intro-mark + .intro-sub { margin-top: calc(-0.09 * var(--wm-w) - 6px); }
/* under 760px the line is wider than the letters leave room for beside the spike (it reaches 12% in
   from the left), so it sits just under the spike's tip instead */
@media (max-width: 760px) { .intro-mark + .intro-sub { margin-top: -10px; } }
/* the call to action: a slow pulse of red light, and on the click a shockwave and a flash */
.conjure .intro .intro-cta { margin-top: 18px; position: relative; overflow: visible; text-transform: uppercase; letter-spacing: .16em;
  padding: 18px 38px; animation: intro-pulse 1.9s ease-in-out infinite; }
.conjure .intro .intro-cta::after { content: ''; position: absolute; inset: -2px; pointer-events: none; opacity: 0;
  border: 2px solid rgba(255, 70, 70, .9); box-shadow: 0 0 24px rgba(255, 40, 50, .8); }
.conjure .intro .intro-cta.fired { animation: intro-fire .9s ease-out forwards; }
.conjure .intro .intro-cta.fired::after { animation: intro-wave .8s ease-out forwards; }
.conjure.intro-flash::after { content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 5;
  background: radial-gradient(60% 50% at 50% 60%, rgba(255, 60, 60, .55), rgba(120, 0, 10, .25) 55%, transparent 80%);
  animation: intro-flash .9s ease-out forwards; }
@keyframes intro-pulse {
  0%, 100% { box-shadow: 0 0 10px 1px rgba(200, 24, 36, .45), 0 0 0 1px rgba(0, 0, 0, .6); }
  50% { box-shadow: 0 0 34px 8px rgba(255, 44, 56, .8), 0 0 70px 14px rgba(190, 20, 34, .35), 0 0 0 1px rgba(0, 0, 0, .6); } }
@keyframes intro-fire {
  0% { transform: scale(1); } 18% { transform: scale(1.08); box-shadow: 0 0 60px 20px rgba(255, 60, 70, .95); }
  100% { transform: scale(.96); opacity: 0; box-shadow: 0 0 90px 30px rgba(255, 60, 70, 0); } }
@keyframes intro-wave { 0% { opacity: 1; transform: scale(1); } 100% { opacity: 0; transform: scale(2.6, 3.4); } }
@keyframes intro-flash { 0% { opacity: 0; } 22% { opacity: 1; } 100% { opacity: 0; } }
@keyframes intro-rise { 0% { opacity: 0; transform: translateY(14px); } 100% { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
  .intro, .conjure .intro .intro-cta, .intro-mark { animation: none; }
  .conjure .intro .intro-cta { box-shadow: 0 0 24px 4px rgba(255, 44, 56, .6); }
}

/* ---- Account: sound settings ---- */
.sound-settings { display: grid; gap: 10px; }
.sound-settings .vol-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.vol-row { display: grid; grid-template-columns: minmax(96px, 140px) minmax(0, 1fr) 3.2em; align-items: center; gap: 12px; }
.vol-row > span { color: var(--text-body); font-size: .9rem; }
.vol-val { text-align: right; font-variant-numeric: tabular-nums; color: var(--text); }
.vol-range { width: 100%; accent-color: #C8202E; height: 22px; }
.vol-range:focus-visible { outline: 2px solid var(--ink-accent-hi, #E0B070); outline-offset: 2px; }

/* ---- battle: nothing the controls do moves the arena (Dom, 2026-09-25) ---- */
/* A fallen Titan's skills are hidden in place while it is banished, keeping their room. */
.actions.settling > * { visibility: hidden; }
/* A skill's full detail opens IN PLACE under it and pushes the skills below it down (Dom, 2026-09-25).
   While one is open (battle.js adds .sd-open) the arena is held at the height it had, the screen may
   grow past the window, and the skill list is uncapped, so the detail pushes the list down rather than
   squeezing the arena and moving the Titans. */
.screen.battle.active.sd-open { height: auto; }
.screen.battle.active.sd-open .arena { flex: 0 0 var(--arena-hold, auto); height: var(--arena-hold, auto); min-height: 0; }
.screen.battle.active.sd-open .skill-grid { max-height: none; overflow: visible; -webkit-mask-image: none; mask-image: none; }

/* A painting MOVED DOWN so its head clears the shield (ui.js ART_DROP), at full size (Dom, 2026-09-25:
   "I don't like shrinking it, can we just move it down and add more black fade"). Its foot runs on
   behind the sill, and the black fade at its top is longer, so the band above it melts into the card. */
.tc-art.dropped img:not(.cut), .tc-art.dropped canvas {
  top: calc((36 + var(--art-drop)) * var(--u));
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, .25) 5%, rgba(0, 0, 0, .6) 10%, rgba(0, 0, 0, .88) 16%, #000 22%);
          mask-image: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, .25) 5%, rgba(0, 0, 0, .6) 10%, rgba(0, 0, 0, .88) 16%, #000 22%); }

/* ---------- the top end: Multiverse variants, foils and ascended Titans (balance.js) ---------- */
/* MULTIVERSE (inst.shiny): the same painting in its second colouring, a version of the Titan from
   another Convergence (Dom, 2026-09-27). The creature changes; the card keeps a gold edge. */
/* Every number below is a custom property so the Art Lab's Finishes panel can tune it live on the
   real card; the defaults are the shipped values. */
img.shiny { filter: hue-rotate(var(--mv-hue, 150deg)) saturate(var(--mv-sat, 1.35)) brightness(1.06) drop-shadow(0 0 6px rgba(255, 224, 140, .55)); }
.tcard.is-shiny { box-shadow: 0 0 0 2px rgba(255, 224, 140, .75), 0 0 18px rgba(255, 224, 140, .35); }
/* FOIL (inst.foil): the CARD changes, the creature does not. A rainbow sheen laid over the painting,
   a light sweep and a prism edge. A cut-out on a holo backdrop was tried and lost the scene. */
.tcard.is-foil { box-shadow: 0 0 0 2px rgba(200, 225, 255, .8), 0 0 16px rgba(160, 200, 255, .4); }
.tcard.is-foil.is-shiny { box-shadow: 0 0 0 2px rgba(255, 224, 140, .85), 0 0 0 4px rgba(200, 225, 255, .55), 0 0 24px rgba(255, 210, 160, .45); }
.tcard.is-foil .tc-art::before {
  /* HOLO BANDS + GLITTER (Dom, 2026-09-27: "it should actually look like a foil card in real life").
     A real foil card shows rainbow diffraction bands over a fine glittering grain. The bands are a
     repeating rainbow fixed to the card; the tilt (installCardTilt, driftTilt) shifts their hue.
     The grain is an SVG turbulence tile, and the whole layer is color-dodged onto the painting so
     the dark parts stay dark and only the lit parts flash, which is how printed foil behaves. */
  content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 1;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 2.2 -1.1'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E"),
    repeating-linear-gradient(115deg,
      hsl(0 60% 58%) 0%, hsl(50 60% 58%) 1.6%, hsl(115 50% 55%) 3.2%, hsl(185 60% 58%) 4.8%,
      hsl(240 60% 64%) 6.4%, hsl(300 55% 62%) 8%, hsl(360 60% 58%) 9.6%),
    repeating-linear-gradient(25deg, rgba(255,255,255,.10) 0 1px, transparent 1px 5px);
  /* Tight, muted bands: saturated ones on dark art read as a rainbow filter, not as foil. The
     second line pattern is the fine hatching printed holo has under the colour. */
  background-size: 140px 140px, 300% 300%, auto;
  /* The stripes are printed on the card, so they never move (Dom, 2026-09-27: "it still acts the
     swim"). Turning the card changes their COLOUR instead: a hue shift driven by the tilt angle
     (--rx/--ry, up to 9deg each way), so every stripe runs through the rainbow in place. */
  background-position: 0 0, 50% 50%, 0 0;
  background-blend-mode: overlay, normal, normal;
  filter: brightness(.75) contrast(1.3) hue-rotate(calc((var(--ry, 0deg) - var(--rx, 0deg)) * 10));
  /* The light (Dom, 2026-09-27, "apply the same effect to the foil sheen"): like the sparkle, the
     foil is a material that is always there at --foil-base, and a band of light placed by the
     same --px/--py brings it up to full where it falls. */
  -webkit-mask-image: linear-gradient(115deg, rgba(0, 0, 0, var(--foil-base, .75)) 20%, #000 42%, #000 58%, rgba(0, 0, 0, var(--foil-base, .75)) 80%);
          mask-image: linear-gradient(115deg, rgba(0, 0, 0, var(--foil-base, .75)) 20%, #000 42%, #000 58%, rgba(0, 0, 0, var(--foil-base, .75)) 80%);
  -webkit-mask-size: 250% 250%; mask-size: 250% 250%;
  -webkit-mask-position: var(--px, 50%) var(--py, 50%); mask-position: var(--px, 50%) var(--py, 50%);
  mix-blend-mode: color-dodge; opacity: var(--foil-rest, .31);
  transition: opacity .25s ease;
}
/* Turned under a hand (or the idle drift), the foil catches the light harder. */
.tcard.is-foil.tilting .tc-art::before { opacity: var(--foil-tilt, .36); }
/* No hand and no drift (a phone at rest, a card in a grid): the bands creep on their own so the
   card never reads as flat. */
.tcard.is-foil:not(.tilting) .tc-art::before { animation: foil-idle 9s ease-in-out infinite alternate; }
@keyframes foil-idle {
  from { filter: brightness(.75) contrast(1.3) hue-rotate(-60deg); -webkit-mask-position: 35% 35%; mask-position: 35% 35%; }
  to   { filter: brightness(.75) contrast(1.3) hue-rotate(60deg); -webkit-mask-position: 65% 65%; mask-position: 65% 65%; }
}
.tcard.is-foil .tc-art::after, .tcard.is-sparkle .tc-art::after {
  /* THE GLARE: the hot spot where the light hits the foil, following the same pointer. */
  content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 2;
  background: radial-gradient(circle at var(--px, 30%) var(--py, 25%), rgba(255, 255, 255, .55) 0%, rgba(255, 255, 255, .18) 18%, transparent 45%);
  mix-blend-mode: overlay; opacity: var(--foil-glare, 1);
}
.tcard.is-foil:not(.tilting) .tc-art::after, .tcard.is-sparkle:not(.tilting) .tc-art::after { animation: foil-glare 9s ease-in-out infinite alternate; }
@keyframes foil-glare { from { background-position: -30% -20%; } to { background-position: 30% 20%; } }
/* SPARKLE FOIL (inst.foil === 'sparkle'). Dom, 2026-09-27: "I want it to act like little gemstones
   getting hit with light and sparkling". Earlier takes were glitter lit by one band, so every flake
   flashed together. Now the card carries four groups of small faceted stones (ruby, sapphire,
   emerald, amethyst, topaz, diamond), each a tile fixed to the card with a coloured facet and a
   cross flare. Each group has its own light: a narrow band at its own angle that answers the tilt
   (--px/--py) differently, so as the card turns different stones catch and flare in turn, the way
   cut stones set at different angles do. At rest the stones sit faintly (--sparkle-base) and the
   four lights drift at unrelated speeds, so it twinkles without anything sliding. The stones are
   drawn by ui.js titanCard as .tc-gems i.g1..g4 inside .tc-art. Colour-dodged, so only the stones
   touch the art. */
.tcard.is-sparkle .tc-art img { filter: brightness(var(--sparkle-dark, .94)) contrast(1.08); }
.tcard.is-sparkle .tc-art img.shiny { filter: hue-rotate(var(--mv-hue, 150deg)) saturate(var(--mv-sat, 1.35)) brightness(var(--sparkle-dark, .94)) contrast(1.08) drop-shadow(0 0 6px rgba(255, 224, 140, .55)); }
.tc-gems, .tc-gems i { position: absolute; inset: 0; pointer-events: none; }
.tc-gems { z-index: 1; }
.tc-gems i {
  -webkit-mask-size: 250% 250%; mask-size: 250% 250%;
  filter: brightness(var(--sparkle-gain, 3));
  mix-blend-mode: color-dodge; opacity: var(--sparkle-op, .4);
}
.tc-gems .g1 {
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='130' height='130'%3E%3Cg transform='rotate(-12 29.5 85.1)'%3E%3Cpath d='M29.5 77.3L29.90 84.70L37.3 85.1L29.90 85.54L29.5 92.9L29.06 85.54L21.7 85.1L29.06 84.70Z' fill='white' opacity='.9'/%3E%3Cpath d='M27.0 82.7L29.8 84.8L31.9 87.6L29.2 85.4ZM31.9 82.7L29.8 85.4L27.0 87.6L29.2 84.8Z' fill='white' opacity='.6'/%3E%3Cpath d='M29.5 83.53L31.07 85.1L29.5 86.70L27.90 85.1Z' fill='%23ffffff'/%3E%3Ccircle cx='29.5' cy='85.1' r='0.60' fill='white'/%3E%3C/g%3E%3Cg transform='rotate(0 97.6 66.3)'%3E%3Cpath d='M97.6 57.4L98.05 65.90L106.5 66.3L98.05 66.74L97.6 75.2L97.21 66.74L88.7 66.3L97.21 65.90Z' fill='white' opacity='.9'/%3E%3Cpath d='M94.8 63.5L97.9 66.0L100.4 69.1L97.3 66.6ZM100.4 63.5L97.9 66.6L94.8 69.1L97.3 66.0Z' fill='white' opacity='.6'/%3E%3Cpath d='M97.6 64.49L99.46 66.3L97.6 68.15L95.81 66.3Z' fill='%23ffc34d'/%3E%3Ccircle cx='97.6' cy='66.3' r='0.69' fill='white'/%3E%3C/g%3E%3Cg transform='rotate(0 51.3 74.0)'%3E%3Cpath d='M51.3 67.1L51.75 73.63L58.3 74.0L51.75 74.47L51.3 81.0L50.91 74.47L44.4 74.0L50.91 73.63Z' fill='white' opacity='.9'/%3E%3Cpath d='M49.1 71.9L51.6 73.7L53.5 76.2L51.0 74.3ZM53.5 71.9L51.6 74.3L49.1 76.2L51.0 73.7Z' fill='white' opacity='.6'/%3E%3Cpath d='M51.3 72.60L52.78 74.0L51.3 75.49L49.89 74.0Z' fill='%23ff4d6d'/%3E%3Ccircle cx='51.3' cy='74.0' r='0.55' fill='white'/%3E%3C/g%3E%3Cg transform='rotate(-12 85.6 58.3)'%3E%3Cpath d='M85.6 49.5L85.98 57.89L94.4 58.3L85.98 58.73L85.6 67.1L85.14 58.73L76.8 58.3L85.14 57.89Z' fill='white' opacity='.9'/%3E%3Cpath d='M82.8 55.5L85.9 58.0L88.3 61.1L85.3 58.6ZM88.3 55.5L85.9 58.6L82.8 61.1L85.3 58.0Z' fill='white' opacity='.6'/%3E%3Cpath d='M85.6 56.24L87.63 58.3L85.6 60.38L83.48 58.3Z' fill='%23ffffff'/%3E%3Ccircle cx='85.6' cy='58.3' r='0.79' fill='white'/%3E%3C/g%3E%3Cg transform='rotate(12 15.6 46.9)'%3E%3Cpath d='M15.6 37.6L16.03 46.45L24.9 46.9L16.03 47.29L15.6 56.1L15.19 47.29L6.4 46.9L15.19 46.45Z' fill='white' opacity='.9'/%3E%3Cpath d='M12.7 44.0L15.9 46.6L18.5 49.8L15.3 47.2ZM18.5 44.0L15.9 47.2L12.7 49.8L15.3 46.6Z' fill='white' opacity='.6'/%3E%3Cpath d='M15.6 44.87L17.61 46.9L15.6 48.86L13.62 46.9Z' fill='%23c070ff'/%3E%3Ccircle cx='15.6' cy='46.9' r='0.76' fill='white'/%3E%3C/g%3E%3C/svg%3E") 17px 29px / 130px 130px;
  -webkit-mask-image: linear-gradient(115deg, rgba(0, 0, 0, var(--sparkle-base, .35)) 44%, #000 49.5%, #000 50.5%, rgba(0, 0, 0, var(--sparkle-base, .35)) 56%);
          mask-image: linear-gradient(115deg, rgba(0, 0, 0, var(--sparkle-base, .35)) 44%, #000 49.5%, #000 50.5%, rgba(0, 0, 0, var(--sparkle-base, .35)) 56%);
  -webkit-mask-position: var(--px, 50%) var(--py, 50%); mask-position: var(--px, 50%) var(--py, 50%);
}
.tcard:not(.tilting) .tc-gems .g1 { animation: gem-light-1 5.3s ease-in-out infinite alternate; }
@keyframes gem-light-1 { from { -webkit-mask-position: 25% 61%; mask-position: 25% 61%; } to { -webkit-mask-position: 74% 36%; mask-position: 74% 36%; } }
.tc-gems .g2 {
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Cg transform='rotate(0 132.7 29.7)'%3E%3Cpath d='M132.7 21.4L133.15 29.27L141.0 29.7L133.15 30.11L132.7 37.9L132.31 30.11L124.5 29.7L132.31 29.27Z' fill='white' opacity='.9'/%3E%3Cpath d='M130.1 27.1L133.0 29.4L135.3 32.3L132.4 30.0ZM135.3 27.1L133.0 30.0L130.1 32.3L132.4 29.4Z' fill='white' opacity='.6'/%3E%3Cpath d='M132.7 27.77L134.65 29.7L132.7 31.60L130.82 29.7Z' fill='%23ff4d6d'/%3E%3Ccircle cx='132.7' cy='29.7' r='0.73' fill='white'/%3E%3C/g%3E%3Cg transform='rotate(0 105.2 122.7)'%3E%3Cpath d='M105.2 114.0L105.61 122.29L113.9 122.7L105.61 123.13L105.2 131.4L104.77 123.13L96.5 122.7L104.77 122.29Z' fill='white' opacity='.9'/%3E%3Cpath d='M102.5 120.0L105.5 122.4L107.9 125.4L104.9 123.0ZM107.9 120.0L105.5 123.0L102.5 125.4L104.9 122.4Z' fill='white' opacity='.6'/%3E%3Cpath d='M105.2 120.52L107.38 122.7L105.2 124.90L103.00 122.7Z' fill='%23bfe8ff'/%3E%3Ccircle cx='105.2' cy='122.7' r='0.83' fill='white'/%3E%3C/g%3E%3Cg transform='rotate(0 52.3 87.7)'%3E%3Cpath d='M52.3 77.7L52.67 87.32L62.3 87.7L52.67 88.16L52.3 97.8L51.83 88.16L42.2 87.7L51.83 87.32Z' fill='white' opacity='.9'/%3E%3Cpath d='M49.1 84.6L52.6 87.4L55.4 90.9L52.0 88.0ZM55.4 84.6L52.6 88.0L49.1 90.9L52.0 87.4Z' fill='white' opacity='.6'/%3E%3Cpath d='M52.3 85.76L54.24 87.7L52.3 89.73L50.27 87.7Z' fill='%23ffc34d'/%3E%3Ccircle cx='52.3' cy='87.7' r='0.75' fill='white'/%3E%3C/g%3E%3Cg transform='rotate(0 84.7 14.7)'%3E%3Cpath d='M84.7 8.4L85.14 14.27L91.0 14.7L85.14 15.11L84.7 21.0L84.30 15.11L78.4 14.7L84.30 14.27Z' fill='white' opacity='.9'/%3E%3Cpath d='M82.7 12.7L85.0 14.4L86.7 16.7L84.4 15.0ZM86.7 12.7L85.0 15.0L82.7 16.7L84.4 14.4Z' fill='white' opacity='.6'/%3E%3Cpath d='M84.7 13.08L86.33 14.7L84.7 16.30L83.11 14.7Z' fill='%233dffa0'/%3E%3Ccircle cx='84.7' cy='14.7' r='0.61' fill='white'/%3E%3C/g%3E%3Cg transform='rotate(0 85.4 130.2)'%3E%3Cpath d='M85.4 118.5L85.86 129.77L97.1 130.2L85.86 130.61L85.4 141.9L85.02 130.61L73.7 130.2L85.02 129.77Z' fill='white' opacity='.9'/%3E%3Cpath d='M81.8 126.5L85.7 129.9L89.1 133.9L85.1 130.5ZM89.1 126.5L85.7 130.5L81.8 133.9L85.1 129.9Z' fill='white' opacity='.6'/%3E%3Cpath d='M85.4 127.90L87.72 130.2L85.4 132.47L83.15 130.2Z' fill='%23bfe8ff'/%3E%3Ccircle cx='85.4' cy='130.2' r='0.87' fill='white'/%3E%3C/g%3E%3C/svg%3E") 34px 58px / 150px 150px;
  -webkit-mask-image: linear-gradient(65deg, rgba(0, 0, 0, var(--sparkle-base, .35)) 44%, #000 49.5%, #000 50.5%, rgba(0, 0, 0, var(--sparkle-base, .35)) 56%);
          mask-image: linear-gradient(65deg, rgba(0, 0, 0, var(--sparkle-base, .35)) 44%, #000 49.5%, #000 50.5%, rgba(0, 0, 0, var(--sparkle-base, .35)) 56%);
  -webkit-mask-position: calc(100% - var(--px, 50%)) var(--py, 50%); mask-position: calc(100% - var(--px, 50%)) var(--py, 50%);
}
.tcard:not(.tilting) .tc-gems .g2 { animation: gem-light-2 6.7s ease-in-out infinite alternate; }
@keyframes gem-light-2 { from { -webkit-mask-position: 30% 52%; mask-position: 30% 52%; } to { -webkit-mask-position: 68% 47%; mask-position: 68% 47%; } }
.tc-gems .g3 {
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='110' height='110'%3E%3Cg transform='rotate(-12 91.5 93.6)'%3E%3Cpath d='M91.5 87.3L91.96 93.16L97.8 93.6L91.96 94.00L91.5 99.9L91.12 94.00L85.2 93.6L91.12 93.16Z' fill='white' opacity='.9'/%3E%3Cpath d='M89.6 91.6L91.8 93.3L93.5 95.6L91.2 93.9ZM93.5 91.6L91.8 93.9L89.6 95.6L91.2 93.3Z' fill='white' opacity='.6'/%3E%3Cpath d='M91.5 92.20L92.92 93.6L91.5 94.96L90.15 93.6Z' fill='%23bfe8ff'/%3E%3Ccircle cx='91.5' cy='93.6' r='0.53' fill='white'/%3E%3C/g%3E%3Cg transform='rotate(0 44.6 42.8)'%3E%3Cpath d='M44.6 36.4L45.03 42.37L51.0 42.8L45.03 43.21L44.6 49.2L44.19 43.21L38.2 42.8L44.19 42.37Z' fill='white' opacity='.9'/%3E%3Cpath d='M42.6 40.8L44.9 42.5L46.6 44.8L44.3 43.1ZM46.6 40.8L44.9 43.1L42.6 44.8L44.3 42.5Z' fill='white' opacity='.6'/%3E%3Cpath d='M44.6 41.30L46.10 42.8L44.6 44.29L43.12 42.8Z' fill='%23ffffff'/%3E%3Ccircle cx='44.6' cy='42.8' r='0.57' fill='white'/%3E%3C/g%3E%3Cg transform='rotate(-12 65.3 89.5)'%3E%3Cpath d='M65.3 83.0L65.75 89.12L71.9 89.5L65.75 89.96L65.3 96.1L64.91 89.96L58.8 89.5L64.91 89.12Z' fill='white' opacity='.9'/%3E%3Cpath d='M63.3 87.5L65.6 89.2L67.4 91.6L65.0 89.8ZM67.4 87.5L65.6 89.8L63.3 91.6L65.0 89.2Z' fill='white' opacity='.6'/%3E%3Cpath d='M65.3 88.16L66.72 89.5L65.3 90.93L63.95 89.5Z' fill='%23ff4d6d'/%3E%3Ccircle cx='65.3' cy='89.5' r='0.53' fill='white'/%3E%3C/g%3E%3Cg transform='rotate(0 13.7 66.0)'%3E%3Cpath d='M13.7 59.9L14.10 65.56L19.8 66.0L14.10 66.40L13.7 72.1L13.26 66.40L7.6 66.0L13.26 65.56Z' fill='white' opacity='.9'/%3E%3Cpath d='M11.8 64.0L14.0 65.7L15.6 67.9L13.4 66.3ZM15.6 64.0L14.0 66.3L11.8 67.9L13.4 65.7Z' fill='white' opacity='.6'/%3E%3Cpath d='M13.7 64.32L15.34 66.0L13.7 67.64L12.02 66.0Z' fill='%23c070ff'/%3E%3Ccircle cx='13.7' cy='66.0' r='0.63' fill='white'/%3E%3C/g%3E%3C/svg%3E") 51px 87px / 110px 110px;
  -webkit-mask-image: linear-gradient(160deg, rgba(0, 0, 0, var(--sparkle-base, .35)) 44%, #000 49.5%, #000 50.5%, rgba(0, 0, 0, var(--sparkle-base, .35)) 56%);
          mask-image: linear-gradient(160deg, rgba(0, 0, 0, var(--sparkle-base, .35)) 44%, #000 49.5%, #000 50.5%, rgba(0, 0, 0, var(--sparkle-base, .35)) 56%);
  -webkit-mask-position: var(--py, 50%) calc(var(--px, 50%) * 1.4 - 20%); mask-position: var(--py, 50%) calc(var(--px, 50%) * 1.4 - 20%);
}
.tcard:not(.tilting) .tc-gems .g3 { animation: gem-light-3 7.9s ease-in-out infinite alternate; }
@keyframes gem-light-3 { from { -webkit-mask-position: 35% 43%; mask-position: 35% 43%; } to { -webkit-mask-position: 62% 58%; mask-position: 62% 58%; } }
.tc-gems .g4 {
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='170' height='170'%3E%3Cg transform='rotate(0 116.0 134.6)'%3E%3Cpath d='M116.0 128.7L116.42 134.19L121.9 134.6L116.42 135.03L116.0 140.5L115.58 135.03L110.1 134.6L115.58 134.19Z' fill='white' opacity='.9'/%3E%3Cpath d='M114.2 132.8L116.3 134.3L117.9 136.5L115.7 134.9ZM117.9 132.8L116.3 134.9L114.2 136.5L115.7 134.3Z' fill='white' opacity='.6'/%3E%3Cpath d='M116.0 133.09L117.52 134.6L116.0 136.13L114.48 134.6Z' fill='%234d8dff'/%3E%3Ccircle cx='116.0' cy='134.6' r='0.58' fill='white'/%3E%3C/g%3E%3Cg transform='rotate(12 109.9 25.4)'%3E%3Cpath d='M109.9 14.2L110.37 24.98L121.2 25.4L110.37 25.82L109.9 36.6L109.53 25.82L98.7 25.4L109.53 24.98Z' fill='white' opacity='.9'/%3E%3Cpath d='M106.4 21.9L110.2 25.1L113.5 28.9L109.6 25.7ZM113.5 21.9L110.2 25.7L106.4 28.9L109.6 25.1Z' fill='white' opacity='.6'/%3E%3Cpath d='M109.9 23.10L112.25 25.4L109.9 27.70L107.65 25.4Z' fill='%23bfe8ff'/%3E%3Ccircle cx='109.9' cy='25.4' r='0.87' fill='white'/%3E%3C/g%3E%3Cg transform='rotate(0 117.8 123.8)'%3E%3Cpath d='M117.8 115.4L118.19 123.41L126.2 123.8L118.19 124.25L117.8 132.2L117.35 124.25L109.4 123.8L117.35 123.41Z' fill='white' opacity='.9'/%3E%3Cpath d='M115.1 121.2L118.1 123.5L120.4 126.5L117.5 124.1ZM120.4 121.2L118.1 124.1L115.1 126.5L117.5 123.5Z' fill='white' opacity='.6'/%3E%3Cpath d='M117.8 122.06L119.54 123.8L117.8 125.60L116.00 123.8Z' fill='%23c070ff'/%3E%3Ccircle cx='117.8' cy='123.8' r='0.67' fill='white'/%3E%3C/g%3E%3Cg transform='rotate(12 16.1 36.6)'%3E%3Cpath d='M16.1 27.8L16.53 36.20L24.9 36.6L16.53 37.04L16.1 45.4L15.69 37.04L7.3 36.6L15.69 36.20Z' fill='white' opacity='.9'/%3E%3Cpath d='M13.3 33.9L16.4 36.3L18.9 39.4L15.8 36.9ZM18.9 33.9L16.4 36.9L13.3 39.4L15.8 36.3Z' fill='white' opacity='.6'/%3E%3Cpath d='M16.1 34.58L18.15 36.6L16.1 38.66L14.07 36.6Z' fill='%23c070ff'/%3E%3Ccircle cx='16.1' cy='36.6' r='0.77' fill='white'/%3E%3C/g%3E%3Cg transform='rotate(0 85.0 157.3)'%3E%3Cpath d='M85.0 147.1L85.38 156.87L95.2 157.3L85.38 157.71L85.0 167.5L84.54 157.71L74.8 157.3L84.54 156.87Z' fill='white' opacity='.9'/%3E%3Cpath d='M81.8 154.1L85.3 157.0L88.2 160.5L84.7 157.6ZM88.2 154.1L85.3 157.6L81.8 160.5L84.7 157.0Z' fill='white' opacity='.6'/%3E%3Cpath d='M85.0 155.23L87.03 157.3L85.0 159.35L82.90 157.3Z' fill='%23ffffff'/%3E%3Ccircle cx='85.0' cy='157.3' r='0.78' fill='white'/%3E%3C/g%3E%3Cg transform='rotate(0 111.5 152.2)'%3E%3Cpath d='M111.5 144.5L111.88 151.77L119.2 152.2L111.88 152.61L111.5 159.9L111.04 152.61L103.7 152.2L111.04 151.77Z' fill='white' opacity='.9'/%3E%3Cpath d='M109.0 149.8L111.8 151.9L113.9 154.6L111.2 152.5ZM113.9 149.8L111.8 152.5L109.0 154.6L111.2 151.9Z' fill='white' opacity='.6'/%3E%3Cpath d='M111.5 150.35L113.30 152.2L111.5 154.04L109.61 152.2Z' fill='%23ff4d6d'/%3E%3Ccircle cx='111.5' cy='152.2' r='0.70' fill='white'/%3E%3C/g%3E%3C/svg%3E") 68px 116px / 170px 170px;
  -webkit-mask-image: linear-gradient(25deg, rgba(0, 0, 0, var(--sparkle-base, .35)) 44%, #000 49.5%, #000 50.5%, rgba(0, 0, 0, var(--sparkle-base, .35)) 56%);
          mask-image: linear-gradient(25deg, rgba(0, 0, 0, var(--sparkle-base, .35)) 44%, #000 49.5%, #000 50.5%, rgba(0, 0, 0, var(--sparkle-base, .35)) 56%);
  -webkit-mask-position: calc(120% - var(--py, 50%) * 1.4) calc(100% - var(--px, 50%)); mask-position: calc(120% - var(--py, 50%) * 1.4) calc(100% - var(--px, 50%));
}
.tcard:not(.tilting) .tc-gems .g4 { animation: gem-light-4 9.1s ease-in-out infinite alternate; }
@keyframes gem-light-4 { from { -webkit-mask-position: 40% 34%; mask-position: 40% 34%; } to { -webkit-mask-position: 56% 69%; mask-position: 56% 69%; } }
.tcard.is-sparkle { box-shadow: 0 0 0 2px rgba(210, 215, 235, .85), 0 0 18px rgba(170, 180, 255, .35); }
.tcard.is-sparkle.is-shiny { box-shadow: 0 0 0 2px rgba(255, 224, 140, .85), 0 0 0 4px rgba(210, 215, 235, .6), 0 0 24px rgba(255, 210, 160, .45); }
/* Foil + Sparkle (inst.foil === 'both'): the holo sheen and the stones on one card, each on its own
   layer, and an edge of both colours. */
.tcard.is-foil.is-sparkle { box-shadow: 0 0 0 2px rgba(200, 225, 255, .9), 0 0 0 4px rgba(210, 215, 235, .6), 0 0 22px rgba(170, 190, 255, .5); }
/* CONVERGENCE (economy.js isConvergence): Multiverse, holo and sparkle on one card. The edge runs
   through every colour, slowly, and the card names itself at the foot of the art. */
.tcard.is-convergence { animation: converge-edge 6s linear infinite; }
@keyframes converge-edge {
  0%   { box-shadow: 0 0 0 2px #ffd36a, 0 0 0 4px rgba(255, 120, 170, .6), 0 0 30px rgba(255, 190, 110, .55); }
  33%  { box-shadow: 0 0 0 2px #8ff0ff, 0 0 0 4px rgba(170, 130, 255, .6), 0 0 30px rgba(120, 220, 255, .55); }
  66%  { box-shadow: 0 0 0 2px #c9a0ff, 0 0 0 4px rgba(120, 255, 190, .6), 0 0 30px rgba(200, 150, 255, .55); }
  100% { box-shadow: 0 0 0 2px #ffd36a, 0 0 0 4px rgba(255, 120, 170, .6), 0 0 30px rgba(255, 190, 110, .55); }
}
.tc-converge {
  position: absolute; left: 50%; bottom: 6px; transform: translateX(-50%); z-index: 3; pointer-events: none;
  padding: 2px 9px; border-radius: 999px; white-space: nowrap;
  font: 700 9px/1.4 system-ui, sans-serif; letter-spacing: .18em; text-transform: uppercase; color: #fff;
  background: linear-gradient(90deg, #ff6fa8, #ffd36a, #7dffc4, #7fc8ff, #c89bff);
  text-shadow: 0 1px 2px rgba(0, 0, 0, .6); box-shadow: 0 0 10px rgba(255, 255, 255, .35);
}
.tcard.is-foil.is-sparkle.is-shiny { box-shadow: 0 0 0 2px rgba(255, 224, 140, .9), 0 0 0 4px rgba(200, 225, 255, .65), 0 0 0 6px rgba(210, 215, 235, .45), 0 0 28px rgba(255, 210, 160, .5); }
.tc-stars { color: #FFD76A; letter-spacing: -1px; text-shadow: 0 0 6px rgba(255, 200, 80, .8); }
.tcard.is-ascended { box-shadow: 0 0 0 2px rgba(255, 200, 80, .6), 0 0 28px rgba(255, 190, 70, .45); }
@media (prefers-reduced-motion: reduce) { .tcard.is-foil .tc-art::before, .tcard.is-foil .tc-art::after, .tcard.is-sparkle .tc-art::after { animation: none; } .tc-gems i, .tcard.is-convergence { animation: none !important; } }

/* WHAT ETHER BUYS FOR A CARD (balance.js CARD_EDGES, EDGE_ETHER, HALO_ETHER). Foils are found, never
   bought (Dom, 2026-09-29), so both of these live on the FRAME and never touch the painting: a
   bought look must never be mistaken for a found foil.
   GILDED EDGE: a metal ring round the face (only the border shows, through a mask), with a band
   of light running slowly round it. The metals are three color stops each. */
/* z-index 3, never more: a card makes no stacking context of its own, so this competes at the ROOT
   with the collection's sticky team panel (4) and at 5 drew the edge over it as cards scrolled
   underneath (Dom, 2026-09-29: "There is a display bug for cards with frames"). 3 still clears the
   art (1) and the rarity wash (2). */
.tc-edge { position: absolute; inset: 0; z-index: 3; pointer-events: none;
  --eb: calc(7 * var(--u));
  padding: var(--eb);
  background:
    linear-gradient(115deg, transparent 38%, rgba(255, 255, 255, .85) 48%, transparent 58%) 0 0 / 300% 300% no-repeat,
    linear-gradient(135deg, var(--m1), var(--m2) 30%, var(--m3) 50%, var(--m2) 70%, var(--m1));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .45);
  animation: edge-run 6s linear infinite; }
@keyframes edge-run { from { background-position: 120% 120%, 0 0; } to { background-position: -120% -120%, 0 0; } }
/* The metals are set on the element from EDGE_CATALOG (ui.js titanCard). */
/* HALO: the Titan's element color (or the player's pick, --hc) breathing round the face. On the face, not the card, so it
   hugs the picture and not the footer under it. */
.tcard.is-halo .tc-face { box-shadow: 0 0 calc(12 * var(--u)) calc(1 * var(--u)) color-mix(in srgb, var(--hc, var(--eg)) 60%, transparent), inset 0 0 0 1px #2A313B;
  animation: halo-breathe 3.6s ease-in-out infinite alternate; }
.tcard.framed.is-halo .tc-face { box-shadow: 0 0 calc(12 * var(--u)) calc(1 * var(--u)) color-mix(in srgb, var(--hc, var(--eg)) 60%, transparent),
  inset 0 0 0 1px var(--fedge), inset 0 0 0 3px var(--finner); }
@keyframes halo-breathe { from { filter: brightness(1); } to { filter: brightness(1.06) drop-shadow(0 0 calc(5 * var(--u)) color-mix(in srgb, var(--hc, var(--eg)) 70%, transparent)); } }
@media (prefers-reduced-motion: reduce) { .tc-edge, .tcard.is-halo .tc-face { animation: none; } }
/* CARD LOOKS (app.js looksPanel): under the card in the Titan detail. Open for good on a desktop,
   where the heading is just a heading; a fold behind it on a phone. */
.looks { margin-top: 16px; }
.looks-head { list-style: none; cursor: default; pointer-events: none; font-family: var(--font-display, inherit); font-size: 1.05rem;
  letter-spacing: .06em; margin-bottom: 10px; color: #E6E9ED; }
.looks-head::-webkit-details-marker { display: none; }
.looks-body { display: grid; gap: 12px; }
.looks-note { margin: 0; }
.looks-buys { margin: 0; }
@media (max-width: 900px) {
  .looks-head { pointer-events: auto; cursor: pointer; display: flex; align-items: center; justify-content: space-between;
    padding: 10px 14px; margin: 0; border: 1px solid rgba(255, 255, 255, .12); background: rgba(255, 255, 255, .03); }
  .looks-head::after { content: '\25BE'; transition: transform .2s ease; }
  .looks[open] > .looks-head { margin-bottom: 12px; }
  .looks[open] > .looks-head::after { transform: rotate(180deg); }
}
/* An edge the card does not own yet: tapping it previews it, and the + says it is for sale. */
.look-tabs .tab.unowned { color: #9AA3AE; }
.look-tabs .tab.unowned::after { content: '+'; margin-left: 3px; opacity: .75; }
.look-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.look-label { min-width: 44px; font-size: .78rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted, #9AA3AE); }
.look-tabs { margin: 0; }
.look-tabs .tab { padding: 0 10px; letter-spacing: .03em; }
/* It grows a segment for every edge a card owns, seasonal ones included, so it wraps rather than
   squeezing them. */
.look-tabs { flex-wrap: wrap; }
@media (max-width: 760px) { .look-tabs { flex: 1 1 100%; grid-auto-flow: row; grid-template-columns: repeat(auto-fit, minmax(72px, 1fr)); }
  .look-tabs .tab { padding: 0 2px; font-size: .74rem; letter-spacing: 0; } }
.halo-swatches { display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  /* Room for the selected ring (a 2px outline 2px out): the dialog body clips at its left edge,
     which cut the ring off "Off" (Dom, 2026-09-29). */
  padding: 5px; }
/* On the big card while a look it does not own is being tried. */
.preview-tag { position: absolute; top: 62%; left: 50%; transform: translateX(-50%); z-index: 6;
  display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 8px 10px;
  background: rgba(10, 12, 16, .9); border: 1px solid #FFC94D; }
.preview-tag > span { color: #FFE7A3; font-size: .72rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; white-space: nowrap; }
.preview-tag .btn { white-space: nowrap; }
.swatch { width: 30px; height: 30px; border-radius: 50%; border: 2px solid rgba(255, 255, 255, .18); cursor: pointer; padding: 0;
  background: var(--sw); box-shadow: 0 0 8px color-mix(in srgb, var(--sw) 55%, transparent); }
.swatch.off { width: auto; border-radius: 15px; padding: 0 12px; background: transparent; box-shadow: none; color: #C9D0D8; font-size: .75rem; }
.swatch.on { border-color: #fff; outline: 2px solid color-mix(in srgb, var(--sw, #fff) 70%, transparent); outline-offset: 2px; }
.swatch:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

/* The cookie banner (client/js/track.js). Only ever shown when a tag is configured. */
.consent { position: fixed; left: 12px; right: 12px; bottom: calc(12px + env(safe-area-inset-bottom, 0px)); z-index: 9000; max-width: 560px; margin: 0 auto;
  display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: center; padding: 12px 14px; border-radius: 10px;
  background: rgba(14, 16, 22, .96); border: 1px solid rgba(255, 255, 255, .12); box-shadow: 0 8px 30px rgba(0, 0, 0, .5); color: #d9d4c7; font-size: 13px; }
.consent p { margin: 0; flex: 1 1 260px; line-height: 1.45; }
.consent a { color: #e0b25a; }
.consent-btns { display: flex; gap: 8px; margin-left: auto; }

/* The starter choice when painting is down on the first conjure (conjure.js offerStarter). */
.starter-pick { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; }

/* Where other players see your looks (Dom, 2026-09-30). The leaderboard's podium: the top three
   and the lead Titan each is seen with. */
.lb-podium { display: grid; grid-template-columns: repeat(3, minmax(0, 200px)); justify-content: center; gap: 16px; margin: 0 0 20px; align-items: end; }
.lb-podium-slot { display: flex; flex-direction: column; align-items: center; gap: 8px; text-align: center; min-width: 0; }
.lb-podium-slot .tcard { width: 100%; }
.lb-podium-slot.p1 { order: 2; } .lb-podium-slot.p2 { order: 1; } .lb-podium-slot.p3 { order: 3; }
.lb-podium-slot.p2, .lb-podium-slot.p3 { padding-top: 28px; }
.lb-place { font-family: var(--display); letter-spacing: .14em; text-transform: uppercase; font-size: .8rem; color: #C9CED8; }
.lb-podium-slot.p1 .lb-place { color: #FFD76A; }
@media (max-width: 760px) { .lb-podium { gap: 8px; } .lb-podium-slot.p2, .lb-podium-slot.p3 { padding-top: 16px; } }
/* The Square off: both players and their squads, face up, before the lead is chosen. */
.squareoff .modal-title { text-align: center; font-family: var(--display); letter-spacing: .2em; text-transform: uppercase; }
.sq-grid { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 16px; }
.sq-side { display: flex; flex-direction: column; gap: 10px; min-width: 0; animation: sq-in .45s ease-out both; }
.sq-side.them { animation-name: sq-in-r; }
.sq-side .pbanner { margin: 0; }
.sq-name { display: flex; flex-direction: column; gap: 2px; padding: 10px 14px; border: 1px solid var(--line); }
.sq-record { font-size: .85rem; color: var(--muted); letter-spacing: .04em; }
.sq-record b { color: var(--text); }
.sq-cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
.sq-card { min-width: 0; animation: sq-card .4s ease-out both; animation-delay: calc(.25s + var(--i) * .12s); }
.sq-card .tcard { width: 100%; }
.sq-record { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.sq-streak { font-size: .72rem; letter-spacing: .06em; text-transform: uppercase; padding: 1px 8px; border: 1px solid currentColor; }
.sq-streak.hot { color: #FFB25A; }
.sq-streak.cold { color: #8FA6C9; }
.sq-rival { text-align: center; margin: -4px 0 12px; font-family: var(--display); letter-spacing: .06em; color: var(--gold); }
.sq-hint { margin: 0; }
/* Your cards are the lead picker. */
.sq-pick { position: relative; display: flex; flex-direction: column; gap: 6px; padding: 4px; background: none; border: 1px solid transparent; color: var(--text); cursor: pointer; text-align: left; }
.sq-pick:hover { border-color: var(--line); }
.sq-pick.on { border-color: var(--gold); box-shadow: 0 0 0 1px var(--gold), 0 0 22px color-mix(in srgb, var(--gold) 35%, transparent); }
.sq-pick .lead-tags { justify-content: center; font-size: .6rem; gap: 4px; }
.sq-pick .lead-tags span { padding: 1px 5px; }
.sq-lead-tag { position: absolute; top: -9px; left: 50%; transform: translateX(-50%); display: none; background: var(--gold); color: #14100A; font-family: var(--display); font-size: .66rem; letter-spacing: .14em; text-transform: uppercase; padding: 1px 10px; z-index: 3; }
.sq-pick.on .sq-lead-tag { display: block; }
.sq-vs { font-family: var(--display); font-size: 2.2rem; color: var(--gold); letter-spacing: .08em; text-shadow: 0 0 18px color-mix(in srgb, var(--gold) 55%, transparent); animation: sq-vs .5s .15s ease-out both; }
@keyframes sq-in { from { opacity: 0; transform: translateX(-24px); } }
@keyframes sq-in-r { from { opacity: 0; transform: translateX(24px); } }
@keyframes sq-card { from { opacity: 0; transform: translateY(14px) rotateY(70deg); } }
@keyframes sq-vs { from { opacity: 0; transform: scale(1.8); } }
@media (max-width: 760px) {
  .sq-grid { grid-template-columns: 1fr; gap: 8px; }
  /* On a phone the page is read top down, so what you face comes first and your pick sits right
     above the Fight button. */
  .sq-side.them { order: 0; } .sq-vs { order: 1; } .sq-side.me { order: 2; }
  .sq-vs { text-align: center; font-size: 1.6rem; }
}
@media (prefers-reduced-motion: reduce) { .sq-side, .sq-card, .sq-vs { animation: none; } }

/* ACCOUNTS FOR NEW PLAYERS (Dom, 2026-09-30). The intro's way in for someone who already has an account: quiet, under the
   one gold button, fading in with the rest of the intro. */
.conjure .intro .intro-login { margin-top: 0; padding: 9px 20px; font-size: .95rem; letter-spacing: .04em; text-transform: none;
  color: #ECE6DA; background: rgba(8, 9, 12, .62); border: 1px solid rgba(236, 230, 218, .32); backdrop-filter: blur(3px); }
.conjure .intro .intro-login:hover, .conjure .intro .intro-login:focus-visible { border-color: rgba(236, 230, 218, .7); background: rgba(8, 9, 12, .8); }
/* a guest on Home: a line under their banner with the button that signs them in */
.guest-login { display: flex; gap: 10px; align-items: center; margin: 6px 0 4px; }
/* the sign-in form opened in place on the home page (authForm), kept to the hero column's reading width */
.home-login { max-width: 380px; margin: 4px 0 10px; padding: 14px 16px; background: rgba(12, 15, 20, .82); border: 1px solid rgba(255, 255, 255, .1); border-radius: 6px; }
.home-login[hidden] { display: none; }
.home-login .form { margin: 0; }
/* KEEP YOUR FIRST TITAN: the card it came through as beside the form that keeps it */
.keep-first { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 28px; align-items: start; max-width: 880px; margin: 0 auto; padding: 18px 0; }
.keep-first .keep-card { width: 250px; max-width: 100%; }   /* (a card fills its column; an auto column is as wide as its content, zero) */
.keep-first .keep-copy h2 { margin: 0 0 6px; font-family: var(--display); letter-spacing: .08em; }
.keep-first .keep-copy > p { margin: 0 0 14px; max-width: 52ch; }
.keep-first .optin-note { margin-top: 2px; }
@media (max-width: 720px) { .keep-first { grid-template-columns: 1fr; justify-items: center; gap: 16px; } .keep-first .keep-copy { width: 100%; }
  .keep-first .keep-card { width: 170px; } }   /* (small on a phone, so the form is in the first screenful) */

/* ---- the zone map's win: the stop just beaten flares, and the next one takes the light ---- */
.zspot.just-won .zmark { animation: zspotWon 900ms 250ms cubic-bezier(.2, 1.4, .4, 1) both; }
.zspot.just-won::after { content: ''; position: absolute; left: 50%; top: 50%; width: 100%; height: 100%;
  transform: translate(-50%, -50%); border-radius: 50%; border: 2px solid var(--e); pointer-events: none;
  animation: zspotRing 1000ms 300ms ease-out both; }
@keyframes zspotWon { 0% { transform: scale(1); filter: brightness(1); } 35% { transform: scale(1.7); filter: brightness(2.2); } 100% { transform: scale(1); filter: brightness(1); } }
@keyframes zspotRing { 0% { opacity: .95; transform: translate(-50%, -50%) scale(.6); } 100% { opacity: 0; transform: translate(-50%, -50%) scale(3.6); } }
.zspot.arrived .zmark { animation: zspotArrive 700ms cubic-bezier(.2, 1.5, .4, 1) both; }
@keyframes zspotArrive { 0% { transform: scale(.5); } 60% { transform: scale(1.35); } 100% { transform: scale(1); } }
@media (prefers-reduced-motion: reduce) { .zspot.just-won .zmark, .zspot.just-won::after, .zspot.arrived .zmark { animation: none; } }

/* ---- the boss entrance (battle.js bossIntro) ---- */
.boss-intro { position: fixed; inset: 0; z-index: 90; display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  align-items: center; gap: 2vw; padding: 4vh 6vw; overflow: hidden; cursor: pointer;
  background: radial-gradient(90% 70% at 30% 50%, color-mix(in srgb, var(--c) 30%, transparent), transparent 70%),
              linear-gradient(100deg, rgba(4, 5, 7, .96), rgba(4, 5, 7, .88) 55%, rgba(4, 5, 7, .96));
  animation: biIn .35s ease-out both; }
.boss-intro.guard { --c: #C9D3E0; }
.boss-intro.final { --c: #E5324A; }
.boss-intro.out { animation: biOut .38s ease-in both; }
.bi-slash { position: absolute; left: -10%; right: -10%; height: 3px; background: linear-gradient(90deg, transparent, var(--c), transparent);
  box-shadow: 0 0 18px var(--c); transform: rotate(-8deg); opacity: .8; }
.bi-slash.a { top: 22%; animation: biSlash .7s .1s cubic-bezier(.2, .8, .2, 1) both; }
.bi-slash.b { bottom: 22%; animation: biSlash .7s .25s cubic-bezier(.2, .8, .2, 1) both reverse; }
.bi-art { justify-self: end; width: min(46vw, 70vh); aspect-ratio: 1; display: grid; place-items: center;
  filter: drop-shadow(0 0 40px color-mix(in srgb, var(--c) 55%, transparent)); animation: biArt .6s .12s cubic-bezier(.2, 1.3, .3, 1) both; }
.bi-art img { width: 100%; height: 100%; object-fit: contain; object-position: 50% 100%; }
.bi-text { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.bi-title { font-family: var(--display); letter-spacing: .32em; text-transform: uppercase; font-size: clamp(.8rem, 1.6vw, 1.1rem); color: var(--c);
  animation: biUp .5s .45s ease-out both; }
.bi-name { font-family: var(--display); font-weight: 700; line-height: .95; font-size: clamp(2.4rem, 7.5vw, 6rem); color: #FFFFFF;
  text-transform: uppercase; letter-spacing: .04em; text-shadow: 0 0 28px color-mix(in srgb, var(--c) 60%, transparent), 0 4px 0 rgba(0, 0, 0, .6);
  overflow-wrap: anywhere; animation: biName .6s .6s cubic-bezier(.2, 1.4, .3, 1) both; }
.bi-meta { font-size: clamp(.8rem, 1.4vw, 1rem); letter-spacing: .14em; text-transform: uppercase; color: var(--st-light); animation: biUp .5s .85s ease-out both; }
.bi-desc { max-width: 42ch; font-style: italic; color: var(--muted); font-size: clamp(.85rem, 1.3vw, 1rem); animation: biUp .5s 1.05s ease-out both; }
.bi-skip { position: absolute; bottom: 4vh; left: 50%; transform: translateX(-50%); font-size: .72rem; letter-spacing: .2em; text-transform: uppercase;
  color: var(--muted); animation: biUp .4s 1.4s ease-out both; }
/* The first of them all takes longer to arrive. */
.boss-intro.final .bi-art { animation-duration: 1.1s; animation-delay: .5s; }
.boss-intro.final .bi-title { animation-delay: 1.2s; }
.boss-intro.final .bi-name { animation-delay: 1.45s; font-size: clamp(2.6rem, 8.5vw, 7rem); }
.boss-intro.final .bi-meta { animation-delay: 1.8s; }
.boss-intro.final .bi-desc { animation-delay: 2.05s; }
.bi-shake { animation: biShake .5s linear; }
@keyframes biIn { from { opacity: 0; } }
@keyframes biOut { to { opacity: 0; transform: scale(1.04); } }
@keyframes biSlash { from { transform: rotate(-8deg) translateX(-100%); opacity: 0; } }
@keyframes biArt { from { opacity: 0; transform: translateX(18vw) scale(.8); filter: brightness(3) drop-shadow(0 0 60px var(--c)); } }
@keyframes biUp { from { opacity: 0; transform: translateY(14px); } }
@keyframes biName { from { opacity: 0; transform: scale(1.6); letter-spacing: .4em; } }
@keyframes biShake { 0%, 100% { transform: none; } 20% { transform: translate(-6px, 3px); } 40% { transform: translate(5px, -4px); } 60% { transform: translate(-4px, 2px); } 80% { transform: translate(3px, -2px); } }
@media (max-width: 760px) {
  .boss-intro { grid-template-columns: 1fr; grid-template-rows: auto auto; align-content: center; text-align: center; padding: 6vh 6vw; gap: 2vh; }
  .bi-art { justify-self: center; width: min(78vw, 46vh); }
  .bi-text { align-items: center; }
  .bi-art { animation-name: biArtUp; }
}
@keyframes biArtUp { from { opacity: 0; transform: translateY(8vh) scale(.8); filter: brightness(3); } }
@media (prefers-reduced-motion: reduce) {
  .boss-intro, .boss-intro *, .bi-shake { animation: none !important; }
}

/* ---- the Perfect Throw (battle.js aimThrow) ---- */
.aim-throw { position: fixed; inset: 0; z-index: 80; cursor: crosshair; touch-action: none; }
.aim-throw .aim-target, .aim-throw .aim-ring { position: absolute; left: var(--x); top: var(--y); width: var(--s); height: var(--s);
  border-radius: 50%; transform: translate(-50%, -50%); pointer-events: none; }
.aim-throw .aim-target { border: 3px solid rgba(255, 255, 255, .9); box-shadow: 0 0 0 2px rgba(0, 0, 0, .55), 0 0 18px rgba(61, 242, 255, .6), inset 0 0 18px rgba(61, 242, 255, .35); }
.aim-throw.pulse .aim-target { box-shadow: 0 0 0 2px rgba(0, 0, 0, .55), 0 0 18px rgba(255, 61, 166, .6), inset 0 0 18px rgba(255, 61, 166, .35); }
.aim-throw .aim-ring { border: 4px solid #3DF2FF; box-shadow: 0 0 12px #3DF2FF; will-change: transform; }
.aim-throw.pulse .aim-ring { border-color: #FF3DA6; box-shadow: 0 0 12px #FF3DA6; }
.aim-throw .aim-tip { position: absolute; left: clamp(110px, var(--x), calc(100vw - 110px)); top: min(calc(var(--y) + var(--s) * .5 + 18px), calc(100vh - 60px)); transform: translateX(-50%);
  font-family: var(--display); font-weight: 700; letter-spacing: .14em; text-transform: uppercase; font-size: .9rem; color: #FFFFFF;
  background: rgba(8, 9, 11, .78); padding: 4px 12px; white-space: nowrap; pointer-events: none; }
.aim-throw.perfect .aim-tip { color: #FFD76A; font-size: 1.15rem; }
.aim-throw.perfect .aim-target { border-color: #FFD76A; box-shadow: 0 0 0 2px rgba(0, 0, 0, .55), 0 0 40px #FFD76A, inset 0 0 30px #FFD76A; animation: aimHit .6s ease-out both; }
.aim-throw.good .aim-tip { color: #7FE9FF; }
.aim-throw.off .aim-tip { color: var(--muted); }
.aim-throw.perfect .aim-ring, .aim-throw.good .aim-ring, .aim-throw.off .aim-ring { opacity: 0; transition: opacity .2s; }
@keyframes aimHit { 0% { transform: translate(-50%, -50%) scale(1); } 40% { transform: translate(-50%, -50%) scale(1.25); } 100% { transform: translate(-50%, -50%) scale(1); opacity: 0; } }

/* ---- the enrage: the call over the action bar, the timed Brace, the glow on the foe ---- */
.enrage-call { position: relative; display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; margin-bottom: 8px; padding: 8px 12px 12px;
  background: linear-gradient(90deg, rgba(229, 50, 74, .28), rgba(229, 50, 74, .08)); border: 1px solid rgba(229, 50, 74, .7); overflow: hidden;
  animation: enrageIn .35s ease-out both; }
.enrage-call b { font-family: var(--display); letter-spacing: .12em; text-transform: uppercase; color: #FF8FA3; }
.enrage-call span { font-size: .85rem; color: var(--text); }
.enrage-call.late { background: rgba(229, 50, 74, .1); border-color: rgba(229, 50, 74, .35); }
.enrage-timer { position: absolute; left: 0; bottom: 0; height: 4px; width: 100%; background: #FF8FA3; transform-origin: 0 50%; animation: enrageTimer var(--t) linear forwards; }
@keyframes enrageTimer { from { transform: scaleX(1); } to { transform: scaleX(0); } }
@keyframes enrageIn { from { opacity: 0; transform: translateY(6px); } }
.btn.brace { background: linear-gradient(180deg, #C4364A, #7E121F); border-color: #FF8FA3; animation: bracePulse 1s ease-in-out infinite; }
.btn.brace small { opacity: .7; margin-left: 6px; }
@keyframes bracePulse { 50% { box-shadow: 0 0 0 3px rgba(255, 143, 163, .35), 0 0 18px rgba(229, 50, 74, .6); } }
.sprite.enraged img, .sprite.enraged canvas { filter: drop-shadow(0 0 14px rgba(229, 50, 74, .9)) saturate(1.3); animation: enragedThrob 1.2s ease-in-out infinite; }
@keyframes enragedThrob { 50% { filter: drop-shadow(0 0 26px rgba(229, 50, 74, 1)) saturate(1.5) brightness(1.1); } }
@media (prefers-reduced-motion: reduce) {
  .enrage-call, .btn.brace, .sprite.enraged img, .sprite.enraged canvas, .aim-throw.perfect .aim-target { animation: none; }
  .enrage-timer { animation-duration: 0s; }
}
