/* ============================================================================
   app-shell — 应用外壳（侧栏 + 顶栏 + 主区）
   ============================================================================
   为什么需要它
   ---------------------------------------------------------------------------
   本库已有的四个 recipe 都是**内容型**版式：
     · analysis-report —— 报告正文 + 辅助栏（读）
     · detail-page     —— 实体详情（查）
     · report          —— 版式系统，三种排版取舍（写）
     · table           —— 数据表（比）
   缺的是**工具型**骨架：左边一列常驻导航、上面一条工具栏、右边是主区。
   每个后台项目都在手写它，而且每一版都会踩同样的两个坑（下面写清了）。

   两个坑（这是本组件存在的**主要**理由）
   ---------------------------------------------------------------------------
   ① `height: 100vh` 在移动端是错的。
      浏览器地址栏收起/展开时视口高度会变，`100vh` 取的是**最大**那一个
      ⇒ 底部永远被裁掉一截（iOS Safari 上最明显）。
      正解是 `100dvh`（动态视口），并用 `100vh` 兜底给不支持的引擎。

   ② 侧栏 `position: fixed` 会把它从 grid 流里摘出去。
      后果是主区不知道侧栏存在，内容自己往左铺 ⇒ 要么被盖住，要么手动加
      `margin-inline-start`，然后在 RTL 里全错。
      正解是 `position: sticky`：它留在流里（grid 照常算宽度），
      滚到顶后又固定住。RTL 自动正确，因为用的是逻辑属性。

   设计要点
   ---------------------------------------------------------------------------
   ① 三区域 grid，不是"三个浮动的盒子"
   ② 侧栏 sticky + 自己滚（`overflow-y: auto`），主区跟着页面滚
   ③ `min-width: 0` 落在主区上 —— 少了它，一个宽表格就能把整个 shell 撑出横滚
   ④ 窄屏退化：侧栏 `display: none`，主区全宽。**折叠成抽屉不是本组件的活**
      —— 抽屉已经有 `03-patterns/drawer`，在这里再实现一遍就是第二份真值。
   ⑤ 本组件是**唯一**该用 `@media` 判宽度的：它本身就是"视口的容器"，
      没有"槽"可言。其余组件一律 `@container`（CONVENTIONS 第十一节）。
   ========================================================================== */

.shell {
  /* 组件私有槽位 */
  --shell-side-w:   15rem;      /* 侧栏宽；240px 是导航文字 + 图标不折行的下限附近 */
  --shell-topbar-h: 3.5rem;     /* 顶栏高 */
  --shell-gap:      var(--sp-4);
  --shell-pad:      var(--sp-4);
  --shell-bp:       48rem;      /* 侧栏收起阈值：768px 以下是"手机/窄平板" */

  display: grid;
  grid-template-columns: var(--shell-side-w) minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr);
  grid-template-areas:
    "side topbar"
    "side main";
  /* 🔴 100dvh + 100vh 兜底。顺序不能反：后写的优先，
     支持 dvh 的引擎用 dvh，不支持的退到 vh（底边被裁，但至少不塌成 0）。 */
  min-height: 100vh;
  min-height: 100dvh;
  /* 🔴 底色铺在**外壳**上，而不是只给 `.shell__side`。
     ---------------------------------------------------------------------
     侧栏是 `sticky` + `align-self: start`（见下），它的高度只到内容为止。
     如果底色只铺在侧栏元素上，条目少的页面会看到"侧栏背景只到一半"，
     下面露一条纸色 —— 那看起来像渲染坏了，其实是高度自然。

     解法不是给侧栏加 `min-height: 100dvh`（那会让它在 demo 的固定高度容器里
     顶出去，也会让"内容不足一屏"的页面底部多一块死色），
     而是把侧栏色铺在整个外壳上，再让主区盖回纸色：

       .shell        = 侧栏色
       .shell__main  = 纸色

     这个分层不依赖任何方向，RTL 下自动正确。 */
  background: var(--surface);
}

/* ============================================================================
   1 · 侧栏
   ============================================================================ */
.shell__side {
  grid-area: side;
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  padding: var(--shell-pad) var(--sp-3);
  /* 底色与分界线都**不**画在侧栏上：
     · 底色 —— 侧栏高度只到内容为止（`align-self: start` 的必然结果），
       画在这里会出现"侧栏背景只到一半"
     · 右边框 —— 同理，会变成"只到一半的竖线"，看起来像渲染断了
     两者都交给 `.shell`（底色）与 `.shell__main`（左边框，全高）。 */
  /* 🔴 sticky 而不是 fixed —— 见文件头坑 ②。
     `align-self: start` 是必需的：grid 项默认 stretch，
     stretch 之后元素和容器一样高，"粘住"就无从谈起。 */
  position: sticky;
  inset-block-start: 0;
  align-self: start;
  /* 🔴 自己滚：侧栏条目多的时候，主区不该跟着一起滚走。
     高度取视口高（同样 dvh + vh 兜底）。 */
  max-height: 100vh;
  max-height: 100dvh;
  overflow-y: auto;
  /* 滚动链：侧栏滚到底后不要接着滚主区（iOS 上的橡皮筋会很难受） */
  overscroll-behavior: contain;
  /* 强制色预留：边框已存在，这条是为了让"侧栏边界"在强制色下更明确 */
  outline: 1px solid transparent;
  outline-offset: -1px;
}

.shell__brand {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-2) var(--sp-3);
  font-weight: 500;
  color: var(--text-primary);
  text-decoration: none;
  /* 品牌名过长时截断，不要把侧栏撑宽 */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 品牌的两段：标记（图形）+ 文字。
   🔴 为什么必须分成两段，而不是让品牌名当纯文本：
     图标态（`.shell--rail`）侧栏只有 4.5rem，品牌名放不下。
     纯文本只能截成「金…」，而 `display: none` 掉整个 `.shell__brand`
     又等于没有品牌。分成两段后，rail 下只留标记、文字视觉隐藏
     —— 与 `.shell__link-label` 同一套手法、同一个理由：
     文字留在 DOM 与可访问树里，宽屏/抽屉里能原样回来，名字仍只有一份。
   ⚠️ `.shell__brand-mark` 用 `currentColor` 画（不用 <img>）：
      强制色下 `currentColor` 会被换成 `CanvasText`，图形还在；
      `<img>` 里的颜色则完全不受当前颜色影响。 */
.shell__brand-mark {
  flex: 0 0 auto;
  display: inline-flex;
  color: var(--accent);
}

/* 文字自己截断：`.shell__brand` 上的 text-overflow 对 flex 子项不起作用 */
.shell__brand-text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.shell__side-group {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.shell__side-group + .shell__side-group {
  margin-top: var(--sp-2);
  padding-top: var(--sp-2);
  border-top: 1px solid var(--border-decor);
}

.shell__side-label {
  padding: var(--sp-2) var(--sp-2) var(--sp-1);
  font-size: var(--fs-xs);
  color: var(--text-tertiary);
  letter-spacing: var(--tracking-normal);
  text-transform: uppercase;
}

/* 侧栏条目：与 nav 组件的 .nav__link 视觉一致，但**不复用它的类** ——
   nav 是"页内目录 + 抽屉行为层"，语义不同；复用类会让两边的改动互相牵连。
   ⚠️ 但间距/圆角/焦点环必须与 nav 对齐，否则同一页里两处导航长得不一样。 */
.shell__link {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  /* 44px 命中区（CONVENTIONS 第八节） */
  min-height: var(--sz-touch);
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-sm);
  color: var(--text-secondary);
  font-size: var(--fs-sm);
  text-decoration: none;
  /* 长条目名截断，不撑宽侧栏 */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}

/* 当前页：**两条线索**，不只靠底色。
   左侧一条 3px 的竖条 + 文字变主色 + 字重加一档 —— 灰度打印也能分辨。 */
.shell__link[aria-current="page"] {
  color: var(--text-primary);
  background: var(--surface-sunken);
  font-weight: 500;
  /* 用 inset 阴影画竖条：不占布局，也不会被 overflow 裁掉 */
  box-shadow: inset 3px 0 0 var(--accent);
}

/* 焦点环（states 门禁 HIGH：缺 focus-visible 就是"键盘用户看不到自己停在哪"）。
   ⚠️ 用 `:focus-visible` 而不是 `:focus` —— 后者会让鼠标点一下也留个框。 */
.shell__link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* 悬停：只改文字色，与 `03-patterns/nav` 的 `.nav__link` 保持一致 ——
   两处导航在同一页里出现时，反馈方式不同会显得像两个库拼的。
   ⚠️ 门控 hover：触屏上"点一下"会先点亮 hover 再 click ⇒ 状态粘住。 */
@media (hover: hover) and (pointer: fine) {
  .shell__link:hover {
    color: var(--text-primary);
  }
}

/* RTL 下竖条换到右边。⚠️ box-shadow 的 x 偏移**不是**逻辑属性，
   必须显式翻 —— 这是本文件里唯一需要方向判断的地方。 */
[dir="rtl"] .shell__link[aria-current="page"] {
  box-shadow: inset -3px 0 0 var(--accent);
}

/* 强制色：inset 阴影会被整个移除（0.17.0 实测），
   所以在强制色下必须换一条**不靠颜色**的线索表达"当前项"。

   🔴 0.22.1 修的是这里的第一版做法：当时用
   `.shell__link { border-inline-start: 3px solid transparent }` 给所有项
   "免费预留"3px，只给当前项换成实色。实拍发现**预留不是免费的** ——
   强制色会把透明边框解析成实线（链接上实测是 LinkText 深蓝 rgb(0,0,159)，
   当前项是 CanvasText 黑），于是**五项全部长出一条 3px 竖线**，
   当前项与其它项的差别只剩一个色相 —— 正是本库反对的"只靠颜色"。

   ⇒ 改成：不预留边框（零位移），当前项用**字重 + 行首圆点**两条非颜色线索。
     圆点用系统色背景画（不是字符），位置用绝对定位 —— 不参与布局，也不进可访问名。 */
@media (forced-colors: active) {
  .shell__link {
    position: relative;
    box-shadow: none;      /* inset 阴影在强制色下会被整个移除，留着没意义 */
  }
  .shell__link[aria-current="page"] {
    font-weight: 700;
  }
  /* 圆点用**系统色背景**画，不用字符。
     实测：`content: "•"` 会被算进可访问名（读屏念成「圆点 总览」），
     而 `content: ""` + `background: CanvasText` 两者都不发生 —— 空内容不进名字，
     系统色背景不会被强制色替换（对比进度条的 `Highlight` 也是这么活的）。 */
  .shell__link[aria-current="page"]::before {
    content: "";
    position: absolute;
    inset-inline-start: 3px;      /* 微几何，不是节奏间距 ⇒ 不走 --sp-* */
    top: 50%;
    width: 5px;
    height: 5px;
    margin-top: -2.5px;
    border-radius: 50%;
    background: CanvasText;
  }
}

/* ============================================================================
   2 · 顶栏
   ============================================================================ */
.shell__topbar {
  grid-area: topbar;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: var(--shell-topbar-h);
  padding: 0 var(--shell-pad);
  background: var(--surface);
  border-block-end: 1px solid var(--border-decor);
  /* 顶栏也粘住：长表单里用户滚到一半看不见"保存"是最常见的抱怨 */
  position: sticky;
  inset-block-start: 0;
  z-index: var(--z-header);
}

/* 窄屏导航按钮（由 shell-nav.js 注入，见该文件头）。
   ----------------------------------------------------------------------------
   🔴 默认 `display: none`，只有在下面的窄屏 @media 块里才变 `inline-flex`。
      这个"默认隐藏"不是样式选择，是**契约的一半**：
        · JS 靠 `offsetParent === null` 判断"现在该不该有抽屉"，
          所以"什么时候显示"必须由 CSS 唯一决定，JS 一行数字都不写
        · 桌面端即使有人手贱引了 shell-nav.js，也不会多出一个多余的汉堡按钮

   ⚠️ 没有 JS 时这条规则仍然生效（按钮根本不存在），所以基础形态不受影响。 */
.shell__menu {
  display: none;
  align-items: center;
  justify-content: center;
  flex: none;                        /* 标题很长时不要让按钮被压扁 */
  width: var(--sz-touch);            /* 44px 命中区（CONVENTIONS 第八节） */
  height: var(--sz-touch);
  /* 视觉上贴起始边：负 margin 只收视觉，热区仍是 44px */
  margin-inline-start: calc(-1 * var(--sp-2));
  padding: 0;
  color: var(--text-secondary);
  background: none;
  /* 强制色预留：透明边框会被解析成 CanvasText 实线，不需要 media query */
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  cursor: pointer;
}

.shell__menu:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

@media (hover: hover) and (pointer: fine) {
  .shell__menu:hover {
    color: var(--text-primary);
    background: var(--surface-sunken);
  }
}

/* 靠右的一组操作（头像/通知/保存） */
.shell__topbar-end {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-inline-start: auto;
  /* 🔴 不许被压缩。默认 `flex-shrink: 1` 时，窄屏上这组会被标题挤到
     只剩一半宽，里面的文字**折成两行** —— 顶栏从 56px 长到 76px，
     看起来像"手机上顶栏突然变厚了"（0.19.0 实拍）。
     让它不收缩，标题自然会先截断（`.shell__title` 已经 `min-width: 0`）。 */
  flex: none;
}

/* ⚠️ 选择器带 `.shell__topbar` 前缀是为了压过页面骨架的 `.page h2`
   （(0,1,1) > (0,1,0)）。不加的话标题的上下 margin 会把顶栏从 56px 撑到 99px ——
   而"顶栏比设计高 43px"这种问题在截图里只像"留白多了点"，很容易放过。 */
.shell__topbar .shell__title {
  margin: 0;
  font-size: var(--fs-md);
  font-weight: 500;
  color: var(--text-primary);
  /* 标题长时截断，不要挤掉右侧操作区 */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ============================================================================
   3 · 主区
   ============================================================================ */
.shell__main {
  grid-area: main;
  padding: var(--shell-pad);
  /* 盖回纸色：外壳是侧栏色（见文件头分层说明） */
  background: var(--paper);
  /* 分界线画在**主区**上：它占满整个 grid 区（含"内容不足一屏"的部分），
     所以这条线始终是通高的。画在侧栏上就只到侧栏内容为止。 */
  border-inline-start: 1px solid var(--border-decor);
  /* 🔴 `min-width: 0` —— 整个组件最容易漏、后果最大的一行。
     grid 项默认 `min-width: auto`（= 内容的最小宽度），
     一个 12 列的宽表格就能把主区撑到 2000px，然后整个 shell 横滚。
     加了它，表格自己滚（配合 .shell__main 里的 overflow 容器）。 */
  min-width: 0;
}

/* 主区内容的宽度约束：正文别铺满 1920px。
   ⚠️ 用 `max-width` + `margin-inline: auto` 而不是 `padding` ——
      后者在窄屏上会把内容挤窄两次（padding + 视口）。 */
.shell__content {
  max-width: var(--measure-page);
  margin-inline: auto;
}

/* 🔴 宽内容的**唯一**正确容器。
   -------------------------------------------------------------------------
   `.shell__main { min-width: 0 }` 只保证"grid 轨道不被撑开"，
   它**不会**让溢出的内容自己滚 —— `overflow` 的默认值就是 `visible`，
   于是 80rem 宽的表格照旧溢出到页面上，整页横滚。

   第一版这里只写了"宽内容要在自己的 overflow 容器里滚"，
   却没给那个容器 ⇒ 契约是半截的（0.18.0 门禁 C2 抓到：375px 视口下
   docScrollWidth 748）。现在把它做出来：

     <div class="shell__scroll" tabindex="0" role="region" aria-label="宽表格，可横向滚动">
       <table>…</table>
     </div>

   🔴 `tabindex="0"` 与 `role="region"` 不是可选的（0.18.0 axe 实测报
      `scrollable-region-focusable`）：滚动容器不聚焦，键盘用户就**滚不动**它 ——
      鼠标用户可以拖滚动条，键盘用户没有任何办法看到右边那几列。
      `aria-label` 也不能省：`role="region"` 没有名字时读屏只会说"区域"，
      用户不知道这块是干什么的。

   判据：app-shell-check.js B1（主区不被撑破）与 C2（窄屏不横滚）。 */
.shell__scroll {
  overflow-x: auto;
  /* 横向滚到头之后不要接着滚页面（触控板上很容易误触） */
  overscroll-behavior-x: contain;
  /* 滚动条不占位：内容区不会因为"多了一条滚动条"而重新排版 */
  scrollbar-width: thin;
}

/* ============================================================================
   4 · 窄屏退化
   ============================================================================
   🔴 这里是**唯一**用 @media 判宽度的地方，理由写在文件头 ⑤：
      app-shell 就是"视口的容器"，它没有外层槽可言。

   🔴 纯 CSS 时：侧栏直接 `display: none`，页面上**没有**别的导航入口。
      这是**故意**的降级 —— 只读看板可以接受；管理型后台请加下面这一行脚本。

   🔴 要导航入口就引 `shell-nav.js`（行为层，同目录；START-HERE 归「必需」）：
      它把 `.shell__side` **克隆**进 `03-patterns/drawer`，于是 Esc 关闭、
      焦点陷阱、焦点归位、背景 inert、锁滚动五件事全部来自 drawer，
      本组件一行行为逻辑都不重写。
      （`03-patterns/nav` 做的是同一件"汉堡 → 抽屉"，但它的列表常驻在文档里、
        靠移动位置，而这里的侧栏在窄屏是隐藏的 ⇒ 生命周期相反，不能共用。）

   ⚠️ 别在这里自己写一份抽屉。抽屉要的不是"滑出来"，是那五件事 ——
      自己写必然漏两件，而且漏掉的那两件（焦点归位 / 背景 inert）
      在鼠标用户眼里完全正常。
   ========================================================================== */
@media (max-width: 47.99rem) {
  .shell {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr);
    grid-template-areas:
      "topbar"
      "main";
    --shell-pad: var(--sp-3);
  }

  .shell__side {
    display: none;
  }

  /* 侧栏收走 ⇒ 把导航入口交还给顶栏的菜单按钮。
     🔴 这一行是 shell-nav.js 的**唯一**开关：JS 读 `offsetParent` 决定有没有抽屉。
        （所以窄屏断点只有这一个数字，改这里就是改全部 —— JS 不会错位。） */
  .shell__menu {
    display: inline-flex;
  }
}

/* 🔴 抽屉里的那份侧栏必须**无视**上面那条 `display: none`。
   ----------------------------------------------------------------------------
   克隆出来的节点带着 `.shell__side` 类 ⇒ 它同样命中窄屏那条规则
   ⇒ 抽屉能打开、遮罩在、关闭按钮在，但**内容是空的**（0.19.0 实拍）。
   这不是"忘记加样式"，是"同一条规则对两种容器说了相反的话"：

     · 桌面侧栏 在窄屏应该消失（顶栏有菜单按钮）
     · 抽屉副本 在窄屏必须显示（它就是为了替代侧栏才存在的）

   所以给副本加修饰类，用 0,2,0 的选择器压过 0,1,0 的那条 ——
   不靠源码顺序，也不靠 `!important`。 */
.shell__side.shell__side--drawer {
  display: flex;
  /* 抽屉自己会滚（.drawer__body），别在里头再套一个滚动容器 */
  max-height: none;
  overflow-y: visible;
  position: static;
}

/* ============================================================================
   5 · 变体
   ============================================================================ */

/* 侧栏更窄（只有图标）：适合"工具条"型产品。
   ----------------------------------------------------------------------------
   🔴 图标态的文字用 `.shell__link-label` 包起来，这里只做**视觉隐藏**：

     <a class="shell__link" href="#" title="总览">
       <svg … aria-hidden="true">…</svg><span class="shell__link-label">总览</span>
     </a>

   为什么不是"删掉文字 + 写 aria-label"：
     ① 名字有两个来源就会漂 —— 改了可见文字忘了改 aria-label，读屏念的还是旧的
     ② 抽屉里那份是侧栏的**克隆**，克隆被挂在 `<body>` 下（drawer.js），
        它**离开了 `.shell--rail` 的作用域** ⇒ 下面这条隐藏规则不再命中
        ⇒ 抽屉里自动变成"图标 + 文字"。文字删了就永远补不回来。

   ⚠️ 不能用 `display: none`：那样文字不参与可访问名计算，链接退化成
      "链接、链接、链接"。门禁 F12 守名字、G4 守这条（改成 display:none 必须变红）。
   ⚠️ 仍建议给 `title`：44px 的图标对鼠标用户需要悬停提示。但它只是提示 ——
      名字必须来自 `.shell__link-label`（`title` 会兜出名字，把上面的问题盖住）。 */
.shell--rail {
  --shell-side-w: 4.5rem;
}

.shell--rail .shell__side {
  padding-inline: var(--sp-1);
  align-items: center;
}

.shell--rail .shell__link {
  /* 绝对定位的标签以链接为包含块，不会被算到侧栏的滚动区外 */
  position: relative;
  justify-content: center;
  padding-inline: 0;
  width: var(--sz-touch);
}

/* 视觉隐藏（保留在可访问树里）。这是 Web 上最老的一段样板之一，
   每一行都有作用：1px 盒 + 裁切；`white-space: nowrap` 防止文字在
   1px 宽里折成几十行（Safari 会因此把行高算得很夸张）。 */
.shell--rail .shell__link-label {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* 品牌：72px 里只留标记，名字视觉隐藏（与 `.shell__link-label` 同一手法）。
   ⚠️ 没有 `.shell__brand-text` 的老写法（品牌名是直接文本）会退化成
      `.shell__brand` 自己的 ellipsis ⇒ 显示成「金…」。那不是坏，
      是**提醒你补上两段结构** —— 门禁 H1 会对 rail 实例判这一条。 */
.shell--rail .shell__brand {
  justify-content: center;
  padding-inline: 0;
  gap: 0;
}

.shell--rail .shell__brand-text {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* 分组标签：72px 放不下「数据 / 校验」这种词，折成两行会把三条图标挤散。
   ⇒ 图标态**不要**用 `.shell__side-label`：让两个 `.shell__side-group` 相邻，
      `+` 那条规则会自动给出一条分隔线与间距（这才是分隔线的设计用途）。
   这条规则是**兜底**：使用者直接沿用宽屏结构时，宁可标签消失，也不要它折行；
   标签是纯视觉文本（`<p>`，不是标题、也不进任何元素的可访问名），
   所以 `display: none` 不会让任何元素失去名字。 */
.shell--rail .shell__side-label {
  display: none;
}

/* 图标态的分隔线要与图标同宽居中，否则在 72px 里看着像一条贴边的毛刺 */
.shell--rail .shell__side-group {
  align-items: center;
  width: 100%;
}

/* 侧栏无边框（与主区同色，靠留白分层） */
.shell--flat {
  background: var(--paper);        /* 外壳回到纸色 ⇒ 侧栏与主区同色 */
}
.shell--flat .shell__main {
  border-inline-start-color: transparent;   /* 无色差时也不画线：靠留白分层 */
}

/* 主区自己滚（看板型：侧栏与顶栏完全不动，只有内容区滚动）。
   ⚠️ 用这个变体时**不要**再让页面滚 —— 两个滚动容器会让"滚到底"的行为不可预期。 */
.shell--scroll-main {
  height: 100vh;
  height: 100dvh;
  min-height: 0;
  overflow: hidden;
}

.shell--scroll-main .shell__side {
  position: static;
  max-height: none;
  height: 100%;
}

.shell--scroll-main .shell__topbar {
  position: static;
}

.shell--scroll-main .shell__main {
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* ============================================================================
   6 · 减少动效
   ============================================================================
   本组件的动效只有链接的底色/文字色过渡（颜色档），按 CONVENTIONS 第四节
   第 3 档处理：可以缩短保留，不强制关。这里不写 media 块 ——
   `--dur-fast` 本身已在 tokens 里被 reduce 档缩短。
   ========================================================================== */
