/* ══════════════════════════════════════════════════════════════════════
   星图邀请码分享页 · 样式（作用域版）
   ──────────────────────────────────────────────────────────────────────
   来源：设计方交付的 xingtu-invite 源码包（styles/tokens.css + base.css +
   components.css），由 tools/scope-invite-css.py 机械转换而来，**规则内容
   一字未改**，只做了三类适配：

   ① 作用域：源站是独立页面（body 即画布），H5 里画布是 #view 内的 `.iv`
      容器。故 html/body 复位类规则整条丢弃，body 规则改写到 `.iv`，
      其余选择器统一加 `.iv ` 前缀 —— 否则 `.toast / .copy / .body / .foot`
      这些通用类名会与 H5 主样式表互相污染。
   ② 入场编排：源页用 `html.js` / `body.is-ready` 两个钩子，这里换成根元素
      上的 `.iv--js` / `.iv--ready`（JS 里加，见 app.js 的 vInvite）。
   ③ 视口 → 画布：源页按 vw/vh 定尺寸、断点按视口宽判断；H5 的画布是被钉死
      的 .phone（≤420px），两者不等价。见文件末尾「画布尺寸适配」段。

   改版时请回源码目录改，然后重跑转换脚本；**不要只改这份产物**。
   ══════════════════════════════════════════════════════════════════════ */

/* ─────────── 源文件：tokens.css ─────────── */
/* ══════════════════════════════════════════════════════════════════════
   设计令牌 · 星图科技邀请码分享页
   色值 100% 取自品牌手册《BRAND COLOR SYSTEM》，一个新增色都没有 ——
   邀请页是品牌触点，自创颜色等于稀释品牌。
   ══════════════════════════════════════════════════════════════════════ */
.iv {
  /* ── 品牌原色：改动前请先看品牌手册 ── */
  --brand-violet: #6e4fe9; /* PRIMARY VIOLET */
  --brand-black: #000000; /* DEEP BLACK */
  --brand-violet-soft: #9ba6ff; /* LIGHT VIOLET */
  --brand-lavender: #e3dff8; /* LAVENDER */
  --brand-grey: #f2f2f2; /* GREY */
  --brand-yellow: #ffd200; /* ACCENT YELLOW */

  /* ── 语义别名：组件只许用这一层 ── */
  --color-bg: var(--brand-black);
  --color-surface: #0b0a14; /* 近黑带紫偏（票卡下半区） */
  --color-text: var(--brand-lavender); /* 对黑 16.0:1 */
  --color-muted: #a9a5be; /* 次级文字，对黑 8.8:1 */
  --color-ink: #08080a; /* 浅色底上的字 */
  --color-accent: var(--brand-yellow); /* 单页唯一强调色 → 只留给"复制"这个动作 */
  --color-line: rgba(227, 223, 248, 0.14);
  --color-line-strong: rgba(227, 223, 248, 0.3);

  /* ── 字体 ──
     拉丁展示字用 Chakra Petch：切角几何体，与 XT 标记的 45° 切角同源。
     中文走系统字面（PingFang / HarmonyOS / MiSans / 思源），零下载字节 ——
     完整中文字库动辄 3~8MB，在微信里加载等于劝退，这是中文 H5 的正确工程取舍。 */
  --font-cn: "PingFang SC", "HarmonyOS Sans SC", "MiSans", "Noto Sans SC",
    "Source Han Sans SC", "Microsoft YaHei", sans-serif;
  --font-display: "Chakra Petch", var(--font-cn);
  --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Consolas, monospace;

  /* ── 字阶（液体，375 → 1440 一把梭） ── */
  --fs-hero: clamp(34px, 10.4vw, 58px);
  --fs-code: clamp(23px, 7.4vw, 34px);
  --fs-lead: clamp(14px, 3.9vw, 16px);
  --fs-body: 15px;
  --fs-label: 12px;
  --fs-micro: 11px;

  /* ── 间距 ── */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 16px;
  --sp-4: 24px;
  --sp-5: 40px;
  --sp-6: 64px;
  --sp-7: 96px;

  /* ── 首屏高度 ──
     斜切光板的几何全部由这个变量推算（见 components.css 的 .beam）。
     早先按 body 百分比算高度，结果页面总高一变、光板就漂，
     宽屏下亮部直接压到 eyebrow 上，实测对比度掉到 1.70:1。
     锚定首屏高度之后，光板与文案的相对位置在任何断点都恒定。

     窄屏从 40vh 收到 32vh：容器里只放一个 min(76vw,336px) 的标记（375 下约
     173px 高），40vh 会多留出上下各 ~76px 的空档，整页被顶到 1272px ——
     手机上得滚动两次才看得完。收到 32vh 后上下各留 ~43px，仍然透气，
     但整页能落进一张截图里。 */
  --hero-h: clamp(240px, 32vh, 320px);

  /* ── 圆角 / 描边 ── */
  --r-sm: 8px;
  --r-md: 14px;
  --r-lg: 22px;
  --r-pill: 999px;

  /* ── 阴影：三级高度，带紫环境色而不是纯黑，暗底上才有"光感" ── */
  --shadow-1: 0 2px 8px rgba(0, 0, 0, 0.5);
  --shadow-2: 0 8px 28px rgba(0, 0, 0, 0.55);
  --shadow-3: 0 24px 64px rgba(23, 10, 74, 0.72), 0 0 0 1px rgba(227, 223, 248, 0.08);

  /* ── 动效 ── */
  --dur-fast: 160ms;
  --dur-base: 320ms;
  --dur-slow: 640ms;
  --dur-scene: 760ms;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-soft: cubic-bezier(0.32, 0.72, 0, 1);
}

/* ─────────── 源文件：base.css ─────────── */
/* ══════════════════════════════════════════════════════════════════════
   基础层：字体加载、重置、排版、焦点、动效降级
   ══════════════════════════════════════════════════════════════════════ *//* ── 自托管拉丁字体：只取真正用到的字重，latin 子集各 10~22KB ── */
@font-face{
  font-family: "Chakra Petch";
  src: url("/assets/fonts/chakra-petch-latin-500-normal.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face{
  font-family: "Chakra Petch";
  src: url("/assets/fonts/chakra-petch-latin-600-normal.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

@font-face{
  font-family: "JetBrains Mono";
  src: url("/assets/fonts/jetbrains-mono-latin-500-normal.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face{
  font-family: "JetBrains Mono";
  src: url("/assets/fonts/jetbrains-mono-latin-700-normal.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

.iv *, .iv *::before, .iv *::after {
  box-sizing: border-box;
}

.iv {
  min-height: 100dvh;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-cn);
  font-size: var(--fs-body);
  line-height: 1.75; /* 中文正文行高 ≥1.7 */
  letter-spacing: 0.02em;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

.iv img {
  display: block;
  max-width: 100%;
  height: auto;
}

.iv button {
  font: inherit;
  color: inherit;
  border: 0;
  background: none;
  cursor: pointer;
}
/* 焦点圈：键盘用户必须看得见（黄色 + 2px 偏移） */
.iv :focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

.iv ::selection {
  background: var(--brand-yellow);
  color: var(--color-ink);
}
/* 无障碍：只给读屏的文本 */
.iv .sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
/* ── 动效降级：关掉全部动画，内容直接呈现 ── */
@media (prefers-reduced-motion: reduce){

.iv *, .iv *::before, .iv *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }

.iv .reveal {
    opacity: 1 !important;
    transform: none !important;
  }
}

/* ─────────── 源文件：components.css ─────────── */
/* ══════════════════════════════════════════════════════════════════════
   构成层：氛围 → 首屏 → 文案 → 邀请凭证 → 页脚 → 动效编排 → 响应式
   ══════════════════════════════════════════════════════════════════════ *//* ════════════ 1. 氛围层 ════════════
   暗底页最忌讳"纯色平铺"。这里三层叠加：
   斜切紫色光块（取自品牌手册里贯穿大屏/包装/工牌的巨型紫色斜板）
   + 两束径向辉光 + 噪点颗粒。全部 pointer-events:none，不参与交互。 */
.iv .atmosphere {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}
/* 斜切紫光板：品牌手册里贯穿大屏 / 包装 / 工牌的巨型紫色斜板。
   两条纪律：
   ① 溢出视口 —— 左右各甩出画面外，于是只剩上下两条对角长边可见，
      永远不会露出直角或圆角。硬边是对的：品牌里的斜板就是硬边，不是柔光。
   ② 尺寸锚定 --hero-h 而不是 body 百分比 —— 页面总高一变（手机 1290 / 宽屏 900），
      按百分比算的光板几何就漂，亮部会压到文案上。锚定首屏后，
      下缘中心线恒定落在 0.80 × 首屏高，叠加 ±39px 的旋转落差后仍高于正文起点。 */
.iv .beam {
  position: absolute;
  top: calc(var(--hero-h) * -0.90);
  left: -42%;
  width: 186%;
  height: calc(var(--hero-h) * 1.66);
  background: linear-gradient(152deg, #6e4fe9 0%, #5433c8 28%, #241566 64%, #0a0714 100%);
  transform: rotate(-8deg) skewX(-13deg);
  /* 基础值就是最终值：没有 JS 时这里必须已经是对的，入场动画只是把它"演"出来 */
  opacity: 0.9;
}
/* 第二道更亮的细斜带：单板太平，叠一道才有品牌手册里的"层叠透明"感。
   这条两端都渐变到全透明，所以不产生任何硬边，只贡献一道掠光。 */
.iv .beam--2 {
  top: calc(var(--hero-h) * -0.90);
  left: -42%;
  width: 186%;
  height: calc(var(--hero-h) * 1.66);
  opacity: 0.7;
  background: linear-gradient(
    152deg,
    rgba(155, 166, 255, 0) 6%,
    rgba(155, 166, 255, 0.45) 36%,
    rgba(110, 79, 233, 0) 76%
  );
  transform: rotate(-8deg) skewX(-13deg);
}

.iv .glow {
  position: absolute;
  border-radius: 50%;
  opacity: 1;
}

.iv .glow--violet {
  top: calc(var(--hero-h) * -0.52);
  left: 50%;
  width: min(150vw, 980px);
  height: calc(var(--hero-h) * 1.75);
  transform: translateX(-50%);
  background: radial-gradient(closest-side, rgba(110, 79, 233, 0.6), rgba(110, 79, 233, 0) 74%);
}

.iv .glow--soft {
  right: -24%;
  bottom: 4%;
  width: 76%;
  aspect-ratio: 1 / 1;
  background: radial-gradient(closest-side, rgba(155, 166, 255, 0.16), rgba(155, 166, 255, 0) 72%);
}
/* 星图：几个节点 + 发丝连线。名字叫星图，背景就该有一次"星图"的呼应。
   同样锚定 --hero-h：早先用 min(74vh,580px) 时，视口一高星图就往下长，
   节点圆点直接落到 eyebrow 背后 —— 实测把对比度从 8.5:1 拉到 3.5:1。
   现在它的高度跟着首屏走，遮罩也在首屏底部之前收干净，永远够不到正文。 */
.iv .constellation {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: calc(var(--hero-h) * 1.05);
  opacity: 0.55;
  -webkit-mask-image: linear-gradient(to bottom, #000 0 26%, transparent 90%);
  mask-image: linear-gradient(to bottom, #000 0 26%, transparent 90%);
}

.iv .grain {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0.05;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* ════════════ 2. 骨架 ════════════ */
.iv .shell {
  position: relative;
  z-index: 2;
  max-width: 1080px;
  margin-inline: auto;
  padding: 0 20px calc(34px + env(safe-area-inset-bottom, 0px));
}
/* ════════════ 3. 首屏 ════════════ */
.iv .hero {
  display: flex;
  align-items: center;
  justify-content: center;
  height: var(--hero-h);
}
/* 标记已提为透明底（见 tools/extract-alpha.py），所以能直接压在紫色光块上，
   不会出现"黑方块糊在光束中间"这类合成事故 */
.iv .hero__mark {
  width: min(76vw, 336px);
  height: auto;
}
/* ════════════ 4. 文案 ════════════ */
.iv .lede {
  text-align: center;
}

.iv .eyebrow {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  margin: 0 0 14px;
  font-family: var(--font-display);
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--brand-violet-soft);
}

.iv .eyebrow::before, .iv .eyebrow::after {
  content: "";
  width: 26px;
  height: 1px;
  background: var(--color-line-strong);
}
/* 中文不吃 0.3em 的字距，单独收一档，否则字符之间会散成"标点感" */
.iv .eyebrow__cn {
  font-family: var(--font-cn);
  letter-spacing: 0.18em;
}

.iv .headline {
  margin: 0;
  font-size: var(--fs-hero);
  font-weight: 800;
  line-height: 1.06;
  letter-spacing: -0.02em;
  color: var(--brand-grey);
}
/* 两色标题：第二行用亮紫，直接呼应品牌手册手机界面里
   "Explore A Larger Universe" 那处关键词高亮的手法 */
.iv .headline__hi {
  color: var(--brand-violet-soft);
}

.iv .lead {
  margin: 12px 0 0;
  font-size: var(--fs-lead);
  line-height: 1.7;
  color: var(--color-muted);
}
/* 品牌关键词带：词直接取自品牌手册的语义清单，不是自造文案。
   只在宽屏出现 —— 手机上它插在正文与凭证之间只会添乱。 */
.iv .kicker {
  display: none;
}
/* ════════════ 5. 邀请凭证 ════════════
   一张卡承载两条加入路径：上半张浅灰放二维码（二维码必须压浅底才扫得动），
   下半张近黑放邀请码。中间那道缝上镶一枚黄菱 —— 取自 logo 里的那颗菱形。 */
.iv .ticket {
  position: relative;
  width: min(100%, 384px);
  margin: clamp(22px, 5.5vw, 38px) auto 0;
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--shadow-3);
}

.iv .ticket__top {
  background: var(--brand-grey);
  padding: 20px 22px 18px;
}

.iv .ticket__qr {
  width: min(54vw, 172px);
  margin-inline: auto;
}

.iv .ticket__id {
  margin: 14px 0 0;
  text-align: center;
}

.iv .ticket__id-name {
  display: block;
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--brand-violet); /* 对 #F2F2F2 实测 4.74:1 */
}

.iv .ticket__bottom {
  position: relative;
  background: var(--color-surface);
  padding: 20px 22px 20px;
}

.iv .ticket__seal {
  position: absolute;
  top: -8px;
  left: 50%;
  width: 16px;
  height: 16px;
  margin-left: -8px;
  background: var(--color-accent);
  transform: rotate(45deg);
}

.iv .code-label {
  margin: 0;
  text-align: center;
  font-family: var(--font-display);
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--brand-violet-soft);
}

.iv .code-label span {
  font-family: var(--font-cn);
  letter-spacing: 0.16em;
}

.iv .code {
  display: block;
  /* 定高：滚动落字动画期间不能让卡片跟着抖 */
  min-height: 1.25em;
  margin: 10px 0 0;
  text-align: center;
  font-family: var(--font-mono);
  font-size: var(--fs-code);
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "zero" 1; /* 点零，避免 O / 0 看混 */
  user-select: all;
  -webkit-user-select: all;
}

.iv .copy {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  min-height: 48px; /* 触摸目标 ≥44px */
  margin-top: 14px;
  padding: 0 20px;
  border-radius: var(--r-pill);
  background: var(--color-accent);
  color: var(--color-ink);
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 0.06em;
  box-shadow: 0 6px 22px rgba(255, 210, 0, 0.2);
  transition: transform var(--dur-fast) ease-out, box-shadow var(--dur-fast) ease-out,
    background-color var(--dur-fast) ease-out;
}

.iv .copy:hover {
  transform: translateY(-1px);
  box-shadow: 0 10px 30px rgba(255, 210, 0, 0.3);
}

.iv .copy:active {
  transform: translateY(1px) scale(0.99);
}
/* 失败态：不引入新颜色，用描边把它降级成次级动作 */
.iv .copy[data-state="error"] {
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--brand-violet-soft);
  color: var(--brand-violet-soft);
}

.iv .copy__icon {
  flex: none;
  width: 18px;
  height: 18px;
}
/* ════════════ 6. 提示条 ════════════ */
.iv .toast {
  position: fixed;
  left: 50%;
  bottom: calc(28px + env(safe-area-inset-bottom, 0px));
  z-index: 10;
  max-width: calc(100vw - 40px);
  padding: 10px 20px;
  border-radius: var(--r-pill);
  background: rgba(11, 10, 20, 0.94);
  border: 1px solid var(--color-line-strong);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  color: var(--color-text);
  font-size: 14px;
  letter-spacing: 0.04em;
  text-align: center;
  opacity: 0;
  transform: translate(-50%, 14px);
  pointer-events: none;
  transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
}

.iv .toast.is-on {
  opacity: 1;
  transform: translate(-50%, 0);
}
/* ════════════ 7. 页脚 ════════════ */
.iv .foot {
  margin-top: clamp(30px, 7vw, 56px);
  text-align: center;
}

.iv .foot__logo {
  width: 116px;
  margin: 0 auto 14px;
  opacity: 0.92;
}

.iv .foot__rule {
  width: 56px;
  height: 1px;
  margin: 0 auto 14px;
  background: var(--color-line-strong);
}

.iv .foot__en {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--brand-violet-soft);
}

.iv .foot__cn {
  margin: 8px 0 0;
  font-size: 13px;
  letter-spacing: 0.1em;
  color: var(--color-muted);
}
/* ════════════ 8. 入场编排 ════════════
   一次编排好的序列：氛围 → 标记 → 标签 → 标题 → 副标题 → 凭证 → 页脚。
   错峰 80~120ms，整段 1.4s 内收完，不拖沓。

   注意所有"初始隐藏"都挂在 html.js 下：脚本被拦、被禁用、或者加载失败时，
   .reveal 就是普通可见元素，页面是一份完整的静态版，而不是一片黑。
   这是分享页的底线 —— 它天然要跑在各种不确定的 WebView 里。 */
.iv--js .reveal {
  opacity: 0;
  transform: translateY(22px);
}

.iv--js .beam, .iv--js .beam--2, .iv--js .glow {
  opacity: 0;
}

.iv--ready .reveal {
  opacity: 1;
  transform: none;
  transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out);
  transition-delay: var(--d, 0ms);
}

.iv--ready .beam {
  opacity: 0.9;
  transition: opacity 820ms var(--ease-out);
}

.iv--ready .beam--2 {
  opacity: 0.7;
  transition: opacity 900ms var(--ease-out) 160ms;
}

.iv--ready .glow {
  opacity: 1;
  transition: opacity 1100ms var(--ease-out) 120ms;
}

.iv .hero__mark {
  --d: 120ms;
}

.iv .eyebrow {
  --d: 240ms;
}

.iv .headline {
  --d: 340ms;
}

.iv .lead {
  --d: 420ms;
}

.iv .kicker {
  --d: 460ms;
}

.iv .ticket {
  --d: 520ms;
}

.iv .foot {
  --d: 640ms;
}
/* 辉光呼吸：只给一个元素做持续动效，且幅度必须小到"若隐若现" */
@media (prefers-reduced-motion: no-preference){

.iv--ready .glow--violet {
    animation: halo-breathe 9s var(--ease-soft) infinite 1.4s;
  }

@keyframes halo-breathe{
    0%,
    100% {
      transform: translateX(-50%) scale(1);
    }
    50% {
      transform: translateX(-50%) scale(1.05);
    }
  }
}
/* 极窄机（iPhone SE 类 320~360px）收紧留白，别让内容顶到边框 */
@media (max-width: 360px){

.iv .shell {
    padding-inline: 16px;
  }

.iv .ticket__top, .iv .ticket__bottom {
    padding-inline: 18px;
  }
}

/* ══════════════════════════════════════════════════════════════════════
   H5 容器适配（源站点没有这一段：那边 body 就是画布）
   ══════════════════════════════════════════════════════════════════════ */

/* ── 隔离兜底：主样式表里**同名**的全局类会漏进来 ──
   `.iv .hero` 这类选择器特异性(0,2,0)虽高于主表的 `.hero`(0,1,0)，但那只对
   「两边都声明过」的属性成立 —— 主表声明过、这里没声明的属性会**直接漏进来**。
   实测漏的是 `.hero`：H5 工作台那张橙黑渐变卡（background / color / padding /
   border-radius / position / overflow）把邀请页的 <header class="hero">
   刷成了一块橙色方块。逐条重置如下。
   ⚠️ 改版或新增区块后请重跑本脚本：末尾会自动重做撞名比对，有未处理的就告警。 */
.iv .hero {
  background: none;
  color: inherit;
  padding: 0;
  margin: 0;
  border: 0;
  border-radius: 0;
  position: static;
  overflow: visible;
  cursor: auto;
  opacity: 1;
  font-size: inherit;
  font-weight: inherit;
}

/* 另两个撞名类：源 CSS 的特异性已经接管，这里再显式声明一次，防将来主表加属性时静默漏进来 */
.iv .code { margin: 10px 0 0; }
.iv .toast { margin: 0; }

/* 本页是全屏深色，根容器要吃掉 #view 的内边距并铺满可用高度 */
.iv {
  position: relative;
  display: block;
  overflow: hidden;
  min-height: 100%;
}

/* 邀请页路由下：#view 去内边距、铺黑底；顶部栏跟着变深色，整页连成一体
   （与 app.js:render() 里的 view--dark / appbar--dark 类名对应） */
.view.view--dark {
  padding: 0;
  background: #000;
}

/* 二维码：源码里是一张 <img src="assets/qr.svg">，本项目换成动态生成的 SVG
   （见 app.js 的 vInvite），宽度交给容器，SVG 自己铺满 */
.iv .ticket__qr svg {
  display: block;
  width: 100%;
  height: auto;
}

.appbar.appbar--dark {
  background: #000;
  border-bottom-color: rgba(227, 223, 248, 0.14);
}

.appbar.appbar--dark .title,
.appbar.appbar--dark .back,
.appbar.appbar--dark .action {
  color: #e3dff8;
}

/* ══════════════════════════════════════════════════════════════════════
   画布尺寸适配 —— 视口宽 ≠ 画布宽（2026-09-19 用户反馈后加）
   ──────────────────────────────────────────────────────────────────────
   源站是独立页面：浏览器视口就是画布，所以整份样式按 vw/vh 定尺寸、
   断点按视口宽判断。H5 不是 —— 画布是被钉死的 `.phone` 容器
   （width:100%; max-width:420px; height:100vh; max-height:812px）。
   于是桌面宽屏下（视口 1440、画布仍 420）出现两处失真：

     · 「按视口算的字号」全部顶到 clamp 上限：标题 58px、二维码 172px、
       标记 336px …… 塞进 420px 的框里 → 标题一字一行、凭证卡只剩 154px；
     · 「按视口判断的断点」错误命中 @media (min-width:900px) 的两栏版式
       → 170px + 154px 两列 + 56px 间隙。

   修法：把画布的真实尺寸抽成 --canvas-w / --canvas-h，所有 vw/vh 派生值
   改为按它们计算。因为 画布宽 = min(视口宽, 420)、画布高 = min(视口高, 812)，
   所以视口 ≤420 时结果与源站**逐像素一致**，视口更宽时则冻结在「420 宽
   那一刻」的取值 —— 不论窗口多大，渲染结果恒等于设计稿的手机那一版。
   （阈值 >420 的 min-width 断点已在转换阶段整块丢弃，见 is_unreachable_media。）

   ⚠️ 若源包改版新增了 vw/vh 定尺寸的规则，请在这里补一条同口径覆盖，
   脚本末尾的 check_viewport_units() 会把未覆盖的列出来告警。
   ══════════════════════════════════════════════════════════════════════ */
.iv {
  --canvas-w: min(100vw, 420px); /* 420 = .phone 的 max-width，改主表须同步 */
  --canvas-h: min(100vh, 812px); /* 812 = .phone 的 max-height */
}

/* 字阶：源 --fs-hero: clamp(34px, 10.4vw, 58px) 等三条 */
.iv {
  /* ⚠️ 2026-09-22 起标题是**单行**（HTML 里的 <br> 已删，见 app.js vInvite）。
     字号上限改按「一行塞得下」反推：可用宽 = canvas-w − 2×20（shell 内边距），
     CJK 单字宽 = 字号 ×(1 − 0.02 字距)，共 11 个字。
       375 → 可用 335 → 字号 ≤ 31.1 ｜ 420 → 380 → ≤ 35.2
     取 0.078 斜率 + 34px 上限，任何宽度都留 ~6~7% 富余（换中文字体也不溢出）。
     ⚡ .iv 是 overflow:hidden —— 真溢出会被**静默裁掉**，不会报错，必须靠算准。 */
  --fs-hero: clamp(24px, calc(var(--canvas-w) * 0.078), 34px);
  --fs-code: clamp(23px, calc(var(--canvas-w) * 0.074), 34px);
  --fs-lead: clamp(14px, calc(var(--canvas-w) * 0.039), 16px);
  /* 首屏高度：源 32vh。用户要求「整页一屏 + 留白减少」，标记同步收小
     （见下方 .hero__mark），首屏收到 15.5vh。氛围层的斜切光板/辉光/星图
     几何全部由本变量推算（top/height 都是 --hero-h 的倍数），所以它们会
     同步收窄，相对位置关系不变 —— 这正是当初锚定 --hero-h 的意义。 */
  --hero-h: clamp(96px, calc(var(--canvas-h) * 0.155), 136px);
}

/* 首屏标记：源 min(76vw, 336px)。收到 210px —— 标记图 930×511，
   210 宽 → 高约 115，能落进 96~136 的首屏内不越界。 */
.iv .hero__mark {
  width: min(calc(var(--canvas-w) * 0.50), 210px);
}

/* 紫色辉光板：源 min(150vw, 980px) */
.iv .glow--violet {
  width: min(calc(var(--canvas-w) * 1.5), 980px);
}

/* 二维码：源 min(54vw, 172px)。
   ⚠️ 二维码既要留静区、又必须扫得动。本项目邀请链接约 45 字符
   （QR 版本 4~5，约 37 模块），140px 下每模块 ~3.8px，微信扫码余量充足。
   **不要**为了再省几十像素把它压到 120px 以下。 */
.iv .ticket__qr {
  width: clamp(124px, calc(var(--canvas-w) * 0.36), 140px);
}

/* 凭证卡上边距：源 clamp(22px, 5.5vw, 38px) */
.iv .ticket {
  margin: clamp(14px, calc(var(--canvas-w) * 0.038), 24px) auto 0;
}

/* 页脚上边距：源 clamp(30px, 7vw, 56px) */
.iv .foot {
  margin-top: clamp(12px, calc(var(--canvas-h) * 0.022), 20px);
}

/* 自带提示条：源 calc(100vw - 40px) */
.iv .toast {
  max-width: calc(var(--canvas-w) - 40px);
}

/* ── 窄屏补丁（≤360px）：源包在这一档只收了 shell / 凭证卡的内边距 ──
   实测 320px（iPhone SE 一代、部分老安卓）下 eyebrow 一行需要 ~290px，
   可用宽只有 288px —— 差 2px 就折成两行（`SHARE / INVITATION`、
   `分享邀 / 请`），观感正是用户说的「元素挤在一起」。
   收紧间隙与两侧装饰线补回余量（省 28px，留 ~26px 富余抗字体差异）。
   ⚠️ 这是本项目对源码的补丁，不在交付包里；将来若拿到新版源码，
   先看它是否已自行修掉这一条，再决定是否删除本段。 */
@media (max-width: 360px) {

.iv .eyebrow {
    gap: 8px;
  }

.iv .eyebrow::before, .iv .eyebrow::after {
    width: 18px;
  }
}

/* ══════════════════════════════════════════════════════════════════════
   2026-09-22 改版 · 一屏压缩 + 「保存图片」
   ──────────────────────────────────────────────────────────────────────
   需求（用户原话）：「在复制邀请链接的同一行新增按钮【保存图片】，点击将当前
   页面保存为图片；调整页面整体大小（扫码加入，共绘更大星图字体变小，并成一行，
   页面整体留白减少，行间距减少），尽量在一屏内展示，方便保存图片」。

   ⚠️ 为什么这些改动写在**产物**里、而没回源码包改：
   源码包只有「版面」，而本轮两件事都是 H5 特有的 ——
   ①「一屏」是相对 H5 画布可用高（min(100vh,812) − 56px 顶栏）而言的，源站
      没有这个约束；②「保存图片」是本项目后加的功能。两者都属于「H5 容器适配」，
   按文件头第 ③ 条纪律落在本段。纯版面数值（字号/首屏/间距）则改在了上面的
   「画布尺寸适配」段里，就近可读。

   高度预算（375 宽 / canvas-h=790 这一档；改本段前请先重算这张表）：
     hero 126 + eyebrow 27 + headline 29 + lead 28 + ticket 355 + foot 133 ≈ 698
     H5 可用高 = min(100vh, 812) − 56（顶栏） = 734 ⇒ 余 ~36px。
   ══════════════════════════════════════════════════════════════════════ */

/* ── 1. 行距全线收紧（用户要求「行间距减少」）──
   源站 .iv 是 1.75（中文**正文**行高的保守值）。本页全是短标题/标签/单行说明，
   1.55 既压得下高度又不糊。⚡ 将来若往本页加成段正文，请给它单独写回 1.7+。 */
.iv {
  line-height: 1.55;
}

.iv .eyebrow {
  margin: 0 0 8px;
}

/* ── 2. 标题单行 ──
   nowrap 是**必要**的兜底：万一日后换了字宽更大的中文字体，宁可让上面的
   clamp 显得保守，也不要让「共绘更大星图」掉到第二行 —— 那会同时破坏
   版面节奏和一屏目标。容器 .iv 的 overflow:hidden 会把溢出**静默裁掉**，
   所以真正的保险是字号算准（见上面 --fs-hero 的推导）。 */
.iv .headline {
  white-space: nowrap;
}

.iv .lead {
  margin: 8px 0 0;
}

/* ── 3. 邀请凭证内部收紧 ── */
.iv .ticket__top {
  padding: 14px 20px 12px;
}

.iv .ticket__id {
  margin: 8px 0 0;
}

.iv .ticket__id-name {
  font-size: 15px;
  line-height: 1.5;
}

.iv .ticket__bottom {
  /* 左右内边距从 20 收到 16：让出 8px 给两个按钮 —— 见下方 .copy 的宽度推算。
     这 8px 是「复制邀请链接」能否单行放下的关键，别随手改回去。 */
  padding: 14px 16px 16px;
}

.iv .code {
  margin: 6px 0 0;
}

/* ── 4. 动作区：一行两个按钮 ──
   flex-wrap 是**刻意留的降级**：≤320px 的极窄机上两个按钮挤不进一行，
   与其把「复制邀请链接」压进 121px 的格子里溢出，不如让它整行折下去。
   主流宽度（≥360）都在一行内。 */
.iv .actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 12px;
}

/* 覆盖源规则的 width:100% —— 在 flex 行里那会变成「每个按钮独占一整行」。
   ⚠️ 宽度账（375 宽）：凭证卡 335 − 左右 16×2 = 303 − 间距 10 = 293 ÷ 2 ≈ 146（按钮）
      → 扣掉 padding 10×2 = 126 可用；「复制邀请链接」6 字 ×15px×(1+0.02 字距) = 91.8
      ＋ 图标 17 ＋ 间隙 8 = 116.8 ⇒ 富余 ~9px。
   源样式的 letter-spacing 是 0.06em，直接沿用会让这行**多出 3.6px** 而折成
   「复制邀请链 / 接」两行（实测踩到）—— 所以这里必须收到 0.02em。
   不要改成 white-space:nowrap 来「解决」折行：那会把字挤出按钮外，更难看；
   ≤320px 放不下时靠 .actions 的 flex-wrap 整行折下去才是对的降级。 */
.iv .copy {
  flex: 1 1 132px;
  width: auto;
  min-height: 46px; /* 触摸目标 ≥44px */
  margin-top: 0; /* 外边距上移到 .actions，两个按钮才对得齐 */
  padding: 0 10px;
  gap: 8px;
  font-size: 15px;
  letter-spacing: 0.02em;
}

/* 图标 18 → 17：从上面的宽度账里再抠出 1px 余量 */
.iv .copy__icon {
  width: 17px;
  height: 17px;
}

/* 次要动作（保存图片）：复用「失败态」那套描边降级，不新造颜色。
   刻意**不**把品牌黄给保存按钮 —— 复制邀请链接是主路径（好友点开即进产品页），
   视觉主次不能因为这次的加法而反转。 */
.iv .copy--ghost {
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--color-line-strong);
  color: var(--brand-violet-soft);
  font-weight: 600;
}

.iv .copy--ghost:hover {
  background: rgba(227, 223, 248, 0.06);
  box-shadow: inset 0 0 0 1px var(--brand-violet-soft);
}

.iv .copy[disabled] {
  opacity: 0.6;
  cursor: default;
  transform: none;
}

/* ── 5. 容器与页脚收紧 ── */
.iv .shell {
  padding: 0 20px calc(20px + env(safe-area-inset-bottom, 0px));
}

.iv .foot__logo {
  width: 84px;
  margin: 0 auto 6px;
}

.iv .foot__rule {
  margin: 0 auto 6px;
}

.iv .foot__cn {
  margin: 4px 0 0;
  line-height: 1.5;
}

/* ── 6. 出图模式（.iv--shot）──
   截图那一刻由 app.js 给 .iv 挂上、截完立刻摘掉。两件事：
   ① 隐藏按钮行与提示条 —— 分享图里不该出现「看起来能点、实际点不动」的 UI；
   ② min-height 归零 —— 平时 .iv 被撑到 H5 可用高（min-height:100%），内容不满
      时底部会留一大片空；出图时按内容实际高度裁，图更紧凑。 */
.iv.iv--shot .actions,
.iv.iv--shot .toast {
  display: none;
}

.iv.iv--shot {
  min-height: 0;
}

/* ── 7. 「保存图片」浮层 ──
   微信里 <a download> 是无效的，唯一可靠的保存路径是「长按图片 → 保存到相册」，
   所以生成后必须把图真真切切摆出来让用户长按。
   position:fixed 是**故意的**：它要盖住顶部栏（.appbar z-index 30）。
   .iv 及其祖辈都没有 transform/filter/backdrop-filter，不会把 fixed 变成
   「相对祖先定位」，所以这里安全 —— 改这一带结构时请复核这个前提。 */
.iv .shot {
  position: fixed;
  inset: 0;
  z-index: 60;
  /* 显隐走 .is-on 而不是 [hidden]：本选择器特异性(0,2,0)高于 [hidden](0,1,0)，
     用 hidden 属性会**关不掉**浮层。默认隐藏，出图成功后由 app.js 加 .is-on。 */
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 20px;
  padding-bottom: calc(20px + env(safe-area-inset-bottom, 0px));
  background: rgba(4, 3, 10, 0.94);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
}

.iv .shot__img {
  display: block;
  width: auto;
  max-width: min(100%, 340px);
  max-height: calc(100dvh - 200px);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-3);
}

/* 出图成功才显示（app.js 加 .is-on） */
.iv .shot.is-on {
  display: flex;
}

.iv .shot__tip {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  letter-spacing: 0.04em;
  color: var(--brand-lavender);
  text-align: center;
}

.iv .shot__acts {
  display: flex;
  gap: 10px;
}

.iv .shot__btn {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 40px;
  padding: 0 18px;
  border-radius: var(--r-pill);
  border: 1px solid var(--color-line-strong);
  background: rgba(227, 223, 248, 0.06);
  color: var(--brand-lavender);
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}

.iv .shot__btn:active {
  background: rgba(227, 223, 248, 0.14);
}

/* ── 9. 邀请注册页表单（2026-09-22 新增） ──
   专属邀请码页的二维码/链接现在落在「邀请注册页」（#/invite-register）。
   该页复用同一个 .iv 作用域与上面全部版式（hero / eyebrow / headline / lead / ticket / foot），
   这里只补表单本身。刻意不改动任何已有规则 —— 邀请页的版面账（一屏 756px）是量过的，别被这次加法带偏。

   结构：.ticket--form 内，上半区（.ticket__top，浅灰底）放「直推上级」，
   下半区（.reg，近黑 --color-surface）放表单 —— 与凭证卡「上浅下深」同一套语言。 */

/* 直推上级：浅灰底上必须用深色字。源包的 .code-label 是给深色下半区用的
   （--brand-violet-soft #9ba6ff），照搬到浅灰底上对比度只有 1.5:1，所以这里另给一套。 */
.iv .reg__ref {
  text-align: center;
}

.iv .reg__ref-label {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: rgba(8, 8, 10, 0.55); /* 对 #F2F2F2 ≈ 5.6:1 */
}

.iv .reg__ref-label span {
  font-family: var(--font-cn);
  letter-spacing: 0.16em;
}

.iv .reg__ref-name {
  display: block;
  margin-top: 6px;
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--brand-violet); /* 对 #F2F2F2 实测 4.74:1，与 .ticket__id-name 同口径 */
}

/* 表单区：近黑底，接在浅灰上半区之下 */
.iv .reg {
  background: var(--color-surface);
  padding: 16px 20px 20px;
}

.iv .reg__field {
  display: block;
}

.iv .reg__field + .reg__field {
  margin-top: 12px;
}

.iv .reg__label {
  display: block;
  margin-bottom: 6px;
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--color-muted);
}

.iv .reg__label i {
  margin-left: 3px;
  font-style: normal;
  color: var(--color-accent); /* 必填星号借用强调色，不做第二个颜色 */
}

/* 「微信号在哪看？」提示（2026-09-28 需求）：字段标签右侧的灰字可点链接。
   ⚠️ 只作用于 .iv 内的标签；弹窗挂在 body 上（.iv 之外），其内容样式在 styles.css（两页共用）。
   float:right 让提示与标签同行右对齐 —— 不改 .reg__label 的 display（那是源包规则，动了会连带
   影响联系人/联系手机两个标签）。 */
.iv .reg__hint {
  float: right;
  font-size: 12px;
  font-weight: 400;
  letter-spacing: 0.02em;
  color: var(--color-muted);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}

.iv .reg__field input {
  display: block;
  width: 100%;
  min-height: 46px; /* 触摸目标 ≥44px */
  padding: 0 14px;
  border: 0;
  border-radius: var(--r-sm);
  background: rgba(227, 223, 248, 0.08);
  box-shadow: inset 0 0 0 1px var(--color-line);
  color: var(--color-text);
  font-family: var(--font-cn);
  font-size: 16px; /* 16px 是 iOS 聚焦时不缩放的下限，别改小 */
  letter-spacing: 0.02em;
  outline: none;
  transition: box-shadow var(--dur-fast) ease-out, background-color var(--dur-fast) ease-out;
}

.iv .reg__field input::placeholder {
  color: rgba(169, 165, 190, 0.75); /* --color-muted 再压一档，避免与真值混淆 */
}

.iv .reg__field input:focus {
  background: rgba(227, 223, 248, 0.12);
  box-shadow: inset 0 0 0 1px var(--brand-violet-soft);
}

/* 报错文案：源包调色板里**没有红**（唯一强调色是品牌黄，留给主按钮）。
   但错误提示不能借用装饰色，否则用户读不出「这是错」—— 这里刻意引入一个浅红，
   仅用于本行文案，不进调色板。 */
.iv .reg__err {
  margin: 8px 0 0;
  min-height: 16px; /* 常驻占位：报错出现/消失时按钮不会跳一下 */
  font-size: 12px;
  line-height: 16px;
  color: #ff8a8a;
}

/* 主按钮：直接复用 .copy（品牌黄 pill），只把外边距与宽度交回来 ——
   .copy 在邀请页被 .actions 的 flex 布局收窄过（flex:1 1 132px / margin-top:0），
   那两条是给「两个按钮并排」用的，这里必须显式还原成整宽。 */
.iv .reg__submit {
  flex: none;
  width: 100%;
  margin-top: 10px;
}

/* ── 10. 注册成功弹窗（openCenterAlert 的内容块） ──
   弹窗挂在 body 上、在 .iv 之外，所以这里**不能用 .iv 前缀**，
   只能靠 .reg__ok 这个自造类名做作用域（它在 H5 主样式表里没有同名规则）。 */
.reg__ok {
  font-size: 13px;
  line-height: 1.8;
  color: var(--ink, #1b1b23);
}

.reg__ok p {
  margin: 0;
}

.reg__ok p + p {
  margin-top: 6px;
}

.reg__ok-tip {
  color: #6b6b7a;
}

/* ── 8. 极窄机补丁 ──
   本段插在文件末尾，会按「同特异性后者胜」盖掉源包 @media(max-width:360px)
   里对 .ticket__top/bottom 的 padding-inline 收窄，所以这里显式写回来。
   ⚠️ 只补 .ticket__top（二维码那一半）：源包那条是给 iPhone SE 一代那档
   320px 用的，不能丢。**.ticket__bottom 故意不收窄** —— 它的 16px 是上一条
   宽度账的分母，收窄会让两个按钮挤在一起、「复制邀请链接」重新折行。 */
@media (max-width: 360px) {

.iv .ticket__top {
    padding-inline: 18px;
  }
}

@media (max-width: 340px) {

.iv .shot {
    padding-inline: 12px;
  }
}
