/* ==========================================================================
   PLAYER CABINET

   The inventory/case screen behind /player. It is the one page on the site
   whose audience is the kids playing in the arena rather than the owners
   buying it, so it borrows the loot-grid language they already know from CS2
   and Standoff — rarity colour on every tile, a spinning case reel, a reveal —
   and draws all of it out of the site's own parts: the reborn palette, the 2px
   ink outline, the hard offset shadow and Rubik as the display face.

   Load order is reborn-core.css first (tokens, page base) and this file after;
   nothing here invents a colour that core already names. Light and dark both
   come from home-theme.js, exactly like every other page.
   ========================================================================== */

/* ===== The arcade pass ====================================================
   The cabinet used to inherit the site's editorial voice wholesale — white
   page, hairline type, muted greys. That voice is right for the pages that
   sell an arena to its owner and wrong for the screen a fourteen-year-old
   opens to see what dropped. So the cabinet keeps core's token *names* and
   overrides their values here: a hotter page, a harder ink, a type scale a
   full step up, and one ember accent the whole screen shouts with.

   Nothing below this block had to change to follow — every component already
   read these tokens. The rules further down only add the parts a louder
   palette cannot express on its own: glow, gradient edges and slanted
   display type.
   ========================================================================== */

.pl-body {
  /* Page-local scale. Everything else is a --reset-* token from core. */
  --pl-rail: 16.5rem;
  /* Поля главной колонки живут в токене, потому что липкая оснастка наверху
     обязана их знать: полоса, которая кончается там же, где текст, оставляет
     по бокам две прозрачные щели, и в них едет страница. Полосы консоли
     выезжают в поля отрицательным margin и возвращают его себе padding'ом —
     фон идёт до кромки, содержимое стоит по той же линии, что и сетка под ним. */
  --pl-gutter: clamp(1rem, 2.6vw, 2.2rem);
  /* Грунт полос управления в консоли: паз, проточенный в плите. На кремовой
     странице это буквально углубление — семь процентов тени поверх фона; в
     тёмной теме тот же токен переопределён на подъём, потому что чернить и без
     того почти чёрный фон нечем. Общее у них одно: полоса обязана быть видна
     как полоса, а не как ещё одна карточка. */
  --pl-groove: color-mix(in srgb, var(--reset-shadow) 7%, var(--reset-bg));
  --pl-radius: 20px;
  --pl-radius-sm: 14px;
  --pl-border: 2px solid var(--reset-cream);
  --pl-lift-sm: 3px 3px 0 var(--reset-shadow);
  --pl-lift: 5px 5px 0 var(--reset-shadow);
  --pl-lift-hi: 8px 8px 0 var(--reset-shadow);

  /* ===== Type =====
     One step up from core across the board. The cabinet is read at arm's
     length on a phone in a noisy arena, not at a desk, and every number on it
     — a level, a balance, a 3/5 — is the point of the screen rather than
     supporting detail. The top of the ramp moves furthest: a screen title
     that was 2rem is now nearly 3. */
  --fs-2xs: clamp(.66rem, .6rem + .24vw, .8rem);
  --fs-xs: clamp(.79rem, .73rem + .28vw, .96rem);
  --fs-sm: clamp(.92rem, .87rem + .28vw, 1.08rem);
  --fs-base: clamp(1.02rem, .97rem + .3vw, 1.2rem);
  --fs-md: clamp(1.14rem, 1.04rem + .42vw, 1.44rem);
  --fs-lg: clamp(1.34rem, 1.16rem + .74vw, 1.85rem);
  --fs-xl: clamp(1.85rem, 1.4rem + 1.6vw, 3rem);
  --fs-2xl: clamp(2.3rem, 1.5rem + 3.2vw, 4.2rem);

  /* ===== Light palette =====
     Warmer and more saturated than core's near-white. The cream ground gives
     the black outline something to bite into, which is the whole reason the
     light reference reads as a game board and not as a form. */
  --reset-bg: #f4efe3;
  --reset-panel: #fffdf7;
  --reset-panel-raised: #ffffff;
  --reset-cream: #14100a;
  --reset-muted: #5a5142;
  --reset-line: rgba(20, 16, 10, .16);
  --reset-line-strong: rgba(20, 16, 10, .42);
  --reset-sun: #ffb01f;
  --reset-coral: #ff5c22;
  --reset-shadow: #14100a;
  --reset-dot: rgba(20, 16, 10, .07);

  /* ===== The ember =====
     One accent, used for exactly one job: *this is the thing to touch*. Tabs,
     claim buttons, the headset key, the level badge. Anything that is merely
     information stays off it. */
  --pl-ember: #ff6a1f;
  --pl-ember-hi: #ff9d3d;
  --pl-flare: #ffb32b;
  --pl-ember-ink: #2a1201;
  --pl-ember-ramp: linear-gradient(135deg, var(--pl-flare) 0%, var(--pl-ember) 52%, #e8410a 100%);
  /* The plain button's ground. Core's --reset-control-surface is white in both
     themes on purpose — it is the ground for controls that sit *on* a
     saturated tile. Most of this cabinet is no longer a saturated tile, and a
     row of solid-white pills down a black rail reads as a stack of paper, so
     the neutral button gets its own pair and the tile controls keep theirs. */
  --pl-control: var(--reset-control-surface);
  --pl-control-ink: var(--reset-tile-ink);
  /* Glow is a dark-theme instrument — on cream it turns into a smudge — so the
     light side spends the token on nothing and the dark side redefines it.
     Nothing has to be a *transparent shadow* rather than `none`: these tokens
     are read inside comma lists, and one `none` in a list of shadows throws the
     whole declaration away, hard offset included. */
  --pl-glow: 0 0 0 0 rgba(0, 0, 0, 0);
  --pl-glow-soft: 0 0 0 0 rgba(0, 0, 0, 0);
  --pl-edge: var(--pl-border);
  /* The faint diagonal hatch every hero panel wears. Slanted, because every
     surface in this file that wants to read as *equipment* is slanted. */
  --pl-hatch: repeating-linear-gradient(115deg,
    rgba(20, 16, 10, .05) 0 2px, transparent 2px 9px);

  /* Every avatar in the roster is character art painted on a dark hex-panel
     backdrop, and one of the thirty is masked to a circle on pure black. A
     dark plate under all of them makes the set read as one deck of cards in
     both themes; a cream one would ring the circle-masked portrait with a
     halo and leave the other twenty-nine floating on a seam. */
  --pl-plate: #0d0f14;
  --pl-plate-glow: #1b2029;
  /* The Battle Token ramp. Fixed in both themes on purpose — see the currency
     section further down; a coin that changes colour with the page stops being
     an object the player owns. */
  --pl-gold-hi: #ffeeb0;
  --pl-gold: #ffc32f;
  --pl-gold-deep: #de8300;
  --pl-gold-ink: #3b2200;
  /* Read by every rarity-tinted surface. Items set it inline from the colour
     the catalogue stores; anything without a rarity (a case, an empty slot)
     falls back to the page line, so the tint quietly disappears instead of
     needing a second rule. */
  --rarity: var(--reset-line-strong);
  min-height: 100vh;
  /* The dot grid is the homepage's texture. Here it also keeps a long
     inventory from reading as cards floating on a blank sheet. Over it sits a
     single warm bloom behind the masthead — the cabinet's one light source,
     which is what stops a flat page from reading as a document. */
  background-color: var(--reset-bg);
  background-image:
    radial-gradient(90rem 34rem at 62% -12rem, rgba(255, 106, 31, .12), transparent 70%),
    radial-gradient(var(--reset-dot) 1.5px, transparent 1.5px);
  background-size: auto, 22px 22px;
  background-attachment: fixed, scroll;
}

/* ===== Dark =====
   The reference the cabinet is drawn against: near-black, one ember light,
   and depth carried by glow rather than by the hard offset shadow the cream
   page uses. Offsetting a black block against a black page prints nothing, so
   every --pl-lift here becomes a bloom instead of a stamp. */
:root[data-home-theme="dark"] .pl-body {
  --reset-bg: #07080c;
  --reset-panel: #101319;
  --reset-panel-raised: #171b24;
  --reset-cream: #ffffff;
  --reset-surface: #1c212b;
  --reset-muted: #949cab;
  --reset-line: rgba(255, 255, 255, .09);
  --reset-line-strong: rgba(255, 255, 255, .22);
  --reset-shadow: #000000;
  --reset-dot: rgba(255, 255, 255, .05);
  --reset-ink-sun: var(--pl-flare);
  --reset-ink-coral: var(--pl-ember-hi);

  /* A white 2px outline on every card is what the site's ink border becomes in
     the dark, and it turns the screen into graph paper. The dark cabinet draws
     its edges as a faint line plus the glow below. */
  --pl-border: 2px solid rgba(255, 255, 255, .08);
  /* Паз консоли в тёмной не углубляют, а приподнимают: страница уже почти
     чёрная, и семь процентов чёрного поверх неё не печатают ничего. Полоса
     проявляется тем же способом, каким на этом фоне проявляется любая панель. */
  --pl-groove: color-mix(in srgb, var(--reset-cream) 4%, var(--reset-bg));
  --pl-edge: 2px solid color-mix(in srgb, var(--pl-ember) 42%, transparent);
  --pl-lift-sm: 0 6px 18px -10px #000;
  --pl-lift: 0 22px 44px -26px #000, inset 0 1px 0 rgba(255, 255, 255, .05);
  --pl-lift-hi: 0 30px 60px -28px #000, inset 0 1px 0 rgba(255, 255, 255, .07);
  --pl-glow: 0 0 0 1px color-mix(in srgb, var(--pl-ember) 45%, transparent),
    0 0 32px -6px color-mix(in srgb, var(--pl-ember) 55%, transparent);
  --pl-glow-soft: 0 0 26px -10px color-mix(in srgb, var(--pl-ember) 65%, transparent);
  --pl-hatch: repeating-linear-gradient(115deg,
    rgba(255, 255, 255, .045) 0 2px, transparent 2px 9px);
  --pl-control: #212633;
  --pl-control-ink: var(--reset-cream);
}

/* Scoped, not global: core ships no reset, and every width + padding pair in
   this file assumes the padding sits inside the box. */
.pl-body *,
.pl-body *::before,
.pl-body *::after { box-sizing: border-box; }

.pl-body :is(h1, h2, h3),
.pl-item__name,
.pl-display {
  font-family: var(--heading-font-family);
  font-weight: 900;
  letter-spacing: -.015em;
}

/* Headings are set in caps because every scoreboard the audience has ever read
   is. Rubik ships no italic on the weights we load, so the slant is a skew —
   which is also the honest way to do it here: the letterforms stay Rubik's and
   only the whole word leans, the way a stencil on a crate does. Capped at h1
   and h2; an h3 is a label inside a card and a leaning label is just noise. */
.pl-body :is(h1, h2) {
  text-transform: uppercase;
  letter-spacing: -.005em;
}

.pl-display,
.pl-body .pl-slant {
  transform: skewX(-7deg);
  transform-origin: left bottom;
}

/* The HUD voice: mono, wide-tracked, tiny. Eyebrows, counters and anything
   that should read as a readout rather than as prose. */
.pl-hud {
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
}

.pl-app {
  display: grid;
  grid-template-columns: var(--pl-rail) minmax(0, 1fr);
  min-height: 100vh;
  transition: opacity .4s ease;
}

/* Laid out but invisible rather than display:none — the cabinet is already
   measured and settled when the curtain lifts, so nothing reflows into place
   in front of the player. */
.pl-body.is-booting .pl-app {
  opacity: 0;
  pointer-events: none;
}

/* ===== Rail ==============================================================
   A sticky column of chunky tabs. The active tab is a filled tile carrying
   the page ink — the same move the homepage makes with its colour tiles,
   which is most of why this screen still reads as the same product. */

.pl-rail {
  position: sticky;
  top: 0;
  z-index: 6;
  display: flex;
  flex-direction: column;
  gap: .75rem;
  height: 100vh;
  height: 100dvh;
  padding: 1rem .875rem;
  border-right: var(--pl-border);
  /* A shade darker than the board it sits beside, with the same single ember
     light bleeding in from the top. The rail is the cabinet's frame; the board
     is what is playing inside it. */
  background:
    radial-gradient(22rem 16rem at 0 -4rem, rgba(255, 106, 31, .16), transparent 72%),
    var(--reset-panel);
}

.pl-brand {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  gap: .7rem;
  color: var(--reset-cream);
  text-decoration: none;
}

.pl-brand__mark {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 48px;
  height: 48px;
  overflow: hidden;
  border: var(--pl-border);
  border-radius: 50%;
  /* minilogo.png is a white glyph on transparent, so on the light page it
     needs its dark disc back — the same fix core applies to the site header. */
  background: var(--reset-logo-disc);
  box-shadow: var(--pl-lift-sm), var(--pl-glow-soft);
}

.pl-brand__mark img {
  width: 28px;
  height: 28px;
}

.pl-brand__name {
  display: grid;
  gap: .05rem;
  line-height: 1.05;
  transform: skewX(-7deg);
}

/* The wordmark is the loudest thing in the rail and the only place the two
   halves of the name are allowed to disagree: FRONTLINE in ember, PLAYER in
   ink under it. It is the arena's sign over the door. */
.pl-brand__name b {
  font-family: var(--heading-font-family);
  font-size: 1.18rem;
  font-weight: 900;
  letter-spacing: .01em;
  color: var(--pl-ember);
}

.pl-brand__name span {
  color: var(--reset-cream);
  font-size: .74rem;
  letter-spacing: .28em;
  opacity: .75;
}

.pl-nav {
  display: grid;
  flex: 1;
  min-height: 0;
  align-content: start;
  gap: .15rem;
  padding: .2rem .25rem .4rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}

.pl-nav button {
  position: relative;
  display: grid;
  grid-template-columns: 1.5rem minmax(0, 1fr) auto;
  align-items: center;
  gap: .6rem;
  min-height: 44px;
  padding: .5rem .65rem;
  overflow: hidden;
  border: 2px solid transparent;
  border-radius: var(--pl-radius-sm);
  background: transparent;
  color: var(--reset-muted);
  font: inherit;
  font-family: var(--heading-font-family);
  font-size: .94rem;
  font-weight: 800;
  line-height: 1.25;
  text-align: left;
  text-transform: uppercase;
  cursor: pointer;
  transition: transform .15s ease, box-shadow .15s ease, background-color .15s ease, color .15s ease;
}

.pl-nav button i {
  justify-self: center;
  font-size: 1.12rem;
}

.pl-nav button:hover {
  background: var(--reset-panel-raised);
  color: var(--reset-cream);
  box-shadow: var(--pl-lift-sm);
  transform: translateX(2px);
}

/* The active tab is the ember, and it is the only fill in the rail — the eye
   finds the current room before it has read a single label. The bar down its
   left edge is the same mark the mission cards wear, which is what ties the
   rail to the board it steers. */
.pl-nav button.is-active {
  background: var(--pl-ember-ramp);
  color: var(--pl-ember-ink);
  box-shadow: var(--pl-lift-sm), var(--pl-glow-soft);
  transform: none;
}

.pl-nav button.is-active::before {
  content: "";
  position: absolute;
  top: .38rem;
  bottom: .38rem;
  left: 0;
  width: 4px;
  border-radius: 0 4px 4px 0;
  background: var(--pl-ember-ink);
  opacity: .5;
}

.pl-nav button.is-active::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--pl-hatch);
  opacity: .55;
  pointer-events: none;
}

.pl-nav button.is-active i,
.pl-nav button.is-active span { position: relative; }

.pl-nav__badge {
  min-width: 1.55rem;
  padding: .1rem .4rem;
  border: 1.5px solid var(--reset-cream);
  border-radius: 999px;
  background: var(--reset-coral);
  color: var(--reset-tile-ink);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-weight: 700;
  text-align: center;
}

.pl-nav__badge[hidden] { display: none; }

.pl-rail__foot {
  display: grid;
  flex-shrink: 0;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: .45rem;
  margin-top: auto;
  padding-top: .75rem;
  border-top: 1px solid var(--reset-line);
}

/* ===== Buttons ===========================================================
   Three weights — filled, quiet and square-icon — all sharing the outline and
   the offset shadow so they stay one family. Pressing a button drops it onto
   its own shadow, which is the whole tactile joke of the theme. */

.pl-btn,
.pl-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .45rem;
  border: var(--pl-border);
  border-radius: 999px;
  background: var(--pl-control);
  color: var(--pl-control-ink);
  font: inherit;
  font-weight: 800;
  text-decoration: none;
  box-shadow: var(--pl-lift-sm);
  cursor: pointer;
  transition: transform .15s ease, box-shadow .15s ease, background-color .15s ease;
}

.pl-btn {
  padding: .68rem 1.35rem;
  font-family: var(--heading-font-family);
  font-size: var(--fs-sm);
  font-weight: 900;
  letter-spacing: .02em;
  text-transform: uppercase;
}

.pl-btn:hover:not(:disabled),
.pl-icon-btn:hover:not(:disabled) {
  box-shadow: var(--pl-lift);
  transform: translate(-1px, -1px);
}

.pl-btn:active:not(:disabled),
.pl-icon-btn:active:not(:disabled) {
  box-shadow: 1px 1px 0 var(--reset-shadow);
  transform: translate(2px, 2px);
}

.pl-btn[hidden],
.pl-icon-btn[hidden] { display: none; }

/* The primary is the ember ramp rather than a flat coral: a button a player is
   meant to hit — open the case, claim the reward, go to the arena — should look
   lit from inside, and the gradient plus the bloom is what does that without a
   single image. */
.pl-btn--primary {
  border-color: transparent;
  background: var(--pl-ember-ramp);
  color: var(--pl-ember-ink);
  box-shadow: var(--pl-lift-sm), var(--pl-glow-soft);
}

.pl-btn--primary:hover:not(:disabled) {
  box-shadow: var(--pl-lift), var(--pl-glow);
}

.pl-btn--sun {
  border-color: transparent;
  background: linear-gradient(135deg, #ffdd6e 0%, var(--pl-flare) 55%, #f08c00 100%);
  color: var(--pl-ember-ink);
}

.pl-btn--danger { background: var(--reset-danger); }
.pl-btn--wide { width: 100%; }

.pl-btn:disabled,
.pl-icon-btn:disabled {
  opacity: .5;
  box-shadow: none;
  transform: none;
  cursor: not-allowed;
}

.pl-icon-btn {
  width: 2.85rem;
  height: 2.85rem;
  border-radius: var(--pl-radius-sm);
  background: var(--reset-panel-raised);
  color: var(--reset-cream);
  font-size: var(--fs-base);
  font-weight: 900;
}

.pl-icon-btn:hover:not(:disabled) { color: var(--pl-ember); }

/* Rail controls are wide rows rather than squares. */
.pl-rail__foot .pl-btn {
  min-width: 0;
  min-height: 36px;
  padding: .4rem .3rem;
  border: 1px solid transparent;
  border-radius: 10px;
  background: transparent;
  color: var(--reset-muted);
  box-shadow: none;
  font-size: .75rem;
  line-height: 1.25;
}

.pl-rail__foot .pl-btn--install {
  grid-column: 1 / -1;
}

.pl-rail__foot .pl-btn:hover:not(:disabled) {
  background: var(--reset-panel-raised);
  color: var(--reset-cream);
}

/* Headset login is an action; reserve the orange fill for the active section. */
.pl-btn[data-link-headset],
.pl-rail__foot .pl-btn[data-link-headset] {
  border: 2px solid var(--reset-tile-ink);
  background: var(--reset-sky);
  color: var(--reset-tile-ink);
  box-shadow: var(--pl-lift-sm);
}

.pl-rail__foot .pl-btn[data-link-headset] {
  grid-column: 1 / -1;
  min-height: 52px;
  gap: .65rem;
  padding: .65rem .5rem;
  border-radius: var(--pl-radius-sm);
  font-size: .94rem;
}

.pl-btn[data-link-headset]:hover:not(:disabled),
.pl-rail__foot .pl-btn[data-link-headset]:hover:not(:disabled) {
  background: color-mix(in srgb, var(--reset-sky) 82%, white);
  color: var(--reset-tile-ink);
  box-shadow: var(--pl-lift);
}

[data-link-headset]:focus-visible {
  outline: 3px solid var(--pl-ember);
  outline-offset: 3px;
}

.pl-rail__foot .pl-btn i { width: 1.1rem; }

/* Одиннадцать строк и четыре заголовка групп в ноутбучные 900px по высоте уже
   не влезают, а рельс с обрезанным пополам пунктом читается как поломка, а не
   как прокрутка. Поэтому порог сжатия поднят до 960px: сначала список ужимается
   и помещается целиком, и только потом начинает скроллиться. */
@media (min-width: 901px) and (max-height: 960px) {
  .pl-rail { gap: .5rem; padding-block: .75rem; }
  .pl-nav { gap: .1rem; }
  .pl-nav button { min-height: 36px; padding-block: .35rem; font-size: .88rem; }
  .pl-nav button i { font-size: 1rem; }
  .pl-nav__group { margin: .5rem 0 .1rem; }
}

/* ===== Main column ======================================================= */

.pl-main {
  min-width: 0;
  padding: 0 var(--pl-gutter) 4rem;
}

/* ===== Консоль ===========================================================
   Мачта, лента групп и панель полки — не три плавающие карточки, а три полосы
   одного прибора, и склеены они здесь.

   До этого каждая была отдельным островом внутри колонки: она кончалась там
   же, где текст, поэтому слева и справа от неё оставались поля главной
   колонки — две прозрачные щели во всю высоту экрана, сквозь которые ехала
   страница. Между лентой и панелью висела ещё и горизонтальная прорезь: панель
   липла на двенадцать точек ниже, чем кончалась лента, и в этот зазор было
   видно уезжающую карусель. Скруглённые углы добавляли по вырезу в каждом
   стыке. Верх экрана читался дырявым — набором плашек, а не кромкой кабинета.

   Теперь полосы уходят под обе кромки колонки и стыкуются вплотную. Мачта
   лежит на грунте страницы и закрыта ембер-росчерком; ленты управления —
   в пазу на два тона глубже грунта, с бликом по верхней кромке и одной
   линией по нижней. Сквозь консоль больше ничего не просвечивает, и порядок
   полос всегда один: где ты — что рядом — чем сузить. */

.pl-top,
.pl-subnav,
.pl-board {
  /* Полоса консоли: фон до кромки колонки, содержимое — по её сетке. */
  margin-inline: calc(var(--pl-gutter) * -1);
  padding-inline: var(--pl-gutter);
}

/* Три ячейки: имя экрана слева, кошелёк ровно по центру мачты, кнопки справа.
   Боковые колонки — равные `1fr`, поэтому плашка с жетонами стоит на одном и
   том же месте на каждом экране кабинета, а не пляшет за длиной заголовка. */
.pl-top {
  position: sticky;
  top: 0;
  z-index: 5;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: 1rem;
  padding-block: 1.15rem 1.05rem;
  /* Непрозрачная, без матового стекла. Восемьдесят восемь процентов были
     красивы ровно до первой карточки, проехавшей под мачтой: сквозь них видно,
     что именно едет, и кромка экрана переставала быть кромкой. Заодно это
     снимает с телефона пересчёт блюра на каждом кадре прокрутки. */
  background: var(--reset-bg);
}

/* Кромка мачты — ембер, гаснущий в ничто, а не сплошная линия во всю ширину:
   читается как свет, стекающий с заголовка, а не как верх таблицы. Теперь она
   идёт до самых краёв колонки, потому что и сама мачта туда дошла. */
.pl-top::after {
  content: "";
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  height: 2px;
  background: linear-gradient(90deg,
    var(--pl-ember) 0%,
    color-mix(in srgb, var(--pl-ember) 40%, transparent) 26%,
    var(--reset-line) 60%,
    transparent 100%);
}

.pl-top__eyebrow {
  margin: 0;
  color: var(--reset-ink-coral);
}

.pl-top h1 {
  display: flex;
  align-items: center;
  gap: .7rem;
  margin: .1rem 0 0;
  font-size: var(--fs-xl);
  line-height: 1;
  transform: skewX(-7deg);
  transform-origin: left center;
}

/* Three speed marks ahead of the screen name. They are the cheapest possible
   piece of arcade furniture — no asset, no markup — and they do the one job a
   heading on this screen needs doing: telling the player which of the seven
   rooms they are standing in before they have read the word. */
.pl-top h1::before {
  content: "";
  flex: 0 0 auto;
  width: 1.6rem;
  height: .82em;
  background:
    linear-gradient(90deg, var(--pl-ember) 0 26%, transparent 26% 42%,
      var(--pl-ember) 42% 68%, transparent 68% 84%, var(--pl-ember) 84% 100%);
  clip-path: polygon(14% 0, 100% 0, 86% 100%, 0 100%);
  opacity: .9;
}

.pl-top__text { min-width: 0; }

.pl-top__actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: .4rem;
}

/* The chip is a control now: it carries the portrait, so it is also the
   shortest route to the screen where the portrait is changed. */
.pl-user-chip {
  display: flex;
  align-items: center;
  gap: .55rem;
  padding: .3rem 1rem .3rem .3rem;
  border: 2px solid color-mix(in srgb, var(--pl-ember) 45%, transparent);
  border-radius: 999px;
  background: var(--reset-panel-raised);
  box-shadow: var(--pl-lift-sm), var(--pl-glow-soft);
  color: inherit;
  font: inherit;
  font-family: var(--heading-font-family);
  font-size: var(--fs-sm);
  font-weight: 800;
  cursor: pointer;
  transition: transform .16s ease, box-shadow .16s ease;
}

.pl-user-chip:hover {
  box-shadow: var(--pl-lift);
  transform: translate(-1px, -1px);
}

/* The chip is a destination like any tab — and on a phone it is the profile's
   only entry point — so it lights up like one when that is where you are. */
.pl-user-chip.is-current {
  border-color: var(--pl-ember);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--pl-ember) 55%, transparent), var(--pl-lift-sm), var(--pl-glow-soft);
}

.pl-user-chip:focus-visible {
  outline: 3px solid var(--reset-sun);
  outline-offset: 2px;
}

.pl-avatar {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 2.45rem;
  height: 2.45rem;
  overflow: hidden;
  /* Dark, like every plate the portraits sit on — the art is painted on a
     dark backdrop and a cream disc behind it would show as a halo. */
  border: 1.5px solid var(--reset-cream);
  border-radius: 50%;
  background: var(--pl-plate);
  color: var(--reset-on-image);
  font-family: var(--heading-font-family);
  font-weight: 800;
}

/* No portrait yet: the initial needs the old sun disc to stay legible. */
.pl-avatar:not(:has(img)) {
  background: var(--reset-sun);
  color: var(--reset-tile-ink);
}

.pl-avatar img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.pl-view { display: none; }
.pl-view.is-active {
  display: block;
  padding-top: 1.5rem;
  animation: pl-view-in .32s cubic-bezier(.2, .8, .2, 1);
}

@keyframes pl-view-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}

.pl-section-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
  margin: 0 0 1rem;
}

/* Every section title in the cabinet gets the lit bar the mission board's
   headings wear, so a player scrolling a long screen keeps meeting the same
   mark wherever a new block starts. */
.pl-section-head h2 {
  display: flex;
  align-items: center;
  gap: .6rem;
  margin: 0;
  font-size: var(--fs-lg);
}

.pl-section-head h2::before {
  content: "";
  flex: 0 0 auto;
  width: .34rem;
  height: 1.1em;
  border-radius: 2px;
  background: var(--pl-ember);
  box-shadow: 0 0 14px -1px color-mix(in srgb, var(--pl-ember) 80%, transparent);
}

.pl-section-head p {
  max-width: 62ch;
  margin: .25rem 0 0;
  color: var(--reset-muted);
  font-size: var(--fs-sm);
}

/* How much of the grid the filters are currently letting through. It lives in
   the head rather than beside the chips so it does not move when a chip row
   wraps, and it reads as a readout, not as a label. */
.pl-count {
  flex: 0 0 auto;
  align-self: center;
  padding: .25rem .7rem;
  border: 2px solid var(--reset-line-strong);
  border-radius: 999px;
  color: var(--reset-muted);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-weight: 700;
  white-space: nowrap;
}

.pl-count:empty { display: none; }

/* ===== Player card =======================================================
   The first thing on the inventory tab: who you are, how far the collection
   has come, and the two counts that matter. The progress bar is derived
   client-side from the skins actually owned — it is a collection meter, not a
   second currency, and it is labelled that way. */

.pl-hero {
  display: grid;
  grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
  gap: .9rem;
  align-items: stretch;
  margin-bottom: 1.6rem;
}

/* The same band the mission screen opens with, one room over: ember light from
   the corner, the hatch over it, and the portrait as the only fully lit object
   in the frame. The two screens a player bounces between all evening should
   greet them with the same face. */
.pl-hero__card {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 1.35rem;
  overflow: hidden;
  padding: 1.4rem;
  border: var(--pl-edge);
  border-radius: var(--pl-radius);
  background:
    radial-gradient(58% 150% at 96% -10%, color-mix(in srgb, var(--pl-ember) 30%, transparent), transparent 66%),
    linear-gradient(150deg, var(--reset-panel-raised) 0%, var(--reset-panel) 100%);
  box-shadow: var(--pl-lift), var(--pl-glow-soft);
}

.pl-hero__card::before {
  position: absolute;
  inset: 0;
  background-image: var(--pl-hatch);
  opacity: .8;
  pointer-events: none;
  content: "";
}

.pl-hero__card > * { position: relative; }

.pl-hero__body {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-width: 0;
}

.pl-hero__body > .pl-hud {
  margin: 0;
  color: var(--reset-ink-coral);
}

/* ===== Portrait ==========================================================
   The avatars are 512px character paintings, not 32px gravatars, so on the
   inventory tab one of them gets to be the second-biggest thing on the
   screen. The plate is a window: art edge to edge, ink outline, hard shadow,
   the rank shield in the corner, and a scrim that names what the click does —
   because the picture is also the button that changes it. */

.pl-portrait {
  --pl-tier: var(--reset-sun);
  position: relative;
  flex: 0 0 auto;
  display: block;
  /* Centred rather than stretched: the plate is square, and a grid row that
     is taller than the art would otherwise leave a dark strip under it with
     the rank shield floating in the middle of nothing. */
  align-self: center;
  width: clamp(6.75rem, 14vw, 11.5rem);
  padding: 0;
  overflow: hidden;
  border: var(--pl-border);
  border-radius: var(--pl-radius);
  background: var(--pl-plate);
  box-shadow: var(--pl-lift);
  color: var(--reset-on-image);
  font: inherit;
  cursor: pointer;
  transition: transform .18s ease, box-shadow .18s ease;
}

.pl-portrait:hover,
.pl-portrait:focus-visible {
  box-shadow: var(--pl-lift-hi), 0 0 22px color-mix(in srgb, var(--pl-tier) 32%, transparent);
  transform: translate(-2px, -2px);
}

.pl-portrait:focus-visible {
  outline: 3px solid var(--reset-sun);
  outline-offset: 2px;
}

.pl-portrait__plate {
  display: grid;
  place-items: center;
  width: 100%;
  aspect-ratio: 1;
  overflow: hidden;
  background:
    radial-gradient(circle at 50% 34%, color-mix(in srgb, var(--pl-tier) 26%, transparent), transparent 72%),
    var(--pl-plate-glow);
  color: var(--reset-on-image);
  font-family: var(--heading-font-family);
  font-size: 2.6rem;
  font-weight: 800;
}

.pl-portrait__plate img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform .3s cubic-bezier(.2, .8, .2, 1);
}

.pl-portrait:hover .pl-portrait__plate img { transform: scale(1.06); }

/* The affordance, in two strengths. The pip is always on — a phone has no
   hover to reveal anything, and a portrait that silently happens to be a
   button is a portrait nobody presses. The scrim says the same thing at full
   volume once a pointer or the keyboard arrives, and the pip steps aside. */
.pl-portrait__pip {
  position: absolute;
  top: .45rem;
  left: .45rem;
  display: grid;
  place-items: center;
  width: 1.6rem;
  height: 1.6rem;
  border: 2px solid var(--reset-cream);
  border-radius: 50%;
  background: var(--reset-panel-raised);
  color: var(--reset-cream);
  font-size: .66rem;
  box-shadow: var(--pl-lift-sm);
  transition: opacity .18s ease;
}

.pl-portrait:hover .pl-portrait__pip,
.pl-portrait:focus-visible .pl-portrait__pip { opacity: 0; }

.pl-portrait__scrim {
  position: absolute;
  inset: 0;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: .3rem;
  background: linear-gradient(to top,
    color-mix(in srgb, #06070a 88%, transparent),
    color-mix(in srgb, #06070a 34%, transparent));
  color: var(--reset-on-image);
  opacity: 0;
  transition: opacity .18s ease;
}

.pl-portrait:hover .pl-portrait__scrim,
.pl-portrait:focus-visible .pl-portrait__scrim { opacity: 1; }

.pl-portrait__scrim i { font-size: 1.15rem; }

.pl-portrait__scrim em {
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-style: normal;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
}

/* The rank shield, slapped on the corner of the plate the way a level badge
   sits on a character card. It reads over the art, so it carries the outline
   and the offset shadow rather than relying on contrast with the painting. */
.pl-portrait__level {
  position: absolute;
  right: .45rem;
  bottom: .45rem;
  display: inline-flex;
  align-items: center;
  gap: .28rem;
  padding: .16rem .5rem;
  border: 2px solid var(--reset-cream);
  border-radius: 999px;
  /* Lightened the same way the rarity badge is: several rank hues (the epic
     purple in particular) miss contrast as 11px type at full strength. */
  background: color-mix(in srgb, var(--pl-tier) 74%, #fff);
  color: var(--reset-tile-ink);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-weight: 700;
  box-shadow: var(--pl-lift-sm);
}

.pl-portrait__level i { font-size: .7em; }

/* The profile identity block wants the same object at chip scale. */
.pl-portrait--sm {
  width: clamp(4.5rem, 11vw, 5.5rem);
  border-radius: var(--pl-radius-sm);
  box-shadow: var(--pl-lift-sm);
}

.pl-portrait--sm .pl-portrait__plate { font-size: 1.7rem; }
.pl-portrait--sm .pl-portrait__scrim i { font-size: .95rem; }

.pl-portrait--sm .pl-portrait__pip {
  top: .3rem;
  left: .3rem;
  width: 1.35rem;
  height: 1.35rem;
  font-size: .58rem;
}

.pl-hero__name {
  margin: .15rem 0 .45rem;
  overflow-wrap: anywhere;
  font-size: clamp(var(--fs-lg), 2.6vw, var(--fs-2xl));
  line-height: 1.05;
}

.pl-hero__chips {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
  margin-bottom: .2rem;
}

/* XP is the number the rank is made of, so it sits right beside it and is set
   in the HUD face — it is a readout, not a word. */
.pl-hero__xp {
  display: inline-flex;
  align-items: center;
  padding: .2rem .6rem;
  border: 1.5px solid var(--reset-cream);
  border-radius: 999px;
  background: var(--reset-sun);
  color: var(--reset-tile-ink);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-weight: 700;
}

.pl-hero__rank {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  padding: .2rem .6rem;
  border: 1.5px solid var(--reset-cream);
  border-radius: 999px;
  background: var(--reset-sky);
  color: var(--reset-tile-ink);
  font-size: var(--fs-2xs);
  font-weight: 800;
}

.pl-hero__meter {
  height: 12px;
  margin-top: .7rem;
  overflow: hidden;
  border: 1.5px solid var(--reset-cream);
  border-radius: 999px;
  background: var(--reset-panel);
}

.pl-hero__meter span {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: repeating-linear-gradient(
    135deg,
    var(--reset-coral) 0 8px,
    color-mix(in srgb, var(--reset-coral) 72%, var(--reset-sun)) 8px 16px);
  transition: width .6s cubic-bezier(.2, .8, .2, 1);
}

.pl-hero__hint {
  margin: .45rem 0 0;
  color: var(--reset-muted);
  font-size: var(--fs-xs);
}

/* ===== Vault =============================================================
   The counts, and underneath them what the collection is actually made of.
   The mix bar is the same colour vocabulary the grid below uses, so a player
   learns the tiers here and then filters by them one section down. */

.pl-vault {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: .9rem;
  min-width: 0;
}

.pl-vault__counts {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: .9rem;
}

.pl-vault__mix {
  display: grid;
  align-content: start;
  gap: .5rem;
  padding: .9rem 1rem 1rem;
  border: var(--pl-border);
  border-radius: var(--pl-radius);
  background: var(--reset-panel-raised);
  box-shadow: var(--pl-lift);
}

.pl-vault__mix > .pl-hud {
  margin: 0;
  color: var(--reset-muted);
}

.pl-mix {
  display: flex;
  height: 14px;
  overflow: hidden;
  border: 1.5px solid var(--reset-cream);
  border-radius: 999px;
  background: var(--reset-panel);
}

.pl-mix > span {
  min-width: 3px;
  background: var(--rarity);
  transition: flex-grow .5s cubic-bezier(.2, .8, .2, 1);
}

.pl-mix > span + span { border-left: 1.5px solid var(--reset-cream); }

/* Nothing owned yet: a dashed rail instead of an empty pill, so the row does
   not read as a bar that failed to load. */
.pl-mix.is-empty {
  border-style: dashed;
  border-color: var(--reset-line-strong);
}

.pl-mix__legend {
  display: flex;
  flex-wrap: wrap;
  gap: .25rem .7rem;
}

.pl-mix__key {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  color: var(--reset-muted);
  font-size: var(--fs-2xs);
  font-weight: 700;
}

.pl-mix__key i {
  width: .62rem;
  height: .62rem;
  border: 1.5px solid var(--reset-cream);
  border-radius: 50%;
  background: var(--rarity);
}

.pl-mix__key b {
  color: var(--reset-cream);
  font-family: "JetBrains Mono", monospace;
}

.pl-mix__empty {
  margin: 0;
  color: var(--reset-muted);
  font-size: var(--fs-xs);
}

/* These two were flat pastel slabs — a sky tile and a rose tile — which on a
   loot screen read as stationery. They are counters of things the player owns,
   so they join the family every owned object on this page belongs to: a dark
   plate, one hue lighting it from a corner, and the number as the lit part.
   Dark in both themes for the same reason the coin is — a tally that repaints
   itself when the page theme flips stops feeling like a possession. */
.pl-stat {
  --stat-tint: var(--pl-flare);
  position: relative;
  display: grid;
  align-content: space-between;
  gap: .45rem;
  overflow: hidden;
  padding: 1.15rem 1.2rem;
  border: 2px solid color-mix(in srgb, var(--stat-tint) 38%, transparent);
  border-radius: var(--pl-radius);
  background:
    radial-gradient(85% 140% at 10% -15%, color-mix(in srgb, var(--stat-tint) 34%, transparent), transparent 68%),
    linear-gradient(152deg, #1a1f29 0%, #0c0e13 100%);
  color: var(--reset-on-image);
  box-shadow: var(--pl-lift), 0 0 30px -18px color-mix(in srgb, var(--stat-tint) 90%, transparent);
  transition: transform .18s ease, box-shadow .18s ease;
}

.pl-stat:hover {
  box-shadow: var(--pl-lift-hi), 0 0 34px -12px color-mix(in srgb, var(--stat-tint) 90%, transparent);
  transform: translate(-2px, -2px);
}

.pl-stat--skins { --stat-tint: var(--reset-sky); }
.pl-stat--cases { --stat-tint: var(--reset-rose); }

.pl-stat i { color: var(--stat-tint); font-size: 1.25rem; }

.pl-stat b {
  display: block;
  color: #fff;
  font-family: var(--heading-font-family);
  font-size: var(--fs-2xl);
  font-weight: 900;
  line-height: .95;
  letter-spacing: -.03em;
}

.pl-stat span {
  color: color-mix(in srgb, var(--reset-on-image) 72%, transparent);
  font-size: var(--fs-xs);
  font-weight: 800;
  letter-spacing: .1em;
  text-transform: uppercase;
}

/* ===== Панель полки ======================================================
   Прибор над сеткой: поиск, кнопки со списками, метки включённого. Он один и
   тот же над инвентарём и над витриной — игрок учит его один раз.

   Раньше на этом месте лежали три ряда чипов, и ряд «оружие» рос вместе с
   коллекцией, пока не занимал треть экрана. Список уехал внутрь кнопки; чтобы
   свёрнутый список ничего не спрятал, под панелью идёт строка меток.

   Панель липнет под шапкой: на полке в пятьсот карточек фильтр нужен там, куда
   смотрит глаз, а не там, где начинается страница. Высоту шапки меряет JS и
   кладёт в --pl-top-h. И это третья полоса консоли, а не карточка над сеткой:
   пока она приклеена, она физически продолжает ленту — тот же паз, та же
   кромка, тот же край. Отсюда и ноль зазора: карточка со скруглениями внутри
   колонки оставляла между собой и лентой прорезь, в которую было видно
   страницу. */

.pl-board {
  position: sticky;
  top: var(--pl-top-h, 4.6rem);
  /* Выше шапки: сама панель под неё не заезжает — она к ней приклеена, — но
     список, раскрытый вверх, дотянуться до шапки может. */
  z-index: 6;
  display: grid;
  gap: .55rem;
  margin-block: 0 1.1rem;
  padding-block: .6rem .65rem;
  border-bottom: var(--pl-border);
  background: var(--pl-groove);
  box-shadow: inset 0 3px 0 color-mix(in srgb, var(--reset-cream) 7%, transparent);
}

.pl-board__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .45rem;
  /* Сетка карточек под панелью узкая ровно настолько, насколько ей позволит
     эта строка: без нуля кнопки продавили бы колонку за край экрана. */
  min-width: 0;
}

.pl-board__row .pl-search { flex: 1 1 13rem; }

.pl-board__tools {
  display: flex;
  flex-wrap: wrap;
  gap: .45rem;
  min-width: 0;
}

/* Строка «что сейчас включено». Живёт только когда есть что показывать. */
.pl-board__on {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .35rem;
  padding-top: .5rem;
  border-top: 2px dashed var(--reset-line);
}

.pl-board__on[hidden] { display: none; }

.pl-board__on-label {
  margin-right: .15rem;
  color: var(--reset-muted);
  font-family: var(--heading-font-family);
  font-size: var(--fs-2xs);
  font-weight: 800;
  letter-spacing: .04em;
  text-transform: uppercase;
}

.pl-board__clear {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  padding: .25rem .5rem;
  border: 0;
  border-radius: 999px;
  background: none;
  color: var(--reset-muted);
  font: inherit;
  font-size: var(--fs-2xs);
  font-weight: 800;
  text-decoration: underline;
  text-transform: uppercase;
  cursor: pointer;
}

.pl-board__clear:hover { color: var(--pl-ember); }

/* --- Кнопка со списком --------------------------------------------------- */

.pl-pick { position: relative; }

.pl-pick__trigger,
.pl-toggle {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  max-width: 100%;
  padding: .5rem .8rem;
  border: 2px solid var(--reset-line-strong);
  border-radius: 999px;
  background: var(--reset-panel-raised);
  color: var(--reset-cream);
  font: inherit;
  font-size: var(--fs-xs);
  font-weight: 700;
  white-space: nowrap;
  cursor: pointer;
  transition: border-color .14s ease, box-shadow .14s ease, transform .14s ease;
}

.pl-pick__trigger:hover,
.pl-toggle:hover {
  border-color: var(--reset-cream);
  transform: translate(-1px, -1px);
  box-shadow: var(--pl-lift-sm);
}

.pl-pick__trigger:focus-visible,
.pl-toggle:focus-visible {
  outline: 3px solid var(--reset-sun);
  outline-offset: 2px;
}

/* Одно слово на кнопке: имя фильтра, пока он не тронут, дальше — что он
   показывает. */
.pl-pick__value {
  overflow: hidden;
  max-width: 10rem;
  font-family: var(--heading-font-family);
  font-size: var(--fs-xs);
  font-weight: 900;
  text-overflow: ellipsis;
  text-transform: uppercase;
  white-space: nowrap;
}

.pl-pick__caret {
  margin-left: .1rem;
  color: var(--reset-muted);
  font-size: .7em;
  transition: transform .16s ease;
}

.pl-pick.is-open .pl-pick__caret { transform: rotate(180deg); }

/* Зажжённая кнопка — тот же уголёк, которым горит выбранная вкладка: панель
   должна отвечать «полка сейчас показывает не всё» одним взглядом. */
.pl-pick.is-lit .pl-pick__trigger,
.pl-toggle.is-lit {
  border-color: transparent;
  background: var(--pl-ember-ramp);
  color: var(--pl-ember-ink);
  box-shadow: var(--pl-lift-sm), var(--pl-glow-soft);
}

.pl-pick.is-lit .pl-pick__caret { color: inherit; }

.pl-pick.is-open .pl-pick__trigger { border-color: var(--reset-cream); }

/* --- Список -------------------------------------------------------------- */

.pl-pick__menu {
  position: absolute;
  top: calc(100% + .4rem);
  left: 0;
  z-index: 20;
  display: grid;
  gap: .3rem;
  width: max-content;
  min-width: 100%;
  max-width: min(20rem, calc(100vw - 2rem));
  padding: .4rem;
  border: var(--pl-border);
  border-radius: var(--pl-radius-sm);
  background: var(--reset-panel-raised);
  box-shadow: var(--pl-lift);
  animation: pl-pick-in .14s ease;
}

/* Последняя кнопка в ряду роняет список за правый край, а панель у нижнего
   края экрана — за нижний. JS меряет место и просит раскрыться влево или
   вверх. */
.pl-pick.is-flipped .pl-pick__menu {
  right: 0;
  left: auto;
}

.pl-pick.is-up .pl-pick__menu {
  top: auto;
  bottom: calc(100% + .4rem);
  animation-name: pl-pick-up;
}

@keyframes pl-pick-up {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: none; }
}

@keyframes pl-pick-in {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: none; }
}

.pl-pick__menu[hidden] { display: none; }

.pl-pick__list {
  display: grid;
  gap: .1rem;
  /* Сто стволов — это сто строк, и они листаются внутри списка, а не растягивают
     его на весь экран. */
  overflow-y: auto;
  max-height: min(19rem, 50vh);
  overscroll-behavior: contain;
}

.pl-pick__find {
  display: flex;
  align-items: center;
  gap: .4rem;
  padding: .1rem .5rem;
  border: 2px solid var(--reset-line-strong);
  border-radius: 999px;
  color: var(--reset-muted);
}

.pl-pick__find input {
  width: 100%;
  padding: .35rem 0;
  border: 0;
  background: none;
  color: var(--reset-cream);
  font: inherit;
  font-size: var(--fs-xs);
  font-weight: 600;
  outline: none;
}

.pl-pick__row {
  display: flex;
  align-items: center;
  gap: .45rem;
  padding: .45rem .55rem;
  border: 0;
  border-radius: 10px;
  background: none;
  color: var(--reset-cream);
  font: inherit;
  font-size: var(--fs-xs);
  font-weight: 700;
  text-align: left;
  cursor: pointer;
}

.pl-pick__row[hidden] { display: none; }

.pl-pick__row:hover,
.pl-pick__row:focus-visible {
  background: color-mix(in srgb, var(--pl-ember) 14%, transparent);
  outline: none;
}

.pl-pick__row.is-on { background: color-mix(in srgb, var(--pl-ember) 20%, transparent); }

.pl-pick__text {
  overflow: hidden;
  flex: 1;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pl-pick__icon { color: var(--reset-muted); }

.pl-pick__dot {
  flex: 0 0 auto;
  width: .55rem;
  height: .55rem;
  border-radius: 50%;
  background: var(--rarity);
}

/* Число рядом со строкой — сколько карточек она покажет. Пустой результат
   виден до нажатия. */
.pl-pick__count {
  padding: .05rem .35rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--reset-cream) 10%, transparent);
  color: var(--reset-muted);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-weight: 700;
}

.pl-pick__tick {
  color: var(--pl-ember);
  font-size: .7em;
  opacity: 0;
}

.pl-pick__row.is-on .pl-pick__tick { opacity: 1; }

.pl-pick__blank {
  margin: 0;
  padding: .5rem;
  color: var(--reset-muted);
  font-size: var(--fs-xs);
  text-align: center;
}

.pl-pick__blank[hidden] { display: none; }

/* --- Метка включённого фильтра ------------------------------------------- */

.pl-tag {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  padding: .25rem .6rem;
  border: 2px solid var(--reset-line-strong);
  border-radius: 999px;
  background: var(--reset-panel-raised);
  color: var(--reset-cream);
  font: inherit;
  font-size: var(--fs-2xs);
  font-weight: 800;
  cursor: pointer;
  transition: border-color .14s ease, color .14s ease;
}

.pl-tag:hover {
  border-color: var(--reset-danger);
  color: var(--reset-danger);
}

.pl-tag .fa-xmark { font-size: .8em; opacity: .7; }

.pl-tag__dot {
  width: .5rem;
  height: .5rem;
  border-radius: 50%;
  background: var(--rarity);
}

/* --- Низ полки ----------------------------------------------------------- */

/* Пачками полка выкладывается ради браузера, но игроку про пачки знать не
   надо: он видит кнопку и сколько ещё лежит за ней. */
.pl-tail {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .7rem;
  margin-top: 1.1rem;
}

.pl-tail[hidden] { display: none; }

.pl-tail__rest {
  color: var(--reset-muted);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-weight: 700;
}

/* ===== Filter bar ========================================================
   Chips rather than <select>s: a ten-year-old on a phone taps a rarity, they
   do not open a picker. The arsenal keeps them — its drawers hold six classes,
   not a hundred guns. */

.pl-filters {
  display: grid;
  gap: .7rem;
  margin-bottom: 1.1rem;
}

.pl-search {
  display: block;
  position: relative;
  max-width: 22rem;
}


.pl-search i {
  position: absolute;
  top: 50%;
  left: .85rem;
  color: var(--reset-muted);
  transform: translateY(-50%);
  pointer-events: none;
}

.pl-search input {
  width: 100%;
  padding: .72rem 1rem .72rem 2.5rem;
  border: var(--pl-border);
  border-radius: 999px;
  background: var(--reset-panel-raised);
  color: var(--reset-cream);
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 600;
}

.pl-search input:focus-visible {
  outline: 3px solid var(--reset-sun);
  outline-offset: 2px;
}

.pl-chips[hidden] { display: none; }

.pl-chips {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
}

.pl-chip {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  padding: .48rem 1rem;
  border: 2px solid var(--reset-line-strong);
  border-radius: 999px;
  background: var(--reset-panel-raised);
  color: var(--reset-muted);
  font: inherit;
  font-family: var(--heading-font-family);
  font-size: var(--fs-xs);
  font-weight: 800;
  letter-spacing: .01em;
  text-transform: uppercase;
  cursor: pointer;
  transition: transform .14s ease, box-shadow .14s ease, color .14s ease, border-color .14s ease;
}

.pl-chip::before {
  content: "";
  width: .55rem;
  height: .55rem;
  border-radius: 50%;
  background: var(--rarity);
}

/* The weapon chips have no colour to show, so they drop the dot. */
.pl-chip--plain::before { display: none; }

.pl-chip:hover {
  border-color: var(--reset-cream);
  color: var(--reset-cream);
  transform: translate(-1px, -1px);
  box-shadow: var(--pl-lift-sm);
}

/* A selected filter is the ember, not the ink. The old inverted-pill read as a
   toolbar toggle; this reads as the one lane the board is currently showing. */
.pl-chip.is-active {
  border-color: transparent;
  background: var(--pl-ember-ramp);
  color: var(--pl-ember-ink);
  box-shadow: var(--pl-lift-sm), var(--pl-glow-soft);
}

/* ===== Item grid =========================================================
   The card is the page's centrepiece: rarity glow behind the art, a rarity
   band under it (the CS2 tell every player reads before the name), a sticker
   for the count, and a shine that sweeps across on hover. */

.pl-grid {
  display: grid;
  /* The skins are the product. They get the biggest cell the shell allows —
     roughly three across on a laptop, two on a phone — because a rarity glow
     and a weapon render are wasted at thumbnail size. */
  grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
  gap: 1rem;
}

.pl-item {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  overflow: hidden;
  border: var(--pl-border);
  border-radius: var(--pl-radius);
  background: var(--reset-panel-raised);
  box-shadow: var(--pl-lift);
  text-align: left;
  font: inherit;
  color: inherit;
  cursor: pointer;
  transition: transform .18s ease, box-shadow .18s ease;
}

.pl-item:hover,
.pl-item:focus-visible {
  box-shadow: var(--pl-lift-hi);
  transform: translate(-2px, -2px);
}

.pl-item:focus-visible {
  outline: 3px solid var(--reset-sun);
  outline-offset: 2px;
}

/* ===== Art frame =========================================================
   One component behind every catalogue picture on the page — grid tile, rack
   slot, kit row, picker, inspect sheet, reel and prize.

   The box comes first, because the old markup had none. A bare <img> with
   `max-height: 100%` dropped straight into a grid cell is constrained by
   nothing in Chrome — the percentage has no definite height to resolve
   against — so a wide render hung out of the inspect sheet over its own name
   and sat cropped inside its tile. The frame is a real element sized by its
   container, and the picture is fitted into it with object-fit, which cannot
   overflow whatever proportions the render arrives in.

   On that box sit the two things every picture needs: a state — loading,
   ready, missing — and the frame proper, four corner brackets in the rarity
   colour. */

.pl-art {
  position: relative;
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
}

/* Absolutely positioned, and that is the fix rather than a detail of it: a
   percentage height on a replaced element inside a grid cell is not resolved
   by Chrome, while `inset: 0` takes its size straight from the frame. The
   picture is then fitted into that box by object-fit, so no render can
   overflow the frame no matter what shape it arrives in. */
.pl-art .pl-art__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  filter: drop-shadow(0 8px 12px color-mix(in srgb, var(--reset-shadow) 45%, transparent));
  opacity: 0;
  transform: scale(.94);
  transition: opacity .3s ease, transform .35s cubic-bezier(.2, .8, .2, 1);
}

/* Arrival is a fade up rather than a pop-in: the tile is already the right
   size and the right colour, so only the picture itself has to appear. */
.pl-art.is-ready .pl-art__img { opacity: 1; transform: none; }
.pl-art.is-failed .pl-art__img { display: none; }

/* The indicator. A ring in the rarity colour over a shimmer, so a frame that
   is still working reads as "coming" instead of "empty". */
.pl-art__state {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  overflow: hidden;
  border-radius: inherit;
  opacity: 0;
  transition: opacity .2s ease;
  pointer-events: none;
}

.pl-art.is-loading .pl-art__state,
.pl-art.is-failed .pl-art__state { opacity: 1; }

/* A thin scrim under the indicator so the ring reads on a bright tile as well
   as on a dark one. */
.pl-art.is-loading .pl-art__state {
  background: color-mix(in srgb, var(--reset-panel) 62%, transparent);
}

.pl-art.is-loading .pl-art__state::before {
  content: "";
  position: absolute;
  inset: -40% -70%;
  background: linear-gradient(105deg,
    transparent 42%,
    color-mix(in srgb, var(--rarity, var(--reset-line-strong)) 32%, transparent) 50%,
    transparent 58%);
  animation: pl-art-sweep 1.5s ease-in-out infinite;
}

@keyframes pl-art-sweep {
  from { transform: translateX(-55%); }
  to { transform: translateX(55%); }
}

.pl-art__spin {
  display: none;
  width: min(1.7rem, 40%);
  height: auto;
  aspect-ratio: 1;
  border: 3px solid color-mix(in srgb, var(--rarity, var(--reset-line-strong)) 26%, transparent);
  border-top-color: color-mix(in srgb, var(--rarity, var(--reset-line-strong)) 90%, var(--reset-cream));
  border-radius: 50%;
  animation: pl-spin .85s linear infinite;
}

.pl-art.is-loading .pl-art__spin { display: block; }
.pl-art.is-loading .pl-art__state i { display: none; }

/* A render that never arrived falls back to the same icon the catalogue uses
   when it ships without one, dimmed. A browser's broken-image glyph tells the
   player their inventory is broken, which is not what happened. */
.pl-art.is-failed .pl-art__state i { opacity: .5; }

/* Said in words as well, but only in the frames with room for a line. On a
   grid tile the dimmed icon is the whole message; on the inspect sheet, where
   the picture is the point, it is worth saying why there isn't one. */
.pl-art__note { display: none; }

.pl-inspect__art .pl-art.is-failed .pl-art__note,
.pl-prize__art .pl-art.is-failed .pl-art__note {
  display: block;
  color: var(--reset-muted);
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
}

.pl-art.is-failed .pl-art__state {
  align-content: center;
  gap: .5rem;
}

/* The brackets. Eight hairlines parked in the four corners of the frame —
   the way a piece under glass is cornered, and the cheapest frame that does
   not fight the tile's own border. */
.pl-item__art .pl-art::before,
.pl-slot__art .pl-art::before,
.pl-inspect__art .pl-art::before,
.pl-prize__art .pl-art::before {
  --pl-corner: 1.1rem;
  --pl-corner-ink: color-mix(in srgb, var(--rarity, var(--reset-line-strong)) 60%, var(--reset-cream));
  content: "";
  position: absolute;
  /* Outside the picture, inside the tile: the brackets need air on both sides
     or they read as a second border rather than as a mount. */
  inset: -.5rem;
  background:
    linear-gradient(var(--pl-corner-ink) 0 0) 0 0 / var(--pl-corner) 2px,
    linear-gradient(var(--pl-corner-ink) 0 0) 0 0 / 2px var(--pl-corner),
    linear-gradient(var(--pl-corner-ink) 0 0) 100% 0 / var(--pl-corner) 2px,
    linear-gradient(var(--pl-corner-ink) 0 0) 100% 0 / 2px var(--pl-corner),
    linear-gradient(var(--pl-corner-ink) 0 0) 0 100% / var(--pl-corner) 2px,
    linear-gradient(var(--pl-corner-ink) 0 0) 0 100% / 2px var(--pl-corner),
    linear-gradient(var(--pl-corner-ink) 0 0) 100% 100% / var(--pl-corner) 2px,
    linear-gradient(var(--pl-corner-ink) 0 0) 100% 100% / 2px var(--pl-corner);
  background-repeat: no-repeat;
  opacity: .8;
  pointer-events: none;
}

/* "Equipped", said on the stage itself in the inspect sheet — the sheet is
   the one place the tile's own corner chip is not there to say it. */
.pl-art-tag {
  position: absolute;
  top: .65rem;
  left: .65rem;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  padding: .2rem .6rem;
  border: 2px solid var(--reset-cream);
  border-radius: 999px;
  background: var(--reset-ok);
  color: var(--reset-tile-ink);
  font-size: var(--fs-2xs);
  font-weight: 800;
  letter-spacing: .06em;
  text-transform: uppercase;
}

/* The frames set a display size for their fallback icon; the chip's tick is
   not one of those and has to stay chip-sized — hence the extra class, which
   is what puts this rule above `.pl-inspect__art i`. */
.pl-art-tag i.fas {
  color: inherit;
  font-size: 1em;
}

/* "Рассмотреть". The whole plate is the hit area and the chip is only where
   the invitation is written, because the first instinct on a render worth
   looking at is to press the render itself. */
.pl-art-zoom {
  position: absolute;
  z-index: 3;
  inset: 0;
  display: grid;
  align-items: end;
  justify-items: end;
  padding: .65rem;
  border: 0;
  border-radius: inherit;
  background: transparent;
  color: var(--reset-cream);
  font: inherit;
  cursor: zoom-in;
}

.pl-art-zoom__chip {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  padding: .26rem .6rem;
  border: 2px solid var(--reset-cream);
  border-radius: 999px;
  background: color-mix(in srgb, var(--reset-panel-raised) 88%, transparent);
  box-shadow: var(--pl-lift-sm);
  font-size: var(--fs-2xs);
  font-weight: 800;
  letter-spacing: .06em;
  text-transform: uppercase;
  backdrop-filter: blur(4px);
  transition: transform .15s ease, box-shadow .15s ease;
}

.pl-art-zoom__chip i.fas {
  color: inherit;
  font-size: 1em;
}

.pl-art-zoom:hover .pl-art-zoom__chip {
  box-shadow: var(--pl-lift);
  transform: translate(-1px, -1px);
}

.pl-art-zoom:active .pl-art-zoom__chip {
  box-shadow: 1px 1px 0 var(--reset-shadow);
  transform: translate(2px, 2px);
}

.pl-art-zoom:focus-visible {
  outline: 3px solid var(--reset-sun);
  outline-offset: -3px;
}

/* On a rack tile the invitation waits to be asked for. A hundred cards each
   carrying the word is a wall of chips, and the plate is the hit area with or
   without it — the cursor already says `zoom-in` over the render. */
.pl-art-zoom--quiet .pl-art-zoom__chip {
  opacity: 0;
  transform: translate(0, .35rem);
  transition: opacity .15s ease, transform .15s ease, box-shadow .15s ease;
}

.pl-art-zoom--quiet:hover .pl-art-zoom__chip,
.pl-art-zoom--quiet:focus-visible .pl-art-zoom__chip {
  opacity: 1;
  transform: translate(-1px, -1px);
}

/* The pin. In the picker the plate is already a button that puts the skin on,
   so the way to look at one first is a badge in the corner of the render,
   clear of the press that equips it. The badge is small on purpose; the
   button around it is not — the padding is what a thumb actually lands on,
   and a 29px target beside a button that equips is a mis-tap waiting. */
.pl-art-zoom--pin {
  inset: auto;
  top: .45rem;
  right: .45rem;
  place-items: center;
  padding: .4rem;
  border-radius: 999px;
}

/* Where the word cannot be carried — the picker's corner badge, and a touch
   screen, which has no cursor to fade the chip in with — the magnifier says
   it on its own, and the label stays for the accessible name. */
.pl-art-zoom--pin .pl-art-zoom__chip {
  display: grid;
  place-items: center;
  width: 1.9rem;
  height: 1.9rem;
  padding: 0;
}

.pl-art-zoom--pin .pl-art-zoom__label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

@media (hover: none) {
  .pl-art-zoom--quiet .pl-art-zoom__chip {
    display: grid;
    place-items: center;
    width: 1.9rem;
    height: 1.9rem;
    padding: 0;
    opacity: 1;
    transform: none;
  }

  .pl-art-zoom--quiet .pl-art-zoom__label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

.pl-item__art {
  position: relative;
  display: grid;
  place-items: center;
  /* Keep the whole render inside the mount, but give it most of the plate.
     The former 1.2rem gutter made long weapons look like thumbnails; trying
     to compensate with the hover zoom then clipped barrels and glove tips. */
  height: 13.5rem;
  padding: .7rem;
  overflow: hidden;
  background:
    radial-gradient(circle at 50% 42%, color-mix(in srgb, var(--rarity) 42%, transparent), transparent 68%),
    repeating-linear-gradient(135deg,
      color-mix(in srgb, var(--rarity) 12%, transparent) 0 10px,
      transparent 10px 20px),
    var(--reset-panel);
}

/* The shine already carries the hover state. Keep the fitted render still so
   its full silhouette remains visible right up to the card's safe edge. */
.pl-item:hover .pl-item__art img { transform: none; }

.pl-item__art i {
  color: color-mix(in srgb, var(--rarity) 75%, var(--reset-cream));
  font-size: 3.4rem;
}

/* The shine. One skewed highlight parked off-screen left, swept across the
   art on hover — the cheapest way to make a static PNG feel handled. */
.pl-item__art::after {
  content: "";
  position: absolute;
  inset: -40% -60%;
  background: linear-gradient(105deg, transparent 42%, color-mix(in srgb, #fff 45%, transparent) 50%, transparent 58%);
  transform: translateX(-120%);
  transition: transform .55s ease;
  pointer-events: none;
}

.pl-item:hover .pl-item__art::after { transform: translateX(120%); }

.pl-item__band {
  height: 6px;
  background: var(--rarity);
  border-top: 2px solid var(--reset-cream);
  border-bottom: 2px solid var(--reset-cream);
}

.pl-item__body {
  display: grid;
  gap: .3rem;
  padding: .8rem .9rem 1rem;
}

.pl-age-mark {
  display: inline-grid;
  width: 2.35rem;
  height: 2rem;
  place-items: center;
  margin-left: .45rem;
  padding: .1rem;
  border: 0;
  background: var(--pl-ember);
  clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
  color: var(--reset-tile-ink);
  font-family: var(--body-font-family);
  font-size: .68rem;
  font-weight: 900;
  letter-spacing: 0;
  line-height: 1;
  vertical-align: middle;
}

.pl-item__meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .55rem;
  min-width: 0;
}

.pl-item__kicker {
  color: var(--reset-muted);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
}

/* Case descriptions varied between a sentence and an empty slot. A compact
   theme stamp is the stable comparison point instead: cyan for the sci-fi
   line, warm gold for the grounded line, both using the cabinet's existing
   ink outline and mono label language. */
.pl-case-tag {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
  padding: .16rem .48rem;
  border: 1.5px solid var(--reset-cream);
  border-radius: 999px;
  color: var(--reset-cream);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-weight: 800;
  letter-spacing: .04em;
  line-height: 1;
  box-shadow: 1px 1px 0 var(--reset-shadow);
  white-space: nowrap;
}

.pl-case-tag--scifi {
  background: color-mix(in srgb, var(--reset-sky) 72%, var(--reset-panel));
}

.pl-case-tag--realistic {
  background: color-mix(in srgb, var(--reset-sun) 78%, var(--reset-panel));
}

.pl-item__name {
  margin: 0;
  font-size: var(--fs-md);
  line-height: 1.15;
  text-transform: uppercase;
}

.pl-item__desc {
  margin: 0;
  color: var(--reset-muted);
  font-size: var(--fs-xs);
  line-height: 1.45;
  /* Two lines maximum: the grid must stay a grid, not a column of essays. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

/* ===== Tier ==============================================================
   The rarity is the first thing a player looks for, so it stops being a small
   coloured word and becomes a badge: the catalogue hue as a fill, lightened
   just enough that the dark tile ink clears contrast on every one of the five
   tiers (raw epic purple at 12px would not), wearing the same ink outline as
   every other control on the site. */

.pl-tier {
  display: inline-flex;
  align-items: center;
  align-self: start;
  gap: .3rem;
  padding: .18rem .6rem;
  border: 2px solid var(--reset-cream);
  border-radius: 999px;
  background: color-mix(in srgb, var(--rarity) 74%, #fff);
  color: var(--reset-tile-ink);
  font-size: var(--fs-2xs);
  font-weight: 800;
  letter-spacing: .07em;
  text-transform: uppercase;
}

/* The badge with nothing in it. A slot without a skin used to put a run of
   grey mono caps where every other slot puts a badge, so "Штурмовые" and
   "Скин не выбран" ran together into one grey line and the rows never aligned.
   The empty slot keeps the badge and empties it out instead: same pill, dashed
   edge, no fill — a socket rather than a sentence. */
.pl-tier--blank {
  border-color: var(--reset-line-strong);
  border-style: dashed;
  background: none;
  color: var(--reset-muted);
  letter-spacing: .09em;
}

/* The top two tiers in play get a halo. Which tiers those are is decided at
   render time from the rarities the catalogue actually uses, so renumbering
   them in the admin panel cannot strand this rule. */
.pl-tier.is-elite {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--rarity) 38%, transparent);
}

/* An elite card glows in its own colour, wears a thicker rarity band and gets
   a slow sheen across the art — the "this one is special" language every loot
   grid uses. */
.pl-item.is-elite,
.pl-slot.is-elite {
  box-shadow: var(--pl-lift), 0 0 26px color-mix(in srgb, var(--rarity) 42%, transparent);
}

.pl-item.is-elite:hover {
  box-shadow: var(--pl-lift-hi), 0 0 34px color-mix(in srgb, var(--rarity) 55%, transparent);
}

.pl-item.is-elite .pl-item__band,
.pl-slot.is-elite .pl-item__band { height: 10px; }

.pl-item.is-elite .pl-item__art::before {
  content: "";
  position: absolute;
  inset: -30%;
  background: conic-gradient(from 0deg,
    transparent 0 68%,
    color-mix(in srgb, var(--rarity) 45%, transparent) 84%,
    transparent 100%);
  animation: pl-sheen 5s linear infinite;
  pointer-events: none;
}

@keyframes pl-sheen { to { transform: rotate(360deg); } }

/* "Equipped" — static in an arsenal slot header, pinned to the corner of a
   grid card. */
.pl-worn {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  padding: .15rem .55rem;
  border: 2px solid var(--reset-cream);
  border-radius: 999px;
  background: var(--reset-ok);
  color: var(--reset-tile-ink);
  font-size: var(--fs-2xs);
  font-weight: 800;
}

.pl-item .pl-worn {
  position: absolute;
  top: .6rem;
  left: .6rem;
  z-index: 2;
  box-shadow: var(--pl-lift-sm);
}

.pl-item.is-worn { border-color: var(--reset-ink-ok); }

/* The count sticker, tilted like something slapped onto the case. */
.pl-qty {
  position: absolute;
  top: .6rem;
  right: .6rem;
  padding: .2rem .55rem;
  border: 2px solid var(--reset-cream);
  border-radius: 999px;
  background: var(--reset-sun);
  color: var(--reset-tile-ink);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-weight: 700;
  box-shadow: var(--pl-lift-sm);
  transform: rotate(4deg);
}

.pl-item__row {
  display: flex;
  align-items: center;
  gap: .5rem;
  /* The row sits in a grid or flex body, where an item may not shrink below
     its own content by default — so a long label used to push the row past
     the card edge, where the card's overflow clipped it. The row and its
     button both give that minimum up, and the label wraps instead. */
  min-width: 0;
  margin-top: .5rem;
}

.pl-item__row .pl-btn {
  flex: 1;
  min-width: 0;
}

/* ===== The Battle Token ===================================================
   The soft currency, and the one part of this cabinet that has to feel like an
   object rather than a field. Everything below serves that: the coin is struck
   metal with a reeded rim and a gleam, prices are stamped tags, payouts are the
   same tag in green, and the balance in the header reacts every time it moves.

   The gold ramp is deliberately theme-independent. A coin is the same coin on
   the cream page and on the dark one, and pinning it is what makes a price tag,
   a payout pill and a history delta read as the same physical thing. Ink on
   gold is always the dark end of the ramp, so contrast never depends on the
   theme either. */

/* The sprite is markup, not layout: nothing should reserve space for it. */
.pl-coin-sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.pl-coin {
  position: relative;
  display: inline-grid;
  place-items: center;
  flex: 0 0 auto;
  width: 1.2em;
  height: 1.2em;
  vertical-align: -.22em;
  border-radius: 50%;
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .35));
}

.pl-coin svg {
  display: block;
  width: 100%;
  height: 100%;
}

.pl-coin--sm { width: 1.55rem; height: 1.55rem; }
.pl-coin--md { width: 2.15rem; height: 2.15rem; }
.pl-coin--lg { width: 3.3rem; height: 3.3rem; }
.pl-coin--xl { width: 4.7rem; height: 4.7rem; }

/* The gleam, and only on the big ones. A price list with twenty coins all
   catching the light reads as noise; one struck coin catching it reads as
   treasure, which is the entire point of the screen. */
.pl-coin--sm::after,
.pl-coin--md::after,
.pl-coin--lg::after,
.pl-coin--xl::after {
  content: "";
  position: absolute;
  inset: 7%;
  border-radius: 50%;
  background: linear-gradient(108deg,
    rgba(255, 255, 255, 0) 42%,
    rgba(255, 255, 255, .9) 50%,
    rgba(255, 255, 255, 0) 58%);
  background-repeat: no-repeat;
  background-size: 280% 100%;
  animation: pl-coin-gleam 4.6s cubic-bezier(.6, 0, .4, 1) infinite;
  pointer-events: none;
}

@keyframes pl-coin-gleam {
  0%, 58% { background-position: 195% 0; }
  84%, 100% { background-position: -95% 0; }
}

/* Struck: the coin flips when the balance moves in the player's favour. */
.pl-coin.is-struck svg { animation: pl-coin-flip .8s cubic-bezier(.3, 0, .25, 1); }

@keyframes pl-coin-flip {
  from { transform: rotateY(0) scale(1); }
  55% { transform: rotateY(360deg) scale(1.16); }
  to { transform: rotateY(720deg) scale(1); }
}

/* ===== Balance in the header =============================================
   A button, because in every game a player has ever touched, tapping the
   currency opens the place to spend it. The plate is dark on both themes so
   the gold has something to sing against. */

.pl-wallet {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: .55rem;
  padding: .28rem .95rem .28rem .32rem;
  border: var(--pl-border);
  border-radius: 999px;
  background:
    radial-gradient(120% 180% at 14% -20%, rgba(255, 206, 92, .38), transparent 60%),
    linear-gradient(146deg, #2e2213 0%, #16130f 56%, #241a0f 100%);
  color: var(--reset-on-image);
  font: inherit;
  cursor: pointer;
  box-shadow: var(--pl-lift-sm), inset 0 1.5px 0 rgba(255, 232, 168, .22);
  transition: transform .16s cubic-bezier(.2, .9, .3, 1.4), box-shadow .16s ease;
}

.pl-wallet[hidden] { display: none; }

.pl-wallet:hover {
  transform: translate(-1px, -1px);
  box-shadow: var(--pl-lift), inset 0 1.5px 0 rgba(255, 232, 168, .3);
}

.pl-wallet:active {
  transform: translate(1px, 1px);
  box-shadow: 1px 1px 0 var(--reset-shadow), inset 0 1.5px 0 rgba(255, 232, 168, .22);
}

.pl-wallet:focus-visible { outline: 3px solid var(--reset-ink-sun); outline-offset: 3px; }

.pl-wallet__text {
  display: grid;
  gap: .12rem;
  text-align: left;
}

.pl-wallet__label {
  color: rgba(255, 222, 150, .78);
  font-size: .55rem;
  line-height: 1;
}

.pl-wallet__value {
  color: #ffd96f;
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-md);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  text-shadow: 0 0 14px rgba(255, 176, 22, .6);
}

/* The reward beat: the pill bumps, a ring flies off it and the amount floats
   away above. Three cheap things at once is what reads as "you got paid". */
.pl-wallet__ring {
  position: absolute;
  inset: -2px;
  border: 2px solid var(--reset-ink-sun);
  border-radius: 999px;
  opacity: 0;
  pointer-events: none;
}

.pl-wallet.is-gain .pl-wallet__ring { animation: pl-wallet-ring .7s ease-out; }
.pl-wallet.is-gain { animation: pl-wallet-bump .5s cubic-bezier(.2, .9, .3, 1.5); }
.pl-wallet.is-spend { animation: pl-wallet-nudge .45s ease; }

@keyframes pl-wallet-ring {
  from { opacity: .9; transform: scale(1); }
  to { opacity: 0; transform: scale(1.4); }
}

@keyframes pl-wallet-bump {
  0%, 100% { transform: scale(1); }
  35% { transform: scale(1.08); }
}

@keyframes pl-wallet-nudge {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-4px); }
  60% { transform: translateX(3px); }
}

.pl-wallet__delta {
  position: absolute;
  top: -.1rem;
  left: 50%;
  z-index: 2;
  padding: .14rem .5rem;
  border: 2px solid var(--reset-cream);
  border-radius: 999px;
  background: var(--reset-ok);
  color: var(--reset-tile-ink);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-weight: 800;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transform: translate(-50%, 0);
}

.pl-wallet__delta.is-minus { background: var(--reset-coral); }
.pl-wallet__delta.is-live { animation: pl-wallet-delta 1.5s cubic-bezier(.2, .9, .3, 1) forwards; }

@keyframes pl-wallet-delta {
  0% { opacity: 0; transform: translate(-50%, .4rem) scale(.7); }
  18% { opacity: 1; transform: translate(-50%, -.6rem) scale(1.06); }
  70% { opacity: 1; transform: translate(-50%, -1.6rem) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -2.7rem) scale(.95); }
}

/* ===== "You cannot buy this" =============================================
   The single most valuable fact about the currency, so it is the loudest
   object on the store rather than a footnote: a struck coin, the five ways to
   earn one, and the player's own lifetime totals under the rule. */

/* Promo-code ticket. It sits at the top of the store because redemption is a
   store action, but keeps a compact form so it does not push the case shelf
   below the first viewport. */
.pl-code {
  display: grid;
  grid-template-columns: auto minmax(15rem, .9fr) minmax(19rem, 1.25fr);
  align-items: center;
  gap: 1rem;
  margin-bottom: 1rem;
  padding: 1rem;
  border: var(--pl-border);
  border-radius: var(--pl-radius);
  background: var(--reset-panel);
  box-shadow: var(--pl-lift);
  overflow: hidden;
}

.pl-code__ticket {
  display: grid;
  place-items: center;
  width: 3.8rem;
  height: 3.8rem;
  border: var(--pl-border);
  border-radius: 14px;
  background: var(--reset-sun);
  color: var(--reset-ink);
  box-shadow: var(--pl-lift-sm);
  font-size: 1.35rem;
  transform: rotate(-4deg);
}

.pl-code__copy { min-width: 0; }
.pl-code__copy .pl-hud { margin: 0 0 .2rem; color: var(--reset-ink-coral); }
.pl-code__copy h3 { margin: 0; font-size: var(--fs-md); }
.pl-code__copy > p:last-child { margin: .25rem 0 0; color: var(--reset-muted); font-size: var(--fs-xs); line-height: 1.45; }

.pl-code__form { display: grid; gap: .45rem; min-width: 0; }
.pl-code__form > label {
  color: var(--reset-muted);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
}

.pl-code__input-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .55rem; }
.pl-code__input-row input {
  width: 100%;
  min-height: 44px;
  border: 2px solid var(--reset-line-strong);
  border-radius: var(--pl-radius-sm);
  background: var(--reset-bg);
  color: var(--reset-ink);
  padding: .7rem .8rem;
  font: 800 var(--fs-sm)/1 "JetBrains Mono", monospace;
  letter-spacing: .08em;
  text-transform: uppercase;
}

.pl-code__input-row input:focus-visible {
  outline: 3px solid color-mix(in srgb, var(--reset-coral) 42%, transparent);
  outline-offset: 2px;
  border-color: var(--reset-ink-coral);
}

.pl-code__captcha { min-height: 0; overflow: hidden; }
.pl-code__captcha:not(:empty) { min-height: 78px; padding-top: .2rem; }
.pl-code__preview {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  min-height: 42px;
  color: var(--reset-muted);
  font-size: var(--fs-xs);
}

.pl-code__status { min-height: 1.25em; margin: 0; color: var(--reset-muted); font-size: var(--fs-xs); line-height: 1.35; }
.pl-code__status.is-error { color: var(--reset-ink-coral); }
.pl-code__status.is-success { color: var(--reset-ink-ok); font-weight: 800; }

.pl-earn {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 1.15rem;
  overflow: hidden;
  margin-bottom: 1.1rem;
  padding: 1.25rem 1.35rem;
  border: var(--pl-border);
  border-radius: var(--pl-radius);
  background:
    radial-gradient(85% 150% at 6% -10%, rgba(255, 200, 74, .45), transparent 58%),
    radial-gradient(70% 130% at 102% 110%, rgba(255, 107, 67, .38), transparent 60%),
    linear-gradient(148deg, #2a1e10 0%, #14120f 58%, #1e1810 100%);
  color: var(--reset-on-image);
  box-shadow: var(--pl-lift);
}

.pl-earn[hidden] { display: none; }

.pl-earn__sheen {
  position: absolute;
  inset: -60% -20%;
  background: conic-gradient(from 210deg,
    transparent 0 62%,
    rgba(255, 214, 110, .22) 78%,
    transparent 92%);
  animation: pl-sheen 14s linear infinite;
  pointer-events: none;
}

.pl-earn > *:not(.pl-earn__sheen) { position: relative; }

.pl-earn__coin {
  display: grid;
  place-items: center;
  width: 6.2rem;
  height: 6.2rem;
  border: 2px solid rgba(255, 219, 140, .32);
  border-radius: 50%;
  background: radial-gradient(circle at 50% 40%, rgba(255, 190, 60, .32), transparent 68%);
}

.pl-earn__kicker {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  margin: 0 0 .4rem;
  padding: .24rem .62rem;
  border-radius: 999px;
  background: rgba(255, 210, 90, .16);
  color: #ffd777;
}

.pl-earn h3 {
  margin: 0;
  color: #fff3d0;
  font-size: clamp(1.05rem, 2.4vw, 1.45rem);
  line-height: 1.15;
  text-wrap: balance;
}

.pl-earn__lede {
  max-width: 62ch;
  margin: .45rem 0 0;
  color: rgba(247, 242, 232, .78);
  font-size: var(--fs-xs);
  line-height: 1.55;
}

.pl-earn__ways {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
  margin: .9rem 0 0;
  padding: 0;
  list-style: none;
}

.pl-earn__ways li {
  display: inline-flex;
  align-items: center;
  gap: .38rem;
  padding: .3rem .68rem;
  border: 1.5px solid rgba(255, 219, 140, .3);
  border-radius: 999px;
  background: rgba(255, 255, 255, .06);
  font-size: var(--fs-2xs);
  font-weight: 700;
}

.pl-earn__ways i { color: #ffc94d; }

.pl-earn__stats {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem 1.6rem;
  margin-top: .95rem;
  padding-top: .85rem;
  border-top: 2px solid rgba(255, 219, 140, .2);
}

.pl-earn__stats > span { display: grid; gap: .15rem; }

.pl-earn__stats small {
  color: rgba(247, 242, 232, .62);
  font-family: "JetBrains Mono", monospace;
  font-size: .56rem;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
}

.pl-earn__stats b {
  display: inline-flex;
  align-items: center;
  gap: .32rem;
  color: #ffd96f;
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
}

/* ===== Daily reward ======================================================
   Free money once a day is the cheapest reason to open the app, so while it is
   unclaimed the card breathes and the gift wobbles, and the second it is taken
   both stop dead and the card goes calm green. */

.pl-daily {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 1.1rem;
  margin-bottom: 1.1rem;
  padding: 1.15rem 1.3rem;
  border: var(--pl-edge);
  border-radius: var(--pl-radius);
  background:
    radial-gradient(70% 190% at 2% 50%, color-mix(in srgb, var(--pl-ember) 34%, transparent), transparent 64%),
    var(--reset-panel-raised);
  box-shadow: var(--pl-lift);
}

.pl-daily[hidden] { display: none; }
.pl-daily:not(.is-claimed) { animation: pl-daily-glow 2.6s ease-in-out infinite; }

@keyframes pl-daily-glow {
  0%, 100% { box-shadow: var(--pl-lift), 0 0 0 0 rgba(255, 140, 40, 0); }
  50% { box-shadow: var(--pl-lift), 0 0 0 8px rgba(255, 140, 40, .16), 0 0 44px -12px rgba(255, 140, 40, .8); }
}

.pl-daily__gift {
  display: grid;
  place-items: center;
  width: 3.6rem;
  height: 3.6rem;
  border: 0;
  border-radius: var(--pl-radius-sm);
  background: var(--pl-ember-ramp);
  color: var(--pl-ember-ink);
  font-size: 1.45rem;
  box-shadow: var(--pl-lift-sm), 0 0 26px -6px color-mix(in srgb, var(--pl-ember) 80%, transparent);
  animation: pl-daily-beat 1.9s ease-in-out infinite;
}

@keyframes pl-daily-beat {
  0%, 100% { transform: translateY(0) rotate(-3deg); }
  50% { transform: translateY(-3px) rotate(3deg); }
}

.pl-daily__kicker { margin: 0 0 .15rem; color: var(--reset-ink-sun); }
.pl-daily h3 { margin: 0; font-size: var(--fs-md); text-transform: uppercase; }
.pl-daily p { margin: .2rem 0 0; color: var(--reset-muted); font-size: var(--fs-xs); }

.pl-daily.is-claimed {
  background:
    radial-gradient(70% 170% at 2% 50%, color-mix(in srgb, var(--reset-ok) 30%, transparent), transparent 62%),
    var(--reset-panel-raised);
}

.pl-daily.is-claimed .pl-daily__gift {
  background: var(--reset-ok);
  animation: none;
}

.pl-daily.is-claimed .pl-daily__kicker { color: var(--reset-ink-ok); }
.pl-daily.is-claimed .pl-token { filter: grayscale(.5) opacity(.7); }

/* ===== Price tags, payouts, deltas =======================================
   One pill, used everywhere a number of tokens appears. */

.pl-token {
  display: inline-flex;
  align-items: center;
  gap: .34rem;
  padding: .22rem .62rem .22rem .3rem;
  border: 2px solid var(--reset-cream);
  border-radius: 999px;
  background: linear-gradient(158deg, var(--pl-gold-hi) 0%, var(--pl-gold) 46%, var(--pl-gold-deep) 100%);
  color: var(--pl-gold-ink);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-xs);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  box-shadow: 2px 2px 0 var(--reset-shadow), inset 0 1px 0 rgba(255, 255, 255, .55);
}

.pl-token--lg {
  padding: .32rem .9rem .32rem .38rem;
  font-size: var(--fs-lg);
  box-shadow: 3px 3px 0 var(--reset-shadow), inset 0 1px 0 rgba(255, 255, 255, .55);
}

/* Ghost: the same shape without the metal, for the places where a filled tag
   beside a filled tag would be two competing prices. */
.pl-token--ghost {
  border-color: var(--reset-line-strong);
  background: var(--reset-panel);
  color: var(--reset-ink-sun);
  box-shadow: none;
}

/* ===== The buy bar =======================================================
   One control at the foot of an offer, carrying everything needed to decide:
   what it costs on the left, what pressing it does on the right. There is no
   balance caption — the balance is pinned in the header, on every screen — and
   no "no limit" line, because a limit that does not exist is not news. The bar
   is struck from the same gold as the coin, so the thing you press to spend
   money looks like money. */

.pl-store-shelf {
  display: grid;
  gap: .75rem;
  margin-top: 1.25rem;
}

.pl-store-shelf + .pl-store-shelf { margin-top: 1.75rem; }

.pl-store-shelf .pl-rack__head { margin-bottom: 0; }

/* Ряд тем стоит в той же сетке, что и заголовок с полкой, — свой отступ снизу
   ему здесь не нужен, иначе кейсы уезжают от собственной шапки. */
.pl-case-tabs { margin-bottom: 0; }

.pl-store-shelf .pl-rack__head > div { min-width: 0; }

.pl-store-shelf .pl-rack__head p {
  margin: .18rem 0 0;
  color: var(--reset-muted);
  font-size: var(--fs-xs);
}

.pl-offer { cursor: default; }

.pl-avatar-offer .pl-item__art { padding: .55rem; }

.pl-avatar-offer .pl-item__art .pl-art {
  width: 100%;
  height: 100%;
  border-radius: calc(var(--pl-radius-sm) - 2px);
}

.pl-avatar-offer .pl-art__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.pl-avatar-offer__style {
  max-width: 58%;
  overflow: hidden;
  padding: .18rem .42rem;
  border: 1px solid var(--reset-line-strong);
  border-radius: 999px;
  color: var(--reset-muted);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-3xs);
  font-weight: 700;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pl-avatar-offer.is-owned .pl-item__band { background: var(--reset-ok); }

/* The bar is pinned to the foot of the card so a row of offers lines up even
   when one description runs to two lines and the next to one. */
.pl-offer .pl-item__body {
  display: flex;
  flex: 1;
  flex-direction: column;
}

.pl-offer .pl-item__desc { margin-bottom: auto; }
.pl-offer .pl-case-contents-trigger { margin-top: auto; }

.pl-buy {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: .5rem;
  width: 100%;
  margin-top: .8rem;
  padding: .4rem .45rem;
  border: var(--pl-border);
  border-radius: 999px;
  background: linear-gradient(158deg, var(--pl-gold-hi) 0%, var(--pl-gold) 46%, var(--pl-gold-deep) 100%);
  color: var(--pl-gold-ink);
  font: inherit;
  cursor: pointer;
  box-shadow: var(--pl-lift-sm), inset 0 1px 0 rgba(255, 255, 255, .5);
  transition: transform .16s cubic-bezier(.2, .9, .3, 1.4), box-shadow .16s ease;
}

.pl-buy:hover:not(:disabled) {
  transform: translate(-1px, -1px);
  box-shadow: var(--pl-lift), inset 0 1px 0 rgba(255, 255, 255, .5);
}

.pl-buy:active:not(:disabled) {
  transform: translate(1px, 1px);
  box-shadow: 1px 1px 0 var(--reset-shadow), inset 0 1px 0 rgba(255, 255, 255, .5);
}

.pl-buy:focus-visible { outline: 3px solid var(--reset-ink-sun); outline-offset: 3px; }

/* The price sits in a struck-in well, so it reads as stamped into the bar
   rather than printed on it. */
.pl-buy__price {
  display: inline-flex;
  align-items: center;
  gap: .34rem;
  padding: .16rem .6rem .16rem .16rem;
  border-radius: 999px;
  background: rgba(59, 34, 0, .16);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-md);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.pl-buy__state {
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  gap: .38rem;
  padding-right: .5rem;
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-weight: 800;
  letter-spacing: .1em;
  text-transform: uppercase;
  white-space: nowrap;
}

.pl-buy__state b { font-weight: 800; letter-spacing: .04em; }

/* Free offers have no price to show, so the label takes the whole bar and the
   colour switches to the page's action coral — a free thing should not look
   like a thing that costs. */
.pl-buy--free {
  grid-template-columns: minmax(0, 1fr);
  padding: .55rem .8rem;
  background: var(--reset-coral);
  color: var(--reset-tile-ink);
}

.pl-buy--free .pl-buy__state {
  justify-content: center;
  padding-right: 0;
  font-size: var(--fs-xs);
}

/* ===== The stack stepper =================================================
   Buying ten cases used to be ten presses. The stepper is drawn only when a
   second copy is actually within reach, so a card the player can just afford
   still shows one plain price instead of a control that can only say one; and
   the cap beside it is the player's own cap — what this balance covers — not
   an abstract rule, so the count and the reason it stops are read together.
   The price in the bar below is the total, never the unit: the number you are
   about to spend is the number you press. */

.pl-buy-qty {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  margin-top: .8rem;
}

.pl-buy-qty__pill {
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  padding: .16rem;
  border: var(--pl-border);
  border-radius: 999px;
  background: var(--reset-panel);
  box-shadow: var(--pl-lift-sm);
}

.pl-buy-qty__step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.9rem;
  height: 1.9rem;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: var(--reset-control-surface);
  color: var(--reset-tile-ink);
  font: inherit;
  font-size: var(--fs-2xs);
  cursor: pointer;
  transition: transform .14s cubic-bezier(.2, .9, .3, 1.4), background-color .14s ease;
}

.pl-buy-qty__step:hover:not(:disabled) {
  transform: scale(1.09);
  background: var(--reset-coral);
}

.pl-buy-qty__step:active:not(:disabled) { transform: scale(.92); }

.pl-buy-qty__step:focus-visible {
  outline: 3px solid var(--reset-ink-sun);
  outline-offset: 2px;
}

.pl-buy-qty__step:disabled {
  opacity: .34;
  cursor: not-allowed;
}

.pl-buy-qty__value {
  min-width: 2.3rem;
  color: var(--reset-cream);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-sm);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

.pl-buy-qty__hint {
  color: var(--reset-muted);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  white-space: nowrap;
}

/* The bar is already spaced by the stepper above it. */
.pl-buy-qty + .pl-buy { margin-top: .45rem; }

.pl-buy:disabled {
  cursor: not-allowed;
  transform: none;
  filter: grayscale(.45) brightness(.96);
  box-shadow: 2px 2px 0 var(--reset-shadow), inset 0 1px 0 rgba(255, 255, 255, .35);
}

/* Out of reach, not broken. The gold drops away — nothing here is spendable —
   but the coin and the shape stay, so the card still reads "not yet" rather
   than "unavailable". */
.pl-item.is-unaffordable { border-color: var(--reset-ink-danger); }

.pl-item.is-unaffordable .pl-buy {
  border-color: var(--reset-ink-danger);
  background: color-mix(in srgb, var(--reset-rose) 12%, var(--reset-panel));
  color: var(--reset-ink-danger);
  filter: none;
}

.pl-item.is-unaffordable .pl-buy__price {
  background: color-mix(in srgb, var(--reset-rose) 20%, transparent);
  color: var(--reset-muted);
}

.pl-item.is-unaffordable .pl-buy__price .pl-coin { filter: grayscale(.5) brightness(.95); }

/* The scarcity sticker: only ever drawn for an offer that really is running
   out, so it can afford to shout. */
.pl-qty--stock {
  background: var(--reset-coral);
  font-weight: 800;
  letter-spacing: .04em;
  text-transform: uppercase;
  transform: rotate(-4deg);
}

.pl-buy[aria-busy="true"] {
  display: flex;
  justify-content: center;
  gap: .5rem;
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-xs);
  font-weight: 800;
}

.pl-buy[aria-busy="true"] i { animation: pl-spin .8s linear infinite; }
.pl-item__row .pl-btn[aria-busy="true"] i { animation: pl-spin .8s linear infinite; }

.pl-sell {
  display: grid;
  gap: .65rem;
  margin-top: 1rem;
  padding: .85rem;
  border: 2px solid var(--reset-line);
  border-radius: var(--pl-radius-sm);
  background: var(--reset-panel);
}

.pl-sell__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .7rem;
}

.pl-sell__head h3 { margin: 0; font-size: var(--fs-sm); }
.pl-sell__head p, .pl-sell > p { margin: 0; color: var(--reset-muted); font-size: var(--fs-xs); }
.pl-sell__controls { display: grid; grid-template-columns: minmax(5rem, .45fr) 1fr; gap: .6rem; }
.pl-sell__controls label { display: grid; gap: .25rem; color: var(--reset-muted); font-size: var(--fs-2xs); }
.pl-sell__quantity { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .35rem; }
.pl-sell__controls input {
  min-width: 0;
  padding: .55rem .65rem;
  border: 2px solid var(--reset-line-strong);
  border-radius: var(--pl-radius-sm);
  background: var(--reset-panel-raised);
  color: var(--reset-ink);
  font: inherit;
}

.pl-sell__max {
  min-width: 3.7rem;
  padding: .55rem .6rem;
  border: 2px solid var(--reset-line-strong);
  border-radius: var(--pl-radius-sm);
  background: color-mix(in srgb, var(--reset-sun) 18%, var(--reset-panel-raised));
  color: var(--reset-ink);
  font-family: var(--heading-font-family);
  font-size: var(--fs-2xs);
  font-weight: 900;
  letter-spacing: .04em;
  cursor: pointer;
  transition: border-color .15s ease, background-color .15s ease, transform .15s ease;
}

.pl-sell__max:hover {
  border-color: var(--reset-sun);
  background: color-mix(in srgb, var(--reset-sun) 28%, var(--reset-panel-raised));
}

.pl-sell__max:active { transform: translateY(1px); }
.pl-sell__max:focus-visible { outline: 3px solid var(--reset-sun); outline-offset: 2px; }

.pl-sell__payout {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  padding: .55rem .65rem;
  border: 2px solid var(--reset-line);
  border-radius: var(--pl-radius-sm);
  background: var(--reset-panel-raised);
  font-size: var(--fs-xs);
}

/* Spending is the moment the currency has to feel real, so the sheet is built
   around the coin: struck metal on top, the amount as a stamped tag, and the
   before/after of the player's own balance underneath — nobody should have to
   go and check what a purchase leaves them with. */
.pl-currency-confirm { max-width: 30rem; text-align: center; }

.pl-currency-confirm__coin {
  display: grid;
  place-items: center;
  width: 4.6rem;
  height: 4.6rem;
  margin: .2rem auto .55rem;
  border: 2px solid var(--reset-line-strong);
  border-radius: 50%;
  background: radial-gradient(circle at 50% 38%, rgba(255, 190, 60, .34), transparent 70%);
}

.pl-currency-confirm > .pl-hud { margin: 0; color: var(--reset-ink-sun); }
.pl-currency-confirm h2 { margin: .35rem 0 .5rem; font-size: var(--fs-lg); }

.pl-currency-confirm > p:not(.pl-hud) {
  max-width: 36ch;
  margin: 0 auto;
  color: var(--reset-muted);
  line-height: 1.55;
}

.pl-currency-confirm__amount { margin: 1.1rem 0 .8rem; }

.pl-currency-confirm__ledger {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: .6rem;
  margin: 0 0 1.1rem;
  padding: .65rem .85rem;
  border: 2px solid var(--reset-line);
  border-radius: var(--pl-radius-sm);
  background: var(--reset-panel);
}

.pl-currency-confirm__ledger span { display: grid; gap: .18rem; }

.pl-currency-confirm__ledger small {
  color: var(--reset-muted);
  font-family: "JetBrains Mono", monospace;
  font-size: .56rem;
  font-weight: 600;
  letter-spacing: .13em;
  text-transform: uppercase;
}

.pl-currency-confirm__ledger b {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .3rem;
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
}

.pl-currency-confirm__ledger > i { color: var(--reset-muted); }
.pl-currency-confirm__ledger b.is-after { color: var(--reset-ink-sun); }
.pl-currency-confirm__ledger b.is-short { color: var(--reset-ink-danger); }
.pl-currency-confirm__actions { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; }

/* The receipt is also the shortest route to using what was just bought. The
   item keeps the same rarity-lit art frame it had on the shelf, so the player
   can recognise the purchase before choosing the next action. */
.pl-purchase-success { width: min(31rem, 100%); text-align: center; }

.pl-purchase-success__check {
  display: grid;
  place-items: center;
  width: 3.7rem;
  height: 3.7rem;
  margin: .1rem auto .65rem;
  border: 2px solid color-mix(in srgb, var(--reset-ok) 70%, var(--reset-line-strong));
  border-radius: 50%;
  background: color-mix(in srgb, var(--reset-ok) 14%, var(--reset-panel));
  color: var(--reset-ok);
  font-size: 1.35rem;
  box-shadow: 0 0 1.5rem color-mix(in srgb, var(--reset-ok) 18%, transparent);
}

.pl-purchase-success > .pl-hud { margin: 0; color: var(--reset-ink-sun); }
.pl-purchase-success > h2 { margin: .35rem 2.5rem 1rem; font-size: var(--fs-lg); }

.pl-purchase-success__item {
  display: grid;
  grid-template-columns: 7.5rem minmax(0, 1fr);
  align-items: center;
  gap: 1rem;
  padding: .8rem;
  border: 2px solid color-mix(in srgb, var(--rarity, var(--reset-sun)) 48%, var(--reset-line));
  border-radius: var(--pl-radius-sm);
  background: color-mix(in srgb, var(--rarity, var(--reset-sun)) 7%, var(--reset-panel));
  text-align: left;
}

.pl-purchase-success__art {
  display: grid;
  place-items: center;
  width: 100%;
  height: 6.2rem;
  min-width: 0;
  padding: .45rem;
  overflow: hidden;
  border: 1.5px solid color-mix(in srgb, var(--rarity, var(--reset-sun)) 42%, var(--reset-line));
  border-radius: calc(var(--pl-radius-sm) - 2px);
  background: var(--reset-panel-raised);
}

.pl-purchase-success__art .pl-art { width: 100%; height: 100%; }
.pl-purchase-success__art .pl-art__img { object-fit: contain; }
.pl-purchase-success__copy { min-width: 0; }

.pl-purchase-success__copy .pl-item__kicker {
  display: block;
  margin-bottom: .3rem;
}

.pl-purchase-success__copy h3 {
  margin: 0;
  overflow-wrap: anywhere;
  font-size: var(--fs-md);
  line-height: 1.15;
}

.pl-purchase-success__qty {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  margin-top: .55rem;
  padding: .28rem .55rem;
  border: 1.5px solid var(--reset-line-strong);
  border-radius: 999px;
  color: var(--reset-muted);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-weight: 800;
  letter-spacing: .04em;
}

.pl-purchase-success > p:not(.pl-hud) {
  max-width: 40ch;
  margin: .9rem auto 0;
  color: var(--reset-muted);
  line-height: 1.5;
}

.pl-purchase-success__actions {
  display: grid;
  grid-template-columns: 1fr 1.35fr;
  gap: .6rem;
  margin-top: 1.1rem;
}

#pl-purchase-success-action[aria-busy="true"] i { animation: pl-spin .8s linear infinite; }

/* ===== Arsenal ===========================================================
   One slot per weapon type. It reads as a rack: the weapon name on top, what
   is worn in the middle, the tier and the change button underneath. Empty
   slots stay in the grid on purpose — seeing the gap is what makes a player
   want to fill it.

   Above the racks sit the drawer tabs, because the Unity catalogue is a
   hundred entries deep and no one scrolls that to change one gun. */

/* --- Drawer tabs -------------------------------------------------------- */

.pl-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin-bottom: 1rem;
}

.pl-tabs[hidden] { display: none; }

.pl-tab {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding: .62rem 1.15rem;
  border: var(--pl-border);
  border-radius: 999px;
  background: var(--reset-panel-raised);
  color: var(--reset-muted);
  font: inherit;
  font-family: var(--heading-font-family);
  font-size: var(--fs-sm);
  font-weight: 800;
  text-transform: uppercase;
  cursor: pointer;
  transition: transform .14s ease, box-shadow .14s ease, color .14s ease, background .14s ease;
}

.pl-tab:hover {
  color: var(--reset-cream);
  transform: translate(-1px, -1px);
  box-shadow: var(--pl-lift-sm);
}

/* The filled tile the rail uses for its active tab, borrowed so the three
   levels of navigation on this page speak the same language. */
.pl-tab.is-active {
  border-color: transparent;
  background: var(--pl-ember-ramp);
  color: var(--pl-ember-ink);
  box-shadow: var(--pl-lift-sm), var(--pl-glow-soft);
}

.pl-tab em {
  padding: 0 .4rem;
  border-radius: 999px;
  background: color-mix(in srgb, currentColor 16%, transparent);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-style: normal;
  font-weight: 700;
}

/* --- Arsenal filters ---------------------------------------------------- */

.pl-filters[hidden] { display: none; }

.pl-filters--arsenal {
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
}

.pl-filters--arsenal .pl-chips { grid-column: 1 / -1; }

.pl-switch {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding: .45rem .9rem;
  border: 2px solid var(--reset-line-strong);
  border-radius: 999px;
  background: var(--reset-panel-raised);
  color: var(--reset-muted);
  font-size: var(--fs-xs);
  font-weight: 700;
  white-space: nowrap;
  cursor: pointer;
  transition: border-color .14s ease, color .14s ease;
}

/* The native box is kept for the keyboard and the screen reader and hidden
   from sight; the drawn one is the `i` beside it. */
.pl-switch input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.pl-switch i {
  display: grid;
  place-items: center;
  width: 1.1rem;
  height: 1.1rem;
  border: 2px solid var(--reset-line-strong);
  border-radius: 5px;
  transition: background .14s ease, border-color .14s ease;
}

.pl-switch i::before {
  content: "\f00c";
  color: var(--reset-bg);
  font-family: "Font Awesome 6 Free";
  font-size: .55rem;
  font-weight: 900;
  opacity: 0;
}

.pl-switch:hover {
  border-color: var(--reset-cream);
  color: var(--reset-cream);
}

.pl-switch:has(input:checked) {
  border-color: var(--reset-cream);
  color: var(--reset-cream);
}

.pl-switch:has(input:checked) i {
  border-color: var(--reset-cream);
  background: var(--reset-cream);
}

.pl-switch:has(input:checked) i::before { opacity: 1; }

.pl-switch:has(input:focus-visible) {
  outline: 3px solid var(--reset-sun);
  outline-offset: 2px;
}

/* --- Racks -------------------------------------------------------------- */

.pl-rack + .pl-rack { margin-top: 1.6rem; }

.pl-rack__head {
  display: flex;
  align-items: center;
  gap: .6rem;
  margin: 0 0 .7rem;
}

.pl-rack__head h3 {
  margin: 0;
  font-size: var(--fs-base);
}

/* The hairline that turns a heading into a shelf label. */
.pl-rack__head::after {
  content: "";
  flex: 1;
  height: 2px;
  background: var(--reset-line);
}

.pl-rack__count {
  order: 3;
  padding: .1rem .5rem;
  border: 2px solid var(--reset-line-strong);
  border-radius: 999px;
  color: var(--reset-muted);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-weight: 700;
}

.pl-slots {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15.5rem, 1fr));
  gap: 1rem;
}

.pl-slot {
  display: flex;
  flex-direction: column;
  min-width: 0;
  overflow: hidden;
  border: var(--pl-border);
  border-radius: var(--pl-radius);
  background: var(--reset-panel-raised);
  box-shadow: var(--pl-lift);
}

.pl-slot__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  padding: .6rem .9rem;
  border-bottom: 2px solid var(--reset-line);
}

.pl-slot__art {
  position: relative;
  display: grid;
  place-items: center;
  height: 10.5rem;
  padding: 1.1rem;
  overflow: hidden;
  background:
    radial-gradient(circle at 50% 45%, color-mix(in srgb, var(--rarity) 40%, transparent), transparent 68%),
    repeating-linear-gradient(135deg,
      color-mix(in srgb, var(--rarity) 10%, transparent) 0 10px,
      transparent 10px 20px),
    var(--reset-panel);
}

.pl-slot__art i {
  color: color-mix(in srgb, var(--rarity) 70%, var(--reset-cream));
  font-size: 2.6rem;
}

.pl-slot.is-empty .pl-slot__art {
  background:
    repeating-linear-gradient(135deg,
      var(--reset-line) 0 10px,
      transparent 10px 20px),
    var(--reset-panel);
}

.pl-slot.is-empty .pl-slot__art i { color: var(--reset-line-strong); }

.pl-slot__body {
  display: grid;
  gap: .35rem;
  padding: .8rem .9rem 1rem;
}

/* Class and tier on one line: two facts a player scans, not two paragraphs
   they read. It wraps rather than truncates — "Пистолеты-пулемёты" beside a
   long tier name is the normal case, not the exception. */
.pl-slot__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .35rem .5rem;
  min-height: 1.4rem;
}

.pl-slot__class {
  color: var(--reset-muted);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
}

/* The foot of a slot with nothing to put on it. A disabled button in the place
   where every other card carries an ember CTA reads as a broken screen, so the
   dead pill is gone and what is left is the outline of one: a dashed plate the
   width of the card that opens the store, where cases come from. The gift mark
   is the only colour on it — the card must not compete with the slots the
   player can actually change. */
.pl-slot__get {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .45rem;
  width: 100%;
  min-width: 0;
  margin-top: .5rem;
  padding: .6rem .8rem;
  border: 2px dashed var(--reset-line-strong);
  border-radius: 999px;
  background: none;
  color: var(--reset-muted);
  font-family: var(--heading-font-family);
  font-size: var(--fs-xs);
  font-weight: 900;
  letter-spacing: .03em;
  line-height: 1.15;
  text-align: center;
  text-transform: uppercase;
  cursor: pointer;
  transition: color .15s ease, border-color .15s ease, background-color .15s ease,
    transform .15s ease;
}

.pl-slot__get i {
  color: var(--reset-ink-sun);
  font-size: 1em;
}

.pl-slot__get:hover,
.pl-slot__get:focus-visible {
  border-style: solid;
  border-color: color-mix(in srgb, var(--pl-ember) 65%, transparent);
  background: color-mix(in srgb, var(--pl-ember) 12%, transparent);
  color: var(--reset-cream);
}

.pl-slot__get:hover { transform: translate(-1px, -1px); }

.pl-slot__get:active { transform: translate(1px, 1px); }

/* The picker sheet: every owned skin for one weapon, plus the default look. */
.pl-picker {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr));
  gap: .6rem;
  margin-top: 1rem;
}

/* The option and its magnifier sit side by side in the cell rather than one
   inside the other: both are buttons, and the browser is owed a tree it can
   make sense of. The cell is what the pin is pinned to. */
.pl-picker__cell {
  position: relative;
  display: grid;
}

.pl-picker__option {
  display: grid;
  justify-items: center;
  gap: .35rem;
  padding: .6rem;
  border: 2px solid var(--reset-line-strong);
  border-radius: var(--pl-radius-sm);
  background: var(--reset-panel);
  color: inherit;
  font: inherit;
  text-align: center;
  cursor: pointer;
  transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
}

.pl-picker__option b {
  font-size: var(--fs-xs);
  line-height: 1.2;
}

.pl-picker__option:hover:not(:disabled) {
  border-color: var(--reset-cream);
  box-shadow: var(--pl-lift-sm);
  transform: translate(-1px, -1px);
}

.pl-picker__option.is-current {
  border-color: var(--reset-cream);
  background: var(--reset-panel-raised);
  box-shadow: var(--pl-lift-sm);
}

.pl-picker__art {
  position: relative;
  display: grid;
  place-items: center;
  width: 100%;
  height: 5.75rem;
  overflow: hidden;
  border-radius: var(--pl-radius-sm);
  background: radial-gradient(circle at 50% 45%, color-mix(in srgb, var(--rarity) 45%, transparent), transparent 70%);
}

/* The tiles letterbox the same square canvas the stage does, and a skin the
   player cannot make out is not a choice. They are pressed to be equipped, not
   studied, so the overrun here is gentler than on the stage — a tile is barely
   wider than it is tall and has less transparent margin to spend. */
.pl-picker__art .pl-art__img { scale: 1.2; }

.pl-picker__art i {
  color: color-mix(in srgb, var(--rarity) 70%, var(--reset-cream));
  font-size: 1.8rem;
}

/* Waiting for a skin to go on. The tile that was tapped keeps its colour and
   spins over its own render while the rest of the grid steps back, so the
   sheet says both which choice is being made and that it has not stopped. */
.pl-picker__option:disabled { cursor: default; }

.pl-picker__option:disabled:not([aria-busy="true"]) { opacity: .4; }

.pl-picker__option[aria-busy="true"] {
  border-color: color-mix(in srgb, var(--rarity, var(--reset-line-strong)) 70%, var(--reset-cream));
  cursor: progress;
}

.pl-picker__busy {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background: color-mix(in srgb, var(--reset-panel) 82%, transparent);
}

.pl-picker__busy::after {
  content: "";
  width: 1.7rem;
  height: 1.7rem;
  border: 3px solid color-mix(in srgb, var(--rarity, var(--reset-line-strong)) 26%, transparent);
  border-top-color: color-mix(in srgb, var(--rarity, var(--reset-line-strong)) 90%, var(--reset-cream));
  border-radius: 50%;
  animation: pl-spin .85s linear infinite;
}

.pl-inspect__equip { margin-top: 1rem; }

.pl-inspect__equip[aria-busy="true"] {
  opacity: 1;
  cursor: progress;
}

.pl-inspect__equip[aria-busy="true"] i { animation: pl-spin .8s linear infinite; }

/* What is on the weapon right now, said in words above the grid — the ring on
   the current tile is easy to miss on a phone. */
.pl-inspect__note {
  margin: .35rem 0 0;
  color: var(--reset-muted);
  font-size: var(--fs-sm);
}

/* ===== Case cards ========================================================
   Same skeleton as an item, plus the drop preview: a row of overlapping
   rarity discs so you can see what is inside before you spend the click. */

.pl-case-grid { grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); }

.pl-case .pl-item__art {
  height: 11rem;
  background:
    radial-gradient(circle at 50% 45%, color-mix(in srgb, var(--reset-sun) 40%, transparent), transparent 70%),
    repeating-linear-gradient(45deg,
      color-mix(in srgb, var(--reset-coral) 10%, transparent) 0 12px,
      transparent 12px 24px),
    var(--reset-panel);
}

/* A case that can still be opened breathes, so the tab never looks static.
   The whole frame floats rather than the picture inside it, so the brackets
   do not stay behind — which is also why a case tile does not wear them. */
.pl-case .pl-item__art .pl-art { animation: pl-float 3.4s ease-in-out infinite; }
.pl-case .pl-item__art .pl-art::before { display: none; }

@keyframes pl-float {
  0%, 100% { transform: translateY(0) rotate(-1deg); }
  50% { transform: translateY(-7px) rotate(1.5deg); }
}

/* Case names run to one line or to three, and the cards stand shoulder to
   shoulder, so the actions are pinned to the foot of the card the way a store
   card pins its buy bar. Otherwise every card starts its buttons at a
   different height and the shelf reads as ragged. */
.pl-case .pl-item__body {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: .3rem;
}

.pl-case .pl-drops { margin-bottom: auto; }

.pl-drops {
  display: flex;
  align-items: center;
  margin-top: .2rem;
}

.pl-drops span {
  display: grid;
  place-items: center;
  width: 1.7rem;
  height: 1.7rem;
  margin-left: -.45rem;
  border: 2px solid var(--reset-cream);
  border-radius: 50%;
  background: var(--rarity);
  color: var(--reset-tile-ink);
  font-size: .6rem;
}

.pl-drops span:first-child { margin-left: 0; }

.pl-drops em {
  margin-left: .5rem;
  color: var(--reset-muted);
  font-size: var(--fs-2xs);
  font-style: normal;
  font-weight: 700;
}

/* A case card has two decisions: inspect the pool, then open or buy. The
   contents trigger is a quiet full-width row — on the shelf and in the store
   alike — so it remains visible even when the primary button is disabled,
   without competing with it and without halving the width either one gets. */
.pl-case-contents-trigger {
  display: flex;
  align-items: center;
  gap: .5rem;
  width: 100%;
  min-width: 0;
  min-height: 2.6rem;
  margin-top: .75rem;
  padding: .5rem .65rem;
  border: 2px solid var(--reset-line-strong);
  border-radius: var(--pl-radius-sm);
  background: var(--reset-panel);
  color: var(--reset-cream);
  font: inherit;
  text-align: left;
  cursor: pointer;
  box-shadow: 2px 2px 0 var(--reset-shadow);
  transition: border-color .15s ease, background-color .15s ease, transform .15s ease, box-shadow .15s ease;
}

.pl-case-contents-trigger > span {
  display: inline-flex;
  flex: 1 1 auto;
  align-items: center;
  gap: .42rem;
  min-width: 0;
  font-family: var(--heading-font-family);
  font-size: var(--fs-xs);
  font-weight: 850;
  line-height: 1.15;
  text-transform: uppercase;
}

.pl-case-contents-trigger > b {
  display: inline-grid;
  flex: 0 0 auto;
  min-width: 1.65rem;
  min-height: 1.65rem;
  place-items: center;
  padding: 0 .35rem;
  border: 2px solid var(--reset-line-strong);
  border-radius: 999px;
  background: var(--reset-surface);
  color: var(--reset-cream);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-variant-numeric: tabular-nums;
}

.pl-case-contents-trigger > i:last-child {
  flex: 0 0 auto;
  color: var(--reset-muted);
  font-size: var(--fs-2xs);
}

.pl-case-contents-trigger:hover {
  border-color: var(--reset-sky);
  background: color-mix(in srgb, var(--reset-sky) 9%, var(--reset-panel));
  box-shadow: var(--pl-lift-sm);
  transform: translate(-1px, -1px);
}

.pl-case-contents-trigger:active {
  box-shadow: 1px 1px 0 var(--reset-shadow);
  transform: translate(1px, 1px);
}

.pl-case-contents-trigger:focus-visible {
  outline: 3px solid var(--reset-sun);
  outline-offset: 2px;
}

/* The skin count under an arsenal rack. The store used to carry one of these
   too; its cooldown and stock now live on the buy bar and the art instead. */
.pl-limit {
  color: var(--reset-muted);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-weight: 600;
}

/* ===== Case shelf ========================================================
   Cases sit at the top of the inventory instead of on a tab of their own, so
   the shelf has to say where it ends: a recessed panel with its own head, one
   step behind the cards standing on it. With nothing to open it collapses to a
   dashed single line — an empty tab was invisible, an empty shelf is a
   sentence about where cases come from. */

.pl-shelf {
  margin-bottom: 1.6rem;
  padding: .9rem 1rem 1.1rem;
  border: var(--pl-border);
  border-radius: var(--pl-radius);
  background: var(--reset-panel);
}

.pl-shelf__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem .8rem;
  margin-bottom: .9rem;
}

.pl-shelf__icon {
  display: grid;
  flex: 0 0 auto;
  place-items: center;
  width: 2.6rem;
  height: 2.6rem;
  border: var(--pl-border);
  border-radius: var(--pl-radius-sm);
  background: var(--reset-sun);
  color: var(--reset-tile-ink);
  box-shadow: var(--pl-lift-sm);
}

.pl-shelf__text {
  flex: 1 1 12rem;
  min-width: 0;
}

.pl-shelf__text h2 { margin: 0; font-size: var(--fs-md); }

.pl-shelf__text p {
  max-width: 58ch;
  margin: .2rem 0 0;
  color: var(--reset-muted);
  font-size: var(--fs-xs);
}

.pl-shelf__cta { flex: 0 0 auto; }

/* Nothing to open. The shelf keeps its head — it is where the store button
   lives — and gives up everything else. */
.pl-shelf.is-empty {
  padding: 0;
  border: 0;
  background: transparent;
}

.pl-shelf.is-empty .pl-shelf__head { margin-bottom: .6rem; }

.pl-shelf.is-empty .pl-shelf__text p { display: none; }
.pl-shelf.is-empty .pl-case-grid { display: block; }

/* An empty shelf must not cost a phone screen of height, so its head gives up
   the width the store button needs and stays on one row. */
.pl-shelf.is-empty .pl-shelf__text { flex: 1 1 7rem; }

.pl-shelf__empty {
  display: flex;
  align-items: center;
  gap: .7rem;
  padding: .7rem .85rem;
  border: 2px dashed var(--reset-line-strong);
  border-radius: var(--pl-radius-sm);
  color: var(--reset-muted);
  font-size: var(--fs-xs);
}

.pl-shelf__empty i {
  flex: 0 0 auto;
  color: var(--reset-line-strong);
  font-size: 1.15rem;
}

.pl-shelf__empty p { margin: 0; }

/* ===== History =========================================================== */

.pl-history {
  display: grid;
  overflow: hidden;
  border: var(--pl-border);
  border-radius: var(--pl-radius);
  background: var(--reset-panel-raised);
  box-shadow: var(--pl-lift);
}

.pl-history__row {
  display: grid;
  grid-template-columns: 2.6rem minmax(0, 1fr) auto;
  align-items: center;
  gap: .9rem;
  padding: .85rem 1rem;
  border-bottom: 2px solid var(--reset-line);
}

.pl-history__row:last-child { border-bottom: 0; }

.pl-history__icon {
  display: grid;
  place-items: center;
  width: 2.6rem;
  height: 2.6rem;
  border: 1.5px solid var(--reset-cream);
  border-radius: var(--pl-radius-sm);
  background: var(--reset-sun);
  color: var(--reset-tile-ink);
}

.pl-history__row.is-minus .pl-history__icon { background: var(--reset-rose); }
.pl-history__row.is-case .pl-history__icon { background: var(--reset-sky); }
.pl-history__row.is-currency .pl-history__icon {
  background: linear-gradient(155deg, var(--pl-gold-hi), var(--pl-gold) 48%, var(--pl-gold-deep));
  color: var(--pl-gold-ink);
}

.pl-history__row.is-currency.is-minus .pl-history__icon {
  background: var(--reset-coral);
  color: var(--reset-tile-ink);
}

/* Money moving is the one row in this list that gets a stamped chip rather
   than a bare number — a ledger of tokens should look like receipts. */
.pl-history__row.is-currency .pl-history__delta {
  display: inline-flex;
  align-items: center;
  gap: .34rem;
  padding: .24rem .62rem .24rem .34rem;
  border: 2px solid var(--reset-cream);
  border-radius: 999px;
  color: var(--reset-tile-ink);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  box-shadow: 2px 2px 0 var(--reset-shadow);
}

.pl-history__row.is-currency .pl-history__delta.is-plus { background: var(--reset-ok); }
.pl-history__row.is-currency .pl-history__delta.is-minus { background: var(--reset-coral); }

.pl-history__row h3 {
  margin: 0;
  font-size: var(--fs-sm);
}

.pl-history__row p {
  margin: .15rem 0 0;
  color: var(--reset-muted);
  font-size: var(--fs-xs);
}

.pl-history__row time {
  color: var(--reset-muted);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
}

.pl-history__delta {
  font-family: var(--heading-font-family);
  font-size: var(--fs-md);
  font-weight: 800;
}

.pl-history__delta.is-plus { color: var(--reset-ink-ok); }
.pl-history__delta.is-minus { color: var(--reset-ink-danger); }

/* The ledger frame around that list. The head carries the filters so they are
   read before the rows, and the expander sits under the list so the panel ends
   on an edge rather than on a fade. */

.pl-ledger__head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: .7rem;
  margin-bottom: .9rem;
}

.pl-ledger__head h2 { margin: 0 0 .2rem; }

.pl-ledger__head p {
  max-width: 56ch;
  margin: 0;
  color: var(--reset-muted);
  font-size: var(--fs-sm);
}

.pl-ledger__filters { flex: 0 0 auto; }

.pl-chip__count {
  padding: 0 .35rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--reset-line-strong) 45%, transparent);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-style: normal;
}

.pl-chip.is-active .pl-chip__count { background: color-mix(in srgb, var(--reset-bg) 25%, transparent); }

.pl-chip:disabled { opacity: .45; cursor: not-allowed; }

.pl-chip:disabled:hover {
  border-color: var(--reset-line-strong);
  color: var(--reset-muted);
  transform: none;
  box-shadow: none;
}

.pl-ledger__more {
  display: flex;
  justify-content: center;
  margin-top: .8rem;
}

/* ===== Match feed ========================================================
   Same frame as the inventory history, but keyed by outcome: the badge on the
   left is the first thing scanned, then the K/D/A, then what it paid. */

.pl-match {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: .9rem;
  padding: .8rem 1rem;
  border-bottom: 2px solid var(--reset-line);
}

.pl-match:last-child { border-bottom: 0; }

.pl-match__result {
  padding: .25rem .7rem;
  border: 2px solid var(--reset-cream);
  border-radius: 999px;
  color: var(--reset-tile-ink);
  font-size: var(--fs-2xs);
  font-weight: 800;
  text-transform: uppercase;
}

.pl-match.is-win .pl-match__result { background: var(--reset-ok); }
.pl-match.is-loss .pl-match__result { background: var(--reset-danger); }
.pl-match.is-draw .pl-match__result { background: var(--reset-line-strong); }

.pl-match h3 {
  margin: 0;
  font-size: var(--fs-sm);
}

.pl-match time {
  color: var(--reset-muted);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
}

.pl-match__kda {
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-sm);
  font-weight: 700;
  white-space: nowrap;
}

.pl-match__kda i {
  color: var(--reset-muted);
  font-style: normal;
  padding: 0 .15rem;
}

.pl-match__xp {
  color: var(--reset-ink-sun);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-xs);
  font-weight: 700;
  white-space: nowrap;
}

/* ===== Profile =========================================================== */

.pl-profile {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
  gap: .9rem;
  align-items: start;
}

.pl-panel--wide { grid-column: 1 / -1; }

/* The promo ticket is a row of the profile grid now, so it drops the store's
   bottom margin and takes the raised surface of the panels it sits between —
   the ticket keeps its own shape, not its own colour. */
.pl-profile .pl-code {
  grid-column: 1 / -1;
  margin-bottom: 0;
  background: var(--reset-panel-raised);
}

.pl-profile__identity {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: .85rem;
  margin-bottom: .7rem;
}

.pl-profile__identity h2 { margin-bottom: .15rem; }

.pl-profile__identity p {
  margin: 0;
  overflow-wrap: anywhere;
  color: var(--reset-muted);
  font-size: var(--fs-xs);
}

.pl-btn--avatar { margin-top: .55rem; }

.pl-profile-collection {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: .6rem;
  margin-top: 1.2rem;
}

.pl-panel {
  padding: 1.5rem 1.6rem;
  border: var(--pl-border);
  border-radius: var(--pl-radius);
  background: var(--reset-panel-raised);
  box-shadow: var(--pl-lift);
}

.pl-panel h2 {
  margin: 0 0 .3rem;
  font-size: var(--fs-md);
}

.pl-panel > p {
  margin: 0;
  color: var(--reset-muted);
  font-size: var(--fs-sm);
}

.pl-panel__actions {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin-top: 1.2rem;
}

.pl-profile-stats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: .6rem;
  margin-top: 1rem;
}

.pl-security__heading {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  padding-bottom: 1rem;
  border-bottom: 2px solid var(--reset-line);
}

.pl-security__heading h2 { margin-top: .2rem; }

.pl-security__heading p:not(.pl-hud) {
  max-width: 48rem;
  margin: 0;
  color: var(--reset-muted);
  font-size: var(--fs-sm);
}

.pl-security__shield,
.pl-security__icon {
  display: grid;
  flex: 0 0 auto;
  place-items: center;
  border: 2px solid var(--reset-line-strong);
  background: var(--reset-panel);
}

.pl-security__shield {
  width: 3rem;
  height: 3rem;
  border-radius: 50%;
  color: var(--reset-coral);
  font-size: var(--fs-md);
}

.pl-security__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: .7rem;
  margin-top: 1rem;
}

.pl-security__item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: .75rem;
  min-width: 0;
  padding: 1rem;
  border: 2px solid var(--reset-line);
  border-radius: var(--pl-radius-sm);
  background: var(--reset-panel);
}

.pl-security__item > div { min-width: 0; }

.pl-security__item h3 {
  margin: 0 0 .2rem;
  font-family: var(--heading-font-family);
  font-size: var(--fs-sm);
  line-height: 1.25;
}

.pl-security__item p {
  margin: 0;
  color: var(--reset-muted);
  font-size: var(--fs-xs);
  line-height: 1.45;
}

.pl-security__icon {
  width: 2.35rem;
  height: 2.35rem;
  border-radius: .75rem;
  color: var(--reset-coral);
}

.pl-security__item > .pl-btn,
.pl-security__item > .pl-security__actions {
  grid-column: 2;
  justify-self: start;
  margin-top: .2rem;
}

.pl-security__actions {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
}

.pl-security__item.is-enabled {
  border-color: color-mix(in srgb, var(--reset-ok) 72%, var(--reset-line-strong));
}

.pl-security__item.is-enabled .pl-security__icon { color: var(--reset-ok); }

.pl-security__item--danger {
  border-color: color-mix(in srgb, var(--reset-danger) 58%, var(--reset-line));
}

.pl-security__item--danger .pl-security__icon { color: var(--reset-danger); }

.pl-mini {
  display: grid;
  align-content: start;
  gap: .15rem;
  padding: .8rem;
  border: 2px solid var(--reset-line);
  border-radius: var(--pl-radius-sm);
  background: var(--reset-panel);
}

.pl-mini b {
  font-family: var(--heading-font-family);
  font-size: var(--fs-xl);
  font-weight: 800;
  line-height: 1;
}

.pl-mini span {
  color: var(--reset-muted);
  font-size: var(--fs-2xs);
  font-weight: 700;
}

/* The best pull gets the full row and a stripe in its own rarity. */
.pl-mini--wide {
  grid-column: 1 / -1;
  gap: .4rem;
  border-left: 8px solid var(--rarity);
}

/* Label above, figure below, spanning the row: for the totals that are a
   sentence rather than a single number (14 / 6 / 3, "2 ч 30 мин"). */
.pl-mini--row {
  grid-column: 1 / -1;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: .8rem;
}

.pl-mini--row b { font-size: var(--fs-md); }
.pl-mini--row span { font-size: var(--fs-xs); }

.pl-mini--wide b { font-size: var(--fs-md); }

/* ===== States ============================================================ */

.pl-empty {
  grid-column: 1 / -1;
  padding: 3rem 1.5rem;
  border: 2px dashed var(--reset-line-strong);
  border-radius: var(--pl-radius);
  color: var(--reset-muted);
  text-align: center;
}

.pl-empty p { margin: 0; }

.pl-empty .pl-btn { margin-top: 1rem; }

.pl-empty i {
  display: block;
  margin-bottom: .7rem;
  color: var(--reset-line-strong);
  font-size: 2.2rem;
}

/* ===== Boot screen =======================================================
   A curtain over the whole cabinet until the dashboard has been fetched and
   painted. It is drawn in the page's own parts — the dot grid, the ink
   outline, the hard shadow, the XP meter's stripe — so the screen the player
   waits on and the screen they land on are obviously the same product. */

.pl-boot {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: grid;
  place-items: center;
  padding: 1.5rem;
  background-color: var(--reset-bg);
  /* The curtain lights its own centre with the cabinet's ember, so the first
     frame a player sees is already the arcade rather than a blank sheet with a
     logo on it. */
  background-image:
    radial-gradient(60rem 40rem at 50% 42%, color-mix(in srgb, var(--pl-ember) 16%, transparent), transparent 70%),
    radial-gradient(var(--reset-dot) 1.5px, transparent 1.5px);
  background-size: auto, 22px 22px;
  transition: opacity .4s ease, visibility .4s;
}

.pl-boot[hidden] { display: none; }

.pl-boot.is-done {
  opacity: 0;
  visibility: hidden;
}

/* The card shrinks a hair as it goes, so the curtain reads as being taken
   away rather than as the page having flickered. */
.pl-boot__card { transition: transform .4s cubic-bezier(.2, .8, .2, 1); }
.pl-boot.is-done .pl-boot__card { transform: scale(.96); }

.pl-boot__card {
  display: grid;
  justify-items: center;
  width: min(23rem, 100%);
  padding: 2rem 1.6rem 1.6rem;
  border: var(--pl-border);
  border-radius: var(--pl-radius);
  background: var(--reset-panel-raised);
  box-shadow: var(--pl-lift-hi);
  text-align: center;
}

.pl-boot__mark {
  display: grid;
  place-items: center;
  width: 4rem;
  height: 4rem;
  margin-bottom: 1rem;
  overflow: hidden;
  border: var(--pl-border);
  border-radius: 50%;
  /* minilogo.png is a white glyph on transparent — same dark disc the rail
     brand and the site header give it. */
  background: var(--reset-logo-disc);
  box-shadow: var(--pl-lift-sm);
  animation: pl-boot-beat 1.9s ease-in-out infinite;
}

.pl-boot__mark img {
  width: 62%;
  height: auto;
}

/* A slow breath rather than a spinner: the bar underneath already says the
   work is moving, and two things spinning at once reads as a stall. */
@keyframes pl-boot-beat {
  0%, 100% { transform: none; }
  50% { transform: scale(1.06); }
}

.pl-boot__eyebrow {
  margin: 0;
  color: var(--reset-ink-coral);
}

.pl-boot__title {
  margin: .2rem 0 0;
  font-family: var(--heading-font-family);
  font-size: var(--fs-lg);
  font-weight: 800;
  letter-spacing: -.01em;
}

.pl-boot__meter {
  width: 100%;
  height: 12px;
  margin-top: 1.2rem;
  overflow: hidden;
  border: 1.5px solid var(--reset-cream);
  border-radius: 999px;
  background: var(--reset-panel);
}

.pl-boot__meter span {
  display: block;
  width: 10%;
  height: 100%;
  border-radius: 999px;
  /* The XP meter's stripe, so the first bar a player sees is the same bar
     they will be watching fill for the rest of the session. */
  background: repeating-linear-gradient(
    135deg,
    var(--pl-ember) 0 8px,
    var(--pl-flare) 8px 16px);
  box-shadow: 0 0 16px -2px color-mix(in srgb, var(--pl-ember) 85%, transparent);
  transition: width .5s cubic-bezier(.2, .8, .2, 1);
}

.pl-boot__status {
  min-height: 1.3em;
  margin: .7rem 0 0;
  color: var(--reset-muted);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: .1em;
}

/* The cabinet never loaded. The curtain stays up and becomes the answer —
   revealing an empty inventory behind an error banner helps nobody. */
.pl-boot.is-error .pl-boot__mark { animation: none; }
.pl-boot.is-error .pl-boot__meter span {
  width: 100% !important;
  background: var(--reset-danger);
}

.pl-boot.is-error .pl-boot__status {
  color: var(--reset-ink-danger);
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: 700;
  letter-spacing: 0;
}

.pl-boot__error { margin-top: 1rem; }
.pl-boot__error[hidden] { display: none; }

@keyframes pl-spin { to { transform: rotate(360deg); } }

.pl-alert {
  display: none;
  margin-top: 1rem;
  padding: .8rem 1rem;
  border: var(--pl-border);
  border-radius: var(--pl-radius-sm);
  background: var(--reset-danger);
  color: var(--reset-tile-ink);
  font-weight: 700;
  box-shadow: var(--pl-lift-sm);
}

.pl-alert.is-visible { display: block; }

/* A short-lived banner for the good news — a claim landing, a case opened. */
.pl-toast {
  position: fixed;
  right: 1.2rem;
  bottom: 1.2rem;
  z-index: 40;
  display: flex;
  align-items: center;
  gap: .55rem;
  padding: .7rem 1.1rem;
  border: var(--pl-border);
  border-radius: 999px;
  background: var(--reset-ok);
  color: var(--reset-tile-ink);
  font-weight: 800;
  box-shadow: var(--pl-lift);
  animation: pl-toast-in .3s cubic-bezier(.2, .9, .3, 1.4);
}

.pl-toast[hidden] { display: none; }

/* Token news gets its own colour. A payout that pops up in the same green as
   "settings saved" is a payout nobody feels. */
.pl-toast--gold {
  padding-left: .45rem;
  background: linear-gradient(158deg, var(--pl-gold-hi) 0%, var(--pl-gold) 46%, var(--pl-gold-deep) 100%);
  color: var(--pl-gold-ink);
}

.pl-toast--gold span {
  font-family: "JetBrains Mono", monospace;
  font-variant-numeric: tabular-nums;
}

.pl-toast--spend { background: var(--reset-coral); color: var(--reset-tile-ink); }

@keyframes pl-toast-in {
  from { opacity: 0; transform: translateY(1.2rem) scale(.9); }
  to { opacity: 1; transform: none; }
}

/* ===== Modals ============================================================
   The profile portrait picker, a quiet inspect sheet, and the case theatre. */

.pl-modal {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  place-items: center;
  padding: 1rem;
  background: color-mix(in srgb, var(--reset-shadow) 72%, transparent);
  backdrop-filter: blur(6px);
}

.pl-modal[hidden] { display: none; }

.pl-modal__card {
  position: relative;
  width: min(34rem, 100%);
  min-width: 0;
  max-height: 90vh;
  overflow-x: hidden;
  overflow-y: auto;
  padding: 1.4rem;
  border: var(--pl-border);
  border-radius: var(--pl-radius);
  background: var(--reset-panel-raised);
  box-shadow: var(--pl-lift-hi);
  animation: pl-pop .22s cubic-bezier(.2, .9, .3, 1.3);
}

@keyframes pl-pop {
  from { opacity: 0; transform: scale(.94) translateY(10px); }
  to { opacity: 1; transform: none; }
}

/* The close button is the first child of the card and everything under it is
   positioned, so without a layer of its own the sheet's own artwork paints
   straight over it — the cross went missing and the corner stopped taking
   clicks. It sits above the content, not in the same pile as it. */
.pl-modal__close {
  position: absolute;
  z-index: 5;
  top: .8rem;
  right: .8rem;
  /* It lands on artwork rather than on a panel, so it carries its own ground
     and the cross reads on a bright render as well as on an empty mount. */
  background: color-mix(in srgb, var(--reset-panel-raised) 92%, transparent);
  backdrop-filter: blur(4px);
}

/* Пока открыт хоть один всплывающий слой, страница под ним не листается:
   колесо и палец на фоне уводили игрока с того места, куда он вернётся,
   закрыв окно. Замок вешает сторож в player.js — по одному классу на оба
   элемента, и это не перестраховка. Ядро держит на html `overflow-x: clip`,
   а значит прокруткой окна распоряжается html, а не body: сам по себе
   `overflow` на body туда больше не всплывает, и запертым он был только на
   бумаге. */
html.pl-modal-open,
body.pl-modal-open { overflow: hidden; }

/* Ширину ушедшей полосы прокрутки меряет player.js — scrollbar-gutter здесь
   не помогает: overflow корня уходит во вьюпорт, а gutter вместе с ним не
   едет, и место под полосу так и остаётся незарезервированным. Отступ внутри
   body, а не снаружи: фон кабинета остаётся во всю ширину окна. */
body.pl-modal-open { padding-right: var(--pl-lock-gap, 0px); }

/* Докрутив список внутри окна до конца, дальше игрок толкал страницу за ним. */
.pl-modal,
.pl-modal__card { overscroll-behavior: contain; }

/* The inspect sheet scrolls its body rather than the whole card. An absolutely
   positioned child of a scrolling box travels with the content, so on a short
   screen the cross used to slide up out of the sheet the moment a player
   scrolled down to the sell panel. */
.pl-inspect {
  display: grid;
  grid-template-rows: minmax(0, 1fr);
  overflow: hidden;
}

/* Wider than the item sheet: this one is a catalogue of renders, and the
   width is what the pictures are made of. */
.pl-inspect--case { width: min(64rem, 100%); }

#pl-inspect-body {
  min-height: 0;
  overflow-x: hidden;
  overflow-y: auto;
  scrollbar-gutter: stable;
}

.pl-security-modal { z-index: 50; }

.pl-security-dialog { width: min(31rem, 100%); }

.pl-security-dialog h2 {
  max-width: calc(100% - 3rem);
  margin: .3rem 0 .45rem;
  font-size: var(--fs-lg);
  line-height: 1.08;
}

.pl-security-dialog > p:not(.pl-hud, .pl-security-feedback, .pl-security-secret-label) {
  margin: 0;
  color: var(--reset-muted);
  font-size: var(--fs-sm);
  line-height: 1.5;
}

.pl-security-dialog--danger {
  border-color: color-mix(in srgb, var(--reset-danger) 72%, var(--reset-line-strong));
}

.pl-security-form {
  display: grid;
  gap: .85rem;
  margin-top: 1.1rem;
}

.pl-security-form label {
  display: grid;
  gap: .35rem;
  min-width: 0;
  color: var(--reset-cream);
  font-size: var(--fs-xs);
  font-weight: 800;
}

.pl-security-form input {
  width: 100%;
  min-width: 0;
  min-height: 2.8rem;
  padding: .65rem .75rem;
  border: 2px solid var(--reset-line-strong);
  border-radius: var(--pl-radius-sm);
  outline: none;
  background: var(--reset-panel);
  color: var(--reset-cream);
  font: inherit;
  box-shadow: inset 0 0 0 1px transparent;
}

.pl-security-form input:focus-visible {
  border-color: var(--reset-coral);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--reset-coral) 24%, transparent);
}

.pl-security-form small {
  color: var(--reset-muted);
  font-size: var(--fs-2xs);
  font-weight: 600;
  line-height: 1.4;
}

.pl-security-dialog__actions {
  display: flex;
  flex-wrap: wrap;
  gap: .55rem;
  margin-top: 1.15rem;
}

.pl-security-feedback {
  margin: .9rem 0 0;
  padding: .65rem .75rem;
  border: 2px solid var(--reset-danger);
  border-radius: var(--pl-radius-sm);
  background: color-mix(in srgb, var(--reset-danger) 14%, var(--reset-panel));
  color: var(--reset-cream);
  font-size: var(--fs-xs);
  font-weight: 750;
  line-height: 1.4;
}

.pl-security-feedback[data-tone="success"] {
  border-color: var(--reset-ok);
  background: color-mix(in srgb, var(--reset-ok) 14%, var(--reset-panel));
}

.pl-security-feedback[hidden] { display: none; }

.pl-security-qr {
  display: grid;
  width: min(13.5rem, 100%);
  min-height: 13.5rem;
  place-items: center;
  margin: 1.15rem auto .75rem;
  padding: .55rem;
  border: 2px solid var(--reset-line-strong);
  border-radius: var(--pl-radius-sm);
  background: #fff;
  color: #241d14;
  font-size: var(--fs-xl);
  text-align: center;
}

.pl-security-qr img {
  display: block;
  width: 100%;
  height: auto;
}

.pl-security-qr p {
  margin: 0;
  color: #6b1d1d;
  font-size: var(--fs-xs);
  font-weight: 800;
}

.pl-security-secret-label {
  margin: 0 0 .35rem;
  color: var(--reset-muted);
  font-size: var(--fs-xs);
  text-align: center;
}

.pl-security-secret {
  display: block;
  overflow-wrap: anywhere;
  padding: .6rem;
  border: 2px solid var(--reset-line);
  border-radius: var(--pl-radius-sm);
  background: var(--reset-panel);
  color: var(--reset-cream);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-xs);
  letter-spacing: .08em;
  text-align: center;
  user-select: all;
}

#pl-security-mfa-code,
#pl-security-mfa-disable-code {
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-md);
  letter-spacing: .22em;
  text-align: center;
}

#pl-security-delete-confirm { text-transform: uppercase; }

@media (max-width: 820px) {
  .pl-security__grid { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 520px) {
  .pl-security__heading { align-items: center; }
  .pl-security__shield { width: 2.5rem; height: 2.5rem; }
  .pl-security__item { padding: .85rem; }
  .pl-security__item > .pl-btn,
  .pl-security__item > .pl-security__actions { grid-column: 1 / -1; }
  .pl-security__item > .pl-btn { width: 100%; }
  .pl-security__actions { width: 100%; }
  .pl-security__actions .pl-btn { flex: 1 1 auto; }
  .pl-security-dialog__actions .pl-btn { flex: 1 1 100%; }
}

/* The headset handoff remains a separate no-storage document for security,
   while the player experiences it as one modal inside the cabinet. */
.pl-player-link-modal {
  z-index: 45;
  padding: clamp(.45rem, 2vw, 1rem);
}

.pl-player-link-dialog {
  /* One task — read six digits off a headset and type them — so the dialog is
     sized like the login card it is, not like a full page. `--pl-link-height`
     is the frame's own measured content height, posted in by the card; until
     it lands the fallback holds a sensible first paint, and either way the
     viewport clamp wins, so the frame can never push the dialog off screen. */
  width: min(37rem, 100%);
  height: min(var(--pl-link-height, 34rem), calc(100dvh - 2rem));
  max-height: calc(100dvh - 2rem);
  overflow: hidden;
  padding: 0;
  /* The frame paints the whole card, so the dialog only supplies the outline
     and the ground it sits on — both from the page tokens, in either theme. */
  background: var(--reset-panel-raised);
  transition: height .18s ease;
}

.pl-player-link-dialog iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
  background: var(--reset-panel-raised);
}

.pl-player-link-close {
  /* Above the frame, and drawn like every other control on the page. */
  z-index: 2;
}

@media (max-width: 620px) {
  .pl-player-link-modal { padding: 0; }
  .pl-player-link-dialog {
    width: 100%;
    height: 100dvh;
    max-height: none;
    border: 0;
    border-radius: 0;
    transition: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .pl-player-link-dialog { transition: none; }
}

/* Character select, not a settings field. The portrait about to be worn is
   shown at the size the art was painted for, the roster scrolls beside it,
   and the tiles are pure faces — the name rides the bottom of each one on a
   gradient rather than stealing a row of grid height from the picture. */
.pl-avatar-picker {
  display: flex;
  flex-direction: column;
  width: min(64rem, calc(100vw - 2rem));
  max-width: 100%;
  max-height: min(90vh, 44rem);
  /* The card scrolls nothing itself: the roster is the only scroller, so the
     preview and the Save bar stay put while thirty faces go past. */
  overflow: hidden;
}

.pl-avatar-picker > .pl-hud {
  margin: 0 3rem .15rem 0;
  color: var(--reset-ink-coral);
}

.pl-avatar-picker > h2 {
  margin: 0 3rem .25rem 0;
  font-size: var(--fs-lg);
}

.pl-avatar-picker > p {
  margin: 0;
  color: var(--reset-muted);
  font-size: var(--fs-sm);
}

.pl-avatar-picker__layout {
  display: grid;
  grid-template-columns: minmax(0, 14rem) minmax(0, 1fr);
  gap: 1.1rem;
  min-height: 0;
  margin-top: 1.1rem;
}

/* ----- The preview ------------------------------------------------------ */

.pl-avatar-preview {
  display: grid;
  align-content: start;
  gap: .65rem;
  min-width: 0;
  margin: 0;
}

.pl-avatar-preview__plate {
  position: relative;
  display: grid;
  place-items: center;
  width: 100%;
  aspect-ratio: 1;
  overflow: hidden;
  border: var(--pl-border);
  border-radius: var(--pl-radius);
  background: var(--pl-plate-glow);
  box-shadow: var(--pl-lift);
}

.pl-avatar-preview__plate img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  animation: pl-portrait-in .25s ease;
}

@keyframes pl-portrait-in {
  from { opacity: 0; transform: scale(1.04); }
  to { opacity: 1; transform: none; }
}

.pl-avatar-preview__caption {
  display: grid;
  gap: .2rem;
  justify-items: center;
  text-align: center;
}

.pl-avatar-preview__caption b {
  font-family: var(--heading-font-family);
  font-size: var(--fs-md);
  font-weight: 800;
  line-height: 1.15;
}

.pl-avatar-preview__note {
  color: var(--reset-muted);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
}

/* ----- The roster ------------------------------------------------------- */

.pl-avatar-roster {
  min-width: 0;
  min-height: 0;
  overflow-y: auto;
  padding: .25rem .4rem .25rem .25rem;
  border: 2px solid var(--reset-line);
  border-radius: var(--pl-radius-sm);
  background: var(--reset-panel);
  scrollbar-width: thin;
}

.pl-avatar-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(6rem, 1fr));
  gap: .45rem;
  margin: 0;
}

.pl-avatar-option {
  position: relative;
  display: block;
  aspect-ratio: 1;
  min-width: 0;
  padding: 0;
  overflow: hidden;
  border: 2px solid var(--reset-line-strong);
  border-radius: var(--pl-radius-sm);
  background: var(--pl-plate);
  color: var(--reset-on-image);
  font: inherit;
  cursor: pointer;
  transition: border-color .16s ease, box-shadow .16s ease, transform .16s ease;
}

.pl-avatar-option:hover {
  border-color: var(--reset-sky);
  transform: translateY(-2px);
}

.pl-avatar-option:focus-visible {
  outline: 3px solid var(--reset-sun);
  outline-offset: 2px;
}

.pl-avatar-option.is-selected {
  border-color: var(--reset-sun);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--reset-sun) 45%, transparent);
  transform: translateY(-2px);
}

.pl-avatar-option img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform .28s cubic-bezier(.2, .8, .2, 1);
}

.pl-avatar-option:hover img,
.pl-avatar-option.is-selected img { transform: scale(1.06); }

.pl-avatar-option__name {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  overflow: hidden;
  padding: 1.1rem .35rem .3rem;
  background: linear-gradient(to top, color-mix(in srgb, #06070a 92%, transparent), transparent);
  color: var(--reset-on-image);
  font-size: .68rem;
  font-weight: 800;
  line-height: 1.2;
  text-align: center;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pl-avatar-option__check {
  position: absolute;
  top: .25rem;
  right: .25rem;
  display: grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  border: 2px solid var(--reset-cream);
  border-radius: 50%;
  background: var(--reset-sun);
  color: var(--reset-tile-ink);
  font-size: .68rem;
  opacity: 0;
  transform: scale(.7);
  transition: opacity .16s ease, transform .16s ease;
}

.pl-avatar-option.is-selected .pl-avatar-option__check {
  opacity: 1;
  transform: scale(1);
}

/* The one already saved keeps a quiet marker, so "what I wear now" and "what
   I am about to wear" are never the same signal. */
.pl-avatar-option.is-current::before {
  content: "";
  position: absolute;
  top: .3rem;
  left: .3rem;
  z-index: 1;
  width: .55rem;
  height: .55rem;
  border: 2px solid var(--reset-cream);
  border-radius: 50%;
  background: var(--reset-ok);
}

.pl-avatar-picker__actions {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: flex-end;
  gap: .8rem;
  margin: 1.1rem -1.4rem -1.4rem;
  padding: .8rem 1.4rem 1.1rem;
  border-top: var(--pl-border);
  background: var(--reset-panel-raised);
}

.pl-avatar-picker__status {
  min-width: 0;
  margin-right: auto;
  color: var(--reset-muted);
  font-size: var(--fs-xs);
  font-weight: 700;
}

#pl-avatar-save[aria-busy="true"] i { animation: pl-spin .8s linear infinite; }

/* The stage. The sheet is where a player looks at one skin properly, so the
   art gets a lit case rather than a padded box: light dropped from above in
   the rarity colour, the tier's own hatching behind it, a pedestal shadow
   under the render, and a hard edge that clips everything — a wide render can
   no longer end up lying across its own name. */
.pl-inspect__art {
  position: relative;
  display: grid;
  place-items: center;
  height: 17.5rem;
  margin-bottom: 1.1rem;
  padding: .85rem;
  overflow: hidden;
  border: var(--pl-border);
  border-radius: var(--pl-radius-sm);
  background:
    radial-gradient(120% 88% at 50% 2%, color-mix(in srgb, var(--rarity) 42%, transparent), transparent 72%),
    repeating-linear-gradient(135deg,
      color-mix(in srgb, var(--rarity) 9%, transparent) 0 12px,
      transparent 12px 24px),
    var(--reset-panel);
  box-shadow: inset 0 0 2.6rem color-mix(in srgb, var(--rarity) 18%, transparent);
}

/* Nothing is standing on the plate in the picker's "default look" state, so
   the light goes out and the case reads as an empty mount. */
.pl-inspect__art.is-empty {
  background:
    repeating-linear-gradient(135deg, var(--reset-line) 0 12px, transparent 12px 24px),
    var(--reset-panel);
  box-shadow: none;
}

.pl-inspect__art::after {
  content: "";
  position: absolute;
  bottom: .85rem;
  left: 50%;
  width: 56%;
  height: .75rem;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(closest-side,
    color-mix(in srgb, var(--rarity) 52%, transparent), transparent);
  filter: blur(2px);
  pointer-events: none;
}

.pl-inspect__art.is-empty::after { display: none; }

/* The render stands on the pedestal, not under it. */
.pl-inspect__art .pl-art { z-index: 1; }

/* Renders arrive on a square canvas and a weapon is drawn as a thin band
   across it, so fitting that canvas into a stage nearly twice as wide as it is
   tall fits the empty margins: the gun came out a third of the width it had
   room for, adrift in its own case. The render is allowed to overrun its
   canvas instead — the stage clips, and what leaves the frame is transparent
   margin rather than the weapon. The factor stays below the stage's own aspect
   ratio, so nothing can ever be cut off at the sides, and a subject filling up
   to two thirds of the canvas height still clears the top and bottom. */
.pl-inspect__art .pl-art__img { scale: 1.5; }

/* The top tiers turn the case slowly, the same halo the grid gives them. */
.pl-inspect__art.is-elite::before {
  content: "";
  position: absolute;
  inset: -30%;
  background: conic-gradient(from 0deg,
    transparent 0 68%,
    color-mix(in srgb, var(--rarity) 40%, transparent) 84%,
    transparent 100%);
  animation: pl-sheen 5s linear infinite;
  pointer-events: none;
}

.pl-inspect__art i {
  color: color-mix(in srgb, var(--rarity) 75%, var(--reset-cream));
  font-size: 4rem;
}

.pl-inspect h2 {
  margin: .2rem 0 .5rem;
  font-size: var(--fs-lg);
}

/* The case sheet is a catalogue rather than a text drop table. The case keeps
   a compact identity plate at the top and the reward art gets the available
   room below it — this is the view players came here for. */
.pl-case-contents { min-width: 0; }

.pl-case-contents__head {
  display: grid;
  grid-template-columns: 8rem minmax(0, 1fr);
  align-items: center;
  gap: 1rem;
  padding-right: 2.8rem;
}

.pl-case-contents__case-art {
  position: relative;
  display: grid;
  place-items: center;
  height: 7rem;
  padding: .65rem;
  overflow: hidden;
  border: 2px solid var(--reset-line);
  border-radius: var(--pl-radius-sm);
  background:
    radial-gradient(circle at 50% 44%, color-mix(in srgb, var(--reset-sun) 38%, transparent), transparent 70%),
    repeating-linear-gradient(45deg,
      color-mix(in srgb, var(--reset-coral) 9%, transparent) 0 10px,
      transparent 10px 20px),
    var(--reset-panel);
}

.pl-case-contents__case-art i {
  color: color-mix(in srgb, var(--reset-sun) 78%, var(--reset-cream));
  font-size: 2.6rem;
}

.pl-case-contents__title { min-width: 0; }

.pl-case-contents__title h2 {
  max-width: 100%;
  margin: .2rem 0 .3rem;
  overflow-wrap: anywhere;
}

.pl-case-contents__title p {
  margin: 0;
  color: var(--reset-muted);
  font-size: var(--fs-xs);
  line-height: 1.4;
}

.pl-case-contents__title .pl-case-contents__odds {
  display: flex;
  align-items: center;
  gap: .4rem;
  margin-top: .3rem;
  color: var(--reset-muted);
}

.pl-case-contents__odds i { color: var(--reset-sky); }

/* Four across on the full sheet, not five: a skin the player cannot make
   out is not a drop table entry, it is a grey smudge. Every extra column
   comes straight out of the render. */
.pl-case-rewards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  gap: .75rem;
  margin: 1rem 0 0;
  padding: 0;
  list-style: none;
}

.pl-case-reward-item {
  display: flex;
  min-width: 0;
}

.pl-case-reward {
  display: grid;
  grid-template-rows: auto 5px minmax(0, 1fr);
  width: 100%;
  min-width: 0;
  padding: 0;
  overflow: hidden;
  border: 2px solid var(--reset-line-strong);
  border-radius: var(--pl-radius-sm);
  background: var(--reset-panel);
  color: var(--reset-cream);
  font: inherit;
  text-align: left;
  appearance: none;
  cursor: zoom-in;
  transition: border-color .16s ease, box-shadow .16s ease, transform .16s ease;
}

.pl-case-reward:hover {
  border-color: var(--rarity);
  box-shadow: var(--pl-lift-sm);
  transform: translateY(-2px);
}

.pl-case-reward:active { transform: translateY(0); }

.pl-case-reward:focus-visible {
  outline: 3px solid var(--reset-sun);
  outline-offset: 2px;
}

.pl-case-reward__art {
  position: relative;
  display: grid;
  min-width: 0;
  min-height: 0;
  /* Sized by the column rather than by a fixed height, so the plate grows
     with the sheet and every card in a row still lines up. */
  aspect-ratio: 5 / 4;
  place-items: center;
  overflow: hidden;
  background:
    radial-gradient(circle at 50% 48%, color-mix(in srgb, var(--rarity) 34%, transparent), transparent 72%),
    repeating-linear-gradient(135deg,
      color-mix(in srgb, var(--rarity) 8%, transparent) 0 9px,
      transparent 9px 18px),
    var(--reset-panel-raised);
}

/* Renders arrive on a square canvas with the weapon drawn as a band across
   the middle of it — the catalogue's own measurements put the gun at about
   nine tenths of the canvas wide and four tenths of it tall. Fitting that
   canvas into the plate therefore fits the empty margins, and the gun came
   out a third of the width it had room for. The render overruns its canvas
   instead and the plate clips. The factor is paired with the plate's own
   proportions, and that pair is what decides the size: at the limit the gun
   comes out as wide as the card whatever ratio is chosen, so the ratio only
   buys back the clipping. At 5:4 and 1.25 even the widest render in the
   catalogue — 98% of its canvas — still clears the sides, and only the few
   drawn taller than four fifths of their canvas lose anything: a trim off the
   top and bottom of an upright piece, never a barrel. */
.pl-case-reward__art .pl-art__img { scale: 1.25; }

.pl-case-reward__art .pl-art {
  transition: transform .2s cubic-bezier(.2, .8, .2, 1);
}

.pl-case-reward:is(:hover, :focus-visible) .pl-case-reward__art .pl-art {
  transform: scale(1.045);
}

.pl-case-reward__expand {
  position: absolute;
  z-index: 2;
  top: .45rem;
  right: .45rem;
  display: grid;
  width: 2rem;
  height: 2rem;
  place-items: center;
  border: 2px solid var(--reset-line-strong);
  border-radius: 50%;
  background: color-mix(in srgb, var(--reset-panel-raised) 88%, transparent);
  color: var(--reset-cream);
  font-size: .85rem;
  box-shadow: 2px 2px 0 var(--reset-shadow);
  opacity: .82;
  transition: background-color .16s ease, opacity .16s ease, transform .16s ease;
}

/* The same trap the tick on the inspect plate fell into: the plate sets a size
   and a rarity colour for the fallback glyph of a missing render, and `i` on
   its own is loose enough to catch the badge in the corner too. The expand
   arrows came out as tall as the circle holding them and painted in the rarity
   rather than the badge ink, so the corner read as two shapes stacked on each
   other. The extra class is what puts this above `.pl-case-reward__art i`. */
.pl-case-reward__expand i.fas {
  color: inherit;
  font-size: 1em;
}

.pl-case-reward:is(:hover, :focus-visible) .pl-case-reward__expand {
  background: var(--rarity);
  color: var(--reset-tile-ink);
  opacity: 1;
  transform: scale(1.06);
}

.pl-case-reward__art i {
  color: color-mix(in srgb, var(--rarity) 74%, var(--reset-cream));
  font-size: 2rem;
}

.pl-case-reward__band {
  border-top: 1px solid var(--reset-cream);
  border-bottom: 1px solid var(--reset-cream);
  background: var(--rarity);
}

.pl-case-reward__body {
  display: grid;
  align-content: start;
  gap: .22rem;
  min-width: 0;
  padding: .6rem .7rem .75rem;
}

.pl-case-reward__rarity {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: color-mix(in srgb, var(--rarity) 72%, var(--reset-cream));
  font-family: "JetBrains Mono", monospace;
  font-size: .62rem;
  font-weight: 750;
  letter-spacing: .09em;
  line-height: 1.2;
  text-transform: uppercase;
}

.pl-case-reward__chance {
  position: absolute;
  z-index: 2;
  top: .45rem;
  left: .45rem;
  padding: .14rem .42rem;
  border: 2px solid color-mix(in srgb, var(--rarity) 62%, var(--reset-line-strong));
  border-radius: 999px;
  background: color-mix(in srgb, var(--reset-panel) 88%, transparent);
  color: var(--reset-cream);
  font-family: "JetBrains Mono", monospace;
  font-size: .62rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  line-height: 1.25;
  backdrop-filter: blur(3px);
}

.pl-case-reward__body > b {
  overflow-wrap: anywhere;
  font-family: var(--heading-font-family);
  font-size: var(--fs-xs);
  font-weight: 850;
  line-height: 1.2;
}

.pl-case-reward__body > span:last-child {
  color: var(--reset-muted);
  font-size: var(--fs-2xs);
  line-height: 1.3;
}

.pl-case-rewards__empty {
  display: grid;
  grid-column: 1 / -1;
  min-height: 9rem;
  place-items: center;
  gap: .5rem;
  padding: 1rem;
  border: 2px dashed var(--reset-line-strong);
  border-radius: var(--pl-radius-sm);
  color: var(--reset-muted);
  text-align: center;
}

.pl-case-rewards__empty i { color: var(--reset-sun); font-size: 2rem; }

@media (prefers-reduced-motion: reduce) {
  .pl-case-reward,
  .pl-case-reward__art .pl-art,
  .pl-case-reward__expand { transition: none; }
}

/* ===== Zoom stage ========================================================
   The sheet says what a skin is; this says how it looks. Always a dark room
   in both themes — the same move the case theatre makes, and for the same
   reason: a render is lit by the page around it, and a white page lights it
   badly. The core tokens are re-pointed rather than a second palette being
   hard-coded, so the buttons and chips inside keep working untouched. */

.pl-zoom {
  --reset-cream: #f7f2e8;
  --reset-panel: #14161c;
  --reset-panel-raised: #1c1f27;
  --reset-surface: #22262f;
  --reset-muted: #a8a094;
  --reset-line: rgba(247, 242, 232, .16);
  --reset-line-strong: rgba(247, 242, 232, .38);
  --reset-shadow: #000;
  --rarity: var(--reset-line-strong);
  z-index: 40;
  padding: 0;
  background:
    radial-gradient(120% 80% at 50% 0%, color-mix(in srgb, var(--rarity) 12%, transparent), transparent 68%),
    rgba(5, 6, 10, .96);
}

.pl-zoom__card {
  position: relative;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto auto;
  gap: .55rem;
  width: 100%;
  height: 100%;
  min-height: 0;
  padding: clamp(.8rem, 2.2vw, 1.5rem);
}

.pl-zoom__close {
  position: absolute;
  z-index: 5;
  top: clamp(1.3rem, 2.9vw, 2.1rem);
  right: clamp(1.3rem, 2.9vw, 2.1rem);
}

/* The room. Hatching and a drop of rarity light behind the render, and a hard
   edge so a picture dragged sideways stops at the wall instead of crossing
   the caption. */
.pl-zoom__stage {
  position: relative;
  min-height: 0;
  overflow: hidden;
  border: 2px solid var(--reset-line);
  border-radius: var(--pl-radius);
  background:
    /* Vignette first: the corners fall away so the eye is walked to the middle
       of the room, where the render is standing. */
    radial-gradient(130% 100% at 50% 50%, transparent 34%, rgba(0, 0, 0, .58) 100%),
    radial-gradient(76% 58% at 50% 0%, color-mix(in srgb, var(--rarity) 20%, transparent), transparent 72%),
    repeating-linear-gradient(135deg,
      color-mix(in srgb, var(--rarity) 5%, transparent) 0 16px,
      transparent 16px 32px),
    #0a0c11;
  box-shadow: inset 0 0 4rem color-mix(in srgb, var(--rarity) 14%, transparent);
  /* Every gesture on the stage is ours: the browser must not pan the page or
     run its own pinch zoom underneath the one being driven here. По той же
     причине снимается всё, что браузер предлагает поверх картинки сам —
     выделение, перетаскивание файла и меню по долгому нажатию: любое из них
     перехватывает палец, которым игрок двигает скин. */
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  cursor: zoom-in;
  animation: pl-pop .22s cubic-bezier(.2, .9, .3, 1.3);
}

.pl-zoom__stage.is-zoomed { cursor: grab; }
.pl-zoom__stage.is-panning { cursor: grabbing; }

/* The pedestal shadow the inspect sheet gives its plate, kept here so the
   render still stands on something at 1×. */
.pl-zoom__stage::after {
  content: "";
  position: absolute;
  bottom: 6%;
  left: 50%;
  width: 46%;
  height: 1rem;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(closest-side,
    color-mix(in srgb, var(--rarity) 46%, transparent), transparent);
  filter: blur(3px);
  opacity: 1;
  transition: opacity .18s ease;
  pointer-events: none;
}

/* Once the render is walked into, the pedestal is behind the player rather
   than under the picture — a glow parked mid-image reads as a smudge. */
.pl-zoom__stage.is-zoomed::after { opacity: 0; }

.pl-zoom__plate {
  position: absolute;
  inset: clamp(1rem, 3.5vw, 2.6rem);
  transform: translate(var(--pan-x, 0px), var(--pan-y, 0px)) scale(var(--zoom, 1));
  transform-origin: 50% 50%;
  transition: transform .18s cubic-bezier(.2, .8, .2, 1);
  will-change: transform;
}

/* A dragged picture must not lag behind the finger, so the easing is dropped
   for the duration of the drag. */
.pl-zoom__stage.is-panning .pl-zoom__plate { transition: none; }

/* Nothing is fitted into a tile here — the render is the whole point, so it
   gets the room and a deeper shadow than the grid gives it. */
.pl-zoom__plate .pl-art .pl-art__img {
  filter: drop-shadow(0 1.4rem 1.8rem rgba(0, 0, 0, .55));
  -webkit-user-drag: none;
}

.pl-zoom__plate i.fas {
  color: color-mix(in srgb, var(--rarity) 70%, var(--reset-cream));
  font-size: clamp(4rem, 16vw, 9rem);
}

.pl-zoom__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: .5rem 1rem;
}

.pl-zoom__meta {
  display: grid;
  gap: .15rem;
  min-width: 0;
}

.pl-zoom__meta h2 {
  margin: 0;
  color: var(--reset-cream);
  font-size: var(--fs-lg);
  line-height: 1.1;
}

.pl-zoom__meta .pl-tier { justify-self: start; margin-top: .25rem; }

.pl-zoom__controls {
  display: flex;
  align-items: center;
  gap: .4rem;
}

.pl-zoom__level {
  min-width: 3.6rem;
  color: var(--reset-cream);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-xs);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

.pl-zoom__hint {
  margin: 0;
  color: var(--reset-muted);
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: .04em;
}

.pl-inspect__meta {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin-top: 1rem;
}

.pl-inspect__meta span {
  padding: .3rem .7rem;
  border: 1.5px solid var(--reset-line-strong);
  border-radius: 999px;
  font-size: var(--fs-xs);
  font-weight: 700;
}

/* ===== Install guide =====================================================
   The fallback for every browser that will not let a page ask to be installed
   — Safari on iOS above all, where the Share sheet is the only route. Three
   device tabs and one numbered list at a time: a player holding a phone should
   never have to read the steps for the other two. */

.pl-install { width: min(33rem, 100%); }

.pl-install h2 {
  max-width: calc(100% - 3rem);
  margin: .3rem 0 .45rem;
  font-size: var(--fs-lg);
}

.pl-install__lead {
  margin: 0;
  color: var(--reset-muted);
  font-size: var(--fs-sm);
}

.pl-install__perks {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  gap: .5rem;
  margin: 1rem 0 0;
  padding: 0;
  list-style: none;
}

.pl-install__perks li {
  display: grid;
  justify-items: center;
  gap: .35rem;
  padding: .65rem .5rem;
  border: 2px solid var(--reset-line);
  border-radius: var(--pl-radius-sm);
  background: var(--reset-panel);
  color: var(--reset-muted);
  font-size: var(--fs-2xs);
  font-weight: 700;
  text-align: center;
}

.pl-install__perks i { color: var(--reset-ink-coral); font-size: 1.05rem; }

.pl-install__tabs { margin-top: 1.1rem; }

.pl-install__steps {
  display: grid;
  gap: .55rem;
  margin: .9rem 0 0;
  padding-left: 1.25rem;
  color: var(--reset-ink);
  font-size: var(--fs-sm);
  line-height: 1.45;
}

.pl-install__steps[hidden] { display: none; }
.pl-install__steps li { padding-left: .2rem; }
.pl-install__steps li::marker { color: var(--reset-ink-coral); font-weight: 800; }

.pl-install__actions {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin-top: 1.2rem;
}

/* ===== Case theatre ======================================================
   Always dark, in both themes: the opening is the one moment the page stops
   being a cabinet and becomes a stage, and a spotlight needs a dark room.
   It re-points the core tokens rather than hard-coding a second palette, so
   every component inside keeps working — the same trick core plays with its
   photo bands. */

.pl-open {
  --reset-cream: #f7f2e8;
  --reset-panel: #14161c;
  --reset-panel-raised: #1c1f27;
  --reset-surface: #22262f;
  --reset-muted: #a8a094;
  --reset-line: rgba(247, 242, 232, .16);
  --reset-line-strong: rgba(247, 242, 232, .38);
  --reset-shadow: #000;
  /* --rarity is the case colour up front and the prize colour at the end.
     --live is the colour of whatever tile is under the marker right now, which
     the script repaints as the strip travels — that moving colour is the whole
     tell, so the room, the frame and the halo all read from it. --pl-heat
     climbs from 0 to 1 as the reel decelerates and drives every "it is about
     to land" cue at once. */
  --rarity: var(--reset-sun);
  --live: var(--reset-line-strong);
  --pl-heat: 0;
  position: fixed;
  inset: 0;
  z-index: 50;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  place-items: center;
  padding: 1rem;
  overflow: hidden;
  background: #05060a;
  color: var(--reset-cream);
}

.pl-open[hidden] { display: none; }

/* --- The room ----------------------------------------------------------
   A single flat radial wash was the old background and it read as a loading
   screen. This is a room instead: three drifting light orbs for depth, a
   perspective floor running toward the horizon, and a vignette to keep the eye
   pinned on the marker. All of it is painted in the rarity hue, so a gold case
   and a blue one do not share a backdrop. */

.pl-open__sky {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  background:
    radial-gradient(circle at 50% 38%, color-mix(in srgb, var(--rarity) 16%, transparent), transparent 58%),
    radial-gradient(circle at 50% 42%, #1b1f2b, #070911 62%, #04050a 100%);
  pointer-events: none;
}

.pl-open__orb {
  position: absolute;
  border-radius: 50%;
  filter: blur(70px);
  mix-blend-mode: screen;
  opacity: .45;
}

.pl-open__orb--a {
  top: -14%;
  left: -6%;
  width: 46vmax;
  height: 46vmax;
  background: radial-gradient(circle, color-mix(in srgb, var(--rarity) 85%, transparent), transparent 68%);
  animation: pl-orb-a 19s ease-in-out infinite alternate;
}

.pl-open__orb--b {
  right: -12%;
  bottom: -18%;
  width: 52vmax;
  height: 52vmax;
  background: radial-gradient(circle, rgba(255, 138, 46, .8), transparent 68%);
  animation: pl-orb-b 23s ease-in-out infinite alternate;
}

.pl-open__orb--c {
  top: 26%;
  right: 14%;
  width: 30vmax;
  height: 30vmax;
  background: radial-gradient(circle, rgba(74, 168, 214, .75), transparent 68%);
  animation: pl-orb-c 17s ease-in-out infinite alternate;
}

@keyframes pl-orb-a {
  to { transform: translate3d(14vmax, 8vmax, 0) scale(1.18); }
}

@keyframes pl-orb-b {
  to { transform: translate3d(-16vmax, -10vmax, 0) scale(1.12); }
}

@keyframes pl-orb-c {
  to { transform: translate3d(-10vmax, 12vmax, 0) scale(.86); }
}

/* The floor: laid flat, tipped away from the camera and scrolling toward the
   viewer — the same trick an arcade attract screen uses to say "this is a
   place, and it is moving". */
.pl-open__grid {
  position: absolute;
  bottom: 0;
  left: 50%;
  width: 240vmax;
  height: 62vmax;
  background-image:
    repeating-linear-gradient(90deg, color-mix(in srgb, var(--rarity) 55%, transparent) 0 2px, transparent 2px 8rem),
    repeating-linear-gradient(0deg, color-mix(in srgb, var(--rarity) 40%, transparent) 0 2px, transparent 2px 8rem);
  opacity: .5;
  transform: translateX(-50%) perspective(36rem) rotateX(66deg);
  transform-origin: 50% 100%;
  mask-image: linear-gradient(0deg, #000, transparent 62%);
  animation: pl-floor 4.5s linear infinite;
}

@keyframes pl-floor {
  to { background-position: 0 0, 0 8rem; }
}

.pl-open__vignette {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at 50% 44%, transparent 30%, rgba(2, 3, 6, .74) 86%),
    repeating-linear-gradient(0deg, rgba(255, 255, 255, .03) 0 1px, transparent 1px 3px);
}

/* Rays behind the stage: two counter-rotating fans rather than one, so the
   pattern never visibly repeats. Heat drives their brightness but deliberately
   not their speed — changing animation-duration mid-flight keeps the elapsed
   time and recomputes the progress, which shows up as the fan jumping a few
   degrees. The wind-up is carried by everything else instead. */
.pl-open__rays {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 1;
  width: 160vmax;
  height: 160vmax;
  background: conic-gradient(
    from 0deg,
    color-mix(in srgb, var(--rarity) 32%, transparent) 0deg 6deg,
    transparent 6deg 22deg);
  opacity: calc(.16 + var(--pl-heat) * .26);
  transform: translate(-50%, -50%);
  animation: pl-spin 26s linear infinite;
  pointer-events: none;
}

.pl-open__rays::after {
  content: "";
  position: absolute;
  inset: 0;
  background: conic-gradient(
    from 12deg,
    color-mix(in srgb, var(--rarity) 22%, transparent) 0deg 3deg,
    transparent 3deg 31deg);
  animation: pl-spin 41s linear infinite reverse;
}

.pl-open[data-stage="reveal"] .pl-open__rays {
  opacity: .38;
  animation-duration: 9s;
}

/* Edge glow. Dark at rest; by the final tile the whole frame breathes in the
   colour of whatever is about to stop under the marker. */
.pl-open::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 6;
  box-shadow: inset 0 0 13rem color-mix(in srgb, var(--live) 60%, transparent);
  opacity: var(--pl-heat);
  transition: opacity .3s ease;
  pointer-events: none;
}

/* Dust drifting up through the room. Cheap, and the stage stops looking like a
   screenshot the moment something in the air moves. */
.pl-open__dust {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  pointer-events: none;
}

.pl-open__dust span {
  position: absolute;
  bottom: -5%;
  width: var(--pl-size, 4px);
  height: var(--pl-size, 4px);
  border-radius: 50%;
  background: color-mix(in srgb, var(--rarity) 70%, #fff);
  opacity: 0;
  animation: pl-dust var(--pl-dur, 11s) linear var(--pl-delay, 0s) infinite;
}

@keyframes pl-dust {
  0% { opacity: 0; transform: translate3d(0, 0, 0); }
  14%, 86% { opacity: var(--pl-peak, .5); }
  100% { opacity: 0; transform: translate3d(var(--pl-dx, 0), -108vh, 0); }
}

.pl-open__stage {
  position: relative;
  z-index: 3;
  width: min(68rem, 100%);
  /* The overlay clips (the rays run past its edges), so on a short laptop the
     stage has to take the scroll itself or the buttons under the prize would
     be unreachable. */
  max-height: calc(100vh - 2rem);
  /* The x axis has to be spelled out. `overflow-y: auto` on its own computes
     `overflow-x` to `auto` as well, and then the smallest sideways overflow —
     a fractional stage width at 125% browser zoom, the reel frame's border
     rounding, the landing kick's scale — hangs a native horizontal scrollbar
     across the bottom of the stage for the length of the spin. Nothing in
     here is ever meant to scroll sideways. */
  overflow-x: hidden;
  overflow-y: auto;
  /* And a wheel that runs out of stage does not go on to scroll the cabinet
     underneath it. */
  overscroll-behavior: contain;
  padding: 1.5rem;
  border: var(--pl-border);
  border-radius: var(--pl-radius);
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--rarity) 10%, transparent), transparent 42%),
    color-mix(in srgb, var(--reset-panel) 82%, transparent);
  box-shadow:
    0 30px 80px rgba(0, 0, 0, .6),
    0 0 0 1px color-mix(in srgb, var(--live) 26%, transparent);
  text-align: center;
  animation: pl-stage-in .42s cubic-bezier(.2, .9, .3, 1.25) both;
}

@keyframes pl-stage-in {
  0% { opacity: 0; transform: scale(.92) translateY(1.4rem); }
  100% { opacity: 1; transform: none; }
}

.pl-open__title {
  margin: .2rem 0 0;
  font-size: var(--fs-lg);
}

/* The running commentary under the title. It is reserved at full height from
   the start so the reel never shifts when the line changes. */
.pl-open__sub {
  min-height: 1.3em;
  margin: .3rem 0 0;
  color: var(--reset-muted);
  font-size: var(--fs-sm);
  transition: color .2s ease;
}

.pl-open[data-heat="3"] .pl-open__sub {
  color: color-mix(in srgb, var(--live) 70%, #fff);
  font-weight: 800;
  animation: pl-sub-beat .5s ease-in-out infinite alternate;
}

@keyframes pl-sub-beat {
  to { transform: scale(1.06); }
}

/* --- The lock (the wait on the server) ---------------------------------
   Spent, not hidden: the case rattles on its pedestal while the RPC settles,
   so the dead half-second before the reel exists reads as the lock giving. */

.pl-charge { display: none; }

.pl-open[data-stage="charge"] .pl-charge {
  display: grid;
  justify-items: center;
  gap: 1rem;
  padding: 2.4rem 1rem 2rem;
}

.pl-charge__box {
  display: grid;
  place-items: center;
  width: 13rem;
  height: 11rem;
  border: var(--pl-border);
  border-radius: var(--pl-radius);
  background:
    radial-gradient(circle at 50% 40%, color-mix(in srgb, var(--rarity) 48%, transparent), transparent 70%),
    #12151c;
  box-shadow: 0 0 46px color-mix(in srgb, var(--rarity) 45%, transparent);
  animation: pl-rattle .34s ease-in-out infinite;
}

.pl-charge__box .pl-art {
  width: 84%;
  height: 84%;
}

.pl-charge__box i {
  color: color-mix(in srgb, var(--rarity) 80%, #fff);
  font-size: 3.4rem;
}

@keyframes pl-rattle {
  0%, 100% { transform: translate(0, 0) rotate(-1.2deg); }
  25% { transform: translate(2px, -2px) rotate(1.4deg); }
  50% { transform: translate(-2px, 1px) rotate(-1.6deg); }
  75% { transform: translate(1px, 2px) rotate(1deg); }
}

.pl-charge__meter {
  width: min(22rem, 70%);
  height: .5rem;
  overflow: hidden;
  border-radius: 999px;
  background: rgba(247, 242, 232, .12);
}

.pl-charge__meter span {
  display: block;
  width: 40%;
  height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, transparent, var(--rarity), transparent);
  animation: pl-charge-run 1s ease-in-out infinite;
}

@keyframes pl-charge-run {
  0% { transform: translateX(-110%); }
  100% { transform: translateX(320%); }
}

/* --- The reel ---------------------------------------------------------
   Tiles run left to right past a fixed marker. JS measures a tile plus the
   gap to work out how far to travel, so these two values are the contract
   between the stylesheet and the animation. */

.pl-reel-wrap {
  position: relative;
  min-width: 0;
  height: 15rem;
  margin: 1.4rem 0;
  overflow: hidden;
  border: 2px solid color-mix(in srgb, var(--live) 55%, var(--reset-line));
  border-radius: var(--pl-radius-sm);
  background: linear-gradient(180deg, #0b0d12, #15181f);
  /* Both glows are inset: the wrap is masked at its ends, and a mask clips an
     outer shadow along with everything else. */
  box-shadow:
    inset 0 0 calc(1rem + var(--pl-heat) * 5rem) color-mix(in srgb, var(--live) 60%, transparent),
    inset 0 0 4rem rgba(0, 0, 0, .55);
  /* Fade the ends so tiles enter and leave instead of popping at a hard edge. */
  mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  transition: border-color .18s linear, box-shadow .18s linear;
}

.pl-open[data-stage="idle"] .pl-reel-wrap,
.pl-open[data-stage="charge"] .pl-reel-wrap { display: none; }

.pl-reel {
  --pl-tile: 11rem;
  --pl-tile-gap: .7rem;
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: var(--pl-tile-gap);
  height: 100%;
  padding-left: calc(50% - var(--pl-tile) / 2);
  /* Motion blur, quantised by the script so the layer is re-rasterised a
     handful of times across the whole spin rather than every frame. Being
     unreadable at speed is the point: the names resolving out of the smear is
     what makes the last second work. */
  filter: blur(calc(var(--pl-blur, 0) * 1px));
  will-change: transform;
}

.pl-reel__tile {
  display: grid;
  place-items: center;
  gap: .35rem;
  flex: 0 0 var(--pl-tile);
  height: 12.6rem;
  padding: .7rem;
  border: 2px solid var(--reset-line-strong);
  border-radius: var(--pl-radius-sm);
  background:
    radial-gradient(circle at 50% 35%, color-mix(in srgb, var(--rarity) 40%, transparent), transparent 72%),
    #12151c;
  /* The rarity band again, this time as the tile's foot. */
  border-bottom: 5px solid var(--rarity);
}

/* The top tiers get a halo on the strip itself, so a good tile is visible
   coming before anyone can read its name. */
.pl-reel__tile.is-elite {
  box-shadow: 0 0 2.2rem color-mix(in srgb, var(--rarity) 70%, transparent);
}

/* The strip's rows are auto-sized, so the frame carries its own height here
   instead of asking the tile for one. */
.pl-reel__tile .pl-art {
  height: 7.4rem;
}

/* Nothing fades in at speed: a tile that is on screen for three frames has to
   be there the moment it arrives. */
.pl-reel__tile .pl-art__img {
  opacity: 1;
  transform: none;
  transition: none;
}

.pl-reel__tile .pl-art.is-loading .pl-art__state::before { animation: none; }

.pl-reel__tile i {
  color: color-mix(in srgb, var(--rarity) 80%, #fff);
  font-size: 2.8rem;
}

.pl-reel__tile span {
  width: 100%;
  overflow: hidden;
  color: #efe9df;
  font-size: var(--fs-2xs);
  font-weight: 700;
  text-align: center;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Streaks that exist only while the strip is genuinely fast: --pl-speed is 1
   at full tilt and 0 at rest. They run *along* the travel, not across it —
   vertical bars over a horizontally moving strip read as a dirty screen, and
   horizontal ones read as speed. */
.pl-reel__speed {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: repeating-linear-gradient(
    180deg,
    rgba(255, 255, 255, .55) 0 1px,
    transparent 1px 13px);
  opacity: calc(var(--pl-speed, 0) * .2);
  mask-image: linear-gradient(90deg, transparent, #000 30%, #000 70%, transparent);
  mix-blend-mode: screen;
  pointer-events: none;
}

/* The halo under the marker, painted in the colour of the tile passing it. */
.pl-reel__halo {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 2;
  width: 20rem;
  height: 20rem;
  background: radial-gradient(circle, color-mix(in srgb, var(--live) 65%, transparent), transparent 62%);
  opacity: calc(.18 + var(--pl-heat) * .5);
  transform: translate(-50%, -50%);
  mix-blend-mode: screen;
  pointer-events: none;
  transition: opacity .2s linear;
}

.pl-reel__marker {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  z-index: 3;
  width: 3px;
  margin-left: -1.5px;
  background: var(--reset-sun);
  box-shadow: 0 0 18px 3px color-mix(in srgb, var(--reset-sun) 60%, transparent);
  pointer-events: none;
}

/* The two arrowheads pinching the marker line. They grow with the heat, which
   is a second read on "nearly there" for anyone watching the middle rather
   than the edges of the frame. */
.pl-reel__marker::before,
.pl-reel__marker::after {
  content: "";
  position: absolute;
  left: 50%;
  border: 9px solid transparent;
  transform: translateX(-50%) scale(calc(1 + var(--pl-heat) * .5));
  transition: transform .2s ease;
}

.pl-reel__marker::before {
  top: 0;
  border-top-color: var(--reset-sun);
}

.pl-reel__marker::after {
  bottom: 0;
  border-bottom-color: var(--reset-sun);
}

/* The crawl: the strip has all but stopped a fraction of a tile short and is
   creeping the rest of the way. Everything that can pulse, pulses. */
.pl-open.is-crawl .pl-reel__marker {
  animation: pl-marker-beat .42s ease-in-out infinite;
}

@keyframes pl-marker-beat {
  0%, 100% { box-shadow: 0 0 18px 3px color-mix(in srgb, var(--reset-sun) 60%, transparent); }
  50% { box-shadow: 0 0 34px 9px color-mix(in srgb, var(--reset-sun) 90%, transparent); }
}

/* The tile the strip stopped on, held up for half a second before the card
   takes over. */
.pl-reel__tile.is-hit {
  z-index: 2;
  border-color: var(--rarity);
  animation: pl-hit .55s cubic-bezier(.2, .9, .3, 1.4) both;
}

@keyframes pl-hit {
  0% { transform: scale(1); box-shadow: 0 0 0 transparent; }
  45% { transform: scale(1.12); box-shadow: 0 0 3.6rem color-mix(in srgb, var(--rarity) 90%, transparent); }
  100% { transform: scale(1.06); box-shadow: 0 0 2.6rem color-mix(in srgb, var(--rarity) 70%, transparent); }
}

/* The landing kick, on the frame rather than the strip — moving the strip
   would drag the prize off the marker it just landed on. */
.pl-open.is-land .pl-reel-wrap {
  animation: pl-land .38s cubic-bezier(.2, .9, .3, 1.4);
}

@keyframes pl-land {
  0%, 100% { transform: scale(1); }
  35% { transform: scale(1.035); }
}

/* --- The reveal -------------------------------------------------------- */

.pl-open[data-stage="reveal"] .pl-reel-wrap { display: none; }

/* A column of light dropped on the prize. */
.pl-open__beam {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  z-index: 2;
  width: min(34rem, 80vw);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--rarity) 55%, transparent),
    color-mix(in srgb, var(--rarity) 12%, transparent) 55%,
    transparent);
  opacity: 0;
  transform: translateX(-50%) scaleY(.2);
  transform-origin: 50% 0;
  mask-image: linear-gradient(90deg, transparent, #000 22%, #000 78%, transparent);
  pointer-events: none;
}

.pl-open[data-stage="reveal"] .pl-open__beam {
  animation: pl-beam .9s cubic-bezier(.16, .84, .3, 1) both;
}

@keyframes pl-beam {
  0% { opacity: 0; transform: translateX(-50%) scaleY(.15); }
  40% { opacity: .85; }
  100% { opacity: .42; transform: translateX(-50%) scaleY(1); }
}

/* The shockwave ring. A jackpot gets two, a beat apart. */
.pl-open__shock {
  position: absolute;
  top: 44%;
  left: 50%;
  z-index: 5;
  width: 16rem;
  height: 16rem;
  border: 3px solid color-mix(in srgb, var(--rarity) 85%, #fff);
  border-radius: 50%;
  opacity: 0;
  transform: translate(-50%, -50%) scale(.2);
  pointer-events: none;
}

.pl-open.is-shock .pl-open__shock {
  animation: pl-shock .75s cubic-bezier(.1, .7, .3, 1) both;
}

.pl-open.is-shock.is-jackpot .pl-open__shock {
  animation:
    pl-shock .95s cubic-bezier(.1, .7, .3, 1) both,
    pl-shock .95s cubic-bezier(.1, .7, .3, 1) .22s both;
}

@keyframes pl-shock {
  0% { opacity: .95; transform: translate(-50%, -50%) scale(.2); border-width: 5px; }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(3.4); border-width: 1px; }
}

/* Two frames of white. The cheapest dopamine in the business. */
.pl-open__flash {
  position: absolute;
  inset: 0;
  z-index: 7;
  background: radial-gradient(circle at 50% 44%, #fff, color-mix(in srgb, var(--rarity) 70%, #fff) 45%, transparent 72%);
  opacity: 0;
  pointer-events: none;
}

.pl-open.is-flash .pl-open__flash {
  animation: pl-flash .42s ease-out both;
}

@keyframes pl-flash {
  0% { opacity: .82; }
  100% { opacity: 0; }
}

.pl-prize {
  position: relative;
  display: grid;
  justify-items: center;
  gap: .5rem;
  padding: 1.2rem 1rem 1.4rem;
}

.pl-prize[hidden] { display: none; }

.pl-prize__art {
  position: relative;
  display: grid;
  place-items: center;
  width: 17rem;
  height: 13rem;
  padding: 1rem;
  border: var(--pl-border);
  border-radius: var(--pl-radius);
  background:
    radial-gradient(circle at 50% 45%, color-mix(in srgb, var(--rarity) 55%, transparent), transparent 70%),
    #12151c;
  box-shadow: 0 0 42px color-mix(in srgb, var(--rarity) 55%, transparent);
  animation:
    pl-prize-in .5s cubic-bezier(.2, .9, .3, 1.5) both,
    pl-prize-float 3.4s ease-in-out .5s infinite alternate;
}

/* A shine sweeping the plate — what makes a still picture of a skin feel like
   an object sitting under a light. */
.pl-prize__art::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(115deg, transparent 38%, rgba(255, 255, 255, .38) 50%, transparent 62%);
  background-size: 260% 100%;
  animation: pl-sheen 2.6s ease-in-out .6s infinite;
  pointer-events: none;
}

/* The render sits above the plate's sheen: the shine skims the plate around
   the prize rather than washing over the prize itself. */
.pl-prize__art .pl-art { z-index: 1; }

.pl-prize__art i {
  color: color-mix(in srgb, var(--rarity) 80%, #fff);
  font-size: 3.4rem;
}

@keyframes pl-prize-in {
  0% { opacity: 0; transform: scale(.4) rotate(-8deg); }
  100% { opacity: 1; transform: none; }
}

@keyframes pl-prize-float {
  to { transform: translateY(-.6rem); }
}

@keyframes pl-sheen {
  0% { background-position: 160% 0; }
  55%, 100% { background-position: -60% 0; }
}

/* Name, tier, count and buttons arrive one after another rather than all at
   once — a quarter of a second of reading order is worth more than any of the
   effects above it. */
.pl-prize h3 {
  margin: .6rem 0 0;
  font-size: var(--fs-xl);
  animation: pl-prize-line .4s ease-out .16s both;
}

.pl-prize__rarity {
  padding: .25rem .8rem;
  border: 2px solid var(--reset-cream);
  border-radius: 999px;
  background: var(--rarity);
  color: #1a150e;
  font-size: var(--fs-2xs);
  font-weight: 800;
  letter-spacing: .08em;
  text-transform: uppercase;
  animation: pl-prize-line .4s ease-out .26s both;
}

.pl-open.is-jackpot .pl-prize__rarity {
  animation:
    pl-prize-line .4s ease-out .26s both,
    pl-badge-pulse 1.4s ease-out .7s infinite;
}

@keyframes pl-badge-pulse {
  0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--rarity) 75%, transparent); }
  100% { box-shadow: 0 0 0 1.6rem transparent; }
}

@keyframes pl-prize-line {
  0% { opacity: 0; transform: translateY(.7rem); }
  100% { opacity: 1; transform: none; }
}

.pl-prize__owned {
  margin: 0;
  color: var(--reset-muted);
  font-size: var(--fs-xs);
  animation: pl-prize-line .4s ease-out .34s both;
}

/* The tokens a case pays out on opening. Set beside the owned count rather
   than under it, so the reveal keeps one line of small print instead of two. */
.pl-prize__bonus {
  margin-left: .55rem;
  vertical-align: middle;
}

.pl-open__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: .6rem;
  animation: pl-prize-line .4s ease-out .44s both;
}

.pl-open__actions[hidden] { display: none; }

/* A legendary pull earns a shove. Kept small — this is a nudge, not a quake,
   and it is switched off entirely under reduced motion. */
.pl-open.is-jackpot .pl-open__stage { animation: pl-shake .5s cubic-bezier(.36, .07, .19, .97); }

@keyframes pl-shake {
  10%, 90% { transform: translate(-2px, 1px); }
  20%, 80% { transform: translate(4px, -2px); }
  30%, 50%, 70% { transform: translate(-7px, 2px); }
  40%, 60% { transform: translate(7px, -1px); }
}

/* Confetti. Plain spans, each given its own drift, spin and delay inline.
   Two shapes in the mix — the round ones read as sparks next to the ribbons —
   and the burst arcs down at the end rather than flying straight out, because
   straight lines look like a particle system and arcs look like something was
   thrown. */
.pl-confetti {
  position: absolute;
  inset: 0;
  z-index: 5;
  overflow: hidden;
  pointer-events: none;
}

.pl-confetti span {
  position: absolute;
  top: 42%;
  left: 50%;
  width: var(--pl-size, 10px);
  height: calc(var(--pl-size, 10px) * .42);
  background: var(--pl-color, var(--reset-sun));
  opacity: 0;
  animation: pl-confetti var(--pl-life, 1.8s) cubic-bezier(.15, .7, .4, 1) var(--pl-delay, 0s) forwards;
}

.pl-confetti span.is-spark {
  width: var(--pl-size, 8px);
  height: var(--pl-size, 8px);
  border-radius: 50%;
  box-shadow: 0 0 10px var(--pl-color, var(--reset-sun));
}

@keyframes pl-confetti {
  0% { opacity: 1; transform: translate(0, 0) rotate(0deg); }
  60% { opacity: 1; }
  100% {
    opacity: 0;
    transform: translate(var(--pl-dx), calc(var(--pl-dy) + 22vh)) rotate(var(--pl-rot));
  }
}

/* ===== Mobile tab bar ====================================================
   Below 900px the rail becomes a bottom bar, the way a game app would do it:
   thumb-reachable, five targets, no hamburger to find — and no "More" either.
   Every slot opens a screen; the screens that used to hide behind the fifth
   slot now share a slot with their neighbours and split with .pl-subnav. */

.pl-tabbar { display: none; }

@media (max-width: 900px) {
  .pl-app { grid-template-columns: minmax(0, 1fr); }
  .pl-rail { display: none; }

  .pl-main { padding-bottom: 6.5rem; }

  .pl-code { grid-template-columns: auto minmax(0, 1fr); }
  .pl-code__form { grid-column: 1 / -1; }

  .pl-tabbar {
    position: fixed;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 20;
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    gap: .3rem;
    padding: .55rem .5rem calc(.55rem + env(safe-area-inset-bottom));
    border-top: 0;
    /* The bar is the console's front panel: darker than the board above it in
       either theme, with the ember bleeding up out of the headset key in the
       middle. A plain panel fill left it reading as a strip of page that
       happened to have buttons on it. */
    background:
      radial-gradient(60% 200% at 50% 130%, color-mix(in srgb, var(--pl-ember) 26%, transparent), transparent 72%),
      var(--reset-panel);
    box-shadow: 0 -1px 0 var(--reset-line), 0 -18px 34px -22px #000;
  }

  .pl-tabbar button {
    position: relative;
    display: grid;
    justify-items: center;
    gap: .22rem;
    padding: .5rem .2rem;
    border: 2px solid transparent;
    border-radius: var(--pl-radius-sm);
    background: transparent;
    color: var(--reset-muted);
    font: inherit;
    font-family: var(--heading-font-family);
    font-size: .68rem;
    font-weight: 800;
    letter-spacing: .04em;
    text-transform: uppercase;
    cursor: pointer;
  }

  .pl-tabbar button i { font-size: 1.05rem; }

  /* No fill on the active tab down here: five filled slabs in a row is a strip
     of confetti. It lights its glyph and its label instead and hangs a bar
     under it, which is the tell every phone app already trained the player to
     read — and it leaves the one fill in the bar to the headset key. */
  .pl-tabbar button.is-active {
    border-color: transparent;
    background: transparent;
    color: var(--pl-ember-hi);
  }

  .pl-tabbar button.is-active::after {
    position: absolute;
    bottom: -.15rem;
    left: 50%;
    width: 1.6rem;
    height: 3px;
    border-radius: 999px;
    background: var(--pl-ember);
    box-shadow: 0 0 14px 0 color-mix(in srgb, var(--pl-ember) 85%, transparent);
    transform: translateX(-50%);
    content: "";
  }

  .pl-tabbar .pl-nav__badge {
    position: absolute;
    top: -.1rem;
    right: 12%;
    padding: 0 .3rem;
    font-size: .6rem;
  }

  /* Приподнятый ключ в середине. Раньше он уводил в шлем, теперь открывает
     «Главное» — экран, на котором эта же кнопка стоит во весь палец. Синий он
     остался нарочно: оранжевым в этой панели горит только текущая вкладка, и
     ключ не должен с ней соревноваться за тот же смысл.
     Keep `button` in the selector to override the shared tab styles. */
  .pl-tabbar button.pl-tabbar__home {
    /* The row stretches its cells, and on a slot half again as tall as the
       others that pushes the icon to the ceiling and the label to the floor.
       The pair is centred as a group instead. */
    align-content: center;
    gap: .1rem;
    margin-top: -1.75rem;
    padding: .75rem .2rem .6rem;
    border: 2px solid var(--reset-tile-ink);
    border-radius: var(--pl-radius-sm);
    background: var(--reset-sky);
    color: var(--reset-tile-ink);
    box-shadow: var(--pl-lift-sm);
  }

  .pl-tabbar button.pl-tabbar__home:is(:hover, :focus-visible) {
    background: color-mix(in srgb, var(--reset-sky) 82%, white);
  }
  /* Стоя на «Главном», ключ не подчёркивается снизу, как остальные вкладки —
     его подчёркивать нечем, он и так выше всех. Вместо этого он берёт ободок
     цвета текущей вкладки: то же «ты здесь», сказанное языком ключа. */
  .pl-tabbar button.pl-tabbar__home.is-active {
    color: var(--reset-tile-ink);
    box-shadow: var(--pl-lift-sm), 0 0 0 3px color-mix(in srgb, var(--pl-ember) 72%, transparent);
  }
  .pl-tabbar button.pl-tabbar__home.is-active::after { display: none; }
  .pl-tabbar button.pl-tabbar__home i { font-size: 1.5rem; }
  .pl-tabbar button.pl-tabbar__home:active { transform: translateY(1px); }

  .pl-hero { grid-template-columns: minmax(0, 1fr); }
  .pl-profile { grid-template-columns: minmax(0, 1fr); }
  .pl-portrait { width: clamp(6.5rem, 22vw, 8.5rem); }

  /* Five slots share the bar, so the labels tighten. */
  .pl-tabbar button { font-size: .6rem; padding: .45rem .1rem; }
  .pl-tabbar button i { font-size: 1rem; }
}

/* Планшет и всё, что уже: трёхколоночная шапка тут больше не помещается —
   заголовок ломается на три строки, а знак возраста наезжает на него. */
@media (max-width: 900px) {
  /* Трёх колонок в один ряд тут нет места, а плашка жетонов отдельной строкой
     по центру съедала полосу экрана и висела ни к чему не привязанная. Шапка
     становится сеткой 2×2: сверху бренд и управление, под ними — название
     экрана и жетоны, всё по одним и тем же краям. */
  .pl-top {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    column-gap: .7rem;
    row-gap: .35rem;
  }

  /* Строки текстового блока встают прямо в сетку шапки, каждая в свой ряд. */
  .pl-top__text { display: contents; }

  .pl-top__eyebrow {
    display: flex;
    grid-row: 1;
    grid-column: 1;
    align-items: center;
    min-width: 0;
    white-space: nowrap;
  }

  .pl-top h1 {
    grid-row: 2;
    grid-column: 1;
    margin: 0;
  }

  /* Кегль десктопной шапки в узкую колонку не влезает и рвёт самое длинное
     название на две строки. */
  .pl-top__text h1 {
    min-width: 0;
    font-size: var(--fs-lg);
    line-height: 1.05;
  }

  .pl-top__actions {
    grid-row: 1;
    grid-column: 2;
  }

  .pl-top .pl-wallet {
    grid-row: 2;
    grid-column: 2;
    justify-self: end;
  }
}

/* Below this the two-column picker stops paying for itself: the preview lies
   down into a header row and the roster takes the whole card. */
@media (max-width: 760px) {
  .pl-avatar-picker { max-height: min(92vh, 42rem); }

  .pl-avatar-picker__layout {
    grid-template-columns: minmax(0, 1fr);
    gap: .8rem;
  }

  .pl-avatar-preview {
    grid-template-columns: 5.25rem minmax(0, 1fr);
    align-items: center;
    gap: .85rem;
  }

  .pl-avatar-preview__plate { border-radius: var(--pl-radius-sm); }

  .pl-avatar-preview__caption {
    justify-items: start;
    text-align: left;
  }
}

@media (max-width: 620px) {
  .pl-top { padding-block: .7rem .6rem; row-gap: .3rem; }
  .pl-top__text h1 { font-size: var(--fs-lg); }
  .pl-user-chip__name { display: none; }
  .pl-user-chip { padding: .25rem; }

  /* Ряд управления и так забирает больше половины ширины, поэтому строка
     бренда сбрасывает разрядку, а знак возраста — лишние миллиметры: иначе
     ромб не помещается рядом и переносится под подпись, налезая на заголовок. */
  .pl-top__actions { gap: .3rem; }
  .pl-top__eyebrow { letter-spacing: .06em; }
  .pl-top__brand { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

  .pl-top__eyebrow .pl-age-mark {
    width: 1.6rem;
    height: 1.4rem;
    margin-left: .4rem;
    font-size: .56rem;
  }

  /* Подпись «боевые жетоны» на телефоне лишняя: монета говорит то же самое,
     а пилюля без неё встаёт вровень с заголовком. */
  .pl-top .pl-wallet { padding: .28rem .7rem .28rem .3rem; }
  .pl-top .pl-wallet__label { display: none; }

  /* Четыре кнопки в ряд читаются как один прибор, только если они одного
     размера: у чипа профиля своя рамка и свой аватар, и без выравнивания он
     на пять пикселей выше соседей. */
  .pl-top__actions .pl-icon-btn,
  .pl-top__actions .pl-user-chip {
    width: 2.75rem;
    height: 2.75rem;
    flex: 0 0 auto;
    justify-content: center;
    padding: 0;
  }

  .pl-top__actions .pl-user-chip .pl-avatar { width: 2.3rem; height: 2.3rem; }

  /* Самое длинное название («Боевой пропуск») рядом с жетонами не помещается
     в строку и переносится — но высоту шапки задаёт плашка жетонов, поэтому
     две строки в неё укладываются и шапка не прыгает между вкладками. */
  .pl-top__text h1 {
    min-width: 0;
    line-height: 1.05;
  }

  /* The portrait stays beside the name rather than stacking above it: a card
     that reads left to right is shorter, and the loot is what the player
     scrolled here for. */
  .pl-hero__card { gap: .85rem; padding: 1rem; }
  .pl-portrait { width: clamp(5.5rem, 27vw, 7rem); border-radius: var(--pl-radius-sm); }
  .pl-portrait__plate { font-size: 1.9rem; }
  .pl-portrait__scrim em { display: none; }
  .pl-portrait__level { right: .3rem; bottom: .3rem; padding: .12rem .4rem; }
  .pl-portrait__pip { top: .3rem; left: .3rem; width: 1.35rem; height: 1.35rem; font-size: .58rem; }
  .pl-vault__counts { gap: .7rem; }
  .pl-vault, .pl-hero { gap: .7rem; }

  .pl-grid { grid-template-columns: repeat(auto-fill, minmax(10.5rem, 1fr)); gap: .7rem; }
  .pl-store-grid { grid-template-columns: minmax(0, 1fr); }
  .pl-code { grid-template-columns: minmax(0, 1fr); padding: .9rem; }
  .pl-code__ticket { width: 3rem; height: 3rem; font-size: 1.05rem; }
  .pl-code__form { grid-column: auto; }
  .pl-code__input-row { grid-template-columns: minmax(0, 1fr); }
  .pl-code__input-row .pl-btn { width: 100%; min-height: 44px; }
  .pl-daily { grid-template-columns: auto minmax(0, 1fr) auto; }
  .pl-daily .pl-btn { grid-column: 1 / -1; }
  .pl-daily__gift { width: 2.6rem; height: 2.6rem; font-size: 1rem; }

  /* The banner keeps its coin — it is the picture of the thing the sentence is
     about — but the coin moves above the copy instead of beside it. */
  .pl-earn { grid-template-columns: minmax(0, 1fr); gap: .85rem; padding: 1rem; }
  .pl-earn__coin { width: 4.4rem; height: 4.4rem; }
  .pl-coin--xl { width: 3.2rem; height: 3.2rem; }
  .pl-earn__stats { gap: .35rem 1.1rem; }

  /* The header pill drops its caption, never its number. */
  .pl-wallet { padding: .24rem .72rem .24rem .28rem; }
  .pl-wallet__label { display: none; }
  .pl-coin--md { width: 1.85rem; height: 1.85rem; }
  .pl-item__art { height: 10rem; padding: .55rem; }
  .pl-case .pl-item__art { height: 8rem; }
  /* Smaller frames, shorter brackets — full-size corners on a 9rem tile turn
     into a second border. */
  .pl-item__art .pl-art::before,
  .pl-slot__art .pl-art::before { --pl-corner: .8rem; }
  /* The sheet is narrower here, so the stage is closer to square and the
     render has less width to grow into before its own margins run out. */
  .pl-inspect__art { height: 13rem; }
  .pl-inspect__art .pl-art__img { scale: 1.35; }
  .pl-case-contents__head {
    grid-template-columns: 5.5rem minmax(0, 1fr);
    gap: .7rem;
    padding-right: 2.4rem;
  }
  .pl-case-contents__case-art { height: 5.5rem; padding: .45rem; }
  .pl-case-rewards { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .5rem; }
  .pl-case-reward { grid-template-rows: auto 4px minmax(0, 1fr); }
  /* Two cards to a phone screen: the badge keeps the plate, not the other
     way round. */
  .pl-case-reward__chance {
    top: .35rem;
    left: .35rem;
    padding: .1rem .34rem;
    font-size: .56rem;
  }
  .pl-case-reward__body { padding: .5rem .55rem .6rem; }
  .pl-item__body { padding: .6rem .7rem .8rem; }
  .pl-item__name { font-size: var(--fs-sm); }

  /* Two racks across on a phone: eight weapon types in one column is a lot of
     scrolling for a screen whose whole job is "what am I wearing?". */
  .pl-slots { grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr)); gap: .7rem; }
  .pl-slot__head { flex-wrap: wrap; gap: .3rem; padding: .5rem .6rem; }
  .pl-slot__art { height: 7rem; padding: .6rem; }
  .pl-slot__body { padding: .6rem .7rem .8rem; }
  .pl-slot .pl-item__row { flex-wrap: wrap; }
  /* A real control on a phone: the plate keeps a thumb-sized target even
     when the type steps down. */
  .pl-slot__get { min-height: 42px; padding: .5rem .55rem; font-size: var(--fs-2xs); letter-spacing: .02em; }
  /* The drawer tabs become a swipeable strip — the same move the rarity
     chips make. */
  .pl-filters--arsenal { grid-template-columns: minmax(0, 1fr); }
  .pl-tabs {
    flex-wrap: nowrap;
    margin-right: -1rem;
    padding-right: 1rem;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .pl-tabs::-webkit-scrollbar { display: none; }
  .pl-tab { flex: 0 0 auto; }
  .pl-picker { grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr)); }
  .pl-picker__art { height: 4.75rem; }
  .pl-picker__art .pl-art__img { scale: 1.12; }
  .pl-profile-stats { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .pl-avatar-picker { padding: 1rem; }
  .pl-avatar-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .4rem; }
  /* Three faces across a phone leaves no room for a caption anyone could read.
     The preview above the roster is already saying the name of whatever is
     selected, so the tiles become pure portraits. */
  .pl-avatar-option__name { display: none; }
  .pl-avatar-picker__actions {
    margin: .8rem -1rem -1rem;
    padding: .7rem 1rem calc(.8rem + env(safe-area-inset-bottom));
  }

  .pl-sell__controls { grid-template-columns: minmax(0, 1fr); }

  .pl-purchase-success { padding: 1rem; }
  .pl-purchase-success > h2 { margin-right: 2.5rem; margin-left: 2.5rem; }
  .pl-purchase-success__item { grid-template-columns: 5.5rem minmax(0, 1fr); gap: .75rem; }
  .pl-purchase-success__art { height: 5rem; }
  .pl-purchase-success__actions { grid-template-columns: minmax(0, 1fr); }
  .pl-purchase-success__actions .pl-btn { min-height: 44px; }

  .pl-section-head { flex-direction: column; align-items: stretch; }
  .pl-count { align-self: flex-start; }

  .pl-chips {
    flex-wrap: nowrap;
    overflow-x: auto;
    padding-bottom: .2rem;
    scrollbar-width: none;
  }

  .pl-chips::-webkit-scrollbar { display: none; }
  .pl-chip { flex: 0 0 auto; }

  /* Панель на телефоне: поиск занимает строку целиком, кнопки переносятся под
     ним. Свайпающейся лентой их сделать нельзя — лента обрезала бы список,
     который из кнопки выпадает, — поэтому у кнопок вместо подписи остаётся
     иконка и само значение. Значение и есть то, что читают. */
  /* На телефоне шапка уже съедает сто сорок точек, и липкая панель поверх
     неё оставила бы полке треть экрана. Здесь она едет вместе со страницей —
     до неё один взмах вверх. */
  /* Не static: раскрытый список должен лежать над карточками полки, а не
     уезжать под них. Относительная с z-index держит его сверху, но со
     страницей едет. Слой ниже обеих липких полос — мачты (5) и ленты (4), —
     потому что уехавшая наверх непрозрачная панель обязана скрыться под ними,
     а не проехать поверх; карточки полки лежат ниже и так. */
  .pl-board { position: relative; top: auto; z-index: 3; padding-block: .5rem .55rem; }
  .pl-board__row .pl-search { flex: 1 1 100%; max-width: none; }
  .pl-board__tools { flex: 1 1 100%; }
  .pl-pick,
  .pl-pick__trigger,
  .pl-toggle { max-width: 100%; }
  .pl-pick__value { max-width: 8rem; }
  .pl-pick__menu { max-width: calc(100vw - 1.6rem); }
  .pl-pick__list { max-height: min(15rem, 42vh); }
  .pl-tail { flex-direction: column; gap: .5rem; }


  .pl-reel-wrap { height: 11.5rem; }
  .pl-reel { --pl-tile: 7.6rem; }
  .pl-reel__tile { height: 9.4rem; }
  .pl-reel__tile .pl-art { height: 5rem; }
  .pl-open__stage { padding: 1rem .8rem; }
  .pl-prize__art { width: 11rem; height: 9rem; }
  .pl-reel__halo { width: 13rem; height: 13rem; }
  .pl-charge__box { width: 10rem; height: 8.6rem; }
  .pl-history__row { grid-template-columns: 2.2rem minmax(0, 1fr) auto; padding: .7rem; }

  .pl-match {
    grid-template-columns: auto minmax(0, 1fr);
    gap: .5rem .7rem;
    padding: .7rem;
  }

  .pl-match__kda { grid-column: 1; }
  .pl-match__xp { grid-column: 2; justify-self: end; }
}

/* ===== The phone's own case theatre ======================================
   JS selects this profile for phones, coarse pointers and Save-Data. It used
   to be the desktop room with its lights switched off, which held the frame
   rate by giving up the show. This is the same room rebuilt out of the work a
   phone's compositor does for nothing.

   One rule underneath all of it. A phone will animate `transform` and
   `opacity` on a bounded number of layers all day, because that work belongs
   to the compositor and never touches the main thread; what it will not do is
   re-rasterise. So every layer here that used to cost a frame is redrawn as
   something that costs a texture instead — the blurred orbs become the
   gradients they were already drawing, the floor scrolls by moving rather
   than repainting, the strip's motion blur becomes streaks laid over it, the
   masks clipping moving content become fades painted beside it, and the whole
   wind-up is timed off `--pl-spin` in CSS rather than measured sixty times a
   second in JS. Nothing was deleted to make the phone keep up. */

/* --- The room ----------------------------------------------------------
   The orbs stay, minus the two things that made them expensive. A 70px blur
   over half a screen has to be recomputed whenever the layer's scale changes,
   and the desktop keyframes change it on every frame — for a gradient that
   already fades to nothing by 68%, which is to say for almost nothing.
   `screen` over a near-black sky is the same bargain: a full-area blend every
   frame to reach a result plain alpha arrives at on its own. */
.pl-open.is-lite .pl-open__orb {
  filter: none;
  mix-blend-mode: normal;
  opacity: .3;
}

/* Drift without the scale. The travel is what reads as depth; the scale is
   what forces the layer to be drawn again. */
.pl-open.is-lite .pl-open__orb--a { animation-name: pl-orb-a-lite; }
.pl-open.is-lite .pl-open__orb--b { animation-name: pl-orb-b-lite; }
/* Two lights make a room. The third only makes a bill. */
.pl-open.is-lite .pl-open__orb--c { display: none; }

@keyframes pl-orb-a-lite { to { transform: translate3d(12vmax, 7vmax, 0); } }
@keyframes pl-orb-b-lite { to { transform: translate3d(-13vmax, -8vmax, 0); } }

/* The floor keeps running toward the camera, but by travelling instead of
   being repainted: `background-position` on a 240vmax-wide element is a full
   repaint every frame, and the identical movement done as a transform on a
   child is one layer sliding under a clip. The mask goes for the same reason
   — a mask over moving content makes the browser compose the whole thing
   through an offscreen surface every frame, where a fade painted on top of it
   is one more thing in the same texture. */
.pl-open.is-lite .pl-open__grid {
  overflow: hidden;
  background-image: none;
  mask-image: none;
  animation: none;
}

.pl-open.is-lite .pl-open__grid::before {
  content: "";
  position: absolute;
  inset: -8rem 0 0;
  background-image:
    repeating-linear-gradient(90deg, color-mix(in srgb, var(--rarity) 55%, transparent) 0 2px, transparent 2px 8rem),
    repeating-linear-gradient(0deg, color-mix(in srgb, var(--rarity) 40%, transparent) 0 2px, transparent 2px 8rem);
  animation: pl-floor-lite 4.5s linear infinite;
}

/* Exactly one period of the pattern, so the loop has no seam in it. */
@keyframes pl-floor-lite { to { transform: translate3d(0, 8rem, 0); } }

.pl-open.is-lite .pl-open__grid::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(0deg, transparent, #070911 62%);
}

/* Rays, cut down to a size the phone can hold. The desktop fan is 160vmax
   square: on a 3× screen that is a texture in the tens of megabytes, which is
   the number at which a compositor starts dropping tiles rather than drawing
   them. Sized to the middle of the screen and faded out by its own mask, one
   fan reads as the same light as two. The mask is safe here in a way it is
   not over the strip — nothing inside this element moves, so it is baked into
   the texture once and the rotation stays pure compositing. */
.pl-open.is-lite .pl-open__rays {
  width: min(52rem, 132vmin);
  height: min(52rem, 132vmin);
  mask-image: radial-gradient(circle, #000 26%, transparent 68%);
  /* Held at 34s from the lock to the reveal. Every rule below that touches
     this fan repeats the figure rather than changing it: a rotation whose
     duration changes mid-flight keeps its elapsed time and recomputes the
     progress, and that lands as the fan jumping several degrees. */
  animation-duration: 34s;
}

.pl-open.is-lite .pl-open__rays::after { display: none; }

/* --- The wind-up -------------------------------------------------------
   Everything the desktop's rAF loop paints from the strip's real position is
   a CSS animation here: same length as the spin, same shape as the heat
   curve, and every one of them animating opacity and nothing else.
   `--pl-spin` is the single value JS writes, once, before the reel moves. */
@keyframes pl-lite-heat {
  0%, 46% { opacity: .1; }
  72% { opacity: .34; }
  88% { opacity: .5; }
  100% { opacity: .62; }
}

/* The room's edge glow takes the case colour rather than the live one. It is
   a full-screen inset shadow, and repainting it every time a tile passes the
   marker is the one repaint in here big enough to feel; the halo is small
   enough to carry the live colour on its own. */
.pl-open.is-lite::after {
  box-shadow: inset 0 0 11rem color-mix(in srgb, var(--rarity) 60%, transparent);
  opacity: 0;
  transition: opacity .4s ease;
}

.pl-open.is-lite.is-spinning::after {
  animation: pl-lite-heat var(--pl-spin, 6600ms) linear both;
}

.pl-open.is-lite.is-spinning .pl-open__rays {
  animation:
    pl-spin 34s linear infinite,
    pl-lite-heat var(--pl-spin, 6600ms) linear both;
}

.pl-open.is-lite .pl-reel__halo {
  mix-blend-mode: normal;
  opacity: .16;
  transition: none;
}

.pl-open.is-lite.is-spinning .pl-reel__halo {
  animation: pl-lite-halo var(--pl-spin, 6600ms) linear both;
}

@keyframes pl-lite-halo {
  0%, 40% { opacity: .14; transform: translate(-50%, -50%) scale(.8); }
  72% { opacity: .42; transform: translate(-50%, -50%) scale(1); }
  100% { opacity: .78; transform: translate(-50%, -50%) scale(1.14); }
}

/* The marker's arrowheads close on the prize as the reel runs down — the
   desktop drives this off `--pl-heat`, and the shape of that climb is the
   same one written out here. */
.pl-open.is-lite.is-spinning .pl-reel__marker::before,
.pl-open.is-lite.is-spinning .pl-reel__marker::after {
  animation: pl-lite-pinch var(--pl-spin, 6600ms) linear both;
}

@keyframes pl-lite-pinch {
  0%, 52% { transform: translateX(-50%) scale(1); }
  100% { transform: translateX(-50%) scale(1.5); }
}

/* And the room dims around the reel for the hesitation: one opacity step on
   one layer, for the only moment in the spin where nothing is moving. */
.pl-open.is-lite .pl-open__sky { transition: opacity .45s ease; }
.pl-open.is-lite.is-crawl .pl-open__sky { opacity: .5; }

/* --- The strip ---------------------------------------------------------
   The motion blur, moved off the thing in motion. `filter: blur()` on the
   reel re-rasterises the whole travelling strip every frame, and on a phone
   that is the single most expensive object in the theatre. Streaks laid over
   the top read as the same speed and never touch it: one static texture whose
   opacity is animated down as the reel slows. */
.pl-open.is-lite .pl-reel { filter: none; }

.pl-open.is-lite .pl-reel__speed {
  /* `screen` would have to read the moving strip behind it, every frame. */
  mix-blend-mode: normal;
  opacity: 0;
}

.pl-open.is-lite.is-spinning .pl-reel__speed {
  animation: pl-lite-streak var(--pl-spin, 6600ms) linear both;
}

@keyframes pl-lite-streak {
  0% { opacity: 0; }
  4% { opacity: .26; }
  30% { opacity: .14; }
  58%, 100% { opacity: 0; }
}

/* The frame keeps its glow and stops animating it: an inset shadow whose blur
   radius changes is a repaint of the frame and everything under it. The ends
   are faded by a painted overlay rather than a mask, on the same reasoning as
   the floor — this is the one mask in the room with moving content under it,
   which makes it the one that costs a surface per frame. */
.pl-open.is-lite .pl-reel-wrap {
  contain: paint;
  mask-image: none;
  background: #0d1017;
  box-shadow:
    inset 0 0 2.4rem color-mix(in srgb, var(--live) 45%, transparent),
    inset 0 0 2rem rgba(0, 0, 0, .55);
  transition: border-color .18s linear;
}

.pl-open.is-lite .pl-reel-wrap::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  background:
    linear-gradient(90deg, #0d1017, rgba(13, 16, 23, 0) 13%),
    linear-gradient(270deg, #0d1017, rgba(13, 16, 23, 0) 13%);
  pointer-events: none;
}

/* A 2.2rem halo on however many elite tiles the strip happens to hold, each
   of them repainted as it travels past. One tile can afford it: the one that
   stops under the marker, which keeps its own `is-hit` glow below. */
.pl-open.is-lite .pl-reel__tile.is-elite { box-shadow: none; }

/* --- The lock and the reveal -------------------------------------------
   The rattle is a transform, so it stays; it is just quantised, because
   nobody can see the difference between a case shaking at 60fps and one
   shaking at 3, and the phone can see the difference in everything else. */
.pl-open.is-lite .pl-charge__box {
  box-shadow: 0 0 20px color-mix(in srgb, var(--rarity) 32%, transparent);
  animation-duration: .68s;
  animation-timing-function: steps(2, end);
}

/* The reveal is a one-shot on a screen that has stopped moving, so it keeps
   all of it — the pop, the float, the shockwave, the flash. The sheen is the
   exception: it animates `background-position`, which is a repaint of the
   plate every frame for as long as the player sits looking at their prize. */
.pl-open.is-lite .pl-prize__art::after { display: none; }

/* Some of the glow survives the spin rather than snapping off with it. */
.pl-open.is-lite[data-stage="reveal"]::after { opacity: .5; }

/* The desktop reveal speeds the fan from 26s to 9s. Changing the duration of
   a running rotation keeps the elapsed time and recomputes the progress from
   it, which shows up as the fan jumping several degrees — affordable once on
   a desktop, not on the frame a phone is already spending on the reveal. */
.pl-open.is-lite[data-stage="reveal"] .pl-open__rays {
  opacity: .34;
  animation: pl-spin 34s linear infinite;
}

/* 320-360 px: «Frontline Player» рядом с четырьмя кнопками уже не помещается
   и лезет на них — на этой ширине бренд остаётся одним словом. */
@media (max-width: 380px) {
  .pl-top__brand-tail { display: none; }
  .pl-top__eyebrow .pl-age-mark { width: 1.5rem; height: 1.3rem; margin-left: .3rem; }
}


/* ===== Mobile performance profile ========================================
   The phone's copy of the cabinet: same screens, same controls, same arcade
   voice — minus the handful of layers a mid-range Android spends real frames
   on. Nothing here is a taste change. Every rule removes per-frame work, and
   where removing it would leave a hole, a static version of what it took goes
   back in its place.

   The predicate is the CSS half of the profile `caseTheatreLite()` already
   picks in player.js, so a phone gets one consistent cabinet rather than half
   of each. It is width *or* coarse pointer on purpose: a phone in landscape is
   still a phone, and a tablet has the same GPU budget at twice the width. */
@media (max-width: 900px), (pointer: coarse) {

  /* --- 1. The sticky bars ------------------------------------------------
     Nothing to switch off here any more: the console's bands went opaque for
     everyone when they were welded together — a translucent band shows the
     grid sliding under it, which is exactly the see-through the welding was
     there to remove. The frame budget that used to go on re-blurring a third
     of a phone screen every scrolled frame comes back for free. */

  /* Same argument over a scrim already dark enough to separate the sheet from
     the page on its own — and a modal blur is paid for on top of whatever the
     sheet itself is animating as it opens. */
  .pl-modal {
    background: color-mix(in srgb, var(--reset-shadow) 84%, transparent);
    backdrop-filter: none;
  }

  /* Both of these sit on artwork rather than on a panel, which is what the
     blur was buying them. Opaque grounds buy the same legibility for free. */
  .pl-modal__close {
    background: var(--reset-panel-raised);
    backdrop-filter: none;
  }

  .pl-art-zoom__chip {
    background: var(--reset-panel-raised);
    backdrop-filter: none;
  }

  /* --- 2. The wallet coin ------------------------------------------------
     The gleam animates `background-position`, which no compositor can take:
     the element repaints on the main thread every frame for as long as it is
     on screen. This one is the worst instance of it in the cabinet, because
     it is inside the sticky header — so its repaint was dragging the header's
     blur along with it — and because the wallet is on screen on every single
     tab, so it never stopped. Parked mid-sweep instead of removed: the coin
     keeps its struck highlight, it just stops repainting to move it. */
  .pl-coin--sm::after,
  .pl-coin--md::after,
  .pl-coin--lg::after,
  .pl-coin--xl::after {
    background-position: 50% 0;
    animation: none;
  }

  /* --- 3. The daily-reward card -----------------------------------------
     An animated `box-shadow` repaints the card and the whole bloom around it
     every frame, forever, on the one screen the player opens first. Frozen at
     the bright half of the cycle: the card still reads as the lit thing on
     the shelf, and the gift icon above it still beats — that one is a
     transform, which the compositor does own. */
  .pl-daily:not(.is-claimed) {
    box-shadow: var(--pl-lift), 0 0 0 6px rgba(255, 140, 40, .14), 0 0 40px -12px rgba(255, 140, 40, .75);
    animation: none;
  }

  /* --- 4. Layers that exist only while art loads -------------------------
     The store draws seventeen tiles at once, and every one of them was
     running a full-width sweep across its art box until its render arrived —
     seventeen composited layers, on the phone, at the exact moment it is
     already busy decoding seventeen images over a mobile connection. The
     sweep goes and the ring stays, on the same reasoning the reduced-motion
     block uses: the ring is a status, the sweep is decoration. */
  .pl-art.is-loading .pl-art__state::before { display: none; }

  /* --- 5. The elite sheen ------------------------------------------------
     A conic gradient turning behind every elite tile. The rotation is cheap
     per frame but not free per card: each one needs its own composited layer
     holding a rasterised conic gradient, and an inventory full of elites adds
     those up faster than a phone's compositor likes. Held still at the angle
     it looks best from, so the tile keeps its gloss and gives up only the
     turning. */
  .pl-item.is-elite .pl-item__art::before {
    transform: rotate(-42deg);
    animation: none;
  }

  /* --- 6. Off-screen tiles ----------------------------------------------
     A shelf grows to a hundred cards as the player scrolls, and every one of
     them keeps costing style, layout and paint whether or not it is anywhere
     near the screen. `content-visibility` lets the browser skip all three
     until a card is close to the viewport; the `auto` in each intrinsic size
     means the fallback below is only ever used before a card's first paint,
     after which the browser substitutes the height it actually had — so the
     scrollbar does not jump and the "load more" sentinel at the foot of the
     shelf stays where it belongs.

     Scoped to the three paginated grids by name. The cards that JS measures
     (the pass road, the mission rail) are deliberately not in this list. */
  #pl-inventory > .pl-item {
    content-visibility: auto;
    contain-intrinsic-size: auto 275px;
  }

  #pl-store > .pl-item {
    content-visibility: auto;
    contain-intrinsic-size: auto 381px;
  }

  #pl-avatar-store > .pl-item {
    content-visibility: auto;
    contain-intrinsic-size: auto 298px;
  }
}

/* ===== Заголовки групп в рельсе ==========================================
   Рельс держит те же четыре группы, что и телефонная панель, только
   развёрнутые в список. Подпись — не декорация: без неё одиннадцать
   одинаковых строк читаются как свалка, а с ней рельс и панель рассказывают
   одну историю. */

.pl-nav__group {
  margin: .85rem 0 .2rem;
  padding: 0 .65rem;
  color: var(--reset-muted);
  opacity: .72;
}

.pl-nav__group:first-child { margin-top: .35rem; }

/* ===== Переключатель внутри группы =======================================
   Лента под шапкой — то место, куда уехало «Ещё»: она отвечает на один вопрос,
   «эта кнопка внизу открывает вот эти экраны, ты сейчас на втором».

   Первая версия была прозрачной пилюлей с невидимыми вкладками — неактивная
   вкладка не имела ни фона, ни рамки и терялась в странице целиком. Поэтому
   здесь железка, а не строка ссылок: утопленный лоток на две ступени темнее
   доски, внутри — плашки во всю его ширину, и активная лежит в нём залитой,
   как переключатель колоды в аркаде. Три уровня навигации на этой странице
   теперь говорят одним языком: рельс, лента и фильтры `.pl-tab` — одна и та же
   ембер-заливка под «ты здесь».

   Лента липнет под мачтой, потому что на длинной полке переключаться нужно
   там, куда смотрит глаз, а не там, где начинается страница. И приварена к
   ней вплотную: раньше между мачтой и лентой стоял отступ в девять десятых
   рема, и на любой прокрутке в него был виден кусок уезжающей страницы —
   тонкая щель поперёк всего экрана. Лоток был карточкой, теперь он паз.

   И её нет на широком экране. Лента существует ровно потому, что телефонная
   панель прячет три экрана за одной кнопкой; рельс на столе перечисляет их
   отдельными строками под заголовком группы, и второй такой же переключатель
   в двадцати сантиметрах от первого — это не подсказка, а шум. Поэтому она
   появляется только там, где без неё до экрана не дойти. */

.pl-subnav {
  position: sticky;
  top: var(--pl-top-h, 4.6rem);
  z-index: 4;
  display: none;
  gap: .35rem;
  /* Ноль сверху и снизу: воздух под лентой отмеряет `.pl-view.is-active`, а
     всё, что здесь было бы margin'ом, стало бы щелью между полосами консоли. */
  margin-block: 0;
  padding-block: .38rem .42rem;
  border-bottom: var(--pl-border);
  background: var(--pl-groove);
  box-shadow: inset 0 3px 0 color-mix(in srgb, var(--reset-cream) 7%, transparent);
}

.pl-subnav[hidden] { display: none; }

.pl-subnav__tab {
  position: relative;
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  min-height: 46px;
  padding: .5rem 1.5rem;
  overflow: hidden;
  border: 2px solid transparent;
  /* Своя величина, а не вычитание из радиуса лотка: лоток стал полосой без
     скруглений, вычитать больше не из чего. */
  border-radius: var(--pl-radius-sm);
  background: transparent;
  color: var(--reset-muted);
  font: inherit;
  font-family: var(--heading-font-family);
  font-size: var(--fs-sm);
  font-weight: 900;
  letter-spacing: .03em;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  transition: color .15s ease, background-color .15s ease, box-shadow .15s ease;
}

.pl-subnav__tab i { font-size: .95em; opacity: .8; }

.pl-subnav__tab:hover {
  background: var(--reset-panel-raised);
  color: var(--reset-cream);
}

.pl-subnav__tab.is-active {
  background: var(--pl-ember-ramp);
  color: var(--pl-ember-ink);
  box-shadow: var(--pl-lift-sm), var(--pl-glow-soft);
}

.pl-subnav__tab.is-active i { opacity: 1; }

/* Тот же штрих, что на активной вкладке рельса: одна залитая плашка на две
   навигации — это одна навигация с двумя формами. */
.pl-subnav__tab.is-active::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--pl-hatch);
  opacity: .5;
  pointer-events: none;
}

.pl-subnav__tab > * { position: relative; }

.pl-subnav__tab:focus-visible {
  outline: 3px solid var(--reset-ink-sun);
  outline-offset: 2px;
}

/* Магазин — золотой всегда, и это не то же самое, что «ты здесь». Цвет
   говорит «здесь покупают», заливка — «ты тут стоишь». В первой версии
   вкладка меняла цвет при входе, и это читалось как две разные кнопки. */
.pl-subnav__tab--buy {
  border-color: color-mix(in srgb, var(--pl-gold) 68%, transparent);
  background: color-mix(in srgb, var(--pl-gold) 14%, transparent);
  color: var(--reset-cream);
}

.pl-subnav__tab--buy i { color: var(--pl-gold-deep); opacity: 1; }

.pl-subnav__tab--buy:hover {
  background: color-mix(in srgb, var(--pl-gold) 26%, transparent);
  color: var(--reset-cream);
}

.pl-subnav__tab--buy.is-active {
  border-color: transparent;
  background: linear-gradient(135deg, #ffdd6e 0%, var(--pl-flare) 55%, #f08c00 100%);
  color: var(--pl-ember-ink);
  box-shadow: var(--pl-lift-sm);
}

.pl-subnav__tab--buy.is-active i { color: inherit; }

.pl-subnav__tab .pl-nav__badge { margin-left: .1rem; }

/* ===== Главное ===========================================================
   Домашний экран мобильной аркады: сверху — кто ты, посередине — одна кнопка
   размером с ладонь, под ней — плитки в соседние окна. Порядок не случайный:
   игрок открывает кабинет, чтобы зайти в шлем, и всё остальное на этом экране
   стоит ниже той кнопки, ради которой он пришёл. */

/* Сетка вешается на активное состояние, а не на сам экран: `.pl-view` прячет
   неактивные панели через `display: none`, и голое `.pl-home { display: grid }`
   с той же специфичностью ниже по файлу перебивало бы это правило — главный
   экран остался бы видимым поверх любого другого. */
.pl-view.pl-home.is-active {
  display: grid;
  gap: 1rem;
}

.pl-home {
  max-width: 62rem;
  margin-inline: auto;
}

.pl-headsets { margin-top: 1.25rem; scroll-margin-top: 7rem; }
.pl-headsets__heading, .pl-headsets__footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: .8rem;
}
.pl-headsets__heading { margin-bottom: .6rem; }
.pl-headsets__heading h2 { min-width: 0; overflow-wrap: anywhere; }
.pl-headsets__count {
  display: inline-block;
  margin-left: .3rem;
  padding: .1rem .5rem;
  border-radius: var(--pl-radius-sm);
  background: var(--pl-groove);
  color: var(--reset-muted);
  font-variant-numeric: tabular-nums;
}
.pl-headsets__list { list-style: none; padding: 0; margin: .8rem 0; }
.pl-headsets__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: .8rem;
  padding: .9rem 0;
  border-top: 1px solid var(--reset-line);
}
.pl-headsets__row strong, .pl-headsets__row small { display: block; overflow-wrap: anywhere; }
.pl-headsets__row small, .pl-headsets__footer small { color: var(--reset-muted); font-size: var(--fs-xs); }
.pl-headsets__row small { margin-top: .2rem; }
.pl-headsets__footer small { flex: 1 1 15rem; }
.pl-headsets .pl-headsets__status:not(:empty) { margin: .8rem 0; }
.pl-headsets .pl-btn { min-height: 44px; white-space: normal; }
.pl-headsets .pl-btn:focus-visible, #pl-headsets-title:focus-visible {
  outline: 3px solid var(--pl-ember);
  outline-offset: 4px;
}
@media (max-width: 600px) {
  .pl-headsets { padding: 1rem; }
  .pl-headsets__row { grid-template-columns: minmax(0, 1fr); }
  .pl-headsets__row .pl-btn, .pl-headsets__footer .pl-btn { width: 100%; }
}

/* Карточка игрока. Целиком кнопка: в профиль ведёт весь блок, а не стрелка в
   углу — на телефоне промахнуться по стрелке проще, чем попасть. */
.pl-home__card {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: .9rem;
  padding: .7rem .9rem;
  border: var(--pl-border);
  border-radius: var(--pl-radius);
  background: var(--reset-panel);
  color: var(--reset-cream);
  font: inherit;
  text-align: left;
  box-shadow: var(--pl-lift-sm);
  cursor: pointer;
  transition: transform .15s ease, box-shadow .15s ease;
}

.pl-home__card:hover {
  box-shadow: var(--pl-lift);
  transform: translate(-1px, -1px);
}

.pl-home__card:active {
  box-shadow: 1px 1px 0 var(--reset-shadow);
  transform: translate(2px, 2px);
}

.pl-home__card:focus-visible {
  outline: 3px solid var(--reset-ink-sun);
  outline-offset: 3px;
}

.pl-portrait--xs {
  width: clamp(3.4rem, 14vw, 4.4rem);
  border-radius: var(--pl-radius-sm);
  box-shadow: var(--pl-lift-sm);
  cursor: inherit;
}

.pl-portrait--xs .pl-portrait__plate { font-size: 1.35rem; }

.pl-portrait--xs .pl-portrait__level {
  right: .2rem;
  bottom: .2rem;
  gap: .18rem;
  padding: .08rem .32rem;
  border-width: 1.5px;
  font-size: .58rem;
}

.pl-home__card-body {
  display: grid;
  gap: .22rem;
  min-width: 0;
}

.pl-home__card-name {
  overflow: hidden;
  font-family: var(--heading-font-family);
  font-size: var(--fs-md);
  font-weight: 900;
  line-height: 1.1;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pl-home__card-rank {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  color: var(--reset-muted);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-2xs);
  letter-spacing: .06em;
  text-transform: uppercase;
}

.pl-home__card-meter {
  display: block;
  height: 8px;
  overflow: hidden;
  border: 1.5px solid var(--reset-line-strong);
  border-radius: 999px;
  background: color-mix(in srgb, var(--reset-cream) 8%, transparent);
}

.pl-home__card-meter > span {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--pl-ember-ramp);
  transition: width .5s cubic-bezier(.2, .8, .2, 1);
}

.pl-home__card-hint {
  color: var(--reset-muted);
  font-size: var(--fs-2xs);
}

.pl-home__card-go { color: var(--reset-muted); }
.pl-home__card:hover .pl-home__card-go { color: var(--pl-ember); }

/* Сцена под кнопку. Всё, что здесь есть, кроме самой кнопки, — свет и одна
   строка подписи: у главного действия не должно быть соседей. */
.pl-home__stage {
  position: relative;
  display: grid;
  justify-items: center;
  gap: .55rem;
  padding: 1.6rem 1.1rem 1.35rem;
  overflow: hidden;
  border: var(--pl-border);
  border-radius: var(--pl-radius);
  background:
    var(--pl-hatch),
    radial-gradient(120% 90% at 50% 0%, color-mix(in srgb, var(--reset-sky) 22%, transparent), transparent 70%),
    var(--reset-panel);
  box-shadow: var(--pl-lift);
  text-align: center;
}

.pl-home__glow {
  position: absolute;
  top: -40%;
  left: 50%;
  width: min(30rem, 120%);
  aspect-ratio: 1;
  background: radial-gradient(circle, color-mix(in srgb, var(--reset-sky) 34%, transparent), transparent 62%);
  transform: translateX(-50%);
  pointer-events: none;
}

.pl-home__kicker {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  margin: 0;
  color: var(--reset-muted);
}

/* Кнопка запуска. Синяя, как ключ в панели снизу: это одно и то же действие,
   и игрок должен узнавать его по цвету раньше, чем прочтёт надпись. */
.pl-home__key {
  position: relative;
  display: grid;
  justify-items: center;
  gap: .1rem;
  width: min(100%, 26rem);
  padding: 1.05rem 1.4rem 1.1rem;
  border: 3px solid var(--reset-tile-ink);
  border-radius: var(--pl-radius);
  background: var(--reset-sky);
  color: var(--reset-tile-ink);
  font: inherit;
  box-shadow: var(--pl-lift-hi);
  cursor: pointer;
  transition: transform .15s ease, box-shadow .15s ease, background-color .15s ease;
}

.pl-home__key i { font-size: 1.9rem; }

.pl-home__key b {
  font-family: var(--heading-font-family);
  font-size: var(--fs-xl);
  font-weight: 900;
  letter-spacing: .04em;
  line-height: 1.05;
  text-transform: uppercase;
}

.pl-home__key small {
  font-size: var(--fs-2xs);
  font-weight: 700;
  opacity: .78;
}

.pl-home__key:hover {
  background: color-mix(in srgb, var(--reset-sky) 85%, white);
  box-shadow: 10px 10px 0 var(--reset-shadow);
  transform: translate(-2px, -2px);
}

.pl-home__key:active {
  box-shadow: 2px 2px 0 var(--reset-shadow);
  transform: translate(4px, 4px);
}

.pl-home__key:focus-visible {
  outline: 3px solid var(--pl-ember);
  outline-offset: 4px;
}

.pl-home__stage-note {
  position: relative;
  max-width: 34ch;
  margin: 0;
  color: var(--reset-muted);
  font-size: var(--fs-xs);
}

/* Плитки-виджеты. Три штуки одинакового веса: это входы, а не реклама. */
.pl-home__widgets {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: .7rem;
}

.pl-tile {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: .8rem;
  padding: .85rem .95rem;
  border: var(--pl-border);
  border-radius: var(--pl-radius-sm);
  background: var(--reset-panel);
  color: var(--reset-cream);
  font: inherit;
  text-align: left;
  box-shadow: var(--pl-lift-sm);
  cursor: pointer;
  transition: transform .15s ease, box-shadow .15s ease;
}

.pl-tile:hover { box-shadow: var(--pl-lift); transform: translate(-1px, -1px); }
.pl-tile:active { box-shadow: 1px 1px 0 var(--reset-shadow); transform: translate(2px, 2px); }

.pl-tile:focus-visible {
  outline: 3px solid var(--reset-ink-sun);
  outline-offset: 3px;
}

/* Восьмиугольная плашка под иконкой — тот же срез, что у ключа в панели: на
   этом экране скошенный угол означает «это железка, её можно нажать». */
.pl-tile__icon {
  display: grid;
  place-items: center;
  width: 2.7rem;
  height: 2.7rem;
  background: var(--pl-ember-ramp);
  clip-path: polygon(18% 0, 82% 0, 100% 26%, 100% 74%, 82% 100%, 18% 100%, 0 74%, 0 26%);
  color: var(--pl-ember-ink);
  font-size: 1.05rem;
}

.pl-tile--arenas .pl-tile__icon {
  background: linear-gradient(135deg, #7ad0ff 0%, var(--reset-sky) 60%, #1d6ea8 100%);
  color: var(--reset-tile-ink);
}

.pl-tile--promos .pl-tile__icon {
  background: linear-gradient(135deg, #ffdd6e 0%, var(--pl-flare) 55%, #f08c00 100%);
}

.pl-tile__text { display: grid; gap: .12rem; min-width: 0; }

.pl-tile__text b {
  font-family: var(--heading-font-family);
  font-size: var(--fs-sm);
  font-weight: 900;
  letter-spacing: .03em;
  text-transform: uppercase;
}

.pl-tile__text small {
  color: var(--reset-muted);
  font-size: var(--fs-2xs);
  line-height: 1.35;
}

.pl-tile__value {
  color: var(--pl-ember);
  font-family: "JetBrains Mono", monospace;
  font-size: var(--fs-md);
  font-style: normal;
  font-weight: 700;
}

.pl-tile__value:empty { display: none; }

/* Хвост: то, что раньше жило в подвале рельса и на телефоне не помещалось
   никуда. Это служебные ссылки, поэтому они тихие и стоят последними. */
.pl-home__rest {
  display: flex;
  flex-wrap: wrap;
  gap: .45rem;
  padding-top: .35rem;
  border-top: 1px solid var(--reset-line);
}

.pl-home__rest-item {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  padding: .5rem .85rem;
  border: 1px solid transparent;
  border-radius: 999px;
  background: transparent;
  color: var(--reset-muted);
  font: inherit;
  font-size: var(--fs-xs);
  font-weight: 700;
  text-decoration: none;
  cursor: pointer;
  transition: background-color .15s ease, color .15s ease;
}

.pl-home__rest-item:hover {
  background: var(--reset-panel-raised);
  color: var(--reset-cream);
}

.pl-home__rest-item:focus-visible {
  outline: 3px solid var(--reset-ink-sun);
  outline-offset: 2px;
}

/* «Установлено» — не ошибка, а результат, поэтому кнопка гаснет, а не исчезает:
   игрок должен видеть, что кабинет уже стоит на его экране. */
.pl-home__rest-item:disabled {
  opacity: .5;
  cursor: default;
}

/* ===== Виджет пропуска на экране заданий =================================
   Пропуск — отдельный экран, но живёт он рядом с заданиями, и одна полоса
   сверху объясняет это быстрее любой подписи: печать сезона слева, шкала
   уровня посередине, следующая награда справа, и весь блок — одна кнопка.
   Полоса намеренно не похожа на карточку задания: другой фон, другой ритм,
   другая форма — чтобы её не приняли за ещё одну цель. */

.pl-pass-widget {
  display: block;
  margin: 0 0 1.1rem;
}

.pl-pass-widget[hidden] { display: none; }

.pl-pass-widget__button {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 1rem;
  width: 100%;
  padding: .85rem 1rem;
  overflow: hidden;
  border: var(--pl-border);
  border-radius: var(--pl-radius);
  background:
    var(--pl-hatch),
    linear-gradient(115deg,
      color-mix(in srgb, var(--pl-flare) 26%, var(--reset-panel)) 0%,
      var(--reset-panel) 58%);
  color: var(--reset-cream);
  font: inherit;
  text-align: left;
  box-shadow: var(--pl-lift);
  cursor: pointer;
  transition: transform .15s ease, box-shadow .15s ease;
}

.pl-pass-widget__button:hover { box-shadow: var(--pl-lift-hi); transform: translate(-2px, -2px); }
.pl-pass-widget__button:active { box-shadow: 2px 2px 0 var(--reset-shadow); transform: translate(2px, 2px); }

.pl-pass-widget__button:focus-visible {
  outline: 3px solid var(--reset-ink-sun);
  outline-offset: 3px;
}

.pl-pass-widget__seal {
  display: grid;
  place-items: center;
  width: 3.4rem;
  height: 3.4rem;
  border: 2px solid var(--reset-cream);
  border-radius: 50%;
  background: var(--pl-ember-ramp);
  color: var(--pl-ember-ink);
  line-height: 1;
  box-shadow: var(--pl-lift-sm);
}

.pl-pass-widget__seal b {
  font-family: var(--heading-font-family);
  font-size: 1.25rem;
  font-weight: 900;
}

.pl-pass-widget__seal small {
  font-family: "JetBrains Mono", monospace;
  font-size: .52rem;
  letter-spacing: .12em;
}

/* Печать без уровня — пока сезон не приехал: медаль вместо числа, чтобы
   полоса не меняла размер, когда число появится. */
.pl-pass-widget__seal i { font-size: 1.2rem; }

.pl-pass-widget__body { display: grid; gap: .3rem; min-width: 0; }

.pl-pass-widget__kicker {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  margin: 0;
  color: var(--reset-muted);
}

.pl-pass-widget__title {
  margin: 0;
  overflow: hidden;
  font-family: var(--heading-font-family);
  font-size: var(--fs-md);
  font-weight: 900;
  letter-spacing: .02em;
  line-height: 1.1;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pl-pass-widget__meter {
  display: block;
  height: 9px;
  overflow: hidden;
  border: 1.5px solid var(--reset-line-strong);
  border-radius: 999px;
  background: color-mix(in srgb, var(--reset-cream) 8%, transparent);
}

.pl-pass-widget__meter > span {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--pl-ember-ramp);
}

.pl-pass-widget__note {
  margin: 0;
  color: var(--reset-muted);
  font-size: var(--fs-2xs);
}

/* Следующая награда: не подпись, а сама картинка — за ней и идут. */
.pl-pass-widget__next {
  display: grid;
  justify-items: center;
  gap: .25rem;
  width: 4.6rem;
}

.pl-pass-widget__art {
  display: grid;
  place-items: center;
  width: 4.6rem;
  height: 4.6rem;
  overflow: hidden;
  border: 2px solid var(--reset-line-strong);
  border-radius: var(--pl-radius-sm);
  background: var(--pl-plate);
}

.pl-pass-widget__art img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.pl-pass-widget__art .pl-coin { font-size: 2.2rem; }

.pl-pass-widget__next span {
  color: var(--reset-muted);
  font-family: "JetBrains Mono", monospace;
  font-size: .58rem;
  letter-spacing: .08em;
  text-align: center;
  text-transform: uppercase;
}

/* Есть что забрать — полоса перестаёт быть справкой и становится задачей. */
.pl-pass-widget__cta {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding: .55rem 1rem;
  border: 2px solid transparent;
  border-radius: 999px;
  background: var(--pl-ember-ramp);
  color: var(--pl-ember-ink);
  font-family: var(--heading-font-family);
  font-size: var(--fs-xs);
  font-weight: 900;
  letter-spacing: .03em;
  text-transform: uppercase;
  box-shadow: var(--pl-lift-sm);
}

.pl-pass-widget__go { color: var(--reset-muted); font-size: 1rem; }

.pl-pass-widget.is-ready .pl-pass-widget__button {
  border-color: var(--pl-ember);
  box-shadow: var(--pl-lift), 0 0 22px -4px color-mix(in srgb, var(--pl-ember) 70%, transparent);
}

/* ===== Телефон ===========================================================
   Ниже 900px лента и главный экран перестраиваются под палец. */

@media (max-width: 900px) {
  /* Здесь лента и включается — вместе с телефонной панелью, которая прячет
     группу за одной кнопкой. Лоток забирает всю ширину, а плашки делят её
     поровну: три равные клавиши читаются как один переключатель, а не как три
     ссылки, сбежавшиеся к левому краю. */
  .pl-subnav {
    display: flex;
    gap: .3rem;
    padding-block: .3rem .34rem;
  }

  .pl-subnav__tab {
    flex: 1 1 0;
    min-width: 0;
    min-height: 44px;
    gap: .4rem;
    padding: .45rem .35rem;
    font-size: .72rem;
    letter-spacing: .02em;
  }

  .pl-subnav__tab i { font-size: .9rem; }
  .pl-subnav__tab span { overflow: hidden; text-overflow: ellipsis; }

  .pl-home { gap: .75rem; }

  .pl-home__stage { padding: 1.2rem .8rem 1rem; }

  .pl-home__key { padding: .9rem 1rem .95rem; }
  .pl-home__key i { font-size: 1.6rem; }

  .pl-home__widgets { grid-template-columns: minmax(0, 1fr); gap: .5rem; }

  .pl-pass-widget__button {
    grid-template-columns: auto minmax(0, 1fr) auto;
    gap: .7rem;
    padding: .7rem .75rem;
  }

  /* Кнопка «забрать» на телефоне уезжает под шкалу во всю ширину: в четвёртой
     колонке она отнимала у названия сезона половину строки. */
  .pl-pass-widget__cta {
    grid-column: 1 / -1;
    justify-content: center;
  }

  .pl-pass-widget__go { display: none; }

  .pl-pass-widget__next,
  .pl-pass-widget__art { width: 3.6rem; }
  .pl-pass-widget__art { height: 3.6rem; }
}

/* Панель полки липнет под лентой, а не на её место. На широком экране ленты
   нет, и полка возвращается прямо под мачту — поэтому слагаемое живёт здесь,
   а не в базовом правиле.

   Нижняя граница у запроса не для красоты. На телефоне панель вообще не
   липкая — она едет со страницей, — и это `top` перебивало там `top: auto`
   просто потому, что стояло ниже по файлу: панель уезжала на сто восемьдесят
   точек вниз и ложилась поверх первого ряда карточек, а на её месте в потоке
   оставалась дыра во весь этот отступ. Правило теперь живёт ровно в той
   полосе ширин, где панель действительно липнет. */
@media (min-width: 621px) and (max-width: 900px) {
  .pl-board { top: calc(var(--pl-top-h, 4.6rem) + var(--pl-subnav-h, 0px)); }
}

@media (max-width: 400px) {
  .pl-subnav__tab { gap: .3rem; padding-inline: .25rem; font-size: .66rem; }
  .pl-home__card { gap: .65rem; padding: .6rem .7rem; }
}


/* ===== Reduced motion ====================================================
   The reel still runs — it is the mechanism, not decoration — but JS cuts it
   to a fraction of a second. Everything purely decorative stops here. */

@media (prefers-reduced-motion: reduce) {
  .pl-case .pl-item__art .pl-art,
  .pl-item.is-elite .pl-item__art::before,
  .pl-inspect__art.is-elite::before,
  /* The shimmer goes; the ring stays. It is the only thing on screen saying
     the render is still on its way, and a status is not decoration. */
  .pl-art.is-loading .pl-art__state::before,
  .pl-open__rays,
  .pl-open__rays::after,
  .pl-open__orb,
  .pl-open__grid,
  .pl-open__dust span,
  .pl-open__stage,
  .pl-charge__box,
  .pl-charge__meter span,
  .pl-open__beam,
  .pl-open__shock,
  .pl-open__flash,
  .pl-open__sub,
  .pl-prize__art,
  .pl-prize__art::after,
  .pl-prize h3,
  .pl-prize__rarity,
  .pl-prize__owned,
  .pl-open__actions,
  .pl-open.is-crawl .pl-reel__marker,
  .pl-open.is-land .pl-reel-wrap,
  .pl-reel__tile.is-hit,
  .pl-confetti span { animation: none; }

  /* Reduced motion also lands on the phone's profile, and every rule in it is
     more specific than the list above — so the room it rebuilt has to be
     switched off by name here rather than by the shorthands that stop the
     desktop one. */
  .pl-open.is-lite .pl-open__orb,
  .pl-open.is-lite .pl-open__grid::before,
  .pl-open.is-lite .pl-charge__box,
  .pl-open.is-lite.is-spinning::after,
  .pl-open.is-lite.is-spinning .pl-open__rays,
  .pl-open.is-lite[data-stage="reveal"] .pl-open__rays,
  .pl-open.is-lite.is-spinning .pl-reel__halo,
  .pl-open.is-lite.is-spinning .pl-reel__speed,
  .pl-open.is-lite.is-spinning .pl-reel__marker::before,
  .pl-open.is-lite.is-spinning .pl-reel__marker::after { animation: none; }

  .pl-open.is-lite .pl-open__sky { transition: none; }

  .pl-open.is-jackpot .pl-open__stage { animation: none; }
  .pl-confetti { display: none; }
  .pl-open__dust { display: none; }
  /* The strip still moves — it is the mechanism — but nothing is smeared and
     nothing strobes on top of it. */
  .pl-reel { filter: none; }
  .pl-reel__speed,
  .pl-open.is-lite .pl-reel__speed { display: none; }
  .pl-open[data-stage="reveal"] .pl-open__beam { opacity: .38; transform: translateX(-50%); }

  /* The zoom still zooms — that is the feature — it just arrives at the new
     scale instead of gliding there. */
  .pl-zoom__stage { animation: none; }
  .pl-zoom__plate { transition: none; }

  .pl-view.is-active { animation: none; }
  .pl-boot__mark { animation: none; }
  .pl-item__art::after { transition: none; }
  .pl-item:hover .pl-item__art img { transform: none; }
  /* The picture still appears when it lands, it just does not travel there. */
  .pl-art .pl-art__img { transform: none; transition: opacity .2s ease; }
  .pl-portrait:hover .pl-portrait__plate img { transform: none; }
  .pl-portrait__pip { transition: none; }
  .pl-avatar-option:hover img,
  .pl-avatar-option.is-selected img { transform: none; }
  .pl-avatar-preview__plate img { animation: none; }
  .pl-mix > span { transition: none; }

  .pl-btn,
  .pl-icon-btn,
  .pl-slot__get,
  .pl-item,
  .pl-stat,
  .pl-avatar-option,
  .pl-avatar-option__check,
  .pl-subnav__tab,
  .pl-home__card,
  .pl-home__card-meter > span,
  .pl-home__key,
  .pl-tile,
  .pl-pass-widget__button { transition: none; }

  /* The currency stops shining, breathing and flipping. The one thing kept is
     the floating delta — it is the only feedback that says how much the
     balance just moved, so it fades in place instead of travelling. */
  .pl-coin--sm::after,
  .pl-coin--md::after,
  .pl-coin--lg::after,
  .pl-coin--xl::after,
  .pl-earn__sheen,
  .pl-daily__gift { animation: none; }

  .pl-coin.is-struck svg { animation: none; }
  .pl-daily:not(.is-claimed) { animation: none; }
  .pl-wallet.is-gain,
  .pl-wallet.is-spend,
  .pl-wallet.is-gain .pl-wallet__ring { animation: none; }
  .pl-wallet { transition: none; }
  .pl-wallet__delta.is-live { animation: pl-wallet-hold 1.5s ease forwards; }

  @keyframes pl-wallet-hold {
    0%, 78% { opacity: 1; transform: translate(-50%, -1.4rem); }
    100% { opacity: 0; transform: translate(-50%, -1.4rem); }
  }
}
