/* AppStore theme — reset + base typography + glass baseline + cover v3. §1.1, p1.6, p1.7 */

*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
/* set directly on html, not just body — .page-glow's containing block is
   the initial containing block (neither html nor body has its own
   `position`), so clipping needs to happen at the actual viewport/root
   scroller level to reliably suppress the horizontal scrollbar it causes
   by design (see the body rule below for the full explanation). */
html { overflow-x: hidden; }
/* P2 §1: locks the page scroll behind an expanded card's overlay — toggled
   by expand.js on document.documentElement, not <body>, so it wins
   regardless of which element the browser treats as the real root
   scroller (Chromium uses <html>, which is also why the p1.10 overflow-x
   fix above lives here rather than on body). */
html.no-scroll { overflow: hidden; }
body {
  font-family: var(--font-ui);
  font-size: var(--fs-body);
  line-height: 1.4;
  color: var(--text-primary);
  background: var(--bg-page);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* p1.10 §4 actual root cause: .page-glow (p1.7) is deliberately
     min(1400px, 120vw) wide, position:absolute, centered — it's SUPPOSED
     to bleed past both viewport edges (that's how a large blurred glow
     avoids a hard-edged cutoff). Nothing was clipping that intentional
     bleed at the body level, so it was creating a real page-level
     horizontal scrollbar on every viewport narrower than ~1167px (where
     120vw first exceeds the 1400px cap) — confirmed by checking which
     element's getBoundingClientRect() actually extended past the
     viewport, not just which elements had scrollWidth > clientWidth (the
     emoji-span and grid-column bugs fixed alongside this one were real,
     but were already self-contained by their own overflow:hidden
     ancestors — neither was the actual cause of <html>'s scrollbar). */
  overflow-x: hidden;
}
img { max-width: 100%; display: block; }
/* .fe (Fluent Emoji <img>s) override this — see the .cover-emoji/
   .cover-watermark block below for why max-width:100% is actively harmful
   there, and docs/p1.10-closeout.md for how that was actually diagnosed. */
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
ul { list-style: none; margin: 0; padding: 0; }
h1, h2, h3, p { margin: 0; }

/* ==================== p3.27⑥a:首屏加载罩 ==================== */
.ask-page-loader {
  position: fixed; inset: 0; z-index: 9999;
  display: flex; align-items: center; justify-content: center;
  background: var(--bg-page);
  transition: opacity .35s var(--ease, ease);
}
.ask-page-loader.is-hidden { opacity: 0; pointer-events: none; }
.ask-page-loader__dot {
  width: 34px; height: 34px; border-radius: 50%;
  border: 3px solid var(--separator);
  border-top-color: var(--tint);
  animation: ask-page-loader-spin .8s linear infinite;
}
@keyframes ask-page-loader-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .ask-page-loader__dot { animation: none; opacity: .5; }
}

/* ==================== p3.27⑥b:AJAX 导航顶部进度条 ==================== */
.ask-ajax-progress {
  position: fixed; top: 0; left: 0; right: 0; z-index: 10000;
  height: 2.5px; background: var(--tint);
  transform: scaleX(0); transform-origin: left;
  opacity: 0; pointer-events: none;
  transition: transform .25s ease, opacity .2s ease;
}
.ask-ajax-progress.is-active { opacity: 1; }
.ask-ajax-progress.is-done { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .ask-ajax-progress { transition: opacity .2s ease; }
}

/* ==================== ambient page glow (p1.7 §1) ==================== */
/* --page-h is set on <html> by app.js, synced to the active hero slide's hue,
   with a 900ms crossfade. Sits behind everything (z-index:-1) so the sticky
   glass nav naturally tints from it without any nav-specific code. */
.page-glow {
  position: absolute; top: -120px; left: 50%; z-index: -1;
  /* p1.10 §4: was min(1400px, 120vw) — deliberately wider than the content
     column so the blur falloff never shows a hard edge inside it, but nobody
     had noticed it was also wider than the VIEWPORT itself below ~1167px
     wide, which serves no visual purpose (nothing past the viewport edge is
     ever visible) while being the actual cause of a real page-level
     horizontal scrollbar — confirmed by checking which element's
     getBoundingClientRect() extended past the viewport, not just by
     scrollWidth/clientWidth (overflow:hidden ancestors show that mismatch
     too, harmlessly). Capped at 100vw: still plenty wide enough that its
     blur falloff clears the ~980-1040px content column with room to spare. */
  width: min(1400px, 100vw); height: 900px;
  transform: translateX(-50%);
  background: radial-gradient(50% 50% at 50% 30%,
              hsl(var(--page-h) 55% 34% / .38), transparent 70%);
  filter: blur(40px);
  /* transitioning "background" directly does NOT smoothly interpolate a
     gradient whose color comes from a var() substitution — the browser just
     snaps to the new resolved gradient string (confirmed by sampling
     computed backgroundImage frame-by-frame: no bug, instant jump). What
     needs to transition is --page-h itself; @property (tokens.css) makes
     that legal, and background then naturally re-evaluates every frame
     against the live-interpolating number. */
  transition: --page-h 900ms ease;
  pointer-events: none;
}
html[data-theme="light"] .page-glow {
  background: radial-gradient(50% 50% at 50% 30%,
              hsl(var(--page-h) 70% 82% / .55), transparent 70%);
}

.container {
  padding-inline: var(--space-page);
}
@media (min-width: 700px) {
  .container {
    max-width: 980px;
    margin-inline: auto;
    padding-inline: 24px;
  }
}

/* ---- Liquid Glass: ONE recipe, every browser, every glass surface (p1.12 §2) ----
   Glassmorphism needs 4 things at once — translucent base, backdrop blur,
   rounded corners, a thin light border — and p1.11 shipped 3 of them. The
   border ("最容易被省略的灵魂") was simulated with an inset box-shadow
   instead of a real border, and a diagonal sheen ::after gradient stood in
   for it visually — that sheen is what tore the player card's edge into a
   jagged seam on a real device once refraction sat on top of it (rolled
   back in step0). This version has no sheen, no noise, no per-component
   variants: ONE class, a real `border`, applied identically everywhere.
   Each component keeps only its own border-radius as a local variable —
   never its own background/blur/border recipe. Never use this over a flat,
   content-less background — there's nothing there to blur, so it just
   looks like dirty glass (p1.6 §3.2 is why chips stay solid). */
.glass {
  background: color-mix(in srgb, var(--glass-tint) 45%, transparent);
  backdrop-filter: blur(24px) saturate(180%);
  -webkit-backdrop-filter: blur(24px) saturate(180%);
  border: 1px solid rgb(255 255 255 / .35);
  box-shadow: 0 8px 32px rgb(0 0 0 / .16);
}
:root[data-theme="dark"] .glass {
  background: color-mix(in srgb, var(--glass-tint) 55%, transparent);
  border: 1px solid rgb(255 255 255 / .18);
  box-shadow: 0 8px 32px rgb(0 0 0 / .35);
}
/* graceful degrade when backdrop-filter isn't supported */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .glass {
    background: color-mix(in srgb, var(--glass-tint) 92%, transparent);
  }
}

/* text sitting directly on glass/imagery needs a shadow to hold WCAG AA */
.text-on-glass {
  text-shadow: 0 1px 2px rgb(0 0 0 / .25);
}

/* ==================== cover v5 (p1.14 §1 — single-hue, two lightness ramps) ====================
   p1.12's mesh photos are gone (step0) — a photo carries several hues of
   its own, and hue-rotate() can't turn a multi-hue image into one hue, so
   covers read as a small rainbow no matter what --h asked for. Back to
   p1.6's original, user-accepted direction: one hue (--h, still sourced
   from hues.json per p1.13, extraction tuned further this round) carried
   through a lightness ramp — cover background, watermark, attribution bar
   all key off the same --h so a card reads as one consistent surface.
   --s is NOT the hues.json-extracted saturation (p1.13's "coffee cup reads
   purple" finding was half caused by literally averaging a photo-realistic
   S/L across a stylized icon) — it's a fixed per-context value from the
   .cover--* rules below (58 large / 52 small), so texture stays
   consistent regardless of which emoji supplied the hue. The cover now
   flips its OWN lightness ramp with html[data-theme] instead of just
   dimming a fixed image via filters — light mode is a genuinely different,
   lighter gradient, not the dark one seen through a brightness() lens. */
.cover {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  /* p3.42(Collin 反馈"渐变糊成一坨、发脏"):封面渐变重做——
     旧配方的问题:单色相 + 大跨度明度直落(浅色 80%→60%、深色
     34%→18%),中段发灰发脏。新配方的三层各自负责一件事:
     ① 顶光:左上偏白色柔光(radial,高亮度低不透明),给卡片"被环
        境光照亮"的底气;
     ② 色深:右下角一团同族稍暗的色(hue -14°),给体积感,不发黑;
     ③ 基底线性:明度跨度收窄到 ~12 个百分点(浅色 92%→80%,深色
        34%→24%)且带 +10° 的色相微漂——渐变干净的关键不是颜色多,
        是明度跨度小 + 有一点点色相变化(纯单色相大明度落差的渐变必
        然在中间发灰)。 */
  background:
    radial-gradient(90% 70% at 22% 10%, hsl(var(--h) calc(var(--s) * .5%) 60% / .42), transparent 62%),
    radial-gradient(85% 85% at 88% 96%, hsl(calc(var(--h) - 14) calc(var(--s) * 1%) 22% / .38), transparent 68%),
    linear-gradient(158deg, hsl(var(--h) calc(var(--s) * 1%) 34%), hsl(calc(var(--h) + 10) calc(var(--s) * 1%) 24%));
}
html[data-theme="light"] .cover {
  /* p3.42 二次修正:第一版浅色基底 92%→80% 对低饱和色相的 emoji(音
     符这类深灰紫)会过曝成近乎白色(hero 实测:上半洗白,和深色底色
     域撞出生硬接缝)。亮度区间下沉到 88%→72%,顶光 alpha 降到 .5,
     色深角稍提——有颜色的存在感,又保留"干净不发脏"的新质感。 */
  background:
    radial-gradient(90% 70% at 22% 10%, hsl(var(--h) calc(var(--s) * .4%) 94% / .50), transparent 62%),
    radial-gradient(85% 85% at 88% 96%, hsl(calc(var(--h) - 14) calc(var(--s) * 1%) 68% / .45), transparent 68%),
    linear-gradient(158deg, hsl(var(--h) calc(var(--s) * .95%) 88%), hsl(calc(var(--h) + 10) calc(var(--s) * 1%) 72%));
}
.cover--hero, .cover--story-half, .cover--story-wide { --s: 58; }
.cover--row, .cover--mini { --s: 52; }
/* p2.1: post-icon (96/112px app-icon-style block, expand.js) — between the
   small row/mini tiles and the large hero/story covers, so its own
   saturation lands between the two established values instead of reusing
   either verbatim. */
.cover--icon { --s: 56; }
/* p3.26:歌单页卡片封面(桌面 180-200px/手机小一号,量级上更接近
   hero/story 那一组大卡,不是 row/mini 那种小图标),跟大卡用同一档饱
   和度。 */
.cover--playlist { --s: 58; }
.cover::after {
  content: "";
  position: absolute; inset: 0; z-index: 1;
  background-image: var(--noise);
  background-size: 160px;
  opacity: .03;
  mix-blend-mode: overlay;
  pointer-events: none;
}
/* p1.12: direct-child combinator, not a descendant selector — the emoji
   glyph's own <img class="fe"> now nests 3 levels inside .cover (.cover >
   .cover-center > .cover-emoji > img), and a bare ".cover img" descendant
   selector caught it too (same bug CLASS as the p1.6 icon-over-title fix:
   an unintentionally-broad descendant selector grabbing an element it
   wasn't written for). Its position:absolute + width/height:100% collapsed
   the emoji image to 0×0 — confirmed via getComputedStyle walking the
   ancestor chain, not assumed. Real photo <img>s are always a direct child
   of .cover in the markup, so > loses nothing here. */
.cover > img {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%;
  object-fit: cover;
}
/* photo covers only (p1.7 §4): tints the photo into the card's --h so it
   doesn't read as a raw, un-styled photo dropped onto a designed surface.
   Sibling of img inside the same isolated stacking context (.cover), so the
   blend only mixes with that photo, never bleeds into the page. */
.cover-tint {
  position: absolute; inset: 0; z-index: 0;
  background: hsl(var(--h) 60% 30% / .18);
  mix-blend-mode: soft-light;
  pointer-events: none;
}

/* large-format covers (hero, story wide/half) get a flat, oversized, cropped
   watermark glyph instead of a blurred "ambient" layer — no filter, ever.
   Mobile-first: p1.6's 340/220/300px sizes read as a centered background
   blob on a 390px viewport (there's nowhere for a "corner bleed" to hide at
   that width) — p1.7 §4 shrinks it uniformly on narrow screens and restores
   the larger, size-differentiated values once there's room (≥700px). */
/* p1.10 §1/§4 real root cause (superseding the first pass's .fe-only fix):
   .cover-watermark/.cover-emoji are position:absolute with width:auto
   (shrink-to-fit — centered via transform, or anchored via right/bottom).
   Giving the CHILD .fe an explicit size (previous fix) stopped the *image*
   from collapsing, but the SPAN itself was still shrink-to-fit, and
   shrink-to-fit's preferred-width pass still resolved to 0 for these two —
   confirmed via scrollWidth/clientWidth: the span's own clientWidth stayed
   0 even after the child rendered correctly, so the now-properly-sized
   image simply overflowed OUT of a nominally-zero-width box. Where an
   overflow:hidden ancestor sat in between (.cover does), that stayed
   invisible; where it didn't (.section has none), the overflow propagated
   all the way to <html> — this was the actual mechanism behind the list-
   width bug in §4, not a grid issue. Fix: give the SPANS themselves an
   explicit width+height per context (matching font-size, so the
   native-character fallback still sizes correctly if .fe never hydrates),
   so there's no shrink-to-fit computation left to go circular. .fe then
   just fills its now-definite parent at 100%/100% — percentages against a
   definite containing block have no circularity to hit. */
.cover-watermark {
  position: absolute; z-index: 0;
  pointer-events: none;
  right: -6%; bottom: -14%; /* p1.11 §1: was -18%, spec's literal corrected value */
  width: 180px; height: 180px;
  font-size: 180px;
  line-height: 1;
  opacity: .10;
  transform: rotate(-10deg);
  filter: none;
}
@media (min-width: 700px) {
  .cover-watermark { width: 300px; height: 300px; font-size: 300px; opacity: .14; }
}
/* P3 emoji 二次放大轮:"水印字形同步放大"——水印本来就是故意做得比主
   字形大、往卡外溢出被裁切的"背景鬼影",这里跟主字形用同一套相对单位
   (story 用 cqh、hero 用 vh/桌面定值),保持"水印明显比主字形大"这个
   一直以来的视觉关系,不是各自独立调出来的数字。宽高/字号三个值必须
   保持一致(不能只改 font-size),不然 emoji 图片的正方形画布和文字排
   版盒子对不上,会整个走形。 */
.cover--story-wide .cover-watermark {
  width: clamp(180px, 73cqh, 300px); height: clamp(180px, 73cqh, 300px); font-size: clamp(180px, 73cqh, 300px);
}
.cover--story-half .cover-watermark {
  width: clamp(170px, 64cqh, 290px); height: clamp(170px, 64cqh, 290px); font-size: clamp(170px, 64cqh, 290px);
}
.cover--hero .cover-watermark {
  width: clamp(240px, 34vh, 340px); height: clamp(240px, 34vh, 340px); font-size: clamp(240px, 34vh, 340px);
}
@media (min-width: 700px) {
  .cover--hero .cover-watermark { width: 480px; height: 480px; font-size: 480px; } /* 桌面 art zone 固定 300px,同主字形一样改回定值,和 170px 的主字形保持约 2.8x 的既有比例关系 */
}

/* ==================== emoji: bare, structurally centered, per-scenario (p1.13 §2) ====================
   p1.11's squircle plate is gone (step0). Centering is a real flex/grid box
   (place-items:center centers WHATEVER is inside it by construction, no
   height assumption to get wrong — see docs/p1.11-polish.md 施工笔记 for
   the three rounds of clamp() math that this replaced) rather than a tuned
   top:XX%/clamp() offset. p1.12 gave every context the SAME bottom-heavy
   inset (18% default / 8% hero) so small tiles with no text/attribution
   below them still lost 18% of their box to a let-go that had nothing to
   avoid — that's what read as "still off-center" this round: default is
   now a plain full-box center, and only the two contexts that actually
   have something below the cover (story cards' attribution bar, hero's
   text stack) carve out room for it. Drop-shadow is per-scenario too — the
   one shared 0 14px 28px value was tuned for the 112px hero glyph and just
   ate a 30px row icon's bottom edge, reading as "shifted up" to the eye. */
.cover-center {
  position: absolute; inset: 0; z-index: 2;
  display: grid; place-items: center;
  pointer-events: none;
}
/* story cards: carve out exactly .attribution's own min-height (64px, a
   fixed px value regardless of card size) — not a % guess of the card's
   own height, which varies by aspect-ratio/viewport while the bar doesn't. */
.cover--story-half .cover-center,
.cover--story-wide .cover-center {
  inset: 0 0 64px 0;
}
/* hero 封面 emoji 的定心框下移(上边界收紧到 42%),让它靠近 art-zone
   下半区,和下方独立绝对定位的标题/副标题挨得更近,不再隔着一大截空
   白。需要配合下面 .hero__art-zone 的遮罩渐变起点同步后移,否则 emoji
   移下去后底部会被那条"融入 hero 纯色背景"的渐变过早吃掉。只影响封
   面图层自身定位,不影响标题/副标题的九宫格绝对定位系统。 */
/* p3.48:hero art-zone 铺满 100% 后(P3.47),emoji 不再从 42% 往下
   压——改回和 story 大卡同一套居中(inset:0 + place-items:center),
   只给底部信息条让出高度(和 story 卡的 64px 让位同一个思路,hero 的
   信息条高一些,让 130px)。 */
.cover--hero .cover-center { inset: 0 0 130px 0; }

.cover-emoji {
  display: block;
  line-height: 1;
}
/* p3.42:emoji 贴纸化——参考图里 emoji 像"贴在卡上的立体贴纸"。三层
   光影:① 一圈薄薄的白色覆膜边(drop-shadow 0 0 跟随 alpha 轮廓,读
   起来像贴纸的透明包边);② 主投影用卡片自己的色相(hue-tinted,投
   影带色比纯黑高级——真实光照下影子带环境色);③ 贴地接触影。
   尺寸分档沿用既有约定(大卡大影、小卡小影)。 */
.cover--row .cover-emoji,
.cover--mini .cover-emoji {
  filter:
    drop-shadow(0 0 1px rgb(255 255 255 / .65))
    drop-shadow(0 3px 6px hsl(var(--h) 45% 25% / .30))
    drop-shadow(0 1px 2px rgb(0 0 0 / .18));
}
.cover--story-half .cover-emoji,
.cover--story-wide .cover-emoji {
  filter:
    drop-shadow(0 0 1.5px rgb(255 255 255 / .75))
    drop-shadow(0 9px 18px hsl(var(--h) 45% 28% / .34))
    drop-shadow(0 2px 5px rgb(0 0 0 / .18));
}
.cover--hero .cover-emoji {
  filter:
    drop-shadow(0 0 2px rgb(255 255 255 / .80))
    drop-shadow(0 14px 28px hsl(var(--h) 45% 26% / .40))
    drop-shadow(0 3px 7px rgb(0 0 0 / .20));
}
.cover--icon .cover-emoji {
  filter:
    drop-shadow(0 0 1.2px rgb(255 255 255 / .70))
    drop-shadow(0 7px 14px hsl(var(--h) 45% 26% / .32))
    drop-shadow(0 2px 4px rgb(0 0 0 / .18));
}
.cover--playlist .cover-emoji {
  filter:
    drop-shadow(0 0 1.5px rgb(255 255 255 / .75))
    drop-shadow(0 8px 16px hsl(var(--h) 45% 26% / .34))
    drop-shadow(0 2px 4px rgb(0 0 0 / .18));
}

.fe { display: block; max-width: none; height: 1em; width: auto; }

/* main emoji size per context. .cover-center's structural centering (grid
   place-items:center, above) doesn't care about the glyph's own size, so
   none of this needs any centering-logic changes — just the font-size. */
.cover--row .cover-emoji         { font-size: 40px; } /* P3 emoji放大轮①: 30→40, confirmed good, untouched this round */
.cover--mini .cover-emoji        { font-size: 22px; } /* not in either放大轮's list */
/* P3 emoji 二次放大轮: story 卡改用 cqh(相对 .story-card 自己的渲染
   高度,见该规则的 container-type:size),卡越大字号跟着越大,不再是
   "大卡里一个固定小 emoji"。clamp() 的上下限是保险丝,不是主要驱动力
   ——地板防止极端小卡片跌破可读、天花板防止桌面双列宽卡(实测渲染高
   度可达 560px 左右)把 emoji 撑到荒谬的尺寸。38cqh/33cqh 这两个百分
   比不是凭感觉定的:第一版按"移动端单列典型卡高 330/374px"估算用了
   30/28cqh,实测移动端真实卡高只有 263–358px(比估算矮得多——宽卡
   4/3、窄卡 4/3.4 两种长宽比,乘以移动端实际可用宽度算出来的结果),
   30cqh 在这个真实高度区间只能落到地板值(90px),没有真正按比例伸
   缩。反推移动端中段高度(~310/315px)要落在 120px/104px 目标值上分
   别需要 38%/33%,改用这两个数字后地板才真正只在极端情况兜底,中段
   高度才是 cqh 在实际驱动尺寸。 */
.cover--story-wide .cover-emoji  { font-size: clamp(85px, 38cqh, 155px); }
.cover--story-half .cover-emoji  { font-size: clamp(75px, 33cqh, 140px); }
/* hero 艺术区没有 story 卡那种"渲染宽度随网格布局变化"的问题——移动端
   固定 38vh、桌面端固定 300px(components.css .hero__art-zone),两个
   断点各自都是已知的定值,不需要 container query 也能做到"跟着区域
   走":移动端用 vh(区域本身就是 vh 定义的,emoji 字号跟着同一个基准
   走天然成比例),桌面端断点内直接给一个校准过的定值。19vh 是在常见
   移动视口高度(800–930px)下反推校准到 160–180px 目标区间的系数,不
   是"艺术区高度的 34%"这个近似说法的字面换算(38vh×34%≈13vh 换算出
   来的像素结果明显达不到 160px,两个说法对不上,这里以给出的具体像
   素区间为准,百分比只是大致的口语化说法)。 */
.cover--hero .cover-emoji        { font-size: clamp(150px, 19vh, 180px); }
@media (min-width: 700px) {
  .cover--hero .cover-emoji      { font-size: 170px; } /* 桌面 art zone 固定 300px,不再随 vh 变化,改回定值 */
}
/* icon's own box grew alongside this (96→128 / 112→140, expand.css) so a
   108px glyph still sits comfortably inside with room to spare, not
   clipped by .post-icon's overflow:hidden — see expand.css's own comment
   on that box-size bump for why growing the glyph alone would have been
   a real overflow, not just a cosmetic tweak. */
.cover--icon .cover-emoji        { font-size: 108px; }
/* p3.26:歌单卡片封面——卡片宽度就两档已知定值(playlist.css 的
   700px 断点,不是任意父级网格算出来的宽度),直接按两档给定值,不需
   要引入 container query。 */
.cover--playlist .cover-emoji    { font-size: 80px; }
@media (min-width: 700px) {
  .cover--playlist .cover-emoji  { font-size: 100px; }
}

/* p1.14 §1: dark mode back to a plain 2-stop fade (the mesh-era 3-stop
   tweak was compensating for the mesh's own brightness curve, gone now).
   Light mode is the opposite direction, not just a lighter dark: .cover's
   own background is light there and .attribution's text is now dark
   (below), so a BLACK scrim would just muddy a light card for no reason —
   a white-based gradient gently brightens the bottom edge instead, "顶部
   提亮不压黑". Scoped to the emoji-driven half cards only — the wide
   card's cover is a real photo with permanently-white .cover-text
   (unrelated to --h, same "image covers sit outside this logic" rule as
   p1.12/p1.13), so it keeps the dark scrim in both themes or its caption
   loses contrast. */
.cover-scrim {
  position: absolute; inset: auto 0 0 0; height: 46%; z-index: 2;
  background: linear-gradient(to top, rgb(0 0 0 / .42), transparent);
  pointer-events: none;
}
html[data-theme="light"] .cover--story-half .cover-scrim {
  background: linear-gradient(to top, rgb(255 255 255 / .30), transparent 55%);
}
.cover-text {
  position: absolute; z-index: 3;
  left: 14px; bottom: 12px;
  font-weight: 700;
  color: #fff;
  text-shadow: 0 1px 3px rgb(0 0 0 / .35);
  line-height: 1.15;
}
.cover-text.is-italic { font-style: italic; }

/* ---- pill button ---- */
.btn-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 16px;
  border-radius: var(--radius-chip);
  font-size: 15px;
  font-weight: 600;
  color: var(--tint);
  background: var(--pill-bg);
  border: none;
  transition: background-color var(--dur-hover) var(--ease);
}
@media (hover: hover) {
  .btn-pill:hover { background: rgb(120 120 128 / .24); }
}
.btn-pill:active { opacity: .6; }

/* ---- card depth: story cards, hero, player card only (p1.6 §3.3 — NOT article rows) ---- */
.card-depth {
  box-shadow:
    0 1px 2px rgb(0 0 0 / .30),
    0 12px 32px rgb(0 0 0 / .35),
    inset 0 1px 0 rgb(255 255 255 / .07);
  transition: transform 200ms var(--ease-spring), box-shadow 200ms;
}
@media (hover: hover) {
  .card-depth:hover {
    transform: translateY(-2px) scale(1.004);
    box-shadow:
      0 2px 4px rgb(0 0 0 / .30),
      0 20px 44px rgb(0 0 0 / .42),
      inset 0 1px 0 rgb(255 255 255 / .09);
  }
}
.card-depth:active { transform: scale(.982); }
