/* ============================================================
   ESS 管理 — Design System
   Inspired by Linear, Vercel Dashboard, Raycast
   Premium dark-mode admin dashboard
   ============================================================ */

/* ----------------------------------------------------------
   1. CSS Variables
   ---------------------------------------------------------- */
:root {
    /* ============================================================
       新令牌 —— 唯一真值
       所有颜色对比度均已实测，守卫在 tests/test_css_contrast.py
       ============================================================ */

    /* 中性色阶：去紫、边框实色 */
    --bg-sunken: #08090b;
    --bg-base: #0b0d10;
    --surface-1: #14171c;
    --surface-2: #1b1f26;
    --surface-3: #232830;
    --border-1: #262b33;
    --border-2: #333a45;

    /* 文字三档 + 操作色（在三档 surface 上均 ≥4.5:1） */
    --text-1: #e6e8ec;
    --text-2: #a8aeb9;
    --text-3: #8a92a0;
    --accent-text: #5b9bff;

    /* 填充色 —— 与文字色是两套东西。
       白字放在 --accent-text 上只有 2.77:1，放在旧的 #3b82f6 上是 3.68:1。 */
    --accent-bg: #2563eb;
    --accent-bg-hover: #1d4ed8;
    --on-accent: #ffffff;
    --success-bg: #15803d;
    --success-bg-hover: #166534;
    --on-success: #ffffff;
    --danger-bg: #b91c1c;
    --danger-bg-hover: #991b1b;
    --on-danger: #ffffff;

    /* 语义色（仅作文字与描边，永不作实心填充） */
    --ok: #4ade80;
    --warn: #fbbf24;
    --err: #f87171;
    --info: #5b9bff;

    /* 语义 tint：13% alpha 底，在任何 surface 上都成立 */
    --ok-tint: rgba(74, 222, 128, 0.13);
    --warn-tint: rgba(251, 191, 36, 0.13);
    --err-tint: rgba(248, 113, 113, 0.13);
    --info-tint: rgba(91, 155, 255, 0.13);

    /* 字号：--fs-xs 仅限数字与拉丁，含中文的文本下限 13px */
    --fs-xs: 12px;
    --fs-sm: 13px;
    --fs-base: 14px;
    --fs-md: 15px;
    --fs-lg: 17px;
    --fs-xl: 20px;
    --fs-2xl: 24px;

    /* 行高：中文需求高于拉丁 */
    --lh-tight: 1.35;
    --lh: 1.6;
    --lh-loose: 1.8;

    /* 间距 */
    --sp-1: 4px;
    --sp-2: 8px;
    --sp-3: 12px;
    --sp-4: 16px;
    --sp-5: 24px;
    --sp-6: 32px;
    --sp-7: 48px;

    /* 圆角 */
    --radius-sm: 6px;
    --radius: 8px;
    --radius-lg: 11px;
    --radius-xl: 16px;
    --radius-full: 20px;

    /* 层叠 —— 必须令牌化：toast 原先硬编码 z-index:300，
       粘性保存栏与 Modal 一进来必然打架 */
    --z-dropdown: 100;
    --z-sticky: 200;
    --z-modal: 300;
    --z-toast: 400;

    /* 动效（降级见文件末尾的 prefers-reduced-motion 块） */
    --dur-fast: 100ms;
    --dur: 150ms;
    --dur-slow: 250ms;
    --focus-ring: 0 0 0 2px var(--accent-text);

    /* 布局宽度 —— 新增。原先 .main-content 完全没有上限，
       1920px 屏下表单标签到输入框横跨约 1400px */
    --w-content: 1280px;
    --w-form: 720px;
    --w-prose: 560px;
    --sidebar-w: 220px;

    /* 字体 */
    --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    --font-mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, monospace;

    /* ============================================================
       旧名 —— 指向新值。
       让既有的 972 处内联样式与 2237 行 CSS 原地生效，
       不改一行 HTML 即可整站换肤。
       阶段 3 按页改造时逐页删除，删完移除本块。
       ============================================================ */
    --bg-primary: var(--bg-base);
    --bg-secondary: var(--surface-1);
    --bg-card: var(--surface-1);
    --bg-hover: var(--surface-2);

    --text-primary: var(--text-1);
    --text-secondary: var(--text-2);
    --text-muted: var(--text-3);

    --accent: var(--accent-bg);
    --accent-hover: var(--accent-bg-hover);
    --accent-subtle: var(--info-tint);

    --success: var(--ok);
    --success-subtle: var(--ok-tint);
    --warning: var(--warn);
    --warning-subtle: var(--warn-tint);
    --error: var(--err);
    --error-subtle: var(--err-tint);

    --border: var(--border-1);
    --border-color: var(--border-1);
    --border-highlight: var(--border-2);

    --input-bg: var(--bg-base);

    --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.3);
    --shadow-card-hover: 0 4px 16px rgba(0, 0, 0, 0.4);
    --shadow-modal: 0 16px 48px rgba(0, 0, 0, 0.6);

    --transition-fast: var(--dur-fast) ease;
    --transition: var(--dur) ease;
    --transition-slow: var(--dur-slow) ease;
}

/* ----------------------------------------------------------
   2. Global Reset & Base
   ---------------------------------------------------------- */
*, *::before, *::after {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

html {
    scroll-behavior: smooth;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
}

body {
    font-family: var(--font-sans);
    font-size: 14px;
    line-height: 1.5;
    background: var(--bg-primary);
    color: var(--text-primary);
    min-height: 100vh;
    overflow-x: hidden;
}

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

::selection {
    background: var(--accent);
    color: #fff;
}

/* Focus-visible for accessibility */
:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* Scrollbar styling */
::-webkit-scrollbar {
    width: 6px;
    height: 6px;
}
::-webkit-scrollbar-track {
    background: transparent;
}
::-webkit-scrollbar-thumb {
    background: rgba(255, 255, 255, 0.08);
    border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
    background: rgba(255, 255, 255, 0.14);
}

/* ----------------------------------------------------------
   3. Layout — Sidebar + Main
   ---------------------------------------------------------- */
.layout {
    display: flex;
    min-height: 100vh;
}

.sidebar {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    width: 220px;
    background: var(--bg-secondary);
    border-right: 1px solid var(--border);
    display: flex;
    flex-direction: column;
    z-index: 50;
    overflow-y: auto;
    overflow-x: hidden;
}

.sidebar-header {
    padding: 24px 20px 16px;
    display: flex;
    align-items: center;
    gap: 12px;
    border-bottom: 1px solid var(--border);
    margin-bottom: 4px;
}

.sidebar-logo {
    background: linear-gradient(135deg, var(--accent), #8b5cf6);
    border-radius: var(--radius);
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 16px;
    color: #fff;
    flex-shrink: 0;
    padding: 6px 12px;
    letter-spacing: 0.02em;
    white-space: nowrap;
}

.sidebar-title {
    font-size: 16px;
    font-weight: 600;
    color: var(--text-primary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.sidebar-nav {
    flex: 1;
    padding: 8px;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.sidebar-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 12px;
    border-radius: var(--radius-sm);
    color: var(--text-secondary);
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    transition: background var(--transition-fast), color var(--transition-fast);
    border: none;
    background: none;
    width: 100%;
    text-align: left;
    position: relative;
    user-select: none;
}

.sidebar-item:hover {
    background: var(--bg-hover);
    color: var(--text-primary);
}

.sidebar-item.active {
    background: var(--accent-subtle);
    color: var(--text-primary);
}

.sidebar-item.active::before {
    content: '';
    position: absolute;
    left: 0;
    top: 6px;
    bottom: 6px;
    width: 3px;
    border-radius: 0 2px 2px 0;
    background: var(--accent);
}

.sidebar-item-icon {
    width: 18px;
    height: 18px;
    flex-shrink: 0;
    opacity: 0.7;
}

.sidebar-item.active .sidebar-item-icon {
    opacity: 1;
}

.sidebar-divider {
    height: 1px;
    margin: 8px 12px;
    background: linear-gradient(90deg, transparent, var(--border-highlight), transparent);
}

.sidebar-footer {
    padding: 8px;
    border-top: 1px solid var(--border);
}

.sidebar-footer .sidebar-item {
    color: var(--text-muted);
}

.sidebar-footer .sidebar-item:hover {
    color: var(--error);
    background: var(--error-subtle);
}

.sidebar-user {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px 16px;
    border-top: 1px solid var(--border);
    margin-top: 4px;
}

.sidebar-user-info {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
}

.sidebar-user-avatar {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: var(--accent);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 13px;
    font-weight: 600;
    flex-shrink: 0;
}

.sidebar-user-name {
    font-size: 13px;
    color: var(--text-secondary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.sidebar-logout {
    background: none;
    border: none;
    color: var(--text-muted);
    cursor: pointer;
    font-size: 16px;
    padding: 4px 6px;
    border-radius: var(--radius-sm);
    transition: var(--transition);
    line-height: 1;
}

.sidebar-logout:hover {
    color: var(--error);
    background: var(--error-subtle);
}

.main-content {
    flex: 1;
    min-width: 0;
    margin-left: var(--sidebar-w);
    padding: var(--sp-6) 40px;
    min-height: 100vh;
}

/* 正文阅读宽度上限。
   包在 .main-content 内层而不是直接限制它本身，是为了让背景/粘性元素
   仍能铺满视口宽度，只有内容受限。 */
.main-content > * {
    max-width: var(--w-content);
}

.page-header {
    margin-bottom: 28px;
}

.page-title {
    font-size: 22px;
    font-weight: 600;
    color: var(--text-primary);
    letter-spacing: -0.02em;
}

.page-subtitle {
    font-size: 13px;
    color: var(--text-secondary);
    margin-top: 4px;
}

/* ----------------------------------------------------------
   4. Cards
   ---------------------------------------------------------- */
.card {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-card);
    transition: border-color var(--transition), box-shadow var(--transition);
}

.card:hover {
    border-color: var(--border-highlight);
    box-shadow: var(--shadow-card-hover);
}

.card-header {
    padding: 16px 20px;
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    gap: 10px;
}

/* 头部末尾的操作/徽章显式靠右。
   原先靠 justify-content:space-between 达到这个效果，但头部通常只有
   「图标、标题」两个子元素，space-between 会把标题一起推到最右边。
   排除全部标题级别保证「图标 + 标题」这种两元素头部不受影响；
   :not(:first-child) 保证只有标题的单元素头部不受影响。

   为什么排 h1-h6 而不只排 h3：本仓已经在 card-header 里用了 h4
   （BackupView.js:381、WizardView.js:1109）。只写 :not(h3) 的话，
   谁给那两处任一加个图标凑成 [图标, h4]，h4 立刻吃到 margin-left:auto
   飞到最右——正是本条规则要修的 bug 原样复现。 */
.card-header > :last-child:not(:first-child):not(h1):not(h2):not(h3):not(h4):not(h5):not(h6) {
    margin-left: auto;
}

.card-header h3 {
    font-size: 14px;
    font-weight: 600;
}

.card-body {
    padding: 20px;
}

.card-footer {
    padding: 12px 20px;
    border-top: 1px solid var(--border);
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 8px;
}

/* ----------------------------------------------------------
   5. Buttons
   ---------------------------------------------------------- */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 8px 16px;
    border: none;
    /* 基类必须自带底色：不写的话，不带修饰类的 `<button class="btn">` 会掉回
       浏览器原生按钮（Chromium 是浅灰底 + 黑字），在暗色界面里白晃晃地冒出来
       ——消息管理「清理日志」的刷新、安全设置的「更换配置」都中招。
       取中性档，语义是「没指定语义的按钮就是中性按钮」。
       六个修饰类都各自声明了 background 且都排在本规则之后，压得住；
       这里刻意不加 border，免得给 primary/danger/success/ghost 套上边框、
       并因 border-box 让它们各长 2px。 */
    background: var(--surface-2);
    color: var(--text-1);
    border-radius: var(--radius-sm);
    font-family: var(--font-sans);
    font-size: 13px;
    font-weight: 500;
    line-height: 1;
    cursor: pointer;
    transition: background var(--transition-fast), transform var(--transition-fast), box-shadow var(--transition-fast);
    user-select: none;
    white-space: nowrap;
    position: relative;
    overflow: hidden;
}

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

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

/* Primary */
.btn-primary {
    background: var(--accent);
    color: #fff;
}
.btn-primary:hover:not(:disabled) {
    background: var(--accent-hover);
    filter: brightness(1.1);
}

/* Danger */
/* 用填充色而不是语义色：--error(#f87171) 是给**文字和描边**用的浅红，
   白字放上去只有 2.77:1。实心按钮必须走 --danger-bg(#b91c1c)，6.47:1。
   这正是 spec 硬规则 B「语义色永不作实心填充」。 */
.btn-danger {
    background: var(--danger-bg);
    color: var(--on-danger);
}
.btn-danger:hover:not(:disabled) {
    background: var(--danger-bg-hover);
}

/* Success */
/* 同上：--success(#4ade80) 上的白字只有 1.74:1，几乎看不清。 */
.btn-success {
    background: var(--success-bg);
    color: var(--on-success);
}
.btn-success:hover:not(:disabled) {
    background: var(--success-bg-hover);
}

/* Ghost */
.btn-ghost {
    background: transparent;
    color: var(--text-secondary);
}
.btn-ghost:hover:not(:disabled) {
    background: var(--bg-hover);
    color: var(--text-primary);
}

/* Outline */
.btn-outline {
    background: transparent;
    color: var(--text-primary);
    border: 1px solid var(--border-highlight);
}
.btn-outline:hover:not(:disabled) {
    background: var(--bg-hover);
    border-color: rgba(255, 255, 255, 0.18);
}

/* Icon-only */
.btn-icon {
    padding: 8px;
    border-radius: var(--radius-sm);
}

/* Small */
.btn-sm {
    padding: 4px 10px;
    font-size: 12px;
}

/* Secondary */
.btn-secondary {
    background: var(--bg-hover);
    color: var(--text-primary);
    border: 1px solid var(--border-highlight);
}
.btn-secondary:hover:not(:disabled) {
    background: rgba(255, 255, 255, 0.12);
    border-color: rgba(255, 255, 255, 0.18);
}

/* Loading button */
.btn-loading {
    pointer-events: none;
    opacity: 0.7;
}
.btn-loading::after {
    content: '';
    display: inline-block;
    width: 14px;
    height: 14px;
    border: 2px solid transparent;
    border-top-color: currentColor;
    border-radius: 50%;
    animation: spin 0.6s linear infinite;
    margin-left: 6px;
}

/* ----------------------------------------------------------
   6. Inputs
   ---------------------------------------------------------- */
.input {
    width: 100%;
    padding: 8px 12px;
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-primary);
    font-family: var(--font-sans);
    font-size: 14px;
    line-height: 1.5;
    transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
    outline: none;
}

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

.input:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-subtle);
}

.input-error {
    border-color: var(--error) !important;
    box-shadow: 0 0 0 3px var(--error-subtle) !important;
}

textarea.input {
    min-height: 80px;
    resize: vertical;
}

select.input {
    cursor: pointer;
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%238888a0' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 12px center;
    padding-right: 32px;
}

/* ----------------------------------------------------------
   7. Toggle Switch
   ---------------------------------------------------------- */
.toggle {
    position: relative;
    width: 40px;
    height: 22px;
    background: rgba(255, 255, 255, 0.1);
    border-radius: 11px;
    border: none;
    cursor: pointer;
    transition: background var(--transition);
    flex-shrink: 0;
}

.toggle::after {
    content: '';
    position: absolute;
    width: 16px;
    height: 16px;
    background: #fff;
    border-radius: 50%;
    top: 3px;
    left: 3px;
    transition: transform 200ms ease;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}

.toggle.on {
    background: var(--accent);
}

.toggle.on::after {
    transform: translateX(18px);
}

.toggle:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* ----------------------------------------------------------
   8. Badges
   ---------------------------------------------------------- */
.badge {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 8px;
    /* 基类自带中性外观：不写的话，不带修饰类的 `class="badge"` 只有形状
       没有颜色，渲染成一段带内边距的裸文字（MessageView 的快捷标签就是
       这样）。七个修饰类都各自声明了 background 且排在本规则之后，压得住。 */
    background: var(--surface-2);
    color: var(--text-2);
    border-radius: var(--radius-full);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.02em;
    white-space: nowrap;
}

.badge-default {
    background: var(--bg-hover);
    color: var(--text-secondary);
}

.badge-accent {
    background: var(--accent-subtle);
    color: var(--accent);
}

/* 用于「默认/提示」语义（MessageView 的「所有房间」策略标签）。
   此前模板里一直写着 badge-info 而 CSS 从未定义它——未知 class 不报错，
   那个标签就渲染成没有底色的裸文字，看起来还像是设计如此。 */
.badge-info {
    background: var(--info-tint);
    color: var(--info);
}

.badge-success {
    background: var(--success-subtle);
    color: var(--success);
}

.badge-warning {
    background: var(--warning-subtle);
    color: var(--warning);
}

.badge-error {
    background: var(--error-subtle);
    color: var(--error);
}

.badge-danger {
    background: var(--error-subtle);
    color: var(--error);
}

.badge-secondary {
    background: var(--bg-hover);
    color: var(--text-secondary);
}

/* ----------------------------------------------------------
   9. Modal
   ---------------------------------------------------------- */
.modal-overlay {
    display: flex;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    backdrop-filter: blur(4px);
    z-index: var(--z-modal);        /* 原先硬编码 200，正好撞上 --z-sticky */
    align-items: center;
    justify-content: center;
}

.modal {
    background: var(--bg-card);
    border: 1px solid var(--border-highlight);
    border-radius: var(--radius-xl);
    box-shadow: var(--shadow-modal);
    padding: 24px;
    min-width: 400px;
    max-width: 90vw;
    max-height: 90vh;
    overflow-y: auto;
    animation: modalIn 200ms ease;
}

@keyframes modalIn {
    from {
        opacity: 0;
        transform: scale(0.96) translateY(8px);
    }
    to {
        opacity: 1;
        transform: scale(1) translateY(0);
    }
}

.modal h3 {
    font-size: 16px;
    font-weight: 600;
    margin-bottom: 16px;
}

.modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 16px;
}

.modal-header h3 {
    margin: 0;
    font-size: 16px;
    font-weight: 600;
}

.modal-close {
    background: none;
    border: none;
    color: var(--text-muted);
    font-size: 20px;
    cursor: pointer;
    padding: 4px 8px;
    border-radius: var(--radius-sm);
    transition: color var(--transition-fast), background var(--transition-fast);
    line-height: 1;
}
.modal-close:hover {
    color: var(--text-primary);
    background: var(--bg-hover);
}

.modal-body {
    margin-bottom: 8px;
}

.modal-footer {
    display: flex;
    gap: 12px;
    justify-content: flex-end;
    padding-top: 16px;
    border-top: 1px solid var(--border);
    margin-top: 8px;
}

/* .modal-actions 已删（2026-08-15）：它只服务手写弹层的按钮行，七处手写
   弹层全部迁 Modal 组件后按钮行走 #footer slot（.modal-footer），零消费者。 */

/* ----------------------------------------------------------
   10. Progress Bar
   ---------------------------------------------------------- */
.progress-bar {
    width: 100%;
    height: 6px;
    background: var(--bg-secondary);
    border-radius: 3px;
    overflow: hidden;
}

.progress-fill {
    height: 100%;
    background: var(--accent);
    border-radius: 3px;
    transition: width 300ms ease;
    position: relative;
}

.progress-fill::after {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent);
    animation: progressShimmer 2s infinite;
}

@keyframes progressShimmer {
    from { transform: translateX(-100%); }
    to { transform: translateX(100%); }
}

.progress-fill.success {
    background: var(--success);
}

.progress-fill.error {
    background: var(--error);
}

/* ----------------------------------------------------------
   11. Spinner
   ---------------------------------------------------------- */
.spinner {
    display: inline-block;
    width: 20px;
    height: 20px;
    border: 2px solid var(--border-highlight);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spin 0.6s linear infinite;
}

.spinner-sm {
    width: 14px;
    height: 14px;
    border-width: 1.5px;
}

.spinner-lg {
    width: 32px;
    height: 32px;
    border-width: 3px;
}

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

/* ----------------------------------------------------------
   12. Skeleton Loading
   ---------------------------------------------------------- */
.skeleton {
    background: linear-gradient(
        90deg,
        var(--bg-hover) 25%,
        rgba(255, 255, 255, 0.06) 50%,
        var(--bg-hover) 75%
    );
    background-size: 200% 100%;
    animation: shimmer 1.5s infinite ease-in-out;
    border-radius: var(--radius-sm);
}

.skeleton-text {
    height: 14px;
    margin-bottom: 8px;
}

.skeleton-text:last-child {
    width: 60%;
}

.skeleton-circle {
    border-radius: 50%;
}

.skeleton-rect {
    border-radius: var(--radius);
}

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

/* ----------------------------------------------------------
   13. Toast（useToast / ToastHost）
   容器一直在 DOM 里（role=status + aria-live），内容进出靠 v-for。
   消失由 JS 计时器负责，不用 CSS 动画 forwards ——后者只是把元素画没了，
   DOM 里还留着，屏幕阅读器与 Tab 序列都还能碰到它。
   ---------------------------------------------------------- */
.c-toast-host {
    position: fixed;
    right: var(--sp-5);
    bottom: var(--sp-5);
    z-index: var(--z-toast);
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    pointer-events: none;          /* 空容器不许挡住底下的界面 */
    max-width: min(380px, calc(100vw - var(--sp-6)));
}

.c-toast {
    pointer-events: auto;
    display: flex;
    align-items: flex-start;
    gap: var(--sp-2);
    padding: var(--sp-3);
    background: var(--surface-3);
    border: 1px solid var(--border-2);
    border-left-width: 3px;
    border-radius: var(--radius);
    box-shadow: var(--shadow-card-hover);
    font-size: var(--fs-sm);
    line-height: var(--lh);
    color: var(--text-1);
    animation: toastIn var(--dur-slow) ease;
}

.c-toast-text { flex: 1; min-width: 0; word-break: break-word; }
.c-toast-count {
    flex: none;
    align-self: center;
    font-size: var(--fs-xs);          /* 纯数字，允许 12px（硬规则 D） */
    font-variant-numeric: tabular-nums;
    color: var(--text-2);
}
.c-toast-icon { margin-top: 2px; }

.c-toast-close {
    flex: none;
    padding: 0;
    background: none;
    border: none;
    color: var(--text-2);
    cursor: pointer;
    border-radius: var(--radius-sm);
}
.c-toast-close:hover { color: var(--text-1); }
.c-toast-close:focus-visible { outline: none; box-shadow: var(--focus-ring); }

.c-toast--success { border-left-color: var(--ok); }
.c-toast--success .c-toast-icon { color: var(--ok); }
.c-toast--error { border-left-color: var(--err); }
.c-toast--error .c-toast-icon { color: var(--err); }
.c-toast--warn { border-left-color: var(--warn); }
.c-toast--warn .c-toast-icon { color: var(--warn); }
.c-toast--info { border-left-color: var(--info); }
.c-toast--info .c-toast-icon { color: var(--info); }

@keyframes toastIn {
    from { opacity: 0; transform: translateY(8px); }
    to { opacity: 1; transform: translateY(0); }
}

/* ----------------------------------------------------------
   14. Log Viewer
   ---------------------------------------------------------- */
.log-viewer {
    background: var(--bg-primary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    font-family: var(--font-mono);
    font-size: 12px;
    line-height: 1.7;
    overflow-y: auto;
    padding: 12px 16px;
    max-height: 500px;
    min-height: 200px;
    white-space: pre-wrap;
    word-break: break-all;
}

.log-line {
    padding: 1px 0;
}

.log-cmd {
    color: var(--accent);
}

.log-success {
    color: var(--success);
}

.log-error {
    color: var(--error);
}

.log-warning {
    color: var(--warning);
}

.log-dim {
    color: var(--text-muted);
}

.log-timestamp {
    color: var(--text-muted);
    margin-right: 8px;
    font-size: 11px;
}

/* Legacy log panel styles (backward compat) */
.log-panel {
    background: var(--bg-card);
    border-radius: var(--radius);
    border: 1px solid var(--border);
    display: flex;
    flex-direction: column;
}

.log-header {
    padding: 12px 16px;
    border-bottom: 1px solid var(--border);
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.log-header h3 {
    font-size: 14px;
    font-weight: 600;
}

.log-body {
    flex: 1;
    overflow-y: auto;
    padding: 12px 16px;
    font-family: var(--font-mono);
    font-size: 12px;
    line-height: 1.7;
    max-height: 500px;
    min-height: 200px;
    white-space: pre-wrap;
    word-break: break-all;
    background: var(--bg-primary);
}

.log-body .cmd { color: var(--accent); }
.log-body .success { color: var(--success); }
.log-body .error { color: var(--error); }

/* ----------------------------------------------------------
   15. Tables
   ---------------------------------------------------------- */
.table {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
}

.table th,
.table td {
    padding: 10px 16px;
    text-align: left;
    border-bottom: 1px solid var(--border);
}

.table th {
    color: var(--text-muted);
    font-weight: 500;
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.table tbody tr {
    transition: background var(--transition-fast);
}

.table tbody tr:hover {
    background: var(--bg-hover);
}

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

/* Legacy DNS table (backward compat) */
.dns-table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 12px;
}

.dns-table th,
.dns-table td {
    padding: 8px 12px;
    text-align: left;
    border-bottom: 1px solid var(--border);
    font-size: 13px;
}

.dns-table th {
    color: var(--text-secondary);
    font-weight: 500;
}

.dns-status {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
}

.dns-status.ok { background: var(--success); }
.dns-status.fail { background: var(--error); }
.dns-status.unknown { background: var(--text-muted); }

/* ----------------------------------------------------------
   16. Page Transitions
   ---------------------------------------------------------- */
.page-enter {
    animation: pageEnter 200ms ease;
}

@keyframes pageEnter {
    from {
        opacity: 0;
        transform: translateY(8px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.page-leave {
    animation: pageLeave 150ms ease forwards;
}

@keyframes pageLeave {
    from {
        opacity: 1;
        transform: translateY(0);
    }
    to {
        opacity: 0;
        transform: translateY(-4px);
    }
}

/* ----------------------------------------------------------
   17. Tabs
   ---------------------------------------------------------- */
.tabs {
    display: flex;
    gap: 0;
    border-bottom: 1px solid var(--border);
    margin-bottom: 20px;
}

.tab {
    padding: 10px 16px;
    border: none;
    background: none;
    color: var(--text-secondary);
    font-family: var(--font-sans);
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    position: relative;
    transition: color var(--transition-fast);
}

.tab:hover {
    color: var(--text-primary);
}

.tab.active {
    color: var(--text-primary);
}

.tab.active::after {
    content: '';
    position: absolute;
    bottom: -1px;
    left: 0;
    right: 0;
    height: 2px;
    background: var(--accent);
    border-radius: 1px 1px 0 0;
}

/* ----------------------------------------------------------
   18. Form Groups
   ---------------------------------------------------------- */
.form-group {
    margin-bottom: 16px;
}

.form-group label {
    display: block;
    font-size: 13px;
    font-weight: 500;
    color: var(--text-secondary);
    margin-bottom: 6px;
}

.form-group .input {
    width: 100%;
}

.form-group .form-error,
.setting-row .form-error,
.form-error {
    font-size: 12px;
    color: var(--error);
    margin-top: 4px;
}

.form-group .form-hint,
.setting-row .form-hint,
.form-hint {
    font-size: 11px;
    color: var(--text-muted);
    margin-top: 3px;
}

.input-error,
input.input-error {
    border-color: var(--error) !important;
    box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.15) !important;
}

/* Legacy form-group input styles */
.form-group input:not(.input) {
    width: 100%;
    padding: 8px 12px;
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-primary);
    font-size: 14px;
    outline: none;
    transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

.form-group input:not(.input):focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-subtle);
}

/* ----------------------------------------------------------
   19. Wizard Steps
   ---------------------------------------------------------- */
.wizard-steps {
    display: flex;
    align-items: center;
    gap: 0;
    padding: 20px 0;
}

.wizard-step {
    display: flex;
    align-items: center;
    gap: 8px;
    position: relative;
}

.wizard-step-circle {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 13px;
    font-weight: 600;
    flex-shrink: 0;
    transition: background var(--transition), color var(--transition);
    border: 2px solid transparent;
}

/* Pending state */
.wizard-step.pending .wizard-step-circle {
    background: var(--bg-hover);
    color: var(--text-muted);
    border-color: var(--border);
}

/* Active state */
.wizard-step.active .wizard-step-circle {
    background: var(--accent);
    color: #fff;
    border-color: var(--accent);
    box-shadow: 0 0 0 4px var(--accent-subtle);
}

/* Done state */
.wizard-step.done .wizard-step-circle {
    background: var(--success);
    color: #fff;
    border-color: var(--success);
}

/* Error state */
.wizard-step.error .wizard-step-circle {
    background: var(--error);
    color: #fff;
    border-color: var(--error);
}

.wizard-step-label {
    font-size: 12px;
    font-weight: 500;
    color: var(--text-muted);
    white-space: nowrap;
}

.wizard-step.active .wizard-step-label {
    color: var(--text-primary);
}

.wizard-step.done .wizard-step-label {
    color: var(--text-secondary);
}

.wizard-step-connector {
    flex: 1;
    height: 2px;
    background: var(--border);
    margin: 0 8px;
    min-width: 24px;
}

.wizard-step.done + .wizard-step-connector,
.wizard-step-connector.done {
    background: var(--success);
}

/* ----------------------------------------------------------
   20. Step Items (Deploy wizard)
   ---------------------------------------------------------- */
.steps-grid {
    display: grid;
    grid-template-columns: 340px 1fr;
    gap: 24px;
    min-height: 500px;
}

.step-list {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.step-item {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
    border-radius: var(--radius);
    cursor: pointer;
    transition: background var(--transition-fast), border-color var(--transition-fast);
    border: 1px solid transparent;
}

.step-item:hover {
    background: var(--bg-hover);
}

.step-item.active {
    background: var(--bg-card);
    border-color: var(--accent);
}

.step-num {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 13px;
    font-weight: 600;
    background: var(--bg-hover);
    color: var(--text-muted);
    flex-shrink: 0;
    transition: background var(--transition-fast), color var(--transition-fast);
}

.step-item[data-status="done"] .step-num {
    background: var(--success);
    color: #fff;
}

.step-item[data-status="running"] .step-num {
    background: var(--accent);
    color: #fff;
    animation: pulse 1.5s infinite;
}

.step-item[data-status="error"] .step-num {
    background: var(--error);
    color: #fff;
}

.step-info { flex: 1; }

.step-name {
    font-size: 14px;
    font-weight: 500;
}

.step-desc {
    font-size: 12px;
    color: var(--text-secondary);
    margin-top: 2px;
}

.step-actions {
    padding: 12px 16px;
    border-top: 1px solid var(--border);
    display: flex;
    gap: 8px;
    align-items: center;
    flex-wrap: wrap;
}

@keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.5; }
}

/* ----------------------------------------------------------
   21. Connection Status Indicator
   ---------------------------------------------------------- */
.conn-status {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 12px;
    color: var(--text-secondary);
}

.conn-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--error);
    transition: background var(--transition);
}

.conn-dot.connected {
    background: var(--success);
    box-shadow: 0 0 6px var(--success-subtle);
}

/* ----------------------------------------------------------
   22. Domain Section
   ---------------------------------------------------------- */
.domain-section {
    background: var(--bg-card);
    border-radius: var(--radius-lg);
    border: 1px solid var(--border);
    padding: 20px;
    margin-bottom: 20px;
}

.domain-section h3 {
    margin-bottom: 12px;
    font-size: 16px;
    font-weight: 600;
}

.domain-input-row {
    display: flex;
    gap: 8px;
    margin-bottom: 16px;
}

.domain-input-row input {
    flex: 1;
    padding: 8px 12px;
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-primary);
    font-size: 14px;
    outline: none;
    transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

.domain-input-row input:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-subtle);
}

/* ----------------------------------------------------------
   23. Backup Grid
   ---------------------------------------------------------- */
.backup-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 24px;
}

.backup-controls {
    background: var(--bg-card);
    border-radius: var(--radius-lg);
    border: 1px solid var(--border);
    padding: 20px;
}

.backup-controls h3 {
    margin-bottom: 16px;
    font-weight: 600;
}

.backup-list {
    background: var(--bg-card);
    border-radius: var(--radius-lg);
    border: 1px solid var(--border);
    padding: 20px;
}

.backup-list h3 {
    margin-bottom: 16px;
    font-weight: 600;
}

.backup-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px 0;
    border-bottom: 1px solid var(--border);
}

.backup-item:last-child {
    border-bottom: none;
}

.backup-info {
    font-size: 13px;
}

.backup-info .name {
    font-weight: 500;
}

.backup-info .size {
    color: var(--text-secondary);
    margin-left: 8px;
}

.backup-actions {
    display: flex;
    gap: 4px;
}

.backup-log {
    margin-top: 16px;
}

/* ----------------------------------------------------------
   24. Settings Row
   ---------------------------------------------------------- */
.setting-row {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 12px;
}

.setting-row label {
    font-size: 13px;
    min-width: 100px;
    color: var(--text-secondary);
}

/* ----------------------------------------------------------
   25. Panel (tab content switching)
   ---------------------------------------------------------- */
.panel {
    display: none;
}

.panel.active {
    display: block;
    animation: pageEnter 200ms ease;
}

/* ----------------------------------------------------------
   26. Container
   ---------------------------------------------------------- */
.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 24px;
}

/* ----------------------------------------------------------
   27. Login Page
   ---------------------------------------------------------- */
.login-wrapper,
.login-page {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg-primary);
    padding: 24px;
}

.login-card {
    width: 100%;
    max-width: 380px;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-xl);
    padding: 32px;
    box-shadow: var(--shadow-modal);
}

.login-card h2 {
    font-size: 20px;
    font-weight: 600;
    text-align: center;
    margin-bottom: 24px;
    letter-spacing: -0.02em;
}

.login-card .form-group {
    margin-bottom: 16px;
}

.login-card .btn-primary {
    width: 100%;
    padding: 10px 16px;
    font-size: 14px;
}

.login-header {
    text-align: center;
    margin-bottom: 24px;
}

.login-title {
    font-size: 22px;
    font-weight: 700;
    color: var(--text-primary);
    letter-spacing: -0.02em;
    margin-bottom: 6px;
}

.login-subtitle {
    font-size: 13px;
    color: var(--text-muted);
    margin-top: 4px;
}

.login-form {
    display: flex;
    flex-direction: column;
}

.login-error {
    font-size: 13px;
    color: var(--error);
    text-align: center;
    margin-bottom: 12px;
}

.captcha-row {
    display: flex;
    gap: 8px;
    align-items: flex-end;
}

.captcha-row .input {
    flex: 1;
}

.captcha-img {
    height: 38px;
    border-radius: var(--radius-sm);
    cursor: pointer;
    border: 1px solid var(--border);
}

/* ----------------------------------------------------------
   28. Empty State
   ---------------------------------------------------------- */
.empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 48px 24px;
    color: var(--text-muted);
    font-size: 14px;
    text-align: center;
    gap: 12px;
}

.empty-state-icon {
    width: 48px;
    height: 48px;
    opacity: 0.3;
}

/* ----------------------------------------------------------
   29. Wizard Additions
   ---------------------------------------------------------- */
.wizard-container {
    min-height: 100vh;
    background: var(--bg-primary);
    padding: 24px 0;
}

.wizard-panel {
    animation: pageEnter 200ms ease;
}

.wizard-step {
    cursor: pointer;
}

/* DNS status badges */
.dns-status {
    font-size: 12px;
    font-weight: 500;
    white-space: nowrap;
}

.dns-ok { color: var(--success); }
.dns-checking { color: var(--warning); }
.dns-pending { color: var(--text-muted); }
.dns-fail { color: var(--error); }

/* DNS 单条记录的诊断说明（负缓存 / 指向不对 / 记录未添加） */
.dns-hint {
    margin-top: 4px;
    max-width: 340px;
    font-size: 11px;
    line-height: 1.5;
    color: var(--text-secondary);
    white-space: normal;
}

/* Sub-step icons */
.sub-icon-done { background: var(--success) !important; color: #fff !important; }
.sub-icon-running { background: var(--accent) !important; color: #fff !important; animation: pulse 1.5s infinite; }
.sub-icon-error { background: var(--error) !important; color: #fff !important; }
.sub-icon-pending { background: var(--bg-hover) !important; color: var(--text-muted) !important; }

/* Progress bar */
.progress-bar {
    width: 100%;
    height: 6px;
    background: var(--bg-hover);
    border-radius: 3px;
    overflow: hidden;
}

.progress-fill {
    height: 100%;
    background: var(--accent);
    border-radius: 3px;
    transition: width 0.5s ease;
}

/* Config sections (collapsible) */
.config-sections {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-bottom: 16px;
}

.config-section {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
}

.config-section-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px 16px;
    background: var(--bg-secondary);
    cursor: pointer;
    font-size: 13px;
    font-weight: 500;
    user-select: none;
    transition: background var(--transition-fast);
}

.config-section-header:hover {
    background: var(--bg-hover);
}

.config-toggle {
    font-size: 10px;
    color: var(--text-muted);
}

.config-section-body {
    padding: 12px 16px;
    border-top: 1px solid var(--border);
}

.config-section-body .setting-row {
    margin-bottom: 8px;
}

.config-section-body .setting-row:last-child {
    margin-bottom: 0;
}

.config-section-body .input-sm {
    height: 32px;
    font-size: 13px;
    padding: 4px 10px;
}

/* Simple table for DNS records */
.table {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
}

.table th, .table td {
    padding: 8px 12px;
    text-align: left;
    border-bottom: 1px solid var(--border);
}

.table th {
    font-weight: 600;
    font-size: 12px;
    color: var(--text-secondary);
    background: var(--bg-secondary);
}

.table code {
    font-family: var(--font-mono);
    font-size: 12px;
    background: var(--bg-hover);
    padding: 2px 6px;
    border-radius: var(--radius-sm);
}

.form-label {
    display: block;
    font-size: 13px;
    font-weight: 500;
    color: var(--text-secondary);
    margin-bottom: 6px;
}

/* ----------------------------------------------------------
   30z. Alert banners
   ---------------------------------------------------------- */
.alert {
    padding: 12px 16px;
    border-radius: var(--radius);
    font-size: 13px;
    line-height: 1.5;
}

.alert-error {
    background: var(--error-subtle);
    color: var(--error);
    border: 1px solid rgba(239, 68, 68, 0.2);
}

.alert-success {
    background: var(--success-subtle);
    color: var(--success);
    border: 1px solid rgba(34, 197, 94, 0.2);
}

/* ----------------------------------------------------------
   30a. form-input (dark theme native inputs)
   ---------------------------------------------------------- */
.form-input {
    width: 100%;
    padding: 8px 12px;
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-primary);
    font-family: var(--font-sans);
    font-size: 14px;
    line-height: 1.5;
    transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
    outline: none;
}
.form-input::placeholder {
    color: var(--text-muted);
}
.form-input:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-subtle);
}

/* ----------------------------------------------------------
   30b. Link Muted
   ---------------------------------------------------------- */
.link-muted {
    color: var(--text-secondary);
    font-size: 13px;
    font-weight: 500;
    text-decoration: none;
    transition: color var(--transition-fast);
}
.link-muted:hover {
    color: var(--text-primary);
}

/* ----------------------------------------------------------
   30c. Toggle Switch (checkbox replacement)
   ---------------------------------------------------------- */
.toggle-switch {
    position: relative;
    display: inline-flex;
    align-items: center;
    cursor: pointer;
    user-select: none;
    flex-shrink: 0;
}

.toggle-switch input {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
}

.toggle-slider {
    position: relative;
    width: 40px;
    height: 22px;
    background: rgba(255, 255, 255, 0.1);
    border-radius: 11px;
    transition: background var(--transition);
    flex-shrink: 0;
}

.toggle-slider::after {
    content: '';
    position: absolute;
    width: 16px;
    height: 16px;
    background: #fff;
    border-radius: 50%;
    top: 3px;
    left: 3px;
    transition: transform 200ms ease;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}

.toggle-switch input:checked + .toggle-slider {
    background: var(--accent);
}

.toggle-switch input:checked + .toggle-slider::after {
    transform: translateX(18px);
}

.toggle-switch input:focus-visible + .toggle-slider {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* ----------------------------------------------------------
   30d. Card consecutive spacing

   相邻卡片的间距只在**普通流**里成立（实测全站 14 处靠它，删不得）。
   但 grid / flex 容器自己用 gap 排间距，而这条 margin 会被 stretch 连着
   外边距框一起拉伸——第二张卡片被压低 20px、同时矮 20px。两栏卡片
   「没对齐」就是这么来的（实测 top 差 20px、高差 20px，两处症状完全一致）。

   CSS 选不到「父元素是 grid」，只能靠容器打标记。**别再往豁免名单里加新的
   容器类名**——`.status-summary-grid` 那条就是第一次踩坑时加的，后来新增的
   两个双栏 grid 照样中招，没人记得补。统一用 `.u-card-grid`，并由
   tests/e2e/test_l10_frontend.py::TestCardGridAlignment 兜底：任何 grid/flex
   容器里的相邻卡片只要拿到非零 margin-top 就判红，忘了加类会当场被抓。
   ---------------------------------------------------------- */
.card + .card {
    margin-top: 20px;
}

.u-card-grid > .card + .card,
.status-summary-grid > .card + .card {
    margin-top: 0;
}

/* 自定义 tooltip */
.tooltip-wrap:hover .tooltip-text {
    display: block !important;
}
.tooltip-wrap .tooltip-text::before {
    content: '';
    position: absolute;
    top: -6px;
    left: 12px;
    border: 6px solid transparent;
    border-bottom-color: var(--border);
}

/* ----------------------------------------------------------
   30e. Action bar (button groups)
   ---------------------------------------------------------- */
.action-bar {
    display: flex;
    gap: 12px;
    align-items: center;
    flex-wrap: wrap;
}

/* ----------------------------------------------------------
   30f. Config view input override
   ---------------------------------------------------------- */
.config-view input[type="text"] {
    background: var(--bg-secondary) !important;
    color: var(--text-primary) !important;
}

/* ----------------------------------------------------------
   30g. Page header with actions
   ---------------------------------------------------------- */
.page-header-with-actions {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 28px;
    flex-wrap: wrap;
    gap: 16px;
}

/* ----------------------------------------------------------
   30h. Log empty state
   ---------------------------------------------------------- */
.log-empty {
    text-align: center;
    padding: 24px;
    color: var(--text-muted);
    font-size: 13px;
}

/* ----------------------------------------------------------
   30i. Sidebar icon and label
   ---------------------------------------------------------- */
.sidebar-icon {
    margin-right: 10px;
    font-size: 16px;
}

.sidebar-label {
    font-size: 14px;
}

/* ----------------------------------------------------------
   30. Utility Classes
   ---------------------------------------------------------- */
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.justify-center { justify-content: center; }
.gap-4 { gap: 4px; }
.gap-8 { gap: 8px; }
.gap-12 { gap: 12px; }
.gap-16 { gap: 16px; }
.gap-24 { gap: 24px; }
.mt-4 { margin-top: 4px; }
.mt-8 { margin-top: 8px; }
.mt-12 { margin-top: 12px; }
.mt-16 { margin-top: 16px; }
.mt-24 { margin-top: 24px; }
.mb-8 { margin-bottom: 8px; }
.mb-12 { margin-bottom: 12px; }
.mb-16 { margin-bottom: 16px; }
.mb-24 { margin-bottom: 24px; }
.p-16 { padding: 16px; }
.p-20 { padding: 20px; }
.p-24 { padding: 24px; }
.text-sm { font-size: 12px; }
.text-xs { font-size: 11px; }
.text-secondary { color: var(--text-secondary); }
.text-muted { color: var(--text-muted); }
.text-success { color: var(--success); }
.text-error { color: var(--error); }
.text-warning { color: var(--warning); }
.text-center { text-align: center; }
.font-mono { font-family: var(--font-mono); }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.w-full { width: 100%; }
.hidden { display: none !important; }

/* ----------------------------------------------------------
   30. Responsive
   ---------------------------------------------------------- */
@media (max-width: 1024px) {
    .main-content {
        padding: 24px 20px;
    }
}

@media (max-width: 768px) {
    .sidebar {
        transform: translateX(-100%);
        transition: transform var(--transition-slow);
        z-index: 100;
    }

    .sidebar.open {
        transform: translateX(0);
    }

    .sidebar-header {
        /* 给固定汉堡按钮让位：按钮是 fixed 的，侧栏划回来后它仍浮在
           左上角，正好压住同色的 .sidebar-logo。这条 longhand 写在
           基础规则（153 行的 padding shorthand）之后，能覆盖左内边距。 */
        padding-left: 62px;
    }

    .main-content {
        margin-left: 0;
        /* 顶部留出固定汉堡按钮（38px + 上下各 12px）的高度。
           必须写在这条 shorthand 里：另起一条 padding-top 会被它覆盖。 */
        padding: 62px 16px 16px;
    }

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

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

    .modal {
        min-width: 0;
        margin: 16px;
        width: calc(100% - 32px);
    }

    .domain-input-row {
        flex-direction: column;
    }

    .wizard-steps {
        overflow-x: auto;
        padding: 16px 0;
    }

    .c-toast-host {
        left: var(--sp-3);
        right: var(--sp-3);
        bottom: var(--sp-3);
        max-width: none;
    }

    /* 汉堡按钮由 Sidebar 自持、fixed 在左上角（38px + 12px 边距）。
       页头不让位的话标题会被它压住。 */
    .c-pagehead { padding-left: 50px; }
}

/* Sidebar mobile overlay */
.sidebar-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
    z-index: 99;
}

@media (max-width: 768px) {
    .sidebar-backdrop.show {
        display: block;
    }
}

/* Mobile menu toggle */
.mobile-menu-btn {
    display: none;
    padding: 8px;
    background: none;
    border: none;
    color: var(--text-primary);
    cursor: pointer;
}

@media (max-width: 768px) {
    .mobile-menu-btn {
        display: flex;
        align-items: center;
        justify-content: center;
        /* 定位是新加的：原样式假设它嵌在某个页头里，而现在它由 Sidebar
           自持、需要在侧栏划出屏幕时仍然可见，所以必须 fixed。 */
        position: fixed;
        top: 12px;
        left: 12px;
        z-index: 120;              /* 高于 .sidebar-backdrop(99) 与 .sidebar(100) */
        width: 38px;
        height: 38px;
        background: var(--bg-secondary);
        border: 1px solid var(--border-highlight);
        border-radius: var(--radius);
    }
}

/* ----------------------------------------------------------
   Search Select Component
   ---------------------------------------------------------- */
.search-select {
    position: relative;
}

.search-select-input {
    width: 100%;
}

.search-select-dropdown {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    margin-top: 4px;
    background: var(--bg-card);
    border: 1px solid var(--border-highlight);
    border-radius: var(--radius);
    box-shadow: var(--shadow-modal);
    max-height: 240px;
    overflow-y: auto;
    z-index: 100;
}

.search-select-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 12px;
    cursor: pointer;
    transition: var(--transition-fast);
}

.search-select-item:hover {
    background: var(--bg-hover);
}

.search-select-avatar {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: var(--accent);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 12px;
    font-weight: 600;
    flex-shrink: 0;
}

.search-select-item-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.search-select-label {
    font-size: 13px;
    font-weight: 500;
    color: var(--text-primary);
}

.search-select-secondary {
    font-size: 11px;
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.search-select-loading,
.search-select-empty,
.search-select-error {
    padding: 16px;
    text-align: center;
    color: var(--text-muted);
    font-size: 13px;
}

.search-select-error {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    color: var(--error);
}

/* ----------------------------------------------------------
   31. Stats Page (SimpleChart + timeline placeholders)
   ---------------------------------------------------------- */
.simple-chart {
    width: 100%;
    overflow-x: auto;
}

/* 画布：宽度交给容器铺满，高度由 SVG 的 height presentation attribute 给
   （height 是 prop，数据驱动，样式表里写不出）——这里不设 height，免得把
   那个属性盖掉。 */
.c-chart-svg {
    width: 100%;
    display: block;
}

.chart-empty,
.timeline-empty {
    font-size: 13px;
}

.chart-empty {
    text-align: center;
    padding: var(--sp-4);
}

/* ----------------------------------------------------------
   32. 布局工具类
   972 处内联样式里绝大多数是一次性的 margin/gap/flex。
   组件化吃不掉它们——没有这一节，它们会以「组件外围布局」的
   名义全员存活。全部由令牌驱动，不写死像素值。
   ---------------------------------------------------------- */
.u-flex { display: flex; }
.u-flex-col { display: flex; flex-direction: column; }
.u-inline-flex { display: inline-flex; }
.u-items-center { align-items: center; }
.u-items-start { align-items: flex-start; }
.u-items-baseline { align-items: baseline; }
.u-justify-between { justify-content: space-between; }
.u-justify-end { justify-content: flex-end; }
.u-wrap { flex-wrap: wrap; }

.u-gap-1 { gap: var(--sp-1); }
.u-gap-2 { gap: var(--sp-2); }
.u-gap-3 { gap: var(--sp-3); }
.u-gap-4 { gap: var(--sp-4); }
.u-gap-5 { gap: var(--sp-5); }

.u-mt-1 { margin-top: var(--sp-1); }
.u-mt-2 { margin-top: var(--sp-2); }
.u-mt-3 { margin-top: var(--sp-3); }
.u-mt-4 { margin-top: var(--sp-4); }
.u-mt-5 { margin-top: var(--sp-5); }

.u-mb-1 { margin-bottom: var(--sp-1); }
.u-mb-2 { margin-bottom: var(--sp-2); }
.u-mb-3 { margin-bottom: var(--sp-3); }
.u-mb-4 { margin-bottom: var(--sp-4); }
.u-mb-5 { margin-bottom: var(--sp-5); }

.u-ml-auto { margin-left: auto; }
.u-w-full { width: 100%; }
.u-min-w-0 { min-width: 0; }

.u-truncate {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* 数值列：等宽数字，让上下行的数字能对齐比较 */
.u-tabular {
    font-variant-numeric: tabular-nums;
    font-family: var(--font-mono);
}

.u-text-2 { color: var(--text-2); }
.u-text-3 { color: var(--text-3); }
.u-fs-sm { font-size: var(--fs-sm); }
.u-fs-xs { font-size: var(--fs-xs); }

/* ----------------------------------------------------------
   33. EmptyState / DegradedPanel
   空态是正常结果，降级态是拿不到数据——两者的视觉语言必须分开：
   空态用中性色，降级态用警示色 + 重试入口。

   空态/降级态的图标是这块面板的主角，比正文里的 16/20 两档更大。
   Icon 只给两档是有意的（档位一多各页就开始填奇怪的数字），组件确实
   需要别的尺寸时走自己的 class 覆盖——CSS 的 width/height 压得住 SVG
   的表现属性。这是这条规矩唯一正当的出路，不要给 Icon 加尺寸档。
   ---------------------------------------------------------- */
.c-empty,
.c-degraded {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-6) var(--sp-4);
    text-align: center;
}

.c-empty-icon { color: var(--text-3); width: 28px; height: 28px; }
.c-empty-title { font-size: var(--fs-base); color: var(--text-2); }
.c-empty-hint { font-size: var(--fs-sm); color: var(--text-3); max-width: var(--w-prose); }
.c-empty-action { margin-top: var(--sp-1); }

.c-degraded-icon { color: var(--warn); width: 28px; height: 28px; }
.c-degraded-title { font-size: var(--fs-base); color: var(--warn); font-weight: 600; }
.c-degraded-reason {
    font-size: var(--fs-sm);
    color: var(--text-2);
    max-width: var(--w-prose);
    font-family: var(--font-mono);
}
.c-degraded-retry { margin-top: var(--sp-1); }

/* ----------------------------------------------------------
   34. DataTable
   ---------------------------------------------------------- */
.dt-toolbar {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    flex-wrap: wrap;
    padding: var(--sp-3) var(--sp-4);
    border-bottom: 1px solid var(--border-1);
}

/* 宽表在窄容器里横向滚动，而不是把卡片撑破 */
.dt-scroll { overflow-x: auto; }

.dt-table { width: 100%; }

/* 数值列：右对齐 + 等宽数字。不这么做，上下两行的数字扫不齐、比不了大小。
   两条选择器都必须带 `.dt-table td` 前缀：光写 `.dt-end`（0,1,0）压不住
   `.table th, .table td { text-align: left }`（0,1,1），而 DataTable 的
   <table> 正带着 `class="table dt-table"`——源码顺序在后也没用，特异性先判。
   这一条是 /gallery 的 getComputedStyle 断言量出来的（实测 left），
   源码扫描器和模板编译守卫都看不见。 */
.dt-table td.dt-end,
.dt-table th.dt-end { text-align: right; font-variant-numeric: tabular-nums; }
.dt-mono { font-family: var(--font-mono); font-size: var(--fs-xs); }

.dt-sortable { cursor: pointer; user-select: none; }
.dt-sortable:hover { color: var(--text-1); }
.dt-arrow { color: var(--accent-text); margin-left: var(--sp-1); }

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

.dt-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    border-top: 1px solid var(--border-1);
    font-size: var(--fs-sm);
    color: var(--text-2);
}
.dt-pager { display: flex; align-items: center; gap: var(--sp-2); }
.dt-pageno { font-variant-numeric: tabular-nums; color: var(--text-2); }

/* .dt-range（「共 42 条，显示 1-10」那句文字）没有独立样式——它是
   .dt-footer 的直接文字子节点，字号/颜色都从 .dt-footer 继承。留空
   规则只为让 class 契约守卫查得到。 */
.dt-range {}

/* 语义色工具类：给单元格插槽用，免得视图里为了标个红绿又写内联 style。
   （第 30 节那套无前缀旧工具类已冻结，新的一律走 .u- 前缀。） */
.u-text-ok { color: var(--ok); }
.u-text-err { color: var(--err); }
.u-text-warn { color: var(--warn); }

/* ----------------------------------------------------------
   36. Icon
   几何数据在 static/js/icons.js。这里只管颜色继承与垂直对齐——
   图标默认吃 currentColor，跟着它所在文字的颜色走。
   ---------------------------------------------------------- */
.c-icon {
    display: inline-block;
    vertical-align: -0.15em;
    flex: none;                 /* flex 容器里不许被压扁 */
}

/* ----------------------------------------------------------
   37. Card（折叠）/ Section
   .card 与 .card-header 沿用第 4 节 —— 那是 25 张手写卡片共同的语言。
   这里只加折叠相关的部件与纯标题分组。
   ---------------------------------------------------------- */
.c-card-title {
    margin: 0;
    font-size: var(--fs-md);
    font-weight: 600;
    color: var(--text-1);
}

.c-card-icon { color: var(--text-2); }

/* 操作区与折叠按钮靠右。card-header 的靠右规则在第 4 节，这里只保证
   两者之间有间距、且折叠按钮永远在最右。 */
.c-card-actions {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: var(--sp-2);
}

.c-card-toggle {
    margin-left: var(--sp-2);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    padding: 0;
    background: none;
    border: none;
    border-radius: var(--radius-sm);
    color: var(--accent-text);        /* 硬规则 A：可点击项不许用 --text-3 */
    cursor: pointer;
    transition: background var(--dur) ease;
}
.c-card-toggle:hover { background: var(--surface-2); }
.c-card-toggle:focus-visible { outline: none; box-shadow: var(--focus-ring); }

.c-card-body { padding: var(--sp-4); }
.c-card-footer {
    padding: var(--sp-3) var(--sp-4);
    border-top: 1px solid var(--border-1);
}

/* 收起状态：头部单独存在时不要留一条悬空的下边框 */
.c-card--collapsed .card-header { border-bottom: none; }

/* ---- Section：纯标题分组，无背景无边框 ---- */
.c-section { margin-bottom: var(--sp-5); }

.c-section-head { margin-bottom: var(--sp-3); }

.c-section-title {
    margin: 0;
    font-size: var(--fs-base);
    font-weight: 600;
    color: var(--text-1);
}

.c-section-hint {
    margin: var(--sp-1) 0 0;
    font-size: var(--fs-sm);
    line-height: var(--lh);
    color: var(--text-2);
    max-width: var(--w-prose);
}

.c-section-body { display: flex; flex-direction: column; gap: var(--sp-3); }

/* actions + toggle 同框：让 actions 吃掉剩余空间，toggle 紧贴其后。
   不然两者的 margin-left:auto 都会被算进 flex 的自由空间分配，被 1:1
   平分——实测头部中间会空出近一半宽度。
   四个类选择器压得过第 4 节 `.card-header > :last-child:not(...)`：
   class 数量列 4 > 2 直接赢，不必跟它拼 :not() 的数量。
   旧卡片匹配不上这条规则——.c-card-actions / .c-card-toggle 是本轮
   新引入的类名，25 张手写卡片的 DOM 里根本没有。 */
.c-card .card-header > .c-card-actions + .c-card-toggle {
    margin-left: var(--sp-2);
}

/* ----------------------------------------------------------
   38. StatusDot
   形状 + 文字 + 颜色三重冗余（spec 硬规则 C）。
   底色一律走 13% tint（硬规则 B：语义色永不作实心填充）——
   一套值在任何 surface 上都成立，不必逐个算 on-color。
   ---------------------------------------------------------- */
.c-dot {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    padding: 2px var(--sp-2);
    border-radius: var(--radius-full);
    font-size: var(--fs-sm);          /* 含中文，下限 13px（硬规则 D） */
    line-height: var(--lh-tight);
    border: 1px solid transparent;
}

.c-dot-mark { font-size: var(--fs-xs); line-height: 1; }
.c-dot-label { white-space: nowrap; }

.c-dot--ok      { background: var(--ok-tint);   color: var(--ok);   border-color: var(--ok); }
.c-dot--busy    { background: var(--info-tint); color: var(--info); border-color: var(--info); }
.c-dot--warn    { background: var(--warn-tint); color: var(--warn); border-color: var(--warn); }
.c-dot--error   { background: var(--err-tint);  color: var(--err);  border-color: var(--err); }
.c-dot--unknown { background: var(--surface-2); color: var(--text-2); border-color: var(--border-2); }

/* ----------------------------------------------------------
   39. Tabs（组件补充）
   基础外观在第 17 节（.tabs / .tab / .tab.active）。这里只加徽章与焦点态。
   ---------------------------------------------------------- */
.c-tab-badge {
    display: inline-block;
    margin-left: var(--sp-2);
    padding: 0 var(--sp-2);
    border-radius: var(--radius-full);
    background: var(--info-tint);          /* 硬规则 B：tint 底，不作实心填充 */
    color: var(--accent-text);
    border: 1px solid var(--accent-text);
    font-size: var(--fs-xs);               /* 纯数字，允许 12px（硬规则 D） */
    font-variant-numeric: tabular-nums;
    line-height: 1.5;
}

/* 焦点必须看得见：roving tabindex 下，方向键移动时唯一的视觉反馈就是它 */
.tab:focus-visible { outline: none; box-shadow: var(--focus-ring); border-radius: var(--radius-sm); }

/* .c-tabs（组件根）本身不需要样式——布局全靠内层 .tabs（页签条）与
   .c-tabs-panel（内容区）。它只是 Vue 把非 prop attrs 透传落脚的稳定
   选择器（gallery 的 #gal-tabs 就落在它上面），留空规则只为让 class
   契约守卫查得到。 */
.c-tabs {}

.c-tabs-panel { padding-top: var(--sp-1); }

/* ----------------------------------------------------------
   40. Modal（组件补充）
   基础外观在第 9 节（.modal-overlay / .modal / .modal-header ...）。
   2026-08-15 起第 9 节的唯一消费者就是 Modal.js 自己——七处手写弹层
   已全部迁到组件上。这里只加尺寸档与滚动约束。
   ---------------------------------------------------------- */
.c-modal { display: flex; flex-direction: column; }

/* Modal.js 的模板写的是 class="modal-header c-modal-header"：第 9 节的
   .modal-header 已经给了完整外观（flex 行 + 两端对齐），c-modal-header
   只是 BEM 命名约定要求的组件私有钩子，没有独立样式要加。留一条空规则
   只为让 class 契约守卫查得到——它不是没写全，是本来就没有要写的东西。 */
.c-modal-header {}

.c-modal--sm { width: 420px; }
.c-modal--md { width: 560px; }
.c-modal--lg { width: 760px; }

/* 长内容在**主体**里滚，不要让整个弹层滚——头部和按钮区得一直看得见，
   否则内容一长，「取消 / 确认」就被滚出视口了 */
.c-modal-body {
    overflow-y: auto;
    max-height: 60vh;
    line-height: var(--lh);
    color: var(--text-2);
}

.c-modal-footer { gap: var(--sp-2); }

.c-modal:focus-visible { outline: none; box-shadow: var(--focus-ring); }

/* ----------------------------------------------------------
   41. FormRow
   --w-form 的第一个消费者。原先 .main-content 无宽度上限，1920px 下
   表单标签到输入框横跨约 1400px。
   ---------------------------------------------------------- */
.c-formrow {
    max-width: var(--w-form);
    margin-bottom: var(--sp-4);
}

.c-formrow-label {
    display: block;
    margin-bottom: var(--sp-1);
    font-size: var(--fs-sm);
    font-weight: 600;
    color: var(--text-1);
    cursor: pointer;                  /* 点标签能聚焦输入，得让人看出来 */
}

.c-formrow-req { color: var(--err); margin-left: 2px; }

.c-formrow-hint {
    margin: 0 0 var(--sp-2);
    font-size: var(--fs-sm);
    line-height: var(--lh);
    color: var(--text-2);
    max-width: var(--w-prose);
}

/* 控件区是 flex：真实表单里「输入框 + 紧挨一个按钮」是常见形态
   （续签天数 + 保存、离线包路径 + 校验、代理 URI + 更换配置，至少三处）。
   不是 flex 的话按钮会掉到下一行去。
   输入框用 flex 而不是 width:100% —— 后者在 flex 容器里会把按钮挤出去；
   min-width:0 是必须的，否则输入框的默认最小内容宽度撑破容器。 */
.c-formrow-control {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
}

.c-formrow-control > .input,
.c-formrow-control > .form-input {
    flex: 1 1 auto;
    min-width: 0;
}

.c-formrow-constraint,
.c-formrow-error {
    margin: var(--sp-1) 0 0;
    font-size: var(--fs-sm);          /* 含中文，不许掉到 12px（硬规则 D） */
    line-height: var(--lh);
}

.c-formrow-constraint { color: var(--text-2); }
.c-formrow-error { color: var(--err); }

.c-formrow--error .input,
.c-formrow--error .form-input { border-color: var(--err); }

/* 错误态聚焦时光圈也得是红的。.c-formrow--error .input 与 .input:focus
   特异性打平 (0,2,0)，边框色靠源码顺序赢，但错误规则没声明 box-shadow，
   蓝色的强调光圈会原样留着——红边框套蓝光圈。 */
.c-formrow--error .input:focus,
.c-formrow--error .form-input:focus {
    box-shadow: 0 0 0 3px var(--err-tint);
}

/* ----------------------------------------------------------
   42. PageHeader / StickySaveBar
   ---------------------------------------------------------- */
.c-pagehead {
    display: flex;
    align-items: flex-start;
    gap: var(--sp-3);
    margin-bottom: var(--sp-5);
}

.c-pagehead-main { min-width: 0; }

.c-pagehead-title {
    margin: 0;
    font-size: var(--fs-xl);
    line-height: var(--lh-tight);
    color: var(--text-1);
}

.c-pagehead-sub {
    margin: var(--sp-1) 0 0;
    font-size: var(--fs-sm);
    line-height: var(--lh);
    color: var(--text-2);
    max-width: var(--w-prose);
}

.c-pagehead-status { margin-left: var(--sp-3); }

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

/* ---- StickySaveBar ---- */
.c-savebar {
    position: sticky;
    bottom: 0;
    z-index: var(--z-sticky);
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    background: var(--surface-3);
    border: 1px solid var(--border-2);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-card-hover);
}

.c-savebar-count { font-size: var(--fs-sm); color: var(--text-1); font-weight: 600; }
.c-savebar-spacer { margin-left: auto; }

/* .c-savebar-review / -discard / -apply 没有独立样式——三个按钮的外观
   全靠各自并列的 .btn(-ghost|-primary|-danger) .btn-sm 承担，这三个
   BEM 钩子只是给测试/未来样式定制留的稳定选择器。留空规则只为让 class
   契约守卫查得到。 */
.c-savebar-review {}
.c-savebar-discard {}
.c-savebar-apply {}

.c-savebar-warn {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    font-size: var(--fs-sm);
    color: var(--err);
}

.c-savebar--destructive {
    border-color: var(--err);
    background: var(--err-tint);      /* 硬规则 B：tint 底，不作实心填充 */
}

/* ----------------------------------------------------------
   43. ConfirmHost
   破坏性操作的确认词闸门。分寸见 spec §8.1：确认词只给破坏性档，
   全都要求输入的话它会沦为肌肉记忆，真正危险那次照样拦不住。
   ---------------------------------------------------------- */
.c-confirm-message { margin: 0 0 var(--sp-3); line-height: var(--lh); color: var(--text-1); }

.c-confirm-detail {
    margin: 0 0 var(--sp-3);
    padding-left: var(--sp-4);
    border-left: 3px solid var(--border-2);
    list-style: none;
    color: var(--text-2);          /* 默认色挂在容器上，由 li 继承 */
}
.c-confirm-detail li {
    margin-bottom: var(--sp-2);
    font-size: var(--fs-sm);
    line-height: var(--lh);
    /* 这里**不要**写 color：写了就是 (0,1,1)，会压住下面两条 (0,1,0) 的
       修饰符，语气着色永远不生效——本仓第三次栽在特异性上（前两次是
       .table td 压 .dt-end、.card-header > :last-child 压 .c-card-actions）。
       继承值永远输给直接声明，所以默认色挂容器、修饰符挂 li 才是稳的。 */
}
.c-confirm-detail--warn { color: var(--warn); }
.c-confirm-detail--err { color: var(--err); }

.c-confirm-gate {
    padding: var(--sp-3);
    background: var(--err-tint);
    border: 1px solid var(--err);
    border-radius: var(--radius);
}
.c-confirm-gate-label {
    display: block;
    margin-bottom: var(--sp-2);
    font-size: var(--fs-sm);
    color: var(--text-1);
}
.c-confirm-phrase {
    font-family: var(--font-mono);
    padding: 0 var(--sp-1);
    background: var(--bg-sunken);
    border-radius: var(--radius-sm);
    color: var(--text-1);
}
.c-confirm-input { width: 100%; }

.c-confirm-accept:disabled { opacity: 0.5; cursor: not-allowed; }

/* ----------------------------------------------------------
   44. 公告发送记录：内容单元格可展开
   默认单行截断，点一下展开全文。原先只挂了个原生 title，鼠标悬停才看得到，
   而长公告在 tooltip 里根本读不完（用户原话：「发完的公告我是看不到消息内容的」）。
   ---------------------------------------------------------- */
.c-announce-content {
    display: block;
    max-width: 42ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    cursor: pointer;
    border-bottom: 1px dashed var(--border-2);   /* 提示「这里可以点」 */
}

.c-announce-content:hover { color: var(--accent-text); }

.c-announce-content:focus-visible { outline: none; box-shadow: var(--focus-ring); }

/* 展开态：放开换行与宽度上限，长公告完整可读 */
.c-announce-content--open {
    max-width: none;
    overflow: visible;
    text-overflow: clip;
    white-space: pre-wrap;
    word-break: break-word;
    border-bottom-color: transparent;
}

/* ----------------------------------------------------------
   45. MarkupEditor（公告正文：工具栏 + textarea + 实时预览）

   预览区与输入框**必须一眼看出是两块**：这个编辑器的全部价值就是「左边打、
   右边看」，同底色时管理员分不清哪边能打字。所以两者的底色分处色阶两端
   （输入框 --bg-base 下沉，预览 --surface-2 抬起），预览再补一圈虚线边框
   与一行标题，颜色之外还有形状和文字（硬规则 C）。
   ---------------------------------------------------------- */
.c-mked {
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
}

.c-mked-bar {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-1);
}

.c-mked-tool {
    min-width: 32px;
    padding: var(--sp-1) var(--sp-2);
    background: var(--surface-2);
    color: var(--text-1);
    border: 1px solid var(--border-1);
    border-radius: var(--radius-sm);
    /* 「链接」「列表」「引用」是中文，下限 13px（硬规则 D） */
    font-size: var(--fs-sm);
    line-height: var(--lh-tight);
    cursor: pointer;
    transition: background var(--transition-fast), border-color var(--transition-fast);
}

.c-mked-tool:hover:not(:disabled) {
    background: var(--surface-3);
    border-color: var(--border-2);
}

.c-mked-tool:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* 三个记号按钮用字形本身表意，比读中文标签快。
   由 e2e 量渲染结果守着（class 契约扫描不到运行时绑定的修饰类） */
.c-mked-tool--bold { font-weight: 700; }
.c-mked-tool--italic { font-style: italic; }
.c-mked-tool--code { font-family: var(--font-mono); }

.c-mked-input {
    width: 100%;
    min-height: 140px;
    padding: var(--sp-3);
    resize: vertical;
    background: var(--bg-base);
    color: var(--text-1);
    border: 1px solid var(--border-1);
    border-radius: var(--radius);
    font-family: var(--font-sans);
    font-size: var(--fs-base);
    line-height: var(--lh);
}

.c-mked-input:focus {
    outline: none;
    border-color: var(--accent-text);
    box-shadow: var(--focus-ring);
}

/* 禁用 ＝ 正在群发，正文仍要读得清（实测 6.3:1），只是改不动 */
.c-mked-input:disabled {
    opacity: 0.6;
    cursor: not-allowed;
}

.c-mked-meta {
    /* 纯数字，--fs-xs 在硬规则 D 的允许范围内 */
    font-size: var(--fs-xs);
    font-variant-numeric: tabular-nums;
    color: var(--text-3);
    text-align: right;
}

.c-mked-pvlabel {
    font-size: var(--fs-sm);
    color: var(--text-3);
}

.c-mked-preview {
    min-height: 64px;
    /* 预览自己滚，不许把整页顶下去。接进公告卡片（T10）之后量出来的：
       4000 字顶格时预览有 ~1500px 高，「发送公告」和「已到上限」那条提示
       一起被推到折叠线以下——而那条提示恰恰是顶格那一刻最该看见的东西。 */
    max-height: 40vh;
    overflow-y: auto;
    padding: var(--sp-3);
    background: var(--surface-2);
    color: var(--text-1);
    /* 虚线 ＝ 这块不是输入框，不用读颜色也分得出来 */
    border: 1px dashed var(--border-2);
    border-radius: var(--radius);
    font-size: var(--fs-base);
    line-height: var(--lh);
    word-break: break-word;
}

.c-mked-preview p,
.c-mked-preview ul,
.c-mked-preview blockquote {
    margin: 0 0 var(--sp-2);
}

.c-mked-preview p:last-child,
.c-mked-preview ul:last-child,
.c-mked-preview blockquote:last-child {
    margin-bottom: 0;
}

.c-mked-preview ul { padding-left: var(--sp-5); }

.c-mked-preview blockquote {
    padding-left: var(--sp-3);
    border-left: 3px solid var(--border-2);
    color: var(--text-2);
}

.c-mked-preview code {
    padding: 0 var(--sp-1);
    background: var(--surface-3);
    border-radius: var(--radius-sm);
    font-family: var(--font-mono);
    /* 不写 0.9em：14px × 0.92 ＝ 12.9px，代码块里一样可能是中文 */
    font-size: var(--fs-sm);
}

.c-mked-preview a { color: var(--accent-text); }

/* ----------------------------------------------------------
   46. 全员公告：附件区与几行结论

   附件行不靠颜色分类型：image / file 的区分写成「图片」「文件」两个字
   （硬规则 C——色觉障碍下两枚不同颜色的药丸完全等价）。因此这里只有一套
   中性药丸样式，没有按 kind 分叉的修饰类——那种运行时拼出来的类名，
   test_css_class_contract.py 按设计查不到，写错了就是静默失去样式。
   ---------------------------------------------------------- */

/* 原生文件选择框藏起来，点击由「添加附件」按钮转发。
   按钮才是 Tab 位（它能 disabled、能带 spinner），所以这个 input 用
   display:none 而不是视觉隐藏——不能让键盘停在一个看不见的控件上。 */
.c-attach-input { display: none; }

.c-attach-hint {
    font-size: var(--fs-sm);        /* 含中文，不能用 --fs-xs（硬规则 D） */
    color: var(--text-3);
    line-height: var(--lh);
}

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

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

.c-attach-kind {
    flex: none;
    padding: 2px var(--sp-2);
    border-radius: var(--radius-full);
    background: var(--surface-3);
    border: 1px solid var(--border-2);
    color: var(--text-2);
    font-size: var(--fs-sm);
    line-height: var(--lh-tight);
}

.c-attach-name {
    flex: 1 1 auto;
    min-width: 0;                   /* 不写它 flex 子项不肯缩，truncate 失效 */
    color: var(--text-1);
    font-size: var(--fs-sm);
}

.c-attach-size {
    flex: none;
    color: var(--text-3);
    font-size: var(--fs-xs);        /* 纯数字与拉丁单位，允许 12px */
}

.c-attach-remove { flex: none; }

/* 发送前的几行结论：每人收到几条、顶格提醒、进度、未尝试人数、附件失败数。
   全部 13px（含中文），颜色只是第二重信号——每一条都自带文字。 */
.c-announce-plan {
    font-size: var(--fs-sm);
    color: var(--text-2);
    line-height: var(--lh);
}

.c-announce-note {
    margin-top: var(--sp-3);
    font-size: var(--fs-sm);
    color: var(--text-2);
    line-height: var(--lh);
}

.c-announce-note--warn { color: var(--warn); }
.c-announce-note--err { color: var(--err); }

/* 顶格提醒：只把计数器变红不够——颜色说不出「为什么点了加粗没反应」。
   所以这里是一整句话，底色与描边只负责让它跳出来。 */
.c-announce-cap {
    margin-top: var(--sp-2);
    padding: var(--sp-2) var(--sp-3);
    background: var(--warn-tint);
    border: 1px solid var(--warn);
    border-radius: var(--radius);
    color: var(--warn);
    font-size: var(--fs-sm);
    line-height: var(--lh);
}

/* ----------------------------------------------------------
   47. 全员公告：发送记录的回放

   历史行要回答三件事：当时发出去的是什么（那份 HTML，不是现渲染的）、
   带了哪些附件、有没有人没收到。后两件在折叠态就得看得见——展开一行才
   发现「原来这条有 10 个附件」等于没记。

   两枚提示药丸都是「tint 底 + 原色文字 + 同色描边」（硬规则 B，语义色
   永不作实心填充），并且各自带一句中文（硬规则 C，状态不许只靠颜色）。
   两者颜色不同不是装饰而是分档：附件没送达 ＝ 人收到了公告只是少了图
   （warn）；未尝试 ＝ 这批人什么都没收到（err）。
   ---------------------------------------------------------- */
.c-announce-cell {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-2);
}

.c-attach-fail,
.c-announce-untried {
    flex: none;
    padding: 2px var(--sp-2);
    border-radius: var(--radius-full);
    border: 1px solid;
    /* 含中文，不能用 --fs-xs（硬规则 D） */
    font-size: var(--fs-sm);
    line-height: var(--lh-tight);
}

.c-attach-fail {
    background: var(--warn-tint);
    border-color: var(--warn);
    color: var(--warn);
}

.c-announce-untried {
    background: var(--err-tint);
    border-color: var(--err);
    color: var(--err);
}

/* 展开态的富文本：独占一行（里面是 <p>/<ul>/<blockquote> 这些块级元素，
   挤在药丸旁边会被压成一条竖线）。child 规则与 .c-mked-preview 那一套
   同源——预览与回放显示的本来就该是同一段字。 */
.c-announce-rich {
    flex: 0 0 100%;
    color: var(--text-1);
    font-size: var(--fs-base);
    line-height: var(--lh);
    word-break: break-word;
}

.c-announce-rich p,
.c-announce-rich ul,
.c-announce-rich blockquote { margin: 0 0 var(--sp-2); }

.c-announce-rich p:last-child,
.c-announce-rich ul:last-child,
.c-announce-rich blockquote:last-child { margin-bottom: 0; }

.c-announce-rich ul { padding-left: var(--sp-5); }

.c-announce-rich blockquote {
    padding-left: var(--sp-3);
    border-left: 3px solid var(--border-2);
    color: var(--text-2);
}

.c-announce-rich code {
    padding: 0 var(--sp-1);
    background: var(--surface-3);
    border-radius: var(--radius-sm);
    font-family: var(--font-mono);
    /* 不写 0.9em：代码块里一样可能是中文 */
    font-size: var(--fs-sm);
}

.c-announce-rich a { color: var(--accent-text); }

/* 附件清单与收起按钮各自独占一行 */
.c-announce-atts {
    flex: 0 0 100%;
    margin-top: 0;
}

.c-announce-fold { flex: none; }

/* ----------------------------------------------------------
   48. 全员公告：未送达名单与定向补发

   名单最长 100 行（ESS 非商业版封顶 100 用户）。100 行摊开是 2000px 以上，
   会把历史表撑成一条走廊——上一轮 .c-mked-preview 就是因为没有高度上限，
   4000 字时把发送按钮推到了折叠线以下。

   这里的处置是**限高 + 内部滚动**，不是「只显示前 N 个，还有 M 人」：
   名单的全部价值就在于「照着 uid 去线下找人」，被截断的那部分在界面上
   再也拿不回来（没有单条公告的详情页，端点也只有列表这一个）。滚动至少
   把所有人留在够得着的地方，代价是一个嵌套滚动区——可接受，因为这一块
   是用户显式展开出来的。

   限高的是**名单本身**而不是整块：标题与补发按钮排在名单上方且不参与
   滚动，永远留在视野里。这颗按钮正是这一屏存在的理由。

   两档药丸沿用硬规则 B（tint 底 + 原色文字 + 同色描边）：
     --none     完全没收到（failed / untried）——可补发，err
     --partial  正文收到了只缺附件、以及将来的未知档——不补发，warn
   颜色只是第二重信号，每一档都自带一句中文（硬规则 C）。
   ---------------------------------------------------------- */
.c-undeliv {
    flex: 0 0 100%;                 /* 独占一行，不跟折叠态那几枚药丸挤 */
    margin-top: var(--sp-2);
    padding: var(--sp-3);
    background: var(--surface-2);
    border: 1px solid var(--border-1);
    border-radius: var(--radius);
}

.c-undeliv-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-3);
    margin-bottom: var(--sp-2);
}

.c-undeliv-title {
    flex: none;
    font-size: var(--fs-sm);        /* 含中文，不能用 --fs-xs（硬规则 D） */
    color: var(--text-1);
    line-height: var(--lh);
}

.c-undeliv-note {
    flex: 1 1 240px;
    min-width: 0;
    font-size: var(--fs-sm);
    color: var(--text-2);
    line-height: var(--lh);
}

.c-undeliv-resend { flex: none; }

.c-undeliv-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--sp-1);
    max-height: 216px;              /* 约 7 行，够看清一屏又不喧宾夺主 */
    overflow-y: auto;
}

.c-undeliv-item {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-1) var(--sp-2);
    background: var(--surface-1);
    border-radius: var(--radius-sm);
}

.c-undeliv-uid {
    flex: 1 1 auto;
    min-width: 0;
    font-family: var(--font-mono);
    font-size: var(--fs-xs);        /* 纯拉丁与符号，允许 12px */
    color: var(--text-1);
    /* uid 里没有空格，不给断点的话一个长域名会把整行撑出容器 */
    word-break: break-all;
}

.c-undeliv-reason {
    flex: none;
    padding: 2px var(--sp-2);
    border-radius: var(--radius-full);
    border: 1px solid;
    font-size: var(--fs-sm);        /* 含中文（硬规则 D） */
    line-height: var(--lh-tight);
}

.c-undeliv-reason--none {
    background: var(--err-tint);
    border-color: var(--err);
    color: var(--err);
}

.c-undeliv-reason--partial {
    background: var(--warn-tint);
    border-color: var(--warn);
    color: var(--warn);
}

/* 补发关系标记：折叠态就要看得见——同一份正文在历史里出现两遍，不标的话
   像是有人手滑发了两次。中性底给「已补发 N 次」（那是原条上的一条注记），
   info 底给「补发 · 源自 #N」（那是这一行自己的身份）。 */
.c-undeliv-tag {
    flex: none;
    padding: 2px var(--sp-2);
    border-radius: var(--radius-full);
    background: var(--surface-3);
    border: 1px solid var(--border-2);
    color: var(--text-2);
    font-size: var(--fs-sm);
    line-height: var(--lh-tight);
}

.c-undeliv-tag--resend {
    background: var(--info-tint);
    border-color: var(--info);
    color: var(--info);
}

/* ----------------------------------------------------------
   99. 动效降级（哨兵节：永远排在文件最后，新增节一律插在它前面）
   ---------------------------------------------------------- */
@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;
    }
}
