/**
 * AUTO-GENERATED — do not edit directly.
 * Edit styles-src/*.css and run: pnpm build:styles-pages
 */

/* ========== 定价页（底部弹出卡片） ========== */
.pricing-sheet-modal[hidden] {
    display: none !important;
    pointer-events: none !important;
}

.pricing-sheet-modal {
    position: fixed;
    inset: 0;
    z-index: 12100;
    display: none;
    align-items: flex-end;
    justify-content: center;
    padding: 0;
    margin: 0;
    width: 100%;
    max-width: none;
    pointer-events: none;
    isolation: isolate;
}

.pricing-sheet-modal.is-open,
.pricing-sheet-modal:not([hidden]) {
    display: flex !important;
    pointer-events: auto;
}

.pricing-sheet-overlay {
    position: absolute;
    inset: 0;
    z-index: 0;
    border: 0;
    padding: 0;
    margin: 0;
    cursor: pointer;
    background: var(--modal-overlay-bg, rgba(15, 23, 42, 0.45));
    animation: pricing-sheet-fade-in 0.22s ease;
    -webkit-tap-highlight-color: transparent;
}

.pricing-sheet-card.pricing-page {
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    flex: 0 1 auto;
    align-self: flex-end;
    width: min(1080px, 100vw);
    max-height: min(90dvh, 920px);
    min-height: 0;
    margin: 0;
    padding: 0;
    overflow: hidden;
    pointer-events: auto;
    text-align: left;
    color: var(--text-primary);
    background: var(--modal-card-bg, var(--panel-surface-bg, #fff));
    border: 1px solid var(--border-mid, rgba(0, 0, 0, 0.08));
    border-bottom: none;
    border-radius: 22px 22px 0 0;
    box-shadow: 0 -16px 48px rgba(15, 23, 42, 0.18);
    animation: pricing-sheet-slide-in 0.34s cubic-bezier(0.32, 0.72, 0, 1);
}

.pricing-sheet-scroll {
    position: relative;
    z-index: 1;
    flex: 1 1 auto;
    min-height: 0;
    padding: 28px 24px 28px;
    overflow-x: hidden;
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    touch-action: pan-y;
    scrollbar-width: none;
    -ms-overflow-style: none;
}

.pricing-sheet-scroll::-webkit-scrollbar {
    display: none;
    width: 0;
    height: 0;
}

.pricing-sheet-head {
    display: block;
    margin: 0 0 8px;
    padding: 0 44px;
    text-align: center;
}

.pricing-sheet-head .pricing-title {
    margin: 0;
    text-align: center;
    font-size: clamp(22px, 3vw, 32px);
}

.pricing-sheet-close {
    position: absolute;
    top: 8px;
    right: 12px;
    z-index: 6;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--text-secondary);
    cursor: pointer;
    pointer-events: auto;
    transition: background 0.15s ease, color 0.15s ease;
}

.pricing-sheet-close:hover,
.pricing-sheet-close:focus-visible {
    background: color-mix(in srgb, var(--text-primary) 12%, transparent);
    color: var(--text-primary);
    outline: none;
}

.pricing-sheet-close svg {
    width: 18px;
    height: 18px;
    pointer-events: none;
}

.pricing-sheet-card .pricing-subtitle {
    text-align: center;
    margin: 0 auto 18px;
    max-width: 560px;
}

.pricing-sheet-card .pricing-billing-toggle {
    display: flex;
    width: fit-content;
    margin: 0 auto 24px;
}

.pricing-sheet-card .pricing-cards {
    margin-bottom: 40px;
}

.pricing-sheet-card .pricing-faq {
    margin: 0 auto 8px;
    max-width: 720px;
}

.pricing-sheet-card .pricing-faq-list {
    width: 100%;
    max-width: 720px;
    margin: 0 auto;
}

html.is-pricing-sheet-open {
    overflow: hidden !important;
    height: 100%;
}

html.is-pricing-sheet-open body {
    overflow: hidden !important;
    overscroll-behavior: none;
}

/* 会员弹层打开时，登录/支付/Toast 必须压在其上，否则会像「点了没反应」 */
html.is-pricing-sheet-open .modal.show,
html.is-pricing-sheet-open .toast {
    z-index: 12250 !important;
}

.pricing-sheet-card .pricing-billing-btn,
.pricing-sheet-card .pricing-card-cta {
    position: relative;
    z-index: 2;
    pointer-events: auto;
    touch-action: manipulation;
    cursor: pointer;
}

@keyframes pricing-sheet-fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

@keyframes pricing-sheet-fade-out {
    from { opacity: 1; }
    to { opacity: 0; }
}

@keyframes pricing-sheet-slide-in {
    from { transform: translateY(100%); }
    to { transform: translateY(0); }
}

@keyframes pricing-sheet-slide-out {
    from { transform: translateY(0); }
    to { transform: translateY(100%); }
}

.pricing-sheet-modal.is-closing {
    display: flex !important;
    pointer-events: none;
}

.pricing-sheet-modal.is-closing .pricing-sheet-overlay {
    animation: pricing-sheet-fade-out 0.22s ease forwards;
}

.pricing-sheet-modal.is-closing .pricing-sheet-card.pricing-page {
    animation: pricing-sheet-slide-out 0.28s cubic-bezier(0.32, 0.72, 0, 1) forwards;
}

@media (prefers-reduced-motion: reduce) {
    .pricing-sheet-modal.is-closing .pricing-sheet-overlay,
    .pricing-sheet-modal.is-closing .pricing-sheet-card.pricing-page {
        animation-duration: 0.01ms !important;
    }
}

@media (max-width: 900px) {
    .pricing-sheet-card.pricing-page {
        width: 100vw;
        max-height: min(92dvh, 100%);
        border-radius: 18px 18px 0 0;
    }

    .pricing-sheet-scroll {
        padding: 28px 16px 24px;
    }

    .pricing-sheet-close {
        right: 10px;
    }

    .pricing-sheet-card .pricing-cards {
        grid-template-columns: 1fr;
        gap: 14px;
    }
}

.pricing-page {
    width: 100%;
    max-width: none;
    margin: 0;
    padding-bottom: 48px;
}

.pricing-header {
    text-align: center;
    margin-bottom: 40px;
}

.pricing-title {
    font-size: clamp(28px, 4vw, 40px);
    font-weight: 800;
    letter-spacing: -0.5px;
    margin-bottom: 12px;
}

.pricing-subtitle {
    font-size: 15px;
    color: var(--text-secondary);
    max-width: 520px;
    margin: 0 auto 24px;
    line-height: 1.6;
}

.pricing-billing-toggle {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 4px;
    background: var(--bg-elev-1);
    border: 1px solid var(--border-soft);
    border-radius: 100px;
}

.pricing-billing-btn {
    padding: 8px 18px;
    border-radius: 100px;
    font-size: 13px;
    font-weight: 600;
    color: var(--text-tertiary);
    transition: background 0.2s ease, color 0.2s ease;
}

.pricing-billing-btn:hover {
    color: var(--text-secondary);
}

.pricing-billing-btn.is-active {
    background: #fff;
    color: #111;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}

.pricing-billing-save {
    margin-left: 8px;
    padding: 4px 10px;
    font-size: 11px;
    font-weight: 700;
    color: #fff;
    background: linear-gradient(135deg, #22c55e, #16a34a);
    border-radius: 100px;
}

.pricing-cards {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 20px;
    margin-bottom: 56px;
    align-items: start;
}

.pricing-card {
    position: relative;
    display: flex;
    flex-direction: column;
    height: 100%;
    padding: 28px 24px;
    background: var(--bg-elev-1);
    border: 1px solid var(--border-soft);
    border-radius: var(--radius-lg);
    transition: border-color 0.25s ease, transform 0.25s ease, box-shadow 0.25s ease;
}

.pricing-card:hover {
    border-color: var(--border-mid);
    transform: translateY(-2px);
}

.pricing-card-featured {
    border-color: rgba(167, 139, 250, 0.45);
    background: linear-gradient(165deg, rgba(139, 92, 246, 0.12) 0%, var(--bg-elev-1) 45%);
    box-shadow: 0 12px 40px rgba(139, 92, 246, 0.15);
}

.pricing-card-badge,
.pricing-card-save {
    padding: 4px 10px;
    font-size: 11px;
    font-weight: 700;
    border-radius: 100px;
}

.pricing-card-meta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    min-height: 28px;
    margin-bottom: 8px;
}

.pricing-card-head {
    display: flex;
    flex-direction: column;
}

.pricing-card-badge {
    color: #fff;
    background: var(--gradient-accent);
}

.pricing-card-save {
    color: #fff;
    background: linear-gradient(135deg, #22c55e, #16a34a);
}

.pricing-card-name {
    font-size: 20px;
    font-weight: 700;
    margin-bottom: 12px;
}

.pricing-card-price {
    display: flex;
    align-items: baseline;
    gap: 4px;
    min-height: 36px;
    margin-bottom: 6px;
}

.pricing-price-amount {
    font-size: 36px;
    font-weight: 800;
    letter-spacing: -1px;
    line-height: 1;
}

.pricing-price-unit {
    font-size: 14px;
    color: var(--text-tertiary);
    font-weight: 500;
}

.pricing-card-billing-slot {
    min-height: 20px;
    margin-bottom: 8px;
}

.pricing-card-billing-note {
    font-size: 12px;
    color: var(--text-muted);
    margin: 0;
}

.pricing-card-desc {
    font-size: 13px;
    color: var(--text-secondary);
    margin-bottom: 20px;
    min-height: 2.5em;
}

.pricing-card-cta {
    width: 100%;
    padding: 12px 16px;
    margin-bottom: 24px;
    border-radius: 100px;
    font-size: 14px;
    font-weight: 600;
    color: #fff;
    background: var(--gradient-accent);
    box-shadow: 0 4px 14px rgba(139, 92, 246, 0.35);
    transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.pricing-card-cta:hover {
    transform: translateY(-1px);
    box-shadow: 0 6px 20px rgba(139, 92, 246, 0.45);
}

.pricing-card-cta-outline {
    color: var(--text-primary);
    background: transparent;
    border: 1px solid var(--border-mid);
    box-shadow: none;
}

.pricing-card-cta-outline:hover {
    background: var(--bg-elev-2);
    box-shadow: none;
}

.pricing-card-cta-primary {
    background: var(--gradient-primary);
    box-shadow: 0 4px 18px rgba(236, 72, 153, 0.35);
}

.pricing-features {
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin: 0;
    padding: 0;
}

.pricing-features li {
    position: relative;
    padding-left: 22px;
    font-size: 13px;
    color: var(--text-secondary);
    line-height: 1.45;
}

.pricing-features li::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0.45em;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--accent-purple);
    opacity: 0.85;
}

.pricing-faq-title {
    font-size: 28px;
    font-weight: 800;
    text-align: center;
    margin-bottom: 8px;
}

.pricing-faq-subtitle {
    text-align: center;
    font-size: 14px;
    color: var(--text-secondary);
    margin-bottom: 28px;
}

.pricing-faq {
    width: 100%;
    max-width: none;
}

.pricing-faq-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
    width: 100%;
}

.pricing-faq-item {
    width: 100%;
    box-sizing: border-box;
    background: var(--bg-elev-1);
    border: 1px solid var(--border-soft);
    border-radius: var(--radius-md);
    overflow: hidden;
}

.pricing-faq-item summary {
    padding: 16px 18px;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    list-style: none;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
}

.pricing-faq-item summary::-webkit-details-marker {
    display: none;
}

.pricing-faq-item summary::after {
    content: "+";
    font-size: 18px;
    font-weight: 400;
    color: var(--text-tertiary);
    flex-shrink: 0;
}

.pricing-faq-item[open] summary::after {
    content: "−";
}

.pricing-faq-item p {
    padding: 0 18px 16px;
    font-size: 13px;
    color: var(--text-secondary);
    line-height: 1.65;
}
/* ========== AI 工具工作台（左右排版，对齐资产库弹窗） ========== */
.modal.ai-tool-modal {
    z-index: 1100;
}

.modal.ai-tool-modal.show {
    align-items: center;
    justify-content: center;
    padding: 40px;
    box-sizing: border-box;
}

.modal.ai-tool-modal .modal-overlay {
    background: rgba(0, 0, 0, 0.35);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
}

.ai-tool-modal-card {
    position: relative;
    z-index: 1;
    width: min(1120px, calc(100vw - 80px));
    height: min(720px, calc(100vh - 80px));
    max-width: none;
    max-height: none;
    margin: 0;
    padding: 0;
    text-align: left;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    border: 1px solid rgba(0, 0, 0, 0.08);
    border-radius: 16px;
    box-shadow:
        0 8px 16px rgba(0, 0, 0, 0.06),
        0 24px 48px rgba(0, 0, 0, 0.12);
    animation: ai-tool-modal-in 0.24s ease;
}

@keyframes ai-tool-modal-in {
    from {
        opacity: 0;
        transform: scale(0.98) translateY(8px);
    }
    to {
        opacity: 1;
        transform: none;
    }
}

[data-theme="dark"] .ai-tool-modal-card {
    border-color: rgba(255, 255, 255, 0.1);
    box-shadow:
        0 8px 16px rgba(0, 0, 0, 0.25),
        0 24px 48px rgba(0, 0, 0, 0.45);
}

@media (prefers-color-scheme: dark) {
    [data-theme="system"] .ai-tool-modal-card {
        border-color: rgba(255, 255, 255, 0.1);
        box-shadow:
            0 8px 16px rgba(0, 0, 0, 0.25),
            0 24px 48px rgba(0, 0, 0, 0.45);
    }
}

.ai-tool-modal-layout {
    display: flex;
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
}

.ai-tool-modal-sidebar {
    flex: 0 0 320px;
    width: 320px;
    display: flex;
    flex-direction: column;
    gap: 12px;
    padding: 24px 16px 16px 20px;
    border-right: 1px solid rgba(0, 0, 0, 0.06);
    background: #f7f7f8;
    box-sizing: border-box;
    min-height: 0;
}

[data-theme="dark"] .ai-tool-modal-sidebar {
    background: #161618;
    border-right-color: rgba(255, 255, 255, 0.08);
}

@media (prefers-color-scheme: dark) {
    [data-theme="system"] .ai-tool-modal-sidebar {
        background: #161618;
        border-right-color: rgba(255, 255, 255, 0.08);
    }
}

.ai-tool-modal-sidebar-head {
    flex: 0 0 auto;
    min-width: 0;
    padding: 0 4px 4px;
}

.ai-tool-modal-title {
    margin: 0;
    font-size: 18px;
    font-weight: 700;
    line-height: 1.3;
    color: var(--text-primary);
}

.ai-tool-modal-desc {
    margin: 6px 0 0;
    font-size: 13px;
    line-height: 1.45;
    color: var(--text-tertiary);
}

.ai-tool-modal-main {
    flex: 1 1 auto;
    min-width: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background: #ffffff;
}

[data-theme="dark"] .ai-tool-modal-main {
    background: var(--panel-surface-bg, #1e1e1e);
}

@media (prefers-color-scheme: dark) {
    [data-theme="system"] .ai-tool-modal-main {
        background: var(--panel-surface-bg, #1e1e1e);
    }
}

.ai-tool-modal-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    min-height: 56px;
    padding: 12px 20px;
    border-bottom: 1px solid rgba(0, 0, 0, 0.06);
    background: #ffffff;
    flex: 0 0 auto;
    box-sizing: border-box;
}

[data-theme="dark"] .ai-tool-modal-head {
    background: var(--panel-surface-bg, #1e1e1e);
    border-bottom-color: rgba(255, 255, 255, 0.08);
}

@media (prefers-color-scheme: dark) {
    [data-theme="system"] .ai-tool-modal-head {
        background: var(--panel-surface-bg, #1e1e1e);
        border-bottom-color: rgba(255, 255, 255, 0.08);
    }
}

.ai-tool-modal-head-leading {
    min-width: 0;
    flex: 1 1 auto;
}

.ai-tool-modal-head-actions {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    margin-left: auto;
}

/* 结果操作被搬进标题行之后（手机端，见 15-ai-tools.js 的
   syncAiToolResultActionsPlacement），下面那条 12px 的上边距失去参照 —— 它是
   给「贴在结果图下面」写的，在标题行里只会把整行顶高。间距也收紧一档：
   标题行比结果区窄，同样 8px 会显得松。 */
.ai-tool-modal-head-actions .ai-tool-result-actions {
    margin-top: 0;
    gap: 6px;
}

.ai-tool-modal-head .ai-tool-pane-title {
    margin: 0;
    text-transform: none;
    letter-spacing: 0;
    font-size: 15px;
    font-weight: 650;
    color: var(--text-primary);
}

.ai-tool-modal-close {
    flex: 0 0 auto;
    width: 36px;
    height: 36px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: none;
    border-radius: 50%;
    background: transparent;
    color: var(--text-secondary);
    cursor: pointer;
}

.ai-tool-modal-close:hover,
.ai-tool-modal-close:focus-visible {
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
    color: var(--text-primary);
    outline: none;
}

.ai-tool-modal-close svg {
    width: 18px;
    height: 18px;
}

.ai-tool-modal-body {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.ai-tool-pane {
    min-width: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
    padding: 16px 20px 18px;
    flex: 1 1 auto;
}

.ai-tool-pane--result {
    height: 100%;
}

.ai-tool-pane-title {
    margin: 0 0 12px;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted, var(--text-tertiary));
}

.ai-tool-form {
    display: flex;
    flex-direction: column;
    gap: 12px;
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
    padding-right: 4px;
}

.ai-tool-field {
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
}

.ai-tool-field-label {
    font-size: 13px;
    font-weight: 550;
    color: var(--text-secondary);
}

.ai-tool-input {
    width: 100%;
    box-sizing: border-box;
    border: 1px solid var(--border-mid);
    border-radius: 12px;
    background: #fff;
    color: var(--text-primary);
    font: inherit;
    font-size: 14px;
    line-height: 1.45;
    padding: 10px 12px;
    outline: none;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
    transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
}

.ai-tool-input:focus {
    border-color: color-mix(in srgb, var(--text-primary) 28%, var(--border-mid));
    background: #fff;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--text-primary) 8%, transparent);
}

[data-theme="dark"] .ai-tool-input {
    background: color-mix(in srgb, var(--text-primary) 6%, transparent);
    box-shadow: none;
}

@media (prefers-color-scheme: dark) {
    [data-theme="system"] .ai-tool-input {
        background: color-mix(in srgb, var(--text-primary) 6%, transparent);
        box-shadow: none;
    }
}

.ai-tool-select {
    position: relative;
    width: 100%;
}

.ai-tool-select-native {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

.ai-tool-select-trigger {
    width: 100%;
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin: 0;
    padding: 10px 12px;
    border: 1px solid var(--border-mid);
    border-radius: 12px;
    background: #fff;
    color: var(--text-primary);
    font: inherit;
    font-size: 14px;
    line-height: 1.45;
    text-align: left;
    cursor: pointer;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
    transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
}

.ai-tool-select-trigger:hover {
    border-color: color-mix(in srgb, var(--text-primary) 22%, var(--border-mid));
}

.ai-tool-select-trigger:focus-visible {
    outline: none;
    border-color: color-mix(in srgb, var(--text-primary) 28%, var(--border-mid));
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--text-primary) 8%, transparent);
}

.ai-tool-select.is-open .ai-tool-select-trigger {
    border-color: color-mix(in srgb, var(--text-primary) 28%, var(--border-mid));
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--text-primary) 8%, transparent);
}

.ai-tool-select-value {
    min-width: 0;
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ai-tool-select-chevron {
    width: 16px;
    height: 16px;
    flex: 0 0 auto;
    color: var(--text-tertiary);
    transition: transform 0.18s ease;
}

.ai-tool-select.is-open .ai-tool-select-chevron {
    transform: rotate(180deg);
}

.ai-tool-select-menu {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    right: 0;
    z-index: 40;
    display: flex;
    flex-direction: column;
    gap: 2px;
    max-height: var(--ai-tool-select-menu-max, 240px);
    overflow: auto;
    padding: 6px;
    border: 1px solid color-mix(in srgb, var(--text-primary) 10%, var(--border-soft));
    border-radius: 14px;
    background: #fff;
    box-shadow: 0 14px 32px rgba(0, 0, 0, 0.12);
    box-sizing: border-box;
    overscroll-behavior: contain;
}

/* 靠近表单底部时翻到触发器上方展开（is-drop-up 由 JS 按可用空间判定），
   阴影也跟着翻个方向，不然会朝没有菜单的那一侧投。 */
.ai-tool-select.is-drop-up .ai-tool-select-menu {
    top: auto;
    bottom: calc(100% + 6px);
    box-shadow: 0 -14px 32px rgba(0, 0, 0, 0.12);
}

.ai-tool-select-menu[hidden] {
    display: none !important;
}

.ai-tool-select-option {
    appearance: none;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    width: 100%;
    margin: 0;
    padding: 9px 10px;
    border: none;
    border-radius: 10px;
    background: transparent;
    color: var(--text-primary);
    font: inherit;
    font-size: 13.5px;
    line-height: 1.35;
    text-align: left;
    cursor: pointer;
}

.ai-tool-select-option:hover,
.ai-tool-select-option:focus-visible {
    background: color-mix(in srgb, var(--text-primary) 6%, transparent);
    outline: none;
}

.ai-tool-select-option.is-active {
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
    font-weight: 600;
}

.ai-tool-select-option-check {
    width: 14px;
    height: 14px;
    flex: 0 0 auto;
    opacity: 0;
    color: var(--text-primary);
}

.ai-tool-select-option.is-active .ai-tool-select-option-check {
    opacity: 1;
}

[data-theme="dark"] .ai-tool-select-trigger,
[data-theme="dark"] .ai-tool-select-menu {
    background: #2c2c2e;
    box-shadow: 0 14px 32px rgba(0, 0, 0, 0.35);
}

[data-theme="dark"] .ai-tool-select.is-drop-up .ai-tool-select-menu {
    box-shadow: 0 -14px 32px rgba(0, 0, 0, 0.35);
}

@media (prefers-color-scheme: dark) {
    [data-theme="system"] .ai-tool-select-trigger,
    [data-theme="system"] .ai-tool-select-menu {
        background: #2c2c2e;
        box-shadow: 0 14px 32px rgba(0, 0, 0, 0.35);
    }

    [data-theme="system"] .ai-tool-select.is-drop-up .ai-tool-select-menu {
        box-shadow: 0 -14px 32px rgba(0, 0, 0, 0.35);
    }
}

.ai-tool-textarea {
    min-height: 120px;
    resize: vertical;
}

.ai-tool-form-actions {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: auto;
    padding-top: 12px;
    flex: 0 0 auto;
    width: 100%;
}

.ai-tool-generate-btn {
    appearance: none;
    border: none;
    border-radius: 999px;
    padding: 10px 18px;
    width: 100%;
    min-width: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    font: inherit;
    font-size: 14px;
    font-weight: 600;
    color: #fff;
    background: var(--text-primary);
    cursor: pointer;
}

.ai-tool-generate-btn:hover:not(:disabled) {
    filter: brightness(1.05);
}

.ai-tool-generate-btn:disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

[data-theme="dark"] .ai-tool-generate-btn {
    color: #111;
    background: #f5f5f7;
}

@media (prefers-color-scheme: dark) {
    [data-theme="system"] .ai-tool-generate-btn {
        color: #111;
        background: #f5f5f7;
    }
}

.ai-tool-generate-price {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    flex: 0 0 auto;
    color: inherit;
    white-space: nowrap;
    line-height: 1;
}

.ai-tool-generate-price[hidden] {
    display: none !important;
}

.ai-tool-generate-price-icon.topbar-points-icon,
.ai-tool-generate-btn .ai-tool-generate-price-icon {
    display: block;
    width: 14px;
    height: 14px;
    color: inherit;
    flex-shrink: 0;
}

.ai-tool-generate-price-value {
    font-size: 14px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: inherit;
    line-height: 1;
}

.ai-tool-generate-label {
    line-height: 1;
}

.ai-tool-stop-btn {
    display: none !important;
}

.ai-tool-upload {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 10px;
}

.ai-tool-upload-input {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

.ai-tool-upload-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 72px;
    height: 72px;
    margin: 0;
    padding: 0;
    border: 1px dashed color-mix(in srgb, var(--text-primary) 22%, var(--border-mid));
    border-radius: 14px;
    background: color-mix(in srgb, var(--text-primary) 4%, transparent);
    color: var(--text-secondary);
    cursor: pointer;
    box-sizing: border-box;
    transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.ai-tool-upload-btn:hover,
.ai-tool-upload-btn:focus-visible {
    color: var(--text-primary);
    border-color: color-mix(in srgb, var(--text-primary) 40%, var(--border-mid));
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
    outline: none;
}

.ai-tool-upload-btn svg {
    width: 22px;
    height: 22px;
}

.ai-tool-upload.has-file .ai-tool-upload-btn {
    display: none;
}

.ai-tool-upload-meta {
    display: none;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
}

.ai-tool-upload.has-file .ai-tool-upload-meta {
    display: flex;
}

.ai-tool-upload-preview-wrap {
    position: relative;
    width: 72px;
    height: 72px;
    border-radius: 14px;
    overflow: hidden;
    border: 1px solid var(--border-soft);
    background: color-mix(in srgb, var(--text-primary) 4%, transparent);
    flex: 0 0 auto;
}

.ai-tool-upload-preview {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.ai-tool-upload-clear {
    position: absolute;
    top: 4px;
    right: 4px;
    z-index: 2;
    width: 22px;
    height: 22px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin: 0;
    padding: 0;
    border: none;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.62);
    color: #fff;
    cursor: pointer;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
}

.ai-tool-upload-clear:hover,
.ai-tool-upload-clear:focus-visible {
    background: rgba(0, 0, 0, 0.82);
    outline: none;
}

.ai-tool-upload-clear[hidden] {
    display: none !important;
}

.ai-tool-upload-clear svg {
    width: 12px;
    height: 12px;
}

.ai-tool-upload-name {
    font-size: 12px;
    color: var(--text-tertiary);
    max-width: 180px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ai-tool-result-wrap {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
}

.ai-tool-result-stage {
    flex: 1 1 auto;
    min-height: 0;
    display: none;
}

.ai-tool-result-stage.is-active {
    display: flex;
    flex-direction: column;
}

.ai-tool-result-frame {
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
    border-radius: 14px;
    border: 1px dashed color-mix(in srgb, var(--text-primary) 12%, transparent);
    background: color-mix(in srgb, var(--text-primary) 2.5%, transparent);
    overflow: auto;
    box-sizing: border-box;
}

.ai-tool-result-frame--text {
    align-items: stretch;
    justify-content: flex-start;
}

.ai-tool-result-frame--image,
.ai-tool-result-frame--video {
    background:
        linear-gradient(45deg, color-mix(in srgb, var(--text-primary) 4%, transparent) 25%, transparent 25%),
        linear-gradient(-45deg, color-mix(in srgb, var(--text-primary) 4%, transparent) 25%, transparent 25%),
        linear-gradient(45deg, transparent 75%, color-mix(in srgb, var(--text-primary) 4%, transparent) 75%),
        linear-gradient(-45deg, transparent 75%, color-mix(in srgb, var(--text-primary) 4%, transparent) 75%);
    background-size: 20px 20px;
    background-position: 0 0, 0 10px, 10px -10px, -10px 0;
}

.ai-tool-result-gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: 10px;
    width: 100%;
}

.ai-tool-result-gallery img {
    width: 100%;
    aspect-ratio: 1;
    object-fit: cover;
    border-radius: 10px;
}

.ai-tool-result-image,
.ai-tool-result-video {
    max-width: 100%;
    max-height: 100%;
    border-radius: 12px;
}

/* Class display rules otherwise override the hidden attribute and push the placeholder off-center. */
.ai-tool-result-gallery[hidden],
.ai-tool-result-image[hidden],
.ai-tool-result-video[hidden],
.ai-tool-result-body[hidden],
.ai-tool-result-placeholder[hidden],
.ai-tool-result-empty[hidden] {
    display: none !important;
}

.ai-tool-result-placeholder,
.ai-tool-result-empty {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    margin: 0;
    width: max-content;
    max-width: min(260px, calc(100% - 32px));
    font-size: 13px;
    line-height: 1.5;
    color: var(--text-tertiary);
    text-align: center;
    pointer-events: none;
}

.ai-tool-result-body {
    margin: 0;
    white-space: pre-wrap;
    word-break: break-word;
    font-size: 14px;
    line-height: 1.65;
    color: var(--text-primary);
    border-radius: 12px;
    width: 100%;
}

.ai-tool-result-body.is-streaming::after {
    content: "▍";
    margin-left: 2px;
    animation: ai-tool-caret 1s steps(1) infinite;
    color: var(--text-tertiary);
}

@keyframes ai-tool-caret {
    50% { opacity: 0; }
}

.ai-tool-result-actions {
    display: flex;
    justify-content: flex-end;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 12px;
    flex: 0 0 auto;
}

.ai-tool-copy-btn,
.ai-tool-download-btn {
    appearance: none;
    border: 1px solid var(--border-mid);
    border-radius: 999px;
    padding: 8px 14px;
    font: inherit;
    font-size: 13px;
    font-weight: 550;
    color: var(--text-primary);
    background: color-mix(in srgb, var(--text-primary) 4%, transparent);
    cursor: pointer;
}

.ai-tool-copy-btn:hover,
.ai-tool-download-btn:hover {
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
}

/* Legacy batch / studio modal: keep two-column body until migrated */
.ai-tool-modal-card:not(:has(.ai-tool-modal-layout)) {
    width: min(920px, 94vw);
    height: auto;
    max-height: min(88vh, 820px);
}

.ai-tool-modal-card:not(:has(.ai-tool-modal-layout)) .ai-tool-modal-head {
    align-items: flex-start;
    padding: 20px 22px 14px;
}

.ai-tool-modal-card:not(:has(.ai-tool-modal-layout)) .ai-tool-modal-body {
    display: grid;
    grid-template-columns: minmax(280px, 0.95fr) minmax(300px, 1.05fr);
    overflow: hidden;
}

.ai-tool-modal-card:not(:has(.ai-tool-modal-layout)) .ai-tool-pane--form {
    border-right: 1px solid var(--border-mid);
}

.ai-tool-modal-card:not(:has(.ai-tool-modal-layout)) .ai-tool-form-actions {
    margin-top: 14px;
}

@media (max-width: 900px) {
    /* 手机端：底部 sheet，规格与素材库（#asset-library-modal）对齐。
       要点与素材库一致：
         - 高度用 92dvh 而不是 92vh —— vh 不随地址栏收起/展开变化，
           按 92vh 算出来的高度会顶到可见区下面，底部的「生成」按钮被地址栏盖住；
         - 顶端 18px 圆角 + 只留上边框 + 向上投影，是「贴底抽屉」的视觉语言，
           居中式弹窗那套四周投影（0 24px 48px）贴在屏幕底边会显得断层；
         - 从底部滑入，而不是居中式弹窗的缩放淡入。 */
    .modal.ai-tool-modal.show {
        padding: 0;
        align-items: flex-end;
        justify-content: center;
    }

    /* 选择器带到 .modal 层级（3 个 class）是有意的：
       [data-theme="dark"] .ai-tool-modal-card 和
       [data-theme="system"] .ai-tool-modal-card 都只有 2 个 class，
       它们写的是居中式弹窗那套四周投影，会盖掉这里的向上投影 ——
       深色模式下 sheet 底部就会挂一圈假阴影。 */
    .modal.ai-tool-modal .ai-tool-modal-card {
        position: relative;
        top: auto;
        right: auto;
        bottom: auto;
        left: auto;
        align-self: flex-end;
        width: 100vw;
        max-width: none;
        height: min(92dvh, 100%);
        max-height: min(92dvh, 100%);
        margin: 0;
        transform: none;
        border-radius: 18px 18px 0 0;
        /* 不写 border：手机端弹层统一不画描边，见 27-mobile.css 里的说明。 */
        overflow: hidden;
        box-shadow: 0 -16px 48px rgba(15, 23, 42, 0.18);
        animation: ai-tool-sheet-slide-in 0.34s cubic-bezier(0.32, 0.72, 0, 1);
    }

    .ai-tool-modal-layout {
        flex-direction: column;
    }

    /* 选择器带到 .modal 层级同样是压特异性：输入区在深色下被
       [data-theme="dark"] .ai-tool-modal-sidebar 写成了 #161618（比卡片底色更深），
       那两条规则只有 2 个 class，会盖掉这里的背景色。

       这里用 transparent 而不是 var(--modal-card-bg)：
       顶部拖拽区是透明的，「它的背景」就是卡片底色。输入区跟着透明，
       两者画出来必然是同一个色 —— 而且 $settings 面板背景支持自定义颜色，
       玻璃态（opacity < 100）时 --panel-surface-bg 是 rgba，
       输入区要是自己也刷一层 rgba，叠在卡片那层上面会明显更深一道。 */
    .modal.ai-tool-modal .ai-tool-modal-sidebar {
        flex: 0 0 auto;
        width: 100%;
        max-height: 46%;
        border-right: none;
        border-bottom: 1px solid rgba(0, 0, 0, 0.06);
        background: transparent;
    }

    [data-theme="dark"] .ai-tool-modal-sidebar {
        border-bottom-color: rgba(255, 255, 255, 0.08);
    }

    /* 顶部已由 .mobile-sheet-close 提供关闭入口（与素材库一致），
       卡片内那个留着就是两颗叉。 */
    .ai-tool-modal-card > .ai-tool-modal-layout .ai-tool-modal-close {
        display: none !important;
    }

    /* 标题上移到抓手那一行的左边（公共的 .mobile-sheet-title-row，
       见 11-auth-boot.js + 27-mobile.css），说明文字在手机端直接省掉 ——
       抽屉高度本来就紧，这两行吃掉 73px 却不提供任何操作，往下还有一屏表单要放。
       素材库在手机端同样把侧栏标题 display:none，这里保持一致。 */
    .modal.ai-tool-modal .ai-tool-modal-sidebar-head {
        display: none;
    }

    /* ---- 结果操作搬到「结果」那一行 ----
       下载 / 发送到画布 / 查看生成记录 在宽屏是结果图下面的一排药丸。手机端这一排
       连间距要吃掉 50px，而抽屉高度本来就紧（图片得尽量大），把它搬到标题行靠右摆。

       节点是 15-ai-tools.js 的 syncAiToolResultActionsPlacement 搬的，不是 CSS：
       操作和标题不在同一棵子树里（标题在 .ai-tool-modal-main 下，操作在
       .ai-tool-modal-body > section 下），绝对定位能凑出同样的画面，但「头部换行、
       按钮换行」都得自己算，真搬进标题行就交给 flex 了。

       高度仍是 56px（跟抓手那一行、跟素材库对齐）：上下内边距从 12px 收到 8px，
       否则内容盒只剩 32px，38px 高的按钮会把这一行撑到 62px。 */
    .modal.ai-tool-modal .ai-tool-modal-head {
        padding: 8px 16px;
        /* 窄机上「结果」+ 三颗按钮摆不下一行时，整组操作落到第二行，
           而不是把按钮压扁 —— 38px 是触控目标的尺寸，压不得 */
        flex-wrap: wrap;
    }

    /* 标题保持自己的宽度。它的基准是 flex: 1 1 auto + min-width: 0，
       让它收缩的话会被压成 0 去给操作腾地方，而正确做法是换行。 */
    .modal.ai-tool-modal .ai-tool-modal-head-leading {
        flex: 0 0 auto;
    }

    /* 下载排到「查看生成记录」右边。
       同一行内的先后用 order，不动节点：搬节点是为了换容器，换位置不用。 */
    .modal.ai-tool-modal #ai-tool-download-btn {
        order: 1;
    }

    /* 继续编辑在手机端是多余的一步：输入区就压在结果上面，想改参数直接往上滑，
       不需要先「回到编辑」。宽屏保留 —— 那边是两栏并排，意义不一样。 */
    .modal.ai-tool-modal #ai-tool-reedit-btn {
        display: none;
    }

    /* 还没点生成时结果区不占位。
       手机上输入区压在结果区上面，闲置状态下结果区只能显示「填写信息后点击生成…」
       这句占位文案，却要吃掉小半屏 —— 弹层看起来像坏了一半。

       用 :not(...="active") 而不是 =="idle"：属性缺失时也算空闲，
       这样即使有哪条路径没走到 clearAiToolResult，也不会漏出占位文案。
       状态由 15-ai-tools.js 的 syncAiToolResultVisibility 维护。

       三条都要带上 .modal.ai-tool-modal 前缀：卡片和输入区的基准规则是 3 个
       class 的选择器，这里只有 2 个的话高度会被基准值顶回去（收不起来）。 */
    .modal.ai-tool-modal .ai-tool-modal-card:not([data-ai-tool-result="active"]) .ai-tool-modal-head,
    .modal.ai-tool-modal .ai-tool-modal-card:not([data-ai-tool-result="active"]) .ai-tool-pane--result {
        display: none;
    }

    /* 收起结果区后弹层跟着变成内容高度，底部不留半屏空白；
       但高度上限还得保留 —— 字段多的工具（批处理那种）会超过 92dvh。

       max-height: none 是必须的：输入区的 46% 上限是按 92dvh 定的，
       卡片改成内容高度后这个百分比失去参照，会一直按固定高度撑住，
       反而把收缩抵消掉。

       flex-shrink / overflow 也是必须的，不是顺手加的：
       输入区基准是 flex: 0 0 auto（不收缩）。去掉高度上限后，字段多的工具
       内容会超过 92dvh，卡片被 max-height 卡住，而输入区不肯收缩，
       底部的「生成」按钮就被卡片裁掉了 —— 用户点不到。允许收缩之后，
       .ai-tool-form 的 overflow:auto 才会接手，在表单内部滚。 */
    .modal.ai-tool-modal .ai-tool-modal-card:not([data-ai-tool-result="active"]) {
        height: auto;
        max-height: min(92dvh, 100%);
    }

    .modal.ai-tool-modal .ai-tool-modal-card:not([data-ai-tool-result="active"]) .ai-tool-modal-sidebar {
        max-height: none;
        flex-shrink: 1;
        overflow: hidden;
    }

    .ai-tool-modal-card:not(:has(.ai-tool-modal-layout)) .ai-tool-modal-body {
        grid-template-columns: 1fr;
        overflow: auto;
    }

    .ai-tool-modal-card:not(:has(.ai-tool-modal-layout)) .ai-tool-pane--form {
        border-right: none;
        border-bottom: 1px solid var(--border-mid);
    }

    .ai-tool-result-frame {
        min-height: 180px;
    }
}

@keyframes ai-tool-sheet-slide-in {
    from {
        transform: translateY(100%);
    }
    to {
        transform: translateY(0);
    }
}

/* ========== 本地工具：滑块 / 开关 / 色卡 / 重复图分组 ========== */

.ai-tool-range-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
}

.ai-tool-range-value {
    font-size: 13px;
    font-variant-numeric: tabular-nums;
    color: var(--text-primary);
}

.ai-tool-range-input {
    width: 100%;
    margin: 2px 0 0;
    accent-color: var(--ui-accent, #0f766e);
}

.ai-tool-field--switch {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    cursor: pointer;
}

.ai-tool-switch {
    position: relative;
    flex: 0 0 auto;
    width: 44px;
    height: 26px;
}

.ai-tool-switch input {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    opacity: 0;
    cursor: pointer;
}

.ai-tool-switch-ui {
    display: block;
    width: 100%;
    height: 100%;
    border-radius: 999px;
    background: color-mix(in srgb, var(--text-primary) 14%, transparent);
    transition: background 0.16s ease;
}

.ai-tool-switch-ui::after {
    content: "";
    position: absolute;
    top: 3px;
    left: 3px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.22);
    transition: transform 0.16s ease;
}

.ai-tool-switch input:checked + .ai-tool-switch-ui {
    background: var(--ui-accent, #0f766e);
}

.ai-tool-switch input:checked + .ai-tool-switch-ui::after {
    transform: translateX(18px);
}

.ai-tool-switch input:focus-visible + .ai-tool-switch-ui {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--ui-accent, #0f766e) 28%, transparent);
}

.ai-tool-palette-hero {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    gap: 2px;
    min-height: 132px;
    padding: 14px;
    margin-bottom: 14px;
    border-radius: 14px;
    color: #fff;
    text-shadow: 0 1px 6px rgba(0, 0, 0, 0.35);
}

.ai-tool-palette-hero-hex {
    font-size: 22px;
    font-weight: 650;
    letter-spacing: 0.02em;
}

.ai-tool-palette-hero-label {
    font-size: 12px;
    opacity: 0.92;
}

.ai-tool-palette-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.ai-tool-palette-row {
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 13px;
}

.ai-tool-palette-swatch {
    flex: 0 0 auto;
    width: 26px;
    height: 26px;
    border-radius: 8px;
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}

.ai-tool-palette-hex {
    font-variant-numeric: tabular-nums;
    font-weight: 550;
    color: var(--text-primary);
}

.ai-tool-palette-rgb {
    color: var(--text-tertiary);
    font-size: 12px;
}

.ai-tool-palette-pct {
    margin-left: auto;
    color: var(--text-secondary);
    font-variant-numeric: tabular-nums;
}

.ai-tool-palette-tag {
    padding: 1px 8px;
    border-radius: 999px;
    font-size: 11px;
    color: var(--ui-accent, #0f766e);
    background: color-mix(in srgb, var(--ui-accent, #0f766e) 12%, transparent);
}

/* 内容审核结论：绿通过 / 琥珀存疑 / 红拦截。颜色本身是主要信息，
   文字只是补充，所以底色给足，别和旁边的中性数值混在一起。 */
.ai-tool-nsfw-tag--pass {
    color: #047857;
    background: color-mix(in srgb, #10b981 16%, transparent);
}

.ai-tool-nsfw-tag--warn {
    color: #b45309;
    background: color-mix(in srgb, #f59e0b 18%, transparent);
}

.ai-tool-nsfw-tag--block {
    color: #b91c1c;
    background: color-mix(in srgb, #ef4444 16%, transparent);
}

/* 标签 PDF：给一张单张标签的预览 + 真正的下载入口 */
.ai-tool-label-preview {
    display: block;
    width: 100%;
    max-width: 280px;
    height: auto;
    margin: 0 auto 12px;
    border: 1px solid color-mix(in srgb, var(--text-primary) 12%, transparent);
    border-radius: 10px;
    background: #fff;
}

.ai-tool-label-download {
    display: block;
    margin: 0 auto 14px;
    padding: 9px 18px;
    width: fit-content;
    border-radius: 10px;
    font-size: 13px;
    font-weight: 600;
    text-decoration: none;
    color: #fff;
    background: var(--ui-accent, #0f766e);
}

.ai-tool-label-download:hover {
    filter: brightness(1.06);
}

/* ---------- 表格导入结果 ---------- */

.ai-tool-sheet-wrap {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.ai-tool-sheet-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

/* 宽表（几十列）一定要能横向滚，不然会把结果区撑破 */
.ai-tool-sheet-scroll {
    overflow: auto;
    max-height: 340px;
    border: 1px solid color-mix(in srgb, var(--text-primary) 12%, var(--border-soft));
    border-radius: 12px;
    overscroll-behavior: contain;
}

.ai-tool-sheet-table {
    border-collapse: collapse;
    width: 100%;
    font-size: 12.5px;
    white-space: nowrap;
}

.ai-tool-sheet-table th,
.ai-tool-sheet-table td {
    padding: 7px 10px;
    text-align: left;
    border-bottom: 1px solid color-mix(in srgb, var(--text-primary) 8%, transparent);
    border-right: 1px solid color-mix(in srgb, var(--text-primary) 8%, transparent);
}

.ai-tool-sheet-table th {
    position: sticky;
    top: 0;
    z-index: 1;
    font-weight: 600;
    background: color-mix(in srgb, var(--text-primary) 6%, #fff);
}

.ai-tool-sheet-table tbody tr:hover td {
    background: color-mix(in srgb, var(--ui-accent, #0f766e) 6%, transparent);
}

.ai-tool-sheet-more {
    margin: 0;
    font-size: 12px;
    color: var(--text-secondary);
}

.ai-tool-sheet-code {
    /* pre-wrap 在这里是必须的：结果区是 pre-wrap 的，长 JSON 不换行会横向溢出 */
    margin: 0;
    padding: 12px;
    max-height: 260px;
    overflow: auto;
    border-radius: 12px;
    font-size: 12px;
    line-height: 1.6;
    white-space: pre-wrap;
    word-break: break-word;
    background: color-mix(in srgb, var(--text-primary) 5%, transparent);
    overscroll-behavior: contain;
}

[data-theme="dark"] .ai-tool-sheet-table th {
    background: color-mix(in srgb, #fff 10%, #1a1a1a);
}

@media (prefers-color-scheme: dark) {
    [data-theme="system"] .ai-tool-sheet-table th {
        background: color-mix(in srgb, #fff 10%, #1a1a1a);
    }
}

.ai-tool-dedupe-sum {
    margin: 0 0 14px;
    font-size: 13px;
    color: var(--text-secondary);
}

.ai-tool-dedupe-group + .ai-tool-dedupe-group {
    margin-top: 16px;
}

.ai-tool-dedupe-head {
    margin-bottom: 8px;
    font-size: 13px;
    font-weight: 600;
    color: var(--text-primary);
}

.ai-tool-dedupe-thumbs {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
}

.ai-tool-dedupe-item {
    margin: 0;
    width: 112px;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.ai-tool-dedupe-item img {
    width: 112px;
    height: 112px;
    object-fit: contain;
    border-radius: 10px;
    background: color-mix(in srgb, var(--text-primary) 5%, transparent);
    box-shadow: inset 0 0 0 1px var(--border-soft);
}

.ai-tool-dedupe-item.is-keep img {
    box-shadow: inset 0 0 0 2px var(--ui-accent, #0f766e);
}

.ai-tool-dedupe-item figcaption {
    font-size: 11px;
    line-height: 1.35;
    color: var(--text-tertiary);
}

.ai-tool-dedupe-item.is-keep figcaption {
    color: var(--ui-accent, #0f766e);
    font-weight: 550;
}

.ai-tool-dedupe-names {
    margin-top: 8px;
    font-size: 12px;
    line-height: 1.6;
    color: var(--text-tertiary);
    word-break: break-all;
}

/* ===== 违规词检测结果 ===== */
.ai-tool-compliance {
    white-space: normal;
    display: flex;
    flex-direction: column;
    gap: 12px;
    width: 100%;
}

.ai-tool-compliance-sum {
    margin: 0;
    font-size: 13px;
    font-weight: 600;
    color: var(--text-primary);
}

.ai-tool-compliance-clean {
    margin: 0;
    font-size: 14px;
    font-weight: 600;
    color: var(--ui-accent, #0f766e);
}

.ai-tool-compliance-stats {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.ai-tool-compliance-stats > span {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 84px;
    padding: 8px 12px;
    border-radius: 10px;
    background: color-mix(in srgb, var(--text-primary) 5%, transparent);
}

.ai-tool-compliance-stats b {
    font-size: 17px;
    font-weight: 650;
    line-height: 1.1;
    color: var(--text-primary);
}

.ai-tool-compliance-stats i {
    font-style: normal;
    font-size: 11px;
    color: var(--text-tertiary);
}

.ai-tool-compliance-group {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding-top: 12px;
    border-top: 1px solid var(--border-soft);
}

.ai-tool-compliance-cat {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 13px;
    font-weight: 600;
    color: var(--text-primary);
}

.ai-tool-compliance-cat em {
    font-style: normal;
    font-size: 11px;
    font-weight: 600;
    padding: 1px 7px;
    border-radius: 999px;
    color: #b4530a;
    background: color-mix(in srgb, #f59e0b 22%, transparent);
}

.ai-tool-compliance-tip {
    margin: 0;
    font-size: 12px;
    line-height: 1.55;
    color: var(--text-tertiary);
}

.ai-tool-compliance-item {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 4px 8px;
    padding: 7px 10px;
    border-radius: 9px;
    background: color-mix(in srgb, var(--text-primary) 4%, transparent);
}

.ai-tool-compliance-term {
    font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
    font-size: 12px;
    padding: 1px 7px;
    border-radius: 6px;
    color: #b02a1a;
    background: color-mix(in srgb, #ef4444 18%, transparent);
    white-space: nowrap;
}

.ai-tool-compliance-ctx {
    flex: 1 1 220px;
    min-width: 0;
    font-size: 12px;
    line-height: 1.5;
    color: var(--text-secondary);
    word-break: break-word;
}

.ai-tool-compliance-ctx b {
    font-weight: 650;
    color: #b02a1a;
}

.ai-tool-compliance-alt {
    font-size: 11px;
    color: var(--text-tertiary);
}

.ai-tool-compliance-markwrap {
    padding-top: 12px;
    border-top: 1px solid var(--border-soft);
}

.ai-tool-compliance-marktitle {
    margin-bottom: 6px;
    font-size: 12px;
    font-weight: 600;
    color: var(--text-secondary);
}

.ai-tool-compliance-marktext {
    margin: 0;
    font-size: 13px;
    line-height: 1.8;
    color: var(--text-secondary);
    word-break: break-word;
}

.ai-tool-compliance-marktext mark {
    padding: 0 2px;
    border-radius: 4px;
    color: #7f1d1d;
    background: color-mix(in srgb, #f97316 32%, transparent);
}

.ai-tool-compliance-note {
    margin: 0;
    padding-top: 10px;
    border-top: 1px solid var(--border-soft);
    font-size: 11px;
    line-height: 1.55;
    color: var(--text-quaternary, var(--text-tertiary));
}

/* ===== 产品宣传：详情图 + 种草文案合并结果 =====
   body 是 pre-wrap 的（纯文本结果要靠它保留换行），
   但这里的排版由标签负责，模板里的缩进换行会变成多余空行，
   所以在容器上改回 normal；文案的换行已经在 JS 里换成了 <br />。 */
.ai-tool-promo {
    white-space: normal;
    display: flex;
    flex-direction: column;
    gap: 10px;
    width: 100%;
}

.ai-tool-promo-title {
    font-size: 13px;
    font-weight: 600;
    color: var(--text-secondary);
}

.ai-tool-promo-gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: 8px;
}

.ai-tool-promo-item {
    position: relative;
    margin: 0;
    border-radius: 10px;
    overflow: hidden;
    background: var(--surface-soft, rgba(0, 0, 0, 0.04));
    border: 1px solid var(--border-soft);
}

.ai-tool-promo-item img {
    display: block;
    width: 100%;
    height: 100%;
    aspect-ratio: 1 / 1;
    object-fit: cover;
}

/* 下载链接压在图上：详情图是这次的主要产物，
   要让用户能单张拿走，而不是只能复制整段文案。 */
.ai-tool-promo-download {
    position: absolute;
    right: 6px;
    bottom: 6px;
    padding: 3px 9px;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.58);
    color: #fff;
    font-size: 11px;
    line-height: 1.5;
    text-decoration: none;
    opacity: 0;
    transition: opacity 0.15s ease;
}

.ai-tool-promo-item:hover .ai-tool-promo-download,
.ai-tool-promo-download:focus-visible {
    opacity: 1;
}

/* 触摸设备没有 hover，直接常显，否则这个入口等于不存在 */
@media (hover: none) {
    .ai-tool-promo-download {
        opacity: 1;
    }
}

.ai-tool-promo-copy {
    padding: 10px 12px;
    border-radius: 10px;
    background: var(--surface-soft, rgba(0, 0, 0, 0.04));
    font-size: 13.5px;
    line-height: 1.7;
    color: var(--text-primary);
    word-break: break-word;
}
/* AI 批量生成工作台 */

.ai-batch-modal-card {
    width: min(960px, calc(100vw - 32px));
}

.ai-batch-modal-body {
    align-items: stretch;
}

.ai-batch-options {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
}

.ai-batch-options .ai-tool-field[hidden] {
    display: none !important;
}

.ai-batch-prompts {
    min-height: 180px;
    resize: vertical;
    line-height: 1.55;
    font-family: inherit;
}

.ai-batch-count-hint {
    margin: 4px 0 0;
    font-size: 12px;
    color: var(--text-tertiary);
}

.ai-batch-progress {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-bottom: 12px;
}

.ai-batch-progress[hidden] {
    display: none !important;
}

.ai-batch-progress-bar-track {
    height: 6px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
    overflow: hidden;
}

.ai-batch-progress-bar {
    height: 100%;
    width: 0;
    border-radius: inherit;
    background: var(--text-primary);
    transition: width 0.2s ease;
}

.ai-batch-progress-text {
    margin: 0;
    font-size: 12px;
    color: var(--text-secondary);
}

.ai-batch-result-list {
    flex: 1 1 auto;
    min-height: 220px;
    max-height: min(56vh, 560px);
    overflow: auto;
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding-right: 2px;
}

.ai-batch-result-item {
    border: 1px solid var(--border-mid);
    border-radius: 12px;
    padding: 12px;
    background: color-mix(in srgb, var(--text-primary) 2%, transparent);
}

.ai-batch-result-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 6px;
}

.ai-batch-result-index {
    font-size: 12px;
    font-weight: 600;
    color: var(--text-secondary);
}

.ai-batch-result-status {
    font-size: 12px;
    color: var(--text-tertiary);
}

.ai-batch-result-item.is-running .ai-batch-result-status {
    color: var(--text-primary);
}

.ai-batch-result-item.is-done .ai-batch-result-status {
    color: #1f8f4e;
}

.ai-batch-result-item.is-error .ai-batch-result-status {
    color: #d14343;
}

.ai-batch-result-prompt {
    margin: 0 0 10px;
    font-size: 13px;
    line-height: 1.5;
    color: var(--text-primary);
    white-space: pre-wrap;
    word-break: break-word;
}

.ai-batch-result-media {
    display: block;
    width: 100%;
    max-height: 280px;
    object-fit: contain;
    border-radius: 10px;
    background: color-mix(in srgb, var(--text-primary) 4%, transparent);
}

.ai-batch-result-error {
    margin: 0;
    font-size: 13px;
    color: #d14343;
}

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

    .ai-batch-result-list {
        max-height: none;
    }
}
/* 创作增强：任务中心 / 风格包 / 对比 / 分镜 / 批量管理 */

.ai-studio-modal-card {
    width: min(980px, calc(100vw - 32px));
    max-height: min(90vh, 860px);
    display: flex;
    flex-direction: column;
}

.ai-studio-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    padding: 0 20px 12px;
    border-bottom: 1px solid var(--border-mid);
}

.ai-studio-tab {
    appearance: none;
    border: 1px solid transparent;
    background: transparent;
    color: var(--text-secondary);
    font: inherit;
    font-size: 12px;
    padding: 6px 10px;
    border-radius: 8px;
    cursor: pointer;
}

.ai-studio-tab:hover,
.ai-studio-tab:focus-visible {
    color: var(--text-primary);
    background: color-mix(in srgb, var(--text-primary) 6%, transparent);
}

.ai-studio-tab.is-active {
    color: var(--text-primary);
    border-color: var(--border-mid);
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
    font-weight: 600;
}

.ai-studio-modal-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
    padding: 16px 20px 20px;
}

.ai-studio-panel-body {
    display: flex;
    flex-direction: column;
    gap: 14px;
    min-height: 280px;
}

.ai-studio-empty,
.ai-studio-hint {
    margin: 0;
    font-size: 13px;
    line-height: 1.5;
    color: var(--text-secondary);
}

.ai-studio-form {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.ai-studio-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
}

.ai-studio-check {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 13px;
    color: var(--text-primary);
}

.ai-studio-task-list,
.ai-studio-pack-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.ai-studio-task-item,
.ai-studio-pack-item {
    border: 1px solid var(--border-mid);
    border-radius: 12px;
    padding: 12px;
    background: color-mix(in srgb, var(--text-primary) 2%, transparent);
}

.ai-studio-task-head,
.ai-studio-pack-item {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 10px;
}

.ai-studio-task-meta,
.ai-studio-pack-item p {
    margin: 6px 0 0;
    font-size: 12px;
    color: var(--text-tertiary);
}

.ai-studio-progress-track {
    margin-top: 10px;
    height: 6px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
    overflow: hidden;
}

.ai-studio-progress-bar {
    height: 100%;
    width: 0;
    border-radius: inherit;
    background: var(--text-primary);
}

.ai-studio-pack-actions {
    display: flex;
    gap: 6px;
    flex-shrink: 0;
}

.ai-studio-pack-actions button {
    appearance: none;
    border: 1px solid var(--border-mid);
    background: transparent;
    color: var(--text-secondary);
    font: inherit;
    font-size: 12px;
    padding: 4px 8px;
    border-radius: 8px;
    cursor: pointer;
}

.ai-studio-pack-item.is-active {
    border-color: color-mix(in srgb, var(--text-primary) 35%, var(--border-mid));
}

.ai-studio-compare-grid,
.ai-studio-template-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: 10px;
}

.ai-studio-compare-card,
.ai-studio-template-card {
    display: flex;
    flex-direction: column;
    gap: 6px;
    border: 1px solid var(--border-mid);
    border-radius: 12px;
    padding: 8px;
    background: color-mix(in srgb, var(--text-primary) 2%, transparent);
    text-align: left;
    cursor: pointer;
    color: inherit;
    font: inherit;
}

.ai-studio-compare-card img,
.ai-studio-compare-card video,
.ai-studio-compare-figure img,
.ai-studio-compare-figure video {
    width: 100%;
    aspect-ratio: 1;
    object-fit: cover;
    border-radius: 8px;
    background: color-mix(in srgb, var(--text-primary) 4%, transparent);
}

.ai-studio-compare-card span,
.ai-studio-template-card span,
.ai-studio-template-card p {
    font-size: 11px;
    color: var(--text-tertiary);
    line-height: 1.4;
}

.ai-studio-template-card strong {
    font-size: 13px;
    color: var(--text-primary);
}

.ai-studio-template-card p {
    margin: 0;
}

.ai-studio-compare-stage {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: 12px;
}

.ai-studio-compare-figure {
    margin: 0;
}

.ai-studio-compare-figure figcaption {
    margin-top: 6px;
    font-size: 12px;
    color: var(--text-secondary);
}

.ai-studio-storyboard-result {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.ai-studio-shot {
    border: 1px solid var(--border-mid);
    border-radius: 10px;
    padding: 10px 12px;
    background: color-mix(in srgb, var(--text-primary) 2%, transparent);
}

.ai-studio-shot p {
    margin: 6px 0 0;
    font-size: 13px;
    line-height: 1.5;
    color: var(--text-secondary);
    white-space: pre-wrap;
}

.ai-studio-task-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    margin-left: 4px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 600;
    line-height: 1;
    color: #fff;
    background: #d14343;
}

.ai-studio-task-badge[hidden] {
    display: none !important;
}

.ai-batch-extras-toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 4px;
}

.ai-batch-ref-images-preview {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.ai-batch-ref-images-preview[hidden],
.ai-batch-opt-img2video[hidden] {
    display: none !important;
}

.ai-batch-ref-thumb {
    width: 56px;
    height: 56px;
    object-fit: cover;
    border-radius: 8px;
    border: 1px solid var(--border-mid);
}

.ai-batch-result-actions {
    display: flex;
    gap: 8px;
    margin-top: 10px;
}

.ai-batch-send-canvas-btn {
    appearance: none;
    border: 1px solid var(--border-mid);
    background: transparent;
    color: var(--text-secondary);
    font: inherit;
    font-size: 12px;
    padding: 6px 10px;
    border-radius: 8px;
    cursor: pointer;
}

.ai-batch-send-canvas-btn:hover,
.ai-batch-send-canvas-btn:focus-visible {
    color: var(--text-primary);
    background: color-mix(in srgb, var(--text-primary) 6%, transparent);
}

/* ⚠️ 「生成记录」那套随生成记录画廊一起删了（素材库只留 AI 生成的云盘网格）：
   `.generations-batch-toolbar` / `.generations-batch-count` / `#panel-generations.is-batch-mode` /
   `.generation-card-batch-check`（批量选择）本轮一并清掉。 */

@media (max-width: 900px) {
    .ai-studio-tabs {
        padding-inline: 14px;
        flex-wrap: nowrap;
        overflow-x: auto;
    }

    .ai-studio-modal-body {
        padding-inline: 14px;
    }

    .ai-studio-compare-grid,
    .ai-studio-template-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}
/* —— 工具集首页 —— */
.tools-page {
    width: 100%;
    box-sizing: border-box;
}

/* 高度链条见下面「工具页的高度链条：宽窄共用」那一段（放在列表包裹规则之后，
   这样它压得住上面几条基础规则里的 align-items / min-height）。 */

html[data-tools-tabs-docked] .topbar {
    background: var(--topbar-bg);
    border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}

html[data-tools-tabs-docked][data-topbar-glass="true"] .topbar,
html[data-tools-tabs-docked][data-topbar-custom="true"] .topbar {
    background: var(--topbar-bg);
}

html[data-theme="dark"][data-tools-tabs-docked] .topbar {
    border-bottom-color: rgba(255, 255, 255, 0.08);
}

@media (prefers-color-scheme: dark) {
    html[data-theme="system"][data-tools-tabs-docked] .topbar {
        border-bottom-color: rgba(255, 255, 255, 0.08);
    }
}

/* 整页左右留白。原来这里是「1200px 上限 + margin: 0 auto」——
   宽屏下内容列被居中，左右各空出 88px，导轨和顶栏那条「最近使用」都缩在中间，
   和左侧栏之间挂着一大块空。改成固定 --tools-hub-gutter 的两侧留白、容器跟着撑满：
   左右对称，导轨按需求贴在离栏左缘 35px 的位置，卡片网格顺势变宽。 */
.tools-hub {
    width: calc(100% - 2 * var(--tools-hub-gutter));
    margin: 0 auto;
    padding: var(--tools-hub-pad-top) 0 0;
    box-sizing: border-box;
    /* 高度链条的中间一层：上面是搜索那一格（宽屏绝对定位浮着、窄屏在顶栏），
       下面是左右两栏。 */
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

/* #main-content 还自带 .content 的 40px 左右内边距，.tools-hub 的 --tools-hub-gutter
   是在那之上再叠的 —— 两层加起来实际是 75px，导轨左缘会落在 75，不是设计要的 35。
   工具集这一页的横向留白统一由 --tools-hub-gutter 说了算，
   所以把通用内边距让出去，免得同一个值要在两个地方各减一半。
   只让宽屏让：窄屏（≤900px）导轨是横排的，留白另有安排，不跟着动。 */
@media (min-width: 901px) {
    #main-content:has(#panel-tools.is-active) {
        padding-left: 0;
        padding-right: 0;
    }
}

/* ---- 宽屏：工具页的顶栏整条收起来 ----

   需求原话：「顶栏删除或者不显示」。搜索搬到内容区左上（见下面「宽屏：搜索钮落在
   内容区左上」那一段）之后，顶栏这一页就真的空了：用户 / 积分 / 通知都收在侧栏里
   （见 __syncSidebarToolsHubActive），留着只剩一条 64px 的纯色带子。

   --topbar-h 必须一起归零，否则下面两处会各错一处：
     · .content 的上内边距是 max(0, --content-first-top - --topbar-h)（见 06-content.css）。
       归零后拿到 --content-first-top 全额（110px）—— 顶栏没了，但下面那块内容的起点
       仍然落在原来那条线上，不会突然往上跳一截，也不会多出一段空白。
       （这里量的是内容区自己的第一行；侧栏首个 nav-item 现在在 70px 那条线上，两者早已不同源。）
     · --tools-hub-tabs-top 是在 :root 上算好的（继承下来时里面的 var() 已经代换完，
       在 .main 上改 --topbar-h 改不动它），所以这里必须就地重算一份：
       顶栏没了，吸顶偏移只剩「安全区 + 15px」，分类条直接贴住视口上沿。
       不重算的话它还按 64 + 15 走，条子会吊在半空中。

   断点取 721px 而不是 901px：720px 以下顶栏还在（那格要报当前分类），
   而 721px 起内容区已经是「导轨 + 内容」两栏，搜索钮也跟着落到内容区左上。 */
@media (min-width: 721px) {
    .main:has(#panel-tools.is-active) {
        --topbar-h: 0px;
        --tools-hub-tabs-top: calc(var(--safe-top, 0px) + var(--tools-hub-tabs-gap));
    }

    .main:has(#panel-tools.is-active) .topbar {
        display: none !important;
    }
}

html[data-tools-tabs-docked] {
    --topbar-search-expanded-w: 255px;
}

/* 顶栏那颗药丸：贴右端、绝对定位、盖在「最近使用」那格上面。
   这一套只属于顶栏 —— 药丸现在有两个落点（见下面 .tools-hub-search-slot 的说明），
   内容区那个是正常占位的流内元素，不该跟着绝对定位。 */
.topbar-tools-search-slot .tools-hub-search-box.search-box {
    position: absolute;
    right: 0;
    top: 0;
    left: auto;
    margin: 0;
    z-index: 40;
}

/* 药丸本体（两个落点共用）：收起时是一颗 32px 的圆，展开时才长出输入框。
   宽度上的差异各自覆盖 —— 顶栏那颗是「右端固定、往左撑到 255px」（见下），
   内容区那颗是「左端固定、往右撑满导轨宽」。 */
.tools-hub-search-slot .tools-hub-search-box.search-box {
    width: var(--search-box-height);
    min-width: var(--search-box-height);
    max-width: var(--search-box-height);
    height: var(--search-box-height);
    min-height: var(--search-box-height);
    padding: 0;
    gap: 0;
    justify-content: center;
    border-radius: 999px;
    border: 1px solid var(--border-soft);
    box-shadow: none;
    background: var(--panel-surface-bg);
    overflow: hidden;
    flex-shrink: 0;
    transition:
        width 0.2s ease,
        max-width 0.2s ease,
        padding 0.2s ease,
        gap 0.2s ease;
}

/* 这两个覆盖原本把底色钉成 #ffffff，把 .search-box 自己那套深色适配
   （[data-theme="dark"] .search-box 里的 --search-bar-bg: var(--panel-surface-bg)）
   按选择器优先级压掉了。改回同一个 token，深浅主题就都跟得上，
   下面那两段硬编码的深色块也就没有存在的必要了。 */
.tools-hub-search-slot .tools-hub-search-box.search-box:not(.is-expanded):not(.is-search-click-open) .search-icon-btn,
.tools-hub-search-slot .tools-hub-search-box.search-box:is(.is-expanded, .is-search-click-open) {
    background: var(--panel-surface-bg);
}

.tools-hub-search-slot .tools-hub-search-box.search-box:is(.is-expanded, .is-search-click-open) {
    width: var(--topbar-search-expand-w, var(--topbar-search-expanded-w));
    min-width: var(--search-box-height);
    max-width: min(
        var(--topbar-search-expand-w, var(--topbar-search-expanded-w)),
        calc(100vw - 48px)
    );
    height: var(--search-box-height);
    min-height: var(--search-box-height);
    padding: 0 4px 0 10px;
    gap: 4px;
    justify-content: flex-start;
}

/* 顶栏那颗是「右端钉住、往左撑进最近使用那条」 */
.topbar-tools-search-slot .tools-hub-search-box.search-box:is(.is-expanded, .is-search-click-open) {
    right: 0;
    left: auto;
}

/* Click-to-expand only — ignore topbar search hover/focus width rules */
.tools-hub-search-slot .tools-hub-search-box.search-box:not(.is-expanded):not(.is-search-click-open):hover,
.tools-hub-search-slot .tools-hub-search-box.search-box:not(.is-expanded):not(.is-search-click-open):focus-within {
    width: var(--search-box-height);
    min-width: var(--search-box-height);
    max-width: var(--search-box-height);
    height: var(--search-box-height);
    min-height: var(--search-box-height);
    padding: 0;
    gap: 0;
    justify-content: center;
}

.tools-hub-search-slot .tools-hub-search-box.search-box:not(.is-expanded):not(.is-search-click-open) .search-expand,
.tools-hub-search-slot .tools-hub-search-box.search-box:not(.is-expanded):not(.is-search-click-open) .search-submit-btn,
.tools-hub-search-slot .tools-hub-search-box.search-box:not(.is-expanded):not(.is-search-click-open) .search-clear-btn,
.tools-hub-search-slot .tools-hub-search-box.search-box:not(.is-expanded):not(.is-search-click-open):hover .search-expand,
.tools-hub-search-slot .tools-hub-search-box.search-box:not(.is-expanded):not(.is-search-click-open):hover .search-submit-btn,
.tools-hub-search-slot .tools-hub-search-box.search-box:not(.is-expanded):not(.is-search-click-open):hover .search-clear-btn,
.tools-hub-search-slot .tools-hub-search-box.search-box:not(.is-expanded):not(.is-search-click-open):focus-within .search-expand,
.tools-hub-search-slot .tools-hub-search-box.search-box:not(.is-expanded):not(.is-search-click-open):focus-within .search-submit-btn,
.tools-hub-search-slot .tools-hub-search-box.search-box:not(.is-expanded):not(.is-search-click-open):focus-within .search-clear-btn {
    display: none !important;
}

.tools-hub-search-slot .tools-hub-search-box.search-box:is(.is-expanded, .is-search-click-open) .search-expand,
.tools-hub-search-slot .tools-hub-search-box.search-box:is(.is-expanded, .is-search-click-open) .search-submit-btn {
    display: inline-flex;
}

.tools-hub-search-slot .tools-hub-search-box.search-box .search-clear-btn {
    display: none !important;
}

.tools-hub-search-slot .tools-hub-search-box.search-box:is(.is-expanded, .is-search-click-open) .search-clear-btn:not([hidden]) {
    display: inline-flex !important;
}

.tools-hub-search-slot .tools-hub-search-box.search-box:is(.is-expanded, .is-search-click-open) .search-expand {
    flex: 1;
    width: auto;
    opacity: 1;
    pointer-events: auto;
}

.tools-hub-search-slot .tools-hub-search-box.search-box:is(.is-expanded, .is-search-click-open) .search-expand input {
    opacity: 1;
    font-size: var(--filter-tabs-tab-font-size, 13px);
}

.tools-hub-search-slot .tools-hub-search-box.search-box:not(.is-expanded):not(.is-search-click-open) .search-icon-btn {
    width: 100%;
    height: 100%;
    border-radius: 999px;
}

.tools-hub-search-slot .tools-hub-search-box.search-box:is(.is-expanded, .is-search-click-open) .search-icon-btn {
    width: 16px;
    height: 16px;
}

.tools-hub-search-slot .tools-hub-search-box.search-box .search-icon-btn svg {
    width: 16px;
    height: 16px;
}

.tools-hub-search-slot .tools-hub-search-box.search-box:is(.is-expanded, .is-search-click-open) .search-clear-btn {
    width: calc(var(--search-box-height) - 4px);
    height: calc(var(--search-box-height) - 4px);
}

.tools-hub-search-slot .tools-hub-search-box.search-box:is(.is-expanded, .is-search-click-open) .search-submit-btn {
    height: calc(var(--search-box-height) - 4px);
    min-height: calc(var(--search-box-height) - 4px);
    padding: 0 8px;
    font-size: var(--filter-tabs-tab-font-size, 13px);
}

/* 窄屏：工具集搜索仍然「在药丸里原地展开」，输入框跟着药丸一起出现。

   25-topbar-responsive.css 在 ≤700px 把 .search-expand 拎出药丸、挂到顶栏下缘
   变成一个绝对定位的浮层（opacity 0 → 展开时才淡入）。那套是给模板搜索用的：
   那边药丸固定 36px 宽，不靠下拉就装不下输入框。
   工具集这颗药丸本来就会向左撑到 --topbar-search-expanded-w（255px）把输入框装进去，
   浮层再挂到它下面，就变成「药丸撑开了、里面空的，输入框是一条没有底板、
   没有描边的透明细线掉在顶栏外」—— 看到的就是「点了展开怎么没有输入框」。
   所以这里把 position / width / opacity 按回药丸内部，和 >700px 时的行为对齐。

   药丸展开后会占掉顶栏右边那 255px，正好压在「最近使用」上 ——
   那条由 CSS 在 ≤720px 里淡出让位，见文件末尾的 720px 断点。 */
@media (max-width: 700px) {
    .tools-hub-search-slot .tools-hub-search-box.search-box:is(.is-expanded, .is-search-click-open) .search-expand {
        position: static;
        right: auto;
        top: auto;
        width: auto;
        max-width: none;
        opacity: 1;
        pointer-events: auto;
        transform: none;
    }

    /* 浮层那套里的淡入要一并撤掉，否则输入框还是透明的 */
    .tools-hub-search-slot .tools-hub-search-box.search-box:is(.is-expanded, .is-search-click-open) .search-expand input {
        width: 100%;
        min-width: 0;
        opacity: 1;
        /* 字号不在这里定：手机端全局把 input 钉在 16px 上（防 iOS 聚焦自动放大）*/
    }
}

/* ---- 宽屏：搜索钮落在内容区左上，分类导轨正上方 ----

   需求原话：「#topbar-tools-search-host 展开态显示在 nav.tools-hub-section-tabs 上方」。
   顶栏在宽屏整条收起来了（见上面「宽屏：工具页的顶栏整条收起来」），搜索没法再留在
   顶栏右端，于是搬进内容区这一格（#tools-hub-search-host，见 index.html）。

   它是 .tools-hub 的第一个子元素，所以宽度 / 居中跟着 .tools-hub 走，不用再抄一遍
   「calc(100% - 2 * --tools-hub-gutter)」；左缘靠 padding-left 和导轨对齐 ——
   导轨的左白就是这个 --tools-hub-rail-pad-l，两颗元素才会落在同一条竖线上。
   flex 布局不写：这层是普通块盒，不设 display 才不会把 [hidden] 压掉。

   下边距就是「搜索那格 → 导轨」那道缝（--tools-hub-rail-gap）：左栏导轨要往下让出
   这一格、右栏内容要顶回这一格，三处用的是同一个值。 */
#tools-hub-search-host {
    padding: 0 0 var(--tools-hub-rail-gap) var(--tools-hub-rail-pad-l);
}

/* 电脑端这一格：**始终展开**（需求：「始终展开」）。

   它落在左栏导轨正上方，是这一栏唯一的搜索入口 —— 所以这一格没有收起态，
   一整格就是一片铺满导轨宽的输入框。尺寸对照下面那排分类项：
     · 宽 = --tools-hub-rail-w（200px）＝分类项那一条的宽
     · 高 = --tools-hub-search-h（40px）
     · 圆角 = --tools-hub-tab-radius，和分类项的圆角同源，两颗上下挨着差 2px 看得出来
     · 底色 = --tools-hub-tab-active-bg（= --bg-elev-1），和分类项选中态同源
       （需求：「展开后的搜索框背景色要和 .tools-hub-section-tab.is-active 的选中背景色一致」）
       —— 不是「碰巧相同的另一个值」，是同一个 token，改一处两处一起动。
   窄屏那颗不走这里：它住在顶栏，外形和开合仍由共用的 .tools-hub-search-slot 那套管。

   「开 / 合」那套类（is-expanded / is-search-click-open）由 18-tools-hub.js 常驻挂着 ——
   共用规则里「收起态收掉输入框 / 清除 / 提交」挂在 :not(...) 上，类不在就只剩空壳。
   这里不写那两组选择器：类反正是常驻的，写成基础规则更直白，
   也省得再维护一份「收起时长什么样」的死声明。 */
#tools-hub-search-host .tools-hub-search-box.search-box {
    align-items: center;
    width: var(--tools-hub-rail-w);
    /* min/max 都得让开：共用规则把三者一起钉在 --search-box-height（32px）上，
       只改 width 的话会被那条 max-width: 32px 当场压回顶栏那颗的大小。
       max-width 给 100% 而不是 none：宿主就是导轨那一列、内容盒正好 200px，
       留一条相对长度，以后导轨改宽时这一格不会溢出。 */
    min-width: 0;
    max-width: 100%;
    height: var(--tools-hub-search-h);
    min-height: var(--tools-hub-search-h);
    padding: 0 12px;
    gap: 8px;
    justify-content: flex-start;
    border: 0;
    border-radius: var(--tools-hub-tab-radius);
    background: var(--tools-hub-tab-active-bg);
    box-shadow: none;
    box-sizing: border-box;
}

/* 注意：这一格没有「悬停 / 聚焦改尺寸」那条覆盖的必要了 ——
   共用规则里那句「悬停回到 --search-box-height」是挂在
   :not(.is-expanded):not(.is-search-click-open) 上的，常驻展开的这颗永远匹配不到。 */

#tools-hub-search-host .tools-hub-search-box.search-box .search-expand {
    position: relative;
    flex: 1 1 auto;
    min-width: 0;
    order: 1;
    display: flex;
    align-items: center;
    overflow: hidden;
}

#tools-hub-search-host .tools-hub-search-box.search-box .search-expand input {
    width: 100%;
    min-width: 0;
    border: none;
    outline: none;
    background: transparent;
    color: var(--text-primary);
    font-size: 13px;
    caret-color: var(--text-primary);
}

#tools-hub-search-host .tools-hub-search-box.search-box .search-expand input::placeholder {
    color: var(--text-tertiary, rgba(255, 255, 255, 0.5));
}

/* 这颗输入框自己带一颗「清除」钮（.search-clear-btn，见 18-tools-hub.js 的
   renderToolsHubToolbar），而 type="search" 的输入框在 Chrome / Safari 下
   还会在框内右端再画一颗原生的叉 —— 一有文字就是两颗并排，看着像坏了。

   顶栏那套搜索框早就有一条同样的覆盖（见 05-language-dropdown.css 的
   `.topbar .search-box .search-expand input::-webkit-search-cancel-button`），
   但电脑端这颗已经搬到内容区（#tools-hub-search-host），不在 .topbar 里了，
   那条带顶栏作用域的规则盖不到它。

   这里按类名写、不绑宿主：宽窄两处宿主（内容区那颗、顶栏那颗）用的是同一个
   .tools-hub-search-box，谁在上面都不用再补一条。 */
.tools-hub-search-box .search-expand input::-webkit-search-cancel-button {
    -webkit-appearance: none;
    appearance: none;
    display: none;
}

#tools-hub-search-host .tools-hub-search-box.search-box .search-icon-btn {
    /* 排在输入框和清除钮之后 —— 放大镜落在这一格的右端（需求：「靠右显示」）。
       始终展开之后它不再是「开合开关」，而是「把光标放进输入框」那颗钮，
       外加撑住右端、给输入框收出一个明确的边界。 */
    order: 3;
    width: 16px;
    height: 16px;
    flex-shrink: 0;
    color: var(--text-tertiary, rgba(255, 255, 255, 0.5));
}

#tools-hub-search-host .tools-hub-search-box.search-box .search-icon-btn svg {
    width: 15px;
    height: 15px;
    /* 线宽跟左侧主导航那排图标同源（需求：「图标线条粗细要和 nav-item 一致」）。
       token 在 03-app-sidebar.css 的 :root 上，所以这一页够得着；
       svg 上的 stroke-width="1.8"（见 buildAiToolIconHtml）是 presentation attribute，
       CSS 一律压过它，改线宽不用回 JS。 */
    stroke-width: var(--sidebar-icon-stroke, 1.4);
}

#tools-hub-search-host .tools-hub-search-box.search-box .search-clear-btn {
    order: 2;
    width: 18px;
    height: 18px;
    flex-shrink: 0;
    background: transparent;
    color: var(--text-tertiary);
}

#tools-hub-search-host .tools-hub-search-box.search-box .search-submit-btn {
    display: none !important;
}

/* 窄屏这一格整个不出现 —— 靠的是 JS 那一刀（syncToolsHubSearchHost 只让当期宿主
   解除 hidden，窄屏选中的是顶栏那颗），这里不再写一条 @media：
   ≤720px 那条断点在文件末尾另有一整块，多一条同名查询会把 mediaBlock 这类
   「取第一处」的读法带偏。 */

/* ---- 窄屏顶栏那格：当前分类 ----
   分类条在手机上收成了一排纯图标（名字没地方写），于是分类名搬进顶栏。
   里面那两颗元素用的还是内容区同一套类名（.tools-hub-section-title /
   .tools-hub-section-count），外层另起一个类 —— 不跟 .tools-hub-section-head
   的吸顶那套搅在一起（窄屏下内容区那条是 display: none，同一个 class 挂到顶栏
   还得再写一遍覆盖，白搭）。

   这一格现在只在窄屏有内容（见 18-tools-hub.js 的 syncToolsHubTopbarSlot）：
   电脑端顶栏整个腾空 ——「最近使用」已经搬到左栏导轨下方，见 .tools-hub-rail-recent。 */
.topbar-tools-recent-slot .tools-hub-docked-head {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    /* 窄屏那条是绝对定位 + 左右各 60px，宽度由 CSS 钉死；这里铺满那一格 */
    width: 100%;
    min-width: 0;
    margin: 0;
}

/* 分类名长过剩余宽度时截断 —— 顶栏那一格左右都被 logo / 搜索药丸占着，
   溢出会把搜索框顶开。数量胶囊 flex: 0 0 auto，不会被一起挤扁。 */
.topbar-tools-recent-slot .tools-hub-docked-head .tools-hub-section-title {
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* 右侧功能区每一节的标题条，滚动时吸在列表顶端 —— 常驻告诉用户「现在看的是哪个分类、
   里面有多少个工具」。

   吸顶线是 0 而不是某个「距视口顶多少」的偏移：它的滚动容器就是列表那一层
   （.tools-hub-list-scroll），0 正好等于列表在屏幕上的上缘。
   以前那圈往上补同色的 box-shadow 也一起撤了 —— 那是为了盖住「顶栏下缘 → 标题条上缘」
   那 15px 的缝，而现在标题条贴的就是容器的上缘，缝不存在了（补出来的色带还会被
   滚动容器裁掉一半）。

   两个关键点：
   1. 节标题和卡片之间的 12px 用 padding-bottom 而不是 margin-bottom。
      margin 在盒子外面，背景盖不到 —— 卡片会从这条缝里透出来。
   2. 「全部工具」视图下六节同时存在，六条标题条吸在同一条线上。
      同为 z-index: 10 时靠 DOM 顺序决胜负，后一节压住前一节，正好是想要的效果；
      前一节被它的 section 下边界顶出去，不会露出来。

   标题 + 数量这一对居中：卡片网格是整栏铺满的，标题条却贴死在左边，视觉重心整条歪掉
   （左边 344 挤着「图像处理 12」，右边空着 700 多像素）。justify-content 而不是给
   标题加 margin-left：这样两个数的间距（gap）保持 8px，居中也是按「标题 + 数量」整体算的。 */
.tools-hub-section-head {
    position: sticky;
    top: 0;
    z-index: 10;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    margin: 0;
    padding: 2px 0 12px;
    background: var(--bg-base);
}

.tools-hub-section-title {
    margin: 0;
    font-size: 18px;
    font-weight: 650;
    color: var(--text-primary);
    /* 写死行高（不用 normal）：标题条的高度因此是个定值 41px，
       上翻页箭头靠它算「躲开标题条」的偏移（见 --tools-hub-section-head-h）。 */
    line-height: 27px;
}

.tools-hub-section-count {
    flex: 0 0 auto;
    min-width: 22px;
    padding: 2px 7px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--text-primary) 6%, transparent);
    color: var(--text-tertiary);
    font-size: 12px;
    font-weight: 600;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

.tools-hub-section-desc {
    margin: 4px 0 0;
    font-size: 13px;
    color: var(--text-tertiary);
}

.tools-hub-featured,
.tools-hub-section,
.tools-hub-recent,
.tools-hub-chains {
    margin-bottom: 28px;
    /* 分类导轨改成竖排后就不再压在内容上方了，滚进来的标题只要不钻进顶栏
       （即不越过吸顶偏移那条线）就够了，不用再额外加上导轨自身的高度。 */
    scroll-margin-top: calc(var(--tools-hub-tabs-top) + 14px);
}

.tools-hub-chip-row {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

/* 「最近使用」那颗折叠按钮（需求：「改成可折叠，默认收起，下拉按钮样式要和
   [导轨里的「全部工具」] 一致，但是不要显示数标」）。

   声明和 .tools-hub-section-tab 逐条相同 —— 但写成两条独立规则，而不是并进同一个
   选择器组：测试里的 ruleBody() 按「`选择器 {` 第一处匹配」读规则体，
   把基础那条改成 `A,\nB {` 之后 `ruleBody(css, ".tools-hub-section-tab")` 就一条都读不到，
   一批拿它当基准的断言会连带失效（导轨字号、字重、高度、选中态都在那些断言里）。
   两份会不会漂？有测试逐条比对着（见 tests/tools-hub-recent-collapse.test.js）。

   图标块和名称块不另立门户：直接复用 .tools-hub-section-tab-icon / -label ——
   需求要的「一致」因此是结构上的一致，而不是抄一份像素。 */
.tools-hub-recent-toggle {
    display: flex;
    align-items: center;
    gap: 9px;
    width: 100%;
    height: var(--tools-hub-tabs-h);
    padding: 0 10px;
    border: 0;
    border-radius: var(--tools-hub-tab-radius);
    background: transparent;
    color: var(--text-secondary);
    font: inherit;
    font-size: 14px;
    font-weight: 400;
    text-align: left;
    cursor: pointer;
    box-sizing: border-box;
    transition: background 0.15s ease, color 0.15s ease;
}

/* 折叠箭头。放在名称后面（名称那条 flex: 1 1 auto 把剩余宽度全吃掉，
   箭头自然被顶到右端 —— 和导轨项里数量胶囊的位置一致）。
   stroke-width 跟导轨项里的图标同源，箭头细一档会显得是另一个控件。 */
.tools-hub-recent-caret {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 16px;
    height: 16px;
    color: inherit;
}

.tools-hub-recent-caret svg {
    width: 16px;
    height: 16px;
    stroke-width: var(--sidebar-icon-stroke, 1.4);
    transition: transform 0.18s ease;
}

/* 展开时翻过来（箭头本身是朝下的 chevron，复用列表翻页箭头那一份）。
   挂在块上而不是按钮上：按钮只负责点按，状态永远是整块的。 */
.tools-hub-recent:not(.is-collapsed) .tools-hub-recent-caret svg {
    transform: rotate(180deg);
}

/* 悬停 / 键盘焦点：和导轨项逐条同值（那一对规则在上面的行式导航那一段里，
   这里同样写成独立规则，理由见本段开头）。 */
.tools-hub-recent-toggle:hover {
    background: color-mix(in srgb, var(--text-primary) 5%, transparent);
    color: var(--text-primary);
}

.tools-hub-recent-toggle:focus-visible {
    outline: 2px solid color-mix(in srgb, var(--ui-accent, #7c5cff) 45%, transparent);
    outline-offset: -2px;
    background: color-mix(in srgb, var(--text-primary) 5%, transparent);
    color: var(--text-primary);
}

/* 收起态：只留那颗按钮，胶囊行整行收掉（不是高度归零）。
   胶囊行是 flex-wrap 的，宽度一变行数就变，算高度做过渡迟早对不上；
   这里是「默认收起 / 点开」的一次性动作，不差这一下过渡。 */
.tools-hub-recent.is-collapsed .tools-hub-chip-row {
    display: none;
}

/* 「最近使用」胶囊：去边框、去底色，平时只是文字；悬停才浮出一层浅灰。
   原来是一颗有描边、有浅底的药丸 —— 左栏导轨下方一排药丸和上面那条分类行
   （行式导航，靠底色分隔）不是同一套语言，看着比分类还重。
   悬停不写死灰色，仍走 --text-primary 混合：深色主题下自动变成一层淡白。 */
.tools-hub-chip {
    height: 32px;
    padding: 0 12px;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--text-primary);
    font-size: 13px;
    cursor: pointer;
    transition: background 0.15s ease;
}

.tools-hub-chip:hover,
.tools-hub-chip:focus-visible {
    background: color-mix(in srgb, var(--text-primary) 6%, transparent);
}

.tools-hub-chain-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 12px;
}

.tools-hub-chain-card {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
    padding: 16px;
    text-align: left;
    border-radius: 14px;
    border: 1px solid var(--border-soft);
    background: color-mix(in srgb, var(--ui-accent, #0f766e) 6%, var(--bg-base));
    color: inherit;
    cursor: pointer;
    transition: transform 0.15s ease, border-color 0.15s ease;
}

.tools-hub-chain-card:hover {
    transform: translateY(-1px);
    border-color: color-mix(in srgb, var(--ui-accent, #0f766e) 35%, var(--border-soft));
}

.tools-hub-chain-title {
    font-size: 15px;
    font-weight: 650;
    color: var(--text-primary);
}

.tools-hub-chain-desc {
    font-size: 12px;
    line-height: 1.45;
    color: var(--text-secondary);
}

.tools-hub-chain-steps {
    margin-top: 4px;
    font-size: 11px;
    color: var(--text-tertiary);
}

.tools-hub-grid {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 12px;
}

.tools-hub-grid--featured {
    grid-template-columns: repeat(4, minmax(0, 1fr));
}

.tools-hub-card {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
    /* 加了 34px 的图标块之后，原来的 112px 已经兜不住内容高度；
       同一个 grid 行里的卡片会被拉到一样高，所以这里只是给最短的那张一个下限。 */
    min-height: 150px;
    padding: 16px;
    text-align: left;
    border: 1px solid var(--border-soft);
    border-radius: 14px;
    background: var(--panel-surface-bg);
    color: inherit;
    cursor: pointer;
    transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
    box-sizing: border-box;
}

/* 工具图标块：一块裸图标 + 中性前景色，照左侧主导航那颗按钮来
   （需求：「背景色以及图标颜色要和侧边栏的按钮一致」）。

   原先这里是「11% 分类色淡底 + 10px 圆角 + 同色图标」：61 张卡片排下来是一面
   彩色方片墙，图标又各自吃自己的分类色；而旁边那列主导航从头到尾都是裸图标 +
   中性前景色（.app-sidebar-nav .nav-item，未选中 --text-secondary），
   两块并排时最扎眼的成了「谁的颜色更亮」。现在底色整个撤掉 ——
   导轨图标上一版走的就是这条路（见 .tools-hub-section-tab-icon 那条的说明）。

   颜色取的是主导航按钮的那一档 token（那边 .nav-item 的 color 就是它，图标
   currentColor 继承下来）：基础态 --text-secondary；悬停 / 聚焦时主导航那边会把图标
   转成纯黑 #000000（深色 #ffffff），这里照同一对值来（见下面那条的说明）。
   和主导航一样，底色 / 前景色只负责说「指针在这里」，卡片本身的语言还是
   描边 + 阴影（见下面 .tools-hub-card:hover 那条）。

   34px 的盒子留着：它是卡片里图标那一行的行高，撤掉底色之后再撤尺寸，
   卡片的纵向节奏会跟着塌（标题会和图标贴到一起）。圆角跟着底色一起撤 ——
   没有底的方盒子，10px 谁也看不见。 */
.tools-hub-card-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    margin-bottom: 2px;
    background: transparent;
    color: var(--text-secondary);
}

.tools-hub-card-icon svg {
    width: 19px;
    height: 19px;
}

/* 悬停 / 聚焦：图标转纯黑，和主导航那颗按钮的悬停同一档。

   这两个字面值（#000000 / #ffffff）只能照抄 —— 主导航那半写的就是它们
   （见 03-app-sidebar.css 的 `.app-sidebar-nav .nav-item:hover:not(.active) svg.nav-item-icon`
   与它的 [data-theme="dark"] / system 两段），不是 token，所以这边没有可复用的变量。
   不用 --text-primary 顶替：那个 token 是 #1a1523，和 #000000 不是一回事 ——
   两处并排时差的那一点正好落在「一眼看得出不是同一个黑」的量级上。 */
.tools-hub-card:hover .tools-hub-card-icon,
.tools-hub-card:focus-visible .tools-hub-card-icon {
    color: #000000;
}

[data-theme="dark"] .tools-hub-card:hover .tools-hub-card-icon,
[data-theme="dark"] .tools-hub-card:focus-visible .tools-hub-card-icon {
    color: #ffffff;
}

@media (prefers-color-scheme: dark) {
    [data-theme="system"] .tools-hub-card:hover .tools-hub-card-icon,
    [data-theme="system"] .tools-hub-card:focus-visible .tools-hub-card-icon {
        color: #ffffff;
    }
}

.tools-hub-card:hover,
.tools-hub-card:focus-visible {
    background: var(--panel-surface-bg);
    border-color: color-mix(in srgb, var(--text-primary) 18%, var(--border-soft));
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.06);
    outline: none;
}
/* 这里原本还有一条 translateY(-1px)（上浮 1px 的按压感），已去掉。
   原因：分类标题条是 sticky + 不透明底，它那块底一直铺到第一行卡片的上边缘
   （padding-bottom 撑出来的 12px 就是给吸顶时盖住滚动内容用的）。
   卡片一旦上浮 1px，它的上边框就正好钻进标题条底板的覆盖范围里，
   因为标题条 z-index: 10 而卡片是 static —— 上边框会被整条吃掉，
   悬停时反而比不悬停少一道线。要保留上浮就得给卡片加 z-index，
   但那样卡片会反过来压住吸顶标题，更糟。所以悬停反馈只用
   「描边加深 + 阴影 + 图标底色」，几何完全不动的做法最稳。 */

/* 卡片底色走 --panel-surface-bg（浅色 #ffffff / 深色 #1e1e1e），描边走 --border-soft，
   标题正文本来就用 --text-primary / --text-secondary —— 换成 token 之后
   这里原来那四段 [data-theme="dark"] / [data-theme="system"] 硬编码
   （background #ffffff + color #111 + rgba(0,0,0,0.55)）就都删掉了。
   那几段是把卡片钉死在白底黑字上的，深色主题下整片亮白卡片跟页面底色打架，
   也正是「按钮背景色没有适配主题色」的来源。 */

/* 徽章用变量取色，深色主题下换成亮一档的青绿。
   卡片变深之后 #0f766e 在 #1e1e1e 上只有约 2.1:1 的对比度，基本读不出来。 */
.tools-hub-card-badge--beta {
    --tools-hub-beta-hue: #0f766e;
    color: var(--tools-hub-beta-hue);
    background: color-mix(in srgb, var(--tools-hub-beta-hue) 14%, transparent);
}

[data-theme="dark"] .tools-hub-card-badge--beta {
    --tools-hub-beta-hue: #5eead4;
}

@media (prefers-color-scheme: dark) {
    [data-theme="system"] .tools-hub-card-badge--beta {
        --tools-hub-beta-hue: #5eead4;
    }
}

.tools-hub-card.is-drop-target {
    border-color: color-mix(in srgb, var(--ui-accent, #0f766e) 55%, var(--border-soft));
    background: color-mix(in srgb, var(--ui-accent, #0f766e) 10%, var(--bg-base));
}

.tools-hub-card.is-disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

.tools-hub-card-title {
    font-size: 15px;
    font-weight: 650;
    color: var(--text-primary);
}

.tools-hub-card-desc {
    font-size: 12px;
    line-height: 1.45;
    color: var(--text-secondary);
}

.tools-hub-card-meta {
    margin-top: auto;
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

.tools-hub-card-meta-item,
.tools-hub-card-badge {
    display: inline-flex;
    align-items: center;
    height: 22px;
    padding: 0 8px;
    border-radius: 999px;
    font-size: 11px;
    color: var(--text-secondary);
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
}

/* 卡片底部的「要求 / 花费」（需上传、约 N 积分、结果类型）平时收起来，鼠标悬停才淡入。
   状态徽章不在其列：本地 · 免费 / Beta / 即将上线 是「这工具是什么来路」的静态信息，
   本地那 20 张卡全靠「本地 · 免费」说明「不上传、不花钱」，一起收掉这一整类就没标识了。
   所以只收 .tools-hub-card-meta-item，徽章常显。

   收起用 opacity 而不是 display / visibility，两个原因：
   1) 必须仍然占位。不占位的话这一行卡片会矮一截（实测 165 → 150，网格 489 → 474）：
      卡片内容（图标 34 + 标题 21 + 描述 35 = 90，加 32 的内边距）本来就够不着
      min-height: 150px 那道下限，元信息占的 22px + 6px 间距才是把卡片顶到 165 的原因，
      去掉后被下限兜住、只掉 15px。掉多少不重要，「一悬停整行卡片长高、下面内容被顶下去」
      这件事本身才是问题 —— 和上面「卡片悬停不改几何」那段是同一类坑。
   2) opacity: 0 的元素仍留在无障碍树里，读屏照样念得出「需上传 / 约 4 积分」；
      换成 visibility: hidden 会把这条信息从读屏里一并抹掉。

   转场只碰 opacity / transform，不涉及任何影响布局的属性 —— 卡片盒子纹丝不动，
   动的只是这排 pills 自己。 */
.tools-hub-card-meta-item {
    opacity: 0;
    transform: translateY(3px);
    transition: opacity 0.18s ease, transform 0.18s ease;
}

.tools-hub-card:hover .tools-hub-card-meta-item,
.tools-hub-card:focus-visible .tools-hub-card-meta-item,
.tools-hub-card.is-drop-target .tools-hub-card-meta-item {
    opacity: 1;
    transform: none;
}

/* 触屏没有悬停，收起来就等于永远看不到；这类设备上直接常显。
   （:hover 在触屏上会「粘」在最后一次点过的元素上，靠它显示更不可靠。） */
@media (hover: none) {
    .tools-hub-card-meta-item {
        opacity: 1;
        transform: none;
        transition: none;
    }
}

@media (prefers-reduced-motion: reduce) {
    .tools-hub-card-meta-item {
        transition: none;
    }
}
@media (prefers-reduced-motion: reduce) {
    /* 折叠箭头那下翻转：动效开关关掉时直接到位（transition + 旋转一起去掉，
       只留 transition: none 的话它会「瞬间慢慢转」—— 还是有一段动画时间）。 */
    .tools-hub-recent-caret svg,
    .tools-hub-recent:not(.is-collapsed) .tools-hub-recent-caret svg {
        transition: none;
        transform: none;
    }
}

.tools-hub-card-badge--soon {
    color: var(--text-tertiary);
}

.tools-hub-empty {
    margin: 0;
    padding: 24px 0;
    color: var(--text-tertiary);
    font-size: 14px;
}

/* Tool shell size tiers */
.ai-tool-modal-card.ai-tool-shell--light {
    width: min(1120px, calc(100vw - 80px));
}

.ai-tool-modal-card.ai-tool-shell--mid {
    width: min(1120px, calc(100vw - 80px));
    height: min(720px, calc(100vh - 80px));
}

.ai-tool-modal-card.ai-tool-shell--full {
    width: min(1200px, calc(100vw - 64px));
    height: min(820px, calc(100vh - 64px));
}

@media (max-width: 1100px) {
    .tools-hub-grid,
    .tools-hub-grid--featured,
    .tools-hub-chain-grid {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }
}

@media (max-width: 900px) {
    .tools-hub {
        /* 窄屏 35px 太占地方了，收成 16px。
           这里改的是同一个变量，所以 .tools-hub 的宽度公式不用重复写一遍；
           顶栏「最近使用」也还是靠 JS 量出来的位置对齐，自动跟着走。 */
        --tools-hub-gutter: 16px;
        padding: 16px 0 48px;
    }

    .tools-hub-grid,
    .tools-hub-grid--featured,
    .tools-hub-chain-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

/* 手机端也要一行 2 个（原来是 1 个）。
   一列时 61 个工具要滑十几屏，找东西全靠滚；两列则要解决「每张卡只剩 ~150px」，
   所以这里不是单纯改列数，而是连内边距、图标块、描述行数一起收一档。
   断点取 560px：再窄下去（360px 级别的机器）两列仍有 ~150px，读得下去；
   真要更窄，宁可挤也不退回一列。 */
@media (max-width: 560px) {
    .tools-hub-grid,
    .tools-hub-grid--featured,
    .tools-hub-chain-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 8px;
    }

    .tools-hub-card {
        /* 两边各收 4px：卡片宽度掉到 150px 上下时，16px 的内边距会把标题挤成两行 */
        padding: 12px;
        gap: 5px;
        /* 下限跟着内边距一起降，否则内容撑不满、卡片下面空一截 */
        min-height: 132px;
    }

    .tools-hub-card-icon {
        width: 30px;
        height: 30px;
    }

    .tools-hub-card-icon svg {
        width: 17px;
        height: 17px;
    }

    .tools-hub-card-title {
        font-size: 14px;
    }

    /* 描述两行封顶。不封顶时一张长描述的卡会把同 grid 行里另外那张一起拉高，
       行高再由最高的那张决定 —— 整页会被零星几张卡拖长。 */
    .tools-hub-card-desc {
        display: -webkit-box;
        -webkit-box-orient: vertical;
        -webkit-line-clamp: 2;
        line-clamp: 2;
        overflow: hidden;
        font-size: 11.5px;
        line-height: 1.4;
    }

    .tools-hub-card-meta {
        gap: 4px;
    }

    .tools-hub-card-meta-item,
    .tools-hub-card-badge {
        height: 20px;
        padding: 0 6px;
        font-size: 10.5px;
    }
}

/* ========== 工具集：分类导轨（竖排，贴在内容左边） ========== */

/* 分类导轨吸顶：始终停在「#main-content 顶边下方 15px」。
   顶栏是 sticky 的，内容区顶边一直贴着它下缘，所以把偏移写成
   「顶栏真实高度 + 15px」就能在整个滚动过程中保持不变。
   顶栏真实高度 = --topbar-h + 刘海安全区（见 .topbar 的 height）。 */
:root {
    /* 整页左右留白，.tools-hub 用它算宽度（见上面 .tools-hub 的说明）。
       35px 是需求给的值：导轨离开侧栏后，这一段的左留白就是 35px。
       注意这是「离内容区左缘」的值 —— #main-content 自带的那 40px 通用内边距
       已经在上面被让出去了（见「工具集这一页的横向留白统一由 --tools-hub-gutter 说了算」），
       否则两层叠起来会变成 75px。
       顶栏那条「最近使用」靠 JS 量 .tools-hub 左缘来对齐，会自动跟上同一个值。 */
    --tools-hub-gutter: 35px;
    --tools-hub-tabs-gap: 15px;
    /* .tools-hub 的上内边距。搜索那一格在宽屏是绝对定位浮上来的（见「宽屏：搜索那一格」
       那一段），它自己也要吃同一个值才落在同一条线上；抽成变量是给这两处共用。 */
    --tools-hub-pad-top: 20px;
        /* 分类导轨按钮与侧栏导航保持同高：统一 40px。
             窄屏用作圆钮的直径，七个能排在一行；宽屏与侧栏主导航展开态同高。 */
    --tools-hub-tabs-h: 40px;
    /* 右侧功能区那条吸顶标题条（.tools-hub-section-head）的高度：
       2px 上内边距 + 27px 标题行高 + 12px 下内边距 = 41px。

       为什么要把它固成一个值：上翻页箭头是水平居中的，标题条里的「分类名 + 数量」
       也是水平居中的，两个都贴顶就必然叠在一起 —— 箭头因此要落到这条带子的下沿之外
       （见 .tools-hub-list-arrow.is-up 的 top）。要算这个偏移就得知道带子有多高，
       所以 .tools-hub-section-title 的 line-height 写死成 27px，不再是跟着字体走的
       「normal」—— 后者在不同中文字体上会差几像素，箭头就会忽高忽低。 */
    --tools-hub-section-head-h: 41px;
    /* 导轨项的圆角，同时也是内容区那颗搜索框的圆角
       （需求：「搜索框圆角大小参考导轨选中项」）。抽成变量是为了两处永远同源 ——
       写两份的话，调了导轨就漏了搜索框，两颗挨在一起一眼就看出来不齐。 */
    --tools-hub-tab-radius: 10px;
    /* 导轨选中态的配方：底色 / 字色都跟左侧主导航的选中项同源
       （需求：「高度和选中背景色要和 .app-sidebar-nav .nav-item.active 一致」）。

       底色走 --bg-elev-1 —— 就是 .nav-item.active 铺的那一层（自带主题值：
       浅色 4% 黑 / 深色 3% 白），所以两个主题都不用在这里另写覆盖。
       字色走 .nav-item.active 的那两个字面量：浅色 #000000、深色 #ffffff
       （见下面那条 [data-theme="dark"] 覆盖）。
       这里原先写的是 --text-primary —— 单看没问题，两处并排就露馅：浅色下它是
       #1a1523，压在选中项那层 4% 灰底上和主导航的纯黑差一档，导轨的选中分类名
       会显得比左侧主导航的选中项「浅」一点。既然需求是「和主导航一致」，
       就照抄它写的那两个值是唯一不会漂的做法。

       这里原先是一套「浅蓝底 + 蓝字」的配方（--accent-blue 混进面板底色、字再往
       --text-primary 压）。撤掉不是因为不好看，而是同一个产品里不该并存两套「我在这里」：
       左侧主导航铺中性灰、工具页导轨铺淡蓝，两个选中态并排站着就是两种语言。
       底色用固定的 --bg-elev-1 而不是用户可调的 --ui-accent，理由同前 —— 要的就是
       跟主导航一模一样的那层灰。 */
    --tools-hub-tab-active-bg: var(--bg-elev-1);
    --tools-hub-tab-active-fg: #000000;
    /* 140px 只够放六个词，看着像换了个方向的 tab 条；200px 才撑得起「分类区」。
       注意这 60px 是从卡片网格里让出来的，所以 .tools-hub 的最大宽度同步加到 1200px，
       内容列宽度基本回到改动前，卡片不会被挤小。 */
    --tools-hub-rail-w: 200px;
    /* 导轨左右两道白，拆成两个变量、各调各的。
       合在一起（以前是一个 --tools-hub-rail-gutter）时，「跟顶栏左缘对齐」
       和「两边留白一样宽」这两件事只能二选一 —— 左边调到 0 右边就必然多一截。
       拆开之后两边都能单独摆：
         · 左边给 2px           → 导轨往左压 10px，贴近栏的左缘（当前取值）
         · 右边给 35px          → 导轨和功能区之间留出 35px（当前取值，跟整页那道 35px 边距同宽）
       左右白加起来 = 栏间距 + 左白，左白调小的同时第一栏跟着变窄，
       所以「左白 + 导轨 + 右白」总宽会缩，内容列的位置随之左移、宽度变大，
       这是预期内的：导轨往左让出的空间直接给了卡片区。
       右边这道白是从功能区身上让出来的（导轨和 pills 宽度都不动）。 */
    --tools-hub-rail-pad-l: 2px;
    --tools-hub-rail-pad-r: 35px;
    /* 搜索药丸和它下面那条导轨之间那道缝。药丸自己带着这条缝的下边距
       （见 #tools-hub-search-host 的 padding），抽成变量是给下面三处共用：
       「导轨往下让出这一格」「右栏内容顶回这一格」「搜索态标题条吸在药丸下面」。 */
    --tools-hub-rail-gap: 16px;
    /* 宽屏搜索那一格的高度 = 收起态那颗圆钮的直径，也是展开态搜索框的高度。
       40px 是照搬资产库侧栏那颗搜索钮来的（见「宽屏：搜索那一格」那一段的说明），
       不是通用的 --search-box-height（32px）—— 顶栏那颗仍然是 32px，
       所以这里不能去改那个全局值。 */
    --tools-hub-search-h: 40px;
    /* 宽屏搜索那一格占掉的高度 = 圆钮 + 那道缝。
       它是这一页所有竖向对齐的基准：右栏第一条标题的顶边、左栏导轨的吸顶线，
       都从它推出来。 */
    --tools-hub-search-block: calc(var(--tools-hub-search-h) + var(--tools-hub-rail-gap));
    --tools-hub-tabs-top: calc(
        var(--topbar-h, 64px) + var(--safe-top, 0px) + var(--tools-hub-tabs-gap)
    );
    /* 窄屏那条分类条改成「贴着列表底部悬浮」之后，它自己不再占行位，
       但列表得给它让出一条道；下翻页箭头也要抬到它上面。
       三处（条高 / 列表底部内边距 / 箭头偏移）都从这一个值算出来，改一处就同步。
       52px = 图标按钮 40 + 上下各 6 的内边距（见 720px 断点里的
       .tools-hub-tabs-nav::before 和 .tools-hub-section-tabs）。 */
    --tools-hub-float-bar-h: 52px;
    /* 悬浮条离列表下沿的距离。列表要让出的总高度 = 条高 + 2 倍这个值。 */
    --tools-hub-float-bar-gap: 8px;
}

/* 深色下面选中态的字色换成纯白 —— 跟 .app-sidebar-nav .nav-item.active 的深色覆盖
   一一对应（那边也是 [data-theme="dark"] 一段 + prefers-color-scheme 里的
   [data-theme="system"] 一段，两个入口都得补，漏一个就会出现「跟随系统时是黑字
   压深灰底」的情形）。 */
[data-theme="dark"] {
    --tools-hub-tab-active-fg: #ffffff;
}

@media (prefers-color-scheme: dark) {
    [data-theme="system"] {
        --tools-hub-tab-active-fg: #ffffff;
    }
}

/* 「分类导轨 + 内容」的网格。
   align-items: start 不是可选项：默认的 stretch 会把导轨拉满整行，
   导轨盒子高度 = 网格区高度，sticky 就没有可移动的余量、直接失效。
   右列必须走 minmax(0, 1fr) 而不是 1fr，否则卡片网格的最小内容宽度
   会把整列撑破，横向溢出到视口外。
   左白＝导轨自己的 padding-left，右白＝栏间距，两个值各走各的变量。
   注意右白只能在这两个地方出现一次：第一栏里含了右白、栏间距又给一遍的话，
   内容列会被整体推右一个右白的距离。

   position: relative 是给两栏中间那条分割线（::before，见下）当定位祖先用的，
   顺便也是窄屏那颗悬浮分类条的定位祖先（那一层另有说明）。 */
.tools-hub-layout {
    display: grid;
    grid-template-columns: calc(var(--tools-hub-rail-pad-l) + var(--tools-hub-rail-w)) minmax(0, 1fr);
    /* 单行 + stretch：行高由容器钉死（一屏减掉搜索那一格），两栏都拉到整行高 ——
       右栏才有确定高度的滚动区，左栏也才有确定的高度上限给自己内部滚。
       行写 minmax(0, 1fr) 而不是 auto：auto 行会被内容顶高，列表就撑出容器、滚不起来。 */
    grid-template-rows: minmax(0, 1fr);
    align-items: stretch;
    column-gap: var(--tools-hub-rail-pad-r);
    position: relative;
    /* .tools-hub-body 是 flex 列，布局层吃掉它剩下的高度再往下传给两栏。 */
    flex: 1 1 auto;
    min-height: 0;
}

/* 左右两栏中间那条分割线（需求：「中间添加分割线」）。

   横坐标直接从网格公式推：左栏宽 = 左白 + 导轨宽，再往右半个栏间距就是缝的正中。
   挂在网格容器上而不是某一栏上 —— 两栏高度不等时（导轨比内容高，或反过来），
   挂在单栏上只能贯通那一栏，挂在外层才取得到两者的最大值。

   落在这个位置有个额外好处：它不落在任何一栏的盒子里，所以右侧那条吸顶标题条
   （自带不透明底色）盖不到它，线不会被滚动的标题条切断。 */
.tools-hub-layout::before {
    content: "";
    position: absolute;
    left: calc(
        var(--tools-hub-rail-pad-l) + var(--tools-hub-rail-w) + var(--tools-hub-rail-pad-r) / 2
    );
    top: 0;
    bottom: 0;
    width: 1px;
    background: var(--border-soft);
    pointer-events: none;
}

/* 左栏这个盒子：上面「最近使用」、下面分类导轨（见 18-tools-hub.js 的
   renderToolsHubSectionTabs）。它是网格第一栏，所以宽度就是「左白 + 导轨宽」，
   里面的导轨和最近胶囊都靠自己的 padding 对齐，不在这里再留白。

   min-width: 0：flex 项默认按内容撑最小宽度，长分类名会把这一栏顶宽、
   进而把内容列挤窄。 */
.tools-hub-rail {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

/* 左栏顶部的「最近使用」（排在搜索框下方，见 18-tools-hub.js 的 renderToolsHubSectionTabs）。
   头那颗可折叠的按钮（.tools-hub-recent-toggle）和分类导轨项一样宽 —— 它也吃这道白，
   里面的 10px 内边距和导轨项相同，两行图标因此落在同一条竖线上。
   下面那颗胶囊行沿用基础 .tools-hub-chip-row 的 flex-wrap —— 左栏只有 200px 宽，
   顶栏那条「不换行 + 横向滚动」的写法在这里一行只看得见一颗半。

   不再收右边距（原来是「左白 + 右白」）：需求要求胶囊行的宽度和这一层
   （.tools-hub-rail-recent，即网格第一栏宽度）一致，收了右白就只剩 165px。
   现在只有左边让出一道白，胶囊行拿到 200px —— 跟上面那颗搜索药丸、下面那排分类
   按钮一样宽。

   左边这道白（--tools-hub-rail-pad-l）必须留着：左栏本身比搜索药丸宽出这么多，
   而药丸自己缩了同样的 2px（见 #tools-hub-search-host 的 padding-left），
   不要这道白的话「最近使用」会比药丸的左边多冒出来 2px，一眼就看出没对齐。
   分类那排按钮也是靠同一道白对齐的（见 .tools-hub-section-tabs 的 padding-left）。 */
.tools-hub-rail-recent {
    padding-left: var(--tools-hub-rail-pad-l);
}

/* 没有最近记录时 renderToolsHubRecent() 返回空串，这层要整个收掉：
   留着的话它的下内边距会在导轨上方多压出一条空白。 */
.tools-hub-rail-recent:empty {
    display: none;
}

.tools-hub-rail .tools-hub-recent {
    display: flex;
    flex-direction: column;
    gap: 8px;
    /* 基础规则那条 28px 是和别的区块共用的下边距；这一块现在排在导轨上面，
       （展开态）那颗胶囊行和下面的分类导轨之间就靠它分开。
       收起态只有那颗 48px 的按钮，这 20px 把「最近使用」和「分类」两行拉开一档，
       不至于连成一片。 */
    margin-bottom: 20px;
}

/* 搜索态不渲染分类导轨，网格要收成一列 —— 否则内容会掉进左边那一栏，
   右边留出一片空白。
   单列时中间那条分割线也不成立：左栏已经没了，线会画在搜索结果的卡片上。 */
.tools-hub-layout.is-tabs-less {
    grid-template-columns: minmax(0, 1fr);
}

.tools-hub-layout.is-tabs-less::before {
    content: none;
}

/* ---- 高度链条：宽窄共用 ----

   .content → #panel-tools → .tools-hub → .tools-hub-body → .tools-hub-layout
   → .tools-hub-sections → .tools-hub-list-wrap → .tools-hub-list-scroll

   这一页两边都是「只有列表那一层滚」：宽屏是内容区里那两条栏（左栏整列常驻），
   窄屏是整幅列表浮着一条分类条。所以整条链是一个形状、两边共用，
   差别只在 .content 的高度算式（宽屏顶栏整条收起来，--topbar-h 归零；
   窄屏留着顶栏那格报当前分类）和几处窄屏专属的摆法，那些写在文件末尾的 ≤720px 块里。

   为什么不干脆让整页滚（改动前就是那样）：整页滚的话左栏只能靠 position: sticky
   假装钉住，而 sticky 的活动范围被父盒子卡死 —— 左栏的父盒子是网格行，行高取决于
   右栏内容高度，于是右栏越短、窗口越矮，左栏被拖走的距离越大（1280×560 下
   「图像处理」实测被带走 183px，整条滚出视口）。改成列表自己滚之后，左栏就是个普通
   流内盒子，页面不动它就不可能动，不用再靠任何高度余量去凑。

   「撑满一屏」挂 .content 而不是面板：内边距记在 .content 名下，面板自己写
   height: calc(100vh - 顶栏) 算的是不含内边距的高度，再叠上 .content 的上下内边距
   就比视口高出上百 px，工具再少也能空滚一屏。

   中间任何一层漏了 min-height: 0，那一层就按内容高度撑，下一层永远超不过容器、
   也就永远滚不起来（表现为「列表没有滚动条、翻页箭头永远不出现」）。 */
#main-content:has(#panel-tools.is-active) {
    display: flex;
    flex-direction: column;
    padding-top: 0;
    padding-bottom: 0;
    height: calc(100dvh - var(--topbar-h, 64px) - var(--safe-top, 0px));
    min-height: 0;
    overflow: hidden;
}

/* 面板也得是「能收缩的 flex 列」：它是 .content 的 flex 项，高度先封在这一层，
   下面的百分比高度才算得出来。少了这一层，.tools-hub 的百分比高度对着一个
   auto 高度的父盒子算 —— 按规范直接退化成 auto，整条链按内容高撑成几千像素。 */
#main-content:has(#panel-tools.is-active) > #panel-tools {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

.tools-hub-body {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

/* 网格项默认 align-self: stretch，行有多高它就多高，不用再写 height。 */
.tools-hub-sections {
    display: flex;
    flex-direction: column;
    min-height: 0;
}

/* ---- 工具列表包裹 ----
   箭头 / 遮罩都挂在这一层上（见下面那对规则），它同时是渲染的最小单元：
   一直渲染而不是按断点决定渲不渲染，是因为断点来回切就得重跑一遍
   renderToolsHubSections，那会把「画到哪儿了」连同列表一起丢掉。 */
.tools-hub-list-wrap {
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
}

/* 真正滚的那一层。滚动条藏掉（Firefox 一条、WebKit 一条，缺一条 Chrome 上那根条还在），
   上下越过边界时不要把手势传给外层 —— 否则滑到头会带动整个页面。
   滚动提示交给已有的上下箭头 + 两端渐隐，不用浏览器那根条。 */
.tools-hub-list-scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-x: hidden;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: none;
    -ms-overflow-style: none;
}

.tools-hub-list-scroll::-webkit-scrollbar {
    display: none;
    width: 0;
    height: 0;
}

/* ---- 两端渐隐遮罩 ----
   从列表底色渐到透明，把滚到边上的卡片「晕」出去而不是硬切一刀。
   只在「还能往那头滚」时显形：滚到最顶就没有上遮罩，否则第一行永远是糊的。
   宽窄两屏共用（两边都是列表自己滚），只有窄屏那条「分类条浮在底部」的例外。 */
.tools-hub-list-mask {
    display: block;
    position: absolute;
    left: 0;
    right: 0;
    height: 48px;
    z-index: 4;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.16s ease;
}

.tools-hub-list-mask.is-top {
    top: 0;
    background: linear-gradient(
        to bottom,
        var(--bg-base) 0%,
        color-mix(in srgb, var(--bg-base) 55%, transparent) 55%,
        transparent 100%
    );
}

.tools-hub-list-mask.is-bottom {
    bottom: 0;
    background: linear-gradient(
        to top,
        var(--bg-base) 0%,
        color-mix(in srgb, var(--bg-base) 55%, transparent) 55%,
        transparent 100%
    );
}

.tools-hub-list-wrap.can-scroll-up .tools-hub-list-mask.is-top,
.tools-hub-list-wrap.can-scroll-down .tools-hub-list-mask.is-bottom {
    opacity: 1;
}

/* ---- 上下翻页箭头 ----
   居中浮在列表的两端，压在遮罩上面（z-index 比遮罩高一层）。
   滚到那头就整颗收起来：留着它会压住第一张 / 最后一张卡片。
   唯一动它的是窄屏那条「分类条浮在底部」——下箭头得再往上抬，见文件末尾。 */
.tools-hub-list-arrow {
    position: absolute;
    left: 50%;
    z-index: 6;
    display: none;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    margin: 0;
    padding: 0;
    border: none;
    border-radius: 999px;
    transform: translateX(-50%);
    background: var(--panel-surface-bg);
    color: var(--text-secondary);
    box-shadow: 0 1px 2px color-mix(in srgb, var(--text-primary) 8%, transparent),
        0 4px 12px color-mix(in srgb, var(--text-primary) 8%, transparent);
    cursor: pointer;
    transition: color 0.2s ease, background-color 0.2s ease;
}

.tools-hub-list-arrow svg {
    width: 16px;
    height: 16px;
    display: block;
}

/* 上箭头落在吸顶标题条的下沿之外。
   标题条是 sticky 的，永远压在列表顶上；它里面的「分类名 + 数量」是水平居中的，
   而上箭头也是水平居中的 —— 留在原来的 8px 就正好盖住分类名（实测箭头 676–708
   正压在标题 637–709 上）。
   偏移从标题条高度算出来，不写死的道理见 --tools-hub-section-head-h。 */
.tools-hub-list-arrow.is-up {
    top: calc(var(--tools-hub-section-head-h) + 8px);
}

.tools-hub-list-arrow.is-down {
    bottom: 8px;
}

.tools-hub-list-wrap.can-scroll-up .tools-hub-list-arrow.is-up,
.tools-hub-list-wrap.can-scroll-down .tools-hub-list-arrow.is-down {
    display: inline-flex;
}

.tools-hub-list-arrow:hover,
.tools-hub-list-arrow:focus-visible {
    background: color-mix(in srgb, var(--text-primary) 7%, var(--panel-surface-bg));
    color: var(--text-primary);
}

.tools-hub-list-arrow:focus-visible {
    outline: 2px solid color-mix(in srgb, var(--ui-accent, #7c5cff) 55%, transparent);
    outline-offset: 2px;
}

/* 导轨外面这层包裹有两个作用，都不是装饰：

   1) 窄屏横滑时，它给两颗翻页箭头当定位父级（见文件末尾的 720px 断点）。
   2) 宽屏下它是左栏里「分类」那一段的盒子：底色铺在这一层，分类变多时自己滚
      （见 .tools-hub-section-tabs 的 overflow-y）。

   宽屏这边的吸顶（position: sticky / top / 补带）在左栏改成整列常驻之后不再生效 ——
   左栏自己那层不滚，没有可吸的东西；保留是因为这几条和窄屏共用一条基础规则，
   窄屏块会把它们整个撤掉换成底部悬浮条（见文件末尾）。 */
.tools-hub-tabs-nav {
    position: sticky;
    top: var(--tools-hub-tabs-top);
    z-index: 20;
    background: var(--bg-base);
    box-shadow: 0 calc(-1 * var(--tools-hub-tabs-gap)) 0 0 var(--bg-base);
}

.tools-hub-section-tabs {
    display: flex;
    flex-direction: column;
    /* 行式导航之间的缝比原来的胶囊小：胶囊靠外形分隔，行靠底色分隔，
       缝一大反而显散。 */
    gap: 4px;
    /* 导轨盒子比 pills 宽出左边那道白，网格第一栏也按「左白 + 导轨宽」开，
       pills 才不会被挤窄。右白是栏间距，不在这里重复留。 */
    padding-left: var(--tools-hub-rail-pad-l);
    /* 分类变多、或窗口变矮时，导轨自己滚 —— 不把页面撑长，也不让末尾几项够不着。
       用 dvh 不用 vh：移动端地址栏收起/展开时 vh 不跟着变，导轨底部会被顶出视口。
       上下各留 24px，滚动时首尾两项不会贴着视口边。 */
    max-height: calc(100dvh - var(--tools-hub-tabs-top) - 24px);
    overflow-y: auto;
    /* 滚到底不要再把滚动传给页面 —— 否则在导轨上继续滚会带着整页一起走。 */
    overscroll-behavior: contain;
    scrollbar-width: none;
    -ms-overflow-style: none;
}

.tools-hub-section-tabs::-webkit-scrollbar {
    display: none;
}

/* 行式导航：图标 + 名称 + 数量。
   选中态完全靠底色表达（见下面的 .is-active），这里不加 border-left 之类的
   左侧装饰 —— 那会让选中项的内容整体右移，切来切去像在抖。

   字号 / 字重 / 字色都和左侧主导航的 .nav-item 同一个配方（需求：
   「文字和图标线条粗细要和 [nav#main-nav 的 nav-item] 一致」）：
   14px、400、--text-secondary（选中换 --tools-hub-tab-active-fg）——
   这两列导航在很多页面上并排站着，字号差 1px、字重差一档就看得出不是一套。 */
.tools-hub-section-tab {
    display: flex;
    align-items: center;
    gap: 9px;
    width: 100%;
    height: var(--tools-hub-tabs-h);
    padding: 0 10px;
    border: 0;
    border-radius: var(--tools-hub-tab-radius);
    background: transparent;
    color: var(--text-secondary);
    font: inherit;
    font-size: 14px;
    font-weight: 400;
    text-align: left;
    cursor: pointer;
    box-sizing: border-box;
    transition: background 0.15s ease, color 0.15s ease;
}

.tools-hub-section-tab:hover {
    background: color-mix(in srgb, var(--text-primary) 5%, transparent);
    color: var(--text-primary);
}

.tools-hub-section-tab:focus-visible {
    outline: 2px solid color-mix(in srgb, var(--ui-accent, #7c5cff) 45%, transparent);
    outline-offset: -2px;
    background: color-mix(in srgb, var(--text-primary) 5%, transparent);
    color: var(--text-primary);
}

/* 选中态：中性灰底 + 主色字，与左侧主导航的选中项同一套（需求：「高度和选中背景色要和
   .app-sidebar-nav .nav-item.active 一致」）。

   这一条改过几轮，每一轮的动机都值得留一笔：最早是「浅底 + 左侧强调条」两套指示叠着用 ——
   深色主题下那层 10% 的浅底几乎看不见，剩下的就只有一条孤零零的竖线，而竖线是拿 inset
   阴影画的，本质上还是个「贴边的装饰」；中间那版改成整块铺主题色 + 文字反白，视觉重量又跟
   「按下的按钮」一样，看不出是「当前在哪」；上一版是「浅蓝底 + 蓝字」。
   现在换成中性灰，是因为它不该自成一套 —— 同一屏里左侧主导航铺的就是这层灰。

   底色 / 字色都走 :root 那两条 --tools-hub-tab-active-*（见那里的说明），
   不在这里现写颜色：写死了就没法跟主导航保持同源。 */
.tools-hub-section-tab.is-active {
    background: var(--tools-hub-tab-active-bg);
    color: var(--tools-hub-tab-active-fg);
    /* 不跟着加粗：主导航的 .nav-item.active 也保持 400，
       「你在这里」只由底色 + 字色说（见那条的说明）。加粗会让选中项的字比
       左边那一列粗一档，两列并排就不像同一套导航了。 */
    font-weight: 400;
    box-shadow: none;
}

/* 图标块：尺寸 / 颜色 / 线宽都照左侧主导航那排图标来（需求：「图标颜色大小参考
   [nav#main-nav 里 nav-item 的] 图标」+「图标线条粗细要和 [同一个 nav-item] 一致」）。
     · 尺寸 22px —— svg.nav-item-icon 就是 22px（原先这里 15px，200px 宽的导轨里
       比主导航那排小一圈，两块并排站着不像同一套导航）
     · 颜色继承整行前景色 —— 主导航的图标不带头自己的颜色：未选中 --text-secondary、
       选中 #000000 / #ffffff（见 03-app-sidebar.css 的 .nav-item.active）。
       这里同样只写 color: inherit，让「选中＝纯黑/纯白、未选中＝次级灰」跟着整行走。
     · 线宽 1.4（--sidebar-icon-stroke）—— 主导航那排走的也是这个 token。
   原先每颗图标套的是分类色（--tools-hub-hue）：12 颗彩图标和左侧那排中性图标并排
   就是两种语言；而且选中项的图标本来就得换成行前景色（否则一行里两个前景色打架），
   于是同一排里只剩未选中项带彩 —— 看上去像「没选中的更醒目」。
   卡片图标随后也走了同一条路（需求：「背景色以及图标颜色要和侧边栏的按钮一致」），
   所以这套分类色到此为止一个消费者都没有了。 */
.tools-hub-section-tab-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 22px;
    height: 22px;
    /* 图标不垫底（需求：「去除背景色」）。
       原先这里是一层 12% 分类色淡底 + 7px 圆角，把图标圈成一块小方片；
       选中项再叠一层蓝。导轨本来就窄，文字和数量挨着一排彩色方片，
       整条看上去是「一堆按钮」而不是「一列分类」；而且那层底只在这一页出现 ——
       左侧主导航的选中项从头到尾都是裸图标。
       底色撤了之后圆角也跟着撤：没有底的方盒子，7px 谁也看不见。 */
    color: inherit;
}

.tools-hub-section-tab-icon svg {
    width: 22px;
    height: 22px;
    /* 线宽跟主导航同源。token 定义在 03-app-sidebar.css 的 :root 上，这一页够得着；
       ⚠️ 第 205 轮主导航曾自己换成 --sidebar-nav-icon-stroke（需求「导航里的图标线条粗一些」），
       这条「同源」当时跟着换过；第 337 轮主导航按需求并回 --sidebar-icon-stroke（1.4，跟收起按钮一档），
       这里也跟着并回去 —— 两列图标是并排看的，一个细一个粗会很显眼。
       svg 上的 stroke-width="1.8"（见 buildAiToolIconHtml）是 presentation attribute，
       CSS 一律压过它 —— 所以调线宽不用回 JS 改那个字符串。
       写 var(..., 1.4) 的兜底：token 万一被挪走，这里也不至于落成 svg 自带的 1.8。 */
    stroke-width: var(--sidebar-icon-stroke, 1.4);
}

/* 选中项不需要再写一条覆盖：整行选中时 color 已经是 --tools-hub-tab-active-fg
   （浅色 #000000 / 深色 #ffffff，和主导航的选中项同值），图标继承下来就是同一个色 ——
   这正是「颜色参考主导航图标」想要的。窄屏是唯一的例外，见 720px 断点里那条反白。 */

/* 窄屏那排圆钮背后的滑动选中块（位置由 18-tools-hub.js 的
   syncToolsHubTabsIndicator 写 transform，这里只管外观和尺寸）。

   宽屏导轨的选中态是整行的主题色底、画在按钮自己身上；窄屏按钮收成一颗圆之后，
   同一件底色换成「跟着滑的一块」：切换时从旧钮滑到新钮。一排纯图标里，
   硬跳的「亮 / 灭」很难看出是选中了还是刚点过。

   基础规则是 display: none —— 宽屏那排不走这套，只有 720px 断点里才放出来。 */
.tools-hub-tabs-indicator {
    display: none;
    position: absolute;
    top: 0;
    left: 0;
    width: var(--tools-hub-tabs-h);
    height: var(--tools-hub-tabs-h);
    border-radius: 50%;
    background: var(--ui-accent, #7c5cff);
    /* 纯装饰层：压在按钮底下，绝不能拦住点按 */
    pointer-events: none;
    will-change: transform;
}

/* 深色主题不再需要给卡片图标「提亮一档」了：分类色整套已经撤掉
   （见 .tools-hub-card-icon 那条的说明），图标走的是 --text-secondary ——
   它自己就带深色值（rgba(255,255,255,.72)），压在深色卡片上对比度本来就够。 */
.tools-hub-section-tab-label {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    line-height: 1;
}

.tools-hub-section-tab-count {
    flex: 0 0 auto;
    min-width: 20px;
    padding: 1px 6px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--text-primary) 6%, transparent);
    color: var(--text-tertiary);
    font-size: 11px;
    font-weight: 600;
    line-height: 1.6;
    text-align: center;
    /* 数字等宽：数量变化时胶囊宽度不会跳。 */
    font-variant-numeric: tabular-nums;
}

/* 选中项里的数量胶囊：垫底那层原先跟图标块一样是 18% 的蓝，配的是「浅蓝底 + 蓝字」。
   底色换成中性灰之后，一颗蓝胶囊就成了这一行里唯一的彩色 —— 比选中态本身还抢眼。
   跟着换成中性灰：比未选中那层（6%）深一档（10%），压在选中行的 4% 灰底上才分得出来。 */
.tools-hub-section-tab.is-active .tools-hub-section-tab-count {
    background: color-mix(in srgb, var(--text-primary) 10%, transparent);
    color: var(--tools-hub-tab-active-fg);
}

.tools-hub-card-badge--local {
    color: var(--ui-accent, #0f766e);
    background: color-mix(in srgb, var(--ui-accent, #0f766e) 12%, transparent);
    border-color: color-mix(in srgb, var(--ui-accent, #0f766e) 24%, transparent);
}

.tools-hub-card-badge--local + .tools-hub-card-meta-item {
    margin-left: 0;
}

/* ==== 宽屏：左栏常驻，右栏自己滚 ====

   这一块单独放在文件末尾，是有原因的：测试里的 ruleBody() 取的是文件里
   「第一处」匹配，而这里用到的选择器（#tools-hub-search-host、.tools-hub-rail、
   .tools-hub-section-head、.tools-hub-sections）在上面都各有一条基础规则 ——
   块要是插在它们前面，读法就会拿到这一块，把基础规则挡住。
   （同样的注意事项在 .tools-hub-section-tab-icon 那一段也写过一次。）

   —— 需求：「拆分成左右布局，右边滚动不要带动左边列表」。

   滚动权从「页面」交给右栏（高度链条见上面那一段），于是左栏就是个普通流内盒子：
   页面不动它就不可能动。之前那一版是「整页滚 + 左栏 sticky」，而 sticky 的活动范围
   被父盒子（网格行）卡死 —— 行高取决于右栏内容高度，右栏越短、窗口越矮，
   左栏被拖走的距离越大（1280×560 下「图像处理」实测被带走 183px，整条滚出视口）。
   那一版的搜索格和导轨两条 sticky、以及右栏那条负外边距，在这里一并撤掉。 */
@media (min-width: 721px) {
    /* 搜索那一格脱离流：两栏从同一个顶边起，右栏第一条标题（它吸在自己那层的
       滚动容器顶上，也就是这一行的顶边）正好和搜索框齐平。
       用绝对定位而不是 sticky —— 页面已经不滚了，「跟着不动」由布局保证，
       不必再靠粘性和高度余量去凑。 */
    #tools-hub-search-host {
        position: absolute;
        top: 0;
        left: 0;
        /* 只占导轨那一列的宽度，不横跨整幅内容区（原先写的是 right: 0）——
           需求把圆钮改成了「靠右显示」，而「右」指的是导轨的右缘：
           这一格要是继续铺满 860px，圆钮的 auto 外边距会把它甩到卡片区的最右端，
           和它下面那排分类项离着 600 多 px，看着像两颗不相干的东西。

           宽度 = 导轨内容宽 + 导轨自己那道左白，和 .tools-hub-rail 的盒子宽度同源；
           右边那道 35px（--tools-hub-rail-pad-r）是导轨和功能区之间的留白，
           这一格本来也不该跨过去。 */
        right: auto;
        width: calc(var(--tools-hub-rail-w) + var(--tools-hub-rail-pad-l));
        z-index: 30;
        /* 顶上那 20px 是 .tools-hub 的上内边距（--tools-hub-pad-top）。
           搜索格脱离流之后得自己补回来，否则会顶到内容区最上边。 */
        padding: var(--tools-hub-pad-top) 0 0 var(--tools-hub-rail-pad-l);
    }

    .tools-hub {
        position: relative;
    }

    /* 左栏：普通网格项，宽高都由网格给，自己不滚也不动。
       顶上让出搜索那一格 —— 搜索框是浮上来的，这里必须空出来；
       让出来之后第一个流内子元素就是「最近使用」，它因此正好落在搜索框下面（见需求）。 */
    .tools-hub-rail {
        padding-top: var(--tools-hub-search-block);
    }

    /* 分类这一段吃掉左栏剩下的高度：分类变多、或窗口变矮时它自己滚，
       不把上面的「最近使用」顶出视口（左栏整列的高度有上限，不像原来那样能撑长）。
       「最近使用」排在它前面、又是 flex: 0 0 auto，所以要从「剩下的高度」里分，
       这里得写 1 1 auto 去主动吃掉剩余空间（0 1 auto 的话这一层会缩回内容高，
       分类多出来那几项就没地方滚了）。 */
    .tools-hub-tabs-nav {
        flex: 1 1 auto;
        min-height: 0;
        display: flex;
        flex-direction: column;
    }

    .tools-hub-section-tabs {
        flex: 1 1 auto;
        min-height: 0;
    }

    /* 窗口变矮时收缩的是这一列的滚动区，不是每一颗胶囊本身：胶囊是定高的，
       被压扁会让图标和文字挤在一起（收缩到一定程度还会看不到滚动的迹象）。 */
    .tools-hub-section-tab {
        flex: 0 0 auto;
    }

    /* 「最近使用」排在导轨上面，不参与收缩（它是固定高度的一小块，
       压扁了里面的胶囊会挤在一起）。 */
    .tools-hub-rail-recent {
        flex: 0 0 auto;
    }

    /* 右栏是滚动容器了，最后一行卡片不贴下边缘 */
    .tools-hub-list-scroll {
        padding-bottom: 24px;
    }

    /* 搜索态是单列、整幅铺开的，结果标题会压在浮着的搜索框上 —— 让出一格。
       让给 .tools-hub-sections 而不是标题条自己：标题条吸的是容器的顶边（0），
       容器下移之后它自然落在搜索框下面。 */
    .tools-hub-layout.is-tabs-less .tools-hub-sections {
        padding-top: var(--tools-hub-search-block);
    }

    /* 箭头要压在这条带子上面：标题条的 z-index 是 10，箭头基础值是 6。
       上箭头已经挪到带子的下沿之外（见 .tools-hub-list-arrow.is-up 的 top），
       这条留着是兜底 —— 万一标题条比 --tools-hub-section-head-h 还高
       （字体 / 行高被改过），箭头宁可压在带子那条空的下内边距上，
       也不要被不透明底吃掉半截。 */
    .tools-hub-list-arrow {
        z-index: 30;
    }
}

/* 窄屏放不下「导轨 + 内容」两栏，退回成内容上方一行横滑的分类条。
   同时列表改成「自己滚」：页面不再滚，只有列表那层滚（见下面的高度链条）。 */
@media (max-width: 720px) {
    /* 两栏并排时才有的那条中间分割线：网格收成一列之后它就不成立了 ——
       留着会变成一条贴着屏幕左缘的竖线，还跟着列表一起滚。 */
    .tools-hub-layout::before {
        content: none;
    }

    /* 左栏这层盒子在窄屏要整个「消失」。
       分类条在窄屏是 absolute 悬浮的（见下面 .tools-hub-tabs-nav 那条），
       它的定位祖先必须是 .tools-hub-layout；中间夹一层实体盒子会把包含块换成这一层，
       悬浮条的位置就跟着左栏的高度跑了。display: contents 后导轨和内容重新成为
       .tools-hub-layout 的直接网格项，跟加这层之前一模一样。 */
    .tools-hub-rail {
        display: contents;
    }

    /* 「最近使用」在窄屏本来就不显示（顶栏那格报的是当前分类），
       跟着搬到左栏之后同样收掉 —— 否则列表上面会多出一行胶囊。 */
    .tools-hub-rail-recent {
        display: none;
    }

    .tools-hub-layout {
        grid-template-columns: minmax(0, 1fr);
        /* 分类条改浮在列表底部之后就不再是网格里的第一行了（它 position: absolute
           退出了流），网格收成一整行，全部高度给列表。单行 + stretch 那条在基础层。 */
        gap: 0;
        /* 悬浮的分类条拿它当定位祖先。用 .tools-hub-layout 而不是 .tools-hub-body：
           两者在窄屏下高度一致，但列表底部内边距那几条规则也要挂在同一层上，
           挂同一处读起来才是一条线。 */
        position: relative;
    }

    /* 搜索态没有分类条，只剩列表一个子项 —— 它落在唯一那一行里（基础层已经钉好单行）。 */

    .tools-hub {
        /* 顶栏在窄屏还在（报当前分类），高度链条的其余部分见基础层那一段。 */
        padding-top: 0;
    }

    .tools-hub-section-tabs {
        flex-direction: row;
        flex-wrap: nowrap;
        overflow-x: auto;
        /* 竖排时给的那条高度上限和纵向滚动在横排下用不上，必须撤掉：
           留着 max-height 会把这条横滑条压扁。 */
        max-height: none;
        overflow-y: hidden;
        gap: 6px;
        /* 这条现在整条飘在列表底部，和下面第一行卡片之间的间距由悬浮条自己的
           外框和内边距负责，不再留 margin-bottom（基础规则那条 22px 会让条子
           在圆角里多出一条空气带）。 */
        margin-bottom: 0;
        /* 横排时导轨占满整行，左白和栏间距一起撤掉；四周补一圈内边距，
           图标才不会贴着圆角边。 */
        padding: 6px;
        scrollbar-width: none;
        -ms-overflow-style: none;
    }

    /* 图标按钮在条里居中。
       用两边自动外边距，而不是 justify-content: center —— 这条是
       overflow-x: auto 的滚动容器，justify-content 居中一旦内容比容器宽，
       超出左边的那几颗就永远滚不到（滚动原点被推到内容中间，scrollLeft 到 0
       时左边仍是负坐标区）。自动外边距在「放得下」时吸收剩余空间（＝居中），
       「放不下」时归零，第一颗照旧贴着滚动原点、滚得到。 */
    .tools-hub-section-tab:first-child {
        margin-left: auto;
    }

    .tools-hub-section-tab:last-child {
        margin-right: auto;
    }

    .tools-hub-section-tabs::-webkit-scrollbar {
        display: none;
    }

    /* ---- 分类条：贴着列表底部悬浮 ----
       手机上分类条收成了一排图标（当前分类名在顶栏那格报着），它不再需要占一整行：
       底下那条 66px（44 的条 + 22 的间距）整条还给卡片，拇指也不用往上够。

       半透明 + 毛玻璃：底下的卡片从条子后面透出来一点，既说明「这是浮在上面的」，
       又不至于像一块实心板子把列表拦腰切断。

       它从网格里退出来（absolute）之后就不占行位了，所以 .tools-hub-layout 改成了
       单行 + position: relative 给它当定位祖先（见上面那条规则）。 */
    .tools-hub-tabs-nav {
        position: absolute;
        left: var(--tools-hub-float-bar-gap);
        right: var(--tools-hub-float-bar-gap);
        bottom: var(--tools-hub-float-bar-gap);
        /* 基础规则给宽屏吸顶用的 top / 不透明底 / 往上补的同色带，这里一并撤掉 */
        top: auto;
        flex: 0 0 auto;
        background: transparent;
        box-shadow: none;
        /* 胶囊型：条高就是 --tools-hub-float-bar-h（52px），999px 会被夹到 26px，
           两端各收成一个半圆，跟里面那排圆钮是同一套语言。 */
        border-radius: 999px;
    }

    /* 半透明那层铺在 ::before 上，而不是直接给 .tools-hub-tabs-nav 上底色：
       里面的横滑条是 overflow-x: auto 的滚动容器，底色挂在它身上会跟着内容一起滚，
       滚到边界时左右两端各露出一个实心直角。铺在包裹层上就永远和圆角对齐。

       不画描边：半透明底 + 毛玻璃已经把这条和底下的卡片分开了，再加一圈线就是
       双重分隔；而且描边是不透明的，卡片从底下透上来时只有那圈线实心，反而很脏。
       边界靠下面那两道投影钉住就够了。 */
    .tools-hub-tabs-nav::before {
        content: "";
        position: absolute;
        inset: 0;
        border-radius: inherit;
        background: color-mix(in srgb, var(--panel-surface-bg) 66%, transparent);
        box-shadow: 0 2px 6px color-mix(in srgb, var(--text-primary) 10%, transparent),
            0 10px 28px color-mix(in srgb, var(--text-primary) 14%, transparent);
        /* Safari 要带前缀的那份，少写就是 iOS 上一块纯半透明板子（不模糊） */
        -webkit-backdrop-filter: blur(16px) saturate(1.6);
        backdrop-filter: blur(16px) saturate(1.6);
        /* 这层纯装饰，别把点按挡掉（它盖在按钮上面） */
        pointer-events: none;
    }

    /* 横滑条本身要压在 ::before 上面，否则图标和选中态都被那层半透明底盖住 */
    .tools-hub-tabs-nav .tools-hub-section-tabs {
        position: relative;
    }

    /* ---- 列表自己滚（窄屏的差异部分）----
       高度链条和滚动条的隐藏都在基础层，这里只有窄屏自己的上下留白。 */
    .tools-hub-list-scroll {
        /* 滚起来更跟手（老 iOS 的惯性滚动） */
        -webkit-overflow-scrolling: touch;
        /* 上下各留一点，卡片不贴边。
           顶部那 10px 比底部多：窄屏下没有内容区标题条（名字在顶栏那格），
           第一行卡片原本是紧贴着顶栏下缘的，看着像被压了一半。 */
        padding-top: 10px;
        /* 底部留一点：滚到最后一行时卡片不贴边，也给下遮罩一点渐变余地 */
        padding-bottom: 8px;
    }

    /* 列表底部让出悬浮的分类条那一条高度：不让的话最后一行卡片永远压在条子底下，
       滚到底也翻不出来（scrollHeight 里没这段空间，滚不动）。

       内边距挂在滚动容器上（不是包裹层）是有讲究的：它进 scrollHeight，
       所以最后一行能被滚到条子上面；换 margin 就白写了 —— margin 不参与
       scrollHeight，滚到底还是压在条子底下。

       选择器挂在 :has(.tools-hub-tabs-nav) 上：搜索态没有分类条（.is-tabs-less），
       那时不该白留一条空白。 */
    .tools-hub-layout:has(.tools-hub-tabs-nav) .tools-hub-list-scroll {
        padding-bottom: calc(
            var(--tools-hub-float-bar-h) + 2 * var(--tools-hub-float-bar-gap) + 8px
        );
    }

    /* ---- 两端渐隐遮罩 ----
       从列表底色渐到透明，把滚到边上的卡片「晕」出去而不是硬切一刀。
       只在「还能往那头滚」时显形：滚到最顶就没有上遮罩，否则第一行永远是糊的。 */
    /* 分类条浮在列表底部之后，下遮罩必须撤掉 —— 两个理由：

       1) 它们占的是同一块地方。遮罩那条 48px 的渐变横跨悬浮条（离底 8–62px），
          条子上半截压着未渐变的卡片、下半截压着渐成底色的部分，条子里会裂出一道横缝。
       2) 遮罩渐到的是列表底色，条子又是半透明的 —— 透过条子看到的是一片灰蒙，
          等于把「半透明」做成了「不透明」。

       往上挪也不行：那样内容会先在遮罩里淡掉、再从条子底下原样冒出来。
       底部那道「还有内容」的提示交给下翻页箭头（它自己带底色和阴影，不受影响）。 */
    .tools-hub-layout:has(.tools-hub-tabs-nav) .tools-hub-list-mask.is-bottom {
        display: none;
    }

    /* 下箭头要抬到悬浮的分类条上面 —— 留在底边 8px 会被条子整个吃掉。
       偏移 = 条子离底距离 + 条高 + 一个间距，和列表让出的那条内边距同源。 */
    .tools-hub-list-arrow.is-down {
        bottom: calc(var(--tools-hub-float-bar-h) + 2 * var(--tools-hub-float-bar-gap));
    }

    /* 窄屏的内容区标题条整个收掉了（见下面 .tools-hub-section-head 那条 display: none），
       顶头没有那条带子要躲，上箭头回到贴顶的 8px。 */
    .tools-hub-list-arrow.is-up {
        top: 8px;
    }

    /* 分类名搬进顶栏之后，横滑条只留图标：
       七个分类各 40px 见方（--tools-hub-tabs-h），一屏就摆得下，
       不用再横滑找。图标块的尺寸和配色沿用基础规则，只把按钮从
       「图标 + 名称 + 数量」的行式导航收成正方形。 */
    .tools-hub-section-tab {
        flex: 0 0 auto;
        width: var(--tools-hub-tabs-h);
        padding: 0;
        justify-content: center;
        /* 圆形：宽高都是 --tools-hub-tabs-h（40px），50% 正好是半径 20 的圆。 */
        border-radius: 50%;
        /* 提到位图层，是为了压住下面那颗滑动选中块。
           指示器是 position: absolute，按钮是静态的话它画在按钮之上 ——
           圆会盖住图标。两层都在位图层里、按钮的 DOM 顺序在后，就翻过来了。 */
        position: relative;
    }

    /* 选中态从「按钮自己铺色」改成「背后那颗圆铺色」。
       两处同时铺的话，按钮的不透明底会把指示器整个盖住，滑动也就看不见了。

       顺手把 :hover 那层 5% 的灰底也撤掉：触摸端的 hover 会「粘」在刚点过的
       那颗钮上，指示器滑走之后原地留一圈灰，看着像没滑干净。
       点按的反馈已经由指示器本身给了。 */
    .tools-hub-section-tab:hover,
    .tools-hub-section-tab:focus-visible,
    .tools-hub-section-tab.is-active {
        background: transparent;
    }

    /* 滑动选中块在这个断点才放出来（宽屏导轨用的是按钮自己的整行底色）。
       位移不写在这里：切分类会整块重建，新节点没有「上一帧位置」可过渡，
       所以走 JS 的两个关键帧（见 syncToolsHubTabsIndicator）。 */
    .tools-hub-tabs-indicator {
        display: block;
    }

    /* 名称在横滑条上收起来 —— 顶栏那格已经在报了。
       收起用 display: none（不是透明度），按钮的可访问名就只剩 title 和
       aria-label，两个都得留：title 见 renderToolsHubSectionTabs，
       aria-label 在同一个函数里写，读屏念得到的还是「本地免费」。 */
    .tools-hub-section-tab-label {
        display: none;
    }

    /* 横滑条上位置紧张，数量胶囊先让位（导轨本身已经能滚，不靠它导航） */
    .tools-hub-section-tab-count {
        display: none;
    }

    /* 图标块那层垫底（原先是 12% 分类色 + 7px 圆角）现在是全断点都没有的 ——
       基础规则里就撤掉了（需求：「去除背景色」），窄屏这里不用再撤一遍。
       窄屏真正的差异只有下面这条反白。 */

    /* 窄屏的选中指示是背后那颗主题色圆（.tools-hub-tabs-indicator），
       压在它上面的图标必须反白 —— 电脑端那套「整行灰底 + 前景色图标」的配方
       摆到实心主题色圆上没对比度。数量胶囊这一屏本来就 display: none，不用管。

       这是图标色唯一一处例外：电脑端的选中色（--tools-hub-tab-active-fg：
       浅色纯黑）压在主题色圆上会糊掉，所以这一屏的选中图标直接给纯白。 */
    .tools-hub-section-tab.is-active .tools-hub-section-tab-icon {
        color: #ffffff;
    }

    /* 内容区那条标题条在窄屏整个收掉：它的名字和数量已经搬到顶栏那一格
       （.tools-hub-docked-head，见上面），同一屏里出现两份没意义 ——
       而且窄屏下它是列表顶上的第一行，占着 41px 却不提供任何操作。
       顺带解决「横滑导轨也吸顶、标题条再吸一次会叠在一起」那件事：
       display: none 的元素不参与吸顶。 */
    .tools-hub-section-head {
        display: none;
    }

    /* 搜索态是例外，得单独放回来。
       搜索框一展开，顶栏那格就淡出让位（见下面那条 opacity: 0）——「最近使用」
       可以让，因为它是可选信息；但「搜索结果」这时是列表里唯一的标题，
       让掉就什么标识都没有了。所以内容区这一条留给它。
       吸顶那套照旧要撤：窄屏是列表自己滚，stick 在滚动容器里会吊在半空。 */
    #tools-hub-section-search .tools-hub-section-head {
        display: flex;
        position: static;
        z-index: auto;
        box-shadow: none;
    }

    /* ---- 窄屏顶栏那格：当前分类，左右各留 60px 居中 ----
       左端躲开侧栏 logo（固定定位在 10–42px），右端躲开搜索药丸（右端 16px 起）。
       用绝对定位而不是在 flex 里加 margin：只有这样才能同时钉住左右两条边、真正居中。
       电脑端这一格是空的，这条规则不会碰上它。 */
    html[data-tools-tabs-docked] .topbar-tools-recent-slot {
        position: absolute;
        left: 60px;
        right: 60px;
        top: 50%;
        transform: translateY(-50%);
        width: auto;
        margin-left: 0;
        padding-left: 0;
        padding-right: 0;
        /* 窄屏那格里只有一颗分类名，居中摆 */
        justify-content: center;
        transition: opacity 0.16s ease;
    }

    /* 药丸展开会向左撑到 255px，正好压在顶栏那格上 ——
       与其让分类名从搜索框底下钻出来，不如整条淡出让位。 */
    .topbar:has(.tools-hub-search-box:is(.is-expanded, .is-search-click-open)) .topbar-tools-recent-slot {
        opacity: 0;
        pointer-events: none;
    }

    /* ---- 搜索药丸的右缘离顶栏右缘 10px ----
       顶栏自己留了 16px；`.topbar-actions` 里那个空的 `.topbar-right` 还带着
       10px 的 flex gap 和 6px 的 margin-right（工具页的积分 / 头像都收进侧栏了，
       它的宽度其实是 0）。加起来药丸右缘离屏幕边 32px，比设计稿宽了一大截。
       工具页窄屏上把这三处一起收干净：32 → 10。 */
    html[data-tools-tabs-docked] .topbar {
        padding-right: max(10px, var(--safe-right));
    }

    html[data-tools-tabs-docked] .topbar-actions {
        gap: 0;
    }

    html[data-tools-tabs-docked] .topbar-actions > .topbar-right {
        margin-right: 0;
    }

    /* ---- 顶栏半透明 ----
       跟列表底部那条悬浮分类条凑成一套「玻璃浮层」的观感：上下两条都是
       半透明 + 毛玻璃，中间那块列表夹在两者之间。

       注意窄屏下这条列表是独立滚动容器（#main-content 高度封在 100dvh - 顶栏），
       卡片不会滚到顶栏底下去 —— 所以这层滤镜在工具页上采到的其实是页面底色，
       观感取决于底色和 --topbar-bg 的差；底色是同一个 token 时它就只是「不画分割线」
       那一半在起作用。真要让卡片从顶栏底下滑过、把毛玻璃用满，得让列表顶边
       顶到 0（整条高度链要跟着改），那是另一件事。

       玻璃态 / 自定义底色那两个变体的选择器是 3 个属性（见文件顶部那组规则），
       只写 html[data-tools-tabs-docked] .topbar 会被它们盖回不透明底，
       所以这里逐个列出来，保持同层级、靠后覆盖。 */
    html[data-tools-tabs-docked] .topbar,
    html[data-tools-tabs-docked][data-topbar-glass="true"] .topbar,
    html[data-tools-tabs-docked][data-topbar-custom="true"] .topbar {
        /* 底色和模糊都交给 ::after，顶栏自己必须是透明的：
           backdrop-filter 采样的是「这个元素背后已经画好的东西」。底色留在
           顶栏身上的话，::after 采到的就是那层半透明底自己（一片纯色），
           等于模糊了个寂寞。 */
        background: transparent;
        /* 底下那条分割线去掉：半透明底已经把两层分开了，再画一条实色线是双重分隔，
           而且卡片从底下透上来时只有那条线不透明，看着最脏。
           写 border-bottom: none（而不是只改颜色）：深色主题那两条规则只覆盖
           border-bottom-color，宽度和样式仍归这里，等价于彻底不画。 */
        border-bottom: none;
    }

    /* 模糊和底色铺在 ::after 上、而不是直接给 .topbar 上 backdrop-filter：
       带 backdrop-filter 的元素会变成 fixed 后代的包含块。顶栏里那个
       .notify-dropdown.is-open 在窄屏正是 position: fixed + inset: 0 的遮罩
       （见 25-topbar-responsive.css），包含块一变成 56px 高的顶栏，遮罩就从
       「盖住整屏」缩成顶栏那一条。
       ::after 没有后代，z-index: -1 让它在顶栏内容（分类名 / 搜索药丸）之下、
       页面背景之上，模糊照样采到顶栏背后那层。 */
    html[data-tools-tabs-docked] .topbar::after {
        content: "";
        position: absolute;
        inset: 0;
        z-index: -1;
        background: color-mix(in srgb, var(--topbar-bg) 72%, transparent);
        /* Safari 要带前缀的那份，少写就是 iOS 上一块纯半透明板子（不模糊） */
        -webkit-backdrop-filter: blur(16px) saturate(1.6);
        backdrop-filter: blur(16px) saturate(1.6);
        /* 纯装饰层，别把顶栏里按钮的点按挡掉 */
        pointer-events: none;
    }
}

@media (prefers-reduced-motion: reduce) {
    html[data-tools-tabs-docked] .topbar-tools-recent-slot {
        transition: none;
    }
}

/* ========== 角色库弹窗（底部 Sheet，对齐素材库 / 套餐页） ========== */
#character-library-modal.modal.show,
#character-library-modal.modal.is-closing {
    align-items: flex-end;
    justify-content: center;
    z-index: 12100;
    padding: 0;
    margin: 0;
    animation: none;
    isolation: isolate;
}

#character-library-modal.modal.is-closing {
    display: flex !important;
    pointer-events: none;
}

#character-library-modal .modal-overlay {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: var(--modal-overlay-bg, rgba(15, 23, 42, 0.45));
    animation: character-library-sheet-fade-in 0.22s ease;
    cursor: pointer;
}

#character-library-modal.is-closing .modal-overlay {
    animation: character-library-sheet-fade-out 0.22s ease forwards;
}

#character-library-modal .character-library-modal-card {
    position: relative;
    z-index: 1;
    top: auto;
    right: auto;
    bottom: auto;
    left: auto;
    align-self: flex-end;
    transform: none;
    width: min(1080px, 100vw);
    height: min(90dvh, 920px);
    max-width: none;
    max-height: min(90dvh, 920px);
    margin: 0;
    border: 1px solid var(--border-mid, rgba(0, 0, 0, 0.08));
    border-bottom: none;
    border-radius: 22px 22px 0 0;
    box-shadow: 0 -16px 48px rgba(15, 23, 42, 0.18);
    padding: 0;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    text-align: left;
    animation: character-library-sheet-slide-in 0.34s cubic-bezier(0.32, 0.72, 0, 1);
}

#character-library-modal.is-closing .character-library-modal-card {
    animation: character-library-sheet-slide-out 0.28s cubic-bezier(0.32, 0.72, 0, 1) forwards;
}

@keyframes character-library-sheet-fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

@keyframes character-library-sheet-fade-out {
    from { opacity: 1; }
    to { opacity: 0; }
}

@keyframes character-library-sheet-slide-in {
    from { transform: translateY(100%); }
    to { transform: translateY(0); }
}

@keyframes character-library-sheet-slide-out {
    from { transform: translateY(0); }
    to { transform: translateY(100%); }
}

html.is-character-library-sheet-open {
    overflow: hidden !important;
    height: 100%;
}

html.is-character-library-sheet-open body {
    overflow: hidden !important;
    overscroll-behavior: none;
}

html.is-character-library-sheet-open .modal.show:not(#character-library-modal),
html.is-character-library-sheet-open .toast {
    z-index: 12250 !important;
}

@media (prefers-reduced-motion: reduce) {
    #character-library-modal.is-closing .modal-overlay,
    #character-library-modal.is-closing .character-library-modal-card {
        animation-duration: 0.01ms !important;
    }
}

.character-library-layout {
    flex: 1 1 auto;
    min-height: 0;
    display: grid;
    grid-template-columns: 220px minmax(0, 1fr);
    height: 100%;
}

/* 第 990 轮（需求）：「@aside 的背景色要跟随主题色」——
   原来这里是写死的浅灰 #f7f7f8 + 深色 #161618 一对，自定义主题色（面板底色）改了它不跟。
   现在按面板色往文字色偏一点点算，浅色/深色/自定义主题色三种情况都自动成立。 */
.character-library-sidebar {
    width: 220px;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 24px 12px 16px 20px;
    border-right: 1px solid var(--border-soft, rgba(0, 0, 0, 0.06));
    /* 第 991 轮（需求）：「@aside 这里背景色也要和主题色一致」——
       主题色 = `--bg-base`（＝弹窗卡片那层 92% 玻璃面），不是 --panel-surface-bg（设置面板底色）。 */
    background: var(--glass-surface-bg, color-mix(in srgb, var(--bg-base, #f8f7f7) 92%, transparent));
    box-sizing: border-box;
}

.character-library-sidebar-title {
    margin: 0;
    padding: 0 8px 8px;
    font-size: 18px;
    font-weight: 700;
    line-height: 1.3;
    color: var(--text-primary);
}

.character-library-new-category {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    flex: 0 0 auto;
    width: 100%;
    height: 40px;
    min-height: 40px;
    margin: 0 0 4px;
    padding: 0 12px;
    border: none;
    border-radius: 999px;
    font: inherit;
    font-size: 14px;
    font-weight: 600;
    text-align: left;
    color: var(--text-primary);
    background: color-mix(in srgb, var(--text-primary) 6%, transparent);
    cursor: pointer;
    box-sizing: border-box;
    transition: background 0.15s ease, color 0.15s ease;
}

.character-library-new-category:hover,
.character-library-new-category:focus-visible {
    background: color-mix(in srgb, var(--text-primary) 10%, transparent);
    outline: none;
}

.character-library-new-category-icon {
    display: inline-flex;
    width: 16px;
    height: 16px;
    flex-shrink: 0;
}

.character-library-new-category-icon svg {
    width: 100%;
    height: 100%;
}

.character-library-new-category-label {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.character-library-category-list {
    list-style: none;
    margin: 0;
    padding: 0;
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.character-library-category-item {
    margin: 0;
    display: flex;
    align-items: center;
    min-width: 0;
}

.character-library-category-btn {
    flex: 1 1 auto;
    min-width: 0;
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin: 0;
    height: 40px;
    min-height: 40px;
    padding: 0 8px 0 12px;
    border: none;
    border-radius: 999px;
    background: transparent;
    color: var(--text-secondary);
    font: inherit;
    font-size: 14px;
    font-weight: 500;
    text-align: left;
    cursor: pointer;
    box-shadow: none;
    box-sizing: border-box;
}

.character-library-category-btn:hover,
.character-library-category-btn:focus-visible,
.character-library-category-btn:focus-within {
    background: rgba(0, 0, 0, 0.04);
    color: var(--text-primary);
    outline: none;
}

.character-library-category-item.is-active .character-library-category-btn {
    background: rgba(0, 0, 0, 0.06);
    color: var(--text-primary);
    font-weight: 600;
}

[data-theme="dark"] .character-library-category-btn:hover,
[data-theme="dark"] .character-library-category-btn:focus-visible,
[data-theme="dark"] .character-library-category-btn:focus-within {
    background: rgba(255, 255, 255, 0.06);
}

[data-theme="dark"] .character-library-category-item.is-active .character-library-category-btn {
    background: rgba(255, 255, 255, 0.1);
    color: #f4f4f5;
}

@media (prefers-color-scheme: dark) {
    [data-theme="system"] .character-library-category-btn:hover,
    [data-theme="system"] .character-library-category-btn:focus-visible,
    [data-theme="system"] .character-library-category-btn:focus-within {
        background: rgba(255, 255, 255, 0.06);
    }

    [data-theme="system"] .character-library-category-item.is-active .character-library-category-btn {
        background: rgba(255, 255, 255, 0.1);
        color: #f4f4f5;
    }
}

.character-library-category-name {
    min-width: 0;
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.character-library-category-count {
    flex: 0 0 auto;
    min-width: 1.25em;
    margin-right: 2px;
    font-size: 12px;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    color: var(--text-tertiary, #8a8a8a);
    opacity: 0;
    transform: translateX(2px);
    transition: opacity 0.15s ease, transform 0.15s ease;
    pointer-events: none;
}

.character-library-category-btn:hover .character-library-category-count,
.character-library-category-btn:focus-visible .character-library-category-count,
.character-library-category-btn:focus-within .character-library-category-count {
    opacity: 1;
    transform: translateX(0);
}

.character-library-category-actions {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: 0;
    margin-left: 2px;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.15s ease;
}

.character-library-category-btn:hover .character-library-category-actions,
.character-library-category-btn:focus-within .character-library-category-actions {
    opacity: 1;
    pointer-events: auto;
}

.character-library-category-action-btn {
    width: 28px;
    height: 28px;
    padding: 0;
    border: none;
    border-radius: 8px;
    background: transparent;
    color: var(--text-tertiary);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
}

.character-library-category-action-btn svg {
    width: 14px;
    height: 14px;
}

.character-library-category-action-btn:hover,
.character-library-category-action-btn:focus-visible {
    background: var(--filter-tabs-tab-hover-bg, rgba(0, 0, 0, 0.06));
    color: var(--text-primary);
}

.character-library-category-action-btn--danger:hover,
.character-library-category-action-btn--danger:focus-visible {
    color: #ef4444;
}

.character-library-main {
    min-width: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
}

.character-library-head {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    min-height: 60px;
    padding: 10px 16px;
    box-sizing: border-box;
    border-bottom: 1px solid var(--border-soft);
}

.character-library-title {
    margin: 0;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 16px;
    font-weight: 600;
    color: var(--text-primary);
}

.character-library-head-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    flex: 0 0 auto;
}

.character-library-close,
#character-library-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    min-width: 32px;
    min-height: 32px;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: transparent;
    color: var(--text-tertiary, #8a8a8a);
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
}

.character-library-close:hover,
.character-library-close:focus-visible,
#character-library-close:hover,
#character-library-close:focus-visible {
    background: rgba(0, 0, 0, 0.06);
    color: var(--text-primary);
    outline: none;
}

[data-theme="dark"] .character-library-close:hover,
[data-theme="dark"] .character-library-close:focus-visible,
[data-theme="dark"] #character-library-close:hover,
[data-theme="dark"] #character-library-close:focus-visible {
    background: rgba(255, 255, 255, 0.08);
}

@media (prefers-color-scheme: dark) {
    [data-theme="system"] .character-library-close:hover,
    [data-theme="system"] .character-library-close:focus-visible,
    [data-theme="system"] #character-library-close:hover,
    [data-theme="system"] #character-library-close:focus-visible {
        background: rgba(255, 255, 255, 0.08);
    }
}

.character-library-close svg,
#character-library-close svg {
    width: 18px;
    height: 18px;
    display: block;
    flex: 0 0 auto;
}

.character-library-body {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    padding: 16px;
    box-sizing: border-box;
}

.character-library-grid-wrap {
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
}

.character-library-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(100px, 112px));
    gap: 12px 10px;
    justify-content: start;
}

/* 导入卡片：对齐项目首页「新建项目」 */
.character-library-import-card {
    display: flex;
    flex-direction: column;
    gap: 10px;
    min-width: 0;
    margin: 0;
    padding: 0;
    border: none;
    background: transparent;
    text-align: left;
    cursor: pointer;
    color: inherit;
    font: inherit;
}

.character-library-import-card:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

.character-library-import-thumb {
    position: relative;
    width: 100%;
    aspect-ratio: 9 / 16;
    border-radius: 12px;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg-elev-2, #e8e8e8);
}

.character-library-import-card:hover:not(:disabled) .character-library-import-thumb,
.character-library-import-card:focus-visible:not(:disabled) .character-library-import-thumb {
    outline: none;
}

.character-library-import-plus {
    width: 36px;
    height: 36px;
    color: #9a9a9a;
    transition: transform 0.18s ease, color 0.18s ease;
}

.character-library-import-plus svg {
    display: block;
    width: 100%;
    height: 100%;
}

.character-library-import-card:hover:not(:disabled) .character-library-import-plus,
.character-library-import-card:focus-visible:not(:disabled) .character-library-import-plus {
    color: #6b6b6b;
    transform: scale(1.12);
}

.character-library-import-meta {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
    padding: 0 2px;
}

.character-library-import-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 14px;
    font-weight: 500;
    line-height: 1.35;
    color: var(--text-primary, #1a1523);
}

[data-theme="dark"] .character-library-import-thumb {
    background: rgba(255, 255, 255, 0.08);
}

[data-theme="dark"] .character-library-import-plus {
    color: rgba(255, 255, 255, 0.45);
}

[data-theme="dark"] .character-library-import-card:hover:not(:disabled) .character-library-import-plus,
[data-theme="dark"] .character-library-import-card:focus-visible:not(:disabled) .character-library-import-plus {
    color: rgba(255, 255, 255, 0.72);
}

@media (prefers-color-scheme: dark) {
    [data-theme="system"] .character-library-import-thumb {
        background: rgba(255, 255, 255, 0.08);
    }

    [data-theme="system"] .character-library-import-plus {
        color: rgba(255, 255, 255, 0.45);
    }

    [data-theme="system"] .character-library-import-card:hover:not(:disabled) .character-library-import-plus,
    [data-theme="system"] .character-library-import-card:focus-visible:not(:disabled) .character-library-import-plus {
        color: rgba(255, 255, 255, 0.72);
    }
}

.character-library-item {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 10px;
    min-width: 0;
    margin: 0;
    padding: 0;
    border: none;
    background: transparent;
    text-align: left;
    cursor: pointer;
    color: inherit;
    font: inherit;
}

.character-library-item-thumb {
    position: relative;
    width: 100%;
    aspect-ratio: 9 / 16;
    border-radius: 12px;
    overflow: hidden;
    background: var(--bg-elev-2, #f5f5f5);
}

.character-library-item:hover .character-library-item-thumb,
.character-library-item:focus-visible .character-library-item-thumb {
    outline: none;
}

.character-library-item-thumb img,
.character-library-item-thumb video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: transform 0.18s ease;
}

.character-library-item:hover .character-library-item-thumb img,
.character-library-item:hover .character-library-item-thumb video,
.character-library-item:focus-visible .character-library-item-thumb img,
.character-library-item:focus-visible .character-library-item-thumb video {
    transform: scale(1.06);
}

.character-library-item-delete {
    position: absolute;
    top: 6px;
    right: 6px;
    width: 26px;
    height: 26px;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.55);
    color: #fff;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    cursor: pointer;
    transition: opacity 0.15s ease;
    z-index: 1;
}

.character-library-item:hover .character-library-item-delete,
.character-library-item:focus-within .character-library-item-delete {
    opacity: 1;
}

.character-library-item-delete svg {
    width: 13px;
    height: 13px;
}

.character-library-empty,
.character-library-select-hint {
    margin: 0;
    padding: 24px 12px;
    text-align: center;
    font-size: 13px;
    color: var(--text-tertiary);
}

.character-library-grid-wrap.is-uploading {
    opacity: 0.65;
    pointer-events: none;
}

.character-library-upload-input {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

@media (max-width: 900px) {
    #character-library-modal .character-library-modal-card {
        width: 100vw;
        height: min(92dvh, 100%);
        max-height: min(92dvh, 100%);
        border-radius: 18px 18px 0 0;
    }

    .character-library-layout {
        grid-template-columns: 1fr;
        grid-template-rows: minmax(160px, 34%) minmax(0, 1fr);
    }

    .character-library-sidebar {
        width: auto;
        border-right: none;
        border-bottom: 1px solid rgba(0, 0, 0, 0.06);
        padding: 16px 12px 12px;
    }

    [data-theme="dark"] .character-library-sidebar {
        border-bottom-color: rgba(255, 255, 255, 0.08);
    }

    .character-library-category-list {
        flex-direction: row;
        flex-wrap: nowrap;
        overflow-x: auto;
        overflow-y: hidden;
    }

    .character-library-category-item {
        flex: 0 0 auto;
    }

    .character-library-category-actions {
        display: none;
    }
}
/* ========== 视频混剪 ========== */
.video-remix-page {
    display: none;
    width: 100%;
    min-height: 0;
    background: var(--bg-base, #f4f5f7);
    box-sizing: border-box;
}

.video-remix-page.is-active {
    display: flex;
    flex-direction: column;
}

[data-theme="dark"] .video-remix-page {
    background: var(--bg-base, #101114);
}

.main:has(#panel-video-remix.is-active) {
    --topbar-h: 0px;
}

.main:has(#panel-video-remix.is-active) .topbar {
    display: none !important;
}

.video-remix-shell {
    /* 第 681 轮：标题栏摘出流后以它为基准（见下面那条媒体查询） */
    position: relative;
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
    width: 100%;
    max-width: 100%;
    padding: 16px 18px 12px;
    gap: 12px;
    box-sizing: border-box;
}

.video-remix-shell.is-drop-target {
    outline: 2px dashed color-mix(in srgb, var(--text-primary) 28%, transparent);
    outline-offset: -6px;
}

/* 第 680 轮（需求）：「@h1 @p 删除」——标题栏里的「视频混剪」和那句副标题都撤了，
   标题栏只剩动作条，于是靠右对齐（原来靠 space-between 被左边那块顶到右边）。 */
.video-remix-topbar {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 16px;
    flex: 0 0 auto;
}

/* 第 681 轮（需求）：「@aside @section 高度高一些，顶边要和 @header 的顶边贴合」
   「把 header 那 46px 也让出来给面板」。

   标题栏只剩「新建/导出」两颗按钮，却还占着一整行（34px + 12px 缝）。这里把它摘出流，
   钉在外壳内容区右上角（top/right 取外壳自己的 padding），于是：
     · 面板顶边 = 标题栏顶边 = 16px（原来的 62px 往上提 46px）；
     · 省下的 46px 全给 .video-remix-workspace（它本来就是 flex: 1 1 auto）；
     · 两颗按钮浮在「属性」卡右上角的空白上（那张卡右上角没有内容）。

   ⚠️ 只在 ≥1101px 生效：三栏档最右边才是「属性」卡，按钮浮在它上面不挡东西。
      ≤1100px 时「属性」跑到下面一行、最上面是素材库（右上角就是「导入」），
      ≤760px 更是单列（最上面还是素材库）—— 那两档继续让标题栏老实占着一行。
      这条规则写在文件末尾那条宽屏媒体查询里（别另起一条同名查询：
      老断言按「第一条宽屏查询」取内容，注释里连查询原文都别写，会被 indexOf 命中）。 */


.video-remix-topbar-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    justify-content: flex-end;
}

/* 第 680 轮（需求）：「@div 改成下拉框选择，然后移到 @section 右下角」——
   画幅从标题栏的分段控件改成下拉框，落到预览区（.video-remix-stage）的右下角。
   ⚠️ 基准是 .video-remix-stage（position: relative，见下面那条），
      别挂到 .video-remix-preview-frame 上 —— 预览框会跟着画幅改尺寸，下拉框会跟着跳。 */
.video-remix-ratio-select {
    position: absolute;
    right: 12px;
    bottom: 12px;
    z-index: 2;
    height: 28px;
    padding: 0 8px;
    border: 1px solid var(--border-soft);
    border-radius: 8px;
    font: inherit;
    font-size: 12px;
    font-weight: 600;
    color: var(--text-secondary);
    background: color-mix(in srgb, var(--text-primary) 6%, transparent);
    cursor: pointer;
}

.video-remix-chip,
.video-remix-btn,
.video-remix-icon-btn {
    border: none;
    cursor: pointer;
    font: inherit;
    color: var(--text-primary);
    background: transparent;
}

.video-remix-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 34px;
    padding: 0 14px;
    border-radius: 10px;
    font-size: 13px;
    font-weight: 600;
    background: color-mix(in srgb, var(--text-primary) 6%, transparent);
}

.video-remix-btn--compact {
    height: 28px;
    padding: 0 10px;
    font-size: 12px;
}

.video-remix-btn--primary {
    background: #111;
    color: #fff;
}

[data-theme="dark"] .video-remix-btn--primary {
    background: #f5f5f5;
    color: #111;
}

.video-remix-btn--ghost {
    background: transparent;
    border: 1px solid var(--border-soft);
}

/* 第 671 轮（需求）：「#video-remix-new / #video-remix-export 改成胶囊型」——
   只动标题栏这两颗：.video-remix-btn 是公用的（导入 / 分割 / 删除 / 加文字 / 缩放 ± 都在用），
   整族改圆角会把时间线和属性栏那几颗一并改掉。 */
#video-remix-new,
#video-remix-export {
    border-radius: 999px;
}

.video-remix-btn--danger {
    background: color-mix(in srgb, #e5484d 14%, transparent);
    color: #c62828;
}

.video-remix-btn:disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

.video-remix-workspace {
    display: grid;
    grid-template-columns: minmax(200px, 240px) minmax(0, 1fr) minmax(220px, 260px);
    gap: 12px;
    flex: 1 1 auto;
    min-height: 0;
}

.video-remix-bin,
.video-remix-stage,
.video-remix-inspector,
.video-remix-timeline {
    background: var(--bg-elev-1, #fff);
    border: 1px solid var(--border-soft);
    border-radius: 16px;
    box-sizing: border-box;
}

[data-theme="dark"] .video-remix-bin,
[data-theme="dark"] .video-remix-stage,
[data-theme="dark"] .video-remix-inspector,
[data-theme="dark"] .video-remix-timeline {
    background: #17181c;
}

.video-remix-bin,
.video-remix-inspector {
    display: flex;
    flex-direction: column;
    min-height: 0;
    overflow: hidden;
    padding: 12px;
}

.video-remix-bin-head,
.video-remix-inspector-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 10px;
}

.video-remix-bin-head strong,
.video-remix-inspector-head strong {
    font-size: 13px;
    font-weight: 700;
    color: var(--text-primary);
}

.video-remix-bin-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-bottom: 10px;
}

.video-remix-chip {
    height: 26px;
    padding: 0 10px;
    border-radius: 999px;
    font-size: 12px;
    color: var(--text-secondary);
    background: color-mix(in srgb, var(--text-primary) 5%, transparent);
}

.video-remix-chip.is-active {
    color: var(--text-primary);
    /* 第 671 轮（需求）：「@button 按钮选中背景色改成白色」——素材库筛选的选中态
       原来是 12% 墨，跟 5% 墨的常态拉不开；实白同上，跟画幅那颗同一口径。 */
    background: #fff;
}

/* 同上：深色主题不吃实白。 */
[data-theme="dark"] .video-remix-chip.is-active {
    background: #2a2b2f;
}

/* 属性栏里那句「时间轴为估算」 */
.video-remix-inspector-note {
    margin: 0;
    padding: 6px 8px;
    border-radius: 8px;
    font-size: 11px;
    color: var(--text-secondary);
    background: color-mix(in srgb, var(--text-primary) 6%, transparent);
}

/* 导出字幕的格式下拉（跟旁边那颗紧凑按钮一样高） */
.video-remix-subtitle-format {
    height: 28px;
    padding: 0 6px;
    border: 1px solid var(--border-soft);
    border-radius: 8px;
    font-size: 12px;
    font-family: inherit;
    color: var(--text-primary);
    background: transparent;
}

.video-remix-subtitle-format:disabled,
#video-remix-export-subtitles:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.video-remix-bin-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
    overflow: auto;
    min-height: 0;
    flex: 1 1 auto;
}

.video-remix-bin-empty {
    margin-top: 24px;
    padding: 0 8px;
    text-align: center;
    color: var(--text-tertiary);
    font-size: 12px;
    line-height: 1.5;
}

.video-remix-bin-card {
    position: relative;
    display: grid;
    /* 第 673 轮：右列从「一颗 +」变成「识别字幕 + 加入时间线 + 字幕状态」，所以右列交给
       .video-remix-bin-actions 自己排，卡片这边只留一列给它。 */
    grid-template-columns: 56px minmax(0, 1fr) auto;
    gap: 8px;
    align-items: center;
    padding: 8px;
    border-radius: 12px;
    background: color-mix(in srgb, var(--text-primary) 4%, transparent);
    cursor: grab;
}

.video-remix-bin-actions {
    display: flex;
    align-items: center;
    gap: 6px;
}

/* 「识别字幕」：常态低调，悬停卡片才浮出来 —— 素材多的时候不至于满屏按钮 */
.video-remix-bin-subtitle {
    height: 22px;
    padding: 0 8px;
    border: none;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 600;
    color: var(--text-secondary);
    background: color-mix(in srgb, var(--text-primary) 6%, transparent);
    cursor: pointer;
    white-space: nowrap;
}

.video-remix-bin-card:hover .video-remix-bin-subtitle,
.video-remix-bin-subtitle:focus-visible {
    color: var(--text-primary);
    background: color-mix(in srgb, var(--text-primary) 10%, transparent);
}

.video-remix-bin-subtitle:disabled {
    opacity: 0.7;
    cursor: progress;
}

/* 已识别的状态：缩略图角上那颗小标 */
.video-remix-bin-cues {
    position: absolute;
    top: 4px;
    left: 4px;
    padding: 1px 6px;
    border-radius: 999px;
    font-size: 10px;
    font-weight: 600;
    color: #fff;
    background: rgba(17, 17, 17, 0.72);
    pointer-events: none;
}

[data-theme="dark"] .video-remix-bin-cues {
    background: rgba(255, 255, 255, 0.82);
    color: #111;
}

.video-remix-bin-thumb {
    width: 56px;
    height: 40px;
    border-radius: 8px;
    overflow: hidden;
    background: #111;
    display: grid;
    place-items: center;
}

.video-remix-bin-thumb img,
.video-remix-bin-thumb video {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.video-remix-bin-audio-icon {
    color: #fff;
    font-size: 16px;
}

.video-remix-bin-meta {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.video-remix-bin-meta strong {
    font-size: 12px;
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.video-remix-bin-meta span {
    font-size: 11px;
    color: var(--text-tertiary);
}

.video-remix-bin-add {
    width: 28px;
    height: 28px;
    border: none;
    border-radius: 8px;
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
    color: var(--text-primary);
    font-size: 16px;
    cursor: pointer;
}

.video-remix-stage {
    /* 第 680 轮：底下的画幅下拉框以这里为基准（绝对定位到右下角） */
    position: relative;
    display: flex;
    flex-direction: column;
    min-width: 0;
    min-height: 0;
    padding: 12px;
    gap: 10px;
}

.video-remix-preview-frame {
    position: relative;
    flex: 1 1 auto;
    min-height: 220px;
    display: grid;
    place-items: center;
    border-radius: 12px;
    background: #0b0b0c;
    overflow: hidden;
}

/* 第 681 轮（需求）：「@canvas 要按 @select#video-remix-ratio 这里的比例显示，
   但是不要改变 @section 的高度」。

   原来是三条 `[data-ratio="…"] #video-remix-canvas` 各自给 aspect-ratio + height/width，
   而画布是网格里**在流**的孩子：百分比高度对着「由画布自己撑开」的网格区解析不了
   （CSS 里是循环依赖，按 auto 处理），于是 9:16 素材直接长成 716×1273，
   把 stage 顶到 1223px、整页滚起来、面板顶边钻到标题栏底下。

   改法：画布摘出流（绝对定位 + inset:0 + margin:auto），对着**有确定高度**的预览框
   解析 max-width / max-height。画布是替换元素，两条 max 约束会按内在比例收缩，
   于是「按画幅比例显示」和「不改变 stage 高度」同时成立，宽扁/竖屏素材都不会再溢出。
   ⚠️ 那三条 [data-ratio] 规则别加回来：它们就是溢出的源头。 */
#video-remix-canvas {
    position: absolute;
    inset: 0;
    margin: auto;
    display: block;
    width: auto;
    height: auto;
    max-width: 100%;
    max-height: 100%;
    background: #0b0b0c;
}

.video-remix-preview-empty {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    padding: 24px;
    text-align: center;
    color: rgba(255, 255, 255, 0.55);
    font-size: 13px;
    pointer-events: none;
}

.video-remix-preview-empty[hidden] {
    display: none !important;
}

.video-remix-transport {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
}

.video-remix-icon-btn {
    width: 36px;
    height: 36px;
    border-radius: 999px;
    display: grid;
    place-items: center;
    background: color-mix(in srgb, var(--text-primary) 6%, transparent);
}

.video-remix-icon-btn.is-primary {
    width: 44px;
    height: 44px;
    background: #111;
    color: #fff;
}

[data-theme="dark"] .video-remix-icon-btn.is-primary {
    background: #f3f3f3;
    color: #111;
}

.video-remix-icon-btn svg {
    width: 18px;
    height: 18px;
}

.video-remix-icon-btn .video-remix-icon-pause {
    display: none;
}

.video-remix-icon-btn.is-playing .video-remix-icon-play {
    display: none;
}

.video-remix-icon-btn.is-playing .video-remix-icon-pause {
    display: block;
}

.video-remix-icon-btn .video-remix-icon-play[hidden],
.video-remix-icon-btn .video-remix-icon-pause[hidden] {
    display: none !important;
}

.video-remix-time {
    min-width: 96px;
    font-variant-numeric: tabular-nums;
    font-size: 12px;
    color: var(--text-secondary);
}

.video-remix-inspector-empty {
    margin-top: 28px;
    text-align: center;
    color: var(--text-tertiary);
    font-size: 12px;
}

.video-remix-inspector-body {
    display: flex;
    flex-direction: column;
    gap: 10px;
    overflow: auto;
    min-height: 0;
}

.video-remix-field {
    display: flex;
    flex-direction: column;
    gap: 6px;
    font-size: 12px;
    color: var(--text-secondary);
}

/* ⚠️ `[hidden]` 只有 UA 的 `display: none`，压不过上面那条作者写的 `display: flex` ——
   不补这一句，属性栏里用 `row.hidden = true` 藏起来的字段（第 673 轮：选字幕 cue 时
   藏掉速度 / 音量 / 转场）其实一直挂在界面上。 */
.video-remix-field[hidden] {
    display: none;
}

.video-remix-field input,
.video-remix-field select,
.video-remix-field textarea {
    width: 100%;
    box-sizing: border-box;
    border: 1px solid var(--border-soft);
    border-radius: 10px;
    background: var(--bg-base, #f7f7f8);
    color: var(--text-primary);
    padding: 8px 10px;
    font: inherit;
}

[data-theme="dark"] .video-remix-field input,
[data-theme="dark"] .video-remix-field select,
[data-theme="dark"] .video-remix-field textarea {
    background: #101114;
}

.video-remix-field em {
    font-style: normal;
    color: var(--text-tertiary);
}

.video-remix-inspector-actions {
    display: flex;
    gap: 8px;
    margin-top: 4px;
}

.video-remix-timeline {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    min-height: 188px;
    max-height: 280px;
    overflow: hidden;
}

.video-remix-timeline-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 10px 12px 6px;
}

.video-remix-timeline-tools {
    display: flex;
    align-items: center;
    gap: 6px;
}

.video-remix-timeline-hint {
    font-size: 11px;
    color: var(--text-tertiary);
}

.video-remix-timeline-scroll {
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
    padding: 0 0 10px;
}

.video-remix-timeline-inner {
    position: relative;
    min-height: 100%;
    width: max-content;
    min-width: 100%;
}

.video-remix-ruler {
    position: relative;
    height: 22px;
    margin: 0 12px 6px;
    border-bottom: 1px solid var(--border-soft);
}

.video-remix-ruler-mark {
    position: absolute;
    top: 0;
    width: 1px;
    height: 100%;
    background: color-mix(in srgb, var(--text-primary) 16%, transparent);
}

.video-remix-ruler-mark i {
    position: absolute;
    top: 2px;
    left: 4px;
    font-style: normal;
    font-size: 10px;
    color: var(--text-tertiary);
    white-space: nowrap;
}

.video-remix-tracks {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 0 12px;
}

.video-remix-track {
    display: grid;
    grid-template-columns: 76px minmax(0, 1fr);
    gap: 8px;
    align-items: stretch;
    min-height: 42px;
}

.video-remix-track-label {
    display: flex;
    align-items: center;
    font-size: 12px;
    font-weight: 600;
    color: var(--text-secondary);
}

.video-remix-track-lane {
    position: relative;
    min-height: 42px;
    border-radius: 10px;
    background: color-mix(in srgb, var(--text-primary) 4%, transparent);
    border: 1px dashed transparent;
}

.video-remix-track-lane.is-drop-hover {
    border-color: color-mix(in srgb, var(--text-primary) 35%, transparent);
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
}

.video-remix-clip {
    position: absolute;
    top: 4px;
    bottom: 4px;
    display: flex;
    align-items: center;
    padding: 0 8px;
    border: none;
    border-radius: 8px;
    overflow: hidden;
    cursor: pointer;
    color: #fff;
    background: linear-gradient(180deg, #3a3f4a 0%, #2a2e36 100%);
    text-align: left;
}

.video-remix-track[data-track="audio"] .video-remix-clip {
    background: linear-gradient(180deg, #2f5d4a 0%, #214336 100%);
}

.video-remix-track[data-track="text"] .video-remix-clip {
    background: linear-gradient(180deg, #4a3f6b 0%, #342c4d 100%);
}

/* 字幕 cue（第 673 轮）：跟另外三条轨的观感区分开 —— 它们是「首尾相接的片段」，
   字幕是「落在一段时间上的小段」，所以更薄、圆角更大、底色更亮一点。 */
.video-remix-track[data-track="subtitle"] .video-remix-clip--cue {
    top: 8px;
    bottom: 8px;
    border-radius: 999px;
    padding: 0 10px;
    background: linear-gradient(180deg, #2b6cb0 0%, #1f4e7f 100%);
}

/* 时间轴是估算出来的（供应商没有时间轴）→ 虚线边，提醒核对 */
.video-remix-track[data-track="subtitle"] .video-remix-clip--cue.is-estimated {
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.55);
    background-image: repeating-linear-gradient(
        135deg,
        rgba(255, 255, 255, 0.14) 0 6px,
        rgba(255, 255, 255, 0) 6px 12px
    ),
    linear-gradient(180deg, #2b6cb0 0%, #1f4e7f 100%);
}

.video-remix-track[data-track="subtitle"] .video-remix-clip--cue.is-selected {
    box-shadow: inset 0 0 0 2px #fff;
}

.video-remix-clip span {
    font-size: 11px;
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.video-remix-clip.is-selected {
    box-shadow: inset 0 0 0 2px #fff;
}

.video-remix-playhead {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 14px;
    margin: 0;
    padding: 0;
    border: none;
    background: transparent;
    cursor: ew-resize;
    touch-action: none;
    pointer-events: auto;
    z-index: 8;
}

.video-remix-playhead::before {
    content: "";
    position: absolute;
    top: 0;
    left: 2px;
    width: 10px;
    height: 10px;
    border-radius: 2px;
    background: #ef4444;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
}

.video-remix-playhead::after {
    content: "";
    position: absolute;
    top: 8px;
    bottom: 0;
    left: 6px;
    width: 2px;
    background: #ef4444;
}

.video-remix-playhead.is-dragging {
    z-index: 12;
}

/* 第 669 轮（需求）：「@aside @section @aside#video-remix-inspector 高度要自适应」——
   宽屏三栏时三块的高度由**最高的那一块**（属性栏）说了算：选中片段后属性栏内容约 700px，
   `.video-remix-workspace` 又是「内容撑高」的（行高 auto、内容区高度也 auto），
   于是整行、整个混剪页跟着长到 1200+，视口只有 900 时整页滚 —— 时间线被顶到屏幕外，
   素材库 / 预览也跟着被拉高（明明用不满）。

   这里把高度收成「一屏以内」，再把三栏那一行变成占满余量的弹性行（`minmax(0, 1fr)`），
   让三块都跟着可用高度走：素材库列表、属性栏内容各自内部滚动（这两处本来就有
   `overflow: auto`），预览画面按比例缩放（预览框 `flex: 1 1 auto`）。窗口变高就一起变高。

   几点约束（改之前先看）：
     · `100dvh` 挂在 `.content`（#main-content）上、面板再吃 `height: 100%`：
       面板自己写 `calc(100dvh - …)` 会把内边距漏掉（内边距记在 .content 名下），
       和 36-tools-hub.css 那条 `#main-content:has(#panel-tools.is-active)` 同一套做法。
     · 每一层都要 `min-height: 0`，断一层就按内容撑高，下面永远超不过容器、也就永远滚不起来。
     · 行里给一个 300px 的地板：可用高度比这还小时（窗口特别矮）宁可整页滚，
       也不要把预览挤塌到盖住时间线。
     · 只管 ≥1101px 的三栏；≤1100px 那两档本来就是「属性栏封 220px + 内部滚动」，
       不跟着改（窄屏/手机上属性栏是独立一行，收一屏反而会把素材库撑满）。 */
@media (min-width: 1101px) {
    /* ⚠️ `:not([data-photo-edit-page])`：停靠修图页时底层面板的 `is-active` 还在
       （那边靠 display: none 盖住，见 39-photo-edit.css），这时不能再去改内容区高度 ——
       修图页的面板是 `height: 100%`，父盒子一旦从 auto 变成定值，它就不再按内容算了。
       底部内边距也一并让出去（上内边距是第 670 轮让出去的，见 06-content.css）：
       编辑器的「一屏」就是 顶边留白（外壳自己的 16px）→ 时间线，
       留着那 80px 会在时间线下面空一条，矮一点的窗口反而又把时间线挤到屏幕外
       （外壳自己还有 12px 下内边距，贴着视口底边的是它）。 */
    html:not([data-photo-edit-page]) #main-content:has(#panel-video-remix.is-active) {
        height: 100dvh;
        min-height: 0;
        padding-bottom: 0;
    }

    html:not([data-photo-edit-page]) #main-content:has(#panel-video-remix.is-active) > #panel-video-remix {
        height: 100%;
    }

    .video-remix-workspace {
        grid-template-rows: minmax(0, 1fr);
        min-height: 300px;
    }

    /* 第 681 轮（需求）：「@aside @section 顶边要和 @header 的顶边贴合」——
       标题栏只剩两颗按钮，摘出流钉在外壳内容区右上角（top/right = 外壳自己的内边距），
       面板顶边于是从 62px 提到 16px，省下的 34px + 12px 缝全归 .video-remix-workspace。
       按钮浮在「属性」卡右上角的空白上（那张卡右上角没有内容）。 */
    .video-remix-topbar {
        position: absolute;
        top: 16px;
        right: 18px;
        z-index: 3;
    }
}

@media (max-width: 1100px) {
    .video-remix-workspace {
        grid-template-columns: minmax(180px, 220px) minmax(0, 1fr);
        grid-template-rows: minmax(240px, 1fr) auto;
    }

    .video-remix-inspector {
        grid-column: 1 / -1;
        max-height: 220px;
    }
}

@media (max-width: 760px) {
    .video-remix-shell {
        padding: 12px;
    }

    .video-remix-workspace {
        grid-template-columns: 1fr;
    }

    .video-remix-bin,
    .video-remix-inspector {
        max-height: 220px;
    }

    .video-remix-timeline-hint {
        display: none;
    }
}
/* 第 952 轮（需求）：
   「全站的菜单界面和弹层界面 背景色 / 透明效果 / 双边框等等样式都要和
     #gen-settings-drawer 一致」

   抽屉那套规格（token 见 01-reset-tokens 的 --glass-*，本体见 10-prompt-desktop-dock 的 .gen-drawer）：
     · 底色 color-mix(--bg-base 92%, transparent)
     · 磨砂 blur(10px) saturate(1.5)
     · 边框：外圈 1px --gen-drawer-border（浅色纯白 / 深色 12% 白）；第 953 轮撤掉内侧线
     · 浮层投影 --glass-drop（抽屉贴在创作栏上不用投影，菜单 / 弹窗要压住内容）

   这份文件排在 manifest 最后，统一用 !important 落地。各组件原来的底色 / 描边 / 投影写法
   五花八门，而且首页创作栏那批还带着 `html[...] :is(#panel-home .prompt-box …)` 这种
   带 id 的 !important 规则（权重 1,4,x）—— 单靠层叠顺序压不动，所以这里用两枚不存在的
   id（:not(#glass-a):not(#glass-b)）把权重顶到 2,x,x，才真正「全站一个样」。

   :not(.gen-drawer-panel-host *) 把抽屉内部那块排掉：它是从下拉面板搬进来的同一份 DOM，
   玻璃面交给抽屉本体，自己保持透明（否则会套出第二层底、变成三层边框）。

   第 1744 轮（需求）：「@div#image-preview-ai-popover 的背景和透明效果要和
   @div.image-preview-extra-row.is-card（参考线样式那块卡片）一致」——
   停靠进修图工具列的那一份（`#image-preview-studio-tools …`）也排掉：它已经不是浮层，
   而是那一列参数卡片里的一块，底色由 39-photo-edit.css 按卡片那份写。浮层态照旧走玻璃面。 */

/* ── ① 浮层菜单 ─────────────────────────────────────────────────── */
html body :is(
        .chip-dropdown-panel,
        .chip-dropdown-menu,
        .settings-dropdown-menu,
        .settings-lang-menu,
        .composer-translate-menu,
        .composer-batch-add-menu,
        .quick-prompts-panel,
        .quick-tag-menu,
        .mention-popover,
        .gen-settings-select-menu,
        .gen-drawer-mode-menu,
        .action-confirm-popover-panel,
        .ai-create-tool-guide-pop,
        .mode-dropdown-panel,
        .cloud-storage-filter-panel,
        .image-preview-body-size-menu,
        .image-preview-zoom-speed-menu,
        .image-preview-ai-popover,
        .user-menu-main-card,
        .user-menu-surface-card,
        /* 第 954 轮（需求）：「@button#home-gen-strip-btn（生成记录）的菜单界面和
           个人中心界面不要阴影效果」—— 这两块上一轮漏在清单外，各自还挂着
           0 12px 32px 那两层投影（底色也只有 80%）。一起并进来：
           跟抽屉同一份 92% 底 + 一圈白边 + 无投影。 */
        #home-gen-strip-menu,
        #user-menu,
        /* 第 1397 轮：缩略图右键那颗也走同一份玻璃面（需求「毛玻璃透明度一致」） */
        #home-gen-strip-context-menu
    ):not(#glass-a):not(#glass-b):not(.gen-drawer-panel-host *):not(.image-preview-studio-tools .image-preview-ai-popover) {
    /* ⚠️ 第 1802 轮（回归修复）：这里**不能**再挂 `:not(#quick-tag-menu)`。
       第 1678 轮为了把那颗菜单排出去加了一个 id 级 `:not()`，权重从 (3,4,2) 涨到 (4,4,2) ——
       而「个人中心只留一层玻璃」那条（本文件 48-glass 末尾，`html body #user-menu :is(...)`
       挂三枚假 id = (4,1,2)）和「那三屏分类不要边框」那条（同样 (4,1,2)）靠的就是
       id 数压这条一头。id 一旦打平、这条的类选择器更多，它反而赢了：
       个人中心里每张卡又各自糊一层 54% 玻璃 + 白边，叠出来几乎不透明，
       毛玻璃全没了。第 1680 轮那颗菜单已经改回半透玻璃面（跟 #home-goals-menu 一致，
       见 11-quick-templates.css），本来也不需要排出去。 */
    background: var(--glass-surface-bg, color-mix(in srgb, var(--bg-base, #f8f7f7) 92%, transparent)) !important;
    border: var(--gen-drawer-border-w, 1px) solid var(--gen-drawer-border, #fff) !important;
    box-shadow: none !important;
    -webkit-backdrop-filter: var(--glass-filter, blur(10px) saturate(1.5)) !important;
    background-image: none !important;
    backdrop-filter: var(--glass-filter, blur(10px) saturate(1.5)) !important;
}

/* ── ② 弹层 / 弹窗 ───────────────────────────────────────────────── */
/* 第 979 轮（需求）：「@div#image-preview-body-frame 除了 @img#image-preview-img 其他位置不要显示背景色」——
   聊天里点开大图走的是「纯查看器」（`.is-plain-viewer`），那时候整块 `.image-preview-dialog`
   不该再铺这层 92% 玻璃底 + 白边 + 磨砂：铺了就是图片外面套一张灰卡片。
   修图 / 模板那些正经弹层照旧（它们要有画布表面）。 */
/* 第 1210 轮（需求）：「弹出的通知背景色为白色，圆角为 32px」——
   这里把通知弹窗那颗 `.notify-popup-card` 排掉：它是**固定白底**，不跟玻璃面走
   （白底 + 32px 圆角 + 自己的投影写在 05-notifications.css 里）。 */
html body :is(.modal-card, .link-parse-consent-card, .mobile-prompt-sheet-card):not(#glass-a):not(#glass-b):not(.notify-popup-card),
html body .image-preview-modal:not(.is-plain-viewer) > .image-preview-dialog:not(#glass-a):not(#glass-b) {
    background: var(--glass-surface-bg, color-mix(in srgb, var(--bg-base, #f8f7f7) 92%, transparent)) !important;
    border: var(--gen-drawer-border-w, 1px) solid var(--gen-drawer-border, #fff) !important;
    box-shadow: none !important;
    -webkit-backdrop-filter: var(--glass-filter, blur(10px) saturate(1.5)) !important;
    backdrop-filter: var(--glass-filter, blur(10px) saturate(1.5)) !important;
}

/* 第 1622 轮（需求）：「@div（.modal-card.modal-card-form.image-set-module-card，编辑模块那张）
   背景色改成白色，界面以外为毛玻璃遮罩，账号登录界面背景色也要白色」——
   弹层卡不再跟菜单一起铺半透明玻璃面，改成**不透明的白色卡片**（深色主题用它自己的
   面板色 #1e1e1e，不硬写白）；「毛玻璃」那半句交给外面那层遮罩去做
   （`.modal-overlay` 在 18-modals.css 里加了 backdrop-filter，见 --modal-overlay-filter）。
   登录卡（#login-modal .modal-card.login-card.auth-card）本来就是 `.modal-card`，这一条一起管到。
   ⚠️ 权重：上面那条是 (2,3,2)，这里三个 `:not(#…)` 把 id 数顶到 3 → (3,2,2)，
      才能同时压过它和 1017 轮那条给充值卡单独铺玻璃底的 (3,1,2)。
   ⚠️ `.notify-popup-card` 照第 1210 轮排掉（通知弹窗是固定白底 + 32px 圆角，自己那套）。 */
html body .modal-card:not(.notify-popup-card):not(#glass-a):not(#glass-b):not(#glass-c) {
    background: var(--modal-card-bg, #fff) !important;
}

/* 第 1017 轮（需求）：「@div（div.modal-card.login-card.points-redeem-card，兑换码充值那张）
   界面不要添加边框」——
   上面那条给所有 `.modal-card` 挂了 1px 白边；充值是贴右边整高的抽屉，
   描边只会在屏幕边缘留一条亮线。
   ⚠️ 权重：上面那条带 `:not(#glass-a):not(#glass-b)`，两个 :not 各算一个 id —— 它是 (2,3,2)。
   只写 `#points-redeem-modal .points-redeem-card` 只有 1 个 id，压不住。
   这里照它的形状写一份（`:is()` 里带 id、外面同样两个 :not），拿到 (3,2,2) 才真的能覆盖。 */
html body :is(#points-redeem-modal .points-redeem-card):not(#glass-a):not(#glass-b) {
    border: none !important;
}

/* 第 1017 轮（需求）：「消息界面和积分充值界面打开之后界面以外的地方要使用毛玻璃效果，
   可以参考资产库界面」——
   资产库那颗遮罩是 blur(14px) saturate(1.25) + 36% 深色底（见 24-asset-library.css），
   这两处照抄：底色也从原来那层更实的 --modal-overlay-bg 降到 36%（深色 52%），
   不然糊了也看不出来。 */
html body .notify-sheet-overlay,
html body #points-redeem-modal .modal-overlay {
    -webkit-backdrop-filter: blur(14px) saturate(1.25);
    backdrop-filter: blur(14px) saturate(1.25);
    background: color-mix(in srgb, #0f172a 36%, transparent);
}

[data-theme="dark"] body .notify-sheet-overlay,
[data-theme="dark"] body #points-redeem-modal .modal-overlay {
    background: color-mix(in srgb, #000 52%, transparent);
}

@media (prefers-color-scheme: dark) {
    [data-theme="system"] body .notify-sheet-overlay,
    [data-theme="system"] body #points-redeem-modal .modal-overlay {
        background: color-mix(in srgb, #000 52%, transparent);
    }
}

/* 消息中心和积分充值卡也是全站弹层表面，跟随用户设置的颜色与透明度。 */
html body #panel-notify .notify-sheet-card,
html body :is(#points-redeem-modal .points-redeem-card):not(#glass-a):not(#glass-b) {
    background: var(--glass-surface-bg, #1e1e1e) !important;
}

/* 第 1260 轮（需求）：「@div（div.modal-card.hero-image-text-card，图片编辑弹层）界面以外为毛玻璃效果」——
   遮罩原来只是 `.modal-overlay` 那层 30% 的深色底（--modal-overlay-bg），糊不出东西。
   照资产库 / 消息界面那一套来：blur(14px) saturate(1.25) + 36% 深色底（深色主题 52%）。
   ⚠️ 底色必须比 --modal-overlay-bg 淡一档，否则糊了也看不出来。 */
html body #hero-image-text-modal .modal-overlay {
    -webkit-backdrop-filter: blur(14px) saturate(1.25);
    backdrop-filter: blur(14px) saturate(1.25);
    background: color-mix(in srgb, #0f172a 36%, transparent);
}

[data-theme="dark"] body #hero-image-text-modal .modal-overlay {
    background: color-mix(in srgb, #000 52%, transparent);
}

/* 第 1367 轮：视频重生成弹层（选段 + 首尾帧）的遮罩照图片编辑弹层那一套 ——
   同一个弹层家族，别一个毛玻璃一个没有。 */
html body #hero-video-clip-modal .modal-overlay {
    -webkit-backdrop-filter: blur(14px) saturate(1.25);
    backdrop-filter: blur(14px) saturate(1.25);
    background: color-mix(in srgb, #0f172a 36%, transparent);
}

[data-theme="dark"] body #hero-video-clip-modal .modal-overlay {
    background: color-mix(in srgb, #000 52%, transparent);
}

@media (prefers-color-scheme: dark) {
    [data-theme="system"] body #hero-video-clip-modal .modal-overlay {
        background: color-mix(in srgb, #000 52%, transparent);
    }
}

@media (prefers-color-scheme: dark) {
    [data-theme="system"] body #hero-image-text-modal .modal-overlay {
        background: color-mix(in srgb, #000 52%, transparent);
    }
}

/* 第 1275 轮（需求）：「@div（div.modal-card.stack-quick-gen-card）界面以外的地方毛玻璃效果」——
   快捷生成 / 抠图弹层的遮罩原来只有 `.modal-overlay` 那层 30% 深色底，糊不出东西。
   照图片编辑弹层（第 1260 轮）那一套来：blur(14px) saturate(1.25)。
   浅色主题的底色跟 --modal-overlay-bg 一个量级（30%），只是多了糊 —— 卡片是白底，
   底色再淡就压不住了；深色主题照旧走 52% 那档。 */
html body #stack-quick-gen-modal .modal-overlay {
    -webkit-backdrop-filter: blur(14px) saturate(1.25);
    backdrop-filter: blur(14px) saturate(1.25);
    background: color-mix(in srgb, #0f172a 30%, transparent);
}

[data-theme="dark"] body #stack-quick-gen-modal .modal-overlay {
    background: color-mix(in srgb, #000 52%, transparent);
}

@media (prefers-color-scheme: dark) {
    [data-theme="system"] body #stack-quick-gen-modal .modal-overlay {
        background: color-mix(in srgb, #000 52%, transparent);
    }
}

/* 添加预设弹窗以外的页面区域沿用全站弹层的毛玻璃遮罩。 */
html body #quick-prompt-modal .modal-overlay {
    -webkit-backdrop-filter: blur(14px) saturate(1.25);
    backdrop-filter: blur(14px) saturate(1.25);
    background: color-mix(in srgb, #0f172a 36%, transparent);
}

[data-theme="dark"] body #quick-prompt-modal .modal-overlay {
    background: color-mix(in srgb, #000 52%, transparent);
}

@media (prefers-color-scheme: dark) {
    [data-theme="system"] body #quick-prompt-modal .modal-overlay {
        background: color-mix(in srgb, #000 52%, transparent);
    }
}

/* 第 954 轮：生成记录的右键 / 悬停菜单与个人中心面板——圆角沿用 24px，
   投影一律撤掉（上面那条已经给 box-shadow: none，这里只是把圆角钉住，
   免得被各自文件里的旧声明拉开）。 */
html body :is(#home-gen-strip-menu, #user-menu):not(#glass-a):not(#glass-b) {
    border-radius: 24px;
}

/* ── ③ 只接管「描边颜色」的那一拨浮层 ───────────────────────────────
   第 1034 轮（需求）：「全部弹层和菜单界面的边框颜色都要和首页创作栏的颜色一致」。

   ① / ② 两份清单里的组件整圈都读 `--gen-drawer-border`，而那个 token 本轮已经等于
   创作栏底色（见 01-reset-tokens），它们自动跟上、这里不用再列一遍。
   剩下这批弹层各自写着 `--border-soft / --border-mid` 之类的描边，底色 / 投影 / 磨砂
   都是各自调过的 —— 所以这里**只接管颜色**，其余一个字不动（写 background 会把
   通知中心、AI 创作那些自定义面板一起改掉）。

   ⚠️ 权重照抄 ①②：两枚不存在的 id 把权重顶到 (2,x,x)，才压得住带 id 的 !important 老规则。 */
html body :is(
        /* 顶栏 / 通知 */
        #topbar-primary-nav-menu,
        #topbar-primary-nav-menu .topbar-logo-submenu,
        .notify-popover,
        /* 个人中心里的下拉 */
        .settings-popover,
        .settings-lang-menu,
        /* AI 工具 / AI 创作那一批 */
        .ai-tool-select-menu,
        .ai-create-queue-menu,
        .ai-create-choice-popover,
        .ai-create-menu,
        #ai-create-settings-menu,
        #ai-create-director-voice-menu,
        .ai-create-fixed-param-menu,
        .ai-create-thread-menu,
        #ai-create-model-menu,
        #ai-create-drafts-popover,
        /* 素材库 / 聊天里的右键菜单（第 405 轮那组里没列进来的两支） */
        .materials-context-menu-submenu,
        /* 首页对话里的选择气泡 */
        .hero-chat-choice-popover,
        /* 侧栏底部「⋯」那颗按钮弹出的菜单 */
        .app-sidebar-more-menu
    ):not(#glass-a):not(#glass-b) {
    border-color: var(--gen-drawer-border, #fff) !important;
}

/* 下拉菜单也跟随「全站弹层外观」的 --glass-surface-bg / --glass-filter；
   外层菜单壳负责底色和磨砂，内部面板透明，避免叠出两层不透明表面。 */

html body .chip-dropdown-panel:not(#glass-a):not(#glass-b):not(#glass-c) {
    background: transparent !important;
    background-image: none !important;
    box-shadow: none !important;
}

/* 类型 / 模型菜单的外壳也吃上面的全站弹层 token；撤掉组件自带投影。
   桌面端菜单会停靠到 body，第二个选择器用 :has() 识别停靠后的菜单节点。 */
html body :is(.chip-dropdown-mode, .chip-dropdown-model) > .chip-dropdown-menu:not(#glass-a):not(#glass-b):not(#glass-c),
html body > .chip-dropdown-menu.is-desktop-prompt-dropdown-fixed:not(#glass-a):not(#glass-b):not(#glass-c):has(> .chip-dropdown-panel-mode, > .chip-dropdown-panel-model) {
    box-shadow: none !important;
}

/* 菜单外壳承担唯一一层全站玻璃表面；里层不能再叠磨砂或描边。 */
html body :is(.chip-dropdown-mode, .chip-dropdown-model) :is(.chip-dropdown-panel, .chip-dropdown-options-mode, .chip-dropdown-options-model):not(#glass-a):not(#glass-b):not(#glass-c),
html body > .chip-dropdown-menu.is-desktop-prompt-dropdown-fixed:has(> .chip-dropdown-panel-mode, > .chip-dropdown-panel-model) :is(.chip-dropdown-panel, .chip-dropdown-options-mode, .chip-dropdown-options-model):not(#glass-a):not(#glass-b):not(#glass-c) {
    background: transparent !important;
    background-image: none !important;
    border: none !important;
    -webkit-backdrop-filter: none !important;
    backdrop-filter: none !important;
}

/* ── 第 1269 轮（需求）：「个人中心界面的透明度和毛玻璃效果要和 #gen-settings-drawer 一致」──
   ① 那份浮面清单把 `.user-menu-main-card` / `.user-menu-surface-card` 也收了进去，
   于是个人中心里**每一张卡片**都在 `#user-menu` 那层 92% 玻璃之上再糊一层 92% 玻璃、
   再磨一次砂：叠出来约 99.4% 不透明，底下的画面一点也透不上来 —— 跟抽屉「只有一层玻璃、
   能看到背后虚化的界面」完全不是同一个观感。
   这里把内层卡片退回透明（白边 / 圆角 / 圆角内的排版都不动），个人中心只剩 `#user-menu`
   自己那一层玻璃，底色 / 磨砂 / 白边逐项等于抽屉。
   ⚠️ 权重：① 是 (3,1,3)（两枚假 id + `:is()` 里那颗 id）。而且 styles-pages.css / styles-mobile.css
      里各有一份 ① 的拷贝、且都排在 styles.css 之后，单靠「写在后面」赢不了 —— 这里挂**三枚**
      假 id 把这条顶到 (4,1,2)，哪张表里都压得住。 */
html body #user-menu :is(.user-menu-main-card, .user-menu-surface-card):not(#glass-a):not(#glass-b):not(#glass-c) {
    background: transparent !important;
    background-image: none !important;
    -webkit-backdrop-filter: none !important;
    backdrop-filter: none !important;
}

/* ── 第 1769 轮（需求）：「#user-menu（修改账号 / 账单查询 / 意见反馈这 3 个界面）里面的
   分类不要显示边框」──
   这三屏里的每一块「分类」都是 `.user-menu-surface-card`，而 ① 那份浮面清单给它们挂了
   1px 白边（`--gen-drawer-border`）—— 跟 `#user-menu` 自己的外圈同色，等于卡片外面又描
   了一道，叠在上一条「只留一层玻璃」的透明卡片上尤其显眼。这里把这三屏里的卡撤掉描边。
   ⚠️ 权重：① 那份清单实际是 (3,4,2) —— 除了两枚假 id，它后面还挂着
      `:not(.gen-drawer-panel-host *)` / `:not(.image-preview-studio-tools .image-preview-ai-popover)`
      两组类选择器；只凑到三枚 id 只是打平、类数不够仍然输（实测过）。
      这里挂**三枚**假 id，拿到 (4,1,2) 才压得住 ①，也压得住手机端那条没有 !important 的 `border: none`。
   输入框自己的 1px 细描边不在这条里，照旧。 */
html body :is(#user-menu-pane-account, #user-menu-pane-billing, #user-menu-pane-feedback) .user-menu-surface-card:not(#glass-a):not(#glass-b):not(#glass-c) {
    border: none !important;
}

/* 快捷生成卡片也跟随偏好设置里的全站玻璃面颜色与透明度。 */
html body :is(#stack-quick-gen-modal .stack-quick-gen-card):not(#glass-a):not(#glass-b):not(#glass-c) {
    background: var(--glass-surface-bg, #1e1e1e) !important;
    -webkit-backdrop-filter: var(--glass-filter, blur(10px) saturate(1.5)) !important;
    backdrop-filter: var(--glass-filter, blur(10px) saturate(1.5)) !important;
}

/* 第 1766 轮（需求）：「@div（添加预设）和 @div（编辑模块）界面也要改」——
   把上面那条玻璃覆盖扩到这两张（连同第 1765 轮的侵权举报），并统一成商品白底图那档
   24px 圆角 / 无阴影。⚠️ 不并进来是不行的：`html body .modal-card:not(...){background:
   var(--modal-card-bg,#fff)!important}` 那条会把玻璃底压回纯白，只写在各自文件里赢不了。 */
/* 第 1777 轮（需求）：「@div（.gen-anim-preview-card，动画预览那张）界面背景色 / 毛玻璃透明度
   要和 @div（商品白底图）一致」—— 同一档玻璃面，跟着并进这份清单。 */
html body :is(
        #stack-quick-gen-modal .stack-quick-gen-card,
        #quick-prompt-modal .modal-card-form,
        #image-set-module-modal .image-set-module-card,
        #infringement-report-modal .infringement-report-panel,
        #gen-animation-preview-modal .gen-anim-preview-card
    ):not(#glass-a):not(#glass-b):not(#glass-c) {
    background: var(--glass-surface-bg, #1e1e1e) !important;
    -webkit-backdrop-filter: var(--glass-filter, blur(10px) saturate(1.5)) !important;
    backdrop-filter: var(--glass-filter, blur(10px) saturate(1.5)) !important;
    border-radius: 24px;
    box-shadow: none !important;
}
