/* 色板：灯箱 / 接触印相。底色带棕不是纯黑，中性灰偏暖向主调靠。
   钨丝灯琥珀作主调，冷青只给数据，语义色（免费/计费/丢弃）与主调分开。 */
/* 深色只保留轻微层级，不让输入框、关注管理和数据详情陷进近乎纯黑的第三档。
   2026-08-30 按实际画面回执，把 ground 从 #020408 抬到 #080A0D；所有使用
   token 的页面、侧栏、输入与选中面同步生效。 */
/* 浅色一档不带色相。2026-09-04 实测 vercel.com 后台：白底那套中性色从 #FFFFFF 一路到
   #171717 全部落在 hsl(0,0%,·) 上，悬停与选中是纯黑透明度（5%）而不是调过色的实色灰。
   带蓝的浅灰在白底上读成脏，实色中间态又比它所在的面板还重，合起来就是「太暗、太脏」。
   数值与来源见 docs/reference-snapshots/vercel-geist-semantics-measured.md。 */
:root{
  --ground:#FFFFFF; --surface:#FAFAFA; --sunk:#F2F2F2; --hover:rgba(0,0,0,.05); --skeleton-sheen:#EBEBEB;
  --line:#E5E5E5; --line-soft:#EBEBEB;
  /* 白面与页面底之间的那一档，全站的「中间色」。分段器和 tab 的选中格、面板里嵌进去的
     那些行都填它：白面上再叠一张白等于没填，填页面底那一档又跟页面本身同色——两种做法
     在浅色一档上都读不出「这一格是被选中的」，也读不出「这一行是嵌进去的」。
     取值是 Geist 的 gray-200：实测 vercel.com 的 Tabs secondary 选中态就是 `bg-gray-200`
     #EBEBEB（vercel-geist-semantics-measured.md「选中态与它的悬停」），暗色一档按同一
     方向在面上叠 9% 白。 */
  --inset:#EBEBEB;
  --ink:#171717; --ink-2:#4D4D4D; --muted:#666666;
  /* 压在缩略图上的标记用这一对，两档同值、不跟主题翻面：它底下是媒体不是页面。
     取 --ink／--ground 的话，同一张照片上浅色一档是深片白勾、暗色一档是浅片深勾，
     同一个东西两副长相。选中标记与它在卡面上那圈环都引用这里。 */
  --on-media:#F5F7FA; --on-media-ink:#090B0F;
  /* 蓝只给焦点环、链接、进度／数据和 Toggle 开态；按下／选中态与主动作走 --ink／--ink-2 反相，
     其它选择器引用 --tungsten 由 tests/test_web_ui.py 拒绝。 */
  --tungsten:#0095F6;
  /* 评分那一排的琥珀，空心实心都用它，两态的差别只有填不填。它和上面那几个语义色一样
     两档各写各的值：借 --line 画空心星在深色底上站得住（#3A3A3D 压在近黑上），
     换到浅色一档就是 #E5E5E5 压在 #FAFAFA 上，同一份写法在两个主题下不是同一件事。 */
  --rating:#B8860B;
  --loupe:#127A72; --keep:#2E7D32; --meter:#B26A10; --drop:#C0392B; --success:#2E7D32;
  --tile:336px; --topH:56px; --filterH:58px; --filterItemH:40px; --sortH:53px; --railW:72px;
  /* 圆角也是有词汇的，不是随手挑一个数。
     --pill:整圆，只给真正的胶囊——标签、筛选令牌、滚动条和进度条这类连续的条。
     --control-radius:控件（按钮、分段器），实测 Geist 的 6px。
     --badge-radius:状态与元信息标记。把「WIP」「最长」这种普通元信息做成整圆胶囊，
     是 vercel-report-design 点名要拒绝的反射：它们会跟真标签抢同一种视觉身份，
     用户以为可以点。
     --surface-radius:不浮起的内嵌表面——缩略图、行卡片、Note、Toast、分段器外框，介于控件与浮层之间。
     除这五个 token、50% 和 0 之外不写字面圆角；带边框容器里的头尾条用 calc(… - 1px) 保持同心。
     字重同理只有 400／500／600 三档：vercel.com/design.md 明说不要自造数字字重，650、700 全部收到 600。 */
  --pill-radius:999px; --tag-radius:var(--pill-radius);
  --control-radius:6px; --badge-radius:4px; --floating-radius:12px; --surface-radius:8px; --tag-pad-y:4px; --tag-pad-x:10px; --hover-delay:5s;
  /* 侧栏光晕的参数层。三枚光晕的颜色、椭圆尺寸和收边位置全部落在这里；
     `.glowlayer::before` 只引用变量，一个字面颜色都不写。这组默认值就是预设「钨丝暖阁」，
     颜色与 js/home-glow.js 里那一档一字不差，页面第一帧和 applyHomeGlow() 写完之后画面相同。
     只有颜色那三个由设置面板写到 `.glowlayer` 自己的行内样式上，写在这一层而不是 <html>
     上的理由记在下面那条规则里；半轴和收边只有这一份，用户不调、也不进 localStorage。
     圆心根本不在参数里：三枚光晕是侧栏玻璃面上那两团反光换成的，位置由 glowdrift 那两条
     动画一直推着走，写死一个圆心等于把会动的光钉成一张贴图。存几何还有第二重代价——
     旧版本那套横躺在页面顶部的坐标会原样跟着老用户留下来。
     半轴的参照是一张两倍于侧栏的画布（和 board.css 里 --glass-drift-a/b 同一套算法），
     20% 落到侧栏上就是 40%，跟着侧栏自己的长宽各向异性地拉成椭圆。
     「大小」和「柔化」两条拉条各自乘在半轴与收边上，倍率由页面写进 --glow-size 与
     --glow-soften；基准值留在这里，用户存的是那两条拉条的档位，不是算完的百分比。
     「漂移速度」同理写进 --glow-drift-scale（时长倍率）与 --glow-drift-play：拉到 0 时
     暂停漂移那两条动画，不去算一个除以零的时长。
     --glow-lerp 是插值空间：默认空串等于按 sRGB 插，认得 `in oklab` 的引擎在下面那条
     @supports 里把它换成 `in oklab`。写成变量而不是把整段 background 抄第二遍，是因为
     两份三层渐变里任何一层改歪一个百分点都看不出来，而它们本该永远相同。 */
  --glow-lerp: ;
  --glow-size:1; --glow-soften:1; --glow-drift-scale:1; --glow-drift-play:running;
  --glow-spot-1-color:rgba(143,152,164,.52); --glow-spot-1-w:calc(20% * var(--glow-size)); --glow-spot-1-h:calc(20% * var(--glow-size)); --glow-spot-1-fade:calc(72% * var(--glow-soften));
  --glow-spot-2-color:rgba(182,188,196,.38); --glow-spot-2-w:calc(26% * var(--glow-size)); --glow-spot-2-h:calc(26% * var(--glow-size)); --glow-spot-2-fade:calc(74% * var(--glow-soften));
  --glow-spot-3-color:rgba(111,119,131,.50); --glow-spot-3-w:calc(23% * var(--glow-size)); --glow-spot-3-h:calc(23% * var(--glow-size)); --glow-spot-3-fade:calc(73% * var(--glow-soften));
  /* 强度是用户那一档，主题缩放是色板那一档，实际不透明度是两者相乘。
     深色侧栏那块玻璃吃光、浅色那块反光，同一组颜色照搬到浅色会浓得压住导航文字，所以
     浅色把整层按同一个比例收下来，而不是另存一套颜色：用户挑的是同一片光，两个主题不该
     是两种东西。这一档的 .6 是在浏览器里逐档量出来的，判据是玻璃面上那块底色偏暖多少
     （红减蓝）：.34 只有 4 级，一眼看过去和没开一样；.6 是 7 级，三枚光晕读得出来是三种
     颜色；到 1 是 11 级，整条侧栏跟着泛米色，那时候有颜色的就不是光而是侧栏本身了。 */
  --glow-noise:0; --glow-strength:1; --glow-theme-scale:.6;
  /* 一颗标签的脸只有这一张：--tag-radius 的圆角、一圈 --line、这块比面再沉一档的填充、
     --ink-2 的字。同一个词出现在卡片上、详情面板里、筛选条上和交集条里，读的人认的是
     同一样东西；四处各画各的，同一个词就成了四种控件。写成 token 是因为这块填充是一句
     算式，抄第三遍时总有一处的比例会错开一两个百分点，而差一档正好是「另一种东西」。 */
  --tag-fill:color-mix(in srgb,var(--surface) 82%,var(--ground));
  /* 一行里的输入框、下拉触发器和按钮共用这一个高度。写成变量是因为差 3px 就不是一行了，
     而窄屏那条把输入框字号顶到 16 像素的防放大规则只抬没写死高度的那几个，同排的按钮不动。
     Geist 自己的实测值是 32px，见 docs/reference-snapshots/vercel-geist-controls-measured.md。 */
  --control-h:38px;
  /* 框体上下两条横条的统一高度。写成变量是因为它同时管数据管理的卡片、链接管理
     和资源同步：这三处任何一处单独调，页面上就会出现两种「同一种条」。 */
  --fieldset-bar-h:52px;
  /* 全站只有两张面。--page 是退到后面的那张：页面底、框体里的操作条。
     --ground 是浮在它上面的那张：顶栏、窄栏、卡片、面板、菜单、按钮和输入框，
     靠这一档差加一条发丝线浮起来，不靠投影。
     2026-09-05 实测 vercel.com 后台：页面底与左栏都是 --ds-background-200 #FAFAFA，
     部署行、设置 fieldset 的正文和次级按钮是 --ds-background-100 #FFFFFF 配 1px 的 8% 黑，
     不带投影；fieldset 的操作条落回 #FAFAFA，与页面底是同一个 token。
     暗色那档 Vercel 是纯黑页面配 #0A0A0A 的面——方向不变，仍是面比底亮。
     Peach 的暗色底带蓝，所以按同一方向在 --ground 之下取一档，不跳到纯黑。
     顶栏与窄栏是与 Vercel 有意分开的一处：Vercel 的页头和页面底同色、不浮，
     Peach 有一条从上贯到底的窄栏，两条一起留在 --ground 才不会在左上角撞出两种白。 */
  --page:#FAFAFA;
  /* 选中填的那张面，就是上面那个中间色。判据是「跟周围不同」，不是「更亮」：选中格多半
     站在白卡上，往亮里填等于白叠白，一排 tab 里哪一枚被选中读不出来。Geist 的取法是往下
     压一档到 gray-200。深色一档叠白后本来就比面亮，同一个 token 两边都成立。 */
  --picked:var(--inset);
  /* 小窗播放浮在整页内容、吸顶条、抽屉（97）和沉浸层（200）之上，却要让设置面板、
     菜单与 Toast 盖住它：dialog 以上三档都不是这个位置。 */
  --layer-miniplayer:900; --layer-dialog:1000; --layer-popover:1100; --layer-tooltip:1200;
  --frost-panel:rgba(250,250,250,.92); --frost-light:rgba(255,255,255,.86);
  /* 盖住整页的弹层共用这一档遮罩：设置面板和作品详情浮窗底下都是同一片馆藏，压成两种
     深浅只会让人以为自己打开的是两种东西。取值沿用设置面板在 Board UI 一版里的实际渲染
     （`board.css` 的 `.settingspanel.settingspanel`），两档主题同值、不带模糊。 */
  --scrim:rgba(0,0,0,.7);
  --overlay-5:rgba(0,0,0,.05);--border-10:rgba(0,0,0,.10);--border-15:rgba(0,0,0,.15);
  /* 输入框静止那条边。2026-09-05 实测 vercel.com 的输入框：静止 1px 的
     gray-alpha-400、悬停 alpha-500。焦点不是蓝的——蓝在这套里只给链接和数据。
     悬停与聚焦的环另走 BoardUI Input 的 border-button 两档，见 .geist-input；
     --field-ring-focus 留给 .pickrow 这类「选中的那一行」标边。 */
  --field-ring:rgba(0,0,0,.08);--field-ring-hover:rgba(0,0,0,.21);
  --field-ring-focus:rgba(0,0,0,.34);
  /* 字号只有这一套刻度。之前站内散着 22 种字号（9…48px），相邻两档差半像素，
     既排不出层级也没法复核。收敛成 8 档，下限 12px：更小的灰字在 Vercel 的
     design.md 里被明确列为要拒绝的生成式设计反射。新增字号前先证明现有 8 档都不合适。 */
  --fs-xs:12px; --fs-sm:13px; --fs-md:14px; --fs-lg:16px;
  --fs-xl:20px; --fs-2xl:24px; --fs-3xl:32px; --fs-4xl:48px;
}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){
  --ground:#080A0D; --surface:#0C0F14; --sunk:#0B0D11; --hover:rgba(255,255,255,.07); --skeleton-sheen:rgba(255,255,255,.12);
  --line:#3A3A3D; --line-soft:#2C2C30; --inset:rgba(255,255,255,.09);
  --ink:#FFFFFF; --ink-2:#C9CDD4; --muted:#9AA0A8;
  --tungsten:#0095F6;
  --frost-panel:rgba(6,9,14,.94); --frost-light:rgba(14,18,24,.90);
  --overlay-5:rgba(245,250,255,.05);--border-10:rgba(245,250,255,.10);--border-15:rgba(245,250,255,.15);
  --field-ring:rgba(255,255,255,.14);--field-ring-hover:rgba(255,255,255,.24);
  --field-ring-focus:rgba(255,255,255,.51);
  --page:#04060A;
  --picked:var(--inset);
  --rating:#E5B34A;
  --loupe:#6FD0C8; --keep:#5FB95F; --meter:#E0912F; --drop:#FF5252; --success:#5FB95F;
  --glow-theme-scale:1;}}
:root[data-theme="dark"]{
  --ground:#080A0D; --surface:#0C0F14; --sunk:#0B0D11; --hover:rgba(255,255,255,.07); --skeleton-sheen:rgba(255,255,255,.12);
  --line:#3A3A3D; --line-soft:#2C2C30; --inset:rgba(255,255,255,.09);
  --ink:#FFFFFF; --ink-2:#C9CDD4; --muted:#9AA0A8;
  --tungsten:#0095F6;
  --frost-panel:rgba(6,9,14,.94); --frost-light:rgba(14,18,24,.90);
  --overlay-5:rgba(245,250,255,.05);--border-10:rgba(245,250,255,.10);--border-15:rgba(245,250,255,.15);
  --field-ring:rgba(255,255,255,.14);--field-ring-hover:rgba(255,255,255,.24);
  --field-ring-focus:rgba(255,255,255,.51);
  --page:#04060A;
  --picked:var(--inset);
  --rating:#E5B34A;
  --loupe:#6FD0C8; --keep:#5FB95F; --meter:#E0912F; --drop:#FF5252; --success:#5FB95F;
  --glow-theme-scale:1;}

*,*::before,*::after{box-sizing:border-box}
.sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}
/* 整页也不给滚动条留那一列。原生滚动条自己占 15px 实宽，浅色一档的槽还是块比 --page
   更亮的死白，窗口右边于是常年挂着一条从头贯到底的白带；要它不跳版又得 scrollbar-gutter
   把这 15px 永久扣下来，等于为一条装饰付一整列。改成 .ovtrack 那套覆盖式滑块之后，
   宽度归零，跳版的前提也一并消失——设置面板、灯箱和播放器给 body 加 overflow:hidden 时，
   本来就没有东西会消失。 */
html{color-scheme:light;scroll-padding-top:calc(var(--topH) + 8px);scrollbar-width:none}
html::-webkit-scrollbar{display:none}
/* 换筛选那一路上关掉滚动锚定：卡片换成骨架、骨架再换成新卡片，锚定每一步都要照新内容
   把位置推一次，而这一路正有一段回到顶部的滚动在走，两边会互相顶。 */
html.refiltering{overflow-anchor:none}
@media (prefers-color-scheme:dark){html:not([data-theme="light"]){color-scheme:dark}}
html[data-theme="dark"]{color-scheme:dark}
html,body{margin:0;padding:0}
body{background:var(--page);color:var(--ink);
  font-family:Roboto,"Segoe UI Variable Text","Segoe UI","Microsoft YaHei UI","PingFang SC",sans-serif;
  font-size:var(--fs-md);line-height:1.45;-webkit-font-smoothing:antialiased;
  font-variant-numeric:tabular-nums}
/* 光晕住在侧栏里自己那枚空 div 上，而不是 `body` 的伪元素上，为的是把自定义属性的失效
   范围关在这一层里。自定义属性是继承的：往 <html> 或 <body> 上写一次 --glow-strength，
   整棵树都要重算样式。2026-09-16 在首页量过，判据是「写一次变量再强制 layout 的耗时」，取
   三轮每帧平均：写在 <html> 上 12.7–16.6ms（页面约 5800–6000 个节点），写一个根本没人
   引用的 --zzz-unused 同样是 15.0–16.2ms——这笔钱买的全是继承失效，和光晕怎么画无关；
   同一份代码写到一枚没有子节点的元素上是 0.09–0.10ms。拖拉条时每帧都要写一次，
   16.7ms 的帧预算里先扣掉 15ms，掉帧就是必然。
   `will-change:opacity` 也试过并量过：同样三轮 60 步合成拖动，提层后每帧间隔取最好一轮
   18.86ms、不提层 19.49ms，轮内波动本身就有 2ms 上下，这 0.6ms 读不出是效果，代价却是
   一整块合成层常驻显存，所以不提。
   三层都只引用 --glow-* 变量，这里一个颜色都不写死：用户在设置面板里换配色时，改的就是
   那组变量，规则本身不动。插值空间走 --glow-lerp。

   这一层画的就是侧栏玻璃面自带的那两团慢漂反光，换成用户配得了色的三枚。board.css 那条
   玻璃规则把 --glass-drift-a/b 铺在每一块玻璃上，抽屉这一块交给这里来铺（那条规则里
   .drawer 的两个变量因此置为 none，两套光斑叠着漂会糊成一团）；搜索框、筛选浮层、图标钮
   那些玻璃面保持自己原来的漂移不动——它们身上那一点正是从侧栏折射过去的光。
   正文身后什么都不铺：整条页面宽的光带是背景不是光晕，字压在上面读不出来。

   位置不是参数，由 glowdriftx/glowdrifty 一直推着走，算法照搬 board.css 那两条：画布是
   元素的两倍大，background-position 从 0% 到 100% 正好把光斑中心从右下角推到左上角，
   -10% 与 110% 那两档让它探出框外一点。三枚各走一条相位不同的李萨如轨迹，横竖两条动画
   41s 与 67s 互质，合起来四十多分钟不重样。
   z-index 取 -2：.drawer 自己带 backdrop-filter，是一个层叠上下文，负层只压在它那块玻璃
   填充之上、被导航内容全部盖住；-1 已经归了选中项那条 .navglide，光晕要在它下面。
   溢出由 .drawer 的 overflow:hidden 连圆角一起裁掉，这一层不必自己再写一遍圆角。
   首屏 ambient-in 那一下淡入留着：这一层一直在动，开页时从 0 亮起来读成「灯亮了」，
   直接带着满强度出现反而像一张贴图突然贴上去。 */
.glowlayer{--ambient-opacity:calc(var(--glow-strength) * var(--glow-theme-scale));
  position:absolute;inset:0;z-index:-2;pointer-events:none}
.glowlayer::before{content:"";position:absolute;z-index:0;pointer-events:none;inset:0;
  opacity:var(--ambient-opacity);background:
    radial-gradient(var(--glow-lerp) var(--glow-spot-1-w) var(--glow-spot-1-h) at 50% 50%,var(--glow-spot-1-color),transparent var(--glow-spot-1-fade)),
    radial-gradient(var(--glow-lerp) var(--glow-spot-2-w) var(--glow-spot-2-h) at 50% 50%,var(--glow-spot-2-color),transparent var(--glow-spot-2-fade)),
    radial-gradient(var(--glow-lerp) var(--glow-spot-3-w) var(--glow-spot-3-h) at 50% 50%,var(--glow-spot-3-color),transparent var(--glow-spot-3-fade));
  background-size:200% 200%;background-repeat:no-repeat;
  background-position:50% 20%,80% 70%,20% 80%;
  animation:glowdriftx calc(41s * var(--glow-drift-scale)) linear infinite,glowdrifty calc(67s * var(--glow-drift-scale)) linear infinite,ambient-in .8s ease .5s both;
  animation-play-state:var(--glow-drift-play),var(--glow-drift-play),running}
/* 三条轨迹是同一条李萨如曲线的三个相位：第一枚与 board.css 的 --glass-drift-a 同相，
   第二枚是它的镜像，第三枚横向错四分之一周期、纵向错八分之一，三枚因此从不在同一处会合。 */
@keyframes glowdriftx{
  0%{background-position-x:50%,50%,110%}
  12.5%{background-position-x:7.6%,92.4%,92.4%}
  25%{background-position-x:-10%,110%,50%}
  37.5%{background-position-x:7.6%,92.4%,7.6%}
  50%{background-position-x:50%,50%,-10%}
  62.5%{background-position-x:92.4%,7.6%,7.6%}
  75%{background-position-x:110%,-10%,50%}
  87.5%{background-position-x:92.4%,7.6%,92.4%}
  100%{background-position-x:50%,50%,110%}
}
@keyframes glowdrifty{
  0%{background-position-y:-10%,50%,7.6%}
  12.5%{background-position-y:7.6%,7.6%,50%}
  25%{background-position-y:50%,-10%,92.4%}
  37.5%{background-position-y:92.4%,7.6%,110%}
  50%{background-position-y:110%,50%,92.4%}
  62.5%{background-position-y:92.4%,92.4%,50%}
  75%{background-position-y:50%,110%,7.6%}
  87.5%{background-position-y:7.6%,92.4%,-10%}
  100%{background-position-y:-10%,50%,7.6%}
}
/* 认得 oklab 插值的引擎在这里才拿到它。反过来写——默认 `in oklab`、老引擎再关掉——
   的后果是老引擎把整条 background 判成无效，首页顶上一片光都没有，而不是退回 sRGB。 */
@supports (background:linear-gradient(in oklab,red,blue)){:root{--glow-lerp:in oklab}}
/* 颗粒感是可选的一层，默认强度 0 就是关。瓦片由 feTurbulence 当场生成，仓库里不落
   位图；overlay 只压在光晕上，导航内容在它之后绘制，不受影响。 */
.glowlayer::after{content:"";position:absolute;z-index:0;pointer-events:none;inset:0;
  opacity:var(--glow-noise);mix-blend-mode:overlay;
  background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='peachglowgrain'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23peachglowgrain)'/%3E%3C/svg%3E") 0 0/120px 120px repeat}
@keyframes ambient-in{from{opacity:0}to{opacity:var(--ambient-opacity)}}
/* 跟着动效偏好走：一屋子的光一直在挪是这块玻璃读成实物的理由，但 prefers-reduced-motion
   的人要的正是「别再动了」。停在当前那一帧，光还在，只是不动——整层关掉等于换了个配色。 */
@media (prefers-reduced-motion:reduce){.glowlayer::before{animation:ambient-in .8s ease .5s both}}
.disp{font-family:Bahnschrift,"Segoe UI Variable Display","Microsoft YaHei UI",sans-serif;
  font-weight:600;letter-spacing:.02em}
.mono{font-family:"Cascadia Mono",Consolas,ui-monospace,"Microsoft YaHei UI","PingFang SC",monospace;
  font-variant-numeric:tabular-nums;font-size:var(--fs-xs)}
button,input,textarea{font:inherit;color:inherit}
button,a,input,textarea,select,summary{touch-action:manipulation}
/* React 子树的焦点环由 BoardUI 组件自己画（输入框画在外框上）；`:where` 让特指度保持 0,1,0。 */
:where(:not(.peach-react *)):focus-visible{outline:2px solid var(--tungsten);outline-offset:2px}
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
/* Geist Spinner = 用户触发动作的即时反馈；Loading Dots = 后台任务仍在推进。 */
@keyframes geist-spinner-opacity{0%{opacity:1}100%{opacity:.15}}
.geist-spinner{position:relative;display:inline-block;flex:none;width:16px;height:16px;color:currentColor}
.geist-spinner>i{position:absolute;top:1px;left:7px;width:2px;height:4px;border-radius:var(--pill-radius);
  background:currentColor;transform-origin:1px 7px;transform:rotate(var(--spinner-angle));
  animation:geist-spinner-opacity 1s linear var(--spinner-delay) infinite}
@keyframes geist-loading-dot{0%,80%,100%{opacity:.25;transform:translateY(0)}40%{opacity:1;transform:translateY(-2px)}}
.geist-loading{display:inline-flex;align-items:center;min-width:0;gap:8px;color:var(--muted)}
.geist-loading-dots{display:inline-flex;align-items:center;gap:3px;flex:none}
.geist-loading-dots>i{width:4px;height:4px;border-radius:50%;background:currentColor;
  animation:geist-loading-dot 1.2s ease-in-out infinite}
.geist-loading-dots>i:nth-child(2){animation-delay:.15s}.geist-loading-dots>i:nth-child(3){animation-delay:.3s}
/* Geist Search Input：搜索图标占前缀位，查找中原位换 Spinner，输入框几何不变。
   全站带搜索语义的输入共用这一份，各处只加宽度和位置。 */
.geist-search{position:relative;min-width:0}
.geist-search-prefix{position:absolute;z-index:1;left:12px;top:11px;
  display:grid;place-items:center;width:16px;height:16px;color:var(--muted);pointer-events:none}
.geist-search-prefix>svg{width:16px;height:16px;stroke:currentColor;fill:none}
.geist-search input[type="search"]{width:100%;min-width:0;height:38px;
  box-sizing:border-box;border:1px solid var(--field-ring);border-radius:var(--control-radius);
  background:var(--ground);color:var(--ink);padding:0 12px 0 38px;font-size:var(--fs-md);font-family:inherit;
  line-height:20px;transition:box-shadow .12s ease}
.geist-search input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none}
.geist-search input[type="search"]::placeholder{color:var(--muted)}
/* 悬停与聚焦是同一枚 2px inset ring 在两档灰之间换档，聚焦压过悬停（BoardUI Input
   的 `ring-2 ring-inset`，2026-09-09 取证登记 boardui-input）。环画在静止那条边的
   内侧，三态边框同色——把聚焦态的边改成透明只会露出控件自己的底，那正是环外那条
   亮边的来源。上游把 box-shadow 写进了 transition，环是渐变出来的，不是硬切。 */
.geist-search input[type="search"]:hover{box-shadow:inset 0 0 0 2px var(--color-border-button-hover)}
.geist-search input[type="search"]:focus{outline:0;box-shadow:inset 0 0 0 2px var(--color-border-button-active)}
.geist-search:focus-within .geist-search-prefix{color:var(--ink)}
/* 纯文本输入框：全站唯一一份。此前每处自己拼内边距，控件高度散在 34/40/43 三档，
   而 <=760px 那条把输入框字号顶到 16 像素的防放大规则只抬没写死高度的那几个，
   同一行里的按钮不动，两个控件就差一截。
   高度走 --control-h，与 .geist-search 同档；同一行的按钮和下拉触发器引用同一个 token，
   要换档只改 :root 那一处。Geist 自己的实测值是 32px，见
   docs/reference-snapshots/vercel-geist-controls-measured.md。 */
.geist-input{box-sizing:border-box;width:100%;min-width:0;height:var(--control-h);
  border:1px solid var(--field-ring);border-radius:var(--control-radius);
  background:var(--ground);color:var(--ink);padding:0 12px;
  font:inherit;font-size:var(--fs-md);line-height:20px;transition:box-shadow .12s ease}
.geist-input::placeholder{color:var(--muted)}
.geist-input:hover{box-shadow:inset 0 0 0 2px var(--color-border-button-hover)}
.geist-input:focus{outline:0;box-shadow:inset 0 0 0 2px var(--color-border-button-active)}
/* 按钮分档只靠填充，边框一条不画：Geist 的四个变体 `border-width` 全是 0、字重全是 500。
   次级那一档另挂一圈 `box-shadow:0 0 0 1px`（2026-09-07 复测 vercel.com/geist/button：次级
   填 #FFFFFF、环 rgb(235,235,235)；primary #171717、error、warning 三档 `box-shadow:none`；
   禁用档填 #F2F2F2、字 #8F8F8F、环仍在、`cursor:not-allowed`）。
   这圈环不能省。次级填的是 --ground，而面板、卡片和框体本身也是 --ground：按钮落在它们
   上面时两块同色，一条边都读不出来（暗色一档 --ground 是 #080A0D，整颗键化在面里）。
   「只在操作条上出现、比条子亮一档就是边界」对配置页的「添加文件夹」「选择文件夹」不成立：
   它们直接坐在 --ground 上。换成实底亮色会让一颗次要动作抢过主动作的份量，所以照 Geist
   的做法：填充不动，补一圈 1px。实心的三档不挂环，它们自己的填充就是边界。
   环用 box-shadow 而不是 border：不占盒子，32px 的行高和并排实心档的外沿仍然齐平。 */
.geist-button{box-sizing:border-box;height:32px;padding:0 14px;border:0;
  border-radius:var(--control-radius);background:var(--ground);color:var(--ink);display:inline-flex;
  align-items:center;justify-content:center;gap:6px;font:inherit;font-size:var(--fs-md);font-weight:500;
  line-height:1;white-space:nowrap;cursor:pointer;box-shadow:0 0 0 1px var(--line-soft);
  transition:background-color .15s ease-in-out,color .15s ease-in-out}
/* 次级的悬停抬到 Geist 实测的 gray-200 那一档（浅色 #EBEBEB、深色 #1F1F1F），墨色 8% 压在
   自己那块面上即是这个值。比它淡的一档在白底上与静止态只差两三个色阶，鼠标压在哪颗按钮
   上看不出来；用墨色混合而不是某个固定 token，是因为浅色要往下压、深色要往上抬。 */
.geist-button:hover:not(:disabled){background:color-mix(in srgb,var(--ink) 8%,var(--ground))}
.geist-button,.geist-button:hover{text-decoration:none}
/* 按钮里唯一还挂图标的一类是走出 Peach 的链接，那一枚的长相由下面两条说。 */
.geist-button svg{width:14px;height:14px;flex:none;stroke:currentColor;fill:none;stroke-width:2}
/* 走出 Peach 的那一枚标，全站一个写法。
   2026-09-06 实测 vercel.com/geist/colors 的 `Learn More`：14px/20px 的文字配 16×16 图标、
   紧跟文字 2px、与文字行居中、颜色就是链接自己的 currentColor
   （docs/reference-snapshots/vercel-geist-external-link-mark.md）。尺寸不是全站一个常数，
   而是相对所在文字抬一档，16/14≈1.15em，所以 12px 小字旁边它也跟着小、比例不变。
   全站只有这两条，页面各自不再调档：这一枚每次说的都是同一件事，尺寸和间距按页面
   分档的话，同一个意思就有八个长相，并排放着像是八种不同的标。
   间距只由 .externallink 的 gap 说：挂在标自己身上会和容器的 gap 叠起来。 */
.externallink{display:inline-flex;align-items:center;gap:2px;min-width:0}
svg.externalmark{width:1.15em;height:1.15em;flex:none;align-self:center;
  stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.geist-button:focus-visible{outline:2px solid var(--tungsten);outline-offset:2px}
.geist-button:disabled{background:var(--sunk);color:var(--muted);cursor:not-allowed}
/* Vercel/Geist loading actions look unavailable and reject repeat activation,
   but remain focusable; JS pairs this with aria-disabled instead of disabled. */
button[aria-busy="true"],[role="button"][aria-busy="true"]{
  cursor:wait!important;opacity:.55!important;filter:saturate(.35)
}
/* 强调档那一面不在这里：它的渐变、边、阴影和悬停归 `board.css` 的 Board primary 一处，
   入口页由 `routes_pages._board_button_rules()` 把同一批规则内联进去。这里留着一份
   Geist 的墨色实底加悬停提亮的话，它的 `.primary:hover:not(:disabled)` 比 Board 那条
   静止规则还重一个类，压上去就跳回墨色——那正是错误页上看到的那颗。 */
/* 实心档的禁用态要自己写：.geist-button:disabled 排在 .primary 前面，同权重下后者赢，
   不补这条的话禁用的主动作还是满格实底，看着可点。 */
.geist-button.primary:disabled{background:var(--sunk);color:var(--muted);box-shadow:0 0 0 1px var(--line-soft)}
/* 危险档静止就是实底红，不是描红边等悬停才填。只换描边和文字色的话，两个状态在暗色底上
   几乎一样亮，按下去之前看不出这是不可逆动作；Geist 的 error 变体也是 `rgb(217,48,54)`
   实底配纯白字，静止态就红。销毁按钮全站只有这一个写法：标记上写 `.danger`。
   档位只能挂在 `<button>` 的 `.danger` 上，不要另起一个页面类来承担同一件事。
   `.junkactions button` 这类容器规则自己就填底色，权重是一个类加一个类型名；
   页面单类顶不过它——同权重时按文件顺序决胜，而分区号更大的页面文件总在后面，
   红会被容器那份底色静静盖掉，选择器和声明单看都没有问题。
   类名写两遍是为了权重：`button.danger.danger` 算两个类加一个类型名，比容器规则
   高一档，进哪个容器都赢；只写一遍就和容器规则同权重，而它排在前面，红会被盖掉。 */
button.danger.danger{
  background:var(--drop);color:#fff;box-shadow:none}
button.danger.danger:hover:not(:disabled){
  background:color-mix(in srgb,var(--drop) 82%,#000);color:#fff}
button.danger.danger:disabled{
  background:var(--sunk);color:var(--muted);box-shadow:0 0 0 1px var(--line-soft)}
.geist-button.error{background:#da2f35;color:#fff;box-shadow:none}
.geist-button.warning{background:#ff990a;color:#000;box-shadow:none}
.geist-button.error:hover:not(:disabled){background:color-mix(in srgb,#da2f35 85%,#000);color:#fff}
.geist-button.warning:hover:not(:disabled){background:color-mix(in srgb,#ff990a 85%,#000);color:#000}
.geist-button:is(.error,.warning):disabled{background:var(--sunk);color:var(--muted);box-shadow:0 0 0 1px var(--line-soft)}
.geist-badge{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;height:20px;padding:0 6px;border-radius:var(--badge-radius);background:var(--hover);color:var(--ink-2);font-size:var(--fs-xs);font-weight:500;line-height:1;white-space:nowrap}
.skiplink{position:fixed;z-index:1000;left:12px;top:8px;transform:translateY(-150%);
  padding:9px 13px;border-radius:var(--control-radius);background:var(--ink);color:var(--ground);text-decoration:none;
  transition:transform .12s ease}
.skiplink:focus-visible{transform:translateY(0)}
/* 覆盖式滚动条：原生的藏掉，滑块浮在内容上，一列宽度都不占。
   2026-09-05 实测 vercel.com 侧栏（`nav.overflow-y-auto` + 它的兄弟 `div.group/track`）：
   容器 `scrollbar-width:none`、`padding-right:0`，轨道 `absolute right-0 top-2 bottom-2`
   宽 12px、底透明；滑块 `absolute right-0` 宽 3px、`border-radius` 实测 9999px（Peach 走
   --pill-radius 的 999px，整圆只留一个来源）、
   `background:var(--ds-gray-500)`（浅色一档实测 rgb(201,201,201)，暗色一档 rgb(69,69,69)），
   轨道悬停时宽到 6px，`transition:width .15s cubic-bezier(.4,0,.2,1)`；高度和位移写在滑块的
   行内样式上，按可视高/内容高的比例算。
   Peach 的 --field-ring-hover 是 rgba(0,0,0,.21)，压在白面上正好是 rgb(201,201,201)，
   同一个数还自带暗色一档，所以直接用它，不再为滑块单开一个颜色 token。
   轨道 12px 只是好瞄的命中区，看得见的永远只有那 3px。
   有意差异一：Vercel 的轨道是 `hidden pointer-fine:block`，粗指针一档整条不画。Peach 让滑块
   照画，只把「能拖」收给细指针。原生滚动条已被容器关掉，跟着 Vercel 收掉整条就等于触屏上
   既没有滑块也没有系统指示器，读不出这一列有多长、自己在哪儿；而滑块本身不占宽度，
   留着不挡任何东西。拖不动的东西不该吃走触摸事件，所以命中区由 pointer-events 分档。
   有意差异二：Vercel 只在内部滚动区这么做，根滚动条留给浏览器；Peach 整页也换掉，见上面
   `html` 那条。整页那一条的命中区必须收到滑块本身：轨道横跨整个窗口右边，12px 宽的透明
   条会把右边缘所有点击吃掉——设置面板就贴在那儿。 */
.ovtrack{position:absolute;z-index:2;pointer-events:none}
.ovtrack[hidden]{display:none}
@media (pointer:fine){.ovtrack{pointer-events:auto}}
.ovtrack.ov-y{right:0;top:8px;bottom:8px;width:12px}
.ovtrack.ov-x{left:8px;right:8px;bottom:0;height:12px}
.ovthumb{position:absolute;border-radius:var(--pill-radius);background:var(--field-ring-hover);
  cursor:default;transition:width .15s cubic-bezier(.4,0,.2,1),height .15s cubic-bezier(.4,0,.2,1)}
.ov-y .ovthumb{right:0;top:0;width:3px}
.ov-x .ovthumb{left:0;bottom:0;height:3px}
.ov-y:hover .ovthumb,.ov-y.dragging .ovthumb{width:6px}
.ov-x:hover .ovthumb,.ov-x.dragging .ovthumb{height:6px}
.ovtrack.page{position:fixed;top:0;bottom:0;right:0;z-index:91;pointer-events:none}
@media (pointer:fine){.ovtrack.page .ovthumb{pointer-events:auto}}
.ovtrack.page:hover .ovthumb{width:3px}
.ovtrack.page .ovthumb:hover,.ovtrack.page.dragging .ovthumb{width:6px}
/* 弹层盖住页面时整页那条轨道要收起来：页面已经 overflow:hidden，它拖不动也没有对应的
   滚动可读，留着就是弹层旁边多一条竖线。判据跟着锁走——下面两个类就是加 overflow:hidden
   的那两处，弹层自己那条轨道挂在卡片上，不受这条影响。 */
body.settings-open>.ovtrack.page,body.photolight-open>.ovtrack.page{display:none}
/* 原生滚动条在挂上覆盖式那条之后才藏：属性是 attachOverlayScrollbar() 挂上去的，
   脚本没跑到的容器就还留着系统滚动条，不会变成一个既没有滑块也拖不动的死框。 */
[data-overlay-scrollbar]{scrollbar-width:none}
[data-overlay-scrollbar]::-webkit-scrollbar{display:none}
/* 横向行不显示滚动条（本来就能拖动/滚轮横滚） */
.tier,.tagbar,.srow,.nrow{scrollbar-width:none}
.tier::-webkit-scrollbar,.tagbar::-webkit-scrollbar,
.srow::-webkit-scrollbar,.nrow::-webkit-scrollbar{display:none}
/* 横滚容器的 overflow-x:auto 会把计算后的 overflow-y 一起变成 auto，圆头像贴着容器
   上沿时，:hover 和选中态那圈外扩 box-shadow 就被削平。留 3px 上方余量再用等量负
   margin 抵掉，位置一点不动。头像圈被遮是同一个缺陷重犯多次，横向头像行统一走这
   一条，不要再逐处补 padding。`.followauthors` 自带余量，不在此列。 */
.tier,.relatedpeople{padding-top:3px;margin-top:-3px}

/* 自绘单值拉条（`dialSliderHtml` / `wireDialSlider`）。几何取自 feralui.dev/gradients 的
   `.dial-slider`，实测记在 docs/reference-snapshots/feralui-studio-boardui-accent-measured.md：
   30px 高、20px 轨道、每 10% 一根 1×8px 刻度、3×20px 的圆头抓手，刻度与抓手默认透明，
   悬停或拖动时才现形——一条静止的拉条上，九根刻度和一枚抓手比那条线本身还抢眼。
   上游用自己的 ink 色画焦点环，这里仍走站内的 --tungsten：蓝只给焦点环这条是全站的。
   当前位置只有 --dial-at 一个来源，填充宽度和抓手位置读的是同一个数。 */
.dial{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:10px;min-width:0}
.dial-slider{position:relative;display:flex;align-items:center;height:30px;min-width:0;
  cursor:pointer;touch-action:none;user-select:none;-webkit-user-select:none}
.dial-track{position:relative;width:100%;height:20px;border-radius:var(--pill-radius);
  background:color-mix(in srgb,var(--ink) 11%,transparent);box-shadow:inset 0 1px 2px var(--overlay-5);overflow:hidden}
.dial-fill{position:absolute;inset:0 auto 0 0;width:var(--dial-at,0%);
  background:color-mix(in srgb,var(--ink) 42%,transparent)}
.dial-slider[data-dragging] .dial-fill{background:color-mix(in srgb,var(--ink) 55%,transparent)}
.dial-ticks{position:absolute;left:0;right:0;display:flex;justify-content:space-between;
  padding:0 10%;opacity:0;pointer-events:none;transition:opacity .12s ease}
.dial-ticks span{width:1px;height:8px;background:color-mix(in srgb,var(--ink) 20%,transparent)}
.dial-handle{position:absolute;left:var(--dial-at,0%);width:3px;height:20px;transform:translateX(-50%);
  border-radius:var(--pill-radius);background:var(--ink);box-shadow:0 1px 2px var(--overlay-5);
  opacity:0;pointer-events:none;transition:opacity .12s ease}
.dial-slider:hover :is(.dial-ticks,.dial-handle),
.dial-slider:focus-visible :is(.dial-ticks,.dial-handle),
.dial-slider[data-dragging] :is(.dial-ticks,.dial-handle){opacity:1}
.dial-slider:focus-visible{outline:2px solid var(--tungsten);outline-offset:3px;border-radius:var(--pill-radius)}
.dial-value{min-width:44px;color:var(--ink-2);font-weight:400;text-align:right}


/* 锚定菜单：无展开动画，固定在视口内，内容在菜单内滚动而不是把页面撑出滚动条。
   2026-09-04 实测 vercel.com 后台的 Filter and Sort 菜单：底色是页面底色本身（浅色下
   纯白），不是下一档面板灰；6px 内边距、12px 圆角、行高 36px；阴影是四层叠出来的
   薄影加一圈 1px 描边，不是一大团黑。这里的描边由 border 给。 */
.popmenu{position:fixed;z-index:var(--layer-popover);max-height:calc(100dvh - 16px);
  padding:6px;border:1px solid var(--border-15);border-radius:var(--floating-radius);
  background:var(--ground);display:grid;gap:2px;
  box-shadow:0 1px 1px light-dark(rgba(0,0,0,.02),rgba(0,0,0,.4)),0 4px 8px -4px light-dark(rgba(0,0,0,.04),rgba(0,0,0,.45)),0 16px 24px -8px light-dark(rgba(0,0,0,.06),rgba(0,0,0,.55));
  overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain}
.popmenu[hidden]{display:none}
/* 进顶层的那几个要把 popover 的默认盒子拆掉：浏览器给 `[popover]` 的是 inset:0 加
   margin:auto 的居中盒，菜单会被拉宽并落在屏幕正中，而定位算的是 left/top。 */
.popmenu[popover]{inset:auto;margin:0}

/* Geist Select：触发器穿输入框那身盒子，面板就是上面那个锚定菜单。为什么不用原生
   下拉、几何取自哪里，见 web/js/ui-components.js 里这个组件的注释。 */
.gselect{position:relative;display:inline-flex;min-width:0}
.gselectfield{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:space-between;
  gap:8px;width:100%;min-width:0;height:var(--control-h);padding:0 10px 0 12px;
  border:1px solid var(--field-ring);border-radius:var(--control-radius);
  background:var(--ground);color:var(--ink);font:inherit;font-size:var(--fs-md);line-height:20px;
  text-align:left;cursor:pointer;transition:border-color .12s ease,background-color .12s ease,box-shadow .12s ease}
.gselectfield>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gselectmenu [data-select-content]{display:flex;align-items:center;gap:8px;min-width:0}
.gselectfield .gselectmark{vertical-align:-3px;margin-inline-end:8px}
.gselectmark{width:16px;height:16px;flex:none;object-fit:contain;stroke:currentColor;fill:none;stroke-width:2}
.gselectfield>svg{width:16px;height:16px;flex:none;stroke:currentColor;fill:none;color:var(--muted)}
.gselectfield:hover:not(:disabled){border-color:var(--field-ring-hover)}
/* 展开时按住焦点态：菜单开着而触发器看起来没被点中，读起来像两个控件。 */
.gselectfield[aria-expanded="true"]{box-shadow:inset 0 0 0 2px var(--color-border-button-active)}
.gselectfield:disabled{background:var(--surface);color:var(--muted);cursor:not-allowed}
.gselectfield:focus-visible{outline:2px solid var(--tungsten);outline-offset:2px}
/* 行首是站标，不是勾：选中态只有填充，行里也就没有一枚勾要占位。几何以 board.css 那条
   浮层菜单项为准，这里是遗留层自己的基线，两边的内边距写同一个值。 */
.popmenu.gselectmenu button{display:flex;align-items:center;justify-content:flex-start;gap:8px;width:100%;min-height:var(--control-h);
  padding:0 8px;border:0;border-radius:var(--control-radius);background:transparent;color:var(--ink-2);
  font:inherit;font-size:var(--fs-md);text-align:left;cursor:pointer}
.popmenu.gselectmenu button:hover{background:var(--hover);color:var(--ink)}
.popmenu.gselectmenu button[aria-selected="true"]{background:var(--hover);color:var(--ink)}

/* 工具行收窄：宽度不够时，带图标又带文字的按钮和下拉只留图标，而不是把半排控件
   推到第二行去——一行控件断成两行以后，读的人要先认出哪几个原本是一组的。

   判据是这一行自己的宽度，不是视口：同一个视口下侧栏收起与展开留给这行的宽度差
   两百像素，按视口断点会在一边早早折掉、在另一边照样超框。容器查询量的是内容宽，
   740px 是这一行所有控件加间隔的实测占位（646px 控件 + 7 道 12px 间隔）再留一档。

   收哪几件由控件自己声明，不按位置猜：同排还有纯图标键和分段控件（本来就没有字），
   以及只有文字的按钮（收起来是一排认不出的空框）。收起后名字全靠 title 与
   aria-label，所以文字只许 display:none，不能连可访问名字一起藏掉。 */
[data-collapse-toolbar]{container-type:inline-size}
@container (max-width:740px){
  [data-collapse-toolbar] [data-collapse-label]{display:none}
  [data-collapse-toolbar] :is(.fbtn,.geist-button):has(>[data-collapse-label]){width:var(--control-h);padding:0}
  /* 下拉收掉的是当前值和那枚 chevron，框内左侧的前缀图标移到正中，成为它唯一的字形。 */
  [data-collapse-toolbar] [data-collapse-field] .gselectfield{width:var(--control-h);padding:0}
  [data-collapse-toolbar] [data-collapse-field] .gselectfield>*{display:none}
  [data-collapse-toolbar] [data-collapse-field]>svg{left:50%;transform:translateX(-50%)}
}

/* Geist Modal：确认一次写操作。实测 https://vercel.com/geist/modal（2026-09-04）：
   540px 宽、12px 圆角、窄屏两侧各留 10px，正文 20px 内边距，操作条粘在底、12px 内边距、
   两端对齐，遮罩纯黑不带模糊。淡入用它给出的 .3s cubic-bezier(.175,.885,.32,1.1)；
   它同时过渡 transform，起始值未取得，所以这里只做透明度。 */
.geist-modal{box-sizing:border-box;width:min(540px,calc(100vw - 20px));
  /* max-width 要自己写死：<dialog> 的 UA 样式带一条 calc((100% - 6px) - 2em)，
     窄屏上它比 Geist 的 calc(100vw - 20px) 更小，卡片会莫名再窄十几像素。 */
  max-width:min(540px,calc(100vw - 20px));max-height:min(800px,80vh);
  padding:0;border:0;border-radius:var(--floating-radius);
  background:var(--surface);color:var(--ink);overflow:hidden auto;overscroll-behavior:contain;
  box-shadow:0 0 0 1px var(--border-15),0 32px 72px -12px #0009,0 8px 32px -12px #0009;
  opacity:0;transition:opacity .3s cubic-bezier(.175,.885,.32,1.1),
    overlay .3s allow-discrete,display .3s allow-discrete}
.geist-modal::backdrop{background:#0009;opacity:0;
  transition:opacity .3s cubic-bezier(.175,.885,.32,1.1),
    overlay .3s allow-discrete,display .3s allow-discrete}
.geist-modal[open],.geist-modal[open]::backdrop{opacity:1}
@starting-style{.geist-modal[open],.geist-modal[open]::backdrop{opacity:0}}
.geist-modal-body{padding:20px;font-size:var(--fs-md);line-height:20px;color:var(--ink-2)}
.geist-modal-body h3{margin:0;font-size:var(--fs-xl);line-height:26px;font-weight:600;color:var(--ink)}
.geist-modal-body p{margin:8px 0 0;line-height:20px;overflow-wrap:anywhere}
.geist-modal-body [data-modal-error]:not(:empty){margin-top:12px}
.geist-modal-footer{position:sticky;bottom:0;display:flex;justify-content:space-between;gap:16px;
  padding:12px;background:var(--surface)}
.geist-modal-footer>div{display:flex;gap:16px}
/* 表单正文：字段和列表都在这 20px 内边距里，和标题、操作条对着同一条边。 */
.geist-modal-form{display:contents}
.geist-modal-fields{display:grid;gap:14px;margin-top:16px}
.geist-modal-fields:empty{display:none}
.modalfield{display:grid;gap:8px}
.modalfield>span{color:var(--muted);font-size:var(--fs-xs)}
/* 一排能撤掉的名牌。整块不是命中区，能点的只有每块右边那一枚——名字本身在这里
   没有别的动作，让整行可点只会让人以为点了会发生什么。 */
.aliaschips{display:flex;flex-wrap:wrap;gap:6px}
.aliaschip{display:inline-flex;align-items:center;gap:2px;min-height:28px;padding:0 2px 0 10px;
  border:1px solid var(--border-10);border-radius:var(--pill-radius);background:var(--overlay-5);
  color:var(--ink-2);font-size:var(--fs-sm);overflow-wrap:anywhere}
.aliaschip button{display:grid;place-items:center;width:24px;height:24px;flex:none;padding:0;border:0;
  border-radius:50%;background:transparent;color:var(--muted);cursor:pointer}
.aliaschip button:hover{background:var(--hover);color:var(--drop)}
.aliaschip svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:2;
  stroke-linecap:round;stroke-linejoin:round}
/* 勾选一行等于选中它，所以整行都是命中区，勾选框在行首。 */
.picklist{display:grid;gap:6px;max-height:280px;overflow-y:auto;overscroll-behavior:contain}
.pickrow{display:flex;align-items:center;gap:11px;min-height:44px;padding:0 12px;
  border:1px solid var(--border-10);border-radius:var(--control-radius);
  background:var(--overlay-5);color:var(--ink);cursor:pointer}
.pickrow:hover{background:var(--hover)}
.pickrow:has(input:checked){border-color:var(--field-ring-focus)}
.pickrowtext{display:flex;align-items:baseline;justify-content:space-between;gap:10px;
  min-width:0;flex:1}
.pickrowtext b{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}
.pickrowtext small{flex:none;color:var(--muted)}
/* 卡片上的次要动作收进这一枚点点点：它们不该和「打开」并排占一整行。 */
.cardmenu{flex:none;margin-left:auto;position:relative}
.cardmenubtn{display:grid;place-items:center;width:30px;height:30px;padding:0;border:0;
  border-radius:var(--control-radius);background:transparent;color:var(--muted);cursor:pointer}
.cardmenubtn:hover,.cardmenubtn[aria-expanded="true"]{background:var(--hover);color:var(--ink)}
.cardmenubtn svg{width:18px;height:18px;stroke:currentColor;fill:none}
.cardmenupanel{min-width:172px}
.cardmenupanel button{display:flex;align-items:center;gap:9px;width:100%;min-height:34px;
  padding:0 9px;border:0;border-radius:var(--control-radius);background:transparent;
  color:var(--ink-2);font:inherit;font-size:var(--fs-md);text-align:left;cursor:pointer}
.cardmenupanel button:hover{background:var(--hover);color:var(--ink)}
/* 菜单里的销毁项是红字，不是那块实底红：实底是主操作条上的按钮档，摆进一列菜单
   行里会变成整条红块，读起来像这一项被选中了。所以不走 .danger 那个类。 */
.cardmenupanel button.destructive{color:var(--drop)}
.cardmenupanel button.destructive:hover{background:color-mix(in srgb,var(--drop) 12%,transparent);color:var(--drop)}
.cardmenupanel svg{width:16px;height:16px;flex:none;stroke:currentColor;fill:none}

/* 「图比框小就别放大」的共用摆法。索引页的厂牌大格和厂牌资料页的大位共用它：那两处
   摆的都是标识，而标识的原始尺寸差得很远，实测 42 px 到 1378 px 都有。
   度量由 fitNativeImage 在图片加载后写进 --markw / --markh，这里只负责摆：
   没有变量时 min(100%,100%) 就是铺满，JS 没跑到的地方按 CSS 自己收场。
   补底那一层是同一张图放大后模糊，往框外多铺一圈，让糊边落在 overflow 之外。 */
[data-fit-native]{position:relative}
/* 选择器点名这两处容器：`.icell .ring img` 与 `.entityportrait img` 各自写着
   width:100% 和 object-fit:cover，光凭一个属性选择器压不过它们。

   尺寸走 `--markw` / `--markh` 而不是 `width:auto`：还没度量过的图按 auto 是 0×0，
   而 `loading="lazy"` 见到 0×0 的框就认定它不在视口里，于是永远不去取这张图——
   图不来就没有 load、没有 load 就没有度量，两边互相等着，格子一直空着。缺省值
   仍是铺满，度量之后才换成那张图自己的像素数。 */
.icell .ring[data-fit-native="mark"] img,.entityportrait[data-fit-native="mark"] img,
.icell .ring[data-native-small="true"] img,.entityportrait[data-native-small="true"] img{
  width:var(--markw,100%);height:var(--markh,100%);margin:auto;object-fit:contain;
  max-width:100%;max-height:100%;z-index:1}
[data-fit-native]::before{content:"";position:absolute;inset:-14%;z-index:0;
  background:var(--markbg,none) center/cover no-repeat;filter:blur(16px) saturate(1.4);
  opacity:.55;pointer-events:none}
/* 首字母是取不到图时的垫底。图不再铺满框之后它会从旁边露出来，所以有图就收起来。
   资料页那一侧的首字母是 `.entityportrait>span`，`:has(img)` 已经替它收好了。 */
.icell .ring[data-fit-native]:has(img) .ini{display:none}
/* ── 顶栏：无侧栏，图占主导 ── */
.top{position:sticky;top:0;z-index:70;display:flex;align-items:center;gap:12px;
  height:var(--topH);padding:0 16px;border-bottom:1px solid var(--line-soft);
  background:color-mix(in srgb,var(--ground) 88%,transparent);
  backdrop-filter:saturate(1.8) blur(20px);-webkit-backdrop-filter:saturate(1.8) blur(20px)}
.ib{background:none;border:0;border-radius:var(--control-radius);cursor:pointer;width:40px;height:40px;
  flex:none;display:inline-flex;align-items:center;justify-content:center;color:var(--ink-2);
  padding:0;line-height:0;transition:background .12s,color .12s}
.top .ib[hidden]{display:none}
.ib svg{width:21px;height:21px;stroke:currentColor;fill:none;stroke-width:2;
  stroke-linecap:round;stroke-linejoin:round;display:block}
.ib:hover{color:var(--ink);background:var(--hover)}
.ib[aria-pressed="true"]{background:var(--hover);color:var(--ink)}
.brand{display:flex;align-items:center;gap:9px;flex:none;text-decoration:none;color:inherit;height:32px}
.mark{width:28px;height:28px;flex:none;display:block;object-fit:contain}
.brand h1{font-family:Bahnschrift,"Segoe UI Variable Display","Microsoft YaHei UI","PingFang SC",sans-serif;font-weight:600;margin:0;font-size:var(--fs-xl);line-height:1;letter-spacing:.08em}
.brand .sub{font-size:var(--fs-xs);line-height:1;letter-spacing:.14em;color:var(--muted);padding-top:2px}
.search{flex:1;max-width:620px;margin:0 auto;display:flex;align-items:center;gap:8px;
  background:var(--surface);border:1px solid var(--line);border-radius:var(--pill-radius);padding:8px 16px;position:relative;
  transition:box-shadow .12s ease}
.search:focus-within{box-shadow:inset 0 0 0 2px var(--color-border-button-active)}
.search input{flex:1;min-width:0;background:none;border:0;outline:none;font-size:var(--fs-lg)}
.search>svg{width:21px;height:21px;flex:none;stroke:var(--muted);fill:none;stroke-width:2;stroke-linecap:round}
/* 补全按种类分组，一次可能有七组，所以下拉栏自己滚：滚到底不把页面接着往下带，
   否则在搜索框里翻补全会把身后的列表也翻走。 */
.searchmenu{position:absolute;z-index:130;left:0;right:0;top:calc(100% + 8px);padding:8px;
  max-height:min(60vh,520px);overflow:auto;overscroll-behavior:contain;
  border:1px solid var(--border-10);border-radius:var(--floating-radius);
  background:color-mix(in srgb,var(--surface) 96%,var(--ground));
  box-shadow:0 22px 56px -20px #000b;backdrop-filter:saturate(1.5) blur(26px)}
.searchmenu[hidden]{display:none}.searchgroup+ .searchgroup{border-top:1px solid var(--line-soft);margin-top:7px;padding-top:7px}
.searchgroup h3{margin:6px 10px;color:var(--muted);font-size:var(--fs-xs);letter-spacing:.14em}
.searchoption{width:100%;height:38px;border:0;border-radius:var(--control-radius);background:transparent;color:var(--ink);
  display:flex;align-items:center;gap:11px;padding:0 10px;text-align:left;cursor:pointer}
.searchoption:hover,.searchoption.active{background:var(--hover)}.searchoption svg{width:18px;height:18px;stroke:var(--muted);fill:none;flex:none}
.searchoption span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.searchoption .removehistory{margin-left:auto;border:0;background:none;color:var(--muted);padding:7px;cursor:pointer}
/* 命中的别名跟在统称后面，作品数靠右。两个都是这一行的注脚，不与名字争分量。
   宽度不够时别名先让位（收缩权重比统称高）：系列的规范名本身就是一整句日文标题、
   别名是它的整句英文译名，平摊的话一行里两段都收在省略号里，谁也读不出来。 */
.searchoption .matched{flex:0 8 auto;min-width:0;color:var(--muted);font-size:var(--fs-xs)}
.searchoption .n{margin-left:auto;flex:none;color:var(--muted);font-size:var(--fs-xs);font-variant-numeric:tabular-nums}
/* 两栏与否看下拉栏自己有多宽，不看窗口：宽屏里搜索框随侧栏开合变宽变窄，窄屏它盖过
   返回键铺成一整条。一栏里摆不摆近作、视频排成格还是排成行，看的是那一栏自己的宽度。 */
.searchmenu{container:searchmenu/inline-size}
.searchcol{container:searchcol/inline-size;min-width:0}
/* 页签照复核分类那一排药丸：选中只有填充，未选中悬停只提字色；数用 Board 的计数徽标。 */
.searchtabs{display:flex;gap:4px;padding:2px 2px 8px;margin-bottom:4px;border-bottom:1px solid var(--line-soft);
  overflow-x:auto;scrollbar-width:none;overscroll-behavior-x:contain}
.searchtabs::-webkit-scrollbar{display:none}
.searchtabs button{flex:none;display:inline-flex;align-items:center;height:30px;padding:0 10px;border:0;
  border-radius:var(--control-radius);background:none;color:var(--color-text-secondary);font-size:var(--fs-sm);
  white-space:nowrap;cursor:pointer}
.searchtabs button:hover{color:var(--ink)}
.searchtabs button[aria-selected="true"]{background:var(--picked);color:var(--ink)}
.searchgroup+ .searchresults,.searchcol+ .searchcol{border-top:1px solid var(--line-soft);margin-top:7px;padding-top:7px}
/* 门面一律圆片，和顶栏头像、厂牌药丸同一个形状；人是 40，公司是 28。 */
.searchface{position:relative;flex:none;width:28px;height:28px;border-radius:50%;overflow:hidden;background:var(--hover)}
.searchperson .searchface{width:40px;height:40px}
.searchface img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
.searchface .ini{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  color:var(--muted);font-size:var(--fs-sm)}
.searchoption:is(.searchperson,.searchwork){height:auto;min-height:52px;padding-block:6px}
.searchmeta{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.searchname{display:flex;align-items:baseline;gap:8px;min-width:0}
.searchsub{color:var(--muted);font-size:var(--fs-xs)}
/* 小封面和卡片同一个 3:4 容器，取景换算读的就是这个比例。宽 36 刚好排下不加字距的「无预览」。 */
.searchmenu .pic{--card-ratio:.75;flex:none}
.searchmenu .pic .nopic{letter-spacing:0}
.searchwork>.pic{width:36px}
/* 行里的 span 一律截断裁切，这一排要让出焦点环。 */
.searchoption .searchpeeks{display:none;flex:none;gap:6px;margin-left:auto;overflow:visible}
.searchpeek{width:36px;padding:0;border:0;border-radius:var(--badge-radius);background:none;cursor:pointer}
.searchpeek .pic{width:100%}
.searchpeek:hover .pic{outline:1px solid var(--border-15);outline-offset:-1px}
.searchpeek:focus-visible{outline:2px solid var(--tungsten);outline-offset:2px}
@container searchcol (min-width:420px){.searchoption .searchpeeks{display:flex}}
/* 一栏够宽时视频排成封面格，番号在下、署名再下一行；窄了就是一行一部，封面缩成一枚小图。 */
@container searchcol (min-width:400px){
  .searchgroup[data-kind="asset"] .searchitems{display:grid;grid-template-columns:repeat(auto-fill,minmax(96px,1fr));gap:4px}
  .searchoption.searchwork{flex-direction:column;align-items:stretch;gap:6px;padding:6px}
  .searchwork>.pic{width:100%}
}
/* 右栏钉在顶上：左栏人名一长串往下翻时，视频那一格不跟着滚走。 */
@container searchmenu (min-width:720px){
  .searchresults[data-split]{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);column-gap:8px;align-items:start}
  .searchresults[data-split]>.searchcol+ .searchcol{position:sticky;top:0;border-top:0;margin-top:0;padding-top:0;
    border-left:1px solid var(--line-soft);padding-left:8px}
}
.readout{display:flex;gap:20px;flex:none;align-items:center}
.readout div{display:flex;flex-direction:column;line-height:1.3;align-items:flex-start}
.readout b{font-size:var(--fs-lg);font-weight:600;font-variant-numeric:tabular-nums}
.readout span{font-size:var(--fs-xs);color:var(--muted)}

/* ── 顶部三层：女优圆头像 / 厂牌 / 内容标签 ── */
/* 有显式 display 的元素不会被浏览器默认的 [hidden]{display:none} 隐藏，
   而 showHomeSurfaces 会把它的行内 display 清空——这条写死以免两边抵消。 */
.tiers[hidden]{display:none}
.tiers{position:relative;z-index:1;padding:10px 0 8px;
  border-bottom:0;
  background:linear-gradient(180deg,color-mix(in srgb,var(--frost-light) 38%,transparent),transparent)}
/* padding-top 归上面那条头像圈余量的规则管，这里不能用 padding 简写把它清掉。 */
.tier{display:flex;gap:14px;overflow-x:auto;scrollbar-width:none;padding-inline:16px;padding-bottom:8px;align-items:flex-start}
.tier::-webkit-scrollbar{display:none}
.tier:last-child{padding-bottom:2px}
/* 一层：圆头像 */
.av{flex:none;width:76px;cursor:pointer;text-align:center;background:none;border:0;padding:0;color:inherit}
.av .ring{display:block;width:64px;height:64px;border-radius:50%;margin:0 auto 5px;overflow:hidden;
  background:transparent;position:relative;transition:box-shadow .14s}
.av:hover .ring{box-shadow:0 0 0 1px var(--border-15)}
.av[aria-pressed="true"] .ring{box-shadow:0 0 0 2px var(--ink)}
.av .ring img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
.av .ring .ini{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  font-size:var(--fs-xl);color:var(--muted);font-family:Bahnschrift,"Microsoft YaHei UI","Microsoft YaHei","PingFang SC",sans-serif}
.av .nm{display:block;font-size:var(--fs-xs);color:var(--ink-2);overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap;max-width:76px}
.av[aria-pressed="true"] .nm{color:var(--ink)}
/* 二层：厂牌大药丸 */
/* 这一排点开的是厂牌页，不是往当前列表上加一条筛选，所以走实线：虚线留给
   `#tagbar` 那一排真能开关的筛选，见 03-filterbar.css。选中填 --picked 与 .pill 一致。 */
.brandpill{flex:none;display:flex;align-items:center;gap:8px;padding:6px 14px 6px 6px;
  border-radius:var(--pill-radius);background:transparent;border:1px solid var(--field-ring);cursor:pointer;
  font-size:var(--fs-md);white-space:nowrap}
.brandpill:hover{border-color:var(--field-ring-hover);color:var(--ink)}
.brandpill[aria-pressed="true"]{border-color:var(--field-ring);background:var(--picked);color:var(--ink)}
.brandpill .mk{width:32px;height:32px;border-radius:50%;background:#CFCFCF;flex:none;
  display:flex;align-items:center;justify-content:center;font-size:var(--fs-xs);
  font-family:Bahnschrift,"Microsoft YaHei UI","Microsoft YaHei","PingFang SC",sans-serif;color:var(--ground);overflow:hidden;position:relative}
/* 厂牌标识文件本身就是不透明方图（`peach.images.bake_square`），边距烤在图里，
   所以这里铺满圆片：留 inset 会在图自带的底之外再围出一圈灰。`#CFCFCF` 只是无图
   时垫在首字母后面的底色。
   标识按文件原样出图，不去饱和也不压亮度：这些颜色是厂牌自己的识别色，调过之后
   同一张图在这一排和资料页、详情页对不上，浅色一档里还会读成一块脏掉的红。 */
.brandpill .mk img{position:absolute;inset:0;width:100%;height:100%;
  object-fit:cover;display:block}

/* ── 常驻筛选层：原位保持透明，只在真正吸顶后加磨砂黑底 ── */
.tagbar{position:sticky;top:var(--topH);z-index:61;height:var(--filterH);padding:9px 16px;
  border-bottom:1px solid transparent;background:transparent;
  display:flex;gap:7px;overflow:hidden;align-items:center}
/* 横滚只发生在右半截。左边那四枚是视图，四选一、恒有一枚生效，读的是「这一屏现在
   在看什么」；右边的标签才是可加可不加的筛选。读数跟着筛选一起滚走的话，滚到一半
   这条上就没有任何东西说明现在看的是哪一档了。
   这三格是同一个控件，哪一页用到都是这一份：最左端是页面自己的那一档（资料页的媒体
   类型，首页没有），住在 `.filterscroll` 外，任何宽度下都不滚；`.viewpills` 与
   `.tagscroll` 住在里面。
   这一层定位不为自己，是给那块滑动玻璃当落脚点：窄屏时它是横滚的那一层，玻璃住进来
   才会跟着内容走、越界跟着被裁。 */
.filterscroll{display:flex;gap:7px;flex:1;min-width:0;align-items:center;position:relative}
.viewpills{display:flex;gap:7px;flex:none;align-items:center}
.tagscroll{display:flex;gap:7px;flex:1;min-width:0;align-items:center;
  overflow-x:auto;overflow-y:hidden;scrollbar-width:none;overscroll-behavior-inline:contain}
.tagscroll::-webkit-scrollbar{display:none}
/* 窄到这个宽度，钉住的东西得让位。四枚视图在 375 上要占掉两百多像素，钉住它们等于把
   标签挤到屏幕外——那一条看着满，其实一枚可点的筛选都露不出来。所以换成整段一起滚：
   滚动从标签那一格上提到外面这层，视图跟着标签走，只有最左端那一档还钉着。
   它才是离开就没有上下文的那个——四枚视图和标签问的都是「这一批里挑哪些」，滑走了
   滑回来就是；媒体类型定的是这一页现在摆的是视频还是照片，两枚圆键一共六十来像素，
   钉住它的代价是窄屏上唯一付得起的。 */
@media(max-width:760px){
  .filterscroll{overflow-x:auto;overflow-y:hidden;scrollbar-width:none;overscroll-behavior-inline:contain}
  .filterscroll::-webkit-scrollbar{display:none}
  .filterscroll>.tagscroll{flex:0 0 auto;min-width:auto;overflow:visible}
}
/* 选中药丸和生效筛选共用高亮填充。 */
.pill{flex:none;height:var(--filterItemH);padding:0 20px;border:1px solid var(--field-ring);
  border-radius:var(--pill-radius);background:transparent;display:inline-flex;align-items:center;justify-content:center;
  color:var(--muted);cursor:pointer;font-size:var(--fs-lg);white-space:nowrap;text-decoration:none}
.pill:hover{border-color:var(--field-ring-hover);color:var(--ink)}
.pill[aria-pressed="true"]{border-color:var(--field-ring);background:var(--picked);color:var(--ink)}
/* 计数是标签的附注，不是标签名的一部分。同字号同色贴在一起时「巨乳63」读成一个词，
   那个数看着像这条标签自己就叫这个名字；降一档字号、退一档颜色，再留出一个字的间隔，
   它才回到「这个标签下有多少」的位置上。间隔只能由这里给：`.pill` 是 flex 容器，
   标签与计数之间写一个空格文本节点不参与布局，量出来仍然是零。
   等宽数字让一排药丸里的数位对齐，换一批标签时数字不左右跳。 */
.pill .n{margin-left:7px;color:var(--muted);font-size:var(--fs-xs);font-variant-numeric:tabular-nums}
.pill[aria-pressed="true"] .n,.pill:hover .n{color:var(--ink-2)}

/* 虚线只画在「可加可不加、此刻还没加上去」的筛选药丸上：首页筛选条右半截的标签是这一条；
   关注页的标签与来源、资料页的标签、标签页的类型药丸都住在 Board 的玻璃浮层里，
   同一判据由 board.css 那几条写。视图那几枚恒有一枚生效，一律实线。
   2026-09-05 实测 vercel.com/<team>/~/deployments 的筛选令牌，两端是这样的：
   未生效 `background:rgba(0,0,0,0)` + `1px dashed rgba(0,0,0,.21)`；
   已生效 `background:#FFFFFF` + `1px solid rgba(0,0,0,.08)`。两个数就是 Peach 的
   --field-ring-hover 与 --field-ring，暗色一档换成同位置的白透明度。
   分界就是那道 `.sep`：左边四枚 `[data-state]` 是视图，四选一、恒有一枚生效，
   「这条筛选还没加上去」对它们从来不成立，画成虚线等于说三枚视图都还没生效。
   右边的 `[data-tag]` 才是可加可不加的筛选，虚线在那里才指向一件真事。
   悬停只把虚线拉成实线——「这颗点得动」——填充留给真正选中的那颗：
   Vercel 的令牌可叠加，悬停预演成生效态没有代价；Peach 这一排的标签也是可叠加的，
   但同排还有恒常生效的视图胶囊，悬停也填就会同屏读出两种「已生效」。 */
#tagbar .pill[data-tag]:not([aria-pressed="true"]){border-style:dashed;border-color:var(--field-ring-hover)}
#tagbar .pill[data-tag]:not([aria-pressed="true"]):hover{border-style:solid}
.sep{flex:none;width:1px;height:19px;background:var(--field-ring-hover);margin:0 3px}

/* 多标签交集筛选条 */
/* 这一条从无到有会把底下整块推下四十像素。留着空位等它是另一种代价——一屏没有任何
   筛选时头顶白占一条。所以让它自己长出来：高度从 0 走到 auto，二百来毫秒，推的过程
   看得见就不叫跳。`interpolate-size` 是这段动画的前提，缺了它高度直接跳到终值。 */
.combo{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin:0 0 10px;
  overflow:hidden;interpolate-size:allow-keywords;height:auto;
  transition:height .22s cubic-bezier(0,0,.2,1),margin-bottom .22s cubic-bezier(0,0,.2,1)}
/* 一条也没有时不占位：高度是 0，那 10px 下边距却照留，成了下面那块浮层头顶的一段空白。 */
.combo:empty{height:0;margin-bottom:0}
@media(prefers-reduced-motion:reduce){.combo{transition:none}}
/* 交集条上的每一颗都是一个标签，跟详情面板里那一颗是同一样东西：名字加一颗撤掉它的
   键。所以照那一颗画——同一档圆角、一圈线、一样的身量和一样的移除键，只把面换成
   --picked，因为这一颗说的是「已经加上去了」。差别留给状态，形状不参与表意。 */
.combo .cb{box-sizing:border-box;display:inline-flex;align-items:center;min-height:30px;padding:0 0 0 11px;
  border:1px solid var(--line);border-radius:var(--tag-radius);overflow:hidden;
  background:var(--picked);color:var(--ink-2);font-size:var(--fs-md);line-height:20px}
.combo .cb b{display:grid;place-items:center;width:28px;height:28px;margin-left:5px;
  cursor:pointer;color:var(--muted);font-size:var(--fs-xs);font-weight:400}
.combo .cb b:hover{color:var(--drop);background:color-mix(in srgb,var(--drop) 12%,transparent)}
.combo .clr{min-height:28px;display:inline-flex;align-items:center;padding:0 6px;background:none;border:0;color:var(--muted);cursor:pointer;font-size:var(--fs-xs);text-decoration:none}
/* 管理页标题下的说明只用这一种字形和间距；不要再让统计容器或吸顶计数栏各画一份。 */
.pagelede{margin:0 0 16px;color:var(--muted);font-size:var(--fs-sm);line-height:1.5}
.pagelede[hidden]{display:none}
/* 页面动作挂在说明行右端：它和左边那句说的是同一批文件，自己占一整行时，标题和
   内容之间会空出一条只放一个按钮的带子。 */
.pagelede-actions{display:flex;align-items:center;justify-content:space-between;gap:16px}
.pagelede-actions .batchaction{box-sizing:border-box;min-height:32px;padding:0 14px;flex:none;
  display:inline-flex;align-items:center;border:1px solid transparent;border-radius:var(--control-radius);
  background:var(--ink);color:var(--ground);font:inherit;font-size:var(--fs-xs);font-weight:500;cursor:pointer}
.pagelede-actions .batchaction:focus-visible{outline:2px solid var(--tungsten);outline-offset:2px}
/* 统计页 */
.stats{padding:0 0 42px}
.playlistpage{padding:4px 0 42px}.playlistpage>header{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;margin-bottom:16px}.playlistpage h2{margin:0 0 8px}.playlistpage p{margin:0;color:var(--muted);font-size:var(--fs-sm)}
.playlistcreate{display:flex;align-items:flex-end;gap:8px;flex-wrap:wrap}.playlistcreate label{display:grid;gap:8px;color:var(--muted);font-size:var(--fs-xs);flex:1 1 200px;max-width:320px}.playlistcreate input{min-width:200px}
/* 提交键是这一屏的主动作，走 Geist 的实心档；38px 取自 .geist-input，
   与输入框同高才算一行，和「添加关注」那张表也是同一档。 */
.playlistcreate .geist-button{height:38px;padding:0 13px}
.playlistcreate [data-playlist-state]{flex-basis:100%;color:var(--drop);font-size:var(--fs-xs)}
/* 一份播放列表就是一叠视频，和首页那张 Mix 卡是同一件东西：封面、叠层、悬浮翻动和
   署名行全部走 .card / .mixstack / .mixmeta，这里只定这一页的栅格。 */
.playlistcards{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:18px}
/* 账本只读那一条是 Note：框、图标字形和整套色调都在 `.geist-note` 那一份里，这里只留
   它在页面里的位置。自己再描一圈暖色边、把字写成 --ink 的话，底色说的是「注意」、
   文字说的是「普通说明」，同一条提示上两句话对不上。 */
.geist-note.runtimegate{margin:0 0 12px}
/* 复核页的正文归 React 子树（ADR-0031）。这几条只服务遗留骨架：一列分类、工具条上
   那三枚占位和六张占位卡，卡高由 `--review-card-height` 说了算。 */
.review{--review-card-height:440px;padding:0 0 42px}
.reviewsection{margin-bottom:20px}
.reviewlist{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:14px;align-items:start}
.reviewbulkbar{display:flex;align-items:center;flex-wrap:wrap;gap:12px;margin-bottom:16px}
/* 数据管理整页收进一条窄列（vercel.com/geist/fieldset 实测示例块宽 812px），
   fieldset 一行一个，不再拼三列。 */
.cleanuppage{width:min(812px,100%);margin:0 auto;display:grid;gap:32px}
/* 活动页的骨架（`board-skeleton.ts` 的 `/activity`）：遗留外壳在 React 子树接手之前
   铺的那几张空卡。窄列与数据管理同一条 812px，因为接手之后的正文也停在这条中线上；
   卡片框体沿用 `.cleanupfieldset`，这里只排列表与卡内那一列。 */
.activitypage{width:min(812px,100%);margin:0 auto;display:grid;gap:32px}
.activitysection{display:grid;gap:12px;min-width:0}
.activitysection .geist-fieldset-title{margin:0}
.activity-runs{list-style:none;margin:0;padding:0;display:grid;gap:12px;min-width:0}
.activity-run>.geist-fieldset-content{display:grid;gap:6px}
.cleanupgrid{display:grid;grid-template-columns:minmax(0,1fr);gap:20px}
/* Geist Fieldset：全框只有一条线，在底部操作条上方。标题是正文区的第一行，
   底下不划线（证据：vercel.com/geist/fieldset 的 Multiple Fieldsets 示例）。
   操作条固定 52px、竖直居中，按钮一律靠右；左侧有说明时说明推到最左。
   字阶照 Geist 示例实测：标题 20px/600、说明 14px/400——标题必须压过正文。 */
.geist-fieldset-title{margin:0 0 8px;color:var(--ink);font-size:var(--fs-xl);font-weight:600}
.cleanupfieldset>.geist-fieldset-footer{box-sizing:border-box;min-height:var(--fieldset-bar-h);
  margin:0;padding:12px 12px 12px 20px;display:flex;align-items:center;justify-content:flex-end;gap:8px;
  border-top:1px solid var(--line-soft);background:var(--overlay-5)}
/* 说明可以换行、按钮不能被压扁：窄屏下操作条仍是一行，说明占剩下的宽度，
   条子按内容长高（证据：vercel.com/geist/fieldset 在 375px 下操作条仍是
   flex-direction:row、nowrap，min-height 56px 实测长到 65/85/105px，
   按钮保持 70–186px 的内容宽度，没有一个铺满）。 */
.geist-fieldset-footer>p,.geist-fieldset-footer>span,
.resourcesyncfooter>p,.resourceapplyrow>p{min-width:0;margin-right:auto}
.cleanupfieldset>.geist-fieldset-footer>p{margin:0 auto 0 0;text-align:left}
/* 没有 min-height：卡片高度由内容决定，和同页的「网盘与账本」一致。三列时那条
   176px 地板是用来对齐同一行的，改成单列后它只是把每张卡都撑出一段空白。 */
.cleanupfieldset{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;overflow:hidden;
  border:1px solid var(--field-ring);border-radius:var(--floating-radius);background:var(--ground)}
.cleanupfieldset>.geist-fieldset-content{flex:1;min-height:0;padding:20px}.cleanupfieldset strong{display:block;font-size:var(--fs-md);font-weight:500;color:var(--ink-2)}
.cleanupfieldset p{margin:0;color:var(--muted);font-size:var(--fs-md);line-height:1.45}
/* 主数字下面那行分项。空着时整行不占位——没有分项的卡不该比别人多一段留白。 */
.cleanupmeta{margin-top:6px;font-size:var(--fs-sm)}
.cleanupmeta:empty{display:none}
/* 操作条上的按钮一律填 --ground：条子是 --page，按钮要比条子亮才分得出来，
   与 .geist-button 的次级档同一套。hover 只提亮底色、描边和文字，不出蓝色。

   危险按钮静止态就是 --drop 实底加白字，配方在 `01-base.css` 的 `button.danger`，
   全站一份。次级不描边，与 `.geist-button` 同一条：它自己是一块比操作条亮一档的面。 */
/* 排除下拉触发器要用 `:where(:not(…))`，不能直接写 `:not(…)`。采集来源那张卡的下拉触发器
   正好是 `.cleanupfieldset` 里的一个 button，这条不排除它就把 `.gselectfield` 那圈线也去了
   ——按钮不描边，输入类控件仍旧描边。而裸 `:not()` 会把这条抬到 0,2,1，反过来压过
   `.geist-button.primary`（0,2,0），同一张卡上的主动作于是丢掉实底变成一行字；
   `:where()` 权重恒为零，排除生效、层级不动。 */
.cleanupfieldset button:where(:not(.gselectfield)){box-sizing:border-box;flex:none;min-height:32px;padding:0 12px;border:0;border-radius:var(--control-radius);box-shadow:0 0 0 1px var(--line-soft);
  background:var(--ground);color:var(--ink-2);display:inline-flex;align-items:center;justify-content:center;gap:6px;cursor:pointer;
  transition:color .15s ease-in-out,background-color .15s ease-in-out}
.cleanupfieldset button:where(:not(.gselectfield)):hover{background:color-mix(in srgb,var(--ink) 8%,var(--ground));color:var(--ink)}
.cleanupfieldset button:where(:not(.gselectfield)) svg{width:16px;height:16px;stroke:currentColor;fill:none}
.cleanupfieldset .cleanupstate{margin-top:8px;font-size:var(--fs-sm);color:var(--ink-2)}
.cleanupfieldset .cleanupstate:empty{display:none}
/* 整理那张卡：两份模板、一排预设、一段结果。字段标签是块级、排在控件上方（Geist 的
   Label 就是这个形状）；来源下拉靠框内前缀图标自报用途，同一行不再挂文字标签。 */
.organizefields{display:grid;gap:12px;margin-top:12px}
.organizefield{display:grid;gap:6px;min-width:0}
.organizefield>span{color:var(--ink-2);font-size:var(--fs-sm)}
.organizepresets{display:flex;flex-wrap:wrap;gap:8px}
.organizeplan{list-style:none;margin:8px 0 0;padding:0;display:grid;gap:4px;min-width:0}
.organizeplan li{display:flex;align-items:center;gap:8px;min-width:0;
  color:var(--ink-2);font-size:var(--fs-sm)}
.organizeplan li span{min-width:0;overflow:hidden;white-space:nowrap}
.organizeplan li span:last-child{color:var(--ink)}
.cleanupfieldset .organizestate{margin-top:12px;display:grid;gap:8px}
.cleanupfieldset .organizestate:empty{display:none}
.stats h2{margin:0 0 4px;font-size:var(--fs-2xl)}
.stats .sub{color:var(--muted);margin:0}
/* 来源和凭证页的正文归 React 子树（ADR-0031）。这两条只服务遗留骨架：那一句静态说明
   和四块占位卡，宽度另由 `board.css` 那条钉到 `--board-content`。 */
.scraping-page{width:min(812px,100%);margin:0 auto;display:grid;gap:20px}
.scraping-page>h1,.scraping-page p{margin:0}
#libraryProcessing{scroll-margin-top:80px}

/* 分类那一列与工具条在骨架里也要占住位置，正文归 React 子树之后只剩这几条。 */
.reviewcontrols{display:grid;gap:12px;padding-block:12px;margin-bottom:16px}
.reviewcontrols .reviewtabs{margin-bottom:0}
.reviewbulktoolbar{padding-block:0;margin-bottom:0}
/* 统计页与口味页的等待态版式：指标带、主详情、同层数据面板。
   两页的正文都归 React（ADR-0031），这里留下的是遗留壳铺的那张骨架读到的层级。 */
.insightpage,.tastepage{width:min(1100px,100%);margin:0 auto;padding:0 0 48px;min-width:0}.statsdashboard{display:grid;gap:16px}
.insighttoolbar,.tastehead{min-height:40px;display:flex;align-items:center;justify-content:space-between;gap:10px 14px;flex-wrap:wrap}
.insighttoolbar p{margin:0;color:var(--muted);font-size:var(--fs-xs)}
.insightswitch{display:inline-flex;padding:3px;border:1px solid var(--line-soft);border-radius:var(--surface-radius);background:var(--ground)}.insightswitch label{cursor:pointer}.insightswitch input{position:absolute;opacity:0;pointer-events:none}.insightswitch span{height:30px;display:grid;place-items:center;padding:0 12px;border-radius:var(--control-radius);color:var(--muted);font-size:var(--fs-sm)}.insightswitch input:checked+span{background:var(--picked);color:var(--ink);font-weight:500}.insightswitch input:focus-visible+span{outline:2px solid var(--tungsten);outline-offset:-2px}
/* 下限按内容实测取：一格里最宽的是 24px 的数字和那行 12px 的补充说明，加左右
   各 16px 内边距是 140px。四格因此到 645px 视口仍是一整条，再窄由下面的手机
   规则接手改成露边滚动。写 190px 时窗口一收窄就先滚起来，横向滚动条压在一条
   本来能压扁的带子下面。 */
.metricstrip,.tastesummaries{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(140px,1fr);overflow-x:auto;overscroll-behavior-x:contain;border:1px solid var(--line-soft);border-radius:var(--floating-radius);background:var(--ground)}
.tastesummary{position:relative;box-sizing:border-box;min-height:96px;min-width:0;display:grid;align-content:center;gap:4px;padding:14px 16px;border:0;border-right:1px solid var(--line-soft);border-radius:0;background:transparent;color:var(--ink);text-align:left}.tastesummary:last-child{border-right:0}
.tastesummary>span{color:var(--muted);font-size:var(--fs-xs)}.tastesummary>b{font-size:var(--fs-2xl);line-height:1.15;font-variant-numeric:tabular-nums}.tastesummary>small{color:var(--ink-2);font-size:var(--fs-xs);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tastestate{min-height:20px;color:var(--meter);font-size:var(--fs-xs);margin:8px 0}.tastesummaries{margin-bottom:16px}
.tastehero{overflow:hidden;border:1px solid var(--line-soft);border-radius:var(--floating-radius);background:var(--ground);min-height:330px;display:grid;grid-template-columns:minmax(260px,36%) minmax(0,1fr);margin-bottom:16px}
.insightcopy{padding:28px 24px;border-right:1px solid var(--line-soft);display:flex;flex-direction:column;align-items:flex-start}.insightcopy>span{color:var(--muted);font-size:var(--fs-xs)}
.tastebars{min-width:0;padding:24px;align-self:center}
.insightpanel{overflow:hidden;border:1px solid var(--line-soft);border-radius:var(--floating-radius);background:var(--ground)}.insightpanel>header{min-height:52px;display:flex;align-items:center;padding:0 16px;border-bottom:1px solid var(--line-soft)}.insightpanel>header h3{margin:0;font-size:var(--fs-md)}.insightpanel>header p{margin:2px 0 0;color:var(--muted);font-size:var(--fs-xs)}.insightpanelbody{min-width:0;padding:14px 16px 16px}
/* `/js/ui-components.js` 的 `progressHtml` 那一条，关注管理页与遗留任务状态都读它。 */
.geist-progress{height:8px;margin-top:7px;overflow:hidden;border-radius:var(--pill-radius);background:var(--line-soft)}.geist-progress i{display:block;width:var(--progress-value);height:100%;border-radius:inherit;background:var(--tungsten)}
@media(max-width:900px){.tastehero{grid-template-columns:minmax(220px,40%) minmax(0,1fr)}}
@media(max-width:640px){.insighttoolbar,.tastehead{align-items:stretch}.insightswitch{align-self:flex-start}.metricstrip,.tastesummaries{grid-auto-columns:minmax(168px,82%)}.tastehero{min-height:0;grid-template-columns:minmax(0,1fr)}.insightcopy{border-right:0;border-bottom:1px solid var(--line-soft);padding:22px 18px}.tastebars{padding:18px}.insightpanel>header{padding:0 14px}.insightpanelbody{padding:12px}.tastesummary>b{font-size:var(--fs-xl)}}
/* 索引页：全部创作者 / 全部标签 */
.index{padding:4px 0 20px;min-width:0;max-width:100%;overflow-x:clip}
/* 实体吸顶栏用负边距把分隔线铺到 main 左右边缘；默认的横向裁剪会把这 16 px 截掉。 */
body.entity-open .index{overflow-x:visible}
.index .ihead{display:flex;align-items:center;gap:12px;margin-bottom:20px}
.index .ihead h2{margin:0}
.indexheading{display:inline-flex;align-items:center}
.index .ihead .geist-search{flex:1;max-width:320px;margin-left:auto}
#indexCount{color:var(--muted)}
/* 浮层住在 `#indexFilters` 里方便整块重画，但 sticky 只能在父级的高度里吸住，而这个父级
   就只有浮层自己那么高。`display:contents` 让它退出盒树，浮层的边界改由整页 `.index` 给。 */
#indexFilters{display:contents}
.igrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:16px 10px}
.icell{display:flex;flex-direction:column;align-items:center;gap:6px;cursor:pointer;
  background:none;border:0;color:inherit;padding:8px 4px;border-radius:var(--surface-radius)}
.icell:hover{background:var(--hover)}
.icell .ring{display:block;width:70px;height:70px;border-radius:50%;overflow:hidden;
  background:var(--sunk);position:relative}
.icell .ring img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;
  object-position:var(--face,50% 50%)}
.icell .ring .ini{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  font-size:var(--fs-2xl);color:var(--muted);font-family:Bahnschrift,"Microsoft YaHei UI","Microsoft YaHei","PingFang SC",sans-serif}
.icell .nm{font-size:var(--fs-xs);text-align:center;line-height:1.3;word-break:break-all}
.icell .n{font-size:var(--fs-xs);color:var(--muted)}
/* 大图版式照 JAV 大图那条规矩：列宽和列数一个字都不动，只把圆框拉成 3:4 竖幅，
   于是每屏人数不变、每张脸更大，窄屏也不会突然掉成一列。 */
.igrid[data-layout="big"] .icell{padding:6px 4px 10px}
.igrid[data-layout="big"] .icell .ring{width:100%;height:auto;aspect-ratio:3/4;
  border-radius:var(--surface-radius)}
.igrid[data-layout="big"] .icell .nm{font-size:var(--fs-sm)}
/* 公司那一格里是一枚标识。竖幅是给脸留的形状：方标铺进 3:4 的框，`object-fit:cover`
   左右各裁掉四分之一，SOD 和 S1 的字就顶在边上。方框一个像素都不裁，同一列宽下
   标识反而更大；列也放宽一档，省下的高度换成宽度。 */
.igrid[data-cells="company"]{grid-template-columns:repeat(auto-fill,minmax(180px,1fr))}
.igrid[data-cells="company"][data-layout="big"] .icell .ring{aspect-ratio:1}
.index .ihead .iconswitch{flex:none}
.tagwall{display:flex;flex-wrap:wrap;gap:7px}
/* 标签页的类型药丸住在首页那块玻璃浮层的上排，是同一枚 `.pill`，只多一个说类型的色点；
   选中那一枚的线和填充取自己的类型色，跟标签在卡片上的着色一致。 */
.tagcategories .pill{gap:7px}
.tagcategories .pill::before,.alphatag::before{content:"";width:7px;height:7px;border-radius:50%;
  background:var(--tag-color,var(--muted));flex:none}
.tagcategories .pill[aria-pressed="true"]{color:var(--ink);border-color:color-mix(in srgb,var(--tag-color,var(--ink)) 45%,transparent);
  background:color-mix(in srgb,var(--tag-color,var(--ink)) 12%,transparent)}
/* 浮层下排：读数、按首字跳转、视图切换。跳转那一排按首字排开，放不下就横滚。 */
.tagcount .alphajump{display:flex;gap:2px;min-width:0;overflow-x:auto;scrollbar-width:none}
.tagcount .alphajump::-webkit-scrollbar{display:none}
.alphajump button{flex:none;height:30px;min-width:30px;padding:0 6px;border:0;border-radius:var(--control-radius);
  background:transparent;color:var(--muted);font-size:var(--fs-xs);cursor:pointer}
.alphajump button:hover{color:var(--ink)}
/* 「Aa」是 1.4:1 的字形，宽高都锁死只能按宽缩，画出来比邻座矮一截。 */
.tagcount .iconswitch svg.iconwide{width:auto}
.tagselection{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin:0 0 18px;padding:12px 14px;
  border:1px solid var(--line-soft);border-radius:var(--floating-radius);background:var(--ground)}
.tagselection[hidden]{display:none}.tagselection label{display:flex;align-items:center;gap:9px;margin-right:auto;cursor:pointer}
.tagselection label>span{display:grid;gap:2px}.tagselection label b{font-size:var(--fs-sm)}
.tagselection label small{color:var(--muted);font-size:var(--fs-xs)}
.tagselection button{height:34px;padding:0 13px;border:0;border-radius:var(--control-radius);box-shadow:0 0 0 1px var(--line-soft);background:var(--ground);color:var(--ink-2);cursor:pointer}
.tagselection button:hover:not(:disabled){background:var(--hover);color:var(--ink)}
/* 强调档那一面归 `board.css` 的 Board primary 一处；这里再写一份 Geist 的墨色实底，
   它的 `:hover` 比 Board 那条静止规则重一个类，压上去就跳回墨色。 */
.tagselection button:disabled{background:var(--sunk);color:var(--muted);cursor:not-allowed}
.tagcategories .all::before{background:linear-gradient(135deg,#55A7FF,#D783FF)}
.index-tags .tg,.alphatag{--tag-color:#7A91A8}
.index-tags .tg.r34-general,.alphatag.r34-general,.tagcategories .general{--tag-color:#55A7FF;--r34-tag:#55A7FF}
.index-tags .tg.r34-artist,.alphatag.r34-artist,.tagcategories .artist{--tag-color:#E36C6C;--r34-tag:#E36C6C}
.index-tags .tg.r34-character,.alphatag.r34-character,.tagcategories .character{--tag-color:#68C76F;--r34-tag:#68C76F}
.index-tags .tg.r34-copyright,.alphatag.r34-copyright,.tagcategories .copyright{--tag-color:#D675D6;--r34-tag:#D675D6}
.index-tags .tg.r34-metadata,.alphatag.r34-metadata,.tagcategories .metadata{--tag-color:#F5A24A;--r34-tag:#F5A24A}
.index-tags .tg.r34-unknown,.alphatag.r34-unknown{--tag-color:var(--muted);--r34-tag:var(--muted)}
/* 抽屉里的关注标签沿用同一个青色，让「这是在线那套词表」在两处一致。 */
.chip.online{border-color:color-mix(in srgb,#5AC8D8 42%,transparent);background:color-mix(in srgb,#5AC8D8 10%,transparent)}.tg.general,.alphatag.general,.tagcategories .general{--tag-color:#55A7FF}
.index-tags .tg.artist,.alphatag.artist,.tagcategories .artist{--tag-color:#F1A454}
.index-tags .tg.meta,.alphatag.meta,.tagcategories .meta{--tag-color:#7F909E}
.index-tags .tg.relationship,.alphatag.relationship,.tagcategories .relationship{--tag-color:#F1A454}
.index-tags .tg.role,.alphatag.role,.tagcategories .role{--tag-color:#ED78AF}
.index-tags .tg.appearance,.alphatag.appearance,.tagcategories .appearance{--tag-color:#A989F5}
.index-tags .tg.scene,.alphatag.scene,.tagcategories .scene{--tag-color:#53B8A4}
.index-tags .tg.story,.alphatag.story,.tagcategories .story{--tag-color:#E19A5B}
.index-tags .tg.position,.alphatag.position,.tagcategories .position{--tag-color:#D66E8D}
/* 标签云里的标签比卡片上的大一档：这一页它们是主体，不是附注。这三行原先写在
   openIndex 的模板里当内联 style，于是同一个控件的外观有两个来源，改一次要
   两处都记得。 */
.index .index-tags .tg{border-color:color-mix(in srgb,var(--tag-color) 32%,transparent);color:var(--ink-2);
  background:color-mix(in srgb,var(--tag-color) 7%,transparent);padding:5px 12px;font-size:var(--fs-sm)}
.index .index-tags .tg .n{opacity:.6;font-size:var(--fs-xs)}
.index .index-tags .tg:hover{border-color:color-mix(in srgb,var(--tag-color) 62%,transparent);
  background:color-mix(in srgb,var(--tag-color) 13%,transparent)}
.index .index-tags .tg.selected,.alphatag.selected{background:var(--picked);color:var(--ink)}
/* 字母表：每个首字一张卡，字头是卡的标题，右边挂这一组有几个。卡顶给吸顶的浮层让出
   位置，浮层下排那排跳转键滚过来时卡的上沿正好落在浮层下面。 */
.alphabet{display:grid;gap:16px}
.alphagroup{padding:16px 20px 12px;border-radius:var(--floating-radius);background:var(--ground);
  scroll-margin-top:calc(var(--topH) + 124px)}
.alphagroup h3{display:flex;align-items:center;margin:0 0 8px;font-size:var(--fs-xl);color:var(--ink)}
.alphalist{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:2px 16px}
/* 一行一枚标签：36px 高、控件圆角，悬停抬主文字色 6% 的薄底，取的是排名行那一档身量。 */
.alphatag{display:flex;align-items:center;justify-content:flex-start;gap:9px;height:36px;border:0;background:transparent;
  color:var(--ink-2);padding:0 10px;border-radius:var(--control-radius);text-align:left;cursor:pointer;min-width:0}
.alphatag:hover{background:color-mix(in srgb,var(--ink) 6%,transparent);color:var(--ink)}/* nowrap 是省略号的前提：少了它 text-overflow 永远不触发，长标签改成折行，
   同一行里的其它标签跟着被拉高，两列各行高度还对不齐。 */
.alphatag span:first-of-type{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
.alphatag .n{color:var(--muted);font-size:var(--fs-xs);flex:none}
/* 表头一行放不下四样东西：标题、计数、版式开关、过滤框。挤在一行时每样都被压到
   竖排两行，过滤框直接被压成 0 宽。窄屏改成标题与过滤框一行、开关另起一行；
   开关自己 flex:none，宁可整组换行也不把它拆成两行。 */
@media (max-width:760px){
  .index .ihead{flex-wrap:wrap}
  .indexheading,#indexCount{white-space:nowrap}
  .index .ihead .geist-search{order:1}
  .index .ihead::after{content:"";order:2;flex-basis:100%;height:0}
  .index .ihead .iconswitch{order:3;flex:none}
  /* 公司格放宽一档是给标识换宽度，手机上 343px 只装得下一列，整块卡横着铺满，
     标识却还是那 70px：这一档换不来更大的标识，只换来一屏三家。收回人格那一档，两列。 */
  .igrid[data-cells="company"]{grid-template-columns:repeat(auto-fill,minmax(150px,1fr))}
}
/* 资料卡：一块 --ground 底、浮层圆角、不描边的卡。正文是头像加身份三行，卡底那条色阶带
   放同台艺人——那是这个人的附注，不是这一页的正文。`overflow:hidden` 让底带的直角被卡的
   圆角裁掉。 */
.entityhero{display:flex;flex-direction:column;margin:0 0 22px;border-radius:var(--floating-radius);
  background:var(--ground);overflow:hidden;min-width:0;max-width:100%}
/* 头像跟着右边那列的高度长。身份列有几行是看这个人有多少东西：只有名字和别名时是两行，
   加上外链和看片那一行就是四行，头像钉死 120px 的话卡上下会空出一截，圆框反倒比文字轻。
   所以这一列不再写死宽度：圆框拉满这一行的高度，宽由 `aspect-ratio` 推出来，下限 120px
   保住原来的尺寸，上限 160px 免得内容再多时圆框把这一页的主角抢了。 */
.entityprofile{display:grid;grid-template-columns:auto minmax(0,1fr);gap:20px;padding:20px;
  align-items:center;min-width:0}
.entityprofile>.entityportraitwrap{align-self:stretch;display:flex;align-items:center}
/* 女优有资料时多出第三栏：右边那张资料表（ADR-0069），和身份信息之间隔一道竖线。
   宽 280–360px：生日、身材一行放得下，出道片名截断后还剩一半字。
   三栏的判据是内容区的宽度，不是视口：侧栏展开时同一个窗口留给卡的宽度少两百多像素，
   按视口断点的话 1000px 的窗口里身份那一栏只剩 44px，名字竖着排。内容区不到 900px
   （头像 160、两道间隔、资料表 360 之外身份栏还有 300）时它排到身份信息下面，隔的线换成
   横的；760px 以下整张卡单列居中，表里仍左对齐，两列字才对得齐。容器查询做不了这件事：
   它顺带的布局隔离会让卡里那几枚 fixed 定位的菜单和遮罩改以卡为基准，所以宽度由
   `#index` 上的 `data-hero-wide` 带进来（`syncHeroWide`）。 */
#index[data-hero-wide] .entityprofile:has(>.entityfacts){grid-template-columns:auto minmax(0,1fr) minmax(280px,360px)}
.entityfacts{margin:0;align-self:stretch;justify-self:stretch;align-content:center;display:grid;
  grid-template-columns:auto minmax(0,1fr);gap:10px 16px;min-width:0;padding:2px 0 2px 24px;
  border-left:1px solid var(--line-soft);font-size:var(--fs-sm);line-height:22px;text-align:left}
#index:not([data-hero-wide]) .entityfacts{grid-column:1/-1;border-left:0;border-top:1px solid var(--line-soft);padding:14px 0 0}
.entityfacts dt{display:flex;align-items:center;align-self:start;gap:6px;color:var(--muted);white-space:nowrap}
.entityfacts dd{margin:0;min-width:0;color:var(--ink)}
.entityfacts .num{font-variant-numeric:tabular-nums}
.entityfacts .sub{color:var(--muted)}
.entityfacts .clip{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* 站上的标签只是读数，不是这一页的筛选（那排在下面的筛选条里），所以是标签的脸、不接点击。 */
.entityfacts .facttags{display:flex;flex-wrap:wrap;gap:6px}
.facttag{padding:0 var(--tag-pad-x);border:1px solid var(--line);border-radius:var(--tag-radius);
  background:var(--tag-fill);color:var(--ink-2);font-size:var(--fs-xs);line-height:20px;white-space:nowrap}
.entityfacts dt svg,.entityhero .metaitem>svg{width:16px;height:16px;flex:none;color:var(--muted);
  stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
/* 圆框外面这一层只为换头像那枚加号存在：圆框自己是 `overflow:hidden`（图要被裁成
   圆的），贴在它边上的按钮放进去就没了。 */
.entityportraitwrap{position:relative;width:max-content;max-width:100%}
/* 外面这一层拉满整行的高，圆框却封顶 160px、竖直居中：资料表的标签折成几行时，行比圆框高，
   加号贴这一层的角就掉到圆框下面去了。所以挂加号的宿主叠成和圆框一样大、一样位置的一块，
   高度与居中取圆框同一个 `clamp`，加号的 `right/bottom` 就量在圆框上。它盖在脸上，不接指针。
   不用 `transform` 居中：它会给弹层的 `fixed` 遮罩另起包含块。 */
.entityportraitwrap>[data-avatar-picker]{position:absolute;left:0;right:0;height:clamp(120px,100%,160px);
  top:calc((100% - clamp(120px,100%,160px)) / 2);pointer-events:none}
.entityportraitwrap>[data-avatar-picker]>*{pointer-events:auto}
/* `position:relative` 是给人脸放大留的包含块：放大后的 img 转成绝对定位，偏移必须
   相对这个圆框算，否则会挂到最近的定位祖先上去。见 app.js 的 avatarFrame。 */
.entityportrait{height:100%;min-height:120px;max-height:160px;width:auto;
  aspect-ratio:1;border-radius:50%;overflow:hidden;background:var(--overlay-5);
  display:grid;place-items:center;font-size:var(--fs-4xl);color:var(--muted);max-width:100%;
  position:relative}
.entityportrait.square{border-radius:var(--floating-radius)}.entityportrait img,.entityportrait>span{grid-area:1/1}
/* 首字母只是取不到图时的垫底，有图就必须让位：两者叠在同一格里会一起显出来。
   取图失败时 img 会被兜底链移除，:has 随之不匹配，首字母自动回来——不用再写
   一段 JS 去同步。厂牌标识本身是不透明方图，下面那条 cover 把它铺满整框。 */
.entityportrait:has(img)>span{display:none}
.entityportrait img{position:absolute;inset:0;grid-area:auto;width:100%;height:100%;object-fit:cover;display:block;z-index:1}
.entityidentity{min-width:0;max-width:100%}.entityhero h2{font-size:var(--fs-2xl);font-weight:500;
  line-height:1.25;letter-spacing:0;margin:0 0 9px;overflow-wrap:anywhere}.entityhero .alias b{color:var(--ink);font-weight:inherit}
/* 统称选择器贴着标题：它改的就是标题上那个名字，隔开就看不出改的是哪一个。
   下边距归到这一行上，标题自己不再留。 */
.entitytitle{display:flex;align-items:center;gap:8px;min-width:0;margin:0 0 9px}
.entitytitle h2{margin-bottom:0}
.namepick{position:relative;flex:none}
.npbtn{width:32px;height:32px;flex:none;border:1px solid var(--border-10);border-radius:var(--control-radius);
  background:var(--overlay-5);color:var(--muted);display:grid;place-items:center;cursor:pointer}
.npbtn:hover{background:var(--hover);color:var(--ink)}
.npbtn svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;
  stroke-linecap:round;stroke-linejoin:round}
.npmenu{width:min(260px,calc(100vw - 16px))}
.npmenu button{display:flex;align-items:center;gap:8px;width:100%;padding:7px 9px;border:0;
  border-radius:var(--control-radius);background:transparent;color:var(--ink-2);font-size:var(--fs-sm);
  text-align:left;cursor:pointer;overflow-wrap:anywhere}
.npmenu button:hover{background:var(--hover)}
/* 勾只给当前统称。未选中那几行也占着同样宽度的位置，否则切换时整列文字会横向跳一格。 */
.npmenu button svg{width:16px;height:16px;flex:none;visibility:hidden;stroke:currentColor;fill:none;
  stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.npmenu button[aria-checked="true"]{background:var(--hover);color:var(--ink)}
.npmenu button[aria-checked="true"] svg{visibility:visible}
/* 末尾那一项不在这几个名字之列，隔一条线。它那枚加号要显出来——上一条为了让勾对齐
   把菜单里的图标一律藏了，而这一行本来就没有勾可言。 */
.npmenu hr{margin:4px 1px;border:0;border-top:1px solid var(--border-10)}
.npmenu button[role="menuitem"] svg{visibility:visible}
.entityhero .alias{color:var(--muted);
  font-size:var(--fs-md);line-height:1.45;margin-bottom:0;overflow-wrap:anywhere}
/* 女优页这一行是三项并排，各带一枚图标：视频数、事务所、别名。装不下就整项换行，不在项中间断。 */
.entityhero .alias.metaline{display:flex;flex-wrap:wrap;align-items:center;gap:4px 18px}
.metaitem{display:inline-flex;align-items:center;gap:6px;min-width:0}
.aliasline{flex-wrap:wrap}
.aliasnames>span+span::before,.aliaspop dd>span+span::before{content:"·";margin:0 6px;color:var(--muted)}
.aliasmore,.factmore{height:22px;margin-left:4px;padding:0 8px;border:1px solid var(--line);border-radius:var(--pill-radius);
  background:transparent;color:var(--ink);font:inherit;font-size:var(--fs-xs);cursor:pointer}
.aliasmore:hover,.aliasmore[aria-expanded="true"],.factmore:hover,.factmore[aria-expanded="true"]{
  background:color-mix(in srgb,var(--ink) 8%,var(--ground))}
/* 标签那一格的「+N」排在标签的流里，间距归 flex 的 gap。 */
.factmore{margin-left:0}
/* 「+N」的浮层借锚定菜单那身盒子（`.popmenu`：底色、描边、圆角、投影与开合动效），它是
   `popover`，进顶层才不被资料卡的 `overflow:hidden` 裁掉；`.popmenu` 的 `display:grid` 会压过
   浏览器给没打开的 popover 的 `display:none`，所以这里自己收起。位置由 `wireHeroPop` 算。
   标签的「+N」浮层（`#entityTagPop`）是同一身盒子，里面的标签仍是 `.facttags` 那一排。 */
.popmenu.aliaspop{width:max-content;max-width:min(440px,calc(100vw - 16px));padding:12px 14px;gap:8px;
  color:var(--ink);font-size:var(--fs-sm);line-height:1.5;text-align:left}
.popmenu.aliaspop:not(:popover-open){display:none}
.aliaspophead{margin:0;font-weight:500}
.aliaspop dl{margin:0;display:grid;grid-template-columns:auto minmax(0,1fr);gap:6px 14px}
.aliaspop dt{color:var(--muted);white-space:nowrap}
.aliaspop dd{margin:0;min-width:0;overflow-wrap:anywhere}
.aliaspop dd.wide{grid-column:1/-1}
.aliaspop small{margin-left:4px;color:var(--muted);font-size:inherit}
/* 外链排成一行次级按钮：36px 高、控件圆角、一圈线，跟 Board 的 secondary Button 同一个形。
   它们是「去哪」的入口，不是标签，所以不画成药丸。 */
.entitylinks{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px;max-width:100%}
.entitylinks a,.entitylinks .private{box-sizing:border-box;min-width:0;max-width:100%;height:36px;border:1px solid var(--line);
  border-radius:var(--control-radius);padding:0 12px 0 8px;font-size:var(--fs-sm);text-decoration:none;color:var(--ink);
  background:transparent;display:inline-flex;align-items:center;gap:8px;overflow:hidden}
.entitylinks a:hover{background:color-mix(in srgb,var(--ink) 8%,var(--ground))}
.entitylinks .private{color:var(--muted);border-style:dashed}
/* 圆盘本身不垫底色。X 这类内联品牌标记直接画在按钮上，垫一层 `--sunk` 只会让它比
   周围暗一档，看着像这条链接被禁用了——而图标要说的是「去哪」，不是「能不能点」。 */
.entitylinkicon{width:20px;height:20px;border-radius:var(--badge-radius);background:transparent;display:grid;place-items:center;
  overflow:hidden;position:relative;flex:none}.entitylinkicon svg{width:16px;height:16px;stroke:currentColor;fill:none;
  stroke-width:2}
/* favicon 是别人服务器上的位图，取不到时 `onerror` 会把 img 摘掉、露出下面的地球。
   但白底会让「取到了一张空图」看起来和「圆盘坏了」一模一样——X 的 favicon 正是这种。
   现在 X 走内联品牌标记不再发请求，其余主机也不再垫白底。 */
.entitylinkicon img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;background:var(--sunk)}
/* 品牌标记是填充字形，不吃上面那套描边；它自己画满整个圆盘，所以外面那圈
   `overflow:hidden` 落不到它身上——圆内切在方框里，一个角都碰不到。 */
.entitylinkicon.brand svg{width:100%;height:100%;stroke:none;filter:none}
/* 纯图标的社媒链接：收成正方形，没有文字就不给文字留位置。 */
.entitylinks .iconlink{width:36px;padding:0;gap:0;justify-content:center}
/* 官网域名前显示站点自己那枚图标，取不到才露出地球；与带名称的链接共用间距。 */
.entitylinks a.urllink{letter-spacing:.02em}
/* 看片的那一行：两站自己的横向标识排在一起，不套药丸。标识本身已经说清是哪家，再画
   一圈框、再写一遍站名，说的是同一件事，占的却是三倍高度。
   高度统一 22px、按标识自己的比例定宽；hover 垫一层与上面那排外链同一档的填充，圆角走
   `--control-radius`，负外边距让垫层不把这一行撑开。`--hover` 用不得：浅色下它和资料卡
   的 `--ground` 同是 #f5f5f5，垫上去看不见。32px 的间距减掉左右各 8px 的垫层，
   两枚标识之间还剩 16px，看着才是两条链接而不是一长条。
   选择器写得这么长是为了压过 board.css 里「一句话就是一个链接」那条：它按 `[target=_blank]`
   收所有外链，把字染蓝、抹平内边距，标识里跟着墨色走的那半会一起变蓝。那条算三个类，
   加上 `:any-link` 这里才是四个；board.css 又在最后加载，同分也是它赢。 */
.entrymarks{display:flex;flex-wrap:wrap;align-items:center;gap:32px;margin-top:14px;max-width:100%}
.entityhero .entrymarks a.entrymark:any-link{display:inline-flex;align-items:center;gap:6px;min-width:0;
  text-decoration:none;color:var(--ink);border-radius:var(--control-radius);padding:4px 8px;margin:-4px -8px}
.entityhero .entrymarks a.entrymark:any-link:hover{
  background:color-mix(in srgb,var(--ink) 8%,var(--ground));text-decoration:none}
/* 「订阅新作」开关排在两枚标识后面：它订的就是左边那个 JavDB 演员页。只摆一枚 RSS 图标，
   高度跟两枚标识一样是 22px；关着跟页面墨色，订上了换成 RSS 自己的橙。开关本身藏给读屏
   与键盘，悬停垫底与两枚标识同一档。 */
.entryfeed{position:relative;display:inline-flex;align-items:center;color:var(--ink);cursor:pointer;
  border-radius:var(--control-radius);padding:4px;margin:-4px}
.entryfeed:hover{background:color-mix(in srgb,var(--ink) 8%,var(--ground))}
.entryfeed:has(:checked){color:var(--feed-mark)}
.entryfeed:has(:focus-visible){outline:2px solid var(--tungsten);outline-offset:2px}
.entryfeed:has([aria-busy="true"]){cursor:progress;opacity:.6}
.entryfeed svg{width:22px;height:22px;stroke:currentColor;fill:none;stroke-width:2}
/* 没订的时候那枚图标在墨色和订阅橙之间慢慢呼吸：一枚墨色小图标排在两枚彩色标识后面，
   不提醒就没人知道它能点。只动颜色，重绘只落在这 22px 上；订上就停在橙色。 */
@keyframes entryfeed-breathe{0%,100%{color:var(--ink)}50%{color:var(--feed-mark)}}
.entryfeed:not(:has(:checked)){animation:entryfeed-breathe 2.4s ease-in-out infinite}
.entryfeed:not(:has(:checked)):is(:hover,:has(:focus-visible)){animation:none;color:var(--feed-mark)}
/* 图标的说明走 Tooltip（Geist 实测：最宽 250px、8px 圆角、内边距 8／6px、13px 字、
   1.3 行高），不复制箭头与淡入。悬停和键盘聚焦都出，Escape 收起。
   它是 `popover`，开着的时候进顶层：资料卡为了裁底带直角带着 `overflow:hidden`，写在卡里的
   绝对定位浮层在窄屏上会被右缘切掉一截，顶层既不受祖先裁切，也不会被下方同台艺人那条
   压住。位置由 `wireEntityFeed` 按视口算：默认贴图标下方 8px、左右夹在视口内 8px。
   浏览器给 `[popover]` 的居中盒子在这里拆掉。 */
.entryfeedtip{position:fixed;z-index:var(--layer-tooltip);inset:auto;margin:0;border:0;overflow:visible;
  width:max-content;max-width:250px;padding:6px 8px;border-radius:var(--surface-radius);
  background:var(--ink);color:var(--ground);font-size:var(--fs-sm);font-weight:400;line-height:1.3;
  white-space:normal;pointer-events:none}
/* 两枚标识按字母的高度对齐，不按外框：JavDB 那枚里 `Jav` 三个字母占 viewBox 高的
   71/111，高 22px 时是 14.1px；MISSAV 是排出来的字，衬线体的大写字母高约 0.71em，
   `--fs-xl` 时是 14.2px。外框对齐的话 MISSAV 的字母比 `Jav` 高出两成，一眼看过去它大一号。
   标识自己带着 fill：蓝块和白字是站点的配色，`currentColor` 那半跟页面墨色走。
   这里只定高，宽度由外层 viewBox 的比例推出来。 */
.entrymark svg{height:22px;width:auto}
.entryordinal{color:var(--muted);font-size:var(--fs-sm)}
/* MISSAV 那枚的规则照它站上那套：Halant 500、`MISS` 跟页面墨色、`AV` 用它的粉。 */
.missavmark{font-family:Halant,Georgia,"Times New Roman",serif;font-weight:500;
  font-size:var(--fs-xl);line-height:22px;letter-spacing:.01em;white-space:nowrap}
.missavmark span:last-child{color:#FE628E}
.entityskeletontext{width:100%}
.entityskeletontext .entitytitle .skeleton{width:60%;height:auto}
.entityskeletontext .alias .skeleton{display:block;width:40%;height:20px}
.entityskeletontext .entitylinks .skeleton{width:180px;height:36px}
[data-skeleton^="entity/"] .entityportrait{height:auto}
[data-skeleton^="entity/"] .entityfacts dt .skeleton{display:block;width:56px;height:16px;margin-block:3px}
[data-skeleton^="entity/"] .entityfacts dd .skeleton{display:block;width:70%;height:16px;margin-block:3px}
[data-skeleton^="entity/"] .entitycollectionhead .skeleton{width:90px;height:20px}
@media(max-width:600px){.entityskeletontext .alias .skeleton{margin-inline:auto}}
.entitylinklabel{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* 卡底那条带：横滚的同台艺人轨道，这一排是谁由 aria-label 说，不另写一枚小标签。 */
.entityfoot{display:flex;align-items:center;gap:16px;min-width:0;padding:12px 20px;
  border-top:1px solid var(--line-soft);background:var(--overlay-5)}
.entitytagbar{position:sticky;top:var(--topH);z-index:61;display:flex;align-items:center;
  height:var(--filterH);margin:0 -16px;padding:9px 16px;background:transparent;
  border-bottom:1px solid transparent}
/* 这一格在筛选条里就是首页那条的标签区，横滚、滚动条和窄屏让位都归 `.tagscroll`。
   这里只补一件这一页独有的事：标签不折行，装不下的滑出去。`.entitytags` 这个类名
   在骨架里单独用到，所以它自己那份摆法留着。 */
.entitytagbar .entitytags{flex-wrap:nowrap}
.entitytags{display:flex;align-items:center;flex-wrap:wrap;gap:7px}.entitytags .pill{flex:none}
/* 这条是横向滚动行，里面每一项都得拒绝被压扁——挤瘪的开关点不准，也读不出图标。 */
.entitytags .iconswitch{flex:none}
.entitytags small{color:var(--muted);margin-left:4px}
/* 那排标签数的是视频。看照片或名册时它们一个都对不上屏幕上摆着的东西，点下去还会把
   视图拨回视频，所以那两档只留媒体开关。切回视频它们照旧在。 */
.entitytagbar[data-media-only] .entitytags .pill{display:none}
/* 交集条列的也是作品筛选，照片视图下同样不成立。 */
.entitycombo:has(+.board-filter-frame .entitytagbar[data-media-only]){display:none}
.mediaviewbuttons{display:inline-flex;align-items:center;gap:2px;flex:none}
.mediaviewbuttons .mediaviewbutton{display:grid;place-items:center;flex:0 0 var(--filterItemH);width:var(--filterItemH);height:var(--filterItemH);padding:0;
  border:0;border-radius:50%;background:transparent;color:var(--muted);cursor:pointer}
.mediaviewbuttons .mediaviewbutton svg{width:20px;height:20px;stroke:currentColor;fill:none;stroke-width:2}
.mediaviewbuttons .mediaviewbutton[data-media-icon="image"] svg{fill:currentColor;stroke:none}
.mediaviewbuttons .mediaviewbutton:hover{color:var(--ink)}
.mediaviewbuttons .mediaviewbutton[aria-pressed="true"]{background:var(--picked);color:var(--ink)}
/* 每一枚就是首页那枚 `.av` 女优头像，尺寸、间距与悬停都归 `board.css` 那套共享规则；
   这里只管横滚轨道本身。 */
.relatedpeople{display:flex;flex:1;min-width:0;overflow-x:auto;scrollbar-width:none;overscroll-behavior-inline:contain}
.relatedpeople::-webkit-scrollbar{display:none}
.entitysection{min-width:0;max-width:100%}.entitysection h3{font-size:var(--fs-sm);letter-spacing:.08em;color:var(--muted);margin:0 0 12px}
.entitycollectionhead{position:sticky;top:var(--topH);z-index:60;display:flex;align-items:center;gap:10px;
  min-height:var(--sortH);margin:0 -16px 12px;padding:8px 16px;min-width:0;
  background:transparent;border-bottom:1px solid transparent}
.entitycollectionhead h3{margin:0;flex:0 0 auto}.entitycollectionhead .javlayout{display:inline-flex;gap:2px}
/* 照片这一栏的抬头就是 `.entitycollectionhead`，横向排布、间距和吸顶都归那一条管；
   `.photohead` 只是个记号，给这一屏特有的返回键认门；换一批那枚键跟视频那一排共用
   `.entitybatch`。 */
.photoback{display:flex;align-items:center;gap:4px;height:28px;padding:0 10px 0 6px;border:1px solid var(--line);
  border-radius:var(--control-radius);background:transparent;color:var(--ink-2);font-size:var(--fs-xs);cursor:pointer}
/* 图标是 Lucide 的描边路径，`stroke:currentColor;fill:none` 少一样就会被浏览器
   按默认的 fill:black/stroke:none 画成黑块——深色底上等于看不见。 */
.photoback svg{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:2}
.photoback:hover{background:var(--overlay-5);color:var(--ink)}
/* 固定比例视图使用方框和 cover；瀑布流、响应式列数与关注图片墙共用 21-online.css。
   两种视图在缩略图懒加载前都有非零高度，灯箱使用全图。 */
.photowall{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:10px}
.photowall[data-size="big"]{grid-template-columns:repeat(3,minmax(0,1fr))}
@media (max-width:1400px){.photowall{grid-template-columns:repeat(4,minmax(0,1fr))}
  .photowall[data-size="big"]{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (max-width:1000px){.photowall{grid-template-columns:repeat(3,minmax(0,1fr))}
  .photowall[data-size="big"]{grid-template-columns:repeat(1,minmax(0,1fr))}}
.photocell{display:block;width:100%;aspect-ratio:1;border:0;padding:0;margin:0;background:var(--sunk);
  border-radius:var(--surface-radius);overflow:hidden;cursor:zoom-in;line-height:0}
/* 图还没到的格子是一块空底，不是一行文件名。`alt` 留着给读屏，但格子既然已经按比例
   占好了位置，浏览器就有地方把它画出来——一屏几十格全在等图时，看到的是满屏
   `118abp00325pl.jpg`。这块 `--sunk` 底本身就是这里的骨架。 */
.photocell img{width:100%;height:100%;object-fit:cover;display:block;background:var(--sunk);color:transparent}
.photocell:hover img{filter:brightness(1.06)}
/* 样张取不到时摘掉的只是 <img>，格子留着。瀑布流里格高来自图，空格会塌成 0，所以按
   官方样张最常见的横幅比例撑住那块 `--sunk` 底。 */
[data-layout="masonry"]>.photocell:not(:has(img)){aspect-ratio:3/2}
/* 番号集那排卡片在本地图片墙上方，间距与卡片网格的行距相同。 */
.photosets{margin-bottom:24px}
/* 媒体圆钮只共享 48/24 的几何与图标居中；表面色由使用场景决定。 */
.media-circle{box-sizing:border-box;width:48px;height:48px;padding:0;border:0;border-radius:50%;
  background:rgba(255,255,255,.1);color:#f1f1f1;display:grid;place-items:center;cursor:pointer}
.media-circle:hover{background:rgba(255,255,255,.18)}
.media-circle:focus-visible{outline:2px solid var(--tungsten);outline-offset:3px}
.media-circle svg{display:block;width:24px;height:24px;flex:none;stroke:currentColor;fill:none;
  stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.media-circle.media-overlay{background:rgba(0,0,0,.6);color:#fff;backdrop-filter:blur(16px)}
.media-circle.media-overlay:hover{background:rgba(0,0,0,.78)}
/* 灯箱本体只负责固定视口；主图、顶部工具和底部缩略图各自绝对定位。
   旧三行 grid 会和动态后载的 Swiper 百分比高度形成循环，超宽视口下主图行收成 0，
   结果就是工具跑到顶部、缩略图贴左、原图按自然尺寸溢出。 */
.photolight{position:fixed;inset:0;z-index:200;background:#000;display:block;overflow:hidden}
.photolight:modal{margin:0;padding:0;border:0;max-width:none;max-height:none;width:100vw;height:100dvh;color:#fff}
body.photolight-open{overflow:hidden}
/* 主画布始终占满视口，安全内边距为顶部工具、左右翻页和底部缩略图留位；
   单图没有缩略图条，底部空间会自动还给图片。 */
.photolight .photomain{position:absolute;inset:0;min-width:0;min-height:0;width:100%;height:100%;overflow:hidden}
/* Swiper 的 CSS 和脚本虽会一起等待，仍保留最小结构样式，避免缓存/首载竞态把所有
   slide 当普通块元素同时画出来。只限定 photomain 的直接子级，不影响底部缩略图。 */
.photolight .photomain>.swiper-wrapper{display:flex;width:100%;height:100%}
.photolight .photomain>.swiper-wrapper>.swiper-slide{flex:0 0 100%;width:100%;height:100%;min-width:0;
  min-height:0;display:grid;place-items:center}
.photolight .photomain .swiper-zoom-container{width:100%;height:100%;min-height:0;min-width:0;
  box-sizing:border-box;display:grid;place-items:center;padding:24px 72px 76px}
.photolight.has-strip .photomain .swiper-zoom-container{padding-bottom:148px}
.photolight .photomain img{max-width:100%;max-height:100%;min-width:0;min-height:0;
  object-fit:contain;display:block}
.photoclose{position:absolute;top:12px;right:14px;z-index:5}
.photonav{position:absolute;top:50%;transform:translateY(-50%);z-index:4}
/* 类名用 back/fwd 而不是 prev/next：详情页「接下来」那块用的就是无前缀的 `.next`
   （padding、border-top 和背景色都在里面），同为 0-1-0 特指度且写在后面，会连人带
   样式盖到灯箱按钮上，图标被挤得偏右下。翻页方向只翻图标，不转整个按钮。 */
.photonav.back{left:14px}.photonav.fwd{right:14px}
.photonav.fwd svg{transform:rotate(180deg)}
.photonav.swiper-button-disabled{opacity:0;visibility:hidden;pointer-events:none}
/* 详情、页码和缩放操作统一落在底部；多图时整体抬到缩略图条上方。 */
.photobar{position:absolute;z-index:4;bottom:14px;left:14px;right:14px;display:grid;
  grid-template-columns:1fr auto 1fr;align-items:center;min-height:48px;pointer-events:none}
.photolight.has-strip .photobar{bottom:98px}
.photobar>*{pointer-events:auto}
.photocount{justify-self:center;padding:7px 11px;border-radius:var(--pill-radius);background:rgba(0,0,0,.6);
  color:rgba(255,255,255,.82);font-size:var(--fs-xs);backdrop-filter:blur(16px)}
.photodetailtoggle{justify-self:start;width:40px;height:40px;display:grid;place-items:center;border:0;padding:0;
  border-radius:50%;background:rgba(0,0,0,.6);color:rgba(255,255,255,.82);cursor:pointer;backdrop-filter:blur(16px)}
.photodetailtoggle:hover,.photodetailtoggle[aria-expanded="true"]{background:rgba(0,0,0,.78);color:#fff}
.photodetailtoggle svg{width:20px;height:20px;display:block;stroke:currentColor;fill:none;stroke-width:2;
  stroke-linecap:round;stroke-linejoin:round}
.photozoom{justify-self:end;display:flex;align-items:center;gap:7px;padding:4px;border-radius:var(--pill-radius);
  background:rgba(0,0,0,.6);color:rgba(255,255,255,.82);backdrop-filter:blur(16px)}
.photozoom button{width:28px;height:28px;display:grid;place-items:center;border:0;border-radius:50%;
  padding:0;background:transparent;color:#fff;cursor:pointer}
.photozoom button svg{width:15px;height:15px;display:block;stroke:currentColor;fill:none;stroke-width:2}
.photozoom button:hover{background:rgba(255,255,255,.22)}
.photozoom .photoscale{width:30px}
.photozoom .photooriginal{width:38px;border-radius:var(--control-radius);font-size:var(--fs-xs)}

/* 展开着不动时那道裁边要摘掉：高度过渡需要它，展开完就只剩副作用——里面最后一张卡片
   的落影正好落在下沿外，被切掉半条。`wireCollapse` 在过渡跑完时挂上这个类，收起那一下
   先摘掉。 */
.fcollapse{overflow:hidden;transition:height .2s ease-in-out}
.fcollapse.fcollapse-settled{overflow:visible}
/* Geist Skeleton：整页／大区块等待内容结构时先占住稳定区域。 */
.skeletonpanel{min-height:240px;padding:0}
.skeletonpanel>div{display:grid;gap:12px}
.index-skeleton .icell .ring{background:var(--hover)}
.index-skeleton .icell .nm{width:60%;height:1.3em}
.index-skeleton .icell .n{width:30%;height:1.3em}
.index-skeleton .alphatag{pointer-events:none}
.index-skeleton .alphatag::before{background:var(--hover)}
.index-skeleton .alphatag .skeleton:first-of-type{width:65%;height:1.3em}
.index-skeleton .alphatag .n{width:24px;height:1.3em}
.indexletterskeleton{display:block;width:48px;height:28px;margin:0 0 10px}
.index-skeleton .tagwall .tg{height:auto;border-color:transparent;background:var(--hover)}
.skeleton{display:block;height:18px;border-radius:var(--control-radius);background:var(--hover)}
/* 读数占位是行内的一截：它坐在最终那枚 `b`、`strong`、说明行里，行高仍由那枚元素自己给，
   数字一到这一行不长不缩。 */
.skeleton-text{display:inline-block;vertical-align:middle;height:1em;width:12em;max-width:70%}
/* 整理卡：来源下拉按「本地」那一档的宽度画，三颗预设就是最终那种次级键，库名和预设名要等数据。 */
.organize-source-skeleton{width:100px}
.organize-preset-skeleton>.skeleton-text{width:4.5em;max-width:none}
.organize-input-skeleton{display:flex;align-items:center}
.skeleton-cards>div{grid-template-columns:repeat(auto-fill,minmax(min(260px,100%),1fr));gap:14px}
.skeletoncard{display:grid;gap:10px;min-width:0}
.skeletoncard i,.skeletoncard b,.skeletoncard em{display:block;
  border-radius:var(--control-radius);background:var(--hover)}
.skeletoncard i{aspect-ratio:16/9}.skeletoncard b{width:72%;height:18px}.skeletoncard em{width:46%;height:14px}
/* 头像格与标签格只有海报网格用得上，行政界面的骨架照 fieldset 排，多出来的两格
   会在一列长条中间凭空插两块灰。默认关掉，由用得上的那张骨架自己打开。 */
.skeletoncard s,.skeletoncard u{display:none}
.grid>.skeletonpanel{grid-column:1/-1;width:100%;min-width:0}
/* 海报卡骨架照 .card 的实测轮廓画：列宽、列距和行距直接沿用 .grid 的算式，
   卡内每一格的高度都由真实卡片同一枚 token 算出——标题是 .meta .t 的 2.7em、
   归属行是 .mono 的一行 1.45em、标签行再加上 .tg 的内边距与描边。这样一行骨架
   和一行真卡等高，数据到位那一下下面的内容不会往上跳。
   封面与下面那组之间是 8px：行距 3px 由 .mtext 的行距定，剩下 5px 记在头像格和
   标题格的上边距里，两格一起加才不会把头像顶偏。
   首页和关注页是同一种卡，所以共用这一块；关注页只差归属行高一点，见下面那条。 */
.postercard-skeleton>div{grid-template-columns:repeat(auto-fill,minmax(var(--tile),1fr));gap:24px 16px}
.postercard-skeleton .skeletoncard{grid-template-columns:38px minmax(0,1fr);column-gap:10px;row-gap:3px;
  align-content:start}
/* 封面格的圆角与真卡的 `.pic` 同一枚 token，到货那一下角不变形。 */
.postercard-skeleton .skeletoncard i{grid-column:1/-1;border-radius:var(--surface-radius)}
.postercard-skeleton .skeletoncard s{display:block;grid-column:1;grid-row:2/span 3;align-self:start;
  width:38px;height:38px;margin-top:5px;border-radius:50%;background:var(--hover)}
.catalog-placeholder{pointer-events:none}
.catalog-placeholder .ring,.catalog-placeholder .nm,.catalog-placeholder .mk,.catalog-placeholder .placeholder-name,.catalog-placeholder .pill{background:var(--hover)}
.catalog-placeholder .nm{width:60%;margin-inline:auto;border-radius:var(--badge-radius)}
.catalog-placeholder .placeholder-name{width:48px;border-radius:var(--badge-radius)}
.tier.catalog-placeholder{overflow:hidden}
.placeholder-tags{display:inline-flex;gap:8px;flex:1 0 240px;min-width:240px;overflow:hidden}
.placeholder-tags .pill{width:64px;border-color:transparent}
.postercard-skeleton .skeletoncard b,
.postercard-skeleton .skeletoncard em,
.postercard-skeleton .skeletoncard u{grid-column:2}
.postercard-skeleton .skeletoncard b{width:100%;margin-top:5px;font-size:var(--fs-md);height:2.7em}
.postercard-skeleton .skeletoncard em{width:58%;font-size:var(--fs-xs);height:1.45em}
.postercard-skeleton .skeletoncard u{display:block;width:64%;font-size:var(--fs-xs);
  height:calc(1.45em + var(--tag-pad-y) * 2 + 2px);
  border-radius:var(--tag-radius);background:var(--hover)}
/* 关注卡和首页卡差两处，一页十来行叠起来就是一屏的错位：归属行有 min-height，
   高 3.6px；末尾还有一条报状态的 aria-live 行，本身没高度，却照样占掉一格 8px 的
   卡内间距。 */
.follow-content-skeleton .skeletoncard em{height:21px}
.follow-content-skeleton .skeletoncard{padding-bottom:8px}
/* 数据管理是一列 fieldset，不是海报网格：骨架照 fieldset 的轮廓排，
   否则整页刷新先看到三列 16:9 占位，加载完又整个换成一列长条。
   底部那条是操作条的位置，不参与微光——它是框体，不是待填的内容。 */
.cleanup-skeleton>div{grid-template-columns:minmax(0,1fr);gap:16px}
.cleanup-skeleton .skeletoncard{gap:0;overflow:hidden;border:1px solid var(--line-soft);
  border-radius:var(--floating-radius);background:var(--ground)}
.cleanup-skeleton .skeletoncard i{aspect-ratio:auto;width:38%;height:20px;margin:20px 20px 0}
.cleanup-skeleton .skeletoncard b{width:26%;height:16px;margin:12px 20px 20px}
.cleanup-skeleton .skeletoncard em{width:100%;height:var(--fieldset-bar-h);margin:0;
  border-radius:0;border-top:1px solid var(--border-10);background:var(--overlay-5)}
/* 人工复核是左边一列分类、右边工具条加一格一格等高的卡片。骨架直接用最终容器
   那几个类名（`.review-workspace`、`.reviewcontrols`、`.reviewbulktoolbar`、`.reviewlist`），
   分栏、列宽和间距就都由页面自己那套规则给，这里只补占位件的尺寸——版面从占位到到货
   一格不挪，靠的是两边根本是同一套规则，不是两处抄同样的数字。
   分类名是静态文案，直接显示；等的是每类多少条，所以只有计数是占位。
   框体（工具条、操作条）不参与微光，它不是待填的内容。 */
.reviewcountskeleton{flex:none;width:28px;height:14px}
.reviewtoolskeleton{width:120px;height:36px}
.review-skeleton .reviewtabs button{color:var(--muted)}
.review-skeleton .skeletoncard{display:grid;gap:16px;box-sizing:border-box;height:var(--review-card-height);
  grid-template-rows:auto minmax(0,1fr) auto;overflow:hidden;padding:20px;
  border:1px solid var(--line-soft);border-radius:var(--surface-radius);background:var(--surface)}
.review-skeleton .skeletoncard i{aspect-ratio:auto;width:56%;height:22px;margin:0}
.review-skeleton .skeletoncard b{width:auto;height:auto;margin:0}
.review-skeleton .skeletoncard em{width:100%;height:36px;margin:0;border-radius:var(--control-radius);background:var(--overlay-5)}
/* 关注管理是一列三块（添加关注、关注列表、凭据）的行政界面：骨架照 .fsec 的轮廓画，
   一块对一块、宽度跟着 .followmanage 一起限到 812px。头部条是框体不是待填内容，
   跟 .fsechead 同高同底色，不参与微光。 */
.followmanage-skeleton>div{grid-template-columns:minmax(0,1fr);gap:16px;width:min(812px,100%);
  margin-left:auto;margin-right:auto;align-items:start}
.followmanage-skeleton .skeletoncard{gap:0;overflow:hidden;border:1px solid var(--border-10);
  border-radius:var(--surface-radius);background:var(--ground)}
.followmanage-skeleton .skeletoncard i{aspect-ratio:auto;height:56px;border-radius:0;
  background:var(--overlay-5);border-bottom:1px solid var(--border-10)}
.followmanage-skeleton .skeletoncard b{width:auto;margin:16px 16px 0}
.followmanage-skeleton .skeletoncard em{width:42%;height:14px;margin:12px 16px 16px}
.followmanage-skeleton .skeletoncard:nth-child(1) b{height:38px}
.followmanage-skeleton .skeletoncard:nth-child(2) b{height:300px}
.followmanage-skeleton .skeletoncard:nth-child(3) b{height:180px}
/* 塌成一列时要压掉上面那三条显式定位，特异性必须与它们持平，所以仍写 nth-child。 */
@media (max-width:1080px){.followmanage-skeleton>div{grid-template-columns:minmax(0,1fr)}
  .followmanage-skeleton .skeletoncard:nth-child(n){grid-area:auto}}
/* 关注列表骨架的容器与按键都是 React 那侧的类名（见 `frontend/src/board-skeleton.ts`），这里只补
   一件 Tailwind 表达不了的事：工具行窄于 740px 时带字的键收成纯图标。React 按工具行自己的宽度量，
   骨架拿关注列表那张卡做容器，它的内容宽度就是工具行的宽度，按同一条线收。 */
.follow-skeleton-surface{container:follow-list/inline-size}
@container follow-list (max-width:739.98px){.follow-skeleton-toolbar [data-compact-label]{display:none}}
.skeleton-dashboard>div{gap:16px}
/* 四格与真的指标带同宽同高：下限 140px、单格 96px、内边距 14/16、间隙 4px 都取自
   .metricstrip / .tastesummaries，手机上同样露边滚动。 */
.skeletondashstrip{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(140px,1fr);overflow-x:auto;
  border:1px solid var(--line-soft);border-radius:var(--floating-radius);background:var(--ground)}
.skeletondashstrip>span{box-sizing:border-box;min-height:96px;min-width:0;display:grid;align-content:center;
  gap:4px;padding:14px 16px;border-right:1px solid var(--line-soft)}
.skeletondashstrip>span:last-child{border-right:0}
.skeletondashstrip i,.skeletondashstrip b,.skeletondashstrip em{display:block;background:var(--hover);
  border-radius:var(--control-radius)}
.skeletondashstrip i{width:52%;height:17px}.skeletondashstrip b{width:68%;height:28px}
.skeletondashstrip em{width:60%;height:17px}
@media(max-width:640px){.skeletondashstrip{grid-auto-columns:minmax(168px,82%)}}
.skeletondashhero,.skeletondashpanel{display:grid;min-width:0;overflow:hidden;border:1px solid var(--line-soft);
  border-radius:var(--floating-radius);background:var(--ground)}
.skeletondashhero{min-height:330px;grid-template-columns:minmax(260px,36%) minmax(0,1fr)}
.skeletondashhero i,.skeletondashhero b,.skeletondashpanel i,.skeletondashpanel b,.skeletondashpanel em{
  display:block;background:var(--hover)}
.skeletondashhero i{margin:28px 24px;border-radius:var(--control-radius)}.skeletondashhero b{margin:24px;border-radius:var(--control-radius)}
.skeletondashpanel{grid-template-columns:1fr 1fr;gap:12px;padding:16px}
.skeletondashpanel i{grid-column:1/-1;width:26%;height:18px;border-radius:var(--control-radius)}
.skeletondashpanel b,.skeletondashpanel em{height:84px;border-radius:var(--control-radius)}
@media(max-width:640px){.skeletondashhero{grid-template-columns:minmax(0,1fr)}.skeletondashhero i{min-height:180px}.skeletondashhero b{min-height:220px}.skeletondashpanel{grid-template-columns:1fr}}
/* 微光取代整块呼吸。2026-09-04 实测 vercel.com/geist/skeleton：Geist 不改元素自己的
   不透明度，而是在 `::after` 上铺一条比容器宽三倍的横向渐变，靠 `transform` 左右扫，
   容器 `overflow:hidden` 裁掉溢出。渐变只在 gray-100 与 gray-200 之间走（明度 10%
   与 12%），也就是说它靠移动被看见，不靠明暗跳变；原来的呼吸是整块 .55↔1 的透明度
   变化，一屏十几块同时喘，比内容本身还抢眼。节奏照抄实测值：1.5s ease-in-out
   infinite reverse，位移半个自身宽度。全局的 prefers-reduced-motion 规则会关掉它。 */
@keyframes skeleton-sweep{to{transform:translateX(-50%)}}
/* 本地读取在 180ms 内完成时不展示骨架；JS 到门槛后直接移除这一类，不依赖动效偏好。 */
.skeleton-awaiting{visibility:hidden}
.skeleton,.skeletoncard i,.skeletoncard b,.skeletoncard em,
.skeletondashhero i,.skeletondashhero b,
.skeletondashpanel i,.skeletondashpanel b,.skeletondashpanel em,
.skeletondashstrip i,.skeletondashstrip b,.skeletondashstrip em,
.skeletoncard s,.skeletoncard u,
.countskeleton,.avskeleton .ring,.avskeleton .nm,.brandskeleton,.tagskeleton,
body.refreshing #tagbar [data-tag],body.refreshing #tiers .av .ring,
body.refreshing #tiers .av .nm,body.refreshing #tiers .brandpill{position:relative;overflow:hidden}
.skeleton::after,.skeletoncard i::after,.skeletoncard b::after,.skeletoncard em::after,
.skeletondashhero i::after,.skeletondashhero b::after,
.skeletondashpanel i::after,.skeletondashpanel b::after,.skeletondashpanel em::after,
.skeletondashstrip i::after,.skeletondashstrip b::after,.skeletondashstrip em::after,
.skeletoncard s::after,.skeletoncard u::after,
.countskeleton::after,.avskeleton .ring::after,.avskeleton .nm::after,
.brandskeleton::after,.tagskeleton::after,
body.refreshing #tagbar [data-tag]::after,body.refreshing #tiers .av .ring::after,
body.refreshing #tiers .av .nm::after,
body.refreshing #tiers .brandpill::after{content:'';position:absolute;inset:0;right:-200%;
  visibility:visible;border-radius:inherit;
  background:linear-gradient(to right,var(--hover),var(--skeleton-sheen) 50%,var(--hover));
  background-size:50% 100%;
  animation:skeleton-sweep 1.5s ease-in-out infinite reverse}
/* 框体不是待填的内容，本来就不参与呼吸，也不参与微光。 */
.cleanup-skeleton .skeletoncard em::after,
.review-skeleton .skeletoncard em::after,
.followmanage-skeleton .skeletoncard i::after{content:none}
/* 封面与头像还在路上：框上铺同一条微光，到手后淡出并糊掉，和换头像那一格
   （`reveal-skeleton`）同一形态。类名由 app.js 的 `watchPendingImages` 标、`settleImage`
   收。微光晚 180ms 才淡入，门槛与 `.skeleton-awaiting` 同一个数：本地图多半在这之前
   就到了，那种一帧都不该看见它。底色垫 `--ground`，否则半透明的微光落在 `.pic` 的
   黑底上只剩一片黑。收尾后转 `visibility:hidden`，淡出完就不再占合成层。 */
.imgwait::after,.imgdone::after{content:'';position:absolute;inset:0;right:-200%;z-index:5;
  pointer-events:none;border-radius:inherit;
  background:linear-gradient(to right,var(--hover),var(--skeleton-sheen) 50%,var(--hover)) 0 0/50% 100%,
    var(--ground);
  opacity:0;filter:blur(2px);visibility:hidden;
  transition:opacity var(--motion-reveal),filter var(--motion-reveal),visibility 0s var(--motion-reveal)}
.imgwait::after{opacity:1;filter:none;visibility:visible;transition-delay:180ms;
  animation:skeleton-sweep 1.5s ease-in-out infinite reverse}
@starting-style{.imgwait::after{opacity:0;filter:blur(2px)}}
/* 首页刷新那一层微光也画在头像圈的 `::after` 上；已经收尾的圈别把它一起藏掉。 */
body.refreshing #tiers .av .ring.imgdone::after{opacity:1;filter:none;visibility:visible}


/* 计数骨架的宽高定死：跟着真实文本走的话，数字回来那一刻整行会横向弹一下。
   14px 远低于筛选条按钮的 32px，而 `.count` 有 min-height 兜底，所以换成真数字
   不改变行高。`视频 · N` 用的是同一枚。 */
.countskeleton{display:inline-block;width:150px;height:14px;border-radius:var(--control-radius);
  background:var(--hover)}
.entitycollectionhead h3 .countskeleton{width:96px}
/* 索引页这一枚等的是「118 项」，四五个字。150px 是照筛选条那句长读数定的，摆到
   标题右边就是一条明显长过答案的灰条，看着像还有一整句没回来。 */
.index .ihead .countskeleton{width:54px}

/* 顶部三层与标签条的首屏骨架。形状照 Geist 的判据选：头像走 pill（圆），
   胶囊走 rounded。几何不另算一套，直接套 .av/.ring/.nm、.brandpill、.pill 本身，
   这样骨架的宽高就是最终内容的宽高，数据到位那一下不可能位移。 */
.avskeleton{display:block}
.avskeleton .ring,.avskeleton .nm{background:var(--hover)}
.avskeleton .nm{width:52px;margin:0 auto;border-radius:var(--control-radius);color:transparent}
.brandskeleton{background:var(--hover);border-color:transparent}
.brandskeleton .mk{background:transparent}
.tagskeleton{background:var(--hover);border-color:transparent;padding:0}
/* 顶部三层第一次填内容之前只有 18px 的内边距，内容到位后长到一百多，
   每次冷启动都把整页往下推一截；标签条有固定高度不塌，但空着一整条。 */
.tiers[aria-busy="true"],.tagbar[aria-busy="true"]{pointer-events:none}
/* 占位不是控件：整块骨架和每一枚占位都不接指针，套在最终形状上的 hover 描边、底色和
   手型光标也就一条都触发不了，点下去落到后面的页面上。占位直接借用 `.av`、`.pill` 这些
   最终类名画形状，所以只能在这里一处关掉，不能逐条给 hover 规则补 `:not()`。
   分层骨架只认每一枚占位：`data-skeleton-tier` 那一排数据到了还留在原地装真头像。
   等数据的键（`data-skeleton-action`）是例外，它要亮出 not-allowed 光标说明「现在按不了」。 */
:is([data-skeleton],.avskeleton,.brandskeleton),:is([data-skeleton],.avskeleton,.brandskeleton) *,.tagskeleton,.countskeleton,.skeletoncard,.skeleton{pointer-events:none}
[data-skeleton-action]:disabled{pointer-events:auto}

/* 换一批时标签条走 Geist 的 wrap-children 变体：真实胶囊留在 DOM 里，用 visibility
   藏起来，微光画在 ::after 上并单独 visible。框就是胶囊自己的框，所以零位移；
   visibility:hidden 的元素同时不可聚焦，正好满足规范里「加载期间不要把可聚焦控件
   放进骨架」。只盖会变的标签胶囊——四枚视图胶囊由 state 决定，这次请求不改它们。 */
body.refreshing #tagbar [data-tag]{visibility:hidden;background:var(--hover);border-color:transparent}
/* 头像和厂牌跟标签条同一套：换一批换的是这三层的成员，所以三层一起盖。头像那格
   分成圆片和名字条两块，尺寸取首屏骨架的同一组值（64 圆 + 52 名条），两条通道
   看起来就是同一枚；`.av` 宽度写死 76px，名条居中收窄不动周围任何东西。 */
body.refreshing #tiers .av .ring,body.refreshing #tiers .av .nm,
body.refreshing #tiers .brandpill{visibility:hidden;background:var(--hover)}
body.refreshing #tiers .av .nm{width:52px;margin:0 auto;border-radius:var(--control-radius)}
body.refreshing #tiers .brandpill{border-color:transparent}

.stats-lede-skeleton{display:block;width:min(380px,75%);height:22px;margin-bottom:16px}
.config-skeleton-card .skeleton{display:block;height:20px;width:60%}
.config-skeleton-card .geist-fieldset-content>.skeleton:first-child{height:24px;width:35%}
.config-skeleton-card .geist-fieldset-content>.skeleton:last-child{height:var(--control-h);width:100%}
.config-skeleton-card .geist-fieldset-footer>.skeleton{width:100px;height:var(--control-h);margin:0}
/* 缩放滑杆：原生 accent-color 的轨道太粗，自绘成与时长双滑块同一套 4px 细轨。
   底色是视频上的白色，所以这一套固定用白色而不是主题色。 */
.photozoom input{-webkit-appearance:none;appearance:none;width:116px;height:20px;background:transparent;cursor:pointer}
.photozoom input::-webkit-slider-runnable-track{height:4px;border-radius:var(--pill-radius);background:rgba(255,255,255,.28)}
.photozoom input::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;margin-top:-5px;border:0;border-radius:50%;background:#fff;transition:box-shadow .12s ease}
.photozoom input:hover::-webkit-slider-thumb{box-shadow:0 0 0 4px rgba(255,255,255,.22)}
.photozoom input:focus-visible{outline:none}
.photozoom input:focus-visible::-webkit-slider-thumb{outline:2px solid #fff;outline-offset:2px}
.photozoom input::-moz-range-track{height:4px;border-radius:var(--pill-radius);background:rgba(255,255,255,.28)}
.photozoom input::-moz-range-thumb{width:14px;height:14px;border:0;border-radius:50%;background:#fff}
.photozoom b{font-weight:400;font-size:var(--fs-xs);width:38px;text-align:right}
.photodetail{position:absolute;z-index:5;left:14px;bottom:72px;width:min(360px,calc(100vw - 28px));
  box-sizing:border-box;display:grid;align-items:start;gap:14px;padding:16px;
  border:1px solid rgba(255,255,255,.16);border-radius:var(--floating-radius);background:rgba(18,18,20,.96);
  color:#fff;box-shadow:0 18px 48px rgba(0,0,0,.45),0 2px 8px rgba(0,0,0,.32);backdrop-filter:blur(18px)}
.photodetail[hidden]{display:none}
.photodetailcopy{min-width:0;display:grid;gap:4px}
.photodetailcopy h2{overflow:hidden;white-space:nowrap;margin:0;font-size:var(--fs-sm)}
.photodetailmeta{color:rgba(255,255,255,.62);font-size:var(--fs-xs)}
.photodetail>button{justify-self:start;height:32px;display:flex;align-items:center;gap:7px;border:1px solid rgba(255,255,255,.18);
  border-radius:var(--control-radius);padding:0 10px;background:transparent;color:#fff;cursor:pointer}
.photodetail>button:hover{background:rgba(255,255,255,.12)}
.photodetail>button[hidden]{display:none}
.photodetail>button:disabled{cursor:not-allowed;opacity:.68}
.photodetail>button svg{width:16px;height:16px;display:block;stroke:currentColor;fill:none;stroke-width:2}
.photodetail .srcstate{color:rgba(255,255,255,.62)}
.photodetail .srcstate:empty{display:none}
.photolight.has-strip .photodetail{bottom:156px}
.photolight .photostrip{position:absolute;z-index:3;left:72px;right:72px;bottom:14px;width:auto;height:72px;padding:0}
.photolight:not(.has-strip) .photostrip{display:none}
.photolight .photostrip .swiper-slide{width:auto;height:72px;opacity:.5;cursor:pointer;border-radius:var(--control-radius);overflow:hidden}
.photolight .photostrip .swiper-slide img{height:100%;width:auto;display:block;object-fit:cover}
.photolight .photostrip .swiper-slide-thumb-active{opacity:1;box-shadow:0 0 0 2px #fff inset}
/* 窄屏断点写在这一段之后：和上面同优先级，写在前面会被原样盖掉。 */
@media (max-width:640px){
  .photowall{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
  .photowall[data-size="big"]{grid-template-columns:repeat(1,minmax(0,1fr))}
  .photozoom input{width:72px}.photozoom b{display:none}
  .photolight .photomain .swiper-zoom-container{padding:12px 12px 70px}
  .photolight.has-strip .photomain .swiper-zoom-container{padding-bottom:136px}
  .photobar{left:12px;right:12px;bottom:10px;min-height:44px}
  .photolight.has-strip .photobar{bottom:80px}
  .photodetailtoggle{width:40px;height:40px}
  .photodetail{left:12px;bottom:62px;width:calc(100vw - 24px)}
  .photolight.has-strip .photodetail{bottom:132px}.photodetail>button{min-height:44px}
  .photolight .photostrip{left:56px;right:56px;bottom:12px}
  .photolight .photostrip,.photolight .photostrip .swiper-slide{height:56px}
  .photoclose{top:10px;right:10px}.photonav{width:44px;height:44px}.photonav.back{left:8px}.photonav.fwd{right:8px}}
/* 显式 display 会盖掉浏览器默认的 [hidden]{display:none}：按钮已经 hidden=true
   却仍然画出来，而 requestMore 首行就是 if(more.hidden)return，于是点了没反应。 */
/* 身份区按类别分组：标签是组标题，同类横向排开并按需换行。
   旧版每个名字独占一行、左侧还重复一列标签，共演作品会把整个侧栏撑满。 */
.detailidentity{display:flex;flex-direction:column;gap:14px;margin:0 0 18px}
.identityprimary{display:flex;flex-wrap:wrap;gap:14px 26px;align-items:flex-start}
.identityprimary>.idgroup{width:max-content;max-width:100%}
.idgroup{min-width:0}
.idlabel{margin:0 0 7px;
  font-size:var(--fs-xs);font-weight:400;color:var(--muted);letter-spacing:.04em}
.idrow{display:flex;flex-wrap:wrap;gap:10px 12px;align-items:flex-start}
/* 组标题、头像和名字共用左边缘；详情栏里的所有身份信息都从内容区起点对齐。 */
.idgroup{--id-cell:62px;--id-face:46px}
.idgroup-performer{--id-cell:58px}
.idgroup-performer .idrow{gap:10px}
.idcell{display:flex;flex-direction:column;align-items:flex-start;gap:5px;width:var(--id-cell,62px);
  border:0;background:transparent;color:inherit;padding:0;cursor:pointer}
.idcell[hidden]{display:none}
.idface{position:relative;width:var(--id-face,46px);height:var(--id-face,46px);border-radius:50%;overflow:hidden;
  background:var(--sunk);color:var(--muted);display:grid;place-items:center;font-size:var(--fs-md)}
/* `#fff` 与首字母只在取不到图时露出来：厂牌标识文件是自带边距的不透明方图，
   `.idface img` 那条 cover 铺满整格，不再另给 logo 格加 padding 和 contain。 */
.idcell.logo .idface{border-radius:var(--surface-radius);background:#fff;color:#555}
.idface img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
.idface svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2}
/* 行高必须容得下下伸部。1.25 在 12px 下只有 15px 行框，而这里同时开着 overflow:hidden
   截省略号，于是「Prestige」的 g 尾巴被切掉——用户实测看到的就是这个。 */
.idname{font-size:var(--fs-xs);line-height:1.5;text-align:left;width:100%;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ink-2)}
.idgroup-performer .idcell{align-items:center}
.idgroup-performer .idname{text-align:center}
.idcell:not(.entitylink):not(.unownedlink){cursor:default}
.idcell.entitylink:hover .idface,.idcell.unownedlink:hover .idface{box-shadow:0 0 0 2px var(--ink-2)}
.idcell.entitylink:hover .idname,.idcell.unownedlink:hover .idname{color:var(--ink)}
.castmore{align-self:center;height:46px;padding:0 12px;border:1px dashed var(--overlay-10);
  border-radius:var(--floating-radius);background:transparent;color:var(--ink-2);font-size:var(--fs-xs);cursor:pointer}
.castmore:hover{background:var(--hover)}
.seriesrows{display:grid;gap:7px}
.serieslink,.serieslink.entitylink{display:flex;width:100%;align-items:flex-start;gap:7px;padding:0;border:0;
  background:transparent;color:var(--ink-2);font-size:var(--fs-sm);line-height:1.4;text-align:left;
  white-space:normal;overflow-wrap:anywhere;text-decoration:none}
.serieslink svg{width:16px;height:16px;margin-top:1px;flex:none;stroke:currentColor;fill:none;stroke-width:2}
button.serieslink{cursor:pointer}.serieslink:not(.entitylink){cursor:default}
button.serieslink.entitylink:hover{color:var(--tungsten);text-decoration:none}
/* 页面标题只有一套字形。原来管理区 26px、索引页 20px、播放列表 28px，
   在侧栏里一路点过去就是三种大小；间距由各自的版式决定，字形不再各拍各的。 */
/* 行高 1.25 取自 2026-09-04 实测的 Geist Heading 32（32px/40px/600）。字距那一档
   不照抄：Geist 的 -0.04em 是给拉丁字母调的，压在中文标题上会把字挤成一团。 */
.pagetitle,.listtitle,.managetitle,.index .ihead h2,.playlistpage h2{
  font-size:var(--fs-3xl);line-height:1.25;letter-spacing:-.01em;font-weight:600}
.listtitle,.managetitle,.follow>.pagetitle{margin:0 0 20px}
.managetitle:has(+.pagelede:not([hidden])){margin-bottom:8px}
.listtitle[hidden],.managetitle[hidden]{display:none}
/* 「批量保留」那条的几何与材质归 board.css 的 `body[data-surface="/duplicates"]` 一组：
   它是一块吸顶的玻璃，跟浏览页的筛选框同一份料。 */
.dupactions{display:flex;align-items:center;gap:8px;margin-bottom:16px}
/* 组头上这几颗是会动数据的批量操作（每组只留一个，其余进回收站），必须看着像按钮，
   不能是一段可点的文字。 */
.dupbtns button{height:32px;padding:0 16px;border-radius:var(--control-radius);
  border:1px solid var(--border-15);background:var(--surface);color:var(--ink);
  cursor:pointer;font-size:var(--fs-sm);line-height:1}
.dupbtns button:hover{background:var(--hover);color:var(--ink)}
.dupbtns button:disabled{opacity:.5;cursor:wait}
.dupgroup{border:1px solid var(--overlay-10);border-radius:var(--surface-radius);margin-bottom:12px;overflow:hidden}
.duphead{display:flex;align-items:center;gap:10px;padding:10px 12px;background:var(--overlay-5);flex-wrap:wrap}
.duphead .dupbtns{margin-left:auto;display:flex;gap:6px}
.dupflag{font-size:var(--fs-xs);color:var(--muted);border:1px solid var(--overlay-10);
  border-radius:var(--badge-radius);padding:2px 8px}
.dupflag.ok{color:var(--keep);border-color:color-mix(in srgb,var(--keep) 40%,transparent)}
.duprow{display:grid;grid-template-columns:96px minmax(0,1fr) 84px 84px 84px;gap:10px;
  align-items:center;padding:7px 12px;border-top:1px solid var(--overlay-5)}
.duprow .mono{color:var(--muted);font-size:var(--fs-xs)}
/* 所在库：图标在前、名字在后，图形跟侧栏媒体库菜单同一枚。 */
.duploc{display:inline-flex;align-items:center;gap:6px;min-width:0}
.duploc>svg{width:16px;height:16px;flex:none;fill:none;stroke:currentColor;stroke-width:2}
.duploc>span{white-space:nowrap}
.duppath{grid-column:2/-1;min-width:0;overflow:hidden;white-space:nowrap;
  padding-top:2px;color:var(--muted)}
/* 文件名和「最大」「最长」标记排在同一格：名字按自己的宽度排、装不下时中间截断，标记
   紧跟其后、不换行也不被截掉。可用宽度由 middle-truncate 按这一格量（data-middle-truncate-within）。 */
.duptitle{display:flex;align-items:center;gap:8px;min-width:0}
.dupname{flex:0 1 auto;min-width:0;overflow:hidden;white-space:nowrap;text-align:left;
  border:0;background:transparent;color:inherit;cursor:pointer;padding:0}
.dupname:hover{color:var(--ink-2)}
.dupmarks{display:flex;flex:none;gap:4px}
.dupmarks:empty{display:none}
.dupmarks i{font-style:normal;font-size:var(--fs-xs);padding:1px 6px;border-radius:var(--badge-radius)}
.dupmarks i.big{background:var(--hover);color:var(--ink-2)}
.dupmarks i.long{background:color-mix(in srgb,var(--keep) 22%,transparent);color:var(--keep)}
/* 高清版目标页的正文归 React 子树（ADR-0031）。这两条只服务遗留骨架的那几张空卡，
   列数、间距与卡片几何由 `board.css` 的 `body[data-surface="/quality-goals"]` 那组给。 */
.qualitylist{display:grid}
.qualityitem{min-width:0;background:var(--ground)}
.indexmore[hidden],.entitymore[hidden]{display:none}
.indexmore,.entitymore{display:flex;align-items:center;justify-content:center;margin:20px auto 0;min-width:124px;
  min-height:40px;padding:8px 16px;border:1px solid var(--border-15);border-radius:var(--control-radius);background:transparent;
  color:var(--ink-2);cursor:pointer}.indexmore:hover,.entitymore:hover{background:var(--hover);color:var(--ink)}
.indexmore:disabled,.entitymore:disabled{opacity:.5;cursor:wait}
/* 「未归属」和实体名一样是个入口，只是它指向一个集合而不是一个人，样式同源。 */
.entitylink,.unownedlink{border:0;background:none;padding:0;color:var(--tungsten);cursor:pointer;text-decoration:none}
.entitylink:hover,.unownedlink:hover{color:var(--ink);text-decoration:none}
/* Peach 的可点击身份只用颜色、头像描边和背景反馈，不使用链接下划线。 */
.idcell.entitylink:hover,.idcell.unownedlink:hover,.mav.entitylink:hover{text-decoration:none}
/* ── 主区：满屏，大图 ── */
main{position:relative;z-index:1;padding:14px 16px 90px}
/* 管理区二级导航：统计 / 垃圾文件 / 回收站 / 人工复核 共用一条 */
.managebar{display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin:0 0 16px;
  padding:6px;border:1px solid var(--line-soft);border-radius:var(--floating-radius);background:var(--ground);width:max-content;max-width:100%}
.managebar[hidden]{display:none}
.managebar .managebar-toggle{display:none}
.managebar-menu{display:flex;gap:6px;align-items:center;flex-wrap:wrap}
.managebar button{display:inline-flex;align-items:center;gap:7px;min-height:40px;border:0;border-radius:var(--control-radius);
  background:transparent;color:var(--muted);padding:0 14px;cursor:pointer;font-size:var(--fs-sm);white-space:nowrap}
.managebar button svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;
  stroke-linecap:round;stroke-linejoin:round}
.managebar button:hover{color:var(--ink)}
.managebar button[aria-pressed="true"]{background:var(--hover);color:var(--ink)}
.managebar{margin-left:auto;margin-right:auto}
.insight-layout .managetitle,.insight-layout .pagelede{width:min(1100px,100%);margin-left:auto;margin-right:auto}
.cleanup-layout .geist-breadcrumb,.cleanup-layout .managetitle,.cleanup-layout .pagelede,
.follow-manage-layout .managetitle,.follow-manage-layout .pagelede{width:min(812px,100%);margin-left:auto;margin-right:auto}
/* Geist Breadcrumbs（vercel.com/geist/breadcrumbs 实测）：nav > ol > li，
   上一级与分隔符 --muted、当前页 --ink 且带 aria-current，hover 同样升亮；
   分隔符是 16px chevron、钉在 --muted 上不随当前项提亮，最后一项隐藏；
   项与项、字与分隔符都留 6px。 */
.geist-breadcrumb{margin:0 0 6px}
.geist-breadcrumb[hidden]{display:none}
.geist-breadcrumb ol{display:flex;align-items:center;gap:6px;margin:0;padding:0;list-style:none}
.geist-breadcrumb li{display:flex;align-items:center;gap:6px;font-size:var(--fs-md);line-height:20px;color:var(--muted);transition:color .2s ease}
.geist-breadcrumb li[aria-current]{color:var(--ink)}
.geist-breadcrumb a{color:inherit;text-decoration:none}
.geist-breadcrumb li:not([aria-current]):hover{color:var(--ink)}
.geist-breadcrumb li svg{width:16px;height:16px;flex:none;stroke:var(--muted);fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.geist-breadcrumb li:last-child svg{display:none}
.count{position:sticky;top:calc(var(--topH) + var(--filterH));z-index:60;
  min-height:var(--sortH);color:var(--ink-2);margin:0 -16px 16px;padding:8px 16px;
  display:flex;align-items:center;gap:10px;flex-wrap:nowrap;
  background:transparent;border-bottom:1px solid transparent}
/* 管理区隐藏了 tagbar，count 吸顶若仍按「顶栏+筛选条」留位会悬空一条缝。
   回收站这类页面直接贴着顶栏吸。manage-static（垃圾复核）不吸顶，要压过这条。 */
.count.no-tagbar{top:var(--topH)}
.count.manage-static{position:relative;top:auto;z-index:1}
.count.count-actions-only{position:relative;top:auto;min-height:0;margin:0 0 16px;padding:0;justify-content:flex-end}
.count.count-actions-only:empty{display:none}
.resourcesync{scroll-margin-top:calc(var(--topH) + 18px);display:grid;gap:16px}.resourcesync>h2{margin:0;font-size:var(--fs-xl)}
.resourcesyncbox,.resourcepanel{overflow:clip;border:1px solid var(--field-ring);border-radius:var(--floating-radius);background:var(--ground)}.resourcesyncbody{padding:20px}.resourcesyncbody p{max-width:760px;margin:0;color:var(--muted);font-size:var(--fs-sm);line-height:1.55}.resourcesyncfooter,.resourceapplyrow{box-sizing:border-box;min-height:var(--fieldset-bar-h);display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:12px 20px;border-top:1px solid var(--line-soft);background:var(--overlay-5)}.resourcesyncfooter p,.resourceapplyrow>p{margin:0;color:var(--muted);font-size:var(--fs-xs);line-height:1.5}.resourceaction{box-sizing:border-box;height:36px;padding:0 12px;border:0;border-radius:var(--control-radius);box-shadow:0 0 0 1px var(--line-soft);background:var(--ground);color:var(--ink-2);display:inline-flex;align-items:center;justify-content:center;gap:8px;flex:none;font:inherit;font-size:var(--fs-sm);font-weight:500;cursor:pointer;transition:background-color .12s,color .12s}.resourceaction:hover:not(:disabled){background:color-mix(in srgb,var(--ink) 8%,var(--ground));color:var(--ink)}.resourceaction:focus-visible{outline:2px solid var(--tungsten);outline-offset:2px}.resourceaction:disabled{background:var(--sunk);color:var(--muted);cursor:not-allowed}.resourceaction svg{width:16px;height:16px;stroke:currentColor;fill:none}#linkCheckResult:empty,#resourceSyncResult:empty{display:none}.resourcescanning{margin:0;padding:14px 16px;border:1px solid var(--line-soft);border-radius:var(--control-radius);color:var(--muted)}/* 扫描中的环形进度和加载点同一只框：链接检查的进度住在 resourcepanel 里有边，
   这条独居在结果槽里，自己带框才和页面其他进度一个形状。 */
#resourceSyncResult>.board-job-progress{margin:0;padding:14px 16px;border:1px solid var(--line-soft);border-radius:var(--control-radius)}.resourceapplyrow .resourcesyncok{color:var(--success)}.resourcesyncok{margin:0;color:var(--success)}
.tagbar,.count,.entitytagbar,.entitycollectionhead{transition:background-color .16s ease,border-color .16s ease}
/* 吸顶那条是底色的半透明版，配方跟着 --ground 走：写死深色底会在浅色主题下
   变成一条黑带压在白底内容上。 */
.tagbar.is-stuck,.count.is-stuck,.entitytagbar.is-stuck,.entitycollectionhead.is-stuck{background:color-mix(in srgb,var(--ground) 84%,transparent);
  border-bottom-color:var(--line-soft);backdrop-filter:saturate(1.35) blur(16px);
  -webkit-backdrop-filter:saturate(1.35) blur(16px)}
.count:empty{display:none}
.count>span:first-child{flex:none}
/* 等数据时换批键自己画：先画完第一条线，再从左到右画第二条，停住让图案看清，两条一起
   淡出，下一圈立刻起笔。淡出结束正好落在一圈的末尾，所以中间没有一帧是空的。让它整圈
   旋转的话，这枚两条交叉箭头的图标指向会一直变，读起来像图标画错了而不是像在忙。
   动效写在 #i-shuffle 自己的两条线上，忙态只负责把两个自定义属性传下去：`<use>` 的影子树
   里选不到外层那些忙态标记，但继承来的自定义属性进得去。它是这一行唯一还在动的东西，
   其余部分必须保持静止。全局的 prefers-reduced-motion 规则会关掉它，图标停在画满的样子。 */
@keyframes peach-strand-draw{
  0%{stroke-dashoffset:100;opacity:1}
  34%,86%{stroke-dashoffset:0;opacity:1}
  100%{stroke-dashoffset:0;opacity:0}}
@keyframes peach-strand-draw-late{
  0%,36%{stroke-dashoffset:100;opacity:1}
  70%,86%{stroke-dashoffset:0;opacity:1}
  100%{stroke-dashoffset:0;opacity:0}}
#i-shuffle .strand{stroke-dasharray:var(--strand-dash,none);
  animation:peach-strand-draw var(--strand-cycle,0s) linear infinite}
#i-shuffle .strand-b{animation-name:peach-strand-draw-late}
.count[aria-busy="true"] :is(#batchAction,#followShuffle) svg,
body.refreshing #batchAction svg,
.entitycollectionhead[aria-busy="true"] .entitybatch svg{
  --strand-dash:100;--strand-cycle:2.2s}
/* 数据管理里两枚「扫一遍」的键各自适配忙态，不统一转圈：检查文件的 git-compare 照
   lucide-animated 的 GitCompare 逐笔描画（右下圆→两条枝→左上圆，画完停住再一起淡出），
   扫描空文件夹的 scan-search 四角按左上右下／右上左下两拍轮流亮，放大镜保持不动。
   机制同换批键：动效写在 symbol 自己的零件上，忙态把动画名和周期传下去；空闲时名字是
   none，什么都不动。全局的 prefers-reduced-motion 规则会关掉它们。 */
@keyframes peach-gc-draw-a{
  0%{stroke-dashoffset:100;opacity:1}
  17%,86%{stroke-dashoffset:0;opacity:1}
  100%{stroke-dashoffset:0;opacity:0}}
@keyframes peach-gc-draw-b{
  0%,25%{stroke-dashoffset:100;opacity:1}
  42%,86%{stroke-dashoffset:0;opacity:1}
  100%{stroke-dashoffset:0;opacity:0}}
@keyframes peach-gc-draw-c{
  0%,50%{stroke-dashoffset:100;opacity:1}
  67%,86%{stroke-dashoffset:0;opacity:1}
  100%{stroke-dashoffset:0;opacity:0}}
@keyframes peach-scan-corner-a{
  0%,40%{opacity:1}
  50%,90%{opacity:.25}
  100%{opacity:1}}
@keyframes peach-scan-corner-b{
  0%,40%{opacity:.25}
  50%,90%{opacity:1}
  100%{opacity:.25}}
#i-git-compare .gc-part{stroke-dasharray:var(--gc-dash,none);
  animation-name:var(--gc-anim,none);animation-duration:var(--gc-cycle,1.8s);
  animation-timing-function:linear;animation-iteration-count:infinite}
#i-git-compare .gc-line-a,#i-git-compare .gc-line-b{animation-name:var(--gc-anim-b,none)}
#i-git-compare .gc-end-b{animation-name:var(--gc-anim-c,none)}
#i-scan-search .scan-corner{animation-name:var(--scan-anim,none);
  animation-duration:var(--scan-cycle,1.2s);animation-timing-function:ease-in-out;animation-iteration-count:infinite}
#i-scan-search .scan-corner-b{animation-name:var(--scan-anim-b,none)}
#resourceScan[aria-busy="true"] svg,[data-cleanup-empty-scan][aria-busy="true"] svg{
  --gc-dash:100;--gc-anim:peach-gc-draw-a;--gc-anim-b:peach-gc-draw-b;--gc-anim-c:peach-gc-draw-c;
  --scan-anim:peach-scan-corner-a;--scan-anim-b:peach-scan-corner-b}
.count .sorts,.entitycollectionhead .sorts{display:flex;gap:5px;margin-left:auto;flex-wrap:nowrap;
  overflow-x:auto;scrollbar-width:none;min-width:0}
.count .sorts::-webkit-scrollbar,.entitycollectionhead .sorts::-webkit-scrollbar{display:none}
.count .sorts button,.entitycollectionhead .sorts button{min-height:32px;padding:0 11px;display:inline-flex;align-items:center;
  border:1px solid var(--border-15);border-radius:var(--control-radius);
  background:transparent;cursor:pointer;font-size:var(--fs-xs);color:var(--muted);
  flex:0 0 auto;white-space:nowrap}
.count .sorts button:hover,.entitycollectionhead .sorts button:hover{color:var(--ink)}
.count .sorts button[aria-pressed="true"],.entitycollectionhead .sorts button[aria-pressed="true"]{background:var(--picked);color:var(--ink)}
/* 方向箭头只画在选中的那一枚里，跟着它的文字色走。自带 class 而不是选 `button>svg`：
   同一排里的批量键是 36px 方形图标键，那条规则会给它的图标也加上左边距。 */
.sortdir{width:14px;height:14px;flex:none;margin-left:5px;stroke:currentColor;fill:none;stroke-width:2;
  stroke-linecap:round;stroke-linejoin:round}
/* 这一排里的批量键是强调档，面色连同悬停归 `board.css` 一处：那一层按这条工具带的形状
   重排过它们，在这里再写一份，算出来生效的仍是那一层。 */
/* 形状按类写，不按 id：这一排在首页、资料页和关注页各有一枚同样的换一批键，挂在 id 上
   的话每多一页就要在这里补一个选择器，而漏掉的那一页不是报错——是图标算出 0×0，
   按钮变成一块纯色方块。 */
.count .sorts .batchaction,.entitycollectionhead .sorts .batchaction{font-weight:600;width:36px;padding:0;display:inline-grid;place-items:center}
.count .sorts .batchaction svg,.entitycollectionhead .sorts .batchaction svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2}
/* 「仅显示图片」是一枚 30px 见方的图标开关，跟旁边版式分段器的每一枚选项同一副身量；
   开着时垫的玻璃由筛选条那块滑动玻璃给（GLIDE_ROWS.imagesonly），这里只管形状。那块
   玻璃挂在这枚键自己身上，键是它的定位基准，字形要压在它上面，跟媒体那几枚圆键同一条。 */
.count .sorts .followimagesonly{width:30px;padding:0;display:inline-grid;place-items:center;position:relative}
.count .sorts .followimagesonly svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;position:relative;z-index:1}
/* 目录列表是一叠卡：连续的一段视频合成一个 `.grid`，竖屏带插进来时把那一段从行边界
   剪开，上下两半于是各自成为一张独立的卡。`#grid` 本身只是这叠卡的容器，不再是网格。 */
.gridstack{display:flex;flex-direction:column;gap:16px}
/* 列距 16px 是给悬停面留的：卡片悬停时底色往盒外多铺 8px（见下方 `:hover`），列距再窄，
   两张卡的悬停面就顶到邻卡上。关注页的视频列表 `.followlist` 用同一副间距。 */
.grid,.followlist:not(.followphotowall){gap:24px 16px}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--tile),1fr))}
.card{cursor:pointer;display:flex;flex-direction:column;gap:8px;min-width:0;border-radius:var(--surface-radius);
  position:relative;transition:transform .18s ease,filter .18s ease,background-color .15s ease,box-shadow .15s ease;transform-origin:center}
/* 馆藏、资料页作品和关注网格里，视口外的卡片跳过渲染：滚动和侧栏开合时要算的只剩屏上那几十张。
   资料页（女优、厂牌、创作者、系列、事务所五种共用一处）的作品区是 `.entitysection>.grid`，
   卡片出自首页同一个 `cardHtml()`、同一个 `--tile` 列宽和 JAV 版式开关，下面的估计原样成立；
   标签页点进去落在首页 `#grid`，回收站也是，不另列。
   跳过的是卡里的两块，封面 `.pic` 和元信息区 `.meta`；卡片盒本身照常排版，所以屏外卡的
   高度里只有元信息区一项是估计值。
   - 封面高度由 `aspect-ratio` 从列宽推出，跳过渲染时照样成立，列宽随视口和侧栏开合怎么
     变都与真实渲染一致。卡高随列宽在 270～590px 之间变（JAV 大图一档最高），整张卡
     跳过就只能给一个固定占位，对不上所有版式，没渲染过的卡越多滚动条漂得越远。
   - 元信息区高度不随列宽变，由它自己的排版 token 推出，见 `--card-meta-block`：两行标题
     （`.meta .t` 的 2.7em）、一行归属（1lh，关注卡另有 21px 的最小高度）、`.mtext` 的 3px
     行距；有标签的卡再加一行标签（`.ctags` 的高度）和一道行距，关注卡的徽章行 `.fbadges`
     同理（`.fbadge` 一行字加上下 2px 内边距和 1px 边框）。密集一档的高度是 `.mtext` 固定的
     三行，同一个变量。
   叠层纸边画在 `.pic` 外面，不在裁切范围里，Mix 卡、分卷／版次卡和竖屏带的卡一并适用。
   跳过渲染连带 paint containment，裁切边是元信息区的 padding box：贴着左缘的头像悬停
   描边、头像与标签的 2px 焦点环都画在内容盒外面。所以元信息区四周垫 8px 内边距、再用
   -8px 外边距抵消，裁切边往外扩 8px（和整张卡的悬停面同宽），内容盒、卡片尺寸和它与
   封面的间距一个像素不动；`frontend/e2e/design.test.ts` 按几何检查。垫出来的那一圈伸到
   卡片盒外面，元信息区自己因此不接指针，只有它的子元素接：卡片的悬停面、点击区和悬停
   预览的触发范围仍是卡片盒本身。元信息区没有背景，垫出来的一圈不画任何东西。
   图片墙不在其内：它的封面没有固定比例，高度取自图片本身，跳过渲染时会按零高排版。
   资料页的照片墙 `.photowall` 与名册 `.igrid` 同理不在其内，它们也不是 `.card`。资料页
   新作那一行（`.feednew`）和详情页的接着看（`#nrow`）只有一行、往横向排，屏外的卡不改变
   整页高度，标题字号也不是 `--fs-md`（`.feednewcard .meta .t` 是 `--fs-sm`），不套这份估计。 */
.meta{--card-meta-byline:1lh;--card-meta-tags:0px;--card-meta-badges:0px;
  --card-meta-block:calc(var(--fs-md)*2.7 + 3px + var(--card-meta-byline) + var(--card-meta-badges) + var(--card-meta-tags))}
.meta:has(>.mtext>.ctags){--card-meta-tags:calc(var(--fs-xs)*1.45 + var(--tag-pad-y)*2 + 2px + 3px)}
.followitem .meta{--card-meta-byline:max(1lh,21px)}
.followitem .meta:has(>.mtext>.fbadges){--card-meta-badges:calc(1lh + 6px + 3px)}
:is(#grid,.entitysection>.grid,.followlist:not(.followphotowall)) .card :is(.pic,.meta){content-visibility:auto}
/* `flex-shrink:0`：跳过渲染时元信息区的内容不参与最小尺寸，卡里有别的子元素要抢空间时它会
   被压扁。关注卡的 `.fstate` 是 `flex:1 1 100%`，不钉住的话屏外关注卡的元信息区只剩 21px。
   渲染时它本来就压不动（自动最小尺寸就是内容高度），钉住不改变屏上的排版。
   估计高度作用在内容盒上，内边距另外加在外面，所以 `--card-meta-block` 只估内容。 */
:is(#grid,.entitysection>.grid,.followlist:not(.followphotowall)) .card .meta{flex-shrink:0;
  margin:-8px;padding:8px;pointer-events:none;contain-intrinsic-block-size:auto var(--card-meta-block)}
:where(#grid,.entitysection>.grid,.followlist:not(.followphotowall)) .card .meta>*{pointer-events:auto}
/* 叠层往上溢出到行间距里，卡片本体不为它留白。留白的写法（padding-top:7px）会把
   整张卡压低 7px 并加高 7px，同一行里 Mix、分卷、版次卡的封面和下面的文字就比
   邻居矮一截；往上堆之后卡片盒和普通卡完全同高，下沿自然平齐。
   纸边与封面是同一种表面，圆角同一档：容器取封面那一档，纸边继承容器。 */
.mixstack,.partstack{position:relative;isolation:isolate;border-radius:var(--badge-radius)}
.mixstack::before,.mixstack::after,.partstack::before,.partstack::after{content:"";position:absolute;border-radius:inherit;
  border:1px solid var(--border-15);background:color-mix(in srgb,var(--surface) 84%,var(--video-glow,#15202a));z-index:0}
.mixstack::before,.partstack::before{inset:0 12px 8px;transform:translateY(-7px);opacity:.54}
.mixstack::after,.partstack::after{inset:0 6px 4px;transform:translateY(-4px);opacity:.78}
.mixstack .pic,.partstack .pic{z-index:1}
/* 悬浮 Mix 卡片时逐张翻走：最上面那张向上抬起、轻转并淡出，下一张随即接管。
   和静止时的叠层是同一个隐喻，不是另一套动效；滞后动画偏好下根本不启动（JS 侧拦下）。 */
.mixfaces{position:absolute;inset:0;z-index:2;pointer-events:none;background:#000}   /* 遮住静止封面：不遮的话两张正在交接的时候它会从中间透出来 */
.mixface{position:absolute;inset:0;opacity:0;transform:scale(.985);transform-origin:50% 88%;
  transition:transform .46s cubic-bezier(.34,.02,.24,1),opacity .4s ease}
.mixface.on{opacity:1;z-index:2;transform:none;transition:none}
.mixface.off{opacity:0;z-index:3;transform:translateY(-11%) rotate(-3.4deg) scale(.94)}
.mixbadge{position:absolute;right:9px;bottom:9px;z-index:6;display:flex;align-items:center;gap:6px;
  min-height:28px;padding:4px 9px;border-radius:var(--control-radius);background:rgba(12,8,8,.72);color:#fff;
  backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);font-size:var(--fs-xs);font-weight:600}
.mixbadge svg{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:2}
.partbadge{position:absolute;right:9px;top:9px;z-index:6;min-height:27px;padding:5px 9px;border-radius:var(--control-radius);
  background:rgba(12,8,8,.72);color:#fff;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);
  font-size:var(--fs-xs);font-weight:600}
.mixcard .mixmeta,.playlistcard .mixmeta{display:flex;min-width:0;gap:9px;align-items:flex-start}
.playlistcard .mixcopy{flex:1}
.mixglyph{width:40px;height:40px;flex:none;border-radius:50%;display:grid;place-items:center;
  color:var(--ink);background:var(--overlay-5);border:1px solid var(--border-10)}
.mixglyph svg{width:20px;height:20px;stroke:currentColor;fill:none;stroke-width:2}
.mixcopy{min-width:0}.mixcopy b,.mixcopy span{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mixcopy b{font-size:var(--fs-md);line-height:1.45}.mixcopy span{font-size:var(--fs-xs);color:var(--muted);margin-top:2px}
/* 标记是正圆，全站一个形状——复核缩略图那一枚已经是圆的，两处只差尺寸档。 */
.selectionMark{display:none;position:absolute;top:10px;right:10px;z-index:8;width:26px;height:26px;border-radius:50%;
  place-items:center;color:rgba(255,255,255,.72);background:rgba(0,0,0,.34);border:1px solid rgba(255,255,255,.28);
  backdrop-filter:saturate(1.8) blur(12px);-webkit-backdrop-filter:saturate(1.8) blur(12px)}
.selectionMark svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
body.select-mode .selectionMark,.card.selected .selectionMark{display:grid}
.card.selected .selectionMark{background:var(--on-media);color:var(--on-media-ink);
  border-color:var(--on-media);box-shadow:0 3px 12px #0008}
.deleteMark{position:absolute;z-index:7;left:10px;bottom:10px;display:none;align-items:center;gap:5px;height:28px;padding:0 9px;
  border-radius:var(--badge-radius);background:rgba(0,0,0,.58);color:#FFB14A;border:1px solid rgba(255,177,74,.4);
  backdrop-filter:saturate(1.8) blur(12px);-webkit-backdrop-filter:saturate(1.8) blur(12px);font-size:var(--fs-xs);font-weight:600}
.deleteMark svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.card.pending-delete .deleteMark{display:flex}
.card.pending-delete .poster,.card.pending-delete .hv,
.card.pending-delete .hvframes{filter:grayscale(.9) brightness(.54);opacity:.68}
/* 模糊层要跟着一起暗下去，否则待删卡片上只有两条留白还是亮的。这条自带 blur()，
   `filter` 不能继承也不能叠加，只能把三个函数写在一起。 */
.card.pending-delete .pic::before{filter:blur(26px) grayscale(.9) brightness(.27);opacity:.68}
.card.pending-delete .meta{opacity:.58}
.card.pending-delete:hover .meta{opacity:.76}
.pic{position:relative;aspect-ratio:var(--card-ratio,16/9);background:#000;border-radius:var(--badge-radius);
  overflow:hidden;display:flex;align-items:center;justify-content:center}
/* 正封窄于卡片时两侧的留白，垫的是同一张封面的模糊放大版；`--cover-blur` 由
   `posterPanel` 在图片加载完写进来，没写就是 `none`，这层什么都不画。
   伪元素排在所有子元素之前，不用 z-index 就压在封面底下。往外扩一圈是因为
   `blur()` 会让边缘向内淡出，留白那两条正好贴着边——扩出去的部分由 `overflow` 切掉。 */
.pic::before{content:"";position:absolute;inset:-8%;pointer-events:none;
  background:var(--cover-blur,none) center/cover no-repeat;filter:blur(26px) brightness(.5)}
/* 预览图：单格，contain 放进固定 16:9 框 —— 不拉伸、不裁切、不改框体大小 */
.poster{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;
  display:block;background:#000}
/* 装着封面的格子底下不垫黑：封面和黑底各按圆角裁一次，边缘那圈半透明像素里黑底会
   透出来，浅色封面的圆角上就是一圈灰边（纯白图实测边缘最暗压到 175）。封面自己的黑底
   在 `19-immersive.css` 里一并清掉；留白由 `::before` 的模糊底垫，悬停预览层自带黑底。 */
.pic:has(.poster.cover){background:none}
/* 叠层卡的封面格背后压着两层纸边，不能是空的：模糊衬底贴边那一截是半透明的
   （`blur()` 向内淡出），封面落地前整格也是空的，纸边的线条会从这两处透进封面。
   垫一层页面底色，透出来的就和普通卡一样只是底色本身。 */
:is(.mixstack,.partstack) .pic:has(.poster.cover){background:var(--page)}
.cardopenhit{position:absolute;inset:0;z-index:1;border:0;border-radius:inherit;background:transparent;padding:0;cursor:pointer}
.cardopenhit:focus-visible{outline:2px solid var(--tungsten);outline-offset:2px}
.card>.pic,.card>.partstack,.card>.meta{position:relative;z-index:2}
/* 悬停即播真视频（仅免费源，计费源永远不自动拉流） */
.hv{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:#000;
  opacity:0;transition:opacity .2s}
.hv.on{opacity:1}
/* 远端源的悬停扫视层。几何和 `.hv` 逐字一致：不透明黑底盖掉下面的封面，接触印相
   的格子在里面 contain。大图版式的容器是 0.75 竖比例，16:9 的格子于是居中、上下留黑。
   不写 z-index，和 `.hv` 一样靠 DOM 次序压在 `.poster` 之上。 */
img.hvframes{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;
  background:#000;display:block}
/* 悬停面往卡片盒外多铺 8px：不动网格尺寸、不挤相邻卡，只让整张卡的命中反馈比内容
   大一圈。背景仍落在卡本身，保留浏览器里可直接核验的悬停底色。 */
body .card:not(.junkcard,.resourcecard):hover{background:var(--hover);box-shadow:0 0 0 8px var(--hover)}
body .card:not(.junkcard,.resourcecard):hover .pic{filter:saturate(.9) brightness(.94)}
body .card:not(.junkcard,.resourcecard):focus-within{background:var(--hover);box-shadow:0 0 0 8px var(--hover)}
.select-mode .card{cursor:default}
.select-mode .cardopenhit,.select-mode .hovertools,.select-mode .previewcounter{pointer-events:none!important}
.card.selected .pic,.card.selected:hover .pic{box-shadow:inset 0 0 0 2px var(--on-media)}
@media (hover:hover) and (pointer:fine){
  .card.longhover{transform:translateY(-7px) scale(1.065);z-index:8;filter:brightness(1.04)}
}
.hovertools{position:absolute;z-index:4;display:flex;align-items:center;gap:14px;
  opacity:0;transition:opacity .15s;pointer-events:none}
.card.longhover .seektools,.card:hover .later-tools{opacity:1}
.hovertools.seektools{inset:50% auto auto 50%;transform:translate(-50%,-50%)}
.hovertools button{pointer-events:none;width:58px;height:58px;border-radius:50%;border:1px solid rgba(255,255,255,.12);
  background:rgba(0,0,0,.24);color:#fff;backdrop-filter:saturate(180%) blur(12px);
  -webkit-backdrop-filter:saturate(180%) blur(12px);display:flex;align-items:center;
  justify-content:center;cursor:pointer;position:relative;transition:transform .15s ease,background .15s ease}
.hovertools button svg{width:25px;height:25px;stroke:currentColor;fill:none;stroke-width:2;
  stroke-linecap:round;stroke-linejoin:round}
.hovertools button:hover{transform:scale(1.12);background:rgba(0,0,0,.34)}
/* 居中那三颗和右下角的「稍后看」是同一种控件，只是尺寸不同：共用磨砂圆底、同一条
   边框和同一档悬停填充，图标按 58／36 的比例放到 34px。裸图标在浅色封面上要靠投影
   才读得出，而投影本身又是这一层里唯一一处没有面的写法。秒数交给 title／aria-label，
   图标上不压数字。参考证据见 docs/reference-snapshots/hover-seek-controls-user-screenshot.md。 */
.hovertools.seektools button svg{width:34px;height:34px}
.hovertools.later-tools{right:12px;bottom:12px}
.card.longhover .seektools button,.card:hover .later-tools button{pointer-events:auto}
.hovertools .laterbtn{width:36px;height:36px;padding:0;font-family:inherit}
.hovertools .laterbtn svg{width:21px;height:21px}.hovertools .laterbtn[aria-pressed="true"]{background:var(--ink);color:var(--ground)}
.previewcounter{position:absolute;right:12px;top:12px;z-index:4;width:36px;height:36px;border:0;padding:0;
  border-radius:50%;display:grid;place-items:center;color:#fff;cursor:pointer;opacity:0;pointer-events:none;
  background:rgba(0,0,0,.24);backdrop-filter:saturate(180%) blur(12px);-webkit-backdrop-filter:saturate(180%) blur(12px);
  transition:transform .15s ease,opacity .15s}
.card.previewing .previewcounter{opacity:1;pointer-events:auto}.card.longhover .previewcounter{opacity:0;pointer-events:none}
.previewcounter:hover{transform:scale(1.2)}.previewcounter>svg{position:absolute;inset:0;width:36px;height:36px;transform:rotate(-90deg)}
.previewcounter circle{fill:none;stroke-width:2}.previewcounter circle:first-child{stroke:rgba(255,255,255,.18)}
.previewcounter circle:last-child{stroke:#fff;stroke-linecap:round;stroke-dasharray:106.814;stroke-dashoffset:106.814}
.card.previewing .previewcounter circle:last-child{animation:preview-arc var(--hover-delay) linear forwards}.card.longhover .previewcounter circle:last-child{animation:none}
.previewcounter .ringplay{position:relative;inset:auto;width:14px;height:14px;transform:none;stroke:#fff;fill:none;stroke-width:2;margin-left:1px}
@keyframes preview-arc{to{stroke-dashoffset:0}}
.nopic{font-size:var(--fs-xs);letter-spacing:.12em;color:var(--muted);text-transform:uppercase}
.resourceglyph{display:grid;place-items:center;gap:8px;color:var(--muted);font-size:var(--fs-xs);letter-spacing:.08em}
.resourceglyph svg{width:32px;height:32px;stroke:currentColor;fill:none;stroke-width:2}
.resourcecardaction{position:absolute;right:10px;bottom:10px;z-index:5;display:inline-flex;align-items:center;gap:6px;
  min-height:32px;padding:0 10px;border:1px solid rgba(255,255,255,.25);border-radius:var(--control-radius);
  background:rgba(0,0,0,.64);color:#fff;font:inherit;font-size:var(--fs-xs);cursor:pointer;
  backdrop-filter:saturate(1.5) blur(10px);-webkit-backdrop-filter:saturate(1.5) blur(10px)}
.resourcecardaction:hover:not(:disabled){background:var(--hover);color:var(--ink)}
.resourcecardaction:focus-visible{outline:2px solid var(--tungsten);outline-offset:2px}
.resourcecardaction:disabled{cursor:wait;opacity:.72}.resourcecardaction svg{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:2}
.resourcekind{display:grid;place-items:center;color:var(--muted);border-radius:var(--control-radius)}
.resourcekind svg{width:19px;height:19px;stroke:currentColor;fill:none;stroke-width:2}
.resourcecardtitle{cursor:default}
.junkcount{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:48px}
.junksummary{flex:none;font-size:var(--fs-sm);color:var(--muted);white-space:nowrap}
/* 两条分类切换共用一套外观，语义各自不同：复核那条在 10 个互不相同的候选数据集之间切
   （真 tablist），垃圾文件那条是同一批候选按 type 收窄（落 URL 的筛选链接）。
   几何仍取 Geist Tabs 的 secondary 变体（vercel.com/geist/tabs 实测 2026-09-03）：高 32px、
   左右 12px、6px 圆角、13px/400，hover 只换文字色，选中只抬底到 --hover、不动边框。
   有意偏离一处：上游 secondary 不描边，Peach 描 1px --border-15。上游正文没有
   「secondary 必须放在容器内」这类条款（未取得），但 Peach 这两条直接坐在页面顶部，
   下面就是全宽网格，四周没有任何容器边线；不描边时只有选中那一枚有底色，其余几项
   没有可视边界，看不出是一排可点的控件。取值与排序条同族，两处认得出是同类控件。 */
.reviewtabs,.junkfilters{display:flex;align-items:center;gap:5px;min-width:0;overflow-x:auto;scrollbar-width:none}
.reviewtabs::-webkit-scrollbar,.junkfilters::-webkit-scrollbar{display:none}
.reviewtabs{margin-bottom:16px}
/* 一枚也不描边。给每一枚都画一圈线之后，一排读起来是七个一模一样的框，选中那一枚
   靠的是比页面底亮 2% 的填充——线比填充响得多，于是「选中」在浅色一档上根本看不出来。
   Geist Tabs secondary 两态都没有 border，选中只有 `aria-selected:bg-gray-200` 一件事
   （vercel-geist-semantics-measured.md「选中态与它的悬停」）。 */
.reviewtabs button,.junkfilters a{box-sizing:border-box;height:32px;padding:0 12px;flex:none;
  display:inline-flex;align-items:center;gap:6px;border:0;
  border-radius:var(--control-radius);background:transparent;color:var(--muted);
  text-decoration:none;font:inherit;font-size:var(--fs-sm);font-weight:400;
  white-space:nowrap;cursor:pointer}
.reviewtabs button:hover,.junkfilters a:hover{color:var(--ink)}
.junkfilters a[aria-current="page"]{background:var(--picked);color:var(--ink)}
.reviewtabs button:focus-visible,.junkfilters a:focus-visible{outline:2px solid var(--tungsten);outline-offset:2px}
.junkfilters .n{color:var(--muted);font-size:var(--fs-xs);font-variant-numeric:tabular-nums}
.junkfilters i{width:1px;height:20px;background:var(--line-soft);flex:none;margin:0 6px}
.junkfilters a>svg{width:16px;height:16px;flex:none;stroke:currentColor;fill:none;stroke-width:2;
  stroke-linecap:round;stroke-linejoin:round}
.junkcard{cursor:default;gap:0;overflow:hidden;border:1px solid var(--line-soft);border-radius:var(--floating-radius);background:var(--ground);
  content-visibility:auto;contain-intrinsic-size:auto 360px}
.junkcard .pic{border:0;border-radius:0}.junkcard:hover .pic{box-shadow:none}
.junkcard .poster{object-fit:contain}.junkbody{display:flex;flex:1;flex-direction:column;min-height:0}
.select-mode .junkcard .junkactions{pointer-events:none;opacity:.52}
.junkmeta{display:grid;gap:6px;min-width:0;padding:12px}.junkmeta .s{display:flex;gap:7px;min-width:0;flex-wrap:wrap;
  color:var(--muted);font-size:var(--fs-xs)}
.junkmeta .why{overflow-wrap:anywhere}.junkmeta .size{margin-left:auto;white-space:nowrap}
.junkcardtitle{display:block;width:100%;min-width:0;
  border:0;padding:0;background:transparent;color:var(--ink);font:inherit;font-weight:600;line-height:1.4;text-align:left}
button.junkcardtitle{cursor:pointer}button.junkcardtitle:hover{color:var(--ink-2)}
button.junkcardtitle:focus-visible{outline:2px solid var(--tungsten);outline-offset:3px;border-radius:var(--control-radius)}
.junkactions{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:auto;padding:10px 12px;
  border-top:1px solid var(--line-soft);background:var(--overlay-5)}
/* 三个按钮等宽，图标加标签在各自那一格里居中。 */
.junkactions button{box-sizing:border-box;min-width:0;min-height:36px;padding:0 10px;display:inline-flex;align-items:center;justify-content:center;gap:7px;
  border:0;border-radius:var(--control-radius);box-shadow:0 0 0 1px var(--line-soft);background:var(--ground);color:var(--ink-2);
  font:inherit;font-size:var(--fs-xs);white-space:nowrap;cursor:pointer}
.junkactions button:hover:not(:disabled){background:var(--inset);color:var(--ink)}
.junkactions button:focus-visible{outline:2px solid var(--tungsten);outline-offset:2px}
.junkactions button:disabled{cursor:wait;opacity:.64}.junkactions button svg{width:15px;height:15px;flex:none;stroke:currentColor;fill:none;stroke-width:2}
/* 「移入回收站」的红来自 `.danger`，这一格只管几何。请求期间三颗键一起 `cursor:wait`
   变淡，红那颗的禁用底交给危险档，页面不给它另写一份灰。 */
.junkstate{grid-column:1/-1;color:var(--drop);font-size:var(--fs-xs)}.junkstate:empty{display:none}
/* 紧凑密度下只剩图标。 */
body[data-density="dense"] .junkcard .junkactions button span{display:none}
.badge{position:absolute;top:6px;left:6px;display:flex;gap:4px;z-index:2;align-items:center;
  transition:opacity .15s ease}
/* 首页流和详情只留来源图标；完整来源与费用通过 title/aria-label 提供。 */
.src{display:grid;place-items:center;width:20px;height:20px;padding:0;border:0;background:transparent}
.src img,.src svg{width:18px;height:18px;border-radius:var(--badge-radius);object-fit:contain;display:block;flex:none}
/* 线条类来源图标（本地盘 / 在线）没有品牌底色，亮封面上会看不见：
   垫一层深色方角底（--badge-radius，与圆形实体头像划清界限）。
   115 / PikPak 的官方位图 logo 自带完整底色，保持裸图不再叠底。 */
.src:has(svg){width:auto;height:auto;padding:3px;border-radius:var(--badge-radius);background:color-mix(in srgb,#080A0D 62%,transparent)}
.src.free{color:#CFE8CF}.src.metered{color:#FFD9AE}
.src svg{stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
/* 详情面板里的来源 */
.srcbig{display:inline-grid;place-items:center;width:22px;height:22px;padding:0;border:0;background:transparent}
.srcbig img,.srcbig svg{width:17px;height:17px;border-radius:var(--badge-radius);object-fit:contain;flex:none}
.srcbig svg{stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.srcbig.metered{color:var(--meter)}.srcbig.free{color:var(--keep)}
.dur{position:absolute;right:6px;bottom:6px;z-index:2;background:color-mix(in srgb,#000 70%,transparent);
  color:#F2EADF;padding:2px 6px;border-radius:var(--badge-radius);transition:opacity .15s ease}
@media (hover:hover) and (pointer:fine){
  .card:hover .badge,.card:hover .dur{opacity:0}
}
.scrub{position:absolute;left:0;right:0;bottom:0;height:3px;z-index:2;
  background:color-mix(in srgb,#000 45%,transparent)}
.scrub i{display:block;height:100%;background:var(--loupe)}
.watchprogress{position:absolute;left:0;right:0;bottom:0;height:4px;z-index:5;background:rgba(255,255,255,.3)}
.watchprogress i{display:block;height:100%;background:var(--tungsten)}
.meta{display:flex;gap:10px;min-width:0}
.mav{width:38px;height:38px;border-radius:50%;flex:none;overflow:hidden;background:var(--sunk);
  position:relative;margin-top:2px;border:0;padding:0;cursor:pointer;color:inherit}
.mav:hover{box-shadow:0 0 0 2px var(--ink-2)}
.mav img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
.mav .ini{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  font-size:var(--fs-sm);color:var(--muted);font-family:Bahnschrift,"Microsoft YaHei UI","Microsoft YaHei","PingFang SC",sans-serif}
/* 共演头像最多五枚，常态紧叠；上、左、下三边给放大与焦点环留出空间，头像本身仍与单人
   头像站在同一条左对齐线上。 */
.mavstack{display:flex;flex:none;max-width:calc(100% - 48px);margin:-4px -2px -2px;padding:6px 2px 2px 5px}
.mavstack .mav{margin-top:0;box-shadow:0 0 0 2px var(--ground)}
.mavstack .mav+.mav{margin-left:-22px}
.mavstack .mav:nth-child(1){z-index:5}
.mavstack .mav:nth-child(2){z-index:4}
.mavstack .mav:nth-child(3){z-index:3}
.mavstack .mav:nth-child(4){z-index:2}
.mavstack .mav:nth-child(5){z-index:1}
.mavstack .mav:nth-child(n+6){display:none}
.mavstack .mav:is(:hover,:focus-visible){z-index:6;box-shadow:0 0 0 2px var(--ink-2)}
.meta .whomore{color:var(--muted)}
.mtext{display:flex;flex-direction:column;gap:3px;min-width:0;flex:1}
/* 高度另用 max-height 封在两行：标题里的番号块是 inline-flex，WebKit 的 line-clamp 遇到非行内的
   子元素就不截，iOS 上长标题会整段摊开，把「接着看」那一排撑得老高。 */
.meta .t{font-size:var(--fs-md);line-height:1.35;min-height:2.7em;max-height:2.7em;overflow:hidden;display:-webkit-box;
  -webkit-line-clamp:2;-webkit-box-orient:vertical;word-break:break-all}
.meta .cardtitle{width:100%;padding:0;border:0;background:transparent;color:inherit;text-align:left;cursor:pointer}
.javidentity{display:inline-flex;align-items:center;gap:5px;vertical-align:baseline;white-space:nowrap}
.javcode{font-weight:600;letter-spacing:.01em}.javtitle{font-weight:400}
.javedition{display:inline-flex;align-items:center;min-height:18px;padding:0 5px;border:1px solid currentColor;
  border-radius:var(--badge-radius);font-size:var(--fs-xs);line-height:16px;font-weight:600;letter-spacing:0;vertical-align:1px}
.javedition.subtitle{color:var(--ink-2)}
.javedition.uncensored{color:var(--meter)}
.javedition.cracked{color:var(--drop)}
.javedition.censored{color:var(--muted)}
/* 卷号说的是「这是第几份文件」，不是版次，用中性灰和三种版次色区分开。 */
.javedition.partlabel{color:var(--muted);margin-left:6px}
.meta .s{display:flex;gap:8px;color:var(--muted);align-items:center;flex-wrap:wrap}
.meta .who{color:var(--ink-2);min-width:0;max-width:100%;display:inline-block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom}
/* 归属渲染成 `<span>` 就是不可点的那一种（队列行整行本身是按钮，里面嵌不了按钮）。
   染成强调色会让人以为能点，点下去却落到卡片本身、打开的是视频详情。
   「未归属」不在此列：它有自己的集合可去，出的是 `.who.unownedlink` 按钮。 */
.meta span.who{color:var(--ink-2);cursor:default}
.flags{display:flex;gap:3px;margin-left:auto}
.flags i{width:6px;height:6px;border-radius:0;display:block}
.flags i.dislike{background:var(--drop)}.flags i.seen{background:var(--muted)}
.flags i.dispose{background:var(--meter)}
.flags i.later{background:var(--ink)}
/* 标签语义分级 */
/* 卡片上宁可少放几个标签，也要把名字写全：截成「主…」「背…」之后，那既不是标签，
   也认不出原来是哪一个。所以这一行改成可换行、只有一颗标签那么高——放不下的整颗
   落到第二行，被容器整颗切掉，切掉的是标签本身，不是它的名字。行高按 .tg 的三个
   token 算出来，改字号或内边距时不用回来对第二个数。
   裁切边是 padding box：四周垫 4px 内边距再用 -4px 外边距抵消，标签 2px 偏移加 2px 线宽
   的焦点环整圈落在裁切边以内，这一行占的位置和标签的排布一个像素不动；第二行离第一行
   隔着 5px 行距，仍整颗落在裁切边外面。垫出来的一圈伸到卡片盒外面，这一行自己不接指针，
   只有标签接。
   .tg 只留 max-width:100% 这一层兜底：一颗标签自己就比整张卡还宽时，横着溢出会
   压到卡片外面，这时候省略号是唯一的出路。 */
.ctags{display:flex;flex-wrap:wrap;align-content:flex-start;gap:5px;min-width:0;overflow:hidden;
  margin:-4px;padding:4px;height:calc(var(--fs-xs)*1.45 + var(--tag-pad-y)*2 + 2px + 8px);pointer-events:none}
.ctags>*{pointer-events:auto}
.tg{padding:var(--tag-pad-y) var(--tag-pad-x);border-radius:var(--tag-radius);font:inherit;font-size:var(--fs-xs);border:1px solid var(--line);
  line-height:1.45;white-space:nowrap;cursor:pointer;color:var(--ink-2);overflow:hidden;text-overflow:ellipsis;max-width:100%;
  background:var(--tag-fill)}
.tg.general,.tg.artist,.tg.meta,.tg.relationship,.tg.role,.tg.appearance,.tg.scene,.tg.story,.tg.position{color:var(--ink-2);border-color:var(--line)}
.tg.artist{font-weight:600}
.tg:hover{color:var(--ink);background:var(--hover)}
.tg:disabled{cursor:not-allowed}
body[data-density="dense"] .card .ctags .tg:nth-child(n+2){display:none}
body[data-density="dense"] .card .ctags .tg{max-width:100%;overflow:hidden;text-overflow:ellipsis}
/* 顶栏的密集模式固定为标题、身份、标签三行。缺少标签时第三行仍保留，
   同一网格不会再被大小、观看次数或长身份撑成四行。分卷卡已有 7px 叠层空间，
   密集网格的普通卡也保留同一高度，避免每行是否出现分卷卡决定整行高度。
   三行由 `.mtext` 裁住，它和 `.ctags` 一样四周垫 4px 再用 -4px 抵消、自己不接指针，标签的
   焦点环不被这一层裁掉。子元素把指针接回来，标签行由它自己那条规则管；资料页新作那一行
   的卡整块放行指针（见 `.feednewcard`），不接回来。 */
body[data-density="dense"] .grid>.card{padding-top:7px}
body[data-density="dense"] .card .meta{--card-meta-block:calc(2.7em + 36px)}
body[data-density="dense"] .card .mtext{display:grid;grid-template-rows:1.35em 1.35em 30px;
  gap:3px;margin:-4px;padding:4px;height:calc(var(--card-meta-block) + 8px);overflow:hidden;pointer-events:none}
body[data-density="dense"] .card:not(.feednewcard) .mtext>:not(.ctags){pointer-events:auto}
body[data-density="dense"] .card .meta .t{display:block;max-width:100%;min-height:1.35em;overflow:hidden;
  -webkit-line-clamp:unset;white-space:nowrap;text-overflow:ellipsis}
body[data-density="dense"] .card .meta .s{height:1.35em;min-height:0;flex-wrap:nowrap;overflow:hidden;white-space:nowrap}
body[data-density="dense"] .card .meta .who{flex:0 1 auto}
body[data-density="dense"] .card .meta .whomore,
body[data-density="dense"] .card .meta .size{flex:none;white-space:nowrap}
body[data-density="dense"] .card .ctags{height:38px;align-items:flex-start;flex-wrap:nowrap;overflow:hidden}
body[data-density="dense"] .card .meta .why,
body[data-density="dense"] .card .meta .watchcount{display:none}

/* 短片带是这一叠卡里的一张，和上下两段视频平级：同一张 --ground 面、同一圈发丝边、
   同一个圆角，只有内容不同。它不套在视频框里，也不是把一个框从中间划开——划开的是
   一张卡，两张卡之间那 16px 页面底才是分隔。 */
.shorts-inline{padding:16px;background:var(--ground);
  border:1px solid var(--field-ring);border-radius:var(--floating-radius)}
.shorts-inline h2{margin:0 0 11px;font-size:var(--fs-lg);display:flex;align-items:center;gap:10px}
.shorts-inline .scard .pic{aspect-ratio:var(--card-ratio,9/16)}
/* 竖屏计数的灰字。index.html 和 openItem 的内联片段原先各写一份，一模一样。 */
.shortscount{color:var(--muted);font-size:var(--fs-xs)}
.shorts-inline h2 button{margin-left:auto;min-height:48px;display:inline-flex;align-items:center;gap:7px;
  font-size:var(--fs-xs);background:var(--overlay-5);border:0;border-radius:var(--pill-radius);padding:0 16px;cursor:pointer;
  color:var(--ink);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);transition:background .14s,translate .14s,scale calc(var(--spring-press-ms) * 1ms) var(--spring-press)}
.shorts-inline h2 button:hover{background:color-mix(in srgb,var(--ink) 16%,transparent);translate:0 -1px}
/* 抬起和缩放各占一个属性：两样都写在 `transform` 上时，按下那一刻抬起的那 1px 被顶
   掉，按钮在缩小的同时还往下掉一格。 */
.shorts-inline h2 button:active{scale:.96;transition:scale .06s ease-out}
.shorts-inline h2 button svg{width:20px;height:20px;stroke:currentColor;fill:none;stroke-width:2}
.srow{display:flex;gap:13px;overflow-x:auto;padding-bottom:7px}
.srow .scard{flex:none;width:214px;cursor:pointer}
body[data-density="dense"] .shorts-inline .scard{width:calc(214px * 168 / 336)}

/* 订阅源发现的新作（ADR-0041）。横排复用 `.srow`，卡片复用 `.card` 的封套与元数据；
 * 这里只定宽度、淡出已读那一档，以及那两颗悬浮动作键——其余形态跟着资产卡走，
 * 两种卡长得不一样的地方只应该是它们真的不一样的地方。 */
.feednew{margin:0 0 18px}
/* RSS 自己的橙：只给人物页「订阅新作」开关订上时用。新作卡等封面时的微光与全站等待态
   同一种灰，骨架不另起一种颜色。 */
:root{--feed-mark:#F78422}
/* 卡片悬停时底色向四周多铺 8px（`box-shadow` 的 spread），而横滚容器把纵向溢出一并
   裁掉：四边各留 8px 内边距，再用同样的负外边距收回来，卡片落点不动。 */
.feednewrow{padding:8px;margin:-8px -8px 0}
/* 往右翻时卡片从侧栏底下穿过去，和首页那几排（`board.css` 的 `#tiers .tier`）同一个
   理由、同一种写法：让出轨道宽度加 `main` 的 16px 内边距，再原样加回内边距，第一张
   仍站在这一屏的左对齐线上。首页与人物页的这一行都直接住在 `main` 里，所以让出的是
   同一个数；窄屏的抽屉盖在内容上面，没有这条常驻的边，那一档不让。 */
@media(min-width:761px){
  .feednewrow{margin-left:calc(-1 * (var(--railW) + 16px));padding-left:calc(var(--railW) + 16px)}
}
/* 连不上图片主机那条 Note 与下面一行隔 12px：横排自己往上收了 8px，这里先垫回来。 */
.feednetwork{margin-bottom:20px}
.feednewcard{flex:none;width:168px}
/* 已看过的不移走，只退到背景里：已读是标记，不是关掉。 */
.feednewcard.isread{opacity:.55}
/* 通向作品页的点击区。形态同 `.cardopenhit`，但那一个的语义是站内落点，这一条是外链。
   资产卡的 `.pic` 与 `.meta` 压在点击区上面（z-index 2），那边的点击由卡片自己的脚本
   接；这张卡只有这条外链，所以两块放行指针，落到下面的链接上，悬浮键自己再收回来。 */
.feednewopen{position:absolute;inset:0;z-index:1;border-radius:inherit}
.feednewcard>.pic,.feednewcard>.meta{pointer-events:none}
.feednewopen:focus-visible{outline:2px solid var(--tungsten);outline-offset:2px}
/* 两颗动作键沿用 `.hovertools` 的磨砂圆底，只换尺寸和落点。`.hovertools` 默认
   `pointer-events:none`——那是为了让指针落到底下的预览上，而这两颗是这张卡唯一能按的
   东西，所以在显形的同时把点击收回来。 */
.feednewtools{inset:auto 7px 7px auto;gap:6px}
.feednewcard:hover .feednewtools,.feednewcard:focus-within .feednewtools{opacity:1}
.feednewcard:hover .feednewtools button,.feednewcard:focus-within .feednewtools button{pointer-events:auto}
.feednewtools button{width:32px;height:32px}
.feednewtools button svg{width:17px;height:17px}
.feednewcard .meta .t{font-size:var(--fs-sm)}
/* 厂牌加发行日一行放完，放不下截掉厂牌的尾巴：折成两行时断在日期中间，读成两个数。 */
.feednewcard .meta .s{flex-wrap:nowrap;white-space:nowrap}
.feednewcard .meta .s>span{flex:none}
.feednewcard .meta .s>.feednewstudio{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis}
/* 占位卡直接套真卡的 `.t` 与 `.s`：标题块是两行 2.7em，下面那行放一个零宽字符，行高
   跟真卡那行字同一套规则，到货那一下整行不跳。 */
.feednewskeleton{pointer-events:none}
.feednewskeleton .t>.skeleton{height:2.7em}
.feednewskeleton .s>.skeleton{width:72%;height:auto}
/* 占位卡不是在等哪一张图，它本身就是等待态：微光和骨架同一帧出现。`.imgwait` 那 180ms
   的淡入门槛是给本地图的，套在这里，那 180ms 里露出来的是 `.pic` 的黑底。 */
.feednewskeleton .pic.imgwait::after{transition:none}
/* ── 作品详情浮窗 ── */
/* 氛围色定义在这里，右侧详情栏、切换队列和「接着看」共用同一档。三块同属
   一格详情，底色差 4% 就会在整幅宽度上留下一条界线，看着像两块面板拼起来；
   分块靠留白和小标题，不靠色差。 */
.stage{--detail-surface:color-mix(in srgb,var(--video-glow,#15202a) 12%,var(--surface) 88%);
  position:fixed;inset:16px;margin:auto;padding:0;width:min(1440px,calc(100vw - 32px));
  max-width:calc(100vw - 32px);max-height:calc(100dvh - 32px);height:fit-content;
  color:var(--ink);border-radius:var(--floating-radius);overscroll-behavior:contain;
  background:radial-gradient(circle at 28% 24%,color-mix(in srgb,var(--video-glow,#15202a) 40%,transparent),transparent 58%),
    color-mix(in srgb,var(--surface) 88%,transparent);scroll-margin-top:calc(var(--topH) + 8px);
  border:1px solid var(--line);overflow:auto}
/* 遮罩和设置弹层同一档：两个都是盖住整页的弹层，底下都是同一片馆藏，压成两种深浅
   只会让人以为自己打开的是两种东西。值与依据在 `01-base.css` 的 `--scrim`。 */
.stage::backdrop{background:var(--scrim)}
body.detail-open{overflow:hidden}
.stage:not(.ambient-on){background:color-mix(in srgb,var(--surface) 88%,transparent)}
.stage[hidden]{display:none}
.grid>.stage{grid-column:1/-1;width:100%;min-width:0}
.followlist>.stage{grid-column:1/-1;width:100%;min-width:0}
body.entity-open #tiers,body.entity-open #tagbar,
body.index-open #tiers,body.index-open #tagbar{display:none}
.sgrid{display:grid;grid-template-columns:minmax(0,1fr) minmax(350px,390px)}
/* Mix、分卷、播放列表和关注合集都保留普通详情的主结构：媒体在左、信息在右；
   切换队列作为完整的第二行，避免详情栏跨到队列旁边并制造背景断层。 */
.sgrid.mixgrid{grid-template-columns:minmax(0,1fr) minmax(350px,390px);
  grid-template-areas:"media side" "queue queue";align-items:start}
.sgrid.mixgrid>.vwrap{grid-area:media}
.sgrid.mixgrid>.side{grid-area:side;background:var(--detail-surface)}
.sgrid.mixgrid>.mixqueue{grid-area:queue;max-height:360px;border-left:0;border-top:1px solid var(--line-soft);
  background:var(--detail-surface)}
.sgrid.mixgrid .mixqueueactions [data-queue-close],
.sgrid.mixgrid .mixqueueactions [data-follow-queue-close]{display:none}
.mixqueue{min-width:0;max-height:76vh;display:flex;flex-direction:column;border-left:1px solid var(--line-soft);
  background:color-mix(in srgb,var(--video-glow,#15202a) 10%,var(--surface) 90%)}
.mixqueuehead{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px;align-items:center;
  min-height:70px;padding:12px 12px 10px 15px;border-bottom:1px solid var(--line-soft)}
.mixqueuehead h2{margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--fs-lg)}
.mixqueuehead span{display:block;margin-top:3px;color:var(--muted);font-size:var(--fs-xs)}
.mixqueuehead button{width:36px;height:36px;padding:0;border:0;border-radius:var(--control-radius);background:transparent;
  color:var(--ink);cursor:pointer;display:grid;place-items:center}.mixqueuehead button:hover{background:var(--hover)}
.mixqueuehead svg{width:20px;height:20px;stroke:currentColor;fill:none;stroke-width:2}
.mixqueueactions{display:flex;align-items:center;gap:2px}
.mixlist{min-height:0;overflow-y:auto;padding:6px}
.mixrow{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;border-radius:var(--control-radius)}.mixrow:has(.mixitem.current){background:var(--hover)}.mixrow .mixitem.current{background:transparent}
.mixitem{width:100%;display:grid;grid-template-columns:94px minmax(0,1fr);gap:10px;align-items:center;
  padding:7px;border:0;border-radius:var(--control-radius);background:transparent;color:var(--ink);text-align:left;cursor:pointer}
.mixitem:hover{background:var(--overlay-5)}.mixitem.current{background:var(--hover)}
.mixitempic{position:relative;aspect-ratio:16/9;border-radius:var(--badge-radius);overflow:hidden;background:#000}
.mixitempic img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}.mixitempic .dur{right:4px;bottom:3px}
/* 徽章和标题同一行。标题那一行因此要自己成为 flex 容器：`.mixitemtext` 的 b/span
   都是块级的，两者并排只能靠这一层，`min-width:0` 才让中段省略照旧按剩余宽度算。 */
.mixitemtext .mixitemhead{display:flex;align-items:center;gap:5px;margin-top:0;color:var(--ink)}
.mixitemtext .mixitemhead b{flex:1;min-width:0}
.mixitemtext .qedition{flex:none;font-style:normal}
/* 队列行的署名层照抄卡片的 `.meta`：横向队列和「接着看」并排出现在同一屏，
   一边有头像一边没有，看着就是两套控件。头像在这里不可点（整行是一个 button，
   里面不能再嵌 button），所以也不给 hover 提示。 */
.mixitemmeta{display:flex;gap:10px;min-width:0;align-items:center}
.mixitemmeta .mav{cursor:inherit}
.mixitemmeta .mav:hover{box-shadow:none}
.mixitemmeta .mavstack .mav{box-shadow:0 0 0 2px var(--ground)}
.mixitemtext{min-width:0}.mixitemtext b,.mixitemtext span{display:block;overflow:hidden;white-space:nowrap}.mixitemtext [data-truncate-end]{text-overflow:ellipsis}
.mixitemtext b{font-size:var(--fs-xs);line-height:1.4}.mixitemtext span{font-size:var(--fs-xs);color:var(--muted);margin-top:3px}
/* 顺序靠拖，行尾只剩抓手和移出。取色用 --ink-2 而不是 --muted：这两枚是这一行仅有的
   两个动作，压到副文字那一档在深色底上几乎读不出来，选中行的底色又更深一层。 */
.queueedit{display:grid;grid-auto-flow:column;align-items:center;gap:2px;padding-right:5px}
.queuegrip{display:grid;place-items:center;width:20px;color:var(--muted);cursor:grab}
.queuegrip svg{width:16px;height:16px;stroke:currentColor;fill:none}
.queueedit button{width:26px;height:26px;border:0;border-radius:var(--control-radius);background:transparent;color:var(--ink-2);padding:0;display:grid;place-items:center;cursor:pointer}
.queueedit button:hover{background:var(--hover);color:var(--ink)}
.queueedit svg{width:15px;height:15px;stroke:currentColor;fill:none}
/* 拖动中的行减淡，落点画一条线——和侧栏顺序那一列同一套表现，类名由共用的
   wireDragReorder 挂上。 */
.mixrow[data-queue-row]{position:relative;cursor:grab}
.mixrow[data-queue-row]:active{cursor:grabbing}
.mixrow.dragging{opacity:.42}
.mixrow.drop-before::before,.mixrow.drop-after::after{content:'';position:absolute;z-index:4;left:6px;right:6px;height:2px;border-radius:var(--pill-radius);background:var(--ink)}
.mixrow.drop-before::before{top:-3px}.mixrow.drop-after::after{bottom:-3px}
/* 详情下方的集合不是侧栏列表，而是与「接着看」同语义的横向切换带。
   Mix、分卷、播放列表、视频合集与多媒体只改变标题和标识，共用同一套卡片尺寸。 */
.sgrid.mixgrid>.mixqueue{padding:14px 14px 16px;overflow:hidden}
.sgrid.mixgrid>.mixqueue .mixqueuehead{min-height:0;padding:0 0 11px;border:0}
.sgrid.mixgrid>.mixqueue .mixqueuehead h2{font-size:var(--fs-sm);letter-spacing:.06em}
.sgrid.mixgrid>.mixqueue .mixqueuehead span{max-width:72ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sgrid.mixgrid>.mixqueue .mixlist{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(190px,220px);
  gap:10px;overflow-x:auto;overflow-y:hidden;padding:0 0 3px;overscroll-behavior-inline:contain;scroll-snap-type:x proximity}
.sgrid.mixgrid>.mixqueue .mixrow{display:block;position:relative;min-width:0;background:transparent;scroll-snap-align:start}
.sgrid.mixgrid>.mixqueue .mixitem{display:flex;flex-direction:column;align-items:stretch;gap:7px;padding:0}
.sgrid.mixgrid>.mixqueue .mixitem:hover,.sgrid.mixgrid>.mixqueue .mixitem.current{background:transparent}
.sgrid.mixgrid>.mixqueue .mixitempic{width:100%;border:1px solid transparent}
.sgrid.mixgrid>.mixqueue .mixitem.current .mixitempic{border-color:var(--ink);box-shadow:0 0 0 1px var(--ink)}
.sgrid.mixgrid>.mixqueue .mixitemmeta{width:100%;padding:0 2px;align-items:flex-start}
.sgrid.mixgrid>.mixqueue .mixitemmeta .mav,.sgrid.mixgrid>.mixqueue .mixitemmeta .mavstack{margin-top:2px}
.sgrid.mixgrid>.mixqueue .mixitemtext{flex:1;min-width:0}
.sgrid.mixgrid>.mixqueue .mixitemtext b{font-size:var(--fs-sm)}
.sgrid.mixgrid>.mixqueue .mixitem[data-media-kind="image"] .mixitempic img{object-fit:cover}
.sgrid.mixgrid>.mixqueue .queueedit{position:absolute;z-index:3;right:5px;top:5px;padding:3px;border-radius:var(--control-radius);background:rgba(0,0,0,.62);color:#fff}
.sgrid.mixgrid>.mixqueue .queueedit button{color:#fff}
/* 宽屏时详情栏决定首行高度，脱盘／错误占位也要铺满同一行，不能留下半块空背景。 */
@media (min-width:1081px){
  /* 浮窗里那一层竖着排：媒体格拿剩下的高度，「接着看」按内容定高钉在底下。浮窗照旧按
     内容定高、由 max-height 封顶，超过封顶时被压缩的是媒体那一行，不是推荐带——否则
     76vh 的画面加一条推荐带比视口高，推荐带整条落在浮窗外面。格里的媒体和详情栏都要
     `min-height:0`，行才压得下去；画面的上限跟着格走，宽了就按 contain 留黑边。 */
  .stage:not(.theater-mode)>.stagescroll{display:flex;flex-direction:column;max-height:calc(100dvh - 34px)}
  .stage:not(.theater-mode)>.stagescroll>.sgrid{flex:1 1 auto;min-height:0}
  .stage:not(.theater-mode) .sgrid>.vwrap{min-height:0}
  .stage:not(.theater-mode) .vwrap>.video-js{max-height:min(76vh,100%)}
  /* 影院模式画面独占一行、详情栏整段排在下面，总高必然超过浮窗：这一层自己滚，和窄屏
     那条同一个写法。 */
  .stage.theater-mode>.stagescroll{max-height:calc(100dvh - 34px);overflow:hidden auto;overscroll-behavior:contain}
  .sgrid.mixgrid>.vwrap{align-self:stretch}
  .sgrid.mixgrid>.vwrap>.gate{height:100%;aspect-ratio:auto}
  .stage.theater-mode .sgrid{grid-template-columns:minmax(0,1fr)}
  .stage.theater-mode .sgrid:not(.mixgrid)>.side{grid-column:1;border-left:0;border-top:1px solid var(--line-soft)}
  .stage.theater-mode .sgrid.mixgrid{grid-template-areas:"media" "side" "queue"}
  .stage.theater-mode .sidecontent:is(*){height:auto;max-height:none}
  .stage.theater-mode .vwrap>.video-js,.stage.theater-mode .vwrap>video{max-height:calc(100vh - var(--topH) - 56px)}
}
/* 横向队列把标题和摘要视为一个信息块，保存入口紧跟整个信息块并垂直居中。 */
.sgrid.mixgrid>.mixqueue .mixqueuehead{grid-template-columns:max-content max-content minmax(0,1fr);gap:5px 8px}
.sgrid.mixgrid>.mixqueue .mixqueuehead>div:first-child{min-width:0}
.sgrid.mixgrid>.mixqueue .mixqueueactions{grid-column:2;grid-row:1;align-self:center}
.vwrap{background:#000;position:relative;display:flex;align-items:center;justify-content:center;isolation:isolate}
.ambientcanvas{position:absolute;z-index:0;inset:-34px;width:calc(100% + 68px);height:calc(100% + 68px);
  filter:blur(46px) saturate(1.45);opacity:.42;transform:scale(1.06);pointer-events:none;
  mask-image:radial-gradient(ellipse at center,#000 28%,rgba(0,0,0,.72) 55%,transparent 86%);-webkit-mask-image:radial-gradient(ellipse at center,#000 28%,rgba(0,0,0,.72) 55%,transparent 86%)}
.stage:not(.ambient-on) .ambientcanvas{display:none}
.vwrap>video,.vwrap>.video-js{position:relative;z-index:1}
.vwrap video{width:100%;max-height:76vh;display:block}
/* 计费来源点击前只显示拉流入口。显式 display:block 会覆盖浏览器的 hidden 默认样式，
   让尚未启动的播放器和入口各占一半，所以这里必须让隐藏视频完全退出布局。 */
.vwrap>video[hidden]{display:none}
/* 高度只由 16:9 和宽度决定，不设最低高度。给一个像素地板的话，窄屏下画面按
   object-fit:contain 缩在中间，上下各补一条黑边——414px 宽的视口上那是 112px 的空白，
   比它保护的东西更显眼。设置面板与统计面板本来就是 max-height 加内部滚动，播放器矮下来
   它们跟着矮，不会被裁。占位门同用一套判据，否则点开的一瞬间整页会往下跳一截。 */
.vwrap>.video-js{width:100%;height:auto;max-height:76vh;aspect-ratio:16/9;background:#000}
/* 画面层的高度只跟着外框走。上面那条 `.vwrap video` 的 76vh 是给裸 `<video>` 的，它同样
   命中 Video.js 里面这一枚：影院模式把外框放高之后，画面还停在 76vh，底下空出一条黑边。 */
.vwrap .video-js .vjs-tech{object-fit:contain;max-height:none}
.vwrap .video-js .vjs-big-play-button{left:50%;top:50%;width:56px;height:56px;margin:0;
  transform:translate(-50%,-50%);border:0;border-radius:50%;
  background:rgba(0,0,0,.3);box-shadow:none;line-height:56px}
.vwrap .video-js .vjs-big-play-button:hover,.vwrap .video-js .vjs-big-play-button:focus{background:rgba(255,255,255,.1)}
.vwrap .video-js .vjs-big-play-button .vjs-icon-placeholder:before{content:"";left:50%;top:50%;width:0;height:0;
  border-top:.72em solid transparent;border-bottom:.72em solid transparent;border-left:1.05em solid #fff;
  transform:translate(-38%,-50%)}
/* 中心提示圆自己就是画面中央的播放态入口，原生大播放键留着只会跟它抢同一块地方。 */
.vwrap .video-js:has(.vjs-peach-bezel) .vjs-big-play-button{display:none}
/* YouTube e937390a `FsY` spinner DOM 与当前四段动画；复用 Video.js 的 waiting/seeking 状态。 */
.vwrap .video-js .vjs-loading-spinner{z-index:18;left:50%;top:50%;width:64px;height:64px;margin:0;padding:0;border:0;border-radius:0;background:transparent;opacity:1;transform:translate(-50%,-50%)}
.vwrap .video-js .vjs-loading-spinner:before,.vwrap .video-js .vjs-loading-spinner:after{content:none}
.vjs-peach-spinner-container{pointer-events:none;position:absolute;width:100%;height:100%;top:0;left:0;animation:peach-spinner-linspin 1.5682352941176s linear infinite}
.vjs-peach-spinner-rotator{position:absolute;width:100%;height:100%;animation:peach-spinner-easespin 5332ms cubic-bezier(.4,0,.2,1) infinite both}
.vjs-peach-spinner-left,.vjs-peach-spinner-right{position:absolute;inset:0;overflow:hidden}.vjs-peach-spinner-left{right:49%}.vjs-peach-spinner-right{left:49%}
.vjs-peach-spinner-circle{box-sizing:border-box;position:absolute;width:200%;height:100%;border:6px solid #ddd;border-bottom-color:transparent;border-radius:50%}
.vjs-peach-spinner-left .vjs-peach-spinner-circle{left:0;right:-100%;border-right-color:transparent;animation:peach-spinner-left-spin 1333ms cubic-bezier(.4,0,.2,1) infinite both}
.vjs-peach-spinner-right .vjs-peach-spinner-circle{left:-100%;right:0;border-left-color:transparent;animation:peach-spinner-right-spin 1333ms cubic-bezier(.4,0,.2,1) infinite both}
@keyframes peach-spinner-linspin{to{transform:rotate(1turn)}}
@keyframes peach-spinner-easespin{12.5%{transform:rotate(135deg)}25%{transform:rotate(270deg)}37.5%{transform:rotate(405deg)}50%{transform:rotate(540deg)}62.5%{transform:rotate(675deg)}75%{transform:rotate(810deg)}87.5%{transform:rotate(945deg)}to{transform:rotate(3turn)}}
@keyframes peach-spinner-left-spin{0%{transform:rotate(130deg)}50%{transform:rotate(-5deg)}to{transform:rotate(130deg)}}
@keyframes peach-spinner-right-spin{0%{transform:rotate(-130deg)}50%{transform:rotate(5deg)}to{transform:rotate(-130deg)}}
/* YouTube `e937390a` 实测控制层：59px 控制栏、40px 左钮、右侧共享胶囊和 6px 进度命中区。 */
/* video.js 自带 Arial；播放器里的时间、菜单、加载速度都跟页面主字体走，不另起一套。 */
.vwrap .video-js{font-family:inherit}
.vwrap .video-js .vjs-control-bar{box-sizing:border-box;left:12px;right:12px;bottom:8px;width:auto;height:59px;
  padding:3px 0 0;display:flex;align-items:flex-end;border-radius:0;background:transparent;backdrop-filter:none}
.vwrap .video-js .vjs-control-bar>.vjs-play-control{position:relative;align-self:flex-end;flex:0 0 40px;width:40px;height:40px;margin:0 0 8px;
  border:0;border-radius:50%;background:rgba(0,0,0,.6);box-shadow:none;overflow:visible}
.vwrap .video-js .vjs-volume-panel>.vjs-mute-control .vjs-icon-placeholder:before,
.vwrap .video-js .vjs-peach-right-controls .vjs-icon-placeholder:before{display:grid;place-items:center;width:100%;height:100%;line-height:normal;filter:drop-shadow(0 0 1px rgba(0,0,0,.8))}
.vwrap .video-js [data-peach-explicit-icon]>.vjs-icon-placeholder:before,
.vwrap .video-js [data-peach-explicit-icon]>.vjs-icon-placeholder:after{display:none}
.vwrap .video-js .vjs-volume-panel>.vjs-mute-control[data-peach-explicit-icon]>.vjs-icon-placeholder{display:none!important}
.vjs-peach-control-icon{position:absolute;z-index:2;left:50%;top:50%;width:24px;height:24px;display:block;
  transform:translate(-50%,-50%) scale(1);fill:currentColor;stroke:none;filter:drop-shadow(0 0 1px rgba(0,0,0,.8));pointer-events:none;
  transition:transform .1s cubic-bezier(.05,0,0,1)}
.vwrap .video-js [data-peach-explicit-icon]:active>.vjs-peach-control-icon{transform:translate(-50%,-50%) scale(.9)}
/* 上游 delhi-modern 的播放键是 56px 圆里放一个 36px 的图标框（`svg{padding:10px}`），
   图标本体 26 个单位。Peach 的键是 40px，同比例给 26px 的框，画出来 18.6px。 */
.vwrap .video-js .vjs-control-bar>.vjs-play-control>.vjs-peach-control-icon{width:26px;height:26px}
/* 播放↔暂停靠插值同一条路径完成，不是换图：上游那两条路径的记号结构逐位相同（233 个记号、
   116 个数字，命令序列都是 MLCCLCCLCCLCCZMLCCVCCLCCVLCCZ），所以浏览器插 `d` 得到的中间
   形状就是上游 `eST` 自己插出来的那些。不支持插值 `d` 的浏览器退回瞬间切换。 */
.vjs-peach-morph-icon path{transition:d .2s cubic-bezier(.4,0,.2,1)}
/* 两道弧的缩放中心写在变换里，所以 `transform-origin` 必须归零：默认的 50% 50% 会把中心
   整体挪一段，弧就不是缩进自己而是斜着飞出去。`transform-box:view-box` 让这里的 px
   等于视框单位，跟上游 `transform` 属性里的数字对得上。 */
.vjs-peach-morph-icon .vjs-peach-volume-arc-inner,
.vjs-peach-morph-icon .vjs-peach-volume-arc-outer{transform-box:view-box;transform-origin:0 0;
  transition:transform .25s cubic-bezier(.4,0,.2,1)}
.vjs-peach-morph-icon[data-silent="true"] .vjs-peach-volume-arc-inner{transform:translate(18px,12px) scale(0) translate(-18px,-12px)}
.vjs-peach-morph-icon[data-silent="true"] .vjs-peach-volume-arc-outer,
.vjs-peach-morph-icon[data-loud="false"] .vjs-peach-volume-arc-outer{transform:translate(22px,12px) scale(0) translate(-22px,-12px)}
/* 弧缩完那一刻整块换成静音那张图标，和上游 `jjc` 走到 q===1 时替换整个 svg 一样：时长 0、
   延迟 250ms，实心喇叭让位，挖空的喇叭和叉号顶上。挖空那条的外框与实心那条是同一串数字，
   所以换的那一刻只有中间的洞出现，轮廓一动不动。取消静音走没有延迟的那一条，洞和叉号
   立刻消失、弧再长回来。 */
.vjs-peach-morph-icon .vjs-peach-volume-speaker{opacity:1;transition:opacity 0s linear}
.vjs-peach-morph-icon .vjs-peach-volume-speaker-muted,
.vjs-peach-morph-icon .vjs-peach-volume-x{opacity:0;transition:opacity 0s linear}
.vjs-peach-morph-icon[data-silent="true"] .vjs-peach-volume-speaker{opacity:0;transition:opacity 0s linear .25s}
.vjs-peach-morph-icon[data-silent="true"] .vjs-peach-volume-speaker-muted,
.vjs-peach-morph-icon[data-silent="true"] .vjs-peach-volume-x{opacity:1;transition:opacity 0s linear .25s}
.vjs-peach-hover{position:absolute;z-index:0;left:0;top:4px;width:100%;height:32px;border-radius:var(--pill-radius);pointer-events:none;
  background:transparent;transition:background .2s cubic-bezier(.05,0,0,1)}
/* 40px 的圆形播放键里再铺一层 40×32 的胶囊，hover 时亮起来的是个两头圆的方块，
   和键本身的圆形对不上。圆键的高亮层统一是内缩 4px 的同心圆——音量键、时间钮的
   `:after{inset:4px}` 早就是这个样子，播放键、左上统计钮、右上关闭钮跟着走同一套。 */
.vwrap .video-js .vjs-control-bar>.vjs-play-control>.vjs-peach-hover{inset:4px;width:auto;height:auto;border-radius:50%}
.vwrap .video-js .vjs-control-bar>.vjs-play-control:hover>.vjs-peach-hover,
.vwrap .video-js .vjs-control-bar>.vjs-play-control:focus-visible>.vjs-peach-hover,
.vjs-peach-right-controls>.vjs-control:hover>.vjs-peach-hover,
.vjs-peach-right-controls>.vjs-control:focus-within>.vjs-peach-hover{background:rgba(255,255,255,.1)}
.vwrap .video-js .vjs-control-bar>.vjs-play-control:active>.vjs-peach-hover,
.vjs-peach-right-controls>.vjs-control:active>.vjs-peach-hover{background:rgba(255,255,255,.2)}
.vwrap .video-js .vjs-control-bar>.vjs-play-control .vjs-icon-placeholder,
.vwrap .video-js .vjs-peach-right-controls button,.vjs-peach-settings>button,.vjs-peach-theater>button{position:relative;z-index:1}
/* 音量键和左边的播放键、右边的胶囊同一排，底色必须同一档 .6；之前是 .3，
   一排三个黑底就它一个发灰。 */
.vwrap .video-js .vjs-control-bar>.vjs-volume-panel{box-sizing:border-box;z-index:3;position:relative;align-self:flex-end;flex:0 0 40px;width:40px;height:40px!important;
  margin:0 0 8px 12px;padding:0;border:0;border-radius:var(--pill-radius);background:rgba(0,0,0,.6);box-shadow:none;
  backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
  display:grid;grid-template-columns:40px 0;column-gap:0;align-items:center;isolation:isolate;overflow:visible;transition:width .2s cubic-bezier(.05,0,0,1),flex-basis .2s cubic-bezier(.05,0,0,1),padding-right .2s cubic-bezier(.05,0,0,1)}
.vwrap .video-js .vjs-control-bar>.vjs-volume-panel:after{content:"";position:absolute;z-index:0;inset:4px;border-radius:var(--pill-radius);background:transparent;pointer-events:none;transition:background .2s cubic-bezier(.05,0,0,1)}
.vwrap .video-js .vjs-control-bar>.vjs-volume-panel:hover:after,
.vwrap .video-js .vjs-control-bar>.vjs-volume-panel:focus-within:after,
.vwrap .video-js .vjs-control-bar>.vjs-volume-panel.vjs-hover:after,
.vwrap .video-js .vjs-control-bar>.vjs-volume-panel.vjs-slider-active:after{background:rgba(255,255,255,.1)}
.vwrap .video-js .vjs-control-bar>.vjs-volume-panel:active:after{background:rgba(255,255,255,.2)}
.vwrap .video-js .vjs-control-bar>.vjs-volume-panel:hover,
.vwrap .video-js .vjs-control-bar>.vjs-volume-panel:focus-within,
.vwrap .video-js .vjs-control-bar>.vjs-volume-panel.vjs-hover,
.vwrap .video-js .vjs-control-bar>.vjs-volume-panel.vjs-slider-active{flex-basis:111px;width:111px;grid-template-columns:40px 52px;column-gap:3px;padding-right:16px;background:rgba(0,0,0,.6)}
.stage.theater-mode .vwrap .video-js .vjs-control-bar>.vjs-volume-panel,
.video-js.vjs-fullscreen .vjs-control-bar>.vjs-volume-panel{background:rgba(0,0,0,.6)!important;
  backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px)}
.vwrap .video-js .vjs-volume-panel>.vjs-mute-control{box-sizing:border-box;z-index:1;position:relative!important;left:0!important;top:0!important;align-self:center;flex:0 0 40px;width:40px!important;min-width:40px;height:40px!important;margin:0!important;padding:0!important;border:0;background:transparent;transform:none!important}
.vwrap .video-js .vjs-volume-panel>.vjs-mute-control>.vjs-peach-control-icon{width:18px;height:18px}
.vwrap .video-js .vjs-volume-panel>.vjs-volume-control.vjs-volume-horizontal{box-sizing:border-box;z-index:1;position:relative!important;left:0!important;top:0!important;display:block!important;align-self:center;flex:0 0 52px;width:52px!important;min-width:52px;height:40px!important;min-height:36px;margin:0!important;padding:0!important;overflow:visible;opacity:0;visibility:hidden;pointer-events:none;transition:opacity .1s cubic-bezier(0,0,.2,1)}
.vwrap .video-js .vjs-volume-panel:hover>.vjs-volume-control.vjs-volume-horizontal,
.vwrap .video-js .vjs-volume-panel:focus-within>.vjs-volume-control.vjs-volume-horizontal,
.vwrap .video-js .vjs-volume-panel.vjs-hover>.vjs-volume-control.vjs-volume-horizontal,
.vwrap .video-js .vjs-volume-panel.vjs-slider-active>.vjs-volume-control.vjs-volume-horizontal{opacity:1;visibility:visible;pointer-events:auto;transition-delay:.08s}
/* 滑轨本体撑满面板那 40px 高：Video.js 只在滑轨元素上接鼠标，画出来的那条 2px 线要是
   就是滑轨，鼠标得正好压在线上才点得到。线和进度各画在竖直中线上，滑轨自己透明。 */
.vwrap .video-js .vjs-volume-panel .vjs-volume-bar.vjs-slider-horizontal{box-sizing:border-box;position:absolute!important;left:0!important;right:auto!important;top:0!important;width:52px!important;height:40px!important;margin:0!important;overflow:visible!important;background:transparent;transform:none}
.vwrap .video-js .vjs-volume-panel .vjs-volume-bar.vjs-slider-horizontal::before{content:"";position:absolute;left:0;right:0;top:50%;height:2px;margin-top:-1px;background:rgba(255,255,255,.3)}
.vwrap .video-js .vjs-volume-panel .vjs-volume-level{top:50%;bottom:auto;height:2px;margin-top:-1px;background:#fff}
/* Video.js 在鼠标所在的位置画一条 1px 黑线、高度随滑轨，滑轨撑满面板后它成了一整条竖杠。
   位置已经由上面那枚数字说了，线本身只去掉底色，数字照旧挂在它身上。 */
.vwrap .video-js .vjs-volume-panel .vjs-mouse-display{background:transparent}
.vwrap .video-js .vjs-volume-panel .vjs-volume-level:before{top:50%;right:-6px;width:12px;height:12px;margin:0;font-size:0;border-radius:50%;background:#fff;transform:translateY(-50%)}
/* `right:auto` 必须带 !important：Video.js 的 VolumeLevelTooltip 每 30ms 往元素上写一句
   行内 `style.right=-Npx`，行内声明压过样式表里同名的普通声明。`left:50%` 和那个 right
   同时成立时，绝对定位的宽度就由两边反推，不再收缩到文字宽——底色会摊成一块比数字大得多
   的方块，而且它下一次测量自己时又按新宽度算 N，尺寸每帧都在飘。 */
.vwrap .video-js .vjs-volume-panel .vjs-volume-tooltip{z-index:5!important;left:50%;right:auto!important;top:auto;bottom:calc(50% + 31px);
  float:none;transform:translateX(-50%);padding:5px 9px;border-radius:var(--surface-radius);background:rgba(0,0,0,.6);
  backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);color:#fff;font-family:inherit;font-size:var(--fs-sm);font-weight:500;
  line-height:15px;text-shadow:0 0 2px #000;overflow:visible;white-space:nowrap}
.vjs-peach-right-controls{box-sizing:border-box;position:relative;align-self:flex-end;flex:0 0 auto;height:40px;margin:0 0 8px;
  padding:0 4px;display:flex;align-items:center;border:0;border-radius:var(--pill-radius);background:rgba(0,0,0,.6);box-shadow:none;
  overflow:visible;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px)}
.vwrap .video-js .vjs-peach-right-controls>.vjs-control{box-sizing:border-box;position:relative;flex:0 0 48px;width:48px!important;min-width:48px;height:40px!important;
  margin:0;padding:0;border:0;border-radius:0;background:transparent;box-shadow:none;color:#fff}
.vwrap .video-js .vjs-peach-right-controls>.vjs-control>button{width:100%;height:100%;margin:0;padding:0;border:0;background:transparent;color:inherit}
.vwrap .video-js .vjs-peach-right-controls>.vjs-control:focus,
.vwrap .video-js .vjs-peach-right-controls>.vjs-control:focus-visible,
.vwrap .video-js .vjs-peach-right-controls>.vjs-control>button:focus,
.vwrap .video-js .vjs-peach-right-controls>.vjs-control>button:focus-visible{outline:0;box-shadow:none}
/* 命中区比看得见的那条粗得多：彩条只有 6px，要在 6px 里对准才能拖动进度。上游
   YouTube 的 `.ytp-progress-bar-container` 同样把可点高度留得比彩条高，而多出来的那
   一截全在条上方——往下扩会盖住按钮那一排的顶边，读起来就是按钮时灵时不灵。Video.js
   的 `ProgressControl` 收到落在自己身上的 mousedown 就转交给 seek bar，横向落点仍按
   彩条自己的宽度算，所以只加高不会让进度跳到别处。 */
.vwrap .video-js .vjs-progress-control{z-index:2;position:absolute;left:0;right:0;top:-12px;width:auto;height:18px;
  min-width:0;display:flex;align-items:flex-end}
.vwrap .video-js .vjs-progress-control .vjs-progress-holder{width:100%;height:6px;margin:0;border-radius:0;background:rgba(40,40,40,.6);transform:scaleY(.667);transition:transform .2s cubic-bezier(.05,0,0,1)}
.vwrap .video-js .vjs-progress-control:hover .vjs-progress-holder,.vwrap .video-js .vjs-progress-control:focus-within .vjs-progress-holder{transform:scaleY(1)}
.vwrap .video-js .vjs-load-progress{background:rgba(255,255,255,.4)}
.vwrap .video-js .vjs-load-progress div{background:rgba(255,255,255,.2)}
.vwrap .video-js .vjs-play-progress{background:var(--tungsten)}
/* 圆点常驻，只在悬停时长大：上游 `.ytp-delhi-modern .ytp-scrubber-button` 静止就是 12px 的圆，
   只有 `.ytp-hide-scrubber-button` 才缩成 0。上游的滑块是被压扁那层的兄弟，Peach 的圆点长在
   `.vjs-play-progress` 里，父层的 `scaleY(.667)` 会连它一起压扁，所以静止时纵向先撑回去；
   悬停父层回到 1，圆点按上游的 1.67 长大。 */
.vwrap .video-js .vjs-play-progress:before{content:"";position:absolute;right:-6px;top:50%;width:12px;height:12px;
  border-radius:50%;background:var(--tungsten);transform:translateY(-50%) scale(1,1.5);box-shadow:none;transition:transform .2s cubic-bezier(.05,0,0,1)}
.vwrap .video-js .vjs-progress-control:hover .vjs-play-progress:before,.vwrap .video-js .vjs-progress-control:focus-within .vjs-play-progress:before{transform:translateY(-50%) scale(1.67)}
.vwrap .video-js .vjs-mouse-display .vjs-time-tooltip,
.vwrap .video-js .vjs-play-progress .vjs-time-tooltip{display:none!important}
.vjs-peach-seek-preview{position:absolute;z-index:4;bottom:calc(100% - 2px);transform:translateX(-50%);
  display:flex;flex-direction:column;align-items:center;pointer-events:none;color:#fff}
.vjs-peach-seek-preview[hidden]{display:none}
/* 取景框自己裁，图在里面挪。时间轴预览取回来的是一张 10×10 的接触印相，要看的那一格
   靠把图放大十倍再往左上推整格数得到；边框和圆角画在框上，图挪动时不跟着走。 */
.vjs-peach-seek-frame{position:relative;width:240px;aspect-ratio:16/9;display:block;overflow:hidden;
  border:2px solid #fff;border-radius:var(--floating-radius);background:#000;box-shadow:0 8px 24px #0009}
.vjs-peach-seek-frame[hidden]{display:none}
.vjs-peach-seek-preview img{position:absolute;left:0;top:0;width:100%;height:100%;display:block;object-fit:cover}
.vjs-peach-seek-preview span{min-width:58px;margin-top:8px;padding:6px 10px;border-radius:var(--pill-radius);
  background:rgba(0,0,0,.68);font-size:var(--fs-sm);font-weight:600;line-height:1;text-align:center;box-shadow:0 2px 8px #0006}
.vwrap .video-js .vjs-custom-control-spacer{display:block;flex:1 1 auto}
.vwrap .video-js .vjs-time-control{display:none!important}
.vwrap .video-js .vjs-peach-time{box-sizing:border-box;align-self:flex-end;flex:0 0 auto;width:auto;height:40px;margin:0 0 8px 12px;
  padding:0 16px;border:0;border-radius:var(--pill-radius);background:rgba(0,0,0,.6);color:#fff;cursor:pointer;
  position:relative;isolation:isolate;overflow:visible;font-weight:400;font-size:var(--fs-md);line-height:40px;font-family:inherit;font-variant-numeric:tabular-nums;white-space:nowrap;text-shadow:0 0 1px rgba(0,0,0,.8)}
.vwrap .video-js .vjs-peach-time:after{content:"";position:absolute;z-index:0;inset:4px;border-radius:var(--pill-radius);background:transparent;transition:background .2s cubic-bezier(.05,0,0,1)}
.vwrap .video-js .vjs-peach-time:hover:after,.vwrap .video-js .vjs-peach-time:focus-visible:after{background:rgba(255,255,255,.1)}
.vwrap .video-js .vjs-peach-time:active:after{background:rgba(255,255,255,.2)}
.vwrap .video-js .vjs-peach-time:focus-visible{outline:0}.vjs-peach-time-text{position:relative;z-index:1}
.vjs-peach-settings,.vjs-peach-theater,.vjs-peach-expand{position:relative;flex:0 0 48px;width:48px!important;min-width:48px}
.vjs-peach-settings>button,.vjs-peach-theater>button,.vjs-peach-expand>button{width:100%;height:100%;padding:0;border:0;background:transparent;color:inherit;cursor:pointer;display:grid;place-items:center}
.vjs-peach-expand>button>svg{width:24px;height:24px;fill:currentColor;stroke:none;filter:drop-shadow(0 0 1px rgba(0,0,0,.8))}
.vjs-peach-settings>button>svg,.vjs-peach-theater>button>svg{width:24px;height:24px;stroke:currentColor;fill:none;stroke-width:2;filter:drop-shadow(0 0 1px rgba(0,0,0,.8))}
.vjs-peach-settings [data-player-quality-badge]{position:absolute;z-index:2;right:4px;top:1px;min-width:17px;height:12px;padding:0 2px;
  display:grid;place-items:center;border-radius:var(--badge-radius);background:#f03;color:#fff;font-size:var(--fs-xs);font-weight:600;line-height:1;transform:scale(.67);transform-origin:top right}
.vjs-peach-settings [data-player-quality-badge][hidden]{display:none}
.vjs-peach-settings-menu{box-sizing:border-box;position:absolute;z-index:2300;right:-100px;bottom:52px;width:min(274px,calc(100vw - 28px));max-height:min(420px,calc(100vh - 130px));overflow:auto;
  padding:0;border:0;border-radius:var(--floating-radius);background:rgba(0,0,0,.6);box-shadow:none;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
  color:#fff;text-shadow:0 0 1px rgba(0,0,0,.8);font-weight:400;font-size:var(--fs-md);line-height:1.3;font-family:inherit}
/* 面板的显隐照 YouTube 播放器 9470c977 的 www-player.css：`.ytp-popup` 淡入 .1s
   cubic-bezier(0,0,.2,1)，`[aria-hidden=true]` 淡出 .1s cubic-bezier(.4,0,1,1)。
   所以关闭不能用 display:none——那样没有可过渡的中间态。visibility 延后到淡出结束，
   面板既退出无障碍树也不再接命中测试。 */
.vjs-peach-settings-menu{opacity:1;visibility:visible;transition:opacity .1s cubic-bezier(0,0,.2,1)}
.vjs-peach-settings-menu[aria-hidden="true"]{opacity:0;visibility:hidden;pointer-events:none;
  transition:opacity .1s cubic-bezier(.4,0,1,1),visibility 0s .1s}
/* 次级菜单同一份来源：`.ytp-popup-animating` 与其面板都是 .25s cubic-bezier(.4,0,.2,1)，
   `.ytp-panel-animate-back/forward` 是 translateX(∓100%) 加透明度 0。 */
.vjs-peach-settings-menu.vjs-peach-popup-animating{overflow:hidden;pointer-events:none;transition:height .25s cubic-bezier(.4,0,.2,1)}
.vjs-peach-panel{position:relative}
.vjs-peach-popup-animating .vjs-peach-panel{transition:transform .25s cubic-bezier(.4,0,.2,1),opacity .25s cubic-bezier(.4,0,.2,1)}
.vjs-peach-panel-leaving{position:absolute;left:0;top:0;width:100%}
.vjs-peach-panel-animate-back{opacity:0;transform:translateX(-100%)}
.vjs-peach-panel-animate-forward{opacity:0;transform:translateX(100%)}
.vjs-peach-settings-menu button{box-sizing:border-box;position:relative;isolation:isolate;overflow:hidden;width:100%;min-height:48px;padding:0;border:0;border-radius:var(--control-radius);background:transparent;color:#fff;font:inherit;cursor:pointer}
.vjs-peach-settings-menu button:before{content:"";position:absolute;z-index:0;inset:0;border-radius:inherit;background:transparent;pointer-events:none;transition:background .1s cubic-bezier(.05,0,0,1)}
.vjs-peach-settings-menu button:hover:before,.vjs-peach-settings-menu button:focus-visible:before{background:rgba(255,255,255,.1)}
.vjs-peach-settings-menu button:active:before{background:rgba(255,255,255,.2)}
.vjs-peach-settings-menu button:focus-visible{outline:0}
.vjs-peach-settings-menu button>*{position:relative;z-index:1}
.video-js .vjs-peach-menu-row{display:grid;grid-template-columns:56px minmax(0,1fr) minmax(0,max-content) 32px;gap:0;align-items:center;padding:0!important;text-align:left}
.vjs-peach-menu-row>svg{justify-self:start;margin-left:8px;width:24px;height:24px;fill:currentColor;stroke:none}.vjs-peach-menu-row>span{font-size:var(--fs-md);line-height:18.2px}.vjs-peach-menu-row>b{padding-left:8px;font-size:var(--fs-md);line-height:18.2px;font-weight:400;color:rgba(255,255,255,.7);white-space:nowrap}
.vjs-peach-menu-row>svg:last-child{justify-self:center;margin:0;width:18px;height:18px;color:rgba(255,255,255,.7);fill:currentColor;stroke:none}
.vjs-peach-menu-row[data-player-ambient]{grid-template-columns:56px minmax(0,1fr) auto;padding-right:8px!important}.vjs-peach-switch{box-sizing:border-box;display:block;position:relative;width:40px;height:24px;border-radius:var(--floating-radius);background:rgba(0,0,0,.3)}
.vjs-peach-switch:after{content:"";position:absolute;top:2px;left:2px;width:20px;height:20px;border-radius:50%;background:rgba(255,255,255,.7);box-shadow:none;transition:transform .2s cubic-bezier(.05,0,0,1),background .2s cubic-bezier(.05,0,0,1)}
.vjs-peach-menu-row[aria-checked="true"] .vjs-peach-switch{background:rgba(255,255,255,.3)}.vjs-peach-menu-row[aria-checked="true"] .vjs-peach-switch:after{background:#fff;transform:translateX(16px)}
.vjs-peach-panel-header{box-sizing:border-box;height:57px;padding:8px 0;display:flex;align-items:center;gap:0;border-bottom:1px solid rgba(255,255,255,.2);color:#eee;white-space:nowrap}
.vjs-peach-panel-header .vjs-peach-menu-back{flex:0 0 48px;width:48px;min-height:48px;height:48px!important;display:grid;place-items:center;border:0!important;border-radius:50%!important;color:#eee}
.vjs-peach-settings-menu .vjs-peach-panel-header .vjs-peach-menu-back:before{inset:4px}
.vjs-peach-panel-header .vjs-peach-menu-back svg{width:32px;height:32px;display:block;fill:currentColor;stroke:none}
.vjs-peach-panel-header strong{font-size:var(--fs-md);line-height:18.2px;font-weight:500;color:#eee}
.vjs-peach-panel-menu{padding:8px}
.video-js .vjs-peach-menu-option{display:grid;grid-template-columns:35px minmax(0,1fr);align-items:center;padding:0 20px 0 0!important;text-align:left;font-size:var(--fs-md)!important;line-height:18.2px}
.vjs-peach-option-check{width:35px;height:24px;display:grid;place-items:center}.vjs-peach-option-check svg{width:24px;height:24px;display:block;fill:currentColor;stroke:none}
.vjs-peach-option-label{min-width:0}
/* 播放速度面板照 YouTube delhi-modern（player 9470c977 的 www-player.css 与 base.js）：
   内容区 24px/16px/16px 内距；读数 18px/900/22px 居中、下留 24px；滑条一行 gap 16px、
   下留 24px，加减键 32px 圆、24px 字，各动 0.05；预设胶囊 53×32、圆角 16、12px 字、gap 8px，
   底色 rgba(255,255,255,.1)、悬停 .2、按下 .3，过渡 .2s cubic-bezier(.05,0,0,1)；
   1.0 下面挂一行 14px 行高的说明。滑条轨道 4px 高，已过部分 #fff、余下 #909090，
   把手 16px 白圆上移 6px；已过比例由脚本写进自定义属性，上游同样是自定义属性驱动。
   字号、字重和圆角走 Peach 自己的 token：上游读数那档 18px/900 和说明那档 10px 都不在
   Peach 的字号刻度上，900 也不在 Geist 的三档字重里；胶囊和轨道的圆角本来就大于自身高度的
   一半，`--pill-radius` 渲染出来与上游那两个字面值一模一样。 */
.vjs-peach-speed-panel{box-sizing:border-box;display:flex;flex-direction:column;padding:24px 16px 16px}
.vjs-peach-speed-display{display:flex;justify-content:center;margin-bottom:24px;
  font-size:var(--fs-lg);font-weight:600;line-height:22px;color:#fff}
.vjs-peach-speed-slider{display:flex;align-items:center;gap:16px;margin-bottom:24px}
.vjs-peach-speed-chips{display:flex;align-items:flex-start;gap:8px}
.vjs-peach-speed-preset{display:flex;flex:0 1 53px;min-width:0;flex-direction:column;align-items:center}
.vjs-peach-speed-preset-label{margin-top:4px;font-size:var(--fs-xs);font-weight:400;line-height:14px;color:rgba(255,255,255,.7)}
.vjs-peach-settings-menu .vjs-peach-speed-button{display:flex;justify-content:center;align-items:center;
  height:32px;min-height:32px;padding:0;border:0;border-radius:var(--pill-radius);background:rgba(255,255,255,.1);
  color:#fff;font-weight:500;line-height:16px;text-align:center;cursor:pointer;
  transition:background .2s cubic-bezier(.05,0,0,1)}
.vjs-peach-settings-menu .vjs-peach-speed-button:before{content:none}
.vjs-peach-settings-menu .vjs-peach-speed-button:hover{background:rgba(255,255,255,.2)}
.vjs-peach-settings-menu .vjs-peach-speed-button:active{background:rgba(255,255,255,.3)}
/* 加减两键画的是图标不是字形：`−` 与 `+` 的墨迹绕数学轴排布，行盒居中之后实测还偏
   下 3.0px、偏左 1.8px（32px 圆里 24px Roboto，行高 16px），而减号的墨迹本身只有 2px 高，
   这点位移一眼就看得见。文字没有按墨迹居中的写法，换成 24px 图标就是几何居中。 */
.vjs-peach-speed-slider .vjs-peach-speed-button{flex:none;width:32px}
.vjs-peach-speed-slider .vjs-peach-speed-button>svg{width:24px;height:24px;display:block;
  stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round}
.vjs-peach-speed-chips .vjs-peach-speed-button{width:100%;gap:4px;font-size:var(--fs-xs)}
.vjs-peach-speed-range{--peach-speed-percent:0%;-webkit-appearance:none;appearance:none;flex:1 1 auto;
  width:100%;min-width:0;height:24px;margin:0;background:transparent;cursor:pointer}
.vjs-peach-speed-range:focus-visible{outline:0}
.vjs-peach-speed-range::-webkit-slider-runnable-track{height:4px;border-radius:var(--pill-radius);
  background:linear-gradient(to right,#fff 0,#fff var(--peach-speed-percent),#909090 var(--peach-speed-percent),#909090 100%)}
.vjs-peach-speed-range::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:16px;height:16px;
  margin-top:-6px;border-radius:50%;background:#fff}
.vjs-peach-speed-range::-moz-range-track{height:4px;border-radius:var(--pill-radius);
  background:linear-gradient(to right,#fff 0,#fff var(--peach-speed-percent),#909090 var(--peach-speed-percent),#909090 100%)}
.vjs-peach-speed-range::-moz-range-thumb{width:16px;height:16px;border:0;border-radius:50%;background:#fff}
/* 320px 高的播放器只给面板 246px：内距和两处 24px 间隔都收到 16px 就是 225px。 */
.video-js.vjs-peach-xsmall .vjs-peach-speed-panel{padding:16px}
.video-js.vjs-peach-xsmall .vjs-peach-speed-display,
.video-js.vjs-peach-xsmall .vjs-peach-speed-slider{margin-bottom:16px}
/* 中心提示照同一份 www-player.css：`.ytp-bezel` 78px 圆、blur(16px)、rgba(0,0,0,.6)，
   图标 54px；窄屏收到 64px 圆配 48px 图标。动画 1s cubic-bezier(.05,0,0,1) 走
   0→1.33→1 的缩放淡出，`role="status"` 带这一次的动作名。 */
.vjs-peach-bezel{position:absolute;z-index:19;left:50%;top:50%;width:78px;height:78px;margin:-39px 0 0 -39px;
  display:grid;place-items:center;border-radius:50%;background:rgba(0,0,0,.6);
  backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);color:#fff;opacity:0;pointer-events:none}
.vjs-peach-bezel[hidden]{display:none}
.vjs-peach-bezel-icon{display:grid;place-items:center;width:54px;height:54px}
.vjs-peach-bezel-icon svg{width:100%;height:100%;display:block;fill:currentColor;stroke:none}
.vjs-peach-bezel-run{animation:peach-bezel-fadeout 1s cubic-bezier(.05,0,0,1) 1 normal forwards}
@keyframes peach-bezel-fadeout{0%{opacity:0}25%,75%{opacity:1;transform:scale(1.33)}to{opacity:0;transform:scale(1)}}
.video-js.vjs-peach-xsmall .vjs-peach-bezel{width:64px;height:64px;margin:-32px 0 0 -32px}
.video-js.vjs-peach-xsmall .vjs-peach-bezel-icon{width:48px;height:48px}
/* 转圈和报错卡片都停在同一个圆心上：提示圆还在淡出时它们就位，两块东西会叠着画。 */
.video-js.vjs-waiting .vjs-peach-bezel,.video-js.vjs-seeking .vjs-peach-bezel,
.video-js.vjs-error .vjs-peach-bezel{display:none}
/* 悬停提示取自 YouTube delhi-modern（player 9470c977 的 www-player.css）：毛玻璃 blur(16px)、
   8px 圆角（--surface-radius）、5px 9px 内距、13px/15px 500 字重（--fs-sm）、
   text-shadow 0 0 2px #000；快捷键徽标 1px rgba(255,255,255,.3) 边、4px 圆角（--badge-radius）、
   最小宽 11px、左外边距 4px，空了就收起。控制条上每个按钮共用这一套。
   底色是 Peach 主动偏离上游的一处：上游 rgba(0,0,0,.3) 加 blur 落在亮画面上只剩一块低对比
   灰板，悬停时看着像凭空多出来一块阴影。提示和播放器其它悬浮件同档，一律 rgba(0,0,0,.6)。 */
.vjs-peach-tooltip{position:absolute;z-index:5;right:50%;bottom:calc(100% + 12px);transform:translateX(50%);
  display:none;align-items:center;padding:5px 9px;border-radius:var(--surface-radius);background:rgba(0,0,0,.6);
  backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);color:#fff;white-space:nowrap;
  font-size:var(--fs-sm);font-weight:500;line-height:15px;text-shadow:0 0 2px #000;pointer-events:none}
.vjs-peach-tooltip,.vjs-volume-tooltip{--surface-radius:8px;--badge-radius:4px}
.vjs-peach-tooltip kbd{display:flex;justify-content:center;align-items:center;min-width:11px;margin-left:4px;padding:0 2px;
  border:1px solid rgba(255,255,255,.3);border-radius:var(--badge-radius);color:#fff;font:inherit}
.vjs-peach-tooltip kbd[hidden]{display:none}
.vwrap .video-js .vjs-control-bar button:hover>.vjs-peach-tooltip,
.vwrap .video-js .vjs-control-bar button:focus-visible>.vjs-peach-tooltip{display:flex}
.vjs-peach-theater>button[aria-pressed="true"]{color:#fff}
@media (max-width:640px){
  .vwrap .video-js .vjs-control-bar{left:8px;right:8px;bottom:6px;height:59px;padding:3px 0 0}
  .vwrap .video-js .vjs-progress-control,.vwrap .video-js.vjs-layout-tiny .vjs-progress-control,
  .vwrap .video-js.vjs-layout-x-small .vjs-progress-control{left:0;right:0;top:-12px;height:18px;display:flex}
  .vwrap .video-js.vjs-layout-small .vjs-current-time,.vwrap .video-js.vjs-layout-small .vjs-duration,
  .vwrap .video-js.vjs-layout-small .vjs-time-divider{display:flex}
  .vwrap .video-js .vjs-time-control{font-size:var(--fs-xs)}
  .vjs-peach-seek-frame{width:160px}
  .vjs-peach-settings-menu{right:-100px;width:min(274px,calc(100vw - 16px));border-radius:var(--floating-radius)}
}
/* 窄屏梯度照搬 YouTube delhi-modern：base.js 9470c977 里 `v.width<528` 打开 ytp-xsmall-width-mode，
   右侧只留设置键和展开键，按钮从 48 收到 36、图标 24 收到 18；展开后铺开全部按钮。
   YouTube 展开时把时间显示改成 visibility:hidden 占着位，Peach 改成 display:none 让出宽度——
   它的控制条比 YouTube 窄，留着那段空位就又超框了。 */
.vjs-peach-expand{display:none}
.video-js.vjs-peach-xsmall .vjs-peach-right-controls>.vjs-control{flex:0 0 36px;width:36px!important;min-width:36px}
.video-js.vjs-peach-xsmall .vjs-peach-right-controls .vjs-peach-control-icon,
.video-js.vjs-peach-xsmall .vjs-peach-right-controls>.vjs-control:not(.vjs-peach-expand)>button>svg{width:18px;height:18px}
/* 窄屏只有展开键不缩：YouTube 那边其余按钮的 svg 是 18px 加 7px 内边距，
   `.ytp-expand-right-bottom-section-button` 的 svg 内边距是 0，整块 32px 铺满按钮。
   跟着缩到 18px 的话，这个箭头在窄屏上几乎看不出是个可点的键。 */
.video-js.vjs-peach-xsmall .vjs-peach-expand>button>svg{width:32px;height:32px}
/* 窄屏的悬停底照 `.ytp-delhi-modern.ytp-xsmall-width-mode .ytp-chrome-bottom .ytp-button:before`：
   32×32 的正圆。上游按钮本体 32px 配 4px 2px 外边距，正好是这里 40px 行高里 36px 的一格。 */
.video-js.vjs-peach-xsmall .vjs-peach-right-controls>.vjs-control>.vjs-peach-hover{
  left:2px;width:32px;height:32px;border-radius:50%}
/* 折叠态里设置键就是这排的右端，`right:-100px` 是给它右边还有影院和全屏时留的位；
   窄屏照原样偏移会把整个面板推到播放器外面。贴住设置键右缘往左铺，高度按播放器
   自己的高度收顶（`--peach-player-h` 由布局脚本写在播放器元素上）。 */
.video-js.vjs-peach-xsmall .vjs-peach-settings-menu{right:0;width:min(274px,calc(100vw - 48px));
  max-height:calc(var(--peach-player-h,420px) - 74px)}
/* 面板底边离播放器底边 66px（离设置键 52px 加控制条那 14px），顶上再留 8px，所以收顶取
   播放器高度减 74px：320px 高的播放器给出 246px。八档播放速度单列要 57+16+8×48=457px，
   排两列、行高 44px 是 57+8+4×44=241px，最挤的那个面板也不用滚。 */
.video-js.vjs-peach-xsmall .vjs-peach-panel-menu{padding:4px 8px}
.video-js.vjs-peach-xsmall .vjs-peach-menu-option{min-height:44px}
.video-js.vjs-peach-xsmall .vjs-peach-panel-menu:has(>.vjs-peach-menu-option:nth-child(5)){display:grid;grid-template-columns:1fr 1fr}
.video-js.vjs-peach-xsmall .vjs-peach-right-controls>.vjs-control:not(.vjs-peach-settings):not(.vjs-peach-expand){display:none}
.video-js.vjs-peach-xsmall .vjs-peach-expand{display:block}
.video-js.vjs-peach-xsmall.vjs-peach-right-expanded .vjs-peach-right-controls>.vjs-control:not(.vjs-peach-settings):not(.vjs-peach-expand){display:block}
.video-js.vjs-peach-xsmall.vjs-peach-right-expanded .vjs-peach-time{display:none}
.vjs-peach-expand>button>svg{transition:transform .3s cubic-bezier(.05,0,0,1);transform:rotate(180deg)}
.video-js.vjs-peach-right-expanded .vjs-peach-expand>button>svg{transform:rotate(0)}
/* 报错文案本来就居中，压住它的是统计面板（z-index 8）。所以不改居中，改成一张自带底色、
   盖在统计面板上方的卡片，并撤掉 Video.js 那层铺满全画面的渐变——留着它就等于用报错
   把统计面板整个糊掉，而加载失败时正需要看统计里的编码和体积。 */
.vwrap .video-js.vjs-error .vjs-error-display{background:none}
.vwrap .video-js.vjs-error .vjs-error-display .vjs-modal-dialog-content{
  z-index:9;left:50%;top:50%;width:max-content;max-width:min(560px,calc(100% - 48px));height:auto;
  padding:14px 18px;border-radius:var(--surface-radius);background:rgba(2,4,8,.86);
  display:flex;align-items:center;justify-content:center;text-align:center;overflow-wrap:anywhere;
  transform:translate(-50%,-50%)}
.vwrap :is(.vwrap>.video-js.vjs-fullscreen,.video-js.vjs-fullscreen,.video-js[data-peach-fullscreen],body.vjs-full-window .video-js,.video-js:fullscreen,.video-js:-webkit-full-screen,.video-js:-moz-full-screen,.vwrap:fullscreen>.video-js,.vwrap:-webkit-full-screen>.video-js,.vwrap:-moz-full-screen>.video-js){position:fixed!important;inset:0!important;width:100vw!important;height:100vh!important;padding:0!important;
  max-width:none!important;max-height:none!important;aspect-ratio:auto!important;z-index:10000!important}
.vwrap :is(.vwrap>.video-js.vjs-fullscreen,.video-js.vjs-fullscreen,.video-js[data-peach-fullscreen],body.vjs-full-window .video-js,.video-js:fullscreen,.video-js:-webkit-full-screen,.video-js:-moz-full-screen,.vwrap:fullscreen>.video-js,.vwrap:-webkit-full-screen>.video-js,.vwrap:-moz-full-screen>.video-js) .vjs-tech{
  position:absolute!important;inset:0!important;width:100vw!important;height:100vh!important;max-width:none!important;max-height:none!important;object-fit:cover!important}
.playerstatsbtn{position:absolute;left:11px;top:11px;z-index:8;width:40px;height:40px;padding:0;
  display:grid;place-items:center;border:0;border-radius:50%;
  background:rgba(0,0,0,.6);color:#fff;cursor:pointer;box-shadow:none;isolation:isolate;overflow:hidden}
.playerstatsbtn[hidden]{display:none}
.playerstatsbtn:after,.closestage:after{content:"";position:absolute;z-index:0;inset:4px;border-radius:50%;background:transparent;pointer-events:none;transition:background .2s cubic-bezier(.05,0,0,1)}
.playerstatsbtn:hover:after,.playerstatsbtn:focus-visible:after,.closestage:hover:after,.closestage:focus-visible:after{background:rgba(255,255,255,.1)}
.playerstatsbtn:active:after,.closestage:active:after{background:rgba(255,255,255,.2)}
.playerstatsbtn:focus-visible,.closestage:focus-visible{outline:0}
.playerstatsbtn[aria-pressed="true"]{background:var(--hover);color:var(--ink)}
.playerstatsbtn svg{position:relative;z-index:1;width:20px;height:20px;stroke:currentColor;fill:none;stroke-width:2;filter:drop-shadow(0 0 1px rgba(0,0,0,.8))}
.playerstats{position:absolute;left:11px;top:58px;z-index:8;min-width:320px;max-width:min(520px,calc(100% - 22px));
  max-height:calc(100% - 114px);overflow-y:auto;overscroll-behavior:contain;padding:10px 12px;border:1px solid rgba(255,255,255,.18);border-radius:var(--surface-radius);
  background:rgba(2,4,8,.78);color:#eef3f8;box-shadow:0 12px 28px #0008;
  backdrop-filter:saturate(1.25) blur(14px);font-size:var(--fs-xs);line-height:1.5;font-family:inherit}
.playerstats dl{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:1px 12px;margin:0}.playerstats dt{text-align:right;font-weight:600}.playerstats dd{margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.playerstats dd.playerstatsmetric{display:grid;grid-template-columns:166px minmax(0,1fr);gap:9px;align-items:center;overflow:visible}
.playerstatsmetric>span{overflow:hidden;text-overflow:ellipsis}
.playerstatsplot{height:20px;padding:2px;display:grid;grid-template-columns:repeat(24,minmax(2px,1fr));align-items:end;gap:1px;background:#000;overflow:hidden}
.playerstatsplot i{display:block;min-height:0;background:transparent}
.playerstatsplot.speed i.active{background:#55b5bd}.playerstatsplot.activity i.active{background:#4ba8df}
.playerstatsplot.buffer i.active{background:#d8e98a}.playerstatsplot.buffer i.mid{background:#efb55f}.playerstatsplot.buffer i.low{background:#e16962}
.playerstats[hidden]{display:none}
@media(max-width:600px){.playerstats dd.playerstatsmetric{grid-template-columns:96px minmax(0,1fr)}}
.playernet{box-sizing:border-box;position:absolute;left:58px;top:11px;z-index:8;height:40px;min-height:40px;padding:0 11px;
  display:flex;align-items:center;gap:7px;white-space:nowrap;border:0;border-radius:var(--floating-radius);
  background:rgba(0,0,0,.6);color:#fff;box-shadow:none;backdrop-filter:blur(16px);font-size:var(--fs-xs);line-height:1;font-family:inherit}
/* 徽标里的图标必须自己声明描边：sprite 里的仪表盘是描边图形，不声明就按 SVG 默认的
   fill 画成黑色实心块，压在 rgba(0,0,0,.6) 的底上等于没有。速率也要按整体不换行，
   否则「640 KB/s」会在这条 flex 行里断成两行，把 40px 的胶囊顶破。 */
.playernet svg{width:18px;height:18px;flex:none;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round}
.playernet[hidden]{display:none}
.gate{aspect-ratio:16/9;width:100%;background:var(--sunk);border:1px dashed var(--meter);
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;cursor:pointer}
/* 占位说明的字号原先写在 openItem 的模板里当内联 style，三处各写一遍、
   连 `color:var(--muted)` 也抄了三份。 */
.gate b{font-size:var(--fs-md)}
.gate span{font-size:var(--fs-xs);color:var(--muted)}
/* 被门挡住的 <video> 只带 hidden 属性，而 `.video-js{display:block}` 是作者样式，
   压得住 UA 的 `[hidden]`。原先靠内联 `style="display:none"` 顶住，三个门里
   只有两个写了——第三个能对只是因为它那一支还没初始化成 `.video-js`。 */
.vwrap video[hidden]{display:none}
.closestage{position:absolute;top:11px;right:11px;z-index:9;width:40px;height:40px;
  background:rgba(0,0,0,.6);color:#F0E9DE;
  border:0;border-radius:50%;padding:0;cursor:pointer;box-shadow:none;
  display:grid;place-items:center;isolation:isolate;overflow:hidden}
.closestage svg{position:relative;z-index:1;width:20px;height:20px;stroke:currentColor;fill:none;stroke-width:2;filter:drop-shadow(0 0 1px rgba(0,0,0,.8))}
.side{min-width:0;min-height:0;align-self:stretch;border-left:1px solid var(--line-soft);overflow:hidden;
  background:var(--detail-surface);backdrop-filter:blur(18px)}
/* 背景属于整列，滚动属于内容。两者不能放在同一层，否则 76vh 会把在线占位、
   图片详情和合集详情右侧的背景一起截短。所有详情分支共用这层结构。 */
.sidecontent{box-sizing:border-box;width:100%;height:100%;max-height:76vh;padding:18px;display:flex;gap:0;
  flex-direction:column;overflow-y:auto;overflow-x:hidden}
.stitle{font-size:var(--fs-lg);line-height:1.4;word-break:break-all;margin-bottom:8px}
/* 标题默认折成两行：番号在前、正文在后，两行够认出是哪一部；正文动辄五六行，整段摊开
   会把评分、标签和动作全推到折叠线以下。真溢出时标题下面那排键最前面多一枚展开键，展开
   了换成收起；没溢出就不画它（判定在 app.js 里量）。来源徽标是行内块、站在第一行开头，
   随文字一起被折。那排键自成一行排在标题下面：夹在被折的文字末尾会一起被裁掉。 */
.detailtitle{display:block;margin-bottom:10px}
.detailtitle .srcbig{display:inline-grid;vertical-align:middle;width:17px;height:28px;margin:0 8px 0 0;place-items:center}
.detailtitle .stitle{min-width:0;margin:0;line-height:1.75}
.stitletext{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden}
.stitletext[data-expanded]{display:block;-webkit-line-clamp:unset;line-clamp:unset}
.stitletext[data-foldable]{cursor:pointer}
/* 源文件管理：定位 + 目录对账。图标必须显式写 stroke/fill，Lucide 是描边路径，
   缺了就被按默认 fill:black 画成黑块（详见 .photonav 那一段的同类修复）。 */
.srctools{display:flex;align-items:center;gap:6px;margin:0 0 10px;flex-wrap:wrap}
.srctools button{width:26px;height:26px;display:grid;place-items:center;flex:none;
  border:1px solid var(--line);border-radius:var(--control-radius);background:transparent;color:var(--muted);
  padding:0;cursor:pointer}
.srctools button svg{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:2}
.srctools button:hover{background:var(--hover);color:var(--ink)}
.srctools button:disabled{background:var(--sunk);border-color:var(--line-soft);color:var(--muted);cursor:not-allowed}
.srcstate{font-size:var(--fs-xs);color:var(--muted);min-width:0;flex:1 1 100%}
.detailtitletools{display:flex;margin:6px 0 0;flex-wrap:nowrap}
/* 在线条目没有那两枚文件键，标题又没溢出时这一排里一枚可见的键都没有，整行连边距一起收掉。 */
.detailtitletools:not(:has(button:not([hidden]))){display:none}
.detailtitlestate{display:block;margin:-4px 0 10px}
.detailtitlestate:empty{display:none}
/* 来源那两个键跟大小、刷新排在同一组里，靠 `.sorts` 自己的 `margin-left:auto` 一起推到
   右边；再给它加一次 auto 会把它从那组里推开，中间空出一截。下边距同理，这一排是横着的。
   `.srcstate` 默认独占一整行，而这一排不换行，占满行等于把它右边的东西挤到看不见。 */
.photohead .srctools{margin:0}
.photohead .srcstate{flex:none}
.smeta{display:flex;gap:9px;flex-wrap:wrap;color:var(--muted);margin-bottom:18px}
.detailmetaitem{display:inline-flex;align-items:center;gap:5px}.detailmetaitem svg{width:15px;height:15px;
  flex:none;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
/* 产地那一条是能点开的集合，其余几条是读数。按钮只脱掉浏览器默认外观，
   位置、字号和颜色仍由 .detailmetaitem 给，两者在同一行上不能高低不齐。 */
.regionlink{border:0;background:transparent;padding:0;font:inherit;color:inherit;cursor:pointer}
.regionlink:hover{color:var(--ink)}
.stags{position:relative;display:flex;flex-wrap:wrap;align-items:center;gap:7px;margin:0 0 18px}
.detailtag{display:inline-flex;align-items:center;border:1px solid var(--line);border-radius:var(--tag-radius);
  background:var(--tag-fill);overflow:hidden;min-height:30px}
.detailtag button{border:0;background:transparent;color:var(--ink-2);cursor:pointer;height:28px}
.detailtag .tagfilter{padding:3px 5px 3px 11px}.detailtag .tagremove{width:28px;padding:0;display:grid;place-items:center;color:var(--muted)}
/* 标签左半边点下去是按这个标签筛选，得和右边的删除键一样有悬停反馈；
   它没有选中态，照孤立按钮的写法抬填充。 */
.detailtag .tagfilter:hover{background:var(--hover);color:var(--ink)}
.detailtag .tagremove:hover{color:var(--drop);background:color-mix(in srgb,var(--drop) 12%,transparent)}
.detailtag svg,.tagplus svg{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:2}
.tagplus{width:30px;height:30px;border:1px dashed var(--line);border-radius:50%;display:grid;place-items:center;
  background:transparent;color:var(--muted);cursor:pointer}.tagplus:hover{background:var(--hover);color:var(--ink)}
.tagpicker{position:absolute;z-index:120;left:12px;right:12px;top:calc(100% + 8px);max-height:min(480px,70vh);
  overflow:hidden;border:1px solid color-mix(in srgb,var(--ink) 15%,transparent);border-radius:var(--floating-radius);
  background:var(--frost-panel);box-shadow:0 24px 70px -24px #000;
  backdrop-filter:saturate(1.8) blur(20px);-webkit-backdrop-filter:saturate(1.8) blur(20px)}
.tagpicker[hidden]{display:none}.tagpicksearch{height:54px;display:flex;align-items:center;gap:10px;padding:0 14px;
  border-bottom:1px solid color-mix(in srgb,var(--ink) 10%,transparent)}
.tagpicksearch svg{width:20px;height:20px;stroke:currentColor;fill:none;stroke-width:2;color:var(--muted)}
.tagpicksearch input{min-width:0;flex:1;border:0;background:transparent;outline:0;font-size:var(--fs-lg)}
.tagpickbody{max-height:calc(min(480px,70vh) - 54px);overflow:auto;padding:12px}
.tagpicksection+ .tagpicksection{border-top:1px solid color-mix(in srgb,var(--ink) 9%,transparent);margin-top:12px;padding-top:12px}
.tagpicksection h4{margin:0 0 8px;color:var(--muted);font-size:var(--fs-xs);letter-spacing:.12em;font-weight:600}
.tagpickgrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:3px 12px}
.tagpickitem{min-width:0;height:34px;border:0;border-radius:var(--control-radius);background:transparent;color:var(--ink-2);
  padding:0 9px;display:flex;align-items:center;gap:8px;text-align:left;cursor:pointer}
.tagpickitem:hover,.tagpickitem.active{background:var(--hover);color:var(--ink)}
.tagpickitem.selected{color:var(--ink);background:var(--hover)}.tagpickitem .pickname{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tagpickitem .pickcount{margin-left:auto;color:var(--muted);font-size:var(--fs-xs)}.tagpickitem svg{width:14px;height:14px;flex:none}
@media (max-width:760px){.tagpicker{position:fixed;left:12px;right:12px;top:auto;bottom:12px}.tagpickgrid{grid-template-columns:1fr}}
.trace{margin:0 0 18px;padding-top:2px}
.trace .lab{display:flex;justify-content:space-between;color:var(--muted);margin-bottom:5px}
.trace .bar{position:relative;height:4px;background:var(--line);border:0;border-radius:var(--pill-radius)}
.trace .bar u{position:absolute;inset:0 auto 0 0;background:color-mix(in srgb,var(--loupe) 26%,transparent);text-decoration:none}
.trace .bar b{position:absolute;left:0;top:-3px;bottom:-3px;width:2px;background:var(--tungsten);border-radius:var(--pill-radius)}
/* 真实观看那条用另一个色标区分「看过的位置」和「看过多少」。这行原先是模板里的
   内联 style，等于把一条配色写在 JS 里。 */
#realBar{background:color-mix(in srgb,var(--keep) 40%,transparent)}
.trace-real{margin-top:10px}
/* 五颗星站在标题和那行规格之间：评分是对这一条作品的判断，属于身份的一部分，
   不是反馈条里那些「怎么处置它」的动作。整排是同一种琥珀 --rating，空心是未评、
   实心是已评，两态的差别只有填不填。星形自带这个不靠亮度的区分，所以不再拿墨色深浅
   兼职表示评没评——那一档在浅色主题下要同时比页面底重、又比实心轻，凑不出两级都成立的灰。 */
/* 这一排要跟标题、规格行印在同一条左边线上，而对齐的依据是印出来的星形，不是按钮的
   命中区：26px 的方块里画 19px 的星，星形自己在 24 格视区里又从第 2 格才起笔，两层加起来
   把第一颗星的墨往右推了约 4px。命中区仍旧保持 26px——缩到贴着星形，指针就得瞄得很准。
   上下同理。标题那行的 line-height 是 28px、字号 16px，末行下面自带约 8px 的行距；
   星形在自己的方块里上下也各空掉约 5px。两侧都算进去，10px 的上外边距印出来是 22px、
   下边却只有 16px，看着像这一排贴着规格行、离标题很远。上边距回收掉那 6px 的行距，
   两边印出来才是同一个数。 */
.rating{display:flex;align-items:center;gap:8px;margin:-6px 0 10px}
.ratingstars{display:inline-flex;align-items:center;gap:2px;margin-left:-4px}
.ratingstars .star{width:26px;height:26px;padding:0;border:0;border-radius:var(--control-radius);
  background:transparent;color:var(--rating);display:grid;place-items:center;cursor:pointer}
.ratingstars .star svg{width:19px;height:19px;stroke:currentColor;fill:none;stroke-width:2;
  stroke-linecap:round;stroke-linejoin:round}
.ratingstars .star[data-on="true"] svg{fill:currentColor}
/* 悬停要预演的是「点下去会变成几星」，所以整排先退回空心，再把指针那一颗和它左边的填上。
   `:has(~ .star:hover)` 是这里唯一需要的「前一个兄弟」——把 DOM 倒过来写也能做到，
   代价是读屏和 Tab 顺序跟着倒过来，五颗星会从「评为 5 星」开始念。 */
.ratingstars:hover .star svg{fill:none}
.ratingstars:hover .star:hover svg,.ratingstars:hover .star:has(~ .star:hover) svg{fill:currentColor}
.ratingstars .star:focus-visible{outline:2px solid var(--tungsten);outline-offset:2px}
.ratingvalue{color:var(--muted);font-size:var(--fs-xs);font-variant-numeric:tabular-nums}
.fb{display:flex;align-items:center;gap:0;margin:0 0 12px;padding:0;border:1px solid var(--border-10);border-radius:var(--control-radius);background:var(--overlay-5);width:max-content;overflow:hidden;flex:none}
.fb button{width:44px;height:42px;padding:0;border:0;border-right:1px solid var(--border-10);background:transparent;border-radius:0;
  cursor:pointer;display:grid;place-items:center;color:var(--muted);transition:transform .14s ease,color .14s ease,background .14s ease}
.fb button svg{width:21px;height:21px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.fb button:last-child,.fb .fdownload:last-child{border-right:0}.fb button:hover{transform:none;background:var(--hover)}
/* 下载是 `<a download>`，不是按钮：让浏览器自己落盘，不经过 JS。几何与同排按钮一致。 */
.fb .fdownload{box-sizing:border-box;width:44px;height:42px;display:grid;place-items:center;border-right:1px solid var(--border-10);color:var(--ink-2)}
.fb .fdownload:hover{background:var(--hover);color:var(--ink)}
.fb .fdownload svg{width:21px;height:21px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.fb .like:hover{color:var(--ink);background:var(--hover)}
.fb .like[aria-pressed="true"]{color:var(--ink);background:var(--picked)}
/* 加入播放列表开的是弹层，没有按下态，所以只有悬停这一档，和喜欢同走中性墨色：
   这一排里的色相各自指向一种判断（红=不合口味、绿=看过、橙=回收），整理动作不占色相。 */
.fb .playlistadd:hover{color:var(--ink);background:var(--hover)}
.fb .reason:hover,.fb .reason[aria-expanded="true"],.fb .reason[data-has-reason="true"]{color:#FF669D;background:color-mix(in srgb,#FF669D 12%,transparent)}
.fb .dislike:hover,.fb .dislike[aria-pressed="true"]{color:var(--drop);background:color-mix(in srgb,var(--drop) 12%,transparent)}
.fb .seen:hover,.fb .seen[aria-pressed="true"]{color:var(--keep);background:color-mix(in srgb,var(--keep) 12%,transparent)}
.fb .dispose:hover,.fb .dispose[aria-pressed="true"]{color:var(--meter);background:color-mix(in srgb,var(--meter) 12%,transparent)}
.fb .later:hover,.fb .later[aria-pressed="true"]{color:var(--loupe);background:color-mix(in srgb,var(--loupe) 12%,transparent)}
.fb .upgrade:hover,.fb .upgrade[aria-pressed="true"]{color:#B78CFF;background:color-mix(in srgb,#B78CFF 13%,transparent)}
.preference{border:0;padding:0;margin:0 0 14px;background:transparent}.preference[hidden]{display:none}
.preference textarea{display:block;width:100%;min-height:62px;resize:vertical;padding:8px 9px;
  color:var(--ink);background:var(--ground);border:1px solid var(--line);border-radius:var(--control-radius);
  line-height:1.5;font-size:var(--fs-md);transition:border-color .12s ease,box-shadow .12s ease}
.preference textarea:hover{border-color:var(--field-ring-hover)}
.preference textarea:focus{box-shadow:inset 0 0 0 2px var(--color-border-button-active);outline:0}
.preference-foot{display:flex;align-items:center;justify-content:flex-end;gap:8px;margin-top:7px}
.preference-foot>span{margin-right:auto;color:var(--muted);font-size:var(--fs-xs)}.preference-foot>span:empty{display:none}
.savepreference{min-width:58px;padding-inline:12px}
.obtn{width:100%;min-height:48px;padding:10px 14px;border:1px solid color-mix(in srgb,#FF4D8D 38%,transparent);
  background:color-mix(in srgb,#FF4D8D 10%,transparent);
  color:#FF669D;border-radius:var(--control-radius);cursor:pointer;margin-bottom:10px;font-size:var(--fs-md);font-weight:600;
  display:flex;align-items:center;justify-content:center;gap:9px;transition:background-color .14s ease}
.obtn svg{width:22px;height:22px;fill:currentColor}.obtn:hover{background:color-mix(in srgb,#FF4D8D 16%,transparent)}
/* 底色和上面的详情栏同源：半透明的氛围色落在 `.stage` 那圈只铺到 58% 的径向渐变
   外面，会让这一条浅一档，整幅宽度上于是有一道深浅不匀的色差。分界靠 `--line-soft`
   那条明确的线画出来——「接着看」是另一件事，同色的两块之间只留白读不出边界。 */
.next{border-top:1px solid var(--line-soft);padding:11px 15px 12px;background:var(--detail-surface)}
.next h3{margin:0 0 8px;font-size:var(--fs-xs);letter-spacing:.1em;color:var(--muted);text-transform:uppercase}
/* 横滚写法与详情下方的切换带 `.mixlist` 同一份：竖向不滚，滑到头也不把手势交给外面那层浮窗。 */
.nrow{display:flex;gap:11px;overflow-x:auto;overflow-y:hidden;overscroll-behavior-inline:contain;padding-bottom:6px}
.ncard{flex:none;width:216px;cursor:pointer}
.ncard .why{color:var(--loupe)}
/* 216px 的窄卡上，第三个头像挤掉的正好是署名那一行的宽度，元数据于是折成三到四行，
   整条横向带被最高的一张顶起来、矮的下面全是空。窄卡只放两个头像，元数据钉成一行不换行：
   大小和观看次数干脆不放，推荐理由留着截尾——那才是这条带存在的理由。
   不能用固定高度加 overflow 去裁：`.s` 的行高和 gap 凑不出整行，第三行会露半截字。 */
.ncard .mavstack .mav:nth-child(n+3),
.sgrid.mixgrid>.mixqueue .mavstack .mav:nth-child(n+3){display:none}
.ncard .meta .s{flex-wrap:nowrap;overflow:hidden}
.ncard .meta .who{flex:0 1 auto;max-width:55%}
.ncard .meta .whomore{flex:none;white-space:nowrap}
.ncard .meta .why{flex:1 1 0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ncard .meta .size,.ncard .meta .watchcount{display:none}
/* 内边距吃掉刘海和底部横条：iOS 上 `inset:0` 会一直铺到状态栏和 Home 指示条底下。 */
body.settings-open{overflow:hidden}
/* 页面滚动条属于 html 而不只是 body：锁住 body 后文档根仍可滚，打开弹层时
   背后的页滚动条还在，弹层自身加上它就是两条。 */
@supports selector(:has(*)){html:has(body.settings-open){overflow:hidden}}
.iconswitch.javimageswitch label{width:auto;min-width:52px;padding-inline:10px}
.javimageswitch label>span[aria-hidden]{display:inline-flex;align-items:center;gap:6px}
.settingspanel{position:fixed;z-index:var(--layer-dialog);inset:0;isolation:isolate;display:grid;place-items:center;background:var(--scrim);padding:18px;padding-top:max(18px,env(safe-area-inset-top));padding-bottom:max(18px,env(safe-area-inset-bottom));overscroll-behavior:contain}
.settingspanel[hidden]{display:none}/* 高度用 dvh 不用 vh。iOS 上 `vh` 算的是「大视口」——不减地址栏，所以按 90vh 撑出来的
   面板会顶到地址栏和状态栏底下，上半截被遮住。`dvh` 跟着当前可见高度走。
   vh 那行留作旧浏览器兜底，后一行覆盖前一行。 */
/* 遮罩取 `--scrim`，值与依据写在 `01-base.css` 的 `:root` 上；作品详情浮窗的
   `::backdrop` 引的是同一个 token。 */
.settingspanel:not([hidden]){animation:settings-backdrop-in .35s cubic-bezier(.4,0,.2,1) both}
.settingspanel.closing{animation-name:settings-backdrop-out}
.settingspanel.closing :where(.settingscard){animation-name:settings-dialog-out}
@keyframes settings-backdrop-in{from{opacity:0}to{opacity:1}}
@keyframes settings-backdrop-out{from{opacity:1}to{opacity:0}}
@keyframes settings-dialog-in{from{transform:translate3d(0,-40px,0);opacity:0}to{transform:translate3d(0,0,0);opacity:1}}
@keyframes settings-dialog-out{from{transform:translate3d(0,0,0);opacity:1}to{transform:translate3d(0,-40px,0);opacity:0}}
/* 面板照首页那两张面来：卡片壳是退到后面的 --page，标题栏和里面每个分组是浮起来的
   --ground——和首页一样，顶栏与卡片同为亮面，页面底在它们之间露出来。
   靠一档亮度差加一条 --field-ring 发丝线浮起，不加投影。原来正好反着——白壳里嵌灰块，
   同一套控件在设置里和在首页上读出的层级相反。投影一并去掉：全站只有两张面这条规则
   不因为它是弹层就不算，压暗的遮罩已经把它和底下的内容分开了。 */
.settingscard{display:flex;flex-direction:column;width:min(520px,100%);max-height:min(720px,90vh);max-height:min(720px,90dvh);overflow:clip;animation:settings-dialog-in .35s cubic-bezier(.4,0,.2,1) both;
  border:1px solid var(--field-ring);border-radius:var(--floating-radius);background:var(--page)}.settingsscroll{flex:1;min-height:0;overflow-y:auto;padding:0 20px 20px;overscroll-behavior:contain}.settingshead{z-index:2;display:flex;align-items:center;justify-content:space-between;flex:none;padding:20px 20px 16px;border-bottom:1px solid var(--field-ring);background:var(--ground)}
/* 这个文件里一条分主题的规则都没有：卡片与分组走 --page、--ground 和 --field-ring，
   遮罩走 --scrim，两档主题由 token 自己换。 */
.settingshead h2{margin:0;font-size:var(--fs-xl)}
/* 标题行右端那颗关闭键。 */
.settingshead button{border:0;background:transparent;color:var(--muted);width:34px;height:34px;border-radius:var(--control-radius);cursor:pointer;display:grid;place-items:center;transition:background-color .12s ease,color .12s ease}.settingshead button:hover{background:var(--hover);color:var(--ink)}
.settingshead svg{width:20px;height:20px;stroke:currentColor;fill:none}.settingrow{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:18px;align-items:center;padding:14px 0 14px;border-top:1px solid var(--line-soft)}
.settinggroup>h3+.settingrow{border-top:0}.settingrow b{display:block;font-size:var(--fs-md)}.settingrow small{display:block;color:var(--muted);margin-top:8px;line-height:1.45}
.settingrow .gselect{min-width:148px}
.settingrow.settingrelated{border-top:0}
.settinggroup .settingrow.settingsortrow{grid-template-columns:minmax(0,1fr);gap:12px;padding-block:14px 16px}
.settingsortcontrols{display:flex;gap:8px;min-width:0}
.settingsortcontrols>div{flex:1;min-width:0}
.settingsortcontrols>[hidden]{display:none}
.settingsortcontrols .gselect{min-width:0;width:100%}
.settinggroup{margin:16px 0 0;border:1px solid var(--field-ring);border-radius:var(--floating-radius);
  background:var(--ground);padding:0 16px 12px}
.settinggroup>h3{margin:0;padding:14px 0 10px;font-size:var(--fs-lg);font-weight:600;color:var(--ink)}
.settinggroup .settingrow{margin:0;padding-left:0;padding-right:0}
/* 布尔开关用 Geist Toggle 而不是原生复选框（取证 vercel-geist-semantics-measured.md）：
   中号 36×20 轨道 + 17px 圆点，关 = 灰轨道圆点靠左，开 = 主题蓝圆点靠右；
   Geist 的「Switch」是分段选择器（radio 语义），布尔开关的正确控件叫 Toggle。
   选择器只认 `.ptoggle` 这一个类，不许再把 id 混进 `:is()`：`:is()` 取的是里面最强的
   那一项的权重，混一个 id 进去整组就变成 (1,x,0)，同文件后面任何 `.ptoggle` 规则都再也
   压不过它。开态和禁用态于是同时成立——「静默启动」在「开机后启动」关着时明明拿到了
   `disabled`，轨道却仍是那抹蓝，只有标签变灰。 */
.ptoggle{appearance:none;-webkit-appearance:none;width:36px;height:20px;flex:none;
  border:0;box-sizing:border-box;padding:0;border-radius:var(--pill-radius);background:var(--line);position:relative;cursor:pointer;margin:0;
  transition:background-color .15s ease}
.ptoggle::after{content:'';position:absolute;top:1.5px;left:2px;width:17px;height:17px;
  border-radius:50%;background:#fff;transition:left .15s ease}
.ptoggle:checked{background:var(--tungsten)}
.ptoggle:checked::after{left:17px}
.ptoggle:focus-visible{outline:2px solid var(--tungsten);outline-offset:2px}
/* 禁用要压过开态，所以写在 `:checked` 之后：一颗开着又被禁的开关（「静默启动」在
   「开机后启动」关着时就是这样）得读成灰的，不是蓝的。 */
.ptoggle:disabled{background:var(--surface);cursor:not-allowed}
.ptoggle:disabled::after{background:var(--muted)}
.ptoggle:disabled:checked{background:var(--surface)}
/* 主题选择器是 Geist 里唯一给选中项加环的控件：三档的底色和它坐着的面板同色，
   光靠填充分不出当前是哪一档。几何按 2026-09-04 实测 vercel.com 的 system／light／dark
   三枚圆形按钮：外框是 32px 高、无填充的胶囊加一圈 1px 环，每档 32×32 正圆、图标 16px，
   未选中 --muted、悬停只提文字色（这条由 .iconswitch 本体给），选中填 --ground 再加环。
   两个类名叠着写是为了压过 19-immersive.css 里的 .iconswitch 本体——那份排在后面。 */
.iconswitch.themeswitch{display:inline-flex;flex:none;padding:0;border:0;background:transparent;
  border-radius:var(--pill-radius);box-shadow:0 0 0 1px var(--border-15)}
.iconswitch.themeswitch label{width:32px;height:32px;border-radius:50%}
.iconswitch.themeswitch label:has(input:checked){background:var(--ground);
  box-shadow:0 0 0 1px var(--line),0 1px 2px var(--overlay-5)}
.iconswitch.themeswitch svg{width:16px;height:16px}
/* 手机上三枚圆各自撑到 44px 命中区，圆心间距跟着长，指头点得中。 */
@media (max-width:760px){.iconswitch.themeswitch label{width:44px;height:44px}}
/* 侧栏光晕的参数区。它跟着上面那一行开关走，关掉就整块收起来——一组调不出任何效果的
   拉条和色块留在原地，读的人只会以为是坏了。

   右边那 16px 外边距加 10px 内边距是这一块唯一的定位修正，不是留白偏好。Board 那一层
   给 `.settinggroup` 的内边距是 `0 0 0 12px`——只有左边；右边那一段由每个 `.settingrow`
   自己的 `margin-right:16px`（board.css `.settingsscroll .settingrow`）和 `padding-right:10px`
   一起给。`.glowsetting` 不是 settingrow，两样都拿不到，于是一路顶到分组的右边缘：
   2026-09-16 在 1440 宽下实测，同组设置行的盒子右缘落在 1108、正文右缘 1098，这一块
   落在 1124，正好压在分组那 16px 圆角的弧线上，读数、色块和「恢复默认」被切掉一角。
   `.sidebarsetting` 早就为同一件事单独写过同样的两个值，这里按同一个办法补齐，
   不靠 `overflow:hidden` 把溢出盖住。

   版式照 feralui.dev/gradients 的工作台面板：分组标题是一行小号灰字，参数行是
   「84px 标签 + 自绘拉条 + 等宽读数」，颜色行点开在自己下方弹一张色板。
   取证见 docs/reference-snapshots/feralui-studio-boardui-accent-measured.md。

   竖向节奏跟同一张卡里的设置行走同一份数：`board.css` 的
   `.settingscard .settingsscroll .settingrow` 是最小高 52px、上下各 10px 内边距。
   `--glowrow-min` / `--glowrow-pad` 就是那两个值，本块每一行都读它。这一块此前是三层
   gap（16 + 12 + 12）叠出来的间距，同一屏里「当前配色」到「强度」的落差和上面几行设置
   之间的落差对不上，读起来像是中间漏了一条线。 */
.glowsetting{--glowrow-min:52px;--glowrow-pad:10px;
  display:grid;gap:0;margin-right:16px;padding:0 10px var(--glowrow-pad) 0;min-width:0}
.glowsetting[hidden]{display:none}
.glowcurrent{display:flex;align-items:center;gap:8px;margin:0;min-height:var(--glowrow-min);
  padding-block:var(--glowrow-pad);color:var(--muted);font-size:var(--fs-sm)}
.glowcurrent b{color:var(--ink);font-weight:500}
/* 预设色块和侧栏配色卡同一组、同一条 `.board-glow-grid`，间距是那条规则里的 6px。
   这一行有整块设置那么宽，列数跟着可用宽度走：每格就是一枚球（`.board-glow-chip` 的
   28px），能放几枚放几枚，排满再换行；列宽不跟行宽走，球挨着排、不被拉开。
   左边不留那 4px：这一行的字从内容左缘起，球也从那里起。 */
.glowsetting .board-glow-grid{grid-template-columns:repeat(auto-fill,28px);padding:0}
.glowgroup{display:grid;gap:0;min-width:0}
/* 「玻璃原色」那一档下面这行字顶替被收走的几条拉条和三行颜色。和上面那行「当前配色」
   同一档字色与行高，读起来是同一列里的一句话，不是插进来的提示条。 */
.glownative{margin:0;padding-block:var(--glowrow-pad);color:var(--muted);font-size:var(--fs-sm);line-height:1.6}
/* 上游那一行是大写加宽字距的英文小标题，中文不大写：中文没有大小写，`text-transform`
   在这里什么也不做，字距倒是会把两个字拉散。 */
.glowgroup>h4{display:flex;align-items:center;min-height:var(--glowrow-min);
  padding-block:var(--glowrow-pad);margin:0;color:var(--muted);font-size:var(--fs-xs);font-weight:600}
.glowfields{display:flex;flex-direction:column;gap:0;min-width:0}
.glowfield{display:flex;align-items:center;gap:10px;min-height:var(--glowrow-min);
  padding-block:var(--glowrow-pad);min-width:0}
/* 带显式 display 的元素盖得住浏览器默认的 `[hidden]{display:none}`：「玻璃原色」那一档
   要收走其余几条拉条和整组颜色行，少这一条的话它们照样摆在那里，拖也拖得动、什么也
   不影响。 */
.glowfield[hidden],.glowgroup[hidden]{display:none}
.glowfieldlabel{flex:none;width:84px;color:var(--muted);font-size:var(--fs-xs);font-weight:600;white-space:nowrap}
.glowfield .dial{flex:1;min-width:0}
/* 颜色行：34px 圆点 + 名称与十六进制两行 + 行尾折角。相邻两行之间那条分隔线从圆点
   右侧起算，左边那一段留给圆点本身——线一路铺到最左边的话，三枚圆点会被切成一列格子。 */
.glowstops{display:grid;min-width:0}
.glowstop{position:relative;min-width:0}
.glowstop+.glowstop::before{content:'';position:absolute;left:54px;right:6px;top:0;height:1px;
  background:linear-gradient(to right,var(--line-soft),transparent)}
.glowstopmain{display:flex;align-items:center;gap:10px;width:100%;min-width:0;
  min-height:var(--glowrow-min);padding:var(--glowrow-pad) 7px var(--glowrow-pad) 8px;
  border:0;border-radius:var(--floating-radius);background:transparent;
  color:inherit;font:inherit;text-align:left;cursor:pointer}
.glowstopmain:hover{background:color-mix(in srgb,var(--ink) 3.2%,transparent)}
.glowstopmain[aria-expanded="true"]{background:color-mix(in srgb,var(--ink) 4.6%,transparent)}
.glowstopmain:focus-visible{outline:2px solid var(--tungsten);outline-offset:-2px}
.glowstopmain>svg{width:14px;height:14px;flex:none;margin-left:auto;color:var(--muted);stroke:currentColor;fill:none}
.glowstopdot{width:34px;height:34px;flex:none;border-radius:50%;background:var(--glow-swatch,var(--line));
  box-shadow:inset 0 1px 1px var(--frost-light),inset 0 -1px 2px var(--overlay-5),0 0 0 1px var(--border-15)}
.glowstoptext{display:grid;min-width:0}
.glowstoptext>b{font-size:var(--fs-sm);font-weight:600}
.glowstoptext>small{color:var(--muted)}
/* 色板弹层贴着行开，宽度由 JS 写成行宽。盒子、阴影和开合动效都来自共用的 .popmenu。 */
.glowstoppop{display:grid;gap:0;padding:0;overflow:hidden}
.glowpills{display:flex;gap:6px;padding:8px;border-bottom:1px solid var(--line-soft);
  overflow-x:auto;overscroll-behavior:contain;scrollbar-width:none}
.glowpills::-webkit-scrollbar{display:none}
.glowpill{flex:none;padding:5px 13px;border:0;border-radius:var(--pill-radius);
  background:color-mix(in srgb,var(--ink) 6%,transparent);color:var(--ink-2);
  font-size:var(--fs-xs);font-weight:500;cursor:pointer}
.glowpill[aria-pressed="true"]{background:var(--hover);color:var(--ink)}
.glowpill:not([aria-pressed="true"]):hover{color:var(--ink)}
.glowpill:focus-visible{outline:2px solid var(--tungsten);outline-offset:2px}
.glowpalette{display:grid;grid-template-columns:repeat(auto-fill,minmax(34px,1fr));gap:8px 9px;
  max-height:218px;padding:10px;overflow-y:auto;overscroll-behavior:contain}
.glowswatch{width:34px;height:34px;padding:0;border:1px solid var(--border-15);border-radius:50%;
  background:var(--glow-swatch);box-shadow:inset 0 1px 1px var(--frost-light);cursor:pointer;
  transition:transform .12s ease}
.glowswatch[hidden]{display:none}
.glowswatch:hover{transform:scale(1.055)}
/* 选中这一枚要连底色一起重申：这块色板住在 `.popmenu` 里，board.css 给浮层菜单项的
   `[aria-checked=true]` 配了一张选中面，只写 box-shadow 的话那张面会盖掉色块自己的颜色，
   当前这一枚就成了唯一一个没有颜色的圆点。选择器多一节是为了压过那条，不是为了好看。 */
.glowpalette .glowswatch[aria-checked="true"]{background:var(--glow-swatch);
  box-shadow:inset 0 1px 1px var(--frost-light),
  0 0 0 1.5px var(--ground),0 0 0 2.5px color-mix(in srgb,var(--ink) 30%,transparent)}
.glowswatch:focus-visible{outline:2px solid var(--tungsten);outline-offset:2px}
.glowactions{display:flex;justify-content:flex-end;padding-top:var(--glowrow-pad)}
/* 同一组里排在设置行后面的分区自带分隔线。这里只给线：盒子的边距归 board.css 那条
   `.settingscard .settingsscroll .sidebarsetting`，它带 `!important`，在这里另写一份
   只会留下一组读着像在生效、其实一个像素也没动的值。 */
.settinggroup :is(.settingrow,.glowsetting)+.sidebarsetting{border-top:1px solid var(--line-soft)}
.sidebarsetting{display:grid;gap:12px;padding:0}.sidebarsetting>span b,.sidebarsetting>span small{display:block}.sidebarsetting>span small{margin-top:3px;color:var(--muted);line-height:1.45}.sidebarorder{display:grid;gap:6px}/* 这一列的行、下面那个下拉和「添加」按钮同高：它们是同一张清单的三段，差几像素就
   看得出是两拨控件。34px 的图标钮加上下 1px 内边距和 1px 边框正好是 --control-h。 */
.sidebarorderrow{position:relative;display:flex;align-items:center;justify-content:space-between;min-height:var(--control-h);padding:1px 5px;border:1px solid var(--line-soft);border-radius:var(--surface-radius);background:var(--surface);cursor:grab}.sidebarorderrow:active{cursor:grabbing}.sidebarorderrow.dragging{opacity:.42}.sidebarorderrow.drop-before::before,.sidebarorderrow.drop-after::after{content:'';position:absolute;z-index:2;left:7px;right:7px;height:2px;border-radius:var(--pill-radius);background:var(--ink)}.sidebarorderrow.drop-before::before{top:-5px}.sidebarorderrow.drop-after::after{bottom:-5px}.sidebarorderrow>span{display:flex;align-items:center;gap:8px}.sidebarorderrow svg{width:18px;height:18px;stroke:currentColor;fill:none}.sidebarorderlabel{min-width:0}.sidebarorderlabel>b{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sidebardrag{display:grid;place-items:center;width:24px;color:var(--muted)}.sidebardrag svg{width:20px;height:20px}.sidebarorderactions{flex:none}.sidebarorderrow button,.sidebaraddmenu button{display:grid;place-items:center;min-width:34px;height:34px;border:0;border-radius:var(--control-radius);background:transparent;color:var(--muted);cursor:pointer}.sidebarorderrow button:hover:not(:disabled),.sidebaraddmenu button:hover:not(:disabled){background:var(--hover);color:var(--ink)}.sidebarorderrow button:disabled,.sidebaraddmenu button:disabled{opacity:.25;cursor:not-allowed}.sidebaradd{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px;margin-top:5px}
.sidebaraddpicker{position:relative;min-width:0}/* 它是 listbox 触发器，不是按钮：穿输入框那身盒子，右边的实心档才是这一屏的主动作。 */.sidebaradd .sidebaraddfield{display:grid;grid-template-columns:auto minmax(0,1fr) auto;width:100%;height:var(--control-h);box-sizing:border-box;align-items:center;justify-items:start;gap:9px;padding:0 11px;border:1px solid var(--field-ring);border-radius:var(--control-radius);background:var(--ground);color:var(--ink);text-align:left;font:inherit;cursor:pointer}.sidebaradd .sidebaraddfield:hover:not(:disabled){border-color:var(--field-ring-hover)}.sidebaradd .sidebaraddfield:disabled{color:var(--muted);cursor:not-allowed}.sidebaraddfield svg:last-child{justify-self:end;color:var(--muted)}/* 盒子、2px 行距和投影都来自共用的 .popmenu，这里只接管定位：它贴着触发器往上开，不是 fixed。
   行距不能省——相邻两项一个悬停一个选中时，两块填充会连成一整条，看不出是两行。 */
.sidebaraddmenu{position:absolute;z-index:4;left:0;right:0;bottom:calc(100% + 6px);max-height:min(312px,48vh)}.sidebaradd .sidebaraddmenu button{grid-template-columns:auto minmax(0,1fr);width:100%;height:var(--control-h);justify-content:start;gap:9px;padding:0 8px;border:0;text-align:left}.sidebaraddmenu button[aria-selected="true"]{background:var(--hover);color:var(--ink)}.sidebaradd svg{width:17px;height:17px;stroke:currentColor;fill:none}
.sidebaradd .geist-button{height:var(--control-h);padding:0 14px}@media(max-width:600px){.settingsscroll{padding:0 17px 17px}.settingshead{padding:17px 17px 14px}.settingrow{grid-template-columns:1fr;gap:9px}
  /* 单列是给下拉留的：148px 的下拉配上标题和说明在窄屏挤不下。开关只有 36px，
     跟标题同一行绰绰有余，跟着换行只是白占一行高度。 */
  .settingrow:has(input[type="checkbox"]){grid-template-columns:minmax(0,1fr) auto;gap:12px}
  .settingrow .gselect{width:100%;min-width:0}.settingrow .pcheck{justify-self:start}}
.sidebaradd .sidebaraddmenu button{justify-items:start}
/* ── Toast：Geist 的非阻塞回执 ──
   右下角浮层，自动消失，hover 暂停计时，右上角关闭。挂在 body 直下
   （#stats 外），整页重画冲不掉。行为取证见 docs/reference-snapshots/
   vercel-geist-toast.md；量纲沿用本站实测 Geist 的那一套：8px 圆角、
   发丝边、比页面亮一档的表面。warn 态加一圈红边和图标，但失败的原因
   仍以页内持久行为准——toast 只负责提一句。 */
/* 行距用每条自己的上外边距，不用容器 gap：收起时高度和外边距一起过渡到 0，
   栈才收得平顺；gap 是容器的，收不掉。 */
.toasts{position:fixed;right:16px;bottom:22px;z-index:var(--layer-popover);display:grid;
  justify-items:end;pointer-events:none}
.toast{pointer-events:auto;box-sizing:border-box;display:flex;align-items:center;gap:9px;
  width:min(440px,calc(100vw - 32px));margin-top:8px;padding:11px 10px 11px 13px;
  border:1px solid var(--line);border-radius:var(--surface-radius);background:var(--surface);color:var(--ink);
  font-size:var(--fs-sm);line-height:1.6;box-shadow:0 2px 4px #0003,0 24px 80px #000b;
  animation:toast-in .18s ease}
@keyframes toast-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
.toast.warn{border-color:color-mix(in srgb,var(--drop) 38%,var(--line))}
.toast.warn>svg{width:15px;height:15px;flex:none;stroke:var(--drop);fill:none;
  stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.toast p{flex:1;margin:0;min-width:0}
.toast b{font-variant-numeric:tabular-nums}
/* 动作是具名的下一步（去看更新），文字按钮跟正文同行；关闭才是 24px 图标位。 */
.toast .tact{flex:none;border:0;background:none;padding:0;
  color:var(--ink);font:inherit;font-size:var(--fs-sm);cursor:pointer}
.toast .tact:hover{color:var(--ink-2)}
.toast .tact[aria-busy="true"]{color:var(--muted);cursor:default}
.toast .tclose{flex:none;width:24px;height:24px;padding:0;border:0;border-radius:var(--control-radius);
  background:transparent;color:var(--muted);display:grid;place-items:center;cursor:pointer}
.toast .tclose:hover{background:var(--hover);color:var(--ink)}
.toast .tclose svg{width:13px;height:13px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round}
.toast.leaving{height:0!important;margin-top:0;padding-block:0;border-block-width:0;opacity:0;overflow:hidden;
  transition:height .18s ease,margin .18s ease,padding .18s ease,border-width .18s ease,opacity .12s ease}
.mixgrouplabel{display:flex;align-items:center;gap:8px;margin:12px 10px 6px;color:var(--ink);
  font-size:var(--fs-xs);font-weight:600}
.mixgrouplabel:first-child{margin-top:4px}
.mixgrouplabel span{color:var(--muted);font-variant-numeric:tabular-nums;font-weight:400}

/* SFW 模式默认关闭，浏览器保存显式选择。全站图片、视频及 videojs 海报层
   使用模糊、降低饱和度与压暗处理；品牌标识、来源图标保持可见。
   悬停预览由 JS 停止启动，文字不受图片滤镜影响。 */
body.censor img,body.censor video,body.censor .vjs-poster{
  filter:blur(30px) saturate(.3) brightness(.6)}
body.censor .brand .mark,body.censor .src img,body.censor .ficon{filter:none}
/* ── 筛选抽屉（默认收起，不占版面） ── */
/* 常驻 72 px 窄图标条。外部实现证据和有意差异登记在 docs/HANDOFF.md。 */
.edge{position:fixed;top:0;bottom:0;left:0;width:var(--railW);z-index:92;
  padding-top:14px;
  background:var(--ground);
  /* 窄栏和内容区背景接近，没有这条线就看不出左边那一条到哪里为止。
     抽屉展开时它从 left:0 盖住窄栏，线自然跟着消失，那时候分界由抽屉自己的右边框接管。 */
  border-right:1px solid var(--line-soft);
  display:flex;flex-direction:column;align-items:center;gap:4px;padding:10px 0}
.edge button{width:52px;height:52px;border-radius:var(--control-radius);background:none;border:0;cursor:pointer;
  display:flex;align-items:center;justify-content:center;color:var(--muted);transition:background .12s}
/* 侧栏导航是「填充让位给选中」的例外，Geist 自己就这么写。2026-09-04 实测
   vercel.com/geist 的左栏：未选中项 `hover:bg-gray-100`（悬停底 rgb(26,26,26)），
   文字全程停在 rgb(161,161,161) 不动；当前项 `bg-gray-alpha-100`（合成后亮度
   和悬停底几乎相同）加文字提到 rgb(237,237,237)，且不带任何 hover 类。
   也就是分工反过来：填充是「鼠标在这儿」，文字/图标色才是「你在这儿」。
   纯图标窄栏尤其需要这样——52px 方块里只有一个描边图标，光靠图标换色近乎看不见。 */
.edge button:hover{background:var(--hover)}
.edge button[aria-pressed="true"]{background:var(--hover);color:var(--ink)}
.edge button.nav-dragging,.dnav button.nav-dragging{opacity:.38}.edge button.nav-drop-before::before,.edge button.nav-drop-after::after,.dnav button.nav-drop-before::before,.dnav button.nav-drop-after::after{content:'';position:absolute;z-index:2;left:7px;right:7px;height:2px;border-radius:var(--pill-radius);background:var(--ink)}.edge button.nav-drop-before::before,.dnav button.nav-drop-before::before{top:-3px}.edge button.nav-drop-after::after,.dnav button.nav-drop-after::after{bottom:-3px}.edge button[data-nav],.dnav button[data-nav]{position:relative}
.edge svg{width:24px;height:24px;stroke:currentColor;fill:none;stroke-width:2;
  stroke-linecap:round;stroke-linejoin:round}
/* 窄栏从 y=0 贯通到底，顶栏只在右侧内容列里。 */
body{padding-left:var(--railW)}
@media (max-width:760px){body{padding-left:0}.edge{display:none}}
/* 抽屉从窄栏右侧开始，不再盖住窄栏。
   原来它压在窄栏上并把窄栏整个 `pointer-events:none`，于是「悬停展开」变成了
   「目标从光标底下挪走」：注释声称「图标落在窄栏原来的位置」，实测两边差 10–54px
   （窄栏行距 52px、抽屉 47px、起点还差 54px），瞄准「在线追更」点下去打开的是「统计」。
   与其逐像素对齐两套各自会因溢出、滚动、条目增减而漂移的列表，不如让它们不重叠——
   光标底下的图标就永远不动。 */
/* 抽屉自己不再滚，滚的是里面那层。分开是为了给覆盖式滚动条腾出一个定位祖先：
   轨道必须是滚动容器的兄弟，跟着内容一起滚的轨道等于没有轨道。 */
.drawer{position:fixed;top:0;bottom:0;left:0;width:360px;z-index:97;
  background:linear-gradient(180deg,var(--frost-light),var(--frost-panel));
  backdrop-filter:saturate(1.8) blur(20px);-webkit-backdrop-filter:saturate(1.8) blur(20px);
  border-right:1px solid var(--line);overflow:hidden;
  transform:translateX(-100%);transition:transform .2s cubic-bezier(.3,.8,.3,1)}
.drawerscroll{height:100%;box-sizing:border-box;padding:16px 12px 60px;
  overflow-y:auto;overflow-x:hidden;scrollbar-width:none}
.drawerscroll::-webkit-scrollbar{display:none}

.drawer.open{transform:none;box-shadow:18px 0 60px -24px #000}
/* 窄栏必须压在抽屉之上，否则抽屉一弹出就吃掉窄栏的点击 */
.edge{z-index:96}
body.drawer-open .edge{opacity:0;pointer-events:none}   /* 展开时窄栏让位，抽屉就是它的展开态 */
/* 遮罩必须低于抽屉，否则每一下点击都落在遮罩上，而遮罩的 onclick 正好是收起抽屉——
   表现成「能弹出但点哪儿都只是把它收起来」。窄栏此时已 pointer-events:none，
   所以它夹在 95 和 97 之间不影响任何判断。 */
.dnav button{padding-left:0}
/* 图标落在窄栏原来的位置 */
.dnav svg{width:24px;height:24px;margin:0 15px 0 24px}
.dnav{display:flex;flex-direction:column;gap:3px;margin-bottom:18px}
.dnav button{padding-left:12px}
.dnav button{display:flex;align-items:center;gap:14px;padding:11px 14px;background:none;
  border:0;border-radius:var(--control-radius);cursor:pointer;text-align:left;font-size:var(--fs-lg);color:var(--muted)}
/* 抽屉是窄栏的展开态，走同一条例外：悬停抬填充，颜色留给当前项。文字与图标同色，
   都从按钮继承——Geist 左栏实测是 63% 灰对 93% 白，两态填充亮度几乎一样，
   分辨「你在这儿」全靠颜色。 */
.dnav button:hover{background:var(--hover)}
.dnav button[aria-pressed="true"]{background:var(--hover);color:var(--ink)}
.dnav svg{width:20px;height:20px;flex:none;stroke:currentColor;fill:none;stroke-width:2;
  stroke-linecap:round;stroke-linejoin:round}
.drawer .chip{font-size:var(--fs-xs);padding:4px 10px}
/* 收起时整块 display:none，不能只靠 opacity:0：iOS 26 的 Safari 给状态栏和地址栏取色时会读贴着屏幕
   上下沿、铺满宽度的 fixed 元素的底色，透明度为 0 的也照读，于是这层看不见的黑遮罩把两条栏刷成实色，
   页面内容透不上去。淡入淡出靠 allow-discrete 让 display 跟着 opacity 一起过渡。 */
.scrim{position:fixed;inset:0;z-index:95;background:rgba(0,0,0,.42);opacity:0;pointer-events:none;display:none;
  transition:opacity .18s,display .18s allow-discrete}
.scrim.on{display:block;opacity:1;pointer-events:auto}
@starting-style{.scrim.on{opacity:0}}
.sec{border-top:1px solid var(--line-soft);padding-top:11px;margin-bottom:13px}
.sec:first-of-type{border-top:0;padding-top:0}
.sec > h3{margin:0 0 7px;font-size:var(--fs-xs);letter-spacing:.2em;text-transform:uppercase;
  color:var(--muted);font-weight:600;display:flex;justify-content:space-between;align-items:center}
.sec > h3 button{background:none;border:0;cursor:pointer;color:var(--muted);font-size:var(--fs-xs);padding:0}
.sec > h3 button:hover{color:var(--ink)}
.chips{display:flex;flex-wrap:wrap;gap:4px}
.chip img,.chip>svg{width:14px;height:14px;border-radius:var(--badge-radius);object-fit:contain;flex:none}
.source-icon{display:block!important;width:16px!important;height:16px!important;min-width:16px;opacity:1!important;filter:none!important;object-fit:contain}
.chip>svg{stroke:currentColor;fill:none;stroke-width:2}
.chip{display:inline-flex;align-items:center;gap:6px;padding:var(--tag-pad-y) var(--tag-pad-x);border:1px solid var(--line);
  border-radius:var(--tag-radius);background:transparent;cursor:pointer;font-size:var(--fs-xs)}
.chip:hover{color:var(--ink)}
.chip[aria-pressed="true"]{background:var(--hover);color:var(--ink)}
/* 不同类别保留语义色，内容卡片本身维持中性。 */
.sec.cat-artist  .chip{color:#D2703F;border-color:#D2703F44}
.sec.cat-artist  .chip[aria-pressed="true"]{background:#D2703F22;border-color:#D2703F}
.sec.cat-general .chip{color:#6C9BD6;border-color:#6C9BD644}
.sec.cat-general .chip[aria-pressed="true"]{background:#6C9BD622;border-color:#6C9BD6}
.sec.cat-meta    .chip{color:var(--muted);border-color:var(--line)}
.sec.cat-meta    .chip[aria-pressed="true"]{background:var(--hover);border-color:var(--ink-2);color:var(--ink)}
.sec.cat-src     .chip{color:#7FBF7F;border-color:#7FBF7F44}
.sec.cat-src     .chip[aria-pressed="true"]{background:#7FBF7F22;border-color:#7FBF7F}
.sec > h3{color:var(--ink-2)}
.sec.cat-artist > h3{color:#D2703F}
.sec.cat-general > h3{color:#6C9BD6}
.sec.cat-src > h3{color:#7FBF7F}
.chip .n{font-size:var(--fs-xs);color:var(--muted);font-variant-numeric:tabular-nums}
.cost{width:5px;height:5px;border-radius:50%;flex:none}
.cost.free{background:var(--keep)} .cost.metered{background:var(--meter)}
.duration-filter{padding:3px 2px 8px}
/* 时长拉条是控件不是进度条，蓝只剩它的焦点环：已选段填 --ink，抓手填 --ground 再描一圈
   --ink。两个 token 随主题翻面的方向相反，所以两档读出来是同一句话——抓手永远和它坐落
   的那张面同色，像是从墨色轨道上抠下来的一段。单给抓手填 --ink 就不成立：浅色下它是压在
   轨道上的黑疙瘩，深色下却是枚亮抓手，同一个控件读出两种东西。 */
.dual-range{position:relative;height:28px;--lo:0%;--hi:100%}.range-base,.range-fill{position:absolute;left:0;right:0;top:12px;height:4px;border-radius:var(--pill-radius);background:var(--line)}
.range-fill{left:var(--lo);right:calc(100% - var(--hi));background:var(--ink)}
.dual-range input{position:absolute;inset:0;width:100%;height:28px;margin:0;background:none;pointer-events:none;appearance:none;-webkit-appearance:none}
.dual-range input::-webkit-slider-thumb{appearance:none;-webkit-appearance:none;width:16px;height:16px;border-radius:50%;background:var(--ground);border:2px solid var(--ink);pointer-events:auto;cursor:grab;transition:box-shadow .12s ease}
.dual-range input:hover::-webkit-slider-thumb{box-shadow:0 0 0 4px var(--hover)}
.dual-range input:focus-visible::-webkit-slider-thumb{outline:2px solid var(--tungsten);outline-offset:2px}
.dual-range input::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:var(--ground);border:2px solid var(--ink);pointer-events:auto;cursor:grab}

/* ── 沉浸模式 ──
   桌面采用 YouTube Shorts 的三段式布局：固定 9:16 舞台居中，动作列贴在舞台右侧，
   作者与标题留在左下角。手机才让舞台重新铺满视口。 */
.tok{position:fixed;inset:0;z-index:200;background:#0f0f0f;display:flex;align-items:center;justify-content:center;overflow:hidden}
.tok[hidden]{display:none}
.tokstage{position:absolute;left:50%;top:50%;width:min(56.25vh,calc(100vw - 240px));aspect-ratio:9/16;
  transform:translate(-50%,-50%)}
.tokstage.wide{left:50%;right:auto;width:min(64vw,177.778vh);aspect-ratio:16/9;transform:translate(-50%,-50%)}
.toktrack{position:absolute;inset:0;overflow:hidden;border-radius:var(--floating-radius);background:#000;
  box-shadow:0 0 64px rgba(0,0,0,.34);touch-action:manipulation;cursor:pointer}.toktrack video{position:absolute;inset:0;max-height:none;max-width:none;
  width:100%;height:100%;left:50%;transform:translateX(-50%);object-fit:cover;background:#000;will-change:transform;
  transition:transform .2s cubic-bezier(.2,.2,.4,.9)}
/* 片源与视口比例差得多时按原始比例完整显示（留黑边），不裁掉画面。
   判据在 JS 的 tokFitOne：这里只认结果，不再假设「竖屏才需要 contain」。 */
.toktrack video.contain{object-fit:contain}
.tokloading{position:absolute;z-index:12;left:50%;top:50%;transform:translate(-50%,-50%);
  display:flex;align-items:center;gap:10px;padding:10px 14px;border:1px solid rgba(255,255,255,.24);
  border-radius:var(--pill-radius);background:rgba(0,0,0,.58);color:#fff;backdrop-filter:blur(12px);pointer-events:none;
  font:var(--fs-xs)/1 "Cascadia Mono",monospace;white-space:nowrap}
.tokloading[hidden]{display:none}
.tokui{position:absolute;left:20px;bottom:20px;width:min(520px,calc(50% - 28.125vh - 36px));
  padding:0;color:#f1f1f1;pointer-events:none}
.tok.tok-wide .tokui{width:min(500px,calc(36vw - 56px))}
.tokbtns{position:absolute;left:calc(100% + 12px);bottom:8px;width:72px;
  display:flex;flex-direction:column;align-items:center;gap:16px}
.tokstage.wide .tokbtns{left:auto;right:12px;bottom:18px}
.tokaction{display:flex;flex-direction:column;align-items:center;gap:6px;color:#f1f1f1}
.tokbtns button{position:relative;transition:scale calc(var(--spring-press-ms) * 1ms) var(--spring-press),background .14s}
.tokbtns button:hover{scale:1.05;border-color:rgba(255,255,255,.5)}
.tokbtns button:active{scale:.96;transition:scale .06s ease-out}
.tokbtns button:focus-visible{outline:2px solid var(--tungsten);outline-offset:3px}
.toklabel{font-size:var(--fs-xs);font-weight:500;line-height:1.25;white-space:nowrap}.toklabel b{font:inherit}
/* 进度条。可见部分只有 4px，但命中区得够高——屏幕最下沿的 4px 是鼠标最难瑞的
   那一条。所以 .tokbar 本体是 20px 的透明热区，轨道和已播部分画在它底部，
   鼠标靠近（或拖动中）才变粗。touch-action:none 是为了让触控拖动不被当成页面滚动。 */
.tokbar{position:absolute;left:0;right:0;bottom:0;height:20px;z-index:3;
  cursor:pointer;touch-action:none;display:flex;align-items:flex-end}
.tokbar::before{content:"";position:absolute;left:0;right:0;bottom:0;height:4px;
  background:rgba(255,255,255,.16);transition:height .12s}
.tokbar i{position:relative;display:block;height:4px;width:0;background:var(--tungsten);
  transition:height .12s}
.tokbar:hover::before,.tokbar:hover i,
.tokbar.scrubbing::before,.tokbar.scrubbing i{height:9px}
.tokauthor{display:flex;align-items:center;gap:10px;margin-bottom:12px;font-size:var(--fs-md);line-height:1.2}
.tokauthor>a{color:#f1f1f1;font-weight:600;text-decoration:none;pointer-events:auto}
.tokauthor>a:hover{color:var(--tungsten);text-decoration:none}.tokauthor>span{color:#aaa;font-size:var(--fs-sm)}
.tokavatar{position:relative;width:40px;height:40px;flex:none;overflow:hidden;border:0;border-radius:50%;padding:0;
  background:#333;color:#f1f1f1;cursor:pointer;pointer-events:auto}
.tokavatar img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
.tokavatar .ini{position:absolute;inset:0;display:grid;place-items:center;font-weight:600}
.tokavatar:hover{box-shadow:0 0 0 2px #f1f1f1}
/* 标题是进详情页的入口，所以是按钮；但它在画面上就是一行字，
   不能长成按钮的样子。只在悬停时用下划线提示可点，和旁边的创作者链接一致。 */
.tokui .toktitle{display:block;max-width:100%;margin:0;padding:0;border:0;background:none;
  color:inherit;font:inherit;font-size:var(--fs-xl);font-weight:600;line-height:1.25;text-align:left;cursor:pointer;pointer-events:auto;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tokui .toktitle:hover{color:var(--ink-2)}
.tokbtns button[aria-pressed="true"]{background:#f1f1f1;color:#0f0f0f}
.tokclose{position:absolute;top:15px;right:15px;width:42px;height:42px;background:rgba(0,0,0,.5);color:#EEE;
  border:0;border-radius:50%;padding:0;display:grid;place-items:center;cursor:pointer}.tokclose svg{width:21px;height:21px;stroke:currentColor;fill:none}
.tokhint{display:none}

.loadmore{min-height:42px;margin:18px auto 0;text-align:center;color:var(--muted);padding:12px}
.empty{color:var(--muted);padding:20px 0;text-align:center}
/* Geist Scroller：内容只沿指定轴滚动，边缘渐隐负责提示仍有内容。 */
.geist-scroller{position:relative;min-width:0;min-height:0;overflow:hidden}
.geist-scroller-container{box-sizing:border-box;width:100%;height:100%;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;scrollbar-width:none}
.geist-scroller-container::-webkit-scrollbar{display:none}
.geist-scroller-container[data-overflow="y"]{overflow:hidden auto}.geist-scroller-container[data-overflow="x"]{overflow:auto hidden}.geist-scroller-container[data-overflow="both"]{overflow:auto}
.geist-scroller-overlay{position:absolute;z-index:2;inset:0;pointer-events:none}
.geist-scroller-overlay::before,.geist-scroller-overlay::after{content:'';position:absolute;left:0;right:0;height:40px;opacity:0;transition:opacity .18s ease}
.geist-scroller-overlay::before{top:0;background:linear-gradient(to bottom,var(--surface),transparent)}
.geist-scroller-overlay::after{bottom:0;background:linear-gradient(to top,var(--surface),transparent)}
.geist-scroller-overlay.can-scroll-top::before,.geist-scroller-overlay.can-scroll-bottom::after{opacity:1}
/* Geist Empty State：标题和说明是一个组件；外层容器只负责与上方工具条保持 16px。 */
.emptystate{grid-column:1/-1;display:grid;justify-items:center;align-content:center;gap:8px;box-sizing:border-box;width:100%;min-height:320px;padding:48px 24px;text-align:center;border:1px solid var(--line-soft);border-radius:var(--floating-radius);background:var(--ground)}
.emptystate.compact{min-height:220px;border:0;background:transparent}
.emptystate .es-icon{display:grid;place-items:center;box-sizing:border-box;width:54px;height:54px;padding:10px;border-radius:var(--floating-radius);border:1px solid var(--border-15);color:var(--muted);margin-bottom:8px}
.emptystate .es-icon svg{width:32px;height:32px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.emptystate .es-copy{display:grid;justify-items:center;gap:8px}
.emptystate h3{margin:0;font-size:var(--fs-md);font-weight:600;color:var(--ink)}
.emptystate p{margin:0;max-width:340px;font-size:var(--fs-sm);line-height:1.55;color:var(--muted)}
.emptystate .es-actions{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;margin-top:8px}
.selectiondock{position:fixed;left:50%;bottom:max(22px,env(safe-area-inset-bottom));z-index:180;transform:translateX(-50%);display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:7px;width:max-content;max-width:calc(100vw - 24px);box-sizing:border-box;
  padding:9px 11px;border:1px solid var(--line);border-radius:var(--floating-radius);background:var(--surface);
  box-shadow:0 8px 24px #0005}.selectiondock[hidden]{display:none}
.selectiondock>button{white-space:nowrap}
.selectiondockcount{padding:0 8px;color:var(--ink-2);white-space:nowrap}
.tagselection.selectiondock{margin:0}
.selectiondock [hidden]{display:none}
body:has(#batchbar:not([hidden])) #stage{padding-bottom:180px}
#index:has(.tagselection:not([hidden])){padding-bottom:220px}
.batchbar span{padding:0 8px;color:var(--ink-2);white-space:nowrap}.batchbar button{border:0;border-radius:var(--floating-radius);
  box-shadow:0 0 0 1px var(--line-soft);background:var(--ground);color:var(--ink);padding:9px 12px;cursor:pointer}.batchbar button:hover{background:var(--hover);color:var(--ink)}
.batchbar button svg{width:17px;height:17px;margin-right:6px;vertical-align:-4px;stroke:currentColor;fill:none;stroke-width:2}

@media (max-width:1080px){.sgrid,.sgrid.mixgrid{grid-template-columns:minmax(0,1fr)}
  .sgrid.mixgrid{grid-template-areas:"media" "side" "queue"}
  .side,.sgrid.mixgrid>.side{grid-column:auto;border-left:0;border-top:1px solid var(--line-soft)}
  .sidecontent{height:auto;max-height:none}
  .mixqueue,.sgrid.mixgrid>.mixqueue{max-height:420px;border-left:0;border-top:1px solid var(--line-soft)}}
@media (max-width:760px){
  .readout{display:none} .brand .sub{display:none}
  :root{--tile:168px;--topH:52px;--sortH:60px}
  /* 确认弹层的两个键是本项目 44px 命中区的对象，桌面的 32px 是 Geist 控件高度。 */
  .geist-modal-footer .geist-button{min-height:44px;padding:0 14px}
  /* 管理入口在手机上先收成一行，按需展开为两列。当前页仍有明确上下文，
     但不会让八个入口挤成三四行、把正文推到屏幕下方。 */
  .managebar{display:block;width:100%;margin-bottom:14px;padding:0;border:0;background:transparent}
  .managebar-toggle{display:flex!important;width:100%;min-height:46px;align-items:center;justify-content:space-between;
    padding:10px 12px!important;border:1px solid var(--line-soft)!important;border-radius:var(--floating-radius)!important;background:var(--ground)!important;color:var(--ink)!important}
  .managebar-current{display:inline-flex;align-items:center;gap:8px;font-weight:600}
  .managebar-toggle>svg:last-child{transition:transform .16s ease}
  .managebar.is-open .managebar-toggle>svg:last-child{transform:rotate(180deg)}
  .managebar-menu{display:none;margin-top:8px;padding:6px;border:1px solid var(--line-soft);border-radius:var(--floating-radius);
    background:var(--ground);grid-template-columns:repeat(2,minmax(0,1fr));gap:4px}
  .managebar.is-open .managebar-menu{display:grid}
  .managebar-menu button{width:100%;min-height:42px;justify-content:flex-start;padding:9px 11px}
  .cleanupgrid{grid-template-columns:minmax(0,1fr)}
  /* 说明行右端的动作在手机上要够手指点：桌面 32px 是 Geist 的控件高度，触摸区按本项目
     的 44px 命中区来。 */
  .pagelede-actions .batchaction{height:44px;padding-inline:16px}
  .count{align-items:center;flex-direction:row;gap:10px;padding-block:8px;
    overflow-x:auto;overflow-y:hidden;scrollbar-width:none;overscroll-behavior-inline:contain}
  .count::-webkit-scrollbar{display:none}
  .count>span:first-child{line-height:36px;white-space:nowrap}
  .count .sorts{width:max-content;margin-left:0;flex:0 0 auto;overflow:visible}
  .count .sorts button{min-height:36px}
  .count.junkcount{align-items:stretch;flex-direction:column;overflow:visible;padding-block:8px}
  .junkcount .junksummary{line-height:1.4}.junkfilters{width:100%;padding-bottom:2px}
  .reviewtabs button,.junkfilters a{height:44px}.junkactions button{min-height:44px;padding-inline:7px}
  #batchbar[hidden]{display:none}
  .batchbar:has([data-junk-batch]:not([hidden])){left:12px;right:12px;bottom:12px;width:auto;transform:none;
    display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px;padding:9px 10px}
  .batchbar:has([data-junk-batch]:not([hidden]))>span{grid-column:1/-1;padding:0;text-align:center}
  .batchbar:has([data-junk-batch]:not([hidden])) button{display:inline-flex;min-width:0;min-height:44px;
    align-items:center;justify-content:center;padding:8px 6px;white-space:nowrap;font-size:var(--fs-xs)}
  .batchbar:has([data-junk-batch]:not([hidden])) button[hidden]{display:none}
  .batchbar:has([data-junk-batch]:not([hidden])) button svg{margin-right:4px}
  .playlistpage>header{align-items:stretch;flex-direction:column}.playlistcreate label{max-width:none}.playlistcreate input{min-width:0}
  .fb{width:100%}.fb button{flex:1 1 0;min-width:38px}
  /* 关注详情只有三四个动作键，撑满一行就是四个大得离谱的方块。 */
  .fb.followdetailactions{width:max-content;max-width:100%}
  .fb.followdetailactions button,.fb.followdetailactions .fdownload{flex:0 0 auto}
  /* 顶栏在窄屏会被搜索框撑破 —— 搜索收成一个按钮，点了才展开。只裁横向：竖向也裁的话，
     那几枚玻璃按钮往下落的投影在顶栏下沿被切平，头像那一排上方就多出一条横线。 */
  .top{gap:4px;padding:0 8px;overflow:clip visible}
  /* 顶栏 fixed、正文让出同样的高度，与 YouTube 手机版同一写法：`ytm-mobile-topbar-renderer`
     fixed 贴顶，`ytm-app` 让出 48px。吸顶顶栏留在文档流里，iPhone 上一聚焦搜索框，Safari 把
     整页往上滚来显露它（用户录屏量得约 200px），筛选框也跟着翻出来。取证与未取得的部分见
     docs/reference-snapshots/youtube-mobile-topbar-measured.md。 */
  .top{position:fixed;top:0;left:0;right:0}
  body{padding-top:var(--topH)}
  .top:has(.search.open){overflow:visible}
  .brand h1{font-size:var(--fs-lg)}
  .ib{width:36px;height:36px}
  .ib svg{width:19px;height:19px}
  /* 展开的搜索栏不铺满整条：左边留出返回按钮，和 YouTube 手机版一致——
     否则进了搜索就没有退出的入口，只能靠失焦或 Esc。 */
  /* 左右留白 8、返回键与搜索框间距 4、控件同高 36，按顶栏实际高度垂直居中。 */
  .search{position:absolute;left:48px;right:8px;top:calc(50% - 18px);height:36px;padding:0 14px;
    max-width:none;margin:0;display:none;z-index:5;background:var(--surface)}
  .search.open{display:flex}
  /* 展开搜索时整条顶栏让位：筛选、品牌和右侧动作全部收起，只留返回箭头和输入框。
     不收起的话筛选按钮和返回箭头会叠在同一个位置上。 */
  .top:has(.search.open) #filterBtn,
  .top:has(.search.open) .brand,
  .top:has(.search.open) #searchBtn,
  .top:has(.search.open) #immerseBtn,
  .top:has(.search.open) #selectMode,
  .top:has(.search.open) #settingsBtn{display:none!important}
  .top:has(.search.open) .searchback{display:inline-flex;position:absolute;left:8px;top:calc(50% - 18px);z-index:6}
  /* 下拉栏以搜索框为包含块（搜索框自身是 absolute），往左伸 40px 盖过返回键那一列，
     和顶栏两侧同样留 8。不用 fixed：玻璃层的 backdrop-filter 会让搜索框成为 fixed 元素的
     包含块，关掉透明效果时又变回视口，两种情形下拉栏落点不同。 */
  .searchmenu{position:absolute;left:-40px;right:0;top:calc(100% + 8px);max-height:70vh;overflow:auto;overscroll-behavior:contain}
  #searchBtn{display:inline-flex!important}
  /* 移动端不需要密度切换和筛选抽屉按钮挤在一起 */
  #density{display:none}
  /* 宽屏靠 `.search{flex:1}` 把动作按钮顶到右边；窄屏搜索框收成绝对定位后脱离
     了流，没有东西再撑开中间，按钮会全部挤在品牌名右侧、右半条留空。
     动作组的第一个按钮吃掉剩余空间，整组回到右边。 */
  #searchBtn{margin-left:auto}
  /* 窄屏改成单列居中，头像在名字上方——参考 beeg 的资料页。
     左像右文那套是给宽屏的：手机上 92 px 头像旁边只剩两百多像素，别名和链接被挤成
     两三行，头像下面又空着一大片。居中之后名字和链接都拿到整行宽度。 */
  .entityprofile{grid-template-columns:minmax(0,1fr);gap:12px;padding:16px;justify-items:center;text-align:center}
  /* 单列时头像上下各是一整行，没有「右边那列有多高」可跟，尺寸就还是钉住的 96px。 */
  .entityportrait{height:96px;min-height:96px;max-height:96px;width:96px}.entityhero h2{font-size:var(--fs-xl)}
  .entityidentity{width:100%}
  .entityfoot{padding:10px 16px}
  /* 标题行是 flex，`text-align:center` 管不到它，得自己居中。 */
  .entitytitle{justify-content:center}
  /* 这枚开关紧挨着名字，画成 44px 的方块就比名字本身还重一档，一屏里最先看见的
     成了它。所以画的仍是 32px，命中区由一层伸出去、不着墨的方块补到 44px——
     指头点的是那一层。菜单项没有这个顾虑，行本来就该有 44px 高。 */
  .npbtn{position:relative}
  .npbtn::after{content:"";position:absolute;top:50%;left:50%;width:44px;height:44px;
    transform:translate(-50%,-50%)}
  .npmenu button,.popmenu.gselectmenu button{min-height:44px}
  .entityhero .alias{margin-bottom:10px}
  /* 女优页那一行三项是 flex，`text-align:center` 同样管不到；别名那一项折行时「+N」也居中。 */
  .entityhero .alias.metaline,.entityhero .aliasline{justify-content:center}
  /* 这一排外链窄屏下横滚，不换行：十条链接摊成四行是 192px，顶着头像和名字
     底下，把作品列表整个推到折线以外，而它是「顺便看一眼」的东西。收成一行加
     滑动，跟筛选条那条 `.filterscroll` 同一个做法。
     居中要写 `safe center`：普通的 `center` 在装不下时把溢出的那半推到滚动起点
     之前，那几条怎么滑都够不着。safe 的意思就是一旦溢出就改成贴左。
     `overflow-x` 一开，竖直方向也就不能再是 visible，只能钉成 hidden；焦点环画在
     药丸外 2px，所以上下各让出 4px 再用负外边距收回来，键盘走到最后一条时环还在。
     滚动的那一层要顶到卡的左右边：收在卡的 16px 内边距里的话，最后那枚药丸是在卡当中
     被切一刀，看着像画坏了；顶到边上切口就落在卡沿，和筛选条滚到屏幕边是同一个读法。
     负外边距把层撑到边，等量的内边距把药丸推回原位，`scroll-padding-inline` 让键盘
     滚过去的那一枚也停在这个内边距里。 */
  .entityhero .entitylinks{justify-content:safe center;flex-wrap:nowrap;
    margin-top:9px;margin-bottom:-4px;margin-inline:-16px;padding-block:4px;padding-inline:16px;
    scroll-padding-inline:16px;max-width:none;
    overflow-x:auto;overflow-y:hidden;scrollbar-width:none;overscroll-behavior-inline:contain}
  .entityhero .entitylinks::-webkit-scrollbar{display:none}
  .entityhero .entitylinks>*{flex:none}
  /* 看片那一行至多几枚，窄屏下换行就够，不值得再接一套横滑；整行跟着单列卡居中。 */
  .entityhero .entrymarks{justify-content:center}
  .stats h2{font-size:var(--fs-xl)}.stats .insightcopy h2{font-size:var(--fs-3xl)}
  /* 窄屏下标题也得一起缩。`/follow` 渲染在 `#stats` 里，上面那条 `.stats h2`
     会只把它打到 23px，而 `#manageTitle` 在 `#stats` 外面，手机上又变成两种大小。 */
  .pagetitle,.listtitle,.managetitle,.index .ihead h2,.playlistpage h2{font-size:var(--fs-2xl)}
  .kv .u{display:block;margin-left:0;margin-top:2px}
  .ctags .tg:nth-child(n+3){display:none}
  .tokstage,.tokstage.wide{inset:0;width:100%;height:100%;aspect-ratio:auto;transform:none}
  .toktrack{border-radius:0;box-shadow:none}
  .tokbtns{left:auto;right:max(8px,env(safe-area-inset-right));bottom:92px;width:56px;gap:14px}
  .tokstage.wide .tokbtns{left:auto;right:max(8px,env(safe-area-inset-right));bottom:92px}
  .tokui{left:14px;right:82px;bottom:28px;width:auto;padding:0 0 env(safe-area-inset-bottom)}
  .tok.tok-wide .tokui{width:auto}
  .tokauthor{margin-bottom:8px}.tokauthor>span{display:none}.tokavatar{width:36px;height:36px}
  .tokui .toktitle{font-size:var(--fs-md)}
  .tokclose{top:max(10px,env(safe-area-inset-top));right:max(10px,env(safe-area-inset-right))}
}
/* 这个 16px 是 iOS 的阈值，不是刻度里的一档：Safari 对小于 16px 的输入框会自动放大页面。
   所以它写死，不跟 --fs-lg 走——刻度改一档就把这个防放大保护悄悄破坏掉了。 */
@media (max-width:760px){input,textarea,select{font-size:16px!important}}
/* 标题分两级往下收：平板宽度走 24px，真手机才到 20px。一步从 32 掉到 20 会把
   页面标题压得比它下面 24px 的指标数字还小，层级整个翻过来。24 这一档是 Geist
   自己就有的 Heading 24，不是新开的字号。 */
@media (max-width:640px){
  .pagetitle,.listtitle,.managetitle,.index .ihead h2,.playlistpage h2{font-size:var(--fs-xl)}
}
#searchBtn{display:none}
.searchback{display:none}

/* JAV 版式条。只在 JAV 语境出现——首页混着创作者作品与素人流出，
   给它们切「封面」没有意义。 */
.javbar{display:flex;align-items:center;gap:8px;padding:0 0 10px;flex-wrap:wrap}
.javbar[hidden]{display:none}
.javbar button{width:34px;height:30px;display:inline-grid;place-items:center;border-radius:var(--control-radius);border:1px solid var(--overlay-10);background:transparent;color:var(--muted);cursor:pointer;padding:0}.javbar button svg{width:17px;height:17px;stroke:currentColor;fill:none;stroke-width:2}.javbar button:hover{color:var(--ink)}.javbar button[aria-pressed="true"]{background:var(--picked);color:var(--ink)}
.iconswitch{min-width:0;margin:0;padding:3px;border:1px solid var(--line-soft);border-radius:var(--surface-radius);background:var(--overlay-5)}.iconswitch label{position:relative;display:inline-grid;width:34px;height:28px;place-items:center;border-radius:var(--control-radius);color:var(--muted);cursor:pointer}.iconswitch input{position:absolute;inline-size:1px;block-size:1px;opacity:0}.iconswitch label:hover{color:var(--ink)}.iconswitch label:has(input:checked){background:var(--surface);color:var(--ink);box-shadow:0 1px 2px #0005}.iconswitch label:has(input:focus-visible){outline:2px solid var(--tungsten);outline-offset:2px}.iconswitch svg{display:block;width:17px;height:17px;stroke:currentColor;fill:none;stroke-width:2}

/* 共用勾选框：站内此前一半地方用原生 checkbox（只调了 accent-color，未选中态由浏览器
   自绘、没有悬停反馈），另一半用关注列表自绘的那份，两种画法并排出现。统一成这一份。
   悬停只抬填充不提边，与按钮同口径。 */
.pcheck{position:relative;display:grid;place-items:center;width:20px;height:20px;flex:none;cursor:pointer}
.pcheck input{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
.pcheck>span{box-sizing:border-box;width:16px;height:16px;border:1px solid var(--border-15);border-radius:var(--badge-radius);
  display:grid;place-items:center;background:var(--ground);color:transparent;transition:.14s ease}
.pcheck>span svg{display:block;width:13px;height:13px;stroke:currentColor;fill:none;stroke-width:2.5}
.pcheck:hover>span,label:hover>.pcheck>span{background:var(--hover)}
.pcheck input:checked+span{border-color:var(--ink-2);color:var(--ink)}
.pcheck input:focus-visible+span{outline:2px solid var(--tungsten);outline-offset:2px}
.pcheck input:disabled+span{cursor:not-allowed;border-color:var(--line)}
.pcheck input:disabled:checked+span{color:var(--muted)}
.pcheck:has(input:disabled){cursor:not-allowed}
.pcheck:has(input:disabled)>span{background:var(--ground)}
/* 取景：正封模式下，整张封套要把镜头推到右半边；本身就是竖版正封的则整张用；
   16:9 官方剧照没有「正封那一块」，只能按人脸取。
   判据是图片自己的宽高比，由 onload 写进 data-frame。
   封面不垫底色：`.poster` 的黑底铺满整格，会盖住 `.pic::before` 那层模糊底，还会从圆角
   边缘的半透明像素里透出一圈灰边（`12-cards.css` 里 `.pic` 那一条同理）。 */
.poster.cover{width:100%;height:100%;object-fit:cover;display:block;background:none}
/* --cover-x / --cover-y 都由人脸检出写入。哪个轴生效取决于容器和图片谁更竖：
   `object-fit:cover` 只裁一个轴，另一个轴的锚点是死值。 */
/* 没有折痕数据的封套只能贴最右边缘：那是「正封顶着封套右缘」这条规律的最小假设。 */
.poster.cover.front[data-frame="sleeve"]{object-position:100% var(--cover-y,22%)}
/* 有框数据的走这一档：`clip-path` 把框外面四边都切掉，剩下的那一块由 `--panel-left`
   与 `--panel-top` 摆正、`--panel-height` 放到铺满卡片高度。`inset:0` 定死了 left/right，
   和这里的 left+width 过约束，显式撤掉再写，不靠浏览器取舍；元素盒本身就是原始宽高比，
   `object-fit` 在这里三种取值等价，写 fill 表明「不要再插手缩放」。
   四个值都由 `panelFrame` 算好：沿折痕和按比例算出来的框满高贴右缘，top 为 0、
   height 为 100%，纵向一个像素都不裁；只有人手工横着框才用得上另两个轴。 */
.poster.cover.front.panel{inset:var(--panel-top,0%) auto auto var(--panel-left,0%);width:auto;
  max-width:none;height:var(--panel-height,100%);object-fit:fill;
  clip-path:inset(var(--panel-clip,0 0 0 0))}
.poster.cover.front[data-frame="front"]{object-position:50% var(--cover-y,22%)}
/* 剧照比大图容器宽得多，裁的是横向，所以横向锚点必须跟着人走。没检出时回落
   居中：那是取不到人脸时唯一站得住的猜测。 */
.poster.cover.front[data-frame="still"]{object-position:var(--cover-x,50%) var(--cover-y,22%)}
/* 整张放进卡片：两侧留白透出 `.pic::before` 那层模糊底。 */
.poster.cover.whole{object-fit:contain;object-position:50% 50%}


/* 版式按钮嵌在排序行里，必须比 `.sorts button` 更具体，否则那条规则的内边距和
   行高会把图标顶偏。图标按钮不设左右内边距，靠 grid 居中。 */
/* `flex:none` 是必须的，不是保险。`.sorts` 是不换行的横向滚动条，里面的项默认可收缩，
   而 `.iconswitch` 自己带着 `min-width:0`（fieldset 需要它才不撑破容器）——两条合起来
   允许它被压到内容宽度以下。窄屏上排序按钮一多，三个 34 px 的版式按钮就会叠在一起，
   还压住旁边的「发行时间」。`.sorts button` 早就有 `flex:0 0 auto`，但 fieldset 不是
   button，那条规则选不到它。照片那一排的大小开关是同一个控件，走同一条。 */
.sorts .iconswitch{display:inline-flex;gap:2px;margin:0 6px;flex:none}


/* ── 脱盘模式 ────────────────────────────────────────────────────────────────
   来源盘不在时只置灰该来源的筛选，其余来源照常。数量仍然显示：账本是本机真相，
   脱的是盘不是记录。 ── */
.chip.offline{opacity:.42;cursor:not-allowed;text-decoration:none}
.chip.offline .n{text-decoration:none}
.gate.offline{cursor:default;border-style:solid;border-color:var(--meter);gap:7px}
.gate.offline b{letter-spacing:.08em}
.gate.offline span{max-width:34ch;text-align:center;line-height:1.6}
.gate.offline .chip{cursor:pointer;margin-top:4px}


.gate.offline{box-sizing:border-box;min-height:220px;padding:24px;background:var(--ground);color:var(--ink)}
.gate.offline>svg{width:32px;height:32px;flex:none;fill:none;stroke:currentColor;stroke-width:2}
.gate.offline span{color:var(--ink-2)}
/* ── 在线追更 ── */
.follow{padding:4px 0 42px}
.followhead{display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:20px}
.followhead .pagetitle{margin:0}
.fheadactions{display:inline-flex;align-items:center;gap:8px;flex:none}
.follow .fcheck{font-weight:500}
.follow .fcheck svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:2}
.followauthors{padding:3px 0 10px;margin:0 0 2px;overscroll-behavior-inline:contain}
.followauthors .av{width:76px}
.followauthors .av[aria-pressed="true"] .ring{box-shadow:0 0 0 2px var(--ink)}
.followauthors .favatar{width:64px;height:64px;border-radius:50%;outline:0}
.followauthors .favatar.none{font-size:var(--fs-lg);background:var(--overlay-5)}
/* 关注的筛选条就是首页那条 `.tagbar`，跟读数那排 `.count` 一起收进同一块浮层：
   左端五枚状态钉住、右半截来源和标签横滚，分工、尺寸和材质都取自那边，这里只补
   来源图标键自己的身量——它是一枚只有图标的药丸。 */
.followfilters .sourcepill{width:34px;padding:0}
.followfilters .sourcepill .ficon{width:18px;height:18px;margin:0}
.ficon{width:14px;height:14px;border-radius:var(--badge-radius);object-fit:contain;
  vertical-align:-2px;margin-right:5px}
.followlist{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--tile),1fr))}
.followlist.followphotowall{grid-template-columns:repeat(5,minmax(0,1fr));gap:14px 10px;align-items:start}
/* 固定比例视图的媒体框在懒加载前占好高度。 */
@media (max-width:1400px){.followlist.followphotowall{grid-template-columns:repeat(4,minmax(0,1fr))}}
@media (max-width:1000px){.followlist.followphotowall{grid-template-columns:repeat(3,minmax(0,1fr))}}
.followitem{min-width:0}
.followphotowall[data-images-only="true"] .followitem :is(.meta,.badge,.mixbadge,.fstate){display:none}
.followphotowall[data-images-only="true"] .skeletoncard :is(b,em){display:none}
.followitem.imagecard{display:flex;width:100%;margin:0}
.followitem.imagecard .pic{aspect-ratio:4/3;min-height:0;background:var(--sunk)}
.followitem.imagecard .followvisual .pic>img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:var(--sunk)}
/* 翻动面板遮住静止封面，底色要跟着这张卡自己的底走：图片墙是浅底，视频卡是黑底。
   面板里每一张面也是 `.poster`，它自带的黑底是给视频卡留白用的，在浅底的图片卡上一悬浮
   就把留白翻成黑的，所以两层一起换。 */
.followitem.imagecard :is(.mixfaces,.mixface .poster){background:var(--sunk)}
.followitem a{text-decoration:none}.followitem a:hover{text-decoration:none}
/* 已看的卡片保持原样：看过不等于不想再看，只有忽略的才淡下去。 */
.followitem[data-status="ignored"]{opacity:.62}
.followvisual{position:relative;isolation:isolate}.followvisual .pic{z-index:1}
.followvisual .pic>img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;display:block;background:#000}
.fnothumb{position:absolute;inset:0;display:grid;place-items:center;color:var(--muted);font-size:var(--fs-xs)}
.followitem .badge .ficon{width:18px;height:18px;margin:0}
.fsourceavatar{display:grid;place-items:center;cursor:default;background:var(--overlay-5);border:1px solid var(--line-soft)}
.fsourceavatar:hover{box-shadow:none}.fsourceavatar .favatar{position:absolute;inset:0;width:100%;height:100%;border-radius:50%;outline:0;object-fit:cover}
.fsourceavatar .favatar.none{display:grid;place-items:center;font-size:var(--fs-sm);color:var(--muted)}
.followitem .cardtitle{display:block;width:100%;border:0;background:transparent;color:inherit;padding:0;text-align:left;font-family:inherit;cursor:pointer;text-decoration:none}
.followitem .tg,.followdetailtags .tg{--r34-tag:#55A7FF;border-color:color-mix(in srgb,var(--r34-tag) 42%,transparent);background:color-mix(in srgb,var(--r34-tag) 10%,transparent)}
.followfilters .pill[class*="r34-"]{--r34-tag:#55A7FF}
.followfilters .pill[class*="r34-"][aria-pressed="true"]{border-color:color-mix(in srgb,var(--r34-tag) 72%,transparent);background:color-mix(in srgb,var(--r34-tag) 22%,transparent);color:var(--glass-text,var(--ink))}
.followitem .r34-artist,.followdetailtags .r34-artist,.followfilters .r34-artist{--r34-tag:#E36C6C}
.followitem .r34-character,.followdetailtags .r34-character,.followfilters .r34-character{--r34-tag:#68C76F}
.followitem .r34-copyright,.followdetailtags .r34-copyright,.followfilters .r34-copyright{--r34-tag:#D675D6}
.followitem .r34-metadata,.followdetailtags .r34-metadata,.followfilters .r34-metadata{--r34-tag:#F5A24A}
.followdetailtags .r34-unknown{--r34-tag:var(--muted)}
.followitem .meta .s{min-height:21px}.fbadges{display:flex;flex-wrap:nowrap;gap:5px;min-width:0;overflow:hidden}
.followitem .meta .followbyline{display:flex;flex-wrap:nowrap;gap:6px;min-width:0;overflow:hidden}
.followbyline .followauthor{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.followbyline time{flex:none;white-space:nowrap}
/* 署名行只在作者换成发布者时出现，弱于作者名，一行放不下就截断。 */
.followitem .meta .followcredit{min-height:0;font-size:var(--fs-xs);color:var(--muted);overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}
/* 标题前的版本字样。「含 WIP」说的是同组里还有一条半成品，这一条本身不是，配色弱于
   WIP 本体。配音版着色带底，无声版弱化加虚线框。 */
.javedition.followmark{margin-right:6px;color:var(--ink-2)}
.javedition.followmark.wip{color:var(--meter)}
.javedition.followmark.wip.partial{color:var(--muted)}
.javedition.followmark.voiced{color:var(--keep);background:color-mix(in srgb,var(--keep) 16%,transparent)}
.javedition.followmark.silent{color:var(--muted);border-style:dashed}
.fbadge{padding:2px 8px;border:1px solid var(--border-15);border-radius:var(--badge-radius);font-size:var(--fs-xs);color:var(--ink-2)}
.fbadge.dup{border-style:dashed;display:inline-flex;align-items:center;gap:4px}.fbadge.dup .ficon{margin:0}
.fnote{font-size:var(--fs-xs);color:var(--muted)}
/* 右上角：合并计数与主页卡片的版本角标同一个位置，右下角只留给时长。
   悬停时计数让位，操作键占住右上角；触屏上操作键常驻，排在计数下面。 */
.fcorner{position:absolute;right:10px;top:10px;z-index:7;display:flex;flex-direction:column;align-items:flex-end;
  gap:6px;pointer-events:none}
.followitem .mixbadge{position:static;border:0;cursor:pointer;font:inherit;pointer-events:auto}
.factions{display:flex;gap:6px;align-items:center;opacity:0;pointer-events:none;transition:opacity .15s ease}
.factions button{width:36px;height:36px;border:1px solid rgba(255,255,255,.16);border-radius:50%;padding:0;
  display:grid;place-items:center;background:rgba(0,0,0,.48);color:#fff;cursor:pointer;
  backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px)}
.factions button svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2}
.factions button:hover:not(:disabled){background:rgba(255,255,255,.18);transform:scale(1.08)}
.factions button:disabled{opacity:.42;cursor:not-allowed}
@media (hover:hover) and (pointer:fine){.followitem:hover .factions{opacity:1;pointer-events:auto}
  .followitem:hover .fcorner .mixbadge{display:none}}
@media (hover:none),(pointer:coarse){.followitem .factions{opacity:1;pointer-events:auto}}
.select-mode .followitem a,.select-mode .followitem [data-follow-detail],.select-mode .followitem .factions,.select-mode .followitem .mixbadge{pointer-events:none!important}
.fstate{flex:1 1 100%;font-size:var(--fs-xs);color:var(--drop)}
/* 详情右栏是纵向 flex：`flex:1 1 100%` 在这里会把整栏的剩余高度都算给这条
   错误行，标签被推到栏底、中间空出一大段。错误行按内容高就够。 */
.followdetailside .fstate{flex:none}
.fvkind{display:inline-block;max-width:120px;margin-right:6px;padding:1px 7px;border-radius:var(--badge-radius);overflow:hidden;text-overflow:ellipsis;vertical-align:-2px;background:var(--overlay-5);color:var(--muted);font-size:var(--fs-xs);font-style:normal}
.fvkind.wip{color:var(--meter)}.fvkind.alt{color:var(--ink-2)}
/* 合集队列卡的出处与时间：标记、站点图标和时间按盒子居中成一行，不靠基线偏移去凑。
   标记可收窄（自带尾部省略），时间不收。 */
.followqueue .fqmeta{display:flex;align-items:center;gap:6px}
.followqueue .fqmeta .ficon{flex:none;margin:0}
.followqueue .fqmeta .fvkind{flex:0 1 auto;min-width:0;margin:0}
.followqueue .fqmeta time{flex:none;white-space:nowrap}
/* 媒体框不设视口高度的地板：里面的播放器高度由 16:9 和自己的宽度推出来，地板挂在 vh 上，
   两个量朝相反方向走——414×913 的视口给出 566px 的框装 208px 的画面，上下各空 179px；
   1280×900 的宽屏给出 558px 的框装 435px 的画面，多出的 123px 就是画面之外那层黑边。
   播放器未初始化时 `.vwrap>.video-js` 那条 16:9 已经把高度定住了，不会塌。 */
.followdetailmedia{--follow-image-arrow-inset:16px}.followdetailmedia.image{background:color-mix(in srgb,var(--video-glow,#15202a) 12%,var(--surface) 88%)}.followdetailposter.zoomable{cursor:zoom-in}.followdetailposter{position:relative;z-index:1;display:block;width:auto;height:auto;max-width:100%;max-height:76vh;object-fit:contain;background:transparent}.followdetailmedia.framed .followdetailposter{width:100%;aspect-ratio:var(--follow-frame-ratio)}.followimagearrow{position:absolute;z-index:4;top:50%;transform:translateY(-50%)}.followimagearrow.prev{left:var(--follow-image-arrow-inset)}.followimagearrow.next{right:var(--follow-image-arrow-inset)}.followimagedots{position:absolute;z-index:4;left:50%;bottom:18px;transform:translateX(-50%);display:flex;align-items:center;justify-content:center;gap:8px;max-width:calc(100% - 120px);padding:8px 11px;border-radius:var(--pill-radius);background:rgba(0,0,0,.5);overflow:hidden}.followimagedots button{flex:none;width:8px;height:8px;padding:0;border:0;border-radius:50%;background:rgba(255,255,255,.42);cursor:pointer}.followimagedots button[aria-current="true"]{width:20px;border-radius:var(--pill-radius);background:#fff}
/* 什么都没有的那一格占同一个 16:9 框：它顶替的就是画面本身，缩成一条窄带看着像没加载完。 */
.followdetailplaceholder{aspect-ratio:16/9;display:grid;place-items:center;gap:10px;color:var(--muted)}.followdetailplaceholder .ficon{width:34px;height:34px;margin:0}
.followdetailtitle{display:flex;gap:5px;align-items:flex-start;margin-bottom:8px}.followdetailtitle .stitle{width:auto;max-width:calc(100% - 35px);margin:0}.followdetailtitle .followorigin{flex:none;width:30px;height:30px;margin:0;display:grid;place-items:center;border-radius:var(--control-radius)}.followdetailtitle .followorigin:hover{background:var(--hover)}
.followdetailidentity{display:flex;align-items:center;gap:10px;margin:2px 0 12px}.followdetailidentity .mav{flex:none}.followdetailidentity b,.followdetailidentity span{display:block}.followdetailidentity>div>span{margin-top:3px;color:var(--muted);font-size:var(--fs-xs)}
.followdetailsummary{margin:0 0 16px;line-height:1.6;color:var(--ink-2);white-space:pre-line}.followdetailtags .tg{max-width:none;overflow:visible;text-overflow:clip;white-space:normal;text-align:left;color:var(--ink-2);font-family:inherit}/* 通用 `.tg:hover` 的填充和文字色被上面按类型着色的两条同权重规则压掉了，
   悬停得按标签自己那个类型色加深，不能回到中性的 `--hover`。 */
.followdetailtags .tg:hover{border-color:color-mix(in srgb,var(--r34-tag) 68%,transparent);
  background:color-mix(in srgb,var(--r34-tag) 18%,transparent);color:var(--ink)}
.followqueue .mixitempic>.fnothumb{position:static;width:100%;height:100%}.followqueue .fnothumb svg{width:22px;height:22px;stroke:currentColor;fill:none;stroke-width:2}
.fnote.followmediaissue{margin:18px 0 8px;color:var(--drop)}.followresources{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 16px}.followresources a{align-items:center;gap:2px;height:34px;padding:0 11px;border:1px solid var(--border-15);border-radius:var(--control-radius);color:var(--ink-2);text-decoration:none}.followresources a:hover{background:var(--hover);color:var(--ink);text-decoration:none}.followresources svg{width:15px;height:15px;stroke:currentColor;fill:none}.followdetailactions{margin-top:2px}
/* 标签沉到侧栏最后：来源站的标签动辄几十个，夹在正文和操作之间就得先滚过整片
   标签云才够得着「已看／忽略」。 */
.followdetailside .followdetailtags{margin:16px 0 0}.followdetailactions button:disabled{opacity:.45;cursor:not-allowed}.followorigin{width:max-content;margin-top:auto;color:var(--muted);text-decoration:none}.followorigin:hover{color:var(--ink);text-decoration:none}
/* 被隐藏的图退到这条恢复带：一行小缩略图，各带一枚撤销角标。整带保持安静
   （灰调、小尺寸），它们是「不在看」的那部分，不能和正片抢视线。 */
.followhiddenmedia{display:flex;align-items:center;gap:10px;margin-top:14px}.followhiddenlabel{color:var(--muted);white-space:nowrap}.followhiddenthumbs{display:flex;flex-wrap:wrap;gap:6px}.followhiddenthumbs button{position:relative;width:40px;height:40px;padding:0;border:1px solid var(--border-15);border-radius:var(--control-radius);background:none;cursor:pointer;overflow:hidden}.followhiddenthumbs button:hover{border-color:var(--ink-3)}.followhiddenthumbs img{width:100%;height:100%;object-fit:cover;display:block;opacity:.55}.followhiddenthumbs i{position:absolute;right:2px;bottom:2px;display:flex;width:16px;height:16px;align-items:center;justify-content:center;border-radius:50%;background:var(--hover);color:var(--ink-2)}.followhiddenthumbs svg{width:11px;height:11px;stroke:currentColor;fill:none}
@media (max-width:640px){.followhead{align-items:center}
  .followauthors{margin-right:-12px;padding-top:3px}
  .followlist.followphotowall{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 8px}
  .followdetailmedia{--follow-image-arrow-inset:10px}.followimagearrow{width:44px;height:44px}.followimagedots{bottom:12px}
}

/* 图片墙共用列数与布局；未加载图片由备用比例占位，加载后使用天然比例。 */
:is(.photowall,.followlist.followphotowall)[data-size]{--photo-columns:5;grid-template-columns:repeat(var(--photo-columns),minmax(0,1fr))}
:is(.photowall,.followlist.followphotowall)[data-size="big"]{--photo-columns:3}
@media(max-width:1400px){:is(.photowall,.followlist.followphotowall)[data-size]{--photo-columns:4}
  :is(.photowall,.followlist.followphotowall)[data-size="big"]{--photo-columns:2}}
@media(max-width:1000px){:is(.photowall,.followlist.followphotowall)[data-size]{--photo-columns:3}
  :is(.photowall,.followlist.followphotowall)[data-size="big"]{--photo-columns:1}}
@media(max-width:640px){:is(.photowall,.followlist.followphotowall)[data-size]{--photo-columns:2}
  :is(.photowall,.followlist.followphotowall)[data-size="big"]{--photo-columns:1}}
:is(.photowall,.followlist.followphotowall)[data-layout="masonry"]{display:block;column-count:var(--photo-columns);column-gap:10px}
[data-layout="masonry"]>.emptystate{column-span:all}
[data-layout="masonry"]>.photocell,[data-layout="masonry"]>.followitem.imagecard{break-inside:avoid;margin:0 0 14px}
[data-layout="masonry"]>.photocell{aspect-ratio:auto}
[data-layout="masonry"]>.photocell img{height:auto;aspect-ratio:auto 1}
[data-layout="masonry"] .followitem.imagecard .pic{aspect-ratio:1}
/* 只有带 width/height 属性的图才放开高度按固有比例占位：瀑布流按卡片高度分列，
   图没加载完高度未知时放手，每张加载完都会把整墙的列再平衡一遍，卡片在列间跳。
   没有尺寸的图保持 1:1 占位框，图在框内 contain——列从首帧起就是稳定的。 */
[data-layout="masonry"] .followitem.imagecard .pic:has(>img[width][height]){aspect-ratio:auto}
[data-layout="masonry"] .followitem.imagecard .followvisual .pic>img[width][height]{position:static;height:auto;aspect-ratio:auto 1}
/* ── 关注管理页 ──
   取证记录见 docs/reference-snapshots/vercel-geist-controls-measured.md。
   上一版把「避免卡片套卡片」误读成「不要任何容器」，做成了没有可读性的裸列表——
   而同一份文档明确警告过不要因为躲开那些默认套路就做出一个无设计的模板。
   2026-08-26 实测 vercel.com/templates：页面底色纯黑，卡片是 8px 圆角 +
   1px `rgb(41,41,41)` 发丝边 + `rgb(10,10,10)` 比页面亮一档的表面，没有阴影。
   所以容器要有框；不要的是**框里再套框**——卡片内部的行只用分隔线。
   控件仍按实测 Geist：32px 高、14px 字、6px 圆角。 */
.followmanage{padding:2px 0 48px;display:grid;gap:20px;width:min(812px,100%);
  margin-left:auto;margin-right:auto;align-items:start;font-size:var(--fs-md)}
.fmain{display:grid;gap:20px;min-width:0}
/* 容器：发丝边 + 比页面亮一档的表面。内部的行不再各自套框。 */
.fsec{min-width:0;border:1px solid var(--field-ring);border-radius:var(--surface-radius);
  background:var(--ground);padding:14px 16px 16px}
/* 面板上下异色：头部条通到卡边（负边距抵消内边距），正文是全站最亮的 --ground，
   那条横条落到 --page 并带一条发丝线；圆角随卡收口。 */
.fsechead{display:flex;align-items:center;gap:12px;flex-wrap:wrap;box-sizing:border-box;min-height:56px;
  margin:-14px -16px 12px;padding:11px 16px;background:var(--overlay-5);
  border-bottom:1px solid var(--line-soft);border-radius:calc(var(--surface-radius) - 1px) calc(var(--surface-radius) - 1px) 0 0}
.fsechead h3{margin:0;font-size:var(--fs-md);font-weight:600;letter-spacing:.01em}
/* 说明文字是这一行里唯一可以缩的东西：标题、排序框和动作键都要完整读出来。
   基准取 0 而不是 auto：这一行允许换行，按内容宽度参与排线的话它会先把整行挤断，
   缩放根本轮不到发生。基准 0 让它先占 0 再吃剩下的宽度，末尾省略。 */
.fsechead .fmeta{flex:1 1 0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fsechead .fbtn{flex:none}
/* 升降序键是这一行里唯一的纯图标键：正方形、边长取 --control-h，和同排的下拉、
   版式开关一样高，图标 16px 与排序框左侧那枚同尺寸。写成两级选择器是因为 .fbtn 的
   `padding:0 12px` 排在本文件后面——单类名压不过它，内容宽只剩 8px，图标会被压成一条。 */
/* 排序框照 Geist Select 的 prefix 做法：图标压在框内左侧，不在框外挂一个文字标签。
   Geist 的文字 Label 是块级、排在控件上方（block ... mb-2），行内并排那种写法它没有；
   而工具行里既没有上方空间，图标又足够把下拉框和普通按钮区分开。 */
/* 版式开关和排序框、按钮同处分区标题行，一起收到 --control-h 上（内边距 2px + 1px 边框
   凑齐同一个外高）。这一行不给自己开小一号的档：同排三个控件里只有下拉缩了尺寸和字号，
   读起来就是一个点不动的说明标签，而它恰恰是这行里唯一能改变列表内容的控件。 */
.fsechead .iconswitch{display:inline-flex;gap:2px;flex:none;padding:2px}
.fsechead .iconswitch label{width:34px;height:32px}
.fsechead .fbtn{height:var(--control-h)}
/* 排序图标压在触发器左侧，所以左内边距要给它让出位置。 */
.fmeta{font-size:var(--fs-xs);color:var(--muted)}
.fmeta b{color:var(--ink);font-variant-numeric:tabular-nums}
.fmeta.warn{color:var(--drop)}
.fnote{margin:8px 0 0;font-size:var(--fs-sm);line-height:1.7;color:var(--muted)}
.fnote:empty{display:none}
.fnote b{color:var(--ink-2);font-weight:600}
.fnote code{font-size:var(--fs-xs)}

/* Geist Note：字段、卡片、分区旁的持久上下文，以及需要用户处理的失败，全部走这里。
   瞬时回执用 toast，销毁确认用 dialog/confirm。整页范围的失败也是这个盒子，只是多
   一个关闭键（见下面的 .fwarn），不另立一套横幅。 */
/* Note 默认无填充，filled 使用不透明浅色面；状态含义由文字和图标共同表达。 */
:root{--feedback-success:oklch(53.18% .2399 256.99)}
:root[data-theme="dark"]{--feedback-success:oklch(71.7% .1648 250.794)}
@media(prefers-color-scheme:dark){:root:not([data-theme="light"]){--feedback-success:oklch(71.7% .1648 250.794)}}
.geist-note{--feedback-color:var(--muted);display:grid;grid-template-columns:16px minmax(0,1fr);align-items:center;gap:12px;margin:0;padding:6px 12px;min-height:40px;box-sizing:border-box;border:1px solid var(--line);border-radius:var(--control-radius);background:color-mix(in srgb,var(--feedback-color) 8%,var(--ground));color:var(--feedback-color);font-size:var(--fs-md);line-height:24px;overflow-wrap:anywhere}
.geist-note>svg{width:16px;height:24px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.geist-note.geist-note>p{margin:0;color:inherit;font:inherit;align-self:center}.geist-note>p b{margin-right:8px;color:inherit}
.geist-note-warning{--feedback-color:var(--meter);border-color:color-mix(in srgb,var(--meter) 32%,var(--ground))}
.geist-note-error{--feedback-color:var(--drop);border-color:color-mix(in srgb,var(--drop) 30%,var(--ground))}
.geist-note-success{--feedback-color:var(--feedback-success);border-color:color-mix(in srgb,var(--feedback-success) 30%,var(--ground))}
.geist-note-filled{background:color-mix(in srgb,var(--feedback-color) 8%,var(--ground))}
.geist-note-small{min-height:32px;padding:4px 8px;font-size:var(--fs-xs);line-height:22px}
.geist-note-small>svg{height:22px}
/* 折叠明细跟结论同一列，带操作按钮时那一列到行尾为止。 */
.geist-note-details{grid-column:2/-1;min-width:0;margin-bottom:6px;font-size:var(--fs-sm);line-height:22px}
.geist-note-details>summary{cursor:pointer;color:inherit}
/* 明细可能有上千条：只在这块里滚，长路径就地换行，不让页面被撑破。 */
.geist-note-details ul{box-sizing:border-box;display:grid;gap:8px;max-height:min(40vh,520px);margin:8px 0 0;padding-inline-start:0;list-style:none;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain}
.geist-note-details li{display:grid;gap:2px;min-width:0;overflow-wrap:anywhere}
.geist-note-details li>a{justify-self:start;color:var(--ink);font-weight:500}
.geist-note-details li>b{font-weight:500}
.geist-note-details li>span,.geist-note-details li>code,.geist-note-details p{color:var(--muted)}
.geist-note-details li>code{font-size:var(--fs-xs)}
.geist-note-details p{margin:8px 0 0;overflow-wrap:anywhere}
.project-banner{--feedback-color:var(--muted);display:flex;align-items:center;justify-content:center;gap:8px 16px;flex-wrap:wrap;padding:8px 24px;min-height:40px;border-block:1px solid color-mix(in srgb,var(--feedback-color) 30%,var(--ground));background:color-mix(in srgb,var(--feedback-color) 5%,var(--ground));color:var(--feedback-color);font-size:var(--fs-md);line-height:20px}
.project-banner>div{display:flex;align-items:center;gap:8px;min-width:0}.project-banner p{margin:0;overflow-wrap:anywhere}
/* 圆头端点是这枚字形的一部分：alert 与 info 圆圈里那个点是长度 .01 的路径，
   butt 端点会把它渲染成看不见的薄片，横幅上只剩上半截竖杠，警告读成了半个符号。
   只认直接子元素：带进度时横幅里那一枚是 `.geist-gauge` 的圆环，它的端点是弧的
   两头而不是字形笔画，跟着一起圆掉就成了另一种控件的长相。 */
.project-banner>div>svg{width:16px;height:16px;flex:none;fill:none;stroke:currentColor;stroke-width:2;
  stroke-linecap:round;stroke-linejoin:round}
.project-banner>a{color:inherit;text-decoration:underline;text-underline-offset:5px;font-weight:500;flex:none}
.project-banner-error{--feedback-color:var(--drop)}.project-banner-warning{--feedback-color:var(--meter)}.project-banner-success{--feedback-color:var(--feedback-success)}
.context-card{position:fixed;inset:auto;margin:0;width:320px;max-width:calc(100vw - 16px);box-sizing:border-box;overflow:auto;overscroll-behavior:contain;padding:16px;border:1px solid var(--line);border-radius:var(--floating-radius);background:var(--ground);color:var(--ink);box-shadow:0 8px 24px light-dark(rgba(0,0,0,.16),rgba(0,0,0,.6));font-size:var(--fs-md);line-height:1.5;overflow-wrap:anywhere;z-index:var(--layer-popover);pointer-events:auto}
.context-card[hidden]{display:none}.context-card h3{margin:0 0 8px;font-size:var(--fs-md)}.context-card dl{display:grid;grid-template-columns:auto minmax(0,1fr);gap:8px 12px;margin:0}.context-card dt{color:var(--muted)}.context-card dd{margin:0}
/* 窄屏上说明从左边起，动作靠右端：`auto` 而不是定值缩进，说明长短不一时那个链接才
   总在同一条竖线上，换行之后也还在这一横条的右端。 */
@media(max-width:600px){.project-banner{justify-content:flex-start}.project-banner>a{margin-left:auto}}


/* 检查失败明细持续保留到状态解决，复用 Note 的边界、图标和语义色。 */
.fwarn{grid-template-columns:16px minmax(0,1fr);margin:10px 0 14px}
.fwarn>span{min-width:0}
.fwarn p{margin:0}
.fbtn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:32px;
  box-sizing:border-box;padding:0 12px;border:0;border-radius:var(--control-radius);
  box-shadow:0 0 0 1px var(--line-soft);
  background:var(--ground);color:var(--ink);font-size:var(--fs-md);font-weight:500;font-family:inherit;cursor:pointer;
  white-space:nowrap;transition:color .15s ease-in-out,background-color .15s ease-in-out}
.fbtn:hover:not(:disabled){background:color-mix(in srgb,var(--ink) 8%,var(--ground))}
.fbtn:disabled{background:var(--sunk);color:var(--muted);cursor:not-allowed}
/* 强调档那一面归 `board.css` 的 Board primary 一处，跟 `.geist-button.primary` 一样不在
   这里另写：留一份 Geist 的墨色实底，它的 `:hover:not(:disabled)` 比 Board 那条静止规则
   还重一个类，压上去整颗跳成墨色、还描一圈同色的边——「管理关注」上那道黑边就是它。 */
.fbtn.primary{font-weight:500}
/* 去边框那一条只针对动作按钮。升降序那枚不是动作：它跟旁边的下拉触发器是同一组
   控件，共用输入框那圈线。少了线，它在页面上只剩一个箭头浮着。 */
.fbtn.small{height:26px;padding:0 10px;font-size:var(--fs-sm)}
.fbtn svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:2}

/* 卡片内部的行：只用分隔线，不套框——框里再套框正是要避开的那条。 */
/* grid 项默认 min-width:auto，最宽的一行会把整列撑出容器——窄的那一栏尤其明显。
   容器和每一项都要显式 min-width:0 才会真的收缩。 */
.frows{display:grid;min-width:0}
.frows>*{min-width:0}
/* 表格视图。数值取自 boardui.com/components/data-table 的 `.bui-table`（2026-09-08，
   登记见 docs/BOARD_UI.md）：表头与格子都是 10px 12px 内边距、14/20、竖直居中；表头
   500 字重、次要字色、次级底色、上下各一条分隔线；正文行只有一条下边线，悬停不换底。
   窄屏不折叠列——它的做法是容器横向滚动，这里照做。 */
.ftablewrap{overflow-x:auto;overscroll-behavior-x:contain}
.ftable{width:100%;min-width:760px;border-collapse:collapse;text-align:left;font-size:var(--fs-md);line-height:20px}
.ftable th,.ftable td{padding:10px 12px;vertical-align:middle}
.ftable th{font-weight:500;white-space:nowrap;color:var(--muted);background:var(--sunk);
  border-top:1px solid var(--line-soft);border-bottom:1px solid var(--line-soft)}
.ftable tbody tr{border-bottom:1px solid var(--line-soft)}
.ftable tbody tr:last-child{border-bottom:0}
.frow{display:flex;align-items:center;gap:12px;padding:9px 0;flex-wrap:wrap;
  border-bottom:1px solid var(--border-10)}
.frows>.frow:last-child{border-bottom:0}
.frows>.frow:last-child[open]{border-bottom:0}
.frow>b{flex:1 1 180px;min-width:0;font-weight:500;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap}
.frow .fmeta{flex:0 0 auto}
.fsource.bad>b{color:var(--drop)}

/* 进度面板与它上下的分区是同一档容器：发丝边 --field-ring 加 --ground 的面。
   `--border-10` 配 `--surface` 会让它在同一列里比 `.fsec` 亮一档、边又淡一档，
   读起来像另一种东西浮在页面上。 */
.followtask{border:1px solid var(--field-ring);border-radius:var(--surface-radius);background:var(--color-background-primary-default);margin-bottom:16px}
/* 进度卡常住在说明行（data-follow-add-state 是 p.fnote）的 data-follow-job
   挂载层里，说明的字号与颜色不许往里面灌：任务标签与正文同色可读，
   百分比那行保留 board 自己的更淡小字。 */
p.fnote .followtask{color:var(--ink-2)}
.followtask>.geist-fieldset-content{padding:20px;min-width:0;overflow-wrap:anywhere}
.followmanage .followtask{margin-bottom:0}
.fsource.disabled{opacity:.62}
.favatar{width:28px;height:28px;flex:none;border-radius:var(--pill-radius);object-fit:cover;
  background:var(--overlay-5);outline:1px solid oklch(1 0 0 / 0.1);outline-offset:-1px}
/* 取不到头像时是一个明确的空位，不是首字母——首字母会让「未取得」看着像取到了。 */
.favatar.none{display:grid;place-items:center;color:var(--muted);
  font-size:var(--fs-xs);outline-color:var(--line-soft)}
/* 行内文字链接：与上下文同字号同行高，悬停出下划线。间距不在基类里写死——
   句中链接（「去管理关注看原因」）多出 10px 就成了断句；确实要与前文
   拉开的场合（空态的一句话）由所在容器给。 */
.flink{border:0;background:none;padding:0;color:var(--tungsten);font:inherit;cursor:pointer}
.flink:hover{color:var(--tungsten);text-decoration:none}
.empty .flink{margin-left:6px}
.followmanage [data-follow-workspace-panel="list"]{scroll-margin-top:calc(var(--topH) + 16px)}

@media (max-width:640px){
  .fsec{padding:12px 13px 14px}
  .fsechead{margin:-12px -13px 12px;padding:12px 13px}
  .frow{gap:8px}
  /* 省略后剩给说明文字的不到 120px，读出来只是「N 个来源 · 」这种半句；
     未看条数在下面每一行来源上都有，标题行让给排序和两个动作键。 */
  .fsechead .fmeta{display:none}
  .frow .fmeta:nth-of-type(2){display:none}
}

/* 关注页底部：两个翻页动作同属列表末尾，放在一条底栏；
   按钮仍保留各自的本地加载 / 站点抓取语义。 */
.followpagination{display:flex;align-items:center;gap:14px 24px;min-width:0;margin:18px 0 0;
  padding-top:14px;border-top:1px solid var(--line-soft);white-space:nowrap;overflow-x:auto}
.followpageaction{display:inline-flex;align-items:center;gap:12px;min-width:0;flex:none}
.followpageaction .fmeta{min-width:0;overflow:hidden;text-overflow:ellipsis}
@media(max-width:640px){.followpagination{align-items:flex-start;flex-wrap:wrap;white-space:normal}.followpageaction{width:100%}}

/* 资源标识统一由 MiddleTruncate 计算可见文本。星号让本规则与「类 + 元素」的末尾省略
   选择器同权重，并凭源码顺序覆盖，避免两套省略同时生效。 */
*[data-middle-truncate]{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:clip}
/* 链接管理沿用资源同步那套盒子，只加这里独有的两块：概览格和结果表。
   表格必须自己横向滚动——地址列很长，让它撑破 body 会把整页拖成横向可滚。 */
.linkstats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px 32px;margin-top:16px}
.linkstats>div,.linkhosts{display:grid;gap:2px;min-width:0}
.linkstats span,.linkhosts span{color:var(--muted);font-size:var(--fs-xs)}
.linkstats b{font-size:var(--fs-xl);font-variant-numeric:tabular-nums}
.linkstats small{color:var(--muted);font-size:var(--fs-xs)}
.linkhosts{margin-top:16px}.linkhosts b{font-weight:500;font-size:var(--fs-sm);overflow-wrap:anywhere}
.linkgroup{padding:16px;border-top:1px solid var(--line-soft)}.linkgroup:first-child{border-top:0}
.linkgroup h4{margin:0 0 4px;font-size:var(--fs-md);font-weight:600}
.linkgroup h4 b{margin-left:6px;color:var(--muted);font-weight:inherit;font-variant-numeric:tabular-nums}
.linkgroup>p{margin:0 0 12px;color:var(--muted);font-size:var(--fs-xs);line-height:1.5}
.linktablewrap{overflow-x:auto}
.linktable{width:100%;border-collapse:collapse;font-size:var(--fs-xs);white-space:nowrap}
.linktable th{padding:6px 10px;border-bottom:1px solid var(--line-soft);color:var(--muted);font-weight:600;text-align:left}
.linktable td{padding:6px 10px;border-bottom:1px solid var(--line-soft);color:var(--ink-2)}
.linktable tr:last-child td{border-bottom:0}
.linktable .linknote{color:var(--drop);font-variant-numeric:tabular-nums}
/* 勾选格只放一个方框，宽度收到内容最小；表头那个是全选。 */
.linktable .linkpick{width:1%;padding-right:0}
.linktable .linkpick input{margin:0;display:block;accent-color:var(--ink)}
/* 重试条贴着表格底边，和「删除失效链接」那条操作条同一形态；负外边距是为了让它
   顶到 .linkgroup 的内边距之外，跟分组框对齐。 */
.linkretryrow{display:flex;align-items:center;justify-content:flex-end;gap:8px;margin:12px -16px -16px;padding:12px 16px;border-top:1px solid var(--line-soft);background:var(--overlay-5)}
/* 地址用中间截断而不是尾部省略：这张表要回答「哪一条坏了」，而
   `/official/talent/X` 与 `/talent/X` 的差别就在尾部，切掉尾部等于切掉答案。 */
.linktable .linkurl{max-width:360px;overflow:hidden}
/* 中缩要量可用宽度，量不出行内元素的：地址自己占一个 minmax(0,1fr) 的格子，
   外链标固定宽度排在它后面。 */
.linktable .linkurl a{display:grid;grid-template-columns:minmax(0,1fr) auto;color:var(--muted)}
.linktable .linkurl a:hover{color:var(--ink)}
.photolight.has-strip .photodetail.context-card,.photodetail.context-card{position:fixed;inset:auto;width:320px;max-width:calc(100vw - 16px);background:var(--ground);color:var(--ink);backdrop-filter:none}.photodetail.context-card .photodetailmeta,.photodetail.context-card .srcstate{color:var(--muted)}.photodetail.context-card>button{color:var(--ink);border-color:var(--line)}

.geist-gauge{display:inline-flex;width:32px;height:32px;flex:none;vertical-align:middle;margin-inline-end:8px}.geist-gauge svg{width:100%;height:100%;fill:none;stroke-width:3}.geist-gauge circle:first-child{stroke:var(--line-soft)}.geist-gauge circle:last-child{stroke:var(--feedback-success);transform:rotate(-90deg);transform-origin:center}.geist-progress{position:relative}.geist-progress i{background:var(--progress-color,var(--feedback-success))}.geist-progress-stop{position:absolute;top:0;bottom:0;width:2px;background:var(--ground);transform:translateX(-50%)}
.geist-note:has(>[data-note-action]){grid-template-columns:16px minmax(0,1fr) auto;align-items:center}
.geist-note>[data-note-action]{margin:0;white-space:nowrap}
.geist-gauge[data-level="warning"] circle:last-child{stroke:var(--meter)}
.geist-gauge[data-level="error"] circle:last-child{stroke:var(--drop)}
.gauge-status{margin-inline-end:8px;font-size:var(--fs-xs)}
@media(prefers-reduced-motion:no-preference){.geist-progress i{transition:width .2s ease-out,background-color .2s ease-out}.geist-gauge circle:last-child{transition:stroke-dasharray .2s ease-out,stroke .2s ease-out}}
@media(max-width:600px){.geist-note:has(>[data-note-action]){grid-template-columns:16px minmax(0,1fr)}.geist-note>[data-note-action]{grid-column:2;justify-self:start}}
/* 配置页（/configuration）的外壳：分组小标题排在一条 812px 窄列里，与数据管理 hub 同宽。
   分区本身是 React 子树（`frontend/src/react/settings/`），样式归 `peach-react.css`。
   `.configfieldset` 几条只给配置骨架用。 */
.configuration-layout .managetitle,.configuration-layout .pagelede{width:min(812px,100%);margin-left:auto;margin-right:auto}
.configpage{width:min(812px,100%);margin:0 auto;display:grid;gap:20px}
.configgroup{margin:16px 0 0;font-size:var(--fs-2xl);line-height:1.3;font-weight:600}
.configgroup:first-child{margin-top:0}
.resourceapplyrow>p{margin-right:auto;text-align:left}
.resourcesyncfooter>p{margin:0 auto 0 0;text-align:left}
.resourceaction.warning{background:var(--meter);color:light-dark(var(--ink),var(--ground))}
.resourceaction.warning:hover:not(:disabled){background:color-mix(in srgb,var(--meter) 88%,#fff);color:light-dark(var(--ink),var(--ground))}
.resourceaction.warning:disabled{background:var(--sunk);color:var(--muted);box-shadow:0 0 0 1px var(--line-soft)}
.configfieldset{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;overflow:hidden;
  border:1px solid var(--line);border-radius:var(--control-radius);background:var(--ground)}
.configfieldset>.geist-fieldset-content{flex:1;min-height:0;padding:20px;display:grid;gap:20px}
.configfieldset .geist-fieldset-title{margin:0}
.configfieldset>.geist-fieldset-footer{box-sizing:border-box;min-height:var(--fieldset-bar-h);
  margin:0;padding:12px 12px 12px 20px;display:flex;align-items:center;justify-content:flex-end;gap:8px;
  border-top:1px solid var(--line);background:var(--surface)}
.configfieldset>.geist-fieldset-footer>p{min-width:0;margin:0 auto 0 0;color:var(--muted);font-size:var(--fs-sm)}
@media(max-width:560px){.configfieldset>.geist-fieldset-footer{flex-wrap:wrap}}
/* 小窗播放：离开详情时正在播放的视频缩到视口角落继续放。几何照 YouTube 桌面版
   f82dea74 的 ytd-miniplayer 实测（docs/reference-snapshots/youtube-miniplayer-measured.md）：
   fixed，默认右下角、离边 16px，宽 400，画面区按视频比例，信息栏 76px，12px 圆角，
   双层阴影 0 2px 5px rgba(0,0,0,.16) / 0 3px 6px rgba(0,0,0,.2)。宽度在窄屏收成
   视口减 32px。四角落点由 data-corner 决定，拖动只改 transform，松手后吸附。 */
.miniplayer{position:fixed;z-index:var(--layer-miniplayer);width:min(400px,calc(100vw - 32px));max-height:calc(100dvh - 32px);
  touch-action:none;will-change:opacity,transform;user-select:none}
.miniplayer[hidden]{display:none}
.miniplayer[data-corner="br"]{right:16px;bottom:16px}
.miniplayer[data-corner="bl"]{left:16px;bottom:16px}
/* 顶上两角让出顶栏：Peach 的顶栏是吸顶的，压在上面搜索和设置就点不到了。 */
.miniplayer[data-corner="tr"]{right:16px;top:calc(var(--topH) + 16px)}
.miniplayer[data-corner="tl"]{left:16px;top:calc(var(--topH) + 16px)}
.miniplayer.miniplayer-dragging{cursor:grabbing}
.miniplayer.miniplayer-dragging .miniplayercard{cursor:grabbing}
/* 吸附回角落：上游 .ytdMiniplayerComponentAnimatingSnap 是 transform .5s cubic-bezier(.05,0,0,1)。 */
.miniplayer.miniplayer-snapping{transition:transform .5s cubic-bezier(.05,0,0,1)}
/* 进场：上游 .ytdMiniplayerComponentAnimatingFade 为 .366s cubic-bezier(.05,0,0,1)，关键帧的位移量未取得，只做透明度。 */
@media (prefers-reduced-motion:no-preference){
  .miniplayer.miniplayer-entering{animation:miniplayer-fade-in .366s cubic-bezier(.05,0,0,1) both}
}
@keyframes miniplayer-fade-in{from{opacity:0}to{opacity:1}}
.miniplayercard{display:flex;flex-direction:column;overflow:hidden;border-radius:var(--floating-radius);background:var(--surface);
  box-shadow:0 2px 5px light-dark(rgba(0,0,0,.16),rgba(0,0,0,.5)),0 3px 6px light-dark(rgba(0,0,0,.2),rgba(0,0,0,.55));cursor:grab}
.miniplayerframe{position:relative;width:100%;aspect-ratio:var(--miniplayer-aspect,16/9);max-height:calc(100dvh - 32px - 76px);background:#000;overflow:hidden}
/* Video.js 的壳整块搬进来，几何全由这一格给：控件条只剩底部那条进度。 */
.miniplayerframe .video-js{position:absolute;inset:0;width:100%!important;height:100%!important;padding-top:0!important;background:#000;font-family:inherit}
.miniplayerframe .video-js .vjs-tech{object-fit:contain}
.miniplayerframe .video-js :is(.vjs-big-play-button,.vjs-peach-bezel,.vjs-peach-settings-menu,.vjs-peach-tooltip,.vjs-loading-spinner,[data-player-seek-preview],.vjs-text-track-display){display:none!important}
.miniplayerframe .video-js .vjs-control-bar{position:absolute;left:0;right:0;top:auto;bottom:0;z-index:2;display:flex;width:auto;height:5px;padding:0;
  background:transparent;opacity:1!important;visibility:visible!important;pointer-events:auto!important;transition:none}
.miniplayerframe .video-js .vjs-control-bar>:not(.vjs-progress-control){display:none!important}
/* 进度条常显，5px，圆点 12px：上游 .ytp-miniplayer-ui 里进度条不随控件条隐藏。 */
.miniplayerframe .video-js .vjs-progress-control{position:absolute;inset:0;width:auto;height:5px;min-width:0;display:flex;align-items:center}
.miniplayerframe .video-js .vjs-progress-control .vjs-progress-holder{width:100%;height:5px;margin:0;border-radius:0;background:rgba(255,255,255,.2)}
.miniplayerframe .video-js .vjs-load-progress{background:rgba(255,255,255,.4)}
.miniplayerframe .video-js .vjs-load-progress div{background:rgba(255,255,255,.2)}
.miniplayerframe .video-js .vjs-play-progress{background:var(--tungsten)}
.miniplayerframe .video-js .vjs-play-progress:before{content:"";position:absolute;right:-6px;top:50%;width:12px;height:12px;border-radius:50%;
  background:var(--tungsten);transform:translateY(-50%);box-shadow:none;font-size:0}
.miniplayerframe .video-js .vjs-time-tooltip{font-size:var(--fs-xs);font-family:inherit}
/* 悬停幕：上游 .ytp-miniplayer-scrim rgba(0,0,0,.5)，.2s cubic-bezier(.4,0,.2,1) 延迟 .1s，
   hover / focus-within 才显；关闭右上、展开左上各 40px，中央播放键 42px，时间左下。 */
.miniplayerscrim{position:absolute;inset:0;z-index:1;display:flex;align-items:center;justify-content:center;gap:6px;background:rgba(0,0,0,.5);
  opacity:0;transition:opacity .2s cubic-bezier(.4,0,.2,1) .1s;color:#fff}
.miniplayerframe:hover .miniplayerscrim,.miniplayerframe:focus-within .miniplayerscrim,.miniplayer.miniplayer-dragging .miniplayerscrim{opacity:1}
.miniplayerbtn,.miniplayerplay,.miniplayerseek{position:relative;isolation:isolate;display:grid;place-items:center;margin:0;padding:0;border:0;background:transparent;color:#fff;cursor:pointer}
.miniplayerbtn{position:absolute;top:0;width:40px;height:40px}
.miniplayerbtn svg{position:relative;z-index:1;width:24px;height:24px;stroke:currentColor;fill:none;stroke-width:2;filter:drop-shadow(0 0 1px rgba(0,0,0,.8))}
.miniplayerexpand{left:0}
.miniplayerclose{right:0}
.miniplayerplay{width:42px;height:42px}
.miniplayerplay svg{position:relative;z-index:1;width:36px;height:36px;fill:currentColor;stroke:none;filter:drop-shadow(0 0 1px rgba(0,0,0,.8))}
/* 快退和快进分列播放键两侧。上游 YouTube 的小窗没有这两颗，Peach 自己要：小窗一开就
   离开了详情，站内那条控制条一颗键都递不进来，只剩进度条能拖，想跳十秒得先回详情。
   36px 比中间那颗小一档，三颗并排读得出主次。 */
.miniplayerseek{width:36px;height:36px}
.miniplayerseek svg{position:relative;z-index:1;width:22px;height:22px;stroke:currentColor;fill:none;stroke-width:2;filter:drop-shadow(0 0 1px rgba(0,0,0,.8))}
/* 悬停圈与详情页的关闭键、统计键同一套。 */
.miniplayerbtn:after,.miniplayerplay:after,.miniplayerseek:after{content:"";position:absolute;z-index:0;inset:4px;border-radius:50%;background:transparent;pointer-events:none;transition:background .2s cubic-bezier(.05,0,0,1)}
.miniplayerbtn:hover:after,.miniplayerbtn:focus-visible:after,.miniplayerplay:hover:after,.miniplayerplay:focus-visible:after,.miniplayerseek:hover:after,.miniplayerseek:focus-visible:after{background:rgba(255,255,255,.1)}
.miniplayerbtn:active:after,.miniplayerplay:active:after,.miniplayerseek:active:after{background:rgba(255,255,255,.2)}
.miniplayerbtn:focus-visible,.miniplayerplay:focus-visible,.miniplayerseek:focus-visible{outline:0}
.miniplayertime{position:absolute;left:12px;bottom:16px;font-size:var(--fs-xs);line-height:15px;text-shadow:0 0 2px #000;pointer-events:none}
/* 信息栏：上游 76px，12px 16px 内边距；标题 14/20 500 单行省略，副标题 12/18 400、上距 4px。
   整条是「展开到详情」的按钮，拖动从这里起手最顺，所以光标是 grab。 */
.miniplayerinfo{box-sizing:border-box;display:flex;flex-direction:column;justify-content:center;align-items:flex-start;min-height:76px;width:100%;
  margin:0;padding:12px 16px;border:0;background:transparent;color:var(--ink);text-align:left;font:inherit;cursor:grab}
.miniplayerinfo:focus-visible{outline:2px solid var(--tungsten);outline-offset:-2px}
.miniplayertitle{display:block;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:var(--fs-md);line-height:20px;font-weight:500}
.miniplayersub{display:block;max-width:100%;margin-top:4px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:var(--fs-xs);line-height:18px;color:var(--muted)}
.miniplayersub:empty{display:none}

/* 播放器右键菜单和右下角设置面板是同一块浮层，外观取同一份：rgba(0,0,0,.6) 加 blur(16px)、
   --floating-radius 圆角、8px 内边距、无阴影；每项 48px 高、项内 --control-radius 圆角，首列 56px
   里靠左 8px 放 24px 图标，--fs-md 常规字重白字，悬停 rgba(255,255,255,.1)、按下 .2；勾选项右侧
   32px 一格放 18px 白勾。选择器挂在 id 上：页面菜单那组 `body :is(.popmenu…)` 规则会把字色压成
   次级灰、行高压到 36px、勾选项铺浅底，按类名写压不过它。 */
#playerMenu.playermenu{padding:8px;border:0;gap:0;border-radius:var(--floating-radius);background:rgba(0,0,0,.6);
  backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);box-shadow:none;color:#fff;
  min-width:240px;max-width:calc(100vw - 16px);text-shadow:0 0 1px rgba(0,0,0,.8)}
.playermenu[popover]{inset:auto;margin:0}
#playerMenu>.playermenuitem{box-sizing:border-box;display:grid;grid-template-columns:56px minmax(0,1fr) 32px;gap:0;align-items:center;width:100%;min-height:48px;
  margin:0;padding:0;border:0;border-radius:var(--control-radius);background:transparent;box-shadow:none;color:#fff;
  font:inherit;font-size:var(--fs-md);font-weight:400;line-height:18.2px;text-align:left;white-space:nowrap;cursor:pointer}
#playerMenu>.playermenuitem:hover,#playerMenu>.playermenuitem:focus-visible{background:rgba(255,255,255,.1);color:#fff;outline:0}
#playerMenu>.playermenuitem:active{background:rgba(255,255,255,.2)}
.playermenuitem>svg{justify-self:start;margin-left:8px;width:24px;height:24px;stroke:currentColor;fill:none;stroke-width:2}
.playermenuitem>svg.playermenufill{fill:currentColor;stroke:none}
.playermenuitem>span{overflow:hidden;text-overflow:ellipsis}
.playermenuitem>.playermenucheck{grid-column:3;justify-self:center;margin:0;width:18px;height:18px;visibility:hidden}
.playermenuitem[aria-checked="true"]>.playermenucheck{visibility:visible}
/* 原地换态的一组动效：字形换字形、读数换读数、占位换内容、成功与失败的那一下。

   共同的形态取自 transitions.dev 的免费配方（逐条的来源 URL、它那边的时长／模糊／
   缩放取值和 Peach 落成的值记在 docs/reference-snapshots/transitions-dev-measured.md）。
   借的是形态和参数，类名与 token 都是 Peach 自己的：时长与缓动一律读 board.css 的
   `--motion-swap`／`--motion-reveal`／`--board-motion`／`--spring-*`，所以
   `prefers-reduced-motion` 由那一份全局规则一次关掉，这里不再逐处补。

   只动 opacity／transform／filter／stroke-dashoffset，不动 width、height、left 这类
   会触发重排的属性——这几条都长在读数、按钮和整页占位上，一次重排就是一整棵子树。 */

/* ── 字形换字形 ──
   两枚字形叠在同一个 grid 格里，换的只是容器上的 `data-icon-state`：innerHTML 重写会
   把旧字形连同它的动画一起丢掉，看上去就是硬切。退场那一枚缩到 .25 并糊掉，
   进场那一枚从同样的位置长回原大小。 */
.iconswap{display:inline-grid;place-items:center;line-height:1;flex:none}
.iconswap>[data-icon]{grid-area:1/1;display:grid;place-items:center;
  transition:opacity var(--motion-swap),filter var(--motion-swap),transform var(--motion-swap)}
.iconswap[data-icon-state="a"]>[data-icon="a"],
.iconswap[data-icon-state="b"]>[data-icon="b"]{opacity:1;filter:blur(0);transform:scale(1)}
/* 退场那一枚要收掉指针和读屏：它还占着同一个格子，不收的话按钮上有两个可点区域，
   读屏也会把两枚字形的名称连着念出来。 */
.iconswap[data-icon-state="a"]>[data-icon="b"],
.iconswap[data-icon-state="b"]>[data-icon="a"]{opacity:0;filter:blur(2px);transform:scale(.25);
  pointer-events:none}

/* ── 一行字换成另一行字 ──
   退场往上走 4px 并糊掉，新字从下方 4px 处回到原位。`.entering` 那一帧不带过渡，
   它只是把新字瞬间摆到起点，下一帧摘掉类名才开始走。 */
.textswap{display:inline-block;transition:opacity var(--motion-swap),
  filter var(--motion-swap),transform var(--motion-swap)}
.textswap.leaving{opacity:0;filter:blur(2px);transform:translateY(-4px)}
.textswap.entering{opacity:0;filter:blur(2px);transform:translateY(4px);transition:none}

/* ── 读数按位错峰 ──
   每一位从下方 8px 处带模糊长上来，相邻两位差一个 `--motion-stagger`。缓动那条超出
   1 的控制点是数字自己要的：读数落位时轻轻过一点再收住，才读成「跳到了新值」而不是
   「被推到了新值」。分隔符（千分位逗号、间隔点）跟着相邻那一位走，不单独占一档。 */
@keyframes digit-pop-in{
  from{opacity:0;filter:blur(2px);transform:translateY(8px)}
  to{opacity:1;filter:blur(0);transform:none}}
.digits{display:inline-flex;align-items:baseline}
.digits>span{display:inline-block;white-space:pre}
.digits.popping>span{animation:digit-pop-in var(--motion-count) both;
  animation-delay:calc(var(--motion-stagger) * var(--digit-at,0))}

/* ── 占位换成真内容 ──
   骨架和内容在同一段时间里交叉：骨架淡出并糊掉，内容从模糊里清晰起来。两层必须叠在
   同一处坐标，所以换的那一刻把旧的整块抬成绝对定位的一层盖在容器上，动画走完再摘掉；
   容器本身的高度全程由新内容决定，不留一段空白，也不让下面的东西跳两次。 */
/* `<dialog>` 自己已经是 fixed，它就是淡出层的包含块；再给它一条 relative 会把浮窗从
   视口定位打回文档流里，整块详情会掉到页面底下。 */
.skelreveal:not(dialog){position:relative}
.skelreveal>.skelfade{position:absolute;inset:0;z-index:2;overflow:hidden;pointer-events:none;
  opacity:1;filter:blur(0);transition:opacity var(--motion-reveal),filter var(--motion-reveal)}
.skelreveal.revealing>.skelfade{opacity:0;filter:blur(2px)}
/* 用 `backwards` 不用 `both`：终点帧是内容本来的样子，留着它只多留下一个
   `filter:blur(0)`，而非 none 的 filter 会另起一个 backdrop root，内容里任何
   backdrop-filter 从此只采样得到它自己。 */
.skelreveal>:not(.skelfade){animation:skel-reveal-in var(--motion-reveal) backwards}
@keyframes skel-reveal-in{from{opacity:0;filter:blur(2px)}to{opacity:1;filter:blur(0)}}

/* ── 成功的那一下 ──
   勾从下方荡上来的同时转正、从模糊里清晰起来，笔画随后画出。荡那一下走
   `--spring-press`：它冲过落点再收回来，正是一个「落定」的手感；其余三条走站内那条
   通用缓动。笔画长度 24 是这枚字形的整条折线，dasharray 与 dashoffset 都是继承属性，
   写在外层 svg 上就能穿过 `<use>` 落到里面那条 path。 */
@keyframes check-settle{from{opacity:0;transform:rotate(80deg);filter:blur(10px)}
  to{opacity:1;transform:rotate(0);filter:blur(0)}}
@keyframes check-bob{from{translate:0 14px}to{translate:0 0}}
@keyframes check-draw{from{stroke-dashoffset:24}to{stroke-dashoffset:0}}
.checkdraw>svg{stroke-dasharray:24;
  animation:check-settle var(--board-motion) backwards,
    check-bob calc(var(--spring-press-ms)*1ms) var(--spring-press) backwards,
    check-draw var(--board-motion) var(--motion-stagger) backwards}

/* ── 失败的那一下 ──
   已经有红边表达「这一格不对」，抖动只是把视线拉过去，所以不另加红字、不改边框色，
   只在原位左右各走一趟再回中。走的是站内那条通用时长与缓动，reduced motion 下归零。
   `[aria-invalid]` 一挂上就开始走，是纯 CSS 的触发；同一次提交里同一格再报同一个错，
   属性没变过，这一下就不重放——判据是「这一格刚变成不对」，不是「又提交了一次」。 */
@keyframes field-shake{0%{transform:translateX(0)}
  28.57%{transform:translateX(6px)}
  57.14%{transform:translateX(-6px)}
  78.57%{transform:translateX(4px)}
  100%{transform:translateX(0)}}
:is(input,textarea,select)[aria-invalid="true"],
.board-number-control:has([aria-invalid="true"]){animation:field-shake var(--board-motion)}

/* ── 作品卡共演头像里指到的那一枚抬起来 ──
   叠放头像本来彼此覆盖，指到的一枚抬起并放大，右侧邻座逐枚朝标题方向让开。全程只动
   transform，卡片的几何与首页顶栏头像、厂牌筛选都不受影响。进出各用一条既有缓动，
   `:has(:hover)` 让整组在同一时刻切换。 */
.card .mavstack{
  --lift-motion:calc(var(--spring-press-ms) * 1ms) var(--spring-press)}
.card .mavstack:has(:is(:hover,:focus-visible)){--lift-motion:var(--board-motion)}
.card .mavstack .mav{
  transform:translate(var(--lift-x,0),var(--lift-y,0)) scale(var(--lift-scale,1))}
.card .mavstack .mav{transition:transform var(--lift-motion,var(--board-motion))}
.card .mavstack .mav:is(:hover,:focus-visible){--lift-y:-4px;--lift-scale:1.05}
/* 叠放头像的左缘不动，只让右侧邻座朝标题方向逐枚让开；折叠时每枚前进 16px，展开后
   恢复成 26px 的可辨步长，五枚连同左侧安全距仍收在 168px 密集卡内。键盘焦点与指针
   使用同一套几何。 */
.card .mavstack .mav:is(:hover,:focus-visible)+.mav{--lift-y:-1.8px;--lift-x:10px}
.card .mavstack .mav:is(:hover,:focus-visible)+.mav+.mav{--lift-y:-.81px;--lift-x:20px}
.card .mavstack .mav:is(:hover,:focus-visible)+.mav+.mav+.mav{--lift-x:30px}
.card .mavstack .mav:is(:hover,:focus-visible)+.mav+.mav+.mav+.mav{--lift-x:40px}

/* ── 清空时输入的内容溶解掉 ──
   输入框自己的 value 是一瞬间没的，没有动画可挂。所以清空前先照着输入框的位置和字体
   摆一份同样的字，让它往上飘 12px、糊掉、淡出，真正的输入框当场就空了——光标和输入法
   一刻也不等这段动画。这一层不可点、不读屏，走完自己摘掉。

   上游还在同一条配方里给每个词铺一层 radial-gradient 做扫光，那部分要逐帧写背景才
   画得出来，这里不做：搜索框每清一次就跑一段逐帧 JS，换来的是一道一眼看不出来的光。 */
@keyframes clear-dissolve{to{opacity:0;filter:blur(2px);transform:translateY(-12px)}}
.cleardissolve{position:absolute;z-index:2;display:flex;align-items:center;overflow:hidden;
  white-space:nowrap;pointer-events:none;color:inherit;
  animation:clear-dissolve var(--motion-reveal) forwards}
.cleardissolve>[data-dissolve-value]{display:block;min-width:max-content;will-change:transform}
.search input.dissolving::placeholder{opacity:0}

/* ── 计数徽标弹出来 ──
   从零长到原大小，同时从模糊里清晰起来。整枚一起弹，不按位拆：这一枚说的是「这里有
   几条」，读的是有没有变多，不是变成了哪几个数字——按位拆是读数那一条的事。
   `--motion-pop` 的缓动过一点再收住，落位才读成「弹出来的」。

   `.popped` 是起点帧，摘掉就往原位走。它自己那一帧不带过渡：带的话，挂上去这一枚就
   开始朝零缩，同一次调用里紧接着摘掉时它才走了零点几个像素，回程于是无处可走——屏幕上
   什么也没发生。首屏那一批不挂这个类：整排计数一起弹是一屏烟花，而它要说的是
   「这一枚刚变了」。 */
.countbadge{display:inline-block;transition:transform var(--motion-pop),
  opacity var(--motion-pop),filter var(--motion-pop)}
.countbadge.popped{transform:scale(0);opacity:0;filter:blur(2px);transition:none}

/* ── 标题从模糊里揭示出来 ──
   每一行从下方 12px 处带模糊长上来，相邻两行差一个 `--motion-stagger`。按行不按词：
   逐词要把整句拆成一串 `<span>`，复制出来的文字会跟着散架，而这几处标题（作品名、
   页面名）正是最常被复制走的那几段字。

   走完把类名一起摘掉。留着的话终点帧上还挂着一个 `filter:blur(0)`，非 none 的 filter
   会另起一个 backdrop root，标题块里任何 backdrop-filter 从此只采样得到它自己。

   起点那一帧不带过渡：这几处标题里有一半是一直在页面上的节点（页面标题、设置面板的
   分区名），带过渡的话挂上去只是开始朝下淡，下一帧点亮时回程只剩零点几个像素。 */
.revealline{opacity:0;transform:translateY(12px);filter:blur(3px);transition:none}
.revealline.revealing{opacity:1;transform:none;filter:blur(0);
  transition:opacity var(--motion-reveal),transform var(--motion-reveal),filter var(--motion-reveal);
  transition-delay:calc(var(--motion-stagger) * var(--reveal-at,0))}

/* 横排滚到头的回弹：`wireHorizontalScroller` 在越界那一下给这一排挂 `.edgepull`，逐帧改
   `--edge-pull`，子元素整排跟着平移。属性要注册成长度，Web Animations 才插得了值；
   没注册的自定义属性只会在两端之间硬跳。 */
@property --edge-pull{syntax:'<length>';inherits:true;initial-value:0px}
.edgepull>*{translate:var(--edge-pull) 0}
