/* 锡山研究所 LIMS 二期（商务委托 + 合同管理）页面样式
   视觉基准取自 references/20260828/附件4-LIMS系统界面演示.html，不另行设计。 */
:root {
  --blue-700:#174a70; --blue-600:#1f5e8c; --blue-500:#2878b5; --blue-100:#ddecf6; --blue-50:#f0f7fb;
  --teal:#16847a; --teal-light:#e5f4f1; --ink:#172033; --text:#344054; --muted:#667085; --weak:#98a2b3;
  --border:#d0d5dd; --line:#e4e7ec; --bg:#f7f9fb; --surface:#fff; --success:#16805b; --success-bg:#e9f6f0;
  --warning:#b76e00; --warning-bg:#fff4dd; --danger:#c33a32; --danger-bg:#fcecea; --info:#2563a9; --info-bg:#eaf2fb;
  --shadow:0 16px 44px rgba(23,32,51,.14); --focus:0 0 0 3px rgba(40,120,181,.18);
  --form-control-height: 36px;
  /* 页面级浮层层级（2026-10-22，M200）。全站 z 序的档位表见 docs/z-index-contract.md §2.1，
     本变量是那一表里「页面级浮层面板」档（L5′）的取值，写成变量是为了让 form-kit 能在
     **控件这一处** scoped 覆盖 vendor 的 --choices-z-index，而不是各处写死数字。
     取 45：压过侧栏(40)、顶栏(30)、吸顶表头(3)，仍低于演示入口(90)、弹窗(100)、提示(150)。
     弹窗内与表头筛选行里的下拉**不取这一档**（契约 §4.2-A/C），它们仍走 vendor 默认的 1。 */
  --z-page-panel:45;
  /* 错误态光晕（2026-10-05，M83）：与 --focus 同形状同浓度，只换色相到 --danger(#c33a32)。
     校验失败时红框配蓝光晕看着不协调（用户原话：「整个颜色就变红了，但是光晕还是蓝颜色的」），
     光晕一并转红，错误清除后回到 --focus。--danger 在门户主题下不覆写，故这支出的是定值。 */
  --focus-danger:0 0 0 3px rgba(195,58,50,.18);
  /* select 基座三角（2026-09-30）：内嵌 SVG，换主题只改这两个变量，不引任何依赖 */
  --select-arrow:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 7'%3E%3Cpath d='M1 1.4L6 5.5 11 1.4' fill='none' stroke='%23667085' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --select-arrow-off:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 7'%3E%3Cpath d='M1 1.4L6 5.5 11 1.4' fill='none' stroke='%23aab2bf' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --select-arrow-size:12px 7px;
  --select-arrow-x:right 10px center;
  --select-arrow-gap:28px;
  /* 侧栏/登录页主题变量：默认 = 所内蓝；门户页 body.theme-portal 覆写为绿色系（录屏切身份一眼可辨） */
  --side-bg:#102f49;
  --side-grad-a:#102f49; --side-grad-b:#174a70; --side-grad-c:#1f5e8c;
  --side-text:#c9d6df; --side-soft:#b9ccda; --side-muted:#8fb8d4;
  --side-line:rgba(255,255,255,.12); --side-hover:rgba(255,255,255,.1);
  --side-badge-bg:rgba(255,255,255,.14); --side-badge-text:#d9e7f2;
  --side-active-dot:#73b6e8;
}
/* 客户门户主题：绿色系 */
body.theme-portal {
  --blue-700:#0c5a41; --blue-600:#128263; --blue-500:#199f78; --blue-100:#d6f0e5; --blue-50:#edfaf3;
  --side-bg:#0e3d2f;
  --side-grad-a:#0e3d2f; --side-grad-b:#135842; --side-grad-c:#1a7454;
  --side-text:#c4e2d4; --side-soft:#a9d4c2; --side-muted:#7fb9a2;
  --side-badge-text:#d8efe4;
  --side-active-dot:#4fcca0;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.55 "Noto Sans SC","Microsoft YaHei","PingFang SC",system-ui,-apple-system,sans-serif}
button,input,select,textarea{font:inherit;color:inherit}
button{cursor:pointer}
button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:none;box-shadow:var(--focus)}
a{color:var(--blue-600)}

/* 框架：侧边导航 + 顶栏 */
.app-shell{min-height:100vh}
.sidebar{position:fixed;top:0;bottom:0;left:0;width:236px;background:var(--side-bg);color:#fff;padding:22px 16px;display:flex;flex-direction:column;z-index:40;overflow-y:auto}
.brand{display:flex;align-items:center;gap:12px;padding:0 8px 20px;border-bottom:1px solid var(--side-line)}
.brand-mark{display:grid;place-items:center;width:38px;height:38px;border-radius:8px;background:#fff;color:var(--blue-700);font-weight:700;font-size:18px;flex:none}
.brand strong,.brand small{display:block}
.brand strong{font-size:15px;letter-spacing:.02em}
.brand small{margin-top:2px;color:var(--side-soft);font-size:11px}
.side-nav{display:grid;gap:4px;margin-top:18px}
.nav-group-label{padding:12px 12px 4px;font-size:10px;letter-spacing:.12em;color:var(--side-muted)}
.side-nav a{display:flex;align-items:center;gap:11px;padding:9px 12px;color:var(--side-text);text-decoration:none;border-radius:6px;transition:.16s ease;font-size:13px}
.side-nav a:hover,.side-nav a.active{background:var(--side-hover);color:#fff}
.side-nav a .nav-badge{margin-left:auto;background:var(--side-badge-bg);color:var(--side-badge-text);font-size:10px;padding:1px 7px;border-radius:99px}
.side-nav a .nav-external{margin-left:auto;display:inline-flex;align-items:center;flex:none;opacity:.7}
.side-nav a:hover .nav-external,.side-nav a.active .nav-external{opacity:1}
.nav-dot{width:7px;height:7px;border:1px solid currentColor;border-radius:50%;flex:none}
.side-nav a.active .nav-dot{background:var(--side-active-dot);border-color:var(--side-active-dot)}
.sidebar-note{margin-top:auto;padding:14px;border:1px solid var(--side-line);border-radius:8px;color:var(--side-text)}
.sidebar-note span{font-size:10px;letter-spacing:.12em;color:var(--side-muted)}
.sidebar-note p{margin:7px 0 0;font-size:12px}
.main{margin-left:236px;min-width:0}
.topbar{position:sticky;top:0;z-index:30;height:64px;background:rgba(255,255,255,.96);border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 28px}
.breadcrumb{color:var(--muted);font-size:13px}
.breadcrumb span{padding:0 8px;color:var(--weak)}
.breadcrumb b{color:var(--ink);font-weight:600}
.lims-home-link{color:inherit;text-decoration:none}
.topbar>.lims-home-link-mobile{display:none}
.lims-home-link:hover{text-decoration:underline}
.lims-home-link:focus-visible{outline:2px solid var(--blue-600);outline-offset:3px;border-radius:2px}
.lims-home-link-login{display:block;width:max-content;margin:0 0 12px auto;color:var(--blue-600);font-size:12px}
.top-actions{display:flex;align-items:center;gap:12px}
.profile{display:flex;align-items:center;gap:8px;padding-left:3px}
.profile strong,.profile small{display:block}
.profile strong{font-size:12px}
.profile small{font-size:10px;color:var(--muted)}
.avatar{width:34px;height:34px;border-radius:50%;background:var(--blue-100);color:var(--blue-700);display:grid;place-items:center;font-weight:600}

/* 内容区 */
.content{max-width:1480px;margin:0 auto;padding:28px 32px 72px}
.page-head{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;margin-bottom:18px}
.page-head h1{margin:0;font-size:22px;line-height:1.3}
.page-head .kicker{margin:0 0 6px;color:var(--blue-600);font-size:10px;letter-spacing:.14em;font-weight:700}
.page-head p{margin:6px 0 0;color:var(--muted);font-size:13px}
.page-actions{display:flex;gap:9px;flex:none}
.panel{background:#fff;border:1px solid var(--line);border-radius:8px;padding:18px}
.panel-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;gap:12px}
.panel-heading>div{display:flex;align-items:center;gap:8px;min-width:0}
.panel-heading h3{margin:0;font-size:15px}
.panel-heading .muted{color:var(--muted);font-size:11px}
.count-badge{background:var(--blue-50);color:var(--blue-700);font-size:10px;padding:2px 7px;border-radius:99px}
.count-badge.warn{background:var(--warning-bg);color:var(--warning)}
.grid{display:grid;gap:14px;margin-top:14px}
.grid.two{grid-template-columns:1.6fr 1fr}

/* 按钮 */
.button{height:36px;border-radius:6px;padding:0 14px;border:1px solid transparent;font-weight:500;transition:.15s ease;font-size:13px}
.primary-button{background:var(--blue-600);color:#fff;border-color:var(--blue-600)}
.primary-button:hover{background:var(--blue-700)}
.secondary-button{background:#fff;color:var(--text);border-color:var(--border)}
.secondary-button:hover{border-color:#9aabb9;background:#fbfcfd}
.ghost-button{background:transparent;color:var(--blue-600)}
.ghost-button:hover{background:var(--blue-50)}
.danger-button{background:#fff;color:var(--danger);border-color:#e5aaa6}
.danger-button:hover{background:var(--danger-bg)}
.button:disabled{opacity:.48;cursor:not-allowed}
.text-button{border:0;background:transparent;color:var(--blue-600);font-size:12px;padding:4px}
.text-button:hover{text-decoration:underline}
.small-button{height:28px;padding:0 10px;font-size:12px;border-radius:5px}

/* 表格 */
.table-wrap{overflow:auto}
table{width:100%;border-collapse:collapse;min-width:760px}
th{padding:9px 10px;background:#f8fafb;color:var(--muted);font-size:11px;font-weight:500;text-align:left;border-block:1px solid var(--line);white-space:nowrap}
td{padding:11px 10px;border-bottom:1px solid var(--line);color:var(--text);font-size:12px}
td strong,td small{display:block}
td strong{font-size:12px;color:var(--ink);font-weight:600}
td small{color:var(--muted);font-size:10px;margin-top:2px}
.number{text-align:right;font-variant-numeric:tabular-nums}

/* ── 粘性表头（2026-10-03 M86）──────────────────────────────────────────────
   吸顶 64px 让开 .topbar（本表:64，高 64px）；z-index 取 3——高于 .modal-header/.modal-footer 的 2，
   低于 .topbar 的 30，三层互不穿插。表头自带 #f8fafb 不透明底色，粘住时不会透出行内容。

   粘的是 <thead> 整体而不是单个 <th>：表头第二行（筛选行，§2.6）也必须跟着吸住，
   否则滚下去筛选控件就没了、而它的全部价值正是「边滚边筛」。<thead> 的 sticky 从 Chrome 91 /
   Safari 13.4 / Firefox 59 起可用，比本表已在用的 :has() 基线还早，够用。

   为什么必须连带摘掉 .table-wrap 的 overflow：sticky 的参照系是**最近的滚动容器**，
   .table-wrap{overflow:auto} 就是那个容器；它自己纵向不滚，表头相对它算出来的偏移恒为 0，
   于是照抄一条 th{top:64px} 是彻底的空操作（Chromium 实测，60 行长表）：
   有包裹层时滚过头表头 1000px，表头停在 −308（随页面走）；无包裹层时吸在 64px。
   `overflow-x:auto; overflow-y:clip` 也救不了——X 向 auto 仍使它成为滚动容器。

   断点 1140px 的来历：内容区净宽 = 视口 − 236（侧栏）− 64（.content 左右内边距）
   − 36（.panel 左右内边距）= 视口 − 336 ≥ 804px，已经大于 table 的基础 min-width:760px。
   「基础」两个字是留给下面两条豁免的：**少数表自己声明了比包裹层更宽的 min-width**
   （横向滚动是它们的设计），这些页在各自的页面表里把 overflow 加回去，见
   assets/business.css（委托进度总览）、assets/samples.css（样品台账）、assets/testing.css（资源占位台账的
   多列栅格卡片）。通用判据 CSS 量不出来——:has() 读不到渲染后的内容宽，只能按页登记；
   新增更宽的表时照那三处的写法登记一行即可。 */
@media(min-width:1140px){
  .table-wrap{overflow:visible}
  .table-wrap>table>thead{position:sticky;top:64px;z-index:3}
  /* 弹窗里的表格**两个半边都要豁免**，缺一个就把问题挪到别处：
     ① thead 交回 static——.modal 自己 overflow-y:auto（本表弹窗段），那里的表头会相对弹窗滚，
        而 .modal-header 是 sticky z-index:2，再压一个 z-index:3 的表头就会盖住弹窗标题；
     ② overflow 交回 auto——.modal 的 overflow-y:auto 会把 overflow-x 的计算值一并变成 auto，
        于是包裹层不兜底之后，比弹窗宽的表（拆分入库 .split-items-table 1220px / 弹窗 880px、
        处置记录 .disposal-table 980px / 弹窗 865px）就把横向滚动交给了弹窗本体。
        而 .modal-header / .modal-footer 都只吸纵轴——横着一滚，弹窗标题和「提交处置 / 保存草稿」
        一起移出视野。基线上这两处是被包裹层兜住的，摘掉 overflow 就必须显式加回来。
        弹窗内的宽表一律由这一条兜住，不用像页级那样逐页登记。 */
  .modal .table-wrap{overflow:auto}
  .modal .table-wrap>table>thead{position:static}
}
/* picker 选项面板能不能宽过控件本体（M92）：本表在这里只提供 form-kit 那条逐实例判定
   （findPanelBound：往上找第一个横向裁剪的祖先，面板不许越过它的可视盒）所依赖的环境事实。

   · ≥1140 的 .table-wrap 是 overflow:visible —— 这是表头筛选行里的下拉能超控件的**前提**。
     上面提到的三处「把 auto 加回去」的逐页登记因此也是面板宽度的分界线：登记进去的表，
     面板回到 100%、不宽出去。这不是取舍，是裁剪环境下的必然（宽出去只会被切掉）。
   · <1140 的 .table-wrap 仍是 auto —— 面板以包裹层的可视宽为界：控件右边还有位置就仍能
     超控件（实测委托单管理在 1139/1000 宽出 68/70px），没有位置就退回控件宽、与改动前同形。
     两种形态都不出现截断；包裹层本身可横向滚动，用户自己横向滑过去仍能看全。
   · .modal 自己 overflow-y:auto（按 CSS 规定 overflow-x 的计算值一并变 auto）——弹窗内的
     picker（含弹窗里的表格筛选）以弹窗为界：在弹窗可视宽内可以超控件，出不了弹窗。
   · 窄视口（innerWidth ≤ 900）表头筛选行降级出来的 .filter-head-bar 挂在 .table-wrap
     **外面**，它的下拉因此不受包裹层裁剪，按视口算——见下方 .filter-head-bar 一段。

   本表刻意不再给 .choices__list--dropdown 写任何宽度规则：写在这里就绕不开「哪个页面把
   overflow 加回去了」的逐页登记，而那是 CSS 量不出来的东西（:has() 读不到渲染后的内容宽）。
   宽度规则随引擎 CSS 一起注入，只在 form-kit 判过之后才生效，见 form-kit.js PARITY_STYLE ⑥。 */
/* 打印：表头筛选行是交互控件，打出来是一排空框。整行摘掉，数据表头不受影响 */
@media print{.filter-head-row{display:none}}

/* 状态标签 */
.status{display:inline-flex;align-items:center;gap:5px;padding:3px 8px;border-radius:999px;font-size:11px;font-weight:500;white-space:nowrap}
.status:before{content:"";width:5px;height:5px;border-radius:50%;background:currentColor}
.status.info{color:var(--info);background:var(--info-bg)}
.status.success{color:var(--success);background:var(--success-bg)}
.status.warning{color:var(--warning);background:var(--warning-bg)}
.status.danger{color:var(--danger);background:var(--danger-bg)}
.status.neutral{color:var(--muted);background:#f0f2f5}
.status.teal{color:var(--teal);background:var(--teal-light)}

/* 页内标签 */
.tabs{display:flex;gap:4px;border-bottom:1px solid var(--line);margin-bottom:16px;flex-wrap:wrap}
/* 内联 display:grid 会压过 [hidden] 的 UA 规则，这里显式兜住（会签意见输入框） */
#cd-sign-remark-wrap[hidden]{display:none}
.tabs button{border:0;background:transparent;padding:9px 14px;font-size:13px;color:var(--muted);border-bottom:2px solid transparent;margin-bottom:-1px}
.tabs button:hover{color:var(--ink)}
.tabs button.active{color:var(--blue-700);border-bottom-color:var(--blue-600);font-weight:600}
.tab-page{display:none}
.tab-page.active{display:block}
/* 门户页切换（与所内 .tab-page 同一套规则） */
.page{display:none}
.page.active{display:block}

/* 待办任务列表 */
.task-list{display:grid}
.task-row{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:12px;border:0;border-top:1px solid var(--line);background:#fff;padding:11px 2px;text-align:left;width:100%}
.task-row:first-child{border-top:0}
.task-row:hover{background:#fbfcfd}
.task-type{width:38px;height:38px;border-radius:7px;display:grid;place-items:center;font-size:11px;font-weight:600;flex:none}
.task-type.blue{background:var(--info-bg);color:var(--info)}
.task-type.amber{background:var(--warning-bg);color:var(--warning)}
.task-type.teal{background:var(--teal-light);color:var(--teal)}
.task-content strong,.task-content small,.task-meta b,.task-meta small{display:block}
.task-content strong{font-size:13px;font-weight:600}
.task-content small,.task-meta small{color:var(--muted);font-size:10px;margin-top:2px}
.task-meta{text-align:right}
.task-meta b{color:var(--blue-600);font-size:11px;font-weight:600}

/* 步骤条 */
.steps{display:flex;align-items:flex-start;margin:6px 0 2px}
.step{flex:1;text-align:center;position:relative;padding-top:2px}
.step:before{content:"";position:absolute;top:13px;left:-50%;right:50%;height:1px;background:var(--border);z-index:0}
.step:first-child:before{display:none}
.step.done:before{background:var(--blue-500)}
.step .step-dot{position:relative;z-index:1;width:26px;height:26px;margin:0 auto;border-radius:50%;border:1px solid var(--border);background:#fff;color:var(--muted);display:grid;place-items:center;font-size:11px}
.step.done .step-dot{background:var(--blue-600);border-color:var(--blue-600);color:#fff}
.step.active .step-dot{border-color:var(--blue-600);color:var(--blue-700);box-shadow:var(--focus);font-weight:700}
.step .step-label{margin-top:7px;font-size:11px;color:var(--muted)}
.step.done .step-label,.step.active .step-label{color:var(--ink)}
.step .step-sub{font-size:10px;color:var(--weak);margin-top:1px}

/* 时间线 */
.timeline{display:grid;gap:0}
.timeline-item{display:grid;grid-template-columns:120px 18px 1fr;gap:10px;padding:10px 0;border-top:1px solid var(--line);font-size:12px}
.timeline-item:first-child{border-top:0}
.timeline-item .t-time{color:var(--muted);font-size:11px;white-space:nowrap}
.timeline-item .t-dot{width:9px;height:9px;border-radius:50%;background:var(--blue-500);margin:4px auto 0;position:relative}
.timeline-item .t-body strong{font-size:12px;font-weight:600;color:var(--ink)}
.timeline-item .t-body small{display:block;color:var(--muted);font-size:10px;margin-top:2px}

/* 提示条 */
.inline-note{display:flex;gap:9px;align-items:flex-start;padding:10px 12px;border-radius:6px;background:var(--info-bg);color:var(--info);font-size:12px;margin-bottom:14px}
.inline-note.warning-note{background:var(--warning-bg);color:var(--warning)}
.inline-note .n-icon{width:18px;height:18px;border:1px solid currentColor;border-radius:50%;display:grid;place-items:center;font-size:10px;flex:none;font-style:normal}
.inline-note p{margin:0}
.inline-note b{font-weight:600}

.modal-backdrop{position:fixed;inset:0;background:rgba(10,24,36,.46);display:grid;place-items:center;padding:24px;z-index:100}
.modal-backdrop[hidden]{display:none}
/* 弹窗：标题与页脚常驻，正文照旧滚动（2026-09-30 全站推广）。
   .modal 仍是滚动容器，头脚用 sticky 顶住上沿 / 踩住下沿：原来整窗滚动，标题和「保存 / 取消」
   跟着内容一起卷走，表单一长就得先滚到底才看得见按钮。
   刻意不做「.modal 变 flex 纵向 + .modal-body 自己滚」——那样 .modal-body 变成裁剪祖先，
   两处会坏，且都在本表改不到的文件里：
   ① 弹窗内的 formkit-picker 检索面板（testing/registry、training 等）是绝对定位，
      贴底时会被裁掉（实测：面板底部命中 .modal-footer 而不是选项行）。
      **量级订正（2026-10-05，M95）**：这里原来写「面板最高 200px」，那是已退役的自绘面板
      .formkit-picker-panel 的值——渲染引擎换成 choices.js 之后实测外框恒为 302px
      （内层列表被 choices.min.css 封在 max-height:300px，选项数不改变它：8 项与 48 项同值），
      量级差 50%，照旧数会低估一截半。**注意越界不是被 200px 撑出来的，是引擎按 window
      视口判翻向、不知道上游有这两条 sticky**：净空带比视口浅 86+65=151px，于是它在
      「其实已经压到页脚」的位置上仍判下方放得下。修法在引擎层（form-kit 的
      PARITY_STYLE ⑦ / fitPanelBandCap：开面板时按本弹窗实量净空带，把面板封到带内），
      与本表这条 overflow 的取舍互为前提——sticky 不动裁剪关系，越界就改由 JS 收进带内；
      两边都还原成 200px 那套自绘规则则此条重新成立。
   ② modal-busy 的遮罩是挂在 .modal-body 上的 absolute inset:0，body 一旦成为滚动容器，
      遮罩就跟内容一起滚，滚下去下半截表单露在外面（实测：滚动后命中 .form-busy-host 本身）。
   sticky 方案里裁剪关系与遮罩挂载点都不变，这两处保持原样。 */
.modal{width:min(720px,100%);max-height:calc(100vh - 80px);overflow-y:auto;background:#fff;border-radius:9px;box-shadow:var(--shadow);animation:modal-in .18s ease}
.modal.modal-wide{width:min(880px,100%)}
@keyframes modal-in{from{opacity:0;transform:translateY(8px) scale(.99)}to{opacity:1;transform:none}}
.modal-header{position:sticky;top:0;z-index:2;display:flex;justify-content:space-between;align-items:flex-start;padding:20px 22px 16px;border-bottom:1px solid var(--line);background:#fff}
.modal-header h2{margin:0;font-size:18px}
.modal-header .kicker{margin:0 0 6px;color:var(--blue-600);font-size:10px;letter-spacing:.14em;font-weight:700}
.close-button{width:32px;height:32px;border:0;background:transparent;border-radius:5px;color:var(--muted);font-size:22px;line-height:1}
.close-button:hover{background:#f2f4f7}
.modal-body{padding:20px 22px}
.modal-footer{position:sticky;bottom:0;z-index:2;display:flex;justify-content:flex-end;gap:9px;padding:14px 22px;border-top:1px solid var(--line);flex-wrap:wrap;background:#fff}

/* 弹窗纵向堆栈（五个 *-common.js 里的同名块，2026-09-30）
   后台层向上位移、逐层缩小、只留 .modal-header，消失点在窗口上方中央。
   位移挂在 .modal-backdrop 而不是 .modal 上：.modal 自己的 modal-in 入场动画也写 transform，
   两者写同一个属性就会互相顶掉（老抽屉堆栈踩过，见 specs/20260625 计划 §2.4 注 3）。
   消失点落在 transform-origin: top center——.modal-backdrop 是 inset:0 的整屏固定层，
   它的顶边中点就是视口上方正中。三个档位变量都按 depth 线性推，JS 只写 --stack-depth 一个整数。

   排版上有一处非对称：后台层顶对齐（align-items:start）排在视口顶部，栈顶整体让出 --stack-band
   的条带往下挪。否则后台卡片会整个躲在栈顶那张大卡片背后——后台层的 z-index 更低、又是白色不透明的，
   居中对齐时谁也看不见谁。条带高度不是写死的数：后台卡片的 z-index 抬到了栈顶整屏遮罩之上，
   一旦压过栈顶上沿就是真的盖住栈顶文字（写死 --stack-gap 时八个页面恒定压 12.1px，testing/tasks
   压 32.6px 并盖掉 kicker——那些页面标题条更高）。改成由 JS 量出后台标题条最高高度写进
   --stack-hdr，条带在这里按它算；没有后台层时不写 data-stack-behind，padding-top 仍是 24px、
   max-height 仍是 100vh-80px，与改动前逐像素一致。 */
.modal-backdrop[data-stack-depth]{
  --stack-layer-y:-22px;      /* 每深一层向上位移 */
  --stack-layer-scale:.945;   /* 每深一层缩小到 */
  --stack-layer-opacity:.94;  /* 每深一层淡到 */
  --stack-fan-top:64px;       /* 后台层顶对齐的落点：距视口顶多少 */
  --stack-hdr:0px;            /* 后台 .modal-header 的最高高度，由 JS 量出来写 */
  --stack-clear-base:40px;    /* 栈顶顶到 max-height 时的上沿，见下方推导 */
  --stack-clear-margin:10px;  /* 条带之外再留的安全间距 */
  --stack-depth:0;
  --stack-y:calc(var(--stack-layer-y) * var(--stack-depth));
  --stack-scale:calc(1 - (1 - var(--stack-layer-scale)) * var(--stack-depth));
  --stack-opacity:calc(1 - (1 - var(--stack-layer-opacity)) * var(--stack-depth));
  /* 后台层最深的可见底边。取 depth=1 的式子当上界即可：depth=2 的 scale 更小、位移更负，
     同样高度下必然落在 depth=1 之下（.945 > .89，-22 > -44），故一个式子覆盖所有可见后台层。 */
  --stack-fan-bottom:calc((var(--stack-fan-top) + var(--stack-hdr)) * var(--stack-layer-scale) + var(--stack-layer-y));
  --stack-band:max(0px, var(--stack-fan-bottom) - var(--stack-clear-base) + var(--stack-clear-margin));
  transform-origin:top center;
  transform:translateY(var(--stack-y)) scale(var(--stack-scale));
  opacity:var(--stack-opacity);
  z-index:calc(100 - var(--stack-depth));
  transition:transform .22s cubic-bezier(.2,.7,.3,1),opacity .22s ease,padding-top .22s cubic-bezier(.2,.7,.3,1)
}
/* 栈顶：无后台层时与基座逐像素一致；有后台层（data-stack-behind 由 JS 写）才让条带 + 等量收高。
   --stack-clear-base 40px 的来历：栈顶 padding-top = 24 + band，内容盒高 = 100vh - 48 - band，
   而 max-height = 100vh - 80 - band，于是卡片顶到 max-height 时上沿 = 24 + band + (32)/2 = 40 + band。
   band 因此不自反馈——它无论多大，栈顶上沿都恰好是 40 + band，条带算出来就是最终位置。 */
.modal-backdrop[data-stack-depth="0"]{padding-top:24px}
.modal-backdrop[data-stack-depth="0"][data-stack-behind]{padding-top:calc(24px + var(--stack-band))}
/* 等量收高：否则 max-height 顶到 100vh-80px 的长表单会被推出视口下沿，页脚（sticky 踩底）看不见了。
   !important 只为压过 business.css:63 的 #modal-new-quote .modal / #modal-quote-detail .modal
   （特异度 1-2-0，类选择器堆多少都压不过 ID）。 */
.modal-backdrop[data-stack-depth="0"] .modal{max-height:calc(100vh - 80px)!important}
.modal-backdrop[data-stack-depth="0"][data-stack-behind] .modal{
  max-height:calc(100vh - 80px - var(--stack-band))!important
}
/* 视觉上限 3 层（含顶层）：再深的层留白成雾，看不出是第几张，但仍留在栈里，Esc 照样能逐层退回 */
.modal-backdrop[data-stack-deep]{opacity:0;pointer-events:none}
/* 后台层只露标题条：正文与页脚收成 0 高，.modal 随即缩到只剩 .modal-header。
   max-height 刻意不做成 0→auto 的过渡：auto 不可插值，要过渡就得给正文一个上界，
   而正文高度无上界（长表单全靠 .modal 自己滚），钉一个数等于给表单设高度上限。
   改用 opacity 收放：收起是瞬时裁剪（本来就在 transform 过渡里），展开时晚 80ms 淡入，落在卡片归位时。 */
.modal-backdrop[data-stack-depth]:not([data-stack-depth="0"]) .modal-body,
.modal-backdrop[data-stack-depth]:not([data-stack-depth="0"]) .modal-footer{
  min-height:0;max-height:0;overflow:hidden;opacity:0;
  padding-top:0;padding-bottom:0;border-top-width:0;border-bottom-width:0;
  transition:opacity .16s ease
}
.modal-backdrop[data-stack-depth="0"] .modal-body,
.modal-backdrop[data-stack-depth="0"] .modal-footer{transition:opacity .18s ease .08s}
/* 后台层弱化并抬到栈顶遮罩之上：顶对齐排在视口顶部、遮罩淡下来（几层 0.46 叠在一起会越叠越黑）、阴影收浅。
   z-index 必须抬过栈顶那张整屏 .modal-backdrop，否则它的标题条虽然 pointer-events:auto 也点不到——
   被栈顶的整屏遮罩盖住了。抬起来的前提是后台卡片绝不与栈顶卡片重叠，由上面那条带 + 等量收高保证。
   depth=3（data-stack-deep）算出来正好是 100、与栈顶遮罩同层：今天无害——那一档 opacity:0 且
   pointer-events:none；动这两条之前先想清楚。
   后台卡片空白处一律不接收点击：收敛到标题条才点得到，别把「关掉」摊给看不见的地方。 */
.modal-backdrop[data-stack-depth]:not([data-stack-depth="0"]){
  align-items:start;padding-top:var(--stack-fan-top);pointer-events:none;
  z-index:calc(103 - var(--stack-depth));background:rgba(10,24,36,.14)
}
.modal-backdrop[data-stack-depth]:not([data-stack-depth="0"]) .modal{box-shadow:0 10px 26px rgba(23,32,51,.13)}
.modal-backdrop[data-stack-depth]:not([data-stack-depth="0"]) .modal-header{pointer-events:auto;cursor:pointer}
/* 表单栅格与字段层（docs/form-rendering-standard.md §1.3 / §1.5 / §2.1 / §2.2 / §2.3，2026-09-28 解冻）
   D-布局：默认单列；只有显式声明并排的表单（form-kit 的 colSpan → data-cols="2"）才回到两列
   D-勾选：checkbox / radio / file 不再继承文本框几何（全宽 × 36px 高） */
.form-grid{display:grid;grid-template-columns:1fr;gap:12px}
.form-grid[data-cols="2"]{grid-template-columns:1fr 1fr}
.form-grid label{display:grid;gap:5px;color:var(--text);font-size:11px;padding-left:10px}
.form-grid label.wide{grid-column:1/-1}
.form-grid .fk-col-full{grid-column:1/-1}
.form-grid input,.form-grid select,.form-grid textarea{width:100%;border:1px solid var(--border);border-radius:6px;background:#fff;padding:8px 10px;outline:none;font-size:12px}
.form-grid input,.form-grid select{height:var(--form-control-height)}
/* 多行文本框不可拖拽改大小（2026-10-05，M96）：用户原话「系统内产生的多行文本框不能改变大小，
   其他不管」。只改这一个声明，.form-grid 之外不涉；高度仍由 rows / min-height:80px 决定，
   文本超出时框内滚动（overflow 默认 auto）。 */
.form-grid textarea{min-height:80px;resize:none}
.form-grid input:focus,.form-grid select:focus,.form-grid textarea:focus{border-color:var(--blue-500);box-shadow:var(--focus)}
.form-grid input:disabled,.form-grid select:disabled,.form-grid textarea:disabled{background:#f4f6f9;color:var(--muted)}
/* 勾选 / 单选 / 文件控件恢复自然尺寸；--primary 只在 tokens.css 定义，本表回退到 --blue-500
   justify-self:start 不可省：label 是 grid 容器，width:auto 的网格项仍会被 justify-self:stretch 拉满整行 */
.form-grid :is(input[type=checkbox],input[type=radio],input[type=file]){width:auto;height:auto;justify-self:start;accent-color:var(--primary,var(--blue-500));line-height:var(--form-control-height)}
/* 必填星号由 required 属性自动渲染（label 内文字不得写「（必填）」）；左侧留白统一，控件列对齐 */
.form-grid label:has(:required){position:relative}
.form-grid label:has(:required)::before{content:'*';position:absolute;left:0;top:0;color:var(--danger);font-weight:700;font-size:12px;line-height:1.4}
/* 字段级错误态：控件红框 + 行内红字 + 表单顶部错误列表 */
.form-grid :is(input,select,textarea)[aria-invalid="true"]{border-color:var(--danger)}
/* 错误态的焦点光晕同色转红（2026-10-05，M83）。上面 :focus 那条只管 box-shadow，配的是
   常规蓝光晕，红框压不住它——这就是「框红了、光晕还是蓝的」那处不协调的来源。
   这条多带一个 [aria-invalid]，权重 (0,3,1) 压过 (0,2,1)，且必须带 :focus：
   未聚焦的错误态只该有红框，不该冒光晕；清掉 aria-invalid 后自动回到常规蓝色。 */
.form-grid :is(input,select,textarea)[aria-invalid="true"]:focus{box-shadow:var(--focus-danger)}
.field-error{margin:0;color:var(--danger);font-size:11px;line-height:1.4}
.form-errors{display:grid;gap:4px;margin:0 0 12px;padding:10px 12px;border:1px solid var(--danger);border-left-width:3px;border-radius:6px;background:var(--danger-bg)}
.form-errors[hidden]{display:none}
.form-errors:empty{display:none}
.form-errors ul{margin:0;padding:0;list-style:none;display:grid;gap:4px}
.form-errors li{color:var(--danger);font-size:12px}
/* 横排勾选组 / 单选组：控件 + 文字内联 */
.choice-row{display:flex;flex-wrap:wrap;gap:8px 14px}
.choice-row label{display:inline-flex;align-items:center;gap:5px;color:var(--text);font-size:12px}
.choice-row input[type=checkbox],.choice-row input[type=radio]{width:14px;height:14px;accent-color:var(--primary,var(--blue-500))}

/* ── select 基座（2026-09-30 全站，替掉当年 bootstrap-select 的做法）
   目标：select 与 input 观感一致（边框/圆角/背景）+ 自定义三角。高度与左右内边距仍归各容器
   （.form-grid 36px、.filter-row 34px、.task-toolbar、.pay-register…，它们本来就与同排 input 对齐），
   基座只补两件事：① 没套容器的裸 select 也拿到 input 那套边框圆角；② 三角 + 给三角让出的右侧留白。
   三角那四条 background 长标与留白用 !important：各容器都用 padding 与 background 简写，简写会把
   background-image 一起重置掉，不锁住三角就被清空；锁的只是「三角 + 留白」，高度/边框/字号仍归容器。 */
select{border:1px solid var(--border);border-radius:6px;background-color:#fff}
/* 多选与带 size 的列表框留原生外观：appearance:none 会吃掉原生滚动条与多选高亮。
   基座刻意不定高度：高度一律归容器（.form-grid 36px、.filter-row 34px、.split-items-table 30px…），
   在基座加 min-height 会把同排 input 与 select 拉出高度差（实测 .split-items-table：input 30 / select 34）。 */
select:not([multiple]):not([size]){
  appearance:none;-webkit-appearance:none;
  background-image:var(--select-arrow)!important;
  background-position:var(--select-arrow-x)!important;
  background-repeat:no-repeat!important;
  background-size:var(--select-arrow-size)!important;
  padding-right:var(--select-arrow-gap)!important;
}
select:disabled{background-color:#f4f6f9;color:var(--muted);cursor:not-allowed}
/* 停用态换一支淡三角。选择器要带上同样的 :not()：两个 :not() 各按参数计权重，
   上面那条是 (0,2,1)，光写 select:disabled 是 (0,1,1)，压不过它 */
select:not([multiple]):not([size]):disabled{background-image:var(--select-arrow-off)!important}
/* 下拉面板由系统绘制，appearance:none 之后 option 的配色得自己兜住，浅底浅字不可读 */
select option{background-color:var(--surface);color:var(--text)}

/* ── form-kit 字段层补齐（2026-09-28，form-kit.js 是「零 CSS、类名由公共表负责」的写法）
   .fk-field = 字段外层（label 或分组 div）；.fk-col/.fk-col-full = 整行 / 同行两格（§2.1）
   .fk-group/.fk-legend = 勾选组包裹层与组名（§2.3）；.fk-filter-field = 筛选条字段（§2.5） */
.form-grid .fk-col{min-width:0}
.form-grid .fk-group{display:grid;gap:5px;color:var(--text);font-size:11px}
.fk-legend{color:var(--text);font-size:11px}
.fk-field .hint{color:var(--muted);font-size:11px;line-height:1.6}
.fk-filter-field{display:grid;gap:5px;color:var(--text);font-size:11px;min-width:170px}

/* ── 筛选条与表头筛选行（§2.5 / §2.6，2026-10-03 M86）
   .filter-row 三条此前是 assets/testing.css:7-9、assets/reports.css:14-16 与
   business/index.html 内联 <style> 三份逐字副本（business 是首个不引 testing.css 却用
   hydrateFilter 的页面，于是自己带了一份）。本卡统一收到 lims2.css——全站 41 个页面都引它，
   三份副本一并删除。以后新增筛选页不必再判断「我这页引哪张表」。

   .filter-head-row 是 M86 的表头第二行形态（form-kit hydrateFilter layout:'header-row'）：
   控件渲染成 <thead> 里列头行下面的第二行，与数据列同一网格，故不写任何定位代码、
   也不会被 .table-wrap 的 overflow:auto 裁掉。 */
.filter-row{display:flex;gap:9px;align-items:end;flex-wrap:wrap}
.filter-row label{display:grid;gap:5px;color:var(--text);font-size:11px;min-width:170px}
.filter-row input,.filter-row select{width:100%;height:var(--form-control-height);border:1px solid var(--border);border-radius:6px;padding:7px 9px;background:#fff;font-size:12px;box-sizing:border-box}
.filter-row .hint{color:var(--muted);font-size:11px;line-height:1.7}
/* 表头行里的控件贴列宽走：.fk-filter-field 的 170px 下限是给页内横排条留的，
   搬进表格后它会把整列撑宽（列宽由内容最大值决定，见 lims2.css 的 table 段） */
.filter-head-row>td{padding:7px 10px;vertical-align:bottom;background:#f8fafb}
.filter-head-row .fk-filter-field{gap:3px;min-width:0}
/* 对齐用的空单元格：无内容无底色，只把高度让掉，底边线仍由 td 的 border-bottom 连续画出 */
.filter-head-row>td.filter-head-empty{padding:0}
/* 一格里两个控件（2026-10-03 M88）：index 与 colspan 完全相同的多条 columns 声明会并进同一个 td，
   典型是两个日期条件共处「创建时间」/「时间」列。间距与对齐沿用页内横排筛选条的口径（gap:9px、
   align-items:flex-end），单控件的格不套这层，形状与 M86 零变化。 */
.filter-head-row .filter-head-group{display:flex;gap:9px;align-items:flex-end}
/* 每个筛选控件的「× 清除」（2026-10-06，M105）。默认态不显示（[hidden]，见 JS 侧 makeFilterClear），
   非默认态时露出来；点它恢复该控件默认并立刻重新取数，不必再点「刷新」。
   落点的 left/top 由 JS 量可见盒子写进来（输入框量 input、picker 量 .choices__inner——原生 select
   被 choices 顶成不可见，量它等于量 0），本段只管**相对那条右沿的位移**：
   transform 把按钮整体挪到 left 指定的竖线上，两个分支各让开一段内缩——输入框让 6px，
   下拉让 36px（多出的 30px 是 M96 的三角命中条带 .formkit-arrow-band，× 排在它左侧，不抢它的点击）。
   落在 <td> 上而不是字段外层 label 里：label 的激活转发会把点击再合成一次 click 派给 select，
   点 × 会连带把下拉面板开出来。 */
.filter-head-row>td.filter-head-has-clear{position:relative}
.filter-clear{position:absolute;z-index:2;margin:0;padding:0;width:18px;height:18px;line-height:16px;
  text-align:center;border:1px solid var(--border);border-radius:50%;background:#fff;color:var(--muted);
  font-size:13px;cursor:pointer;transform:translate(calc(-100% - 6px),-50%)}
.filter-clear:hover{border-color:var(--blue-500,#2878b5);color:var(--blue-500,#2878b5)}
.filter-clear:focus-visible{outline:none;box-shadow:var(--focus,0 0 0 3px rgba(40,120,181,.18))}
/* 下拉那支多让 30px：M96 的三角命中条带 .formkit-arrow-band 占了右沿 30px，× 排在它左侧，不抢它的点击。
   条带挂不挂上都按这一支算——引擎没加载时原生 select 的箭头也在同一个位置。 */
.filter-clear-select{transform:translate(calc(-100% - 36px),-50%)}
/* 显隐一律用 hidden（page-layout-standard.md §1.8）。button 的 display 来自 UA 与本段，
   [hidden] 的 display:none 压不过作者表的 display —— 与 .form-errors[hidden] 同类兜底（§1.6）。 */
.filter-clear[hidden]{display:none}
/* 输入框那支要给右端让出文字的位置，否则输入内容压在 × 底下（只在这一格内生效） */
.filter-head-row>td.filter-head-input input{padding-right:28px}
/* 窄视口降级出来的页内筛选条里 × 走文档流，跟着控件横排，不再绝对定位 */
.filter-clear-inline{position:static;transform:none;margin-left:-6px}
/* 窄视口（innerWidth ≤ 900）降级出来的页内筛选条：form-kit 把表头行里那批 label 搬进这个容器，
   表头行同时 hidden，宽视口再搬回去。**搬节点是 JS 干的，断点只由 form-kit 的 HEADER_ROW_NARROW
   一处决定，本表没有对应的 @media(max-width:900px) 也不该有** —— 两条规则各改各的会分叉。
   搬过去的位置在 .table-wrap 外面（form-kit 插在包裹层之前），所以这里的下拉不受表格包裹层
   裁剪，面板按视口算宽——与宽视口的表头形态同一条边界规则，见上方 picker 面板一段。 */
.filter-head-bar{margin-bottom:10px}
/* 条件字段显隐：.fk-field 就是 .form-grid 里的 label（display:grid），会压过 UA 的 [hidden]，
   条件必填字段只能切 hidden（page-layout-standard.md §1.8），故与 .form-errors[hidden] 同类兜底（§1.6） */
.form-grid .fk-field[hidden]{display:none}
/* 多选 / 带 size 的 select 不吃 .form-grid 的 36px 固定高，否则选项被压成一行 */
.form-grid select[multiple],.form-grid select[size]{height:auto;min-height:96px;padding:4px}
.form-grid select[multiple] option,.form-grid select[size] option{padding:3px 5px;font-size:12px}
/* picker 增强层（§2.4，渲染引擎 = choices.js 11.2.4）：原生 select 仍是唯一事实源，
   choices 只负责画——它把 select 挪进 .choices 包装并置 hidden，并自己给它加 .choices__input 类；
   顶回 display 的是 choices.min.css 里那两条作者规则（.choices__input{display:inline-block} 与
   .choices[data-type*=select-one] .choices__input{display:block}），压过 UA 的 [hidden]{display:none}。
   .choices [hidden]{position:absolute;inset:0;pointer-events:none;opacity:0} 一句 display 都没有，
   只负责不影响布局、不可点。结论不变：required 仍可聚焦、报错照常（PKCHOICE01 浏览器实测 checkValidity=false、requestSubmit 被拦、焦点落在 select 上；机制经 M38 评审 r2 实测复核）。
   检索框与光晕的形态（2026-10-03 M82）也归引擎那一层管，本表不参与：单选那支检索框引擎原生插在
   .choices__list--dropdown 里（落在选项列表内），form-kit 的 mount() 把它搬回 .choices__inner，
   于是两型都是「检索框压在框本体上」；焦点光晕统一落在 .choices__inner 上（尺寸＝框本身），
   检索框自身不再各画一圈小光晕。两条都写在 form-kit 的 PARITY_STYLE 里——它随引擎 CSS 一起注入、
   选择器一律 .formkit-picker 收口，select-lab 那张手搓 choices 的对照页不受影响。
   下面几条 .formkit-picker-drop/-search/-panel/-option 是自绘面板时代的样式，
   引擎换成 choices 后已无对应节点（choices 自带 choices.min.css），按「本卡只许改基座豁免」的约定原样留着未删。 */
.formkit-picker{position:relative}

/* 页面级浮层的层级（2026-10-22，M200）。档位表见 docs/z-index-contract.md §2.1，
   本表只出取值（:root 的 --z-page-panel:45），**声明本身在 form-kit 的 PARITY_STYLE ⑨**
   （.formkit-picker.is-page-panel{--choices-z-index:var(--z-page-panel,45)}）——
   它必须 scoped 到控件这一处：vendor 的 --choices-z-index 是全局变量，直接在本表改
   会连带全站所有 choices 控件（弹窗内的、表头筛选行里的）一起抬起来，
   而那两档按契约 §4.2-A/C 是不能抬的：弹窗内靠净空带封顶解决，
   表头筛选行里的面板出不了 <thead> 的层叠上下文，抬了也没用。
   哪些控件算「页面级」由 JS 判（form-kit 的 applyPanelLayer：面板所在的最近层叠上下文
   是根上下文、祖先链上没有裁剪盒、控件也不在表格里），CSS 判不了上下文。 */

/* 三角条带上的光标（2026-10-05，M96）。几何与命中在 form-kit 的 PARITY_STYLE ⑧（它随引擎 CSS
   一起注入、选择器一律 .formkit-picker 收口），本表只管指针长什么样：条带 pointer、检索区 text。
   收起态条带是 pointer-events:none，光标透给下层——单选透出 vendor 给 .choices[data-type*=select-one]
   的 pointer，多选透出 .choices__inner 的 text；两型收起态的三角因此不是一个颜色，这是
   「收起时多选框本体是文本区、那里没有可点的箭头」的必然结果，不是漏配。 */
.formkit-picker .formkit-arrow-band{cursor:pointer}
.formkit-picker .choices__inner>input.choices__input--cloned{cursor:text}

/* ── 基座豁免（PKCHOICE01，本表为 picker 改动的唯一一处）──────────────────────
   增强后的原生 select 是「事实源」而不是给人看的东西，但它仍参与布局计算，
   于是上面两条基座规则会在它身上留下只对可见控件才有意义的痕迹：
   ① `select:not([multiple]):not([size])` 那条带 padding-right:var(--select-arrow-gap)!important
      （28px 三角留白）与三角 background——都是为「看得见的下拉」预留的；
   ② `.form-grid select[multiple],[size]{min-height:96px}` 是给多选列表框留的固定高。
      实测（M38 探针，11.2.4）：多选 staff-items 的隐藏 select 被拉到 96px 高，
      而 .choices 包装只有 54px，绝对定位的溢出照样撑大 .modal-body 的可滚动区。

   宿主必须是 `:has(> .choices)` 而不是光 .formkit-picker：**窄视口降级时 choices 不初始化，
   但 .formkit-picker 包裹层还在**（choices destroy 走 unwrap，只把 select 退回这层），
   光写 .formkit-picker 会把降级态的原生 select 一起豁免掉——三角被抹成 none、28px 留白与
   96px 列表框高一起消失，手机上就剩「有边框没箭头」的方框，正好打反「系统选择器在移动端最优」。
   :has() 的权重取参数里最具体的那个（.choices = 0,1,0），叠上后面两条 = (0,4,1)，
   压得住基座的 (0,2,1)，!important 也照旧只作用在增强态这一侧。 */
.formkit-picker:has(> .choices) select:not([multiple]):not([size]){padding-right:0!important;background-image:none!important}
.formkit-picker:has(> .choices) select[multiple],.formkit-picker:has(> .choices) select[size]{height:auto;min-height:0;padding:0}
.formkit-picker-drop{position:static;display:grid;gap:6px;padding-top:4px}
.formkit-picker .formkit-picker-search{width:100%;height:30px;border:1px solid var(--border);border-radius:6px;background:#fff;padding:0 9px;font-size:12px;outline:none}
.formkit-picker .formkit-picker-search:focus{border-color:var(--blue-500);box-shadow:var(--focus)}
.formkit-picker-panel{position:absolute;top:100%;left:0;right:0;z-index:20;display:grid;gap:1px;max-height:200px;overflow-y:auto;background:#fff;border:1px solid var(--border);border-radius:6px;padding:7px;box-shadow:var(--shadow)}
/* display:grid 会压过 UA 的 [hidden]，面板与过滤掉的选项行都要显式兜底（page-layout-standard.md §1.6） */
.formkit-picker-panel[hidden],.formkit-picker-option[hidden]{display:none}
.formkit-picker-option{padding:5px 7px;border-radius:4px;font-size:12px;line-height:1.5;color:var(--text);cursor:pointer}
.formkit-picker-option:hover{background:var(--blue-50)}
.formkit-picker-option.is-selected{background:var(--blue-100);color:var(--blue-700);font-weight:600}
.formkit-picker-option[aria-disabled="true"]{color:var(--weak);cursor:not-allowed}
/* overlay 模式：页面自担必填呈现时隐藏原生 select，面板改为常驻展开 */
.formkit-picker.is-overlay .formkit-picker-panel{position:static}

/* 只读字段排版 */
.kv-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:14px}
.kv-grid .kv{background:#f8fafb;border:1px solid var(--line);border-radius:6px;padding:9px 11px}
.kv-grid .kv span{display:block;color:var(--muted);font-size:10px}
.kv-grid .kv b{display:block;margin-top:3px;font-size:13px;color:var(--ink);font-weight:600;word-break:break-all}

/* 消息提示 */
.toast{position:fixed;right:24px;bottom:24px;z-index:150;display:flex;align-items:center;gap:10px;min-width:270px;max-width:400px;padding:12px 14px;background:#1f2937;color:#fff;border-radius:7px;box-shadow:0 10px 30px rgba(17,24,39,.25);animation:toast-in .18s ease}
.toast[hidden]{display:none}
.toast span{width:20px;height:20px;display:grid;place-items:center;background:var(--success);border-radius:50%;font-size:11px;flex:none}
.toast.err span{background:var(--danger)}
.toast p{margin:0;font-size:12px}
@keyframes toast-in{from{opacity:0;transform:translateY(7px)}to{opacity:1;transform:none}}

/* 登录页（门户） */
.login-wrap{min-height:100vh;display:grid;place-items:center;background:linear-gradient(160deg,var(--side-grad-a) 0%,var(--side-grad-b) 55%,var(--side-grad-c) 100%)}
/* display:grid 会覆盖 UA 的 [hidden]，登录后需显式隐藏 */
.login-wrap[hidden]{display:none}
.login-card{width:min(400px,92vw);background:#fff;border-radius:10px;box-shadow:var(--shadow);padding:30px 30px 26px}
.login-card .brand{border:0;padding:0 0 18px}
.login-card h1{margin:0 0 4px;font-size:19px}
.login-card>p{margin:0 0 18px;color:var(--muted);font-size:12px}
.login-card form{display:grid;gap:12px}
.login-card input{width:100%;height:40px;border:1px solid var(--border);border-radius:6px;padding:0 12px;font-size:13px;outline:none}
.login-card input:focus{border-color:var(--blue-500);box-shadow:var(--focus)}
.login-card .button{width:100%;height:40px}

/* 页头身份切换（演示身份预置，菜单直接切换，无需登入登出） */
.identity-switch{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--muted)}
.identity-switch select{height:30px;border:1px solid var(--border);border-radius:6px;background:#fff;padding:0 6px;font-size:12px;color:var(--text);outline:none}
.identity-switch select:focus{border-color:var(--blue-500);box-shadow:var(--focus)}
@media(max-width:720px){
  .top-actions .profile>span:not(.avatar){display:none}
  .identity-switch{font-size:0;gap:0}
  .identity-switch select{max-width:88px;font-size:12px}
  /* 窄屏控件（如上 88px 的身份下拉）给三角让 28px 太奢侈，收窄一档 */
  :root{--select-arrow-gap:24px}
}
.login-error{color:var(--danger);font-size:12px;min-height:16px;margin:0}

/* 响应式 */
@media(max-width:1100px){
  .sidebar{width:208px}
  .main{margin-left:208px}
  .grid.two{grid-template-columns:1fr}
  .kv-grid{grid-template-columns:1fr 1fr}
}
@media(max-width:760px){
  .main{margin:0}
  .topbar{padding:0 16px}
  .content{padding:20px 14px 56px}
  .page-head{display:block}
  .page-actions{margin-top:12px}
  .form-grid{grid-template-columns:1fr}
  .modal-footer .button{flex:1}
  .timeline-item{grid-template-columns:70px 14px 1fr}
}
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}

/* ── 侧栏抽屉（≤1023px）────────────────────────────────────
   2026-10-05，M98：≤760 把侧栏改 position:static 堆到顶部，.side-nav 变横排，
   而 .side-nav a.side-tab{width:100%}（见下）在 flex 行容器里被等分压成方形块、
   文字挤成竖排单字列——就是用户投诉的「菜单本身还是从上到下排列，菜单栏不正常」。
   改法：≤1023 侧栏走 off-canvas 抽屉，默认收起、点 topbar 左侧汉堡钮弹出，桌面布局逻辑不变。
   · 断点 1023 是本仓既有语义断点（app.css 的平板档），不是新魔数；数字只在
     lims2.css 与 assets/sidebar.js 各写一处，文档交叉引用 form-rendering-standard.md。
   · 1024+ 本块一条规则都不生效，桌面逐像素不变。
   · 类名统一 .nav-drawer* 前缀：.drawer/.drawer-open/.drawer-overlay 已被 M95 合同抽屉占用
     （tokens.css / app.css），而 tech-leader 同时加载 lims2.css + tokens.css + app.css，不能撞名。
   · z 序：遮罩 25 < 面板 26 < topbar 30 < 页面级浮层面板 45 < identity-menu 60 < 演示入口 90 < modal 100 < toast 150。
     面板 top:64px 让开 topbar，弹开后原地就是汉堡钮（复用为关闭钮），不必再加关闭控件。
     **页面级浮层面板 45**（M200 新增，--z-page-panel）落在抽屉(25/26)、topbar(30)、
     侧栏(40) 之上、演示入口(90) 之下——它是根上下文里的页面级下拉面板，
     见 docs/z-index-contract.md §2.1 的 L5′ 档。
   · 收起态除 transform 外必须 visibility:hidden——否则收起后仍可 Tab 进侧栏、
     也过不了 guided-demo runtime-browser.js 的 isElementVisible（display/visibility 判隐藏）。
     但**光有 visibility:hidden 挡不住「抽屉开过一次之后」的 Tab 泄漏**（浏览器把焦点曾进入过的
     区域留作顺序焦点锚点），那一层要靠 sidebar.js 在收起态给侧栏加 inert，本块管不着。
   · 页签是 LimsSidebar 运行时渲染的（可能再嵌套 div.side-group），本块不假设扁平列表。 */
.nav-drawer-toggle{display:none}
@media(max-width:1023px){
  /* 汉堡钮：由 assets/sidebar.js 注入 .topbar 首子位；桌面档 display:none，1024+ 零影响 */
  .nav-drawer-toggle{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex:none;border:1px solid var(--border);border-radius:8px;background:#fff;padding:0;color:var(--text)}
  .nav-drawer-toggle:hover{border-color:#9aabb9}
  .nav-drawer-toggle:focus-visible{outline:none;box-shadow:var(--focus)}
  .nav-drawer-toggle svg{display:block}

  /* 顶栏注入汉堡钮后 space-between 会把面包屑顶到正中；改 flex-start + 面包屑 flex:1。
     min-width:0 + nowrap + 省略号是刚需：面包屑默认会被压成竖排单字列（.topbar 是 flex） */
  .topbar{gap:12px;justify-content:flex-start}
  .breadcrumb{flex:1;min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}

  .main{margin-left:0}
  .sidebar{
    position:fixed;top:64px;bottom:0;left:0;
    width:264px;max-width:84vw;padding:18px 16px;
    z-index:26;
    transform:translateX(-100%);visibility:hidden;
    /* visibility 两向不同步的原因就是收起时别急着隐藏：收起方向给 0s 过渡但延迟 .2s，
       面板滑出动画跑完才真正 hidden；展开方向 0s 无延迟。
       **不要**为了"两向对称"把它写成 visibility .2s——那样收起会立刻 hidden，
       面板原地消失、滑出动画没了，这正是它必须非对称的唯一理由。
       展开后焦点能否送进侧栏与本行无关：那是 Chrome 的 stale-style 窗口
       （rAF 回调跑在本帧样式重算之前），由 sidebar.js 的 focusDrawerEntrySoon
       逐帧重试解决。别把焦点问题记到这条过渡头上。 */
    transition:transform .2s ease,visibility 0s linear .2s;
  }
  html[data-nav-drawer="open"] .sidebar{transform:none;visibility:visible;transition:transform .2s ease,visibility 0s}
  .nav-drawer-overlay{
    display:none;position:fixed;inset:64px 0 0 0;z-index:25;
    background:rgba(16,26,40,.42);
  }
  html[data-nav-drawer="open"] .nav-drawer-overlay{display:block}
}
@media(max-width:500px){
  .topbar{gap:8px}
  .topbar>.lims-home-link-mobile{display:block;flex:none;white-space:nowrap;color:var(--muted);font-size:13px}
  .topbar .breadcrumb{display:none}
  .topbar .identity-trigger{gap:3px;padding:0 6px}
  .topbar .identity-trigger-label{white-space:nowrap}
  .topbar .identity-trigger-value{display:none}
}
@media(max-width:370px){
  .guided-demo-entry .button{max-width:72px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
}

/* ── 统一身份下拉（LimsIdentity）──────────────────────────────
   两级：.identity-trigger 展开 .identity-menu → .identity-menu-group → .identity-menu-users */
.identity-dropdown{position:relative}
.identity-trigger{display:flex;align-items:center;gap:6px;height:30px;border:1px solid var(--border);border-radius:6px;background:#fff;padding:0 9px;font-size:12px;color:var(--text);cursor:pointer}
.identity-trigger:hover{border-color:#9aabb9}
.identity-trigger:focus-visible{outline:none;box-shadow:var(--focus)}
.identity-trigger-label{font-size:11px;color:var(--muted)}
.identity-trigger-value{color:var(--text)}
.identity-trigger-caret{color:var(--weak);font-size:10px}
.identity-menu{position:absolute;top:calc(100% + 6px);right:0;z-index:60;min-width:236px;max-height:62vh;overflow-y:auto;background:#fff;border:1px solid var(--border);border-radius:8px;box-shadow:var(--shadow);padding:6px;display:none}
.identity-dropdown.open .identity-menu{display:block}
.identity-menu-group{border-radius:6px}
.identity-menu-system{display:flex;align-items:center;gap:8px;width:100%;border:0;background:transparent;padding:8px 10px;font-size:12px;color:var(--text);border-radius:6px;text-align:left;cursor:pointer}
.identity-menu-system:hover{background:var(--blue-50)}
.identity-menu-group.current>.identity-menu-system{color:var(--blue-700);font-weight:600}
.identity-menu-system-label{flex:1;min-width:0}
.identity-menu-current{font-size:10px;color:var(--blue-600);background:var(--blue-50);padding:1px 6px;border-radius:99px;flex:none}
.identity-menu-caret{color:var(--weak);font-size:10px;flex:none;transition:transform .15s ease}
.identity-menu-group.expanded>.identity-menu-system .identity-menu-caret{transform:rotate(90deg)}
.identity-menu-users{display:none;padding:0 0 6px 8px}
.identity-menu-group.expanded>.identity-menu-users{display:block}
.identity-user{display:block;width:100%;border:0;background:transparent;padding:7px 10px;font-size:12px;color:var(--text);border-radius:6px;text-align:left;cursor:pointer}
.identity-user:hover{background:var(--blue-50)}
.identity-user.current{color:var(--blue-700);font-weight:600}
.identity-user:disabled{opacity:.55;cursor:wait}
.identity-user-status{padding:7px 10px;font-size:12px;color:var(--muted)}
.identity-user-status.error{color:var(--danger)}
.identity-portal-link{display:block;margin:4px 6px 0;padding:8px 10px;border-radius:6px;background:var(--blue-50);color:var(--blue-700);font-size:12px;font-weight:600;text-decoration:none}
.identity-portal-link:hover,.identity-portal-link:focus-visible{background:var(--blue-100);text-decoration:underline}

/* ── 侧栏页签（LimsSidebar）──────────────────────────────────
   .side-nav 容器内渲染：.side-title（系统名）+ .side-tab（页签项）+ .side-footer（工具组） */
.side-title{padding:2px 12px 8px;font-size:13px;font-weight:600;color:#fff;letter-spacing:.02em}
.side-nav a.side-tab{width:100%}
.side-nav a .count-badge{margin-left:auto;background:var(--side-badge-bg);color:var(--side-badge-text);font-size:10px;padding:1px 7px;border-radius:99px}
.side-footer{margin-top:14px;padding-top:4px;border-top:1px solid var(--side-line);display:grid;gap:2px}
.side-nav a.side-footer-link{font-size:12px}
