/* =========================================================
   ONEPLAY Bridge Page — style.css
   Premium / Luxury Tech / Dark / Cinematic / Minimal
   Hero 右侧：双人品牌肖像式 Campaign Hero（非站立舞台）
   ========================================================= */

:root {
  --color-bg: #02040a;
  --color-bg-2: #060b16;
  --color-text: #eef2fb;
  --color-text-dim: rgba(238, 242, 251, 0.56);
  --color-text-faint: rgba(238, 242, 251, 0.34);
  --color-line: rgba(255, 255, 255, 0.08);
  --color-metal-1: #dfe9ff;
  --color-metal-2: #7fa6e8;
  --color-blue: #3d7fe0;
  --color-blue-deep: #16305c;
  --max-width: 420px;
  --stage-h: 260px;
  /* logo trans（人物背后的品牌核心）：--logo-left 是 Logo 图片自己的
     水平位置，--brandmark-top 是共用的垂直位置。这一轮新增 hud.png
     （人物背后的科技 HUD 背景），HUD 和 Logo 共用同一组
     --logo-left/--brandmark-top 中心坐标，responsive 时保证不分离。
     旧的 CSS 手绘 Portal（ring/streak/node）已删除，避免和 hud.png
     自带的科技环视觉重复堆叠。 */
  --brandmark-w: 33%;
  --brandmark-opacity: 0.55;
  --logo-left: 47%;
  --brandmark-top: 56%;
  /* hud.png 是一整张带透明中心孔的科技环图片，--hud-w 是它相对 stage
     宽度的显示宽度（宽高比按原图等比缩放，不单独设 height） */
  --hud-w: 90%;
  /* 吉祥物全身图容器裁切高度（决定最终可见长度，不是整图缩放） */
  --mascot-h: clamp(188px, 52vw, 208px);
  /* 代言人内部图片缩放基准：现在直接等于她完整原图（chloe.png 整张画布）
     的显示高度，frame 不再用它做二次裁切，只用来定位，见 .ambassador-frame
     大段注释 */
  --ambassador-scale: clamp(220px, 60.5vw, 240px);
  /* CTA 呼吸动画峰值 scale，Mobile 比 Desktop 稍明显一点，桌面在
     >=1024px 断点覆盖为更克制的 1.020 */
  --cta-breath-scale: 1.035;
  /* 人物组整体向上位移：只应用在 .hero-characters 这一个容器上
     （transform: translateY），mascot/ambassador 内部所有几何参数、
     两人相对位置、HUD、Logo 完全不受影响，各断点数值不同 */
  --characters-shift-y: -18px;
}

* {
  box-sizing: border-box;
}

html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  overflow-x: hidden;
  background: var(--color-bg);
}

body {
  color: var(--color-text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
    "Microsoft YaHei", Roboto, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  min-height: 100vh;
  position: relative;
  background:
    radial-gradient(120% 70% at 50% 0%, #0a1428 0%, rgba(10, 20, 40, 0) 55%),
    linear-gradient(180deg, var(--color-bg) 0%, var(--color-bg-2) 55%, var(--color-bg) 100%);
}

img {
  max-width: 100%;
  display: block;
}

a {
  color: inherit;
}

/* =========================================================
   环境光效层（纯装饰，不承载内容）
   ========================================================= */

.fx-hairline {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 1px;
  z-index: 5;
  pointer-events: none;
  background: linear-gradient(
    90deg,
    rgba(125, 170, 255, 0) 0%,
    rgba(125, 170, 255, 0.5) 50%,
    rgba(125, 170, 255, 0) 100%
  );
}

.fx-grid {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.05;
  background-image:
    linear-gradient(rgba(150, 180, 230, 0.6) 1px, transparent 1px),
    linear-gradient(90deg, rgba(150, 180, 230, 0.6) 1px, transparent 1px);
  background-size: 34px 34px;
  mask-image: radial-gradient(80% 60% at 50% 30%, #000 0%, transparent 75%);
  -webkit-mask-image: radial-gradient(80% 60% at 50% 30%, #000 0%, transparent 75%);
}

.fx-grain {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0.035;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 120px 120px;
}

.fx-vignette {
  position: fixed;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: radial-gradient(120% 90% at 50% 30%, transparent 45%, rgba(0, 0, 0, 0.55) 100%);
}

/* =========================================================
   页面容器
   ========================================================= */

.page {
  position: relative;
  z-index: 3;
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 0 20px;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/* ---------- Top bar ---------- */

.top-bar {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px 0 2px;
}

.logo {
  height: 26px;
  width: auto;
  object-fit: contain;
  opacity: 0.95;
}

.logo-fallback {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 3px;
  color: var(--color-metal-1);
}

/* ---------- Main / Hero Stage ---------- */

.main {
  flex: 1;
  display: flex;
  flex-direction: column;
}

.hero-stage {
  display: flex;
  flex-direction: column;
  animation: fadeUp 600ms ease both;
}

/* ---------- Hero Stage 右侧视觉舞台（开放式，无边框卡片） ---------- */

.stage-visual {
  position: relative;
  height: var(--stage-h);
  margin: 4px 0 6px;
  overflow: visible; /* 保证人物头部不会被舞台边界裁切 */
}

/* ---- hud.png：z-index 1，人物背后的科技 HUD 背景（实拍/成品图片，
   不是 CSS 画的） ------------------------------------------------------
   旧的 CSS 手绘 Portal（.logo-energy 容器 + 双层 ring + node + streak）
   已经整体删除：hud.png 本身就是一整张带透明中心孔的科技环图片，视觉
   信息量已经足够，再叠加旧 Portal 只会重复、穿插、让背景显得杂乱。
   hud.png 原始画布 1309×1202，中心透明孔（留给 logo trans 的位置）不在
   画布正中心，用 Python 对 alpha 通道做连通域分析实测得到孔的视觉中心：
   x=49.73% / y=48.71%（画布宽高的百分比）。.stage-hud 和 .stage-brandmark
   共用同一组 --logo-left / --brandmark-top 坐标当"目标中心点"，
   transform 里用这个实测出来的孔中心百分比（而不是 -50%/-50%）做偏移，
   这样孔的视觉中心才会真正和 Logo 中心重合，且 responsive 时两者绝不
   分离（因为始终是同一组变量）。层级顺序：logo trans(0) < hud.png(1) <
   trace(2) < 人物(3+)，HUD 必须盖在 logo trans 前面。 */
.stage-hud {
  position: absolute;
  z-index: 1;
  left: var(--logo-left);
  top: var(--brandmark-top);
  width: var(--hud-w);
  max-width: 900px;
  height: auto;
  transform: translate(-49.73%, -48.71%);
  object-fit: contain;
  /* 撤销上一轮的 contrast/saturate（会把 HUD 变成灰蓝色、压平原图的
     颜色和高光层次）。保持 hud.png 原图所有颜色/对比度/饱和度/高光
     完全不变，只用 opacity 把整体视觉强度统一调暗，不改变颜色关系 */
  opacity: 0.72;
  pointer-events: none;
  user-select: none;
}

/* ---- HUD 细线 trace：z-index 2（画在 hud.png 上面，logo trans 下面
   由 .stage-hud 挡住看不见的部分不影响，人物在更上层）----------------
   hud.png 本身完全静止，不做任何 transform 动画。上一版是大面积
   conic-gradient 扇形（视觉上像扫光/光块），这一版改成"conic-gradient
   控制弧长 + radial-gradient ellipse mask 控制径向厚度"两者交集：
   mask 只留一条极窄的 band（约 1.5 个百分点宽，对应视觉上 2-3px 的细
   环线，不是大面积半径区间），conic-gradient 只在约 4%（~14.4°）的
   角度范围内不透明，两者叠加后可见区域只剩"贴着 HUD 圆环走的一小段
   细线"，不会覆盖圆环内部的大面积区域。只保留 1 条，顺时针转动，
   旋转的是这个透明容器本身，不是 hud.png 图片。 */
.hud-trace {
  position: absolute;
  z-index: 2;
  left: var(--logo-left);
  top: var(--brandmark-top);
  width: var(--hud-w);
  max-width: 900px;
  aspect-ratio: 1309 / 1202;
  border-radius: 50%;
  transform: translate(-49.73%, -48.71%);
  pointer-events: none;
  user-select: none;
}

/* trace-1：外侧轨道，约 4%圆周（~14.4°），顺时针，6.5s。前端（弧段末端，
   顺时针前进方向）最亮最清晰（深蓝→蓝色→淡蓝），后面极短距离渐隐到
   透明；极轻微 drop-shadow 只贴着这条细线本身，模糊半径很小，不会扩散
   成大片光雾 */
.hud-trace-1 {
  background: conic-gradient(
    from 0deg,
    transparent 0%,
    rgba(30, 80, 180, 0.12) 0.3%,
    rgba(70, 150, 235, 0.55) 2%,
    rgba(165, 215, 255, 0.95) 3.8%,
    transparent 4%,
    transparent 100%
  );
  -webkit-mask-image: radial-gradient(
    ellipse,
    transparent 0%,
    transparent 50%,
    #000 51%,
    #000 52.3%,
    transparent 53.5%,
    transparent 100%
  );
  mask-image: radial-gradient(
    ellipse,
    transparent 0%,
    transparent 50%,
    #000 51%,
    #000 52.3%,
    transparent 53.5%,
    transparent 100%
  );
  filter: drop-shadow(0 0 2px rgba(110, 190, 255, 0.5));
  animation: hudTraceSpinCW 6.5s linear infinite;
}

/* trace-2：比 trace-1 稍微内侧的另一条轨道（径向 band 整体往内挪约 3.5
   个百分点，制造"两条不同轨道"的层次），约 3.33%圆周（~12°），逆时针，
   7.5s，和 trace-1 duration/方向都不同、起始角度也错开（from 160deg），
   两条会不断靠近/交错/分开，不固定 180° 对称。颜色整体比 trace-1 淡，
   头部（弧段起点，逆时针前进方向）最亮，尾部渐隐；同样只有极轻微
   drop-shadow，不叠加大片 glow */
.hud-trace-2 {
  background: conic-gradient(
    from 160deg,
    rgba(120, 180, 240, 0.7) 0%,
    rgba(55, 120, 200, 0.38) 1.6%,
    rgba(20, 60, 150, 0.08) 3%,
    transparent 3.33%,
    transparent 100%
  );
  -webkit-mask-image: radial-gradient(
    ellipse,
    transparent 0%,
    transparent 46.5%,
    #000 47.5%,
    #000 48.8%,
    transparent 50%,
    transparent 100%
  );
  mask-image: radial-gradient(
    ellipse,
    transparent 0%,
    transparent 46.5%,
    #000 47.5%,
    #000 48.8%,
    transparent 50%,
    transparent 100%
  );
  filter: drop-shadow(0 0 2px rgba(90, 160, 230, 0.4));
  animation: hudTraceSpinCCW 7.5s linear infinite;
}

@keyframes hudTraceSpinCW {
  from {
    transform: translate(-49.73%, -48.71%) rotate(0deg);
  }
  to {
    transform: translate(-49.73%, -48.71%) rotate(360deg);
  }
}

@keyframes hudTraceSpinCCW {
  from {
    transform: translate(-49.73%, -48.71%) rotate(0deg);
  }
  to {
    transform: translate(-49.73%, -48.71%) rotate(-360deg);
  }
}

/* ONEPLAY 品牌 Logo：z-index 0 — 必须在 hud.png(1) / trace(2) / 人物(3+)
   全部后面，不能夹在人物中间，允许被 HUD 和人物自然遮挡 */

.stage-brandmark {
  position: absolute;
  z-index: 0;
  left: var(--logo-left);
  top: var(--brandmark-top);
  transform: translate(-50%, -50%);
  width: var(--brandmark-w);
  max-width: 500px;
  height: auto;
  object-fit: contain;
  opacity: var(--brandmark-opacity);
  /* 目标是"浅色 Logo"而不是"发光 Logo"：不用 brightness（会整体提亮变成
     发光感），改用 contrast<1（把深蓝/深灰往中灰拉，暗部变浅）+
     saturate<1（降低饱和度，深蓝→浅蓝灰、深灰→浅银灰），drop-shadow
     比上一轮进一步减弱（blur/alpha 都调小），避免任何"灯牌"观感 */
  filter: drop-shadow(0 0 24px rgba(90, 150, 255, 0.1)) contrast(0.72) saturate(0.75);
  pointer-events: none;
  user-select: none;
  /* Logo 位置由 --logo-left / --brandmark-top 控制，这两个变量本轮不动，
     只调整 --brandmark-w（放大）和 filter（变浅）。 */
}

/* 人物层：吉祥物（z-index 2）+ 代言人（z-index 3），绝对定位构成同一组合。
   .hero-characters 自己因为有 translateY 这个 transform，会形成一个新的
   stacking context——如果不显式给它设置 z-index，它在浏览器里会按
   z-index:auto 处理，排在 .stage-hud(1)/.hud-trace(2) 这些有明确正整数
   z-index 的兄弟元素下面，人物反而会被 HUD/trace 盖住，或者被判定成
   "夹在 logo 和 HUD 之间"这种不确定的层级。必须显式给 .hero-characters
   本身也设一个比 trace(2) 更高的 z-index，人物整组才会稳定盖在
   HUD/logo/trace 前面；组内 mascot-frame(2)/ambassador-frame(3) 只决定
   两人之间谁盖谁，不影响这一层对外的比较。 */
.hero-characters {
  position: absolute;
  inset: 0;
  z-index: 3;
  transform: translateY(var(--characters-shift-y));
}

/* ---- 吉祥物：全身素材通过容器裁切显示范围，图片本身不做任何编辑 ----
   --mascot-h 是"缩放基准"，头部大小/头顶位置/frame 宽度/图片内部缩放
   完全没有改动：
     frame width = --mascot-h * 0.6251
     img  height = --mascot-h * 2.1330
     img  left   = --mascot-h * -0.2245（水平居中进 frame）
     img  top    = --mascot-h * -0.3610（去掉素材头顶多余留白，未变）
   这一轮把「代言人」当成唯一基准：frame 的 height/bottom 不再用自己的
   独立倍数计算，改成直接等于代言人当前的可见高度（--ambassador-scale），
   代数上可以证明：
     mascot_top = 容器高度 - bottom - height
                = 容器高度 - (18 + mascot-h - ambassador-scale) - ambassador-scale
                = 容器高度 - 18 - mascot-h   （ambassador-scale 项相消）
   这正好和 ambassador 自己的 top 公式完全一样（ambassador 的 bottom 也是
   同一套「18 + mascot-h - ambassador-scale」推导出来的），所以头顶永远
   精确持平，不需要额外微调；frame 变高之后，露出更多原本被裁掉的大腿/
   小腿部分（已实测 mascot.png 该区域内容完整、左右也没有超出 frame 宽度，
   不会出现露空白或裁到腿外侧的问题）。这一轮取消了底部 mask 渐隐，多出
   来的尾端直接由 frame 的 overflow:hidden 硬裁掉，不做任何 fade/haze。
   --mascot-h 本身的数值完全没有改动，ambassador 侧引用的 --mascot-h
   因此也不受影响，ambassador 的几何结果和上一轮完全一致。
*/
.mascot-frame {
  position: absolute;
  z-index: 2;
  overflow: hidden;
  left: 13%;
  bottom: calc(18px + var(--mascot-h) - var(--ambassador-scale));
  height: var(--ambassador-scale);
  width: calc(var(--mascot-h) * 0.6251);
  /* 底部 fade：改用椭圆 radial-gradient，中心（人物身体主轴）多保留一点，
     两侧更早融入背景，避免一条笔直的水平消失线／"站在黑色平台上"的
     人工感。椭圆锚定在 frame 顶部中心（at 50% 0%），纵向半径 108% 大于
     frame 高度，只有越靠边、越靠下的像素才会先越过 100% 距离阈值。
     只作用在 .mascot-frame 自己这个盒子上，和 .ambassador-frame 完全
     独立，不存在共享 haze / 横向穿模。 */
  mask-image: radial-gradient(
    ellipse 84% 108% at 50% 0%,
    #000 0%,
    #000 78%,
    rgba(0, 0, 0, 0.95) 86%,
    rgba(0, 0, 0, 0.68) 92%,
    rgba(0, 0, 0, 0.3) 96%,
    transparent 100%
  );
  -webkit-mask-image: radial-gradient(
    ellipse 84% 108% at 50% 0%,
    #000 0%,
    #000 78%,
    rgba(0, 0, 0, 0.95) 86%,
    rgba(0, 0, 0, 0.68) 92%,
    rgba(0, 0, 0, 0.3) 96%,
    transparent 100%
  );
}

.mascot-frame .mascot {
  position: absolute;
  top: calc(var(--mascot-h) * -0.3610);
  left: calc(var(--mascot-h) * -0.2245);
  height: calc(var(--mascot-h) * 2.1330);
  width: auto;
  max-width: none;
  object-fit: contain;
  filter: drop-shadow(0 12px 14px rgba(0, 0, 0, 0.4));
}

/* ---- 代言人：完整展示 chloe.png 原图内容，不做任何裁切 -------------
   这一轮彻底改变思路：不再用 frame 的 overflow:hidden 强行裁出和吉祥物
   一样长的可见区域。frame 的 height/width 现在直接等于图片按
   --ambassador-scale 缩放后的"完整画布"尺寸（1024:1536 → 宽 = 高 *
   0.6667），和 .ambassador 图片本身的渲染尺寸完全一致，等于frame 恰好
   包住整张图，天然没有任何内容会被 overflow 裁掉；overflow 改成
   visible 只是为 drop-shadow 的柔光溢出留余地，不承担裁切职责。
   头顶对齐：frame 的 bottom 用代数解出——只要 frame height 换成
   --ambassador-scale，直接复用「mascot_bottom + mascot_frame_height -
   ambassador_scale」这条恒等式，就能让代言人的 top 坐标和吉祥物的 top
   坐标始终相等（同一算式推导，两侧 mascot-h 项相消），不需要再裁图就
   能对齐头顶：
     mobile : bottom = 18px + mascot-h - ambassador-scale
     desktop: bottom = mascot-h - ambassador-scale
   因为 chloe.png 完整显示后本身比吉祥物的可见长度更长，多出来的部分
   会自然地在底部超出 --stage-h 的名义高度，超出去的区域落进
   .stage-visual 本身的 overflow:visible 里，不会被截断，这是本轮明确
   接受的构图（"代言人可以比吉祥物更长"），不再通过裁图解决。 */
.ambassador-frame {
  position: absolute;
  z-index: 3;
  overflow: visible;
  /* 注意：right% 是"距离容器右边的内缩量"，right 越大代言人越往左靠
     （越靠近吉祥物），right 越小反而越靠右（离吉祥物更远）。 */
  right: 13%;
  bottom: calc(18px + var(--mascot-h) - var(--ambassador-scale));
  height: var(--ambassador-scale);
  width: calc(var(--ambassador-scale) * 0.6667);
  /* 底部 fade：同样改用椭圆 radial-gradient，椭圆比 mascot 更宽（代言人
     身形更舒展），中心多保留一点、两侧更早融入背景，不再是一条直线。
     只作用在 .ambassador-frame 自己这个盒子上，和 .mascot-frame 完全
     独立。 */
  mask-image: radial-gradient(
    ellipse 92% 108% at 50% 0%,
    #000 0%,
    #000 82%,
    rgba(0, 0, 0, 0.92) 89%,
    rgba(0, 0, 0, 0.55) 94%,
    transparent 100%
  );
  -webkit-mask-image: radial-gradient(
    ellipse 92% 108% at 50% 0%,
    #000 0%,
    #000 82%,
    rgba(0, 0, 0, 0.92) 89%,
    rgba(0, 0, 0, 0.55) 94%,
    transparent 100%
  );
}

.ambassador-frame .ambassador {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  height: var(--ambassador-scale);
  width: auto;
  max-width: none;
  object-fit: contain;
  filter: drop-shadow(0 12px 14px rgba(0, 0, 0, 0.4));
}


/* 素材缺失时的兜底（不影响布局） */
.hero-characters img[src=""],
.hero-characters img:not([src]) {
  display: none;
}

/* ---------- 文字层级 ---------- */

.stage-content {
  text-align: center;
  padding-bottom: 6px;
}

.eyebrow {
  display: block;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.22em;
  color: rgba(200, 215, 235, 0.72);
  margin-bottom: 15px;
}

.headline {
  margin: 0 0 18px;
  /* 新 Slogan「ONE WORLD · ONE PLAY · ONE WIN」比旧标题长很多，
     30px 是按实际字体宽度核算过的：390px 视口下单行/两行都能放得下，
     不会把单词拆碎（见 HTML 里 &nbsp; 连接每个 "ONE X" 词组的处理） */
  font-size: 28px;
  line-height: 1.04;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: #f2f5fa;
}

/* HEADLINE SEGMENT WRAPPING (FINAL LARGE-SCREEN FULLNESS + HEADLINE PASS):
   each segment never breaks internally (redundant with the &nbsp; already
   joining its words, kept as an explicit belt-and-braces guard); the
   normal breakable space BETWEEN segments (in the HTML source) is what
   lets the browser wrap by whole phrase instead of by word or, worse,
   orphaning a "·" at the start of a line. Base rule (not desktop-scoped)
   -- harmless everywhere, since portrait's own .headline already sets
   white-space:nowrap on the whole element (390-1024px unified block),
   making this a no-op there; it only matters where the parent allows
   wrapping, i.e. desktop. */
.headline-segment {
  white-space: nowrap;
}

.headline .hl {
  background: linear-gradient(180deg, #eef5ff 0%, #afcbff 55%, #7fa8e8 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  text-shadow: 0 0 18px rgba(80, 130, 220, 0.08);
}

.subhead {
  margin: 0 0 21px;
  font-size: 14px;
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: rgba(190, 205, 225, 0.68);
}

/* ---------- CTA ---------- */

.cta-btn {
  appearance: none;
  border: none;
  outline: none;
  width: 100%;
  max-width: 380px;
  height: 58px;
  border-radius: 14px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  font-size: 18px;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--color-metal-1);
  /* 静态质感微调：背景改用更克制的深蓝双色渐变，边框提亮一点点，
     内部顶部高光 alpha 从 .18 收到 .16，更精致、不那么"普通蓝色按钮"；
     呼吸动画（下面的 ctaBreath keyframes）完全没有改，只在它的
     0%/100% 静止帧里同步了这三个新数值，保持"静止帧 = 这里的静态声明"
     这个原有设计一致，duration/scale/25%/50%/75% 的光效进度完全不变 */
  background: linear-gradient(180deg, rgba(34, 74, 126, 0.96), rgba(13, 42, 82, 0.98));
  border: 1px solid rgba(150, 200, 255, 0.34);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.16),
    0 10px 24px rgba(4, 10, 24, 0.55),
    0 0 22px rgba(61, 127, 224, 0.22);
  cursor: pointer;
  transition: transform 0.15s ease, box-shadow 0.15s ease;
  -webkit-tap-highlight-color: transparent;
  animation: ctaBreath 2.0s ease-in-out infinite;
  transform-origin: center;
}

/* 高级呼吸感：scale + 双层蓝色 glow（近光收紧 + 远光柔化）+ 边框/内高光
   同步增强，不动 opacity（保持 1，避免廉价的忽明忽暗），不改按钮基础
   渐变设计。25%/75% 是中间过渡帧，让"开始变亮→到峰值→慢慢回落"的
   节奏比单纯 0%/50%/100% 更明显；25%/75% 的 scale 用 --cta-breath-scale
   按比例换算，desktop/mobile 共用同一组关键帧。 */
@keyframes ctaBreath {
  0%,
  100% {
    transform: scale(1);
    border-color: rgba(150, 200, 255, 0.34);
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.16),
      0 10px 24px rgba(4, 10, 24, 0.55),
      0 0 22px rgba(61, 127, 224, 0.22),
      0 0 40px rgba(61, 127, 224, 0);
  }
  25%,
  75% {
    transform: scale(calc(1 + (var(--cta-breath-scale, 1.028) - 1) * 0.45));
    border-color: rgba(165, 205, 255, 0.38);
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.24),
      0 10px 24px rgba(4, 10, 24, 0.55),
      0 0 21px rgba(61, 127, 224, 0.32),
      0 0 40px rgba(61, 127, 224, 0.1);
  }
  50% {
    transform: scale(var(--cta-breath-scale, 1.028));
    border-color: rgba(175, 215, 255, 0.5);
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.32),
      0 10px 24px rgba(4, 10, 24, 0.55),
      0 0 20px rgba(61, 127, 224, 0.42),
      0 0 40px rgba(61, 127, 224, 0.22);
  }
}

/* hover / active / focus 时暂停呼吸，让原有的交互态 transform 与 box-shadow 优先生效 */
.cta-btn:hover,
.cta-btn:active,
.cta-btn:focus-visible {
  animation-play-state: paused;
}

/* FINAL DETERMINISM FIX (portrait-fit.js round 8): while a portrait fit
   transaction is solving/measuring, suspend .cta-btn's ctaBreath
   animation. Confirmed by tracing portrait-fit.js's own
   getBoundingClientRect() calls: #registerBtn's real rect feeds
   directly into the CTA-visibility gate and Gate C's
   requiredContentBottom, and ctaBreath never pauses during automated
   testing (only :hover/:active/:focus-visible pause it, none of which
   a headless resize test triggers) — so the solver's measured CTA
   geometry silently depended on the real-clock phase of an ongoing 2s
   animation, which differs between a fresh page load and a resize that
   happens mid-page-lifetime. That was the confirmed root cause of
   live-resize vs cold-load --hero-scale drift. The other selectors are
   included defensively (none currently carry a transition/animation of
   their own; confirmed by a full-file audit) so a future addition of
   one to any Hero/content element cannot silently reintroduce the same
   class of bug. This is a measurement-time-only state:
   html.is-portrait-fitting is added at the start of every fit
   transaction and removed immediately after the final scale is
   committed and re-verified (assets/js/portrait-fit.js), so users
   never see anything beyond momentarily catching the CTA at rest
   instead of mid-breath. */
html.is-portrait-fitting .stage-visual,
html.is-portrait-fitting .stage-visual-master,
html.is-portrait-fitting .stage-hud,
html.is-portrait-fitting .stage-brandmark,
html.is-portrait-fitting .hero-characters,
html.is-portrait-fitting .mascot-frame,
html.is-portrait-fitting .ambassador-frame,
html.is-portrait-fitting .stage-content,
html.is-portrait-fitting .logo,
html.is-portrait-fitting .cta-btn {
  transition: none !important;
  animation: none !important;
}

.cta-arrow {
  width: 8px;
  height: 8px;
  border-top: 1.5px solid currentColor;
  border-right: 1.5px solid currentColor;
  transform: rotate(45deg);
  opacity: 0.85;
}

.cta-btn:active {
  transform: scale(0.985);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.14),
    0 6px 16px rgba(4, 10, 24, 0.5),
    0 0 14px rgba(61, 127, 224, 0.28);
}

@media (hover: hover) {
  .cta-btn:hover {
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.22),
      0 12px 28px rgba(4, 10, 24, 0.6),
      0 0 28px rgba(61, 127, 224, 0.3);
  }
}

.cta-microcopy {
  margin: 17px 0 0;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.2em;
  color: rgba(200, 215, 235, 0.4);
}

/* ---------- Footer ---------- */

.site-footer {
  margin-top: auto;
  padding: 22px 0 20px;
  text-align: center;
}

.footer-links {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  margin-bottom: 8px;
}

.footer-item {
  font-size: 10.5px;
  color: var(--color-text-faint);
  letter-spacing: 0.5px;
}

.footer-link {
  text-decoration: none;
}

.footer-link:hover {
  color: var(--color-text-dim);
}

.footer-copy {
  margin: 0;
  font-size: 10.5px;
  color: var(--color-text-faint);
  opacity: 0.8;
}

/* =========================================================
   Debug Panel
   ========================================================= */

.debug-panel {
  position: fixed;
  left: 10px;
  right: 10px;
  bottom: 10px;
  z-index: 20;
  max-width: 460px;
  margin: 0 auto;
  background: rgba(3, 6, 14, 0.96);
  border: 1px solid rgba(150, 190, 255, 0.25);
  border-radius: 10px;
  padding: 10px 12px;
  font-size: 11px;
  line-height: 1.5;
  backdrop-filter: blur(6px);
  max-height: 45vh;
  overflow-y: auto;
}

.debug-panel__header {
  color: var(--color-metal-1);
  font-weight: 700;
  letter-spacing: 1px;
  margin-bottom: 6px;
}

.debug-panel__list {
  display: grid;
  grid-template-columns: 90px 1fr;
  gap: 2px 8px;
  margin: 0;
}

.debug-panel__list dt {
  color: var(--color-text-dim);
}

.debug-panel__list dd {
  margin: 0;
  color: var(--color-text);
  word-break: break-all;
}

.debug-panel__url {
  font-size: 10px;
}

/* =========================================================
   进场动画（极轻微，尊重 prefers-reduced-motion）
   ========================================================= */

@keyframes fadeUp {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .hero-stage {
    animation: none;
  }
  .cta-btn {
    animation: none;
  }
  .hud-trace-1,
  .hud-trace-2 {
    animation: none;
  }
}

/* =========================================================
   响应式断点
   ========================================================= */

@media (min-width: 375px) {
  :root {
    --stage-h: 280px;
  }
}

@media (min-width: 431px) {
  :root {
    --max-width: 460px;
    --stage-h: 312px;
    --characters-shift-y: -20px;
    --brandmark-w: 34%;
    /* 按 mascot 右侧实际可见边缘 / ambassador 左侧实际可见边缘（含各自
       alpha 内容裁切）算出的中点，再按实际 render 肉眼效果往左微调 */
    --logo-left: 46%;
  }
  /* 431-599px：容器变宽但人物尺寸已封顶（mascot-h / ambassador-scale
     都到各自 clamp 上限），root 的 right 数值不够用，单独收紧一次 */
  .ambassador-frame {
    right: 17%;
  }
  .headline {
    font-size: 32px;
  }
}

@media (min-width: 600px) {
  :root {
    --max-width: 520px;
    --characters-shift-y: -26px;
    --brandmark-w: 32%;
    /* 同样按两人实际边缘中点算出（这一档 ambassador right 收紧到
       31.9%，间隙位置随之左移），再按实际 render 肉眼效果往左微调 */
    --logo-left: 39%;
  }
  /* 600-1023px：容器进一步变宽（且在此区间内保持恒定 480px），人物仍
     封顶，再收紧一次避免空隙持续变大 */
  .ambassador-frame {
    right: 31.9%;
  }
}

/* ---- 桌面：左右分栏，右侧人物成为第一视觉重点 ----
   PORTRAIT RESPONSIVE FINAL FIX: added "and (orientation: landscape)".
   1024x1366 (and any other >=1024px PORTRAIT viewport) must never enter
   this two-column desktop cascade at all — not "enter it and then get
   80% overridden back" by a later portrait block, which is exactly the
   fragile ownership-fighting pattern this fix eliminates. Real desktop
   monitors are always landscape, so this has zero effect on actual
   desktop/landscape behavior.

   DESKTOP VISUAL RESPONSIVENESS (this round): replaces the old
   --desktop-height-scale (a short-height-only mechanism, capped at a
   maximum of 1, so it could shrink the Hero on short viewports but
   could never grow the composition on generously large ones — the
   confirmed cause of 1440-2560px screens gaining empty canvas instead
   of a genuinely larger composition) with ONE desktop master scale,
   --desktop-scale, anchored at 1366x768 (scale=1, byte-for-byte the
   same rendered result as before this round at that exact viewport):
     --desktop-scale = clamp(0.78, min(vw/1366, vh/768), 1.55)
   This is a single continuous formula, not a per-viewport table — it
   naturally reproduces (verified by direct calculation, not asserted):
     1024x768 -> 0.78 (floor)   1280x720 -> ~0.937   1366x768 -> 1.000
     1440x900 -> ~1.054         1600x900 -> ~1.171    1920x1080 -> ~1.406
     2560x1440 -> 1.55 (ceiling)
   Using min(width-ratio, height-ratio) means short-height protection
   (the previous round's entire purpose) is now a natural byproduct of
   the same formula, not a separate mechanism — a short-but-wide
   viewport is governed by the height term exactly as before, while a
   generously tall viewport is governed by the width term and keeps
   growing well past 1.0. Every dimension below that should visually
   scale together — Hero, typography, CTA, header logo, column gap,
   container width — is now driven by this ONE variable via
   calc(REFERENCE_PX * var(--desktop-scale)), where REFERENCE_PX is
   exactly what that property already evaluated to at the 1366x768
   anchor under the OLD per-property formulas (so scale=1 changes
   nothing), never a re-guessed number. */
@media (min-width: 1024px) and (orientation: landscape) {
  :root {
    /* DESKTOP VISUAL POLISH + FINAL LARGE-SCREEN FULLNESS PASS:
       --desktop-scale-base is the exact same type-valid formula as
       before (px-unit divisors are load-bearing — see the regression
       warning further down this comment). Below scale=1 (1024-1366px
       range) --desktop-scale is still IDENTICAL to
       --desktop-scale-base -- the max(0, base-1) term is exactly zero
       there, so the whole <=1366 range (including the 0.78 floor) is
       byte-for-byte unchanged from before either round. Above scale=1,
       the same mild continuous "gain" as before (coefficient 0.08,
       i.e. desktopScale = base + (base-1) * 1.08 for base>1); the
       ceiling is raised again this round, 1.70 -> 1.82 (within the
       1.80-1.84 tuning range given), because at 1.70 2560x1440's
       raw ratio (~1.87) was still hitting the cap and reading as
       under-filled (MainW/VPW ~0.797, screenshots confirmed). Only
       2560x1440 is actually affected by this change -- every other
       QA viewport's baseScale (max ~1.438 at 1920x1080) sits well
       under both the old and new cap, so 1600/1920 are untouched by
       this specific edit (verified by direct calculation, not
       asserted). Verified to keep the one-screen height budget's
       tightest margin (1280x720, itself unaffected since its base is
       <1) unchanged, and to give 2560x1440 a ~237px vertical margin at
       the new scale=1.82 -- comfortable, not razor-thin -- while
       landing MainW/VPW at ~0.853 (inside the requested 0.83-0.86
       band). */
    --desktop-scale-base: min(calc(100vw / 1366px), calc(100vh / 768px));
    --desktop-scale: clamp(
      0.78,
      calc(var(--desktop-scale-base) + max(0, calc(var(--desktop-scale-base) - 1)) * 0.08),
      1.82
    );
    /* --desktop-content-pct: the left content column's share of the
       hero-stage row. Flat 39% below scale=1 (max(0, scale-1) is zero
       there, so 1024-1366 is unchanged). REVERT LAYOUT round: the
       "PROMO COPY + VISUAL REBALANCE" round's flat-47% version is
       reverted -- the user reported that whole visual rebalance went
       too far and asked to return to this previously-accepted
       FINAL CONTENT/HERO COMPROMISE (+5pp, 39% -> 44% max at the 1.82
       cap, linear in max(0, desktopScale-1), coefficient = 5% / (1.82
       - 1) = 6.098%), which was the last state confirmed working via
       real Chrome measurement (contentVisibleWidth=884.08px,
       headlineLineCount=1 at 2560 with the OLD long slogan). --stage-
       visual always takes the complementary calc(100% - content-pct),
       so the two always sum to exactly 100% by construction. */
    --desktop-content-pct: calc(39% + max(0, calc(var(--desktop-scale) - 1)) * 6.098%);
    /* --desktop-gap-mult: the Hero<->content gap grows at only 35% of
       the rate of everything else above scale=1 (reduced from 50% the
       previous round, per this round's "reduce the gap a little more"
       instruction, priority #2 in the headline-fit priority order), so
       large screens read as one composition and the content column has
       a little more real width to work with. Flat 1 (unchanged 32px
       gap) below scale=1. */
    --desktop-gap-mult: calc(1 + max(0, calc(var(--desktop-scale) - 1)) * 0.35);
    /* --desktop-logo-mult: the header logo is a secondary element and
       must never compete with the Hero for visual weight, so it grows
       at only 35% of the main rate above scale=1. Flat 1 (unchanged
       22px) below scale=1. */
    --desktop-logo-mult: calc(1 + max(0, calc(var(--desktop-scale) - 1)) * 0.35);
    /* --desktop-headline-scale (HEADLINE SCALE FINALIZATION round):
       the structural reason widening the content column alone never
       reached one line is that the headline's font-size was growing in
       lockstep with --desktop-scale -- so the text got proportionally
       wider at exactly the same rate the column got wider, and the
       wrapping outcome barely changed. This is a SEPARATE, dampened
       scale for headline typography only: identical to --desktop-scale
       at and below the 1366 master (max(0, scale-1) is zero there, so
       1024-1366 is byte-for-byte unchanged -- this is "grow slower
       above the master", never "shrink the master"), and above it,
       only 45% of the rate (desktopScale=1.82 at 2560 -> headlineScale
       ~1.369, matching 1 + (1.82-1)*0.45 exactly). Built with the same
       piecewise technique as --desktop-scale itself: headlineScale =
       desktopScale - max(0, desktopScale-1) * 0.55, which algebraically
       equals desktopScale for desktopScale<=1, and 1 + (desktopScale-1)
       * 0.45 for desktopScale>1 -- verified by direct calculation (not
       asserted) to reproduce 1.000 / 1.083 / 1.197 / 1.369 at
       1366 / 1600 / 1920 / 2560 respectively. Applied ONLY to
       .headline's font-size below -- never to Hero/HUD/mascot/
       ambassador/content-column-width/CTA/stage geometry, all of which
       keep using --desktop-scale directly, unchanged. .headline's
       line-height (1.04, unitless) and letter-spacing (-0.02em) are
       both relative units that already scale automatically with
       whatever font-size is in effect, so neither needs a separate
       multiplier here. */
    --desktop-headline-scale: calc(var(--desktop-scale) - max(0, calc(var(--desktop-scale) - 1)) * 0.55);
    /* --desktop-character-spread (FINAL HERO INTERNAL SPACING PASS,
       CHARACTER SPREAD FINAL TUNING round: coefficient 43px -> 80px --
       35.3px/side at the cap left the frames overlapping by ~107px and
       the center brandmark too covered; real-measured target is
       ~64-66px/side at the 1.82 cap):
       0px at and below the 1366 master (max(0, scale-1) is zero there
       -- hard requirement, verified below), growing continuously above
       it so mascot/ambassador read the center stage-brandmark less as
       their silhouettes scale up together. At the 1.82 cap: 0.82 * 80px
       ~= 65.6px each side (~131px total added separation), matching the
       target exactly. Does not touch --desktop-scale, --max-width,
       --stage-h, --mascot-h, --ambassador-scale, HUD size/position, or
       stage-brandmark's optical-centering transform -- this is a pure
       horizontal nudge applied to .mascot-frame/.ambassador-frame
       (the outer frame boxes) via translateX below, layered on top of
       their existing left%/right%/bottom positioning, which is
       unaffected by a transform (transforms apply post-layout, they
       never change what an element contributes to its container's box
       model or disturb sibling layout). Neither frame element had any
       transform before this round, so nothing existing is overwritten. */
    --desktop-character-spread: calc(max(0, calc(var(--desktop-scale) - 1)) * 80px);
    /* --desktop-left-release ("FINAL PROMO RESPONSIVE" round,
       reinstated): the automated clipping diagnostic (runtime-only,
       zero production edits) proved the real clipping owner was
       .main's own `overflow: hidden` -- not .page, not any FX layer,
       not a background seam. Diagnostic evidence: forcing
       `.main { overflow: visible }` alone made the clipped leading
       letters reappear with geometryUnchanged=true at all three
       viewports (clip X measured at 131/166/275 for 1366/1920/2560,
       matching .main's own left edge exactly). Fixed at the source
       this round (see `.main` below: `overflow: hidden` -> `visible`,
       .page's own overflow:hidden kept as-is as the outer safety
       boundary). With the real clipping ancestor fixed, this
       left-release offset is safe to reinstate -- same value/design
       as before (24px * --desktop-scale, half of .page's 48px*scale
       padding), but now scoped ONLY to .logo-wrap / .stage-content /
       .site-footer's actual left-side content group (not the whole
       .top-bar), since .top-bar and .site-footer currently contain no
       right-side controls to accidentally drag along -- confirmed via
       index.html: .top-bar's only child is .logo-wrap, .site-footer's
       only children are .footer-links (18+/Terms/Privacy) and
       .footer-copy (copyright), i.e. the entire footer already IS the
       "left content group". One shared variable, applied via
       translateX() (paint-only, never changes box-model contribution
       or Hero's flex math) at exactly these anchor points, so they
       stay aligned with each other by construction. */
    --desktop-left-release: calc(24px * var(--desktop-scale));
    /* --max-width: REVERT LAYOUT round -- restored to the previously
       accepted formula. The "PROMO COPY + VISUAL REBALANCE" round's
       vw*ratio formula (--desktop-outer-ratio) is removed entirely;
       the user reported the resulting wider composition was not
       wanted and asked to return to the exact old desktop container
       system. */
    --max-width: calc(1200px * var(--desktop-scale));
    --stage-h: calc(580px * var(--desktop-scale));
    --characters-shift-y: calc(-32px * var(--desktop-scale));
    --brandmark-w: 30%;
    --brandmark-opacity: 0.52;
    /* Logo 自己的水平位置：按 mascot 右侧实际可见边缘 / ambassador 左侧
       实际可见边缘（1440px 测试点算出），再按实际 render 肉眼效果往左
       微调；hud.png 复用同一组坐标，跟着一起移动。百分比本身已经跟随
       容器宽度自然缩放，不需要再乘 --desktop-scale。 */
    --logo-left: 44%;
    --brandmark-top: 51%;
    --hud-w: 90%;
    --cta-breath-scale: 1.028;
    /* --mascot-h / --ambassador-scale used to be their own independent
       vw-based clamp() (calibrated around the 1024px/1213px inflection
       points, which is exactly what the now-removed 1024-1439px
       "transition zone" patch further below existed to paper over).
       Replaced with a literal reference size — exactly what each
       clamp() evaluated to at 1366px width (34.2vw=467.17px,
       37.7vw=514.98px) — multiplied by the SAME --desktop-scale as
       everything else, so the Hero, the container, and the typography
       all grow in lockstep with no independent inflection points left
       to misalign. REVERT LAYOUT round: the "PROMO COPY + VISUAL
       REBALANCE" round's --desktop-hero-mult (0.91) is removed --
       restored to plain --desktop-scale, the previously accepted
       Hero sizing behavior. */
    --mascot-h: calc(467.17px * var(--desktop-scale));
    --ambassador-scale: calc(514.98px * var(--desktop-scale));
  }

  /* DESKTOP ONE-SCREEN FIX (kept from the previous round, unchanged in
     mechanism): .page is a fixed-height CSS GRID shell — header row
     (auto), a flexible middle row (minmax(0,1fr) — gets the REAL
     remaining space after the header/footer rows, computed by the
     browser from actual rendered geometry, never a guessed magic
     number), and a footer row (auto). height:100dvh (with a height:
     100vh fallback declared first, for browsers that don't support
     dvh) makes .page exactly one viewport tall, not "at least". Page
     edge padding now scales with --desktop-scale along with
     everything else inside it. */
  .page {
    padding: 0 calc(48px * var(--desktop-scale));
    height: 100vh;
    height: 100dvh;
    display: grid;
    grid-template-rows: auto minmax(0, 1fr) auto;
    /* Defensive guard only — the real fit comes from --desktop-scale
       above (its height term already shrinks the whole composition
       proportionally on short viewports, verified by calculation to
       leave comfortable margin at every QA viewport down to 1280x720);
       this is insurance against residual sub-pixel rounding, not what
       makes any test pass. */
    overflow: hidden;
  }

  .top-bar {
    justify-content: flex-start;
    padding: calc(24px * var(--desktop-scale)) 0 0;
  }

  .logo-wrap {
    /* FINAL PROMO RESPONSIVE round: left-release applied to the logo's
       own wrapper, not the whole .top-bar (.top-bar has no other
       children to leave behind, but scoping it here keeps the offset
       tied to the actual visual content group rather than the row
       container, matching .stage-content/.site-footer below). Paint-
       only -- .top-bar's own row height/box in .page's grid is
       unaffected. */
    transform: translateX(calc(-1 * var(--desktop-left-release)));
  }

  .logo {
    /* DESKTOP VISUAL POLISH: the header logo is secondary to the Hero
       and must never compete with it visually -- --desktop-logo-mult
       grows it at only 35% of the main rate above scale=1. Unchanged
       (22px) at the 1366 master and below. */
    height: calc(22px * var(--desktop-logo-mult));
  }

  .main {
    justify-content: center;
    /* min-height:0 lets .main honor the grid row's real remaining
       space instead of being forced taller by flex/grid's default
       min-height:auto (intrinsic content) behavior. */
    min-height: 0;
    /* FINAL PROMO RESPONSIVE round: changed from `hidden` to
       `visible`. Diagnostic-confirmed (see --desktop-left-release
       above): .main's own overflow:hidden was the real clipping
       ancestor cutting off shifted content's leading letters --
       overflow-x:visible alone was proven NOT sufficient by the
       real-browser test, only the shorthand `overflow: visible` made
       the clip disappear (geometryUnchanged=true). .page (the outer
       shell, one level up) keeps its own overflow:hidden unchanged as
       the one-screen-fit safety boundary, so this does not reopen any
       vertical-scroll risk -- anything that would have exceeded
       .page's box is still clipped there. */
    overflow: visible;
  }

  .hero-stage {
    flex-direction: row;
    align-items: center;
    /* DESKTOP VISUAL POLISH: gap grows at a dampened rate above
       scale=1 (see --desktop-gap-mult above) so Hero and content read
       as one composition instead of drifting apart on large screens.
       Unchanged (32px) at the 1366 master and below. */
    gap: calc(32px * var(--desktop-gap-mult));
  }

  .stage-content {
    order: 1;
    /* DESKTOP VISUAL POLISH: --desktop-content-pct is flat 39% up to
       the 1366 master (unchanged) and grows up to +6 points toward the
       1.70 scale cap, giving the column (and therefore the headline's
       real wrapping width) more breathing room on large screens. See
       the "HEADLINE WIDTH" fix below for why the text itself also
       needs its own max-width to scale in lockstep. */
    flex: 0 0 var(--desktop-content-pct);
    max-width: var(--desktop-content-pct);
    text-align: left;
    padding-bottom: 0;
    /* FINAL PROMO RESPONSIVE round: translateX(--desktop-left-release)
       combined with the pre-existing translateY (multiple functions in
       one transform value are layered, not overwritten). Now safe to
       reinstate -- the real clipping owner (.main's overflow:hidden)
       is fixed above, not this transform. .stage-content's flex-basis/
       width (what .stage-visual's complementary calc(100% -
       content-pct) is computed from) is unaffected, since transform
       never changes box-model contribution -- Hero's flex-basis and
       position cannot shift as a result of this. */
    transform: translateX(calc(-1 * var(--desktop-left-release))) translateY(calc(-7px * var(--desktop-scale)));
  }

  .stage-visual {
    order: 2;
    /* Always the exact complement of --desktop-content-pct, so the two
       columns sum to 100% of hero-stage by construction -- never an
       independently-tuned second percentage that could drift out of
       sync with the content column. */
    flex: 0 0 calc(100% - var(--desktop-content-pct));
    max-width: calc(100% - var(--desktop-content-pct));
    margin: 0;
  }

  /* .stage-hud / .hud-trace share a global max-width:900px safety cap
     (base rule, shared with portrait — left untouched there) that
     would otherwise silently stop the HUD from growing past 900px
     real pixels on large desktop screens even though --hud-w is a
     percentage of an already-growing container. Scaling the cap itself
     keeps it purely a runaway-growth guard, never a binding limit
     within the designed range. */
  .stage-hud,
  .hud-trace {
    max-width: calc(900px * var(--desktop-scale));
  }

  .stage-brandmark {
    max-width: calc(850px * var(--desktop-scale));
    /* OPTICAL CENTERING FIX (this round): .stage-hud's transform
       (translate(-49.73%,-48.71%), base rule) is an EMPIRICALLY
       MEASURED offset compensating for hud.png's own transparent
       center-hole not sitting at the canvas's literal 50%/50%
       midpoint (real alpha-channel connected-component analysis puts
       it at ~50.13%/48.31%, consistent with the base rule's own
       documented methodology and value). The base .stage-brandmark
       rule instead used a NAIVE translate(-50%,-50%), assuming logo
       trans.png's visible artwork IS centered at its canvas's literal
       midpoint. It is not: measuring the actual non-transparent pixel
       bounding box of logo trans.png (same technique, this file) gives
       a real visual center at x=50.03%, y=47.27% of its own canvas —
       nearly 3 percentage points off vertically. Both .stage-hud and
       .stage-brandmark already share the exact same anchor point
       (left:var(--logo-left); top:var(--brandmark-top), both
       positioned within the same .stage-visual containing block) — the
       bug was never a "different coordinate system", it was one
       image's transform using a corrected optical-center offset and
       the other using an uncorrected literal-box-center guess. This
       override is scoped to desktop only (the reported real-browser
       defect was specifically observed at 1024x768 desktop) — portrait
       is not touched by this round and continues to inherit the base
       rule's translate(-50%,-50%) unchanged. */
    transform: translate(-50.03%, -47.27%);
  }

  .mascot-frame {
    left: 3%;
    bottom: calc(var(--mascot-h) - var(--ambassador-scale));
    /* FINAL HERO INTERNAL SPACING PASS: pure horizontal nudge, added
       on top of the existing left/bottom positioning above -- this
       frame element had no transform before, so nothing is
       overwritten. Negative = moves left (away from HUD center),
       exactly mirroring .ambassador-frame's positive translateX
       below so the two move symmetrically apart around the
       HUD/brandmark anchor. */
    transform: translateX(calc(-1 * var(--desktop-character-spread)));
  }

  .ambassador-frame {
    right: 10%;
    bottom: calc(var(--mascot-h) - var(--ambassador-scale));
    /* See .mascot-frame above -- symmetric positive nudge (moves
       right). This element also had no transform before this round;
       its child .ambassador img's own translateX(-50%) (self-
       centering within this frame box) is untouched. */
    transform: translateX(var(--desktop-character-spread));
  }

  .eyebrow {
    /* FINAL TYPOGRAPHY ENLARGEMENT round (Part J): 13px -> 15px is
       +15.4%, inside the requested +15-20% band. Font-size only --
       no max-width/position/margin change, so the content anchor and
       Hero geometry are unaffected. */
    font-size: calc(15px * var(--desktop-scale));
    letter-spacing: 0.28em;
    margin-bottom: calc(15px * var(--desktop-scale));
  }

  .headline {
    /* font-size 的参照值是旧 clamp(34px, 2.6vw, 44px) 在 1366px 锚点处
       的真实计算结果（35.52px）。HEADLINE SCALE FINALIZATION 这轮：
       乘数从 --desktop-scale 换成专属的、增长更慢的 --desktop-headline-scale
       —— 1366px 处两者相等（渲染结果和改动前逐像素一致），1366px 以上
       标题只按 45% 的速率跟着长，不再和 Hero/整体构图同速率放大，这样
       文本宽度增长得比内容列宽度慢，才有机会自然收敛成一行。
       FINAL TYPOGRAPHY ENLARGEMENT round (Part J): base bumped
       35.52px -> 43px (+21.1%, inside the requested +18-25% band).
       The --desktop-headline-scale multiplier itself is untouched, so
       the growth-rate-vs-viewport behavior described above is
       unchanged; only the 1366px anchor value grows. max-width still
       scales with --desktop-scale (unchanged this round) -- larger
       text may now wrap across 2 lines more readily at small desktop
       widths, which is explicitly allowed ("headline max 2 lines,
       natural wrapping allowed"). */
    font-size: calc(43px * var(--desktop-headline-scale));
    /* HEADLINE WIDTH FIX: max-width now scales with --desktop-scale
       instead of being frozen at a flat 460px — a frozen column width
       against a scale-driven Hero/typography was exactly what produced
       inconsistent wrapping (e.g. "ONE WIN" forced to its own line, or
       a separator landing at the start of a wrapped line) across
       viewports. This does not hardcode any line break; it only lets
       the intended 2-line composition keep the same relative amount of
       breathing room at every scale that it already had at the 1366
       anchor. */
    max-width: calc(460px * var(--desktop-scale));
    margin-bottom: calc(18px * var(--desktop-scale));
  }

  .subhead {
    /* FINAL TYPOGRAPHY ENLARGEMENT round (Part J): 16px -> 18px is
       +12.5%, inside the requested +12-18% band. max-width/margin
       unchanged -- anchor untouched. */
    font-size: calc(18px * var(--desktop-scale));
    max-width: calc(400px * var(--desktop-scale));
    margin-bottom: calc(21px * var(--desktop-scale));
  }

  .cta-btn {
    /* FINAL TYPOGRAPHY ENLARGEMENT round (Part J): height 55->60px
       (+9.1%), padding 32->35px, font 18->20px (+11.1%) -- all inside
       the requested +8-12% band. width stays auto (button hugs its
       own text/padding, does not reposition the content anchor). */
    width: auto;
    height: calc(60px * var(--desktop-scale));
    padding: 0 calc(35px * var(--desktop-scale));
    font-size: calc(20px * var(--desktop-scale));
  }

  .cta-microcopy {
    font-size: calc(10px * var(--desktop-scale));
    margin-top: calc(17px * var(--desktop-scale));
  }

  .site-footer {
    text-align: left;
    /* FINAL PROMO RESPONSIVE round: see --desktop-left-release above.
       .site-footer's only children are .footer-links (18+/Terms/
       Privacy) and .footer-copy (copyright) -- there is no separate
       right-side control in the footer, so the entire footer IS the
       "left content group" the brief describes; no DOM split needed.
       .footer-copy (child, not a flex item of anything with a
       competing side) moves with it automatically. */
    transform: translateX(calc(-1 * var(--desktop-left-release)));
  }

  .footer-links {
    justify-content: flex-start;
  }
}

/* DESKTOP VISUAL RESPONSIVENESS (this round): both the old "1024-1439px
   transition zone" patch (which existed ONLY to compensate for
   --mascot-h's independent vw-clamp having a different inflection
   point than .page's own max-width cap) and the old flat >=1440px
   override block (a second, disconnected reference point for
   --max-width/--stage-h/--characters-shift-y) have been REMOVED. Both
   were viewport-specific patches working around problems that no
   longer exist now that .page's max-width, --stage-h, --mascot-h,
   --ambassador-scale, and --characters-shift-y are ALL driven by the
   one continuous --desktop-scale formula defined in the >=1024px
   landscape block above — there is no second independent formula left
   to misalign with the first, at 1439px, 1440px, or anywhere else.
   .mascot-frame/.ambassador-frame's left:3%/right:10% (percentages,
   set in that same block) now govern the entire 1024px+ landscape
   range continuously, with no separate per-range override. */

/* 极小屏幕（360px）安全边距 */
@media (max-width: 374px) {
  .page {
    padding-left: 16px;
    padding-right: 16px;
  }
  .headline {
    font-size: 27px;
  }
  :root {
    --stage-h: 258px;
    --brandmark-w: 33%;
    --logo-left: 47%;
  }
}

/* =========================================================
   多语言 Typography 微调
   ========================================================= */

/* .eyebrow / .cta-microcopy 用的是"全大写 + 很宽字距"的西文排版习惯，
   中文没有大小写概念、字距拉太开会显得松散，泰文字形本身带上下附标，
   字距拉大会让单词的视觉整体感被破坏，所以这两种语言单独收紧字距。
   英文 / 马来文（也是全大写拉丁字母）保留原本的字距，不需要 override。
   Slogan 本身四语言统一是英文原文，不受这里影响。 */
html[lang="zh"] .eyebrow,
html[lang="zh"] .cta-microcopy {
  letter-spacing: 0.08em;
}

html[lang="th"] .eyebrow,
html[lang="th"] .cta-microcopy {
  letter-spacing: 0.02em;
}

/* PORTRAIT RESPONSIVE FINAL ARCHITECTURE (round 7): the legacy
   <=390px "Mobile Hero 专项放大" block that lived here has been
   REMOVED. It previously established a second, independent Hero/
   typography visual system for 320-390px that diverged from the
   391-1024px system at the 391px boundary (different --hud-w,
   different --stage-content margin formula, different --headline
   font-size mechanism, etc.), which is exactly the "two different
   designs at 390 vs 430" defect confirmed by real QA. 320-1024px
   portrait is now owned by ONE continuous block below, driven by
   exactly one --hero-scale variable (desiredScale = viewportWidth /
   390), so 390 and 430 are now the same composition at two scale
   factors, not two different layouts. */

/* =========================================================
   Portrait Responsive Adaptation（追加于文件末尾，优先级靠 source
   order 覆盖前面所有 min-width:431/600/1024 及 1024–1439 的旧
   tablet/desktop 规则，不删除任何旧规则，也不用 !important）
   =========================================================
   核心判断依据只有 orientation + viewport width，不针对任何设备型号。
   覆盖范围：竖屏 <=1024px 的所有内容全部改用「Mobile 单列结构 +
   按 viewport 比例放大」，横屏（包含 1024x768 / 1180x820 / 1366x768 /
   1440x900）完全不受影响 —— 因为每一条新规则都显式带
   (orientation: portrait)，横屏永远不会命中这些 selector。
   390–430px 的既有 max-width:430px 区块（Slogan/CTA 上一轮已锁定）
   完全不touch：下面的结构重置块虽然覆盖到 0–1024px 全宽，但只重置
   flex-direction/order/frame 定位这些"即使在 390px 也没有冲突"的属性，
   不重新声明 .headline / .cta-btn 的 font-size/gradient/width/height，
   所以 390–430px 的最终视觉和上一轮确认结果完全一致。
   ========================================================= */

/* ---- 结构重置（0–1024px 全部竖屏共用）----------------------------
   目的：任何 <=1024px 的竖屏，无论宽度落在哪个 tier，都强制单列、
   人物在上文字在下（对应 HTML 里 .stage-visual 在前 .stage-content
   在后的 DOM 顺序），并让 .page 用 flex column + min-height:100svh
   撑满视口，footer 保持在正常流里、贴近视口底部而不是被巨大空白
   挤到很下面。1024px 桌面双栏规则（min-width:1024px 那个 block）
   本身不删除，只是在竖屏这个交集里被下面这几条同 specificity、
   source order 更靠后的规则覆盖掉。 */
@media (orientation: portrait) and (max-width: 1024px) {
  .page {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    min-height: 100svh;
  }

  /* Desktop 的 min-width:1024px block 给 .main 加了
     justify-content:center + min-height:calc(100vh-130px)（把
     .hero-stage 在 .main 里垂直居中，双栏布局专用）。1024×1366
     竖屏这个宽度会同时命中 desktop block 和这里，必须显式重置回
     "从顶部开始排、不额外撑高度"，否则会和下面新的 scene 实际
     高度叠加出诡异的居中效果。 */
  .main {
    flex: 1;
    justify-content: flex-start;
    min-height: 0;
  }

  /* Desktop 的 min-width:1024px block 给 .top-bar 加了
     justify-content:flex-start + padding:24px 0 0（logo 靠左，
     双栏布局专用的顶栏）。1024×1366 竖屏命中这条 desktop 规则后，
     顶部 ONEPLAY Logo 会被推到左上角——这是本轮要修的 bug 之一。
     这里显式改回和 base 规则一样的居中 + padding，320–1024px
     竖屏的顶部 Logo 水平居中方式统一。 */
  .top-bar {
    justify-content: center;
    padding: 16px 0 2px;
  }

  .hero-stage {
    flex-direction: column;
    align-items: stretch;
    gap: 0;
  }

  /* order 显式重置：desktop 规则给 .stage-content order:1、
     .stage-visual order:2（用来在 row 方向把文字放左边）。竖屏这里
     即使某些宽度本来就没被 desktop block 命中过（<1024px 时
     desktop block 根本不生效），显式重置也是无害的，统一保证
     "人物在上、文字在下"这条规则不依赖任何隐式默认值。 */
  .stage-visual {
    order: 0;
    flex: 0 0 auto;
    max-width: 100%;
    /* margin-bottom 改成 0（原来是 6px）：431–1024px 连续 geometry
       block 里 .stage-content 的 margin-top 已经用
       --portrait-real-overflow-y + --portrait-content-gap 独立算好了
       完整的 gap，这里的 6px 是重复叠加的第二份 spacing——真机
       Playwright 实测 431/600/768/820/853/1024 六个点的 actualGap
       全部比目标值多 6px（1024 那组 24+6=30px 是最直接的证据），
       删掉这个 6px 之后，"人物视觉底部→WELCOME"这段间距只由
       .stage-content margin-top 这一个机制负责。 */
    margin: 4px 0 0;
  }

  .stage-content {
    order: 1;
    flex: 0 0 auto;
    /* FIX PORTRAIT TABLET SYMMETRY ONLY round: replaces the previous
       width:auto + max-width:94vw + margin-inline:-bleed trio.
       That trio was only self-consistent while bleed > 0 (phones).
       Once bleed reached its 0px floor and max-width:94vw became the
       smaller (capping) value, margin-inline was still literally 0/0
       -- a capped box with 0/0 margins sits flush at the parent's
       LEFT edge, dumping 100% of the leftover slack onto the right
       side only. That was measured as a deterministic, growing
       left-drift (1.6px at 720px -> 10.73px at 1024px), not noise.
       --portrait-copy-width (defined above, alongside
       --portrait-copy-bleed) is now the ONE formula that decides the
       used width in every case: min(parent + 2*bleed, 94vw). width
       and margin-inline below both consume that SAME value, so the
       margin is always algebraically derived from whichever width
       actually won, not a separately-guessed constant -- there is no
       second, independently-capping max-width left to conflict with
       it (removed). See the long comment on --portrait-copy-width
       above for the full phone-vs-tablet algebra. */
    width: var(--portrait-copy-width);
    margin-inline: calc((100% - var(--portrait-copy-width)) / 2);
    /* Desktop 的 min-width:1024px block 把 .stage-content 改成
       text-align:left + transform:translateY(-7px)（双栏布局里文字
       靠左、和右侧人物做像素级微调）。1024×1366 竖屏必须显式改回
       居中、且清掉这个 -7px 位移，否则 WELCOME/Slogan/subtitle/CTA
       会整体左对齐、和 431–853px 的视觉不一致。 */
    text-align: center;
    transform: none;
  }

  /* mascot/ambassador 水平位置统一回到和 390–430px 一致的 13%/13%，
     覆盖掉 431px(17%)/600px(31.9%)/1024px(10%)/1024-1439px(80px)
     这些为「双栏或窄容器」调过的旧值。第二版起 .hero-characters 自己
     有了一个连续增长的 max-width（见下面的连续 portrait geometry
     block），13%/13% 现在是相对那个居中、有上限宽度的 group 计算，
     而不是相对整个 stage-visual，这样宽屏时两人不会被继续往两侧
     viewport 边缘拉开。 */
  /* bottom 显式重置：桌面 min-width:1024px block 的 .mascot-frame/
     .ambassador-frame 把 bottom 改成了 calc(mascot-h - ambassador-
     scale)，比 base 规则少了 "+18px" 这个常数（桌面双栏布局本身
     不需要这 18px 安全边）。这条桌面规则只按 min-width:1024px 生效，
     和 orientation 无关——1024×1366 竖屏同时满足 min-width:1024px
     和 orientation:portrait 两个条件，如果这里不显式把 bottom 也
     重置回 base 公式，桌面那条"少18px"的规则会在恰好 1024px 宽的
     竖屏视口上生效，人物整体多下移 18px，这正是 1024 端实测溢出量
     （95px）比连续趋势预测值（约77px）多出约18px的真实原因。 */
  .mascot-frame {
    left: 13%;
    bottom: calc(18px + var(--mascot-h) - var(--ambassador-scale));
  }

  .ambassador-frame {
    right: 13%;
    bottom: calc(18px + var(--mascot-h) - var(--ambassador-scale));
  }

  .site-footer {
    text-align: center;
  }

  .footer-links {
    justify-content: center;
  }
}

/* ---- 431–1024px 竖屏：单一连续 geometry block（第三版，修正第二版
   的坐标系 bug，不是继续调数值）------------------------------------
   第二版的真实 bug（已用手工矩阵推导确认，不是靠猜）：
   1) 水平方向：给 .hero-characters 同时写了 left:50%（相对 stage-visual
      的 50%）和 transform-origin:50%（相对 .hero-characters 自身宽度
      的 50%），这两个"50%"是相对不同基准的两次独立偏移，叠加之后
      "有效缩放中心"被推到偏离 stage-visual 中心的位置（宽度差越大——
      也就是 --portrait-group-w 比可用宽度窄得越多——偏移越夸张），
      导致 scaleX 把 mascot 往一侧、ambassador 往另一侧不对称地推出去，
      这正是"ambassador跑到viewport外面"的直接原因。
   2) 垂直方向：--hud-w 相对 --stage-h 定得太大（例如 600px 时 HUD
      渲染高度约608px，但 --stage-h 只有434px），HUD 底边远远超出
      stage-visual 的正常流高度，伸进了 .stage-content 的区域，这才是
      "HUD压住WELCOME/Slogan"的真正原因——和 margin-top 公式本身无关，
      我之前只检查了角色的溢出量，完全没检查 HUD 自己是否会溢出
      stage-h，这是第二版的疏漏。
   本版修复方式：
   - .hero-characters 撤回到和 390–430px 完全一样的模式（不设
     left/width/right/top/bottom，直接吃 base 规则的 inset:0，
     transform 里也去掉 translateX），彻底消除"双重50%"问题。
   - "人物不能分太开"改成调整 .mascot-frame / .ambassador-frame 自己的
     left%/right%（相对 inset:0 的 .hero-characters，等价于相对
     stage-visual 本身，坐标系统一、无嵌套偏移），用一条连续公式反推
     出能让"两人组合可视宽度"落在你给的占比目标区间的百分比，不再
     需要额外的 group 容器。
   - --hud-w 大幅调小（跟随本轮 I 给的新目标），--stage-h 改成按
     "实际需要容纳 HUD 渲染高度 + 上下净空"反推，确保 HUD 底边不再
     超出 stage-h（下面完成报告里逐点验证过，5个采样点 HUD 溢出量
     全部 <=0，即 HUD 完全落在 stage-visual 盒子内）。
   角色 scaleY>1 造成的"脚部超出 stage-h 名义盒子"这件事本身是这套
   "从人物视觉顶部往下放大"技术的数学必然结果，和 390–430px 已锁定
   版本用的是完全相同的原理（该版本也有类似量级的 margin-top 补偿），
   把 scaleY 保持 >1 的前提下无法把这个补偿量简化成一个固定 20px——
   详见对话里这一轮的完成报告，里面有推导和你可以选择的取舍方案。 */
@media (orientation: portrait) and (min-width: 320px) and (max-width: 1024px) {
  :root {
    /* .page 在这个区间不再吃旧的 --max-width（600px 时 520px /
       1024px 时 1200px 那两级），直接用 max-width:none（见下面
       .page 规则）+ 20px 左右 padding，让内容宽度 = 视口 - 40px，
       全程线性连续，不会在 600/1024 这两个旧断点产生页面宽度跳变。 */

    /* =====================================================
       PORTRAIT RESPONSIVE FINAL FIX — STRUCTURAL CORRECTION
       =====================================================
       ROOT CAUSE of the previous round's failure (confirmed by real
       Chromium QA, not guessed): mascot-frame/ambassador-frame's
       left:13%/right:13% resolved against .hero-characters' width,
       which equalled .stage-visual's box — a box that grows with
       viewport width. So Hero WIDTH silently grew with viewport even
       while --hero-scale stayed pinned at 1 (the JS solver's floor
       probe at scale=1 already failed Gate B, so it never searched
       past the floor at all — every viewport reported heroScale=1).

       THE FIX: introduce ONE fixed-size, heroScale-independent master
       coordinate system — .stage-visual-master (new wrapper, added in
       index.html, zero content/semantic change) — sized at exactly the
       390 master's own content-box dimensions (350x280px, literal, not
       vw/%/viewport-relative in any way). ALL Hero-core geometry below
       is now a LITERAL 390-master constant (no --hero-scale
       multiplication anywhere inside the master). --hero-scale is
       applied exactly ONCE, atomically, as transform:scale() on
       .stage-visual-master itself — so HUD/brandmark/mascot/ambassador
       necessarily scale together, by construction, and cannot drift
       out of relative sync the way per-child formulas could.

       .stage-visual (the ORIGINAL element) is no longer a visual
       container itself — it is now purely the flow RESERVATION SLOT:
       its real height is set by assets/js/portrait-fit.js via actual
       getBoundingClientRect() measurement of the real, rendered Hero
       union bbox (never a formula, never estimated), so .stage-content
       always starts exactly where the real scaled Hero visually ends,
       at any --hero-scale, with no negative margins and no per-width
       patches. */
    --hero-scale: 1;
    /* round 7: lower clamp bound relaxed 1 -> 0.90 so typography/CTA can
       shrink slightly below the master when --hero-scale < 1 (short
       viewports such as 540x720, height-constrained) instead of being
       floored at the 390-master size and overflowing. At --hero-scale:1
       this still resolves to exactly 1 (no change to 390/430). */
    --secondary-scale: clamp(
      0.90,
      calc(1 + (var(--hero-scale) - 1) * 0.35),
      1.25
    );

    /* Pin every custom property the earlier width-only, non-portrait
       breakpoints (min-width:431/600px, still above and still
       correctly governing LANDSCAPE at those widths) would otherwise
       leak into this PORTRAIT range through shared :root — same
       specificity, this block is later in source order. */
    --max-width: none;
    --brandmark-opacity: 0.55;
    --cta-breath-scale: 1.035;
    --logo-left: 47%;

    /* ---- Hero-core geometry: LITERAL 390-master constants, no
       --hero-scale multiplication. The scaling authority is entirely
       .stage-visual-master's own transform:scale(), applied once,
       below. These are read directly off the <=390px block at w=390,
       with --mobile-fit-scale=1: stage-h=280px; mascot-h=52vw=
       202.8px; ambassador-scale=60.5vw=235.95px; hud-w=130% of 350px
       content width=455px; brandmark-w=33% of 350px=115.5px;
       brandmark-top=0.56*280+0.1789*350=219.415px; characters-shift-
       y=-18px; frame bottom offset constant=18px; .stage-visual-
       master shift=24px. scaleX(1.48)/scaleY(1.58) on .hero-characters
       are the fixed SHAPE ratio of the master and stay exactly as-is —
       part of the frozen composition, not part of the outer scale. */
    --stage-h: 280px;
    --mascot-h: 202.8px;
    --ambassador-scale: 235.95px;
    --hud-w: 455px;
    --brandmark-w: 115.5px;
    --brandmark-top: 219.415px;
    --characters-shift-y: -18px;
    --master-shift-y: 24px;
    --frame-bottom-offset: 18px;

    /* ---- Secondary: typography / CTA / logo / rhythm — all driven
       by the single --secondary-scale clamp above, nothing else.

       FINAL PROMO RESPONSIVE round: the promo copy needs much stronger
       visual weight (brief Part C3/C6/C7). Every base px reference
       below is simply raised (still multiplied by the SAME
       --secondary-scale as before, still zero new variables, zero new
       scaling authority) so the 390-430px master (--secondary-scale=1
       there) lands inside the requested target bands: headline
       18.72->28px (target 26-30px, heavy 800-weight already set on
       .headline below), eyebrow 11->14px (target 13-15px), subhead
       14->16px (target 15-17px), cta font 18->19px (target 18-20px).
       --cta-height stays at 60px -- already inside the 58-64px target,
       no change needed. Vertical rhythm (brief Part C7, at the master):
       hero-content-gap (Hero->eyebrow) 12->18px (target 14-22px),
       eyebrow-mb (eyebrow->headline) 15->10px (target 6-12px),
       headline-mb (headline->subtitle) 18->15px (target 12-18px),
       subhead-mb (subtitle->CTA, plus the separate 3px cta-mt-extra
       below = 22px total) 21->19px (target 18-24px). --cta-width
       changed from an independent 314px-based cap to 100% of
       .stage-content's own (now 92vw-capped, see above) width, so the
       CTA "visually uses approximately the same width" as the copy
       column above it at every viewport, per brief Part C2 -- rather
       than staying pinned near its old ~370-390px cap while the copy
       column above it grows much wider on large portrait/tablet
       screens. microcopy stays untouched (10px/17px) -- the node is
       empty (no visible microcopy text this round, see i18n.js), so
       neither of these two values render any visible text.
       --hero-content-gap remains purely the small aesthetic breathing
       margin below the Hero — the STRUCTURAL "don't collide"
       requirement is still handled entirely by portrait-fit.js's real
       measurement of .stage-visual's reserved height, not by this
       formula; that measurement/solver mechanism itself is completely
       unchanged this round (see portrait-fit.js -- not modified). */
    /* FINAL TYPOGRAPHY ENLARGEMENT round (Part E): further increase on
       top of the above, at the 390-430px master (--secondary-scale=1):
       headline 28->32px (target 30-34px, weight bumped 800->900 on
       the portrait .headline rule below so it visually competes with
       the Hero), eyebrow 14->15px (target 14-16px), subhead 16->17px
       (target 16-18px), cta font 19->20px (target 19-21px), cta
       height 60->63px (target 60-66px). All vertical rhythm variables
       (--eyebrow-mb/--headline-mb/--subhead-mb/--cta-mt-extra/
       --hero-content-gap) are intentionally left unchanged this round
       -- Part A locks the "CTA/content vertical anchor system", so
       only font-size/weight/height change, never spacing. */
    --headline-font: calc(32px * var(--secondary-scale));
    --eyebrow-font: calc(15px * var(--secondary-scale));
    --subhead-font: calc(17px * var(--secondary-scale));
    --microcopy-font: calc(10px * var(--secondary-scale));
    --cta-font: calc(20px * var(--secondary-scale));
    --cta-width: 100%;
    --cta-height: calc(63px * var(--secondary-scale));
    --logo-h: calc(40px * var(--secondary-scale));
    --eyebrow-mb: calc(10px * var(--secondary-scale));
    --headline-mb: calc(15px * var(--secondary-scale));
    --subhead-mb: calc(19px * var(--secondary-scale));
    --cta-mt-extra: calc(3px * var(--secondary-scale));
    --microcopy-mt: calc(17px * var(--secondary-scale));
    --hero-content-gap: calc(18px * var(--secondary-scale));

    /* PORTRAIT COPY WIDTH FINAL FIX round: symmetric horizontal bleed
       for .stage-content, added ON TOP OF the existing width:100%/
       max-width:94vw system below -- this variable does NOT replace
       that system, it works alongside it.
       .stage-content's own box was exactly equal to .main's content
       box (viewport - 40px, from .page's 20px/side padding) at every
       phone width, which is what produced the "wall" -- side margins
       pinned at a flat 20px no matter how the copy grew.
       Formula: clamp(0px, calc(20px - 3vw - 0.3px), 7px). Continuous,
       no breakpoint. Derived directly from the constraint "the bled
       width must never exceed the existing max-width:94vw cap" (so
       the two mechanisms never fight each other / never trigger the
       CSS "over-constrained margins" rule, which would otherwise
       silently break left/right symmetry): solving
       (viewport-40)+2*bleed <= 0.94*viewport for bleed gives
       bleed <= 20 - 0.03*viewport, i.e. 20px - 3vw in px+vw units;
       the extra -0.3px is a small safety epsilon against sub-pixel
       rounding at the boundary. Clamped to a 7px ceiling (matching
       the requested ~11-13px final side margin, since margin =
       20px - bleed) and a 0px floor so it never goes negative/
       inverts into a positive margin at wide widths -- at the
       existing ~666.7px crossover where max-width:94vw already takes
       over unassisted, this formula independently fades to ~0, so it
       hands off to the pre-existing tablet behavior with zero
       discontinuity and zero change to 768-1024px composition.
       Verified by direct arithmetic at all 4 required viewports:
       375: bleed=7.00 -> width=349.0 (352.5 cap, 3.5px clear)
       390: bleed=7.00 -> width=364.0 (366.6 cap, 2.6px clear)
       414: bleed=7.00 -> width=388.0 (389.2 cap, 1.2px clear)
       430: bleed=6.80 -> width=403.6 (404.2 cap, 0.6px clear)
       In every case the max-width cap has real clearance and never
       engages, so margin-left and margin-right stay exactly equal
       (both -bleed) -- centerX is therefore unchanged by construction,
       not just by measurement (see .stage-content below). */
    --portrait-copy-bleed: clamp(0px, calc(20px - 3vw - 0.3px), 7px);

    /* FIX PORTRAIT TABLET SYMMETRY ONLY round: single source of truth
       for .stage-content's actual used width, replacing the previous
       separate width:auto + max-width:94vw + margin-inline:-bleed
       trio. That trio worked correctly only while bleed > 0 (phones):
       width solved itself out to (parent + 2*bleed) with margin-inline
       honoring the negative value on both sides symmetrically. Once
       bleed reaches its 0px floor (~666.7px viewport and up) AND
       max-width:94vw is the smaller of the two (i.e. once it actually
       caps the box below the parent's own width), margin-inline was
       still explicitly 0/0 (not auto) -- so the capped box simply sat
       flush against the parent's LEFT edge, and 100% of the leftover
       slack (parent width - 94vw) landed on the right side only. That
       is the measured, deterministic left-drift (1.6px at 720px,
       growing to 10.73px at 1024px) -- not noise, exactly as
       diagnosed: an under-constrained-in-the-wrong-way margin, not a
       bleed problem (--portrait-copy-bleed itself is correct and
       untouched).
       --portrait-copy-width is the ONE formula that decides the used
       width in every case: min(parent + 2*bleed, 94vw) -- expressed
       here as 100% (=parent, since this variable is only ever
       consumed by properties whose percentage basis is .stage-content's
       own containing block, .hero-stage/.main's stretched box) plus
       bleed twice, capped at 94vw. Whichever branch of the min() wins,
       .stage-content's own width: var(...) declaration below and the
       margin-inline: calc((100% - var(--portrait-copy-width)) / 2)
       declaration both reference the SAME single value -- so the
       margin is always ALGEBRAICALLY DERIVED from the actual width
       actually used, not a separately-guessed constant. That is what
       makes both branches correct at once:
         PHONE   (parent+2*bleed < 94vw): --portrait-copy-width
                 resolves to parent+2*bleed (bigger than parent) ->
                 margin = (parent - (parent+2*bleed))/2 = -bleed on
                 each side -- identical to the previous, already-
                 accepted phone bleed result, not just "similar".
         TABLET  (94vw < parent, i.e. bleed already at its 0px floor):
                 --portrait-copy-width resolves to 94vw (smaller than
                 parent) -> margin = (parent - 94vw)/2, a POSITIVE
                 value, split exactly in half on each side -- true
                 centering, not the old flush-left placement.
       There is no other max-width on .stage-content anymore (removed
       below) -- this is the only width-capping mechanism, so there is
       no way for two competing constraints to re-create the
       over-constrained-margin bug this round and the previous round
       both ran into. */
    --portrait-copy-width: min(
      calc(100% + var(--portrait-copy-bleed) + var(--portrait-copy-bleed)),
      94vw
    );
  }

  /* One-screen, no-scroll structural rules. round 7: this now covers
     320-1024px uniformly (widened from 391px) — 390 no longer has a
     separate mechanism; it is just this same block evaluated with
     --hero-scale naturally resolving to 1. overflow:hidden remains the
     safety net against a hard crop at every width in range. */
  html,
  body {
    height: 100%;
  }
  body {
    overflow: hidden;
  }

  .page {
    max-width: none;
    padding: 0 20px;
    height: 100vh;
    height: 100svh;
    min-height: 0;
    overflow: hidden;
  }

  .main {
    /* PORTRAIT COPY WIDTH FINAL FIX round: hidden -> visible, portrait
       only. .stage-content now deliberately extends past .main's own
       content-box edges by --portrait-copy-bleed on each side (a
       standard negative-margin bleed, see .stage-content below) --
       with overflow:hidden here, that extension would be clipped,
       silently cancelling the whole fix. This is not a "maybe": it is
       a direct, structural consequence of the CSS overflow model
       (overflow:hidden clips ANY child box that extends past the
       padding edge, however that extension was produced -- negative
       margins are one of the most standard ways to trigger exactly
       this), so it is applied together with the bleed rather than
       waiting on a real-browser run to "discover" it. .page's own
       overflow:hidden (below, outside this rule) is UNCHANGED and
       remains the outer one-screen-fit safety boundary, exactly as
       required -- nothing here removes that safety net, it only stops
       .main from ALSO clipping, one layer further in. */
    overflow: visible;
  }

  .site-footer {
    flex: 0 0 auto;
  }

  /* .stage-visual: no longer carries any transform or visual geometry
     of its own — it is the pure flow reservation slot. Its real height
     is set by assets/js/portrait-fit.js (reserveStageVisualHeight()),
     via real getBoundingClientRect() measurement of the master's
     actual rendered position, every time --hero-scale changes. The
     literal --stage-h fallback below (280px, the master's own
     unscaled height) is only the pre-JS/no-JS state. */
  .stage-visual {
    height: var(--stage-h);
    max-width: 1000px; /* safety cap only, content width tops out at 984px */
  }

  /* .stage-visual-master: the fixed, heroScale-independent 390
     coordinate system. Literal 350px = the 390 master's own content
     width (390 - 2*20px padding). Centered inside the wider
     .stage-visual via margin:auto, so --hero-scale's transform-
     origin:top center bleeds symmetrically around .stage-visual's own
     horizontal center — matching the master's already-symmetric bleed,
     with no separate horizontal-position formula needed. translateY is
     written before scale() (not after) so the 24px shift stays a
     literal screen-space offset, unaffected by --hero-scale — same
     technique already proven in the <=390px master block. */
  .stage-visual-master {
    position: relative;
    width: 350px;
    height: var(--stage-h);
    margin: 0 auto;
    overflow: visible;
    transform: translateY(var(--master-shift-y)) scale(var(--hero-scale));
    transform-origin: top center;
  }

  /* .hero-characters: inherits the base rule's inset:0 relative to
     .stage-visual-master's now-FIXED 350x280px box (never the growing
     .stage-visual box) — this is the actual structural fix. scaleX/
     scaleY are the fixed 390-master shape ratio; --frame-bottom-offset/
     --mascot-h/--stage-h are now literal constants too, so this
     transform-origin is a fixed point, identical to the 390 master. */
  .hero-characters {
    transform-origin: 50%
      calc(var(--stage-h) - var(--frame-bottom-offset) - var(--mascot-h));
    transform: translateY(var(--characters-shift-y)) scaleX(1.48)
      scaleY(1.58);
  }

  /* Horizontal position: fixed 13%/13%, identical to the 390 master,
     now resolved against the FIXED 350px .stage-visual-master box —
     always exactly 45.5px from each edge, at every viewport width and
     every --hero-scale. This is what makes Hero width scale strictly
     with --hero-scale instead of drifting with viewport width. */
  .mascot-frame {
    left: 13%;
    bottom: calc(
      var(--frame-bottom-offset) + var(--mascot-h) - var(--ambassador-scale)
    );
  }
  .ambassador-frame {
    right: 13%;
    bottom: calc(
      var(--frame-bottom-offset) + var(--mascot-h) - var(--ambassador-scale)
    );
  }

  .logo {
    height: var(--logo-h);
  }

  .headline {
    /* FINAL PROMO RESPONSIVE round: white-space:nowrap removed again.
       The previous round's revert was correct for the smaller 18.72px
       base size (every language's copy fit on one line there), but
       this round raises the headline base to 28px (target 26-30px,
       brief Part C3) specifically so it has strong visual weight --
       at that size, on narrow phones (320-360px, ~92vw content column
       ~294-331px), longer headlines like MS "Deposit Dapat Bonus 185%"
       or EN "Get 185% Bonus on Deposit" will not reliably fit on one
       line, and nowrap would either clip them or (if a browser/user
       zoom shrank the font to compensate) silently fight the master
       scale system. Brief Part C4 explicitly requires allowing natural
       wrapping up to 2 lines instead of forcing a tiny single-line
       font or clipping. overflow-wrap:break-word is a safety net only
       (prevents one single very long unbroken token from overflowing);
       normal word-spacing wrapping is what actually produces the
       2-line layout for longer strings. max-width:none is unchanged --
       the headline's box is simply .stage-content's own width (now
       92vw-capped, see above), no separate cap. */
    overflow-wrap: break-word;
    word-wrap: break-word;
    font-size: var(--headline-font);
    /* FINAL TYPOGRAPHY ENLARGEMENT round (Part E): 800 -> 900,
       portrait-scoped only (the base, non-portrait .headline rule
       used by Desktop keeps its own 800 -- Part J did not request a
       Desktop weight change). Combined with the --headline-font bump
       above, this is what makes the headline "visually compete with
       the Hero" at the 390-430 master. */
    font-weight: 900;
    line-height: 1.08;
    letter-spacing: -0.02em;
    max-width: none;
    margin: 0 0 var(--headline-mb);
    background: linear-gradient(
      90deg,
      #f4f7fc 0%,
      #e7f0ff 28%,
      #bcd5ff 62%,
      #83aeef 100%
    );
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
  }

  .headline .hl {
    background: none;
    -webkit-background-clip: initial;
    background-clip: initial;
    -webkit-text-fill-color: inherit;
    color: inherit;
    text-shadow: none;
  }

  .eyebrow {
    font-size: var(--eyebrow-font);
    margin-bottom: var(--eyebrow-mb);
  }

  /* FINAL PROMO RESPONSIVE round (brief Part C5): the base .eyebrow
     rule's letter-spacing:0.22em (defined once, outside any media
     query, so it otherwise applies everywhere including here) is wide
     "premium" tracking that reads fine for EN/MS's Latin script but
     visibly over-spaces already-dense Chinese/Thai glyphs. Scoped to
     portrait only (desktop typography is locked this round, out of
     scope for Part C) via the existing html[lang] state i18n.js
     already sets synchronously on load/switch -- no new JS, no
     per-language layout branching, just one CSS override reusing that
     existing attribute. EN/MS are untouched (still inherit the base
     0.22em -- "moderate premium tracking is acceptable" per the
     brief). */
  html[lang="zh"] .eyebrow,
  html[lang="th"] .eyebrow {
    letter-spacing: 0.05em;
  }

  .subhead {
    font-size: var(--subhead-font);
    margin: 0 0 var(--subhead-mb);
  }

  .cta-microcopy {
    font-size: var(--microcopy-font);
    margin: var(--microcopy-mt) 0 0;
  }

  .cta-btn {
    width: var(--cta-width);
    max-width: none;
    height: var(--cta-height);
    font-size: var(--cta-font);
    padding: 0;
    margin-left: auto;
    margin-right: auto;
    margin-top: var(--cta-mt-extra);
  }

  /* Hero-to-content gap: purely the small aesthetic breathing margin
     now (--hero-content-gap, secondary-scaled, ~12-15px). The
     STRUCTURAL "no collision" guarantee comes from .stage-visual's
     real measured height (set by portrait-fit.js), not from this
     formula — .stage-content simply follows it in normal flow. */
  .stage-content {
    margin-top: var(--hero-content-gap);
  }
}

/* round 7: the legacy 320-390px "短屏 one-screen fit" override block
   (--mobile-fit-scale as an independent transforming authority applied
   via a SECOND transform/scale on .stage-visual, on top of the
   --hero-scale system above) has been REMOVED. --hero-scale is now the
   ONLY Hero-sizing authority for all of 320-1024px portrait — short
   viewports such as 540x720 are handled by the unified solver in
   assets/js/portrait-fit.js choosing a --hero-scale below 1 via the
   same binary search used everywhere else, not by a second, separate
   scale mechanism layered on top. --mobile-fit-scale is still WRITTEN
   by portrait-fit.js as a telemetry mirror of the final resolved
   --hero-scale (for QA/debugging visibility only); no CSS rule reads
   it anymore. */
