/* ============================================================
   验真码发放平台 —— 样式
   目标：干净、留白足、卡片有质感；手机和电脑都能用。
   ============================================================ */

:root {
  --bg: #f4f6fb;
  --bg-grad-a: #dfe7ff;
  --bg-grad-b: #ffe6f0;
  --surface: #ffffff;
  --surface-2: #f7f9fc;
  --border: #e6eaf2;
  --text: #1a2033;
  --text-soft: #6b7488;
  --text-faint: #9aa3b5;
  --brand: #3b5bfd;
  --brand-2: #7a5cfd;
  --ok: #12a150;
  --ok-soft: #e6f7ed;
  --warn: #d98324;
  --warn-soft: #fdf3e3;
  --danger: #d64545;
  --danger-soft: #fdeaea;
  /* 说明文档的强调色。先是偏紫的 --brand-2，现场说不好看换成了红色，
     现在又要蓝色 —— 就用主站这抹品牌蓝，整页要换色只改这里一个变量 */
  --doc-accent: var(--brand);
  --doc-soft: rgba(59, 91, 253, .07); /* 引用框/折叠块/行内代码的底色 */
  --doc-line: rgba(59, 91, 253, .22); /* 说明页的边线 */
  --doc-mark: rgba(59, 91, 253, .14); /* ==高亮== 的底色 */
  --shadow-sm: 0 1px 2px rgba(20, 28, 54, .06), 0 2px 8px rgba(20, 28, 54, .04);
  --shadow-md: 0 4px 12px rgba(20, 28, 54, .08), 0 12px 32px rgba(20, 28, 54, .08);
  --shadow-lg: 0 12px 32px rgba(20, 28, 54, .16), 0 32px 64px rgba(20, 28, 54, .14);
  --radius: 16px;
  --radius-sm: 10px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0f131e;
    --bg-grad-a: #1b2440;
    --bg-grad-b: #331d2c;
    --surface: #171c2a;
    --surface-2: #1e2433;
    --border: #2a3243;
    --text: #eef1f8;
    --text-soft: #a2acc0;
    --text-faint: #767f92;
    --brand: #6d86ff;
    --brand-2: #9d86ff;
    --ok: #35c47a;
    --ok-soft: #16301f;
    --warn: #e0a458;
    --warn-soft: #33270f;
    --danger: #ef6b6b;
    --danger-soft: #35191b;
    --doc-accent: #ff8a80;
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, .3);
    --shadow-md: 0 4px 12px rgba(0, 0, 0, .35), 0 12px 32px rgba(0, 0, 0, .3);
    --shadow-lg: 0 12px 32px rgba(0, 0, 0, .5), 0 32px 64px rgba(0, 0, 0, .4);
  }
}

* { box-sizing: border-box; }

/* ------------------------------------------------------------
   必须放在所有规则前面（且带 !important）：
   HTML 的 hidden 属性靠浏览器的 display:none 生效，但作者样式里
   任何一条 display 都会把它盖掉。弹层用了 display:grid 居中，
   于是 hidden 失效——页面一打开弹层就杵在那儿，点 × 也关不掉。
   这条兜底规则让 hidden 永远说了算。
   ------------------------------------------------------------ */
[hidden] { display: none !important; }

html, body { height: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  /* 正文按"先拉丁、后中文"排：中文优先挑系统里更清爽的雅黑 UI / 苹方 / 鸿蒙 / 思源，
     一路都缺才落回老雅黑。全是系统自带字体，不联网、不加载外部字体。 */
  font: 15px/1.65 system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
        "PingFang SC", "HarmonyOS Sans SC", "MiSans", "Microsoft YaHei UI",
        "Microsoft YaHei", "Source Han Sans SC", "Noto Sans SC", sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* 背景光斑，让页面不那么"死板" */
.glow {
  position: fixed;
  border-radius: 50%;
  filter: blur(90px);
  opacity: .55;
  pointer-events: none;
  z-index: 0;
}
.glow-a { width: 480px; height: 480px; background: var(--bg-grad-a); top: -180px; left: -140px; }
.glow-b { width: 420px; height: 420px; background: var(--bg-grad-b); top: 120px; right: -160px; }

.topbar, main { position: relative; z-index: 1; }

/* ---------------- 顶栏 ---------------- */
.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  max-width: 1120px;
  margin: 0 auto;
  padding: 28px 24px 8px;
}

.brand { display: flex; align-items: center; gap: 14px; }

.brand-mark {
  width: 46px; height: 46px;
  border-radius: 14px;
  /* logo 本身的底色是 #FCFCFC，这里也用近白底，两者接缝看不出来 */
  background: #fcfcfc;
  overflow: hidden;
  box-shadow: var(--shadow-md);
  flex: none;
}
.brand-mark img { width: 100%; height: 100%; object-fit: cover; display: block; }

.brand-text h1 { margin: 0; font-size: 21px; font-weight: 650; letter-spacing: .3px; }
.brand-text p { margin: 3px 0 0; font-size: 13px; letter-spacing: .2px; color: var(--text-soft); }

/* 手机上一行放不下时让每项自己换行，不要把一个「已连接」压成竖排 */
.topbar-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

/* 版本号：放在页面最下方、居中的一行浅灰小字。
   不抢注意力，但要能一眼看到 —— 排查"手机上还是老样子"时全靠它。
   刻意不做成药丸/badge：贴到顶栏会跟「连接状态」「刷新」挤成一团，
   在窄屏上还会把刷新按钮顶到第二排。 */
.app-footer {
  padding: 18px 16px 28px;
  text-align: center;
}
.app-version {
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  letter-spacing: .3px;
  color: var(--text-faint);
  opacity: .72;
  user-select: all; /* 让用户能一次选全，方便复制给对方排查 */
}

.conn {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 13px; color: var(--text-soft);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 6px 12px;
  box-shadow: var(--shadow-sm);
  /* 被挤窄时宁可换行，也不要让文字竖起来 */
  flex: none;
  white-space: nowrap;
}
.conn .dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--text-faint);
  transition: background .2s;
}
.conn.is-ok .dot { background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
.conn.is-bad .dot { background: var(--danger); box-shadow: 0 0 0 3px var(--danger-soft); }

/* ---------------- 主体 ---------------- */
main {
  max-width: 1120px;
  margin: 0 auto;
  padding: 12px 24px 64px;
  display: grid;
  /* 单列必须写成 minmax(0, 1fr)：手机上「发放台账」这张表比屏幕宽时，
     只让表格自己左右滑，不会把整页顶宽（以前会把右边的内容和按钮裁掉） */
  grid-template-columns: minmax(0, 1fr);
  gap: 22px;
}

.panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 20px 22px 22px;
  box-shadow: var(--shadow-sm);
}

.panel-head {
  display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
  margin-bottom: 16px;
}
/* 标题前面立一根小色条，跟卡片顶上那道呼应；比整块加粗、加底纹都轻 */
.panel-head h2 {
  margin: 0;
  padding-left: 11px;
  font-size: 16px;
  font-weight: 650;
  letter-spacing: .3px;
  line-height: 1.25;
  border-left: 3px solid var(--brand);
  border-radius: 2px;
}
.muted { color: var(--text-soft); font-size: 13px; letter-spacing: .2px; margin: 0; }

/* ---------------- 种类卡片 ---------------- */
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 16px;
}

.card {
  position: relative;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  padding: 18px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  overflow: hidden;
  transition: transform .16s ease, box-shadow .16s ease;
}
.card::before {
  content: "";
  position: absolute; inset: 0 0 auto 0; height: 4px;
  background: linear-gradient(90deg, var(--accent, var(--brand)), transparent);
}
.card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.card.is-empty { opacity: .72; }

/* 卡片本身也能点：点一下下面的「发放记录」就只看这个种类，再点一次取消。
   悬停上浮本来就有，这里补上手型和键盘焦点，别让人猜。 */
.card { cursor: pointer; }
.card:focus-visible { outline: 3px solid var(--accent-glow, rgba(59,91,253,.45)); outline-offset: 2px; }
.card.is-selected {
  border-color: var(--accent, var(--brand));
  box-shadow: 0 0 0 4px var(--accent-soft, rgba(59,91,253,.14)), var(--shadow-md);
}
.card.is-selected::before { height: 6px; }
/* 选中的卡片右上角盖一个小标签，一眼看出"现在只看这个" */
.card.is-selected .card-top::after {
  content: "只看这个";
  align-self: center;
  padding: 2px 9px;
  border-radius: 999px;
  background: var(--accent-soft, rgba(59,91,253,.14));
  color: var(--accent, var(--brand));
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .4px;
  white-space: nowrap;
}

.card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
/* 卡片下半部分单独包一层（数字 / 核销上限 / 取码按钮）：
   桌面版照旧竖着排，手机版它跟左边那条竖着的种类名并排，见文件末尾的窄屏断点。 */
.card-body { display: flex; flex-direction: column; gap: 14px; }
/* 产品名可能很长（如「瑞蓝·丽瑅®·丽多™」），撑不破卡片 */
.card-name, .card-source, .card-limits { overflow-wrap: anywhere; }
.card-name { font-size: 17px; font-weight: 650; letter-spacing: .3px; margin: 0; }
/* 产品名前一个小色点，用这张卡自己的强调色，跟顶上那道色条对得上 */
.card-name::before {
  content: "";
  display: inline-block;
  width: 8px; height: 8px;
  margin-right: 9px;
  border-radius: 50%;
  background: var(--accent, var(--brand));
  vertical-align: .1em;
}
.card-source {
  font-size: 11px; color: var(--text-faint);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 2px 8px;
  white-space: nowrap;
}

/* 数字区：可发放 / 已发放 两块，各占一半。
   中间那块居中 —— 原来「已发放」紧贴着左边的「可发放」，右边空一大片，很难看。 */
.stats { display: flex; align-items: flex-end; gap: 18px; flex-wrap: wrap; }
.stat { display: flex; flex-direction: column; gap: 3px; flex: 1 1 0; }
/* 主角数字：跟着卡片颜色、加粗（数字等宽，自动刷新时不会左右抖） */
.stat-num {
  font-size: 40px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -.8px;
  font-variant-numeric: tabular-nums;
  color: var(--accent, var(--brand));
}
/* 主角数字再给一点极淡的竖向渐变（满色 → 半透明本色）。
   整段包在 @supports 里：万一浏览器不认 background-clip，就还是纯色，
   绝不会出现"字变透明、看着像没有"的情况 */
@supports ((background-clip: text) or (-webkit-background-clip: text)) {
  .stat-primary .stat-num {
    background-image: linear-gradient(168deg, var(--accent, var(--brand)) 58%, var(--accent-glow, var(--brand)));
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
  }
}
/* 已发放 是配角：柔和灰 + 不加粗
   （现场反馈「2000 又粗又黑」，所以它不抢主角的视觉。
   「积分/个」原来也在这行里，现在挪到卡片右上角 / 手机版名字下面去了，
   见 .card-points） */
.stat-done { text-align: center; }
.stat-done .stat-num { color: var(--text-soft); font-weight: 500; }

/* 积分/个：一枚小牌子，一行写完（现场反馈：竖成两行特别丑）。
   桌面版贴在卡片右上角（靠 .card-top 的 space-between）；手机版落进左边名字栏，
   那里只有几十像素宽，才竖回"数字 / 单位"两行，见窄屏断点里的 .card-points。
   它是配置死的固定值，用配角灰、比主角数字小一档，别跟「可发放」抢眼神 */
.card-points {
  display: flex;
  align-items: baseline;
  gap: 5px;
  padding: 3px 9px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  white-space: nowrap;
}
.card-points .points-num {
  font-size: 15px;
  /* 写 500 不写 600：中文字体只有 400/700 两档，600 会落到 700，
     数字看着又黑又粗（现场反馈过），500 稳稳落在正常粗细上 */
  font-weight: 500;
  line-height: 1.15;
  color: var(--text-soft);
  font-variant-numeric: tabular-nums;
}
.card-points .points-label {
  font-size: 10.5px;
  letter-spacing: .3px;
  color: var(--text-faint);
}
.stat-label {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: .4px;
  color: var(--text-faint);
}
.tag {
  display: inline-block;
  border-radius: 999px; padding: 3px 10px;
  font-size: 12px;
  background: var(--surface); border: 1px solid var(--border); color: var(--text-soft);
  white-space: nowrap;
}
.tag.is-warn { background: var(--warn-soft); border-color: transparent; color: var(--warn); }
.tag.is-ok { background: var(--ok-soft); border-color: transparent; color: var(--ok); }

/* ---------------- 按钮 ---------------- */
.btn {
  appearance: none;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: 10px 16px;
  font: inherit;
  font-weight: 600;
  font-size: 14px;
  letter-spacing: .4px;
  cursor: pointer;
  transition: filter .15s ease, transform .1s ease, background .15s;
  white-space: nowrap;
}
.btn:active:not(:disabled) { transform: translateY(1px); }
.btn:disabled { cursor: not-allowed; opacity: .5; }

/* 顶上一条几乎看不见的白色内高光，让渐变按钮有一点厚度 */
.btn-primary {
  background: linear-gradient(135deg, var(--brand), var(--brand-2));
  color: #fff;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .22), var(--shadow-sm);
}
.btn-primary:hover:not(:disabled) { filter: brightness(1.07); }

.btn-ghost {
  background: var(--surface);
  border-color: var(--border);
  color: var(--text);
}
.btn-ghost:hover:not(:disabled) { background: var(--surface-2); }

.btn-block { width: 100%; }
.btn-lg { padding: 13px 18px; font-size: 15px; }

/* ---------------- 台账 ---------------- */
/* 台账是一块固定高度的滚动区：记录一天上百条，全铺出来的话页面会被拉得很长，
   想看一眼卡片上的数字得往上滚半天。所以这里给它一个上限，多出来的在里面滚
   （横竖都能滑：窄屏还要横向滑一下才看得全）。 */
.table-wrap {
  overflow: auto;
  max-width: 100%;
  max-height: min(62vh, 620px);
  /* iOS 上用这块自己的滚动：手指在表里滑不会把整页带着一起弹 */
  -webkit-overflow-scrolling: touch;
}
.ledger { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.ledger th {
  text-align: left; font-weight: 600; color: var(--text-faint);
  font-size: 12.5px; letter-spacing: .4px;
  padding: 8px 10px; border-bottom: 1px solid var(--border); white-space: nowrap;
  /* 表头钉在滚动区顶上：往下翻的时候还知道哪列是哪列。
     得给个不透明底色，不然行会从表头底下透出来 */
  position: sticky; top: 0; z-index: 1;
  background: var(--surface);
}
.ledger td {
  padding: 10px; border-bottom: 1px solid var(--border); vertical-align: middle;
  font-variant-numeric: tabular-nums;
}
.ledger tbody tr { transition: background .15s ease; }
.ledger tbody tr:hover { background: var(--surface-2); }
.ledger tr:last-child td { border-bottom: none; }
.ledger .mono {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 12.5px; letter-spacing: .2px;
}
.ledger .empty-row td { text-align: center; color: var(--text-faint); padding: 26px 10px; }

/* 「日志ID」列：只有管理员看得到（服务端对普通用户也不返回 logId），
   手机上无论谁来都不显示（见窄屏断点）。 */
.ledger .col-log { display: none; }
.ledger.show-log .col-log { display: table-cell; }
/* 日志ID 是拿去搜日志的，点一下就把整串选中，省得手抄错 */
.ledger .log-id {
  user-select: all; -webkit-user-select: all;
  color: var(--text-soft); letter-spacing: .2px;
}

/* 用户那一列（谁发的码 / 谁核销的）别被压成两行 */
.ledger .col-user { white-space: nowrap; }

/* 用户底下的“小字行”（发放时间）：桌面版各自有“发放时间”那一列，所以这里不显示；
   手机版把时间挪到用户下面，省掉一整列（见窄屏断点）。 */
.ledger .cell-sub { display: none; }

/* 「待核销」的行可以点：点一下重新调出二维码和核销图（发码那个弹层） */
.ledger tbody tr.is-openable { cursor: pointer; }
.ledger tbody tr.is-openable:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
/* 小箭头：告诉人这一行点得动 */
.ledger .row-cue { margin-left: 6px; color: var(--text-faint); font-size: 15px; line-height: 1; }

/* 表格底下那行「共 N 条 · 已显示 M 条 · 往下滚看更早的」：
   滚动区自己有边界，这行是告诉人底下还有没有 */
.ledger-note {
  margin: 10px 0 0;
  font-size: 12px;
  letter-spacing: .2px;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

/* ---------------- 弹层 ---------------- */
.modal-backdrop {
  position: fixed; inset: 0; z-index: 50;
  background: rgba(14, 19, 34, .45);
  backdrop-filter: blur(6px);
  display: grid; place-items: center;
  padding: 20px;
  animation: fade .18s ease;
}
@keyframes fade { from { opacity: 0 } to { opacity: 1 } }

.modal {
  position: relative;
  width: min(420px, 100%);
  max-height: 92vh;
  overflow: auto;
  background: var(--surface);
  border-radius: 22px;
  padding: 22px;
  box-shadow: var(--shadow-lg);
  animation: pop .2s cubic-bezier(.2, .9, .3, 1.2);
}
@keyframes pop { from { opacity: 0; transform: translateY(10px) scale(.97) } to { opacity: 1; transform: none } }

.modal-close {
  position: absolute; top: 14px; right: 14px;
  width: 32px; height: 32px; border-radius: 50%;
  border: 1px solid var(--border); background: var(--surface);
  color: var(--text-soft); font-size: 18px; line-height: 1; cursor: pointer;
}
.modal-close:hover { background: var(--surface-2); }

.modal-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 6px; padding-right: 34px; }

.pill {
  font-size: 12px; font-weight: 600;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 999px; padding: 4px 12px; color: var(--text-soft);
}

.badge {
  font-size: 12px; font-weight: 600; border-radius: 999px; padding: 4px 12px;
  background: var(--warn-soft); color: var(--warn);
}
.badge.is-ok { background: var(--ok-soft); color: var(--ok); }
.badge.is-danger { background: var(--danger-soft); color: var(--danger); }

.modal-title { margin: 4px 0 16px; font-size: 18px; }

.qr-frame {
  background: #fff;
  border-radius: var(--radius);
  padding: 14px;
  display: grid; place-items: center;
  box-shadow: inset 0 0 0 1px var(--border);
}
.qr-box { width: 100%; max-width: 268px; aspect-ratio: 1; display: grid; place-items: center; }
.qr-box img, .qr-box svg { width: 100%; height: auto; display: block; }

.modal-actions { display: flex; gap: 10px; margin-top: 20px; }
.modal-actions .btn { flex: 1; }

/* 弹框里那张"保存核销图"：整张核销卡存成 PNG，不只是二维码 */
.modal-download {
  width: 100%;
  margin-top: 14px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}
.modal-download .icon { width: 17px; height: 17px; flex: none; }
/* 微信这类内置浏览器里下载走不通：主按钮改成一句说明（它本来也点不成下载），
   免得人反复点、以为是手滑。文案由 app.js 按 UA 换成「长按图片后点击保存到手机下载」。 */
.modal-download.is-blocked {
  color: var(--text-faint);
  cursor: default;
}
.modal-download.is-blocked .icon { opacity: .5; }
.modal-download .download-label { text-align: left; }

/* 微信里直接把核销图摊出来，用户长按就能存进相册 */
.card-preview { margin-top: 12px; text-align: center; }
.card-preview img {
  display: block;
  width: 100%;
  max-width: 232px;
  height: auto;
  margin: 0 auto;
  border-radius: 12px;
  box-shadow: 0 8px 20px rgba(26, 32, 51, .18);
  /* 长按要能弹出系统的「存储图像」，别把长按手势吃掉 */
  -webkit-touch-callout: default;
}
/* 图还没出来时先占个位：和核销图同样比例（760×1000 ≈ 1:1.316）的空框 + 转圈。
   —— 以前这里会露一个"裂开的图标 + alt 文字"，很难看。 */
.card-preview-slot {
  width: 100%;
  max-width: 232px;
  /* 核销图是 760×1000，占位框按同一比例撑开，图换进来时不会跳一下 */
  aspect-ratio: 760 / 1000;
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 12px;
  border: 1px dashed var(--border);
  background: linear-gradient(100deg, var(--surface-2) 30%, var(--surface) 50%, var(--surface-2) 70%);
  background-size: 220% 100%;
  animation: shimmer 1.3s infinite linear;
  color: var(--text-faint);
}
.card-preview-spinner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  animation: none;
}
.card-preview-spinner svg {
  color: var(--accent, #5b6cff);
  animation: spin 0.9s linear infinite;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}
/* 图出来了就把占位框收掉 */
.card-preview img:not(.is-loading) ~ .card-preview-slot,
.card-preview-slot[hidden] { display: none; }

.card-preview-hint {
  margin: 10px 0 0;
  font-size: 12px;
  line-height: 1.6;
  color: var(--text-faint);
}
/* 图还没加载出来时先别显示它：
   微信里 img 没 src 时会渲染成一个破图标 + 把 alt 当正文显示，
   现场看起来就是一张"碎图配一行丑字"。加载完再露出来。 */
.card-preview img.is-loading {
  opacity: 0;
  min-height: 0;
  height: 0;
}
.card-preview img:not(.is-loading) {
  opacity: 1;
  transition: opacity .18s ease;
}
/* 图彻底加载失败：别留一个破图标在那，改成一句人话 */
.card-preview img.is-broken { display: none; }
.card-preview.is-broken .card-preview-slot { display: none; }
.card-preview.is-broken .card-preview-hint::before {
  content: '核销图没加载出来，关掉重开这张码再试；还不行就点「刷新」。';
  display: block;
  margin-bottom: 6px;
  color: var(--warn);
}

/* ---- 微信这类内置浏览器：弹层只留一张能长按的核销图 ---- */
/* 核销图里本来就画着二维码，上面那个大二维码再显示出来就是两张码，既乱又占屏。
   这里把它和"保存核销图"那枚按钮一起去掉，整屏只给用户一个动作：长按存图。
   按钮的文案仍然由 app.js 改成「长按图片后点击保存到手机下载」，但不再占位 —— 
   因为指令就写在图正下方一行。 */
body.is-wechat .modal .qr-frame,
body.is-wechat .modal .modal-download,
body.is-wechat .modal .modal-title {
  display: none;
}
/* 微信里图给大一点：这是整屏唯一有用的东西，别抠在 232px */
body.is-wechat .modal { padding-top: 8px; }
body.is-wechat .card-preview { margin-top: 6px; }
body.is-wechat .card-preview img {
  max-width: min(78vw, 300px);
  box-shadow: 0 10px 26px rgba(26, 32, 51, .22);
}
body.is-wechat .card-preview-slot {
  max-width: min(78vw, 300px);
}
body.is-wechat .card-preview-hint {
  margin-top: 12px;
  font-size: 13px;
  font-weight: 500;
  color: var(--text);
}

/* ---------------- 其他 ---------------- */
.skeleton-card {
  height: 178px; border-radius: var(--radius);
  background: linear-gradient(100deg, var(--surface-2) 30%, var(--surface) 50%, var(--surface-2) 70%);
  background-size: 220% 100%;
  animation: shimmer 1.3s infinite linear;
  border: 1px solid var(--border);
}
@keyframes shimmer { from { background-position: 180% 0 } to { background-position: -60% 0 } }

.empty {
  text-align: center; color: var(--text-faint);
  padding: 36px 10px; font-size: 14px;
}

.toast {
  position: fixed; left: 50%; bottom: 32px; transform: translateX(-50%);
  z-index: 80;
  background: var(--text); color: var(--bg);
  padding: 11px 20px; border-radius: 999px;
  font-size: 14px; font-weight: 500;
  box-shadow: var(--shadow-lg);
  animation: fade .18s ease;
  max-width: 90vw; text-align: center;
}
.toast.is-bad { background: var(--danger); color: #fff; }

/* ---------------- 登录页 ---------------- */
.login-wrap {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 32px 20px;
  position: relative;
  z-index: 1;
}

.login-card {
  width: 100%;
  max-width: 388px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 28px 26px 22px;
  box-shadow: var(--shadow-lg);
}

.login-brand { display: flex; align-items: center; gap: 13px; margin-bottom: 24px; }
.login-brand .brand-text h1 { font-size: 18px; }
.login-brand .brand-text p { font-size: 12.5px; }

.field { display: block; margin-bottom: 15px; }
.field > span {
  display: block;
  font-size: 13px;
  color: var(--text-soft);
  margin-bottom: 6px;
}
.field input {
  width: 100%;
  padding: 11px 13px;
  font: inherit;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  transition: border-color .15s, background .15s, box-shadow .15s;
}
.field input:focus {
  outline: none;
  background: var(--surface);
  border-color: var(--brand);
  box-shadow: 0 0 0 3px rgba(59, 91, 253, .16);
}

.login-error {
  margin: 0 0 13px;
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  background: var(--danger-soft);
  color: var(--danger);
  font-size: 13px;
}

.login-hint {
  margin: 16px 0 0;
  font-size: 12.5px;
  line-height: 1.7;
  color: var(--text-faint);
}
.login-hint code {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 1px 5px;
  font-size: 12px;
}

/* ---------------- 顶栏里的当前用户 ---------------- */
.user-box {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 5px 6px 5px 14px;
  box-shadow: var(--shadow-sm);
  font-size: 13px;
  /* 同上：宁可整体换行，也不要让「欢迎您管理员」竖着排 */
  flex: none;
  white-space: nowrap;
}
.user-box .user-greet { font-weight: 600; }
.user-box .user-name { color: var(--text-faint); font-size: 12.5px; }
.user-box .btn { padding: 6px 12px; font-size: 12.5px; }

/* ---------------- 卡片上的「核销上限」 ---------------- */
/* 显示在积分那行的后面：每天最多核销几个、一周内最多几天 */
.card-limits {
  margin-top: 2px;
  padding: 8px 11px;
  border-radius: 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--text-soft);
}
/* 「核销上限」做成小胶囊，颜色跟着这张卡的强调色，整张卡才像一套 */
.card-limits .limit-label {
  display: inline-block;
  margin-right: 7px;
  padding: 1.5px 8px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--accent, var(--brand));
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .4px;
}
.card-limits b {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--accent, var(--brand));
}

/* 面板标题右边当按钮用的链接（种类 → 验真黑号说明） */
.panel-head .btn-link { margin-left: auto; }

/* 「发放记录」标题右边那颗"只看某个种类"的小标签，点一下取消筛选 */
.filter-chip {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 11px 5px 13px;
  border: 1px solid var(--brand);
  border-radius: 999px;
  background: rgba(59, 91, 253, .1);
  color: var(--brand);
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: .3px;
  cursor: pointer;
  transition: background .15s ease;
}
.filter-chip:hover { background: rgba(59, 91, 253, .18); }
.filter-chip[hidden] { display: none; }
.filter-chip-x { font-size: 15px; line-height: 1; opacity: .65; }
.btn-sm { padding: 6px 12px; font-size: 13px; }

/* 顶栏里当按钮用的链接（返回发码页面） */
a.btn {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
}

/* ---------------- 说明文档页（验真黑号说明） ---------------- */
.doc-panel { max-width: 920px; margin: 0 auto; }

.doc-hero { margin: 0 0 20px; }
.doc-hero a { display: block; }
.doc-hero img {
  display: block;
  width: 100%;
  max-width: 100%; /* 手机上别让这张大图把整页撑宽 */
  height: auto;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: #0b1020; /* 图是深色底，加载时先铺个底色，别闪白 */
}
.doc-hero figcaption {
  margin-top: 9px;
  text-align: center;
  font-size: 12.5px;
  color: var(--text-faint);
}

.doc {
  font-size: 15px; line-height: 1.8; color: var(--text);
  /* 手机兜底：正文里的长英文、长数字（如 PRODUCT_LIMIT）也按页面宽度折行，
     宽图/代码块不许超过页面宽度，免得整页被撑宽、文字跑到屏幕外 */
  overflow-wrap: anywhere;
  word-break: break-word;
}
.doc img, .doc pre, .doc table, .doc iframe { max-width: 100%; }
.doc pre { overflow-x: auto; white-space: pre-wrap; }
/* 标题：左边一道品牌蓝竖条，跟页面上「种类」「发放记录」一个样子 */
.doc h1 { margin: 4px 0 14px; font-size: 25px; letter-spacing: .2px; }
.doc h2 {
  position: relative;
  margin: 32px 0 14px;
  padding: 0 0 9px 14px;
  font-size: 19px;
  color: var(--text);
  border-bottom: 2px solid var(--doc-line);
}
.doc h2::before {
  content: "";
  position: absolute;
  left: 0;
  top: .22em;
  width: 4px;
  height: 1em;
  border-radius: 2px;
  background: linear-gradient(180deg, var(--brand), var(--brand-2));
}
/* 产品名这类三级标题铺一条浅蓝底，扫一眼就能找到 */
.doc h3 {
  margin: 22px 0 8px;
  padding: 5px 12px;
  font-size: 16px;
  color: var(--doc-accent);
  background: var(--doc-soft);
  border-radius: var(--radius-sm);
}
.doc h4 { margin: 18px 0 6px; font-size: 15px; color: var(--doc-accent); }

.doc p { margin: 10px 0; }
.doc ul, .doc ol { margin: 10px 0; padding-left: 22px; }
/* 列表圆点跟着品牌蓝走，默认那个黑点太闷 */
.doc ul > li::marker, .doc ol > li::marker { color: var(--doc-accent); }
.doc li { margin: 5px 0; }

.doc strong { font-weight: 700; color: var(--doc-accent); }
.doc mark {
  padding: 2px 6px;
  border-radius: 6px;
  background: var(--doc-mark);
  color: var(--doc-accent);
  font-weight: 600;
}
.doc blockquote {
  margin: 16px 0;
  padding: 11px 15px;
  border: 1px solid var(--doc-line);
  border-left: 4px solid var(--doc-accent);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background: var(--doc-soft);
  color: var(--text-soft);
}
.doc code {
  padding: 1px 6px;
  border-radius: 6px;
  background: var(--doc-soft);
  border: 1px solid var(--doc-line);
  color: var(--doc-accent);
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 12.5px;
}
.doc hr { margin: 26px 0; border: none; border-top: 1px dashed var(--doc-line); }

/* 折叠块：默认那个黑三角太原始，收成蓝色圆角块，三角自己画、开合会转 */
.doc details {
  margin: 12px 0;
  padding: 10px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}
.doc details[open] { background: var(--surface); border-color: var(--doc-line); }
.doc details > summary {
  position: relative;
  padding-left: 20px;
  font-weight: 600;
  color: var(--doc-accent);
  cursor: pointer;
  list-style: none;
}
.doc details > summary::-webkit-details-marker { display: none; }
.doc details > summary::before {
  content: "▸";
  position: absolute;
  left: 3px;
  top: 0;
  transition: transform .15s ease;
}
.doc details[open] > summary::before { transform: rotate(90deg); }
.doc details > summary + * { margin-top: 10px; }

.doc-loading, .doc-meta { color: var(--text-faint); font-size: 12.5px; text-align: center; }
.doc-meta:empty { display: none; }
.doc-error { color: var(--danger); }

/* ------------------------------------------------------------
   动效：只做很轻的一点，别喧宾夺主
   （现场反馈上一版太花：扫光、底色渐变游走、绿点心跳、二维码呼吸都去掉了，
     按钮的光晕也调淡调慢了一档，现在这一档现场认可了。想再挑别的强度说一声。）
   - 「获取验真码」按钮：跟着卡片自己的颜色极缓地呼吸一下，4.8 秒一轮
     （光晕色是 app.js 按卡片颜色算好塞进 --accent-glow 的）
   - 卡片第一次出现：只淡入，不位移
   - 数字变化：轻轻淡出来一下，不放大
   - 台账里新发的那条：左侧亮一条细边
   系统开了「减少动态效果」时这些全部关掉，见文件末尾。
   ------------------------------------------------------------ */

@keyframes breathe {
  0%, 100% { box-shadow: inset 0 1px 0 rgba(255, 255, 255, .22), var(--shadow-sm); }
  50%      { box-shadow: inset 0 1px 0 rgba(255, 255, 255, .22), var(--shadow-sm), 0 3px 12px -4px var(--accent-glow, rgba(59, 91, 253, .45)); }
}

@keyframes appear {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes bump {
  from { opacity: .45; }
  to   { opacity: 1; }
}

@keyframes row-in {
  from { background: var(--ok-soft); box-shadow: inset 3px 0 0 var(--ok); }
  to   { background: transparent; box-shadow: inset 3px 0 0 transparent; }
}

/* 只有"能点"的按钮才呼吸：禁用的按钮一动不动，免得看着像能点 */
.card .btn-primary:not(:disabled) { animation: breathe 4.8s ease-in-out infinite; }

.card .btn-primary:active:not(:disabled) { transform: translateY(1px); }

/* 第一次渲染时卡片淡淡出现（.anim-in 由 app.js 只加一次） */
.cards.anim-in .card {
  animation: appear .3s ease-out backwards;
  animation-delay: calc(var(--i, 0) * 35ms);
}

/* 数字变了就轻轻淡一下 */
.stat-num.bump { animation: bump .45s ease-out; }

/* 刚发出去的那条记录，左边亮一条细边 */
.ledger tbody tr.is-new { animation: row-in 1.1s ease-out; }

@media (max-width: 560px) {
  .topbar { padding: 18px 16px 4px; row-gap: 10px; }
  /* 顶栏状态那一排单独占一行，每项保持横排、不被压成竖排文字 */
  .topbar-actions { width: 100%; gap: 8px; }
  main { padding: 10px 14px 48px; }
  .panel { padding: 16px; }
  .user-box { padding-left: 12px; }
  .user-box .user-name { display: none; }

  /* ---------------- 手机版卡片：横过来放 ---------------- */
  /* 几个种类竖着叠起来，手机上要滑一屏半才看得到「发放记录」。
     所以窄屏改成横排：种类名 + 积分挪到左边，成一条窄竖栏（名字长了自己折行），
     右边只剩「可发放 / 已发放」两个数字，就能放大一号。
     手机上主要就是「看一眼数字、点取码」，所以按钮还是整行、够大好点。 */
  /* 内容从右边挪走一块以后，卡片不用再顶着两边的边线，
     左右各让出 12px 居中：整体看着收一点，也不影响单手够到按钮 */
  .cards { gap: 10px; padding: 0 12px; }
  .card {
    flex-direction: row;
    align-items: stretch;
    gap: 12px;
    padding: 12px 12px 12px 0;
  }
  /* 顶上那道色条改到左边竖着走，跟左边那条名字栏对得上 */
  .card::before {
    inset: 0 auto 0 0;
    width: 4px; height: auto;
    background: linear-gradient(180deg, var(--accent, var(--brand)), transparent);
  }
  /* 左边那条：产品名在上、「600 积分/个」在下，底色用这张卡自己的颜色淡淡铺一层 */
  .card-top {
    flex: 0 0 68px;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 7px;
    padding: 8px 6px 8px 12px;
    margin: -12px 0; /* 抵消卡片的上下内边距，让底色铺满整张卡的高度 */
    background: linear-gradient(180deg, var(--accent-soft, rgba(59, 91, 253, .1)), transparent 88%);
  }
  .card-name { font-size: 14.5px; line-height: 1.35; text-align: center; }
  /* 左边栏自带颜色，名字前的小色点就不用了 —— 窄栏里它白占位置 */
  .card-name::before { display: none; }
  /* 积分/个 跟着名字落在窄栏里：窄栏只有五十来像素，桌面版那样一行写不下，
     这里竖回"数字一行、单位一行"。它是固定值，用配角灰、字号小一档，
     不跟「可发放」抢眼神 */
  .card-points {
    flex-direction: column;
    align-items: center;
    gap: 0;
    padding: 0;
    border: none;
    background: none;
  }
  .card-points .points-num { font-size: 14px; }
  .card-points .points-label { font-size: 10px; letter-spacing: 0; }
  /* 「只看这个」那个小标签是给桌面版看的：手机版一点就把别的卡收起来了，
     不必再说一遍，还占地方 */
  .card.is-selected .card-top::after { display: none; }

  /* 右边那栏：只剩「可发放 / 已发放」两个数字，字就能放大一号。
     标签挪到数字前面 ——「可发放 14」比「14 可发放」好读，所以这里用 order 换一下顺序（不动 HTML）；
     两块靠在一起读（不再 space-between 铺满一行），右边就不会空一大片 */
  .card-body { gap: 8px; }
  .stats { gap: 16px; justify-content: flex-start; align-items: baseline; }
  .stat { flex: 0 1 auto; flex-direction: row; align-items: baseline; gap: 6px; }
  .stat-label { order: 0; font-size: 13px; }
  .stat-num { order: 1; font-size: 22px; letter-spacing: -.2px; }
  .stat-done { text-align: left; }
  /* 「核销上限」那一格收一收：窄屏上让它尽量一行放完，别白占两行高度 */
  /* 「核销上限」胶囊自己占一行，下面那行细则就能整行放下，不会在句子中间断开 */
  .card-limits { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; padding: 6px 9px; font-size: 12.5px; line-height: 1.45; }
  .card-limits .limit-label { margin-right: 0; padding: 1px 6px; font-size: 11px; }
  /* 按钮比桌面版收一档：卡片里它最占地方，手机上不用那么高 */
  .card-body .btn-block { padding: 7px 12px; font-size: 13px; }

  /* 点某张卡＝只看这个种类：把别的卡收起来，「发放记录」立刻上到第一屏。
     再点一次这张卡（或点标题右边「只看 XX」那个标签）就恢复全部。 */
  .cards.has-selection .card:not(.is-selected) { display: none; }
  /* 收起来时留一句话，免得不知道还能恢复 */
  .card.is-selected .card-body::after {
    content: "再点这张卡看全部种类";
    font-size: 11px;
    color: var(--text-faint);
    text-align: center;
  }

  /* 手机上把台账压紧一点：字号小一号、内边距收一收，
     剩下的列就能在屏幕上放下，不用横向滑 */
  .ledger { font-size: 12px; }
  .ledger th { font-size: 11px; padding: 8px 6px; letter-spacing: 0; }
  .ledger td { padding: 8px 6px; }
  /* 发放时间挪到「用户」下面那一行，整个“发放时间”列就不用占了 ——
     少一列，手机上点这一行调二维码时也不用横向滑 */
  .ledger .col-time { display: none; }
  /* 「日志ID」列手机上不显示（管理员也不显示）：那么长的ID挤进来没法看，
     要查日志的人在电脑上查。两条规则都要写，不然 specificity 打不过上面那个。 */
  .ledger .col-log, .ledger.show-log .col-log { display: none; }
  .ledger .cell-sub { display: block; margin-top: 2px; font-size: 10.5px; color: var(--text-faint); letter-spacing: 0; }
  /* 手机上那行是手指点的，箭头给小一点，别把状态挤到第二行 */
  .ledger .row-cue { margin-left: 4px; font-size: 13px; }
  /* 手机屏矮：滚动区再收一档，免得整块占满一屏、看不到底下的说明 */
  .table-wrap { max-height: 54vh; }
  .ledger-note { margin-top: 8px; font-size: 11.5px; }

  /* 只看某个种类时，那一列「种类」全是同一个名字，白占一列宽度；
     手机上正好省掉一次横向滑动（类名由 renderLedgerFilter 打上去） */
  .ledger-only-one .ledger .col-product { display: none; }

  /* 手机上正文和留白都收一点，图和文字都好读 */
  .doc { font-size: 15px; line-height: 1.75; }
  .doc h1 { font-size: 21px; }
  .doc h2 { margin-top: 26px; font-size: 18px; padding-bottom: 8px; padding-left: 12px; }
  .doc h3 { font-size: 15.5px; padding: 4px 10px; }
  .doc ul, .doc ol { padding-left: 18px; }
  .doc blockquote { padding: 10px 12px; }
  .doc details { padding: 9px 12px; }
  .doc code { font-size: 12px; }
  .doc-hero { margin-bottom: 16px; }
}

/* 打印 / 存 PDF：去掉光斑和按钮，省纸也省墨（页脚版本号也一起省掉） */
@media print {
  .glow, .topbar-actions, .toast, .app-footer { display: none !important; }
  .panel { border: none; box-shadow: none; padding: 0; }
  .doc-panel { max-width: none; }
  .doc-hero img { border: none; }
  a.btn { display: none; }
}

/* 有些同事会晕动效，系统里开了「减少动态效果」就把动画和过渡全关掉：
   页面回到静态，功能一点不受影响 */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
  }
}
