/* 播放页那一屏的样式：氛围背景、黑胶、歌词、进度与控制。
   从一起听页里抽出来的 —— 单人播放页和一起听页共用同一套观感，
   两边各留一份的话，改了一边另一边就会慢慢跑偏。

   这里刻意只放「播放页」的部分。一起听特有的那些（两个人头像、
   邀请、大厅、弹幕、抛小星星）不在这儿，它们留在 together.html 里。 */

:root {
  --accent:  #ff6b9d;
  --accent2: #c44dff;
  --accent3: #6c5ce7;
  /* 半透明的同色，只给光晕的尾巴收口用（见 .blob / .beam）。
     必须在这里给默认值：applyHue 只在取到封面色时才跑，没封面或者封面取不出
     色相时它不会执行 —— 变量没定义，var() 又没写兜底，整条 background 会
     在计算值阶段作废，色块直接消失。 */
  --accent-s:  rgba(255, 107, 157, .45);
  --accent2-s: rgba(196, 77, 255, .45);
  --accent3-s: rgba(108, 92, 231, .45);
  --glow: rgba(255, 107, 157, .45);
  --bg: #0a0a0f;

  /* 一个「设计像素」折合多少真像素。整套尺寸都写成 calc(N * var(--u))，
     于是全站按同一个系数缩放，不会出现「头像小了按钮还是那么大」。

     基准取宽和高里更紧的那个：横屏、窄屏、平板、桌面拿到的观感就都差不
     多。屏幕再大也不放大（封顶 1.18）—— 这是一屏之内的布局，元素跟着视口
     膨胀只会把那排按钮顶出去。

     它是长度不是数字（min/max 只接受同类型的参数），用的时候乘一下。

     默认值是给老内核兜底的：不认 min()/max() 的浏览器整条 --u 声明在代入
     var() 时才失败，属性会退回 initial（width:auto 之类），比不缩放还糟。
     所以下面把真正那版关在 @supports 里。 */
  --u: 1px;
}
@supports (width: min(1px, 1px)) {
  :root {
    --u: max(.8px, min(100vw / 355, 100vh / 760, 1.18px));
  }
}
/* 上面那条 100vh 量的是「浏览器工具栏收起来」时的最高值。手机上工具栏
   （地址栏、底部工具条）一摊开就吃掉一截，两头都错：
     · --u 按最高值算，内容就比看得见的那块高，底部那栏（进度、按钮）
       被顶到屏幕外面 —— 表现就是「得手动缩小页面比例才看得全」；
     · #app 钉在 top:0/bottom:0 上，底边同样落在工具栏底下，光把内容缩小也
       救不回来（缩了内容，那一栏还是贴着布局视口的底边）。
   换成 svh —— 它按「工具栏摊开」算，永远留得下。它只跟浏览器自己的工具栏
   有关，键盘弹出不会动它。认不出 svh 的内核这整条作废，自动退回上面那版。 */
@supports (height: 1svh) {
  :root {
    --u: max(.8px, min(100vw / 355, 100svh / 760, 1.18px));
  }
  /* top 和 height 都给了，bottom 得让开（不然三者同时生效时 height 赢、
     bottom 被忽略，靠的是那条容易看漏的规则，不如写明白）。 */
  #app {
    height: 100svh;
    bottom: auto;
  }
}

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

html, body {
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
  background: var(--bg);
  color: #eceaf4;
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
}

/* 整个应用钉在视口上。这一层是为了「键盘弹出来时页面别乱」：
   文档里没有任何可滚动的部分，浏览器就没法把它推来推去 —— 输入框获得
   焦点时那个「整页被顶歪、比例像是被拉伸了」就没了。
   刻意不写 overflow:hidden：内部要滚的地方（歌词、播放列表）各有自己的
   滚动容器，别在 iOS 上给它们添一层裁剪祖先。
   四边分开写而不是 inset:0 —— inset 这个简写要 Chromium 87，国产浏览器
   （vivo/iqoo 那类）内核常常更老，不认的话这条就整个失效，元素退回
   static 位置、尺寸塌成内容大小，整页就散了。长写是从上古就有的。 */
#app {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}

/* 页面看不见的时候（锁屏、切到别的 App、浮层被盖住）把氛围动画全停下。
   十几个元素在兜里照样满速转，纯烧电。JS 那边挂 visibilitychange。 */
html.page-hidden *, html.page-hidden *::before, html.page-hidden *::after {
  animation-play-state: paused !important;
}

/* touch-action: manipulation 关掉的是「双击缩放」。这一条必须挂在所有能点的
   东西上，不能只管 <button>：黑胶是 div[role=button]（要自己控制键盘焦点和
   外观，没法用真 button），于是原来一个都没沾上。

   为什么安卓看不出来：视口写了 width=device-width 之后，Chrome 会自己把双击
   缩放关掉；iOS Safari 不管这个，照样缩。表现就是连点的时候整页突然被放大
   一下又弹回来 —— 看着像上下抖了一下。 */
button, [role="button"] { font-family: inherit; touch-action: manipulation; }

/* ============================================================
   氛围背景：四层叠加，只动 transform / opacity
   ============================================================ */
.ambient {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

/* 底层：模糊的专辑封面，缓慢漂移，切歌时交叉淡入 */
.ambient-cover {
  position: absolute;
  /* 只留够平移的余量。原来是 -30%，配上 keyframes 里的缩放，那一层 64px
     的高斯模糊要按 160% 视口的面积栅格化 —— 手机 GPU 内存和算力都吃不消。 */
  top: -12%;
  right: -12%;
  bottom: -12%;
  left: -12%;
  background-size: cover;
  background-position: center;
  filter: blur(64px) saturate(1.5);
  opacity: 0;
  transition: opacity 1.2s ease;
  transform: translate3d(0, 0, 0);
  animation: drift 44s ease-in-out infinite alternate;
}
.ambient-cover.on { opacity: .5; }

/* 只平移，不缩放。带 filter 的层一旦缩放，模糊就得每帧重新算一遍；
   平移是合成层自己的活，模糊只算一次，之后就是贴图挪位置。
   这一条是手机发烫最大的一笔。 */
@keyframes drift {
  from { transform: translate3d(-3%, -2%, 0); }
  to   { transform: translate3d(3%, 2%, 0); }
}

/* 中层：跟着封面主色走的三团色块 */
.ambient-blobs { position: absolute; top: 0; right: 0; bottom: 0; left: 0; }
/* 径向渐变自己就是软边，原来那层 70px 的高斯模糊几乎是白烧 GPU：
   去掉之后肉眼基本看不出差别，手机上省下一大块。 */
.blob {
  position: absolute;
  border-radius: 50%;
  opacity: .62;
  will-change: transform;
}
/* 每个色块中间垫一层半透明的同色（--accent-s 之类），尾巴才是慢慢收口。
   直接「实色 → transparent」的话，渐变在终点处斜率一断，往外又是一整片恒定值，
   人眼靠马赫带就能看见一道弧线 —— 用户报的那条「线」就是这类收口。
   中间这档别取太小（试过 .20）：那等于把整团色块压暗一成多，氛围会明显变淡。
   取 .45 时前后两段斜率几乎一样，看不出坎，亮度也和原来持平。

   收口点必须落在 70.7% 以内：`circle` 的默认尺寸是「最远角」= 半径 ×√2，
   而元素自己的 border-radius 只到半径处（= 70.7%）。写大了（86% 那种），
   渐变还没归零就被圆边硬切，反而切出一道更明显的弧线。 */
.b1 {
  width: 78vw; height: 78vw; left: -18vw; top: 4vh;
  background: radial-gradient(circle, var(--accent) 0%, var(--accent-s) 36%, transparent 70%);
  animation: wander1 30s ease-in-out infinite alternate;
}
.b2 {
  width: 68vw; height: 68vw; right: -16vw; top: 26vh;
  background: radial-gradient(circle, var(--accent2) 0%, var(--accent2-s) 36%, transparent 70%);
  animation: wander2 38s ease-in-out infinite alternate;
}
.b3 {
  width: 84vw; height: 84vw; left: 6vw; bottom: -22vh;
  background: radial-gradient(circle, var(--accent3) 0%, var(--accent3-s) 36%, transparent 70%);
  animation: wander3 46s ease-in-out infinite alternate;
}

@keyframes wander1 { from { transform: translate3d(0,0,0); }    to { transform: translate3d(14vw, 10vh, 0); } }
@keyframes wander2 { from { transform: translate3d(0,0,0); }    to { transform: translate3d(-16vw, 14vh, 0); } }
@keyframes wander3 { from { transform: translate3d(0,0,0); }    to { transform: translate3d(10vw, -12vh, 0); } }

/* 斜着扫过的两束光。做成一个细长的椭圆只做平移 ——
   比一整层 conic 旋转便宜得多（那种得按旋转后的外接矩形重新栅格化）。
   用径向渐变而不是 linear-gradient：线性渐变的轴几乎是水平的，垂直方向颜色
   就是恒定的，元素的上下两条边会硬切出一条横线。径向自己就是软边，没有这个
   问题。首尾都停在自己屏幕外（-60vw → +170vw），回头那一下看不见，不会闪。 */
.ambient-beams { position: absolute; top: 0; right: 0; bottom: 0; left: 0; }
.beam {
  position: absolute;
  top: -16vh;
  left: -60vw;
  width: 46vw; height: 132vh;
  will-change: transform;
  animation-name: sweep;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
/* 尺寸写死成 ellipse 50% 50%（而不是默认的「最远角」），这样收口点一定落在
   元素内部 —— 默认那套的 100% 在角上，收口写 84% 就够不到，四角会留下一道硬边。 */
.beam.a {
  opacity: .24;
  background:
    radial-gradient(ellipse 50% 50% at 50% 50%, rgba(255,255,255,.5) 0%, rgba(255,255,255,.12) 34%, rgba(255,255,255,0) 62%),
    radial-gradient(ellipse 50% 50% at 50% 50%, var(--accent) 0%, var(--accent-s) 36%, transparent 78%);
  animation-duration: 43s;
}
.beam.b {
  opacity: .2;
  background:
    radial-gradient(ellipse 50% 50% at 50% 50%, rgba(255,255,255,.45) 0%, rgba(255,255,255,.1) 32%, rgba(255,255,255,0) 60%),
    radial-gradient(ellipse 50% 50% at 50% 50%, var(--accent3) 0%, var(--accent3-s) 36%, transparent 78%);
  animation-duration: 61s;
  animation-delay: -27s;
}
@keyframes sweep {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(230vw, 0, 0); }
}

/* 上层：漂浮光点 */
.particle {
  position: absolute;
  bottom: -6vh;
  width: 6px; height: 6px;
  border-radius: 50%;
  /* 光晕用渐变画，不用 filter: blur()。十几个小点各自挂一个 filter，
     每个都是一个要单独合成的层。 */
  background: radial-gradient(circle, rgba(255,255,255,.95) 0%, rgba(255,255,255,0) 70%);
  animation-name: float;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
@keyframes float {
  0%   { transform: translate3d(0, 0, 0) scale(var(--s, 1)); opacity: 0; }
  12%  { opacity: .55; }
  88%  { opacity: .55; }
  100% { transform: translate3d(0, -108vh, 0) scale(var(--s, 1)); opacity: 0; }
}

/* 飘着的爱心和气球。比光点显眼，是氛围的主要来源 ——
   所以放在蒙版之后，不被压暗；又因为整个 .ambient 在 .stage 下面，
   再怎么飘也盖不住文字。
   每个元素自己带 --dx / --s / --o / 时长，靠 JS 随机撒开，
   不然十几个一模一样的东西一起升会像故障。 */
.ambient-floats { position: absolute; top: 0; right: 0; bottom: 0; left: 0; }
.floaty {
  position: absolute;
  bottom: -14vh;
  line-height: 1;
  will-change: transform, opacity;
  animation-name: floatUp;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  /* 用 text-shadow 而不是 filter: drop-shadow —— 后者是独立的滤镜通道，
     每帧都要单独算一遍。text-shadow 跟着字形一起栅格化，白送。 */
  text-shadow: 0 0 10px rgba(255, 107, 157, .5);
  user-select: none;
}
@keyframes floatUp {
  0%   { transform: translate3d(0, 0, 0) scale(var(--s, 1)) rotate(0deg); opacity: 0; }
  12%  { opacity: var(--o, .6); }
  50%  { transform: translate3d(var(--dx, 4vw), -56vh, 0) scale(var(--s, 1)) rotate(14deg); }
  88%  { opacity: var(--o, .6); }
  100% { transform: translate3d(calc(var(--dx, 4vw) * -1), -120vh, 0) scale(var(--s, 1)) rotate(-14deg); opacity: 0; }
}

/* 固定的星空。74 颗点全写在同一个元素的 background-image 里 —— 一颗一个元素
   的话，就算不挂动画也是 74 个绘制对象；写成渐变层就只是一层，一次栅格化，
   之后再也不动。和上面那些光点、星芒是两种东西：那些在动，这些是钉在那儿的底子。
   坐标是写死的（不是随机生成的），每次刷新长得一模一样 —— 背景每次进来都换
   一副样子，反而像加载没完成。 */
.ambient-stars {
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  background-image:
    radial-gradient(1px 1px at 18.9% 2.8%, rgba(255,255,255,0.80), transparent),
    radial-gradient(1px 1px at 86.0% 53.6%, rgba(214,200,255,0.40), transparent),
    radial-gradient(1px 1px at 6.7% 83.7%, rgba(255,255,255,0.84), transparent),
    radial-gradient(1.3px 1.3px at 50.8% 6.7%, rgba(214,200,255,0.86), transparent),
    radial-gradient(1px 1px at 70.8% 2.1%, rgba(255,255,255,0.49), transparent),
    radial-gradient(1px 1px at 58.7% 24.7%, rgba(214,200,255,0.58), transparent),
    radial-gradient(1px 1px at 72.8% 42.7%, rgba(214,200,255,0.78), transparent),
    radial-gradient(1px 1px at 96.9% 18.7%, rgba(214,200,255,0.62), transparent),
    radial-gradient(1px 1px at 13.4% 61.5%, rgba(255,255,255,0.34), transparent),
    radial-gradient(1px 1px at 17.2% 47.0%, rgba(255,255,255,0.65), transparent),
    radial-gradient(1.7px 1.7px at 38.3% 12.8%, rgba(255,255,255,0.55), transparent),
    radial-gradient(1px 1px at 6.7% 42.5%, rgba(255,255,255,0.80), transparent),
    radial-gradient(1px 1px at 59.1% 79.6%, rgba(255,255,255,0.57), transparent),
    radial-gradient(1.3px 1.3px at 16.6% 92.1%, rgba(255,255,255,0.44), transparent),
    radial-gradient(1.3px 1.3px at 61.1% 2.4%, rgba(214,200,255,0.36), transparent),
    radial-gradient(1px 1px at 15.4% 54.9%, rgba(214,200,255,0.69), transparent),
    radial-gradient(1px 1px at 49.6% 23.2%, rgba(214,200,255,0.54), transparent),
    radial-gradient(1px 1px at 51.1% 72.6%, rgba(255,255,255,0.80), transparent),
    radial-gradient(1px 1px at 65.0% 90.0%, rgba(214,200,255,0.36), transparent),
    radial-gradient(1px 1px at 49.8% 56.5%, rgba(214,200,255,0.45), transparent),
    radial-gradient(1.3px 1.3px at 54.9% 68.8%, rgba(255,255,255,0.69), transparent),
    radial-gradient(1px 1px at 78.3% 10.8%, rgba(255,255,255,0.67), transparent),
    radial-gradient(1.3px 1.3px at 39.3% 80.8%, rgba(214,200,255,0.39), transparent),
    radial-gradient(1.3px 1.3px at 90.1% 33.6%, rgba(255,255,255,0.82), transparent),
    radial-gradient(1px 1px at 14.0% 76.6%, rgba(255,255,255,0.62), transparent),
    radial-gradient(1.3px 1.3px at 76.9% 38.5%, rgba(214,200,255,0.40), transparent),
    radial-gradient(1px 1px at 12.9% 71.8%, rgba(255,255,255,0.60), transparent),
    radial-gradient(1.3px 1.3px at 65.9% 60.2%, rgba(255,255,255,0.37), transparent),
    radial-gradient(1px 1px at 73.0% 72.7%, rgba(255,255,255,0.43), transparent),
    radial-gradient(1px 1px at 25.5% 26.5%, rgba(255,255,255,0.55), transparent),
    radial-gradient(1px 1px at 2.8% 3.2%, rgba(255,255,255,0.54), transparent),
    radial-gradient(1px 1px at 18.7% 67.5%, rgba(214,200,255,0.33), transparent),
    radial-gradient(1.7px 1.7px at 56.8% 38.1%, rgba(214,200,255,0.70), transparent),
    radial-gradient(1.7px 1.7px at 18.7% 83.6%, rgba(255,255,255,0.50), transparent),
    radial-gradient(1.3px 1.3px at 79.6% 16.2%, rgba(255,255,255,0.79), transparent),
    radial-gradient(1px 1px at 92.6% 58.3%, rgba(255,255,255,0.53), transparent),
    radial-gradient(1px 1px at 22.1% 99.6%, rgba(214,200,255,0.81), transparent),
    radial-gradient(1px 1px at 92.4% 1.6%, rgba(214,200,255,0.41), transparent),
    radial-gradient(1px 1px at 26.2% 8.8%, rgba(255,255,255,0.76), transparent),
    radial-gradient(1.7px 1.7px at 85.6% 23.3%, rgba(255,255,255,0.51), transparent),
    radial-gradient(1px 1px at 78.0% 68.4%, rgba(255,255,255,0.72), transparent),
    radial-gradient(1px 1px at 16.5% 19.4%, rgba(255,255,255,0.54), transparent),
    radial-gradient(1.3px 1.3px at 64.6% 74.7%, rgba(255,255,255,0.45), transparent),
    radial-gradient(1px 1px at 21.8% 41.8%, rgba(214,200,255,0.69), transparent),
    radial-gradient(1px 1px at 28.0% 52.8%, rgba(255,255,255,0.76), transparent),
    radial-gradient(1.3px 1.3px at 93.0% 29.2%, rgba(255,255,255,0.63), transparent),
    radial-gradient(1.7px 1.7px at 91.1% 11.2%, rgba(255,255,255,0.67), transparent),
    radial-gradient(1.7px 1.7px at 0.8% 73.9%, rgba(255,255,255,0.75), transparent),
    radial-gradient(1px 1px at 75.7% 21.8%, rgba(255,255,255,0.43), transparent),
    radial-gradient(1.3px 1.3px at 52.7% 17.3%, rgba(255,255,255,0.30), transparent),
    radial-gradient(1.7px 1.7px at 70.4% 97.6%, rgba(214,200,255,0.42), transparent),
    radial-gradient(1px 1px at 7.4% 57.8%, rgba(255,255,255,0.65), transparent),
    radial-gradient(1.7px 1.7px at 28.1% 83.0%, rgba(214,200,255,0.62), transparent),
    radial-gradient(1.7px 1.7px at 30.7% 66.3%, rgba(214,200,255,0.71), transparent),
    radial-gradient(1px 1px at 98.8% 5.1%, rgba(255,255,255,0.33), transparent),
    radial-gradient(1.3px 1.3px at 79.1% 26.6%, rgba(214,200,255,0.33), transparent),
    radial-gradient(1.3px 1.3px at 58.3% 55.5%, rgba(255,255,255,0.62), transparent),
    radial-gradient(1px 1px at 37.7% 4.2%, rgba(214,200,255,0.58), transparent),
    radial-gradient(1.3px 1.3px at 54.4% 46.5%, rgba(255,255,255,0.31), transparent),
    radial-gradient(1.3px 1.3px at 9.2% 66.2%, rgba(255,255,255,0.63), transparent),
    radial-gradient(1px 1px at 50.0% 63.1%, rgba(214,200,255,0.74), transparent),
    radial-gradient(1.3px 1.3px at 34.9% 33.5%, rgba(255,255,255,0.52), transparent),
    radial-gradient(1px 1px at 67.0% 11.5%, rgba(255,255,255,0.73), transparent),
    radial-gradient(1px 1px at 39.7% 73.3%, rgba(214,200,255,0.67), transparent),
    radial-gradient(1px 1px at 83.8% 93.1%, rgba(255,255,255,0.71), transparent),
    radial-gradient(1.3px 1.3px at 42.6% 30.4%, rgba(255,255,255,0.55), transparent),
    radial-gradient(1.3px 1.3px at 78.5% 81.9%, rgba(214,200,255,0.39), transparent),
    radial-gradient(1.3px 1.3px at 11.4% 13.2%, rgba(255,255,255,0.43), transparent),
    radial-gradient(1px 1px at 24.3% 77.4%, rgba(255,255,255,0.77), transparent),
    radial-gradient(1px 1px at 97.4% 45.5%, rgba(255,255,255,0.34), transparent),
    radial-gradient(1px 1px at 32.7% 20.4%, rgba(214,200,255,0.47), transparent),
    radial-gradient(1px 1px at 68.4% 36.4%, rgba(255,255,255,0.70), transparent),
    radial-gradient(1px 1px at 35.5% 8.5%, rgba(214,200,255,0.75), transparent),
    radial-gradient(1px 1px at 53.4% 31.2%, rgba(255,255,255,0.65), transparent);
}

/* 星芒。和光点（.particle）不是一回事：光点是圆的、往上飘，星芒是四角、
   原地一闪一闪。四角不是拿文字 ✦ 画的 —— 那个字形各家字体不一样，有的
   内核还会退回成五角。两个细长的径向渐变十字叠出来就是四角星：中间亮、
   四头尖，任何内核上长得都一样。

   数量压在 6 个：和光点、漂浮物一样，每一个都是一个常驻合成层，
   一多就是一直满载、一直发热。 */
.ambient-sparks { position: absolute; top: 0; right: 0; bottom: 0; left: 0; }
.spark {
  position: absolute;
  width: calc(24px * var(--sz, 1));
  height: calc(24px * var(--sz, 1));
  /* 尾巴照老规矩垫一档再收口（见 .blob 那段）。这个尺寸上其实看不出马赫带，
     但收口干净的成本是零。 */
  background:
    radial-gradient(ellipse 50% 8% at 50% 50%,
      rgba(255,255,255,.95) 0%, rgba(255,255,255,.40) 38%,
      rgba(255,255,255,.10) 72%, transparent 100%),
    radial-gradient(ellipse 8% 50% at 50% 50%,
      rgba(255,255,255,.95) 0%, rgba(255,255,255,.40) 38%,
      rgba(255,255,255,.10) 72%, transparent 100%);
  animation: twinkle 4s ease-in-out infinite;
  will-change: transform, opacity;
}
/* 缩到看不见再胀回来，顺带转 45° —— 四角星转到一半正好是个叉，
   一闪的过程里形状是活的；不然六颗星一起「一亮一暗」很呆。 */
@keyframes twinkle {
  0%, 100% { transform: scale(.35) rotate(0deg);  opacity: 0; }
  50%      { transform: scale(1)   rotate(45deg); opacity: 1; }
}

/* 呼吸光晕：一层暖玫瑰色的大光斑，很慢地一亮一暗。 */
.ambient-breath {
  position: absolute;
  left: 50%; top: 24%;
  width: 152vw; height: 152vw;
  margin: -76vw 0 0 -76vw;
  border-radius: 50%;
  background: radial-gradient(circle,
    rgba(255, 138, 168, .30) 0%,
    rgba(255, 107, 157, .15) 28%,
    rgba(196, 77, 255, .075) 50%,
    rgba(196, 77, 255, .028) 64%,
    transparent 70%);
  /* 不挂 filter：这本来就是四段式径向渐变，边已经够软了。而 breathe 是
     缩放动画 —— 带 filter 缩放等于每帧重算一遍 28px 模糊，还要按 152vw 的
     面积算。去掉之后呼吸感一模一样，GPU 直接闲下来。 */
  animation: breathe 9s ease-in-out infinite;
  will-change: transform, opacity;
}
@keyframes breathe {
  0%, 100% { transform: scale(.9);  opacity: .5; }
  50%      { transform: scale(1.08); opacity: 1; }
}

/* 蒙版：保证文字始终可读。
   两条都要是单调、平缓的，别出现「最亮的一条线」：线性那层原来在 40% 处
   由变亮转折成变暗，斜率一翻，人眼靠马赫带就看出一条横线；径向那层的亮心
   现在挪到 40% 高度，中间那一带亮起来，上下两头压住。 */
.ambient-scrim {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background:
    radial-gradient(ellipse 58% 52% at 50% 40%,
      rgba(10,10,15,.14) 0%, rgba(10,10,15,.50) 60%, rgba(10,10,15,.88) 100%),
    linear-gradient(to bottom,
      rgba(10,10,15,.38) 0%, rgba(10,10,15,.30) 46%, rgba(10,10,15,.80) 100%);
}

/* ============================================================
   舞台
   ============================================================ */
.stage {
  position: relative;
  z-index: 1;
  /* 高度以 #app 为准，不用 vh 单位。
     dvh 要 Chromium 108，国产浏览器（vivo/iqoo 那类）常常更老，会退回
     100vh —— 而 100vh 是「地址栏收起时」那一档高度，比眼前真能看见的
     那块高，底部那条就被顶到屏幕外面，看着就是「显示异常、根本看不全」。
     #app 是 fixed 贴 0，高度就是当前可视区（认得出 svh 的内核按 svh 算，
     见上面那条 @supports），拿它当基准比任何 vh 都稳。 */
  height: 100%;
  max-width: 520px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  /* 底下留一点，那一栏不压边。这一版是不带 env() 的：不认 env() 的浏览器
     整条声明解析不过，正好退回这里，而不是 padding 直接归零。 */
  padding: calc(40 * var(--u)) calc(14 * var(--u)) calc(10 * var(--u));
  padding: max(calc(40 * var(--u)), env(safe-area-inset-top, 0px))
           calc(14 * var(--u))
           max(calc(10 * var(--u)), env(safe-area-inset-bottom, 0px));
}

/* ---------- 中上：黑胶唱片 ---------- */
.disc-wrap {
  flex: 1 1 auto;
  min-height: 0;
  /* 兜底：高度真不够时宁可把唱片裁掉，也不许它压到下面的歌词上。
     正常情况下唱片本身已经按 dvh 缩过，这里裁不到东西。 */
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: calc(10 * var(--u));
  transition: flex-basis .38s ease, opacity .3s ease, transform .3s ease;
}
.lyrics-open .disc-wrap {
  flex: 0 0 0;
  opacity: 0;
  transform: scale(.82);
  overflow: hidden;
  pointer-events: none;
}

/* 唱片外面那颗大星芒。和 .spark 同一个画法，只是大一个量级，而且慢慢呼吸、
   不闪 —— 这个尺寸上一闪一闪会像故障灯。
   包一层 .vinyl-stage 是为了让星芒有个自己的定位参照：直接挂在 .vinyl 上的话
   它会跟着唱片一起转（.vinyl 上挂着 spin），而唱片暂停时那圈又停下，
   星芒就会自顾自地绕圈，看着像贴纸没贴住。 */
.vinyl-stage { position: relative; flex: 0 0 auto; }
.flare {
  position: absolute;
  /* 压在唱片偏右下一点，不是正中 —— 正中像靶心，偏一点才像光打在盘面上。 */
  left: 62%; top: 58%;
  width: calc(300 * var(--u)); height: calc(300 * var(--u));
  margin: calc(-150 * var(--u)) 0 0 calc(-150 * var(--u));
  pointer-events: none;
  /* 一定要斜着（28°），别摆成正十字 —— 正的横着那一道就是一条贯穿唱片的
     白线，和用户连着报过两次的「水平分割线」长得一模一样，只是更亮。
     斜过来读起来才是「光打在盘面上」。
     转完外接矩形会大一圈（√2 倍那套），超出 .disc-wrap 的部分会被 overflow
     裁掉 —— 裁掉的正好是四头的尖，那里 alpha 已经接近 0，看不见。 */
  background:
    radial-gradient(ellipse 50% 2% at 50% 50%,
      rgba(255,255,255,.62) 0%, rgba(255,255,255,.30) 22%,
      rgba(255,255,255,.13) 48%, rgba(255,255,255,.05) 72%, transparent 96%),
    radial-gradient(ellipse 2% 50% at 50% 50%,
      rgba(255,255,255,.62) 0%, rgba(255,255,255,.30) 22%,
      rgba(255,255,255,.13) 48%, rgba(255,255,255,.05) 72%, transparent 96%);
  animation: flareBreath 7s ease-in-out infinite;
}
@keyframes flareBreath {
  0%, 100% { opacity: .38; transform: rotate(28deg) scale(.94); }
  50%      { opacity: .78; transform: rotate(28deg) scale(1); }
}

.vinyl {
  position: relative;
  /* 随全局系数走，尺寸写在同一个基准上。第一对是不封顶的兜底版。
     240 是量出来的，不是拍的：竖屏长手机上 --u 是按宽度算的（约 1.0），
     这一版让唱片占屏宽约六成。再大就开始挤掉唱片下方歌名那两行。 */
  width: calc(240 * var(--u));
  height: calc(240 * var(--u));
  /* 再按视口高度封顶：--u 只看「更紧的那一边」，竖屏长手机上它是按宽度
     算出来的，唱片照样能顶破 disc-wrap。 */
  width: min(calc(240 * var(--u)), 30vh);
  height: min(calc(240 * var(--u)), 30vh);
  /* 高度跟着宽度写死，不指望 aspect-ratio（要 Chromium 88）—— 老内核上它
     不生效，高就是 auto，这张脸会被压成一个椭圆。dvh 那版放后面，不认的
     内核整条丢掉，自然退回上面那对 vh 值，宽高仍然相等。 */
  width: min(calc(240 * var(--u)), 30dvh);
  height: min(calc(240 * var(--u)), 30dvh);
  flex: 0 0 auto;
  aspect-ratio: 1;
  border-radius: 50%;
  background:
    repeating-radial-gradient(circle at 50% 50%,
      rgba(255,255,255,.045) 0px, rgba(255,255,255,.045) 1px,
      transparent 1px, transparent 5px),
    radial-gradient(circle at 50% 50%, #16161f 30%, #0d0d14 72%);
  box-shadow: 0 22px 56px -18px rgba(0,0,0,.9), 0 0 0 1px rgba(255,255,255,.06) inset;
  display: flex;
  align-items: center;
  justify-content: center;
  animation: spin 20s linear infinite;
  animation-play-state: paused;
}
.vinyl.spinning { animation-play-state: running; }

/* 黑胶转、加载圈转，共用这一条 */
@keyframes spin { to { transform: rotate(360deg); } }

.disc-art {
  /* 62% 那张黑胶的圈留得太宽，封面缩在里面显小。66% 让封面占回来一点，
     黑边还在（真唱片的标也就是这个量级），但「图」本身看得清了。
     注意这儿和 .disc-empty 要一起改，不然没封面时圆盘大小会跳一下。 */
  width: 66%; height: 66%;
  border-radius: 50%;
  object-fit: cover;
  box-shadow: 0 0 0 3px rgba(255,255,255,.07), 0 6px 22px rgba(0,0,0,.6);
  background: #1b1b28;
  animation: coverIn .5s ease;
}
/* 换歌时封面是硬切，看着很跳。元素带 :key，换 url 会重建，动画就重放一次 */
@keyframes coverIn {
  from { opacity: 0; transform: scale(.9) rotate(-8deg); }
  to   { opacity: 1; transform: scale(1) rotate(0); }
}
.disc-empty {
  /* 跟着 .disc-art 走，两边必须一样大 —— 这是没封面时的占位，尺寸对不上
     换歌的那一瞬圆盘会弹一下。 */
  width: 66%; height: 66%;
  border-radius: 50%;
  background: radial-gradient(circle at 40% 35%, #2a2a3e, #14141d);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: calc(30 * var(--u));
  color: #4d4a63;
}
.disc-hole {
  position: absolute;
  width: 9%; height: 9%;
  border-radius: 50%;
  background: #08080c;
  box-shadow: 0 0 0 2px rgba(255,255,255,.1);
}

/* 歌名/歌手这层原来没给宽度。disc-wrap 是 align-items:center 的 flex 列，
   子元素按内容收缩，而下面两个 max-width 又是百分比 —— 百分比套在一个
   「由内容决定、而内容又被百分比限制」的宽度上，浏览器只能收敛到很小的值：
   歌名被截成两三个字，歌手整行压成一道线。给足宽度就正常了。 */
.disc-info {
  width: 100%;
  min-width: 0;
  padding: 0 4%;
  box-sizing: border-box;
}

.now-title {
  font-size: calc(16 * var(--u));
  font-weight: 600;
  text-align: center;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  animation: fadeUp .4s ease;
}
@keyframes fadeUp {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}
.now-artist {
  font-size: calc(12 * var(--u));
  color: #9c99ad;
  margin-top: calc(4 * var(--u));
  text-align: center;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------- 中间：歌词 ---------- */
.lyric-panel {
  flex: 0 0 auto;
  height: 18vh;
  min-height: calc(72 * var(--u));
  display: flex;
  flex-direction: column;
  justify-content: center;
  position: relative;
  transition: flex-basis .38s ease;
}
.lyrics-open .lyric-panel {
  flex: 1 1 auto;
  height: auto;
  justify-content: flex-start;
}

/* 折叠态：只显示当前一行 */
.lyric-current {
  text-align: center;
  font-size: calc(16 * var(--u));
  line-height: 1.5;
  color: #fff;
  padding: 0 calc(8 * var(--u));
  text-shadow: 0 2px 14px rgba(0,0,0,.6);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.lyric-current.empty { color: #6c6a7e; font-size: calc(13 * var(--u)); }

.lyric-toggle {
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  width: calc(38 * var(--u)); height: calc(38 * var(--u));
  border: none;
  background: transparent;
  color: #8b88a0;
  font-size: calc(14 * var(--u));
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform .3s ease, color .2s;
}
.lyric-toggle:active { color: #fff; }
/* 展开后当前行正好落在面板正中，也就是这个箭头原本待的地方 —— 长句的尾巴
   会被箭头压住。挪到顶上那条渐隐带里，让开正文。 */
.lyrics-open .lyric-toggle { top: 4px; transform: rotate(180deg); }

/* 展开态：可滚动全列表。
   上下留白挂在里面的 .lyric-pad 上，不挂在这里 —— 挂这里的话 padding 本身就是
   776px，比歌词区（约 600px）还高，flex 再怎么缩也缩不到面板里，整块就漫出去
   盖到底部的进度条上；而且 scrollToActiveLyric 是按 clientHeight 的一半定位的，
   容器比看得见的区域高一截，当前行就落在视觉中心偏下。 */
.lyric-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 0;
  scrollbar-width: none;
  mask-image: linear-gradient(to bottom, transparent 0%, #000 16%, #000 84%, transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 16%, #000 84%, transparent 100%);
}
.lyric-scroll::-webkit-scrollbar { display: none; }

/* 上下各留半屏：第一行和最后一行都能滚到正中。留白比半屏多一点没关系，
   多出来的部分只是让人能滚过头一点。 */
.lyric-pad { padding: 46vh 6px; }

.lrc-line {
  padding: calc(9 * var(--u)) calc(10 * var(--u));
  font-size: calc(15 * var(--u));
  line-height: 1.5;
  text-align: center;
  color: #6f6c82;
  transition: color .28s ease, transform .28s ease, opacity .28s ease;
  opacity: .72;
  cursor: pointer;
}
.lrc-line.active {
  color: #fff;
  font-size: calc(17 * var(--u));
  font-weight: 600;
  opacity: 1;
  transform: scale(1.04);
  text-shadow: 0 0 20px var(--glow);
}
/* 播放列表和展开的歌词用的是同一个规矩：整行不再是点击目标，只有文字那一段
   是（歌名切歌、歌词跳进度）。行的空白处点一下是「返回」（关面板 / 收起歌词），
   所以光标得跟着分开，不然还是满行一只手。 */
.lrc-line.pick-row { cursor: default; }
.lrc-pick { cursor: pointer; }

/* ---------- 底部：进度与控制 ---------- */
.transport {
  flex: 0 0 auto;
  padding-top: calc(4 * var(--u));
  /* 不带 env() 的先写一条兜底，认不出 env() 的浏览器退回它，而不是 padding 归零 */
  padding-bottom: calc(8 * var(--u));
  padding-bottom: calc(8 * var(--u) + env(safe-area-inset-bottom, 0px));
}

.time-row {
  display: flex;
  justify-content: space-between;
  font-size: calc(10 * var(--u));
  color: #8b88a0;
  font-variant-numeric: tabular-nums;
  padding: 0 calc(2 * var(--u));
}

.progress-range {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: calc(26 * var(--u));
  background: transparent;
  cursor: pointer;
  display: block;
}
.progress-range::-webkit-slider-runnable-track {
  height: calc(4 * var(--u));
  border-radius: calc(2 * var(--u));
  background: linear-gradient(to right,
    var(--accent) 0%, var(--accent) var(--p, 0%),
    rgba(255,255,255,.15) var(--p, 0%), rgba(255,255,255,.15) 100%);
}
.progress-range::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: calc(13 * var(--u)); height: calc(13 * var(--u));
  margin-top: calc(-5 * var(--u));
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 12px var(--glow);
}
.progress-range::-moz-range-track {
  height: calc(4 * var(--u)); border-radius: calc(2 * var(--u));
  background: rgba(255,255,255,.15);
}
.progress-range::-moz-range-progress {
  height: calc(4 * var(--u)); border-radius: calc(2 * var(--u)); background: var(--accent);
}
.progress-range::-moz-range-thumb {
  width: calc(13 * var(--u)); height: calc(13 * var(--u)); border: none; border-radius: 50%;
  background: #fff; box-shadow: 0 0 12px var(--glow);
}

.controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: calc(8 * var(--u));
  padding: calc(2 * var(--u)) 0 calc(4 * var(--u));
}

.ctrl {
  width: calc(38 * var(--u)); height: calc(38 * var(--u));
  border: none;
  border-radius: 50%;
  background: transparent;
  color: #d6d3e4;
  font-size: calc(17 * var(--u));
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: transform .15s ease, background .2s;
}
.ctrl:active { transform: scale(.9); background: rgba(255,255,255,.07); }
.ctrl.mode { font-size: calc(15 * var(--u)); color: #8b88a0; }
.ctrl.mode.on { color: var(--accent); }

.play-big {
  width: calc(54 * var(--u)); height: calc(54 * var(--u));
  border: none;
  border-radius: 50%;
  background: linear-gradient(140deg, var(--accent), var(--accent2));
  color: #fff;
  font-size: calc(20 * var(--u));
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 calc(8 * var(--u)) calc(26 * var(--u)) calc(-6 * var(--u)) var(--glow);
  transition: transform .15s ease;
}
.play-big:active { transform: scale(.92); }
/* 自动播放被拦时，让播放键自己招手 */
.play-big.need-tap { animation: tapPulse 1.8s ease-in-out infinite; }
@keyframes tapPulse {
  0%, 100% { box-shadow: 0 calc(8 * var(--u)) calc(26 * var(--u)) calc(-6 * var(--u)) var(--glow); }
  50%      { box-shadow: 0 calc(8 * var(--u)) calc(26 * var(--u)) calc(-6 * var(--u)) var(--glow),
                        0 0 0 calc(12 * var(--u)) rgba(255,255,255,.06); }
}

/* 收起 / 退出。原来是个 ✕，图标看不出是干什么的，现在写成字。 */
.exit-btn {
  position: fixed;
  top: calc(8 * var(--u));
  top: calc(8 * var(--u) + env(safe-area-inset-top, 0px));
  right: calc(12 * var(--u));
  z-index: 60;
  height: calc(30 * var(--u));
  padding: 0 calc(13 * var(--u));
  border: 1px solid rgba(255,255,255,.16);
  border-radius: calc(15 * var(--u));
  /* 原来是 rgba(20,18,30,.6) + backdrop-filter: blur(6px)。磨砂要成立，合成器
     就得把身后那片重新抓一遍再模糊 —— 而身后是永远在动的氛围背景，于是每帧
     都得重来。这颗按钮是常驻的，等于一秒钟白烧六十次模糊。把不透明度提上去
     顶掉模糊的观感，肉眼几乎分不出来，省下来的是实打实的。 */
  background: rgba(20,18,30,.82);
  color: #cfcbdd;
  font-size: calc(12 * var(--u));
  white-space: nowrap;
  cursor: pointer;
}
.exit-btn:active { transform: scale(.94); }
/* 播放页把它摆在左上：右上角留给「切到歌词」，两颗挨着容易点错。 */
.exit-btn.left {
  left: calc(12 * var(--u));
  left: calc(12 * var(--u) + env(safe-area-inset-left, 0px));
  right: auto;
}

/* ---------- 面板（播放列表） ---------- */
.sheet {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: calc(18 * var(--u));
  padding: calc(28 * var(--u)) calc(24 * var(--u));
  padding-bottom: calc(28 * var(--u) + env(safe-area-inset-bottom, 0px));
  /* 原来这层是 rgba(8,8,13,.9) + backdrop-filter: blur(16px)，一整屏的磨砂。
     代价很大：身后那套氛围背景一直在动，合成器每帧都要把整屏重新抓一遍再
     模糊。0.9 的底子本来就快看不见后面了，抹平成 .96 观感几乎一样。 */
  background: rgba(8,8,13,.96);
  text-align: center;
}
.sheet-title { font-size: calc(18 * var(--u)); font-weight: 600; }
.sheet-sub { font-size: calc(13 * var(--u)); color: #9c99ad; line-height: 1.7; max-width: calc(260 * var(--u)); }
.sheet-link { color: #8b88a0; font-size: calc(12 * var(--u)); text-decoration: underline; border: none; background: none; cursor: pointer; padding: calc(6 * var(--u)); }

/* 提示条要够显眼 —— 它说的都是「这首放不了」这类一眼就得看到的事。 */
.toast {
  position: fixed;
  left: 50%;
  top: calc(16 * var(--u));
  top: calc(16 * var(--u) + env(safe-area-inset-top, 0px));
  transform: translateX(-50%);
  z-index: 40;
  padding: calc(12 * var(--u)) calc(22 * var(--u));
  border-radius: calc(22 * var(--u));
  background: rgba(26,26,38,.97);
  border: 1px solid rgba(255,255,255,.22);
  font-size: calc(15 * var(--u));
  font-weight: 600;
  letter-spacing: .2px;
  color: #fff;
  white-space: nowrap;
  /* 歌名可能很长，别让提示条顶出屏幕 */
  max-width: min(88vw, calc(430 * var(--u)));
  overflow: hidden;
  text-overflow: ellipsis;
  box-shadow: 0 calc(10 * var(--u)) calc(30 * var(--u)) calc(-8 * var(--u)) rgba(0,0,0,.65),
              0 0 calc(18 * var(--u)) calc(-4 * var(--u)) var(--glow);
  animation: toastIn .3s ease;
}
@keyframes toastIn {
  from { opacity: 0; transform: translate(-50%, -10px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}

/* 加载中那条。长得跟 .toast 一样，区别是它不会自己走 —— 挂到 loadingLabel
   变空为止（也就是真出声那一刻）。切歌到出声之间可能好几秒：服务端得先把
   整首从 QQ 拉下来缓存。没有这条就是「点了没反应」。 */
.loading-toast {
  position: fixed;
  left: 50%;
  top: calc(16 * var(--u));
  top: calc(16 * var(--u) + env(safe-area-inset-top, 0px));
  transform: translateX(-50%);
  z-index: 41;
  display: flex;
  align-items: center;
  gap: calc(10 * var(--u));
  padding: calc(12 * var(--u)) calc(22 * var(--u));
  border-radius: calc(22 * var(--u));
  background: rgba(26,26,38,.97);
  border: 1px solid rgba(255,255,255,.22);
  font-size: calc(15 * var(--u));
  font-weight: 600;
  letter-spacing: .2px;
  color: #fff;
  white-space: nowrap;
  max-width: min(88vw, calc(430 * var(--u)));
  overflow: hidden;
  text-overflow: ellipsis;
  box-shadow: 0 calc(10 * var(--u)) calc(30 * var(--u)) calc(-8 * var(--u)) rgba(0,0,0,.65),
              0 0 calc(18 * var(--u)) calc(-4 * var(--u)) var(--glow);
  animation: toastIn .3s ease;
}
.loading-toast .spinner {
  flex: none;
  width: calc(14 * var(--u)); height: calc(14 * var(--u));
  border: 2px solid rgba(255,255,255,.25);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin .8s linear infinite;
}
/* 两条同时在的时候错开，别叠在一起（上面那条从 16u 起、约 45u 高） */
.toast.pushed-down { top: calc(68 * var(--u)); }
.toast.pushed-down { top: calc(68 * var(--u) + env(safe-area-inset-top, 0px)); }

.preparing { font-size: calc(12 * var(--u)); color: var(--accent); animation: pulse 1.5s ease-in-out infinite; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .4; } }
/* 「我这边还没出声，点一下播放键」。和 .preparing 分开：那句是在说「等着就行」，
   这句是在说「得你动一下」，不该跟着一起闪。 */
.tap-hint { font-size: calc(12 * var(--u)); color: var(--accent); }

/* 无障碍：尊重系统的减弱动态效果 */
@media (prefers-reduced-motion: reduce) {
  .ambient-cover, .blob, .particle, .floaty, .vinyl,
  .preparing, .play-big.need-tap,
  .loading-toast, .loading-toast .spinner,
  .ambient-breath, .disc-art, .now-title,
  .beam, .spark, .flare {
    animation: none !important;
  }
  .ambient-cover.on { opacity: .38; }
  .particle, .floaty, .spark { display: none; }
  /* 不动了就固定在最亮那一下，别停在半暗 */
  .ambient-breath { opacity: .8; }
  .lyric-scroll { scroll-behavior: auto; }
}
