/* 天地通 OA · 界面样式
   ============================================================================

   两个事实决定了整套排版，不是审美偏好：

   1. 实测 68% 的登录来自手机，会话中位 16 分钟 —— 人是站着、走着、
      在车间里用这个东西的。所以：大点击区、高对比、不横向滚动、
      输入框字号 ≥16px（低于它 iOS 会自动放大整页）。
   2. 桌面端是「坐下来处理一批」的场景 —— 导航常驻、操作固定在右侧，
      眼睛不用来回找。

   视觉语言：**玻璃只给控制层，内容层是实底。**
   浮在内容之上的东西 —— 侧栏、手机顶栏与标签栏、弹层、贴底操作条 —— 用半透明
   材质加模糊；正文里的一切 —— 列表、表格、表单、详情、右栏面板 —— 一律实底 +
   1px 发丝线 + 零阴影。
   理由是**玻璃要有东西从它底下滚过去才成立**：桌面上右栏底下什么都没有，
   模糊出来的只是一层假厚度，看着像贴图。Mac 上的邮件与系统设置就是这么分的 ——
   Liquid Glass 只在侧栏和工具条上，正文区一片玻璃都没有，字号 13。
   强调色只有天地通品牌靛蓝 #343474 一个，语义色（逾期橙 / 驳回红 / 通过绿）与它分开。

   结构：
     01 令牌      02 基底      03 材质      04 外壳（侧栏/主区/右栏/标签栏）
     05 排版      06 控件      07 列表与卡片  08 表格      09 页面专属
     10 打印      11 水印      12 动效开关
   ========================================================================= */

/* ---------- 01 令牌 ---------- */

:root {
  /* 告诉浏览器当前主题，原生控件的弹出层 / 滚动条 / 系统色才跟着走。两份暗色块各写一遍 dark。 */
  color-scheme: light;
  --brand-blue: #2A2380;
  --nav-height: 64px;
  --assistant-height: 100dvh;
  --assistant-top: 0px;
  --glass-x: 24%;
  --login-glass: color-mix(in srgb, var(--surface) 42%, transparent);
  --glass-face: color-mix(in srgb, var(--surface) 54%, transparent);
  --glass-edge: color-mix(in srgb, var(--surface) 65%, var(--line-strong));
  --glass-shine: color-mix(in srgb, var(--surface) 62%, transparent);
  --glass-shadow: inset 0 1px 1px var(--glass-shine), inset 0 -1px 1px var(--line), 0 8px 28px -10px color-mix(in srgb, var(--ink) 28%, transparent);
  --glass-blur: saturate(160%) blur(16px);
  --r-sheet: 28px;
  --r-composer: 28px;
  --r-composer-mobile: 26px;
  /* 内外圆角按真实嵌套距离相减，附件卡和缩略图才会与外框同心。 */
  --r-composer-inner: 21px;
  --r-composer-inner-mobile: 20px;
  --r-composer-preview: 15px;
  --brand-mark: var(--brand-blue);
  /* 品牌。全站只有这一个强调色，其余靠中性灰与留白拉层级。 */
  --tint: #343474;
  /* 支路颜色。用户 2026-09-07：「不同分支不同颜色，同序号。」
     六档，够这家公司（三家分公司 + 一条兜底）用，还留了余量。
     色相拉开而明度压平 —— 它要区分的是「哪条路」，不是「哪条更重要」。 */
  /* 流程图的几何。这里只剩两个数了 —— 主干竖过来之后，
     「并排的支路对不对齐」交给了 CSS Grid（等宽列 + 行号），
     不再靠一串手算的常量互相咬合。原来那四个（--gseg / --lane-gap /
     --lane-tag / --fork-rail）都是横主干时代用来手工对齐的，已经删掉。
     --gcard-w 在 .graphwrap 上被改写成 236px：那是管理区那张大图的列宽；
     这里这个 176 留给发起页那条窄链。 */
  --gcard-w: 176px;
  --lane-h: 104px;
  --lane-1: #3B5BDB; --lane-2: #0B7285; --lane-3: #9C36B5;
  --lane-4: #28843B; --lane-5: #C2410C; --lane-6: #5C5F66;
  --tint-2: #3C3C7C;
  --tint-soft: color-mix(in srgb, var(--tint) 12%, transparent);
  --on-tint: #FFFFFF;
  /* 压在 tint 填色上的次要文字。85% 是两套主题都过 4.5 的最淡一档（暗 5.5 / 亮 8.5）；
     72% 在暗色下只有 4.2，75% 是 4.47。从 --on-tint 派生，所以两套主题各自成立，不用写两遍。 */
  --on-tint-2: color-mix(in srgb, var(--on-tint) 85%, transparent);

  /* 中性色带一点点靛蓝偏色。纯中灰会显得像没调过。 */
  --canvas: #F0F0F4;
  --surface: #FFFFFF;
  --surface-2: #F7F7FA;
  --ink: #12121A;
  --ink-2: #494957;
  /* #74748A 只在纯白上过得了 4.5:1（4.56）。而这个色最常出现的地方
     恰恰不是纯白：页面底 #F0F0F4 上是 4.01、--surface-2 上 4.26、
     玻璃操作栏上 4.41 —— 三处都不及格，而这套系统「没有 IT 可问」
     的那些解释文字全用它。压到 #66667A：页底 4.93，其余更高。 */
  --mute: #66667A;
  --line: rgba(18, 18, 40, 0.10);
  --line-strong: rgba(18, 18, 40, 0.16);
  /* 输入框的静态边框要能把控件从背景里分出来：WCAG 2.2 的 1.4.11
     要求 3:1，而 --line-strong 在白底上只有 1.41:1。
     它只用于「这是一个可以输入的框」，不做装饰性分隔线。 */
  --field-line: rgba(18, 18, 40, 0.42);

  /* 语义色与品牌色分开：警示不该跟强调色抢眼睛。 */
  --ok: #1B7A3F;
  --ok-bg: #E4F3EA;
  --bad: #B3261E;
  --bad-bg: #FAE7E5;
  --late: #9A4B00;
  --late-bg: #FDF0E4;
  --admin: #9A4B00;
  --star: #E0A526;  /* 收藏的星。金色在两套里都成立，所以只有一个值 */
  /* 管理色在明暗两套里深浅相反（深棕 ↔ 浅琥珀），所以压在它上面的字
     不能写死 #fff。写死的后果实测是暗色下 2.25:1 —— 而那是常驻的
     「管理」导航标签，全站最不该看不清的那一个。 */
  --on-admin: #FFFFFF;

  /* 材质。三档，对应 Apple 的 thin / regular / thick。**只给控制层。** */
  --mat-thin: color-mix(in srgb, var(--surface) 58%, transparent);
  --mat-reg: color-mix(in srgb, var(--surface) 74%, transparent);
  --mat-thick: color-mix(in srgb, var(--surface) 86%, transparent);
  --blur: saturate(180%) blur(28px);
  /* 顶边高光 + 底边收口，玻璃的「厚度」全靠这一行。 */
  --specular: inset 0 1px 0 rgba(255, 255, 255, 0.66),
              inset 0 -1px 0 rgba(18, 18, 40, 0.04);
  /* 阴影分两类，不要混：
     **控制层**（侧栏、弹层、贴底操作条）真的浮在别的东西上面 → --shadow / --shadow-lift
     **内容层**（内容面板）不浮在任何东西上面，但它仍然要**看得出是一块东西** → --shadow-card

     ⚠️ 这里原来写的是「内容里的列表、表格、详情、右栏面板**一律不投影** ——
     它们不浮在任何东西上面，那道影子是投给谁看的？」
     **2026-09-08 被用户推翻**：「右边看起来很奇怪像是未完成的系统 ——
     直接背景色+文字。太原始了。」
     推翻的理由：投影的作用**不只是**「A 浮在 B 上面」，还有「**这是一个东西**」。
     白面板落在 #F0F0F4 上、边只有 10% 的一条线时，它和页面底色几乎分不开，
     于是内容读起来不是「面板里的内容」，是「散在页面上的文字」。
     这道影子极轻（8px 模糊、-14px 收缩），它只负责给边界一点厚度。 */
  --shadow: 0 10px 26px -14px rgba(12, 12, 32, 0.26);
  --shadow-lift: 0 20px 46px -20px rgba(12, 12, 32, 0.34);
  --shadow-card: 0 1px 2px rgba(12, 12, 32, 0.05),
                 0 8px 18px -14px rgba(12, 12, 32, 0.20);

  /* 圆角三档：控件 8、卡片 12、面板 18。**这三个数不是随手取的，是同心的** ——
     Apple 的规矩是「内层半径 = 外层半径 − 两者之间的内边距」，
     这样嵌套的圆角看起来才是同一个圆心画出来的，而不是三个不相干的弧。
     对一下：面板 18、内边距 22 → 卡片本该是 18−22（负数）所以取下限 12；
     卡片 12、内边距 14 → 控件 8。差得不多，读起来就是「一层套一层」。
     ⚠️ 改其中任何一个之前，先想清楚它外面那层的内边距是多少。
     ⚠️ 原来是两档，而且 md / lg / xl **全是 14** —— 于是一个小药丸和
     一整块面板一个角，什么都不像「嵌在什么里面」。分开之后层级靠角就读得出来：
     面板（18）里放卡片（12），卡片里放控件（8）。
     pill 留给状态标签、计数、头像、圆形图标键和手机标签栏 ——
     按钮也做成 pill 的话，一页上全是胶囊，主次就消失了。 */
  --r-sm: 8px;
  --r-md: 12px;
  --r-lg: 18px;
  --r-pill: 999px;

  /* 窗口边距。**只有这一个数**：侧栏四边、内容面板的上右下、
     以及两者之间的缝，全是它。散成好几个值的那一版，
     用户一眼就看出来了（「高度不一致，没有对齐」）。 */
  --frame: 12px;

  /* 字号，Apple 文本样式的对应关系。
     **单位是 rem 不是 px**，虽然默认根字号下两者一模一样（1rem = 16px）。
     写 px 的后果实测过：把浏览器的默认字号从 16 调到 20，
     这套界面的正文**纹丝不动**还是 17px —— 只有那十几处用 rem 的间距变大了，
     于是字没变大、行距变宽，看起来像排版坏了。
     视力不好的人在系统设置里调大字号，是他能做的第一件事，
     而这套系统里最需要看清的恰恰是那些解释性的小字。
     版式撑得住：把整套字号乘 1.25 实测过，1440 和 375 两个宽度上
     零溢出、零截断、零重叠。 */
  --fs-large: clamp(1.375rem, 2.3vw, 1.625rem);
  --fs-title: 1.1875rem;
  --fs-head: 1rem;
  --fs-body: 0.9375rem;
  --fs-callout: 0.9375rem;
  --fs-sub: 0.875rem;
  --fs-foot: 0.8125rem;
  --fs-cap: 0.75rem;
  /* 比 cap 再小一档：只给徽章上的数字、图表的轴标签、折叠三角这种不是正文的字。 */
  --fs-micro: 0.6875rem;
  /* 只给 KPI 卡上那一个数字。 */
  --fs-display: 2rem;
  /* 上面这一套是**桌面**的（正文 15px）。手机在 ≤768 段整体换回 iOS 尺度。
     原来两边共用一套、正文 17px —— 那是 iPhone 的正文尺寸，搬到 1440 的屏上
     整套界面比同类工具大三分之一，一屏装不下一批待办。
     参照：macOS 正文 13，Web 工具类普遍 14~15。手臂到屏幕的距离不一样，
     字号就不能是同一个。

     输入框是唯一的例外，永远 ≥16px：低于它 iOS 一聚焦就放大整页，
     而且放大了回不去。写成 max() 是为了不破坏上面那条 rem 的用意 ——
     用户把系统字号调大时它跟着大，调小时它守住 16px 这条下限。 */
  --fs-input: max(1rem, 16px);
  --fs-root-small: 100%;
  --fs-root-standard: 108%;
  --fs-root-large: 120%;

  /* 字重四档。之前散落着 400 / 500 / 560 / 600 / 620 / 640 / 650 / 660 / 700 九种 ——
     而这套字体栈在 Windows 上落到微软雅黑，它只有 Regular 和 Bold：560 到 700 全渲染成粗体、
     400 / 500 全是常规；mac 上 PingFang 到 600 封顶。九档在真实设备上从来只是两三档，
     多出来的只是数字。要更重就上一档，不要再发明 630。 */
  --w-regular: 400; --w-medium: 500; --w-semibold: 600; --w-bold: 700;

  /* 三套字体栈，一处分职一件事（理由见下面 @font-face 那段）：
       --font-ui       正文、控件、用户内容 —— 中文交给系统字族，拉丁也走系统，
                       因为聊天和知识库里的英文段落跟着系统的西文走更耐读。
       --font-display  页面标题与区块标题 —— Manrope 打头，中文回退到同一套系统栈。
                       中英混排时英文用 Manrope、中文用系统黑体，这是中文产品里
                       最常见的搭配法，不是「两种字体打架」：Manrope 的几何骨架
                       和思源 / 雅黑的中宫比例接近，放在一起不会散。
       --font-num      数字 —— 单据号、日期、金额、计数、角标。配 tabular-nums
                       一起用，数字等宽，列自动对齐。
     顺序里把 MiSans / HarmonyOS Sans / 思源黑体 排在雅黑前面：这三款都是明确
     免费商用的，员工机器上装了就用更好的，没装就退回雅黑，零成本零风险。 */
  --font-ui: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC",
             "MiSans", "HarmonyOS Sans SC", "Noto Sans SC", "Source Han Sans SC",
             "Microsoft YaHei", system-ui, sans-serif;
  --font-display: "Manrope", -apple-system, BlinkMacSystemFont, "PingFang SC",
                  "MiSans", "HarmonyOS Sans SC", "Noto Sans SC", "Source Han Sans SC",
                  "Microsoft YaHei", system-ui, sans-serif;
  --font-num: "Manrope", ui-monospace, "SF Mono", "PingFang SC", "Microsoft YaHei",
              system-ui, sans-serif;

  --tap: 44px;             /* Apple HIG 的最小点击区 */

  /* 表单控件的两档宽度。短档按 11 位手机号 + 内边距算，中档按一个邮箱地址算。 */
  --fw-s: 200px;
  --fw-m: 420px;
  /* ---- 动效令牌 ----
     对照 Soybean Admin 与 Tabler 之后定的（`docs/motion-direction-20260907.md`）。
     两家的「丝滑」拆到源码里都很朴素：Soybean 全站就是一个
     `transition-all-300`（300ms / .4,0,.2,1），Tabler 是 Bootstrap 十年前的
     `all .2s ease-in-out`。**它们流畅是因为一致和克制，不是因为动画好。**

     所以这里只有四档时长、三条曲线，**全站禁止裸写秒数** ——
     不一致比没有动画更糟：眼睛会把两个本该一样的动作差 40ms 当成故障。
     这条有测试守着（`motion_test.go`）。

       --t-1  按压。一天按几百次的动作，只够给一个「收到了」的回执
       --t-2  hover / 小菜单 / 提示 / 列表选中
       --t-3  弹层 / 抽屉 / 换页 —— 界面动效的上限就在这一档附近
       --t-4  侧栏这种大位移。一天点一两次，而且用户盯着它看

     曲线三条，用途不重叠：
       --ease-out    进场、出现、hover。先快后慢 —— 人盯着的是起步那一瞬
       --ease-inout  屏幕上的位移（拖拽归位、共享元素平移）。两头都收
       --ease-drawer 抽屉 / 侧滑。起步略缓、收尾极缓，手感像被「拉」出来 */
  --t-1: 100ms;
  --t-2: 160ms;
  --t-3: 220ms;
  --t-4: 320ms;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-inout: cubic-bezier(0.77, 0, 0.175, 1);
  --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
  /* 首屏列表错开进场的步长（第 8 条）。只给第一次，翻页和筛选不用。 */
  --stagger: 30ms;
  /* 交接用的延迟：侧栏展开时文字晚一点再进（第 3 条）。
     它不是第五档时长，是「两件事之间隔多久」—— 用途不同，所以另起一个名字。 */
  --lag: 80ms;
  /* **唯一一处超出四档的时长**：主题切换的圆形揭开（第 5 条）。
     破例是有据的 —— 频率法则里它是「极少」档（一天最多切一两次），
     而且切的时候用户正盯着看，所以允许它有戏。
     除它之外再想破例，先回答「这个动作一天发生几次」。 */
  --t-reveal: 360ms;

  --sidebar-w: 244px;
  --sidebar-w-mini: 68px;
  /* 收起态那一列：一个方格尺寸、一个间隔、一个图标格。
     三个数管住用户提过三次的那三件事 —— 宽度一样、间隔一样、图标在正中。
     侧栏内容宽 = 68 - 12*2 = 44，所以方格 40 两边各留 2。 */
  --rail-size: 40px;
  --rail-gap: 6px;
  --rail-glyph: 22px;
  --rail-w: 304px;
  --gutter: 20px;
}

/* ---- 深浅色：系统跟随 + 手动挡（第 5 条）----

   原来只跟系统。加手动挡的理由不是「多一个开关」，是**办公室的实际情况**：
   同一台机器上午在窗边下午在里间，而 Windows 的自动切换是按时间不是按光线。

   两个 :root 块的值必须一模一样。**这是刻意的重复，不是漏合并** ——
   合并的办法只有 `light-dark()`，它要 Chrome 123+，
   而手机上是微信内置浏览器（X5 内核，版本未知）：
   `light-dark()` 不支持时**整套配色会直接失效**，那不是降级是崩。
   重复的代价是可能改一处忘另一处，所以写了用例盯着两块必须逐字相同
   （motion_test.go 的 TestBothDarkBlocksAgree）。

   `:not([data-theme="light"])` 让手动选「浅色」能压过系统的深色。 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --brand-mark: #F2F3FF;
    --tint: #9A9AE4;
    --lane-1: #8CA5F5; --lane-2: #4FC3D9; --lane-3: #D493EF;
    --lane-4: #74D18C; --lane-5: #F0A06A; --lane-6: #A9ADB5;
    --tint-2: #8A8ADA;
    --tint-soft: color-mix(in srgb, var(--tint) 18%, transparent);
    --on-tint: #14142A;

    --canvas: #0A0A10;
    --surface: #17171F;
    --surface-2: #1F1F29;
    --ink: #ECECF2;
    --ink-2: #B8B8C6;
    --mute: #8A8A9C;
    --line: rgba(255, 255, 255, 0.10);
    --line-strong: rgba(255, 255, 255, 0.16);
    --field-line: rgba(255, 255, 255, 0.46);

    --ok: #56C77E;
    --ok-bg: #14281C;
    --bad: #F2837A;
    --bad-bg: #2C1714;
    --late: #E0A05A;
    --late-bg: #2A1F12;
    --admin: #E0A05A;
    --on-admin: #2A1B08;

    --mat-thin: color-mix(in srgb, var(--surface) 56%, transparent);
    --mat-reg: color-mix(in srgb, var(--surface) 72%, transparent);
    --mat-thick: color-mix(in srgb, var(--surface) 88%, transparent);
    --specular: inset 0 1px 0 rgba(255, 255, 255, 0.10),
                inset 0 -1px 0 rgba(0, 0, 0, 0.30);
    --shadow: 0 12px 32px -14px rgba(0, 0, 0, 0.70);
    --shadow-lift: 0 24px 56px -20px rgba(0, 0, 0, 0.80);
    --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.30),
                   0 10px 22px -16px rgba(0, 0, 0, 0.60);
  }
}
:root[data-theme="dark"] {
    color-scheme: dark;
    --brand-mark: #F2F3FF;
    --tint: #9A9AE4;
    --lane-1: #8CA5F5; --lane-2: #4FC3D9; --lane-3: #D493EF;
    --lane-4: #74D18C; --lane-5: #F0A06A; --lane-6: #A9ADB5;
    --tint-2: #8A8ADA;
    --tint-soft: color-mix(in srgb, var(--tint) 18%, transparent);
    --on-tint: #14142A;

    --canvas: #0A0A10;
    --surface: #17171F;
    --surface-2: #1F1F29;
    --ink: #ECECF2;
    --ink-2: #B8B8C6;
    --mute: #8A8A9C;
    --line: rgba(255, 255, 255, 0.10);
    --line-strong: rgba(255, 255, 255, 0.16);
    --field-line: rgba(255, 255, 255, 0.46);

    --ok: #56C77E;
    --ok-bg: #14281C;
    --bad: #F2837A;
    --bad-bg: #2C1714;
    --late: #E0A05A;
    --late-bg: #2A1F12;
    --admin: #E0A05A;
    --on-admin: #2A1B08;

    --mat-thin: color-mix(in srgb, var(--surface) 56%, transparent);
    --mat-reg: color-mix(in srgb, var(--surface) 72%, transparent);
    --mat-thick: color-mix(in srgb, var(--surface) 88%, transparent);
    --specular: inset 0 1px 0 rgba(255, 255, 255, 0.10),
                inset 0 -1px 0 rgba(0, 0, 0, 0.30);
    --shadow: 0 12px 32px -14px rgba(0, 0, 0, 0.70);
    --shadow-lift: 0 24px 56px -20px rgba(0, 0, 0, 0.80);
    --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.30),
                   0 10px 22px -16px rgba(0, 0, 0, 0.60);
}

/* ---------- 02 基底 ---------- */

* { box-sizing: border-box; }
/* 三档全站字号。当前旧版字号成为“小”；新用户默认“标准”。所有正文令牌
   都是 rem，所以表格、聊天、管理页和移动端会一起变化，不需要逐页打补丁。 */
html { -webkit-text-size-adjust: 100%; font-size: var(--fs-root-standard); }
html[data-font-size="small"] { font-size: var(--fs-root-small); }
html[data-font-size="standard"] { font-size: var(--fs-root-standard); }
html[data-font-size="large"] { font-size: var(--fs-root-large); }

/* 全站滚动条只有这一套：不占视觉重量，实际滚动时才显出圆角滑块。
   新增滚动区域不要另写皮肤；输入框这类短内容可继续显式隐藏。 */
* { scrollbar-width: thin; scrollbar-color: transparent transparent; }
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { border: 2px solid transparent; border-radius: var(--r-pill); background-color: transparent; background-clip: content-box; }
.is-scrolling { scrollbar-color: var(--mute) transparent; }
.is-scrolling::-webkit-scrollbar-thumb { background-color: var(--mute); }

/* ---- 字体（2026-09-15）------------------------------------------

   用户：「标题啥的做出点层次感，别全站都一种字体不灵动。」

   先说清楚**中文字体为什么不自托管** —— 这是量过的，不是省事：
   思源黑体系（Noto Sans SC）按 unicode-range 分片的整套包解压 **71.06MB**，
   单字重也在 8MB 量级；而这套系统的服务器是公网小机器、客户端还有手机 4G。
   更关键的是**逐字回退会串字体**：中文标题里混着单号、人名、部门名，
   子集覆盖不到的字会掉回系统字体，同一行里两种黑体，比现在更难看。
   而 Windows 有雅黑、macOS / iOS 有苹方、Android 有思源，系统中文黑体本来就不缺。
   所以：**中文交给系统字族，层次由字号 / 字重 / 字距 / 颜色建。**

   真正值得自托管的是**拉丁与数字**：单个字重只有 14KB，本轮随包走 **600 / 700 两档共 28KB**，
   而单据号、日期、金额、计数在 OA 里到处都是，Windows 雅黑的数字恰恰是最平庸的一段。
   （一开始也下了 400，但没有任何地方以 400 用到 display 字族 —— 标题 700、区块标题 600、
   数字 700。**用不上的字体文件不留**，删掉了。）
   选 Manrope（SIL OFL 1.1，可商用，见 static/fonts/LICENSE-Manrope.txt）：
   几何人文无衬线，数字开阔、`1` 带脚、`0` 不带斜杠，做数据比系统拉丁更清楚。
   **没有选 Inter** —— 它是「没想清楚时」的默认解，跟这套界面已有的性格不搭。
   字体随二进制一起走（go:embed static/*），不引任何外部 CDN：
   和 GSAP 本地嵌入是同一个决定，内网、弱网、断网都不该掉字。 */
@font-face {
  font-family: "Manrope";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/static/fonts/manrope-latin-600-normal.woff2?v=1) format("woff2");
}
@font-face {
  font-family: "Manrope";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/static/fonts/manrope-latin-700-normal.woff2?v=1) format("woff2");
}

body {
  margin: 0;
  background: var(--canvas);
  color: var(--ink);
  font: var(--fs-body)/1.55 var(--font-ui);
  -webkit-font-smoothing: antialiased;
  min-height: 100dvh;
}

/* 这里原来铺着一层靛蓝渐晕，理由写的是「让玻璃有东西可以透」。
   那是倒果为因 —— 桌面上没有任何内容会滚到侧栏底下，
   渐晕是为一块假玻璃做的假背景。画布现在是干净的单色。 */

a { color: inherit; text-decoration: none; }
img { max-width: 100%; }

/* `.orgform button` 那 41 个 Windows 原生凸起按钮已经就地修过了。
   这两行是同一个病的通治：没戴类名的 button / select 会掉进浏览器默认字体
   （Chrome 是 Arial 13.3333px），而人员页一页里量出 8 种字号就有它一份。
   元素选择器特异性最低（0,0,1），任何带类名的规则都压得过它 ——
   它只在「谁都没管这个按钮」的时候才生效。 */
button, input, select, textarea { font-family: inherit; }
button, select { font-size: var(--fs-sub); }

:where(a, button, summary, input, select, textarea):focus-visible {
  outline: 2px solid var(--tint);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

.sr {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%);
}

/* 跳到正文。用 transform 移出视口而不是 display:none —— 后者拿不到焦点，
   等于这个链接不存在。拿到焦点才滑下来。 */
.skip {
  position: fixed; z-index: 200; top: 0; left: 12px;
  transform: translateY(-120%);
  padding: 11px 18px; border-radius: 0 0 var(--r-sm) var(--r-sm);
  background: var(--tint); color: var(--on-tint);
  font-size: var(--fs-sub); font-weight: var(--w-semibold);
  transition: transform var(--t-3) var(--ease-out);
}
/* 用 :focus 不用 :focus-visible。跳转链接只有键盘用户够得着，
   而 :focus-visible 对「代码调用的 focus()」在各家浏览器上认定不一 ——
   赌它匹配，代价是这个链接在某些浏览器上永远滑不下来，等于没有。 */
.skip:focus { transform: none; }
main:focus { outline: none; }

/* ---------- 03 材质 ---------- */

.glass {
  background: var(--mat-reg);
  -webkit-backdrop-filter: var(--blur);
  backdrop-filter: var(--blur);
  border: 1px solid var(--line);
  box-shadow: var(--specular), var(--shadow);
}

/* 不支持 backdrop-filter 时退回不透明底，否则文字会糊在背景上。 */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .glass { background: var(--surface); }
}

/* 图标。内联 SVG 精灵，认 currentColor —— emoji 在三个系统上长得都不一样，
   也没法跟着主题色走。线宽 1.7、圆头圆角，接近 SF Symbols 的手感。 */
.sprite { display: none; }
.side__ic, .gl, .tabbar .ic, .quick .ic, .fabmenu .ic, .secbox__ic {
  width: 20px; height: 20px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}
.gl { width: 16px; height: 16px; }
.tabbar .ic { width: 21px; height: 21px; }
.ai-metal-icon {
  display: block; object-fit: contain;
  filter: drop-shadow(1px 2px 1px rgb(0 0 0 / .22)) drop-shadow(3px 4px 4px rgb(0 0 0 / .16));
}

/* ---------- 04 外壳 ---------- */
/*
   桌面：左侧常驻悬浮导航（可收成图标条）+ 中间内容 + 右侧操作栏。
   平板：导航默认收成图标条，点开覆盖。
   手机：导航变成从左滑出的抽屉，主要入口落到底部标签栏 —— 拇指够得到。
*/

.shell { display: block; }

/* 侧栏 ------------------------------------------------ */
.side {
  position: fixed;
  z-index: 60;
  /* 和内容面板共用同一个窗口边距。散成三个 12 的那一版，
     改其中一个另外两个不会跟着走 —— 用户看到的就是那个结果
     （「高度不一致，没有对齐」）。 */
  left: var(--frame);
  top: var(--frame);
  bottom: var(--frame);
  width: var(--sidebar-w);
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 14px 12px calc(12px + env(safe-area-inset-bottom));
  /* 和内容面板同一个角。两块并排、边距一样、角不一样，
     眼睛立刻能看出来「这两块不是一套的」。 */
  border-radius: var(--r-lg);
  background: var(--mat-thick);
  -webkit-backdrop-filter: var(--blur);
  backdrop-filter: var(--blur);
  border: 1px solid var(--line);
  box-shadow: var(--specular), var(--shadow);
  /* width 是布局属性，动它会让主区每帧重排 —— 检测器报的就是这条，
     报得对。留着它是权衡后的决定，不是漏改：
     收起侧栏是**用户按一下才发生一次**的事，不在滚动或输入的热路径上；
     而这条滑动正是用户点名要的那种原生感（「液态玻璃、很苹果」）。
     真要去掉重排，就得让主区宽度不跟着变 —— 那样收起侧栏就白收了。
     配套的另一半在 main 的 margin-left 上，两条一起改或一起留。 */
  transition: width var(--t-4) var(--ease-out), transform var(--t-4) var(--ease-out);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .side { background: var(--surface); }
}

.side__head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 2px 6px 12px;
  min-height: var(--tap);
  /* 🔴 头部**不许被压缩**。竖排 flex 里它原来是 `0 1 auto`，窗口一矮就被压：
     收起态它的内容（logo 40 + 间隔 6 + 折叠键 40 = 86）比压出来的盒子高，
     两个孩子于是溢出到下一个兄弟身上 —— 实测 500px 高时头部被压到 44.9px，
     **折叠键正好落在搜索键上**，就是用户报的「搜索和展开重叠」。
     该收缩的是导航，不是头部；见下面 .side__nav。 */
  flex: none;
}
.side__mark {
  flex: none; width: 30px; height: 30px;
  color: var(--brand-mark); display: grid; place-items: center;
}
.brand-svg { display: block; width: 100%; height: 100%; }
.side__mark > .brand-svg { width: 30px; height: 30px; }
.side__name {
  font-size: var(--fs-sub); font-weight: var(--w-semibold);
  letter-spacing: -0.01em; white-space: nowrap;
  overflow: hidden;
}

/* 圆形图标按钮 —— 收起/展开、关闭抽屉都用它 */
.iconbtn {
  flex: none;
  width: 34px; height: 34px;
  display: grid; place-items: center;
  border: 0; padding: 0; cursor: pointer;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--ink) 7%, transparent);
  color: var(--ink-2);
  font-size: var(--fs-body); line-height: 1;
  transition: background var(--t-2) var(--ease-out), transform var(--t-2) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) { .iconbtn:hover { background: color-mix(in srgb, var(--ink) 12%, transparent); } }
.iconbtn:active { transform: scale(0.97); transition-duration: var(--t-1); }

/* 🔴 手指够得着：34px 的圆钮在鼠标下没问题，在拇指下不够（44px 是下限）。
   **不改画出来的尺寸，只把可点区域撑开** —— 撑尺寸会把顶栏和侧栏整排重排，
   而这几颗钮在每一页上都在（菜单 / 铃铛 / 偏好 / 退出 / 头像）。
   条件用 pointer: coarse 不用宽度：手指还是鼠标是这件事的真正依据，
   iPad 横屏也是手指。

   进这份名单的判据只有一条：**在 375px 上量出来不足 44px 的可点控件**。
   翻页那几颗是 34×34（2026-09-09 量的），和圆钮一样差 10px。 */
@media (pointer: coarse) {
  .iconbtn, .side__avatar, .lpager a { position: relative; }
  .iconbtn::after, .side__avatar::after, .lpager a::after {
    content: ""; position: absolute; inset: -5px; border-radius: inherit;
  }
}

/* 收起键只在桌面有意义；手机上抽屉是覆盖层，那里要的是「关掉」。 */
.side__toggle { margin-left: auto; }
.side__toggle .gl { transform: rotate(180deg); transition: transform var(--t-4) var(--ease-out); }
.side.mini .side__toggle .gl { transform: none; }
.side__grab { display: none; }
[data-drawer-ready]:focus { outline: none; }
.side__open, .side__close { display: none; margin-left: auto; }
.side__close .gl { transform: none; }

.side__nav {
  display: flex; flex-direction: column; gap: 2px;
  overflow-y: auto; overscroll-behavior: contain;
  margin: 0 -4px;
  padding: 0 4px;
  /* 🔴 这两行是「这是唯一可以让步的东西」。
     可滚动孩子默认 `min-height: auto` —— 它不肯矮于自己的内容，
     所以它宁可用滚动条也不收缩，而是**溢出自己的盒子**：
     实测 682px 高时导航内容比盒子高 45px、内容压在页脚上 38.6px；
     640px 高时压 73.3px；500px 高时压 188.9px。
     用户看到的就是「头像压住 AI / 设置」。只有它收缩，才有滚动，
     也才轮到头部和页脚保持原样。 */
  flex: 1 1 auto;
  min-height: 0;
}

.side__link {
  display: flex; align-items: center; gap: 12px;
  min-height: 42px; padding: 0 12px;
  border-radius: var(--r-sm);
  color: var(--ink-2);
  font-size: var(--fs-sub); font-weight: var(--w-semibold);
  white-space: nowrap;
  transition: background var(--t-2) var(--ease-out), color var(--t-2) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) { .side__link:hover { background: color-mix(in srgb, var(--ink) 6%, transparent); } }
.side__link:active { background: color-mix(in srgb, var(--ink) 10%, transparent); transition: none; }
.side__link.on { background: var(--tint); color: var(--on-tint); }
.side__link.on .side__ic { opacity: 1; }
.side__ic { width: 22px; height: 22px; opacity: 0.85; }
.side__txt { overflow: hidden; text-overflow: ellipsis; }
.side__badge {
  margin-left: auto;
  font-size: var(--fs-cap); font-weight: var(--w-bold);
  font-variant-numeric: tabular-nums;
  padding: 1px 7px; border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--ink) 9%, transparent);
  color: var(--ink-2);
}
.side__link.on .side__badge { background: color-mix(in srgb, var(--on-tint) 22%, transparent); color: inherit; }

/* 管理区用另一种颜色。这里每一次点击都会改变别人能做什么，
   不该和日常操作长得一样 —— 进了管理区一眼就该知道自己在哪。 */
.side__link.adm { color: var(--admin); }
.side__link.adm.on { background: var(--admin); color: var(--on-admin); }

.side__sep {
  height: 1px; margin: 8px 10px;
  background: var(--line);
}
/* 页脚同样不参与压缩：它是「我是谁 / 切换 / 退出」，被压就会自己叠在一起。 */
.side__foot { margin-top: auto; padding-top: 8px; flex: none; }
.side__user {
  display: flex; align-items: center; gap: 10px;
  padding: 6px 8px; border-radius: var(--r-sm);
}
/* 侧栏那个头像现在就是 .ava（043 起它是一个链接，点开是「我的资料」）——
   尺寸、形状、底色全部由 .ava 给，这里一条都不重复写。
   ⚠️ 原来它是「淡底 + 品牌色字」，和通讯录里的实心圆是两种东西；
   同一个人在两处长得不一样，是最容易积累出来的不一致。 */
.side__avatar { text-decoration: none; }
.side__who {
  font-size: var(--fs-foot); font-weight: var(--w-semibold);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.side__out { margin-left: auto; }

/* 收起态：只剩图标。文字用 grid 折叠而不是 display:none —— 有过渡。 */
/* ---- 收起侧栏：一次交接，不是一次硬切（第 3 条）----

   原来文字是 `display: none` —— 宽度滑了 320ms，文字在第 0 帧就消失了。
   两件本该是一件事的动作被拆成了「一下」和「一段」，
   看起来像页面先坏了一下再恢复。

   现在：**文字先淡出并左移 4px，图标一动不动，宽度随后收拢。**
   展开时反过来 —— 文字晚 --lag 再进，等宽度先让出位置。
   谁先谁后是有讲究的：收起时先走的是要消失的东西，
   展开时先到的是承载它的容器，两次都不会出现「内容溢出容器」的那一帧。

   图标不动是这一条的核心，所以 mini 态**不改 padding 也不居中** ——
   一居中图标就跟着滑，那正是「硬切」换了个方式回来。
   宽度动画是布局属性，全站只允许这一处（理由在 .side 上那段注释里）。 */
.side.mini {
  width: var(--sidebar-w-mini);
  /* 竖排 flex 里 align-items 管的是**横向**。默认 stretch 碰上
     一个定死了宽度的孩子，结果是靠左站 —— 搜索键（.side 的直接孩子）
     因此比别人歪了 1.3px，而它旁边的 .side__nav 是被拉满宽再自己居中的。 */
  align-items: center;
}
.side__txt, .side__name, .side__badge, .side__who {
  overflow: hidden; white-space: nowrap;
  transition: opacity var(--t-2) var(--ease-out) var(--lag),
              transform var(--t-2) var(--ease-out) var(--lag),
              max-width var(--t-2) var(--ease-out) var(--lag),
              max-height var(--t-2) var(--ease-out) var(--lag);
  max-width: 12rem; max-height: 3rem;
}
.side.mini .side__txt,
.side.mini .side__name,
.side.mini .side__badge,
.side.mini .side__who {
  opacity: 0; transform: translateX(-4px);
  /* ⚠️ **两个方向都要塌。**只塌宽度是横排时代的写法：
     收起态的头部和用户行是**竖排**，一段 max-width:0 的文字在竖排里
     照样占着一整行的高 —— 量出来是 21.7px 和 20.1px，
     正好就是用户说的「天和折叠键之间过远」「头像和设置之间过远」。 */
  max-width: 0; max-height: 0;
  transition-delay: 0s;   /* 收起时立刻开始淡出，不等宽度 */
}
/* 侧栏名和用户名坐在**两个图标中间**（天↕折叠、头像↕设置）。
   塌成 0 尺寸还不够 —— flex 的 gap 在它两边**各算一次**，
   于是这两处的间隔是 6+6=12，而别处是 6。用户量得出来：
   「不应该某个和某个过近、某个和某个过远」。
   拿出文档流才是真的不占位；仍然保留淡出，所以收起那一下不是硬切。 */
.side.mini .side__name, .side.mini .side__who {
  position: absolute; pointer-events: none;
}
/* ══ 收起态：一列一样大的方格，一个间隔，一条中轴 ═════════════
   用户 2026-09-08（第三次指着这块）：「头像、logo、按键应该**间隔都一样**，
   不应该某个和某个过近、某个和某个过远，而且他们的**宽度不管是圆形还是方形，
   都应该一样宽**，缩小后的已选中方块也一样。现在已选中方块也有个问题就是
   **图标不在已选中方块的正中，有点歪**。」

   量出来他三条全对：
     间隔  天→折叠 25.7 / 折叠→搜索 14 / 搜索→首页 6 / 导航之间 2 /
           工作台→管理 21 / 头像→设置 24.1   —— 六种
     宽度  徽章 30、其余 42.7            —— 两种
     居中  选中药丸中心 46，里面的图标中心 47.7  —— 差 1.7px

   前两条是「每个部件自带自己的尺寸和边距，收起时谁也没让谁」。
   第三条是**上一版那条决定自己的代价**：为了「收起时图标一动不动」，
   图标被钉在展开态的位置（左内缩 12px），而药丸是整行宽的 ——
   钉住的点和药丸的中心本来就不是同一个点。

   🔴 **所以这一版推翻了「收起是一次交接、图标一动不动」那条。**
   它买到的是「什么都不动」，代价是这一列永远不可能是对齐的。
   换成：一个尺寸 --rail-size、一个间隔 --rail-gap、全部水平居中。
   图标在收起时横向挪 1.7px —— 在一段 320ms 的宽度动画里根本看不见，
   而「像一列设计过的东西」是一直看得见的。 */
.side.mini .side__head,
.side.mini .side__user,
.side.mini .side__nav {
  flex-direction: column;
  align-items: center;
  gap: var(--rail-gap);
  padding: 0;
  margin: 0;
}
/* 容器一律不再自己加边距：`.side` 自己的 gap 就是那 6px。
   头部留一个 padding-bottom 的话，头部里面是 6、头部到下一个是 6+6=12,
   而这两处在屏幕上是一模一样的两个图标之间 —— 用户会（也确实）看出来。 */
.side.mini .side__foot { padding-top: 0; }

/* 🔴 收起态**不要占位的滚动条**（用户 2026-09-15：「收起后从首页到 AI 都向左偏了」）。
   全站那条 `*::-webkit-scrollbar { width: 8px }` 也管到这里：收起态内容宽只有
   68 − 12×2 = 44px，8px 的条一进去，居中的 40px 方格就被挤掉一半的余量 ——
   `align-items: center` 下每个图标**左移 4px**。头部、搜索、页脚不在这个滚动容器里，
   于是「只有导航那几项偏了」。而且 `overflow-y: auto` 会把另一轴也算成 auto，
   40 + 8 > 44 还会再冒一条横向条出来。
   仍可滚（滚轮 / 触控板 / 键盘），只是不画出来 —— 一列 40px 的图标里，
   滚动条本来就只是噪声。 */
.side.mini .side__nav { scrollbar-width: none; }
.side.mini .side__nav::-webkit-scrollbar { width: 0; height: 0; }
.side.mini .side__out { width: auto; margin: 0; padding: 0; display: block; }

/* 一个尺寸。圆的、方的、药丸的，宽高全一样 —— 用户原话
   「不管是圆形还是方形，都应该一样宽」。
   ⚠️ 只点名该显示的那几个，**不要写 `> *`**：头部里还躺着两个
   平板抽屉专用的键（.side__open / .side__close），它们在桌面上是
   display:none，而 `> *` 会把它们一起打开，顶上会冒出三个箭头。 */
.side.mini .side__mark,
.side.mini .side__avatar,
.side.mini .side__toggle,
.side.mini .side__prefs,
.side.mini .side__out .iconbtn,
.side.mini .side__link {
  width: var(--rail-size);
  height: var(--rail-size);
  min-width: var(--rail-size);
  min-height: var(--rail-size);
  flex: none;
  padding: 0;
  margin: 0;
  /* flex 而不是 grid：grid 会给旁边那段零尺寸的文字**再排一行**，
     图标于是被挤得偏上；flex 里零尺寸的兄弟一点空间都不占。 */
  display: flex; align-items: center; justify-content: center;
  gap: 0;
}
/* 图标格也统一：三种尺寸（导航 22 / iconbtn 16 / 徽章里的字）
   在同一列里是看得出来的。 */
.side.mini .side__toggle .gl,
.side.mini .side__prefs .gl,
.side.mini .side__out .iconbtn .gl {
  width: var(--rail-glyph); height: var(--rail-glyph);
}
/* 分隔线在收起态去掉：它会造出一个和别处不一样的间隔，
   而它要表达的「下面是管理区」已经由 .adm 那一档颜色说清楚了。
   **一条线换不来破坏整列节奏的代价。** */
.side.mini .side__sep { display: none; }

/* 🔴 `side-settle` 那个入场动画删掉了（2026-09-08）。
   它是为「收起时按钮要动画挪到合适位置」加的，但 CSS 动画分不出
   「刚刚收起」和「打开时本来就是收起的」——**而侧栏状态是记住的**，
   于是它在每一次换页都重放一遍：整列静止不动，唯独这三个键弹一下。
   排查这一列的对齐时我自己都被它骗过：量到的 36.8×36.8 是它第一帧的
   scale(.92)，看起来像「有几个键比别人小」。

   现在不需要它了：这一列是**居中**的，而侧栏的宽度本来就是过渡的，
   所以收起的那一下每个图标都是跟着宽度连续滑到新位置的 ——
   用户要的那个「挪过去」由布局给出，不用再写一遍。**删优于加。** */

/* ---- 矮窗口：先收紧密度，别让用户去滚一条看不见的条（2026-09-15）----

   收起态这一列是**定高**的（fixed 的 top/bottom 各 12）。九个导航项按 40 / 6 的密度
   摊开需要：头部 76.6 + 搜索 40 + 导航 408.4 + 页脚 132 + 三个 6px 间隔 = 675，
   再加上下 12 的窗框和侧栏自己的 14 / 12 内边距，**窗口高要到 725 才装得下**。
   13 寸笔记本开浏览器后可视高度常在 660–700 —— 那份差额原来既压头部（折叠键落到搜索键上），
   又让导航溢出压住页脚。收紧密度比让用户去滚一条藏在 40px 图标列里的滚动条划算得多。

   每一档都按上面的算式倒推，留出余量，不是拍出来的数：
     36 / 4 → 头部 76 + 搜索 36 + 导航 356 + 页脚 116 + 间隔 18 = 602 → 窗口高 ≥ 652
     32 / 3 → 头部 67 + 搜索 32 + 导航 312 + 页脚 102 + 间隔 18 = 531 → 窗口高 ≥ 581
     28 / 2 → 头部 58 + 搜索 28 + 导航 268 + 页脚  88 + 间隔 18 = 460 → 窗口高 ≥ 510
   再矮（< 500）就不硬塞了：那时导航滚动、页脚仍然钉在底部，不重叠 —— 只是不好看。

   ⚠️ 这几档只算九个导航项。管理员 / 待用印 / 用印台账那些入口多的账号会更长，
   超出的部分走滚动（收起态的条不占位，见上面 .side.mini .side__nav），不会重叠。 */
@media (max-height: 730px) {
  :root { --rail-size: 36px; --rail-gap: 4px; --rail-glyph: 20px; }
}
@media (max-height: 651px) {
  :root { --rail-size: 32px; --rail-gap: 3px; --rail-glyph: 18px; }
}
@media (max-height: 580px) {
  :root { --rail-size: 28px; --rail-gap: 2px; --rail-glyph: 17px; }
}

/* 主区 ------------------------------------------------ */
/* ⚠️ 2026-09-08 第二次改：**侧栏和内容面板是同一个窗口框里的两块**。

   用户原话：「左侧导航栏竟然和右侧视窗卡片离那么远，浪费了很多横向空间，
   而且竟然高度不一致，没有对齐。」他是对的，而且原因是两条各自合理的规则叠在了一起：

     ① 侧栏是 `position: fixed`，四边留 12px —— 它是一块贴着窗口边的浮层；
     ② 内容用 `.page-wrap { max-width; margin-inline: auto }` **居中** ——
        于是窗口越宽，它离侧栏越远，右边也空着。

   ②这条在没有侧栏的页面上是对的（登录页），在有侧栏的页面上就是错的：
   **内容不是「页面中央的一栏」，是「窗口里挨着侧栏的那一块」。**
   Finder / Mail / 系统设置都是这个几何：一个窗口边距，两块并排，等高。

   所以现在只有一个数字：`--frame`（12px）。侧栏四边是它，
   面板的上、右、下也是它，面板左边贴着侧栏 + 一个同样的 12。 */
main {
  padding: var(--frame) var(--frame) var(--frame) 0;
  transition: margin-left var(--t-4) var(--ease-out);
}
/* 只有真的有侧栏时才让位。登录页没有侧栏，让了位就整页偏右。 */
.side ~ main { margin-left: calc(var(--sidebar-w) + var(--frame) * 2); }
.side.mini ~ main { margin-left: calc(var(--sidebar-w-mini) + var(--frame) * 2); }
/* 登录页没有侧栏，让整块居中，而不是贴在内容列的左边 */
.page-wrap:has(.login) { justify-items: center; }
/* 登录页自己就是一张卡（.login__card），列不要再套一层面板 ——
   两层白底叠在一起，那张卡就不是一张卡了。 */
.col:has(.login) {
  max-width: none; width: 100%;
  background: none; border: 0; box-shadow: none; padding: 0;
}

.page-wrap {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--frame);
  /* 不封顶、不居中 —— 见上面 main 那段。窗口有多宽，这一块就有多宽。
     「一行字太长读不完」由**正文自己**的 62~76ch 上限管，
     不该由整块面板来管：清单和表格吃得下宽度，段落吃不下，
     而这两种东西在同一块面板里。 */
}
/* 有右栏时才分两列。:has 不支持时右栏落到下面，仍然可用。 */
.page-wrap:has(.rail) { grid-template-columns: minmax(0, 1fr) var(--rail-w); }

/* ⚠️ 2026-09-08：内容列**本身**是一块面板，这是这一轮最要紧的一条改动。
   在此之前 `<main>` 和 `.col` 都是透明的，于是每一页的标题、说明、
   空状态、工具条**直接画在页面底色上** —— 用户的原话：
   「右边看起来很奇怪像是未完成的系统 —— 直接背景色+文字。太原始了。」

   他是对的，而且根因是量得出来的：侧栏是一块有边、有三层影、有圆角的浮层，
   内容区一样都没有。**同一个界面里一半有材质一半没有，没有材质的那一半
   就会被读成「还没做完」**，而不是「刻意的朴素」。

   为什么是给整列一块面板，而不是给二十个子元素各挂一次影子：
   子元素（清单 / 表格 / 卡片 / 详情）本来就在白底上，缺的从来不是它们，
   是**它们和页面底色之间那一层**。给列上底之后，
   「卡片嵌在面板里」这个层级第一次在屏幕上成立 —— 这也是圆角要分三档的原因。 */
.col {
  min-width: 0;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
  /* 底下不留 88 给悬浮的「发起」按钮：那 88 会让每一页都比一屏高出一点，
     于是**每一页都有一条本来不必要的滚动条**。按钮是 fixed 的，
     它浮在面板右下角的空白上；清单是左对齐的文字，压不到。 */
  padding: 22px 24px 28px;
  /* **撑满一屏高。**内容少的页面（比如待办空的时候）原来只有 360px 高，
     底下是一大片页面底色 —— 用户 2026-09-08 的原话：
     「宽度限制住以后有大块空白区域」。
     一块只占屏幕上三分之一的面板，读起来仍然像「这一页还没做完」。 */
  /* 和侧栏严格等高：侧栏是 top/bottom 各 12 的 fixed，
     所以面板的高度就是「视口 − 上下两个 frame」。
     内容多的时候它自然往下长，短的时候不会缩成半屏。 */
  min-height: calc(100dvh - var(--frame) * 2);
}
/* 有右栏时给正文一个上限，免得右栏被挤成一条。没有右栏就铺满。 */
.page-wrap:has(.rail) .col { max-width: 1120px; }

/* 右侧操作栏 ------------------------------------------ */
.rail {
  align-self: start;
  position: sticky;
  top: 24px;
  display: grid;
  gap: 12px;
}
/* 格子是 grid item，默认 min-width:auto —— 缩不到自己的 min-content 以下。装着链的那一格
   min-content 是 270，1280 宽上右栏只有 253（268 减竖滚动条），每个发起页都被它撑出去
   （审查量到的 clientWidth 253 / scrollWidth 269）。PITFALLS #250 那一族，落在 grid 上。 */
.rail > * { min-width: 0; }
/* 右栏面板是内容，不是控制层：它固定在右边跟着页面滚，底下没有东西经过。 */
.rail__box {
  padding: 16px;
  border-radius: var(--r-lg);
  background: var(--surface);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-card);
}
.rail__t {
  margin: 0 0 10px;
  font-size: var(--fs-cap); font-weight: var(--w-bold);
  letter-spacing: 0.06em;
  color: var(--mute);
}

/* 主操作。桌面上是右栏顶上的一块；窄屏上它跟着右栏落回正文下面，
   并重新贴到屏幕底部 —— 手机是单手拿着用的。 */
/* 桌面上它是右栏里的一块面板（内容层，实底）；≤1024 它变成贴屏幕底的
   操作条（控制层），玻璃与阴影在那一段里才加上。
   同一个类落在两种层里，材质就得跟着**层**走，不能跟着类走。 */
.rail__act {
  padding: 16px;
  border-radius: var(--r-lg);
  background: var(--surface);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-card);
}
.rail__act textarea { margin-bottom: 10px; }
/* 一排动作。顶层规则：这个类在十来个模板里都写着，此前只在 .rail__act / .actbar / .sheetbar
   三个作用域里有样式（PITFALLS #171 那一族），别处的 class="btns" 静默地什么都不发生 ——
   /me 的「保存 / 在通讯录里看看」就是靠行内空格隔开的。一个类表达一个含义就该是顶层规则（#211）。 */
.btns { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
/* 分体按钮自带的 22px 上边距是给它直接站在 .form 网格里用的；在动作行里它会把自己顶低 11px（1440 上量到）。 */
.btns > .split { margin-top: 0; }
.rail__act .btns { display: flex; gap: 10px; }
.rail__act .btns button { flex: 1; padding: 0 12px; }
.rail__act > form > button.primary { width: 100%; }

.rail__kv {
  display: grid; grid-template-columns: 4.2rem 1fr;
  gap: 9px 12px; margin: 0 0 16px; font-size: var(--fs-foot);
}
.rail__kv dt { color: var(--mute); }
.rail__kv dd { margin: 0; overflow-wrap: anywhere; }
.rail__wide { width: 100%; }

/* 管理模式标记。原先是 main 顶上一道 3px 橙线 —— 在浮起的版式里那道线
   无处可依。改成一枚 pill，位置固定在内容最上方，作用一样：
   进了管理区一眼就知道自己在哪。 */
.admbadge {
  display: inline-flex; align-items: center; gap: 7px;
  margin: 0 0 14px; padding: 4px 12px 4px 9px;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--admin) 13%, transparent);
  color: var(--admin);
  font-size: var(--fs-cap); font-weight: var(--w-bold); letter-spacing: 0.02em;
}
.admbadge span {
  width: 6px; height: 6px; border-radius: var(--r-pill);
  background: currentColor;
}

/* 顶栏（手机与平板） ---------------------------------- */
.topbar { display: none; }

/* 底部标签栏（手机） ---------------------------------- */
.tabbar { display: none; }

/* 抽屉遮罩 -------------------------------------------- */
.scrim { display: none; }

/* ---------- 05 排版 ---------- */

/* 层次表（2026-09-15）—— 三级，靠字族 + 字号 + 字重 + 字距四件事分开，
   不用颜色去分（颜色留给状态）。改字距是因为 Manrope 的字面偏宽，
   大字号下原来那点收紧不够，标题会显得松。 */
h1.page {
  font-family: var(--font-display);
  font-size: var(--fs-large);
  font-weight: var(--w-bold);
  letter-spacing: -0.022em;
  line-height: 1.2;
  margin: 0 0 6px;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
h2.sec {
  font-family: var(--font-display);
  font-size: var(--fs-head);
  font-weight: var(--w-semibold);
  letter-spacing: -0.012em;
  margin: 30px 0 10px;
  display: flex; align-items: center; gap: 8px;
}
/* 被当成锚点落点的那几个小标题，跳过来时别贴着窗口顶边。
   写成 [id] 而不是给所有 h2.sec：不是落点的那些不需要留这段。
   同 .gcard[id]。 */
h2.sec[id] { scroll-margin-top: 16px; }
.hint {
  color: var(--mute);
  font-size: var(--fs-foot);
  margin: 0 0 16px;
  max-width: 62ch;
}

/* 数字分两档，别一刀切 —— 表格单元格是「中文标签 + 数字」混排，
   整格换成 Manrope 会让中文旁边的英文标签也跟着变形，那是另一种不统一。

   ① 数字当主角的地方（KPI 大数字、侧栏角标）→ 换字族 + 等宽：
      和标题共用 Manrope 的骨架，于是「标题 + 数字」读起来是一家人。
   ② 数字只是列里的一格（表格）→ 只加等宽，不动字族。
      `tabular-nums` 让每个数字等宽，位数变化时列不抖动 ——
      这是「一眼看出被设计过」里最便宜的一条。 */
:where(.kpi b, .side__badge) {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
}
:where(table) { font-variant-numeric: tabular-nums; }
.hint.none { margin: 0; }
.hint.bad { color: var(--late); }

/* 🔴 「这儿本来该有个人 / 岗位，现在没有」。全站一个含义，所以是一条顶层规则。
   在这之前它只在 `.step__who` 和 `.mecard__who` 底下有颜色 ——
   流程编辑器右栏那句「该人员已停用」**从写下那天起就是一句普通灰字**，
   和它旁边的名字长得一样（#167 那一族：有 class，只是那个 class 在这儿不带样式）。
   `font-style: normal` 是因为三处调用方一半用 `<i>` 一半用 `<span>`，
   而「已停用」是不是斜体不该由标签名决定。 */
.vacl { color: var(--late); font-style: normal; }
/* 空状态。**这是整套界面里最容易露怯的一格。**
   原来它是一个 <p>：白底上居中一句灰字，上下 56px 空白。
   用户看到的首页就是这个 ——「像是未完成的系统」里，它是最像的一处。

   一句灰字有两个毛病：它和「内容还没加载出来」长得一模一样；
   而且它只说了「没有」，没说「那我现在该干什么」。
   现在是一块虚线围起来的区域（虚线本身在说「这里本来会有东西」）
   加一行更醒目的话，页面各自在后面接一个动作链接。 */
.empty {
  color: var(--mute);
  padding: 40px 24px; text-align: center;
  font-size: var(--fs-sub);
  background: var(--surface-2);
  border: 1px dashed color-mix(in srgb, var(--ink) 14%, transparent);
  border-radius: var(--r-md);
  margin: 4px 0;
}
.empty b { display: block; color: var(--ink-2); font-size: var(--fs-body); margin-bottom: 4px; }
/* 动作另起一行。和说明挤在同一行时，它读起来像句子的一部分而不是一个按钮。 */
.empty .btn { display: block; width: fit-content; margin: 14px auto 0; }
.empty.ok { color: var(--ok); }

/* 「注」：对着上面那个东西解释规则的一段话（考勤那两段最典型 ——
   迟到怎么算、「只有一条记录」为什么不算早退）。
   左边那道线在说「这几行是**对上面那个东西的解释**」，不是页面的下一段内容。

   ⚠️ 这里第一版是按**位置**做的（`.col > .hint` 全都框起来），当场砸了：
   单据页那个「8 张」也是 `<p class="hint">`，于是两个字被框成了一条大灰带。
   根因是 `.hint` **按外观命名**（小灰字），不按用途 —— 页面里
   「解释」「计数」「字段下的提示」全叫这一个名字，位置区分不了它们。
   和 `i-down` 其实是下载图标是同一个病（PITFALLS #159）。
   所以改成按用途显式挂 `.note`，不去猜。 */
.note {
  background: var(--surface-2);
  border-left: 3px solid color-mix(in srgb, var(--tint) 34%, transparent);
  border-radius: 0 var(--r-md) var(--r-md) 0;
  padding: 12px 14px;
  margin: 14px 0;
  max-width: 76ch;
  color: var(--ink-2);
  font-size: var(--fs-sub);
}
/* 说明框里也会放一小段清单（偏离、没人能开的章）：第一行是结论，清单跟在后面。 */
/* 只有「结论一行 + 清单」那种 .note 的首句是块；句子中间的 <b> 照旧行内 ——
   第二批写成 .note > b 时把 /attendance 和 /admin/units 上两段说明拆成了断句（截图才看见）。 */
.note > b:has(+ ul) { display: block; margin-bottom: 4px; }
.note ul { margin: 0; padding-left: 18px; }
.note > .hint { margin: 8px 0 0; }

/* ---------- 控制台（2026-09-08，登录后的第一页） ----------

   排布按「结果 → 在等谁 → 点进去」三层：
   顶上一排数字（每张都能点）→ 两列面板 → 每一行链到那张单。

   为什么两列而不是网格：左边那两块（等我处理 / 我发起的在路上）是
   **要动手的事**，右边三块（公告 / 考勤 / 常用）是**看一眼的事**。
   摆成等大的网格会让这两类看起来一样重，而它们的点击率差一个量级。 */
.kpis {
  display: grid;
  /* 永远一排四个：auto-fit 在 820 宽上排成 3+1，在 393 上退成一列（审查量到的）。 */
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 12px;
  margin: 4px 0 20px;
}
/* 首页上这一排住在 .dash 里（和待办面板同一个容器，手机上才换得了位置）：桌面横跨两列。 */
.dash > .kpis { grid-column: 1 / -1; margin: 0; }
.kpi {
  display: block;
  padding: 14px 16px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface-2);
  text-decoration: none;
  color: inherit;
  transition: border-color var(--t-2) var(--ease-out),
              transform var(--t-2) var(--ease-out);
}
.kpi b {
  display: block;
  font-size: var(--fs-display); font-weight: var(--w-bold); line-height: 1.1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}
.kpi span { display: block; margin-top: 2px; font-size: var(--fs-foot); color: var(--mute); }
/* 有事的那张才上色。四张全上色等于四张都不显眼 ——
   而这一排存在的意义就是**一眼看出有没有事**。 */
.kpi--hot { background: color-mix(in srgb, var(--tint) 8%, var(--surface)); }
.kpi--hot b { color: var(--tint); }
@media (hover: hover) and (pointer: fine) {
  .kpi:hover { border-color: color-mix(in srgb, var(--tint) 40%, var(--line)); }
}

.dash { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 18px; align-items: start; }
.dash__main, .dash__side { display: grid; gap: 16px; min-width: 0; }

.dpanel {
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface-2);
  padding: 14px 16px 16px;
}
.dpanel__hd {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px; margin-bottom: 10px;
  flex-wrap: wrap; /* 标题和右边的表单挤不下就换行，不许把标题压成一字一行（375 上量到过） */
}
.dpanel__hd h2 { margin: 0; font-size: var(--fs-sub); font-weight: var(--w-bold); font-family: var(--font-display); letter-spacing: -0.01em; }
.dpanel__hd a { font-size: var(--fs-foot); color: var(--mute); text-decoration: none; white-space: nowrap; }
@media (hover: hover) and (pointer: fine) { .dpanel__hd a:hover { color: var(--tint); } }
/* 面板里的卡片回到白底：它嵌在灰面板里，再用灰就没有层次了。 */
.dpanel .card { background: var(--surface); }
.dpanel .empty { background: var(--surface); padding: 24px 16px; }

.dlist { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
.dlist a {
  display: block; padding: 7px 8px; border-radius: var(--r-sm);
  text-decoration: none; color: inherit; font-size: var(--fs-sub);
}
@media (hover: hover) and (pointer: fine) {
  .dlist a:hover { background: var(--surface); }
}
.dnone { margin: 0; color: var(--mute); font-size: var(--fs-foot); }

/* 本月考勤：一行一个数。**不画趋势图** —— 一个员工看自己这个月的
   打卡趋势，看完不会做任何事，而这一页只放「要不要做点什么」的东西。 */
.dstat { margin: 0; display: grid; gap: 1px; }
.dstat > div {
  display: flex; justify-content: space-between; align-items: baseline;
  padding: 7px 8px; border-radius: var(--r-sm); background: var(--surface);
}
.dstat dt { color: var(--mute); font-size: var(--fs-foot); }
.dstat dd { margin: 0; font-weight: var(--w-bold); font-variant-numeric: tabular-nums; }
.dstat > div.bad dd { color: var(--bad); }
.dstat > div.warn dd { color: var(--late); }

/* 侧栏里的快捷入口竖着排 —— 320px 宽放不下首页那种横向一排。 */
.quick--side { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 0; }

@media (max-width: 1024px) {
  .dash { grid-template-columns: minmax(0, 1fr); }
}
/* 手机：急着批单的人先看到的必须是待办。数字那一排退到待办后面、收成两列小条
   （数字和标签并排一行，两行一共不到 100px）；页头那颗「发起」藏起来 —— 右下角已经浮着一颗。
   审查量到的：393 上任务区从 y≈667 才开始，前面是一屏摘要。 */
@media (max-width: 700px) {
  .dash__main { order: 1; }
  .dash > .kpis { order: 2; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
  .dash__side { order: 3; }
  .kpi { display: flex; align-items: baseline; gap: 8px; padding: 10px 12px; }
  .kpi b { font-size: var(--fs-title); }
  .kpi span { margin: 0; }
  /* 写成后代选择器不是为了限定位置：这段媒体查询排在 .btn 的基础规则**前面**，同为一层类名时后者赢，
     `.home__new { display: none }` 量出来是 inline-flex（复量时才看见，用例只看源码看不出顺序）。 */
  .pagehead__act .home__new { display: none; }
}

/* 面板里的卡片改用二级底色。
   `.col` 现在自己是白面板，卡片再用白底就只剩一条 10% 的边 ——
   在 1440 的屏幕上那条边几乎看不见，一组卡片会糊成一片。
   **底色差一档 + 圆角小一档**，「卡片嵌在面板里」才读得出来。
   数据表（.ledger）**故意不在这张名单里**：表格靠行线读，
   给它上灰底反而会和表头那一行的灰撞在一起。 */
.col :is(.stat, .card, .detail, .files, .secbox,
         .gcard, .chainhint, .chainsbox, .dcard, .sub-field) {
  background: var(--surface-2);
}
/* 控制台面板里的卡片是个例外：它嵌在灰面板里，要回到白底才有层次。
   规则写在上面那条后面，靠顺序覆盖 —— 选择器权重一样时后写的赢。 */
.col .dpanel :is(.card, .quick a) { background: var(--surface); }

/* 首页按内容分段：整列已经提供底色，分区和空状态不再各包一张卡。
   作用域只在首页，其他页面的编辑面板仍使用 .dpanel 的共同外观。 */
.home { min-width: 0; }
.home > .pagehead { margin-bottom: 24px; }
.home .dash { gap: 32px 36px; }
.home :is(.dash__main, .dash__side) { gap: 24px; }
.home .dpanel { background: transparent; border: 0; border-radius: 0; padding: 0; }
.home .dpanel + .dpanel { border-top: 1px solid var(--line); padding-top: 24px; }
.home .dpanel__hd { margin-bottom: 12px; }
.home .home__heading { font-family: var(--font-display); font-weight: var(--w-semibold); letter-spacing: -0.012em; font-size: var(--fs-head); display: flex; align-items: center; gap: 10px; }
.home [data-topic] { --section-color: var(--lane-1); }
.home [data-topic="mine"] { --section-color: var(--lane-3); }
.home [data-topic="announcements"], .home [data-topic="seal"] { --section-color: var(--lane-4); }
.home [data-topic="attendance"] { --section-color: var(--lane-2); }
.home [data-topic="shortcuts"] { --section-color: var(--lane-6); }
.home__mark {
  display: grid; place-items: center; flex: none; width: 34px; height: 34px;
  border-radius: var(--r-sm); color: var(--section-color);
  background: color-mix(in srgb, var(--section-color) 10%, var(--surface));
}
.home__mark .gl { width: 20px; height: 20px; }
.home .dash__side { border-left: 1px solid var(--line); padding-left: 28px; }
.home .dash__side .dpanel__hd h2 { font-size: var(--fs-body); }
.home .empty {
  background: transparent; border: 0; border-radius: 0;
  display: flex; align-items: center; gap: 14px;
  padding: 18px 0 18px 4px; margin: 0; text-align: left; font-size: var(--fs-foot);
}
.home .home__emptyicon { width: 30px; height: 30px; flex: none; color: var(--section-color); }
.home .empty b { font-size: var(--fs-sub); font-weight: var(--w-medium); margin-bottom: 6px; }
.home .kpis { gap: 0; padding: 16px 0; border-block: 1px solid var(--line); }
.home .kpi { border: 0; border-radius: 0; background: transparent; padding: 0 20px; }
.home .kpi:first-child { padding-left: 0; }
.home .kpi + .kpi { border-left: 1px solid var(--line); }
.home .dpanel .cards { gap: 8px; }
.home .dstat > div { background: transparent; border-radius: 0; padding-inline: 0; }
.home .dlist a { padding-inline: 0; }
.home .quick--side { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.home .quick--side > a {
  flex-direction: row; justify-content: flex-start; gap: 10px;
  padding: 12px; border: 0; min-width: 0; text-decoration: none;
}
.home .quick--side .ic { width: 22px; height: 22px; flex: none; }
.home .quick--side b { font-size: var(--fs-foot); font-weight: var(--w-medium); }
@media (max-width: 1024px) {
  .home .dash__side { border-left: 0; padding-left: 0; }
}
@media (hover: hover) and (pointer: fine) {
  .home .kpi:hover span { color: var(--tint); }
}
@media (max-width: 700px) {
  .col:has(> .home) { border-color: transparent; box-shadow: none; }
  .home > .pagehead { margin-bottom: 22px; }
  .home .dash { gap: 24px; }
  .home .kpis { row-gap: 16px; padding: 18px 0; }
  .home .kpi { padding: 0 0 0 14px; gap: 8px; }
  .home .kpi:nth-child(odd) { padding-left: 0; border-left: 0; }
  .home .kpi span { font-size: var(--fs-cap); }
  .home .empty { padding-block: 12px; gap: 10px; }
  .home .dpanel + .dpanel { padding-top: 20px; }
}
.mono {
  font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
  /* 不用 0.9em：它会算出 15.3px 这种不在令牌表里的值，
     而「一页 8 种字号」正是这类相对值一处一个算出来的。 */
  font-size: var(--fs-foot); color: var(--mute);
  font-variant-numeric: tabular-nums;
}
/* 页头：一行说完「这是哪一页」和「能对这一页做什么」。
   动作靠右，和标题同一行 —— 导出原先占着右栏一整块 304px 的面板，
   里面只有四个按钮，而正文被那块面板挤窄了 300px。
   导出是「对这一页做什么」，属于动作，不属于内容，也不值一整栏。 */
.pagehead {
  display: flex; align-items: flex-end; gap: 12px;
  flex-wrap: wrap; justify-content: space-between;
  margin-bottom: 16px;
}
.pagehead h1.page { margin-bottom: 0; }
/* flex: none 让它在桌面上不跟标题抢宽度；但 none 也不许它比整行还窄 —— 手机上四颗钮的
   max-content 是 442，整行只有 360，光加 flex-wrap 不换行（量过）。max-width 把它按回整行里，里面才会折。 */
.pagehead__act { display: flex; align-items: center; gap: 8px; flex: none; flex-wrap: wrap; max-width: 100%; }
/* 页头动作区里的 <form> 只是个提交壳。给它 flex 是因为**不给的话
   两个按钮之间会多出一个行内空格**，而 .btns 那个类只在 .rail__act /
   .actbar / .sheetbar 三个作用域里有规则（PITFALLS #171 那个坑），
   在这儿写 class="btns" 会静默地什么都不发生。 */
.pagehead__act form { display: flex; gap: 8px; }

/* 页头菜单。**这是控制层**：它浮在正文上面，正文从它底下过 ——
   所以给玻璃和影子，和侧栏、弹出菜单同一档材质。
   用 <details> 不用脚本：没有 JS 也开得了合得上（同发起菜单、加号菜单）。 */
.menu { position: relative; }
.menu > summary {
  list-style: none; cursor: pointer;
  font-size: var(--fs-sub); font-weight: var(--w-semibold);
  padding: 0 14px; min-height: 38px;
  display: inline-flex; align-items: center; gap: 7px;
  border: 1px solid var(--line-strong); border-radius: var(--r-sm);
  background: var(--surface); color: var(--ink);
  transition: background var(--t-2) var(--ease-out), border-color var(--t-2) var(--ease-out);
}
.menu > summary::-webkit-details-marker { display: none; }
@media (hover: hover) and (pointer: fine) { .menu > summary:hover { background: var(--surface-2); } }
.menu[open] > summary { border-color: var(--tint); color: var(--tint); }
.menu__panel {
  position: absolute; z-index: 40;
  right: 0; top: calc(100% + 6px);
  width: 15rem; padding: 6px;
  border-radius: var(--r-md);
  background: var(--mat-thick);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  border: 1px solid var(--line);
  box-shadow: var(--specular), var(--shadow-lift);
  /* 从触发它的那个按钮长出来 —— 原点对着按钮，不是对着面板自己的中心。
     菜单挂在 <details> 上，关闭时浏览器直接把内容拿掉，**退场动不了**：
     这是平台的限制不是漏做。要退场动画得换成 popover + JS，
     而那会让「没有脚本也能用」这条不再成立。 */
  transform-origin: top right;
  animation: menu-in var(--t-2) var(--ease-out);
}
@keyframes menu-in {
  from { opacity: 0; transform: scale(0.96) translateY(-4px); }
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .menu__panel { background: var(--surface); }
}
.menu__panel a, .menu__panel button {
  font: inherit; font-size: var(--fs-sub); font-weight: var(--w-semibold); text-align: left;
  width: 100%; cursor: pointer;
  /* 菜单行走 --tap，不走 40 —— 手机上这张面板是整宽的，
     四个目标挤在一起，差 4px 就是点错邻居。 */
  padding: 0 10px; min-height: var(--tap);
  display: flex; align-items: center; gap: 9px;
  border: 0; border-radius: var(--r-sm);
  background: transparent; color: var(--ink);
  transition: background var(--t-2) var(--ease-out), color var(--t-2) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .menu__panel a:hover, .menu__panel button:hover{
    background: var(--tint-soft); color: var(--tint);
  }
}
.menu__panel .gl { color: var(--mute); }
@media (hover: hover) and (pointer: fine) { .menu__panel a:hover .gl, .menu__panel button:hover .gl { color: inherit; } }
.menu__sep { height: 1px; margin: 5px 8px; background: var(--line); }
.menu__note {
  margin: 2px 10px 4px; font-size: var(--fs-cap);
  color: var(--mute); line-height: 1.5;
}

/* 窄屏：按钮保持紧凑（导出是次要动作，整宽会让它看起来像主操作），
   浮层**仍然右对齐**，只把宽度封顶。

   这里踩过一次：改成左对齐是因为“窄屏上按钮会折到标题下面”——
   而那只对长标题成立。「下载记录」四个字，393px 上标题和按钮同行放得下，
   于是浮层从按钮左沿往右铺 310px，直接冲出屏幕 216px。
   flex-wrap 折不折行取决于内容长度，不取决于断点 ——
   浮层的锚点不能建立在“它一定在那一行”上。右对齐两种情况都对。 */
@media (max-width: 560px) {
  .menu > summary { min-height: var(--tap); }
  .menu__panel { width: min(20rem, calc(100vw - 2 * var(--gutter))); }
}

/* 面包屑。九个管理页原先一个返回口都没有 —— 从「人员」到「印章」，
   唯一的路是退回 /admin/ 重新扫一遍九行。侧栏的分区解决了横向跳转，
   这一行解决的是另一件事：**告诉你现在站在哪一格**。
   只有两级，所以不做成可折叠的长链。 */
.crumb {
  display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
  margin-bottom: 8px; font-size: var(--fs-foot);
}
.crumb a {
  color: var(--mute); padding: 3px 0;
  border-bottom: 1px solid transparent;
  transition: color var(--t-2) var(--ease-out), border-color var(--t-2) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) { .crumb a:hover { color: var(--admin); border-bottom-color: currentColor; } }
.crumb span { color: var(--mute); }
.crumb b { color: var(--ink-2); font-weight: var(--w-semibold); }

/* 「为什么这么设计」折起来。

   删掉是不对的：这些理由是这套系统的一部分，第一次接手的管理员会需要，
   而且其中有几条是**用户自己拍板的取舍**（比如「附件盖不上水印，所以记下载」）。
   但它们不该占住每一页的第一屏 —— 每天要点二十次的人已经知道了。

   **会出事的警告不进这里**，它们留在原位。折的只是「为什么」，不是「小心」。 */
details.why { margin: -4px 0 18px; }
details.why > summary {
  list-style: none; cursor: pointer;
  font-size: var(--fs-foot); font-weight: var(--w-semibold); color: var(--tint);
  min-height: 34px; display: inline-flex; align-items: center; gap: 6px;
}
details.why > summary::-webkit-details-marker { display: none; }
details.why > summary::after {
  content: "\25BE"; font-size: var(--fs-micro); transition: transform var(--t-3) var(--ease-out);
}
details.why[open] > summary::after { transform: rotate(180deg); }
details.why p {
  margin: 4px 0 0; max-width: 62ch;
  font-size: var(--fs-foot); color: var(--mute); line-height: 1.65;
}

/* 说明文字里的行内链接。行内元素撑不高行盒，但**内边距照样算命中区** ——
   于是用 padding 把可点范围撑到 --tap，再用等量负外边距把版面推回原位。
   实测这类链接原先是 30×21 / 48×17，在手机上是要点两三次才中的尺寸。 */
.hint a, .lede a, .secbox__d a {
  color: var(--tint); text-decoration: underline;
  text-underline-offset: 3px; text-decoration-thickness: 1px;
  padding: 11px 2px; margin: -11px 0;
}

/* ---------- 06 控件 ---------- */

/* 计数与状态：一律 pill */
.count {
  font-size: var(--fs-foot); font-weight: var(--w-bold);
  font-variant-numeric: tabular-nums;
  background: var(--tint); color: var(--on-tint);
  border-radius: var(--r-pill); padding: 2px 10px;
}
.tag {
  flex: none; white-space: nowrap;
  font-size: var(--fs-cap); font-weight: var(--w-semibold);
  padding: 3px 9px; border-radius: var(--r-pill);
  background: var(--tint-soft); color: var(--tint);
  letter-spacing: 0.01em;
}
.tag.mute { background: color-mix(in srgb, var(--ink) 8%, transparent); color: var(--ink-2); }
.tag.warnbg { background: color-mix(in srgb, var(--late) 15%, transparent); color: color-mix(in srgb, var(--late) 80%, var(--ink)); }

.state {
  flex: none;
  font-size: var(--fs-cap); font-weight: var(--w-bold);
  padding: 4px 11px; border-radius: var(--r-pill);
}
.state.ok { background: var(--ok-bg); color: var(--ok); }
.state.bad { background: var(--bad-bg); color: var(--bad); }
.state.run { background: var(--tint-soft); color: var(--tint); }
.state.mute { background: color-mix(in srgb, var(--ink) 8%, transparent); color: var(--ink-2); }

/* 按钮。填充 / 描边 / 危险三种，圆角 8 —— 不再是 pill。
   pill 是给状态标签用的形状：它表示「这是一小块只读的东西」。
   按钮也做成 pill，一页上就分不出哪个是状态、哪个能按。 */
/* ★ button.ghost 必须在这一行里。它以前不在 ——
   于是 `<button class="ghost">` 只拿到了 ghost 的配色，**拿不到按钮的几何**
   （圆角、内边距、min-height、inline-flex），实测圆角 0、字号 14。
   `.btn.ghost` 一直是好的，因为 .btn 在这一行里 —— 所以这个洞只在
   「用 button 而不是 a」的地方出现：组织页的 41 个「设为主管」、
   流程编辑器的「搜 / 前移 / 后移」。 */
/* 🔴 2026-09-08：**一个不带类的 `<button>` 原来什么样式都没有。**
   用户看到的是单据页正中间那个大白方块（「补充说明」的提交键），
   原话：「画面中心这种大白方按钮不要再出现了。**要做到 token 化所有 ui 元素**，
   不要老出现这种跳脱的东西。」

   根因就在下面这条选择器上：它是 `button.primary, button.danger, button.ghost`
   —— 也就是说**按钮的默认外观是浏览器默认外观**，
   只有记得挂类的那些才进这套系统。全站有 15 个这样的裸按钮。

   修法是把默认反过来：**裸按钮先长成系统里的样子**（描边中性键），
   类只是修饰。`:not([class])` 的特异度低于任何带类的组件规则，
   所以 `.iconbtn` `.split__b` `.gadd__b` 这些自带外观的一个都不受影响。 */
button:not([class]) {
  font: inherit;
  font-size: var(--fs-callout); font-weight: var(--w-semibold);
  cursor: pointer;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 0 16px; min-height: var(--tap);
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  background: var(--surface);
  color: var(--ink);
  transition: background var(--t-2) var(--ease-out), border-color var(--t-2) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  button:not([class]):hover { background: var(--surface-2); border-color: var(--ink-2); }
}
button:not([class]):disabled { opacity: .45; cursor: not-allowed; }

/* 原生文件选择键同理：`Choose Files` 那个灰块是浏览器画的，
   在一屏中文界面里它是**唯一一个英文控件**。 */
input[type="file"] { font: inherit; font-size: var(--fs-sub); color: var(--mute); }
input[type="file"]::file-selector-button {
  font: inherit; font-size: var(--fs-sub); font-weight: var(--w-semibold);
  cursor: pointer;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 8px 14px; margin-right: 10px;
  background: var(--surface-2);
  color: var(--ink);
}

/* ⚠️ `button.sm` 必须在这张名单里。`.sm` 只是个**尺寸修饰**（下面那条只设了
   高度和字号），模板里有 26 个 `<button class="sm">` —— 它们曾经一条基样式
   都没吃到，在深色下就是浏览器默认的灰方块、直角、白边。
   用户两次指着它说「画面中心这种大白方按钮不要再出现了」。
   上一轮我加的 `button:not([class])` 治不了它们：**它们有 class，只是那个
   class 不带样式**。守卫改成了「每个按钮的 class 里至少有一个设过背景」。 */
button.primary, button.danger, button.ghost, button.sm, .btn, .btnlink {
  font: inherit;
  font-size: var(--fs-callout); font-weight: var(--w-semibold);
  border: 0; cursor: pointer;
  border-radius: var(--r-sm);
  padding: 0 18px; min-height: var(--tap);
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  transition: transform var(--t-2) var(--ease-out), background var(--t-2) var(--ease-out),
              box-shadow var(--t-2) var(--ease-out);
}
/* 只带尺寸修饰的按钮（模板里 26 个 `class="sm"`）是**次要动作**：描边不填色。
   在此之前它们一条背景都没吃到，深色下就是浏览器默认的灰方块 ——
   用户两次说的「画面中心这种大白方按钮」有一半是它们。
   放在 primary / danger / ghost **前面**：那三条在后面按顺序覆盖它，
   所以 `sm primary`、`sm danger`、`sm ghost` 仍然各是各的。 */
/* 按钮上的字永远一行 —— 375 上工作台那颗「保存首页快捷入口」被旁边的说明挤成两行。
   只给这几张脸：选项卡（button.nodeitem）是一张卡，一行标签一行说明，要换行。 */
button:not([class]), button.primary, button.danger, button.ghost, button.sm, .btn, .btnlink { white-space: nowrap; }
button.sm {
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line-strong); box-shadow: none;
}
@media (hover: hover) and (pointer: fine) { button.sm:hover { background: var(--surface-2); } }
button.sm:active { transform: scale(0.97); transition-duration: var(--t-1); }

button.primary, .btn, .btnlink {
  background: var(--tint); color: var(--on-tint);
}
@media (hover: hover) and (pointer: fine) { button.primary:hover, .btn:hover, .btnlink:hover { background: var(--tint-2); } }
/* ---- 按压：全站一个值（第 9 条）----
   原来有 .92 / .93 / .95 / .96 / .97 五种。五种在同一屏上是读得出来的：
   点侧栏的图标按钮和点主按钮「陷下去的深浅」不一样，
   可这两件事在按的人心里是同一件事。
   时长走 --t-1：按压是一天几百次的动作，频率法则里它只配一个回执，不配一段动画。 */
button.primary:active, .btn:active, .btnlink:active {
  transform: scale(0.97); transition-duration: var(--t-1);
}
button.danger {
  background: color-mix(in srgb, var(--bad) 12%, transparent);
  color: var(--bad);
}
@media (hover: hover) and (pointer: fine) { button.danger:hover { background: color-mix(in srgb, var(--bad) 18%, transparent); } }
button.danger:active { transform: scale(0.97); transition-duration: var(--t-1); }
button.sm, .btn.sm { min-height: 36px; font-size: var(--fs-foot); padding: 0 14px; }

/* 中性描边：次要动作用它。填充色只留给「这一页要做的那一件事」。 */
button.ghost, .btn.ghost {
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line-strong); box-shadow: none;
}
@media (hover: hover) and (pointer: fine) {
  button.ghost:hover, .btn.ghost:hover{
    background: var(--surface-2); color: var(--ink);
  }
}
button.ghost:active, .btn.ghost:active { transform: scale(0.97); transition-duration: var(--t-1); }
/* 主按钮不横贯整行。100% 宽的按钮在手机上是对的（拇指够得着），
   在 1920 的桌面上是一块 1360px 的深色板砖 —— 它比页面上任何内容都重，
   而它只是「保存」。给它一个下限宽度、靠左站在动作行里。 */
.form > button.primary { margin-top: 22px; min-width: 200px; justify-self: start; }
@media (max-width: 640px) { .form > button.primary { width: 100%; } }

/* 输入。字号走 --fs-input（≥16px）：低于 16 时 iOS 会放大整页，一放大就回不去。 */
label {
  display: block;
  font-size: var(--fs-foot); font-weight: var(--w-semibold);
  color: var(--ink-2); margin: 18px 0 6px;
  letter-spacing: 0.01em;
}
label .opt { color: var(--mute); font-weight: var(--w-regular); }

/* ★ 这里原先是**按 type 一个个列**：text / password / number / date / search。
   于是 `type="tel"` 和 `type="email"` 一个都没匹配上 —— 新建账号页的
   「手机」「邮箱」两个框拿到的是浏览器默认样式：小一圈、边框不一样、高度不够。
   用户 2026-09-07 拿截图报过来「各个框大小都不一样」，说的就是这两个。

   **枚举式的选择器，漏掉的那一个不会报错，只会长得不一样。**
   改成按 type 排除：不该长成输入框的是一个**封闭且不会增长**的集合
   （勾选框、单选、文件、隐藏、各种按钮、滑块、取色器），其余一律是输入框。
   用 `:not(:is(...))` 而不是链式 `:not()`：前者的特异度是 (0,1,1)，
   和原来那一串 `input[type=x]` 完全相等，换上去不会悄悄压过后面的规则
   （`.filterbar > input[type=search]` 就靠那点差额活着，见 PITFALLS #84）。 */
input:not(:is([type=checkbox], [type=radio], [type=file], [type=hidden],
               [type=submit], [type=button], [type=image], [type=range], [type=color])),
select, textarea, .rte__doc {
  width: 100%; font: inherit; font-size: var(--fs-input);
  padding: 11px 14px; min-height: var(--tap);
  border: 1px solid var(--field-line);
  border-radius: var(--r-sm);
  background: var(--surface); color: var(--ink);
  transition: border-color var(--t-2) var(--ease-out), box-shadow var(--t-2) var(--ease-out);
}
input:focus, select:focus, textarea:focus, .rte__doc:focus {
  border-color: var(--tint);
  box-shadow: 0 0 0 3px var(--tint-soft);
  outline: none;
}
textarea { resize: vertical; line-height: 1.6; }
select {
  appearance: none;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--mute) 50%),
    linear-gradient(135deg, var(--mute) 50%, transparent 50%);
  background-position: right 18px center, right 13px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: 36px;
}

/* 表单不撑满。1440 的屏上量到过一个 1117px 宽的输入框 —— 比一张 A4 还宽，
   眼睛从标签走到输入框要横扫大半个屏幕，而这一路上什么都没有。
   右栏里的表单本来就窄，这条对它无害。 */
.form { display: grid; max-width: 640px; }

/* ---- 控件宽度：两档，按**内容**定，不按容器定 ----
   用户 2026-09-07：「你很多地方都直接用的大方框……页面布局不均匀，各个框大小都不一样。」

   两件事要同时成立，很容易只做到一件：

     统一   同一页上的框必须看起来是一套的（高度、边框、圆角、左边缘对齐）。
            这一条靠上面那条排除式选择器保证 —— 不均匀的根因是有的框没拿到样式。
     不夸张 一个装 11 位手机号的框没有理由和一个装邮箱的框一样宽 640px。

   所以宽度只有两档，而且档位由**内容长度**决定，不由它待在多宽的列里决定：
   手机 / 工号 / 日期 / 数字这类有天然长度上限的用短档，其余用中档。
   两档而不是五档 —— 三档以上就看不出是刻意的，会重新变成「各个框大小都不一样」。 */
.form input, .form select, .form textarea,
.field > input, .field > select, .field > textarea { max-width: var(--fw-m); }
.field > input[type=date], .field > input[type=number], .field > input[type=tel],
.form > input[type=date], .form > input[type=number], .form > input[type=tel],
.field.short > input, .field.short > select { max-width: var(--fw-s); }
/* 需要占满的（长文本、正文标题）显式说出来。 */
.field.full > input, .field.full > select, .field.full > textarea,
.form textarea.long { max-width: none; }
.form label:first-child { margin-top: 0; }
/* 用 .field 包着的字段，行距靠 row-gap 不靠 label 的上边距 —— 每个 .field 里的 label 都是
   :first-child，上边距被上一条归零，字段就贴在一起（/me 那一页，用户 2026-09-12：「过于紧凑」）。
   .form.grid 早就这么做，这里是同一条推广到不分栏的表单。 */
.form:has(> .field) { row-gap: 18px; }
.form > .btns { margin-top: 6px; }
/* 即时校验：填过又填错的格子当场标出来。用 :user-invalid 不用 :invalid ——
   后者在页面一打开就把所有必填标红，等于什么都没标。零脚本；
   09-12 在独立 Chrome 里真键入 + 失焦量过：计算样式当场变红（内置面板里量不到，PITFALLS #251）。
   不限在 .form 里：建账号 / 离职 / 认领 / 登录那几张表单用的是别的壳，第一版只写 .form，
   复量时在建账号页一格都不红。 */
:is(input, select, textarea):user-invalid {
  border-color: var(--bad);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--bad) 16%, transparent);
}
.fe { color: var(--bad); font-size: var(--fs-foot); margin: 6px 0 0; }

label.check {
  display: flex; align-items: center; gap: 12px;
  min-height: var(--tap); margin: 20px 0 0;
  font-size: var(--fs-callout);
}
label.check input { width: 22px; height: 22px; accent-color: var(--tint); flex: none; }

.radio {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 12px 14px; min-height: var(--tap);
  border-radius: var(--r-sm);
  transition: background var(--t-2) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) { .radio:hover { background: color-mix(in srgb, var(--ink) 4%, transparent); } }
.radio input { margin-top: 3px; width: 20px; height: 20px; flex: none; accent-color: var(--tint); }
.radio b { display: block; font-weight: var(--w-semibold); font-size: var(--fs-callout); }
.radio i { font-style: normal; font-size: var(--fs-foot); color: var(--mute); }

.sub-field {
  padding: 14px; margin-top: 12px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.sub-field .hint { margin: 8px 0 0; }

.searchbar { display: flex; gap: 10px; margin: 0 0 18px; max-width: 640px; }
.searchbar input { flex: 1; }
.searchbar button {
  font: inherit; font-size: var(--fs-sub); font-weight: var(--w-semibold);
  padding: 0 18px; min-height: var(--tap); cursor: pointer;
  border: 1px solid var(--line-strong); border-radius: var(--r-sm);
  background: var(--surface); color: var(--ink);
}
.filter { margin: 0 0 20px; }
.filter select { max-width: 26rem; }

.err {
  background: var(--bad-bg); color: var(--bad);
  padding: 13px 16px; border-radius: var(--r-md);
  font-size: var(--fs-sub); margin: 0 0 18px;
}
/* 成功回执。原先只有 admin_policy 用 `class="ok banner"`，
   而 .ok 这个类**根本没有规则** —— 那句「已保存。」渲染出来是一段
   有内边距的黑字，看起来和普通说明文字没有区别。
   现在它和 .err 对称，并且每一页都用得上（见 render.go 的 OK/Err）。 */
.ok.banner, .ok {
  background: var(--ok-bg); color: var(--ok);
  padding: 13px 16px; border-radius: var(--r-md);
  font-size: var(--fs-sub); font-weight: var(--w-semibold); margin: 0 0 18px;
}
.err.banner { padding: 13px 16px; }
/* 行内的「已归还」这类小标记不该被上面那条撑成横幅。 */
span.ok { background: none; padding: 0; margin: 0; font-size: inherit; }

/* 底部操作条。手机上贴底，桌面上化成一块面板。 */
.actbar {
  position: sticky; bottom: 0; z-index: 30;
  margin: 22px calc(var(--gutter) * -1) calc(var(--gutter) * -1);
  padding: 14px var(--gutter) calc(14px + env(safe-area-inset-bottom));
  background: var(--mat-thick);
  -webkit-backdrop-filter: var(--blur);
  backdrop-filter: var(--blur);
  border-top: 1px solid var(--line);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .actbar { background: var(--surface); }
}
.actbar .btns { display: flex; gap: 10px; margin-top: 10px; }
.actbar .btns button { flex: 1; }

/* 折叠的危险操作。一屏摊开十几个「停用」按钮，迟早误触一个。 */
.ops { margin-top: 12px; }
.ops summary {
  cursor: pointer; list-style: none;
  color: var(--tint); font-size: var(--fs-foot); font-weight: var(--w-semibold);
  min-height: 34px; display: inline-flex; align-items: center; gap: 6px;
  padding: 0 12px; border-radius: var(--r-sm);
  background: var(--tint-soft);
}
.ops summary::-webkit-details-marker { display: none; }
.ops summary::after { content: "▾"; font-size: var(--fs-micro); transition: transform var(--t-3) var(--ease-out); }
.ops[open] summary::after { transform: rotate(180deg); }
.opbox { padding: 12px 0 2px; display: grid; gap: 10px; }
.opline { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.opline input, .opline select { flex: 1; min-width: 150px; }
.opline button {
  font: inherit; font-size: var(--fs-foot); font-weight: var(--w-semibold);
  padding: 0 16px; min-height: var(--tap); margin: 0; cursor: pointer;
  border: 1px solid var(--line-strong); border-radius: var(--r-sm);
  background: var(--surface); color: var(--ink);
}
.opline button.danger { color: var(--bad); border-color: transparent; }
.opline button.sm { min-height: 36px; }
.opbox .hint { margin: 0; }
.inline-form { display: grid; gap: 4px; margin-bottom: 8px; }

/* ---------- 07 列表与卡片 ---------- */
/*
   分组列表：一组卡片合成一块圆角面板，行与行之间是发丝线。
   这是 iOS 设置那种「成组」的读法 —— 比十几张各自带边框的卡片安静得多，
   也更容易一眼看出「这是一组同类的东西」。
*/
.cards {
  list-style: none; margin: 0 0 4px; padding: 0;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  overflow: hidden;
}
.cards > li + li { border-top: 1px solid var(--line); }

.card {
  display: block; position: relative;
  padding: 13px 16px;
  background: transparent;
  color: inherit;
  transition: background var(--t-2) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) { a.card:hover { background: color-mix(in srgb, var(--ink) 4%, transparent); } }
/* 按下立刻变色，松开才淡出 —— 原生列表就是这个手感。
   两头都用同一条 transition 的话，手指按上去要等 0.16s 才有反应，
   在手机上会被当成「没点中」。 */
a.card:active { background: color-mix(in srgb, var(--ink) 8%, transparent); transition: none; }
.card.dim { color: var(--mute); }

/* 逾期：整行淡底说「这里有问题」，橙字说「问题是什么」。
   若整张卡都变橙加粗，标题和细节就一样重，等于没有重点。 */
/* 这里原先还有一道 3px 的左侧色条。去掉了：
   整行淡底已经说了「这里有问题」，橙字说了「问题是什么」，
   色条是第三遍说同一件事 —— 而「圆角卡片 + 一侧色条」恰好是
   最容易被认出来的那种模板腔。信息一点没少，噪音少一层。 */
.card.late { background: var(--late-bg); }

.row1 { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.row1 .title, .row1 .ttl { font-weight: var(--w-semibold); font-size: var(--fs-callout); letter-spacing: -0.005em; }
.row2 {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  color: var(--mute); font-size: var(--fs-foot); margin-top: 6px;
}
.row2 .due { margin-left: auto; font-variant-numeric: tabular-nums; }
.row2 .due.late { color: var(--late); font-weight: var(--w-bold); }
.row2 .none, .who.none { color: var(--bad); font-weight: var(--w-semibold); }
.row3 { margin-top: 6px; font-size: var(--fs-foot); }
.ttl { font-weight: var(--w-semibold); overflow-wrap: anywhere; }
.serial {
  font-size: var(--fs-cap); color: var(--mute); margin-top: 6px;
  font-variant-numeric: tabular-nums; letter-spacing: 0.01em;
}
.sep { color: var(--line-strong); }
.sub { color: var(--mute); font-size: var(--fs-foot); }
.who { color: var(--mute); font-size: var(--fs-foot); }

span.warn { color: var(--late); font-weight: var(--w-semibold); }
span.ok { color: var(--ok); font-weight: var(--w-semibold); }
span.late { color: var(--late); font-weight: var(--w-semibold); }

/* 统计块 */
.stats {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
  gap: 12px; margin: 4px 0 22px;
}
.stat {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 14px;
  display: block; text-align: center;
  transition: border-color var(--t-2) var(--ease-out), background var(--t-2) var(--ease-out);
}
/* 悬停不再抬起来：抬起要有影子才说得通，而这一层没有影子。 */
@media (hover: hover) and (pointer: fine) { a.stat:hover { border-color: var(--line-strong); background: var(--surface-2); } }
.stat b {
  display: block; font-size: var(--fs-large); font-weight: var(--w-bold);
  letter-spacing: -0.02em; font-variant-numeric: tabular-nums;
}
.stat span { color: var(--mute); font-size: var(--fs-cap); }
.stat.bad { background: var(--late-bg); border-color: color-mix(in srgb, var(--late) 30%, transparent); }
.stat.bad b { color: var(--late); }

/* 详情 */
.detail {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 22px 20px;
}
.detail .head h1 { font-size: var(--fs-title); font-weight: var(--w-bold); letter-spacing: -0.01em; margin: 10px 0 4px; }
.detail .meta { color: var(--mute); font-size: var(--fs-foot); margin: 0; font-variant-numeric: tabular-nums; }
.detail h2.sec {
  margin: 26px 0 10px; padding-top: 18px;
  border-top: 1px solid var(--line);
}
.fields {
  /* 标签列按标签本身定宽、封顶 8rem —— 写死 rem 会把六个字的标签折成两行（375 上量到过）。 */
  display: grid; grid-template-columns: fit-content(8rem) 1fr;
  gap: 10px 16px; margin: 20px 0 0; font-size: var(--fs-callout);
}
.fields dt { color: var(--mute); font-size: var(--fs-sub); }
.fields dd { margin: 0; }
.fields dd.wrap { white-space: pre-wrap; }

/* 流转轨迹 */
.chain { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.chain li { border-left: 3px solid var(--line-strong); padding: 3px 0 3px 14px; }
.chain li.ok { border-left-color: var(--ok); }
.chain li.bad { border-left-color: var(--bad); }
.chain li.run { border-left-color: var(--tint); }
.c1 { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.c2 { font-size: var(--fs-foot); color: var(--mute); margin-top: 3px; }
.c3 {
  font-size: var(--fs-sub); margin-top: 8px; padding: 10px 12px;
  background: var(--surface-2); border-radius: var(--r-sm); white-space: pre-wrap;
}

/* 附件 */
input[type=file] {
  width: 100%; font: inherit; font-size: var(--fs-sub);
  padding: 12px 14px; min-height: var(--tap);
  border: 1.5px dashed var(--line-strong); border-radius: var(--r-md);
  background: var(--surface-2); color: var(--ink-2);
}
input[type=file]::file-selector-button {
  font: inherit; font-size: var(--fs-foot); font-weight: var(--w-semibold); margin-right: 12px;
  padding: 8px 14px; border: 0; border-radius: var(--r-sm);
  background: var(--tint-soft); color: var(--tint); cursor: pointer;
}
.files {
  list-style: none; margin: 0; padding: 0;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-md); overflow: hidden;
}
.files li { padding: 13px 16px; }
.files li + li { border-top: 1px solid var(--line); }
.fname { display: block; color: var(--tint); font-weight: var(--w-semibold); word-break: break-all; }
.fmeta {
  display: block; margin-top: 4px; color: var(--mute);
  font-size: var(--fs-cap); font-variant-numeric: tabular-nums;
}
.upload { display: grid; gap: 12px; margin-top: 14px; }
.upload button {
  font: inherit; font-size: var(--fs-sub); font-weight: var(--w-semibold); justify-self: start;
  padding: 0 20px; min-height: var(--tap); cursor: pointer;
  border: 1px solid var(--line-strong); border-radius: var(--r-sm);
  background: var(--surface); color: var(--ink);
}

/* 快捷入口 —— iOS 那种一格一格的图标。首页（.quick--side，格子就是 <a>）和工作台（.tile 包着 <a> + 一个角标）共用。 */
.quick {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr));
  gap: 10px; margin: 0 0 24px;
}
.quick > a, .quick .tile__hit {
  display: flex; flex-direction: column; align-items: center; gap: 7px;
  padding: 14px 6px; min-height: var(--tap); height: 100%; box-sizing: border-box;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-md); color: var(--ink);
  transition: transform var(--t-2) var(--ease-out), background var(--t-2) var(--ease-out), border-color var(--t-2) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .quick > a:hover, .quick .tile__hit:hover { background: var(--surface-2); border-color: var(--line-strong); }
}
.quick > a:active, .quick .tile__hit:active { transform: scale(0.97); transition-duration: var(--t-1); }
/* 图标是 SVG 精灵，不再是 emoji（apps.go 那段注释说了为什么）。
   尺寸用 width/height，颜色走 currentColor —— 它们因此在暗色下也是线，
   而且不再以「字号」的身份混进排版统计。 */
.quick .ic { width: 24px; height: 24px; color: var(--tint); }
.quick b { font-weight: var(--w-semibold); font-size: var(--fs-cap); text-align: center; }
.quick .more { color: var(--mute); }
.quick .more .ic { color: var(--mute); }

/* ══ 工作台：长按整理（2026-09-12）═══════════════════════════════════
   用户：「类似苹果长按所有图标抖动然后右上角加号添加到常用菜单，常用菜单里右上角减号移出，
   并且手指拖动排序。桌面端也要类似操作手感。做的灵动。」

   .tile = 格子（<a class="tile__hit">）+ 角标（<button class="badge">）。角标平时不画（脚本没接管时由
   <noscript> 露出来），整理态（.wb.editing）才冒出来：全部里是 ＋、常用里是 －、已在常用的是 ✓。
   格子的说明（<i>）只在 ≥700 画：手机上四格一行、格子 80 来宽，iOS 也只画名字。
   抖动是 CSS 关键帧，每格用 --i 错开相位，看起来才像「每个都在抖」而不是整排一起晃。
   常用里的格子在整理态 touch-action: none —— 没有它触屏一动就是 pointercancel（PITFALLS #254）；
   只给常用那几格，全部那一列照样能滚。拖起来的那一格 .dragging 浮起，其他格子由脚本 FLIP 让开。 */
.wb__hd { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
.wb__hd .sec { margin: 0; flex: none; white-space: nowrap; } /* flex 里两个字的标题会被压成竖排（#250） */
.wb__hd .wb__tip { margin: 0; flex: 1 1 auto; min-width: 0; }
.wb__hd .wb__edit { margin-left: auto; flex: none; }
.wb__empty { margin: -12px 0 24px; }
.wb .tile { position: relative; --i: 0; }
.wb .tile__hit { -webkit-touch-callout: none; user-select: none; -webkit-user-select: none; }
.wb .tile i {
  font-style: normal; font-size: var(--fs-micro); line-height: 1.45; color: var(--mute); text-align: center;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
}
.wb .tile em {
  font-style: normal; font-size: var(--fs-micro); font-weight: var(--w-semibold);
  color: var(--mute); background: color-mix(in srgb, var(--ink) 7%, transparent);
  border-radius: var(--r-pill); padding: 1px 7px; margin-left: 4px;
}
.wb .tile.soon .tile__hit { background: transparent; border-style: dashed; color: var(--mute); }
.wb .tile.soon .ic { color: var(--mute); }
/* 格子本身是「面」，网格不是：网格不上底色（第一版把 .quick 留在 .col 那张底色名单里，每一组后面拖着一条灰带）。
   内容列里的格子和 .card 一样用二级底，悬停浮成白的。 */
.col .quick > a, .col .quick .tile__hit { background: var(--surface-2); }
@media (hover: hover) and (pointer: fine) {
  .col .quick > a:hover, .col .quick .tile__hit:hover { background: var(--surface); transform: translateY(-1px); }
}
/* 整理态里右下角那颗浮着的「发起」让位 —— 它压在最后一排格子上，而整理时没人要发起。 */
.wb-editing .fabwrap { opacity: 0; pointer-events: none; }
/* 已在常用的格子在全部里退成灰字灰图标（不用 opacity：半透明不是灰色，#244）—— 整理态里它的角标是 ✓ 不是 ＋。 */
.wb.editing .quick--all .tile.in .tile__hit { color: var(--mute); }
.wb.editing .quick--all .tile.in .ic { color: var(--mute); }

/* 角标：右上角一颗 24 的圆，压在格子的边上。＋ / － 是真按钮（44 的触摸区由 ::after 撑）。
   里面是精灵图的线（i-plus / i-minus / i-check），不是字 —— 字形的度量随字体走，真机上「＋」「✓」
   在圆里偏右偏下（用户 2026-09-12 实机照片，PITFALLS #261）；一条路径在哪台机器上都在正中。 */
.wb .badge {
  position: absolute; top: -8px; right: -8px; z-index: 2;
  display: none; place-items: center;
  width: 24px; height: 24px; padding: 0; margin: 0;
  border-radius: var(--r-pill); border: 2px solid var(--surface);
  background: var(--tint); color: var(--on-tint);
  box-shadow: var(--shadow-card);
  cursor: pointer;
}
.wb .badge svg {
  display: block; width: 13px; height: 13px;
  fill: none; stroke: currentColor; stroke-width: 2.6; stroke-linecap: round; stroke-linejoin: round;
}
.wb .badge::after { content: ""; position: absolute; inset: -10px; border-radius: inherit; }
.wb .badge--rm { background: var(--bad); }
.wb .badge--in { background: var(--surface-2); color: var(--mute); border-color: var(--line); box-shadow: none; cursor: default; }
.wb.editing .badge--add, .wb.editing .badge--rm { display: grid; }
.wb.editing .tile.in .badge--add { display: none; }
.wb.editing .tile.in .badge--in { display: grid; }
.wb .badge { animation: badgepop var(--t-3) var(--ease-out) both; }
@keyframes badgepop { from { transform: scale(0); opacity: 0; } }

/* 整理态：抖。相位按 --i 错开，幅度 1.5°、半周期 --t-2（160ms，一来一回 320ms）。
   iOS 的 wobble 一来一回约 260~300ms；第一版用 --t-4（一来一回 640ms）被用户说「像慢动作」。
   顺带一点位移（±0.6px）让它像在抖不是在转；will-change 让合成器提前接手
   （apple-design：只动 transform / opacity，动画一开始就在合成层上）。 */
.wb.editing .tile:not(.soon) .tile__hit {
  animation: jiggle var(--t-2) ease-in-out infinite alternate;
  animation-delay: calc(var(--i) * var(--t-1) * -1);
  will-change: transform;
}
@keyframes jiggle {
  from { transform: rotate(-1.5deg) translate(0.4px, 0.6px); }
  to   { transform: rotate(1.5deg) translate(-0.4px, -0.6px); }
}
.wb.editing .tile__hit:active { transform: none; }
.wb.editing .quick--fav .tile { touch-action: none; cursor: grab; }
.wb .tile.dragging { z-index: 5; cursor: grabbing; }
.wb .tile.dragging .tile__hit {
  animation: none; transform: scale(1.06); box-shadow: var(--shadow-lift);
  background: var(--surface); transition: none;
}
.wb .tile.dragging .badge { display: none; }
/* 让开的格子：脚本写 transform，这里给它过渡。拖着的那格不要过渡（要跟手）。 */
.wb.editing .quick--fav .tile:not(.dragging) { transition: transform var(--t-3) var(--ease-out); }
/* 刚搬进常用 / 刚被移出的格子：同名的 view-transition 让浏览器把它从原来的位置飞过去。 */
.wb .tile.vt { view-transition-name: wb-move; }
::view-transition-group(wb-move) { animation-duration: var(--t-4); animation-timing-function: var(--ease-out); }

@media (max-width: 700px) {
  .wb .quick { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
  .wb .tile__hit { padding: 12px 4px; gap: 6px; }
  .wb .tile i { display: none; }
  .wb .tile b { font-size: var(--fs-micro); }
  .wb .badge { top: -6px; right: -6px; }
}

/* 发起链预告。
   原先是「圆角卡片 + 一道 3px 的左侧色条」，色条在 .none 时变橙。
   去掉了色条：它承载的那一位信息（这张单有没有人能批）卡片里的文字
   本来就说了，而这个形状恰好是最容易被认出来的那种模板腔。
   状态改用整块淡底表达 —— 和逾期卡片同一套语汇，少一种说法。 */
.chainhint {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 13px 15px; margin-bottom: 20px;
}
.chainhint.none { background: var(--late-bg); border-color: transparent; }
.chainhint.none .lbl { color: var(--late); }
.chainhint .lbl { font-size: var(--fs-cap); color: var(--mute); font-weight: var(--w-semibold); }
.chainhint ol { margin: 8px 0 0; padding-left: 1.3rem; }
.chainhint li { padding: 2px 0; font-size: var(--fs-sub); }
.chainhint .hint { margin: 8px 0 0; }

/* 组织树：现在是 .cards 里的一行（第 4 步，和人员 / 印章同一套「选一行、右边改它」）。
   层级靠 padding 缩进画在成组面板**里面** —— margin 会把面板撑破。
   原先这里有一套 .orgunit / .orgform（每行一个 select + 一个按钮，41 份），
   连同那 41 个 Windows 原生凸起按钮的修法一起退役了。 */
.orgtree .card.d1 { padding-left: 34px; }
.orgtree .card.d2 { padding-left: 52px; }
.orgtree .card.d3 { padding-left: 70px; }
.orgtree .card.d4 { padding-left: 88px; }
.orgtree .row2 .late { color: var(--late); }

/* ---- 流程编辑器 ----

   一条自动画出来的流程图：发起人 → 节点 → 办结。
   **PC 横着排、手机竖着排 —— 同一段 HTML，只换 flex 方向。**
   连接线是 .glink 自己（伪元素画的），加号嵌在线的中间：
   加号在哪，插进去的节点就在哪，不需要「加到末尾再一路上移」。
*/
/* 这里原来有两端的滚动阴影（background-attachment 的 local/scroll 组合）。
   那是给**横主干**画的：容器一横滚，两边就浮出一道灰，告诉人还能往右拖。
   主干竖过来之后容器不再滚，两层重合，那两道灰变成了常驻的污渍 —— 所以撤了。
   窄屏上分流那一块仍然自己横滚，它的滚动提示写在它自己那一节里。 */
.graphwrap {
  /* 管理区这张图的卡比发起页那条链宽：那儿是一条窄链，这儿要放得下
     「交给谁」加一行解析结果。同一个令牌，两处不同的值。 */
  --gcard-w: 236px;
  /* 主干竖过来之后这里不再需要横向滚动条（那是给横主干留的）——
     宽度不够时由分流那一块自己滚，见文件下面 700px 那个断点。
     留着的话，面板比它那一列宽的时候会凭空多出一条 5px 的滚动条。 */
  overflow: visible;
  margin: 0 -4px; padding: 4px;
  /* 连线那张 SVG 铺在这上面，所以它得是定位祖先。 */
  position: relative;
}
.graph {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: row; align-items: center;
  min-width: min-content;
}
/* 关掉动效的处理**全站只有一处**，在文件末尾的「动效纪律」那一节。
   这里原来有一块专管流程图的，已经并过去了 —— 三处各写各的，
   结果是「哪些动作会被关掉」这个问题没有一个地方答得全。 */

/* 这里原来有一条给「加号菜单」让高度的规则。菜单没有了（换成两条臂），
   规则也删了，但那个坑值得留一句：overflow-x 设成 auto 之后，
   CSS 会把 overflow-y 的 visible 强制算成 auto —— 盒子在**两个方向**上都变成裁剪盒。
   主干竖过来之后 .graphwrap 已经不再横向滚（overflow: visible），所以这条不再咬人；
   哪天又给它加回 overflow-x，浮出容器的东西就会重新被切。 */

/* 起点与终点：一枚圆点加一个词。刻意做得比节点轻 —— 它们不可编辑。 */
.gend {
  flex: 0 0 auto; display: flex; align-items: center; gap: 8px;
  font-size: var(--fs-foot); color: var(--mute); font-weight: var(--w-semibold);
}
.gend__dot {
  width: 30px; height: 30px; border-radius: var(--r-pill);
  display: grid; place-items: center;
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  color: var(--ink-2); font-size: var(--fs-cap); font-weight: var(--w-bold);
}
.gend__dot.ok { background: var(--ok-bg); color: var(--ok); }

/* 连接线。桌面横线、手机竖线。 */
/* 旧的「加号点开一个三选一菜单」那套样式（.gadd / .gadd__menu / @keyframes gadd-in）
   已经删掉：2026-09-07 换成了 hover 时长出来的两条臂（.gplus / .garm）。
   .gadd__b 留着 —— 那是发起页的空位按钮，和这里没关系，只是名字像。 */

/* 节点卡 */
.gcard {
  flex: 0 0 auto; position: relative; display: grid; gap: 2px;
  align-content: center;
  width: var(--gcard-w); min-height: var(--lane-h); padding: 12px 13px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-md);
  text-decoration: none; color: var(--ink);
  transition: border-color var(--t-2) var(--ease-out), background var(--t-2) var(--ease-out),
              transform var(--t-2) var(--ease-out), box-shadow var(--t-2) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) { .gcard:hover { border-color: var(--line-strong); background: var(--surface-2); } }
/* 抬 2px。图上的卡是可点的，但它们又必须看起来在同一个平面上
   （连接线画在那个平面里）—— 所以静止时没有影子，只有指上去的那一张浮起来。 */
@media (hover: hover) and (pointer: fine) { .gnode:hover > .gcard { transform: translateY(-2px); box-shadow: var(--shadow-lift); } }
/* 标题最多两行。等高是这张图的地基（主脊的端点算得准全靠它），
   所以宁可截断也不让一张卡把整条支路撑高。全名在右栏里看得到。 */
.gcard__t {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: var(--fs-callout); font-weight: var(--w-semibold); margin-top: 2px;
}
/* 选中态靠一圈色环，不靠影子 —— 这张卡和它旁边的连接线在同一个平面上。 */
.gcard.on { border-color: var(--tint); box-shadow: 0 0 0 3px var(--tint-soft); }
.gcard.bad { border-color: color-mix(in srgb, var(--late) 42%, transparent); }
.gcard.notify { background: var(--surface-2); }
.gcard__n {
  position: absolute; top: -9px; left: -7px;
  width: 22px; height: 22px; border-radius: var(--r-pill);
  display: grid; place-items: center;
  background: var(--tint); color: var(--on-tint);
  font-size: var(--fs-cap); font-weight: var(--w-bold);
}
.gcard.notify .gcard__n { background: var(--mute); }
.gcard__k {
  justify-self: start; font-size: var(--fs-cap); font-weight: var(--w-semibold);
  padding: 1px 8px; border-radius: var(--r-pill);
  background: var(--tint-soft); color: var(--tint);
}
.gcard__k.mute { background: color-mix(in srgb, var(--ink) 8%, transparent); color: var(--ink-2); }
.gcard__s { font-size: var(--fs-cap); color: var(--mute); }
.gcard__c {
  justify-self: start; font-size: var(--fs-cap); color: var(--tint);
  background: var(--tint-soft); border-radius: var(--r-sm);
  padding: 3px 7px; margin-top: 4px;
}
.gcard__h { font-size: var(--fs-cap); color: var(--mute); margin-top: 2px; }
.gcard.bad .gcard__s { color: var(--late); }

@media (max-width: 768px) {
  /* 窄屏上面板的左右内边距收一半：屏幕本来就只有 393，
     两边各留 24 等于把正文列又砍掉 48。手机端整体优化另排期，
     但这一轮加的面板不该让窄屏比改之前更挤。 */
  .col { padding: 16px 12px 20px; border-radius: var(--r-md); }

  /* 手机：竖着画。线转 90 度，卡片占满宽度。 */
  .graphwrap { overflow-x: visible; margin: 0; padding: 0; background: none;
               scroll-snap-type: none; }
  .graph { flex-direction: column; align-items: stretch; min-width: 0; }
  .gend { justify-content: flex-start; }
  .glink { width: 100%; height: 48px; justify-items: start; }
  .glink::before {
    left: 15px; right: auto; top: 0; bottom: 0;
    width: 2px; height: auto; margin: 0;
  }
  .gcard { width: auto; }
  .gcard__n { left: 4px; }

  /* 分流那几条手机覆盖挪到 .gfork 整段之后了 —— 见文件里
     「手机上的分流」那一节。写在这里会被后面同权重的规则盖掉（踩过一次）。 */
}

/* ---- 选中节点的解析明细（图上只放一行，明细在这里） ---- */
.step__who { list-style: none; margin: 10px 0 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.step__who li {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px;
  font-size: var(--fs-sub);
}
.step__who b { font-weight: var(--w-semibold); }
.step__who span { color: var(--ink-2); }
.step__who li.none b, .step__who li.vac b { color: var(--late); }
.step__who i {
  font-style: normal; font-size: var(--fs-cap); color: var(--mute);
  background: color-mix(in srgb, var(--ink) 6%, transparent);
  border-radius: var(--r-pill); padding: 1px 8px;
}
.step__warn {
  margin: 8px 0 0; font-size: var(--fs-foot); line-height: 1.5;
  color: var(--late); background: var(--late-bg);
  border-radius: var(--r-sm); padding: 8px 10px;
}
.step__note {
  margin: 8px 0 0; font-size: var(--fs-foot); line-height: 1.5;
  color: var(--ink-2); background: var(--surface-2);
  border-radius: var(--r-sm); padding: 8px 10px;
}

/* ---- 节点菜单（右栏） ---- */
/* 弹层里的小标题只有这一种（原来还有 .flab 和 .nodenow 两种，同一屏三种字重）。 */
.rail__t.sub { margin: 14px 0 6px; }

.nodemenu {
  display: grid; gap: 1px; margin-bottom: 18px;
  background: var(--line); border-radius: var(--r-sm); overflow: hidden;
}
.nodeitem {
  background: var(--surface); color: var(--ink); text-decoration: none;
  padding: 11px 13px; min-height: var(--tap);
  display: flex; align-items: center; gap: 8px;
  transition: background var(--t-2) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) { .nodeitem:hover { background: var(--surface-2); } }
.nodeitem b { flex: none; font-size: var(--fs-callout); font-weight: var(--w-semibold); }
.nodeitem em { font-style: normal; font-size: var(--fs-cap); color: var(--mute); }
.nodeitem i { margin-left: auto; font-style: normal; color: var(--mute); transition: transform var(--t-3) var(--ease-out); }
.nodeitem--sub { display: block; padding: 0; }
.nodeitem--sub summary {
  list-style: none; cursor: pointer;
  padding: 11px 13px; min-height: var(--tap);
  display: flex; align-items: center; gap: 8px;
}
.nodeitem--sub summary::-webkit-details-marker { display: none; }
.nodeitem--sub[open] summary i { transform: rotate(180deg); }
.nodesub { padding: 2px 13px 13px; }
.nodesub form { display: grid; gap: 6px; }
.nodesub button {
  font: inherit; font-size: var(--fs-sub); text-align: left; cursor: pointer;
  padding: 9px 12px; min-height: 40px;
  display: flex; align-items: baseline; gap: 8px;
  border: 1px solid var(--line-strong); border-radius: var(--r-sm);
  background: var(--surface); color: var(--ink);
}
@media (hover: hover) and (pointer: fine) { .nodesub button:hover { background: var(--surface-2); } }
.nodesub button.on { border-color: var(--tint); color: var(--tint); background: var(--tint-soft); }
.nodesub button em { font-style: normal; margin-left: auto; font-size: var(--fs-cap); color: var(--mute); }
.nodesub .hint { margin: 10px 0 0; }

/* ---- 选人 / 选岗位 ---- */
.picksearch { display: flex; gap: 8px; margin-bottom: 12px; }
.picksearch input { flex: 1; min-width: 0; }
.picksearch button { flex: none; }

.crumbs {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px;
  font-size: var(--fs-cap); margin-bottom: 8px;
}
.crumbs a { color: var(--tint); text-decoration: none; }
.crumbs a:last-child { color: var(--ink-2); font-weight: var(--w-semibold); }
.crumbs span { color: var(--mute); }

.picklist {
  list-style: none; margin: 0 0 12px; padding: 0;
  display: grid; gap: 1px;
  background: var(--line); border-radius: var(--r-sm); overflow: hidden;
}
.picklist li { background: var(--surface); }
.pickform { display: flex; }
.pickrow {
  font: inherit; text-align: left; cursor: pointer; width: 100%;
  border: 0; background: var(--surface); color: var(--ink);
  padding: 10px 13px; min-height: var(--tap);
  display: flex; align-items: baseline; gap: 8px;
  transition: background var(--t-2) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) { .pickrow:hover { background: var(--tint-soft); } }
.pickrow b { font-size: var(--fs-callout); font-weight: var(--w-semibold); }
.pickrow span { font-size: var(--fs-cap); color: var(--mute); margin-left: auto; }
.pickrow i { font-style: normal; color: var(--mute); margin-left: 4px; }
.pickrow.dir { text-decoration: none; }
.pickrow.dim { color: var(--mute); cursor: default; }
@media (hover: hover) and (pointer: fine) { .pickrow.dim:hover { background: var(--surface); } }
.pickadd {
  font: inherit; font-size: var(--fs-body); cursor: pointer; flex: none;
  width: 40px; border: 0; border-left: 1px solid var(--line);
  background: var(--surface); color: var(--tint);
}
@media (hover: hover) and (pointer: fine) { .pickadd:hover { background: var(--tint-soft); } }

/* ---------- 08 表格 ---------- */

.tablewrap { overflow-x: auto; -webkit-overflow-scrolling: touch; border-radius: var(--r-lg); }
.ledger {
  width: 100%; min-width: 620px; border-collapse: collapse;
  background: var(--surface); font-size: var(--fs-sub);
}
.ledger th, .ledger td {
  padding: 10px 14px; text-align: left;
  border-bottom: 1px solid var(--line); vertical-align: top;
  /* 中文名词不拆行：自动表格布局会把每一列按比例往 min-content（一个字）压，
     1100 宽上「启用」曾是一字一行、人名全是两行。词只在标点和空格处断，长句自己让路。 */
  word-break: keep-all;
}
.ledger thead th {
  font-size: var(--fs-cap); font-weight: var(--w-semibold); color: var(--mute);
  letter-spacing: 0.04em; white-space: nowrap;
  background: var(--surface-2);
  position: sticky; top: 0; z-index: 1;
}
.ledger tbody tr:last-child td { border-bottom: 0; }
.ledger tbody tr { transition: background var(--t-2) var(--ease-out); }
@media (hover: hover) and (pointer: fine) { .ledger tbody tr:hover { background: color-mix(in srgb, var(--ink) 3%, transparent); } }
.ledger .num { font-variant-numeric: tabular-nums; white-space: nowrap; }
/* 一个名词不许被相邻的宽列挤成两行（考勤设置里「北京公司」曾被拆成「北京公 / 司」）。 */
.nw { white-space: nowrap; }
.ledger td .sub { display: block; margin-top: 2px; max-width: 32ch; overflow-wrap: anywhere; }
.ledger td .wraps { display: block; max-width: 26ch; overflow-wrap: anywhere; }
.seal-form { margin-top: 6px; }

/* 窄屏把表拆成「一条记录一块、每行带字段名」。
   同一个 <table>，靠 data-label 把表头搬进单元格里，不另写一套标记。 */
@media (max-width: 768px) {
  .tablewrap { overflow: visible; }
  .ledger { min-width: 0; display: block; background: none; }
  .ledger thead { display: none; }
  .ledger tbody, .ledger tr, .ledger td { display: block; }
  .ledger tr {
    background: var(--surface); border: 1px solid var(--line);
    border-radius: var(--r-md); padding: 8px 16px; margin-bottom: 10px;
  }
  /* 🔴 flex-wrap 不能省。一格里只装一个值时怎么都行，
     而「打卡明细」那一格装的是一天的全部刺卡（N 个 .tag）——
     不换行就排成一条 4179px 的长行，而这一层没有任何祖先能横向滚。
     在 375px 上量到：第二、第三枚标签落在 x=508 / 691，**看不见也滚不到**。 */
  .ledger td { border: 0; padding: 7px 0; display: flex; flex-wrap: wrap; gap: 12px; align-items: baseline; word-break: normal; }
  .ledger td + td { border-top: 1px solid var(--line); }
  .ledger td::before {
    content: attr(data-label); flex: 0 0 5.5em;
    color: var(--mute); font-size: var(--fs-cap);
  }
  .ledger td .sub, .ledger td .wraps { max-width: none; }
}

/* 保密设置里的对照表 */
.scope { margin-bottom: 24px; }
.scopetbl { width: 100%; border-collapse: collapse; font-size: var(--fs-foot); }
.scopetbl th, .scopetbl td {
  border: 1px solid var(--line); padding: 10px 12px; text-align: left; vertical-align: top;
}
.scopetbl th { width: 42%; background: var(--surface-2); font-weight: var(--w-semibold); }
.scopetbl span { display: block; font-weight: var(--w-regular); font-size: var(--fs-cap); color: var(--mute); margin-top: 3px; line-height: 1.6; }
.scopetbl td.yes { color: var(--ok); font-weight: var(--w-semibold); }
.scopetbl td.part { color: var(--late); font-weight: var(--w-semibold); }
.scopetbl td.no { color: var(--bad); font-weight: var(--w-semibold); }
.scopetbl td span { color: var(--mute); font-weight: var(--w-regular); }

/* ---- 组织页：摘要 + 收起的明细 ---- */
.orgsum { margin-bottom: 26px; }
.finding.ok a { background: var(--ok-bg); border-color: transparent; box-shadow: none; }
.finding.ok .finding__n { color: var(--ok); }
.finding.ok .finding__d { color: color-mix(in srgb, var(--ok) 68%, var(--ink)); }

.chainsbox {
  margin-top: 28px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-lg); overflow: hidden;
}
.chainsbox > summary {
  list-style: none; cursor: pointer;
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding: 15px 18px; min-height: var(--tap);
}
.chainsbox > summary::-webkit-details-marker { display: none; }
.chainsbox > summary b { font-size: var(--fs-callout); font-weight: var(--w-semibold); }
.chainsbox > summary em { font-style: normal; font-size: var(--fs-foot); color: var(--mute); }
.chainsbox > summary i { margin-left: auto; color: var(--mute); transition: transform var(--t-3) var(--ease-out); }
.chainsbox[open] > summary i { transform: rotate(180deg); }
.chainsbox[open] > summary { border-bottom: 1px solid var(--line); }
.chainsbox > :not(summary) { padding-inline: 18px; }
.chainsbox .tablewrap { padding: 0 18px 18px; }
.chains .who {
  font-size: var(--fs-cap); font-weight: var(--w-bold); color: var(--ink-2);
  background: color-mix(in srgb, var(--ink) 7%, transparent);
  border-radius: var(--r-pill); padding: 1px 8px; margin-right: 6px;
}

/* ---- 不可逆动作的确认块 ----

   全站只有一处用它：办理离职。刻意**没有**做成通用组件 ——
   到处都要确认，确认就变成了闭着眼睛点的那一下。
   其余「危险」按钮（停用、停用此章、结束任职）都撤得回来，
   它们走的是「一键 + 说清后果的回执」。 */
.confirmbox {
  background: var(--bad-bg); border-radius: var(--r-lg);
  padding: 18px 20px; margin-top: 4px;
}
.confirmbox__t { margin: 0 0 12px; font-size: var(--fs-body); font-weight: var(--w-bold); color: var(--bad); }
.confirmbox__l { margin: 0 0 14px; padding-left: 1.15rem; font-size: var(--fs-sub); line-height: 1.7; }
.confirmbox__l li.bad { color: var(--bad); }
.confirmbox__l ul { margin: 4px 0 0; padding-left: 1.1rem; font-size: var(--fs-foot); }
.confirmbox__w { margin: 0 0 12px; font-size: var(--fs-sub); color: var(--bad); }
.confirmbox__f { display: grid; gap: 12px; }
.confirmbox__f input[type="text"] { max-width: 34rem; }

/* 离职确认里「这个岗位交给谁」（044），以及代管卡片底下那一行。
   两处是同一个形状：一句事实 + 一个选择框，横着一排。
   **不画边框不画底色** —— 它们长在 .confirmbox / .card 里面，
   再来一层就是卡中卡。 */
.succs { list-style: none; margin: 6px 0 0; padding: 0; display: grid; gap: 8px; max-width: 46rem; }
.succ { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* select 的基础规则是 `width: 100%`，放进 flex 里就成了「独占一行还撑满」——
   1400 宽的屏上量到过一个横贯整页的下拉框。宽度上限落在**控件**上，
   落中档（见上面「控件宽度：两档」那一节），不跟着容器走。 */
.succ select { flex: 0 1 var(--fw-m); width: auto; min-width: 12rem; }
.confirmbox__f input { background: var(--surface); }

/* ---- 管理区：侧栏分区 ----

   只在 /admin/* 下出现。竖线是「这几项属于上面那一项」的最轻表达 ——
   缩进 + 一根发丝线，不另做一套导航语汇。
   收成图标条时整块隐掉：九个管理入口做成图标是认不出来的，
   而这一段的价值全在「一眼看到四个名字」。 */
.side__sub {
  display: grid; gap: 2px;
  margin: 2px 0 2px 15px; padding-left: 11px;
  border-left: 1px solid var(--line);
  animation: subin var(--t-3) var(--ease-out) both;
}
.side__link.sub {
  min-height: var(--tap); font-size: var(--fs-sub); font-weight: var(--w-semibold);
  color: var(--ink-2);
}
.side__link.sub .side__ic { width: 18px; opacity: 0.7; }
.side__link.sub.on {
  background: color-mix(in srgb, var(--admin) 14%, transparent);
  color: var(--admin); font-weight: var(--w-semibold);
}
.side__link.sub.on .side__ic { opacity: 1; }
.side.mini .side__sub { display: none; }

/* 展开是一次「长出来」，不是一次淡入：让人看见这四项是从「管理」里来的。
   只动 transform / opacity，不动布局属性。 */
@keyframes subin {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}

/* ---- 管理首页：需要处理 ----

   这一块**没事的时候整块不出现**。一排零和一排非零长得一样，
   那它就永远不值得看第二眼。 */
.lede {
  margin: 6px 0 22px; max-width: 62ch;
  font-size: var(--fs-body); color: var(--ink-2); line-height: 1.65;
}
.findings { margin-bottom: 30px; }
.findlist { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
/* 逐条进场，间隔 45ms。
   这是这一页唯一该动的东西 —— 它存在的理由就是「看这里」，
   而依次落下比同时出现更能让人把它们**数一遍**。
   最多五条，所以不必写循环；只动 transform / opacity。 */
/* 首屏错开进场（第 8 条）：每行 --stagger（30ms），最多 6 行，第 7 行起一起进。

   错开是为了让眼睛顺着往下扫一遍，不是为了好看 —— 行数一多，
   最后几行的等待就从「引导」变成了「页面还没加载完」。

   **只给第一次**：`.stagger` 这个类由服务端在「地址上没有参数」时才加
   （`firstLook`）—— 翻页、筛选、排序、以及做完动作跳回来的那一次都不错开，
   那时人是来找一行具体的，最后一行晚 150ms 就是白等 150ms。

   `.finding` 和列表共用同一套，别再各写一遍。 */
.finding, .stagger > li { animation: findin var(--t-3) var(--ease-out) both; }
.finding:nth-child(2), .stagger > li:nth-child(2) { animation-delay: calc(var(--stagger) * 1); }
.finding:nth-child(3), .stagger > li:nth-child(3) { animation-delay: calc(var(--stagger) * 2); }
.finding:nth-child(4), .stagger > li:nth-child(4) { animation-delay: calc(var(--stagger) * 3); }
.finding:nth-child(5), .stagger > li:nth-child(5) { animation-delay: calc(var(--stagger) * 4); }
.finding:nth-child(6), .stagger > li:nth-child(6) { animation-delay: calc(var(--stagger) * 5); }
@keyframes findin {
  from { opacity: 0; transform: translateY(7px); }
  to   { opacity: 1; transform: none; }
}
.finding a {
  display: grid; align-items: baseline; gap: 2px 12px;
  grid-template-columns: auto 1fr;
  padding: 13px 16px; border-radius: var(--r-md);
  background: var(--surface); border: 1px solid var(--line);
  transition: border-color var(--t-2) var(--ease-out), background var(--t-2) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) { .finding a:hover { border-color: var(--line-strong); background: var(--surface-2); } }
.finding__n {
  grid-row: span 2; align-self: center;
  font-size: var(--fs-large); font-weight: var(--w-bold); line-height: 1;
  font-variant-numeric: tabular-nums; letter-spacing: -0.02em;
}
.finding__t { font-size: var(--fs-callout); font-weight: var(--w-semibold); }
.finding__d { font-size: var(--fs-foot); color: var(--mute); line-height: 1.55; }
.finding.bad a { background: var(--bad-bg); border-color: transparent; }
.finding.bad .finding__n { color: var(--bad); }
.finding.bad .finding__d { color: color-mix(in srgb, var(--bad) 74%, var(--ink)); }
.finding.warn a { background: var(--late-bg); border-color: transparent; }
.finding.warn .finding__n { color: var(--late); }
.finding.warn .finding__d { color: color-mix(in srgb, var(--late) 70%, var(--ink)); }

.allclear {
  margin: 0 0 30px; padding: 15px 18px;
  border-radius: var(--r-md);
  background: var(--ok-bg); color: var(--ok);
  font-size: var(--fs-callout); font-weight: var(--w-semibold);
}

/* ---- 管理首页：四个分区 ---- */
.secgrid {
  display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fit, minmax(272px, 1fr));
}
.secbox {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 18px 18px 8px;
}
.secbox__h {
  display: flex; align-items: center; gap: 9px;
  margin: 0 0 6px; font-size: var(--fs-body); font-weight: var(--w-bold);
  letter-spacing: -0.01em;
}
.secbox__ic { width: 19px; height: 19px; color: var(--tint); flex: none; }
.secbox__d {
  margin: 0 0 12px; font-size: var(--fs-foot);
  color: var(--mute); line-height: 1.6;
}
.seclinks { list-style: none; margin: 0 -18px; padding: 0; }
.seclinks li + li { border-top: 1px solid var(--line); }
.seclinks a {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding: 11px 18px; min-height: var(--tap);
  font-size: var(--fs-callout); font-weight: var(--w-semibold);
  transition: background var(--t-2) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) { .seclinks a:hover { background: var(--tint-soft); color: var(--tint); } }
.seclinks em {
  font-style: normal; font-size: var(--fs-foot);
  font-weight: var(--w-regular); color: var(--mute);
}
.hint.foot { margin-top: 26px; }

@media (max-width: 768px) {
  .finding a { padding: 12px 14px; }
  .secgrid { grid-template-columns: 1fr; }
}

/* ---------- 09 页面专属 ---------- */

/* ---- 登录（2026-09-12 重做，参考 WorkBuddy/TDTSAT天地通 那个项目的登录页）----
   素底 + 品牌色晕 + 一组轨道环压在卡片后面；一张分两栏的玻璃卡（左图右表单）；整张卡跟着鼠标轻轻倾斜
   （app.js [data-tilt]）。
   🔴 玻璃只给控制层 —— 这一页是那条规矩唯一的例外，理由和当初写「实底就够」时相反：
   现在卡片后面**有东西**（轨道环穿过卡片，一半被磨糊一半清晰），玻璃才成立；页面上没有别的内容和它抢层次。
   材质走 --mat-reg / --blur / --specular，和侧栏同一套。 */
.login {
  position: relative;
  display: flex; flex-direction: column; align-items: center;
  width: 100%; min-height: calc(100dvh - var(--frame) * 2);
  padding: 56px 0 16px;
  /* 轨道环伸到视口外面：只横向裁掉（clip 不产生滚动容器），竖向留给卡片的影子。 */
  overflow-x: clip;
  perspective: none;
  /* 三团品牌色晕，压在卡片后面给玻璃提供可磨的内容 */
  background:
    radial-gradient(720px 520px at 34% 26%, var(--login-aura), transparent 70%),
    radial-gradient(520px 400px at 60% 42%, var(--login-aura), transparent 70%),
    radial-gradient(640px 480px at 70% 74%, var(--login-aura), transparent 70%);
  --login-aura: color-mix(in srgb, var(--tint) 14%, transparent);
  --login-ring: color-mix(in srgb, var(--tint) 30%, transparent);
}
/* 轨道：渐变光盘大环 + 同心细环，一半穿过玻璃卡（被磨糊）、一半露在外面（清晰）。
   排在卡片前面、不带 z-index，靠 DOM 顺序垫在卡片下面就能被玻璃采样。环用 box-shadow 画不用 border：
   它是装饰不是「盒子」，别进那个棘轮。 */
.login__orbit {
  position: absolute; left: 58%; top: 6%;
  width: 620px; height: 620px; border-radius: var(--r-pill);
  background: radial-gradient(circle at 38% 34%, var(--login-aura) 58%, transparent 76%);
  box-shadow: 0 0 0 1.5px var(--login-ring);
  pointer-events: none;
}
.login__orbit::after {
  content: ""; position: absolute; inset: 88px; border-radius: inherit;
  box-shadow: 0 0 0 1.5px var(--login-ring);
}
.login__prefs { position: absolute; top: 12px; right: 12px; z-index: 2; }
/* tilt 载体：入场动画在卡片上，倾斜在这一层，二者互不干扰 */
.login__tilt { position: relative; margin: auto 0; transform-style: flat; }
/* 登录卡：这一页唯一的「盒子」（.login__card 在棘轮基线里本来就有一个）。 */
.login__card {
  position: relative; overflow: hidden;
  display: grid; grid-template-columns: minmax(280px, 420px) minmax(0, 1fr);
  width: min(1040px, calc(100vw - var(--frame) * 4)); min-height: 560px;
  border-radius: var(--r-lg); border: 1px solid var(--line);
  background: var(--login-glass);
  -webkit-backdrop-filter: saturate(155%) blur(18px); backdrop-filter: saturate(155%) blur(18px);
  box-shadow: var(--glass-shadow);
  animation: login-rise var(--t-4) var(--ease-out) both;
}
@keyframes login-rise { from { opacity: 0; transform: translateY(12px); } }
/* 左：品牌视觉。**两套主题下都是深的**（参考稿也是），所以字色在这里局部改写成浅的 —— 别在外面用 --on-tint，
   它在深色主题下是深字。 */
.login__visual {
  position: relative; isolation: isolate;
  display: flex; flex-direction: column; justify-content: space-between;
  padding: 28px 30px 30px; min-height: 100%;
  --ink: #F2F3FF; --ink-2: #C9CEEE; --mute: #AEB4DC;
  color: var(--ink);
}
.login__img { position: absolute; inset: 0; z-index: -1; width: 100%; height: 100%; object-fit: cover; }
/* 图上再压一层由深到浅到深的暗纱：两角的字要压得住任何一张照片 */
.login__visual::before {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: linear-gradient(180deg, rgba(8, 12, 24, 0.42) 0%, rgba(8, 12, 24, 0.06) 42%, rgba(8, 12, 24, 0.6) 100%);
}
.login__brand { display: flex; align-items: center; gap: 10px; font-size: var(--fs-head); font-weight: var(--w-semibold); letter-spacing: 0.04em; }
.login__mark { display: block; flex: none; width: 156px; height: 28px; color: var(--ink); }
.login__brand > span:last-child { padding-left: 12px; border-left: 1px solid var(--ink-2); white-space: nowrap; font-size: var(--fs-foot); }
.login__slogan { max-width: 32ch; }
.login__headline { margin: 0; font-size: var(--fs-large); font-weight: var(--w-bold); letter-spacing: 0.02em; }
.login__desc { margin: 8px 0 0; font-size: var(--fs-foot); line-height: 1.6; color: var(--ink-2); }
/* 右：表单 */
.login__panel { display: flex; flex-direction: column; justify-content: center; padding: 44px 52px; min-width: 0; }
.login__accent { display: block; width: 28px; height: 3px; border-radius: var(--r-pill); background: var(--tint); margin-bottom: 16px; }
.login h1 { font-size: var(--fs-large); font-weight: var(--w-bold); letter-spacing: -0.01em; margin: 0 0 6px; }
.login .sub { color: var(--mute); margin: 0 0 14px; font-size: var(--fs-sub); }
.login form { display: grid; }
.login form label { margin-top: 18px; }
.login form label:first-of-type { margin-top: 12px; }
.login__err { margin: 8px 0 0; animation: login-shake var(--t-4) var(--ease-out); }
/* 出现时轻微横向抖动：错误反馈的通用体感，只一次不循环 */
@keyframes login-shake {
  0% { opacity: 0; transform: translateX(0); } 25% { opacity: 1; transform: translateX(-5px); }
  50% { transform: translateX(4px); } 75% { transform: translateX(-2px); } 100% { transform: translateX(0); }
}
/* 输入框左边一颗图标，聚焦时跟着变成品牌色 */
.login__in { position: relative; display: block; }
.login__in > input { padding-left: 42px; max-width: none; width: 100%; }
.login__ic {
  position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
  width: 18px; height: 18px; pointer-events: none;
  fill: none; stroke: var(--mute); stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round;
  transition: stroke var(--t-1) var(--ease-out);
}
.login__in:focus-within .login__ic { stroke: var(--tint); }
.login button.primary { width: 100%; margin-top: 26px; transition: background var(--t-2) var(--ease-out), transform var(--t-1) var(--ease-out), box-shadow var(--t-1) var(--ease-out); }
@media (hover: hover) and (pointer: fine) {
  .login button.primary:hover { transform: translateY(-1px); box-shadow: var(--shadow-card); }
}
.login__foot { margin: 24px 0 0; text-align: center; color: var(--mute); font-size: var(--fs-cap); }
/* 窄了：一列，图退成顶上一条横幅，字还在两角。 */
@media (max-width: 900px) {
  .login { padding-top: 52px; }
  .login__orbit { left: 50%; top: -10%; width: 420px; height: 420px; }
  .login__orbit::after { inset: 60px; }
  .login__card { grid-template-columns: minmax(0, 1fr); width: min(560px, calc(100vw - var(--frame) * 4)); min-height: 0; }
  .login__visual { min-height: 200px; padding: 20px 22px 22px; }
  .login__headline { font-size: var(--fs-title); }
  /* 横幅只有 200 高，图保留的是底边（台站和天线）；说明那行压在台站上就挤了，页脚本来就写着公司名。 */
  .login__desc { display: none; }
  .login__panel { padding: 26px 24px 30px; }
}

/* 管理区「登录页」那一页的预览：按登录页那块 440 × 580 的比例缩到一半。 */
.lvprev { display: flex; gap: 20px; align-items: flex-start; flex-wrap: wrap; margin: 4px 0 24px; }
.lvprev__box { width: 220px; height: 290px; border-radius: var(--r-md); overflow: hidden; padding: 14px 15px 15px; flex: none; }
.lvprev__box .login__brand { font-size: var(--fs-foot); gap: 6px; }
.lvprev__box .login__mark { width: 114px; height: 20px; }
.lvprev__box .login__headline { font-size: var(--fs-body); }
.lvprev__box .login__desc { font-size: var(--fs-micro); margin-top: 4px; }
.lvprev__facts { flex: 1 1 240px; min-width: 0; }
.lvprev__facts > p { margin: 0 0 8px; }

/* 审批单：按 A4 一页调的，屏幕上只是预览 */
.sheetbar { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 16px; }
.sheetbar .back { color: var(--mute); font-size: var(--fs-sub); }
.sheetbar .btns { display: flex; gap: 10px; }
/* 存 PDF 那条路藏在打印对话框里 —— 用户说「有的人不会用」。
   一行字比一个按钮便宜，而且它说的是**怎么做**，不是又一个能点的东西。 */
.sheethint { margin: -8px 0 16px; color: var(--mute); font-size: var(--fs-foot); }
.sheet {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 24px 22px;
}
.sh-logo { display: block; width: 126px; height: 22px; margin-bottom: 10px; color: var(--brand-mark); }
.sh-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
.sh-head .org { margin: 0 0 3px; font-size: var(--fs-cap); color: var(--mute); }
.sh-head h1 { margin: 0; font-size: var(--fs-title); font-weight: var(--w-bold); letter-spacing: -0.01em; }
.sh-head .stamp {
  flex: none; border: 2px solid currentColor; border-radius: var(--r-sm);
  padding: 5px 12px; font-weight: var(--w-bold); font-size: var(--fs-foot);
  transform: rotate(-8deg);
}
.sh-head .stamp.ok { color: var(--ok); }
.sh-head .stamp.bad { color: var(--bad); }
.sh-head .stamp.run { color: var(--late); }
.sh-head .stamp.mute { color: var(--mute); }
.sh-sec { font-size: var(--fs-sub); font-weight: var(--w-semibold); margin: 22px 0 8px; padding-bottom: 5px; border-bottom: 1px solid var(--line); }
.sh-none { margin: 0; color: var(--mute); font-size: var(--fs-foot); }
.sh-info, .sh-fields, .sh-flow { width: 100%; border-collapse: collapse; font-size: var(--fs-foot); margin-top: 10px; }
.sh-info th, .sh-info td, .sh-fields th, .sh-fields td, .sh-flow th, .sh-flow td {
  border: 1px solid var(--line); padding: 8px 10px; text-align: left; vertical-align: top;
}
.sh-info th, .sh-fields th, .sh-flow th { background: var(--surface-2); font-weight: var(--w-semibold); color: var(--ink-2); white-space: nowrap; }
.sh-info th { width: 5.5rem; }
.sh-fields th { width: 6.5rem; }
.sh-fields tr.wide td { white-space: pre-wrap; line-height: 1.7; }
.sh-flow td { word-break: break-word; }
/* 还没轮到的那几关是**预测**，和已办的不能一个颜色 —— 屏幕上那条链是虚线淡色，
   纸上从写下那天起就是同一种黑（tr.sh-soon 一直没有规则，PITFALLS #241）。 */
.sh-flow tr.sh-soon td { color: var(--mute); }
.sheetlink { margin: 16px 0 0; }
.sheetlink a { font-size: var(--fs-sub); color: var(--tint); font-weight: var(--w-semibold); }

/* 水印预览 */
.wmsample {
  display: grid; place-items: center;
  min-height: 100px; padding: 18px 12px; overflow: hidden;
  border: 1.5px dashed var(--line-strong); border-radius: var(--r-md); background: var(--surface);
}
/* 水印文本可以很长（姓名 + 时间 + IP + 自定义文字），而这块预览在手机上
   只有三百多像素宽。不换行就会被两头裁掉 —— 预览里看到的「026-09-03」
   会让人以为水印本身是坏的。 */
.wmsample span {
  transform: rotate(-14deg); font-weight: var(--w-semibold); letter-spacing: 0.04em;
  color: color-mix(in srgb, var(--ink) 24%, transparent);
  font-size: clamp(11px, 3.1vw, 15px);
  max-width: 88%; text-align: center; line-height: 1.6;
}
.wmsample em { color: var(--mute); font-style: normal; font-size: var(--fs-foot); }
.banner { padding: 12px 14px; border-radius: var(--r-md); }
.foot { margin-top: 22px; }

/* 发起按钮与菜单 -------------------------------------- */
/*
   用 <details> 而不是脚本：没有 JS 也能开合，手机上更不容易失灵。
   桌面上它是右下角的浮起气泡，手机上是从底部升起的一张卡。
*/
.fabwrap {
  position: fixed; right: 20px; z-index: 55;
  bottom: calc(20px + env(safe-area-inset-bottom));
  transition: opacity var(--t-2) var(--ease-out); /* 工作台整理态让位（.wb-editing） */
}
.fabwrap > summary { list-style: none; cursor: pointer; }
.fabwrap > summary::-webkit-details-marker { display: none; }
.fab {
  display: inline-flex; align-items: center; gap: 8px;
  height: 52px; padding: 0 22px; border-radius: var(--r-pill);
  background: var(--tint); color: var(--on-tint);
  font-weight: var(--w-semibold); font-size: var(--fs-callout);
  box-shadow: 0 14px 34px -12px color-mix(in srgb, var(--tint) 85%, transparent),
              var(--specular);
  transition: transform var(--t-2) var(--ease-out), background var(--t-2) var(--ease-out);
}
.fab:active { transform: scale(0.97); transition-duration: var(--t-1); }
.fabwrap[open] .fab { background: var(--tint-2); }

.fabmenu {
  position: absolute; right: 0; bottom: calc(100% + 12px);
  width: min(20rem, calc(100vw - 40px));
  border-radius: var(--r-lg); overflow: hidden;
  background: var(--mat-thick);
  -webkit-backdrop-filter: var(--blur);
  backdrop-filter: var(--blur);
  border: 1px solid var(--line);
  box-shadow: var(--specular), var(--shadow-lift);
  animation: pop var(--t-4) var(--ease-out);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .fabmenu { background: var(--surface); }
}
.fabmenu a {
  display: grid; grid-template-columns: 2.3rem 1fr; align-items: center;
  gap: 2px 12px; padding: 13px 16px; min-height: var(--tap);
  color: var(--ink);
}
.fabmenu a + a { border-top: 1px solid var(--line); }
.fabmenu a:active { background: color-mix(in srgb, var(--ink) 8%, transparent); }
.fabmenu__title { margin: 0; padding: 16px 18px 8px; color: var(--mute); font-size: var(--fs-cap); }
.fabmenu__list { max-height: min(52dvh, 420px); overflow-y: auto; overscroll-behavior: contain; }
.fabmenu__list a { display: flex; min-height: 52px; text-decoration: none; }
.fabmenu__empty { margin: 0; padding: 12px 18px 18px; font-size: var(--fs-foot); color: var(--mute); }
.fabmenu > .more { border-top: 1px solid var(--line); text-decoration: none; }
.fabmenu .ic { grid-row: 1 / 3; width: 24px; height: 24px; justify-self: center; color: var(--tint); }
.fabmenu .more .ic { color: var(--mute); }
.fabmenu b { font-weight: var(--w-semibold); }
.fabmenu i { font-style: normal; font-size: var(--fs-cap); color: var(--mute); }

@keyframes pop {
  from { opacity: 0; transform: translateY(8px) scale(0.97); }
  to { opacity: 1; transform: none; }
}

/* ---------- 断点 ---------- */

/* 1280 以下收窄留白 */
@media (max-width: 1280px) {
  :root { --rail-w: 268px; }
}

/* 大桌面（≥1440）：桌面不能和 iPad 一样宽，不然桌面就没有意义了。
   量出来的事实：1180 的上限在 1920 的屏上两边各空 236px；有右栏的页面
   内容列是 820 —— 和 iPad 横屏（1024）上一模一样，右栏里的输入框只有 270px。
   宽度给谁：并排的两栏（列表 + 右栏编辑）、表格、横排的流程图。
   不给谁：段落仍是 62ch，搜索框仍是 640 —— 读和填的宽度不随屏幕变。
   下限取 1440 而不是 1025：1025–1439 之间主区本来就不到 1180，上限根本没生效。 */
@media (min-width: 1440px) {
  :root { --rail-w: 360px; }
  .page-wrap:has(.rail) .col { max-width: 1360px; }
  /* 单列表单的上限从 640 放到 720：再宽一行输入框就成了一条横线，眼睛跟不住。 */
  .form { max-width: 720px; }
}

/* 超宽屏上右栏可以宽一点。正文那一列不再封顶 ——
   ⚠️ 这里原来写着「1360 封顶」，2026-09-08 用户点掉了：
   「浪费了很多横向空间」。封顶的初衷（一行字别太长）由正文自己的
   ch 上限管，用整块面板去管，代价是清单和表格也跟着挨饿。 */
@media (min-width: 1800px) {
  :root { --rail-w: 400px; }
}

/* 右栏在 1024 以下一律落回正文下面，主操作重新贴底。
   这一段对平板和手机都成立，所以不加下限。 */
@media (max-width: 1024px) {
  .page-wrap:has(.rail) { grid-template-columns: minmax(0, 1fr); }
  .rail { position: static; }
  /* 用 fixed 不用 sticky：右栏正好被这块填满，sticky 没有可滑动的余量，
     结果是它跟着正文滚出屏幕，按钮在页尾找不到。 */
  .rail__act {
    position: fixed; z-index: 45;
    left: 0; right: 0; bottom: 0;
    margin: 0;
    padding: 12px var(--gutter) calc(12px + env(safe-area-inset-bottom));
    border-radius: var(--r-md) var(--r-md) 0 0;
    /* 到这里它才真的浮在正文上面、正文从它底下滚过去 —— 玻璃在这一层才成立。 */
    background: var(--mat-thick);
    -webkit-backdrop-filter: var(--blur);
    backdrop-filter: var(--blur);
    box-shadow: var(--specular), var(--shadow-lift);
  }
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .rail__act { background: var(--surface); }
  }
  .rail__act .rail__t { display: none; }
  .rail__act textarea { min-height: 46px; }
  /* 这一页有自己的主操作时，标签栏让位 —— 底部只该有一个「现在要做的事」。
     iOS 的详情页也是这么做的：进去之后标签栏收起来，靠返回退出。 */
  .has-act .tabbar { display: none; }
  .has-act .side ~ main, .has-act main { padding-bottom: 210px; }

  /* 🔴 删掉了 .rail-first（原来给所有 /new/ 页加的）。
     它的注释写着「右栏里放的是一张表单」，而三个发起页量下来都相反：
     .rail 里是「这张单会经过」那条链（1 个输入），main 里才是表单（9~11 个）。
     于是手机上点开发起，先看到的是一张无标题的流程图 ——
     **连页面标题和面包屑都在 main 里**，所以落地那一屏连自己是哪一页都不说。 */
}

/* 平板（含 iPad 竖屏）：侧栏默认收成图标条，点开是一次覆盖。
   **必须带下限 769**：不带的话这一段会盖到手机上，而它的选择器
   （.side:target）比手机段的 .side 更强，抽屉就永远张不开。 */
@media (min-width: 769px) and (max-width: 1024px) {
  :root { --sidebar-w: var(--sidebar-w-mini); }
  .side .side__txt, .side .side__name, .side .side__badge, .side .side__who { display: none; }
  .side .side__link { justify-content: center; padding: 0; gap: 0; }
  .side .side__head { justify-content: center; padding-left: 0; padding-right: 0; min-height: 34px; }
  .side .side__mark { display: none; }
  .side .side__user { justify-content: center; padding: 6px 0; }
  .side .side__out { margin-left: 0; }
  /* 自动收起的平板侧栏与桌面 mini 使用相同的单列排列。 */
  .side:not(:target) .side__kbd { display: none; }
  .side:not(:target) .side__user { flex-direction: column; gap: var(--rail-gap); padding: 0; }
  .side:not(:target) .side__foot { padding-top: 0; }
  .side:not(:target) .side__nav { align-items: center; gap: var(--rail-gap); }
  .side:not(:target) .side__link,
  .side:not(:target) .side__avatar,
  .side:not(:target) .side__prefs,
  .side:not(:target) .side__out .iconbtn {
    width: var(--rail-size); height: var(--rail-size); min-height: var(--rail-size);
    flex: none; padding: 0; margin: 0; align-self: center;
  }

  /* 点开不推开内容 —— iPad 竖屏上推开会把正文挤没。 */
  .side:target {
    width: var(--sidebar-w);
    box-shadow: var(--specular), var(--shadow-lift);
  }
  .side:target .side__txt, .side:target .side__name,
  .side:target .side__badge, .side:target .side__who { display: block; }
  .side:target .side__link { justify-content: flex-start; padding: 0 12px; gap: 12px; }
  .side:target .side__head { justify-content: flex-start; padding-left: 6px; padding-right: 6px; }
  .side:target .side__mark { display: grid; }
  .side:target .side__user { justify-content: flex-start; padding: 6px 8px; }
  .side:target .side__out { margin-left: auto; }
  .side:target ~ .scrim {
    display: block; position: fixed; inset: 0; z-index: 55;
    background: rgba(10, 10, 22, 0.30); opacity: 1; pointer-events: auto;
  }
  /* 收成图标条时给「展开」，展开之后给「关闭」。 */
  .side__toggle { display: none; }
  .side__open { display: grid; margin-left: 0; }
  .side:target .side__open { display: none; }
  .side:target .side__close { display: grid; }
}

/* 手机：侧栏变抽屉，主要入口落到底部标签栏 */
@media (max-width: 768px) {
  /* 手机换回 iOS 的尺度。手臂到屏幕的距离和屏幕本身都不一样，
     一套字号伺候不了两种设备 —— 桌面那套在这儿会小，这套在桌面上会大。
     仍然是 rem，系统字号调大时整套跟着大。 */
  :root {
    --gutter: 16px;
    --fs-large: 1.5625rem;
    --fs-title: 1.25rem;
    --fs-head: 1.0625rem;
    --fs-body: 1.0625rem;
    --fs-callout: 1rem;
    --fs-sub: 0.9375rem;
  }

  .side {
    left: 8px; right: 8px; top: auto; bottom: 8px;
    width: auto; max-height: min(82dvh, 680px); height: auto;
    border-radius: var(--r-sheet); padding: 0 14px max(14px, env(safe-area-inset-bottom));
    background: var(--mat-thick); box-shadow: var(--glass-shadow);
    transform: translateY(calc(100% + 24px)); visibility: hidden;
    transition: transform var(--t-4) var(--ease-out), visibility var(--t-4);
  }
  .side:target, .side.open { transform: none; width: auto; visibility: visible; }
  .side[data-drawer-ready] { transition: none; }
  .side__grab { display: grid; place-items: center; height: 24px; flex: none; touch-action: none; cursor: grab; }
  .side__grab span { width: 34px; height: 5px; border-radius: var(--r-pill); background: var(--line-strong); }
  .side .side__nav { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px; overflow: auto; }
  .side .side__sub { grid-column: 1 / -1; }
  .side .side__sep { grid-column: 1 / -1; margin: 6px; }
  .side .side__link { min-height: 48px; font-size: var(--fs-sub); }
  .side .side__find { flex: none; }
  .side .side__kbd { display: none; }
  .side .side__foot { flex: none; border-top: 1px solid var(--line); margin-top: 10px; }
  body.mobile-drawer-open { overflow: hidden; }
  /* 手机上从顶栏和标签栏进来，抽屉里只需要「关闭」。 */
  .side__toggle, .side__open { display: none; }
  .side__close { display: grid; }
  /* 抽屉里恢复完整文字：它是覆盖层，不占正文的地方。 */
  .side .side__txt, .side .side__name, .side .side__badge, .side .side__who { display: block; }
  .side .side__link { justify-content: flex-start; padding: 0 12px; gap: 12px; }
  .side .side__head { justify-content: flex-start; padding-left: 6px; padding-right: 6px; }
  .side .side__mark { display: grid; }
  .side .side__user { justify-content: flex-start; padding: 6px 8px; }
  .side .side__out { margin-left: auto; }
  .side__badge { display: inline-block; }

  .scrim {
    display: block; position: fixed; inset: 0; z-index: 55;
    background: rgba(10, 10, 22, 0.36);
    -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
    opacity: 0; pointer-events: none;
    transition: opacity var(--t-4) var(--ease-out);
  }
  .side:target ~ .scrim, .side.open ~ .scrim { opacity: 1; pointer-events: auto; }

  /* 选择器要压得住 .side ~ main，不然侧栏的让位在手机上还留着，
     正文被凭空削掉一截。写成同样的形状而不是光写 main。
     `.side.mini ~ main` 也要点名：它比 `.side ~ main` 多一层类名，媒体查询不加特异性压不住它 ——
     手机浏览器里只要存过一次「收起」偏好（oa.side.mini），每一页正文整个偏右 92px，
     用户 2026-09-12 实机照片抓到的就是它（PITFALLS #259）。 */
  .side ~ main, .side.mini ~ main, main {
    margin-left: 0;
    padding: calc(16px + env(safe-area-inset-top) + 52px) var(--gutter) calc(100px + env(safe-area-inset-bottom));
  }
  /* 登录页没有顶栏也没有底栏（没登录时不画），上面那条给它们留的位置在这一页是空白。 */
  main:has(.login) { padding: 0; }

  /* 顶栏：只放标题与两个圆键。滚动时正文从它下面穿过去。 */
  .topbar {
    display: flex; align-items: center; gap: 10px;
    position: fixed; z-index: 50; left: 0; right: 0;
    top: 0; height: calc(52px + env(safe-area-inset-top));
    padding: env(safe-area-inset-top) 14px 0;
    background: linear-gradient(var(--canvas), color-mix(in srgb, var(--canvas) 88%, transparent));
    -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
    border-bottom: 0;
  }
  .topbar__t {
    font-size: var(--fs-head); font-weight: var(--w-bold); letter-spacing: -0.01em;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    opacity: 0; transition: opacity var(--t-3) var(--ease-out);
  }
  .topbar.scrolled .topbar__t { opacity: 1; }
  .topbar__sp { margin-left: auto; }

  /* 底部标签栏。悬浮成一颗胶囊，不贴死边 —— iOS 26 的做法。 */
  .topbar > .iconbtn { width: 44px; height: 44px; min-height: 44px; color: var(--ink-2); background: transparent; }
  .topbar > .iconbtn .gl { width: 23px; height: 23px; }
  .topbar > .iconbtn:active { background: var(--tint-soft); transform: scale(0.97); }
  .tabbar {
    display: flex; align-items: center; gap: 10px;
    position: fixed; z-index: 50; left: 50%; transform: translateX(-50%);
    bottom: calc(10px + env(safe-area-inset-bottom));
    width: min(460px, calc(100% - 24px)); height: var(--nav-height);
    padding: 0; border: 0; border-radius: 0; background: transparent; box-shadow: none;
    -webkit-backdrop-filter: none; backdrop-filter: none;
  }
  .tabbar__items, .tabbar__more {
    position: relative; isolation: isolate; border-radius: var(--r-pill);
    background: var(--glass-face); box-shadow: var(--glass-shadow);
    border: 1px solid var(--glass-edge);
    -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur);
  }
  .tabbar__items { flex: 1; min-width: 0; height: 100%; display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); padding: 5px; }
  .tabbar__items::before, .tabbar__more::before {
    content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; z-index: -1;
    background: radial-gradient(ellipse at var(--glass-x, 24%) 0%, var(--glass-shine), transparent 65%), linear-gradient(120deg, transparent 30%, color-mix(in srgb, var(--surface) 14%, transparent), transparent 70%);
  }
  .tabbar a {
    display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 3px;
    min-width: 0; min-height: 48px; padding: 3px 2px; border-radius: var(--r-pill);
    white-space: nowrap; text-decoration: none; color: var(--ink-2); font-size: var(--fs-micro); font-weight: var(--w-medium); line-height: 1.15;
    transition: color var(--t-1) var(--ease-out), transform var(--t-1) var(--ease-out);
  }
  .tabbar .ic { width: 24px; height: 24px; flex: none; stroke-width: 1.8; }
  .tabbar a.on { background: var(--tint-soft); color: var(--tint); font-weight: var(--w-semibold); }
  .tabbar.has-lens a.on { background: transparent; }
  .tabbar a:active { transform: scale(0.97); }
  .tabbar .tabbar__more { width: var(--nav-height); height: var(--nav-height); flex: none; }
  .tabbar__selection { display: none; position: absolute; top: 5px; bottom: 5px; left: 5px; border-radius: var(--r-pill); background: var(--tint-soft); pointer-events: none; z-index: -1; }
  .tabbar.has-lens .tabbar__selection { display: block; }
  .tabbar__more[aria-expanded="true"] { color: var(--tint); }
  /* 底部有标签栏时，浮动发起键让开它 */
  .fabwrap { bottom: calc(74px + env(safe-area-inset-bottom)); right: 16px; }
  .fab { height: 48px; padding: 0 18px; }

  /* 发起菜单在手机上从底部升起，而不是挂在按钮旁边 */
  .fabmenu {
    position: fixed; left: 12px; right: 12px; width: auto;
    bottom: calc(74px + env(safe-area-inset-bottom));
    animation: rise var(--t-4) var(--ease-out);
  }
  @keyframes rise {
    from { opacity: 0; transform: translateY(18px); }
    to { opacity: 1; transform: none; }
  }

  .rail__act, .actbar {
    bottom: calc(66px + env(safe-area-inset-bottom));
    margin-bottom: calc((66px + env(safe-area-inset-bottom)) * -1);
    border-radius: var(--r-md) var(--r-md) 0 0;
    border: 1px solid var(--line);
    box-shadow: var(--shadow-lift);
  }

  .detail, .sheet { padding: 18px 16px; }
  .cards { border-radius: var(--r-md); }
  .card { padding: 14px 16px; }
}

/* 小屏（iPhone SE 一类）：再收一点 */
@media (max-width: 480px) {
  :root { --fs-large: 1.4375rem; }
  /* 第一屏要留给待办本身。入口压成一行图标条，够点就行。 */
  .quick { grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 18px; }
  .quick a { padding: 10px 4px; gap: 5px; }
  .quick .ic { width: 22px; height: 22px; }
  .quick b { font-size: var(--fs-cap); white-space: nowrap; }
  .tabbar a { min-width: 0; padding: 3px 1px; }
  .sh-info, .sh-info tbody, .sh-info tr, .sh-info th, .sh-info td { display: block; }
  .sh-info tr { margin-bottom: -1px; }
  .sh-info th { width: auto; border-bottom: 0; }
  .sh-flow { font-size: var(--fs-cap); }
}

/* ---------- 10 打印 / 存为 PDF ---------- */
/*
   PDF 不用库生成，用浏览器打印。中文 PDF 要嵌 CJK 字体：好字体多半不可
   自由分发，还会把 13MB 的二进制撑到二十几兆。而「打印 → 存为 PDF」
   排版所见即所得，手机上也能用，代价是零。
   所以这一段样式表就是我们的 PDF 引擎。
*/
@media print {
  @page { margin: 14mm 12mm; }
  /* 与手动主题同等优先级；仅 :root 压不过 :root[data-theme="dark"]。 */
  :root, :root[data-theme] {
    color-scheme: light;
    --brand-mark: var(--brand-blue);
    --canvas: #fff; --surface: #fff; --surface-2: #fff;
    --ink: #000; --ink-2: #222; --mute: #555;
    --line: #bbb; --line-strong: #999;
    --shadow: none; --shadow-lift: none; --specular: none;
  }
  body { background: #fff; color: #000; font-size: 11pt; }
  body::before { display: none; }

  /* 屏幕上的交互件在纸上没有意义 */
  .side, .topbar, .tabbar, .scrim, .fabwrap, .fab, .admbadge, .rail__act,
  .actbar, .menu, .searchbar, .filter, .ops, form,
  /* 流程图上那些加号是编辑用的，打出来的图上不该有 */
  .gplus, .gx {
    display: none !important;
  }
  /* 流程图印出来是有用的（贴在墙上说明这类单要过谁），所以让它换行铺开，
     而不是留一条横向滚动条 —— 纸没有滚动条。 */
  /* 纸上没有滚动条，那两端的滚动阴影也就没有意义 —— 打出来是两块脏灰。 */
  .graphwrap { overflow: visible !important; background: none !important; }
  .graph { flex-wrap: wrap; }
  /* 纸上没有滚动条：分流段必须换行铺开，否则超出页宽的那几关**直接消失**，
     而打出来的人不会知道自己少看了两关。屏幕上宁可滚不换行，纸上正好反过来。 */
  .gfork__body { overflow: visible !important; }
  .glane, .glane__track { flex-wrap: wrap !important; width: auto !important; }
  .glane__rail, .ghandle { display: none !important; }
  .glane__tag { position: static !important; box-shadow: none !important; }
  .printhead { display: flex !important; }

  main { margin: 0 !important; padding: 0 !important; }
  .page-wrap { display: block; max-width: none; }
  /* 🔴 **用户报的「自动生成的 PDF 基本都会多一页空白」就是这一条。**
     `.col` 在屏幕上是一块面板，写着 `min-height: calc(100dvh - --frame*2)`
     —— 那是为了「内容少的时候别缩成半屏」（用户 2026-09-08 那句
     「宽度限制住以后有大块空白区域」）。到了纸上，`100dvh` **就是一页**，
     于是这块面板一个人就吃掉整页，它上面的密级抬头一挤，
     剩下几十像素溢到第二页 —— 那一页看着是空的，其实是面板的下半截。

     判据不是「把 min-height 调小」，是**纸上根本没有「面板」这回事**：
     页边距已经是那个框了，底色、边、影、圆角、内边距全是屏幕上的affordance。
     实测：只去 min-height 仍然 2 页（内边距 22+28 顶出去），两样都去才 1 页。 */
  .col {
    max-width: none; min-height: 0;
    background: none; border: 0; box-shadow: none; border-radius: 0; padding: 0;
  }
  .rail { display: none; }

  /* 折叠区在纸上必须全展开，否则导出的 PDF 是残缺的 */
  details { display: block; }
  details > summary { display: none; }

  .card, .stat, li { break-inside: avoid; page-break-inside: avoid; }
  .cards { border-radius: 0; box-shadow: none; }
  .card { border: 0; border-bottom: 1px solid #bbb; padding: 8px 0; }
  .card.late { background: #fff; }
  .card.late::before { background: #000; }
  .detail, .sheet, .stat, .files { box-shadow: none; }

  /* 表格要能跨页，且每页重复表头 */
  .tablewrap { overflow: visible; }
  .ledger { min-width: 0; font-size: 9.5pt; }
  .ledger thead { display: table-header-group; position: static; }
  .ledger tr { break-inside: avoid; page-break-inside: avoid; }
  .ledger td .sub { max-width: none; }

  .sheet { border: 0; padding: 0; }
  .sh-head h1 { font-size: 17pt; }
  .sh-info, .sh-fields, .sh-flow { font-size: 10pt; }
  .sh-flow tr, .sh-fields tr { break-inside: avoid; page-break-inside: avoid; }
  .sh-flow thead { display: table-header-group; }

  a { text-decoration: none; color: #000; }
  h1.page, h2.sec { break-after: avoid; page-break-after: avoid; }
  .noprint { display: none !important; }
}

.printhead {
  display: none; justify-content: space-between; align-items: baseline;
  border-bottom: 1px solid #999; padding-bottom: 6px; margin-bottom: 10px; font-size: 10pt;
}
@media print {
  .printhead > span { display: flex; align-items: baseline; gap: 10px; }
  .printhead .lv {
    font-style: normal; font-weight: var(--w-bold); border: 1px solid #000; padding: 0 5px;
  }
}

/* ---------- 11 水印 ---------- */
/*
   只在打印/存为 PDF 时显形。屏幕上盖满水印只会挡住正在办事的人，
   而屏幕本来也拦不住截图 —— 水印防的是「文件被转发出去以后追不到人」，
   不是防看。

   print-color-adjust: exact 是必须的：不写这一行，浏览器会把这层浅灰
   当成「省墨可去掉的背景」直接丢掉，打出来一片干净。
*/
.watermark { display: none; }
@media print {
  /* ⚠️ `inset: -20%` 看着很像「多一页空白」的元凶，**实测不是**：
     `position: fixed` 的东西在打印里按页重复，超出的部分不参与分页。
     单层 / 双层裁剪两种写法各印一次，页数一模一样。
     真正的元凶是上面 `.col` 那条 —— 别再来改这里。 */
  .watermark {
    display: flex; flex-wrap: wrap; gap: 34px 46px;
    align-content: space-around; justify-content: space-around;
    position: fixed; inset: -20%; z-index: 9999;
    pointer-events: none; user-select: none;
    transform: rotate(-28deg);
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
  }
  /* 每块按文字自身宽度排，不写死列数：水印内容长短差得很远，
     固定列数时长的那种会溢出格子、和邻块糊成一片，看着像坏了。 */
  .watermark span {
    white-space: nowrap; font-size: 10.5pt;
    color: rgba(0, 0, 0, 0.10); font-weight: var(--w-semibold); letter-spacing: 0.06em;
  }
}

/* ---- 命令面板（第 7 条）----

   摆在离顶 12% 的地方，不居中：打开之后眼睛要落在输入框上，
   而屏幕正中偏低 —— 居中的对话框适合「读一句话再做决定」，
   这个面板适合「立刻开始打字」。

   入场动画由 .cfm 那一套的同款写法负责（`@starting-style` + allow-discrete），
   键盘打开时 `[data-kbd]` 把时长压成 0（第 6 条）。 */
.cmdk {
  border: 0; padding: 0; background: transparent;
  width: min(560px, calc(100vw - 32px));
  margin-top: 12vh; margin-inline: auto;
  opacity: 0; transform: scale(0.98) translateY(-6px);
  transition: opacity var(--t-2) var(--ease-out),
              transform var(--t-2) var(--ease-out),
              overlay var(--t-2) var(--ease-out) allow-discrete,
              display var(--t-2) var(--ease-out) allow-discrete;
}
.cmdk[open] { opacity: 1; transform: none; transition-duration: var(--t-3); }
@starting-style {
  .cmdk[open] { opacity: 0; transform: scale(0.98) translateY(-6px); }
}
.cmdk[data-kbd], .cmdk[data-kbd][open] { transition-duration: 0s; }
.cmdk::backdrop {
  background: rgba(18, 18, 40, 0);
  transition: background var(--t-2) var(--ease-out),
              overlay var(--t-2) var(--ease-out) allow-discrete,
              display var(--t-2) var(--ease-out) allow-discrete;
}
.cmdk[open]::backdrop { background: rgba(18, 18, 40, 0.42); }
@starting-style { .cmdk[open]::backdrop { background: rgba(18, 18, 40, 0); } }

.cmdk__box {
  background: var(--mat-thick);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  border: 1px solid var(--line); border-radius: var(--r-lg);
  box-shadow: var(--specular), var(--shadow-lift);
  overflow: hidden;
}
.cmdk__bar {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 14px; border-bottom: 1px solid var(--line);
}
.cmdk__bar .gl { color: var(--mute); flex: 0 0 auto; }
/* 这个框是个例外：它就是整块界面本身，不是表单里的一个字段，
   所以不套 --fw-m 的宽度上限（PITFALLS #99 说的是「控件宽度由内容定」，
   而这个控件的内容就是整行）。 */
.cmdk__in {
  flex: 1 1 auto; min-width: 0;
  border: 0; background: transparent; padding: 0;
  font-size: var(--fs-input); color: var(--ink);
}
.cmdk__in:focus { outline: none; }
.cmdk__in::-webkit-search-cancel-button { display: none; }
.cmdk__bar kbd, .side__kbd {
  font: inherit; font-size: var(--fs-cap); color: var(--mute);
  padding: 2px 7px; border-radius: var(--r-sm);
  border: 1px solid var(--line); background: var(--surface-2);
  white-space: nowrap;
}
.cmdk__list { list-style: none; margin: 0; padding: 6px; max-height: 52vh; overflow-y: auto; }
.cmdk__k {
  padding: 8px 10px 4px; font-size: var(--fs-cap); color: var(--mute);
}
.cmdk__r a {
  display: grid; gap: 1px; padding: 8px 10px;
  border-radius: var(--r-sm); text-decoration: none; color: var(--ink);
}
.cmdk__r b { font-size: var(--fs-sub); font-weight: var(--w-semibold); }
.cmdk__r small { font-size: var(--fs-cap); color: var(--mute); }
/* 选中态跟着键盘走，所以是类不是 :hover —— 鼠标划过会把 cur 同步过来。 */
.cmdk__r.on a { background: var(--tint-soft); color: var(--tint); }
.cmdk__r.on small { color: inherit; opacity: 0.78; }
.cmdk__hint {
  margin: 0; padding: 9px 14px; border-top: 1px solid var(--line);
  font-size: var(--fs-cap); color: var(--mute);
}

/* 侧栏里那个鼠标入口。长得和导航项一样，末尾挂一个快捷键提示。 */
.side__find { width: 100%; border: 0; background: transparent; cursor: pointer; text-align: left; }
.side__kbd { margin-left: auto; }
.side.mini .side__kbd { display: none; }

/* ---- 设置弹窗 ----

   和 .cfm / .cmdk 同一套开合写法（@starting-style + allow-discrete，
   退出比进入快一档）。三个都用同一份写法是刻意的：
   全站的「弹出来的东西」只有一种开合手感，人不用为第二种再学一遍。 */
/* <dialog> 的 UA 样式是 color: CanvasText。这个系统色只跟浏览器的 prefers-color-scheme 走，
   不跟 data-theme 走 —— 手动选「深」的人打开弹层，底是 --surface 的深色、字还是黑的（量到 1.18）。
   系统深色下量不到这条：那时 CanvasText 恰好是白的。所以弹层的字色明写。 */
dialog { color: var(--ink); }

.prefs {
  border: 0; padding: 0; background: transparent;
  width: min(440px, calc(100vw - 32px));
  opacity: 0; transform: scale(0.98) translateY(6px);
  transition: opacity var(--t-2) var(--ease-out),
              transform var(--t-2) var(--ease-out),
              overlay var(--t-2) var(--ease-out) allow-discrete,
              display var(--t-2) var(--ease-out) allow-discrete;
}
.prefs[open] { opacity: 1; transform: none; transition-duration: var(--t-3); }
@starting-style { .prefs[open] { opacity: 0; transform: scale(0.98) translateY(6px); } }
.prefs[data-kbd], .prefs[data-kbd][open] { transition-duration: 0s; }
.prefs::backdrop {
  background: rgba(18, 18, 40, 0);
  transition: background var(--t-2) var(--ease-out),
              overlay var(--t-2) var(--ease-out) allow-discrete,
              display var(--t-2) var(--ease-out) allow-discrete;
}
.prefs[open]::backdrop { background: rgba(18, 18, 40, 0.42); }
@starting-style { .prefs[open]::backdrop { background: rgba(18, 18, 40, 0); } }

/* 换头像那一屏要放 49 张图，440 装不下。**只改宽度，壳子不另做一个。** */
.prefs--wide { width: min(880px, calc(100vw - 32px)); }

/* ---- 头像弹窗 ----
   它是**服务端渲染**的（地址上 ?avatar=1），所以没有脚本时也是 open 的。
   非模态的 <dialog open> 默认 position:absolute 且拿不到 ::backdrop，
   这两条各补一句：固定居中，遮罩画在自己的 ::before 上。
   有脚本时 app.js 把它升级成真模态（Esc + 焦点圈定），那时 ::backdrop
   也会出现 —— 所以把它调成透明，免得两层遮罩叠成一片死黑。 */
.urldlg[open] {
  position: fixed; inset: 0; margin: auto; height: fit-content;
  max-height: calc(100vh - 32px); overflow: auto;
}
.urldlg[open]::before {
  content: ""; position: fixed; inset: 0; z-index: -1;
  background: rgba(18, 18, 40, 0.42);
}
.urldlg::backdrop { background: transparent; }

/* 「插一个分流」现在长在这个弹层里（原来是图下面的一节）。
   它自带的外框由 .prefs__box 提供，所以这里要把 .card 那份卸掉 ——
   两层框套在一起就是卡中卡。 */
.prefs__box.bpick { background: var(--surface); border: 0; }
.prefs__box.bpick > .hint { margin-top: 6px; }

/* ---- 名片：头像、姓名与任职、一个「更改头像」 ---- */
.mecard { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
/* 名片下面两块并排（我的信息 / 联系方式）：各只有几行，叠着放桌面上右边一大片空（用户 2026-09-12）。
   窄了自动落成一列（auto-fit + 20rem 下限），手机上和以前一样。 */
.me2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); gap: 12px; align-items: start; margin-top: 12px; }
.me2 > .dpanel { margin: 0; min-width: 0; }
/* 面板脚下那句说明：和上面的内容隔开一段，不再往面板底上加 16px 的空。 */
.dpanel > .hint:last-child { margin: 14px 0 0; }
.mecard__who { display: grid; gap: 2px; flex: 1 1 240px; min-width: 0; }
.mecard__who b { font-size: var(--fs-title); font-weight: var(--w-bold); }
.mecard__who span { font-size: var(--fs-sub); color: var(--ink-2); }

.prefs__box {
  background: var(--surface); border-radius: var(--r-lg);
  box-shadow: var(--shadow-lift); padding: 18px 20px 16px;
}
.prefs__h { display: flex; align-items: center; gap: 12px; }
.prefs__h h2 { margin: 0; font-size: var(--fs-title); flex: 1 1 auto; }
/* 关闭那个箭头是「收起」的方向，转 90 度朝下 —— 用 i-chev 省一个图标。 */
.prefs__h .iconbtn .gl { transform: rotate(90deg); }
.prefs__sec { margin-top: 16px; }
.prefs__sec h3 {
  margin: 0 0 8px; font-size: var(--fs-cap); font-weight: var(--w-semibold); color: var(--mute);
}
/* 三挡竖排、整行可点。**不用 select** —— 三个选项里每一个都要配一句
   「它到底会怎样」，而下拉框里放不下第二行字（PITFALLS #93 那条的延伸：
   选项之间的差别要在按下之前就看得见）。 */
.prefs__opts { display: grid; gap: 6px; }
.prefs__opt {
  display: grid; gap: 1px; text-align: left; cursor: pointer;
  padding: 10px 12px; border-radius: var(--r-sm);
  border: 1px solid var(--line); background: var(--surface);
  color: var(--ink);
  transition: border-color var(--t-2) var(--ease-out),
              background var(--t-2) var(--ease-out);
}
.prefs__opt b { font-size: var(--fs-sub); font-weight: var(--w-semibold); }
.prefs__opt small { font-size: var(--fs-cap); color: var(--mute); }
@media (hover: hover) and (pointer: fine) {
  .prefs__opt:hover { border-color: var(--line-strong); background: var(--surface-2); }
}
.prefs__opt:focus-visible { border-color: var(--tint); }
/* 选中态：一圈色环 + 淡底。不画勾 —— 三行里只有一行是亮的，
   勾是多余的第二个信号。 */
.prefs__opt.on {
  border-color: var(--tint); background: var(--tint-soft); color: var(--tint);
}
.prefs__opt.on small { color: inherit; opacity: 0.8; }
.prefs__foot {
  margin: 16px 0 0; padding-top: 12px; border-top: 1px solid var(--line);
  font-size: var(--fs-cap); color: var(--mute); line-height: 1.6;
}

/* ---- 主题切换：从点中的那一挡圆形揭开（第 5 条）----

   频率法则里这件事是「极少」档 —— 一天最多切一两次，所以**允许它有戏**，
   360ms 也不嫌长。它同时是这套动效里唯一一处用 --ease-inout 做全屏动作的：
   圆从按钮长到盖满屏幕，两头都要收，起步就冲会显得像误触。

   做法是 View Transition + clip-path：新的一版**盖在**旧的上面被揭开，
   所以旧配色不会先闪一下白。圆心是**设置弹窗里被点中的那一挡**
   （不是左下角那个入口）—— 人的视线和手指都在那一行上，
   从那儿长出来才对得上「是我点的这一下让它变的」。
   坐标由 app.js 在点下去那一刻写进 --tx / --ty，
   半径取到屏幕最远角（取近的那个角，屏幕角落会留一块没揭开）。

   不支持 View Transition 的浏览器：JS 里直接换 data-theme，
   靠下面那条 200ms 的颜色过渡收尾 —— 少一个揭开的动作，不少一个功能。 */
html.theming, html.theming body { transition: none; }
/* 🔴 换主题的那一下，**把命了名的那几块还给 root 快照**。
   用户：「明暗切换动画不错 但侧栏应该在动画圈碰到侧栏后再变色。」
   他看到的是：圆还在右下角，左边侧栏已经变了。

   原因是 `view-transition-name: vt-side` 把侧栏**从 root 快照里摘了出去** ——
   那个圆是画在 `::view-transition-new(root)` 上的，摘出去的元素它盖不到，
   侧栏走的是自己那条 160ms 的交叉淡入，所以先变。

   命名是为了**换页**时让侧栏留在原地（那时它确实不该动）。
   换主题不是换页：整屏都在变，没有「留在原地」这回事。
   所以这几个名字只在换页时成立，换主题时收回。 */
html.theming .side,
html.theming .topbar,
html.theming .tabbar { view-transition-name: none; }
::view-transition-old(root), ::view-transition-new(root) { mix-blend-mode: normal; }
html.theming::view-transition-old(root) { animation: none; z-index: 1; }
html.theming::view-transition-new(root) {
  animation: theme-wipe var(--t-reveal) var(--ease-inout) both; z-index: 2;
}
@keyframes theme-wipe {
  from { clip-path: circle(0 at var(--tx, 50%) var(--ty, 0%)); }
  to { clip-path: circle(var(--tr, 150vmax) at var(--tx, 50%) var(--ty, 0%)); }
}

/* ---------- 12 动效纪律 ---------- */

/* ---- 换页：跨文档 View Transitions（第 2 条）----

   这是和 SPA 差距最大的一处：服务端渲染的每次导航都会白一下，
   而那一下白屏和「加载慢」是同一种观感，哪怕局域网上渲染只要 10ms。

   `navigation: auto` 一行就让同源导航变成一次过渡。**不动的部分各自命名**，
   于是换页时侧栏、顶栏、底部标签栏**留在原地**，只有内容换 ——
   这正是 SPA「路由切换」给人的感觉，而这里不需要路由、不需要状态管理、
   也不需要 nprogress 那根进度条（那根条是 SPA 为了掩盖异步取数才需要的）。

   为什么内容层敢动：`ui-direction-20260904` 定的是「玻璃只给控制层，
   内容层实底不飘」。换页是内容层唯一允许的动 —— 因为这时候整页内容都换了，
   动的不是某一块内容，是「这一页」本身。

   降级：Firefox 不支持跨文档过渡，表现是「没有过渡」，不是「坏」。
   所有动效都按这个口径写（和 app.js 那条既有原则一致）。 */
@view-transition { navigation: auto; }

/* 命名 = 从 root 快照里摘出来，自己单独过渡。
   名字必须全页唯一，所以只给这几处**每页有且只有一个**的元素。 */
.side { view-transition-name: vt-side; }
.topbar { view-transition-name: vt-topbar; }
.tabbar { view-transition-name: vt-tabbar; }
/* ---- 标题不再 morph（2026-09-08 用户推翻）----
   原来这里是 `h1.page { view-transition-name: vt-title; }`，
   让页标题在两页之间平移过去。用户原话：
   「提醒页 提醒两字的动画过于夸张，**全站不要出现这个动画**」。

   他是对的，而且原因很具体：命了名的元素会被浏览器做**几何插值** ——
   两个标题字数不同就宽度不同，于是快照被拉伸 / 压扁。
   「待办」→「提醒」这种两个字对两个字的还好，
   一碰上「组织架构图」那种长标题，字会胖一圈再缩回去。

   **平移只在两个标题是同一个东西时才成立**（列表行 → 单据页标题）。
   不同页的标题不是同一个东西，把它们强行配对，
   动画就在说一句不存在的关系。

   换成什么：标题**跟着整页一起**淡入上移（root 那两条已经在做了）。
   有变化的感觉，没有几何形变。 */

::view-transition-old(root) { animation: vt-out var(--t-2) var(--ease-out) both; }
::view-transition-new(root) { animation: vt-in var(--t-3) var(--ease-out) both; }
@keyframes vt-out { to { opacity: 0; transform: translateY(-4px); } }
@keyframes vt-in { from { opacity: 0; transform: translateY(6px); } }

/* 侧栏和顶栏只交叉淡入（当前项变了的话正好看得见），不做位移。 */
::view-transition-old(vt-side), ::view-transition-new(vt-side),
::view-transition-old(vt-topbar), ::view-transition-new(vt-topbar),
::view-transition-old(vt-tabbar), ::view-transition-new(vt-tabbar) {
  animation-duration: var(--t-2);
}

/* 大页面只淡不滑：View Transition 要给两页各截一张快照，页面越大越贵；
   流程图那种横向铺开的页面再叠一层位移，低端机上会掉帧。
   判据用 :has 直接问「这一页上有没有那张图」，不用在 Go 里传标记。 */
html:has(.graphwrap) ::view-transition-new(root) { animation-name: vt-fade; }
html:has(.graphwrap) ::view-transition-old(root) { animation-name: vt-fadeout; }
@keyframes vt-fade { from { opacity: 0; } }
@keyframes vt-fadeout { to { opacity: 0; } }

/* 列表行 → 单据页标题的平移放大：**同时取消了**。

   它在道理上比上面那个站得住 —— 列表行和单据标题**确实**是同一个东西，
   而且它是“跨文档共享元素”这件事在 MPA 里比 SPA 好做的演示。
   但它是同一种视觉：小字被拉成大字，而且尺寸差得更多、形变更明显。
   用户说的是**全站**不要，只改他正好看到的那一页，
   下次在单据页上会再撞一次。

   app.js 里在 `pageswap` 那一刻给被点中的行打名字的那段，也跟着拿掉了。
   要是哪天想加回来，先想清楚：尺寸差得越多，形变越夸张。 */


/* ---- 触屏：靠 hover 才出现的东西，改成一直在 ----
   全站 45 条 hover 规则都包进了 `@media (hover: hover) and (pointer: fine)`，
   为的是触屏上不再出现「粘住的 hover」—— 点过一张卡之后它一直亮着，
   直到你去点别的地方。那不是状态，是残留。

   代价是**靠 hover 才出现的手柄在触屏上永远不出现**，所以这里逐条补回来。
   判据很简单：那个东西是「装饰」还是「唯一的入口」。
   × 是删除节点的唯一入口，补；加号淡着也点得到，但淡着会被当成不可用，补。

   用 (hover: none) 而不是窄屏宽度：触摸屏的笔记本一样没有 hover，
   而它的屏幕不窄。今天流程图那个 × 就是按宽度写的，
   换成宽屏触摸设备照样点不到。 */
@media (hover: none) {
  .gx button { opacity: 1; transform: none; }
}

/* ---------- 12 动效开关：全站唯一一处 ---------- */

/* 关掉的是**位移和缩放**，留下的是透明度和颜色 —— 这是第 11 条的原话，
   也是它和「一刀切 transition: none」的区别。

   会晕动症的人受不了的是东西在屏幕上**移动**；淡入淡出和变色不但无害，
   还是「这里刚刚变了」的唯一提示。全部关掉的话，展开一段折叠内容
   会变成整块内容凭空出现，反而更难跟上。

   所以这里覆盖的是 transition-property（只留下不产生位移的那几个），
   而不是 transition-duration —— 后者会把颜色过渡也一起压没。

   animation 只能整条压掉（一条关键帧里位移和透明度是绑在一起的），
   压成 0.01ms 而不是 none：终态照常到达，进场的元素仍然是可见的。
   animation-delay 必须一起归零 —— 时长归零而延迟留着的话，
   错开进场的最后一行会凭空晚 150ms 才出现，
   在关了动效的人眼里那就是「页面卡了一下」。

   这一处原来散在三个地方（全局一处、流程图一处、选人面板一处），
   于是「关掉动效之后到底还有什么会动」没有任何一个地方答得全。 */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-property:
      opacity, color, background-color, border-color, fill, stroke, box-shadow !important;
    transition-duration: var(--t-2) !important;
    animation-duration: 0.01ms !important;
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
  /* 跨文档换页过渡整体关掉：它本身就是一次全屏的位移 / 淡变。 */
  @view-transition { navigation: none; }
  /* 通讯录侧栏收起的那一下也算位移。 */
  .dir, .dir__side { transition: none; }
  .chat__tools .chat__tool-list { transition: none !important; transform: none !important; }
  .chatviewer__stage img { will-change: auto; }
  .assistant__aurora { display: none !important; }
  .assistant[data-ai-light-renderer="canvas"] .assistant__aurora { display: block !important; }
  .assistant__turn.is-generating .assistant__thinking { color: var(--mute) !important; background: none !important; -webkit-text-fill-color: currentColor !important; animation: none !important; }
}

/* ---- 发起页：这张单会经过谁（015 第二版）--------------------
   ⚠️ 这一段是【Go/模板那边的 agent 加的结构样式】，只管排布、命中区和状态，
   材质与配色全走令牌，没有一次性色值 —— 视觉归 UI 工程师，改这里不用问我。

   上一版是勾选框 + 三个下拉框，用户原话「非常难用」。这一版按他要的
   「加技能点那种感觉」重做：链本身就是界面，一次点击一次改动，
   状态靠形状读 —— 去不掉的关不画 ×、到上限不画 +，所以没有错误提示要读。

   链复用管理区流程图的 .graph/.gcard 语汇，不另造一套。 */
.planwrap { margin-bottom: 20px; }
.planwrap[aria-busy="true"] { opacity: 0.55; transition: opacity var(--t-2) var(--ease-out); }

/* 【竖排，桌面手机同一套。】
   管理区那张流程图是横排的（用户当初指定的），那是「坐下来配一条要用很久的
   流程」的界面。发起页不是：这条链最多 8 个节点，**横排在任何屏幕上都装不下 8 个**
   —— 实测 1440 桌面上 3 个节点就已经要横向滚动了（内容 823 / 容器 814）。
   横着滚动去看「我的单要经过谁」，是「丝滑」的反面。

   竖排还顺带解决三件事：每行整宽，姓名和岗位一行放得下（横排时警告文案
   要折三行）；顺序就是审批顺序，读法和事情发生的顺序一致；
   加号落在两行中间的空档里，「插在这儿」不用解释。 */
.graph.plan {
  display: flex; flex-direction: column; align-items: stretch;
  gap: 0; padding: 0; margin: 0; list-style: none; min-width: 0;
}
.planwrap .graphwrap { overflow: visible; }
.graph.plan > li { display: flex; align-items: center; min-width: 0; width: auto; }
.graph.plan > li.gend { gap: 9px; font-size: var(--fs-cap); color: var(--mute); }

/* 竖线把整条链串起来，和加号共用一条轴。
   基础 .glink 是给管理区那张【横排】图写的：width:44px、连接线是一条横线。
   竖排这里两条都要推掉 —— 尤其 width:44px，**显式宽度会让
   align-items:stretch 失效**，整行只有 44px 宽（踩过一次）。 */
.graph.plan > li.glink {
  display: flex; width: auto; align-self: stretch;
  justify-content: flex-start; padding-left: 11px;
  height: 26px; position: relative; flex: 0 0 auto;
}
.graph.plan > li.glink::before {
  content: ""; position: absolute;
  left: 11px; right: auto; top: 0; bottom: 0; margin-top: 0;
  width: 0; height: auto; background: none;
  border-left: 2px dotted var(--line-strong); border-radius: 0;
}

.plan .gcard.plan__c {
  position: relative; flex: 1; min-width: 0;
  display: grid; grid-template-columns: 1fr auto auto;
  align-items: center; gap: 2px 10px;
  padding: 10px 12px; text-align: left; cursor: default;
  border-radius: var(--r-md);
}
.plan .gcard__t {
  grid-column: 1; grid-row: 1;
  font-size: var(--fs-sub); font-weight: var(--w-semibold);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.plan .gcard__s {
  grid-column: 1; grid-row: 2;
  font-size: var(--fs-cap); color: var(--mute);
  overflow: hidden; text-overflow: ellipsis;
}
.plan__c.mine { border-color: var(--tint); box-shadow: 0 0 0 1px var(--tint) inset; }
.plan__c.bad  { background: var(--late-bg); border-color: transparent; }
.plan__c.bad .gcard__s { color: color-mix(in srgb, var(--late) 74%, var(--ink)); }

/* 「审批 / 知会」：自己加的能点着切，模板给的是静态标签。
   一个词就是全部说明，不配解释文字。 */
.plan__k {
  grid-column: 2; grid-row: 1 / span 2; align-self: center;
  font-size: var(--fs-cap); font-weight: var(--w-semibold);
  color: var(--tint); background: var(--tint-soft);
  border: 0; border-radius: var(--r-pill); padding: 4px 10px;
  cursor: pointer; font-family: inherit; white-space: nowrap;
}
.plan__k.mute { color: var(--mute); background: var(--surface-2); cursor: default; }

/* × 在行尾。命中区靠伪元素撑到 --tap，视觉尺寸不变（PITFALLS #44）。 */
.plan__x {
  grid-column: 3; grid-row: 1 / span 2; align-self: center;
  position: relative; width: 24px; height: 24px; line-height: 22px;
  text-align: center; border: 0; border-radius: 50%; background: transparent;
  color: var(--mute); font-size: var(--fs-head); cursor: pointer; font-family: inherit;
}
.plan__x::after {
  content: ""; position: absolute; top: 50%; left: 50%;
  width: var(--tap); height: var(--tap); transform: translate(-50%, -50%);
}
@media (hover: hover) and (pointer: fine) { .plan__x:hover { background: var(--bad-bg); color: var(--bad); } }
.plan__x:focus-visible { background: var(--bad-bg); color: var(--bad); }

/* 加号坐在两行之间那条竖线上 */
.gadd__b {
  position: relative; z-index: 1;
  width: 24px; height: 24px; padding: 0; line-height: 1;
  border: 1px dashed var(--line-strong); border-radius: 50%;
  background: var(--canvas); color: var(--mute);
  font-size: var(--fs-body); cursor: pointer; font-family: inherit;
  transition: transform var(--t-2) var(--ease-out),
    border-color var(--t-2) var(--ease-out), color var(--t-2) var(--ease-out);
}
.gadd__b::after {
  content: ""; position: absolute; top: 50%; left: 50%;
  width: var(--tap); height: var(--tap); transform: translate(-50%, -50%);
}
@media (hover: hover) and (pointer: fine) {
  .gadd__b:hover { border-style: solid; border-color: var(--tint); color: var(--tint);
    transform: scale(1.15); }
}
.gadd__b:focus-visible { border-style: solid; border-color: var(--tint); color: var(--tint);
    transform: scale(1.15); }

/* 选人：一屏能扫完的名字网格 + 一个搜索框。
   点一个名字就插进链里 —— 没有第二个下拉框，没有确认。 */
/* 面板开在空档里：这一格从「一条细缝」变成整块 */
.graph.plan > li.glink.picking {
  display: block; height: auto; width: auto; padding: 6px 0 6px 11px;
}
.graph.plan > li.glink.picking::before { border-left-style: solid; }
.picker {
  margin: 0; padding: 10px; border-radius: var(--r-md);
  background: var(--surface-2); border: 1px solid var(--line);
  animation: pickin var(--t-2) var(--ease-out) both;
}
@keyframes pickin { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
.picker__q { width: 100%; margin-bottom: 8px; }
.picker__g {
  display: flex; flex-wrap: wrap; gap: 6px;
  max-height: 232px; overflow-y: auto;
}
.pick {
  border: 1px solid var(--line); border-radius: var(--r-pill);
  background: var(--surface); color: var(--ink);
  padding: 0 12px; min-height: var(--tap);
  font-size: var(--fs-sub); font-family: inherit; cursor: pointer;
  display: inline-flex; align-items: center; gap: 5px;
  transition: background var(--t-2) var(--ease-out), border-color var(--t-2) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) { .pick:hover { background: var(--tint-soft); border-color: var(--tint); color: var(--tint); } }
.pick:focus-visible { background: var(--tint-soft); border-color: var(--tint); color: var(--tint); }
.pick em { font-style: normal; font-size: var(--fs-cap); color: var(--mute); }
.picker__x {
  margin-top: 8px; border: 0; background: none; padding: 0;
  color: var(--mute); font-size: var(--fs-foot); font-family: inherit; cursor: pointer;
  min-height: var(--tap);
}

.planfoot {
  display: flex; align-items: center; gap: 10px;
  margin: 8px 0 0; font-size: var(--fs-foot); color: var(--tint);
}
.linky {
  border: 0; background: none; padding: 0; font-family: inherit;
  font-size: inherit; color: var(--tint); text-decoration: underline;
  cursor: pointer; min-height: var(--tap);
}


/* ==================================================================
   016 三角色框架（Go/模板那边的 agent 追加；视觉稿仍以 UI 工程师为准）
   —— 发起页的「发起人自选」空位、单据页的加签、管理区菜单里的按钮项
   ================================================================== */
/* 空位：虚线卡，整张就是一个按钮。长得像要点的样子，就不用写「请选人」 */
.plan__c.slot { border: 1.5px dashed var(--tint); background: transparent; box-shadow: none; padding: 0; }
.plan__c.slot.bad { border-color: var(--late); }
.plan__pick {
  grid-column: 1 / -1; width: 100%; min-height: var(--tap);
  border: 0; background: transparent; color: var(--tint);
  font: inherit; font-weight: var(--w-semibold); font-size: var(--fs-callout);
  padding: 8px 12px; text-align: left; cursor: pointer;
  display: flex; flex-direction: column; gap: 2px; border-radius: inherit;
}
.plan__pick em { font-style: normal; font-weight: var(--w-regular); font-size: var(--fs-cap); color: var(--mute); }
@media (hover: hover) and (pointer: fine) { .plan__pick:hover { background: var(--tint-soft); } }
.plan__pick:focus-visible { background: var(--tint-soft); }
.plan__c.picked { border-color: var(--tint); }
/* 已选的空位：「换人」挨着审批/知会那个标签 */
.plan__k.alt { grid-row: 2; align-self: start; margin-top: 4px; background: var(--surface-2); color: var(--ink); }
.plan__c { grid-template-rows: auto auto; }

/* 管理区菜单：发起人自选是个 POST，所以是 button 不是 a —— 长得必须一样 */
button.nodeitem { width: 100%; border: 0; font: inherit; text-align: left; cursor: pointer; }
button.nodeitem.on { background: linear-gradient(var(--tint-soft), var(--tint-soft)) var(--surface); }
button.nodeitem.on b { color: var(--tint); }

/* 单据页右栏的加签：收在 details 里，打开就是那块选人面板 */
.addsign { margin-top: 10px; }
.addsign > summary {
  list-style: none; cursor: pointer; min-height: var(--tap);
  display: flex; align-items: center; gap: 8px; padding: 0 4px;
  font-weight: var(--w-semibold); color: var(--tint);
}
.addsign > summary::-webkit-details-marker { display: none; }
.addsign > summary em { font-style: normal; font-weight: var(--w-regular); font-size: var(--fs-cap); color: var(--mute); }
.addsign > summary::after { content: "▾"; margin-left: auto; color: var(--mute); transition: transform var(--t-3) var(--ease-out); }
.addsign[open] > summary::after { transform: rotate(180deg); }
.addsign .picker { margin-top: 6px; }
.addsign .picker__g { max-height: 40vh; overflow: auto; }

/* 「相对角色」里那条少见用法（跳到第 N 级）：收起来，但不藏死 */
.nodeadv { margin-top: 8px; border-top: 1px solid var(--line); padding-top: 8px; }
.nodeadv > summary {
  list-style: none; cursor: pointer; min-height: var(--tap);
  display: flex; align-items: center;
  font-size: var(--fs-cap); color: var(--mute);
}
.nodeadv > summary::-webkit-details-marker { display: none; }
.nodeadv > summary::after { content: "▾"; margin-left: auto; transition: transform var(--t-3) var(--ease-out); }
.nodeadv[open] > summary::after { transform: rotate(180deg); }

/* 016 打磨：两个 pill 竖着排，别叠在一起（量出来重叠 12px） */
.plan__ks {
  grid-column: 2; grid-row: 1 / span 2; align-self: center;
  display: flex; flex-direction: column; align-items: flex-end; gap: 3px;
}
.plan__k.alt { grid-row: auto; margin-top: 0; background: var(--surface-2); color: var(--ink); }

/* 空位选人时，面板要占满这一行 —— 原先它和空位卡并排，把卡挤成 26px 宽 */
.graph.plan > li.picking { flex-direction: column; align-items: stretch; gap: 6px; }
.graph.plan > li.picking .plan__c.slot { display: none; }
.graph.plan > li.picking .picker { width: 100%; }

/* 编辑器右栏：自选节点的可选范围 */
.nodesub--range { padding: 10px 12px; }
.rangelist { list-style: none; margin: 0 0 6px; padding: 0; display: grid; gap: 4px; }
.rangelist li {
  display: flex; align-items: center; gap: 6px; min-height: 32px;
  padding: 4px 4px 4px 10px; border-radius: var(--r-pill);
  background: var(--surface-2); font-size: var(--fs-cap);
}
.rangelist li > span { flex: 1 1 auto; min-width: 0; }
.rangelist form { margin-left: auto; }
.rangelist button {
  position: relative; width: 24px; height: 24px; line-height: 22px;
  border: 0; border-radius: 50%; background: transparent;
  color: var(--mute); font-size: var(--fs-body); cursor: pointer; font-family: inherit;
}
.rangelist button::after {
  content: ""; position: absolute; left: 50%; top: 50%;
  width: var(--tap); height: var(--tap); transform: translate(-50%, -50%);
}
@media (hover: hover) and (pointer: fine) { .rangelist button:hover { background: var(--bad-bg); color: var(--bad); } }
.rangelist button:focus-visible { background: var(--bad-bg); color: var(--bad); }

/* 单据页加签：那个「只让他知道」的勾 */
.signkind {
  display: flex; align-items: center; gap: 8px; min-height: var(--tap);
  padding: 0 2px; font-size: var(--fs-cap); color: var(--mute); cursor: pointer;
}
.signkind input { width: 18px; height: 18px; accent-color: var(--tint); }

/* ==================================================================
   表单编辑器（018）—— 左边画的是员工会看到的那张表，不是字段清单
   ================================================================== */
.fpreview { display: grid; gap: 10px; max-width: 640px; margin: 0 0 8px; }
.fprow { border: 1px solid var(--line); border-radius: var(--r-md); background: var(--surface); }
@media (hover: hover) and (pointer: fine) { .fpreview.editing .fprow:hover { border-color: var(--tint); } }
.fprow.on { border-color: var(--tint); box-shadow: 0 0 0 1px var(--tint) inset; }
.fprow__pick {
  display: block; padding: 12px 14px; text-decoration: none; color: inherit;
  border-radius: inherit; min-height: var(--tap);
}
.fprow > .fprow__lab:first-child { display: block; padding: 12px 14px 0; }
.fprow > .fprow__box { margin: 0 14px 12px; }
.fprow__lab {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  font-weight: var(--w-semibold); font-size: var(--fs-callout); margin-bottom: 6px;
}
.fprow__lab i { font-style: normal; font-size: var(--fs-cap); font-weight: var(--w-regular); }
.fprow__lab .req { color: var(--bad); }
.fprow__lab .opt, .fprow__lab .ty { color: var(--mute); }
.fprow__lab .ty {
  background: var(--surface-2); border-radius: var(--r-pill); padding: 1px 8px;
}
.fprow__lab .ti { color: var(--tint); background: var(--tint-soft);
  border-radius: var(--r-pill); padding: 1px 8px; }
.fprow__box {
  display: block; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface-2); min-height: 34px; padding: 7px 10px;
  color: var(--mute); font-size: var(--fs-sub);
}
.fprow__box.area { min-height: 68px; }
.fprow__box.check { border: 0; background: none; padding: 0; font-size: var(--fs-title); }
.fprow__help { display: block; margin-top: 6px; font-size: var(--fs-cap); color: var(--mute); }
.pair { display: flex; gap: 8px; }
.pair input { flex: 1 1 0; min-width: 0; }
.checkline {
  display: flex; align-items: center; gap: 8px; min-height: var(--tap);
  cursor: pointer; font-weight: var(--w-medium);
}
.checkline input { width: 18px; height: 18px; accent-color: var(--tint); }

/* 预览里两行之间的加号：和 /admin/flows 的加号同一个意思 */
/* ---- 拖拽（第 6 项）----
   两种手势共用这一套提示：一条落在行上边或下边的实线。
   和流程图那张卡是同一个做法（那边是左右，这边是上下）——
   两个编辑器的手感不该是两种。 */
.fpreview.editing .fprow { position: relative; cursor: grab; }
.fpreview.editing .fprow:active { cursor: grabbing; }
.fpreview.editing .fprow.dragging { opacity: 0.35; }
.fpreview.editing .fprow::before {
  content: ""; position: absolute; left: 0; right: 0; top: -7px; height: 3px;
  border-radius: 2px; background: var(--tint); opacity: 0; pointer-events: none;
}
.fpreview.editing .fprow.dropT::before, .fpreview.editing .fprow.dropB::before { opacity: 1; }
.fpreview.editing .fprow.dropB::before { top: auto; bottom: -7px; }
/* 把手只是**告诉人这一行能拖**，不是唯一的抓取点（整行都能抓）。
   放在右上角、绝对定位：预览要长得像真表单，多一列会把它推变形。 */
.fphandle {
  position: absolute; top: 6px; right: 8px;
  font-size: var(--fs-cap); line-height: 1; letter-spacing: -2px;
  color: var(--mute); opacity: 0.45; pointer-events: none;
}
.nodeitem[draggable="true"] { cursor: grab; }
.nodeitem.dragging { opacity: 0.45; }

.fpadd { display: flex; justify-content: center; margin: -4px 0; }
.fpadd button {
  position: relative; width: 26px; height: 26px; line-height: 24px;
  border: 1px dashed var(--line-strong); border-radius: 50%;
  background: var(--surface); color: var(--mute);
  font-size: var(--fs-sub); font-family: inherit; cursor: pointer;
  transition: color var(--t-2) var(--ease-out), border-color var(--t-2) var(--ease-out);
}
.fpadd button::after {
  content: ""; position: absolute; left: 50%; top: 50%;
  width: var(--tap); height: var(--tap); transform: translate(-50%, -50%);
}
@media (hover: hover) and (pointer: fine) { .fpadd button:hover { color: var(--tint); border-color: var(--tint); } }
.fpadd button:focus-visible { color: var(--tint); border-color: var(--tint); }

/* ==================================================================
   管理清单页「选中的那一行」（020）—— 人员/任职/印章共用
   左边选一行、右边改它。和 .fprow.on 保持同一种选中语言，
   免得同一套系统里两个编辑器长得不一样。
   ================================================================== */
.card.on { border-color: var(--tint); box-shadow: 0 0 0 1px var(--tint) inset; }

/* ==================================================================
   2026-09-07 发起页重排 + 「审批」入口
   用户：「还是没有利用全宽」「这个修改流程用起来真的非常难用」。
   表单占内容列（字段两列网格）、链进右栏；加号从虚线小圆变成写着字的按钮。
   ================================================================== */
/* 两列字段网格：短字段并排、长字段整行。手机和平板仍是一列。
   .form 本身的 640 / 720 上限对它不生效 —— 它旁边就是右栏，列宽本来就是算好的。
   行距靠 row-gap，不靠 label 的上边距：两列并排时两边的 label 才对得齐。 */
.form.grid { max-width: none; row-gap: 18px; }
.form.grid > .field { min-width: 0; }
.form.grid > .field > label:first-child { margin-top: 0; }
.form.grid > button.primary { margin-top: 6px; }
@media (min-width: 1025px) {
  .form.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 24px; align-items: start; }
  .form.grid > .field.wide, .form.grid > button.primary, .form.grid > .split,
  .form.grid > .fe, .form.grid > .err, .form.grid > .ok { grid-column: 1 / -1; }
}

/* 链在右栏里。右栏是 sticky 的，链加上打开的选人面板可能比一屏高：给它自己的滚动。 */
.rail__plan .planwrap { margin: 0; }
.rail:has(.planwrap) { max-height: calc(100vh - 48px); overflow-y: auto; overscroll-behavior: contain; }
@media (max-width: 1024px) {
  .rail:has(.planwrap) { max-height: none; overflow: visible; }
}

/* 「＋ 加人」：从一个 24px 的虚线小圆变成一个写着字的按钮 ——
   原先暗色下几乎看不见，也没人看得出它能点。 */
.gadd__b {
  width: auto; height: 30px; padding: 0 12px 0 9px; line-height: 28px;
  border-radius: var(--r-pill); border: 1px dashed var(--tint);
  background: var(--surface); color: var(--tint);
  font-size: var(--fs-cap); font-weight: var(--w-semibold);
  display: inline-flex; align-items: center; gap: 4px;
}
@media (hover: hover) and (pointer: fine) { .gadd__b:hover { border-style: solid; background: var(--tint-soft); transform: none; } }
.gadd__b:focus-visible { border-style: solid; background: var(--tint-soft); transform: none; }
.graph.plan > li.glink { height: 40px; }
.graph.plan > li.glink.picking { height: auto; }

/* 审批 / 知会：两格开关，亮着的是现在的样子，点另一格就切过去。
   原先是一个长得像标签的 pill，没人看得出它能点。 */
.plan__seg {
  display: inline-flex; border: 1px solid var(--line-strong);
  border-radius: var(--r-pill); overflow: hidden;
}
.plan__seg > * {
  font: inherit; font-family: inherit; font-size: var(--fs-cap); font-weight: var(--w-semibold); line-height: 1.4;
  padding: 3px 9px; border: 0; background: transparent; color: var(--mute); cursor: pointer;
}
.plan__seg > .on { background: var(--tint); color: var(--on-tint); cursor: default; }
@media (hover: hover) and (pointer: fine) { .plan__seg > button:hover { background: var(--tint-soft); color: var(--tint); } }
.plan__seg > button:focus-visible { background: var(--tint-soft); color: var(--tint); }

/* × 常显、有边，不靠悬停才看得见 */
.plan__x { width: 26px; height: 26px; line-height: 24px; border: 1px solid var(--line-strong); color: var(--mute); }
.planhint { margin: 8px 0 0; font-size: var(--fs-cap); color: var(--mute); line-height: 1.5; }

/* 「审批」入口页 */
.launch .ic { width: 22px; height: 22px; flex: none; color: var(--tint); fill: none; stroke: currentColor; stroke-width: 1.7; }

/* ==================================================================
   附件：站内预览、打包下载、选文件累加（2026-09-07）
   用户：「尽量能站内展示就站内展示而不是直接下载」。
   下载出去的那一份从此不受这套系统管 —— 能在页面里看完的就别落到磁盘上。
   ================================================================== */

/* ---- 单据页的附件行 ---- */
.file { display: grid; grid-template-columns: auto 1fr auto; gap: 4px 12px; align-items: center; }
.file__pick { display: flex; align-items: center; justify-content: center; min-height: var(--tap); margin: 0; }
.file__pick input { width: 18px; height: 18px; accent-color: var(--tint); }
.file__main { min-width: 0; }
.file__main .row1 { gap: 8px; }
.file__main .row2 { margin-top: 4px; }
.file__act { display: flex; gap: 8px; flex: none; }
/* 展开的那一块占满整行 —— 它是这一行的内容，不是第四列 */
.file__pv { grid-column: 1 / -1; margin-top: 10px; }

.filebar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
  padding: 12px 16px; border: 1px solid var(--line); border-top: 0;
  border-radius: 0 0 var(--r-md) var(--r-md); background: var(--surface-2);
}
.filebar .hint { margin: 0; }
.filebox .cards { border-radius: var(--r-md) var(--r-md) 0 0; margin-bottom: 0; }
.filebar button[disabled] { opacity: 0.5; cursor: not-allowed; }

@media (max-width: 560px) {
  .file { grid-template-columns: auto 1fr; }
  .file__act { grid-column: 1 / -1; justify-content: flex-end; }
}

/* ---- 预览块本体 ---- */
.filepv {
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface-2); padding: 12px; overflow: hidden;
}
.pvimg { display: block; max-width: 100%; max-height: 62vh; margin: 0 auto; object-fit: contain; }
.pvpdf { display: block; width: 100%; height: 70vh; border: 0; background: var(--surface); }
.pvnote { margin: 8px 0 0; font-size: var(--fs-cap); color: var(--mute); line-height: 1.6; }
.pvwarn {
  margin: 0 0 10px; padding: 8px 12px; border-radius: var(--r-sm);
  background: var(--late-bg); color: var(--late);
  font-size: var(--fs-cap); line-height: 1.6;
}
.pvbar { display: flex; gap: 8px; justify-content: flex-end; margin-bottom: 8px; }

/* Word / Excel / 文本转出来的内容。它是**这一页的一部分**，
   所以打印时水印第一次盖得到附件里的东西 —— 图片和 PDF 仍然盖不到。 */
.pvdoc { max-height: 62vh; overflow: auto; background: var(--surface); border-radius: var(--r-sm); padding: 14px 16px; }
.pvdoc .pvp { margin: 0 0 8px; font-size: var(--fs-sub); line-height: 1.75; white-space: pre-wrap; overflow-wrap: anywhere; }
.pvdoc .pvh { margin: 16px 0 8px; font-size: var(--fs-callout); font-weight: var(--w-bold); }
.pvdoc .pvh:first-child { margin-top: 0; }
.pvsheet { margin: 14px 0 6px; font-size: var(--fs-cap); font-weight: var(--w-bold); color: var(--mute); }
.pvsheet:first-child { margin-top: 0; }
/* 列宽跟着内容走，装不下就横向滚 —— 不是把某一列挤成一条竖带。
   真实的报销表里第一列常是很长的事由，浏览器的自动布局会为了塞下
   后面八列而把它压到 150px，读起来是一列竖排的字。 */
.pvtable { border-collapse: collapse; font-size: var(--fs-foot); width: max-content; min-width: 100%; }
.pvtable th, .pvtable td {
  border: 1px solid var(--line); padding: 6px 10px; text-align: left;
  vertical-align: top; white-space: pre-wrap;
  min-width: 6ch; max-width: 34ch; overflow-wrap: anywhere;
}
.pvtable th { background: var(--surface-2); font-weight: var(--w-bold); position: sticky; top: 0; }
.pvtable td { font-variant-numeric: tabular-nums; }

/* 看不了的那一种。展开的是「为什么」加一个下载按钮 ——
   点一下没反应会让人去猜是文件坏了、权限不够、还是系统没做。 */
.pvnone { text-align: center; padding: 18px 12px; }
.pvnone__t { margin: 0; font-weight: var(--w-semibold); font-size: var(--fs-callout); }
.pvnone__w { margin: 8px auto 14px; max-width: 46ch; font-size: var(--fs-sub); color: var(--mute); line-height: 1.7; }

/* ---- 全屏 ---- */
.pvdlg {
  width: min(1200px, 94vw); max-width: none; height: min(88vh, 100%);
  padding: 0; border: 1px solid var(--line); border-radius: var(--r-lg);
  background: var(--surface); color: var(--ink);
  box-shadow: var(--specular), var(--shadow-lift);
}
.pvdlg::backdrop { background: rgba(10, 10, 22, 0.55); }
.pvdlg__head {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 14px; border-bottom: 1px solid var(--line);
}
.pvdlg__head b { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pvdlg__x {
  width: 32px; height: 32px; flex: none; border: 0; border-radius: 50%;
  background: var(--surface-2); color: var(--ink);
  font-size: var(--fs-title); line-height: 1; cursor: pointer; font-family: inherit;
}
.pvdlg__body { padding: 12px; height: calc(100% - 57px); overflow: auto; }
.pvdlg__body .filepv { border: 0; background: none; padding: 0; }
.pvdlg__body .pvimg { max-height: none; }
.pvdlg__body .pvpdf { height: 74vh; }
.pvdlg__body .pvdoc { max-height: none; }

/* ---- 选文件：已选的排成一列，能单独去掉 ---- */
.filepick { display: grid; gap: 8px; }
.picked { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.picked li {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface); font-size: var(--fs-sub);
}
.picked li b { font-weight: var(--w-semibold); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.picked li .sz { margin-left: auto; flex: none; color: var(--mute); font-size: var(--fs-cap); }
.picked li em { font-style: normal; flex: none; color: var(--bad); font-size: var(--fs-cap); }
.picked li.bad { border-color: var(--bad); background: var(--bad-bg); }
.picked__x {
  flex: none; width: 26px; height: 26px; line-height: 24px; text-align: center;
  border: 1px solid var(--line-strong); border-radius: 50%;
  background: transparent; color: var(--mute);
  font-size: var(--fs-body); font-family: inherit; cursor: pointer;
}
@media (hover: hover) and (pointer: fine) { .picked__x:hover { background: var(--bad-bg); color: var(--bad); border-color: var(--bad); } }
.picked__x:focus-visible { background: var(--bad-bg); color: var(--bad); border-color: var(--bad); }
.pickmsg { margin: 0; font-size: var(--fs-cap); color: var(--mute); }

@media print {
  /* 打印一张单时，展开着的附件跟着印出来 —— 但 PDF 的 iframe 印不出来，
     印出来是一块空白，不如不占版面。 */
  .pvpdf, .pvbar, .file__act, .filebar, .file__pick, .filepick { display: none !important; }
  .pvdoc, .filepv { max-height: none; overflow: visible; border: 0; padding: 0; }
}

/* ===============================================================
   2026-09-07 · 完整链路 / 提醒 / 单据查询 / 印章分组
   =============================================================== */

/* ---- 流转轨迹：已办、进行中、还没轮到 ----
   三档深浅，不是三种颜色。颜色分类要人先学会图例；深浅是**时间方向**，
   看一眼就知道哪边是过去哪边是将来。
   「还没轮到」那几格必须一眼看出是预测 —— 虚线边加淡字：
   它长得就不像一条已经发生的记录。 */
.chain.full li { border-left: 3px solid var(--line-strong); }
.chain.full li.st-done { border-left-color: var(--ok); }
.chain.full li.st-end { border-left-color: var(--ok); }
.chain.full li.st-current { border-left-color: var(--tint); background: var(--tint-soft); }
.chain.full li.st-stopped { border-left-color: var(--bad); }
.chain.full li.st-skipped { border-left-color: var(--line); color: var(--mute); }
.chain.full li.st-future {
  border-left-style: dashed; border-left-color: var(--line-strong); opacity: 0.72;
}
.chain.full li.st-future .c1 b { font-weight: var(--w-semibold); }
.c2 .soon { color: var(--mute); }
.c3.note {
  background: transparent; padding: 4px 0 0; font-size: var(--fs-foot); color: var(--mute);
}
.chain.full li.st-stopped .c3.note, .chain.full li.st-current .c3.note { color: var(--late); }

.sec.sub { font-size: var(--fs-callout); margin-top: 22px; }
.nudges { list-style: none; margin: 8px 0 0; padding: 0; display: grid; gap: 6px; }
.nudges li { font-size: var(--fs-sub); color: var(--ink-2); }

/* ---- 侧栏与标签栏上的数字 ----
   两个数字，两种权重：待办是中性的计数，未读提醒是有人在等你 —— 后者上色。 */
.side__n {
  margin-left: auto; flex: none; min-width: 20px; padding: 0 6px;
  border-radius: var(--r-pill); background: var(--surface-2); color: var(--ink-2);
  font-size: var(--fs-cap); font-weight: var(--w-bold); line-height: 19px; text-align: center;
}
.side__n.hot { background: var(--bad); color: var(--on-tint); }
/* 徽标数字跟着文字一起淡出，不硬切 —— 它和文字在同一行上，
   一个淡出一个瞬灭的话，那一帧会很脏。 */
.side.mini .side__n { opacity: 0; max-width: 0; overflow: hidden;
  /* min-width 和 padding 也要归零：max-width: 0 压不过 min-width: 20px，那颗看不见的药丸照样占 32px，
     居中的图标被它顶偏 10px —— 只有「提醒」偏，因为只有它有计数（用户 2026-09-12 点名，PITFALLS #260）。 */
  min-width: 0; padding: 0; margin-left: 0;
  transition: opacity var(--t-2) var(--ease-out), max-width var(--t-2) var(--ease-out); }
.side__link.sub .side__n { display: none; }
.tab__n {
  position: absolute; top: 4px; left: 50%; margin-left: 6px;
  min-width: 16px; padding: 0 4px; border-radius: var(--r-pill);
  background: var(--tint); color: var(--on-tint);
  font-size: var(--fs-micro); font-weight: var(--w-bold); line-height: 16px; text-align: center;
}
.tabbar a { position: relative; }
.topbar__bell { margin-left: auto; position: relative; }
.topbar__bell .dot {
  position: absolute; top: 8px; right: 8px; width: 8px; height: 8px;
  border-radius: 50%; background: var(--bad);
}
.count.hot { background: var(--bad); color: var(--on-tint); }

/* ---- 提醒列表 ---- */
.notices .card.unread { border-left: 3px solid var(--bad); }
.udot {
  display: inline-block; width: 7px; height: 7px; margin-left: 8px;
  border-radius: 50%; background: var(--bad); vertical-align: middle;
}

/* ---- 单据查询：范围切换 + 筛选条 ----
   范围是一排分段控件（看得出来是「这几个里选一个」），
   筛选是一条表单（看得出来是「填完按一下」）。两者形状不同，
   因为一个是立刻生效的导航，一个要提交。 */
.segs { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; }
.seg {
  padding: 7px 13px; border-radius: var(--r-pill); border: 1px solid var(--line);
  background: var(--surface); color: var(--ink-2);
  font-size: var(--fs-sub); font-weight: var(--w-semibold); text-decoration: none;
  min-height: 34px; display: inline-flex; align-items: center;
}
@media (hover: hover) and (pointer: fine) { .seg:hover { border-color: var(--line-strong); } }
/* 选中态用淡底 + 主题色字，**不是一块黑**。
   这是内容层，内容层的「选中」在这套界面里一直是 --tint-soft
   （.state.run、.card.on 都是），实心深色是侧栏那一层的语言 ——
   把控制层的重量搬到内容层，那一格就会重得像个按钮而不是个筛子。 */
.seg.on {
  background: var(--tint-soft); border-color: var(--tint);
  color: var(--tint); font-weight: var(--w-semibold);
}
@media (max-width: 560px) {
  /* 四个范围要挤在一行。分段控件一旦折成 3+1，读起来就是两组东西 ——
     而它表达的是「这四个里选一个」。 */
  .seg { padding: 6px 10px; font-size: var(--fs-foot); }
  .segs { gap: 5px; }
}

.filterbar {
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 16px;
}
/* 表单控件的基础规则是 width:100%（表单里那是对的），
   在这条横向的筛选条里必须逐个拆掉 —— 否则每个控件各占一行，
   「一条筛选条」变成四行，而它本来就该是扫一眼填完的那一下。 */
/* 搜索框长到 1026px 就不再是搜索框，是一条横贯内容区的空槽
   —— 用户 2026-09-07 说的「大方框」正是这种。给它一个上限，
   多出来的宽度留给它右边的筛选控件。 */
.filterbar > input[type="search"] { flex: 0 1 var(--fw-m); width: auto; min-width: 0; }
.filterbar > select { flex: 0 0 auto; width: auto; min-width: 120px; max-width: 200px; }
.filterbar > button, .filterbar > .btn { flex: none; }
@media (max-width: 560px) {
  /* 手机上竖着排，但搜索框独占一行、状态和排序并排 —— 
     三个控件各占一行会把这一段拉成半屏。 */
  .filterbar > input[type="search"] { flex: 1 1 100%; }
  .filterbar > select { flex: 1 1 0; max-width: none; }
}
.sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.docs .at { color: var(--mute); }
.docs .at.late { color: var(--late); }

/* ---- 印章按主体分组 ----
   折叠不能把问题一起折叠掉：标题上带着「几枚没人能开」，
   而有问题的那一组默认展开（由服务端决定，不靠脚本）。 */
.grp { margin-bottom: 14px; }
.grp__h {
  list-style: none; cursor: pointer;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 12px 14px; min-height: var(--tap);
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface);
}
.grp__h::-webkit-details-marker { display: none; }
.grp__h::after {
  content: ""; margin-left: auto; width: 9px; height: 9px;
  border-right: 2px solid var(--mute); border-bottom: 2px solid var(--mute);
  transform: rotate(45deg); transition: transform var(--t-3) var(--ease-out);
}
.grp[open] > .grp__h::after { transform: rotate(-135deg); }
.grp[open] > .grp__h { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.grp__n { font-size: var(--fs-callout); font-weight: var(--w-semibold); }
.grp > .cards { margin-top: 8px; }

/* ---- 待办页最上面那一段：管理员必须先处理的 ---- */
.urgent { margin-bottom: 22px; }
.urgent .sec { margin-top: 0; }

/* ---- 新建账号里的勾选框 ---- */
.check { display: flex; align-items: center; gap: 9px; font-weight: var(--w-medium); }
.check input { width: 18px; height: 18px; flex: none; }

/* ---- 补充说明：发出去之后补的那几句 ----
   「后来补的」必须一眼看出来。它不是正文的一部分 ——
   前面已经批过的人没有看到过它，而这件事只能靠视觉说清楚。 */
.notes { list-style: none; margin: 0 0 16px; padding: 0; display: grid; gap: 10px; }
.notes li {
  border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 11px 13px; background: var(--surface);
}
.notes li.late { border-color: var(--late); background: var(--late-bg); }
.notes__h { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: var(--fs-cap); }
.notes__t { margin: 6px 0 0; font-size: var(--fs-sub); white-space: pre-wrap; }
.notes__w { margin: 7px 0 0; font-size: var(--fs-foot); color: var(--late); }
.notes__add { margin-bottom: 18px; }

/* ---- 组织页：岗位清单（每行一个改名框 + 两个按钮） ----
   这一段刻意**不**做成「选一行、右边改它」的第二层：右栏本身就是那个「右边」，
   再套一层就要先选部门再选岗位再看第三块。岗位一个部门通常一两个，
   每行直接可改是更短的路。 */
.postlist { list-style: none; margin: 0 0 14px; padding: 0; display: grid; gap: 10px; }
.postlist li.off { color: var(--mute); }
.postrow { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.postrow input[type="text"] { flex: 1 1 120px; width: auto; min-width: 0; }
.postrow button { flex: none; }
.postnote { margin: 4px 0 0; font-size: var(--fs-cap); color: var(--mute); }
.addrow { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line); }
.addrow select { margin-top: 6px; }

/* ---- 点之前问一句 ----
   小、居中、两个按钮。**取消在左、确认在右且是主色** ——
   误触的人下一秒会去点离手指最近的那个，所以危险的那个不能占据默认位置。
   文案说的是会发生什么，不是「确定吗」。 */
/* ---- 弹层：从「关着」长出来，退出比进入快一档（第 4 条）----

   `@starting-style` + `transition-behavior: allow-discrete` 让
   `display: none → block` 这一段也能过渡。在它之前，<dialog> 只能靠 keyframes
   做入场，**退场根本没法动**（浏览器一关就 display:none）。

   退出比进入快：进来的时候人要看清这是什么，出去的时候他已经知道了，
   慢一拍就是在浪费他的时间。这条对所有「开 / 关」类的东西都成立。

   键盘打开时零动画（第 6 条）：`[data-kbd]` 由 app.js 在 showModal 上打。
   一天按上百次的东西不该每次都演一遍 —— 频率法则。 */
.cfm {
  border: 0; padding: 0; background: transparent; max-width: min(420px, 92vw);
  opacity: 0; transform: scale(0.98) translateY(4px);
  transition: opacity var(--t-2) var(--ease-out),
              transform var(--t-2) var(--ease-out),
              overlay var(--t-2) var(--ease-out) allow-discrete,
              display var(--t-2) var(--ease-out) allow-discrete;
}
.cfm[open] { opacity: 1; transform: none; transition-duration: var(--t-3); }
@starting-style {
  .cfm[open] { opacity: 0; transform: scale(0.98) translateY(4px); }
}
/* 键盘开的：零动画。两条都要压，否则关的时候还会拖一下。 */
.cfm[data-kbd], .cfm[data-kbd][open] { transition-duration: 0s; }
.cfm::backdrop {
  background: rgba(18, 18, 40, 0);
  transition: background var(--t-2) var(--ease-out),
              overlay var(--t-2) var(--ease-out) allow-discrete,
              display var(--t-2) var(--ease-out) allow-discrete;
}
.cfm[open]::backdrop { background: rgba(18, 18, 40, 0.42); }
@starting-style {
  .cfm[open]::backdrop { background: rgba(18, 18, 40, 0); }
}
.cfm__box {
  background: var(--surface); border-radius: var(--r-lg);
  box-shadow: 0 24px 60px rgba(18, 18, 40, 0.28);
  padding: 20px 22px 18px;
}
.cfm__t { margin: 0; font-size: var(--fs-callout); font-weight: var(--w-semibold); }
.cfm__d { margin: 10px 0 0; font-size: var(--fs-sub); color: var(--ink-2); line-height: 1.62; }
.cfm__act { display: flex; gap: 10px; justify-content: flex-end; margin-top: 18px; }
.cfm__act button { min-height: var(--tap); padding-inline: 18px; }
@media (max-width: 480px) {
  /* 手机上铺满宽度、按钮各占一半 —— 单手拿着的时候，
     两个挤在右下角是最容易点错的排法。 */
  .cfm { max-width: 100%; width: calc(100vw - 32px); }
  .cfm__act button { flex: 1 1 0; }
}

/* ==================================================================
   分流：几条支路并排画在图上
   ==================================================================

   用户 2026-09-07：「需要有条件分支节点，比如说我想设置 按发起人部门跑
   不同流程……北京公司走这条，固安走那条，新疆走自己单独的。」

   同一天他还说了另一句，这块界面按那句改过一遍：
   「你的后台管理页面和界面操作逻辑非常像 CLI 的逻辑，就是一层一层展开
   选择什么的。我希望你用更图形化操作界面的想法去写界面交互。」

   所以这里不画成「一列节点、每个挂一个条件标签」（那是把分支写成了
   一串带前缀的命令），而是**几条支路真的并排铺开**，每条支路头上
   写着按现在的组织算有多少人会走它。看一眼就知道三条路各是什么、
   谁多谁少、有没有哪条一个人都走不到。 */

/* ================= 流程图：竖着的主干，横着排开的支路 =================

   用户 2026-09-07 三轮反馈，这一版同时回答后两条：

     「条件分支应该是自动横向生成树状图……不是在 y 轴上一直增加完全看不懂」
     「窗口就这么大，流程图这块非常局促，需要高频点横滑块左右挪」

   # 为什么整张图转了 90 度

   上一版主干横着走：8 个节点 × 220px ≈ 1900px，而内容列只有 733px ——
   **看一条流程要拖三次横向滚动条**。转过来之后：

     竖 = 先后（主干往下）—— 页面本来就能上下滚，不需要滚动条这个控件
     横 = 并行（支路排开）—— 支路最多三四条，一屏正好放得下

   「两个轴各表示一件事」这条没变（那是上一版最要紧的收获），
   变的只是把长的那一维交给页面自己的滚动。
   对标稿实测的 RuoYi-Office 设计器正是这个形状，和我们上一版是转置关系
   （docs/benchmark-opensource-oa-20260907.md 4.8）。用户点名让我参考它们。

   # 支路用 CSS Grid 对齐，不再靠等高截断

   一条支路一列、一关一行，**行高由那一行里最高的卡决定**。
   于是同序号自动对齐，而且文字再长也不会被截 ——
   上一版拿固定行高换对齐，代价是「发起人的直接主管」和「30/32 人解析得出」
   两行叠在一起（对标稿走查时记下的那个显示问题）。 */

.flowmap {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; align-items: center;
  min-width: 0;
}
.flowmap > li { width: 100%; display: flex; flex-direction: column; align-items: center; }
.flowmap > li.gend { flex-direction: row; gap: 9px; justify-content: center; }

/* ---- 连接段 ----
   命中区是**整段**，不是一个 44px 的圆：竖排之后它横贯整行，指哪儿都能指到。
   平时只有一条竖线；编辑模式下指上去才长出两条臂。 */
.glink {
  position: relative; min-height: 30px; width: 100%;
  display: flex; justify-content: center; align-items: stretch;
}
.glink::before {
  content: ""; position: absolute; top: 0; bottom: 0; left: 50%;
  width: 2px; margin-left: -1px; background: var(--line-strong);
}
.graphwrap.editing .glink { min-height: 52px; }

/* ---- 两条臂 ----
   用户 2026-09-07：「hover 到加号键上，自动动画长出两条分支：一条横着出去，
   另一条向上再向右长出来。那么横着那个就代表不加分支，上面那个就代表加一条分支。
   类似这种思维方式，要把交互做的很灵动。」

   照做，方向随主干转了 90 度：**直着往下的那条 = 在这里加一关，
   拐向右边的那条 = 从这里分叉**。形状本身就是那句话，不用读菜单。

   原来是一个圆加号点开三行文字（审批 / 知会 / 分流），用户说「要仔细阅读才能读懂」。
   现在加出来的一律是审批节点（最常见的那种），想改成知会点那张卡改一下 ——
   **一次三选一，拆成了一次二选一加一次可选的修改**。

   用 transition 不用 keyframes：手在两条臂之间移的时候要能随时改主意（第 12 条）。 */
.gplus {
  position: absolute; inset: 0; z-index: 5;
  display: flex; justify-content: center; align-items: center;
  pointer-events: none;
}
/* 🔴 **两条臂不再靠 hover 长出来。** 用户 2026-09-09 为同一件事提了三次
   （「hover 出来的分支线他就有问题」「hover 加分支和加节点按钮也没了」
   「我根本加不上分支」），三次指向同一个根因：

   藏着的时候两条臂**叠在同一个点上**（都是 top:50% left:50% 再各自 transform）——
   量到的：「加一关」那颗点的中心 elementFromPoint 拿到的是「分流」，
   **主干上五个「加一关」全部点不到**；而一 hover 两条臂各自跳开，
   你瞄准的那个位置已经不是它们现在在的位置。
   **一个会在你伸手去点的那一刻移位的按钮，好不好看都不重要。**

   现在只有一种形态：编辑模式下就摆着（看图的时候 .garm 压根不渲染）。
   四块 hover / focus-within / 触屏兜底的规则全删。
   标签也不再是 absolute —— 以前它不占宽，于是看得见的「加一关」四个字点下去没反应。 */
.garm {
  position: absolute; top: 50%; left: 50%; text-decoration: none;
  display: flex; align-items: center; gap: 7px;
  pointer-events: auto;
}
.garm button {
  all: unset; display: flex; align-items: center; gap: 7px; cursor: pointer;
}
.garm__dot {
  width: 22px; height: 22px; border-radius: var(--r-pill);
  display: grid; place-items: center; flex: 0 0 auto;
  background: var(--surface); border: 1px solid var(--line-strong);
  color: var(--tint); font-size: var(--fs-body); line-height: 1;
  transition: background var(--t-1) var(--ease-out), color var(--t-1) var(--ease-out),
              border-color var(--t-1) var(--ease-out), transform var(--t-1) var(--ease-out);
}
.garm__lb {
  font-size: var(--fs-cap); color: var(--mute); white-space: nowrap;
  transition: color var(--t-1) var(--ease-out);
}
/* 直着往下那条：就在主干线上。 */
.garm--in { transform: translate(-11px, 4px); }
/* 拐出去那条：一段横线把圆点推到右边，看起来是「从主干伸出去一条岔路」。 */
.garm--fork { transform: translate(-11px, -26px); }
.garm--fork .garm__el {
  width: 74px; height: 2px; background: var(--line-strong);
  border-radius: 2px; flex: 0 0 auto;
}
.garm button:focus-visible .garm__dot, .garm:focus-visible .garm__dot {
  background: var(--tint); color: var(--on-tint); border-color: transparent;
  transform: scale(1.08);
}
/* 指上去只是加重，不再决定看不看得见 ——
   所以触屏上不需要兑底，照规矩挡在 hover 后面就行。 */
@media (hover: hover) and (pointer: fine) {
  .garm:hover .garm__dot {
    background: var(--tint); color: var(--on-tint); border-color: transparent;
    transform: scale(1.08);
  }
  .garm:hover .garm__lb { color: var(--ink); }
}
.garm button:active .garm__dot { transform: scale(0.97); }

/* ---- 分流段 ---- */
.gfork {
  width: 100%;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
}
.gfork__h {
  margin: 0; display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  justify-content: center;
  font-size: var(--fs-cap); color: var(--mute);
}
.gfork__h b { font-size: var(--fs-foot); font-weight: var(--w-semibold); color: var(--ink-2); }
/* 「自动分流」是给「手动分流」（发起人自己选走哪条）留的位置：
   两种在图上必须一眼分得开 —— 一个是系统替人决定，一个是人替系统决定。 */
.gfork__auto {
  font-size: var(--fs-cap); font-weight: var(--w-semibold);
  padding: 2px 9px; border-radius: var(--r-pill);
  background: var(--tint-soft); color: var(--tint);
}

/* 一条支路一列、一关一行。行高由那一行最高的卡决定 —— 对齐和不截断同时成立。
   第 1 行是分叉横杆，第 2 行是支路名，第 3 行起是节点，最后一行是汇合横杆。 */
.gfork__body {
  display: grid;
  grid-template-columns: repeat(var(--lanes, 3), minmax(0, var(--gcard-w)));
  /* 行必须**显式**声明出来，否则 grid-row: -1 / -2 那种从末尾数的写法
     落在隐式行上会失效 —— 第一版就是这么把「＋加一关」摆到了节点上面。
     1 = 分叉横杆，2 = 支路名，中间 --rows 行是节点（--rows 取最长那条支路），
     倒数第二行是每条支路的「＋加一关」，最后一行是汇合横杆。 */
  grid-template-rows: auto auto repeat(var(--rows, 1), auto) auto auto;
  gap: 10px 18px;
  justify-content: center;
  max-width: 100%;
}
/* 分叉 / 汇合的横杆：两端正好落在第一列和最后一列的中心上
   —— 缩进半个卡宽就是列心，这是等宽列换来的便利。 */
.gfork__split, .gfork__merge {
  grid-column: 1 / -1; height: 2px; align-self: center;
  background: var(--line-strong); border-radius: 2px;
  margin-inline: calc(var(--gcard-w) / 2);
}
.gfork__split { grid-row: 1; }
.gfork__merge { grid-row: -1; }

/* 每条支路一条贯穿到底的竖线：短的那条也画到汇合点，因为它确实要在那儿等其余几条。 */
.glane__line {
  grid-column: var(--i); grid-row: 1 / -1;
  width: 2px; justify-self: center; align-self: stretch;
  background: var(--lane); opacity: 0.42; border-radius: 2px;
  transition: opacity var(--t-2) var(--ease-out);
}

/* 支路名：线上的一枚小胶囊，同色淡底、无边框，明显比卡片小 ——
   一行里两个长得差不多大的圆角方块会读成「两张卡」。 */
.glane__tag {
  grid-column: var(--i); grid-row: 2; justify-self: center; z-index: 3;
  display: flex; align-items: baseline; gap: 2px 6px; flex-wrap: wrap;
  max-width: 100%; padding: 5px 11px; border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--lane) 14%, var(--surface));
  cursor: default;
}
.glane__tag b { font-size: var(--fs-foot); font-weight: var(--w-semibold); color: var(--lane); }
/* 编辑态里支路名是个链接（点它 = 点这条线，改「谁走这条」）。
   只读态仍是 <span>，所以指针形状由标签自己决定，不写死在这条规则上。 */
a.glane__tag { cursor: pointer; text-decoration: none; }
@media (hover: hover) and (pointer: fine) {
  a.glane__tag:hover { background: color-mix(in srgb, var(--lane) 26%, var(--surface)); }
}
.glane__n {
  font-style: normal; font-size: var(--fs-cap);
  color: color-mix(in srgb, var(--lane) 70%, var(--ink-2));
}
/* 一个人都走不到的支路：几乎一定是单位选错了，标出来。 */
.glane__n.zero { color: var(--bad); font-weight: var(--w-semibold); }

/* 一格 = 一条支路的一关。里面套着这一关**上面**那个插入点。 */
/* 指着支路名，其余支路淡下去 —— 「这条路经过哪几关」变成一次悬停。
   触发点是支路名不是整条支路，否则鼠标横穿过去点卡片时整张图都在闪。 */
@media (hover: hover) and (pointer: fine) {
  .gfork__body:has(.glane__tag:hover) .gcell,
  .gfork__body:has(.glane__tag:hover) .glane__line { opacity: 0.3; }
  .gfork__body:has(.glane__tag:hover) .glane__tag { opacity: 0.3; }
  .gfork__body .glane__tag:hover { opacity: 1 !important; }
}
.gcell {
  grid-column: var(--i); grid-row: calc(var(--k) + 2);
  position: relative; z-index: 2;
  display: flex; flex-direction: column; align-items: center;
  transition: opacity var(--t-2) var(--ease-out);
}
.gcell .gplus { top: -14px; bottom: auto; height: 28px; }
.gcell--add { grid-row: -2; min-height: 28px; }
.gcell--add .gplus { position: relative; inset: auto; height: 28px; }

/* 支路上色：卡片左边一道色条 + 序号圈染色。
   刻意**不**给整张卡染底 —— 内容层是实底的（ui-direction），
   几张彩色方块并排会把「哪一关配错了」这种真正的信号淹掉。 */
.gcell .gcard { border-left: 3px solid var(--lane); }
.gcell .gcard__n { background: var(--lane); color: var(--on-tint); }
.gcell .gcard.notify .gcard__n { background: color-mix(in srgb, var(--lane) 55%, var(--mute)); }
.lane-1 { --lane: var(--lane-1); }
.lane-2 { --lane: var(--lane-2); }
.lane-3 { --lane: var(--lane-3); }
.lane-4 { --lane: var(--lane-4); }
.lane-5 { --lane: var(--lane-5); }
.lane-6 { --lane: var(--lane-6); }

/* ---- 节点面板：长在被点的那张卡下面 ----
   用户 2026-09-07：「点击的节点菜单还不在点击的地方出现，在右侧出现，这很反直觉。」
   所以它不再是钉在屏幕右边的 .rail，而是这张卡自己的一段。
   宽度不跟着卡走（卡只有 236px，装不下选人面板），而是往两边撑开到 380 ——
   **锚点是卡，尺寸由内容定**，这两件事不必是同一个数。 */
/* 节点面板现在就是弹层里的内容（见 admin_flow.html 那段注释）。
   外框由 .prefs__box 给，所以这里一张皮都不画 —— 两层框套在一起就是卡中卡。 */
.gpanel { text-align: left; }
.gtrunk { position: relative; }
.gcard[id] { scroll-margin-top: 88px; }
/* 「再加一条支路」摄在汇合横杠下面、居中 ——
   它作用于**整一段**，不属于任何一条支路。 */
.gfork__more { margin: 10px 0 0; text-align: center; }

/* ---- 线上的条件：「这条线什么时候走」 ----
   用户 2026-09-09：「点击线路出菜单，可以选自动分支、手动分支。」
   **有条件的时候一直亮着**（那是这条线上最该被看见的一句话），
   没条件的时候跟着两条臂一起在 hover / focus 时才浮出来 ——
   八条线上各挂一句「每张单都走」是噪音，不是信息。
   摆在中线**左边**：右边留给「分流」那条臂，两个东西不抢一个位置。 */
.gcond {
  position: absolute; right: calc(50% + 16px); top: 50%; transform: translateY(-50%);
  z-index: 6; max-width: 42%;
  padding: 3px 10px; border-radius: var(--r-pill);
  background: var(--surface-2); color: var(--mute);
  font-size: var(--fs-cap); text-decoration: none;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  opacity: 0; transition: opacity var(--t-2) var(--ease-out);
}
.gcond.on {
  opacity: 1; font-weight: var(--w-semibold);
  background: var(--tint-soft); color: var(--tint);
}
.graphwrap.editing .glink:focus-within .gcond { opacity: 1; }
@media (hover: hover) and (pointer: fine) {
  .graphwrap.editing .glink:hover .gcond { opacity: 1; }
  .gcond:hover { background: var(--tint-soft); color: var(--tint); }
}

/* 卡片面板里只留一句现状 + 一个落点：条件本体在线上那个弹层里。
   留这一句是因为卡片和线是两个地方，站在卡片上也得看得见
   「这一关不是每张单都走」。 */
.nodecond {
  display: flex; align-items: baseline; gap: 8px;
  margin: 10px 0 2px; font-size: var(--fs-cap);
}
.nodecond span { color: var(--mute); }
.nodecond a { color: var(--tint); font-weight: var(--w-semibold); }

/* ---- 加一关的选择器（?add=N）----
   三栏：**缩小范围（树）→ 认出名字（名单）→ 看挑了什么（已选）**。
   和通讯录那三栏是同一句话，也是同一个坑：光有 max-height 不够，
   grid 的行按内容撑，不写 minmax(0,…) 三栏会一起溢出到弹窗外面。 */
.pktab {
  display: flex; gap: 2px; margin: 2px 0 12px;
  border-bottom: 1px solid var(--line);
}
.pktab a {
  padding: 8px 14px; margin-bottom: -1px; text-decoration: none;
  font-size: var(--fs-sub); font-weight: var(--w-semibold); color: var(--ink-2);
  border-bottom: 2px solid transparent;
  transition: color var(--t-1) var(--ease-out), border-color var(--t-1) var(--ease-out);
}
.pktab a.on { color: var(--tint); border-bottom-color: var(--tint); }
@media (hover: hover) and (pointer: fine) { .pktab a:hover { color: var(--ink); } }

.gadd__body {
  display: grid;
  grid-template-columns: minmax(0, 210px) minmax(0, 1fr) minmax(0, 200px);
  grid-template-rows: minmax(0, min(46vh, 400px));
  gap: 0 16px;
}
/* 节点面板里的选择器只有两栏（选中一行就生效，没有「已选」那一栏）。 */
/* 节点面板里的选择器只有两栏（选中一行就生效，没有「已选」那一栏）。
   用户 2026-09-09：「窗口过于局促」—— 那一屏原来跟节点面板共用 440px，
   两栏各分不到 200，树自己就带了一根滚动条。它现在走 .prefs--wide。 */
.gadd__body--two {
  grid-template-columns: minmax(0, 260px) minmax(0, 1fr);
  grid-template-rows: minmax(0, min(56vh, 460px));
}
.gadd__body--two .gadd__sel { display: none; }
.gadd__tree, .gadd__list, .gadd__sel {
  min-height: 0; overflow-y: auto; overscroll-behavior: contain;
}
.gadd__list, .gadd__sel { padding-inline-start: 16px; border-inline-start: 1px solid var(--line); }
.gadd__tree .otree { padding: 0; }
.gadd__sel .rail__t { margin-top: 0; }
/* 已经挑过的那一行打个勾并留在原地 —— 点第二下不该变成两条（pkAdd 挡着），
   而「它已经在里面了」必须看得出来，否则人会以为没点上。

   🔴 **类名不能叫 picked**：`.picked` 在这份文件里早就是一个**组件**
   （附件清单那个 <ul class="picked">，自带 display:grid / gap / list-style），
   而我写的是复合规则 `.pickrow.picked` —— 复合那条里没写 display，
   于是老那条原样生效：行从 flex 变成 grid，岗位名和在任人**掉成了两行**。
   用例全绿（我并没有重复定义任何东西），只有在 Chrome 里量才知道。同 PITFALLS #125。 */
.pickrow.pkon { background: var(--tint-soft); }
.pickrow.pkon b::after { content: " ✓"; color: var(--tint); }
.pksel { list-style: none; margin: 0; padding: 0; }
.pksel li {
  display: flex; align-items: baseline; gap: 6px;
  padding: 8px 0; border-bottom: 1px solid var(--line);
}
.pksel b { font-size: var(--fs-sub); font-weight: var(--w-semibold); }
.pksel span { font-size: var(--fs-cap); color: var(--mute); }
.pksel__x {
  margin-left: auto; padding: 0 6px; text-decoration: none;
  color: var(--mute); font-size: var(--fs-head); line-height: 1;
}
@media (hover: hover) and (pointer: fine) { .pksel__x:hover { color: var(--bad); } }
.gadd__act { margin-top: 16px; display: grid; gap: 8px; }

/* ---- 发布确认：多了 / 少了哪几关、每一批人走哪几关 ----
   两张清单，没有新的「面」：靠一条左边线和颜色分加 / 减。 */
.pubdiff { list-style: none; margin: 0 0 14px; padding: 0; display: grid; gap: 6px; }
.pubdiff li {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  padding: 6px 10px; border-left: 3px solid var(--line-strong);
  font-size: var(--fs-sub);
}
.pubdiff li b { font-weight: var(--w-semibold); }
.pubdiff li em { font-style: normal; font-size: var(--fs-cap); color: var(--mute); flex-basis: 100%; }
.pubdiff li.add { border-left-color: var(--ok); }
.pubdiff li.del { border-left-color: var(--bad); }
.pubdiff li.del b { color: var(--bad); }
.pubdiff li.chg { border-left-color: var(--late); }
.pubdiff li.chg b { color: var(--late); }
.pubpaths { list-style: none; margin: 0 0 14px; padding: 0; display: grid; gap: 10px; }
.pubpaths > li { font-size: var(--fs-sub); }
.pubpaths > li > b { font-weight: var(--w-semibold); }
.pubpaths .who { margin-left: 8px; font-size: var(--fs-cap); color: var(--mute); }
.pubpaths ol {
  margin: 4px 0 0; padding: 0; list-style: none;
  display: flex; flex-wrap: wrap; gap: 4px 0; align-items: center;
  font-size: var(--fs-cap); color: var(--ink-2);
}
.pubpaths ol li + li::before { content: "→"; margin: 0 6px; color: var(--mute); }
.pubpaths ol i { font-style: normal; margin-left: 4px; color: var(--mute); }
.pubpaths small { display: block; margin-top: 3px; color: var(--mute); }
/* 弹层里这两组二选一要压扁：它们是**按下确定那一刻**才起作用的开关，
   不该和上面那三栏抢高度 —— 原样用 .twoup 两组占 164px，
   而挑人那一栏才 400px。 */
.gadd__act .twoup label, .gadd__act .twoup a { padding: 8px 12px; }
.gadd__act .twoup label b, .gadd__act .twoup label em,
.gadd__act .twoup a b, .gadd__act .twoup a em { font-size: var(--fs-cap); }

/* 窄屏：三栏叠成一列，各自不再单独滚（页面自己滚就够了）。 */
@media (max-width: 720px) {
  .gadd__body { grid-template-columns: 1fr; grid-template-rows: none; }
  .gadd__tree, .gadd__list, .gadd__sel { overflow: visible; max-height: none; }
  .gadd__list, .gadd__sel {
    padding-inline-start: 0; border-inline-start: 0;
    border-top: 1px solid var(--line); padding-top: 12px; margin-top: 12px;
  }
}
.gpanel > :last-child { margin-bottom: 0; }
.gpanel .rail__wide { width: 100%; }

/* 「流程谁定」是模板级政策，不属于任何一个节点，也不是这一页的动作
   （动作全在页头），所以它单独摆在图下面，宽度按可读行长封顶 ——
   一行字横铺 1400px 是没法读的。 */
.gsettings { margin: 24px auto 8px; max-width: 620px; }
/* 图下面的每一句说明都跟着「流程谁定」走同一条中线 —— 「共 N 个节点…」那几句排在
   .gsettings 前面，第二批写成 `.gsettings + .hint` 时够不着它们，截图上那一段贴在最左边。 */
.graphwrap ~ .hint { max-width: 620px; margin-inline: auto; text-align: center; }
/* 图下面的一切走同一条中线：「版本」那一节原来横贯整列，和上面 620 的那块对不齐。 */
.gsettings ~ .sec, .gsettings ~ .cards { max-width: 620px; margin-inline: auto; }

.gfork__foot {
  margin: 0; max-width: 620px; text-align: center;
  font-size: var(--fs-cap); color: var(--mute); line-height: 1.6;
}
.gfork__miss {
  display: block; margin-top: 7px; color: var(--late); text-align: left;
  background: var(--late-bg); border-radius: var(--r-sm); padding: 8px 11px;
}

/* ---- 连线：一张 SVG 铺在整张图上 ----

   用户 2026-09-09：「两个节点中间的线实时变化，用肘线。」
   CSS 边框只画得出直上直下、横平竖直的段（分流那儿是一条竖线加一条横轨
   拼出来的，拐角是直角对接），而且拖动时跟不上。SVG 一条 path
   就能画「竖 → 圆角拐 → 横 → 圆角拐 → 竖」，每帧只改一个 d 属性。

   **没有脚本时这张 SVG 不存在**，.wired 也不会加上，下面那几条 CSS 直线
   原样还在 —— 少一种画法，不少一条线。 */
.gwires { position: absolute; inset: 0; pointer-events: none; z-index: 0; overflow: visible; }
.gwire { fill: none; stroke: var(--line-strong); stroke-width: 2; stroke-linejoin: round; }
/* 拖动时线要跟手，所以这一层不做过渡；其余时候淡一点变化就够了。 */
.graphwrap.dragging .gwire { stroke: var(--tint); }

/* 有了 SVG 就把 CSS 那几条直线撤掉，否则两套线会重叠成一条粗的。 */
.wired .glink::before,
.wired .glane__line,
.wired .gfork__split,
.wired .gfork__merge { display: none; }

/* ---- 卡片外壳：× 和拖拽手柄挂在这里 ----
   它们只在编辑模式下渲染，所以这一段平时一条都不生效。

   🔴 **width 必须跟着卡走，不能是 100%。** 这里原来写的是 width:100%，
   那是主干还横着画的时候留下的（每个节点是一行里的一个 flex item）。
   转成竖排之后这一条同时坏了三样，而且三样都没人报：

     · 主干的卡贴在整行的最左边，而竖线画在 50% —— **线根本不碰卡片**。
       量到卡 x=117..353、竖线 x=858，中间隔着七百像素的空白。
     · 插一关的 ＋ 也在 50%，于是它离它要插进去的位置六百多像素。
     · × 是 absolute right:-9px，贴的是这个满宽行的右边缘，不是卡的右上角。

   fit-content 之后三样一起对上：父级 .gtrunk / .gcell 都是
   flex-direction:column + align-items:center，卡自然落在中线上，
   而 .gx / .ghandle 贴的是卡本身。**一个属性，不是三条补丁。** */
/* 拖的时候同伴要**让开**（用户 2026-09-09：「没有拖动到两个节点中间
   两个节点让开的动画和准备吸附的位置示意」）。
   平移量由脚本算（它在拖起来那一刻快照了每一格的纵坐标），
   这里只负责让它滑过去。正在拖的那张不能有过渡 —— 它要粘着指针。 */
.gnode { position: relative; display: flex; width: fit-content; max-width: 100%;
         transition: transform var(--t-2) var(--ease-out); }
.gnode.dragging { transition: none; }
.gnode.drag { cursor: grab; }
/* 同上一条的另一半：图片和文字选中也会打断按住拖。 */
.gnode.drag .gcard { -webkit-user-drag: none; user-select: none; }
/* 触屏：没有这一条，手指一动浏览器就把它当成滚动、把指针收走（pointercancel），
   拖拽在手机上一次都成不了 —— 独立 Chrome 触摸模拟下量到的事件序列就是
   pointerdown → pointermove → pointercancel，什么都没提交。只在编辑态的卡上关掉手势，
   看图时照常能在卡上滑动页面。 */
.graphwrap.editing .gnode.drag { touch-action: none; }
.gnode.drag:active { cursor: grabbing; }
/* 拖动中的卡**跟着指针走**，所以这一刻不能有 transition（会拖出橡皮筋），
   也不能半透明（用户要看的就是它现在停在哪儿）。 */
.gnode.dragging { z-index: 9; }
.gnode.dragging > .gcard {
  transition: none; cursor: grabbing;
  box-shadow: var(--shadow-lift); border-color: var(--tint);
}
.graphwrap.dragging { user-select: none; }

/* 落点提示（第 12 条：transition 不用 keyframes —— 手在两张卡之间来回移的时候
   关键帧会从头放三遍，transition 是从当前值直接转向新目标）。
   竖排之后落点在上边或下边，不再是左右。 */
.gnode.drag::before {
  content: ""; position: absolute; left: -4px; right: -4px; height: 3px;
  top: -7px; background: var(--tint); border-radius: 3px; z-index: 7;
  opacity: 0; transform: scaleX(0.4);
  transition: opacity var(--t-1) var(--ease-inout), transform var(--t-1) var(--ease-inout);
}
/* 🔴 名字必须和脚本里发的那两个一致。
   这里原来写的是 dropL / dropR（横排时代的遗留），而 app.js 发的是
   dropT / dropB —— **于是拖拽的落点提示从竖排那天起一次都没显示过**，
   而且不报任何错。同 PITFALLS #171 那一族。 */
.gnode.drag.dropB::before { top: auto; bottom: -7px; }
.gnode.dropT::before, .gnode.dropB::before { opacity: 1; transform: none; }

.gx { position: absolute; top: -9px; right: -9px; z-index: 6; }
.gx button {
  width: 22px; height: 22px; min-height: 0; padding: 0;
  display: grid; place-items: center;
  border-radius: var(--r-pill); border: 1px solid var(--line);
  background: var(--surface); color: var(--mute);
  font-size: var(--fs-body); line-height: 1; cursor: pointer;
  /* 看不见的时候也不接指针：它贴在卡的右上角（top:-9px），
     正好压在上一段连接线上—— hover 长出来的「分流」那条臂就在那儿。
     opacity:0 的东西照样吃指针，而且吃得无声无息。 */
  pointer-events: none;
  opacity: 0; transform: scale(0.7);
  transition: opacity var(--t-2) var(--ease-out), transform var(--t-2) var(--ease-out),
              background var(--t-2) var(--ease-out), color var(--t-2) var(--ease-out);
}
/* 平时藏着，指到这张卡或者键盘落到它上面才出现 —— 删除是这张图上唯一
   撤不回来的动作，不该一直杵在那儿等误触。 */
@media (hover: hover) and (pointer: fine) {
  .gnode:hover .gx button { opacity: 1; transform: none; pointer-events: auto; }
}
.gx button:focus-visible { opacity: 1; transform: none; pointer-events: auto; }
/* 键盘要到得了它：pointer-events:none 不挡 Tab，但它得能拿到焦点才会显形。 */
.gnode:focus-within .gx button { opacity: 1; transform: none; pointer-events: auto; }
@media (hover: hover) and (pointer: fine) {
  .gx button:hover { background: var(--bad-bg); color: var(--bad); border-color: transparent; }
}

.ghandle {
  position: absolute; left: 50%; bottom: -8px; transform: translateX(-50%) scale(0.8);
  font-size: var(--fs-micro); letter-spacing: -1px; line-height: 1;
  color: var(--mute); opacity: 0; pointer-events: none;
  background: var(--surface); padding: 2px 6px; border-radius: var(--r-pill);
  border: 1px solid var(--line);
  transition: opacity var(--t-2) var(--ease-out), transform var(--t-2) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .gnode.drag:hover .ghandle { opacity: 1; transform: translateX(-50%); }
}

/* 窄屏：支路多到一屏放不下时，让分流那一块自己横滚 —— 整块一起滚，
   几条支路的行还是对齐的（同序号靠的就是它）；各滚各的就再也没法比着看。 */
@media (max-width: 700px) {
  .gfork__body { overflow-x: auto; padding-bottom: 4px; justify-content: start; }
  .graphwrap { --gcard-w: 200px; }
  /* 手机那段 `.gcard { width: auto }` 是给单据页的链写的；编辑器的卡要等宽，不然主干上三种宽。 */
  .graphwrap .gcard { width: var(--gcard-w); }
}

/* 图上的加号菜单：三个动作平铺，不做二级菜单。 */
.gadd__b {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 10px 12px; text-align: left; background: transparent;
  border: 0; border-radius: var(--r-sm); color: inherit; cursor: pointer;
  text-decoration: none; min-height: var(--tap);
}
@media (hover: hover) and (pointer: fine) { .gadd__b:hover { background: var(--surface-2); } }
.gadd__b span { display: flex; flex-direction: column; }
.gadd__b b { font-size: var(--fs-sub); }
.gadd__b small { font-size: var(--fs-cap); color: var(--mute); }

/* ---- 选组织：点卡片，不是勾列表 ----
   41 个单位排成缩进列表时，找「固安公司」要先读懂缩进层级；
   排成卡片网格时，它就是一张写着「固安公司 · 9 人」的卡。
   人数印在卡上，是为了让「勾了一个没有人的单位」在**按下之前**就看得出来。 */
.bpick__t {
  margin: 16px 0 8px; font-size: var(--fs-cap); color: var(--mute);
  text-transform: none; letter-spacing: 0.02em;
}
.bpick__grid {
  display: grid; gap: 8px;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
}
.upick { display: block; position: relative; }
.upick.wide { margin-top: 16px; }
/* 勾选框本身藏起来，但仍在 tab 顺序里 —— 键盘照样能选。 */
.upick input { position: absolute; opacity: 0; width: 0; height: 0; }
.upick__box {
  display: flex; flex-direction: column; gap: 2px;
  padding: 10px 12px; min-height: var(--tap);
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--r-md); cursor: pointer;
}
.upick__box b { font-size: var(--fs-sub); font-weight: var(--w-semibold); }
.upick__box small { font-size: var(--fs-cap); color: var(--mute); line-height: 1.5; }
@media (hover: hover) and (pointer: fine) { .upick__box:hover { border-color: var(--tint); } }
.upick input:checked + .upick__box {
  background: var(--tint-soft); border-color: var(--tint);
  box-shadow: inset 0 0 0 1px var(--tint);
}
.upick input:checked + .upick__box b { color: var(--tint); }
.upick input:focus-visible + .upick__box { outline: 2px solid var(--tint); outline-offset: 2px; }
/* 动作条钉在弹层底部：内容比视口高时（按部门分有几十张卡、发布对比有几十行）
   不用滚到底才按得到。用户 2026-09-12：「确认需要滚动屏幕，不符合直觉。」 */
.dlg__act {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 18px;
  position: sticky; bottom: 0; background: var(--surface); padding: 12px 0 6px;
  border-top: 1px solid var(--line);
}
/* 节点面板里靠右的那三颗（前移 / 后移 / 删除）：动的是位置，不是属性。 */
.nodeops { display: flex; gap: 8px; flex-wrap: wrap; margin-inline-start: auto; }
/* 三家公司一排 */
.bpick__grid--co { grid-template-columns: repeat(auto-fill, minmax(116px, 1fr)); }
/* 按部门分：通讯录那棵折叠树的壳，行里是勾选。行高按触摸区；
   勾选行没有三角，左边留出和折叠行一样的 22px 对齐。 */
.bpick__tree { margin-top: 8px; font-size: var(--fs-sub); }
.bpick__tree .dtree__a { min-height: var(--tap); align-items: center; cursor: pointer; }
.bpick__row { padding-left: 22px; color: var(--ink); }
.bpick__row input { position: absolute; opacity: 0; width: 0; height: 0; }
.bpick__row:has(input:checked) { background: var(--tint-soft); color: var(--tint); font-weight: var(--w-semibold); }
.bpick__row:has(input:checked) .dtree__n::before { content: "✓ "; }
.bpick__row:has(input:focus-visible) { outline: 2px solid var(--tint); outline-offset: 2px; }

@media (max-width: 720px) {
  /* 手机上支路竖着叠 —— 并排两列在 393 宽度下每条只剩不到 170px。 */
  .glane { flex: 1 1 100%; }
  .bpick__grid { grid-template-columns: 1fr 1fr; }
}

/* ==================================================================
   分体按钮（split button）
   ==================================================================

   用户 2026-09-07：「下拉要活用 select / multi-select / grouped-select /
   cascader / split button / megamenu / datepicker。」

   分体按钮的用处很窄，也正因为窄才值得单独有一个形状：
   **一个主动作 + 它的几个变体**，而变体只在某些场景才对。
   新建账号是标准场景 —— 默认「建好账号」回清单，但入职常常是一批人，
   那时候要的是「建好并再建一个」。

   做成两个并排的大按钮就错了：那会让两者看起来一样重。
   分体按钮的形状本身在说「主动作是左边那个，右边是它的变体」。 */
/* grid 里的项默认拉伸填满整格 —— inline-flex 也拦不住它。
   不写 justify-self 的话，这个按钮会横成 668px，又变回一块板砖。 */
.split { display: inline-flex; align-items: stretch; margin-top: 22px; justify-self: start; }
.split > button.primary {
  margin-top: 0; min-width: 180px;
  border-top-right-radius: 0; border-bottom-right-radius: 0;
}
.split__more { position: relative; }
.split__more > summary {
  list-style: none; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--tap); padding: 0 12px; height: 100%;
  background: var(--tint); color: var(--on-tint);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  /* 一道发丝线把主动作和它的变体分开 —— 不用间隙，间隙会让它看起来是两个按钮。 */
  box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.28);
}
.split__more > summary::-webkit-details-marker { display: none; }
@media (hover: hover) and (pointer: fine) { .split__more > summary:hover { background: var(--tint-2); } }
.split__more > summary .gl { width: 18px; height: 18px; }
.split__menu {
  position: absolute; z-index: 40; top: calc(100% + 6px); left: 0;
  min-width: 260px; padding: 6px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-md); box-shadow: var(--shadow-lift);
}
.split__b {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 10px 12px; text-align: left; min-height: var(--tap);
  background: transparent; border: 0; border-radius: var(--r-sm);
  color: inherit; font: inherit; cursor: pointer;
}
@media (hover: hover) and (pointer: fine) { .split__b:hover { background: var(--surface-2); } }
.split__b span { display: flex; flex-direction: column; }
.split__b b { font-size: var(--fs-sub); font-weight: var(--w-semibold); }
.split__b small { font-size: var(--fs-cap); color: var(--mute); }
@media (max-width: 640px) {
  /* 手机上主动作要够宽，变体收窄成一个箭头。
     justify-self 要一起改回 stretch —— 桌面那条 start 会把它按在 222px 上。 */
  .split { display: flex; justify-self: stretch; }
  .split > button.primary { flex: 1 1 auto; }
}

/* ---- 结构代替标点 ----
   用户 2026-09-07：「不要老是全都给我用斜杠做分隔，明显很难用呀。」
   「北京财务部/财务部经理」这种写法把**两个字段**压成了一个字符串：
   读的人要先找到斜杠、再判断哪边是部门，而卡片上最显眼的那一行
   有一半篇幅给了部门名 —— 可他要找的是岗位。
   拆成两段之后，部门退成小一号的前缀，岗位回到主位。 */
.gcard__u, .pickrow__u, .rangelist__u {
  font-style: normal; font-weight: var(--w-regular);
  color: var(--mute); font-size: var(--fs-cap);
  margin-right: 6px;
}

/* 这里原来有三条 .glane 覆盖（flex-direction: row + flex-wrap: wrap）。
   **它们就是「在 y 轴上一直增加完全看不懂」的直接原因**，已经删掉。
   意图是对的（支路之间并行、支路内部先后），实现是错的：
   wrap 一开，窄下来的时候节点自己折行，两个轴的含义就混了。
   现在这件事由上面那一整段 .gfork / .glane 负责，靠 nowrap 而不是靠 wrap。 */

/* ===================================================================
   考勤（2026-09-07，8099 线）
   -------------------------------------------------------------------
   只有四个新 class。其余一律复用已有的（.secbox / .form / .ledger /
   .filterbar / button.primary / .tag …）——
   上一版我发明了 .punch / .tbl / .inline / .devbox / .kv / .plain 六个
   却一行 CSS 都没写，页面退化成浏览器默认外观：原生小按钮、
   英文的 Choose File、表格挤成一坨。教训记在 PITFALLS #95。
   =================================================================== */

/* 上班 / 下班两个按钮并排，手机上各占一半。
   两个动作分量相同（都是「记一笔事实」），所以等宽；
   主次靠 .primary / .ghost 区分，不靠大小。 */
.punchrow { display: flex; gap: 12px; margin-top: 20px; }
.punchrow button { flex: 1; min-height: 46px; }
@media (max-width: 380px) {
  /* 极窄屏竖排，免得两个按钮都被挤到只剩两个字。 */
  .punchrow { flex-direction: column; }
}

/* 定位状态那一行。它要能被一眼看出是「说明」而不是「错误」——
   拿不到位置是**正常情况**（http、没授权、室内），
   用红色会让员工以为打卡失败了。 */
.geoline {
  margin: 4px 0 0;
  font-size: var(--fs-foot);
  color: var(--mute);
  padding: 8px 10px;
  border-radius: var(--r-sm);
  background: var(--surface-2);
}
.geoline.ok { color: var(--ok); background: var(--ok-bg); }
.geoline.off { color: var(--mute); }

/* 表格单元格里的小表单（认领那一列：一个下拉 + 一个按钮 + 一句说明）。
   默认的 .form 是竖排 grid，塞进 <td> 会把行撑得很高。 */
.inlineform {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  margin: 0;
}
.inlineform button { flex: none; }
/* 宽度两档（短 220 / 中 420），上限落在**控件**上而不是网格上 ——
   否则控件会跟着容器一路长，整页宽的地方就变成用户说的「大方框」
   （PITFALLS #99）。 */
.inlineform select { min-width: 0; flex: 0 1 220px; max-width: 260px; margin: 0; }
.inlineform input[type="text"] { flex: 1 1 260px; max-width: 420px; min-width: 0; margin: 0; }
.inlineform .hint { flex: 1 0 100%; margin: 0; }

/* 表格单元格里例外：那一列本来就只有 200 来 px，
   下拉 + 备注 + 保存 + 清除挤一行的结果是每个都换行，读起来像四个动作。
   所以在 <td> 里下拉占满、备注另起一行。 */
td .inlineform select { flex: 1 1 8em; max-width: none; }
td .inlineform input[type="text"] { flex: 1 0 100%; max-width: none; }

/* 「填进设备的三个格子」那张对照表：左窄右宽，值用等宽字体。 */
.ledger.kv { max-width: 640px; }
.ledger.kv th { width: 11em; white-space: nowrap; text-align: left; }
.ledger.kv code { font-size: var(--fs-foot); word-break: break-all; }

/* 右栏里的无标记列表（已认领的工号）。 */
.plainlist { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.plainlist > li { display: grid; gap: 4px; }
.plainlist .hint { margin: 0; }

/* 需要处理的事（待认领工号）。复用 .secbox 的盒子，只把左边那道杠染红——
   和管理首页 .finding.bad 同一个视觉语汇：红杠 = 有人得管这件事。 */
.secbox.warnbox { border-left: 3px solid var(--bad); }
.secbox.warnbox .secbox__h { color: var(--bad); }

/* .filterbar 的基础规则逐个拆掉了 search / select / button 的 width:100%
   （见那一段的注释：「否则每个控件各占一行」），但**漏了 date**。
   考勤的记录筛选是第一个用 <input type=date> 的筛选条，于是两个日期框
   各占满一行、把按钮也挤了下去。补上，写在这里而不是改那一段，
   是为了不和另一条线正在动的 CSS 撞车。 */
.filterbar > input[type="date"] {
  flex: 0 0 auto; width: auto; min-width: 0; max-width: 190px;
}
@media (max-width: 560px) {
  /* 手机上两个日期各占一半，仍在同一行 —— 「从 X 到 Y」本来就是一句话。 */
  .filterbar > input[type="date"] { flex: 1 1 0; max-width: none; }
}

/* ===================================================================
   我的考勤：今天 + 月历（2026-09-07，8099 线）
   -------------------------------------------------------------------
   用户看了钉钉的考勤界面之后：「思考，思考，做一个好用好看丝滑的 oa
   而不是各种堆砌」。这一页因此按「他打开时脑子里的问题」排 ——
   今天要做什么 → 这个月怎么样 → 哪几天有问题 → 明细。
   =================================================================== */

/* 「今天」那一段。整页只有它是一块深色，因为进这一页的人九成
   只想确认一件事：我今天没问题吧。其余都是它的展开。 */
.todaybox {
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 18px 20px;
  margin: 0 0 22px;
}
.todaybox__t { margin: 0; font-size: var(--fs-title); font-weight: var(--w-semibold); line-height: 1.45; }
.todaybox__hm { font-variant-numeric: tabular-nums; }
.todaybox__d { margin: 8px 0 0; color: var(--ink-2); font-size: var(--fs-body); line-height: 1.7; }
.todaybox__f { margin-top: 16px; }
/* 认领没做的时候那句话。它是**待办**不是错误：记录没丢，只是还没归到人头上。 */
.todaybox__warn {
  margin: 14px 0 0;
  padding: 10px 12px;
  border-radius: var(--r-sm);
  background: var(--late-bg);
  color: var(--late);
  font-size: var(--fs-foot);
  line-height: 1.7;
}

/* 月份切换。左右各一个小按钮，中间是月份 —— 不做成下拉，
   因为看考勤的动作九成是「上个月」而不是「跳到某个月」。 */
.monthnav { display: flex; align-items: center; gap: 12px; margin: 0 0 14px; }
.monthnav__t { font-size: var(--fs-title); font-weight: var(--w-semibold); }

/* ---- 月历 ---- */
/* 一格一天，格子里是**事实**（几点到、几点走），不是判定。
   颜色只编码四种状态，全部在下面的图例里出现过 ——
   页面上出现的每一种颜色都必须能在图例里找到，否则它就是装饰。 */
.cal {
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--surface);
  margin: 0 0 10px;
}
.cal__hd, .cal__wk { display: grid; grid-template-columns: repeat(7, 1fr); }
.cal__hd {
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
}
.cal__hd > span {
  padding: 8px 0;
  text-align: center;
  font-size: var(--fs-cap);
  color: var(--mute);
}
.cal__hd .cal__we { color: var(--mute); }
.cal__wk + .cal__wk .cal__d { border-top: 1px solid var(--line); }
.cal__d {
  min-height: 62px;
  padding: 6px 4px 8px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  border-left: 1px solid var(--line);
}
.cal__d:first-child { border-left: 0; }
.cal__n { font-size: var(--fs-cap); color: var(--mute); font-variant-numeric: tabular-nums; }
.cal__t {
  font-size: var(--fs-foot);
  font-variant-numeric: tabular-nums;
  line-height: 1.35;
  color: var(--ink);
}
.cal__pad { background: var(--surface-2); }
.cal__d.we { background: color-mix(in srgb, var(--surface-2) 60%, transparent); }
/* 今天：一圈内描边，不用背景色 —— 背景色的位置留给状态。 */
.cal__d.now { box-shadow: inset 0 0 0 2px var(--tint); }
/* 未来的日子不该被标成「没有记录」：那不是缺卡，是还没到。 */

/* 四种状态。用左上角一个小圆点，不给整格铺色 ——
   铺色会让月历看起来像一张热力图，而这里没有「程度」只有「种类」。 */
.cal__d.s-pair  .cal__n::before,
.cal__d.s-one   .cal__n::before,
.cal__d.s-field .cal__n::before,
.cal__d.s-none:not(.fu):not(.we) .cal__n::before {
  content: "";
  display: inline-block;
  width: 6px; height: 6px;
  border-radius: 50%;
  margin-right: 5px;
  vertical-align: 1px;
}
.cal__d.s-pair  .cal__n::before { background: var(--ok); }
.cal__d.s-one   .cal__n::before { background: var(--late); }
.cal__d.s-field .cal__n::before { background: var(--tint); }
.cal__d.s-none:not(.fu):not(.we) .cal__n::before {
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--line-strong, var(--line));
}
.cal__d.s-one { background: var(--late-bg); }

/* 图例里的色点。和格子里用的是同一组颜色，
   所以图例改颜色不用改两处 —— 它们共用 .s-* 这几个类名。 */
.calkey {
  display: inline-block;
  width: 8px; height: 8px;
  border-radius: 50%;
  margin-right: 2px;
  vertical-align: 0;
}
.calkey.s-pair  { background: var(--ok); }
.calkey.s-one   { background: var(--late); }
.calkey.s-field { background: var(--tint); }
.calkey.s-none  { background: transparent; box-shadow: inset 0 0 0 1px var(--line-strong, var(--line)); }

/* 折起来的明细。要查的时候才展开 —— 一个月三十几行，
   放在首屏会把「这个月怎么样」那几个数字挤下去。 */
.fold {
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 0 16px;
  margin: 22px 0 0;
  background: var(--surface);
}
.fold > summary {
  cursor: pointer;
  padding: 14px 0;
  font-weight: var(--w-semibold);
  list-style-position: inside;
}
.fold[open] > summary { border-bottom: 1px solid var(--line); margin-bottom: 12px; }
.fold > :last-child { margin-bottom: 16px; }

/* 手机（考勤是这一页最常在手机上看的）：格子更矮、字更小，
   七列必须保住 —— 月历一旦换行就不是月历了。 */
@media (max-width: 560px) {
  .todaybox { padding: 14px 15px; }
  /* 窄屏上「席雨涵 · 2025 年 8 月」会折成两行，把左右两个箭头顶歪。
     让标题单独占一行，箭头在它下面排成一排。 */
  .monthnav { flex-wrap: wrap; }
  .monthnav__t { order: -1; flex: 1 0 100%; }
  .cal__d { min-height: 52px; padding: 5px 1px 6px; }
  .cal__t { font-size: var(--fs-micro); }
  .cal__n::before { margin-right: 3px; }
}

/* ===================================================================
   考勤月报：人 × 天矩阵 + 三张柱图（2026-09-07，8099 线）
   -------------------------------------------------------------------
   图是手画的。引一个 30 万行的 JS 库来画三条柱子，
   违反「单二进制、无外部依赖」，也违反 TCO（架构稿第 6 节）。
   =================================================================== */

/* 矩阵：31 列 + 姓名 + 三列汇总，横向滚动是必然的。
   滚的是**这一块**，不是整个页面 —— 页面横向滚动的话，
   左边的侧栏会跟着跑掉。 */
.matwrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--r-md); background: var(--surface); }
.mat { border-collapse: collapse; font-size: var(--fs-cap); width: max-content; min-width: 100%; }
.mat th, .mat td { padding: 0; }
.mat thead th {
  padding: 8px 2px;
  font-weight: var(--w-medium);
  color: var(--mute);
  text-align: center;
  border-bottom: 1px solid var(--line);
  min-width: 18px;
}
.mat thead th.we { color: var(--mute); }
/* 姓名列钉住：滚到月底还要认得出这是谁。 */
.mat .mat__nm {
  position: sticky; left: 0; z-index: 1;
  background: var(--surface);
  text-align: left;
  padding: 6px 12px 6px 14px;
  white-space: nowrap;
  font-weight: var(--w-medium);
  border-right: 1px solid var(--line);
}
.mat tbody tr + tr .mat__nm, .mat tbody tr + tr td { border-top: 1px solid var(--line); }
.mat .mat__nm .tag { margin-left: 6px; }
.mat__d { width: 18px; height: 26px; }
.mat__sum {
  padding: 6px 10px !important;
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.mat__sum.bad { color: var(--late); font-weight: var(--w-semibold); }
/* 格子里不写字（31 列写不下），只用底色。四种状态和月历图例是同一组色。 */
.mat__d.s-pair  { background: color-mix(in srgb, var(--ok) 55%, transparent); }
.mat__d.s-one   { background: color-mix(in srgb, var(--late) 65%, transparent); }
.mat__d.s-field { background: color-mix(in srgb, var(--tint) 45%, transparent); }
.mat__d.s-none  { background: transparent; }
.mat__d.s-none.we { background: var(--surface-2); }

/* 导出按钮排成一行。 */
.dlrow { display: flex; flex-wrap: wrap; gap: 10px; margin: 14px 0 6px; }

/* ---- 柱图 ---- */
/* 每张图旁边都有一句「它在回答什么问题」。写不出来的那张就该删掉。 */
.chart {
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface);
  padding: 16px 18px 12px;
  margin: 0 0 18px;
}
.chart__h { margin: 0; font-size: var(--fs-head); font-weight: var(--w-semibold); }
.chart__q { margin: 6px 0 14px; color: var(--ink-2); font-size: var(--fs-foot); line-height: 1.7; }
.bars {
  display: grid;
  grid-template-columns: repeat(var(--n), minmax(0, 1fr));
  align-items: end;
  gap: 2px;
  height: 132px;
}
/* 排行那张图：柱子给个上限宽度，否则六根柱子铺满整行，
   看起来像热力图而不像「谁最多」。左对齐，空的地方留白。 */
.bars.wide {
  gap: 10px;
  height: 150px;
  grid-template-columns: repeat(var(--n), minmax(0, 84px));
  justify-content: start;
}
.bar {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: center;
  height: 100%;
  min-width: 0;
}
/* 柱子本身。min-height 让「0」也占一像素 —— 完全消失会被读成「没有这一天」。 */
.bar__v {
  width: 100%;
  min-height: 1px;
  border-radius: 3px 3px 0 0;
  background: var(--tint);
}
.bar.hot .bar__v { background: var(--late); }
.bar__n { font-size: var(--fs-cap); font-variant-numeric: tabular-nums; margin-top: 2px; }
.bar__l {
  margin-top: 4px;
  font-size: var(--fs-micro);
  color: var(--mute);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
@media (max-width: 560px) {
  .bars { height: 108px; }
  .bars.wide { gap: 4px; }
}

/* 月历格子里的判定标记（031 之后才会出现）。
   数字用红字而不是给整格铺色 —— 铺色的位置留给「有没有记录」那一维，
   两维都铺色，格子会变成看不懂的调色板。 */
.cal__v {
  font-size: var(--fs-micro);
  font-variant-numeric: tabular-nums;
  color: var(--late);
  background: var(--late-bg);
  border-radius: 4px;
  padding: 0 4px;
  line-height: 1.5;
}
.cal__t.bad { color: var(--late); font-weight: var(--w-semibold); }
b.cal__v { font-weight: var(--w-semibold); }

/* ── 作息卡片（031，考勤设置页） ────────────────────────────────
   一家公司一张卡。做成卡而不是表格的一行：一行里塞十二个控件之后，
   「北京九点、新疆十点」这件唯一重要的事就看不见了。
   卡上先印**现在按什么算**（大字），再给改的表单 —— 顺序反过来的话，
   人会先动手改，才发现原来的是什么。 */
.rulecards { list-style: none; margin: 0 0 14px; padding: 0; display: grid; gap: 10px; }
@media (min-width: 900px) { .rulecards { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); } }
.rulecard {
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface);
  padding: 10px;
}
.rulecard__h { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
.rulecard__nm { font-size: var(--fs-callout); }
.rulecard__now { margin: 0 0 4px; display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.rulecard__t { font-size: var(--fs-head); font-variant-numeric: tabular-nums; letter-spacing: .01em; }
.rulecard__t.none { color: var(--bad); font-size: var(--fs-callout); }
.rulecard__d { font-size: var(--fs-sub); color: var(--ink-2); }
.rulecard__past { margin: 8px 0 0; font-size: var(--fs-cap); color: var(--mute); }
.rulecard__past span { display: inline-block; margin-right: 10px; font-variant-numeric: tabular-nums; }

.ruleform { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line); }
.ruleform__r { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; }
.ruleform__r:last-child { margin-bottom: 0; }
.ruleform label { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-sub); color: var(--ink-2); }
.ruleform__l { font-size: var(--fs-sub); color: var(--ink-2); }
.ruleform input[type="time"], .ruleform input[type="date"] { width: auto; min-width: 0; }
.ruleform input[type="text"] { flex: 1 1 180px; min-width: 0; max-width: 320px; }
.ruleform input.num { width: 68px; text-align: right; }

/* 七个勾。做成勾而不是一个「周一至周五」的下拉框：
   勾的状态同时看得见和改得动，下拉框要点开才知道周六在不在里面。 */
.daypills { display: inline-flex; gap: 4px; }
/* inline-block 而不是默认的 inline：inline 的 label 是个零高的框，
   里面那个 width/height:100% 的勾就量出 0×0。点得动（点的是 span，
   label 转给它），但键盘和辅助技术拿到的是一个没有尺寸的控件。 */
.daypill { position: relative; display: inline-block; line-height: 0; }
.daypill input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.daypill span {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  font-size: var(--fs-cap);
  color: var(--mute);
  background: var(--surface);
}
.daypill input:checked + span { background: var(--tint); border-color: var(--tint); color: var(--on-tint); font-weight: var(--w-semibold); }
.daypill input:focus-visible + span { outline: 2px solid var(--tint); outline-offset: 2px; }

/* ── 月报格子上的判定角标（031） ────────────────────────────────
   底色那一维是「有没有记录」，判定另起一维用**角标**，不抢底色。
   两维都铺色的话，一张 31 列的表会变成看不懂的调色板 —— 而且
   「有记录但迟到」和「只有一条」会混成同一种橙。 */
.mat__d { position: relative; }
.mat__d.v-late::before,
.mat__d.v-both::before {
  content: ""; position: absolute; top: 0; right: 0;
  border-top: 6px solid var(--bad); border-left: 6px solid transparent;
}
.mat__d.v-early::after,
.mat__d.v-both::after {
  content: ""; position: absolute; bottom: 0; right: 0;
  border-bottom: 6px solid var(--bad); border-left: 6px solid transparent;
}
/* 图例里的两个角标：拿同一个三角，放在一个和其它图例一样大的方块里。 */
.calkey.v-late, .calkey.v-early { background: transparent; position: relative; box-shadow: inset 0 0 0 1px var(--line); }
.calkey.v-late::before {
  content: ""; position: absolute; top: 0; right: 0;
  border-top: 6px solid var(--bad); border-left: 6px solid transparent;
}
.calkey.v-early::after {
  content: ""; position: absolute; bottom: 0; right: 0;
  border-bottom: 6px solid var(--bad); border-left: 6px solid transparent;
}

/* 宽限印在作息标题这一行里（用户 2026-09-07：「9 点 +15 分那种」）。
   小一号、淡一点，但和时间同一行 —— 它是制度的一部分，不是补充说明。 */
.rulecard__g {
  font-size: var(--fs-cap);
  font-weight: var(--w-semibold);
  color: var(--tint);
  vertical-align: 2px;
}
.rulecard__eff {
  margin: 0 0 4px;
  font-size: var(--fs-sub);
  color: var(--ink-2);
}
.rulecard__eff b { font-variant-numeric: tabular-nums; }

/* ==================================================================
   流程编辑器 · 节点面板里的控件
   ==================================================================

   用户 2026-09-07：「操作选项也都很难用，非常反直觉。
   要仔细阅读才能读懂什么是什么。」

   四条改法，都是同一句话的不同说法 ——**把结果画出来，别把机制写出来**：

     1 「交给谁」四张卡，卡面上写的是这一关现在落到谁头上，不是引擎怎么算的
     2  两个选项的地方一律不用 <select>（下拉要点开才知道有什么）
     3  条件只显示当前依据用得上的控件，靠 :has() 做，不需要脚本
     4  期限给档位，别让人把「三天」换算成 72

   这是 PITFALLS #93「不要做成一层层展开去选」在这一页上的第二次施工。 */

/* ---- 交给谁：四张等大的卡 ---- */
.whogrid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 8px 0 0;
}
/* form 自己当格子，按钮把它填满 —— 用 display:contents 的话
   表单在部分引擎的无障碍树里会整个消失。 */
.whogrid > form { display: grid; margin: 0; }
.whocard {
  display: grid; grid-template-rows: auto auto auto; gap: 2px;
  align-content: start; justify-items: start; text-align: left;
  min-height: 92px; padding: 12px 12px 11px;
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface); color: var(--ink); width: 100%;
  cursor: pointer; font: inherit;
  transition: border-color var(--t-2) var(--ease-out),
              background var(--t-2) var(--ease-out),
              transform var(--t-1) var(--ease-out);
}
.whocard__i { width: 22px; height: 22px; fill: none; stroke: var(--mute);
  stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round;
  transition: stroke var(--t-2) var(--ease-out); }
.whocard b { font-size: var(--fs-sub); font-weight: var(--w-semibold); }
.whocard em {
  font-style: normal; font-size: var(--fs-cap); color: var(--mute);
  line-height: 1.45; overflow-wrap: anywhere;
}
.whocard.on {
  border-color: var(--tint); background: var(--tint-soft);
}
.whocard.on b { color: var(--tint); }
.whocard.on em { color: var(--ink-2); }
.whocard.on .whocard__i { stroke: var(--tint); }
.whocard:focus-visible { outline: 2px solid var(--tint); outline-offset: 2px; }
@media (hover: hover) and (pointer: fine) {
  .whocard:hover { border-color: var(--tint); }
  .whocard:hover .whocard__i { stroke: var(--tint); }
}
.whocard:active { transform: scale(0.97); }

/* 选中那张卡的细节，展开在四张卡下面而不是卡里面 ——
   长在卡里面会把那一张撑高，四张就不一样大了（用户点过名的毛病）。 */
.whomore {
  margin-top: 10px; padding: 12px; border-radius: var(--r-md);
  background: var(--tint-soft);
  border: 1px solid color-mix(in srgb, var(--tint) 24%, transparent);
}
.whomore__t {
  margin: 0 0 8px; font-size: var(--fs-cap); font-weight: var(--w-semibold); color: var(--tint);
}
.whomore .hint { margin: 8px 0 0; }
.whomore .rail__wide { margin-top: 10px; }

/* ---- 主管链的台阶 ---- */
.ladderform { margin: 0; }
.ladder { display: grid; grid-template-columns: repeat(auto-fit, minmax(104px, 1fr)); gap: 6px; }
.rung {
  display: grid; gap: 1px; justify-items: start; text-align: left;
  padding: 9px 10px; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface); color: var(--ink); cursor: pointer; font: inherit;
  transition: border-color var(--t-2) var(--ease-out), background var(--t-2) var(--ease-out);
}
.rung__n {
  display: grid; place-items: center; width: 18px; height: 18px; border-radius: 50%;
  background: var(--tint-soft); color: var(--tint);
  font-size: var(--fs-micro); font-weight: var(--w-bold); font-variant-numeric: tabular-nums;
}
.rung__l { font-size: var(--fs-cap); font-weight: var(--w-semibold); margin-top: 3px; }
.rung__c { font-size: var(--fs-micro); color: var(--mute); font-variant-numeric: tabular-nums; }
.rung.on { border-color: var(--tint); background: var(--tint-soft); }
.rung.on .rung__n { background: var(--tint); color: var(--on-tint); }
.rung:focus-visible { outline: 2px solid var(--tint); outline-offset: 2px; }
@media (hover: hover) and (pointer: fine) { .rung:hover { border-color: var(--tint); } }

/* ---- 两三个选项的分段控件 ---- */
/* align-items 要显式写 stretch：同一行两张卡文案有长有短，
   不拉平高度就不一样 —— 正是用户点名的「各个框大小都不一样」。 */
.twoup { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; align-items: stretch; }
/* 148 在 273px 宽的面板里永远排不下两列（2×148+8 > 273），四个选项会站成一竖条。
   120 才是这个容器里真的能两列的下限。 */
.twoup--wrap { grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); }
/* 单选钮不用 display:none —— 那会让它从 Tab 序列里消失。
   压成一个像素留在原处，键盘和读屏都还找得到它。 */
.twoup > input {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; border: 0; clip-path: inset(50%); overflow: hidden;
}
.twoup > label, .twoup > a {
  display: grid; gap: 2px; align-content: center; min-height: var(--tap);
  padding: 9px 11px; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface); color: inherit; text-decoration: none; cursor: pointer;
  transition: border-color var(--t-2) var(--ease-out), background var(--t-2) var(--ease-out);
}
.twoup > label b, .twoup > a b { font-size: var(--fs-sub); font-weight: var(--w-semibold); }
.twoup > label em, .twoup > a em { font-style: normal; font-size: var(--fs-micro); color: var(--mute); line-height: 1.4; }
.twoup > input:checked + label {
  border-color: var(--tint); background: var(--tint-soft);
}
.twoup > input:checked + label b { color: var(--tint); }
/* 选中态底色是 tint-soft，mute 落在上面两套主题都不到 4.5（暗 3.88 / 亮 3.73）：
   次要文字换 ink-2。.whocard.on em 一直是这么写的，这里补齐其余几处。 */
.twoup > input:checked + label em, .twoup > a.on em,
.upick input:checked + .upick__box small,
.whomore .hint, .whomore .nodeadv > summary,
.rung.on .rung__c, button.nodeitem.on em,
.chain.full li.st-current .who { color: var(--ink-2); }
.twoup > input:focus-visible + label { outline: 2px solid var(--tint); outline-offset: 2px; }
/* 链接版（.twoup > a）：选中态从地址里来（.on），不靠 :checked。「加一关」弹层里的
   串/并发、审批/知会用它 —— 它们原来是 radio，弹层每换一屏就回到默认（PITFALLS #234）。
   **和 label 版是同一条规则**，不另造一块面（棘轮 TestSurfaceComponentsDoNotGrow 守着）。 */
.twoup > a.on { border-color: var(--tint); background: var(--tint-soft); }
.twoup > a.on b { color: var(--tint); }
.twoup > a:focus-visible { outline: 2px solid var(--tint); outline-offset: 2px; }
@media (hover: hover) and (pointer: fine) { .twoup > label:hover, .twoup > a:hover { border-color: var(--tint); } }

/* ---- 条件：只显示当前依据用得上的控件 ---- */
.condbox .condpart { display: none; gap: 8px; }
.condbox:has(#cb-company:checked) .condpart[data-for~="company"],
.condbox:has(#cb-dept:checked) .condpart[data-for~="dept"],
.condbox:has(#cb-field:checked) .condpart[data-for~="field"] { display: grid; }
.condrow { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; }
.condbox .flab:first-child { margin-top: 2px; }

/* ---- 期限：数字 + 常用档位 ---- */
.dur { display: grid; grid-template-columns: 120px auto; gap: 6px 8px; align-items: center; }
.dur__u { font-size: var(--fs-cap); color: var(--mute); }
.dur__q { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 6px; }
.dur__q button {
  padding: 5px 10px; border: 1px solid var(--line); border-radius: var(--r-pill);
  background: var(--surface); color: var(--ink-2); font-size: var(--fs-cap);
  cursor: pointer; font-family: inherit;
  transition: border-color var(--t-2) var(--ease-out), color var(--t-2) var(--ease-out);
}
.dur__q button.on { border-color: var(--tint); color: var(--tint); background: var(--tint-soft); }
.dur__q button:focus-visible { outline: 2px solid var(--tint); outline-offset: 2px; }
@media (hover: hover) and (pointer: fine) {
  .dur__q button:hover { border-color: var(--tint); color: var(--tint); }
}

/* ==================================================================
   管理区的骨架：一个名词一页，一个页面形状
   ==================================================================

   用户 2026-09-07 夜，拿致远的管理端做对照：「明显他这种太老了但是逻辑是清晰的，
   我们新做的逻辑就非常混乱。」

   致远清晰在哪，拆准了是三句话：**一个名词一页**、**每页一个形状**、
   **导航按名词域分组**。三条的共同点是「我要改 X，去哪儿」永远一跳可达。
   理由和逐条对照写在 oa/docs/seeyon-admin-skeleton-20260907.md。

   页面形状统一成三段：

     工具条（.pagehead）→ 清单（.tablewrap > .ledger.lsel，**带总数** .lcount）
     → 明细就地展开在清单下面（.ldetail）

   明细在下面不在右栏 —— 用户同一天另一条反馈的结论，而致远本来就是这么摆的。 */

/* ---- 侧栏的第二级：当前这一组把它的页展开 ----
   六个组名一直在，进到哪一组就在它下面展开那一组的页。
   全展开是 22 行一屏放不下，全收起又回到「去哪儿改 X」没有答案。
   收放靠**当前路径**判，不靠脚本、不存状态。 */
.side__pages {
  display: grid; gap: 1px; margin: 1px 0 5px;
  padding-left: 30px; position: relative;
}
/* 一条竖线把这一组的页串起来 —— 缩进表示从属，线让它读起来是一组而不是四个散项。 */
.side__pages::before {
  content: ""; position: absolute; left: 19px; top: 4px; bottom: 4px;
  width: 1px; background: var(--line);
}
.side__link.pg {
  min-height: 32px; padding: 5px 10px; font-size: var(--fs-cap);
  color: var(--ink-2); border-radius: var(--r-sm);
}
.side__link.pg.on { background: var(--tint-soft); color: var(--tint); font-weight: var(--w-semibold); }
@media (hover: hover) and (pointer: fine) {
  .side__link.pg:hover { background: color-mix(in srgb, var(--ink) 6%, transparent); }
  .side__link.pg.on:hover { background: var(--tint-soft); }
}

/* ---- 可选行的清单 ---- */
.ledger.lsel tbody tr.on { background: var(--tint-soft); }
.ledger.lsel tbody tr.on td:first-child { box-shadow: inset 3px 0 0 var(--tint); }
.ledger.lsel tbody tr.off td { color: var(--mute); }
.ledger.lsel td:first-child a {
  font-weight: var(--w-semibold); text-decoration: none; color: var(--ink);
}
.ledger.lsel tbody tr.on td:first-child a { color: var(--tint); }
@media (hover: hover) and (pointer: fine) {
  .ledger.lsel tbody tr:hover { background: var(--surface-2); }
  .ledger.lsel tbody tr.on:hover { background: var(--tint-soft); }
  .ledger.lsel td:first-child a:hover { color: var(--tint); }
}

/* 清单底下那一行数。致远每张清单都报数（共 79 条岗位、共 31 人），
   我们原先一个数都不报 —— 「这家公司有 29 个岗位」是事实，藏起来是丢信息。 */
.lcount {
  margin: 10px 2px 0; font-size: var(--fs-cap); color: var(--mute);
  font-variant-numeric: tabular-nums;
}
.lcount b { color: var(--ink-2); font-weight: var(--w-semibold); }

/* ---- 明细：清单底下，不是右栏 ----
   从 360px 的右栏搬到整幅宽度之后，几块可以并排 —— 原来要上下滚三屏。 */
.ldetail {
  margin-top: 22px; padding-top: 20px; border-top: 1px solid var(--line);
  display: grid; gap: 16px;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  align-items: start;
}
.ldetail > .rail__box, .ldetail > .rail__act { margin: 0; }
/* 「删除」永远自己占满一行、排在最后：它和上面几块不是一类动作，
   混在网格里会和「改名」挨着，而那两件事的代价差着一个数量级。 */
.ldetail > .rail__act { grid-column: 1 / -1; }

/* ⚠️ `.rail__act` 在 1024 以下会变成**贴屏幕底的固定条** ——
   那条规则是给右栏写的：右栏里 `.rail__act` 装的是这一页唯一的主操作，
   贴底是对的。搬到清单底下之后它装的是**删除**，
   贴底就成了「最危险的那个按钮永远浮在拇指底下」，而且你还在滚清单、
   根本没选中任何东西的时候它就在那儿。所以这里原样按回去。
   写在文件末尾是必须的：媒体查询不加权重，同权重后来居上（PITFALLS #114）。 */
.ldetail > .rail__act {
  position: static; margin: 0;
  padding: 16px; border-radius: var(--r-md);
  background: var(--surface); border: 1px solid var(--line);
  backdrop-filter: none; -webkit-backdrop-filter: none; box-shadow: none;
}
.ldetail > .rail__act .rail__t { display: block; }

/* ---- 显示层级：三个档位摆开，不用下拉 ---- */
.lvls { display: flex; gap: 6px; }
.lvl {
  padding: 6px 11px; border-radius: var(--r-pill); border: 1px solid var(--line);
  background: var(--surface); color: var(--ink-2);
  font-size: var(--fs-cap); text-decoration: none;
  transition: border-color var(--t-2) var(--ease-out), color var(--t-2) var(--ease-out);
}
.lvl.on { border-color: var(--tint); color: var(--tint); background: var(--tint-soft); }
.lvl:focus-visible { outline: 2px solid var(--tint); outline-offset: 2px; }
@media (hover: hover) and (pointer: fine) {
  .lvl:hover { border-color: var(--tint); color: var(--tint); }
}

@media (max-width: 768px) {
  /* 明细在手机上回到一列；侧栏第二级缩进浅一点，否则文字挤成两行。 */
  .ldetail { grid-template-columns: 1fr; }
  .side__pages { padding-left: 24px; }
  .side__pages::before { left: 14px; }
}

/* ---- 清单共用：搜、翻页、拖拽把手、描述 ---- */

/* 搜。宽度落在**控件**上（中档 420），不跟着列宽长 ——
   一个搜索框长到 1026px 是这一页最刺眼的东西（PITFALLS #99）。 */
.lsearch { display: flex; gap: 8px; margin: 0 0 14px; align-items: center; }
.lsearch input[type="search"] { max-width: 420px; flex: 1 1 auto; }

/* 翻页。只在装不下时才画（模板里判的），所以这一段多数页面上不出现。 */
.lpager { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 12px 0 0; }
.lpager a, .lpager b, .lpager .off {
  min-width: 34px; min-height: 34px; padding: 0 10px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  font-size: var(--fs-cap); font-variant-numeric: tabular-nums;
  text-decoration: none; color: var(--ink-2); background: var(--surface);
}
.lpager b { border-color: var(--tint); background: var(--tint-soft); color: var(--tint); }
/* 到头的那一端不做成能点的样子 —— 灰着比消失好：位置不跳。 */
.lpager .off { color: var(--mute); }
.lpager a:focus-visible { outline: 2px solid var(--tint); outline-offset: 2px; }
@media (hover: hover) and (pointer: fine) {
  .lpager a:hover { border-color: var(--tint); color: var(--tint); }
}

/* 拖拽把手。只有它可拖，整行不可拖 —— 整行会和「点一行看明细」抢手势。 */
.gripcol { width: 34px; padding-right: 0 !important; }
.lgrip {
  display: inline-grid; place-items: center; width: 22px; height: 26px;
  cursor: grab; color: var(--mute); opacity: 0.45;
  transition: opacity var(--t-2) var(--ease-out), color var(--t-2) var(--ease-out);
}
.lgrip svg { width: 16px; height: 16px; fill: currentColor; }
.lgrip:active { cursor: grabbing; }
@media (hover: hover) and (pointer: fine) {
  /* 平时淡着**在**，不是藏起来 —— 藏起来没人知道这张清单能排序。 */
  tr:hover .lgrip { opacity: 1; color: var(--tint); }
}
@media (hover: none) { .lgrip { opacity: 0.75; } }
tr.dragging { opacity: 0.45; }
tr.dragging td { background: var(--tint-soft); }

/* 描述那一行，明细里的输入框。 */
.lnote { margin-top: 14px; }
.lnote input { max-width: 420px; }

/* ---- 通讯录 ----
   之前**根本不存在**：组织、岗位、在任关系、电话邮箱全都已经在库里，
   只是没有一个地方把它们摆出来。零新表、零新字段。 */
.dirgrp { margin: 22px 0 0; }
.dirgrp__h {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  margin: 0 0 10px; font-size: var(--fs-callout); font-weight: var(--w-semibold);
}
.dirgrp__h span { color: var(--ink-2); font-weight: var(--w-semibold); }
.dirgrp__h span::before { content: "·"; margin-right: 8px; color: var(--mute); }
.dirgrp__h em { font-style: normal; font-size: var(--fs-cap); color: var(--mute); margin-left: auto; }
.dirlist {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
.dircard {
  padding: 12px 14px; border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface);
}
.dircard__n { margin: 0; font-size: var(--fs-callout); font-weight: var(--w-semibold); }
.dircard__n .tag { margin-left: 6px; }
.dircard__p { margin: 3px 0 0; font-size: var(--fs-cap); color: var(--ink-2); }
.dircard__c { margin: 8px 0 0; display: flex; flex-direction: column; gap: 3px; }
/* 电话和邮箱是**能点的** —— 手机上这是通讯录唯一真正要做的事。 */
.dircard__c a { font-size: var(--fs-cap); font-variant-numeric: tabular-nums; }

/* ---- 公告 ---- */
.annlist { list-style: none; margin: 18px 0 0; padding: 0; display: grid; gap: 14px; }
.anncard {
  padding: 16px 18px; border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface);
}
/* 没看过的那几条左边一道色条 —— 比在标题后面缀一个「未读」省一次阅读。 */
.anncard.new { border-left: 3px solid var(--tint); }
.anncard.pin { background: var(--tint-soft); border-color: color-mix(in srgb, var(--tint) 26%, transparent); }
.anncard__h { margin: 0; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.anncard__h b { font-size: var(--fs-callout); font-weight: var(--w-semibold); }
.anndot { width: 7px; height: 7px; border-radius: 50%; background: var(--tint); flex: none; }
.anncard__m { margin: 4px 0 0; font-size: var(--fs-cap); color: var(--mute); }
/* 正文按原样换行。全站换行都用 pre-wrap，不引模板函数。 */
.anncard__b { margin: 10px 0 0; white-space: pre-wrap; line-height: 1.7; }
.anncard__a { margin: 12px 0 0; }
.annedit textarea { min-height: 140px; }
/* 公告的编辑框要宽：正文是**一整篇文章**，不是一个字段。
   .ldetail 是「300px 起、能塞几列塞几列」的网格（管理区清单页共用），
   在 1440 上排到四列 —— 于是一个 326px 的框里要写一篇通知，
   工具条还被挤成三行。让这一块横跨两格：不改那个网格，只占两个位置。 */
@media (min-width: 1000px) {
  .ldetail > .annedit { grid-column: span 2; }
}

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

/* ── 出差 / 请假的角标（034） ─────────────────────────────────
   **不用红色。**迟到早退是问题，出差是「有人报备过」——
   同一个位置、同一个形状，颜色说的是要不要管它。
   一格里不会同时出现两个角标：出差那天本来就不判迟到早退（attend.Judge）。 */
.cal__v.cov { color: var(--ink-2); background: var(--surface-2, var(--line)); }
/* 「进行中」：今天还没到下班点，早退和缺卡都还没到该算的时候。
   **必须是中性色** —— .cal__v 默认是红的（那一维是「违规几分钟」），
   把一条「还没算」画成红的，等于把等待读成犯规。 */
.cal__v.mute { color: var(--mute); background: color-mix(in srgb, var(--ink) 6%, transparent); }
/* 报备过的日子不再画那个「没有记录」的空心点 —— 它已经有解释了。
   留着的话，月历上会同时说「这天缺记录（值得注意）」和「这天出差」。 */
.cal__d.cov.s-none:not(.fu):not(.we) .cal__n::before {
  background: var(--tint);
  box-shadow: none;
}
/* 月报矩阵里的出差 / 请假格：和月历用同一个想法 ——
   斜纹表示「这天有解释」，不参与「有没有记录」那一维的配色。 */
.mat__d.cov {
  background: repeating-linear-gradient(45deg,
    transparent, transparent 3px,
    color-mix(in srgb, var(--tint) 30%, transparent) 3px,
    color-mix(in srgb, var(--tint) 30%, transparent) 6px);
}
.calkey.cov {
  background: color-mix(in srgb, var(--tint) 45%, transparent);
  border-radius: 2px;
}

/* ── 发起页：找一种单据 + 分类分组（035） ──────────────────────
   分组标题用小字大间距，不用卡片外框 —— 12 张单本来就一屏放得下，
   再给每组套一个框，框比内容还显眼。 */
.findbox { margin: 0 0 14px; max-width: 420px; }
.findbox__l {
  display: block;
  font-size: var(--fs-cap);
  color: var(--ink-2);
  margin-bottom: 4px;
}
.findbox__n { margin-top: 10px; }
.lgrp { margin: 0 0 20px; }
.lgrp__h {
  font-size: var(--fs-cap);
  font-weight: var(--w-semibold);
  letter-spacing: .06em;
  color: var(--ink-2);
  margin: 0 0 10px;
}

/* ── 首页上的新公告（036） ────────────────────────────────────
   一条横幅，不是卡片列表：它不是活，只是「有件事你该知道」。
   做成和待办同样重的卡片，就会有人以为它要处理。
   读过就不再出现，所以这块地方大部分时候是空的。 */
.annstrip {
  margin: 0 0 14px;
  padding: 10px 14px;
  border-radius: var(--r-md);
  background: var(--tint-soft, var(--surface));
  border: 1px solid var(--line);
}
.annstrip ul { list-style: none; margin: 6px 0 0; padding: 0; }
.annstrip li { padding: 2px 0; }
.annstrip__all { display: inline-block; margin-top: 6px; font-size: var(--fs-cap); }

/* ── 应用内推送的那一句提示（036） ────────────────────────────
   右下角一条，六秒后自己走。**不挡任何东西、不需要点掉** ——
   要点掉的提示，最后都会被闭着眼睛点掉（和确认对话框是同一个道理）。
   点它是去看提醒，不是关掉它。 */
.livetoast {
  position: fixed;
  right: 14px;
  bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  z-index: 60;
  padding: 10px 14px;
  border-radius: var(--r-md);
  background: var(--ink);
  color: var(--surface);
  font-size: var(--fs-foot);
  text-decoration: none;
  box-shadow: 0 6px 24px rgba(0, 0, 0, .18);
  transition: opacity var(--t-2) var(--ease-out), transform var(--t-2) var(--ease-out);
}
@starting-style { .livetoast { opacity: 0; transform: translateY(8px); } }
.livetoast.out {
  opacity: 0;
  transform: translateY(8px);
  transition-duration: var(--t-1);
}
/* 手机上底栏占着下面，往上让一格 */
@media (max-width: 768px) {
  .livetoast { bottom: calc(64px + env(safe-area-inset-bottom, 0px)); left: 14px; text-align: center; }
}
/* 「共 N 天」跟在结束日期后面。它是算出来的事实，不是一个能改的值。 */
.rangeday {
  display: inline-block;
  margin-left: 10px;
  font-size: var(--fs-cap);
  color: var(--ink-2);
}

/* ── 组织架构图：真的树（2026-09-08，09-08 二改配色与呼吸） ─────
   一改（09-08 早）：从「带缩进的文本大纲」改成真的树 —— 线是画出来的。
   二改（09-08 晚）用户：「非常的难看，排布没有呼吸感，傻大黑粗
   那么大的页面空白都没有用上。用不同颜色区分不同层级。」

   三件事分别是三个原因，各治各的：

   **空白没用上** —— 每列封顶 340px，1360 的内容列上三列只占 1080，
   右边二百多像素一直空着。改成 flex: 1 1 0 平分整宽，不封顶。

   **没有呼吸感** —— 不是留白不够，是留白落错了地方：盒子内边距 8px（挤），
   兄弟间距 6px（挤），而每个盒子却是三行竖排、右侧空掉半个盒子宽。
   改成「名字 + 元信息」并排一行 + 主管一行，盒子矮一半、宽度被用起来，
   省下的高度还给层与层之间。

   **颜色** —— 层级用**重量**（根实心 → 公司着色 → 部门浅底），
   颜色用来分**哪一支**。37 个部门按层级同色仍然是一片一样的盒子，
   而看这张图最常问的是「这个部门是固安的还是北京的」。
   配色复用流程图那六档 .lane-N（深浅两套都已定义），不新造。

   **两个通道不许混**：色底说「哪一支」，左边那道彩条只说「未设主管」——
   它是这张图上唯一要人动手的东西。把它俩塞进同一条边，警告就被稀释成装饰。

   全部是伪元素画的线：没有 SVG、没有库、没有脚本。 */
/* --lane 的兜底放在**祖先**上，不能放在 .ocbox 自己身上 ——
   自定义属性一旦在元素上有自有声明，就会压过从祖先继承来的值，
   于是公司列上的 .lane-N 永远吃不到（量出来三家公司底色一模一样，
   而 --lane 本身是对的：这种「变量对、结果错」最难从代码上看出来）。 */
.orgchart { --lane: var(--tint); margin: 0 0 20px; }

/* ---- 上两层：根 → 竖线 → 横轨 → 各家公司 ---- */
.oc__root { display: flex; flex-direction: column; align-items: center; }
.oc__root > .ocbox {
  min-width: 260px;
  text-align: center;
  background: var(--tint);
  border-color: var(--tint);
  color: var(--on-tint);
  padding: 12px 18px;
}
.oc__root > .ocbox .ocbox__hd { justify-content: center; gap: 10px; }
.oc__root > .ocbox .ocbox__m,
.oc__root > .ocbox .ocbox__l { color: var(--on-tint-2); }

/* 横轨：一条横线 + 每家公司头上一根短竖线。
   两端的内缩 = 半格，所以线正好落在第一家和最后一家的中心。
   --n 由模板给（有几家公司）；只有一家时不画轨。 */
.oc__rail {
  display: flex;
  /* 必须显式拉满：.oc__root 是 align-items:center 的列 flex，
     子项默认按内容收缩 —— 少了这一行，各列一直卡在 min-width 上，
     右边空一截，也就是用户说的「那么大的页面空白都没有用上」。 */
  align-self: stretch;
  gap: 20px;
  margin-top: 34px;
  position: relative;
  align-items: flex-start;
  flex-wrap: wrap;
}
.oc__rail::before {            /* 根底下垂下来的那一段 */
  content: "";
  position: absolute;
  left: 50%; top: -34px;
  width: 1px; height: 17px;
  background: var(--line-strong, var(--line));
}
.oc__rail::after {             /* 横轨本身 */
  content: "";
  position: absolute;
  top: -17px;
  left: calc(50% / var(--n, 3)); right: calc(50% / var(--n, 3));
  height: 1px;
  background: var(--line-strong, var(--line));
}
.oc__rail:has(> .oc__co:only-child)::after { display: none; }

/* 平分整宽，不封顶 —— flex-basis: 0 才是真平分（240px 的 basis 会让
   部门多的那一列悄悄变宽）。 */
.oc__co { position: relative; flex: 1 1 0; min-width: 250px; }
.oc__co::before {              /* 每家公司头上那根短竖线，跟着这一支的颜色 */
  content: "";
  position: absolute;
  left: 50%; top: -17px;
  width: 1px; height: 17px;
  background: color-mix(in srgb, var(--lane) 45%, var(--line-strong, var(--line)));
}

/* ---- 往下：肘形连线的缩进树 ----
   竖脊贴在左边，每个子节点一根横杈；**最后一个子节点的竖线只画到自己那一杈**，
   否则线会拖到底下变成一根没有落点的尾巴。这一条是「缩进」和「树」的分界。
   线也吃这一支的颜色：连线本身就在说「这几个是一伙的」。 */
.oc__kids { list-style: none; margin: 10px 0 0; padding: 0 0 0 20px; }
.oc__kids > li { position: relative; padding-left: 20px; margin-top: 10px; }
.oc__kids > li::before {       /* 竖脊 */
  content: "";
  position: absolute;
  left: 0; top: -10px; bottom: 0;
  border-left: 1px solid color-mix(in srgb, var(--lane) 28%, var(--line));
}
.oc__kids > li:last-child::before { bottom: auto; height: 26px; }
.oc__kids > li::after {        /* 横杈 */
  content: "";
  position: absolute;
  left: 0; top: 26px;
  width: 15px;
  border-top: 1px solid color-mix(in srgb, var(--lane) 28%, var(--line));
}

/* ---- 节点盒子 ---- */
.ocbox {
  display: block;
  padding: 11px 13px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
}
/* 名字在左、元信息在右：盒子被拉宽之后，这一行才把宽度用起来。
   min-width:0 是给中文长名字留的省略空间，否则它会把右边那截挤出去。 */
.ocbox__hd {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}
.ocbox__n { display: block; font-size: var(--fs-foot); line-height: 1.4; min-width: 0; }
.ocbox__m, .ocbox__l {
  display: block;
  font-size: var(--fs-cap);
  color: var(--ink-2);
  line-height: 1.5;
}
.ocbox__m { white-space: nowrap; flex: none; }
.ocbox__l { margin-top: 2px; }

/* 层级 = 重量。公司着色最重，部门只留一层极浅的底 ——
   37 个盒子如果都上到公司那个浓度，整页就变成一张色卡。 */
.ocbox.k-company {
  padding: 13px 15px;
  background: color-mix(in srgb, var(--lane) 9%, var(--surface));
  border-color: color-mix(in srgb, var(--lane) 34%, var(--line));
}
.ocbox.k-company .ocbox__n { font-size: var(--fs-sub); font-weight: var(--w-semibold); }
.ocbox.k-dept { background: color-mix(in srgb, var(--lane) 3.5%, var(--surface)); }
.ocbox.dim { color: var(--mute); }
/* 未设主管：这张图上唯一要人动手的东西，所以左边这道彩条**只归它**。
   色底表达「哪一支」，这道条表达「要处理」—— 两个通道各说一件事。 */
.ocbox.gap { border-left: 3px solid var(--late); padding-left: 11px; }

@media (max-width: 768px) {
  /* 手机上各家公司竖着排 —— 横轨没有意义了，去掉线，靠缩进树本身表达层级。 */
  .oc__rail { flex-direction: column; gap: 14px; margin-top: 14px; }
  .oc__rail::after, .oc__rail::before { display: none; }
  .oc__co { min-width: 0; }
  .oc__co::before { display: none; }
  .ocbox__hd { flex-direction: column; align-items: flex-start; gap: 0; }
}

/* ── 左树 + 右清单：人员 / 部门 / 岗位 / 考勤按人看 共用的外壳（2026-09-08） ──
   用户：「左侧是公司、部门等，可展开看子部门这些的去筛选右侧人员显示的范围，
   右侧大部分是人员（账号）列表。」同一句话在同一批要求里出现了四次，
   所以它是**外壳**不是部件。

   桌面优先（用户原话「手机端需要你专门优化，我们先做好桌面端」）：
   左 232px 固定、右边吃掉剩下全部。树跟着滚动条粘住 ——
   清单有三十几行，滚到底还能一眼换部门。 */
.orgpane {
  display: grid;
  grid-template-columns: 232px minmax(0, 1fr);
  gap: 22px;
  align-items: start;
}
.orgpane__main { min-width: 0; }   /* 少了它，宽表格会把左树挤没 */

/* 🔴 **页内侧栏要有框。**用户 2026-09-09：「全站的这种侧栏都要框起来。」
   它和内容列是两件东西（一个筛、一个看），只用空白隔开时它看起来
   像是“浮在页面上的一串字”—— 和 2026-09-08 那条「内容层没有面会被读成
   未完成」是同一件事。**一条规则管两棵树**（orgtree 四页 + 弹层，
   dtree 通讯录），不给每个容器各写一份。 */
.otree, .dtree {
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface-2);
}
.otree {
  position: sticky;
  top: 12px;
  font-size: var(--fs-foot);
  line-height: 1.45;
}
.otree ul { list-style: none; margin: 0; padding: 0 0 0 11px; }
.otree > ul { padding-left: 0; }

/* 一行 = 一次筛选。名字在左、人数在右 ——
   人数是选部门时唯一要比较的量，右对齐才扫得起来。 */
.otree__i {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  padding: 5px 8px;
  border-radius: var(--r-sm);
  color: var(--ink-2);
  text-decoration: none;
}
.otree__n { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.otree__c { flex: none; font-size: var(--fs-cap); color: var(--mute); font-variant-numeric: tabular-nums; }
.otree__all { font-weight: var(--w-semibold); color: var(--ink); margin-bottom: 4px; }
.otree__i.dim { color: var(--mute); }

/* 选中的那一行：实底 + 白字。
   **不是加一道左边框** —— 侧栏那么窄，一道 3px 的边和没有几乎看不出来，
   而「我现在在看哪个部门」是这一页最需要一眼确认的一件事。 */
.otree__i.on { background: var(--tint); color: var(--on-tint); font-weight: var(--w-semibold); }
.otree__i.on .otree__c { color: var(--on-tint-2); }

@media (hover: hover) {
  .otree__i:hover { background: var(--surface-2); color: var(--ink); }
  .otree__i.on:hover { background: var(--tint); color: var(--on-tint); }
}

/* 树内的层级靠一条竖线，不靠继续加缩进 —— 侧栏只有 232px，
   四层缩进下去，班组的名字会只剩两个字。 */
.otree ul ul { border-left: 1px solid var(--line); margin-left: 8px; padding-left: 4px; }

@media (max-width: 900px) {
  /* 手机上先摞起来，树限高自己滚。**这是过渡不是终点** ——
     用户明说了手机端要专门优化，那一轮再动。 */
  .orgpane { grid-template-columns: minmax(0, 1fr); gap: 14px; }
  .otree { position: static; max-height: 220px; overflow: auto; }
}

/* ── 明细卡：看 / 改两个状态（2026-09-08，人员页） ─────────────
   用户：「右下半侧展示人员详细信息，但是是不可修改的状态，
   除非点击修改按钮，那就进入编辑模式。」

   两个状态**长得要不一样**，否则「我现在是在看还是在改」得靠找控件来判断。
   看：白底、字段是文本。改：带底色和一道左边，一眼知道自己在改东西。 */
.dcard {
  margin-top: 18px;
  padding: 16px 18px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface);
  scroll-margin-top: 16px;      /* 锚点跳过来时别贴着窗口顶边 */
}
.dcard--empty { background: var(--surface-2); border-style: dashed; }
.dcard--edit {
  background: color-mix(in srgb, var(--tint) 3%, var(--surface));
  border-left: 3px solid var(--tint);
}

.dcard__hd {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 14px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--line);
}
.dcard__t { font-size: var(--fs-head); margin-right: 8px; }
.dcard__sub { color: var(--ink-2); margin-right: 8px; }

/* 两栏：左边「他是谁 / 在哪儿」，右边档案。
   一栏铺下去的话，一屏看不完，而这两组本来就是并列的两件事。 */
.dgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; }
.dgrid__h {
  font-size: var(--fs-foot);
  color: var(--mute);
  font-weight: var(--w-semibold);
  margin: 0 0 8px;
}
.dsec { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--line); }

/* 在任岗位：一行一个，主岗副岗靠标签区分。
   **不用两个小标题分成两组** —— 多数人只有一个岗位，
   那样会看到一个「副岗」标题底下空着。 */
.heldposts { list-style: none; margin: 0 0 10px; padding: 0; }
.heldposts li {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 8px;
  padding: 7px 0;
  border-bottom: 1px solid var(--line);
}
.heldposts li:last-child { border-bottom: 0; }
.inl { display: inline; }

/* 「加一个岗位」这一行：标签和控件横着排，它是一次动作不是一张表。 */
.inline-add {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 6px;
}
.inline-add label { margin: 0; font-size: var(--fs-foot); color: var(--ink-2); }
.inline-add select { max-width: var(--fw-m); }
.inline-add .twoup { flex: 1 1 100%; }

/* 清单里点姓名进明细。整行可点会和「选中文字」抢手势，所以只让名字是链接。 */
.lsel__a { font-weight: var(--w-semibold); text-decoration: none; white-space: nowrap; } /* 「罗资轩-固安」1100 宽上曾在「-」处断成两行；keep-all 拦不住连字符 */
.ledger tr.on { background: color-mix(in srgb, var(--tint) 7%, transparent); }
.ledger tr.dim { color: var(--mute); }

/* ── 到岗时间轴（2026-09-08，考勤总览） ─────────────────────
   用户：「『现在有什么不对』这个改成 dashboard。**模块化图形化展示。**」

   替掉的是一张五列的表（姓名 / 最早 / 最晚 / 记录数 / 来源）。
   表要一行行读；而管理员打开当天名单想知道的三件事 ——
   **大家什么时候来的、谁只刷了一次、谁还没走** —— 在一条轴上是看出来的。

   为什么这一个可以新加（DESIGN-SYSTEM 2.2 说新页面不许再造盒子）：
   它**不是盒子** —— 没有底色、没有边、没有影，就是一条轨和一段填充，
   和 .bars 里的 .bar__v 是同一类东西（一个量画成一条形状）。
   材质仍然全部来自令牌。 */
.tline { display: grid; gap: 2px; }
.tline__r {
  display: grid;
  grid-template-columns: 5.5em minmax(0, 1fr) 6.5em;
  align-items: center;
  gap: 10px;
  padding: 5px 8px;
  border-radius: var(--r-sm);
  color: var(--ink-2);
  text-decoration: none;
  font-size: var(--fs-foot);
}
.tline__n { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: var(--w-semibold); color: var(--ink); }
.tline__v { text-align: right; color: var(--mute); font-size: var(--fs-cap); font-variant-numeric: tabular-nums; }

/* 轨：整点用一层重复渐变画出来，不用一堆空 div —— 刻度是背景，不是内容。 */
.tline__t {
  position: relative;
  height: 12px;
  border-radius: var(--r-pill);
  background: var(--surface-2);
}
.tline__b {
  position: absolute; top: 0; bottom: 0;
  min-width: 12px;              /* 短到看不见的一段，和「没有」长得一样 */
  border-radius: var(--r-pill);
  background: var(--tint);
}
/* 只有一条记录：画成一个点，**不画成一条零长的线**。
   一条极短的线会被读成「他只待了十分钟」，而事实是我们不知道他几点走的。 */
.tline__r.one .tline__b {
  width: 12px !important;
  background: var(--late);
}

/* 刻度那一行：和下面每一条共用同一个 grid，所以整点和轨是对齐的。 */
.tline__ax { padding-top: 0; padding-bottom: 2px; }
.tline__ax .tline__t {
  height: auto; background: none;
  display: flex; justify-content: space-between;
  font-size: var(--fs-cap); color: var(--mute); font-variant-numeric: tabular-nums;
}

@media (hover: hover) {
  .tline__r:hover { background: var(--surface-2); color: var(--ink); }
  .tline__ax:hover { background: none; }
}

@media (max-width: 700px) {
  .tline__r { grid-template-columns: 4.5em minmax(0, 1fr); }
  .tline__v { grid-column: 2; text-align: left; }
  .tline__ax .tline__v { display: none; }
}

/* 面板标题栏里的那个小表单（考勤总览的「看哪天」）。 */
.dpanel__f { display: flex; gap: 8px; align-items: center; }

/* ══ 通讯录：树 → 名单 → 名片（2026-09-08） ═══════════════════
   用户给了两张 macOS「通讯录」的截图：「通讯录按这个做……侧栏内容改成
   **可展开收起**的 公司（父级）、各个部门（子级）、分部（如果有、次子级），
   并且**侧栏本身也可以展开收起**。」

   抄的不是外观，是它的信息结构：通讯录只有一个动作（找到某个人），
   三栏正好把它拆成三步 —— **缩小范围 → 认出名字 → 看联系方式**。
   原来那一版是一长列分组卡片，三步全压在一次滚动里。

   ⚠️ 三栏都不是「盒子」（没有同时设底 + 边 + 角），
   所以 TestSurfaceComponentsDoNotGrow 的基线不动：
   它们是**同一块面板里的三个区**，靠一条 1px 竖线分开，不是三张卡片叠在一起。 */
.col:has(> .directory) { height: calc(100dvh - var(--frame) * 2); display: flex; flex-direction: column; }
.directory { display: flex; flex-direction: column; flex: 1; min-height: 0; min-width: 0; }
.directory > .pagehead, .directory > .note { flex: none; }
.directory > .pagehead { margin-bottom: 0; }
.dir__label { margin: 0 0 12px; color: var(--mute); font-size: var(--fs-cap); font-weight: var(--w-medium); }
.dir__title { margin: 0 0 16px; font-size: var(--fs-head); }
.dir__back { display: none; align-items: center; gap: 2px; width: fit-content; min-height: var(--tap); text-decoration: none; font-size: var(--fs-foot); }
.dir__back .gl { width: 18px; height: 18px; transform: rotate(180deg); }
.dir {
  display: grid;
  grid-template-columns: 216px minmax(260px, 1fr) minmax(300px, 360px);
  align-items: stretch;
  /* 收起 / 展开的动画就在这一条上（用户 2026-09-09）。 */
  transition: grid-template-columns var(--t-3) var(--ease-out);
  /* 用满页头下面的空间，名单长时滚名单；不再用 74vh 截出一块闲置页脚。 */
  flex: 1;
  min-height: 0;
  /* ⚠️ 光有 max-height 不够：grid 的行是按内容撑的，容器被截断而**行不会**，
     于是三栏各自 2770px 高、整块溢出到面板外面（第一版就是这样）。
     `minmax(0, 1fr)` 把行钉在容器的高度上，里面那三栏才轮得到自己滚。 */
  grid-template-rows: minmax(0, 1fr);
  margin-top: 16px;
}
/* 树自己有框了，这里就不再画竖线 —— 框旁边再一条线是两层分隔。 */
.dir__side {
  padding-right: 14px; min-width: 0; overflow: auto; border-right: 1px solid var(--line);
  transition: opacity var(--t-2) var(--ease-out), padding var(--t-3) var(--ease-out);
}
.dir__list { padding: 0 16px; border-right: 1px solid var(--line); min-width: 0; min-height: 0;
             display: flex; flex-direction: column; }
.dir__card { padding-left: 16px; min-width: 0; overflow: auto; }
.dir:not(:has(.dir__card)) .dir__list { grid-column: 2 / -1; border-right: 0; }
.directory .empty { background: transparent; border: 0; border-radius: 0; text-align: left; }
.directory .dtree { border: 0; border-radius: 0; background: transparent; padding: 0; }
.directory .dtree__a { padding-block: 8px; }
.directory .dtree__g > summary::before { margin-top: 15px; }

/* 侧栏收起：三栏变两栏。**不是 display:none 一个元素就完了** ——
   列宽是画在 .dir 上的，不跟着改的话中间和右边不会长大，
   左边只会留下一块空白。 */
/* 收起 / 展开要有动画（用户 2026-09-09）。
   原来是 `display: none` —— 那个本身就动不了，一下子消失。
   改成把那一列的宽度收到 0（grid-template-columns 可以过渡），
   内容用 overflow 裁掉。**不能只藏内容不收列** —— 列宽画在 .dir 上，
   不收的话中间和右边不会长大，左边只剩一块空白。 */
.dir.nosb { grid-template-columns: 0 minmax(260px, 1fr) minmax(300px, 360px); }
.dir.nosb .dir__side {
  opacity: 0; padding-right: 0; border-right: 0; overflow: hidden; pointer-events: none;
}
.dir.nosb .dir__list { padding-left: 0; }


/* ---- 左：可展开收起的树 ---- */
/* `<details>` 是浏览器自带的折叠，键盘和读屏都现成 ——
   自己用脚本做一遍，等于把这三样重新实现一次并且做得更差。 */
.dtree { font-size: var(--fs-foot); line-height: 1.45; }
.dtree ul { list-style: none; margin: 0; padding: 0 0 0 14px; }
.dtree > ul { padding-left: 0; }
.dtree__g > summary {
  list-style: none;                 /* 换掉默认三角，自己画一个 */
  cursor: default;
  display: block;
}
.dtree__g > summary::-webkit-details-marker { display: none; }
.dtree__a {
  display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
  padding: 5px 8px;
  border-radius: var(--r-sm);
  color: var(--ink-2);
  text-decoration: none;
}
.dtree__n { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dtree__c { flex: none; font-size: var(--fs-cap); color: var(--mute); font-variant-numeric: tabular-nums; }
.dtree__all { font-weight: var(--w-semibold); color: var(--ink); }
/* 选中：实底 + 白字。**不是一道左边框** —— 216px 宽的一栏里，
   3px 的边和没有几乎看不出来，而「我在看哪个部门」是这一栏唯一要回答的事。 */
.dtree__a.on { background: var(--tint); color: var(--on-tint); font-weight: var(--w-semibold); }
.dtree__a.on .dtree__c { color: var(--on-tint-2); }

/* 三角画在名字左边，跟着 open 转 90°。它属于 summary 不属于链接 ——
   点它只展开、不跳转，这是两个不同的动作。 */
.dtree__g > summary .dtree__a { padding-left: 22px; position: relative; }
.dtree__g > summary::before {
  content: ""; position: absolute;
  width: 0; height: 0; margin: 12px 0 0 9px;
  border: 4px solid transparent; border-left-color: var(--mute);
  transition: transform var(--t-2) var(--ease-out);
  transform-origin: 2px 4px;
}
.dtree__g[open] > summary::before { transform: rotate(90deg); }
.dtree__g > summary { position: relative; }
.dtree ul ul { border-left: 1px solid var(--line); margin-left: 12px; padding-left: 4px; }

@media (hover: hover) {
  .dtree__a:hover { background: var(--surface-2); color: var(--ink); }
  .dtree__a.on:hover { background: var(--tint); color: var(--on-tint); }
}

/* ---- 中：名单 ---- */
.dir__bar { display: flex; align-items: center; gap: 8px; padding-bottom: 10px; }
.dir__find { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
.dir__find input { flex: 1; min-width: 0; max-width: 320px; }
.dir__scroll { flex: 1; min-height: 0; overflow: auto; }
/* 分节标题贴住顶端：滚到第二十行时还知道自己在哪个部门里。 */
.dir__h {
  position: sticky; top: 0; z-index: 1;
  margin: 0; padding: 8px 6px 6px;
  background: var(--surface);
  font-size: var(--fs-cap); font-weight: var(--w-bold); letter-spacing: .04em;
  color: var(--mute); text-transform: none;
}
.dir__h span { font-weight: var(--w-medium); color: var(--mute); }
.dir__h span::before { content: " · "; }
.dir__ul { list-style: none; margin: 0 0 6px; padding: 0; }
.dirrow {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 8px;
  border-radius: var(--r-sm);
  text-decoration: none; color: inherit;
}
.dirrow__t { min-width: 0; display: flex; flex-direction: column; }
.dirrow__t b { font-weight: var(--w-semibold); }
.dirrow__t em {
  font-style: normal; font-size: var(--fs-cap); color: var(--mute);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dirrow .tag { margin-left: auto; flex: none; }
.dirrow.on { background: var(--tint); color: var(--on-tint); }
.dirrow.on .dirrow__t em { color: var(--on-tint-2); }
/* 选中那一行是实心 tint，而 .tag 是「tint-soft 底 + tint 字」—— 压上去字和底是同一个颜色（量到 1.0）。 */
.dirrow.on .tag { background: color-mix(in srgb, var(--on-tint) 18%, transparent); color: var(--on-tint); }
@media (hover: hover) { .dirrow:hover { background: var(--surface-2); } .dirrow.on:hover { background: var(--tint); } }
.dir__n { margin: 10px 0 0; padding-top: 8px; border-top: 1px solid var(--line);
          font-size: var(--fs-cap); color: var(--mute); }

/* 头像。**全站只有这一个**（模板片段 part_avatar.html）——
   有图就放图，没图就放名字第一个字，两者是同一个形状同一个重量：
   它们会出现在同一张列表的同一列里。
   底色复用流程分支那六档 `.lane-1..6`，不新造调色板、不新增令牌（043）。 */
.ava {
  flex: none;
  width: var(--ava, 30px); height: var(--ava, 30px);
  border-radius: var(--r-pill);
  display: grid; place-items: center;
  overflow: hidden;                 /* 图是方的，圆是这里裁的 */
  background: var(--lane, var(--mute)); color: var(--on-tint);
  font-size: calc(var(--ava, 30px) * .42); font-weight: var(--w-bold);
  text-decoration: none;
}
.ava img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ava--lg { --ava: 56px; }
.ava--xl { --ava: 84px; }

/* ---- 右：名片 ---- */
/* 联系信息直接落在内容面上，靠头像、标题和行线分层。 */
.vcard {
  padding: 18px 8px 20px;
  text-align: center;
}
.vcard__ava { margin: 0 auto 14px; }
.vcard__n { margin: 0; font-size: var(--fs-title); font-weight: var(--w-bold); }
.vcard__n .tag { margin-left: 8px; vertical-align: middle; }
.vcard__p { margin: 4px 0 0; color: var(--ink-2); }
.vcard__u { margin: 2px 0 0; font-size: var(--fs-cap); color: var(--mute); }
.vcard__act { display: flex; justify-content: center; gap: 10px; margin: 16px 0 18px; }
.vcard__act:empty { display: none; }

.vcard__kv { margin: 0; text-align: left; display: grid; gap: 1px; }
.vcard__kv > div {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 12px 0;
  border-bottom: 1px solid var(--line);
}
.vcard__kv dt { font-size: var(--fs-cap); color: var(--mute); flex: none; }
.vcard__kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
.vcard__kv--sub { margin-top: 10px; }
.vcard__none { margin: 0; padding: 14px 0; color: var(--mute); font-size: var(--fs-foot); }
.vcard__more { display: flex; justify-content: center; gap: 8px; margin-top: 16px; }

/* 平板保留分类列，右侧在名单和详情间替换；手机一次只展示一级。
   当前级别来自 URL，刷新、分享、浏览器返回不依赖临时 DOM。 */
@media (max-width: 1200px) {
  .directory .dir, .directory .dir.nosb { grid-template-columns: 210px minmax(0, 1fr); }
  .directory .dir__side, .directory .dir.nosb .dir__side { opacity: 1; overflow: auto; padding-right: 16px; border-right: 1px solid var(--line); pointer-events: auto; }
  .dir__list { border-right: 0; }
  .dir__bar [data-toggle-pane="dir"] { display: none; }
  .directory:not([data-stage="person"]) .dir__card,
  .directory[data-stage="person"] .dir__list { display: none; }
  .dir__card { grid-column: 2; scroll-margin-top: 72px; }
  .dir__back { display: flex; }
  .dir__card > .dir__label { display: none; }
  .dir__back--categories { display: none; }
}
@media (max-width: 768px) {
  main:has(.directory), main:has(.chat) { padding-bottom: 88px; }
  .col:has(> .directory) { height: calc(100dvh - 152px); min-height: 360px; }
}
@media (max-width: 700px) {
  .directory .dir, .directory .dir.nosb { grid-template-columns: minmax(0, 1fr); }
  .directory .dir__side, .directory .dir.nosb .dir__side { padding: 0; border: 0; }
  .directory:not([data-stage="categories"]) .dir__side,
  .directory[data-stage="categories"] .dir__list { display: none; }
  .directory .dir__card, .directory .dir__list { grid-column: 1; padding: 0; }
  .dir__back--categories { display: flex; }
  .directory .dtree__a { min-height: var(--tap); align-items: center; }
  .directory .dtree__g > summary::before { margin-top: 19px; }
}


/* ── 我的资料：选头像 ─────────────────────────────────────────────
   两个 fieldset 是用户要求的两只框：常用选择 / 上传裁切在上，系统图库在下。
   系统图库只滚自己，底部保存条一直够得着。 */
.avform { --lane: var(--lane-1); display: grid; gap: 14px; }
.avform:has(#t1:checked) { --lane: var(--lane-1); }
.avform:has(#t2:checked) { --lane: var(--lane-2); }
.avform:has(#t3:checked) { --lane: var(--lane-3); }
.avform:has(#t4:checked) { --lane: var(--lane-4); }
.avform:has(#t5:checked) { --lane: var(--lane-5); }
.avform:has(#t6:checked) { --lane: var(--lane-6); }
.avgroup {
  min-width: 0; margin: 0; padding: 16px;
  border: 1px solid var(--line); border-radius: var(--r-md);
}
.avgroup legend { padding: 0 7px; font-weight: var(--w-semibold); color: var(--ink-2); }
.avpreview { display: flex; align-items: center; gap: 18px; margin-bottom: 14px; }
.avpreview > .ava { --ava: 92px; box-shadow: 0 8px 24px color-mix(in srgb, var(--lane) 24%, transparent); }
.avpreview__set { display: grid; gap: 4px; min-width: 0; }
.avpreview__set > span { color: var(--mute); font-size: var(--fs-cap); }
.tintrow { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 7px; }
.tintdot {
  width: 30px; height: 30px; border-radius: var(--r-pill);
  background: var(--lane); cursor: pointer;
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 2px transparent;
}
.tintrow input:checked + .tintdot { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--ink); }
.tintrow input:focus-visible + .tintdot { outline: 2px solid var(--tint); outline-offset: 3px; }

.avpersonal { display: grid; grid-template-columns: repeat(3, minmax(92px, 1fr)); gap: 8px; }
.avupload { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 10px; }
.avupload .hint { margin: 0; }
/* 全站文件框本来是整行控件；这里的原生框藏在按钮后面，必须把那份宽高一起卸掉。
   只写 .sr 压不过 input[type=file]，会在弹窗右边撑出 36px 横向滚动。 */
.avform input[type="file"].sr { width: 1px; min-height: 1px; padding: 0; border: 0; }
.avcrop { display: grid; grid-template-columns: minmax(180px, 240px) minmax(150px, 1fr); gap: 18px; align-items: center; margin-top: 14px; }
.avcrop[hidden] { display: none; }
.avcrop__stage {
  position: relative; width: min(240px, 100%); aspect-ratio: 1; overflow: hidden;
  border-radius: var(--r-md); background: var(--surface-2); cursor: grab; touch-action: none;
  box-shadow: inset 0 0 0 1px var(--line);
}
.avcrop__stage:active { cursor: grabbing; }
.avcrop__stage img { position: absolute; max-width: none; user-select: none; pointer-events: none; }
.avcrop__tools { display: grid; gap: 12px; color: var(--ink-2); font-size: var(--fs-foot); }
.avcrop__tools label { display: grid; gap: 6px; }
.avcrop__tools input[type="range"] { width: 100%; }
.avsystem {
  height: 286px; overflow-y: auto; overscroll-behavior: contain; scrollbar-gutter: stable;
  padding: 2px 6px 2px 2px; border-radius: var(--r-sm);
}
.avsystem:focus-visible { outline: 2px solid var(--tint); outline-offset: 3px; }

.avgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(76px, 1fr));
  gap: 8px;
  margin: 0;
}
.avpick {
  display: grid; justify-items: center; gap: 4px;
  padding: 8px 4px;
  border-radius: var(--r-sm);
  cursor: pointer;
}
/* 人员明细标题行：头像 + 名字 + 几个标签排一行。
   不加这条的话，头像会自己占一行，标题看起来像换了行的两截。 */
.dcard__who { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

.avpick em { font-style: normal; font-size: var(--fs-cap); color: var(--mute); }
.avform input[name="avatar"]:checked + .avpick { box-shadow: inset 0 0 0 2px var(--tint); }
.avgrid input:focus-visible + .avpick { outline: 2px solid var(--tint); outline-offset: 2px; }
.avpersonal input:focus-visible + .avpick { outline: 2px solid var(--tint); outline-offset: 2px; }
@media (hover: hover) and (pointer: fine) { .avpick:hover { background: var(--surface-2); } }
@media (max-width: 560px) {
  .avgroup { padding: 12px; }
  .avpreview { align-items: flex-start; }
  .avpreview > .ava { --ava: 76px; }
  .avpersonal { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .avcrop { grid-template-columns: minmax(0, 1fr); }
  .avcrop__stage { margin-inline: auto; }
  .avsystem { height: 250px; }
}

/* ---------- 富文本（045：公告正文） ----------
   用户 2026-09-08：「就像一个精简版 word 一样。」

   .prose 是**排好版的正文长什么样**，编辑时和读的时候共用一份 ——
   两处各写一份的话，写的人看到的和收到的人看到的会是两种排版，
   而那正是「所见即所得」这四个字唯一的意义。

   编辑区本身没有自己的一套外观：它挂在上面那条 input/select/textarea
   的基础规则上（多一个选择器，不是多一块「面」）。 */
.rte { display: grid; gap: 8px; }
.rte__bar { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.rte__sep { width: 1px; height: 18px; background: var(--line); margin: 0 2px; }
.rte__doc { min-height: 15rem; max-height: 60vh; overflow-y: auto; }

.prose > :first-child { margin-top: 0; }
.prose > :last-child { margin-bottom: 0; }
.prose p { margin: 0 0 10px; line-height: 1.7; }
/* 大标题 / 小标题必须**一眼看得出不一样**，所以取的是 --fs-title 和 --fs-head：
   --fs-callout 和 --fs-body 是同一个值（0.9375rem），拿它们当两档
   会得到两个一模一样的标题，而工具条上写着「大」和「小」。 */
.prose h2 { font-size: var(--fs-title); font-weight: var(--w-bold); margin: 18px 0 6px; }
.prose h3 { font-size: var(--fs-head); font-weight: var(--w-semibold); margin: 14px 0 4px; }
.prose ul, .prose ol { margin: 0 0 10px; padding-left: 1.5em; }
.prose li { margin: 2px 0; line-height: 1.7; }
/* 缩进就是 blockquote —— execCommand("indent") 产出的正是它。
   所以不能把它画成「引用」的样子（大号引号、斜体）：它在这套系统里的
   语义是「往里缩一层」，一条淡竖线足够说明这件事。 */
.prose blockquote { margin: 0 0 10px; padding-left: 14px; border-left: 2px solid var(--line); }
.prose a { color: var(--tint); }
.prose code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.92em; }
.prose hr { border: none; border-top: 1px solid var(--line); margin: 14px 0; }
.prose img { display: block; max-width: 100%; height: auto; margin: 8px 0; border-radius: var(--r-sm); }

/* ══ 触屏放大：这一块必须是整份文件的最后一块 ══════════════════════
   分段控件 34、小按钮 36、面板头上的「全部 →」20 —— 都不到 --tap（触摸模拟下量到的）。
   只在触屏上放大，桌面版式不动；链接用内边距撑高、负外边距抵消，行高不变。
   ⚠️ 第一版把这几行写在上面那个 pointer: coarse 块里（第 490 行附近），复量时 .seg 仍是 34：
   `.seg { min-height: 34px }` 在第 3700 行，同一层类名**后写的赢**，媒体查询不加特异性。
   放大类的覆盖只能排在被覆盖的规则后面 —— 所以它住在文件末尾，守卫按位置验（finish_test.go）。 */
@media (pointer: coarse) {
  .seg, button.sm, .btn.sm, .opline button.sm { min-height: var(--tap); }
  .dpanel__hd a { padding: 10px 0; margin: -10px 0; }
}

/* 会话与正文各滚自己的内容；玻璃只给工具栏，消息复用主题令牌。 */
.col:has(> .chat) { height: calc(100dvh - var(--frame) * 2); min-height: 420px; padding: 0; overflow: hidden; }
.chat { --chat-height: 100dvh; display: grid; grid-template-columns: minmax(0, 1fr); height: 100%; min-height: 0; }
.chat__side { display: flex; flex-direction: column; min-height: 0; min-width: 0; padding: 20px 18px 12px; }
.chat:not([data-stage="list"]) .chat__side { display: none; }
.chat__head { display: flex; align-items: center; gap: 12px; min-height: 62px; padding: 0 10px 12px; flex: none; }
.chat__head h1, .chat__head h2 { margin: 0; }
.chat__head .iconbtn { margin-left: auto; flex: none; }
.chat__side > input { margin: 0 6px 12px; width: calc(100% - 12px); }
.chat__rooms { overflow: auto; flex: 1; min-height: 0; }
.chatroom { display: flex; align-items: center; gap: 12px; padding: 14px 10px; text-decoration: none; color: var(--ink); border-radius: var(--r-md); }
@media (hover: hover) and (pointer: fine) { .chatroom:hover { background: var(--tint-soft); } }
.chatroom.on { background: var(--tint); color: var(--on-tint); }
.chatroom__text { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1; }
.chatroom__text b, .chatroom__text > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chatroom__text > span { font-size: var(--fs-foot); color: var(--mute); }
.chatroom.on .chatroom__text > span { color: var(--on-tint-2); }
.chatroom[hidden], .chatperson[hidden] { display: none; }
.chat__conversation { display: flex; flex-direction: column; min-height: 0; min-width: 0; position: relative; }
.chat__conversation > .chat__head { padding: 16px 24px; border-bottom: 1px solid var(--line); background: var(--mat-reg); backdrop-filter: var(--blur); }
.chat__heading { min-width: 0; flex: 1; }
.chat__title { font-size: var(--fs-head); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chat__back { display: flex; align-items: center; text-decoration: none; min-height: var(--tap); flex: none; width: var(--tap); justify-content: center; }
.chat__back .gl { transform: rotate(180deg); width: 18px; height: 18px; }
.chat__messages { flex: 1; min-height: 0; overflow: auto; padding: 24px clamp(16px, 4vw, 56px); overscroll-behavior: contain; overflow-anchor: none; }
.chat__stream { display: flex; flex-direction: column; gap: 18px; }
.chatmsg { display: flex; align-items: flex-start; max-width: min(86%, 680px); align-self: flex-start; gap: 10px; }
.chatmsg--mine { flex-direction: row-reverse; align-self: flex-end; }
.chatmsg__content { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; min-width: 0; }
.chatmsg--mine .chatmsg__content { align-items: flex-end; }
.chatmsg__body { background: var(--surface-2); color: var(--ink); border-radius: var(--r-lg); padding: 10px 15px; white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.55; }
.chatmsg--mine .chatmsg__body { background: var(--tint); color: var(--on-tint); }
.chatmsg__name, .chatmsg__time { color: var(--mute); font-size: var(--fs-cap); padding-inline: 6px; }
.chatmsg--system { align-self: center; max-width: 90%; color: var(--mute); font-size: var(--fs-cap); text-align: center; }
.chatmsg--system p { margin: 4px 0; }
.chatmsg[data-status="failed"] .chatmsg__body { color: var(--bad); }
.chat__history { display: block; text-align: center; padding-bottom: 22px; font-size: var(--fs-foot); }
.chat__status { min-height: 0; margin: 0 24px; font-size: var(--fs-foot); }
.chat__status:empty { display: none; }
.chat__compose { display: flex; align-items: flex-end; gap: 10px; padding: 16px 24px 22px; flex: none; }
.chat__compose textarea { flex: 1; min-width: 0; min-height: var(--tap); max-height: 140px; resize: none; border-radius: var(--r-lg); margin: 0; padding: 10px 15px; scrollbar-width: none; }
.chat__compose textarea::-webkit-scrollbar { display: none; }
.chat__compose .chat__send { border-radius: var(--r-pill); width: var(--tap); height: var(--tap); padding: 0; flex: none; }
.chat__send .gl { width: 22px; height: 22px; }
.chat__mention[hidden], .chat__new[hidden], .chat__empty[hidden] { display: none; }
.chat__new { align-self: center; margin: 6px; }
.chat__start { display: flex; flex-direction: column; gap: 12px; }
.chat__people { max-height: 30dvh; overflow: auto; }
.chatperson { display: flex; align-items: center; gap: 12px; min-height: 54px; padding: 6px 4px; border-bottom: 1px solid var(--line); }
.chatperson > input { width: 18px; height: 18px; margin: 0; flex: none; }
.chatperson > form { margin-left: auto; }
.chat__members { max-height: 28dvh; overflow: auto; }
.chat__setting { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-block: 16px; }
.chat__setting label { flex-basis: 100%; font-size: var(--fs-foot); }
.chat__setting input, .chat__setting select { flex: 1; min-width: 0; }
.chat + .prefs details { margin-top: 22px; }
@media (max-width: 1200px) {
  .chat { grid-template-columns: minmax(0, 1fr); }
  .chat__conversation > .chat__head, .chat__compose { padding-inline: 16px; }
}
@media (max-width: 768px) {
  .col:has(> .chat) { height: calc(100dvh - 152px); min-height: 300px; }
}
@media (max-width: 700px) {
  .chat { grid-template-columns: minmax(0, 1fr); }
  .chat__side { border: 0; background: transparent; }
  .chat__back { display: flex; flex: none; }
  .chat__head { gap: 8px; }
  .chat__conversation > .chat__head { padding-block: 8px; }
  .chat__title { font-size: var(--fs-body); }
  .chat__compose { gap: 8px; padding: 10px 12px 14px; }
  .chat__messages { padding: 20px 14px; }
  .chatmsg { max-width: 90%; }
}

.chat__retry { margin: 0; }

/* 提示挂在文档层，避免被可滚动的侧栏导航裁掉；内容来自入口自身的名字。 */
.sidehint { position: fixed; z-index: 150; pointer-events: none; padding: 7px 12px; border-radius: var(--r-sm); background: var(--ink); color: var(--surface); box-shadow: var(--shadow); font-size: var(--fs-foot); white-space: nowrap; }
.sidehint[hidden] { display: none; }

.chat__actions { margin-left: auto; }
.chat__actions > summary, .chat__tools > button { width: var(--tap); height: var(--tap); min-height: var(--tap); padding: 0; border: 0; border-radius: var(--r-pill); justify-content: center; background: var(--surface-2); }
.chat__actions .gl, .chat__tools .gl { width: 22px; height: 22px; }
.chat__tools { flex: none; position: relative; }
.chat__tools .menu__panel { top: auto; bottom: calc(100% + 8px); left: 0; right: auto; max-height: min(460px, 65dvh); overflow: auto; transform-origin: bottom left; }
.chat__info { display: flex; flex-direction: column; min-height: 0; }
.chat__info > .chat__head { padding: 16px 24px; border-bottom: 1px solid var(--line); }
.chat__info-body { overflow: auto; padding: 0 24px 24px; }
.chat__info-body > .chat__setting, .chat__info-body > .hint { max-width: 700px; }
.chat__info-body details { margin-top: 22px; }
.chatmsg__body img, .chatmsg__body video { display: block; max-width: 100%; max-height: 360px; border-radius: var(--r-md); object-fit: contain; }
.chatmsg__body audio { display: block; width: 260px; max-width: 100%; }
.chatmsg__body:has(img, video, audio) { padding: 0; background: transparent; white-space: normal; }
.chatmsg__file { display: flex; gap: 10px; align-items: center; color: inherit; text-decoration: none; white-space: normal; }
.chatmsg__file .gl { width: 24px; height: 24px; flex: none; }
.chatmsg__file small { display: block; font-size: var(--fs-cap); margin-top: 5px; }
.chat__file-input[hidden] { display: none; }
.chat__media-preview img, .chat__media-preview video { display: block; max-width: 100%; max-height: 40dvh; margin: 16px auto; border-radius: var(--r-md); }
.chat__media-name { overflow-wrap: anywhere; }
.chat__voice { flex: none; padding: 12px 24px 20px; border-top: 1px solid var(--line); }
.chat__voice[hidden], .chat__compose[hidden], .chat__voice [hidden] { display: none; }
.chat__recording { display: flex; align-items: center; gap: 6px; min-height: 48px; color: var(--ink); }
.chat__recording canvas { flex: 1; min-width: 0; height: 40px; }
.chat__recording output { font-variant-numeric: tabular-nums; font-size: var(--fs-foot); }
.chat__recording button { flex: none; }
.chat__recording [data-voice-stop], .chat__voice[data-state="recording"] output { color: var(--bad); }
.chat__voice > p { margin: 6px 0; font-size: var(--fs-cap); }
.chat__voice [data-voice-send] { width: var(--tap); height: var(--tap); padding: 0; border-radius: var(--r-pill); }
.chat__voice .gl { width: 20px; height: 20px; }
.chat__voice [data-voice-record] { padding-inline: 4px; font-variant-numeric: tabular-nums; white-space: nowrap; }
.chat__voice [data-voice-status]:empty { display: none; }
.chat__voice [data-voice-pause-icon], .chat__voice[data-playing="true"] [data-voice-play-icon] { display: none; }
.chat__voice[data-playing="true"] [data-voice-pause-icon] { display: initial; }
.chatmsg > .ava { flex: none; width: 38px; height: 38px; border-radius: var(--r-sm); }
.chatmsg__content:has(audio, video, img) { max-width: calc(100% - 48px); }
/* 手机进入会话后使用自己的导航；键盘升起也只压缩消息区。 */
@media (max-width: 768px) {
  body:has(.chat:not([data-stage="list"])) { overflow: hidden; }
  body:has(.chat:not([data-stage="list"])) > .topbar,
  body:has(.chat:not([data-stage="list"])) > .tabbar,
  body:has(.chat:not([data-stage="list"])) > .side { display: none; }
  body main:has(.chat:not([data-stage="list"])) { position: fixed; inset: 0; margin: 0; padding: 0; height: var(--chat-height, 100dvh); }
  .page-wrap:has(.chat:not([data-stage="list"])) { height: 100%; }
  .col:has(> .chat:not([data-stage="list"])) { height: 100%; min-height: 0; border: 0; border-radius: 0; box-shadow: none; }
  .chat__conversation > .chat__head, .chat__info > .chat__head { padding-top: max(8px, env(safe-area-inset-top)); }
  .chat__compose, .chat__voice { padding-bottom: max(12px, env(safe-area-inset-bottom)); }
  .chat__voice { padding-inline: 14px; }
}
.chat__emoji-grid { display: grid; grid-template-columns: repeat(6,minmax(0,1fr)); gap: 4px; margin-top: 16px; }
.chat__emoji-grid button { font-size: var(--fs-title); padding: 0; min-width: 0; min-height: var(--tap); background: transparent; border: 0; }
.directory[data-from-chat] .dir__side, .directory[data-from-chat] .dir__list { display: none; }
.directory[data-from-chat] .dir { grid-template-columns: minmax(0,1fr); }
.directory[data-from-chat] .dir__card { grid-column: 1; padding: 0; }
.directory[data-from-chat] .dir__back { display: flex; }
.directory[data-from-chat] .dir__card > .dir__label { display: none; }

[data-ai-custom][hidden] { display: none; }
/* Assistant: a quiet history column, one reading surface, and a bottom composer. */
body:has(.assistant) { overflow: hidden; }
.col:has(> .assistant) { height: calc(100dvh - var(--frame) * 2); min-height: 460px; padding: 0; overflow: hidden; }
.assistant { display: grid; grid-template-columns: 248px minmax(0, 1fr); height: 100%; min-height: 0; position: relative; }
.assistant [hidden] { display: none !important; }
.assistant__history { display: flex; flex-direction: column; gap: 10px; min-height: 0; padding: 20px 14px 14px; border-right: 1px solid var(--line); background: var(--surface-2); }
.assistant__brand { display: flex; align-items: center; gap: 10px; min-height: 40px; padding-inline: 8px; font-size: var(--fs-head); }
.assistant__brand .gl { color: var(--tint); }
.assistant__brand oa-ai-star { margin-left: -2px; }
.assistant__new { justify-content: center; gap: 9px; min-height: var(--tap); padding-inline: 14px; border-color: color-mix(in srgb, var(--tint) 20%, var(--line)); border-radius: var(--r-md); background: color-mix(in srgb, var(--surface) 88%, transparent); }
.assistant__new .gl { width: 18px; height: 18px; }
.assistant__search { display: grid; grid-template-columns: 20px minmax(0,1fr); align-items: center; gap: 7px; min-height: var(--tap); padding: 0 12px; border: 1px solid transparent; border-radius: var(--r-md); background: var(--surface); }
.assistant__search:focus-within { border-color: color-mix(in srgb, var(--tint) 50%, var(--line)); box-shadow: 0 0 0 3px color-mix(in srgb, var(--tint) 8%, transparent); }
.assistant__search .gl { width: 17px; height: 17px; color: var(--mute); }
.assistant__history .assistant__search input { width: 100%; min-width: 0; padding: 0; border: 0; background: transparent; box-shadow: none; font-size: var(--fs-foot); }
.assistant__caption { margin: 8px 10px 2px; color: var(--mute); font-size: var(--fs-cap); font-weight: var(--w-semibold); letter-spacing: .02em; }
.assistant__threads { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
.assistant__thread { display: grid; grid-template-columns: minmax(0,1fr) 36px; align-items: center; gap: 2px; min-height: 42px; margin-bottom: 2px; border-radius: var(--r-sm); }
.assistant__thread > a { display: block; min-width: 0; padding: 10px; color: var(--ink-2); text-decoration: none; border-radius: var(--r-sm); white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }
.assistant__thread:has(> a[aria-current]) { background: var(--surface); }
.assistant__thread > a[aria-current] { color: var(--ink); font-weight: var(--w-semibold); }
.assistant__thread form { margin: 0; }
.assistant__thread-delete { width: 32px; height: 32px; color: var(--mute); background: transparent; opacity: 0; }
.assistant__thread-delete .gl { width: 17px; height: 17px; }
.assistant__thread:focus-within .assistant__thread-delete { opacity: 1; }
.assistant__resources { display: grid; gap: 2px; padding-top: 10px; border-top: 1px solid var(--line); }
.assistant__resources a { display: grid; grid-template-columns: minmax(0,1fr) 18px; align-items: center; min-height: 42px; padding-inline: 10px 8px; border-radius: var(--r-sm); color: var(--mute); font-size: var(--fs-foot); text-decoration: none; }
.assistant__resources .gl { width: 16px; height: 16px; justify-self: end; }
.assistant__conversation { display: flex; flex-direction: column; min-height: 0; min-width: 0; position: relative; isolation: isolate; overflow: hidden; }
/* AI 光场分两条互不冒充的时间线：进入页面只播一次底部波场；真正生成时才让顶部蓝 / 绿 / 银持续流动。 */
.assistant__aurora { --ai-flow-blue: #3158ff; --ai-flow-green: #00b982; --ai-flow-silver: #e9f7ff; position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.assistant__aurora > span { position: absolute; display: block; opacity: 0; pointer-events: none; will-change: transform, opacity; }
.assistant__aurora-entry {
  left: -34%; bottom: -24%; width: 168%; height: 69%; transform-origin: 50% 100%;
  filter: saturate(122%);
}
.assistant__aurora-entry-blue {
  background:
    radial-gradient(ellipse 66% 96% at 18% 104%, color-mix(in srgb, var(--ai-flow-blue) 61%, transparent) 0, color-mix(in srgb, var(--ai-flow-blue) 27%, transparent) 43%, transparent 74%),
    radial-gradient(ellipse 48% 74% at 76% 116%, color-mix(in srgb, #7698ff 25%, transparent) 0, transparent 70%);
  -webkit-mask-image: radial-gradient(ellipse 76% 85% at 18% 106%, #000 0 48%, rgb(0 0 0 / .76) 68%, transparent 86%), radial-gradient(ellipse 60% 68% at 78% 112%, #000 0 44%, transparent 83%);
  mask-image: radial-gradient(ellipse 76% 85% at 18% 106%, #000 0 48%, rgb(0 0 0 / .76) 68%, transparent 86%), radial-gradient(ellipse 60% 68% at 78% 112%, #000 0 44%, transparent 83%);
}
.assistant__aurora-entry-green {
  background:
    radial-gradient(ellipse 68% 100% at 84% 108%, color-mix(in srgb, var(--ai-flow-green) 56%, transparent) 0, color-mix(in srgb, var(--ai-flow-green) 24%, transparent) 42%, transparent 75%),
    radial-gradient(ellipse 44% 70% at 34% 120%, color-mix(in srgb, #70e8c1 23%, transparent) 0, transparent 68%);
  -webkit-mask-image: radial-gradient(ellipse 72% 84% at 84% 108%, #000 0 46%, rgb(0 0 0 / .72) 67%, transparent 85%), radial-gradient(ellipse 54% 62% at 28% 116%, #000 0 42%, transparent 82%);
  mask-image: radial-gradient(ellipse 72% 84% at 84% 108%, #000 0 46%, rgb(0 0 0 / .72) 67%, transparent 85%), radial-gradient(ellipse 54% 62% at 28% 116%, #000 0 42%, transparent 82%);
}
.assistant__aurora-entry-silver {
  background:
    radial-gradient(circle, color-mix(in srgb, var(--ai-flow-silver) 60%, transparent) 0 1.2px, transparent 1.7px) 50% 50% / 13px 13px,
    radial-gradient(ellipse 64% 70% at 50% 112%, color-mix(in srgb, var(--ai-flow-silver) 24%, transparent) 0, transparent 70%);
  -webkit-mask-image: radial-gradient(ellipse 72% 68% at 50% 113%, #000 0 30%, rgb(0 0 0 / .82) 56%, transparent 82%);
  mask-image: radial-gradient(ellipse 72% 68% at 50% 113%, #000 0 30%, rgb(0 0 0 / .82) 56%, transparent 82%);
}
.assistant__aurora-top { top: -24%; left: -39%; width: 178%; height: 82%; transform-origin: 50% 16%; filter: saturate(118%); }
.assistant__aurora-blue {
  background:
    radial-gradient(ellipse 58% 76% at 18% 20%, color-mix(in srgb, var(--ai-flow-blue) 54%, transparent) 0, color-mix(in srgb, var(--ai-flow-blue) 25%, transparent) 42%, transparent 74%),
    radial-gradient(ellipse 46% 66% at 70% 4%, color-mix(in srgb, #6b94ff 28%, transparent) 0, transparent 69%);
  -webkit-mask-image: radial-gradient(ellipse 66% 74% at 16% 8%, #000 0 47%, rgb(0 0 0 / .76) 69%, transparent 88%), radial-gradient(ellipse 54% 62% at 74% 0, #000 0 42%, transparent 84%);
  mask-image: radial-gradient(ellipse 66% 74% at 16% 8%, #000 0 47%, rgb(0 0 0 / .76) 69%, transparent 88%), radial-gradient(ellipse 54% 62% at 74% 0, #000 0 42%, transparent 84%);
}
.assistant__aurora-green {
  background:
    radial-gradient(ellipse 62% 78% at 84% 16%, color-mix(in srgb, var(--ai-flow-green) 51%, transparent) 0, color-mix(in srgb, var(--ai-flow-green) 22%, transparent) 41%, transparent 74%),
    radial-gradient(ellipse 43% 61% at 38% 0, color-mix(in srgb, #56e6b8 25%, transparent) 0, transparent 68%);
  -webkit-mask-image: radial-gradient(ellipse 67% 76% at 86% 8%, #000 0 46%, rgb(0 0 0 / .72) 68%, transparent 87%), radial-gradient(ellipse 50% 58% at 34% 0, #000 0 40%, transparent 83%);
  mask-image: radial-gradient(ellipse 67% 76% at 86% 8%, #000 0 46%, rgb(0 0 0 / .72) 68%, transparent 87%), radial-gradient(ellipse 50% 58% at 34% 0, #000 0 40%, transparent 83%);
}
.assistant__aurora-silver {
  background:
    radial-gradient(ellipse 34% 52% at 22% 18%, color-mix(in srgb, var(--ai-flow-silver) 36%, transparent) 0, color-mix(in srgb, var(--ai-flow-silver) 14%, transparent) 38%, transparent 72%),
    radial-gradient(ellipse 42% 58% at 76% 8%, color-mix(in srgb, var(--ai-flow-silver) 29%, transparent) 0, transparent 69%),
    radial-gradient(ellipse 60% 70% at 48% -2%, transparent 0 48%, color-mix(in srgb, var(--ai-flow-silver) 15%, transparent) 56%, transparent 67%);
  -webkit-mask-image: radial-gradient(ellipse 58% 72% at 20% 4%, #000 0 44%, rgb(0 0 0 / .72) 65%, transparent 86%), radial-gradient(ellipse 54% 64% at 78% 0, #000 0 42%, transparent 84%);
  mask-image: radial-gradient(ellipse 58% 72% at 20% 4%, #000 0 44%, rgb(0 0 0 / .72) 65%, transparent 86%), radial-gradient(ellipse 54% 64% at 78% 0, #000 0 42%, transparent 84%);
}
.assistant[data-ai-motion="thinking"] .assistant__aurora-blue { opacity: .72; }
.assistant[data-ai-motion="thinking"] .assistant__aurora-silver { opacity: .72; }
.assistant[data-ai-motion="answering"] .assistant__aurora-blue { opacity: .72; }
.assistant[data-ai-motion="answering"] .assistant__aurora-green { opacity: .72; }
.assistant[data-ai-motion="answering"] .assistant__aurora-silver { opacity: .72; }
/* Native-pixel curved light sheets replace fixed gradients where Canvas is available. */
.assistant[data-ai-light-renderer="canvas"] .assistant__aurora > span { display: none; will-change: auto; }
.assistant__light-canvas { position: absolute; left: 0; width: 100%; pointer-events: none; }
.assistant__light-canvas--bottom { bottom: 0; height: clamp(210px, 40%, 360px); }
.assistant__light-canvas--top { top: 0; height: clamp(180px, 34%, 300px); }
.assistant__head, .assistant__scroll, .assistant__bottom { position: relative; z-index: 1; }
.assistant__head { display: flex; align-items: center; gap: 12px; padding: 14px 26px; min-height: 68px; border-bottom: 1px solid var(--line); }
.assistant__head h1 { flex: 1; min-width: 0; margin: 0; font-size: var(--fs-body); font-weight: var(--w-semibold); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.assistant__head .iconbtn { flex: none; }
.assistant__history-toggle, .assistant__close, .assistant__back { display: none; }
.assistant__head-actions { display: flex; align-items: center; gap: 6px; }
.assistant__scroll { flex: 1; min-height: 0; overflow: auto; overscroll-behavior: contain; overflow-anchor: none; padding: 32px clamp(20px, 4vw, 64px) 24px; }
.assistant__stream { max-width: 780px; margin-inline: auto; }
.assistant__welcome { padding-top: clamp(28px, 8vh, 88px); }
.assistant__welcome > oa-ai-star { display: block; margin: 0 0 18px -7px; }
.assistant__welcome h2 { font-size: var(--fs-large); letter-spacing: -0.025em; margin: 0 0 10px; }
.assistant__welcome > p { color: var(--mute); margin: 0; }
.assistant__suggestions { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; margin-top: 30px; }
.assistant__suggestions button { display: flex; align-items: center; gap: 12px; text-align: left; white-space: normal; padding: 10px 0; color: var(--ink-2); min-height: var(--tap); border: 0; border-radius: 0; background: transparent; font-weight: var(--w-medium); }
.assistant__suggestions .gl { width: 19px; height: 19px; flex: none; color: var(--mute); }
.assistant__turn { margin-bottom: 40px; overflow-wrap: anywhere; }
.assistant__question { width: fit-content; max-width: 90%; margin-left: auto; margin-bottom: 24px; padding: 12px 18px; border-radius: var(--r-lg); background: var(--surface-2); white-space: pre-wrap; line-height: 1.7; }
.assistant__byline { display: flex; gap: 9px; align-items: center; color: var(--ink-2); font-size: var(--fs-foot); font-weight: var(--w-semibold); }
.assistant__byline .gl { width: 18px; height: 18px; color: var(--tint); }
.assistant__byline oa-ai-star { margin-left: -8px; }
.assistant__activity { margin: 14px 0; color: var(--mute); font-size: var(--fs-foot); }
.assistant__activity summary { cursor: pointer; min-height: 28px; }
.assistant__activity p, .assistant__activity ol { margin: 8px 0; padding-left: 18px; line-height: 1.9; }
.assistant__turn.is-generating [data-ai-retry] { display: none; }
.assistant__thinking { display: inline-block; color: var(--mute); }
.assistant__turn.is-generating .assistant__thinking { color: transparent; background: linear-gradient(105deg, var(--mute) 18%, var(--mute) 34%, var(--ink) 48%, var(--mute) 62%, var(--mute) 82%); background-size: 260% 100%; background-position: 130% 0; -webkit-background-clip: text; background-clip: text; animation: assistant-shimmer calc(var(--t-4) * 5) linear infinite; } /* impeccable-disable-line gradient-text: requested Codex-style generation-state shimmer, not emphasis */
@keyframes assistant-shimmer { to { background-position: -130% 0; } }
.assistant__body { line-height: 1.85; color: var(--ink); overflow-wrap: anywhere; }
.assistant__body.is-plain { white-space: pre-wrap; }
.assistant__body > :first-child { margin-top: 0; }
.assistant__body h1, .assistant__body h2, .assistant__body h3 { font-size: var(--fs-head); margin: 28px 0 12px; letter-spacing: -0.012em; }
.assistant__body p { margin: 0 0 14px; }
.assistant__body ul, .assistant__body ol { padding-left: 24px; margin-block: 14px; }
.assistant__body li { margin-block: 5px; }
.assistant__body a { color: var(--tint); text-decoration-line: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; }
.assistant__body a:focus-visible { outline: 2px solid var(--tint); outline-offset: 3px; border-radius: 2px; }
.assistant__body pre { max-width: 100%; overflow: auto; margin: 18px 0; padding: 16px 18px; border: 1px solid var(--line); border-radius: var(--r-md); background: var(--surface-2); font-size: var(--fs-foot); line-height: 1.65; }
.assistant__body code { padding: 2px 5px; border-radius: var(--r-sm); background: var(--surface-2); font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: var(--fs-foot); white-space: pre-wrap; }
.assistant__body pre code { padding: 0; background: transparent; white-space: pre; }
.assistant__body table { display: block; width: max-content; min-width: 100%; max-width: 100%; overflow-x: auto; margin: 18px 0; border: 1px solid var(--line); border-spacing: 0; border-radius: var(--r-md); font-size: var(--fs-foot); font-variant-numeric: tabular-nums; }
.assistant__body th, .assistant__body td { min-width: 120px; padding: 10px 13px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }
.assistant__body th { color: var(--ink-2); background: var(--surface-2); font-weight: var(--w-semibold); }
.assistant__body tr:nth-child(even) td { background: color-mix(in srgb, var(--surface-2) 58%, transparent); }
.assistant__body tr > :last-child { border-right: 0; }
.assistant__body tbody tr:last-child td { border-bottom: 0; }
.assistant__body blockquote { margin: 18px 0; padding: 13px 16px; border-radius: var(--r-sm); color: var(--ink-2); background: var(--surface-2); }
.assistant__body blockquote > :last-child { margin-bottom: 0; }
.assistant__file-link { display: inline-flex; align-items: center; gap: 6px; margin-block: 3px; padding: 5px 9px; border-radius: var(--r-pill); background: var(--surface-2); font-weight: var(--w-medium); text-decoration: none !important; }
.assistant__file-link::before { content: '↓'; color: var(--mute); }
.assistant__image { display: flex; flex-direction: column; align-items: flex-start; text-align: left; min-height: 80px; padding: 14px 0; max-width: 100%; white-space: normal; }
.assistant__image small { color: var(--mute); }
.assistant__body img { display: block; max-width: 100%; max-height: 420px; object-fit: contain; border-radius: var(--r-md); }
.assistant__citations { color: var(--mute); font-size: var(--fs-foot); margin-top: 20px; }
.assistant__citations summary { cursor: pointer; min-height: 32px; font-weight: var(--w-medium); }
.assistant__citations ol { display: grid; gap: 8px; padding: 0; list-style: none; }
.assistant__citations li { padding: 12px 14px; border-radius: var(--r-sm); background: var(--surface-2); }
.assistant__citations li > a { color: var(--ink); font-weight: var(--w-semibold); text-underline-offset: 3px; }
.assistant__citations li > span { color: var(--mute); font-size: var(--fs-cap); }
.assistant__citations p { display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 3; white-space: pre-wrap; color: var(--ink-2); font-size: var(--fs-cap); line-height: 1.65; margin: 7px 0 0; }
.assistant__problem { color: var(--bad); line-height: 1.7; }
.assistant__reply-actions { display: flex; gap: 8px; margin-top: 14px; }
.assistant__reply-actions button { color: var(--mute); }
.assistant__bottom { flex: none; padding: 0 clamp(20px, 4vw, 64px) 12px; position: relative; }
.assistant__ask { --composer-pad: 4px; --composer-control: 42px; display: grid; grid-template-columns: var(--composer-control) minmax(0,1fr) var(--composer-control); grid-template-areas: "attach input send"; align-items: end; gap: 4px; max-width: 780px; margin-inline: auto; padding: var(--composer-pad); border: 1px solid var(--line); border-radius: var(--r-pill); background: color-mix(in srgb, var(--surface) 78%, transparent); -webkit-backdrop-filter: blur(24px) saturate(170%); backdrop-filter: blur(24px) saturate(170%); box-shadow: var(--shadow-card); }
.assistant__ask.has-attachment { grid-template-areas: "pending pending pending" "attach input send"; border-radius: var(--r-composer); }
.assistant__ask:focus-within { border-color: color-mix(in srgb, var(--tint) 55%, var(--line)); box-shadow: 0 0 0 3px color-mix(in srgb, var(--tint) 8%, transparent), var(--shadow-card); }
.assistant__ask textarea { grid-area: input; width: 100%; min-height: var(--composer-control); max-height: 180px; resize: none; overflow-y: auto; scrollbar-width: none; padding: 8px 5px; border: 0; border-radius: 0; background: transparent; outline: none; box-shadow: none; line-height: 1.45; }
.assistant__ask textarea::-webkit-scrollbar { display: none; }
.assistant__ask > .iconbtn { width: var(--composer-control); height: var(--composer-control); min-height: var(--composer-control); border-radius: 50%; }
.assistant__attach { grid-area: attach; }
.assistant__ask > .assistant__attach { color: var(--mute); background: transparent; }
.assistant__ask > [data-ai-send], .assistant__ask > [data-ai-stop] { grid-area: send; justify-self: end; }
.assistant__connection { display: none; }
.assistant__footnote { text-align: center; color: var(--mute); font-size: var(--fs-cap); margin: 9px 0 0; }
.assistant__latest { position: absolute; bottom: calc(100% + 10px); left: 50%; transform: translateX(-50%); background: var(--surface); box-shadow: var(--shadow); }

@media (hover: hover) and (pointer: fine) {
  .assistant__thread:hover { background: var(--tint-soft); }
  .assistant__thread:hover .assistant__thread-delete { opacity: 1; }
  .assistant__resources a:hover, .assistant__suggestions button:hover { color: var(--tint); }
}
@media (hover: none) { .assistant__thread-delete { opacity: 1; } }
@media (max-width: 1050px) {
  .assistant { grid-template-columns: 208px minmax(0, 1fr); }
  .assistant__history { padding: 20px 12px 14px; }
}
@media (max-width: 768px) {
  body:has(.assistant) > .topbar, body:has(.assistant) > .tabbar, body:has(.assistant) > .side { display: none; }
  body main:has(.assistant) { position: fixed; inset: 0; height: var(--assistant-height, 100dvh); margin: 0; padding: 0; transform: translateY(var(--assistant-top, 0px)); }
  .page-wrap:has(.assistant) { height: 100%; }
  .col:has(> .assistant) { height: 100%; min-height: 0; border: 0; border-radius: 0; box-shadow: none; background: var(--canvas); }
  .assistant { grid-template-columns: minmax(0, 1fr); }
  .assistant__history { position: absolute; inset: 0 auto 0 0; width: min(86%, 340px); z-index: 3; visibility: hidden; transform: translateX(-100%); transition: transform var(--t-3) var(--ease-out), visibility var(--t-3); padding: max(12px, env(safe-area-inset-top)) 16px max(16px, env(safe-area-inset-bottom)); }
  .assistant.is-history-open .assistant__history { visibility: visible; transform: none; }
  .assistant__history[data-drawer-ready] { transition: none; }
  .assistant__scrim { position: absolute; inset: 0; z-index: 2; width: 100%; border: 0; border-radius: 0; background: color-mix(in srgb, var(--ink) 24%, transparent); }
  .assistant__history-toggle, .assistant__close, .assistant__back { display: inline-flex; }
  .assistant__close { margin-left: auto; }
  .assistant__brand { touch-action: pan-y; }
  .assistant__head { min-height: 64px; padding: max(8px, env(safe-area-inset-top)) 12px 8px; gap: 10px; border: 0; }
  .assistant__head .iconbtn { width: 44px; height: 44px; background: var(--surface-2); border-radius: var(--r-pill); }
  .assistant__head .gl { width: 22px; height: 22px; }
  .assistant__scroll { padding: 16px 20px 24px; }
  .assistant__welcome { padding-top: clamp(20px, 8dvh, 68px); }
  .assistant__welcome h2 { font-size: var(--fs-display); letter-spacing: -0.035em; }
  .assistant__welcome > p { font-size: var(--fs-sub); }
  .assistant__suggestions { margin-top: 24px; gap: 8px; }
  .assistant__suggestions button { font-size: var(--fs-sub); }
  .assistant__question { max-width: 92%; padding: 12px 16px; }
  .assistant__aurora-entry { left: -52%; bottom: -19%; width: 204%; height: 64%; }
  .assistant__aurora-top { top: -18%; left: -49%; width: 198%; height: 72%; }
  .assistant__thread-delete { opacity: 1; }
}


.assistant__text { white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.8; }
.assistant__sources { list-style: none; padding: 0; margin-block: 24px; }
.assistant__sources > li { padding-block: 14px; border-bottom: 1px solid var(--line); overflow-wrap: anywhere; }

/* 两端共用加号菜单；桌面把加号、语音和发送排在输入区底部。 */
.chat__idle { display: none; }
.chat__tools .chat__tool-list { width: min(280px, calc(100vw - 28px)); padding: 10px; visibility: hidden; opacity: 0; transform: translateY(8px) scale(0.97); pointer-events: none; transition: opacity var(--t-2) var(--ease-out), transform var(--t-2) var(--ease-out), visibility var(--t-2); }
.chat__tools.open .chat__tool-list { visibility: visible; opacity: 1; transform: none; pointer-events: auto; }
.chat__tools.open > button .gl { transform: rotate(45deg); }
.chat__tool-list > button { display: flex; align-items: center; justify-content: flex-start; gap: 16px; width: 100%; min-height: 58px; padding: 8px 12px; color: var(--ink); font-size: var(--fs-head); border: 0; text-align: left; background: transparent; }
.chat__tool-list > button:active { transform: scale(0.97); }
.chat__tool-list > button[hidden] { display: none; }
.chat__tool-list > button .gl { width: 40px; height: 40px; padding: 9px; flex: none; border-radius: var(--r-pill); color: var(--tint); background: var(--tint-soft); }
.chat__tool-list [data-chat-tool="voice"] .gl { color: var(--late); background: var(--late-bg); }
.chat__tool-list [data-chat-file="image"] .gl { color: var(--ok); background: var(--ok-bg); }
.chat__tool-list [data-chat-tool="contact"] .gl { color: var(--lane-2); }
.chat__conversation > .chat__head { background: var(--surface); backdrop-filter: none; }
@media (min-width: 769px) {
  .chat { grid-template-columns: clamp(250px, 26vw, 320px) minmax(0, 1fr); }
  .chat:not([data-stage="list"]) .chat__side { display: flex; }
  .chat__side { border-right: 1px solid var(--line); background: var(--surface-2); padding: 18px 10px 12px; }
  .chat__head h1 { font-size: var(--fs-head); }
  .chat__conversation > .chat__head > .chat__back { display: none; }
  .chat__idle { display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; padding: 24px; }
  .chat__idle > .gl { width: 48px; height: 48px; color: var(--mute); }
  .chat__idle h2 { font-size: var(--fs-head); margin-bottom: 0; }
  .chat__compose { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 8px; border-top: 1px solid var(--line); padding: 10px 18px 16px; }
  .chat__compose textarea { grid-column: 1 / -1; grid-row: 1; min-height: 110px; border: 0; border-radius: 0; background: transparent; padding: 8px 0; box-shadow: none; }
  .chat__tools { grid-row: 2; grid-column: 1; min-width: 0; }
  .chat__tools .chat__tool-list { width: 248px; }
  .chat__tool-list > button { min-height: 44px; font-size: var(--fs-body); }
  .chat__tool-list > button .gl { width: 32px; height: 32px; padding: 7px; }
  .chat__compose .chat__voice-shortcut { grid-row: 2; grid-column: 2; }
  .chat__compose .chat__send { grid-row: 2; grid-column: 3; }
}
.chat__voice-shortcut { flex: none; width: var(--tap); height: var(--tap); min-height: var(--tap); border-radius: var(--r-pill); }
.chat__voice-shortcut .gl { width: 22px; height: 22px; }
@media (max-width: 768px) {
  .chat__compose:has(textarea:placeholder-shown) .chat__send,
  .chat__compose:has(textarea:not(:placeholder-shown)) .chat__voice-shortcut { display: none; }
}
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) { .chat__tools .chat__tool-list { background: var(--surface); backdrop-filter: none; } }

/* Mobile workspace: content gets space; glass belongs to floating controls. */
.wb__intro { margin-bottom: 28px; }
.wb__eyebrow { font-size: var(--fs-cap); color: var(--mute); letter-spacing: 0.06em; margin: 0 0 10px; }
.wb__intro .page { margin: 0 0 8px; letter-spacing: -0.035em; }
.wb__intro .hint { margin: 0; }
@media (max-width: 768px) {
  .col:has(> .wb), .col:has(> .chat[data-stage="list"]) { border: 0; border-radius: 0; background: transparent; box-shadow: none; padding: 0; }
  .wb__intro .page { font-size: var(--fs-display); }
  .wb__intro { padding-top: 8px; }
  .wb__hd { margin-bottom: 16px; }
  .wb__hd .wb__tip { font-size: var(--fs-cap); }
  .wb__edit { color: var(--tint); border-color: transparent; background: var(--tint-soft); border-radius: var(--r-pill); }
  .wb__section { margin: 30px 0 16px; letter-spacing: -0.015em; }
  .wb .quick { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 8px; }
  .col .wb .tile__hit { border: 0; background: transparent; padding: 0; gap: 8px; box-shadow: none; }
  .wb .tile__hit .ic { width: 56px; height: 56px; padding: 14px; color: var(--tint); background: linear-gradient(145deg, color-mix(in srgb, var(--tint) 15%, var(--surface)), color-mix(in srgb, var(--tint) 5%, var(--surface))); border-radius: var(--r-lg); box-shadow: inset 0 1px 0 var(--glass-shine), var(--shadow-card); }
  .wb .tile__hit[data-icon="chat"] .ic, .wb .tile__hit[data-icon="people"] .ic, .wb .tile__hit[data-icon="mega"] .ic { color: var(--lane-2); background: color-mix(in srgb, var(--lane-2) 10%, var(--surface)); }
  .wb .tile__hit[data-icon="clock"] .ic, .wb .tile__hit[data-icon="alert"] .ic { color: var(--late); background: var(--late-bg); }
  .wb .tile__hit b { font-size: var(--fs-cap); line-height: 1.4; font-weight: var(--w-medium); }
  .wb .quick--fav { padding: 20px 10px; background: var(--surface); border-radius: var(--r-sheet); box-shadow: var(--shadow-card); }
  .wb .badge { right: calc(50% - 34px); top: -6px; }
  .wb .tile i { display: none; }
  .chat__side { padding: 0; }
  .chat__head { padding: 6px 0 16px; }
  .chat__head h1 { font-size: var(--fs-display); letter-spacing: -0.035em; }
  .chat__side > input { margin: 0 0 14px; width: 100%; border: 0; background: var(--surface-2); border-radius: var(--r-md); }
  body main:has(.login) { padding: 0; }
  .page-wrap:has(.login), .col:has(> .login) { width: 100%; max-width: none; margin: 0; padding: 0; border: 0; border-radius: 0; box-shadow: none; background: transparent; }
  .login { min-height: 100dvh; padding: calc(60px + env(safe-area-inset-top)) 20px max(18px, env(safe-area-inset-bottom)); }
  .login__tilt { width: 100%; transform: none !important; }
  .login__card { width: 100%; border-radius: var(--r-sheet); }
  .login__visual { min-height: 170px; padding: 20px; }
  .login__panel { padding: 24px 22px 28px; }
  .login__orbit { top: 25%; left: 34%; }
  .login__prefs { top: max(12px, env(safe-area-inset-top)); right: 18px; }
  .login__in > input { background: color-mix(in srgb, var(--surface) 38%, transparent); }
  .login__foot { padding-top: 24px; }
}
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
  .tabbar__items, .tabbar__more, .login__card { background: var(--surface); -webkit-backdrop-filter: none !important; backdrop-filter: none !important; }
  .tabbar__items::before, .tabbar__more::before { display: none; }
}

/* Application identity follows this browser's appearance preference. */
.prefs__icons { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.prefs__font { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.prefs__font .prefs__opt { min-width: 0; text-align: center; }
.prefs__font .prefs__opt small { white-space: normal; }
.prefs__icons .prefs__opt { display: flex; align-items: center; gap: 10px; min-width: 0; }
.prefs__icons .prefs__opt span { display: grid; gap: 3px; }
.prefs__icons img, img[data-app-icon] { object-fit: contain; border-radius: var(--r-sm); flex: none; }
.login__panel > img.login__accent { width: 40px; height: 40px; margin-bottom: 18px; background: transparent; }

/* 057 · 消息、AI 附件与可打断的媒体交互。 */
.iconbtn > .gl, button > .gl, a > .gl { display: block; margin: auto; }

.chatroom__system { display: inline-flex; align-items: center; justify-content: center; color: var(--tint); background: var(--tint-soft); }
.chatroom__system .gl { width: 21px; height: 21px; }
.chatroom.on .chatroom__system { color: var(--on-tint); background: color-mix(in srgb, var(--on-tint) 18%, transparent); }
.chatnotice { display: flex; align-items: flex-start; gap: 10px; max-width: min(88%, 680px); }
.chatnotice .chatmsg__body { border: 1px solid var(--line); background: var(--surface-2); }
.chatnotice .chatmsg__body p { margin: 0; }
.chatnotice.unread .chatmsg__body { border-color: color-mix(in srgb, var(--tint) 45%, var(--line)); box-shadow: 0 0 0 3px color-mix(in srgb, var(--tint) 8%, transparent); }
.chatnotice__open { display: inline-flex; margin-top: 10px; color: var(--tint); font-weight: var(--w-semibold); text-decoration: none; }

/* 输入区只有一层胶囊；左右键与输入本体等高，不再像三块拼起来。 */
.chat__compose, .chat__compose:is(form) { display: flex; align-items: flex-end; gap: 8px; padding: 10px 18px 16px; border-top: 1px solid var(--line); }
.chat__compose textarea, .chat__compose:is(form) textarea { flex: 1; grid-column: auto; grid-row: auto; min-height: var(--tap); max-height: 140px; margin: 0; padding: 10px 16px; border: 1px solid var(--line); border-radius: var(--r-pill); background: var(--surface-2); box-shadow: none; line-height: 1.45; }
.chat__compose textarea:focus { border-color: var(--tint); }
.chat__compose .chat__tools, .chat__compose .chat__voice-shortcut, .chat__compose .chat__send { grid-row: auto; grid-column: auto; width: var(--tap); height: var(--tap); min-height: var(--tap); flex: none; }

body.viewer-open { overflow: hidden; }
.chatviewer { --viewer-alpha: .94; --viewer-ink: #F2F3FF; position: fixed; inset: 0; z-index: 400; overflow: hidden; color: var(--viewer-ink); background: rgb(0 0 0 / var(--viewer-alpha)); touch-action: none; }
.chatviewer[hidden] { display: none; }
.chatviewer__stage { position: absolute; inset: 0; display: grid; place-items: center; touch-action: none; user-select: none; }
.chatviewer__stage img { display: block; max-width: min(92vw, 1440px); max-height: 84dvh; width: auto; height: auto; object-fit: contain; transform-origin: center; will-change: transform, opacity; user-select: none; -webkit-user-drag: none; }
.chatviewer .iconbtn { position: absolute; z-index: 2; width: 46px; height: 46px; border: 1px solid rgb(255 255 255 / .18); color: var(--viewer-ink); background: rgb(18 18 20 / .42); -webkit-backdrop-filter: blur(20px) saturate(160%); backdrop-filter: blur(20px) saturate(160%); }
.chatviewer__close { top: max(16px, env(safe-area-inset-top)); right: 18px; }
.chatviewer__nav { top: 50%; transform: translateY(-50%); }
.chatviewer__nav:disabled { opacity: .22; pointer-events: none; }
.chatviewer__prev { left: 18px; }
.chatviewer__prev .gl { transform: rotate(180deg); }
.chatviewer__next { right: 18px; }
.chatviewer__meta { position: absolute; z-index: 2; left: 20px; bottom: max(22px, env(safe-area-inset-bottom)); display: grid; gap: 4px; max-width: min(44vw, 520px); text-shadow: 0 1px 6px #000; }
.chatviewer__meta b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chatviewer__meta span { color: rgb(255 255 255 / .66); font-size: var(--fs-cap); }
.chatviewer__actions { position: absolute; z-index: 2; right: 18px; bottom: max(16px, env(safe-area-inset-bottom)); display: flex; gap: 8px; padding: 7px; border: 1px solid rgb(255 255 255 / .18); border-radius: var(--r-pill); background: rgb(22 22 24 / .42); -webkit-backdrop-filter: blur(24px) saturate(180%); backdrop-filter: blur(24px) saturate(180%); box-shadow: 0 12px 36px rgb(0 0 0 / .3); }
.chatviewer__actions :is(button,a) { display: flex; align-items: center; gap: 7px; min-height: 42px; padding: 0 14px; border: 0; border-radius: var(--r-pill); color: var(--viewer-ink); background: transparent; text-decoration: none; font: inherit; cursor: pointer; }
@media (hover: hover) and (pointer: fine) { .chatviewer__actions :is(button,a):hover { background: rgb(255 255 255 / .12); } }
.chatviewer__actions .gl { width: 20px; height: 20px; }

.chatsheet, .chatgallery, .assistant__attach-sheet { width: 100%; max-width: none; height: 100%; max-height: none; margin: 0; padding: 0; border: 0; overflow: hidden; background: transparent; }
.chatsheet::backdrop, .chatgallery::backdrop, .assistant__attach-sheet::backdrop { background: color-mix(in srgb, #000 34%, transparent); -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px); }
.chatsheet__box, .assistant__attach-box { position: absolute; left: 50%; bottom: 0; width: min(620px, 100%); max-height: min(58dvh, 620px); padding: 8px 18px max(20px, env(safe-area-inset-bottom)); border: 1px solid var(--glass-edge); border-bottom: 0; border-radius: var(--r-sheet) var(--r-sheet) 0 0; color: var(--ink); background: color-mix(in srgb, var(--surface) 82%, transparent); -webkit-backdrop-filter: blur(32px) saturate(180%); backdrop-filter: blur(32px) saturate(180%); box-shadow: 0 -18px 60px rgb(0 0 0 / .18); transform: translateX(-50%); }
.chatsheet__handle, .assistant__sheet-handle { width: 38px; height: 5px; margin: 2px auto 5px; border-radius: var(--r-pill); background: var(--line-strong); }
.chatsheet__list { max-height: calc(58dvh - 88px); overflow-y: auto; overscroll-behavior: contain; }
.chatsheet__list > button { display: flex; align-items: center; gap: 12px; width: 100%; min-height: 62px; padding: 8px 4px; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; color: var(--ink); background: transparent; text-align: left; }
.chatsheet__list > button > span:last-child { display: grid; gap: 3px; min-width: 0; }
.chatsheet__list > button small { overflow: hidden; color: var(--mute); text-overflow: ellipsis; white-space: nowrap; }
.chatmention .chatsheet__box { height: min(52dvh, 560px); }
.chatmention input[type="search"] { width: 100%; margin-bottom: 8px; border-radius: var(--r-pill); }

.chatgallery__box { position: absolute; inset: max(18px, env(safe-area-inset-top)) max(18px, env(safe-area-inset-right)) max(18px, env(safe-area-inset-bottom)) max(18px, env(safe-area-inset-left)); max-width: 1000px; margin: auto; overflow: hidden; border: 1px solid var(--glass-edge); border-radius: var(--r-sheet); color: var(--ink); background: color-mix(in srgb, var(--surface) 88%, transparent); -webkit-backdrop-filter: blur(34px) saturate(170%); backdrop-filter: blur(34px) saturate(170%); box-shadow: var(--shadow); }
.chatgallery__head { display: flex; align-items: center; gap: 12px; min-height: 66px; padding: 10px 16px; border-bottom: 1px solid var(--line); }
.chatgallery__head h2 { flex: 1; margin: 0; text-align: center; font-size: var(--fs-head); }
.chatgallery__head > span { min-width: 44px; color: var(--mute); text-align: right; font-size: var(--fs-cap); }
.chatgallery__tabs { display: flex; gap: 6px; padding: 12px 16px 4px; overflow-x: auto; }
.chatgallery__tabs button { min-height: 36px; padding: 0 16px; border: 0; border-radius: var(--r-pill); background: transparent; color: var(--mute); }
.chatgallery__tabs button.on { color: var(--tint); background: var(--tint-soft); font-weight: var(--w-semibold); }
.chatgallery__grid { display: grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap: 10px; height: calc(100% - 120px); padding: 12px 16px 22px; overflow-y: auto; align-content: start; }
.chatgallery__item { min-width: 0; position: relative; }
.chatgallery__item[hidden] { display: none; }
.chatgallery__item > button { display: block; width: 100%; aspect-ratio: 1; padding: 0; overflow: hidden; border: 0; border-radius: var(--r-sm); background: var(--surface-2); }
.chatgallery__item img, .chatgallery__item video { display: block; width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: var(--r-sm); }
.chatgallery__item--video { grid-column: span 2; }
.chatgallery__item--video video { aspect-ratio: 16/10; }
.chatgallery__item--file { grid-column: span 2; padding: 14px; border: 1px solid var(--line); border-radius: var(--r-md); background: var(--surface-2); }
.chatgallery__date { display: block; margin-top: 5px; color: var(--mute); font-size: var(--fs-micro); }

.assistant__attachment { display: flex; align-items: center; gap: 10px; width: fit-content; max-width: min(86%, 520px); margin: 0 0 10px auto; padding: 9px 12px; border: 1px solid var(--line); border-radius: var(--r-md); color: var(--ink); background: var(--surface-2); text-decoration: none; }
.assistant__attachment > .gl, .assistant__attachment > img { width: 42px; height: 42px; flex: none; border-radius: var(--r-sm); object-fit: cover; }
.assistant__attachment > .gl { padding: 9px; color: var(--tint); background: var(--tint-soft); }
.assistant__attachment > span { display: grid; gap: 3px; min-width: 0; }
.assistant__attachment b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.assistant__attachment small { color: var(--mute); }
.assistant__pending { grid-area: pending; display: grid; grid-template-columns: 50px minmax(0,1fr) 40px; grid-template-rows: auto auto; align-items: center; gap: 1px 11px; margin: 0; padding: 6px; border-radius: var(--r-composer-inner); background: var(--surface-2); transform-origin: bottom center; }
.assistant__pending[hidden] { display: none; }
.assistant__pending [data-ai-preview] { grid-row: 1/3; display: grid; place-items: center; width: 50px; height: 50px; overflow: hidden; border-radius: var(--r-composer-preview); color: var(--tint); background: var(--tint-soft); }
.assistant__pending [data-ai-preview] :is(img,.gl) { width: 100%; height: 100%; object-fit: cover; }
.assistant__pending [data-ai-preview] .gl { padding: 9px; }
.assistant__pending b, .assistant__pending small { grid-column: 2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.assistant__pending small { color: var(--mute); }
.assistant__pending button { grid-column: 3; grid-row: 1/3; width: 40px; height: 40px; min-height: 40px; border-radius: var(--r-pill); color: var(--mute); background: color-mix(in srgb, var(--surface) 72%, transparent); }
.assistant__pending button .gl { width: 18px; height: 18px; }
.assistant__attach-options { display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); gap: 10px; padding: 8px 0 6px; }
.assistant__attach-options > button { display: grid; justify-items: center; gap: 8px; min-width: 0; min-height: 112px; padding: 14px 8px; border: 0; color: var(--ink); background: var(--surface-2); }
.assistant__attach-options .gl { width: 34px; height: 34px; padding: 6px; color: var(--tint); }
.assistant__attach-options span { display: grid; gap: 3px; min-width: 0; }
.assistant__attach-options small { color: var(--mute); font-size: var(--fs-cap); font-weight: var(--w-regular); line-height: 1.35; white-space: normal; overflow-wrap: anywhere; text-align: center; }

@media (max-width: 768px) {
  /* 主视口只按可见高度收缩，不再叠加 visualViewport.offsetTop 的二次上移。 */
  body main:has(.assistant) { top: 0; bottom: auto; transform: none; }
  .assistant__bottom { padding: 7px 10px max(8px, env(safe-area-inset-bottom)); }
  .assistant__ask { --composer-pad: 4px; --composer-control: 42px; }
  .assistant__ask.has-attachment { border-radius: var(--r-composer-mobile); }
  .assistant__pending { border-radius: var(--r-composer-inner-mobile); }
  .assistant__ask textarea { max-height: min(180px, 28dvh); font-size: var(--fs-input); }
  .assistant__footnote { display: none; }
  .assistant__attach-options { gap: 6px; }
  .assistant__attach-options > button { min-height: 104px; }
  .chatviewer__stage img { max-width: 100vw; max-height: 78dvh; }
  .chatviewer__nav { display: none; }
  .chatviewer__meta { left: 16px; right: 16px; bottom: calc(82px + env(safe-area-inset-bottom)); max-width: none; text-align: center; }
  .chatviewer__actions { left: auto; right: max(14px, env(safe-area-inset-right)); bottom: max(14px, env(safe-area-inset-bottom)); transform: none; }
  .chatviewer__actions :is(button,a) { min-width: 70px; padding-inline: 10px; flex-direction: column; gap: 2px; font-size: var(--fs-micro); }
  .chatgallery__box { inset: 0; border: 0; border-radius: 0; }
  .chatgallery__head { padding-top: max(10px, env(safe-area-inset-top)); }
  .chatgallery__grid { grid-template-columns: repeat(3,minmax(0,1fr)); gap: 4px; padding-inline: 4px; }
  .chatgallery__item--file { grid-column: 1/-1; margin: 3px 8px; }
  .chatgallery__item--video { grid-column: span 2; }
  .fabwrap { display: none !important; }
  /* 真实透明材质：降低表面填色，让后方内容参与模糊和饱和采样。 */
  .tabbar__items, .tabbar__more { background: color-mix(in srgb, var(--surface) 32%, transparent); -webkit-backdrop-filter: blur(28px) saturate(190%); backdrop-filter: blur(28px) saturate(190%); box-shadow: inset 0 1px 0 var(--glass-shine), 0 10px 34px rgb(0 0 0 / .16); }
}

@media (prefers-contrast: more) {
  .is-scrolling { scrollbar-color: var(--ink) transparent; }
  .is-scrolling::-webkit-scrollbar-thumb { background-color: var(--ink); }
}
@media (forced-colors: active) {
  .is-scrolling { scrollbar-color: ButtonText Canvas; }
  .assistant__turn.is-generating .assistant__thinking { color: CanvasText; background: none; -webkit-text-fill-color: currentColor; animation: none !important; }
}
