/* =========================================================================
   Tsunami Network — 设计系统
   子夜海蓝 · 配色取自全屏动态壁纸：
   深藏青海天 #060a17 · azure 亮蓝 #58a6ff · 日冕冰青 #6fe3ff · 香槟金 #f0d5a2
   ========================================================================= */

/* 像素字体：只用于大字号展示元素，正文一律无衬线体。
   ⚠️ 这里挂的是**子集化 + WOFF2** 的版本，不是原始的 968KB TTF：
   它只被两处用到 —— 导航栏品牌名（Tsunami Network / Tsunami Studio）和首屏水印
   （TSUNAMI / DOWNLOAD / HISTORY / RANK），所以子集只留了 ASCII 可打印 + 几个排印符号，
   968KB → 5.9KB（2026-09-26 实测）。Layout.astro 里还对它做了一条 preload。
   换字 / 加新水印文案后如果发现某个字符用了兜底字体，用这条命令重新生成：
     python -m fontTools.subset "public/fonts/像素体.ttf"        --unicodes=U+0020-007E,U+00A0,U+00B7,U+2013,U+2014,U+2018-201D,U+2026        --flavor=woff2 --no-hinting --output-file=public/fonts/pixel-logo.woff2
   第二个 src 是给不支持 woff2 的浏览器兜底的（现代浏览器永远走第一个，不会去下 TTF）。 */
@font-face {
  font-family: 'PixelLogo';
  src: url('/fonts/pixel-logo.woff2') format('woff2'),
       url('/fonts/像素体.ttf') format('truetype');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

:root {
  /* 原生 UI 跟着主题走：滚动条（Blink 之外的那些）、表单控件、自动填充、
     以及页面加载期浏览器自己的底色。不写这一行，深色主题下这些东西还是浅色的
     —— 等于已经做过的 ::-webkit-scrollbar 只做了一半。
     深色那份必须写进 [data-theme="dark"]（它特异性更高，两处都写才盖得住）。 */
  color-scheme: light;
  /* Firefox 的滚动条没法定制宽度，给它「细」档，和 Blink 那 10px 大致对齐 */
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;

  /* ---------- 品牌色（主色调，取自壁纸，变量名是契约勿改） ---------- */
  --brand-navy: #070c1e;   /* 子夜海蓝：比旧藏青更深更纯，压得住发光壁纸 */
  --brand-wave: #58a6ff;   /* azure 亮蓝：天空的发光感 */
  --brand-ice: #c9e4ff;
  --brand-cyan: #6fe3ff;   /* 日冕冰青辉光 */
  --brand-deep: #2b63ec;   /* azure 的深端，渐变落点 */

  /* ---------- 语义色 · 浅色 ---------- */
  --bg: #e8eef8;
  --bg-elevated: #ffffff;
  --surface: #ffffff;
  --surface-2: #e9f0f9;
  --surface-hover: #f4f8fd;
  --border: rgba(13, 30, 66, 0.10);
  --border-strong: rgba(13, 30, 66, 0.18);

  --text: #0a1226;
  --text-dim: #4f6188;
  --text-faint: #8593b0;

  --brand: #1f6fe5;
  --brand-hover: #1a5fd0;
  --brand-fg: #ffffff;
  --brand-soft: rgba(31, 111, 229, 0.10);
  --brand-glow: rgba(31, 111, 229, 0.24);
  /* 边缘光：指针在卡片边框上描出的那一圈光，浅色下用品牌蓝压一点才看得见 */
  --edge-glow: rgba(56, 118, 240, 0.55);
  /* 按钮立体渐变的高低两端，浅色下靠拉开明度造厚度 */
  --brand-hi: #4a90f2;
  --brand-lo: #1858c8;

  --accent: #0b7d99;
  /* 香槟金：只给第一名、强调徽标这类极少数位置，浅色下压暗一档保对比度 */
  --gold: #a87e2c;
  --gold-soft: rgba(168, 126, 44, 0.12);
  --gold-glow: rgba(168, 126, 44, 0.25);
  --success: #059669;
  --success-soft: rgba(5, 150, 105, 0.12);
  --danger: #dc2626;

  /* 品牌字标的渐变。浅色下必须整体压深（亮底上的浅渐变等于看不见），
     所以走 brand → accent 这对深色系；深色下反过来走「白 → 冰青 → 日冕青」，
     和首屏高亮词是同一套语言。写死一套必有一套不可见（见交接文档坑 10）。 */
  --logo-grad: linear-gradient(120deg, var(--brand) 0%, var(--accent) 100%);

  /* 导航栏滚动态的乳浊度。刻意比 --glass-bg-strong 透：
     胶囊是浮在正文上的，用通栏的重玻璃会糊成一块实心深色，
     毛玻璃的「透」才是高级感的来源。浅色下给得比深色足 ——
     背后是同一张深色壁纸，白玻璃太透会让深色文字失去对比度。 */
  --nav-glass-scrolled: rgba(252, 253, 255, 0.72);

  /* 手机端下拉面板的底色。刻意比 --glass-bg-strong(0.82) 实得多：
     它悬在**不可预知**的内容上 —— Studio 世界里正下方就是一张亮色的身份卡，
     80% 的暗色玻璃透出来之后面板和头像互相干扰，菜单项都读不清。
     毛玻璃的「透」在有明确阅读任务的面板上要让位于可读性
     （苹果的菜单也基本是实的）。保留一点透明度 + 模糊，只为了让边缘
     还能看出「这是一块玻璃」，而不是要穿透它。 */
  --nav-menu-bg: rgba(252, 253, 255, 0.95);

  /* ---------- 磨砂玻璃（WinUI Acrylic 配方：乳浊染色 + 模糊 + 噪点） ----------
     磨砂不是「越透越像」，而是靠一层很淡的噪点把背后揉成毛面。
     染色偏乳白、不透明度给足，颜色透进来本身就是糊的，不需要再加饱和度。 */
  --glass-bg: rgba(252, 253, 255, 0.60);
  --glass-bg-strong: rgba(252, 253, 255, 0.82);
  --glass-border: rgba(255, 255, 255, 0.85);
  --glass-edge: rgba(255, 255, 255, 0.95);
  /* 自上而下三段：顶部受光、中段回落、底部带一点 azure 反光，模拟材料厚度 */
  --glass-sheen: linear-gradient(180deg,
    rgba(255, 255, 255, 0.35) 0%,
    rgba(255, 255, 255, 0.08) 45%,
    rgba(120, 170, 255, 0.05) 100%);
  /* 多层环境阴影：近处一层贴边小阴影给轮廓，远处一层大半径软阴影给浮起感 */
  --glass-shadow: 0 2px 6px rgba(13, 30, 66, 0.07),
    0 20px 48px -12px rgba(13, 30, 66, 0.22);
  --glass-blur: 16px;
  /* 内阴影分层：顶部受光边 + 底部暗边 + 一圈极淡的轮廓环，边缘才有「切削感」 */
  --glass-inset: inset 0 1px 0 rgba(255, 255, 255, 0.85),
    inset 0 -1px 0 rgba(13, 30, 66, 0.04),
    inset 0 0 0 1px rgba(255, 255, 255, 0.55);
  --glass-grain: 0.028;

  /* 渐变描边配色：azure → 冰青 → 回落，描边跟着光走而不是一圈死边 */
  --gradient-border: conic-gradient(from var(--gb-angle, 200deg) at 50% 0%,
    rgba(31, 111, 229, 0.55) 0deg,
    rgba(11, 125, 153, 0.30) 100deg,
    rgba(31, 111, 229, 0.12) 200deg,
    rgba(31, 111, 229, 0.55) 360deg);

  /* 噪点贴图：卡片、导航栏、页脚共用 */
  --noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");

  /* 光晕 / 氛围 */
  /* 壁纸遮罩：只给文字垫对比度的最低限度。
     之前 76-86% 的白遮罩把壁纸糊成一片灰，壁纸的质感和色彩全没了 ——
     现在只在顶部（导航/标题区）和底部（正文起点）稍作加深，中段几乎全透。 */
  --ambient-veil: linear-gradient(180deg,
    rgba(232, 238, 248, 0.62) 0%,
    rgba(232, 238, 248, 0.44) 38%,
    rgba(232, 238, 248, 0.58) 100%);

  /* 首屏文字的方向性压暗：从左往右很快消失，右侧壁纸保持原样。
     有它之后大标题就能用干净的纯白 + 纯青色，不必再往每个字周围描一圈
     深色投影去硬撑对比度 —— 那圈描边在亮云上就是一团脏。 */
  --hero-scrim: linear-gradient(100deg,
    rgba(232, 238, 248, 0.86) 0%,
    rgba(232, 238, 248, 0.64) 30%,
    rgba(232, 238, 248, 0.24) 52%,
    transparent 72%);

  /* 首屏高亮词的渐变。浅色下标题是深色，高亮词必须跟着深下去；
     深色下反过来走「白 → 冰青 → 日冕青」。写死一套，必有一套看不见。 */
  --hero-accent-grad: linear-gradient(165deg, var(--brand) 0%, var(--accent) 100%);

  /* ---------- Studio 世界（个人站）----------
     两个世界共用同一套玻璃、圆角、动效曲线，只在「配色语义」上分家：
     Network 取深海的 azure，Studio 取云端的海系色阶（--grad）。
     下面这些 token 原本长在个人站自己的 :root 里，合并时全部并进这里，
     于是两套主题只有这一处定义、别无副本（见交接文档「同一个类不要定义两遍」）。 */

  /* 名字质感：实心渐变 + 描边套印。亮色下整体压深，压在亮云壁纸上才看得见 */
  --grad: linear-gradient(115deg, #0b8f9c, #0f74cf 34%, #1f4ac0 68%, #2a35a8);
  /* 白字压渐变必须走这条：--grad 的浅端在白字下只有 1.19~1.5:1。
     取同一色阶的深段后实测 5.2~8.8:1，整条都过 AA */
  --grad-btn: linear-gradient(115deg, #0f74cf, #1f4ac0 55%, #2a35a8);
  /* 两段式字标（前半实心 + 后半描边）2026-09-26 由站主要求**恢复**：
     --title-stroke 与 --t-stroke 又有了消费者（studio.css 的 .title .ltr.t-line）。
     「套印错位」那一层没有跟着回来 —— 它是当年字标发糊的三份软边之一，
     站主给的目标观感里也没有它，所以 --title-accent / --t-off 仍然**没有消费者**，
     数值留着备查。指针扫过实心段的那圈「边缘光」用的还是 --title-rim。 */
  --title-stroke: rgba(9, 36, 96, 0.95);
  --title-accent: rgba(10, 92, 118, 0.6);
  /* 字标投影：**2026-09-26 起没有消费者** —— 站主看到深色主题下那圈投影，
     原话是「我不需要黑边这个阴影」。来历：这份托底最早是一份 blur(20px) 的渐变副本
     （字标发糊的主因，09-25 去掉），09-25 到 09-26 之间由 `.title` 上的 text-shadow
     消费，09-26 一并去掉。对比度现在只交给 `.hero::before` 那层方向性压暗 ——
     和 Network 侧「大标题不再描投影」是同一套做法。
     数值留着备查：真要加回托底时**只能用 text-shadow，不能用 filter: drop-shadow()**，
     原因见 studio.css 的 `.title` 那段说明（会把整块字标变成纯色矩形）。 */
  --title-shadow: rgba(9, 25, 60, 0.22);
  /* 指针扫过时那圈边缘光的颜色（消费方见 studio.css 的 .title .ltr::after）。
     这一圈必须同时压得住色阶的两头，所以两套主题取的**不是同一个色相**：
     浅色主题下字是「青 → 藏蓝」，两头都偏深，用近白；
     深色主题下字是「近白 → 宝蓝」，近白的边在左端等于没画，所以改用饱和青。
     这是这块字标唯一需要按主题分别取色的地方。 */
  --title-rim: rgba(240, 251, 255, 0.95);
  /* 线框段的描边线宽：跟着字标字号走（0.045em —— 84px 下约 3.8px、40px 下 1.8px），
     两端各留 px 兜底。这个比例是照站主给的目标图量的：细到原来那档（≤2px）时，
     整块字只剩「描了边的字」，读不出两种处理。
     浅色主题这档略细一点：深墨压亮底的视觉重量比深色主题那圈近白描边更大。 */
  --t-stroke: clamp(1.7px, 0.042em, 3.5px);
  --t-off: clamp(1.2px, 0.25vw, 1.8px);

  --wash: linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0) 36%);
  --chip-bg: rgba(255, 255, 255, 0.55);
  --dot: #30d158;
  /* 暗角强度：亮色主题几乎不需要，深色主题才靠它把视线收到中间 */
  --vig: 0.26;
  /* 头像框叠在垫片上的那一层。亮色沿用标准玻璃档即可 */
  --avatar-edge: rgba(16, 34, 70, 0.18);
  --avatar-frame-fill: var(--glass-bg);
  /* Studio 的玻璃基材。个人站原本带 saturate(165%)，这里保留 ——
     它背后是一张静态的肯伯恩图片，不是 Network 那层一直播的视频，
     没踩「每个滤镜函数多一趟像素遍历」那条红线（见交接文档·性能） */
  --studio-glass-filter: blur(var(--glass-blur)) saturate(165%);

  /* ---------- 潮汐转场（世界切换的过场）----------
     两个世界是同一套美术里的海与天：Network 在深海（动态壁纸），
     Studio 在云端（亮蓝壁纸）。切换就是一次「下潜 / 上浮」，
     过场用一层带波峰的海水从一极扫到另一极，换页就发生在它盖住屏幕的那一瞬。

     配色在这里定义，crossing.js 只负责**按方向把变量名选出来**写到 <html> 上
     （`--tide-fill: var(--tide-body-to-studio)` 这种）。颜色值只此一份，
     改色不用翻 JS。 */
  /* 水体的颜色：两个方向共用同一片海，只是受光不同 */
  --tide-body-to-studio: #0b2a46;
  /* 比页面底色（#060a17）亮一点点：跟底色一样暗就读成「熄屏」而不是「水」了 */
  --tide-body-to-network: #08172e;
  /* 波峰的高光：后排浪往这个颜色偏一点，两层就分得开，
     像光从水面上方透下来。上浮偏冰青（天空漏下来的光），下潜偏 azure（海面反射） */
  --tide-crest-to-studio: rgba(111, 227, 255, 0.55);
  --tide-crest-to-network: rgba(88, 166, 255, 0.45);
  /* 满屏那一下中间的小字 */
  --tide-label: rgba(226, 240, 255, 0.92);
  /* 这里原本有 --ws-water-to-studio / -network 两个 token（世界切换胶囊里那汪水的颜色）。
     2026-09-25 那汪水整层撤掉了（原因见下方「世界切换入口」那一段），token 一并删除。 */

  /* 当前生效的取值。默认给「下潜」那一套：JS 没跑起来时也是一片像样的海，
     而不是一屏黑。 */
  --tide-fill: var(--tide-body-to-network);
  --tide-crest: var(--tide-crest-to-network);

  /* 一个周期的波。两次同向的贝塞尔：峰 100 宽、谷 100 宽，
     起点 (0,42) 与终点 (200,42) 的切线方向一致，所以平铺时接缝处是 C1 连续的
     —— 直接 repeat-x 也看不出拼缝，不需要额外做淡出遮罩。
     用 alpha 通道当 mask（CSS mask-mode 默认 match-source，图片走 alpha），
     所以填什么颜色都行，真正取的是形状。 */
  --tide-wave: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 100' preserveAspectRatio='none'%3E%3Cpath d='M0 42C25 6 75 6 100 42C125 78 175 78 200 42L200 100L0 100Z' fill='black'/%3E%3C/svg%3E");

  /* ---------- 尺寸 ---------- */
  --r-xs: 8px;
  --r-sm: 10px;
  --r-md: 16px;
  --r-lg: 22px;
  --r-xl: 30px;
  --r-full: 999px;

  --nav-h: 64px;
  --container: 1160px;

  /* ---------- 阴影：近实远虚的多层环境阴影 ---------- */
  --shadow-sm: 0 1px 2px rgba(13, 30, 66, 0.05),
    0 1px 6px rgba(13, 30, 66, 0.04);
  --shadow-md: 0 2px 6px rgba(13, 30, 66, 0.06),
    0 12px 28px -8px rgba(13, 30, 66, 0.12);
  --shadow-lg: 0 4px 12px rgba(13, 30, 66, 0.07),
    0 28px 64px -16px rgba(13, 30, 66, 0.18);

  /* ---------- 动效 ---------- */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  /* 大位移专用：起步快、收尾极慢，iOS 那种「滑到位」的手感。
     个人站的 Dock 吸附与卡片凝结走它 */
  --ease-ios: cubic-bezier(0.32, 0.72, 0, 1);

  /* 个人站原来叫 --text-2，语义和这里的 --text-dim 完全一样。
     合并时不留两份，别名指过去即可（主题切换后取值自动跟上） */
  --text-2: var(--text-dim);

  /* ---------- 字体 ---------- */
  --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
    "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC",
    "Source Han Sans SC", sans-serif;
  --font-mono: ui-monospace, "SF Mono", "Cascadia Code", Consolas,
    "Liberation Mono", monospace;
  --font-display: 'PixelLogo', ui-monospace, monospace;

}

/* ---------- 语义色 · 深色（默认） ---------- */
[data-theme="dark"] {
  color-scheme: dark;
  --bg: #060a17;
  --bg-elevated: #0a1024;
  --surface: #0b1126;
  --surface-2: #111a36;
  --surface-hover: #0f1730;
  /* 描边带一点蓝相，和 azure 主色同族，比纯白边更融合 */
  --border: rgba(150, 180, 255, 0.10);
  --border-strong: rgba(150, 180, 255, 0.18);

  --text: #ecf1fd;
  --text-dim: #9badcf;
  --text-faint: #6a7ca2;

  --brand: #58a6ff;
  --brand-hover: #7bbbff;
  --brand-fg: #04101f;
  --brand-soft: rgba(88, 166, 255, 0.13);
  --brand-glow: rgba(88, 166, 255, 0.32);
  /* 深色下的边缘光偏冰蓝：像光打在玻璃棱上的那一圈反光 */
  --edge-glow: rgba(195, 218, 255, 0.62);
  /* 深色下渐变靠拉开饱和度而不是明度，避免按钮在暗底上发灰 */
  --brand-hi: #7bbbff;
  --brand-lo: #3b8af0;

  --accent: #6fe3ff;
  /* 深色下的香槟金提亮到「发光但不刺眼」的档位 */
  --gold: #f0d5a2;
  --gold-soft: rgba(240, 213, 162, 0.14);
  --gold-glow: rgba(240, 213, 162, 0.30);
  --success: #34d399;
  --success-soft: rgba(52, 211, 153, 0.14);
  --danger: #f87171;

  /* 字标渐变：白 → 冰青 → 日冕青。白色起手是为了在暗底上有一处最亮的高光，
     整条渐变才有「金属反光」的走向，而不是一片均匀的蓝。 */
  --logo-grad: linear-gradient(120deg,
    #ffffff 4%,
    var(--brand-ice) 44%,
    var(--brand-cyan) 100%);

  /* 见 :root 里的说明。深色壁纸背后本来就暗，比浅色更需要「透」 */
  --nav-glass-scrolled: rgba(13, 19, 40, 0.60);

  /* 见 :root 里的说明 */
  --nav-menu-bg: rgba(13, 19, 40, 0.94);

  /* ---------- 磨砂玻璃 · 深色 ----------
     深色下用「深色乳浊染色」而不是白色染色：白染色会让发光壁纸顶上来压掉文字对比度。
     染色不透明度给足，颜色透进来是糊的而不是亮的。 */
  --glass-bg: rgba(13, 19, 40, 0.55);
  --glass-bg-strong: rgba(13, 19, 40, 0.80);
  --glass-border: rgba(150, 180, 255, 0.12);
  --glass-edge: rgba(180, 205, 255, 0.22);
  --glass-sheen: linear-gradient(180deg,
    rgba(190, 215, 255, 0.07) 0%,
    rgba(190, 215, 255, 0.015) 45%,
    rgba(88, 166, 255, 0.03) 100%);
  --glass-shadow: 0 2px 6px rgba(2, 5, 14, 0.35),
    0 16px 40px -8px rgba(2, 5, 14, 0.55);
  --glass-blur: 16px;
  --glass-inset: inset 0 1px 0 rgba(200, 220, 255, 0.12),
    inset 0 -1px 0 rgba(2, 5, 14, 0.35),
    inset 0 0 0 1px rgba(180, 205, 255, 0.05);
  --glass-grain: 0.038;

  --gradient-border: conic-gradient(from var(--gb-angle, 200deg) at 50% 0%,
    rgba(88, 166, 255, 0.85) 0deg,
    rgba(111, 227, 255, 0.35) 100deg,
    rgba(88, 166, 255, 0.12) 200deg,
    rgba(88, 166, 255, 0.85) 360deg);

  --ambient-veil: linear-gradient(180deg,
    rgba(6, 10, 23, 0.52) 0%,
    rgba(6, 10, 23, 0.22) 38%,
    rgba(6, 10, 23, 0.48) 100%);

  /* 见 :root 里的说明。深色下用子夜海蓝，右半边完全透明，壁纸质感不受影响 */
  --hero-scrim: linear-gradient(100deg,
    rgba(6, 10, 23, 0.84) 0%,
    rgba(6, 10, 23, 0.62) 30%,
    rgba(6, 10, 23, 0.22) 52%,
    transparent 72%);

  --hero-accent-grad: linear-gradient(165deg,
    #ffffff 6%,
    var(--brand-ice) 46%,
    var(--brand-cyan) 100%);

  --shadow-sm: 0 1px 2px rgba(2, 5, 14, 0.30),
    0 1px 6px rgba(2, 5, 14, 0.24);
  --shadow-md: 0 2px 8px rgba(2, 5, 14, 0.40),
    0 12px 32px -10px rgba(2, 5, 14, 0.50);
  --shadow-lg: 0 4px 16px rgba(2, 5, 14, 0.45),
    0 28px 72px -16px rgba(2, 5, 14, 0.65);

  /* ---------- Studio 世界 · 深色 ----------
     深色下的色阶整体提亮成「冰蓝 → 天青 → 天蓝 → 宝蓝」，
     所以它压在深色壁纸上才是发光的而不是糊的。
     两个坑写在这里免得下次踩：不要紫（AI 渐变标配）、不要薄荷绿（廉价感），
     深端也不能滑向灰蓝——明度一掉，在深蓝底上反而更糊。 */
  --grad: linear-gradient(115deg, #d4f9ff, #86dcf8 20%, #3fa4f2 50%, #3b74ee 71%, #2f57e0);
  /* 浅端是近白的冰蓝，白字压上去只剩 1.19:1，所以按钮另取深段（宝蓝起），
     实测 4.9~6.3:1。别把这条换回 var(--grad) */
  --grad-btn: linear-gradient(115deg, #376ee4, #2f57e0 62%, #2a4fd6);
  /* 线框段的描边颜色（2026-09-26 起又有了消费者：.title .ltr.t-line）。
     近白微蓝 —— 描边要压得住它背后的中蓝色天空，同时不能亮到与实心段的最浅端打架 */
  --title-stroke: rgba(216, 240, 255, 0.95);
  --title-accent: rgba(150, 226, 250, 0.8);
  /* 同浅色那份：2026-09-26 起没有消费者。深色下这条原本给到 .55 才托得住，
     站主仍然不要（他就是在深色下看到那圈黑边的）。数值留着备查 */
  --title-shadow: rgba(2, 8, 22, 0.55);
  /* 深色主题下字是「近白 → 宝蓝」，近白的边缘光得靠饱和度才看得见，不能也用近白 */
  --title-rim: rgba(41, 182, 232, 0.95);
  /* 深色主题下名字后半段是「近白描边、字心透出天空」，描边就是它唯一的形，
     所以这一档比浅色主题厚一点（见 :root 里那条说明） */
  --t-stroke: clamp(1.8px, 0.045em, 3.8px);

  /* ⚠️ 顶部这层压暗刻意给得很轻。深色主题下背景本身已经被 brightness(.78) 压过一道，
     再叠 0.3 的黑会在页面顶部糊出一条边界分明的暗带 —— 用户的反馈是
     「这一块大黑边和其他的不契合」。它原本的作用只是托一下固定的导航栏，
     而导航栏现在有自己的底色，不需要这么重。 */
  --wash: linear-gradient(180deg, rgba(8, 10, 22, 0.16), rgba(8, 10, 22, 0) 34%);
  --chip-bg: rgba(255, 255, 255, 0.09);
  /* 暗角强度保持个人站原值。中途我一度把它收到 .34 —— 当时测出「左上角被压掉
     40/255、角与心接近 4:1」，但那个数字把 ken-burns 壁纸的位移一起算进去了
     （两次对比截图之间壁纸一直在动）。冻住动画重测后，暗角的真实贡献只有
     13/255，而且只落在最外圈，**在首屏文字那一片的贡献是 0**。
     既然用户指的是「文字背后那块」，就不该顺手改这里，已还原成原值。 */
  --vig: 0.58;
  /* ⚠️ 这里曾经有一条 `--ambient-lift`（在壁纸上压一层半透明浅蓝，把左半边那团
     近黑云影托起来）。数值上确实有效（<60 的像素 16.6% → 3.8%），
     但**站主否决了这个做法**：等于给整张壁纸蒙了一层浅蓝，深色主题的质感被洗掉。
     2026-09-26 已撤掉，**别再往回加**。这条抱怨的完整来回见交接文档
     「Studio 壁纸那条大黑边」—— 想再动它，先读完那一节。 */
  --avatar-edge: rgba(255, 255, 255, 0.18);
  /* 暗色的框不能也叠深色：深叠深只会更暗、去饱和，摊不出层次。
     改叠一层偏白的霜，每叠一层更亮一档，才一眼看出是两块堆叠的玻璃 */
  --avatar-frame-fill: rgba(255, 255, 255, 0.06);

}

/* =========================================================================
   基础
   ========================================================================= */

*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; padding: 0; }

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
  /* 横向溢出在根上裁掉。注意这里必须用 hidden 而不是放在 body 上：
     body 一旦 overflow: hidden 会自己变成滚动容器，下面所有元素的
     view() / scroll() 时间线就会去追踪一个永远不会滚动的盒子，
     滚动驱动的动画全部失效（首屏溶解就是这么哑掉的）。 */
  overflow-x: hidden;
}

body {
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 1.65;
  color: var(--text);
  background: var(--bg);
  /* clip 不会创建滚动容器（hidden 会），横向溢出照样裁掉但不干扰滚动时间线 */
  overflow-x: clip;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  transition: background-color 220ms var(--ease-out), color 220ms var(--ease-out);
}

a { color: inherit; text-decoration: none; }
img { max-width: 100%; display: block; }

/* 触屏点按不要系统那格灰底：本站每个控件都自带 :active 反馈
   （Studio 那边本来就写了，Network 这边一直漏着 —— 手机上点链接会闪一格灰）。 */
html { -webkit-tap-highlight-color: transparent; }

/* 图片解码完之后淡入（.is-loading 由 public/scripts/interactions.js 加，
   只加在「确实还没解码完」的图上）。没有 JS 时这两个类都不存在 ⇒ 图照旧立刻显示，
   所以这是一条纯增强、没有「图看不见」的失败路径。
   过渡只挂在加了 data-fade 的图上，不去碰别的元素。 */
img[data-fade] { transition: opacity 0.45s var(--ease-out); }
img.is-loading { opacity: 0; }

button {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  cursor: pointer;
}

code, kbd, samp { font-family: var(--font-mono); }

::selection { background: var(--brand); color: var(--brand-fg); }

:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 3px;
  border-radius: var(--r-xs);
}

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--border-strong);
  border-radius: var(--r-full);
  border: 2px solid var(--bg);
}
::-webkit-scrollbar-thumb:hover { background: var(--text-faint); }

/* =========================================================================
   布局
   ========================================================================= */

.main-content {
  position: relative;
  z-index: 1;
  min-height: 100vh;
}

.page-container {
  width: 100%;
  max-width: var(--container);
  margin: 0 auto;
  padding: 0 24px 100px;
}

.page-content {
  display: flex;
  flex-direction: column;
  gap: 72px;
}

.section-head { margin-bottom: 28px; }

.section-title {
  font-size: clamp(1.75rem, 2.6vw, 2.25rem);
  font-weight: 720;
  letter-spacing: -0.03em;
  color: var(--text);
  text-wrap: balance;
}

.section-description {
  margin-top: 8px;
  color: var(--text-dim);
  font-size: 0.95rem;
  max-width: 60ch;
}

/* 带编号的分节标题 */
.section-index {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  color: var(--brand);
  margin-bottom: 12px;
}

.section-index::before {
  content: "";
  width: 18px;
  height: 1px;
  background: currentColor;
  opacity: 0.6;
}

/* =========================================================================
   氛围层：壁纸 + 遮罩 + 点阵 + 颗粒
   整页固定，不随滚动移动。毛玻璃要「有东西可虚化」才成立，
   所以这一层必须在所有卡片背后一直存在。
   ========================================================================= */

.ambient {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
}

/* 全屏动态壁纸：整页固定，不随滚动移动 */
.ambient-video,
.ambient-image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 34%;
  /* 单独提成合成层，避免整页重绘 */
  will-change: transform;
  backface-visibility: hidden;
}

.ambient-image {
  background-size: cover;
  background-position: center 34%;
  background-repeat: no-repeat;
}

/* ---------- 滚动驱动的空间感 ----------
   壁纸推近和首屏溶解由 interactions.js 的阻尼循环驱动（initScrollFeel）：
   原生滚轮是一格一格的，直接把滚动位置喂给动画会跟着一格一格跳；
   先经过阻尼平滑再写 transform，才是「滑」进去的。
   这里只保留 will-change；不在这里用 CSS 滚动时间线，因为它跟不了滚轮的步进。 */

/* 遮罩：壁纸要看得见，但正文和磨砂面板必须压得住。
   这一层是静态渐变，不参与逐帧计算；真正贵的是壁纸本身的解码。 */
.ambient-veil {
  position: absolute;
  inset: 0;
  background: var(--ambient-veil);
}

/* （原来的点阵网格和噪点层已移除：视频自带质感和颗粒，
   再叠只会发灰。遮罩是壁纸上方唯一的一层。） */

/* 兜底：不支持 backdrop-filter 时给不透明底色，避免面板变全透明看不清 */
@supports not ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
  :root {
    --glass-bg: rgba(255, 255, 255, 0.94);
    --glass-bg-strong: rgba(255, 255, 255, 0.97);
    --nav-glass-scrolled: rgba(255, 255, 255, 0.97);
  }
  [data-theme="dark"] {
    --glass-bg: rgba(10, 16, 36, 0.94);
    --glass-bg-strong: rgba(10, 16, 36, 0.97);
    --nav-glass-scrolled: rgba(10, 16, 36, 0.97);
  }
}

/* ---------- 偏好：减少透明 / 更高对比 ----------
   §14 里那两条「用户主动开启才生效」的偏好，只在开启时命中，所以可以写足。
   ⚠️ 深色那份必须单独写 [data-theme="dark"]：它比 :root 特异性高，
   只写一份的话深色下不生效（和下面 blur 半径那条是同一个坑）。 */
@media (prefers-reduced-transparency: reduce) {
  :root {
    --glass-bg: rgba(255, 255, 255, 0.98);
    --glass-bg-strong: rgba(255, 255, 255, 0.99);
    --nav-glass-scrolled: rgba(255, 255, 255, 0.99);
  }
  [data-theme="dark"] {
    --glass-bg: rgba(10, 16, 36, 0.98);
    --glass-bg-strong: rgba(10, 16, 36, 0.99);
    --nav-glass-scrolled: rgba(10, 16, 36, 0.99);
  }
  /* 底色已经不透了，模糊再留着就是纯烧 GPU。
     通配 + !important 是刻意的：玻璃面散在三个文件里（本表、Navigation.astro、
     studio.css），一个个列一定会漏 —— 这条例外值得，因为它的语义就是「全都别透」。 */
  *, *::before, *::after {
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
  }
}

@media (prefers-contrast: more) {
  :root {
    --border: rgba(13, 30, 66, 0.30);
    --border-strong: rgba(13, 30, 66, 0.46);
    --glass-border: rgba(13, 30, 66, 0.28);
    --glass-bg: rgba(255, 255, 255, 0.96);
    --glass-bg-strong: rgba(255, 255, 255, 0.98);
  }
  [data-theme="dark"] {
    --border: rgba(236, 241, 253, 0.30);
    --border-strong: rgba(236, 241, 253, 0.46);
    --glass-border: rgba(190, 210, 255, 0.30);
    --glass-bg: rgba(10, 16, 36, 0.96);
    --glass-bg-strong: rgba(10, 16, 36, 0.98);
  }
}

/* 移动端 blur 半径降一档：高斯模糊是 GPU 开销大户。
   注意 [data-theme="dark"] 的特异性比 :root 高，两行都得写在媒体查询里，
   否则默认的深色模式下手机上还是按桌面半径在模糊。 */
@media (max-width: 768px) {
  :root { --glass-blur: 12px; }
  [data-theme="dark"] { --glass-blur: 12px; }
}

/* =========================================================================
   首屏
   ========================================================================= */

.hero {
  position: relative;
  padding: calc(var(--nav-h) + 88px) 0 88px;
  /* clip 而不是 hidden：这里裁的是水印大字，
     但 hidden 会把 .hero 变成滚动容器，首屏溶解的 view() 时间线
     就会去追踪 hero 自己（它永远不滚），动画直接哑掉 */
  overflow: clip;
}

/* 首屏的压暗层：横向渐隐 + 纵向到 62% 就开始化开，
   免得在 hero 底边留下一道横向的硬接缝。

   尺寸刻意只罩住文字那一块，不铺满整个 hero —— 这是一层 alpha 混合，
   成本按面积走。渐变在 72% 处已经全透明，所以元素边界看不出来。 */
.hero::before {
  content: "";
  position: absolute;
  inset: 0 auto auto 0;
  width: min(100%, 880px);
  height: min(100%, 640px);
  z-index: 0;
  background: var(--hero-scrim);
  mask-image: linear-gradient(180deg, #000 0%, #000 62%, transparent 98%);
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 62%, transparent 98%);
  pointer-events: none;
}

.hero-watermark {
  position: absolute;
  left: 50%;
  bottom: -0.14em;
  transform: translateX(-50%);
  font-family: var(--font-display);
  font-size: clamp(90px, 20vw, 280px);
  line-height: 0.8;
  letter-spacing: 0.02em;
  white-space: nowrap;
  color: var(--text);
  opacity: 0.045;
  pointer-events: none;
  user-select: none;
  z-index: 0;
}

.hero-inner {
  position: relative;
  z-index: 2;
}

/* 首屏溶解（随滚动淡出上移）也由 initScrollFeel 驱动，理由同上。
   只写 opacity 和 transform：都是合成器属性，静止即停帧。 */

/* ---------- 页面切换的过渡 ----------
   Astro 客户端路由换页时会在 <html> 上挂 data-astro-transition，
   只在路由跳转时存在 —— 所以这里的样式不会碰主题切换的圆形揭幕。
   旧页轻轻上浮、新页从下方接住，比纯交叉淡入更有「往前走」的方向感。 */
html[data-astro-transition]::view-transition-old(root) {
  animation: page-out 200ms var(--ease-out) both;
}

html[data-astro-transition]::view-transition-new(root) {
  animation: page-in 320ms var(--ease-out) both;
}

@keyframes page-out {
  to { opacity: 0; transform: translate3d(0, -10px, 0); }
}

@keyframes page-in {
  from { opacity: 0; transform: translate3d(0, 16px, 0); }
}

/* 用户要求减少动效：跳转就是跳转，别加戏 */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: 1ms !important;
  }
}

/* 眉毛标签：不再是一颗玻璃胶囊 —— 那在壁纸上是一块多余的深色圆角块。
   现在只是一行带小方块前缀的标签，字距拉开一点，像产品页的 kicker。 */
.hero-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--brand);
}

/* 纯装饰的小方块，不代表在线状态（真状态是首页的胶囊） */
.hero-eyebrow .eyebrow-dot {
  width: 6px;
  height: 6px;
  flex-shrink: 0;
  border-radius: 1px;
  background: var(--brand);
}

.hero-title {
  margin: 24px 0 0;
  font-size: clamp(2.75rem, 7vw, 5rem);
  font-weight: 760;
  line-height: 1.02;
  letter-spacing: -0.045em;
  color: var(--text);
  text-wrap: balance;
}

/* 大标题不再描投影：对比度交给 .hero::before 那层方向性压暗。
   之前每个字外面套一圈 24px 的深色投影，压在亮云上就是一团脏边，
   这正是「看着廉价」的来源。副文案字小，留一层很轻的兜底。 */
[data-theme="dark"] .hero-sub,
[data-theme="dark"] .section-title,
[data-theme="dark"] .section-description {
  text-shadow: 0 1px 12px rgba(2, 5, 14, 0.45);
}

/* 浅色下副文案是深灰字，壁纸亮起来之后可能压在深色云层上，
   给它一圈白晕才压得住；幽灵按钮同理 */
[data-theme="light"] .hero-sub,
[data-theme="light"] .hero-actions .btn-ghost {
  text-shadow: 0 1px 2px rgba(255, 255, 255, 0.9), 0 0 18px rgba(255, 255, 255, 0.6);
}

/* 逐字入场：普通字由 Hero.astro 拆好、按 --ch-i 排队；
   <em> 强调词在 Hero.astro 里作为整体最后落下。
   inline-block 才能带着位移动起来；间距靠字距本身，不受影响。 */
.hero-title .ch {
  display: inline-block;
  opacity: 0;
  transform: translate3d(0, 0.42em, 0);
  animation: char-in 620ms var(--ease-spring) forwards;
  animation-delay: calc(140ms + var(--ch-i, 0) * 42ms);
}

@keyframes char-in {
  to { opacity: 1; transform: translate3d(0, 0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .hero-title .ch {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

/* 标题里的高亮词：白 → 冰青 → 日冕青的渐变字。
   em 本身带着入场位移，但文字是它的直接内容，background-clip: text
   照常绘制；被截断的只会是「被变换的子元素」—— 那个坑在状态卡的
   滚轮数字上踩过（见 .status-card .stat-value）。 */
.hero-title em {
  font-style: normal;
  position: relative;
  white-space: nowrap;
  background-image: var(--hero-accent-grad);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* 副文案：字号上调一档、颜色提到正文的 84% —— 之前是 --text-dim 的中灰小字，
   压在花哨的壁纸上几乎读不出来（写死冰青的话浅色主题下又看不见了，
   所以用 color-mix 从 --text 兑，两套主题自动都成立）。
   宽度 38ch 约 20 个汉字一行，刚好两行；注意 ch 是按「0」的宽度算的，
   汉字比它宽得多。 */
.hero-sub {
  margin-top: 20px;
  max-width: 38ch;
  font-size: 1.125rem;
  line-height: 1.72;
  color: color-mix(in srgb, var(--text) 84%, transparent);
  text-wrap: pretty;
}

/* 中文没有词间空格，两行时让断点更均匀 */
@supports (text-wrap: balance) {
  .hero-sub { text-wrap: balance; }
}

.hero-actions {
  margin-top: 36px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
}

/* 状态胶囊 */
.status-pill {
  position: absolute;
  right: 0;
  bottom: 64px;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 11px 18px;
  border-radius: var(--r-full);
  background-color: var(--glass-bg-strong);
  background-image: var(--glass-sheen);
  border: 1px solid var(--glass-border);
  box-shadow: var(--glass-shadow), var(--glass-inset);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  font-size: 0.875rem;
  font-weight: 550;
}

@media (max-width: 900px) {
  .status-pill { position: static; margin-top: 28px; }
}

/* 滚动提示 */
.scroll-hint {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
  font-size: 0.8125rem;
  color: var(--text-faint);
}

.scroll-hint .mdi {
  animation: nudge 2s var(--ease-in-out) infinite;
}

@keyframes nudge {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(4px); }
}

/* =========================================================================
   状态灯
   ========================================================================= */

.status-dot {
  position: relative;
  width: 8px;
  height: 8px;
  border-radius: var(--r-full);
  background: var(--text-faint);
  flex-shrink: 0;
}

.status-dot.online {
  background: var(--success);
}

/* 在线时的雷达脉冲：这是「状态」本身，不是装饰 */
.status-dot.online::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--success);
  animation: radar 2.4s var(--ease-out) infinite;
}

@keyframes radar {
  0% { transform: scale(1); opacity: 0.7; }
  70%, 100% { transform: scale(3); opacity: 0; }
}

/* 页面切到后台就别闪了（在线脉冲和空态呼吸一起停） */
.tab-hidden .status-dot.online::after,
.tab-hidden .status-dot.idle::after {
  animation-play-state: paused;
}

/* =========================================================================
   主题切换按钮的图标：浅色显月亮（点了变深色），深色显太阳。
   必须是全局样式 —— 组件的 scoped 样式会给 [data-theme] 祖先也加
   作用域标记，而 data-theme 在 <html> 上没有标记，规则永远匹配不上，
   按钮就会变成看不见图标的空壳。
   ========================================================================= */

.theme-icon-light,
.theme-icon-dark {
  display: none;
}

[data-theme="light"] .theme-icon-light {
  display: block;
}

[data-theme="dark"] .theme-icon-dark {
  display: block;
}

.status-dot.offline {
  background: var(--danger);
}

/* 服务器活着，但没人在线 —— 不闪，也不报警 */
.status-dot.idle {
  background: var(--brand);
  opacity: 0.45;
}

/* 空态的呼吸：比在线脉冲慢一倍、轻得多，表达「在等」而不是「有事发生」 */
.status-dot.idle::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--brand);
  animation: breathe 4.2s var(--ease-out) infinite;
}

@keyframes breathe {
  0% { transform: scale(1); opacity: 0.5; }
  70%, 100% { transform: scale(2.6); opacity: 0; }
}

.status-dot.stale {
  background: var(--text-faint);
}

/* =========================================================================
   Bento 栅格
   ========================================================================= */

.bento {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 16px;
}

.bento > * { min-width: 0; }

.col-2 { grid-column: span 2; }
.col-3 { grid-column: span 3; }
.col-4 { grid-column: span 4; }
.col-6 { grid-column: span 6; }

@media (max-width: 900px) {
  .bento { grid-template-columns: repeat(2, 1fr); }
  .col-2, .col-3, .col-4, .col-6 { grid-column: span 2; }
}

/* =========================================================================
   卡片 —— 毛玻璃面板
   半透明染色 + 背景虚化提饱和 + 顶部受光边 + 分层内阴影
   ========================================================================= */

.card {
  position: relative;
  border-radius: var(--r-lg);
  border: 1px solid var(--glass-border);
  background-color: var(--glass-bg);
  background-image: var(--glass-sheen);
  /* saturate 让壁纸的 azure/冰青透进来时保持发光感，而不是灰成一片 */
  backdrop-filter: blur(var(--glass-blur));
  -webkit-backdrop-filter: blur(var(--glass-blur));
  box-shadow: var(--glass-shadow), var(--glass-inset);
  padding: 24px;
  overflow: hidden;
  /* 只过渡 border-color（重绘很小）和 transform（走合成器）。
     不过渡 box-shadow —— 毛玻璃元素外层套着 backdrop-filter，
     阴影一旦逐帧插值就等于整块面板逐帧重绘。 */
  transition: border-color 220ms var(--ease-out),
    transform 300ms var(--ease-out);
}

/* 磨砂面：一层很淡的噪点。这是「磨砂」和「透明」的分界线，
   没有它面板再模糊也只是一块半透明色块。 */
.card::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  border-radius: inherit;
  opacity: var(--glass-grain);
  background-image: var(--noise);
  background-size: 160px 160px;
}

@media (hover: hover) and (pointer: fine) {
  .card:hover {
    border-color: var(--glass-edge);
  }
  /* 上浮 3px 单独写一条并排除平台卡：那张卡的 transform 归 3D 视差（见 download.astro），
     同特异性的悬停规则一旦压上去，倾斜在悬停时会被整条替换掉 —— 表现为「鼠标一进去
     倾斜就没了」，而且只在悬停时没、松手又回来，最难查。
     :where() 计 0 特异性，所以上面那条仍然是 (0,2,0)，其它卡片的行为一个字没变。 */
  .card:hover:where(:not(.platform-card)) {
    transform: translateY(-3px);
  }
}

/* 跟随鼠标的边缘光：由 interactions.js 设置 --mx / --my。
   之前是把一团光晕泼在卡片内部，容易读成「发光贴纸」；
   现在用双层 mask 把光压进 1px 的边框里，指针走到哪，
   玻璃的棱就被光描到哪 —— 这才是玻璃该有的反光方式。
   注意 mask-composite 必须写在 mask 简写之后（简写会把它重置）。 */
.card--glow::after {
  content: "";
  position: absolute;
  inset: 0;
  padding: 1px;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  transition: opacity 340ms var(--ease-out);
  background: radial-gradient(
    260px circle at var(--mx, 50%) var(--my, 0%),
    var(--edge-glow),
    transparent 70%
  );
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
}

@media (hover: hover) and (pointer: fine) {
  .card--glow:hover::after { opacity: 1; }
}

/* 渐变描边：填充层裁进 padding-box，渐变层留在 border-box，
   描边自然透出 conic 渐变；不用伪元素，和 .card 的噪点/高光互不打架 */
.card--gradient-border {
  --gb-angle: 200deg;
  border-color: transparent;
  background-color: transparent;
  background-image:
    var(--glass-sheen),
    linear-gradient(var(--glass-bg), var(--glass-bg)),
    var(--gradient-border);
  background-origin: border-box;
  background-clip: padding-box, padding-box, border-box;
  transition: border-color 220ms var(--ease-out),
    transform 300ms var(--ease-out),
    --gb-angle 900ms var(--ease-out);
}

/* 悬停时让光沿描边走半圈。@property 注册过的角度才能平滑过渡；
   不支持的浏览器会瞬时跳 360deg 的整数倍，视觉上等于没跳，安全兜底 */
@media (hover: hover) and (pointer: fine) {
  .card--gradient-border:hover { --gb-angle: 560deg; }
}

@property --gb-angle {
  syntax: '<angle>';
  inherits: false;
  initial-value: 200deg;
}

/* 重玻璃：导航栏、悬浮面板这类要压住复杂壁纸的位置用，
   染色更不透、模糊更足，代价是更「闷」，卡片别用 */
.glass-strong {
  background-color: var(--glass-bg-strong);
  background-image: var(--glass-sheen);
  border: 1px solid var(--glass-border);
  backdrop-filter: blur(var(--glass-blur));
  -webkit-backdrop-filter: blur(var(--glass-blur));
  box-shadow: var(--glass-shadow), var(--glass-inset);
}

.card-label {
  display: block;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: 14px;
}

/* 大数字 */
.stat-value {
  font-size: 1.75rem;
  font-weight: 680;
  letter-spacing: -0.03em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

.stat-value-lg {
  font-size: clamp(2rem, 4vw, 2.75rem);
}

/* =========================================================================
   按钮
   ========================================================================= */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 11px 20px;
  border-radius: var(--r-sm);
  font-size: 0.9375rem;
  font-weight: 560;
  line-height: 1.4;
  white-space: nowrap;
  cursor: pointer;
  border: 1px solid transparent;
  /* transform 走 spring：按下快、松手带回弹，是「有弹性」的关键 */
  transition: background-color 160ms var(--ease-out),
    border-color 160ms var(--ease-out), color 160ms var(--ease-out),
    box-shadow 200ms var(--ease-out), transform 260ms var(--ease-spring);
}

.btn:active { transform: scale(0.96); }

.btn-primary {
  position: relative;
  overflow: hidden;
  /* 三段立体渐变：顶端受光、中段主色、底端压深，按钮在壁纸上才有体积感 */
  background: linear-gradient(180deg,
    var(--brand-hi) 0%,
    var(--brand) 46%,
    var(--brand-lo) 100%);
  color: var(--brand-fg);
  border-color: transparent;
  /* 常驻一层柔和的 azure 环境光，贴合主色的「发光感」；
     hover 只改亮度和位移，不动阴影——避免逐帧重绘阴影 */
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.30),
    inset 0 -1px 0 rgba(2, 8, 24, 0.16),
    0 8px 24px -10px var(--brand-glow);
  transition: background-color 160ms var(--ease-out),
    color 160ms var(--ease-out), filter 180ms var(--ease-out),
    transform 260ms var(--ease-spring);
}

/* 悬浮光泽扫过：整条斜向高光从左滑到右。
   只过渡 transform/opacity，重绘范围限在这枚伪元素自己的合成层 */
.btn-primary::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(105deg,
    transparent 35%,
    rgba(255, 255, 255, 0.38) 50%,
    transparent 65%);
  transform: translateX(-110%);
  opacity: 0;
  transition: transform 620ms var(--ease-out), opacity 200ms var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .btn-primary:hover {
    filter: brightness(1.08);
    transform: translateY(-1px);
  }
  .btn-primary:hover::before {
    transform: translateX(110%);
    opacity: 1;
  }
}

/* 按下时扫光让位，避免和缩放叠在一起显得闪 */
.btn-primary:active::before { opacity: 0; }

.btn-secondary {
  background-color: var(--glass-bg-strong);
  background-image: var(--glass-sheen);
  color: var(--text);
  border-color: var(--glass-border);
  box-shadow: var(--glass-inset);
}

@media (hover: hover) and (pointer: fine) {
  .btn-secondary:hover {
    background-color: var(--glass-bg-strong);
    border-color: var(--glass-edge);
  }
}

.btn-ghost {
  background: transparent;
  color: var(--text-dim);
  border-color: transparent;
}

@media (hover: hover) and (pointer: fine) {
  .btn-ghost:hover {
    background-color: var(--glass-bg-strong);
    color: var(--text);
  }
}

.btn-sm { padding: 7px 13px; font-size: 0.875rem; gap: 6px; }
.btn-sm .mdi { font-size: 1rem; }
.btn-lg { padding: 14px 26px; font-size: 1rem; border-radius: var(--r-md); }

/* =========================================================================
   徽标
   ========================================================================= */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: var(--r-full);
  font-size: 0.8125rem;
  font-weight: 550;
  line-height: 1.5;
  background-color: var(--glass-bg);
  border: 1px solid var(--glass-border);
  box-shadow: var(--glass-inset);
  color: var(--text-dim);
}

.badge-success { background-color: var(--success-soft); border-color: transparent; box-shadow: none; color: var(--success); }
.badge-brand { background-color: var(--brand-soft); border-color: transparent; box-shadow: none; color: var(--brand); }
/* 金色徽标：全站最高级的点缀，只给第一名这类位置 */
.badge-gold { background-color: var(--gold-soft); border-color: transparent; box-shadow: none; color: var(--gold); }

/* =========================================================================
   图标
   ========================================================================= */

.mdi { line-height: 1; vertical-align: -0.125em; }

/* =========================================================================
   入场动效
   ========================================================================= */

.reveal,
.reveal-scale {
  transition: opacity 620ms var(--ease-out), transform 620ms var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}

/* 只有 JS 正常时才先藏起来——脚本挂了也要能读到内容 */
/* ⚠️ 这三条必须排除「自己要用 transform 做交互」的元素：
   账户卡 .acct、服务器大卡 .server-frame（倾斜 + 视差）、下载页的平台卡 .platform-card
   （2026-09-26 也换成了同一套 3D 视差，于是第三次踩进这个坑）。
   用 :where() 包住 :not() 是为了让特异性一个字都不变（:where() 计 0 特异性），
   所以还是原来的 (0,2,0) / (0,3,0) —— .dock 和 .identity 自己没有 transform，
   入场完全靠这两条，不能受牵连。
   为什么要排除：这三类元素的入场走的是独立属性 translate + opacity
   （见 studio.css 的 .js .acct.reveal / acctFade 与 .server-frame.reveal，
   以及 download.astro 的 .js .platform-card.reveal），
   可这两条写的是 transform、特异性 (0,3,0)、又排在 .acct:hover (0,2,0) 之后 ——
   于是元素一挂上 .in，transform 就被永久钉死在 translateY(0) 上。
   后果：JS 一直在算 --rx/--ry/--px/--py，但 3D 倾斜一次都没渲染出来。
   （实测：悬停时 box-shadow 已经是悬停值、transform 却还是恒等矩阵。）
   ⚠️ 注意 data-tilt 的元素不受影响 —— 它由 JS 写**内联** transform，内联优先于样式表。
   ⚠️ 下次再给某个卡片加 3D 倾斜，第一件事就是往这个列表里补一个 :not()。 */
.js .reveal:where(:not(.acct):not(.server-frame):not(.platform-card)) {
  opacity: 0;
  transform: translateY(18px);
}

.js .reveal.in:where(:not(.acct):not(.server-frame):not(.platform-card)) {
  opacity: 1;
  transform: none;
}

/* 缩放变体：上浮幅度更小、带一点放大，适合卡片/大面板，
   比纯位移更有「浮现」感；幅度刻意克制，大了会晕 */
.js .reveal-scale {
  opacity: 0;
  transform: translateY(12px) scale(0.965);
}

.js .reveal-scale.in {
  opacity: 1;
  transform: none;
}

.fade-in { animation: fadeIn 560ms var(--ease-out) both; }

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(12px); }
  to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  .js .reveal,
  .js .reveal-scale { opacity: 1; transform: none; }
}

/* =========================================================================
   主题切换：从按钮扩散的圆形揭幕（theme.js）
   ========================================================================= */

::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}

/* 新的一层盖在上面，由 JS 用 clip-path 揭开 */
::view-transition-old(root) { z-index: 1; }
::view-transition-new(root) { z-index: 2; }

/* =========================================================================
   响应式
   ========================================================================= */

@media (max-width: 768px) {
  .page-container { padding: 0 16px 72px; }
  .page-content { gap: 48px; }
  .card { padding: 20px; border-radius: var(--r-md); }
  .section-title { font-size: 1.375rem; }
  .hero { padding: calc(var(--nav-h) + 56px) 0 52px; }
  .hero-actions { gap: 10px; }
  /* 首屏按钮降一档，免得一屏放不下还要占两行 */
  .hero-actions .btn-lg { padding: 11px 18px; font-size: 0.9375rem; border-radius: var(--r-sm); }
  .bento { gap: 12px; }
  /* 触控区补足：小按钮在手机上按 44px 给（原本 36px），
     状态卡的「刷新」和排行榜的展开/收起都受益 */
  .btn-sm { padding: 11px 15px; }
}

/* =========================================================================
   潮汐转场 —— 两个世界之间的过场
   -------------------------------------------------------------------------
   合并后的站点有两个「世界」，它们是同一套美术里的海与天：

     Network 世界（/、/download…）  深海。全屏动态壁纸，azure 冷光
     Studio  世界（/studio）        云端。亮蓝天空壁纸，个人名片

   所以在两者之间走一趟，最贴切的过场就是**一次海水的涨落**：
   一层带波峰的海水从一极扫到另一极，把旧页面冲走、把新页面送上来，
   而**换页恰好发生在海水盖满屏幕的那一瞬** —— 用户永远看不到白屏或闪烁。

   两个方向语义相反，用同一套机制实现，只是 translateY 的起止对调：

     → Studio  上浮：海水从上方压下来盖住，再从下方退走（水面下沉过镜头）
     → Network 下潜：海水从下方涌上来盖住，再从上方退去（水面上升过镜头）

   成本：整层只动 transform（合成器线程），波峰用 mask 平铺而不是 SVG 变形，
   所以全程没有重排、没有逐帧重绘。空闲时 visibility:hidden，成本为零。
   ========================================================================= */

.tide {
  position: fixed;
  inset: 0;
  z-index: 9000;
  pointer-events: none;
  /* clip 而不是 hidden：只裁剪，不创建滚动容器（见「滚动时间线会失效」那条坑） */
  overflow: clip;
  /* 用 layout/paint 隔离，不用 size —— size 隔离会让它不再按 inset 撑满 */
  contain: layout paint style;
  /* 空闲态彻底移出合成与命中测试 */
  visibility: hidden;
}

.tide[data-armed="1"] {
  visibility: visible;
}

/* ---------- 抵达态（整页导航刚落地的那一帧）----------
   新文档必须**第一帧就是盖住的**，否则会先闪一下内容再被海水压住 ——
   那一闪就是全站最容易露馅的地方。这里用了两层保险：

   ① 根元素和 body 直接铺水体色。它们跟文档一起绘制，**不依赖任何一个
      元素被解析到**，所以哪怕 HTML 还在流式下载、只在最开头绘制了一帧，
      那一帧也已经是一片海水。只铺 <html> 不够 —— body 有自己的不透明
      底色（各个页面的 body background），会盖在上面。
   ② 海水层本身也立刻摆到「盖住」的位置，等它被解析到就接上（波峰这时候
      在屏幕外，所以和①的纯色无缝衔接）。

   <head> 里的内联脚本在 body 之前挂上 .tide-cold；crossing.js（打包模块，
   解析完才执行）接上后先开动画、再摘掉这个类 —— 顺序不能反，
   摘早了 #1 的纯色会消失、露出还没被波峰盖住的页面。 */
html.tide-cold,
html.tide-cold body {
  background-color: var(--tide-fill);
}

html.tide-cold .tide { visibility: visible; }
html.tide-cold .tide-sheet { transform: translateY(0); }
html.tide-cold .tide-label { opacity: 0; }

/* 波浪的一层：把「一个周期的波」用 mask 平铺，再整体横向平移一个周期做无缝循环。
   用 mask 而不是 background-image，是因为水体颜色要跟着方向换 ——
   mask 拿形状、background-color 拿颜色，改色只动一个变量。 */
.tide-edge__roll {
  position: absolute;
  left: 0;
  top: 0;
  height: 100%;
  width: 200%;
  background-color: var(--tide-fill);
  -webkit-mask-image: var(--tide-wave);
  mask-image: var(--tide-wave);
  -webkit-mask-repeat: repeat-x;
  mask-repeat: repeat-x;
  -webkit-mask-position: bottom;
  mask-position: bottom;
  animation: tideDrift 11s linear infinite;
  /* 空闲时不空转。这四条是 infinite 动画，一直跑就是一直占合成器 ——
     只在过场期间开。 */
  animation-play-state: paused;
}

.tide[data-armed="1"] .tide-edge__roll {
  animation-play-state: running;
}

/* 泳层：宽度是 sheet 的两倍、每个周期占其 25%，于是平移自身 25% 正好一个周期，
   首尾无缝。位移是 transform，走合成器。 */
@keyframes tideDrift {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(-25%, 0, 0); }
}

/* 承载整块海水的移动层。translateY 由 crossing.js 用 WAAPI 驱动 —— 
   之所以不在 CSS 里写 transition，是因为换页那一刻它要「停在原地」，
   而客户端路由的换页本身不是可以 await 的 CSS 过渡。 */
.tide-sheet {
  position: absolute;
  inset: 0;
  /* 空闲位置：完全推到屏幕下方，任何情况下都不会误遮内容 */
  transform: translateY(101%);
}

.tide[data-armed="1"] .tide-sheet {
  will-change: transform;
}

/* 水体本身 */
.tide-sheet::before {
  content: "";
  position: absolute;
  inset: 0;
  background-color: var(--tide-fill);
}

/* ---------- 波峰带 ----------
   贴在移动层的前后两条边上。两条边都要有波峰，因为「扫过来」和「退走」
   用的是不同的那条边：进场看前缘，退场看后缘。 */
.tide-edge {
  position: absolute;
  left: -12%;
  width: 124%;
  height: clamp(86px, 13vh, 168px);
  overflow: clip;
  pointer-events: none;
}

.tide-edge--top { bottom: 100%; }
.tide-edge--bottom { top: 100%; }

/* 上缘的波朝上长，下缘的波朝下长 —— 同一个形状翻过来用，
   免得为两个方向各画一份 mask */
.tide-edge--bottom { transform: scaleY(-1); }

.tide-edge__sheet {
  position: absolute;
  inset: 0;
  overflow: clip;
}

/* 后排浪：周期更长、幅度更大、半透明，给前景浪垫出深度。
   ⚠️ 这两条是调出来的，不是随手写的：
   · top 别给 0（整条波峰带的高度都吃满）—— 那样它的浪在宽屏上会摊成一个
     巨大的圆弧，看着像色块而不是水。压到 18% 后它仍比前排浪高，层次还在。
   · mask-size 是「一个波长的宽度占整卷的百分比」，卷宽是波峰带的 200%，
     所以 26% ≈ 视口宽的一半一个波长。再宽就回到上面那个「大圆弧」了。 */
.tide-edge__sheet--back {
  top: 18%;
  opacity: 0.45;
}

/* 后排浪亮一档：让两层分得开，也像光从水面上方透下来。
   用 color-mix 而不是写死颜色，配色跟着 --tide-fill / --tide-crest 一起变。 */
.tide-edge__sheet--back .tide-edge__roll {
  background-color: color-mix(in srgb, var(--tide-fill) 72%, var(--tide-crest) 28%);
  -webkit-mask-size: 26% 100%;
  mask-size: 26% 100%;
  animation-duration: 17s;
  animation-direction: reverse;
}

/* 前排浪：周期短、贴着水体边，是真正的那条水线 */
.tide-edge__sheet--front {
  top: 34%;
}

.tide-edge__sheet--front .tide-edge__roll {
  -webkit-mask-size: 21% 100%;
  mask-size: 21% 100%;
}

/* ---------- 过场标题 ----------
   海水盖住屏幕的那一段时间里，中间用很小的字说明「正在去哪」。
   它让这一秒从「卡了一下」变成「有意为之」。 */
.tide-label {
  position: absolute;
  left: 50%;
  top: 50%;
  margin: 0;
  /* 只动 transform/opacity，位置由 WAAPI 之外的一条 CSS 变量控制语义方向 */
  transform: translate(-50%, -50%);
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  letter-spacing: 0.34em;
  /* letter-spacing 会在右端多留一个字距，补回来才是视觉居中 */
  text-indent: 0.34em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--tide-label);
  opacity: 0;
}

/* ---------- （已删除）转场进行中关掉整页交叉淡入 ----------
   这里原本有两条 html.tide-active::view-transition-* 规则，
   把 root 快照的 opacity 压到 0，好让「海水冲过去」期间不叠加一次页面淡出淡入。
   但它们**是死的** —— 全仓库搜不到任何地方给 <html> 加 tide-active 类。
   原因是过场后来改成了整页导航（crossing.js 里 depart() 用 location.assign），
   根本不会产生 View Transition，也就没有快照需要压掉。
   这套是「客户端路由版过场」的遗留。哪天真要回到客户端路由，
   记得同时把加类的代码和这两条规则一起补回来。 */

/* 减少动态效果：不做整屏横扫，只留一次很短的交叉淡入。
   换页照常发生，只是不再有位移。 */
@media (prefers-reduced-motion: reduce) {
  .tide-edge__roll,
  .tide-edge__sheet--back .tide-edge__roll {
    animation: none;
  }

  .tide-label {
    letter-spacing: 0.18em;
    text-indent: 0.18em;
  }
}

/* 小屏把波峰带收矮一档，免得占掉太多视野 */
@media (max-width: 768px) {
  .tide-edge { height: clamp(64px, 9vh, 110px); }
  .tide-label { font-size: 0.75rem; letter-spacing: 0.26em; text-indent: 0.26em; }
}

/* =========================================================================
   世界切换入口
   -------------------------------------------------------------------------
   两个世界之间唯一的「官方通道」，点下去是一整屏潮汐过场。

   ⚠️ 这块地方**连着被推翻三次**。动它之前先把下面三条读完：

     1) 「不好看」≠「不够动」（坑 32）
        2026-09-25 往胶囊里养了一汪水（悬停涨满）→「这个胶囊太丑」。
        34px 高的地方冒出扇贝形波峰，读起来像一圈蕾丝边。
     2) 「加了特效显得廉价」≠「不许有动效」
        2026-09-26 早上改成「跑马灯描边 + 指针追光 + 目的地染色」→
        「加了特效之后没有高级感…反而显得很低级」。跑马灯与追光**是错的**。
     3) **别改他没说的那一层** ← 最容易被忽略的一条
        同一天晚上，我顺手把材质也换了（中性底 + 图标垫片）→
        「还是没有一开始的图标好；**我的意思是改鼠标悬停特效，不是改材质**」。
        站主说「不够有创意、悬停没有高级感」时，**批评的是悬停那一刻的响应**；
        我把材质一起改了，等于答错了题，还把他认可的东西弄丢了。

   所以这一版：**材质回到他认可的样子（品牌色描边 + 淡品牌底 + 光板波纹图标），
   设计全部放进悬停。** 静止 = 一枚安静的胶囊；悬停 = 图标涌起来。

     静止   品牌色描边 `--ws-edge: 30%` + 淡品牌底 `--ws-fill: 11%`
            + 上沿一道内白光 + 一枚很小的落地影
            波纹**冻住**、图标不浮动（常驻动效最招「廉价」的评价）
     悬停   图标「涌起来」这一件事，四层同一个弹簧：
              整枚图标放大到 1.12（Dock 那种吸附感）· 两道波纹各自上浮
              （前浪多、后浪少 → 浪高差）· 开始流动 · 颜色提亮一档
            胶囊同时：底亮一档、描边亮一档、抬起 1px
   按下 scale(.97)。**没有 ::before/::after 光效层，没有跑马灯，没有追光。**
   （潮汐过场本身没动，那套在 crossing.js 与 .tide 里，和这里无关。）
   ========================================================================= */

.world-switch {
  /* 品牌色的两个比例：底材与描边。两套主题共用（品牌色自己会变） */
  --ws-fill: 11%;
  --ws-edge: 30%;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 14px;
  border-radius: var(--r-full);
  border: 1px solid color-mix(in srgb, var(--brand) var(--ws-edge), transparent);
  background-color: color-mix(in srgb, var(--brand) var(--ws-fill), var(--surface-2));
  color: var(--text-dim);
  font-size: 0.8125rem;
  font-weight: 500;
  /* 紧一点的字距是「现代」的一半来源 */
  letter-spacing: -0.01em;
  white-space: nowrap;
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--text) 8%, transparent),
    0 1px 2px rgba(6, 10, 23, 0.1);
  transition: background-color 260ms var(--ease-out), border-color 260ms var(--ease-out),
    box-shadow 260ms var(--ease-out), transform 260ms var(--ease-spring);
}

/* 波纹图标。**静止就是静止** —— 不常驻浮动、不常驻流动。
   悬停那下「涌起来」是这块地方唯一的设计点，所以这里给一条稍长的弹簧曲线。
   几何约定：一个「波峰对」在 x 上正好 9.2 个单位，改路径必须保持它的整数倍，
   否则循环接缝会跳。`overflow: hidden` 负责裁掉画到窗口外的部分。 */
.world-switch__glyph {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  color: var(--brand);
  overflow: hidden;
  transition: transform 420ms var(--ease-spring), color 220ms var(--ease-out);
}

/* 两道波纹的横移循环。**动画只碰 `transform`**：悬停时那点上浮走独立的
   `translate` 属性 —— 否则运行中的动画会盖掉 hover 里的 transform。 */
.ws-wave {
  animation: wsDrift 3.6s linear infinite;
  animation-play-state: paused;
  transition: translate 420ms var(--ease-spring);
}

/* 后浪慢一档、反向走：流起来时两道波纹一直错位，读起来是「水流」而不是「图层在动」 */
.ws-wave--back {
  animation-duration: 5.6s;
  animation-direction: reverse;
}

@keyframes wsDrift {
  from { transform: translateX(0); }
  to { transform: translateX(-9.2px); }
}

.world-switch__to {
  color: var(--text-faint);
  font-weight: 400;
}

/* 目的地名比「Go to」重一档、亮一档 —— 一眼扫过去先看到的是「去哪儿」 */
.world-switch__name {
  color: var(--text);
  font-weight: 600;
}

/* ---------------- 悬停：图标「涌起来」 ----------------
   四层同一个弹簧（420ms spring），读起来是**一个**动作而不是四个效果：
     ① 整枚图标放大 1.12 —— Dock 那种吸附感，指针一碰上「够到」了
     ② 两道波纹各自上浮，前浪 2.6、后浪 1.3 个单位 —— 浪高差 = 涌起来
     ③ 波纹开始流动（静止时是 paused）
     ④ 图标颜色提亮一档
   胶囊本身只做材质响应：底亮一档、描边亮一档、抬起 1px。

   为什么上浮用 `translate` 而不是 `transform`：横移动画正占着 `transform`，
   运行中的动画优先于普通声明，写 transform 会被吃掉（这条踩过）。 */
@media (hover: hover) and (pointer: fine) {
  .world-switch:hover {
    --ws-fill: 17%;
    --ws-edge: 55%;
    color: var(--text);
    box-shadow:
      inset 0 1px 0 color-mix(in srgb, var(--text) 10%, transparent),
      0 2px 8px -2px rgba(6, 10, 23, 0.22);
    transform: translateY(-1px);
  }

  .world-switch:hover .world-switch__to {
    color: var(--text-dim);
  }

  .world-switch:hover .world-switch__glyph {
    transform: scale(1.12);
    color: var(--brand-hover);
  }

  .world-switch:hover .ws-wave--front { translate: 0 -2.6px; }
  .world-switch:hover .ws-wave--back { translate: 0 -1.3px; }

  .world-switch:hover .ws-wave {
    animation-play-state: running;
  }
}

.world-switch:active {
  transform: scale(0.97);
  transition-duration: 120ms;
}

@media (prefers-reduced-motion: reduce) {
  .ws-wave { animation: none; }
  /* `!important`：上面那两条带 :hover 的上浮规则特异度更高，同特异度赢不了 */
  .ws-wave { translate: none !important; }
  .world-switch { transition: none; }
  .world-switch:hover { transform: none; }
  .world-switch__glyph { transition: none; }
  .world-switch:hover .world-switch__glyph { transform: none; color: var(--brand); }
}

/* 手机上导航栏本来就挤（品牌名 + 两个图标按钮已接近满宽），
   世界切换的文案收成只留目的地，图标和胶囊还在。
   纵向留 8px 是为了把触摸目标顶到 ~38px，别再压 */
@media (max-width: 768px) {
  .world-switch {
    padding: 8px 12px;
    gap: 6px;
  }
}

/* 窄屏省略「Go to」，只留目的地 —— 但**菜单里那枚例外**：它是整行宽的，
   有的是地方，省掉反而像缺了一块。 */
@media (max-width: 400px) {
  .world-switch:not(.world-switch--menu) .world-switch__to { display: none; }
}

