/* ==========================================================================
   Community: leaderboard, profile, credits
   ========================================================================== */

.community-page { padding: var(--page-top) 0 var(--section-gap); }
.leaderboard-shell { max-width: calc(860px + 2 * var(--gutter-page)); padding-inline: var(--gutter-page); }
.profile-shell { max-width: calc(var(--col-wide) + 2 * var(--gutter-page)); padding-inline: var(--gutter-page); }

/* Shared by the leaderboard's labels and the profile's groups: the card
   label role, for secondary labels on glass. */
.side-label,
.main-label,
.shelf-subhead,
.detail-list dt,
.rank-figures dt {
  display: block;
  margin: 0;
  font: 700 .6875rem/1.4 var(--font);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-2);
}

/* ==========================================================================
   Rank marks
   --------------------------------------------------------------------------
   Every rank colour arrives from lib/levels.js as --rank-colour on the
   element itself. The hue and the tier are identity data; only the shape is
   the site's. The label's rank share is measured on glass against every
   milestone colour: 38% in dark, 30% in light (worst 5.16:1 and 5.06:1).
   ========================================================================== */

.rank-chip {
  --rank-ink-share: 38%;
  display: inline-flex;
  align-items: center;
  padding: 4px 11px;
  border-radius: var(--r-pill);
  font: 700 var(--step--1)/1.3 var(--font);
  letter-spacing: 0;
  white-space: nowrap;
  color: color-mix(in srgb, var(--rank-colour) var(--rank-ink-share), var(--text));
  background: color-mix(in srgb, var(--rank-colour) 10%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--rank-colour) 40%, transparent);
}

/* Tiers (lib/levels.js tierForLevel): flat adds nothing, gradient a two-stop
   fill, aura a halo in the rank's own colour, boundless a white halo that
   breathes. --rank-edge and --rank-glow lean toward white in dark and toward
   --text in light, where a white halo on a white page is nothing. Hosts set
   the halo size, so a table chip and the account badge share one recipe. */
.rank-mark {
  --rank-edge: color-mix(in srgb, var(--rank-colour) 72%, #fff);
  --rank-glow: var(--rank-colour);
  --rank-halo-blur: 14px;
  --rank-halo-share: 38%;
  --rank-fill-top: 30%;
  --rank-fill-mid: 13%;
  --rank-fill-stop: 64%;
  --rank-fill-boost: 1;
  position: relative;
}
:root[data-theme='light'] .rank-chip { --rank-ink-share: 30%; }
:root[data-theme='light'] .rank-mark {
  --rank-edge: color-mix(in srgb, var(--rank-colour) 72%, var(--text));
  --rank-glow: color-mix(in srgb, var(--rank-colour) 55%, var(--text));
}
/* Up to twenty-five chips per board: a halved halo keeps one lit row from
   reading as a lit table, and a lighter fill keeps the label legible over
   the lit edge (the white top tier measured 3.9:1 at full fill). */
.rank-chip.rank-mark {
  --rank-halo-blur: 9px;
  --rank-halo-share: 26%;
  --rank-fill-top: 20%;
  --rank-fill-mid: 9%;
}
/* The account badge is about fifteen chips in area, so its wash is cut back
   and the rim and halo carry the tier. */
.rank-badge.rank-mark {
  --rank-fill-top: 22%;
  --rank-fill-mid: 9%;
  --rank-fill-stop: 26%;
  --rank-halo-blur: 18px;
  --rank-halo-share: 34%;
}
.rank-mark[data-rank-tier='gradient'],
.rank-mark[data-rank-tier='aura'],
.rank-mark[data-rank-tier='boundless'] {
  background-color: transparent;
  background-image: linear-gradient(152deg,
    color-mix(in srgb, var(--rank-edge) calc(var(--rank-fill-top) * var(--rank-fill-boost)), transparent),
    color-mix(in srgb, var(--rank-colour) calc(var(--rank-fill-mid) * var(--rank-fill-boost)), transparent) var(--rank-fill-stop),
    transparent calc(var(--rank-fill-stop) + 30%));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--rank-glow) 56%, transparent);
}
.rank-mark[data-rank-tier='aura'],
.rank-mark[data-rank-tier='boundless'] {
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--rank-glow) 66%, transparent),
    0 0 0 1px color-mix(in srgb, var(--rank-glow) 26%, transparent),
    0 0 var(--rank-halo-blur) color-mix(in srgb, var(--rank-glow) var(--rank-halo-share), transparent);
}
.rank-mark[data-rank-tier='boundless'] { --rank-fill-boost: 1.4; --rank-halo-share: 52%; }
/* The breathing halo is its own layer so only opacity moves. inset 0 with an
   inherited radius needs no overflow clip, which would slice the halo. */
.rank-mark[data-rank-tier='boundless']::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  box-shadow: 0 0 calc(var(--rank-halo-blur) * 1.3) 2px color-mix(in srgb, var(--rank-glow) var(--rank-halo-share), transparent);
  opacity: .35;
}
@keyframes rank-breathe {
  0%, 100% { opacity: .35; }
  50% { opacity: 1; }
}
@media (prefers-reduced-motion: no-preference) {
  .rank-mark[data-rank-tier='boundless']::before { animation: rank-breathe 6s var(--ease) infinite; }
}

.rank-badge {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  min-width: 132px;
  padding: 18px 20px;
  border-radius: var(--r-control);
  background: color-mix(in srgb, var(--rank-colour) 12%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--rank-colour) 45%, transparent);
  text-align: center;
}
.rank-level {
  font: 700 var(--step-3)/1 var(--font);
  letter-spacing: var(--track-h1);
  font-variant-numeric: tabular-nums;
  color: color-mix(in srgb, var(--rank-colour) 55%, var(--text));
}
:root[data-theme='light'] .rank-level { color: color-mix(in srgb, var(--rank-colour) 38%, var(--text)); }
.rank-name { font: 600 var(--step--1)/1.3 var(--font); color: var(--text-soft); }
.rank-perks { display: flex; flex-wrap: wrap; gap: 8px; }

/* ==========================================================================
   Leaderboard
   ========================================================================== */

.leaderboard-controls {
  display: grid;
  justify-items: start;
  gap: 12px;
  margin: 0 0 20px;
}
/* Each rail's thumb travels between boards as its own view-transition
   group (timing in controls.css). */
.leaderboard-category .period-tab.is-active { view-transition-name: segment-category; }
.leaderboard-period .period-tab.is-active { view-transition-name: segment-period; }
/* On a phone the period track scrolls sideways and the current window can
   start out of view; where supported, the track opens on it. scroll-snap
   is added so a manual swipe along the track rests on a tab's edge rather
   than an arbitrary offset; verified live (Chrome 153) that it does NOT
   change where scroll-initial-target itself lands on load (that computation
   ignores scroll-snap-type, scroll-padding and scroll-margin alike here),
   so the initial slice this was meant to fix is unchanged and would need a
   small onload scrollLeft correction in site.js (U1) to close fully. */
.leaderboard-controls .period-tab.is-active { scroll-initial-target: nearest; }
.leaderboard-controls .period-tabs { scroll-snap-type: x proximity; }
.leaderboard-controls .period-tab { scroll-snap-align: start; }

.discord-icon { width: 18px; height: 18px; flex-shrink: 0; }
.join-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 14px;
  margin: 0;
}
.join-note {
  max-width: 34ch;
  font: 400 var(--step--1)/1.45 var(--font);
  color: var(--ink-2);
}

/* One group, rows 56px, inset hairlines, no zebra. The table can scroll
   sideways inside it on a narrow phone, so the group is the scroller. */
.leaderboard-scroll {
  margin: 0;
  border: 0;
  overflow-x: auto;
  border-radius: var(--r-control);
  background: var(--mat-neutral);
  box-shadow: var(--glass-rim);
}
.leaderboard {
  width: 100%;
  margin: 0;
  border-collapse: collapse;
  font: 400 var(--step-0s)/1.45 var(--font);
  color: var(--text-soft);
}
.leaderboard th {
  padding: 14px 16px 10px;
  border: 0;
  background: none;
  text-align: left;
  vertical-align: bottom;
  white-space: nowrap;
  font: 700 .6875rem/1.4 var(--font);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-2);
}
.leaderboard td {
  height: 56px;
  padding: 8px 16px;
  border: 0;
  vertical-align: middle;
  text-align: left;
  box-shadow: inset 0 1px 0 var(--hairline);
}
.leaderboard .col-number { text-align: right; font-variant-numeric: tabular-nums; }
.leaderboard td.col-number { color: var(--text); }
.leaderboard .col-position { width: 3.25rem; padding-right: 0; }
.leaderboard td.col-position {
  font: 700 var(--step-0s)/1 var(--font);
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
}
/* The top three wear the eyebrow ink, whose share is measured for the
   ground; on this glass dark needs 56% (boundless measured 3.91:1 at 68%). */
.leaderboard td.col-position.is-top-rank { color: color-mix(in srgb, var(--tint) 56%, var(--text)); }
:root[data-theme='light'] .leaderboard td.col-position.is-top-rank { color: var(--eyebrow-ink); }
.leaderboard tbody tr:has(a) td { transition: background var(--dur-chip) linear; }
.leaderboard tbody tr:has(a):hover td { background: var(--row-hover); }

.leaderboard-reader {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}
/* The creator panel reuses a.leaderboard-reader with its own inner markup,
   so the hover underline stays on the anchor. */
a.leaderboard-reader { color: inherit; text-decoration: none; }
a.leaderboard-reader:hover { text-decoration: underline; text-underline-offset: 3px; }
.leaderboard-identity { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
/* A Discord name may be 32 characters, and an auto table sizes a column by
   its widest cell: capped and allowed to break anywhere, a long name wraps
   inside its row instead of scrolling the whole board sideways. */
.leaderboard-username {
  max-width: 24ch;
  overflow-wrap: anywhere;
  font: 600 var(--step-0s)/1.3 var(--font);
  color: var(--text);
}
.leaderboard-meta {
  display: none;
  flex-wrap: wrap;
  column-gap: 10px;
  font: 500 var(--step--1)/1.35 var(--font);
  color: var(--ink-2);
}
.leaderboard-meta > * { white-space: nowrap; }
.rank-none { font: 500 var(--step--1)/1.3 var(--font); color: var(--ink-2); }

/* 32px circles. A letter tile when Discord gave no picture. */
.leaderboard .avatar-sm {
  flex: none;
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  font: 700 var(--step--1)/1 var(--font);
  color: var(--ink-2);
  background: var(--mat-etched);
  box-shadow: var(--rim-etched);
}
/* A picture that fails to load (a Discord avatar gone stale) shows only its
   background, because every avatar img carries alt="". The glyph matches
   public/img/default-avatar.svg exactly, so the default picture lands on it
   without a double edge. Ink is the --ink-2 hex of each theme. */
.leaderboard .avatar-photo,
.profile-avatar {
  object-fit: cover;
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 96 96' fill='%239ea4b8'%3E%3Ccircle cx='48' cy='38' r='16'/%3E%3Cpath d='M18,86 C18,64 32,54 48,54 C64,54 78,64 78,86 Z'/%3E%3C/svg%3E") center / 100% no-repeat,
    var(--mat-etched);
}
:root[data-theme='light'] :is(.leaderboard .avatar-photo, .profile-avatar) {
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 96 96' fill='%23595f74'%3E%3Ccircle cx='48' cy='38' r='16'/%3E%3Cpath d='M18,86 C18,64 32,54 48,54 C64,54 78,64 78,86 Z'/%3E%3C/svg%3E") center / 100% no-repeat,
    var(--mat-etched);
}
/* A failed image renders its pseudo-elements (a loaded one never does), so
   this layer covers the browser's broken-image icon with the same glyph. */
.leaderboard .avatar-photo,
.profile-avatar { position: relative; }
.leaderboard .avatar-photo::after,
.profile-avatar::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: inherit;
  background-color: var(--on-glass-solid);
}

.table-note {
  max-width: 66ch;
  margin: 14px 4px 0;
  font: 400 var(--step--1)/1.55 var(--font);
  color: var(--ink-2);
}
.leaderboard-shell .pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  margin: 20px 0 0;
}
.leaderboard-shell .pagination .field-hint {
  margin: 0;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}

/* Empty board: a group holding the shared empty state. */
.leaderboard-empty {
  border-radius: var(--r-group);
  background: var(--mat-neutral);
  box-shadow: var(--glass-rim);
  overflow: clip;
}

/* Six columns cannot fit a phone by any arrangement, so rank and level move
   into the reader cell as one quiet line (never both on at once, so no fact
   is read twice) and the board keeps position, reader and the figure it is
   ranked by. XP stays rather than level: on a period board it is what
   explains the order. */
@media (max-width: 759.98px) {
  .leaderboard-discord .col-rank,
  .leaderboard-discord .col-level,
  .leaderboard-discord .col-messages { display: none; }
  .leaderboard-meta { display: flex; }
  .leaderboard th,
  .leaderboard td { padding-inline: 12px; }
  .leaderboard .col-position { width: 2.5rem; padding-right: 0; }
  .leaderboard td { height: 60px; }
  .leaderboard-identity { max-width: 16ch; }
}

/* A reader's own font and effect for their display name, drawn by
   partials/styled-name.ejs wherever a name renders (profile, leaderboard,
   account, private stub, header button). Identity data: the values arrive
   resolved from lib/name-style.js. The span carries no size of its own, so
   every effect is sized in em and reads the same at 13px and at 28px. */
.name-styled { display: inline; }

.name-styled[data-name-font='bold'] { font-weight: 800; }
.name-styled[data-name-font='condensed'] { font-stretch: 82%; letter-spacing: -0.01em; }
.name-styled[data-name-font='wide'] { letter-spacing: 0.09em; text-transform: uppercase; }
.name-styled[data-name-font='italic'] { font-style: italic; }
.name-styled[data-name-font='smallcaps'] { font-variant-caps: all-small-caps; letter-spacing: 0.02em; }

/* The bespoke faces ship one static instance each (see fonts.css), so the
   weight is pinned to that instance rather than synthesised. */
.name-styled[data-name-font='bungee'] { font-family: 'Bungee', var(--font); font-weight: 400; }
.name-styled[data-name-font='fredoka'] { font-family: 'Fredoka Bespoke', var(--font); font-weight: 600; }
.name-styled[data-name-font='fraunces'] { font-family: 'Fraunces Bespoke', var(--font); font-weight: 600; }
.name-styled[data-name-font='caveat'] { font-family: 'Caveat Bespoke', var(--font); font-weight: 600; }
.name-styled[data-name-font='spacemono'] { font-family: 'Space Mono Bespoke', var(--mono); font-weight: 700; }
.name-styled[data-name-font='delagothic'] { font-family: 'Dela Gothic One', var(--font); font-weight: 400; }

.name-styled[data-name-font='fraunceswonk'] { font-family: 'Fraunces Wonk', var(--font); font-weight: 900; }
.name-styled[data-name-font='recursivewild'] { font-family: 'Recursive Wild', var(--font); font-weight: 1000; }
.name-styled[data-name-font='recursivemonocasual'] { font-family: 'Recursive Mono Casual', var(--mono); font-weight: 800; }

.name-styled[data-name-effect='solid'] { color: inherit; }

/* --name-share exists only inside .profile-card, so every read carries the
   measured 36% as its own fallback: an unresolved var() would invalidate
   the whole gradient and, with color: transparent, the text with it. */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .name-styled[data-name-effect='gradient'] {
    background-image: linear-gradient(100deg,
      color-mix(in srgb, var(--violet) var(--name-share, 36%), var(--text)) 0%,
      color-mix(in srgb, var(--blue) var(--name-share, 36%), var(--text)) 27.5%,
      color-mix(in srgb, var(--teal) var(--name-share, 36%), var(--text)) 50%,
      color-mix(in srgb, var(--blue) var(--name-share, 36%), var(--text)) 72.5%,
      color-mix(in srgb, var(--violet) var(--name-share, 36%), var(--text)) 100%);
    background-size: 200% 200%;
    background-position: 0% 0%;
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    -webkit-box-decoration-break: clone;
    box-decoration-break: clone;
  }
  @media (prefers-reduced-motion: no-preference) {
    .name-styled[data-name-effect='gradient'] {
      animation: name-gradient-roll 10s ease-in-out infinite alternate;
    }
    @keyframes name-gradient-roll {
      from { background-position: 0% 0%; }
      to   { background-position: 100% 100%; }
    }
  }
}

/* Local custom properties built from global tokens, so the effect resolves
   outside a profile card too. The stroke is in em because text-stroke draws
   centred on the outline and a px width only suits one size. */
.name-styled[data-name-effect='toon'] {
  --toon-fill: color-mix(in srgb, var(--blue) 82%, var(--text));
  --toon-ink: color-mix(in srgb, var(--indigo) 42%, var(--text));
  color: var(--toon-fill);
  text-shadow: 0.05em 0.06em 0 var(--toon-ink);
}
@supports (-webkit-text-stroke: 1px black) {
  .name-styled[data-name-effect='toon'] {
    -webkit-text-stroke: 0.045em var(--toon-ink);
  }
}
@media (prefers-reduced-motion: no-preference) {
  .name-styled[data-name-effect='toon'] {
    animation: name-toon-swing 8s linear infinite alternate;
  }
  @keyframes name-toon-swing {
    0% {
      text-shadow: 0.05em 0.06em 0 var(--toon-ink);
      animation-timing-function: ease-in;
    }
    50% {
      text-shadow: 0 0.078em 0 var(--toon-ink);
      animation-timing-function: ease-out;
    }
    100% {
      text-shadow: -0.05em 0.06em 0 var(--toon-ink);
    }
  }
}

@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .name-styled[data-name-effect='prism'] {
    --prism-share: var(--name-share, 36%);
    background-image: repeating-linear-gradient(100deg,
      color-mix(in srgb, var(--indigo) var(--prism-share), var(--text)) 0%,
      color-mix(in srgb, var(--violet) var(--prism-share), var(--text)) 7%,
      color-mix(in srgb, color-mix(in srgb, var(--violet), var(--blue)) var(--prism-share), var(--text)) 14%,
      color-mix(in srgb, var(--blue) var(--prism-share), var(--text)) 21%,
      color-mix(in srgb, color-mix(in srgb, var(--blue), var(--teal)) var(--prism-share), var(--text)) 28%,
      color-mix(in srgb, var(--teal) var(--prism-share), var(--text)) 35%,
      color-mix(in srgb, color-mix(in srgb, var(--teal), var(--indigo)) var(--prism-share), var(--text)) 42.5%,
      color-mix(in srgb, var(--indigo) var(--prism-share), var(--text)) 50%);
    background-size: 200% 200%;
    background-position: 0% 0%;
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    -webkit-box-decoration-break: clone;
    box-decoration-break: clone;
  }
  @media (prefers-reduced-motion: no-preference) {
    .name-styled[data-name-effect='prism'] {
      animation: name-prism-sweep 16s linear infinite;
    }
    @keyframes name-prism-sweep {
      from { background-position: 0% 0%; }
      to   { background-position: 100% 100%; }
    }
  }
}

.name-styled[data-name-effect='neon'] {
  --neon-core: color-mix(in srgb, var(--teal) 55%, white);
  --neon-glow: var(--teal);
  color: var(--neon-core);
  text-shadow:
    0 0 0.05em color-mix(in srgb, var(--neon-core) 85%, transparent),
    0 0 0.22em color-mix(in srgb, var(--neon-glow) 42%, transparent),
    0 0 0.48em color-mix(in srgb, var(--neon-glow) 22%, transparent);
}
/* A near-white core on a light page is a blank; lean toward --text. */
:root[data-theme='light'] .name-styled[data-name-effect='neon'] {
  --neon-core: color-mix(in srgb, var(--teal) 62%, var(--text));
  --neon-glow: color-mix(in srgb, var(--teal) 55%, var(--text));
}
@media (prefers-reduced-motion: no-preference) {
  .name-styled[data-name-effect='neon'] {
    animation: name-neon-breathe 6s ease-in-out infinite;
  }
  @keyframes name-neon-breathe {
    0%, 100% {
      text-shadow:
        0 0 0.05em color-mix(in srgb, var(--neon-core) 85%, transparent),
        0 0 0.22em color-mix(in srgb, var(--neon-glow) 42%, transparent),
        0 0 0.48em color-mix(in srgb, var(--neon-glow) 22%, transparent);
    }
    50% {
      text-shadow:
        0 0 0.05em color-mix(in srgb, var(--neon-core) 100%, transparent),
        0 0 0.22em color-mix(in srgb, var(--neon-glow) 62%, transparent),
        0 0 0.48em color-mix(in srgb, var(--neon-glow) 34%, transparent);
    }
  }
}

@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .name-styled[data-name-effect='gummy'] {
    --gummy-glint: color-mix(in srgb, var(--teal) 20%, #fff);
    --gummy-pool: color-mix(in srgb, var(--indigo) 65%, var(--violet));
    background-image: linear-gradient(180deg,
      color-mix(in srgb, var(--teal) 30%, #fff),
      color-mix(in srgb, var(--blue) 36%, var(--text)) 42%,
      color-mix(in srgb, var(--violet) 36%, var(--text)));
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    -webkit-box-decoration-break: clone;
    box-decoration-break: clone;
    text-shadow:
      0 -0.03em 0.06em var(--gummy-glint),
      0 0.07em 0.06em var(--gummy-pool);
  }
  @media (prefers-reduced-motion: no-preference) {
    .name-styled[data-name-effect='gummy'] {
      animation: name-gummy-sway 12s linear infinite;
    }
    @keyframes name-gummy-sway {
      0% {
        text-shadow:
          0 -0.03em 0.06em var(--gummy-glint),
          0 0.07em 0.06em var(--gummy-pool);
        animation-timing-function: ease-out;
      }
      25% {
        text-shadow:
          0.06em -0.03em 0.06em var(--gummy-glint),
          0 0.07em 0.06em var(--gummy-pool);
        animation-timing-function: ease-in;
      }
      50% {
        text-shadow:
          0 -0.03em 0.06em var(--gummy-glint),
          0 0.07em 0.06em var(--gummy-pool);
        animation-timing-function: ease-out;
      }
      75% {
        text-shadow:
          -0.06em -0.03em 0.06em var(--gummy-glint),
          0 0.07em 0.06em var(--gummy-pool);
        animation-timing-function: ease-in;
      }
      100% {
        text-shadow:
          0 -0.03em 0.06em var(--gummy-glint),
          0 0.07em 0.06em var(--gummy-pool);
      }
    }
  }
}

/* Inlined mixes, not the profile's --accent-ink: the partial renders
   outside a profile card too. */
.name-styled[data-name-effect='pop'] {
  --pop-ink: color-mix(in srgb, var(--teal) 62%, var(--text));
  --pop-shadow: color-mix(in srgb, var(--indigo) 74%, var(--text));
  color: var(--pop-ink);
  font-weight: 800;
  text-shadow:
    0.035em 0.035em 0 var(--pop-shadow),
    0.07em 0.07em 0 var(--pop-shadow),
    0.105em 0.105em 0 var(--pop-shadow),
    0.14em 0.14em 0 var(--pop-shadow);
}
:root[data-theme='light'] .name-styled[data-name-effect='pop'] {
  --pop-ink: color-mix(in srgb, var(--teal) 40%, var(--text));
}
@media (prefers-reduced-motion: no-preference) {
  .name-styled[data-name-effect='pop'] {
    animation: name-pop-press 7s ease-in-out infinite alternate;
  }
  @keyframes name-pop-press {
    from {
      text-shadow:
        0.035em 0.035em 0 var(--pop-shadow),
        0.07em 0.07em 0 var(--pop-shadow),
        0.105em 0.105em 0 var(--pop-shadow),
        0.14em 0.14em 0 var(--pop-shadow);
    }
    to {
      text-shadow:
        0.025em 0.025em 0 var(--pop-shadow),
        0.05em 0.05em 0 var(--pop-shadow),
        0.075em 0.075em 0 var(--pop-shadow),
        0.1em 0.1em 0 var(--pop-shadow);
    }
  }
  .btn:active .name-styled[data-name-effect='pop'],
  .name-style-swatch:active .name-styled[data-name-effect='pop'] {
    animation: none;
    text-shadow:
      0.0175em 0.0175em 0 var(--pop-shadow),
      0.035em 0.035em 0 var(--pop-shadow),
      0.0525em 0.0525em 0 var(--pop-shadow),
      0.07em 0.07em 0 var(--pop-shadow);
  }
}


/* Effects start their loops at first paint, so every reading-board row
   wearing the same one would move in lockstep. The view writes --i on each
   row (capped at 8), and a negative delay starts each a little further into
   its cycle. After the effect rules on purpose: their animation shorthand
   resets the delay, and the specificity ties. */
@media (prefers-reduced-motion: no-preference) {
  .leaderboard-username .name-styled { animation-delay: calc(var(--i, 0) * -1.1s); }
}

/* ==========================================================================
   Public profile  /u/:username
   --------------------------------------------------------------------------
   article.profile-card[data-accent] carries the owner's palette: tokens.css
   re-declares every palette-embedding token on [data-accent], so the banner
   glass, the avatar ring, the ladder, the tick and the marks all wear it and
   nothing else on the page does. The side column is unboxed text on the
   field; the main column is one featured card (the ladder) plus groups.
   ========================================================================== */

.profile-card {
  /* Measured ceilings (worst point across ten accents): the name gradient
     share, the role label share and the aura tag glyph share. */
  --name-share: 36%;
  --role-share: 42%;
  --aura-ink-share: 70%;
  /* The ink the identity marks draw from: tick, badges, tag marks. */
  --accent-ink: color-mix(in srgb, var(--teal) 62%, var(--text));
  position: relative;
  max-width: var(--col-wide);
  margin: 0 auto;
  /* A size container for the main column's two-up layout; sticky still
     works inside one. */
  container-type: inline-size;
}
:root[data-theme='light'] .profile-card {
  --accent-ink: color-mix(in srgb, var(--teal) 40%, var(--text));
  --role-share: 36%;
  --aura-ink-share: 50%;
}

/* ---- Banner ------------------------------------------------------------
   4 / 1 is a contract with the crop tool (BANNER_W / BANNER_H in site.js,
   1440 x 360): a stored banner framed at 4:1 must meet a 4:1 box or
   object-fit crops what the owner centred. Without an image, the owner's
   accent glass. */
.profile-banner {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 1;
  border-radius: var(--r-card);
  overflow: clip;
  outline: 1px solid var(--hairline);
  outline-offset: -1px;
  background: var(--mat-card);
  box-shadow: var(--glass-rim);
}
.profile-banner-image {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* With an image the glass moves onto the image's ::after, which only a
   failed image renders, so a banner that 404s looks like one with no
   upload. Chrome still paints its broken-image icon over that layer here;
   site.js marks a failed image .is-broken, which hides it outright. */
.profile-banner.has-image { background: none; }
.profile-banner-image::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--mat-card);
  box-shadow: var(--glass-rim);
}
.profile-banner-image.is-broken { visibility: hidden; }
.profile-banner.has-image:has(> .is-broken) { background: var(--mat-card); }

/* ---- Columns ------------------------------------------------------------ */
.profile-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 24px;
}
.profile-side { min-width: 0; padding-inline: 16px; }
.profile-side-inner { position: relative; }
.profile-main {
  min-width: 0;
  display: grid;
  gap: 16px;
  align-content: start;
}
@media (min-width: 900px) {
  .profile-body { grid-template-columns: 300px minmax(0, 1fr); column-gap: 32px; }
  .profile-main { padding-top: 24px; }
  /* is-sticky is added by site.js (initProfileRail) only when the rail fits
     the viewport, since a stuck element taller than the viewport cannot be
     scrolled into view. top adds the avatar's 48px overhang, which that
     script also reads from the avatar's margin. */
  .profile-side-inner.is-sticky {
    position: sticky;
    top: calc(var(--header-height, var(--header-clear)) + 20px + 48px);
  }
  /* At this width the column is fixed (not stacked with .main-group), so the
     full-width span below only misaligns the card from the name/handle.
     A third class (not just .profile-side .side-stats) is needed to outrank
     the unconditional -16px rule below at equal specificity, since it comes
     later in the file regardless of which media query is active. */
  .profile-body .profile-side .side-stats { margin-inline: 0; }
}
/* Wide enough, the rank detail and the library sit side by side; without a
   rank the library keeps the full width. */
@container (min-width: 960px) {
  .profile-main { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .profile-main > :is(.ladder, .profile-note, .main-group-library, .main-group-shelf) { grid-column: 1 / -1; }
  .profile-main > .main-group-rank + .main-group-library { grid-column: auto; }
}

/* ---- Avatar -------------------------------------------------------------
   The ring is the owner's selection gradient; the 3px cut-out between it
   and the picture is the opaque stand-in for glass, and it doubles as the
   disc behind the transparent default silhouette. Positioned so it paints
   over the banner it overlaps. */
.profile-avatar-wrap {
  position: relative;
  z-index: 1;
  width: max-content;
  margin-top: -48px;
  padding: 3px;
  border-radius: 50%;
  background: var(--grad-select);
  box-shadow: 0 6px 17px rgb(0 0 0 / .35);
}
.profile-avatar {
  display: block;
  box-sizing: border-box;
  width: 96px;
  height: 96px;
  border: 3px solid var(--on-glass-solid);
  border-radius: 50%;
  background-color: var(--on-glass-solid);
}

/* ---- Name, handle ------------------------------------------------------- */
.profile-name {
  margin: 14px 0 0;
  font: 700 1.75rem/1.15 var(--font);
  letter-spacing: -0.02em;
  color: var(--text);
  overflow-wrap: anywhere;
}
.profile-handle {
  margin: 2px 0 0;
  font: 500 var(--step-0s)/1.4 var(--font);
  color: var(--ink-2);
}
.profile-edit-link { margin-top: 14px; }
.profile-private-hint {
  margin: 10px 0 0;
  font: 500 var(--step--1)/1.4 var(--font);
  color: var(--ink-2);
}

/* ---- Verified tick ------------------------------------------------------
   An eight-lobed seal: a polar rosette r = R + A cos(8t) sampled at its
   extrema, joined with cubic segments from the curve's own derivative. The
   seal is a live gradient masked by that shape, so it answers to the theme
   and the accent; the check is stroked in the opaque stand-in for glass. The
   creator panel's .admin-check draws the same badge at a fixed px size. */
.profile-tick,
.admin-check {
  --seal-shape: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 1.13C13.42 1.13 14.42 3.01 15.53 3.47C16.65 3.93 18.68 3.31 19.69 4.31C20.69 5.32 20.07 7.35 20.53 8.47C20.99 9.58 22.87 10.58 22.87 12C22.87 13.42 20.99 14.42 20.53 15.53C20.07 16.65 20.69 18.68 19.69 19.69C18.68 20.69 16.65 20.07 15.53 20.53C14.42 20.99 13.42 22.87 12 22.87C10.58 22.87 9.58 20.99 8.47 20.53C7.35 20.07 5.32 20.69 4.31 19.69C3.31 18.68 3.93 16.65 3.47 15.53C3.01 14.42 1.13 13.42 1.13 12C1.13 10.58 3.01 9.58 3.47 8.47C3.93 7.35 3.31 5.32 4.31 4.31C5.32 3.31 7.35 3.93 8.47 3.47C9.58 3.01 10.58 1.13 12 1.13Z'/%3E%3C/svg%3E");
  --seal-ink: var(--on-glass-solid);
  --seal-tone: var(--accent-ink, var(--teal));
  position: relative;
  display: inline-grid;
  place-items: center;
  isolation: isolate;
}
.profile-tick::before,
.admin-check::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  -webkit-mask: var(--seal-shape) center / contain no-repeat;
          mask: var(--seal-shape) center / contain no-repeat;
  background-image: linear-gradient(145deg,
    color-mix(in srgb, var(--violet) 46%, var(--seal-tone)),
    var(--seal-tone) 52%,
    color-mix(in srgb, var(--teal) 38%, var(--seal-tone)));
}
.profile-tick svg,
.admin-check svg {
  position: relative;
  z-index: 1;
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.profile-tick path,
.admin-check path {
  fill: none;
  stroke: var(--seal-ink);
  stroke-width: 2.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* A length, not the baseline keyword: an inline-grid box with no text of its
   own has no defined baseline, and engines disagree on the fallback. */
.profile-tick {
  width: .76em;
  height: .76em;
  margin-left: .16em;
  vertical-align: -0.06em;
}

/* ---- Role marks ---------------------------------------------------------
   Capsules in the rank chip's shape. --role-tone (and --role-tone-2 for a
   filled two-stop role) is written inline from role_definitions, so a role
   made in the creator panel draws with no CSS of its own; the glyph, not the
   tint, is what tells roles apart on mono and void. */
.role-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 8px;
  margin: 14px 0 0;
  padding: 0;
  list-style: none;
}
.role-mark {
  --role-tone: var(--violet);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 11px 4px 9px;
  border-radius: var(--r-pill);
  font: 700 var(--step--1)/1.3 var(--font);
  letter-spacing: 0;
  white-space: nowrap;
  color: color-mix(in srgb, var(--role-tone) var(--role-share, 42%), var(--text));
  background: color-mix(in srgb, var(--role-tone) 10%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--role-tone) 40%, transparent);
  transition: background var(--dur-chip) linear, box-shadow var(--dur-chip) linear;
}
:root[data-theme='light'] .role-mark { color: color-mix(in srgb, var(--role-tone) var(--role-share, 36%), var(--text)); }
.role-mark svg { flex: none; width: 13px; height: 13px; fill: currentColor; }
.role-mark:hover {
  background: color-mix(in srgb, var(--role-tone) 16%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--role-tone) 62%, transparent);
}
/* The one filled two-stop role (creator by default). The label stays on
   --text so it holds in both themes. */
.role-mark[data-gradient] {
  --role-tone-2: var(--role-tone);
  color: var(--text);
  background: linear-gradient(120deg,
    color-mix(in srgb, var(--role-tone) 40%, var(--bg)),
    color-mix(in srgb, var(--role-tone-2) 32%, var(--bg)));
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--role-tone-2) 50%, transparent),
    0 2px 8px color-mix(in srgb, var(--role-tone) 26%, transparent);
}
:root[data-theme='light'] .role-mark[data-gradient] { color: var(--text); }
.role-mark[data-gradient]:hover {
  background: linear-gradient(120deg,
    color-mix(in srgb, var(--role-tone) 46%, var(--bg)),
    color-mix(in srgb, var(--role-tone-2) 38%, var(--bg)));
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--role-tone-2) 72%, transparent),
    0 4px 12px color-mix(in srgb, var(--role-tone) 34%, transparent);
}

/* ---- Bio, joined, side groups ------------------------------------------- */
.profile-bio {
  margin: 14px 0 0;
  font: 400 var(--step-0s)/1.55 var(--font);
  color: var(--text-soft);
  overflow-wrap: anywhere;
}
.profile-bio-empty { color: var(--ink-2); }
.profile-joined {
  margin: 6px 0 0;
  font: 500 var(--step--1)/1.4 var(--font);
  color: var(--ink-2);
}
.side-group { margin-top: 24px; }
.side-group > .side-label { margin-bottom: 10px; }
.profile-side-inner > .badge-row { margin-top: 14px; }

/* The stats are a group of rows. The group spans the column's full width
   while the text above keeps the column's inset, so it lines up with the
   groups in the main column when the two stack. */
.profile-side .side-stats { margin-inline: -16px; }
.profile-side .stat-list {
  display: block;
  margin: 0;
  border-radius: var(--r-group);
  background: var(--mat-neutral);
  box-shadow: var(--glass-rim);
  overflow: clip;
}
.profile-side .stat {
  position: relative;
  display: grid;
  grid-template-columns: minmax(96px, 40%) minmax(0, 1fr);
  align-items: baseline;
  gap: 4px 16px;
  min-height: 48px;
  padding: 13px 16px;
}
.profile-side .stat + .stat::before {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  left: 16px;
  height: 1px;
  background: var(--hairline);
}
.profile-side .stat-label {
  font: 700 .6875rem/1.4 var(--font);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-2);
}
.profile-side .stat-figure {
  margin: 0;
  font: 500 var(--step-0s)/1.4 var(--font);
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.profile-side .stat-figure .unit {
  font: 500 var(--step--1)/1.4 var(--font);
  color: var(--ink-2);
}

/* The creator panel's stat lists keep their own layout (admin keeps its
   legacy look). */
.stat-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(112px, 1fr));
  gap: 16px 14px;
  margin: 0;
}
.stat-list .stat { min-width: 0; }
.stat-label {
  display: block;
  margin: 0;
  font: 700 var(--step--2)/1.4 var(--font);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-2);
}
.stat-figure {
  display: block;
  margin: 4px 0 0;
  font: 600 var(--step-1)/1.1 var(--font);
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--text);
}
.stat-figure .unit {
  font: 500 var(--step--1)/1.1 var(--font);
  letter-spacing: 0;
  color: var(--ink-2);
}

/* ---- Earned badges and tag marks ----------------------------------------
   Round identity marks, rarest first, each a real button so focus and tap
   both work. The tooltip shows on hover and focus-visible in CSS and on tap
   through .is-open (site.js). --i is the mark's index in its row. */
.badge-row {
  --badge-size: 30px;
  --badge-gap: 9px;
  position: relative;
  display: flex;
  flex-wrap: wrap;
  gap: var(--badge-gap);
  margin: 0;
  padding: 0;
  list-style: none;
  /* The tooltip measures itself against the row (cqw), see .badge-tip. */
  container-type: inline-size;
}
.badge { --i: 0; }
.badge:nth-child(2) { --i: 1; }
.badge:nth-child(3) { --i: 2; }
.badge:nth-child(4) { --i: 3; }
.badge:nth-child(5) { --i: 4; }
.badge:nth-child(6) { --i: 5; }
.badge:nth-child(7) { --i: 6; }
.badge:nth-child(8) { --i: 7; }
.badge:nth-child(9) { --i: 8; }
.badge:nth-child(10) { --i: 9; }
.badge:nth-child(11) { --i: 10; }
.badge:nth-child(12) { --i: 11; }
.badge-mark {
  position: relative;
  display: grid;
  place-items: center;
  width: var(--badge-size);
  height: var(--badge-size);
  padding: 0;
  margin: 0;
  border: 1px solid color-mix(in srgb, var(--accent-ink) 40%, var(--border));
  border-radius: 50%;
  /* A button's own default face would otherwise paint under the tint. */
  background-color: transparent;
  background-image: linear-gradient(160deg,
    color-mix(in srgb, var(--accent-ink) 17%, transparent),
    color-mix(in srgb, var(--accent-ink) 5%, transparent));
  color: var(--accent-ink);
  font: inherit;
  cursor: default;
  transition: border-color var(--dur-chip) linear, box-shadow var(--dur-chip) linear;
}
.badge-mark svg { display: block; width: 16px; height: 16px; fill: currentColor; }
.badge-mark .stroke { fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
.badge-mark .stroke-faint { opacity: .3; }
.badge-mark:hover,
.badge-mark:focus-visible,
.badge.is-open .badge-mark {
  border-color: var(--accent-ink);
  background-image: linear-gradient(160deg,
    color-mix(in srgb, var(--accent-ink) 30%, transparent),
    color-mix(in srgb, var(--accent-ink) 12%, transparent));
  box-shadow: 0 4px 10px color-mix(in srgb, var(--accent-ink) 22%, transparent);
}
/* Podium marks are the only filled ones. Second and third step toward
   --text, so the glyph (the opaque stand-in for glass) keeps its distance
   from the fill in every accent: measured worst 6.95:1 dark, 6.97:1 light. */
.badge-podium .badge-mark {
  background-image: linear-gradient(150deg,
    color-mix(in srgb, var(--violet) 40%, var(--accent-ink)),
    var(--accent-ink) 54%,
    color-mix(in srgb, var(--teal) 32%, var(--accent-ink)));
  background-color: var(--accent-ink);
  color: var(--on-glass-solid);
  border-color: transparent;
  box-shadow: 0 2px 8px color-mix(in srgb, var(--accent-ink) 32%, transparent);
}
.badge-podium-2 .badge-mark { background-color: color-mix(in srgb, var(--accent-ink) 72%, var(--text)); }
.badge-podium-3 .badge-mark { background-color: color-mix(in srgb, var(--accent-ink) 50%, var(--text)); }
.badge-podium-2 .badge-mark,
.badge-podium-3 .badge-mark { background-image: none; }
.badge-podium .badge-mark:hover,
.badge-podium .badge-mark:focus-visible,
.badge-podium.is-open .badge-mark {
  box-shadow: 0 5px 14px color-mix(in srgb, var(--accent-ink) 42%, transparent);
}

/* A pane that floats over the page, so it is real glass: no opacity fade
   (visibility and a 4px rise only), and nothing up its ancestor chain may
   carry opacity, filter or a mask. Centred over its own mark, then clamped
   to the row so it never runs past a narrow column or the viewport. */
.badge-tip {
  --tip-x: calc(var(--i, 0) * (var(--badge-size) + var(--badge-gap)) + var(--badge-size) / 2);
  position: absolute;
  z-index: 20;
  bottom: calc(100% + 8px);
  left: var(--tip-x);
  translate: -50% 0;
  translate: clamp(calc(-1 * var(--tip-x)), -50%, calc(100cqw - var(--tip-x) - 100%)) 0;
  width: max-content;
  max-width: min(240px, 100cqw);
  padding: 8px 12px;
  border-radius: var(--r-chip);
  background: var(--mat-pane);
  box-shadow: var(--glass-rim), var(--shadow-pop);
  -webkit-backdrop-filter: var(--blur-pane);
  backdrop-filter: var(--blur-pane);
  font: 500 var(--step--1)/1.4 var(--font);
  color: var(--text);
  text-align: left;
  pointer-events: none;
  visibility: hidden;
  transform: translateY(4px);
  transition: transform var(--dur-fade) var(--ease-out-cubic), visibility 0s linear var(--dur-fade);
}
.badge-tip b { display: block; margin-bottom: 2px; font-weight: 700; }
.badge.is-open .badge-tip,
.badge-mark:hover + .badge-tip,
.badge-mark:focus-visible + .badge-tip {
  visibility: visible;
  transform: none;
  transition: transform var(--dur-fade) var(--ease-out-cubic), visibility 0s;
}
.badge-empty {
  margin: 0;
  font: 400 var(--step--1)/1.45 var(--font);
  color: var(--ink-2);
}

/* Tag marks: granted by hand, drawn in the badge vocabulary. --tag-tone (and
   --tag-tone-2) and data-treatment come inline from tag_definitions, so a
   tag made in the creator panel draws with no rule for its key. The tone is
   mixed toward --text at the same 62% as --accent-ink: the raw hex is picked
   for a swatch, not for contrast. */
.tag-mark {
  --tag-ink: color-mix(in srgb, var(--tag-tone, var(--teal)) 62%, var(--text));
  color: var(--tag-ink);
  border-color: color-mix(in srgb, var(--tag-tone, var(--teal)) 40%, var(--border));
  background-image: linear-gradient(160deg,
    color-mix(in srgb, var(--tag-tone, var(--teal)) 17%, transparent),
    color-mix(in srgb, var(--tag-tone, var(--teal)) 5%, transparent));
}
.tag-mark:hover,
.tag-mark:focus-visible,
.badge.is-open .tag-mark {
  border-color: var(--tag-ink);
  background-image: linear-gradient(160deg,
    color-mix(in srgb, var(--tag-tone, var(--teal)) 30%, transparent),
    color-mix(in srgb, var(--tag-tone, var(--teal)) 12%, transparent));
  box-shadow: 0 4px 10px color-mix(in srgb, var(--tag-tone, var(--teal)) 22%, transparent);
}
.tag-mark[data-gradient] {
  background-image: linear-gradient(160deg,
    color-mix(in srgb, var(--tag-tone, var(--teal)) 20%, transparent),
    color-mix(in srgb, var(--tag-tone-2, var(--tag-tone, var(--teal))) 20%, transparent));
}
.tag-mark[data-gradient]:hover,
.tag-mark[data-gradient]:focus-visible,
.badge.is-open .tag-mark[data-gradient] {
  background-image: linear-gradient(160deg,
    color-mix(in srgb, var(--tag-tone, var(--teal)) 34%, transparent),
    color-mix(in srgb, var(--tag-tone-2, var(--tag-tone, var(--teal))) 34%, transparent));
}
/* aura: a still halo in the palette's own lightest stop. Its states are
   restated at the hover rules' specificity so a finish stays a finish. */
.tag-mark[data-treatment='aura'] {
  --aura: color-mix(in srgb, var(--teal) 78%, white);
  color: color-mix(in srgb, var(--aura) var(--aura-ink-share, 70%), var(--text));
  border-color: color-mix(in srgb, var(--aura) 62%, transparent);
  background-image: linear-gradient(160deg,
    color-mix(in srgb, var(--aura) 26%, transparent),
    color-mix(in srgb, var(--aura) 8%, transparent));
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--aura) 30%, transparent),
    0 0 12px color-mix(in srgb, var(--aura) 42%, transparent);
}
.tag-mark[data-treatment='aura']:hover,
.tag-mark[data-treatment='aura']:focus-visible,
.badge.is-open .tag-mark[data-treatment='aura'] {
  border-color: color-mix(in srgb, var(--aura) 80%, transparent);
  background-image: linear-gradient(160deg,
    color-mix(in srgb, var(--aura) 38%, transparent),
    color-mix(in srgb, var(--aura) 14%, transparent));
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--aura) 45%, transparent),
    0 0 20px color-mix(in srgb, var(--aura) 60%, transparent);
}
/* rainbow: the one spectrum on the site, fixed hexes because it answers to
   no palette. No single ink clears 3:1 on every stop, so the glyph sits on a
   small disc of the opaque stand-in for glass. It turns only while hovered. */
@property --spin {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}
.tag-mark[data-treatment='rainbow'],
.tag-mark[data-treatment='rainbow']:hover,
.tag-mark[data-treatment='rainbow']:focus-visible,
.badge.is-open .tag-mark[data-treatment='rainbow'] {
  border-color: transparent;
  background-color: transparent;
  background-image: conic-gradient(from var(--spin),
    #e8503a, #e8b84b, #6fd08a, #17aacb, #4b3aed, #a24bd8, #e8503a);
  color: var(--text);
  box-shadow: 0 2px 8px rgb(0 0 0 / .28);
}
.tag-mark[data-treatment='rainbow']::before {
  content: '';
  position: absolute;
  inset: 22%;
  border-radius: 50%;
  background: var(--on-glass-solid);
}
@keyframes tag-spin { to { --spin: 360deg; } }
@media (prefers-reduced-motion: no-preference) and (hover: hover) and (pointer: fine) {
  .tag-mark[data-treatment='rainbow']:hover { animation: tag-spin 2.4s linear infinite; }
}

/* ---- Main column: the ladder --------------------------------------------
   The profile's accent moment: the one featured card (wash 26). Secondary
   text on it takes --ink-2-strong. */
.ladder {
  padding: 24px;
  border-radius: var(--r-card);
  background: var(--mat-card);
  box-shadow: var(--glass-rim);
}
.ladder-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 8px 16px;
  margin: 0 0 18px;
}
.ladder-id { display: grid; justify-items: start; gap: 8px; min-width: 0; }
.ladder .main-label { color: var(--ink-2-strong); }
/* The chip on this wash-26 card needs more ink than on the table glass
   (measured against every accent and the palest and bluest milestones). */
.ladder .rank-chip { --rank-ink-share: 28%; }
:root[data-theme='light'] .ladder .rank-chip { --rank-ink-share: 24%; }

.ladder-title,
.main-title {
  margin: 0;
  font: 700 1.25rem/1.2 var(--font);
  letter-spacing: -0.015em;
  color: var(--text);
}
.ladder-count {
  margin: 0;
  font: 500 var(--step--1)/1.4 var(--font);
  font-variant-numeric: tabular-nums;
  color: var(--ink-2-strong);
}

.profile-progress { margin: 0; }
.progress-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 4px 12px;
  margin: 0 0 8px;
  color: var(--ink-2-strong);
}
.progress-next {
  font: 700 .6875rem/1.4 var(--font);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}
.progress-count {
  font: 600 .75rem/1.4 var(--font);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.progress-next b,
.progress-count b { font-weight: 700; color: var(--text); }
.profile-track {
  height: 6px;
  border-radius: 3px;
  background: rgb(var(--ink) / .12);
  overflow: hidden;
}
.profile-fill {
  height: 100%;
  min-width: 6px;
  border-radius: inherit;
  background: var(--grad-select);
}

/* The rung line: a node per milestone passed, the held one ringed, the one
   being chased hollow and dashed. Nodes are spaced by index, not by level
   (the ladder runs 5, 10 ... 1000, so true positions would pile up at the
   left); equal flex cells do the spacing with no division by a count. Each
   rung draws the segment arriving at it, so the line cannot drift from the
   nodes. */
.rungline {
  display: flex;
  align-items: flex-start;
  margin: 20px 0 0;
  padding: 0;
  list-style: none;
}
.rung {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  padding-top: 24px;
  text-align: center;
}
.rung::before {
  content: '';
  position: absolute;
  top: 6px;
  left: -50%;
  width: 100%;
  height: 2px;
  background: color-mix(in srgb, var(--tint) 55%, transparent);
}
.rung:first-child::before { content: none; }
.rung-dot {
  position: absolute;
  top: 2px;
  left: 50%;
  translate: -50% 0;
  box-sizing: border-box;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--grad-select);
}
.rung-current .rung-dot {
  top: 0;
  width: 14px;
  height: 14px;
  box-shadow: 0 0 0 2px var(--on-glass-solid), 0 0 0 4px var(--eyebrow-ink);
}
.rung-next .rung-dot {
  background: transparent;
  border: 1.5px dashed rgb(var(--ink) / .45);
}
.rung-next::before {
  background: repeating-linear-gradient(90deg, rgb(var(--ink) / .3) 0 4px, transparent 4px 8px);
}
.rung-label { display: block; }
.rung-label b {
  display: block;
  font: 700 .75rem/1.2 var(--font);
  font-variant-numeric: tabular-nums;
  color: var(--ink-2-strong);
}
.rung-name {
  display: none;
  margin-top: 2px;
  font: 500 .75rem/1.25 var(--font);
  color: var(--ink-2-strong);
}
.rung-current .rung-label b,
.rung-current .rung-name { color: var(--text); }
.rung-current .rung-name,
.rung-next .rung-name { display: block; }
/* Past nine rungs a cell is too narrow for a label, so only the held rung
   keeps one. Nothing is lost: the heading names it and the progress line
   names the next. */
.rungline.is-dense .rung-label { display: none; }
.rungline.is-dense .rung-current .rung-label { display: block; }
.rungline.is-dense .rung-current .rung-name { display: none; }
.rungline.is-dense .rung:last-child .rung-label,
.rungline.is-dense .rung:nth-last-child(2) .rung-label { text-align: right; }

/* ---- Main column: groups ------------------------------------------------ */
.main-group {
  margin: 0;
  padding: 22px 24px 24px;
  border-radius: var(--r-group);
  background: var(--mat-neutral);
  box-shadow: var(--glass-rim);
}
.main-group-shelf { border-radius: var(--r-card); }
.main-group > .main-label { margin-bottom: 6px; }
@media (max-width: 759.98px) {
  .ladder,
  .main-group { padding: 20px; }
}

/* Rows with inset hairlines; inside a group, no box of their own. */
.detail-list { margin: 12px 0 0; }
.detail-list > div {
  display: grid;
  grid-template-columns: minmax(96px, 34%) minmax(0, 1fr);
  align-items: baseline;
  gap: 4px 16px;
  padding: 11px 0;
  box-shadow: inset 0 1px 0 var(--hairline);
}
.detail-list > div:last-child { padding-bottom: 0; }
.detail-list dd {
  margin: 0;
  font: 500 var(--step-0s)/1.45 var(--font);
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.detail-list dd b { font-weight: 700; }

.profile-note {
  margin: 10px 0 0;
  font: 400 var(--step-0s)/1.55 var(--font);
  color: var(--ink-2);
}
.profile-note b { font-weight: 700; color: var(--text); }
/* Standing in for the ladder and the rank detail when there is no rank. */
.profile-main > .profile-note {
  margin: 0;
  padding: 18px 22px;
  border-radius: var(--r-group);
  background: var(--mat-neutral);
  box-shadow: var(--glass-rim);
}

/* ---- Shelf -------------------------------------------------------------- */
.cover,
.shelf-cover,
.shelf-featured-cover {
  display: block;
  width: 100%;
  aspect-ratio: 2 / 3;
  object-fit: cover;
  border-radius: var(--r-cover);
  outline: 1px solid rgb(255 255 255 / .13);
  outline-offset: -1px;
  box-shadow: var(--shadow-cover);
  background: var(--mat-etched);
}
:root[data-theme='light'] :is(.cover, .shelf-cover, .shelf-featured-cover) { outline-color: rgb(0 0 0 / .08); }
.shelf-cover { border-radius: var(--r-cover-sm); }
a > :is(.cover, .shelf-cover) { transition: transform var(--dur-release) var(--ease-out-back); }
a:active > :is(.cover, .shelf-cover) { transform: scale(.965); transition: transform var(--dur-press) var(--ease-decel); }

.shelf-featured {
  display: flex;
  align-items: flex-end;
  gap: 16px;
  margin: 16px 0 0;
}
.shelf-featured-art { position: relative; flex: none; width: 112px; }
/* A mark on art, not glass: a dark scrim chip that holds on any cover. */
.shelf-featured-tag {
  position: absolute;
  left: 8px;
  bottom: 8px;
  padding: 3px 8px;
  border-radius: var(--r-pill);
  font: 700 var(--step--3)/1.3 var(--font);
  letter-spacing: var(--track-meta);
  text-transform: uppercase;
  color: #fff;
  background: rgb(0 0 0 / .58);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .14);
}
.shelf-featured-title {
  min-width: 0;
  padding-bottom: 4px;
  font: 700 1.0625rem/1.3 var(--font);
  letter-spacing: -0.01em;
  color: var(--text);
}
.shelf-subhead { margin: 22px 0 10px; }
.shelf-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(92px, 1fr));
  gap: 12px;
  margin: 16px 0 0;
  padding: 0;
  list-style: none;
}
.shelf-subhead + .shelf-grid { margin-top: 0; }
.shelf-grid-liked { grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); }
.shelf-tile { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
/* Two lines, whole words, one cut. No text-overflow (Safari ellipsises
   every overflowing line) and no breaking inside words. */
.shelf-title {
  display: -webkit-box;
  overflow: hidden;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow-wrap: normal;
  font: 600 var(--step-chip)/1.3 var(--font);
  color: var(--text-soft);
}

/* ---- Private profile ----------------------------------------------------
   Still inside a .profile-card, so it wears the owner's accent. */
.profile-card-private { max-width: 440px; }
.profile-private {
  display: grid;
  justify-items: center;
  padding: 32px;
  text-align: center;
  border-radius: var(--r-group);
  background: var(--mat-neutral);
  box-shadow: var(--glass-rim);
}
.profile-private-glyph { margin-bottom: 16px; }
.profile-private-name {
  margin: 0;
  font: 700 1.5rem/1.2 var(--font);
  letter-spacing: -0.02em;
  color: var(--text);
  overflow-wrap: anywhere;
}
.profile-private-handle {
  margin: 4px 0 0;
  font: 500 var(--step-0s)/1.4 var(--font);
  color: var(--ink-2);
}
.profile-private-note {
  margin: 14px 0 0;
  font: 400 var(--step-0s)/1.55 var(--font);
  color: var(--text-soft);
}

@media (max-width: 759.98px) {
  .rank-badge { min-width: 0; flex: 1 1 100%; }
}

/* ==========================================================================
   Account page badge block
   --------------------------------------------------------------------------
   The same earned row outside a .profile-card, so it declares its own ink,
   plus the hint the public profile never shows.
   ========================================================================== */

.badge-field { --accent-ink: color-mix(in srgb, var(--teal) 62%, var(--text)); }
:root[data-theme='light'] .badge-field { --accent-ink: color-mix(in srgb, var(--teal) 40%, var(--text)); }
.badge-field .badge-row { margin-top: 8px; }

/* ==========================================================================
   Credits
   --------------------------------------------------------------------------
   The head sits on the shared ambient field; the sky keeps the lemniscate
   star layout (positions computed server side and written as --x and --y),
   with public/js/credits.js drawing the curve and its light underneath.
   ========================================================================== */

.credits-head { margin-bottom: clamp(24px, 4vw, 40px); }
.credits-empty { max-width: 560px; margin: 0 auto; }

.sponsors-sky {
  position: relative;
  height: calc(var(--loops, 1) * var(--loop-h, 90vh));
  overflow: hidden;
}
.sky-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
.star {
  position: absolute;
  left: var(--x);
  top: var(--y);
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 4px;
  transform: translate(-50%, -50%);
  text-decoration: none;
  color: var(--text-soft);
}
.star--below { flex-direction: column-reverse; }
.star--above { flex-direction: column; }
.star-dot {
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--eyebrow-ink);
  box-shadow: 0 0 10px color-mix(in srgb, var(--tint) 60%, transparent);
}
/* The padding is there at rest so the capsule does not shift the name. */
.star-name {
  padding: 4px 10px;
  border-radius: var(--r-pill);
  font: 500 var(--step--1)/1.3 var(--font);
  white-space: nowrap;
  color: var(--text-soft);
  transition: background var(--dur-chip) linear, box-shadow var(--dur-chip) linear, color var(--dur-chip) linear;
}
.star--below .star-name,
.star--above .star-name { max-width: 22ch; white-space: normal; text-align: center; }
/* Hover and focus lift the name into a pane: real glass over the sky. */
.star:hover .star-name,
.star:focus-visible .star-name {
  color: var(--text);
  background: var(--mat-pane);
  box-shadow: var(--glass-rim);
  -webkit-backdrop-filter: var(--blur-pane);
  backdrop-filter: var(--blur-pane);
}
.star:focus-visible { outline: none; }
.star:focus-visible .star-name { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.star--solo .star-dot { width: 8px; height: 8px; }
.star--solo .star-name { font-size: var(--step-0s); }
/* One sponsor is not a loop: a full-height sky with the point parked at the
   curve's t=0 (off to one edge, not the shape's centre) reads as an empty
   page. Shrink the field and recentre the single star instead. */
.sponsors-sky--solo { height: 40vh; }
.sponsors-sky--solo .star { left: 50%; top: 50%; }

.sponsor-index { padding-top: clamp(40px, 6vw, 64px); }
.sponsor-index h2 {
  margin: 0 0 18px;
  text-align: center;
  font: 700 var(--step-2)/1.15 var(--font);
  letter-spacing: var(--track-h2);
}
.sponsor-list {
  max-width: 560px;
  margin: 0 auto;
  padding: 0;
  list-style: none;
  border-radius: var(--r-group);
  background: var(--mat-neutral);
  box-shadow: var(--glass-rim);
  overflow: clip;
}
.sponsor-list li { position: relative; }
.sponsor-list li + li::before {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  left: 16px;
  height: 1px;
  background: var(--hairline);
}
.sponsor-list .group-row {
  display: flex;
  align-items: center;
  min-height: 52px;
  padding: 13px 16px;
  font: 500 var(--step-0s)/1.3 var(--font);
  color: var(--text);
  text-decoration: none;
  transition: background var(--dur-chip) linear;
}
.sponsor-list .group-row:hover { background: var(--row-hover); }
.sponsor-list .group-row:active { background: var(--row-press); }
.sponsor-list .group-row:focus-visible { outline-offset: -2px; }
.closing-line {
  margin: 28px 0 0;
  text-align: center;
  font: 400 var(--step-0s)/1.55 var(--font);
  color: var(--ink-2);
}

