/* 白名单自助平台 —— 视觉系统
 *
 * 主题：网络设备面板。这个站只回答一个问题 —— 你的包能不能过闸。
 * 所以整套视觉都从它自己的拓扑图里长出来：你 → CDN 边缘 → 站点。
 * 通道色（teal）= 放行，信号色（amber）= 拦下，两个颜色是语义而不是装饰。
 *
 * 约束：CSP 是 script-src 'self'; style-src 'self'，没有 font-src。
 * 所以没有外部字体、没有内联样式，全部走系统字体栈 + 这一个文件。
 */

:root {
  color-scheme: light dark;

  /* 表面：冷灰蓝的纸，不是暖米色 */
  --paper: #f3f6f7;
  --panel: #ffffff;
  --rule: #dce3e5;
  --rule-strong: #c2cdd1;
  --etch: #ffffff;

  /* 文字 */
  --ink: #0f191d;
  --ink-soft: #445661;
  --ink-dim: #71848d;

  /* 语义色。-lit 是深色面上用的亮版本 */
  --pass: #0c8a72;
  --pass-lit: #35e0c0;
  --hold: #a86500;
  --hold-lit: #f2a93b;
  --halt: #bd3529;
  --halt-lit: #ff6b60;
  --accent: var(--pass);

  --pass-wash: color-mix(in oklab, var(--pass) 12%, var(--panel));
  --hold-wash: color-mix(in oklab, var(--hold) 13%, var(--panel));
  --halt-wash: color-mix(in oklab, var(--halt) 11%, var(--panel));

  --sans: system-ui, -apple-system, "Segoe UI Variable Text", "Segoe UI",
          "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
          "Noto Sans CJK SC", sans-serif;
  --mono: ui-monospace, "SF Mono", SFMono-Regular, "JetBrains Mono",
          "Cascadia Mono", Consolas, "Liberation Mono", monospace;

  --r: 10px;
  --r-sm: 6px;
  --shadow: 0 1px 2px rgb(15 25 29 / 5%), 0 10px 30px -18px rgb(15 25 29 / 22%);
  /* 工作区的最大量度。不设上限的话宽屏上输入框会拉到 1400px、
     表格列被甩到两边，中间全是空白。 */
  --work: 96rem;
  --field: 26rem;
}

@media (prefers-color-scheme: dark) {
  :root {
    --paper: #080e11;
    --panel: #0f1a1e;
    --rule: #1c292e;
    --rule-strong: #2b3d43;
    --etch: #16242a;

    --ink: #dde9eb;
    --ink-soft: #9db0b6;
    --ink-dim: #6c838a;

    --pass: #35e0c0;
    --hold: #f2a93b;
    --halt: #ff6b60;

    --pass-wash: color-mix(in oklab, var(--pass) 16%, var(--panel));
    --hold-wash: color-mix(in oklab, var(--hold) 16%, var(--panel));
    --halt-wash: color-mix(in oklab, var(--halt) 16%, var(--panel));

    --shadow: 0 1px 2px rgb(0 0 0 / 40%), 0 14px 34px -20px rgb(0 0 0 / 70%);
  }
}

*, *::before, *::after { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--paper);
  color: var(--ink);
  font: 15px/1.75 var(--sans);
  -webkit-text-size-adjust: 100%;
  accent-color: var(--pass);
}

/* CJK 正文在 1.75 行距下才不挤；标题反过来要收紧 */
h1, h2, h3 {
  margin: 0 0 .55rem;
  line-height: 1.3;
  letter-spacing: .005em;
  text-wrap: balance;
}
h1 { font-size: clamp(1.32rem, 1.16rem + .55vw, 1.6rem); font-weight: 650; }
h2 { font-size: 1.075rem; font-weight: 620; }
h3 { font-size: .95rem; font-weight: 620; margin: 1.35rem 0 .5rem; }
p { margin: .5rem 0; text-wrap: pretty; }

/* 眉标：每块面板顶上的系统标识（SITE #999 / ACL / SMTP …）。
   用的是这套系统里真实存在的标识符，不是装饰性的编号。 */
.eyebrow {
  margin: 0 0 .7rem;
  font: 600 10.5px/1 var(--mono);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-dim);
  display: flex;
  align-items: center;
  gap: .6rem;
}
.eyebrow::after {
  content: "";
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, var(--rule), transparent);
}

code, .mono {
  font-family: var(--mono);
  font-size: .9em;
  font-variant-numeric: tabular-nums;
}
code {
  padding: .1em .4em;
  border-radius: 4px;
  background: color-mix(in oklab, var(--ink) 7%, transparent);
  /* 必须是 break-word 而不是 break-all。break-all 允许在任意两个字符之间断行，
     于是这个元素的 min-content 宽度只剩一个字符 —— 表格里只要有一列被指定吃掉
     剩余宽度（th.grow），其余列就会被压到最小宽度，IP 直接竖着排成一条。
     break-word 的断行机会按规范不参与 min-content 计算，列宽永远不低于整个词。 */
  overflow-wrap: break-word;
}

a { color: var(--pass); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: color-mix(in oklab, var(--pass) 78%, var(--ink)); }

.hint { color: var(--ink-soft); font-size: .92rem; margin: .5rem 0; }
.dim { color: var(--ink-dim); }
.right { text-align: right; }
.warn-text { color: var(--hold); }

:focus-visible {
  outline: 2px solid var(--pass);
  outline-offset: 2px;
  border-radius: 3px;
}

/* ---- 状态导轨 ------------------------------------------------------------
   页面最顶上那条 3px 的灯带，颜色本身就是结论：通道色=你已放行，
   信号色=你不在名单里。在读到任何一个字之前就已经回答了问题。 */
.rail {
  height: 3px;
  background: var(--rule-strong);
  view-transition-name: rail;
  transform-origin: left;
  animation: rail-in .5s cubic-bezier(.2, .75, .25, 1) both;
}
@keyframes rail-in { from { transform: scaleX(0); } }
.rail.pass { background: linear-gradient(90deg, var(--pass), color-mix(in oklab, var(--pass) 45%, var(--paper))); }
.rail.hold { background: linear-gradient(90deg, var(--hold), color-mix(in oklab, var(--hold) 40%, var(--paper))); }
.rail.halt { background: linear-gradient(90deg, var(--halt), color-mix(in oklab, var(--halt) 40%, var(--paper))); }

/* ---- 版面 ----------------------------------------------------------------
   浅色为主：侧栏用比工作区略深一点的纸色，靠一条细线分开，不做深色机箱。
   工作区是纯白，内容靠左贴着侧栏。未登录时不给侧栏，只有一张居中的卡片。 */
.app {
  flex: 1;
  display: grid;
  grid-template-columns: 15rem minmax(0, 1fr);
  min-height: 0;
}
/* 未登录时没有 aside，别让 main 掉进 15rem 那一列 */
body.plain .app { grid-template-columns: minmax(0, 1fr); }

.side {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  align-self: start;
  position: sticky;
  top: 0;
  height: 100dvh;
  overflow-y: auto;
  padding: 1.15rem .9rem 1rem;
  background: var(--paper);
  color: var(--ink);
  border-right: 1px solid var(--rule);
  view-transition-name: side;
}
.side .mark {
  display: block;
  padding: .1rem .45rem;
  color: var(--ink);
  font-weight: 640;
  font-size: .95rem;
  letter-spacing: .01em;
  text-decoration: none;
}
.side .mark span {
  display: block;
  margin-top: .25rem;
  font: 500 10.5px/1.4 var(--mono);
  letter-spacing: .06em;
  color: var(--ink-dim);
}
.side nav { display: flex; flex-direction: column; gap: .1rem; }
.side .navgroup {
  margin: .9rem 0 .35rem;
  padding: 0 .45rem;
  font: 600 9.5px/1 var(--mono);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.side .navgroup:first-child { margin-top: 0; }

.side nav a {
  display: flex;
  align-items: center;
  gap: .55rem;
  padding: .42rem .45rem;
  border-radius: var(--r-sm);
  color: var(--ink-soft);
  font-size: .9rem;
  text-decoration: none;
  transition: background-color .12s, color .12s;
}
.side nav a:hover { background: color-mix(in oklab, var(--ink) 5%, transparent); color: var(--ink); }
/* 当前页：左沿点亮一小段通道色，比整块高亮安静 */
.side nav a.on {
  color: var(--ink);
  font-weight: 560;
  background: color-mix(in oklab, var(--pass) 9%, transparent);
  box-shadow: inset 2px 0 0 var(--pass);
}
/* 有事待办（没绑邮箱）：右侧一个琥珀色小点。
   没有这个提示，普通用户根本不会想到点进"邮箱"那一页，
   于是忘了密码还是来找管理员 —— 找回功能等于白做。 */
.side nav a.needs::after {
  content: "";
  width: .4rem;
  height: .4rem;
  margin-left: auto;
  border-radius: 50%;
  background: var(--hold);
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--hold) 22%, transparent);
}

.side-foot {
  margin-top: auto;
  padding-top: .9rem;
  border-top: 1px solid var(--rule);
  display: flex;
  align-items: center;
  gap: .6rem;
  flex-wrap: wrap;
  font-size: .85rem;
}
.side-foot .who {
  flex: 1 1 auto;
  min-width: 0;
  font: 500 11.5px/1 var(--mono);
  color: var(--ink);
  overflow-wrap: anywhere;
}
.side-foot a { color: var(--ink-soft); text-decoration: none; }
.side-foot a:hover { color: var(--pass); }
.side-foot button.link { color: var(--ink-soft); text-decoration: none; }
.side-foot button.link:hover { color: var(--pass); }
/* 窄屏上机箱变成一条横向导航，这里的按钮会被挤到只剩一个字的宽度，
   "退出"就竖成两行。两个字的按钮不许换行。 */
.side-foot a, .side-foot button.link { white-space: nowrap; }

main {
  flex: 1;
  min-width: 0;
  background: var(--panel);
  padding: clamp(1.1rem, 2.4vw, 2rem) clamp(.9rem, 2.6vw, 2.25rem) 3.5rem;
  view-transition-name: main;
}
/* 工作区靠左贴着机箱，但有量度上限 */
main > * { max-width: var(--work); }
/* 未登录：没有侧栏，把卡片摆到视野中间 */
body.plain main {
  display: grid;
  align-content: center;
  background: none;
}

/* ---- 页头与分区 ----------------------------------------------------------
   工作区里不用"白盒子浮在灰底上"那一套。分区之间只有一条细线，
   标题和说明落在左侧的标签栏里，控件在右边 —— 读起来像一张规格表，
   而不是一叠卡片。 */
.page { max-width: 46rem; margin-bottom: .5rem; }
.page h1 { margin-bottom: .3rem; }
.lede { margin: 0; color: var(--ink-soft); font-size: .92rem; }

.pane {
  display: grid;
  grid-template-columns: 13.5rem minmax(0, 1fr);
  gap: clamp(1rem, 3vw, 2.75rem);
  padding: 1.6rem 0;
  border-top: 1px solid var(--rule);
}
.pane:last-child { border-bottom: 1px solid var(--rule); }
.pane-label { min-width: 0; }
.pane-label h2 { margin: 0 0 .3rem; font-size: .98rem; }
.pane-label .hint { margin: 0; font-size: .85rem; }
.pane-body { min-width: 0; container-type: inline-size; }
.pane-body > :first-child { margin-top: 0; }
.pane-body > .hint:first-child { margin-top: -.2rem; }

/* 数据型分区：左侧标签栏对表格是灾难 —— 表被挤成中间一条，左右都是空白。
   这种分区把标题压成一行放在上面，表格从左边缘起算，铺满整个量度。 */
.pane.data {
  grid-template-columns: minmax(0, 1fr);
  gap: .5rem;
}
.pane.data .pane-label {
  display: flex;
  align-items: baseline;
  gap: .3rem .9rem;
  flex-wrap: wrap;
}
.pane.data .pane-label h2 { margin: 0; }
.pane.data .pane-label .hint { margin: 0; }
/* 让列宽贴着内容，而不是把剩余空间平摊出去 —— 否则两列的表会把
   "归属"甩到一千像素开外。表整体不超过量度，窄屏仍可横向滚动。 */
.pane.data table { width: auto; min-width: min(100%, 24rem); max-width: 100%; }
/* 列多的表反过来要铺满量度，否则宽屏上右边空一大块。
   诀窍是让某一列（备注、详情这种可长可短的）吃掉剩余空间，
   其余列仍然紧贴内容，不会被平摊开。 */
.pane.data table.fill { width: 100%; }
.pane.data table.fill th.grow { width: 100%; }
/* 单元格一律不换行，只有指定吃宽度的那一列（备注/详情/邮箱）可以折行。
   为什么不能让它们自由换行：th.grow 吃掉剩余宽度后其余列被压到 min-content，
   而中日韩文字默认任意两字之间都能断行，min-content 等于一个字 ——
   "生效白名单"会竖成五行、时间戳断成三行。
   （这和 code 的 break-all 是同一个坑的两种形态，上一轮只修了 code。）
   代价是宽度不够时表格会横向溢出，所以下面给数据分区加了滚动容器。 */
.pane.data table th,
.pane.data table td { white-space: nowrap; }
.pane.data table td.grow { white-space: normal; }
/* 表格自己横向滚动，而不是把整个文档撑宽。不按断点猜宽度 ——
   靠断点判断"装不装得下"取决于内容长短（一个长邮箱就能推翻假设），
   之前就是这么漏掉 941..1200 这一段的。代价是表头的 position: sticky
   在这个滚动容器里会失效，所以上面那条 sticky 已经去掉了。 */
.pane.data .pane-body { overflow-x: auto; }
/* 列宽贴着内容之后，列间距全靠 padding 撑，得给足一点 */
.pane.data th, .pane.data td { padding: .45rem .9rem; }
.pane.data th:first-child, .pane.data td:first-child { padding-left: 0; }
.pane.data th:last-child, .pane.data td:last-child { padding-right: 0; }
@media (max-width: 940px) {
  .pane { grid-template-columns: 1fr; gap: .9rem; }
}

/* 窄屏：机箱转成顶部一条可横向滑动的导航 */
@media (max-width: 900px) {
  .app { grid-template-columns: 1fr; }
  .side {
    flex-direction: row;
    align-items: center;
    gap: .75rem;
    position: static;
    height: auto;
    overflow-x: auto;
    padding: .6rem .75rem;
    border-right: 0;
    border-bottom: 1px solid var(--rule);
  }
  .side .mark span { display: none; }
  .side nav { flex-direction: row; gap: .15rem; }
  .side .navgroup { display: none; }
  .side nav a { white-space: nowrap; }
  .side nav a.on { box-shadow: inset 0 -2px 0 var(--pass); }
  .side-foot {
    margin: 0 0 0 auto;
    padding: 0 0 0 .75rem;
    border-top: 0;
    border-left: 1px solid var(--rule);
    flex-wrap: nowrap;
  }
  .side-foot .who { flex: none; }
}

/* ---- 卡片：只留给登录 / 注册 / 找回这类"只做一件事"的页面 --------------- */
.card {
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--r);
  padding: clamp(1.1rem, 2.2vw, 1.5rem);
  container-type: inline-size;
}
.card:has(> .flash.error) { border-left: 2px solid var(--halt); }
.card:has(> .flash.warn) { border-left: 2px solid var(--hold); }

.card.narrow {
  width: 100%;
  max-width: 26rem;
  margin: 0 auto;
  box-shadow: var(--shadow);
}
.card.center { text-align: center; }
.card.center .eyebrow { justify-content: center; }
.card.center .eyebrow::after { display: none; }

.card.narrow button.primary { width: 100%; margin-top: .5rem; }
.card.narrow + .card.narrow { margin-top: 1rem; }
.foot-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: .5rem 1rem;
  margin: 1rem 0 0;
  padding-top: .85rem;
  border-top: 1px solid var(--rule);
  font-size: .85rem;
  color: var(--ink-dim);
}
.foot-links > * { margin: 0; }

/* ---- 闸口 ----------------------------------------------------------------
   这一块就是这个站的全部内容：你的包走到哪一跳、过没过。
   拓扑图不是插画，它是 /whoami 那个页面存在的原因 —— 到底哪一跳才是你。
   浅色底上靠信号色的淡染和边框立出来，不做深色面板。 */
.gate {
  position: relative;
  overflow: hidden;
  display: grid;
  grid-template-columns: minmax(0, auto) minmax(13rem, 1fr);
  gap: clamp(1.2rem, 4vw, 3rem);
  align-items: center;
  margin-bottom: 1rem;
  padding: clamp(1.2rem, 3vw, 2rem);
  border: 1px solid color-mix(in oklab, var(--sig) 32%, var(--rule));
  border-radius: var(--r);
  background: color-mix(in oklab, var(--sig) 5%, var(--panel));
  --sig: var(--pass);
  --wire: color-mix(in oklab, var(--ink) 20%, transparent);
  animation: gate-in .4s cubic-bezier(.2, .7, .3, 1) both;
}
.gate.is-pass { --sig: var(--pass); }
.gate.is-hold { --sig: var(--hold); }
.gate.is-halt { --sig: var(--halt); }

.gate::before {
  content: "";
  position: absolute;
  top: -50%;
  right: -6%;
  width: 24rem;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle,
              color-mix(in oklab, var(--sig) 10%, transparent), transparent 68%);
  pointer-events: none;
}
.gate > * { position: relative; }
.gate .eyebrow { color: var(--ink-dim); }
@media (max-width: 700px) {
  .gate { grid-template-columns: 1fr; }
}

.gate-ip {
  margin: 0;
  font: 620 clamp(1.5rem, 1.05rem + 2.3vw, 2.45rem)/1.08 var(--mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -.015em;
  word-break: break-all;
}
.gate-verdict {
  display: flex;
  align-items: center;
  gap: .55rem;
  margin: .6rem 0 0;
  font-size: .92rem;
  color: var(--sig);
}
.gate-verdict::before {
  content: "";
  flex: none;
  width: .45rem;
  height: .45rem;
  border-radius: 50%;
  background: var(--sig);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--sig) 24%, transparent);
}
.gate-note {
  margin: .75rem 0 0;
  font-size: .85rem;
  color: var(--ink-soft);
}
.gate-note a { color: var(--sig); }

.chain-track { display: flex; align-items: center; }
.chain-track .dot {
  flex: none;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--sig);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--sig) 20%, transparent);
  animation: dot-in .34s cubic-bezier(.2, .8, .3, 1.2) both;
}
.chain-track .dot:nth-of-type(1) { animation-delay: .1s; }
.chain-track .dot:nth-of-type(2) { animation-delay: .22s; }
.chain-track .dot:nth-of-type(3) { animation-delay: .34s; }

/* 线缆用 <i>，节点用 <span>，这样 nth-of-type 各数各的，不用靠 nth-child 算位置 */
.chain-track .link {
  position: relative;
  flex: 1;
  min-width: 1.75rem;
  height: 14px;
  overflow: hidden;
  background-image: linear-gradient(var(--wire), var(--wire));
  background-size: 100% 1px;
  background-position: center;
  background-repeat: no-repeat;
}
/* 数据包：从左往右穿过线缆。放行时穿到底，拦下时停在闸门前。 */
.chain-track .link::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  width: 30%;
  height: 1px;
  transform: translate(-110%, -50%);
  background: linear-gradient(90deg, transparent, var(--sig), transparent);
  animation: packet .95s cubic-bezier(.45, .05, .35, 1) var(--packet-delay, .4s) both;
}
.chain-track .link:nth-of-type(1) { --packet-delay: .4s; }
.chain-track .link:nth-of-type(2) { --packet-delay: .82s; }

/* 没在名单里：最后一跳灭灯，第二段线缆上竖起闸门，包停在闸门前 */
.gate.is-hold .chain-track .dot:last-of-type,
.gate.is-halt .chain-track .dot:last-of-type {
  background: var(--wire);
  box-shadow: none;
}
.gate.is-hold .chain-track .link:nth-of-type(2)::before,
.gate.is-halt .chain-track .link:nth-of-type(2)::before {
  content: "";
  position: absolute;
  left: 62%;
  top: 1px;
  bottom: 1px;
  width: 2px;
  border-radius: 1px;
  background: var(--sig);
}
.gate.is-hold .chain-track .link:nth-of-type(2)::after,
.gate.is-halt .chain-track .link:nth-of-type(2)::after {
  animation-name: packet-halt;
}
/* 放行时终点灯呼吸 —— 全站唯一的环境动效，就是设备上的 link 灯 */
.gate.is-pass .chain-track .dot:last-of-type {
  animation: dot-in .34s .34s both, led 3.4s ease-in-out 1.6s infinite;
}

.chain-stops {
  display: flex;
  justify-content: space-between;
  gap: .5rem;
  margin: .8rem 0 0;
  padding: 0;
  list-style: none;
  font: 500 10.5px/1.4 var(--mono);
  letter-spacing: .08em;
  color: var(--ink-dim);
}
.chain-stops li:nth-child(2) { text-align: center; }
.chain-stops li:last-child { text-align: right; }

@keyframes gate-in { from { opacity: 0; transform: translateY(10px); } }
@keyframes dot-in { from { opacity: 0; transform: scale(.25); } }
@keyframes packet {
  from { transform: translate(-110%, -50%); }
  to { transform: translate(380%, -50%); }
}
@keyframes packet-halt {
  from { transform: translate(-110%, -50%); }
  72% { transform: translate(142%, -50%); opacity: 1; }
  to { transform: translate(142%, -50%); opacity: .2; }
}
@keyframes led {
  50% { box-shadow: 0 0 0 7px color-mix(in oklab, var(--sig) 7%, transparent); }
}

/* ---- 表单 --------------------------------------------------------------- */
label { display: block; margin: .85rem 0; font-size: .9rem; font-weight: 520; }
label small {
  display: block;
  margin-top: .3rem;
  font-size: .78rem;
  font-weight: 400;
  line-height: 1.6;
  color: var(--ink-dim);
}

input[type=text], input[type=password], input[type=email], input[type=number],
input:not([type]), select {
  width: 100%;
  max-width: var(--field);
  margin-top: .35rem;
  padding: .55rem .7rem;
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  transition: border-color .14s, background-color .14s, box-shadow .14s;
}
input::placeholder { color: color-mix(in oklab, var(--ink-dim) 75%, transparent); }
input:hover:not(:disabled), select:hover { border-color: var(--rule-strong); }
input:focus, select:focus {
  border-color: var(--pass);
  background: var(--panel);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--pass) 14%, transparent);
  outline: none;
}
input[name=ip], input[name=host], input[name=port], input[name=ident] {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}

label.check { display: flex; align-items: center; gap: .55rem; }
label.check input[type=checkbox] { width: auto; margin: 0; }
label.check small { display: inline; margin: 0; }

button {
  font: 520 .9rem/1.4 var(--sans);
  cursor: pointer;
  padding: .5rem .95rem;
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-sm);
  background: var(--panel);
  color: var(--ink);
  transition: border-color .14s, background-color .14s, transform .14s, box-shadow .14s;
}
button:hover:not(:disabled) {
  border-color: var(--pass);
  color: var(--pass);
  transform: translateY(-1px);
}
button:active:not(:disabled) { transform: translateY(0); }
button:disabled { opacity: .45; cursor: not-allowed; }
/* 禁用的主按钮不能还是一片实心通道色，否则看着像可以点 */
button.primary:disabled {
  background: color-mix(in oklab, var(--ink) 8%, transparent);
  color: var(--ink-dim);
  border-color: var(--rule);
  box-shadow: none;
  opacity: 1;
}

button.primary {
  border-color: transparent;
  background: var(--pass);
  color: var(--panel);
  font-weight: 600;
  box-shadow: 0 1px 2px color-mix(in oklab, var(--pass) 35%, transparent);
}
button.primary:hover:not(:disabled) {
  background: color-mix(in oklab, var(--pass) 88%, var(--ink));
  color: var(--panel);
  box-shadow: 0 4px 14px -4px color-mix(in oklab, var(--pass) 55%, transparent);
}
button.danger {
  color: var(--halt);
  border-color: color-mix(in oklab, var(--halt) 35%, var(--rule));
}
button.danger:hover:not(:disabled) {
  border-color: var(--halt);
  background: var(--halt-wash);
  color: var(--halt);
}
button.link {
  padding: 0;
  border: none;
  background: none;
  color: var(--ink-soft);
  /* 正文里的 button.link 要看得出是可点的，和旁边的 <a> 一致 */
  text-decoration: underline;
  text-underline-offset: 2px;
}
button.link:hover { color: var(--pass); transform: none; }
button.copy { padding: .3rem .6rem; font-size: .78rem; white-space: nowrap; }
/* 表格行里的按钮。行高是收紧过的，默认尺寸会把每一行顶开。 */
button.small { padding: .22rem .62rem; font-size: .78rem; white-space: nowrap; }

form.inline { display: inline; }
form.block { flex: 1 1 17rem; }
/* 并排的表单块：不要方框（面板里套方框再套输入框太吵），
   只在中间立一条竖线；竖排时换成横线。 */
.row {
  display: flex;
  gap: 0 2rem;
  flex-wrap: wrap;
  margin-top: .6rem;
  align-items: flex-start;
}
.row .block { min-width: 0; }
.row > .block + .block {
  padding-left: 2rem;
  margin-left: -2rem;
  border-left: 1px solid var(--rule);
}
.myip { font-size: .9rem; margin-bottom: .5rem; color: var(--ink-soft); }

/* 面板窄的时候（挤在两栏里、或者手机上），并排改成上下排 */
@container (max-width: 40rem) {
  .row { flex-direction: column; gap: 0; }
  /* flex-basis 在竖排时量的是高度，17rem 会把每块撑出一截空白 */
  .row > .block { width: 100%; flex: 0 0 auto; }
  .row > .block + .block {
    margin: 1rem 0 0;
    padding: 1rem 0 0;
    border-left: 0;
    border-top: 1px solid var(--rule);
  }
}

/* ---- 状态条与标签 ------------------------------------------------------- */
.flash {
  margin-bottom: .6rem;
  padding: .6rem .85rem;
  border-radius: var(--r-sm);
  border-left: 2px solid currentColor;
  font-size: .88rem;
}
.flash.ok { background: var(--pass-wash); color: var(--pass); }
.flash.warn { background: var(--hold-wash); color: var(--hold); }
.flash.error { background: var(--halt-wash); color: var(--halt); }
.flash strong { color: var(--ink); }

.tag {
  display: inline-block;
  padding: .05rem .55rem;
  border-radius: 999px;
  font: 500 11px/1.7 var(--mono);
  letter-spacing: .04em;
  white-space: nowrap;
}
.tag.on { background: var(--pass-wash); color: var(--pass); }
.tag.off { background: var(--halt-wash); color: var(--halt); }
.tag.admin { background: var(--hold-wash); color: var(--hold); }

/* ---- 读数卡片 ------------------------------------------------------------
   概览上的关键数字。做成卡片而不是裸排：一屏里有两组读数，
   有边框才分得清哪几个是一组。 */
/* 7rem 而不是更宽：概览上两组读数分别是 4 个和 5 个，
   卡片再宽一点就会掉下来一个变成孤行，很难看。 */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
  gap: .6rem;
  margin: .2rem 0 .3rem;
}
.stat {
  padding: .8rem .9rem .7rem;
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  background: linear-gradient(var(--panel), color-mix(in oklab, var(--paper) 70%, var(--panel)));
  transition: border-color .14s, transform .14s, box-shadow .14s;
}
.stat:hover {
  border-color: var(--rule-strong);
  transform: translateY(-1px);
  box-shadow: 0 4px 14px -8px rgb(15 25 29 / 18%);
}
.stat b {
  display: block;
  font: 620 1.7rem/1.1 var(--mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -.03em;
}
/* 非数字的读数（"邀请码注册"、"已启用"）用正文字体，
   等宽字排 CJK 又宽又重，跟旁边的数字放一起很别扭。
   字号压到刚好让"邀请码注册"五个字排一行 —— 折行会把这张卡片撑高，
   整排卡片就参差不齐了。 */
.stat.text b {
  font: 620 .98rem/1.75 var(--sans);
  letter-spacing: 0;
  text-wrap: balance;
}
.stat span {
  display: block;
  margin-top: .3rem;
  font-size: .78rem;
  color: var(--ink-dim);
}
/* 读数本身带语义时给左沿一条色带 */
.stat.warn { border-left: 2px solid var(--hold); }
.stat.warn b { color: var(--hold); }

/* 分区内部的分隔线（比如"发验证码"和"填验证码"之间） */
hr.rule {
  height: 0;
  margin: 1.15rem 0;
  border: 0;
  border-top: 1px solid var(--rule);
}

/* 验证码输入框：短、等宽、字距拉开，方便逐位核对。
   选择器必须带上 input —— 上面的 `input:not([type])` 特异度是 (0,1,1)，
   光靠 .code-input (0,1,0) 压不住那条 max-width: var(--field)，
   框子会被拉到 26rem 宽。 */
input.code-input {
  display: block;          /* 不然窄输入框会和 label 文字挤在同一行，跟别的字段不齐 */
  max-width: 9rem;
  font: 600 1.15rem/1.4 var(--mono);
  letter-spacing: .3em;
  text-align: center;
}

/* ---- 表格 ----------------------------------------------------------------
   运维台该密一点：行高收紧，数字等宽对齐，只有细线没有斑马纹。
   第一 / 最后一列贴着分区边缘，不留多余的内缩。 */
table {
  width: 100%;
  border-collapse: collapse;
  margin-top: .5rem;
  font-size: .86rem;
  font-variant-numeric: tabular-nums;
}
th, td {
  text-align: left;
  padding: .42rem .6rem;
  border-bottom: 1px solid var(--rule);
}
th:first-child, td:first-child { padding-left: 0; }
th:last-child, td:last-child { padding-right: 0; }
/* 只有一两列的表（比如待清理的 IP 列表）别把按钮甩到屏幕最右边 */
table.compact { max-width: 26rem; }

/* 一串 IP + 一个按钮：竖着排一列会在宽屏上留出一大片空白，
   改成按可用宽度自动分栏。11 条在 2K 屏上就是四五列，一眼扫完。 */
.iplist {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 0 2.5rem;
  margin: .4rem 0 0;
  padding: 0;
  list-style: none;
}
.iplist li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  padding: .3rem 0;
  border-bottom: 1px solid var(--rule);
  font-size: .86rem;
}
.iplist code { font-variant-numeric: tabular-nums; }
.iplist button { padding: .18rem .55rem; font-size: .78rem; }
th {
  background: var(--panel);
  font: 600 10.5px/1.6 var(--mono);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-dim);
  border-bottom-color: var(--rule-strong);
}
tbody tr { transition: background-color .12s; }
tbody tr:hover { background: color-mix(in oklab, var(--pass) 6%, transparent); }
tr.dimrow { color: var(--ink-dim); }
tr.dimrow code { opacity: .7; }
td code { background: none; padding: 0; color: inherit; }

.linkrow { display: flex; gap: .4rem; align-items: center; margin-top: .4rem; }
.linkbox {
  flex: 1 1 22rem;
  min-width: 0;
  max-width: none;
  margin-top: 0;
  padding: .3rem .5rem;
  border: 1px solid var(--rule);
  border-radius: 5px;
  background: var(--paper);
  color: var(--ink-dim);
  font: .74rem/1.6 var(--mono);
}

/* ---- 账号操作浮层 --------------------------------------------------------
   用户管理的全部操作都在这一层里。用原生 <dialog>：焦点陷阱、Esc 关闭、
   暗底都是浏览器给的，不用自己实现；关闭按钮是 <form method="dialog">，
   连 JS 都不需要。打开走 app.js 的 showModal。

   ⚠️ 关于 display 的两条，都是同一件事的两面 —— 一屏有几十个这样的层，
   一旦它们没被藏起来就会全部当普通内容摊在页面上，这是这套样式里唯一
   "写错就很难看"的地方：
     1. display 只能写在 [open] 上。写在 dialog.sheet 上会盖掉 UA 那条
        dialog:not([open]) { display: none }。
     2. 下面这条把那个 UA 规则自己写一遍，兜住根本不认 <dialog> 的浏览器
        （那种浏览器没有任何 UA 规则可依赖）。 */
dialog:not([open]) { display: none; }

dialog.sheet {
  width: min(52rem, calc(100vw - 2rem));
  max-height: min(88dvh, 50rem);
  padding: 0;
  border: 1px solid var(--rule-strong);
  border-radius: var(--r);
  background: var(--panel);
  color: var(--ink);
  box-shadow: 0 24px 70px -22px rgb(15 25 29 / 45%);
  overflow: hidden;                /* 圆角要盖得住里面的滚动区 */
  animation: sheet-in .18s cubic-bezier(.2, .8, .3, 1);
}
dialog.sheet[open] { display: flex; flex-direction: column; }
dialog.sheet::backdrop {
  background: rgb(8 14 17 / 55%);
  animation: sheet-veil .18s ease;
}
/* 浮层开着的时候别让背后那张长表格还能滚。挂在 :modal 上而不是 [open] 上：
   只有真·模态才该锁滚动，而 :modal 和 showModal 是同一批浏览器支持的 ——
   认不出 :modal 的浏览器会整条丢掉这个规则，正好让它还能滚到降级显示的那一层。 */
body:has(dialog.sheet:modal) { overflow: hidden; }

@keyframes sheet-in { from { opacity: 0; transform: translateY(10px) scale(.99); } }
@keyframes sheet-veil { from { opacity: 0; } }

.sheet-x { position: absolute; top: .5rem; right: .65rem; margin: 0; }
.sheet-x button {
  padding: 0 .35rem;
  border: 0;
  background: none;
  color: var(--ink-dim);
  font-size: 1.4rem;
  line-height: 1.3;
}
.sheet-x button:hover { border-color: transparent; color: var(--halt); transform: none; }

.sheet-head {
  flex: none;
  padding: 1.05rem 2.4rem .85rem 1.3rem;   /* 右边给关闭按钮留位置 */
  border-bottom: 1px solid var(--rule);
  background: var(--paper);
}
.sheet-head h2 { margin: 0; }
.sheet-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .25rem .8rem;
  margin: .45rem 0 0;
  font: .82rem/1.7 var(--sans);
  color: var(--ink-dim);
}
.sheet-meta .warn-text { color: var(--hold); }

/* 内部滚动区。container-type 是必须的：里面并排的 .row 要按这一层自己的
   宽度决定竖不竖排，而页面上的 .row 是拿 .pane-body 当容器的。 */
.sheet-body {
  overflow-y: auto;
  padding: 0 1.3rem 1.2rem;
  container-type: inline-size;
}
.sheet-body > section { padding: 1.05rem 0; border-top: 1px solid var(--rule); }
.sheet-body > section:first-child { border-top: 0; }
.sheet-body h3 { margin: 0; }

/* 并排的字段。这里不用页面上的 .row/.block —— 那套是给"两个独立表单并排、
   中间立一条竖线"设计的，塞成对的输入框进去，两栏会一宽一窄（负 margin 和
   padding 都算在 flex 基准上）。栅格才保证等宽，auto-fit 顺带负责窄屏折行。 */
.sheet-body .fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 0 1.6rem;
}
.sheet-body .fields input { max-width: none; }

/* 一段说明 + 右边一两个按钮。角色、停用、删号都是这个形状：
   要读的东西在左边，能按的东西在右边，不用把一句话拆成三张卡片。 */
.sheet-act { display: flex; align-items: flex-start; gap: 1.5rem; }
.sheet-act > div:first-child { flex: 1 1 auto; min-width: 0; }
.sheet-act .hint { margin-bottom: 0; }
.sheet-do { flex: none; display: flex; flex-direction: column; gap: .5rem; }
.sheet-do form { margin: 0; }
.sheet-do button { width: 100%; }

@container (max-width: 34rem) {
  .sheet-act { flex-direction: column; gap: .5rem; }
  .sheet-do { width: 100%; }
}

/* ---- 浮层提示 ------------------------------------------------------------
   fixed 定位，不参与文档流。退场靠 animation-delay 触发，所以 app.js
   没加载上也会自己消失，不会永久糊在屏幕上。动画名被 app.js 引用，别改。 */
.toasts {
  position: fixed;
  top: .8rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 100;
  display: flex;
  flex-direction: column;
  gap: .5rem;
  width: max-content;
  max-width: min(92vw, 34rem);
  pointer-events: none;
}
.toast {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: .75rem;
  padding: .6rem .85rem;
  border-radius: var(--r-sm);
  border: 1px solid currentColor;
  font-size: .88rem;
  line-height: 1.6;
  box-shadow: var(--shadow);
  animation: toast-in .2s cubic-bezier(.2, .8, .3, 1),
             toast-out .3s ease-in forwards;
  animation-delay: 0s, var(--toast-life, 2.5s);
}
.toast.ok { background: var(--pass-wash); color: var(--pass); }
.toast.warn { background: var(--hold-wash); color: var(--hold); }
.toast.error { background: var(--halt-wash); color: var(--halt); --toast-life: 6s; }
.toast:hover, .toast:focus-within { animation-play-state: paused, paused; }
.toast-x {
  flex: none;
  padding: 0 .1rem;
  border: none;
  background: none;
  color: inherit;
  font-size: 1.1rem;
  line-height: 1.35;
  opacity: .5;
}
.toast-x:hover { opacity: 1; border-color: transparent; transform: none; color: inherit; }

@keyframes toast-in { from { opacity: 0; transform: translateY(-10px); } }
@keyframes toast-out { to { opacity: 0; transform: translateY(-10px); visibility: hidden; } }
@keyframes toast-fade { to { opacity: 0; visibility: hidden; } }

/* 被 app.js 搬进操作层头部的那一条：模态浮层在 top layer 里，留在外面的
   fixed 提示条会被暗底盖掉。搬进来之后改走正常流。 */
.toasts.inline {
  position: static;
  transform: none;
  width: auto;
  max-width: none;
  margin-top: .7rem;
}
/* 而且不再自动消失 —— 它是这层弹出来的原因，用户读完自己关。
   改 --toast-life 而不是覆盖 animation：减弱动效那段是 !important 的，
   直接改 animation 压不住它，改延迟对两条路都有效。 */
.toasts.inline .toast { --toast-life: 100000s; }

/* ---- 跨页转场 ------------------------------------------------------------
   这是个传统的多页应用，每次点击都是整页刷新。跨文档 View Transition
   让顶栏和状态导轨在导航之间保持连续 —— 导轨的颜色会平滑地过渡到
   新页面的状态。不支持的浏览器就是普通跳转，没有降级负担。 */
@view-transition { navigation: auto; }

/* ---- 滚动进场 ------------------------------------------------------------
   原生 scroll-driven animation，零 JS。只给第二块之后的面板用：
   view() 需要一个真的会滚动的文档，页面短到不滚动时进度取不到，
   元素会卡在起始帧上。登录这类窄卡页面只有一两块，绝不参与。 */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    main > .pane:nth-of-type(n+2) {
      animation: rise linear both;
      animation-timeline: view();
      animation-range: entry 5% entry 45%;
    }
    @keyframes rise {
      from { opacity: 0; transform: translateY(14px); }
    }
  }
}

/* ---- 减弱动效 ------------------------------------------------------------
   全部关掉，只留提示条的自动消失（否则它永远不走）。 */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  .toast {
    animation: toast-fade .01ms linear forwards !important;
    animation-delay: var(--toast-life, 2.5s) !important;
  }
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

@media (max-width: 640px) {
  /* 表格的横向滚动由 .pane.data .pane-body 统一处理，这里不要再
     table { display: block } —— 那会让表格退出表格布局，列就对不齐了。 */
  main { padding-bottom: 2.5rem; }
}
