/* p3.17①:播放器样式。断点参考值:手机 <700px / 中等(iPad)700~1200px /
   桌面 ≥1200px——三套响应式 DOM(__mobile 底部灵动岛 / __tablet 右下角
   圆圈弹窗 / __desktop 导航胶囊旁圆圈弹窗)全部渲染,这里只靠 display
   切换哪一套可见,和这个主题 tab-bar/nav-pill 两套 DOM 常年共存、CSS
   切换可见性是同一个既有模式。*/

.ask-player { --ask-player-radius: var(--radius-row, 14px); }

/* p3.17①:三态断点改成和站点自己的导航胶囊/tab-bar 切换点(components.css
   里 nav-pill/tab-bar 用的 700px)完全对齐——旧版这里用的是 768px/1200px
   两个和站点胶囊断点(700px)不一致的数字,700~767.98px 这段窄窗口里出
   现"桌面态导航胶囊已经显示,播放器却还按手机断点渲染"的错位,是
   "手机首页出现两个灵动岛"这个 bug 的诱因之一(另一个更直接的根因是
   下面 .ask-player__desktop-island 那条已删除的规则曾经在没有任何媒体
   查询包裹的情况下无条件写了 display:flex,把这条 display:none 的默认
   值和下面 @media 的响应式开关都覆盖掉了——完整诊断见 build log)。现
   在只剩三个断点入口,和站点其它 chrome 完全同步。 */
.ask-player__desktop, .ask-player__tablet, .ask-player__mobile { display: none; }

@media (max-width: 699.98px) { .ask-player__mobile { display: block; } }
@media (min-width: 700px) and (max-width: 1199.98px) { .ask-player__tablet { display: block; } }
@media (min-width: 1200px) { .ask-player__desktop { display: block; } }

/* ==================== 通用封面组件 ==================== */
.ask-player__cover {
  position: relative; overflow: hidden; border-radius: 20%; flex: none;
  display: flex; align-items: center; justify-content: center;
}
.ask-player__cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ask-player__cover--emoji {
  background:
    radial-gradient(75% 60% at 50% 30%, hsl(var(--h) 58% 52% / .55), transparent 70%),
    linear-gradient(180deg, hsl(var(--h) 58% 34%), hsl(var(--h) 58% 18%));
}
html[data-theme="light"] .ask-player__cover--emoji,
:root:not([data-theme="dark"]) .ask-player__cover--emoji {
  background:
    radial-gradient(75% 60% at 50% 28%, hsl(var(--h) 58% 90% / .70), transparent 72%),
    linear-gradient(180deg, hsl(var(--h) 58% 80%), hsl(var(--h) 58% 60%));
}
.ask-player__cover--emoji img.fe { width: 60%; height: 60%; object-fit: contain; }
.ask-player__cover--island { width: 28px; height: 28px; }
.ask-player__cover--circle { width: 100%; height: 100%; border-radius: 50%; }
.ask-player__cover--list { width: 36px; height: 36px; }
.ask-player__cover--coverflow { width: 100%; height: 100%; border-radius: 4%; }

/* ==================== 手机:灵动岛迷你条 ==================== */
/* p3.13 三处修正(对照 Collin 图 4):①上移——这个主题移动端自己有一条
   64px 高的底部 tab-bar(.tab-bar,components.css),之前灵动岛只留了
   safe-area 的量,会整个盖在 tab-bar 上面挡住首页/分类/歌单/关于四个
   入口;bottom 偏移量改成 tab-bar 高度(64px)+ 间距 + safe-area 三者
   叠加,浮在 tab-bar 上方而不是压在它上面。②仅播放时出现——不再常
   驻,JS 那边(player.js renderAll)按 state.playing 切换 .is-visible
   class,没有歌在放就整个隐藏(display:none 之外还加了淡入淡出,不是
   生硬地瞬间消失)。③做小巧——宽度从"几乎顶满屏幕"的 92vw/360px 收窄
   到贴合内容的紧凑胶囊(不再撑满,内容本身决定宽度,靠 fit-content)。 */
/* p3.16 B1 bug 修正:原来 width:fit-content(整个胶囊跟着歌名文字内容
   撑缩),歌名一长一短切歌时,右边的波形/播放按钮就跟着挪位置("音浪
   和控制按钮的位置取决于歌名长度")。改成 width 用固定公式
   (viewport 相关,但和"当前这首歌歌名多长"完全无关)算出来的固定
   值,歌名区 flex:1+min-width:0 永远占掉"总宽度减去封面/波形/播放按
   钮这几个固定宽度元素"之后剩下的那一份空间——不管歌名文字实际多
   长,这个区域的宽度本身是恒定的,波形和播放按钮的位置因此也恒定,
   歌名文字太长时交给下面 .ask-player__island-title-text 的跑马灯在
   这个固定宽度的窗口内部滚动,不会撑开外层。 */
.ask-player__island {
  position: fixed; left: 50%; bottom: calc(64px + 10px + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%) translateY(0);
  width: min(78vw, 320px); height: 44px;
  display: flex; align-items: center; gap: 8px; padding: 0 14px 0 5px;
  background: rgb(20 20 22 / .88); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
  border-radius: 999px; box-shadow: 0 8px 24px rgb(0 0 0 / .35);
  z-index: 245; cursor: pointer;
  opacity: 0; pointer-events: none;
  transition: opacity 220ms ease, transform 220ms ease;
}
.ask-player__island.is-visible { opacity: 1; pointer-events: auto; transform: translateX(-50%) translateY(0); }
.ask-player__island:not(.is-visible) { transform: translateX(-50%) translateY(8px); }
.ask-player__island-cover { width: 30px; height: 30px; border-radius: 9px; overflow: hidden; flex: none; }
.ask-player__island-title { flex: 1 1 auto; min-width: 0; overflow: hidden; }
.ask-player__island-title-text {
  display: inline-block; white-space: nowrap;
  color: #fff; font: 500 13px/1.3 var(--font-ui);
}
.ask-player__waveform { display: flex; align-items: center; gap: 2px; height: 16px; flex: none; }
.ask-player__waveform-bar {
  width: 3px; height: 6px; border-radius: 2px; background: #fff; opacity: .5;
  transform-origin: center; animation: none;
}
.ask-player__waveform.is-playing .ask-player__waveform-bar {
  animation-name: ask-wave; animation-timing-function: ease-in-out; animation-iteration-count: infinite; animation-direction: alternate;
}
@keyframes ask-wave { from { transform: scaleY(.4); opacity: .5; } to { transform: scaleY(1.6); opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .ask-player__waveform.is-playing .ask-player__waveform-bar { animation: none; opacity: .9; } }

/* p3.16 B1:歌名跑马灯——只有 player.js buildMarqueeBox() 量出来真的溢
   出(scrollWidth > 容器 clientWidth)才会挂 is-marquee,没溢出的短歌
   名保持静止(不是所有歌名都无差别地跑起来)。往返式(alternate),不
   是常见的首尾拼接单向循环——省掉复制一份文字做无缝衔接的复杂度,和
   这个文件里波形动画的 alternate 手法一致。 */
.ask-player__island-title-text.is-marquee,
.ask-coverflow__title-text.is-marquee,
.ask-player__inline-title-text.is-marquee {
  animation: ask-marquee 3.6s ease-in-out infinite alternate;
}
@keyframes ask-marquee { from { transform: translateX(0); } to { transform: translateX(var(--marquee-distance, 0)); } }
@media (prefers-reduced-motion: reduce) {
  .ask-player__island-title-text.is-marquee,
  .ask-coverflow__title-text.is-marquee,
  .ask-player__inline-title-text.is-marquee { animation: none; }
}
.ask-player__island-play {
  width: 26px; height: 26px; border-radius: 50%; background: #fff; color: #111;
  display: flex; align-items: center; justify-content: center; flex: none;
}
.ask-player__island-play svg { width: 14px; height: 14px; }

/* ==================== 手机:上滑全屏播放器 ==================== */
.ask-player__sheet-mask {
  position: fixed; inset: 0; background: rgb(0 0 0 / .5); z-index: 260;
  opacity: 0; pointer-events: none; transition: opacity 320ms ease;
}
.ask-player__sheet-mask.is-open { opacity: 1; pointer-events: auto; }
html.ask-player-no-scroll { overflow: hidden; }

/* p3.13 bug 修正:手机全屏播放器本应只占 75vh(从底部弹起,顶部露出
   ~25% 背景,符合 iOS sheet 手势直觉),之前错写成 inset:0 占满 100%
   整屏——改成 bottom:0 起、固定 75vh 高、顶部两只圆角,和真正的 iOS
   sheet 一样"没有铺满、能看到上方还有一层"。
   p3.15①再修正:这里原来还带了 overflow-y:auto,等于整个 sheet 内容
   (CoverFlow→进度→音量→控制→歌词→歌单)是一整条可以被手指下拉滚动
   的区域——手势和"下滑关闭"的 grabber 拖拽（见 player.js
   onStart/onMove,只绑在 .ask-player__sheet-grabber 上）很容易混淆,一
   不小心把整个播放器当成列表拖走。总指挥原话"不允许整页被手势下拉滚
   动……关闭播放器用顶部下拉横条/返回,不是整页拖"——去掉这个
   overflow,sheet 主体固定不滚动,只有下面 .ask-player__full-list 自
   己的 overflow-y:auto 负责"看更多歌"这件事,两种手势不再共享同一个
   可滚动区域。 */
.ask-player__sheet {
  position: fixed; left: 0; right: 0; bottom: 0; top: auto; height: 75vh; z-index: 261;
  border-radius: 20px 20px 0 0;
  background: linear-gradient(180deg, hsl(var(--h, 210) 45% 16%), hsl(var(--h, 210) 55% 7%));
  color: #fff; padding: 10px 20px calc(16px + env(safe-area-inset-bottom, 0px));
  overflow: hidden;
  display: flex; flex-direction: column;
  transform: translateY(100%);
  transition: transform 380ms cubic-bezier(.2,.9,.2,1);
}
.ask-player__sheet.is-open { transform: translateY(0); }
/* 同 .ask-player__modal-mask 那条(见其注释完整根因)——手机全屏 sheet
   关闭时靠 translateY(100%) 平移到视口外,不是没有物理尺寸,里面的
   CoverFlow 卡片同样带着行内 pointer-events:auto,理论上也可能在极端
   滚动/布局情况下盖住别的可点元素,一并加上防御。 */
.ask-player__sheet:not(.is-open) .ask-coverflow__card {
  pointer-events: none !important;
}
.ask-player__sheet-grabber { width: 36px; height: 5px; border-radius: 999px; background: rgb(255 255 255 / .35); margin: 6px auto 8px; }
.ask-player__sheet-close {
  position: absolute; top: 14px; right: 18px; color: rgb(255 255 255 / .7);
  font: 500 14px var(--font-ui); background: none; padding: 6px 10px;
}

/* ==================== 手机+平板共用:全屏内容(CoverFlow→倒影区叠歌名→进度→音量→歌词→列表) ==================== */
/* p3.15①:sheet 主体不再整体滚动(见上面 .ask-player__sheet 的改动说
   明),这个内容区改成纵向 flex,除歌单列表外的兄弟元素都是
   flex:none(按内容自身高度排布,不参与拉伸),歌单列表拿 flex:1 吃掉
   "取消整页下拉+歌名挪进 CoverFlow"腾出来的全部剩余空间——多出多少空
   间,歌单区就长多高,不是写死一个固定像素值。min-height:0 是让 flex
   子元素的 overflow-y:auto 真的生效必须加的(flex 默认子项
   min-height:auto,内容撑高了就不会触发内部滚动,这是 flex 布局的既
   知坑,不是多余的写法)。 */
.ask-player__full-content {
  display: flex; flex-direction: column; align-items: stretch; gap: 8px;
  max-width: 420px; width: 100%; margin: 0 auto; padding-top: 8px;
  flex: 1; min-height: 0;
}
.ask-player__full-content > * { flex: none; }

/* p3.13 bug 修正:倒影之前会从卡片底部继续往下延伸,超出 .ask-coverflow
   自己的盒子(容器高度只等于卡片高度,倒影是 position:absolute 不占布局
   空间但视觉上会画到下一个 flex 兄弟——歌名/进度条那些元素——的区域
   里,糊成一片看不清)。修法:容器整体留出比卡片更高的空间(卡片只占
   62%,倒影占接下来的 38%,两者相加不超过 100%),再加 overflow:hidden
   兜底裁掉任何残余溢出——这样倒影完整落在容器自己的区域内,不会侵入
   下方的歌名/进度条。 */
/* p3.15①:34vh/280px 那版是歌名单独占一整行时期的尺寸,现在歌名叠进
   了倒影区自己不再占独立行、sheet 又不整体滚动了,收窄到 28vh/230px
   给下面的歌单列表腾出实测够用的"约三行"高度(见
   .ask-player__full-list 的说明和 build log 里的实测像素记录)。 */
.ask-coverflow { position: relative; height: 25vh; max-height: 205px; perspective: 1000px; margin-bottom: 4px; overflow: hidden; }
.ask-coverflow__track { position: relative; width: 100%; height: 100%; transform-style: preserve-3d; }
.ask-coverflow__card {
  position: absolute; top: 0; left: 50%; width: 62%; height: 62%; margin-left: -31%;
  transform-style: preserve-3d; backface-visibility: hidden;
  transition: transform 360ms cubic-bezier(.2,.9,.2,1), opacity 360ms ease;
  cursor: pointer;
}
.ask-coverflow__card > .ask-player__cover { width: 100%; height: 100%; box-shadow: 0 20px 40px rgb(0 0 0 / .4); }
.ask-coverflow__reflection {
  position: absolute; top: 100%; left: 0; width: 100%; height: 60%;
  transform: scaleY(-1); opacity: .32;
  -webkit-mask-image: linear-gradient(180deg, rgb(0 0 0 / .55), transparent 85%);
  mask-image: linear-gradient(180deg, rgb(0 0 0 / .55), transparent 85%);
  pointer-events: none;
}
.ask-coverflow__reflection .ask-player__cover { width: 100%; height: 100%; }
.ask-coverflow__nav {
  position: absolute; top: 31%; transform: translateY(-50%); z-index: 6;
  width: 34px; height: 34px; border-radius: 50%; background: rgb(255 255 255 / .12);
  color: #fff; display: flex; align-items: center; justify-content: center;
}
.ask-coverflow__nav svg { width: 18px; height: 18px; }
.ask-coverflow__nav--prev { left: 4px; }
.ask-coverflow__nav--next { right: 4px; }

/* p3.15①:歌名/歌手叠在倒影区域(卡片占容器高度 62%,倒影接着占约
   38%——见上面 .ask-coverflow 系列注释)中段,不再是下面单独一行。
   pointer-events:none 不挡 CoverFlow 拖拽/点击,text-shadow 保证叠在
   会动的倒影图案上仍然清晰可读(倒影本身已经有透明度渐变,但专辑封
   面颜色深浅不定,单靠倒影的暗度不够稳定,阴影是必须的,不是可有可
   无的装饰)。歌名加粗、加大到 19px(原来 18px/700 已经是粗体,这轮再
   提一档,和总指挥"歌名字体加粗"的层级要求对齐,歌手保持次一级)。 */
.ask-coverflow__titlewrap {
  position: absolute; left: 0; right: 0; top: 76%; z-index: 7;
  text-align: center; pointer-events: none; padding: 0 14px;
}
/* p3.16 B1:歌名从"整块 ellipsis 截断"换成"固定窗口+内层 span 跑马
   灯"(applyMarqueeText,和灵动岛共用)。这里是全文里唯一需要
   justify-content 在"居中"和"靠左"之间切换的一处——不溢出时沿用原来
   居中的观感(justify-content:center),真的溢出触发跑马灯时改成靠左
   起步(flex-start),不然居中的长文字被两头裁切、动画起始位置会很奇
   怪。 */
.ask-coverflow__title { display: flex; justify-content: center; overflow: hidden; }
.ask-coverflow__title.is-marquee { justify-content: flex-start; }
.ask-coverflow__title-text {
  display: inline-block; white-space: nowrap;
  font: 800 19px/1.3 var(--font-ui); color: #fff;
  text-shadow: 0 2px 10px rgb(0 0 0 / .55), 0 1px 2px rgb(0 0 0 / .4);
}
.ask-coverflow__artist {
  font: 400 13px/1.4 var(--font-ui); color: rgb(255 255 255 / .78); margin-top: 2px;
  text-shadow: 0 2px 8px rgb(0 0 0 / .5);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.ask-player__progress-track {
  position: relative; height: 4px; border-radius: 999px; background: rgb(255 255 255 / .25); cursor: pointer;
}
.ask-player__progress-fill { position: absolute; top: 0; left: 0; height: 100%; border-radius: 999px; background: #fff; }
.ask-player__progress-knob {
  position: absolute; top: 50%; width: 12px; height: 12px; border-radius: 50%; background: #fff;
  transform: translate(-50%, -50%); box-shadow: 0 1px 4px rgb(0 0 0 / .4);
}
.ask-player__progress-times { display: flex; justify-content: space-between; font: 400 11px var(--font-ui); color: rgb(255 255 255 / .55); margin-top: 6px; font-variant-numeric: tabular-nums; }

.ask-player__volume { display: flex; align-items: center; gap: 8px; }
.ask-player__volume-icon { flex: none; color: rgb(255 255 255 / .55); }
.ask-player__volume-icon svg { width: 16px; height: 16px; display: block; }
.ask-player__volume-track { position: relative; flex: 1; height: 4px; border-radius: 999px; background: rgb(255 255 255 / .25); cursor: pointer; }
.ask-player__volume-fill { position: absolute; top: 0; left: 0; height: 100%; border-radius: 999px; background: rgb(255 255 255 / .8); }

.ask-player__full-controls { display: flex; align-items: center; justify-content: center; gap: 28px; }
.ask-player__full-ctrlbtn { color: #fff; width: 28px; height: 28px; }
.ask-player__full-ctrlbtn svg { width: 100%; height: 100%; display: block; }
.ask-player__full-playbtn {
  width: 56px; height: 56px; border-radius: 50%; background: #fff; color: #111;
  display: flex; align-items: center; justify-content: center;
}
.ask-player__full-playbtn svg { width: 24px; height: 24px; }

/* p3.20⑤:固定 3 行可视高度(21px 行高 × 1.5 line-height × 3 ≈
   94.5px,取整 96px)+ overflow:hidden——真正滚动的是里面的
   .ask-player__lyrics-track(内容整条歌词,通过 translateY 位移),这一
   层只负责"开一个固定大小的窗口,超出的部分裁掉"。 */
.ask-player__lyrics { position: relative; height: 96px; overflow: hidden; text-align: center; }
.ask-player__lyrics-track {
  transition: transform 420ms var(--ease-spring);
}
.ask-player__lyric-line {
  height: 32px; line-height: 32px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font: 500 14px/32px var(--font-ui); color: rgb(255 255 255 / .35);
  opacity: .5; transform: scale(.92); transition: opacity 280ms ease, transform 280ms ease, color 280ms ease;
}
.ask-player__lyric-line.is-current { color: #fff; opacity: 1; transform: scale(1); font-weight: 700; }
@media (prefers-reduced-motion: reduce) {
  .ask-player__lyrics-track { transition: none; }
}

/* p3.13 补的"下方歌曲列表"(总指挥原话"下方歌词/列表区没了"),p3.15①
   ②再修正两处:①数据源从"最多截三首"改成完整剩余歌单(见
   player.js buildSongList() 的改动说明),不然有 CSS 滚动容器也没有第
   四首可滚;②高度从写死的 132px 改成 flex:1 吃满 sheet 取消整页滚动
   后腾出的剩余空间(見 .ask-player__full-content 的 flex 说明)——视
   觉上仍然是"一屏大约三首歌的高度",但具体像素值由页面实际布局决
   定,不是一个固定死数。超出部分靠这个列表自己的 overflow-y:auto 滚
   动,不影响 sheet 主体。 */
.ask-player__full-list {
  display: flex; flex-direction: column; gap: 2px;
  flex: 1; min-height: 0; overflow-y: auto;
  padding-top: 10px; border-top: 1px solid rgb(255 255 255 / .12);
}

/* p3.22②:实测(见 build log)桌面弹窗这套 flex:1/min-height:0/overflow-y:auto
   链路本身是通的——弹窗真的封顶在 82vh(738px @900px 视口),歌单真的内
   部滚动(clientHeight 244 < scrollHeight 1204)。真实存在的问题是另一
   件事:flex:1 让歌单吃掉的"剩余空间"是个随上方 CoverFlow/进度/歌词等
   兄弟高度浮动的余数,几乎不可能刚好等于整数行高的倍数——边界那一行
   永远卡在半行高的位置被硬裁一刀,矮的那一截文字在截图里糊成一团难以
   辨认的样子(总指挥看到的"歌单还是无限拉长"很可能就是在看这个被从中
   腰斩的边界行,而不是真的没有上限)。
   两条防御一起上,不改这条链路本身的结构:
   ①max-height 用一行高(50px 实测)的整数倍封一道上限,天再高的视口
   歌单也不会长得离谱,和总指挥给的"最多 5 首"参考量级对齐;
   ②mask-image 在底部裁出一小段渐隐,不管边界具体停在整行的哪个分数
   位置,视觉上都是"淡出"而不是"硬生生腰斩一行文字"。 */
.ask-player__modal .ask-player__full-list {
  max-height: 270px;
  -webkit-mask-image: linear-gradient(180deg, #000 calc(100% - 24px), transparent 100%);
  mask-image: linear-gradient(180deg, #000 calc(100% - 24px), transparent 100%);
}
.ask-player__full-list-row {
  display: flex; align-items: center; gap: 10px; padding: 7px 4px; border-radius: 10px;
  text-align: left; color: #fff; flex: none;
}
.ask-player__full-list-row:active { background: rgb(255 255 255 / .08); }
.ask-player__full-list-title { font: 500 13px var(--font-ui); flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ask-player__full-list-artist { font: 400 12px var(--font-ui); color: rgb(255 255 255 / .5); flex: none; }

/* ==================== 中等宽度(iPad):圆圈按钮 + 弹窗 ==================== */
.ask-player__circle {
  position: fixed; right: 24px; bottom: 24px; z-index: 240;
  width: 56px; height: 56px; border-radius: 50%; overflow: hidden;
  box-shadow: 0 8px 20px rgb(0 0 0 / .3); border: 2px solid rgb(255 255 255 / .8);
  /* p3.20②:圆→胶囊的展开过渡(has-title 类由 player.js 按"当前有没有
     歌名文本"实时算,不是靠 hover 猜)。width/border-radius/padding 一起
     过渡,纯 transform 系动画军规在这里不适用——这个形变本身就是布局尺
     寸变化,不是位移/缩放能表达的效果,阻尼曲线保持和站内其它形变动画
     一致的观感。 */
  transition: width 260ms var(--ease-spring), border-radius 260ms var(--ease-spring), padding 260ms var(--ease-spring);
}
/* p3.23③:播放中的光影呼吸——总指挥原话"让访客一眼发现有音乐在放,别
   找不到入口"。只动 box-shadow(军规1允许的 opacity/transform 之外,
   box-shadow/filter 同属"不触发布局重排,只触发合成层重绘"的安全动画
   属性,和站内其它呼吸类效果——比如评论卡片提交成功浮窗——同一个技术
   选型),叠加在原有的投影阴影之上,不是替换掉它(两层 shadow 永远同
   时存在于同一条 box-shadow 声明里,只是发光那一层的扩散/透明度在关
   键帧之间变化,不会出现"动画开始瞬间阴影突然消失又出现"的跳变)。呼
   吸周期 2.6s,柔和而不刺眼,同时给 is-playing 才挂动画,暂停立刻停
   (state 由 player.js 的 listeners 广播驱动,和音浪是否在跳同一个开
   关)。 */
@keyframes ask-circle-breathe {
  0%, 100% { box-shadow: 0 8px 20px rgb(0 0 0 / .3), 0 0 0 0 rgb(255 149 0 / 0); }
  50% { box-shadow: 0 8px 20px rgb(0 0 0 / .3), 0 0 18px 5px rgb(255 149 0 / .5); }
}
.ask-player__circle.is-playing {
  animation: ask-circle-breathe 2600ms ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .ask-player__circle.is-playing { animation: none; }
}
/* p3.20①:coverWrap 包一层封面(+叠加音浪)——默认 100%/100% 填满整个
   圆,和以前"封面直接塞进 circle"视觉等价。position:relative 让音浪
   (circleWave,绝对定位)始终相对这一层居中,而不是相对整条(可能已经
   展开变宽的)胶囊居中。里面的封面 div(circleCover)本身要再撑满
   coverWrap、裁成圆——以前这两条是"circle 的任意 div 直接子元素"这一
   条通用规则顺带给的(circleCover 曾经直接是 circle 的子元素),现在
   circleCover 隔了一层 coverWrap,通用规则够不到它,这里单独给一份等
   价的规则,不是"顺便"而是"明确"。 */
.ask-player__circle-coverwrap { position: relative; width: 100%; height: 100%; }
.ask-player__circle-coverwrap > div { width: 100%; height: 100%; border-radius: 50%; overflow: hidden; }

/* p3.20①:原来这里还有一份 .ask-player__circle--desktop(桌面导航胶囊
   旁的音乐圆,JS positionDesktopCircle() 按胶囊真实尺寸写行内坐标定
   位)——已经整个删除,连同 player.js 里的 positionDesktopCircle()/建圆
   的那几行一起。桌面(≥1200px)现在和独立文章页共用同一份
   .ask-player__circle--inline(下面这份规则),塞进 header.php 新建的
   .topbar-row 里 #appstoreTopbarBalls 这个真实 flex 槽位,不再有任何
   一处球的位置是 JS 算出来的坐标。

   p3.18⑥:独立文章页顶栏槽位里的音乐球——不是 position:fixed(那是給
   "贴在视口固定位置、跟随全站导航"这种场景用的),这里是分享钮左边一
   个普通的 flex 子元素,跟着 .post-topbar 一起在文章正文区域顶部滚
   动/停留,和返回/分享按钮同样大小、同一条水平线。 */
.ask-player__circle--inline {
  /* p3.20①实测发现的真实 bug:基础 .ask-player__circle 规则写了
     right:24px; bottom:24px(给已删除的 position:fixed 右下角悬浮态用
     的)——position 换成 relative 后,CSS 里 right/bottom 不会自动清
     零,relative 定位下"非 auto 的 right/bottom"照样会把元素从它在
     flex 流里本来的位置再往左上方向偏移 24px,实测截图能看到音乐球
     比语言球/分享按钮明显偏高。这里显式清零 right/bottom 为 auto,让
     这颗圆完全交给 flex 布局本身决定位置,不残留任何一丝旧定位模式的
     偏移量。 */
  position: relative; right: auto; bottom: auto; width: 36px; height: 36px;
  border: 1px solid rgb(255 255 255 / .35); box-shadow: none; padding: 2px;
  flex: none;
}
.ask-player__topbar-circle { display: contents; } /* 纯挂载容器,不参与布局,真正占位的是里面的 .ask-player__circle--inline 本身 */

/* p3.20②:总指挥原话"音乐球在文章页展开成显歌名+音浪的胶囊"——圆变
   胶囊,固定高度 36px 不变(和分享/返回按钮同一条水平线)。
   p3.23④:胶囊内容从"封面+歌名"扩到"封面+歌名歌手+音浪+播放按钮"
   (总指挥原话"同一胶囊里加音浪+暂停键"),宽度上限跟着从 168px 放宽
   到 260px,给多出来的两个固定尺寸元素留够空间——这两个新元素
   (.ask-player__inline-wave/.ask-player__inline-playbtn)和封面
   coverWrap 一样是 flex:none,只有中间的歌名歌手区
   (.ask-player__inline-title)是 flex:1;min-width:0,超出部分交给它
   自己的跑马灯在固定宽度窗口内滚动(见 buildCircleTrigger 里
   buildMarqueeBox 那段注释)——不会撑破槽位,也不会因为歌名忽长忽短
   把音浪/播放按钮的位置带跑。has-title 类没挂上(还没有任何歌名可显
   示)时维持普通 36px 正圆,不会显示一条空荡荡的胶囊。 */
.ask-player__circle--rich {
  width: 36px; max-width: 260px; border-radius: 50%;
  padding: 2px; display: flex; align-items: center; gap: 6px;
  /* p3.20②实测发现的真实 bug:歌名文字是白色(.ask-player__inline-title),
     胶囊背景是 glass 类默认的浅色调(--glass-tint 默认接近白色),白字
     叠在浅色玻璃上完全看不见,截图确认过(文字区域视觉上是一片空白)。
     和 .btn-back/.btn-share 同一个解法——那两个按钮同样是"白色图形叠
     在玻璃质感底上",本来就固定把 --glass-tint 覆盖成深色,不管页面背
     景本身是深是浅,玻璃底色都够暗,白色内容始终读得清。 */
  --glass-tint: #1c1c1e;
}
.ask-player__circle--rich.has-title {
  width: auto; border-radius: 18px; padding: 2px 10px 2px 2px;
}
.ask-player__circle--rich .ask-player__circle-coverwrap {
  width: 32px; height: 32px; flex: none;
}
/* p3.23④:封面上那颗小音浪(.ask-player__circle-wave,叠在 32px 封面正
   中间)是给"没有展开成胶囊的普通圆"用的指示灯——rich 模式下胶囊里已
   经有一个更显眼的专属音浪(.ask-player__inline-wave,下面单独定义),
   两个音浪同时出现是重复信息,这里关掉叠在封面上的那个。 */
.ask-player__circle--rich .ask-player__circle-wave { display: none; }

/* p3.18⑤:封面上叠一个小音浪,让访客一眼看出"这个圆在放东西",不用点
   开弹窗才知道。绝对定位贴圆的底边(封面通常上半部分留白/人像/图案,
   底边一条更不容易挡住封面主体),半透明深色胶囊托底保证在任何封面
   颜色上都看得清白色音浪条。默认 opacity:0(没有播放时完全不占视觉重
   量,不是"永远在但静止"),is-playing 时才淡入 + 条形开始跳——和手机/
   桌面灵动岛的音浪是同一份 .ask-player__waveform 结构、同一套 ask-wave
   关键帧,只是尺寸更小、加了个背景胶囊。这颗圆本身不响应悬停,音浪纯
   展示,不是按钮,不会和"点圆=开弹窗"这个唯一交互抢语义。 */
.ask-player__circle-wave.ask-player__waveform {
  /* p3.19 A1:总指挥原话"音浪居中放大"——绝对定位居中(left/top 50% +
     translate(-50%,-50%)),相对 .ask-player__circle-coverwrap(不是相
     对整条可能已经展开变宽的胶囊)居中,圆形封面的最宽处正好在垂直中
     点,放在这个位置放大也不会被圆形裁切边界削掉。 */
  width: auto; height: 16px;
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  padding: 3px 6px; border-radius: 999px;
  background: rgb(0 0 0 / .45); backdrop-filter: blur(2px);
  opacity: 0; transition: opacity 220ms ease; pointer-events: none;
}
.ask-player__circle-wave.ask-player__waveform.is-playing { opacity: 1; }
.ask-player__circle-wave .ask-player__waveform-bar { width: 3px; height: 8px; }

/* p3.20②结构性重写:原来是悬停才淡出的绝对定位提示条(手机摸不到悬
   停,等于没有),现在是胶囊展开后常驻可见的普通 flex 子元素——
   min-width:0 是能让 flex 子元素的 overflow/ellipsis 真正生效的那个容
   易漏掉的前提(flex 子元素默认 min-width:auto,内容再长也不缩,
   text-overflow 永远没有机会触发,这里显式清零)。
   p3.23④:从"纯文字+省略号截断"换成和手机灵动岛同一套跑马灯结构
   (buildMarqueeBox 建的 box/text 两层)——.ask-player__inline-title 现
   在是外层固定宽度窗口(唯一一个 flex:1 的弹性格子),
   .ask-player__inline-title-text 是内层真正做跑马灯位移的文字本身,
   溢出判定/动画挂载逻辑和岛的标题跑马灯逐字复用(见下面
   .ask-player__inline-title-text.is-marquee 并入岛那条共用规则),不是
   另起一套。 */
.ask-player__inline-title {
  flex: 1 1 auto; min-width: 0; max-width: 140px; overflow: hidden;
}
.ask-player__inline-title-text {
  display: inline-block; white-space: nowrap;
  font-size: 11px; font-weight: 600; color: #fff;
}
/* p3.23④:和岛的标题跑马灯共用同一套溢出判定/动画(ask-marquee 关键
   帧、.is-marquee 触发规则都并进了上面 .ask-player__island-title-text.is-marquee
   那条共用选择器列表里,不在这里重复定义一份)。

   p3.23④:胶囊专属音浪——和岛(.ask-player__waveform 基础规则)共用同一
   套竖条+关键帧,flex:none 固定尺寸,不随歌名区宽度变化。 */
.ask-player__inline-wave { flex: none; }
.ask-player__inline-wave .ask-player__waveform-bar { width: 2px; height: 6px; }

/* p3.23④:胶囊里的播放/暂停——role="button" 的 <span>(不是真
   <button>,原因见 buildCircleTrigger 里 inlinePlay 那段注释),flex:none
   固定 20px,视觉上和 .ask-player__island-play 同一路白底深图标按钮,
   缩小一号适配这颗胶囊本来就矮的 36px 高度。 */
.ask-player__inline-playbtn {
  width: 20px; height: 20px; border-radius: 50%; background: #fff; color: #111;
  display: flex; align-items: center; justify-content: center; flex: none;
  cursor: pointer;
}
.ask-player__inline-playbtn svg { width: 11px; height: 11px; }

.ask-player__modal-mask {
  position: fixed; inset: 0; z-index: 250; background: rgb(0 0 0 / .45);
  display: flex; align-items: center; justify-content: center;
  opacity: 0; pointer-events: none; transition: opacity 260ms ease;
}
.ask-player__modal-mask.is-open { opacity: 1; pointer-events: auto; }
/* p3.18 实测发现的真实 bug:CoverFlow 卡片(player.js buildCoverFlow()
   position() 里)按"离当前歌多近"给每张卡片各自设了行内 pointer-events
   (离得近的几张 auto,离得远的 none)——这个判断只跟"哪张卡片该能点"有
   关,从来没考虑过"这个弹窗现在到底是不是打开的"。弹窗关闭时整块靠
   opacity:0+这条 pointer-events:none 隐藏,但卡片自己的行内 auto 会覆
   盖掉从父级继承来的 none(pointer-events 不是靠"父级挡住"生效的,是
   每个元素各自决定要不要参与命中测试),于是一个视觉上完全不可见的弹
   窗,它内部离得近的那几张卡片依然真实地"浮在"页面上准备接收点击——
   页面上任何恰好被这块看不见的区域盖住的其它元素(这一轮新增的多语
   言弹层就撞上了,排查详情见 build log)点不到,鼠标点击被这些看不见
   的卡片截胡。用 !important 兜底覆盖行内样式,只在弹窗真的关闭时生
   效,不影响打开时卡片自己的点击判断。 */
.ask-player__modal-mask:not(.is-open) .ask-coverflow__card {
  pointer-events: none !important;
}
/* p3.20⑥结构性修复:原来是 max-height:82vh + overflow-y:auto 整块弹窗
   一起滚——歌单列表(.ask-player__full-list)自己虽然也有 flex:1 +
   overflow-y:auto,但那套设定依赖"父级容器有确定的高度边界"才会真的
   触发内部滚动(和手机 .ask-player__sheet 用 height:75vh 固定高度是同
   一个道理),桌面这个弹窗给的是 max-height(没有下边界确定值)+
   overflow-y:auto 在弹窗自己身上——歌单一多,弹窗就跟着无限长高、自己
   出现滚动条,歌单内部那层 flex:1/overflow-y:auto 永远轮不到生效,这
   正是"歌单撑爆桌面弹窗"这个老毛病在桌面复发的真正原因(总指挥点名:
   和之前手机端的解法必须是同一套结构,不是桌面另外单独写一份)。
   改成弹窗本身 overflow:hidden + flex 列布局,把"要不要滚"完全下放给
   里面 .ask-player__full-content(下面新增的 flex:1;min-height:0)→
   .ask-player__full-list 这条已经在手机端验证过的责任链,弹窗自己的高
   度到 82vh 就封顶,不再跟着内容动态变长。 */
.ask-player__modal {
  position: relative; width: min(88vw, 420px); max-height: 82vh; overflow: hidden;
  display: flex; flex-direction: column;
  padding: 20px 22px 26px; border-radius: 24px;
  background: linear-gradient(180deg, hsl(var(--h, 210) 45% 16%), hsl(var(--h, 210) 55% 7%));
  color: #fff;
  transform: scale(.92); transition: transform 260ms ease;
}
.ask-player__modal .ask-player__full-content { flex: 1; min-height: 0; }
.ask-player__modal-mask.is-open .ask-player__modal { transform: scale(1); }
.ask-player__modal-close {
  position: absolute; top: 12px; right: 14px; width: 28px; height: 28px; border-radius: 50%;
  background: rgb(255 255 255 / .12); color: #fff; font-size: 13px;
}

/* p3.17①:原来这里还有一套独立的"导航胶囊下方横条灵动岛
   (.ask-player__desktop-island)+ macOS 缩放弹出面板
   (.ask-player__desktop-panel)"——总指挥定"桌面不要左侧、不要横条灵
   动岛,就是圆圈点击弹窗",已删除整块(连同 player.js 里对应的
   buildDesktopIsland/buildDesktop/positionDesktopIsland/
   setDesktopPanelOrigin 四个函数)。桌面现在复用上面的
   .ask-player__circle(+ --desktop 定位修饰)+ .ask-player__modal 这套
   已经在用的组件,不是另起一份新样式。也顺带修掉一个真实 bug:被删掉
   的 .ask-player__desktop-island 规则里有一行没包在任何 @media 里的
   display:flex,层叠顺序在文件末尾、晚于顶部 display:none 默认值和
   @media(min-width:1200px) 的响应式开关,结果无条件覆盖了响应式规
   则——这个元素在任意宽度下其实都是 display:flex,可见性全靠
   opacity(is-visible),于是手机宽度只要一放歌(或常驻开关开着),灵动
   岛和这条"横条"会同时可见,这正是总指挥说的"手机首页出现两个灵动
   岛"。完整实测记录见 build log。 */
