html, body {
    /* ⚠ **中文字体必须排在泛型 `sans-serif` 之前**（2026-09-21 实打定位的坑）：
       只写 `… Arial, sans-serif` 时，页面在屏幕上**看着正常**，但**打印成 PDF 时所有汉字会消失**
       （实测：文本层只剩拉丁字符，嵌入字体只有 ArialMT）——因为泛型族一旦出现在中文字体之前，
       Chromium 打印路径就不再为 CJK 做字体回退，而屏幕上用的是可视化回退、不受影响。
       这个 bug 只在"打印/导出 PDF"时暴露，屏幕上永远看不出来，故在此写明：
       **新增/修改字体栈时，中文字体一定要写在 `sans-serif` / `serif` 之前。**
       验证方式：Microsoft Print to PDF 打一份含汉字最多的单据（工单卡），
       在 PDF 里搜任意一个汉字（能搜到 = 汉字还在；搜不到 = 字体栈又坏了）。
       ⚠ 此前这里指向 `node tools/print-harness.js`，但**该文件在本仓不存在**
       （`tools/` 下只有 migration 脚本）⇒ 那条"验证方式"是死引用，已换成上面这条可执行的人工步骤。 */
    font-family: 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', 'PingFang SC', sans-serif;
}

a, .btn-link {
    color: #006bb7;
}

.btn-primary {
    color: #fff;
    background-color: #1b6ec2;
    border-color: #1861ac;
}

.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
    box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}

.btn-group {
    display: flex;
    align-items: center;
}

/* ── 行内操作按钮组：**中文按钮不得被压成竖排** ──
   Bootstrap 的 .table 是 width:100% + auto 版式，表格宽裕时多出来的宽度会按"列宽比例"分给
   **内容稀疏的列**（实测：采购单号列 291px、日期列各 251px，而操作列被自己的 width 上限卡在 170px）。
   操作列拿到的是**最小值那一档**，于是每个 .btn 被压到 40px 左右——中文没有词内断行规则，
   浏览器就一字一行排下去：「打印送货标签」渲染成竖排四行，比整行还高，看着像坏了。
   这里的两个 nowrap 是**契约**（不是修饰）：
     · .btn 上的 nowrap → 按钮宽度回到"图标 + 文字"的首选宽度，绝不再逐字折行；
     · 容器上的 nowrap → 按钮之间不折行（即使将来列宽又被改窄，宁可撑破列宽也不要竖排）。
   配合页面里操作列**给足宽度**（SupplierPortal.razor 的打印操作列）一起生效：
   宽度不够时表格应当横向滚动，而不是把字竖起来。 */
.btn-group-nowrap {
    flex-wrap: nowrap;
}

.btn-group-nowrap > .btn {
    white-space: nowrap;
}

.content {
    padding-top: 1.1rem;
}

h1:focus {
    outline: none;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid #e50000;
}

.validation-message {
    color: #e50000;
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI26My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA9NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjOzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDg2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

    .blazor-error-boundary::after {
        content: "An error has occurred."
    }

.darker-border-checkbox.form-check-input {
    border-color: #929292;
}

.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
    color: var(--bs-secondary-color);
    text-align: end;
}

.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
    text-align: start;
}

/* Shared iframe style for report pages */
iframe.report-frame {
    width: 100%;
    height: 90vh;
    border: 0;
    margin: 0;
    padding: 0;
    display: block;
}

/* Shared iframe style for report pages */
iframe.report-frame-full {
    width: 100%;
    height: 100vh;
    border: 0;
    margin: 0;
    padding: 0;
    display: block;
}

/* Remove borders/lines around QuickGrid paginator and set vertical spacing */
.card-footer .paginator {
    margin: 0 !important;
    padding: 0 !important;
    border-top: none !important;
}

.card-header .card-title {
    margin: 0;
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

/* QuickGrid */
.table:has(td[b-vznuo5fmpv]) > tbody > tr:not(:has(td:not(:empty))) {
    display: none;
}


/* Remove padding inside header wrapper elements so header labels align with cells */
table.quickgrid.table thead th .col-header-content,
table.quickgrid.table thead th .col-title,
table.quickgrid.table thead th .col-title-text {
    padding: 0 !important;
    margin: 0 !important;
}

#stationDropdownContainer {
    top: 35%;
    left: 1%;
}

#stationDropdown {
    background-color: rgba(15, 23, 42, 0.8); /* 更深、略带蓝色的半透明背景 */
    color: aliceblue; /* 使用柔和的白色 (slate-200) */
    border: none;
}

/* Carousel controls shared styles */
.carousel-controls {
    position: fixed; /* 或 absolute，取决于父容器 */
    top: 5rem;
    right: 1rem; /* 靠右放置；如居中使用 start-50 translate-middle-x 时可移除此行 */
    /* width 由 Blazor 根据分组数量动态通过 inline style 设置 */
    height: 35px; /* 整体高度 */
    padding: 8px 12px; /* 内边距（适配内部元素） */
    display: flex;
    align-items: center;
    gap: 8px;
    z-index: 1001;
    background: rgba(0, 0, 0, 0.15);
    border-radius: 12px;
    backdrop-filter: blur(10px);
    border: 1px solid rgba(255, 255, 255, 0.1);
}

    /* 若使用居中类 translate-middle-x，禁用靠右定位以避免冲突 */
    .carousel-controls.translate-middle-x {
        right: auto;
    }

/* Channel 圆形按钮 */
.carousel-channel-btn {
    width: 25px;
    height: 25px;
    border-radius: 50%;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: rgba(255, 255, 255, 0.8);
    border: 1px solid rgba(255, 255, 255, 0.3);
    background-color: transparent;
    font-size: 13px;
    font-weight: 500;
    transition: all 0.3s ease;
}

/* 生产信息 / 产量统计 切换开关 */
.carousel-show-btn {
    min-width: 25px;
    height: 22px;
    border-radius: 10%;
    padding: 0 8px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 2px;
    color: rgba(255, 255, 255, 0.8);
    border: 1px solid rgba(255, 255, 255, 0.3);
    background-color: transparent;
    font-size: 12px;
    line-height: 1;
    font-weight: 500;
    transition: all 0.3s ease;
}

.carousel-channel-btn:hover {
    background-color: rgba(20, 174, 253, 0.2);
    border-color: rgba(20, 174, 253, 0.5);
    transform: scale(1.1);
}

.carousel-show-btn:hover {
    background-color: rgba(20, 174, 253, 0.2);
    border-color: rgba(20, 174, 253, 0.5);
    transform: scale(1.1);
}

.carousel-channel-btn.active {
    background-color: #14aefd;
    border-color: #14aefd;
    color: white;
    box-shadow: 0 0 10px rgba(20, 174, 253, 0.6);
    transform: scale(1.15);
}

/* 标签文本 */
.carousel-label {
    font-size: 14px;
    color: rgba(255, 255, 255, 0.9);
    white-space: nowrap;
    margin-right: 4px; /* 与输入间距更紧凑 */
}

/* 输入框容器（带底部装饰线），且不在 flex 中拉伸 */
.carousel-input-container {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto; /* 不拉伸 */
    width: auto;
}

    .carousel-input-container::after {
        content: '';
        position: absolute;
        bottom: 0;
        left: 0;
        width: 100%;
        height: 1px;
        background: rgba(255, 255, 255, 0.4);
        transition: background 0.3s ease;
    }

    .carousel-input-container:focus-within::after {
        background: #14aefd;
    }

/* 轮播间隔(秒)输入：按字符(ch)收窄，并禁止在 flex 中拉伸 */
.carousel-input {
    width: 2.6ch; /* 两位数(15)刚好；如需三位可改 3.2ch */
    min-width: 2.6ch;
    max-width: 3.4ch; /* 允许最多三位时略放宽 */
    height: 22px; /* 更紧凑高度 */
    background-color: transparent;
    color: white;
    border: none;
    outline: none;
    font-size: 14px;
    font-weight: 400; /* 保持现有权重 */
    text-align: center;
    padding: 0;
    margin: 0;
    border-radius: 0;
    -moz-appearance: textfield; /* Firefox */
    flex: 0 0 auto; /* 不拉伸 */
}

    .carousel-input::-webkit-outer-spin-button,
    .carousel-input::-webkit-inner-spin-button {
        -webkit-appearance: none;
        margin: 0;
    }

    .carousel-input:focus {
        background-color: transparent;
        box-shadow: none;
        color: #14aefd;
    }

/* 自动轮播开关（.form-check 方案） */
.carousel-switch .form-check-input {
    width: 40px;
    height: 20px;
    background-color: rgba(255, 255, 255, 0.2);
    border: 1px solid rgba(255, 255, 255, 0.3);
}

    .carousel-switch .form-check-input:checked {
        background-color: #14aefd;
        border-color: #14aefd;
    }

.carousel-switch .form-check-label {
    font-size: 14px;
    color: rgba(255, 255, 255, 0.9);
}
/* ===== Studio 表单：水平标签对齐 =====
   标签不足 8 个字时按 8 个字宽占位（1em ≈ 1 个汉字宽），使输入框对齐。
   窄屏（<768px）降级：标签独占一行，避免输入框被挤压。 */
.studio-field-horizontal > label {
    flex: 0 0 auto;
    min-width: 8em; /* 标签不足 8 个字时按 8 个字宽占位（1em ≈ 1 个汉字宽），使输入框对齐 */
}
@media screen and (max-width: 767.98px) {
    .studio-field-horizontal {
        flex-wrap: wrap;
    }
    .studio-field-horizontal > label {
        min-width: 100% !important; /* 窄屏时标签独占一行，压过上面的 min-width:8em */
    }
}

/* ===== Studio 设计器(WYSIWYG)三栏布局 =====
   左面板 175px + 画布(flex) + 右属性面板 260px；
   <992px 降级为纵向堆叠：左面板 → 画布(60vh 可滚动) → 右面板。 */
.studio-designer-main {
    display: flex;
    height: 70vh;
    overflow: hidden;
}
.studio-designer-left {
    width: 175px;
    flex: 0 0 175px;
    border-right: 1px solid #dee2e6;
    overflow-y: auto;
    background: #f8f9fa;
    padding: 8px;
    font-size: 0.8rem;
}
.studio-designer-canvas {
    flex: 1;
    overflow-y: auto;
    padding: 16px;
    background: #e8eaed;
}
.studio-designer-right {
    width: 260px;
    flex: 0 0 260px;
    border-left: 1px solid #dee2e6;
    overflow-y: auto;
    padding: 10px;
    background: #fafafa;
    font-size: 0.8rem;
}
@media (max-width: 991.98px) {
    .studio-designer-main {
        flex-direction: column;
        height: auto;
        overflow: visible;
    }
    .studio-designer-left,
    .studio-designer-right {
        width: 100%;
        flex: none;
        border: 0;
        border-bottom: 1px solid #dee2e6;
    }
    .studio-designer-canvas {
        flex: none;
        height: 60vh;
    }
}

/* ===== Studio 设计器拖拽反馈 ===== */
.designer-layout-node.drag-over,
.designer-canvas-field.drag-over {
    outline: 2px dashed #0d6efd;
    outline-offset: -2px;
    background-color: #f0f6ff;
}
.designer-canvas-field.dragging {
    opacity: 0.4;
}

/* ===== Studio 设计器：Odoo 风格字段类型/结构元素工具面板拖拽 ===== */
.studio-type-palette {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 4px;
}
.studio-type-item {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 4px 6px;
    border: 1px solid #dee2e6;
    border-radius: 4px;
    background: #fff;
    cursor: grab;
    font-size: 0.72rem;
    line-height: 1.2;
    user-select: none;
    transition: border-color 0.1s, background-color 0.1s, box-shadow 0.1s;
}
.studio-type-item:hover {
    border-color: #0d6efd;
    background: #f0f6ff;
}
.studio-type-item:active {
    cursor: grabbing;
}
.studio-type-item .bi {
    font-size: 0.9rem;
    color: #0d6efd;
    flex: none;
}
.studio-type-item .studio-type-label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.studio-struct-item {
    cursor: grab;
}
.studio-struct-item:active {
    cursor: grabbing;
}

/* 空画布 drop 目标 */
.studio-empty-canvas {
    text-align: center;
    padding: 40px;
    color: #999;
    border: 2px dashed transparent;
    border-radius: 8px;
    transition: border-color 0.1s, background-color 0.1s;
}
.studio-empty-canvas.drag-over {
    border-color: #0d6efd;
    background-color: #f0f6ff;
    color: #0d6efd;
}

/* 拖拽提示条 */
.studio-drag-hint {
    padding: 4px 10px;
    font-size: 0.78rem;
    color: #0d6efd;
    background: #e7f1ff;
    border: 1px solid #b6d4fe;
    border-radius: 4px;
}

/* ===== modal 背景遮罩(避免内联 style 重复) ===== */
.modal-backdrop-static {
    background-color: rgba(0,0,0,0.5);
}
.modal-backdrop-static-light {
    background-color: rgba(0,0,0,0.4);
}

/* ===== WYSIWYG 设计器预览 =====
   提取画布内内联 style 为 class，减少重复 */
.wysiwyg-previewed {
    font-size: 0.75rem;
    pointer-events: none;
}
.wysiwyg-sm {
    font-size: 0.7rem;
}
.wysiwyg-xs {
    font-size: 0.65rem;
}
.wysiwyg-node-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 6px 10px;
}
.wysiwyg-node-grip {
    font-size: 0.9rem;
    color: #6c757d;
    line-height: 1;
}
.wysiwyg-node-header.node-drop-target {
    outline: 2px dashed #0d6efd;
    outline-offset: -2px;
}

/* Studio 表单顶部状态栏（statusbar） */
.studio-statusbar-step {
    border-radius: 999px;
    padding: 0.25rem 0.8rem;
    font-size: 0.85rem;
}
.studio-statusbar-step.active {
    background-color: #0d6efd;
    border-color: #0d6efd;
    color: #fff;
}
.studio-statusbar-step:disabled {
    opacity: 1;
    cursor: default;
}
.wysiwyg-label-btn {
    font-size: 0.78rem;
    font-weight: 500;
    color: #0d6efd;
    text-decoration: none;
    padding: 0;
    border: 0;
    background: none;
    text-align: start;
}

/* WYSIWYG 补充 */
.wysiwyg-tab-name { font-size:0.8rem; width:120px; padding:2px 4px; }
.wysiwyg-xxs { font-size:0.6rem; }
.wysiwyg-field-row { display:flex; gap:6px; align-items:flex-start; margin-bottom:4px; }
.wysiwyg-actions { display:flex; flex-direction:row; gap:2px; margin-top:18px; }

/* ── 自定义表单（StudioDataForm）样式：从组件内嵌 <style> 迁移 ── */
/* Quill toolbar 和 editor 强制块级上下排列、撑满宽度 */
.ql-toolbar,
.ql-container {
    display: block !important;
    width: 100% !important;
}
/* Quill 外观微调：仅 .quill-editor-box 内的 Quill 去内框，外框由 .quill-editor-box 提供 */
.quill-editor-box .ql-toolbar.ql-snow {
    border: none !important;
    border-bottom: 1px solid #ced4da !important;
}
.quill-editor-box .ql-container.ql-snow {
    border: none !important;
}
/* Html 编辑框外框（Bootstrap 表单控件风格） */
.quill-editor-box {
    border: 1px solid #ced4da !important;
    border-radius: 0.375rem !important;
}

/* 只读形态的 Quill（内容查看页 `/website/articles/view`，以及无写权限时的编辑页）：
   工具栏对它没有意义 ⇒ 整条隐藏；正文按普通文档排版，不再是“输入框”观感。 */
.quill-editor-box.website-quill-readonly .ql-toolbar.ql-snow {
    display: none !important;
}
.quill-editor-box.website-quill-readonly .ql-container.ql-snow {
    border: none !important;
    font-size: 1rem;
}
.quill-editor-box.website-quill-readonly .ql-editor {
    padding: 0.75rem 1rem;
}

.ql-container {
    box-sizing: border-box !important;
}
/* 布局模式：隐藏 Html 字段的横向溢出（仅自定义表单作用域） */
.studio-data-form .card-body .row > .col-12 {
    overflow-x: hidden !important;
}

/* ── 打印报表：仅当页面含 #studio-print-report 时，打印时只显示报表区域，其余隐藏（通过浏览器打印另存 PDF）── */
@media print {
    /* ═══════════════════════════════════════════════════════════════════════════
       打印规则的**唯一出处**（2026-09-21 合并：此前散在 5 个 @media print 段里、相隔 300 行，
       彼此还在注释里互相指路——"见上面 .print-area .row 的说明"就是那种散落的症状）。
       合并时**逐条保持了原来的级联顺序**（不同选择器之间的先后有意义，不能按主题重排）。
       分节对应五类打印件：Studio 报表 / 打印卡 / .print-area 主干 / 批次标签 / 来料标签。
       ═══════════════════════════════════════════════════════════════════════════ */

    /* ── ① Studio 报表（#studio-print-report）：只印报表区 ──
       这一节仍用 visibility（不是 display:none）：.report-footer 需要 position:fixed 钉在每页底部，
       而定位元素在 display:none 的祖先里无法参与分页。⚠ 换选择器时请连**方案**一起换，别只改一半。 */
    body:has(#studio-print-report) * { visibility: hidden; }
    body:has(#studio-print-report) #studio-print-report,
    body:has(#studio-print-report) #studio-print-report * { visibility: visible; }
    body:has(#studio-print-report) #studio-print-report {
        position: absolute;
        left: 0;
        top: 0;
        width: 100%;
    }
    /* 每页重复表头 */
    body:has(#studio-print-report) #studio-print-report thead { display: table-header-group; }
    /* 每页固定页脚 */
    body:has(#studio-print-report) #studio-print-report .report-footer {
        position: fixed;
        bottom: 0;
        left: 0;
        right: 0;
    }

    /* ── ② 工单卡明细表：一行不被拦腰截断 ── */
    .print-card-table tr { page-break-inside: avoid; }

    /* ── ③ .print-area 主干：只印打印区 ──
       总原则：打印时的**显示控制交给 markup 里的 Bootstrap 5 打印工具类**
       （d-print-block / d-print-table / d-print-none 决定各块打印时的 display）；
       本段只保留工具类表达不了的部分：隐藏兄弟节点、分页、iframe/画布尺寸、放开预览宽度。

       「用 table 还是用 div」的判据（2026-09-21 审核后固化：按内容形态选，不按好看选）——
       ① 行列表格（送货单 / 图纸清单 / 出货单 / 工单卡明细 / 凭证 / 账簿 / 财务报表）
          用 table.table：打印引擎对表格的宽度分配与分页最确定；thead 配
          display:table-header-group 还能让长表格每页重复表头（见上面 ① 的 Studio 报表）。
       ② 固定尺寸的标签与卡片（来料标签 / 送货标签 / 批次标签 / 图纸卡）
          用 div + CSS 版式：它们按物理尺寸（如 50x30mm）与"一张一页"打印，内容是被定位的块
          ——表格表达不了固定物理尺寸，而"每页重复表头"对单件内容毫无意义。
       ③ 内嵌文件（工单卡里的随卡图纸）**只能**用 &lt;img&gt;：iframe 里的 PDF 插件内容
          **不会进 Chromium 的打印输出**（"预览有图、纸上空白"的根因）⇒ PDF 图纸在打印前由
          `js/print-pdf.js` 光栅化成位图（尺寸见下面 ③.3）。
       实例：① SupplierPortal 的送货单/图纸清单、Shipments 的送货单、FinancePrintDoc；
       ② .incoming-label-card / .batch-label-card / 图纸卡；③ .job-doc-media。

       ⚠ 打印区所在的弹窗：页脚必须显式带 d-print-none（双保险，不是形式主义）——
       下面 ③.1 的兜底规则会把弹窗页脚一并隐藏，漏加不会立刻出问题；但兜底方式一旦变动
       （换成 JS 克隆宿主、或替换掉 :has()），漏加的地方会一起变成"每张单据末尾多印一页说明与按钮"。
       该一致性由 DataWeb.Tests/PrintMarkupConsistencyTests.cs 钉住。

       ── ③.1 只输出 .print-area 子树（纯 CSS，无 JS 宿主、无 pdf.js）──
       Bootstrap 5 的打印工具类只能控制"元素自身"在打印时的 display，表达不了「隐藏兄弟节点」，
       故这里用 :has() 补齐（Chromium 105+，本应用已依赖）：
       · 隐藏「既不含 .print-area、也不在 .print-area 内」的所有元素，且用 display:none —— 隐藏即不占位，
         打印区从第 1 页顶部自然流式分页；旧的 visibility:hidden 方案隐藏后仍占位，
         会让内容整体偏移、多页被截断（这是当初引入 JS 克隆宿主的原因，现由 display:none 直接解决）；
       · 打印区的祖先链必须保留 display，并放开 overflow/高度限制、把弹窗还原为静态流式，
         否则 position:fixed + overflow:hidden 会裁剪多页内容。 */
    body:has(.print-area) *:not(.print-area):not(.print-area *):not(:has(.print-area)) { display: none !important; }
    /* ⚠ `overflow` 必须显式还原：`.modal-body` 带 Bootstrap 的 `overflow-y: auto`，
       而 `overflow != visible` 的元素**不是分页容器**——Chromium 会把里面的内容整体裁到一页里
       （实测：卡片 1 张 + 图纸 2 张的打印区高约 394mm，输出却只有 1 页、后两页整块消失）。
       `height`/`max-height` 也要一起放开，否则 `.modal-dialog-scrollable` 的
       `height: calc(100% - 3.5rem)` 在分页媒体里会算出一个"一页高"的盒子。
       （此前工单卡只有一页、恰好没触发；本批加了"随卡图纸每份一页"才暴露出来。） */
    body:has(.print-area) *:has(.print-area) {
        overflow: visible !important;
        height: auto !important;
        max-height: none !important;
    }
    body:has(.print-area) .modal { position: static !important; display: block !important; overflow: visible !important;
        /* 遮罩底色清零：`PrintPreviewModal.razor:11` 的内联 rgba(0,0,0,.5) 只在"用户勾了背景图形"时
           才会印出大片灰底——CSS 覆盖不了内联样式，那是**已知边界**（打印对话框里的"背景图形"
           由用户控制，任何 print-color-adjust 都只能提高保真度、不能覆盖"省墨"）。这里把**能控制的
           那一半**做到：底色显式转白，用户即便勾了背景图形也不会印出灰底。 */
        background: #fff !important; }
    body:has(.print-area) .modal-dialog { margin: 0 !important; max-width: none !important; }
    body:has(.print-area) .modal-content { border: 0 !important; border-radius: 0 !important; box-shadow: none !important; }
    body:has(.print-area) .modal-body { overflow: visible !important; padding: 0 !important; }
    body:has(.print-area) .modal-backdrop { display: none !important; }

    /* ── ③.2 文档块分页：卡片块独占一页、每张图纸块各占一页 ──
       ⭐ 2026-10-12 收口批：打印入口只剩一个【打印】⇒ **"打印范围"这一维整体消失**，
       本段回到"按块分页"这一件事。随之删除的三样东西（都曾为"一次只印一种范围"服务）：
         · `body:has(.print-area.print-scope-card/-drawing) … { display:none }` 两条收敛规则；
         · `.print-no-released-block` 的范围显隐（那是"只打图纸时卡片被藏起来、fail-closed 那句话
           跟着消失"的补丁；现在卡片**总是**印，卡上的「图纸版次」行已经把话说全 ⇒ 补丁多余）；
         · `js/print-scope.js` 整套"打印前切范围/切纸面宽/顺序多作业"（文件已删除）。
       ① 卡片块：**后面还有图纸块**才换页 —— 判据是结构性的 `:has(~ .job-doc-block)`，
          ⛔ 不再读内联 `display:none`（那套信号随范围收敛一起没了）。它同时排除了
          "没有图纸时多出一张空白页"（`.print-foot` 是打印区最后一个子元素，够不着
          `.print-area > :last-child` 的复位，故必须由这条条件自己成立）。
       ② 图纸块：每份**强制另起一页**、块内不拆（`break-inside: avoid`）。
       ③ `.print-first-visible`：第 1 张图纸块**不再另起一页**（卡片那条 `break-after` 已经换过页；
          相邻的强制分页在 Chromium 上会合并，这里显式标出"这是第一个块"，是为了将来卡片块
          被条件性隐藏时不会多出一张空白页）。
          ⚠ 该类的**唯一来源是 markup 的静态标记**（`ProductionOrders.razor` 的图纸循环给第 1 块加）
          —— 不再有"JS 在打印那一次重新标记"这条路径（那也随 print-scope.js 删除）。 */
    .print-area .print-first-visible { break-before: auto !important; page-break-before: auto !important; }
    .print-area .print-card-block:has(~ .job-doc-block) {
        break-after: page;
        page-break-after: always;
    }
    .print-area .job-doc-block {
        break-before: page;
        page-break-before: always;
        break-inside: avoid;
        page-break-inside: avoid;
    }
    .print-area > :last-child { break-after: auto; page-break-after: auto; }
    /* 二维码行必须与它下面那两条说明同页（只有 tr{page-break-inside:avoid} 不保它与上一行同页） */
    .print-area .print-card-table tr:has(img.print-qr),
    .print-area .print-card-table tr:has(img.print-qr) + tr { break-inside: avoid; page-break-inside: avoid; }

    /* ── ③.3 图纸页几何：一张图纸必须**正好占一页 A4 横向**，且不多出一张废纸 ──
       @page wo-drawing 为 A4 横向、页边距 8mm/6mm（finance-print.css）→ 可打印区 285mm 宽 × 194mm 高。
       ① **容器必须先给确定高度**：`.job-doc-block{height:190mm}` —— 否则下面图片的百分比 `max-height`
          在分页媒体里会解析成 `none`（块的包含块高度是 auto），"最大不超过一页"的约束整体失效；
       ② 图纸位图按 285mm × 186mm（给标识行留 4mm）等比缩放、**永不放大**（小图放大就是糊）；
       ③ 打印区自身的 Bootstrap 内边距（p-3 = 4.2mm）与文档块外边距（0.75rem = 3.2mm）必须在打印时清零，
          否则可用高度只剩 190 - 4.2×2 - 3.2 ≈ 178mm，同样会把图纸挤到第二张纸；边框只为屏幕预览好看，打印去掉。
       用 :has(.job-doc-media) 限定，避免影响其它打印件（凭证/报表）的 .print-area 内边距。
       ⚠ **位图不是可选项**：PDF 图纸由 `js/print-pdf.js` 先光栅化成 `.job-doc-media` 才印得出来
       （iframe 里的 PDF 内容不会进 Chromium 的打印输出 —— 2026-10-12 现场缺陷"预览有图、纸上空白"）。 */
    body:has(.job-doc-media) .print-area { width: 100%; padding: 0 !important; border: 0 !important; }
    /* 扫码指令表同理（2026-10-13 对齐批）：它也是"一张纸装满"的打印件，Bootstrap 的 p-3 内边距
       （4.2mm×2）与 border/rounded 只为屏幕预览好看 —— 实测它们白吃约 8.5mm 页高，并把一圈浅灰
       1px 边框印到纸上（离线台量到灰度 225–248 ≈ #dee2e6）。清零后纵向单页余量约 60mm。 */
    body:has(.scan-command-sheet) .print-area { width: 100%; padding: 0 !important; border: 0 !important; border-radius: 0 !important; }
    .print-area .job-doc-block {
        margin-top: 0 !important;
        height: 190mm;      /* 见上面 ①：给百分比 max-height 一个解析对象 */
        overflow: hidden;   /* 兜底：内容若仍超出（异常长图），裁掉而不是溢出成第二张纸。
                               实测（Chromium Print to PDF）1 张图纸 = 1 页、无空白页。 */
    }

    /* ── ③.3b 图纸页顶部「版次标识行」的高度再分配 ──
       本节的职责只有一个：把标识行的高度**从 190mm 里扣出来**，保持"一张图纸正好一页"
       （③ 节的 `height:190mm; overflow:hidden` 会裁内容，被裁出来的溢出在分页媒体里就是多出来的一张纸）。
       做法：块高仍是 190mm，用 `padding-top: 4mm` 给标识行**留出**位置（`box-sizing` 是 border-box
       ⇒ 内容盒变成 186mm）；`max-height:100%` 于是按 186mm 解析，标识行自身 4mm 落在 padding 区里。
       ⚠ 这两个 4mm 与 `.job-doc-stamp` 的高度必须**同源**：改一处就要改另一处。 */
    .print-area .job-doc-block:has(> .job-doc-stamp) { padding-top: 4mm; }
    .print-area .job-doc-block > .job-doc-stamp { height: 4mm; }

    /* ── ③.3c 图纸准备中 / 渲染失败：纸面上必须看得见 ──
       · `.job-doc-pending`（"正在准备图纸…"）只在**没画完**时出现；画好/失败后按 `data-pdf-state` 隐藏。
         它留在 DOM 里而由 CSS 管显隐（**不**由 JS 删除）：Blazor 按自己的渲染树打补丁，
         JS 擅自删树里的节点会让后续 diff 与真实 DOM 对不上（见 js/print-pdf.js 注释）。
       · `.job-doc-error` 是**渲染失败**那一页唯一的内容 ⇒ 必须印得出来（红框红字 + 精确输出颜色），
         否则车间拿到的就是一张"什么都没有、却写着随卡图纸 N 份"的纸。 */
    .print-area .job-doc-pending { color: #6c757d; font-size: 12px; padding: 6px; }
    .print-area .job-doc-block[data-pdf-state="ready"] .job-doc-pending,
    .print-area .job-doc-block[data-pdf-state="failed"] .job-doc-pending { display: none; }
    .print-area .job-doc-error {
        border: 1px solid #b02a37;
        color: #b02a37;
        font-weight: 600;
        font-size: 12px;
        padding: 6px;
        -webkit-print-color-adjust: exact;
        print-color-adjust: exact;
    }

    /* 图纸位图：按图纸页可打印区等比缩放、**永不放大**（小图放大对位图就是糊）、不裁切。
       ⚠ `object-fit` 在这里是空转（width/height:auto + max-* 让盒子跟随固有比例，没有可作用的确定尺寸）；
       只有"固定画框"变体（给死 width/height）才用得上它，而那会把小图也放大 ⇒ 默认不用。 */
    .print-area .job-doc-media {
        display: block;
        margin: 0 auto;
        width: auto;
        height: auto;
        max-width: 100%;
        max-height: 100%;
        object-fit: contain;
        object-position: center;
    }

    /* ── ③.4 打印时宽度统一为可打印区整宽：屏幕预览的 max-width（.print-area-w-*）只在 screen 媒体生效 ── */
    .print-area { width: 100%; }

    /* ── ③.5 工单卡二维码：打印尺寸只由 mm 决定（markup 里**不写** width/height 属性）──
       为什么删 px 属性：源图边长 = (模块数 + 8) × pixelsPerModule ⇒ 随编号长度升版本而变，
       固定 150px 会让倍率在 1.0/0.97 之间跳、非整数重采样。
       为什么 30mm：ECC Q + ≤16 位大写字母数字 ⇒ version 1 = 21 模块，含 QRCoder 已烘焙进 PNG 的
       4 模块静区 = 29 模块 ⇒ 模块边长 30/29 = 1.03mm、静区 4×1.03 = **4.14mm ≥ 4mm ✓**
       （25mm 时静区只有 3.45mm，低于扫码可读下限 —— 所以不能更小）。
       display:block 去掉 inline 基线的下方间隙（所在 td 是 vertical-align:middle）。 */
    .print-area .print-qr {
        display: block;
        width: 30mm;
        height: 30mm;
        margin: 2mm auto 0;
        flex: none;
    }

    /* ── ③.6 页脚（**只印在工单卡这一页**，是卡片块内的流内一块）──
       ⚠ **做不出"第 x 页 / 共 y 页"**：页码要靠 @page 的 margin at-rules（@bottom-center 里的
       counter(page)/counter(pages)），而 **Chromium 不实现 @page 边距框** ⇒ CSS 里没有承载页码的盒子；
       前端枚举页数也不可行（分页由 Blink 在打印时完成，window.print() 同步阻塞且无返回值、
       beforeprint/afterprint 不带页数、DOM 里没有"页"这个元素）。
       故这里只印**可预知**的内容：公司名 + 工单号 + 本单图纸份数 —— 不依赖分页结果，也不骗人。
       ⛔ 2026-10-12 收口批：它**不再是 `position:fixed` 的"每页重复页脚"**（那是本条的旧形态）。
       实测（Edge 155 `--headless=new` + CDP `Page.printToPDF`）：固定定位元素与**命名页**
       （`page: wo-card` / `page: wo-drawing`）同处一份文档时，`bottom:0` 会被算到另一张纸的几何上
       —— 图纸页上的页脚落在页面中部、压在图上（对照组：把 `page:` 声明去掉，页脚正确落页底）。
       故页脚改为卡片页里的流内块；图纸页的身份信息由每页顶部的**版次标识行**承担
       （含工单号，见 ProductionOrders.razor 的 printDocBlocks 构造）。
       ⚠ 屏幕段仍隐藏它（`.print-area .print-foot{display:none}`）⇒ **预览里看不到它**，
       这正是"打印的就是预览里的内容"该有的样子（它是页脚，不是正文）。 */
    .print-area .print-foot {
        display: block;
        height: 8mm;
        overflow: hidden;
        font-size: 9pt;
        color: #333;
        border-top: 0.3mm solid #999;
        padding-top: 1mm;
    }
    .print-area .print-foot-logo { height: 6mm; vertical-align: middle; margin-right: 2mm; }
    .print-area .print-foot-note { float: right; }

    /* ── ④ 批次标签：一张标签一页（最后一页不分页）──
       根因：.print-area 内的 .row 是 Bootstrap flex 容器——**Chromium 不碎片化 flex 布局**，
       flex 子项之间的 page-break 被忽略，标签全部挤在一页 → 打印时把 .row 改为块布局、每列占满整行，
       再配合 .batch-label-card 的 page-break-after 实现每签一页。
       （.print-area 打印时的 display 由 markup 的 d-print-block 负责，此处不重复声明。）

       ⚠ **本节的"一签一页"在当前 Chromium 下**未能**通过实打验证（2026-09-21 记录，别当成已解决）**：
       实测（**当时用的 headless 打印脚本**；该脚本 `tools/print-harness.js` **在本仓已不存在** ⇒
       下面的复现暂时只能人工做）三张标签**打成 1 页**；同一份 markup 若去掉
       `.row > .col-*` 包裹（卡片直接是 .print-area 的子元素）则正确打成 3 页。
       已排除的原因（都试过、都不解决）：规则是否被解析（CSSOM 里在）、选择器是否匹配
       （`matches()` 为 true）、`!important`（加了仍 1 页）、Bootstrap 的 `.col-*` flex 属性、`g-2` gutter。
       仍然匹配的是"**卡片的祖先 `.col-*` 是 flex 项、Chromium 不碎片化 flex 项**"——
       若如此，CSS 修不好，需要改 markup（让卡片不落在 flex 项里）。
       **改这里之前先人工复验页数**：Microsoft Print to PDF 打一次批次标签（3 张），
       数出来不是 3 页就说明问题仍在。 */
    .print-area .row { display: block !important; }
    .print-area .row > [class*="col-"] { width: 100% !important; max-width: 100% !important; flex: none !important; }
    .print-area .batch-label-card { page-break-after: always; break-after: page; }
    .print-area .batch-label-card:last-child { page-break-after: avoid; break-after: auto; }

    /* ── ⑤ 来料标签：把 flex 容器换成块容器 ──
       Chromium 不对 flex 容器做分片（与 ④ 是同一个根因），标签多于一页时后面的签会整块丢失。
       改成块容器后 .incoming-label-card 是 inline-block，在块流里按行折行、行间能正常分页；
       卡片自身的 page-break-inside: avoid 保证不被拦腰截断。 */
    .incoming-label-sheet { display: block !important; }
    .incoming-label-card { display: inline-block; vertical-align: top; margin: 0 1mm 1mm 0; }
}

/* ── 主页应用卡片（Odoo 风格应用列表）── */
.home-app-card {
    transition: transform .15s ease, box-shadow .15s ease;
}
.home-app-card:hover {
    transform: translateY(-3px);
    box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .12) !important;
}
.home-app-icon {
    width: 44px;
    height: 44px;
    font-size: 1.25rem;
}
.home-app-desc {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    min-height: 2.6em;
}

/* ── 工单卡打印（仅打印 .print-area）── */
/* 工单卡表格布局：Bootstrap 5 表格（table table-sm table-bordered）+ 固定列宽——
   HTML 表格是打印引擎最可靠的布局方式（分页/宽度/单元格定位确定性最好）。
   选择器前缀 .table 提升优先级，压过 Bootstrap 自带 td 的 border/padding/vertical-align；
   print-color-adjust 保证打印时表头灰底照常输出。 */
.table.print-card-table {
    width: 100%;
    border-collapse: collapse;
    table-layout: fixed;
    margin-bottom: 0;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
}
.table.print-card-table td {
    border: 1px solid #444;
    padding: 4px 8px;
    vertical-align: middle;
    font-size: 14px;
    word-break: break-all;
}
.table.print-card-table td.border-0 { border: 0 !important; }
.table.print-card-table .print-card-label { background: #eee; font-weight: 600; }
.table.print-card-table .print-card-m80 { background: #f8f9fa; }

/* ── 图纸「版次可辨」（收口批；屏幕与打印都显示）──
   三要素 = 图号 + 版本 + 发布状态（+ 最后变更日期）⇒ 车间一眼认出手上这张是不是最新已发布版。
   屏幕与打印同源（这是纸面上最有信息量的一行，不给它单独做"只打印"分支）。
   ⚠ 打印时的**几何代价**：图纸页那一行必须从 `.job-doc-block` 的 190mm 里**扣出来**，
   否则每份图纸会多一张空白页 —— 见上面 @media print ③.3b 的高度再分配。 */
.table.print-card-table .print-card-stamp { font-size: 12px; line-height: 1.35; }
/* fail-closed 的那一句必须**一眼可见**（黑底白字、不靠颜色区分——黑白打印机也要看得见） */
.print-no-released {
    display: inline-block;
    background: #000;
    color: #fff;
    font-weight: 700;
    letter-spacing: 1px;
    padding: 1px 6px;
    margin-bottom: 2px;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
}
.print-stamp-line { word-break: break-word; }
.print-released-mark { font-weight: 600; }
/* 每份图纸页顶部的标识行（高度由打印段的 4mm / 185mm 再分配决定，别只改这里） */
.job-doc-stamp {
    background: #f0f0f0;
    border-bottom: 1px solid #444;
    font-size: 12px;
    font-weight: 600;
    line-height: 1.3;
    padding: 1px 6px;
    word-break: break-all;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
}
/* 打印规则已合并到文件上方「打印规则的唯一出处」那一节（含 .print-card-table 的分页规则）。 */
/* 随卡图纸（位图/光栅化后的 PDF 页）：屏幕预览里按纸面宽等比显示（**屏幕**规则；
   纸上的尺寸由 @media print 的 ③.3 决定）。`max-width:100%` 且 height:auto ⇒ 永不放大。 */
.print-area .job-doc-media {
    display: block;
    margin: 0.5rem auto 0;
    width: auto;
    height: auto;
    max-width: 100%;
}
/* 图纸准备中（还没画完）：屏幕与纸上都显示同一句话（纸上那句在 @media print 的 ③.3c 里给颜色/字号） */
.print-area .job-doc-pending { padding: 6px 0; font-size: 12px; color: #6c757d; }
.print-area .job-doc-block[data-pdf-state="ready"] .job-doc-pending,
.print-area .job-doc-block[data-pdf-state="failed"] .job-doc-pending { display: none; }
/* 图纸渲染失败：红框红字（屏幕看得到，纸上也印得出来 —— 印不出来就等于"静默少一张图"） */
.print-area .job-doc-error {
    margin-top: 0.5rem;
    padding: 6px;
    border: 1px solid #b02a37;
    color: #b02a37;
    font-size: 12px;
    font-weight: 600;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
}

/* 文档中心聚合的图纸/作业文件：块内不拆分（**块间分页在 @media print 的 ③.2 里统一定义**，
   按"可见块的相邻关系"算，见那一段的注释——不要在这里再加 break-before，两处并存会双倍分页）。 */
.print-area .job-doc-block {
    page-break-inside: avoid;
    margin-top: 0.75rem;
}

/* ── 打印预览弹窗的**最大档**（XXL）：`.modal-xxl` ──
   Bootstrap 5 自带的固定档只到 `.modal-xl`（1140px），**没有** `.modal-xxl`
   （它只有 `.modal-fullscreen-xxl-down` 这种"窄屏全屏"变体）⇒ 这一档的类由本仓补：
   1400px 取 Bootstrap 的 xxl 断点宽度（与 `.modal-xl{1140px}` 同一套断点口径），
   `96vw` 是"窄屏不许横向溢出"的那一半（`min()` 取小者）。
   · 为什么需要它：工单作业文件打印预览的页脚要同时摆下「补印原因/打印记录 + 关闭/打印」，
     `modal-lg`（800px）下被挤成好几行（用户截图反馈"内容被挤"）；
   · 窄屏：`<576px` 由下面「全站移动适配 ④」的 `.modal-dialog{margin:0;max-width:none;min-height:100%}`
     接管（整屏弹窗，Bootstrap `.modal-fullscreen-sm-down` 的等价物）；576~1400px 之间由 `96vw` 兜住；
   · 高度/滚动：`PrintPreviewModal.razor:13` 已带 `modal-dialog-scrollable` + `modal-dialog-centered`
     ⇒ 弹窗比视口高时正文内部滚动，不把页脚顶出屏外；
   · 打印时由 @media print 的 `body:has(.print-area) .modal-dialog{max-width:none !important}` 覆盖，
     与屏幕宽度无关。
   由 DataWeb.Tests/PrintMarkupConsistencyTests 的「弹窗尺寸类必须在样式表里有定义」钉住。 */
.modal-xxl { max-width: min(1400px, 96vw); }

/* ── 打印预览模态：**外框宽度由纸面派生**（本批修：产品方"预览的内容的宽度和 Modal 框
   没有对应上，有大多空白"）──
   `PrintPreviewModal` 只发一个内联变量 `--app-print-paper-w`（= 纸面可打印区宽度，
   @96dpi：工单卡 A4 纵 733px / 随卡图纸 A4 横 1077px），这里换算成外框：
     外框 = 纸面 + `.print-area` 的 p-3 内边距（1rem×2 = 32px）+ 纸框边框（1px×2）
          + 滚动条余量（2px） = 纸面 + 36px
   于是纸面两侧只剩那圈内边距（此前是 `modal-xxl` 1400px 配 `PrintWidth=Md` 720px
   ⇒ 两侧各留约 340px 死白）。
   ⚠ **这个变量是全部屏幕宽度的唯一来源**（外框 + 纸面都读它，见下面 `@media screen` 那两段），
   而它**对打印没有任何影响**（`@page` 的可打印区说了算；`.print-area` 上也没有内联宽度了）。
   · **本批修掉的 15px（"滚动条余量"原先只是写在账上的数）**：上面那条 `+ 36px` 里滚动条余量
     记的是 **2px**，而 Chromium 的经典滚动条实占 **15px** ⇒ `modal-dialog-scrollable` 的滚动
     容器 `.modal-body` 的内容盒 = 769 − 2(纸框边框) − 32(p-3 内边距) = 735，再 − 15(滚动条)
     = **720** ⇒ 纸面被压成 720px（外框仍按 733 算 ⇒ 右侧白白多出 15px 死白）。
     Edge headless + CDP 实测（1280/1440/1600/1920 四视口，改前）：外框 769 / 纸面 **720** /
     左 17 右 32；把正文换成"不滚动"的短内容 ⇒ 纸面 733、两侧 18/18 —— 这一步同时排除了
     "`.print-area-w-md` 的 720px 上限在压纸面"这个怀疑（改前纸面挂 `-w-md` / 挂 `-w-full` /
     不挂类，实测一律 720；`.print-area-w-md` 只在**有人显式传 `PrintWidth=Md`** 时才出现，
     本仓已无这种调用点）。
     ⇒ 本批把**纸框模式下**的正文滚动条钉成账面已经留出的 **2px**：内容盒 = 纸面宽，
     纸面如实出 733 / 1077（±1px）。另三条候选途径（外框改 `+ 51px` / 在 `.modal-body` 内边距
     里扣回滚动条 / 去掉 `modal-dialog-scrollable`）与实测数字见报告 §1，分别被
     "违背 `+36` 的账"、"把平台滚动条宽度写进样式表（换 DPI 就漂）"、"页脚随正文滚出屏外"
     否掉。
   · ⛔ 那条 `::-webkit-scrollbar` 钉法**只认** `.print-paper-framed`：标签类
     （`PaperFrame=false`）与非纸框弹窗的滚动条一点不动；也**不许**顺手加 `scrollbar-width`
     —— 一旦它不是 `auto`，Chromium 就忽略 `::-webkit-scrollbar`、宽度回落 15px，
     那正是本批修掉的那 13px。
   · 上界 `96vw`：窄屏不外溢（576~1400px 之间由它兜住；<576px 由下面「全站移动适配 ④」
     的 `.modal-dialog{margin:0;max-width:none;min-height:100%}` 接管成整屏），
     此时**纸面按比例缩小**发生在浏览器对 `max-width` 的常规收缩上（纸面 1:1 语义不变：
     宽屏永远是 1:1，只有视口容不下时才被压缩）。
   · `padding: 0`：Bootstrap 的 `.modal-dialog` 两侧没有内边距，这里显式钉住 ——
     否则将来有人给它加内边距，上面那条 `+ 36px` 的账就不成立（外框会挤出横向滚动条）。
   · ⛔ 打印时这条规则不生效：`@media print` 的
     `body:has(.print-area) .modal-dialog{max-width:none !important}` 只压 `max-width`，
     故这里**只设 `width`**（不设 `max-width`），打印段那条不必加 `!important` 也不会被顶掉；
     而且打印时纸面整宽由 `.print-area{width:100%}` 决定 ⇒ **纸的尺寸与屏幕无关**。
     ⚠ 本批（"图纸打印太窄"）之后这条"纸上宽度"才是**完整**的：此前 `.print-area` 上还有一条
     **内联** `max-width`（`PrintPreviewModal.PrintAreaStyle`），内联样式不分媒体 ⇒
     纸上宽度实际是 `min(可打印区, 那条内联值)`，把 A4 横的图纸压成 194mm 的带子。
     那条内联值已删（屏幕侧由下面 `@media screen` 里读同一个变量的规则接管）。
   由 DataWeb.Tests/Guards/PrintPreviewDialogWidthGuardTests 的行为级断言钉住
   （`--app-print-paper-w` ↔ `.print-area` 的 max-width ↔ 实测外框宽三者同源）。
   ⚠ 钩子是**类名** `.print-paper-framed`（组件在纸框模式下加），不是
   `[style*="--app-print-paper-w"]`：属性选择器把"发不发变量"藏进样式匹配里，且实测
   在本地 `file://` 页面下不可靠（第一版用它时外框宽度根本没生效 —— 死白一点没少）。 */
@media screen {
    .modal-dialog.print-paper-framed {
        width: calc(var(--app-print-paper-w) + 36px);
        max-width: 96vw;
        padding: 0;
    }

    /* 纸框模式的**正文滚动条**：钉成上面那条账里已经留出的 2px（本批修：纸面被滚动条吃掉 15px）。
       · 为什么是 2px 而不是"隐藏"：账里本来就写着"滚动条余量（2px）= 32 + 2 + 2 = 36"，
         钉成 2px 让这条账**第一次真的成立**，同时保留一条可见（可拖）的细滚动条；
       · ⛔ 选择器只认 `.print-paper-framed`（标签类/非纸框弹窗不碰，守卫会红）；
       · ⛔ 不要写 `scrollbar-width`（值不是 auto 时 Chromium 忽略 ::-webkit-scrollbar
         ⇒ 宽度回落到 15px，纸面又变 720）；
       · 打印时不存在滚动条（打印段 `body:has(.print-area) .modal-body{overflow:visible
         !important;padding:0 !important}`），故这两条只在屏幕媒体里、纸上零影响。 */
    .modal-dialog.print-paper-framed .modal-body::-webkit-scrollbar {
        width: 2px;
        height: 2px;
    }
    .modal-dialog.print-paper-framed .modal-body::-webkit-scrollbar-thumb {
        background: rgba(0, 0, 0, .35);
        border-radius: 2px;
    }
    .modal-dialog.print-paper-framed .modal-body::-webkit-scrollbar-track { background: transparent; }

    /* ── 纸框模式的**纸面宽**：与上面那条外框宽读**同一个变量**（本批起它也是**屏幕上**
       纸面唯一的上限）──
       · 这个变量由 `PrintPreviewModal.ModalDialogStyle` 按**纸面形状**发一次
         （`PreviewPaper.PaperWidthPx`：A4 纵 733 / A4 横 1077），**不再由 JS 改写** ——
         原先"范围按钮在打印那一次把纸面从卡换成图"那条路径已随打印范围整体删除
         （2026-10-12 收口批：打印入口只剩一个【打印】，纸张方向改由
         finance-print.css 的**按块命名页**决定，屏幕上的纸面宽是静态的）。
       · ⛔ `!important` 是必须的：`.print-area` 上还挂着 Bootstrap 的宽度工具类
         （纸框模式 = `.print-area-w-full{max-width:100%}`，finance-print.css 也有一份同样的类；
         `PrintWidth` 档位另有 `-w-sm/-md/-lg` 的**数字**上限）。不加 `!important`，
         同优先级的后置规则与档位类都可能盖过这一条。
       · ⛔ 绝不写进 `@media print`：**这一条只在屏幕媒体生效**，于是"屏幕上的纸面宽"
         完全由它（+ 那个变量）决定，纸上则由 `@page` 的可打印区决定。
         ⚠ 本批（"图纸打印太窄"）修的就是这条**分工**：此前 `.print-area` 上另有一条
         **内联** `max-width`（`PrintPreviewModal.PrintAreaStyle` 派生），内联样式**不分媒体**
         ⇒ 它成了纸上唯一的上限，把 A4 横的图纸压成 194mm 的带子（左右各约 45.5mm 白边）。
         实测（Edge printToPDF 逐页墨迹包围盒，其余 DOM 逐字节相同，只有那一条内联值不同）：
         内联 733px ⇒ 图纸页墨迹带宽 193.94mm；内联 1077px 或**没有内联上限** ⇒ 285.22mm。
         修法是**把那条内联上限删掉**（不是往打印段加规则）：屏幕上这条规则本来就把它压住了，
         故屏幕侧一字不变（卡 733 / 图 1077、外框 = 纸面 + 36）。离线台与对照表见
         `.artifacts82/seed-review/printw` 与报告 `.artifacts82/seed-review/图纸打印铺满.md`。
       · 值只能是 `var(--app-print-paper-w)`：⛔ 不许出现任何数字上限（那等于把纸面压回
         720/560/960，`PrintPreviewDialogWidthGuardTests` 的"防误伤"那条会红）。 */
    .modal-dialog.print-paper-framed .print-area {
        max-width: var(--app-print-paper-w) !important;
    }
}

/* 打印区（预览弹窗内）宽度：**仅屏幕预览生效**（故用 @media screen 明确限定，打印时自动失效，
   无需在打印规则里再覆盖）。用固定值是因为 Bootstrap 的 w-* 工具类只有 25/50/75/100%，
   无法表达 560/720px 这类预览宽度。
   ⚠ 这些类是 `PrintPreviewWidth` 档位的**样式出处**（档位→类名在组件里一处映射）。
   ⚠ 本批（"图纸打印太窄"）起，`.print-area` 上**不再有内联 `max-width`** ⇒ 屏幕上纸面的宽度
   上限按**优先级**取：
     · 纸框模式：上面那条 `.modal-dialog.print-paper-framed .print-area{max-width:var(--app-print-paper-w)!important}`
       带 `!important` ⇒ 赢过下面前三个**数字**档位类，纸面 = 纸面宽（卡 733 / 图 1077）；
     · 非纸框用法（`PrintAreaClass=` / 显式 `PrintWidth`）：仍吃下面这些类（`-w-md` 720 等）。
   现状（本批实测核对）：本仓已无调用点手配 `PrintWidth` ⇒ 纸框模式挂的是 `-w-full`
   （= `max-width:100%`，**不是上限**）。`-w-md`(720) 只在**有人显式传
   `PrintWidth="PrintPreviewWidth.Md"`** 时才出现；那时屏幕上限由纸框那条 `!important`
   规则给（720px，与档位同值）⇒ 不构成差额。此前"框模式纸面 720"的真因是 `.modal-body`
   的滚动条 15px（见上面打印预览模态那段）。
   ⛔ 这几个类**不许**按纸框/非纸框改口径，更**不许**删：非纸框用法（`PrintAreaClass=`）
   与 `PrintPreviewWidth` 的档位反查都靠它们（守卫 Framed_paper_... 的"防误伤"那条会红）。 */
@media screen {
    .print-area-w-sm { max-width: 560px; }
    .print-area-w-md { max-width: 720px; }
    .print-area-w-lg { max-width: 960px; }
    .print-area-w-full { max-width: 100%; }

    /* 打印专用件在屏幕上不显示：每页固定页脚只在纸上才有意义（屏幕上它会钉在预览框底部、
       与"打印"按钮抢位置）。
       （不用 d-print-block 工具类是为了让 .print-foot 成为"打印专属块"的单一出处——
       它没有对应的屏幕语义，散在 markup 里容易在别处被误用。） */
    .print-area .print-foot { display: none; }

    /* G7 那块 `.print-no-released-block` 的屏幕规则已随该块本身删除（2026-10-12 收口批：
       卡片现在总是印，fail-closed 那句话就在卡片的「图纸版次」行里 ——
       不再需要"只在图纸范围才显示"的第二块，也就没有它的显示控制）。 */
}

/* ── 批次原料标签打印（对齐 PartLists 批量标签：预览卡 + 打印时每签一页）── */
.batch-label-card {
    border: 1px solid #000;
    background: #fff;
    color: #000;
    margin: 0;
    padding: 4px;
    overflow: hidden;
    page-break-inside: avoid;
}
.batch-label-card .lot-code {
    font-family: Consolas, "Courier New", monospace;
    font-weight: bold;
    letter-spacing: .5px;
}
/* ── 来料入库标签（IQC 判 OK 入库后打印；尺寸口径在 IncomingLabelRules.SizeMillimeters）──
   与批次标签的区别：批次标签一签一页（A4 上只贴一张）；来料标签按**现场热敏纸的真实 mm 尺寸**排版，
   一页能排几张就排几张（.incoming-label-sheet 用 flex 流式排列，不走 Bootstrap 的 .row/.col-*——
   那两个类在打印时被上面的规则改成了块布局，会把每张标签顶到独立一页）。

   ⚠⚠ 本节所有字号/行高/二维码边长**必须与 IncomingLabelRules.Metrics 逐项一致** ⚠⚠
   卡是**定尺**（热敏纸就那么大）且 overflow:hidden，装不下的内容会被**静默裁掉**（不报错、也不换行）。
   2026-09「格式优化」前的实测：大签溢出 8.8mm（丢约 2.3 行——暂收单号/入库日期整行消失）、
   中签溢出 24.8mm（丢约 6.5 行）、小签溢出 7.5mm（连批号都丢）。
   现在由 IncomingLabelRules.Budget() 计算"装不装得下"，并由 IncomingLabelRulesTests 与
   IncomingLabelStyleGuardTests 双重看守——改这里就要同步改那边。 */
.incoming-label-sheet {
    display: flex;
    flex-wrap: wrap;
    gap: 2mm;
    align-items: flex-start;
    justify-content: center;   /* 预览里一签居中（打印走块流布局，此属性不生效） */
}

.incoming-label-card {
    border: 1.5px solid #333;      /* 标签要剪下来贴，边框即剪切线 */
    padding: 2mm;
    display: flex;
    flex-direction: column;
    overflow: hidden;              /* 兜底：真实排版由预算保证装得下，这里防内容画出剪切线 */
    background: #fff;
    color: #000;
    font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
    page-break-inside: avoid;
    break-inside: avoid;
}

.incoming-label-card.size-lg { width: 100mm; height: 70mm; }
.incoming-label-card.size-md { width: 70mm; height: 50mm; }
.incoming-label-card.size-sm { width: 50mm; height: 30mm; }

/* 抬头：字号与 Metrics.TitlePt 对应。长公司名不许折行——折一行就吃掉一行的高度预算 */
.incoming-label-card .label-title {
    font-size: 8pt; font-weight: 700; text-align: center;
    border-bottom: 1px solid #333; padding-bottom: 0.5mm;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* 物料名：**单行 + 省略号**。物料名可长达几十字，允许折行会让高度不可预测
   （多一行 = 多约 4mm，正好把末尾两行挤出去），故宁可截断——物料编码在下一行完整可读。
   9.5pt（不是 10pt）：大签要装 11 行（含「含不合格」）时，10pt 会让余量掉到 0.5mm 以下，
   而热敏纸尺寸与打印机走纸都有误差——擦边通过等于迟早裁行。 */
.incoming-label-card .label-material {
    font-size: 9.5pt; font-weight: 700; text-align: center;
    margin-top: 0.5mm;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* 规格：**入库标签已不再使用**（规格统一由下面的「规　格」行承载，避免同一信息印两遍）。
   保留本节是因为**供货商送货标签**（SupplierDeliveryLabelRules / SupplierPortal.razor）
   复用同一套 .incoming-label-card 样式，它仍以居中规格行的形态呈现。 */
.incoming-label-card .label-spec {
    font-size: 7pt; text-align: center; color: #333;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.incoming-label-card .label-rows {
    flex: 1; font-size: 7.5pt; margin-top: 0.5mm; overflow: hidden;
}

/* 行：标签左、值右。值**不折行**（折行同样破坏高度预算），过长时省略号。
   min-width:0 是关键——flex 子项默认 min-width:auto，不给 0 则 text-overflow 不生效。 */
.incoming-label-card .label-row {
    display: flex; justify-content: space-between; gap: 1mm;
    border-bottom: 1px dotted #ccc; line-height: 1.20;
}
.incoming-label-card .label-row > span:first-child { color: #555; white-space: nowrap; flex: none; }
.incoming-label-card .label-row > span:last-child {
    text-align: right; flex: 1; min-width: 0;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* 页脚：左侧是二维码内容的可读副本，右侧二维码 */
.incoming-label-card .label-foot {
    display: flex; align-items: center; justify-content: space-between; gap: 1mm;
    border-top: 1px solid #eee; padding-top: 0.5mm;
    font-size: 6.5pt; color: #444;
}
.incoming-label-card .label-foot > span {
    min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.incoming-label-card .label-foot img { width: 17mm; height: 17mm; flex: none; }
.incoming-label-card.size-lg .label-foot img { width: 17mm; height: 17mm; }

/* ── 按档位显隐行（分档口径在 IncomingLabelRules.RowTier）──
   这是"装得下"的主要手段：与其让末尾几行被静默裁掉，不如显式声明"这一档就印这些"。
     · 大签 size-lg：core + standard + full（9~10 行）
     · 中签 size-md：core + standard（5~6 行）——暂收单号靠页脚仍在
     · 小签 size-sm：仅 core（批号 + 数量） */
.incoming-label-card.size-md .label-row.tier-full { display: none; }
.incoming-label-card.size-sm .label-row:not(.tier-core) { display: none; }
/* 兼容旧标记：早期渲染用 .secondary 表示"非核心行" */
.incoming-label-card.size-sm .label-row.secondary { display: none; }

/* 中签 70×50：整卡字号下调，与 Metrics 的 Medium 档逐项对应 */
.incoming-label-card.size-md .label-title { font-size: 7pt; }
.incoming-label-card.size-md .label-material { font-size: 9pt; }
.incoming-label-card.size-md .label-spec { font-size: 6.5pt; }
.incoming-label-card.size-md .label-rows { font-size: 7pt; }
.incoming-label-card.size-md .label-foot { font-size: 6pt; }
.incoming-label-card.size-md .label-foot img { width: 14mm; height: 14mm; }

/* 小签 50×30：只留批号/数量，字号最小档 */
.incoming-label-card.size-sm .label-title { font-size: 6.5pt; }
.incoming-label-card.size-sm .label-material { font-size: 8.5pt; }
.incoming-label-card.size-sm .label-rows { font-size: 6.5pt; }
.incoming-label-card.size-sm .label-row { line-height: 1.15; }
.incoming-label-card.size-sm .label-foot { font-size: 5.5pt; }
.incoming-label-card.size-sm .label-foot img { width: 10mm; height: 10mm; }

/* 来料标签的打印规则（flex → 块容器）已合并到文件上方「打印规则的唯一出处」那一节的 ⑤。 */

/* ── 锚点定位留白（帮助文档的「直达」按钮 / 目录跳转）──
   MainLayout 的 .top-row 是 height:3.5rem + position:sticky（顶栏），锚点滚到标题顶端时标题会被盖住。
   这里留 5rem（> 3.5rem，余 1.5rem 呼吸位）；浏览器原生锚点定位与脚本的 scrollIntoView 都认这个属性
   （见 wwwroot/js/anchor-scroll.js）。 */
.markdown-body :is(h1, h2, h3, h4, h5, h6)[id] {
    scroll-margin-top: 5rem;
}

/* 帮助文档里的插图（如手册附录 A 的流程图）：绝不顶破卡片，高度自适应；框线让白底插图与页面有边界。 */
.markdown-body img {
    max-width: 100%;
    height: auto;
    border: 1px solid var(--bs-border-color, #dee2e6);
    border-radius: .35rem;
    background: #fff;
}

/* ===== 全站移动适配（Bootstrap 5）=====
   手册 §25 的三档宽度（640.98px 侧边栏转横条 / 768px 文档抽屉 / 992px 设计器堆叠）由布局各自处理；
   本段补的是**工具类管不到**的四类问题（全部限定 screen，绝不参与打印版式）：

   ① 内联固定宽度：93 个页面共 289 处 style="width:NNNpx"（筛选框 220px 最常见）。内联 width 压不过
      max-width，所以窄屏只要给 max-width:100% 就会"缩到容器宽"而不是顶破卡片；对筛选类控件直接拉满一行，
      手指也更好点。
   ② Bootstrap 的 .d-flex 默认 flex-wrap:nowrap —— 工具条里的 ms-auto（靠右的模板/上传按钮）在手机上
      会被挤出可视区。窄屏改为可换行。
   ③ 触屏：.btn-sm/.form-control-sm 是桌面紧凑尺寸（≈31px），低于移动端 44px 手势目标；且 iOS 在字号
      <16px 的输入框上聚焦时会自动放大整页。用 pointer:coarse 一次性抬升，不动桌面鼠标端。
   ④ 弹窗：全站 114 处 modal-lg/xl 在手机上仍是卡片式，表单一长就要来回滚整页；窄屏改为全屏弹窗
      （等价 .modal-fullscreen-sm-down，但不必逐页改 114 个 modal）。 */

/* ① 窄屏（<576px）：控件与卡片不得溢出，内边距让位给内容 */
@media screen and (max-width: 575.98px) {
    .form-control, .form-select, .input-group, .card, .modal-body img {
        max-width: 100%;
    }
    .form-control[style*="width"], .form-select[style*="width"] {
        width: 100% !important;
    }
    .card-body { padding: .75rem; }
    .container-fluid { padding-left: .75rem; padding-right: .75rem; }
}

/* ② 窄屏（<768px，含手机横屏与小屏平板）：工具条换行 + 表格更紧凑（仍可左右滑动） */
@media screen and (max-width: 767.98px) {
    .d-flex.justify-content-between,
    .card-header .d-flex,
    .card-header .card-title,
    .modal-header .d-flex,
    .modal-footer {
        flex-wrap: wrap;
        gap: .5rem;
    }
    .table-responsive { -webkit-overflow-scrolling: touch; }
    .table > :not(caption) > * > * { padding: .35rem .45rem; }
}

/* ③ 触屏（手机/平板，含触屏笔记本）：把紧凑控件抬到 44px 手势目标，输入框字号 ≥16px */
@media screen and (pointer: coarse) {
    .btn-sm, .btn-group-sm > .btn { min-height: 2.5rem; }
    .form-control-sm, .form-select-sm { min-height: 2.5rem; font-size: 1rem; }
    .form-check-input { width: 1.15rem; height: 1.15rem; margin-top: .25rem; }
    .page-link {
        min-height: 2.5rem;
        min-width: 2.5rem;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }
}

/* ── 业务页控制面板（Components/Shared/PageControlPanel.razor）──
   原生 input[type=file] 的宽度是 100%，放进 flex 行会把「导入模板 + 选择文件」挤成两行
   （2026-09-21 用户实测截图：选择文件独占一行）。这里给动作区里的文件框固定宽度，
   保证「导入模板 与 选择文件」**同一行**显示；窄屏仍由 flex-wrap 正常换行。 */
.page-control-panel-actions input[type="file"] {
    width: 210px;
    flex: 0 0 auto;
}

/* ── 控制面板在**手机上**：过滤器要保住自带的紧凑宽度（2026-10-14；用现场手机截图换来）──────
   症状：上面 ① 那条"带内联宽度的控件一律 width:100% !important"**把面板里的过滤器也算进去了** ——
   产品页在 390px 上，`全部类别 / 全部单位 / 全部标签` 三个下拉被撑成一整行一个，
   加上搜索、按钮与文件框，工具栏涨到 6 行 ⇒ **列表首屏被推出屏幕外**，用户以为"没页面数据"。
   修法只有一句：把面板里的过滤器**排除在"一律 100%"之外**，让它们按自带宽度（130/120/140px）
   参与 flex 收缩 —— 三个正好并排一行，工具栏回到 3~4 行。
   ⚠ 实测（headless Edge，390px，真 bootstrap + 本文件）：改前面板 6 行；
     套上这条之后 5 行、且三个下拉并排（各 ~102px，仍可读）。
   ⛔ 别再"给过滤器/动作各套一层独占整行（flex: 1 1 100%）的容器" —— 试过，**反而更高**
      （每层都独占一行 ⇒ 210px / 6 行，比修前还差）；窄屏要的是**更紧**，不是把东西分行摆放。 */
@media screen and (max-width: 575.98px) {
    .page-control-panel .form-select[style*="width"],
    .page-control-panel .form-control[style*="width"] { width: auto !important; }

    /* 过滤器允许被压到更窄（不加这条，flex 的 min-width:auto 会让它们拒绝收缩、从而换行） */
    .page-control-panel .form-select,
    .page-control-panel .form-control { min-width: 0; }
}

/* ── 记录页智能按钮（Components/Shared/SmartButton.razor + SmartButtonBox.razor）──
   对齐 Odoo 表单的 button box：成组出现在状态栏与字段区之间，一块=图标 + 计数 + 名词。
   样式只做三件事：**成组排布**、**计数醒目**、**0 值弱化**（颜色不是唯一语义，文字始终在）。
   窄屏由 flex-wrap 换行——智能按钮在手机上不该横向滚动。 */
.smart-button-box {
    display: flex;
    flex-wrap: wrap;
    gap: .5rem;
    margin-bottom: .75rem;
}

.smart-button {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: .35rem .7rem;
    border: 1px solid var(--bs-border-color, #dee2e6);
    border-radius: .5rem;
    background: var(--bs-body-bg, #fff);
    color: var(--bs-body-color, #212529);
    text-decoration: none;
    line-height: 1.2;
    white-space: nowrap;
}

.smart-button:hover,
.smart-button:focus-visible {
    border-color: var(--bs-primary, #0d6efd);
    color: var(--bs-primary, #0d6efd);
}

/* 计数：等宽数字，位数变化时按钮宽度不跳 */
.smart-button-count {
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.smart-button-label {
    font-size: .875rem;
    color: var(--bs-secondary-color, #6c757d);
}

/* 0 值：**弱化但保留**——数字本身是"有没有关联记录"的信息，藏起来等于让用户以为没这项 */
.smart-button-empty {
    border-style: dashed;
    background: transparent;
}

.smart-button-empty .smart-button-count {
    font-weight: 400;
    color: var(--bs-secondary-color, #6c757d);
}

/* ④ 手机（<576px）：弹窗全屏 + 弹窗体自身滚动；页脚按钮拉满便于拇指点击 */
@media screen and (max-width: 575.98px) {
    .modal-dialog { margin: 0; max-width: none; min-height: 100%; }
    .modal-content { border: 0; border-radius: 0; min-height: 100vh; }
    .modal-body { max-height: calc(100vh - 8.5rem); overflow-y: auto; }
    .modal-footer > * { flex: 1 1 auto; }
}

/* ── 二次确认对话框（Components/Shared/ConfirmDialog.razor）──
   替代浏览器原生 confirm()：它由浏览器渲染（样式与站内脱节）且阻塞渲染线程。
   遮罩色与站内其它弹窗保持同一口径（rgba(0,0,0,0.5)）。 */
.app-confirm-backdrop {
    background-color: rgba(0, 0, 0, 0.5);
}
/* ── 通用弹窗外壳（Components/Shared/ModalShell.razor）──
   遮罩色与站内既有弹窗统一（原先 119 处各写一份，0.5 / 0.6 / 漏写都有）。
   为什么遮罩要吃掉点击：不吃的"弹窗"底下的列表仍然可点，用户会点到背后的行。 */
.app-modal {
    background-color: rgba(0, 0, 0, 0.5);
}
/* ── 站内提示条（Components/Shared/ToastHost.razor，交互批次 3）──
   为什么要有它：本仓 109 个页面各自维护一个 message / messageIsError 字段渲染成**常驻** alert，
   于是 ① 成功提示不消失（一直挂着，用户分不清"这一步成没成"）；② 失败会被下一条成功覆盖
   （同一字段承载两种语义，后写的盖掉前面的失败——"操作没成却没看见原因"的常见来源）。
   分工：成功自动消失、失败留在屏幕上（时长/角色/配色都在 ToastPolicy 里唯一实现）。

   右上角堆叠、最多 4 条（超出丢最旧的，新提示永远看得见）；z-index 高于弹窗（1055）与确认框，
   因为提示条可能在弹窗操作**之后**才出现（如"弹窗里保存成功"），被压住就等于没有。 */
.app-toast-host {
    position: fixed;
    top: 4.5rem; /* = 顶栏 3.5rem（MainLayout.razor.css 的 .top-row height）+ 1rem 呼吸位：
                    提示条**只从顶栏下沿开始**，绝不压住右上角的「退出 / 通知铃 / 用户名 / 管理」。 */
    right: 1rem;
    z-index: 1090;
    display: flex;
    flex-direction: column;
    gap: .5rem;
    max-width: min(420px, calc(100vw - 2rem));
    /* 竖直方向也钉在顶栏之下：堆到第 4 条时从**下沿往上**长，不会向上长回去盖住顶栏按钮。
       （2026-10-02 可用性批次：原先 top:1rem ⇒ 容器 16px~76px 与顶栏 0~56px 重叠 40px，
        「退出」正落在提示条的关闭按钮那一格；关掉再点同一点就是退出登录。） */
    max-height: calc(100dvh - var(--app-toast-top, 4.5rem) - 1rem);
    justify-content: flex-end;
    overflow: hidden;
    pointer-events: none; /* 宿主本身不吃点击，只有提示条自己吃（否则会挡住右上角的功能按钮） */
}

/* 窄屏（<641px）顶栏会换行、可能高过 3.5rem（侧边栏也转为横条），提示条整体再下移一档。
   唯一的位置变量就是 --app-toast-top：偏移与 max-height 的减数同源，改一处即可。 */
@media screen and (max-width: 640.98px) {
    .app-toast-host {
        --app-toast-top: 6.5rem;
    }
}

.app-toast {
    display: flex;
    align-items: flex-start;
    padding: .5rem .75rem;
    border-radius: .5rem;
    box-shadow: 0 6px 24px rgba(0, 0, 0, .22);
    font-size: .875rem;
    pointer-events: auto;
    /* 进度条绝对定位的参照系；overflow 裁掉圆角外的进度条 */
    position: relative;
    overflow: hidden;
}

.app-toast-text {
    flex: 1 1 auto;
    white-space: pre-wrap; /* 文案里带换行的（如"每行一个原因"）按原样折行 */
}

/* 剩余时间进度条（对齐 Odoo 通知的 `o_notification_progress`，见
   odoo/addons/web/static/src/core/notifications/notification.xml:25 与 notification.js:66-81）：
   宽度从 100% 线性走到 0，**时长由该条的 DismissMs 内联给出**（不在这里写死）。
   ⚠ 只有"会自动消失"的提示才渲染它（失败那条不自动消失，条子永远走不完＝假信息）。
   鼠标停住时由宿主的 `_frozen` 给它加 `.paused`——**与停表同步**：表停了条子也停，
   移开时宿主按整段时长重新计时，条子靠 `@key` 换新节点从 100% 重走（与官方 refresh() 同语义）。 */
.app-toast-progress {
    position: absolute;
    left: 0;
    bottom: 0;
    height: 2px;
    width: 0;
    background: currentColor;
    opacity: .55;
    animation-name: app-toast-countdown;
    animation-timing-function: linear;
    animation-fill-mode: forwards;
}

.app-toast-progress.paused {
    animation-play-state: paused;
}

@keyframes app-toast-countdown {
    from { width: 100%; }
    to { width: 0; }
}

/* ═══════════════════════════════════════════════════════════
   Chatter（活动记录 / 字段级审计、消息）长值换行
   ---------------------------------------------------------------
   问题：字段级审计里的旧值/新值常是 **哈希、JSON、无空格长串**；
   `.chatter` 内用了 `white-space: pre-wrap`（message bubble），
   而 pre-wrap **只在空格处断行** ⇒ 这类值会整行溢出、撑破卡片。

   规则：允许在长词内部断行（overflow-wrap:anywhere），
   并用 word-break:break-word 兼容旧内核。
   ⚠ 只作用于 .chatter 内部，不影响页面其它区域。
   ═══════════════════════════════════════════════════════════ */
.chatter-longvalue,
.chatter .card-body,
.chatter .chatter-entry {
    overflow-wrap: anywhere;
    word-break: break-word;
}

/* 审计明细里的等宽值（键/旧值/新值）额外允许横向滚动兜底，
   避免极长单个 token 在窄屏上仍被裁切。 */
.chatter code,
.chatter .font-monospace {
    overflow-wrap: anywhere;
    word-break: break-all;
    white-space: pre-wrap;
}

/* ── 站点 logo（.nav-logo）：跨布局统一定尺寸（2026-10-10 补） ──────────────────
   ⚠ 这条规则必须放在**全局** app.css：旧侧栏 CSS 里那份是**作用域** CSS（编译后带 [b-xxx] 属性选择器），
     只对 旧侧栏 自己生效；DocumentsLayout / PortalLayout 用同一个 class 时**拿不到**它 ⇒ logo 会按 SVG
     的原始尺寸渲染（撑破头部）。取值与 旧侧栏 CSS 的 .nav-logo 逐字一致，避免两处漂移。 */
.nav-logo {
    display: block;
    max-height: 40px;
    height: 40px;
    width: auto;
    object-fit: contain;
}

/* ══════════════════════════════════════════════════════════════════════════════
   模块外壳的**共用**品牌样式（内容门户 / 文档管理 / 仪表盘大屏三个顶栏）

   ⚠ 来源：对齐 `D:\DataWebRS`（本仓的并行副本，内容门户走得更前）的 `app.css` 同名段（上游 :582-655），
     逐字搬迁。唯一改动：注释里的素材口径 —— 上游写 `/assets/logo-navbar.png`，**本仓用 `logo.svg`**
     （矢量；且上游那套 .png 在本仓 `wwwroot/assets/` 下不存在，写 .png 恒为破图）。

   为什么放在这里（而不是各模块自己的 CSS）：品牌是**站点级**元素，三个外壳必须长得一样 ——
   它此前只在 `documents.css` / `website.css` 里各写一份 `.documents-logo` / `.website-logo`，
   同一件事两份口径；而 `旧侧栏 CSS` 的 `.nav-logo` 是**组件 scoped** 的
   （带 `b-xxxx` 属性选择器），⛔ 别处复制类名拿不到任何样式（本仓踩过这个坑）。
   模块**各自的配色**（链接色 / 选中态）仍留在各自的 CSS 里，见下。
   ══════════════════════════════════════════════════════════════════════════════ */

/* 站点标志：三个顶栏同一张 `/assets/logo.svg`（本仓口径；上游是 logo-navbar.png）。
   ⚠ `height` 必须与 `<img>` 的 `height` 属性成对：属性负责加载前占位（防 CLS），
     这里负责实际渲染尺寸。标志是横长图 ⇒ `width: auto` 不能省（写死会拉变形）。
   ★ 本批（2026-10-11 产品反馈："logo 太小"）：`1.75rem` → **`2.5rem`**（28px → 40px），
      `max-width` 同步 9rem → 12rem（40px 高时整图宽约 150px；9rem=144px 会把横长图压住并等比缩小 ⇒ 白放大）。
      为什么原来显得小：logo.svg 是 **750×200** 横长图，里面同时画了六边形标志 **和** 公司名文字，
      而六边形只占 x∈[30,170]、y∈[20,180]（≈全宽 19%、全高 80%）⇒ 整图按 28px 高渲染时
      **六边形实际只有 ~22px**，右边还挤着两行公司名。放大到 40px 后六边形约 32px，
      与右侧「内容门户」文字（1.0625rem 加粗）的大小关系才正常。
      ⚠ 代价：模块顶栏比原先高约 12px（仍远小于 旧侧栏 那个独立标志块，不会翻倍）。
      ⛔ 不要把 width 写死（会拉变形）。 */
.site-logo {
    display: block;
    height: 2.5rem;
    width: auto;
    max-width: 12rem;
    object-fit: contain;
}

/* 品牌链接（可点 ⇒ 回站点首页 `/`）。两个外壳都把它做成链接，行为必须一致：
   点标志 = 回 `http://<host>/`。⛔ 不要在一个外壳上指模块首页、另一个指站点首页。 */
.site-brand {
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    text-decoration: none;
    line-height: 1;
}

.site-brand:hover {
    opacity: .85;
}

/* 品牌文字：比正文大一档、加粗 —— 它应当是顶栏里视觉最重的元素。 */
.site-brand-text {
    font-size: 1.0625rem;
    font-weight: 600;
    letter-spacing: .01em;
}

/* 账号簇里的人名（`AccountCluster`）：窄屏只留图标 —— 长工号会把顶栏撑宽折行。 */
@media screen and (max-width: 575.98px) {
    .site-account-name {
        display: none;
    }
}

/* ── 模块导航：**下划线页签**（三个外壳同一形态）───────────────────────────────
   为什么不是 `nav-pills`：胶囊是"按钮"的语言。导航项的通用语言是"文字 + 选中下划线"，
   而且它在只有 1 项（内容门户）或 4 项（文档管理）时都不会显得空或挤。
   ⚠ 具体的链接色/选中色由各模块 CSS 覆盖（`.documents-nav` / `.website-nav`）。 */
.site-nav {
    gap: .25rem;
    flex-wrap: nowrap;
}

.site-nav .nav-link {
    padding: .25rem .5rem .35rem;
    border-bottom: 2px solid transparent;
    text-decoration: none;
}

.site-nav .nav-link:hover {
    border-bottom-color: var(--bs-border-color, #dee2e6);
}

/* `NavLink` 命中时自己加 `.active`（本仓既有形态），这里只定义"下划线"这一半。 */
.site-nav .nav-link.active {
    font-weight: 500;
}

/* ══ 扫码报工页的**摄像头扫码浮层**（手机/平板；2026-10-12 HTML5 扫码批）══════════
   为什么有这一块：工单卡/图纸卡上的二维码内容是**纯文本**（工单号 / 图号，不是网址）
   ⇒ 手机的系统相机与微信扫一扫只会显示一串字符，必须**在页面内**用摄像头扫。
   分工：标记与文案在 ReportScan.razor，**取流/解码/显隐**在 js/scan-report.js，
   这里只管长什么样。⛔ 它是屏幕 UI，纸面上绝不允许出现 ⇒ 末尾单独写打印隐藏。 */
.scan-camera {
    position: fixed;
    inset: 0;
    z-index: 1080;                  /* 高于 Bootstrap 模态遮罩（1055），低于 Toast */
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 12px;
    background: rgba(0, 0, 0, .88);
}

.scan-camera-box {
    width: min(560px, 100%);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
}

/* 取景框要**压在视频正中**：故两层结构（stage 定位、frame 绝对居中），
   ⛔ 不用负 margin 去凑（那种写法一改视频高度就错位）。 */
.scan-camera-stage { position: relative; width: 100%; }

/* object-fit: cover + 固定高度：手机竖屏下画面不拉变形，取景框落在中间 */
.scan-camera-video {
    display: block;
    width: 100%;
    height: min(58vh, 420px);
    background: #000;
    border-radius: 10px;
    object-fit: cover;
}

/* 取景框：**四角括号**（不是整圈方框）+ 框外轻压暗。
   为什么不用白框：纸是白的 —— 白框在白纸上等于没画（离线台截图实测：完全看不见），
   故改成高对比的琥珀色 + 深色描边（白纸与暗场景都看得见），四角也不遮住码本身。 */
.scan-camera-frame {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: min(240px, 62vw);
    aspect-ratio: 1 / 1;
    pointer-events: none;
    background:
        linear-gradient(#ffd666, #ffd666) 0 0 / 30px 3px no-repeat,
        linear-gradient(#ffd666, #ffd666) 0 0 / 3px 30px no-repeat,
        linear-gradient(#ffd666, #ffd666) 100% 0 / 30px 3px no-repeat,
        linear-gradient(#ffd666, #ffd666) 100% 0 / 3px 30px no-repeat,
        linear-gradient(#ffd666, #ffd666) 0 100% / 30px 3px no-repeat,
        linear-gradient(#ffd666, #ffd666) 0 100% / 3px 30px no-repeat,
        linear-gradient(#ffd666, #ffd666) 100% 100% / 30px 3px no-repeat,
        linear-gradient(#ffd666, #ffd666) 100% 100% / 3px 30px no-repeat;
    filter: drop-shadow(0 0 2px rgba(0, 0, 0, .65));
    box-shadow: 0 0 0 100vmax rgba(0, 0, 0, .10);
}

.scan-camera-tip,
.scan-camera-status {
    color: #fff;
    font-size: 13px;
    line-height: 1.5;
    text-align: center;
    word-break: break-word;
}

.scan-camera-status { color: #ffd666; }

.scan-camera-status:empty { display: none; }

/* 触屏/粗指针：关闭按钮必须好按（≥44px 手势目标）；这条与全站移动适配同口径 */
.scan-camera .btn { min-height: 44px; }

@media print {
    /* 打印时绝不进纸：本浮层只在屏幕上存在（.d-none 之外的这道是双保险） */
    .scan-camera { display: none !important; }
}

/* ══ 扫码反馈：闪屏（对齐 Odoo 的 screen flash；2026-10-13 对齐批）══════════════
   为什么要有它：扫码枪"到底扫上没有"必须**当场**能看出来 —— 现场嘈杂听不见提示音、
   戴手套摸不到震动、手机可能还在口袋里。三通道（音/震动/闪屏）里任何一条单独都会漏。
   成功=绿、失败=红；动画 260ms 由 JS 开关类触发（同类别连闪也能重放，见 scan-report.js 的强制回流）。 */
@keyframes scan-flash-success-kf {
    from { box-shadow: inset 0 0 0 100vmax rgba(25, 135, 84, .35); }
    to { box-shadow: inset 0 0 0 100vmax rgba(25, 135, 84, 0); }
}

@keyframes scan-flash-error-kf {
    from { box-shadow: inset 0 0 0 100vmax rgba(220, 53, 69, .35); }
    to { box-shadow: inset 0 0 0 100vmax rgba(220, 53, 69, 0); }
}

.scan-flash-success { animation: scan-flash-success-kf 260ms ease-out; }
.scan-flash-error { animation: scan-flash-error-kf 260ms ease-out; }

/* 取景浮层里的缩放缓块（摄像头支持 zoom 能力时才由 JS 插进来） */
.scan-camera-zoom {
    width: min(320px, 80%);
    accent-color: #ffd666;
}

/* 手电筒开关（摄像头声明支持 torch 能力时才由 JS 插进来，见 scan-report.js 的 attachTorch）。
   为什么值得单独一个按钮：车间/仓库光线暗是"扫不出来"的**第二大**原因（第一是糊），
   而这一步不需要解码器做任何事 —— 补光是最便宜的命中率提升。
   ⚠ `aria-pressed` 是开关状态的唯一判据（JS 同时改文本与它）：键盘/读屏也认得出。 */
.scan-camera-torch {
    min-width: min(200px, 60%);
    color: #1a1a1a;
    background: #ffd666;
    border-color: #ffd666;
}

.scan-camera-torch[aria-pressed="true"] {
    background: #fff3cd;
    border-color: #fff3cd;
}

/* ══ 扫码指令表（对齐 Odoo /stock_barcode/print_inventory_commands；2026-10-13 对齐批）═══════
   打印形态：A4 纵向一张纸，按分组排"二维码 + 中文名 + 码内容"三行一格。
   ⚠ 尺寸口径与标签一致：静区 ≥ 4mm（见上方标签那段的论证），格子间距留 6mm 便于剪开单贴。 */
.scan-command-sheet { font-size: 12px; }
.scan-command-sheet-title { font-size: 18px; font-weight: 700; text-align: center; }
.scan-command-sheet-sub { text-align: center; color: #555; margin-bottom: 8px; }
.scan-command-sheet-group {
    font-weight: 700; margin: 10px 0 4px; padding-bottom: 2px; border-bottom: 1px solid #333;
}
.scan-command-sheet-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6mm 4mm; }
.scan-command-sheet-cell { text-align: center; break-inside: avoid; page-break-inside: avoid; }
.scan-command-sheet-cell img { width: 22mm; height: 22mm; }
.scan-command-sheet-name { font-weight: 600; margin-top: 2px; }
.scan-command-sheet-code { font-family: ui-monospace, Consolas, monospace; font-size: 10px; color: #333; }

/* ⭐ 抽屉打开时锁滚动（F7，2026-10-13 由独立审核抓出）：
   这条原先写在 MainLayout.razor.css（**作用域**样式）里用了 `:global(body.nav-drawer-open)`，
   而 scoped CSS 会把它编成 `:global(body.nav-drawer-open)[b-xxxx]`：`:global` 不是 CSS 选择器、
   body 也没有作用域属性 ⇒ **规则从不生效**（手机端抽屉打开时背后还能滚）。
   ⚠ 凡是要作用到 body/html 的规则，一律写在**非作用域**样式表里。 */
body.nav-drawer-open {
    overflow: hidden;
}
