/* Component styles, in roughly the order a visitor meets them:
 * chrome, portraits, bars, tiles, the ladder, games, matches, and the modal. */

/* ---------------------------------------------------------------- chrome */

.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  background: linear-gradient(var(--bg-deep), #0b0e12);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5);
}

.topbar__inner {
  display: flex;
  align-items: center;
  gap: var(--s5);
  max-width: var(--shell);
  margin: 0 auto;
  padding: var(--s3) var(--s5);
}

/* ------------------------------------------------------------ title text */

/* The game's own logotype: carved-gold lettering carrying the whole effect
 * itself (a hot gradient fill, a warm outline, a stepped shadow for depth),
 * rather than sitting inside a framed plate or alongside an icon -- at this
 * size the wordmark alone is the whole header's centre of gravity. */
.title-banner {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  transition: filter 140ms var(--ease);
}

.title-banner:hover {
  filter: brightness(1.08);
}

.title-banner__text {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}

.title-banner__line {
  font-family: var(--font-fantasy);
  font-size: 36px;
  line-height: 1.1;
  letter-spacing: 0.02em;
  white-space: nowrap;
  /* Flat golden yellow, the same gold LOG GAME owns. No stroke: Jacquard 12
   * is a fine, detailed face, and an artificial outline thick enough to see
   * clogs its counters and thin joins into mud at any size worth reading --
   * exactly the "muddy" look a stroke was added to previous fonts to fix,
   * but this one doesn't need it. A single soft shadow for depth is enough. */
  color: var(--hero);
  filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.6));
}

/* -------------------------------------------------------- header divider */

/* Stands in for a plain border-bottom. The end caps reuse the same corner
 * ornament every panel draws from (see installCornerOrnaments in sprite.js),
 * so this shares its ink with the rest of the app rather than being a new
 * hand-drawn asset. */
.header-divider {
  display: flex;
  align-items: center;
  gap: var(--s2);
  max-width: var(--shell);
  margin: 0 auto;
  padding: 0 var(--s4) var(--s2);
}

.header-divider__line {
  flex: 1 1 auto;
  min-width: 0;
  height: 3px;
  background: linear-gradient(
    90deg, transparent, var(--edge-bright) 12%, var(--edge) 50%, var(--edge-bright) 88%, transparent
  );
  box-shadow: 0 2px 0 var(--edge-dim);
}

.header-divider__gem {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  filter: drop-shadow(0 0 5px rgba(242, 206, 86, 0.55));
}

.header-divider__end {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  background-image: var(--corner-tl);
  background-size: contain;
  background-repeat: no-repeat;
  image-rendering: pixelated;
  opacity: 0.9;
}

.header-divider__end--r {
  transform: scaleX(-1);
}

.nav {
  display: flex;
  gap: var(--s1);
  margin-left: auto;
}

.nav__item {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: 10px var(--s3);
  font-family: var(--font-display);
  font-size: 9px;
  letter-spacing: 0.06em;
  color: var(--ink-faint);
  /* Reserve the active underline's space so nothing shifts on hover. */
  border-bottom: var(--fw) solid transparent;
  transition: color 120ms;
}

/* Desktop has room for words, so the icon is redundant there. */
.nav__icon {
  display: none;
}

.nav__item .sprite {
  transition: filter 140ms var(--ease);
}

.nav__item:hover {
  color: var(--ink-dim);
}

.nav__item.is-active {
  color: var(--gold);
  border-bottom-color: var(--gold);
}

.topbar__actions {
  flex-shrink: 0;
}

/* ---------------------------------------------------------------- headings */

.section-head {
  display: flex;
  align-items: center;
  gap: var(--s3);
}

.section-head__title {
  display: flex;
  align-items: center;
  gap: var(--s2);
}

.section-head__label {
  font-size: 13px;
  letter-spacing: 0.06em;
  color: var(--ink);
}

.section-head__sub {
  font-family: var(--font-display);
  font-size: 8px;
  letter-spacing: 0.04em;
  color: var(--ink-faint);
}

/* The rule fills whatever space is left between title and action. */
.section-head__rule {
  flex: 1;
  height: 3px;
  background: repeating-linear-gradient(90deg, var(--orn) 0 3px, transparent 3px 8px);
}

.divider {
  display: flex;
  align-items: center;
  gap: var(--s3);
  opacity: 0.5;
}

.divider__rule {
  flex: 1;
  height: 3px;
  background: repeating-linear-gradient(90deg, var(--orn) 0 3px, transparent 3px 8px);
}

/* ---------------------------------------------------------------- portraits */

.portrait {
  position: relative;
  width: var(--portrait-size);
  /* max-width + aspect-ratio rather than a fixed height, so a portrait shrinks
   * to fit a narrow column instead of overflowing it, and stays square. */
  max-width: 100%;
  aspect-ratio: 1;
  overflow: hidden;
  background:
    radial-gradient(circle at 50% 70%, color-mix(in srgb, var(--accent) 22%, transparent), transparent 70%),
    var(--panel-2);
  box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--accent) 30%, var(--edge));
  clip-path: polygon(
    4px 0, calc(100% - 4px) 0, 100% 4px, 100% calc(100% - 4px),
    calc(100% - 4px) 100%, 4px 100%, 0 calc(100% - 4px), 0 4px
  );
}

.portrait__img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Chips zoom in on the head rather than scaling the whole figure down, because a
 * 48px full body shrunk to a 28px chip is unreadable mush.
 *
 * The window is the sprite's top edge, which is where every character's head
 * sits -- but not at the same *horizontal* spot, since each is a different
 * pose. --bust-x (set in portrait(), from each player's own `bustX`) shifts
 * that window to whichever character is being drawn, so a face is centred in
 * its own chip instead of a fixed window that only suits one pose. Stated in
 * percentages of the rendered box rather than in source pixels, so it holds
 * at any portrait size and survives any re-export. */
.portrait--bust {
  background-image:
    var(--src),
    radial-gradient(circle at 50% 60%, color-mix(in srgb, var(--accent) 26%, transparent), transparent 72%);
  background-size: 200% auto, cover;
  background-position: var(--bust-x, 50%) 0, center;
  background-repeat: no-repeat;
  image-rendering: pixelated;
}

/* The turntable. The strip is an <img> eight portraits wide inside this clipped
 * box, stepped along by transform. steps(8) lands every jump exactly on a frame
 * boundary, so a half-frame is never visible, and -100% of the image's own width
 * is exactly eight frames -- no size maths needed. */
.portrait__strip {
  width: 800%;
  height: 100%;
  max-width: none;
  object-fit: fill;
  animation: portrait-turn 6s steps(8) infinite;
}

@keyframes portrait-turn {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-100%);
  }
}

/* A slowly rotating character is exactly the kind of ambient motion a
 * reduced-motion preference is asking to be spared, so it rests front-on. */
@media (prefers-reduced-motion: reduce) {
  .portrait__strip {
    animation: none;
  }
}

.player-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  vertical-align: middle;
}

.player-chip.is-dim {
  opacity: 0.62;
}

.player-chip__name {
  font-family: var(--font-display);
  font-size: 9px;
}

/* ---------------------------------------------------------------- bars */

.stat-bar {
  display: flex;
  flex-direction: column;
  gap: 5px;
  width: 100%;
}

.stat-bar__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s2);
}

.stat-bar__label {
  font-family: var(--font-display);
  font-size: 8px;
  letter-spacing: 0.06em;
  color: var(--ink-faint);
}

.stat-bar__value {
  font-family: var(--font-display);
  font-size: 9px;
  color: var(--ink-dim);
}

.stat-bar__track {
  position: relative;
  height: 12px;
  background: var(--well);
  box-shadow: inset 0 0 0 2px var(--edge);
  overflow: hidden;
}

.stat-bar__track.is-muted {
  height: 8px;
}

.stat-bar__fill {
  height: 100%;
  min-width: 0;
  background: var(--accent);
  box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.3);
  transition: width 420ms var(--ease);
}

/* Notches over the whole track, so a filled bar reads as discrete RPG segments
 * rather than a smooth web progress bar. */
.stat-bar__track::after {
  content: '';
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(90deg, transparent 0 7px, rgba(0, 0, 0, 0.55) 7px 9px);
  pointer-events: none;
}

/* ---------------------------------------------------------------- tiles */

.overview {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: var(--s3);
}

.stat-tile {
  --accent: var(--gold);
}

.stat-tile .panel__in {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  padding: var(--s4);
}

.stat-tile__top {
  display: flex;
  align-items: center;
  gap: var(--s2);
}

.stat-tile__label {
  font-family: var(--font-display);
  font-size: 8px;
  letter-spacing: 0.07em;
  color: var(--ink-faint);
}

.stat-tile__value {
  display: block;
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: 400;
  color: var(--ink);
}

.stat-tile__foot {
  display: block;
  font-family: var(--font-display);
  font-size: 8px;
  line-height: 1.6;
  color: var(--ink-dim);
}

.champion-tile__row {
  display: flex;
  align-items: center;
  gap: var(--s3);
}

.champion-tile__row .stat-tile__value {
  font-size: 15px;
}

.stat-tile--champion .panel__in {
  background:
    linear-gradient(120deg, color-mix(in srgb, var(--accent) 16%, transparent), transparent 65%),
    linear-gradient(var(--panel-2), var(--panel) 42%);
}

.sheet-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--s3);
}

/* ---------------------------------------------------------------- badges */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 7px;
  font-family: var(--font-display);
  font-size: 7px;
  letter-spacing: 0.06em;
  background: var(--panel-3);
  color: var(--ink-dim);
}

.badge--streak {
  background: #3a2408;
  color: #ffbe5c;
}

.badge--win {
  background: #12331f;
  color: var(--win);
}

.badge--loss {
  background: #331414;
  color: var(--loss);
}

.badge--best {
  background: var(--gold-dark);
  color: var(--gold);
}

.level-tag {
  display: inline-block;
  padding: 4px 7px;
  font-family: var(--font-display);
  font-size: 8px;
  letter-spacing: 0.05em;
  background: var(--well);
  color: var(--accent, var(--gold));
  box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--accent, var(--gold)) 40%, transparent);
}

.level-tag--lg {
  padding: 6px 10px;
  font-size: 12px;
}

.ribbon {
  display: inline-block;
  align-self: flex-start;
  padding: 4px 9px;
  margin-bottom: var(--s1);
  font-family: var(--font-display);
  font-size: 8px;
  letter-spacing: 0.14em;
  background: var(--gold);
  color: #23180a;
  clip-path: polygon(3px 0, 100% 0, calc(100% - 3px) 100%, 0 100%);
}

/* ---------------------------------------------------------------- ladder */

.ladder {
  display: flex;
  flex-direction: column;
  gap: var(--s3);
}

.ladder-row {
  --accent: var(--gold);
  cursor: pointer;
  transition: transform 140ms var(--ease);
}

.ladder-row .panel__in {
  display: grid;
  grid-template-columns: 52px auto minmax(0, 1fr) auto minmax(190px, 240px);
  align-items: center;
  gap: var(--s4);
  padding: var(--s3) var(--s4);
  background:
    linear-gradient(100deg, color-mix(in srgb, var(--deep) 55%, var(--panel)) 0%, transparent 46%),
    linear-gradient(var(--panel-2), var(--panel) 42%);
}

.ladder-row.is-champion {
  --accent: var(--gold);
}

.ladder-row.is-champion {
  /* A visibly heavier frame than any other card, with a deeper corner cut. */
  --notch: 10px;
  padding: 5px;
  background: linear-gradient(#ffeec0, var(--edge) 42%, var(--edge-dim));
  box-shadow: 0 4px 22px rgba(0, 0, 0, 0.55);
}

.ladder-row.is-champion .panel__in {
  padding: var(--s4);
  background:
    linear-gradient(100deg, color-mix(in srgb, var(--deep) 55%, var(--panel)) 0%, transparent 40%),
    radial-gradient(circle at 20% 10%, rgba(246, 205, 103, 0.14), transparent 55%),
    linear-gradient(180deg, rgba(242, 206, 86, 0.18), transparent 58%),
    linear-gradient(var(--panel-2), var(--panel) 42%);
  /* Doubled inner border: a bright gold hairline inside a dark one, which is what
   * reads as "framed twice" rather than just "thicker". */
  box-shadow:
    inset 0 0 0 2px var(--gold-dark),
    inset 0 0 0 4px rgba(242, 206, 86, 0.55),
    inset 0 3px 0 var(--bevel),
    inset 0 -3px 0 var(--bevel-dim);
}

/* Ambient light motes, drifting behind the champion's content. An absolutely
 * positioned sibling paints above static ones regardless of DOM order, so the
 * real content needs its own stacking layer to stay on top of the motes. */
.ladder-row.is-champion > .panel__in > :not(.champion-motes) {
  position: relative;
  z-index: 1;
}

.champion-motes {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

.champion-motes__mote {
  position: absolute;
  opacity: 0;
  animation: mote-drift 6s ease-in-out infinite;
}

@keyframes mote-drift {
  0% { opacity: 0; transform: translate(0, 4px); }
  15% { opacity: 0.55; }
  50% { opacity: 0.85; transform: translate(2px, -6px); }
  85% { opacity: 0.35; }
  100% { opacity: 0; transform: translate(0, -10px); }
}

@media (prefers-reduced-motion: reduce) {
  .champion-motes__mote {
    animation: none;
    opacity: 0.4;
  }
}

/* LOG GAME's own aura: the same drifting-mote technique as the champion
 * card, in a halo extending past the button's edges rather than confined
 * inside it -- the button itself is opaque, so motes only read behind it in
 * that surrounding margin. */
.cta-wrap {
  position: relative;
  display: inline-flex;
}

.cta-wrap .pixel-btn {
  position: relative;
  z-index: 1;
}

.cta-motes {
  position: absolute;
  inset: -14px;
  z-index: 0;
  pointer-events: none;
}

.cta-motes__mote {
  position: absolute;
  opacity: 0;
  animation: mote-drift 4.2s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .cta-motes__mote {
    animation: none;
    opacity: 0.35;
  }
}

.ladder-row__rank {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}

.ladder-row__rank-num {
  font-family: var(--font-display);
  font-size: 22px;
  color: var(--ink-faint);
}

.is-champion .ladder-row__rank-num {
  font-size: 26px;
  color: var(--gold);
}

.ladder-row__identity {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}

.ladder-row__name {
  font-size: 17px;
  color: var(--accent);
}

.is-champion .ladder-row__name {
  font-size: 22px;
}

.ladder-row__title {
  font-family: var(--font-display);
  font-size: 9px;
  letter-spacing: 0.04em;
  color: var(--ink-dim);
}

.ladder-row__meta {
  display: flex;
  align-items: center;
  gap: var(--s2);
  flex-wrap: wrap;
  margin-top: 2px;
}

.ladder-row__xp {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin-top: var(--s1);
}

.ladder-row__xp-text {
  font-family: var(--font-display);
  font-size: 7px;
  color: var(--ink-faint);
  white-space: nowrap;
}

.ladder-row__figures {
  display: flex;
  gap: var(--s5);
}

.figure {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
}

.figure__value {
  display: flex;
  align-items: center;
  gap: 5px;
  font-family: var(--font-display);
  font-size: 15px;
  color: var(--ink);
}

.figure--primary .figure__value {
  font-size: 21px;
  color: var(--gold);
}

.ladder-row__best-inner {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.ladder-row__best-game {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-family: var(--font-display);
  font-size: 10px;
}

/* ---------------------------------------------------------------- sheet */

.page--sheet,
.page--world {
  --accent: var(--gold);
}

/* ------------------------------------------------------------- trophies */

/* Six slots, always -- a Pokédex, not a highlight reel. A game never topped
 * stays visible rather than disappearing, so the case reads as a checklist
 * ("3 of 6 claimed") instead of only ever showing what's already been won. */
.trophy-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: var(--s3);
}

.trophy-slot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s1);
  padding: var(--s3) var(--s2);
  text-align: center;
  background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 16%, var(--panel-2)), var(--panel) 70%);
  box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--accent) 35%, var(--edge));
  clip-path: polygon(
    6px 0, calc(100% - 6px) 0, 100% 6px, 100% calc(100% - 6px),
    calc(100% - 6px) 100%, 6px 100%, 0 calc(100% - 6px), 0 6px
  );
  transition: transform 140ms var(--ease), filter 140ms var(--ease);
}

.trophy-slot__art {
  line-height: 0;
  filter: drop-shadow(0 3px 4px rgba(0, 0, 0, 0.4));
}

.trophy-slot__art img {
  image-rendering: pixelated;
}

.trophy-slot__name {
  font-family: var(--font-display);
  font-size: 9px;
  letter-spacing: 0.02em;
  color: var(--ink);
}

.trophy-slot__label {
  font-size: 8px;
  line-height: 1.4;
  color: var(--ink-faint);
}

/* Currently held: the loudest state, gold-framed like the champion treatment
 * elsewhere. Held once, since lost: a quieter version of the same win --
 * still full colour, just without the glow. Never held: visible but flattened
 * toward the stone, so the case reads as "not yet" rather than "missing". */
.trophy-slot.is-current {
  box-shadow: inset 0 0 0 2px var(--gold);
}

.trophy-slot.is-current .trophy-slot__name {
  color: var(--gold);
}

.trophy-slot.is-held .trophy-slot__label {
  color: color-mix(in srgb, var(--accent) 55%, var(--ink-dim));
}

.trophy-slot.is-locked {
  background: var(--panel-2);
  box-shadow: inset 0 0 0 2px var(--panel-3);
}

.trophy-slot.is-locked .trophy-slot__art {
  filter: grayscale(1) brightness(0.7) drop-shadow(0 3px 4px rgba(0, 0, 0, 0.4));
  opacity: 0.5;
}

.trophy-slot.is-locked .trophy-slot__name {
  color: var(--ink-faint);
}

/* ---------------------------------------------------------------- mastery */

.mastery .panel__in {
  display: flex;
  flex-direction: column;
  padding: var(--s2) var(--s4);
}

.mastery__row {
  display: grid;
  grid-template-columns: minmax(150px, 200px) minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s4);
  padding: var(--s3) 0;
  border-bottom: 2px solid var(--panel-2);
}

.mastery__row:last-child {
  border-bottom: 0;
}

.mastery__game {
  display: flex;
  align-items: center;
  gap: var(--s2);
}

.mastery__name {
  font-family: var(--font-display);
  font-size: 10px;
}

.mastery__figures {
  display: flex;
  align-items: baseline;
  gap: 5px;
  min-width: 108px;
  justify-content: flex-end;
  font-family: var(--font-display);
  font-size: 10px;
}

.mastery__figures strong {
  font-weight: 400;
  color: var(--ink);
  min-width: 38px;
  text-align: right;
}

.mastery__row.is-best .mastery__name {
  color: var(--gold);
}

/* ---------------------------------------------------------------- h2h */

.h2h .panel__in {
  display: flex;
  flex-direction: column;
  padding: var(--s2) var(--s4);
}

.h2h__row {
  display: grid;
  grid-template-columns: minmax(130px, 180px) minmax(0, 1fr) auto 84px;
  align-items: center;
  gap: var(--s4);
  padding: var(--s3) 0;
  border-bottom: 2px solid var(--panel-2);
}

.h2h__row:last-child {
  border-bottom: 0;
}

.h2h__who {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-family: var(--font-display);
  font-size: 10px;
}

.h2h__score {
  display: flex;
  align-items: baseline;
  gap: var(--s1);
  font-family: var(--font-display);
  font-size: 12px;
}

.h2h__count {
  font-weight: 400;
  color: var(--ink-dim);
}

.h2h__count.is-ahead {
  color: var(--win);
}

.h2h__count.is-behind {
  color: var(--loss);
}

.h2h__verdict {
  font-family: var(--font-display);
  font-size: 7px;
  letter-spacing: 0.08em;
  text-align: right;
  color: var(--ink-faint);
}

.h2h__verdict.is-ahead {
  color: var(--win);
}

.h2h__verdict.is-behind {
  color: var(--loss);
}

/* ---------------------------------------------------------------- games */

.game-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(215px, 1fr));
  gap: var(--s3);
}

.game-card {
  cursor: pointer;
  transition: transform 140ms var(--ease);
}

.game-card:hover {
  transform: translateY(-3px);
}

.game-card .panel__in {
  display: flex;
  flex-direction: column;
  gap: var(--s3);
  height: 100%;
  background:
    linear-gradient(160deg, color-mix(in srgb, var(--deep) 55%, var(--panel)), transparent 60%),
    linear-gradient(var(--panel-2), var(--panel) 42%);
}

.game-card__head {
  display: flex;
  align-items: center;
  gap: var(--s3);
}

.game-card__icon {
  display: grid;
  place-items: center;
  padding: var(--s2);
  background: var(--bg-deep);
  box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--accent) 34%, transparent);
}

.game-card__name {
  font-size: 14px;
  color: var(--accent);
}

.game-card__realm {
  font-family: var(--font-display);
  /* 7px, because the pixel face is wide: at 8px "The Property Wars" wrapped
   * beside the icon and knocked the cards in a row out of alignment. */
  font-size: 7px;
  line-height: 1.7;
  color: var(--ink-faint);
}

.game-card__count {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
}

.game-card__count strong {
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 400;
}

.game-card__champ {
  /* Bottom-anchored, so this row lines up across a grid of cards even when one
   * card's content runs a line taller than its neighbours. */
  margin-top: auto;
  padding-top: var(--s3);
  border-top: 2px solid var(--panel-3);
}

.game-card__champ-row {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin-top: var(--s2);
}

.game-card__champ-name {
  flex: 1;
  font-family: var(--font-display);
  font-size: 10px;
}

.game-card__champ-wins {
  font-family: var(--font-display);
  font-size: 10px;
  color: var(--gold);
}

.world-hero .panel__in {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s5);
  padding: var(--s5);
  background:
    linear-gradient(100deg, color-mix(in srgb, var(--deep) 55%, var(--panel)), transparent 58%),
    linear-gradient(var(--panel-2), var(--panel) 42%);
}

.world-hero__crest {
  display: grid;
  place-items: center;
  padding: var(--s4);
  background: var(--bg-deep);
  box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--accent) 34%, transparent);
  clip-path: polygon(
    6px 0, calc(100% - 6px) 0, 100% 6px, 100% calc(100% - 6px),
    calc(100% - 6px) 100%, 6px 100%, 0 calc(100% - 6px), 0 6px
  );
}

.world-hero__realm {
  font-family: var(--font-display);
  font-size: 9px;
  letter-spacing: 0.14em;
  color: var(--ink-faint);
}

.world-hero__name {
  margin: var(--s2) 0 var(--s1);
  font-size: 30px;
  color: var(--accent);
}

.world-hero__blurb {
  font-size: 9px;
  line-height: 1.9;
  color: var(--ink-dim);
}

.world-hero__figures {
  display: flex;
  gap: var(--s5);
  margin-top: var(--s4);
}

.world-hero__figures strong {
  display: block;
  font-family: var(--font-display);
  font-size: 17px;
  font-weight: 400;
}

/* ---------------------------------------------------------------- standings */

.standings .panel__in {
  display: flex;
  flex-direction: column;
  padding: var(--s2) var(--s4);
}

.standings__row {
  display: grid;
  grid-template-columns: 34px minmax(150px, 220px) minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s4);
  padding: var(--s3) 0;
  border-bottom: 2px solid var(--panel-2);
  cursor: pointer;
}

.standings__row:last-child {
  border-bottom: 0;
}

.standings__row:hover .standings__name {
  color: var(--accent);
}

.standings__row.is-absent {
  opacity: 0.45;
}

.standings__pos {
  display: grid;
  place-items: center;
  font-family: var(--font-display);
  font-size: 13px;
  color: var(--ink-faint);
}

.standings__who {
  display: flex;
  align-items: center;
  gap: var(--s3);
}

.standings__name {
  display: block;
  font-family: var(--font-display);
  font-size: 12px;
  font-weight: 400;
  transition: color 120ms;
}

.standings__sub {
  font-size: 8px;
  line-height: 1.7;
  color: var(--ink-faint);
}

.standings__figures {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  min-width: 108px;
  justify-content: flex-end;
}

.standings__wins {
  font-family: var(--font-display);
  font-size: 13px;
  font-weight: 400;
  color: var(--accent);
}

.standings__rate {
  font-family: var(--font-display);
  font-size: 10px;
  color: var(--ink-faint);
}

.is-leader .standings__wins {
  color: var(--gold);
}

/* ---------------------------------------------------------------- matches */

.match-list {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
}

.match-card .panel__in {
  position: relative;
  display: grid;
  grid-template-columns: minmax(170px, 210px) auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s4);
  padding: var(--s3) var(--s4);
  /* The game reads as a wash of its own hue off the left edge rather than a
   * solid stripe -- a hard-edged bar that close to the gold frame (the frame
   * is only --fw wide) read as colliding with it rather than sitting inside
   * it, especially on saturated accents. A gradient has no edge to collide
   * with. */
  background:
    linear-gradient(100deg, color-mix(in srgb, var(--deep) 55%, var(--panel)), transparent 40%),
    linear-gradient(var(--panel-2), var(--panel) 42%);
}

.match-card__game {
  display: flex;
  align-items: center;
  gap: var(--s3);
}

/* A small framed badge rather than a bare icon floating next to the text --
 * the same recipe game-card's own icon uses, so a match row and a world card
 * read as the same family of object. */
.match-card__game-icon {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  padding: 5px;
  background: var(--bg-deep);
  box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--accent) 40%, transparent);
}

.match-card__game-name {
  display: block;
  font-family: var(--font-display);
  font-size: 12px;
  letter-spacing: 0.02em;
  color: var(--accent);
}

.match-card__date {
  font-family: var(--font-display);
  font-size: 8px;
  color: var(--ink-faint);
}

.match-card__outcome {
  padding: 4px 8px;
  font-family: var(--font-display);
  font-size: 8px;
  letter-spacing: 0.1em;
}

.match-card__outcome.is-win {
  background: #12331f;
  color: var(--win);
}

.match-card__outcome.is-loss {
  background: #331414;
  color: var(--loss);
}

.match-card__roster {
  display: flex;
  align-items: center;
  gap: var(--s4);
  flex-wrap: wrap;
  min-width: 0;
}

/* Set apart from the also-played roster with a warm gold-tinted plate --
 * the winner is the one fact this row exists to record, and used to carry no
 * more visual weight than anyone else in it. */
.match-card__winner {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: 5px var(--s3) 5px var(--s2);
  background: linear-gradient(100deg, rgba(242, 206, 86, 0.14), transparent 70%);
  box-shadow: inset 0 0 0 2px rgba(242, 206, 86, 0.28);
  clip-path: polygon(
    5px 0, calc(100% - 5px) 0, 100% 5px, 100% calc(100% - 5px),
    calc(100% - 5px) 100%, 5px 100%, 0 calc(100% - 5px), 0 5px
  );
}

.match-card__winner-name {
  font-family: var(--font-display);
  font-size: 10px;
  color: var(--gold);
}

.match-card__losers {
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex-wrap: wrap;
}

.match-card__loser {
  display: flex;
  align-items: center;
  gap: 5px;
  font-family: var(--font-display);
  font-size: 8px;
  color: var(--ink-faint);
}

.match-card__aside {
  display: flex;
  align-items: center;
  gap: var(--s3);
  justify-content: flex-end;
}

.match-card__ago {
  font-family: var(--font-display);
  font-size: 8px;
  color: var(--ink-faint);
  white-space: nowrap;
}

/* ---------------------------------------------------------------- empty */

.empty-state .panel__in {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s3);
  padding: var(--s8) var(--s5);
  text-align: center;
  background:
    radial-gradient(circle at 50% 0%, rgba(246, 205, 103, 0.07), transparent 60%),
    linear-gradient(var(--panel-2), var(--panel) 42%);
}

.empty-state--sm .panel__in {
  padding: var(--s6) var(--s5);
}

.empty-state__crest {
  margin-bottom: var(--s2);
}

.empty-state__title {
  font-size: 18px;
  letter-spacing: 0.1em;
  color: var(--gold);
}

.empty-state__body {
  max-width: 34ch;
  font-size: 10px;
  line-height: 1.9;
  color: var(--ink-dim);
}

/* ------------------------------------------------------------- passcode */

/* The gate's own backdrop: the castle scene at close to full brightness --
 * a heavy dark scrim was hiding the actual photo rather than showing it.
 * Just enough of a bottom vignette to keep the card's edges from floating on
 * a hard cut, not a wash over the whole thing. Written as
 * `.overlay.overlay--card` (two classes) rather than relying on source
 * order, since the base `.overlay` background rule lives in modal.css and
 * would otherwise win any specificity tie by loading later in the
 * concatenated stylesheet. */
.overlay.overlay--card {
  background:
    linear-gradient(180deg, transparent 40%, rgba(20, 30, 50, 0.22) 100%),
    url('../assets/background/new-background.jpg');
  background-size: cover;
  background-position: center 30%;
}

/* A slow, barely-there zoom on the scene -- not the card -- so the castle
 * feels alive without anything moving fast enough to distract from the
 * passcode field. Applied to the overlay itself via a keyframed background
 * size rather than a child element, since there's no extra markup to hang it
 * on. */
@media (prefers-reduced-motion: no-preference) {
  .overlay.overlay--card {
    animation: scene-drift 30s ease-in-out infinite alternate;
  }
}

@keyframes scene-drift {
  from { background-size: cover; }
  to { background-size: 115% auto; }
}

.passcode-scene-motes {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

.passcode-scene-motes__mote {
  position: absolute;
  opacity: 0;
  animation: mote-drift 7s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .passcode-scene-motes__mote {
    animation: none;
    opacity: 0.3;
  }
}

/* No card at all, on purpose: a boxed panel here was competing with the
 * scene rather than framing it, and the whole point of this screen is the
 * background. Title, copy, input and button all sit directly on the photo,
 * each carrying its own drop-shadow or small glass treatment for legibility
 * instead of one shared panel doing it for all of them. */
.passcode-gate {
  width: min(320px, 100%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s4);
  text-align: center;
  animation: gate-float 5s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .passcode-gate {
    animation: none;
  }
}

@keyframes gate-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-6px); }
}

.passcode-gate__title {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}

/* Flat white, no stroke. Jacquard 12 is a fine, detailed face -- a stroke
 * thick enough to see clogs its counters and thin joins into mud, which read
 * as illegible rather than bold. A single soft shadow does the legibility
 * work instead, same as the header banner. */
.passcode-gate__title-line {
  font-family: var(--font-fantasy);
  font-size: 30px;
  line-height: 1.1;
  letter-spacing: 0.02em;
  white-space: nowrap;
  color: #ffffff;
  filter: drop-shadow(0 2px 4px rgba(10, 14, 24, 0.75));
}

.passcode-gate__field {
  width: 100%;
}

/* The one surface left that still reads as "glass" -- everything else on
 * this screen is bare text on the photo, but a text input needs some visual
 * edge or it doesn't read as interactive at all. Kept as small a footprint
 * as a field can be: a notched pill, not a card. */
.passcode-gate__input {
  width: 100%;
  padding: var(--s3);
  font-family: var(--font-display);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-align: center;
  color: #fbfeff;
  background: rgba(255, 255, 255, 0.16);
  backdrop-filter: blur(10px) saturate(160%);
  -webkit-backdrop-filter: blur(10px) saturate(160%);
  border: 0;
  clip-path: polygon(
    var(--notch-sm) 0, calc(100% - var(--notch-sm)) 0,
    100% var(--notch-sm), 100% calc(100% - var(--notch-sm)),
    calc(100% - var(--notch-sm)) 100%, var(--notch-sm) 100%,
    0 calc(100% - var(--notch-sm)), 0 var(--notch-sm)
  );
  box-shadow:
    inset 0 0 0 2px rgba(255, 255, 255, 0.6),
    0 6px 16px rgba(10, 14, 24, 0.28);
  transition: box-shadow 140ms var(--ease), background 140ms var(--ease);
}

.passcode-gate__input::placeholder {
  color: rgba(255, 255, 255, 0.88);
  text-shadow: 0 1px 2px rgba(20, 30, 45, 0.4);
}

.passcode-gate__input:focus {
  outline: none;
  box-shadow:
    inset 0 0 0 2px var(--cta-bright),
    0 6px 16px rgba(10, 14, 24, 0.28);
}

.passcode-gate__error {
  font-size: 9px;
  letter-spacing: 0.04em;
  color: #ffb4a8;
  filter: drop-shadow(0 1px 2px rgba(20, 30, 45, 0.5));
}

/* ---------------------------------------------------------------- notes */

.note-marker {
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  line-height: 0;
  color: var(--gold-deep);
  background: var(--well);
  box-shadow: inset 0 0 0 2px var(--orn);
  transition: color 140ms var(--ease), box-shadow 140ms var(--ease), transform 140ms var(--ease);
}

.note-marker:hover,
.note-marker[aria-expanded='true'] {
  box-shadow: inset 0 0 0 2px var(--gold);
  transform: translateY(-1px);
}

.note-marker .sprite {
  /* Dimmed until hovered, so a column of markers is not a row of bright dots. */
  opacity: 0.72;
  transition: opacity 140ms var(--ease);
}

.note-marker:hover .sprite,
.note-marker[aria-expanded='true'] .sprite {
  opacity: 1;
}

/* Mounted on <body>, so it needs its own frame rather than inheriting a panel's. */
.popover {
  position: fixed;
  z-index: 70;
  width: max-content;
  max-width: min(280px, calc(100vw - 24px));
  padding: var(--fw);
  background: linear-gradient(var(--edge-bright), var(--edge) 38%, var(--edge-dim));
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 120ms var(--ease), transform 120ms var(--ease);
  pointer-events: none;
  clip-path: polygon(
    6px 0, calc(100% - 6px) 0, 100% 6px, 100% calc(100% - 6px),
    calc(100% - 6px) 100%, 6px 100%, 0 calc(100% - 6px), 0 6px
  );
  box-shadow: 0 10px 26px rgba(3, 5, 10, 0.7);
}

.popover.is-open {
  opacity: 1;
  transform: none;
}

.popover.is-below {
  transform: translateY(-4px);
}

.popover.is-below.is-open {
  transform: none;
}

.popover__label {
  display: block;
  padding: var(--s2) var(--s3) 0;
  font-family: var(--font-display);
  font-size: 7px;
  letter-spacing: 0.14em;
  color: var(--gold);
  background: var(--panel);
}

.popover__text {
  padding: var(--s1) var(--s3) var(--s3);
  font-size: 9px;
  line-height: 1.9;
  color: var(--ink);
  background: var(--panel);
}

/* ---------------------------------------------------------- hover polish
 *
 * Two rules across every card:
 *
 *  1. Interactive cards move; informational cards only brighten. A lift on a
 *     card you cannot click is a promise the UI does not keep.
 *  2. Portraits and icons brighten rather than scale. These are pixel art, and a
 *     non-integer scale resamples the grid into mush -- brightness keeps every
 *     pixel exactly where it was.
 */

.ladder-row,
.game-card {
  transition:
    transform 160ms var(--ease),
    filter 160ms var(--ease);
}

.ladder-row .panel__in,
.game-card .panel__in,
.match-card .panel__in {
  transition: box-shadow 160ms var(--ease), background-color 160ms var(--ease);
}

.portrait,
.game-card__icon,
.world-hero__crest {
  transition: filter 180ms var(--ease), box-shadow 180ms var(--ease);
}

/* Interactive cards lift and warm their accent edge.
 *
 * Deliberately no `filter` on the card wrapper: the wrapper paints the gold
 * frame, and brightening it made the frame flash yellow on every hover. The lift
 * plus an accent inner edge plus a brighter portrait is the same read without
 * touching the gold. */
.ladder-row:hover,
.game-card:hover {
  transform: translateY(-3px);
}

.ladder-row:hover .panel__in,
.game-card:hover .panel__in {
  background-color: var(--panel-2);
  box-shadow:
    inset 0 0 0 2px color-mix(in srgb, var(--accent) 42%, transparent),
    inset 0 2px 0 var(--bevel),
    inset 0 -2px 0 var(--bevel-dim);
}

/* The filigree comes up to full strength on hover -- the only thing about the
 * gold that changes, and it reads as the frame catching the light. */
.ladder-row:hover .panel__in::after,
.game-card:hover .panel__in::after {
  opacity: 1;
}

.ladder-row:hover .portrait,
.game-card:hover .game-card__icon {
  filter: brightness(1.12);
}

/* A card you can click should acknowledge the click. */
.ladder-row:active,
.game-card:active {
  transform: translateY(-1px);
}

/* Informational: no movement, just a slightly warmer frame. */
.match-card:hover .panel__in {
  box-shadow:
    inset 0 0 0 2px color-mix(in srgb, var(--accent) 26%, transparent),
    inset 0 2px 0 var(--bevel),
    inset 0 -2px 0 var(--bevel-dim);
}

.match-card:hover .match-card__game-name {
  color: color-mix(in srgb, var(--accent) 100%, white 14%);
}

.stat-tile {
  transition: filter 160ms var(--ease);
}

.stat-tile:hover {
  filter: brightness(1.05);
}

/* Rows inside a panel: a left accent bar wipes in, which reads as a row
 * highlight without shifting any text. */
.standings__row,
.mastery__row,
.h2h__row {
  position: relative;
  transition: background-color 140ms var(--ease);
}

.standings__row::before,
.mastery__row::before,
.h2h__row::before {
  content: '';
  position: absolute;
  left: calc(var(--s3) * -1);
  top: 2px;
  bottom: 2px;
  width: 3px;
  background: var(--accent, var(--gold));
  transform: scaleY(0);
  transform-origin: center;
  transition: transform 160ms var(--ease);
}

.standings__row:hover,
.mastery__row:hover,
.h2h__row:hover {
  background: rgba(255, 255, 255, 0.028);
}

.standings__row:hover::before,
.mastery__row:hover::before,
.h2h__row:hover::before {
  transform: scaleY(1);
}

.standings__row:hover .portrait {
  filter: brightness(1.12);
}

/* Tabs get the same easing as everything else. */
.nav__item,
.note-marker {
  transition:
    color 140ms var(--ease),
    background-color 140ms var(--ease),
    box-shadow 140ms var(--ease),
    transform 140ms var(--ease);
}

/* ---------------------------------------------------------------- data card */

/* Clearing and restoring the sample season, at the foot of the history tab. */
.data-card > .panel__in {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s4);
  flex-wrap: wrap;
}

.data-card__note {
  max-width: 46ch;
  font-size: 9px;
  line-height: 1.9;
  color: var(--ink-faint);
}

/* ---------------------------------------------------------------- vault */

/* The room a character sheet opens on. Full-bleed out of .view's centred,
 * max-width column via the 50%/50vw breakout, plus a negative top margin
 * cancelling .view's own top padding so the scene starts flush under the
 * header. */
.vault {
  position: relative;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-top: calc(var(--s6) * -1);
  /* svh tracks the *stable* viewport, so the room does not resize by the
   * height of the mobile URL bar as it hides on scroll. */
  /* One screen tall, measured from under the sticky header -- see
   * measureHeader() in app.js for why the offset cannot be a constant. */
  height: calc(100vh - var(--topbar-h, 0px) - var(--bottombar-h, 0px));
  height: calc(100svh - var(--topbar-h, 0px) - var(--bottombar-h, 0px));
  overflow: hidden;
  background: #06080d;
}

/* The scene's coordinate space, and the reason this works.
 *
 * Locked to the artwork's own aspect ratio and scaled until it covers the
 * viewport, anchored to the bottom so the floor is always in frame -- a tall
 * room loses its ceiling on a wide screen, which is the right thing to lose.
 * The figure inside is positioned as a percentage of THIS box, so it stays on
 * the flagstones at any window size or aspect. Position it against the
 * viewport instead and it slides off. */
.vault__stage {
  position: absolute;
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  aspect-ratio: var(--ar);
  width: max(100%, calc((100svh - var(--topbar-h, 0px) - var(--bottombar-h, 0px)) * var(--ar)));
}

.vault__art {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: fill;
  /* The rooms are pixel art: smooth upscaling turns mortar lines and torch
   * flames to mush. */
  image-rendering: pixelated;
}

/* The character: large, centred on the whole scene, and the same size in
 * every room regardless of that room's own floor or perspective -- this is a
 * character portrait the room sets a mood for, not a figure standing at a
 * specific spot in it. Sized in vh so it scales with the room's own height
 * rather than the page's width, with px bounds so it neither vanishes on a
 * short window nor swamps a tall narrow one.
 *
 * Anchored from the *bottom* of the room rather than centred: the text block
 * above (name, epithet, form line, stats, and an optional trophy row) is a
 * different height on every player and in every language, and centring the
 * figure on a fixed midpoint meant it only cleared that text by luck. Pinned
 * to the floor instead, the figure's position never depends on how tall the
 * copy above it happens to be -- shrinking the figure (as the phone override
 * below does) only ever opens *more* headroom, never less. */
.vault__figure {
  position: absolute;
  /* Above the background, below the shade and the readable content -- the
   * shade's dark bands still need to read over the character at the very top
   * and bottom of the scene, and the name/stats must never be fought for
   * legibility against the figure passing behind them. */
  z-index: 1;
  left: 50%;
  bottom: 3%;
  width: clamp(160px, min(38vh, 54vw), 460px);
  aspect-ratio: 1;
  overflow: hidden;
  /* --nudge (set per-player in rooms.js) recentres art whose bounding box
   * isn't where its body actually reads as centred -- a held weapon can pull
   * the square canvas's midpoint off to one side. Percentages here are of
   * this box's own width, same as the base -50%, so the correction scales
   * with the figure at every viewport size instead of needing its own units. */
  transform: translateX(calc(-50% + var(--nudge, 0%)));
  filter: drop-shadow(0 10px 14px rgba(0, 0, 0, 0.65));
}

.vault__figure-strip {
  image-rendering: pixelated;
  /* Rests on the strip's own first frame (south-facing) rather than turning --
   * .portrait__strip's steps(8) animation is turned back off here. */
  animation: none;
}

/* Legibility layer. The rooms are busy and vary from sunlit to near-black, so
 * the copy gets its own ground: darker at top and bottom, clear through the
 * middle where the character stands. */
.vault__shade {
  position: absolute;
  z-index: 2;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(180deg, rgba(5, 8, 14, 0.92) 0%, rgba(5, 8, 14, 0.7) 22%,
      rgba(5, 8, 14, 0.34) 38%, transparent 54%),
    linear-gradient(0deg, rgba(5, 8, 14, 0.9) 0%, rgba(5, 8, 14, 0.4) 14%, transparent 30%);
}

/* The live layer: name and read-outs at the top, the room's own middle and
 * foot left clear for the character. */
.vault__inner {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  max-width: var(--shell);
  margin: 0 auto;
  padding: var(--s6) var(--s5) 0;
  text-align: center;
  pointer-events: none;
}

/* Only the controls take pointer events back, so the room stays clickable-
 * through where there is nothing to click. */
.vault__scroll {
  pointer-events: auto;
}

.vault__head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s2);
}

.vault__crest {
  display: flex;
  align-items: center;
  gap: var(--s2);
}

.vault__rank {
  font-family: var(--font-display);
  font-size: 9px;
  letter-spacing: 0.14em;
  color: var(--gold);
  text-shadow: 0 2px 5px rgba(0, 0, 0, 0.95);
}

/* The name carries the scene, so it gets the display face at hero scale. Flat
 * gold with a soft shadow, matching the app's title treatment -- Jacquard is
 * fine enough that an outline clogs it. */
.vault__name {
  font-family: var(--font-fantasy);
  font-size: clamp(44px, 8vw, 88px);
  line-height: 1;
  letter-spacing: 0.02em;
  color: var(--hero);
  filter: drop-shadow(0 3px 7px rgba(0, 0, 0, 0.9));
}

.vault__epithet {
  font-family: var(--font-display);
  font-size: 11px;
  letter-spacing: 0.16em;
  /* Mixed toward cream: several of these accents are dark enough to disappear
   * against a night-lit room on their own. */
  color: color-mix(in srgb, var(--accent) 55%, #ffeccc);
  text-shadow: 0 2px 5px rgba(0, 0, 0, 0.95);
}

.vault__form {
  max-width: 34ch;
  font-size: 8px;
  line-height: 1.9;
  /* --ink-dim is tuned for a panel, and vanishes over a sunlit floor. */
  color: color-mix(in srgb, var(--ink) 82%, transparent);
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.95);
}

/* The only thing telling the reader the page continues, so it is a real
 * control rather than a decorative chevron. */
.vault__scroll {
  position: absolute;
  z-index: 3;
  left: 50%;
  bottom: var(--s3);
  transform: translateX(-50%);
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s1);
  padding: var(--s2) var(--s4) 0;
  font-family: var(--font-display);
  font-size: 8px;
  letter-spacing: 0.16em;
  color: var(--ink-faint);
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.95);
  transition: color 120ms var(--ease);
}

.vault__scroll:hover {
  color: var(--gold);
}

.vault__scroll-arrow {
  font-size: 9px;
  animation: vault-beckon 2.4s ease-in-out infinite;
}

@keyframes vault-beckon {
  0%, 100% { transform: translateY(0); opacity: 0.5; }
  50% { transform: translateY(3px); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .vault__scroll-arrow {
    animation: none;
    opacity: 0.85;
  }
}
