/* ============================================================================
   SSL Manager 控制台
   ----------------------------------------------------------------------------
   配线面板：每个受管站点是面板上的一个插口，有自己的状态灯、跳线、封签。

   暗场是场景决定的，不是品类决定的：这台控制台被打开的时刻，通常是管理员
   被到期告警叫醒的凌晨，屏幕是房间里唯一的光源。所以地面压到近黑，而面板
   材质、发丝线、连续曲率圆角、弹簧动效这些苹果语汇全部保留——只是从浅色
   半透明翻译成暗场活力。

   颜色被严格限制：除五种状态灯外没有别的彩色。这不是洁癖，是因为在这块
   面板上「颜色」本身就是信息——一旦装饰也用彩色，灯就不再一眼可读了。
   ========================================================================= */

/* --- 令牌 ---------------------------------------------------------------- */

:root {
  /* 材质层级。三层足够：地面、面板、抬升面。 */
  --ground: #0a0a0b;
  --panel: #1c1c1e;
  --raised: #2c2c2e;
  --sunk: #131315;

  --hairline: rgba(255, 255, 255, 0.08);
  --hairline-strong: rgba(255, 255, 255, 0.14);

  /* 文字。在 --ground 上：fg-2 约 7.4:1，fg-3 约 5.4:1。
     两个都过 4.5:1——这块面板上的「次要文字」是签发者、剩余天数、
     上游端口这类要被人读进去的内容，不是装饰，所以它不能掉到
     对比度下限以下。真正纯装饰的场合在这套界面里不存在。 */
  --fg: #f5f5f7;
  --fg-2: rgba(235, 235, 245, 0.66);
  --fg-3: rgba(235, 235, 245, 0.55);

  /* 五种灯，没有第六种。 */
  --online: #30d158;
  --renewing: #ff9f0a;
  --broken: #ff453a;
  --busy: #0a84ff;
  --off: #8e8e93;

  --accent: var(--busy);
  --accent-ink: #ffffff;

  /* 暗场活力材质：苹果的活力材质默认是浅色半透明，这里翻到暗面。 */
  --glass: rgba(28, 28, 30, 0.72);
  --glass-filter: saturate(180%) blur(20px);

  --r-sm: 6px;
  --r: 10px;
  --r-lg: 14px;

  /* 弹簧收敛、不反弹。整个界面只用一个缓动，让动效有统一的手感。 */
  --spring: cubic-bezier(0.22, 1, 0.36, 1);
  --swift: cubic-bezier(0.4, 0, 0.2, 1);

  --bar: 52px;

  --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC",
    "Hiragino Sans GB", "Noto Sans SC", "Microsoft YaHei", system-ui, sans-serif;
  /* 等宽只用于真正是代码的东西：Nginx 配置文本、任务日志行。
     它是功能，不是「技术感」的戏服。 */
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
    "Liberation Mono", monospace;
}

/* --- 基底 ---------------------------------------------------------------- */

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

html,
body {
  height: 100%;
}

body {
  margin: 0;
  background: var(--ground);
  color: var(--fg);
  font-family: var(--font);
  font-size: 14px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow: hidden;
}

/* 浏览器自带的那些表面也属于这套设计。它们最容易被漏掉，
   也最能说明一个页面是被「建造」的还是被「拼装」的。 */
::selection {
  background: rgba(10, 132, 255, 0.34);
  color: var(--fg);
}

body {
  caret-color: var(--accent);
}

* {
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.16) transparent;
}

::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.14);
  border: 3px solid transparent;
  background-clip: content-box;
  border-radius: 999px;
}

::-webkit-scrollbar-thumb:hover {
  background: rgba(255, 255, 255, 0.24);
  background-clip: content-box;
}

::-webkit-scrollbar-corner {
  background: transparent;
}

:focus {
  outline: none;
}

/* 这里不设 border-radius。
   以前写的是 4px，本意是「让描边贴着圆角」，但 outline 本来就会跟着
   元素自身的圆角走；而这行写下去等于给元素**自己**改了圆角——一个 8px
   的输入框一聚焦就变成 4px 的方角，松手又弹回来。描边贴着控件靠的是
   不干预，不是覆盖。 */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

a {
  color: var(--accent);
  text-decoration: none;
  text-underline-offset: 3px;
}

a:hover {
  text-decoration: underline;
}

button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}

/* 所有数字一律等宽对齐。到期天数、配额余量、耗时、端口——
   这些是要被比较的量，字形跳动会让人读错。 */
.num,
.tnum {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

.mono {
  font-family: var(--mono);
  font-variant-ligatures: none;
}

.sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* --- 应用骨架 ------------------------------------------------------------ */

.app {
  display: grid;
  /* 列必须是 minmax(0, 1fr)，不能留成隐式的 auto。
     隐式 auto 列的最小尺寸取自内容的 min-content，而顶栏的 min-content
     是它全部导航项排成一行的宽度（390px 屏上是 524px）。于是栅格被撑到
     524px，body 的 overflow: hidden 又把多出来的部分裁掉——右侧的
     管理员按钮和「到期」列不是被挤走，是根本够不着。
     minmax(0, …) 把轨道的最小尺寸钉成 0，顶栏才肯回到 390px，
     导航条也才会去滚动。 */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: var(--bar) 1fr;
  height: 100vh;
  height: 100dvh;
}

/* 顶栏是仅有的常驻玻璃。它下方确实有内容滚过——切换视图时主区在动，
   顶栏钉在原地，这层毛玻璃让「上面还有一层」被感觉到，而不是画出来。 */
.topbar {
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 0 16px 0 18px;
  background: var(--glass);
  backdrop-filter: var(--glass-filter);
  -webkit-backdrop-filter: var(--glass-filter);
  border-bottom: 1px solid var(--hairline);
  z-index: 30;
}

.mark {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 13px;
  font-weight: 590;
  letter-spacing: -0.01em;
  white-space: nowrap;
}

.mark svg {
  width: 19px;
  height: 19px;
  color: var(--online);
  flex: none;
}

.mark .mark-sub {
  color: var(--fg-3);
  font-weight: 400;
  letter-spacing: 0;
}

.nav {
  display: flex;
  gap: 2px;
  flex: 1;
  min-width: 0;
}

.nav a {
  padding: 6px 12px;
  border-radius: 8px;
  color: var(--fg-2);
  font-size: 13px;
  font-weight: 500;
  text-decoration: none;
  white-space: nowrap;
  transition: background 0.16s var(--swift), color 0.16s var(--swift);
}

.nav a:hover {
  background: rgba(255, 255, 255, 0.06);
  color: var(--fg);
}

.nav a[aria-current="page"] {
  background: rgba(255, 255, 255, 0.1);
  color: var(--fg);
}

.topbar-end {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: none;
}

/* 会话剩余时间：管理员最该知道、却最容易忘的一件事。 */
.session-clock {
  color: var(--fg-3);
  font-size: 12px;
  white-space: nowrap;
}

.session-clock.is-expiring {
  color: var(--renewing);
}

/* --- 主体 ---------------------------------------------------------------- */

.body {
  display: grid;
  /* 单列主区。曾经这里是「280px 插口长列 + 主区」的两列——长列与配线
     面板是同一份信息的两次渲染，删掉之后布局只剩一种职责：装内容。 */
  grid-template-columns: minmax(0, 1fr);
  min-height: 0;
}

/* --- 主面板 -------------------------------------------------------------- */

/* 不再用 grid-template-rows: auto 1fr：仪表条从外壳搬进看板视图之后，
   .main 只剩一个孩子。残留的 auto 行会把 .view 压进内容高度里，
   让它失去滚动——flex 布局没有这个坑。 */
.main {
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.main .view {
  flex: 1;
  min-height: 0;
}

/* --- 面板工具栏 ---------------------------------------------------------- */

/* 面板自己的工具行：站点页的筛选框与「新建站点」、证书页的
   「签发证书」、体检页的过滤开关都住在这里。主操作回到它服务的
   面板旁边，而不是挂在一个位置固定、经常用不上的全局按钮上。 */
.panel-toolbar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 22px 12px;
}

.panel-toolbar .spring {
  flex: 1;
}

.filter-input {
  max-width: 360px;
}

/* 工具栏计数器：元素常驻（在重绘区外），draw() 只改文字。
   它回答「这里有多少东西、筛选筛走了多少」——不占一行版面。 */
.toolbar-count {
  color: var(--fg-3);
  font-size: 12px;
  white-space: nowrap;
}

/* 详情弹窗头部的状态行：灯徽标 + 「接在哪里」的一句话。 */
.dlg-status {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 14px;
}

.dlg-status-sub {
  color: var(--fg-2);
  font-size: 12.5px;
}

/* --- 仪器条 -------------------------------------------------------------- */

/* 四项横排、发丝线分隔。刻意不做成大数字卡片：卡片会把四个量拆成四个
   各自为政的「成绩」，而这里需要的是把它们并排读成一台仪器的四个表盘。

   它只出现在看板上，作为视图内容的一部分，不再是外壳的常驻条——
   在配置页和记录页上，这四块表盘与手头的事无关，占着一行只会在
   每次滚动时提醒人它在那儿。 */
.instruments {
  display: flex;
  align-items: stretch;
  gap: 0;
  margin: 0 22px 16px;
  padding: 4px 16px;
  border: 1px solid var(--hairline);
  border-radius: var(--r);
  background: var(--panel);
  min-height: 58px;
}

.gauge {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  padding: 10px 18px;
  min-width: 0;
  /* 上限是给「最快到期」那格留的：它的副标题带一个域名，
     没有上限时会把右边的配额表盘挤到贴边。 */
  max-width: 320px;
  border-right: 1px solid var(--hairline);
}

.gauge:first-child {
  padding-left: 2px;
}

.gauge-label {
  font-size: 11px;
  color: var(--fg-3);
  letter-spacing: 0.02em;
  white-space: nowrap;
}

.gauge-value {
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* 数值本身永远完整显示，可以让步的是它旁边那句说明。 */
.gauge-value > span:first-child {
  flex: none;
}

.gauge-value .unit {
  font-size: 12px;
  color: var(--fg-3);
  font-weight: 400;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.gauge.is-warn .gauge-value {
  color: var(--renewing);
}

.gauge.is-bad .gauge-value {
  color: var(--broken);
}

/* 配额余量是一条真正的量尺，不是进度环。 */
.gauge-bar {
  width: 72px;
  height: 3px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.12);
  overflow: hidden;
  flex: none;
}

.gauge-bar i {
  display: block;
  height: 100%;
  background: var(--online);
  border-radius: 2px;
  transition: width 0.5s var(--spring), background 0.3s var(--swift);
}

.gauge-bar.is-warn i {
  background: var(--renewing);
}

.gauge-bar.is-bad i {
  background: var(--broken);
}

/* --- 按钮 ---------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: 30px;
  padding: 0 12px;
  border: 1px solid var(--hairline-strong);
  border-radius: 8px;
  background: var(--raised);
  color: var(--fg);
  font-size: 12.5px;
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
  transition: background 0.14s var(--swift), border-color 0.14s var(--swift),
    transform 0.1s var(--swift), opacity 0.14s var(--swift);
}

.btn:hover:not(:disabled) {
  background: #3a3a3c;
}

.btn:active:not(:disabled) {
  transform: scale(0.97);
}

.btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.btn svg {
  width: 14px;
  height: 14px;
  flex: none;
}

.btn-primary {
  background: var(--accent);
  border-color: transparent;
  color: var(--accent-ink);
}

.btn-primary:hover:not(:disabled) {
  background: #2f95ff;
}

.btn-danger {
  background: transparent;
  border-color: rgba(255, 69, 58, 0.4);
  color: var(--broken);
}

.btn-danger:hover:not(:disabled) {
  background: rgba(255, 69, 58, 0.14);
}

.btn-quiet {
  background: transparent;
  border-color: transparent;
  color: var(--fg-2);
}

.btn-quiet:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.08);
  color: var(--fg);
}

.btn-sm {
  height: 26px;
  padding: 0 9px;
  font-size: 12px;
}

.btn-icon {
  width: 30px;
  padding: 0;
}

/* --- 视图容器 ------------------------------------------------------------ */

.view {
  overflow-y: auto;
  overscroll-behavior: contain;
  min-height: 0;
  padding: 0 0 64px;
}

.view-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 20px;
  padding: 22px 22px 14px;
}

/* 标题自己承担重量。它上方没有小标签——那种「眉题」是纯装饰，
   删掉之后标题反而更响。 */
.view-head h1 {
  margin: 0;
  font-size: 21px;
  font-weight: 590;
  letter-spacing: -0.022em;
}

.view-head p {
  margin: 4px 0 0;
  color: var(--fg-2);
  font-size: 12.5px;
  max-width: 68ch;
}

/* --- 告警条 -------------------------------------------------------------- */

/* 一条一行，没有卡片、没有彩色左边框。左边框那种做法会让「颜色」
   从状态信息退化成装饰，而这块面板上颜色是稀缺资源。 */
.alerts {
  margin: 0 22px 6px;
  border: 1px solid var(--hairline);
  border-radius: var(--r);
  overflow: hidden;
  background: var(--panel);
}

.alert-row {
  display: grid;
  grid-template-columns: 14px 1fr auto;
  align-items: center;
  gap: 11px;
  padding: 11px 14px;
  border-bottom: 1px solid var(--hairline);
}

.alert-row:last-child {
  border-bottom: 0;
}

.alert-row .alert-text {
  font-size: 12.5px;
  color: var(--fg);
  min-width: 0;
  /* 告警里带的是域名，而域名是没有空格的超长 token。
     不强制断行的话，一条告警就能把整页撑出横向滚动条。 */
  overflow-wrap: anywhere;
}

.alert-row .alert-sub {
  color: var(--fg-3);
  font-size: 11.5px;
}

/* --- 看板的行卡与任务行 -------------------------------------------------- */

/* 「一行就说完」的卡片（体检入口），头部内边距收紧一档，
   让它读起来像一条提示而不是一块需要展开的内容。 */
.card.slim .card-head {
  padding: 10px 16px;
}

.card.slim .card-head h2 {
  font-size: 12.5px;
}

.card.slim .card-head .sub {
  font-size: 11px;
}

/* 最近动态：一行一条任务，整行点开日志流。 */
.job-row {
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 10px 16px;
  border: 0;
  border-bottom: 1px solid var(--hairline);
  background: transparent;
  text-align: left;
  color: inherit;
  font-size: 12.5px;
  cursor: pointer;
  transition: background 0.14s var(--swift);
}

.job-row:last-child {
  border-bottom: 0;
}

.job-row:hover {
  background: rgba(255, 255, 255, 0.035);
}

.job-row .job-kind {
  color: var(--fg);
  white-space: nowrap;
}

.job-row .job-target {
  color: var(--fg-2);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.job-row .job-time {
  color: var(--fg-3);
  font-size: 11.5px;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* --- 插口排（配线面板本体） ---------------------------------------------- */

.bay {
  /* 列宽定义在面板上而不是行上：表头与每一行必须共用同一套栅格，
     否则表头会在第一次改字号时就和数据错开。

     第二列是**空的**，它唯一的用途是给跳线留出跨度。把两个插座并排放，
     线就只有十几像素长，看上去像一条缝而不是一根线；而「谁接在哪里」
     这件事，恰恰需要一段看得见的距离才成立。 */
  --bay-cols: minmax(140px, auto) minmax(72px, 1fr) minmax(0, auto) minmax(96px, auto) 84px;
  position: relative;
  margin: 6px 22px 0;
  border: 1px solid var(--hairline);
  border-radius: var(--r);
  background: var(--panel);
  overflow: hidden;
}

/* 跳线画在这一层上。它铺满整块面板但不吃指针事件——
   否则每一根线都会变成一行插口的点击热区上的洞。 */
.cords {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 1;
}

.cord {
  fill: none;
  stroke-width: 1.5;
  stroke-linecap: round;
  transition: stroke 0.4s var(--swift), stroke-width 0.24s var(--swift),
    opacity 0.3s var(--swift);
}

.cord-online {
  stroke: var(--online);
}
.cord-renewing {
  stroke: var(--renewing);
}
.cord-broken {
  stroke: var(--broken);
}
.cord-busy {
  stroke: var(--busy);
}
.cord-off {
  stroke: var(--off);
  opacity: 0.55;
}

/* 停用的插口，跳线是拔下来的：只从上游插座垂出一小段，末端悬空。
   断口由路径本身表达（见 app.js 的 drawCords），这里只压暗它。
   刻意不用 stroke-dasharray——那个属性归入场动画所有。 */
.cord-unplugged {
  opacity: 0.4;
}

.bay-head {
  display: grid;
  grid-template-columns: var(--bay-cols);
  gap: 16px;
  padding: 9px 14px;
  border-bottom: 1px solid var(--hairline);
  font-size: 10.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-3);
  background: var(--sunk);
}

.bay-head span:nth-child(4) {
  text-align: right;
}

/* 一行插口 = 一个站点。左端是上游插座，右端是域名插座，
   中间留给跳线。 */
.socket {
  display: grid;
  grid-template-columns: var(--bay-cols);
  align-items: center;
  gap: 16px;
  padding: 0 14px;
  min-height: 52px;
  border-bottom: 1px solid var(--hairline);
  position: relative;
  z-index: 2;
  cursor: pointer; /* 整行可点：点一下开详情弹窗 */
  transition: background 0.14s var(--swift);
}

.socket:last-child {
  border-bottom: 0;
}

.socket:hover {
  background: rgba(255, 255, 255, 0.035);
}

/* 聚焦一个插口时，让它在余光里也是亮的。

   做法是**把聚焦的那一行抬起来**，而不是把其余的行压下去。

   压行这个直觉很诱人（一行 opacity 就够了），但在这块面板上它是错的：
   状态和说明文字在同一行里，而 opacity 会连同子孙一起合成。整行退到
   .34 时，11px 的次级文字（--fg-3）实际只剩 1.7:1 左右——远在 4.5:1
   之下，等于读不出来；更要命的是插座灯也一起退了，于是「鼠标停在别的
   行上」这件事，足以让一屏插口里的第二个红灯淡到看不见。
   这块面板被打开的时刻，要找的恰恰就是那个红灯。

   所以：行不退，跳线退。线是图形，不是文字，压暗它不会让任何一条状态
   信息离开可读集合；而抬起来的行和亮起来的线，已经足够说明在看哪一根。 */
.bay[data-focus] .socket.is-focus {
  background: rgba(255, 255, 255, 0.05);
}

.bay[data-focus] .cord:not(.is-focus) {
  opacity: 0.28;
}

/* 被看着的那根线加粗一点。它和其余跳线的差别必须一眼可见，
   因为这是整块面板唯一表达「我在看这一根」的动效。 */
.bay[data-focus] .cord.is-focus {
  stroke-width: 2.4;
}


.port {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  font-size: 12.5px;
}

/* 两个插座面对面：上游插座的插座口贴在它所在列的右缘，域名插座的
   插座口贴在左缘，中间的空白正好留给跳线。这是配线面板本来的样子，
   也让「线接在哪两个点之间」不需要任何解释。 */
.port-up {
  grid-column: 1;
  justify-content: flex-end;
}

/* 域名插座落在第 3 列——第 2 列是跳线的跨度，刻意空着。 */
.port-dom {
  grid-column: 3;
  justify-content: flex-start;
}

.socket .metrics {
  grid-column: 4;
}

.socket .row-actions {
  grid-column: 5;
}

.port .addr {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* 域名是这一行里最重要的标识，但也得有个上限：
   一个超长域名不该把跳线的跨度挤成零。 */
.port-dom .addr {
  max-width: 36ch;
}

.port .addr-sub {
  color: var(--fg-3);
  font-size: 11px;
}

/* 插座本体：一个小小的凹槽，跳线从这里插出去。 */
.jack {
  width: 13px;
  height: 13px;
  border-radius: 3px;
  border: 1.5px solid var(--hairline-strong);
  background: var(--sunk);
  flex: none;
  position: relative;
}

.socket.is-live .jack {
  border-color: rgba(255, 255, 255, 0.3);
}

.socket.is-live .jack::after {
  content: "";
  position: absolute;
  inset: 3px;
  border-radius: 1px;
  background: currentColor;
  opacity: 0.9;
}

.socket.state-online .jack {
  color: var(--online);
}
.socket.state-renewing .jack {
  color: var(--renewing);
}
.socket.state-broken .jack {
  color: var(--broken);
}
.socket.state-busy .jack {
  color: var(--busy);
}
.socket.state-off .jack {
  color: var(--off);
}

/* 状态灯。整块面板只有这五种颜色。 */
.lamp {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex: none;
  background: var(--off);
  transition: background 0.35s var(--swift), box-shadow 0.35s var(--swift);
}

.lamp-online {
  background: var(--online);
  box-shadow: 0 0 0 3px rgba(48, 209, 88, 0.14);
}
.lamp-renewing {
  background: var(--renewing);
  box-shadow: 0 0 0 3px rgba(255, 159, 10, 0.14);
}
.lamp-broken {
  background: var(--broken);
  box-shadow: 0 0 0 3px rgba(255, 69, 58, 0.16);
}
.lamp-busy {
  background: var(--busy);
  box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.16);
  animation: breathe 1.4s ease-in-out infinite;
}
.lamp-off {
  background: var(--off);
  box-shadow: none;
}

@keyframes breathe {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.42;
  }
}

/* 封签：证书的状态印记，贴在域名插座上。
   完整时是实心，过期时中间裂开一道。 */
.seal {
  width: 13px;
  height: 13px;
  flex: none;
  color: var(--fg-3);
}

.seal-valid {
  color: var(--online);
}
.seal-due {
  color: var(--renewing);
}
.seal-broken {
  color: var(--broken);
}
.seal-none {
  color: var(--fg-3);
}

.socket .metrics {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.socket .metrics .days {
  font-size: 13px;
  font-weight: 500;
}

.socket .metrics .days.is-warn {
  color: var(--renewing);
}
.socket .metrics .days.is-bad {
  color: var(--broken);
}
.socket .metrics .days.is-off {
  color: var(--fg-3);
}

.socket .metrics .sub {
  display: block;
  font-size: 11px;
  color: var(--fg-3);
  font-weight: 400;
}

.socket .row-actions {
  display: flex;
  gap: 4px;
  justify-content: flex-end;
  opacity: 0;
  transition: opacity 0.16s var(--swift);
}

.socket:hover .row-actions,
.socket:focus-within .row-actions {
  opacity: 1;
}

/* 键盘用户不该因为 hover 才有操作入口。 */
.socket:focus-within .row-actions {
  opacity: 1;
}

/* --- 卡片与区块 ---------------------------------------------------------- */

.card {
  margin: 0 22px 16px;
  border: 1px solid var(--hairline);
  border-radius: var(--r);
  background: var(--panel);
  overflow: hidden;
}

.card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 13px 16px;
  border-bottom: 1px solid var(--hairline);
}

.card-head h2 {
  margin: 0;
  font-size: 13.5px;
  font-weight: 590;
  letter-spacing: -0.01em;
}

.card-head .sub {
  color: var(--fg-3);
  font-size: 11.5px;
}

.card-body {
  padding: 16px;
}

/* 卡片内的最后一段不该再撑出一截空白——那是内边距之外的额外一行，
   看起来像是内容没写完。 */
.card-body > *:last-child {
  margin-bottom: 0;
}

.card-body.flush {
  padding: 0;
}

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

/* 表格只用在真正是「记录列表」的地方：任务、审计、证书。
   站点列表用配线面板，因为站点之间的关系是接线，不是行。 */
table.grid {
  width: 100%;
  border-collapse: collapse;
  font-size: 12.5px;
}

table.grid th {
  text-align: left;
  font-weight: 500;
  font-size: 10.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-3);
  padding: 9px 14px;
  border-bottom: 1px solid var(--hairline);
  background: var(--sunk);
  white-space: nowrap;
}

table.grid td {
  padding: 10px 14px;
  border-bottom: 1px solid var(--hairline);
  vertical-align: middle;
}

table.grid tbody tr:last-child td {
  border-bottom: 0;
}

table.grid tbody tr {
  transition: background 0.12s var(--swift);
}

table.grid tbody tr:hover {
  background: rgba(255, 255, 255, 0.035);
}

/* 右对齐的列一定是数字列。数字折行会把「61 天」拆成两行，
   而一列高度不齐的数字表，读起来比不排序还糟。 */
table.grid td.right,
table.grid th.right {
  text-align: right;
  white-space: nowrap;
}

/* 长签发者名（比如 "(STAGING) Pretend Pear X1"）不该把整行撑高。
   截断，但把全文留在 title 里——信息还在，只是不占版面。 */
.ellip {
  display: inline-block;
  max-width: 22ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}

table.grid td.nowrap {
  white-space: nowrap;
}

/* --- 徽标 ---------------------------------------------------------------- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 19px;
  padding: 0 7px;
  border-radius: 5px;
  font-size: 11px;
  font-weight: 500;
  background: rgba(255, 255, 255, 0.08);
  color: var(--fg-2);
  white-space: nowrap;
}

.badge .lamp {
  width: 6px;
  height: 6px;
  box-shadow: none;
}

.badge-online {
  background: rgba(48, 209, 88, 0.15);
  color: var(--online);
}
.badge-renewing {
  background: rgba(255, 159, 10, 0.15);
  color: var(--renewing);
}
.badge-broken {
  background: rgba(255, 69, 58, 0.15);
  color: var(--broken);
}
.badge-busy {
  background: rgba(10, 132, 255, 0.16);
  color: #64b5ff;
}
.badge-off {
  background: rgba(142, 142, 147, 0.16);
  color: var(--off);
}

.badge-plain {
  background: transparent;
  border: 1px solid var(--hairline-strong);
  color: var(--fg-3);
}

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

.field {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 15px;
}

.field > label {
  font-size: 12px;
  color: var(--fg-2);
  font-weight: 500;
}

.field .hint {
  font-size: 11.5px;
  color: var(--fg-3);
  line-height: 1.55;
}

.field .err {
  font-size: 11.5px;
  color: var(--broken);
  line-height: 1.55;
}

input[type="text"],
input[type="password"],
input[type="number"],
input[type="search"],
input[type="email"],
select,
textarea {
  width: 100%;
  height: 34px;
  padding: 0 11px;
  border: 1px solid var(--hairline-strong);
  border-radius: 8px;
  background: var(--sunk);
  color: var(--fg);
  font-size: 13px;
  transition: border-color 0.14s var(--swift), background 0.14s var(--swift);
}

textarea {
  height: auto;
  min-height: 90px;
  padding: 9px 11px;
  resize: vertical;
  line-height: 1.6;
}

input::placeholder,
textarea::placeholder {
  color: var(--fg-2);
  opacity: 1;
}

input:hover,
select:hover,
textarea:hover {
  border-color: rgba(255, 255, 255, 0.22);
}

input:focus,
select:focus,
textarea:focus {
  border-color: var(--accent);
  background: #17171a;
}

input:disabled,
select:disabled,
textarea:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

select {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--fg-3) 50%),
    linear-gradient(135deg, var(--fg-3) 50%, transparent 50%);
  background-position: calc(100% - 15px) 15px, calc(100% - 10px) 15px;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: 30px;
}

.field-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
}

.check {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  font-size: 12.5px;
  cursor: pointer;
  line-height: 1.5;
}

.check input {
  width: 15px;
  height: 15px;
  margin: 2px 0 0;
  flex: none;
  accent-color: var(--accent);
  cursor: pointer;
}

.check .check-note {
  display: block;
  color: var(--fg-3);
  font-size: 11.5px;
  margin-top: 2px;
}

/* --- 对话框 -------------------------------------------------------------- */

/* 只用于真正需要打断和受保护焦点的事：一次不可逆的变更、
   一份必须被逐字读过的凭据。 */
dialog {
  border: 1px solid var(--hairline-strong);
  border-radius: var(--r-lg);
  background: var(--panel);
  color: var(--fg);
  padding: 0;
  max-width: min(560px, calc(100vw - 32px));
  width: 100%;
  box-shadow: 0 24px 60px -12px rgba(0, 0, 0, 0.7),
    0 2px 8px rgba(0, 0, 0, 0.4);
}

dialog::backdrop {
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(2px);
}

dialog.wide {
  max-width: min(880px, calc(100vw - 32px));
}

.dlg-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 16px 18px 12px;
}

.dlg-head h2 {
  margin: 0;
  font-size: 15px;
  font-weight: 590;
  letter-spacing: -0.015em;
}

.dlg-body {
  padding: 0 18px;
  max-height: min(62vh, 620px);
  overflow-y: auto;
}

.dlg-foot {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 16px 18px 18px;
}

.dlg-note {
  font-size: 12px;
  color: var(--fg-2);
  line-height: 1.65;
  margin: 0 0 15px;
}

/* --- 代码与差异 ---------------------------------------------------------- */

.code {
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.65;
  background: var(--sunk);
  border: 1px solid var(--hairline);
  border-radius: 8px;
  padding: 12px 14px;
  overflow-x: auto;
  white-space: pre;
  tab-size: 4;
  color: var(--fg-2);
}

.code .cmt {
  color: var(--fg-3);
}

.diff {
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.65;
  background: var(--sunk);
  border: 1px solid var(--hairline);
  border-radius: 8px;
  overflow: hidden;
}

.diff-line {
  display: grid;
  grid-template-columns: 22px 1fr;
  white-space: pre;
  min-height: 1.65em;
}

.diff-line .sign {
  text-align: center;
  color: var(--fg-3);
  user-select: none;
}

.diff-line .text {
  padding-right: 12px;
  overflow-x: auto;
}

.diff-add {
  background: rgba(48, 209, 88, 0.11);
}

.diff-add .sign,
.diff-add .text {
  color: #7ee2a0;
}

.diff-del {
  background: rgba(255, 69, 58, 0.1);
}

.diff-del .sign,
.diff-del .text {
  color: #ff8a80;
}

/* --- 日志流 -------------------------------------------------------------- */

.log {
  font-family: var(--mono);
  font-size: 11.5px;
  line-height: 1.6;
  background: #08080a;
  border: 1px solid var(--hairline);
  border-radius: 8px;
  padding: 11px 13px;
  height: 340px;
  overflow-y: auto;
  white-space: pre-wrap;
  word-break: break-all;
  color: var(--fg-2);
  overscroll-behavior: contain;
}

.log-line {
  display: flex;
  gap: 10px;
}

.log-line .ts {
  color: var(--fg-3);
  flex: none;
}

.log-line.is-stderr .msg {
  color: #ff8a80;
}

.log-line.is-status .msg {
  color: #64b5ff;
}

.log-line.is-ok .msg {
  color: #7ee2a0;
}

.log-empty {
  color: var(--fg-3);
}

/* --- 空态 ---------------------------------------------------------------- */

.empty {
  padding: 44px 22px;
  text-align: center;
  color: var(--fg-2);
}

.empty h3 {
  margin: 0 0 6px;
  font-size: 14px;
  font-weight: 590;
  color: var(--fg);
}

.empty p {
  margin: 0 auto 18px;
  max-width: 46ch;
  font-size: 12.5px;
  line-height: 1.7;
}

/* --- 骨架屏 -------------------------------------------------------------- */

.skeleton {
  border-radius: 6px;
  background: linear-gradient(
    90deg,
    rgba(255, 255, 255, 0.05) 25%,
    rgba(255, 255, 255, 0.09) 37%,
    rgba(255, 255, 255, 0.05) 63%
  );
  background-size: 400% 100%;
  animation: shimmer 1.4s ease-in-out infinite;
}

@keyframes shimmer {
  0% {
    background-position: 100% 0;
  }
  100% {
    background-position: 0 0;
  }
}

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

.toasts {
  position: fixed;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  gap: 8px;
  z-index: 80;
  pointer-events: none;
  width: min(460px, calc(100vw - 32px));
}

.toast {
  display: grid;
  grid-template-columns: 16px 1fr auto;
  align-items: start;
  gap: 10px;
  padding: 12px 14px;
  border-radius: var(--r);
  background: var(--glass);
  backdrop-filter: var(--glass-filter);
  -webkit-backdrop-filter: var(--glass-filter);
  border: 1px solid var(--hairline-strong);
  box-shadow: 0 16px 40px -12px rgba(0, 0, 0, 0.7);
  font-size: 12.5px;
  pointer-events: auto;
  animation: toast-in 0.34s var(--spring);
}

.toast .toast-body {
  min-width: 0;
}

.toast .toast-hint {
  display: block;
  color: var(--fg-2);
  font-size: 11.5px;
  margin-top: 3px;
  line-height: 1.55;
}

.toast svg {
  width: 16px;
  height: 16px;
  margin-top: 1px;
}

.toast-ok svg {
  color: var(--online);
}
.toast-err svg {
  color: var(--broken);
}
.toast-warn svg {
  color: var(--renewing);
}
.toast-info svg {
  color: var(--busy);
}

@keyframes toast-in {
  from {
    opacity: 0;
    transform: translateY(14px) scale(0.98);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.toast.is-out {
  animation: toast-out 0.22s var(--swift) forwards;
}

@keyframes toast-out {
  to {
    opacity: 0;
    transform: translateY(8px);
  }
}

/* --- 关于面板 ------------------------------------------------------------ */

.defs {
  display: grid;
  grid-template-columns: 168px 1fr;
  gap: 1px;
  background: var(--hairline);
  border: 1px solid var(--hairline);
  border-radius: var(--r);
  overflow: hidden;
}

.defs dt,
.defs dd {
  margin: 0;
  padding: 10px 14px;
  background: var(--panel);
  font-size: 12.5px;
}

.defs dt {
  color: var(--fg-3);
}

.defs dd {
  font-variant-numeric: tabular-nums;
}

/* --- 体检 ---------------------------------------------------------------- */

.check-row {
  display: grid;
  grid-template-columns: 18px 1fr;
  gap: 12px;
  padding: 13px 16px;
  border-bottom: 1px solid var(--hairline);
}

.check-row:last-child {
  border-bottom: 0;
}

.check-row .check-title {
  font-size: 13px;
  font-weight: 500;
  display: flex;
  align-items: center;
  gap: 9px;
}

.check-row .check-detail {
  color: var(--fg-2);
  font-size: 12px;
  margin-top: 3px;
  line-height: 1.6;
  word-break: break-word;
}

.check-row .check-remedy {
  color: var(--fg-2);
  font-size: 12px;
  margin-top: 7px;
  padding: 8px 11px;
  background: var(--sunk);
  border-radius: 7px;
  line-height: 1.6;
}

.check-row .check-remedy b {
  color: var(--fg);
  font-weight: 500;
}

.sev {
  width: 16px;
  height: 16px;
  margin-top: 2px;
}

.sev-ok {
  color: var(--online);
}
.sev-warn {
  color: var(--renewing);
}
.sev-fail {
  color: var(--broken);
}
.sev-info {
  color: var(--off);
}

/* --- 分段控件 ------------------------------------------------------------ */

.seg {
  display: inline-flex;
  padding: 2px;
  gap: 2px;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.07);
}

.seg button {
  border: 0;
  background: transparent;
  color: var(--fg-2);
  font-size: 12px;
  font-weight: 500;
  padding: 4px 11px;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.16s var(--swift), color 0.16s var(--swift);
}

.seg button[aria-pressed="true"] {
  background: rgba(255, 255, 255, 0.14);
  color: var(--fg);
}

/* --- 登录页 -------------------------------------------------------------- */

/* 登录页是控制室的门。它不该被设计成一块招牌，
   而该像一扇安静、立刻就开的门。 */
.gate {
  min-height: 100vh;
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: 24px;
  background: var(--ground);
  position: relative;
  overflow: hidden;
}

/* 唯一一处刻意的氛围：地面上一道极淡的光晕，暗示面板就在门后。 */
.gate::before {
  content: "";
  position: absolute;
  width: 720px;
  height: 720px;
  left: 50%;
  top: 42%;
  transform: translate(-50%, -50%);
  background: radial-gradient(
    circle,
    rgba(10, 132, 255, 0.09) 0%,
    rgba(10, 132, 255, 0) 62%
  );
  pointer-events: none;
}

.gate-inner {
  width: 100%;
  max-width: 344px;
  position: relative;
  z-index: 1;
}

.gate-mark {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 26px;
}

.gate-mark svg {
  width: 26px;
  height: 26px;
}

/* 门上的那枚灯。登录成功时它会亮起来——这是这一页唯一的编排动效，
   也是整块面板的语言在门上的第一次出现。 */
.gate-lamp {
  display: inline-flex;
  flex: none;
  color: var(--fg-3);
}

.gate-mark .name {
  font-size: 16px;
  font-weight: 590;
  letter-spacing: -0.02em;
}

.gate-mark .sub {
  font-size: 11.5px;
  color: var(--fg-3);
}

.gate form {
  background: var(--panel);
  border: 1px solid var(--hairline);
  border-radius: var(--r-lg);
  padding: 20px 20px 18px;
}

.gate .gate-foot {
  margin-top: 16px;
  font-size: 11.5px;
  color: var(--fg-3);
  text-align: center;
  line-height: 1.7;
}

/* TOTP 输入：6 位数字，字距拉开，一眼能数清。 */
.totp-input {
  font-family: var(--mono);
  font-size: 19px !important;
  letter-spacing: 0.34em;
  text-align: center;
  height: 42px !important;
  padding-left: 0.34em !important;
}

.gate .btn-primary {
  width: 100%;
  height: 36px;
  font-size: 13px;
  margin-top: 4px;
}

.gate-error {
  margin: 0 0 14px;
  padding: 10px 12px;
  border-radius: 8px;
  background: rgba(255, 69, 58, 0.12);
  color: #ff8a80;
  font-size: 12px;
  line-height: 1.6;
}

/* 错误里的补充说明用中性色，让第一行始终是「出了什么事」，
   第二行才是「还能怎么办」。 */
.gate-error .toast-hint {
  color: rgba(255, 255, 255, 0.72);
}

.gate-switch {
  margin-top: 12px;
  text-align: center;
  font-size: 12px;
}

.gate-switch button {
  border: 0;
  background: none;
  color: var(--accent);
  cursor: pointer;
  padding: 0;
  font-size: 12px;
}

.gate-switch button:hover {
  text-decoration: underline;
}

/* --- 响应式 -------------------------------------------------------------- */

@media (max-width: 1080px) {
  .bay {
    --bay-cols: minmax(120px, 1fr) 1fr minmax(84px, auto) 70px;
  }
  .gauge {
    padding: 10px 13px;
  }
  .gauge-bar {
    width: 48px;
  }
}

@media (max-width: 900px) {
  .instruments {
    overflow-x: auto;
    scrollbar-width: none;
    margin-left: 12px;
    margin-right: 12px;
  }

  /* 窄屏上让每个表盘保持自然宽度，装不下就横向滚。
     不加这条的话，.gauge 的 min-width: 0 会允许它一直缩——四个表盘
     全部挤成只剩第一个数字（「7」「5」「37」），标签和单位被裁得一干二净，
     而容器的 overflow-x 因为从来没有内容溢出，滚动条也永远不会出现。
     结果是既不可读、也不可滚，两头落空。 */
  .gauge {
    flex: none;
  }

  .instruments::-webkit-scrollbar {
    display: none;
  }

  /* 窄屏放不下「两端插座 + 跳线」这一行，于是把上游地址折到第二行。
     跳线在这个宽度下不画——它需要横向空间才有意义，硬塞进来只会
     变成一段谁也读不懂的曲线。 */
  .socket {
    grid-template-columns: minmax(0, 1fr) minmax(72px, auto) 64px;
    grid-template-areas:
      "dom days act"
      "up  days act";
    row-gap: 0;
    padding: 9px 14px;
    min-height: 0;
  }

  .socket .port-up {
    grid-area: up;
  }
  .socket .port-dom {
    grid-area: dom;
    justify-content: flex-start;
    text-align: left;
  }
  .socket .metrics {
    grid-area: days;
  }
  .socket .row-actions {
    grid-area: act;
  }

  .cords {
    display: none;
  }

  .bay-head {
    display: none;
  }

  .view-head {
    flex-direction: column;
    align-items: stretch;
    gap: 12px;
  }

  .defs {
    grid-template-columns: 1fr;
  }

  .defs dt {
    padding-bottom: 0;
    background: var(--panel);
  }

  .field-row {
    grid-template-columns: 1fr;
  }

  .card,
  .bay,
  .alerts,
  .instruments {
    margin-left: 12px;
    margin-right: 12px;
  }

  .view-head {
    padding-left: 12px;
    padding-right: 12px;
  }

  .panel-toolbar {
    padding-left: 12px;
    padding-right: 12px;
  }
}

@media (max-width: 560px) {
  .topbar {
    gap: 10px;
    padding: 0 10px 0 12px;
  }

  /* 三个视图加会话时钟在 390px 上排不下。让导航条自己横向滚动，
     而不是让项被挤出屏幕——够不着的入口等于不存在。 */
  .nav {
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }

  .nav::-webkit-scrollbar {
    display: none;
  }

  .nav a {
    padding: 6px 8px;
    font-size: 12px;
  }

  .mark .mark-sub {
    display: none;
  }

  .session-clock {
    display: none;
  }

  /* 折行之后上游地址独占一行，右对齐会让它飘在半空——
     左对齐才和下面的域名对齐成一条边。 */
  .socket .port-up {
    justify-content: flex-start;
  }
}

/* 尊重系统的减少动态偏好。跳线的绘制动画、呼吸灯、骨架屏
   在这个偏好下全部静止。 */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }

  .cord {
    stroke-dasharray: none !important;
    stroke-dashoffset: 0 !important;
  }
}
