/* =====================================================================
   X3 Suki — suki.css
   ===================================================================== */

:root {
  --void:    #070510;
  --bg:      #0d0a1a;
  --panel:   #14102440;
  --panel-2: #1a1430;
  --line:    #2e2450;
  --line-lit:#4a3880;

  --ink:     #f4eeff;
  --ink-2:   #b9abd8;
  --ink-3:   #7a6ba0;
  --ink-4:   #4c4270;

  --pink:    #ff6f91;
  --violet:  #c05fd6;
  --cyan:    #5fd6ff;
  --gold:    #ffc83d;
  --good:    #4ade80;
  --bad:     #ff5a6e;
  --neon:    #9affb0;   /* kiosk neon — same accent ArenaFX draws the grid with */
  --lilac:   #d7b8ff;

  /* Band colours doubled as the odds-row label colours, and at these
     saturations the labels sank into the 3D backdrop. Lifted toward white. */
  --c-common:   #c3c9dd;
  --c-uncommon: #7cf0a6;
  --c-rare:     #93cdff;
  --c-mythic:   #ffd978;

  --tabbar-h: 45px;

  --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;
  --sans: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
}

* { box-sizing: border-box; }

/* `hidden` means hidden.

   The browser's own `[hidden] { display: none }` is an ordinary low-priority
   rule, so ANY author rule that sets `display` on the same element silently
   beats it — and a component that lays itself out with grid or flex sets
   `display` by definition. The result is an element that reports
   `el.hidden === true`, is invisible, and still occupies its full height.

   Measured: the sign-in card was 540px tall in a 390px landscape viewport,
   because `#gateInsecure` (104px), `#gateNewWrap` (48px) and `#gateRefWrap`
   (62px) were all "hidden" and all still taking their space. That pushed the
   submit button below the fold — the exact symptom of the dead sign-in
   button, arriving by a second route.

   `.gate[hidden]` used to carry a one-off fix for this. One-off fixes for
   this are how it comes back, so the rule is general. !important is right
   here: `hidden` is the page saying an element is not relevant, and a
   `display` that resurrects it is a bug, not a preference. */
[hidden] { display: none !important; }

/* THE PAGE IS A COLUMN, AND THE STAGE TAKES WHAT IS LEFT.

   The stage used to be sized by arithmetic — `calc(100dvh - 107px)` on tall
   screens, `calc(100dvh - var(--chrome-h, 84px))` on short ones. Both numbers
   are a guess at how tall the top bar and tab bar happen to be, and the guess
   was wrong: measured at four landscape sizes, the real chrome is 87px
   against the 84px assumed, so every landscape phone overflowed by exactly
   3px and the bottom of the tab bar was cut off. On a real handset the gap is
   worse, because the top bar grows with a larger system font, a safe-area
   inset, or its controls wrapping to a second line — and none of that is in
   the constant.

   Flex removes the guess. The column is exactly the viewport, the two bars
   take the height they need, and the stage gets the remainder whatever that
   turns out to be. `min-height: 0` on the stage is what lets it SHRINK below
   its content, which is the flex default nobody expects. */
body {
  margin: 0; background: var(--void); color: var(--ink);
  font-family: var(--sans); overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  /* HEIGHT, not just min-height.

     `min-height: 100dvh` alone lets the body grow past the viewport, and a
     flex container that is free to grow never asks its children to shrink —
     so `flex: 1 1 auto` on the stage did nothing and the column still
     overflowed, by 7px at 915x412 and 53px at 375x667. Bounding the container
     is what turns "take the remainder" into an instruction rather than a
     suggestion. min-height stays for browsers that treat dvh oddly mid-scroll. */
  height: 100dvh; min-height: 100dvh; display: flex; flex-direction: column;
}
/* Overlays are `position: fixed`, so they are out of flow and must not be
   handed a flex track — without this they become column items and reserve
   space in the layout they are supposed to float above. */
body > .reveal, body > .gate, body > .rotate, body > .sheet,
body > .zoom, body > .fx, body > #araya-fx { flex: none; }

/* ambient backdrop */
.bg-glow {
  position: fixed; inset: 0; pointer-events: none; z-index: 0;
  background:
    radial-gradient(ellipse 70% 50% at 20% 30%, #c05fd61a, transparent 60%),
    radial-gradient(ellipse 70% 50% at 80% 30%, #ff6f911a, transparent 60%),
    radial-gradient(ellipse 90% 60% at 50% 105%, #5fd6ff14, transparent 60%);
}
.bg-grid {
  position: fixed; inset: 0; pointer-events: none; z-index: 0; opacity: .35;
  background-image: linear-gradient(#ffffff06 1px, transparent 1px),
                    linear-gradient(90deg, #ffffff06 1px, transparent 1px);
  background-size: 54px 54px;
  mask-image: radial-gradient(ellipse 80% 60% at 50% 40%, #000 20%, transparent 75%);
}

/* ---------------------------------------------------------- top bar --- */
.topbar {
  --pos: relative; --z: 3;
  position: relative; z-index: 3;
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; padding: 12px 20px;
  border-bottom: 1px solid var(--line);
  background: #0a0816cc; backdrop-filter: blur(14px);
}
.brand { display: flex; align-items: baseline; gap: 9px; }
.brand__x {
  font-family: var(--mono); font-weight: 700; font-size: 19px; letter-spacing: .04em;
  background: linear-gradient(100deg, var(--violet), var(--pink));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.brand__n { font-size: 19px; font-weight: 600; letter-spacing: -.01em; }
.brand__tag {
  font-family: var(--mono); font-size: 8.5px; letter-spacing: .28em;
  text-transform: uppercase; color: var(--ink-4); padding-left: 4px;
}

.topbar__right { display: flex; align-items: center; gap: 10px; }
.bal {
  display: flex; align-items: baseline; gap: 7px;
  border: 1px solid var(--line-lit); background: #1a143080;
  padding: 7px 13px; border-radius: 999px;
}
.bal__k { font-family: var(--mono); font-size: 8.5px; letter-spacing: .2em;
          text-transform: uppercase; color: var(--ink-3); }
.bal__v { font-family: var(--mono); font-size: 15px; font-weight: 600; color: var(--gold);
          font-variant-numeric: tabular-nums; }
.bal.flash { animation: balflash .7s ease; }
@keyframes balflash {
  0%,100% { box-shadow: none; }
  30% { box-shadow: 0 0 0 3px #ffc83d44, 0 0 22px #ffc83d55; }
}

.btn {
  font-family: var(--sans); font-size: 12.5px; font-weight: 600;
  border: 1px solid var(--line-lit); background: #201a3a;
  color: var(--ink); padding: 8px 15px; border-radius: 8px; cursor: pointer;
  transition: background .15s, border-color .15s, transform .12s;
}
.btn:hover { background: #2a2250; border-color: var(--violet); }
.btn:active { transform: translateY(1px); }
.btn--ghost { background: transparent; }
.btn--sm { font-size: 11px; padding: 6px 11px; }
.btn--good { border-color: #4ade8055; color: var(--good); }
.btn--good:hover { background: #4ade8018; border-color: var(--good); }

/* ------------------------------------------------------------ stage --- */
.stage {
  --pos: relative; --z: 2;
  position: relative; z-index: 2;
  display: grid; grid-template-columns: 1fr minmax(320px, 470px) 1fr;
  align-items: end; gap: 0; overflow: hidden;
  flex: 1 1 auto; min-height: 0; height: auto;
  padding: 0 12px;
}
/* A floor only where there is room for one. Below this the stage simply takes
   what is left, which on a landscape phone is the whole point. */
@media (min-height: 700px) { .stage { min-height: 560px; } }

/* The girls are staff at the stall, not decals on the edges of the screen.
   They sit INWARD (so they overlap the kiosk's sides), lower (so the counter
   crosses them at waist height), and carry rim light in the kiosk's own neon
   so they belong to the same scene rather than floating in front of it. */
/* Sized by HEIGHT, not width. The figure is 300x560 now; sizing by width let
   the taller drawing overflow the stage and crop her head, which is exactly
   the "cut off" look. Filling the available height and letting width follow
   keeps her whole, crown to feet. */
.waifu {
  position: relative; align-self: stretch; justify-self: center;
  display: grid; align-items: end; justify-items: center;
  height: 100%; min-height: 0; pointer-events: none; user-select: none;
}
#waifuL { justify-self: end;   margin-right: -7%; }
#waifuR { justify-self: start; margin-left:  -7%; }

.waifu-svg {
  height: 100%; width: auto; max-width: 100%; display: block;
  filter:
    drop-shadow(0 26px 54px #000000b3)
    drop-shadow(0 0 22px var(--rim, #c05fd655))
    drop-shadow(0 0 60px var(--rim2, #ff6f9122));
}
#waifuL .waifu-svg { --rim: #c05fd666; --rim2: #5fd6ff1f; }
#waifuR .waifu-svg { --rim: #ff6f9166; --rim2: #c05fd61f; }

/* the stall's light pools at their feet */
.waifu::after {
  content: ''; position: absolute; left: 50%; bottom: 1%;
  width: 46%; height: 30px; transform: translateX(-50%);
  background: radial-gradient(ellipse at center, var(--rim, #c05fd655), transparent 70%);
  filter: blur(10px); opacity: .7; pointer-events: none;
}
.waifu__name {
  position: absolute; bottom: 7%; left: 50%; transform: translateX(-50%);
  font-family: var(--mono); font-size: 9px; letter-spacing: .34em;
  color: var(--ink-4); text-transform: uppercase; white-space: nowrap;
}

/* ------------------------------------------------------- carousel ----- */
.center {
  display: grid; align-content: center; justify-items: center; gap: 16px;
  padding: 18px 0 30px; position: relative;
}

.carousel { display: flex; align-items: center; gap: 14px; width: 100%; }
/* Arrows pick up the kiosk's neon green so they read against the dark 3D
   backdrop — they were --ink-2 on #16112c, which all but vanished once the
   stage went behind them. */
.carousel__arrow {
  flex: none; width: 44px; height: 44px; border-radius: 50%;
  border: 1px solid var(--neon); background: #0f2a1eb0;
  color: var(--neon); font-size: 20px; cursor: pointer; line-height: 1;
  backdrop-filter: blur(6px);
  box-shadow: 0 0 18px #9affb033, inset 0 0 12px #9affb01f;
  text-shadow: 0 0 10px #9affb088;
  transition: background .15s, color .15s, border-color .15s, box-shadow .2s;
}
.carousel__arrow:hover {
  background: var(--neon); color: #06210f; border-color: var(--neon);
  box-shadow: 0 0 26px #9affb077;
}
.carousel__arrow:disabled { opacity: .3; cursor: default; }
.carousel__arrow:disabled:hover {
  background: #0f2a1eb0; color: var(--neon); border-color: var(--neon);
  box-shadow: 0 0 18px #9affb033;
}

.packwrap { flex: 1; display: grid; place-items: center; min-height: 300px;
            perspective: 1100px; position: relative; }

/* The 3D canvas sits behind the pack text and never takes pointer events —
   the .pack element above it stays the click target, so the rip still works
   identically whether or not WebGL came up. */
/* The 3D canvas spans the WHOLE stage now, not the pack slot. Boxed into the
   centre column the kiosk could only ever be as tall as that column, so it
   was clipped at the top no matter how it was scaled. */
.stage3d { position: absolute; inset: 0; pointer-events: none; z-index: 0; }
.packwrap .pack { position: relative; z-index: 1; }
.stage > *:not(.stage3d) { position: relative; z-index: 1; }

/* With the orb live, the flat card becomes just its label: no plate, no
   border, no sheen — the text floats over the object. */
/* With the orb live the card becomes a label wrapped AROUND it: tier above,
   an empty middle row the orb shows through, price and hook below. Leaving
   the price in the middle put white text on a lit sphere, which is
   unreadable at every brightness the orb passes through while charging. */
.packwrap[data-3d="on"] .pack {
  background: transparent; border-color: transparent; box-shadow: none;
  height: 360px; width: 260px;
  grid-template-rows: auto 1fr auto auto; gap: 4px;
}
.packwrap[data-3d="on"] .pack__tier {
  grid-row: 1; text-align: center; justify-self: center;
  background: #0a0816b8; border: 1px solid var(--line-lit); border-radius: 999px;
  padding: 5px 14px; backdrop-filter: blur(6px);
}
/* The price landed on the orb's lower third, where white-on-lit-sphere is
   unreadable at some point in every charge cycle. A dark plate behind it —
   the same treatment the tier pill already gets — fixes it regardless of what
   the orb is doing, without moving the orb off the counter. */
.packwrap[data-3d="on"] .pack__mid  {
  grid-row: 3; justify-self: center; align-self: end;
  background: #0a0816b0; border: 1px solid var(--line-lit); border-radius: 14px;
  padding: 8px 22px 10px; backdrop-filter: blur(7px);
  box-shadow: 0 8px 28px #000000aa;
}
.packwrap[data-3d="on"] .pack__hook { grid-row: 4; }
.packwrap[data-3d="on"] .pack__price { font-size: 38px; }
.packwrap[data-3d="on"] .pack::after { display: none; }
.packwrap[data-3d="on"] .pack:hover { transform: translateY(-4px); box-shadow: none; }
.packwrap[data-3d="on"] .pack__price { text-shadow: 0 2px 26px #000, 0 0 40px #000; }
.packwrap[data-3d="on"] .pack__hook,
.packwrap[data-3d="on"] .pack__tier,
.packwrap[data-3d="on"] .pack__cards { text-shadow: 0 1px 12px #000, 0 0 22px #000; }

.pack {
  width: 208px; height: 290px; border-radius: 16px; position: relative;
  background: linear-gradient(155deg, #2a1a52, #160f2e 55%, #0f0a22);
  border: 1px solid var(--line-lit);
  box-shadow: 0 20px 50px #000000aa, inset 0 1px 0 #ffffff14;
  display: grid; grid-template-rows: auto 1fr auto; padding: 16px 14px;
  cursor: pointer; transform-style: preserve-3d;
  transition: transform .45s cubic-bezier(.2,.8,.2,1), box-shadow .3s, opacity .3s;
}
.pack:hover { transform: translateY(-7px) rotateX(3deg); box-shadow: 0 28px 62px #000000cc, 0 0 40px #c05fd633; }
.pack::after {
  content: ''; position: absolute; inset: 0; border-radius: 16px; pointer-events: none;
  background: linear-gradient(115deg, transparent 38%, #ffffff1f 47%, transparent 56%);
}
.pack__tier { font-family: var(--mono); font-size: 10px; letter-spacing: .3em;
              text-transform: uppercase; color: #fff; font-weight: 600; }
.pack__mid { display: grid; place-content: center; text-align: center; gap: 5px; }
.pack__price { font-size: 42px; font-weight: 700; letter-spacing: -.03em; line-height: 1;
               background: linear-gradient(150deg, #fff, #e6d6ff 62%, #d7b8ff);
               -webkit-background-clip: text; background-clip: text; color: transparent; }
.pack__cards { font-family: var(--mono); font-size: 10px; color: #fff; letter-spacing: .12em; }
.pack__hook { font-size: 11px; color: #f2edff; line-height: 1.45; text-align: center; }
.pack__hook b { color: var(--gold); }

.pack.switching { opacity: 0; transform: scale(.9); }

/* odds summary */
.odds { width: 100%; display: grid; gap: 5px; }
.odds__row { display: grid; grid-template-columns: 62px 1fr auto; align-items: center; gap: 9px;
             font-family: var(--mono); font-size: 10px; }
.odds__name { color: #fff; letter-spacing: .08em; }
.odds__bar { height: 5px; border-radius: 3px; background: #ffffff0e; overflow: hidden; }
.odds__bar i { display: block; height: 100%; border-radius: 3px; }
.odds__pct { color: #efeaff; font-variant-numeric: tabular-nums; min-width: 52px; text-align: right; }
.odds__val { color: #c5bbe4; font-size: 9px; }

/* The centre column now sits over the 3D stage, so the white type needs a dark
   halo or it disappears into the bright parts of the kiosk. .odds is a sibling
   of .carousel, not of .packwrap, so this can't key off [data-3d] — and the
   halo is invisible against the flat background anyway. */
.odds__name, .odds__pct, .odds__val, .disclose {
  text-shadow: 0 1px 10px #000, 0 0 18px #000;
}

.ripbtn {
  width: 100%; padding: 15px; border: 0; border-radius: 12px; cursor: pointer;
  font-family: var(--sans); font-size: 15px; font-weight: 700; letter-spacing: .02em;
  color: #fff; background: linear-gradient(100deg, var(--violet), var(--pink));
  box-shadow: 0 10px 30px #c05fd644; transition: transform .12s, box-shadow .2s, filter .2s;
}
.ripbtn:hover { filter: brightness(1.08); box-shadow: 0 14px 38px #c05fd666; }
.ripbtn:active { transform: translateY(2px); }
.ripbtn:disabled { filter: grayscale(.7) brightness(.6); cursor: default; box-shadow: none; }

.disclose {
  font-family: var(--mono); font-size: 8.5px; letter-spacing: .16em;
  color: #b6abd6; text-align: center; text-transform: uppercase;
  line-height: 1.7;
}

/* ------------------------------------------------------- reveal ------- */
/* The reveal used to wash the ENTIRE screen at 90% opacity and blur 9px,
   which meant the moment a card appeared the performers — the part that makes
   the rip feel like an event happening to someone — vanished. The dimming is
   now a centred radial: heavy where the card is, clearing to almost nothing at
   the sides where the girls stand, so they stay readable and keep reacting
   behind it. The blur follows the same idea by living on a pseudo-element
   that is masked to the middle rather than on the whole layer. */
.reveal {
  --z: 60;
  position: fixed; inset: 0; z-index: 60; display: none;
  place-items: center;
  background: radial-gradient(ellipse 58% 78% at 50% 48%,
              #05030cf2 0%, #05030ce0 38%, #05030c9e 64%, #05030c4d 84%, #05030c1a 100%);
}
.reveal::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  backdrop-filter: blur(7px);
  -webkit-mask-image: radial-gradient(ellipse 56% 76% at 50% 48%,
                      #000 0%, #000 46%, transparent 82%);
  mask-image: radial-gradient(ellipse 56% 76% at 50% 48%,
              #000 0%, #000 46%, transparent 82%);
}
.reveal.on { display: grid; }
/* The whole stage is a column that never exceeds the viewport. It used to be
   an auto-height grid, so a 12-card pack grew the multi row until the verdict
   and the Sell/Ship/Keep buttons were below the fold on a phone — the one
   screen where you MUST be able to reach them. Now the card and the actions
   are fixed rows and the card strip is the only thing that flexes. */
/* The card's geometry lives HERE, not on .rcard, because the stage has to
   know how tall the card is in order to place it — see the spacer below. */
.reveal__stage {
  --pad: 10px;                                    /* card top / side padding */
  --padb: 11px;                                   /* card bottom padding */
  --gap: 6px;                                     /* between the card's rows */
  --rar: 11px;                                    /* the rarity line */
  --meta: 62px;                                   /* name + set + value block */
  --sgap: clamp(8px, 2vh, 22px);                  /* between the stage's rows */
  --spad: 12px;                                   /* the stage's own padding */
  /* Spelled out from the parts rather than as one magic number, because a
     breakpoint that changes the padding and forgets the constant is exactly
     how `--chrome-h: 84px` came to be 3px short of the real chrome and spent
     an afternoon overflowing a landscape phone. */
  --chrome: calc(var(--meta) + var(--pad) + var(--padb) + 2 * var(--gap) + var(--rar));
  --avail: calc(100dvh - 300px);                  /* what the other rows leave */
  --cw: clamp(148px,
              min(252px, 66vw, (var(--avail) - var(--chrome)) / 1.4 + 2 * var(--pad)),
              252px);
  --cardh: calc((var(--cw) - 2 * var(--pad)) * 1.4 + var(--chrome));

  position: relative;
  display: flex; flex-direction: column; align-items: center;
  gap: var(--sgap); text-align: center;
  height: 100dvh; max-height: 100dvh; padding: var(--spad) 0; width: 100%;
}
/* WHY THE CARD SAT HIGH, AND WHAT THIS SPACER DOES.

   The stage is card / strip / verdict / buttons, centred as a block. Centring
   the BLOCK is not centring the CARD: everything below the card pushes its
   midpoint upward by half their combined height. Measured on a 1440x900
   desktop the card's centre landed at y=355 against a viewport centre of 450
   — 95px high, which is exactly the "not in the center of the view" that a
   measurement of the card against its own panel could never catch.

   So the space above the card is not a ratio that happens to look right, it
   is the exact arithmetic: half the screen, less half the card, less what the
   stage's own padding and first gap already contribute. `max(0px, ...)` and
   `flex-shrink: 1` mean it only ever spends room that exists — on the 390px
   landscape phone the rest of this file was fought over it collapses to
   nothing and the layout is what it was. ::after soaks up the remainder, so
   the strip and the buttons settle toward the bottom of the screen, which is
   also where they are easiest to reach. */
.reveal__stage::before,
.reveal__stage::after { content: ''; display: block; width: 0; min-height: 0; }
.reveal__stage::before {
  flex: 0 1 auto;
  height: max(0px, calc(50dvh - var(--cardh) / 2 - var(--spad) - var(--sgap)));
}
.reveal__stage::after { flex: 1 1 0; }

/* THE CARD IS DERIVED FROM ONE NUMBER.

   It used to be a fixed 232x324 with the art given whatever vertical space
   was left after the rarity line and the name block — about 210x216, a panel
   very nearly square. A 5:7 scan poured into a square box is 152px wide
   inside a 232px card: the picture was 66% of the frame, floating in purple,
   which is what made the reveal look nothing like the inventory tile the same
   picture renders in correctly.

   So the geometry is inverted. `--cw` is the only free variable; the art
   panel is the card's full inner width at a true 5:7 — the inventory tile's
   own rule — and the card's HEIGHT is whatever that implies plus the fixed
   chrome. Nothing has to be re-tuned when the width changes, and the picture
   can never be letterboxed again, because the box it goes into is the shape
   of a card by construction.

   `--cw` is clamped by the height budget as well as the width, so the derived
   height cannot exceed the room the stage has: `(avail - chrome) / 1.4` is
   that arithmetic run backwards. */
.rcard {
  width: var(--cw);
  height: var(--cardh);
  flex: none;
  border-radius: 14px; position: relative;
  transform-style: preserve-3d; transition: transform 1.05s cubic-bezier(.3,.9,.25,1);
}
.rcard.flipped { transform: rotateY(180deg); }
.rcard__face {
  position: absolute; inset: 0; border-radius: 14px; backface-visibility: hidden;
  display: grid; overflow: hidden; border: 1px solid var(--line-lit);
}
.rcard__back {
  background: linear-gradient(150deg, #2b1a52, #120c26);
  display: grid; place-items: center;
}
.rcard__back svg { width: 74px; height: 74px; opacity: .5; }
/* `1fr` on the art row let a two-line card name push the set and value out of
   the card. minmax(0,1fr) lets the art shrink instead, which is the row that
   can afford to. */
.rcard__front {
  transform: rotateY(180deg);
  background: linear-gradient(165deg, #201640, #0f0a20);
  /* Rows are now: rarity / art (sized by its own 5:7) / a meta block of fixed
     height. Fixed, because the card's total height is computed from exactly
     these numbers — a meta row free to grow on a two-line card name would
     push the art out the bottom of a card that has no slack in it. The name
     is line-clamped to two lines and the row clips, so the value is never the
     thing that falls off. */
  /* `minmax(0, 1fr)` on the art row, not `auto`.
     With `auto` the row sizes to the panel and the panel's aspect-ratio sizes
     it back from the row — a two-way negotiation that a grid resolves by
     taking the item's min-content contribution, which for an aspect-ratio box
     is (row height x 5/7). On a landscape phone that came out as a 173px-wide
     panel inside a 118px-wide card: the picture hung 55px off the right-hand
     edge of its own card. Pinning the row to the leftover space makes the
     sizing one-directional — width in, height out — which is the only
     arrangement an aspect-ratio box can be trusted in. */
  grid-template-rows: var(--rar) minmax(0, 1fr) var(--meta);
  padding: var(--pad) var(--pad) var(--padb); gap: var(--gap);
  text-align: left; align-content: stretch;
}
/* THE ROW THAT MADE THE PICTURE DRIFT SIDEWAYS.

   `.rcard__set` is `white-space: nowrap` with an ellipsis, and a nowrap box's
   MIN-content width is the whole string — ellipsis or not. A grid column can
   never be narrower than its items' min-content, so on a landscape phone the
   single implicit column was sized by the set line ("Test Set · SR · fair
   abc123def4…", about 190px at 9px mono) rather than by the 114px card it
   lives in. The art panel was the right size all along; it was being centred
   in a column 76px too wide, which put it 38px to the right and hung it off
   the card's edge.

   `min-width: 0` lets every row shrink to the column instead of the other way
   round, which is what makes `text-overflow: ellipsis` actually do its job —
   an ellipsis on a box that refuses to get smaller never has anything to
   elide. Applied to all three rows because any of them could grow a long
   value later; the panel is pinned to `start` as well, so even a future
   over-wide row cannot push the picture off-centre again. */
.rcard__front > * { min-width: 0; }
.rcard__rar { font-family: var(--mono); font-size: 8.5px; letter-spacing: .26em; text-transform: uppercase; }
/* THE PANEL AND THE SPAN INSIDE IT.

   This used to be `place-items: center`, and that one word hid the card art
   for every rip. Centring a grid item stops it stretching, so #rArt shrank to
   its content — and its content is `.art`, which asks for `height: 100%` of
   #rArt. Parent sized to child, child sized to parent, both resolved to zero.
   The picture was in the DOM, loaded from the CDN at 400x558, and 0px tall.
   Nothing errored, so it read as "the images aren't implemented".

   The mini tiles escaped it because `.mini .art--mini` is absolutely
   positioned and therefore never asks its parent how tall it is. That is why
   the small cards showed art and the big one did not.

   Now the panel stretches its span, and the span does the centring — so the
   glyph is still centred and a picture has a definite box to fill. */
/* THE SAME RULE AS .item__art, AT REVEAL SIZE.

   Ryan's instruction was literal — "displayed like they are displayed in the
   inventory section, which is correct and properly sized" — so this is the
   inventory tile's geometry, not an approximation of it: aspect-ratio on the
   CONTAINER, the container at the full width it is given, and the picture
   filling it. Every earlier attempt put the ratio on the inner span instead
   and left the container square, which is the whole reason the picture ended
   up 66% of the card's width with purple down both sides.

   The tier colour is carried as an inset ring exactly as the inventory does
   it, so the two views read as the same object. */
/* MEASURED IN PIXELS, NOT NEGOTIATED.

   `aspect-ratio: 5/7` with `width: 100%` is the right rule for .item__art,
   where the tile's width is settled before the art is laid out. Inside the
   reveal card it is not: the art sits in a grid row, and a box with an aspect
   ratio can take its size from EITHER axis, so the row and the item each wait
   for the other. Chromium resolves that by falling back to the item's
   max-content contribution, and on a landscape phone the answer came out
   173x243 inside a 118px-wide card — the picture hanging 55px past the edge
   of its own card. `minmax(0, 1fr)` on the row and `min-width: 0` on the item
   did not shake it loose.

   The card's width is already derived from one variable, so the panel's size
   is arithmetic that is known before layout starts. Stating it outright ends
   the negotiation: exactly 5:7, exactly the card's inner width, on every
   screen, with nothing left to resolve. */
.rcard__art {
  width: calc(var(--cw) - 2 * var(--pad));
  height: calc((var(--cw) - 2 * var(--pad)) * 1.4);
  min-width: 0; min-height: 0; align-self: start; justify-self: start;
  border-radius: 7px; overflow: hidden; position: relative;
  border: 1px solid #ffffff12;
  box-shadow: 0 6px 18px #00000066, 0 0 0 1px var(--tier, transparent) inset;
  background: radial-gradient(ellipse at 50% 30%, #ffffff12, transparent 70%), #0c0818;
}
/* Carrying a picture, the panel steps back — the scan is its own frame.
   Matched with `:has()` because the class is toggled onto the SPAN by
   showReveal(), not onto the panel. */
.rcard__art:has(> .rcard__art--image) { background: #07050f; }

/* ABSOLUTE, and that is the whole point.

   `.art` asks for `height: 100%`, and everything inside `.art` is absolutely
   positioned, so it has no content to fall back on. Given a parent of auto
   height the two wait on each other and both resolve to zero — the bug that
   made the reveal art invisible for weeks while it rendered correctly in the
   inventory, where `.art--item` sits in a box with a definite size. Taking
   the span out of flow gives it that definite box unconditionally, whatever
   the panel's sizing happens to be. */
.rcard__art > span {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  font-size: 46px; opacity: .8;
}
/* Carrying a picture: no glyph metrics, no dimming. The font-size and opacity
   above are for the fallback mark, and applying them to a photograph washes
   it out for no reason. */
.rcard__art > span.rcard__art--image { font-size: 0; opacity: 1; }

/* ---------------------------------------------------------- card art ---
   One class for every place a card picture appears, because they all have
   the same two problems: real card scans arrive at inconsistent aspect
   ratios (5:7 for Weiss/One Piece, ~2.5:3.5 for Pokemon, and the CDNs
   crop differently), and any of them can 404. `object-fit: contain` on a
   fixed box solves the first without hiding any of the card; the onerror
   handler in app.js strips the src and drops through to the placeholder rule
   below for the second — a card with a broken image icon looks like the app
   is broken, which is worse than a card with a glyph on it. */
.art {
  display: block; position: relative; width: 100%; height: 100%;
  background: radial-gradient(ellipse at 50% 30%, #ffffff14, transparent 70%), #100a24;
  overflow: hidden;
}
/* Always painted, always behind. When the CDN request fails the <img> hides
   itself and this is what is left, so a missing picture degrades to a mark
   rather than to a hole. */
.art__ph {
  position: absolute; inset: 0; display: grid; place-items: center;
  font-size: 26px; color: var(--ink-3); opacity: .5;
}
.art__img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  /* CONTAIN, not cover.

     `cover` fills the box by cropping whatever does not fit, and the box is
     roughly square while a trading card is 5:7 — so it showed the top-left
     corner of the scan and cut off the rest. For a photo that is a reasonable
     trade; for the card someone just paid to pull, the whole card IS the
     thing, and a crop that hides the character and the name is the one
     outcome worth avoiding.

     Letterboxing is the cost, so the bars are the same near-black the panel
     already uses and read as a mount rather than as a mistake. */
  object-fit: contain; object-position: center;
  background: #07050f;
}

/* On the reveal the art fills the whole upper panel rather than sitting in
   a letterboxed frame — the picture IS the payoff. */
.rcard__art--image { background: #07050f; }
.rcard__art--image .art--reveal { border-radius: 8px; }
/* THE PRICE IS NEVER THE THING THAT GETS CUT.

   `--meta` is a hand-typed height that has to match a name plus a set line
   plus a value — the same shape of constant as `--chrome-h: 84px`, and it
   went wrong the same way. On a phone a two-line card name pushed the block
   to about 81px against a 62px row, and what hung off the bottom was the
   value: the one number the whole rip is about, showing as a sliced-off half.

   So the block is a grid whose FIRST row is the flexible one. The name gets
   whatever is left and clips — it is already line-clamped, so clipping is its
   normal behaviour — while the set line and the value are `auto` and are laid
   out before there is any question of running out of room. A long name now
   costs a line of the name, which is what it should have cost all along.

   `align-content: end` keeps the block on the card's baseline whether the
   name took one line or two. */
.rcard__front > :last-child {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto auto;
  align-content: end;
  min-height: 0; overflow: hidden;
}
.rcard__name {
  font-size: 14px; font-weight: 600; line-height: 1.28;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; min-height: 0;
}
.rcard__set  {
  font-family: var(--mono); font-size: 9px; color: var(--ink-3); margin-top: 4px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.rcard__val  { font-family: var(--mono); font-size: 20px; font-weight: 700; margin-top: 7px;
               font-variant-numeric: tabular-nums; }

.reveal__verdict { font-family: var(--mono); font-size: 11px; letter-spacing: .24em;
                   text-transform: uppercase; color: var(--ink-3); }
.reveal__actions { display: flex; gap: 10px; }

/* ------------------------------------------------ multi-card packs ----
   A CAROUSEL, not a wrapping grid. Twelve cards at 58x80 wrapped to three
   rows and pushed the reveal's action buttons off the bottom of a phone.
   One scrolling row is a fixed height whatever the pack size, scroll-snaps
   so a swipe lands on a card, and on a desktop it simply centres because the
   row is shorter than the track. */
.multi {
  display: flex; gap: 7px; justify-content: flex-start;
  max-width: min(640px, 92vw); width: max-content;
  margin: 0 auto; padding: 2px 4px 8px;
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin; scrollbar-color: var(--line-lit) transparent;
  /* fade the ends so a cut-off card reads as "more this way" */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 18px, #000 calc(100% - 18px), transparent);
  mask-image: linear-gradient(90deg, transparent, #000 18px, #000 calc(100% - 18px), transparent);
}
/* When the row fits, centre it; when it does not, start it at the left so
   the first card is never half-hidden behind the mask. */
.multi--fits { width: auto; justify-content: center; }
.multi::-webkit-scrollbar { height: 5px; }
.multi::-webkit-scrollbar-thumb { background: var(--line-lit); border-radius: 3px; }
.multi .mini { flex: 0 0 auto; scroll-snap-align: center; }
.multi__more {
  flex: 0 0 auto; align-self: center; font-family: var(--mono); font-size: 9px;
  letter-spacing: .16em; color: var(--ink-3); padding: 0 6px; white-space: nowrap;
}
.mini {
  width: 58px; height: 80px; border-radius: 6px; border: 1px solid var(--line-lit);
  background: linear-gradient(160deg, #1d1438, #0e0a1e);
  font-family: var(--mono); font-size: 8.5px;
  color: var(--ink-2); text-align: center; line-height: 1.25;
  /* `backwards` alone leaves the element at the keyframe's FROM state when
     the animation has not run — measured opacity 0 on every mini. `both`
     holds the TO state after it finishes, and the explicit `to` below means
     there is a real end state to hold. */
  opacity: 1;
  animation: minipop .4s both cubic-bezier(.2,1.4,.4,1);
  position: relative; overflow: hidden;
}
.mini .art--mini { position: absolute; inset: 0; }
/* the value sits ON the art, on a gradient scrim — a separate caption row
   would eat a third of a 58x80 tile */
.mini__val {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 9px 3px 3px;
  background: linear-gradient(transparent, #05030ce8 62%);
  font-weight: 700; color: #fff; text-shadow: 0 1px 4px #000;
}
@keyframes minipop {
  from { opacity: 0; transform: translateY(14px) scale(.8); }
  to   { opacity: 1; transform: none; }
}

/* particles */
.fx { --z: 59; position: fixed; inset: 0; pointer-events: none; z-index: 59; }

/* --------------------------------------------------------- panels ----- */
/* The sheet stops ABOVE the tab bar rather than covering it. Covering it
   traps the user inside whichever sheet they opened — measured: the Economy
   tab was unclickable with Collection open. */
.sheet {
  --z: 50;
  position: fixed; left: 0; right: 0; bottom: var(--tabbar-h); z-index: 50;
  max-height: calc(76dvh - var(--tabbar-h));
  background: #0c0819f2; border-top: 1px solid var(--line-lit);
  backdrop-filter: blur(16px);
  transform: translateY(calc(100% + var(--tabbar-h)));
  transition: transform .34s cubic-bezier(.2,.8,.2,1); overflow: auto;
}
.sheet.on { transform: none; }
.sheet:not(.on) { pointer-events: none; }
.sheet__head {
  position: sticky; top: 0; background: #0c0819f7; z-index: 2;
  display: flex; align-items: center; justify-content: space-between;
  padding: 15px 20px; border-bottom: 1px solid var(--line);
}
.sheet__title { font-size: 15px; font-weight: 600; }
.sheet__body { padding: 16px 20px 26px; }

.grid {
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fill, minmax(178px, 1fr));
}
.item {
  border: 1px solid var(--line); background: var(--panel-2); border-radius: 10px;
  padding: 11px; display: grid; gap: 7px;
}
/* 5:7 is the trading-card aspect; fixing it here means a grid of mixed
   sources (Weiss, One Piece, Pokemon) still lines up row to row. */
.item__art {
  aspect-ratio: 5 / 7; border-radius: 7px; overflow: hidden;
  border: 1px solid #ffffff12;
  box-shadow: 0 6px 18px #00000066, 0 0 0 1px var(--tier, transparent) inset;
  margin: -2px -2px 2px;
}
.item[data-status="SOLD_BACK"] .item__art { filter: grayscale(.85); }
.item__top { display: flex; justify-content: space-between; align-items: baseline; gap: 7px; }
.item__rar { font-family: var(--mono); font-size: 8px; letter-spacing: .2em; text-transform: uppercase; }
.item__val { font-family: var(--mono); font-size: 13px; font-weight: 700; color: var(--gold);
             font-variant-numeric: tabular-nums; }
.item__name { font-size: 12.5px; font-weight: 600; line-height: 1.3; }
.item__set { font-family: var(--mono); font-size: 9px; color: var(--ink-4); }
.item__acts { display: flex; gap: 6px; margin-top: 2px; }
.item__acts .btn { flex: 1; }
.item[data-status="SOLD_BACK"]        { opacity: .42; }
.item[data-status="REDEEMED_PHYSICAL"]{ border-color: #4ade8055; }
.badge {
  font-family: var(--mono); font-size: 8px; letter-spacing: .16em; text-transform: uppercase;
  padding: 3px 7px; border-radius: 4px; background: #ffffff0e; color: var(--ink-3);
}

.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(146px,1fr)); gap: 9px; }
.stat { border: 1px solid var(--line); background: var(--panel-2); border-radius: 9px; padding: 11px 13px; }
.stat__k { font-family: var(--mono); font-size: 8.5px; letter-spacing: .18em;
           text-transform: uppercase; color: var(--ink-3); }
.stat__v { font-family: var(--mono); font-size: 19px; font-weight: 700; margin-top: 5px;
           font-variant-numeric: tabular-nums; }
.empty { color: var(--ink-3); font-size: 13px; padding: 28px 0; text-align: center; }

/* ------------------------------------------------------------ tabs ---- */
.tabbar {
  --pos: relative; --z: 56;
  position: relative; z-index: 56; min-height: var(--tabbar-h);
  display: flex; border-top: 1px solid var(--line);
  background: #0a0816d9; backdrop-filter: blur(14px);
}
.tabbar button {
  flex: 1; background: transparent; border: 0; border-top: 2px solid transparent;
  padding: 13px 8px; cursor: pointer; color: var(--ink-3);
  font-family: var(--mono); font-size: 10px; letter-spacing: .2em; text-transform: uppercase;
  transition: color .15s, border-color .15s, background .15s;
}
.tabbar button:hover { color: var(--ink-2); }
.tabbar button[aria-selected="true"] {
  color: var(--ink); border-top-color: var(--violet); background: #c05fd612;
}

/* --------------------------------------------------------- responsive - */
/* The top bar's right-hand group is the only thing that overflows a phone —
   measured at 412px against a 390px viewport. Let it shrink rather than
   letting the whole page scroll sideways. */
@media (max-width: 560px) {
  .topbar { padding: 10px 12px; gap: 8px; }
  .topbar__right { gap: 6px; min-width: 0; }
  .bal { padding: 6px 10px; gap: 5px; }
  .bal__k { display: none; }
  .bal__v { font-size: 13.5px; }
  .btn--sm { padding: 6px 9px; font-size: 10.5px; }
  .brand__tag { display: none; }
}

@media (max-width: 900px) {
  .stage { grid-template-columns: 1fr; min-height: auto; padding: 0 16px; }
  .waifu { display: none; }
  .waifu--keep { display: block; width: 190px; margin: 0 auto; opacity: .9; }
  /* The three-column rule pins .center to column 2. Once the grid drops to a
     single column that pin CREATES an implicit second column and the whole
     pack, price and rip button slide off to the right — which is exactly how
     the landscape phone layout ended up stacked on top of the right-hand
     performer. Put it back in column 1 with the rest of the page. */
  .center { grid-column: 1; justify-self: center; width: 100%; max-width: 470px; }
}
@media (max-width: 420px) {
  .pack { width: 178px; height: 250px; }
  .pack__price { font-size: 34px; }
  /* Only the ceiling moves. Height still follows from width, so a narrow
     phone gets a smaller card of exactly the same shape rather than a second
     hand-typed pair of numbers that can drift out of 5:7. */
  .reveal__stage { --cw: clamp(148px, min(206px, 74vw, (var(--avail) - var(--chrome)) / 1.4 + 2 * var(--pad)), 206px); }
}

/* ============================================================== mobile ===
   A phone held sideways is the target shape: ~844 x 390, of which the stage
   gets about 280px once the topbar and tabbar are taken out. Nothing below
   is a width breakpoint — width is not the constraint on a landscape phone,
   HEIGHT is, and a width query would also fire on a small desktop window
   where there is plenty of room. So these key off height.

   Everything is sized in vh/dvh or clamp() rather than fixed px, so the same
   rules cover a 320-tall phone and a 500-tall foldable without a third set of
   numbers. ------------------------------------------------------------ */
@media (max-height: 560px) {
  .topbar { padding: 6px 12px; }
  .stage  { padding: 0 8px; }        /* height comes from flex now, not arithmetic */
  :root   { --tabbar-h: 38px; }

  /* Keep the column narrow on a wide-but-short screen. Full width would throw
     the carousel arrows out to the frame edges, on top of the performers. */
  .center {
    gap: clamp(4px, 1.4vh, 12px); padding: 4px 0 8px; align-content: center;
    width: min(440px, 64vw); justify-self: center; margin: 0 auto;
  }
  .carousel { gap: 10px; }
  .carousel__arrow { width: 36px; height: 36px; font-size: 17px; }

  .packwrap { min-height: 0; }
  .packwrap[data-3d="on"] .pack {
    height: clamp(150px, 46vh, 300px); width: clamp(180px, 34vh, 260px);
  }
  .packwrap[data-3d="on"] .pack__price { font-size: clamp(22px, 5.2vh, 38px); }
  .packwrap[data-3d="on"] .pack__mid { padding: 4px 14px 5px; border-radius: 10px; }
  .pack__tier { font-size: 9px; letter-spacing: .22em; }
  .pack__hook { display: none; }          /* the odds table says the same thing */

  .odds { gap: 2px; }
  .odds__row { grid-template-columns: 54px 1fr auto; gap: 7px; font-size: 9px; }
  .odds__val { display: none; }
  .ripbtn { padding: clamp(8px, 1.8vh, 15px); font-size: clamp(12px, 2.6vh, 15px); }
  .disclose { font-size: 7.5px; letter-spacing: .1em; line-height: 1.5; }
  .disclose br { display: none; }

  .tabbar button { font-size: 10px; }
  .reveal__stage { --sgap: 8px; --spad: 6px; }
  /* A landscape phone is height-bound, so the budget shrinks on both sides:
     a tighter meta block and less room left over by the strip and buttons,
     which the gap drop above also pays into. The clamp floor is 118px — below
     that the picture stops being worth looking at and the rip should be
     watched in portrait. */
  .reveal__stage {
    --meta: 44px; --pad: 7px; --padb: 8px; --gap: 4px;
    --avail: calc(100dvh - 152px);
    --cw: clamp(118px, min(208px, 34vh + 96px, (var(--avail) - var(--chrome)) / 1.4 + 2 * var(--pad)), 208px);
  }
  .rcard__name { font-size: 12px; }
  .rcard__val  { font-size: 16px; margin-top: 3px; }
  .mini { width: 44px; height: 61px; }
  .multi { padding-bottom: 5px; }
  .reveal__verdict { font-size: 9.5px; letter-spacing: .14em; }
  .reveal__actions { gap: 6px; flex-wrap: wrap; justify-content: center; }
  .reveal__actions .btn { font-size: 11px; padding: 7px 11px; }
}

/* Really short — a 320-tall phone, or a landscape phone with a browser chrome
   bar. Drop to the essentials: pack, price, rip. */
@media (max-height: 400px) {
  .odds { display: none; }
  .disclose { display: none; }
  .packwrap[data-3d="on"] .pack { height: clamp(120px, 52vh, 200px); }
}

/* ------------------------------------------------- rotate-to-landscape ---
   A web page cannot rotate the phone for you: screen.orientation.lock() only
   works from fullscreen or an installed PWA, and is refused outright on iOS
   Safari. The manifest asks for landscape when the app is added to the home
   screen; this is what happens the rest of the time — ask, and get out of the
   way the moment the phone turns. Portrait still WORKS, it is just cramped,
   so the overlay is dismissible rather than a wall. */
.rotate {
  --z: 90;
  position: fixed; inset: 0; z-index: 90; display: none;
  place-items: center; text-align: center; padding: 28px;
  background: #05030cf2; backdrop-filter: blur(10px);
}
.rotate__inner { display: grid; gap: 16px; justify-items: center; max-width: 320px; }
.rotate__icon {
  width: 62px; height: 62px; color: var(--neon);
  animation: rotatehint 2.6s ease-in-out infinite;
}
@keyframes rotatehint {
  0%, 38%  { transform: rotate(0deg); }
  55%, 88% { transform: rotate(-90deg); }
  100%     { transform: rotate(0deg); }
}
.rotate__title { font-size: 17px; font-weight: 700; }
.rotate__body  { font-size: 12.5px; color: var(--ink-2); line-height: 1.6; }

@media (orientation: portrait) and (max-width: 860px) {
  body.wants-landscape .rotate { display: grid; }
}
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/* ------------------------------------------------------ ARAYA pool pill - */
.pool {
  display: flex; align-items: center; gap: 7px; cursor: pointer;
  border: 1px solid var(--line-lit); background: #1a143080; color: var(--ink);
  padding: 7px 12px; border-radius: 999px; font-family: var(--mono);
  transition: border-color .2s, background .2s;
}
.pool:hover { border-color: var(--violet); }
.pool__dot { width: 7px; height: 7px; border-radius: 50%; background: var(--ink-4); flex: none; }
.pool[data-state="open"]   .pool__dot { background: var(--good); box-shadow: 0 0 8px var(--good); }
.pool[data-state="locked"] .pool__dot { background: var(--gold); box-shadow: 0 0 8px var(--gold); }
.pool[data-state="unknown"].pool__dot,
.pool[data-state="unknown"] .pool__dot { background: var(--bad); }
.pool[data-sim="1"] { border-style: dashed; border-color: var(--cyan); }
.pool__k { font-size: 8.5px; letter-spacing: .2em; color: var(--ink-3); }
.pool__v { font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; }
.pool__state { font-size: 8.5px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-3); }
.pool[data-state="open"]   .pool__state { color: var(--good); }
.pool[data-state="locked"] .pool__state { color: var(--gold); }
@media (max-width: 700px) { .pool__k, .pool__state { display: none; } }

/* pool card in the vault */
.poolcard {
  border: 1px solid var(--line-lit); background: var(--panel-2); border-radius: 10px;
  padding: 14px 16px; display: grid; gap: 10px;
}
.poolcard__row { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.poolcard__title { font-size: 13px; font-weight: 600; }
.poolcard__mc { font-family: var(--mono); font-size: 22px; font-weight: 700; color: var(--gold);
                font-variant-numeric: tabular-nums; }
.poolcard__bar { height: 8px; border-radius: 4px; background: #ffffff0e; overflow: hidden; }
.poolcard__bar i { display: block; height: 100%; border-radius: 4px;
                   background: linear-gradient(90deg, var(--violet), var(--pink)); transition: width .6s; }
.poolcard__meta { font-family: var(--mono); font-size: 9.5px; color: var(--ink-3); line-height: 1.7;
                  word-break: break-all; }
.poolcard__meta b { color: var(--ink-2); font-weight: 500; }
.poolcard[data-state="open"] .poolcard__bar i { background: var(--good); }
.poolcard__sim { font-family: var(--mono); font-size: 9px; letter-spacing: .2em; color: var(--cyan);
                 border: 1px dashed var(--cyan); padding: 2px 7px; border-radius: 4px; }

/* ledger */
.ledger { display: grid; gap: 3px; font-family: var(--mono); font-size: 10.5px; max-height: 320px; overflow: auto; }
/* Six columns now: the two on the right are what this entry did to YOUR
   balance and what the balance was afterwards. They are the columns that make
   a double debit visible — the amounts alone were all individually correct
   while a rip was taking money twice.

   Fixed widths on both, because a running balance you have to re-find on
   every row is a balance nobody reads down. */
.ledger__row { display: grid; grid-template-columns: 30px 112px 1fr auto 74px 78px; gap: 9px;
               align-items: baseline;
               padding: 5px 8px; border-radius: 5px; background: #ffffff05; }
.ledger__delta, .ledger__bal { text-align: right; font-variant-numeric: tabular-nums; }
.ledger__delta[data-dir="in"]   { color: var(--good); }
.ledger__delta[data-dir="out"]  { color: var(--bad); }
/* A movement that did not touch the wallet — house to pool, say. Dimmed to
   near-nothing so the eye runs past it down the column of real changes,
   while the entry itself stays in the log where it belongs. */
.ledger__delta[data-dir="flat"] { color: var(--ink-4); opacity: .5; }
.ledger__bal { color: var(--ink-2); font-weight: 600; }
/* A phone cannot carry six columns. Drop the flow description — it is the one
   that repeats what the type already said — rather than shrinking the
   numbers, which are the point. */
@media (max-width: 560px) {
  /* FIVE columns, not four. Hiding .ledger__flow removes an item from the
     grid, it does not remove a column — the remaining five items in a
     four-column track simply wrap onto a second row, which looks like a
     rendering fault rather than a narrow layout. */
  .ledger__row { grid-template-columns: 24px auto 1fr 60px 68px; gap: 7px; }
  .ledger__flow { display: none; }
  .ledger__type { font-size: 9.5px; }
}
.ledger__seq { color: var(--ink-4); }
.ledger__type { color: var(--violet); letter-spacing: .06em; }
.ledger__flow { color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ledger__flow b { color: var(--ink-2); font-weight: 500; }
.ledger__amt { color: var(--ink); font-variant-numeric: tabular-nums; }
.ledger__row[data-type="CARD_SELLBACK"] .ledger__amt,
.ledger__row[data-type="CREDIT_TOPUP"] .ledger__amt { color: var(--good); }
.ledger__row[data-type="PACK_BUY"] .ledger__amt,
.ledger__row[data-type="SHIP_COST"] .ledger__amt { color: var(--bad); }
.ledger__row[data-type="REVERSAL"] { background: #ff5a6e12; }

/* settings form */
.form { display: grid; gap: 12px; max-width: 640px; }
.form label { display: grid; gap: 5px; font-size: 12px; color: var(--ink-2); }
.form label small { color: var(--ink-4); font-size: 10.5px; margin-left: 6px; }
.form input {
  font-family: var(--mono); font-size: 12px; color: var(--ink);
  background: #0c0819; border: 1px solid var(--line-lit); border-radius: 7px; padding: 9px 11px;
}
.form input:focus { outline: none; border-color: var(--violet); }
.form__test { border: 1px dashed var(--cyan); padding: 10px; border-radius: 8px; }
.form__test > span { color: var(--cyan); }
.form__note { font-family: var(--mono); font-size: 10px; color: var(--ink-3); line-height: 1.7; margin: 4px 0 0; }

/* gated ship buttons */
.btn[disabled][data-reason] { position: relative; }
.btn[disabled] { opacity: .45; cursor: not-allowed; }
.gate-note { font-family: var(--mono); font-size: 9px; letter-spacing: .1em; color: var(--gold);
             text-transform: uppercase; margin-top: 4px; }


/* ===================================================== stacking rescue ===
   araya-fx injects `body > *:not(#araya-fx) { position: relative; z-index: 1 }`
   so page content sits above its backdrop canvas. That selector carries an
   ID's worth of specificity — (1,0,1) — so it beats every class rule in this
   file and silently un-fixes anything meant to be an overlay.

   This was patched once by listing the affected classes here, and the list
   rotted exactly the way lists do: `.gate` and `.rotate` were written later
   and nobody came back to add them. The consequence was not subtle and was
   not visible in dev — on the deployed site the sign-in wall stopped being a
   wall and became an ordinary block at the END of the document, ~400px below
   the fold. Pressing "sign in" un-hid it faithfully, off screen, and the
   button looked dead.

   So the marker lives in the markup now. An element carrying `data-stack`
   owns its own position and z-index and reads them from `--pos` and `--z`,
   which the FX rule cannot touch because it sets neither. The `:not(#araya-fx)`
   is here purely for specificity: it makes this (1,1,1), one notch above the
   rule it undoes, so it wins regardless of which sheet loads last. Adding an
   overlay now means writing `data-stack` next to it in index.html — in the
   file you are already editing — rather than remembering this one. */
body > [data-stack]:not(#araya-fx) {
  position: var(--pos, fixed);
  z-index: var(--z, 1);
}
body { background: #03040a; }


/* A rigged performer replaces the 2D pair rather than joining them — three
   figures in one shot is the bug that put two bodies on screen in X3Systems. */
/* THE OLD 2D GIRLS, AND WHY THEY ARE HIDDEN BEFORE THE NEW ONES ARRIVE.

   The flat SVG pair render instantly; the rigged VRM is 14.8 MB and takes
   seconds. So every page load showed the old characters first and swapped
   them for the new ones mid-scene — which reads as a bug even though both
   halves are working.

   `data-stage3d="pending"` goes on <body> the moment initStage3D() commits to
   loading, BEFORE the download starts, and the 2D pair are hidden from then
   on. If the stage fails at any point the attribute flips to "off" and they
   come straight back, so the fallback still exists — it just is not shown
   first by default. An empty stall for two seconds beats a cast change. */
body[data-stage3d="pending"] .waifu,
body[data-performer="on"] .waifu { display: none; }
/* `display:none` takes the waifus out of grid PLACEMENT, so .center auto-placed
   into column 1 and the entire layout jumped to the left edge. Pin the centre
   column explicitly instead of relying on source order.

   Scoped to the three-column layout. This rule lives near the bottom of the
   file, so an unscoped version beat the single-column override in the mobile
   block above no matter what that block said — and the pack, price and rip
   button sat in an implicit second column, off to the right of the screen, on
   every landscape phone. */
@media (min-width: 901px) {
  .center { grid-column: 2; }
}

/* ================================================================ gate ===
   The sign-in wall. It covers the app rather than floating over it, because
   signed out there is no app: no balance, no ripping, no inventory. A modal
   over a greyed-out interface would imply there is something behind it worth
   reaching, and there isn't. */
.gate {
  --z: 95;
  position: fixed; inset: 0; z-index: 95; display: grid; place-items: center;
  padding: clamp(14px, 3vw, 28px);
  background:
    radial-gradient(ellipse 70% 50% at 30% 20%, #c05fd626, transparent 62%),
    radial-gradient(ellipse 70% 50% at 75% 30%, #ff6f9122, transparent 62%),
    #05030c;
  overflow: auto;
}
/* (the global [hidden] rule at the top of this file covers .gate too now) */

/* TWO COLUMNS ON A DESKTOP, STACKED ON A PHONE — and on the phone the FORM
   comes first. A returning player opens this screen far more often than a new
   visitor reads it, and making them scroll past a pitch they have already
   accepted to reach the password field would be paying for acquisition with
   retention. `order` puts the card first on small screens without moving it
   in the DOM, so the reading order for a screen reader still starts with what
   the product is. */
.gate__wrap {
  width: min(1040px, 100%);
  display: grid; gap: clamp(18px, 4vw, 40px);
  grid-template-columns: 1fr;
  align-items: center;
}
/* Declared BEFORE the media queries, not after.

   The first version put `display: none` on the base rule below a
   `min-width: 860px` block that set `display: grid`. Same specificity, later
   in the file, so the base rule won and the whole pitch was invisible on every
   desktop — the exact screens it was written for. Visible by default; the
   queries only change how it is arranged. */
.gate__pitch { display: grid; gap: 14px; align-content: center; }
@media (min-width: 860px) {
  .gate__wrap { grid-template-columns: 1.15fr 400px; }
}
@media (max-width: 859px) {
  /* Shown on a phone too, but underneath. The hero line is already in the
     card's own tagline, so this is the detail someone scrolls FOR. */
  .gate__pitch { order: 2; }
  .gate__card  { order: 1; }
  .gate__brand--big { display: none; }
  .pitch__h { display: none; }
}
.gate__brand--big { justify-content: flex-start; font-size: 26px; }
.gate__brand--big .brand__x, .gate__brand--big .brand__n { font-size: 30px; }
.pitch__h {
  font-size: clamp(26px, 3.4vw, 40px); line-height: 1.16; margin: 0;
  font-weight: 700; letter-spacing: -.01em;
  background: linear-gradient(120deg, #fff 30%, #ffb9d8 70%, #c9a2ff);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.pitch__sub { margin: 0; font-size: 14px; line-height: 1.6; color: var(--ink-2); max-width: 46ch; }

/* The three numbers, straight out of the odds file. */
.pitch__stats { display: flex; flex-wrap: wrap; gap: 8px; }
.pitch__stat {
  border: 1px solid var(--line-lit); border-radius: 9px; padding: 8px 12px;
  background: #ffffff06; min-width: 104px;
}
.pitch__stat b {
  display: block; font-family: var(--mono); font-size: 17px; font-weight: 700;
  color: var(--gold); font-variant-numeric: tabular-nums;
}
.pitch__stat span {
  display: block; font-family: var(--mono); font-size: 8.5px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-4); margin-top: 3px;
}

.pitch__points { list-style: none; margin: 4px 0 0; padding: 0; display: grid; gap: 10px; }
.pitch__points li {
  font-size: 12.5px; line-height: 1.55; color: var(--ink-3);
  padding-left: 18px; position: relative; max-width: 52ch;
}
.pitch__points li::before {
  content: '✦'; position: absolute; left: 0; top: 0; color: var(--violet); font-size: 11px;
}
.pitch__points b { color: var(--ink-1); font-weight: 600; display: block; }
.pitch__legal {
  margin: 6px 0 0; font-size: 10.5px; line-height: 1.65; color: var(--ink-4);
  max-width: 54ch; border-top: 1px solid var(--line); padding-top: 10px;
}

.gate__card {
  width: min(400px, 100%);
  border: 1px solid var(--line-lit); border-radius: 16px;
  background: linear-gradient(165deg, #191036e6, #0d0920f2);
  box-shadow: 0 24px 70px #000000aa, inset 0 1px 0 #ffffff12;
  backdrop-filter: blur(12px);
  padding: 26px 24px 20px;
  display: grid; gap: 14px;
}
.gate__brand { display: flex; align-items: baseline; gap: 8px; justify-content: center; }
.gate__tag {
  text-align: center; font-size: 12px; color: var(--ink-2); line-height: 1.5;
  margin-top: -6px;
}

.gate__tabs {
  display: grid; grid-template-columns: 1fr 1fr; gap: 4px;
  background: #0a0816; border: 1px solid var(--line); border-radius: 10px; padding: 4px;
}
.gate__tab {
  font-family: var(--sans); font-size: 12.5px; font-weight: 600;
  border: 0; background: transparent; color: var(--ink-3);
  padding: 9px 8px; border-radius: 7px; cursor: pointer;
  transition: background .15s, color .15s;
}
.gate__tab[aria-selected="true"] { background: #261c4a; color: var(--ink); }

.gate__form { display: grid; gap: 11px; }
.gate__form label {
  display: grid; gap: 5px;
  font-family: var(--mono); font-size: 9px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--ink-3);
}
.gate__form label small { letter-spacing: .06em; text-transform: none; color: var(--ink-4); }
.gate__form input {
  font-family: var(--sans); font-size: 14px; color: var(--ink);
  background: #0c0820; border: 1px solid var(--line-lit); border-radius: 9px;
  padding: 11px 12px; width: 100%;
}
.gate__form input:focus { outline: none; border-color: var(--violet); box-shadow: 0 0 0 3px #c05fd625; }

.gate__bonus {
  font-size: 12px; color: var(--ink-2); text-align: center;
  border: 1px dashed #4ade8055; border-radius: 9px; padding: 9px;
  background: #4ade800d;
}
.gate__bonus b { color: var(--good); }

.gate__note { font-size: 12px; line-height: 1.5; margin: 0; min-height: 1em; text-align: center; }
.gate__note[data-kind="err"]  { color: var(--bad); }
.gate__note[data-kind="ok"]   { color: var(--good); }
.gate__note[data-kind="info"] { color: var(--ink-3); }

.gate__link {
  background: none; border: 0; color: var(--ink-3); font-family: var(--sans);
  font-size: 11.5px; cursor: pointer; text-decoration: underline; justify-self: center;
}
.gate__link:hover { color: var(--ink); }

.gate__legal {
  font-family: var(--mono); font-size: 8.5px; line-height: 1.7; letter-spacing: .04em;
  color: var(--ink-4); text-align: center; margin: 0;
}

/* On a landscape phone the card must not become a scrolling column. */
@media (max-height: 560px) {
  .gate { padding: 10px; align-items: start; }
  .gate__card { padding: 16px 18px 14px; gap: 9px; }
  .gate__tag, .gate__legal { display: none; }
  .gate__form { gap: 8px; }
  .gate__form input { padding: 8px 10px; font-size: 13px; }
  .gate__bonus { padding: 6px; font-size: 11px; }
}

/* --------------------------------------------------------- account ---- */
.acctsheet__row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 11px 0; border-bottom: 1px solid var(--line);
}
.acctsheet__row:last-child { border-bottom: 0; }
.acctsheet__k {
  font-family: var(--mono); font-size: 9px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--ink-3);
}
.acctsheet__v { font-size: 13.5px; font-weight: 600; text-align: right; }
.refcode {
  font-family: var(--mono); font-size: 17px; font-weight: 700; letter-spacing: .22em;
  color: var(--neon); text-shadow: 0 0 14px #9affb055;
}
.reflink {
  font-family: var(--mono); font-size: 10px; color: var(--ink-2);
  background: #0c0820; border: 1px solid var(--line-lit); border-radius: 8px;
  padding: 9px 11px; word-break: break-all; line-height: 1.5;
}

/* =============================================================== pool ===
   The lock-up screen. Each position is one row: how much, how long, how far
   through, and what it pays. The progress bar is the part that makes a
   30-day term feel like something happening rather than something withheld. */
.poolnote {
  font-size: 12px; line-height: 1.6; color: var(--ink-2);
  margin: 0 0 16px; padding: 11px 13px;
  border: 1px solid var(--line); border-left: 2px solid var(--violet);
  border-radius: 8px; background: #16102c66;
}
.poolnote b { color: var(--ink); }

.poolbanner {
  font-size: 12.5px; font-weight: 600; color: #06210f;
  background: linear-gradient(100deg, var(--neon), #7cf0a6);
  border-radius: 9px; padding: 11px 13px; margin-bottom: 14px;
}

.locks { display: grid; gap: 7px; }
.lock {
  display: grid; grid-template-columns: auto 1fr auto; gap: 12px; align-items: center;
  border: 1px solid var(--line); background: var(--panel-2);
  border-radius: 10px; padding: 11px 13px;
}
.lock--ready { border-color: var(--neon); box-shadow: 0 0 18px #9affb022; }
.lock__amt {
  font-family: var(--mono); font-size: 15px; font-weight: 700;
  font-variant-numeric: tabular-nums; min-width: 74px;
}
.lock__term {
  font-family: var(--mono); font-size: 9px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-3); margin-bottom: 6px;
}
.lock__bar { height: 5px; border-radius: 3px; background: #ffffff0e; overflow: hidden; }
.lock__bar i {
  display: block; height: 100%; border-radius: 3px;
  background: linear-gradient(90deg, var(--violet), var(--neon));
  transition: width 1s linear;
}
.lock__right { text-align: right; }
.lock__yield {
  font-family: var(--mono); font-size: 12px; font-weight: 700; color: var(--good);
  font-variant-numeric: tabular-nums;
}
.lock__left {
  font-family: var(--mono); font-size: 9.5px; letter-spacing: .1em;
  color: var(--ink-3); margin-top: 3px; font-variant-numeric: tabular-nums;
}
.lock--ready .lock__left { color: var(--neon); text-transform: uppercase; letter-spacing: .2em; }

.termgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(76px, 1fr)); gap: 7px; }
.term {
  border: 1px solid var(--line); background: var(--panel-2);
  border-radius: 9px; padding: 10px 8px; text-align: center;
}
.term__d {
  font-family: var(--mono); font-size: 9px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--ink-3);
}
.term__v, .term__r {
  font-family: var(--mono); font-size: 14px; font-weight: 700; color: var(--good);
  margin-top: 4px; font-variant-numeric: tabular-nums;
}

/* five tabs need to fit a 320px phone */
@media (max-width: 560px) {
  .tabbar button { font-size: 9.5px; letter-spacing: .06em; padding: 0 2px; }
}

/* The "nothing was lost" note on the reveal. Sits under the verdict, reads as
   good news rather than as a consolation, and removes itself. */
.lockflash {
  font-size: 12px; line-height: 1.6; text-align: center;
  border: 1px solid var(--line-lit); border-radius: 10px;
  padding: 9px 14px; margin-top: 2px; max-width: 420px;
  background: linear-gradient(120deg, #1b1240e0, #120d2ae0);
  color: var(--ink-2);
  animation: lockin .5s backwards cubic-bezier(.2,1.2,.3,1);
}
.lockflash b { color: var(--ink); }
@keyframes lockin { from { opacity: 0; transform: translateY(8px); } }
@media (max-height: 560px) { .lockflash { font-size: 10.5px; padding: 6px 10px; } }

/* The arrow's own feedback, fired when her hand arrives rather than on
   mousedown. Without it the press has nothing to land on: she reaches, and
   then the pack changes for no visible reason. */
.carousel__arrow.pressed {
  animation: arrowpress .32s cubic-bezier(.2,1.5,.35,1);
}
@keyframes arrowpress {
  0%   { transform: scale(1);    background: #0f2a1eb0; box-shadow: 0 0 18px #9affb033; }
  35%  { transform: scale(.88);  background: var(--neon); color: #06210f;
         box-shadow: 0 0 34px #9affb0cc, 0 0 60px #9affb055; }
  100% { transform: scale(1);    background: #0f2a1eb0; box-shadow: 0 0 18px #9affb033; }
}

/* ======================================================= settings + admin ===
   Settings is the person's own account first; the operator dashboard is
   appended underneath only for the admin, and only ever exists in that
   session's DOM. The two are visually distinct on purpose — an operator
   should be able to tell at a glance which half of the screen affects
   everybody. */
.setgrp { border-top: 1px solid var(--line); padding: 14px 0 4px; }
.setgrp:first-child { border-top: none; padding-top: 2px; }
.setgrp__h {
  font-family: var(--mono); font-size: 9.5px; letter-spacing: .24em;
  text-transform: uppercase; color: var(--ink-3); margin-bottom: 10px;
  display: flex; align-items: center; gap: 8px;
}
.setgrp__sub {
  font-family: var(--mono); font-size: 9px; letter-spacing: .2em;
  text-transform: uppercase; color: var(--ink-4); margin: 16px 0 8px;
}
.setgrp__tag {
  font-size: 8.5px; letter-spacing: .16em; padding: 2px 7px; border-radius: 999px;
  background: #ffc83d1f; color: var(--gold); border: 1px solid #ffc83d55;
}
.setgrp--admin { border-top: 1px solid var(--gold); margin-top: 18px; }

.setrow {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 7px 0; border-bottom: 1px solid #2e245066; font-size: 13px;
}
.setrow span { color: var(--ink-3); }
.setrow b { color: var(--ink); font-weight: 600; }
.setrow small { color: var(--ink-4); font-weight: 400; }
.mono { font-family: var(--mono); letter-spacing: .06em; }

.setbtns { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }

#settingsBody label {
  display: grid; gap: 5px; margin-top: 11px;
  font-family: var(--mono); font-size: 9px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-3);
}
#settingsBody label small { letter-spacing: .06em; text-transform: none; color: var(--ink-4); }
#settingsBody input {
  font-family: var(--mono); font-size: 12.5px; letter-spacing: .02em;
  background: #0f0b20; border: 1px solid var(--line-lit); color: var(--ink);
  padding: 9px 11px; border-radius: 8px; width: 100%;
}
#settingsBody input:focus { outline: none; border-color: var(--violet); box-shadow: 0 0 0 3px #c05fd625; }
#settingsBody input[readonly] { color: var(--ink-2); }

/* Operator totals. Tiles rather than a table because these are ten unrelated
   scalars, and a table implies rows that can be compared to each other. */
.admintiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(118px, 1fr)); gap: 8px; }
.tile { border: 1px solid var(--line); border-radius: 10px; padding: 9px 11px; background: #14102466; }
.tile__k { font-family: var(--mono); font-size: 8.5px; letter-spacing: .16em;
           text-transform: uppercase; color: var(--ink-4); }
.tile__v { font-size: 15px; font-weight: 600; color: var(--ink); margin-top: 3px;
           font-variant-numeric: tabular-nums; }

.adminusers { overflow: auto; max-height: 40dvh; margin-top: 4px; }
.atable { width: 100%; border-collapse: collapse; font-size: 12px; }
.atable th {
  position: sticky; top: 0; background: #0c0819f7; text-align: left;
  font-family: var(--mono); font-size: 8.5px; letter-spacing: .16em;
  text-transform: uppercase; color: var(--ink-4); padding: 7px 8px;
  border-bottom: 1px solid var(--line-lit);
}
.atable td { padding: 7px 8px; border-bottom: 1px solid #2e245055; color: var(--ink-2);
             font-variant-numeric: tabular-nums; }
.atable tr:hover td { background: #1a143055; }

/* --------------------------------------------------- funds: both ways ---
   A row of label-and-figure. Used for the numbers that are FACTS rather than
   inputs — price, balance, withdrawable, the cap — so they read as a statement
   of the situation and the two actual inputs stand out from them. */
.aw__row {
  display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
  padding: 6px 0; border-bottom: 1px solid #2e245055; font-size: 12.5px;
}
.aw__row:last-of-type { border-bottom: 0; }
.aw__row span { color: var(--ink-3); }
.aw__row b { font-family: var(--mono); font-variant-numeric: tabular-nums; color: var(--ink-1); }
/* The rows that are a constraint rather than a number: bonus still to play
   through, an unreadable pool. Amber, not red — nothing is wrong, something is
   just not available yet, and red for "wait" trains people to ignore red. */
.aw__row--warn b { color: var(--gold); }

/* ---------------------------------------------------------- promotion ---
   The flag on the buy screen. Gold because it is good news, bordered rather
   than filled so it reads as a note on the terms and not as a button — it
   sits directly above the disclosure text and must not compete with the RIP
   button for a thumb. */
.promo__flag {
  display: inline-block; font-family: var(--mono); font-size: 9.5px;
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--gold); border: 1px solid #ffc83d55; border-radius: 4px;
  padding: 2px 7px; margin-bottom: 3px; background: #ffc83d10;
}
.promo { display: grid; gap: 8px; }

/* ------------------------------------------------------- payout queue ---
   The operator's view of money leaving. Styled to be READ rather than
   skimmed: the reason a payout is waiting is the most important column on
   the screen, so it gets room and the amounts get tabular figures. */
.payouts { display: grid; gap: 10px; }
.pay__head {
  display: grid; gap: 6px; padding: 11px 12px; border-radius: 9px;
  border: 1px solid var(--line); background: var(--panel-2);
}
.pay__state { display: flex; align-items: center; gap: 8px; font-size: 12.5px; }
/* Red when off, green when on, and never only colour — the word ON or OFF is
   next to it, because a dot alone is unreadable to eight percent of men. */
.pay__dot {
  width: 9px; height: 9px; border-radius: 50%; flex: none;
  background: var(--bad); box-shadow: 0 0 0 3px #ff5d7322;
}
.pay__dot.on { background: var(--good); box-shadow: 0 0 0 3px #4ade8022; }
.pay__meta { font-family: var(--mono); font-size: 10px; color: var(--ink-3); line-height: 1.7; }
.pay__meta b { color: var(--ink-2); font-weight: 700; }
.pay__warn { color: var(--bad); }
.pay__ok   { color: var(--good); font-family: var(--mono); font-size: 10px; }
.pay__flight { color: var(--gold); font-family: var(--mono); font-size: 10px; }
.pay__capped {
  font-family: var(--mono); font-size: 8px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--gold); border: 1px solid #ffd97844;
  border-radius: 3px; padding: 1px 4px; white-space: nowrap;
}
.pay__why { margin: 0; padding-left: 14px; font-size: 10.5px; color: var(--ink-3); line-height: 1.6; }
.pay__why li { margin: 1px 0; }
.pay__acts { display: flex; gap: 5px; flex-wrap: wrap; }
/* The one button in the app that broadcasts real transfers. It is marked,
   because "Send eligible" sitting in a row of ghost buttons is how somebody
   clicks it meaning to click Refresh. */
.btn--gold {
  border-color: var(--gold); color: #2a1e00; font-weight: 700;
  background: linear-gradient(180deg, var(--gold), #e0a800);
}
.btn--gold:hover { filter: brightness(1.08); }
.btn--gold:disabled { filter: grayscale(.6) brightness(.8); }
.form__note--bad { color: var(--bad); }
/* The queue is the one table here that genuinely needs the width; let it
   scroll sideways on a phone rather than crushing the reasons column. */
.payouts .atable { min-width: 620px; }
.payouts { overflow-x: auto; }

/* --------------------------------------------- insecure-connection notice --
   Loud on purpose. This is the one message in the app that is worth
   interrupting someone over: the page asking for their password is not on a
   connection that can promise the page is ours. */
.gate__warn {
  display: grid; gap: 9px; text-align: left;
  border: 1px solid var(--bad); border-radius: 12px;
  background: linear-gradient(160deg, #ff5a6e1f, #ff5a6e0a);
  padding: 13px 14px; font-size: 12.5px; line-height: 1.5; color: var(--ink-2);
}
.gate__warn b { color: var(--bad); display: block; margin-bottom: 2px; }
.gate__warn .btn { justify-self: start; text-decoration: none; }

/* ========================================================== add funds ===
   The ARAYA widgets render themselves into #araya-* with their own injected
   stylesheet, built for a floating overlay on a light-ish page. These rules
   only make them sit correctly inside an X3 Suki sheet — they do not restyle
   the widgets themselves, so an update to araya-economy.js does not land in a
   fight with this file. */
.fundsgrid { display: grid; gap: 4px; }

.ppbuy { display: grid; gap: 10px; }
.ppbuy label {
  display: grid; gap: 5px;
  font-family: var(--mono); font-size: 9px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-3);
}
.ppbuy input {
  font-family: var(--mono); font-size: 15px;
  background: #0f0b20; border: 1px solid var(--line-lit); color: var(--ink);
  padding: 10px 12px; border-radius: 8px; width: 140px;
}
.ppbuy input:focus { outline: none; border-color: var(--violet); box-shadow: 0 0 0 3px #c05fd625; }
.ppquick { display: flex; gap: 6px; flex-wrap: wrap; }
#ppButtons { max-width: 380px; }
#ppButtons .btn { display: inline-block; text-decoration: none; }

/* The widgets come with white cards; tone them into the panel so the sheet
   reads as one surface rather than three pasted screenshots. */
#sheetFunds .ae-card {
  background: #14102466 !important; border: 1px solid var(--line) !important;
  border-radius: 12px !important; color: var(--ink-2) !important;
}
#sheetFunds .ae-h { color: var(--ink) !important; }
#sheetFunds .ae-muted { color: var(--ink-3) !important; }
#sheetFunds .ae-in {
  background: #0f0b20 !important; border: 1px solid var(--line-lit) !important;
  color: var(--ink) !important;
}
#sheetFunds .ae-btn {
  background: #201a3a !important; border: 1px solid var(--line-lit) !important;
  color: var(--ink) !important;
}
#sheetFunds .ae-btn.go { background: var(--violet) !important; border-color: var(--violet) !important; color: #fff !important; }
#sheetFunds .ae-mono { font-family: var(--mono); word-break: break-all; }

/* The rails' own floating launcher is switched off via ARAYA_NO_LAUNCHER, but
   a cached copy of an older araya-economy.js could still inject one. If it
   does, it must at least be positioned rather than flattened into the flow by
   araya-fx's rule — which is what put it behind the tab bar at 0x0. */
body > #ae-overlay { position: fixed !important; inset: 0 !important; z-index: 92 !important; }
body > #ae-fab     { position: fixed !important; z-index: 57 !important; }

/* ======================================================== motion test ===
   The panel is useless if it covers the thing it drives, and the performers
   stand at the sides of the stage — which the sheet's normal 76dvh hides
   almost entirely. `peek` drops it to a strip so the motion can actually be
   watched while the buttons stay in reach. */
.sheet--peek { max-height: 30dvh; }

.motiontest { display: grid; gap: 10px; }
.mt__row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.mt__speed, .mt__hold {
  display: flex; align-items: center; gap: 8px;
  font-family: var(--mono); font-size: 9px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-3); margin: 0 !important;
}
.mt__speed input[type="range"] { width: 130px; accent-color: var(--violet); }
.mt__speed b { color: var(--gold); font-family: var(--mono); min-width: 44px; }
.mt__hold select {
  font-family: var(--mono); font-size: 11px;
  background: #0f0b20; border: 1px solid var(--line-lit); color: var(--ink);
  padding: 6px 8px; border-radius: 7px;
}

.mt__table { width: 100%; border-collapse: collapse; }
.mt__table td { padding: 3px 6px 3px 0; vertical-align: middle; }
.mt__table td:first-child { width: 1%; white-space: nowrap; }
/* What normally fires each motion. The reason this column exists is that a
   button which only plays an animation tells you how it looks; one that also
   says when it happens tells you whether it is the right animation for the
   moment it belongs to. */
.mt__when { font-size: 11px; color: var(--ink-3); line-height: 1.4; }

/* ================================================= card zoom + actions ===
   Tapping a pulled card opens it big. The zoom sits ABOVE the reveal (which
   is z 60) because it is opened from inside it, and closing it returns to the
   reveal rather than to the stage. */
.zoom {
  --z: 94;
  position: fixed; inset: 0; z-index: 94; display: none;
  place-items: center; padding: 20px;
  background: #05030cee; backdrop-filter: blur(8px);
}
.zoom.on { display: grid; }
.zoom__card {
  display: grid; gap: 12px; justify-items: center;
  max-width: min(420px, 92vw); width: 100%;
}
.zoom__art {
  width: 100%; aspect-ratio: 5 / 7; max-height: 62dvh;
  border-radius: 14px; overflow: hidden; position: relative;
  border: 1px solid var(--line-lit); background: #100a24;
  box-shadow: 0 30px 80px #000000cc;
}
.zoom__art .art { position: absolute; inset: 0; }
.zoom__meta { text-align: center; display: grid; gap: 3px; }
.zoom__name { font-size: 17px; font-weight: 600; }
.zoom__set  { font-size: 11.5px; color: var(--ink-3); }
.zoom__val  { font-family: var(--mono); font-size: 19px; font-weight: 600; color: var(--gold); }
.zoom__acts { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }

/* The per-card decision on the reveal strip. A mini is a button now, so it
   needs to look pressable and to show which way it has been decided. */
.mini { cursor: pointer; }
.mini__pick {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; border-top: 1px solid #00000066;
}
.mini__pick button {
  flex: 1; border: 0; cursor: pointer; padding: 3px 0;
  font-family: var(--mono); font-size: 8px; letter-spacing: .1em;
  text-transform: uppercase; background: #0c0818cc; color: var(--ink-3);
}
.mini__pick button:hover { color: var(--ink); background: #1a1430ee; }
/* SELECTED IS GOLD. WHICH WAY IT WENT IS THE WORD.

   The first version coloured the border green for keep and gold for sell,
   which made the two decisions look like two different things and made
   "selected at all" look like nothing in particular — on a strip of five
   cards you had to remember what green meant.

   So the gold ring answers one question only: has this card been decided?
   Every picked card gets the same ring and the same lift, so the undecided
   ones are obvious at a glance. WHICH decision it was is spelled out — a
   badge across the corner that says KEEP or SELL — because a word cannot be
   misread the way a colour can, and the person reading it may well be
   colour-blind. */
.mini[data-pick] {
  border-color: var(--gold) !important;
  box-shadow: 0 0 0 2px var(--gold), 0 6px 18px #00000099;
  transform: translateY(-2px);
}
.mini[data-pick]::after {
  content: attr(data-pick);
  position: absolute; top: 0; left: 0; right: 0;
  padding: 2px 0; text-align: center;
  font-family: var(--mono); font-size: 8px; letter-spacing: .18em;
  text-transform: uppercase; font-weight: 700;
  background: var(--gold); color: #2a1e00;
}
/* Sold cards also fade, because they are on their way out of the collection
   and the strip should read as "these are leaving, those are staying". */
.mini[data-pick="sell"] .art { opacity: .55; }
.mini[data-pick="keep"] [data-pick-keep],
.mini[data-pick="sell"] [data-pick-sell] { background: var(--gold); color: #2a1e00; }

/* ------------------------------------------------- inventory sort bar --- */
.collbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; margin-bottom: 12px;
  padding-bottom: 10px; border-bottom: 1px solid var(--line);
}
.collsort {
  display: flex; align-items: center; gap: 8px;
  font-family: var(--mono); font-size: 9px; letter-spacing: .16em;
  text-transform: uppercase; color: var(--ink-3);
}
.collsort select {
  font-family: var(--sans); font-size: 12px; letter-spacing: 0; text-transform: none;
  background: #0f0b20; border: 1px solid var(--line-lit); color: var(--ink);
  padding: 6px 9px; border-radius: 7px;
}
.collsort select:focus { outline: none; border-color: var(--violet); }
.collcount {
  font-family: var(--mono); font-size: 10px; letter-spacing: .1em; color: var(--ink-4);
}
