/* ============================================================
   SkinSignal / shell
   THE DASHBOARD'S FRAME. Loaded by dashboard.html only, which is the
   whole of its scope: every rule here is inert on index.html and on every
   other page, because no other page links this file and css/style.css is
   not touched.

   WHAT THIS IS. The dashboard stops being a page of stacked full width
   sections and becomes an app: a rail down the left, and a twelve column
   grid to the right of it. This stage builds the frame only. Every
   section still spans all twelve columns, so the page reads as it did;
   the modules are placed in a later stage.

   THE RAIL IS THE TOP BAR. The header in dashboard.html is not hidden and
   nothing moves out of it. Above 1024px the same element is laid out as a
   vertical rail: the wordmark at the top, the nav under it, and the
   currency, the upgrade affordance and the account menu pushed to the
   foot. Below 1024px not one rule here applies, so the bar is exactly the
   bar css/style.css already draws, which is what makes the collapse work
   with no JavaScript in it. One DOM, two layouts, and every id, class,
   href and aria attribute in the same node it was in.

   THE SHARED CONTAINER. .container carries the site's max width and
   gutters and is used by every page, so it is left alone. Inside the
   dashboard's main the shell neutralises it (the grid owns the measure
   there and the padding is the grid's), and that override is scoped to
   main on this page. The footer keeps its own container untouched.
   ============================================================ */

/* OUTSIDE THE BREAKPOINT, and the one rule here that is not frame.
   css/flex.css sizes the hero's ambient as a share of the hero and lets it
   spill past it. In a column narrower than the old full width page that
   spill reaches past the viewport and the page scrolls sideways. Capped to
   the hero's own width, which the glow is a whisper inside anyway. Written
   here because this stage does not touch css/flex.css. */
.statushero::before { width: min(1240px, 100%); }

@media (min-width: 1024px) {
  :root {
    --rail: 200px;
    --shell-pad: clamp(20px, 2.2vw, 36px);
    --shell-max: 1560px;
  }

  /* ---------- 1. The rail ---------- */

  /* Fixed, so the nav and the account controls stay put while the grid
     scrolls, and scrollable itself for a short viewport. z-index stays at
     the 40 style.css gives it, which keeps it under the item detail (90)
     and the reveal (120). */
  .topbar {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    width: var(--rail);
    overflow-y: auto;
    overscroll-behavior: contain;
    background: var(--surface-panel, #0e1016);
    border-bottom: 0;
    border-right: 1px solid rgba(255, 255, 255, 0.055);
  }

  .topbar__inner {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 20px;
    height: 100%;
    max-width: none;
    padding: 26px 16px 20px;
  }

  /* The action group dissolves so the currency, the nav and the account
     control can be ordered one by one. css/style.css uses the same device
     under 900px, so this is the page's own idiom rather than a new one. */
  .topbar__actions { display: contents; }

  /* THE SHARED HEADER, AS A RAIL. css/header.css draws the bar every
     other page has; here the same markup stands on end. Its bar only
     pieces step aside: the floor hairline, the scrolled glass, the phone
     menu and its button, the line under the active link, the hairline
     between Upgrade and the account button. The nav and the account side
     stay shown at every rail width (the bar moves them into its menu
     below 1180px; the rail has room for them). */
  .topbar::after,
  .topbar__burger,
  .topbar__sheet,
  .auth__rule,
  .nav__link--active::before,
  .nav__link--active::after { display: none; }
  .topbar.is-scrolled {
    background: var(--surface-panel, #0e1016);
    -webkit-backdrop-filter: none;
            backdrop-filter: none;
    box-shadow: none;
  }
  .topbar .nav { display: flex; }
  .topbar .auth { display: flex; }

  .wordmark { order: 0; }
  .topbar .wordmark__img { width: 100%; height: auto; }

  .nav {
    order: 1;
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
  }

  /* A row in a list rather than a link in a line: the active mark moves
     from an underline to the row itself, which is what reads as "you are
     here" in a rail. */
  .nav__link {
    height: auto;
    padding: 9px 10px;
    border-bottom: 0;
    border-radius: 8px;
    font-size: 13px;
  }

  .nav__link:hover { background: rgba(255, 255, 255, 0.04); }

  .nav__link--active {
    background: rgba(255, 255, 255, 0.06);
    box-shadow: inset 2px 0 0 var(--accent);
  }

  /* The spacer: everything below it sits at the foot of the rail. */
  .currency { order: 2; margin-top: auto; }
  .auth { order: 3; flex-wrap: wrap; gap: 8px; }
  .auth__tier--upgrade,
  .auth__signin { width: 100%; justify-content: center; }
  .auth__user { width: 100%; border-radius: 10px; }
  .auth__name { max-width: none; flex: 1; text-align: left; }

  .currency__button { width: 100%; justify-content: space-between; }

  /* At the foot of a rail a menu has to open upward, and it cannot be
     wider than the rail that clips it. */
  .currency__menu,
  .auth__menu {
    top: auto;
    bottom: calc(100% + 6px);
    left: 0;
    right: 0;
    width: auto;
    min-width: 0;
  }

  /* ---------- 2. The grid ----------
     Twelve columns to the right of the rail, capped so the dashboard does
     not stretch across an ultrawide, and centred in what is left beside
     the rail rather than in the viewport. The padding does the centring:
     its minimum is the frame's own padding and it grows to half of
     whatever is spare, which needs no wrapper element. */
  main {
    margin-left: var(--rail);
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: clamp(14px, 1.2vw, 20px);
    align-content: start;
    /* A MODULE IS AS TALL AS WHAT IS IN IT. Stretching every module to its
       row was what filled the grid with half empty boxes: the content sat
       at the top and the rest was void. Ragged row bottoms are the honest
       shape of a page whose panels hold different amounts. A module opts
       back into the row's height with .mod--fill, and only earns it if its
       content can genuinely use the space. */
    align-items: start;
    padding-block: var(--shell-pad);
    /* THE RAIL IS PART OF THE SUM. A percentage padding resolves against
       the CONTAINING BLOCK, which is the body at the full viewport width,
       not against main's own box, and main is already pushed right by the
       rail. Without the rail in the formula the centring took
       (viewport - max) / 2 a side out of a box that was already the rail
       narrower, and at 1920 the content came out at 1328 instead of the
       1560 the cap names. */
    padding-inline: max(var(--shell-pad), (100% - var(--rail) - var(--shell-max)) / 2);
  }

  /* STAGE 1 HOLDS EVERY SECTION FULL WIDTH. The columns exist and are
     measurable, but nothing is placed into them yet: this stage proves
     the frame, and the modules are laid out in stage 3. */
  main > * {
    grid-column: 1 / -1;
    min-width: 0;
  }

  /* The grid owns the measure and the gutters inside main, so the shared
     container stops setting both. Scoped to main: the footer's container,
     and every other page's, is untouched. */
  main .container {
    max-width: none;
    padding-inline: 0;
  }

  /* The footer sits in the same column as the grid, so it clears the rail
     and keeps its own container and gutters. */
  .footer { margin-left: var(--rail); }
}

@media (min-width: 1280px) {
  :root { --rail: 232px; }
}

/* ============================================================
   2. THE MODULE
   One component for every panel the grid holds. Stage 3 places modules;
   it does not style them. The point of a single component is that twelve
   panels read as one page rather than as twelve decisions: a module has
   one surface, one padding, one set of spans, and three weights.

   WEIGHT, not bespoke styling, is how hierarchy is made here. A page has
   one .mod--hero, several plain .mod, and as many .mod--quiet as it needs
   for the things that support rather than speak.
   ============================================================ */

:root {
  /* One step under the panel, between it and the floor, so a quiet module
     recedes instead of sitting level with the ones that matter. */
  --surface-quiet: #0a0c11;

  /* THE ROW. Grid rows are set rather than grown from content, so a row of
     modules lines up and stage 3 can reason about height. 160px is the
     smallest module that still reads as one: a label, a figure at 24px and
     a caption, inside the module's own padding. Anything shorter is a
     chip, not a module. A row grows past it when its content needs more. */
  --mod-row: 160px;
}

.mod {
  --mod-pad: clamp(16px, 1.4vw, 24px);

  display: flex;
  flex-direction: column;
  padding: var(--mod-pad);
  border: 1px solid var(--edge);
  border-radius: 18px;
  background: var(--surface-panel, #0e1016);
  box-shadow:
    inset 0 1px 0 var(--edge-top),
    0 1px 2px rgba(0, 0, 0, 0.5),
    0 12px 32px -16px rgba(0, 0, 0, 0.9);
}

/* A grid child's min-width is auto, which means one unbroken 60 character
   item name makes the column wider than its share and the whole row
   overflows. Every level of a module is allowed to shrink, and its text is
   allowed to break, so a name can never widen the grid. */
.mod,
.mod__head,
.mod__body,
.mod__foot { min-width: 0; }

/* The same trap one level down. A nested flex row (a section's own head, a
   list item) is a flex item too, and its min-width auto is what pushes a
   long line past a narrow column. :where keeps this at zero specificity,
   so anything that genuinely needs a width still wins. */
.mod :where(div, section, header, footer, p, dl, ul, ol, li, span, a) { min-width: 0; }

/* And the backstop: nothing escapes a module's surface. clip rather than
   hidden, because clip creates no scroll container, and the page's only
   sticky element is the rail, which is not in a module. */
.mod { overflow: clip; }

.mod__head,
.mod__body,
.mod__foot { overflow-wrap: anywhere; }

/* ---------- The parts, all optional ---------- */

.mod__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

/* Only when something follows it: a head with nothing under it is the
   module, and should not hold a margin against the padding. */
.mod__head:not(:last-child) { margin-bottom: clamp(12px, 1vw, 16px); }

.mod__label {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.5);
}

.mod__meta { font-size: 11.5px; color: rgba(255, 255, 255, 0.55); }

.mod__body { flex: 1 1 auto; }

/* Pinned under the body, so a CTA sits at the foot of a tall module
   instead of floating under short content. */
.mod__foot {
  margin-top: clamp(12px, 1vw, 16px);
  padding-top: clamp(12px, 1vw, 16px);
  border-top: 1px solid var(--edge);
}

/* Full bleed, for art and charts that should touch the sides. The head and
   the foot keep their inset, because type never bleeds. */
.mod--flush { padding: 0; }
.mod--flush > .mod__head { padding: var(--mod-pad) var(--mod-pad) 0; }
.mod--flush > .mod__foot { padding: 0 var(--mod-pad) var(--mod-pad); }

/* ---------- The three weights ---------- */

/* The lead. One per page. The page's ambient light already sits behind the
   status hero (css/flex.css), so this adds depth rather than a second
   glow: a wider, deeper shadow and a larger radius. */
.mod--hero {
  border-radius: 22px;
  box-shadow:
    inset 0 1px 0 var(--edge-top),
    0 2px 4px rgba(0, 0, 0, 0.55),
    0 28px 64px -24px rgba(0, 0, 0, 0.95);
}

/* The supporting module: darker than the panel it sits among, and flat, so
   it reads as ground rather than as an object. */
.mod--quiet {
  background: var(--surface-quiet);
  box-shadow: none;
}

/* A module is the section now, so `hidden` hides the module itself. The one
   exception is the close, which is wrapped because a test pins its class
   attribute as a literal: that wrapper follows its section. Named exactly,
   because the desk module holds a head that is hidden for a member while
   the module itself stays. */
.mod:has(> .dash-membership[hidden]) { display: none; }

/* THE MODULE MUST STILL OBEY `hidden`. .mod sets display: flex, which is a
   class rule and therefore beats the browser's own [hidden] { display: none }.
   Without this, every section js/app.js hides (the hero, the arena, the case
   and the close for a member) would still draw as an empty panel. */
.mod[hidden] { display: none; }

/* A module filled by js/app.js and left empty (a member, a page that has
   not loaded) is not a box with nothing in it: it leaves the grid. */
.mod:empty { display: none; }

/* A section carries its page era vertical padding and its own container.
   The module owns both now. */
.mod.section,
.mod > .section { padding-block: 0; }
.mod .container { max-width: none; padding-inline: 0; }

/* ---------- The spans ---------- */

@media (min-width: 1024px) {
  .mod--s    { grid-column: span 3; }
  .mod--xxl  { grid-column: span 9; }   /* the lead, beside a 3 column stack */
  .mod--m    { grid-column: span 4; }
  .mod--l    { grid-column: span 6; }
  .mod--xl   { grid-column: span 8; }
  .mod--full { grid-column: 1 / -1; }

  /* HEIGHT IS AN OPT IN. A module sizes to its content, so the floor is a
     modifier rather than a property of every module: applied to all of
     them it was the second source of void, holding short modules 160px
     tall around 114px of content.

     THE OLD NOTE, still true of what --mod-row means:
     HEIGHT IS A FLOOR, NOT A ROW SPAN. Spanning two grid rows made every
     row in the grid at least --mod-row tall, including the rows holding
     things that are not modules (the line, the rank card), and it pulled a
     spanning module out of line with the module beside it. A minimum height
     gives the same predictable floor and leaves its neighbours alone. */
  /* The opt in to the row's height. */
  .mod--fill { align-self: stretch; }

  .mod--short { min-height: var(--mod-row); }
  .mod--tall  { min-height: calc(var(--mod-row) * 2 + clamp(14px, 1.2vw, 20px)); }
}

/* One column under the rail's breakpoint: every module is full width, in
   source order, and carries its own separation because the sections no
   longer bring their vertical padding. */
@media (max-width: 1023.98px) {
  .mod + .mod { margin-top: clamp(14px, 3vw, 20px); }
}

/* ============================================================
   3. THE COMPOSITION
   What goes where. Every size below is a span class from the scale; there
   are no bespoke widths, and no module is styled by where it sits.

   The reading order, which is the DOM order, and the grid order:
     the strip      page chrome, above the grid entirely
     row 1          the read (9) beside its four facts (3)
     row 2          the value chart (6) beside the climb (6)
     the line       and the rank card under it, both full width
     row 3          Signals (6) beside the case (6)
     row 4          Your book (6) beside the identity strip (6)
     row 5          Holdings, full
     row 6          the close, full
   ============================================================ */

/* THE STRIP IS NOT A MODULE. It is one line of chrome: what is on the page
   and how fresh. It sits above the grid, so it carries no module surface
   and no row floor, and it follows the grid's own measure. */
.dash-strip {
  position: relative;
  z-index: 1;
}

@media (min-width: 1024px) {
  .dash-strip {
    margin-left: var(--rail);
    /* The same sum as main above, for the same reason: the strip has to
       line up with the grid under it at every width. */
    padding-inline: max(var(--shell-pad), (100% - var(--rail) - var(--shell-max)) / 2);
    padding-top: var(--shell-pad);
  }

  .dash-strip .container {
    max-width: none;
    padding-inline: 0;
  }

  /* ---------- The extracted modules ---------- */

  /* THE DESK SIZES TO ITS CONTENT ON THE FREE PAGE. The module opted into
     the row's height when it held the chart, the line and the rank card
     under it; the rank card had nothing left to draw and is gone, and a
     module that keeps the opt in without the content is a 300px hole under
     the chart. A member's desk still holds the whole card, so it keeps the
     fill it earns. */
  .dash--free #deskModule { align-self: start; }

  /* THE FACTS ARE A RAIL, not a box of boxes, and not four rows of empty
     either: the rail sizes to its four rows and stops. Stretched to the
     hero's height it held 280px of content in a 560px column, which is the
     same void the 2x2 block left, moved down the page. Rows are content
     height (.factrow), so the module is too. */
  #statusTiles { align-self: start; }

  /* The identity strip fills its flush module: a 3:1 banner should touch
     the module's edges, and the module is the frame it needed. */
  #playerCardSlot .statushero__player {
    width: 100%;
    margin: 0;
    border-radius: inherit;
    box-shadow: none;
  }

  /* The chart module holds the figure and the chart that proves it, so the
     chart is not a panel inside a panel, and the plot takes the height the
     module has rather than a fixed 160px with dead air under it. */
  #valueChartSlot .vchart {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
    padding: 0;
    border: 0;
    border-radius: 0;
    background: none;
    box-shadow: none;
  }

  #valueChartSlot .vchart__plot {
    flex: 1 1 auto;
    height: auto;
    min-height: 180px;
  }

  /* The portfolio figure, at the head of its own chart. */
  #valueChartSlot .progression__head { margin: 0 0 clamp(12px, 1.1vw, 18px); }

  /* AT 1024 TO 1280 every six column module goes full width, in the same
     order. Measured, not guessed: the content column is 764px there, so a
     six column module is about 375px, and at that width the book's
     composition bar sets one letter per line, the case's shelf loses its
     row and the arena's two column body collapses. The lead keeps its
     companion, because the facts beside it are only three columns and
     hold at 181px. */
  @media (max-width: 1279.98px) {
    .mod--m,
    .mod--l,
    .mod--xl { grid-column: 1 / -1; }
  }
}

/* ============================================================
   4. THREE FORMS
   The module is one component, but what goes inside a module is not. Three
   blocks that shared one template (label, headline, body, figure) get the
   form their content actually has: a table, a feed and a ladder. Density is
   the point. Colour is spent only where it already carries meaning.
   ============================================================ */

/* ---------- 4a. HOLDINGS IS A TABLE ----------
   Rows at 44px instead of 80, one hairline under the header rather than
   between every row, and the figures right aligned in tabular mono so the
   decimal points line up down the column. That alignment is most of what
   reads as premium. Below 1024 the table stands down and the rows are the
   card list they already were. */

/* The header belongs to the table, and the table is a desktop form: below
   1024 the rows are the card list they already were, and a row of column
   names above cards would be a line of loose words. */
.hhead { display: none; }

@media (min-width: 1024px) {
  .holdings__list {
    --hcols: 28px minmax(0, 1fr) minmax(90px, 0.55fr) 62px 104px;

    display: flex;
    flex-direction: column;
  }

  .hhead {
    display: grid;
    grid-template-columns: var(--hcols);
    align-items: center;
    gap: 14px;
    padding: 0 10px 8px;
    border-bottom: 1px solid var(--edge);
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.5);
  }

  .hhead__cell--right { text-align: right; }

  .holdings__list .hrow {
    display: grid;
    grid-template-columns: var(--hcols);
    align-items: center;
    gap: 14px;
    min-height: 44px;
    margin: 0;
    padding: 4px 10px;
    border: 0;
    border-radius: 6px;
    background: none;
  }

  /* Banding rather than a rule between every row: the eye tracks a line
     across without a grid drawn under it. */
  .holdings__list .hrow:nth-child(even) { background: rgba(255, 255, 255, 0.015); }
  .holdings__list .hrow:hover { background: rgba(255, 255, 255, 0.04); }

  .holdings__list .hrow__thumb { width: 28px; height: 28px; border-radius: 5px; }
  .holdings__list .hrow__body { display: flex; flex-direction: column; gap: 0; min-width: 0; }
  .holdings__list .hrow__head { gap: 8px; min-width: 0; }

  .holdings__list .hrow__name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 13px;
    line-height: 1.25;
  }

  .holdings__list .hrow__meta { gap: 8px; font-size: 10.5px; line-height: 1.2; }

  /* The wear label reads at 3.8:1 against the module and 3.7:1 on a banded
     row, under AA either way. The banding is new, so the fix belongs here. */
  .holdings__list .hrow__wear { color: rgba(255, 255, 255, 0.62); }
  .holdings__list .hrow__head { line-height: 1.25; }

  .holdings__list .hrow__share { display: block; min-width: 0; }
  .holdings__list .hrow__share .signal__track { height: 4px; }

  /* The two figure columns: right aligned, tabular, so the decimals stack. */
  .holdings__list .hrow__pct,
  .holdings__list .hrow__figure {
    text-align: right;
    font-variant-numeric: tabular-nums;
  }

  .holdings__list .hrow__sharepct,
  .holdings__list .hrow__value {
    font-size: 12.5px;
    font-variant-numeric: tabular-nums;
  }

  /* The cheap group keeps its own shape, on the table's rhythm. */
  .holdings__list .hgroup { margin: 6px 0 0; }
  .holdings__list .hgroup__toggle { padding: 8px 10px; }

  /* THE SUMMARY IS ONE LINE. */
  .holdings__summary,
  #holdingsSummary {
    display: block;
    margin-bottom: 10px;
  }

  .hsum {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 6px;
    margin: 0;
    font-size: 11.5px;
    line-height: 1.5;
  }

  .hsum__label { color: rgba(255, 255, 255, 0.5); text-transform: uppercase; letter-spacing: 0.1em; font-size: 10px; }
  .hsum__value { color: #f4f6fb; font-variant-numeric: tabular-nums; }
  .hsum__sub { color: rgba(255, 255, 255, 0.55); }
  .hsum__sep { color: rgba(255, 255, 255, 0.3); padding: 0 2px; }

  /* The filter chips tighten and sit with the head rather than in a band. */
  .holdings__filters { gap: 6px; margin: 0 0 10px; }
  .holdings__filters .hchip { padding: 4px 9px; font-size: 11px; }
}

/* ---------- 4b. SIGNALS IS A FEED ----------
   One log, not a set of cards: no gap, a hairline between rows, and the
   tone as a 3px left edge on every row rather than on one of them. */

.signals {
  display: flex;
  flex-direction: column;
  gap: 0;
}

.signals .signal {
  margin: 0;
  gap: 12px;
  align-items: center;
  border-radius: 0;
  border-top: 1px solid var(--edge);
  border-right: 0;
  border-bottom: 0;
  border-left: 3px solid var(--signal-tone, rgba(255, 255, 255, 0.18));
  background: none;
  box-shadow: none;
  padding: 10px 14px;
  transform: none;
}

.signals .signal:first-child { border-top: 0; }
.signals .signal:hover { background: rgba(255, 255, 255, 0.025); transform: none; }

.signals .signal__thumb { width: 34px; height: 34px; }
.signals .signal__body { gap: 4px; }
.signals .signal__text { font-size: 12.5px; line-height: 1.45; }
.signals .signal__meter { margin-top: 6px; max-width: 360px; }
.signals .signal__head { gap: 8px 10px; }
.signals .signal__name { font-size: 12.5px; }
.signals .signal__figure { font-variant-numeric: tabular-nums; }

/* The locked rows are the same feed with things withheld, so they keep the
   stack rather than sitting in a panel of their own. */
.signals .signals__locked {
  margin: 0;
  border: 0;
  border-top: 1px solid var(--edge);
  border-radius: 0;
  background: none;
  padding: 0;
}

.signals .signals__locked .signal--ghost { border-top: 1px solid var(--edge); }

/* ---------- 4c. THE CLIMB IS A LADDER ----------
   Vertical, highest at the top, so the carrot is up. The rung held is lit
   in the gold it already owns; the rungs above are dimmer but never
   invisible, because they are the point; the rungs below are quiet. */

.arena__tiers {
  display: flex;
  flex-direction: column;
  gap: 0;
  margin: 12px 0 0;
  padding: 0;
  list-style: none;
}

.arena__tier {
  display: grid;
  grid-template-columns: 32px minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
  min-height: 44px;
  padding: 4px 10px;
  border-top: 1px solid var(--edge);
  border-left: 3px solid transparent;
  text-align: left;
}

.arena__tier:first-child { border-top: 0; }

.arena__tier .arena__tieremblem {
  width: 32px;
  height: 32px;
  object-fit: contain;
  opacity: 0.62;
  filter: grayscale(0.15) saturate(0.85);
}

.arena__tiername {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--font-display);
  font-stretch: var(--display-header);
  font-size: 13px;
  color: rgba(255, 255, 255, 0.72);
}

.arena__tiermark {
  font-size: 11.5px;
  color: rgba(255, 255, 255, 0.55);
  font-variant-numeric: tabular-nums;
}

/* Held: lit, in the page's gold. */
.arena__tier.is-current {
  border-left-color: #f0b429;
  background: rgba(240, 180, 41, 0.06);
}

.arena__tier.is-current .arena__tieremblem {
  opacity: 1;
  filter: drop-shadow(0 0 14px rgba(240, 180, 41, 0.5));
}

.arena__tier.is-current .arena__tiername { color: #fff; }
.arena__tier.is-current .arena__tiermark { color: #f0b429; }

/* Passed: quiet, and smaller light. */
.arena__tier.is-past .arena__tieremblem { opacity: 0.4; filter: grayscale(0.6) saturate(0.6); }
.arena__tier.is-past .arena__tiername { color: rgba(255, 255, 255, 0.5); }
.arena__tier.is-past .arena__tiermark { color: rgba(255, 255, 255, 0.55); }

/* ============================================================
   5. COMPOSITION
   Fewer modules, one label, one ladder, and colour that comes from data.
   ============================================================ */

/* ---------- 5a. ONE LABEL FOR EVERY MODULE HEADING ----------
   The page had three casings and two type families across eight headings.
   The words are unchanged; only the treatment unifies, so a heading reads
   as a label on a module rather than as a title competing with the content
   under it. */
.arena__title,
.arena__part,
.dash-sechead__title,
.dash--free .dash-sechead__title,
#deskModule .vchart__eyebrow,
#deskModule .eyebrow {
  margin: 0 0 10px;
  font-family: var(--font-mono);
  font-stretch: normal;
  font-weight: 500;
  font-size: 10.5px;
  line-height: 1.4;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.5);
}

/* ---------- 5b. STANDING: one module, two parts ----------
   Where you sit on the ladder, then who is above you. The climb figure is
   the head of the rungs it describes, and a divider separates the two
   parts rather than a second module. */
.arena__body { display: block; }

.arena__climb-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 0.3em;
  margin: 0 0 12px;
  font-size: clamp(26px, 2.6vw, 34px);
  line-height: 1;
}

.arena__climbfig {
  font-family: var(--font-mono);
  font-size: 1em;
  letter-spacing: -0.03em;
  color: #f4f6fb;
  font-variant-numeric: tabular-nums;
}

.arena__climbto {
  font-family: var(--font-display);
  font-stretch: var(--display-header);
  font-weight: 700;
  font-size: 0.42em;
  color: rgba(255, 255, 255, 0.72);
}

/* The divider between the two parts, at every width: css/flex.css put the
   public rows in a second column on a wide page, which was the two module
   look this pass is removing. */
.arena__ladder {
  margin-top: 16px;
  padding: 16px 0 0;
  border-top: 1px solid var(--edge);
  border-left: 0;
  padding-left: 0;
}

.arena__ladderhead { align-items: baseline; }
.arena__part { margin: 0; }

/* ---------- 5c. COLOUR FROM MEANING ----------
   Nothing new is introduced. Each colour below is already carrying this
   meaning somewhere else in the product, and is read from the data rather
   than assigned by module. */

/* The share bar wears the category colour the book's composition bar
   already computes for that same item (--seg, css/dashboard.css). A knife
   row and a sticker row are told apart at a glance, by the same key. */
.hrow[data-cat] .signal__fill {
  background: var(--seg, var(--signal-tone));
  opacity: 1;
}

/* The shelf wears the four rarities the reveal and the collection already
   use (--r-*, css/achievements.css), so a case of cards reads as a case of
   cards rather than three grey thumbnails. */
.case__card[data-rarity="legendary"] { --card-rarity: var(--r-legendary); }
.case__card[data-rarity="epic"]      { --card-rarity: var(--r-epic); }
.case__card[data-rarity="rare"]      { --card-rarity: var(--r-rare); }
.case__card[data-rarity="common"]    { --card-rarity: var(--r-common); }

.case__card[data-rarity] img {
  box-shadow: 0 0 0 1px var(--card-rarity, rgba(255, 255, 255, 0.08));
  filter: saturate(0.72) brightness(0.8);
}

.case__card[data-rarity] .case__name { color: rgba(255, 255, 255, 0.72); }

/* ---------- 5d. THE IDENTITY MODULE ----------
   The banner bleeds to the module's edges; the shelf under it carries the
   inset the flush module gives up. */
.mod--identity > #playerCardSlot:empty { display: none; }
/* The banner sets the module's top edge, so the shelf under it needs less
   room above than beside: this is what brings the row's two modules within
   120px of each other. */
.mod--identity .dash-case { padding: 12px var(--mod-pad) var(--mod-pad); }
.mod--identity .case { gap: 12px; }
.mod--identity .case__shelf { gap: 8px; }
.mod--identity .case__say { margin: 0; }
.mod--identity .dash-case .container { padding: 0; max-width: none; }

/* Both halves gone (a member) means no module. */
.mod--identity:has(> #playerCardSlot:empty):has(> .dash-case[hidden]) { display: none; }

/* ---------- 5e. THE PRIVATE DESK ----------
   The figure, its chart, the honesty note and the rank card, in one module.
   The note is the line that used to run across the whole page. */
#deskModule .deskmark { padding: 0; margin: 4px 0 0; }
#deskModule .deskmark__inner { display: block; padding: 0; max-width: none; }
#deskModule .deskmark__inner::after { display: none; }

/* ---------- 5f. THE FACTS RAIL ----------
   Four numbers with nothing to look at were four numbers. Each is a row
   now: a label, the figure, and one graphic that shows what the figure
   means, over a hairline. Every graphic is CSS or the inline SVG js/app.js
   draws from data the page already holds (renderFactRail); none of them
   moves, and none of them is drawn when the data behind it is missing. */
.factrail { display: flex; flex-direction: column; width: 100%; }

.factrow {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  column-gap: 12px;
  padding: 14px 0;
  border-top: 1px solid var(--edge);
}

.factrow:first-child { border-top: 0; padding-top: 0; }
.factrow:last-child { padding-bottom: 0; }

.factrow__label {
  font-size: 10.5px;
  line-height: 1.4;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.62);
}

.factrow__figs {
  display: flex;
  align-items: baseline;
  gap: 6px;
  justify-self: end;
}

.factrow__fig {
  /* A figure reads as one thing or not at all: money never wraps mid
     number, so the value holds one line. */
  white-space: nowrap;
  font-size: clamp(17px, 1.4vw, 21px);
  line-height: 1;
  letter-spacing: -0.02em;
  color: #f4f6fb;
  font-variant-numeric: tabular-nums;
}

.factrow__second {
  white-space: nowrap;
  font-size: 11px;
  color: rgba(255, 255, 255, 0.62);
}

/* The graphic runs the full width of the row, under the two of them. */
.factrow > .factspark,
.factrow > .factbar { grid-column: 1 / -1; margin-top: 10px; }

/* THE LINE. The recorded series, shape only. A non scaling stroke keeps
   it an even weight while the box stretches to the column. */
.factspark { display: block; width: 100%; height: 26px; overflow: visible; }

.factspark path {
  stroke: #a8c4ff;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* THE BARS. One track, filled by what the row is about. */
.factbar {
  display: flex;
  width: 100%;
  height: 8px;
  border-radius: 4px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.12);
}

.factbar > span { display: block; height: 100%; }

/* Concentration is the page's risk, in the page's risk colour. */
.factbar--risk > span { background: var(--risk); }

/* The split carries the colour css/dashboard.css already gives each
   category, so the rail, the book panel and the holdings rows all say
   knives in the same gold. The segments fill the bar between them, so it
   has no track of its own: each one sits on the module and is read against
   it, and a hairline holds the join where two of them meet, which is where
   the sizes are actually read. */
.factbar--split { background: none; }
.factbar--split > span { background: var(--seg, rgba(255, 255, 255, 0.35)); }
.factbar--split > span + span { box-shadow: inset 1px 0 0 rgba(6, 8, 12, 0.9); }

/* THE TWO DARKEST CATEGORIES ARE LIFTED HERE, and only here. In the book
   panel every part has a legend line naming it and its percentage, so a
   quiet colour is still identified; the rail has no legend, so the colour
   is the whole of it, and cases at #56607a measured 2.21 against the bar
   and other at #3d424e 1.89. Same hue, enough light to clear 3:1. */
.factbar--split > span[data-cat="cases"] { background: #78849f; }
.factbar--split > span[data-cat="other"] { background: #616a7d; }

/* Rank cards against the rest of what was earned: the one division the
   free card counts actually carry. Gold, because a rank card is the rank. */
.factbar__rank { background: #f0b429; }

#deskModule .deskmark__note {
  margin: 0;
  font-size: 11px;
  letter-spacing: 0.02em;
  color: rgba(255, 255, 255, 0.55);
}

#deskModule .herorank { padding: 14px 0 0; margin-top: 14px; border-top: 1px solid var(--edge); }
#deskModule .herorank .container { padding: 0; max-width: none; }

/* ============================================================
   6. THE MEMBER PAGE
   The same modules, a different job. The free page converts, so it leads
   with what is wrong with the book. A member has already bought: their
   page pays out, so it leads with who they are and what changed since
   they were last here. Nothing here is a second set of components, only
   a different arrangement of the same ones, and every rule is scoped to
   .dash--member so the free page cannot feel it.
   ============================================================ */

/* ---------- 6a. WHAT A MEMBER DOES NOT NEED ----------
   The rank card is the last piece of the pre-re-architecture hero. Its
   banner is the player card now, its climb is Standing, and its figure
   and chart moved into the desk, so for a member it is what it already
   was for a free user: an empty panel with an empty box in it. */
.dash--member .herorank { display: none; }

/* The close under the page is the landing pitch: "Sign in with Steam
   and see what your inventory is really worth." They
   have paid, they are signed in, and they are looking at their own
   inventory. The Discord card beside it is for everybody, so it stays
   and takes the width the pitch is no longer using. */
.dash--member .footer__pitch { display: none; }
.dash--member .footer__close { grid-template-columns: minmax(0, 1fr); }

/* ---------- 6b. THE MEMBER GRID ----------
   A member's modules are the player card, the desk (the figure, what
   changed, the chart), Standing, Signals, the book and Holdings. Laid
   out in source order they left a column of void: the desk is tall and
   Signals is short, so whatever sat beside Signals was nothing at all.

   Two rules fix it. The identity banner leads, full width, because it is
   the reward and it is 3:1 art that wants the measure. Signals runs the
   full width too, so nothing is asked to stand beside a 230px module.
   What is left is one pair, the desk and Standing, which are the two
   tall modules and belong side by side anyway. */
/* The banner leads at every width, phone included, and it leads in the
   DOM rather than through `order`: js/app.js moves the module in front of
   the desk for a member (mountMemberIdentity), so what a screen reader
   hears is what the page shows. Below the grid's breakpoint the layout is
   not a grid at all, and `order` there would have done nothing anyway. */
@media (min-width: 1024px) {
  .dash--member .mod--identity, .dash--free .mod--identity { grid-column: 1 / -1; }

  /* THE FREE COCKPIT DROPS TWO FLEX-HERO LEFTOVERS.

     #statusTiles is the facts rail that sat beside the retired flex read;
     its four figures live in the value strip, Holdings, the book and the
     collection now, so it is dropped rather than left as a stray span-3
     box. #earnedSection is the in-banner case shelf, which the phase-1
     collection wall now covers, and hiding it lets the identity banner
     run full width and clean, the same as a member's. A member already
     leaves both empty/hidden, so these only ever act on a free page. */
  .dash--free #statusTiles { display: none; }
  .dash--free #earnedSection { display: none; }

  /* ---------- THE GRID PASS ----------

     IT WAS TWO COLUMNS AT THE TOP AND A STACK UNDER THEM. Signals, the
     scale strip, the collection, the book and holdings all ran 1 / -1,
     so from the reads down the right half of the screen was empty for
     the whole scroll. Measured at 1280, main is 1048 and a column is
     73px, and the modules that were full width by default rather than
     by need are plain in the numbers:

       module        natural content width      slack at 1 / -1
       Signals       657px   =  8 of 12 col       335px
       Your book     653px   =  8 of 12 col       339px
       scale strip   401px   =  5 of 12 col       591px
       Holdings     1162px   = 12 of 12 col      -170px, already squeezed
       collection    auto-fill grid, takes what it is given

     So Holdings and the collection keep 12. Signals does not, and it is
     the change that earns the most: it moves up beside the money.

     EVERY ROW IS A PAIR OF NEIGHBOURS. The pairs are the modules that
     already sit next to each other in the DOM, so nothing is placed
     explicitly and nothing is reordered: these are spans on auto-flow,
     the DOM order IS the visual order, and the tab order still matches
     what the eye does.

     A 2 ROW SPAN WAS TRIED FOR THE DESK AND REJECTED, and the reason is
     worth keeping. Putting desk against a stack of arena and Signals
     looked perfect in the row heights, 1227 against 1226, but measuring
     inside the desk showed why: arena plus Signals is the TALLER side,
     and the grid was stretching the desk to match it. The desk's own
     content ends at 878, so the arrangement bought a tidy row edge with
     330px of void inside the module. That is the defect this pass
     exists to remove, not a way to hide it. Side by side, desk is 896
     to arena's 857 and neither is stretched. */
  /* SIX, THREE AND THREE. The chart is the only one of the three with a
     horizontal axis to earn, so it takes half and the two lists split
     the rest. It was 7 / 5 while Standing was one module holding both
     its halves; splitting the board into its own panel means the chart
     can give up a column and neither list is squeezed, because each now
     holds one thing instead of two. */
  .dash--member #deskModule, .dash--free #deskModule { grid-column: span 6; }
  .dash--member #arena, .dash--free #arena { grid-column: span 3; }
  .dash--member #boardModule, .dash--free #boardModule { grid-column: span 3; }

  /* Expanded, the chart takes the whole row and the two lists flow
     under it. */
  .dash--member.dash--chartwide #deskModule, .dash--free.dash--chartwide #deskModule { grid-column: 1 / -1; }

  /* THREE ACROSS NEEDS THE WIDTH FOR THREE. At 1024 a twelfth is 61px,
     so 6 / 3 / 3 gave the two lists 184px each: the board row is a
     position, an avatar, a name and a figure, and 184 left the name
     about 44. Between 1024 and 1280 the chart takes the first row on
     its own and the two lists share the second, which is the same
     three panels in the same order with none of them squeezed. */
  @media (max-width: 1279px) {
    .dash--member #deskModule, .dash--free #deskModule { grid-column: 1 / -1; }
    .dash--member #arena, .dash--free #arena { grid-column: span 6; }
    .dash--member #boardModule, .dash--free #boardModule { grid-column: span 6; }
  }

  /* ---------- THE READS ROW: FIVE, FOUR AND THREE ----------

     Signals, the movers and the indexes are three readings of the same
     market at the same moment, and they were three separate bands with
     a strip and a banner between them. js/app.js moves the signals
     module in front of the pair for a member (mountMemberReadsRow), so
     the three are neighbours in the DOM and these are spans on
     auto-flow: nothing is placed explicitly and the tab order still
     matches what the eye does.

     5 / 4 / 3 is the reference's 1.5 / 1 / 0.95 laid on twelve columns,
     and the split follows what each row has to carry. Measured at 1280:
     the grid is 1048 wide, a column is 69 and the gap 15.

       panel     span   panel   its row   what that row holds
       Signals     5      404     376     thumbnail, pill, name, sentence, figure
       Movers      4      320     283     a market name, which can run to sixty
       Indexes     3      236     199     glyph, label, figure, 46px of line

     At 1920 the same spans give 638 / 507 / 375.

     The strip goes back to the full width it was authored at: its five
     columns existed only to pair it with the movers module, and that
     pair is gone. */
  .dash--member .mod:has(#signalsList), .dash--free .mod:has(#signalsList) { grid-column: span 5; }
  .dash--member #moversModule, .dash--free #moversModule { grid-column: span 4; }
  .dash--member #indexesModule, .dash--free #indexesModule { grid-column: span 3; }
  .dash--member #scaleStrip, .dash--free #scaleStrip { grid-column: 1 / -1; }

  /* THREE ACROSS NEEDS THE WIDTH FOR THREE, the same arithmetic as the
     panel row above. At 1024 the grid is 824 and a column is 52, so
     three of them give the indexes 184px, and 36 of that is the panel's
     own inset: a 148px row against 90px of glyph, line and gaps and a
     45px percentage leaves 13 for the label. Between 1024 and 1280
     signals takes the first row and the market pair shares the second,
     in the same order, with none of them squeezed: six columns each
     puts the index label at 213 and the mover name at 301. */
  @media (max-width: 1279px) {
    .dash--member .mod:has(#signalsList), .dash--free .mod:has(#signalsList) { grid-column: 1 / -1; }
    .dash--member #moversModule, .dash--free #moversModule { grid-column: span 6; }
    .dash--member #indexesModule, .dash--free #indexesModule { grid-column: span 6; }
  }
}

/* ---------- 6c. WHAT CHANGED, UNDER THE FIGURE ----------
   24h, 7d and 30d are the clearest thing a member gets that a free user
   does not, and they were halfway down the page inside the book. They
   sit under the portfolio figure now (js/app.js, mountMemberChanges),
   which is the figure they are the change in. The column keeps its own
   markup and ids; only its frame changes, because it is no longer one
   half of a two column panel. */
#deskModule .book__col--changes {
  padding: 14px 0 0;
  margin-top: 14px;
  border-top: 1px solid var(--edge);
  border-left: 0;
}

/* THREE ROWS, NOT THREE BOXES. Three tiles across a six column module is
   110px each, and "+A$4.55" at 29px does not fit in 110px: every figure
   broke mid number. They are rows now, label left and figure right over a
   hairline, which is the same shape the facts rail uses on the free page
   and holds at every width. The markup and the figures are untouched. */
#deskModule .book__col--changes .stats {
  display: grid;
  /* THE LABEL AND ITS FIGURE ARE ONE UNIT. Label left and figure hard
     right put 300px of nothing between "24h Change" and the number it
     names, and at 1920 it was closer to 600. The rows share one grid
     now, so the label column is as wide as the widest of the three and
     the figure starts right after it: aligned down the column, and
     coupled across it. */
  grid-template-columns: max-content minmax(0, 1fr);
  gap: 0;
  /* The panel this sat in was the book's. In the desk the module is the
     panel, so the rows sit straight on it. */
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
}

/* The row is not a box: its three parts join the grid above so the three
   rows line up as a table rather than three independent layouts. */
#deskModule .book__col--changes .stat {
  display: contents;
}

#deskModule .book__col--changes .stat__label {
  grid-column: 1;
  align-self: baseline;
  margin: 0;
  padding: 12px 14px 0 0;
}

#deskModule .book__col--changes .stat__value {
  grid-column: 2;
  align-self: baseline;
  margin: 0;
  padding: 12px 0 0;
  white-space: nowrap;
  font-size: clamp(18px, 1.6vw, 23px);
  line-height: 1;
}

/* THE DIVIDER IS THE DELTA'S, not the two cells above it. The label and
   the figure sit on a shared baseline, which means their boxes start at
   different heights, and a border on each drew the line as two steps.
   The delta spans the whole row, so one border under it is one line. */
#deskModule .book__col--changes .stat__delta {
  grid-column: 1 / -1;
  margin: 0;
  padding: 6px 0 12px;
  border-bottom: 1px solid var(--edge);
}

#deskModule .book__col--changes .stat:first-child .stat__label,
#deskModule .book__col--changes .stat:first-child .stat__value { padding-top: 0; }

#deskModule .book__col--changes .stat:last-child .stat__delta {
  padding-bottom: 0;
  border-bottom: 0;
}

#deskModule .book__col--changes .section__note { margin: 14px 0 0; }

/* The book is one column for a member, since the other half moved. What
   is left is a lead line, a bar and a legend, and stretching that one
   bar across 992px made a hairline out of the thing the module is for.
   The legend goes beside it instead: the same three pieces, given a
   measure, out of the module's own content rather than a max-width that
   would leave the right half empty. */
.dash--member .book, .dash--free .book { grid-template-columns: minmax(0, 1fr); }

/* THE TWO COLUMN SPLIT IS GONE, and the reason it existed went with it.
   It put the legend beside the bar because a bar stretched across 992px
   with three short lines under it left the right half of the panel
   empty. The legend is a ledger now: four columns of its own, each row
   carrying a track that wants the width. So the composition stacks, and
   the bar gets the full measure rather than the 534px the old grid left
   it, which on this account is the difference between graffiti drawing
   1px and drawing 3.

   .book__col and the ids inside dashboard.html are untouched: this is
   the layout of #bookComposition's own contents and nothing else. */
@media (min-width: 900px) {
  .dash--member .book__comp {
    display: block;
  }

  .dash--member .book__comp .book__legend { margin-top: 18px; }
}

/* The two quiet lines in the promoted block are --faint, which measures
   3.82 and 3.87 against this panel: under AA for body text. They are
   lifted here, in the member's copy only, so the free page's book keeps
   the colours it was measured with and nothing else on either page
   moves. The same two lines in the book are still --faint and still
   under AA; that is a pre-existing call, not this pass's. */
#deskModule .book__col--changes .section__note,
#deskModule .book__col--changes .stat__delta .sub {
  color: rgba(255, 255, 255, 0.52);
}

/* The loss tone measures 3.5 against this panel, which is under AA for a
   20px figure at weight 500 and for the 12px percentage beside it. A
   member reads these three lines first, so the red is lifted here, in the
   promoted block only: same hue, enough light to clear 4.5. The book's
   own copy of .neg is untouched, on this page and on every other. */
#deskModule .book__col--changes .neg { color: #f26569; }

/* ---------- 6d. THE IDENTITY STRIP, WITH NO CARD ON IT ----------
   The banner is 3:1 because a calling card is 3:1. With nothing equipped
   there is no art, and the same box became 440px of panel holding an
   emblem, three lines and a floating chip: a missing image, drawn at
   full size. So the module collapses to a strip. The parts are the same
   parts with the same classes, taken out of the absolute layout the
   banner needs and put in a row. The banner itself is untouched the
   moment there is a card to draw. */
.statushero__player--bare {
  display: flex;
  align-items: center;
  gap: clamp(14px, 1.6vw, 22px);
  width: 100%;
  aspect-ratio: auto;
  padding: clamp(16px, 1.6vw, 22px) var(--mod-pad);
  overflow: visible;
}

/* Out of the corner it was pinned to, and sized in pixels rather than
   cqi: there is no banner to measure any more. */
.statushero__player--bare .statushero__emblem {
  position: static;
  flex: none;
  width: clamp(56px, 6vw, 72px);
  height: clamp(56px, 6vw, 72px);
  margin: 0;
}

.statushero__player--bare .statushero__ident {
  position: static;
  flex: 1 1 auto;
  height: auto;
  inset: auto;
  gap: 5px;
}

/* The rank reads as a label here, not as the banner's headline. */
.statushero__player--bare .statushero__ranklabel { font-size: clamp(17px, 1.7vw, 21px); }

.statushero__player--bare .statushero__cardbottom {
  position: static;
  flex: none;
  flex-direction: column;
  align-items: flex-end;
  justify-content: center;
  gap: 8px;
  inset: auto;
}

/* No art means no scrim and no crop, so the hairline is the module's. */
.statushero__player--bare::after { box-shadow: none; }

.statushero__equip {
  font-size: 12px;
  letter-spacing: 0.01em;
  color: rgba(255, 255, 255, 0.72);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: rgba(255, 255, 255, 0.28);
}

.statushero__equip:hover { color: #fff; text-decoration-color: rgba(255, 255, 255, 0.6); }

/* ============================================================
   7. THE MEMBER'S MARKET AND COLLECTION
   Three modules whose figures all come from documents the server already
   serves (js/app.js: loadMarketBoard, loadMarketIndexes, loadCollection).
   Every rule here is inside one of those three modules, so nothing can
   reach the rest of the page, free or paid.
   ============================================================ */

/* The three modules are sections, and a .dash-section carries no side
   padding on this page: the book and holdings put a .dpanel inside for
   that. These three draw straight onto the module surface, so the inset
   is theirs to carry. */
#moversModule,
#indexesModule,
#collectionModule { padding-inline: var(--mod-pad); }

/* The section head's meta is --faint, which measures 3.82 on this
   surface: under AA. Lifted inside these three modules only, so the
   heads on Signals, the book and Holdings keep the colour they were
   measured with on both pages. */
#moversModule .dash-sechead__meta,
#indexesModule .dash-sechead__meta,
#collectionModule .dash-sechead__meta { color: rgba(255, 255, 255, 0.52); }

/* ---------- 7a. MOVERS ----------
   Two short lists, gainers and losers, name left and percent right. The
   percent carries its own sign as well as its colour, because a reader
   who cannot see the colour still has to know which way it went. */
.movers__cols {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 18px;
}

@media (min-width: 560px) {
  .movers__cols { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: clamp(16px, 2vw, 28px); }
}

.movers__head {
  margin: 0 0 8px;
  font-size: 10.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.62);
}

.movers__list { margin: 0; padding: 0; list-style: none; }

.mover {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  column-gap: 10px;
  padding: 7px 0;
  border-top: 1px solid var(--edge);
}

.mover:first-child { border-top: 0; padding-top: 0; }

.mover__name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12.5px;
  color: rgba(255, 255, 255, 0.86);
}

.mover__pct { font-size: 12.5px; font-variant-numeric: tabular-nums; color: rgba(255, 255, 255, 0.72); }
.mover.is-up   .mover__pct { color: #57d38c; }
.mover.is-down .mover__pct { color: #f26569; }

.movers__none { margin: 0; font-size: 12px; color: rgba(255, 255, 255, 0.52); }

/* ============================================================
   LOCKED INTELLIGENCE PLACEHOLDERS (free dashboard, phase 2)

   The movers and index panels a free reader now sees, held behind a gold
   lock. All .dlock*, which only the free branches emit, so the member
   rows (.mover / .dashidx__row, .dash--member-scoped) are untouched.
   Static by design: a placeholder that shimmers forever is a loop, and
   the frosted treatment reads as locked without one. The one number ever
   shown is a count the tease sends; where a paid figure would be there is
   LOCKED_FIGURE, glyphs with no digit.
   ============================================================ */
.dlock-line {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 0 0 12px;
  font-size: 12.5px;
  line-height: 1.5;
  color: rgba(255, 255, 255, 0.72);
}
.dlock-line .lock-icon { flex: none; color: var(--r-legendary, #d4a353); }
.dlock-line b { color: var(--text); font-weight: 600; }

.dlock-list,
.dlock-idxlist { margin: 0; padding: 0; list-style: none; }

.dlock-row {
  display: grid;
  grid-template-columns: 30px minmax(0, 1fr) 52px;
  align-items: center;
  column-gap: 11px;
  padding: 9px 0;
  border-top: 1px solid var(--edge, rgba(255, 255, 255, 0.06));
}
.dlock-row:first-child { border-top: 0; padding-top: 2px; }

/* ---------- THE FROSTED LOCK (the market page's, value for value) ----------
   A soft steel bar with a gentle fixed sheen and a faint ring: data behind
   glass, not a loading row. The same values as css/market.css .mk-skel and
   .mk-frost, so a locked row reads the same on the dashboard and on the
   market page. STATIC: nothing here animates, so reduced motion has
   nothing to switch off, and there is no glyph in any of it. */
.dlock-skel {
  display: block;
  height: 11px;
  border-radius: 999px;
  background:
    linear-gradient(100deg,
      rgba(168, 186, 214, 0.07) 0%,
      rgba(168, 186, 214, 0.16) 45%,
      rgba(168, 186, 214, 0.07) 90%);
  box-shadow: inset 0 0 0 1px rgba(168, 186, 214, 0.07);
}
.dlock-skel--name { width: 64%; }
.dlock-skel--sub  { width: 34%; height: 7px; margin-top: 7px; opacity: 0.7; }
.dlock-row:nth-child(2) .dlock-skel--name { width: 50%; }
.dlock-row:nth-child(3) .dlock-skel--name { width: 72%; }
.dlock-row:nth-child(4) .dlock-skel--name { width: 44%; }
.dlock-row:nth-child(5) .dlock-skel--name { width: 58%; }
.dlock-row:nth-child(2) .dlock-skel--sub { width: 28%; }
.dlock-row:nth-child(3) .dlock-skel--sub { width: 38%; }
.dlock-row:nth-child(4) .dlock-skel--sub { width: 24%; }

/* Where a figure would be: a short bar, the same steel a shade stronger. */
.dlock-skel--fig {
  width: 52px;
  height: 12px;
  margin-left: auto;
  background:
    linear-gradient(100deg,
      rgba(168, 186, 214, 0.1) 0%,
      rgba(168, 186, 214, 0.22) 45%,
      rgba(168, 186, 214, 0.1) 90%);
  box-shadow: inset 0 0 0 1px rgba(168, 186, 214, 0.1);
}

.dlock-body { min-width: 0; }

/* The frosted plate where a picture would be. */
.dlock-thumb {
  display: block;
  width: 30px;
  height: 22px;
  border-radius: 5px;
  background:
    linear-gradient(160deg, rgba(168, 186, 214, 0.12), rgba(168, 186, 214, 0.035) 60%),
    rgba(12, 14, 20, 0.6);
  box-shadow: inset 0 0 0 1px rgba(168, 186, 214, 0.08);
}

/* The index rows keep their real, public label and glyph; only the line
   and the change are locked. */
.dlock-idxrow {
  display: grid;
  grid-template-columns: 15px minmax(0, 1fr) 64px auto;
  align-items: center;
  column-gap: 10px;
  padding: 9px 0;
  border-top: 1px solid var(--edge, rgba(255, 255, 255, 0.06));
}
.dlock-idxrow:first-child { border-top: 0; padding-top: 2px; }
.dlock-idxrow .dashidx__glyph { width: 15px; height: 15px; color: rgba(255, 255, 255, 0.5); }
.dlock-idxrow__label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12.5px;
  color: rgba(255, 255, 255, 0.86);
}

/* The locked sparkline: a frosted plate, no path, because no series was
   sent. Sized like the member spark so the row reads the same. The same
   plate as the market page's locked index line (.mk-frost). */
.dlock-spark {
  display: block;
  width: 64px;
  height: 22px;
  border-radius: 6px;
  background:
    linear-gradient(160deg, rgba(168, 186, 214, 0.12), rgba(168, 186, 214, 0.035) 60%),
    rgba(12, 14, 20, 0.6);
  box-shadow: inset 0 0 0 1px rgba(168, 186, 214, 0.08);
}
.dlock-idxrow .dlock-skel--fig { width: 44px; height: 10px; }

.dlock-cta { margin-top: 16px; }

/* ---------- Locked signal rows (free) ----------
   The trend/peak signals a free response withholds, drawn frosted below
   the real ones, in the signal row's own shape. */
.signal--locked {
  border-left-color: rgba(255, 255, 255, 0.12) !important;
  opacity: 0.9;
}
.signal__pill--lock {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: rgba(255, 255, 255, 0.62);
}
.signal__pill--lock .lock-icon { color: var(--r-legendary, #d4a353); }
.dlock-sq {
  flex: none;
  width: 42px;
  height: 42px;
  border-radius: 8px;
  background:
    linear-gradient(160deg, rgba(168, 186, 214, 0.12), rgba(168, 186, 214, 0.035) 60%),
    rgba(12, 14, 20, 0.6);
  box-shadow: inset 0 0 0 1px rgba(168, 186, 214, 0.08);
}
.signal--locked .dlock-skel--head { display: inline-block; width: 120px; }
.signal--locked .dlock-skel--wide { width: 62%; margin-top: 8px; }
.signal--locked .dlock-skel--fig { align-self: center; }

/* THE LOCKED CHANGE-TILE SKELETON. Where a paid 24h/7d/30d figure would
   sit on a free read, a clean steel skeleton bar instead of a blurred row
   of block glyphs. Static, with a fixed sheen and no loop, so it reads as
   "premium data behind a lock", calm, not a broken or loading mark. Only
   ever drawn inside .stats__locked, which a member never renders, so their
   change tiles are untouched. */
.stats__tile-skel {
  display: block;
  margin-top: 10px;
  width: 62%;
  height: 13px;
  border-radius: 999px;
  background:
    linear-gradient(100deg,
      rgba(168, 186, 214, 0.1) 0%,
      rgba(168, 186, 214, 0.22) 45%,
      rgba(168, 186, 214, 0.1) 90%);
  box-shadow: inset 0 0 0 1px rgba(168, 186, 214, 0.1);
}

/* THE FREE VALUE TEASER, under the portfolio value. A small premium plate
   in the space a member's change figures would fill: it ties the real,
   public value to the locked movement and offers the way in. Restrained,
   gold-ghost, no figure of its own. Emitted only for a free reader (JS gates
   it on flexOn), so a member's value block never carries it. */
.valteaser {
  margin-top: 12px;
  padding: 11px 13px;
  border: 1px solid rgba(255, 209, 122, 0.22);
  border-radius: 10px;
  background:
    linear-gradient(180deg, rgba(255, 209, 122, 0.05), rgba(255, 209, 122, 0) 70%),
    rgba(255, 255, 255, 0.015);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02);
}
.valteaser__line {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 0 0 9px;
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--muted);
}
.valteaser__mark {
  display: inline-flex;
  width: 11px;
  height: 12px;
  flex: none;
  color: var(--r-legendary, #d4a353);
}
.valteaser__mark svg { width: 100%; height: 100%; }
.valteaser__cta { margin: 0; }

/* ============================================================
   THE PRESTIGE LADDER SECTION

   A progress cockpit under the value strip: hero, four stat tiles, the
   30-tier strip (the centrepiece), and the milestones / what's-next
   panels. Gold marks the reader's own rung and the prestige moments;
   every sub-panel wears the dashboard's raised-plate language (a top-lit
   gradient, an inner highlight and a soft shadow) so the section reads as
   deep as the ones around it. The reveal (gold line runs to YOU, bars
   grow) is a one-shot driven by .prestige.is-revealed, which js/app.js
   arms once on scroll-in; a repaint under an already-revealed section
   renders at the final state with no replay, and reduced motion skips it.
   ============================================================ */
.prestige {
  --pg-gold: #ffd36b;
  --pg-gold-soft: rgba(255, 211, 107, 0.5);
  --pg-gold-dim: rgba(255, 211, 107, 0.16);
  --pg-plate:
    linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0) 42%),
    var(--surface-quiet);
  --pg-plate-edge:
    inset 0 1px 0 rgba(255, 255, 255, 0.06),
    0 1px 2px rgba(0, 0, 0, 0.4),
    0 12px 30px -20px rgba(0, 0, 0, 0.95);
  gap: clamp(14px, 1.5vw, 20px);
}
.prestige__body { display: flex; flex-direction: column; gap: clamp(12px, 1.3vw, 18px); }

/* ---- HERO: one cohesive banner, tight and lit ---- */
.prestige__hero {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1.25fr) minmax(0, 0.8fr);
  align-items: center;
  gap: clamp(14px, 1.6vw, 22px);
  padding: clamp(15px, 1.5vw, 20px) clamp(16px, 1.8vw, 24px);
  border: 1px solid var(--edge);
  border-radius: 16px;
  background:
    radial-gradient(90% 130% at 0% -10%, rgba(255, 211, 107, 0.09), transparent 50%),
    var(--pg-plate);
  box-shadow: var(--pg-plate-edge), 0 0 34px -22px var(--pg-gold-soft);
}
.prestige__now { display: flex; align-items: center; gap: 14px; min-width: 0; }
.prestige__nowart, .prestige__nextart, .prestige__peakart, .prestige__msart, .prestige__nxart, .prestige__rungart {
  display: block; flex: none; object-fit: contain;
}
.prestige__nowart, .prestige__nowart--none { width: 74px; height: 74px; filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.5)); }
.prestige__nowart img { width: 100%; height: 100%; object-fit: contain; }
.prestige__nowart--none, .prestige__nextart--none, .prestige__peakart--none,
.prestige__msart--none, .prestige__nxart--none, .prestige__rungart--none {
  border-radius: 10px; background: rgba(255, 255, 255, 0.04);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
}
.prestige__nowtext { min-width: 0; }
.prestige__eyebrow { margin: 0; font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--pg-gold); }
.prestige__nowname { margin: 3px 0 4px; font-size: clamp(20px, 2vw, 26px); font-weight: 600; color: var(--text); letter-spacing: -0.01em; }
.prestige__nowval { margin: 0; font-size: 14px; color: var(--muted); }

.prestige__climb { min-width: 0; }
.prestige__gapbar {
  position: relative; height: 10px; border-radius: 999px;
  background: rgba(0, 0, 0, 0.35);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6), inset 0 0 0 1px rgba(255, 255, 255, 0.04);
  overflow: hidden;
}
.prestige__gapfill {
  position: absolute; inset: 0 auto 0 0; width: calc(var(--p, 0) * 100%);
  border-radius: 999px;
  background: linear-gradient(90deg, rgba(255, 211, 107, 0.55), var(--pg-gold));
  box-shadow: 0 0 16px -1px var(--pg-gold-soft), inset 0 1px 0 rgba(255, 255, 255, 0.35);
}
.prestige__gaplabel { margin: 9px 0 0; font-size: 12px; color: var(--muted); }
.prestige__gaplabel::first-line { color: var(--text); }
.prestige__next {
  display: flex; align-items: center; gap: 11px; justify-content: flex-end; min-width: 0;
  padding-left: clamp(10px, 1.4vw, 20px); border-left: 1px solid var(--edge);
}
.prestige__nextart, .prestige__nextart--none { width: 50px; height: 50px; opacity: 0.9; }
.prestige__nextart img { width: 100%; height: 100%; object-fit: contain; }
.prestige__nexttext { min-width: 0; text-align: right; }
.prestige__nextname { margin: 0; font-size: 13px; color: var(--text); }
.prestige__nextval { margin: 2px 0 0; font-size: 12px; color: var(--faint); }
.prestige__apex { grid-column: 2 / -1; text-align: center; color: var(--pg-gold); font-size: 14px; }

/* ---- STAT TILES: dense, plated ---- */
.prestige__tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
.prestige__tile {
  display: flex; flex-direction: column; min-height: 104px;
  padding: 13px 14px; border: 1px solid var(--edge); border-radius: 13px;
  background: var(--pg-plate); box-shadow: var(--pg-plate-edge); min-width: 0;
}
.prestige__tile--locked { border-color: rgba(255, 211, 107, 0.24); }
.prestige__tilelabel { margin: 0 0 auto; font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--faint); }
.prestige__tilefig { margin: 10px 0 0; font-size: 23px; font-weight: 600; color: var(--text); line-height: 1; }
.prestige__tilefig--sm { font-size: 15px; }
.prestige__tilefig.is-up { color: #6bd08a; }
.prestige__tilefig.is-down { color: #e08a8a; }
.prestige__tileof { margin-left: 4px; font-size: 13px; color: var(--faint); font-weight: 400; }
.prestige__tilesub { margin: 5px 0 0; font-size: 12px; color: var(--muted); }
.prestige__muted { color: var(--faint); }
.prestige__tilebar {
  margin-top: 10px; height: 6px; border-radius: 999px;
  background: rgba(0, 0, 0, 0.32); box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.55); overflow: hidden;
}
.prestige__tilebar span {
  display: block; height: 100%; width: calc(var(--p, 0) * 100%); border-radius: 999px;
  background: linear-gradient(90deg, rgba(255, 211, 107, 0.55), var(--pg-gold));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
.prestige__peakrow { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
.prestige__peakart, .prestige__peakart--none { width: 34px; height: 34px; filter: drop-shadow(0 0 8px rgba(255, 211, 107, 0.35)); }
.prestige__peakart img { width: 100%; height: 100%; object-fit: contain; }
.prestige__lockcta { margin-top: 10px; align-self: flex-start; }

/* ---- THE 30-TIER STRIP: the centrepiece ---- */
.prestige__strip {
  --pg-linetop: 72px;
  position: relative; padding: 46px 0 14px;
  border: 1px solid var(--edge); border-radius: 16px;
  background:
    radial-gradient(120% 130% at 50% -20%, rgba(255, 211, 107, 0.07), transparent 55%),
    var(--pg-plate);
  box-shadow: var(--pg-plate-edge);
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: thin;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 26px, #000 calc(100% - 26px), transparent);
          mask-image: linear-gradient(90deg, transparent, #000 26px, #000 calc(100% - 26px), transparent);
}
/* THE TRACK: a recessed channel the emblems sit on, full width. */
.prestige__strip::after {
  content: ""; position: absolute; left: 20px; right: 20px; top: var(--pg-linetop); height: 4px;
  border-radius: 999px; background: rgba(0, 0, 0, 0.4);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.7), inset 0 0 0 1px rgba(255, 255, 255, 0.03);
  pointer-events: none; z-index: 0;
}
/* THE GOLD PROGRESS, on the track, glowing, up to YOU. */
.prestige__stripline {
  position: absolute; left: 20px; top: var(--pg-linetop); height: 4px; border-radius: 999px;
  width: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(90deg, rgba(255, 211, 107, 0.35), var(--pg-gold));
  box-shadow: 0 0 14px 1px var(--pg-gold-soft), inset 0 1px 0 rgba(255, 255, 255, 0.4);
  transition: width 1100ms cubic-bezier(0.22, 1, 0.36, 1);
}
.prestige.is-revealed .prestige__stripline { width: min(var(--reach), calc(100% - 40px)); }
.prestige__rail {
  position: relative; z-index: 1; display: grid; grid-auto-flow: column;
  grid-auto-columns: minmax(66px, 1fr); gap: 2px; margin: 0; padding: 0 12px; list-style: none;
  min-width: 100%;
}
.prestige__rung {
  position: relative; display: flex; flex-direction: column; align-items: center; gap: 7px;
  padding: 0 2px; text-align: center;
}
.prestige__rungart, .prestige__rungart--none { width: 56px; height: 56px; transition: transform 200ms ease, filter 200ms ease; }
.prestige__rungart img { width: 100%; height: 100%; object-fit: contain; }
.prestige__rungname {
  font-size: 10px; line-height: 1.15; color: var(--muted);
  max-width: 74px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* LOCKED (above): recessed and cool, reads "not yet". */
.prestige__rung.is-locked .prestige__rungart { filter: grayscale(0.9) brightness(0.42) contrast(0.95); opacity: 0.55; }
.prestige__rung.is-locked .prestige__rungname { color: rgba(150, 165, 190, 0.55); }
.prestige__rung.is-locked::before {
  content: ""; position: absolute; top: 2px; left: 50%; transform: translateX(-50%);
  width: 60px; height: 60px; border-radius: 14px;
  background: radial-gradient(closest-side, rgba(12, 16, 24, 0.5), transparent);
  pointer-events: none;
}
/* PASSED (below): full colour, conquered. */
.prestige__rung.is-passed .prestige__rungart {
  filter: drop-shadow(0 0 7px rgba(255, 211, 107, 0.28)); opacity: 1;
}
.prestige__rung.is-passed .prestige__rungname { color: var(--text); }
/* CURRENT (YOU): the clear hero. */
.prestige__rung.is-current .prestige__rungart {
  transform: scale(1.2);
  filter: drop-shadow(0 0 14px var(--pg-gold-soft));
}
.prestige__rung.is-current .prestige__rungname { color: var(--pg-gold); font-weight: 600; }
.prestige__rung.is-current::after {
  content: ""; position: absolute; top: -2px; left: 50%; transform: translateX(-50%);
  width: 62px; height: 62px; border-radius: 16px; box-shadow: 0 0 0 2px var(--pg-gold-dim); pointer-events: none;
  animation: prestigePulse 2.4s ease-in-out infinite;
}
@keyframes prestigePulse {
  0%, 100% { box-shadow: 0 0 0 2px var(--pg-gold-dim), 0 0 14px -2px var(--pg-gold-soft); }
  50% { box-shadow: 0 0 0 2px rgba(255, 211, 107, 0.4), 0 0 22px 0 var(--pg-gold-soft); }
}
.prestige__you {
  position: absolute; top: -14px; left: 50%; transform: translateX(-50%);
  padding: 2px 8px; border-radius: 999px; font-size: 8.5px; font-weight: 700; letter-spacing: 0.09em;
  color: #1a1406; background: var(--pg-gold); box-shadow: 0 2px 12px -1px var(--pg-gold-soft); z-index: 3;
}
/* HOVER: lift + tooltip (name . threshold). */
.prestige__rung:hover .prestige__rungart { transform: translateY(-4px) scale(1.08); }
.prestige__rung.is-current:hover .prestige__rungart { transform: translateY(-4px) scale(1.2); }
.prestige__rung:hover::after {
  content: attr(data-tip); position: absolute; top: -34px; left: 50%; transform: translateX(-50%);
  padding: 5px 9px; border-radius: 7px; white-space: nowrap; z-index: 5;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.02em; color: var(--text);
  background: rgba(8, 10, 16, 0.96); border: 1px solid var(--edge);
  box-shadow: 0 6px 18px -6px rgba(0, 0, 0, 0.9); pointer-events: none;
  animation: none;
}
.prestige__rung.is-current:hover::after { top: -36px; }

/* ---- PANELS: milestones + what's next ---- */
.prestige__panels { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.prestige__panel {
  padding: 16px 17px; border: 1px solid var(--edge); border-radius: 14px;
  background: var(--pg-plate); box-shadow: var(--pg-plate-edge); min-width: 0;
}
.prestige__paneltitle { margin: 0 0 13px; font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--faint); }
.prestige__mslist, .prestige__nxlist { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 13px; }
.prestige__ms, .prestige__nx { display: flex; align-items: center; gap: 12px; min-width: 0; }
.prestige__msart, .prestige__msart--none, .prestige__nxart, .prestige__nxart--none { width: 38px; height: 38px; }
.prestige__msart img, .prestige__nxart img { width: 100%; height: 100%; object-fit: contain; }
.prestige__ms .prestige__msart { filter: drop-shadow(0 0 7px rgba(255, 211, 107, 0.25)); }
.prestige__mstext, .prestige__nxtext { min-width: 0; flex: 1; }
.prestige__msname, .prestige__nxname { margin: 0; font-size: 13.5px; color: var(--text); }
.prestige__msdate { margin: 2px 0 0; font-size: 11.5px; color: var(--muted); }
.prestige__msdate--approx { color: var(--faint); font-style: italic; }
.prestige__nxval { margin: 0; font-size: 12px; color: var(--pg-gold); white-space: nowrap; }
.prestige__nxbar {
  margin-top: 6px; height: 5px; border-radius: 999px;
  background: rgba(0, 0, 0, 0.32); box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.5); overflow: hidden;
}
.prestige__nxbar span { display: block; height: 100%; width: calc(var(--p, 0) * 100%); border-radius: 999px; background: var(--pg-gold-soft); }

/* THE MILESTONE EMPTY STATE: premium, not a void. */
.prestige__panel--empty { position: relative; overflow: hidden; }
.prestige__emptywrap {
  position: relative; display: flex; flex-direction: column; align-items: flex-start;
  justify-content: center; min-height: 118px; gap: 8px;
}
.prestige__emptymark, .prestige__emptymark--none {
  position: absolute; right: -12px; bottom: -18px; width: 116px; height: 116px;
  opacity: 0.08; filter: grayscale(0.2); pointer-events: none;
}
.prestige__emptymark img { width: 100%; height: 100%; object-fit: contain; }
.prestige__emptylead { position: relative; margin: 0; max-width: 78%; font-size: 14px; line-height: 1.45; color: var(--text); }
.prestige__emptynudge { position: relative; margin: 0; font-size: 12px; color: var(--pg-gold); }

/* ---- THE ONE-SHOT REVEAL ---- */
.prestige:not(.is-revealed) .prestige__gapfill,
.prestige:not(.is-revealed) .prestige__tilebar span,
.prestige:not(.is-revealed) .prestige__nxbar span { width: 0; }
.prestige:not(.is-revealed) .prestige__rung { opacity: 0; transform: translateY(7px); }
.prestige__gapfill, .prestige__tilebar span, .prestige__nxbar span { transition: width 900ms cubic-bezier(0.22, 1, 0.36, 1); }
.prestige__rung { transition: opacity 420ms ease, transform 420ms ease; transition-delay: calc(var(--i, 0) * 20ms); }

@media (prefers-reduced-motion: reduce) {
  .prestige__stripline, .prestige__gapfill, .prestige__tilebar span,
  .prestige__nxbar span, .prestige__rung, .prestige__rungart { transition: none; }
  .prestige:not(.is-revealed) .prestige__rung { opacity: 1; transform: none; }
  .prestige__rung.is-current::after { animation: none; }
}

@media (max-width: 900px) {
  .prestige__hero { grid-template-columns: minmax(0, 1fr); }
  .prestige__next { justify-content: flex-start; padding-left: 0; border-left: 0; }
  .prestige__nexttext { text-align: left; }
  .prestige__tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .prestige__panels { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 520px) {
  .prestige__tiles { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 640px) {
  .dlock-idxrow { grid-template-columns: 15px minmax(0, 1fr) 44px auto; }
  .dlock-spark { width: 44px; }
}

/* ---------- 7b. CATEGORY INDEXES ----------
   One row per index: what it is, the line it has drawn over the window
   the document carries, where it stands and what it did today. */
.dashidx__list { margin: 0; padding: 0; list-style: none; }

.dashidx__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 88px auto auto;
  align-items: center;
  column-gap: 12px;
  padding: 8px 0;
  border-top: 1px solid var(--edge);
}

.dashidx__row:first-child { border-top: 0; padding-top: 0; }

.dashidx__label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12.5px;
  color: rgba(255, 255, 255, 0.86);
}

.dashidx__spark { display: block; width: 88px; height: 24px; overflow: visible; color: rgba(255, 255, 255, 0.55); }
.dashidx__spark path { stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; }
.dashidx__spark--empty { display: block; width: 88px; height: 24px; }

.dashidx__row.is-up   .dashidx__spark { color: #57d38c; }
.dashidx__row.is-down .dashidx__spark { color: #f26569; }

.dashidx__value { font-size: 12.5px; color: rgba(255, 255, 255, 0.72); font-variant-numeric: tabular-nums; }
.dashidx__change { min-width: 62px; text-align: right; font-size: 12.5px; font-variant-numeric: tabular-nums; color: rgba(255, 255, 255, 0.72); }
.dashidx__row.is-up   .dashidx__change { color: #57d38c; }
.dashidx__row.is-down .dashidx__change { color: #f26569; }

/* ---------- 7c. THE COLLECTION ----------
   How much of it is held, what it is made of, and the cards themselves.
   Every number drawn here is a count the response carries. There is no
   percentage anywhere in this module, by design: see the wash in 8f1.

   ---------- THE SUMMARY: ONE FIGURE AND FOUR COUNTS ----------
   The module used to open with a gold bar across the top and four thin
   bars under it. All five were proportions, and a proportion is exactly
   what this collection has least of: on a member holding one legendary
   card of forty, that rarity's bar was two pixels of a hundred. The
   figure and the four counts say strictly more in the same band, and
   not one of them gets smaller as the collection gets emptier. */
.dsum {
  display: flex;
  align-items: center;
  gap: clamp(18px, 2.4vw, 38px);
  padding-bottom: 26px;
  border-bottom: 1px solid var(--line);
}

.dsum__fig { min-width: 0; }

.dsum__num {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin: 0;
}

.dsum__num b {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(46px, 3.9vw, 72px);
  line-height: 0.85;
  letter-spacing: -0.03em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.dsum__num s {
  text-decoration: none;
  font-size: clamp(14px, 1vw, 17px);
  color: var(--faint);
  font-variant-numeric: tabular-nums;
}

.dsum__cap {
  margin: 8px 0 0;
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
}

/* ---------- THE FOUR COUNTS ----------
   One plate each, the rarity as a 4px edge rather than as the figure's
   colour: epic measured 3.92 against this panel and common 3.53, both
   under the 4.5 a 21px figure owes, while as a 4px graphic both clear
   3:1 comfortably. So the colour is the edge and the number is legible. */
.crar-list {
  display: flex;
  flex-wrap: wrap;
  gap: 11px;
  margin: 0 0 0 auto;
  padding: 0;
  list-style: none;
}

.crar {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 11px 17px 11px 13px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: #101012;
}

.crar__edge {
  flex: none;
  width: 4px;
  height: 26px;
  border-radius: 2px;
  background: var(--card-rarity, var(--r-common));
}

.crar[data-rarity="legendary"],
.cwall-reel__t[data-rarity="legendary"] { --card-rarity: var(--r-legendary); }
.crar[data-rarity="epic"],
.cwall-reel__t[data-rarity="epic"]      { --card-rarity: var(--r-epic); }
.crar[data-rarity="rare"],
.cwall-reel__t[data-rarity="rare"]      { --card-rarity: var(--r-rare); }
.crar[data-rarity="common"],
.cwall-reel__t[data-rarity="common"]    { --card-rarity: var(--r-common); }

.crar__stack { display: block; min-width: 0; }

.crar__count {
  display: block;
  font-size: clamp(17px, 1.2vw, 21px);
  font-weight: 500;
  line-height: 1;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.crar__label {
  display: block;
  margin-top: 4px;
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}

.cshelf__head {
  margin: 20px 0 10px;
  font-size: 10.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.62);
}

.cshelf {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(112px, 1fr));
  gap: 12px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.cshelf__card { min-width: 0; }

.cshelf__card img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 1;
  object-fit: cover;
  border-radius: 8px;
  box-shadow: 0 0 0 1px var(--card-rarity, rgba(255, 255, 255, 0.1));
}

.cshelf__card[data-rarity="legendary"] { --card-rarity: var(--r-legendary); }
.cshelf__card[data-rarity="epic"]      { --card-rarity: var(--r-epic); }
.cshelf__card[data-rarity="rare"]      { --card-rarity: var(--r-rare); }
.cshelf__card[data-rarity="common"]    { --card-rarity: var(--r-common); }

.cshelf__name {
  display: block;
  margin-top: 6px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 11px;
  color: rgba(255, 255, 255, 0.66);
}

.collection__none { margin: 16px 0 0; font-size: 12.5px; color: rgba(255, 255, 255, 0.62); }

/* ============================================================
   THE WEAR LINE (renderCollection, free or lapsed readers only)

   Everybody signed in sees their own collection on the same wall; what
   membership buys is wearing a card. This is the one line that says so,
   under the summary, with the ask. A member never gets it.
   ============================================================ */
.cwear {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px 22px;
  flex-wrap: wrap;
  margin: 18px 0 6px;
  padding: 14px 18px;
  border-radius: 12px;
  background: rgba(228, 174, 57, 0.06);
  box-shadow: inset 0 0 0 1px rgba(228, 174, 57, 0.22);
}
.cwear__say {
  margin: 0;
  max-width: 60ch;
  font-size: 14px;
  line-height: 1.5;
  color: var(--muted);
}
.cwear__say b { color: var(--text); font-weight: 600; }
.cwear__cta { flex: none; }


/* ============================================================
   8. THE MEMBER PAGE, SECOND PASS
   The banner's text gets a bed it can be read on, the grid uses the
   width the cap promised, and the tail of the page stops holding space
   for a block that is not there.
   ============================================================ */

/* ---------- 8a. THE GRID USES ITS MEASURE ----------
   Fixed where the sum lives, in the grid and the strip above it, rather
   than overridden for one view: see the note on main's padding-inline. */

/* ---------- 8b. THE BANNER: TWO ELEMENTS, ONE ROW ----------

   THE FULL BLEED BANNER IS GONE, AND SO IS EVERYTHING THAT PROPPED IT
   UP. The card used to fill the banner with the identity dimmed over
   it. That could never work: all 303 calling cards carry their title in
   the lower left, which is exactly where the identity sat, so on every
   one of them the identity covered the card's title. Fade tuning moved
   the problem around; it did not solve it.

   So they stop overlapping. An identity panel and a card frame, side by
   side with a gap, and nothing on the card but its own corner block.
   The left to right fade, the opaque bed and every rule that existed to
   make text legible over artwork are deleted rather than disabled:
   there is no artwork under the text any more. */

.dash--member #playerCardSlot .statushero__player, .dash--free #playerCardSlot .statushero__player {
  display: grid;
  /* THE PANEL LEADS, 58 / 42. It was 44 when it held an identity and
     nothing else; it holds the climb and four stats as well, and at 44
     its two inner columns came out at 175px each, which wrapped Exit
     Liquidity and Diamond Hands onto two lines.

     IT STAYS 58 EVEN THOUGH THE CARD WANTS MORE. Moving it to 54 was
     tried and measured: it buys the card 42px of width, which is 14px
     off the matte at 1280, and costs the climb column 9px it does not
     have to spare. The card takes its width from the gap instead,
     which costs the panel nothing. */
  grid-template-columns: minmax(0, 58fr) minmax(0, 42fr);

  /* The card starts closer to the panel and keeps the difference as
     width on its own left edge. Both panels carry a 1px tier edge and
     an angled corner, so the gap only has to read as a division, not
     as a margin. */
  gap: clamp(8px, 0.7vw, 11px);
  width: 100%;
  margin: 0;
  /* Room at the top and left for the emblem to break out into without
     leaving the module, which clips. */
  padding: clamp(16px, 1.8vw, 26px) 0 0 clamp(14px, 1.6vw, 22px);
  border-radius: 0;
  background: none;
  box-shadow: none;
  overflow: visible;
  aspect-ratio: auto;
  min-height: 0;
}

/* ---------- The identity ---------- */

/* The wrapper is NOT clipped, so the emblem can leave the panel.

   IT IS ALSO THE QUERY CONTAINER. The panel's inner split has to answer
   to the PANEL's width, not the viewport's: the same 1280 viewport gives
   a different panel width depending on the sidebar, and a media query
   would guess. An element cannot query itself, so the container is the
   wrapper and the rules land on the panel inside it.

   inline-size containment does not clip: that is paint containment. The
   emblem still breaks the wrapper's top and left edges exactly as before. */
.dash--member .statushero__idwrap, .dash--free .statushero__idwrap {
  position: relative;
  min-width: 0;
  container: idpanel / inline-size;
}

/* THE ANGLED CORNERS. Two opposite ones, top left and bottom right, cut
   at 22px. A plain radius is what every other panel on the page has;
   this element is meant to read as a different kind of surface, and the
   cut is the cheapest honest way to say so.

   clip-path cannot take a border, so the edge is done as two stacked
   shapes: this element is the tier colour and carries the outline, and
   ::before is the surface inset by 1px on top of it. */
.dash--member .statushero__panel, .dash--free .statushero__panel {
  --cut: 22px;

  position: relative;
  height: 100%;
  /* TWO LEFT EDGES, AND THAT IS THE WHOLE TRICK. The identity's upper
     rows have to clear the emblem, so that column is inset past it. The
     buttons sit BELOW the emblem's bottom, so they do not, and pulling
     them back to the panel's own inset is what lets them sit side by
     side at 1280. Inset everything and they wrap; inset nothing and the
     emblem lands on the rank name. */
  --inset: clamp(16px, 1.6vw, 22px);
  --emblem-gutter: clamp(98px, 11.8vw, 154px);

  /* ---------- THE PLATE SYSTEM ----------

     ONE ANGLE, AND IT IS THE CORNERS' ANGLE. The panel is chamfered at
     the top left and the bottom right, and those two cuts are lines of
     slope -1. Every plate seam below is parallel to them, which is what
     makes the chamfers read as part of the plate system rather than as
     a radius that happens to be square. A 135deg linear-gradient's
     bands ARE lines of slope -1, at a true 45 degrees whatever the
     panel's aspect ratio, so the angle cannot drift with the viewport
     the way a percentage polygon would.

     --plate is the raised corner plate's leg. --plate-axis is that leg
     measured along the 135deg gradient axis, which is the leg over root
     two: gradient stops are positioned along that axis, and this is
     what puts the lit lip exactly ON the plate's edge at every size
     rather than somewhere near it. */
  --plate: clamp(116px, 19vw, 286px);
  --plate-axis: calc(var(--plate) * 0.7071);

  /* Where the two base plates meet, along the same axis. */
  --seam: 46%;

  /* The lit lip's strength, named because it is the one number on this
     surface that adds light where text sits, so it is the first thing
     to reach for if a line ever comes up short. It is the same at every
     width: the plates read the same on a phone as on a desk. */
  --lip: 0.055;

  padding: clamp(18px, 2vw, 26px) var(--inset);

  /* IDENTITY LEFT, CLIMB RIGHT, THE STAT ROW UNDER BOTH. One track by
     default and the three blocks simply stack, which is what a phone
     gets; the container query below turns it into two.

     align-items: stretch so the climb's left border, which is the
     hairline dividing the two columns, runs the full height of the
     taller of them rather than only as far as its own text. */
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-items: stretch;

  /* THE GAP IS PAID TWICE, which is why it read as too much. This
     column-gap sets where the dividing hairline falls, and the climb's
     own padding-left then insets its text by the same amount again, so
     the identity's last character and the climb's first were 38px
     apart at 1280 and 44px at 1920 with a line in the middle. Both
     halves come down together so the hairline stays centred in the
     space it divides. */
  column-gap: clamp(10px, 0.95vw, 14px);

  background: var(--tier, #5878a8);
  clip-path: polygon(
    var(--cut) 0, 100% 0,
    100% calc(100% - var(--cut)), calc(100% - var(--cut)) 100%,
    0 100%, 0 var(--cut));
}

.dash--member .statushero__panel::before, .dash--free .statushero__panel::before {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: 0;

  /* THE SURFACE. Three layers, in paint order:

       THE SEAM, where the lower plate passes under the upper one. Two
       marks, not one: a soft occlusion ramp running INTO the seam,
       which is the crease, and a thin lit lip on the far side of it,
       which is the upper plate's leading edge catching light from
       above. A value step on its own reads as gradient banding; the
       crease and the lip are what make it read as two objects;

       a fine grain, from feTurbulence at 0.9 baseFrequency. It is
       there to be felt as material and not seen as pattern, so it sits
       at 0.05 opacity. No repeating-linear-gradient: a diagonal hatch
       reads as a broken image, which this file already learned once;

       THE TWO BASE PLATES. The lower one runs from the top left corner
       to the seam, the upper one from the seam to the bottom right,
       and the upper one is lifted about five points of value at the
       seam so the step reads as a plate laid on top rather than as one
       plate getting darker. */
  background:
    linear-gradient(135deg,
      rgba(0, 0, 0, 0) 0 38%,
      rgba(0, 0, 0, 0.32) var(--seam),
      /* 0.055 AND 0.9%, NOT 0.085 AND 1.4%. The lip is a light source
         landing on whatever happens to be above it, and at 1.4% of the
         gradient axis it is an 11px band at 1920: wide enough to sit
         under a whole line of type. It put the streak's second line on
         rgb(49,53,61) and took it to 4.38 against a 4.5 floor. */
      rgba(255, 255, 255, var(--lip)) var(--seam),
      rgba(255, 255, 255, 0) calc(var(--seam) + 0.9%),
      rgba(0, 0, 0, 0) 100%),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='0.05'/%3E%3C/svg%3E"),
    linear-gradient(135deg,
      #181d25 0%,
      #12161e var(--seam),
      #151a23 var(--seam),
      #0d1017 100%);
  clip-path: polygon(
    var(--cut) 0, 100% 0,
    100% calc(100% - var(--cut)), calc(100% - var(--cut)) 100%,
    0 100%, 0 var(--cut));
  pointer-events: none;
}

/* Everything inside sits above the surface layer. */
.dash--member .statushero__ident, .dash--free .statushero__ident {
  position: relative;
  z-index: 1;
  inset: auto;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
  height: auto;
  min-width: 0;
  /* Clears the emblem. The panel's own inset is already applied, so
     this is only the difference between the two. */
  padding-left: calc(var(--emblem-gutter) - var(--inset));
}

/* ---------- THE RIGHT COLUMN ----------

   THE CLIMB AND THE FOUR STATS. Both are drawn only when they have
   something real to say, so this column can hold one, the other, or
   neither, and the panel reads correctly in all three cases: with
   neither, js/app.js leaves the column out entirely and the grid falls
   back to one track. */
.dash--member .statushero__side, .dash--free .statushero__side {
  position: relative;
  z-index: 1;
  min-width: 0;
  display: contents;
}

/* STACKED IS THE DEFAULT, and the hairline runs across rather than
   down: a vertical rule between two blocks that sit one above the
   other divides nothing. */
.dash--member .statushero__climb, .dash--free .statushero__climb {
  padding-top: clamp(12px, 1.3vw, 16px);
  border-top: 1px solid rgba(255, 255, 255, 0.1);
}

@container idpanel (min-width: 470px) {
  /* TWO COLUMNS, AND THE STAT ROW UNDER BOTH. The row is the panel's
     full inner width rather than the right column's: at 1280 the right
     column is 217px and four cells at a 96px minimum cannot sit across
     it, so auto-fit stacked them into a single column of four. Spanning
     the panel gives them 520px and they sit in one row, which is what
     the reference shows and what makes them read as a row of stats
     rather than as a list. */
  .dash--member .statushero__panel, .dash--free .statushero__panel {
    grid-template-columns: minmax(0, 1.8fr) minmax(0, 1fr);
    grid-template-rows: auto auto;
    align-content: start;
    row-gap: clamp(12px, 1.3vw, 18px);
  }
  .dash--member .statushero__ident, .dash--free .statushero__ident { grid-column: 1; grid-row: 1; }
  .dash--member .statushero__climb, .dash--free .statushero__climb {
    grid-column: 2;
    grid-row: 1;
    /* The other half of the gap: see the note on column-gap above. */
    padding: 0 0 0 clamp(10px, 0.95vw, 14px);
    border-top: 0;
    border-left: 1px solid rgba(255, 255, 255, 0.1);
  }
  .dash--member .statushero__statrow, .dash--free .statushero__statrow { grid-column: 1 / -1; grid-row: 2; }

  /* ONE TRACK PER CELL, AND THE COUNT COMES FROM THE MARKUP. js/app.js
     drops any stat whose source has nothing real in it and writes the
     surviving count to data-cells, so a member with no streak gets three
     even tracks rather than four with a hole in the fourth. */
  .dash--member .statushero__statrow[data-cells="1"], .dash--free .statushero__statrow[data-cells="1"] { grid-template-columns: minmax(0, 1fr); }
  .dash--member .statushero__statrow[data-cells="2"], .dash--free .statushero__statrow[data-cells="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dash--member .statushero__statrow[data-cells="3"], .dash--free .statushero__statrow[data-cells="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .dash--member .statushero__statrow[data-cells="4"], .dash--free .statushero__statrow[data-cells="4"] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* ---------- The climb ---------- */
.dash--member .statushero__climb, .dash--free .statushero__climb {
  position: relative;
  z-index: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* LEVEL 5, THE LABEL VOICE. This label, the four stat labels and the
   goal caption are the same thing said in three places, so they are one
   size, one weight and one colour. 500 at 9.5px is not loudness: the
   face was being rendered at its thinnest available weight and the
   counters were closing up at this size.

   #8d9bb4 on the plates measures 4.84 at its worst, against a 4.5
   floor. The old --faint would have been 3.8, which is the value this
   file already had to lift once. */
.dash--member .statushero__climblabel, .dash--free .statushero__climblabel {
  margin: 0;
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: #8d9bb4;
}

.dash--member .statushero__climbmain, .dash--free .statushero__climbmain {
  display: flex;
  align-items: flex-start;
  gap: clamp(12px, 1.4vw, 20px);
  min-width: 0;
}

.dash--member .statushero__climbcol, .dash--free .statushero__climbcol {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.dash--member .statushero__barrow, .dash--free .statushero__barrow {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

.dash--member .statushero__bar, .dash--free .statushero__bar {
  flex: 1 1 auto;
  min-width: 0;
  height: 8px;
  border-radius: 999px;
  /* THE TRACK IS CUT INTO THE PLATE, NOT LAID ON IT. As a translucent
     white it took its value from whichever plate was beneath it, and on
     the lightest one that was rgb(68,73,80), where the fill measured
     2.45 against a 3:1 floor. Cut in, it is dark on every plate and the
     fill clears on all of them. The inset highlight along the bottom is
     the far wall of the groove catching the same light the plate lips
     do, which is what stops it reading as a painted dark pill. */
  background: rgba(0, 0, 0, 0.42);
  box-shadow:
    inset 0 1px 1px rgba(0, 0, 0, 0.5),
    inset 0 -1px 0 rgba(255, 255, 255, 0.05);
  overflow: hidden;
}

.dash--member .statushero__barfill, .dash--free .statushero__barfill {
  display: block;
  height: 100%;
  border-radius: inherit;
  /* NOT the tier colour. The tier colour is already the panel's edge and
     the glow behind the emblem, and at the steel end of the scale it is
     too close to the track to read as a fill. --accent is the one
     colour on this page that means progress. */
  background: var(--accent);
}

/* LEVEL 2, THE SECOND VOICE. It was 19px at 1280 against a rank name of
   19.84: three elements within a point of each other is not a
   hierarchy, it is three headlines. 17.3px at 1280 and 22px at 1920
   puts a clear step under the rank name and a clear step over the
   10.5px label and 12.5px prose it sits between.

   500, not 300. The loaded JetBrains Mono carries 400 and 500 only, so
   every mono element on this panel was being clamped up to 400 and
   every one of them looked identical. 500 is a real weight here and it
   is the only thing separating the figures from the labels. */
.dash--member .statushero__pct, .dash--free .statushero__pct {
  flex: none;
  font-size: clamp(16px, 1.35vw, 22px);
  font-weight: 500;
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--text);
}

.dash--member .statushero__gap, .dash--free .statushero__gap {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.35;
  color: var(--muted);
  overflow-wrap: anywhere;
}

/* Carried by weight rather than size: it sits inside a sentence and a
   size jump mid-line would break the line's rhythm. */
.dash--member .statushero__gapfig, .dash--free .statushero__gapfig { font-weight: 500; color: var(--text); }
.dash--member .statushero__gapgoal, .dash--free .statushero__gapgoal { color: var(--text); }

/* The destination. Decorative, so it is the first thing dropped when
   the column gets tight: the rank it names is in the sentence. */
.dash--member .statushero__goal, .dash--free .statushero__goal {
  flex: none;
  /* 72 AT THE FLOOR, NOT 58, AND THE LONGEST WORD SETS IT. The caption
     carries overflow-wrap: anywhere so a rank name can never push the
     column wider than its track, but at 58px that permission was being
     used: at 768 the panel is full width, the goal is drawn, and Exit
     Liquidity came out as EXIT / LIQUID / ITY, broken mid word across
     three lines. LIQUIDITY is the longest word on the ladder and needs
     about 60px at this size and tracking, so 72 holds it and the break
     falls at the space where it belongs. */
  width: clamp(72px, 6vw, 78px);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  text-align: center;
}

.dash--member .statushero__goalart, .dash--free .statushero__goalart {
  width: 100%;
  height: auto;
  object-fit: contain;
  filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.5));
}

.dash--member .statushero__goalname, .dash--free .statushero__goalname {
  margin: 0;
  font-family: var(--font-body);
  font-size: 9.5px;
  font-weight: 400;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  line-height: 1.2;
  color: #8d9bb4;
  overflow-wrap: anywhere;
}

@container idpanel (max-width: 620px) {
  .dash--member .statushero__goal, .dash--free .statushero__goal { display: none; }

/* The no-card affordance moved onto the placeholder card frame below
   (pass 2): the free banner now always carries a card slot, so it is no
   longer --bare and this panel hint would never show. */

}

/* A stacked panel is tall, so the one seam crosses the identity, the
   climb AND the stat row rather than mostly empty surface. The lip is
   the only part of it that adds light, so it is the only part that
   needs to give way. The crease and the value step are untouched, which
   is what keeps the plates reading at this width. */
@container idpanel (max-width: 469px) {
  /* THE GLOW HUGS THE EMBLEM ONCE THE PANEL STACKS, and this is a real
     bug the wide case hid rather than a tuning preference.

     The wide geometry stops the glow at 22% of the panel's width
     because that is where the identity's text column starts, past the
     emblem gutter. Stacked, the climb and the stat row do NOT start
     there: only .statushero__ident carries the gutter, so those two
     begin at the panel's own inset, about 22px in, which is inside the
     glow. A 66% vertical radius then runs it down almost the whole of
     a 359px tall panel, so it was lighting the climb and the stats
     rather than the emblem.

     Sized to the emblem instead: at 320 that mark is 104px wide and
     breaks the corner by 8, so it occupies roughly the first 96px
     across and 94px down, and the glow now fades out inside 118 and
     121. The climb below it is clear of it entirely. */
  .dash--member .statushero__glow, .dash--free .statushero__glow {
    background: radial-gradient(28% 22% at 15% 12%,
      color-mix(in srgb, var(--tier, #5878a8) 13%, transparent) 0%,
      transparent 72%);
  }
}

/* The top of the ladder. No bar, no gap figure, no empty goal slot. */
.dash--member .statushero__climbsay, .dash--free .statushero__climbsay {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.35;
  color: var(--muted);
}

/* ---------- The four stats ----------

   auto-fit, not four fixed tracks: js/app.js drops any stat whose source
   has nothing real in it, so this row is handed two, three or four cells
   and has to look deliberate at each count. */
.dash--member .statushero__statrow, .dash--free .statushero__statrow {
  position: relative;
  z-index: 1;
  min-width: 0;
  display: grid;
  /* TWO ACROSS ON A PHONE, whatever the count. auto-fit decided the
     column count from the available width and produced a 3 + 1 orphan
     at 375 and at 1024; the row is short enough that a fixed pair reads
     better than a ragged last row. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  row-gap: clamp(8px, 0.9vw, 12px);
  padding-top: clamp(10px, 1.1vw, 14px);
  border-top: 1px solid rgba(255, 255, 255, 0.1);
}

.dash--member .statushero__stat, .dash--free .statushero__stat {
  display: flex;
  align-items: flex-start;
  gap: 7px;
  min-width: 0;
  padding: 2px clamp(8px, 0.9vw, 12px);
  /* The hairline between cells, not around them: the first cell sits
     flush with the column's own left edge. */
  border-left: 1px solid rgba(255, 255, 255, 0.09);
}

.dash--member .statushero__stat:first-child, .dash--free .statushero__stat:first-child {
  padding-left: 0;
  border-left: 0;
}

.dash--member .statushero__glyph, .dash--free .statushero__glyph {
  flex: none;
  width: 14px;
  height: 14px;
  margin-top: 1px;
  /* Decorative, beside a label that says the same thing in words. */
  color: #8d9bb4;
}

.dash--member .statushero__statbody, .dash--free .statushero__statbody { min-width: 0; }

/* LEVEL 3. Mono and 500, against labels that are body face and 400: the
   figures and their labels now differ in size, weight AND face, which
   is what lets the row be scanned as figures rather than read as
   sentences. */
.dash--member .statushero__statfig, .dash--free .statushero__statfig {
  margin: 0;
  font-size: clamp(13.5px, 1.1vw, 15.5px);
  font-weight: 500;
  line-height: 1.15;
  color: var(--text);
  white-space: nowrap;
}

.dash--member .statushero__statlabel, .dash--free .statushero__statlabel {
  margin: 2px 0 0;
  font-family: var(--font-body);
  font-size: 9.5px;
  font-weight: 400;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  line-height: 1.25;
  color: #8d9bb4;
}

/* Under a level 3 figure, so it stays a step below it and keeps the
   mono face: it is a number too. */
.dash--member .statushero__statsub, .dash--free .statushero__statsub {
  margin: 1px 0 0;
  font-size: 9.5px;
  line-height: 1.2;
  color: #8d9bb4;
}

/* ---------- THE RANK GLOW ----------

   ITS OWN ELEMENT, AND THAT IS WHAT MAKES IT BREATHE. It was one of
   the plate stack's background layers, and a background layer has no
   opacity of its own: breathing it there would have breathed the
   plates, the seam and the grain along with it. On its own span it is
   one property on one element, which is the cheapest thing a browser
   can animate.

   Geometry unchanged from the plate stack it left: 21% by 66% at 8%
   30%, spent by 22% of the panel's width, which is where the text
   column starts. It lights the surface the emblem sits on and stops
   before the surface the text sits on.

   The colour is --tier, set by tierStyle() in js/app.js from
   tierColour(), which is the same property the panel's 1px edge reads.
   No second opinion about which tier this is. */
.dash--member .statushero__glow, .dash--free .statushero__glow {
  position: absolute;
  inset: 1px;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(21% 66% at 8% 30%,
    color-mix(in srgb, var(--tier, #5878a8) 13%, transparent) 0%,
    transparent 72%);
}

/* ---------- THE THIRD PLATE, RAISED ----------

   THE OTHER TWO ARE PAINTED, THIS ONE IS A REAL OBJECT. Its edge is
   another line of slope -1, so it is parallel to both chamfers and to
   the seam above, and the panel's own bottom right cut trims its tip:
   the plate and the corner become the same cut, which is the point.

   THE SHADOW IS CAST, NOT DRAWN. filter: drop-shadow follows the
   CLIPPED shape, where a box-shadow would be clipped away with the
   rest of the box and never appear at all. Offset up and left, toward
   the light, so what lands on the plate below is the crease along this
   plate's leading edge rather than a halo around the whole triangle.

   WHY THE BOTTOM RIGHT. A line of slope -1 can only cut the top left
   or the bottom right corner of a box, and the top left is the
   emblem's. A plate edge there would have crossed the one picture this
   panel is allowed to have. */
.dash--member .statushero__panel::after, .dash--free .statushero__panel::after {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: 0;
  clip-path: polygon(
    calc(100% - var(--plate)) 100%,
    100% calc(100% - var(--plate)),
    100% 100%);

  /* The lit lip sits ON the edge, not near it: --plate-axis is the
     plate's leg measured along this gradient's own axis, so the stop
     lands on the hypotenuse at every panel size. */
  background: linear-gradient(135deg,
    rgba(255, 255, 255, 0) calc(100% - var(--plate-axis) - 2px),
    rgba(255, 255, 255, 0.045) calc(100% - var(--plate-axis)),
    rgba(255, 255, 255, 0) calc(100% - var(--plate-axis) + 7px),
    rgba(255, 255, 255, 0.016) 100%);
  filter: drop-shadow(-4px -4px 9px rgba(0, 0, 0, 0.55));
  pointer-events: none;
}

/* ---------- The emblem, breaking the boundary ---------- */
.dash--member .statushero__emblem, .dash--free .statushero__emblem {
  position: absolute;
  z-index: 2;
  /* It clears the panel's top and left edges rather than sitting inside
     them. The offsets are what make it read as designed rather than
     placed, and they are what the row's padding above exists to hold. */
  top: clamp(-30px, -2.2vw, -18px);
  left: clamp(-24px, -1.8vw, -14px);
  width: clamp(118px, 13vw, 182px);
  height: clamp(118px, 13vw, 182px);
  margin: 0;
  object-fit: contain;
  filter: drop-shadow(0 6px 18px rgba(0, 0, 0, 0.55));
  pointer-events: none;
}

/* ---------- The identity's lines ----------
   All on plate now, so none of them needs the treatment they carried
   to survive artwork. The plates differ in value, so each line is
   measured against the plate actually beneath it rather than against
   an average of the panel: see the report.

   THE TIER LINE IS GONE WITH ITS RULE. It printed index + 1, and
   js/ranks.js:169 builds `index` from the loop that walks TIERS and
   documents `tierIndex` as "0 based position in TIERS, same as index
   now". There is no authored tier number on the ladder to print, so the
   rank names carry the tier on their own. */
.dash--member .statushero__rankrow, .dash--free .statushero__rankrow {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
}

.dash--member .statushero__ranklabel, .dash--free .statushero__ranklabel {
  margin: 0;
  /* Sized so the longest rung fits the text column rather than so the
     shortest looks biggest. */
  font-size: clamp(22px, 2.4vw, 34px);
  line-height: 1.02;
  min-width: 0;
  overflow-wrap: anywhere;
  background: linear-gradient(180deg, #fff3cf 0%, #f8cf6a 62%, #f5c451 100%);
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
}

.dash--member .statushero__memberchip, .dash--free .statushero__memberchip {
  display: inline-flex;
  align-items: center;
  padding: 4px 10px 3px;
  border-radius: 999px;
  background: var(--accent);
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: #050505;
}

/* LEVEL 4, THE QUIET LAYER, and this one moved furthest. At 19px in
   Saira it was within a point of the rank name above it, and the
   loaded Saira is a 600..700 variable face: the 400 asked for here was
   being clamped up to 600, so it was nearly the rank name's size AND
   nearly its weight. It is the body face now, which is what the Steam
   link beside it already uses, at a size that reads as the answer to
   the rank rather than as a second headline. */
.dash--member .statushero__name, .dash--free .statushero__name {
  margin: 0;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: clamp(13px, 1.05vw, 15px);
  line-height: 1.25;
  color: var(--muted);
  overflow: visible;
  white-space: normal;
}

.dash--member .statushero__steam, .dash--free .statushero__steam {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 26px;
  padding: 3px 0;
  font-size: 12.5px;
  color: var(--muted);
  text-decoration: none;
}

.dash--member .statushero__steam:hover, .dash--free .statushero__steam:hover { color: var(--text); }

.dash--member .statushero__steam .statushero__avatar, .dash--free .statushero__steam .statushero__avatar {
  width: 20px;
  height: 20px;
  border-radius: 5px;
}

.dash--member .statushero__out, .dash--free .statushero__out { flex: none; opacity: 0.8; }

.dash--member .statushero__actions, .dash--free .statushero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 2px;
  /* THE BUTTONS SIT BELOW THE EMBLEM, so they do not need to clear it.
     Pulling back by the identity column's own padding puts them at the
     column's left edge, which is where the panel's inset already is. */
  margin-left: calc(var(--inset) - var(--emblem-gutter));
}

.dash--member .statushero__ghost, .dash--free .statushero__ghost {
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
  min-height: 34px;
  padding: 0 12px;
  border: 1px solid var(--edge);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.04);
  font-size: 12.5px;
  letter-spacing: 0.02em;
  color: var(--text);
  text-decoration: none;
}

.dash--member .statushero__ghost:hover, .dash--free .statushero__ghost:hover {
  border-color: rgba(255, 255, 255, 0.3);
  background: rgba(255, 255, 255, 0.08);
}

/* ---------- TWO COLUMN SIZING ----------

   THIS BLOCK SITS HERE, NOT WITH THE REST OF THE CONTAINER QUERY, AND
   THE POSITION IS THE POINT. A container query carries no extra
   specificity: these selectors are the same weight as the plain ones
   above, so whichever comes last in the file wins. Written beside the
   layout block it belongs to, it lost to the emblem and rank rules
   below it, the emblem stayed 166px against a 124px gutter, and it
   covered the first 18px of the rank name at 1280 and 33px at 1920.
   Measured, not assumed: see the report.

   THE EMBLEM PAYS FOR THE SECOND COLUMN. At 166px it took 151px of the
   panel before the identity's first character, which left the rank name
   140px and wrapped Exit Liquidity and Diamond Hands onto two lines.
   140px is still the largest mark on the banner and it buys the climb a
   column wide enough to hold a bar. */
@container idpanel (min-width: 470px) {
  .dash--member .statushero__panel, .dash--free .statushero__panel { --emblem-gutter: clamp(92px, 9.8vw, 124px); }
  .dash--member .statushero__emblem, .dash--free .statushero__emblem {
    width: clamp(104px, 11vw, 140px);
    height: clamp(104px, 11vw, 140px);
  }
  .dash--member .statushero__ranklabel, .dash--free .statushero__ranklabel { font-size: clamp(17px, 1.55vw, 28px); }
}


/* FOCUS RINGS ARE NOT CLIPPED. The panel is clip-path'd and clip-path
   clips descendants, so an outline on a control inside it would be cut
   at the panel edge. The controls are inset well clear of the cuts and
   the ring is drawn inside its own box, so nothing of it lands on a
   clipped edge. Measured, not assumed: see the report. */
.dash--member .statushero__ghost:focus-visible, .dash--free .statushero__ghost:focus-visible,
.dash--member .statushero__steam:focus-visible, .dash--free .statushero__steam:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ---------- The card, complete and uncovered ----------

   Content sized here, so the frame is exactly its artwork and nothing
   more. From 1200 up it is stretched to the identity panel's height
   instead: see THE TWO PANELS AS ONE PAIR below. */
.dash--member .statushero__cardframe, .dash--free .statushero__cardframe {
  --cut: 22px;

  position: relative;
  align-self: center;
  min-width: 0;
  padding: 1px;
  background: var(--tier, #5878a8);
  clip-path: polygon(
    var(--cut) 0, 100% 0,
    100% calc(100% - var(--cut)), calc(100% - var(--cut)) 100%,
    0 100%, 0 var(--cut));
}

.dash--member .statushero__cardart, .dash--free .statushero__cardart {
  position: relative;
  inset: auto;
  width: 100%;

  /* THE CARD'S OWN SHAPE, and contain inside it, so a crop is not
     possible at any width. The cards measure 3.000 to 3.010 to 1, so
     the letterbox is at worst three tenths of one percent.

     THIS IS ALSO WHAT GIVES THE FRAME ITS HEIGHT wherever the frame is
     not stretched, which is every width below 1200. Swapping it for
     height: 100% everywhere was tried and measured: under 1024 the row
     is a single column, the frame has no definite height to be 100% OF,
     and the whole card collapsed to its 2px of padding at 320, 375 and
     768. The stretched case is handled in its own block below. */
  aspect-ratio: 3 / 1;
  height: auto;
  overflow: hidden;
  background: #07090d;
  border-radius: 0;
  clip-path: polygon(
    calc(var(--cut) - 1px) 0, 100% 0,
    100% calc(100% - var(--cut) + 1px), calc(100% - var(--cut) + 1px) 100%,
    0 100%, 0 calc(var(--cut) - 1px));
}

.dash--member .statushero__cardimg, .dash--free .statushero__cardimg {
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center center;
}

/* ---------- THE FREE CARD IS BLURRED, WITH AN UNLOCK AFFORDANCE ----------
   Wearing a calling card is a paid feature, so a free reader sees the real
   card the server sent them (their own equipped card, ownership re-checked
   server-side) held behind a blur, never a faked or invented one. A member
   keeps their card sharp. The blur is a CSS filter over the real image; the
   affordance is a centred pill, steel not gold, because gold is the
   membership colour and this is a lock. */
.dash--free .statushero__cardframe { position: relative; }
.dash--free .statushero__cardart { position: relative; overflow: hidden; }
/* FULL BRIGHTNESS, SHARP, UNMODIFIED. A free reader sees their real card
   exactly as it is, at full brightness with no blur and no scrim dimming
   it. The only locked cue is the unlock pill on top; the pill carries its
   own contrast so it stays legible over any bright, busy card art. */
.dash--free .statushero__cardimg {
  filter: none;
}
/* THE EMPTY-SLOT PLACEHOLDER (free, no equipped card). A card-shaped
   frame filling the same slot a member's card does, so the banner keeps
   its proportions. It is a placeholder, not a card: no art, just a dark
   plate and one honest line. */
.dash--free .statushero__cardframe--empty {
  position: relative;
  border-radius: inherit;
  min-height: 132px;
  background:
    radial-gradient(120% 140% at 50% 0%, rgba(255, 255, 255, 0.04), transparent 62%),
    linear-gradient(160deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.008));
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
}
/* THE UNLOCK CONTROL (free only). A real anchor centred over the card, so
   it takes the click the blurred art cannot. Ghost-gold, the same unlock
   language every other free CTA on the page speaks, in a pill sized for the
   slot. z-index sits it above the art; pointer-events are on (the art has
   none), and it is a link, so it focuses and Enter-activates for free. */
.dash--free .statushero__unlock {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 5;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: calc(100% - 24px);
  padding: 8px 16px;
  border: 1px solid rgba(255, 209, 122, 0.6);
  border-radius: 999px;
  background: rgba(8, 11, 18, 0.85);
  backdrop-filter: blur(2px);
  /* THE MONEY MOMENT. With no scrim behind it now, the pill carries its own
     contrast: a near-opaque dark bed and a thin dark separation ring so the
     text stays legible over the brightest, busiest card, plus a soft
     gold-ghost glow so it reads as the primary action. Ghost, never the
     solid gold reserved for the two membership CTAs. */
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.4),
    0 2px 16px rgba(0, 0, 0, 0.55),
    0 0 22px -6px rgba(255, 209, 122, 0.55);
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #ffd97a;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  transition: border-color 160ms ease, background-color 160ms ease, color 160ms ease, box-shadow 160ms ease;
}
.dash--free .statushero__unlock:hover {
  border-color: rgba(255, 209, 122, 0.95);
  background: rgba(22, 17, 9, 0.9);
  color: #ffe4a3;
  box-shadow:
    0 2px 18px rgba(0, 0, 0, 0.55),
    0 0 26px -4px rgba(255, 209, 122, 0.75);
}
.dash--free .statushero__unlock:focus-visible {
  outline: 2px solid rgba(255, 209, 122, 0.9);
  outline-offset: 2px;
}
.dash--free .statushero__unlockmark {
  display: inline-flex;
  width: 11px;
  height: 12px;
}
.dash--free .statushero__unlockmark svg { width: 100%; height: 100%; }

/* ---------- THE TWO PANELS AS ONE PAIR ----------

   MATCHED HEIGHTS, AND ONLY WHERE A MATCH LOOKS LIKE A MATCH. The card
   is a strict 3 to 1 that must not crop or stretch, so the only way to
   make it as tall as the identity panel is to give it three times that
   height in width, and the row does not have it. What is left is matte,
   and how much there is decides whether the pair reads as a pair or as
   a card lost in a box. Measured:

     1920   card 640 wide, artwork 213 of a 274 frame, 30px a side
     1280   card 403 wide, artwork 134 of a 263 frame, 64px a side
     1024   card 316 wide, artwork 105 of a 357 frame, 125px a side

   At 1024 the identity panel is 437, which is under the 470 its own two
   columns need, so it stacks and stands 357 tall against a card that
   can only be 105. Matching there is arithmetically possible and
   visually indefensible, so the frame stays centred below 1200 and the
   pair matches from 1200 up. The report says which widths are which.

   1200 rather than 1024 because that is where the identity panel comes
   back above 470 and stops being taller than it needs to be. */
@media (min-width: 1200px) {
  .dash--member .statushero__cardframe, .dash--free .statushero__cardframe { align-self: stretch; }

  /* The ground fills the frame and the artwork letterboxes against it.
     The frame's own background is the tier colour that paints its 1px
     edge, so leaving the difference to the frame showed a 127px band of
     solid violet down the card at 1280. Measured, not guessed. */
  .dash--member .statushero__cardart, .dash--free .statushero__cardart {
    aspect-ratio: auto;
    height: 100%;
  }
}

/* ---------- The card's corner ---------- */
.dash--member .statushero__cardmeta, .dash--free .statushero__cardmeta {
  position: absolute;
  right: 0;
  bottom: 0;
  z-index: 3;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
  text-align: right;
  /* The tightened vignette, unchanged: sized to the text, radii equal to
     the box so the fade completes on its own far corner and there is no
     edge to read as a shape. It reaches 0.90 because the common rarity
     dot only clears 3:1 against a white card pixel from 0.895 up. */
  padding: 34px 26px 20px 86px;
  background: radial-gradient(100% 100% at 100% 100%,
    rgba(4, 7, 10, 0.92) 0%,
    rgba(4, 7, 10, 0.90) 74%,
    rgba(4, 7, 10, 0.48) 89%,
    rgba(4, 7, 10, 0) 100%);
  pointer-events: none;
}

.dash--member .statushero__standing, .dash--free .statushero__standing {
  display: flex;
  align-items: baseline;
  gap: 2px;
  margin: 0;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.dash--member .statushero__standingnow, .dash--free .statushero__standingnow { font-size: 26px; color: var(--text); }
.dash--member .statushero__standingof, .dash--free .statushero__standingof { font-size: 14px; color: var(--text); opacity: 0.78; }

.dash--member .statushero__rarity, .dash--free .statushero__rarity {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text);
}

.dash--member .statushero__raritydot, .dash--free .statushero__raritydot {
  width: 7px;
  height: 7px;
  border-radius: 2px;
  background: var(--r-common);
}

.dash--member .statushero__rarity[data-rarity="legendary"] .statushero__raritydot, .dash--free .statushero__rarity[data-rarity="legendary"] .statushero__raritydot { background: var(--r-legendary); }
.dash--member .statushero__rarity[data-rarity="epic"] .statushero__raritydot, .dash--free .statushero__rarity[data-rarity="epic"] .statushero__raritydot { background: var(--r-epic); }
.dash--member .statushero__rarity[data-rarity="rare"] .statushero__raritydot, .dash--free .statushero__rarity[data-rarity="rare"] .statushero__raritydot { background: var(--r-rare); }

/* No card equipped: the identity takes the row on its own. */
.dash--member .statushero__player--bare, .dash--free .statushero__player--bare {
  grid-template-columns: minmax(0, 1fr);
}

/* ---------- Below 1024 they stack, identity first ---------- */
@media (max-width: 1023px) {
  .dash--member #playerCardSlot .statushero__player, .dash--free #playerCardSlot .statushero__player {
    grid-template-columns: minmax(0, 1fr);
    padding-right: clamp(14px, 1.6vw, 22px);
    padding-bottom: clamp(14px, 1.6vw, 22px);
  }
  /* THE GUTTER, NOT THE PADDING. The padding reads --emblem-gutter and
     so does the buttons' pull back, so overriding the padding alone put
     them out of step: at 320 the buttons landed 2px from the panel edge
     and their focus ring fell outside the clip-path and was cut. Move
     the variable and both follow. */
  /* THE GUTTER HAS TO CLEAR THE EMBLEM, and at 92px it did not. The
     emblem is 104px wide at 375 and breaks the panel edge by 8, so its
     right edge lands 96px in while the text started at 92: the rank
     name sat on the artwork and measured 1.01 against it. Same at 768,
     where a 150px emblem breaking by 14 reaches 136 against a 128px
     gutter. These are the widths where the emblem is largest relative
     to the panel, so they set the floor and the ceiling. */
  .dash--member .statushero__panel, .dash--free .statushero__panel {
    --emblem-gutter: clamp(108px, 20vw, 152px);
  }
  .dash--member .statushero__emblem, .dash--free .statushero__emblem {
    width: clamp(104px, 27vw, 150px);
    height: clamp(104px, 27vw, 150px);
  }
}

/* At the narrowest the emblem stops breaking out, because there is no
   margin left for it to break into without leaving the module. */
@media (max-width: 420px) {
  .dash--member .statushero__emblem, .dash--free .statushero__emblem { top: -10px; left: -8px; }
}

/* ---------- 8b-motion. THE THREE MOTIONS ----------

   OPT IN, NOT OPT OUT, and the difference matters. Every animation
   below is declared INSIDE prefers-reduced-motion: no-preference, so
   the resting stylesheet has no animation in it at all. Written the
   other way, as a `reduce` block that switches things off, anything I
   forget to list keeps moving; written this way, anything I forget to
   list never moves in the first place. A reader who asked for less
   motion gets the panel exactly as it looks at the end of all three.

   THE RESTING STATE IS THE FINISHED STATE. Each element below is
   styled at its END position and the animation runs FROM an offset,
   so with animations off nothing is left mid-flight: the shine sits at
   opacity 0, the plates sit at their true size, the glow sits at full.

   COMPOSITOR ONLY. Every frame of all three is opacity or transform.
   Nothing animates a size, a position, a colour or a shadow, so none
   of them can cause layout or repaint work while it runs.

   ONE LOOP, AND ONLY ONE. The glow breathes. The other two are `1`
   iteration and are armed once per page load by armBannerEntrance() in
   js/app.js, which is the same guard countUp uses, because paintOnce
   recreates these nodes on every repaint and a CSS animation restarts
   whenever its element is recreated. */

/* THE SHINE. The band is 16% of the sweep wide and travels from fully
   off one edge to fully off the other, so the card is lit across and
   then clear, rather than flashed. 0.13 at its centre: enough to read
   as light crossing a surface, not enough to wash the artwork under
   it. The card art already clips, so the band cannot escape the
   card. */
.dash--member .statushero__cardart--shine::after, .dash--free .statushero__cardart--shine::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(105deg,
    rgba(255, 255, 255, 0) 42%,
    rgba(255, 255, 255, 0.13) 50%,
    rgba(255, 255, 255, 0) 58%);
  transform: translate3d(-100%, 0, 0);
}

@keyframes statusCardShine {
  from { transform: translate3d(-100%, 0, 0); opacity: 0; }
  18%  { opacity: 1; }
  82%  { opacity: 1; }
  to   { transform: translate3d(100%, 0, 0); opacity: 0; }
}

/* THE PLATES SETTLING.

   THE BASE PLATE SCALES DOWN INTO PLACE, IT DOES NOT SLIDE. It is
   inset 1px over a panel whose own background is the tier colour, and
   that 1px IS the tier edge: translate it and the edge opens to 6 or
   8px on one side for the length of the animation. Scaling from 1.014
   to 1 only ever has it larger than its final size, never smaller and
   never offset, so the edge stays exactly 1px the whole way down.

   The raised plate has no such problem, being a corner triangle with
   the base plate behind it, so it does the visible part: it arrives
   along its own 45 degree axis, down and right, which is the one
   direction that reads as a plate sliding into a stack. */
@keyframes statusPlateBase {
  from { transform: scale(1.014); }
  to   { transform: none; }
}

@keyframes statusPlateRaised {
  from { transform: translate3d(11px, 11px, 0); opacity: 0; }
  to   { transform: none; opacity: 1; }
}

/* THE GLOW BREATHING. 0.82 to 1 on a layer that is a 13% colour mix to
   begin with: at its peak the glow adds about 60 units of tint behind
   the emblem, so the swing is roughly 11 units, spread over three and
   a half seconds. That is under the threshold where peripheral vision
   reports it as movement, which is the whole requirement.

   IT BRIGHTENS TO 1 AND NO FURTHER, so the brightest frame of the loop
   is the state every contrast figure in the report was measured at.
   The loop cannot take any text below its floor because it only ever
   makes the surface darker than the measured case. */
@keyframes statusGlowBreathe {
  0%, 100% { opacity: 0.82; }
  50%      { opacity: 1; }
}

@media (prefers-reduced-motion: no-preference) {
  .dash--member .statushero__cardart--shine::after, .dash--free .statushero__cardart--shine::after {
    animation: statusCardShine 1200ms cubic-bezier(0.33, 0, 0.2, 1) 1 both;
  }

  .dash--member .statushero__panel--settle::before, .dash--free .statushero__panel--settle::before {
    animation: statusPlateBase 400ms cubic-bezier(0.22, 0.61, 0.36, 1) 1 both;
  }
  .dash--member .statushero__panel--settle::after, .dash--free .statushero__panel--settle::after {
    animation: statusPlateRaised 400ms cubic-bezier(0.22, 0.61, 0.36, 1) 1 both;
  }

  .dash--member .statushero__glow, .dash--free .statushero__glow {
    animation: statusGlowBreathe 7s ease-in-out infinite;
  }
}

/* ============================================================
   8d. THE VALUE STRIP AND THE THREE PANELS

   Stage 2 of the redesign. The strip states the total and what it has
   done; the three panels under it are the line, the climb and the
   board. Every figure in all four is a node that already existed: the
   strip MOVES .progression__head and the #statsGrid column rather than
   rebuilding them, so #rankFigure is still what runCountUp eases and
   #statsGrid is still what renderStats writes.
   ============================================================ */

/* ---------- The strip ---------- */
.dash--member #valueStrip {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(14px, 1.6vw, 22px);
  align-items: center;
}

@media (min-width: 860px) {
  /* The total on the left at its natural width, the four periods taking
     the rest. auto rather than a fraction: the figure is the largest
     type on the page and must never be the thing that wraps. */
  .dash--member #valueStrip {
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: clamp(20px, 2.4vw, 40px);
  }
  .dash--member .vstrip__total, .dash--free .vstrip__total {
    padding-right: clamp(20px, 2.4vw, 40px);
    border-right: 1px solid var(--edge);
  }
}

/* THE CELLS ARE DIVIDED, NOT BOXED, which is the reference's treatment
   and the one this file uses everywhere else. .vstrip .stat switches the
   rule off in its last declaration, from when the periods sat under the
   total rather than beside it; beside it they are a row and a row needs
   its divisions. The first sits flush with the column's own edge. */
.dash--member .vstrip__periods .stat, .dash--free .vstrip__periods .stat {
  padding-left: clamp(12px, 1.4vw, 20px);
  border-left: 1px solid var(--edge);
}

.dash--member .vstrip__periods .stat:first-of-type, .dash--free .vstrip__periods .stat:first-of-type {
  padding-left: 0;
  border-left: 0;
}

.dash--member .vstrip__total .progression__head, .dash--free .vstrip__total .progression__head { margin: 0; }

/* The eyebrow, the figure and the gain line, in one column. */
.dash--member .vstrip__total .progression__eyebrow, .dash--free .vstrip__total .progression__eyebrow {
  margin: 0 0 6px;
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: #8d9bb4;
}

.dash--member .vstrip__total .progression__figure, .dash--free .vstrip__total .progression__figure {
  display: block;
  margin: 0;
  font-size: clamp(34px, 4.4vw, 62px);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--text);
}

.dash--member .vstrip__total .progression__gain, .dash--free .vstrip__total .progression__gain {
  margin: 10px 0 0;
  font-size: 12.5px;
  line-height: 1.3;
}


/* ---------- The chart panel ---------- */

/* Heading left, ranges right, on one line. */
.dash--member #deskModule .vchart__head, .dash--free #deskModule .vchart__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px;
}

.dash--member #deskModule .vchart__ranges, .dash--free #deskModule .vchart__ranges {
  display: flex;
  gap: 2px;
  padding: 2px;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.05);
}

.dash--member #deskModule .vchart__range, .dash--free #deskModule .vchart__range {
  min-height: 26px;
  padding: 4px 10px;
  border: 0;
  border-radius: 6px;
  background: none;
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.1em;
  color: var(--muted);
  cursor: pointer;
}

.dash--member #deskModule .vchart__range:hover, .dash--free #deskModule .vchart__range:hover { color: var(--text); }

/* The active one is a filled chip, which is the reference's treatment
   and is what aria-pressed already says for a reader who cannot see it. */
.dash--member #deskModule .vchart__range[aria-pressed="true"], .dash--free #deskModule .vchart__range[aria-pressed="true"] {
  background: var(--accent);
  color: #050505;
}

/* ---------- The chart value axis ---------- */

/* Absolutely placed against the plot the same way every other mark is,
   so it moves with the scale rather than being a second drawing of it.
   Left aligned in the plot own left padding, which is 22 of 1000 and is
   why the labels sit clear of the area fill. */
/* THE LABEL CARRIES ITS OWN GROUND. The gutter is 78 of the 1000 wide
   view box, which is 38px at 1280 and only 27 at 375, and a label
   reading A$177.11 is about 50: on a narrow panel it reaches back over
   the line and measured 1.09 against a 4.5 floor there. Widening the
   gutter enough for the narrowest case would have taken a tenth of the
   plot away at every width to fix two of them.

   A chip under the text fixes it at every width instead, and costs the
   plot nothing. It is the panel colour at 0.82, so it reads as the
   label sitting on the panel rather than as a box drawn around it. */
.dash--member #valueChartSlot .vchart__y, .dash--free #valueChartSlot .vchart__y {
  position: absolute;
  left: 0;
  transform: translateY(-50%);
  padding: 1px 4px;
  border-radius: 4px;
  background: rgba(10, 12, 16, 0.92);
  font-size: 9.5px;
  letter-spacing: 0.04em;
  color: #8d9bb4;
  pointer-events: none;
  white-space: nowrap;
}

/* ---------- The chart expand control ---------- */

.dash--member #deskModule .vchart__expand, .dash--free #deskModule .vchart__expand {
  flex: none;
  min-height: 26px;
  padding: 4px 12px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.04);
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text);
  cursor: pointer;
}

.dash--member #deskModule .vchart__expand:hover, .dash--free #deskModule .vchart__expand:hover {
  border-color: rgba(255, 255, 255, 0.3);
  background: rgba(255, 255, 255, 0.08);
}

/* EXPANDED. The module takes the row and the plot takes the height it
   was always short of. Nothing moves in the DOM: this is the same
   chart, the same node and the same renderer, in a bigger box. */
.dash--member.dash--chartwide #valueChartSlot .vchart__plot, .dash--free.dash--chartwide #valueChartSlot .vchart__plot {
  min-height: clamp(300px, 46vh, 520px);
}

/* ---------- The rank panel ---------- */
.dash--member #arena .rankpanel, .dash--free #arena .rankpanel {
  margin-top: clamp(16px, 1.9vw, 24px);
}

/* TALLER, BECAUSE IT IS THE GOLD. Everything else on this panel gave
   its gold back, so the one element that keeps it can afford to be the
   thing the eye lands on after the name. */
.dash--member .rankpanel__bar, .dash--free .rankpanel__bar {
  display: block;
  height: 10px;
  border-radius: 999px;
  /* Cut into the panel, the same groove the banner's bar uses, so the
     fill reads on it whatever the panel is doing behind. */
  background: rgba(0, 0, 0, 0.42);
  box-shadow:
    inset 0 1px 1px rgba(0, 0, 0, 0.5),
    inset 0 -1px 0 rgba(255, 255, 255, 0.05);
  overflow: hidden;
}

.dash--member .rankpanel__fill, .dash--free .rankpanel__fill {
  display: block;
  height: 100%;
  border-radius: inherit;
  /* GOLD, because this bar is the ladder's and gold is the ladder's
     colour on this page. Measured against its groove in the report. */
  background: linear-gradient(90deg, #c9924e 0%, #f5c451 100%);
}

/* WRAPS RATHER THAN COLLIDES. Both halves are nowrap so neither figure
   can break mid number, which means at a narrow panel they cannot both
   fit on one line: at 1280 they met with no space between them. Wrapping
   puts the threshold on its own line, still hard right. */
.dash--member .rankpanel__ends, .dash--free .rankpanel__ends {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 4px 12px;
  margin-top: 10px;
}

/* NOWRAP ON BOTH, because a figure that breaks mid number is worse than
   a row that breaks between its two halves. US$150.00 came out as
   US$150. over 00 in a 230px column at 1280. */
.dash--member .rankpanel__gap, .dash--free .rankpanel__gap {
  font-size: 11.5px;
  color: var(--muted);
  white-space: nowrap;
}

.dash--member .rankpanel__gap b, .dash--free .rankpanel__gap b {
  font-weight: 500;
  color: var(--text);
}

.dash--member .rankpanel__mark, .dash--free .rankpanel__mark {
  font-size: 11.5px;
  color: var(--muted);
  white-space: nowrap;
}

/* ---------- The rank identity, at the head of the panel ---------- */
.dash--member .arena__me, .dash--free .arena__me {
  display: flex;
  align-items: center;
  gap: clamp(10px, 1.1vw, 16px);
  min-width: 0;
}

/* THE EMBLEM IS THE PANEL'S PICTURE. At 56 it read as an icon beside a
   heading; the reference makes it the largest thing in the panel after
   the name, which is what makes the two of them a unit rather than a
   label with a bullet. */
.dash--member .arena__meart, .dash--free .arena__meart {
  flex: none;
  width: clamp(64px, 6.2vw, 92px);
  height: clamp(64px, 6.2vw, 92px);
  object-fit: contain;
}

/* NOT GOLD. Gold on this page means the ladder, and on this panel the
   ladder is the progress bar and the next rank line. A gold rank name
   beside a gold bar beside a gold emblem is three golds saying one
   thing, and it is why the panel read as gold throughout. The name is
   the panel's brightest type instead, which is a different way of being
   first and does not spend the accent. */
.dash--member .arena__mename, .dash--free .arena__mename {
  margin: 0;
  min-width: 0;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(26px, 2.5vw, 40px);
  line-height: 1.02;
  text-transform: uppercase;
  letter-spacing: 0.01em;
  overflow-wrap: anywhere;
  color: var(--text);
}

.dash--member .rankpanel__next, .dash--free .rankpanel__next {
  margin-top: clamp(14px, 1.6vw, 20px);
  padding-top: clamp(14px, 1.6vw, 20px);
  border-top: 1px solid var(--edge);
}

.dash--member .rankpanel__nextlabel, .dash--free .rankpanel__nextlabel {
  margin: 0 0 10px;
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: #8d9bb4;
}

.dash--member .rankpanel__nextrow, .dash--free .rankpanel__nextrow {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}

.dash--member .rankpanel__nextart, .dash--free .rankpanel__nextart {
  flex: none;
  width: 44px;
  height: 44px;
  object-fit: contain;
}

.dash--member .rankpanel__nextname, .dash--free .rankpanel__nextname {
  min-width: 0;
  font-family: var(--font-display);
  font-size: clamp(15px, 1.3vw, 19px);
  line-height: 1.1;
  color: var(--text);
  overflow-wrap: anywhere;
}

.dash--member .rankpanel__link, .dash--free .rankpanel__link,
.dash--member .arena__link, .dash--free .arena__link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 34px;
  margin-top: clamp(12px, 1.4vw, 16px);
  padding: 7px 14px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 9px;
  background: rgba(255, 255, 255, 0.04);
  font-size: 12.5px;
  color: var(--text);
  text-decoration: none;
}

.dash--member .rankpanel__link:hover, .dash--free .rankpanel__link:hover,
.dash--member .arena__link:hover, .dash--free .arena__link:hover {
  border-color: rgba(255, 255, 255, 0.3);
  background: rgba(255, 255, 255, 0.08);
}

.dash--member .rankpanel__max, .dash--free .rankpanel__max {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--muted);
}

/* ---------- The board panel ---------- */
.dash--member .arena__rows, .dash--free .arena__rows { margin: 0; padding: 0; list-style: none; }

/* POSITION, AVATAR, NAME, FIGURE. The name column is the only one that
   flexes, so the figures stay in a hard right column down the list
   however long a display name is. */
.dash--member .arena__row, .dash--free .arena__row {
  display: grid;
  grid-template-columns: 22px 28px minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
  padding: 7px 8px;
  border-radius: 8px;
}

.dash--member .arena__row + .arena__row, .dash--free .arena__row + .arena__row { margin-top: 2px; }

.dash--member .arena__pos, .dash--free .arena__pos {
  font-size: 11px;
  color: var(--muted);
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.dash--member .arena__avatar, .dash--free .arena__avatar {
  width: 28px;
  height: 28px;
  border-radius: 7px;
  object-fit: cover;
  background: rgba(255, 255, 255, 0.06);
}

.dash--member .arena__who, .dash--free .arena__who {
  display: block;
  min-width: 0;
}

.dash--member .arena__who b, .dash--free .arena__who b {
  display: block;
  font-weight: 400;
  font-size: 12.5px;
  line-height: 1.2;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dash--member .arena__who span, .dash--free .arena__who span {
  display: block;
  font-size: 10.5px;
  line-height: 1.2;
  color: #8d9bb4;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* THE FIGURE, AND IT IS THE CARD COUNT. Mono and right aligned, the
   treatment the reference gives its value column. The unit is smaller
   than the number so the numbers align down the list by eye. */
.dash--member .arena__cards, .dash--free .arena__cards {
  font-size: 12.5px;
  font-weight: 500;
  color: var(--text);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.dash--member .arena__cardsunit, .dash--free .arena__cardsunit {
  font-size: 10.5px;
  font-weight: 400;
  color: var(--muted);
}

/* #6f7686 measures 4.18 against this panel, under a 4.5 floor. It is the
   same lift #8d9bb4 that the banner labels already carry, and it is the
   value this file settled on the last time a faint grey had to move. */
.dash--member .arena__meta, .dash--free .arena__meta { color: #8d9bb4; }

.dash--member .arena__ladderhead, .dash--free .arena__ladderhead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: clamp(10px, 1.2vw, 14px);
}

/* ============================================================
   8b2. THE READS ROW: THREE PANELS, ONE LINE EACH

   Signals, Market movers and Category indexes side by side. Nothing
   here changes what any of them says: every signal type, every string,
   the tone system and the ordering rule are the ones already in
   js/app.js, and the only row that loses a part is the index row, which
   gives up its level so a 3 of 12 column is not asked to hold four
   figures. The spans themselves are in section 6b, with the arithmetic.
   ============================================================ */

/* ---------- The three heads read alike ----------
   Title over its meta on the left, the control on the right, so the
   three lists start at the same y and the row has one top edge rather
   than three. Left as a flex row on the rest of the page. */
.dash--member .mod:has(#signalsList) .dash-sechead,
.dash--member #moversModule .dash-sechead,
.dash--member #indexesModule .dash-sechead {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  column-gap: 10px;
  row-gap: 2px;
  margin-bottom: 12px;
}

/* THE THREE LISTS START AT THE SAME Y. "Market movers" and "Category
   indexes" take two lines beside their controls at 1280 and "Signals"
   takes one, which left the signals feed 14px higher than the two
   beside it. The floor is two lines of the heading's own metrics,
   10.5px at 1.4, so a one line title centres in the same box rather
   than a number being picked to make it line up. */
.dash--member .mod:has(#signalsList) .dash-sechead__title,
.dash--member #moversModule .dash-sechead__title,
.dash--member #indexesModule .dash-sechead__title {
  grid-column: 1;
  margin: 0;
  min-height: calc(10.5px * 1.4 * 2);
}

.dash--member .mod:has(#signalsList) .dash-sechead__meta,
.dash--member #moversModule .dash-sechead__meta,
.dash--member #indexesModule .dash-sechead__meta { grid-column: 1; }

/* The control spans both rows so it centres against the pair. */
.dash--member .sechead__ctl { grid-column: 2; grid-row: 1 / span 2; justify-self: end; }

/* THE PANEL IS A CARD AND ITS CONTENT SITS INSIDE IT. The signals
   module was authored without the inline padding the market pair
   carries, which was invisible while it ran the full width and is not
   once it is one of three cards in a row: its rows touched the card
   edge while the two beside it were inset 18px. */
.dash--member .mod:has(#signalsList) { padding-inline: clamp(12px, 1.4vw, 18px); }

/* A GHOST BUTTON, and only where there is somewhere to go. market.html
   carries the full index list under #indexes, drawn from the same
   endpoint this panel reads. The signals panel has no button at all
   because there is no signals page to send a reader to, and a control
   that goes nowhere is worse than no control. */
.dash--member .ghostlink {
  display: inline-flex;
  align-items: center;
  min-height: 26px;
  padding: 4px 11px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 7px;
  background: none;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.72);
  text-decoration: none;
  white-space: nowrap;
}

.dash--member .ghostlink:hover {
  border-color: rgba(255, 255, 255, 0.34);
  color: #f4f6fb;
}

/* The movers button closes its panel rather than sitting in its head,
   because the head is carrying the two tabs. Full width, so it reads as
   the end of the list rather than as a third control floating under it. */
.dash--member .ghostlink--wide {
  display: flex;
  justify-content: center;
  gap: 7px;
  width: 100%;
  min-height: 34px;
  margin-top: clamp(10px, 1.1vw, 14px);
  padding: 8px 12px;
  background: rgba(255, 255, 255, 0.03);
}

/* ---------- TOP SIGNALS ----------
   Already a feed rather than a stack of cards (section 4b): no gap, a
   hairline between rows, the tone as a 3px left edge. What changes here
   is only what a 5 of 12 column can hold. */

/* THE METER GOES. It encodes the two numbers the row already states, in
   words in the sentence and in figures in the column beside it, and at
   213px of body width it was a third statement of one number wrapping
   its own scale labels. The markup is untouched: renderSignals still
   builds it, a free reader still gets it, and the full width feed on
   any other page still shows it. */
.dash--member .signals .signal__meter { display: none; }

/* The figure keeps its size and its tone; it gives up the 104px floor,
   which on this column was reserving room the sentence needed. It comes
   out at 53 for a percentage, which is 51px back to the sentence. */
.dash--member .signals .signal__figure { min-width: 0; }
.dash--member .signals .signal__pct { font-size: clamp(22px, 1.9vw, 27px); }

.dash--member .signals .signal { gap: 14px; padding: 13px 14px; }
.dash--member .signals .signal__thumb { width: 42px; height: 42px; }
.dash--member .signals .signal__text { font-size: 12px; line-height: 1.45; }

/* ---------- MARKET MOVERS ----------
   Two tabs in the head and one list under them. Side by side, each
   column had half of a third of the row, and a market name that runs to
   sixty characters had nowhere to go. */

/* NO DATA, NO SWITCH. renderMovers returns before it paints the tabs
   when the sweep has not run, and the trough is a 2px padded box with
   a ground, so an empty slot drew a 4px dot in the corner of the head. */
#moversTabs:empty { display: none; }

/* A segmented control: one trough, the active chip filled. */
.dash--member #moversTabs {
  display: flex;
  gap: 2px;
  padding: 2px;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.05);
}

.dash--member .movers__tab {
  min-height: 26px;
  padding: 5px 9px;
  border: 0;
  border-radius: 6px;
  background: none;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.55);
  white-space: nowrap;
  cursor: pointer;
}

.dash--member .movers__tab:hover { color: #f4f6fb; }

.dash--member .movers__tab[aria-pressed="true"] {
  background: rgba(255, 255, 255, 0.11);
  color: #f4f6fb;
}

/* THE ROW IS A BUTTON NOW, so the grid moves off the li and onto the
   button: an li with a click listener is not reachable by keyboard, and
   rebuilding Enter, Space, focus and the touch target by hand is how
   that goes wrong. The li keeps the hairline and the list semantics.

   Four columns: the thumbnail, the name, what it costs and what it did.
   The price is here because it is the rule the board is built under: a
   board that hides the price is the board that let four $0.04 skins tie
   at +33.3% and called them the market. */
.dash--member #moversModule .mover {
  display: block;
  padding: 0;
}

.dash--member .mover__hit {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 7px 0;
  border: 0;
  border-radius: 6px;
  background: none;
  text-align: left;
  font: inherit;
  color: inherit;
  cursor: pointer;
}

.dash--member .mover__hit:hover { background: rgba(255, 255, 255, 0.04); }

/* THE PRICE GOES UNDER THE NAME, NOT BESIDE IT. Measured at 1280: as a
   fourth column it took 62px off a name column already down to 178 by
   the thumbnail, and all five names truncated where two had before.
   Stacked it costs height, which this row has, instead of width, which
   it does not. Same shape as a signal row: what it is, then the quiet
   line under it. */
.dash--member .mover__body {
  display: grid;
  min-width: 0;
  row-gap: 1px;
}

.dash--member .mover__price {
  font-size: 11px;
  color: rgba(255, 255, 255, 0.5);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The same well the signals and holdings thumbnails sit in, at the size
   a 4 of 12 column can give it. */
.dash--member .mover__thumb {
  position: relative;
  width: 34px;
  height: 34px;
}

.dash--member .mover__thumb img {
  position: relative;
  z-index: 1;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* THE LETTER IS ALWAYS THERE, UNDER THE IMAGE. Holdings draws one or
   the other on presence, which covers a row the schema has no entry
   for but not a URL that is present and then 404s. Drawn underneath,
   it is what is left when js/app.js drops a failed image, and it costs
   nothing when the image loads because the image covers it. */
.dash--member .mover__glyph {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: var(--font-display);
  font-size: 13px;
  /* 0.45 measured 4.49 against the sunken well behind it, which is one
     hundredth short of AA and therefore short of AA. */
  color: rgba(255, 255, 255, 0.55);
}

/* ---------- CATEGORY INDEXES ----------
   Glyph, name, figure, line. The level (104.62 and the like) is the one
   part that left the row: four figures do not fit 236px, and market.html
   carries it in full behind the View All.

   MEASURED, BECAUSE THIS ROW IS THE TIGHTEST THING IN THE PASS. At 1280
   the panel is 236px and the row inside it 199. The glyph is 14, the
   percentage 45, the line 46 and three gaps 12, which leaves 82 for the
   label. The labels the endpoint returns, in the order it returns them,
   are Knives 39, Gloves 40, Cases 36, Agents 41, Music Kits 60 and
   Vanilla Knives 80, so all six are whole with two pixels to spare.
   That is what the last six pixels of sparkline bought, and the line
   keeps its shape because the view box is unchanged. At 1024 the label
   has 213px and at 1920 199, so the ellipsis is a floor under a longer
   label rather than anything this row normally meets. */
.dash--member .dashidx__row {
  display: block;
  padding: 0;
}

/* Same move as the mover row, same reason. */
.dash--member .dashidx__hit {
  display: grid;
  grid-template-columns: 14px minmax(0, 1fr) auto 46px;
  align-items: center;
  column-gap: 4px;
  width: 100%;
  padding: 8px 0;
  border: 0;
  border-radius: 6px;
  background: none;
  text-align: left;
  font: inherit;
  color: inherit;
  cursor: pointer;
}

.dash--member .dashidx__hit:hover { background: rgba(255, 255, 255, 0.04); }

/* A CATEGORY MARK, not an item picture: there is no image for a category
   anywhere in the data. Decorative and hidden from assistive tech,
   because the label beside it already names it in words. */
.dash--member .dashidx__glyph {
  width: 14px;
  height: 14px;
  color: rgba(255, 255, 255, 0.5);
}

.dash--member .dashidx__change { min-width: 0; }

/* THE LINE TAKES THE DIRECTION'S COLOUR, which is the only thing it has
   to say beyond its shape, and it is drawn by indexSpark from ix.series,
   the index's own daily points, not from a fixture. Narrower here than
   the 88px the market page gives it, because the panel is 236px. The
   view box is unchanged and preserveAspectRatio is none, so the shape
   is the same shape drawn into a smaller box. */
.dash--member .dashidx__spark,
.dash--member .dashidx__spark--empty { width: 46px; height: 20px; }

/* ============================================================
   8b3. THE INDEX DETAIL PANEL

   The item panel's frame at a smaller size. It borrows .detail__close,
   .detail__section and .dstat rather than restating them, so the two
   panels cannot drift; what is its own is the chart, which draws a
   level rebased to 100 rather than money.
   ============================================================ */

.idxdetail { position: fixed; inset: 0; z-index: 70; }
.idxdetail[hidden] { display: none; }

.idxdetail__scrim {
  position: absolute;
  inset: 0;
  background: rgba(4, 5, 8, 0.66);
  opacity: 0;
  transition: opacity 320ms ease;
}

.idxdetail.is-open .idxdetail__scrim { opacity: 1; }

.idxdetail__panel {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(460px, 100%);
  overflow-y: auto;
  padding: clamp(20px, 2.4vw, 30px);
  background: #0e1016;
  border-left: 1px solid var(--edge);
  transform: translate3d(100%, 0, 0);
  transition: transform 320ms cubic-bezier(0.22, 1, 0.36, 1);
}

.idxdetail.is-open .idxdetail__panel { transform: translate3d(0, 0, 0); }

/* The panel slides. Anyone who has asked not to be moved gets it in
   place instead, which is the same rule the rest of the page follows. */
@media (prefers-reduced-motion: reduce) {
  .idxdetail__scrim,
  .idxdetail__panel { transition: none; }
}

.idxdetail__head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: start;
  gap: 16px;
  margin-bottom: clamp(16px, 1.8vw, 22px);
}

.idxdetail__name {
  margin: 2px 0 0;
  font-family: var(--font-display);
  font-stretch: var(--display-header);
  font-weight: 600;
  font-size: clamp(19px, 2vw, 24px);
  letter-spacing: -0.015em;
  color: var(--text);
}

.idxdetail__level {
  text-align: right;
}

.idxdetail__value {
  margin: 2px 0 0;
  font-size: clamp(19px, 2vw, 24px);
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.idxdetail__chart { margin-bottom: 8px; }

.idxdetail__svg {
  display: block;
  width: 100%;
  height: clamp(130px, 17vw, 180px);
  overflow: visible;
}

.idxdetail__area { fill: rgba(122, 162, 247, 0.12); stroke: none; }
.idxdetail__line { fill: none; stroke: #9fb8e6; stroke-width: 1.6; stroke-linejoin: round; stroke-linecap: round; }

/* Where the index started. Every one is rebased to 100 at the window
   start, so above or below this line is the whole read. */
.idxdetail__base {
  stroke: rgba(255, 255, 255, 0.24);
  stroke-width: 1;
  stroke-dasharray: 3 4;
  vector-effect: non-scaling-stroke;
}

.idxdetail__axis {
  display: flex;
  justify-content: space-between;
  margin-top: 6px;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  color: rgba(255, 255, 255, 0.5);
}

.idxdetail__basis {
  margin: 0 0 clamp(16px, 1.8vw, 22px);
  font-size: 12px;
  line-height: 1.5;
  color: rgba(255, 255, 255, 0.55);
}

/* ---------- 8c. AN EMPTY MARKET MODULE IS ONE LINE ----------
   Two big empty boxes side by side read worse than one, and the waiting
   state is a sentence, not a panel. When that sentence is all there is,
   the module closes up around it. */
#moversModule:has(.market-state),
#indexesModule:has(.market-state) { padding-block: clamp(14px, 1.2vw, 18px); }

#moversModule .market-state,
#indexesModule .market-state { margin: 0; }

#moversModule:has(.market-state) .dash-sechead,
#indexesModule:has(.market-state) .dash-sechead { margin-bottom: 8px; }

/* ---------- 8d. THE TAIL ----------
   The close under the page is two columns: the pitch, and the Discord
   card beside it. A member does not get the pitch, so the padding that
   framed it and the width that balanced it were holding open a hole
   between Holdings and a card that is 114px tall. */
.dash--member .footer__close {
  padding-top: clamp(24px, 2.6vw, 40px);
  padding-bottom: clamp(24px, 2.6vw, 40px);
  justify-items: start;
}

.dash--member .footer__discord { max-width: 560px; }

/* ---------- 8e. YOUR BOOK, WITHOUT THE HOLE ----------
   The lead and the bar are two short things beside a five row legend, so
   the left column held 130px of nothing under the bar. The note that
   explains the unpriced items joins them, and what is left of the
   difference is shared above and below rather than all falling to the
   bottom. */
/* The composition is a block now, so there are no grid lines to place
   the note on. It simply follows the legend. */
@media (min-width: 900px) {
  .dash--member .book__comp .book__note { margin: 14px 0 0; }
}

/* ---------- THE LEGEND IS ONE COLUMN AGAIN ----------

   It used to be split into two by an auto-fit grid, and the reason was
   sound at the time: the legend sat BESIDE the bar in a two column
   composition, it was the taller side, and the quadrant under the bar
   was empty across half the module. Stacking the composition removed
   the hole that rule existed to fill.

   And it now actively breaks the row. A .book__part is a four cell
   ledger, 140px of track plus 112px of value plus 64px of percent plus
   its gaps, about 370px before the name gets a pixel. Halving the
   legend to a 460px column left the name 90px and truncated every one
   of them to "Knives & gloves...". One column, full measure. */

/* ---------- 8f. THE SCALE STRIP IS A STRIP ----------

   IT WAS A MODULE HOLDING ONE NUMBER. Three of its four figures come
   from the market documents (coverage and the index list). Where those
   have not been swept, renderScaleStrip correctly draws nothing for
   them, and what was left was a panel, a heading and the figure 303 at
   26px in a 96px box. A module that size is a promise about how much it
   has to say, and one number does not keep it.

   So it stops being a module. No surface, no heading, no 26px figures:
   the same shape the inventory strip at the top of the page already
   uses, which is the same kind of element. One figure is a quiet line.
   Four is a status bar. Neither is a box.

   The section keeps every hook: .mod and .mod--full are still on it in
   dashboard.html, and the surface they carry is turned off here rather
   than by editing the class list. */
.scale {
  display: block;
  padding: clamp(10px, 1vw, 14px) 0 0;
  border: 0;
  border-top: 1px solid var(--line);
  border-radius: 0;
  background: none;
  box-shadow: none;
}

/* NO HEADING, AND STILL A NAME. The section is aria-labelledby this
   element, so it is hidden from the page rather than removed: a strip
   of four bare numbers with no accessible name is worse than the box
   was. Same technique as .visually-hidden in css/style.css. */
.scale__title {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

/* One line if they fit, wrapping only when they do not. */
.scale__facts {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 0;
  margin: 0;
}

/* The divided cells of .dash-strip__fact, to the pixel: this is that
   element in another place on the same page, so it is not given a
   second look of its own. */
.scale__fact {
  display: block;
  min-width: 0;
  padding: 0 18px;
  border-left: 1px solid var(--line);
}

.scale__fact:first-child { padding-left: 0; border-left: 0; }

.scale__label {
  font-size: 9.5px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--faint);
}

.scale__value {
  margin: 3px 0 0;
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.2;
  letter-spacing: 0;
  color: var(--text);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* Narrow: the dividers become the only thing holding four cells in a
   row they cannot fit, so the cells sit flush and wrap instead. */
@media (max-width: 560px) {
  .scale__facts { gap: 10px 18px; }
  .scale__fact { padding: 0; border-left: 0; }
}

/* ---------- 8f1. ONE SECTION PER RARITY: THE BANDED DIVIDER ----------
   The heading was a name, an 8px swatch and a count set at 10.5px, and
   it read as a caption between two grids rather than as the divider
   between two classes of thing. It is a band now: the name at 25px in
   the display face, a bordered count in the rarity colour, a wash
   behind the left of the row, and a control that says what it will do.

   Still the same native <details> as the locked band below and the
   excluded items in css/style.css: one folding pattern on the page. */
.crar-sec { border-top: 0; }

/* ---------- THE BAND IS A RAISED PLATE ----------
   It was a row with a hairline under it, which is a divider, not an
   object. It is a surface now: lit along its top edge, seated on its
   own shadow, with a crisp rarity bar down the left. The depth is all
   static, so it is in the resting stylesheet where it belongs; only the
   lift on hover is behind the motion query. */
.crar-sec__toggle {
  position: relative;
  display: flex;
  align-items: center;
  gap: clamp(10px, 1.1vw, 16px);
  min-height: 84px;
  margin-top: 14px;
  padding: 0 18px 0 22px;
  border: 1px solid rgba(255, 255, 255, 0.075);
  border-radius: 13px;
  overflow: hidden;
  cursor: pointer;
  list-style: none;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.008));
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.06),
    0 12px 30px rgba(0, 0, 0, 0.42);
}

/* The rarity, as an edge rather than as a tint over everything. 3px,
   full height, with a tight bloom off it. */
.crar-sec__toggle::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  pointer-events: none;
  background: var(--card-rarity, var(--r-common));
  box-shadow: 0 0 12px -1px var(--card-rarity, var(--r-common));
}

.crar-sec__toggle:hover {
  border-color: rgba(255, 255, 255, 0.14);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.09),
    0 18px 40px rgba(0, 0, 0, 0.55);
}
.crar-sec__toggle::-webkit-details-marker { display: none; }

/* THE GLOW IS CONTAINED, AND IT IS NOT A MEASUREMENT.

   340px anchored left, sitting behind the name and nothing else, and
   it resolves to transparent rather than to a colour. A gradient that
   fades to a colour instead of to nothing is what made the old 46%
   version muddy: every pixel of the bar carried some of it.

   Its width is the same on every band and comes from nowhere in the
   data. A glow that carried this rarity's completion would be 8px wide
   on a member holding one legendary card of forty, and the sliver at
   the bottom of the plate already draws that proportion in the one
   place where being short is the honest answer. */
.crar-sec__wash {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 340px;
  max-width: 62%;
  pointer-events: none;
  opacity: 0.55;
  background: radial-gradient(220px 120px at 8% 50%,
    color-mix(in srgb, var(--card-rarity, var(--r-common)) 46%, transparent),
    transparent 70%);
}

.crar-sec[data-rarity="legendary"] { --card-rarity: var(--r-legendary); }
.crar-sec[data-rarity="epic"]      { --card-rarity: var(--r-epic); }
.crar-sec[data-rarity="rare"]      { --card-rarity: var(--r-rare); }
.crar-sec[data-rarity="common"]    { --card-rarity: var(--r-common); }

/* THE SHEEN AND THE WIDE AURA ARE GONE. The sheen was a light band
   sliding the width of the row: against a near black plate it read as a
   grey streak rather than as light, and it drew the eye to the middle
   of the bar, which is the part with nothing in it. The aura was a
   460px radial that reached most of the way across and turned the
   whole plate muddy. What replaces both is smaller and does more: one
   contained glow behind the title, and real card art in the gap.

/* Everything over the wash needs its own stacking context, or the
   absolutely positioned band would paint across it. */
.crar-sec__toggle .cshelf__head {
  position: relative;
  margin: 0;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(16px, 1.35vw, 25px);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text);
}

/* THE ONE DELIBERATE MOMENT. Legendary, and only legendary, carries a
   soft bloom off the name. Gold is the rank and premium colour
   everywhere else in the product, so this is the single place in the
   module where it is allowed to do anything beyond name a rarity. It is
   a static shadow: nothing in this pass animates. */
.crar-sec[data-rarity="legendary"] .cshelf__head {
  text-shadow: 0 0 22px rgba(212, 163, 83, 0.45);
}

/* The badge border is the rarity colour and the figure inside it is
   not: at 13px, epic measured 3.92 against this panel and common 3.53,
   under the 4.5 the digits owe, and darkening the badge only takes epic
   to 4.07. As a 1px boundary both clear the 3:1 a graphic owes. */
.crar-sec__count {
  position: relative;
  flex: none;
  padding: 5px 11px;
  border-radius: 6px;
  border: 1px solid var(--card-rarity, var(--r-common));
  font-size: 13px;
  letter-spacing: 0.02em;
  /* --faint measured 4.47 against the epic wash, just under the 4.5 a
     13px figure owes. The total is a smaller fact than the earned
     count, not an unreadable one. */
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.crar-sec__count b { font-weight: 500; color: var(--text); }

/* The badge's soft outer glow, in the section's own rarity. A colour
   change, not a movement, so it is not behind the motion query. */
.crar-sec__count { box-shadow: 0 0 16px -4px var(--card-rarity, var(--r-common)); }

/* ---------- THE CONTROL ----------
   A worded pill, not a character. It states the action rather than the
   state, so there is nothing to work out: HIDE while the section is
   open, SHOW while it is shut, with the mark pointing the way the
   section will move. Both words are in the markup and CSS picks one, so
   the native <details> stays the only copy of the state. */
.crar-sec__tog,
.cwall-fold__tog {
  position: relative;
  flex: none;
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 8px 15px 8px 17px;
  border-radius: 20px;
  /* --line-warm is the page's hover edge and it measures 1.27 against
     this panel, which is not a control, it is a rumour of one. */
  border: 1px solid rgba(255, 255, 255, 0.36);
  background: #0d0d0f;
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
}

.crar-sec:not([open]) .crar-sec__tog__on,
.crar-sec[open] .crar-sec__tog__off,
.cwall-fold:not([open]) .cwall-fold__tog__on,
.cwall-fold[open] .cwall-fold__tog__off { display: none; }

/* Open shows HIDE with the mark up, because up is the way the section
   will move. Shut shows SHOW with it down. A static transform, not a
   transition: the module gained no animation in this pass. */
.crar-sec:not([open]) .crar-sec__tog svg,
.cwall-fold:not([open]) .cwall-fold__tog svg { transform: rotate(180deg); }

.crar-sec__toggle:hover .crar-sec__tog,
.cwall-fold__toggle:hover .cwall-fold__tog {
  color: var(--text);
  border-color: rgba(255, 255, 255, 0.55);
}

.crar-sec__toggle:focus-visible,
.cwall-fold__toggle:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 2px;
}

/* ---------- THE PEEK ----------
   The middle of a collapsed band used to be empty, which is what made
   it read as a divider with a label on it. It holds the rarity's own
   art now: what has been earned first, then what has not, dimmed.

   IT IS DECORATION AND IT IS MARKED AS SUCH. aria-hidden, no tab stop,
   no click of its own. The whole band is already a native <summary>, so
   pressing anywhere on it opens the real grid, and a second set of
   targets inside the first would be two answers to one question.

   The mask is the point: the art does not stop, it dissolves before it
   reaches the count and the control, so nothing has to be cropped and
   there is no hard edge to line anything up against. */
/* A DEFINITE HEIGHT, not a stretched one. height:100% on a tile inside
   an auto height flex parent resolves to auto, so the tile fell back to
   the image's intrinsic 800x267 and dragged the whole plate to 297px.
   56px is the 84px band less its 14px of air top and bottom. */
.crar-peek {
  display: flex;
  flex: 1;
  min-width: 0;
  align-self: center;
  align-items: center;
  height: 56px;
  gap: 9px;
  overflow: hidden;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 7%, #000 68%, transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 7%, #000 68%, transparent 100%);
}

.crar-peek__t {
  position: relative;
  flex: 0 0 auto;
  height: 100%;
  width: auto;
  aspect-ratio: 3 / 1;
  border-radius: 5px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.03);
}

.crar-peek__t img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The rarity edge again, at the size a 56px tile can carry. */
.crar-peek__t::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 2px;
  z-index: 2;
  background: var(--card-rarity, var(--r-common));
}

/* Seats the tile rather than letting it float as a bright rectangle. */
.crar-peek__t::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(180deg, transparent 45%, rgba(4, 6, 10, 0.55));
}

/* An unearned card in the peek is the same card, unlit. */
.crar-peek__t--lock img { filter: brightness(0.3) saturate(0.5); }

.crar-peek__t--lock::after {
  background:
    linear-gradient(180deg, rgba(8, 11, 18, 0.45), rgba(8, 11, 18, 0.72));
}

/* Open bands have the real grid under them, so the tease is spent. */
.crar-sec[open] .crar-peek { opacity: 0; pointer-events: none; }

/* ---------- THE PROGRESS SLIVER ----------
   Three pixels along the bottom of the plate. This is the ONE place in
   the module that draws a proportion, and it is drawn where a sliver
   being short is the honest answer rather than a failure: the badge
   above it carries the same fact in figures at a size that does not
   move, so nothing depends on being able to see 2.5% of a bar. */
.crar-prog {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 3px;
  pointer-events: none;
  background: rgba(255, 255, 255, 0.05);
}

.crar-prog__f {
  display: block;
  height: 100%;
  width: 0;
  background: var(--card-rarity, var(--r-common));
  box-shadow: 0 0 10px -1px var(--card-rarity, var(--r-common));
}

/* js/app.js adds .is-revealed: on the first paint an observer adds it as
   the band scrolls in, and on every paint after that it is already in
   the markup, so the bar is simply at its width with nothing to run. */
.crar-sec.is-revealed .crar-prog__f { width: var(--pct, 0%); }

/* A shut section lets its name fall back, so an open one is the thing
   the eye lands on. */
.crar-sec:not([open]) .cshelf__head { color: var(--muted); }

.crar-sec .cshelf { margin: 18px 0 26px; }
.crar-sec .collection__none { margin: 14px 0 20px; }

/* A tile is a control now: it opens the card detail. */
.cshelf__card[data-index] { cursor: pointer; }

.cshelf__card[data-index]:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}

/* ---------- THE TILE AS A SURFACE ----------
   The shadow and the ring change on hover here, because a colour is not
   a movement. The lift itself is in the motion query. */
.cshelf__card[data-index] {
  border-radius: 8px;
  transition: box-shadow 220ms ease;
}

/* NOT the locked tiles. A locked tile is clickable, so it carries
   data-index too, and [data-index] img outranks .cshelf__card--locked
   img: this rule was silently replacing the locked tile's own filter
   and opacity transitions with box-shadow alone, which killed both the
   materialise and the hover brighten. */
.cshelf__card[data-index]:not(.cshelf__card--locked) img { transition: box-shadow 220ms ease; }

.cshelf__card[data-index]:hover {
  box-shadow:
    0 14px 34px rgba(0, 0, 0, 0.5),
    0 0 26px color-mix(in srgb, var(--accent) 28%, transparent);
}

.cshelf__card[data-index]:hover img {
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 55%, transparent);
}

.cshelf__card[data-index]:hover .cshelf__name { color: #fff; }

/* ---------- THE LOCKED WALL ----------
   The same tile at the same size, so the earned and the unearned read
   as one collection rather than as a grid and a footnote under it. What
   separates them is light: a locked tile is dimmed and desaturated and
   carries a lock, and hovering it brings the art up.

   THE ART IS NOT FETCHED UNTIL IT IS NEEDED. js/app.js puts the urls in
   data-src and data-srcset and an observer swaps them in, the same
   arrangement js/achievements.js uses on its own grid. With the fold
   shut on load, that is zero requests for 230 cards. */
.cshelf__card--locked { position: relative; cursor: pointer; }

.cshelf__card--locked img {
  filter: brightness(0.34) saturate(0.55);
  transition: filter 280ms ease, box-shadow 280ms ease;
}

/* The dark wash over the art, and the rarity's own edge down its left.
   The edge reads --card-rarity, which the tile already carries for its
   ring, so there is no second rarity map for the locked state. */
.cshelf__card--locked::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  aspect-ratio: 3 / 1;
  border-radius: 8px;
  pointer-events: none;
  background: linear-gradient(160deg, rgba(8, 11, 18, 0.35), rgba(8, 11, 18, 0.72));
  transition: opacity 280ms ease;
}

/* The rarity's edge down the left of the ART, not of the tile: the tile
   is taller than its image because the name sits under it. So this box
   is the image's box, the same 3:1 the overlay uses, and the colour is
   painted as a 3px stop inside it. drop-shadow rather than box-shadow
   on hover, so the glow follows the painted strip instead of outlining
   the whole rectangle. */
.cshelf__card--locked::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  aspect-ratio: 3 / 1;
  border-radius: 8px 0 0 8px;
  pointer-events: none;
  background: linear-gradient(90deg,
    var(--card-rarity, var(--r-common)) 0 3px, transparent 3px);
  opacity: 0.4;
  transition: opacity 280ms ease, filter 280ms ease;
}

.cshelf__card--locked .cshelf__lock {
  position: absolute;
  left: 50%;
  top: 0;
  width: 18px;
  height: 18px;
  margin-left: -9px;
  color: var(--muted);
  opacity: 0.6;
  pointer-events: none;
  transition: opacity 260ms ease;
}

.cshelf__card--locked .cshelf__lock svg { display: block; width: 100%; height: 100%; }

.cshelf__card--locked .cshelf__name { color: rgba(255, 255, 255, 0.42); }

/* The grail lighting up. */
.cshelf__card--locked:hover img { filter: brightness(0.92) saturate(1); }
.cshelf__card--locked:hover::before { opacity: 0; }
.cshelf__card--locked:hover::after {
  opacity: 1;
  filter: drop-shadow(0 0 6px var(--card-rarity, var(--r-common)));
}
.cshelf__card--locked:hover .cshelf__lock { opacity: 0; }
.cshelf__card--locked:hover .cshelf__name { color: #fff; }

/* The one shot materialise, and the only ambient motion on this wall.
   The rule applies only once js/app.js has marked the tile, so a browser
   that never runs the observer shows the art rather than nothing. */
.cshelf__card--locked.is-lazy img { opacity: 0; }
.cshelf__card--locked.is-loaded img { opacity: 1; }

/* BEFORE THE ART IS ASKED FOR, AND IF IT FAILS. A deferred image has no
   src until the observer swaps data-src in, and an <img> with no src
   paints as the browser's broken-image glyph. So a deferred image that
   has not been given a src yet is not drawn at all: the tile shows its
   plain plate until the art arrives, the same as the calling cards
   page, whose tiles start hidden until they load. A file that fails to
   load is hidden the same way (.is-missing, set by js/app.js). Covers
   the locked wall, the peek strips and the vault reel, the three places
   the collection defers its art. */
.cshelf__card--locked img:not([src]),
.crar-peek__t img:not([src]),
.cwall-reel__t img:not([src]),
.is-missing img { visibility: hidden; }

/* ---------- 8f2. THE LOCKED BAND FOLDS ----------
   The wall of dim plates is the scale of what is left, which is worth
   being able to look at and not worth 399px of every visit. The count
   is still the handle, the plates are still behind it, and it is still
   shut when the page loads. What it has now is a picture of the pile
   beside the number, which says depth in a way a count cannot. */
.cwall-fold { margin-top: 30px; padding-top: 24px; border-top: 1px solid var(--line); }

/* ---------- THE VAULT PLATE ----------
   The capstone of the module, and deliberately not another rarity band.
   The bands are neutral surfaces carrying one colour each; this is a
   blue surface, because what is behind it is every rarity at once and
   the accent is the one colour in the product that means "the rest of
   it". Same material language, different temperature, so the eye reads
   it as the end of the list rather than as a fifth entry in it. */
.cwall-fold__toggle {
  position: relative;
  display: flex;
  align-items: center;
  gap: clamp(14px, 2vw, 26px);
  min-height: 96px;
  padding: 0 18px 0 22px;
  border: 1px solid rgba(61, 127, 255, 0.16);
  border-radius: 14px;
  overflow: hidden;
  cursor: pointer;
  list-style: none;
  background: linear-gradient(180deg, rgba(20, 28, 48, 0.55), rgba(10, 14, 26, 0.4));
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.06),
    0 14px 36px rgba(0, 0, 0, 0.5);
}

.cwall-fold__toggle:hover {
  border-color: rgba(61, 127, 255, 0.32);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.09),
    0 20px 46px rgba(0, 0, 0, 0.6);
}

/* Contained, like the bands' glow and for the same reason: a gradient
   that reaches across the whole plate leaves every pixel of it tinted.
   This one sits behind the figure and stops. */
.cwall-fold__toggle::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 380px;
  max-width: 55%;
  pointer-events: none;
  opacity: 0.55;
  background: radial-gradient(260px 130px at 6% 50%,
    color-mix(in srgb, var(--accent) 16%, transparent),
    transparent 70%);
}
.cwall-fold__toggle::-webkit-details-marker { display: none; }

/* ---------- THE HERO FIGURE ----------
   THE FAN IS GONE. Five tilted plates were a picture of a pile, which
   was the right idea when the row had nothing else in it, but they were
   silhouettes: an abstraction of cards sitting beside a count of real
   ones. The reel next to this figure is the same idea drawn with the
   actual art, so the drawing of it is redundant.

   The count-up from the earlier pass still writes into .cshelf__head,
   which is why that hook survives the restyle. What changed is what it
   holds: the number alone, with the words beside it on their own line,
   rather than one string carrying both. */
.cwall-vault {
  position: relative;
  flex: none;
  min-width: 0;
}

.cwall-fold__toggle .cshelf__head {
  display: block;
  margin: 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(30px, 2.6vw, 38px);
  line-height: 1;
  letter-spacing: -0.01em;
  text-transform: none;
  color: #fff;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 22px rgba(61, 127, 255, 0.6);
}

.cwall-vault__label {
  display: block;
  margin-top: 7px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}

/* ---------- THE REEL ----------
   The grails, drifting. These are real locked cards, rarest first, at
   the brightness of something behind glass: the row is a promise, not a
   gallery, and the gallery is one press away underneath it.

   IT IS DECORATION, and every attribute says so: aria-hidden, no tab
   stop, no click, pointer-events off. The plate is a native <summary>,
   so pressing anywhere on it opens the wall.

   THE DRIFT IS PAUSED BY DEFAULT and only runs while js/app.js says the
   row is on screen. An infinite animation on a strip that has scrolled
   away is work nobody asked for. */
.cwall-reel {
  position: relative;
  flex: 1;
  min-width: 0;
  align-self: center;
  height: 66px;
  overflow: hidden;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 6%, #000 82%, transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 6%, #000 82%, transparent 100%);
}

.cwall-reel__track {
  display: flex;
  width: max-content;
  height: 100%;
  gap: 10px;
  animation-play-state: paused;
}

.cwall-reel__t {
  position: relative;
  flex: 0 0 auto;
  height: 100%;
  width: auto;
  aspect-ratio: 3 / 1;
  border-radius: 6px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.03);
}

.cwall-reel__t img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: brightness(0.32) saturate(0.5);
}

.cwall-reel__t::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 2px;
  z-index: 2;
  background: var(--card-rarity, var(--r-common));
}

.cwall-reel__t::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(180deg, rgba(8, 11, 18, 0.4), rgba(8, 11, 18, 0.68));
}

.cwall-reel__t .cshelf__lock {
  left: 50%;
  top: 50%;
  width: 13px;
  height: 13px;
  margin: -7px 0 0 -7px;
  z-index: 3;
  opacity: 0.45;
}

/* Reaching for the row brings the glass up a little. */
.cwall-fold__toggle:hover .cwall-reel__t img { filter: brightness(0.5) saturate(0.7); }

/* ---------- THE COMPLETION BAR ----------
   The whole collection, both halves, along the bottom of the plate.
   This is the ONE proportion the vault draws, and it is the only place
   in the module measured against 303 rather than 273: the figure above
   it counts achievement cards, this counts everything the account can
   hold, and the tag beside the control says which. */
.cwall-prog {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 3px;
  pointer-events: none;
  background: rgba(255, 255, 255, 0.05);
}

.cwall-prog__f {
  display: block;
  height: 100%;
  width: 0;
  background: linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent) 55%, #fff));
  box-shadow: 0 0 12px -1px var(--accent);
}

.cwall-fold.is-revealed .cwall-prog__f { width: var(--cpct, 0%); }

.cwall-pct {
  position: relative;
  flex: none;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.cwall-fold__toggle:hover .cshelf__head { color: #fff; }

.cwall-fold .cwall { margin-top: 20px; }

/* ---------- 8g. THE COLLECTION WALL ----------
   The cards held, at a size where the art reads, and the rest of the
   catalogue as the plates they are. The held grid is the centrepiece;
   the locked band under it is the scale of what is left, in one dim
   block rather than 249 empty frames pretending to be cards. */
.cshelf { grid-template-columns: repeat(auto-fill, minmax(clamp(150px, 13vw, 208px), 1fr)); }

/* THE LOCKED GRID IS THE EARNED GRID. It was a dense wall of 52px
   plates whose only job was to show a quantity; it holds the same tile
   at the same size now, so scrolling from what is held into what is not
   is one collection getting darker rather than two different objects.

   .cwall__plate is gone with the element it styled: nothing renders an
   empty plate any more, and the four rgba values it carried were the
   only place in this file that wrote a rarity colour out by hand. */
.cwall {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(clamp(150px, 13vw, 208px), 1fr));
  gap: 12px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* ---------- 8f3. THE BAND AND THE FAN ON A PHONE ----------
   Nothing here is a proportion, so nothing here has to be protected
   from getting narrow. What does have to give is absolute size: the
   fan is 357px wide at its drawn scale and the panel's content box at
   320 is 288px, so the plates and their step shrink together through
   the two custom properties, which keeps the overlap and the rake
   exactly as drawn at a smaller size. The summary stacks, the four
   counts wrap to two rows of two, and the heading keeps every part it
   has at 1920 at a smaller size rather than dropping one. */
@media (max-width: 719.98px) {
  .dsum { display: block; padding-bottom: 20px; }
  .dsum__num b { font-size: 46px; }
  .crar-list { margin: 18px 0 0; gap: 8px; }
  .crar { flex: 1 1 calc(50% - 4px); padding: 9px 12px 9px 10px; gap: 9px; }

  .crar-sec__toggle { gap: 10px; padding: 11px 2px 10px; }
  .crar-sec__toggle .cshelf__head { font-size: 16px; letter-spacing: 0.06em; }
  .crar-sec__count { padding: 4px 8px; font-size: 11px; }
  .crar-sec__tog,
  .cwall-fold__tog { gap: 7px; padding: 7px 11px 7px 13px; font-size: 9px; letter-spacing: 0.12em; }

  .cwall-fold__toggle { flex-wrap: wrap; gap: 12px; }
  .cwall-fold__toggle { min-height: 82px; padding: 0 12px 0 14px; gap: 12px; }
  .cwall-fold__toggle .cshelf__head { font-size: 26px; }
  .cwall-vault__label { margin-top: 5px; font-size: 9.5px; letter-spacing: 0.14em; }
  .cwall-reel { height: 44px; }
  .cwall-pct { display: none; }
}

/* ---------- 8f4. THE MOTION LAYER ----------

   OPT IN, NOT OPT OUT, the same way 8c does it for the status hero.
   Every keyframe is declared out here, and every `animation` that runs
   one is declared inside `prefers-reduced-motion: no-preference`. The
   resting stylesheet has no motion in it at all: the aura sits at its
   dimmest, the sheen at opacity 0, a card at its finished position. A
   reader with reduced motion gets the finished interface rather than
   whichever frame an animation happened to start on.

   Hover colour and shadow are NOT in here. A shadow warming is not a
   movement, and taking it away from a reduced motion reader would cost
   them the affordance without sparing them anything. */

@keyframes crarAura {
  0%, 100% { opacity: 0.35; }
  50%      { opacity: 0.8; }
}

/* The band's glow breathes, barely. Narrower than the old aura in
   range as well as in size: it should be something you notice only if
   you are looking at one band for a while. */
@keyframes cwall-drift {
  to { transform: translateX(-50%); }
}

@keyframes crarWashBreath {
  0%, 100% { opacity: 0.55; }
  50%      { opacity: 0.85; }
}

@keyframes cshelfRise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

/* The bloom peaks a little after the card has arrived and leaves
   nothing behind: it ends on the same box-shadow the tile rests at, so
   there is no state to clean up if it is interrupted. */
@keyframes cshelfBloom {
  0%   { box-shadow: 0 0 0 0 transparent; }
  45%  { box-shadow: 0 0 26px -2px var(--card-rarity, var(--r-common)); }
  100% { box-shadow: 0 0 0 0 transparent; }
}

@media (prefers-reduced-motion: no-preference) {
  /* THE BANDS BREATHE OUT OF STEP. Four glows on one screen pulsing
     together would read as the page throbbing. */
  .crar-sec__wash { animation: crarWashBreath 9s ease-in-out infinite; }

  .crar-sec[data-rarity="legendary"] .crar-sec__wash { animation-delay: 0s; }
  .crar-sec[data-rarity="epic"] .crar-sec__wash { animation-delay: 1.6s; }
  .crar-sec[data-rarity="rare"] .crar-sec__wash { animation-delay: 3.2s; }
  .crar-sec[data-rarity="common"] .crar-sec__wash { animation-delay: 4.8s; }

  /* The plate lifts under the pointer. */
  .crar-sec__toggle { transition: transform 200ms ease, border-color 200ms ease, box-shadow 200ms ease; }
  .crar-sec__toggle:hover { transform: translateY(-2px); }

  /* The tease fades rather than vanishing as the grid appears. */
  .crar-peek { transition: opacity 250ms ease; }

  /* The sliver fills once, as its band arrives. The delay lets the band
     settle first, so the bar is read as part of the plate rather than
     as something that happened to it. */
  .crar-prog__f { transition: width 900ms cubic-bezier(0.16, 0.84, 0.34, 1) 200ms; }

  /* The vault's glow breathes on the same curve as a band's, a beat
     slower, so the capstone is the calmest thing in the module. */
  .cwall-fold__toggle::before { animation: crarWashBreath 8s ease-in-out infinite; }

  .cwall-fold__toggle { transition: transform 200ms ease, border-color 200ms ease, box-shadow 200ms ease; }
  .cwall-fold__toggle:hover { transform: translateY(-2px); }

  /* THE DRIFT. The track holds the selection twice over, so translating
     it exactly half its width lands the copy where the original was and
     the loop has no seam. It stays paused until js/app.js says the row
     is on screen. */
  .cwall-reel__track {
    animation: cwall-drift 60s linear infinite;
    /* AFTER the shorthand, not before. `animation` is a shorthand and it
       resets animation-play-state to running, so the paused default in
       the resting stylesheet was being overwritten the moment the drift
       was declared, and the strip ran whether or not it was on screen. */
    animation-play-state: paused;
  }
  .cwall-fold.is-invault .cwall-reel__track { animation-play-state: running; }

  .cwall-reel__t img { transition: filter 260ms ease; }

  .cwall-prog__f { transition: width 1100ms cubic-bezier(0.16, 0.84, 0.34, 1) 250ms; }

  /* ---------- THE CASCADE ----------
     A band is armed by js/app.js on the FIRST render only, and the
     observer adds .is-revealed as it scrolls in. Arming is what holds
     the card at opacity 0, so a repaint that does not arm draws the
     cards already finished and nothing replays. --ci is the card's
     index inside its own band, set inline. */
  .cshelf.is-armed > .cshelf__card { opacity: 0; }

  .cshelf.is-armed > .cshelf__card.is-revealed {
    animation:
      cshelfRise 500ms cubic-bezier(0.16, 0.84, 0.34, 1) both,
      cshelfBloom 900ms ease-out both;
    animation-delay: calc(var(--ci, 0) * 55ms);
  }

  /* Reaching for a card cancels whatever it was doing. */
  .cshelf__card[data-index]:hover { animation: none; }

    /* Same exclusion as the image rule above, and for the same reason:
     the locked tile has its own, shorter lift. */
  .cshelf__card[data-index] { transition: transform 220ms ease, box-shadow 220ms ease; }
  .cshelf__card[data-index]:not(.cshelf__card--locked):hover {
    transform: translateY(-5px) scale(1.025);
  }

  .cshelf__card--locked { transition: transform 280ms ease; }
  .cshelf__card--locked:hover { transform: translateY(-4px) scale(1.02); }

  /* The materialise. Outside this query the image simply appears. */
  .cshelf__card--locked img { transition: opacity 400ms ease, filter 280ms ease, box-shadow 280ms ease; }
}

/* ON A PHONE THE WALL IS DENSER, not longer. At 320 the held cards were
   one per row and the plates five, which put 7,300px of collection under
   a member's thumb. The art is smaller here than it deserves; a column
   of 46 single file cards is worse. */
@media (max-width: 719.98px) {
  .cshelf {
    grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
    gap: 8px;
  }

  .cshelf__name { font-size: 10px; margin-top: 4px; }

  .cwall {
    grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
    gap: 8px;
  }
}


/* ============================================================
   STAGE 1. THE VALUE STRIP.

   One panel under the banner: the total on the left, the four periods
   across the right. The reference's proportions are 2.1 : 1 : 1 : 1 : 1,
   so the panel is 2.1fr against 4fr and the periods are a nested grid of
   four equal cells with the same 12px gap. Same result as one five track
   grid, without the trap below.

   THE TRAP: the periods arrive by being MOVED, and they arrive inside
   .book__col with two siblings, the block's eyebrow and its honesty
   note. Flattening the column with display:contents put all six into the
   grid and the fourth period wrapped onto a second row behind a 160px
   note. Both siblings span the full width instead, so the four cells are
   the only things competing for the four tracks.

   The nodes are the ones js/app.js moved in, so the figures are the same
   figures, #rankFigure is still what runCountUp eases, #statsGrid is
   still what renderStats writes, and the note is still verbatim.
   ============================================================ */

#valueStrip[hidden] { display: none; }

.vstrip {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 12px;
  min-height: 128px;
  align-items: center;
  border-radius: 9px;
}

@media (min-width: 860px) {
  .vstrip { grid-template-columns: 2.1fr 4fr; }
}

.vstrip__total { min-width: 0; }

/* The moved block keeps its classes; only its measurements change. */
.vstrip__total .progression__head { display: block; margin: 0; padding: 0; border: 0; }
.vstrip__total .progression__value { display: block; }

.vstrip__total .eyebrow {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--muted);
}

.vstrip__total .progression__figure {
  margin: 6px 0 0;
  font-size: clamp(30px, 3.1vw, 42px);
  line-height: 1.02;
  letter-spacing: -0.01em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.vstrip__total .progression__gain { margin: 6px 0 0; font-size: 13px; }
.vstrip__total .progression__basis { margin: 4px 0 0; font-size: 11px; color: var(--faint); }

/* ---------- The four periods ---------- */
.vstrip__periods {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px 12px;
  align-content: center;
  min-width: 0;
}

.vstrip__periods .book__col { display: contents; }
.vstrip__periods .stats { display: contents; }

/* The two that are not periods take the whole width rather than a cell. */
.vstrip__periods .book__eyebrow,
.vstrip__periods .section__note { grid-column: 1 / -1; margin: 0; }

.vstrip__periods .book__eyebrow {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--muted);
}

.vstrip__periods .section__note { font-size: 11.5px; line-height: 1.45; }

.vstrip .stat {
  display: block;
  min-width: 0;
  padding: 0 0 0 clamp(12px, 1.4vw, 20px);
  background: none;
  border-left: 1px solid var(--line);
}

.vstrip .stat:first-child { border-left: 0; padding-left: 0; }
.vstrip .stat:hover { background: none; }

.vstrip .stat__label {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--muted);
}

.vstrip .stat__value {
  margin: 5px 0 0;
  font-family: var(--font-mono);
  font-size: 17px;
  line-height: 1.15;
  font-variant-numeric: tabular-nums;
}

.vstrip .stat__delta { margin: 3px 0 0; font-size: 11px; }

/* Below the panel's breakpoint the four cells go two by two and the
   dividing rule would be a line down the left of a full width row. */
@media (max-width: 859px) {
  .vstrip__periods { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .vstrip .stat { border-left: 0; padding-left: 0; }
}
