/* ================= 本地字体（卡通：站酷快乐体 ZCOOL KuaiLe） ================= */
@font-face {
  font-family: 'ZCOOL KuaiLe';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/zcool-kuaile.woff2') format('woff2');
}

/* ================= 本地字体（印章：方正小篆体，仅子集化「一刻千金」四字） ================= */
@font-face {
  font-family: 'FXSeal';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/fxseal.woff2') format('woff2');
}

/* 数字用的「站酷快乐体·去零面」：同一个字体文件，但 unicode-range 把 U+0030（0）
   排除在外。站酷快乐体的 0 是手绘方零（画成一个小方框），在 22px 的数字位上
   三连排会被误认成「缺字豆腐块」（贡献次数 0 / 贡献价值 0 → □ □）。
   排除后 0 自动回落到栈里的下一款（系统无衬线，正常椭圆零），1-9 仍是原笔触；
   文件完全相同，浏览器按 URL 去重，不产生额外下载 */
@font-face {
  font-family: 'ZCOOL KuaiLe Num';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/zcool-kuaile.woff2') format('woff2');
  unicode-range: U+0-2F, U+31-10FFFF; /* 除 0 之外全都要 */
}

/* ================= 设计变量 · 浅色 ================= */
:root {
  --bg: #ffffff;
  --bg-soft: #f5f5f6;
  /* 页脚「地台」色：宠物挂件与页脚共用一块底色，页尾才是一整片收束的地面，
     而不是「白正文 + 灰页脚」两段拼起来。比 --bg-soft 暖半格——
     插画里的矮墙与石子都是米色系，冷灰底会把它们衬得发青 */
  --stage: #f8f7f4;
  --card: #ffffff;
  --ink: #16181d;
  --ink-soft: #6b7079;
  --line: #e7e8ea;
  --line-strong: #d7d9dd;
  --accent: #16181d;
  --on-accent: #ffffff;
  --gold: #16181d;
  --gold-deep: #2b2f36;
  --gold-soft: rgba(22, 24, 29, 0.06);
  --gold-bright: #eaa62c;            /* 扫光金色 */
  --gold-text: #a9700f;              /* 小字号金色文字：浅底上仍保得住对比度 */
  --gold-core: var(--gold-bright);   /* 扫光金芯：浅色下与金同色，保持原观感 */
  --pulse-ring: rgba(22, 24, 29, 0.20); /* 时钟圆点脉冲光环 */
  /* 开场蒙版：点阵 + 平涂底色与页面背景（body）逐像素一致——同点阵、同 var(--bg) 平涂，
     只多一团金色 Bloom。底色绝不能用渐变（旧版 bg→bg-soft）：页面本身是平涂，
     蒙版淡出到 0 时若有底色差，整屏色调会突变，与页面衔接处「闪一下」；
     平涂同底则全程只衰减金色一层，归零一刻即为页面本身，零色差 */
  --intro-veil:
    radial-gradient(var(--dot) 1px, transparent 1px),
    linear-gradient(336deg, var(--bg) 30%, rgba(234, 166, 44, 0.58) 86%, rgba(234, 166, 44, 0.94) 118%);
  --gold-grad: linear-gradient(135deg, #2a2e35, #101216);
  --seal: #16181d;
  --seal-grad: linear-gradient(135deg, #2a2e35, #101216);
  --dot: rgba(22, 24, 29, 0.05);     /* 背景点阵 */
  --overlay: rgba(255, 255, 255, 0.76); /* 贪吃蛇暂停/结束浮层底色 */
  --board-bg: #f0f1f2;                    /* 俄罗斯方块棋盘底：比面板白、比侧栏灰，一格就能看出是游戏区 */
  --warn: #c2482d;                       /* 表单校验提示（浅底可读） */
  --shadow-sm: 0 1px 3px rgba(20, 24, 29, 0.06), 0 1px 2px rgba(20, 24, 29, 0.04);
  --shadow: 0 12px 32px rgba(20, 24, 29, 0.08);
  --shadow-lg: 0 24px 54px rgba(20, 24, 29, 0.12);
  --serif: "ZCOOL KuaiLe", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", -apple-system, "Segoe UI", sans-serif;
  --seal-font: "FXSeal", "FZXiaoZhuanTi-S13T", serif; /* 印章专用：方正小篆 */
  /* 数字专用字体：与标题同款的站酷快乐体（ZCOOL KuaiLe）——全站唯一的漫画/卡通字体，
     数字与标题同一支笔触，气质统一；该字体文件标题本就在加载，数字零额外成本。
     注意：中文字体的半角数字是比例字（实测 32px 下「1」≈11.7px、「8」≈22.6px，
     相差近一倍）且 tabular-nums 无效，需要列对齐的场景（表格/分页）由 min-width
     等布局手段兜底；「1 窄 8 胖」的错落在卡通风格里反而成手写节奏，予以保留。
     栈首是「去零面」：字形与 ZCOOL 完全一致，只把 0 让给系统字（见上方 @font-face）。
     栈尾带回系统字体：ZCOOL 缺拉丁符号字形时（% × · 等）回退系统渲染 */
  --num: "ZCOOL KuaiLe Num", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", -apple-system, "Segoe UI", sans-serif;
  --seal-pad: 10px; /* 印章内留白：同时作用于字格 padding 与界格线位置 */
  --seal-radius: 18px; /* 印章外圆角：内框圆角由其派生，移动端整体缩小时保持一致比例 */
  --sans: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", -apple-system, "Segoe UI", "Inter", sans-serif;
}

/* ================= 设计变量 · 深色 ================= */
[data-theme="dark"] {
  --bg: #0e0f12;
  --bg-soft: #15171b;
  --stage: #15171b;
  --card: #171a1f;
  --ink: #f2f3f5;
  --ink-soft: #9aa0a8;
  --line: rgba(242, 243, 245, 0.10);
  --line-strong: rgba(242, 243, 245, 0.18);
  --accent: #f2f3f5;
  --on-accent: #16181d;
  --gold: #f2f3f5;
  --gold-deep: #d6d9de;
  --gold-soft: rgba(242, 243, 245, 0.07);
  --gold-bright: #ffc94d;            /* 扫光金色（深底更饱和，与白字拉开对比） */
  --gold-text: #ffc94d;              /* 深底小字号金色与亮金同值即可 */
  --gold-core: #fff3c4;              /* 扫光金芯（高亮暖芯，夜间如光束掠过） */
  --pulse-ring: rgba(242, 243, 245, 0.30); /* 深底脉冲光环（浅色光环不可见） */
  --intro-veil:
    radial-gradient(var(--dot) 1px, transparent 1px),
    linear-gradient(336deg, var(--bg) 30%, rgba(255, 201, 77, 0.46) 86%, rgba(255, 201, 77, 0.88) 118%); /* 开场蒙版：与页面同底 + 金 Bloom */
  --gold-grad: linear-gradient(135deg, #e8eaed, #aeb3bb);
  --seal: #f2f3f5;
  --seal-grad: linear-gradient(135deg, #e8eaed, #aeb3bb);
  --dot: rgba(255, 255, 255, 0.05);  /* 背景点阵 */
  --overlay: rgba(14, 15, 18, 0.76); /* 贪吃蛇暂停/结束浮层底色（深色） */
  --board-bg: #101216;                /* 俄罗斯方块棋盘底（深色）：比卡片再深一档 */
  --warn: #ff8a6b;                   /* 表单校验提示（深底可读） */
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.40);
  --shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
  --shadow-lg: 0 24px 54px rgba(0, 0, 0, 0.55);
}

/* ================= 基础 ================= */
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; scroll-padding-top: 24px; }
section[id] { scroll-margin-top: 24px; }
body {
  font-family: var(--sans);
  background:
    radial-gradient(var(--dot) 1px, transparent 1px),
    var(--bg);
  background-size: 22px 22px, auto;
  color: var(--ink);
  line-height: 1.75;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  transition: background-color .45s ease, color .45s ease;
}
::selection { background: var(--accent); color: var(--on-accent); }
.container { max-width: 1080px; margin: 0 auto; position: relative; z-index: 2; }
section { padding: 96px 24px; position: relative; transition: background-color .45s ease; }
#break { background: var(--bg-soft); }

/* ================= 顶部滚动进度条 ================= */
/* 用 transform: scaleX 驱动而非 width：滚动时每帧改 width 会触发布局，
   scaleX 走合成层，零布局成本 */
.progress {
  position: fixed;
  top: 0; left: 0;
  width: 100%;
  height: 3px;
  background: var(--gold-grad);
  z-index: 200;
  transform: scaleX(0);
  transform-origin: 0 50%;
}

/* ================= 主题切换按钮 ================= */
.theme-toggle {
  position: fixed;
  top: 18px; right: 20px;
  width: 42px; height: 42px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--card);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  display: grid;
  place-items: center;
  z-index: 210;
  transition: transform .25s, opacity .28s, box-shadow .25s, border-color .25s, background-color .45s;
  overflow: hidden;
}
.theme-toggle:hover { transform: translateY(-2px) scale(1.06); box-shadow: var(--shadow); border-color: var(--line-strong); }
/* 让路：贪吃蛇 HUD 滑到视口顶部时（JS 加 .tuck），主题钮先收上去——
   那个位置正叠着面板里的全屏钮；全屏游戏时同理（html.snake-fs-on 覆盖两者） */
.theme-toggle.tuck {
  opacity: 0;
  transform: translateY(-58px) scale(.82);
  pointer-events: none;
}
/* 开场期间先压住页面外壳（主题钮）：蒙版里的金色是半透明的，压不住它就会在
   开场时透出来一个淡淡的圆。用 html.js:not(.intro-done) 而不是给按钮挂类，
   是因为按钮在 intro-guard.js 之后才解析到——那会儿还查不到它。
   .intro-done 由 main.js 在开场收尾（或判定跳过）时加，摘掉后走
   .theme-toggle 自带的 opacity .28s 过渡淡入；无 JS 时 html 没有 .js，按钮照常显示。 */
html.js:not(.intro-done) .theme-toggle { opacity: 0; pointer-events: none; }
html.snake-fs-on .theme-toggle,
html.snake-fs-on .to-top {
  opacity: 0;
  pointer-events: none;
}
.theme-toggle svg {
  position: absolute;
  width: 19px; height: 19px;
  stroke: var(--ink);
  opacity: 0;
  transform: rotate(-70deg) scale(.6);
  transition: opacity .3s, transform .3s;
}
/* 三模式各自图标：浅色=太阳 / 深色=月亮 / 跟随系统=显示器。
   按 data-mode（用户选择的模式）切换，与解析后的 data-theme 解耦，
   避免「跟随系统」时图标随系统深浅乱跳 */
:root[data-mode="light"] .theme-toggle .icon-sun,
:root[data-mode="dark"] .theme-toggle .icon-moon,
:root[data-mode="system"] .theme-toggle .icon-monitor { opacity: 1; transform: none; }
.theme-toggle::after {
  content: attr(data-tip);
  position: absolute;
  right: calc(100% + 10px);
  top: 50%;
  transform: translateY(-50%);
  white-space: nowrap;
  font-size: 12px;
  color: var(--ink-soft);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 4px 10px;
  box-shadow: var(--shadow-sm);
  opacity: 0;
  pointer-events: none;
  transition: opacity .25s;
}
.theme-toggle:hover::after { opacity: 1; }

/* ================= 首屏 ================= */
.hero {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 100px 24px 72px;
  overflow: hidden;
  position: relative;
  text-align: center;
}
.hero::before {
  content: "";
  position: absolute;
  left: 50%; top: 38%;
  transform: translate(-50%, -50%);
  width: 760px; height: 760px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--gold-soft), transparent 62%);
  pointer-events: none;
}
.hero-inner { max-width: 760px; width: 100%; position: relative; z-index: 2; }
.seal {
  width: 132px; height: 132px;
  box-sizing: border-box;
  padding: var(--seal-pad); /* 与 ::after 内框（inset: pad-1px）对齐：字格收缩到框线内，防止字触碰/超出框线 */
  margin: 0 auto 28px;
  background: var(--seal-grad);
  color: var(--on-accent);
  border-radius: var(--seal-radius);
  display: grid;
  grid-template-columns: 1fr 1fr;
  place-items: center;
  font-family: var(--seal-font);
  font-weight: 700; /* 篆书为单字重字体：700 触发浏览器合成加粗，增加笔画厚重感 */
  font-size: 54px; /* 字面宽≈52% em（28px）、高≈89% em（48px）；56px 印格内上下各留 4px，不出框 */
  line-height: 1;
  transform: rotate(-4deg);
  box-shadow: 0 14px 34px rgba(20, 24, 29, 0.18);
  position: relative;
  will-change: transform; /* sealPop/float 全程 transform 动画，常驻合成层，漂浮更顺 */
  /* 三段式开场：飞行印章落位后（1.02s）正式印章线性淡入交棒 + 过冲回弹，再移交 float 漂浮 */
  animation: sealPop .34s cubic-bezier(.22,.61,.36,1) 1.02s both, float 6s ease-in-out 1.55s infinite;
  transition: background .45s ease, color .45s ease;
}
[data-theme="dark"] .seal { box-shadow: 0 14px 34px rgba(0, 0, 0, 0.50); }
/* 界格：汉印四字印标志性的十字分格线（与外框同色、细而淡，不抢字） */
.seal::before {
  content: "";
  position: absolute;
  inset: var(--seal-pad);
  background:
    linear-gradient(var(--on-accent), var(--on-accent)) center / 1px 100% no-repeat,
    linear-gradient(var(--on-accent), var(--on-accent)) center / 100% 1px no-repeat;
  opacity: .22;
  pointer-events: none;
}
/* 篆书字面重心在 baseline 上方约 35% em 处（行盒中心为 50%），下沉 3px 让四字视觉居中 */
.seal span { transform: translateY(3px); }
.seal::after {
  content: "";
  position: absolute;
  inset: calc(var(--seal-pad) - 1px); /* 内框恒定在界格外侧 1px：桌面 9px / 移动 7px，随 --seal-pad 自适应，不再错位 */
  border: 2px solid var(--on-accent);
  border-radius: calc(var(--seal-radius) - 8px);
  opacity: .9;
}
/* ================= LOGO 三段式开场（参考 1Panel 贡献者页） =================
   时间线（自 <html> 获得 .intro-go、字体就绪起算）：
   0.00s 印章从 opacity:0 / scale 1.58 淡入并轻轻落定（0.34s，蒙版仍在）→
   0.40s 蒙版开始淡出(0.5s)、印章先快速「蓄力」再收缩飞向正式位置(0.62s 两段缓动，
   落点由 JS FLIP 写入 --dx/--dy) → 0.90s 蒙版归零（只衰减金色 Bloom，底色与页面
   逐像素一致，归零即页面）→ 1.02s 飞行印章落位 → 1.02s 飞行印章线性淡出(0.12s)与
   正式印章线性淡入(0.12s)精确互补交棒（总墨量恒定，不闪）→ 1.10s 起其余内容依次登场。
   性能约束：全屏蒙版与飞行印章只动 transform/opacity（合成器直跑），
   不用 filter/全屏 scale/width 等每帧重绘属性。 */
.intro {
  position: fixed; inset: 0; z-index: 300; pointer-events: none;
  visibility: hidden; /* JS 注入 .on 后才显示：无 JS 时页面照常，不会被蒙版挡住 */
}
.intro.on { visibility: visible; }
.intro-veil {
  position: absolute; inset: 0;
  background: var(--intro-veil);
  background-size: 22px 22px, auto; /* 第一层点阵按页面同规格平铺，与正文背景完全一致 */
  /* 只做 opacity 淡出：全屏层一旦 transform 缩放，每帧都要重栅格整屏，必卡。
     时长 0.5s：0.40s 起淡、0.90s 归零——比印章飞行(0.62s)早一步收尾，
     保证落位(1.02s)与内容登场(1.10s)时屏幕已是干净页面，不带蒙版残色 */
  transition: opacity .5s ease;
  will-change: opacity; /* 一次性全屏淡出，常驻合成层避免 raster 抖动 */
}
/* 飞行印章：初始「不在场」，等字体就绪、时间线起跑才淡入。
   两条原因都必须这样做——
   ① 蒙版一显示它就跟着出现的话，第 0 帧就是一枚 1.5 倍的印章硬怼在屏幕中央，
      没有任何过渡，观感是「啪」地弹出来（用户反馈的「刚显示很生硬」）；
   ② 篆书 1.2KB 字体虽然预加载，但字体没解码完之前会先以 fallback 字形画一版，
      等 fxseal 换上来字就变一次形（印章字形闪烁）。把 opacity 压到 0，
      等 fonts.ready（JS 才加 .intro-go）再露，字形闪烁这一条从根上消失。
   scale 1.58 → 1.5 是「落印」：略微收一下，像盖下去时压平的那点回弹。 */
.intro-logo {
  position: absolute; left: 50%; top: 50%;
  display: grid; place-items: center;
  opacity: 0;
  transform: translate(-50%, -50%) scale(1.58);
  /* 入场用 transition 而不是 animation：下面 sealFly 要占用 animation 属性，
     两者同时挂在 transform 上会打架；transition 到 0.34s 结束正好交棒给
     sealFly 的 0% 关键帧（同为 scale 1.5），中间不会跳。 */
  transition: opacity .28s ease-out, transform .34s cubic-bezier(.16,.84,.3,1);
  /* 不用 filter: drop-shadow——缩放飞行中滤镜每帧重绘，必卡；
     飞行印章沿用 .seal 自身 box-shadow（随 transform 一起缩放，观感一致且零成本） */
  will-change: transform, opacity;
}
html.intro-go .intro-logo {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1.5);
}
/* 飞行印章复用 .seal 视觉：去掉自动外边距与自身动画（运动由 .intro-logo 驱动） */
.intro-logo .seal { margin: 0; animation: none; }
/* 蒙版淡出必须同时覆盖 moving 与 complete 两个状态（与下面 sealFly 同一类坑）：
   只挂 moving 时，complete 一到规则失效、opacity 会沿 transition 过渡回 1——
   金色蒙版在动画结尾重新盖住屏幕，直到 1.15s 被整体移除（此时透明度约 80% 被硬抽走）。
   这就是用户反馈的「动画结束时蒙版还出现颜色 + 与界面衔接闪一下」的确切根因 */
[data-intro="moving"] .intro-veil,
[data-intro="complete"] .intro-veil { opacity: 0; }
/* sealFly 必须同时覆盖 moving 与 complete 两个状态：若只挂在 moving 上，
   complete 一到达规则失效、动画被移除，fill(both) 随之失效，
   飞行印章会瞬间弹回基础样式（居中 1.5 倍）再淡出——用户看到的「闪烁」正是如此。
   两个状态都挂上，动画才能保持落点不动，只让 opacity 淡出交棒。
   这儿的 transition 只留 opacity .12s：交棒淡出要快，而 transform 已交给动画，
   不能再让入场那条 0.34s 的 transform 过渡插进来。 */
[data-intro="moving"] .intro-logo,
[data-intro="complete"] .intro-logo {
  animation: sealFly .62s both;
  transition: opacity .12s linear;
}
/* 交棒：只淡出飞行印章本身。注意不要让 .intro 父层也淡 opacity——
   父层淡出会再乘一次飞行印章的有效不透明度，与正式印章淡入叠加后
   同点总墨量先塌后升，即用户看到的「闪烁」。
   选择器带 html 前缀：上面 html.intro-go .intro-logo 把 opacity 锁在 1，
   这里必须同权重且写在后面才压得住。 */
html[data-intro="complete"] .intro-logo { opacity: 0; }
/* 聚能 → 收缩飞行：起点/终点旋转均为 0（与静止态一致，无跳变）。
   两段各自缓动，避免一条曲线跑完「蓄力+飞掠」导致的发飘：
   0→18% 快速蓄力，微涨至 1.56 倍并轻摆 3deg（峰值从 1.62 收到 1.56：
   缩放区间越小，文字在飞行途中被重栅格的次数越少，落位那一下越不容易「忽然变清」）；
   18%→100% ease-out 滑行，起步比纯 expo-out 柔半档，末段长而柔地减速落位 */
@keyframes sealFly {
  0%   { transform: translate(-50%, -50%) scale(1.5) rotate(0deg);
         animation-timing-function: cubic-bezier(.4, 0, .6, 1); }
  18%  { transform: translate(-50%, -50%) scale(1.56) rotate(3deg);
         animation-timing-function: cubic-bezier(.24, .86, .3, 1); }
  100% { transform: translate(calc(-50% + var(--dx, 0px)), calc(-50% + var(--dy, 0px))) scale(1) rotate(0deg); }
}
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  font-size: 13px;
  letter-spacing: 0.32em;
  color: var(--ink-soft);
  margin-bottom: 20px;
}
.eyebrow::before, .eyebrow::after {
  content: "";
  width: 26px; height: 1px;
  background: var(--line-strong);
}
.hero h1 {
  font-family: var(--serif);
  font-weight: 900;
  font-size: clamp(52px, 8vw, 92px);
  line-height: 1.1;
  letter-spacing: 0.02em;
}
.hero h1 .ki {
  font-size: 0.4em;
  font-style: italic;
  margin-left: 8px;
  vertical-align: 6px;
}
.hero-sub {
  font-family: var(--serif);
  font-size: 20px;
  color: var(--ink-soft);
  margin-top: 14px;
  letter-spacing: 0.06em;
}
.hero-en {
  font-size: 12px;
  letter-spacing: .14em;
  color: var(--ink-soft);
  max-width: 640px;
  margin: 12px auto 40px;
}
/* 首屏粒子画布（JS 生成，位于内容层之下） */
.hero-particles {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}
.clock {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin-top: 44px;
  font-family: var(--serif);
  font-size: 15px;
  color: var(--ink-soft);
  border: 1px solid var(--line);
  background: var(--card);
  padding: 8px 18px;
  border-radius: 999px;
  box-shadow: var(--shadow-sm);
  transition: background-color .45s ease, border-color .45s ease;
}
.clock .dot {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--accent);
  animation: pulse 2s ease-in-out infinite;
}
/* ================= 鎏金扫光：文字默认墨色，仅扫过处显金色 ================= */
/* 注意：background-size:300% 时 position 可用范围只有 -50%~150%，
   超出即背景整体移出文字区导致文字透明消失；10%~100% 可保证全程有背景覆盖 */
.gold-flow {
  background-image: linear-gradient(110deg,
    var(--ink) 0%, var(--ink) 25%,
    var(--gold-bright) 42%,
    var(--gold-core) 50%,
    var(--gold-bright) 58%,
    var(--ink) 75%, var(--ink) 100%);
  background-size: 300% 100%;
  background-position: 10% 0;
  background-repeat: no-repeat;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
  animation: goldSweep 3s linear infinite;
}
@keyframes goldSweep {
  0%   { background-position: 10% 0; }    /* 金带在右侧外，文字纯墨 */
  75%  { background-position: 100% 0; }   /* 金带扫过文字到左侧外 */
  100% { background-position: 100% 0; }   /* 停顿，保持墨色 */
}
.hero h1 .ki {
  background-image: linear-gradient(110deg,
    var(--ink) 0%, var(--ink) 25%,
    var(--gold-bright) 42%,
    var(--gold-core) 50%,
    var(--gold-bright) 58%,
    var(--ink) 75%, var(--ink) 100%);
  background-size: 300% 100%;
  background-position: 10% 0;
  background-repeat: no-repeat;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
  /* 延迟 1.62s 与 h1 显形落定（fadeUp 1.08s + 0.55s）对齐：文字站稳后再扫光 */
  animation: goldSweep 3s linear 1.62s infinite;
}
/* 不支持 background-clip:text 的浏览器：恢复实色，避免文字透明消失 */
@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
  .gold-flow, .hero h1 .ki {
    background: none;
    -webkit-text-fill-color: currentColor;
    color: inherit;
    animation: none;
  }
}

/* ================= 区块标题 ================= */
.section-head { text-align: center; margin-bottom: 56px; }
.section-head .en {
  font-size: 13px;
  letter-spacing: 0.34em;
  color: var(--ink-soft);
  text-transform: uppercase;
}
.section-head .zh {
  font-family: var(--serif);
  font-weight: 900;
  font-size: 40px;
  margin-top: 6px;
  letter-spacing: 0.04em;
}
.section-head .divider {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  margin-top: 18px;
}
.section-head .divider::before,
.section-head .divider::after {
  content: "";
  width: 60px; height: 1px;
  background: linear-gradient(90deg, transparent, var(--line-strong));
}
.section-head .divider::after {
  background: linear-gradient(90deg, var(--line-strong), transparent);
}
.section-head .divider i {
  width: 7px; height: 7px;
  background: var(--accent);
  transform: rotate(45deg);
}
/* 标题下的半句注解（贡献者墙用）：小一号、灰一档，别抢标题。
   内容是英文，字距比中文放宽一点才不挤（中文 .06em 就够，英文要 .1em） */
.section-note {
  margin-top: 18px;
  font-size: 13px;
  letter-spacing: .1em;
  color: var(--ink-soft);
}

/* ================= 休息一下 · 游戏翻页 =================
   两台游戏各占一页，横向滑动切换。两页高度不同，所以 deck 的高度由 JS 量当前页后写死，
   切换时 height 跟着过渡——不用把两页塞进同一格取最大值，那样矮的那页底下会空一块 */
.game-wrap { max-width: 800px; margin: 0 auto; }
.game-tabs {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 14px;
}
.game-arrow {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--ink-soft);
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: color .25s ease, border-color .25s ease, transform .25s ease, background-color .45s ease;
}
.game-arrow svg { width: 15px; height: 15px; }
.game-arrow:hover { color: var(--gold-text); border-color: var(--line-strong); }
.game-arrow:active { transform: scale(.92); }
.game-arrow:focus-visible { outline: 2px solid var(--gold-text); outline-offset: 2px; }

.game-tab-list {
  position: relative;
  flex: 1;
  display: grid;
  grid-template-columns: 1fr 1fr;
  padding: 4px;
  border-radius: 999px;
  background: var(--card);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-sm);
  transition: background-color .45s ease, border-color .45s ease;
}
/* 选中滑块：一枚金色药丸，靠 transform 滑过去（不改 width，避免每帧重排） */
.game-ink {
  position: absolute;
  top: 4px;
  left: 4px;
  bottom: 4px;
  width: calc(50% - 4px);
  border-radius: 999px;
  background: var(--seal-grad);
  box-shadow: 0 4px 12px rgba(20, 24, 29, .18);
  transition: transform .42s cubic-bezier(.3, 1.08, .38, 1), background .45s ease;
}
[data-theme="dark"] .game-ink { box-shadow: 0 4px 14px rgba(0, 0, 0, .45); }
.game-tabs[data-page="1"] .game-ink { transform: translateX(calc(100% + 0px)); }
.game-tab {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 8px 4px;
  border: none;
  background: none;
  cursor: pointer;
  font-family: var(--serif);
  font-weight: 700;
  font-size: 14px;
  letter-spacing: .06em;
  color: var(--ink-soft);
  border-radius: 999px;
  transition: color .3s ease;
}
.game-tab.is-on { color: var(--on-accent); }
.game-tab:focus-visible { outline: 2px solid var(--gold-text); outline-offset: -2px; }
.game-tab-ic { width: 17px; height: 17px; flex: 0 0 auto; }

/* 舞台：两页叠在同一位置，切换时一页滑出一页滑入 */
.game-deck {
  position: relative;
  transition: height .42s cubic-bezier(.4, 0, .2, 1);
  /* 非活动页停在 translateX(26px)（见下面 .game-slide），那 26px 虽然
     opacity 0 看不见，却会把整页的 scrollWidth 撑宽 6px——手机上左右一晃
     就是这条白边。clip 不生成滚动容器，只把滑出去的那页裁掉。
     不写 overflow-clip-margin：实测 margin 内的内容仍被算进 scrollWidth，
     加了等于没加。代价是面板外投影会被切掉左右两侧，手机上几乎看不出来。 */
  overflow: clip;
}
.game-slide {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  opacity: 0;
  visibility: hidden;
  /* --dir：1 = 往后翻（新页从右边来），-1 = 往回翻（从左来） */
  transform: translateX(calc(var(--dir, 1) * 26px));
  transition:
    opacity .34s ease,
    transform .44s cubic-bezier(.3, .92, .3, 1),
    visibility 0s .44s;
  /* 这里绝不能加 will-change: transform —— 它会让这一页成为 position:fixed 的
     包含块，贪吃蛇的伪全屏（.is-fs-fake）就铺不满视口了 */
}
.game-slide.is-on {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition:
    opacity .34s ease,
    transform .44s cubic-bezier(.3, .92, .3, 1),
    visibility 0s;
}
/* 旧页往反方向滑走（目标值由 JS 写在行内，见 game-switch.js） */
.game-slide.is-out { pointer-events: none; }

/* ================= 休息一下 · 贪吃蛇 ================= */
.snake-panel {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 20px;
  padding: 20px;
  box-shadow: var(--shadow);
  transition: background-color .45s ease, border-color .45s ease;
  /* 桌面：棋盘与排行榜并排——排行榜就在游戏界面里，不用往下滚 */
  display: grid;
  grid-template-columns: minmax(0, 1fr) 232px;
  grid-template-areas:
    "hud  hud"
    "main board";
}
/* 左列：棋盘（转向直接点/触棋盘，桌面与手机一致） */
.snake-main { grid-area: main; min-width: 0; }
.snake-hud {
  grid-area: hud;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  /* 窄屏（手机竖屏全屏）三项数字 + 全屏钮挤在一行：
     不给 min-width:0 的话 flex 项按 min-content 撑住，收不回来，
     右侧那颗「退出全屏」会被顶到视口外——手机上就再也退不出去了 */
  min-width: 0;
  /* 底下压一条细线：三个数才不至于像浮在棋盘上，整条 HUD 有了自己的分区 */
  padding-bottom: 13px;
  margin-bottom: 14px;
  border-bottom: 1px solid var(--line);
}
/* 数据条：标签小字在左、数字大字的横排，读起来就是「本局 12 · 纪录 28 · 等级 5」。
   中文习惯是「名目 + 数值」，横排最顺；两项之间一根细竖线分句 */
.snake-stats { display: flex; align-items: center; min-width: 0; }
.snake-stat {
  position: relative;
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}
.snake-stat + .snake-stat { margin-left: 26px; }
.snake-stat + .snake-stat::before {
  content: "";
  position: absolute;
  left: -13px;
  top: 50%;
  width: 1px;
  height: 22px;
  transform: translateY(-50%);
  background: var(--line-strong);
  opacity: .55;
}
.snake-stat i {
  font-style: normal;
  font-size: 12px;
  line-height: 1;
  letter-spacing: .1em;
  color: var(--ink-soft);
  white-space: nowrap;
  /* 卡通数字的字形重心偏下，标签纯居中会显得偏高，往下压一格才对得上 */
  transform: translateY(2px);
}
.snake-stat b {
  /* 数字用 --num（站酷快乐体）：与标题同一支笔触，全站数字一个语言。
     卡通体只有 400 一个字重，700 由浏览器合成加粗——圆体合成后笔画更饱满，
     和 12px 的规整标签摆一起靠字号/字重分层，不靠字体反差 */
  font-family: var(--num);
  font-weight: 700;
  font-size: 26px;
  line-height: 1.1;
  letter-spacing: -.01em;
  color: var(--ink);
  /* 位数一变（5 → 128）整项就会变宽，后面的项跟着挪；给够三位数的下限压住。
     站酷快乐体数字是比例字（1 窄 8 胖），tabular-nums 对它无效，
     防抖只能靠这个最小宽度 */
  min-width: 1.9em;
  font-variant-numeric: tabular-nums;
  display: inline-block;
  transform-origin: 50% 70%;
}
/* 本局得分是玩家当下唯一在动的数：镀金做主视觉；
   纪录用墨色压住（它是「墙上的旧成绩」，不该比现在更抢眼），
   等级最淡——它只是告诉你蛇有多快了 */
.snake-stat-score b { color: var(--gold-text); }
.snake-stat-lv b { color: var(--ink-soft); }
/* 破纪录 / 升级时脉冲一次（JS 加 .bump） */
.snake-stat b.bump { animation: statBump .62s cubic-bezier(.22,1.5,.36,1) both; }
@keyframes statBump {
  0% { transform: scale(1); }
  36% { transform: scale(1.34); color: var(--gold-bright); }
  100% { transform: scale(1); }
}
/* HUD 右侧：只留一颗全屏钮，右侧对齐到榜单边缘；操作说明交给棋盘浮层，HUD 保持干净 */
.snake-hud-right { display: flex; align-items: center; margin-left: auto; }
/* 全屏钮：细边框方钮，图标随状态在「展开 ↔ 收起」之间切换；
   尺寸与左侧 24px 数字同高一点点，悬停镀金——它是 HUD 里唯一的按钮，得立得住 */
.snake-fs {
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  padding: 0;
  color: var(--ink-soft);
  background: transparent;
  border: 1px solid var(--line-strong);
  border-radius: 12px;
  cursor: pointer;
  transition: color .2s ease, border-color .2s ease, background-color .45s ease, transform .2s ease;
}
.snake-fs:hover { color: var(--gold-bright); border-color: var(--gold-bright); background: var(--gold-soft); }
.snake-fs:active { transform: scale(.94); }
.snake-fs:focus-visible { outline: 2px solid var(--gold-bright); outline-offset: 2px; }
.snake-fs svg { width: 16px; height: 16px; }
.snake-fs .fs-compress { display: none; }
.snake-panel.is-fs .snake-fs .fs-expand { display: none; }
.snake-panel.is-fs .snake-fs .fs-compress { display: block; }
/* 全屏时按钮跟着放大一档，离远了也点得到 */
.snake-panel.is-fs .snake-fs { width: 40px; height: 40px; border-radius: 13px; }
.snake-panel.is-fs .snake-fs svg { width: 18px; height: 18px; }
.snake-btn {
  font-family: var(--serif);
  font-weight: 700;
  font-size: 14px;
  letter-spacing: .08em;
  color: var(--on-accent);
  background: var(--accent);
  border: none;
  border-radius: 999px;
  padding: 9px 22px;
  cursor: pointer;
  white-space: nowrap;
  transition: transform .2s ease, opacity .2s ease, background-color .45s ease, color .45s ease, border-color .2s ease;
}
.snake-btn:hover { transform: translateY(-1px); opacity: .88; }
.snake-btn:active { transform: scale(.96); }
.snake-btn-ghost {
  background: transparent;
  color: var(--ink);
  border: 1px solid var(--line-strong);
}
.snake-btn-ghost:hover { border-color: var(--ink-soft); }
/* 棋盘：正方形舞台，画布由 JS 按 DPR 放大后缩放绘制，边缘清晰 */
.snake-stage {
  position: relative;
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: 14px;
  overflow: hidden;
  background: var(--bg);
  border: 1px solid var(--line);
  cursor: pointer; /* 点一下就能转向：用默认手型，别用十字准星 */
  -webkit-user-select: none;
  user-select: none;
  touch-action: none; /* 触屏点棋盘操作蛇，不让页面跟着滚 */
  -webkit-tap-highlight-color: transparent;
  transition: background-color .45s ease, border-color .45s ease;
}
#snakeCanvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
/* 开始 / 暂停 / 结束浮层：半透明主题色，盖在棋盘上 */
.snake-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  background: var(--overlay);
  cursor: default; /* 浮层里的文字不显示手型，只有按钮是手型 */
  transition: opacity .28s ease;
}
.snake-overlay.hide { opacity: 0; pointer-events: none; }
.snake-overlay-title {
  font-family: var(--serif);
  font-weight: 900;
  font-size: 26px;
  color: var(--ink);
  letter-spacing: .06em;
}
.snake-overlay-text { font-size: 13px; color: var(--ink-soft); letter-spacing: .04em; }
.snake-overlay .snake-btn { margin-top: 8px; }
/* ===== 破纪录：奖杯弹入 + 鎏金标题 + 俏皮话 + 金箔雨，比普通结束热闹一档 ===== */
.snake-overlay-icon { display: none; width: 34px; height: 34px; color: var(--gold-bright); }
.snake-overlay.record {
  background:
    radial-gradient(circle at 50% 34%, rgba(234, 166, 44, 0.22), rgba(234, 166, 44, 0) 62%),
    var(--overlay);
}
.snake-overlay.record .snake-overlay-icon {
  display: block;
  animation: recIcon .72s cubic-bezier(.2, 1.6, .4, 1) both;
}
.snake-overlay.record .snake-overlay-title {
  background: linear-gradient(100deg, #d5901a, #f6c95c 44%, #bf7f10);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: recTitle .66s cubic-bezier(.22, 1.4, .36, 1) both;
}
.snake-overlay-quip {
  max-width: 82%;
  font-size: 11px;
  letter-spacing: .05em;
  line-height: 1.5;
  text-align: center;
  color: var(--gold-text);
}
.snake-overlay-quip:empty { display: none; }
.snake-overlay.record .snake-overlay-quip { animation: recQuip .6s .2s both; }
@keyframes recIcon {
  0% { opacity: 0; transform: translateY(10px) scale(.5) rotate(-14deg); }
  62% { opacity: 1; }
  100% { opacity: 1; transform: none; }
}
@keyframes recTitle {
  0% { opacity: 0; transform: scale(.72) translateY(8px); }
  100% { opacity: 1; transform: none; }
}
@keyframes recQuip {
  0% { opacity: 0; transform: translateY(7px); }
  100% { opacity: 1; transform: none; }
}
/* 金箔雨：盖在浮层之上，纯装饰不吃点击；碎片由 JS 生成并随机化参数 */
.snake-confetti { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 3; }
.snake-confetti span {
  position: absolute;
  top: -14px;
  display: block;
  background: var(--gold-bright);
  opacity: 0;
  will-change: transform, opacity;
  animation: confettiFall var(--dur, 1.5s) var(--delay, 0s) cubic-bezier(.32, .06, .62, 1) forwards;
}
@keyframes confettiFall {
  0% { opacity: 0; transform: translate3d(0, -14px, 0) rotate(0deg) scale(.7); }
  14% { opacity: 1; }
  100% { opacity: 0; transform: translate3d(var(--dx, 0px), var(--fall, 320px), 0) rotate(var(--rot, 360deg)) scale(1); }
}
.snake-tip {
  margin-top: 16px;
  text-align: center;
  font-size: 13px;
  color: var(--ink-soft);
  letter-spacing: .04em;
}

/* ================= 俄罗斯方块 =================
   10×20 的竖棋盘天生比贪吃蛇的正方形窄，硬塞进「棋盘 + 右栏」会两侧空出一大片。
   所以这一页改成三栏：左板（下一个 / 已消）、中板（棋盘 + 按键）、右板（键位表），
   HUD 仍横跨顶部，和贪吃蛇那一页保持同一张脸。

   三栏不再写死 128 / 1fr / 232：中列用 auto，宽度就是棋盘自己（高 ÷ 2）——
   棋盘 250px 宽时，1fr 那一列会撑到 400 多，中间一块窄棋盘左右各空一条，
   看着像三根互不相干的柱子。auto + justify-content: center 让三栏贴着棋盘站。 */
.snake-panel.ttr-panel {
  grid-template-columns: minmax(0, 140px) auto minmax(0, 250px);
  grid-template-areas:
    "hud   hud   hud"
    "sideL main  sideR"
    ".     pad   .";
  column-gap: 18px;
  row-gap: 0;
  justify-content: center;
  /* 面板顶上一道极淡的金光：800px 宽的大卡片纯平色会显得像块没做完的底板 */
  background:
    radial-gradient(620px 200px at 50% -6%, rgba(234, 166, 44, .07), rgba(234, 166, 44, 0) 72%),
    var(--card);
}
.ttr-side {
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: 0;
}
.ttr-side-l { grid-area: sideL; }
.ttr-side-l .ttr-card:first-child { flex: 2 1 0; }
.ttr-side-l .ttr-card:last-child { flex: 1 1 0; }
.ttr-side-r { grid-area: sideR; }
/* 左列容器：普通态 / 竖屏全屏用 contents 拆开（信息归左、按键归棋盘下 / 右）；
   横屏全屏时改 flex，整列站到棋盘左侧 */
.ttr-left { display: contents; }
/* 榜单侧板：和贪吃蛇那一页同一套组件，只是它原来固定 grid-area:board，这里要交回侧栏控制 */
.ttr-board { grid-area: auto; flex: 1 1 auto; height: 100%; }
.ttr-main { grid-area: main; min-width: 0; }

/* 侧板小卡：和棋盘同高的一列，所以卡片用 flex 均分，不留大块空白。
   底子是 --bg-soft（比面板浅一档），再压一道极淡的顶部金光——
   纯色圆角块摆在 800px 宽的面板里会显得「贴上去的」，两层渐变才像面板里长出来的 */
.ttr-card {
  flex: 1 1 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 8px;
  padding: 12px;
  text-align: center;
  border: 1px solid var(--line);
  border-radius: 16px;
  background:
    linear-gradient(180deg, rgba(234, 166, 44, .05), rgba(234, 166, 44, 0) 46%),
    var(--bg-soft);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .04);
  transition: background-color .45s ease, border-color .45s ease;
}
/* 距下一档的进度：0~10 行一条细线，等级往上跳时能看出「还差几行」。
   注意它是 flex 子项、跟着「已消 N 行」居中排，不要用绝对定位钉在卡片底边——
   卡片比内容高的时候，那样子进度条会孤零零浮在离数字 100px 远的地方 */
.ttr-lv-bar {
  flex: 0 0 auto;
  width: min(100%, 128px);
  height: 3px;
  margin-top: 2px;
  border-radius: 2px;
  background: var(--line);
  overflow: hidden;
}
.ttr-lv-bar i {
  display: block;
  width: 0;
  height: 100%;
  border-radius: 2px;
  background: linear-gradient(90deg, var(--gold-deep, #c98d20), var(--gold-bright));
  transition: width .45s cubic-bezier(.3, .9, .3, 1);
}
.ttr-card-t {
  font-size: 11px;
  line-height: 1;
  letter-spacing: .16em;
  color: var(--ink-soft);
  text-transform: uppercase;
  white-space: nowrap;
}
/* 下一个预览：画布铺满卡片剩余空间（JS 按容器尺寸放大绘制）。
   给它一块和主棋盘同色的底 —— 预览本来就是一块缩小的棋盘，
   方块色压在 --bg-soft 上会发灰，压在 --board-bg 上才对得上 */
.ttr-next {
  flex: 1 1 auto;
  min-height: 96px;
  max-height: 196px;
  width: 100%;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--board-bg);
  overflow: hidden;
}
.ttr-next canvas { display: block; width: 100%; height: 100%; }
.ttr-lines {
  flex: 0 0 auto;
  display: flex;
  align-items: baseline;
  gap: 5px;
  font-family: var(--sans);
  color: var(--ink);
}
.ttr-lines b {
  font-size: 26px;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.ttr-lines span { font-size: 12px; color: var(--ink-soft); }

.ttr-main {
  /* 棋盘高度是整个游戏区的标尺。56vh 是量出来的上限：
     900 高的窗口里面板总高 666，从游戏区顶部算下来正好一屏装完
     （再抬高 2vh 面板就 684，底部按键条要滚一下才看得见） */
  --stage-h: clamp(360px, 56vh, 620px);
  /* 中列是 auto：这里给一个确定宽度（棋盘 = 高 ÷ 2，+2px 是左右各 1px 边框），
     列宽量得准，三栏才真的贴着棋盘站 */
  width: calc(var(--stage-h) / 2 + 2px);
  justify-self: center;
}
.ttr-stage {
  position: relative;
  height: var(--stage-h);
  aspect-ratio: 1 / 2;
  margin: 0 auto;
  border-radius: 14px;
  overflow: hidden;
  background: var(--board-bg);
  border: 1px solid var(--line-strong);
  cursor: default;
  -webkit-user-select: none;
  user-select: none;
  touch-action: none;
  -webkit-tap-highlight-color: transparent;
  transition: background-color .45s ease, border-color .45s ease;
}
#ttrCanvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }

/* 棋盘下的操作键：桌面鼠标能点，手机就是手柄。
   宽度锁 100%：它所在的格子就是棋盘列（宽 = 棋盘宽），margin:auto 的缩.fit
   会把五颗键挤成一团；全屏各档各有自己的宽度覆盖 */
.ttr-pad {
  grid-area: pad;
  width: 100%;
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 7px;
  margin: 12px auto 0;
}
.ttr-key {
  display: grid;
  place-items: center;
  height: 44px;
  border-radius: 12px;
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--ink);
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: transform .12s ease, border-color .2s ease, color .2s ease, background-color .45s ease;
  -webkit-tap-highlight-color: transparent;
  touch-action: none;
}
.ttr-key svg { width: 19px; height: 19px; }
.ttr-key:hover { border-color: var(--line-strong); color: var(--gold-text); }
.ttr-key:active,
.ttr-key.is-hit { transform: scale(.92); border-color: var(--gold-bright); color: var(--gold-text); background: var(--gold-soft); }
/* 硬降是「这一手定生死」的键：给它一块金底，别和其余四颗长成一个样 */
.ttr-key-hard {
  color: var(--gold-text);
  border-color: rgba(234, 166, 44, .44);
  background:
    linear-gradient(180deg, rgba(234, 166, 44, .14), rgba(234, 166, 44, .04)),
    var(--card);
}
.ttr-key-hard:hover { border-color: var(--gold-bright); }
.ttr-key:focus-visible { outline: 2px solid var(--gold-text); outline-offset: 2px; }

/* 暂停钮：和贪吃蛇那颗全屏钮同一尺寸与位置，图标在「暂停 / 继续」间切换 */
.ttr-pause .ttr-ic-play { display: none; }
.ttr-panel.is-paused .ttr-pause .ttr-ic-pause { display: none; }
.ttr-panel.is-paused .ttr-pause .ttr-ic-play { display: block; }
.ttr-panel.is-paused #ttrPause { border-color: var(--gold-bright); color: var(--gold-text); }

/* ================= 本机历史记录 =================
   直接摆在游戏界面里：棋盘右侧同一面板、同一只边框，开局/对局时都能一眼看到。
   每局结束往这里记一行 —— 什么时候打的 / 打到几级（附成绩细节）/ 多少分。
   只留最近 10 局，纯 localStorage：不上传、不同步，换台设备就是空白。
   本机最好的那一局淡金打底，刚打完的那行带「新」签并闪一下。 */
.snake-board {
  grid-area: board;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0; /* 内容再多也不撑高所在行，高度随棋盘走 */
  padding-left: 18px;
  border-left: 1px solid var(--line);
}
.snake-board-head { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
.snake-board-icon { width: 15px; height: 15px; color: var(--gold-bright); flex-shrink: 0; }
.snake-board-title {
  font-family: var(--serif);
  font-weight: 900;
  font-size: 15px;
  letter-spacing: .04em;
}
.snake-board-sub { font-size: 11px; letter-spacing: .12em; color: var(--ink-soft); white-space: nowrap; }
/* 累计局数贴在标题行右端（记满了 10 局之后，这个数还会继续往上走） */
.snake-board-count {
  margin-left: auto;
  font-size: 10px;
  letter-spacing: .06em;
  color: var(--ink-soft);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  font-family: var(--num);
  flex-shrink: 0;
}
.snake-board-list {
  list-style: none;
  flex: 1 1 auto;
  min-height: 0;
  /* 10 条：正常高度下由条目均分，极端矮窗口才滚动兜底；滚动条不占位、不显形 */
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
  margin-top: 8px;
  display: flex;
  flex-direction: column;
  /* 不用 justify-content:center——那会让溢出内容向上滚不到；
     改成首尾 auto 外边距：有富余时整组居中，溢出时自动归零照常滚动 */
}
.snake-board-list::-webkit-scrollbar { width: 0; height: 0; }
.snake-board-list > :first-child { margin-top: auto; }
/* 「最早那一局」由 JS 打 is-last 标记，不用 :last-child——
   榜尾还有 display:none 的空位时，:last-child 会落到隐藏元素上，
   真实的末行反而拿不到这个 auto 外边距，居中就失效了 */
.snake-board-list > .is-last { margin-bottom: auto; }
/* 条目：时间 / 细节 / 分数（/ 新签）。
   条目按可用高度均分（10 条也铺得满），高度富裕时上限 46px 不至于松散 */
.snake-board-row {
  position: relative;
  flex: 1 1 0;
  min-height: 34px;
  max-height: 46px;
  display: grid;
  grid-template-columns: 42px minmax(0, 1fr) auto auto;
  grid-template-areas: "time meta score new";
  align-items: center;
  column-gap: 8px;
  padding: 7px;
  border-top: 1px solid var(--line);
  border-radius: 10px;
}
.snake-board-row:first-child { border-top: none; }
/* 本机最好的一局：淡金底衬出来，一眼看出哪次打得最好 */
.snake-board-row.is-best { background: linear-gradient(96deg, rgba(234, 166, 44, .13), rgba(234, 166, 44, 0) 74%); }
/* 刚记下的这一局：整行轻弹入 + 金色闪一下 */
.snake-board-row.is-new { animation: rowNew .6s cubic-bezier(.22, 1.2, .36, 1) both; }
.snake-board-row.is-new::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 10px;
  pointer-events: none;
  animation: rowFlash 1.3s ease-out both;
}
@keyframes rowNew {
  0% { opacity: 0; transform: translateX(12px); }
  100% { opacity: 1; transform: none; }
}
@keyframes rowFlash {
  0% { background: rgba(234, 166, 44, .34); }
  55% { background: rgba(234, 166, 44, .12); }
  100% { background: rgba(234, 166, 44, 0); }
}
.hist-time {
  grid-area: time;
  font-size: 11px;
  line-height: 1.2;
  letter-spacing: .02em;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
  font-family: var(--num);
  white-space: nowrap;
}
.hist-meta {
  grid-area: meta;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 11.5px;
  color: var(--ink-soft);
  font-family: var(--num);
  font-variant-numeric: tabular-nums;
}
.hist-score {
  grid-area: score;
  /* 数字专用字体：分数是榜单里最「数据」的元素，与标题同一支笔触（站酷快乐体）。
     比例字 1 窄 8 胖，min-width 给一位数兜底，右对齐保证列尾齐 */
  font-family: var(--num);
  font-weight: 800;
  font-size: 17px;
  min-width: 1.5em;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.snake-board-row.is-best .hist-score { color: var(--gold-bright); }
.sb-new {
  grid-area: new;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .08em;
  line-height: 1.5;
  color: var(--gold-bright);
  border: 1px solid currentColor;
  border-radius: 999px;
  padding: 0 6px;
  white-space: nowrap;
  animation: tagPop .5s .1s cubic-bezier(.2, 1.6, .4, 1) both;
}
@keyframes tagPop {
  0% { opacity: 0; transform: scale(.6) rotate(-8deg); }
  100% { opacity: 1; transform: none; }
}
.snake-board-empty {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  text-align: center;
  font-size: 12px;
  line-height: 1.7;
  color: var(--ink-soft);
  padding: 14px 0;
}
.sb-empty-art { width: 82px; height: 32px; color: var(--ink-soft); opacity: .75; }
/* 底部：一句「只存本机」把数据去向说清楚，右边是清空 */
.snake-board-foot {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--line);
}
.snake-board-note {
  font-size: 10px;
  letter-spacing: .06em;
  color: var(--ink-soft);
  opacity: .82;
  white-space: nowrap;
}
.snake-board-clear {
  margin-left: auto;
  font-family: var(--sans);
  font-size: 11px;
  letter-spacing: .06em;
  color: var(--ink-soft);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 3px 12px;
  cursor: pointer;
  white-space: nowrap;
  flex-shrink: 0;
  transition: color .2s, border-color .2s;
}
.snake-board-clear:hover { color: var(--ink); border-color: var(--line-strong); }
/* 待确认状态：第一次点只是「上膛」，按钮转警示色，再点一次才真清 */
.snake-board-clear.arm { color: var(--warn); border-color: var(--warn); }

/* ================= 全屏（.is-fs） =================
   原生全屏（桌面 / Android）与伪全屏兜底（iOS 不支持元素全屏）共用一套尺寸。
   全屏是「沉浸舞台」而不是放大的卡片：
   · 底色压掉页面那层暖光，只在棋盘那一侧透一团金雾，四周暗下去；
   · HUD 变成一条带分隔线的信息条，三项数字加竖线隔开，最高分镀金；
   · 棋盘按可用区域自动取最大正方形，细金边 + 深投影，像摆在台面上一块屏；
   · 排行榜立成一张卡片，10 条从上往下排（不再垂直居中——只有两三条时
     会被推到半空，上下空一大片）。 */
html.snake-fs-on,
html.snake-fs-on body { overflow: hidden; } /* 伪全屏时锁住页面滚动 */
.snake-panel.is-fs {
  width: 100vw;
  height: 100vh;
  /* 三级高度：--fs-vh（JS 量到的真实可视高度，解决 iOS 地址栏吃掉一截）
     → 100dvh（Safari 15.4+ / Chrome 108+）→ 100vh（其余） */
  height: var(--fs-vh, 100dvh);
  max-width: none;
  margin: 0;
  padding: 18px 24px;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  overflow: hidden;
  /* 左栏上限 = 棋盘能长到的最大边长：先扣掉右栏与栏距，
     保证 768~1000px 的窗口也不溢出；两栏整体再居中，
     棋盘与榜单贴在一起，宽屏时不会在中间留一大片空白。
     右栏随视口放宽（大屏榜单更舒展），窄屏仍守着 240 的下限 */
  --fs-board: clamp(240px, 21vw, 320px);
  --dpad-w: clamp(140px, 10vw, 176px);
  /* 中列宽度 = min(剩余宽度, 可用高度)。高度这里用 var(--fs-vh, 100vh) 而不是
     100dvh：dvh 在 Safari 15.4 / Chrome 108 以前完全不存在，整条声明会因此作废，
     连带 grid-template-columns 一起失效、全屏布局塌成一列。
     100vh 谁都认识，--fs-vh 由 JS 在全屏时写进来纠正 iOS 的地址栏偏差 */
  grid-template-columns: var(--dpad-w) minmax(0, min(calc(100% - var(--fs-board) - var(--dpad-w) - 48px), calc(var(--fs-vh, 100vh) - 96px))) var(--fs-board);
  grid-template-rows: auto minmax(0, 1fr) auto;
  grid-template-areas: "hud hud hud" "pad main board" "tip tip tip";
  column-gap: 24px;
  justify-content: center;
  background:
    radial-gradient(820px 620px at 34% 50%, rgba(234, 166, 44, .1), rgba(234, 166, 44, 0) 70%),
    radial-gradient(560px 560px at 88% 20%, rgba(234, 166, 44, .045), rgba(234, 166, 44, 0) 68%),
    var(--bg-soft);
}
/* 伪全屏：自己铺一层，盖住页面（原生全屏由浏览器置于顶层，不需要定位） */
.snake-panel.is-fs-fake {
  position: fixed;
  inset: 0;
  z-index: 90;
}
/* ---- 左侧十字方向键（只在全屏出现） ----
   手机全屏后没键盘，点棋盘又要先判断象限，转向容易慢半拍；
   十字键四颗各管一个方向，按哪走哪，拇指不用离开左下角。
   非全屏不显示：普通态棋盘旁边没有它的位置，点棋盘转向够用。 */
.snake-dpad {
  grid-area: pad;
  display: none;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(3, 1fr);
  grid-template-areas:
    ".    up    ."
    "left hub   right"
    ".    down  .";
  gap: 5px;
  align-self: center;
  justify-self: center;
  width: var(--dpad-w);
  height: var(--dpad-w);
  /* 触摸盘：禁掉滚动手势与长按选择，连点才跟手 */
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}
.snake-panel.is-fs .snake-dpad { display: grid; }
.dpad-key {
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--card);
  color: var(--ink);
  cursor: pointer;
  transition: background-color .12s ease, border-color .12s ease, transform .12s ease, color .12s ease;
}
.dpad-key svg { width: 46%; height: 46%; }
.dpad-up { grid-area: up; }
.dpad-down { grid-area: down; }
.dpad-left { grid-area: left; }
.dpad-right { grid-area: right; }
.dpad-hub {
  grid-area: hub;
  place-self: center;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--line-strong);
}
/* 按下态由 JS 加 .is-hit（pointerdown 立刻反馈，不等 :active 的 300ms） */
.dpad-key.is-hit,
.dpad-key:active {
  background: var(--gold-soft);
  border-color: var(--gold-text);
  color: var(--gold-text);
  transform: scale(.94);
}
[data-theme="dark"] .dpad-key { border-color: var(--line-strong); background: rgba(255, 255, 255, .05); }
[data-theme="dark"] .dpad-key.is-hit { background: rgba(234, 166, 44, .18); }

/* ---- HUD 信息条 ---- */
.snake-panel.is-fs .snake-hud {
  position: relative;
  margin-bottom: 12px;
  padding-bottom: 12px;
}
/* 数据条底缘压一道金线，左实右虚：把「仪表条」和棋盘分开，
   也不至于像硬边框（--line 那条太淡，全屏里几乎看不见） */
.snake-panel.is-fs .snake-hud::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 1px;
  background: linear-gradient(90deg, rgba(234, 166, 44, .55), rgba(234, 166, 44, .16) 40%, rgba(234, 166, 44, 0) 68%);
}
.snake-panel.is-fs .snake-stat { gap: 10px; }
.snake-panel.is-fs .snake-stat i { font-size: 15px; letter-spacing: .12em; }
.snake-panel.is-fs .snake-stat b { font-size: 38px; }
/* 全屏离得远：项间距和竖线都拉开一档 */
.snake-panel.is-fs .snake-stat + .snake-stat { margin-left: 40px; }
.snake-panel.is-fs .snake-stat + .snake-stat::before {
  left: -20px;
  height: 38px;
  opacity: .65;
}
.snake-panel.is-fs .snake-fs { width: 44px; height: 44px; border-radius: 14px; }
.snake-panel.is-fs .snake-fs svg { width: 19px; height: 19px; }
/* ---- 棋盘 ---- */
.snake-panel.is-fs .snake-main {
  display: grid;
  place-items: center;
  min-width: 0;
  min-height: 0;
  /* 让棋盘按这块可用区域取最大正方形：宽度取「容器宽 / 容器高」的较小值，
     行高随 HUD、padding 变化时不用再手算扣减像素 */
  container-type: size;
}
.snake-panel.is-fs .snake-stage {
  width: min(100%, calc(var(--fs-vh, 100vh) - 96px)); /* 兜底：不支持容器查询单位时 */
  width: min(100cqw, 100cqh);
  border-radius: 16px;
  border-color: rgba(234, 166, 44, .28);
  /* 投影跟着主题走：浅色下用 --shadow-lg 的柔灰，深色下自然更重 */
  box-shadow:
    var(--shadow-lg),
    0 0 0 6px rgba(234, 166, 44, .05);
}
/* ---- 排行榜：立成一张卡片 ---- */
.snake-panel.is-fs .snake-board {
  padding: 16px 14px 12px;
  border: 1px solid var(--line);
  border-radius: 18px;
  background:
    linear-gradient(180deg, rgba(234, 166, 44, .07), rgba(234, 166, 44, 0) 46%),
    var(--card);
}
.snake-panel.is-fs .snake-board-head {
  gap: 8px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--line);
}
.snake-panel.is-fs .snake-board-icon { width: 17px; height: 17px; }
.snake-panel.is-fs .snake-board-title { font-size: 17px; }
.snake-panel.is-fs .snake-board-sub { font-size: 10px; letter-spacing: .1em; }
.snake-panel.is-fs .snake-board-list { margin-top: 10px; }
/* 条目从标题下面往下排（不垂直居中：居中会让「第 1 名」离标题老远，
   像断成两截）；底部空出来的地方由空榜位填满 */
.snake-panel.is-fs .snake-board-list > :first-child { margin-top: 0; }
.snake-panel.is-fs .snake-board-list > .is-last { margin-bottom: 0; }
.snake-panel.is-fs .snake-board-list > .snake-board-row:last-child { margin-bottom: 0; }
/* 空榜位：还没人坐的名次只画一根浅灰渐隐线，**不印序号**——
   序号是「上榜了才有」的东西，提前摆出来会让空位看着像已被占用。
   第一列（序号列）留空，线从名字列起头，与真实条目左对齐 */
.snake-board-seat { display: none; }
.snake-panel.is-fs .snake-board-seat {
  display: grid;
  grid-template-columns: 54px minmax(0, 1fr);
  align-items: center;
  column-gap: 10px;
  flex: 1 1 0;
  min-height: 42px;
  max-height: clamp(44px, 6.2vh, 64px);
  padding: 7px 9px;
  border-radius: 12px;
  opacity: .46;
}
/* 名字那一栏画一条虚线（右端渐隐）示意「这个位次还空着」。
   用虚线而不是实线：实线看着像「某条记录的文字没渲染出来」 */
.snake-panel.is-fs .snake-board-seat::after {
  content: "";
  grid-column: 2; /* 跳过序号列，从名字那一栏开始画 */
  align-self: center;
  height: 0;
  border-bottom: 1px dashed var(--line-strong);
  -webkit-mask-image: linear-gradient(90deg, #000 8%, transparent 92%);
  mask-image: linear-gradient(90deg, #000 8%, transparent 92%);
}
/* 行高随屏高浮动：矮桌面不溢出（10 条一屏放得下），大屏也不至于松成一片 */
.snake-panel.is-fs .snake-board-row {
  flex: 1 1 0;
  min-height: 42px;
  max-height: clamp(44px, 6.2vh, 64px);
  grid-template-columns: 54px minmax(0, 1fr) auto auto;
  column-gap: 10px;
  padding: 7px 9px;
  border-radius: 12px;
}
.snake-panel.is-fs .snake-board-row + .snake-board-row { margin-top: 2px; }
.snake-panel.is-fs .hist-time { font-size: 13px; }
.snake-panel.is-fs .hist-meta { font-size: 15px; }
.snake-panel.is-fs .hist-score { font-size: 19px; }
.snake-panel.is-fs .snake-board-foot { margin-top: 12px; padding-top: 12px; gap: 10px; }
.snake-panel.is-fs .snake-board-note { font-size: 12px; }
.snake-panel.is-fs .snake-board-clear { font-size: 12px; padding: 6px 12px; }
/* 大屏全屏：浮层文字跟着放大，离远了也读得清 */
@media (min-width: 900px) {
  .snake-panel.is-fs .snake-overlay { gap: 12px; }
  .snake-panel.is-fs .snake-overlay-title { font-size: 40px; }
  .snake-panel.is-fs .snake-overlay-text { font-size: 15px; }
  .snake-panel.is-fs .snake-overlay-quip { font-size: 12px; }
  .snake-panel.is-fs .snake-overlay-icon { width: 44px; height: 44px; }
  .snake-panel.is-fs .snake-overlay .snake-btn { font-size: 16px; padding: 12px 32px; }
}
/* 矮屏（手机横屏 / 小窗口全屏）：HUD 收紧，别挤掉棋盘的高度 */
@media (max-height: 560px) {
  /* 俄罗斯方块不给全屏，横屏时按可用高度重算棋盘，别把按键顶出屏幕 */
  .ttr-main { --stage-h: clamp(200px, calc(var(--fs-vh, 100vh) - 122px), 360px); }
  .ttr-pad { margin-top: 7px; gap: 4px; }
  .ttr-key { height: 36px; border-radius: 9px; }
  .ttr-key svg { width: 15px; height: 15px; }
  .ttr-card { padding: 9px 10px; gap: 5px; }
  .ttr-card-t { font-size: 10px; }
  .ttr-lines b { font-size: 20px; }
  .ttr-panel .snake-hud { padding-bottom: 9px; margin-bottom: 9px; }
  .ttr-panel .snake-stat b { font-size: 20px; }
  .ttr-panel .snake-stat i { font-size: 10px; }

  .snake-panel.is-fs {
    padding: 10px 14px;
    column-gap: 18px;
    --dpad-w: 132px;
    /* 手机横屏（高 ≤ 560）宽度本来就紧：左侧方向键再占一列，
       568 宽的机器上棋盘只剩 148px。这一档把历史榜单收起来
       （全屏是来玩的，榜单在这里连 3 条都露不全），
       棋盘按可用高度吃掉整块宽度；右列留一条与方向键等宽的空白，
       整组才是居中的，HUD 也能横贯到右端（全屏钮回到右上角） */
    grid-template-columns: var(--dpad-w) minmax(0, min(calc(100% - var(--dpad-w) * 2 - 36px), calc(var(--fs-vh, 100vh) - 76px))) var(--dpad-w);
    grid-template-areas: "hud hud hud" "pad main ." "tip tip tip";
  }
  .snake-panel.is-fs .snake-board { display: none; }
  .snake-panel.is-fs .snake-hud { margin-bottom: 8px; padding-bottom: 8px; }
  .snake-panel.is-fs .snake-stat { gap: 6px; }
  .snake-panel.is-fs .snake-stat + .snake-stat { margin-left: 14px; }
  .snake-panel.is-fs .snake-stat + .snake-stat::before { left: -7px; height: 22px; }
  .snake-panel.is-fs .snake-stat i { font-size: 10px; letter-spacing: .06em; }
  .snake-panel.is-fs .snake-stat b { font-size: 20px; }
  .snake-panel.is-fs .snake-fs { width: 34px; height: 34px; border-radius: 11px; }
  .snake-panel.is-fs .snake-fs svg { width: 15px; height: 15px; }
  .snake-panel.is-fs .snake-stage { border-radius: 12px; }
  .snake-panel.is-fs .snake-board { padding: 10px 10px 8px; border-radius: 14px; }
  .snake-panel.is-fs .snake-board-head { gap: 6px; padding-bottom: 8px; }
  .snake-panel.is-fs .snake-board-icon { width: 14px; height: 14px; }
  .snake-panel.is-fs .snake-board-title { font-size: 14px; }
  .snake-panel.is-fs .snake-board-list { margin-top: 6px; }
  /* 矮屏放不下 10 条：露出细滚动条，别让人以为榜单就这几行 */
  .snake-panel.is-fs .snake-board-list { scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; }
  .snake-panel.is-fs .snake-board-list::-webkit-scrollbar { width: 4px; height: 0; }
  .snake-panel.is-fs .snake-board-list::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 2px; }
  .snake-panel.is-fs .snake-board-list::-webkit-scrollbar-track { background: transparent; }
  .snake-panel.is-fs .snake-board-row {
    min-height: 32px;
    max-height: 40px;
    grid-template-columns: 44px minmax(0, 1fr) auto auto;
    column-gap: 7px;
    padding: 4px 6px;
    border-radius: 10px;
  }
  .snake-panel.is-fs .snake-board-seat {
    min-height: 32px;
    max-height: 40px;
    grid-template-columns: 44px minmax(0, 1fr);
    column-gap: 7px;
    padding: 4px 6px;
    border-radius: 10px;
  }
  .snake-panel.is-fs .hist-time { font-size: 11px; }
  .snake-panel.is-fs .hist-meta { font-size: 13px; }
  .snake-panel.is-fs .hist-score { font-size: 16px; }
  .snake-panel.is-fs .snake-board-foot { margin-top: 8px; padding-top: 8px; gap: 8px; }
  .snake-panel.is-fs .snake-board-note { font-size: 11px; }
  .snake-panel.is-fs .snake-board-clear { font-size: 11px; padding: 4px 10px; }
}
/* 横屏、但横向够宽（≥700px）：历史记录回右列。
   上面那一条矮屏规则为了保住棋盘，把榜单整块 display:none 了——
   结果手机横屏全屏之后「记录」这一栏直接消失，很多人以为是成绩没存上。
   其实 844×390 的机器上棋盘只吃 314px 宽，右列原本就空着一条与方向键等宽的
   镜像空白（那是为了整组居中留的），拿来放榜单正好，棋盘一像素都不缩。
   700px 以下（568×320 那类）是真的放不下，仍旧只留 HUD 上的「纪录」数字。 */
@media (max-height: 560px) and (min-width: 700px) {
  .snake-panel.is-fs {
    --board-w: clamp(184px, 25vw, 256px);
    /* 中列仍取「剩余宽度」与「可用高度」的较小值：宽度富裕时棋盘按高度撑满，
       宽度紧张时它自动收窄，榜单不会把它挤出屏幕 */
    grid-template-columns: var(--dpad-w) minmax(0, min(calc(100% - var(--dpad-w) - var(--board-w) - 36px), calc(var(--fs-vh, 100vh) - 76px))) var(--board-w);
    grid-template-areas: "hud hud hud" "pad main board" "tip tip tip";
  }
  .snake-panel.is-fs .snake-board { display: flex; }
  /* 这一列只有 180~256 宽，时间 + 等级 + 分数三栏挤不下。
     丢掉的是「等级」不是时间：等级是打的时候就知道的，
     时间才是回头翻榜单时唯一能定位「那是哪一局」的东西。
     丢掉的等级仍在 title 里（悬停可看完整时刻 · 等级 · 长度） */
  .snake-panel.is-fs .snake-board .hist-meta { display: none; }
  .snake-panel.is-fs .snake-board-row {
    grid-template-columns: auto minmax(0, 1fr) auto auto;
    grid-template-areas: "time . score new";
  }
  .snake-panel.is-fs .snake-board-seat { grid-template-columns: auto minmax(0, 1fr); }
  /* 十条塞不进这一列，必然是滚动的。滚动条只有 4px，裁切处会像「半条记录」——
     底部 18px 淡出，一眼就知道下面还有（内容不满时那里本来也是空白，淡了看不出来） */
  .snake-panel.is-fs .snake-board-list {
    -webkit-mask-image: linear-gradient(180deg, #000 calc(100% - 18px), transparent 100%);
    mask-image: linear-gradient(180deg, #000 calc(100% - 18px), transparent 100%);
  }
}
/* 竖屏提示：只在「全屏 + 竖屏 + 触屏」时露出（浏览器不给自动横屏时的兜底） */
.snake-fs-tip {
  display: none;
  grid-area: tip;
  margin: 8px 0 0;
  text-align: center;
  font-size: 11px;
  letter-spacing: .06em;
  color: var(--ink-soft);
}
@media (orientation: portrait) and (pointer: coarse) {
  .snake-panel.is-fs .snake-fs-tip { display: block; }
}
/* 窄屏竖屏：全屏也回到上下堆叠，避免两栏把棋盘挤成一小块。
   只限竖屏——横屏手机（如 667×375）宽度不到 768，但必须留在两栏布局，
   否则棋盘会按宽度撑成 600 多像素，高度完全放不下 */
@media (max-width: 768px) and (orientation: portrait) {
  .snake-panel.is-fs {
    padding: 12px;
    /* 竖屏横向只有 320~430px：十字键摆成 3 列会白吃掉 118px，
       390 屏的棋盘从 364 掉到 222。竖屏改成单列四键（宽一格的 54px），
       位置仍在棋盘左侧，棋盘能保住 300 上下 */
    --dpad-w: clamp(46px, 13vw, 58px);
    grid-template-columns: var(--dpad-w) minmax(0, 1fr);
    /* 竖屏：HUD /（方向键 + 棋盘）/ 榜单 / 提示 从上往下排。
       行高用 max-content——用 auto 的话内容一超出面板，grid 会压缩各行，
       棋盘和榜单卡片直接叠在一起；max-content 让面板滚动而不是挤压 */
    grid-template-rows: max-content max-content max-content max-content;
    grid-template-areas: "hud hud" "pad main" "board board" "tip tip";
    column-gap: 12px;
    /* 内容比屏幕矮时整组居中（别贴着顶、下面空一大块）；
       放不下时 safe 让它退化成贴顶，滚动才滚得到头 */
    align-content: safe center;
    overflow-y: auto;
  }
  /* 竖屏方向键：一格一个往下排（上 / 左 / 右 / 下），与十字键的方位一致 */
  .snake-panel.is-fs .snake-dpad {
    grid-template-columns: 1fr;
    grid-template-rows: repeat(4, auto);
    grid-template-areas: "up" "left" "right" "down";
    width: var(--dpad-w);
    height: auto;
    gap: 6px;
  }
  .snake-panel.is-fs .dpad-key { aspect-ratio: 1; border-radius: 12px; }
  .snake-panel.is-fs .dpad-hub { display: none; }
  /* 竖屏单列时行高由内容决定，不能用容器查询——container-size 会把
     .snake-main 的高度压成 0，棋盘直接消失。这里退回按宽度铺满 */
  .snake-panel.is-fs .snake-main { place-items: start center; container-type: normal; }
  .snake-panel.is-fs .snake-stage {
    width: 100%;
    border-radius: 14px;
  }
  /* 竖屏榜单同样做卡片，与棋盘上下呼应 */
  .snake-panel.is-fs .snake-board {
    padding: 12px;
    border: 1px solid var(--line);
    border-radius: 16px;
    margin-top: 12px;
  }
  .snake-panel.is-fs .snake-board-head { padding-bottom: 10px; }
  /* 竖屏高度金贵：空榜位不画，榜单只长到「有多少条就多高」 */
  .snake-panel.is-fs .snake-board-seat { display: none; }
  /* 竖屏全屏的 HUD 跟着宽度缩放：
     全屏那套字号是按「横屏 / 桌面」放大过一档的，竖屏只有 320~430px 宽，
     三项数字（38px 字号、每项前置 40px 间距）合计 427px，
     直接把「等级」和右侧全屏钮顶出屏幕——手机上看着就是进去以后数字缺一半、
     右上角的退出按钮彻底消失。这里用 clamp 按视口宽度落到合适一档，
     窄屏压回普通态、iPad 竖屏也不会剩一大片空。 */
  .snake-panel.is-fs .snake-stats { flex: 1 1 auto; }
  .snake-panel.is-fs .snake-stat { gap: 5px; }
  .snake-panel.is-fs .snake-stat i { font-size: clamp(11px, 2.7vw, 15px); letter-spacing: .06em; }
  .snake-panel.is-fs .snake-stat b { font-size: clamp(22px, 6vw, 38px); min-width: 1.5em; }
  .snake-panel.is-fs .snake-stat + .snake-stat { margin-left: clamp(12px, 3.6vw, 40px); }
  .snake-panel.is-fs .snake-stat + .snake-stat::before {
    left: calc(-1 * clamp(6px, 1.8vw, 20px));
    height: 20px;
  }
  /* 窄屏竖屏全屏：按钮回到与普通布局一致的大小 */
  .snake-panel.is-fs .snake-fs { width: 36px; height: 36px; border-radius: 12px; }
  .snake-panel.is-fs .snake-fs svg { width: 16px; height: 16px; }
}
/* 浅色主题下这两处要加一档：白底会吃掉金色描边和淡灰空位 */
:root[data-theme="light"] .snake-panel.is-fs .snake-stage { border-color: rgba(201, 141, 32, .46); }
:root[data-theme="light"] .snake-panel.is-fs .snake-board-seat { opacity: .55; }

/* 桌面全屏：HUD 再放大一档，远看也抓得住数字 */
/* 大屏且高够：HUD 再大一号（矮窗口交给上面的 max-height:560 收紧规则） */
@media (min-width: 900px) and (min-height: 600px) {
  .snake-panel.is-fs .snake-stat b { font-size: 44px; }
}

/* ================= 俄罗斯方块 · 全屏 =================
   与贪吃蛇共用同一份实现（assets/js/game-fs.js）：按钮图标、进场 / 退场动画、
   原生全屏降级、锁横屏、Esc 退出、--fs-vh 全都一样。这里只回答「全屏时怎么摆」。

   难点在比例：10×20 的棋盘宽永远是高的一半，而屏幕是横的。
   所以中列用 auto——宽度直接由 --stage-h 算出——左右两列贴着它站、整组居中。
   若沿用普通态的 1fr 中列，棋盘只有 260px 宽却坐在 400px 的格子里，
   两边各留一条空白，看着像三根互不相干的柱子。

        HUD 横贯三栏
   [下一个 / 已消] [棋盘] [按键]

   按键放右手边、按 3+2 摆两行（左 / 转 / 右 ＋ 软降 / 硬降）：
   拇指原地就够得着。五颗竖排到 320px 高的话，单手要一路滑下来才按得到「硬降」。
   历史榜单全屏收起——全屏是来玩的，这一栏在矮屏里一条都露不全。 */
.snake-panel.ttr-panel.is-fs {
  --ttr-side-w: clamp(148px, 13vw, 210px);
  --ttr-keys-w: clamp(158px, 13vw, 204px);
  grid-template-columns: var(--ttr-side-w) auto var(--ttr-keys-w);
  grid-template-areas:
    "hud   hud   hud"
    "sideL main  keys"
    "tip   tip   tip";
  column-gap: 20px;
}
/* display: contents 让 .ttr-main 不生成盒子：棋盘与按键直接成为面板的 grid 项，
   一个进中列、一个进右列。CSS 变量在 contents 元素上照常继承，--stage-h 不会丢 */
.snake-panel.ttr-panel.is-fs .ttr-main {
  display: contents;
  --stage-h: calc(var(--fs-vh, 100vh) - 112px);
}
/* 棋盘：宽高都由 --stage-h 定死（宽 = 高 ÷ 2），不再让 aspect-ratio 反推 */
.snake-panel.ttr-panel.is-fs .ttr-stage {
  grid-area: main;
  align-self: center;
  justify-self: center;
  width: calc(var(--stage-h) / 2);
  height: var(--stage-h);
  border-radius: 16px;
  border-color: rgba(234, 166, 44, .28);
  /* 与贪吃蛇全屏棋盘同一套投影 + 一圈淡金晕 */
  box-shadow: var(--shadow-lg), 0 0 0 6px rgba(234, 166, 44, .05);
  cursor: default;
}
/* 侧板只剩「下一个 + 已消」这一列 */
.snake-panel.ttr-panel.is-fs .ttr-side-r { display: none; }
.snake-panel.ttr-panel.is-fs .ttr-side-l { grid-area: sideL; }
.snake-panel.ttr-panel.is-fs .ttr-card { padding: 14px; border-radius: 18px; }
.snake-panel.ttr-panel.is-fs .ttr-card-t { font-size: 13px; letter-spacing: .18em; }
.snake-panel.ttr-panel.is-fs .ttr-next {
  min-height: 110px;
  /* 全屏没有「一屏装完」的压力，预览区跟着棋盘长：
     4 格方块画到 ~180px，隔一米也认得出是 L 还是 J */
  max-height: min(300px, calc(var(--stage-h) * .58));
  border-radius: 14px;
}
.snake-panel.ttr-panel.is-fs .ttr-lines b { font-size: 32px; }
.snake-panel.ttr-panel.is-fs .ttr-lv-bar { width: 100%; height: 4px; margin-top: 4px; }
/* 按键：右手边 3+2 两行 */
.snake-panel.ttr-panel.is-fs .ttr-pad {
  grid-area: keys;
  align-self: center;
  width: 100%;
  max-width: none;
  height: auto;
  margin: 0;
  gap: 10px;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-template-rows: repeat(2, minmax(0, 1fr));
  grid-template-areas:
    "left  rot   right"
    "soft  hard  hard";
}
.snake-panel.ttr-panel.is-fs .ttr-key {
  height: auto;
  min-height: clamp(54px, 7.4vh, 82px);
  border-radius: 15px;
}
.snake-panel.ttr-panel.is-fs .ttr-key svg { width: 26px; height: 26px; }
.snake-panel.ttr-panel.is-fs .ttr-key[data-act="left"] { grid-area: left; }
.snake-panel.ttr-panel.is-fs .ttr-key[data-act="rotate"] { grid-area: rot; }
.snake-panel.ttr-panel.is-fs .ttr-key[data-act="right"] { grid-area: right; }
.snake-panel.ttr-panel.is-fs .ttr-key[data-act="soft"] { grid-area: soft; }
.snake-panel.ttr-panel.is-fs .ttr-key[data-act="hard"] { grid-area: hard; }

/* 手机横屏（高 ≤ 560）：一切为棋盘让路，HUD 与侧卡各收一档 */
@media (max-height: 560px) {
  .snake-panel.ttr-panel.is-fs {
    --ttr-side-w: 128px;
    --ttr-keys-w: 150px;
    column-gap: 14px;
  }
  .snake-panel.ttr-panel.is-fs .ttr-main { --stage-h: calc(var(--fs-vh, 100vh) - 88px); }
  .snake-panel.ttr-panel.is-fs .ttr-card { padding: 9px; gap: 4px; border-radius: 14px; }
  .snake-panel.ttr-panel.is-fs .ttr-card-t { font-size: 10px; letter-spacing: .1em; }
  .snake-panel.ttr-panel.is-fs .ttr-lines b { font-size: 22px; }
  .snake-panel.ttr-panel.is-fs .ttr-lv-bar { height: 3px; margin-top: 2px; }
  .snake-panel.ttr-panel.is-fs .ttr-next { min-height: 56px; max-height: 110px; }
  .snake-panel.ttr-panel.is-fs .ttr-pad { gap: 7px; }
  .snake-panel.ttr-panel.is-fs .ttr-key { min-height: 44px; border-radius: 11px; }
  .snake-panel.ttr-panel.is-fs .ttr-key svg { width: 20px; height: 20px; }
}
/* 横屏且够宽：左列（信息 + 操作键）整列站到棋盘左侧，历史记录占右侧，
   中间棋盘。把操作键从原来的「最右独立列」并进左列，三栏变成
   「信息 + 控制 | 游戏 | 历史」。
   操作键并进左列后，左列不再是「信息(128) + 独立控制列(150)」两份宽度，
   所以阈值可以从 760 降到 640；640 以下（如 568 老机横屏）左列 + 历史会把棋盘挤到 40% 以下，
   回落到「信息 | 游戏 | 控制」且不显示历史。 */
@media (max-height: 560px) and (min-width: 640px) {
  /* 左列整体变 flex 容器：信息卡在上、操作键在下，居中对齐棋盘高度 */
  .ttr-left {
    display: flex;
    flex-direction: column;
    gap: 10px;
    grid-area: sideL;
    min-width: 0;
    align-self: center;
  }
  .snake-panel.ttr-panel.is-fs {
    --ttr-side-w: clamp(188px, 22vw, 256px);
    --ttr-hist-w: clamp(156px, calc(100vw - 700px), 232px);
    grid-template-columns: var(--ttr-side-w) auto var(--ttr-hist-w);
    grid-template-areas:
      "hud   hud   hud"
      "sideL main  hist"
      "tip   tip   tip";
    column-gap: 16px;
  }
  .snake-panel.ttr-panel.is-fs .ttr-main { --stage-h: calc(var(--fs-vh, 100vh) - 92px); }
  /* 信息卡横排一条（与窄屏同一套），高度锁 84px：不锁的话列高被按键挤剩 48px，
     预览的 min-height 又会把卡片撑破、压到「已消」上 */
  .snake-panel.ttr-panel.is-fs .ttr-side-l { flex-direction: row; gap: 8px; min-height: 84px; }
  .snake-panel.ttr-panel.is-fs .ttr-card { padding: 9px; gap: 4px; border-radius: 14px; }
  .snake-panel.ttr-panel.is-fs .ttr-card-t { font-size: 10px; letter-spacing: .1em; }
  .snake-panel.ttr-panel.is-fs .ttr-lines b { font-size: 22px; }
  .snake-panel.ttr-panel.is-fs .ttr-lv-bar { height: 3px; margin-top: 2px; }
  .snake-panel.ttr-panel.is-fs .ttr-next { min-height: 0; max-height: none; flex: 1 1 auto; }
  /* 操作键收进左列底部：3 列 2 行（左 / 转 / 右 ＋ 软降 / 硬降）。
     上 = 旋转，下 = 软降，左 / 右移动，金底那颗是直接落底 */
  .snake-panel.ttr-panel.is-fs .ttr-pad {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-template-rows: repeat(2, minmax(0, 1fr));
    grid-template-areas:
      "left  rot   right"
      "soft  hard  hard";
    align-self: auto;
    width: 100%;
    max-width: none;
    height: auto;
    margin: 0;
    gap: 8px;
  }
  .snake-panel.ttr-panel.is-fs .ttr-key {
    height: auto;
    min-height: clamp(46px, 8.2vh, 66px);
    border-radius: 13px;
  }
  .snake-panel.ttr-panel.is-fs .ttr-key svg { width: 22px; height: 22px; }
  .snake-panel.ttr-panel.is-fs .ttr-key[data-act="left"] { grid-area: left; }
  .snake-panel.ttr-panel.is-fs .ttr-key[data-act="rotate"] { grid-area: rot; }
  .snake-panel.ttr-panel.is-fs .ttr-key[data-act="right"] { grid-area: right; }
  .snake-panel.ttr-panel.is-fs .ttr-key[data-act="soft"] { grid-area: soft; }
  .snake-panel.ttr-panel.is-fs .ttr-key[data-act="hard"] { grid-area: hard; }
  /* 历史记录占右侧：必须 min-height:0，否则网格行被列表内容撑高、列表不滚动而溢出视口 */
  .snake-panel.ttr-panel.is-fs .ttr-side-r { display: flex; grid-area: hist; min-height: 0; overflow: hidden; }
  .snake-panel.ttr-panel.is-fs .ttr-side-r .ttr-board { height: 100%; min-height: 0; }
  /* 窄列：只留「时间 + 分数」，等级 / 行数收进 title（与贪吃蛇同一套取舍） */
  .snake-panel.ttr-panel.is-fs .hist-meta { display: none; }
  .snake-panel.ttr-panel.is-fs .ttr-board .snake-board-row {
    grid-template-columns: auto minmax(0, 1fr) auto auto;
    grid-template-areas: "time . score new";
  }
  .snake-panel.ttr-panel.is-fs .ttr-board .snake-board-seat {
    grid-template-columns: auto minmax(0, 1fr);
  }
  .snake-panel.ttr-panel.is-fs .ttr-board .snake-board-list {
    -webkit-mask-image: linear-gradient(180deg, #000 calc(100% - 18px), transparent 100%);
    mask-image: linear-gradient(180deg, #000 calc(100% - 18px), transparent 100%);
  }
}

/* 竖屏全屏：单列堆叠。10×20 的棋盘在这个方向反而最舒展——
   390 宽的屏幕能给出 310×620 的棋盘，比横屏的 150×300 大一倍。
   所以竖屏不是「凑合能玩」，而是这套布局的主力档。
   预算（实测）：面板内边距 24 + HUD 49 + 行距 30 + 侧条 60 + 按键 54 = 217，
   再留 23px 余量 → 240。宁可棋盘小几像素，也不能让按键被屏幕底边裁掉 */
@media (max-width: 768px) and (orientation: portrait) {
  .snake-panel.ttr-panel.is-fs {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: max-content max-content max-content max-content;
    grid-template-areas: "hud" "sideL" "main" "keys";
    row-gap: 10px;
    column-gap: 0;
    /* 内容比屏幕矮时整组居中；放不下时 safe 让它退化成贴顶，滚动才滚得到头 */
    align-content: safe center;
    overflow-y: auto;
  }
  .snake-panel.ttr-panel.is-fs .ttr-main { --stage-h: calc(var(--fs-vh, 100vh) - 240px); }
  .snake-panel.ttr-panel.is-fs .ttr-side-l { flex-direction: row; gap: 10px; }
  /* 竖屏这一行是「预算」里的大头：把两张卡压扁成一指高的信息条 */
  .snake-panel.ttr-panel.is-fs .ttr-card { padding: 8px 12px; gap: 6px; border-radius: 14px; }
  .snake-panel.ttr-panel.is-fs .ttr-card-t { font-size: 10px; letter-spacing: .1em; }
  .snake-panel.ttr-panel.is-fs .ttr-card-lines { flex: 0 0 auto; }
  /* 横排时这条线会变成一根竖杠，直接收掉 */
  .snake-panel.ttr-panel.is-fs .ttr-lv-bar { display: none; }
  .snake-panel.ttr-panel.is-fs .ttr-lines b { font-size: 22px; }
  .snake-panel.ttr-panel.is-fs .ttr-next {
    flex: 0 0 auto;
    width: 84px;
    height: 44px;
    min-height: 0;
    max-height: none;
    border-radius: 10px;
  }
  /* 竖屏按键回到一行五颗：竖排五颗要占掉 300px 高，棋盘就得少 600px 宽 */
  .snake-panel.ttr-panel.is-fs .ttr-pad {
    grid-template-columns: repeat(5, minmax(0, 1fr));
    grid-template-rows: none;
    grid-template-areas: "left rot right soft hard";
    gap: 7px;
    margin: 0 auto;
    max-width: 100%;
  }
  .snake-panel.ttr-panel.is-fs .ttr-key { min-height: 54px; border-radius: 13px; }
  .snake-panel.ttr-panel.is-fs .ttr-key svg { width: 21px; height: 21px; }
}

/* 极老内核（Safari < 11.1 / 老 Edge）没有 display: contents：
   中列拿不回「棋盘 + 按键」，就退成一栏——棋盘在上、按键在下，侧卡竖排在末尾。
   少了「下一个」预览的位置，但玩是能玩的，不会出现空白全屏 */
@supports not (display: contents) {
  .ttr-left { display: block; }
  .snake-panel.ttr-panel.is-fs {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "hud" "main" "sideL" "tip";
  }
  .snake-panel.ttr-panel.is-fs .ttr-main { display: block; width: auto; }
  .snake-panel.ttr-panel.is-fs .ttr-stage { margin: 0 auto; }
  .snake-panel.ttr-panel.is-fs .ttr-pad {
    grid-template-columns: repeat(5, minmax(0, 1fr));
    grid-template-rows: none;
    grid-template-areas: "left rot right soft hard";
    max-width: calc(var(--stage-h) / 2);
    margin: 10px auto 0;
  }
  .snake-panel.ttr-panel.is-fs .ttr-key { min-height: 46px; }
  .snake-panel.ttr-panel.is-fs .ttr-side-l { flex-direction: row; gap: 10px; }
  .snake-panel.ttr-panel.is-fs .ttr-next { width: 96px; height: 56px; min-height: 0; max-height: none; }
  .snake-panel.ttr-panel.is-fs .ttr-side-r { display: none; }
}

/* ================= 贡献者头像墙 =================
   名单里的每一位一个圆位，末尾一个「查看全部贡献者」。
   数据由 index.php 从 contributors 表直出，后台加人就自动出现。
   入场只在「首次进入视口」播一次（JS 加 .is-in 后 disconnect）——
   2026-10-24 那轮把「每滚一屏弹一次」的通用滚动显现整体删过，这里不把它带回来。

   2026-10-27 这一轮按参考页（1Panel 贡献者页）重做交互与密度：
   1) 密度分档 is-t1..is-t4：人数决定直径与每行列数，人少是方阵、人多是密排的墙；
   2) 悬停聚光：指到哪一颗，其余退成半透明——密排的墙上靠放大找目标太累；
   3) 气泡升格成小名片：名字 + 认证 + 贡献，长名字截断、贴边自动避让；
   4) 方向键按真实行列走动，墙也能用键盘「摸」；
   5) 图片淡入：头像不再「啪」地一次齐现，那一瞬间比入场动画还响。

   底板与圆的版式来自参考页那张 PNG：整块浅底圆角平板 + 密排到相接的圆，
   没有描边、没有投影、没有留白。**不要再给每颗头像加框/加投影**，
   一圈套一圈之后密排的墙面就变成一格一格的表格了。 */
.wall-frame {
  /* 板色单独开一个变量：气泡的小三角、圆与圆之间的补白都要跟它同色，
     深浅两套主题各给一份，别在下面写死第二遍 */
  --wall-bg: var(--bg-soft);
  --wall-line: var(--line);
  display: flex;
  justify-content: center;
  width: fit-content;
  max-width: 100%;
  margin: 0 auto;
  padding: 16px;
  border: 1px solid var(--wall-line);
  border-radius: 16px;
  background: var(--wall-bg);
  transition: background-color .45s ease, border-color .45s ease;
}
/* 深色下 --card 与 --bg 只差 8 个灰阶，细边得提亮一档才看得出这块板 */
[data-theme="dark"] .wall-frame {
  --wall-bg: var(--card);
  --wall-line: var(--line-strong);
}

.avatar-wall {
  --a: 68px;                     /* 头像直径：档位与断点只改这一个值，字号与列数跟着走 */
  --g: 0px;                      /* 间距 0：参考页那面墙是密排的，头像彼此相接，
                                    圈与圈只靠自身的圆边分开，不加缝也不加框 */
  --per: 6;                      /* 每行最多几个；列数由 PHP 算（尽量让末行排满） */
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--g);
  /* 行错位的「预算」：每行左右最多偏 ±18% 个直径（由 wall.js 写入 --rowdx）。
     这一圈横向留白就是给它的——行偏出去，最外侧那颗也还在板内，不会被切边 */
  padding-inline: calc(var(--a) * .18);
  /* 行宽锁在「--per 个位」上，是这个阵的边：改 --per 会同时改行列比例。
     max-width 里那 .36 就是上面那条 padding-inline 的左右两份。
     末尾 +1px 是给亚像素留的余量：--a 用 clamp(vw) 算出来常是 39.999…，
     六颗凑起来正好卡在边界上，浏览器会判定「放不下」而把第六颗挤到下一行
     （320 屏会变成 5/5/2 的锯齿） */
  max-width: calc(var(--a) * (var(--per) + .36) + var(--g) * (var(--per) - 1) + 1px);
}
/* 密度分档（由 index.php 按人数打上）。**这一档不是「人多了就把圆缩小」，
   而是「人多了这块板就变宽、圆慢一点缩小」** —— 参考页那面墙 1136×368、
   48 列 15 行、圆只有 23px，它的宽是「列多」堆出来的，不是「圆大」。
   所以列数先跟着人数涨（6 → 9 → 14 → 20），圆最多让到 48px：
     12 人   6 列 × 68px ≈ 443×170（一枚方阵）
     24 人   9 列 × 68px ≈ 612×204（一块横匾）
     48 人  14 列 × 60px ≈ 840×240（一面墙）
     72 人  20 列 × 48px ≈ 960×240（一片人） */
.avatar-wall.is-t1 { --per: 6; }
.avatar-wall.is-t2 { --per: 9; }
.avatar-wall.is-t3 { --a: 60px; --per: 14; }
.avatar-wall.is-t4 { --a: 48px; --per: 20; }

.avatar-item {
  position: relative;
  display: grid;
  place-items: center;
  flex: none;
  /* 每颗一样大：不规则不靠「大小不一」堆出来，那看着像有的头像没加载全 */
  width: var(--a);
  /* 行距比直径多 14%：上下两行错开之后，圆不至于碰在一起，
     也不会因为行贴着行把错位吃掉（行距等于直径时，上下各偏 6% 就撞上了） */
  height: calc(var(--a) * 1.14);
  text-decoration: none;
  /* 整行平移（--rowdx 由 wall.js 按真实行算好写在每颗上）。
     **只走 transform，绝不动 margin**：flex 仍按 --a 算换行，
     行偏移不会撑破列宽、不会把第 N+1 颗挤到下一行、不产生横向滚动 */
  transform: translateX(var(--rowdx, 0px));
  /* 聚光时整颗（圆 + 气泡）一起退后，所以过渡挂在 item 上；
     transform 那条是给转屏后重新分行的平滑用的，入场动画会盖过它 */
  transition: opacity .24s ease,
              transform .45s cubic-bezier(.22, .9, .26, 1);
}
/* 只有 JS 在 <html> 上写过 .js 才先藏起来等入场；脚本没跑时头像直接可见，
   不至于留下一个「墙是空的」的页面 */
.js .avatar-item { opacity: 0; }
.avatar-wall.is-in .avatar-item {
  opacity: 1;
  /* backwards 而不是 both：播完把不透明度交还给上面那条 opacity:1，
     否则动画的终态会一直压着聚光要用的 .4——这是上一版聚光没生效的原因 */
  animation: avatarIn .5s cubic-bezier(.22, .9, .26, 1) backwards;
  /* 24ms 一格：密排的墙上这个错开量刚好看得出「一颗接一颗落位」，
     再慢整面墙就要等一秒，人多了像卡住 */
  animation-delay: calc(var(--i, 0) * 24ms);
}
/* 人多那两档把错峰量收小：72 颗 × 24ms 要等 1.7 秒才铺完，
   那已经不是「一颗接一颗落位」，是「怎么还没出来」 */
.avatar-wall.is-t3 .avatar-item { animation-delay: calc(var(--i, 0) * 14ms); }
.avatar-wall.is-t4 .avatar-item { animation-delay: calc(var(--i, 0) * 9ms); }
/* 位移只有 6px：这是一面「本来就在那儿」的墙，不是从天上掉下来的卡片。
   关键帧里必须把 --rowdx 一起带上——animation 的 transform 会整体接管
   transform，只写 translateY 的话，入场那半秒每颗都会先跳回还没错位的点上，
   等动画一结束再「啪」地滑到本行位置 */
@keyframes avatarIn {
  from { opacity: 0; transform: translateX(var(--rowdx, 0px)) translateY(6px) scale(.93); }
  to   { opacity: 1; transform: translateX(var(--rowdx, 0px)); }
}

/* 「环」这个名字留着，但它已经不再是相框：
   参考页的头像就是一颗实心圆，外面没有留白、没有描边、没有投影。 */
.avatar-ring {
  position: relative;
  display: grid;
  place-items: center;
  /* 圆径就是 --a：全墙同尺寸。上一版给每颗 ±4% 的大小差，
     远看像有的头像没加载全，撤掉了——「不整齐」现在只由行错位负责 */
  width: var(--a);
  height: var(--a);
  border-radius: 50%;
  overflow: hidden;
  /* 1px 的同色描边：头像多时相邻圆在低分辨率屏上会有锯齿，
     这圈描边把它抹平；底色与圆心同色，看不出来 */
  background: var(--bg-soft);
  /* 缩放挂在「环」上而不是 item 上：item 上放 scale 会把名片气泡一起缩放，
     名字就跟着变形了；item 的 transform 还要留给整行平移 */
  transition: transform .42s cubic-bezier(.22, .9, .26, 1),
              box-shadow .35s ease;
}
.avatar-ring img,
.avatar-letter {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  object-fit: cover;
}
.avatar-ring img {
  transition: opacity .55s ease, transform .5s cubic-bezier(.22, .9, .26, 1);
}
/* 淡入：懒加载的头像是一张张到的，直接显示会「啪、啪」地闪，
   入场那点从容全被这一下抢走。
   **挂 .wall-js 而不是 .js**：.js 是 main.js 加的，用来藏整颗头像等入场；
   图片淡入归 wall.js 管，两个脚本各管各的——把图片也挂在 .js 上，
   万一 wall.js 没加载成功，头像就永远停在 opacity:0，墙上只剩一片空圈 */
html.wall-js .avatar-ring img { opacity: 0; }
html.wall-js .avatar-ring img.is-loaded { opacity: 1; }

/* 没上传头像的人：金色渐变 + 名字首字，与 uploads/sample/*.svg 那套示例头像同色，
   墙上不会突然出现几块灰白的小人 */
.avatar-letter {
  display: grid;
  place-items: center;
  background: linear-gradient(135deg, #d8b878, #a98242);
  color: #fff;
  font-family: var(--serif);
  /* 字号跟着圆走（圆的直径就是 --a），小圆里的首字不会顶到边 */
  font-size: calc(var(--a) * .42);
  line-height: 1;
  padding-bottom: 1px;            /* 中文字面重心偏上，压回视觉正中 */
  text-shadow: 0 1px 2px rgba(0, 0, 0, .16);
}
[data-theme="dark"] .avatar-letter { filter: brightness(.88); }

/* ---------- 名片气泡 ----------
   密排之后每格只放一颗头像，名字与来历只能悬停时浮出来。
   底色用 --ink、字用 --bg，深浅两套主题自动反色（不写死白/黑）。
   两行：名字（衬线）+ 一行小字（认证 / 贡献 / 价值）。
   --dx 由 wall.js 按视口边界算出来，贴边的两颗不会被切掉半截。 */
.avatar-tip {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 8px);
  z-index: 9;
  display: grid;
  gap: 1px;
  /* 绝对定位的元素在 left:50% 下「可用宽度」只剩半颗头像，
     不写 max-content 的话气泡会被压成 60px 一坨，第二行必然截断 */
  width: -webkit-max-content;
  width: max-content;
  min-width: 66px;
  max-width: 240px;
  padding: 6px 10px 7px;
  border-radius: 9px;
  background: var(--ink);
  color: var(--bg);
  text-align: center;
  pointer-events: none;
  opacity: 0;
  /* .94 起手：气泡是从圆心「长」出来的，不是从天上掉下来的 */
  transform: translate(calc(-50% + var(--dx, 0px)), 5px) scale(.94);
  transform-origin: 50% 100%;
  transition: opacity .18s ease, transform .3s cubic-bezier(.22, .9, .26, 1);
  box-shadow: 0 6px 18px rgba(16, 20, 26, .18);
}
.tip-name {
  font-family: var(--serif);
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: .02em;
  /* 名字最多两行：二十几个字的昵称截成一行「一个特别特别长的贡献…」
     等于没说。两行装不下才省略号收尾，完整的那份在 aria-label 与目标页里 */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  white-space: normal;
  overflow: hidden;
  word-break: break-word;
}
.tip-meta {
  font-size: 10.5px;
  line-height: 1.4;
  letter-spacing: .04em;
  opacity: .62;
  /* 数字等宽：贡献次数的位数一变，气泡宽度就跳一下 */
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* 小三角：4px 透明边框拼出来的，比再塞一层 SVG 省 DOM。
   位置跟着 --dx 反向偏移，始终对着那颗头像的圆心 */
.avatar-tip::after {
  content: '';
  position: absolute;
  top: 100%;
  left: calc(50% - var(--dx, 0px));
  margin-left: -4px;
  border: 4px solid transparent;
  border-top-color: var(--ink);
}

@media (hover: hover) and (pointer: fine) {
  /* 聚光：密排的墙上光靠一点点放大找目标太累，把其余的压下去最省力。
     只压到 .55——再暗整块板就空了，看不出这里原本有一整面墙；
     人多那两档再狠一点：48 颗以上，不压到 .4 根本分不清哪颗亮着。
     **用 :not(:hover) 反过来写，而不是压完再加一条 opacity:1 盖回来** ——
     盖回来的那条要跟「人多档」比重，越写越像在打仗，改一次错一次 */
  .avatar-wall.is-in:hover .avatar-item:not(:hover):not(:focus-within) {
    /* 不是单纯压低不透明度：这一面墙的圆本身就是浅色，
       压到半透明会整片「发灰」，看着像坏掉了。
       去一点饱和、留一点不透明度，墙还在、颜色退了，被指的那颗是唯一的彩色 */
    opacity: .8;
    filter: grayscale(.42);
  }
  .avatar-wall.is-t3.is-in:hover .avatar-item:not(:hover):not(:focus-within),
  .avatar-wall.is-t4.is-in:hover .avatar-item:not(:hover):not(:focus-within) {
    /* 人多那两档再狠一点：48 颗以上，不退到这份上看不清哪颗亮着 */
    opacity: .58;
    filter: grayscale(.78);
  }

  /* 气泡要压在相邻头像之上：不抬这一层，第二行以后的气泡会被下一格盖住 */
  .avatar-item:hover { z-index: 9; }
  .avatar-item:hover .avatar-ring {
    /* 密排的墙上「上浮 3px」会被上下两颗夹住看不出来，改成放大：
       这颗头像从整片里顶出来，同时压住左右的邻居 */
    transform: scale(1.16);
    box-shadow: 0 0 0 2px var(--gold-bright), 0 12px 24px rgba(20, 24, 29, .24);
  }
  .avatar-item:hover .avatar-tip {
    opacity: 1;
    transform: translate(calc(-50% + var(--dx, 0px)), 0) scale(1);
  }
}
/* 键盘聚焦：键盘用户看不到 hover 的光环，得给一个同等强度的圈 */
.avatar-item:focus-visible { outline: none; z-index: 9; }
.avatar-item:focus-visible .avatar-ring {
  transform: scale(1.12);
  box-shadow: 0 0 0 2px var(--gold-text), 0 0 0 5px var(--gold-soft);
}
.avatar-item:focus-visible .avatar-tip {
  opacity: 1;
  transform: translate(calc(-50% + var(--dx, 0px)), 0) scale(1);
}
.avatar-item:active .avatar-ring { transform: scale(.94); }

.wall-empty {
  margin: 0;
  padding: 6px 10px;
  text-align: center;
  font-size: 13px;
  letter-spacing: .04em;
  color: var(--ink-soft);
}

/* 「查看全部贡献者」：胶囊描边 + 外链图标，与站内 .icp / 主题按钮同一套。
   不再挂人数徽章——数字在框里和标题上都没有，按钮上单独冒一个反而成了唯一出处 */
.wall-more { display: flex; justify-content: center; margin-top: 34px; }
.wall-more-btn {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 10px 18px 10px 20px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--card);
  color: var(--ink-soft);
  font-size: 13px;
  letter-spacing: .06em;
  text-decoration: none;
  box-shadow: var(--shadow-sm);
  transition: transform .35s cubic-bezier(.22, .9, .26, 1),
              box-shadow .35s ease, border-color .3s ease, color .3s ease;
}
.wall-more-btn svg {
  /* 14 而不是 13：外链图标是「方框 + 斜箭头」两笔，13px 里挤成一团看不清 */
  width: 14px;
  height: 14px;
  stroke-width: 1.9;
  /* 中文字形重心偏上，图标居中对齐后看起来会低半像素；用 top 顶回来，
     不写进 transform——transform 留给 hover 那一下 */
  position: relative;
  top: -.5px;
  /* 与名单页卡片上的「进入主页」同一套位移：朝右上，暗示「跳出这一页」 */
  transition: transform .35s cubic-bezier(.22, .9, .26, 1);
}
.wall-more-btn:hover {
  color: var(--ink);
  border-color: var(--line-strong);
  transform: translateY(-2px);
  box-shadow: var(--shadow);
}
.wall-more-btn:hover svg { transform: translate(1px, -1px); }
.wall-more-btn:active { transform: translateY(0) scale(.99); }

@media (max-width: 900px) {
  .avatar-wall { --a: 60px; }
  .avatar-wall.is-t2 { --a: 60px; }
  .avatar-wall.is-t3 { --a: 52px; }
  .avatar-wall.is-t4 { --a: 44px; }
  .wall-frame { padding: 14px; border-radius: 14px; }
}
@media (max-width: 620px) {
  /* 用 vw 跟着屏幕走，而不是写死一个值：密排之后头像之间没有缝，
     尺寸差 4px 就会从「六列」掉成「五列」，多出一行看着就散了。
     38px 是 320 屏的真实下限：那一档正文只有 265 宽，扣掉框内边距与描边剩 239，
     6 × 40 = 240 差 1px 就会掉成 5 列（排出来是 5/5/2 的锯齿），38 才稳；
     52px 是 430 屏的上限——再大就不是「密排的墙」，变成一排大纽扣了。
     人多那几档同比例收到更小，手机上才排得开 */
  .avatar-wall { --a: clamp(38px, 11.6vw, 52px); }
  .avatar-wall.is-t2 { --a: clamp(36px, 10.8vw, 50px); }
  .avatar-wall.is-t3 { --a: clamp(32px, 9.4vw, 44px); }
  .avatar-wall.is-t4 { --a: clamp(28px, 8.2vw, 38px); }
  .wall-frame { padding: 12px; border-radius: 14px; }
  .tip-name { font-size: 11.5px; }
  .tip-meta { font-size: 10px; }
  .wall-more { margin-top: 26px; }
  .section-note { font-size: 12px; letter-spacing: .08em; margin-top: 14px; }
}

/* ================= 页脚 ================= */
footer {
  /* 与宠物挂件同一块地台（--stage）：石头地面下面直接接页脚，中间不再换色，
     页尾是一整片收束的地面，而不是「白正文 / 灰页脚」两段硬拼 */
  background: var(--stage);
  padding: 34px 24px 40px;
  position: relative;
  z-index: 2;
  text-align: center;
  transition: background-color .45s ease;
}
/* 页脚上沿那道 180px 细线撤掉了：砖地自带的接触阴影已经划出边界，
   再叠一条居中短线，会被石头压成一段断头线 */
.footer-inner p { font-family: var(--serif); font-size: 15px; color: var(--ink-soft); letter-spacing: 0.02em; }
.footer-en {
  font-size: 10.5px;
  letter-spacing: .34em;
  /* 英文副行是页脚里唯一的「说明文字」，比站名再淡一档才不会抢扫光的戏 */
  color: var(--ink-soft);
  opacity: .72;
  margin-top: 12px;
}
.icp {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-top: 16px;
  font-size: 13px;
  color: var(--ink-soft);
  text-decoration: none;
  border: 1px solid var(--line);
  background: var(--card);
  padding: 6px 16px;
  border-radius: 999px;
  transition: color .25s, border-color .25s, transform .25s, background-color .45s ease;
}
.icp:hover { color: var(--ink); border-color: var(--line-strong); transform: translateY(-2px); }
.icp svg { width: 14px; height: 14px; flex-shrink: 0; }

/* ================= 回到顶部 ================= */
.to-top {
  position: fixed;
  right: 24px;
  bottom: 24px;
  width: 46px; height: 46px;
  border-radius: 50%;
  border: none;
  background: var(--accent);
  color: var(--on-accent);
  font-size: 20px;
  display: grid;
  place-items: center;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transform: translateY(12px);
  transition: opacity .3s, transform .3s, background .25s, color .45s;
  box-shadow: 0 10px 26px rgba(20, 24, 29, 0.25);
  z-index: 90;
}
.to-top.show { opacity: 1; pointer-events: auto; transform: none; }
.to-top:hover { background: var(--gold-deep); }
[data-theme="dark"] .to-top:hover { background: var(--line-strong); }

/* ================= 入场动画 ================= */
@keyframes fadeUp {
  from { opacity: 0; transform: translateY(26px); }
  to   { opacity: 1; transform: none; }
}
@keyframes float {
  0%, 100% { transform: translateY(0) rotate(-4deg); }
  50%      { transform: translateY(-12px) rotate(-4deg); }
}
@keyframes pulse {
  0%, 100% { box-shadow: 0 0 0 0 var(--pulse-ring); }
  50%      { box-shadow: 0 0 0 6px transparent; }
}
/* ================= 开场交棒：正式印章落定（飞行印章落位后 0.94s 起） ================= */
/* 交棒数学：正式印章 0→35%（119ms）线性淡入，飞行印章 940→1060ms（120ms）线性淡出，
   两条曲线精确互补 → 同点总墨量恒为 1，交棒在画面上完全不可见（不闪）；
   且起始 scale(1) 与落位的飞行印章完全重合，不存在双影。淡入完成后，
   1.02→1.035→1 的轻微过冲回弹接管，读作印章「落定」的重量感 */
@keyframes sealPop {
  0%   { opacity: 0; transform: scale(1) rotate(-4deg);
         animation-timing-function: linear; }
  35%  { opacity: 1; transform: scale(1.02) rotate(-4deg); }
  62%  { transform: scale(1.035) rotate(-4deg); }
  100% { opacity: 1; transform: scale(1) rotate(-4deg); }
}
/* 三段式开场完成后，其余内容依次登场：
   眉标 1.10s → 大标题 1.20s（鎏金扫光同步起步）→ 中文副标 1.34s → 英文行 1.48s → 时钟 1.62s */
/* 印章与 h1 不参与通用 fadeUp 队列：印章走 sealPop，h1 的动画由 .hero h1 单独决定 */
.hero-main > *:not(.seal):not(h1) { animation: fadeUp .55s cubic-bezier(.22,.61,.36,1) both; }
.hero-main > *:not(.seal):not(h1):nth-child(2) { animation-delay: 1.1s; }
.hero-main > *:not(.seal):not(h1):nth-child(4) { animation-delay: 1.34s; }
.hero-main > *:not(.seal):not(h1):nth-child(5) { animation-delay: 1.48s; }
.hero-main > *:not(.seal):not(h1):nth-child(6) { animation-delay: 1.62s; }
/* 注意：.hero h1 优先级高于 .gold-flow，animation 简写会覆盖类的动画，
   此处必须显式写出 goldSweep，否则首屏大标题扫光会被 fadeUp 简写抹掉；
   扫光比 fadeUp 显形晚 0.54s 起步——background-position 逐帧重绘文字，
   两者同跑会互相拖帧，等文字落定（1.75s）后再开始扫 */
.hero h1 { animation: fadeUp .55s cubic-bezier(.22,.61,.36,1) 1.2s both, goldSweep 3s linear 1.74s infinite; }

/* ================= 开场动画总开关：字体就绪后统一起跑 =================
   问题：ZCOOL 字体 871KB，若文字比字体先渲染，会先以 fallback 字形闪现再切换
   （字形闪烁）；且 CSS 动画延迟从「样式应用」起算，与 JS 时间线容易错位。
   方案：JS 给 <html> 加 .js 后，所有开场相关动画默认 paused（停在第 0 帧，
   藏在蒙版后不可见）；待 document.fonts.ready 再加 .intro-go 解除暂停，
   所有延迟从同一时刻起算，整条时间线严格对齐。
   无 JS 时 html 无 .js → 不暂停 → CSS 动画照常播放，页面静态可用。 */
html.js .seal,
html.js .hero-main > *:not(.seal):not(h1),
html.js .hero h1,
html.js .hero h1 .ki { animation-play-state: paused; }
html.intro-go .seal,
html.intro-go .hero-main > *:not(.seal):not(h1),
html.intro-go .hero h1,
html.intro-go .hero h1 .ki { animation-play-state: running; }

/* ================= 刷新跳过开场（<html>.intro-skip，JS 在 DCL 判定注入） =================
   用户在页面下方刷新时（浏览器恢复滚动位置），不重播全屏开场：JS 移除开场层并加此类。
   hero 各元素原本被 html.js 统一 paused（停在第 0 帧即不可见），此处显式放行并清零
   所有延迟：印章 / 标题 / 副标 / 时钟立即以最终状态呈现（顺带 0.3~0.5s 轻入场），
   不再有近 1.6s 的级联等待，鎏金扫光也立即开始而非停在 1.62s 延迟里。
   特异性与上面 html.js / html.intro-go 两组规则相同（两类一元素），靠源码次序胜出；
   .intro 显式隐藏是双保险，防止 JS 移除元素前的那一帧蒙版外泄。 */
html.intro-skip .intro { display: none; }
html.intro-skip .seal,
html.intro-skip .hero-main > *:not(.seal):not(h1),
html.intro-skip .hero h1,
html.intro-skip .hero h1 .ki {
  animation-play-state: running;
  animation-delay: 0s;
}

/* ================= 入场动画策略 =================
   本站仅保留两处「一次性」入场：
   1. 首屏三段式开场（全屏蒙版 + 印章飞行交棒 + hero 内容依次登场 fadeUp）；
   2. 常驻氛围动画（印章漂浮 / 时钟脉冲 / 鎏金扫光 / 首屏粒子）。
   早期版本曾给每个区块标题、卡片、游戏面板挂 .reveal 滚动显现
   （IntersectionObserver 触发，opacity 0→1 + translateY 24px），
   结果每滚一屏都有一次弹入——用户反馈「所有页面都出现这动画」。
   2026-10-24 决定整体移除：内容直接呈现，无 JS 时也不会被藏住。
   开场播放范围：仅限「顶部全新加载」。用户在页面下方刷新时浏览器会恢复
   滚动位置，JS 在 DCL 判定 scrollY > 0 即加 .intro-skip 跳过开场
   （见上方 html.intro-skip 规则与 main.js「刷新跳过开场」段），保住阅读位置。 */

/* ================= 响应式 ================= */
@media (max-width: 960px) {
  section { padding: 72px 20px; }
  .section-head { margin-bottom: 40px; }
}

/* ================= 手机端：与电脑端内容一致，仅做适配优化 ================= */
@media (max-width: 768px) {
  html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
  body { -webkit-tap-highlight-color: transparent; }
  section { padding: 64px 20px; }
  /* 首屏：保留满屏沉浸感，用 svh 兼容移动端浏览器工具栏 */
  .hero { padding: 84px 20px 56px; min-height: 100vh; min-height: 100svh; }
  .hero::before { width: 560px; height: 560px; }
  /* 手机端印章：适度加大提升存在感；内框/界格/圆角全部随 --seal-pad、--seal-radius 自适应 */
  .seal { width: 112px; height: 112px; --seal-pad: 8px; --seal-radius: 15px; font-size: 46px; margin-bottom: 22px; }
  /* 字号变小，篆书重心偏移按比例收紧，保持四字视觉居中 */
  .seal span { transform: translateY(2px); }
  .eyebrow { font-size: 11px; letter-spacing: .26em; margin-bottom: 16px; }
  .eyebrow::before, .eyebrow::after { width: 18px; }
  /* 注意：此处只改字号，不能写 animation 简写，否则会覆盖 h1 的鎏金扫光 */
  .hero h1 { font-size: clamp(46px, 13vw, 92px); }
  .hero h1 .ki { margin-left: 6px; }
  .hero-sub { font-size: 17px; margin-top: 12px; }
  .hero-en { font-size: 11px; letter-spacing: .08em; margin: 10px auto 32px; }
  .clock { margin-top: 34px; font-size: 14px; padding: 7px 16px; }
  /* 区块标题 */
  .section-head { margin-bottom: 36px; }
  .section-head .en { font-size: 11px; letter-spacing: .28em; }
  .section-head .zh { font-size: 28px; }
  .section-head .divider::before, .section-head .divider::after { width: 40px; }
  /* 贪吃蛇：面板收紧留白，小屏也放得下一整局；棋盘与排行榜回到上下堆叠 */
  .snake-wrap { max-width: 560px; }
  .game-wrap { max-width: 560px; }
  .game-tabs { gap: 8px; margin-bottom: 12px; }
  .game-arrow { width: 30px; height: 30px; }
  .game-arrow svg { width: 13px; height: 13px; }
  .game-tab { font-size: 13px; gap: 5px; padding: 7px 2px; }
  .game-tab-ic { width: 15px; height: 15px; }
  /* 俄罗斯方块：三栏放不下，收成「HUD / 信息条 / 棋盘 / 键位」四段；
     两张信息卡改成横排一条，键位表交给棋盘下的按键 */
  .snake-panel.ttr-panel {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "hud" "sideL" "main" "sideR";
    row-gap: 10px;
  }
  .ttr-side-l { flex-direction: row; gap: 8px; }
  .ttr-side-l .ttr-card { flex-direction: row; align-items: center; gap: 10px; padding: 9px 11px; }
  .ttr-side-l .ttr-card:first-child { flex: 2 1 0; }
  .ttr-side-l .ttr-card:last-child { flex: 0 0 auto; }
  .ttr-next { flex: 0 0 auto; width: 72px; height: 42px; min-height: 0; }
  .ttr-lines b { font-size: 20px; }
  .ttr-lv-bar { display: none; }
  .ttr-main { --stage-h: clamp(280px, 54vh, 470px); }
  .ttr-pad { max-width: calc(var(--stage-h) / 2); }
  .ttr-pad { margin-top: 10px; gap: 6px; max-width: none; }
  .ttr-key { height: 44px; border-radius: 11px; }
  .ttr-key svg { width: 19px; height: 19px; }
  .snake-panel {
    padding: 14px;
    border-radius: 16px;
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "hud" "main" "board";
  }
  /* HUD 现在只有统计 + 一颗全屏钮；三项横排，620 这一档放得下，
     320/360 那种极窄屏由文件末尾的「极窄屏 HUD 收口」接手 */
  .snake-hud { margin-bottom: 12px; padding-bottom: 11px; gap: 8px; }
  .snake-fs { width: 34px; height: 34px; border-radius: 11px; }
  .snake-fs svg { width: 15px; height: 15px; }
  .snake-stat { gap: 5px; }
  .snake-stat + .snake-stat { margin-left: 15px; }
  .snake-stat + .snake-stat::before { left: -7px; height: 19px; }
  .snake-stat b { font-size: 21px; }
  .snake-stat i { font-size: 11px; letter-spacing: .04em; }
  .snake-btn { padding: 8px 16px; font-size: 13px; }
  .snake-stage { border-radius: 12px; }
  .snake-overlay { gap: 5px; }
  .snake-overlay-icon { width: 27px; height: 27px; }
  .snake-overlay-title { font-size: 22px; }
  .snake-overlay-text { font-size: 12px; }
  .snake-overlay-quip { font-size: 10px; max-width: 88%; }
  .snake-tip { font-size: 12px; }
  /* 排行榜：竖排时回到面板底部，以分隔线区隔 */
  .snake-board {
    padding-left: 0;
    border-left: 0;
    margin-top: 14px;
    padding-top: 12px;
    border-top: 1px solid var(--line);
  }
  .snake-board-list { flex: none; overflow: visible; }
  .snake-board-title { font-size: 14px; }
  .snake-board-sub { font-size: 10px; letter-spacing: .1em; }
  /* 手机：10 条跟着页面自然排下来（跟着页面滚，不在榜单里再套一层滚动） */
  .snake-board-row { flex: 0 0 auto; min-height: 0; padding: 7px; }
  .hist-score { font-size: 15px; }
  .snake-board-icon { width: 14px; height: 14px; }
  .snake-board-count { font-size: 9px; }
  /* 头像墙的响应式全在上方 .avatar-wall 段里统一处理（这里不再插一条会覆盖它的字号） */
  /* 页脚 */
  footer { padding: 32px 20px; }
  .footer-inner p { font-size: 13px; }
  .footer-en { font-size: 10px; letter-spacing: .24em; }
  .icp { font-size: 12px; padding: 6px 14px; }
  /* 浮动控件：缩小并贴近边缘，避免遮挡内容 */
  .theme-toggle { top: 14px; right: 16px; width: 38px; height: 38px; }
  .theme-toggle svg { width: 17px; height: 17px; }
  .to-top { right: 16px; bottom: 16px; width: 42px; height: 42px; }
}

/* ================= 页脚宠物挂件 =================
   八只小动物趴在矮墙后探头，墙下一带砖地，整块压在页脚上沿。

   结构必须这样叠，否则一定不自然：
     animals.webp 是一张 1948×236 的透明 PNG 式长图（2x，显示 974×118）。
     它的「墙底」在距图片底边 36px 处，墙底以下是一段 alpha 渐隐 + 墙根阴影。
     砖地 ground.webp（3600×72，2x）就 absolute 贴底、高 36px ——
     正好被图片底部那 36px 透明区透出来，砖地上沿又被不透明的墙底挡住，
     于是「白墙坐在砖地上、墙根有影子」是一次成像的，没有拼缝。
     图片左右两端也是全透明的，砖地在两侧继续延伸时不会撞上硬边。

   两张图都在 assets/img/pets/。样式写在 style.css（两页都加载）。
   位置：页脚之前。不放进页脚体内，是因为名单页的页脚挂在 icp_beian_show
   开关下，装饰件不该跟着备案号一起消失。 */
.pets-band {
    /* 插画显示宽度（2x 图折半） */
    --pets-w: 974px;
    /* 砖地高度不写死 36：它必须等于「墙底以下的透明区」，
       而那一段占图片高度的 36/118。图片按屏宽缩小时，砖地若固定 36px，
       390 屏上就会出现「47px 高的动物 + 36px 高的墙」——墙几乎和动物一样高。
       这里按显示宽度换算，1440 屏仍是 36，390 屏自动降到 14.4 */
    --pets-ground: calc(min(var(--pets-w), 100vw) * 36 / 974);
    position: relative;
    margin: 56px 0 0;
    /* 地台：正文的白从这里渐入 --stage，一路延伸到页脚都不再变。
       上一版这块是透明的，于是插画里那条米色矮墙直接贴在纯白页面底上，
       墙的两端（图片只有 974 宽，两侧各留 200 多像素）没有依托，八个动物
       像悬在半空、脚下一排孤零零的石头——「不协调」说的就是这里。
       34% 这个终点落在动物头顶与爪子之间，渐变走完时正好是墙身，
       不会在墙面中间留下横向分界。 */
    background: linear-gradient(to bottom, var(--bg) 0, var(--stage) 34%);
}
/* 砖地：整宽横铺，贴容器底。图片左右透明，所以两侧露出的砖地是接得上的 */
.pets-band::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: var(--pets-ground);
    background: url('../img/pets/ground.webp') repeat-x center bottom / auto 100%;
    /* 投影压得比上一版轻：地台与页脚同色，7px 的灰影会在页脚上沿糊出一条
       脏带；这里只要让石子「压在地面上」的一点点接触感 */
    box-shadow: 0 3px 6px rgba(20, 24, 29, .12);
}
/* 图片压在砖地之上，底部 36px 的透明区让砖地透出来 */
.pets-band img {
    position: relative;
    display: block;
    width: min(var(--pets-w), 100%);
    height: auto;
    margin: 0 auto;
    /* 只防拖拽：拖不进桌面、新标签页，也不会拖出一张半透明虚影。
       属性 draggable="false" 管 HTML5 拖放，-webkit-user-drag 管 WebKit 的
       原生图片拖拽，两个都要——只写一个在部分浏览器上会漏。
       user-select 是配套的：不关掉的话按下拖动会顺手选中整块，
       松手留下一片蓝色高亮。 */
    -webkit-user-drag: none;
    user-select: none;
    -webkit-user-select: none;
}
/* 深色模式：整块压暗一档（与参考站同一个数）。白墙不会跟着变深，
   压暗后是「夜里的白墙」，比纯白一块压在深底上温和 */
[data-theme="dark"] .pets-band img { filter: brightness(.8); }
[data-theme="dark"] .pets-band::before { filter: brightness(.62); }
/* 首页的贡献者名录整块在 <section> 里，section 自带 96px 下内边距，
   再叠 56px 就是一条 150px 的空带；紧跟在 section 后面时把外边距收掉。
   名单页上面隔着一段脚本（main 没有底部留白），仍然吃 56px */
section + .pets-band { margin-top: 0; }

/* ================= 动画策略 =================
   本站为个人作品站，动画即内容的一部分（开场 / 漂浮 / 脉冲 / 扫光 / 滚动显现）。
   早期版本曾按系统「减弱动效」偏好整体跳过开场，但用户本机 Windows 该选项常开，
   浏览器恒报告 prefers-reduced-motion: reduce，导致电脑上任何动画都看不到；
   现调整为无条件播放完整动画。各动画均为低强度设计（无高速位移、无频闪），
   无 JS 时页面静态可用（.intro 无 .on 不显示，内容直接呈现）。 */
  

/* ================= 极窄屏（≤350px）HUD 收口 =================
   320 / 360 这两档，HUD 里「三项统计 + 全屏钮」的最小内容宽是 279px
   （3 × 68 + 2 × 15 间距 + 8 gap + 34 按钮），而面板内宽只有 235 / 277，
   于是 space-between 把按钮顶出面板右沿 —— 整页横向多出 31px，
   手机上左右一晃就是这条白边。
   先缩小分句间距与竖线位置，再窄一点就把全屏钮收起来：
   手机本来就是全屏浏览，这颗钮在 320 屏上只是装饰。
   但**全屏时不能收**——那正是它唯一有用的时刻（点一下退出全屏）。
   收掉的是「还嵌在页面里」的那颗，全屏态无论如何都要留着。 */
@media (max-width: 400px) {
  .snake-stat + .snake-stat { margin-left: 10px; }
  .snake-stat + .snake-stat::before { left: -5px; }
}
@media (max-width: 350px) {
  /* 只收贪吃蛇那颗：320 屏上三项数字加一颗全屏钮正好把宽度吃满。
     俄罗斯方块不能收——它那两颗是「暂停」和「全屏」，全屏本身还能把棋盘放大，
     收掉就再也进不去了；改由下面那条把按钮缩一档 */
  .snake-panel:not(.ttr-panel):not(.is-fs) .snake-hud-right { display: none; }
  .ttr-panel .snake-hud-right { gap: 6px; }
  .ttr-panel .snake-fs { width: 30px; height: 30px; border-radius: 10px; }
  .ttr-panel .snake-fs svg { width: 14px; height: 14px; }
  .ttr-panel .snake-stat + .snake-stat { margin-left: 10px; }
  .ttr-panel .snake-stat + .snake-stat::before { left: -5px; }
  .ttr-panel .snake-stat b { font-size: 19px; }
}

/* 名字气泡只在有指针的宽屏上出现：
   它是绝对定位、居中挂在头像正上方，最左/最右那一格的气泡会伸到框外。
   桌面上看不见（opacity 0）却仍算进 scrollWidth，340/360/375 三档分别
   多出 26/16/8px 的横向滚动。手机上本来也没有 hover，直接收掉。 */
@media (hover: none), (max-width: 620px) {
  .avatar-tip { display: none; }
}

/* ================= 跨浏览器兜底（老内核 / iOS / 国产浏览器） =================
   写法约定，两条：
   ① 能用「顺序声明」表达的（老写法在前、新写法在后），就直接写两行——
      新浏览器用后者，老浏览器丢掉不认识的那条用前者，一条 @supports 都不用；
   ② 只有「新写法失效会让整块布局塌掉」时才用 @supports not 圈起来，
      保证新浏览器一个字节都不进那个块。

   目标内核：iOS Safari 12~15、Android WebView / Chrome 70~100、Firefox，
   以及 UC / QQ / 夸克这类基于旧 Chromium 的国内内核。

   为什么值得单独写一段：这份样式里深度依赖了几样「2021 年前后才有」的东西
   （inset / aspect-ratio / clamp / min / dvh / 容器查询），而它们失效时
   不是「稍微难看一点」，是**整块元素高度归零、游戏直接不能玩**。 */

/* ---- 1) inset → 四边定位 ----
   Safari 14 / Chrome 86 以前不认 inset。棋盘画布、浮层、金箔雨、开场蒙版
   全靠它铺满父层；少了它，这些元素会以静态位置落点、尺寸归零，
   棋盘上是空的、蒙版也盖不住屏幕 */
.intro,
.intro-veil,
.hero-particles,
#snakeCanvas,
#ttrCanvas,
.snake-overlay,
.snake-confetti,
.snake-board-row.is-new::after {
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}
/* 印章的界格与内框不是 0，是 --seal-pad（随尺寸浮动），单独写 */
.seal::before {
  top: var(--seal-pad);
  right: var(--seal-pad);
  bottom: var(--seal-pad);
  left: var(--seal-pad);
}
.seal::after {
  top: calc(var(--seal-pad) - 1px);
  right: calc(var(--seal-pad) - 1px);
  bottom: calc(var(--seal-pad) - 1px);
  left: calc(var(--seal-pad) - 1px);
}
.snake-panel.is-fs-fake {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}

/* ---- 2) 全屏高度：--fs-vh 已在上面的原始规则里接好 ----
   注意这里**不能**再重写 grid-template-columns / .snake-stage 的 width：
   竖屏（max-width:768px + portrait）与矮屏（max-height:560px）各有一套
   grid 定义，媒体查询并不会提升优先级——末尾一条无媒体查询的
   .snake-panel.is-fs 会把它俩一起盖掉，竖屏全屏直接塌成三列（实测过）。
   高度这类「没有媒体查询竞争」的属性才适合在这里补。 */

/* ---- 3) place-items → 两个方向各写一遍 ----
   老 Safari 不认 place-items / place-self，图标会贴到格子左上角。
   整段包在 @supports not 里：place-items 是**能被媒体查询差异化**的属性
   （竖屏 .snake-main 用的是 start center），末尾一条无条件的重写会连
   竖屏那套一起盖掉——所以只在老内核里生效，现代浏览器一个字节都碰不到。 */
@supports not (place-items: center) {
  .intro-logo,
  .dpad-key,
  .snake-fs,
  .avatar-ring {
    align-items: center;
    justify-items: center;
  }
  /* 头像的槽位就是个居中盒：认 grid 但不认 place-items 的浏览器，
     少了这一条，圆会贴在槽的左上角（槽宽还不一样，一眼就歪） */
  .avatar-item,
  .snake-panel.is-fs .snake-main {
    align-items: center;
    justify-items: center;
  }
  @media (max-width: 768px) and (orientation: portrait) {
    .snake-panel.is-fs .snake-main { align-items: start; }
  }
  .dpad-hub {
    align-self: center;
    justify-self: center;
  }
}
.dpad-hub {
  align-self: center;
  justify-self: center;
}

/* ---- 4) 刘海 / 圆角屏的安全区 ----
   全屏时方向键贴着左边缘，横屏 iPhone 的刘海正好盖在上面（iPad 的圆角同理）。
   env() 需要 viewport-fit=cover，index.php 的 viewport 已加；
   不支持 max() 的浏览器整条失效，自动退回上面 18px / 24px 那套，不添乱。 */
.snake-panel.is-fs {
  padding-top: max(18px, env(safe-area-inset-top));
  padding-right: max(24px, env(safe-area-inset-right));
  padding-bottom: max(18px, env(safe-area-inset-bottom));
  padding-left: max(24px, env(safe-area-inset-left));
}
@media (max-height: 560px) {
  .snake-panel.is-fs {
    padding-top: max(10px, env(safe-area-inset-top));
    padding-right: max(14px, env(safe-area-inset-right));
    padding-bottom: max(10px, env(safe-area-inset-bottom));
    padding-left: max(14px, env(safe-area-inset-left));
  }
}
/* 竖屏那一档的 padding 是 12px（比横屏更紧，因为横向只有 320~430px），
   上面的四边写法会把它盖掉——媒体查询不提升优先级，这里按同样条件补回来 */
@media (max-width: 768px) and (orientation: portrait) {
  .snake-panel.is-fs {
    padding-top: max(12px, env(safe-area-inset-top));
    padding-right: max(12px, env(safe-area-inset-right));
    padding-bottom: max(12px, env(safe-area-inset-bottom));
    padding-left: max(12px, env(safe-area-inset-left));
  }
}

/* ---- 5) 移动端手感 ----
   touch-action: manipulation 去掉 iOS / Android 的 300ms 点击判定延迟
   （方向键那种连点场景最明显）；
   -webkit-touch-callout / tap-highlight 关掉长按菜单与点击灰块，
   否则手机上按住方向键会弹出「拷贝 / 存储图像」。
   注意 .snake-dpad 与两个 stage 是 touch-action: none（要做手势），
   不能被这里覆盖——子元素写 manipulation 也压不过祖先的 none，安全。 */
.theme-toggle,
.snake-fs,
.snake-btn,
.dpad-key,
.ttr-key,
.wall-more-btn,
.game-tab {
  touch-action: manipulation;
}
.snake-stage,
.ttr-stage,
.snake-dpad {
  -webkit-touch-callout: none;
}
/* iOS 的橡皮筋回弹：全屏时整页锁了 overflow，再叠一层 overscroll 才真的不弹 */
html.snake-fs-on,
html.snake-fs-on body {
  overscroll-behavior: none;
}
/* 榜单在 iOS 上默认没有惯性滚动，长列表摸着发滞 */
.snake-board-list {
  -webkit-overflow-scrolling: touch;
}

/* ==== 6) 不支持 aspect-ratio（Safari < 15 / Chrome < 88） ====
   这一条最凶：棋盘靠 aspect-ratio 撑高度，不认它的话 .snake-stage 高度是 0，
   画布 0×0，点了开始也只是一片空白。
   padding-bottom: 100% 是「按宽度算百分比」的老办法，等比撑高；
   画布与浮层用 absolute 铺在 padding box 上，覆盖范围与原来完全一致。 */
@supports not (aspect-ratio: 1 / 1) {
  .snake-stage {
    height: 0;
    /* 百分比 padding 按包含块宽度算，但元素本身还有上下各 1px 边框
       （box-sizing: border-box 下 border 也计入总高），不减掉这 2px 就比宽高多 2px，
       画布会被纵向拉伸 0.6%——肉眼看不出来，但格子会跟网格线差半像素 */
    padding-bottom: calc(100% - 2px);
  }
  /* 方块棋盘本来有 --stage-h 撑高度，只是宽度要自己算（同样扣掉左右边框） */
  .ttr-stage {
    width: calc((var(--stage-h) - 2px) / 2);
  }
}

/* ==== 7) 不支持 clamp() / min()（Safari < 13.1 / Chrome < 79） ====
   --fs-board、--dpad-w、--stage-h、--a 全是 clamp()：
   不支持时变量为空 → 引用它的 grid-template-columns 整条作废 →
   全屏布局塌成一列（方向键、棋盘、榜单叠在一起）。
   这里给一套静态值，并把中列从 min() 换成 1fr + max-width 的等价写法。 */
@supports not (width: clamp(1px, 1px, 1px)) {
  .snake-panel.is-fs {
    --fs-board: 280px;
    --dpad-w: 156px;
    /* min() 同样不认，中列直接 1fr，棋盘自己用 max-width 收方 */
    grid-template-columns: var(--dpad-w) minmax(0, 1fr) var(--fs-board);
  }
  .snake-panel.is-fs .snake-stage {
    width: 100%;
    max-width: calc(var(--fs-vh, 100vh) - 96px);
  }
  .snake-panel.is-fs .snake-board-row,
  .snake-panel.is-fs .snake-board-seat {
    max-height: 56px;
  }
  .ttr-main {
    --stage-h: 460px;
  }
  .avatar-wall {
    --a: 52px;
  }
  @media (max-width: 900px) {
    .avatar-wall { --a: 54px; }
  }
  /* 320 屏的 40px 是算出来的硬下限：正文 265 − 框内边距 26 = 239，
     6 × 40 = 240 差 1px 就掉成 5 列（排出来是 5/5/2 的锯齿） */
  @media (max-width: 620px) {
    .avatar-wall { --a: 40px; }
  }
  @media (max-height: 560px) {
    .snake-panel.is-fs {
      grid-template-columns: var(--dpad-w) minmax(0, 1fr) var(--dpad-w);
    }
    .snake-panel.is-fs .snake-stage { max-width: calc(100vh - 76px); }
    .ttr-main { --stage-h: 240px; }
  }
  /* 竖屏必须排在最后：这一段的三列默认值是按「横屏 / 桌面」写的，
     而竖屏在正常样式里另有两列布局（HUD / 方向键+棋盘 / 榜单）。
     媒体查询不提升优先级，所以这里也要按同样的顺序补一遍，
     让竖屏的规则最后落下来 */
  @media (max-width: 768px) and (orientation: portrait) {
    .snake-panel.is-fs {
      --dpad-w: 54px;
      grid-template-columns: var(--dpad-w) minmax(0, 1fr);
    }
    .snake-panel.is-fs .snake-stage { max-width: none; }
  }
}

/* ==== 8) 不支持 dvh（Safari < 15.4 / Chrome < 108） ====
   正常样式里的 100dvh 已经全部换成 var(--fs-vh, 100vh) 的链，
   这里只补最要紧的一条：height。var() 的 fallback 里仍写着 100dvh，
   而不认 dvh 的浏览器会让整条 height 声明作废 → 必须有一条纯 vh 顶着。 */
@supports not (height: 100dvh) {
  .snake-panel.is-fs { height: 100vh; }
}

/* ==== 9) 刘海 / 圆角屏的安全区（页面级） ====
   index.php 的 viewport 加了 viewport-fit=cover，页面从此会画到刘海、圆角、
   底部横条下面——不补这一层，iPhone 横屏时正文会顶到刘海边上，
   右上角的主题钮也会被圆角切掉一角。
   桌面浏览器 env() 恒为 0，等于什么都没做，所以不额外套媒体查询。 */
@supports (padding: env(safe-area-inset-left)) {
  body {
    padding-left: env(safe-area-inset-left);
    padding-right: env(safe-area-inset-right);
    padding-bottom: env(safe-area-inset-bottom);
  }
  /* 全屏游戏时把 body 的安全区补白撤掉：面板自己按 max(18px, env(...)) 排
     内边距，两边都补会变成「补两次」，面板宽度也不再等于 100vw */
  html.snake-fs-on body {
    padding-left: 0;
    padding-right: 0;
    padding-bottom: 0;
  }
  .theme-toggle {
    top: calc(18px + env(safe-area-inset-top));
    right: calc(20px + env(safe-area-inset-right));
  }
  /* 回到顶部按钮同理（右下角那颗），保持原来的 24px 基准 */
  .to-top {
    right: calc(24px + env(safe-area-inset-right));
    bottom: calc(24px + env(safe-area-inset-bottom));
  }
}
