/* AppStore theme — component styles. §1.3, §2.1, §2.4, docs/p1.6-hue-system.md, docs/p1.7-light-and-motion.md */

/* ==================== chrome: nav pill (desktop) / hero overlay + tab bar (mobile) ==================== */
/* p1.8 §1: real-device reference (iPad Arcade/Today) shows a floating,
   centered glass PILL, not a full-width bar — it sits over the hero art,
   hero runs flush to the viewport's top edge behind it. Mobile has no top
   chrome at all; the site name overlays the hero art directly (§1 mobile),
   same as "Arcade" does on the iPhone reference. Bottom tab bar unchanged. */

.topbar-row { display: none; }
.nav-pill { display: none; }

.tab-bar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 50;
  display: flex;
  padding: 8px 12px calc(8px + env(safe-area-inset-bottom, 0px));
}
.tab-bar__item {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 4px 0;
  font-size: 10px;
  color: var(--text-tertiary);
}
.tab-bar__item .tab-bar__icon { font-size: 22px; line-height: 1; }
.tab-bar__icon .fe { height: 22px; width: auto; display: block; }
.tab-bar__item.is-active { color: var(--tint); }

body { padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px)); }

@media (min-width: 700px) {
  .tab-bar { display: none; }
  /* p1.11 §2: reverses p1.9's "pill floats over hero art" — per the iPad
     Today reference the nav pill gets its OWN row at the very top, on the
     page background, not overlapping the hero. 64px reserved via padding
     (not a dedicated wrapper element — the pill itself stays position:fixed
     and just renders inside that reserved strip, "floating and following"
     on scroll exactly as before, which is the part explicitly kept). */
  body { padding-bottom: 0; padding-top: 64px; }

  /* p3.20①:原来 position:fixed + left:50% + translateX(-50%) 这三行是
     写在 .nav-pill 自己身上的——现在挪到外层 .topbar-row,.nav-pill 变
     回一个普通的 flex 子元素,和球槽位(.ask-topbar-balls)平级。整组
     (胶囊+球)作为一个整体居中,视觉效果和以前"胶囊自己居中"完全一
     致,球的相对位置则完全交给 flex 布局,不再需要 JS 读任何坐标。 */
  .topbar-row {
    display: flex;
    align-items: center;
    gap: 8px;
    position: fixed;
    top: 12px; left: 50%;
    transform: translateX(-50%);
    z-index: 50;
  }
  .nav-pill {
    /* p1.12 §2: background/blur/border/shadow all come from the shared
       .glass class in base.css now (in the HTML: class="nav-pill glass") —
       this rule only keeps the pill's own layout + border-radius. */
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 6px;
    border-radius: 999px;
  }
  .nav-pill a {
    padding: 7px 16px;
    border-radius: 999px;
    font-size: 15px;
    font-weight: 500;
    color: var(--text-primary);
    transition: background-color var(--dur-hover) var(--ease), color var(--dur-hover) var(--ease);
  }
  /* p3.26:导航项原来只有 .active 这一档背景色,没点开的项鼠标移上去毫
     无反应——补一条轻量 hover(比 .active 更淡,不会和"当前在哪一页"的
     视觉信号混淆)。 */
  .nav-pill a:hover:not(.active) {
    background: rgb(120 120 128 / .08);
  }
  .nav-pill a.active {
    background: rgb(120 120 128 / .16);
    color: var(--tint);
    font-weight: 600;
  }
  /* p3.20①:导航旁的球槽位——只在原来"桌面"断点(≥1200px)显示,700-
     1199px 那段维持既有的平板右下角圆圈(player.js buildCircleTrigger
     ('ask-player__tablet', ...)那个不同的、本来就不依赖导航胶囊坐标的
     组件,这个断点范围本身从来不是反复错位的那个 bug,不需要跟着大
     改)。#appstoreTopbarBalls 这个 id/class 和 post.php 独立文章页复用
     同一份(两个模板互斥,不会同时出现在同一个页面里),球的具体尺寸/
     动画/落位顺序也因此和文章页那一份完全一致,不是又维护一份新样
     式。 */
  .topbar-row .ask-topbar-balls { display: none; }
  .topbar-row .ask-theme-toggle { display: none; }
}
@media (min-width: 1200px) {
  .topbar-row .ask-topbar-balls { display: flex; }
  .topbar-row .ask-theme-toggle { display: flex; }
}

/* p3.25①:桌面深浅快捷钮——和 .topbar-row 里两球同一断点显示/隐藏(球
   槽位在 <1200px 收起,这颗按钮跟着收起,不然会出现"球都没了,单独一
   个切换钮孤零零飘着"的断点不一致)。用和两球相同的 44px/glass 尺寸,
   紧跟在语言球右边(HTML 顺序已经排在 #appstoreTopbarBalls 后面),视
   觉上读作同一组控件。 */
.ask-theme-toggle {
  width: 44px; height: 44px;
  border-radius: 50%;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  flex: 0 0 auto;
  color: var(--text-primary);
}
/* p3.25①:文章页顶栏(独立直达页 + 首页内联展开态共用)里这颗钮跟着
   同一处两球的 36px 尺寸走,且不受上面 1200px 断点收起规则影响——文章
   页顶栏本来就在所有宽度下常驻显示,不是导航胶囊旁那种"窄屏收起"的
   槽位。 */
.post-topbar__right .ask-theme-toggle {
  display: flex;
  width: 36px; height: 36px;
  font-size: 16px;
}

/* p3.23②:导航栏两球放大到接近胶囊高度——实测导航胶囊(.nav-pill)真实
   高度 49px,两球原来是和文章页顶栏共用的 .ask-player__circle--inline/
   .ask-lang-circle--inline(36px,那边紧贴返回/分享两个 36px 圆按钮,
   本来就该保持 36px 不变),这里单独用 .topbar-row 限定范围只放大导航
   胶囊旁这一份,不动文章页顶栏那份共用样式。44px 留出上下各 2.5px 左
   右的呼吸边,不是贴死胶囊边缘。语言球里的地球 emoji/角标(p3.24①)
   跟着容器同一个放大比例调大,不然容器变大了图标看着更小、显得中间
   空一圈——封面(.ask-player__circle-coverwrap)是百分比尺寸,容器一
   放大它自动跟着满,不需要单独再调。 */
.topbar-row .ask-player__circle--inline,
.topbar-row .ask-lang-circle--inline {
  width: 44px; height: 44px;
}
.topbar-row .ask-lang-circle--inline .ask-lang-circle__globe { font-size: 29px; }
.topbar-row .ask-lang-circle--inline .ask-lang-circle__badge {
  min-width: 18px; height: 18px; font-size: 9px; line-height: 18px;
}

/* ---- mobile-only: site name + circular glass button overlaid on the hero art ---- */
.hero__mobile-bar {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 4;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  padding: 14px var(--space-page) 0;
}
.hero__brand {
  font-size: 28px;
  font-weight: 800;
  color: #fff;
  text-shadow: 0 1px 3px rgb(0 0 0 / .35);
}
/* p3.16 A①:配了 Logo 图片时用图片替代文字站名——建议尺寸 ≤200px 高
   (面板字段说明里也写了这条建议),这里再用 max-height 兜底,即使管理
   员传了张更大的图也不会把整个 hero 顶栏撑爆。 */
.hero__brand--logo { max-height: 36px; width: auto; display: block; text-shadow: none; }
/* p3.23⑤:语言球槽位 + 主题切换按钮的小分组——原来 .hero__mobile-bar
   只有品牌名(左)和主题切换按钮(右)两个 flex 子元素,justify-content:
   space-between 天然两端对齐;现在右边多一个语言球,包一层小分组维持
   "左边品牌名、右边一组控件"这个两端布局不变,组内两颗圆自己再横向排
   一下,不需要重新设计整条 .hero__mobile-bar 的对齐方式。 */
.hero__mobile-controls { display: flex; align-items: center; gap: 8px; }
.hero__mobile-controls .ask-lang-circle {
  width: 32px; height: 32px;
  --glass-tint: #141418; border-color: rgb(255 255 255 / .22);
  box-shadow: 0 4px 16px rgb(0 0 0 / .3); color: rgb(255 255 255 / .92);
}
.hero__mobile-controls .ask-lang-circle__globe { font-size: 18px; }
.hero__mobile-controls .ask-lang-circle__badge {
  min-width: 13px; height: 13px; font-size: 7px; line-height: 13px;
}
.hero__circle-btn {
  /* p1.12 §2: uses the shared .glass class (HTML: class="hero__circle-btn
     glass") for background/blur/border — this rule only keeps layout +
     border-radius, plus a pinned --glass-tint. It always sits on the
     hero's own dark art regardless of page theme, so it can't let
     --glass-tint follow html[data-theme] the way nav-pill/attribution do
     (that's the same bug the p1.7 player-button fix was for — a light-mode
     tint would wash out here). Pinning the token, not forking the recipe. */
  --glass-tint: #141418;
  width: 32px; height: 32px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 15px;
  flex: 0 0 auto;
  border-color: rgb(255 255 255 / .22);
  box-shadow: 0 4px 16px rgb(0 0 0 / .30);
  color: rgb(255 255 255 / .92);
}
@media (min-width: 700px) {
  .hero__mobile-bar { display: none; }
}

/* ==================== hero (p1.8 §2 — rebuilt around the real-device ref) ==================== */
/* Old shape: text overlaid bottom-left on the art, on a scrim. New shape,
   matched to the iPhone/iPad Arcade screenshots: art region up top that
   fades into a solid dark field via mask (NOT overlaid text — the art and
   the text are two separate zones now), then centered eyebrow/title/
   subtitle, then the player card embedded inside the hero's own dark field,
   then centered dots. The whole hero container carries a dark gradient
   field keyed to the active slide's hue (var(--page-h), already kept in
   sync by app.js for the ambient glow) — light theme still gets a dark hero,
   same as the reference: the hero is dark chrome, not page content.  */

/* p1.14 §1: literal spec values use --h, but nothing sets --h at the .hero
   level (it's local to each .cover/.story-card) — --page-h is what's
   actually in scope here (kept in sync with the active slide's hue by
   app.js already, p1.7 §1), so the same number is applied through that
   variable instead. Spec calls this a "纯色段" (solid field) — flattened
   from the old 2-stop gradient to one solid color per theme accordingly. */
/* p3.18①死规矩:hero 高度必须恒定,不随"是否显示播放器卡位"、也不随
   标题/副标题文字多少变化——p3.17⑤那次是"按经验测出的像素差值反向补
   偿 art-zone 高度",本质上还是让 art-zone 和内容区各自浮动、只是凑巧
   把总高度拉平,标题本身仍然不是"真正居中"而是"挪到一个新的固定位
   置"——只要再出现第三种内容组合(比如某天标题特别长换两行),同一个
   bug 就会用不同的样子复发。这次换成结构性修法:.hero 本身是一个固定
   高度的 flex 列容器,art-zone 吃固定的一份(下面 flex:none 那份),剩
   下的空间整块交给 .hero__content(body+播放器卡位+圆点,这一轮新加的
   包裹层,见 index.php)用 flex 居中——不管卡位显示与否、标题一行还是
   两行,.hero__content 这个整体始终在"art-zone 让出的剩余空间"正中
   间,.hero 的总高度是写死的数字,从头到尾没有任何东西会撑高或压矮
   它。 */
.hero {
  position: relative;
  display: flex;
  flex-direction: column;
  /* mobile-only height (desktop overrides to a fixed 480px below at
     min-width:700px) — was 58vh, cut ~15% since it was taking up too much
     of the screen on phones. */
  height: 49vh;
  /* p3.46(Collin 方向纠正):hero 下半不再跟随封面色,统一成 story
     大卡同款的**中性深色信息条**——封面铺满 + 下半深色渐变,任何封
     面都浑然一体,不再断裂/撞色。--page-h 不再驱动 hero 底色(它还管
     氛围光和 eyebrow 的同族色,那两条照旧)。 */
  background: linear-gradient(180deg, hsl(222 20% 13%), hsl(224 24% 8%));
  transition: --page-h 900ms ease;
  overflow: hidden;
}
html[data-theme="light"] .hero {
  /* p3.46:hero 是深色卡(深浅色主题都一样),下半信息条统一中性深
     色——浅色主题不再另配一套亮色底。 */
  background: linear-gradient(180deg, hsl(222 20% 13%), hsl(224 24% 8%));
}

.hero__art-zone {
  position: relative;
  flex: none;
  /* p3.47:封面铺满整张 hero(100%)——原来 65% + 底部 mask 溶进 hero
     底色的做法,让下半是"另一块颜色",是历来的硬切/断裂感来源;现
     在 hero 下半用 story 大卡同款的中性深色信息条(见 .hero__content),
     封面铺满,信息条压在封面上面,浑然一体。 */
  height: 100%;
  overflow: hidden;
}
.hero__track {
  position: relative;
  width: 100%;
  height: 100%;
}
.hero__slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity 600ms ease;
  /* p3.36③B:和上面 .story-card 同一个理由——CoverKit 的封面模板要用
     cqw/cqh 按这张幻灯片自己的实际渲染尺寸算字号/装饰元素比例(hero 的
     art-zone 高度在手机端是 49vh 的 65%、桌面是固定 480px 的 65%,两种
     情况下这张 slide 的实际长宽比完全不同),没有这行的话 cqw/cqh 会退
     化成相对视口算,在 hero 这种"自己不是全屏"的场景里明显偏大。 */
  container-type: size;
}
.hero__slide.is-active {
  opacity: 1;
  z-index: 1;
  pointer-events: auto;
}
.hero__slide .cover {
  position: absolute;
  inset: 0;
}
/* Ken Burns — only the active slide's cover animates, and only while it's
   playing; base.css/tokens.css already forbid animating anything but
   transform/opacity (军规1), and this is exactly that. */
@keyframes hero-kenburns {
  from { transform: scale(1); }
  to   { transform: scale(1.06); }
}
.hero__slide.is-active .cover {
  animation: hero-kenburns 8s linear forwards;
}
/* emoji frames' watermark reads quieter here than on story cards — it's
   sharing the frame with a whole art zone, not filling a card (p1.8 §2). */
.hero__art-zone .cover-watermark { opacity: .08; }

/* p3.19 A2:左右切换按钮——挂在 art-zone 里(和轮播图片同一层),纵向居
   中在图片区域本身,不参与下面 .hero__content 那组文字/圆点的居中计
   算,所以它的位置也不会像圆点曾经那样跟着标题行数漂移。 */
.hero__nav-btn {
  position: absolute; top: 50%; transform: translateY(-50%);
  z-index: 3; width: 32px; height: 32px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  color: #fff; border: 1px solid rgb(255 255 255 / .35);
  opacity: .85; transition: opacity 180ms ease;
}
.hero__nav-btn:hover, .hero__nav-btn:focus-visible { opacity: 1; }
.hero__nav-btn--prev { left: 12px; }
.hero__nav-btn--next { right: 12px; }
@media (max-width: 699px) {
  /* 手机端本来就能滑动切换,按钮做小一点、退到更靠边,不抢图片本身的
     视觉重量,但仍然保留(总指挥要求的是"加"，不是"仅桌面才加"）。 */
  .hero__nav-btn { width: 28px; height: 28px; }
  .hero__nav-btn--prev { left: 8px; }
  .hero__nav-btn--next { right: 8px; }
}

/* .hero__content 是 position:absolute; inset:0,铺满整个 .hero(art-
   zone + 文字区合并成一个框),不是 flex 排列里的一个普通成员——早期
   版本把它当 art-zone 让出的"剩余空间"里的一个 flex 子元素,只占 hero
   底下一小条(约 35% 高),九宫格的垂直档位(上/中/下)在这一条窄带里
   挪来挪去肉眼根本看不出差别,是坐标系选错了,不是哪条 CSS 属性写错
   了。现在九宫格的 align-items/justify-content 是在"整张幻灯片框"内
   定位,能到达 emoji 封面所在的区域,文字压在 emoji 封面上是允许的效
   果,不是 bug。
   pointer-events:none 是因为这一层盖住了原本可交互的 art-zone(轮播图
   片区域),文字块自己没有任何可点内容,不能让它挡住下面的触摸滑动手
   势(app.js 的 touchstart/touchend 监听器绑在 .hero 本身,事件冒泡不
   受这层影响,但如果不加这行,这层本身会先"接住"点击/触摸,某些浏览
   器的手势库可能依赖 touch 目标元素本身,这里保守起见让它整层透传给
   下面的图片层)。 */
.hero__content {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  /* 默认值 = 中下(emoji 封面下方居中)——没配九宫格的幻灯片(data-align
     属性不存在)不能突变成正中央,所以基准规则直接写成中下,而不是居
     中,九个候选值里的"center"(正中)反而要单独覆盖成 center/center。 */
  align-items: center;
  justify-content: flex-end;
  /* .hero__dots 是 .hero 的独立绝对定位子元素(bottom:14px + 自身 6px
     高,顶边约在 hero 底部往上 20px 处),不在 .hero__content 这个 flex
     容器内部,所以这里的 padding-bottom 不是直接量出"文字到圆点"的间
     距,而是"文字到 hero 底边"的间距——要留出圆点占的那 20px,再加上真
     正的呼吸间距,54px 才能在两端都有安全余量。 */
  padding: 52px var(--space-page) 54px;
  overflow: hidden;
  pointer-events: none;
  /* p3.47:下半信息条;p3.48:改成复用 story 大卡 .attribution 的同一条
     配方(色相随 --page-h,深浅色两条和 story 完全一致)——hero 的这条
     是"底部压一条 story 同款信息条",顶部透明渐起做和封面的过渡。 */
  background: linear-gradient(180deg,
    hsl(var(--bar-h, var(--page-h, 220)) var(--bar-s, 38%) 20% / 0) 42%,
    hsl(var(--bar-h, var(--page-h, 220)) var(--bar-s, 38%) 20% / .62) 72%,
    hsl(var(--bar-h, var(--page-h, 220)) var(--bar-s, 38%) 20% / .82) 100%);
}
html[data-theme="light"] .hero__content {
  background: linear-gradient(180deg,
    hsl(var(--bar-h, var(--page-h, 220)) var(--bar-s, 45%) 62% / 0) 42%,
    hsl(var(--bar-h, var(--page-h, 220)) var(--bar-s, 45%) 62% / .55) 72%,
    hsl(var(--bar-h, var(--page-h, 220)) var(--bar-s, 45%) 62% / .82) 100%);
}
/* p3.48:浅色主题的信息条是浅色系的,字色和 story 大卡的浅色信息条同
   一套(同族深色,不是白字)——.hero__body 默认 color:#fff,浅色主题
   在这里覆盖。 */
html[data-theme="light"] .hero__body { color: hsl(var(--page-h) 45% 18%); }
html[data-theme="light"] .hero__title { color: hsl(var(--page-h) 45% 18%); }
html[data-theme="light"] .hero__subtitle { color: hsl(var(--page-h) 45% 18% / .72); }
html[data-theme="light"] .hero__eyebrow { color: hsl(var(--page-h) 55% 32%); }

/* ---- text: single shared block below the art, swapped + re-animated by
   app.js on each transition (not per-slide — see docs/p1.8 施工笔记 for why). ---- */
.hero__body {
  position: relative;
  z-index: 2;
  text-align: center;
  padding: 0 var(--space-page);
  color: #fff;
  max-width: 100%;
}
.hero__eyebrow {
  font-size: 12px;
  font-weight: 600;
  color: hsl(var(--page-h) 70% 75%);
  margin-bottom: 4px; /* p1.9 §3 — was 6px */
}
.hero__title {
  font-size: 24px; /* p1.9 §3 — was 26px */
  font-weight: 800;
  letter-spacing: var(--tracking-title);
  line-height: 1.15;
  margin-bottom: 8px; /* p1.9 §3 — was 10px */
  /* p3.18①"文字多少不撑高"的字面落实——标题最多两行,再长也不会把
     .hero__content 挤得更高,超出部分裁掉(视觉上几乎不会真的触发,
     现有文章标题都在一行以内,这里是给极端情况的硬保证)。 */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}
.hero__subtitle {
  font-size: 15px;
  color: rgb(255 255 255 / .75);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* re-triggered by app.js removing+re-adding .is-visible on every transition
   (content is swapped first, so the fade-in always shows the new text) */
.hero__body {
  --hero-text-y: 12px;
  opacity: 0;
  transform: translateY(var(--hero-text-y));
}
@keyframes hero-text-in {
  from { opacity: 0; transform: translateY(var(--hero-text-y)); }
  to   { opacity: 1; transform: translateY(0); }
}
.hero__body.is-visible {
  animation: hero-text-in 500ms var(--ease-spring) 120ms both;
}

/* 九宫格在整张 slide 框(.hero__content 是 position:absolute; inset:0,
   见上面)里定位,九个值各写一条规则,不用前缀/后缀选择器拼,更直
   白,也不用担心属性选择器覆盖顺序的问题。"center"(正中)是唯一水平
   和垂直都居中的值,单独一条;其余八个值都要么覆盖 justify-content
   (垂直)要么覆盖 align-items(水平)要么两个都覆盖。不带 data-align
   属性(没配过的旧内容/新增文章默认值)沿用上面 .hero__content 基准
   规则的"中下",和"bottom-center"这个显式值是同一个效果,不用重复写
   一条。
   写这段时特意检查过注释里没有"星号紧跟斜杠"这个组合——CSS 注释里出
   现这个组合会把注释提前截断,截断点之后到下一个真正收尾符号之间的
   内容全部被当成非法内容,连累后面第一条真正的规则被整条丢弃且没有
   任何报错,这个坑真实踩过一次。 */
.hero__content[data-align="top-left"]      { justify-content: flex-start; align-items: flex-start; }
.hero__content[data-align="top-center"]    { justify-content: flex-start; align-items: center; }
.hero__content[data-align="top-right"]     { justify-content: flex-start; align-items: flex-end; }
.hero__content[data-align="middle-left"]   { justify-content: center; align-items: flex-start; }
.hero__content[data-align="center"]        { justify-content: center; align-items: center; }
.hero__content[data-align="middle-right"]  { justify-content: center; align-items: flex-end; }
.hero__content[data-align="bottom-left"]   { justify-content: flex-end; align-items: flex-start; }
.hero__content[data-align="bottom-center"] { justify-content: flex-end; align-items: center; }
.hero__content[data-align="bottom-right"]  { justify-content: flex-end; align-items: flex-end; }

/* 靠左/靠右的九个位置里各有三个(*-left/*-right),文字本身的对齐方向
   跟着联动,靠边的两组还要额外让开左右切换箭头(.hero__nav-btn,32px
   圆钮 + 12px 偏移,手机端 28px + 8px)的安全区,不然文字会贴着箭头甚
   至被箭头压在上面——这个安全值取的是能同时盖住桌面/手机两种按钮尺寸
   的一个共同值,没有分别再写一次桌面/手机两条。 */
.hero__content[data-align$="-left"] .hero__body { text-align: left; padding-left: max(var(--space-page), 52px); }
.hero__content[data-align$="-right"] .hero__body { text-align: right; padding-right: max(var(--space-page), 52px); }
.hero__content[data-align="center"] .hero__body,
.hero__content[data-align="top-center"] .hero__body,
.hero__content[data-align="bottom-center"] .hero__body,
.hero__content:not([data-align]) .hero__body {
  text-align: center;
}

/* 文字允许压在 emoji 封面上,这时可读性不能丢——.hero__body 本来就有
   .text-on-glass 类(index.php)自带文字阴影,这里再给标题/副标题加一
   层更强的阴影,压在封面亮色区域上也能看清,不需要靠整块封面变暗这种
   "又回到假透明"的路子。 */
.hero__title, .hero__subtitle {
  text-shadow: 0 1px 3px rgb(0 0 0 / .45), 0 2px 10px rgb(0 0 0 / .3);
}

@media (max-width: 699px) {
  /* 手机端 hero 总高度更矮、art-zone 占比更高,上下两档的留白按比例收
     紧一点,避免标题贴着 art-zone 顶/底边。真机截图踩过一次:
     padding-top 40px 时"中上"档的标题会和左上角的站名 logo(.hero__mo
     bile-bar,14px+32px 圆钮≈46px 高)贴到一起甚至压字,56px 才能干净
     让开。 */
  /* padding-bottom 同样要留出 .hero__dots 占的那 ~20px(见桌面端那条
     注释,道理一样),46px 才能保证手机端也有 20px+ 的实际留白,不是照
     桌面版数字等比例缩小(缩小后会直接跌破 20px 下限,真机回归踩过)。 */
  .hero__content { padding-top: 56px; padding-bottom: 46px; }
}

/* 圆点不是 .hero__content 居中组的一员(见 index.php 结构注释)——绝对
   定位钉在 .hero(position:relative 的最近定位祖先)底部固定偏移,标题
   行数变化不会改变这个像素位置。z-index 和左右切换箭头(.hero__nav-btn,
   同样是 3)对齐——九宫格是绝对定位铺满整个 hero,文字块理论上可以移
   动到圆点这个位置附近,圆点作为一个可点的交互控件,不应该被纯装饰
   性的文字盖住点不到(和"文字允许压在 emoji 封面上"是两回事:那个是
   装饰盖装饰,这里是装饰不能盖交互)。 */
.hero__dots {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 14px;
  z-index: 3;
  display: flex;
  justify-content: center;
  gap: 6px;
}
.hero__dot {
  width: 6px; height: 6px;
  border-radius: 999px;
  background: rgb(255 255 255 / .45);
  transition: width .2s, background-color .2s;
}
.hero__dot.is-active {
  width: 16px;
  background: #fff;
}

@media (min-width: 700px) {
  .hero {
    /* p3.17⑤ bug 修正:这里原来写死 1040px,和下方 .chips/.section 用的
       --container-desktop(tokens.css,980px)对不上——两者都靠
       margin-inline:auto 居中,hero 单方多出的 60px 宽度左右对称地"溢
       出"在内容区两侧,视觉上读成"hero 幻灯片比下面内容区多出一块,不
       平齐"(总指挥原话)。改成同一个 token,不再另起一个数字。 */
    max-width: var(--container-desktop);
    margin-inline: auto;
    /* p1.13 §3: nav row height (body's 64px padding-top, just above) stays
       as-is — this is the "补足" (make-up) margin on hero itself, not a
       change to the row. Measured at scroll 0: pill bottom edge sits at
       61px (top:12px + its own ~49px height), hero's flow position without
       this margin was 64px — a 3px gap. +21px lands hero at 85px, a 24px
       gap from the pill's actual bottom edge. */
    margin-top: 21px;
    /* p1.11 §2 reverses p1.9's flush-top-edge/bottom-only-radius: the pill
       has its own row now, hero doesn't touch the viewport edge, so it's
       a normal fully-rounded card again. */
    border-radius: 18px;
    /* p3.18①:桌面态固定总高度用具体像素(不是 vh)——桌面视口高度变化
       区间比手机小很多,写死一个和原先"卡位还在"时视觉比例一致的数字
       (300px art-zone + 内容区剩余 180px,和手机 65%/35% 是同一个比
       例)比用 vh 更稳,不会因为一个异常宽屏矮窗口的显示器把比例挤
       乱。 */
    height: 480px;
  }
  .hero__art-zone { height: 100%; }  /* p3.47:封面铺满,同桌面 */
  .hero__title { font-size: 32px; }
}

/* p1.7 §2: reduced motion drops Ken Burns and the text's translateY entirely,
   keeping only a plain 300ms fade for both the slide cross-fade and the text. */
@media (prefers-reduced-motion: reduce) {
  .hero__slide { transition-duration: 300ms; }
  .hero__slide.is-active .cover { animation: none; }
  .hero__body { --hero-text-y: 0px; }
  .hero__body.is-visible { animation-duration: 300ms; animation-delay: 0ms; }
}

/* p3.22④:hero 播放器卡位(player-card,连同它的等化条动画/播放按钮等
   一整套样式)整块删除——总指挥改主意,hero 不再放播放器卡,HTML 已
   经不再输出这个元素(index.php),这里跟着删掉不留一段没有任何元素会
   命中的死 CSS。 */

/* ==================== chips row ==================== */

.chips {
  display: flex;
  gap: 10px;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  /* 40px, not --space-section(32px) — this is the first thing after the hero's
     dark field ends, and p1.8 §5 wants that seam to read as a deliberate
     40px gap rather than whatever the generic section rhythm happens to be. */
  padding: 40px var(--space-page) 4px;
  -ms-overflow-style: none;
  scrollbar-width: none;
  /* 16px fade at both ends hints there's more to scroll to (p1.7 §5) */
  mask-image: linear-gradient(to right, transparent, black 16px, black calc(100% - 16px), transparent);
  -webkit-mask-image: linear-gradient(to right, transparent, black 16px, black calc(100% - 16px), transparent);
}
.chips::-webkit-scrollbar { display: none; }
.chip {
  /* solid, not glass: chips float over a flat page background with nothing
     behind them to blur, and there can be many on screen at once — p1.6 §3.2 */
  scroll-snap-align: start;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 36px;
  padding: 0 14px;
  border-radius: var(--radius-chip);
  background: var(--chip-bg);
  border: 1px solid rgb(255 255 255 / .10);
  font-size: var(--fs-subhead);
  font-weight: 600;
  color: var(--text-primary);
  white-space: nowrap;
  transition: transform var(--dur-hover) var(--ease), background-color var(--dur-hover) var(--ease), box-shadow var(--dur-hover) var(--ease);
}
@media (hover: hover) {
  .chip:hover { transform: translateY(-2px); background: rgb(120 120 128 / .2); }
  html[data-theme="light"] .chip:hover { background: #fff; box-shadow: 0 4px 14px rgb(0 0 0 / .12); }
}
.chip:active { transform: scale(.982); }
@media (prefers-reduced-motion: reduce) {
  .chip:hover { transform: none; }
}
.chip__icon { font-size: 16px; }
.chip__icon .fe { height: 18px; width: auto; display: block; } /* p1.10 §1 literal value */
/* light mode: real device reference shows white chips lifted with a soft
   shadow, not a grey fill (p1.8 §4) — dark mode keeps the existing --chip-bg
   grey-fill treatment unchanged. */
html[data-theme="light"] .chip {
  background: #fff;
  border: none;
  box-shadow: 0 2px 10px rgb(0 0 0 / .08), inset 0 0 0 .5px rgb(0 0 0 / .04);
  color: #000;
}

@media (min-width: 700px) {
  .chips { padding: 40px 24px 4px; max-width: var(--container-desktop); margin-inline: auto; }
}

/* ==================== section header ==================== */

.section {
  padding: var(--space-section) var(--space-page) 0;
}
@media (min-width: 700px) {
  .section { padding: var(--space-section) 24px 0; max-width: var(--container-desktop); margin-inline: auto; }
}
.section__header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 14px;
}
.section__title {
  font-size: var(--fs-title2);
  font-weight: 700;
  letter-spacing: -0.022em;
}
/* optional, per section — real device reference carries a second line under
   most section titles ("Top Arcade Games" / editors'-pick blurb); it's the
   bit that makes a list read as curated rather than auto-generated (p1.8 §5) */
.section__subtitle {
  font-size: 13px;
  color: var(--text-secondary);
  font-weight: 500;
  margin-top: 2px;
}
.section__more {
  font-size: 13px;
  font-weight: 600;
  color: var(--tint);
}

/* ==================== story cards ==================== */

.story-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
}
/* P3 emoji 二次放大轮: container-type:size 让子元素能用 cqh(容器查询
   高度单位)算字号——story 卡实际渲染高度受 grid 布局影响(移动端单列
   撑满 vs 桌面双列宽窄卡各不相同,宽卡还是 4/3、窄卡是 4/3.4 两种长宽
   比),固定 px 或简单 vh/vw 都没法让"卡越大 emoji 跟着越大、比例恒
   定"成立,只有让 emoji 的字号直接绑定这张具体卡片自己的渲染尺寸才
   行。iOS Safari 16.0 起就支持 Container Queries,在项目自己定的
   "iOS Safari 16+"支持线内,不需要额外兜底。 */
.story-card {
  position: relative;
  border-radius: 18px; /* p1.8 §5: real device cards measure 16-18px, --radius-card(20px) ran a touch big */
  overflow: hidden;
  display: block;
  container-type: size;
}
/* direct-child combinator on purpose: .attr-icon inside .attribution also
   carries the "cover" class (for its own mini gradient background) and must
   NOT be caught by this — that was the actual cause of the icon-over-title
   bug reported in p1.6 (a descendant selector here absolutely-positioned the
   icon at the bar's padding origin, on top of the text). */
.story-card > .cover { position: absolute; inset: 0; }

/* attribution bar: full-width, flush to the card's bottom edge, only clipped by
   the card's own radius. Real glass — p1.6 §3.1 redefines the backdrop-filter
   budget as "no large blur areas", not a hard count, and a bar is a thin strip,
   so nav + player + bars can all be real glass at once.
   Bug fix (p1.6 §2): no positioned children — plain flex row, icon fixed width,
   text flexes and truncates. That's the only thing that's allowed to keep this
   from overlapping the title. */
/* p1.12 §2: shared .glass class (HTML: class="attribution glass") for
   blur/border/shadow — this rule only keeps layout, border-radius, and
   the one explicitly-permitted override: a hue-tinted background instead
   of .glass's neutral --glass-tint, so the bar still reads as "this
   card's own color" rather than generic chrome.

   p3.48:本块是从 git 历史(commit 9e49706,P3.45 之前)逐字恢复的真·
   原始版本——Collin 认可的"好看的 story 大卡信息条"就是它。P3.45-
   3.47 期间的改色/渐变全部回退。hero 幻灯片的下半信息条复用同一配方
   (见上面 .hero__content,注释里写清两条的对应关系)。 */
.attribution {
  position: absolute; inset: auto 0 0 0;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  min-height: 64px;
  /* p3.49:信息条取色优先级——有封面跟封面(cover-bar.js 写 --bar-h/
     --bar-s:CoverKit 呈现色 / 图片 canvas 主色),没封面(纯 emoji)才
     回退 --h(emoji 主色)。只取色相/饱和度,明暗仍由本配方按深浅色定,
     字色沿用原配方,天然可读。 */
  background: hsl(var(--bar-h, var(--h, 220)) var(--bar-s, 38%) 20% / .55);
}
html[data-theme="light"] .attribution {
  background: hsl(var(--bar-h, var(--h, 220)) var(--bar-s, 45%) 62% / .60);
}
.attr-icon {
  width: 40px; height: 40px;
  border-radius: 10px;
  flex: 0 0 auto;
}
.attr-text {
  flex: 1 1 auto;
  min-width: 0;
}
.attribution .t {
  font-size: 17px;
  font-weight: 700;
  color: #fff;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.attribution .st {
  font-size: 13px;
  color: rgb(255 255 255 / .72);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* p1.14 §1: literal values from spec — light field (.cover v5) needs dark
   text, same hue family as the card so it still reads as "this card's
   color", just at a lightness that holds contrast on the new light bg. */
html[data-theme="light"] .attribution .t { color: hsl(var(--h) 45% 18%); }
html[data-theme="light"] .attribution .st { color: hsl(var(--h) 45% 18% / .72); }

.story-card--wide { aspect-ratio: 4 / 3; }
.story-card:not(.story-card--wide) { aspect-ratio: 4 / 3.4; }
/* mobile default above covers both cases; desktop override below */

@media (min-width: 700px) {
  .story-grid {
    grid-template-columns: 1fr 1fr;
  }
  .story-card--wide {
    grid-column: 1 / -1;
    aspect-ratio: 5 / 3;
  }
}

/* p3.27②:Story 布局样式——"一大两小"是既有默认行为(上面两条规则),
   下面三种是新增的可选样式。"四小"/"两小并排"不需要任何新规则:两者
   都只是"若干张非 --wide 卡放进已有的 2 列网格",数量不同(4 张自然
   排成两行、2 张自然排成一行)就已经是想要的效果。"两大并排"需要单独
   把 aspect-ratio 调大,不然两张并排的卡会用默认的"小卡"比例,看起来
   和"两小并排"没区别。 */
.story-grid--two-big .story-card { aspect-ratio: 4 / 3; }
@media (min-width: 700px) {
  .story-grid--two-big .story-card { aspect-ratio: 16 / 10; }
}
/* p3.28①:"两小并排"原来固定死 2 列、还限宽 640px,PC 宽屏下两个框
   反而显宽了不好看(总指挥原话"PC 其实三个并排宽度才合适")。改成
   auto-fit + minmax 让列数跟着容器宽度自适应,不是写死 2——280px 是
   card 舒服的最小宽度,980px 内容列宽(base.css .container 的
   max-width)除以它约等于 3.5,宽屏天然铺出 3 列;容器变窄时(视口刚
   过 700px 断点、还没到桌面顶宽)自动收成 2 列;移动端(<700px)维持
   上面 .story-grid 的移动端默认单列,不需要再管。 */
@media (min-width: 700px) {
  .story-grid--two-small-side {
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  }
}

/* ==================== article rows ==================== */

.article-list {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
}
/* flat iOS grouped-list row — no card-depth here on purpose (p1.6 §3.3): no
   shadow, no own surface color, just a hairline separator and a hover/active
   background wash on the whole row. */
.article-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 4px;
  border-bottom: 1px solid var(--separator);
  border-radius: var(--radius-row);
  transition: background-color 120ms;
  /* p1.10 §4 root cause: as a CSS Grid item (article-list is display:grid),
     the default min-width:auto means this row's minimum size is its
     min-content size — which for anything containing white-space:nowrap
     text (the ellipsis-truncated title) is the FULL unwrapped text width,
     not the truncated one. One unusually long title was enough to blow its
     1fr column out to ~555px against the other column's ~436px, and since
     neither .article-list nor .section clip overflow, that leaked all the
     way out to a real page-level horizontal scrollbar (confirmed via
     scrollWidth vs clientWidth at every ancestor up to <html>). min-width:0
     is the standard fix — it's what actually makes ellipsis+nowrap work
     *within* a grid/flex item instead of just expanding the item. */
  min-width: 0;
}
@media (hover: hover) {
  .article-row:hover { background: var(--row-hover); }
}
.article-row:active { background: var(--row-active); }
.article-row__cover {
  width: 56px; height: 56px;
  border-radius: 12px; /* p1.8 §5 */
  flex: 0 0 auto;
  box-shadow: 0 1px 3px rgb(0 0 0 / .25);
}
.article-row__meta { flex: 1 1 auto; min-width: 0; }
.article-row__title {
  font-size: 16px;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.article-row__subtitle {
  font-size: 13px;
  color: var(--text-tertiary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: 2px;
}
.article-row__btn {
  flex: 0 0 auto;
}

@media (min-width: 700px) {
  .article-list { grid-template-columns: 1fr 1fr; column-gap: 24px; }
}

/* ==================== p3.23①:歌单页(playlist.php) ====================
   直接复用 .article-row 那套行组件(封面+标题/副标题+右侧按钮),playlist.php
   的注释里说明过为什么——这里只补 <button> 原生样式重置(.article-row
   原本是给 <a> 用的)和"当前播放中"高亮,不是另起一套视觉语言。 */
.playlist-list { max-width: 680px; margin: 0 auto; }
.playlist-row {
  width: 100%; text-align: left; background: none; border: none;
  font: inherit; color: inherit; cursor: pointer;
}
.playlist-row__play {
  width: 30px; height: 30px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--row-hover); color: var(--tint);
}
.playlist-row__play svg { display: block; margin-left: 1px; }
.playlist-row.is-current .article-row__title { color: var(--tint); }
.playlist-row.is-current .playlist-row__play { background: var(--tint); color: #fff; }
.playlist-row.is-current.is-playing .playlist-row__play svg { display: none; }
.playlist-row.is-current.is-playing .playlist-row__play::before {
  content: ''; width: 10px; height: 10px;
  background:
    linear-gradient(currentColor, currentColor) 0 0 / 2px 100% no-repeat,
    linear-gradient(currentColor, currentColor) 4px 0 / 2px 100% no-repeat,
    linear-gradient(currentColor, currentColor) 8px 0 / 2px 100% no-repeat;
}

/* ==================== scroll entrance (p1.7 §3) ==================== */
/* One reveal pattern for the whole page: cards fade/rise in once, the first
   time their section crosses into view (app.js: IntersectionObserver adds
   .is-inview to the .section, unobserves it — no replay on scroll-out).
   Scoped entirely inside this media query so prefers-reduced-motion just
   skips it via the cascade, no JS branching needed. If JS never runs at all,
   the <noscript> fallback in the page head keeps everything visible. */
@media (prefers-reduced-motion: no-preference) {
  .section .story-card,
  .section .article-row,
  .load-more {
    opacity: 0;
    transform: translateY(14px);
    transition: opacity 420ms var(--ease-spring), transform 420ms var(--ease-spring);
  }
  .section.is-inview .story-card,
  .section.is-inview .article-row,
  .load-more.is-inview {
    opacity: 1;
    transform: translateY(0);
  }
  /* stagger direct card children, capped at the 4th — no further increase past it */
  .story-grid > .story-card:nth-child(1),
  .article-list > .article-row:nth-child(1) { transition-delay: 0ms; }
  .story-grid > .story-card:nth-child(2),
  .article-list > .article-row:nth-child(2) { transition-delay: 60ms; }
  .story-grid > .story-card:nth-child(3),
  .article-list > .article-row:nth-child(3) { transition-delay: 120ms; }
  .story-grid > .story-card:nth-child(n+4),
  .article-list > .article-row:nth-child(n+4) { transition-delay: 180ms; }
}

/* ==================== load more ==================== */

.load-more {
  display: flex;
  justify-content: center;
  padding: var(--space-section) 0;
}
.load-more__btn {
  background: var(--bg-elevated);
  color: var(--text-primary);
  box-shadow: 0 1px 2px rgb(0 0 0 / .08);
  padding: 10px 24px;
}

/* ==================== footer ==================== */

.site-footer {
  padding: var(--space-section) var(--space-page);
  text-align: center;
  color: var(--text-tertiary);
  font-size: var(--fs-footnote);
  line-height: 1.8;
}

/* p3.16 A①⑧:社交链接/备案号——沿用页脚本身的字号/配色,不单独抢视
   觉,和落款文字同一档次(次要信息,不是导航)。 */
.site-footer__social { display: flex; justify-content: center; gap: 14px; margin-top: 6px; flex-wrap: wrap; }
.site-footer__social a { color: var(--text-tertiary); text-decoration: none; }
.site-footer__social a:hover { color: var(--text-secondary); text-decoration: underline; }
.site-footer__icp { margin-top: 6px; opacity: .8; }

/* p3.38 阶段五:页脚技术栈徽章块——参照 Collin 现博客页脚那排小色块,
   每块一个强调色(--tech-block-color,行内style 由 footer.php 按配置
   算出/轮询调色板),桌面一行排开,手机自动换行(max 6 块,不需要横
   向滚动那套机制)。放在版权文字上方,和落款/社交链接同一档次的次要
   信息区,不抢主视觉。 */
.site-footer__tech {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 16px;
}
.site-footer__tech-block {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border-radius: var(--radius-chip);
  background: color-mix(in srgb, var(--tech-block-color) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--tech-block-color) 30%, transparent);
  color: var(--text-primary);
  text-decoration: none;
  text-align: left;
  max-width: 220px;
}
a.site-footer__tech-block:hover { background: color-mix(in srgb, var(--tech-block-color) 20%, transparent); }
.site-footer__tech-icon { font-size: 16px; flex: none; color: var(--tech-block-color); }
.site-footer__tech-text { display: flex; flex-direction: column; line-height: 1.3; min-width: 0; }
.site-footer__tech-title { font-size: 12px; font-weight: 600; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.site-footer__tech-desc { font-size: 11px; color: var(--text-tertiary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@media (max-width: 480px) {
  .site-footer__tech-block { max-width: 100%; }
}

/* ==================== 多语言圆 + 弹层(p3.17②,site-i18n.js) ==================== */
/* p3.20①:语言球统一只有一种挂载方式——塞进真实 flex 槽位(--inline 变
   体,下面那条规则),不是 position:fixed 现算坐标。这里只给尺寸无关的
   外观:玻璃质感(.glass,和导航胶囊、播放器桌面音乐圆同一份视觉语
   言)、圆形、层级。 */
.ask-lang-circle {
  position: relative; z-index: 246; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer; padding: 0; border: 1px solid rgb(255 255 255 / .35);
}
/* p3.24①:P3.23⑥把圆里的🌐换成了纯文字语言代号,总指挥这一轮明确改回
   来——"Collin 要的不是纯文字"。地球 emoji 恢复成圆的主体内容(一眼认
   出"这是语言切换"),当前语言代号(shortLabel:EN/日/繁/简/FR/DE/ES)
   降级成右下角一个小角标叠加在上面,两者不是二选一,是"图标+角标"同
   时存在——图标负责"这是什么功能",角标负责"现在是哪个状态",和系统
   里"应用图标叠一个小红点/数字"是同一种组合语言。 */
.ask-lang-circle__globe { line-height: 1; }
.ask-lang-circle__badge {
  position: absolute; right: -2px; bottom: -2px;
  min-width: 15px; height: 15px; padding: 0 3px; border-radius: 999px;
  font-size: 8px; font-weight: 700; line-height: 15px; text-align: center;
  color: #fff; background: var(--tint); border: 1px solid rgb(255 255 255 / .7);
  box-shadow: 0 1px 3px rgb(0 0 0 / .3);
}
/* p3.18⑥:独立文章页顶栏槽位里的普通 flex 子元素(不是 position:fixed
   量坐标),和音乐球的 --inline 变体同一个做法、同样大小,贴着分享钮左
   边、和返回/分享按钮同一条水平线。 */
.ask-lang-circle--inline {
  width: 36px; height: 36px; z-index: auto;
  flex: none;
}
/* p3.19 A4 沿用至今的既有比例:16px×1.5=24px,槽位圆本身仍是 36×36。 */
.ask-lang-circle--inline .ask-lang-circle__globe { font-size: 24px; }

/* p3.19 A5 真实 bug 修复(p3.20①结构性重写后更新):首页把一篇文章内联
   展开成一张几乎铺满屏幕的卡片时(expand.js),原来贴导航胶囊的球还钉
   在原处,浮在新卡片上方,和卡片自己顶栏里新建的那一对重复。
   expand.js 打开卡片时始终会加 documentElement.no-scroll(和"卡片开
   着"完全同一个生命周期,不是专门为这个新开一个类),这里借用它做淡
   出——同一时间 expand.js 会在卡片自己的顶栏槽位里现建一对同款的小
   球,不是"藏起来就完了、不管访客还要不要看这两颗球"。
   p3.20①:两颗球不再各自独立定位(语言球以前是 body 的直接子元
   素)——统一都在 #appstoreTopbarBalls 这一个 flex 槽位里,首页这份槽
   位挂在 .topbar-row 下(只在 ≥1200px 由媒体查询显示),这里只隐藏
   ".topbar-row 里的球槽位"本身,不隐藏 .topbar-row 整体(nav-pill 导航
   胶囊在卡片打开期间应该继续可见、可点,不能连导航一起被藏起来)。
   #ask-player 是另一层(平板右下角圆圈+手机灵动岛,<1200px 断点用),
   和顶栏球槽位是两个独立的挂载点,一起隐藏。 */
html.no-scroll #ask-player,
html.no-scroll .topbar-row .ask-topbar-balls {
  opacity: 0; pointer-events: none;
}
#ask-player, .topbar-row .ask-topbar-balls { transition: opacity 200ms ease; }

/* p3.19 A5:落位动画——两颗球插进 .ask-topbar-balls 槽位(不管是
   post.php 真实整页加载,还是 expand.js 现建的展开卡片顶栏,同一份规
   则,不用区分挂载路径)时播放一次淡入+轻微位移,不是硬邦邦地瞬间出
   现。语言球(紧贴分享钮那一颗)先落,音乐球紧接着落——总指挥原话"顺
   序:语言球、音乐球、分享，从右到左"，从贴着分享钮的锚点开始，一个接
   一个向外落位。 */
@keyframes ask-ball-settle {
  from { opacity: 0; transform: translateX(6px) scale(.7); }
  to { opacity: 1; transform: translateX(0) scale(1); }
}
.ask-topbar-balls .ask-player__circle--inline,
.ask-topbar-balls .ask-lang-circle--inline {
  animation: ask-ball-settle 320ms cubic-bezier(.2, .9, .2, 1) both;
}
.ask-topbar-balls .ask-player__circle--inline { animation-delay: 90ms; }
.ask-topbar-balls .ask-lang-circle--inline { animation-delay: 0ms; }
@media (prefers-reduced-motion: reduce) {
  .ask-topbar-balls .ask-player__circle--inline,
  .ask-topbar-balls .ask-lang-circle--inline { animation: none; }
}
.ask-lang-popover {
  position: fixed; z-index: 247; width: 200px; padding: 8px; border-radius: 14px;
  opacity: 0; pointer-events: none; transform: translateY(-6px);
  transition: opacity 180ms ease, transform 180ms ease;
}
.ask-lang-popover.is-open { opacity: 1; pointer-events: auto; transform: translateY(0); }
.ask-lang-popover__title {
  font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em;
  color: var(--text-secondary); padding: 4px 8px 6px;
}
.ask-lang-popover__item {
  display: block; width: 100%; text-align: left; padding: 8px 10px; border-radius: 8px;
  font-size: 14px; color: var(--text-primary); background: none;
}
.ask-lang-popover__item:hover { background: rgb(127 127 127 / .14); }
.ask-lang-popover__item.is-active { font-weight: 700; color: var(--tint); }

/* ==================== p3.18⑦:搜索页 + 归档页 ==================== */
.search-form {
  display: flex; gap: 10px; margin: 4px 0 20px;
}
.search-form__input {
  flex: 1 1 auto; min-width: 0; padding: 10px 16px; border-radius: 999px;
  border: 1px solid rgb(127 127 127 / .25); background: var(--bg-elevated);
  color: var(--text-primary); font: inherit; font-size: 15px;
}
.search-form__btn { flex: none; border: none; cursor: pointer; }
.search-summary { color: var(--text-secondary); font-size: 14px; margin: 0 0 16px; }
.archive-pagenav {
  display: flex; justify-content: center; gap: 16px; padding: 24px 0;
  color: var(--tint); font-size: 14px;
}
.archive-pagenav a { color: var(--tint); text-decoration: none; }
.archive-pagenav .current { color: var(--text-secondary); font-weight: 700; }
