/* ============================================================
   SkinSignal
   Design system tokens. Change these, not the values below.
   ============================================================ */

:root {
  /* Colour */
  --bg:      #050505;   /* page */
  --text:    #f5f2ec;   /* primary off-white */
  --muted:   #8a8a85;   /* secondary grey */
  /* --faint was #5f5f5b here and #70706b in a later :root, which won.
     At #70706b the token failed AA at EVERY ONE of its use sites, not
     only on the signal cards: 4.09 on the page ground and 3.64 on the
     lightest module surface, against a 4.5 floor. One token, wrong
     everywhere, so it is fixed here rather than patched per component.

     #82827d is the smallest value on this token's own shape (r = g,
     b five lower) that clears 4.5 on every surface the token actually
     sits on. Measured, worst case first:

       #13161d  free stats tile      3.64  ->  4.69
       #141414  --hover, row lift    3.70  ->  4.77
       #121215  .stat:hover          3.76  ->  4.84
       #13151a  holdings chip        3.67  ->  4.73
       #0e1016  module surface       3.82  ->  4.92
       #0e0e10  card surface         3.87  ->  4.99
       #050505  page ground          4.09  ->  5.28

     The three step hierarchy survives the lift: on the worst surface
     faint is 4.69, --muted 6.64 and --text 16.20, so this is still
     clearly the dimmest of the three and not a second --muted. */
  --faint:   #82827d;   /* tertiary, placeholders and micro-labels */
  --accent:  #3d7fff;   /* the only accent. signals, gains, focus */
  --loss:    #c0392b;   /* negative P&L numbers in the table ONLY */

  /* Signal tones. The dashboard is otherwise single accent, but the
     signals section uses colour as information: green means money on
     the table, red means something that can cost you, amber means
     timing. Brighter than --loss on purpose, because these carry
     meaning at a glance rather than annotating a figure. */
  --gain:    #1fd67d;
  --risk:    #e5484d;
  --info:    #e0a53a;
  --hover:   #141414;   /* row / option hover */

  /* Type */
  --font-display: "Saira", "Helvetica Neue", Arial, sans-serif;
  --font-body:    "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono:    "JetBrains Mono", ui-monospace, "SF Mono", Consolas, monospace;

  /* ---------- Display width scale ----------

     Saira carries a wdth axis (100-125 as requested from Google), and
     that axis is the main lever for hierarchy here. Weight alone does
     not separate a wordmark from a table label at these sizes; width
     does, and it does it without touching size or spacing.

     Four rungs, not eighteen magic numbers. Every display rule picks
     one, so the hierarchy is legible in the token names rather than
     inferred by comparing percentages scattered across two files.

     The rule that decides which rung:
       brand and rank   always widest, whatever their pixel size. They
                        are the identity moments, and the wordmark at
                        17px should still read as the widest thing on
                        the page.
       >= 24px          hero
       13-23px          header
       small uppercase  natural width. These already carry 0.14-0.18em
                        tracking, and expanding a 9.5px cut on top of
                        that smears it. Tracking is doing the widening
                        job at that size, so the axis stays out of it.

     If Saira is ever swapped again, retune these four values and every
     display site follows. That is the point of them. */
  --display-wide:   125%;   /* wordmark, footer brand, rank names */
  --display-hero:   118%;   /* large page and section heroes, >= 24px */
  --display-header: 110%;   /* section and card headers, 13-23px */
  --display-label:  100%;   /* small uppercase micro-labels */

  /* ============================================================
     DEPTH / SURFACE LANGUAGE

     Flat surfaces read as a web app. Layered ones read as a product.
     Four rungs, and an element's rung states what it IS, not how it
     looks:

       sunken   wells things sit INSIDE. Thumbnails, progress tracks,
                inputs. Darker than the page, so content reads as
                recessed into the board.
       base     the page itself.
       raised   ordinary cards. Stat blocks, signal rows, the table.
       hero     identity and reward surfaces ONLY. The rank strip and
                the flagship signal. Nothing else may claim this rung,
                because its whole job is to be rarer than the others.

     The blue channel runs one or two points ahead of red and green as
     the rungs climb (0a0a0b, 0e0e10, 131316). That is not a palette
     addition, it is the same neutral family reading very slightly cool
     as it catches light, which is what stops a stack of near blacks
     going muddy. Locked palette is untouched. */
  --surface-sunken: #0a0a0b;
  --surface:        #0e0e10;   /* raised. the default card */
  --surface-hero:   #131316;   /* rank strip, flagship signal */

  /* Hairlines. Two of them, because a border that is one flat grey on
     all four sides reads as a box drawn around content, while a real
     edge is lighter where light lands on it. --edge-lit is a 1px inner
     highlight along the TOP of a surface only; paired with the darker
     border it gives the "machined panel" read that sells the depth. */
  --line:      #1c1c1c;   /* hairline borders and dividers */
  --line-warm: #24242a;   /* hover / active edges */
  --edge-lit:  inset 0 1px 0 rgba(245, 242, 236, 0.045);
  --edge-lit-hero: inset 0 1px 0 rgba(245, 242, 236, 0.075);

  /* Elevation. Deliberately tight, dark and low spread. The default
     web-app drop shadow (large, soft, grey) reads cheap on near black
     because it turns into visible haze. These are almost pure black,
     so they occlude rather than smudge. */
  --elev-1: 0 1px 2px rgba(0, 0, 0, 0.45), 0 2px 10px -6px rgba(0, 0, 0, 0.6);
  --elev-2: 0 2px 5px rgba(0, 0, 0, 0.5),  0 10px 28px -14px rgba(0, 0, 0, 0.75);
  --elev-3: 0 4px 10px rgba(0, 0, 0, 0.55), 0 22px 54px -24px rgba(0, 0, 0, 0.9);

  /* ============================================================
     GLOW LANGUAGE

     Glow is a MEANING, not a finish. Two levels and one rule:

       soft   "this is interactive / this is alive". Hover states,
              focus, the live-inventory badge.
       hero   "this is money or identity". The rank strip, the flagship
              arbitrage signal, the total value figure.

     And the rule that keeps it from reading cheap: DATA NEVER GLOWS.
     The holdings table, raw figures, deltas and float values get no
     glow at any state, ever. Glow marks the handful of places worth
     looking at; if it were everywhere it would mark nothing.

     --glow-tone is parameterised so the signal tones (gain/risk/info)
     reuse the same geometry in their own colour rather than inventing
     a second glow system. */
  --glow-accent: 61, 127, 255;
  --glow-soft: 0 0 0 1px rgba(var(--glow-accent), 0.16),
               0 0 22px -8px rgba(var(--glow-accent), 0.34);
  --glow-hero: 0 0 0 1px rgba(var(--glow-accent), 0.26),
               0 0 38px -10px rgba(var(--glow-accent), 0.5),
               0 0 90px -34px rgba(var(--glow-accent), 0.4);

  /* ============================================================
     MOTION LANGUAGE

     One easing curve for everything that moves. It is a strong ease
     out: fast departure, long settle. That asymmetry is what makes
     motion feel expensive rather than mechanical, and using a single
     curve everywhere is what makes the interface feel like one object
     instead of a pile of components each with its own physics.

     Four durations, chosen by what is moving rather than by taste:
       swift   colour and opacity. Below ~150ms a colour change reads
               as instant, which is correct for feedback.
       lift    hover transforms. Long enough to see, short enough that
               a mouse crossing three cards does not feel like wading.
       reveal  entrances of single elements.
       epic    the dashboard arrival. The one place a long duration is
               earned, because it happens once.

     --stagger is the per-child delay step for list entrances. Kept
     small: eight cards at 55ms is under half a second end to end. */
  --ease:       cubic-bezier(0.16, 1, 0.3, 1);
  --dur-swift:  140ms;
  --dur-lift:   260ms;
  --dur-reveal: 520ms;
  --dur-epic:   900ms;
  --stagger:    55ms;

  /* Hover lift distance. Tokenised so every card rises by exactly the
     same amount and a future change is one edit, not fourteen. */
  --lift: -3px;

  /* ONE SYSTEM FOR THE LANDING'S SECTIONS. Every section's entrance,
     hover and ambient light reads these, so the page moves and is lit
     as one thing rather than nine. Each rule that uses them carries the
     same value as its fallback (pricing.css is also on the dashboard).

       reveal   a section's parts arriving once, on scroll: a fade and a
                short rise. -sm for the small, many things (rows, cells).
       hover    a card under the pointer: a 2px lift, a brighter edge.
       amb      the two lights behind every section, blue and gold, at
                one strength, so no section is lit louder than the next. */
  --reveal-rise:    12px;
  --reveal-rise-sm: 8px;
  --reveal-fade:    550ms;
  --reveal-dur:     650ms;
  --reveal-fade-sm: 420ms;
  --reveal-dur-sm:  480ms;
  --hover-dur:      200ms;
  --hover-lift:     -2px;
  --amb-blue:       0.08;
  --amb-gold:       0.07;

  /* Layout */
  --gutter: 32px;
  --maxw:   1280px;
}


/* ============================================================
   THE LIVING VOID
   Sitewide atmospheric background.

   The brief: the black should read as deep lit space rather than an
   empty div. Three very large, very soft radial glows drift on long
   offset cycles, so the field never repeats visibly and never
   resolves into "three circles".

   THE OPACITY BUDGET, and why it is a budget.
   Each glow peaks around 0.10-0.13 alpha at its centre and falls to
   nothing by 60-70% of its radius, so the brightest point anywhere on
   the page is roughly 12% and typical coverage is far below that. It
   has to stay there: this sits behind body text, and atmosphere that
   costs legibility is not atmosphere, it is noise. If these are ever
   raised, check the holdings table first, since small mono figures on
   a lifted ground are the first thing to suffer.

   GPU BUDGET.
   Only transform is animated, never the gradients themselves, so each
   glow is painted once and then moved by the compositor. No filter:
   blur() anywhere: blurring a 1200px element every frame is exactly
   the kind of thing that turns a background into a frame-rate
   problem, and a soft radial-gradient is free by comparison.

   The layer is fixed and pointer-transparent, so it never scrolls,
   never intercepts a click, and costs nothing on resize.
   ============================================================ */

.void {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  /* Its own compositor layer, so drifting it never repaints content. */
  will-change: transform;
  contain: strict;
}

/* Everything the user actually reads sits above the void. Declared on
   the three landmarks rather than with a blanket child selector, so
   nothing later accidentally inherits a stacking context it did not
   ask for. */
.topbar,
main,
.footer { position: relative; z-index: 1; }

.void__glow {
  position: absolute;
  display: block;
  border-radius: 50%;
  /* Sized in vmax so the field scales with the viewport instead of
     going patchy on ultrawide monitors. */
  width: 92vmax;
  height: 92vmax;
  will-change: transform;
}

/* The house accent, top left, the largest and slowest. */
.void__glow--a {
  top: -34vmax;
  left: -22vmax;
  background: radial-gradient(circle at center,
    rgba(61, 127, 255, 0.115) 0%,
    rgba(61, 127, 255, 0.05) 34%,
    transparent 66%);
  animation: void-drift-a 68s ease-in-out infinite alternate;
}

/* A deeper, colder blue on the right. Different hue, not just a dimmer
   copy of the accent: two tones of the same blue would read as a
   gradient, three related blues read as depth. */
.void__glow--b {
  top: 8vmax;
  right: -30vmax;
  width: 78vmax;
  height: 78vmax;
  background: radial-gradient(circle at center,
    rgba(42, 86, 190, 0.10) 0%,
    rgba(30, 58, 140, 0.045) 38%,
    transparent 68%);
  animation: void-drift-b 84s ease-in-out infinite alternate;
}

/* Low and central, the faintest of the three. Keeps the lower half of
   long pages from going flat black below the fold. */
.void__glow--c {
  bottom: -40vmax;
  left: 24vmax;
  width: 86vmax;
  height: 86vmax;
  background: radial-gradient(circle at center,
    rgba(61, 127, 255, 0.085) 0%,
    rgba(38, 70, 160, 0.035) 40%,
    transparent 70%);
  animation: void-drift-c 96s ease-in-out infinite alternate;
}

/* Long, prime-ish durations that do not share a common multiple, so
   the three never sync back up into a visible pulse. Translation only,
   plus a little scale, all compositor work. */
@keyframes void-drift-a {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(9vmax, 6vmax, 0) scale(1.12); }
}
@keyframes void-drift-b {
  from { transform: translate3d(0, 0, 0) scale(1.06); }
  to   { transform: translate3d(-11vmax, 9vmax, 0) scale(1); }
}
@keyframes void-drift-c {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(7vmax, -8vmax, 0) scale(1.1); }
}

/* Reduced motion keeps the ATMOSPHERE and drops the movement. The
   glows stay exactly where they are, which is the point: the lit-space
   feeling is not motion, the drift is just what stops it looking
   painted on. Removing the glows entirely would punish the setting
   with a flatter page. */
@media (prefers-reduced-motion: reduce) {
  .void__glow { animation: none !important; }
}

/* ---------- Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body, h1, h2, h3, p, ul, figure { margin: 0; }
ul { padding: 0; list-style: none; }
table { border-collapse: collapse; }
button, input { font: inherit; color: inherit; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-weight: 300;
  font-size: 15px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-wrap: break-word;
}

::selection { background: var(--accent); color: #050505; }

:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 2px;
}

.container {
  width: 100%;
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 var(--gutter);
}

/* clip-path as well as clip so the label cannot leak into the layout
   if `clip` is ever dropped by a future browser. */
.visually-hidden {
  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;
}

/* ---------- Shared type utilities ---------- */
.eyebrow {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--muted);
  line-height: 1.4;
}

.mono {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

.pos { color: var(--accent); }
.neg { color: var(--loss); }

/* Hairline pill. Used for the sample-data note. */
.badge {
  display: inline-block;
  padding: 5px 13px 4px;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-family: var(--font-mono);
  font-weight: 400;
  font-size: 10px;
  line-height: 1.4;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}

/* Live data confirmed. Accent is allowed here, it is a positive state.
   This is one of exactly two elements permitted the breathe motion,
   because it is literally a liveness indicator: the pulse and the word
   say the same thing. */
.badge--live {
  position: relative;
  border-color: #24365e;
  color: var(--accent);
  background: rgba(61, 127, 255, 0.05);
  box-shadow: var(--glow-soft);
}

.badge--live::before {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  pointer-events: none;
  box-shadow: 0 0 16px -2px rgba(var(--glow-accent), 0.55);
  opacity: 0.5;
  animation: ss-breathe 6.5s ease-in-out infinite;
}

/* ============================================================
   1. TOP BAR
   ============================================================ */
/* The bar (.topbar, .topbar__inner), its nav and its account control
   are drawn by css/header.css, loaded on every page. The wordmark's type
   rules stay here: the footer uses them too. */

/* Type rules are kept so the alt text still reads as the wordmark
   if the logo image ever fails to load. */
.wordmark {
  display: inline-flex;
  align-items: center;
  font-family: var(--font-display);
  font-stretch: var(--display-wide);
  font-weight: 700;
  font-size: 17px;
  letter-spacing: -0.01em;
  color: var(--text);
  text-decoration: none;
}

/* 2172x724 source, 3:1. At 60px tall it renders 180px wide.
   The artwork carries its own vertical padding, so the visible lettering
   is roughly 60% of this box height. */
.wordmark__img {
  display: block;
  height: 60px;
  width: auto;
}

/* ---------- Currency switcher ---------- */
.currency { position: relative; }

.currency__button {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  height: 34px;
  padding: 0 12px;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 0;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.08em;
  cursor: pointer;
  transition: border-color 0.18s ease, color 0.18s ease;
}
.currency__button:hover { border-color: #2c2c2c; }
.currency__button[aria-expanded="true"] { border-color: var(--accent); }

.currency__chev { color: var(--muted); transition: transform 0.18s ease; }
.currency__button[aria-expanded="true"] .currency__chev { transform: rotate(180deg); color: var(--accent); }

.currency__menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  min-width: 186px;
  padding: 4px 0;
  background: var(--surface);
  border: 1px solid var(--line);
  display: none;
  z-index: 50;
}
.currency__menu.is-open { display: block; }

.currency__option {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 18px;
  padding: 9px 14px;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--muted);
  cursor: pointer;
}
.currency__option:hover,
.currency__option.is-active { background: var(--hover); color: var(--text); }
.currency__option[aria-selected="true"] { color: var(--accent); }
.currency__option span:last-child {
  font-family: var(--font-body);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--faint);
}

/* Which rate the figures are converted at. Not an option, so it is not
   selectable and reads as a footnote to the list above it. */
.currency__asof {
  margin-top: 4px;
  padding: 9px 14px 6px;
  border-top: 1px solid var(--line);
  max-width: 220px;
  font-family: var(--font-body);
  font-size: 10.5px;
  line-height: 1.45;
  color: var(--faint);
  cursor: default;
}

/* ============================================================
   2. THE ANALYSE NOTE STATES
   The pre-redesign centred hero lived here and nothing renders it any
   more. What survives are the two states toggled on the analyse status
   line: js/app.js on the dashboard, js/lander.js on the landing hero.
   ============================================================ */
.hero__note--error { color: var(--text); }
.hero__note--ok    { color: var(--muted); }

/* ============================================================
   3. DASHBOARD
   ============================================================ */
/* Dashboard rhythm is deliberately ~30% tighter than the hero so Portfolio,
   Signals and Holdings read as one board rather than three slabs. */
.section { padding: clamp(40px, 5vw, 64px) 0; }
.section--divided { border-top: 1px solid var(--line); }

.section__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding-bottom: 18px;
  margin-bottom: 28px;
  border-bottom: 1px solid var(--line);
}

.section__title {
  font-family: var(--font-display);
  font-stretch: var(--display-header);
  font-weight: 600;
  font-size: 19px;
  letter-spacing: -0.015em;
}

/* ============================================================
   THE HERO
   "Here is who you are."

   Not a card in a row any more. This is a full-width stage with real
   air around it, and it is the only thing on the board allowed to be
   dramatic at rest. Everything below it is deliberately quieter so
   this reads as the lead.

   The composition is a three-part grid: emblem, identity text, value.
   The equipped calling card spans beneath the text columns. On narrow
   screens it collapses to a single centred column.

   WHY .rank DOES NOT CLIP.
   The whole point of the hero is that its light escapes into the
   void. So overflow stays visible and the sweep gets its own clipped
   layer (.rank__sheen) instead. The old overflow:hidden was there for
   a full-bleed background treatment that no longer exists.
   ============================================================ */

.herorank {
  position: relative;
  padding: clamp(52px, 7vw, 104px) 0 clamp(44px, 5vw, 76px);
  /* The bleed layer below is deliberately wider than the section so
     the light does not end at a visible edge. `clip` rather than
     `hidden` because clip does NOT create a scroll container: hidden
     here would still have let the oversized glow push the document's
     scrollWidth out and give the whole page a horizontal scrollbar.
     Vertical bleed is untouched. */
  overflow-x: clip;
}

/* The hero's light bleeding into the page around it. Sits behind the
   card, wider than it, and is what makes the section read as lit
   rather than as a card sitting on black. */
.herorank::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: min(1500px, 128vw);
  height: 130%;
  transform: translate(-50%, -50%);
  pointer-events: none;
  background: radial-gradient(ellipse at center,
    rgba(61, 127, 255, 0.10) 0%,
    rgba(61, 127, 255, 0.035) 34%,
    transparent 68%);
}

.rank {
  position: relative;
  padding: clamp(30px, 3.6vw, 52px) clamp(26px, 3.2vw, 48px);
  background:
    radial-gradient(90% 120% at 10% 0%, rgba(61, 127, 255, 0.10), transparent 56%),
    linear-gradient(to bottom, var(--surface-hero), var(--surface) 82%);
  border: 1px solid var(--line-warm);
  border-radius: 18px;
  /* --elev-3 plus the hero glow is the heaviest treatment in the
     system, and this is the only resting element that gets it. See the
     note in the system layer about why it is not in the shared block. */
  box-shadow: var(--edge-lit-hero), var(--elev-3), var(--glow-hero);
}

/* The aura. A soft accent bloom that extends PAST the card edges, so
   the hero appears to be emitting light into the void rather than
   being a lit rectangle. Its own element because it must not be
   clipped, while the sheen must be. */
.rank__aura {
  position: absolute;
  inset: -14% -6%;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(ellipse at 28% 40%,
    rgba(61, 127, 255, 0.16) 0%,
    rgba(61, 127, 255, 0.05) 36%,
    transparent 66%);
}

/* The clipped sweep layer. Rounds to the card and hides the overflow,
   so the light band can travel the full width without spilling past
   the corners. */
.rank__sheen {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  border-radius: inherit;
  pointer-events: none;
}

/* A single wide, very low opacity band that crosses the hero once on
   arrival. This is the detail that sells the panel as a physical
   object catching light, and it is deliberately one-shot: a looping
   sweep would turn into a screensaver. */
.rank__sheen::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 45%;
  background: linear-gradient(
    100deg,
    transparent,
    rgba(245, 242, 236, 0.055) 46%,
    rgba(245, 242, 236, 0.10) 52%,
    transparent
  );
  transform: translate3d(-140%, 0, 0);
}

.rank.is-revealing .rank__sheen::after {
  animation: ss-sweep 1700ms var(--ease) 420ms both;
}

.rank > * { position: relative; z-index: 1; }

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

.rank:hover {
  border-color: #33333d;
  box-shadow: var(--edge-lit-hero), var(--elev-3),
              0 0 0 1px rgba(var(--glow-accent), 0.32),
              0 0 54px -10px rgba(var(--glow-accent), 0.62),
              0 0 130px -34px rgba(var(--glow-accent), 0.45);
}

/* The hero does not lift on hover. At this size a 3px translate reads
   as the whole page twitching rather than as feedback, and it is not
   a control: there is nothing to click. The glow alone answers. */
.rank:hover { transform: none; }


/* ============================================================
   EQUIPPED CALLING CARD

   Every card is the same 3:1 banner, so the strip is one fixed frame
   that any of them fills. The art is a background layer, not an <img>,
   because it must be croppable and sit under a scrim without joining
   the flex row.

   READABILITY IS THE WHOLE PROBLEM HERE.
   Card art is busy, bright and high contrast, and the rank label and
   emblem have to stay legible on top of it. Three things do that work:

     1. A left to right scrim: near solid #050505 across the left third,
        easing to fully transparent by the right edge. The emblem and
        text live in that solid zone, so they are effectively on a plain
        dark panel, while the art reads clearly on the right.
     2. A gentle top to bottom darkening, which catches art that happens
        to be bright in its lower half where the progress bar sits.
     3. The art itself is dimmed and slightly desaturated, so it never
        competes with the accent blue on the progress fill.

   The scrim uses two stops close together at 34% and 62% rather than a
   lazy linear fade, so the solid zone stays genuinely solid instead of
   already washing out behind the label.
   ============================================================ */


/* ---------- Emblem ----------
   The artwork is the centrepiece of the card, so it gets the space and
   carries no frame of its own: a border around detailed art cheapens it
   and fights the transparent PNG edges. The box is a fixed square with
   the size reserved up front, so nothing shifts while the image decodes
   and the non square Ape file still sits on the same centre line as the
   rest. */
.rank__emblem {
  position: relative;
  flex: 0 0 auto;
  /* FEATURED. Roughly double the old 144px at desktop, and fluid, so
     the art is the presence in the hero rather than an icon beside
     some text. The size is still reserved up front, so nothing shifts
     while the image decodes and the non-square Ape file still sits on
     the same centre line as the rest. */
  width: clamp(150px, 17vw, 248px);
  height: clamp(150px, 17vw, 248px);
  display: grid;
  place-items: center;

  /* The numeral plate is absolutely positioned and hangs below the box,
     so it contributes nothing to layout height. At the old 96px the
     strip happened to have room for it; at 144px it would have pushed
     past the panel and been cut off by the strip's overflow:hidden.

     This margin reserves that overhang as real layout space, so flex
     centring accounts for it and the plate has somewhere to sit. */
  margin-bottom: 12px;
}

/* A very soft pool of light under the art. Lifts the emblem off the
   near black panel without reading as a glow effect. */
.rank__emblem::before {
  content: "";
  position: absolute;
  inset: -8%;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(245, 242, 236, 0.05) 0%, rgba(245, 242, 236, 0) 68%);
}

.rank__art {
  position: relative;
  width: 100%;
  height: 100%;
  /* contain, not cover. Ape is 1230x1278 rather than square, and cover
     would crop it while every other tier stayed whole. */
  object-fit: contain;
  display: block;
}

/* ---------- Emblem shine ----------
   A slow diagonal gloss passing over the artwork, like light catching
   metal as you turn it.

   The sweep is MASKED BY THE EMBLEM ITSELF, using the same PNG as the
   mask image. Without that the band would cross the transparent corners
   of the box and read as a rectangle of light sliding over the panel,
   which looks like a loading skeleton rather than a reflection. Masked,
   the light only ever appears on the artwork.

   The mask URL is set as --emblem-mask by applyEmblem in js/app.js, so
   it always matches whichever tier is showing.

   Pacing: an 9s cycle where the sweep occupies about 1.5s of it. The
   long wait is the point. A constant loop reads as a busy gif; a rare
   catch of light reads as a material. */
@supports ((mask-image: url("x")) or (-webkit-mask-image: url("x"))) {
  .rank__emblem::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;

    background-image: linear-gradient(
      112deg,
      rgba(255, 255, 255, 0) 42%,
      rgba(255, 255, 255, 0.42) 50%,
      rgba(255, 255, 255, 0) 58%
    );
    background-repeat: no-repeat;
    background-size: 260% 100%;
    background-position: 190% 0;

    -webkit-mask-image: var(--emblem-mask);
    mask-image: var(--emblem-mask);
    -webkit-mask-size: contain;
    mask-size: contain;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-position: center;
    mask-position: center;

    /* screen keeps the sweep additive, so it brightens the metal rather
       than washing a grey film over it. */
    mix-blend-mode: screen;

    animation: emblemShine 9s ease-in-out infinite;
  }

  /* No artwork means no mask, so there would be nothing to light. */
  .rank__emblem--noart::after { display: none; }
}

@keyframes emblemShine {
  0%,  72% { background-position: 190% 0; }
  89%, 100% { background-position: -90% 0; }
}

/* The capstone catches a slightly stronger, cooler light. */
.rank--max .rank__emblem::after {
  background-image: linear-gradient(
    112deg,
    rgba(255, 255, 255, 0) 42%,
    rgba(214, 231, 255, 0.55) 50%,
    rgba(255, 255, 255, 0) 58%
  );
}

@media (prefers-reduced-motion: reduce) {
  .rank__emblem::after { animation: none; opacity: 0; }
}

/* The capstone earns a cold accent pool instead of the neutral one. */
.rank--max .rank__emblem::before {
  background: radial-gradient(circle, rgba(61, 127, 255, 0.16) 0%, rgba(61, 127, 255, 0) 70%);
}

/* ---------- Emblem fallback ----------
   Shown only when an image fails to load. Same monogram the card used
   before the artwork existed, so a missing file degrades to something
   deliberate rather than to a torn icon. */
.rank__fallback { display: none; }

.rank__emblem--noart .rank__art { display: none; }
.rank__emblem--noart .rank__fallback {
  display: grid;
  place-items: center;
  position: absolute;
  inset: 0;
  border: 1px solid #262626;
}

.rank__emblemframe {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 78px;
  height: 78px;
  margin: -39px 0 0 -39px;
  border: 1px solid #2e2e2e;
  transform: rotate(45deg);
}

.rank__initial {
  position: relative;
  font-family: var(--font-display);
  font-stretch: var(--display-wide);
  font-weight: 700;
  font-size: 44px;
  line-height: 1;
  color: var(--text);
}

.rank--max .rank__emblem--noart .rank__fallback { border-color: var(--accent); }
.rank--max .rank__emblemframe { border-color: #2b4c8f; }

/* ---------- Sub-level numeral ----------
   A small plate centred just under the emblem, the way a rank plate sits
   under a crest. Deliberately NOT overlaid on the art: the emblems are
   detailed all the way to their lower edge, and a chip sitting on top of
   one covers the part of the illustration people actually look at.

   It still needs its own solid ground, since the art can run behind its
   top edge. Painted in the panel colour so it reads as cut into the card.
   Hidden outright at Godfather, which has no sub-level. */
.rank__sub {
  position: absolute;
  left: 50%;
  bottom: -11px;
  transform: translateX(-50%);
  min-width: 38px;
  padding: 4px 9px;
  background: var(--surface);
  border: 1px solid #262626;
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.1em;
  text-align: center;
  color: var(--accent);
}
.rank__sub[hidden] { display: none; }

.rank__body {
  flex: 1 1 auto;
  min-width: 0;
}

.rank__eyebrow { margin-bottom: 10px; }

.rank__label {
  font-family: var(--font-display);
  font-stretch: var(--display-wide);
  font-weight: 700;
  font-size: clamp(24px, 2.6vw, 32px);
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--text);
}

/* The bar belongs to the rank text, not to the whole panel.

   Unconstrained it inherited the full width of the body, which on a
   wide screen stretched it most of the way across the strip and made
   the whole composition read as one long line rather than three
   zones. Capping it keeps it under the rank label where it belongs,
   and the end labels come with it since they live inside. */

/* A sunken channel the fill sits in, rather than a grey line. The
   inset shadow is what makes the accent read as sitting inside the
   panel instead of painted on top of it. */
.rank__track {
  height: 4px;
  background: var(--surface-sunken);
  border-radius: 999px;
  overflow: hidden;
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
}

/* The climb. Transitioned rather than set outright, so the bar visibly
   fills as each price batch lands instead of snapping. */
/* The climb. Transitioned rather than set outright, so the bar visibly
   fills as each price batch lands instead of snapping.

   Now on the system easing and carrying its own glow: this is a
   progress-toward-reward bar, which is squarely an identity moment,
   so it is allowed to be lit. The gradient brightens toward the
   leading edge so the tip reads as the live end. */
.rank__fill {
  height: 100%;
  width: 0;
  border-radius: 999px;
  background: linear-gradient(to right, #2f6ae0, var(--accent));
  box-shadow: 0 0 12px -1px rgba(61, 127, 255, 0.8);
  transition: width var(--dur-epic) var(--ease);
}

.rank__ends {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 18px;
  margin-top: 9px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.04em;
}
.rank__from { color: var(--text); }
.rank__to { color: var(--faint); }

.rank__note {
  margin-top: 12px;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  color: var(--muted);
}

/* ---------- Stat cards ----------
   1px grid gap over a --line background gives hairline dividers
   between cards without stacking borders. */
/* The 1px-gap-over-a-line-background trick is kept, but the grid now
   carries the radius and clips its children, so the block reads as one
   machined unit rather than four abutting rectangles. */
.stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: 12px;
  overflow: hidden;
}

.stat {
  position: relative;
  background: var(--surface);
  padding: 26px 24px 24px;
}

/* Hover raises the surface rather than lifting it. These are cells in
   a single unit; translating one out of the grid would tear the block
   apart, so the feedback is light and colour instead of geometry.
   This is the "calm on pure data" side of the principle. */
.stat:hover {
  background: #121215;
  z-index: 1;
}

.stat__label { margin-bottom: 20px; }

.stat__value {
  font-family: var(--font-mono);
  /* Tabular figures reserve equal advance widths, which is what stops
     the count-up reflowing the card on every frame. */
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  font-size: clamp(22px, 2.4vw, 29px);
  line-height: 1;
  letter-spacing: -0.02em;
}

/* NO first-child special case here any more.

   The value hero moved into the rank hero, so this grid is now three
   period comparisons and nothing else: pure data, styled to stay
   calm. Singling one of them out would be decoration rather than
   meaning, which is exactly what the glow language forbids. */

.stat__delta {
  margin-top: 12px;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  color: var(--muted);
}
.stat__delta .sub { color: var(--faint); }

/* ---------- Which items were left out ----------
   The count sits on the delta line. The names sit behind a native
   disclosure, which replaced a title attribute that only ever worked for
   a mouse. <summary> is focusable and operable by keyboard and by tap on
   its own, so there is no script and no ARIA here. */
.stat__excl {
  margin-top: 8px;
  font-family: var(--font-body);
}

.stat__excl-toggle {
  display: inline-block;
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--muted);
  cursor: pointer;
  /* 11px type gives an 18px line box. Padded to 24px so the tap target
     clears the WCAG 2.2 minimum on a phone, where this is the only way
     to reach the names at all. */
  padding: 3px 0;
  /* The native triangle is replaced by the sign below, which reads at
     this size where the triangle does not. Both engines need telling. */
  list-style: none;
}
.stat__excl-toggle::-webkit-details-marker { display: none; }

.stat__excl-toggle::before {
  content: '+';
  display: inline-block;
  width: 10px;
  margin-right: 6px;
  font-family: var(--font-mono);
  color: var(--faint);
}
.stat__excl[open] .stat__excl-toggle::before { content: '-'; }

.stat__excl-toggle:hover { color: var(--text); }

/* The global :focus-visible ring is 2px offset, which clips against the
   card padding here, so the same ring is drawn tight to the summary. */
.stat__excl-toggle:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 1px;
}

.stat__excl-list {
  margin: 8px 0 0;
  padding: 0;
  list-style: none;
  font-size: 11px;
  line-height: 1.55;
  color: var(--muted);
  /* Long market hash names, a narrow card: break rather than push the
     grid wider. */
  overflow-wrap: anywhere;
}

.stat__excl-rest { color: var(--faint); }

/* ---------- Signals ----------
   The money feature, so it is the one place on the dashboard allowed to
   use colour as information rather than decoration.

   THREE TONES, AND ONLY THREE.
     gain  green   money on the table
     risk  red     something that can cost you
     info  amber   timing and context, neither a win nor a loss

   The tone drives the left bar, the pill and the figure together, so a
   card reads before it is read: you sort the list by colour, then look.

   Restraint is what keeps this from turning into a casino. The card
   surface stays the same near black as everything else, colour appears
   only on a 3px edge, a small pill and one number, and the glow is a
   hover state rather than a permanent halo. */

.signals {
  background: transparent;
  border: 0;
  display: grid;
  gap: 10px;
}

.signal {
  position: relative;
  display: flex;
  align-items: center;
  gap: 24px;
  padding: 22px 26px 22px 30px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--signal-tone, var(--muted));
  border-radius: 10px;
  overflow: hidden;
  /* Transition comes from the shared system block. The old per-component
     curve and durations are gone on purpose: one easing everywhere is
     what makes the board feel like a single object. */
}

/* A very faint wash bleeding in from the accent edge. Sits under the
   content and fades out by a third of the way across, so the colour is
   felt at the edge without tinting the text. */
.signal::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Clips itself to the card's corners rather than relying on the
     parent's overflow:hidden to do it. That matters because the
     flagship needs overflow visible so its glow can escape the card,
     and without this the wash would paint square corners the moment
     the clipping was lifted. */
  border-radius: inherit;
  background: linear-gradient(
    to right,
    var(--signal-wash, transparent) 0%,
    transparent 34%
  );
}

.signal > * { position: relative; }

/* ---------- Tones ---------- */
.signal--gain {
  --signal-tone: var(--gain);
  --signal-wash: rgba(31, 214, 125, 0.07);
  --signal-glow: rgba(31, 214, 125, 0.16);
}
.signal--risk {
  --signal-tone: var(--risk);
  --signal-wash: rgba(229, 72, 77, 0.06);
  --signal-glow: rgba(229, 72, 77, 0.14);
}
.signal--info {
  --signal-tone: var(--info);
  --signal-wash: rgba(224, 165, 58, 0.06);
  --signal-glow: rgba(224, 165, 58, 0.13);
}

/* ---------- Hover ----------
   A small lift and a glow in the card's own tone. Two pixels is enough
   to feel responsive; more would make a list of eight cards seasick. */
.signal:hover {
  transform: translate3d(0, var(--lift), 0);
  border-color: var(--line-warm);
  border-left-color: var(--signal-tone);
  box-shadow: var(--edge-lit), var(--elev-2),
              0 0 0 1px var(--signal-glow),
              0 0 30px -6px var(--signal-glow);
}

/* ---------- Item thumbnail ----------
   The actual skin, so a card is recognisable before it is read. Steam
   serves these on a transparent background, so the frame provides the
   ground rather than the image carrying one. */
.signal__thumb {
  flex: 0 0 auto;
  width: 56px;
  height: 56px;
  display: grid;
  place-items: center;
  background: var(--surface-sunken);
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5);
}

.signal__thumb img {
  display: block;
  width: 100%;
  height: 100%;
  /* contain, not cover: these are wide weapon renders and cropping them
     square would cut the barrel off every rifle. */
  object-fit: contain;
}

.signal__body { min-width: 0; flex: 1 1 auto; }

/* ---------- The meter ----------
   Fills what used to be dead space between the text and the figure, and
   earns the room by encoding the two numbers the card already states:
   base against best for arbitrage, now against high for peaks, bid
   against ask for spreads.

   Deliberately NOT a sparkline. No per item price series exists in this
   view, so a sparkline would have to be drawn from invented data. This
   is the real figures made visual. */
.signal__meter {
  margin-top: 14px;
  max-width: 420px;
}

.signal__track {
  height: 4px;
  background: #191919;
  border-radius: 2px;
  overflow: hidden;
}

.signal__fill {
  height: 100%;
  border-radius: 2px;
  background: var(--signal-tone);
  /* The bar reads as a proportion of a whole, so the unfilled part
     matters as much as the filled part. Kept slightly translucent so it
     sits under the figure in the visual hierarchy rather than competing
     with it. */
  opacity: 0.8;
}

.signal__scale {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  margin-top: 7px;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 10.5px;
  letter-spacing: 0.02em;
  color: var(--faint);
}

.signal__scale span:last-child { text-align: right; }

.signal__head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px 14px;
}

/* ---------- Type pill ----------
   Was faint grey text. Now a proper tag carrying its tone, so the
   category is legible at a glance instead of being read. */
.signal__pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 9px 3px;
  border: 1px solid var(--signal-tone);
  border-radius: 3px;
  background: var(--signal-wash);
  font-family: var(--font-display);
  font-stretch: var(--display-label);
  font-weight: 600;
  font-size: 9.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--signal-tone);
  white-space: nowrap;
}

/* The type glyph. Inherits the pill's colour, so it is automatically
   the tone of its own signal. */
.signal__icon {
  flex: 0 0 auto;
  opacity: 0.95;
}

.signal__name {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 15px;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.signal__text {
  margin-top: 9px;
  max-width: 62ch;
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--muted);
}

/* ---------- The figure ----------
   The hero. This is the number somebody scans the section for, so it
   gets the size, the weight and the colour, and everything else on the
   card is arranged around it. */
.signal__figure {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  justify-content: center;
  gap: 3px;
  min-width: 104px;
  text-align: right;
}

.signal__pct {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  font-size: clamp(24px, 2.5vw, 30px);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--signal-tone);
}

.signal__unit {
  font-family: var(--font-body);
  font-size: 9.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--faint);
  white-space: nowrap;
}

/* ---------- The flagship ----------
   Arbitrage names money the user could take today, so it is the loudest
   card in the section: a heavier edge, a permanent low glow rather than
   hover only, a larger figure and a spark on its pill.

   It is deliberately the ONLY card that glows at rest. If two things
   shouted, neither would. */
/* The flagship takes the HERO rung of the depth system, the only
   signal that does. It is money the user could take today, so it gets
   the same structural treatment as the rank strip: raised surface,
   real elevation, and a resting glow in its own tone. */
.signal--flagship {
  position: relative;
  /* The glow lives on ::after and spreads OUTWARD, so it must not be
     clipped to the card box. The wash on ::before now rounds its own
     corners, so lifting the clip here costs nothing. */
  overflow: visible;
  border-left-width: 4px;
  border-color: rgba(31, 214, 125, 0.28);
  border-left-color: var(--gain);
  background:
    linear-gradient(to right, rgba(31, 214, 125, 0.06), rgba(14, 14, 16, 0) 46%),
    linear-gradient(to bottom, var(--surface-hero), var(--surface) 82%);
  box-shadow: var(--edge-lit-hero), var(--elev-2),
              0 0 30px -12px rgba(31, 214, 125, 0.55);
}

/* THE BREATHE. The most restricted motion in the system: a slow glow
   pulse that means "this is live money, right now". It is on an
   ::after overlay rather than the card's own box-shadow so the pulse
   animates opacity only, which the compositor handles on its own
   thread. Animating box-shadow directly would repaint the card every
   frame for the entire time it is on screen.

   6.5s is deliberate. Fast enough to notice peripherally, slow enough
   that it never nags. Only ONE element on the dashboard does this. */
.signal--flagship::after {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  pointer-events: none;
  box-shadow: 0 0 0 1px rgba(31, 214, 125, 0.34),
              0 0 40px -8px rgba(31, 214, 125, 0.45);
  opacity: 0.55;
  animation: ss-breathe 6.5s ease-in-out infinite;
}

.signal--flagship:hover {
  border-color: rgba(31, 214, 125, 0.5);
  box-shadow: var(--edge-lit-hero), var(--elev-3),
              0 0 0 1px rgba(31, 214, 125, 0.32),
              0 0 44px -6px rgba(31, 214, 125, 0.45);
}

/* Hover takes over the glow, so the pulse stands down rather than
   fighting the hover state. */
.signal--flagship:hover::after { animation-play-state: paused; opacity: 0; }

.signal--flagship .signal__pct {
  font-size: clamp(30px, 3.3vw, 40px);
  font-weight: 600;
}

.signal--flagship .signal__pill {
  background: rgba(31, 214, 125, 0.14);
  color: #6ef0b4;
  border-color: rgba(31, 214, 125, 0.55);
}

.signal--flagship .signal__unit { color: var(--muted); }

.signal--flagship .signal__thumb {
  width: 64px;
  height: 64px;
  border-color: rgba(31, 214, 125, 0.3);
}

.signal--flagship .signal__fill { opacity: 1; }
.signal--flagship .signal__meter { max-width: 460px; }

/* ---------- Empty and overflow states ----------
   No signals is a legitimate answer, and it should read as calm rather
   than as a card that failed to load. */
.signal--empty,
.signal--more {
  border-left-color: var(--line);
  background: var(--surface);
}
.signal--empty:hover,
.signal--more:hover {
  transform: none;
  border-color: var(--line);
  border-left-color: var(--line);
  box-shadow: none;
}
.signal--empty .signal__text,
.signal--more .signal__text { margin-top: 0; }

@media (max-width: 700px) {
  .signal {
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 14px;
    padding: 20px 20px 20px 22px;
  }
  /* The thumbnail stays on the left of the first row, and the body
     takes the rest of the width, so the card keeps its shape instead of
     becoming a single tall column. */
  .signal__thumb { width: 44px; height: 44px; }
  .signal__body { flex: 1 1 60%; }
  .signal__meter { max-width: none; }
  /* Stacked, the figure sits under the text and reads left aligned with
     everything else rather than stranded on the right. */
  .signal__figure {
    flex-direction: row;
    align-items: baseline;
    text-align: left;
    min-width: 0;
    gap: 9px;
  }
  .signal__name { white-space: normal; }
}

@media (prefers-reduced-motion: reduce) {
  .signal { transition: none; }
  .signal:hover { transform: none; }
}


/* ---------- Holdings table ---------- */
/* THE CALM SIDE OF THE SYSTEM.

   The table is pure data, so it takes the surface language and
   NOTHING else: no glow at rest or on hover, no lift, no pulse, no
   accent wash. It gets the radius, the lit top edge and a modest
   elevation so it sits on the page as a real surface, and then it
   gets out of the way.

   This restraint is load bearing. The rank strip and the flagship
   signal only read as special because the largest block on the page
   refuses to compete with them. */
.table-scroll {
  overflow-x: auto;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: var(--edge-lit), var(--elev-1);
  -webkit-overflow-scrolling: touch;
}

/* Row hover is defined once, further down with the clickable-row
   rules, so the cursor, focus ring and transition stay together. */

.table {
  width: 100%;
  min-width: 620px;   /* four columns. under 720px rows become cards, css/dashboard.css */
}

.table th {
  padding: 16px 22px;
  border-bottom: 1px solid var(--line);
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--muted);
  text-align: left;
  white-space: nowrap;
}

.table td {
  padding: 18px 22px;
  border-bottom: 1px solid var(--line);
  font-size: 14px;
  white-space: nowrap;
  vertical-align: top;
}

.num, th.num { text-align: right; }
td.num {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  letter-spacing: -0.01em;
}

/* Item cell. Sample rows have no icon, so the flex row collapses to just
   the text block and renders exactly as it did before icons existed. */
.item {
  display: flex;
  align-items: center;
  gap: 14px;
}

.item__icon {
  flex: 0 0 auto;
  display: block;
  width: 34px;
  height: 34px;
  object-fit: contain;
  background: #0a0a0a;
  border: 1px solid var(--line);
}

.item__text { min-width: 0; }

.item__name { color: var(--text); font-weight: 400; }
.item__wear {
  display: block;
  margin-top: 5px;
  font-size: 11px;
  letter-spacing: 0.06em;
  color: var(--faint);
}

/* Float is unavailable for live items. Steam's inventory endpoint does not
   carry it, so the cell says so rather than inventing a number. */
.cell-na { color: var(--faint); }

/* Secondary figure under a mono cell, e.g. the P&L percentage. */
.cell-sub {
  display: block;
  margin-top: 5px;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 11px;
  letter-spacing: 0.02em;
  color: var(--faint);
}

.cell-signal {
  text-align: right;
  font-family: var(--font-display);
  font-stretch: var(--display-label);
  font-weight: 600;
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--accent);
}
.cell-signal--hold { color: var(--muted); }

.table tfoot td {
  padding: 18px 22px;
  border-bottom: 0;
  font-size: 13px;
  white-space: nowrap;
}
.table tfoot .foot-label {
  font-family: var(--font-body);
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--muted);
}

.table-hint {
  display: none;
  margin-top: 14px;
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--faint);
}

/* Standing footnote: which columns are real and which are not. */
.table-note {
  margin-top: 14px;
  font-size: 11px;
  line-height: 1.6;
  letter-spacing: 0.04em;
  color: var(--faint);
}

/* ============================================================
   4. FOOTER

   Moved to css/footer.css, loaded on every page. Only the stacking
   rule stays here, with the other landmarks near the top of this file.
   ============================================================ */

/* Between the phone breakpoint and about 780px all three zones still
   fit side by side, but the text column drops under 200px, the note
   wraps to four lines and the strip stretches to about 240px tall.

   Stacking early is not the answer: a full width card there is taller
   still. Trimming the card's share instead gives the text back enough
   room that it wraps to two lines, and the strip settles. The card is
   only a few percent smaller, which is invisible next to the height it
   saves. */


/* ============================================================
   RESPONSIVE

   Known mobile breakages and how they are handled:

   1. Holdings table. Four columns, min-width 620px. Under 720px each row
      becomes a card (css/dashboard.css), so a phone never scrolls it
      sideways.
   2. Long skin names. td uses white-space: nowrap on purpose so figures
      stay on one line, which means an unusually long StatTrak name widens
      the table further. It scrolls, it does not break, but the scroll gets
      longer. If that bothers you, allow wrapping on the Item column only.
   3. Sticky top bar. 68px is deducted from usable viewport height on iOS
      when the address bar shows. Nothing here relies on 100vh, so it is safe.
   4. Currency dropdown. Anchored right so it never overflows the viewport
      on narrow screens. If you move the switcher left, change right: 0
      to left: 0 or it will clip.
   5. Hero headline. Hard line break at "Know what to do / with your skins."
      At 320px the clamp floor of 38px still fits. Below 300px it overflows
      slightly. overflow-wrap: break-word on body catches it.
   6. Hero form. Flips to stacked at 620px. The button goes full width.
   ============================================================ */

@media (max-width: 1000px) {
  .stats { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 700px) {
  .signal__meta {
    margin-left: 0;
    flex-basis: 100%;
    order: 3;
  }
}

@media (max-width: 620px) {
  :root { --gutter: 20px; }

  .wordmark { font-size: 16px; }
  /* 40px -> 120px wide, which still leaves room for the currency switcher. */
  .wordmark__img { height: 40px; }

  /* The emblem and the label side by side leave the progress bar too
     narrow to read on a phone, so the emblem moves above the text. */
  .rank {
    flex-direction: column;
    align-items: flex-start;
    gap: 18px;
    padding: 20px;
  }

  .rank__ends { font-size: 10px; }

  .stats { grid-template-columns: 1fr; }
  .stat { padding: 22px 20px; }

  .signal { padding: 22px 20px; gap: 14px; }

  .section__head {
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
  }
}

/* Signals panel: empty and overflow states. */
.signal--empty .signal__text,
.signal--more .signal__text {
  margin-top: 0;
  color: var(--faint);
}

/* A stat with no honest figure to show yet. */
.stat__value--muted { color: var(--faint); }

/* ============================================================
   ITEM DETAIL

   A slide-over on desktop, a full sheet on mobile. The chart is
   hand drawn SVG rather than a charting library: no dependency, no
   build step, and full control of the aesthetic. The plot uses a
   non-uniform viewBox so it stretches to any width, which is why the
   axis labels are HTML rather than SVG text (text would distort).
   ============================================================ */

.detail[hidden] { display: none; }

.detail {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: flex;
  justify-content: flex-end;
}

.detail__scrim {
  position: absolute;
  inset: 0;
  background: rgba(5, 5, 5, 0.72);
  opacity: 0;
  transition: opacity 220ms ease;
}
.detail.is-open .detail__scrim { opacity: 1; }

.detail__panel {
  position: relative;
  width: min(560px, 100%);
  height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--bg);
  border-left: 1px solid var(--line);
  /* The panel now reads as a slab lifted off the page rather than as a
     region of it: a hairline of light down the leading edge, and the
     page's own shadow thrown to the left of it. */
  box-shadow:
    inset 1px 0 0 rgba(245, 242, 236, 0.045),
    -24px 0 60px -20px rgba(0, 0, 0, 0.75);
  padding: 26px 28px 40px;
  transform: translate3d(100%, 0, 0);
  transition: transform 320ms cubic-bezier(0.22, 0.61, 0.24, 1);
  will-change: transform;
}

/* ONE GLOW, PINNED TO THE TOP OF THE PANEL AND NOWHERE ELSE.

   It is a child rather than a background on the panel, because the
   panel scrolls: a background would slide up the screen with the
   content and light the wrong thing halfway down. position:sticky at
   the top of the scroller keeps it behind the header, where the art
   and the price are, and out of the way of everything below. It is
   also the reason for the z-index on .detail__panel > * further down;
   without it the glow paints over the header it is meant to sit under. */
.detail__panel::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 260px;
  z-index: 0;
  pointer-events: none;
  /* A soft blue wash behind the item art, gone well before the chart.
     No animation and no sweep: it is depth, not an effect. */
  background: radial-gradient(420px 230px at 18% 0%, rgba(61, 127, 255, 0.085), transparent 70%);
}
.detail__panel > * { position: relative; z-index: 1; }
.detail.is-open .detail__panel { transform: translate3d(0, 0, 0); }

/* ---------- Head ---------- */
.detail__head {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: start;
  gap: 18px;
  padding-bottom: 20px;
  border-bottom: 1px solid var(--line);
}

.detail__id { display: flex; align-items: center; gap: 14px; min-width: 0; }

/* THE ART IS THE HERO NOW. 44px was a favicon beside a title; 66px is
   the thing the panel is about. The frame it sits in is its own rarity,
   read from data-rarity on #detail, which js/app.js sets from exactly
   the field the row's thumbnail plate uses. An item with no recognised
   rarity keeps the plain line border and loses nothing. */
.detail__icon {
  --frame: var(--line);

  flex: 0 0 auto;
  width: 66px;
  height: 66px;
  object-fit: contain;
  padding: 5px;
  background:
    radial-gradient(90% 90% at 50% 100%, color-mix(in srgb, var(--frame) 22%, transparent), transparent 72%),
    #0a0a0a;
  /* FULL STRENGTH, NOT A TINT. This border is the only place in the
     panel that says what rarity the item is, which makes it information
     rather than decoration, and 1.4.11 asks information carried by a
     graphic for 3:1. Mixed down to 42% of the hue it measured 1.7:1
     against the panel. Undiluted, the lowest of the seven is milspec at
     4.4:1 and the rest are higher. */
  border: 1px solid var(--frame);
  border-radius: 3px;
  box-shadow: 0 0 22px -10px var(--frame);
}

.detail[data-rarity="gold"]       .detail__icon { --frame: var(--rar-gold); }
.detail[data-rarity="covert"]     .detail__icon { --frame: var(--rar-covert); }
.detail[data-rarity="classified"] .detail__icon { --frame: var(--rar-classified); }
.detail[data-rarity="restricted"] .detail__icon { --frame: var(--rar-restricted); }
.detail[data-rarity="milspec"]    .detail__icon { --frame: var(--rar-milspec); }
.detail[data-rarity="industrial"] .detail__icon { --frame: var(--rar-industrial); }
.detail[data-rarity="consumer"]   .detail__icon { --frame: var(--rar-consumer); }

.detail__titles { min-width: 0; }

.detail__name {
  font-family: var(--font-display);
  font-stretch: var(--display-header);
  font-weight: 600;
  font-size: 17px;
  line-height: 1.2;
  letter-spacing: -0.01em;
}

.detail__wear {
  margin-top: 5px;
  font-size: 11px;
  letter-spacing: 0.08em;
  color: var(--faint);
}

.detail__price { text-align: right; }

.detail__value {
  margin-top: 8px;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  font-size: 20px;
  letter-spacing: -0.02em;
}

.detail__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  background: transparent;
  border: 1px solid var(--line);
  color: var(--muted);
  cursor: pointer;
  transition: border-color 0.18s ease, color 0.18s ease;
}
.detail__close:hover { border-color: #2c2c2c; color: var(--text); }

/* ---------- Range selector ---------- */
.detail__ranges {
  display: flex;
  gap: 1px;
  margin: 22px 0 18px;
  background: var(--line);
  border: 1px solid var(--line);
  width: fit-content;
}

.range {
  padding: 7px 14px;
  background: var(--bg);
  border: 0;
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  cursor: pointer;
  transition: color 0.15s ease, background-color 0.15s ease;
}
.range:hover { color: var(--text); }
.range.is-active { background: var(--surface); color: var(--accent); }
.range:disabled { color: #3a3a38; cursor: default; }

/* ---------- Chart ---------- */
/* THE BARS AND THE BAND. Both draw the same open/high/low/close; which
   one appears is decided in js/app.js by how many buckets have to fit.
   The close line stays on top of either, so the shape a reader already
   knows is still the shape they see. */
.candle__wick { stroke-width: 1; stroke-linecap: round; vector-effect: non-scaling-stroke; }
.candle__body { stroke: none; }
/* No body to draw, so the wick carries the bucket on its own. The round
   cap is what lets a bucket with no range at all, where the wick is a
   zero length line, still render as a mark rather than as nothing. */
.candle--flat .candle__wick { stroke-width: 2.4; }
.candle--up   .candle__wick { stroke: color-mix(in srgb, var(--gain) 62%, transparent); }
.candle--up   .candle__body { fill: color-mix(in srgb, var(--gain) 46%, transparent); }
.candle--down .candle__wick { stroke: color-mix(in srgb, var(--risk) 62%, transparent); }
.candle--down .candle__body { fill: color-mix(in srgb, var(--risk) 46%, transparent); }

/* Dense ranges: one band from the highs to the lows. Quiet enough to
   sit under the line without competing with it. */
.hlband { fill: rgba(61, 127, 255, 0.13); stroke: none; }

/* ---------- Volume ---------- */
/* Deliberately short and deliberately unlabelled. It answers one
   question, whether anything actually traded, and a y axis on it would
   invite reading absolute depth off a strip 34px tall. The exact count
   is in the tooltip, where it belongs. */
.chart__vol {
  display: block;
  width: 100%;
  height: 34px;
  margin-top: 6px;
  border: 1px solid var(--line);
  border-top: 0;
  background: var(--surface);
}
.chart__vol[hidden] { display: none; }

.chart__volcap {
  margin-top: 5px;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 10px;
  letter-spacing: 0.04em;
  color: var(--faint);
}
.chart__volcap[hidden] { display: none; }

.vbar--up   { fill: color-mix(in srgb, var(--gain) 34%, transparent); }
.vbar--down { fill: color-mix(in srgb, var(--risk) 34%, transparent); }

.chart {
  position: relative;
  height: 260px;
  padding: 10px 52px 26px 0;
  background: var(--surface);
  border: 1px solid var(--line);
}

.chart__svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.chart__yaxis, .chart__xaxis { position: absolute; pointer-events: none; }

.chart__yaxis { top: 10px; right: 8px; bottom: 26px; width: 44px; }

.chart__ylabel {
  position: absolute;
  right: 0;
  transform: translateY(-50%);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 10px;
  color: var(--faint);
  white-space: nowrap;
}

.chart__xaxis { left: 0; right: 52px; bottom: 6px; height: 16px; }

.chart__xlabel {
  position: absolute;
  transform: translateX(-50%);
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--faint);
  white-space: nowrap;
}
.chart__xlabel:first-child { transform: none; }
.chart__xlabel:last-child { transform: translateX(-100%); }

/* The latest print, always marked. */
.chart__dot {
  position: absolute;
  width: 7px;
  height: 7px;
  margin: -3.5px 0 0 -3.5px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 3px rgba(61, 127, 255, 0.18);
  pointer-events: none;
}

.chart__tip {
  position: absolute;
  z-index: 3;
  padding: 7px 10px;
  background: #101010;
  border: 1px solid var(--line);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 11px;
  line-height: 1.5;
  color: var(--text);
  white-space: nowrap;
  pointer-events: none;
  transform: translate(-50%, -120%);
}
.chart__tip .sub { display: block; color: var(--faint); font-size: 10px; }

/* A ceiling as well as the pixel clamp in js/app.js: at 320px the chart
   is about 250px wide, and a tooltip that cannot be centred inside its
   host cannot be clamped into it either. */
.chart__tip { max-width: min(190px, 86%); white-space: normal; }

/* OPEN, HIGH, LOW, CLOSE, in two columns under the headline price.
   Every one of these was in the response from the first day and none
   of them was ever shown. */
.chart__tip .tipgrid {
  display: grid;
  grid-template-columns: auto auto;
  gap: 1px 8px;
  margin: 5px 0 4px;
  padding-top: 4px;
  border-top: 1px solid var(--line-warm, #24242a);
  font-size: 10px;
}
.chart__tip .tipk { color: var(--faint); letter-spacing: 0.1em; }
.chart__tip .tipv { text-align: right; color: var(--text); }

.chart__state {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 24px;
  text-align: center;
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--faint);
  background: var(--surface);
}
.chart__state[hidden] { display: none; }

.detail__coverage {
  margin-top: 10px;
  font-size: 11px;
  line-height: 1.6;
  letter-spacing: 0.03em;
  color: var(--faint);
  min-height: 16px;
}

/* ---------- Stats ---------- */
.detail__section { margin-top: 30px; }
.detail__section > .eyebrow { margin-bottom: 14px; }

.detail__stats {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
}

.dstat { background: var(--surface); padding: 14px 16px; }

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

.dstat__value {
  margin-top: 8px;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 15px;
  letter-spacing: -0.01em;
}
.dstat__value--muted { color: var(--faint); }

.dstat__note {
  margin-top: 5px;
  font-size: 10px;
  letter-spacing: 0.04em;
  color: var(--faint);
}

/* ============================================================
   THE OWNERSHIP SECTIONS

   Three blocks that only appear for an item the reader actually holds:
   what it has earned, where it sits in their book, and what the six
   marketplaces are quoting for it. A market mover gets none of them,
   because it is not theirs.
   ============================================================ */

/* ---------- The badges, in full ---------- */
.dbadges { display: grid; gap: 8px; }

.dbadge {
  --bc: var(--muted);

  display: grid;
  grid-template-columns: 34px minmax(0, 1fr);
  align-items: start;
  gap: 12px;
  padding: 11px 13px;
  background:
    linear-gradient(120deg, color-mix(in srgb, var(--bc) 7%, transparent), transparent 46%),
    var(--surface);
  border: 1px solid var(--line);
  border-left: 2px solid color-mix(in srgb, var(--bc) 62%, transparent);
  border-radius: 3px;
}

/* THE EMBLEM CARRIES THE GLOW, not the text. A rarity tinted digit
   fails AA against this surface at every one of these hues, which is
   why the row's badges put the colour on the border and kept the
   letters near white. The same split holds here: the emblem is a
   graphic and needs 3:1, the name and the sentence under it are text
   and are left at the page's own foreground colours. */
.dbadge__emblem {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  /* THE ICON IS THE GRAPHIC, NOT THE BOX. color is the undiluted badge
     hue and the stroked emblem drawn in it clears 3:1 on every one of
     the nine; the border and the wash behind it are the plate it sits
     on and are deliberately left as a tint, because taking them to full
     strength to satisfy a rule they are not the subject of would turn
     nine small emblems into nine coloured blocks. */
  border: 1px solid color-mix(in srgb, var(--bc) 52%, transparent);
  border-radius: 3px;
  background: color-mix(in srgb, var(--bc) 13%, transparent);
  box-shadow: 0 0 18px -6px var(--bc);
  color: var(--bc);
}

.dbadge__name {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text);
}

.dbadge__why {
  margin-top: 4px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--muted);
}

/* The number that earned it, in the mono face the rest of the page
   uses for anything measured. */
.dbadge__fact {
  margin-top: 5px;
  font-size: 10.5px;
  letter-spacing: 0.03em;
  color: var(--faint);
}

.dbadge--bag     { --bc: var(--rar-gold); }
.dbadge--grail   { --bc: var(--rar-gold); }
.dbadge--ath     { --bc: var(--rar-gold); }
.dbadge--allin   { --bc: var(--rar-classified); }
.dbadge--cooking { --bc: #57d38c; }
.dbadge--rugged  { --bc: #f26569; }
.dbadge--clean   { --bc: var(--rar-milspec); }
.dbadge--warvet  { --bc: var(--r-legendary); }
.dbadge--dust    { --bc: var(--muted); }

/* ---------- What it did not earn ---------- */
.dchase { margin-top: 14px; }
.dchase:empty { display: none; }

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

.dchase__list { margin-top: 8px; display: grid; gap: 5px; }

.dchase__item {
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--faint);
}
.dchase__item b {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  margin-right: 6px;
}

/* ---------- The float meter ---------- */
/* Spans both stat columns because it is a scale, not a figure, and a
   scale squeezed into half the panel stops being readable. */
.dstat--wide { grid-column: 1 / -1; }

.dfloat {
  position: relative;
  display: block;
  height: 6px;
  margin: 10px 0 8px;
  background: linear-gradient(90deg, #1a1c22, #2a2d36);
  border-radius: 3px;
}
.dfloat i {
  position: absolute;
  top: -3px;
  width: 2px;
  height: 12px;
  margin-left: -1px;
  background: var(--text);
  border-radius: 1px;
  box-shadow: 0 0 10px -2px var(--text);
}

.dstat--wide .dstat__note {
  display: flex;
  justify-content: space-between;
  gap: 10px;
}

/* ---------- Across markets ---------- */
.dmarkets {
  display: grid;
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
}

.dmkt {
  display: grid;
  grid-template-columns: minmax(0, 132px) minmax(0, 1fr) 82px;
  align-items: center;
  gap: 4px 12px;
  padding: 11px 14px;
  background: var(--surface);
}

.dmkt__name {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
}

.dmkt__tag {
  display: block;
  margin-top: 3px;
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 0.12em;
  color: var(--gain);
  text-transform: uppercase;
}

.dmkt__bar {
  position: relative;
  display: block;
  height: 5px;
  background: #16171c;
  border-radius: 3px;
  overflow: hidden;
}
.dmkt__bar i {
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--w, 0%);
  background: linear-gradient(90deg, rgba(61, 127, 255, 0.35), var(--accent));
  border-radius: 3px;
}

.dmkt__ask {
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  text-align: right;
  color: var(--text);
}

/* The note runs under the name and the bar, never under the figure,
   so the prices stay in one clean column down the right. */
.dmkt__note {
  grid-column: 1 / 3;
  font-size: 10px;
  letter-spacing: 0.03em;
  color: var(--faint);
}

.dmkt.is-cheapest .dmkt__bar i {
  background: linear-gradient(90deg, color-mix(in srgb, var(--gain) 35%, transparent), var(--gain));
}

/* A quote the price guard threw out is still shown, because hiding it
   would make the disagreement that triggered it invisible. It is held
   back rather than dressed up. */
.dmkt.is-outlier { opacity: 0.55; }
.dmkt.is-outlier .dmkt__bar i { background: #3a3a40; }

/* ============================================================
   THE PANEL'S MOTION

   All of it lives inside no-preference, which is why the reduce rule
   below needs no amendment: there is nothing for it to turn off.

   KEYFRAMES ARE SAFE HERE, unlike in the dashboard's own board. These
   three sections are written once, when the panel opens, and renderAll
   never touches them, so an animation cannot be replayed by a price
   batch landing mid-read. The chart is the one thing that redraws, and
   only when the reader presses a range.
   ============================================================ */
@media (prefers-reduced-motion: no-preference) {
  @keyframes dpop {
    from { opacity: 0; transform: translateY(7px); }
    to   { opacity: 1; transform: none; }
  }

  .dbadge,
  .dmkt { animation: dpop 320ms cubic-bezier(0.22, 0.61, 0.24, 1) var(--d, 0s) both; }

  .dbadge__emblem { transition: box-shadow 180ms ease, border-color 180ms ease; }
  .dbadge:hover .dbadge__emblem {
    border-color: color-mix(in srgb, var(--bc) 62%, transparent);
    box-shadow: 0 0 22px -4px var(--bc);
  }
}

/* Rows open the detail view, so they need to look interactive. */
/* Row hover is a background change only. Deliberately not a lift and
   not a glow: rows are scanned in bulk, and anything that moves under
   a travelling cursor makes a long list feel unstable. This is the
   calm half of the system doing its job. */
.table tbody tr { cursor: pointer; transition: background-color var(--dur-swift) var(--ease); }
.table tbody tr:hover { background: var(--hover); }
.table tbody tr:focus-visible { outline: 1px solid var(--accent); outline-offset: -1px; }

@media (max-width: 620px) {
  .detail__panel {
    width: 100%;
    border-left: 0;
    padding: 20px 20px 36px;
  }
  .detail__head { grid-template-columns: 1fr auto; gap: 12px; }
  /* Keep the close control beside the title rather than letting the
     full width price row push it down to its own line. */
  .detail__close { grid-row: 1; grid-column: 2; }
  .detail__price { grid-row: 2; grid-column: 1 / -1; text-align: left; }
  .detail__value { font-size: 18px; }
  .chart { height: 220px; padding-right: 46px; }
  .chart__yaxis { width: 40px; }
  .detail__stats { grid-template-columns: 1fr; }
  .detail__icon { width: 56px; height: 56px; }
  /* The ask column is the one thing that must not wrap, so the bar
     gives up its width first and then disappears entirely. */
  .dmkt { grid-template-columns: minmax(0, 1fr) 78px; }
  .dmkt__bar { display: none; }
  .dmkt__note { grid-column: 1 / -1; }
}

@media (prefers-reduced-motion: reduce) {
  .detail__panel, .detail__scrim { transition: none; }
}

/* Page behind the detail panel must not scroll with it. */
body.detail-open { overflow: hidden; }

/* ============================================================
   9. UNLOCK CASCADE
   The popup js/unlock.js plays when a rank analyse returns newly
   unlocked achievements. Lives here rather than achievements.css
   because it fires on the dashboard, not on the collection page.

   One card on screen at a time, bottom right, out of the way of the
   dashboard figures. Rarity changes the shimmer and the hold, nothing
   structural, so a legendary reads bigger without a second layout.
   ============================================================ */
.unlock {
  position: fixed;
  right: var(--gutter);
  bottom: var(--gutter);
  z-index: 95;
  width: min(380px, calc(100vw - var(--gutter) * 2));
  pointer-events: none;
}

.unlock__card {
  pointer-events: auto;
  background: var(--surface);
  border: 1px solid var(--line);
  cursor: pointer;
  opacity: 0;
  transform: translateY(14px);
}

.unlock__card.is-in {
  animation: unlockIn 0.62s cubic-bezier(0.16, 0.84, 0.32, 1) forwards;
}
.unlock__card.is-out {
  animation: unlockOut 0.34s ease forwards;
}
.unlock__card.is-static { opacity: 1; transform: none; }

@keyframes unlockIn {
  0%   { opacity: 0; transform: translateY(14px) scale(0.985); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes unlockOut {
  0%   { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateY(-8px); }
}

.unlock__art {
  position: relative;
  width: 100%;
  aspect-ratio: 3 / 1;
  background: #0b0b0b;
  overflow: hidden;
}
.unlock__art img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The sweep. A single pass for common, and it keeps going, brighter and
   wider, the rarer the card gets. */
.unlock__shimmer {
  position: absolute;
  top: 0;
  left: -60%;
  width: 55%;
  height: 100%;
  background: linear-gradient(
    100deg,
    transparent 0%,
    rgba(245, 242, 236, 0.06) 40%,
    rgba(245, 242, 236, 0.16) 50%,
    rgba(245, 242, 236, 0.06) 60%,
    transparent 100%
  );
  transform: skewX(-14deg);
}
.is-in .unlock__shimmer { animation: unlockShimmer 1.5s ease-in-out 0.35s 1; }

@keyframes unlockShimmer {
  from { left: -60%; }
  to   { left: 130%; }
}

.unlock__card--rare .unlock__shimmer {
  background: linear-gradient(
    100deg, transparent 0%,
    rgba(61, 127, 255, 0.10) 40%,
    rgba(245, 242, 236, 0.22) 50%,
    rgba(61, 127, 255, 0.10) 60%,
    transparent 100%);
}
.is-in .unlock__card--rare .unlock__shimmer,
.unlock__card--rare.is-in .unlock__shimmer { animation-iteration-count: 2; }

.unlock__card--epic .unlock__shimmer {
  width: 70%;
  background: linear-gradient(
    100deg, transparent 0%,
    rgba(124, 92, 214, 0.14) 40%,
    rgba(245, 242, 236, 0.28) 50%,
    rgba(124, 92, 214, 0.14) 60%,
    transparent 100%);
}
.unlock__card--epic.is-in .unlock__shimmer { animation-iteration-count: 3; }

.unlock__card--legendary .unlock__shimmer {
  width: 80%;
  background: linear-gradient(
    100deg, transparent 0%,
    rgba(212, 163, 83, 0.18) 40%,
    rgba(255, 245, 225, 0.34) 50%,
    rgba(212, 163, 83, 0.18) 60%,
    transparent 100%);
}
.unlock__card--legendary.is-in .unlock__shimmer {
  animation-iteration-count: 4;
  animation-duration: 1.3s;
}

/* Rarity reaches the frame too, so the card is keyed before the label
   is read. */
.unlock__card--rare      { border-color: rgba(61, 127, 255, 0.45); }
.unlock__card--epic      { border-color: rgba(124, 92, 214, 0.45); }
.unlock__card--legendary { border-color: rgba(212, 163, 83, 0.5); }

.unlock__body {
  padding: 13px 16px 15px;
  border-top: 1px solid var(--line);
}

.unlock__eyebrow {
  font-family: var(--font-body);
  font-size: 9px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--accent);
}

.unlock__name {
  margin-top: 6px;
  font-family: var(--font-display);
  font-stretch: var(--display-header);
  font-weight: 600;
  font-size: 19px;
  line-height: 1.1;
  letter-spacing: -0.01em;
  color: var(--text);
}

.unlock__rarity {
  margin-top: 5px;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--faint);
}
.unlock__card--rare      .unlock__rarity { color: #3d7fff; }
.unlock__card--epic      .unlock__rarity { color: #7c5cd6; }
.unlock__card--legendary .unlock__rarity { color: #d4a353; }

@media (max-width: 620px) {
  .unlock { right: 12px; bottom: 12px; width: calc(100vw - 24px); }
  .unlock__name { font-size: 17px; }
}

@media (prefers-reduced-motion: reduce) {
  .unlock__card,
  .unlock__shimmer { animation: none !important; }
  .unlock__card { opacity: 1; transform: none; }
  .unlock__shimmer { display: none; }
}


/* ============================================================
   SYSTEM LAYER
   Motion vocabulary and shared component behaviour.

   Everything above this line is a specific component. Everything
   below is the KIT: the named motions and the surface/hover
   behaviour that any future page composes from. New pages should
   reach for these rather than inventing a second set.
   ============================================================ */

/* ---------- Named motions ----------

   Five, and each has one job. They are named for what they express,
   not for what they do to the transform matrix, because the point of
   a vocabulary is that "rise" means the same arrival everywhere.

     rise    an element arriving. Up and in.
     settle  a hero arriving. Further, slower, with a whisper of scale
             so it reads as coming toward the viewer rather than
             sliding. Reserved for the rank strip and value hero.
     sweep   a light pass across a surface. Used once, on the rank
             strip, to sell it as a physical panel.
     breathe a slow glow pulse. THE most restricted motion here: it
             says "this is live". Only the flagship signal and the
             live badge may use it.
     draw    a progress fill growing from zero.
*/

@keyframes ss-rise {
  from { opacity: 0; transform: translate3d(0, 14px, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

@keyframes ss-settle {
  from { opacity: 0; transform: translate3d(0, 22px, 0) scale(0.985); }
  to   { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
}

@keyframes ss-sweep {
  from { transform: translate3d(-120%, 0, 0); }
  to   { transform: translate3d(220%, 0, 0); }
}

@keyframes ss-breathe {
  0%, 100% { opacity: 0.55; }
  50%      { opacity: 1; }
}

@keyframes ss-draw {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

/* ---------- Reveal orchestration ----------

   js/motion.js adds .is-revealing exactly once per container, the
   first time it gains children, then removes it. It never re-runs, so
   a re-render during price streaming cannot restart an entrance.

   The base state is VISIBLE. The animation runs FROM hidden, so if
   the JS never loads, every element is simply on screen with no
   motion. Motion is additive here, never load-bearing. */
.is-revealing > * {
  animation: ss-rise var(--dur-reveal) var(--ease) both;
  /* Two terms: where this container sits in the assemble (--enter, set
     by motion.js) plus this child's own place in its stagger. One
     expression, so the sequencing is readable in one line. */
  animation-delay: calc(var(--enter, 0ms) + (var(--i, 0) * var(--stagger)));
}

/* THE HERO ARRIVES FIRST, AND AS ONE OBJECT.

   The settle is on the hero card itself, not its children: the
   emblem, rank name and value should assemble as a single panel
   coming toward the viewer, not as three things sliding in beside
   each other. So the child stagger is explicitly cancelled here.

   ss-settle rather than ss-rise: further, slower, with a whisper of
   scale. It is the only element that gets it. */
.rank.is-revealing {
  animation: ss-settle var(--dur-epic) var(--ease) both;
}
.rank.is-revealing > * { animation: none; }

/* Sections fade up as they enter the viewport, once. */
.reveal-section {
  animation: ss-rise var(--dur-reveal) var(--ease) both;
}

/* ---------- Shared surface behaviour ----------

   The interactive-card contract, in one place. Anything that is a
   raised surface the user can act on gets this: lift, elevation
   climb, edge warms. Non-interactive surfaces (the table) get the
   surface language without the hover, because a lift on something you
   cannot click is a lie about affordance. */
/* Transition is shared by all three. Resting box-shadow is NOT: .rank
   sets its own hero elevation earlier in this file, and listing it
   here too would silently override that, because this block is later
   in the cascade at equal specificity. Base shadows therefore go only
   to the rungs that actually want the default. */
.stat,
.signal,
.rank {
  transition:
    transform    var(--dur-lift) var(--ease),
    box-shadow   var(--dur-lift) var(--ease),
    border-color var(--dur-swift) var(--ease),
    background-color var(--dur-swift) var(--ease);
}

.stat,
.signal {
  box-shadow: var(--edge-lit), var(--elev-1);
}

/* ---------- Reduced motion ----------

   The global rule near the top of this file already collapses every
   duration. This block goes further and removes the things that would
   still read as motion at 0.01ms, or that would leave an element
   mid-animation: the infinite pulses, the sweep, and the entrance
   transforms.

   Crucially it also forces opacity and transform back to their
   resting values, so a user with reduced motion sees the finished
   interface rather than whatever frame the animation started on. */
@media (prefers-reduced-motion: reduce) {
  .is-revealing > *,
  .rank.is-revealing,
  .reveal-section {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
  .rank::after,
  .signal--flagship::after,
  .badge--live::before {
    animation: none !important;
    display: none !important;
  }
  .stat:hover,
  .signal:hover,
  .rank:hover { transform: none !important; }
}


/* ---------- Emblem presence ----------

   Three things make the artwork feel featured rather than placed:
   an aura behind it, a slow float, and the existing shine sweep.

   The aura is on a pseudo-element BEHIND the art (z-index -1 within
   the emblem's own stacking context) so it lights the space around
   the emblem without washing over the illustration itself. Emblems
   are detailed and high contrast; a glow painted on top of one would
   just look like fog. */
.rank__emblem::before {
  content: "";
  position: absolute;
  inset: -22%;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(circle at center,
    rgba(61, 127, 255, 0.28) 0%,
    rgba(61, 127, 255, 0.09) 38%,
    transparent 68%);
  /* Breathes with the float rather than sitting still behind a moving
     object, which would read as the art sliding across its own glow. */
  animation: ss-breathe 7s ease-in-out infinite;
}

/* THE FLOAT. Six seconds, four pixels, and that is the entire effect.

   Deliberately tiny. The emblem is the most detailed thing on the
   page and the eye tracks it, so anything larger stops reading as
   "alive" and starts reading as "drifting". Paired with the aura's
   breathe on a different period (7s vs 6s) so the two never lock into
   an obvious single beat. */
.rank__emblem {
  animation: ss-float 6s ease-in-out infinite;
}

@keyframes ss-float {
  0%, 100% { transform: translate3d(0, 0, 0); }
  50%      { transform: translate3d(0, -4px, 0); }
}

/* ---------- Identity text ---------- */
.rank__eyebrow { margin-bottom: 12px; }

/* The rank name is the headline of the whole dashboard, so it takes
   the display face at its widest rung and hero scale. */
.rank__label {
  font-family: var(--font-display);
  font-stretch: var(--display-wide);
  font-weight: 700;
  font-size: clamp(38px, 5.2vw, 68px);
  line-height: 0.98;
  letter-spacing: -0.02em;
  color: var(--text);
  text-shadow: 0 0 44px rgba(var(--glow-accent), 0.3);
}

.rank__progress {
  margin-top: clamp(20px, 2vw, 30px);
  max-width: 420px;
}

/* ---------- The value ----------

   The portfolio total, living in the hero as half of the identity
   statement. Right-aligned against the rank name so the two read as a
   matched pair across the stage.

   This is the ONE figure on the dashboard that glows. Every other
   number, including the three change cards directly below and the
   whole holdings table, stays flat: that contrast is what makes this
   one read as the headline figure rather than as decoration. */
.rank__value {
  justify-self: end;
  text-align: right;
  min-width: 0;
}

.rank__figure {
  margin-top: 10px;
  font-family: var(--font-mono);
  /* Tabular figures reserve equal advance widths, which is what stops
     the count-up reflowing the hero on every frame. */
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  font-size: clamp(28px, 3.6vw, 50px);
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--text);
  text-shadow: 0 0 34px rgba(var(--glow-accent), 0.42);
}

.rank__figurenote {
  margin-top: 10px;
  font-family: var(--font-mono);
  font-size: 11.5px;
  letter-spacing: 0.04em;
  color: var(--muted);
}



@media (prefers-reduced-motion: reduce) {
  .rank__emblem,
  .rank__emblem::before { animation: none !important; }
  .rank.is-revealing .rank__sheen::after { animation: none !important; }
}

/* ============================================================
   THE VALUE HISTORY SLOT
   The hero used to draw a hand-tuned rising curve here, the same shape
   for everybody. It was decoration that read as data, so it is gone,
   along with its draw, drift and breathe loops. The real value chart now
   lives in the progression block, not on the banner, so a member with a
   card equipped still sees it: css/dashboard.css, .vchart.
   ============================================================ */

/* ============================================================
   THE PLAYER CARD
   Call-of-Duty style: the equipped calling card IS the hero, and the
   identity sits on top of it.

   THE ART IS THE POINT, SO THE ART IS NOT TOUCHED.
   No tint, no overlay, no gradient across the card, no filter. It
   renders exactly as bright as it does in the gallery, because the
   whole reason somebody equipped it is to show it off. The only
   darkening anywhere is one localised pool in the upper left, sized
   to sit under the emblem and the name and nothing else.

   NOTHING IS EVER CROPPED.
   The banner takes its aspect ratio from the equipped card's own
   pixels (--card-ratio, set by applyEquippedCard from
   naturalWidth/naturalHeight), so the art fits edge to edge with no
   crop and no letterbox. This is NOT hardcoded to 3:1 on purpose:
   card-day-one.png is 2:1 and several commons are fractionally off
   3.000, and a fixed ratio would quietly cut those.

   object-fit: contain is belt and braces on top of that. With a
   matching ratio it does nothing; if a ratio ever fails to arrive,
   the card letterboxes rather than crops. Losing art is the one
   outcome that is not allowed.
   ============================================================ */

.rank {
  position: relative;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
.rank:hover { transform: none; box-shadow: none; }

.rank__banner {
  position: relative;
  /* Falls back to 3:1, the ratio 82 of the 83 cards use, for the
     unequipped state and for the moment before the image resolves. */
  aspect-ratio: var(--card-ratio, 3 / 1);
  width: 100%;
  overflow: hidden;
  border-radius: 16px;
  border: 1px solid var(--line-warm);
  background: linear-gradient(to bottom, var(--surface-hero), var(--surface) 82%);
  box-shadow: var(--edge-lit-hero), var(--elev-3), var(--glow-hero);
  transition: box-shadow var(--dur-lift) var(--ease),
              border-color var(--dur-swift) var(--ease);
}

.rank__banner:hover {
  border-color: #33333d;
  box-shadow: var(--edge-lit-hero), var(--elev-3),
              0 0 0 1px rgba(var(--glow-accent), 0.34),
              0 0 60px -10px rgba(var(--glow-accent), 0.66),
              0 0 140px -34px rgba(var(--glow-accent), 0.5);
}

/* The card art. Full bleed, untouched. */
.rank__card {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;     /* never crop. see the header. */
  object-position: center;
}
.rank__card[hidden] { display: none; }

/* ---------- The readability pool ----------

   ONE localised darkening, upper left, and nothing else anywhere on
   the card.

   The geometry is doing careful work. Every card carries its own
   title baked into the art, and on this set that title sits in the
   LOWER left, roughly 50-95% of the height and reaching well toward
   the middle ("ALL IN", "MADE IT"). So the pool is anchored at the
   top left corner and given a vertical radius that has it gone by
   around 58% of the height: under the emblem and the name, above the
   card's own title, which stays at full brightness.

   Two stacked ellipses rather than one: a tighter, denser core where
   the text actually sits, and a wider, very soft one that stops the
   core showing an edge against busy art. */
.rank__scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background:
    radial-gradient(ellipse 38% 40% at 2% 2%,
      rgba(5, 5, 5, 0.93) 0%,
      rgba(5, 5, 5, 0.78) 38%,
      rgba(5, 5, 5, 0.36) 68%,
      transparent 88%),
    radial-gradient(ellipse 60% 50% at 0% 0%,
      rgba(5, 5, 5, 0.62) 0%,
      rgba(5, 5, 5, 0.3) 44%,
      transparent 74%);
}

/* The atmospheric fallback. Visible only when nothing is equipped. */
.rank--equipped .rank__aura { display: none; }

.rank__aura { z-index: 0; }

.rank__sheen { z-index: 3; }

/* ---------- Identity, on the art ---------- */
.rank__stage {
  position: absolute;
  z-index: 4;
  top: 0;
  left: 0;
  /* Held inside the upper-left pool. Percentages, not pixels, so the
     identity stays registered to the scrim at every banner width. */
  width: min(62%, 620px);
  /* HEIGHT CAPPED AT HALF THE BANNER. This is the rule that keeps the
     identity off the card's own baked-in title, which on this set
     lives in the lower left from roughly 50% of the height down. A
     percentage rather than pixels, so it holds at every banner size
     and for the 2:1 card as well as the 3:1 ones. */
  height: 50%;
  padding: clamp(14px, 2.4%, 30px) clamp(16px, 2.4%, 30px);
  display: flex;
  align-items: center;
  gap: clamp(12px, 1.8vw, 26px);
}

/* Sized off the STAGE, which is itself half the banner, so the emblem
   is always a fixed share of the card's height and can never grow
   down into the card's title. Width derives from the square ratio, so
   there is one number to reason about instead of two.

   The cap stops it becoming absurd on an ultrawide monitor where half
   the banner height is already large. */
.rank__emblem {
  height: min(94%, 150px);
  width: auto;
  aspect-ratio: 1 / 1;
  flex: 0 0 auto;
}

.rank__ident { min-width: 0; }

/* The account name. The player identity line, above the rank, in the
   display face. Text shadow rather than a heavier scrim: it buys
   legibility over whatever the art is doing without darkening another
   pixel of the card. */
.rank__player {
  font-family: var(--font-display);
  font-stretch: var(--display-header);
  font-weight: 600;
  font-size: clamp(13px, 1.5vw, 19px);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: rgba(245, 242, 236, 0.86);
  line-height: 1.1;
  margin-bottom: 0.35em;
  text-shadow: 0 1px 12px rgba(0, 0, 0, 0.9), 0 0 3px rgba(0, 0, 0, 0.8);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rank__player[hidden] { display: none; }

.rank__label {
  font-family: var(--font-display);
  font-stretch: var(--display-wide);
  font-weight: 700;
  font-size: clamp(26px, 3.6vw, 52px);
  line-height: 0.98;
  letter-spacing: -0.02em;
  color: var(--text);
  /* Two shadows: a tight one for edge contrast against bright art, a
     wide soft one for the lit feel. */
  text-shadow: 0 2px 20px rgba(0, 0, 0, 0.95),
               0 0 42px rgba(var(--glow-accent), 0.32);
}



@media (max-width: 720px) {
  /* The identity has to give up the pool's width before the rank name
     starts colliding with the card's own title. */
  .rank__stage { width: 76%; gap: 12px; }
  .rank__meta { flex-direction: column; align-items: flex-start; }
  .rank__value { text-align: left; }
}

/* ============================================================
   THE PROGRESSION BLOCK
   Under the player card: value, ladder position, and the carrot.

   This replaces four loose lines of mono text that read like debug
   output. Same information, composed as one surface, with a clear
   hierarchy: the value and the gap figure are the two things you are
   meant to read at a glance, and everything else supports them.
   ============================================================ */

.progression {
  margin-top: clamp(18px, 2vw, 26px);
  padding: clamp(20px, 2.4vw, 32px) clamp(20px, 2.6vw, 36px) clamp(18px, 2.2vw, 28px);
  background:
    radial-gradient(120% 180% at 50% 120%, rgba(61, 127, 255, 0.07), transparent 62%),
    linear-gradient(to bottom, var(--surface), var(--surface-sunken));
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow: var(--edge-lit), var(--elev-1);
}

/* ---------- Value ---------- */
.progression__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
  padding-bottom: clamp(16px, 1.8vw, 24px);
  margin-bottom: clamp(18px, 2vw, 26px);
  border-bottom: 1px solid var(--line);
}

.progression__figure {
  margin-top: 8px;
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  font-size: clamp(30px, 3.8vw, 52px);
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--text);
  /* The one glowing figure on the dashboard, unchanged from before. */
  text-shadow: 0 0 34px rgba(var(--glow-accent), 0.42);
}

.progression__basis {
  font-size: 11.5px;
  letter-spacing: 0.04em;
  color: var(--muted);
}

/* ---------- The rail ---------- */
.progression__rail {
  display: flex;
  align-items: center;
  gap: clamp(14px, 2vw, 28px);
}

.progression__node {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}
.progression__node[hidden] { display: none; }

.progression__nodetext { display: block; min-width: 0; text-align: right; }

.progression__nodelabel {
  display: block;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 9.5px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--faint);
  margin-bottom: 4px;
}

.progression__nodename {
  display: block;
  font-family: var(--font-display);
  font-stretch: var(--display-header);
  font-weight: 600;
  font-size: clamp(14px, 1.5vw, 18px);
  letter-spacing: -0.005em;
  color: var(--text);
  white-space: nowrap;
}

/* The next rank reads dimmer than the current one: it is not yours
   yet. The emblem beside it is what makes it desirable. */
.progression__node--next .progression__nodename { color: var(--muted); }

.progression__nodeart {
  flex: 0 0 auto;
  position: relative;
  width: clamp(38px, 4.4vw, 54px);
  height: clamp(38px, 4.4vw, 54px);
  display: grid;
  place-items: center;
}

/* A soft accent bloom behind the goal emblem. Marks it as the thing
   you are climbing toward without brightening the art itself. */
.progression__nodeart::before {
  content: "";
  position: absolute;
  inset: -26%;
  border-radius: 50%;
  background: radial-gradient(circle at center,
    rgba(61, 127, 255, 0.3) 0%,
    rgba(61, 127, 255, 0.08) 44%,
    transparent 70%);
}

.progression__nodeimg {
  position: relative;
  width: 100%;
  height: 100%;
  object-fit: contain;
  /* Slightly held back, so the goal reads as not-yet-earned beside the
     full-brightness emblem on the card above. */
  opacity: 0.82;
}

/* ============================================================
   THE BAR
   The dopamine moment. Everything here exists to make a number
   climbing feel like something.
   ============================================================ */

.progression__barwrap {
  flex: 1 1 auto;
  min-width: 0;
  /* Soft glow bleeding off the bar. On the wrapper, not the bar,
     because the bar has to clip its own overflow to keep the fill and
     shimmer inside the radius. */
  filter: drop-shadow(0 0 14px rgba(61, 127, 255, 0.22));
}

.progression__bar {
  position: relative;
  height: 14px;
  border-radius: 999px;
  background: var(--surface-sunken);
  border: 1px solid #1f1f24;
  overflow: hidden;
  /* inline-size containment turns 100cqw into "the width of this bar",
     which is what lets the hot tip ride the end of the fill with a
     transform instead of a layout-triggering left or width. */
  container-type: inline-size;
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.75);
}

.progression__fill {
  position: absolute;
  inset: 0;
  transform-origin: left center;
  /* --p is set by renderRank, 0..1. scaleX rather than width: this is
     the property that animates on load, and scaling is compositor
     work where width would relayout the bar every frame. */
  transform: scaleX(var(--p, 0));
  background: linear-gradient(to right, #2a5fd0 0%, #3d7fff 62%, #63a4ff 100%);
  border-radius: 999px;
  transition: transform var(--dur-epic) var(--ease);
}

/* ---------- The shimmer ----------
   A band of light travelling along the filled portion, so the bar
   reads as energised rather than painted. The fill clips it, so it
   only ever lights the part you have earned. */
.progression__shimmer {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 42%;
  border-radius: inherit;
  background: linear-gradient(
    100deg,
    transparent 0%,
    rgba(255, 255, 255, 0.26) 46%,
    rgba(210, 240, 255, 0.46) 52%,
    transparent 100%
  );
  transform: translateX(-140%);
  animation: ss-shimmer 3.4s var(--ease) infinite;
}

@keyframes ss-shimmer {
  0%   { transform: translateX(-140%); }
  /* The long tail is what keeps this from reading as a loading bar:
     the light passes, then the bar rests before the next one. */
  55%  { transform: translateX(360%); }
  100% { transform: translateX(360%); }
}

/* ---------- The hot tip ----------
   The bright charging head at the leading edge. Cyan-white against the
   accent blue, so it reads hotter than the fill behind it.

   Rides on translateX driven by the same --p, in container width
   units, so it stays pinned to the end of the fill at any bar width
   with no JS measuring anything. */
.progression__tip {
  position: absolute;
  top: 50%;
  left: 0;
  width: 3px;
  height: 210%;
  margin-left: -1.5px;
  border-radius: 999px;
  background: linear-gradient(to bottom, #eaf7ff, #9fdcff);
  transform: translate(calc(var(--p, 0) * 100cqw), -50%);
  transition: transform var(--dur-epic) var(--ease);
  box-shadow:
    0 0 8px 1px rgba(190, 235, 255, 0.95),
    0 0 20px 3px rgba(61, 127, 255, 0.75),
    0 0 42px 8px rgba(61, 127, 255, 0.4);
}

/* ---------- The load animation ----------

   The fill climbs from empty to its real position as the dashboard
   arrives. This is the moment the whole block exists for, so it gets a
   long duration and a beat of delay to let the hero land first.

   Scoped to .rank.is-revealing, which motion.js adds once per load.
   The `> *` animation:none rule in the system layer only touches
   DIRECT children of .rank, so these deeper descendants are free to
   animate. */
.rank.is-revealing .progression__fill {
  animation: ss-fill 1400ms var(--ease) 520ms both;
}
.rank.is-revealing .progression__tip {
  animation: ss-tip 1400ms var(--ease) 520ms both;
}

@keyframes ss-fill {
  from { transform: scaleX(0); }
  to   { transform: scaleX(var(--p, 0)); }
}

@keyframes ss-tip {
  from { transform: translate(0, -50%); }
  to   { transform: translate(calc(var(--p, 0) * 100cqw), -50%); }
}



@media (max-width: 720px) {
  /* The rail cannot hold three columns at phone width without the bar
     collapsing to nothing, so the nodes sit above it. */
  .progression__rail { flex-wrap: wrap; }
  .progression__barwrap { order: 3; flex: 1 0 100%; margin-top: 16px; }
  .progression__node--next { margin-left: auto; }
}

/* ---------- Reduced motion ----------
   The bar shows its real position immediately. The resting transform
   is already the final state, so cancelling the animations leaves a
   correctly filled bar rather than an empty one. */
@media (prefers-reduced-motion: reduce) {
  .rank.is-revealing .progression__fill,
  .rank.is-revealing .progression__tip,
  .progression__shimmer {
    animation: none !important;
  }
  .progression__shimmer { opacity: 0; }
  .progression__fill { transform: scaleX(var(--p, 0)) !important; }
  .progression__tip { transform: translate(calc(var(--p, 0) * 100cqw), -50%) !important; }
}

/* ============================================================
   PROGRESSION VOICE
   Retyping the narrative line.

   THE RULE, and it applies to the whole dashboard from here:
   JetBrains Mono is for DATA. Dollar figures, percentages, counts.
   Never for prose. A sentence set in mono reads as a system log, and
   a motivating message that reads like a system log does not
   motivate. Saira carries the voice; mono carries the numbers; they
   sit in the same sentence.
   ============================================================ */

.progression__gap {
  margin-top: clamp(20px, 2.2vw, 28px);
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0 0.6ch;
  line-height: 1.25;
}

/* The voice. Bright, not muted grey: this is the line that is meant
   to make somebody want to close the gap, so it is allowed to be the
   most present text in the block after the value itself. */
.progression__lead {
  font-family: var(--font-display);
  font-stretch: var(--display-header);
  font-weight: 600;
  font-size: clamp(17px, 1.9vw, 23px);
  letter-spacing: -0.01em;
  color: var(--text);
  margin-right: 0.35ch;
}

.progression__say {
  font-family: var(--font-display);
  font-stretch: var(--display-header);
  font-weight: 600;
  font-size: clamp(17px, 1.9vw, 23px);
  letter-spacing: -0.01em;
  /* Deliberately a step down from the lead but nowhere near --muted.
     The old line was --muted mono, which is what made it read as a
     footnote about the user instead of a message to them. */
  color: rgba(245, 242, 236, 0.82);
}

/* The one mono island in the sentence. */
.progression__gapfigure {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  font-size: 1.02em;
  letter-spacing: -0.02em;
  color: var(--text);
  text-shadow: 0 0 26px rgba(61, 127, 255, 0.45);
}

/* The goal, in the accent. The only accent-coloured words in the
   block, so the eye lands on what is being chased. */
.progression__goal {
  color: var(--accent);
  text-shadow: 0 0 22px rgba(61, 127, 255, 0.4);
}

/* The supporting line. Small and quiet, and the only place left that
   is allowed to be muted, because it genuinely is context. */
.progression__note {
  margin-top: 10px;
  font-family: var(--font-body);
  font-weight: 300;
  font-size: 12.5px;
  letter-spacing: 0.01em;
  color: var(--muted);
}
.progression__note .mono {
  font-variant-numeric: tabular-nums;
  color: rgba(245, 242, 236, 0.7);
}

/* ============================================================
   THE PRESTIGE LADDER
   Nine tiers, one track, three states.

   Reads left to right as a journey: what you have taken, where you
   stand, and what is still ahead. The art does the work, so the
   emblems are large enough to actually look at and everything else
   stays out of their way.
   ============================================================ */

.ladder {
  position: relative;
  margin-top: clamp(26px, 3vw, 40px);
  padding-top: clamp(20px, 2.2vw, 28px);
  border-top: 1px solid var(--line);
}

/* ---------- The track ----------
   Runs behind the emblems at their centre line. Sunken, like the
   progress bar, so the two read as the same family of component. */
.ladder__track {
  position: absolute;
  /* From the first emblem's centre to the last one's, inside the rail. */
  left: calc(var(--ladder-edge) + (var(--ladder-node) / 2));
  right: calc(var(--ladder-edge) + (var(--ladder-node) / 2));
  /* On the emblem centre line: the rail's top padding plus half the art
     box. Kept in sync with the variables below. */
  top: calc(var(--ladder-pad) + (var(--ladder-art) / 2));
  height: 2px;
  border-radius: 999px;
  background: #1a1a1f;
  overflow: hidden;
}

.ladder__trackfill {
  position: absolute;
  inset: 0;
  transform-origin: left center;
  transform: scaleX(var(--j, 0));
  background: linear-gradient(to right, #2a5fd0, var(--accent));
  box-shadow: 0 0 12px -1px rgba(61, 127, 255, 0.85);
  transition: transform var(--dur-epic) var(--ease);
}

.rank.is-revealing .ladder__trackfill {
  animation: ss-fill 1600ms var(--ease) 700ms both;
}

/* ---------- Nodes ---------- */
.ladder {
  /* The emblem box, the width each tier gets, the rail's top padding and
     its side margin. Thirty tiers at a readable width are wider than any
     screen, so the strip scrolls rather than squeezing the names into
     each other. */
  --ladder-art: clamp(40px, 4vw, 54px);
  --ladder-node: clamp(72px, 6.2vw, 86px);
  --ladder-pad: 10px;
  --ladder-edge: 36px;
}

/* The scroller. Faded at both edges so the strip reads as continuing
   past the frame rather than cut off by it. */
.ladder__scroll {
  position: relative;
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--line-warm) transparent;
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--ladder-edge), #000 calc(100% - var(--ladder-edge)), transparent 100%);
          mask-image: linear-gradient(to right, transparent 0, #000 var(--ladder-edge), #000 calc(100% - var(--ladder-edge)), transparent 100%);
}

.ladder__scroll:focus-visible {
  outline: 2px solid rgba(var(--glow-accent), 0.7);
  outline-offset: 4px;
  border-radius: 8px;
}

/* As wide as thirty tiers. It carries the track, so the line runs the
   whole climb, and its padding leaves room for the hover lift and the
   threshold teaser inside the scroller. */
.ladder__rail {
  position: relative;
  width: max-content;
  padding: var(--ladder-pad) var(--ladder-edge) 34px;
}

.ladder__nodes {
  position: relative;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: var(--ladder-node);
  align-items: start;
  margin: 0;
  padding: 0;
  list-style: none;
}

.ladder__node {
  position: relative;
  min-width: 0;
  padding: 0 3px;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  transition: transform var(--dur-lift) var(--ease);
}

.ladder__art {
  position: relative;
  width: var(--ladder-art);
  height: var(--ladder-art);
  display: grid;
  place-items: center;
  /* The track passes behind the emblems, so each one needs its own
     ground or the line runs visibly through the artwork. */
  border-radius: 50%;
  background: radial-gradient(circle at center, #0d0d10 62%, rgba(13, 13, 16, 0) 100%);
}

.ladder__img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  transition: opacity var(--dur-lift) var(--ease),
              filter var(--dur-lift) var(--ease);
}

.ladder__name {
  margin-top: 9px;
  font-family: var(--font-display);
  font-stretch: var(--display-label);
  font-weight: 600;
  font-size: clamp(8.5px, 0.78vw, 9.5px);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  line-height: 1.25;
  max-width: 100%;
  overflow-wrap: break-word;
  transition: color var(--dur-lift) var(--ease);
}

/* ---------- PAST: conquered ----------
   Full colour, and a small earned tick. These are yours. */
.ladder__node--past .ladder__img { opacity: 1; }
.ladder__node--past .ladder__name { color: var(--muted); }

/* The earned mark. A real glyph rather than a checkmark drawn from
   gradients: at 14px the drawn version rendered as a smudge, and a
   tick that does not read as a tick is just a blue dot.

   Tucked to the corner and OUTSIDE the art box (negative offsets), so
   it marks the emblem without covering any of it. */
.ladder__node--past .ladder__art::after {
  content: "✓";
  position: absolute;
  right: -1px;
  bottom: -1px;
  width: 15px;
  height: 15px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--accent);
  color: #050505;
  font-family: var(--font-body);
  font-size: 9px;
  font-weight: 700;
  line-height: 1;
  /* The dark ring separates it from whatever the emblem is doing
     underneath, at any tier. */
  box-shadow: 0 0 0 2px #0a0a0c, 0 0 10px -1px rgba(61, 127, 255, 0.9);
}

/* ---------- CURRENT: where you stand ---------- */
.ladder__node--current { transform: translateY(-2px); }

.ladder__node--current .ladder__art {
  background: radial-gradient(circle at center,
    rgba(61, 127, 255, 0.24) 0%,
    rgba(61, 127, 255, 0.07) 46%,
    rgba(13, 13, 16, 0) 76%),
    radial-gradient(circle at center, #0d0d10 62%, rgba(13, 13, 16, 0) 100%);
}

.ladder__node--current .ladder__img {
  opacity: 1;
  filter: drop-shadow(0 0 14px rgba(61, 127, 255, 0.55));
}

.ladder__node--current .ladder__name {
  color: var(--text);
  text-shadow: 0 0 18px rgba(61, 127, 255, 0.5);
}

/* ---------- FUTURE: locked, and tempting ---------- */
.ladder__node--future .ladder__img {
  opacity: 0.3;
  /* Desaturated rather than just dimmed. Colour is the reward, so
     draining it is what makes an unreached emblem read as locked
     rather than as a badly exposed one. */
  filter: grayscale(0.85) brightness(0.75);
}
.ladder__node--future .ladder__name { color: var(--faint); }

/* ---------- THE GOAL ----------
   The next tier, the one being climbed toward. The one locked node
   allowed to glow, because a goal that looks like every other locked
   step is not a goal. */
.ladder__node--goal .ladder__img {
  opacity: 0.62;
  filter: grayscale(0.35) brightness(0.95)
          drop-shadow(0 0 14px rgba(61, 127, 255, 0.45));
}
.ladder__node--goal .ladder__name { color: var(--muted); }

.ladder__node--goal .ladder__art::before {
  content: "";
  position: absolute;
  inset: -20%;
  border-radius: 50%;
  background: radial-gradient(circle at center,
    rgba(61, 127, 255, 0.26) 0%,
    rgba(61, 127, 255, 0.07) 46%,
    transparent 72%);
  animation: ss-breathe 8s ease-in-out infinite;
}

/* ---------- Hover ----------
   Lifts the emblem and reveals its threshold. The tier NAME is always
   visible underneath, so hover only ever adds the number; nothing a
   pointer-less user needs is behind this. */
.ladder__node:hover { transform: translateY(-5px); }

.ladder__node:hover .ladder__img {
  opacity: 1;
  filter: none;
}
.ladder__node--goal:hover .ladder__img,
.ladder__node--current:hover .ladder__img {
  filter: drop-shadow(0 0 18px rgba(61, 127, 255, 0.7));
}
.ladder__node:hover .ladder__name { color: var(--text); }

.ladder__teaser {
  position: absolute;
  top: calc(var(--ladder-art) + 30px);
  left: 50%;
  transform: translate(-50%, -4px);
  padding: 3px 8px 2px;
  border-radius: 4px;
  background: #0b0b0e;
  border: 1px solid var(--line-warm);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 10px;
  letter-spacing: 0.04em;
  white-space: nowrap;
  color: var(--accent);
  box-shadow: var(--elev-2);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-swift) var(--ease),
              transform var(--dur-lift) var(--ease);
}

.ladder__node:hover .ladder__teaser {
  opacity: 1;
  transform: translate(-50%, 0);
}

/* ---------- Entrance ----------
   The nodes come up left to right, so the ladder assembles as a climb
   rather than appearing all at once. Deeper than a direct child of
   .rank, so the system layer's animation suppression does not apply. */
.rank.is-revealing .ladder__node {
  animation: ss-rise 620ms var(--ease) both;
  /* Capped at the first dozen, so thirty tiers are not still assembling
     three seconds in. */
  animation-delay: calc(760ms + (min(var(--i, 0), 12) * 70ms));
}

@media (max-width: 720px) {
  /* The strip scrolls, so on a phone every tier keeps its name. */
  .ladder {
    --ladder-art: clamp(34px, 9vw, 42px);
    --ladder-node: 68px;
    --ladder-edge: 24px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .rank.is-revealing .ladder__node,
  .rank.is-revealing .ladder__trackfill,
  .ladder__node--goal .ladder__art::before {
    animation: none !important;
  }
  .ladder__trackfill { transform: scaleX(var(--j, 0)) !important; }
  .ladder__node:hover { transform: none; }
}

/* ============================================================
   THE TRAILER
   Landing sections 2-5, read as one descending sequence.

   ALTERNATION IS THE MOMENTUM. Each section swaps which side the art
   sits on, so the eye crosses the page on every scroll rather than
   running down one gutter. .trailer--flip does the swap by reordering
   the grid children, NOT by reordering the markup: the text column
   always comes first in the DOM, so reading order and tab order stay
   correct regardless of which side it is painted on.
   ============================================================ */

.trailer {
  position: relative;
  padding: clamp(64px, 8vw, 128px) 0;
  border-top: 1px solid var(--line);
}

.trailer__inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: center;
  gap: clamp(36px, 5.5vw, 84px);
}

/* Art left, text right is the default (section 2 and 4). The flip puts
   the text first visually for 3. */
.trailer__art  { order: 1; min-width: 0; }
.trailer__text { order: 2; min-width: 0; }
.trailer--flip .trailer__art  { order: 2; }
.trailer--flip .trailer__text { order: 1; }

.trailer__eyebrow { margin-bottom: 18px; }

.trailer__title {
  font-family: var(--font-display);
  font-stretch: var(--display-hero);
  font-weight: 700;
  font-size: clamp(30px, 3.6vw, 52px);
  line-height: 1.02;
  letter-spacing: -0.025em;
  color: var(--text);
}

.trailer__body {
  margin-top: clamp(18px, 2vw, 26px);
  max-width: 46ch;
  font-size: 15.5px;
  line-height: 1.7;
  color: var(--muted);
}

.trailer__aside {
  margin-top: 18px;
  max-width: 44ch;
  font-size: 12.5px;
  line-height: 1.65;
  color: var(--faint);
}

/* Section 2's collage and stat row lived here. The section they
   belonged to is now css/game.css: a fanned still and a row of three
   numbers could not do the job that section has, so both went with
   it. Nothing else on the site used either class. */

/* ---------- The intelligence ----------
   The landing's market section is the market intelligence terminal
   (css/market-intel.css), right after the hero. The signal terminal that
   used to be section 3 (css/intel.css) went with it. */

/* Section 4's ladder was replaced by the climbing path; see THE
   CLIMBING PATH near the end of this file. The .ladder--showcase rules
   went with it. The dashboard's own .ladder is untouched. */

/* ============================================================
   SECTION 5: THE CLOSE
   The membership section is styled in css/pricing.css, which only the
   landing page loads. Its entrance stays in ENTRANCE below with the
   other trailer sections.
   ============================================================ */

/* ============================================================
   ENTRANCE
   Each section rises once as it enters the viewport, with the two
   columns offset so the composition assembles rather than appearing.

   Base state is VISIBLE and the animation runs FROM hidden, so a
   visitor with no JS, or a failed observer, sees the whole page.
   ============================================================ */

.trailer.is-revealed .trailer__text,
.trailer.is-revealed .close__head {
  animation: ss-rise 760ms var(--ease) both;
}

.trailer.is-revealed .trailer__art {
  animation: ss-rise 900ms var(--ease) 140ms both;
}

.trailer.is-revealed .tier {
  animation: ss-rise 760ms var(--ease) both;
  animation-delay: calc(160ms + (var(--i, 0) * 110ms));
}

.trailer.is-revealed .tiers__note {
  animation: ss-rise 700ms var(--ease) 420ms both;
}

/* ---------- Responsive ---------- */
@media (max-width: 900px) {
  .trailer__inner { grid-template-columns: 1fr; gap: clamp(30px, 6vw, 48px); }
  /* Stacked, the art always leads: it is the thing that sells the
     section, and alternation means nothing in a single column. */
  .trailer__art, .trailer--flip .trailer__art { order: 1; }
  .trailer__text, .trailer--flip .trailer__text { order: 2; }
}


@media (prefers-reduced-motion: reduce) {
  .trailer.is-revealed .trailer__text,
  .trailer.is-revealed .trailer__art,
  .trailer.is-revealed .close__head,
  .trailer.is-revealed .tier,
  .trailer.is-revealed .tiers__note {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
  .tier:hover { transform: none; }
}

/* ============================================================
   LANDING REFINEMENTS
   ============================================================ */

/* The card fan that lived here went with its section. Its successor
   is .cascade in css/game.css, which does the same job with six cards
   instead of three and arrives instead of sitting there. */

/* The landing ladder became the climbing path; its rules live under
   THE CLIMBING PATH near the end of this file. Nothing on the landing
   page renders .ladder--showcase any more. */

/* ---------- 4. Pricing ----------
   Moved to css/pricing.css with the rest of the section. */

/* ---------- 4b. Buttons ----------

   The premium .btn and the pricing button face that lived here are
   now the site button system in css/buttons.css. Lifetime's button is
   .cta--gold and Monthly's is .cta--explore .cta--ghost, both placed by
   .tier__btn in css/pricing.css. */

/* ---------- 5. Body text legibility ----------

   --muted was #8a8a85, which held up on a card surface but went thin
   over the atmospheric background on the landing page, where the void
   lifts the ground behind it. Raised a step, and warmed very slightly
   toward the off-white so it reads as the same family as --text rather
   than as grey.

   This is a TOKEN change and therefore global, which is deliberate:
   two different muted greys, one per page, is how a design system
   stops being one. Checked against the holdings table, where small
   mono figures on a lifted ground are the first thing to suffer. */
:root {
  --muted: #9d9d97;
  /* --faint used to be overridden here too. It is now declared once, in
     the token block at the top of this file, where the header says to
     change it. Two declarations of one token is how they drift. */
}

/* The long-form paragraphs on the landing page carry the argument, so
   they get a touch more than the token: still restrained, but clearly
   readable at a glance over the drifting background. */
.trailer__body {
  color: #b4b4ad;
}

.trailer__aside { color: var(--muted); }

/* ============================================================
   THE JOURNEY, FULL WIDTH
   The one section that breaks the alternating rhythm.

   WHY IT BREAKS IT
   Nine emblems in half a column can only ever be ~68px: the column is
   about 680px, which is 75px a node once gaps are taken out, and the
   art has to fit inside that. Given the whole point of this section is
   "look how far the ladder goes", 68px was the layout winning an
   argument it should have lost.

   Full width the container is 1216px, so each node gets ~124px and the
   emblems can be 108px. That is roughly a 60% increase, and it is the
   difference between a row of icons and a row of ARTWORK.

   No markup change: the copy and the ladder are still .trailer__text
   and .trailer__art, so the reveal wiring is untouched. Only the grid
   and the order change.
   ============================================================ */

/* The Journey section's layout rules lived here. The pyramid is the
   Climb beat of The Game now and the section is gone, so its grid,
   its ordering and its overflow guard went with it. What the pyramid
   still needs is in css/climb2.css and css/game.css. */

/* ============================================================
   THE CLIMBING PATH
   Moved out. The showcase ladder is the pyramid in css/climb2.css,
   which owns .climb and the whole climb__* namespace including its
   lightbox. The DASHBOARD ladder (.ladder__*) is a different
   component and is untouched, further up this file.
   ============================================================ */

/* ---------- Nav ----------
   Site chrome, shared by every page: drawn by css/header.css, with the
   phone and tablet menu that replaced the two row bar. */

/* ============================================================
   THE LOCKED INVENTORY TEASER

   Drawn in the signals list when the server returned a locked history
   payload. It is the one card on the page whose job is to convert, so
   it carries the accent that every other signal card deliberately does
   not.
   ============================================================ */
.signal--locked {
  border-color: rgba(61, 127, 255, 0.28);
  background:
    linear-gradient(180deg, rgba(61, 127, 255, 0.07), rgba(61, 127, 255, 0) 62%),
    var(--surface);
}

.teaser__eyebrow {
  font-family: var(--font-display);
  font-stretch: var(--display-label);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
  margin: 0 0 6px;
}
.teaser__eyebrow .mono { color: var(--muted); letter-spacing: 0.04em; }

.teaser__headline {
  font-family: var(--font-display);
  font-stretch: var(--display-header);
  font-size: clamp(17px, 2.4vw, 21px);
  font-weight: 600;
  color: var(--text);
  margin: 0 0 12px;
  line-height: 1.25;
}

.teaser__moves { list-style: none; margin: 0 0 12px; padding: 0; display: grid; gap: 6px; }

.teaser__move {
  display: grid;
  grid-template-columns: 52px minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 10px;
  padding: 7px 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface-sunken);
}

.teaser__movelabel {
  font-family: var(--font-display);
  font-stretch: var(--display-label);
  font-size: 10px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--faint);
}

.teaser__movename {
  font-family: var(--font-body);
  font-size: 13px;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.teaser__movepct { font-size: 13px; font-variant-numeric: tabular-nums; }
.teaser__move--up .teaser__movepct { color: var(--gain); }
.teaser__move--down .teaser__movepct { color: var(--risk); }

.teaser__sub {
  font-family: var(--font-body);
  font-size: 13px;
  color: var(--muted);
  line-height: 1.55;
  margin: 0 0 14px;
}

/* The teaser's button is the site button system, green because it
   leads to paying (.cta .cta--go .cta--sm, css/buttons.css). This hook
   only places it under the line it follows. */
.teaser__cta { margin-top: 2px; }

@media (max-width: 620px) {
  .teaser__move { grid-template-columns: 46px minmax(0, 1fr) auto; gap: 8px; padding: 6px 8px; }
  .teaser__movename { font-size: 12px; }
}
