/* ==========================================================================
   MarkHub — 令牌层 (tokens)
   ========================================================================== */

:root {
  color-scheme: light;

  /* 暖琥珀 + 链接蓝 */
  --bg: #fffbeb;
  --bg-sidebar: #fdf4e3;
  --surface: #ffffff;
  --surface-2: #fcf6f0;
  --fg: #0f172a;
  --fg-muted: #475569;
  --border: #f3e6d3;
  --border-strong: #e6d5bd;

  --brand: #d97706;
  --brand-strong: #b45309;
  --brand-soft: #fef3c7;
  --on-brand: #000000;

  --accent: #2563eb;
  --accent-strong: #1d4ed8;
  --accent-soft: #dbeafe;
  --on-accent: #ffffff;

  --danger: #dc2626;
  --danger-soft: #fee2e2;
  --success: #059669;

  --ring: #b45309;

  --shadow-sm: 0 1px 2px rgb(15 23 42 / 0.05), 0 1px 3px rgb(15 23 42 / 0.04);
  --shadow-md: 0 4px 6px -1px rgb(15 23 42 / 0.07), 0 10px 20px -4px rgb(15 23 42 / 0.08);
  --shadow-lg: 0 18px 40px -12px rgb(15 23 42 / 0.22);

  --r-sm: 8px;
  --r-md: 12px;
  --r-lg: 16px;
  --r-full: 999px;

  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 20px;
  --sp-6: 24px;
  --sp-8: 32px;

  --sidebar-w: 268px;
  --topbar-h: 68px;
  /* 页脚那一条的**名义高度**（一行文字 + 上下留白）。
     只给提示条（toasts）让位置用 —— 页脚自己的高度是它内容决定的，
     折行就变高，外壳那一行自动让出位置（见文件末尾「站点页脚」那一节）。 */
  --footer-h: 2.75rem;

  --ease: cubic-bezier(0.4, 0, 0.2, 1);
  --dur: 200ms;

  --g-amber: #d97706;
  --g-blue: #2563eb;
  --g-green: #059669;
  --g-rose: #e11d48;
  --g-violet: #7c3aed;
  --g-cyan: #0891b2;
  --g-orange: #ea580c;
  --g-slate: #64748b;
}

[data-theme='dark'] {
  color-scheme: dark;

  --bg: #0b1120;
  --bg-sidebar: #0e1626;
  --surface: #141d2f;
  --surface-2: #1b263b;
  --fg: #e8edf5;
  --fg-muted: #9aa8bd;
  --border: #263246;
  --border-strong: #35435c;

  --brand: #fbbf24;
  --brand-strong: #f59e0b;
  --brand-soft: rgb(251 191 36 / 0.16);
  --on-brand: #1a1204;

  --accent: #3b82f6;
  --accent-strong: #60a5fa;
  --accent-soft: rgb(59 130 246 / 0.18);
  --on-accent: #ffffff;

  --danger: #f87171;
  --danger-soft: rgb(248 113 113 / 0.18);
  --success: #34d399;

  --ring: #fbbf24;

  --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.4);
  --shadow-md: 0 8px 24px -6px rgb(0 0 0 / 0.55);
  --shadow-lg: 0 20px 48px -12px rgb(0 0 0 / 0.7);

  --g-amber: #fbbf24;
  --g-blue: #60a5fa;
  --g-green: #34d399;
  --g-rose: #fb7185;
  --g-violet: #a78bfa;
  --g-cyan: #22d3ee;
  --g-orange: #fb923c;
  --g-slate: #94a3b8;
}

/* ==========================================================================
   基础
   ========================================================================== */

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

/*
 * 兜底规则：类选择器设置的 display 会盖掉 UA 样式表里的 [hidden]{display:none}
 * （例如 .field{display:flex} 会让 hidden 的输入框照样显示），所以统一压住。
 */
[hidden] {
  display: none !important;
}

html {
  font-size: 16px;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  min-height: 100vh;
  background: var(--bg);
  color: var(--fg);
  font-family: 'Plus Jakarta Sans', 'Inter', system-ui, -apple-system, 'Segoe UI',
    'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans SC', sans-serif;
  font-size: 0.9375rem;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1,
h2,
h3 {
  margin: 0;
  font-weight: 650;
  letter-spacing: -0.01em;
  line-height: 1.25;
}

p {
  margin: 0;
}

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

a:hover {
  text-decoration: underline;
}

button {
  font: inherit;
  color: inherit;
  cursor: pointer;
}

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

svg {
  display: block;
  flex: none;
}

:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

::selection {
  background: var(--brand-soft);
  color: var(--fg);
}

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

/* 滚动条 */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}

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

*::-webkit-scrollbar-thumb {
  background: var(--border-strong);
  border: 3px solid transparent;
  border-radius: var(--r-full);
  background-clip: content-box;
}

*::-webkit-scrollbar-thumb:hover {
  background: var(--fg-muted);
  background-clip: content-box;
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ==========================================================================
   按钮
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: 2.5rem;
  padding: 0 var(--sp-4);
  border: 1px solid transparent;
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: var(--fg);
  font-size: 0.9375rem;
  font-weight: 600;
  white-space: nowrap;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease),
    color var(--dur) var(--ease), transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}

.btn:hover {
  background: var(--border);
}

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

.btn:disabled {
  opacity: 0.55;
  cursor: not-allowed;
  transform: none;
}

.btn svg {
  width: 1.125rem;
  height: 1.125rem;
}

.btn--primary {
  background: var(--accent);
  color: var(--on-accent);
  box-shadow: var(--shadow-sm);
}

.btn--primary:hover {
  background: var(--accent-strong);
}

.btn--brand {
  background: var(--brand);
  color: var(--on-brand);
  box-shadow: var(--shadow-sm);
}

.btn--brand:hover {
  background: var(--brand-strong);
}

.btn--ghost {
  background: transparent;
  border-color: var(--border-strong);
  color: var(--fg);
}

.btn--ghost:hover {
  background: var(--surface-2);
  border-color: var(--fg-muted);
}

.btn--danger {
  background: var(--danger-soft);
  color: var(--danger);
  border-color: transparent;
}

.btn--danger:hover {
  background: var(--danger);
  color: #fff;
}

.btn--block {
  width: 100%;
}

.btn--sm {
  min-height: 2rem;
  padding: 0 var(--sp-3);
  font-size: 0.8125rem;
  border-radius: var(--r-sm);
}

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 2.5rem;
  height: 2.5rem;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  background: transparent;
  color: var(--fg-muted);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

.icon-btn svg {
  width: 1.25rem;
  height: 1.25rem;
}

.icon-btn:hover {
  background: var(--brand-soft);
  color: var(--brand-strong);
}

.icon-btn--danger:hover {
  background: var(--danger-soft);
  color: var(--danger);
}

.icon-btn[aria-pressed='true'] {
  background: var(--accent);
  color: var(--on-accent);
}

.icon-btn[aria-pressed='true']:hover {
  background: var(--accent-strong);
  color: var(--on-accent);
}

.icon-btn--sm {
  width: 2rem;
  height: 2rem;
}

.icon-btn--sm svg {
  width: 1rem;
  height: 1rem;
}

@media (pointer: coarse) {
  .icon-btn {
    width: 2.75rem;
    height: 2.75rem;
  }
}

/* ==========================================================================
   表单
   ========================================================================== */

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

.field__label {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--fg);
}

.field__hint {
  font-size: 0.75rem;
  color: var(--fg-muted);
}

.field__error {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--danger);
}

.field__error svg {
  width: 0.875rem;
  height: 0.875rem;
}

.input,
.select,
.textarea {
  width: 100%;
  min-height: 2.75rem;
  padding: 0.5rem 0.75rem;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  font-size: 0.9375rem;
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
  appearance: none;
}

.input::placeholder,
.textarea::placeholder {
  color: var(--fg-muted);
  opacity: 0.75;
}

.input:hover,
.select:hover,
.textarea:hover {
  border-color: var(--fg-muted);
}

.input:focus,
.select:focus,
.textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.input[aria-invalid='true'],
.textarea[aria-invalid='true'],
.select[aria-invalid='true'] {
  border-color: var(--danger);
}

.input[aria-invalid='true']:focus,
.textarea[aria-invalid='true']:focus {
  box-shadow: 0 0 0 3px var(--danger-soft);
}

.textarea {
  min-height: 4.5rem;
  resize: vertical;
  line-height: 1.5;
}

.select {
  padding-right: 2.25rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23475569' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.75rem center;
  cursor: pointer;
}

[data-theme='dark'] .select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%239aa8bd' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}

.select--bare {
  min-height: 2.25rem;
  padding: 0 2rem 0 0.625rem;
  border-color: transparent;
  background-color: transparent;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--fg-muted);
}

.select--bare:hover {
  background-color: var(--surface-2);
  color: var(--fg);
}

/* 复选 / 开关 */
.check {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  cursor: pointer;
  font-size: 0.875rem;
  user-select: none;
}

.check input {
  width: 1.125rem;
  height: 1.125rem;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}

/* ==========================================================================
   徽标 / 头像 / 图标
   ========================================================================== */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-height: 1.375rem;
  padding: 0 0.5rem;
  border-radius: var(--r-full);
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--fg-muted);
  font-size: 0.75rem;
  font-weight: 600;
  white-space: nowrap;
}

.badge svg {
  width: 0.75rem;
  height: 0.75rem;
}

.badge--group {
  background: color-mix(in srgb, var(--g) 12%, transparent);
  border-color: color-mix(in srgb, var(--g) 30%, transparent);
  color: color-mix(in srgb, var(--g) 82%, var(--fg));
}

.count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.375rem;
  height: 1.375rem;
  padding: 0 0.375rem;
  border-radius: var(--r-full);
  background: var(--surface-2);
  color: var(--fg-muted);
  font-size: 0.6875rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.nav__item[aria-current='page'] .count {
  background: var(--brand-soft);
  color: var(--brand-strong);
}

.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 2rem;
  height: 2rem;
  border-radius: var(--r-full);
  background: linear-gradient(140deg, var(--brand), var(--g-orange));
  color: #fff;
  font-size: 0.8125rem;
  font-weight: 700;
  text-transform: uppercase;
  user-select: none;
}

.favicon {
  position: relative;
  display: grid;
  place-items: center;
  flex: none;
  width: 2rem;
  height: 2rem;
  border-radius: var(--r-sm);
  background: var(--surface-2);
  border: 1px solid var(--border);
  overflow: hidden;
}

.favicon__letter {
  display: grid;
  place-items: center;
  position: absolute;
  inset: 0;
  font-size: 0.8125rem;
  font-weight: 700;
  color: #fff;
  line-height: 1;
}

/*
 * 字母头像的字画**不是文本节点**，是伪元素。
 *
 * 为什么要绕这一下：那个字母是纯装饰（图标没加载出来时的垫底），
 * 父元素已经 aria-hidden 了 —— 但它**在屏幕上看得见**，
 * 而 axe 的 label-content-name-mismatch 规则量的是「可见文字」，
 * 不看 aria-hidden。于是导航页那张卡片（role="link" + aria-label）
 * 的可见文字里有这个字母、可访问名里没有，规则就红了。
 *
 * 画成伪元素之后它不再是 DOM 里的文本，规则量不到它，
 * 而屏幕上那个字一模一样（同一个 span、同一套字体和颜色）。
 */
.favicon__letter::before {
  content: attr(data-initial);
}

.favicon__img {
  position: absolute;
  inset: 0;
  margin: auto;
  object-fit: contain;
  opacity: 0;
  transition: opacity 180ms var(--ease);
}

.favicon__img.is-ready {
  opacity: 1;
}

.favicon--lg {
  width: 2.5rem;
  height: 2.5rem;
}

.favicon--lg .favicon__letter {
  font-size: 1rem;
}

.dot {
  display: block;
  flex: none;
  width: 0.625rem;
  height: 0.625rem;
  border-radius: var(--r-full);
  background: var(--g, var(--brand));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--g, var(--brand)) 18%, transparent);
}

.spinner {
  width: 1.125rem;
  height: 1.125rem;
  border: 2px solid color-mix(in srgb, currentColor 25%, transparent);
  border-top-color: currentColor;
  border-radius: var(--r-full);
  animation: spin 700ms linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* ==========================================================================
   Toast
   ========================================================================== */

.toasts {
  position: fixed;
  z-index: 90;
  right: var(--sp-5);
  bottom: var(--sp-5);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  width: min(22rem, calc(100vw - 2.5rem));
  pointer-events: none;
}

.toast {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  padding: 0.75rem var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-lg);
  font-size: 0.875rem;
  pointer-events: auto;
  animation: toast-in 260ms var(--ease) both;
}

.toast[data-leaving='true'] {
  animation: toast-out 180ms var(--ease) both;
}

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

.toast--ok svg {
  color: var(--success);
}

.toast--error svg {
  color: var(--danger);
}

.toast--info svg {
  color: var(--accent);
}

.toast__body {
  flex: 1;
  min-width: 0;
}

@keyframes toast-in {
  from {
    opacity: 0;
    transform: translateY(12px) scale(0.97);
  }
}

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

/* ==========================================================================
   弹窗
   ========================================================================== */

.modal {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: grid;
  place-items: center;
  padding: var(--sp-4);
  overflow-y: auto;
}

.modal[hidden] {
  display: none;
}

.modal__scrim {
  position: fixed;
  inset: 0;
  background: rgb(15 23 42 / 0.45);
  backdrop-filter: blur(2px);
  animation: fade-in 160ms var(--ease) both;
}

.modal__panel {
  position: relative;
  width: min(34rem, 100%);
  margin: auto;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg);
  animation: modal-in 220ms var(--ease) both;
}

.modal__panel--sm {
  width: min(26rem, 100%);
}

.modal__head {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-5) var(--sp-5) 0;
}

.modal__title {
  flex: 1;
  font-size: 1.0625rem;
}

.modal__sub {
  margin-top: 2px;
  font-size: 0.8125rem;
  font-weight: 400;
  color: var(--fg-muted);
}

.modal__body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  padding: var(--sp-5);
}

.modal__foot {
  display: flex;
  justify-content: flex-end;
  gap: var(--sp-2);
  padding: 0 var(--sp-5) var(--sp-5);
}

.modal__row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-3);
}

@keyframes fade-in {
  from {
    opacity: 0;
  }
}

@keyframes modal-in {
  from {
    opacity: 0;
    transform: translateY(14px) scale(0.98);
  }
}

/* ==========================================================================
   应用外壳
   ========================================================================== */

.app {
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  min-height: 100vh;
}

.scrim {
  position: fixed;
  inset: 0;
  z-index: 40;
  background: rgb(15 23 42 / 0.4);
  animation: fade-in 160ms var(--ease) both;
}

.scrim[hidden] {
  display: none;
}

/* --- 侧边栏 ------------------------------------------------------------- */

.sidebar {
  position: sticky;
  top: 0;
  z-index: 45;
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
  height: 100vh;
  padding: var(--sp-4);
  background: var(--bg-sidebar);
  border-right: 1px solid var(--border);
  overflow-y: auto;
}

.sidebar__head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

.brand {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex: 1;
  min-width: 0;
  color: var(--fg);
  text-decoration: none;
}

.brand:hover {
  text-decoration: none;
}

.brand__mark {
  display: grid;
  place-items: center;
  flex: none;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 0.7rem;
  background: linear-gradient(140deg, var(--brand) 10%, var(--g-orange) 90%);
  color: #fff;
  box-shadow: var(--shadow-sm);
}

.brand__mark svg {
  width: 1.25rem;
  height: 1.25rem;
}

/* 管理员在管理面板传了自定义图标时，服务端把这里的内容换成 <img>。
   容器本身是 2.25rem 的圆角方块，图片填满它并保留同样的圆角。 */
.brand__mark img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
  display: block;
}

.brand__name {
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  white-space: nowrap;
}

/*
 * 品牌名后面跟的「这一页的名字」（导航页上的是「导航页」）。
 *
 * 它不是品牌名的一部分 —— 品牌是 WalkLink，「导航页」只是站在哪一页 ——
 * 所以要跟品牌名分开：用一条竖线隔开。
 *
 * 但也不能小到看不见。原先是 13px / 字重 500 / 淡灰，挨着 16px / 700 / 近黑的
 * 品牌名几乎看不见 —— 一眼扫过去只读到「WalkLink」。现在字号跟正文一样、
 * 字重接近品牌名、颜色跟品牌名一样深，只靠那条竖线跟品牌名区分开。
 */
.brand__suffix {
  display: inline-block;
  margin-left: 0.5rem;
  padding-left: 0.5rem;
  border-left: 1px solid var(--border-strong);
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--fg);
  white-space: nowrap;
}

.brand__sub {
  /*
   * 这一行是**宣传语**，不是正文 —— 所以最多两行，多了夹住。
   *
   * 夹它是因为它在顶栏里，宽度只有品牌名那一栏那么点（402px 屏上 118px）。
   * 导航页那句宣传语改长了，英文那一版天然比中文长得多（96 字符 vs 21 个字），
   * 不夹的话 402px 宽的屏上顶栏会被顶到 240px 高 —— 顶栏是 sticky 的，
   * 那就等于把五分之一的屏占住了。
   */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: 0.6875rem;
  font-weight: 500;
  color: var(--fg-muted);
  letter-spacing: 0.04em;
}

/*
 * 导航页正文顶上那一行（那句宣传语）。
 *
 * 它原来在顶栏品牌下面那一栏里 —— 那一栏在 402px 屏上只有 118px 宽，
 * 中文那句要三行、英文那句要五行，夹也夹不住（截了就是缺字）。
 * 挪到正文顶上（370px 宽）就一两行都放得下：**什么语言都不用截**，
 * 而且在这儿它比待在顶栏那点小字里更显眼。
 */
.nav-lead {
  margin: 0 0 var(--sp-3);
  color: var(--fg-muted);
  font-size: 0.9375rem;
  line-height: 1.55;
}

/*
 * 导航页上那一行小字：访客的语言没开导航页时，说明下面看到的是系统默认
 * 语言那一版。
 */
.nav-hint {
  margin: 0 0 var(--sp-3);
  color: var(--fg-muted);
  font-size: 0.8125rem;
  line-height: 1.55;
}

/* 公开导航页的二级分类按钮行（「全部」+ 这个一级分类底下的每个二级分类）。
 *
 * ★ 与上面那排一级标签同一个按钮形状，但往里缩一档、左边一条细线：
 *   两排按钮长得一模一样的话，看不出哪排是「谁的子分类」。
 * ★ 用块级 flex（**不带** .btn-group —— 那个是 inline-flex，会把这一排挤到
 *   一级标签的同一行去）：这一行要单独占一行、能换行，窄屏上按钮多的时候
 *   才不会把页面撑宽。
 * ★ 只有当前一级分类有子分类时这个元素才显示（nav.js 的 renderSubTabs
 *   用 hidden 控制）—— 藏起来的时候不占任何高度，没子分类的分类跟以前一样。 */
.nav-subtabs {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-1);
  margin: 0 0 var(--sp-4);
  padding-inline-start: var(--sp-3);
  border-inline-start: 2px solid var(--border);
}

/*
 * 导航页开关关着的时候，登录页和书签页上的「前往导航页」入口不出现。
 *
 * 标记是**服务端**在 <html> 上写上的（第一次绘制之前就跑完了），
 * 所以不会出现「先看见入口、然后它消失」的那一闪。
 * 开关本身在服务端算（renderPage —— 导航页现在**每种语言一个开关**，
 * 判据是「请求猜到的那个语言开着 **或** 系统默认语言开着」），
 * 客户端再按**访客真正选过的语言**纠正一次（i18n/client.js 的
 * applyNavVisibility）—— 那个偏好只有浏览器知道，服务端拿不到。
 */
html[data-nav='off'] [data-nav-entry] {
  display: none;
}

.sidebar__close {
  display: none;
}

.nav {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.nav__item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  min-height: 2.5rem;
  padding: 0 var(--sp-3);
  border: 0;
  border-radius: var(--r-md);
  background: transparent;
  color: var(--fg-muted);
  font-size: 0.875rem;
  font-weight: 600;
  text-align: left;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}

.nav__item svg {
  width: 1.125rem;
  height: 1.125rem;
}

.nav__item span {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.nav__item:hover {
  background: var(--surface);
  color: var(--fg);
}

.nav__item[aria-current='page'] {
  background: var(--surface);
  color: var(--brand-strong);
  box-shadow: var(--shadow-sm);
}

.nav__item--dropping {
  background: var(--brand-soft);
  color: var(--brand-strong);
  outline: 2px dashed var(--brand);
  outline-offset: -2px;
}

.nav__label {
  padding: var(--sp-3) var(--sp-3) var(--sp-2);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--fg-muted);
}

.groups {
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 1;
}

.groups__head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding-right: var(--sp-1);
}

.groups__head .nav__label {
  flex: 1;
  padding-bottom: var(--sp-2);
}

.groups__list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0;
  padding: 0 var(--sp-1) 0 0;
  list-style: none;
  overflow-y: auto;
}

.group-row {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  border-radius: var(--r-md);
  transition: background var(--dur) var(--ease);
}

.group-row:hover {
  background: var(--surface);
}

.group-row[aria-current='page'] {
  background: var(--surface);
  box-shadow: var(--shadow-sm);
}

.group-row--dragging {
  opacity: 0.5;
}

.group-row__btn {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex: 1;
  min-width: 0;
  min-height: 2.5rem;
  padding: 0 var(--sp-2) 0 var(--sp-3);
  border: 0;
  border-radius: var(--r-md);
  background: transparent;
  color: var(--fg-muted);
  font-size: 0.875rem;
  font-weight: 600;
  text-align: left;
}

.group-row__btn span {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.group-row:hover .group-row__btn,
.group-row[aria-current='page'] .group-row__btn {
  color: var(--fg);
}

.group-row[aria-current='page'] .group-row__btn {
  color: var(--brand-strong);
}

.group-row__grip {
  width: 0.875rem;
  height: 1.125rem;
  color: var(--border-strong);
  cursor: grab;
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}

.group-row:hover .group-row__grip {
  opacity: 1;
}

.group-row__grip:active {
  cursor: grabbing;
}

.group-row__actions {
  display: none;
  gap: 0;
}

.group-row:hover .group-row__actions,
.group-row:focus-within .group-row__actions {
  display: flex;
}

.sidebar__foot {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--border);
}

/* 用户名/退出那个框固定在侧栏底部，不跟着滚。
 * 桌面端侧栏是 sticky + 100vh，内容多的话滚动的是 .groups__list（见上面），
 * .user 本来就可见；窄屏上整条一起滚，就得把它从滚动流里拿出来。
 *
 * 用 position: sticky 是不行的：它的包含块 .sidebar__foot 整体位于可视区
 * 之下时，sticky 会被包含块边界卡住，元素照样滚出视野。真正可靠的是
 * 让 .sidebar 变成 flex 列、中间放唯一的滚动容器、.user 作为不滚的兄弟节点。 */
.sidebar__body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

.sidebar__links {
  display: flex;
  gap: var(--sp-1);
}

.sidebar__links .nav__item {
  min-height: 2.25rem;
  font-size: 0.8125rem;
}

.user {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-2) var(--sp-2) var(--sp-3);
  border-radius: var(--r-md);
  background: var(--surface);
  border: 1px solid var(--border);
}

.user__meta {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  line-height: 1.3;
}

.user__name {
  font-size: 0.8125rem;
  font-weight: 650;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.user__email {
  font-size: 0.6875rem;
  color: var(--fg-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- 主区 --------------------------------------------------------------- */

.main {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 100vh;
}

.topbar {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: var(--topbar-h);
  padding: var(--sp-3) var(--sp-6);
  background: color-mix(in srgb, var(--bg) 85%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border);
}

.topbar__menu {
  display: none;
}

.search {
  position: relative;
  flex: 1;
  max-width: 34rem;
  display: flex;
  align-items: center;
}

.search svg {
  position: absolute;
  left: 0.75rem;
  width: 1.125rem;
  height: 1.125rem;
  color: var(--fg-muted);
  pointer-events: none;
}

.search input {
  width: 100%;
  min-height: 2.5rem;
  padding: 0 3rem 0 2.5rem;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-full);
  font-size: 0.875rem;
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
  appearance: none;
  -webkit-appearance: none;
}

.search input::-webkit-search-cancel-button {
  display: none;
}

.search input::placeholder {
  color: var(--fg-muted);
}

.search input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.search kbd {
  position: absolute;
  right: 0.625rem;
  padding: 1px 6px;
  border: 1px solid var(--border-strong);
  border-bottom-width: 2px;
  border-radius: 6px;
  background: var(--surface-2);
  color: var(--fg-muted);
  font-family: inherit;
  font-size: 0.6875rem;
  font-weight: 700;
}

.search__clear {
  position: absolute;
  right: 0.5rem;
  width: 1.75rem;
  height: 1.75rem;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: var(--r-full);
  background: none;
  color: var(--fg-muted);
  cursor: pointer;
}

.search__clear:hover {
  background: var(--surface-2);
  color: var(--fg);
}

.search__clear svg {
  position: static;
  width: 0.875rem;
  height: 0.875rem;
}

.topbar__actions {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-left: auto;
}

.segmented {
  display: flex;
  gap: 2px;
  padding: 2px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
}

.content {
  flex: 1;
  width: 100%;
  max-width: 90rem;
  padding: var(--sp-6);
}

.content__head {
  display: flex;
  align-items: flex-end;
  gap: var(--sp-4);
  margin-bottom: var(--sp-5);
}

.content__title {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  font-size: 1.375rem;
}

.content__sub {
  margin-top: 2px;
  font-size: 0.8125rem;
  color: var(--fg-muted);
}

.content__spacer {
  flex: 1;
}

.drop-target {
  border: 2px dashed var(--brand);
  border-radius: var(--r-lg);
  background: var(--brand-soft);
}

.bookmarks {
  display: grid;
  gap: var(--sp-4);
  grid-template-columns: repeat(auto-fill, minmax(15.5rem, 1fr));
  align-content: start;
}

.bookmarks--list {
  /* 必须是 minmax(0, 1fr)：1fr 等于 minmax(auto, 1fr)，而卡片里的
   * .card__url 是 white-space: nowrap 的长网址，它的最小内容宽度会把
   * 整条轨道顶到几百 px，撑破容器 —— 表现就是手机上「书签没居中」。 */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-2);
}

/*
 * 导航页的横列不受下面那两道全局规矩管：
 *   - .content 的 90rem 封顶（1440px 就到顶 → 1392px 内宽 → 正好 5 张卡）
 *     —— 导航页要的是「空间够就多排几张」，不要这个天花板；
 *   - 860px 那条硬写成 1 列（见 @media (max-width: 860px) 里的 .bookmarks）
 *     —— 侧栏在那档是抽屉、不占位，15.5rem 的卡片其实排得下三张；
 *     auto-fill 自己会数，过渡就是 3 → 2 → 1，不会跳过 2。
 *
 * 只动导航页：书签页和管理端的 .content 没人提，别跟着改。
 * body.nav-page 的选择器权重（0,2,1）压得住 .content 和 .bookmarks（0,1,0），
 * 所以不必插进那两条规则内部。
 */
body.nav-page .content {
  max-width: none;
}

body.nav-page .bookmarks {
  grid-template-columns: repeat(auto-fill, minmax(15.5rem, 1fr));
}

/* --- 书签卡片 ----------------------------------------------------------- */

.card {
  position: relative;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease),
    transform var(--dur) var(--ease);
  animation: card-in 300ms var(--ease) both;
}

@keyframes card-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
}

.card:hover {
  border-color: var(--brand);
  box-shadow: var(--shadow-md);
  transform: translateY(-2px);
}

.card:focus-within {
  border-color: var(--brand);
}

.card--dragging {
  opacity: 0.45;
}

.card__top {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
}

.card__main {
  flex: 1;
  min-width: 0;
}

.card__title {
  display: block;
  font-size: 0.9375rem;
  font-weight: 650;
  line-height: 1.35;
  color: var(--fg);
  text-decoration: none;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  word-break: break-word;
}

.card__title:hover {
  color: var(--brand-strong);
  text-decoration: none;
}

.card__url {
  margin-top: 2px;
  font-size: 0.75rem;
  color: var(--fg-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.card__desc {
  font-size: 0.8125rem;
  color: var(--fg-muted);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.card__foot {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-top: auto;
}

.card__foot .badge {
  min-width: 0;
}

.card__foot .badge span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.card__actions {
  display: flex;
  align-items: center;
  gap: 0;
  margin-left: auto;
}

.card__actions .icon-btn--sm {
  width: 1.75rem;
  height: 1.75rem;
}

.card__actions .icon-btn--sm svg {
  width: 0.9375rem;
  height: 0.9375rem;
}

/* 禁用的操作按钮（导航页上「已经收藏过」的那个收藏按钮）。
   禁用态得**看得出来** —— 不然用户点下去没反应，会以为按钮坏了。 */
.card__actions .icon-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* 禁用的按钮不该还有 hover 的反应（那个底色会让人以为还能点） */
.card__actions .icon-btn:disabled:hover {
  background: transparent;
  color: var(--fg-muted);
}

.card__pin {
  color: var(--brand);
}

/* 收藏星标：未收藏时也要看得见、点得到。
   以前未收藏是 opacity: 0，靠 .card:hover 才显出来 —— 桌面端还能用
   hover 找到它，手机上没有 hover，那个位置就永远点不到，等于没法收藏。
   现在常显，用「空心 vs 实心」区分状态。 */
.card__pin[aria-pressed='false'] {
  color: var(--fg-muted);
  opacity: 1;
}

/* 空心星：未收藏时不该长得像已收藏 */
.card__pin[aria-pressed='false'] .icon--star {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.card__pin[aria-pressed='true'] .icon--star {
  fill: currentColor;
  stroke: none;
}

/* 公告卡：长得像书签，但不是书签 —— 没有链接、没有操作按钮，
 * 点一下展开/收起全文。压在所有书签前面，任何视图都显示。
 *
 * ★ 大小跟着书签卡走，不自己撑高一行：
 *   网格一行的高度由整行最高的卡片决定，公告卡是这一行的一格 ——
 *   它只负责「不超过书签卡」，多出来的高度分给正文区（见下面的 .card__top），
 *   底部条再用 margin-top: auto 贴底。它要是比书签卡高，整行会被它撑开，
 *   书签卡跟着被拉长 —— 那就不是「按书签的大小」了。 */
.card--notice {
  background: var(--accent-soft);
  border-color: var(--brand);
  /* 卡片被同行书签拉高、正文塞不下时，最后一行在卡片内被剪掉而不是溢出去 */
  overflow: hidden;
  /* ★ 卡片的高度**不来自自己的内容**：一行的高度只由书签卡决定，
     公告卡是这一行的另一格，跟着拉伸。这样「公告卡与书签卡同高」是确定的，
     正文能放几行就由 notice-card.js 的 fitNoticeCards 按可用高度算。
     没有这一条的话，长公告会把整行撑高（书签卡跟着被拉长）——
     那就成了「按公告的大小」而不是「按书签的大小」。 */
  contain: size;
  /* 一行里只有公告（书签页/待办页一条都没有）时，不能塌成 0 高 */
  min-height: 7rem;
}

/* 这两种情况高度要跟着内容走，不能做 size containment：
 *   - 展开态：用户要看全文；
 *   - 列表视图：一张卡占一行，行高本来就由内容决定。 */
.card--notice.is-open,
.bookmarks--list .card--notice {
  contain: none;
}

/* 窄屏是一列一张卡（.bookmarks 在 860px 那条里硬写成 1 列）：一张卡占一行，
 * 没有「同一行的书签卡」可以参照，公告卡的高度只能由自己决定。
 * 这时再留着 size containment，卡片就被压在 min-height（7rem）上 ——
 * 正文区只剩一行的高度，手机上只能看到半句话。所以这一档退回内容高度，
 * 正文按 CSS 的兜底行数（2 行）显示，想看全文点「展开全文」。 */
@media (max-width: 860px) {
  .card--notice {
    contain: none;
    min-height: 0;
  }
}

.card__notice-icon {
  display: grid;
  place-items: center;
  flex: none;
  width: 2rem;
  height: 2rem;
  border-radius: 0.6rem;
  background: var(--brand);
  color: #fff;
}

.card__notice-icon svg {
  width: 1.125rem;
  height: 1.125rem;
}

/* 关闭按钮：卡片右上角一个 ×。公告是「可以被读者划掉」的，
   所以它必须一直可见、可点 —— 不能跟书签的操作按钮一样 hover 才出现。 */
.card__notice-close {
  position: absolute;
  top: var(--sp-2);
  right: var(--sp-2);
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  border: 0;
  border-radius: 0.5rem;
  background: transparent;
  color: var(--fg-muted);
  cursor: pointer;
}

.card__notice-close:hover {
  background: var(--surface-2);
  color: var(--fg);
}

.card__notice-close svg {
  width: 1rem;
  height: 1rem;
}

/* 列表视图下卡片是一行 nowrap：.card__foot（分组标签 + 展开按钮）就停在
 * 行的最右端，正好是关闭按钮绝对定位的那个角 —— 实测两者重叠 20px，
 * 「公告」标签被压在 X 底下。窄屏时底部条折到第二行，所以只在宽屏露馅。
 * 这里让关闭按钮回归常规流、排在行末，整个列表视图里位置也就统一了。 */
.bookmarks--list .card--notice .card__notice-close {
  position: static;
  order: 5;
  align-self: center;
  flex: none;
}

/* 正文区吃掉卡片里多出来的高度：卡片被同行书签拉高时，空隙留在正文区里，
 * 而不是把底部条推上去。min-height: 0 是 flex 子项能真正收缩的前提 ——
 * 少了它，内容的最小高度会把卡片重新撑开。 */
.card--notice .card__top {
  flex: 1 1 auto;
  min-height: 0;
  /* 主体撑满正文区的高度：fitNoticeCards 量的是 .card__main 的高度，
     它得等于「卡片除去底部条之后还剩多少」 */
  align-items: stretch;
  /* 关闭按钮占着右上角，正文别顶到它下面去 */
  padding-right: 1.75rem;
}

/* ★ 不要给 .card__main 加 display: flex：那样正文变成 flex 子项，
   display: -webkit-box 会被「blockify」成 flow-root，-webkit-line-clamp
   直接失效（正文就只会被 overflow 硬切，最后一行切一半）。 */
.card--notice .card__main {
  min-height: 0;
}

/* 正文上面原本顶着一个蓝色「公告」标题，那个 span 和它的
 * .card__title--text 修饰类已经删掉了：
 * 同一个词在一张卡上出现两次（标题 + 分组标签），
 * 而且标题位是给书签标题用的，公告不需要。
 * 原来这里还有 margin-top: 2px，是用来把正文和标题隔开的，
 * 标题没了就成了凭空多出来的上边距，一并去掉。 */

.card__notice-text {
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--fg);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  display: -webkit-box;
  /* 兜底行数（列表视图、以及脚本还没跑的那一瞬用）。
   * 网格视图里 fitNoticeCards 会按卡片实际能给的高度覆盖它 ——
   * 所以这里不追求「刚好」，只保证不跑版。 */
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.card--notice.is-open .card__notice-text {
  -webkit-line-clamp: unset;
}

/* 这里曾经写 .bookmarks--list .card--notice .card__top { flex: 1 0 100% }，
 * 结果列表视图下「公告」标签跑到卡片外面去了：
 * 桌面那条行是 nowrap，主体 basis 吃满 100% 还不 shrink，
 * .card__foot 没地方去就被挤出卡外（实测溢出 143px）。
 * 窄屏折行那条 @media (max-width: 860px) 里已经有了，桌面就别再写一遍，
 * 让公告和普通书签一样吃 `flex: 1`（可收缩）。 */

/* 底部条（「公告」徽标 + 展开全文）贴底：
 * .card__foot 的基础样式就是 margin-top: auto —— 把卡片里多余的高度全吃掉。
 * 这里以前覆盖成固定间距（var(--sp-2)），于是卡片被同行书签拉高时，
 * 标签和按钮停在半空中、下面空一截。别再覆盖它。 */

/* ------------------------------------------------ 管理端公告面板 */

.notice-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  flex-wrap: wrap;
}

.notice-head__actions {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}

.notice-spacer {
  flex: 1;
}

/* 这里曾经是「保存时自动翻译」那个复选框的样式，开关去掉后一并清了 */

.notice-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: var(--sp-4);
}

.notice-list,
#noticeList > * + * {
  margin-top: var(--sp-2);
}

.notice-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
}

.notice-row__main {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
  flex: 1;
  /*
   * ★ 二级分类那一行多一个「属于：…」的角标，名字会被挤成「国…」——
   *   外层是 flex-wrap，给主内容一个**首选宽度**（15rem ≈ 名字 + 两个角标），
   *   一行放不下时右边的操作按钮自己换行，而不是先牺牲名字（它有 ellipsis，
   *   被牺牲时是最先看不见的那个）。
   */
  flex-basis: 15rem;
}

/* 二级分类（导航页管理面板）。
 *
 * 缩进一个档位 + 左边一条细线：子分类在列表里**紧跟在自己的父下面**
 * （navCategoriesInOrder），这样读下来是「父 → 它底下的」，而不是
 * 一堆平级条目的顺序。
 *
 * ★ 用逻辑属性（inline-start），不用 margin-left：站点有 RTL 的可能，
 *   物理方向会把缩进画到错误的那一边。 */
.nav-cat--child {
  margin-inline-start: var(--sp-6);
  border-inline-start: 2px solid var(--border);
}

.notice-row__preview {
  font-size: 0.875rem;
  color: var(--fg-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.notice-row__meta {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  font-size: 0.75rem;
  color: var(--fg-muted);
}

.notice-row__actions {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
}

/* 「立即翻译」是流式的：一个语言回来就填一个框，别的还在跑。
   所以框有三种中间状态，得让管理员一眼看出哪些已经好了、哪些还在飞。 */
#noticeTexts textarea[data-state='pending'] {
  opacity: 0.55;
  border-style: dashed;
}

#noticeTexts textarea[data-state='pending']::placeholder {
  color: var(--fg-muted);
  font-style: italic;
}

#noticeTexts textarea[data-state='done'] {
  border-color: var(--accent);
}

#noticeTexts textarea[data-state='error'] {
  border-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 6%, transparent);
}

.notice-llm {
  border-top: 1px solid var(--border);
  padding-top: var(--sp-4);
}

.card__grip {
  width: 0.875rem;
  color: var(--border-strong);
  cursor: grab;
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}

.card:hover .card__grip,
.card:focus-within .card__grip {
  opacity: 1;
}

.card__grip:active {
  cursor: grabbing;
}

/* 列表视图 */
.bookmarks--list .card {
  flex-direction: row;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
}

.bookmarks--list .card__top {
  align-items: center;
  flex: 1;
  min-width: 0;
}

.bookmarks--list .card__main {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
}

.bookmarks--list .card__title {
  -webkit-line-clamp: 1;
  /* 原来是 flex: none（0 0 auto），标题不肯收缩，长标题就压到
   * 右边分组徽章上面 —— 实测最长的一条盖住徽章 157px。
   * 可收缩后由 line-clamp 截断，配合 min-width: 0 才能真正缩到内容以下。 */
  flex: 0 1 auto;
  min-width: 0;
  max-width: 22rem;
}

.bookmarks--list .card__url {
  margin-top: 0;
  flex: 1;
  min-width: 0;
}

.bookmarks--list .card__desc {
  display: none;
}

.bookmarks--list .card__foot {
  margin-top: 0;
  flex: none;
  order: 3;
}

.bookmarks--list .card__actions {
  order: 4;
}

/* 手机上列表视图横向放不下：favicon + 标题 + 网址 + 分组徽章 +
   三个操作按钮，固定部分就吃掉将近 200px，标题只剩 50 多 px，
   截成「示…」，而网址的宽度是 0。
   让标题和网址占满整行，分组徽章和操作按钮折到第二行。 */
@media (max-width: 860px) {
  .bookmarks--list .card {
    flex-wrap: wrap;
    row-gap: var(--sp-2);
  }

  .bookmarks--list .card__top {
    flex: 1 0 100%;
  }

  /* 徽章对齐到标题那一行的起点（跳过 favicon 和它的间距） */
  .bookmarks--list .card__foot {
    margin-left: calc(1.75rem + var(--sp-3));
  }
}

@media (pointer: coarse) {
  .card__actions,
  .group-row__actions {
    opacity: 1;
  }
}

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

.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-8) var(--sp-4);
  text-align: center;
  border: 1px dashed var(--border-strong);
  border-radius: var(--r-lg);
  background: var(--surface);
}

.empty__icon {
  display: grid;
  place-items: center;
  width: 3.5rem;
  height: 3.5rem;
  border-radius: var(--r-full);
  background: var(--brand-soft);
  color: var(--brand-strong);
}

.empty__icon svg {
  width: 1.75rem;
  height: 1.75rem;
}

.empty__title {
  font-size: 1rem;
}

.empty__text {
  max-width: 26rem;
  font-size: 0.875rem;
  color: var(--fg-muted);
}

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

.skeleton {
  padding: var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
}

.skeleton__line {
  height: 0.75rem;
  border-radius: var(--r-full);
  background: linear-gradient(90deg, var(--surface-2) 25%, var(--border) 50%, var(--surface-2) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.4s linear infinite;
}

.skeleton__line + .skeleton__line {
  margin-top: var(--sp-2);
}

.skeleton__line--w60 {
  width: 60%;
}

.skeleton__line--w40 {
  width: 40%;
}

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

.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* ==========================================================================
   颜色选择
   ========================================================================== */

.colors {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
}

.color-chip {
  position: relative;
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border: 2px solid transparent;
  border-radius: var(--r-full);
  background: transparent;
  transition: border-color var(--dur) var(--ease), transform var(--dur) var(--ease);
}

.color-chip i {
  display: block;
  width: 1.25rem;
  height: 1.25rem;
  border-radius: var(--r-full);
  background: var(--g);
}

.color-chip:hover {
  transform: scale(1.1);
}

.color-chip[aria-pressed='true'] {
  border-color: var(--g);
}

.color-chip[aria-pressed='true']::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: var(--r-full);
  box-shadow: inset 0 0 0 2px var(--surface);
}

/* ==========================================================================
   登录 / 注册
   ========================================================================== */

.auth {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
  min-height: 100vh;
}

.auth__aside {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--sp-8);
  padding: var(--sp-8);
  background: linear-gradient(155deg, var(--brand) 0%, var(--g-orange) 55%, #b45309 100%);
  color: #fff;
}

.auth__aside .brand__mark {
  background: rgb(255 255 255 / 0.2);
  box-shadow: none;
}

.auth__aside .brand__name,
.auth__aside .brand__sub {
  color: #fff;
}

.auth__pitch h1 {
  font-size: clamp(1.75rem, 3vw, 2.5rem);
  letter-spacing: -0.03em;
}

.auth__pitch p {
  max-width: 30rem;
  margin-top: var(--sp-4);
  font-size: 1rem;
  color: rgb(255 255 255 / 0.85);
}

.auth__features {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  margin: var(--sp-6) 0 0;
  padding: 0;
  list-style: none;
}

.auth__features li {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  font-size: 0.9375rem;
  color: rgb(255 255 255 / 0.92);
}

.auth__features svg {
  width: 1.25rem;
  height: 1.25rem;
  margin-top: 2px;
  flex: none;
  opacity: 0.9;
}

.auth__note {
  font-size: 0.75rem;
  color: rgb(255 255 255 / 0.7);
}

.auth__main {
  display: grid;
  place-items: center;
  padding: var(--sp-6);
}

.auth__card {
  width: min(25rem, 100%);
}

.auth__card h2 {
  font-size: 1.5rem;
}

.auth__card > p {
  margin-top: var(--sp-1);
  font-size: 0.875rem;
  color: var(--fg-muted);
}

.auth__form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  margin-top: var(--sp-6);
}

.auth__switch {
  margin-top: var(--sp-5);
  font-size: 0.875rem;
  color: var(--fg-muted);
  text-align: center;
}

.auth__switch button {
  border: 0;
  background: none;
  padding: 0;
  color: var(--accent);
  font-weight: 650;
  text-decoration: underline;
  text-underline-offset: 3px;
}

/*
 * 登录页上那个「导航页」入口。
 *
 * 它跟上面那几个小字链接不是一类东西 ——「还没有账号？」「忘记密码？」是登录流程里的
 * 脚注，这个是「不登录也能看大家收藏的站点」的另一条路。所以给它一条分隔线，
 * 跟脚注分开；按钮本身是整宽的（btn--block），比小字链接显眼得多。
 */
.auth__naventry {
  margin-top: var(--sp-4);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--border);
}

.auth__summary {
  display: flex;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--danger);
  border-radius: var(--r-md);
  background: var(--danger-soft);
  color: var(--fg);
  font-size: 0.8125rem;
}

.auth__summary svg {
  width: 1.125rem;
  height: 1.125rem;
  flex: none;
  margin-top: 1px;
  color: var(--danger);
}

.auth__summary ul {
  margin: 0;
  padding-left: 1.1rem;
}



/* 手机端专属的品牌条。侧栏在 ≤900px 是整块 display:none 的，
   结果页面上只剩一个表单卡片 —— 没有 logo，也没有一句话说明这是什么。
   这里在表单上方补一条紧凑的，只放 logo + 名称 + 一句主张。 */
.auth__mhead {
  display: none;
}

@media (max-width: 900px) {
  .auth {
    grid-template-columns: 1fr;
  }

  .auth__aside {
    display: none;
  }

  /* 表单卡和品牌条现在是同一个网格里的两行。默认 align-content 会把两行
     拉满整个高度、再各自居中，两者被撑得很远 —— 要让它们作为一个整体居中。 */
  .auth__main {
    align-content: center;
    row-gap: var(--sp-5);
  }

  .auth__mhead {
    display: block;
    width: 100%;
    max-width: 22rem;
    margin: 0 auto;
    text-align: center;
  }

  /* .brand 本来给侧栏用、flex: 1 会横向撑开，这里改成居中的定宽 */
  .auth__mhead .brand {
    display: inline-flex;
    flex: none;
    justify-content: center;
    text-align: left;
  }

  .auth__mhead-title {
    margin: var(--sp-3) 0 0;
    font-size: 1.125rem;
    font-weight: 700;
    line-height: 1.35;
    letter-spacing: -0.01em;
    color: var(--fg);
  }
}

/* ==========================================================================
   响应式
   ========================================================================== */

@media (max-width: 1024px) {
  :root {
    --sidebar-w: 15rem;
  }
}

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

  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    width: min(20rem, 86vw);
    transform: translateX(-102%);
    transition: transform 260ms var(--ease);
    box-shadow: var(--shadow-lg);
  }

  .sidebar[data-open='true'] {
    transform: translateX(0);
  }

  /* 手机上：侧栏整条一起滑，但用户名/退出那个框固定在底部。
     滚动交给 .sidebar__body，.user 作为不参与滚动的兄弟节点留在底部。 */
  .sidebar {
    /* 100vh 会把手机地址栏的高度也算进去，底部掉出可视区 */
    height: 100vh;
    height: 100dvh;
    overflow: hidden;
  }

  .sidebar__body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    /* 内容滚到边缘时不要被橡皮筋带着晃 */
    overscroll-behavior: contain;
  }

  .groups {
    flex: 0 0 auto;
  }

  .groups__list {
    overflow-y: visible;
  }

  .sidebar__close {
    display: inline-flex;
  }

  .topbar__menu {
    display: inline-flex;
  }

  .topbar {
    padding: var(--sp-3) var(--sp-4);
    gap: var(--sp-2);
  }

  .search kbd {
    display: none;
  }

  /* 快捷键提示在这一档已经隐藏，右边那块内边距就白留了。 */
  .search input {
    padding-right: 1rem;
  }

  .content {
    padding: var(--sp-4);
  }

  .content__head {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--sp-2);
  }

  /* 这里原来把 .content__head .btn 做成右下角的浮动圆钮（窄屏的第二份
     「新建」）。那个按钮删掉了 —— 页面头里只剩一个「新建书签」，
     两处并存时它看着像重复。 */

  .bookmarks {
    grid-template-columns: minmax(0, 1fr);
  }

  /* 视图切换留着：只有两个小按钮，手机上也得能换视图。
     排序下拉仍然隐藏 —— 顶栏放不下，而且手机上很少用。 */
  .topbar__actions .select--bare {
    display: none;
  }
}

@media (max-width: 640px) {
  /* 窄到一定程度时，让搜索框换到第二行去独占一整行。
     跟品牌、语言按钮挤在同一条横排时，它会被压到只剩 100px 出头
     （402px 宽时实测 103px），而输入框左 40px 的内边距就把里面
     可用的宽度吃光了 —— 放大镜图标旁边几乎一个字都放不下，
     提示文字被裁成两三个字。
     断点定在 640 而不是 860：860 以上时一行放得下
     （768px 时搜索框还能分到 358px），而把 max-width 留在那儿的话，
     搜索框的 flex-basis 会被截到 34rem，反而把品牌挤成 27px 宽。 */
  .topbar {
    flex-wrap: wrap;
  }

  .search {
    order: 3;
    flex: 1 0 100%;
    max-width: none;
  }

  /* 顶栏那个「回到我的书签」在窄屏放不下：按钮 129px + 语言 101px + 主题 40px
     = 286px，剩给品牌的只有 76px —— 「WalkLink 导航页」被挤成三行。
     窄屏上只留图标（title / aria-label 还在），位置让给品牌。 */
  #toBookmarks span {
    display: none;
  }

  /* 「导航页」那三个字在窄屏上要收一点，否则品牌那一行放不下：
     402px 时品牌只剩 162px，扣掉 36px 的图标，文字那栏 118px ——
     「WalkLink」67px 加「导航页」放不下，于是「导航页」被挤到第二行，
     品牌从 59px 高涨到 82px。
     窄屏上字号收小一号、去掉那条竖线，但**颜色和字重不变**
     （还是深色、还是 600 —— 显眼这就是它要的效果，不能因为窄就退回去）。 */
  .brand__suffix {
    margin-left: 0.35rem;
    padding-left: 0;
    border-left: 0;
    font-size: 0.8125rem;
  }
}

@media (max-width: 480px) {
  .topbar__actions .btn--primary {
    display: none;
  }
}

@media print {
  .sidebar,
  .topbar,
  .card__actions {
    display: none;
  }
}

.input--code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 1.125rem;
  font-weight: 600;
  letter-spacing: 0.4em;
  text-align: center;
}

.input-with-button {
  display: flex;
  gap: var(--sp-2);
  align-items: stretch;
}

.input-with-button .input {
  flex: 1;
  min-width: 0;
}

.input-with-button__btn {
  flex: none;
  white-space: nowrap;
}

.input-with-button__btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

/* --- 拖拽高亮 ----------------------------------------------------------- */

.dropzone--over {
  outline: 2px dashed var(--brand);
  outline-offset: -2px;
  background: var(--brand-soft);
  color: var(--brand-strong);
}

/* --- 管理模块入口 ------------------------------------------------------- */

.sidebar__admin {
  padding-top: var(--sp-1);
  border-top: 1px solid var(--border);
}

.sidebar__admin[hidden] {
  display: none;
}

.sidebar__admin .nav__item[aria-current='page'] {
  background: var(--accent);
  color: var(--on-accent);
}

.sidebar__admin .nav__item[aria-current='page'] .count {
  background: rgb(255 255 255 / 0.25);
  color: var(--on-accent);
}

#adminBadge {
  background: var(--danger-soft);
  color: var(--danger);
}

/* --- 未验证提示条 ------------------------------------------------------- */

.verify-banner {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-bottom: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  background: var(--brand-soft);
  border: 1px solid color-mix(in srgb, var(--brand) 35%, transparent);
  border-radius: var(--r-md);
  font-size: 0.875rem;
}

.verify-banner[hidden] {
  display: none;
}

.verify-banner__icon {
  display: grid;
  place-items: center;
  flex: none;
  width: 2rem;
  height: 2rem;
  border-radius: var(--r-full);
  background: var(--brand);
  color: var(--on-brand);
}

.verify-banner__icon svg {
  width: 1.125rem;
  height: 1.125rem;
}

.verify-banner__body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  line-height: 1.4;
}

.verify-banner__body span {
  font-size: 0.8125rem;
  color: var(--fg-muted);
}

/* --- 待验证全屏卡 ------------------------------------------------------- */

.gate {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: grid;
  place-items: center;
  padding: var(--sp-5);
  background: var(--bg);
  overflow-y: auto;
}

.gate[hidden] {
  display: none;
}

.gate__card {
  width: min(30rem, 100%);
  padding: var(--sp-8) var(--sp-6);
  text-align: center;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg);
}

.gate__icon {
  display: grid;
  place-items: center;
  width: 3.5rem;
  height: 3.5rem;
  margin: 0 auto var(--sp-4);
  border-radius: var(--r-full);
  background: var(--brand-soft);
  color: var(--brand-strong);
}

.gate__icon svg {
  width: 1.75rem;
  height: 1.75rem;
}

.gate__title {
  font-size: 1.25rem;
}

.gate__text {
  margin-top: var(--sp-2);
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--fg-muted);
}

.gate__link {
  margin-top: var(--sp-5);
  padding: var(--sp-4);
  background: var(--surface-2);
  border: 1px dashed var(--border-strong);
  border-radius: var(--r-md);
  text-align: left;
}

.gate__link[hidden] {
  display: none;
}

.gate__linkLabel {
  font-size: 0.75rem;
  color: var(--fg-muted);
}

.gate__linkUrl {
  display: block;
  margin: var(--sp-2) 0 var(--sp-3);
  font-size: 0.75rem;
  line-height: 1.5;
  word-break: break-all;
}

.gate__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--sp-2);
  margin-top: var(--sp-6);
}

.gate__hint {
  min-height: 1.25rem;
  margin-top: var(--sp-3);
  font-size: 0.75rem;
  color: var(--fg-muted);
}

.gate__hint:empty {
  min-height: 0;
}

/* --- 管理模块 ----------------------------------------------------------- */

.stat-grid {
  display: grid;
  gap: var(--sp-4);
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
}

.stat-card {
  padding: var(--sp-5);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
}

.stat-card__value {
  font-size: 1.875rem;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

.stat-card__label {
  margin-top: var(--sp-1);
  font-size: 0.8125rem;
  color: var(--fg-muted);
}

.stat-card--warn .stat-card__value {
  color: var(--brand-strong);
}

.user-table {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.user-row {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  transition: border-color var(--dur) var(--ease);
}

.user-row:hover {
  border-color: var(--brand);
}

.user-row__main {
  flex: 1;
  min-width: 0;
}

.user-row__name {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: 0.9375rem;
  font-weight: 650;
}

.user-row__meta {
  margin-top: 2px;
  font-size: 0.75rem;
  color: var(--fg-muted);
}

.user-row__actions {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  flex: none;
}

.tag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 1px 8px;
  border-radius: var(--r-full);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  white-space: nowrap;
}

.tag--admin {
  background: var(--accent);
  color: var(--on-accent);
}

.tag--disabled {
  background: var(--danger-soft);
  color: var(--danger);
}

.tag--unverified {
  background: var(--brand-soft);
  color: var(--brand-strong);
}

.tag--ok {
  background: color-mix(in srgb, var(--success) 16%, transparent);
  color: var(--success);
}

.detail-section {
  margin-bottom: var(--sp-5);
}

.detail-section__title {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-bottom: var(--sp-2);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-muted);
}

.detail-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.detail-list li {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  background: var(--surface-2);
  border-radius: var(--r-sm);
  font-size: 0.8125rem;
}

.detail-list .detail-list__url {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--fg-muted);
}

.detail-list .badge {
  flex: none;
}

/* --- SMTP 设置 --------------------------------------------------------- */

.settings-card {
  max-width: 46rem;
  padding: var(--sp-6);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
}

/* 导航页面板的三块子页面切换条（语言与导入导出 / 分类 / 站点）。
 *
 * ★ 自己写 display / flex-wrap，**不带** .btn-group：那个类在样式表里排得更靠后
 *   而且是 inline-flex、不换行 —— 三个中文标签在窄屏上会横向溢出
 *   （跟 .nav-subtabs 那次是同一个坑）。
 * ★ 给它自己一个下边距：卡片之间本来就是紧挨着的（.settings-card 没有外边距，
 *   靠边框分开），这一条要是也贴着第一张卡片，会看起来像那张卡片的标题栏。 */
.nav-pages {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  margin-bottom: var(--sp-4);
}

/* 网站设置：按语言分开的网站名 —— 一行一个语言 */

.name-fields {
  display: grid;
  /* 最小轨道取 min(18rem, 100%)：只写 18rem（288px）的话，容器比它窄时
     grid 的最小轨道是硬的、压不下去，整块横向溢出（极窄屏上最明显）。
     取两者小的那个，窄容器里轨道跟着容器走。 */
  grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
  gap: var(--sp-2) var(--sp-4);
}

.name-field {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

/* 语言标签的宽度固定：五种语言的名字长度差得远（英文界面下的
   "Traditional Chinese" 最长），不固定的话输入框就会跟着标签的宽度
   一格一格地跳。放不下就截断，不让它折行 —— 折行会把那一行撑高。 */
.name-field__lang {
  flex: 0 0 7rem;
  font-size: 0.8125rem;
  color: var(--fg-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* 网站设置：图标预览 + 操作按钮 */

.logo-field {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-4);
}

.logo-field__preview {
  display: grid;
  place-items: center;
  flex: none;
  width: 4rem;
  height: 4rem;
  border-radius: var(--r-md);
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--fg-muted);
  overflow: hidden;
}

.logo-field__preview img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.logo-field__preview svg {
  width: 1.75rem;
  height: 1.75rem;
}

/* 标签图标本来就是小方块，预览也用小一号的格子 */
.logo-field__preview--sm {
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--r-sm);
}

.logo-field__preview--sm svg {
  width: 1.25rem;
  height: 1.25rem;
}

.logo-field__side {
  min-width: 0;
  flex: 1;
}

.logo-field__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-top: var(--sp-3);
}

/* 窄屏改成竖排，否则预览框和文字会挤在一起 */
@media (max-width: 560px) {
  .logo-field {
    flex-direction: column;
    align-items: stretch;
  }
}

.settings-status {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-bottom: var(--sp-5);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-md);
  font-size: 0.8125rem;
  line-height: 1.5;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--fg-muted);
}

.settings-status[data-state='ok'] {
  background: color-mix(in srgb, var(--success) 12%, transparent);
  border-color: color-mix(in srgb, var(--success) 32%, transparent);
  color: var(--fg);
}

.settings-status[data-state='warn'] {
  background: var(--brand-soft);
  border-color: color-mix(in srgb, var(--brand) 32%, transparent);
  color: var(--fg);
}

.settings-status svg {
  width: 1.125rem;
  height: 1.125rem;
  flex: none;
}

.settings-status[data-state='ok'] svg {
  color: var(--success);
}

.settings-status[data-state='warn'] svg {
  color: var(--brand-strong);
}

.settings-status strong {
  color: var(--fg);
}

.settings-grid {
  display: grid;
  gap: var(--sp-4);
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
}

.settings-toggle {
  margin-top: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}

.settings-toggle input {
  width: 1.125rem;
  height: 1.125rem;
}

/* 数据源面板：驱动二选一。用可点整行的 label，而不是裸 radio，
   否则只有那个小圆点能点，「选 SQLite」这件事本身不好按。 */
.db-driver {
  display: grid;
  gap: var(--sp-2);
  margin-top: var(--sp-2);
}

.db-driver__option {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  cursor: pointer;
  font-size: 0.875rem;
}

.db-driver__option:hover {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
}

/* 选中态靠 accent-color 就够区分了：一个 accent 色的 radio 加上边框变色，
   不用整块底色 —— 大块底色在深色主题下压过正文，可读性反而更差。 */
.db-driver__option:has(input:checked) {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 8%, var(--surface-2));
}

.db-driver__option input {
  width: 1.125rem;
  height: 1.125rem;
  margin: 0;
  accent-color: var(--accent);
  flex: none;
}

.db-driver__option b {
  display: block;
  font-weight: 600;
}

.db-driver__option i {
  display: block;
  margin-top: 2px;
  font-style: normal;
  font-size: 0.8125rem;
  color: var(--fg-muted);
}

/* 「切换不搬数据」的警示。用与 settings-status 同一套配色，
   两处警示看起来是同一件事：不是报错，是需要人来做决定。 */
.db-warning {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2);
  margin-top: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-md);
  background: var(--brand-soft);
  border: 1px solid color-mix(in srgb, var(--brand) 32%, transparent);
  color: var(--fg);
  font-size: 0.8125rem;
  line-height: 1.5;
}

.db-warning[hidden] {
  display: none;
}

.db-warning svg {
  width: 1.125rem;
  height: 1.125rem;
  flex: none;
  color: var(--brand-strong);
}

.db-warning strong {
  display: block;
  color: var(--fg);
}

.db-warning p {
  margin: 4px 0 0;
  color: var(--fg-muted);
}

/* 测试连接的结果行。data-state 由 admin.js 写，跟 settings-status 一样两态。 */
#dbTestStatus[data-state='ok'] {
  color: var(--success);
}

#dbTestStatus[data-state='warn'] {
  color: var(--brand-strong);
}

.scope-icon {
  display: grid;
  place-items: center;
  flex: none;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--r-md);
  background: var(--brand-soft);
  color: var(--brand-strong);
}

.scope-icon svg {
  width: 1.375rem;
  height: 1.375rem;
}

.groups__empty {
  padding: var(--sp-2) var(--sp-3);
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--fg-muted);
}

.bookmarks[aria-busy='true'] {
  pointer-events: none;
}




/* ---------- 语言菜单 ---------- */
.lang-menu {
  position: fixed;
  z-index: 90;
  min-width: 11rem;
  padding: var(--sp-2);
  display: grid;
  gap: 2px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-lg);
}
.lang-menu[hidden] { display: none; }
.lang-menu__title {
  padding: var(--sp-1) var(--sp-2);
  font-size: 0.75rem;
  color: var(--fg-muted);
}
.lang-menu__item {
  display: block;
  width: 100%;
  padding: var(--sp-2) var(--sp-3);
  font: inherit;
  font-size: 0.875rem;
  text-align: left;
  color: var(--fg);
  background: none;
  border: 0;
  border-radius: var(--r-sm);
  cursor: pointer;
}
.lang-menu__item:hover { background: var(--surface-2); }
.lang-menu__item.is-active { background: var(--accent-soft); color: var(--accent-strong); font-weight: 600; }

/* ---------- 语言切换按钮 ----------
 * 早先只有一个没有文字的地球图标，看不出是干什么的 —— 现在把当前语言
 * 写在按钮上（한국어 / 中文 / English / …），一眼就知道是语言、也知道现在选的是什么。 */
.lang-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  font: inherit;
  font-size: 0.8125rem;
  line-height: 1.2;
  color: var(--fg);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  cursor: pointer;
  white-space: nowrap;
}
.lang-chip:hover { background: var(--surface-2); }
.lang-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.lang-chip[aria-expanded='true'] {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent-strong);
}
.lang-chip__icon { width: 16px; height: 16px; flex: none; }

/* 侧栏底部的撑脚。aside 是 justify-content: space-between，
 * 底部得有一个块，.auth__pitch 才会留在中间而不是掉到底部。
 * 这里原来放的是公司署名；署名去掉后这个块留着占位 ——
 * 高度跟原来那一行差不多，布局不跟着塌。 */
.auth__foot { min-height: 1.25rem; }

/* 侧栏底部：语言胶囊独占一行。
 * 它原本和 导入/导出/主题 并排在同一行，而那一行不换行，胶囊宽度又随
 * 语言名变化（简体中文 101px，更长的更宽），于是整行溢出、按钮被裁掉。 */
.lang-row { display: flex; }
.lang-row .lang-chip { flex: none; margin-bottom: var(--sp-2); }

/* 允许换行：日文「インポート/エクスポート」比中文「导入/导出」宽，
 * 侧栏 287px 装不下就会把「导出」挤出可视区。装得下时仍是原来的单行排布。 */
.sidebar__links { flex-wrap: wrap; }

/* 登录/注册/找回密码这几页没有工具栏，按钮贴在卡片右上角 */
.auth__card { display: flex; flex-direction: column; }
.lang-chip--card { align-self: flex-end; margin-bottom: var(--sp-3); }


/* ---------- 管理端语言选择 ---------- */
.lang-grid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-top: var(--sp-2);
}
.lang-choice {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  font: inherit;
  font-size: 0.875rem;
  color: var(--fg);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  cursor: pointer;
}
.lang-choice:hover { border-color: var(--accent); }
.lang-choice.is-active {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent-strong);
  font-weight: 600;
}
/*
 * 站点语言那五行只是**展示**：能点的只有里面那个按钮。
 *
 * 它们借了 .lang-choice 的样子（那是给**可点的**语言卡片用的），所以指针会
 * 变手型、悬停会变边框色 —— 看着整行可点，点行却什么也不会发生（只有按钮
 * 管用）。看着能点而点不动，比明摆着不可点更让人上火，所以单独一个类把
 * 这两样收掉（这两条要写在 .lang-choice 那两条**后面**才压得住）。
 */
.site-lang-row { cursor: default; }
.site-lang-row:hover { border-color: var(--border); }
.settings-actions {
  display: flex;
  gap: var(--sp-2);
  margin-top: var(--sp-5);
}

/* 「测试连接」「保存设置」「发送测试邮件」进行中的圈圈。它们跟按钮是**兄弟**
   而不是子节点：放进按钮里就得用 textContent / innerHTML 换内容，那会把按钮里
   那个 data-i18n 的 span 冲掉，按钮从此不再跟着语言走。
   flex 布局下 align-items 默认 stretch 会把圈圈拉高，所以显式居中。 */
#dbTestSpinner,
#smtpSaveSpinner,
#smtpTestSpinner {
  align-self: center;
  flex: none;
}

/* 按钮 + 它的圈圈捆在一组，免得在横排里看着像隔壁按钮的。 */
.btn-group {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
}

/* --------------------------------------------------------------------------
   待办（书签页的另一个视图）
   -------------------------------------------------------------------------- */

/* 侧栏顶部的「书签 | 待办」切换 */
.nav-view {
  display: flex;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4) 0;
}

/* 待办卡：与书签卡同一套；标题不是链接，左边多一个勾选框 */
.card--todo .card__top {
  align-items: flex-start;
}

.todo-due-row {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

.todo-check {
  display: flex;
  align-items: center;
  padding-top: 2px;
  flex: none;
}

.todo-check input {
  width: 1.05rem;
  height: 1.05rem;
  accent-color: var(--primary);
  cursor: pointer;
}

/* 备注最多三行：待办是「记一下」，不是一篇文章 */
.card--todo .card__desc {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* 完成：标题划线、整卡淡下去 —— 但仍然看得见，也还能点回来 */
.card--todo.is-done {
  opacity: 0.62;
}

.card--todo.is-done .card__title {
  text-decoration: line-through;
}

/* 截止日期：过期红、今天橙、其余灰；完成的一律灰着（事情已经做完了） */
.todo-due {
  display: inline-block;
  margin-top: var(--sp-1);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  color: var(--fg-muted);
}

.todo-due--today {
  color: var(--warn, #b45309);
  font-weight: 600;
}

.todo-due--overdue {
  color: var(--danger);
  font-weight: 600;
}

/* ==========================================================================
   站点页脚
   ========================================================================== */

/*
 * 页脚只在 <html data-footer="on"> 时出现 —— 那个属性是**服务端**写的
 * （页脚配了才有，见 src/brand.js 的 renderPage）。没有它，下面这几条规则
 * 一条都不生效：老站的页面布局一个字不变。
 *
 * 机制：body 变成**两行网格** —— 第一行给页面外壳（.app / .main / .auth），
 * 第二行给页脚条。外壳那行是 `minmax(min-content, 1fr)`：它把「视口高度
 * 减去页脚的真实高度」全部占满，页脚于是**永远**贴在视口底部 —— 页脚折行
 * 变高时占掉的是外壳那一行，页面不会因此多出一条滚动条
 * （备案信息不能要滚动才看得见，那正是规格里的硬要求）。
 *
 * ⚠️ 三处必须配套，缺一处就回到「页脚掉出折叠线」那个坑：
 *   1. body 的高度得是**确定的**（height: 100vh）：1fr 那行才分得到剩余空间。
 *      body 高度不定时，1fr 只能按内容算 —— 外壳就被侧栏那个名义上的 100vh
 *      顶开，页脚一折行文档就比视口高，页脚最后一行掉出折叠线；
 *   2. 外壳自己的 min-height: 100vh 要清掉（下面那条 min-height: 0）：
 *      不清的话外壳至少 100vh 高，加上页脚就超出一屏；
 *   3. 侧栏的高度改成**跟着外壳走**（height: auto，align-self 默认的 stretch
 *      会把它拉到外壳那一行的高度），不再是那个名义上的 100vh ——
 *      不然它的 100vh 会把外壳顶开（第 1 条说的那个坑），页脚一折行
 *      它还会压在页脚上面（侧栏的 z-index 比页脚高）。
 */
html[data-footer='on'] body {
  display: grid;
  grid-template-rows: minmax(min-content, 1fr) auto;
  height: 100vh;
}

html[data-footer='on'] .app,
html[data-footer='on'] .main,
html[data-footer='on'] .auth {
  min-height: 0;
}

/*
 * 侧栏跟着外壳走：短页面上外壳矮一条（页脚那一条的高度），侧栏也就矮那么
 * 一点 —— 页脚不会压在它的下面（「退出登录」那个框露得出来）。
 * 长页面上外壳很高，用 max-height 把它拉回视口高度 —— 跟从前一样，
 * 侧栏自己滚、页面也滚。
 *
 * ⚠️ `contain: size` 是这里**最要紧的一条**：侧栏的**内在尺寸**不许进入
 *   外壳那一行的基准。
 *   网格行的基准取的是内容的 min-content 贡献，而**滚动容器的内容也会贡献
 *   内在高度**（`overflow-y: auto` 只截断它的*用*高度，不截断它的贡献）——
 *   所以分类一多（实测 20 条以上），外壳那一行就被顶得比视口还高，
 *   文档比视口高出一截，**页脚整条掉到折叠线以下**（实测：页脚可见 0px、
 *   多出一条滚动）。size containment 让「这个盒子的大小与它的内容无关」：
 *   侧栏的高度改由外壳那一行决定（下面的 height: auto + stretch），
 *   分类多了就在它自己内部滚（overflow-y: auto 还在）。
 *
 * ⚠️ 只在宽屏下改：窄屏（≤860px）里侧栏是**抽屉**（position: fixed），
 *   它的高度由 top/bottom 决定，改 height 会把抽屉弄坏。
 */
@media (min-width: 861px) {
  html[data-footer='on'] .sidebar {
    height: auto;
    max-height: 100vh;
    contain: size;
  }
}

/*
 * 提示条也在视口右下角：页脚在的时候把它抬高一点，别盖住页脚那一行。
 *
 * --footer-h 是页脚那一条的**名义高度**（一行文字 + 上下留白）。页脚折行时
 * 实际更高，这时提示条会压到页脚上一角 —— 提示条是几秒钟就消失的东西，
 * 那个名义值够用（页脚本身的位置与高度跟它无关，见上面那三处配套）。
 */
html[data-footer='on'] .toasts {
  bottom: calc(var(--footer-h) + var(--sp-5));
}

.site-footer {
  padding: var(--sp-3) var(--sp-4);
  border-top: 1px solid var(--border);
  background: var(--bg);
  color: var(--fg-muted);
  font-size: 0.75rem;
  text-align: center;
}

/* 窄屏放不下时允许折行（不横滚）：换行由浏览器自己做，页脚条的高度跟着
   变 —— 上面的外壳是网格那一行，会自动让出位置。 */
.site-footer__line {
  margin: 0;
}

/* 段与段之间的那个「·」：只是排版用的，两边留一点空隙。 */
.site-footer__sep {
  margin: 0 0.4em;
}

/* 页脚里的链接用站点的强调色（跟正文里的链接一个颜色）。 */
/* 自定义链接里网址空着的那一项：纯文字，不是 <a>。
   用次要色（跟版权那一段一个色）—— 它没有链接，不该像 .site-footer__link
   那样是强调色：访客看得出哪几个字能点、哪几个不能。 */
.site-footer__text {
  color: var(--fg-muted);
}

.site-footer__link {
  color: var(--accent);
}
