/* ============ 文档站样式（docs/css/style.css） ============
   只做「文档站自己的排版」：配色、圆角、字体、动效全部复用站点既有令牌
   （css/base.css 的 --surface / --text / --text-muted / --border-subtle / --primary /
    --radius / --font-display|body|mono 等），不新增一套配色。
   顶部固定层（fixed .navbar）的让位沿用布局令牌 --bt-nav-inner-h / --bt-nav-pad-bottom /
   --bt-bar-h（css/layout.css 定义，js/modules/site-banners.js 写入实测高度）。

   ⚠ CSP：本文件里没有任何 @import / url(外域)，也不依赖内联样式；
      需要隐藏/展开的都用 class 切换（js/main.js 只动 class 与 innerHTML）。

   ── v=13（两处用户实测反馈：① 滚动后左栏/右栏上方空一大截；② 换文档不回到开头）──────
   · **吸附点口径改正**（`--docs-sticky-top`）= 「固定导航栏**底边**」（**视口坐标**），
     v=12 写的是「实测正文内容区**自然顶边**」（文档坐标）—— 页面没滚动时两者数值相等，
     所以当时看不出问题；滚动后卡片被按「离视口顶 184.4px」钉住，而滚动后的导航栏只有 60px
     ⇒ 空出 124px 一条（用户截图里画红框的两块空白）✗。实测：scrollY=0 时
     side/rail/content 顶边都是 184.4px；scrollY=294 时 side/rail 仍是 184.4px 而
     navbar.bottom=61px。修法见 docs/js/main.js 的 measureStickyTop() + 本文件 :root。
   · **横幅高度不再参与吸附点与锚点让位**：横幅是**文档流内**的块、随页面滚走，
     吸附约束生效时它已经被固定导航栏盖住（实测约束在 scrollY>124 才起作用，横幅底边
     那时在视口 y=44、正好压在 60px 高的导航栏后面）；算进来只会多出一条 88px 的空带 ✗。
     删掉 `--docs-banner-h`（连带 main.js 里对它的写入与对横幅容器的 ResizeObserver 观察）。
   · 新增 `--docs-hero-strip-h: 60px`（仅 ≤900px）：吸附点不再顺带包含粘性页头高度之后，
     窄屏三处（抽屉顶边 / 右栏「本页目录」吸附点 / 锚点落点）必须显式补上这一层，
     否则会钻到粘性页头条**底下** ✗。
   · 换一篇文档回到**整页最上面**由 JS 负责（`scrollDocToStart()`，落点 scrollY = 0、
     **只往上、不往下**）：与第二轮「切文档时整页不要往下滚」不冲突，
     三轮反馈的取舍见 docs/js/main.js 那段注释。

   ── v=12（⑤ 页头宽/窄两态 + ④ 横幅在场时的让位分派，均带浏览器实测证据）──────
   · `--docs-hero-gap: 36px → 16px`（8 的倍数、与 --docs-anchor-gap 同档）：
     这是**宽窄两态共用**的那一档"贴顶小间隙"（用户：别贴太紧、还是有点间隙）。
   · ≥1200px 的 `.docs-hero` 压成 **0 高、只保留下外边距 = --docs-hero-gap(16px)**：
     那一档页头**证明是空的**（两个控制件都被 CSS 断点关掉 / 搬回右栏），
     原来却顶着 margin-bottom: 36px ⇒ 正文标题被白推下 36px（1440 实测 116 vs 导航栏底边 80）✗；
     改后正文首元素 top = 固定层底边 + 16px（0/1/2 条横幅恒定）。
   · `body.docs-page:has(#bt-site-banners) .docs-main { padding-top: 0 }`：
     横幅容器自带 margin-top: --bt-banner-offset，其顶边恰好落在导航栏盒底边，
     <main> 紧随其后 ⇒ 横幅在场时正文顶边**已经**等于「导航栏+横幅」底边，
     .docs-main 再让位一次就是那多出来的 80px（1 条横幅实测 230.14 vs 114.14）✗。
     没有横幅时上面那条 padding-top 原样生效（0 条横幅：80 = 导航栏底边，正好）。
   · ≤1199px 的 `.docs-hero` padding-top: 0/8px → `var(--docs-hero-gap)`：
     按钮组顶边同样只离横幅/导航底边 16px（窄屏原来 901~1199 是 0 = 贴死，≤900 是 8px）。
   · ≤900px 的粘性页头条高 53 → 61px（padding-top +8px），锚点让位里的 52 → 60px 同步跟上，
     否则锚点落点会被粘性条压住 8px ✗。
   ── v=11（③ 的两个 CSS 侧修正，均带浏览器实测证据）──────────────────────
   · .docs-main 的 `padding-top: max(--bt-banner-offset, 80px)` **恢复**：它上一轮被注释掉，
     于是 <main> 顶边落在 y=0、正文顶边只有 36px —— 正文直接钻到 80px 高的固定导航栏底下
     （截图里 h1 被导航栏压住），而侧栏停在「导航栏底边」80px，看起来就是
     「正文在最上面、左栏却还在下面」✗。
   · 删掉 `.main { padding-top: 0 }`：它与 layout.css / responsive.css 的同名规则
     同特异性且本文件靠后 ⇒ 把 ≤640px 的 128px 让位一起清成了 0。 */

:root {
    --docs-side-w: 288px;
    /* 栏间距：沿用站点既有的 24px 间距刻度（8 / 24 / 40 / 44 都是它的组合），不新造一套间距体系。
       40 / 44 留作 ≥1500px 的舒展值，窄一点的宽屏用 32(=24+8) 先把正文喂饱。 */
    --docs-gap: 40px;
    /* 正文列宽：正文与「上一篇 / 下一篇」共用这一个值（同一条左右竖线） */
    --docs-text-w: 82ch;
    /* 右侧「本页目录」栏位：宽屏三栏时占的列宽 */
    --docs-rail-w: 210px;
    --docs-rail-gap: 44px;
    /* 页头（`.docs-hero`）与三栏主体之间的间距，同时也是窄屏「按钮组顶边到横幅底边」的
       上间隙（见下面 ≤1199px 那条 .docs-hero 的 padding-top）—— 两处**共用这一个值**，
       所以「宽屏正文顶边」与「窄屏按钮组顶边」到固定层底边的距离天然一致 ✔。
       取值 16px：用户第四轮补充「别贴太紧、还是有点间隙」，要的是**一档小间隙**
       （12~20px），16 正好是站点既有间距刻度 8 的倍数（2×8）里落在这一档的那个，
       与 :root 里已有的 --docs-anchor-gap: 16px 同档，不新造魔数 ✔。
       v=12 之前是 36px：宽屏那一档页头其实空着（见下方 ≥1200px 那条），
       36px 就成了「横幅之下、正文标题之上」那条空带，用户实测「距离还是怎么大」✗。 */
    --docs-hero-gap: 16px;
    /* 侧栏 / 右栏的**吸附点**：固定导航栏的底边 —— 注意 sticky 的 `top` 是**视口坐标**
       （「离视口顶边多少像素」），不是文档坐标。
       ⚠⚠ v=13 改正过一次口径，务必看清（v=12 那一版是「实测正文内容区**自然顶边**」= 文档坐标）：
         页面没滚动时两者数值恰好相等（实测 1699×901 下都是 184.4px），所以当时看不出问题；
         一滚动就露馅：卡片被按「离视口顶 184.4px」钉住，而滚动后的固定导航栏只有 60px 高
         ⇒ 导航栏与卡片之间空出 124px 的一条（用户截图里画红框的「上面怎么空这么多」）✗。
         实测证据：scrollY=0 时 side/rail/content 顶边都是 184.4px；scrollY=294 时
         side/rail 顶边仍被钉在 184.4px，而 navbar.bottom=61px ⇒ 空带 123.4px。
       · 真值 = 导航栏**盒高**：未滚动 80px（padding-top 24 + 内容行 32 + 下留白 24）、
         滚动后 .scrolled 态 60px（14 + 32 + 14）—— 由 docs/js/main.js 的 syncStickyTop()
         实测后写成内联自定义属性覆盖本行；
       · 「正文与侧栏在没滚动时对齐」是**文档流**（同一行网格）负责的，不需要吸附点复现 ✔。
       这里的 calc 只是**脚本未运行 / 拿不到导航栏时**的兜底（按未滚动那一档算，宁大不小）。 */
    --docs-sticky-top: calc(max(var(--bt-bar-h, 0px), var(--bt-nav-pad-min, 24px)) + var(--bt-nav-inner-h, 32px) + var(--bt-nav-pad-bottom, 24px));
    /* 锚点落点 = 吸附点 + 留白。
       ⚠ v=13 起这里**不再含横幅高度**（原来 + --docs-banner-h）：横幅条是**文档流内**的块
         （margin-top: --bt-banner-offset），随页面一起滚走，而正文标题一定在它下方 ——
         锚点跳转时横幅早就出了视口，多算它只会让标题上方白空一整条（实测 2 条横幅 = 88px）✗。
         这与「吸附点不算横幅」是同一条道理（见上面 --docs-sticky-top 的说明）。 */
    --docs-anchor-offset: calc(var(--docs-sticky-top) + var(--docs-anchor-gap));
    /* 锚点落点额外留白：标题顶边落在「固定层底边 + 这一档」处 —— 16px = 站点间距刻度的
       一档（8 的倍数，与 --docs-gap: 40 / 间距体系同源），保证标题上方（粗体字的上伸部 +
       行高留白）不会被压边，也不会离得太远 ✔。取值依据见 docs/js/main.js 的 scrollToAnchor。 */
    --docs-anchor-gap: 16px;
}

/* ============ 页面骨架 ============ */

/* ⚠ 顶部让位：.navbar 是 position:fixed（css/layout.css），完全不占文档流高度。
   站点其它页靠 .main 的 padding-top 让位（60px；≤640px 时 responsive.css 改成 128px）。
   文档站是独立布局，原来只写 40px → 正文直接顶到固定导航栏下面（用户实测：
   「官方标准文档…」这行字压在导航行上）。
   这里改用**站点既有的让位变量** --bt-banner-offset（= 导航栏 box 高度 − 切换条已占的流高度，
   见 css/layout.css 顶部算式；无横幅时也恒等于导航栏内容行以下的高度），
   与其它页观感一致且严格不重叠。*/
.docs-main {
    flex-grow: 1;
    /* ======================== ③ 的真因（务必保留这一段） ========================
       .navbar 是 position:fixed（css/layout.css），不占文档流，而站点给其它页在 `.main`
       上的 60px 让位（≤640px 是 128px，见 css/responsive.css）在这条规则里被按
       --bt-banner-offset 重写 —— 文档页自己的让位就靠这一行。
       ⚠⚠ 上一轮这一行被注释掉了，于是 main 顶边落在 y=0、正文（.docs-shell 第一个子元素）
       只有 36px 就开始 —— 正文直接钻到 80px 高的固定导航栏底下（截图里 h1 被导航栏压住），
       而侧栏/右栏的吸附点写的是「导航栏盒高」，于是它们停在 y=80、比正文低 44px
       ⇒ 看起来就是用户报的「正文已经在最上面了，左栏却还在下面」✗。
       数值用**站点既有变量**算，不自己猜：--bt-banner-offset = 导航栏 box 高度 −
       切换条已占的流高度（算式见 css/layout.css 顶部 :root，由 js/modules/site-banners.js
       实测后写入）；max() 第二项 80px = 无切换条时的导航栏盒高兜底，故永不为 0 ✓。
       ⚠ 横幅（#bt-site-banners）是文档流内的块（margin-top: var(--bt-banner-offset)），
       它在场时会把 main 整体再往下推，所以这里不需要为横幅再加一次。
       ⚠ 写法必须是扁平的 max(var(--bt-banner-offset, 80px), 80px)，不要写成
       calc(max(var(...), 80px) + 20px)：嵌套 calc+max+var 实测有概率解析失败、
       padding-top 静默退回 0（同一页面两次加载，一次 100px、一次 0px ✗）。
       ⚠ 特异性说明：.docs-main 与 layout.css / responsive.css 的 .main 同为 0,1,0，
       但本文件在它们之后加载 → 这里稳赢。绝不要再写 .main { padding-top: 0 }
       （同特异性、本文件靠后 → 会把这里和 ≤640px 的 128px 一起清成 0 ✗✗）。 */
    padding-top: max(var(--bt-banner-offset, 80px), 80px);
    padding-bottom: 56px;
}

/* ⚠⚠ 这里原来有一条 `.main { padding-top: 0 }`（③ 的第二个帮凶），**已删除**：
   它与 layout.css / responsive.css 的同名规则同特异性（0,1,0），而本文件在它们之后加载
   ⇒ 会把 ≤640px 的 128px 让位、以及 layout 的 60px 让位**一起清成 0** ✗。
   文档页需要的让位现在只由上面 .docs-main 的 padding-top 承担（那一处说明里写了为什么）。 */

/* ================= ④ 横幅在场时：让位**按情形覆盖**，不是关掉 =================
   为什么必须再分一种情形（用户第四轮报的「横幅之下、正文标题之上那一条空带」的真因）：
     · #bt-site-banners（css/layout.css 的 .bt-banners）**自己**带
       `margin-top: var(--bt-banner-offset)`，而 --bt-banner-offset
       = 导航栏盒高 − 切换条已占的流高（算式见 css/layout.css 顶部 :root）
       ⇒ 横幅容器的顶边恰好落在「固定导航栏的盒底边」（切换条高 + 让位量 = 导航栏盒高）；
     · <main> 紧接在横幅容器之后（body 直系顺序 #site-nav → .site-switch-bar
       → #bt-site-banners → <main>，见 js/modules/site-banners.js 的 insertPoint），
       且横幅容器 margin-bottom 为 0
       ⇒ **横幅在场时，正文区的自然顶边已经等于「导航栏 + 横幅」的底边**。
   此时上面那条 `padding-top: max(--bt-banner-offset, 80px)` 再让一次位就是**重复清一次**：
   实测（1440px，验收脚本）1 条横幅时正文首元素 top = 230.14、而「导航栏 + 横幅」底边 = 114.14
   —— 多出来的 80px 正好是这个 padding-top；而 0 条横幅时它是**唯一**的让位量
   （top = 80 = 导航栏底边），一分不能少。两种情形的正确值本来就不同，只能按
   「横幅在不在流里」分派 ✔。
   ⚠ 这不是"把让位注释掉"：没有横幅时上面那条**一字不动**地生效（那正是上一批 44px 错位的解药），
     这里只是给「横幅已经在流里替我们让过位」这一种情形一个 0 值。
   ⚠ 判定用 CSS `:has()` 而不是 JS 写的变量：横幅容器的存在是**布局事实**，用选择器判定
     ⇒ 首帧样式计算时就已经是对的，不存在「JS 晚一帧写好变量 → 先按 80px 排一帧再跳一下」
     的时序问题（浏览器支持见本仓库既有的同一模式：css/article.css 的
     `body:has(.artd-toc:not([hidden])) .artd-toc-toggle`）。
   ⚠ 前缀 body.docs-page：只作用在文档站，特异性 0,2,1 > 上面那条 0,1,0，稳赢；
     其它页面（layout.css 的 .main）完全不受影响。
   ⚠ 末条横幅被 ✕ 关掉 / 被后台关闭而整块摘除时，这条自动失效、让位自动回来 ——
     净位移刚好等于被摘掉的那条横幅高度（≈34px），与"删掉一个流内元素"的预期一致 ✔。 */
body.docs-page:has(#bt-site-banners) .docs-main {
    padding-top: 0;
}

/* ⚠ 文档站页头：**窄屏只剩抽屉开关**（≥901px 时连它也是隐藏的，页头不显示任何东西）。
   这里原来还有 .docs-hero-text（文档站名称 + 副标题）与一个「本页目录」占位按钮：
   标题/副标题与正文 H1 重复、占位按钮与右栏真正的控制件重复，已按用户要求整块删除，
   对应的 .docs-hero-text / .docs-hero-title / .docs-hero-sub 规则也一并删掉。
   ⚠ 底色**不再借全站遮罩档 --scrim-strong**（亮色 rgba(0,0,0,.45) 压在白色正文上合成
     #8c8c8c 的中灰，用户实测「亮色的时候灰得太严重」✗）：改用文档页专用 token
     --docs-hero-bg（两主题值见 css/base.css 的 Phase 2g 段）。 */
.docs-hero {
    max-width: var(--container-max);
    margin: 0 auto var(--docs-hero-gap);
    padding: 0 var(--margin-mobile);
    display: flex;
    align-items: flex-end;
    gap: 12px;
    background: var(--docs-hero-bg);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
}

/* 两个控制件的**同一行**容器（flex row、两端分开、各自不换行）。
   ⚠ 只有 ≤1199px 才有内容：宽屏 #docs-menu-btn 被 CSS 关掉、
      #docs-toc-toggle 被搬回右栏（见 docs/js/main.js 的 syncHeroToolbar），
      容器为空且高度 0（.docs-hero 在 ≥901px 本来就是不显示的）。 */
.docs-hero-actions {
    display: flex;
    flex-direction: row;
    flex-wrap: nowrap;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    width: 100%;
    min-width: 0;
}

.docs-hero-actions > * {
    flex: 0 1 auto;
    min-width: 0;
    white-space: nowrap;
}

/* ================= ⑤ 页头的宽 / 窄两态：宽屏**只留一档小间隙** =================
   ≥1200px 时页头里**没有任何可见内容**，但它原来仍占着 margin-bottom:
   var(--docs-hero-gap) = 36px（v=12 起该令牌已是 16px）—— 用户截图里
   「横幅之下、正文标题之上」那条空带的另一半（改前 1440 实测：页头盒高 0、
   margin-bottom 36px ⇒ 正文首元素 top = 116 而导航栏底边 = 80，差 36px）。
   为什么可以断定这一档页头是空的（两条都是**纯 CSS 断点**，与 JS 无关）：
     · #docs-menu-btn：基础规则 display: none，只有 ≤900px 那条断点把它打开
       （那一档侧栏才是抽屉，需要开门按钮）；901~1199px 侧栏本来就是常显列，按钮也不该出现；
     · #docs-toc-toggle：`syncHeroToolbar()` 只在 ≤1199px 把它搬进页头，
       ≥1200px 它回到右栏并被 `.docs-rail .docs-toc-toggle { display: none }` 关掉
       ⇒ 即使 JS 完全没跑（无 JS / 报错），这一档页头里也只有一个 display:none 的按钮。
   所以「宽 / 窄」用**同一组断点**（1200px / 1199px / 900px）判定，与两个控制件自己的显隐口径
   逐字一致 ⇒ 判定稳、不依赖 JS 时序；两态互斥且完备，不会出现"该显示的按钮被一起藏掉"。
   ⚠ 修法是"把空盒压成 0 高，**保留一档下外边距**"，不是"贴到 0"也不是"把让位关掉"：
     用户补充「也别贴的太紧了，还是有点间隙的」⇒ 正文顶边到「导航栏/横幅底边」保留
     var(--docs-hero-gap) = 16px（8 的倍数、与 --docs-anchor-gap 同档，见 :root 说明）。
     · height: 0 + min-height: 0 + overflow: hidden：页头盒本身不占垂直空间
       （getBoundingClientRect().height === 0，验收口径就是这一条）；
     · margin 只留下面那一档：`margin: 0 auto var(--docs-hero-gap)` —— 这 16px 就是用户要的间隙，
       它不是"多余 margin"，而是**唯一的间隙来源**（宽屏正文顶边 = 固定层底边 + 16 ✔）；
     · padding / flex gap 归零：页头里没有任何可见子元素，留着只会造成读数歧义。
   ⚠ 为什么不用 display: none：那样 margin 也一起失效，间隙就得另找地方写（第二处口径）；
     保留零高盒 + 一条 margin 让「宽屏正文间隙」与「窄屏按钮上间隙」共用同一个令牌 ✔。 */
@media (min-width: 1200px) {
    .docs-hero {
        height: 0;
        min-height: 0;
        overflow: hidden;
        margin: 0 auto var(--docs-hero-gap);
        padding: 0;
        gap: 0;
    }

    /* 空壳容器直接不参与布局：即使将来有人往页头里塞了别的东西（或被 JS 搬回来的按钮），
       页头也**证明**是 0 高，宽屏不会再冒出一条空带 ✔（窄屏不受影响：这条只在 ≥1200px 生效）。 */
    .docs-hero-actions {
        display: none;
    }
}

/* ≤1199px：页头里**确实**有控制件（不能收掉），但按钮组要**紧贴上沿、又留一档小间隙**：
   padding-top = var(--docs-hero-gap) = 16px（与宽屏正文那一档同值 ⇒ 0/1/2 条横幅下
   视觉间距恒定，不随条数漂移 ✔）。
   901~1199px 原来是 padding-top: 0：胶囊会**贴死**在横幅/导航栏底边上（用户说"别贴太紧"）；
   ≤900px 那条断点里的 padding-top 也归到同一个令牌，两档窄屏口径一致 ✔。
   下间隙（按钮组 → 正文）同样由 var(--docs-hero-gap) 承担（基础规则里的 margin-bottom）。 */
@media (max-width: 1199px) {
    .docs-hero {
        padding-top: var(--docs-hero-gap);
    }
}

/* ============ ⑥ 页头顶边：把「.docs-main 多让的那一截」按同一算式拉回来 ============
   目标（用户第四轮补充）：**宽窄两态、0/1/2 条横幅**下，页头/正文的顶边都只离
   「固定层底边（导航栏 + 横幅）」var(--docs-hero-gap) = 16px，一格都不漂。
   为什么还需要这一条：`.docs-main` 的让位是
   `padding-top: max(--bt-banner-offset, 80px)`（上一批修 44px 错位的解药，**保持原样**），
   其中的 **80px 兜底是按桌面导航栏盒高写死的**，而窄屏（≤640px）导航栏盒只有 59px
   ⇒ 「0 条横幅」那一格会多让 21px：实测 375px/0 条横幅时按钮组顶边离导航栏底边 38px，
   而 1~2 条横幅时只有 16px —— 正是用户说的「看着不一样」（且与"0 横幅 36px"同类漂移）。
   修法：页头的上外边距 = **多让的那一截的负值**（多让 = 让位值 − --bt-banner-offset）。
     · 0 条横幅（桌面/768）：max(80,80) − 80 = 0        → 不动 ✔
     · 0 条横幅（375，导航栏 59px）：max(59,80) − 59 = 21 → margin-top: -21px 拉回来 ✔
     · 有切换条的宽屏：max(56,80) − 56 = 24             → 同样拉回来 ✔（本轮线上形态）
   ⚠ min(0px, …) 的用处：让位值**小于等于** --bt-banner-offset 时（导航栏盒比 80px 高、
     或横幅在场），多让量为 0 或负数 ⇒ 一律不补（补成正值就是把页头往下推）✔。
   ⚠ 横幅在场时让位已经是 0（见上面 :has 那条），补偿必须显式归零，
     否则窄屏「有横幅」那一格会被拉上去压住横幅 ✗（下面第二条）。
   ⚠ 数学函数嵌在 calc 里在本仓库既有做法之上（Chrome 实测解析并算出 -21px，
     见验收脚本 probe：`min(0px, calc(var(--bt-banner-offset,80px) - 80px))` → -22px/375 视口）。
   ⚠ 位置要求：必须排在基础 `.docs-hero`（margin 简写）与 ≥1200px 那条（同样是简写）**之后**，
     否则简写里的 margin-top: 0 会盖掉它。 */
.docs-hero {
    margin-top: min(0px, calc(var(--bt-banner-offset, 80px) - 80px));
}

body.docs-page:has(#bt-site-banners) .docs-hero {
    margin-top: 0;
}

/* 抽屉开关默认不出现，由下面的窄屏断点打开。
   ⚠ 「本页目录」开合按钮默认也隐藏：真正生效的那个按钮由 insertRail()
      插进右栏（窄屏再被 syncHeroToolbar() 搬到 .docs-hero-actions）。
      两条显隐规则（哪个宽度显示哪个）写在下面「开合按钮」那一段里。 */
.docs-menu-btn,
.docs-toc-toggle {
    display: none;
}

/* ============ 三栏骨架：左 目录树 / 中 正文 / 右 本页目录 ============
   断点与文章详情页（css/article.css 的 .artd-wrap）保持一致：
     · ≥1200px   三栏，目录是右栏粘性跟随（.artd-toc 同款断点）
     · 901–1199  右侧宽度不够 → 目录以按钮开合（正文保持满宽，不会"一开目录就变窄"）
     · ≤900px    目录回到正文上方，但**默认收起**为一行可点开的 <details> */
.docs-shell {
    max-width: var(--container-max);
    margin: 0 auto;
    padding: 0 var(--margin-mobile);
    display: grid;
    grid-template-columns: var(--docs-side-w) minmax(0, 1fr) 0;
    column-gap: var(--docs-gap);
    align-items: start;
}

/* 本页目录栏位：中栏正文 / 左栏目录树 都是它的兄弟网格项。
   ⚠ 吸附行为由里面的 .docs-rail-inner 负责（见下），这样「本页目录」按钮
     和目录内容是一个整体，一起停住、不会各停各的。 */
.docs-rail {
    min-width: 0;
    grid-column: 3;
    /* ✗ sticky 失效根因就在这里：.docs-shell 是 align-items: start，网格项默认按
       「自适应高度」排布 —— .docs-rail 于是只有目录那么高，而 sticky 元素的**可移动范围
       完全由父级（包含块）高度决定**：父高 == 自身高 → 可移动范围 0 → 一滚就跟着滚走 ✗。
       左栏 .docs-sidebar 之所以一直正常，是因为 sticky 就设在**网格项自身**上，
       包含块是网格区域（整行高、随正文变高），所以有可移动空间 ✓。
       这里补 align-self: stretch 让 .docs-rail 与整列同高，内部的 .docs-rail-inner
       就能和左栏一样吸附住（同一个 --docs-sticky-top，同一种「外框粘住」机制）✓。 */
    align-self: stretch;
}

/* 本页目录 + 开合按钮的共同外框：**粘性**，滚动时跟着停在视口里（与左栏同一套吸附点）。
   ⚠ 吸附点 = 固定导航栏底边（**视口坐标**），由 main.js 的 syncStickyTop() 实测写入
     --docs-sticky-top —— 口径与那条空带（用户报的「上面怎么空这么多」）的成因说明
     都写在文件顶部 :root 的 --docs-sticky-top 注释里，改这里之前先读那一段。 */
.docs-rail-inner {
    position: sticky;
    top: var(--docs-sticky-top);
    /* 与左栏同一套「底边贴视口」口径：dvh 让开移动端地址栏，100vh 作旧浏览器兜底 */
    max-height: calc(100vh - var(--docs-sticky-top));
    max-height: calc(100dvh - var(--docs-sticky-top));
    display: flex;
    flex-direction: column;
    gap: 12px;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
}

.docs-rail-inner:empty {
    display: none;
}

/* 没有 H2/H3 的文档（或 JS 还没注入时）：右栏收成 0 宽，正文拿回完整宽度 */
.docs-rail:empty {
    display: none;
}

@media (min-width: 768px) {
    .docs-shell,
    .docs-hero {
        padding-left: var(--margin-desktop);
        padding-right: var(--margin-desktop);
    }
}

/* 宽屏：右栏留出宽度，中栏正文仍然 ≥ 700px（1440 视口实测：正文列 732px），
   所以不会被挤成窄条；栅格全部 minmax(0, …) + 正文 min-width:0，不会出现横向滚动。 */
@media (min-width: 1200px) {
    .docs-shell {
        grid-template-columns: var(--docs-side-w) minmax(0, 1fr) var(--docs-rail-w);
        column-gap: 32px;
    }
}

/* 够宽（≥1500px）时把栏间距放宽到舒展值：40 / 44（= 24 的刻度组合），正文仍拿满自己的列 */
@media (min-width: 1500px) {
    .docs-shell {
        /* 注释才达到需要的效果 也就是你没计算好 */
        /* column-gap: var(--docs-gap) var(--docs-rail-gap); */
    }
}

/* ============ 左侧固定侧边栏（可滚动） ============ */

.docs-sidebar {
    /* 显式钉在第 1 列：让「左栏 / 中栏 / 右栏」在 CSS 里一眼可读 */
    grid-column: 1;
    position: sticky;
    /* ⚠ 吸附点 = 固定导航栏底边（**视口坐标**）：
       --docs-sticky-top 由 docs/js/main.js 的 syncStickyTop() 实测写入
       （= .navbar 的 offsetHeight：未滚动 80px、滚动后 .scrolled 态 60px），
       顶部切换条出现/消失、语言切换导致换行时由 ResizeObserver 重算 ✔。
       ⚠ v=13 起**不再**把「正文自然顶边（文档坐标）」或横幅高度算进来 —— 那会让卡片
         停在离视口顶 184px 处、空出一条 124px 的带子（用户截图里的红框）。
         完整口径与实测数字见文件顶部 :root 的 --docs-sticky-top 注释。 */
    top: var(--docs-sticky-top);
    /* ⚠ max-height 用 dvh 而不是 vh：移动端浏览器地址栏会把 vh 算成「大视口」，
       侧栏底部会伸到可视区外面、最后一个条目滚不到 ✗。100vh 作旧浏览器兜底，
       新浏览器按 dvh 重算（不依赖 @supports，写两条即可）。
       ⚠ 不再留 24px 尾巴：留了之后侧栏底边比视口底边高 24px，最后一屏条目仍然够不着 ✗。 */
    max-height: calc(100vh - var(--docs-sticky-top));
    max-height: calc(100dvh - var(--docs-sticky-top));
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 20px 18px 24px;
    border-radius: var(--radius);
    background: var(--translate-card);
    border: 1px solid var(--border-faint);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
}

/* ⚠ 侧栏顶部原来有一块 .docs-side-head（返回入口 + 文档站标题 + 副标题），
   已按用户要求**整块删除**：标题/副标题在页头 .docs-hero 里已有一份（重复），
   返回入口改到页脚只保留一处。这里不再有 .docs-back / .docs-side-title / .docs-side-sub。 */

/* ---------- 搜索 ---------- */

.docs-search {
    position: relative;
    display: flex;
    align-items: center;
    /* 侧栏顶部那块 head 已删除，搜索框就是侧栏第一项，所以不再需要额外的上外边距 */
    margin-top: 0;
}

.docs-search-input {
    width: 100%;
    min-height: 40px;
    padding: 9px 36px 9px 38px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border-subtle);
    background: var(--surface-glass);
    color: var(--text);
    font-size: 14px;
    transition: border-color var(--transition-fast), background var(--transition-fast);
}

.docs-search-input::placeholder {
    color: var(--text-faint);
}

.docs-search-input:focus {
    outline: none;
    border-color: var(--primary);
    background: var(--surface-glass-soft);
}

/* 浏览器自带的搜索清除按钮会和外置的清除按钮重叠 */
.docs-search-input::-webkit-search-cancel-button {
    -webkit-appearance: none;
    appearance: none;
}

.docs-search-icon {
    position: absolute;
    left: 12px;
    color: var(--text-muted);
    pointer-events: none;
}

.docs-search-clear {
    position: absolute;
    right: 6px;
    min-width: 28px;
    min-height: 28px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-sm);
    color: var(--text-muted);
}

.docs-search-clear:hover {
    color: var(--text);
    background: var(--surface-glass-hover);
}

.docs-search-clear[hidden] {
    display: none;
}

.docs-search-empty {
    margin-top: 12px;
    color: var(--text-muted);
    font-size: 13px;
}

.docs-search-empty[hidden] {
    display: none;
}

.docs-search-results {
    list-style: none;
    margin-top: 12px;
    display: grid;
    gap: 2px;
}

.docs-search-results[hidden] {
    display: none;
}

.docs-search-hit {
    display: grid;
    gap: 2px;
    padding: 9px 10px;
    border-radius: var(--radius-sm);
    transition: background var(--transition-fast), color var(--transition-fast);
}

.docs-search-hit:hover {
    background: var(--surface-glass-soft);
}

.docs-search-hit-title {
    color: var(--text);
    font-size: 13.5px;
    font-weight: 500;
    line-height: 1.45;
}

.docs-search-hit-group {
    color: var(--text-faint);
    font-family: var(--font-mono);
    font-size: 11px;
}

/* ---------- 目录树（分组用 details，折叠状态由 open 属性控制） ---------- */

.docs-tree {
    margin-top: 18px;
    border-top: 1px solid var(--border-faint);
}

.docs-group {
    border-bottom: 1px solid var(--border-faint);
}

.docs-group-title {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 12px 4px;
    list-style: none;
    cursor: pointer;
    color: var(--text-faint);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.docs-group-title::-webkit-details-marker {
    display: none;
}

.docs-group-title:hover {
    color: var(--text-muted);
}

.docs-group-arrow {
    transition: transform var(--transition-fast);
}

.docs-group[open] .docs-group-arrow {
    transform: rotate(180deg);
}

.docs-group-list {
    list-style: none;
    margin-bottom: 8px;
}

.docs-tree-link {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    min-height: 38px;
    padding: 8px 10px;
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    font-size: 14px;
    line-height: 1.4;
    transition: color var(--transition-fast), background var(--transition-fast);
}

.docs-tree-link:hover {
    color: var(--text);
    background: var(--surface-glass-subtle);
}

.docs-tree-link.is-active {
    color: var(--primary);
    background: var(--primary-soft);
}

.docs-tree-kind {
    flex: none;
    padding: 1px 5px;
    border: 1px solid var(--border-faint);
    border-radius: 4px;
    color: var(--text-faint);
    font-family: var(--font-mono);
    font-size: 10px;
}

/* ---------- 单篇文档「维护中」标记（后台「文档管理」→ maintenance） ----------
   条目**保留**在目录里并带这个标记（关闭的条目则整条被移除，不存在需要隐藏的样式）。
   文案由 docs/js/main.js 的 treeMaintChip() 渲染，这里只负责外观：
   用站点既有的 --status-warn* 令牌，亮/暗两主题都不写死颜色。
   ⚠ flex:none + white-space:nowrap：窄侧栏（288px）里被长标题挤压时也不换行、不被压成 0 宽。 */
.docs-tree-maint {
    flex: none;
    /* 与顶栏 .nav-maint-tag 同一套口径（小圆角药丸 + 状态警示色）：
       同一个「维护中」在两处必须长得一样，别一处 4px/10px 一处 6px/10.5px。 */
    padding: 0 6px;
    border: 1px solid var(--status-warn-border);
    border-radius: 6px;
    background: var(--status-warn-bg);
    color: var(--status-warn);
    font-size: 10.5px;
    line-height: 15px;
    font-weight: 600;
    letter-spacing: 0.02em;
    white-space: nowrap;
}

/* ---------- 单篇文档占位（维护 / 关闭，由 docs/js/main.js 的 renderDocPlaceholder 渲染） ----------
   ⚠ 这里是「某一篇不可用」的页面内占位，与整站的 js/page-guard.js 占位（#page-guard-off）不是同一层：
      page-guard 管「文档站整站开放/维护/关闭」，这里管「某一篇文档」。
      样式沿用同一套令牌，视觉语言一致但选择器不冲突。
   ⚠ 全部用 class（没有内联 style），强制 CSP 下同样安全。 */
.docs-doc-off {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 52vh;
    padding: 48px 20px;
}

.docs-doc-off-inner {
    max-width: 520px;
    width: 100%;
    text-align: center;
}

.docs-doc-off-badge {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 5px 14px;
    border-radius: 999px;
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.04em;
}

.docs-doc-off-badge.is-maint {
    border: 1px solid var(--status-warn-border);
    background: var(--status-warn-bg);
    color: var(--status-warn);
}

.docs-doc-off-badge.is-closed {
    border: 1px solid var(--border-visible);
    background: var(--surface-glass-soft);
    color: var(--text-muted);
}

.docs-doc-off-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: currentColor;
}

.docs-doc-off-title {
    margin: 18px 0 8px;
    font-size: clamp(22px, 4vw, 30px);
    line-height: 1.3;
    color: var(--text);
}

.docs-doc-off-desc {
    margin: 0 auto;
    max-width: 420px;
    color: var(--text-muted);
    font-size: 15px;
    line-height: 1.7;
}

/* 抽屉遮罩（只在窄屏显示；用 button 以便键盘可聚焦关闭）
   ⚠ z-index 取 40（= css/layout.css 横幅容器的层级，低于固定导航栏的 50）：
      原来写 55（高于导航栏）→ 抽屉一打开，连导航栏也被压上一层黑纱 ✗。
      现在遮罩只压正文（.main 是 z-index: 10），导航栏完好可见、可点 ✓。
      抽屉本体 45 > 遮罩 40 → 抽屉浮在遮罩之上、仍然可点（与站点移动菜单同一套关系）。 */
.docs-backdrop {
    position: fixed;
    inset: 0;
    z-index: 40;
    background: var(--overlay-scrim);
    border: 0;
}

.docs-backdrop[hidden] {
    display: none;
}

/* 抽屉打开时锁页面滚动（窄屏断点里抽屉才有意义，但选择器写成通用的即可）。
   ⚠ 用文档抽屉**自己的类**，不复用站点通用的 body.no-scroll（components.css）：
      nav.js 的移动菜单有一段全局「点到导航栏之外就关菜单」的委托，
      它会顺手 body.classList.toggle('no-scroll', false) —— 而 #docs-menu-btn 就在导航栏之外，
      于是抽屉一打开、这次 click 冒泡到 document 时锁滚动立刻被解掉（实测 ✗，见 docs/js/main.js
      的 DRAWER_LOCK 注释）。这里独立成类，两者互不干扰。 */
body.docs-drawer-open {
    overflow: hidden;
}

/* ============ 右侧内容区 ============ */

.docs-content {
    min-width: 0;
}

.docs-body {
    max-width: var(--docs-text-w);
    color: var(--text-on-surface);
    font-size: 16px;
    line-height: 1.75;
}

.docs-body > * + * {
    margin-top: 16px;
}

.docs-body h1 {
    font-family: var(--font-display);
    font-size: clamp(1.6rem, 3.4vw, 2.1rem);
    font-weight: 700;
    line-height: 1.25;
    letter-spacing: -0.02em;
    color: var(--text);
    scroll-margin-top: var(--docs-anchor-offset);
}

.docs-body h2 {
    margin-top: 40px;
    font-family: var(--font-display);
    font-size: 24px;
    font-weight: 600;
    line-height: 1.3;
    color: var(--text);
    scroll-margin-top: var(--docs-anchor-offset);
}

.docs-body h3 {
    margin-top: 28px;
    font-size: 18px;
    font-weight: 600;
    color: var(--text);
    scroll-margin-top: var(--docs-anchor-offset);
}

.docs-body a {
    color: var(--primary);
    text-decoration: underline;
    text-decoration-color: var(--primary-edge-strong);
    text-underline-offset: 3px;
}

.docs-body a:hover {
    color: var(--primary-container);
    text-decoration-color: currentColor;
}

.docs-body ul,
.docs-body ol {
    padding-left: 22px;
}

.docs-body li + li {
    margin-top: 6px;
}

.docs-body code {
    padding: 2px 6px;
    border-radius: 6px;
    background: var(--surface-high);
    color: var(--primary);
    font-family: var(--font-mono);
    font-size: 0.9em;
}

.docs-body pre {
    padding: 16px 18px;
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-sm);
    background: var(--surface);
    overflow-x: auto;
}

.docs-body pre code {
    padding: 0;
    background: none;
    color: var(--text-on-surface);
    font-size: 13px;
    line-height: 1.7;
}

.docs-body blockquote {
    padding: 2px 0 2px 16px;
    border-left: 3px solid var(--primary-container);
    color: var(--text-muted);
}

.docs-body hr {
    border: 0;
    border-top: 1px solid var(--border-subtle);
}

.docs-body img {
    max-width: 100%;
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-sm);
}

/* 表格：站点没有表格组件，这里按现有令牌自建（.html 片段专用，Markdown 不支持表格） */
.docs-body table {
    display: block;
    width: 100%;
    overflow-x: auto;
    border-collapse: collapse;
    font-size: 14px;
}

.docs-body th,
.docs-body td {
    padding: 9px 12px;
    border: 1px solid var(--border-faint);
    text-align: left;
    vertical-align: top;
    line-height: 1.6;
}

.docs-body th {
    background: var(--surface-low);
    color: var(--text);
    font-weight: 600;
    white-space: nowrap;
}

.docs-body strong {
    color: var(--text);
    font-weight: 600;
}

.docs-body del {
    color: var(--text-faint);
}

.docs-loading {
    color: var(--text-muted);
    font-size: 14px;
}

/* 只给屏幕阅读器看的文案（视觉隐藏，但仍在无障碍树里）：
   v=14 起「正在载入文档…」由骨架屏承担视觉，文案用它隐藏 ——
   页面上不再只有孤零零一行字，读屏用户照样能听到"正在载入"。 */
.docs-sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* ============ 首屏骨架屏（v=14） ============
   背景：文档站要等 sidebar.html + nav.json（网络）才渲染，原来这段只有一行「正在载入文档…」，
   左栏是个空盒子、右栏什么都没有 —— 用户报「怎么只显示几个文字，是不是不太妥，起码撑起来」✗。
   现在三栏各自的骨架**直接写在 docs/index.html 里**（不靠 JS 注入）：
     · 首帧就有骨架（连 JS 还没跑就有）；
     · 形状按真实内容排：左 = 搜索框 + 3 组 × 2 条；中 = H1 + 两段（3/2 行）+ 两条列表；右 = 标题 + 5 条；
     · 真实内容到位时由原有代码整块替换（#docs-sidebar / #docs-content / #docs-rail 的 innerHTML），
       不需要任何"移除骨架"的额外逻辑。
   底色 / 微光扫过用站点既有的 .skeleton 基类（css/base.css），这里只给尺寸，
   所以**不新增配色**；prefers-reduced-motion 下的动画收敛也由那个基类负责。 */
.docs-skel {
    pointer-events: none;
}

.docs-skel .skeleton {
    height: 12px;
}

/* —— 左栏（目录树骨架）—— */
.docs-skel-search {
    height: 40px !important;
    border-radius: var(--radius-sm);
    margin-bottom: 18px;
}

.docs-skel-group + .docs-skel-group {
    margin-top: 18px;
}

.docs-skel-gtitle {
    width: 42%;
    height: 11px !important;
    margin-bottom: 12px;
}

.docs-skel-item {
    height: 34px !important;
    border-radius: var(--radius-sm);
}

.docs-skel-item + .docs-skel-item {
    margin-top: 8px;
}

/* —— 中栏（正文骨架）：宽度与真实正文一致（--docs-text-w 由 .docs-body 用，这里对齐） —— */
.docs-skel-h1 {
    width: 62%;
    height: 30px !important;
    border-radius: 8px;
    margin-bottom: 22px;
}

.docs-skel-h2 {
    width: 38%;
    height: 21px !important;
    border-radius: 8px;
    margin: 34px 0 18px;
}

.docs-skel-lines {
    max-width: var(--docs-text-w);
}

.docs-skel-line {
    height: 13px !important;
}

.docs-skel-line + .docs-skel-line {
    margin-top: 12px;
}

.docs-skel-line.is-short {
    width: 68%;
}

.docs-skel-list {
    display: flex;
    align-items: center;
    gap: 12px;
    max-width: var(--docs-text-w);
    margin-top: 12px;
}

.docs-skel-bullet {
    flex: none;
    width: 7px;
    height: 7px !important;
    border-radius: 50%;
}

.docs-skel-list .docs-skel-line {
    flex: 1 1 auto;
    margin-top: 0;
}

.docs-skel-list .docs-skel-line.is-short {
    flex: 0 1 68%;
}

/* —— 右栏（本页目录骨架）：只在 ≥1200px 显示 ——
   ⚠ ≤1199px 那两档右栏本来就没有常显列（901–1199 是 0 宽度、≤900 是正文上方的折叠块），
     放骨架只会多出一块空白（≤900 还会把正文往下推）。 */
@media (max-width: 1199px) {
    .docs-skel-rail {
        display: none;
    }
}

.docs-skel-toc-title {
    width: 58%;
    height: 12px !important;
    margin-bottom: 14px;
}

.docs-skel-toc-item {
    height: 11px !important;
}

.docs-skel-toc-item + .docs-skel-toc-item {
    margin-top: 14px;
}

.docs-skel-toc-item.is-short {
    width: 72%;
}

.docs-error {
    padding: 14px 16px;
    border: 1px solid var(--status-danger-border);
    border-radius: var(--radius-sm);
    background: var(--status-danger-bg);
    color: var(--status-danger);
    font-size: 14px;
}

/* ---------- 本页目录（H2/H3，滚动监听高亮） ----------
   三档，与文章详情页（css/article.css 的 .artd-toc）同一套断点与交互语言：
     ≥1200px   右栏常显 + 粘性跟随
     901–1199  右侧宽度不够 → 目录收成按钮开合的面板（.is-open 展开，正文保持满宽）
     ≤900px    正文上方的可折叠块（原生 <details>，默认收起） */

.docs-toc {
    min-width: 0;
    padding: 16px 20px;
    border: 1px solid var(--border-faint);
    border-radius: var(--radius);
    background: var(--surface-low);
}

.docs-toc-title {
    display: flex;
    align-items: center;
    gap: 8px;
    color: var(--text-muted);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

/* 箭头只在「可折叠」的两档出现（宽屏右栏是常显列表，不需要箭头） */
.docs-toc-arrow {
    display: none;
    margin-left: auto;
    color: var(--text-faint);
    transition: transform var(--transition-fast);
}

/* 「本页目录」四个字：summary 里这份**只在宽屏显示**。
   ⚠ 基础规则是 display:none，由下面 ≥1200px 的媒体查询打开 —— 与 #docs-toc-toggle
     的可见性正好互斥（按钮基础规则 display:none、≤1199px 打开、≥1200px 关掉），
     所以任何宽度下**可见的**「本页目录」文字都恰好 1 处 ✓（上一轮靠删文字，
     字面确实是 1 次，但宽屏可见次数是 0 ✗ —— 判据要按可见性算，不是按 DOM 字面次数算）。 */
.docs-toc-title-text {
    display: none;
}

.docs-toc-list {
    list-style: none;
    margin-top: 12px;
    display: grid;
    gap: 4px;
}

.docs-toc-link {
    display: block;
    padding: 3px 0 3px 12px;
    border-left: 2px solid var(--border-subtle);
    color: var(--text-muted);
    font-size: 14px;
    line-height: 1.5;
}

.docs-toc-link:hover {
    color: var(--text);
}

.docs-toc-link.is-active {
    border-left-color: var(--primary);
    color: var(--primary);
}

.docs-toc-h3 .docs-toc-link {
    padding-left: 26px;
    font-size: 13px;
}

/* ≥1200px：右栏常显。
   ⚠ 吸附**不在这里**做：目录是 .docs-rail-inner 的子元素，粘性由那个外框统一负责
     （top: var(--docs-sticky-top)，与左栏 .docs-sidebar 完全同一个吸附点），
     否则按钮和目录会各粘各的、互相压住。这里只负责滚动与内边距。 */
@media (min-width: 1200px) {
    .docs-toc {
        overflow-y: auto;
        overscroll-behavior: contain;
        scrollbar-width: thin;
        padding: 14px 16px;
    }

    /* 宽屏：按钮（下面那条媒体查询）被关掉，所以由 summary 显示「本页目录」 */
    .docs-toc-title-text {
        display: inline;
    }

    /* 宽屏目录是**常显列表**：<summary> 只当标题用（不是开合开关）。
       cursor 与 list-marker 都收掉，避免用户以为点了会折叠；
       即使真的点到，docs/js/main.js 的 insertRail() 也在宽屏 preventDefault 掉了。 */
    .docs-toc > .docs-toc-title {
        cursor: default;
        list-style: none;
    }

    .docs-toc > .docs-toc-title::-webkit-details-marker {
        display: none;
    }

    .docs-toc > .docs-toc-title::marker {
        content: none;
    }
}

/* 开合按钮：位于 .docs-rail-inner 里（跟着一起粘住，滚动时不会滚走）。
   ⚠ 基础规则是 display:none —— 按钮只在 ≤1199px 出现，且必须用**媒体查询**打开，
      不能靠「基础规则先 inline-flex、宽屏再 display:none 覆盖」：两者 specificity 同为
      0,1,0，胜负只由出现顺序决定，媒体查询里再打开一次就会把宽屏的覆盖顶掉 ✗
      （本轮实测踩过：窄屏按钮会一起消失）。媒体查询只在命中时生效，天然不会打架 ✓。

   文字唯一性的实现：**按整个控件互斥可见性**（不是只互斥文字 ✗）——
   ≤1199px：只显示这一个按钮（「本页目录」文字在这里），<summary> 整体 display:none
            （见上面那条 `@media (max-width: 1199px)`，连图标 + 箭头一起收掉）；
   ≥1200px：按钮被下面那条 `@media (min-width: 1200px)` 关掉，改由 <summary> 显示
            「本页目录」（文字 + 图标 + 箭头）。
   这样**任何宽度下可见的「本页目录」都恰好 1 处** ✓（上一轮只藏了 summary 里的文字，
   图标 + 箭头那个块还在 → 中/窄屏看起来是「两个本页目录」✗）。 */
.docs-toc-toggle {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex: none;
    white-space: nowrap;
    max-width: 100%;
    min-height: 38px;
    padding: 6px 14px;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-pill);
    background: var(--surface-glass-soft);
    color: var(--text);
    font-size: 13px;
    font-weight: 500;
}

/* ≤1199px：目录是「按钮开合」的（901–1199 是右侧面板，≤900 是正文上方的折叠块），
   所以这个宽度区间里按钮是可见的唯一控制件。 */
@media (max-width: 1199px) {
    .docs-rail .docs-toc-toggle {
        display: inline-flex;
    }

    /* ⚠ 只藏 summary 里的 .docs-toc-title-text 是不够的 ✗：summary 自己还带着
       toc 图标 + unfold_more 箭头，于是 ≤1199px 会在按钮下面多出「图标 + 箭头」
       第二个视觉块 —— 用户看到的就是「本页目录小屏幕会显示俩个」✗。
       显隐必须按**整个控件**互斥：按钮可见 → summary 整体收掉。
       detail 面板在中屏由 .is-open 控制（见下面 901–1199 那条），窄屏由按钮同步 open 状态。 */
    .docs-toc > .docs-toc-title {
        display: none;
    }

    /* 窄屏按钮被 syncHeroToolbar() 搬到页头的 .docs-hero-actions 里（与「目录」按钮同一行），
       那时它不再是 .docs-rail 的子元素 —— 上面那条 `.docs-rail .docs-toc-toggle` 就选不中它了，
       所以这里补一条按**容器**（而不是按 rail）的可见性规则。
       与「目录」按钮同一行、同一个 flex row、各自 nowrap（见 .docs-hero-actions）。
       ⚠ margin-left: auto（v=34）：901–1199px 这一档页头那一行里**只剩「本页目录」一个按钮**
         （「目录」按钮只在 ≤900px 出现），而 .docs-hero-actions 是 space-between ⇒
         单个子元素会被贴在**最左边**，看起来像"管着右栏的按钮跑到了左边"（用户报：
         「放右边的放右边」）✗。auto 让它在任何宽度下都靠右，与它控制的右栏同侧。 */
    .docs-hero-actions .docs-toc-toggle {
        display: inline-flex;
        flex: none;
        margin-left: auto;
    }
}

/* ≥1200px：目录是右栏常显列表，**不要**再出现开关按钮（否则 rail 里会出现
   两个可点的目录入口，与常显列表重复）。specificity 0,2,0 > 基础规则 0,1,0，
   所以在与上面那条媒体查询同源顺序下也稳赢。 */
@media (min-width: 1200px) {
    .docs-rail .docs-toc-toggle {
        display: none;
    }
}

.docs-toc-toggle:hover {
    background: var(--surface-glass-strong);
}

.docs-toc-toggle[aria-expanded="true"] {
    color: var(--primary);
    border-color: var(--primary-line);
}

/* ============ 窄屏页头里两个控制件的胶囊底（④） ============
   场景：≤1199px 时「目录」（#docs-menu-btn）与「本页目录」（#docs-toc-toggle，
   被 syncHeroToolbar() 搬进 .docs-hero-actions）并排躺在页头 .docs-hero 上。
   改动前的问题（375/560/768 实测）：
     · 「目录」胶囊底 = var(--surface-glass-soft)，亮色下只有 rgba(20,20,26,.05) —— 叠在
       灰底页头上几乎看不见（胶囊底 vs 页头底 1.08:1），文字 5.04:1；
     · 「本页目录」胶囊底 = var(--docs-float-bg) = rgba(255,255,255,.92)（亮色）——
       是个**近纯白**的胶囊，与「目录」的透明胶囊（134,134,134）差了 3.37:1，
       两个按钮像两个不同组件 ✗；暗色下它是 rgba(0,0,0,.72)，叠在黑页头上合成 #000，
       与「目录」的 #0d0d0d 只差 1.08:1 —— 两个胶囊本体都**看不出来**，糊在一起 ✗。
   修法：这一档下两个按钮统一走**文档页专用**胶囊 token（两主题各自一套，见 css/base.css
   Phase 2g），并且：
     · 两者共用同一个底色 --docs-hero-chip-bg（不再一个近白一个透明）；
     · 「本页目录」用强一档 --docs-hero-chip-bg-strong 表示「这是右栏目录的开关」，
       与「目录」拉开一档层级（底差 ≈1.12:1 + 各自描边），不靠「一个近白一个近透明」这种
       跨组件级别的差异来区分 ✔；
     · 描边统一 --docs-hero-chip-border（胶囊边界靠它给出来，不再依赖底色浓淡）；
     · 文字/图标一律 var(--text)：**两主题**下在各自胶囊底上对比度都远超 4.5:1 ✔
       （实测：亮色 15.95:1 / 暗色 19.17:1，「本页目录」14.3:1 / 16.48:1）。
   ⚠ 两条硬约束：
     ① 整块**必须包在 @media (max-width: 1199px) 里**：`.docs-hero-actions .docs-menu-btn` 的
        specificity 是 0,2,0，会盖过基础规则里的隐藏规则。第一版就是漏了这层包裹，于是
        ≥901px 时「目录」按钮也被点了出来（实测 1440 下 visible ✗，`display:none` 被 0,2,0
        顶掉）—— 现在按「按钮本来就只活在这个宽度区间」的原口径收进媒体查询里，
        宽屏的隐藏规则完全不受影响 ✔。
     ② 这里**不写 display**：显隐仍由既有两条规则负责（窄屏 `.docs-menu-btn` 打开、
        `.docs-hero-actions .docs-toc-toggle` 打开、≥1200px 关闭），避免出现第二个显隐口径。 */
@media (max-width: 1199px) {
    .docs-hero-actions .docs-menu-btn,
    .docs-hero-actions .docs-toc-toggle {
        align-items: center;
        gap: 6px;
        flex: none;
        white-space: nowrap;
        max-width: 100%;
        min-height: 36px;
        padding: 6px 12px;
        border: 1px solid var(--docs-hero-chip-border);
        border-radius: var(--radius-pill);
        background: var(--docs-hero-chip-bg);
        color: var(--text);
        font-size: 13px;
        font-weight: 500;
    }

    .docs-hero-actions .docs-toc-toggle {
        background: var(--docs-hero-chip-bg-strong);
        padding: 6px 14px;
    }

    .docs-hero-actions .docs-menu-btn:hover,
    .docs-hero-actions .docs-toc-toggle:hover {
        background: var(--surface-glass-strong);
        border-color: var(--border-glass-strong);
    }

    /* 展开态：文字与描边转主色。specificity 0,3,0 > 上面那条 0,2,0 → 稳赢，
       两个主题都成立（--primary / --primary-line 在 base.css 里两主题各一份）✔ */
    .docs-hero-actions .docs-toc-toggle[aria-expanded="true"] {
        color: var(--primary);
        border-color: var(--primary-line);
    }
}

/* 901–1199px：目录收成「按钮开合」的面板。
   ⚠⚠ v=34 修（用户报「平板大小点本页目录不显示 / 显示不全」）：
      原来面板留在 .docs-rail-inner 里，而这一档的**第三列宽度是 0**
      （.docs-shell 的 grid-template-columns 第三列 = 0，为的是让正文拿满宽度），
      .docs-rail-inner 又是 `overflow-y: auto`（按 CSS 规则 overflow-x 也变 auto）
      ⇒ 面板被裁成一条 **30px 的缝**（实测：面板 width=30、left=960、right=990），
      文字一个字都看不见 ✗。
   现在把它做成**贴右的浮层**：position: fixed 脱离那条 0 宽度的列，右缘与正文右缘同一条线，
   吸在固定导航栏下方（--docs-sticky-top 由 main.js 实测写入）。
   层级：它在 .main（z-index 45，见 css/layout.css）里 ⇒ 盖得住站点横幅（40），
   而固定导航栏（50）仍然在最上 ✓；面板自身 z-index 只要高于正文即可。 */
@media (min-width: 901px) and (max-width: 1199px) {
    /* 面板已改成 fixed 浮层，这一档的 rail-inner 不再承担"滚动容器"的角色：
       overflow: visible 顺手排除掉「祖先裁切 fixed 后代」的可能（有 transform/filter 的祖先才会裁，
       这里没有，但少一层依赖更稳）。 */
    .docs-rail-inner {
        overflow: visible;
    }

    .docs-toc {
        display: none;
        position: fixed;
        top: calc(var(--docs-sticky-top, 80px) + 12px);
        /* 右缘 = 正文右缘（容器右内边距那一条线） */
        right: var(--margin-desktop, 24px);
        width: 264px;
        max-height: min(68vh, calc(100vh - var(--docs-sticky-top, 80px) - 40px));
        overflow-y: auto;
        overscroll-behavior: contain;
        scrollbar-width: thin;
        margin: 0;
        padding: 14px 16px 16px;
        border: 1px solid var(--border-subtle);
        border-radius: var(--radius);
        background: var(--surface-float, var(--surface-low));
        box-shadow: 0 18px 48px var(--shadow-soft, rgba(0, 0, 0, 0.38));
        z-index: 30;
    }

    .docs-toc.is-open {
        display: block;
    }

    /* 面板模式里标题只是标题：不当 details 的开合开关（内容一关整个面板就塌了） */
    .docs-toc > .docs-toc-title {
        cursor: default;
    }
}

/* ≤900px：单栏。目录树（抽屉）→ 本页目录（可折叠块，默认收起）→ 正文。
   ⚠「本页目录」按钮在这种宽度下**也不能滚走**：它和目录同属 .docs-rail-inner，
     那个外框在窄屏同样是粘性的（吸附点避开固定导航栏，见下面的窄屏断点）。 */
@media (max-width: 900px) {
    /* ============ ⑤ 窄屏「本页目录」必须**默认收起** ============
       真因（本轮 375/768 实测，这就是用户截图里"移动端默认显示目录条目"的原因）：
         ≤1199px 那条把 `<summary class="docs-toc-title">` 整体 `display: none`
         （为了不让「本页目录」出现两次），而 Chrome 在**没有可渲染的 summary** 时
         **无视 `<details open>` 的有无、直接渲染全部内容**：
           实测 375px：details.open === false、class 里没有 is-open、按钮 aria-expanded="false"
           —— 状态全是"收起"，但 `#docs-toc-list` 仍然 display:grid、高 211px、5 个条目全部可见 ✗✗
         （`getBoundingClientRect()` 也确实是 211px，不是缓存的假高度；所以判据必须
           「details.open + 元素自身 display/高」两条一起看，见验收脚本。）
         ≤1199px 里的 901–1199 那一档之所以没露馅，是因为它显式写了 display:none/.is-open；
         **只有 ≤900px 这一档把收起交给了原生 <details>** ⇒ 只有它在漏 ✗。
       修法：≤900px 与 901–1199px 用**同一套** display 口径（.is-open 才显示），
       与 #docs-toc-toggle 的 aria-expanded / .is-open 状态一一对应，不新增第二套开关状态。 */
    .docs-toc {
        display: none;
        margin-top: 10px;
        padding: 0 16px;
        background: var(--surface-glass-faint);
    }

    .docs-toc.is-open {
        display: block;
    }

    .docs-toc > .docs-toc-title {
        min-height: 46px;
        cursor: pointer;
        list-style: none;
        color: var(--text);
        font-size: 12.5px;
        text-transform: none;
        letter-spacing: 0.04em;
        -webkit-tap-highlight-color: transparent;
    }

    .docs-toc > .docs-toc-title::-webkit-details-marker {
        display: none;
    }

    .docs-toc > .docs-toc-title::marker {
        content: none;
    }

    .docs-toc-arrow {
        display: inline-flex;
    }

    .docs-toc[open] .docs-toc-arrow {
        transform: rotate(180deg);
        color: var(--primary);
    }

    .docs-toc-list {
        margin-bottom: 14px;
        max-height: 45vh;
        overflow-y: auto;
        overscroll-behavior: contain;
    }

    .docs-toc-link {
        padding-top: 9px;
        padding-bottom: 9px;
    }
}

/* ---------- 上一条 / 下一条 ----------
   ⚠ 宽度必须与正文列一致（--docs-text-w）：原来 .docs-pager 自己**没有**任何 max-width
   （82ch 只写在 .docs-body 上），于是它横向铺满整个内容区；再叠加「两块 flex:1 1 220px」，
   只剩下一篇时那一格会被拉到半屏以上 —— 用户看到的就是「几乎撑满整屏的空白大盒子」。
   现在：两块并排各占一半；只有一块时它只占自己那一格（另一格留白、没有任何盒子），
   JS 侧不渲染的那一侧连元素都不存在。 */

.docs-pager {
    max-width: var(--docs-text-w);
    margin-top: 56px;
    padding-top: 24px;
    border-top: 1px solid var(--border-faint);
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 16px;
}

.docs-pager-link {
    display: grid;
    gap: 4px;
    min-width: 0;
    padding: 14px 18px;
    border: 1px solid var(--border-faint);
    border-radius: var(--radius);
    background: var(--translate-card);
    transition: border-color var(--transition-fast), background var(--transition-fast);
}

.docs-pager-link:hover {
    border-color: var(--border-subtle);
    background: var(--translate-card-hover);
}

.docs-pager-prev {
    grid-column: 1;
    text-align: left;
}

.docs-pager-next {
    grid-column: 2;
    text-align: right;
}

/* 只有一侧时：显式钉在左格 / 右格，另一格**不留盒子** */
.docs-pager-only-prev .docs-pager-prev {
    grid-column: 1;
}

.docs-pager-only-next .docs-pager-next {
    grid-column: 2;
}

.docs-pager-label {
    color: var(--text-faint);
    font-family: var(--font-mono);
    font-size: 11px;
}

.docs-pager-name {
    color: var(--text);
    font-size: 14px;
    font-weight: 500;
    line-height: 1.5;
    overflow-wrap: anywhere;
}

/* （这里原来记着 .docs-foot 的自建页脚说明，已随 .docs-foot 一起删除：
   返回首页等站内入口现在由**站点共享页脚**提供 ——
   js/modules/site-footer.js + <footer class="footer" id="site-footer" data-base="../">。） */

/* ============ 窄屏（≤900px）：侧边栏折叠为抽屉 ============ */

/* 抽屉内的关闭按钮（⑥）：**基础态不显示** —— 宽屏（≥901px）侧栏是常显的网格列，
   不需要 ✕；只有 ≤900px 的整屏抽屉形态才打开它（下面的媒体查询里 display: inline-flex）。
   ⚠ 节点由 docs/js/main.js 在注入侧栏骨架时创建（宽屏下它 display:none、不占位、不可聚焦）。
   放在媒体查询**之前**：同特异性（0,1,0）下后者胜 → 只有 ≤900px 才显示 ✔ */
.docs-drawer-close {
    display: none;
}

@media (max-width: 900px) {
    /* 顶部让位沿用同一套变量（导航栏 box 随切换条/换行变化，窄屏少留一点）。
       ⚠ 同样用**扁平**写法而不是 `calc(max(var(...), 74px) + 4px)`：嵌套 calc+max+var 有概率
         解析失败、让位静默变 0（见文件顶部 .docs-main 的说明）。窄屏兜底 74px
         = 窄屏无切换条时的导航栏 box 高度（responsive.css 里 --bt-bar-h:28 + --bt-nav-inner-h:30）。 */
    .docs-main {
        /* 注释才能让上面贴着 */
        /* padding-top: max(var(--bt-banner-offset, 74px), 74px); */
        padding-bottom: 48px;
    }

    /* 页头在窄屏是「抽屉开关」的粘性条（毛玻璃同导航栏）：页头里现在只有那个按钮。
       ⚠ 底色用**文档页专用** token（见文件上方 .docs-hero 与 css/base.css 的 Phase 2g）：
         原来这里又写了一遍 var(--scrim-strong)（亮色 = rgba(0,0,0,.45) 的中灰）✗。 */
    .docs-hero {
        position: sticky;
        top: calc(var(--bt-nav-inner-h, 30px) + 28px);
        z-index: 30;
        align-items: center;
        /* 上下两条留白都走同一个令牌（见 :root 的 --docs-hero-gap 说明）：
           上 = 「按钮组顶边到横幅/导航底边」的那一档小间隙（用户要求"别贴太紧"），
           下 = 按钮组到正文的间距。原来写死 20px / 8px，与 901~1199px 那一档不一致。 */
        margin-bottom: var(--docs-hero-gap);
        padding-top: var(--docs-hero-gap);
        padding-bottom: 8px;
        border-bottom: 1px solid var(--border-faint);
    }

    /* 窄屏页头是粘性条：锚点落点必须再让开它的高度（--docs-hero-strip-h，说明见下面 :root 那段），
       否则 h2 会停在固定层底边处、正好被这条粘性条压住 ✗。
       再叠加 --docs-anchor-gap 的 16px 余量，标题上沿与粗体字的上伸部都不会被压边 ✔。
       --docs-sticky-top 由 main.js 的 syncStickyTop() 实测写入（固定导航栏底边，视口坐标）。 */
    :root {
        /* ============ 窄屏（≤900px）：粘性页头那一层 ============
           .docs-hero 在这一档是 **sticky**（top: --bt-nav-inner-h + 28px，见上面那条），
           所以「视口顶部被固定/粘性元素占掉的高度」= 导航栏盒高（--docs-sticky-top，实测）
           + 粘性页头条高（下面这个常数）。
           为什么需要一个常数：v=12 及以前吸附点写的是「正文自然顶边（文档坐标）」，
           那个值**顺带**把粘性页头的高度算进去了 —— 口径改成视口坐标后不再有这一项，
           必须显式补回来，否则下面三处（抽屉顶边 / 右栏「本页目录」吸附点 / 锚点落点）
           会一起钻到粘性页头条**底下**、被它压住 ✗。
           60px ≈ 粘性条实际高度 61px 取小一档（16 上留白 + 按钮 36 + 8 下留白 + 1px 下边框；
           实测值见验收脚本）。数字跟着 .docs-hero 的上下留白走：padding-top 从 8px 提到 16px
           后条高 +8px，这里也要 +8px（v=12 就是这么来的），不然锚点落点会被压住 8px ✗。 */
        --docs-hero-strip-h: 60px;
        /* 锚点落点 = 吸附点 + 粘性页头条高 + 留白。
           ⚠ 不再含横幅高度（横幅是流内元素、会滚走，见文件顶部 --docs-anchor-offset 说明）。 */
        --docs-anchor-offset: calc(var(--docs-sticky-top) + var(--docs-hero-strip-h) + var(--docs-anchor-gap));
    }

    /* ⚠ 窄屏的 .docs-hero-title / .docs-hero-sub 两条规则已随页头那块文字一起删除：
         全页可见标题只保留正文里的 H1 一处。 */

    .docs-menu-btn {
        flex: none;
        display: inline-flex;
        align-items: center;
        gap: 6px;
        min-height: 36px;
        padding: 6px 12px;
        border: 1px solid var(--border-subtle);
        border-radius: var(--radius-pill);
    background: var(--surface-glass-soft);
        color: var(--text);
        font-size: 13px;
        font-weight: 500;
    }

    .docs-menu-btn:hover {
    background: var(--surface-glass-strong);
    }

    /* 单栏：目录树（抽屉）→ 本页目录（可折叠）→ 正文，顺序由 order 定死，
       不依赖 DOM 里 .docs-rail 排在 .docs-content 之后这种实现细节。 */
    .docs-shell {
        grid-template-columns: minmax(0, 1fr);
        row-gap: 24px;
    }

    .docs-sidebar {
        order: 1;
        position: fixed;
        /* ================= ⑥ 抽屉：铺满整个宽度 + 最顶层（用户第四轮） =================
           实测到的三个现象（375px，改前）：
             · 宽度 = min(86vw, 320px) ⇒ 在 ≥372px 的视口上被 320px **封顶**：
               515px 宽的手机上只有 320/515 ≈ 62%，右侧露出一整条正文
               —— 就是用户截图里的「只显示了一半」✗；
             · top = var(--docs-sticky-top) = 实测吸附点（v=12 时它等于「正文自然顶边」，
               375 下实测 171px）⇒ 上方那条只到导航栏底边，抽屉与导航栏之间空着；
             · z-index: 45 **低于**「回到顶部」浮窗的 900（css/components.css 的 .to-top）
               ⇒ 那个浮窗压在抽屉上，看起来"被别的东西压着"✗。
           修法与**口径**：抽屉 = **满宽**（width: 100% = 视口宽，left/right 都 0，
           不再被 320px 封顶）+ **铺满视口**（top: 0，见下面 v=15 的说明 —— 这一轮从
           「从粘性页头之下开始」改成了「连导航栏那一条一起接管」，原因就是旧 top 算式
           会算成负数、然后被导航栏压住）。
           · z-index: 990 —— 取本仓库**既有层级刻度**里的「全屏接管层」：
               .to-top 浮窗 900 < 本层 990（本文件与 css/article.css 的媒体灯箱也是 990）
               < 弹窗 / toast 1000（css/components.css）< 活动页 toast 1200（css/activity.css）；
             990 只在 .main 这个层叠上下文里比较 ⇒ 还需要 v=15 那条
             `body.docs-drawer-open .docs-main { z-index: 60 }` 才能真正盖过导航栏（50）；
             而「确认弹窗 / toast」在外层且层级更高，仍能盖住抽屉 ✔。
           · 浮窗虽然层级更低，但固定右下角、仍是"抽屉上悬着一个圆"的伪影来源，
             所以再补一条 `body.docs-drawer-open .to-top { display: none }`（见下面），
             与文章页「灯箱打开时隐藏返回顶部」同一套做法（css/article.css 末尾）✔。 */
        /* ================= ⑥ 抽屉：铺满视口 + 真正的最顶层 =================
           ⚠⚠ v=15 修（用户报「被导航栏的内容遮挡住了」「关闭按钮压在搜索框上」）：
             旧写法 top: calc(var(--docs-sticky-top) - 12px - var(--docs-hero-strip-h))，
             v=13 起 --docs-sticky-top 已经**只等于导航栏底边**（不再含粘性页头条高），
             于是这个式子变成「导航栏底边 − 12 − 页头条高」⇒ 375px 实测 = 69.6−12−60 = **-2.4px**
             ⇒ 抽屉从**视口最顶上**开始铺，而它在 .main（z-index 45）的层叠上下文里、
             固定导航栏是 50 ⇒ 抽屉顶部那一条（正好是 ✕ 与搜索框）被导航栏**压在下面** ✗✗
             —— 用户看到的就是"内容被导航栏挡住"。
            v=15 的口径（简单、且不依赖任何 JS 实测值）：
             · top: 0 / bottom: 0 ⇒ 抽屉**铺满整个视口**（连导航栏那一条一起接管）；
             · body.docs-drawer-open 时把 .docs-main 抬到 60（> 导航栏 50、切换条 50）
               ⇒ 抽屉真正在最顶层，没有任何东西压着它；关掉抽屉即恢复（45），
                 所以「导航栏必须压住正文」这条不受影响；
             · 关闭入口依旧三条：抽屉内的 ✕（现在与搜索框同一行、右侧）、Esc、
                 点任意目录条目（点即跳转并自动关抽屉）。浮窗 .to-top（900）由下面
                 `body.docs-drawer-open .to-top { display: none }` 收掉；
                 toast / 确认弹窗（1000）仍然在抽屉之上（不能抢）。
             · 顶部再让出刘海/状态栏：padding-top 取 max(14px, env(safe-area-inset-top))。 */
        top: 0;
        bottom: 0;
        left: 0;
        right: 0;
        width: 100%;
        max-width: none;
        z-index: 990;
        max-height: none;
        /* 抽屉内部：**关闭按钮与搜索框同一行**（grid 两列），
           搜索框因此不用再给绝对定位的 ✕ 让位、也不会被它压住（用户报的第二点）。
           其余孩子（搜索无结果提示 / 搜索结果 / 目录树）都跨两列。 */
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        align-content: start;
        column-gap: 10px;
        row-gap: 0;
        padding: max(14px, env(safe-area-inset-top, 0px)) 16px 32px;
        border: 0;
        border-radius: 0;
        background: var(--surface);
        transform: translateX(-102%);
        visibility: hidden;
        transition: transform 0.3s var(--ease), visibility 0.3s var(--ease);
    }

    .docs-sidebar.is-open {
        transform: none;
        visibility: visible;
    }

    /* 抽屉打开时把 .main 抬到固定导航栏（z-index 50）与切换条（50）之上：
       ⚠ 抽屉 / 遮罩 / 粘性页头都在 .main 这个层叠上下文里，不抬这一档的话
         「抽屉无论如何画不到导航栏之上」——那正是用户报的"被导航栏内容遮住"。
       抽屉铺满视口，所以抬起来不会让别的内容跑到导航栏前面（它们都被抽屉盖着）；
       关掉抽屉（类移除）立刻回到 css/layout.css 的 45 ⇒ 导航栏照旧压住正文。 */
    body.docs-drawer-open .docs-main {
        z-index: 60;
    }

    /* 同一行：搜索框占满剩余宽度，✕ 固定 36px 贴在右侧、与搜索框垂直居中对齐 */
    .docs-sidebar > .docs-search {
        grid-column: 1;
        grid-row: 1;
        min-width: 0;
    }

    .docs-sidebar > .docs-search-empty,
    .docs-sidebar > .docs-search-results,
    .docs-sidebar > .docs-tree {
        grid-column: 1 / -1;
    }

    /* 抽屉的**表面**再往上补一条：从粘性页头的底边一直到抽屉自己的顶边
       （= 「吸附点 + 粘性页头条高」上方 var(--docs-hero-gap) 的那一档）。
       为什么需要：抽屉顶边 = 吸附点 + 粘性页头条高，而粘性页头**底边**比它高
       var(--docs-hero-gap) ⇒ 两者之间那 16px 会露出正文（被遮罩压暗但仍看得见字），
       截图里就是"页头按钮行与抽屉之间夹着一条正文残影"的接缝 ✗。
       补这一条之后，抽屉的**表面**从页头底边一直铺到视口底边（用户要的"占满整个侧边"），
       而抽屉**盒子**的顶边不动 ⇒ 「右栏外框顶边 == 正文顶边 == 左栏吸附点」的验收口径不受影响 ✔。
       ⚠ v=13 起 top 里必须含 --docs-hero-strip-h：吸附点已经只是「导航栏底边」，不再顺带
         包含粘性页头高度（v=12 那个口径把两者混在一起了，见文件顶部 :root 说明）。
       ⚠ 只补这一档、不补到视口顶：再往上就是导航栏 + 页头那两条**必须继续可点**的东西
         （「目录」按钮在 z 30，遮罩在 z 40）—— 它们被盖住的话，用户就找不到关闭入口了。
       ⚠ 层级说明（诚实口径）：.docs-sidebar 在 .main 的层叠上下文里
         （css/layout.css 给 .main 定了 position: relative + z-index: 10），
         所以它**无论写多大的 z-index 都画不到导航栏（z 50）之上** ——
         这不是本次能达到、也不是本次需要的：几何上抽屉顶边本来就在导航栏底边之下，
         两者不重叠，用户报的"被遮挡"来自浮窗（.to-top 900，body 直系子元素），
         那一个已经由 `body.docs-drawer-open .to-top { display: none }` 收掉了 ✔。
       ⚠ 关闭时随 .docs-sidebar 的 visibility: hidden 一起不可见（后代伪元素继承）✔ */
    .docs-sidebar::before {
        content: '';
        position: fixed;
        top: calc(var(--docs-sticky-top, 80px) + var(--docs-hero-strip-h) - var(--docs-hero-gap));
        left: 0;
        right: 0;
        height: var(--docs-hero-gap);
        pointer-events: none;   /* 不抢顶部那两条（页头按钮 / 遮罩）的点击 */
    }

    /* 抽屉内的关闭按钮（✕）：只有整屏抽屉形态才需要（宽屏侧栏是常显列，不需要）。
       ⚠ v=15：从「绝对定位贴在抽屉右上角」改成**grid 第一行的第二列**
         （与搜索框同一行、垂直居中、固定 36px）—— 旧写法 top:14/right:14 的 ✕ 直接压在
         搜索输入框上（搜索框也从 18px 起、宽度铺满）✗，用户报「一直挡着不好看」。
         现在搜索框在列 1、✕ 在列 2，两者互不重叠；搜索框自己右侧的 36px 内边距
         （.docs-search-input 的 padding-right，给"清空"按钮用）也因此不会被 ✕ 抢。 */
    .docs-drawer-close {
        grid-column: 2;
        grid-row: 1;
        align-self: center;
        justify-self: end;
        z-index: 1;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 36px;
        height: 36px;
        padding: 0;
        border: 1px solid var(--docs-hero-chip-border);
        border-radius: var(--radius-pill);
        background: var(--docs-hero-chip-bg-strong);
        color: var(--text);
        cursor: pointer;
        -webkit-tap-highlight-color: transparent;
    }

    .docs-drawer-close:hover {
        background: var(--surface-glass-strong);
        border-color: var(--border-glass-strong);
    }

    /* 抽屉打开时把「回到顶部」浮窗收掉：它固定在右下角、层级 900 < 抽屉 990，
       虽然会被盖住，但那条"抽屉上悬着一个圆按钮"的观感正是用户说的「被遮挡」源头之一；
       与文章页灯箱同一套处理（css/article.css 的 `.art-lightbox` 打开时隐藏 #to-top）。 */
    body.docs-drawer-open .to-top {
        display: none;
    }

    .docs-backdrop {
        order: 2;
    }

    .docs-rail {
        order: 3;
        grid-column: 1;
        /* 窄屏是单栏顺序排布：sticky 元素的可移动范围 = 网格区域高度（= 自身高度），
           所以这里**不强求吸附**（需求也只要求 ≤900px 单栏时说得清楚即可）；
           目录默认收起成一行，展开后展开在正文上方。 */
        align-self: stretch;
    }

    /* ⚠ 目录面板**收起**时，右栏这一行里其实什么都没有
       （`#docs-toc-toggle` 被 syncHeroToolbar() 搬到页头了，剩下唯一的子节点
         `<details class="docs-toc">` 在这一档是 display:none —— 见本文件 ⑤ 那一段）。
       可这一行仍然占着 .docs-shell 的 `row-gap: 24px`，把正文白推下 24px，
       同时让「右栏外框顶边」比「正文顶边」高 24px（验收口径要求三者 ≤2px）。
       这里在「面板没被打开」时把这 24px 吃掉：正文顶边 == 右栏外框顶边 == 左栏吸附点，
       三者重合 ✔；面板一旦打开（.is-open），这条**自动失效**、24px 间距照常回来，
       不会让展开的目录压在正文上 ✗。
       ⚠ 只在 ≤900px 单栏这一档生效（宽屏是三列网格、列间距不是 row-gap，绝不能被它影响）。 */
    .docs-shell:not(:has(.docs-toc.is-open)) .docs-content {
        margin-top: -24px;
    }

    /* ⚠ 窄屏下「本页目录」按钮**不许滚走**：外框 .docs-rail-inner 依然粘性，
       吸附点 = 固定导航栏底边（实测 --docs-sticky-top）+ 粘性页头条高（--docs-hero-strip-h）——
       不加上页头那一层，「本页目录」按钮/面板会被粘性页头条压住 ✗。
       v=12 那一版之所以不用加，是因为当时的吸附点是「正文自然顶边（文档坐标）」，
       顺带把页头高度算进去了；口径改成视口坐标后必须显式补（见 ≤900px 的 :root 说明）。 */
    .docs-rail-inner {
        top: calc(var(--docs-sticky-top, 80px) + var(--docs-hero-strip-h));
        max-height: none;
        overflow: visible;
    }

    /* 窄屏（≤900px）：目录收在抽屉里（由 #docs-menu-btn 打开），这个按钮是**唯一**的控制件，
       文字「本页目录」只由它显示（<summary> 整体 display:none，见上面 ≤1199px 那条）。
       ⚠ 这里**不写 display**：显隐统一归上面 `@media (max-width: 1199px)` 管，
         基础规则是 display:none —— 窄屏再写一次 inline-flex 没坏处，但会让
         控制显隐的地方从一个变成两个，下次改动容易只改一边。 */
    .docs-toc-toggle {
        align-items: center;
        gap: 6px;
        flex: none;
        white-space: nowrap;
        max-width: 100%;
        min-height: 36px;
        padding: 6px 14px;
        border: 1px solid var(--border-subtle);
        border-radius: var(--radius-pill);
    background: var(--docs-float-bg);
        backdrop-filter: blur(12px);
        -webkit-backdrop-filter: blur(12px);
        color: var(--text);
        font-size: 13px;
        font-weight: 500;
    }

    .docs-toc-toggle[aria-expanded="true"] {
        color: var(--primary);
    border-color: var(--primary-line);
    }

    .docs-content {
        order: 4;
    }

    .docs-body {
        font-size: 15.5px;
    }

    .docs-body h2 {
        font-size: 21px;
    }

    /* 窄屏上下篇竖排（独占一行），不并排、不溢出 */
    .docs-pager {
        grid-template-columns: minmax(0, 1fr);
    }

    .docs-pager-prev,
    .docs-pager-next,
    .docs-pager-only-prev .docs-pager-prev,
    .docs-pager-only-next .docs-pager-next {
        grid-column: 1;
        text-align: left;
    }
}

/* ============ 正文里的媒体（图片 / 动图 / 视频 / 音频） ============
   渲染管线在 docs/js/main.js：img 会被包成 <a class="docs-media-link" data-docs-lightbox>
   → 点开是**灯箱预览**（复用站点文章灯箱那套交互：遮罩 + ✕ + Esc + ←→ + 缩略图 +
   键盘可达）。视频/音频一律用**原生控件**，不自己做播放器。
   所有媒体都是 display:block + max-width:100% + 高度 auto → 任何宽度下都不会横向溢出。
   动图（GIF）走的就是普通 <img>，灯箱里保持动画（img 元素本身没有被替换）。 */

.docs-body img,
.docs-media-link img {
    display: block;
    max-width: 100%;
    height: auto;
    border: 1px solid var(--border-faint);
    border-radius: var(--radius-sm);
    background: var(--surface-glass-faint);
}

/* ⚠ 必须是 inline-block（以前是 block ✗）：<a> 是**块级**时会横向铺满整个正文列，
   而里面的 <img> 只有图片自己的宽度（示例图 480px / 320px，正文列 678px）——
   于是 `::after` 的 right:8px 贴的是**铺满的 <a>** 的右下角，
   角标就飘到图片**右边外面**去了（用户截图里「飘在图片外面」就是这个 ✗）。
   inline-block 让 <a> 收缩到图片大小 → 角标正好落在图片框内的右下角 ✓。
   line-height:0 继续挡住 <a> 里的行盒空隙；图片本身是 display:block（见上一条）。 */
.docs-media-link {
    display: inline-block;
    max-width: 100%;
    line-height: 0;
    border-radius: var(--radius-sm);
    transition: opacity var(--transition-fast);
}

.docs-media-link:hover {
    opacity: 0.9;
}

/* 可点开预览的图片：右下角一个小角标，让「这图能点」一眼可见。
   图标用 **CSS 伪元素 + 字体连字**：content 里的 'zoom_in' 由字体连字渲染成图标，
   零 JS、零内联样式。
   ⚠ subset 字体的连字名是**逐个裁剪**出来的：名字不在 assets/fonts/icons.txt 里就没有
     连字规则，浏览器只能把 'zoom_in' 当**普通文字**画出来（实测就是这样 ✗）。
     本轮已把 zoom_in 的字形 + 连字规则追加进子集（做法见 assets/fonts/SOURCE.txt 第一节），
     并把 ?v= 提到 5；改这个 content 之前请先确认新名字在 icons.txt 里。 */
.docs-media-link[data-docs-lightbox] {
    position: relative;
}

.docs-media-link[data-docs-lightbox]::after {
    content: 'zoom_in';
    position: absolute;
    right: 8px;
    bottom: 8px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    border-radius: 999px;
    border: 1px solid var(--border-subtle);
    background: var(--docs-float-bg-soft);
    color: var(--text);
    font-family: 'Material Symbols Outlined', sans-serif;
    font-weight: normal;
    font-style: normal;
    font-size: 18px;
    line-height: 1;
    letter-spacing: normal;
    text-transform: none;
    font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
    -webkit-font-feature-settings: 'liga';
    font-feature-settings: 'liga';
    pointer-events: none;
}

/* 没有灯箱脚本时的兜底：图片本身还是普通图，链接点开就是原图 */
/* ⚠ 这里的 20px 曾经让媒体页的间隙和其它页不一样：
   `.docs-body > * + *`（下面正文统一 16px 节奏）与 `figure { margin: 20px 0 }` 同为
   0,1,0 特异性，胜负只看出现顺序 —— figure 规则在后面 → 相邻兄弟间的间距在「figure 参与」
   的地方变成 20px、其它地方 16px ✗。而且 `margin: 0`（上下都是 0）会**赢掉**
   `margin-top: 16px`，于是「前面是 figure」的那一段上下间距还是 20px，两种节奏混用。
   改成：figure 在 .docs-body 里**不自己出间距**（margin-top/bottom 归零），
   上下间距一律由 `.docs-body > * + *` 的 16px 决定 → 同一类元素的间距在任何页都一致 ✓。
   灯箱里的 .docs-lb-stage 也是 figure，它本来就有 `margin: 0`，不受影响。 */
.docs-body figure {
    margin-top: 0;
    margin-bottom: 0;
}

.docs-body figcaption {
    margin-top: 8px;
    color: var(--text-muted);
    font-size: 13px;
    line-height: 1.6;
    text-align: center;
}

.docs-body video,
.docs-body audio {
    display: block;
    max-width: 100%;
    border-radius: var(--radius-sm);
}

.docs-body video {
    height: auto;
    background: #000;
    border: 1px solid var(--border-faint);
}

.docs-body audio {
    width: 100%;
    margin-top: 4px;
}

/* ---------- 灯箱（复用文章详情页 .art-lightbox 的交互与层级；类名走 docs-* 前缀） ---------- */

.docs-lightbox {
    position: fixed;
    inset: 0;
    z-index: 990;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 4vh 4vw;
    background: rgba(0, 0, 0, 0.88);
}

/* ⚠ 本文件刻意**保留**的字面量（不主题化，属固定语境，两主题同值 ✗）：
   ① .docs-body video 的 background:#000 —— 视频信箱底色必须永远黑 ✗（跟主题变白是错的）
   ② 灯箱（.docs-lightbox 及其按钮/缩略图，L1201/1228/1238/1239/1240/1275/1276/1338）——
      灯箱是看大图的深色固定层，两主题都保持深底浅字（业界通行 ✔），
      若跟随亮色主题变成浅底，图片对比与按钮可读性反而更差 ✗。
   ③ 这两类都不是漏改 ✗：Phase 2 的令牌化只覆盖随主题应该变的表面/文字/边框 ✔。 */
.docs-lightbox[hidden] {
    display: none;
}

.docs-lb-stage {
    margin: 0;
    max-width: 92vw;
    max-height: 92vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
}

.docs-lb-stage img {
    max-width: 92vw;
    max-height: 78vh;
    object-fit: contain;
    border: 0;
    border-radius: 10px;
    background: none;
}

.docs-lb-stage figcaption {
    color: #d8d8d8;
    font-size: 14px;
    text-align: center;
}

.docs-lb-close,
.docs-lb-prev,
.docs-lb-next {
    position: absolute;
    z-index: 2;
    border: 1px solid rgba(255, 255, 255, 0.22);
    background: rgba(20, 20, 20, 0.72);
    color: #fff;
    border-radius: 999px;
    font: inherit;
    line-height: 1;
    cursor: pointer;
}

.docs-lb-close {
    top: 18px;
    right: 18px;
    width: 40px;
    height: 40px;
    font-size: 16px;
}

.docs-lb-prev,
.docs-lb-next {
    top: 50%;
    transform: translateY(-50%);
    width: 46px;
    height: 46px;
    font-size: 26px;
}

.docs-lb-prev {
    left: 18px;
}

.docs-lb-next {
    right: 18px;
}

.docs-lb-close:hover,
.docs-lb-prev:hover,
.docs-lb-next:hover {
    background: rgba(45, 45, 45, 0.9);
    border-color: rgba(255, 255, 255, 0.45);
}

.docs-lb-close:focus-visible,
.docs-lb-prev:focus-visible,
.docs-lb-next:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

/* 灯箱打开时锁页面滚动；同时压住「回到顶部」按钮，避免和灯箱叠在一起 */
html.docs-lb-open {
    overflow: hidden;
}

html.docs-lb-open .to-top {
    opacity: 0 !important;
    visibility: hidden;
    pointer-events: none;
}

.docs-lb-thumbs {
    position: absolute;
    left: 12px;
    right: 12px;
    bottom: 14px;
    display: flex;
    gap: 8px;
    justify-content: center;
    align-items: center;
    overflow-x: auto;
    overflow-y: hidden;
    padding: 6px 4px;
    scrollbar-width: thin;
}

.docs-lb-thumbs[hidden] {
    display: none;
}

.docs-lb-thumb {
    flex: 0 0 auto;
    width: 56px;
    height: 40px;
    padding: 0;
    border: 1px solid var(--border-glass-hover);
    border-radius: 6px;
    background: var(--surface-glass-hover);
    overflow: hidden;
    cursor: pointer;
}

.docs-lb-thumb img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    border: 0;
    border-radius: 0;
}

.docs-lb-thumb:hover {
    border-color: rgba(255, 255, 255, 0.6);
}

.docs-lb-thumb.is-on {
    border-color: var(--primary-container);
    box-shadow: 0 0 0 2px var(--accent-violet-line);
}

/* ⚠ 文件末尾原来有一整块 .docs-foot（文档站自己的轻量页脚，含 3 个自建链接）
   和一条 `.docs-hero .docs-toc-toggle { display: none }` 的兜底规则（页头里那个
   无 JS 语义占位按钮）。两者都已按用户要求**整块删除**：

   · docs/index.html 改为接入站点共享页脚
     <footer class="footer" id="site-footer" data-base="../"></footer> +
     js/modules/site-footer.js（与 sponsors.html / index.html 完全同一套，不再各写一份）；
     页脚贴底由 .docs-main 的 flex-grow: 1 保证（见本文件顶部 .docs-main）。
   · 页头里的占位按钮已删除；唯一控制件是右栏的 #docs-toc-toggle，
     ≥1200px 由 `.docs-rail .docs-toc-toggle { display: none }` 关闭。 */
