/* 控制面板样式。深色为主，信息密度偏高，适合长期盯着看。 */

:root {
  --bg: #0f1115;
  --panel: #171a21;
  --panel-2: #1e222b;
  --line: #2a2f3a;
  --text: #e6e8ec;
  --muted: #949aa8;
  --accent: #4f8cff;
  --ok: #35c07f;
  --bad: #e5484d;
  --warn: #e0a33e;
  --radius: 10px;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.6 -apple-system, "Segoe UI", "Microsoft YaHei", "PingFang SC", sans-serif;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

code {
  font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
  font-size: 0.9em;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 5px;
  padding: 1px 6px;
}

h1 { font-size: 22px; margin: 0 0 6px; }
h2 { font-size: 17px; margin: 0 0 6px; }
p { margin: 0 0 12px; }

.muted { color: var(--muted); font-size: 14px; }
.hint { color: var(--muted); font-size: 13px; margin-top: 14px; line-height: 1.7; }
.sub { color: var(--muted); font-size: 12px; margin-top: 3px; }

/* --- 布局 --- */

.center-wrap {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

.wrap { max-width: 1080px; margin: 0 auto; padding: 28px 24px 60px; }

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 14px 24px;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
}

.brand {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 600;
  min-width: 0;
}
.brand a { color: var(--text); }
.brand .home { display: flex; align-items: center; gap: 8px; }
.brand .home:hover { text-decoration: none; color: var(--accent); }
/* 显式写死尺寸，图片没加载完也不会让顶栏抖一下 */
.brand .mark { width: 26px; height: 26px; flex: none; }
.sep { color: var(--muted); }

.card {
  position: relative;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 22px;
  margin-bottom: 22px;
}

/* --- 邻近聚光卡片 ---
   光标不必落在卡片上：只要靠近，离光标最近的那段边框就亮起来，颜色
   还会向外溢出一圈光晕，卡片内部从那条边渗进一层同色的柔光。

   实现的关键取巧：渐变圆心 --spot-x/--spot-y 直接用光标相对卡片的
   坐标，允许它是负数或超出卡片宽高。圆心跑到卡片外面时，卡片内可见
   的最亮部分自然就落在离光标最近的那条边上——省掉了"判断最近是哪条
   边"的一整套逻辑。

   JS 只写三个变量：坐标、强度（--spot-power，按距离衰减）和色相
   （--spot-hue）。渐变绘制全交给 CSS，每帧开销落在合成器上。 */

@media (hover: hover) {
  .card {
    --spot-x: -400px;
    --spot-y: -400px;
    --spot-power: 0;    /* 0 到 1，距离越近越大 */
    --spot-hue: 275;
  }

  .card::before, .card::after {
    content: "";
    position: absolute;
    border-radius: inherit;
    pointer-events: none;
    opacity: var(--spot-power);
  }

  /* 描边亮线：整块渐变用 mask 掏空中间，只剩边框那一圈。
     这里不能用 drop-shadow 去做外溢光晕——CSS 的渲染顺序是 filter
     先于 mask，阴影基于未遮罩的整块渐变算出，随后又被 mask 连同
     边框以外的部分一起剪掉，等于白做。外溢交给 ::after。 */
  .card::before {
    z-index: 1;          /* 亮线要压在光晕之上，否则会被那层柔光糊掉 */
    inset: -1px;
    border-radius: calc(var(--radius) + 1px);
    padding: 1.5px;
    background: radial-gradient(
        200px circle at var(--spot-x) var(--spot-y),
        hsl(var(--spot-hue) 100% 96%) 0%,
        hsl(var(--spot-hue) 96% 74%) 13%,
        hsl(var(--spot-hue) 92% 58% / .5) 34%,
        transparent 60%);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
    mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    mask-composite: exclude;
  }

  /* 光晕：整层向外扩张 --spot-bleed，一层同时承担两件事——
     溢出卡片外的那团辉光，和渗进卡片内部的柔光。因为圆心在卡片外，
     最亮的部分自然落在卡片外，卡片内只吃到衰减尾巴，浓淡关系就对了。
     盒子外扩后坐标原点跟着移，所以圆心要补回 --spot-bleed。

     外扩量必须大于「感应半径 + 光晕可见半径」，否则光还没衰减完就被
     自己的盒子裁断，会露出一个硬边矩形。JS 里 RANGE=120，这里渐变
     130px 见底，220 的外扩留足了余量。 */
  .card::after {
    --spot-bleed: 220px;
    inset: calc(var(--spot-bleed) * -1);
    border-radius: calc(var(--radius) + var(--spot-bleed));
    background: radial-gradient(
        130px circle at calc(var(--spot-x) + var(--spot-bleed))
                        calc(var(--spot-y) + var(--spot-bleed)),
        hsl(var(--spot-hue) 95% 66% / .38) 0%,
        hsl(var(--spot-hue) 90% 60% / .18) 28%,
        hsl(var(--spot-hue) 85% 55% / .05) 55%,
        transparent 100%);
  }

  /* 伪元素是定位元素，默认盖在内容之上；而卡片背景不透明，把光效
     放到 z-index:-1 又会被背景挡死，所以只能把内容抬上来 */
  .card > * { position: relative; z-index: 2; }
}

/* --- 登录 / 封禁 --- */

.auth { width: 100%; max-width: 380px; margin: 0; text-align: center; }
.auth .logo { width: 72px; height: 72px; display: block; margin: 6px auto 10px; }
.auth input[type="password"] { margin-top: 14px; }
.auth button { width: 100%; margin-top: 12px; }
.banned .big { font-size: 40px; margin-bottom: 8px; }
.banned code { display: inline-block; }

/* --- 功能卡片 --- */

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 18px;
  margin-top: 20px;
}

.feature {
  display: block;
  color: var(--text);
  margin: 0;
  transition: border-color .15s, transform .15s;
}
.feature:hover { border-color: var(--accent); text-decoration: none; transform: translateY(-2px); }
.feature .icon { font-size: 26px; margin-bottom: 6px; }
.feature p { color: var(--muted); font-size: 14px; }
.feature .go { color: var(--accent); font-size: 14px; }
.feature.disabled { opacity: .5; pointer-events: none; }

/* --- 表单 --- */

input, select, button, .btn {
  font: inherit;
  border-radius: 7px;
  border: 1px solid var(--line);
  padding: 9px 12px;
}

input, select {
  width: 100%;
  background: var(--bg);
  color: var(--text);
}
input:focus, select:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
input::placeholder { color: #5a6070; }

/* --- 玻璃按钮 ---
   静止态是一块半透明玻璃：背后模糊、上缘一道亮边、下缘一道暗边，
   靠这三层做出厚度。悬停时一束光从中心向两端铺开，同时中心有一道
   竖光柱溢出上下边缘再淡出，就是效果图里那种"被点亮"的感觉。

   光带用 clip-path 而不是 overflow:hidden 来裁切——后者会连带把
   要溢出边缘的光柱一起剪掉，那道溢出恰恰是效果图最出彩的地方。 */

button, .btn {
  position: relative;
  isolation: isolate;          /* 伪元素的层级只在按钮内部排 */
  color: #fff;
  cursor: pointer;
  white-space: nowrap;
  border-color: rgba(255, 255, 255, .16);
  /* 白色高光叠一层极淡的主色，既是玻璃又还能看出这是主按钮 */
  background:
    linear-gradient(rgba(255, 255, 255, .10), rgba(255, 255, 255, .03)),
    rgba(79, 140, 255, .10);
  backdrop-filter: blur(12px) saturate(160%);
  -webkit-backdrop-filter: blur(12px) saturate(160%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .22),
    inset 0 -1px 0 rgba(0, 0, 0, .28),
    0 2px 8px rgba(0, 0, 0, .28);
  transition: box-shadow .35s ease, border-color .35s ease, color .25s ease;
  /* 光色。danger 会把这两个变量换成红色 */
  --glow: rgba(79, 140, 255, .55);
  --glow-hot: rgba(150, 190, 255, .95);
}

/* 从中心向两端铺开的光带 */
button::before, .btn::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;                 /* 压在按钮背景之上、文字之下 */
  border-radius: inherit;
  clip-path: inset(0 round 7px);
  background: linear-gradient(90deg,
      transparent 0%, var(--glow) 22%, var(--glow-hot) 50%,
      var(--glow) 78%, transparent 100%);
  opacity: 0;
  transform: scaleX(.08);      /* 起点只是中间一道细缝 */
  transition: transform .5s cubic-bezier(.22, .68, .28, 1), opacity .3s ease;
}

/* 中心那道溢出上下边缘的光柱 */
button::after, .btn::after {
  content: "";
  position: absolute;
  z-index: -1;
  left: 50%;
  top: -7px;
  bottom: -7px;
  width: 22px;
  margin-left: -11px;
  border-radius: 4px;
  background: var(--glow-hot);
  filter: blur(5px);
  opacity: 0;
}

/* :focus-visible 一并触发——只认 hover 的话，键盘操作的人得不到任何反馈 */
button:hover, .btn:hover,
button:focus-visible, .btn:focus-visible {
  text-decoration: none;
  color: #fff;
  border-color: rgba(255, 255, 255, .34);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .34),
    inset 0 -1px 0 rgba(0, 0, 0, .2),
    0 4px 18px -4px var(--glow),
    0 0 34px -10px var(--glow-hot);
}

button:hover::before, .btn:hover::before,
button:focus-visible::before, .btn:focus-visible::before {
  transform: scaleX(1);
  opacity: 1;
}

button:hover::after, .btn:hover::after,
button:focus-visible::after, .btn:focus-visible::after {
  animation: btn-bleed .62s ease-out forwards;
}

/* 光柱先亮起、再随光带铺开而淡出，避免它一直杵在中间 */
@keyframes btn-bleed {
  0%   { opacity: 0;   transform: scaleY(.55); }
  22%  { opacity: .95; transform: scaleY(1); }
  100% { opacity: 0;   transform: scaleY(1.15); }
}

button:active, .btn:active { transform: translateY(1px); }

/* 禁用态。:hover 在 disabled 按钮上照样匹配，不把光效关掉的话，
   一个点不动的按钮还会亮起来，那是在骗人 */
button:disabled {
  opacity: .4;
  cursor: not-allowed;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12);
}
button:disabled::before, button:disabled::after { display: none; }
button:disabled:hover { border-color: rgba(255, 255, 255, .16); }

/* 次要按钮：同样的玻璃和光效，只是更收敛，不跟主按钮抢视线 */
button.ghost, .ghost.btn {
  color: var(--text);
  padding: 6px 11px;
  font-size: 13px;
  background: rgba(255, 255, 255, .04);
  border-color: rgba(255, 255, 255, .12);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .12),
    0 1px 4px rgba(0, 0, 0, .22);
  --glow: rgba(79, 140, 255, .3);
  --glow-hot: rgba(150, 190, 255, .6);
}
button.ghost::before, .ghost.btn::before { clip-path: inset(0 round 7px); }
button.ghost::after, .ghost.btn::after { top: -5px; bottom: -5px; width: 16px; margin-left: -8px; }

/* 删除是破坏性操作，光色换成红的，和效果图左边那颗一个意思 */
button.danger {
  --glow: rgba(229, 72, 77, .45);
  --glow-hot: rgba(255, 120, 125, .9);
}

/* 用户要求减少动效时，保留颜色与光晕，去掉铺开和闪动 */
@media (prefers-reduced-motion: reduce) {
  button::before, .btn::before {
    transition: opacity .2s ease;
    transform: scaleX(1);
  }
  button::after, .btn::after { display: none; }
  button:active, .btn:active { transform: none; }
}

/* 固定列宽的 grid，而不是 flex 换行：
   字段数量是定死的，用 grid 才能保证目标端口和按钮不被挤到第二行。
   min-width:0 是关键，否则输入框的固有宽度会撑破列宽。 */
.rule-form {
  display: grid;
  grid-template-columns:
    minmax(0, 1.4fr)   /* 规则名称 */
    84px               /* 协议 */
    minmax(0, 1.2fr)   /* 监听地址 */
    92px               /* 监听端口 */
    auto               /* 箭头 */
    minmax(0, 1.4fr)   /* 目标地址 */
    92px               /* 目标端口 */
    auto;              /* 提交按钮 */
  align-items: end;
  gap: 10px;
  margin-top: 16px;
}
.rule-form .field { min-width: 0; }
.rule-form label {
  display: block;
  font-size: 12px;
  color: var(--muted);
  margin-bottom: 5px;
  white-space: nowrap;
}
.rule-form input, .rule-form select { padding: 9px 10px; }
.rule-form .arrow { color: var(--muted); padding-bottom: 10px; }
.rule-form > button, .rule-form > .btn { padding: 9px 14px; }

/* 编辑行里还多一个"取消"，让它跟在按钮后面而不是另起一行 */
.edit-row .rule-form { grid-template-columns:
    minmax(0, 1.4fr) 84px minmax(0, 1.2fr) 92px auto minmax(0, 1.3fr) 92px auto auto; }

/* 窄屏放不下八列，折成两行仍比挤成一列好读 */
@media (max-width: 1000px) {
  .rule-form, .edit-row .rule-form {
    grid-template-columns: minmax(0, 1fr) 84px minmax(0, 1fr) 92px;
  }
  .rule-form .arrow { display: none; }
  .rule-form > button, .rule-form > .btn { grid-column: span 2; }
}

/* --- 提示条 --- */

.alert {
  border-radius: 7px;
  padding: 11px 14px;
  margin: 14px 0;
  font-size: 14px;
  border: 1px solid;
  text-align: left;
}
.alert.ok { background: rgba(53, 192, 127, .1); border-color: rgba(53, 192, 127, .4); }
.alert.error { background: rgba(229, 72, 77, .1); border-color: rgba(229, 72, 77, .45); }

/* --- 表格 --- */

.table-scroll { overflow-x: auto; margin-top: 14px; }
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th, td { text-align: left; padding: 11px 10px; border-bottom: 1px solid var(--line); }
th { color: var(--muted); font-weight: 500; font-size: 12px; text-transform: uppercase; }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
tbody tr:hover { background: var(--panel-2); }
.edit-row:hover, .edit-row { background: var(--panel-2); }

.path code { white-space: nowrap; }
.path .to { color: var(--muted); margin: 0 6px; }
.err-line { color: var(--bad); font-size: 12px; margin-top: 4px; }

.badge {
  display: inline-block;
  font-size: 12px;
  padding: 2px 9px;
  border-radius: 999px;
  border: 1px solid;
  white-space: nowrap;
}
.badge.on { color: var(--ok); border-color: rgba(53, 192, 127, .5); background: rgba(53, 192, 127, .1); }
.badge.off { color: var(--muted); border-color: var(--line); }
.badge.bad { color: var(--bad); border-color: rgba(229, 72, 77, .5); background: rgba(229, 72, 77, .1); }
.live { display: block; font-size: 11px; color: var(--warn); margin-top: 3px; }

.actions { display: flex; gap: 6px; align-items: center; }
.actions form { display: inline; }

.empty { padding: 18px 0; text-align: center; }

@media (max-width: 620px) {
  .rule-form, .edit-row .rule-form { grid-template-columns: minmax(0, 1fr); }
  .rule-form > button, .rule-form > .btn { grid-column: auto; }
  .wrap { padding: 20px 14px 40px; }
}

/* 协议标签：TCP / UDP 一眼分得开 */
.proto {
  display: inline-block;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .04em;
  padding: 1px 6px;
  border-radius: 4px;
  margin-right: 6px;
  border: 1px solid;
}
.proto.tcp { color: #6ea8fe; border-color: rgba(110, 168, 254, .45); background: rgba(110, 168, 254, .1); }
.proto.udp { color: #c08ce0; border-color: rgba(192, 140, 224, .45); background: rgba(192, 140, 224, .1); }

/* 中性提示：会话超时之类的通知，不是错误也不是成功 */
.alert.info { background: rgba(79, 140, 255, .1); border-color: rgba(79, 140, 255, .4); }

/* --- 串口助手 --- */

/* 工具类，职责就是压过元素自己的 display。必须带 !important：
   .hidden 和 .viewer-hint 都是单个类选择器，特异性相同，同分时后写的赢，
   而 .viewer-hint 排在后面——不加的话加了 hidden 类元素照样显示。 */
.hidden { display: none !important; }

ul.plain, ol.plain { margin: 10px 0 0; padding-left: 20px; }
ul.plain li, ol.plain li { margin-bottom: 6px; }

textarea {
  width: 100%;
  font: 13px/1.6 "SFMono-Regular", Consolas, "Liberation Mono", monospace;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 7px;
  padding: 10px 12px;
  resize: vertical;
}
textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; }

/* 连接参数：波特率给宽些，其余几个枚举项都很短 */
.serial-conn {
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) repeat(4, minmax(0, 1fr)) auto;
  align-items: end;
  gap: 10px;
  margin-top: 16px;
}
.serial-conn .field { min-width: 0; }
.serial-conn label {
  display: block;
  font-size: 12px;
  color: var(--muted);
  margin-bottom: 5px;
  white-space: nowrap;
}

.serial-status {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 16px;
  font-size: 14px;
}
.serial-status .counters { color: var(--muted); margin-left: auto; font-size: 13px; }
.serial-status .counters b { color: var(--text); font-variant-numeric: tabular-nums; }

/* 连接状态灯：灰=断开，绿=已连，红=出错 */
.dot {
  width: 9px; height: 9px;
  border-radius: 50%;
  background: var(--muted);
  flex: none;
}
.dot.on  { background: var(--ok);  box-shadow: 0 0 8px var(--ok); }
.dot.err { background: var(--bad); box-shadow: 0 0 8px var(--bad); }

.term-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
}
.term-tools { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }

.chk {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--muted);
  cursor: pointer;
  white-space: nowrap;
}
.chk input { width: auto; margin: 0; cursor: pointer; }

/* 接收区。字号压到 12.5px 是为了一屏能多看几行报文 */
.term {
  margin-top: 14px;
  height: 340px;
  overflow-y: auto;
  padding: 10px 12px;
  background: #0b0d11;
  border: 1px solid var(--line);
  border-radius: 8px;
  font: 12.5px/1.55 "SFMono-Regular", Consolas, "Liberation Mono", monospace;
  white-space: pre-wrap;
  word-break: break-all;
}
.term .line { display: block; }
.term .ts { color: #5a6070; margin-right: 8px; }
.term .rx { color: #8fd7a8; }
.term .tx { color: #7fb0ff; }
.term .sys { color: var(--warn); }
.term .er { color: var(--bad); }

/* 发送区一行五列：勾选框 / 结束符 / 勾选框 / 间隔 / 发送按钮。
   两个带标签的字段给固定宽度，正好容下 "\r\n" 和四位毫秒数——
   框比内容宽出一大截会显得和标签对不上。 */
.serial-send {
  display: grid;
  grid-template-columns: auto 110px auto 110px 1fr;
  align-items: end;
  gap: 8px 16px;
  margin-top: 14px;
}
.serial-send .field { min-width: 0; }
.serial-send label:not(.chk) {
  display: block;
  font-size: 12px;
  color: var(--muted);
  margin-bottom: 5px;
  white-space: nowrap;
  text-align: center;   /* 标签压在自己那个框的正上方，才看得出是一组 */
}
/* 勾选框没有上方标签，只按 align-items:end 会贴在行底、比输入框里的
   文字低一截。撑到和输入框同高再垂直居中，两边的文字就齐平了。 */
.serial-send .chk { min-height: 44px; }
.serial-send > button { justify-self: end; }

@media (max-width: 900px) {
  .serial-conn { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .serial-conn > button { grid-column: span 2; }
  .serial-send { grid-template-columns: auto 1fr; }
  .serial-send > button { grid-column: span 2; justify-self: stretch; }
}

/* --- 可输入的下拉框 ---
   既能直接打字，也能点箭头选预设。没用 datalist：它的下拉提示在各浏览器
   里的外观和触发方式都不统一，Edge 里那个箭头几乎看不见。 */

.combo { position: relative; }
.combo input { padding-right: 30px; cursor: pointer; }

/* 画一个和原生 select 一样的细线雪佛龙。之前用的 ▾ 是实心三角，
   和旁边几个 select 的箭头明显不是一路货，一眼就看得出这个框是"假的"。
   用 SVG 而不是字符，才能把线宽和圆角控制得和原生一致。 */
.combo-arrow {
  position: absolute;
  right: 10px;
  top: 50%;
  width: 14px;
  height: 14px;
  transform: translateY(-50%);
  /* 让点击穿透到输入框：点箭头和点框是同一件事，省掉一个单独的按钮 */
  pointer-events: none;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M3.2 5.4 L7 9.2 L10.8 5.4' fill='none' stroke='%23949aa8' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat center / 14px 14px;
}

.combo-list {
  position: absolute;
  z-index: 20;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  margin: 0;
  padding: 4px;
  max-height: 232px;
  overflow-y: auto;
  list-style: none;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 7px;
  box-shadow: 0 10px 26px rgba(0, 0, 0, .5);
}
.combo-list li {
  padding: 6px 10px;
  border-radius: 5px;
  cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.combo-list li:hover, .combo-list li.on { background: var(--accent); color: #fff; }

/* 连接行要压过下面的状态行，否则展开的列表会被状态行盖住——
   .card > * 给所有直接子元素都设了 z-index:2，同级按文档顺序后来居上 */
.serial-conn { position: relative; z-index: 3; }

/* --- 模型查看器 --- */

/* 工具栏里的下拉不能吃掉整行宽度（全局给 select 设了 width:100%） */
.term-tools select { width: auto; min-width: 150px; }

.viewer {
  position: relative;
  height: 460px;
  margin-top: 14px;
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
  /* 深色渐变底，比纯色更能衬出模型的轮廓 */
  background: radial-gradient(120% 90% at 50% 0%, #1c2130 0%, #0b0d11 70%);
  /* 拖动旋转时别把画布上的内容选中 */
  user-select: none;
  touch-action: none;
}
.viewer canvas { display: block; width: 100%; height: 100%; }

.viewer-hint {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
  font-size: 14px;
  pointer-events: none;
}

.viewer-info {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
  margin-top: 10px;
  font-size: 13px;
  color: var(--muted);
}
.viewer-info .counters b { color: var(--text); font-variant-numeric: tabular-nums; }

@media (max-width: 620px) {
  .viewer { height: 320px; }
}

/* --- 登录页（着陆页）---
   配了模型就走左右分栏：左边模型是主角，右边收一个窄的登录卡片。
   没配模型时模板用回原来的 .center-wrap，页面还是一张居中的卡。 */

.landing {
  min-height: 100vh;
  max-width: 1120px;
  margin: 0 auto;
  padding: 32px 24px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 360px;
  align-items: center;
  gap: 26px;
}

/* 模型区不套卡片：画布本身用 alpha:true 创建是透明的，把 .viewer 的
   边框和渐变底也去掉，页面底色就直接透上来，模型看着是浮在页面上。
   只有模型区这样——登录表单仍是 .card，边框背景和聚光描边都保留。 */
.showcase { margin: 0; text-align: center; }   /* 让胶囊按钮居中 */
.showcase .viewer {
  height: 460px;
  margin: 0;
  border: 0;
  border-radius: 0;
  background: none;
  overflow: visible;
}

/* 下载按钮：贴合文字的胶囊，不是整宽的长条。模型是无框的，底下压一块
   整宽玻璃板会像多出来的 UI；收成一颗小胶囊反而更像"模型的附注"。 */
.btn.download {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 22px;
  padding: 9px 20px;
  border-radius: 999px;
  font-size: 14px;
}
/* 玻璃按钮的光带裁切写死了 7px 圆角，胶囊得跟着改，
   否则悬停时光带的直角会从圆边里透出来 */
.btn.download::before { clip-path: inset(0 round 999px); }

/* 分栏时登录卡不再需要自己居中，宽度交给栅格 */
.landing .auth { max-width: none; }

@media (max-width: 900px) {
  .landing {
    grid-template-columns: minmax(0, 1fr);
    min-height: 0;
    padding: 24px 16px 40px;
  }
  .showcase .viewer { height: 340px; }
  .landing .auth { max-width: 420px; justify-self: center; width: 100%; }
}

/* 上传：文件选择框和按钮并排 */
.upload {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin: 16px 0 6px;
}
.upload input[type="file"] {
  flex: 1 1 260px;
  min-width: 0;
  padding: 8px 10px;
  font-size: 13px;
  cursor: pointer;
}
.upload input[type="file"]::file-selector-button {
  margin-right: 10px;
  padding: 5px 12px;
  font: inherit;
  font-size: 13px;
  color: var(--text);
  background: rgba(255, 255, 255, .06);
  border: 1px solid var(--line);
  border-radius: 6px;
  cursor: pointer;
}
.upload input[type="file"]::file-selector-button:hover { border-color: var(--accent); }

/* --- 登录页模型轮播的左右箭头 ---
   浮在画布左右边缘。往里缩 10px 是为了给拖动旋转留出边角——
   贴边放的话，从边缘起手拖模型很容易先点到箭头。 */

.viewer .nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  width: 40px;
  height: 40px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  color: var(--text);
  background: rgba(255, 255, 255, .05);
  border-color: rgba(255, 255, 255, .14);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .14);
  opacity: .55;
  transition: opacity .2s ease, border-color .25s ease, background .25s ease;
}
.viewer .nav:hover:not(:disabled) {
  opacity: 1;
  background: rgba(255, 255, 255, .1);
  border-color: rgba(255, 255, 255, .3);
}
.viewer .nav:disabled { opacity: .2; }
/* 圆形按钮要盖掉玻璃按钮那套方角光效，否则悬停时直角会露出来 */
.viewer .nav::before, .viewer .nav::after { display: none; }
.viewer .nav.prev { left: 10px; }
.viewer .nav.next { right: 10px; }

/* --- 设置页的条目编辑行 --- */

.sub-head { margin-top: 26px; }

.item-form {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr) minmax(0, 1.6fr) auto;
  align-items: center;
  gap: 8px;
}
.item-form select, .item-form input { padding: 7px 10px; font-size: 13px; }
.item-form > button { padding: 7px 14px; font-size: 13px; }

@media (max-width: 900px) {
  .item-form { grid-template-columns: minmax(0, 1fr); }
}

/* 某一项没配链接时藏起按钮。用 visibility 而不是 .hidden 的 display:none：
   display:none 会把按钮那块高度一并收走，而 .landing 是垂直居中的——
   切到无链接的模型时整列变矮，模型就跟着上下跳一下。visibility 保留
   占位，切换时画面纹丝不动。
   pointer-events 一并关掉，免得留一块看不见却能点的区域。 */
.btn.download.no-link {
  visibility: hidden;
  pointer-events: none;
}
