/* ══════════════════════════════════════════════════════════
   visual.css — 视觉升级层（排在所有样式表最后加载）
   ──────────────────────────────────────────────────────────
   放在这里而不是散进 35 个文件里，是为了：
     · 改动集中，出问题一个文件回滚
     · 层叠顺序最后，不用跟老规则比特异性
   内容：玻璃三档落地 / 主时钟卡材质 / 空态·骨架·错误三件套 /
         游戏封面图案层 / 插画 / 微交互
   ══════════════════════════════════════════════════════════ */

/* ══ 1. 玻璃三档 ═══════════════════════════════════════════
   同一条视线上只模糊一次：卡片用 card 档，浮层用 pop 档，
   整页壁纸那层才用 page 档。原来到处是 blur(24px) 叠 blur(20px)，
   视觉上没差别，GPU 却要多合成好几层。                        */
.card,
.pc-card,
.dz-card,
.tz-card {
  backdrop-filter: var(--glass-card);
  -webkit-backdrop-filter: var(--glass-card);
}
.im-win, .nc-win, #nc-win, .bell-panel,
.lk-glass, .gc-side, .gc-spot,
.custom-select-options, .tk-panel, .cd-modal .cd-card {
  backdrop-filter: var(--glass-pop);
  -webkit-backdrop-filter: var(--glass-pop);
}
/* 弹窗遮罩是全屏的，模糊半径反而要大一点才像「景深」 */
.reset-modal, .player-center-modal, .game-modal, #nc-mask, .sakura-confirm-overlay {
  backdrop-filter: blur(var(--blur-page)) saturate(120%);
  -webkit-backdrop-filter: blur(var(--blur-page)) saturate(120%);
}
/* 弹窗里的卡片不再重复模糊：它下面已经是模糊过的遮罩了 */
.reset-modal .reset-card,
.sakura-confirm-overlay .sakura-confirm-box { backdrop-filter: none; -webkit-backdrop-filter: none; }

/* ══ 2. 主时钟卡：材质 ═════════════════════════════════════
   三层叠出玻璃感：极轻的噪点（消除大面积渐变的色带）、
   顶部内发光（像有光从上方打下来）、底部一道暗边（有厚度）。
   全部用伪元素，pointer-events:none，不影响交互。              */
.main-clock-card { position: relative; isolation: isolate; overflow: hidden; }
.main-clock-card::before {
  content: ''; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background:
    radial-gradient(120% 80% at 50% -10%, rgba(255, 255, 255, .13), transparent 58%),
    radial-gradient(90% 60% at 12% 110%, rgba(255, 105, 180, .10), transparent 62%);
}
/* 噪点：一张 SVG 湍流，缩放后铺开，比 png 省一次请求 */
.main-clock-card::after {
  content: ''; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  opacity: .035; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 120px 120px;
}
.main-clock-card > * { position: relative; z-index: 1; }
/* 时间本体：稍微收一点字距，给数字加一层柔光 */
.main-clock-card .time-display {
  text-shadow: 0 0 40px rgba(255, 183, 197, .18);
}
html.perf-low .main-clock-card::after { display: none; }

/* 卡片悬停：整体抬起一点点，比只换边框色更有物理感。
   ⚠ 翻面卡（全球时区⇄时光进度、秒表⇄倒数日）的正反面自己就带 transform
   （背面是 rotateX(180deg)），再给它加 translateY 会把旋转覆盖掉 ——
   表现就是鼠标一动卡片就来回翻、像抽搐。所以这里必须排除掉它们，
   翻面卡改用阴影表示悬停。 */
.card:not(.flip-face) { transition: transform var(--dur-2) var(--ease-out), box-shadow var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-out); }
.card.flip-face { transition: box-shadow var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-out); }
@media (hover: hover) and (pointer: fine) {
  .card:not(.flip-face):hover { transform: translateY(-2px); }
  .card.flip-face:hover { box-shadow: var(--el-3); }
}

/* ══ 3. 空状态 / 骨架屏 / 错误态 ═══════════════════════════
   三件套统一长这样，JS 侧用 UIState.empty() / .skeleton() / .error() 生成 */
.ui-state {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-3); padding: var(--sp-6) var(--sp-4); text-align: center; color: var(--text-3);
}
.ui-state .ui-state-art { width: 120px; height: 96px; opacity: .9 }
.ui-state b { font-size: .95rem; font-weight: 700; color: var(--text-2); letter-spacing: .5px }
.ui-state span { font-size: .8rem; line-height: 1.6; max-width: 32em }
.ui-state .ui-state-btn {
  margin-top: var(--sp-1); padding: 8px 18px; border-radius: var(--r-full); cursor: pointer;
  font: inherit; font-size: .82rem; font-weight: 700; color: var(--text-on-accent);
  background: linear-gradient(135deg, var(--c-sakura-300), var(--c-sakura-500));
  border: 0; box-shadow: var(--el-1);
  transition: transform var(--dur-1) var(--ease-out), filter var(--dur-1) var(--ease-out);
}
.ui-state .ui-state-btn:hover { transform: translateY(-1px); filter: brightness(1.06) }
.ui-state .ui-state-btn:active { transform: scale(.97) }
.ui-state.is-error .ui-state-art { color: var(--danger) }

/* 骨架屏：一条光带扫过，别用透明度呼吸（那个看起来像卡了） */
.ui-skel { display: flex; flex-direction: column; gap: var(--sp-3); padding: var(--sp-3) 0 }
.ui-skel-row { display: flex; align-items: center; gap: var(--sp-3) }
.ui-skel-b {
  position: relative; overflow: hidden; border-radius: var(--r-sm);
  background: var(--surface-raised);
}
.ui-skel-b::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .09) 45%, transparent);
  transform: translateX(-100%); animation: uiSkel 1.25s var(--ease-inout) infinite;
}
.ui-skel-av { width: 38px; height: 38px; border-radius: var(--r-full); flex: 0 0 auto }
.ui-skel-l { height: 12px; flex: 1 }
.ui-skel-l.s { height: 10px; max-width: 45% }
@keyframes uiSkel { to { transform: translateX(100%) } }
@media (prefers-reduced-motion: reduce) { .ui-skel-b::after { animation: none } }

/* ══ 4. 游戏封面：在图标底下补一层图案，卡片从「列表」变「货架」══ */
.gc-art::before {
  content: ''; position: absolute; inset: 0; pointer-events: none; opacity: .5;
  background-image: var(--gc-pattern, none);
  background-size: var(--gc-pattern-size, 26px 26px);
  mask-image: radial-gradient(85% 75% at 50% 45%, #000 35%, transparent 78%);
  -webkit-mask-image: radial-gradient(85% 75% at 50% 45%, #000 35%, transparent 78%);
}
/* 棋类：棋盘格 */
.gc-card[data-id="gomoku"] .gc-art, .gc-card[data-id="xiangqi"] .gc-art, .gc-card[data-id="weiqi"] .gc-art {
  --gc-pattern: linear-gradient(rgba(255,255,255,.10) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.10) 1px, transparent 1px);
  --gc-pattern-size: 22px 22px;
}
/* 牌类：斜向菱格，像牌背 */
.gc-card[data-id="doudizhu"] .gc-art, .gc-card[data-id="mahjong"] .gc-art, .gc-card[data-id="spider"] .gc-art {
  --gc-pattern: repeating-linear-gradient(45deg, rgba(255,255,255,.09) 0 2px, transparent 2px 12px), repeating-linear-gradient(-45deg, rgba(255,255,255,.09) 0 2px, transparent 2px 12px);
  --gc-pattern-size: auto;
}
/* 街机：扫描线 + 点阵 */
.gc-card.classic .gc-art {
  --gc-pattern: repeating-linear-gradient(0deg, rgba(255,255,255,.07) 0 1px, transparent 1px 4px), radial-gradient(rgba(255,255,255,.10) 1px, transparent 1.4px);
  --gc-pattern-size: auto, 16px 16px;
}
/* 大富翁：同心圆，像世界地图上的经纬 */
.gc-card[data-id="monopoly"] .gc-art {
  --gc-pattern: repeating-radial-gradient(circle at 70% 30%, rgba(255,255,255,.09) 0 1px, transparent 1px 16px);
  --gc-pattern-size: auto;
}
/* 悬停时一道光扫过封面 */
.gc-art .gc-sheen {
  position: absolute; inset: -40% -60%; pointer-events: none; opacity: 0;
  background: linear-gradient(105deg, transparent 38%, rgba(255, 255, 255, .22) 50%, transparent 62%);
  transform: translateX(-35%);
}
@media (hover: hover) and (pointer: fine) {
  .gc-card:hover .gc-sheen { opacity: 1; animation: gcSheen .75s var(--ease-out) }
}
@keyframes gcSheen { from { transform: translateX(-45%) } to { transform: translateX(45%) } }
@media (prefers-reduced-motion: reduce) { .gc-card:hover .gc-sheen { animation: none; opacity: 0 } }

/* ══ 5. 插画：统一描边色跟随文字，填充跟随强调色 ═══════════ */
.sk-art { display: block; color: var(--accent) }
.sk-art .a-line { fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; opacity: .9 }
.sk-art .a-fill { fill: currentColor; opacity: .16 }
.sk-art .a-dim { fill: none; stroke: currentColor; stroke-width: 1.2; stroke-linecap: round; opacity: .38 }
.sk-art .a-accent { fill: var(--c-sakura-400); opacity: .9 }

/* ══ 6. 微交互：按钮按压回弹 ═══════════════════════════════ */
.btn-pure, .pc-btn, .lk-btn, .gc-go, .ui-state-btn, .auth2-submit {
  transition: transform var(--dur-1) var(--ease-spring), filter var(--dur-1) var(--ease-out), box-shadow var(--dur-1) var(--ease-out);
}
.btn-pure:active, .pc-btn:active, .lk-btn:active, .ui-state-btn:active, .auth2-submit:active { transform: scale(.965) }
@media (prefers-reduced-motion: reduce) {
  .btn-pure:active, .pc-btn:active, .lk-btn:active, .ui-state-btn:active, .auth2-submit:active { transform: none }
  /* 同样要避开翻面卡：transform:none 会把背面的 rotateX(180deg) 也一起抹掉 */
  .card:not(.flip-face):hover { transform: none }
}

/* ══ 7. 入场动画：内容一条条浮上来，而不是整块砸出来 ══════════
   统一一个关键帧 + 一套延迟档，谁需要谁挂 class。
   动画只在「第一次出现」时播，重画不重播（各处用 JS 加完就摘）。   */
@keyframes skRise { from { opacity: 0; transform: translateY(10px) } to { opacity: 1; transform: none } }
@keyframes skFade { from { opacity: 0 } to { opacity: 1 } }

/* 登录 / 注册：左边品牌、右边表单各自逐条浮出 */
.auth2-shell.auth-in .auth2-brand > *,
.auth2-shell.auth-in .auth2-form > * { opacity: 0; animation: skRise .52s var(--ease-out) both }
.auth2-shell.auth-in .auth2-brand > *:nth-child(1) { animation-delay: .04s }
.auth2-shell.auth-in .auth2-brand > *:nth-child(2) { animation-delay: .10s }
.auth2-shell.auth-in .auth2-brand > *:nth-child(3) { animation-delay: .16s }
.auth2-shell.auth-in .auth2-brand > *:nth-child(4) { animation-delay: .22s }
.auth2-shell.auth-in .auth2-brand > *:nth-child(n+5) { animation-delay: .28s }
.auth2-shell.auth-in .auth2-form > *:nth-child(1) { animation-delay: .10s }
.auth2-shell.auth-in .auth2-form > *:nth-child(2) { animation-delay: .16s }
.auth2-shell.auth-in .auth2-form > *:nth-child(3) { animation-delay: .22s }
.auth2-shell.auth-in .auth2-form > *:nth-child(4) { animation-delay: .28s }
.auth2-shell.auth-in .auth2-form > *:nth-child(n+5) { animation-delay: .34s }
/* 表单内部的每一栏再排一次队：切换登录/注册时只重播这一段 */
.auth2-form.fields-in .auth2-field,
.auth2-form.fields-in .auth2-submit { opacity: 0; animation: skRise .42s var(--ease-out) both }
.auth2-form.fields-in .auth2-field:nth-of-type(1) { animation-delay: .04s }
.auth2-form.fields-in .auth2-field:nth-of-type(2) { animation-delay: .10s }
.auth2-form.fields-in .auth2-field:nth-of-type(3) { animation-delay: .16s }
.auth2-form.fields-in .auth2-field:nth-of-type(4) { animation-delay: .22s }
.auth2-form.fields-in .auth2-submit { animation-delay: .26s }

/* 个人中心换页：内容淡入上移，别硬切 */
#player-center-modal .pc-subpage:not([hidden]) > *,
#player-center-modal #pc-page-main:not([hidden]) > * { animation: skRise .34s var(--ease-out) both }
#player-center-modal .pc-subpage:not([hidden]) > *:nth-child(2) { animation-delay: .05s }
#player-center-modal .pc-subpage:not([hidden]) > *:nth-child(3) { animation-delay: .10s }
#player-center-modal .pc-subpage:not([hidden]) > *:nth-child(n+4) { animation-delay: .14s }

/* 通知中心条目、好友/私信列表：逐条淡入 */
#nc-body .nc-it { animation: skFade var(--dur-2) var(--ease-out) both }
#nc-body .nc-it:nth-child(2) { animation-delay: .03s }
#nc-body .nc-it:nth-child(3) { animation-delay: .06s }
#nc-body .nc-it:nth-child(4) { animation-delay: .09s }
#nc-body .nc-it:nth-child(n+5) { animation-delay: .12s }

/* 空态 / 错误态出现时也浮一下 */
.ui-state { animation: skRise .4s var(--ease-out) both }

/* ══ 8. 通用过渡：把各处散写的时长收敛到令牌上 ═══════════════
   只补「本来没有过渡」或者「时长离谱」的交互，不动已有的精细动画 */
a, button, .btn-pure, .pc-btn, .lk-btn, .gc-tag, .pc-rail-i, .auth2-tab,
.tz-card, .pc-set-row, .nc-it, .bell-item, .im-conv, .gc-card, .pc-lang-btn, .pc-seg button {
  transition-property: color, background-color, border-color, box-shadow, opacity, transform, filter;
  transition-duration: var(--dur-2);
  transition-timing-function: var(--ease-out);
}
/* 标签切换的下划线：位置和宽度都走同一条曲线 */
.auth2-tab::after, .pc-rail-i::before { transition: transform var(--dur-2) var(--ease-out), opacity var(--dur-2) var(--ease-out) }
/* 展开/收起类：高度用 grid-template-rows 过渡，比 max-height 硬猜准 */
.sk-collapse { display: grid; grid-template-rows: 0fr; transition: grid-template-rows var(--dur-3) var(--ease-out) }
.sk-collapse.open { grid-template-rows: 1fr }
.sk-collapse > * { overflow: hidden }

@media (prefers-reduced-motion: reduce) {
  .auth2-shell.auth-in .auth2-brand > *, .auth2-shell.auth-in .auth2-form > *,
  .auth2-form.fields-in .auth2-field, .auth2-form.fields-in .auth2-submit,
  #player-center-modal .pc-subpage:not([hidden]) > *, #player-center-modal #pc-page-main:not([hidden]) > *,
  #nc-body .nc-it, .ui-state { animation: none !important; opacity: 1 !important }
  .sk-collapse { transition: none }
}

/* ══ 8.5 页面底色兜底 ═══════════════════════════════════════
   body 的渐变只铺 body 这个盒子。一旦出现「内容比 body 高」「壁纸尺寸算错」
   「地址栏收起导致 dvh 抖动」这类情况，多出来的那条就会露出它下面的东西
   （天空层、浏览器默认白底），表现就是页面最底下多一条浅色带。
   给 html 一个同色底，任何缝隙都只会是同一个深色，不会再穿帮。 */
html { background: #04061a; }
body { background-color: var(--surface-0); }
/* 主题类挂在 body 上，选不到 html —— 用 :has() 反选，同时 core.js 也会把
   同名类同步到 html，两条路任意一条生效即可 */
html.theme-light, html:has(> body.theme-light) { background: #f4e8f0; }
@media (prefers-color-scheme: light) {
  html.theme-auto, html:has(> body.theme-auto) { background: #f4e8f0; }
}

/* ══ 9. 长列表跳过屏外渲染 ═════════════════════════════════
   content-visibility:auto 让浏览器不给屏幕外的条目做布局和绘制，
   滚动和首屏都会轻一截；contain-intrinsic-size 给一个占位高度，
   不写的话滚动条会随着滚动不停跳。
   只挑「条目多、结构重复、不靠 JS 量高度」的列表，聊天记录这种
   要靠 scrollHeight 自动滚到底的容器一律不碰。                 */
.gc-card { content-visibility: auto; contain-intrinsic-size: auto 240px }
.fo-post { content-visibility: auto; contain-intrinsic-size: auto 180px }
#pc-page-badges .pc-card { content-visibility: auto; contain-intrinsic-size: auto 320px }
.table-wrap tbody tr { content-visibility: auto; contain-intrinsic-size: auto 44px }

/* ══ 10. 自托管显示字体 ═════════════════════════════════════
   时钟数字现在用的是系统等宽字体（SF Mono / Consolas …），换台机器字形就变。
   想彻底解决：挑一款可变字体，只子集化数字和冒号（20–40KB），
   命名为 assets/fonts/sakura-digits.woff2 上传，然后去掉下面这段的注释。
   正文继续用系统中文字体，不引入几 MB 的中文字体包。
   现在保持注释状态，避免每次加载都多一个 404。

@font-face {
  font-family: 'Sakura Digits';
  src: url('fonts/sakura-digits.woff2') format('woff2');
  font-weight: 100 900;
  font-display: swap;
  unicode-range: U+0030-0039, U+003A, U+002E, U+002F, U+0020, U+002D;
}
*/
