/*!
 * 后台样式（外壳骨架 / 标签页 / 菜单 / 主题面板）
 *
 * 布局是「外壳 + iframe 标签页」结构：
 *   外壳  backend/shell.blade.php —— 左右分栏：[栅栏] + 侧栏（全高）
 *                                    右侧再分上（顶栏）/ 中（标签栏）/ 下（内容）
 *   内容  layouts/app.blade.php   —— 被 iframe 加载的功能页正文
 *
 * 只写 Tabler 没有的东西 —— 凡是能用 Tabler / Bootstrap class 表达的，一律不写在这里。
 * 全部走 Tabler 的变量（--tblr-*）而不是写死色值：深色模式与主色切换都靠变量重算，
 * 这里不需要、也不应该有第二套配色。见 docs/UI.md 第五节与主题章节。
 * 圆角一律走 --rf-r-* 令牌（6px / 4px 这类裸长度已清干净）：裸值看着「就差
 * 零点几像素」，实则不在五档刻度上，改档时它一定被漏掉。档位表见 theme.css 令牌区。
 *
 * 只服务单个页面 / 单个应用的样式不进这里 —— 归属判定（rageframe.css 只写全局外壳、
 * theme.css 只写主题相关、其他一律回自己的地方）见 docs/Guidelines.md §七.2。
 */

/* ============================================================
   外壳骨架
   ============================================================ */

/* 外壳整页不滚动：滚动发生在侧边栏、标签栏、iframe 内部 */
.rf-shell-body {
    height: 100vh;
    overflow: hidden;
}

.rf-shell {
    /* 三个宽度在这里收口（常规侧栏 200px；分栏 栅栏 125px + 第二栏 152px）：
         --rf-fence-w       图标分栏的最左栅栏
         --rf-sidebar-w     侧边导航 / 顶部导航的侧边栏
         --rf-subsidebar-w  图标分栏的第二栏（绑 subsidebar 配色轴的那一栏）
       手机端在下面的媒体查询里把栅栏收成纯图标栏（改这一个变量即可）。 */
    --rf-fence-w: 125px;
    --rf-sidebar-w: 200px;
    --rf-subsidebar-w: 152px;

    display: flex; /* row：[栅栏] 侧边栏 | 右侧三段 */
    height: 100vh;
}

/* ------------------------------------------------------------
   栅栏（最左侧图标导航，仅「分栏」风格显示）
   ------------------------------------------------------------ */

.rf-fence {
    flex: none;
    display: flex;
    flex-direction: column;
    width: var(--rf-fence-w);
    /* 比「深色侧栏」（gray-800）再深一档（gray-900）：分栏布局左侧是
       栅栏 + 侧栏两栏，侧栏配色轴选深色时两栏靠这一档深浅差 + 边框分开，
       不会糊成一块（深底栅栏与白底侧栏的强对比正是靠这一档拉开的） */
    background: var(--tblr-bg-surface-dark, var(--tblr-bg-surface));
    border-right: 1px solid var(--tblr-border-color);
    color: var(--tblr-body-color);
    overflow: hidden;
}

/* 栅栏只在「图标分栏」风格显示（侧边导航 / 顶部导航都是两栏）—— 显隐开关在 body 的
   data-rf-layout，用 :not 反着写：以后加布局只加自己的规则，不用回来补这里 */
.rf-shell-body:not([data-rf-layout="subfield"]) .rf-fence {
    display: none;
}

/* 栅栏顶部的 logo：只放图标，**底色与栅栏一致**（不用主色块）——
   栅栏是一整条竖直导航，顶部突然一块主色底会把它切成两段；
   logo 就是深底上的一个图形，整栏一个底色。 */
.rf-fence-brand {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: none;
    height: 56px;
    background: none;
    color: var(--tblr-primary-fg);
    font-size: 20px;
    text-decoration: none;
}

@media (hover: hover) {
    .rf-fence-brand:hover {
        color: var(--tblr-primary-fg);
        background: var(--tblr-bg-surface-tertiary);
    }
}

.rf-fence-nav {
    flex: 1;
    padding: 8px 6px;
    overflow-y: auto;
    overflow-x: hidden;
}

.rf-fence-list {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin: 0;
    padding: 0;
    list-style: none;
}

/* 分组项：**左图标 + 右文字**横排一行（图标在前、文字在后）。
   125px 宽放得下「系统 / 应用 / 商户端」这类短分组名，一屏内的分组一眼扫完，
   比图标上文字下省高度、也更好认。 */
.rf-fence-item {
    display: flex;
    align-items: center;
    gap: 6px;
    width: 100%;
    padding: 8px 10px;
    border: 0;
    border-radius: var(--rf-r-xs);
    background: none;
    /* 文字走侧栏那条链（theme.css 的 --rf-sidebar-*）：常态 muted、悬停 / 选中回近白。
       栅栏底比侧栏深一档（#101117 vs #191a23），文字与侧栏同源同色相，两栏并排才不打架。 */
    color: var(--rf-sidebar-muted);
    font-size: var(--rf-fs-body);   /* 14 —— 栅栏分组项是**导航项**（不是小标签），走正文档 */
    line-height: 1.2;
    text-align: left;
    cursor: pointer;
    overflow: hidden;
}

/* 图标占固定槽位：各项图标同一列，文字起点也就对齐了（分组名长短不一时不歪）。
   字号走导航档（16），**与侧栏菜单图标同档**：图标分栏下栅栏与侧栏同时显示，
   栅栏是「组」、侧栏是「项」—— 曾经靠栅栏 14 / 侧栏 12 的落差表示层级，
   但那点落差（2px）在 40px 行高里读不出来，反而让栅栏看着比侧栏还轻。
   层级交给位置（最左侧）+ 背景色（更深一档）表达，图标同档即可。 */
.rf-fence-item > i {
    flex: none;
    /* 宽度必须等于 1em（= font-size）：RF Icons 是 mask 雪碧图，mask-size 与
       mask-position 都按 em 换算，元素比 1em 宽，右边就会把**相邻图标的 4px**
       一起裁进这个盒子 —— 表现为每个栅栏图标右侧多一条竖线。
       曾经写死 20px（字号 16px）就是这个下场。 */
    width: 1em;
    text-align: center;
    font-size: var(--rf-icon-size-nav);
}

/* 栅栏只有深色一版（固定 dark 子树），交互态对齐深色轴侧栏菜单：10% 白纱 + 全白字。
   栅栏底比侧栏再深一档，同一层白纱压上去更清楚。选中口径见 theme.css §14。 */
@media (hover: hover) {
    .rf-fence-item:hover {
        color: #fff;
        background: rgba(255, 255, 255, .1);
    }
}

/* 当前分组：与悬停同值（选中 = 悬浮色，见 theme.css §14 的口径）—— 底与字
   都取悬停那一套，鼠标经过时不再有第二次变化 */
/* 当前分组 = 悬停同值：鼠标经过什么样、选中就什么样，不再有第二次变化 */
.rf-fence-item.active {
    color: #fff;
    background: rgba(255, 255, 255, .1);
}

.rf-fence-label {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ------------------------------------------------------------
   右侧：上（顶栏）/ 中（标签栏）/ 下（iframe）
   ------------------------------------------------------------ */

.rf-right {
    flex: 1;
    display: flex;
    flex-direction: column;
    min-width: 0;
}

/* 顶栏：在侧栏右侧，不横跨全宽 */
.rf-topbar {
    position: relative; /* 手机端盖在抽屉之上，汉堡始终可点 */
    z-index: var(--rf-z-topbar);
    flex: none;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    height: 56px;
    padding: 0 12px;
    background: var(--tblr-bg-surface);
    border-bottom: 1px solid var(--tblr-border-color);
}

.rf-topbar-left {
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
}

.rf-topbar-right {
    display: flex;
    align-items: center;
    gap: 2px;
    margin: 0;
    padding: 0;
    list-style: none;
}

/* 汉堡：手机端开抽屉、桌面端收起侧边栏 */
.rf-burger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 32px;
    height: 32px;
    padding: 0;
    border: 0;
    border-radius: var(--rf-r-xs);
    background: none;
    color: inherit;
    /* 导航档 16 —— 与顶栏右侧那排图标钮同一档（那里也是 16）。
       曾经是默认档 14：32px 的方钮里居中还行，但它左边贴着 logo、右边
       紧挨着顶栏导航，比邻居小一档就显「缩」。32 - 16 = 左右各 8px 仍有余量。 */
    font-size: var(--rf-icon-size-nav);
}

@media (hover: hover) {
    .rf-burger:hover {
        background: var(--tblr-bg-surface-secondary);
    }
}

/* ============================================================
   一级导航（仅 topnav 布局显示）
   一级菜单 = 菜单分组，横排在顶栏里；点它切换**左侧侧边栏**显示
   哪一组（与分栏布局下栅栏切组共用同一套 data-rf-group 契约）。
   ============================================================ */

.rf-topnav {
    display: none; /* 默认布局不显示，只有 body[data-rf-layout="topnav"] 展开 */
    align-items: center;
    gap: 4px;
    min-width: 0;
    margin-left: 6px;
    padding-left: 10px;
    border-left: 1px solid var(--tblr-border-color);
    /* 分组多时横向滚动，不把右侧的主题 / 用户图标挤走 */
    overflow-x: auto;
    scrollbar-width: none;
}

.rf-topnav::-webkit-scrollbar {
    display: none;
}

.rf-shell-body[data-rf-layout="topnav"] .rf-topnav {
    display: flex;
}

.rf-topnav-item {
    display: inline-flex;
    align-items: center;
    gap: 8px;   /* 图标与文字的间距：太近会挤成一坨（6px 实测过，不够） */
    flex: none;
    padding: 6px 10px;
    border: 0;
    border-radius: var(--rf-r-xs);
    background: none;
    color: var(--tblr-secondary);
    font-size: var(--rf-fs-body);   /* 14 —— 顶栏项是**导航项**，走正文档 */
    line-height: 1.4;
    white-space: nowrap;
}

.rf-topnav-item i {
    /* 导航档 16 —— 与侧栏一级菜单图标同一枚令牌（见 theme.css 的图标刻度）：
       这两个是同一级导航的两种布局，图标大小必须一致，否则切布局时整排会「跳一下」 */
    font-size: var(--rf-icon-size-nav);
}

@media (hover: hover) {
    .rf-topnav-item:hover {
        color: var(--tblr-body-color);
        background: var(--tblr-bg-surface-secondary);
    }
}

/* 当前分组**不点亮**：顶栏只表达「可以切到哪些组」，「现在在哪一组」由侧栏第一格的
   分组标题承担（见下条）。.active 仍由 rageframe.js 挂着（切组同步 / 无障碍属性），
   但不覆盖任何颜色；悬停是这里的唯一交互反馈。
   别补 border-bottom / 下划线这类「轻量选中」—— 那会和侧栏标题说两遍同一件事。
   外壳的选中口径见 theme.css §14。 */

/* topnav 布局下保留分组标题：侧栏只显示当前组，标题落在菜单第一格，充当
   「现在是哪一组」的上下文（点「系统」第一行就是「系统」，点「应用」换「应用」）。
   零 JS：标题本来就在每个 [data-rf-group-panel] 面板里（shell.blade.php 渲染
   $rfGroup['label']），切组只切面板显隐（rageframe.js 的 showGroup），标题跟着走 ——
   别改成由 JS 往固定节点写文本，那会多出一份状态。 */

/* 顶栏右侧的 nav-item 链接（主题 / 全屏 / 用户）：Tabler 的 .nav-link 有自己的
   padding 与颜色，顶栏里统一收紧成图标按钮的观感 */
.rf-topbar-right .nav-link {
    padding: 6px 8px;
    color: var(--tblr-secondary);
    border-radius: var(--rf-r-xs);
}

/* 顶栏右侧的图标钮（搜索 / 主题 / 全屏 / 通知）：与左侧一级导航**同一档 16px**。
   曾经压到小控件档 12（按「纯图标钮、不与正文并排」判的），实测在同一根顶栏里
   和左边 16px 的导航图标差两档，整条栏看着左重右轻、右侧像一排小点。
   顶栏里的图标统一走 --rf-icon-size-nav，改一处两边一起变。 */
.rf-topbar-right .nav-link .rfi {
    font-size: var(--rf-icon-size-nav);
}

/* ── 顶栏「图标 + 计数」：计数是图标右上方的一枚小角标 ────────────────────
   为什么抬到图标上沿**之上**、而不是压在角上：「99+」这种三位数胶囊最窄也有
   23px，比图标本身还宽 —— 压在角上就必然吃掉图标的顶部（12px 那版实测被压掉
   近一半，铃铛只剩个尾巴）。抬起来之后图标完整，胶囊右缘与按钮右缘对齐，
   仍是「右上角那一枚数字」的读法。

   定位按**按钮**（top: -8px / right: 0）算，不跟图标字号走 —— 所以顶栏图标
   从 12 调到 16 之后，角标与图标的重叠量不变（实测恒 1px，只是图标更高了、
   露出更多）。调按钮 padding 才需要重新对这里。

   absolute 的另一半理由：未读数在 0 与非 0 之间来回时它不占位，顶栏右侧
   其余图标不会跟着左右挪（并排写法一有数字就整排左移 20 多像素）。

   :has 只命中真带计数的按钮：主题 / 全屏 / 用户那几个还是普通图标钮。 */
.rf-topbar-right .nav-link:has(> .rf-count) {
    position: relative;
}

/* 尺寸必须挂 .rf-topbar-right 这一层：.badge 的字号 / 内边距在 theme.css 里
   重写过（12px + 4/10），而 theme.css 排在 rageframe.css 之后 —— 同权重后置
   即胜出，只写 .rf-count 会被它盖掉。配色仍由视图里的 .text-bg-* 给。 */
.rf-topbar-right .rf-count {
    position: absolute;
    top: -8px;
    right: 0;
    min-width: 15px;
    height: 15px;
    padding: 0 4px;
    border-radius: var(--rf-r-pill);
    font-size: 10px;
    line-height: 15px;
    font-weight: 500;
    /* 等宽数字：1 位与 3 位切换时胶囊宽度只随位数变，不随字形抖 */
    font-variant-numeric: tabular-nums;
    text-align: center;
}

@media (hover: hover) {
    .rf-topbar-right .nav-link:hover {
        color: var(--tblr-body-color);
        background: var(--tblr-bg-surface-secondary);
    }
}

/* ============================================================
   品牌区（侧边栏顶部）
   ============================================================ */

.rf-sidebar-brand {
    display: flex;
    align-items: center;
    flex: none;
    height: 56px;
    padding: 0 12px;
    /* 分割线颜色取侧栏自己的背景色：结构上保留 1px 占位（高度不变），
       视觉上与背景融为一条 —— 之前用 --tblr-border-color，深色侧栏下
       是一条明显的亮线，压在 logo 下面很突兀。所以这里必须与侧栏底
       **同一个令牌**（--rf-sidebar-bg）：写成 --tblr-bg-surface 的话，
       「浅色页面 + 深色侧栏」下会解成卡片色，又变成那条亮线。 */
    border-bottom: 1px solid var(--rf-sidebar-bg, var(--tblr-bg-surface));
}

/* 「分栏」风格下侧栏头部的品牌区去掉 —— 最左侧栅栏顶上已经有 logo 块了，
   同一屏出现两次品牌是重复信息。侧栏从菜单直接开始。 */
.rf-shell-body[data-rf-layout="subfield"] .rf-sidebar-brand {
    display: none;
}

/* 只剩一个分组时栅栏不渲染，logo 就无处可去了 —— 品牌区必须留着，
   否则整个外壳一个品牌标识都没有（上面那条 display:none 的前提是「栅栏 + 侧栏」两栏并存） */
.rf-shell-body[data-rf-flat="1"] .rf-sidebar-brand {
    display: flex;
}

.rf-brand {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 4px 8px;
    color: inherit;
    white-space: nowrap;
}

/* 图标块：方角带圆角，底色取主色 —— 换主题色时它跟着变 */
.rf-brand-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 28px;
    height: 28px;
    border-radius: var(--rf-r-sm);
    background: var(--tblr-primary);
    color: var(--tblr-primary-fg);
    font-size: 16px;
}

.rf-brand-mark-lg {
    width: 44px;
    height: 44px;
    border-radius: var(--rf-r-lg);
    font-size: 24px;
}

/* 品牌图（侧栏品牌区与栅栏 logo 块共用）：
   方形定宽 + 25% 圆角（比 .rf-brand-mark 的 8px 圆一点，
   与 RF 图标体系的方角带圆角一致）；object-fit 保证图片不是正方形也不变形。
   33px = rf3 `.brand-link .brand-image` 的 max-height 实测值（学 rf3 品牌区），
   品牌字 22px（同下），图比字高一档的观感不变。 */
.rf-brand-logo {
    display: block;
    flex: none;
    width: 33px;
    height: 33px;
    border-radius: 25%;
    object-fit: cover;
}

.rf-brand-text {
    /* 品牌名不加粗（原本 600），字号 22px = rf3 `.brand-link` 的
       font-size: 1.35rem（21.6px，px 口径四舍五入）—— 靠字号而不是字重
       撑出品牌感，细体在大字号下更干净，和左侧 logo 的方角圆角风格也更搭 */
    font-weight: 400;
    font-size: 22px;
    line-height: 1.2;
    letter-spacing: .02em;
}

/* ============================================================
   侧边栏
   ============================================================ */

.rf-sidebar {
    flex: none;
    display: flex;
    flex-direction: column;
    width: var(--rf-sidebar-w);
    /* 侧栏底走**自己的令牌**，不是 --tblr-bg-surface：侧栏本身是 data-bs-theme
       子树，读后者会解到「该主题的卡片色」（深色块里 = #20222e）—— 于是
       「浅色页面 + 深色侧栏」这组最常见的搭配下，侧栏显示的是深色皮的**卡片色**。
       --rf-sidebar-bg 在浅色块给 #ffffff（浅色轴侧栏）、深色块给 #191a23
       （iView 侧栏真身色），见 theme.css 的刻度区与浅/深两块。 */
    background: var(--rf-sidebar-bg, var(--tblr-bg-surface));
    border-right: 1px solid var(--tblr-border-color);
    /* 滚动交给里面的 .rf-menu：品牌区与搜索框固定，只有菜单本身滚。
        文字与描边取作用域内的变量 —— data-bs-theme="dark" 时 Tabler 会把
        作用域内整套变量换成深色版，文字 / 描边 / 菜单态自动跟随，无第二套色值 */
    overflow: hidden;
    color: var(--tblr-body-color);
}

/* 深色轴侧栏与页面的分界：**不留描边**，靠侧栏底色的硬分界 + 一层贴边柔影
   （口径取自 rf3：它的侧栏是 .elevation-2，一分描边都没有，交界全交给影子）。
   ⚠️ 只有深色轴能这么干：浅色轴侧栏是白底，与页面 #f5f7f9 只差半档，
      描边一去两者就糊在一起 —— 那条 border-right 必须留着。 */
.rf-sidebar[data-bs-theme="dark"] {
    border-right-color: transparent;
    box-shadow: 2px 0 8px rgba(0, 0, 0, .16);
}

/* 图标分栏的第二栏窄一档（152px）：一级导航已经由最左栅栏承担，
   这一栏只放「当前分组」的二级菜单，不需要常规侧栏那么宽 */
.rf-shell-body[data-rf-layout="subfield"] .rf-sidebar {
    width: var(--rf-subsidebar-w);
}

/* 只剩一个分组时没有栅栏可切（栅栏不渲染），这一栏就是唯一那栏 —— 回到常规宽度，
   否则会套用上面那条 152px，一列三层菜单在窄栏里断行。判定见 shell.blade.php 的 $rfFlat。 */
.rf-shell-body[data-rf-flat="1"] .rf-sidebar {
    width: var(--rf-sidebar-w);
}

/* 桌面收起 */
.rf-shell-body.rf-sidebar-collapsed .rf-sidebar {
    display: none;
}

/* ---- 菜单 ---- */

.rf-menu {
    flex: 1;
    padding-bottom: 32px;
    overflow-y: auto;
    overflow-x: hidden;
}

/* 菜单空态（一个分组都没有）：不是「还在加载」，就是没授权 —— 说明白好过留白 */
.rf-menu-empty {
    margin: 14px 16px;
    font-size: 12.5px;
    line-height: 1.7;
    color: var(--rf-sidebar-muted);
}

/* 菜单分组面板：侧栏同一时刻只显示一组（栅栏切换时切组；
   搜索时由 rageframe.js 放开——命中可能在任何一组里）。 */

.rf-menu-nav {
    /* 整棵菜单的「图标区」宽度修正（px 偏移量）：
       顶级图标区共 41px（槽 15px + 图标 18px + 间距 8px）。
       「图标分栏」布局侧栏不渲染图标（视图层就不输出，见 shell.blade.php
       的 $rfMenuIcons），刻度整体左移同样距离，文字不悬在半空。 */
    --rf-icon-shift: 0px;

    margin: 0;
    padding: 4px 8px;
    list-style: none;
}

/* 分组标题：节奏学 rf3（.nav-sidebar .nav-header 的 padding: 10px 25px 10px 15px）——
   上下各 10px 把标题「镶」在两段菜单之间，文字起点 23px（列表内边距 8 + 15），
   与 rf3 同列。不做大写与字距：分组标题是中文，uppercase 无作用，
   letter-spacing 只会把两三个汉字拉开显得松散（rf3 也没有这两条）。
   色走侧栏链的 muted 档（三档定义见 theme.css 的侧栏文字一节）。 */
.rf-menu-nav .nav-section-title {
    padding: 10px 15px;
    color: var(--rf-sidebar-muted);
    font-size: var(--rf-fs-xs);
    font-weight: 400;           /* 分组标题不加重（靠字号 + 色区分） */
}

.rf-menu-nav .nav-link {
    /* 一级菜单：文字起点 49px（刻度起点，见下面 .rf-submenu-link 的刻度表）。
       没配图标的一级项文字也落在同一列，整棵树的每一列都是「刻度」。
       49 而非更小的值：文字起点 49px 时图标左缘落在 23px，
       与 rf3 侧栏（.nav-link padding-left 15px + 列表内边距 8px）同列 ——
       图标离侧栏左缘更远，整栏才不贴边。 */
    --rf-indent: calc(49px + var(--rf-icon-shift, 0px));

    position: relative;
    display: flex;
    align-items: center;
    gap: 8px;
    /* 项间距 3px —— rf3 是 .2rem（3.2px）：10 项累计多出 10px 留白，
       整栏读起来更松（rf5 上一版是 2px，密一档）。 */
    margin-bottom: 3px;
    /* 行高固定 40px，用 min-height 定值 —— flex 垂直居中让
       文字/图标自动保持居中（靠上下 padding 撑高时行高不匀）。 */
    min-height: 40px;
    padding: 7px 8px 7px calc(var(--rf-indent) - 8px);
    border-radius: var(--rf-r-xs);
    /* 常态取侧栏三档链的第一档（ink），悬停 / 展开父项才提亮（见下面交互态两条）；
       二级 / 分组标题 / 箭头走同一链的 --rf-sidebar-muted。三档定义见 theme.css。 */
    color: var(--rf-sidebar-ink, inherit);
    font-size: var(--rf-fs-body);
    line-height: 1.4;
}

/* 开合开关是「无 href 的 <a>」（href="javascript:void(0);" 上 CSP 会被拦掉），
   而没有 href 的链接拿不到浏览器的 pointer 光标 —— 这里补上。
   一级 .nav-link 与二级起 .rf-submenu-link 共用这个类，一条规则覆盖两层。 */
.rf-menu-nav .rf-menu-toggle {
    cursor: pointer;
    user-select: none;
}

@media (hover: hover) {
    .rf-menu-nav .nav-link:hover {
        color: var(--tblr-body-color);
        background: var(--tblr-bg-surface-secondary);
    }
}

/* 展开的父项：与悬停同一个样子 —— rf3 的口径（AdminLTE 把 .menu-open>.nav-link
   和 :hover>.nav-link 写在同一条规则里：一层白纱 + 全白字）。层级靠收 / 展表达，
   不再只给标题单独提亮（那版展开后图标、箭头还是灰的，一块行三种灰，不统一）。 */
.rf-menu-nav .rf-has-children > .nav-link[aria-expanded="true"] {
    color: var(--tblr-body-color);
    background: var(--tblr-bg-surface-secondary);
}

/* 深色轴侧栏：悬停 / 展开父项 / **选中** = rf3 实测口径
   （[class*="sidebar-dark-"] … .menu-open>.nav-link, :hover>.nav-link
    { background-color: rgba(255,255,255,.1); color:#fff }）——
   常态 ink（带底色蓝调的灰，见 theme.css --rf-sidebar-ink）、这三种态全白，
   一收一放；悬停别用 surface-secondary 在深色子树里
   解出的那个灰蓝，白纱是「往亮里走」，压在 #191a23 上更干净。 */
.rf-sidebar[data-bs-theme="dark"] .rf-menu-nav .nav-link:hover,
.rf-sidebar[data-bs-theme="dark"] .rf-menu-nav .nav-link.active,
.rf-sidebar[data-bs-theme="dark"] .rf-menu-nav .nav-item.active > .nav-link,
.rf-sidebar[data-bs-theme="dark"] .rf-menu-nav .rf-has-children > .nav-link[aria-expanded="true"],
.rf-sidebar[data-bs-theme="dark"] .rf-submenu-link:hover,
.rf-sidebar[data-bs-theme="dark"] .rf-submenu-link.active {
    background: rgba(255, 255, 255, .1);
    color: #fff;
}

/* 交互态里箭头跟文字一起变白（默认是次要灰，混在白纱行里是一个杂色） */
.rf-menu-nav .rf-has-children > .nav-link[aria-expanded="true"] .rf-menu-arrow,
.rf-sidebar[data-bs-theme="dark"] .rf-menu-nav .nav-link:hover .rf-menu-arrow,
.rf-sidebar[data-bs-theme="dark"] .rf-submenu-link:hover .rf-menu-arrow {
    color: inherit;
}

/* 选中 = 与悬停**同一个颜色**（用户口径：选中不用亮蓝实底，鼠标经过什么样、
   选中就什么样，且不再有第二次变化）。所以这一条刻意与上面的悬停 / 展开
   同值，放在它们之后只是为保证优先级 —— 值一样，先到后到都看不出来。 */
.rf-menu-nav .nav-link.active,
.rf-menu-nav .nav-item.active > .nav-link {
    color: var(--tblr-body-color);
    background: var(--tblr-bg-surface-secondary);
}

/* 一级图标：绝对定位在文字左侧的固定槽位。
   ⚠️ 定位基准是 <a> 自己（position:relative），不是侧栏左缘 —— 文字在
   padding-left = 刻度 - 8px（列表内边距）处，图标槽要在它左边留出
   间距 8px + 槽宽 18px，所以 left = 刻度 - 8px - 8px - 18px。
   （少算列表内边距 —— 如写成 -26px —— 图标右缘就贴住文字，gap=0，
   这正是「图标和标题挤在一起」的根因。）
   ⚠️ 槽位必须 flex 居中，不能靠 text-align：槽是行内盒（继承 1.4 行高，
   实际高 20px），图标只有 12px 且行内对齐是**基线对齐** —— 图标坐在基线上、
   下方那段 descent 空着，视觉就偏上 2px（实测「图标偏上」的根因）。
   flex 让槽高收到图标本身高度，translateY(-50%) 再把它钉在行中心。
   ⚠️ 行中心之上还要再下移 1px：文字（14px）与图标（12px）的行盒中心虽然
   都在行中心，但两者字形的**墨迹中心**不一致 —— 实测截图逐行量墨迹，
   图标墨迹中心比文字高 0.5~1px（图标字形在 em 框内偏上）。这 1px 是
   字形层面的视觉补偿，与皮肤无关，写死即可。 */
.rf-menu-nav .nav-link-icon {
    position: absolute;
    top: 50%;
    left: calc(var(--rf-indent) - 34px);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    transform: translateY(calc(-50% + 1px));
    font-size: var(--rf-icon-size-nav);   /* 导航档 16 —— 比正文大一档：14 与菜单文字等大时偏轻，见 theme.css 的令牌说明 */
}

.rf-menu-nav .nav-link-title {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* 二级菜单：**就地展开**的子菜单（手风琴），不是 Tabler 的浮动下拉。
   展开靠 .rf-menu-open，开合逻辑在 rageframe.js（同级互斥）。 */
.rf-submenu {
    display: none;
}

.rf-submenu.rf-menu-open {
    display: block;
    /* 不铺底色：rf3 的 .nav-treeview 没有任何背景，子项直接坐在侧栏底上，
       层级全靠缩进（每级 +8px）与文字灰度表达 —— 之前铺的「下沉一档」底块
       （#101117）在侧栏里像一块浮起来的深色面板，rf3 对比之下它才是突兀的那个。 */
}

/* 子菜单容器与父级列表左对齐（缩进只由里面的链接自己给），
   这样每一级的文字起点都能按 depth 精确算出来 */
.rf-submenu-list {
    margin: 0;
    padding: 2px 0 4px;
    list-style: none;
}

/* ------------------------------------------------------------------
   层级缩进：靠「递进距离」分层，不靠导轨线（用户明确不要那条竖线）

   每一级的链接把**文字起点**声明成 --rf-indent，文字只由它决定。

   刻度（从侧栏左边缘算，列表有 8px 内边距，所以 padding-left = 刻度 -8px）：
     一级 49px（图标区：槽 15px + 图标 18px + 间距 8px）
     二级 57px   ┐ 每深一级 +8px —— **相对一级的文字位**只缩进一点点
     三级 65px   │
     四级 73px   │
     五级 81px   ┘
     更深 不渲染：菜单最多显示五级（Menu::MAX_LEVELS，截断在视图层），
          刻度规则也只写到 depth 4 —— 见下面「逐级缩进」一条的说明

   步长始终保持 8px：三级之后继续沿用同一刻度（四级 73px / 五级 81px），
   五级下文字起点距侧栏左边缘 81px，默认侧栏（200px）仍有
   充裕的可读宽度；最窄的「图标分栏」第二栏（152px）会开始出现省略号，
   这是放开到五级的既定代价。

   为什么步长只有 8px：一级的文字起点本来就含 41px 图标区，二级起没有
   图标，拿二级文字去比一级文字**只该差一点点**（+8px）；再大，子项看着
   就像被推到很远 —— 层级感靠颜色（子项 secondary）和内边距
   （6px < 7px）已经够了。

   二级起不渲染图标；顶级图标一律渲染，**「图标分栏」下由下面的规则隐藏**：
   布局是运行时轴（纯前端改 body 的 data-rf-layout），图标必须留在 DOM 里
   靠 CSS 收掉，否则切布局时图标不会跟着消失。
   隐藏后 --rf-icon-shift 把整棵刻度统一左移 26px，补掉空出来的图标区
   （所有层级同一个值 —— 图标区宽度本来就不随层级变）。

   depth 由视图渲染在 data-rf-depth 上（顶级是 .nav-link，不带这个属性；
   它的子级是 depth=1，所以「三级菜单」对应 data-rf-depth="2"）。
   ------------------------------------------------------------------ */
.rf-shell-body[data-rf-layout="subfield"] .rf-menu-nav {
    --rf-icon-shift: -26px;
}

/* 分栏布局的侧栏（第二栏，152px）不带图标：分组图标已经在栅栏上，侧栏再带
   一遍是重复信息，且图标会把窄栏的文字挤到右边。
   必须写成「隐藏」而不是「不渲染」—— 见上面的说明。
   子级分组图标同理（它按 --rf-indent 定位，这一布局下 indent 整体左移 26px，
   不隐藏就会顶到侧栏边缘外）。 */
.rf-shell-body[data-rf-layout="subfield"] .rf-menu-nav .nav-link-icon,
.rf-shell-body[data-rf-layout="subfield"] .rf-menu-nav .rf-submenu-icon {
    display: none;
}

.rf-submenu-link {
    --rf-indent: calc(57px + var(--rf-icon-shift, 0px));

    position: relative;
    display: flex;
    align-items: center;
    gap: 6px;
    margin-bottom: 2px;
    /* 行高与一级统一为 40px（见上面 .rf-menu-nav .nav-link 的说明） */
    min-height: 40px;
    padding: 6px 8px 6px calc(var(--rf-indent) - 8px);
    border-radius: var(--rf-r-xs);
    /* 二级文字走侧栏链的 muted 档（同链不同档，与一级同色相） */
    color: var(--rf-sidebar-muted);
    /* 字号与一级一致（14px）：层级只用缩进区分。小一号（13px）那版
       子项读起来像注释，且和一级并排时明显「掉了半档」。 */
    font-size: var(--rf-fs-body);
    line-height: 1.4;
    text-decoration: none;
}

/* 逐级缩进：depth 1/2 是原有的二级 / 三级刻度，depth 3/4 是放开到五级后
   新增的四级 / 五级，步长延续上面的 8px。
   规则只写到 depth 4 为止：视图受 Menu::MAX_LEVELS 封顶（= 5），正常到不了
   更深。没写规则的深度会掉回基础刻度 57px（.rf-submenu-link 的默认值），
   看起来跟二级平级 —— 哪天放宽层级，记得来这里补刻度（Menu::MAX_LEVELS
   的注释里有同样的提醒）。 */
.rf-submenu-link[data-rf-depth="2"] {
    --rf-indent: calc(65px + var(--rf-icon-shift, 0px));
}

.rf-submenu-link[data-rf-depth="3"] {
    --rf-indent: calc(73px + var(--rf-icon-shift, 0px));
}

.rf-submenu-link[data-rf-depth="4"] {
    --rf-indent: calc(81px + var(--rf-icon-shift, 0px));
}

@media (hover: hover) {
    .rf-submenu-link:hover {
        color: var(--tblr-body-color);
        background: var(--tblr-bg-surface-secondary);
        text-decoration: none;
    }
}

/* 选中的子项：与悬停同值（见菜单一节的口径），深色轴由上面的白纱规则接管 */
.rf-submenu-link.active {
    color: var(--tblr-body-color);
    background: var(--tblr-bg-surface-secondary);
}

/* 子级图标只剩一种来源：**直接挂在应用下、且声明了 `rf-menu-leaf-icon` 的入口**
   （视图只给这些叶子输出 .rf-submenu-icon）。分组与更深的层级一律纯文字 ——
   缩进刻度已经分层，每片叶子再带图标只会把窄侧栏挤乱。

   定位公式与一级 .nav-link-icon **完全相同**（indent - 34px），于是二级图标
   相对一级正好右移一个缩进步长（8px）：图标和文字共用同一套刻度，不是两套
   各自拍的数字。

   为什么必须 absolute：走 flex 流会把文字推右 24px（18 图标 + 6 间距），
   文字就落在 57+24=81px，比它自己的子项（depth 2 = 65px）还深一级 ——
   层级当场反过来。absolute 之后 .rf-submenu-title 的省略号照旧（它靠
   min-width:0 + overflow:hidden 收，不依赖 flex 占比）。 */
.rf-submenu-link .rf-submenu-icon {
    position: absolute;
    top: 50%;
    left: calc(var(--rf-indent) - 34px);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    transform: translateY(calc(-50% + 1px));
    font-size: var(--rf-icon-size-nav);   /* 与一级同一档，见上面 .nav-link-icon */
}

.rf-submenu-title {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* 展开箭头：默认指向右，展开时转 90°（状态读 aria-expanded，与无障碍属性同源）。
   色走侧栏链的 muted 档；展开 / 悬停时由上面的交互态规则改为 inherit，跟文字一起变亮。 */
.rf-menu-arrow {
    flex: none;
    margin-left: auto;
    color: var(--rf-sidebar-muted);
    /* 默认档 14：这个箭头是 **chevron**（只有折线、没有杆），视觉比同字号的
       arrow 细一半 —— 按小控件档 12 渲染时实测只有 3.5px 宽，像一撇灰点。
       箭头本身是辅助指示，不再往上加（16 会跟菜单图标抢注意力）。 */
    font-size: var(--rf-icon-size);
    transition: transform var(--rf-dur) var(--rf-ease-curve);
}

.rf-menu-toggle[aria-expanded="true"] .rf-menu-arrow {
    transform: rotate(90deg);
}

/* （展开父项的配色规则在上面菜单一节：与悬停同一层底、整行一个颜色 ——
   旧版「只提亮标题」在这里，已并回去。） */

/* 搜索过滤结果：命中的显示、其余收起。用 !important 压过 Tabler 的
   .nav-item / .nav-link 显示规则（它们带自己的 display，优先级不好赌）。 */
.rf-menu-hidden {
    display: none !important;
}

/* 手机端：栅栏收成纯图标栏，侧边栏变抽屉（≤lg；桌面是常驻分栏） */
@media (max-width: 991.98px) {
    /* 只改这一个变量：栅栏宽度与抽屉的让位偏移都跟着走 */
    .rf-shell {
        --rf-fence-w: 52px;
    }

    /* 文字藏起来后图标居中（横排时的左对齐槽位在纯图标栏里会偏左） */
    .rf-fence-item {
        justify-content: center;
        padding: 8px 4px;
    }

    .rf-fence-label {
        display: none;
    }

    .rf-sidebar {
        position: fixed;
        top: 56px;
        bottom: 0;
        left: 0;
        z-index: var(--rf-z-drawer);
        transform: translateX(-100%);
        transition: transform var(--rf-dur-slow) var(--rf-ease-curve);
        box-shadow: 0 0 32px rgba(0, 0, 0, .2);
    }

    /* 分栏模式下抽屉让开栅栏，从栅栏右侧滑出。隐藏位要把栅栏宽度一起算进去：
        只 translateX(-100%) 的话会留下一条露在栅栏旁边
        （left 栅栏宽 - 自身宽 ≠ 完全出屏）。 */
    .rf-shell-body[data-rf-layout="subfield"] .rf-sidebar {
        left: var(--rf-fence-w);
        transform: translateX(calc(-100% - var(--rf-fence-w)));
    }

    .rf-shell-body.rf-sidebar-open .rf-sidebar {
        transform: none;
    }

    .rf-sidebar-mask {
        display: none;
        position: fixed;
        inset: 0;
        z-index: var(--rf-z-drawer-mask); /* 低于顶栏（--rf-z-topbar）：开着抽屉时汉堡仍可点 */
        background: rgba(0, 0, 0, .35);
    }

    .rf-shell-body.rf-sidebar-open .rf-sidebar-mask {
        display: block;
    }
}

@media (min-width: 992px) {
    .rf-sidebar-mask {
        display: none;
    }
}

/* ============================================================
   标签栏
   ============================================================ */

.rf-tabs {
    flex: none;
    display: flex;
    align-items: center;
    gap: 4px;
    height: 40px;
    padding: 0 8px;
    background: var(--tblr-bg-surface);
    border-bottom: 1px solid var(--tblr-border-color);
}

/* 标签页轴 = off：整条标签栏收起（右侧从「上中下」变成「上下」）。
   值的真源是 body 上的 data-rf-tabs，服务端渲染初始态、前端切换时改它。 */
.rf-shell-body[data-rf-tabs="off"] .rf-tabs {
    display: none;
}

/* 左右滚动按钮 */
.rf-tabs-nav-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 26px;
    height: 26px;
    padding: 0;
    border: 0;
    border-radius: var(--rf-r-xs);
    background: none;
    color: var(--tblr-secondary);
    font-size: var(--rf-fs-xs);
}

@media (hover: hover) {
    .rf-tabs-nav-btn:hover {
        color: var(--tblr-body-color);
        background: var(--tblr-bg-surface-secondary);
    }
}

/* 页签滚动容器：隐藏滚动条（左右按钮 + 滚轮 / 触摸滑动都能滚） */
.rf-page-tabs {
    flex: 1;
    min-width: 0;
    overflow-x: auto;
    scrollbar-width: none;
}

.rf-page-tabs::-webkit-scrollbar {
    display: none;
}

.rf-page-tabs-content {
    display: flex;
    gap: 4px;
    width: max-content;
    /* 不设上下 padding：标签 30px 由 .rf-tabs 的 align-items:center 在 40px
       条里垂直居中。别凑 5+30+5=40 —— 标签栏是 border-box，扣掉 1px 下边框
       只剩 39px，凑满 40 会溢出 1px；.rf-page-tabs 因 overflow-x:auto 纵向
       也变可裁，在 125%/150% 缩放的取整下就会把字的上/下缘切掉一截。 */
    padding: 0;
}

/* 单个页签
   定高 30px + align-items:center：文字相对标签**严格垂直居中** ——
   靠上下 padding 撑高时，行高与字体度量不同就上下不匀。

   水平居中靠**左右对称的内边距**，不是靠 justify-content —— 关闭按钮是
   绝对定位的（见 .rf-tab-close），不占 flex 流，所以文字的中心就是标签的
   中心。右侧那条 22px 的空白带是给 × 预留的：它只在 hover 时显形，
   显形前后文字一动不动，也不会有「一排标签被推着重排」的抖动。
   首页标签没有 ×（不可关），内边距与其它标签一致，位置不歪。 */
.rf-tab {
    position: relative;
    display: inline-flex;
    align-items: center;
    height: 30px;
    /* 透明 border 常驻占位：选中态只换 border-color，不发生 2px 的布局跳动 */
    border: 1px solid transparent;
    padding: 0 22px;
    border-radius: var(--rf-r-xs);
    color: var(--tblr-secondary);
    font-size: var(--rf-fs-body);
    white-space: nowrap;
    text-decoration: none;
    cursor: pointer;
    user-select: none;
}

@media (hover: hover) {
    .rf-tab:hover {
        color: var(--tblr-body-color);
        background: var(--tblr-bg-surface-secondary);
        text-decoration: none;
    }
}

/* 选中页签：主色描边（浅灰底 + 主色文字），与菜单选中色呼应。
   字重保持 400（控件/导航不靠粗细，激活靠色 + 描边）。
   描边用真 border，不用 box-shadow inset —— 全站约定不靠内阴影画边。 */
.rf-tab.active {
    color: var(--tblr-primary);
    background: var(--tblr-bg-surface-secondary);
    border-color: var(--tblr-primary);
    font-weight: 400;
}

.rf-tab-title {
    min-width: 0;
    max-width: 160px;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* 关闭按钮：默认隐形，鼠标移到标签上才出现
   绝对定位在标签**右内边距那条带子里**（right 6px + 自身 16px 正好顶到
   文字右边界），所以它显形不会挤动文字、也不会盖住标题。
   垂直用 top:50% + translateY(-50%) 定位到标签中线（标签是定位祖先），
   图标再靠 inline-flex 居中 —— 与文字中心严格同一条线。

   隐藏用 opacity + pointer-events 而不是 visibility：opacity:0 的元素仍能
   被 Tab 聚焦，键盘用户 :focus-visible 时同样能看到 ×；pointer-events:none
   则保证隐形状态下点它不误关标签（点击会落到 <a> 上，即切换标签）。
   不用 visibility:hidden —— 那样连键盘都聚焦不到，× 就成了纯鼠标功能。

   × 本身**不给 hover 底色**：标签里突然多出一小块背景很抢眼（看着像多了个
   控件在动），× 只用颜色变化表达状态。 */
.rf-tab-close {
    position: absolute;
    top: 50%;
    right: 6px;
    transform: translateY(-50%);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 16px;
    height: 16px;
    padding: 0;
    border: 0;
    border-radius: var(--rf-r-xs);
    background: none;
    color: inherit;
    font-size: var(--rf-icon-size-xs);   /* 极小档 10：16px 方钮里的 × ，12 会顶满格 */
    line-height: 1;
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--rf-dur-fast) var(--rf-ease-curve);
    cursor: pointer;
}

/* 图标是字体字形，行盒与字形之间可能有基线缝 —— 块级化让它成为 flex item，
   align-items:center 直接对它生效，不再受行内基线影响 */
.rf-tab-close > i {
    display: block;
    line-height: 1;
}

/* 触摸热区：× 本体只有 16px，手指点偏一点就落空了。
   伪元素把可点区域撑到「整条标签高 × 略宽」——视觉一动不动（::before 是
   透明的，不参与布局），只是命中区变大。
   只向右、上下扩：向左会盖住标题末字（那个位置本来是切换标签，不能变成关闭）。
   pointer-events 跟着按钮走（按钮隐形时是 none，热区自然也点不到）。 */
.rf-tab-close::before {
    content: '';
    position: absolute;
    top: -7px;
    right: -4px;
    bottom: -7px;
    left: 0;
}

/* 悬停标签（或键盘聚焦到 ×）才显形 */
.rf-tab:hover .rf-tab-close,
.rf-tab-close:focus-visible {
    opacity: 1;
    pointer-events: auto;
}

/* 悬停 × 本身：只加深颜色，不加底色 */
@media (hover: hover) {
    .rf-tab-close:hover {
        color: var(--tblr-body-color);
    }
}

/* 「关闭操作」下拉 */
.rf-tabs-more-btn {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    flex: none;
    padding: 4px 8px;
    border: 0;
    border-radius: var(--rf-r-xs);
    background: none;
    color: var(--tblr-secondary);
    font-size: var(--rf-fs-sm);
}

@media (hover: hover) {
    .rf-tabs-more-btn:hover {
        color: var(--tblr-body-color);
        background: var(--tblr-bg-surface-secondary);
    }
}

/* 「关闭操作」下拉顶部的标签清单：列出全部已开标签，点击切换到那一页。
   颜色走框架档位：普通项正文色、当前项主色 —— 与侧栏当前菜单项同一档强调。 */
.rf-tabs-list {
    max-height: 240px;
    overflow-y: auto;
}

.rf-tabs-list-item {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 6px 12px;
    border: 0;
    border-radius: 0;
    background: none;
    color: var(--tblr-body-color);
    font-size: var(--rf-fs-sm);
    text-align: left;
    cursor: pointer;
}

.rf-tabs-list-item:hover,
.rf-tabs-list-item:focus-visible {
    color: var(--tblr-body-color);
    background: var(--tblr-bg-surface-secondary);
}

.rf-tabs-list-item:focus-visible {
    outline: var(--rf-focus-ring-width) solid var(--rf-focus-ring-color);
    outline-offset: var(--rf-focus-ring-offset-inner);
}

/* 当前标签：主色 + 右侧对勾。不加深底色 —— 下拉里高亮靠色相就够，
   加底色会和 hover 态打架。 */
.rf-tabs-list-item.active,
.rf-tabs-list-item.active:hover {
    color: var(--tblr-primary);
    font-weight: 500;
}

.rf-tabs-list-check {
    flex: none;
    font-size: var(--rf-fs-sm);
}

/* 标签名超长省略：flex 子项必须 min-width:0 否则 text-overflow 不生效 */
.rf-tabs-list-label {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* ============================================================
   iframe 容器
   ============================================================ */

.rf-iframes {
    position: relative;
    flex: 1;
    min-height: 0;
    background: var(--tblr-bg-surface-secondary);
}

.rf-iframe {
    position: absolute;
    inset: 0;
    display: none;
    width: 100%;
    height: 100%;
    border: 0;
}

.rf-iframe.active {
    display: block;
}

/* 正在加载的那个 iframe 的遮罩：iframe load 后由 rageframe.js 收起。
   DOM 上它**紧跟在它所遮的那个 iframe 之后**（rageframe.js 的 showLoading 负责挪位）：
   容器里只有一个 iframe 时它跟在 boot iframe 后，开出第二个就跟着挪过去。 */
.rf-iframe-loading {
    position: absolute;
    inset: 0;
    z-index: var(--rf-z-high);
    /* 显隐一律由 showLoading / hideLoading 控制（外壳直出的那一块则由 HTML 上的
       内联 style 先铺开，rageframe.js 初始化时收编成 loadingEl 接管）。 */
    display: none;
    /* 骨架撑满整个内容区（形状照着内容页画，见 shell/_skeleton.blade.php）：
       内容到位前它就是内容区的全部内容，所以占满而不是缩在中间。 */
    align-items: stretch;
    justify-content: flex-start;
    overflow: hidden;
    background: var(--tblr-bg-surface-secondary);
}

/* 模板缺失时的兜底（JS 回退到 spinner）：这时仍要居中 */
.rf-iframe-loading > .spinner-border {
    margin: auto;
}

/* ── 内容区骨架屏 ─────────────────────────────────────────────────
   结构见 shell/_skeleton.blade.php，这里只管视觉。

   内边距照抄内容页的 container-fluid + py-4（12 / 24px）：骨架外轮廓与
   真内容对得上，内容到位时不会「骨架小一圈 → 内容撑开」跳一下。

   色块不用固定灰值：base 取 --rf-ink 的 8% 混进 --rf-surface，明暗两皮
   自动跟随；扫光高光只再掺 4%，够亮但不刺眼。 */
.rf-skel {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 20px;
    padding: 24px 12px 32px;
}

.rf-skel-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    min-height: 36px;
}

.rf-skel-actions {
    display: flex;
    gap: 8px;
    flex: 0 0 auto;
}

.rf-skel-card {
    border: 1px solid var(--rf-line);
    border-radius: var(--rf-r-lg);
    background: var(--rf-surface);
    overflow: hidden;
}

.rf-skel-cardhead {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 12px 16px;
    border-bottom: 1px solid var(--rf-line);
}

.rf-skel-rows {
    padding: 4px 16px;
}

.rf-skel-row {
    display: flex;
    align-items: center;
    gap: 12px;
    height: 44px;
    border-bottom: 1px solid var(--rf-line);
}

.rf-skel-row:last-child {
    border-bottom: 0;
}

/* 六行的文字条宽度参差 —— 等宽会显出「这是模板」，参差才像真表格 */
.rf-skel-row:nth-child(1) .rf-skel-bar { width: 46%; }
.rf-skel-row:nth-child(2) .rf-skel-bar { width: 34%; }
.rf-skel-row:nth-child(3) .rf-skel-bar { width: 52%; }
.rf-skel-row:nth-child(4) .rf-skel-bar { width: 28%; }
.rf-skel-row:nth-child(5) .rf-skel-bar { width: 41%; }
.rf-skel-row:nth-child(6) .rf-skel-bar { width: 37%; }

/* 骨架色块：底色 + 扫光。两块共用一套值，改动只在这里。 */
.rf-skel-bar,
.rf-skel-dot,
.rf-skel-block,
.rf-panel-skel-input {
    --rf-skel-base: color-mix(in oklab, var(--rf-ink) 8%, var(--rf-surface));
    --rf-skel-shine: color-mix(in oklab, var(--rf-ink) 4%, var(--rf-surface));
    display: block;
    background-color: var(--rf-skel-base);
    background-image: linear-gradient(
        90deg,
        var(--rf-skel-base) 25%,
        var(--rf-skel-shine) 37%,
        var(--rf-skel-base) 63%
    );
    background-size: 400% 100%;
    animation: rf-skel-shine 1.4s ease infinite;
}

.rf-skel-bar { height: 10px; border-radius: 3px; flex: 0 1 auto; }
.rf-skel-dot { width: 24px; height: 24px; border-radius: 50%; flex: 0 0 auto; }

.rf-skel-block { width: 72px; height: 28px; border-radius: var(--rf-r-sm); flex: 0 0 auto; }
.rf-skel-block-wide { width: 96px; }
.rf-skel-block-sm { width: 44px; height: 18px; margin-left: auto; }

/* 页头标题 / 卡头标题的专用尺寸（比行内文字条厚一档） */
.rf-skel-title { width: 148px; height: 18px; }
.rf-skel-caption { width: 96px; height: 14px; }

@keyframes rf-skel-shine {
    from { background-position: 200% 0; }
    to { background-position: -200% 0; }
}

/* 关掉动画偏好：停在纯色块（仍在「加载中」的语义，只是不闪） */
@media (prefers-reduced-motion: reduce) {
    .rf-skel-bar,
    .rf-skel-dot,
    .rf-skel-block,
    .rf-panel-skel-input {
        animation: none;
    }
}

/* ── 弹层加载占位（panel.js 的 panelSkeleton）─────────────────────────
   形状：「字段名短条在上 + 输入框整行在下」×3 + 末行加高一块。色块复用
   上面那三个类，这里只给布局与尺寸。弹层里的表单多数是竖排（label 上、
   控件下、控件占满整行），骨架与它同构 —— 内容到位时只有「灰条变真字」，
   位置与宽度都不挪；画成行式的话输入框会从半条突然变成整行。

   输入框高度取 32（表单控件那一档）、字段行间距 20 —— 与真实表单同一节拍。 */
.rf-panel-skel {
    display: flex;
    flex-direction: column;
    gap: 20px;
}

/* 一行 = 字段名条 + 它下面的输入框条 */
.rf-panel-skel-row {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
}

/* 字段名条：四条长度参差 —— 等宽会显出「这是模板」 */
.rf-panel-skel-label { flex: 0 0 auto; }
.rf-panel-skel-row:nth-child(1) .rf-panel-skel-label { width: 52px; }
.rf-panel-skel-row:nth-child(2) .rf-panel-skel-label { width: 66px; }
.rf-panel-skel-row:nth-child(3) .rf-panel-skel-label { width: 44px; }
.rf-panel-skel-row:nth-child(4) .rf-panel-skel-label { width: 58px; }

/* 输入框条：与真实控件一样占满整行 */
.rf-panel-skel-input {
    flex: 1 1 auto;
    align-self: stretch;
    height: 32px;
    border-radius: var(--rf-r-sm);
}

/* 末行是多行文本（备注 / 简介这类），高一块 */
.rf-panel-skel-area { height: 64px; }

/* 提示文字（「加载中…」/「正在保存…」）：推到底部 —— 骨架是主体，
   文字只作状态说明，两者挤在一处会互相抢注意力 */
.rf-panel-skel-tip {
    margin-top: auto;
    padding-top: 12px;
    text-align: center;
}

/* ============================================================
   内容页（iframe 里，layouts/app.blade.php）
   ============================================================ */

/* 「返回」悬浮键 —— 水平居中钉在内容视口底部（DOM 由**下钻页自己**写：
   应用详情 / 应用日志页各一条，href 写死指向应用列表）。
   为什么挪出页头工具条：内容页被 iframe 裁成一屏，页头一滚就出了视口；长页面
   想返回得先滚回顶部，很难找。钉在底部后，滚到哪儿它都在。

   为什么居中：它是**整页级**动作，不属于正文左右任何一侧的内容流（靠左容易被
   读成「上半区/某一段的返回」）；居中不偏不倚，视线扫过正文底部就撞见它。

   不做任何显隐判断：按 history.length 猜会出错 —— 应用首页页内过滤一次
   历史就 +1，首页就会冒出一个挂着的返回键。返回是下钻页自己的动作，
   页面直接写链接。

   fixed 相对的是 **iframe 视口**（内容页跑在 iframe 里），也就是内容区可视范围 ——
   正是我们要的「滚到哪都在」。
   --rf-z-drawer（1030）：压过正文，但低于 Tabler 的弹层（backdrop 1050 / modal 1055）——
   确认弹窗打开时它该被遮罩盖住，而不是浮在弹窗上面。 */
.rf-back-float {
    position: fixed;
    left: 50%;
    bottom: 24px;
    transform: translateX(-50%);
    z-index: var(--rf-z-drawer);

    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    /* 左右等宽：图标 + 文字作为整体落在按钮正中。
       （再加「左窄右宽」的补偿内边距反而会让内容看着偏半格。） */
    padding: 7px 16px;

    border: 0;
    border-radius: var(--rf-r-pill);
    background: var(--tblr-primary);
    box-shadow: var(--rf-shadow-pop);
    color: var(--tblr-primary-fg);
    font-size: var(--rf-fs-sm);
    line-height: 1.25;
    text-decoration: none;
    white-space: nowrap;
    transition: filter var(--rf-dur) var(--rf-ease-curve),
                box-shadow var(--rf-dur) var(--rf-ease-curve);
}

/* hover 用 filter 压暗，而不是写一个更深的色值：底色是 --tblr-primary，
   会随「皮肤色」轴变化，写死色值在换色后就错位了。 */
@media (hover: hover) {
    .rf-back-float:hover {
        color: var(--tblr-primary-fg);
        filter: brightness(.94);
    }
}

/* 蓝底按钮上的蓝色 outline 会糊在一起，所以往外挪一格 —— outline-offset
   让描边落在页面底色上，键盘用户才看得见焦点在哪。 */
.rf-back-float:focus-visible {
    outline: var(--rf-focus-ring-width) solid var(--rf-focus-ring-color);
    outline-offset: var(--rf-focus-ring-offset-wide);
}

/* 小屏贴边一点，少挡正文（水平居中照旧，只收下面的间距） */
@media (max-width: 575.98px) {
    .rf-back-float {
        bottom: 16px;
    }
}

/* 悬浮键会盖住正文最后一行：有它的页面，正文底部让出一块。
   用 :has() 而不是无条件给 main 加内边距 —— 没挂返回键的页面
   不该凭空多出一截空白。:has 在 Chrome 105+ / Safari 15.4+ / Firefox 121+ 支持；
   万一不支持，只是少了这段留白，按钮本身照常工作。 */
body:has(.rf-back-float) main {
    padding-bottom: 48px;
}

/* 面包屑选中项文字色由 theme.css 统一给（--rf-ink，同权重后置即胜出），
   这里不再写一份 —— 写了也永远轮不到生效，还容易和皮肤色值漂移。 */

/* 首页欢迎区 */
.rf-home-hero {
    display: flex;
    align-items: center;
    gap: 16px;
}

.rf-home-title {
    margin: 0;
    /* 首页 hero 问候语：**刻意不走 h1 档**（每个内容页页头已有一行 24px 的
       页面标题，首页再顶一个 24px 会打架），比 h2 大一档保持存在感。 */
    font-size: 20px;
}

/* ============================================================
   用户菜单
   ============================================================ */

/* 头像：用户表暂时没有头像列，用名字首字兜底（见 layouts/_user-menu.blade.php） */
.rf-avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 28px;
    height: 28px;
    border-radius: var(--rf-r-sm);
    background: var(--tblr-primary);
    color: var(--tblr-primary-fg);
    font-size: var(--rf-fs-xs);
    font-weight: 400;           /* 头像首字不加重 */
}

.rf-avatar-lg {
    width: 36px;
    height: 36px;
    border-radius: var(--rf-r);
    font-size: var(--rf-fs-body);
}

.rf-user-menu,
.rf-user-card {
    min-width: 208px;
}

.rf-user-card {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 12px 4px;
}

/* ============================================================
   主题面板
   ============================================================ */

.rf-theme-panel {
    width: 240px;
}

.rf-theme-group {
    margin-bottom: 14px;
}

.rf-theme-label {
    margin-bottom: 6px;
    color: var(--tblr-secondary);
    font-size: var(--rf-fs-sm);   /* 13 / 500 —— 主题菜单的分组标签走次要档 */
    font-weight: 500;
}

/* 标题在左、控件在右的一行（Switch 用） */
.rf-theme-toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}

/* 开关（Switch）：标签页轴用。自己画而不是套 .form-switch —— 面板只有 240px 宽，
   Bootstrap 那套 form-check 是 padding-left + float 定位，窄容器里对不齐；
   轨道配色走主题变量，换主色 / 明暗时自动跟随。 */
.rf-switch {
    position: relative;
    display: inline-flex;
    flex: none;
    cursor: pointer;
}

/* 真正接事件的 checkbox 藏在轨道下面（focus 仍可达，走 :focus-visible） */
.rf-switch input {
    position: absolute;
    width: 100%;
    height: 100%;
    margin: 0;
    opacity: 0;
    cursor: pointer;
}

.rf-switch-track {
    position: relative;
    display: block;
    width: 28px;
    height: 16px;
    /* 胶囊：轨道高 16px，圆角取胶囊档（≥ 半高即被钳成半高，永远是真胶囊）——
       早先写的 16px 是「凑巧等于半高」的隐形胶囊，一改高度就露馅。 */
    border-radius: var(--rf-r-pill);
    background: var(--tblr-border-color);
    transition: background-color var(--rf-dur) var(--rf-ease-curve);
}

.rf-switch-track::after {
    content: '';
    position: absolute;
    top: 2px;
    left: 2px;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 1px 2px rgba(0, 0, 0, .25);
    transition: transform var(--rf-dur) var(--rf-ease-curve);
}

.rf-switch input:checked + .rf-switch-track {
    background: var(--tblr-primary);
}

.rf-switch input:checked + .rf-switch-track::after {
    transform: translateX(12px);
}

.rf-switch input:focus-visible + .rf-switch-track {
    outline: var(--rf-focus-ring-width) solid var(--rf-focus-ring-color);
    outline-offset: var(--rf-focus-ring-offset);
}

.rf-theme-swatches {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

/* 色块：底色由内联的 --rf-swatch 给（hex 来自 App\Support\Theme::COLORS） */
.rf-swatch {
    width: 24px;
    height: 24px;
    padding: 0;
    border: 0;
    border-radius: var(--rf-r-sm);
    background: var(--rf-swatch);
    cursor: pointer;
}

.rf-swatch.is-active {
    box-shadow: 0 0 0 2px var(--tblr-bg-surface), 0 0 0 4px var(--rf-swatch);
}

.rf-theme-hint {
    color: var(--tblr-secondary);
    font-size: var(--rf-fs-xs);
    line-height: 1.4;
    min-height: 17px;
}

.rf-theme-hint.is-error {
    color: var(--tblr-danger);
}

/* ============================================================
   工作区面板
   ============================================================ */

/* 比主题面板宽 20px：插件名普遍比「标准 / 紧凑」这类轴标签长，
   240px 下「UI 组件库」「应用市场」会被省略号吃掉。 */
.rf-ws-panel {
    width: 260px;
}

.rf-ws-title {
    margin-bottom: 2px;
    color: var(--tblr-body-color);
    font-size: var(--rf-fs-sm);
    font-weight: 500;
}

.rf-ws-desc {
    margin: 0 0 10px;
    color: var(--tblr-secondary);
    font-size: var(--rf-fs-xs);
    line-height: 1.4;
}

.rf-ws-group {
    margin-bottom: 12px;
}

/* 与主题面板的分组标签同档：主色次要、13px 500 —— 两个面板并排放，
   标签的字重与颜色必须一致，否则一眼看出是两套东西。 */
.rf-ws-label {
    display: block;
    margin-bottom: 6px;
    color: var(--tblr-secondary);
    font-size: var(--rf-fs-sm);
    font-weight: 500;
}

/* 列表限高滚动：插件装到二十几个是常态，全铺开会把面板顶出视口。
   滚动条收在列表内，头尾说明与按钮始终可见。 */
.rf-ws-list {
    max-height: 300px;
    margin: 0 0 12px;
    padding: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    list-style: none;
}

.rf-ws-item {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 5px 6px;
    border-radius: var(--rf-r-xs);
    color: var(--tblr-body-color);
    font-size: var(--rf-fs-sm);
    cursor: pointer;
}

.rf-ws-item:hover {
    background: var(--tblr-bg-surface-secondary);
}

/* 勾选框自己给尺寸：Tabler 的 .form-check 依赖 padding-left 定位，
   面板里是裸 input，交给浏览器默认尺寸会在两行之间忽大忽小。 */
.rf-ws-item input {
    flex: none;
    width: 14px;
    height: 14px;
    margin: 0;
    cursor: pointer;
    accent-color: var(--tblr-primary);
}

.rf-ws-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.rf-ws-count {
    flex: none;
    color: var(--tblr-secondary);
    font-size: var(--rf-fs-xs);
}

.rf-ws-foot {
    display: flex;
    gap: 6px;
}

.rf-ws-foot .btn-primary {
    flex: 1 1 auto;
}

/* ============================================================
   表单校验错误态
   ============================================================ */

/* 报错只在字段下方的 .invalid-feedback 里说话（服务端报错与前端
   form-validate.js 共用同一位置）。Tabler 默认还会在控件右侧画一个 ✕
   背景图标并预留内边距 —— 这里把图标去掉，右内边距收回基础值。
   选择器带元素名（input./textarea./select.）是为了在同等 !important
   下以更高优先级压过 Tabler 的图标内边距规则（本文件在 tabler 之后加载，
   同权重靠 !important 对拼）。 */

/* 输入框 / 文本域：去 ✕，右内边距回到基础值 16px */
input.form-control.is-invalid,
textarea.form-control.is-invalid {
    background-image: none;
    padding-inline-end: 16px !important;
}

/* 下拉：只摘掉 ✕ 那层背景（背景由「箭头 + ✕」两层组成），右内边距回到 48px */
select.form-select.is-invalid {
    background-image: var(--tblr-form-select-bg-img);
    padding-inline-end: 48px !important;
}

/* ==================================================================
 * 减少动态效果（系统级无障碍开关，prefers-reduced-motion）
 *
 * 用户在系统里打开「减少动态效果」时，是在明确说动画会让他不适（前庭障碍、
 * 偏头痛等）。这里把外壳范围内的过渡 / 动画压到近乎瞬时 —— 只改时长，
 * 不删事件：交互逻辑与状态切换完全不变，只是不再「动」。
 *
 * 两条边界：
 *   ① 作用域限 .rf-shell（外壳），不写全局 `*`。rageframe.css 内容页也会加载，
 *      全局通杀会把应用视图、富文本等第三方页面自己的动效一起干掉。
 *   ② 只压 .rf-shell 子树。iframe 是独立文档，其内部动效不由这条规则管
 *      （内容页若要跟随，应由该页自己声明）。
 *
 * scroll-behavior 一并压掉：标签的 scrollIntoView（定位当前选项卡）也属于
 * 会被厌恶的位移。0.01ms 而非 0 —— 有些浏览器对 duration:0 的 transitionend
 * 不再派发，留一点尾巴更稳。
 * ================================================================== */

@media (prefers-reduced-motion: reduce) {
    .rf-shell *,
    .rf-shell *::before,
    .rf-shell *::after {
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .01ms !important;
        scroll-behavior: auto !important;
    }
}

/* 首屏定型期间同样不播过渡（配合 html.rf-booting，见下面 opacity 那条与
   shell.blade.php 的 bootDone()）。
   同上：只压 .rf-shell 子树，iframe 里的内容页是独立文档、不受这条管。
   实测：刷新时侧栏展开箭头原本会完整播一遍 rotate(90deg)（0.15s），矩形 11x15 → 15x11
   —— 那一下就是用户说的「错位感」。 */
html.rf-booting .rf-shell *,
html.rf-booting .rf-shell *::before,
html.rf-booting .rf-shell *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
}

/* 首屏定型期间**侧栏菜单整体不可见** —— 光压过渡不够，变化本身还是看得见。
   菜单展开是「首帧之后才发生」的，而且没法提前到首帧之前：boot 脚本无论放在
   body 的哪个位置（侧栏后 / 标签栏后 / body 末尾），浏览器都会在 HTML 解析中途
   插入渲染机会，抢在脚本前面画一帧（CDP 逐帧实测：第一次 rAF 采样时箭头永远是
   收起态，下一帧才被脚本改成展开）。于是用户看到的就是「子菜单蹦出来 + 箭头转
   一下」—— 冷缓存下全量矩形对账有 20 个元素从 [0,0 0x0] 变成有尺寸。
   压 duration 只能让它瞬间跳、不能让它不发生；所以改成**让它看不见**：菜单在
   定型完成前 opacity:0（布局位置保留，不变形），定型落地后才显形 —— 用户看到的
   是一块菜单出现，而不是一块菜单先折叠再展开。

   ⚠️ 只对 .rf-tab-sync 生效（规则本身仍挂在 .rf-booting 子树里，由 _head 加类）。
   加了 .rf-tab-sync 的唯一场合是「hash 与服务端直出的地址不一致、JS 必须当场
   纠偏」（判断在 shell.blade.php 的 <head> 里）。常规刷新服务端已经按 cookie
   直出终态，菜单在第一帧就是展开 / 高亮的 —— 那时再藏一次，就等于把「先空
   后满」的揭示重新演一遍，白折腾。
   撤除时机由 shell.blade.php 的菜单脚本在「展开已落地」后撤（_head.blade.php
   里另有一个兜底），不是固定延时。 */
html.rf-booting.rf-tab-sync #rf-menu-nav {
    opacity: 0;
}

/* ============================================================
   全局 Toast（右上角悬浮提示）
   ============================================================ */

/* 容器由 layouts/_toasts.blade.php 渲染：position:fixed top-0 end-0。
   层级走 --rf-z-toast（1080），写在元素内联 style 里（压过 Tabler 的 modal 1055 / dropdown 1000）。
   这里只补最大宽度 —— 长消息不撑满整屏，阅读更舒服。
   内容页在 iframe 里时，fixed 是相对于 iframe 视口，即内容区右上角。 */
.rf-toast-container {
    max-width: 384px;
}

/* 外壳页的 toast 往下让开顶栏（+ 标签栏）。
   容器上的定位是 Bootstrap 工具类，`.top-0` 自带 !important —— 普通声明压不过，
   这里必须跟一个（同文件里几处压 Tabler 的规则是同一做法）。
   为什么不让它待在 top:0：那一格正是顶栏的主题 / 全屏 / 账号三个入口所在，
   一条 toast 盖上去既挡住观感、也挡住点击（Bootstrap 的 toast 是 pointer-events:auto）。
   让开之后落在内容区顶端 —— 与内容页（iframe 里 fixed 相对 iframe 视口，落点就是
   内容区右上角）是同一个位置，两处观感一致。
   标签页轴 = off 时标签栏整条收起，那一段高度不占。 */
.rf-shell-body .rf-toast-container {
    top: 56px !important;
}

.rf-shell-body:not([data-rf-tabs="off"]) .rf-toast-container {
    top: 96px !important;
}

/* 单条 Toast：Bootstrap .toast 默认 min-width 与内边距已够用，这里只把
   字号压到正文档、圆角与项目其它卡片统一（theme.css 已覆盖全局 --tblr-border-radius）。 */
.rf-toast-container .toast {
    font-size: var(--rf-fs-body);
}

/* ============================================================
   统一确认弹窗（data-confirm，由 rageframe.js 懒创建）
   ============================================================ */

/* 骨架与配色全用 Tabler 默认（modal-dialog-centered modal-sm），这里只补一条：
   正文支持多行。

   ⚠️ 正文是 rageframe.js 用 **textContent** 灌进去的（不是 innerHTML），所以：
     · 文案要换行只能靠**真实换行符** —— 视图里写 `&#10;`（HTML 字符引用，
       浏览器解析属性时解码成 LF）。写 `\n` 是无效的：那是 JS 字符串转义，
       在 HTML 属性里只是两个普通字符。
     · 没有下面这条 white-space，连续换行会**折叠成一个空格** ——
       「先问一句 + 空行 + 说清后果」的两段式文案会挤成一整块。
     · 单行文案不受影响：pre-line 只保留换行，空格与制表符照旧折叠，
       没写换行符时与 normal 表现完全一致（所以这条对存量弹窗是零风险）。 */
.rf-confirm-text {
    white-space: pre-line;
}

/* 弹窗是「决策」界面，不是阅读区：图标 / 标题 / 按钮一律禁止选中。
   按钮文案（含图标按钮里的 <i>）默认是可选中文字 —— 按下后手稍一抖或手快连点，
   就会在页面上拉出一段选区；弹窗盖住的是视线，盖不掉选区高亮，取消弹窗后它还
   停在原地，看着像页面出了 bug。rageframe.js 已在打开弹窗时清一次选区、并在
   mousedown 阶段压掉触发键上的拖选，这里再补一层兜底：就算有人双击弹窗里的
   图标 / 标题 / 按钮，也拉不出选区。正文保持可选 —— 文案里若带了路径、清单，
   用户还复制得了。 */
.rf-confirm-modal {
    user-select: none;
    -webkit-user-select: none;
    /* 插入符（caret）不是选区，上面两行管不住它：Chrome 开了「文本光标浏览」
       （chrome://settings/accessibility，或误按 F7）后，弹窗一打开 focus 落进
       modal、或使用者在弹窗里点了任何一下，浏览器就会在第一行文本（图标行）
       行首画一个闪烁的插入符 —— 高度 = 图标的 40px 行高、颜色继承
       text-danger 的红，看着像「图标前竖了根红线」。弹窗里没有任何可编辑
       内容，插入符在这里永远没有意义，直接令其不可见。 */
    caret-color: transparent;
}

.rf-confirm-modal .rf-confirm-text {
    user-select: text;
    -webkit-user-select: text;
}

/* 正文的高度上限。更新确认弹窗里会列「本次版本的主要变更条目」（见
   AddonsUpdater::planMessage），文案长度由应用作者写的日志决定 —— 弹窗是
   居中固定的，长到超过视口时底部的「取消 / 确认」就被顶到看不见了。
   这里让正文在弹窗内部滚动，按钮永远够得着。短文案不受影响
   （max-height 没触顶时，这段跟不存在一样）。 */
.rf-confirm-modal .modal-body {
    max-height: 60vh;
    overflow-y: auto;
}

/* 确认弹窗里的富文本附注（data-confirm-notes，更新记录用）
   与正文相反：正文禁选（决策界面），附注**要能选中复制** —— 使用者常要把
   「这一版改了什么」拷给同事确认。左对齐同理：条目列表居中排读不出结构。 */
.rf-confirm-notes {
    text-align: left;
    user-select: text;
    -webkit-user-select: text;
}

/* ============================================================
   更新记录（检测更新 / 更新确认弹窗里的 Markdown 渲染结果）
   ============================================================ */

/* 服务端渲染好的 HTML（App\Support\Markdown / 发布方写的 Markdown）。这里
   只补「文档级」元素的最小排版：弹窗里的字号跟随正文，标题不复用页面
   h1~h6 的层级（否则一个 ## 会把弹窗标题压下去）。色值全走令牌。 */
.rf-release-notes {
    font-size: var(--rf-fs-body);
    line-height: 1.75;
}

.rf-release-notes > :first-child { margin-top: 0; }
.rf-release-notes > :last-child { margin-bottom: 0; }

.rf-release-notes h1,
.rf-release-notes h2,
.rf-release-notes h3,
.rf-release-notes h4,
.rf-release-notes h5,
.rf-release-notes h6 {
    font-size: calc(var(--rf-fs-body) + 1px);
    font-weight: 600;
    margin: 1.1em 0 .45em;
}

.rf-release-notes ul,
.rf-release-notes ol {
    margin-bottom: .75rem;
    padding-left: 1.3rem;
}

.rf-release-notes li { margin-bottom: .3rem; }

.rf-release-notes code {
    font-size: .92em;
    padding: .1rem .35rem;
    border-radius: 4px;
    background: var(--rf-surface-2, rgba(0, 0, 0, .05));
}

.rf-release-notes pre {
    padding: .75rem;
    border-radius: 6px;
    overflow-x: auto;
    background: var(--rf-surface-2, rgba(0, 0, 0, .05));
}

.rf-release-notes pre code { padding: 0; background: none; }

.rf-release-notes img { max-width: 100%; height: auto; }

/* 中间版本列表（<details> 逐版展开）：默认收起，点标题才渲染出内容。
   用原生 details 而不是 collapse 组件 —— 这里没有任何服务端状态要同步，
   原生元素在键盘 / 屏幕阅读器上的行为也已经是完整的。 */
.rf-update-version {
    padding: .45rem 0;
    border-bottom: 1px dashed var(--rf-line);
}

.rf-update-version:last-child { border-bottom: 0; }

.rf-update-version > summary {
    cursor: pointer;
    list-style: none;
    display: flex;
    align-items: baseline;
    gap: .25rem;
}

.rf-update-version > summary::-webkit-details-marker { display: none; }

/* 收起时 ▸、展开时 ▾ —— 只用 CSS，不额外挂图标节点 */
.rf-update-version > summary::before {
    content: '▸';
    color: var(--rf-muted);
    font-size: .8em;
    transition: transform .15s ease;
}

.rf-update-version[open] > summary::before { content: '▾'; }

/* ============================================================
   应用更新日志时间线（应用详情页 · 应用根的 CHANGELOG.md）
   ============================================================ */

/* 结构：<ol class="rf-changelog"> > <li class="rf-changelog-release">，一版一节。

   竖线画在每个节点自己的 ::before 上，而不是容器的 border-left —— 最后一个
   节点不画线，时间线就不会拖出一条没有节点的尾巴。节点圆点带页面底色描边，
   看着才像「穿」在线上而不是浮在旁边。 */

.rf-changelog {
    margin: 0;
    padding: 0 0 0 20px;
    list-style: none;
}

.rf-changelog-release {
    position: relative;
    padding-bottom: 20px;
}

.rf-changelog-release:last-child {
    padding-bottom: 0;
}

.rf-changelog-release::before {
    content: '';
    position: absolute;
    left: -20px;
    top: 8px;
    bottom: 0;
    width: 1px;
    background: var(--rf-line);
}

.rf-changelog-release:last-child::before {
    display: none;
}

.rf-changelog-release::after {
    content: '';
    position: absolute;
    left: -20px;
    top: 7px;
    width: 8px;
    height: 8px;
    margin-left: -4px;
    border: 2px solid var(--rf-line-strong);
    border-radius: 50%;
    background: var(--rf-surface);
}

/* 最新一版的节点染主色 —— 「现在在哪」一眼可见 */
.rf-changelog-release:first-child::after {
    border-color: var(--tblr-primary);
    background: var(--tblr-primary);
}

/* 版本行：版本号 / 日期 / 徽章同一行，窄屏自动折行 */
.rf-changelog-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin-bottom: 8px;
}

/* 变更条数推到行尾：它是这一节的「总量」，不该跟左侧版本号抢视线 */
.rf-changelog-count {
    margin-left: auto;
    font-size: var(--rf-fs-xs);
    color: var(--rf-faint);
}

/* 同一版本内的多个分组之间留一点空隙 */
.rf-changelog-group + .rf-changelog-group {
    margin-top: 12px;
}

.rf-changelog-items {
    margin: 6px 0 0;
    padding-left: 18px;
    font-size: var(--rf-fs-sm);
    color: var(--rf-muted);
}

.rf-changelog-items li + li {
    margin-top: 4px;
}

/* 条目里的 `代码`：日志里出现最多的是方法名 / 配置项 / 路径 */
.rf-changelog-items code,
.rf-changelog-blocks code {
    padding: 1px 5px;
    border-radius: var(--rf-r-xs);
    background: var(--rf-code-bg);
    color: var(--rf-code-ink);
    font-size: .92em;
}

/* 块级内容（作者写的引用 / 代码块 / 表格，以及整节兜底渲染）：与条目列表同处
   一级，但用面板底色区分出「这是补充说明，不是一条变更」。 */
.rf-changelog-blocks {
    margin-top: 8px;
    padding: 10px 14px;
    border-radius: var(--rf-r-sm);
    background: var(--rf-surface-2);
    font-size: var(--rf-fs-sm);
    color: var(--rf-muted);
}

/* 兜底块的首尾元素不带外边距 —— 间距已经由容器的 padding 给了 */
.rf-changelog-blocks > :first-child {
    margin-top: 0;
}

.rf-changelog-blocks > :last-child {
    margin-bottom: 0;
}

/* 代码块在窄屏下可能比容器宽（一行配置 / 一条命令）：内部横向滚动，
   不许它把卡片撑破，也不许无声截断 —— 滚得动才看得全。 */
.rf-changelog-blocks pre {
    max-width: 100%;
    overflow-x: auto;
}

.rf-changelog-blocks table {
    width: 100%;
    margin-bottom: 8px;
}

/* 前言（第一个 `## ` 之前的内容）：卡片头的补充说明，用一条细分隔线与时间线分开 */
.rf-changelog-intro {
    margin-bottom: 16px;
    padding-bottom: 12px;
    border-bottom: 1px solid var(--rf-line-soft);
    font-size: var(--rf-fs-sm);
    color: var(--rf-muted);
}

.rf-changelog-intro > :last-child {
    margin-bottom: 0;
}

/* 正文区：限高 + 内部滚动。
   日志随版本一路积累，几十版时能把页面拉得极长 —— 详情页要滚很久才到底，
   弹窗里更是把标题顶出屏幕。限高后只滚这一段，「这是哪个应用、哪一版」
   始终留在视野里（详情页与弹窗共用这一份，见 _changelog.blade.php）。

   max-height 取 min(512px, 60vh)：桌面定格在一个顺手的高度，矮视口
   （手机横屏 / 笔记本小窗 / 分屏）跟着视口一起缩，不至于把弹窗撑满。

   ⚠️ overflow-y: auto 会把 overflow-x 一并算成 auto（规范不允许 visible 与
      auto 共存）—— 于是时间线节点圆点那 4px 的左溢出会被裁掉、还平白多出
      一条横向滚动条。左侧那点内边距就是给它的：把圆点纳入裁切框内。
      右侧 8px 则给滚动条让位（细滚动条样式见 theme.css 的全局滚动条），
      不给的话内容会贴着滚动条。 */
.rf-changelog-scroll {
    max-height: min(512px, 60vh);
    overflow-y: auto;
    padding: 0 8px 0 4px;
}

/* ============================================================
   分区标签页（data-rf-tabs；切换逻辑在 rageframe.js 的同名模块）
   ============================================================ */

/* 标签名后面的计数（应用详情页：待执行脚本数 / 版本数 / 各日志表条数）。
   它是次要信息，默认比标签名淡；激活时才提亮到主色 —— 同一个数字在未选中的
   标签上与「当前这个标签里有多少条」不该是同一个视觉重量。 */
.rf-tab-count {
    margin-left: 6px;
    font-size: var(--rf-fs-xs);
    color: var(--rf-faint);
}

.nav-link.active .rf-tab-count {
    color: var(--tblr-primary);
}

/* ============================================================
   应用详情页：日志表格（安装日志 / 更新记录 / 全部日志三个标签页共用）
   ============================================================ */

/* 限高 + 内部滚动。三个面板共用一份高度，切标签时卡片不会忽高忽低；
   取值与「更新日志」的时间线一致（同一个页面里两个滚动区高度相同，滚起来
   才连贯，min(512px, 60vh) 的解释见 .rf-changelog-scroll）。

   ⚠️ overflow 写 auto（两个方向）而不是只写 overflow-y：规范不允许 visible
      与 auto 共存，只写 y 的话 x 会被一并算成 auto —— 与其被这条规则牵着走，
      不如明说「宽度不够就横向滚」（全部日志有 5 列，窄屏必然超宽）。 */
.rf-log-scroll {
    max-height: min(512px, 60vh);
    overflow: auto;
}

/* 表头吸顶：滚到第 20 条时还得知道这一列是什么。
   单元格背景来自 Bootstrap .table-light 的变量、本身不透明，吸顶后内容不会
   从下面透出来；下边框改用 box-shadow 画 —— collapse 表格里 sticky 单元格的
   border 会跟着内容一起滚走，最后只剩背景（没有这条线，表头和第一行会糊成一片）。 */
.rf-log-scroll thead th {
    position: sticky;
    top: 0;
    z-index: var(--rf-z-base);
    box-shadow: inset 0 -1px 0 var(--rf-line);
}

/* ============================================================
   本地应用页（列表 / 详情 / 日志）
   ------------------------------------------------------------
   三个页面共用一套排版语言：页头 → 指标条 → 工具条 → 卡片。
   所以样式集中在这一段，不按页面拆三段 —— 拆开之后同一套间距（页头下 20px、
   卡片之间 16px）会各漂各的，改一处得回想另外两处。

   配色一律引 theme.css 的令牌：明暗两套皮、用户换主色都自动跟随，
   这一段不写死任何色值。

   ⚠️ **主题文件在本文件之后加载**（顺序见 layouts/_head.blade.php：
      tabler → rageframe.css → theme.css）。同权重后置即胜出，所以想改
      .card / .card-body / .card-header / .card-footer 这些**theme.css 已经
      定义过**的属性时，选择器必须比它更具体（如 `.rf-addon-card > .card-body`），
      只写一个类名会被后加载的 theme.css 顶掉 —— 这类地方下面都写了注释。
   ============================================================ */

/* 页头已抽成组件 x-ui.page-head（两栏 9/3、两套图标皮、可选下划线），
   样式跟着组件走 —— 见 resources/views/components/ui/page-head.blade.php。
   这里不再留一份：两份同名类迟早在「哪边才是真的」上分叉。 */

/* ---- 工具条：搜索 + 分段筛选 + 计数，同一行 ---- */

.rf-toolbar {
    /* 三个控件（搜索框 / 分段控件 / 按钮）统一到这一个高度，不然并排会参差。
       比页面默认档（32px / 32px）再矮一档；py 配套给 3px，
       工具条里的默认档按钮天然高度（3+3+20+2）才能落在这 28px 上。 */
    --rf-ctl-h: 28px;
    --rf-ctl-py: 3px;

    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    /* 与上方区块（页头）保持一段呼吸空隙（列内边距只给 8px，太少） */
    margin-top: 20px;
    margin-bottom: 16px;
    padding: 12px 16px;
    /* 与卡片同口径：**不描边** —— 工具条就是一条「扁卡片」（白底 + 同一档
       --rf-r-lg 圆角 + --rf-shadow-sm）。卡片都去了边框它还描着一圈，
       列表页里两张白块就会一框一框地不齐。 */
    border: 0;
    border-radius: var(--rf-r-lg);
    background: var(--rf-surface);
    box-shadow: var(--rf-shadow-sm);
}

/* Tabler 的分段控件自带「凹槽」：灰色轨道 + inset 描边阴影，看着像内阴影。
   这里压平：轨道透明、阴影去掉 —— 工具条整体是平面风格，
   只留每一项自己的描边与选中态。 */
.rf-toolbar .nav-segmented {
    --tblr-nav-bg: transparent;
    box-shadow: none;
}

.rf-toolbar .form-control,
.rf-toolbar .form-select {
    height: var(--rf-ctl-h);
    padding-top: 0;
    padding-bottom: 0;
    font-size: var(--rf-fs-sm);
}

/* Tabler 的分段控件（.nav-segmented）：高度与字号改走我们这一行的刻度。
   选择器带 .rf-toolbar 是为了压过 Tabler 自己的 `.nav-sm`（同权重、但它在
   本文件之前，所以其实后置的我们能赢；写全是为了以后调整加载顺序也不塌）。 */
.rf-toolbar .nav-sm {
    --tblr-nav-height: var(--rf-ctl-h);
    --tblr-nav-font-size: var(--rf-fs-sm);
    --tblr-nav-link-padding-x: 12px;
}

/* 选中态统一到项目口径（主色淡底 + 主色字 + 主色描边，同 .btn-check 与页签），
   不用 Tabler 默认的「白底 + 中性描边」—— 那样在浅色皮上与未选中项几乎分不出。 */
.rf-toolbar .nav-segmented .nav-link-input:checked + .nav-link {
    border-color: var(--tblr-primary);
    background: var(--rf-accent-bg);
    color: var(--tblr-primary);
}

.rf-toolbar-spacer {
    margin-left: auto;
}

.rf-toolbar-count {
    font-size: var(--rf-fs-sm);
    color: var(--rf-faint);
    font-variant-numeric: tabular-nums;
}

/* 分段控件每一项后面的数量：与按钮文字同色（选中态会跟着变主色），
   只用透明度压一档 —— 另给颜色会在选中态和主色字打架。 */
.rf-toolbar .rf-seg-count {
    margin-left: 4px;
    font-variant-numeric: tabular-nums;
    opacity: .65;
}

/* 搜索框：图标压在输入框左边（padding 让位），右边给「清空」留位 */
.rf-search {
    position: relative;
    display: flex;
    align-items: center;
    flex: 1 1 240px;
    max-width: 352px;
}

.rf-search > .rfi {
    position: absolute;
    left: 10px;
    font-size: var(--rf-icon-size);   /* 默认档：搜索框里的放大镜 */
    color: var(--rf-faint);
    pointer-events: none;
}

.rf-search .form-control {
    padding-left: 32px;
}

.rf-search-clear {
    position: absolute;
    right: 6px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    padding: 0;
    border: 0;
    border-radius: var(--rf-r-xs);
    background: transparent;
    color: var(--rf-faint);
    font-size: var(--rf-icon-size-sm);   /* 小控件档：搜索框里的清除键 */
    line-height: 1;
    cursor: pointer;
}

@media (hover: hover) {
    .rf-search-clear:hover {
        color: var(--rf-danger);
        background: var(--rf-surface-2);
    }
}

/* ---- 复制键（应用标识 / manifest 原文 / 命令行）---- */

/* 尺寸写死：它出现在标题行、标识行、卡片头三处，位置不同但必须一样大，
   否则同一页里的复制键会一个比一个大。 */
.rf-copy {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 24px;
    height: 24px;
    padding: 0;
    border: 0;
    border-radius: var(--rf-r-xs);
    background: transparent;
    color: var(--rf-faint);
    font-size: var(--rf-icon-size-sm);   /* 小控件档：24px 圆钮里的复制键 */
    line-height: 1;
    cursor: pointer;
    transition: color var(--rf-dur) var(--rf-ease-curve),
                background-color var(--rf-dur) var(--rf-ease-curve);
}

@media (hover: hover) {
    .rf-copy:hover {
        color: var(--tblr-primary);
        background: var(--rf-surface-2);
    }
}

.rf-copy:focus-visible {
    outline: var(--rf-focus-ring-width) solid var(--rf-focus-ring-color);
    outline-offset: var(--rf-focus-ring-offset-tight);
}

/* 复制成功的瞬时反馈 = 图标换成勾 + 上面弹一枚气泡。
   气泡**绝对定位**：只是浮在按钮上方，不参与布局 —— 卡片尺寸一点不动
   （这正是「瞬时反馈不许改变布局」那条硬要求：若改成在行内插一段「已复制」
   文字，整张卡会被撑宽一下再弹回来）。文案由 JS 写在 data-rf-copied-label 上，
   CSS 只管画，所以不用为「已复制 / 已复制命令」这类差异再写一条规则。 */
.rf-copy.rf-copied {
    color: var(--rf-ok);
    background: var(--rf-ok-bg);
}

.rf-copy.rf-copied::after {
    content: attr(data-rf-copied-label);
    position: absolute;
    bottom: calc(100% + 5px);
    left: 50%;
    transform: translateX(-50%);
    z-index: var(--rf-z-raised);
    padding: 2px 6px;
    border-radius: var(--rf-r-xs);
    background: var(--rf-ink);
    color: var(--rf-surface);
    font-size: 11px;
    line-height: 1.5;
    white-space: nowrap;
    pointer-events: none;
}

/* ---- 应用列表（一行一个）---- */

/* 本地应用页（列表 / 详情）：按钮里的图标统一收到极小档（10px）。
   按钮文字是 12 / 14px，图标按 1em 跟随会跟着偏大 —— 按钮里的图标只是
   配角，这里按页面口径整体压到 --rf-icon-size-xs（本页所有 .btn 都在
   .rf-addons-page 内；:where() 那条全局基线零优先级，这条照常胜出）。 */
.rf-addons-page .btn .rfi {
    font-size: var(--rf-icon-size-xs);
}

/* 一行一个应用：整页高度减半以上，扫读是竖直向下的单一动线
   （卡片网格 auto-fill minmax(336px,1fr) 同屏只能看两三个）。 */
.rf-addon-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 10px;
}

/* 行内分栏：图标 | 中段（标题/描述/元信息）| 动作组。垂直居中——
   三段高度不同，顶对齐会让图标和按钮浮在半空。卡片本身是 Tabler 的
   flex column，这里直接改 .card-body 的主轴方向。 */
.rf-addon-grid > .rf-addon-card > .card-body {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 12px 20px;
}

.rf-addon-card {
    transition: box-shadow var(--rf-dur) var(--rf-ease-curve),
                border-color var(--rf-dur) var(--rf-ease-curve);
}

/* hover 抬一档阴影 + 加深描边：整行是「可点进去的」，
   得给出鼠标经过的反馈（与 a.card 的既有观感一致）。 */
@media (hover: hover) {
    .rf-addon-card:hover {
        border-color: var(--rf-line-strong);
        box-shadow: var(--rf-shadow-pop);
    }
}

/* 中段：占满图标与动作组之间的全部宽度，内部纵向排三行 */
.rf-addon-main {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}

/* 标题 → 机器名+复制键 → 状态徽章，一行排开（徽章跟在复制键后面 ——
   状态是行头的附属信息，离标题近才读得顺；行右端已经让给动作组了）。
   放得下就一行，挤了先让徽章折到下一行，名字照旧在自身内省略。 */
.rf-addon-ident {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 8px;
    min-width: 0;
}

/* 图标块底色按应用状态分四档 —— 扫一眼列表就知道哪个开着、哪个没装、
   哪个目录已经丢了。四档都从令牌派生，不写死色值。 */
.rf-addon-mark,
.rf-addon-hero-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    /* 透明 border 常驻占位：描边态只换 border-color，图标块尺寸不变 */
    border: 1px solid transparent;
    background: var(--tblr-primary);
    color: var(--tblr-primary-fg);
}

.rf-addon-mark {
    width: 36px;
    height: 36px;
    border-radius: var(--rf-r);
    font-size: 18px;
    line-height: 1;
}

/* 已安装但已禁用：淡主色底 + 主色字（「装着但没跑」，比实心轻一档） */
.rf-addon-mark[data-state="idle"] {
    background: var(--rf-accent-bg);
    color: var(--tblr-primary);
}

/* 未安装：中性淡底 + 描边（不抢眼，因为它还不是这套系统的一部分） */
.rf-addon-mark[data-state="off"] {
    background: var(--rf-surface-2);
    color: var(--rf-muted);
    border-color: var(--rf-line-strong);
}

/* 目录已丢失（只剩注册残留）：红系淡底，与行上的「目录已丢失」徽章同色 */
.rf-addon-mark[data-state="gone"] {
    background: var(--rf-danger-bg);
    color: var(--rf-danger-ink);
    border-color: var(--rf-danger-line);
}

/* 标题：行头的主体，宽度不参与挤压（名字负责省略） */
.rf-addon-title {
    display: inline-flex;
    align-items: center;
    flex: none;
    font-size: var(--rf-fs-h4);
    font-weight: 500;
    line-height: 1.3;
    color: var(--rf-ink);
    text-decoration: none;
}

@media (hover: hover) {
    .rf-addon-title:hover {
        color: var(--tblr-primary);
    }
}

/* 标识：等宽字 + 复制键。只占一行，长了就省略 ——
   标识是机器名（tiny-demo 这类），留半截比换行好看。 */
.rf-addon-name {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    min-width: 0;
    font-size: var(--rf-fs-xs);
    color: var(--rf-faint);
}

.rf-addon-name > span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* 状态徽章：跟在复制键后面的行头附属信息，常驻行内。
   比常规徽章小一号 —— 它与机器名同排，不该和标题抢视觉重量；
   放不下时整体折到下一行（ident 允许 wrap），不把机器名挤成省略号。 */
.rf-addon-badges {
    display: inline-flex;
    flex: none;
    flex-wrap: wrap;
    gap: 4px;
}

.rf-addon-badges .badge {
    padding: .2em .45em;
    font-size: 10px;
    font-weight: 500;
    line-height: 1.5;
}

/* 描述只给一行：一行一个应用时行高是稀缺资源，
   全文进详情页看。用 line-clamp 而不是后端截断：不破坏原文，
   也不会在词中间断。 */
.rf-addon-desc {
    margin: 0;
    font-size: var(--rf-fs-sm);
    line-height: 1.5;
    color: var(--rf-muted);
    display: -webkit-box;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.rf-addon-desc-empty {
    color: var(--rf-faint);
    font-style: normal;
}

/* 元信息：版本 / 作者 / 入口，小字一行流，窄屏自动折行 */
.rf-addon-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 14px;
    font-size: var(--rf-fs-xs);
    color: var(--rf-faint);
}

.rf-addon-meta-item {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    min-width: 0;
}

.rf-addon-meta-item .rfi {
    font-size: var(--rf-icon-size-sm);   /* 小控件档：应用元信息行与 12px 文字同排 */
}

/* 元信息里的链接（版本旁边的「更新日志」）：与旁边几项同色同号，
   只有 hover 才提亮 —— 这一行是**次级信息**，一条常驻的蓝色链接会把
   行的重量从「应用」拉回「链接列表」。 */
.rf-addon-meta-link {
    color: var(--rf-faint);
    text-decoration: none;
}

@media (hover: hover) {
    .rf-addon-meta-link:hover {
        color: var(--tblr-primary);
        text-decoration: underline;
    }
}

/* 行右端动作组：主动作靠左、危险与「详情」靠右。
   收进行内而不是单独一条页脚 —— 一行一个应用时页脚会把行高翻倍。 */
.rf-addon-actions {
    display: flex;
    flex: none;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: 6px;
}

.rf-addon-actions-side {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

@media (max-width: 991.98px) {
    /* 中窄屏：动作组让到下一行占满宽度靠左，中段的元信息有整行可折 */
    .rf-addon-grid > .rf-addon-card > .card-body {
        flex-wrap: wrap;
    }

    .rf-addon-actions {
        flex: 1 1 100%;
        justify-content: flex-start;
    }

    .rf-addon-actions-side {
        margin-left: 0;
    }
}

/* 图标块在极窄屏下略收一档，给中段的标题让位 */
@media (max-width: 575.98px) {
    .rf-addon-grid > .rf-addon-card > .card-body {
        gap: 12px;
        padding: 12px 16px;
    }
}

/* ---- 详情页：概览头 ---- */

.rf-addon-hero > .card-body {
    display: flex;
    flex-wrap: wrap;
    gap: 20px;
    padding: 24px;
}

.rf-addon-hero-main {
    display: flex;
    align-items: flex-start;
    gap: 16px;
    flex: 1 1 416px;
    min-width: 0;
}

.rf-addon-hero-mark {
    width: 56px;
    height: 56px;
    /* 56px 的图标片取卡片档 —— 它比 .rf-addon-mark(10) / .rf-brand-mark(8) 都大，
       圆角也跟到最大那一档（原来是手写的 16px=16px，不在这把尺上）。 */
    border-radius: var(--rf-r-lg);
    font-size: 28px;
    line-height: 1;
}

/* 概览头的四档底色与列表卡同一套（.rf-addon-mark[data-state] 一起写的，
   见上面的分组选择器） */
.rf-addon-hero-mark[data-state="idle"] {
    background: var(--rf-accent-bg);
    color: var(--tblr-primary);
}

.rf-addon-hero-mark[data-state="off"] {
    background: var(--rf-surface-2);
    color: var(--rf-muted);
    border-color: var(--rf-line-strong);
}

.rf-addon-hero-mark[data-state="gone"] {
    background: var(--rf-danger-bg);
    color: var(--rf-danger-ink);
    border-color: var(--rf-danger-line);
}

.rf-addon-hero-body {
    flex: 1 1 auto;
    min-width: 0;
}

.rf-addon-hero-title {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin: 0;
    font-size: var(--rf-fs-h1);
    font-weight: 500;
    line-height: 1.2;
    color: var(--rf-ink);
}

.rf-addon-hero-name {
    display: flex;
    align-items: center;
    gap: 4px;
    margin-top: 6px;
    font-size: var(--rf-fs-sm);
    color: var(--rf-faint);
}

.rf-addon-hero-badges {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 10px;
}

.rf-addon-hero-desc {
    margin: 12px 0 0;
    font-size: var(--rf-fs-sm);
    line-height: 1.7;
    color: var(--rf-muted);
}

/* 概览头右栏：关键事实 + 动作按钮。
   桌面用一条竖线与左侧正文分开（正文是「这是什么」，右栏是「现在什么状态、
   能干什么」）；窄屏竖线换成横线，两栏叠起来。 */
.rf-addon-hero-side {
    display: flex;
    flex: 0 1 352px;
    flex-direction: column;
    gap: 16px;
    padding-left: 20px;
    border-left: 1px solid var(--rf-line-soft);
}

.rf-addon-facts {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 12px 16px;
}

.rf-addon-fact-label {
    font-size: var(--rf-fs-xs);
    color: var(--rf-faint);
}

.rf-addon-fact-value {
    margin-top: 2px;
    font-size: var(--rf-fs-sm);
    font-weight: 500;
    color: var(--rf-ink);
    font-variant-numeric: tabular-nums;
}

/* 右栏底部的动作区：顶到栏底 —— 左栏描述有几行都不影响动作的位置，
   整页缩到窄屏（右栏折到下面）时也还是紧跟着事实表。 */
.rf-addon-hero-cta {
    margin-top: auto;
}

.rf-addon-hero-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

@media (max-width: 767.98px) {
    .rf-addon-hero-side {
        flex-basis: 100%;
        padding-top: 16px;
        padding-left: 0;
        border-top: 1px solid var(--rf-line-soft);
        border-left: 0;
    }
}

/* ---- 详情页：更新卡的三块勾选 ---- */

/* 把三个 <input type=checkbox> + 说明文字包成一个可点区块：
   说明文字有两行长，散在勾选框右边会看不出「这两行属于这一个选项」。
   整块是 <label>，点哪儿都能勾。 */
.rf-choice-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-bottom: 16px;
}

.rf-choice {
    display: flex;
    gap: 10px;
    padding: 12px 14px;
    border: 1px solid var(--rf-line);
    border-radius: var(--rf-r-sm);
    background: var(--rf-surface-2);
    cursor: pointer;
    transition: border-color var(--rf-dur) var(--rf-ease-curve),
                background-color var(--rf-dur) var(--rf-ease-curve);
}

@media (hover: hover) {
    .rf-choice:hover {
        border-color: var(--rf-line-strong);
    }
}

/* 选中态：主色淡描边 + 主色调淡底。
   用 :has() 读**里面那个勾选框**的状态，而不是自己维护一个 class ——
   勾选状态是表单的真源，样式跟着它走就不会出现「看着没勾、其实会上报」。
   （:has 在 Chrome 105+ / Safari 15.4+ / Firefox 121+ 支持；不支持时只是
    少了这层强调，勾选框本身照常工作。） */
.rf-choice:has(.form-check-input:checked) {
    border-color: var(--rf-accent-line);
    background: color-mix(in oklab, var(--tblr-primary) 6%, var(--rf-surface));
}

.rf-choice .form-check-input {
    flex: none;
    margin-top: 3px;
}

.rf-choice-label {
    font-size: var(--rf-fs-body);
    font-weight: 500;
    line-height: 1.4;
    color: var(--rf-ink);
}

.rf-choice-hint {
    display: block;
    margin-top: 2px;
    font-size: var(--rf-fs-sm);
    font-weight: 400;
    line-height: 1.6;
    color: var(--rf-muted);
}

/* ---- 详情页：清单原文（manifest.json）---- */

/* <pre> 的皮由 theme.css 给（内嵌块底 + 描边 + 6px 圆角，走 --rf-r），这里只补
   「限高 + 内部滚动」——manifest 可以很长，不封顶会把详情页拉到几屏。
   ⚠️ 选择器带 .rf-code 是为了比 theme.css 的裸 `pre` 更具体。 */
.rf-code > pre {
    max-height: min(448px, 60vh);
    margin: 0;
    overflow: auto;
    font-size: var(--rf-fs-xs);
    line-height: 1.7;
}

/* ---- 详情页 / 列表页：最近动态 ---- */

.rf-activity-row {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 10px 20px;
    border-top: 1px solid var(--rf-line-soft);
}

.rf-activity-row:first-child {
    border-top: 0;
}

@media (hover: hover) {
    .rf-activity-row:hover {
        background: var(--rf-surface-2);
    }
}

/* 时间列定宽 + 等宽数字：每行的事件徽章与文案才能对齐成一竖列 */
.rf-activity-time {
    flex: none;
    width: 88px;
    padding-top: 2px;
    font-size: var(--rf-fs-xs);
    color: var(--rf-faint);
    font-variant-numeric: tabular-nums;
}

.rf-activity-main {
    flex: 1 1 auto;
    min-width: 0;
    font-size: var(--rf-fs-sm);
    line-height: 1.6;
    color: var(--rf-muted);
}

.rf-activity-addon {
    color: var(--rf-ink);
    font-weight: 500;
}

/* ---- 日志页 / 详情页日志表：等宽时间列与可换行的摘要 ---- */

/* 时间与版本号都是等宽排版的短串，用等宽数字让每一行竖着对齐 */
.rf-log-time {
    font-size: var(--rf-fs-xs);
    color: var(--rf-faint);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* 摘要列：日志的 message 可能很长（迁移列表、错误信息），
   允许换行但别把表格撑出去 */
.rf-log-msg {
    font-size: var(--rf-fs-sm);
    color: var(--rf-muted);
    word-break: break-word;
}

/* ---- 运行日志（common/runtime-logs）：原始堆栈展开 ---- */

/* 默认收起：一条异常后面可能跟着几十行堆栈，全摊开会把列表挤成一面墙。
   summary 做成可点的浅色小字，展开区限高内滚 —— 表格行高不会因为某一条
   特别长的日志而被撑爆，需要看全文时在框里滚。 */
.rf-log-expand {
    margin-top: 4px;
}

.rf-log-expand > summary {
    font-size: var(--rf-fs-xs);
    color: var(--rf-faint);
    cursor: pointer;
    user-select: none;
}

.rf-log-expand > summary:hover {
    color: var(--tblr-primary);
}

.rf-log-trace {
    max-height: 18rem;
    overflow: auto;
    margin: 6px 0 0;
    padding: 8px 10px;
    border: 1px solid var(--rf-line);
    border-radius: var(--rf-r-sm);
    background: var(--rf-line-soft);
    color: var(--rf-muted);
    font-size: var(--rf-fs-xs);
    line-height: 1.55;
    white-space: pre-wrap;
    word-break: break-word;
}

/* ---- 日志详情弹层（common/behavior-logs/show）---- */

/* 信息区：dl 两栏网格 —— 比表格轻（没有边框线），标签列定宽右起，
   窄屏退单列（标签在上、值在下）。 */
.rf-log-detail {
    display: grid;
    grid-template-columns: 88px minmax(0, 1fr);
    row-gap: 8px;
    column-gap: 16px;
    margin: 0;
    font-size: var(--rf-fs-sm);
}

.rf-log-detail dt {
    color: var(--rf-faint);
    font-weight: 400;
}

.rf-log-detail dd {
    margin: 0;
    min-width: 0;
    overflow-wrap: anywhere;
}

@media (max-width: 575.98px) {
    .rf-log-detail {
        grid-template-columns: 1fr;
        row-gap: 2px;
    }

    .rf-log-detail dd {
        margin-bottom: 6px;
    }
}

/* 区块小标题：细淡、带字距 —— 与信息区之间用一条软分隔线隔开 */
.rf-log-sec {
    margin: 20px 0 8px;
    padding-top: 14px;
    border-top: 1px solid var(--rf-line-soft);
    font-size: var(--rf-fs-xs);
    font-weight: 600;
    letter-spacing: .08em;
    color: var(--rf-faint);
}

/* JSON / 代码块：限高滚动（参数 JSON 可以很长），等宽小字 + 淡色 */
.rf-log-json {
    margin: 0;
    padding: 10px 12px;
    background: var(--rf-surface-2);
    border: 1px solid var(--rf-line-soft);
    border-radius: var(--rf-r-sm);
    max-height: 280px;
    overflow: auto;
    font-size: var(--rf-fs-xs);
    line-height: 1.7;
    color: var(--rf-muted);
}

/* UA 原文：认不出客户端时那是唯一线索 —— 等宽 + 任意位置可断行 */
.rf-log-ua {
    margin-top: 4px;
    font-size: var(--rf-fs-xs);
    word-break: break-all;
    color: var(--rf-faint);
}

/* 授权变更两栏：同一张卡、只换左缘色带（新增绿 / 移除红），
   靠 color-mix 走主题令牌，深浅皮各自动跟随。 */
.rf-log-changes {
    padding: 8px 12px;
    background: var(--rf-surface-2);
    border: 1px solid var(--rf-line-soft);
    border-inline-start: 3px solid var(--rf-line-strong);
    border-radius: var(--rf-r-sm);
    font-size: var(--rf-fs-xs);
}

.rf-log-changes-add {
    border-inline-start-color: color-mix(in oklab, var(--tblr-success) 55%, transparent);
}

.rf-log-changes-remove {
    border-inline-start-color: color-mix(in oklab, var(--rf-danger) 55%, transparent);
}

/* ============================================================
   表单弹层（data-ui-panel：抽屉 / 居中弹窗 / iframe 整页）
   ============================================================ */

/* 弹层本体可聚焦（panel.js 给根元素设了 tabindex="-1"，Bootstrap 打开时会把
   焦点交给它 —— 弹层里没有表单字段时它就一直攥着焦点）。焦点环与插入符都
   不该画在这一层：

   · 焦点环：outline: none（与 theme.css 的 .rf-lightbox 同一处理 ——
     这层是全屏容器，环会画成围着整个视口的一圈，毫无信息量）。
   · 插入符（caret）：Chrome 开了「文本光标浏览」（chrome://settings/accessibility
     或误按 F7）后，焦点落进一个非编辑元素，浏览器就会在它**正文第一行的行首**
     画一个闪烁的插入符 —— 弹窗一开第一行就「竖着根光标」，看着像被人点了进去
     （data-confirm 弹窗同理，见 .rf-confirm-modal 的注释）。
     在弹层根上关掉，再给真正的编辑控件请回来 —— caret-color 会继承，
     不还原的话表单里的输入框也没光标了。正文选区不受影响，日志照旧可复制。 */
.rf-panel:focus,
.rf-panel:focus-visible {
    outline: none;
}

.rf-panel {
    caret-color: transparent;
}

.rf-panel input:not([type="checkbox"]):not([type="radio"]),
.rf-panel textarea {
    caret-color: auto;
}

/* 弹层骨架是 header + body + footer 三段（由 assets/ui/1.0.0/panel.js 造），
   按钮仍写在表单自己的 .rf-panel-actions 里（通用 footer 迟早变成一堆开关，
   而「保存并继续 / 多按钮 / 危险操作二次确认」各家要的都不一样）——
   打开时 panel.js 把这条操作条**搬进 footer**，于是头尾固定、只有中间 body 滚。

   表单撑满内容区是给「操作条还在 body 里」那种形态兜底的（短表单时按钮贴底），
   搬进 footer 之后不再依赖它，留着也无害：表单仍是 body 里的唯一子元素。

   ⚠️ 远程内容摘的是**整页的正文**（panel.js 的 extractContent），外面通常裹着
   页面版的 `.container-fluid` —— 那一层也一起写上，否则这条兜底对远程形态静默
   失效（表单成了 body 的孙子，不再命中 `> form`）。 */
.rf-panel-body > form,
.rf-panel-body > .container-fluid > form,
.rf-panel-body > .container > form {
    display: flex;
    flex-direction: column;
    min-height: 100%;
}

/* 页面版的容器内边距在弹层里是重复的：整页正文自带 container-fluid py-4，
   而弹层 body（.modal-body / .offcanvas-body）自己就给内边距 —— 两套叠起来
   比原来的片段形态松一圈。这里把容器那份收掉，两种形态观感一致。 */
.rf-panel-body > .container-fluid,
.rf-panel-body > .container {
    padding: 0;
}

/* 底栏（footer）：弹窗那份额外带 Bootstrap 的 .modal-footer（拿它的内边距），
   抽屉没有对应的结构类，等价编排由本规则补齐 —— 两种呈现方式观感一致。
   flex-shrink: 0 是关键：body 内容变长时它不能被压扁，否则按钮会变形。 */
.rf-panel-footer {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-start;
    gap: 8px;
    flex-shrink: 0;
    padding: 12px 16px;
    border-top: 1px solid var(--rf-line-soft);
}

/* 没有操作条时（只读内容弹层）底栏不该留一条空边框 —— panel.js 用 hidden 摘掉它。
   ⚠️ 上面写了 display:flex，会盖掉 [hidden] 自带的 display:none，必须补这一条。 */
.rf-panel-footer[hidden] {
    display: none;
}

/* 操作条：搬进底栏后就是底栏本身的内容，它自己那条分隔线与上边距是重复的。
   按钮组本身不在这里的 —— 「取消」这种要 data-bs-dismiss="panel"
   （panel.js 打开时按呈现方式改写成 modal / offcanvas）。

   排布（全站统一）：**取消在左、提交在右，整体居中**。这是 CSS 与 DOM 的
   分工 —— 顺序由 DOM 写定（取消在前），这里只负责对齐。⚠️ 别用 order/flex-row-reverse
   去翻顺序：Tab 焦点与读屏顺序跟着 DOM 走，视觉倒过来就与键盘顺序脱节了。 */
.rf-panel-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 8px;
    margin-top: auto;
    padding-top: 16px;
    border-top: 1px solid var(--rf-line-soft);
}

.rf-panel-footer > .rf-panel-actions {
    /* 底栏是 flex 容器，操作条作为子项默认只占内容宽 —— 撑满后它自己的
       justify-content 才有余量把按钮居中（换行时也是居中的） */
    flex: 1 1 auto;
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
}

/* 抽屉默认 400px（--tblr-offcanvas-width）装不下一行两列的表单，wide 给 560px。
   改的是 Tabler 自己的宽度令牌 —— .offcanvas-end 那条 width 规则照常生效，
   响应式与 RTL 逻辑也一并继承（不去覆盖 width）。 */
.rf-panel.rf-panel-wide {
    --tblr-offcanvas-width: 560px;
}

/* ── 高度档（data-ui-height）───────────────────────────────────────────────
   触发器上是**三个独立的轴**：data-ui-panel 定承载与位置，data-ui-size 定宽度，
   data-ui-height 定高度。后两者在打开那一刻由 panel.js 的 applySize 落成类名：
     宽度  modal / iframe → .modal-dialog 上的 modal-sm/lg/xl 或 rf-panel-frame-sm/lg
           抽屉         → 根上的 rf-panel-wide
     高度  modal / iframe → .modal-dialog 上的 rf-panel-h-*
           抽屉         → 根上的 rf-panel-h-*（只吃这个变量的是上/下抽屉）

   ⚠️ **不写 data-ui-height 就是 auto**（随内容）：定值高度对表单是猜的 ——
   三个单行输入的「个人信息」也要 680 高，下半屏全是空白。弹窗靠 Tabler 的
   scrollable 自己就随内容；iframe 由 panel.js 的 fitFrame 量出像素高（它 body
   里是个 absolute 的框，没有自己长高这回事）。

   五个值只在这一个地方定义，档位类只负责把它交给 --rf-panel-h；
   没有 --rf-panel-h 就没有任何覆盖（= auto 档 = Tabler 原样）。
       sm   min(360px, 60vh)    确认框、一句话提示
       md   min(520px, 76vh)    中等表单
       lg   min(680px, 86vh)    长表单
       xl   min(840px, 92vh)    很长的表单
       full calc(100vh - 32px)  近满屏 —— 但**只有抽屉吃这个定值**：modal / iframe
                                的 full 是「视口减去 dialog 自己的上下 margin」，
                                否则底部会贴住（见下面那条覆盖规则）

   ⚠️ 用 min(px, vh) 而不是纯 vh：1440 高的屏上 88vh 是 1267px，一个 1080 宽的
      框配 1267 高很怪；纯 px 又会在 768 高的笔记本上顶出视口。
   ⚠️ 高度三条一起写：`.modal-dialog-centered` 自带 min-height、
      `.modal-dialog-scrollable` 自带 height，两条都是 calc(100% - margin*2)
      （见 tabler.min.css 的 modal-dialog-centered / modal-dialog-scrollable），
      只写 height 会被它们顶回去 —— 矮屏上看不出，高屏上就是「设了没生效」。
      反过来说，auto 档什么都不用写就有自适应 + 超高封顶滚动：dialog 恒为
      calc(100% - margin*2)，content 按内容收、封顶 100%，body 溢出滚动。
   ⚠️ auto 不落类（见 panel.js 的 resolveHeight）：弹窗靠 Tabler 那两条自己就
      随内容；iframe 那边不落类 = 交给 fitFrame 量（见下面「iframe 承载」段）。 */
.rf-panel {
    --rf-panel-h-sm: min(360px, 60vh);
    --rf-panel-h-md: min(520px, 76vh);
    --rf-panel-h-lg: min(680px, 86vh);
    --rf-panel-h-xl: min(840px, 92vh);
    --rf-panel-h-full: calc(100vh - 32px);
}

.rf-panel .modal-dialog.rf-panel-h-sm   { --rf-panel-h: var(--rf-panel-h-sm); }
.rf-panel .modal-dialog.rf-panel-h-md   { --rf-panel-h: var(--rf-panel-h-md); }
.rf-panel .modal-dialog.rf-panel-h-lg   { --rf-panel-h: var(--rf-panel-h-lg); }
.rf-panel .modal-dialog.rf-panel-h-xl   { --rf-panel-h: var(--rf-panel-h-xl); }

/* full 档在 modal / iframe 上**不吃**那个定值 —— 它是给抽屉的（offcanvas 没有
   margin，自己留 16px；左右抽屉更是天然铺满，压根不看高度档）。
   modal / iframe 的 .modal-dialog 自带上下 28px 的 margin，定值再叠上去就溢出：
   实测 855 的视口里顶 28 / 底 4，看着就是「底下贴住了」。这里换成 Tabler 自己的
   满屏口径（与 .modal-dialog-scrollable 同一条算式），上下各留下 --tblr-modal-margin
   的间距，矮屏 / 移动端跟着它自己的 media query 走（8px ↔ 28px）。 */
.rf-panel .modal-dialog.rf-panel-h-full {
    --rf-panel-h: calc(100% - var(--tblr-modal-margin, 28px) * 2);
}

.rf-panel .modal-dialog:is(.rf-panel-h-sm, .rf-panel-h-md, .rf-panel-h-lg, .rf-panel-h-xl, .rf-panel-h-full) {
    height: var(--rf-panel-h);
    min-height: var(--rf-panel-h);
    max-height: var(--rf-panel-h);
}

/* 光改 dialog 不够 —— 它是透明的那个框，Tabler 只给 .modal-content 设了
   max-height:100%（.modal-dialog-scrollable），content 仍按内容收。
   不补 height:100% 的话，档位只改了透明框的尺寸，弹窗看起来一点没变
   （实测踩过：xl + full 出来还是一条 462px 的矮内容）。 */
.rf-panel .modal-dialog:is(.rf-panel-h-sm, .rf-panel-h-md, .rf-panel-h-lg, .rf-panel-h-xl, .rf-panel-h-full) > .modal-content {
    height: 100%;
}

/* 上/下抽屉的高度。左右抽屉的高度天然满屏（.offcanvas-end 那条 width/height
   规则不吃 --tblr-offcanvas-height），所以这两个方向写 height 档是无声的 ——
   文件里写清，免得下一个人以为「高度档在抽屉上坏了」。 */
.rf-panel.rf-panel-h-sm   { --tblr-offcanvas-height: var(--rf-panel-h-sm); }
.rf-panel.rf-panel-h-md   { --tblr-offcanvas-height: var(--rf-panel-h-md); }
.rf-panel.rf-panel-h-lg   { --tblr-offcanvas-height: var(--rf-panel-h-lg); }
.rf-panel.rf-panel-h-xl   { --tblr-offcanvas-height: var(--rf-panel-h-xl); }
.rf-panel.rf-panel-h-full { --tblr-offcanvas-height: var(--rf-panel-h-full); }

/* ── iframe 承载（data-ui-panel="iframe"）──────────────────────────────────
   header 还是弹层自己的（标题 + 关闭键），往下整块交给 iframe：里面是一张
   **真页面**，它自己排版、自己滚动 —— 所以 body 不留内边距、也不参与滚动。

   footer 与 modal / drawer 共用同一份 .rf-panel-footer：iframe 里那张页面的
   .rf-panel-actions 加载完被 panel.js **克隆**进底栏（跨文档搬不了真节点，
   点击转发回原件），于是「保存 / 取消」在三种承载下落在同一位置。
   .modal-content 是 flex 列，底栏拿自然高度、body 自动让出这一段。
   没有操作条的页面（只读弹层）底栏保持 hidden。

   宽度走自己的两档（sm 560 / lg 880，缺省即最大档 1080），不复用
   modal-sm/lg/xl —— 那三档是给内容少的表单用的窄框，整页装进来需要的是
   「大而稳」的一块视口。档位由 panel.js 的 applySize 落到 .rf-panel-frame-dialog
   上（打开那一刻落，不是加载完才落 —— 否则会先闪一下最大档）。

   高度也归 applySize 管（rf-panel-h-*）。**不写档位 = 随内容**（rf-panel-frame-fit）：
   iframe 的 body 里是个 absolute 的框，没有「自己长高」这回事，所以那段高度由
   panel.js 量了写成内联像素值：打开那一刻按**骨架**的自然高度收着（fitSkeleton，
   首帧就是骨架大小，不吃这条兜底），内容到位后 fitFrame 改写成框里 #content 的
   文档坐标底边 + 弹层头尾，上限同样是视口减 margin；内容比上限还高时框里的
   页面自己滚。

   下面那条 calc 是**兜底**：JS 没跑到（或骨架量不出）时 iframe 绝对定位会 0 高，
   取近满屏至少把框撑起来。

   ⚠️ 宽度改的是 **Tabler 自己的令牌**（`.modal` 上定义的 --tblr-modal-width，
   .modal-dialog 的 max-width 取自它，缺省 540px）—— 直接写 width 会被那条
   max-width 压回去（实测写 1080px 出来还是 540px），与抽屉那边改
   --tblr-offcanvas-width 是同一套手法。 */
.rf-panel-iframe .rf-panel-frame-dialog {
    --tblr-modal-width: min(1080px, 94vw);
    /* 兜底值 = full 档的同一条算式（视口减去 dialog 自己的上下 margin），
       不是 --rf-panel-h-full 那个定值 —— 那会连带把底部顶到贴住（见高度档注释）。 */
    height: calc(100% - var(--tblr-modal-margin, 28px) * 2);
    min-height: calc(100% - var(--tblr-modal-margin, 28px) * 2);
    max-height: calc(100% - var(--tblr-modal-margin, 28px) * 2);
}

/* 自适应档：高度是 fitFrame 量出来的内联像素值，这条只管收放的动作 ——
   加载中按上面的兜底铺着骨架（近满屏），内容到位收成实际高度。没有过渡的话，
   300 多像素的落差是一帧跳过去的。定值档不吃这条：换档是「换了一个高度」，
   不该像量出来那样滑过去。 */
.rf-panel-iframe .rf-panel-frame-dialog.rf-panel-frame-fit {
    transition: height .18s ease;
}

@media (prefers-reduced-motion: reduce) {
    .rf-panel-iframe .rf-panel-frame-dialog.rf-panel-frame-fit {
        transition: none;
    }
}

.rf-panel-iframe .rf-panel-frame-dialog.rf-panel-frame-sm {
    --tblr-modal-width: min(560px, 94vw);
}

.rf-panel-iframe .rf-panel-frame-dialog.rf-panel-frame-lg {
    --tblr-modal-width: min(880px, 94vw);
}

.rf-panel-iframe .modal-content {
    height: 100%;
}

.rf-panel-iframe .rf-panel-body {
    position: relative; /* 里面那层用 absolute 撑满，不赌百分比高度的解析 */
    padding: 0;
    overflow: hidden;
}

.rf-panel-frame-box {
    position: absolute;
    inset: 0;
}

.rf-panel-frame {
    display: block;
    width: 100%;
    height: 100%;
    border: 0;
}

/* ── 表单页外壳（.rf-form-page）────────────────────────────────────────────
   页面的正文壳：渐变灰底 + 卡片是「表单浮在页面上」的两层边界，这里只管限宽与
   上下留白 —— 整页直开时正文铺满整屏既不好读、也没有边界。宽度档位只有下面
   这三档（视图别再写内联 / scoped 的 max-width 覆写 —— 那是同一口径的第二处，
   要换档就换修饰类），横向内边距仍由视图自己的容器出（`.container-fluid` 的 12px）。
   ⚠️ 被弹层装框时它由下面 .rf-panel-framed 那两条摊平 —— 装框态的 max-width:none
      两条选择器都比档位修饰类高，任何档位装框后照旧摊平。 */
.rf-form-page {
    max-width: 760px;
    margin-inline: auto;
    padding-block: 24px;
}

/* 宽档：多卡片分节（≥2 张卡）或字段 ≥ 12，或含「天然宽」的控件（上传网格、
   行编辑 multiple-input、树多选、省市区联动、地图选点）—— 760 里这些控件
   与两列栅格会被压成细条。 */
.rf-form-page-lg {
    max-width: 1080px;
}

/* 通栏：正文主体是编辑器 / 画布的页面 —— 富文本、Markdown、左写右预览这类
   写作区多宽都不嫌宽，**有编辑器一律通栏**（TinyDocs 文档表单、TinyDemo
   表单演示、TinyShop 商品编辑都是正例）。 */
.rf-form-page-full {
    max-width: none;
}

/* 被装框的那份页面**不自绘画布底**（theme.css 给 body 的渐变底）—— 弹层自己
   就是那张「底」：header / footer 用的是弹层的 surface，中间却是页面自己的
   渐变底，两者之间会露出一圈接缝（观感是「框里又嵌了一张页面」，也就是那条
   割裂感）。标记由 panel.js 在 onFrameLoad 里挂在 iframe 文档的 <html> 上，
   这里只负责摘掉底 —— iframe 默认透明，页面不画底，弹层的 surface 就透上来。
   ⚠️ 只认这个标记：整页直开时页面照旧是渐变底。 */
.rf-panel-framed body {
    background: none;
}

/* 装框的页面**也不自绘「第二层框」**：弹层（header / body / footer）本身就是那个
   框，页面里再套一张卡片就是「框里又嵌了一张页面」—— 与片段承载（modal / drawer，
   内容直接落在弹层 body 里）的观感对不上。
   所以这一态里：① 卡片摊平（外框 / 阴影 / 白底，连它那份内边距一起收 —— 框都没
   了，留着就是一圈空白）；② 限宽取消（弹层给多宽就用多宽，那 760px 是整页直开时
   的可读性约束）；③ 内边距换成弹层 body 那一档（Tabler 的 --tblr-modal-padding =
   24px）—— 于是正文与弹层头的标题同一条左基准线，就是「弹层正文」的样子。
   ⚠️ 与上一条同源：整页直开时卡片与限宽照旧，那是页面自己的边界。 */
.rf-panel-framed .rf-form-page {
    max-width: none;
    padding: 24px;
}

.rf-panel-framed .rf-form-page .card {
    background: none;
    border: 0;
    box-shadow: none;
}

.rf-panel-framed .rf-form-page .card > .card-body {
    padding: 0;
}

/* ── 没有卡片底的表单自动补一张 ───────────────────────────────────────────
   标准形态是「限宽容器 + 一张卡片」（UI.md §5.5）：卡片给表单一块白底与边界。
   但历史视图里有大批页面**直接把 form 铺在容器的灰底上**（2026-10-29 统计：
   全站 71 个表单页，光商城就 43 个），字段、提示、分隔线糊成一片 ——
   用户的原话是「底色没有」。

   与其逐页补 `<div class="card">`（71 个文件的视觉改动，还得逐页验），
   不如让**缺失不可能造成观感问题**：form 里没有 card 时，form 自己就是那张卡。
   已经写了卡片分节的页面（store / products 那些）命中不到 `:not(:has(.card))`，
   照旧是它们自己的多卡版式。

   ⚠️ **只在整页直开时补。弹层承载时一律摘掉** —— 弹层（header + body +
      footer）自己就是那层框，里面再补一张白底描边的卡，观感就是「弹层背景里又
      套了一层背景框」。弹层有**两种**承载方式，各摘各的，少写一档就正好漏在
      另一种上：
        · iframe 承载（data-ui-panel="iframe"）—— 页面在自己的 iframe 文档里，
          panel.js 给那份 `<html>` 挂 `.rf-panel-framed`（见 onFrameLoad）；
        · 片段承载（data-ui-panel="modal|drawer"）—— panel.js 摘 `#content`
          注入弹层 body（`.rf-panel-body`），没有 iframe 文档，也就永远拿不到
          `framed` 标记，只能按「内容落在弹层 body 里」这条判。
      ⚠️ 这两档删任何一档都不会报错：页面 HTTP 照样 200、CSS 照样在，只是**某一种
         承载**下多出一层框（`TinyUiFeedbackTest` ④⑦⑧⑨ 各钉一半）。
   ⚠️ `:has()` 是 2023 年起的主流能力（Chrome 105 / Safari 15.4 / Firefox 121），
      不支持时退化成「没有这层白底」—— 也就是回到今天的现状，不会更糟。 */
.rf-form-page > form:not(:has(.card)) {
    background: var(--rf-surface, #fff);
    border: 1px solid var(--rf-line, #e5e6eb);
    border-radius: var(--rf-r, 6px);
    padding: 20px;
    box-shadow: var(--rf-shadow, 0 2px 6px rgba(29, 33, 41, .05));
}

/* 弹层里摘掉这层自绘卡：两种承载（iframe / 片段）都得写 —— 理由见上一条。
   `.modal-body` / `.offcanvas-body` 一并写上，是为了把不经 panel.js 的业务弹层
   也盖住：弹层自己就是白框，里面的表单不需要第二层白底。 */
.rf-panel-framed .rf-form-page > form:not(:has(.card)),
.rf-panel-body .rf-form-page > form:not(:has(.card)),
.modal-body .rf-form-page > form:not(:has(.card)),
.offcanvas-body .rf-form-page > form:not(:has(.card)) {
    background: none;
    border: 0;
    padding: 0;
    box-shadow: none;
}

/* 片段承载的容器本身也摊平：限宽（760 / 1080）是整页直开时的可读性约束，弹层
   给多宽就该用多宽 —— 否则 modal-xl 里正文缩成中间一条，两边各留一大片空白。
   上下留白归零：弹层 body 自己给内边距（横向那份已由
   `.rf-panel-body > .container-fluid` 收掉），再叠 24px 就是一圈空气。 */
.rf-panel-body .rf-form-page,
.modal-body .rf-form-page,
.offcanvas-body .rf-form-page {
    max-width: none;
    padding-block: 0;
}

/* ── 窄栏里的表单自动堆叠 ─────────────────────────────────────────────────
   「左表单 + 右摘要」这类两栏页面，右栏往往只有 350 上下：里面的字段若跟着
   表单级 horizontal 布局走，label 列（12 栅格里的 2 格）只剩 **50px** ——
   中文标签逐字换行（实测「营业状态」拆成两行，760 档左栏更狠：4 个字挤成 3 行、
   「门店封面」8 行）。

   判据必须按**容器宽**而不是视口宽：视口 1440 时右栏只有 357px，任何媒体查询
   都命不中，而它确实窄。所以给卡片体建容器，窄了就堆叠（label 在上、控件通栏）。
   ⚠️ `container-type` 会建立包含块：这里只挂在 `.card-body` 上，卡片体内没有
      fixed 定位的东西（吸底条在 form 末尾，不在卡内），不影响版式。
   ⚠️ 堆叠后 label 必须改成左对齐：横排时是右对齐贴控件，堆叠后继续右对齐会
      在左边留一道空气。 */
.rf-form-page .card-body {
    container-type: inline-size;
}

@container (max-width: 480px) {
    .rf-form-page .row > label.col-form-label {
        flex: 0 0 100%;
        max-width: 100%;
        text-align: left;
        padding-bottom: 2px;
    }

    /* 控件列跟着通栏（col-sm-10 / col-sm-8 / 省市区那种 col-sm-4 三连都算） */
    .rf-form-page .row > [class*="col-sm-"]:not(.col-form-label) {
        flex: 0 0 100%;
        max-width: 100%;
    }
}

/* 首帧之前 iframe 里是一张空白文档，直接露出来就是「白板」——
   盖一层加载中，页面真到了（panel.js 打上 -ready）再撤掉。
   iframe 隐藏走 visibility 而不是 display：导航照跑，只是人看不见中间态。 */
/* iframe 画好之前铺的骨架层（形状见 panel.js 的 .rf-panel-skel）。
   纵向排：骨架占上方、状态文字跟在后面。
   内边距与 modal-body 那一档对齐 —— iframe 承载的 .rf-panel-body 把 padding
   归零了（滚的是 iframe 里那张页），这里得自己给，否则骨架贴着弹层边。 */
.rf-panel-frame-loading {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    padding: 1rem;
    overflow: hidden;
}

.rf-panel-iframe.rf-panel-frame-ready .rf-panel-frame-loading {
    display: none;
}

.rf-panel-iframe:not(.rf-panel-frame-ready) .rf-panel-frame {
    visibility: hidden;
}

/* 「正在保存…」档：提交那一刻起盖到收尾为止 —— 不让列表页在框里闪一遍
   （它比表单页重得多，看得见的中间态就是「先跳到首页再被关掉」）。
   底栏同时点不动：这几百毫秒里 iframe 正在导航，点击本来就发不出去，
   能点只会让人以为「点到取消了」或「又提交了一次」。 */
.rf-panel-iframe.rf-panel-frame-busy .rf-panel-footer {
    opacity: .55;
    pointer-events: none;
}

/* ============================================================
   独立页（登录页 / 错误页）
   ============================================================ */

/* 没有外壳可依的整页 —— 骨架见 layouts/plain.blade.php，
   用在登录页与 401/403/404/419/429/500/503 七张错误页上。

   与外壳页的差别其实只有「居中一张卡」：皮肤完全复用 ——
   底色取 theme.css 的 --rf-page（**纯色**页面底，无渐变），
   圆角 / 阴影 / 字号 / 文字色全部走令牌。所以明暗切换与主色切换自动跟随，
   这里不写死任何色值（与后端其余部分的纪律一致）。

   ⚠️ 这一段只做「结构与排版」，配色一律引令牌；要换观感改 theme.css。

   ⚠️ 范围分界：本节只管两种页面**共用**的骨架（居中、卡宽、页脚、错误卡）。
      登录页自己那一套观感（底纹 + 双栏卡 + 品牌面板）不在这里 —— 它跟着登录视图
      单独加载，见 assets/auth/1.0.0/login.css。改登录页别在本文件里找。 */

/* 整页居中：grid + place-items 一行收口。
   不用 flex + margin:auto —— 内容比视口高时（长错误说明 + 手机横屏）
   grid 的 place-items 仍能正确从顶部开始并允许滚动，flex 居中会切掉顶部。 */
.rf-plain-body {
    display: grid;
    place-items: center;
    min-height: 100vh;
    padding: 32px 16px;
    background: var(--rf-page);
}

.rf-plain {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 20px;
    width: 100%;
    /* 登录卡与错误卡共用一个宽度：两张卡的观感才是同一套。
       480px ≈ 480px —— 够放「两次输入不一致」这类长错误提示，又不至于太空。 */
    max-width: 480px;
}

.rf-plain-box {
    width: 100%;
}

.rf-plain-foot {
    margin: 0;
    font-size: var(--rf-fs-xs);
    color: var(--rf-faint);
}

/* 登录页的整段观感（.rf-auth-*）已搬到 assets/auth/1.0.0/login.css，由登录视图
   单独 @push 引入 —— 搜这些选择器请去那边，这里不留副本。为什么请出去：见那个
   文件开头的说明。 */

/* ---- 错误卡：图标 / 状态码 / 结论 / 说明 / 按钮组 ---- */

/* 图标兜在淡主色的圆形底里 —— 用 Tabler 自己的主色淡底变量
   （--tblr-primary-lt 就是 color-mix 出来的），不自己拼色值。 */
.rf-error-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    margin-bottom: 14px;
    border-radius: var(--rf-r-pill);
    font-size: var(--rf-fs-metric);
    line-height: 1;
    color: var(--tblr-primary);
    background: var(--tblr-primary-lt);
}

/* 大号状态码：唯一的「展示档」字号（--rf-fs-display，见 theme.css 的档位说明）。
   它是这一页的视觉主体，比下面的标题大一倍多 —— 一眼就知道撞的是哪一号错误。 */
.rf-error-code {
    font-size: var(--rf-fs-display);
    font-weight: 700;
    line-height: 1;
    letter-spacing: .02em;
    color: var(--rf-ink);
}

.rf-error-title {
    margin: 10px 0 0;
    font-size: var(--rf-fs-h2);
    font-weight: 600;
    color: var(--rf-ink);
}

.rf-error-message {
    margin-top: 12px;
    font-size: var(--rf-fs-body);
    color: var(--rf-muted);
}

/* 次级提示（「怎么办」的具体建议）：做成一块浅底小面板，
   与正文说明拉开层次 —— 用户跳过它也能看懂结论，需要时才细看。 */
.rf-error-hint {
    max-width: 384px;
    margin: 14px auto 0;
    padding: 10px 14px;
    border: 1px solid var(--rf-line-soft);
    border-radius: var(--rf-r-sm);
    background: var(--rf-surface-2);
    font-size: var(--rf-fs-sm);
    line-height: 1.6;
    color: var(--rf-muted);
    text-align: start;
}

.rf-error-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px;
    margin-top: 24px;
}

/* 手机端：卡片贴边、按钮占满一行（两个按钮各占一半太长，
   竖排更好点）。 */
@media (max-width: 575.98px) {
    .rf-plain-body {
        padding: 20px 14px;
    }

    .rf-error-actions .btn {
        flex: 1 1 100%;
    }
}

/* ==========================================================================
   行内数值输入框（App\Support\Html::ajaxEditableInput() 输出）

   表格单元格里的紧凑数字框：宽度压到 88px、数字等宽对齐（tabular-nums），
   居中显示 —— 排序值是一列数字，左对齐看着会参差不齐。

   ⚠️ 那个 width 在 ajaxEditableInput() 里以内联 style 输出（组件要在没有本 CSS 的
      独立页面上也能用），这里只管「表格内」的观感微调，不重复设宽。
   ========================================================================== */
.rf-sort-input {
    text-align: center;
    font-variant-numeric: tabular-nums;
}

/* 表格里禁掉边框，让它看起来像「可编辑的文本」而不是一个表单控件 ——
   鼠标悬停 / 聚焦时才现形，减少满屏边框的噪音。 */
.rf-sort-input:not(:focus):not(:hover) {
    border-color: transparent;
    background: transparent;
}

.rf-sort-input:focus {
    border-color: var(--rf-accent);
}

/* ==========================================================================
   表单错误汇总条（.rf-form-errors）

   提交失败整页回显时摆在表单顶部，视图 resources/views/components/ui/
   form-error-summary.blade.php 负责结构（图标 / 标题 / 锚点列表），这里只管
   观感。

   旧版直接套 .alert-danger：整块粉底 + 红字 + 粗黑 h4 + 浏览器圆点列表，
   四层强调叠在一起，在窄卡（登录 / 入驻这类单栏表单）里比它提醒的内容还重。

   现在按「轻提示」重做，强度只留两处：
     · 一条 2px 的左竖条（唯一的「重」元素，扫一眼就知道有错）；
     · 字段名用危险色、可点（锚到对应控件），消息仍用正文色 —— 名字是索引，
       不是第二层警告。
   底只给 4% 的红、字号降一档、标题不再是 h4：有颜色，但不是色块。

   ⚠️ 配色全部引令牌（color-mix 只做透明度混合，不写死色值）—— 明暗两套皮
      与主色切换自动跟随。间距只给「下方 1rem」，与旧版 .alert 的默认外边距
      一致，免得表单首个字段贴上来的观感在改版后跳一下。
   ========================================================================== */
.rf-form-errors {
    display: grid;
    gap: 6px;
    margin-bottom: 1rem;
    padding: 11px 14px 12px 13px;
    border: 1px solid color-mix(in oklab, var(--rf-danger) 22%, var(--rf-line));
    border-left: 2px solid var(--rf-danger);
    border-radius: var(--rf-r-sm);
    background: color-mix(in oklab, var(--rf-danger) 4%, var(--rf-surface));
    font-size: var(--rf-fs-xs);
    line-height: 1.65;
    text-align: left;
}

.rf-form-errors-head {
    display: flex;
    /* 标题换行时图标留在首行左肩，而不是浮到两行中间 */
    align-items: flex-start;
    gap: 6px;
    margin: 0;
    font-size: var(--rf-fs-sm);
    font-weight: 600;
    line-height: 1.4;
    /* 标题用正文色：红留给左侧竖条、图标与字段名。整块全红的话，红色就不再
       是「有错」的信号，而只是这块的皮肤。 */
    color: var(--rf-ink);
}

/* 图标比标题字号略大一档即可 —— 再大就回到「告示牌」观感。
   盒子按 glyph 紧贴（line-height:1），再用 2px 上边距把它的中线压到首行
   文字（13px × 1.4 ≈ 18px 高）的中间。 */
.rf-form-errors-head .rfi {
    margin-top: 2px;
    font-size: 15px;
    line-height: 1;
    color: var(--rf-danger);
}

.rf-form-errors-list {
    margin: 0;
    padding: 0;
    list-style: none;
    color: var(--rf-ink);
}

/* 小圆点用 ::before 画：浏览器默认的 disc 太大，缩到 3px 才配这档字号 */
.rf-form-errors-list li {
    position: relative;
    padding-left: 12px;
}

.rf-form-errors-list li + li {
    margin-top: 2px;
}

.rf-form-errors-list li::before {
    content: '';
    position: absolute;
    top: .68em;
    left: 2px;
    width: 3px;
    height: 3px;
    border-radius: 50%;
    background: color-mix(in oklab, var(--rf-danger) 55%, transparent);
}

/* 锚点链接保持「可以点」的暗示，但别做成第二层警告：危险色 + 细虚线底边 */
.rf-form-errors-list a {
    color: var(--rf-danger);
    font-weight: 500;
    text-decoration: none;
    border-bottom: 1px dashed color-mix(in oklab, var(--rf-danger) 45%, transparent);
}

.rf-form-errors-list a:hover {
    border-bottom-style: solid;
}

/* 暗色皮：4% 的红混在深底上几乎看不出，提到 10% 才有「这块是提示」的底色 */
[data-bs-theme="dark"] .rf-form-errors {
    background: color-mix(in oklab, var(--rf-danger) 10%, var(--rf-surface));
}

/* ============================================================================
   菜单搜索（顶栏入口 + 浮层）
   ============================================================================
   浮层挂在 <body> 上而不是顶栏里 —— 顶栏右侧是 overflow 容器，面板会被裁掉。
   定位 fixed + 顶部 12vh：视线落点比正中更靠上，输入时不会挡住结果。

   z-index 取 1050：要盖住顶栏、标签栏与 iframe，但不能高过 Tabler 的 modal
   （bootstrap 里是 1055），否则「搜索开着时弹出的确认框」会被压在下面。 */

.rf-cmd[hidden] {
    display: none;
}

.rf-cmd {
    position: fixed;
    inset: 0;
    z-index: 1050;
    display: flex;
    justify-content: center;
    align-items: flex-start;
    padding: 12vh 16px 16px;
}

.rf-cmd-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, .32);
}

.rf-cmd-panel {
    position: relative;
    display: flex;
    flex-direction: column;
    width: min(560px, 100%);
    max-height: min(60vh, 520px);
    overflow: hidden;
    background: var(--tblr-bg-surface, #fff);
    border: 1px solid var(--tblr-border-color);
    border-radius: 12px;
    box-shadow: 0 12px 32px rgba(0, 0, 0, .18);
}

.rf-cmd-field {
    display: flex;
    flex: none;
    align-items: center;
    gap: 8px;
    padding: 10px 14px;
    border-bottom: 1px solid var(--tblr-border-color);
}

.rf-cmd-field-icon {
    flex: none;
    color: var(--tblr-secondary, #6c7a91);
}

.rf-cmd-input {
    flex: 1;
    min-width: 0;
    padding: 0;
    border: 0;
    outline: 0;
    background: transparent;
    color: var(--tblr-body-color);
    font-size: 14px;
    line-height: 1.6;
}

.rf-cmd-kbd {
    flex: none;
    padding: 1px 6px;
    border: 1px solid var(--tblr-border-color);
    border-radius: 4px;
    background: transparent;
    color: var(--tblr-secondary, #6c7a91);
    font-size: 11px;
}

.rf-cmd-list {
    margin: 0;
    padding: 6px;
    overflow-y: auto;
    list-style: none;
}

/* 一行一个页面：标题在左，完整路径贴右 —— 路径是「它在哪」，
   没它的话搜到的结果点进去仍然不知道这一页归谁管 */
.rf-cmd-item {
    position: relative;
    display: flex;
    align-items: baseline;
    gap: 10px;
    padding: 10px 12px;
    border-radius: var(--rf-r-sm);
    cursor: pointer;
}

.rf-cmd-item:hover {
    background: color-mix(in oklab, var(--tblr-primary) 5%, transparent);
}

/* 选中态：只有底色。之前加过左侧主色竖条，去掉 —— 底色本身就够分辨，
   多一根竖条反而像「这一行被标记了」，不是「焦点在这里」。 */
.rf-cmd-item.is-active {
    background: color-mix(in oklab, var(--tblr-primary) 14%, transparent);
}

.rf-cmd-item-title {
    overflow: hidden;
    color: var(--tblr-body-color);
    font-size: 14px;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.rf-cmd-item-path {
    flex: none;
    margin-left: auto;
    color: var(--tblr-secondary, #6c7a91);
    font-size: 12px;
    white-space: nowrap;
}

.rf-cmd-empty {
    margin: 0;
    padding: 24px 16px;
    color: var(--tblr-secondary, #6c7a91);
    font-size: 13px;
    text-align: center;
}

/* 底部操作条：键盘用法写在界面上，比让人猜「能不能回车」更省事 */
.rf-cmd-foot {
    display: flex;
    flex: none;
    gap: 14px;
    align-items: center;
    padding: 8px 14px;
    border-top: 1px solid var(--tblr-border-color);
    color: var(--tblr-secondary, #6c7a91);
    font-size: 12px;
}

.rf-cmd-foot kbd {
    display: inline-block;
    min-width: 16px;
    margin-right: 3px;
    padding: 1px 5px;
    border: 1px solid var(--tblr-border-color);
    border-radius: 4px;
    font-family: inherit;
    font-size: 11px;
    line-height: 1.4;
    text-align: center;
}

/* 结果数贴右：它答的是「列出来的是全部吗」 */
.rf-cmd-count {
    margin-left: auto;
}

/* placeholder 别用浏览器默认的近黑色，否则看着像已经填了内容 */
.rf-cmd-input::placeholder {
    color: var(--tblr-secondary, #6c7a91);
    opacity: .75;
}

/* ============================================================================
   任务进度面板（安装 / 更新 / 卸载的过程）
   ============================================================================
   见 App\Addons\AddonTask 与 docs/frontend/components/Behaviors.md 八：后台那
   几个动作是同步请求，页面在等待期间靠轮询把执行体写下的阶段画出来。

   圆点吃 currentColor，颜色全部由行上的 .text-* 语义类决定 —— 深浅主题自动
   跟随，不给暗色皮再补一套写死的色值。running 的圆点呼吸，是这块面板唯一的
   动效：它回答「现在卡在哪儿」。

   .rf-task-spin 与 system-info 里「升级进度」的旋转图标同观感，但**不复用**
   那边的类：那个类混在升级页的内联 <style> 里，跨页复用会变成「改一处、坏另一处」。 */

/* 步骤列表**不限高**：正常一个动作 8～12 步，限高反而会把最后一行切一半
   （看着像渲染坏了）。真出现长列表时页面自己会滚，面板不需要再套一层滚动区。 */

.rf-task-steps .rf-task-dot {
    flex: none;
    width: 6px;
    height: 6px;
    margin-top: .45rem;
    border-radius: 50%;
    background: currentColor;
    opacity: .4;
}

.rf-task-steps [data-step-status="running"] .rf-task-dot {
    opacity: 1;
    animation: rf-task-pulse 1.4s ease-in-out infinite;
}

.rf-task-steps [data-step-status="ok"] .rf-task-dot,
.rf-task-steps [data-step-status="warn"] .rf-task-dot {
    opacity: 1;
}

/* 耗时贴右、定宽：数字不跳，一眼能扫出「哪一步最费时间」 */
.rf-task-steps .rf-task-ms {
    flex: none;
    min-width: 56px;
    text-align: right;
}

.rf-task-spin {
    display: inline-flex;
    align-items: center;
    vertical-align: -1px;
}

.rf-task-spin > i {
    animation: rf-task-spin 1s linear infinite;
}

@keyframes rf-task-spin {
    to { transform: rotate(360deg); }
}

@keyframes rf-task-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: .25; }
}

/* ── 图表骨架（x-ui.chart 的报表模式） ─────────────────────────────────────
   报表页首屏挂着好几张图，取数排队期间只显示一行「加载中…」会像页面没反应。
   骨架是纯装饰（aria-hidden），状态文案与 aria-busy 仍在 [data-ui-chart-status]
   上；数据一到，rageframe.js 直接把它从 DOM 里摘掉（不靠动画隐藏 —— 留着空块
   会挡住 ECharts 的鼠标事件）。 */
.rf-chart-skeleton {
    position: absolute;
    inset: 12px 8px 28px;
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 8px 4px;
    pointer-events: none;
}

.rf-chart-skeleton > span {
    display: block;
    flex: 1;
    border-radius: 4px;
    background: linear-gradient(90deg,
        var(--bs-secondary-bg, #f1f3f5) 25%,
        var(--bs-tertiary-bg, #e9ecef) 37%,
        var(--bs-secondary-bg, #f1f3f5) 63%);
    background-size: 400% 100%;
    animation: rf-chart-shimmer 1.4s ease-in-out infinite;
}

/* 四根柱子高度递减：看着像一张正在成形的图，而不是四条一样的灰条 */
.rf-chart-skeleton > span:nth-child(2) { flex: 2; }
.rf-chart-skeleton > span:nth-child(3) { flex: 3; }
.rf-chart-skeleton > span:nth-child(4) { flex: 2.5; }

@keyframes rf-chart-shimmer {
    0%   { background-position: 100% 50%; }
    100% { background-position: 0 50%; }
}

@media (prefers-reduced-motion: reduce) {
    .rf-chart-skeleton > span { animation: none; }
}

/* 指标行（x-ui.chart 的 meta）：端点回 data.meta 才有内容。
   没有内容时整块不占位 —— 空 div 配上 mb-2 会在图上方凭空多出一条 8px 的缝。 */
.rf-chart-meta:empty {
    display: none;
}

.rf-chart-meta-item {
    white-space: nowrap;
}
