/* ==========================================================================
   Tayler — noir

   The palette is SAMPLED from assets/pfp.jpg, not chosen, and that avatar is
   unusually decisive about what this page can be: measured over the inscribed
   circle, 395,847 pixels, it has a mean saturation of 0.0000 and ZERO pixels
   above S15. Every pixel has R == G == B. It is not "mostly grey" the way a
   desaturated photo is — it is literally achromatic.

   So there is no accent hue to spend, and inventing one would look invented.
   The whole design is carried by VALUE, and the value distribution is bimodal:
   55.23% of the circle sits at #030303 and another 20.18% at #797979, with a
   smooth ramp between them. Those two are the page: the floor and the silver.

   One thing the source cannot supply is text contrast. Its brightest tone is
   #939393 (L 0.58, and only 0.03% of the circle) — there is no white anywhere
   in the image. Headline text at #939393 over #030303 would be legible but
   grey-on-grey and cheap-looking. So the ramp is EXTENDED upward past the
   avatar's range for type only (--platinum-100, --white). That is an honest
   move: it adds no hue, it only continues the same neutral ramp to where body
   text needs it.

   The one place colour appears is a link tile's hover state, where it is the
   platform's own brand mark lighting up. At rest the page is 100% neutral;
   colour is the reward for pointing at something. On a page this monochrome a
   single lit red or purple registers instantly, which is the point.
   ========================================================================== */

:root {
  /* % is that bucket's share of the sampled circle */
  --noir-990: #030303; /* 55.23% — the floor, and the page background */
  --noir-970: #0A0A0A; /* derived: between the floor and the first ramp step */
  --ink-900:  #191919; /*  5.32% */
  --ink-850:  #262626; /*  4.67% */
  --ash-800:  #383838; /*  2.69% */
  --ash-700:  #484848; /*  3.40% */
  --ash-600:  #575757; /*  3.32% */
  --steel-500:#686868; /*  4.02% */
  --silver-400:#797979; /* 20.18% — the second peak, the avatar's highlight */
  --silver-300:#848484; /*  1.14% */
  --silver-200:#939393; /*  0.03% — the brightest tone in the source image */

  /* Above the source's range. Type only — see the header. */
  --platinum-100: #C6C6C6;
  --white: #EDEDED;

  --text:       var(--white);
  --text-soft:  rgba(237, 237, 237, 0.74);
  --text-dim:   rgba(237, 237, 237, 0.58);
  --text-faint: rgba(237, 237, 237, 0.40);

  --tile-bg:        rgba(255, 255, 255, 0.045);
  --tile-bg-hover:  rgba(255, 255, 255, 0.085);
  --tile-border:    rgba(255, 255, 255, 0.13);

  /* Per-tile accent. Neutral by default so an unstyled tile stays in the
     palette; [data-brand] overrides it, and it is only ever painted on hover. */
  --brand: var(--platinum-100);

  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-soft: cubic-bezier(0.4, 0, 0.2, 1);
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  min-height: 100svh;
  display: grid;
  place-items: center;
  /* Vertical padding is capped at 2rem, not 3rem. At 1440x900 the card is
     830px tall, and 3rem top and bottom put the page at 948px against a 900px
     viewport — a scrollbar for 48px of nothing. Measured, not guessed. */
  padding: clamp(1.25rem, 4vw, 2rem) 1.25rem;
  background: var(--noir-990);
  color: var(--text);
  font-family: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

::selection { background: var(--platinum-100); color: var(--noir-990); }

:focus-visible {
  outline: 2px solid var(--white);
  outline-offset: 3px;
  border-radius: 4px;
}

/* ==========================================================================
   Background — a baked haze, two slow drifting pools, grain and scanlines
   ========================================================================== */

.bg {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  background: var(--noir-990);
}

/* assets/bg-baked.jpg is three soft light pools and a vignette, rendered ONCE
   into a JPEG rather than stacked as live CSS gradients. A full-screen gradient
   stack has to re-rasterise whenever anything above it recomposites, which
   measured as the single most expensive layer on the page.
   `will-change: transform` pins it its own compositor layer so the image is not
   rescaled into the .bg stack every time the grain shifts — that one line was
   worth 0.844 -> 0.764 renderer CPU-seconds per second when measured. */
.photo {
  position: absolute;
  inset: -2%;
  background: url("assets/bg-baked.jpg") center 40% / cover no-repeat;
  will-change: transform;
}

/* Two neutral pools of light that keep some drift alive behind a still image.
   Both are pure grey — a tinted "aurora" is exactly the invented colour the
   header rules out, and on an otherwise achromatic page the eye finds it
   instantly. These read as a lamp somewhere off-frame, which is what the
   avatar's lighting implies. */
.haze {
  position: absolute;
  border-radius: 50%;
  filter: blur(90px);
  will-change: transform;
}

.haze--1 {
  width: 56rem; height: 56rem;
  top: -26rem; left: -18rem;
  background: radial-gradient(circle, rgba(147, 147, 147, 0.16) 0%, transparent 68%);
  animation: drift-1 28s var(--ease-soft) infinite alternate;
}

.haze--2 {
  width: 36rem; height: 36rem;
  top: 34%; right: -15rem;
  background: radial-gradient(circle, rgba(121, 121, 121, 0.13) 0%, transparent 66%);
  animation: drift-2 41s var(--ease-soft) infinite alternate;
}

/* Translate only — NO scale. Scaling a 90px-blurred element forces the blur to
   be recomputed at the new size on every frame, which is the entire cost of the
   effect; translating an already-rasterised layer is free. */
@keyframes drift-1 {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(7rem, 5rem, 0); }
}
@keyframes drift-2 {
  from { transform: translate3d(0, 0, 0); opacity: 0.7; }
  to   { transform: translate3d(-5rem, 6rem, 0); opacity: 1; }
}

/* inset is -4%, not -50%: the shift animation only moves it by 2%, so that is
   all the overhang it needs, and every extra pixel here goes through a
   full-screen `mix-blend-mode: overlay`. */
.grain {
  position: absolute;
  inset: -4%;
  opacity: 0.05;
  mix-blend-mode: overlay;
  pointer-events: none;
  background-image: url("assets/grain.png");
  background-repeat: repeat;
  animation: grain-shift 0.7s steps(1) infinite;
}

@keyframes grain-shift {
  0%   { transform: translate(0, 0); }
  25%  { transform: translate(-2%, 1%); }
  50%  { transform: translate(1%, -2%); }
  75%  { transform: translate(-1%, -1%); }
  100% { transform: translate(2%, 1%); }
}

/* Vignette and scanlines are two background layers on ONE element. Both are
   static, so merging them costs nothing visually and takes a full-viewport
   layer out of a stack that recomposites together. The vignette is listed
   first because the first background layer paints on top. */
.scanlines {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(125% 95% at 50% 50%, transparent 40%, rgba(0, 0, 0, 0.66) 100%),
    repeating-linear-gradient(
      to bottom,
      rgba(0, 0, 0, 0) 0px,
      rgba(0, 0, 0, 0) 2px,
      rgba(0, 0, 0, 0.024) 3px,
      rgba(0, 0, 0, 0.024) 4px
    );
}

/* ==========================================================================
   Smoke — one canvas, drawn from a pre-baked sprite sheet. See smoke.js.

   z 1 puts it BELOW the card's z 2, so smoke never crosses the type and the
   tiles' backdrop-filter blurs a puff as it passes behind the glass.
   ========================================================================== */

.smoke {
  position: fixed;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  pointer-events: none;
  display: block;
}

@media (prefers-reduced-motion: reduce) {
  .smoke { display: none; }
}

/* ==========================================================================
   Card
   ========================================================================== */

.card {
  position: relative;
  z-index: 2;
  width: 100%;
  max-width: 27.5rem;
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* staggered entrance */
[data-reveal] {
  opacity: 0;
  transform: translateY(14px);
  filter: blur(6px);
  animation: reveal 0.9s var(--ease) forwards;
  animation-delay: calc(var(--i) * 70ms + 120ms);
}

@keyframes reveal {
  to { opacity: 1; transform: translateY(0); filter: blur(0); }
}

/* ==========================================================================
   Hero
   ========================================================================== */

.hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  margin-bottom: 1.9rem;
}

.avatar {
  position: relative;
  width: 8.25rem;
  height: 8.25rem;
  display: grid;
  place-items: center;
  margin-bottom: 1.35rem;
}

.avatar__glow {
  position: absolute;
  inset: -18%;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(147, 147, 147, 0.34) 0%, transparent 68%);
  filter: blur(20px);
  animation: breathe 5.5s var(--ease-soft) infinite alternate;
}

@keyframes breathe {
  from { opacity: 0.5; transform: scale(0.94); }
  to   { opacity: 1;   transform: scale(1.06); }
}

/* A thin conic ring, masked hollow. It sweeps the avatar's own value ramp
   rather than a hue: dark, silver, near-white, dark. On a monochrome page a
   rotating highlight reads as light moving over metal. */
.avatar__ring {
  position: absolute;
  inset: -3px;
  border-radius: 50%;
  padding: 2px;
  background: conic-gradient(
    from 0deg,
    transparent 0deg,
    var(--silver-400) 66deg,
    var(--silver-200) 116deg,
    var(--white) 186deg,
    transparent 248deg,
    transparent 360deg
  );
  -webkit-mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  opacity: 0.9;
  animation: spin 9s linear infinite;
}

@keyframes spin { to { transform: rotate(1turn); } }

.avatar__img {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  object-fit: cover;
  border: 2px solid rgba(3, 3, 3, 0.9);
  box-shadow: 0 12px 40px -10px rgba(0, 0, 0, 0.9);
  transition: transform 0.5s var(--ease);
  user-select: none;
}

.avatar:hover .avatar__img { transform: scale(1.04); }

.name {
  font-family: "Space Grotesk", "Inter", sans-serif;
  font-size: clamp(2.1rem, 8vw, 2.65rem);
  font-weight: 700;
  letter-spacing: -0.045em;
  line-height: 1.05;
  /* Top-lit, like the avatar is: the gradient runs from near-white down into
     the avatar's own silver, so the name looks lit rather than tinted. */
  background: linear-gradient(172deg, #F4F4F4 24%, var(--silver-400) 100%);
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
}

.kana {
  font-family: "Noto Sans JP", "Inter", sans-serif;
  font-size: 0.78rem;
  font-weight: 400;
  letter-spacing: 0.42em;
  text-indent: 0.42em;
  margin-top: 0.55rem;
  color: var(--silver-300);
  opacity: 0.8;
}

.bio {
  margin-top: 0.85rem;
  font-size: 0.9rem;
  font-weight: 300;
  letter-spacing: 0.02em;
  color: var(--text-dim);
}

/* --- the pills --- */

/* width:100% is load-bearing: without it, shrink-to-fit sized this box one
   pixel under its contents and the view pill wrapped to a second line. */
.stats {
  width: 100%;
  margin-top: 1.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.5rem;
}

/* Deliberately quieter than the sub pill beside it: no border, no backdrop
   blur. Two pills of equal weight compete, and the subscriber count is the one
   that matters — this is a footnote that happens to live up here. */
.views {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0.85rem;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.045);
  color: var(--text-dim);
  transition: background 0.4s var(--ease), color 0.4s var(--ease);
}

.views:hover {
  background: rgba(255, 255, 255, 0.1);
  color: var(--platinum-100);
}

.views__icon { width: 0.95rem; height: 0.95rem; flex: none; display: block; }

.views__n {
  font-size: 0.85rem;
  font-weight: 500;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
  transition: opacity 0.4s var(--ease);
}

.views[data-state="loading"] .views__n { opacity: 0.4; }

/* both counter services down — take the whole pill away rather than leaving an
   eye staring at an em dash */
.views[data-state="dead"] { display: none; }

.subs {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.5rem 1.05rem;
  border-radius: 999px;
  text-decoration: none;
  color: var(--text);
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(198, 198, 198, 0.22);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  transition: background 0.4s var(--ease), border-color 0.4s var(--ease),
              transform 0.4s var(--ease), box-shadow 0.4s var(--ease);
}

.subs:hover {
  background: rgba(255, 255, 255, 0.11);
  border-color: rgba(237, 237, 237, 0.48);
  transform: translateY(-2px);
  box-shadow: 0 10px 30px -12px rgba(198, 198, 198, 0.5);
}

.subs__count {
  font-family: "Space Grotesk", "Inter", sans-serif;
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
  color: var(--platinum-100);
  transition: opacity 0.3s var(--ease);
  /* the count-up passes through wider strings than it lands on
     ("1.23K" -> "12.4K"); reserve the widest so the pill cannot resize */
  min-width: 5ch;
  text-align: center;
}

.subs__count[data-state="loading"] { opacity: 0.4; }

/* brief flash when the live count ticks */
.subs__count.is-bump { animation: bump 0.65s var(--ease); }

@keyframes bump {
  0%   { transform: translateY(0); text-shadow: none; }
  35%  { transform: translateY(-3px); text-shadow: 0 0 14px rgba(237, 237, 237, 0.8); }
  100% { transform: translateY(0); text-shadow: none; }
}

.subs__label {
  font-size: 0.85rem;
  font-weight: 300;
  color: var(--text-dim);
}

/* ==========================================================================
   Link tiles

   Brand colour appears ONLY on hover, and only on the icon, the border and the
   shadow. At rest every tile is neutral, which is what keeps the page in the
   avatar's palette; lighting one up on hover is the one deliberate exception.
   ========================================================================== */

/* The column itself. This rule went MISSING in the first draft, which left the
   tiles with no gap at all — six translucent cards sharing an edge read as one
   conjoined block, not as six links. 0.8rem rather than the more usual 0.7rem
   because these tiles are unusually translucent, so the gap is doing more of
   the separating work. */
.links {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 0.8rem;
}

.tile[data-brand="youtube"] { --brand: #FF0033; }
.tile[data-brand="twitch"]  { --brand: #A970FF; }
.tile[data-brand="discord"] { --brand: #5865F2; }
/* TikTok's cyan rather than its red: the red collides with YouTube two tiles up */
.tile[data-brand="tiktok"]  { --brand: #25F4EE; }
.tile[data-brand="x"]       { --brand: #FFFFFF; }
/* The game is Tayler's own, so it does not borrow a platform's colour — it
   stays in the page's palette and earns its emphasis from size and the CTA
   instead. Nothing else on the page is white-on-hover except X, which is white
   because that IS its brand. */
.tile[data-brand="game"]    { --brand: var(--white); }

.tile {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  width: 100%;
  display: flex;
  align-items: center;
  gap: 0.95rem;
  padding: 0.95rem 1.1rem;
  border-radius: 1rem;
  text-align: left;
  text-decoration: none;
  color: var(--text);
  font-family: inherit;
  cursor: pointer;
  background: var(--tile-bg);
  border: 1px solid var(--tile-border);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.055);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  transition:
    transform 0.45s var(--ease),
    background 0.45s var(--ease),
    border-color 0.45s var(--ease),
    box-shadow 0.45s var(--ease);
}

.tile:hover {
  transform: translateY(-3px);
  background: var(--tile-bg-hover);
  border-color: color-mix(in srgb, var(--brand) 45%, transparent);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.09),
    0 14px 34px -14px color-mix(in srgb, var(--brand) 55%, transparent),
    0 0 0 1px color-mix(in srgb, var(--brand) 14%, transparent) inset;
}

.tile:active { transform: translateY(-1px) scale(0.99); }

/* diagonal sheen sweeping across on hover */
.tile__sheen {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(
    105deg,
    transparent 30%,
    color-mix(in srgb, var(--brand) 16%, transparent) 48%,
    transparent 66%
  );
  transform: translateX(-120%);
  transition: opacity 0.3s var(--ease);
}

.tile:hover .tile__sheen {
  opacity: 1;
  animation: sweep 0.95s var(--ease) forwards;
}

@keyframes sweep {
  from { transform: translateX(-120%); }
  to   { transform: translateX(120%); }
}

.tile__icon {
  flex: none;
  width: 2.4rem;
  height: 2.4rem;
  display: grid;
  place-items: center;
  border-radius: 0.7rem;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.06);
  transition: background 0.45s var(--ease), border-color 0.45s var(--ease),
              transform 0.45s var(--ease);
}

.tile__icon svg {
  width: 1.15rem;
  height: 1.15rem;
  fill: var(--text-dim);
  transition: fill 0.45s var(--ease), stroke 0.45s var(--ease),
              filter 0.45s var(--ease);
}

/* the gamepad is a stroked glyph, not a filled brand mark */
.tile[data-brand="game"] .tile__icon svg { fill: none; stroke: var(--text-dim); }

.tile:hover .tile__icon {
  background: color-mix(in srgb, var(--brand) 16%, transparent);
  border-color: color-mix(in srgb, var(--brand) 34%, transparent);
  transform: scale(1.06);
}

.tile:hover .tile__icon svg {
  fill: var(--brand);
  filter: drop-shadow(0 0 8px color-mix(in srgb, var(--brand) 55%, transparent));
}

.tile[data-brand="game"]:hover .tile__icon svg { fill: none; stroke: var(--brand); }

.tile__text {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
  flex: 1;
}

.tile__title {
  font-family: "Space Grotesk", "Inter", sans-serif;
  font-size: 1.02rem;
  font-weight: 600;
  letter-spacing: -0.018em;
}

/* The handle line. Weight 400 and a hair of POSITIVE tracking: small type wants
   opening up, while the title above still tracks negative, which is right at
   16px. 0.74 alpha clears 4.5:1 over the tile substrate with room to spare. */
.tile__sub {
  font-size: 0.82rem;
  font-weight: 400;
  letter-spacing: 0.012em;
  color: var(--text-soft);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: color 0.45s var(--ease);
}

.tile:hover .tile__sub { color: var(--text); }

/* live follower/member count, right-aligned before the arrow */
.tile__count {
  flex: none;
  margin-right: 0.15rem;
  font-family: "Space Grotesk", "Inter", sans-serif;
  font-size: 0.82rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
  transition: color 0.45s var(--ease), opacity 0.4s var(--ease);
}

.tile:hover .tile__count { color: var(--platinum-100); }

/* placeholder before the first fetch lands — dim, so it never looks broken */
.tile__count[data-state="loading"] { opacity: 0.35; }

/* a source that dies just leaves the tile clean rather than showing a lie */
.tile__count[data-state="dead"] { display: none; }

.tile__count.is-bump { animation: count-bump 0.6s var(--ease); }

@keyframes count-bump {
  0%   { transform: translateY(0); text-shadow: none; }
  35%  { transform: translateY(-2px); text-shadow: 0 0 12px rgba(237, 237, 237, 0.75); }
  100% { transform: translateY(0); text-shadow: none; }
}

.tile__arrow {
  flex: none;
  display: grid;
  place-items: center;
  /* an affordance, not decoration — 0.46 lands it around 4.3:1 */
  color: rgba(237, 237, 237, 0.46);
  transition: color 0.45s var(--ease), transform 0.45s var(--ease);
}

.tile__arrow svg { width: 1rem; height: 1rem; display: block; }

.tile:hover .tile__arrow {
  color: var(--brand);
  transform: translateX(3px);
}

/* --- the featured tile ---
   Taller, with an eyebrow above the title and a CTA pill before the arrow. It
   is the only tile that carries a lit border at REST, because it has to read as
   the primary action before anyone points at anything. */

.tile--feature {
  padding: 1.15rem 1.1rem;
  background:
    linear-gradient(105deg, rgba(255, 255, 255, 0.075) 0%, rgba(255, 255, 255, 0.03) 58%);
  border-color: rgba(255, 255, 255, 0.17);
  box-shadow: 0 10px 30px -18px rgba(0, 0, 0, 0.9);
}

.tile--feature:hover {
  border-color: rgba(237, 237, 237, 0.5);
  box-shadow:
    0 16px 38px -16px rgba(198, 198, 198, 0.34),
    0 0 0 1px rgba(255, 255, 255, 0.1) inset;
}

.tile--feature .tile__icon {
  width: 2.7rem;
  height: 2.7rem;
  background: rgba(255, 255, 255, 0.075);
  border-color: rgba(255, 255, 255, 0.12);
}

.tile--feature .tile__icon svg { width: 1.35rem; height: 1.35rem; }

.tile--feature .tile__title { font-size: 1.1rem; }

/* The feature tile is the only one whose handle line is a SENTENCE rather than
   a handle, and the CTA pill takes width the other tiles do not spend — so at
   27.5rem it hit the nowrap ellipsis ("play in your ..."). It is allowed two
   lines; the others stay on one, where an ellipsis is the right answer for a
   long URL. */
.tile--feature .tile__sub {
  white-space: normal;
  line-height: 1.35;
}

.tile__eyebrow {
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.19em;
  text-transform: uppercase;
  color: var(--text-faint);
  transition: color 0.45s var(--ease);
}

.tile--feature:hover .tile__eyebrow { color: var(--silver-200); }

.tile__cta {
  flex: none;
  padding: 0.34rem 0.85rem;
  border-radius: 999px;
  font-family: "Space Grotesk", "Inter", sans-serif;
  font-size: 0.76rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--noir-990);
  background: var(--platinum-100);
  transition: background 0.45s var(--ease), transform 0.45s var(--ease),
              box-shadow 0.45s var(--ease);
}

.tile--feature:hover .tile__cta {
  background: var(--white);
  transform: translateY(-1px);
  box-shadow: 0 6px 18px -8px rgba(237, 237, 237, 0.65);
}

/* ==========================================================================
   Cursor trail — a lagging, speed-stretched glow behind the real pointer.
   The native cursor stays visible on purpose; see cursor.js.
   ========================================================================== */

/* Two things would make this the most expensive 44px on the page, and neither
   is here:

   `mix-blend-mode: screen` forces everything under the glow to recomposite on
   every pointermove — and over a near-black page it buys nothing, since screen
   against 0 is the source colour.

   `filter: blur()` would have to re-rasterise every frame, because the glow is
   scaled per frame by the speed stretch and a scaled filter cannot be reused. A
   radial gradient with soft stops IS a blur, so the spread lives in the
   gradient and the box is sized to cover what the blur would have added. */
.cursor-glow {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 9;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  pointer-events: none;   /* must never eat a click */
  opacity: 0;
  will-change: transform, opacity;
  background: radial-gradient(
    circle,
    rgba(237, 237, 237, 0.4) 0%,
    rgba(198, 198, 198, 0.24) 26%,
    rgba(147, 147, 147, 0.1) 48%,
    rgba(147, 147, 147, 0) 72%
  );
}

@media (pointer: coarse), (prefers-reduced-motion: reduce) {
  .cursor-glow { display: none; }
}

/* ==========================================================================
   Now playing — drives a local <audio> element
   ========================================================================== */

.player {
  width: 100%;
  margin-top: 0.9rem;
  padding: 0.85rem 1rem 0.75rem;
  border-radius: 1rem;
  background: var(--tile-bg);
  border: 1px solid var(--tile-border);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  transition: border-color 0.45s var(--ease), background 0.45s var(--ease);
}

.player:hover { border-color: rgba(198, 198, 198, 0.26); }

/* set by player.js when the file is missing or fails to decode */
.player[hidden] { display: none; }

.player__top {
  display: flex;
  align-items: center;
  gap: 0.8rem;
}

.player__art {
  flex: none;
  width: 3.25rem;
  height: 3.25rem;
  border-radius: 0.7rem;
  overflow: hidden;
  position: relative;
  border: 1px solid rgba(255, 255, 255, 0.09);
  box-shadow: 0 6px 18px -8px rgba(0, 0, 0, 0.9);
}

.player__art img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  user-select: none;
  transition: transform 0.6s var(--ease), filter 0.6s var(--ease);
  filter: brightness(0.85) contrast(1.02);
}

/* the art only comes fully alive while it is actually playing */
.player[data-playing="true"] .player__art img {
  transform: scale(1.06);
  filter: brightness(1) contrast(1);
}

.player__meta {
  display: flex;
  flex-direction: column;
  gap: 0.14rem;
  min-width: 0;
  flex: 1;
}

.player__label {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.6rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.player__title {
  font-size: 0.9rem;
  font-weight: 500;
  letter-spacing: -0.012em;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* three bars that only move while the track is running */
.player__eq {
  display: inline-flex;
  align-items: flex-end;
  gap: 2px;
  height: 0.55rem;
}

.player__eq i {
  width: 2px;
  height: 30%;
  border-radius: 1px;
  background: var(--platinum-100);
  animation: eq 0.9s var(--ease-soft) infinite alternate;
  opacity: 0.35;
}

.player__eq i:nth-child(2) { animation-delay: 0.28s; }
.player__eq i:nth-child(3) { animation-delay: 0.55s; }

.player[data-playing="true"] .player__eq i { opacity: 1; }
.player[data-playing="false"] .player__eq i { animation-play-state: paused; }

@keyframes eq {
  from { height: 25%; }
  to   { height: 100%; }
}

.player__btn {
  flex: none;
  width: 2.3rem;
  height: 2.3rem;
  display: grid;
  place-items: center;
  border-radius: 50%;
  cursor: pointer;
  color: var(--white);
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(198, 198, 198, 0.26);
  transition: background 0.4s var(--ease), border-color 0.4s var(--ease),
              transform 0.4s var(--ease), box-shadow 0.4s var(--ease);
}

.player__btn:hover {
  background: rgba(255, 255, 255, 0.15);
  border-color: rgba(237, 237, 237, 0.55);
  transform: scale(1.06);
  box-shadow: 0 8px 22px -10px rgba(237, 237, 237, 0.6);
}

.player__btn:active { transform: scale(0.97); }

.player__btn svg { width: 0.95rem; height: 0.95rem; fill: currentColor; display: block; }

.player[data-playing="false"] .ico-pause,
.player[data-playing="true"]  .ico-play { display: none; }

/* --- seek --- */

/* padded wrapper so the 4px bar has a comfortable grab area */
.player__seekwrap {
  margin-top: 0.7rem;
  padding: 0.45rem 0;
  cursor: pointer;
  touch-action: none;
}

.player__seekwrap:focus-visible { outline-offset: 6px; }

.player__track {
  position: relative;
  height: 4px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.13);
  overflow: visible;
}

.player__fill {
  width: 0%;
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--ash-600), var(--white));
  box-shadow: 0 0 12px -2px rgba(237, 237, 237, 0.5);
}

.player__knob {
  position: absolute;
  top: 50%;
  left: 0;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--white);
  box-shadow: 0 0 10px rgba(237, 237, 237, 0.7);
  transform: translate(-50%, -50%);
  opacity: 0;
  transition: opacity 0.3s var(--ease);
}

.player:hover .player__knob,
.player__seekwrap:focus-visible .player__knob { opacity: 1; }

/* --- foot: time + volume --- */

.player__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.8rem;
}

.player__time {
  font-size: 0.7rem;
  font-weight: 300;
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
}

.player__sep { margin: 0 0.3rem; opacity: 0.55; }

.player__vol {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}

.player__mute {
  display: grid;
  place-items: center;
  padding: 0.15rem;
  background: none;
  border: 0;
  cursor: pointer;
  color: var(--text-faint);
  transition: color 0.4s var(--ease);
}

.player__mute:hover { color: var(--white); }
.player__mute svg { width: 0.95rem; height: 0.95rem; display: block; }

.player[data-muted="false"] .ico-muted,
.player[data-muted="true"]  .ico-vol { display: none; }
.player[data-muted="true"] .player__mute { color: var(--silver-400); }

.player__slider {
  -webkit-appearance: none;
  appearance: none;
  width: 5.5rem;
  height: 3px;
  border-radius: 999px;
  cursor: pointer;
  /* --vol is kept in sync by player.js so the filled part tracks the thumb */
  background: linear-gradient(
    to right,
    var(--white) calc(var(--vol, 24) * 1%),
    rgba(255, 255, 255, 0.14) calc(var(--vol, 24) * 1%)
  );
}

.player__slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--white);
  box-shadow: 0 0 0 3px rgba(237, 237, 237, 0.14);
  transition: box-shadow 0.3s var(--ease);
}

.player__slider:hover::-webkit-slider-thumb { box-shadow: 0 0 0 5px rgba(237, 237, 237, 0.2); }

.player__slider::-moz-range-thumb {
  width: 10px;
  height: 10px;
  border: 0;
  border-radius: 50%;
  background: var(--white);
  box-shadow: 0 0 0 3px rgba(237, 237, 237, 0.14);
}

/* On anything wide enough the player leaves the column and parks in the
   top-left corner of the viewport: it stops pushing the tiles down past the
   fold, and a persistent transport control belongs at a screen edge rather than
   buried under the links. Below 64rem it drops back into the flow, because a
   fixed panel would cover the hero on a phone.

   This block MUST sit after the base `.player` rule — same specificity means
   source order decides, and in front of it the base `width: 100%` wins and the
   panel spans the whole viewport. */
@media (min-width: 64rem) {
  .player {
    position: fixed;
    top: 1.15rem;
    left: 1.15rem;
    z-index: 20;
    width: 19.5rem;
    margin-top: 0;
  }
}

/* ==========================================================================
   Intro gate — the one click browsers insist on, turned into the entrance.

   Every visitor gets it, because that click is the only thing that guarantees
   audible playback: after a real gesture no browser can refuse. It also holds
   the staggered reveal below, so the page plays its entrance AS the overlay
   dissolves rather than having quietly finished behind it.
   ========================================================================== */

/* the reveal is PAUSED, not delayed — pausing stops the delay clock too, so
   removing .gated replays the whole stagger from the top */
html.gated [data-reveal] { animation-play-state: paused; }
html.gated body { overflow: hidden; }

.gate {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: grid;
  place-items: center;
  cursor: pointer;
  background:
    radial-gradient(70% 55% at 50% 44%, rgba(147, 147, 147, 0.14) 0%, transparent 70%),
    rgba(3, 3, 3, 0.86);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  animation: gate-in 0.55s var(--ease) both;
}

.gate[hidden] { display: none; }

/* Set just before removal, so it dissolves instead of vanishing.

   It deliberately KEEPS pointer-events. Setting `pointer-events: none` here
   makes tapping "click anywhere to enter" open a random link on a phone: a tap
   fires touchstart, the gate dismisses, and then the browser synthesises the
   mouse/click from that same tap — by which point the gate no longer
   hit-tested, so the click falls through to whichever tile is under the finger.
   Staying hit-testable for the 0.6s fade means that phantom click lands on the
   gate, which does nothing. The gate is `hidden` immediately after. */
.gate.is-leaving { animation: gate-out 0.6s var(--ease) forwards; }

@keyframes gate-in {
  from { opacity: 0; backdrop-filter: blur(0); }
  to   { opacity: 1; }
}

@keyframes gate-out {
  to { opacity: 0; transform: scale(1.02); }
}

.gate__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 0 1.5rem;
  animation: gate-rise 0.7s var(--ease) 0.08s both;
}

@keyframes gate-rise {
  from { opacity: 0; transform: translateY(12px); filter: blur(5px); }
  to   { opacity: 1; transform: none; filter: blur(0); }
}

.gate__art {
  position: relative;
  width: 5.4rem;
  height: 5.4rem;
  display: grid;
  place-items: center;
}

.gate__art img {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  object-fit: cover;
  border: 2px solid rgba(3, 3, 3, 0.9);
  box-shadow: 0 14px 40px -12px rgba(0, 0, 0, 0.9);
  user-select: none;
}

/* one slow pulse ring — reads as "waiting for you" without being a button */
.gate__ring {
  position: absolute;
  inset: -6%;
  border-radius: 50%;
  border: 1px solid rgba(198, 198, 198, 0.45);
  animation: gate-pulse 2.6s var(--ease-soft) infinite;
}

@keyframes gate-pulse {
  0%   { transform: scale(0.96); opacity: 0.9; }
  70%  { transform: scale(1.22); opacity: 0; }
  100% { transform: scale(1.22); opacity: 0; }
}

.gate__eq {
  display: inline-flex;
  align-items: flex-end;
  gap: 3px;
  height: 0.85rem;
  margin-top: 1.15rem;
}

.gate__eq i {
  width: 2px;
  height: 30%;
  border-radius: 1px;
  background: var(--platinum-100);
  animation: eq 0.85s var(--ease-soft) infinite alternate;
}

.gate__eq i:nth-child(2) { animation-delay: 0.16s; }
.gate__eq i:nth-child(3) { animation-delay: 0.33s; }
.gate__eq i:nth-child(4) { animation-delay: 0.5s; }
.gate__eq i:nth-child(5) { animation-delay: 0.67s; }

.gate__title {
  font-family: "Space Grotesk", "Inter", sans-serif;
  margin-top: 0.8rem;
  font-size: 1.35rem;
  font-weight: 700;
  letter-spacing: -0.04em;
  background: linear-gradient(172deg, #F4F4F4 26%, var(--silver-400) 100%);
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
}

.gate__hint {
  margin-top: 0.4rem;
  font-size: 0.76rem;
  font-weight: 300;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-faint);
  animation: gate-breathe 2.8s var(--ease-soft) infinite alternate;
}

@keyframes gate-breathe {
  from { opacity: 0.45; }
  to   { opacity: 0.95; }
}

/* ==========================================================================
   Footer + toast
   ========================================================================== */

.foot {
  margin-top: 1.25rem;
  font-size: 0.7rem;
  font-weight: 400;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-faint);
}


/* ==========================================================================
   Small screens
   ========================================================================== */

@media (max-width: 26rem) {
  .avatar { width: 7.25rem; height: 7.25rem; }
  .tile { padding: 0.85rem 0.95rem; gap: 0.8rem; }
  .tile--feature { padding: 1.05rem 0.95rem; }
  /* 0.74rem would be 11.8px — under the ~12px floor where small type stops
     being comfortable */
  .tile__title { font-size: 0.98rem; }
  .tile__sub { font-size: 0.8rem; }
  .tile__cta { padding: 0.3rem 0.7rem; font-size: 0.72rem; }
}

/* ==========================================================================
   Adaptive quality — see quality.js, which sets data-q on <html> from measured
   frame times. The page keeps its shape at every tier; what it sheds is the
   work that costs the most per frame and shows the least.
   ========================================================================== */

/* mid: the full-screen blend goes and the glass gets thinner. Everything still
   moves, so it still reads as the same page. */
[data-q="mid"] .grain { display: none; }

[data-q="mid"] .tile,
[data-q="mid"] .player,
[data-q="mid"] .subs {
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

/* low: nothing that costs per-frame work survives. The haze holds position
   instead of drifting, the backdrop blur goes (it re-blurs whenever anything
   moves behind it), and the cursor trail is gone — cursor.js parks its loop
   when it sees this tier, so the element is not merely hidden. smoke.js stops
   spawning on its own for the same reason.

   The scanlines STAY: they are static, and once the grain is gone and the haze
   is still, the whole .bg stack composites once and is reused. Hiding them
   would take the vignette with them, since the two are one element. */
[data-q="low"] .grain,
[data-q="low"] .cursor-glow { display: none; }

[data-q="low"] .haze { animation: none; }

[data-q="low"] .tile,
[data-q="low"] .player,
[data-q="low"] .subs,
[data-q="low"] .gate {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* the glass carried some of the tile's separation from the background, so give
   it back as opacity rather than leaving the tiles floating */
[data-q="low"] {
  --tile-bg: rgba(255, 255, 255, 0.07);
  --tile-bg-hover: rgba(255, 255, 255, 0.115);
}

/* ==========================================================================
   Reduced motion — keep the design, drop the movement
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  [data-reveal] { opacity: 1; transform: none; filter: none; }
  .grain { animation: none; }
  .avatar__ring { animation: none; }
}
