/* 色板：灯箱 / 接触印相。底色带棕不是纯黑，中性灰偏暖向主调靠。
   钨丝灯琥珀作主调，冷青只给数据，语义色（免费/计费/丢弃）与主调分开。 */
/* 深色只保留轻微层级，不让输入框、关注管理和数据详情陷进近乎纯黑的第三档。
   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、分段器外框，介于控件与浮层之间。
     除这五个 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` 只引用变量，一个字面颜色都不写。这组默认值就是预设「钨丝暖阁」，
     颜色与 frontend/src/appearance/home-glow.ts 里那一档一字不差，页面第一帧和 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 一版里的实际渲染
     （常驻组件 `settings-panel` 的 `[data-settings-panel]`），两档主题同值、不带模糊。 */
  --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:has(.top :focus){scroll-padding-top:0}
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 铺在每一块玻璃上，抽屉这一块交给这里来铺（侧栏岛的
   `sidebar.css` 因此把抽屉上的两个变量置为 none，两套光斑叠着漂会糊成一团）；搜索框、筛选浮层、图标钮
   那些玻璃面保持自己原来的漂移不动——它们身上那一点正是从侧栏折射过去的光。
   正文身后什么都不铺：整条页面宽的光带是背景不是光晕，字压在上面读不出来。

   位置不是参数，由 glowdriftx/glowdrifty 一直推着走，算法照搬 board.css 那两条：画布是
   元素的两倍大，background-position 从 0% 到 100% 正好把光斑中心从右下角推到左上角，
   -10% 与 110% 那两档让它探出框外一点。三枚各走一条相位不同的李萨如轨迹，横竖两条动画
   41s 与 67s 互质，合起来四十多分钟不重样。
   z-index 取 -2：抽屉 #drawer 自己带 backdrop-filter，是一个层叠上下文，负层只压在它那块玻璃
   填充之上、被导航内容全部盖住；-1 已经归了选中项那块玻璃 [data-sidebar-glide]，光晕要在它下面。
   溢出由抽屉的 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,[role="button"]{touch-action:manipulation}
/* 按钮里的字是界面标签时它不是内容：触屏长按按钮、标签 chip 或折叠标题时不选中字、
   不弹系统菜单。链接和输入框不在这里。按钮里装的是馆藏数据时它就是内容——作品卡上的
   标题与番号、女优名、标签，系列名、筛选页的女优与厂牌名、中间省略的文件名与标题——
   照常能长按复制，页内查找也落得到（offscreen-cards.test.ts）。 */
button,[role="button"],summary{user-select:none;-webkit-user-select:none;-webkit-touch-callout:none}
[data-media-card] button,button:is([data-series-link],[data-tier-performer],[data-tier-studio],[data-middle-truncate]){
  user-select:auto;-webkit-user-select:auto;-webkit-touch-callout:default}
/* 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}}
/* 静态搜索框：React 接管前的骨架里用（索引页标题行），接管后换成 BoardUI 的 InputBase。
   两者是同一只框的前后两帧，几何与面照 InputBase 取：无边框的填充底、20px 图标距左 8px、
   文字从 34px 起（8 + 20 + 2 + 4），框高跟同一行按钮共用 --control-h。 */
.geist-search{position:relative;min-width:0}
.geist-search-prefix{position:absolute;z-index:1;left:8px;top:calc((var(--control-h) - 20px) / 2);
  display:grid;place-items:center;width:20px;height:20px;color:var(--color-text-tertiary);pointer-events:none}
.geist-search-prefix>svg{width:20px;height:20px;stroke:currentColor;stroke-width:2;fill:none}
.geist-search input[type="search"]{width:100%;min-width:0;height:var(--control-h);
  box-sizing:border-box;border:0;border-radius:var(--control-radius);
  background:var(--color-background-tertiary-default);color:var(--ink);padding:0 8px 0 34px;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(--color-text-tertiary)}
/* 悬停与聚焦是同一枚 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)}
/* 纯文本输入框：全站唯一一份。此前每处自己拼内边距，控件高度散在 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}
.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 一处。
   这里留着一份 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` 上，不要另起一个页面类来承担同一件事。
   `.xxxactions 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)}
.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}
/* 复核卡正文的纵向轨道位于右侧内边距中，12px 命中区完整留在内容之外。 */
[data-review-key]>div:has(>div[data-overlay-scrollbar])>.ovtrack.ov-y{translate:12px 0}
.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}
/* 边缘只提示仍可滚动的方向；透明渐隐适配每一档底色，覆盖层不拦点击与滚轮。 */
[data-scroll-edges]{mask-image:linear-gradient(to bottom,transparent,#000 var(--scroll-edge-top),#000 calc(100% - var(--scroll-edge-bottom)),transparent)}
/* 横向行不显示滚动条（本来就能拖动/滚轮横滚） */
.tier,.tagbar,.srow{scrollbar-width:none}
.tier::-webkit-scrollbar,.tagbar::-webkit-scrollbar,
.srow::-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}

/* 锚定菜单：无展开动画，固定在视口内，内容在菜单内滚动而不是把页面撑出滚动条。
   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:var(--elevation-dropdown);
  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：触发器穿输入框那身盒子，面板就是上面那个锚定菜单。为什么不用原生
   下拉、几何取自哪里，见 frontend/src/ui-kit/select-field.ts 的头注释。 */
.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)}

/* Geist Modal 的操作条；弹层本身、正文与表单的几何在 frontend/src/ui-kit/modal.css。 */
.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}
.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)}

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

   尺寸走 `--markw` / `--markh` 而不是 `width:auto`：还没度量过的图按 auto 是 0×0，
   而 `loading="lazy"` 见到 0×0 的框就认定它不在视口里，于是永远不去取这张图——
   图不来就没有 load、没有 load 就没有度量，两边互相等着，格子一直空着。缺省值
   仍是铺满，度量之后才换成那张图自己的像素数。 */
[data-person-ring][data-fit-native="mark"] img,
[data-person-ring][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}
/* 首字母是取不到图时的垫底。图不再铺满框之后它会从旁边露出来，所以有图就收起来。
   资料卡岛那一侧的大位与首字母由 `entity-hero.css` 自己收。 */
[data-person-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}
/* 栏里的行、组与页签归 `search` 岛（`frontend/src/react/search/search.css`）。两栏与否看下拉栏自己
   有多宽，不看窗口，所以容器查询名挂在这块面上。 */
.searchmenu{container:searchmenu/inline-size}

/* ── 顶部三层：女优圆头像 / 厂牌 / 内容标签 ── */
/* 有显式 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)}
/* 二层：厂牌大药丸 */
/* 这一排点开的是厂牌页，不是往当前列表上加一条筛选，所以走实线：虚线留给
   首页筛选条上那一排真能开关的标签，见 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}
/* 选中药丸和生效筛选共用高亮填充。 */
.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)}

/* flat 层的药丸一律实线。虚线只画在「可加可不加、此刻还没加上去」的筛选药丸上：首页筛选条的标签归
   `catalog-filter` 岛，判据与 Vercel 筛选令牌的实测出处写在 `catalog-filter.css`；关注页、资料页与
   标签页那几排住在 Board 的玻璃浮层里，同一判据由各自的岛与 board.css 写。 */

/* 首页筛选条（`catalog-filter` 岛）的宿主与 React 根都不生成盒子：两排头像与那块浮层要直接当
   body 的孩子排，浮层吸顶的范围才是整页。实体页与索引页不显示它。 */
[data-catalog-filter],[data-catalog-filter]>.peach-react{display:contents}
body.entity-open [data-catalog-filter],body.index-open [data-catalog-filter]{display:none}

/* 统计页 */
.stats{padding:0 0 42px}
/* 播放列表页骨架的新建框占位；页头与卡片栅格在 frontend/src/board-skeleton.css。 */
.playlistcreate{display:flex;align-items:flex-end;gap:8px;flex-wrap:wrap}
/* 账本只读那一条是 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}
/* 活动页骨架的分区与卡内那一列；窄列与列表在 frontend/src/board-skeleton.css，
   卡片框体沿用 `.cleanupfieldset`。 */
.activitysection{display:grid;gap:12px;min-width:0}
.activitysection .geist-fieldset-title{margin:0;font:var(--board-heading);font-family:var(--font-sans);letter-spacing: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{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}
.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}

/* 分类那一列与工具条在骨架里也要占住位置，正文归 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），这里留下的是遗留壳铺的那张骨架读到的层级；
   两页骨架的数据面板与口味页的页头、主详情在 frontend/src/board-skeleton.css。 */
.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{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}
.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)}
@media(max-width:640px){.insightswitch{align-self:flex-start}.metricstrip{grid-auto-columns:minmax(168px,82%)}.insightcopy{border-right:0;border-bottom:1px solid var(--line-soft);padding:22px 18px}.tastesummary>b{font-size:var(--fs-xl)}}

/* 索引页整页在 React（`frontend/src/react/index/`）。这里留下的是壳铺的那张骨架（页头、
   头像格、标签行与标签云的占位）。资料页骨架里事务所名册那一块也是这张头像格；真格子
   在 React 里（`PeopleGrid`，索引页与资料页正文共用）。
   不裁横向溢出：筛选玻璃的三层投影要往左右各洒出几十像素，资料页吸顶栏也用负边距把
   分隔线铺到 main 左右边缘；横滚的行各自 `overflow-x:auto`，页面本身不会被撑出横向滚动。 */
.index{padding:4px 0 20px;min-width:0;max-width:100%}
.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)}
.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 .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}
/* 骨架里的版式开关是接管后那枚 BoardUI 分段器的替身：每格 28px 见方、图标 16px，整枚宽度对得上。 */
.index .ihead .iconswitch>label{width:28px;height:28px}
.index .ihead .iconswitch svg{width:16px;height:16px}
.tagwall{display:flex;flex-wrap:wrap;gap:7px}
/* 标签行骨架前面那枚色点，占的是真标签类型色点的位置。 */
.alphatag::before{content:"";width:7px;height:7px;border-radius:50%;
  background:var(--tag-color,var(--muted));flex:none}
.index-tags .tg,.alphatag{--tag-color:#7A91A8}
/* 标签云骨架与真标签同一档身量：这一页标签是主体，比卡片上的大一档。 */
.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)}
/* 一行一枚标签：36px 高、控件圆角，取的是排名行那一档身量。 */
.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 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))}
}

/* 资料卡的等待态（`entitySkeletonHtml`）：一块 --ground 底、浮层圆角、不描边的卡，卡底那条
   色阶带是同台艺人的位置。画好的资料卡归 React（`frontend/src/react/entity-hero/`），几何与这里
   一一对齐，骨架换成真卡时高度不跳。`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%}
/* 头像按身份列有哪几行定尺寸（真卡见 `entity-hero.css`）：骨架只画名字、别名和外链三行，
   没有看片那一行，落在 120px 那一档。 */
.entityprofile{display:grid;grid-template-columns:auto minmax(0,1fr);gap:20px;padding:20px;
  align-items:center;min-width:0;--portrait-size:120px}
/* 女优有资料时多出第三栏：右边那张资料表（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)}
.entityportrait{height:var(--portrait-size);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)}
.entityhero h2{font-size:var(--fs-2xl);font-weight:500;
  line-height:1.25;letter-spacing:0;margin:0 0 9px;overflow-wrap:anywhere}
/* 标题那一行的下边距归到行上，标题自己不再留。 */
.entitytitle{display:flex;align-items:center;gap:8px;min-width:0;margin:0 0 9px}
.entitytitle h2{margin-bottom:0}
.entityhero .alias{color:var(--muted);
  font-size:var(--fs-md);line-height:1.45;margin-bottom:0;overflow-wrap:anywhere}
/* 外链那一行的位置：一枚 36px 高的占位按钮。 */
.entitylinks{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px;max-width:100%}
.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:var(--portrait-size)}
[data-skeleton^="entity/"] .entityportrait:not(.square){border-radius:50%}
[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}}
/* 卡底那条带：横滚的同台艺人轨道，这一排是谁由 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)}
/* 资料卡下面的交集条与玻璃浮层是 `entity-filter` 岛。宿主和 React 根那一层都不生成盒子：
   浮层吸顶的范围是它父元素的盒子，只裹着它自己的父元素一格都走不动；两层让开，父元素就是
   `#index`。 */
[data-entity-filter],[data-entity-filter]>.peach-react{display:contents}
/* 每一枚就是首页那枚 `.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}
/* 正文是 `entity-body` 岛，骨架里同一格是 `.entitysection`，两者占同一个位置。宿主不加内边距、
   不起新的格式化上下文：照片墙段头的上边距要穿过它，和筛选浮层的下边距折叠成一段。 */
[data-entity-body]{min-width:0;max-width:100%}
.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}

/* 灯箱打开时的整页锁滚与共用折叠。灯箱本身在 React 里，样式见
   `frontend/src/react/photo-lightbox/photo-lightbox.css`。 */
body.photolight-open{overflow:hidden}


/* 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}
/* 字头占位照页面那行 26px 的标题加 8px 下边距，一组卡的高度才和落地后的一样。 */
.index-skeleton .indexletterskeleton{display:block;width:48px;height:26px;margin:0 0 8px}
.index-skeleton .tagwall .tg{height:auto;border-color:transparent;background:var(--hover)}
/* 标签页骨架里的视图切换靠在读数行右缘，同页面落地后那一枚的位置。 */
.index .board-filter-frame .count>.iconswitch{margin:0 0 0 auto}
.skeleton{display:block;height:18px;border-radius:var(--control-radius);background:var(--hover)}
[data-jav-skeleton-cover]{height:auto}
.board-page-skeleton[data-skeleton="board/jav"]{max-width:none}
/* 读数占位是行内的一截：它坐在最终那枚 `b`、`strong`、说明行里，行高仍由那枚元素自己给，
   数字一到这一行不长不缩。它和 `.skeleton` 挂在同一个元素上、同一档特指度，靠排在后面压住。 */
.skeleton-text{display:inline-block;vertical-align:middle;height:1em;width:12em;max-width:70%}
/* 整理卡的来源下拉按「本地」那一档的宽度画。它挂在 `.gselectfield` 上、同一档特指度，
   靠排在 01-base.css 后面压住那条 `width:100%`。 */
.organize-source-skeleton{width:100px}
.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:var(--skeleton-card-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(min(100%,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)}
.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)}
/* 微光取代整块呼吸。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,
.skeletoncard s,.skeletoncard u,
.countskeleton,.avskeleton .ring,.avskeleton .nm,.brandskeleton,.tagskeleton{position:relative;overflow:hidden}
.skeleton::after,.skeletoncard i::after,.skeletoncard b::after,.skeletoncard em::after,
.skeletoncard s::after,.skeletoncard u::after,
.countskeleton::after,.avskeleton .ring::after,.avskeleton .nm::after,
.brandskeleton::after,.tagskeleton::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{content:none}
/* 封面与头像还在路上：框上铺同一条微光，到手后淡出并糊掉，和换头像那一格
   （`reveal-skeleton`）同一形态。类名由 app.js 的 `watchPendingImages` 标、`settleImage`
   收。微光晚 180ms 才淡入，门槛与 `.skeleton-awaiting` 同一个数：本地图多半在这之前
   就到了，那种一帧都不该看见它。底色垫 `--ground`，否则半透明的微光落在 `.pic` 的
   黑底上只剩一片黑。收尾那一段（`.ui-imgdone`）在 frontend/src/card-art/card-art.css。 */
.imgwait::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)}}
/* 封面和带脸框的头像取景完才露面：大图边下边画，取景要等 `load`，之前画出来的是几何居中，
   取景那一下画面会跳。缓存图在插入时就取景（`frameCachedImages`）；回落图摘掉脸框后不再等。 */
img.cover:not([data-frame]),img[data-facebox]:not([data-face-framed]){visibility:hidden}

/* 计数骨架的宽高定死：跟着真实文本走的话，数字回来那一刻整行会横向弹一下。
   14px 远低于筛选条按钮的 32px，而 `.count` 有 min-height 兜底，所以换成真数字
   不改变行高。 */
.countskeleton{display:inline-block;width:150px;height:14px;border-radius:var(--control-radius);
  background:var(--hover)}
/* 索引页这一枚等的是「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 的内边距，内容到位后长到一百多，
   每次冷启动都把整页往下推一截；标签条有固定高度不塌，但空着一整条。 */
.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}

.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}

/* 页面标题只有一套字形。原来管理区 26px、索引页 20px、播放列表 28px，
   在侧栏里一路点过去就是三种大小；间距由各自的版式决定，字形不再各拍各的。 */
/* 行高 1.25 取自 2026-09-04 实测的 Geist Heading 32（32px/40px/600）。字距那一档
   不照抄：Geist 的 -0.04em 是给拉丁字母调的，压在中文标题上会把字挤成一团。 */
.listtitle,.index .ihead h2{
  font-size:var(--fs-3xl);line-height:1.25;letter-spacing:-.01em;font-weight:600}
.listtitle{margin:0 0 20px}
.listtitle[hidden]{display:none}

/* ── 主区：满屏，大图 ── */
main{position:relative;z-index:1;padding:14px 16px 90px}
#catalogPageHeading,#javLibraryHeader,#listTitle{padding:20px 16px 0;margin:0 0 20px}
#catalogPageHeading[hidden],#javLibraryHeader[hidden]{display:none}
#catalogPageHeading h2{margin:0;font:var(--board-title)}
h2[data-page-title]{margin:0;font:var(--board-title);letter-spacing:0}
@media(max-width:760px){#catalogPageHeading h2,h2[data-page-title]{font:var(--board-figure)}}
.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}
.resourcesyncbox{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{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{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}
.tagbar,.count{transition:background-color .16s ease,border-color .16s ease}
/* 吸顶那条是底色的半透明版，配方跟着 --ground 走：写死深色底会在浅色主题下
   变成一条黑带压在白底内容上。 */
.tagbar.is-stuck,.count.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}
/* 资源同步的「检查文件」键适配自己的忙态，不统一转圈：git-compare 照 lucide-animated 的
   GitCompare 逐笔描画（右下圆→两条枝→左上圆，画完停住再一起淡出）。
   机制同换批键：动效写在 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}}
#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)}
[data-resource-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}
/* 壳自己画进 `#grid` 的那几屏（骨架、垃圾文件队列）是一叠卡：每一段卡合成一个 `.grid`，
   `#grid` 本身只是这叠卡的容器，自己不排网格。目录、回收站与资料页的作品卡由 `catalog-grid`
   island 自己排，版面写在 `frontend/src/react/components/media-card.css`。 */
.gridstack{display:flex;flex-direction:column;gap:16px}
/* 列距 16px 是给悬停面留的：卡片悬停时底色往盒外多铺 8px（见下方 `:hover`），列距再窄，
   两张卡的悬停面就顶到邻卡上。 */
.grid{gap:24px 16px}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,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}
/* 垃圾文件队列里，视口外的卡片跳过渲染：滚动和侧栏开合时要算的只剩屏上那几十张。
   目录、回收站与资料页作品区的作品卡归 `catalog-grid` island，同一份估计写在
   `frontend/src/react/components/media-card.css`，取值与这里一致。
   跳过的是卡里的两块，封面 `.pic` 和元信息区 `.meta`；卡片盒本身照常排版，所以屏外卡的
   高度里只有元信息区一项是估计值。
   - 封面高度由 `aspect-ratio` 从列宽推出，跳过渲染时照样成立，列宽随视口和侧栏开合怎么
     变都与真实渲染一致。卡高随列宽在 270～590px 之间变（JAV 大图一档最高），整张卡
     跳过就只能给一个固定占位，对不上所有版式，没渲染过的卡越多滚动条漂得越远。
   - 元信息区高度不随列宽变，由它自己的排版 token 推出，见 `--card-meta-block`：两行标题
     （`.meta .t` 的 2.7em）、一行归属（1lh）、`.mtext` 的 3px 行距。密集一档的高度是
     `.mtext` 固定的三行，同一个变量。
   叠层纸边画在 `.pic` 外面，不在裁切范围里。
   跳过渲染连带 paint containment，裁切边是元信息区的 padding box：贴着左缘的头像悬停
   描边、头像与标签的 2px 焦点环都画在内容盒外面。所以元信息区四周垫 8px 内边距、再用
   -8px 外边距抵消，裁切边往外扩 8px（和整张卡的悬停面同宽），内容盒、卡片尺寸和它与
   封面的间距一个像素不动；`frontend/e2e/design-*.test.ts` 按几何检查岛里的同一写法。垫出来的那一圈伸到
   卡片盒外面，元信息区自己因此不接指针，只有它的子元素接：卡片的悬停面、点击区和悬停
   预览的触发范围仍是卡片盒本身。元信息区没有背景，垫出来的一圈不画任何东西。
   图片墙不在其内：它的封面没有固定比例，高度取自图片本身，跳过渲染时会按零高排版。
   资料页的照片墙与名册（`entity-body` 岛）同理不在其内，它们也不是 `.card`。资料页
   新作那一行（`.feednew`）只有一行、往横向排，屏外的卡不改变整页高度，标题字号也不是
   `--fs-md`（`.feednewcard .meta .t` 是 `--fs-sm`），不套这份估计。 */
.meta{--card-meta-block:calc(var(--fs-md)*2.7 + 3px + 1lh)}
#grid .card :is(.pic,.meta){content-visibility:auto}
/* `flex-shrink:0`：跳过渲染时元信息区的内容不参与最小尺寸，卡里有别的子元素要抢空间时它会
   被压扁。渲染时它本来就压不动（自动最小尺寸就是内容高度），钉住不改变屏上的排版。
   估计高度作用在内容盒上，内边距另外加在外面，所以 `--card-meta-block` 只估内容。 */
#grid .card .meta{flex-shrink:0;
  margin:-8px;padding:8px;pointer-events:none;contain-intrinsic-block-size:auto var(--card-meta-block)}
:where(#grid) .card .meta>*{pointer-events:auto}
/* 叠层往上溢出到行间距里，卡片本体不为它留白。留白的写法（padding-top:7px）会把
   整张卡压低 7px 并加高 7px，同一行里 Mix 卡的封面和下面的文字就比邻居矮一截；
   往上堆之后卡片盒和普通卡完全同高，下沿自然平齐。
   纸边与封面是同一种表面，圆角同一档：容器取封面那一档，纸边继承容器。 */
.mixstack{position:relative;isolation:isolate;border-radius:var(--badge-radius)}
.mixstack::before,.mixstack::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{inset:0 12px 8px;transform:translateY(-7px);opacity:.54}
.mixstack::after{inset:0 6px 4px;transform:translateY(-4px);opacity:.78}
.mixstack .pic{z-index:1}
.playlistcard .mixmeta{display:flex;min-width:0;gap:9px;align-items:flex-start}
.playlistcard .mixcopy{flex:1}
.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}
/* 圆角只在最外面裁一次：`clip-path` 配一层满格的矩形蒙版，格子里的黑底、封面、悬停层与模糊衬底
   先合成一张再按 `--pic-radius` 切。格子本身不写 `border-radius`，`overflow` 就只是矩形裁切：按圆角
   裁的话每一层各切一次，抗锯齿那一圈里底下的黑会透上来，浅色封面四角就是一圈灰黑边。光有
   `clip-path` 不够，没有蒙版时 Chrome 照样逐层切。 */
.pic{--pic-radius:var(--badge-radius);position:relative;aspect-ratio:var(--card-ratio,16/9);background:#000;
  clip-path:inset(0 round var(--pic-radius));mask-image:linear-gradient(#000 0 0);
  overflow:hidden;display:flex;align-items:center;justify-content:center}
/* 传统正封的比例留白使用同图补底，圆角由最外层统一裁切。 */
.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}
.card>.pic,.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}
/* 悬停面往卡片盒外多铺 8px：不动网格尺寸、不挤相邻卡，只让整张卡的命中反馈比内容
   大一圈。背景仍落在卡本身，保留浏览器里可直接核验的悬停底色。 */
body .card:hover{background:var(--hover);box-shadow:0 0 0 8px var(--hover)}
body .card:hover .pic{filter:saturate(.9) brightness(.94)}
body .card:focus-within{background:var(--hover);box-shadow:0 0 0 8px var(--hover)}
.select-mode .card{cursor:default}
.select-mode .hovertools{pointer-events:none!important}
.card.selected .pic,.card.selected:hover .pic{box-shadow:inset 0 0 0 2px var(--on-media)}
.hovertools{position:absolute;z-index:4;display:flex;align-items:center;gap:14px;
  opacity:0;transition:opacity .15s;pointer-events:none}
.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)}
.nopic{font-size:var(--fs-xs);letter-spacing:.12em;color:var(--muted);text-transform:uppercase}
.junkcount{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:48px}
/* 复核那条分类切换在 10 个互不相同的候选数据集之间切（真 tablist）。
   几何取 Geist Tabs 的 secondary 变体（vercel.com/geist/tabs 实测 2026-09-03）：高 32px、
   左右 12px、6px 圆角、13px/400，hover 只换文字色，选中只抬底到 --hover、不动边框。 */
.reviewtabs{display:flex;align-items:center;gap:5px;min-width:0;overflow-x:auto;scrollbar-width:none}
.reviewtabs::-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{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{color:var(--ink)}
.reviewtabs button:focus-visible{outline:2px solid var(--tungsten);outline-offset:2px}
/* 首页流和详情只留来源图标；完整来源与费用通过 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:0;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)}
.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,.mav [data-source-initial]{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)}
.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}
.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>` 就是不可点的那一种（队列行整行本身是按钮，里面嵌不了按钮）。
   染成强调色会让人以为能点，点下去却落到卡片本身、打开的是视频详情。 */
.meta span.who{color:var(--ink-2);cursor:default}
/* 标签语义分级 */
/* 卡片上那一行标签归 React 卡片（`media-card.css` 的 `[data-media-tags]`）。
   .tg 只留 max-width:100% 这一层兜底：一颗标签自己就比整张卡还宽时，横着溢出会
   压到卡片外面，这时候省略号是唯一的出路。 */
.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.meta,.tg.position{color:var(--ink-2);border-color:var(--line)}
.tg:hover{color:var(--ink);background:var(--hover)}
.tg:disabled{cursor:not-allowed}
/* 顶栏的密集模式固定为标题、身份、标签三行。缺少标签时第三行仍保留，
   同一网格不会再被大小、观看次数或长身份撑成四行。分卷卡已有 7px 叠层空间，
   密集网格的普通卡也保留同一高度，避免每行是否出现分卷卡决定整行高度。
   三行由 `.mtext` 裁住，四周垫 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>*{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 .size{flex:none;white-space:nowrap}

.srow{display:flex;gap:13px;overflow-x:auto;padding-bottom:7px}

/* 订阅源发现的新作（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}
/* 往右翻时卡片从侧栏底下穿过去，和首页那几排头像（`catalog-filter.css` 的 `[data-catalog-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}
/* 通向作品页的点击区，铺满整张卡，是一条外链。
   资产卡的 `.pic` 与 `.meta` 压在点击区上面（z-index 2），那边的点击由卡片自己的脚本
   接；这张卡只有这条外链，所以两块放行指针，落到下面的链接上，悬浮键自己再收回来。 */
.feednewopen{position:absolute;inset:0;z-index:1;border-radius:inherit}
.feednewcard>.pic,.feednewcard>.meta{pointer-events:none}
.feednewcard .meta a.mav{pointer-events:auto}
.feednewcard .meta span.mav{cursor:default}
.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}
/* 「想要」按下时星形填实：同一颗键既是开关也是读数。 */
.feednewtools button[aria-pressed="true"] svg{fill:currentColor}
.feednewcard .meta .t{font-size:var(--fs-sm)}
/* 番号整段不折行；比卡还宽的长串在卡边收成省略号，不被标题块的溢出裁成半个字。 */
/* 出演名单与完整日期分行，窄卡中的姓名仍有自己的宽度。 */
.feednewcard .meta .s{display:grid;gap:0;white-space:nowrap;min-height:2lh}
.feednewcard .meta .s>span{min-width:0}
.feednewcard .meta .s>.feednewperformers{overflow:hidden;text-overflow:ellipsis}
/* 占位卡直接套真卡的 `.t` 与 `.s`：标题块是两行 2.7em，下面那行放一个零宽字符，行高
   跟真卡那行字同一套规则，到货那一下整行不跳。 */
.feednewskeleton{pointer-events:none}
.feednewcard:not(.feednewskeleton) .pic{background-color:var(--surface);color:var(--muted)}
.feednewskeleton .pic{background:var(--hover)}
.feednewskeleton .t>.skeleton{height:2.7em}
.feednewskeleton .s>.skeleton{width:72%;height:auto}
/* 占位卡不是在等哪一张图，它本身就是等待态：微光和骨架同一帧出现。`.imgwait` 那 180ms
   的淡入门槛是给本地图的，套在这里，那 180ms 里露出来的是 `.pic` 的黑底。 */
.feednewskeleton .pic.imgwait::after{transition:none}

/* 源文件管理：定位 + 目录对账。图标必须显式写 stroke/fill，Lucide 是描边路径，
   缺了就被按默认 fill:black 画成黑块（灯箱 `photo-lightbox.css` 的圆钮图标同样显式写了）。 */
.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%}

/* 设置面板打开时锁住背后的页面。面板本身由常驻组件 `settings-panel` 实现，样式在
   `frontend/src/react/settings-panel/settings-panel.css`；类名由它的句柄开合时挂在 body 上。 */
body.settings-open{overflow:hidden}
/* 页面滚动条属于 html 而不只是 body：锁住 body 后文档根仍可滚，打开弹层时
   背后的页滚动条还在，弹层自身加上它就是两条。 */
@supports selector(:has(*)){html:has(body.settings-open){overflow:hidden}}

/* ── Toast：Sonner 的栈，Board 的面 ──
   堆叠、悬停展开、滑动关闭与悬停暂停计时照 Sonner（`frontend/src/react/toaster.tsx`）；面、线、
   外框采用 Agent Progress 的圆角、细边和接触阴影。颜色变量写在每条 toast
   自己身上，压过 Sonner 按浅色主题挂在栈上的那一组，两档主题都跟着 Board token 走。
   层级回到站内的 popover 档：Sonner 自带的九位数会连 tooltip 一起盖住。 */
#toasts [data-sonner-toaster]{z-index:var(--layer-popover);font:var(--board-body)}
#toasts .processing-task-toast{width:100%;right:0}
[data-processing-dock]{width:100%}
/* 状态色叠在不透明卡面上，悬浮任务不透出底下的页面。 */
[data-processing-dock][data-processing-tone=error]{
  background-image:linear-gradient(var(--color-background-tertiary-error),var(--color-background-tertiary-error))}
[data-processing-dock][data-processing-tone=warning]{
  background-image:linear-gradient(var(--color-status-yellow-background),var(--color-status-yellow-background))}
#toasts [data-sonner-toast][data-styled=true]{--normal-bg:var(--color-background-primary-default);
  --normal-border:var(--color-border-button-default);--normal-text:var(--color-text-primary);
  width:max-content;min-width:min(400px,100%);max-width:100%;right:0;
  padding:18px 20px;gap:12px;flex-wrap:wrap;font:var(--board-body)}
#toasts :is([data-sonner-toast][data-styled=true],[data-processing-dock]){
  border-radius:var(--floating-radius);box-shadow:var(--elevation-xs)}
/* 正文按实际内容参与排布，上限留出图标、间距与 Sonner 的光学校正边距。 */
#toasts [data-sonner-toast][data-styled=true] [data-content]{flex:1 1 auto;
  max-width:calc(100% - 28px - var(--toast-icon-margin-start) - var(--toast-icon-margin-end))}
#toasts [data-sonner-toast][data-styled=true] [data-button]{white-space:nowrap;margin-left:auto}
@media(max-width:600px){
  #toasts [data-sonner-toaster]{width:calc(100% - var(--mobile-offset-left) - var(--mobile-offset-right))}
  #toasts [data-sonner-toast][data-styled=true]{width:100%;min-width:0;left:auto}
}
/* 正文里的换行照写，长路径与无空格的串就地折行。一条最高占视口的四分之一，再长在条内滚：
   悬停展开时三条叠起来仍留在视口里，不顶出上沿。 */
#toasts [data-sonner-toast][data-styled=true] [data-title]{font-weight:500;white-space:pre-line;overflow-wrap:anywhere;
  max-height:24dvh;overflow-y:auto;overscroll-behavior:contain}
#toasts [data-sonner-toast] b{font-variant-numeric:tabular-nums}
/* 字形用全站那套描边件：成功是主色的勾，失败是警示色的圈。不写 `fill:none` 就按 `fill` 的
   初值涂黑，一个勾会糊成实心箭头；圆线帽是感叹号那一点的全部，平头线帽下它就消失。 */
#toasts [data-toast-glyph]{display:contents;color:var(--color-accent-600)}
#toasts [data-type=error] [data-toast-glyph]{color:var(--drop)}
#toasts [data-toast-glyph] svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:2;
  stroke-linecap:round;stroke-linejoin:round}
#toasts [data-sonner-toast] [data-close-button]:hover{background:var(--color-background-primary-hover);
  border-color:var(--color-border-button-hover)}
#toasts [data-sonner-toast] [data-button][aria-busy=true]{opacity:.6;cursor:default}
#toasts [data-sonner-toast] [data-action]{background:var(--board-blue);color:var(--color-text-white)}
#toasts [data-sonner-toast] [data-action]:hover:not(:disabled){background:var(--board-blue-hover);color:var(--color-text-white)}
#toasts [data-sonner-toast] [data-action]:active:not(:disabled){background:var(--board-blue-active);color:var(--color-text-white)}
#toasts [data-sonner-toast] [data-action]:focus-visible{outline:2px solid var(--color-border-focus-ring);outline-offset:2px}

/* 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 [data-follow-site-icon]{filter:none}

/* ── 标签按钮 chip ── */
/* 壳里的产地选择与作品详情岛里的几枚次要操作用这一组；侧栏的筛选标签归侧栏岛（`frontend/src/react/sidebar/`）。 */
.chips{display:flex;flex-wrap:wrap;gap:4px}
.source-icon{display:block!important;width:16px!important;height:16px!important;min-width:16px;opacity:1!important;filter:none!important;object-fit:contain}
.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)}

/* ── 列表尾部与通用面 ──
   加载更多、空态、Geist Scroller 与 Empty State、选择条与批量条、全站 760px 以下的窄屏总表，
   以及排序行的版式开关、共用勾选框与封套取景。沉浸模式的样式在
   `frontend/src/react/immerse/immerse.css`。 */
.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}
@media (max-width:760px){
  .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}
  .cleanupgrid{grid-template-columns:minmax(0,1fr)}
  .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.junkcount{align-items:stretch;flex-direction:column;overflow:visible;padding-block:8px}
  .reviewtabs button{height:44px}
  /* 顶栏在窄屏会被搜索框撑破 —— 搜索收成一个按钮，点了才展开。只裁横向：竖向也裁的话，
     那几枚玻璃按钮往下落的投影在顶栏下沿被切平，头像那一排上方就多出一条横线。 */
  .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)}
  .entityfoot{padding:10px 16px}
  /* 标题行是 flex，`text-align:center` 管不到它，得自己居中。 */
  .entitytitle{justify-content:center}
  .popmenu.gselectmenu button{min-height:44px}
  .entityhero .alias{margin-bottom:10px}
  /* 骨架那一行外链占位跟着单列卡居中，上下的让位与资料卡岛窄屏的横滚层一致，换真卡时不跳。 */
  .entityhero .entitylinks{justify-content:center;margin-top:9px;margin-bottom:-4px;padding-block:4px}
  .stats h2{font-size:var(--fs-xl)}.stats .insightcopy h2{font-size:var(--fs-3xl)}
  /* 窄屏下标题也得一起缩。`/follow` 渲染在 `#stats` 里，上面那条 `.stats h2`
     会只把它打到 23px，页面标题要跟它同一档。 */
  .listtitle,.index .ihead h2{font-size:var(--fs-2xl)}
}
/* 这个 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){
  .listtitle,.index .ihead h2{font-size:var(--fs-xl)}
}
#searchBtn{display:none}
.searchback{display:none}

.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}

/* 取景：正封模式下，整张封套要把镜头推到右半边；本身就是竖版正封的则整张用；
   16:9 官方剧照没有「正封那一块」，只能按人脸取。
   判据是图片自己的宽高比，由 onload 写进 data-frame。
   封面本身不垫底色，传统正封的比例留白由容器的同图模糊底承接，整幅大封使用纯色底。 */
.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%);aspect-ratio:var(--panel-aspect);object-fit:fill;
  clip-path:inset(var(--panel-clip,0 0 0 0))}
.poster.cover.front[data-frame="front"]{object-fit:contain;object-position:50% 50%}
/* 剧照比大图容器宽得多，裁的是横向，所以横向锚点必须跟着人走。没检出时回落
   居中：那是取不到人脸时唯一站得住的猜测。 */
/* 整张放进卡片，传统封面的比例留白由容器补底。 */
.poster.cover.whole{object-fit:contain;object-position:50% 50%}
/* 整幅画面按人脸取景并铺满卡片。 */
.poster.cover.front[data-frame="still"]{
  object-fit:cover;object-position:var(--cover-x,50%) var(--cover-y,50%)}

/* ── 在线追更 ── */
.follow{padding:4px 0 42px}
/* 这个文件只剩进页骨架（`followContentSkeletonHtml`）在用；页面本身归 `follow-feed` 岛
   （`frontend/src/react/follow-feed/follow-feed.css`，卡上的站点图标、版本字样与另见徽章也在那里），
   详情归 `follow-detail` 岛（`frontend/src/react/follow-detail/follow-detail.css`）。 */
.followauthors{padding:3px 0 10px;margin:0 0 2px;overscroll-behavior-inline:contain}
.followauthors .av{width:76px}
.followlist{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,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))}}
.followphotowall[data-images-only="true"] .skeletoncard :is(b,em){display:none}
@media (max-width:640px){
  .followauthors{margin-right:-12px;padding-top:3px}
  .followlist.followphotowall{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 8px}
}

/* 骨架里那面图片墙的列数，与资料页照片墙（`frontend/src/react/entity-body/entity-body.css`）、
   岛里那面墙（`follow-feed.css`）同一套档位。骨架一律按固定比例铺。 */
.followlist.followphotowall[data-size]{--photo-columns:5;grid-template-columns:repeat(var(--photo-columns),minmax(0,1fr))}
.followlist.followphotowall[data-size="big"]{--photo-columns:3}
@media(max-width:1400px){.followlist.followphotowall[data-size]{--photo-columns:4}
  .followlist.followphotowall[data-size="big"]{--photo-columns:2}}
@media(max-width:1000px){.followlist.followphotowall[data-size]{--photo-columns:3}
  .followlist.followphotowall[data-size="big"]{--photo-columns:1}}
@media(max-width:640px){.followlist.followphotowall[data-size]{--photo-columns:2}
  .followlist.followphotowall[data-size="big"]{--photo-columns:1}}

/* ── 关注一族与管理区共用的反馈控件 ──
   关注管理页本身归 `follow-manage` 岛，分区卡是 `frontend/src/react/components/card.tsx`。这里剩下的
   是壳与几座岛都在挂的 Geist Note、项目横幅、上下文卡、任务进度面板、创作者圆框、行内链接，
   以及链接管理骨架与进度圆环。控件按实测 Geist（docs/reference-snapshots/vercel-geist-controls-measured.md）。 */

/* Geist Note：字段、卡片、分区旁的持久上下文，以及需要用户处理的失败，全部走这里。
   瞬时回执用 toast，销毁确认用 dialog/confirm。整页范围的失败也是这个盒子，只是多
   一个关闭键（见下面的 .fwarn），不另立一套横幅。 */
/* Note 默认无填充，filled 使用不透明浅色面；状态含义由文字和图标共同表达。 */
:root{--feedback-success:var(--color-accent-600)}
:root[data-theme="dark"]{--feedback-success:var(--color-accent-400)}
@media(prefers-color-scheme:dark){:root:not([data-theme="light"]){--feedback-success:var(--color-accent-400)}}
.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-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}
.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:var(--elevation-dropdown);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}
/* 检查失败明细持续保留到状态解决，复用 Note 的边界、图标和语义色。 */
.fwarn{grid-template-columns:16px minmax(0,1fr);margin:10px 0 14px}
.fwarn>span{min-width:0}
.fwarn p{margin:0}

/* 进度面板（`frontend/src/jobs.ts`）：发丝边 --field-ring 配 primary 的白面，和 CheckboxCard 同一档，
   躺在分区的 --ground 灰面上是一张白卡。 */
.followtask{border:1px solid var(--field-ring);border-radius:var(--surface-radius);background:var(--color-background-primary-default);margin-bottom:16px}
.followtask>.geist-fieldset-content{padding:20px;min-width:0;overflow-wrap:anywhere}
.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}

/* 资源标识统一由 MiddleTruncate 计算可见文本。星号让本规则与「类 + 元素」的末尾省略
   选择器同权重，并凭源码顺序覆盖，避免两套省略同时生效。 */
*[data-middle-truncate]{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:clip}
.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}
@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）骨架的卡片框体：`.configfieldset` 几条只给配置骨架用。
   页面外壳与页签条在 `frontend/src/react/settings/configuration-page.css`；分区本身是 React 子树
   （`frontend/src/react/settings/`），样式归 `peach-react.css`。 */
.resourcesyncfooter>p{margin:0 auto 0 0;text-align:left}
.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}}

/* 原地换态动效里选中旧页面元素的几条：一行字换一行字（`.leaving` 与 board.css 共用）、失败的那一下、
   作品卡共演头像的抬起、清空搜索框时占位字的隐藏。字形换字形、读数错峰、占位换内容、成功的那一下、清空溶解、计数弹出、
   标题揭示与横排回弹在 `frontend/src/ui-kit/motion.css`，形态来源与总规矩写在那份文件的开头。

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

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

/* ── 一行字换成另一行字 ──
   退场往上走 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}

/* ── 失败的那一下 ──
   已经有红边表达「这一格不对」，抖动只是把视线拉过去，所以不另加红字、不改边框色，
   只在原位左右各走一趟再回中。走的是站内那条通用时长与缓动，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"]{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}

/* ── 清空时输入的内容溶解掉 ──
   溶解的那一层（`.cleardissolve`）在 `frontend/src/ui-kit/motion.css`；这里只在溶解期间收起
   搜索框自己的占位字，免得旧字往上飘的同时占位字在原处闪出来。 */
.search input.dissolving::placeholder{opacity:0}

