☰

Southplus Plus

南+(South Plus)论坛美化插件:把首页 / 版块页 / 帖子页清爽化,并附带一批阅读与发帖增强;设置入口在 Userscript 菜单的「⚙ 脚本设置」面板。

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Вам потребуется установить расширение, например Tampermonkey или Violentmonkey, чтобы установить этот скрипт.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         Southplus Plus
// @namespace    https://blue-plus.net/
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjggMTI4IiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSLljZcrIOaYn+WNsCI+PHRpdGxlPuWNlysg5pif5Y2w77yIc2VhbO+8iTwvdGl0bGU+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJhLWJnIiB4MT0iMCIgeTE9IjAiIHgyPSIwLjkiIHkyPSIxIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMxNDIyMzgiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMwQTExMjAiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iYS1zdGFyIiB4MT0iMCIgeTE9IjAiIHgyPSIwLjQiIHkyPSIxIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM4RkRDQ0UiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMyRjZGNjgiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48cmVjdCB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCIgcng9IjMwIiBmaWxsPSJ1cmwoI2EtYmcpIi8+PGNpcmNsZSBjeD0iNjQiIGN5PSI2NCIgcj0iNDYiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzhGRENDRSIgc3Ryb2tlLW9wYWNpdHk9IjAuMTYiIHN0cm9rZS13aWR0aD0iMS41Ii8+PGNpcmNsZSBjeD0iNjQiIGN5PSI2NCIgcj0iMzciIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzhGRENDRSIgc3Ryb2tlLW9wYWNpdHk9IjAuMDkiIHN0cm9rZS13aWR0aD0iMSIvPjxwYXRoIGQ9Ik02NCAyMCBMNzMuNSA1NC41IEwxMDggNjQgTDczLjUgNzMuNSBMNjQgMTA4IEw1NC41IDczLjUgTDIwIDY0IEw1NC41IDU0LjUgWiIgZmlsbD0idXJsKCNhLXN0YXIpIi8+PHBhdGggZD0iTTY0IDY0IEw3My41IDczLjUgTDY0IDEwOCBMNTQuNSA3My41IFoiIGZpbGw9IiNFMDhBNUYiLz48ZyBmaWxsPSIjRUFGNEZGIj48Y2lyY2xlIGN4PSIzMSIgY3k9IjM1IiByPSIyLjEiIG9wYWNpdHk9IjAuOTUiLz48Y2lyY2xlIGN4PSI5OSIgY3k9IjQxIiByPSIxLjUiIG9wYWNpdHk9IjAuNyIvPjxjaXJjbGUgY3g9Ijk1IiBjeT0iOTciIHI9IjIuMiIgb3BhY2l0eT0iMC44NSIvPjxjaXJjbGUgY3g9IjMzIiBjeT0iOTQiIHI9IjEuMyIgb3BhY2l0eT0iMC41NSIvPjwvZz48L3N2Zz4=
// @version      2.55.35
// @description  南+(South Plus)论坛美化插件:把首页 / 版块页 / 帖子页清爽化,并附带一批阅读与发帖增强;设置入口在 Userscript 菜单的「⚙ 脚本设置」面板。
// @author       Auto
// @match        *://*.blue-plus.net/*
// @match        *://*.south-plus.net/*
// @match        *://*.south-plus.org/*
// @match        *://*.north-plus.net/*
// @match        *://*.east-plus.net/*
// @match        *://*.white-plus.net/*
// @match        *://*.level-plus.net/*
// @match        *://*.spring-plus.net/*
// @match        *://*.summer-plus.net/*
// @match        *://*.snow-plus.net/*
// @match        *://*.soul-plus.net/*
// @match        *://*.imoutolove.me/*
// @run-at       document-start
// @grant        GM_addStyle
// @grant        unsafeWindow
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @license      GPL-3.0
// ==/UserScript==

(function () {
    'use strict';

    // ==================== 运行版本标记(2.55.35)====================
    // ── 2.55.35 用户任务 8/9/10:黑夜模式下「引用块 / 已购信息 / 付费提示」看不清 ──
    //   三个测试页(用户给的):引用 tid-2978425、已购信息 tid-2976788、付费提示
    //   tid-1385600(用户允许花 SP 实测)。三问一条根因。
    //   根因(活体逐条反查,不是猜):第 1 节那条扫白把帖内 background-color 全压成
    //   transparent,而它是 (1,5,3);READ_CSS 里付费模块那批规则只有 (0,1,2)~(0,3,2),
    //   带 !important 的两条先比 id 数 ⇒ 一条都赢不了:
    //     ① .spp-buy__bar 的底色被扫掉,只剩白渐变 background-image:黑夜下是一条 926×41
    //        的近白横条(真机读数 rgb(251,254,252)→rgb(243,250,246))。
    //     ② blockquote.spp-buy__body(提取码 / 被解锁正文)剩 #3f4a5a 深蓝灰压黑玻璃 ≈1.9:1。
    //     ③ blockquote.spp-buy__warn 剩 #8a6512 褐字压黑底 ≈2.4:1。
    //     ④ 站内引用块 blockquote.blockquote3(内层 h6.quote2「引用」+ div)剩站点写死的
    //        #555 ≈2.2:1 —— 这就是任务 8 截图里那团看不清的字。
    //   治法:新增 CSS_READ_DARK 第 9 节「付费模块 + 站内引用块:深色托盘」。选择器前缀与
    //   那条扫白同源(三枚 .spp-rdark + 同一个 div.t5.t2 路径),尾部多带一到两枚类 ⇒
    //   类数多一档就稳赢,不依赖 <style> 插入顺序。
    //   · 站内引用块的原始标记是抓 tid=2142330 的**原始 HTML**(fetch,脚本不参与)拿到的:
    //     <blockquote class="blockquote3"><h6 class="quote2">引用</h6><div>…</div></blockquote>;
    //     白昼原样 = #f8f8f8 底 + 4px #ccc 左边线 + 12px #555 字。
    //   · 顺序有意义:通用 blockquote 那组必须排在付费模块各条之前 —— 付费正文框与警告条
    //     身上也带 .blockquote,同特异性时靠后写的赢。
    //   · 全部挂在 html.spp-rdark 下:白昼一个字符都没动。
    //   · 未跑守卫、未改任何 md(用户禁令);反引号 334。
    //    // ==================== 运行版本标记(2.55.34)====================
    // ── 2.55.34 用户第二张截图:「黑夜模式下有很多线条、原始UI按钮被白色线条包裹不美观」──
    //   量法:遍历回贴页 #main 全部元素,逐边读 border-*-width / border-*-color,按 WCAG
    //   相对亮度筛 > 0.35 的「亮线」,再拿 el.matches() 全表反查是谁给的(真机 2.55.33)。
    //   共 59 条命中,扣掉本项目自己的 rgba(255,255,255,.07~.14) 还剩三组,全在回贴页:
    //   ① 43 枚工具栏精灵按钮:站点 .editor-button span{border:1px solid #fff}。
    //      ★ 2.53.x 那批规则写的是 #editor-tool > span —— 工具图标那一排根本不在 #editor-tool
    //        里:#editor-tool 只有 4 个子元素(字数检查那四枚链接),实测 kids=4;
    //        真正的 DOM 是 #editor-content > #editor-button > div > span.editor-button > span。
    //   ② 选项表 .tr3 td/.tr3 th 的 border-bottom:1px solid #eee(9 条)+ th.h 的 #bbb(1 条)。
    //   ③ 拖拽手柄 img(resize_0/1.gif)的 1px #ddd。
    //   顺带同源的一件事:站点在这两处容器上写着 color-scheme:light !important,
    //   白底页是对的,但这一页在黑夜模式下是黑玻璃 ⇒ 复选框画成白方块、文件按钮画成白胶囊、
    //   滚动条也是浅的。改 color-scheme 即可(不动 DOM,键盘与读屏行为不受影响)。
    //   · 精灵笔画是黑的(取样:透明 31571 / 黑 687 / 白 270 / 灰三档),撤框后必须
    //     invert(1) hue-rotate(180deg) 才看得见;两版截图对照过彩色与单色两类图标。
    //   · 全部挂在 html.spp-rdark 下:白昼一个字符都没动。
    //   · 收尾读数(同一套扫描,改完复测):回贴页亮线 59 → 0 条。
    //   · 未跑守卫、未改任何 md(用户禁令);反引号 334。
    //    // ==================== 运行版本标记(2.55.33)====================
    // ── 2.55.33 本轮第三件事:2.55.32 装进真浏览器后按「读数 + 对比度审计」抓出来的四条补丁 ──
    //   量法:逐元素对比度审计(背景按 alpha 逐层合成到 #000,取 WCAG 相对亮度比),
    //   只看两个被罩住的区域 / 回贴页整页;再配一遍「卡内还有哪些浅底」的普查。
    //   ① 表情栏那两枚 UI 的底**没生效**:2.55.31 换规则时漏删了同名的宽选择器旧规则
    //      (多一个 div.t5 与 :has ⇒ 比新规则更具体),实测 2.55.32 的胶囊底仍是
    //      rgb(56,56,58)(= 6% 白压 #2c2c2e),与面板本体只差 12 档。已删,只留记载。
    //   ② 编辑器顶栏那四枚链接(字数检查 / 恢复数据 / 存为草稿 / 草稿箱)只写了
    //      -webkit-text-fill-color ⇒ 画出来是浅的、**计算色仍是黑**(审计报 1.85:1)。补 color。
    //   ③ #editor-tab 当前那枚页签是「实心 #3fbfa8 + 白字」= 2.28:1 —— 整页审计里只剩它一条
    //      没过线。改成淡青底 + 亮字 + 青光(与表情栏勾选态同一族)。
    //   ④ 标题输入框外面那层白壳(站点 div.fl.input.f_one,542×24)不在控件名单里,
    //      深色卡上留了一条白带。按 :has(> input[name="atc_title"]) 认它。
    //   · 收尾读数(同一套审计,改完复测):回贴页与帖子页两个区域**各 0 条**低于 3:1。
    //   · 未跑守卫、未改任何 md(用户禁令);反引号 334。
    //    // ==================== 运行版本标记(2.55.32)====================
    // ── 2.55.32 本轮第二件事:同一批改动的第二次落盘,修 2.55.31 的一条选择器错误 ──
    //   · 2.55.31 装进真浏览器实测时量出来的:回贴页那张表单卡**没变黑** ——
    //     底仍是星空给的 rgba(250,252,255,.93)。原因是我把选择器写成了
    //     #main > .t.f_one(直接子元素),而这一页的实际结构是
    //       #main > div > FORM > div.t.f_one
    //     那条规则一个元素都没命中(同一批里的骨架文字 / 控件 / 编辑器那几条用的是同一个
    //     前缀,所以也一起没生效)。
    //   · 改成后代式 #main .t.f_one —— 与 POST_CSS 里既有的 html.spp-post-on .t.f_one
    //     同一写法;特异性 (1,4,1) 仍高于星空那条 (1,2,1),不需要再加东西。
    //   · 为什么另开一个版本号而不是原地重装:篡改猴对**同版本重装**会弹
    //     「即将重置所有脚本的设置!」—— 那会把用户面板里的设置清回默认,
    //     所以照旧走「升版本 + 更新」这条路(设置一个都不动)。
    //   · 教训(下一轮还会遇到):CSS 选择器里的 > 只能靠实站 DOM 量,不能靠印象 ——
    //     同一页在两次快照之间 DOM 深度都会变(上一次探针用的是带兜底的 #main .t,
    //     它命中了我以为的那个节点,于是把直接子元素这个假设漏了过去)。
    //   · 2.55.31 段头里那七件事全部仍然有效,只有上面这一条被本版修掉。
    //   · 未跑守卫、未改任何 md(用户禁令);反引号 334。
    //    // ==================== 运行版本标记(2.55.31)====================
    // ── 2.55.31 本轮七件事(用户逐条点名,全部落在这版)──
    //   ① 开关改名:登记表与面板里的「帖子区黑玻璃(暗色)」→「黑夜模式」,
    //      解释(hint)→「进入黑夜模式即可」。两处登记同步改(面板是手写 HTML,不遍历登记表)。
    //   ② 表情栏那两枚 UI(emoji / 贴纸 页签 + 分类胶囊)在黑夜里补实底、勾选态加绿色荧光。
    //      量过的旧值:底是 --spp-hole = rgba(255,255,255,.06),压在 #2c2c2e 上只出 56,56,58
    //      (用户截图里量到的就是这三个数),与面板本体差 12 档 ⇒ 观感「是透的」。
    //      现在实底 --spu-overlay(#3a3a3c) + 描边;勾选态(带 ✓ 的胶囊 / 当前那枚页签)
    //      三种荧光叠一起:外圈 12px 绿光晕 + 内圈极淡绿 + 字上一层绿雾。
    //   ③ 图 / 链定位的输入框口径收成一处(pickIdx):空 / 非数字 → 第 1 个;大于总数 → 也回
    //      第 1 个(原来是 Math.min 夹到最末一个);点行原来一律跳整层顶部,现在也读框。
    //   ④ 单页帖里「复制本贴」= 「复制本页」:原来只回一句「本贴只有 1 页」就退出,
    //      现在直接复制,读数里把「1 页」的依据一并写出(没读到 .pagesone 的也如实标注)。
    //   ⑤ 快捷工具栏:按钮字 --spu-secondary(#b6c3d6) → --spu-text(#f5f5f7),
    //      分类字(基础文本格式…)--spu-muted(#93a1b5) → 白 + 700。
    //   ⑥ 回贴页(post.php)三件事:整页跟着黑夜模式变黑 / 「主题回顾」按黑夜·白昼给底 /
    //      底部两枚按钮统一。实测根因与改法见 CSS 第 6、7 段与 POST_CSS 那两个新段:
    //        · 原来这页整页白:.t.f_one 的底 = rgba(250,252,255,.93),而 html 上从来没有
    //          spp-rdark(initReadDark 的门是 WANTS.readOnly)⇒ 只有这一页不跟着变;
    //        · 「主题回顾」那几张表是 #main 的直接子元素、不在任何卡里 ⇒ 一直落在星空上
    //          (与黑模式无关,星空是另一枚默认开启的开关),站点字色 #555 压深空;
    //        · 底部两枚按钮原来是「站点红底切图 19.67px 高 + 我们的内联胶囊 24px 高」,
    //          高度差 4.3px、圆角 0 vs 5px、字体 Arial vs inherit。
    //      改法:门放宽到 read + post;令牌挂 #main(表单里所有自绘 UI 跟着变);
    //      主题回顾的底写成一份变量(白昼 fallback #fff / 黑夜 --spp-rk-glass 黑玻璃);
    //      按钮皮肤从 .spp-quote-on 放开到所有 post.php(同一套几何,两枚按钮同形)。
    //   ⑦ 主页回复(u.php?action-post):板块 / 时间搬到卡片页脚右下角(DOM 搬迁,
    //      纯 CSS 跨不了 th / td 那格边界),主题文字居中 + 12→14px + 纯白 + 冷白光晕。
    //   · 未跑守卫、未改任何 md(用户禁令);反引号 334(连续第六轮同数)。
    //    // ==================== 运行版本标记(2.55.30)====================
    // ── 2.55.30 本轮一件事:面板底再压一档 —— 上一版字色对了,但「字坐的那层底」还差一档 ──
    //   · 用户:「你的颜色怎么感觉还是不对劲」,附两张图(苹果那行被圈的白标题 / 我们这页被圈的正文块)。
    //   · 诊断是逐像素采样两张截图(Pillow),不是看图猜:
    //       苹果页   那行白字 = rgb(245,245,247)   它的底 = rgb(0,0,0)      B-R = 2
    //       我们的页 被圈正文 = rgb(245,245,247)   它的底 = rgb(37,37,38)   B-R = 2
    //     ⇒ **字色一个字不差**(B-R 都是 2,就是苹果那支白);差的是文字坐的那层底:
    //       我们那层是 #252526 的灰,苹果是纯黑。字色相同、底差 37 档 ⇒ 对比度 19.29 → 14.06:1,
    //       观感上就是「白字发闷、面板发灰」,也就是用户说的「感觉不对劲」。
    //   · 底为什么是灰的(逐层算过):面板 = 半透玻璃叠在页面底上,再加一道高光。
    //     页面底在视口下方是苹果那支 #1d1d1f(29),透过 34% 出来 ≈ 10;玻璃自己带 29×.66 ≈ 19;
    //     再叠高光最亮 5.5% 白 ⇒ 实测 37。那 37 是三份加起来的结果。
    //   · 这一版把整族「磨砂黑玻璃」的**底色与高光一起压暗**(只动这两支,不新增选择器):
    //       玻璃底      rgb(29 29 31 / .66) → rgb(0 0 0 / .86)     10 处(短写法 4 + 长写法 6,含 1 处注释)
    //       高光 3 段式 .055 → .03                                4 处(翻页条 / 面包屑 ×2 / 区域令牌)
    //       高光 4 段式 .11/.035/.006/.000 → .05/.016/.003/0      3 处(版块卡片 / 公告 / 设置面板两栏)
    //     ★ 为什么连高光一起动:α 提到 .86 之后透上来的页面底只剩 ~4 档,**残余亮度主要来自高光**;
    //       只压底不压高光,实测底仍是 rgb(13,13,14)(对比度 17.7),压了高光才是 rgb(9,9,10)(18.3)。
    //     ★ 为什么连 4 段式一起动:它是同族的「玻璃高光」,只是多两个停点;不一起压,版块页那张
    //       近黑卡片的顶上会留一道 11% 白的灰角,与两个区域不是一套语言。
    //   · 实测(真装真跑,read.php?tid-2946160,1779×1042 视口,截图后逐像素量):
    //       面板底 rgb(37,37,38) → rgb(9,9,10);正文对比度 14.06 → 18.28:1(苹果同位置 19.28:1)。
    //       剩下那 1 点差在「玻璃仍有 14% 透光 + 一道反光」—— 要做到一字不差只有把玻璃改成不透明,
    //       那样反光也就没了,所以停在这里;要更黑说一声(一个数的事)。
    //   · 未跑守卫、未改任何 md(用户禁令)。
    //    // ==================== 运行版本标记(2.55.29)====================
    // ── 2.55.29 本轮一件事:正文白改成苹果官方的 rgb(245,245,247) ──
    //   · 用户原话:「正文的白色字体,其 RGB 应该参考苹果官方的:rgb(245, 245, 247)。」
    //   · 改的是两个令牌 —— 它们本来就是两个区域内「正文白」的唯二出口(容器级只写 color、不写
    //     -webkit-text-fill-color 那条老规矩没变,见 19 段段头):
    //       --spu-text    #e3eaf6 → #f5f5f7   (脚本自绘控件 / 输入框的正文色)
    //       --spp-rk-text #e3eaf6 → #f5f5f7   (帖子行容器那条 color,以及 .tiptop 与用户卡的文字)
    //     为什么原先那支白偏蓝:#e3eaf6 的 B 比 R 高 19 档,压在上一轮刚换成中性的黑玻璃上就显冷。
    //     实测活体(2.55.28 版):快速回复框里 input[text] 的颜色是 rgb(227,234,246);
    //     本版应为 rgb(245,245,247)。
    //   · 顺手把铭牌那支白也并过来了:2.55.28 我给铭牌写的是纯白 #ffffff(当时用户原话是「白色字体」,
    //     没给具体值)。现在有了官方这支白,同一个面板里就不该同时存在两种白 —— 一起改成 #f5f5f7。
    //     对比度 21.00 → 19.29:1(都远高于 WCAG AAA 的 7:1);肉眼不可分,换的是色温(旧白偏蓝)。
    //   · 相邻两档**没动**(用户只点了「正文的白色字体」):--spu-secondary #b6c3d6(次级文字)、
    //     --spu-muted #93a1b5(弱文字)仍是冷灰。要一起换的话,苹果那套的对位是 #a1a1a6 / #86868b。
    //   · 未跑守卫、未改任何 md(用户禁令)。
    //    // ==================== 运行版本标记(2.55.28)====================
    // ── 2.55.28 本轮两件事:铭牌改「黑色不透明玻璃 + 白字」;页面黑底搬运苹果 ──
    //   · 用户原话:「想了一下,对于黑夜模式来说,头像下面的 ID 应该是黑色不透明玻璃 + 白色字体才是
    //     正常的」,以及「黑夜模式来说它的黑色背景可以搬运苹果的黑色背景(www.apple.com/iphone-18-pro),
    //     苹果的这个黑色背景好很多」。测试帖仍是 read.php?tid-2946160.html。
    //   · 上一轮(2.55.27)我把铭牌按 2.53.42 的设计原值还原成「86% 白毛玻璃条 + #26303f 深灰字」,
    //     用户这一轮明确否掉这个方向。理由成立:铭牌压在**头像照片**下沿,整块黑玻璃面板里就它是一块
    //     亮条,夜里最扎眼。⇒ 改成不透明黑底 + 白字。
    //   · 苹果那支黑不是猜的:把用户截图红框内的空白区按像素统计(Pillow),两个子块各 100.0% 都是
    //     #1d1d1f;苹果页 body 是 #000,各 section 在这两支之间交替。⇒ 底色就用这两支。
    //
    //   ── 具体改了四处 ──
    //   ① 铭牌(CSS 段第 4 节,2.55.27 那条原地改):底 rgba(255,255,255,.86) → #000(不透明)、
    //      字 #26303f → #fff,再叠一道上沿 10% 白高光当「玻璃」的分界。
    //      实测(活体,视口内 5 枚铭牌):底 rgb(0,0,0) / 字 rgb(255,255,255) / 对比度 21.00:1。
    //      为什么必须不透明:铭牌底下是头像照片,明暗随机;半透黑会随头像变深变浅,不透明就没这个变量。
    //   ② 共用的「磨砂黑玻璃」那**一个值**:rgb(15 21 34 / .66) → rgb(29 29 31 / .66)
    //      (= 苹果 #1d1d1f 的 66%)。全脚本 10 处(9 处代码 + 1 处注释),两种写法都在内:
    //      短写法 rgb(... / .66) 4 处、长写法 rgb(... / 0.66) 5 处。
    //      ★ 为什么不止改「两个区域」:这条配方是同一个设计元素(2.52.44 定案),翻页条 / 面包屑 /
    //        版块卡片 / 公告 / 用户页面板 / 设置面板都用它,注释里写的是「玻璃底逐字同步自 SPP-GLASS 段」。
    //        只改两个区域,紧挨着的翻页条与面包屑会当场变成一块蓝黑,比不改更难看。
    //      ★ 旧值 rgb(15 21 34/.66) 在 2.55.25 段头与 SPP-GLASS 段的原文里仍留作历史,未删。
    //   ③ 页面底色(CSS 段第 5 节,新增):原星空底 radial-gradient(ellipse at bottom, #1b2735, #090a0f)
    //      → radial-gradient(ellipse at bottom, #1d1d1f, #000)。**只换色标、不换形状**:页面纵向的明暗
    //      节奏一像素不动。这条只挂 html.spp-rdark —— 星空是**另一枚开关**(spp_home_starfield),
    //      它的夜空是它自己的设计,只有黑夜模式打开时才接管这一层,关了立刻还原。
    //      另加 html.spp-rdark { background-color:#000 } 兜底:星空关掉时页面画布会退回站点浅色,有它
    //      才不会出现「黑玻璃挂在白底上」。实测它**不会**盖住星空(探针:#spp-starfield rect 仍是整屏
    //      1264×760、渐变色有效)—— 它是 z-index:-1 的子元素,负 z-index 画在根背景之上。
    //   ④ 两个区域内的令牌:--spu-surface #151c2b → #2c2c2e、--spu-overlay #1c2436 → #3a3a3c
    //      (苹果那套抬高灰)。不换的话面板底已经中性了,面板里脚本自绘的控件与下拉菜单还是蓝的。
    //      实测菜单底 rgb(58,58,60) = #3a3a3c。
    //
    //   ── 口径边界(说清楚没做什么,别当成「全量去蓝」)──
    //   本轮只动了「磨砂黑玻璃」这一族 + 页面底色。脚本里其余暗色面**没动**,仍带旧蓝黑:
    //     顶栏 / 导航 rgba(9,13,21,.62)、个人信息卡 rgba(14,18,26,.9)、下拉菜单 rgb(11,17,25) /
    //     rgb(13,20,32),以及更细的一批(普查:全文件 644 种颜色字面量里,暗色 135 种、带蓝味 85 种、
    //     合计 206 处)—— 其中还混着 #26303f 这类**文字**色,不能一起扫。全量去蓝是跨模块的独立一轮,
    //     需要单独确认,本轮不做。
    //   · 未跑守卫、未改任何 md(用户禁令)。
    //    // ==================== 运行版本标记(2.55.27)====================
    // ── 2.55.27 本轮一件事:头像框下沿那枚「ID / 用户名」铭牌看不清(用户第三轮截图圈选)──
    //   · 用户原话(截图红框圈住两枚铭牌):「大体上没问题,但是还是有瑕疵,框选的地方的 id
    //     估计要用黑色的字体才能看得清」+ 测试帖 read.php?tid-2946160.html。
    //   · 病根**不是「字色选错了」,是两半一起坏**:2.53.42 那套铭牌的设计是
    //     「86% 白毛玻璃条 + blur(6px) + #26303f 深灰字」(spp-boardtop-style-read 里那条
    //     .r_two:not(.spp-ava-tiled) > div[align=center] > a),而 2.55.25 的黑玻璃上线后:
    //       ① 1a 的扫白 (1,6,3) 把铭牌**自己的浅底**扫成了 transparent;
    //       ② 第 2 段 th.r_two 那条文字规则 (1,5,3) 又把它的字刷成了浅色。
    //     ⇒ 只剩浅色字压在「模糊后的亮头像」上:头像亮时白字就化在亮底里 —— 正是用户看到的样子。
    //   · 修法:**按设计原值把被扫掉的那一半补回来**(底 rgba(255,255,255,.86)、字 #26303f),
    //     而不是另起一套「深底 + 浅字」。理由:铭牌是头像卡的下沿,浅色主题里本来就是浅条 + 深字,
    //     用户点名要「黑字」⇒ 把被扫掉的设计补回来,比换一套配色更贴他的原意。
    //     选择性 (1,9,5):同时压过设计那条 (0,6,3) 与 1a 的扫白 (1,6,3)(两条都带 !important 时比 id 数)。
    //   · 实测(read.php?tid-2946160,视口内 4 枚铭牌):底 rgba(255,255,255,.86)、
    //     字 rgb(38,48,63)、**对比度 13.32:1** —— 浅底是实体色,所以任何头像都不会把它反过来。
    //   · 顺手做了一次「同类漏网」普查:把区域内所有**声明过浅底**的规则逐条列出来(24 条),
    //     再逐个看它此刻的实际底色 —— 除这枚铭牌外,其余(.f_one/.r_one 白底、td.h2 的 #ddd、
    //     头像 img 白底、.tiptop #f8fafc、.abtn 白胶囊、.spp-emoji-*、QR 各控件、屏蔽菜单…)
    //     都已按暗色重画过;只有铭牌是「浅底 + 深字」这一对被拆散的那一对 ⇒ 本轮只补它一条。
    //   · 也在真浏览器里核了一遍发包完整性:window.__SPP_VERSION = 2.55.26、
    //     #spp-rdark-style 的 textContent 长度 = 11333(与 2.55.26 文件里那个模板字面量同长)
    //     ⇒ 2.55.25 那个「注释里出现反引号、提前闭合整份 CSS」的坑没有复发
    //     (node --check 只看语法,「字面量变成 NaN」它看不出来,量长度才看得见)。
    //   ★ 本轮未跑守卫、未改任何 md(用户既有禁令)。2.55.26 已归档进 versions/ 供回滚。

    // ==================== 运行版本标记(2.55.26)====================
    // ── 2.55.26 本轮一件事:修掉 2.55.25 黑玻璃上残留的「白线条 / 白框」(用户第二轮圈选)──
    //   · 用户原话(附帖子页截图,红框圈住四处):「我发现一些UI框会有白色线条出现,
    //     可能你需要重新弄一个UI框来适配?」
    //   · 圈住的四处(活体逐个点名):① 头像装裱框(150×150 那枚图外侧的浅色圆角边)
    //     ② 楼层号胶囊(B4F / B6F)③ 操作条那一条长横线(.tiptop 的上下边,实测 1190px 长)
    //     ④ 快速回复框「快速发帖」标题行 td.h2。
    //   · 根因一句话:2.55.25 的扫白**只写 background-color、一根边框都没管**。
    //     活体清点两个区域 1261 个元素:带 alpha≥.2 的边框一共 16 种,全部是浅色或浅色
    //     inset 发丝线(#e4e9f1 / #e3eaf3 / #edf1f7 / #ddd / 234,239,247 inset…),
    //     没有一枚承载语义 ⇒ 与背景扫白同一套手法:整片扫成发丝线,再点名把「容器」补回来。
    //     (唯一保留的彩色边是快速回复框里三枚 #3fbfa8 —— 脚本自己的勾选态主色。)
    //   · 三处「扫白够不到」的(模块 19 的 1b 段逐条写了):
    //     ① 伪元素 —— 悬停卡左那枚小三角是 ::before,* 选择器匹配不到伪元素(背景扫白也匹配不到);
    //     ② inset 阴影 —— 悬停卡的行分隔线是 box-shadow、不是 border,边框扫白天然管不到;
    //     ③ 被更高特异性浅色规则压住的白底弹层 —— 屏蔽下拉菜单:2.53.24 那条是 (1,6,5),
    //        而扫白只有 (1,6,3),同 class 数、少两个类型选择器 ⇒ 实测 bg 仍是 rgb(255,255,255)。
    //   ★ 本轮最值钱的一课(已写进 1b 注释):星星主题那条
    //     html.spp-hm-star #main .t5:not(.spp-t5bar){border:…rgba(255,255,255,.66)!important}
    //     带 #main ⇒ 是 (1,3,1);「.spp-rdark 写两遍 + div.t5 + :has(…)」只有 (0,4,3) ——
    //     两条都带 !important 时**先比 id 数**,class 堆到 4 也压不过 1 个 id。补上 #main 才赢。
    //     实测:不补时快速回复框那圈白边纹丝不动(量到仍是 rgba(255,255,255,.66))。
    //   ★ 顺带更正 2.55.25 里一处**不生效**的写法:1a 给快速回复框补的
    //     box-shadow:0 0 0 1px var(--spp-rk-line) 实测被上面那条星星规则压住(同一个原因),
    //     本轮把那句删掉、只留磨砂 —— 真框改由 1b 的 border-color 接管,
    //     免得留一句「看着像在画框、其实没画」的死代码。
    //   ★ 覆盖度更正(诚实记录,不藏):2.55.25 交付时那句「区域内 0 处发白」是**视口内**的读数;
    //     本轮把扫描改成**两个区域全量**(含离屏、含悬停才出现的 0×0 元素)才挑出白底弹层,
    //     旧的视口内扫描根本量不到它 —— 当时的结论没错,是口径说窄了。
    //   ★ 本轮未跑守卫、未改任何 md(用户既有禁令)。2.55.25 已归档进 versions/ 供回滚。

    // ==================== 运行版本标记(2.55.25)====================
    // ── 2.55.25 本轮一件事:新增「帖子区 / 快速回复框 黑玻璃」可选开关(仅 read.php,默认关闭)──
    //   · 用户原话(附帖子页截图,红框圈住上下两块):「我希望增加一个黑暗模式,设置面板里面可以
    //     打开(默认关闭);框选的两个区域变成黑色(反光黑玻璃什么的,但是不能影响文本阅读,
    //     可能还要涉及文本黑白转化的需求)」+「修改脚本,我看看效果,不行就回滚版本」。
    //   · 开关:spp_read_dark(面板「功能开关」组,**默认关闭**)。挂在 <html> 上的标是 spp-rdark。
    //   · 两块 = ① #main > form.spp-flow 里那 6 行 .t5.t2(15b 把全楼合成的一体流卡)
    //             ② div.t5:has(textarea[name="atc_content"])(模块 18 的快速回复框)
    //     实测(read.php?tid=2978280,1280×900):① 1400×10753px 六行、② 1400×810px。
    //   · 配色不是新画的:与页面上下那两条 .spp-pgbar / .spp-crumb-panel 同一份「磨砂黑玻璃」
    //     配方(rgba(15 21 34/.66) + 152° 白高光 + blur(18px) saturate(140%) + 1px 白描边)。
    //     ★ 这套本是 2.52.42 / 2.52.44 给帖子区做的;2.52.45 用户说「帖子的背景修改为白色,
    //       不要黑玻璃背景」⇒ 当时整段撤掉、玻璃只留在翻页条上。本轮装回来,但**做成开关、
    //       且只覆盖用户框的那两块**(当时的毛病是强制生效、且连帖子区外围一起罩住)。
    //   ★ 为什么必须动 JS(活体实测,不是推理):initReadFlow 与模块 18 各自给那一块写了
    //     **内联** background-color:#fff !important;CSS 规范里内联 !important 高于任何样式表
    //     !important ⇒ 不改这两处,加多少 CSS 都盖不住。改法是把字面量换成变量
    //     (var(--spp-flow-bg,#fff) / var(--spp-qr-bg,var(--spu-surface,#ffffff)))——
    //     关态变量没定义、兜底值就是原字面量 ⇒ **关态逐字节等价于 2.55.24**。
    //   ★ 遮挡清单(活体逐个点名,不是抽样猜):.t5.t2 行本体、th.r_two 作者格(185×9901)、
    //     头像 img(150×150 白底)、用户名白条(150×22)、.tiptop 操作条(1190×30 #f8fafc)、
    //     a.s3 / a#modify_tpc 两枚白胶囊、.spp-tp-sep 分隔条、hr;快速回复框侧:
    //     站点文本框/下拉/按钮(实测 matched=0,白色来自 UA)、#smiliebox 贴纸面板、
    //     .spp-emoji-cat / .spp-emoji-tab、a.abtn、td.h2「快速发帖」标题行。
    //   ★ 文本可读性三条(详见模块 19 段头):容器只给 color、不给 -webkit-text-fill-color
    //     (后者是继承属性,写在容器上会盖掉发帖人自写的颜色);区域令牌换浅色;剩下写死
    //     深色的逐个点名;再补一道对比度扫描给发帖人手写的深色字(模块 19 的 lift)。
    //   ★ 已知代价:① 帖内引用框 / 代码块 / 发帖人自写底色会被扫成透明(本页实测 0 个,
    //     本轮无法验收,等遇到带引用框的帖子再补「深色托盘」);② backdrop-filter 挂在 6 行上
    //     (首行 9902px 高),模糊面积大,低配机可能掉帧 —— 「反光」就来自它。
    //   ★ 本轮未跑守卫、未改任何 md(用户既有禁令)。2.55.24 已归档进 versions/ 供回滚。

    // ==================== 运行版本标记(2.55.24)====================
    // ── 2.55.24 本轮一件事:无图卡的「收藏」点不动、回复/热度动效消失(仅版块页)──
    //   · 用户原话(截图,红框圈住最后一张无图卡的「收藏」):
    //     「BUG反馈:https://blue-plus.net/thread_new.php?fid-9-page-1.html
    //      问题,无图片的卡片样式底部的收藏按钮不能正常点击;回复和热度的动效也消失了。
    //      你控制的浏览器的脚本已经是最新的」
    //   → 根因:2.50.4 给无图卡「黑色玻璃」时把 backdrop-filter 写在 .inner **本体**上。
    //     该属性会**为 .inner 建立堆叠上下文**,而且它没有 z-index(= auto / 0 层)⇒
    //     .inner 连同它内部那条被抬到 z-index:6 的数据行,整块被压在整卡命中层
    //     .spp-card-hit(z-index:5) 之下。命中测试由绘制在上面的元素先赢,于是:
    //       · 点「收藏」→ 落到命中层 = 打开帖子(收藏动作根本没机会发生);
    //       · .spp-metabtn 永远接不到鼠标 ⇒ button:hover 不触发 ⇒ 弹性回弹动效消失。
    //     有封面卡的玻璃写在 .inner::before(伪元素建上下文,不动 .inner 本体)⇒
    //     那边 z-index:6 > 5 依旧成立 —— 这就是「只有无图卡坏」的全部差别。
    //   → 单变量活体对拍(同一张无图卡,只切这一个属性,其余一字不动):
    //       开着 → document.elementFromPoint(收藏中心) = A.spp-card-hit
    //              document.elementFromPoint(回复按钮中心) = A.spp-card-hit
    //       临时置 none → SPAN.spp-fav-txt / BUTTON.spp-metabtn
    //       再恢复 → 又回到 A.spp-card-hit
    //     对照:有封面卡 .inner 的 backdrop-filter 计算值为 none,两处一直是真元素。
    //   → 修法 = 把 2.45.0 那套「抬容器 + pointer-events 分层」**再抬一级**:
    //       无图卡 .inner 抬到 z-index:6(站到命中层之上)+ 自身 pointer-events:none
    //       (鼠标穿透回命中层),只让真正的交互元素 a / button 用 auto 自己接回来。
    //     实测补上 pointer-events 后:收藏中心 → SPAN.spp-fav-txt、回复按钮 → BUTTON.spp-metabtn、
    //     标题文字 → A(标题链本体)、卡片空白处 → A.spp-card-hit(整卡点击一分不丢)。
    //   ★ 视觉零变化:只动 z-index 与 pointer-events,不碰颜色 / 尺寸 / 模糊 / 任何 DOM 节点。
    //   ★ 就地更正 2.52.66 写下的旧结论(「收藏按钮此前能用…【点击】一直正常」)——
    //     它只对有封面卡成立,见该段 2.55.24 更正框;不更正的话下一个人会照它
    //     以为无图卡已被命中层的「让路」覆盖住了。
    // ==================== 运行版本标记(2.55.23)====================
    // ── 2.55.23 本轮一件事:悬赏帖的悬赏金额不显示 —— 修 + 美化(仅 read.php)──
    //   ★ 版本号为什么跳过 2.55.22:那一号已经用在「守卫结账」轮上(经验.md 41.8 —— 34 处
    //     对齐 + 三个变异测试的假绿,**交付物一个字节没动、没有脚本产物**)⇒ 本轮取 2.55.23,
    //     免得一个号同时指「守卫轮」和「脚本版」两件事。
    //   · 用户原话(截图,红框圈住悬赏框):「网址:https://blue-plus.net/read.php?tid-2952377.html
    //     BUG:对于悬赏区,原网站有悬赏金额,脚本没有显示,如何解决这个问题。
    //     你控制的浏览器脚本已经是最新的了,分析网页结构,给一个解决方案」
    //     →(看完根因与 A/B 实测)「显示出来并且美化」。
    //   → 根因:悬赏框一直在 DOM 里,是**脚本自己**把它藏了。站点把这一块放在
    //     TABLE.js-post 的**末行**(TR.tr1.r_one > TH > DIV.tpc_content > DIV.tips),
    //     而 2.53.7「左栏收口」那条末行规则不带条件 —— 它当时的实测依据是
    //     「末行是空行,只剩正文锚点 div#w_<pid>」,普通帖确实如此;
    //     悬赏 / 活动 / 商品帖的同一行里多了 div.tips ⇒ 连框带数一起 display:none。
    //     真机实测(tid-2952377,脚本 2.55.21):31 个楼层各有一个末行,含 .tips 的只有
    //     1 个(楼主行);该行 getComputedStyle(tr).display === "none"、div.tips 的 rect 是 0 x 0。
    //   → 修法一个词:末行那条选择器加 :not(:has(.tips)) —— 含悬赏框的照常显示,
    //     其余 30 层的空末行照旧藏掉(2.53.7 收掉的那 34px 一分不丢)。
    //     实测 A/B:那一行 0 -> 135px(悬赏框本体 177 x 95),整页高度 +135 正好等于这一行。
    //   → 美化(用户同句要求):站点那口 177 x 95 的居中三行小灰框,改成一条**琥珀卡片**:
    //     三行摊成一排胶囊(状态一枚 + 两枚白底数额胶囊),配色与购买卡的琥珀警告条同源
    //     (#fffaf0 底 / #f2e3c2 描边 / #e8c469 左线 / #8a6512 字)。
    //     数额由新增的 initBountyTips 拆成独立节点后单独提亮:#b45309 13px/700(最佳答案)、
    //     #8a7333 12px/600(热心助人),两处都吃 tabular-nums。
    //   ★ 站点原文一个字没改:只换外观;JS 只把「标签: 数额」拆成两个 span。
    //     JS 没跑时 .spp-bn__k / .spp-bn__v 两条选择器匹配数为 0 ⇒ 卡片照旧成立
    //     (与 2.52.9 版块页工具条同一条规矩:形态由 JS 生成、CSS 只管样式)。
    //   ★ 刻意没动:悬赏行的其余同级节点(div.c 清浮、div#w_tpc 正文锚点)原样留着 ——
    //     它们高度都是 0,一个节点没搬;帖子正文、附件、楼层结构一律不碰。
    //   ★ 同一个假设还留下过一段**死代码**(本轮只记录、不删):initTipadMerge 尾部那段
    //     「内容为空就藏这一行」取的是 post.querySelector('.tpc_content') 与
    //     post.querySelector('.tr1.r_one') —— 即第一枚正文框与第一行,永远不是末行。
    //     现在末行的开合由那一条 CSS 独占负责(含 2.53.7 的收口),保持单一口径。
    // ==================== 运行版本标记(2.55.21)====================
    // ── 2.55.21 本轮一件事:用户中心(u.php 全族)的字体统一提到「白亮」两档 ──
    //   · 用户原话(两张截图,红框圈住整幅内容区):「测试网址:
    //     https://blue-plus.net/u.php 字体修改为白亮色」/「测试网址:
    //     https://blue-plus.net/u.php?action-post-uid-1515110.html 一样,
    //     字体修改为白亮色」。
    //   → 纯改色,一行布局 / 尺寸 / 结构都没动。全族文字只剩两档:
    //     一级(链接 / 列表与卡片标题 / 页签名 / 用户卡按钮 / 更多药丸 /
    //     收藏页工具条 / 翻页页码 / 分类药丸 / 资料页字段值)= #ffffff;
    //     二级(正文 td 与 th、.gray/.gray3/.f9/dt、签名 #honor、日期、作者、
    //     等级与 Lv.、翻页文字、资料页字段名、表单控件字)= #eaf2ff。
    //     改前那六档(#c3dcff 链接 / #c6d5e8 次级 / #d3dce8 正文 / #a9bccb 页签 /
    //     #c7d3e3 卡片继承色 / #93a7bd 资料页字段名)在近黑底上 12px 都被读成「灰」。
    //   → 覆盖的不只是这两张截图:同族任何一页吃的都是同一条读数路径,
    //     个人首页 / 资料 / 主题 / 回复 / 收藏 / 好友 / 商品 一起变;收藏页 2.55.17
    //     已定的两级白原样保留(只把工具条文字与「×」方片并入同一套)。
    //   ★ 刻意没动两处:① 面包屑(#breadcrumbs)—— 它在全站每一页都在,u.php 单独
    //     提亮会与别的页面不一致;② 小节标题 h5 的 teal #8bf7cd —— 那是 2.52.88
    //     定下的高亮,本来就不是「暗字」。
    //   ★ 层级改由字号、粗细与底色承担,不再靠「白 vs 灰」拉开。
    //   ★ 唯一的非纯色改动:小节「更多」药丸补 font-weight:500 —— 站点给 .gray3 写死
    //     100,改纯白后仍是整页最细的一处;这与 2.55.17 在收藏页列表标题上遇到的是
    //     同一条站点规则,照同一档补(字号与盒子不动)。
    // ── 2.55.20 本轮一件事:茶馆热度配色(分档边界 + 四档色)改成设置面板可自定义 ──
    //   · 用户原话:「对于茶馆,不同的热度对应不同的颜色,能否在设置面板中由用户自定义
    //     颜色、热度?分析可行性」→(看过可行性分析)「OK,动手」⇒ 走方案 A(取色器)。
    //   → 面板新增**第六类行 data-type="heat_json"**:一行装 7 个控件(3 个边界
    //     数字框 + 4 个取色器),整份存进一个 JSON 键 spp_heat_style —— 一次保存
    //     原子落盘,不会出现「改了色没改边界」的半新半旧配置。
    //   → 新读数层 SPP_CONFIG.heatStyle():三档边界**升序排序**(写反了不会让某一档
    //     取不到色)、色值只认 #rgb / #rrggbb(这一串直通 style.setProperty('color', …)
    //     与 --spp-mbtn-fg,必须收紧)、数值走 cfgFit(空/非数回落默认、越界拉回边界)。
    //   → getTeaHeatColor() 的**分档逻辑一字未动**,只是把 3 个常量 + 4 个字面量色换成
    //     一份读数;2.55.7 定的 1000 / 5000 / 20000 与 2.55.8 定的冷档纯白仍是默认值
    //     (搬进 HEAT_STYLE_DEFAULTS),老用户升上来看到的还是同一张页面。
    //   ★ 影响面**只有茶馆(fid 9)**:其他版块标题一律纯白(2.55.1 定案)、数字色仍走
    //     20/50/80 那套配色 —— 两处都一个字没动(本轮有断言钉着)。
    // ── 2.55.19 本轮一件事:撤掉 2.55.18 里「编辑器图标条反色」那一处 ──
    //   · 用户原话(指着我给的工具栏截图):「这里不用动」。
    //   → 2.55.18 曾把那一排站点自带的编辑器 GIF(图里自带 #EBEBE3 白底)用
    //     filter:invert(1) hue-rotate(180deg) 翻成暗底浅笔画;本轮把那两条规则整块删掉、
    //     模块头部那段说明同步改写(不留一条已撤掉的规则吹在注释里)。
    //     ★ 侧栏里那两枚白块按钮(①)与表情弹层(③)照旧保留 —— 用户只点了工具栏。
    //     ★ 撤掉后本模块只剩 10 条规则,「hue-rotate」一字不存(自检里钉死了这一条)。
    // ── 2.55.18 本轮一件事:写短消息页(message.php?action-write.html)把白底换掉 ──
    //   · 用户原话:「测试网址:https://blue-plus.net/message.php?action-write.html
    //     美化 UI」→(看完成本后收窄)「把白色底边换掉,让它看起来清楚一点就 OK 了,
    //     是个很简单的更换任务」。
    //   → 纯 CSS:①两枚 A.abtn「添加表情 / 草稿箱」的站点白底(.abtn{background:#fff},
    //     而字色被本页皮肤继承成近白 ⇒ 白底近白字,等于看不见)换成本站体系的暗底胶囊;
    //     ②「内容」那排编辑器图标条:16 枚 GIF **自带不透明白底**(四角 rgb(235,235,227)),
    //     在深底上是一条米白脏带 —— 用 filter:invert+hue-rotate 翻成暗底浅笔画;
    //     ③「添加表情」弹出的面板(站点把它抄进 BODY > DIV.menu 那枚万能浮层)里那两块
    //     原生白(#buttons.face 的 th1.png 底、#showface 的行内白底)一并换掉。
    //   ★ 只换底色:不动布局、不挪一个盒子、不碰 DOM 与事件;站点那套拖拽 / 切页签 /
    //     关闭 / 插入表情全部照旧(见 SPP-MSGWRITE 段尾部注释)。
    // ── 2.55.17 本轮两件事:①表情栏选择区改成**滚轮翻页**(那排「1」「2」页码牌撤掉)
    //    ②收藏页(u.php?action=favor)字体提亮 + 删除 UI 美化 ──────────────────
    //   · 用户原话:「框选部分,无论是 emoji 还是贴纸都通过滚轮翻页,不需要下方的 1、2
    //     点击翻页」/「此外:对于网址:https://blue-plus.net/u.php?action=favor
    //     字体改为亮白色、美化删除 UI」
    //   → ①.cat-pager / .cat-pnum 两条 CSS 与那排节点**整个删掉**(不留没人用的选择
    //     器):翻页改成在选择区本身上滚滚轮 —— 一格一页;滚到两头(第 1 页再往上 /
    //     最后一页再往下)就把事件**还给页面**,不吃掉整页滚动。位置反馈改走悬停提示
    //     (SPP_TIP):滚轮翻页 · 第 x/y 页 · 共 N 类。
    //     ★ 两页共用同一条路:emoji 与贴纸的分类胶囊都铺在同一个 catsTrack 里 ——
    //       用户点名「无论是 emoji 还是贴纸」,所以不必各做一份。
    //     ★ 顺带修一处判页 bug:catsCurPage 原来用「scrollTop ÷ 一页高」整除判当前页,
    //       而最后一页常被 scrollHeight 夹住(目标值 < 页序 × 一页高)⇒ 反白会少算一页
    //       (2.55.10 起就潜伏着,只是页码牌自己盖住了)。现在改成「哪一页的目标位置
    //       离现在最近」,并把平滑滚动那 600ms 交给 catsSeek。
    //   → ②「亮白」= 列表标题从 #c3dcff / font-weight:100(站点写死的极细字,深空底
    //     上 12px 看着就是灰)改成纯白 + 600,作者 500 + #eaf2ff,表头一并提亮。
    //     删除 UI 三处:勾选框(原生白方块 → 方片 + teal 勾)、每个自定义目录右端那枚
    //     16px 位图「×」(tips-close.png → 自绘方片,悬停转红)、工具条那对 radio
    //     (裸文本节点 CSS 选不到 ⇒ JS 包成胶囊;选中「删除」时提交键同步转红)。
    //   ★ 顺带修页型识别:initUprofilePage 原来只认 action- 连字符,而地址栏里
    //     ?action=favor 这种等号写法同样真实存在 —— 那一形态此前恒落 spp-up-index,
    //     .spp-up-post 那整段「回复列表卡片化」也一并永不生效。现在两种形态通吃。
    // ── 2.55.16 本轮一件事:左右两栏那四块(说明 / 示例 / 输入框 / 小结)横向对齐 ──
    //   · 用户原话:「尽可能通过空行实现左右两栏四个方块视觉上的大致对齐。」
    //   → 病根是**两格说明的折行数不一样**(实测:左 3 行 / 右 4 行)。两格等宽,而右格那句
    //     多出「写法与左格相同」等字 ⇒ 折成 4 行,右边整块矮下去 17px,下面的示例、输入框、
    //     小结合着一起错开(实测两枚 row 高 347.8 / 364.8 —— 差的正好是一行)。
    //   → 修法刻意不用 CSS 空行 / min-height 写死行数:面板宽 = min(900px, 100vw-48px),
    //     窗口一窄折行数就变,写死的行数只在某一个宽度成立。改成把**会撑高的文案写成等长**:
    //     ①「进 emoji 页 / 进贴纸页」从说明里挪进上面那行 label(label 永远一行高);
    //     ②两格的说明从此**逐字相同**;③示例块两格只差「我的表情 / 我的贴纸」(都是 4 个字)。
    //     于是不论窗口多窄、怎么折行,两格的每一块都同高。
    //   → 真机实测(面板 900px、每格 435px):四块的顶边逐一对齐,两枚 row 高度相等。
    // ── 2.55.15 本轮三件事:①「自备表情 / 贴纸」那一格拆成**左右两栏**(左 emoji / 右 贴纸)
    //    ②同一张图在两页各用一次不再被去重丢掉 ③两格的示例从 placeholder 挪到输入框上方常驻
    //   · 用户原话:「如果 emoji 和贴纸共用一个自定义栏框,输入文本:测试 / https://… 测试 /
    //     [emoji] 测试 / https://… 测试 会出现两个都无法检索到的情况。我建议分为左右两栏:
    //     左栏为 emoji,右栏为贴纸自定义。此外案例应该在输入框上方,避免出现输入文本后
    //     无法查看案例的情况。建议优化案例文本,使其更易于理解,最好能举例说明。」
    //   → ①存储键一拆为二(spp_custom_emoji / spp_custom_sticker);旧键 spp_custom_packs
    //     不再渲染,但它躺着的内容由一次性迁移 spp_mig_packsplit_on 按页拆进两枚新键
    //     (不迁的话升级后两格全空、表情栏里的自备套系凭空消失)。
    //   → ②病根是 seen 那张去重表**只有一张**(2.55.11 起就是全局的):同一张图既当 emoji
    //     又当贴纸时,第二处的整行会被判「和前面重复」丢掉 —— 那一套随即被判「一个链接
    //     都没跟」一并删掉,用户看到的就是「两个都搜不到」。现在两页各持一张 seen 表。
    //   → ③示例改成输入框上方的常驻小块(左缘一道强调色竖条 + 等宽小字,逐行标注悬停
    //     文案);placeholder 一并撤掉 —— 它正是「一打字就看不见」的那一份。
    // ── 2.55.14 本轮四件事:①「表情名称提示」默认值翻成「开」②自备套系能给单张贴纸
    //    写自定义悬停文本 ③表情 / 贴纸的悬停框换成脚本自绘的提示卡(UI 美化)
    //    ④设置面板新增「自定义图床(粘贴区)」→ 接进「图床 ▾」与认名 / 认链
    //   · 用户原话:「表情名称提示;这个功能默认开启。」/「如果我想为某张贴纸设置弹出的
    //     UI 文本,该如何配置?」/「当鼠标悬停在 emoji 或贴纸上时,弹出的文本框需要进行
    //     UI 美化。」/「在设置面板中增加『图床跳转』功能,允许用户自定义增加更多『图床』
    //     UI 选项。(分析可行性)」
    //   → ①四处同值(登记表 / 手写 HTML / 14 模块门口 / migrateSettings 的写回)+一次
    //     性迁移 spp_mig_emojitip_on(老存储里躺着 false,翻默认值对它们无效)。
    //   → ②语法:地址后面接一个空白再接文本;没写的照旧退回「套系名 + 序号」,可混用。
    //   → ③不再写 img.title,改挂 data-spp-tip,由 SPP_TIP.delegate 在网格容器上统一摆位
    //     (原生 title 没法美化;委托是必须的 —— 网格每次整片重建)。
    //   → ④复用第五类行 data-type="packs"(两格要的东西完全一样,只有 data-key 不同),
    //     解析在文件作用域,认链走 IMG_HOSTS_LOOKUP(自定义在前,免得被自带的截胡)。
    // ── 2.55.13 本轮两件事:①「图片链接转 [img]」从面板撤掉、改成硬性常开
    //    ②「链接自动标注」默认值从「开」翻成「关」(含老用户存储的一次性迁移)
    //   · 用户原话:「img 这个功能默认打开,禁止用户关闭,从设置面板中移除(这个功能
    //     作为默认开启即可,无需在设置面板中显示。)」+「链接自动标注 //这个功能默认是关闭的」
    //   → 撤行是**两张表一起撤**(SPP_PANEL_DEFS 与 buildOverlay 的手写 HTML)——
    //     只撤一边,面板自检立刻报「登记了但没渲染 / 渲染了但没登记」。
    //   → 运行时那句读数改成常量 true:面板上没有控件,存储里写着 false 也不再生效,
    //     「禁止关闭」是硬的;键 spp_self_img 留在存储里当孤儿键(不清理,留个后路)。
    //   → 翻默认值那一枚在 migrateSettings() 里补了一次性迁移:保存循环是把每一行都
    //     写一遍的,凡是在 2.55.13 之前存过面板的机器,存储里都躺着 spp_host_mark=true,
    //     只改默认值对它们无效(2.53.75 的 spp_imgidx_show 吃过同一口亏)。
    // ── 2.55.12 本轮一件事:设置面板两行提示语按用户给的原文重写 ──────────────
    //   · 「链接自动标注」→「作用于文本输入框,勾选后会自动将输入框内的所有超链接按
    //     图床格式标签进行标注」
    //   · 「表情名称提示」→「鼠标悬停在表情上会有文本提示,但由于确定表情包对应的文本
    //     工作量较大,该功能基本无用」
    //   ★ 两张表(SPP_PANEL_DEFS 与 buildOverlay 的手写 HTML)各一处 ⇒ 四处一起改,
    //     否则面板自检与守卫都会报「登记了但没渲染 / 渲染了但没登记」。
    //   ★ 键名、类型、默认值一字未动;原来尾巴上的「默认开启 / 默认关闭」按原文去掉。
    // ── 2.55.11 本轮一件事:表情栏的**自备套系改成面板可粘贴** ──────────────
    //   用户原话(2.55.9 那轮拍板):「以后再加套系走哪条路」→「面板可粘贴」。
    //   → 设置面板多出一整幅「自备表情 / 贴纸」组:一格文本框,粘进来就是套系本身
    //     (一行套系名 + 若干行图片直链,套系之间空一行;名字前写 [emoji] 落 emoji 页)。
    //   → 面板引入第五类行 data-type="packs";解析在文件作用域(模块 3 比模块 14 先跑,
    //     SPP_PACK_URLS 要更早拿到这些链接),面板只摊开它的**小结**。
    //   → 粘进来的链接一并进 SPP_PACK_URLS ⇒ 它们同样享受 2.55.9 的居中豁免。
    // ── 2.55.10 本轮一件事:选择区的翻页从「‹ › 左右挪」改成**页码直选** ──────
    //   用户原话(2.55.9 装好后 + 截图批注):「上方为选项,下方设有『1』、『2』等翻页
    //   按钮,而非通过左右滑动按钮进行翻页」
    //   → ‹ › 两枚箭头与 i/n 页码整个撤掉(CSS 一并删干净),改成胶囊**下方**居中一排
    //     页码牌:有几页就有几枚,点第几枚就翻到第几页,当前页反白。只有一页时不露。
    //   → 选择区排版随之由「横排 [‹][胶囊][›][i/n]」改成「竖排:胶囊在上、页码在下」,
    //     整条高度多出约 19px(页码那一排),网格高度口径 catsH 自动跟着让位。
    // ── 2.55.9 本轮四件事:① 用户自备 5 套表情 / 贴纸(453 张)接进表情栏
    //    ② 动画格在网格里改成静止首帧 ③「图片居中显示」不再管脚本的表情 / 贴纸
    //    ④ 新增「贴纸居中显示」开关(默认关 = 不居中)──────────────────────
    // ── 2.55.8 本轮一件事:茶馆冷档「白得太暗」提亮 ──────────────────────────
    //   用户原话(2.55.7 装好后):「当标题为白色时,如果觉得太暗,能否将其调整为更亮的白色?」
    //   → 真机实测先确认口径没错位:茶馆首页 42 张的档位/色值 42/42 吻合(绿 16 / 冷灰 15 /
    //     橙 8 / 红 3),「看着像白其实偏暗」的那批标题走的正是冷档 #9AA5BB。
    //   → 改法:getTeaHeatColor() 冷档 #9AA5BB → #FFFFFF(相对亮度 0.63 → 1.00,
    //     卡底 #131A2A 上对比度 6.4:1 → 15.6:1)。茶馆一张卡一个色 ⇒ 标题与两枚数字一起变亮。
    //   → 只动这一个常量:档位边界(1000/5000/20000)不动,getReplyColor() 一字未动
    //     (其他版块数字配色与降级路径照旧 #9AA5BB)。

    // ── 2.55.7 本轮一件事:茶馆的变色判据从【回复数】改成【热度】──────────────
    //   用户原话:「我发现茶馆的变色规则有点复杂,直接按热度来划分颜色,你看怎么样?」
    //   → 旧口径 20/50/80 看回复数,而卡片上并排显示「回复 x 热度 y」,两条轴量级
    //     完全不同(回复 0–441 / 热度 9–374324):回复差 1 就换色、热度差一万却同色。
    //   → 阈值从数据里切:实测茶馆 1–5 页 242 张卡的热度分位
    //     min 9 · p10 528 · p25 816 · p50 2494 · p75 6720 · p90 23590 · max 374324
    //     取几何级差 ×5/×4 的圆整数 1000 / 5000 / 20000 ⇒ 冷灰 27% / 绿 41% /
    //     橙 21% / 红 10%(绿是主色,红只给头部一成)。
    //   → 改动面:新增 readHeatCount()/getTeaHeatColor();titleInkColor() 茶馆先问热度
    //     (读不到才退回回复档位);reformatReplyViews() 在茶馆让数字跟标题同色。
    //   → getReplyColor() 一字未动(茶馆那支 20/50/80 保留为降级路径),非茶馆一位不动。

    // ── 2.55.6 本轮一件事:茶馆「作者给标题上过色 ⇒ 标题恒白、档位失效」的止血 ──────
    //   用户原话:「我需要的是,你有没有办法解决这个问题?」
    //   病灶是 2.46.0 归一的副作用:站点把作者的色写成 <font color>(<a> 的子元素),
    //   归一把它刷成 TITLE_WHITE !important,而 colorTitleByHeat 把档位色写在父级 <a> 上
    //   —— 子元素自己的颜色压得过父级 ⇒ 实际渲染是纯白。2.55.1 已修非茶馆一侧,茶馆那侧
    //   当时按用户口径「不针对茶馆」保留原样,本轮补上。
    //   → 修法=把 2.55.1 的「两层同色」推广到茶馆:新增 titleInkColor()(唯一口径:
    //     茶馆=getReplyColor(回复数)、其他版块=TITLE_WHITE、读不到回复数=空串),
    //     归一的刷白与 colorTitleByHeat 的档位色都改成问它 ⇒ 两个写入点结构上不可能不一致。
    //   → 口径一个字没改:茶馆仍按回复数分档(20/50/80),热度依旧不参与。
    //   → 触发率实测:茶馆前 3 页 142 张卡,带色标题 0 张(资源区当年是 44 张里 31 张)。

    // ── 2.55.5 本轮一件事(2.55.4 装进篡改猴之后,用户在面板截图里圈出三行)──────────
    //   「设置面板中的选项:含图楼层悬浮窗、表情名称提示、打开帖子自动索引全贴
    //     (含图 / 链接跨页定位)移动到左侧」
    //   → 与 2.53.57 同一条老办法:**只改累加目标**(html → leftTail),行本身一字未动。
    //     三枚在源码里本就是 imgidx_show → emoji_tip → idx_auto,中间夹着的 host_mark /
    //     self_img 留在右栏 ⇒ 搬到左栏后这三枚正好连成一片,落在「用户名 + 彩色背景头像」
    //     之后、「悬停高亮」之前。
    //   → 面板自检按 data-key 集合比(与顺序、分组无关),所以搬动不影响它。

    // ── 2.55.4 本轮一件事(用户附社区任务页截图,红框圈住「上次领取未超过 158 小时」)──
    //   「在社区任务中,只要用户登录网站,脚本就自动领取奖励…奖励分为每日一次和每周一次。
    //     脚本需支持关闭自动领取功能,但默认开启。」
    //   → 功能本体 2.52.78 就写了(initTaskAutoClaim),但**一次都没领成过**:
    //     站点有两个 ajax 口,脚本把两者当成一个 ——
    //       job  = 申请(首页 p_<cid> 的 sh.png,hack/tasks/image/index.js)
    //       job2 = 领取(进行中页 both_<cid> 的 god.png,hack/tasks/image/newtasks.js)
    //     claim() 一律打 job ⇒ 每次都被顶回「您申请过[日常]还未完成,不需要重新申请!」,
    //     记成 rejected 收工 —— 控制台那行「扫描 1 枚按钮 / ok:0」就是它。
    //   → 本轮四件事:
    //     ① claim() 改成 job → job2 两步走,成败以 job2 为准;
    //     ② 扫描改成「任务首页 + 进行中页」取并集(原来首页有按钮就不看进行中页,
    //        卡在「已申请未领取」的 cid 永远扫不到);
    //     ③ 面板「功能开关」组补一枚可勾选行(此前只在两张只读登记表里,界面无入口);
    //     ④ 本地 24 小时冷却闸门 —— 站点对「日常」不设冷却(领完立刻又能申请),
    //        不自己上闸就成了每 3 小时薅一次 +2 SP 的刷分器。
    //   ★ 实测(2026-10-05,用户账号):job2 从非任务页发也照常生效(不看 Referer),
    //     无需 verify、GET 即可;日常当天即入账(完成时间 2026-10-05 16:39:14)。

    // ── 2.55.3 本轮一件事(2.55.2 交付后,用户附资源区卡片底部截图 + 红框)──────────────
    //   「对于无图床的卡片来说,整体放置的位置需要再靠底边一点。」
    //   → 无图床卡的「数据行」(收藏·已读·回复·热度)原本带 margin-bottom:14px,
    //     而有封面卡那条是 0 —— 实测「行底 → 卡底」无图卡 16.67px、有封面卡 2px,
    //     两类卡的数据行根本不在同一条线上。
    //   → 归零,两条规则改为逐字相同;改后 2.67px(差的那 0.67px 是站点给 .inner 的边框)。
    //   ★ 卡片高度由内容决定 ⇒ 无图卡随之矮 14px,卡底上收、数据行贴底,正是要的效果。

    // ── 2.55.2 本轮两件事(2.55.1 交付后,用户附资源区卡片底部截图 + 红框)──────────────
    //   ① 「有个小瑕疵,已读状态的水平中轴线没有和其他 UI 的水平中轴线对齐。」
    //      → 根因:已读胶囊与「收藏」同住站点那个 span[style="float:left"],而它是普通块盒子,
    //        里面两个行内级盒(收藏 25.33 高、胶囊 21.83 高)按**基线**对齐 ⇒
    //        胶囊的垂直中点比收藏低 2.53px、比「回复 / 热度」低 2.14px(实测数字)。
    //      → 修法:那一格改 flex + align-items:center,改后三者中点逐位相等(4565.44)。
    //        选择器额外挂 :has(.spp-read-chip) —— 没装胶囊的格子(茶馆 / 首页 / 未读卡)
    //        连盒模型都不动。
    //   ② 「在设置面板的清除功能中,增加一个『清除全部记录』的功能」
    //      → readLog.clearAll() + readWipeNow() + 面板第二枚按钮「清除全部记录」;
    //        整表清空不可撤销 ⇒ 两段式确认(第一次点=「确定清空?」,4 秒内再点才真清),
    //        刻意不用 confirm():面板里从来没有原生弹窗,被拦掉时会静默变成「按钮没反应」。

    // ── 2.55.1 本轮两件事(资源区卡片列表,用户五句原话)──────────────────────────
    //   ① 「我发现一和二的字体颜色为什么不一样?能不能让二的统一字体效果为一的效果?」
    //      → 根因是本脚本**自己造的**:标题里那层 <font color> 被 normalizeTitleInlineColor
    //        刷成白色(实测 44 张卡里 31 张带 <font>),而 <font> 是 <a> 的子元素、
    //        子元素自己的颜色压得过父级 ⇒ 那 31 张白、另外 13 张显示 <a> 上的热度色 #9AA5BB。
    //        现在非茶馆分支不再写热度色,与 <font> 层同为白色 ⇒ 两类卡一致(「统一为一号」)。
    //        热度色仍留在「回复 / 热度」两个数字上;茶馆一字不动(用户口径「不针对茶馆」)。
    //   ② 「增加一个功能,记录用户打开了哪些网页…仅对资源区生效」+「只有点击后才会显示
    //      『已读』,未点击则不显示」+「设定页面停留时长…默认数值为 5秒」+「两万条」+
    //      「清除指定时间之前的数据…单位可以是小时,也可以是日期」
    //      → 新模块 4b:GM 键 spp_read_log(上限两万条,超出按时间戳淘汰最旧);
    //        帖子页**实际可见停留**够秒数才记一笔(后台标签页不算停留);资源区列表里
    //        给已读的卡挂一枚「已读」胶囊(与「收藏」同排)。设计取舍见模块 4b 的长注释。
    //      → 设置面板新增「已读记录 (仅资源区)」组:开关(默认开)/ 判定停留秒数(默认 5)/
    //        清除阈值 + 单位(小时 / 天)/「清除记录」按钮(点完就地重绘,不刷新页面)。

    // ★★ 2.55.0 四件事(用户这一轮的三张截图 + 两条批注):
    //   ① 发帖那一行是三格(选项 / 编辑器 / 表情栏),行高由最高那格(表情栏)决定,
    //      而站点把编辑器那格写成 vertical-align:middle ⇒ 整段编辑器被垂直居中,
    //      标题上面空出 177px(用户黄框:「上方框选区域的留白是不需要的」)。
    //      三格一律 top:编辑器贴行顶、留白消失,表情栏也抬到行顶(两枚 UI 那行于是
    //      与标题行大致齐平 —— 正是上一条箭头批注要的)。
    //   ② 选择区:单行横向滚动 + 常态翻页 → **多行换行**,胶囊也放大(10px→11px、
    //      内边距 2/8→3/10);‹ › 与 i/n 退成兜底,超过 3 行才出现、按可视行竖着翻。
    //   ③ 网格行距:定高 flex 容器 + 默认 align-content(= stretch)会把行拉伸填满,
    //      行间于是成片空档(用户:「部分表情包的间隔是你人为加长的,我不需要」)——
    //      改 align-content: flex-start,行距回到 gap 的 2px。
    //   ④ 默认勾选:emoji 进「小圆脸」、贴纸进「滑稽」(CAT_DEF);类不在列表里时
    //      自动退回「全部」,不留白屏;换页也回到本页默认类。
    // ★★ 2.54.9 四件事(用户这一轮的批注 + 截图):
    //   ① 选择区(.spp-emoji-cats):legend 与网格之间多一条分类条 —— 两页各自列出
    //      自己有哪些类(emoji:小圆脸 / ´・ω・` / 颜アニ / 颜文字 / 杂项 / 贴吧表情;
    //      贴纸:滑稽 …… 以后加套系自动多一枚),外加一枚「全部」;张数写在名字右边。
    //      点一枚 = **勾选**(对号 + 强调色描边)= 只看这一类(**分页**),网格同时
    //      滚回顶(**跳转**)。选中具体一类时网格里不再重复那枚分组头。
    //      ★ 类的出处与网格同一处(NATIVE_PACKS / STICKER_PACKS)⇒ 加类不用改这里。
    //      ★ **翻页预案**(用户:「里面的选项可能会比较多,要做好支持翻页的准备」):
    //        选择区自己带一排纸 —— 一屏排不下时 ‹ › 与 i/n 自动出现,翻一页 = 一屏宽
    //        并对齐到胶囊左缘(不切半枚);类少时这三枚隐形,一行照旧全露。
    //      ★ 高度口径同步多算一条(catsH),网格不会把选择区压在底下。
    //   ② 大胶囊撤掉:legend 自己那层「999px + 强调色 10% 底 + 描边」整个删掉,只留透明
    //      容器;两枚 UI 各自只留 1px 实线描边,左一枚收左圆头、右一枚收右圆头,中间
    //      两笔重合成一条竖线 ⇒「左右两侧的实线」把它们连成一体(无底色)。
    //   ③ 工具栏「标」的介绍只留用户给的那一句「自动为所有超链接添加图床标签」
    //      (2.54.8 写长了,用户这一轮明确要短的)。
    //   ④ 帖子页底下那条翻页行:站点写了内联 margin-top:-.4em(-4.8px)把它塞进上一张
    //      帖子卡片的底缘里 ⇒ 卡片最后一道下边框被玻璃压住(用户:「下方的 UI 出现了
    //      折叠重叠的情况,往下移一移」)。现在用 !important 顶掉负外边距 + 留 8px 空隙。
    // ★★ 2.54.8 两件事(用户这一轮的批注):
    //   ① 表情栏按用户拍下的结构重做:
    //      · legend 里原本只有站点那两个字「表情」—— 现在换成两枚分类胶囊 emoji / 贴纸,
    //        占掉它那一行;下面那行独立的 .spp-emoji-tabs 整个撤掉(少 31px,全给网格)。
    //      · 分类要看得见:网格里每一类前面铺一枚满宽、滚动贴顶的分组头(站点原生 5 类各自的
    //        名字与张数、贴吧表情、贴纸的每个套系);两枚胶囊各挂一张提示卡说清该类包含什么。
    //      · 贴纸数据改成套系数组 STICKER_PACKS —— 以后加贴纸只往数组里添一项,
    //        分组头与张数自己就出来(用户:「后续还会增加贴纸种类」)。
    //      · 区域加大:高度口径改成「面板顶 → 所在单元格下缘」,上限 420 → 900,
    //        并撤掉编辑页那条把网格焊死在 316 的 CSS(实测 read.php 上网格 420 → 509)。
    //   ② 工具栏「标」按钮的文案按用户给的说法改:「自动为所有超链接添加图床标签」,
    //      并把会跳过的几类(已有标符 / 已在 [img][url] 里 / [code] 里 / 站内图片与附件)写进提示。
    //   ③ 工具栏撤掉「平」(左右平等 / [align=justify])那一枚 —— 居左 / 居中 / 居右照旧;
    //      预览里 [align=justify] 的规则留着(老帖子与站点原生按钮还在用)。
    // ★★ 2.54.7 一件事(用户:「列表的排序结构即为你提供的文档结构 —— 有序列表:空行列表、
    //   空行条目、紧排列表、紧排条目;无序列表:同上」):
    //   ① 列表那组改成**一类列表一行**,行内顺序 空行列表 → 空行条目 → 紧排列表 → 紧排条目,
    //      有序在前、无序在后(与段头那张表逐字一致)。上一版拆成「空行一行 / 紧排一行」,
    //      续行又没对齐 ⇒ 四行参差、还被读成两组。
    //   ② 续行对齐:新增标记 { brk: 文字, relabel: true } —— 建新行前补一枚与组标签同文本同
    //      样式的隐形占位(宽度天然相等,不用量、不写魔数),「无序列表」正对「有序列表」。
    //   ③ 断行时改收**上一行**的下边框(新行留默认边框):2.54.2 起收的是新行,那条线
    //      停在组内两条之间、整组末尾反倒没线 —— 与当初注释写的意图相反,一并修正。
    // ★★ 2.54.6 四件事(用户这一轮的批注):
    //   ① 工具栏撤掉整个「PHPWind 专有功能」板块(「隐」[hide] / 「售」[sell=] 两枚一起走)——
    //      第四行于是只剩「链接与媒体」,那道人造的竖分隔与共用逻辑一并删。
    //      预览里 [hide] / [sell] / [post] 三条黄条规则留着(老帖子还在用)。
    //   ② 悬浮提示卡:所有按钮 / 色板 / 图床项 / 多媒体档位 / 表格三框的原生 title 换成
    //      自绘卡片(SPP_TIP),鼠标停 180ms 弹在元素正下方,放不下翻上方,滚动即收。
    //   ③ 表情栏 legend 右边那个空胶囊 —— 站点「[更多]」那枚的顶层 span 整条撤出布局
    //      (原生 5 类早就是本栏 emoji 页的一部分,这枚链接没有任何用处)。
    //   ④ 表情栏收成 emoji / 贴纸 两页:emoji = 站点原生 5 类 + 贴吧表情;
    //      贴纸 = 滑稽整套(「滑稽表情」与「滑稽原图」本是同一张表的两条来路,并成一栏,
    //      主源自建 CDN、挂了自动换 jsdelivr 原图);搜索框整个撤掉。
    // ★★ 2.54.5 两件事(其中①的排布已在 2.54.7 被取代,见上):
    //   ① 列表那组当时按「空行 / 紧排」成对排成两枚一行:
    //        无序列表   空行列表  空行条目
    //                   紧排列表  紧排条目
    //        有序列表   空行列表  空行条目
    //                   紧排列表  紧排条目
    //      ★ 2.54.7 改回文档结构(一类一行、有序在前):
    //        有序列表   1. 空行列表 1. 空行条目 1. 紧排列表 1. 紧排条目
    //        无序列表   • 空行列表 • 空行条目 • 紧排列表 • 紧排条目
    //      小标签从「无序 / 有序」改成全称「无序列表 / 有序列表」,组标签收成「列表」。
    //   ② 修掉 2.54.4 段落注释里两个反引号(check-backticks 连块注释里的都算)——换成「」。
    // ★★ 2.54.4 一件事(用户:「移除:回复可见这个功能,理由:网页不支持」):
    //   工具栏撤掉「回复可见」那枚(插 [post] 的)—— 它是 2.52.91 照着站点原生 #wy_post
    //   补进来的,用户实测站点不吃这枚标签。本组只留「隐」([hide]) 与「售」([sell=])。
    //   预览里 [post] 的黄条规则留着(只为老帖子 / 站点原生按钮留下的内容),注释已写明。
    // ★★ 2.54.3 两件事,冲着用户「字号按钮:功能正常,但是预览渲染失效」——
    //   真机复现:打字 AAA → 点「字号」→ 打字 5 → 打字 BBB ⇒ AAA[size=5BBB][/size]
    //   (光标停在参数里,接着打的字全进了标签 ⇒ 预览与服务端都当字面文字)。
    //   ① insertTag 加 bodyCaret:「字号」这类参数可选的按钮把光标放进标签体内
    //      ([size=4]|[/size]),与 B/I/U 一致、点完直接打字;「售」那种必须填参数的照旧选中参数。
    //   ② 预览的 [size] 改吐 <font size="N">(服务端原样),不再用自编的 px 表 ——
    //      实测浏览器映射 1:12 2:13 3:16 4:18 5:24 6:32 7:48,服务端把 7/8/99 夹成 size="6"。
    // ★★ 2.54.2 一件事:列表那组的排布(用户截图 + 「两枚一行」的批注)。
    //   8 枚原来一溜排在一行里、交给 flex 自己换行 ⇒ 有序那组的第 4 枚被挤到下一行
    //   (截图里那根箭头)。现在由脚本显式断行,无序 / 有序各占两行:
    //     列表(空行/紧排) 无序 • 空行列表 • 紧排列表
    //                           • 空行条目 • 紧排条目
    //                     有序 1. 空行列表 1. 紧排列表
    //                          1. 空行条目 1. 紧排条目
    //   实现:按钮定义支持 { sub: 文字 }(行内小标签)、{ brk: 文字 }(换行 + 小标签)、
    //   { brk: 文字, relabel: true }(换行 + 与组标签等宽的隐形占位,续行对齐用),渲染
    //   循环里处理。★ 2.54.7 修:断行时收的应是**上一行**的下边框 —— 2.54.2 收的是新行,
    //   那道线于是落在组内两条之间、整组末尾反倒没线(见「运行版本标记」段头)。
    //   按钮名字、标签写法、点击行为一个字没动。
    // ★★ 2.54.1 五件事,冲着用户对 2.54.0 的四条反馈:
    //   ①「实时预览不认新标签」:模块 12 的 bbcodeToHTML 原来只认老写法([list] 还是按 [*]
    //      切的),[li] / [list=1] / [table] / [blockquote] 全没写 ⇒ 工具栏插出来的东西在
    //      实时预览里是裸标签。本轮补齐:列表(含 [list=a]/[list=A] 与老 [*])、[blockquote]、
    //      [table=宽]+[tr]+[td]、[fly]/[move]、[flash|wmv|rm=…] 播放占位、[hr]/[sup]/[sub]/
    //      [backcolor](连同预览样式)。
    //   ② 列表按钮重做(用户:「增加一个是否空行的选项」):新增独立的「列表」组,8 枚 ——
    //      无序/有序 × 空行列表/紧排列表/空行条目/紧排条目;整段的占位**只放一条**
    //      [li]条目[/li](原来那三条撤了)。
    //      ★ 实测:服务端把 \n 翻成 <br> —— 换行落在两条 <li> 之间就是「空行」,
    //        紧排则一行到底(一个 <br> 都没有)。
    //   ③ 编辑页选项表「没对齐」:那条 calc(50% - 346px) 是引用页的口径,而 spp-quote-on
    //      在 action-modify 页也打 ⇒ 被推右 274px。改成读 syncPostFill 量的 --spp-opt-indent。
    //   ④「直接删除」没有皮肤:它没有 name,提交钮那套按 [name="Submit"] 挑的挑不到。
    //      补一条按 value 认的危险色胶囊。
    //   ⑤ 图片大小仍然一个字没动。
    //   ⑥ 图片链接一律包 [img](用户:「p.inari.site/…webp 点补标符变成了【Inari】…,
    //      实际上我需要 [img]…[/img]」):判定从「站内附件图片」放宽到「任意带图片后缀的
    //      链接」—— 六个图床、任意外站、站内附件一视同仁;旧的那枚【站点名】照样先摘掉。
    //      非图片链接(read.php / .mp4 / 站内 zip 附件)行为不变。
    //   ⑦ 选区首尾的空白搬到标签外面(用户:「整行选中 URL 点图 ⇒ [img]url\n[/img],
    //      应该是 [img]url[/img]」):新增 splitSelEdge(),insertTag / insertMediaTag /
    //      insertLink / 列表整段四处一起改;全空白的选择退化成原样包(不复制空白)。
    // ★★ 2.54.0 五件事,冲着用户这一轮的批注:「修复列表功能/引修改为引文、增加块引/
    //   增加表格(输入框+表格按钮 UI,n*n)/修复、补充:多媒体、跑马灯/图片大小功能不需要」。
    //   ① 列表修好:两枚原来都是坏的 —— [*] 与 [#1] 服务端当字面文字吐出来,真正认的是
    //      [li]…[/li](有序列表也用它,外层 [list] / [list=1])。
    //   ② 「引」→「引文」([quote]),新增「块引」([blockquote]);实测是字面文字的「引@」
    //      ([quote=某人])与死掉的「折」([sp])一起撤。
    //   ③ 新增「表」:右边三个输入框填「行 × 列 宽」,一点铺一张空表,格子占位 A1 / B1 / A2…
    //      (宽度留空 = 站点默认 98%)。
    //   ④ 「媒」从死标签 [media] 换成站点真认的四种:flash / wmv / wmv 单参(mp3 档) / rm,
    //      长宽与自动播放自己填(写法照抄站点原生弹窗的 insertmedia())。
    //      ★ 那三个播放器靠 NPAPI 插件,现代浏览器早不支持 ⇒ 多半只有占位不出画面,
    //        是站点自己的限制。
    //   ⑤ 跑马灯补两枚:「飞」[fly](来回)、「移」[move](单向右滚)。
    //   ⑥ 图片大小一个字没动(用户说不需要);顺手把标注器的开口标签规矩收进 [wmv= / [rm=。
    // ★★ 2.53.99 四件事,冲着用户报的「站内附件被标成【Blue-plus】」——
    //   「输入文本:blue-plus.net/attachment/…jpg 自动变成:【Blue-plus】…」+
    //   「正确的标注手段是:[img]…[/img]」。
    //   ① 站内附件分两类:图片附件 ⇒ 包成 [img]…[/img](新开关 spp_self_img,默认开启);
    //      ★ 2.55.13 起:这一枚从面板撤掉、改成硬性常开,键 spp_self_img 成了孤儿键(见本轮说明)。
    //      其它附件(zip / rar 等)⇒ 不碰。域名按南+ 域族(头部那 12 个 @match 域)判。
    //      ★ 只针对附件 —— 站内普通链接(read.php?tid=… 这类)照旧补【站点名】,
    //        外站的 /attachment/ 也照旧补。
    //   ② 顺手摘误标:`【站点名】` 紧贴站内附件链接前面、且正好是本函数会生成的那一枚时删掉它。
    //   ③ 已经在 [img]/[media]/[url] 里、[code] 里、HTML 标签里的一律不动 ⇒ 包过一遍再点也幂等。
    //   ④ 提示语一次说全:「已给 N 条链接补上标符,已把 M 条站内附件包成 [img],摘掉 K 条旧误标」
    //      /「没有可标注的链接:站内附件 N 条、已有标符 N 条、…」。
    // ★★ 2.53.98 四件事,都冲着用户那句「点击自动补全按钮后,没有标注」——
    //   ① 认链放开到**没写协议头的裸域名**:用户给的测试串
    //      `blue-plus.net/read.php?tid-2977015-page-1.com` 这种现在也补标符。TLD 走白名单,
    //      read.php / 2.53.98 / a.jpg 这些形状相似的照样不认;邮箱、路径片段同样不认。
    //      ★ 裸域名要先补 https:// 再喂 new URL —— 否则被当相对路径、解析成当前站点域名。
    //   ② 一条都没补时把原因分类报出来:已有标符 / 在 [img][url] 标签里 / 在 [code] 里 /
    //      在 HTML 标签里 / 输入框里没找到链接(原先只有一句「没有需要标注的链接」)。
    //   ③ 收掉一条过宽的跳过:原先「紧跟引号 / 等号」也当属性值跳,正文里写
    //      `链接=https://…`、`他说"https://…` 会被误跳过;HTML 标签那一条已完整覆盖
    //      `href="…"`,这条单独判只剩误伤。
    //   ④ 工具栏找编辑器加兜底:`textarea[name=atc_content]` 取不到就退回 `#textarea`,
    //      两边都没有就早退(原先直接取 dataset,取不到会抛错)。
    // ★★ 2.53.97 两件事 ——
    //   ① 表情格:用户「能否屏蔽贴吧表情?即鼠标移上去时不显示文本框?」。那个「文本框」是
    //      **浏览器对 img[title] 的原生提示**(14 模块 8423 / 8503 两处写了 title=表情名)。
    //      ⇒ 两处改成只写 alt;「default」那一栏克隆站点自己的 img、title 会被一起带进来,
    //        另外 removeAttribute('title');名称数据一处没动(搜索按 name 过滤、兜底走 alt)。
    //      ⇒ 新开关 spp_emoji_tip(面板「功能开关」组,默认关闭):勾上才把名称写回 title。
    //      ★ 2.55.14 起:默认值翻成**开**,且弹的不再是原生 title,而是脚本自绘的提示卡
    //        (原生灰框没法美化);老存储里那条 false 的一次性迁移见本轮说明。
    //   ② 补标符放开:用户「只要检测到超链接,便会自动添加。不局限于我提供的那几个图床网址」
    //      ⇒ 认名先查 IMG_HOSTS 那六个(厘图片 / 猫箱 / Inari / Ipfs / Filegarden / Urusai),
    //        查不到按域名推(siteNameOfHost:去子域与公共后缀、首字母大写),
    //        例 ztube.org → Ztube、www.yuepmaa.space → Yuepmaa、yodhcn.pages.dev → Yodhcn。
    //      ⇒ 同时补两条「不碰」:URL 在 HTML 标签内部、或紧跟引号 / 等号(属性值)一律跳过。
    //      ⇒ 按钮与提示文案同步(「已给 N 条链接补上标符」);开关 spp_host_mark 仍是默认开。
    //      ★ 2.55.13 起:这一枚的默认值翻成 **关**(含老用户存储的一次性迁移,见本轮说明)。
    // ★★ 2.53.96(用户:「对于输入框,我希望在该板块增加两个功能:第一个是自动为输入框内
    //   未进行图床标注的所有链接自动添加图床标符。第二个需求是增加一个UI组件:点击后显示
    //   下拉框,其中包含多个图床选项,点击即可跳转。」):编辑器工具栏「链接与媒体」组加两件事 ——
    //   ① 「标」按钮 + 粘贴钩子:在**没标注过**的图床链接前面补一枚【图床名】
    //      (厘图片 / 猫箱 / Inari / Ipfs / Filegarden / Urusai,拉丁名首字母大写)。
    //      三条不碰的规矩:已有同一枚标符(幂等)/紧跟 [img] [media] [url] [url= 开口标签
    //      (会把标符当图片地址)/[code] 块里的。
    //   ② 「图床 ▾」按钮 = 下拉框,六项,点一项即在新标签页打开该图床首页。
    //   ③ 新开关 spp_host_mark(设置面板「功能开关」组,默认开启)只管「自动」那一半;
    //      关掉后手动那枚按钮照旧可用。
    // ★★ 2.53.95(用户:「针对链接或图片,如果数量过多,面板会显示大量结果。最好增加翻页功能,
    //   例如每页显示 15 个条目。」+「如果能在设置面板里让用户自定义这个「15」这个数字,也请增加
    //   这个功能。」):含图楼层面板的两栏**各自分页** ——
    //   ① 每页行数读 spp_idx_per(面板「数值设置」新增的第四枚 number 行,默认 15,可填 5-60)。
    //   ② 只有一页可看时翻页条整条不出现;超过一页才在列表下面出现「‹ 1/3 · 共42层 ›」,
    //      到边界就地置灰。
    //   ③ 两栏页码各记各的(翻含图栏不影响链接栏);页码进了重绘键,所以点 ‹ › 一定重建。
    //   ④ 切片与页码收敛走纯函数 pgSpan(n, per, cur):行数变少时旧页码会被夹回合法范围,
    //      不会画出一片空白。
    //   ⑤ 翻页后该栏回顶部;普通重绘仍旧保持原来的滚动位置与「正在输入的那个框」(2.53.93 的现场保持)。
    //   ⑥ 分页只改面板铺几行,**不改抓取范围** —— 索引该抓多少页还抓多少页。
    // ★★ 2.53.94(用户:「这个功能是默认固定的,无需在设置面板中单独勾选开启。但需要勾选另一个
    //   关联功能,以决定该悬贴定位功能是否自动激活。」+「默认不会自动激活」):
    //   两枚开关的职责对调 ——
    //   ① **撤掉** 2.53.93 那枚「全贴索引(图 / 链跨页定位)」(spp_idx_all):功能本体固定存在,
    //      面板底部那两枚「全贴索引」按钮永远在,不再由任何开关决定它出不出现。
    //   ② **新增**「打开帖子自动索引全贴(含图 / 链接跨页定位)」(spp_idx_auto,**默认关闭**):
    //      它只回答一个问题 —— 打开帖子时要不要自动激活全贴定位(先吃缓存、缺的页才逐页抓,
    //      并把视图切到全贴)。不勾 ⇒ 一个请求都不发,纯手动;勾上 ⇒ 缓存已全时连一页都不抓。
    //   ③ 起一轮的动作抽成 idxStart(),按钮与自动两条路共用;自动入口 idxAuto() 在一页都不用抓时
    //      只切视图、不发请求。
    //   ④ 跳到另一页后面板**继续保持全贴视图**(用户点头的那个岔口):那是顺着刚才那一下点击,
    //      不是自动激活,且索引就在缓存里、不花一个请求。
    // ★★ 2.53.93(用户:「能不能直接定位本贴所有的链接及图片位置?既然能通过点击一键复制获得整个
    //   帖子的链接,那么应该也能定位到整个帖子的图片位置、楼层以及链接位置、楼层。」+ 批准执行):
    //   含图 / 链接面板长出第三层能力 —— 全贴索引与跨页定位。
    //   ① 面板两栏各挂一枚「全贴索引」按钮:逐页抓完整贴(复用「复制本贴」那台机器),边抓边填、
    //      显示进度、再点一次即停;结果按 tid 存 sessionStorage,换页回来、下次再点都是秒出。
    //      抓过的页也能被「复制本贴」直接复用(两处扫同一份文档,索引是顺手收的)。
    //   ② 全贴模式下两栏按页分组(吸顶的「第 N 页」条),非本页的行左缘一条淡绿。
    //   ③ 定位精确到条/张:含图行填「第 k 张」跳到那一张图(原本就有),链接行新补「第 k 条」
    //      跳到那一条链接;跨页的行点一下直接换页并滚到落点,命中处 2.6 秒绿框高亮。
    //   ★ 跨页寻址用 **URL 指纹**(绝对化后的 src / href),不用序号 —— img 的尺寸判据在 fetch
    //     回来的文档里恒为 0(那份文档没渲染过),两边的「第 k 张」不是一个东西;指纹不会错位。
    //   ★ 落点不交给 location.hash:#read_<pid> 在折叠的 mark 楼里是 display:none(真机 5 层
    //     都是),浏览器滚不过去 ⇒ hash 只当加速,真正的落点由目标页 consumeJump() 负责,落完抹掉。
    //   ★ 真机取证:每层唯一 pid(GF 是字面上的 tpc);附件图在 #att_<id> 里 —— 在 .tpc_content
    //     内但不在 #read_<pid> 内,表情图反过来 ⇒ 取数范围是整个 .tpc_content 再过滤。
    //   ★ 新开关「全贴索引(图 / 链跨页定位)」默认开启,可在悬浮设置面板关闭(关掉按钮不出现)。
    //   ★ 落盘后自查补的一条(同版本原地改):索引里也收**当前页**那一份 —— 只收其它页的话,
    //     在第 1 页点完全贴索引、跳到第 2 页之后,第 1 页的楼层会整片消失。
    //   ★ 同批还有一条:IntersectionObserver 只挂「本页真有元素」的行 —— 跨页行没有 row,
    //     observe(null) 会抛错并把整条 bindObserver 打断(滚到哪高亮哪一起哑)。
    //     在第 1 页点完全贴索引、跳到第 2 页之后,第 1 页的楼层会整片消失。
    // ★ 2.53.92(用户:「算了,回滚版本」):撤销 2.53.91 —— 红点「等线速度」(三角点周期
    //   3s → 1.8s)连同那一版顺手修的「方块描边写死 3s」一起回退,内容回到 2.53.90:
    //   三个红点同为 3s 周期、三角点按原件慢 1.66 倍;描边仍写死 3s(--duration 本就是 3s,无差别)。
    //   2.53.89/90 的抗 ADGuard 一套(影子根 · 宿主换血 · 内联锁 · 兜底重挂)与
    //   「默认桃花粉 / 下半区悬停变绿」全部保留。
    // ★ 2.53.90(2.53.89 活体验收抓到的漏):模拟拦截器打 [data-spp-brand]{height:0!important}
    //   时,影子根里的内容没被打到,但宿主被压成 0 高 + 自身 overflow:hidden ⇒ 整条被裁没。
    //   ⇒ 宿主补 height:auto 锁(auto 本就是内容撑高,锁它不影响条长高),并加锁
    //     zoom / scale / rotate / translate / clip / content-visibility。
    // ★ 2.53.89(用户:「默认为粉色,触发后改为绿色」+「避免 ADGuard 插件将其识别为广告并屏蔽」):
    //   ① 两色对调:默认 = 桃花粉实心 + 粉辉光(扫光层),下半区悬停 = 绿实心(actual 层)。
    //   ② 抗识别三招:内容与样式全进**影子根**(页面级 CSS 穿不过 shadow 边界);
    //      宿主换血(另起无 id/class 的空白 div 顶替站点广告容器,按位置写死的老规则落空);
    //      宿主样式内联 !important + 图的广告指纹抹掉 + MutationObserver 兜底重挂(≤3 次)。
    //      从此本模块不往页面注入任何全局样式。
    // ★ 2.53.88(用户:「那个绿色的竖线能不能也去掉?」):竖线 = 扫光层的 border-right,
    //   原件把宽度写成变量 --border-right ⇒ 6px 改 0px 即可,扫光/过渡/字形位置全不动。
    // ★ 2.53.87(用户:「我说的粉色是指桃花粉」+「那个字体也不用变成空心了,改成实心的」):
    //   褪色态 = 效果面里那层 .spp-brand__actual,只动它两个值 —— 描边/填充色白 → 桃花粉
    //   #FFA3B8(暖调浅粉,alpha 0.95),填充从 transparent 点亮成同色 ⇒ 空心变实心;
    //   同色 1px 描边照旧继承,粉字与绿字粗细一致。形态以外一律未动。
    // ★ 2.53.86(用户:「使其褪色时变为粉色」):褪色态 = 效果面里那层 .spp-brand__actual,
    //   它自己没有声明,描边色只来自按钮上的 --text-stroke-color ⇒ 单变量改动,
    //   白 → rgba(255,95,191,0.92)。形态、过渡、翻面、上下分区一字未动。
    // ★ 2.53.85(用户:「翻页时有点困难 ⇒ 分为上下两个区域:上方划过触发翻页,下方只触发
    //   褪色」):2.53.84 把翻面与褪色挂在同一个 hover 上,两件事一起动、互相抢。
    //   ⇒ 中缝外面套 .spp-brand__stage,上面压两块透明感应区,排在卡片**前面**、
    //     用兄弟选择器 ~ 驱动卡片:上半区翻转(--top),下半区只褪色(--bot),
    //     分界 --zd = 整组上边距 + 图标行带 + 半个行距(fit() 按同一套常数算)。
    // ★ 2.53.84(2.53.83 活体验收抓到的真错):褪色原来只挂 .spp-brand__btn:hover ——
    //   卡片一转,效果面背对观众、Chrome 不再命中测试它 ⇒ 按钮 :hover 失效、绿字弹回 100%。
    //   ⇒ 触发面提到整张翻面卡(.spp-brand__flip:hover),褪色与翻面同时发生。
    // ★ 2.53.83(用户:「默认显示为绿色高亮,鼠标悬停时才变为褪色效果」+「整体增加一个翻面
    //   效果:正面封面图、反面我做的效果」+「三张图片可以等比例放大、之间保留间距」):
    //   ① 扫光层两个状态对调:默认 width:100% + 辉光,悬停收到 0(0.5s、6px 光标条、描边不动)。
    //   ② 中缝改成 Y 轴翻面卡(0.8s):默认朝观众 = 效果面(绿字默认可见),悬停翻到封面面。
    //   ③ 条高不再写死:H = (W − 2·SP) ÷ (4.6875×2 + 3.3333),W=1398/SP=40 ⇒ 103;
    //      三图高度全等(等比放大),宽 = 比例 × H,图间固定 40px ⇒ 恰好铺满整条。
    // ★ 2.53.82(真根因):refetch() 里那句 fit() 够不着 —— fit 是 build() 内部的函数
    //   声明,refetch 在模块作用域,调它就是 ReferenceError;那句又恰好在同一个 try 里,
    //   被 catch 吞掉后转头走 fail() ⇒ 把刚取回来的图 display:none(2.53.80/81 都翻在这)。
    //   ⇒ refetch(im, idx, onOk, onFail):onOk 绑在 img 的 load 上(那一刻必有像素),
    //     两个回调都是 build() 里的闭包,fit() 在它们的作用域里。
    // ★ 2.53.81(2.53.80 真机复测):救回来的图被「收占位」按下去了 —— 收尾竞态。
    //   实测:refetch 按 ART_SRC 兜底取回 blob(naturalWidth 450 已解码),但同一 img 上
    //   旧加载中止触发的异步 error 又落到刚赋值、还没解码的那一刻,naturalWidth 读成 0,
    //   give 就把那一侧 display:none 了。
    //   ① give 先自检:仍无像素才收占位,已有像素只重算尺寸;② step 加一次性闸门。
    // ★ 2.53.80(用户:「右侧的图片还是被当成广告屏蔽掉了。」):屏蔽器两条路分别对付 ——
    //   ①外观过滤(元素还在、被 display:none 之类藏住)⇒ 内联 !important 顶回去;
    //   ②网络过滤(字节没下来)⇒ GM_xmlhttpRequest 自取换成 blob: URL,屏蔽器按文件名
    //     匹配的规则不再命中;连 src 被改写成 data:/about:blank 时按左右位置回落到
    //     ART_SRC(/images/sswg9.jpg、/images/segucrwj4.jpg)。
    //   两条都救不回来 ⇒ 收掉那一侧占位(.spp-brand__art--none),不留空黑带,中缝吃掉余宽。
    //   入口同时放宽:原来要两张图都在才动手,现在至少一张就搭条。
    // ★ 2.53.79(用户:「将背景区域改为黑玻璃磨砂背景,不透光。」):#spp-brand 的底换掉 ——
    //   纯黑不透明底 + 两层错位细白点阵(磨砂颗粒)+ 顶部玻璃高光,边框换成中性玻璃边;
    //   旧底那层蓝黑渐变与中心绿雾撤掉,背后星野透不上来。顶部绿色发丝线保留。
    //   没用 backdrop-filter:底色不透光,背后没东西可糊,纯白耗 GPU。
    // ★ 2.53.78(真机验收抓到的第二处真错):南+ 把 text-align:center 与「长词可断」的
    //   overflow-wrap 一起继承进按钮 —— 扫光层内容盒被 border-right:6px 咬掉 6px,
    //   于是亮字在字里断行 + 居中溢出把整行左移(实测底字 695.2 / 亮字 713.1,错位 17.9px,
    //   正是用户看到的「亮字与底字不重合」)。
    //   ⇒ 给自己的类钉上 white-space:nowrap / text-align:left;只溢出不断行,
    //     溢出的 6px 落在尾随 &nbsp; 上被 overflow:hidden 吃掉,可见字形不丢。
    // ★ 2.53.77(2.53.76 活体验收抓到的真错):fit() 里竖向预算的条高读错了对象 ——
    //   mid 是 #spp-brand 的 flex 项、没写 height,高度由内容撑开,拿 mid.clientHeight
    //   当条高会自己跟自己较劲(实测收敛到 71.4,整套 --ik/字号都小一号)。
    //   改读 strip.offsetHeight(=96,含边框),竖向预算回到 0.90 × 96 = 86.4。
    // 用户:「OK,修改脚本。」(品牌条中缝换成两段 Uiverse 原件)
    // ★ 上行 = white-newt-56 的三图标 loader(□ △ ○ 描边 + 红点),下行 = quick-chicken-50 的
    //   扫光字按钮(空心描边,hover 绿色扫光 + 外发光)。两段原件的 CSS 属性值一字未改,
    //   只做三件集成动作:选择器加 #spp-brand 作用域、keyframes 加 sppBrand 前缀防碰撞、
    //   类名带 spp 前缀。动画时间/缓动曲线/描边/虚线数组/hover 效果全部逐字保留。
    // ★ South-Plus 里必须带词连接符 U+2060:原件正文 uiverse 没有断行点,而连字符是断行点,
    //   悬停层 border-box 又被 border-right:6px 咬掉 6px ⇒ 会在 "-" 处折成两行、亮字与底字错位
    //   (样稿实测 62.3px)。加词连接符后两份字形矩形完全一致。
    // ★ 竖向方程:上带 + 间距 + 下带 = 0.90 × 条高、上带 = 0.90 × 下带(上小下大);
    //   96px 条 ⇒ 上带 39.5(--ik .898) + 间距 3 + 下带 43.9(字号 37.9) = 86.4。
    // ★ 上面描边恢复原件白色 --path:#fff(2.53.76 之前误改成过品牌蓝)。
    // 用户:「含图楼层悬浮窗 这个默认关闭」
    // ★ 查账:它的默认值早在 2.53.70 就已经是 false,四处同值(SPP_PANEL_DEFS / buildOverlay
    //   / 运行时 toggle / 守卫 EXPECT)—— 病根是**存储里那条陈旧的 true**:2.53.68 之前
    //   这个键是「整块面板建不建」的总开关、默认值恰恰是 true,翻语义时没人动过已落盘的值,
    //   于是 2.53.70 之前跑过脚本的机器上它一直是 true ⇒ 悬浮窗照旧常显。
    // ★ 本轮:migrateSettings() 加一次性迁移(标记键 spp_mig_imgidx_off)——第一次跑到
    //   本版时把它写回 false;此后用户在面板里怎么勾就是怎么算,迁移不再跑。
    // ★ 2.53.74:2.53.73 把条宽换成 #main 实测宽后,条比内容列**宽 1.3px**(站点没有全局
    //   box-sizing:border-box,那 1px 边框算在 width:100% 之外)—— 真机读数 条 0..1386.0、
    //   #main 0..1384.7。用户这轮要的就是「向两侧边框对齐」⇒ 宿主与 #spp-brand 各补一句
    //   box-sizing:border-box,尺寸一律不动。
    // 用户(附真机截图,蓝框圈着品牌条左右那两条黑带):「将广告区域向两侧边框对齐,
    //   中间空出的区域用于为字体腾出更多空间。」
    // ★ 旧账(2.53.72 真机,viewport 1385):#main 1384.7 从 x=0 起,而条被写死的 1260 钉住、
    //   居中成 62.3..1322.3 ⇒ 左右各空 62.3 / 61.0 的黑带(图与条沿本身是齐的,各 0.7 = 边框)。
    // ★ 本轮:① 条宽上限从写死的 1260 改成 #main 实测宽 ⇒ 左右沿与内容列两侧边框对齐;
    //   ② 中缝随之变宽(1385 时 360 → 483),字号上限 32 → 48,多出来的宽度自动吃进字里
    //   (1400 宽时中缝 498、可用 458、字号 48、整词 429.84、余量 28.16)。
    // 用户:「这两张图片是广告。你需要先去除它们的广告标识符,然后将右侧的一部分宽边移至右边
    //   侧边,中间空出的区域用来显示内容(south-plus、南+ 等字样;可以添加动态效果)。」
    //   随后三轮样稿定稿:「南+ 以及那个框选的 UI 都不要了;取消字体滚动,改为常驻,
    //   确保字体和内容显示完整」+「可以在设置面板把这个板块关闭,默认是开启的。」
    // ★ 新增模块 15m(initBrandBar)+ 第十三枚 bool 行 spp_brand_bar(def: true):
    //   页眉那两块淘宝广告(/images/sswg9.jpg、/images/segucrwj4.jpg,各 450x96)外层
    //   整条 <a>(taobao/tmall 跳转 + onclick 的 ga(...,'ad'))拆掉,只留图当装饰;
    //   两图固定在左右两端,中间留白放一行常驻的 SOUTH-PLUS(无徽标、无滚动)。
    // ★ 尺寸预算:条宽 = min(1260, #main 宽),中缝 = 条宽 − 900 − 2;
    //   整词宽 = 8.955 × 字号(字距 .30em,线上实测 25px→223.88 / 32px→286.55),
    //   字号 = min(32, (中缝 − 40 − 16) / 8.955) ⇒ 条 1260 时整词 286.55、余量 32.1,
    //   窄屏自动降号,整词永不裁切。
    // 用户(附面板截图):「红色框选的区域比较多余,能否去掉?」+
    //   「适当调整区间,增大含图链接与展开按钮之间的间隔。」
    // ★ 旧账:展开态 174 里,两枚页签在内衬框 154.67 中居中(左 25 / 右 25),而折叠按钮 absolute
    //   钉在右端 ⇒ 页签右沿与按钮左沿只差 1px(「+ 贴在链接上」),左边 25px 又纯是空的。
    // ⇒ 表头给按钮留 24px 跑道(按钮 22 + 2 余量),页签在剩下的 126.67 里居中:左 13 / 右 13。
    //   跑道只在按钮真在场的那两档生效(收起态 74 宽的条里按钮是 display:none,扣 24 会挤掉页签)。
    // 用户(附两张面板截图):「含图楼层悬浮窗,这个功能默认为隐藏,即关闭状态。」
    //   +「我需要有一部分功能隐藏到侧边栏里,但你这个没有实现这一点。」
    // ① 默认值 true → **false**:默认不勾选 = 默认就处在收起状态(设置表 / 手写 HTML / toggle
    //    / 守卫 EXPECT 四处同步)。
    // ② 「收起」要真藏:2.53.51 起它只是把面板收窄成 --spp-ii-ws(两枚页签全都在),等于什么都没藏。
    //    现在收起态只留**当前那一枚页签**(绿点 + 含图 / 链接),非当前那枚连同列表、折叠按钮
    //    一起留在屏幕外;条宽也从「两枚全字所需」改成「一枚页签贴边所需」--spp-ii-ws1
    //    (fitWidth() 现量真页签,量不到退 74)。悬停 / 聚焦照旧整块滑回来。
    // 用户(附面板截图):「它的『含图』和『链接』这两个框似乎也没有居中」的收尾 ——
    //   2.53.68 修好了横向(174 档 25/25、窄条档 2/2)与折叠态的竖直(9/9);
    //   本轮补收起成侧边一条(未悬停)那一档:表头那 7px 下外边距让上下变成 9 / 14,
    //   ⇒ 收起时 margin-bottom:0 + 下内衬 9,上下各 9;悬停展开时让位(列表要出来)。
    // ① 2.53.67 的居中只做对一半:2.53.66 插进折叠按钮规则的那句 position:relative 写在
    //    position:absolute **后面**,把它吃掉了 ⇒ 按钮仍在流里占着表头宽度,justify-content
    //    没有余量可分(真机量到 gapLeft=2 / gapRight=48)。本轮删掉那句多余的声明。
    // ② 用户(附悬浮设置面板截图):「脚本悬浮窗设置面板中『含图楼层悬浮窗』我的本意是:
    //    勾选它,它就会自动显示;不勾选,它就会自动隐藏到侧边栏」
    //    2.53.32 的「含图楼层靠边自动隐藏」与 2.53.36 的「含图楼层悬浮窗」总开关本是两枚**打架**的
    //    勾选项 ⇒ 并成一枚(键沿用 spp_imgidx_show,def: true):勾选 = 常显;不勾选 = 收起成侧边一条。
    //    随之撤掉 initImageIndex 门口那句「不勾选就不建面板」,.spp-ii--edge 改由它取反驱动;
    //    设置表 / 手写 HTML / tools/verify/settings-panel.js 的 EXPECT 三处同步(老配置里的残留值没人读)。
    // 用户(2.53.66 交付后,附面板截图):「它的『含图』和『链接』这两个框似乎也没有居中」
    // ★ 旧账:表头剩的空隙被折叠按钮的 margin-left:auto 全吃在右边(展开档量到左 11.67 / 右 59.66),
    //   竖直方向是内衬 9 / 7、折叠态下表头下方无外边距 ⇒ 上 9 下 7。
    // ⇒ 表头 position:relative + justify-content:center;折叠按钮改绝对定位钉右端(right:0 +
    //   top:50% + margin-top:-11px)、不再参与居中;折叠态补 padding-bottom:9px。
    // ★ 代价与边界:靠边收起档悬停时面板从窄条涨回 174,页签会跟着往右挪(抽屉拉开);
    //   折叠按钮的图形(2.53.66 的两根短杠)、宽度阶梯、两栏功能都没动。
    // 用户(2.53.65 交付后,附折叠按钮截图):「那个加号没有位于灰底框 UI 的中间,
    //   而且可以稍微大一点(减号可以稍微小一点。),不要和减号的大小差异太多(可以有差异)」
    // ★ 上一条(2.53.65)只做出了盒子,符号还是字体里的 + 和 —:光心跟字体度量走(雅黑
    //   14px/line-height:1 下 + 的光心比盒子中心低约 0.7px),大小也天生不等宽(— 是 em 宽、
    //   + 只有半个),连盒子宽度都跟着字面在 26 与 22 之间跳。
    // ⇒ 符号改由 ::before / ::after 两根 2px 圆头短杠画:横杠两态共用(10×2 / 折叠态 12×2),
    //   竖杠 2×12 只在折叠态 ⇒ 加号 12×12、减号 10×2,同粗、长短差 1.2 倍。
    //   盒子定死 22×22(font-size:0 让字面不出墨,aria-label 顶上无障碍),
    //   居中改成算术:50% 落在内衬框(20px)⇒ border-box 恰为 11 = 中心,负 margin 各退半个自身
    //   ⇒ 光心与盒子中心重合在整数像素上,与字体字号无关。
    // ★ 顺带修:`.spp-ii--edge` 悬停那档原来是 display:inline(会忽略宽高、把盒子塌成一行内联),
    //   字形时代看不出来,杠时代会散架 ⇒ 改 inline-flex。零 JS 改动(applyTab 一字未动)。
    // 用户(2.53.64 交付后,附「含图 / 链接」两页签截图 · read.php?tid-2717095):
    //   ①「去掉只显示单字的选项,含图和链接这两个字显示全,空间是足够的。」
    //   ②(绿框圈着折叠按钮)「它的折叠按钮不是很明显,点击区域也稍微有点小。」
    // ★ 单字那一档是 2.53.51 加的(「灰色项应显示为图或链单字」),2.53.52 先删掉「两枚都单字」
    //   那半档、只留「非当前那枚单字」,下限 96 → 108;本轮把最后半档也判掉:
    //   页签永远写全「含图 / 链接」,下限 108 → **128**(窄内衬下两枚全字所需;宽内衬 138)。
    //   随之删掉:两个短字面 span、三条单字 CSS、narrow2 那个类与它的切换、II_W_HEAD1 常量。
    //   代价记在宽度上(比原来宽 20px,仍远窄于展开态 174),不碰贴纸块那条 2.53.52 判据。
    // ★ 折叠标记原来是 12px 灰字(— / +),没有盒子 —— 既不像按钮、命中区也只有字那么大,
    //   而它又是唯一的折叠入口(标题栏点了只切页签)⇒ 改成 22×22 的按钮盒子(与页签同高)、
    //   浅描边 + 深底、字形 14px/700,hover 绿边绿字;命中区 = 盒子本身。
    // ★ 未动:`.spp-ii--edge`(靠边自动隐藏:收起 96px / 展开恒 174)、宽度上限 174、
    //   「没地方时反而更窄」那条判据,以及链接/含图两栏的功能本体。
    // 用户(2.53.63 交付后):「5;纯 4 位裸码规则不留,必须要和链接搭配生效」(同一批还点了
    //   tmp 陈旧件清除 —— 含那条规则的审核页 tmp/r1/srv/preview-linkpanel.html)。
    // ★ 这是那条规则(2.53.50 起)的第三次收口:
    //   2.53.51 一层无链接 ⇒ 整层的码作废;2.53.52 售价条 8550 误收靠跳过 h6.quote 特判;
    //   本版把「没人认领的码」(loose)整档取消 ⇒ 输出里只剩被链接认领的码。
    //   于是单枚裸码不再自己成行(「本层无链接」那种行彻底没了),售价条那类误收由规则挡掉。
    // ★ 裸码识别本身**保留**(CODE_BARE 不删)—— 用户要的是「搭配链接才生效」:
    //   「链接:xxx  abcd」照旧收得到,只是它必须贴在链接旁边。
    // ★ scanLinks 里那套散码机器(texts / pool / used)随规则一起清掉;
    //   loose 字段留成恒空数组(面板小标 / mergeByTag / scanDoc 三处仍在读它,恒不命中)。
    // ★ 防线没动:CODE_BARE / NOT_A_CODE / CODE_STOP 与严格档(纯 4 字母要标签词)逐字照旧。
    // 用户(2.53.62 交付后,附三张线上截图 · index.php):
    //   ① 红框圈住信息区与顶部三卡之间那道缝:「红色框选部分需要保持一定距离,
    //      太贴近了,看起来像是已经重叠。」
    //   ②(截图 2 / 3)红框圈住综合交流的「询问&求物」卡、箭头指向漫区特置顶行的空地:
    //      「然后把求物这个卡片放到汉化版发布旁边」
    // ★ 间距这条线上实测过:缝隙**只有 1px**,不是错觉 —— 信息区 bottom 277、卡片 top 278。
    //   根因是站点在 #header 末尾那块 99px 高、mb:-5px 的广告位被广告拦截插件收掉后,
    //   #main 直接顶到信息区下沿(把广告位 display:none 复现:卡片 top 374 → 278,缝隙 97px → 1px)。
    //   ⇒ 给 .t.spp-top4-host 自己加 16px 上边距(= 本模块卡片间距),有没有广告都恒定。
    //   只动这一个类,不碰 #main{padding-top}(那会连带挪版块页/帖子页的首块)。
    // ★ 求物卡片是**搬家不是复制**:
    //   · CM_PINNED 增到两位,新增 CM_FOREIGN 记「这位是隔壁 cate_7 的、徽章写交流/求物」;
    //   · 新增纯函数 pickForeignPins():从 cate_7 捞那一行、复制一份带上徽章语义;
    //     捞不到就返回空数组(站点改名/分类不在本页都不会抛错,置顶行退化成只有汉化本发布);
    //   · renderCmCards 把外板块并进 pool 再交给 pickCmTargets,anchor 仍只用漫区自己的 all;
    //   · HJ_KEEP / HJ_BADGE 各摘一项 ⇒ 综合交流 6 张卡变 5 张,原行照旧被 hideRest 收掉。
    // ★ 徽章不写死的话 buildCmCard 会按「漫区 + 询问」渲染 —— 那等于给这张卡换了块牌子。
    // ★ 守卫侧:check-hj-extract 的「命中 6 个 / 未成卡 3 个」随搬家改成 5 个 / 4 个;
    //   settings-panel 的 F 段跟着把写死调用点从 pickCmTargets(all, …) 改成 (pool, …)。
    // 用户(2.53.61 交付后,附篡改猴脚本列表截图):「框选的这个不要显示」——
    //   圈的是 Southplus Plus 名下那行菜单命令「首页 Comic Market 卡片范围:C104 起(含最新)」;
    //   追问后定案「彻底删掉,不要这个开关」。
    // ★ 那行是第三枚 GM_registerMenuCommand(点一次循环 仅最新一届 → C104 起 → 漫区全部,
    //   写存储后 reload)。篡改猴只在注册时画那一行 ⇒ 不显示的唯一做法就是不再注册它。
    //   本轮把注册整块删除,另外两枚菜单命令(⚙ 页面宽度 / ⚙ 翻页步长)不动 ——
    //   篡改猴菜单自此 3 项 → 2 项。
    // ★ 范围随之**写死 'cm'**:模块 16 里那两行读数(存储键 + 历史档位 c104 归一)
    //   与那条读数方法一并删除,渲染调用点改成字面量。
    //   台面上的代价:存过 仅最新一届 / 漫区全部 的用户,这一版起只会看到「C104 起(含最新)」;
    //   要恢复就是「加回一个读数 + 把调用点从字面量改回变量」,注释里留着这条路。
    // ★ pickCmTargets(all, scope, series) 的三档判定**一字未改**(纯函数,两个测试生成器还按
    //   'cm' 调它);本轮改的只是调用点的实参,面板(buildOverlay)一行未动。
    // ★ 那个范围的存储键自此在脚本里彻底消失(连注释也一起清干净)——
    //   settings-panel 守卫的 F 段
    //   从「菜单/读数两处必须还在」改成「三处必须都不在 + 那处写死调用点必须在」。
    // 用户(2.53.60 交付后,附篡改猴脚本列表截图):「我发现这个脚本好像没有封面图片…
    //   我指的『封面』是脚本对应的小图标」;随后从 assets/ 的候选里选定「A 星印」
    //   (assets/icons/a-starseal.svg):「这个作为脚本头像可以」。
    // ★ 头部新增 @icon:把源 SVG 压成一行后 **base64 内嵌成 data URI** ——
    //   不依赖托管(保持「装一个文件就完事」),也不怕仓库 raw 链接改名/挂掉。
    //   代价是头部多约 1.7 KB;源文件仍在 assets/icons/ 里可读可改,
    //   落地预检有一条「解码回来与压缩后的源逐字相等」的往返断言盯着这两处。
    // ★ 本次**只动头部这一行**,功能代码一行未改。
    // ★ 图标什么时候刷新:篡改猴是在**保存脚本时**读头部的 ⇒ 保存(或重新安装)之后图标才变;
    //   旧图标有缓存,切换面板 / 重启浏览器后再看。
    // 用户(2.53.59 交付后):「实时默认渲染这个功能默认是打开的,可以在脚本的悬浮设置面板那里关闭,
    //   然后再动手修改脚本」。
    // ★ 模块 12 从「点一次渲染一次的切换预览」升级成**实时预览(默认开)**:
    //   · 输入框与预览块并存(老路径是预览时把输入框整块换成预览块);
    //   · input / paste / compositionend 三条事件 + 200ms debounce(中文输入法组字期间不渲染);
    //   · 600ms 兜底轮询:工具栏胶囊 / 表情面板 / 站点按钮都直接改 textarea.value,不派发 input
    //     事件;同一处还跟着站点隐藏输入框(post.php 的「所见即所得」模式)把预览收起来;
    //   · 重渲染保留 scrollTop(不保就被顶回顶部);大文本降级:> 5 万字符走 800ms。
    // ★ 新增面板开关 spp_preview_live(默认 true)—— 关掉即回到 2.53.59 的老行为,
    //   所以老路径(切换式预览)一行未动,实时态的一切差异都挂在 --live 修饰类与 LIVE 分支里。
    // ★ 顺手修:[img] 原先在预览里被钉成 max-24px(行内小图的口径),实时预览里等于把图缩成
    //   一个点 ⇒ 改成 max-width:100% + height:auto。
    // ★ 顺手修 2:站点表情 [s:id] 原先把 URL 拼成 /images/post/smile/emotion/<id>.gif,实测 404
    //   (实时预览里一打字就是一排破图)⇒ 改走站点自己的数据源 picpath + face[sid][0]
    //   (实测 picpath = 'images/post/smile/'、face[718] = 'smallface/face077.gif'),
    //   拿不到就原样留着标签。
    // 用户(2.53.58 交付后):「你排序的那一个移到悬浮设置面板那里就行了,在悬浮设置面板中设置即可,
    //   无需放在网页上」。
    // ★ 撤掉:2.53.58 加在结果页的那条排序条整套(那条 SPP-SSORT 样式段 + findTable / dataRows /
    //   orderHref / 表单条件记录)—— 页面上不再多一行控件。
    // ★ 保留:① 搜索表单页把 select[name="orderway"] 预设成面板里选的那一种;
    //   ② 索引过期自救(2.53.44)重搜时也用同一种(原先写死 lastpost)。
    // ★ 面板那枚开关从 bool(人气 开/关)升级成**下拉框**:本项目第四类行 data-type="select",
    //   四选一(人气 / 回复 / 发帖时间 / 最后回复),默认「人气」,读数走 SPP_CONFIG.pick 枚举收敛。
    //   五处同步:渲染 / 面板自检选择器 / 保存分支 / 打开时回填 / 守卫 EXPECT 表。
    // ★ 键名 spp_search_hits → spp_search_order(旧的布尔值不再被任何地方读取)。
    // 用户(测试网址 search.php?):「能否增加一个功能,将排序方式由时间排序改为人气排序?」
    // ★ 站点读数(CDP 实测):搜索表单的「排序方式」是 select[name="orderway"],四个 option
    //   postdate / lastpost / replies / hits,HTML 默认选中 postdate(发帖时间)——
    //   这就是用户说的「时间排序」;结果页【没有】这个 select(数量 0),只有结果表 + 两条翻页条。
    // ★★ 关键读数:地址里带 sid 时 orderway 会被忽略(服务端按索引回放)——
    //   实测 ?step=2&sid=259703573&orderway=postdate 拿回来的仍是原序 ⇒ 换序必须摘 sid 重发。
    // ★ 交付两块:① 表单页把 orderway 预设成「人气」(URL 显式带 orderway 时照 URL);
    //   ② 结果页在结果表上方补一条排序条(人气 / 回复 / 发帖时间 / 最后回复),点一下就地换序。
    //   两条共用新开关 spp_search_hits(面板第十一枚 bool 行,默认开启)——
    //   关掉即完全站点原样(表单不预设、结果页不加条、自救重搜仍用 lastpost)。
    // ★ 排序条上「当前是哪种序」不读 URL:翻页后的地址里根本没有 orderway,
    //   改成按整页读数反推(四种序逐列测「整页不升」,唯一命中才算数;不足 3 行不猜)。
    // ★ 最新实测:查「本子」全站 500 条 10 页,orderway=hits&asc=DESC 拿到 535641 / 414651 / …
    //   严格降序;同一份 sid 带 orderway=postdate 仍是这套降序 ⇒ 上述 ④ 条成立。
    // 用户(第二张面板截图):「再排版一下」—— 三行蓝框(用户名 + 彩色背景头像 / 悬停高亮 /
    //   翻页悬浮框靠边自动隐藏)+ 两支蓝箭头指向左栏底部(标注「移动」)+ 红箭头指向左栏内容末尾(标注「分割线」)。
    // ★ 读数:2.53.56 的左栏到 y=577 就没了(图片设置 212 + 数值设置 206),右栏到 y=918(功能开关 773)
    //   ⇒ 左栏空 341px、右栏满,就是用户圈的那块不平衡。
    // ★ 改法:把用户框的那三枚开关搬到左栏,收成一个小分组「功能开关(续)」;
    //   搬完左栏 ≈640 / 右栏 ≈590,两栏底部基本齐平。
    // ★ 实现:那三行只改**累加目标**(html → leftTail),行本身一字未动;
    //   左栏留一个 HTML 注释占位符,收尾处(innerHTML 之前)替换成那个小组 ——
    //   顺序必须是这样,否则行点击 / 保存循环绑定时会漏掉这三行。
    // ★ 没有新增开关,也没有改任何开关的默认值。
    // 用户(附面板截图,红框圈着「取消 / 保存并刷新」与底部四行):「请排版一下悬浮设置面板的UI,
    //   使其尽量左右对齐。」
    // ★ 真机实测(CDP 读活面板):columns:2 是按高度装箱 —— 左栏 = 图片设置 + 功能开关(~1080px)、
    //   右栏 = 数值设置 + 按钮组(~390px)⇒ 右下角一大片空白,保存按钮悬在中段。
    //   ★ 同时核对了行内:所有 checkbox 右缘都是 658、行右缘 671(各列等宽 428)——行内本来就是对齐的,
    //     截图里看着参差是预览缩放的错觉;所以这一轮只动「栏」,不动行内。
    // ★ 改法:多栏 → 网格。左栏 图片设置 / 数值设置,右栏 功能开关(跨两行),按钮组横跨整幅收尾 ——
    //   它的上边框自此与标题分隔线同宽。窄屏(<=760px)退回单栏。
    // ★ .spp-gs-group 的 break-inside:avoid 随多栏一起删掉(多栏没了就没人认它)。
    // 用户原话:「能否添加一个双箭头的两个按钮,用于滑动到第一页和最后一页?
    //   这两个镜头可以在悬浮设置面板中选择开启或关闭,默认是开启的。」(附一张翻页条截图)
    // ★ 站点原本那两枚就是连体双箭头 « / »,2.52.29 起被本脚本改成「上一页 / 下一页」并换成单箭头 ‹ / »,
    //   所以本轮**新建**两枚 « / »:放在 ‹ 之前、› 之后,点一下直接去第 1 页 / 最后一页。
    //   · 纯 <a href> 跳转(复用同一个 build 模板改写),不挂 JS 点击;
    //   · 到边界照老规矩摘 href + .spp-pg-off;读不到总页数时只建首页那枚(末页宁可不给也不给错的);
    //   · 幂等:data-spp-pg-end="first|last" 认领。
    // ★ 新增开关 spp_pg_ends(默认开):SPP_PANEL_DEFS + buildOverlay 手写 HTML + 本模块读数点 +
    //   tools/verify/settings-panel.js 的 EXPECT 表,四处齐。
    // ★ 没有其它改动;「复制本页 / 复制本贴」的链接格式与 2.53.54 一致。
    // 用户这回给了可复现的地址:https://blue-plus.net/read.php?tid-2717095-fpage-0-toread--page-2.html
    //   「复制本贴」的结果只有一行 `- B32F  https://www.touchgal.us/`,而第 1 页明明有一串链接。
    // ★ 真因(活体实测,不是猜):`pageUrl()` 换的是**第一处** `page-N`,而站点翻页地址里前面就有个
    //   `fpage-0`(fpage 含 "page")⇒ 换完是 `…-fpage-3-toread--page-2.html`,末尾目标页号仍是 2。
    //   实证:`…-fpage-3-toread--page-2.html` ⇒ Pages: 2/10;`…-fpage-0-toread--page-3.html` ⇒ Pages: 3/10。
    //   于是 9 次抓取全拿回第 2 页,再按 (层,URL) 去重 ⇒ 只剩第 2 页自己那一层。改法:只换最后一处。
    // ★ 顺带按用户给的骨架改输出:一层的多条链接收成一行,用「 、 」分隔,码紧跟它自己那条链接。
    //   `- B15F  https://acgdb.de/ 、 https://alist.erogame.space/ 密码 faust`
    // ★ 没有新增设置项。
    // 用户两条新报障(原话):「点击『复制本页链接』时,B15F 这个楼层仍然生成了两套链接,但只复制了一份。」
    //   「复制本贴链接时,它只抓取了 30 个楼层(没有抓取到后面的页码,我怀疑这个功能能不能实现。)」
    // ★ 真机上两条都复现不出来(同贴同 URL:本页 B15F 两条都在;本贴 `已复制 18 条链接 · 全贴 10 页`)
    //   ⇒ 不猜,堵住「环境不同就会走到」的两个口子,并让结果自报病因:
    //   ① 同名楼层合并(mergeByTag):同一层在 DOM 里出现两次时面板会出两行,而复制按 (层,URL) 去重
    //      ⇒ 正是「面板两套、复制一份」。
    //   ② totalPages() 补分页条兜底(原来只认 .pagesone,读不到就当 1 页 ⇒ 「只抓了本页」);
    //      抓页失败重试一次,并把第一处失败原因写进提示(第 N 页 HTTP 403 / 网络错误);
    //      「只有 1 页」那句改成带分页读数的诊断句。
    // ★ 没有新增设置项。
    // 用户装上 2.53.51 后当场指出(原话):「如果高亮的是『图』这个字,它应该显示为图片。
    // 如果是另一种情况,该图片应显示为图片,另一个应显示为链接。」
    // ⇒ 按上下文=**高亮(当前)那枚永远两个字**:含图 / 链接;另一枚才收成一字(图 / 链)。
    //   2.53.51 为了让窄条压到 96px 多留了一档「两枚都单字」,那一档把「现在索引哪一栏」丢了 —— 整条删掉,
    //   窄条下限 96 → 108(代价记在宽度上,不记在字面上)。CSS 侧 narrow3 合并成 narrow2。
    // ★ 仍然没有新增设置项。
    // ★ 真机验收:找一个窄条落在 108–137 的窗口宽度,确认页签是「含图 + 链」(当前那枚两个字)。
    // 本轮用户三条(原话):
    //   ①「对于密码、验证码等而言,只有与链接同时存在时,其才生效。如果没有链接,即便检测到有验证码,
    //      也等同于没有。」⇒ scanLinks() 加闸:本层没有链接 ⇒ 整层的码作废(连带整层不再进链接栏)。
    //   ②「美化一下收缩策略……若未点击,灰色项应显示为图或链单字,以便为高亮展开显示为两个字。」
    //      ⇒ 收起态**不再压宽度**(96px 正是「含/图」被挤成上下两行的原因),只藏列表;
    //        字面按现算宽度分三档:都全字 / 当前全字 + 另一枚单字 / 都单字。
    //   ③「位置再向上移动一点,因为居中对阅读体验稍差」+「长度可以稍微扩展一些,但不能触碰到贴纸板块」
    //      ⇒ 竖向中心 50% → calc(50% - 64px);宽度交给 fitWidth() 按「贴纸块左边 − left − 10px」现算
    //        (下限 114 / 上限 174),并据此切换行紧凑排版与码标字面。
    // ★ 没有新增设置项。★ 真机验收:注入 srv/payload-current.js,量宽 / 切页签 / 复制两条路径。
    // ★★ 2.53.50:「链接楼层」并进含图楼层面板(用户拍板「合并成一个面板、分两个页签」),
    //   链接栏里带**提取码**与**两枚复制**。三轮要求都落在这里:
    //   ①「增加一个类似的功能,称为『链接楼层』,用于定位链接」+「复制本页的全部链接 / 复制本贴的全部链接」;
    //   ②「提取码 + 数字、英文的结构也要复制」;
    //   ③「密码、验证码、单独存在或者和中文混在一起的四个字符的英文+数字,都按照提取码来看」。
    //   ★ 规则先做成审核页给用户看过(tmp/r1/srv/preview-linkpanel.html:严格/宽松两种输出并排现算),
    //     用户看完说「修改脚本」⇒ 按**严格档**落:纯 4 字母要有标签词才认(挡掉 mark / good / note 这类词)。
    //     换档不必改脚本:控制台 localStorage.setItem('spp_lk_mode','lax') 后刷新;
    //     输出格式同理 spp_lk_fmt = a / b / c(a 一行一条 · b 一层一块 · c 纯 URL)。
    //   ★ 提取码认法:标签词(提取码/提取碼/访问码/訪問碼/解压码/解压密码/密码/密碼/验证码/驗證碼/pwd/password)
    //     + 裸码(4 位字母数字,前后不是字母数字即算);只排除楼层号形(B12F)、年份(19xx/20xx)、
    //     以及已被标签词或 URL 认走的同一串。全角先折半角。
    //   ★ 真机实测第一天就抓到一个误收:真实楼层里写着 ed2k(eMule 协议名,字母+数字混合的 4 字符,
    //     严格档也挡不住)⇒ 补一张协议名 / 分辨率小名单(ed2k / m3u8 / h264 / x265 / 1080 / 2160 …)。
    //   ★ 码归哪条链接:按 DOM 顺序把一层拆成「文本段 / 链接」交替序列,先看链接**后面**那段文本,
    //     再回头看**前面**那段 ⇒「链接+码同行」与「码在链接之前」两种写法都认,一层两链接各带各码也分得开。
    //   ★ 剪贴板走 navigator.clipboard(HTTPS + 用户手势),失败退 textarea+execCommand;
    //     **不申请 GM_setClipboard** —— 2.52.93 明确删过这个 @grant,别再加回来。
    //   ★ 本贴抓取:按 .pagesone 的「Pages: x/y」逐页顺序 fetch(跳过当前页),封顶 60 页;
    //     失败不静默,结果里写「(N 页抓取失败)」,已抓到的部分照样复制。
    //   ★ 不加设置面板开关:档位 / 格式 / 页签都走 localStorage(少一处手写 HTML 就少一次 2.53.32 那种漏改)。
    // ★★ 2.53.49:三条批注一次落 ——
    //   ①「图中框选的左右翻页按钮功能失效了」+「将那个带有两个箭头的图标换成一个箭头」:
    //      retargetEndPager 的 build() 里 `replace(/page([-=])\d+/i, …)` 命中的是**第一处** page[-=]N,
    //      而站点地址串前头还有个 fpage-0(fpage 里也含 "page")⇒ 改完是 `…-fpage-N-toread--page-1.html`,
    //      真正的目标页号一直是 1,两个按钮都回第 1 页(在第 1 页上按更是毫无反应)。
    //      实测:…-fpage-2-toread--page-1.html → Pages: 1/3。改成只换**最后一处**;
    //      读当前页号那句同病(fpage-N 会被当成页号)也一并改成取最后一处;
    //      字形 `«`/`»` → `‹`/`›`(byText 正则本就两种都认)。
    //      ★ 第二刀:把范围收成 row.querySelector('.pages') —— 收窄到了「上一主题 / 下一主题」那一块
    //        (同一行里两块 .pages,那一块在 DOM 里更靠前),里面根本没有箭头 ⇒ 直接 return 0,
    //        两枚箭头于是回到站点原样(真机读数:字形仍是 « »)。
    //      ★ 第三刀(正解):在行内**挑出「同时含 ‹ 与 › 的那一块 .pages」**,箭头与模板都从这块取。
    //      ★ 顺带解释用户看到的「失效」:站点自己给 « 的 href 是第 1 页、» 是最后一页,
    //        在末页点 » 等于重新加载末页 ⇒ 观感就是「按了没反应」。
    //   ②「底部的位置栏相比顶部的位置栏,缺少了『本贴』的位置」(用户确认过理解):
    //      站点底部那条面包屑天生没有标题节(实测 `[img] 南+ South Plus 茶馆 帖子浏览记录 版块浏览记录`)
    //      ⇒ 新模块 15j initCrumbTitle() 克隆顶部那节 <strong><a>标题</a></strong> 插在 .fr 之前,
    //        打 .spp-crumb-cur 标,CSS 上主色绿 + 补一枚 › 分隔符。幂等。
    //   ③「贴吧表情的文本和表情的对应关系依旧是错误的」:整表按审计表重建(113 条里 76 条错配,67.3%),
    //      name := 该图实际内容;跳过 id 1(仓库 404 文本,改用不带编号那张)与 102–124(角色贴纸);
    //      同名的第二条加 ②。**编号一个没动**——老帖里写死的 [tb:N] 仍渲染同一张图。
    // ★★ 2.53.48:含图楼层悬浮窗两件事 ——
    //   ①「把含图楼层的悬浮面板,从右下角移到左侧中间」(用户拍板,原话)。
    //      · #spp-imgidx:right:14px; bottom:16px → left:14px; top:50% + translateY(-50%)。
    //      · 2.53.30 那个 bottom:16px - 18px 是让开右侧导航面板 #spp-nav-panel 的错位量,
    //        左侧没有别的常驻浮层 ⇒ 一并去掉。
    //      · 靠边自动隐藏(2.53.32)跟着翻面:右侧往右缩,左侧往左缩(−100% + 68px),
    //        且三处 transform 都补回 translateY(-50%),否则悬停时会跳回顶对齐。
    //      · 已知代价:read.php 左栏是楼层头像(约 x 13–163),174px 的面板会压住头像列右缘。
    //   ② 修标题栏重字(本轮从用户截图里看出来的真 bug):点一下标题折叠后,面板变成
    //      「含图楼层 含图楼层」(绿 + 灰并排)。根因是折叠回调里多写了一句
    //      `this.querySelector('span').textContent = '含图楼层'`,而标题栏里只有
    //      .spp-ii__fold 这一枚 span ⇒ 刚写好的 '+' / '—' 被覆写。删掉该行即可。
    //   ③ 两枚开关的 hint 文案随位置改(两张表共 4 处「右下角那块」→「左侧中间那块」)。
    //   ④ 左侧贴边重做:原右侧那套 translateX 滑出去,翻面后在左侧露出的是**面板右端**
    //      (「张」/ 输入框 / 折叠标记),绿点与标题反被推出屏幕 ⇒ 改成「收窄到 96px + 裁掉列表
    //      与折叠标记」,露出的正是绿点 + 「含图楼层」;hover / focus-within 再把宽度放回去。
    //      过渡随之从 transform 改到 width,并补一条 prefers-reduced-motion 分支。
    //   ⑤ 修「收起后键盘进不去」:贴边态把列表 display:none 了,而**没渲染的节点收不到焦点**
    //      ⇒ :focus-within 永远匹配不上(mock 实测:num.focus() 后宽度仍是 96)。
    //      给标题栏加 tabindex="0" + role="button" + aria-label:收起时它仍在渲染,Tab 到它就展开。
    //   ★ 量这一条时踩了个假信号:三种状态读数一模一样、规则在 CSSOM、选择器也 matches,
    //     看着像「规则不生效」;真因是 **tab 在后台、CSS transition 不推进**(同 2.50.9 / 2.52.19),
    //     把 transition 临时置 none 再量就对了。
    // ★★ 2.53.47:本轮做的是用户「计划目标功能.md」里除「每页 60/90/120 楼」之外的四项
    //   (那一项用户看过可行性评估后拍板**不做** —— 服务端恒定 30 楼/页,6 种 URL 参数实测
    //    全部返回 Pages: 1/10 且楼数不变,只能客户端抓页追加,代价约 200 行 + 616KB/120楼)。
    //   ★ ① 楼层顶部「小 中 大」收掉,替代品是设置面板新增的「正文文字大小 (px)」(默认 14)。
    //       站点实现只有一行:fontsize(text,id){ getObj("read_"+id).className = text; },
    //       配套三条站点 CSS .small{12px} / .middle{14px} / .big{18px}。新模块 15i 做两件事:
    //       把所有 div[id^="read_"] 的档位类按面板值统一刷一遍(含站点等价类),
    //       以及把顶部行里这几枚 a 与「顶端」摘掉(连空 span 和多余分隔符一起收拾)。
    //   ★ ② 设置面板新增第九枚 bool「翻页悬浮框靠边自动隐藏」(**默认关闭**)+ 第三枚 number
    //       「正文文字大小 (px)」;两张表同步,守卫 EXPECT 也同步加了两条读数点。
    //   ★ ③ 「选择文件」按钮:站点全局 .input{appearance:none} 把 <input type=file> 的外壳剥了,
    //       内层那枚原生按钮一直是系统灰方块;本轮用 ::file-selector-button 给它套上浅色玻璃皮
    //       (皮肤与设置页那份同源,只是配色从深色改浅色)。
    //   ★ ④ 两条「浏览记录」的字体:真凶是站点那条全局 .abtn{font-family:simsun},
    //       而管辖它们的那条规则原先只写了 font-weight:500、没碰字体族 ⇒ 宋体一路赢下来。
    //       补 font-family:inherit + 字重提到 700,与参照物「社区论坛任务」对齐。
    //   ★★ 三处**活体实测纠偏**(都写进 经验.md 了,这里留索引):
    //     · 只藏 <a> 没用 —— 脚本自己有一条 #main > form.spp-flow .tiptop > .fr > a {
    //       display:inline-flex !important } 把**父级**点亮,子级 display:none 仍占位。
    //       实测「顶端」computed 仍是 inline-flex、盒子 41×19,规则却在 CSSOM 里查得到。
    //     · 两个父级不是同一种东西:「顶端」的父级是裸 <span>(用 span:has(> a[…]));
    //       「小中大」三枚的父级**就是 DIV.fr**(5 种 span:has(…) 写法实测 count 全为 0)。
    //     · 面包屑那两枚不在 .spp-t5bar 里(实测 .spp-t5bar 数量为 0)—— 它们住在
    //       #main > .bdbA > #breadcrumbs > .fr;给 .spp-t5bar 加字体是白费功夫。
    //   ★ 详情:15i 模块头(楼层顶部行)、第 2 模块 buildNavPanel(靠边隐藏)、
    //     回复框附件区那三条 CSS、「贴吧表情」那一项**没做**(根因在数据表不在代码,另行处理)。
    // ★★ 2.53.46:15g 购买框架支持**一页多个购买块**,并修掉自动购买的三处瑕疵(用户:
    //   「测试网址:read.php?tid-2717095-keyword-网址.html / 对于自动购买免费帖子没生效,
    //    此外这个自动购买貌似还是有一些瑕疵」)。
    //   ★ 先量后改(只读探针 tmp/r1/srv/buy-tid2717095-recon.js、buy-variants.js、
    //     buy-containers.js):实测那个帖的第 28 楼自己出售内容,块挂在
    //     div.tpc_content > div#read_35357708.f14 > h6.quote.jumbotron(span.s3 售价 0 SP币、
    //     871 人购买;input.btn.btn-danger 的 onclick 是
    //     location.href='job.php?action=buytopic&tid=2717095&pid=35357708&verify=c23ac2aa')。
    //     又扫了同板块 14 个帖:13 个没有购买块,tid-2217905 一页**三块**(1/2/末楼,其中一块
    //     售价 999999999 SP币)。判据于是定为:全页 h6 + 最近的 read_* 祖先。
    //   ★ 五处改动:① 扫描全页购买块(幂等改成逐块判,不再整页 return);
    //     ② 控件判据放宽 + 认不出地址就整块不动;③ 成功判定按 pid 认(新 stillBuyable);
    //     ④ 免刷新换页按这一块的宿主做,纯出售块才整段重建;⑤ 自动购买按序号错峰 400ms。
    //   ★ 设置面板与 SPP_PANEL_DEFS **一行不动**(开关 spp_autobuy_free 早已默认开),
    //     所以 tools/verify/settings-panel.js 的 EXPECT 也不必改。
    //   ★★ 落地当轮真买两单(用户:「直接跑,看看情况」):tid-2717095 的 28 楼当场买下、
    //     正文不刷新就出现(服务端 871 → 872、action 消失);tid-2217905 一页三块里两块各自买下、
    //     收费那块一次请求都没发。**同一轮抓到一条竞态**:核实复读紧跟在购买请求后面时,
    //     服务端可能还没落库就渲染了页 ⇒ 1 楼那单买成功却被读成失败;
    //     ⇒ doBuy 改成判失败前退让重试两趟(600ms / 1400ms)。
    // ★★ 2.53.45:楼层悬停高亮改成可关(用户:「鼠标停在某一楼层时候会高亮 / 测试网址:
    //   read.php?tid-2960896.html / 这个功能能在设置面板那里设置为关闭吗?(默认关闭)」)。
    //   ★ 先量后改(探针 tmp/r1/srv/hover-src.js:真鼠标 hover + getComputedStyle):
    //     被悬停那一楼 th.r_one 由 rgba(0,0,0,0) → rgba(47,111,104,0.07),同页其余
    //     30 楼全程纯白;把该页所有 :hover 规则逐条与元素对了一遍,带 background 的
    //     只有我们注入的 <style> ⇒ 这层灰是**我们星空皮肤**(spp_home_star_glass)那条
    //     html.spp-hm-star #main .t5 tr:hover > th/td,不是站点给的,也不是别的扩展。
    //     (作者栏 th.r_two 不受影响:.t5.t2 th.r_two{background:#fff!important} 压得更死。)
    //   ★ 本版:新增第八枚可勾选开关「悬停高亮」(spp_floor_hover,**默认关闭**)
    //     + 新模块 15h —— 关掉时给 <html> 打 spp-fh-off,用一条多带一枚 class 的规则
    //     把那层 7% 青灰底刷回 transparent(transparent 就是这些格的默认态);
    //     2.52.48 一体流那圈 ::after 悬停内描同属「悬停高亮」,一并归这枚开关管。
    //   ★ 三处登记照旧:SPP_PANEL_DEFS(def: false)+ buildOverlay 手写 HTML 行 +
    //     15h 里的读取点,tools/verify/settings-panel.js 逐项对着核。
    //   ★★ 活体验收(把本版真身注入真站 read.php?tid-2960896.html,GM 垫片见
    //     tmp/r1/srv/inject-25345.js;真鼠标 hover + 逐行取中位色):
    //       · 注入后 <html> 自己带上 spp-fh-off、#spp-fh-style 已挂上(2 条 spp-fh-off 规则);
    //       · 被悬停那一楼:带标时 mean=(255,255,255) 纯白,摘掉标时 mean=(242,245,244)
    //         —— 后者与用户截图里那层灰**逐通道相同**(rgba(47,111,104,.07) 盖在白底上算得
    //         (240.4,244.9,244.4),探针实测 242/245/244)⇒ 认定「用户看到的就是这条」;
    //       · 面板(后挂的那个 ⚙ 实例):15 行、含 spp_floor_hover:bool、
    //         勾选框 checked=false 且 enabled ⇒ 默认关、随时可开;
    //       · 对照图 tmp/r1/hover-ab-25345.png(上=打开、下=默认关)。
    // ★★ 2.53.44:搜索页翻页那件事(用户:「使用脚本后,检索面板翻到第二页及以后会报错;
    //   关闭脚本后,翻页功能恢复正常」「有把握修改好问题吗?如果有就修改脚本」)。
    //   取证:tmp/r1/srv/pager-raw-vs-live.js · hit-test.js · hit2.js · click-rec.js ·
    //     beyond.js · search-rescue.js · rescue-post2.js(全部在真站上跑)。
    //   ★ 先把「谁报的错」钉死:那个页面是**站点自己**发的「南+ South Plus 提示信息 ▸
    //     没有查找匹配的内容」—— 站点按 sid 存一份搜索索引,索引过期/被清掉之后,
    //     那个 URL 就只剩这一页。三条实测:
    //       ① 用户给的 sid-259606590:page-1 与 page-2 **表现完全一样**(都是提示页)——
    //          若真是脚本把「第二页」弄坏,第一页不该陪着一起死;
    //       ② 同一时刻、同条件的新搜索(sid-259611738,500 条 / 10 页)用本脚本翻
    //          1/2/3/10 页全部正常;翻过头(11/12/13/20/50/999)站点给的是
    //          「Pages: 11/10」**空表页**,也不是提示页;
    //       ③ 本脚本在搜索页**一个请求都不发**(performance 只有 css/img/script、
    //          控制台 0 条)⇒ 它没有能力让站点删索引。「关掉脚本就好了」多半是重载/
    //          重搜顺带拿到一条新索引(混淆变量)。
    //   ★ 顺手把「脚本 vs 站点」的账算清:站点自己的翻页链接在 DOM 里与服务器原文
    //     **逐字节相同**(数字 1/2/3/4/5/6 与 Go 输入框的 inline onkeydown 一字未改),
    //     脚本只按设计动了 « 与 » 两枚箭头(改成上一页/下一页)。
    //   ★★ 但排查里挖到一个**真属于脚本**的缺陷,本版修掉:右侧悬浮翻页面板
    //     (#spp-nav-panel:⬆◀▶⬇⚙ + 页码格)的 navigateToPage() 只夹下界
    //     Math.max(1, …)、**没有上界**;◀ 在第一页会置灰,▶ 在末页**不置灰**。
    //     实测第 10/10 页:面板自己写着 10/10,▶ 仍是 spp-enabled,一点跳到 page-11。
    //     修法四条:① 用站点自报的「Pages: x/y」当权威总数(**新增**:原先只数链接里
    //     的最大页码,而搜索页那枚 » 会被翻页条美化改成「下一页」⇒ 10 会读成 5);
    //     ② navigateToPage 上下界都夹、且目标页等于当前页时不跳;③ 末页把 ▶ 置灰、
    //     到边界时 ←/→ 连按键都不吃;④ getPageInfoFromUrl 补一条通用 `…-page-N.html`
    //     规则 —— search.php 原先**完全不在这个函数里**,pattern 只能靠 DOM 兜底。
    //   ★★ 用户真正撞到的那一格也补上:新模块「搜索索引过期自救」。落在提示页且 URL
    //     带 keyword+sid 时,用同样的关键词与版块 POST 重搜一次(字段名与站点表单逐一
    //     对应,实测 200 / 65,105 B / 新 sid-259614494 / 500 条 10 页),把目标页号夹进
    //     [1, 总页数] 后 location.replace 过去 ⇒ 「翻到第二页」不再顶在一张提示页上。
    //     三条护栏:同 URL 30 秒内只救一次 + 每标签页最多 3 次(sessionStorage 记账)/
    //     响应认不出翻页模板就
    //     **一个字符都不动** / URL 里没有 sid 直接放弃(关键词真搜不到东西时站点也是
    //     这一页,那不是「索引过期」)。只恢复 URL 里带着的条件,时间范围与排序不在
    //     URL 里,只能回落到站点默认值 —— 索引没了,这是能给的最好结果。
    // ★★ 2.53.43:免刷新把买回来的正文摆出来(用户:「这个购买成功,但是购买后的信息没有
    //   暴露出来;分析一下在不刷新的情况下能实现自动购买和暴露信息」)。
    //   取证脚本:tmp/r1/srv/feasible-25343.js(**在未购买页上跑**,只读不改)。
    //   ★ 根因:被卖的内容**不是 display:none 藏着的,是服务端不发**。三个实测读数:
    //     ① 未购买页 #read_tpc = [h6 购买卡(69px), blockquote 警告条(33px)],纯文本 101 字、
    //        没有任何正文块(bodyBlockquote=false);
    //     ② 已购买页同一位置 = [正文文本/链接/图×5/br×3, h6 售价行, blockquote(gofile 链接), br]
    //        (h6 在第 11 个孩子上),纯文本 382 字,而且**警告条不在了**;
    //     ③ 从页内 fetch 一份已购买页 HTML:**364ms / 204,212 B**,被解锁的内容就在里面。
    //   ★ 修法:那次「复读本页核实成功」的 GET 自 2.53.39 起就在跑,只是返回值只被
    //     /action=buytopic/ 用了一次就丢 ⇒ 现在把它接着用(零新增请求):
    //       DOMParser 解 → 取新 #read_tpc → 把我们的卡搬到新结构里 h6 的位置 →
    //       用新页的孩子整段换掉 #read_tpc 现有的孩子 → 重新认领正文框。
    //   ★ 免费帖自动购买与手动购买走的是同一个 doBuy(),两条路一起修好。
    //   ★ 为什么敢整段换(三条都验过):① #read_tpc 里除了这枚卡没有别的脚本产物;
    //     ② 图片点击是**文档级委托**(document 捕获阶段 + closest('.tpc_content') +
    //        patchImg 懒标),换掉正文节点不会丢绑定;
    //     ③ 换页是同步一次替换 —— 不跳转、不丢滚动位置、没有停顿可等(所以不做
    //        「内容已到,点此载入」那种按钮:没有需要等的东西)。
    //   ★ 两个刻意的兜底:
    //     · 认不出新结构(没有 #read_tpc 或没有那枚 h6)⇒ **一个节点都不动**,
    //       宁可维持现状(卡是已购买、正文没出来),也绝不把页面掏空;
    //     · fetch 失败/超时 ⇒ setFail(),和今天一样给「点我重试」,不重试、不循环。
    //   ★ 顺带修好一处视觉不一致:买完之后那条「若发现会员…封掉ID」警告条会随换页一起
    //     消失 —— 站点自己的已购态就是不渲染它(实测 warnInFresh=false),
    //     以前留着它是因为没换页。
    //   ★ 买家数改用新页里的真实数字覆盖(`已有 N 人购买`),比我们自己 +1 更准,
    //     也顺带盖掉「买的同时别人也在买」那点误差。
    //   ★ 用户那张截图正是这套推断的完美样本:那个帖只把**一部分**内容标了「购买可见」,
    //     免费可见的正文买前就在、买完当然还在;真正被卖的 gofile 链接一直没出来,
    //     而卡已经翻成「已购买 · 已解锁」—— 现在这一段会当场补上。
    // ★★ 2.53.42:头像区那行名字 —— 两种楼层,两种活法(用户拍板选「④ 叠在头像框上」)。
    //   审核稿:tmp/r1/srv/preview-ava-id.html(①站点原样 / ②收空行 / ③同族名牌 / ④叠在框上,
    //   四个选项带实时高度读数)。
    //   ★ 用户原文:「关闭的时候用户 ID 都消失;开启的时候无图床头像的用户有 ID、有图床头像的
    //     用户无 ID。无论开启还是关闭,都希望用户有 ID。」
    //   ★ 两个症状,一个根因:2.53.7 那条「左栏收口」写成了**页面级**(只认 html.spp-read-on,
    //     不认那个开关),凡是没换成彩色块的楼层,名字就被它一起压成 0 高。实测 tid-2973029
    //     全部 31 楼:名字层 rect 高 0、名字链接 offsetHeight 0、字号被继承成 0px ——
    //     节点一直都在,只是被 0 高 + overflow:hidden 藏住,一个节点都不必重建。
    //   ★ 修法:收口加条件 —— 只对 .spp-ava-tiled(换过彩色块)的楼层生效;这枚类由 15c 在
    //     彩色块**真正插进 DOM 的那一行**打在 .r_two 上,「恢复默认头像」时一并抹掉。
    //   ★ 没换块的楼层改成「名字叠在头像框下沿」:负外边距 -25px(= 条高 22 + 框底距 3)
    //     + 半透明毛玻璃条(rgba(255,255,255,.86) + blur(6px))+ 下圆角 14 与框的外圆角对齐。
    //     ⇒ 实测本页 31 楼**总高 5061 → 5061,一分没涨**,每个用户都有且只有一个 ID。
    //   ★ 为什么不用 position:absolute:bottom:0 认的是**单元格**底边,而顶楼那种「正文很长
    //     把左栏撑到 266px」的楼层头像只占顶部 156px ⇒ 名字条会掉到单元格最下面去;
    //     负外边距走文档流(紧跟头像表),与单元格多高无关。
    //   ★ 顺带量准的一格:只写 margin-top:-25px 会让单元格从 156 缩到 153(头像框丢掉底下
    //     那 3px 呼吸位),补一条 margin-bottom:3px 之后前 6 楼逐像素回到 266/206/156/156/156/156。
    //   ★ 四个选项的实测代价(同一页 31 楼各注入一遍):① 站点原样 +1263px、② 收空行 +625px、
    //     ③ 同族名牌 +899px、④ 叠加 ±0 —— 这就是用户选 ④ 的依据。
    //   ★ 长 ID 换行、**不截断**(本页最宽 72px / 可用 134px):宁可让这一楼长十几像素,
    //     也不做省略号 —— 用户要的正是「ID 要看得见」。
    //   ★ 真头像的**图**一个字没动(不裁不缩不换 src,2.53.27 的比例规则照旧),只在框下沿
    //     浮了一枚名字条;2.52.93 那条「真头像一律不动」按本次拍板放宽到「图不动、框上可加字」。
    //   ★ 本模块没有新增开关:名字行不给关(身份信息,不是装饰);开关仍然只决定要不要换彩色块。
    // ★★ 2.53.41:15g 购买框架第二轮 —— 用户圈出截图里「购买后包裹信息的那个 UI 框」,
    //   即**买完之后包住被解锁内容的那一层**(站点 blockquote.jumbotron)。上一版把它当
    //   「正文,一个字不动」放过了,这一版按审核稿改成 A「同族框」。
    //   审核稿三步:preview-frame.html(四套选型)→ preview-frame-radius.html(圆角定值)
    //   → preview-bought.html(卡收成通行证条)。用户拍板:**A 同族框 + 圆角 12px**。
    //   ★ 现场实测(tid-2973029,已购页):那一层是 823×91、#f8f8f8 底、0.667px #eee 四边、
    //     2.667px #eee 左线、**圆角 0**、内边距 3px 5px 3px 8px、12px/28px #736933 字。
    //   ★★ 顺手量出并修掉一个错位:站点给 h6 与那层 blockquote 的**都是 width:80%**
    //     (内容列 1008.33 ⇒ 两者都是 806.67),但那层是 content-box、又自带 8+5px 内边距
    //     与 3.33px 边框 ⇒ 成盒 823;而 h6 自带 2px 外边距 ⇒ 卡 807 且右移 2px。
    //     修法不是加那对负外边距,而是「框改 box-sizing:border-box、h6 去掉那 2px」——
    //     两边成盒都回到 80%,实测左右偏差 0 / 0,不依赖任何写死的像素。
    //     ★★ 这里踩过一格:第一版写的是 `width:auto`,当场把站点那条 80% 顶掉,框撑成 100%
    //        (实测 1008 宽、右边比卡宽 202px)—— 这一层只能动盒模型,不能动宽度的来源。
    //   ★ 第二条:已购买态那张卡从 69px 收成 40px 的「通行证条」。原来它把「已购买」念三遍
    //     (印章 / 标题 / 右侧那枚**点不动的绿按钮**),而买完之后它的活儿只剩报告状态。
    //     ⇒ 收成一行:✓ 圆标 +「已购买」+「已解锁」胶囊 + 右侧淡字「售价 N SP币 · 已有 M 人购买」。
    //     ⇒ 顺带:自动购买(默认开)那条路**一上来就用这条的骨架**(⋯ 购买中…),不再先画一张
    //       带大按钮的卡再切走 —— 否则每次自动购买都会有一次 67 → 40 的可见跳动。
    //     ⇒ 失败态分两副面孔:自动那条路就地变红「点我重试」+ 站点原路兜底链接;
    //       手动那条路照旧回到大卡那枚红按钮(它本来就是为「失败了再试一次」准备的)。
    //   ★ 第三条:新开关 spp_buy_lift(提亮已购正文,默认开)—— 站点给「付费可见内容」用的是
    //     引用块的字号字色(12px、偏褐 #736933),花钱买回来的东西按正常正文(13px、深灰)
    //     显示更好读。它改的是**正文自身**,所以单独一枚开关,随时能在面板里关掉。
    //   ★ 认领判据沿用「按内容、不按位置」:只在服务端认为已购买时认,跳过同时含「举报」与
    //     「封掉ID」的警告条,取其后第一条非空 blockquote;认不出就一个标都不打,页面保持原样。
    // ★★ 2.53.40:购买框架补一格 —— **已购买态也做成卡**(用户真买一单后拍板)。
    //   ★ 上一版(2.53.39)的守则是「没有那枚按钮就交回站点原样」,而服务端在已购买后
    //     正是**不再渲染按钮** ⇒ 买完刷新一下就退回 9.8px 灰字行。
    //   ★ 2.53.40 用真买一单把两个未知一起钉死了:
    //     ① 成功页形态:job.php 返回 **200 + 807 字节**、**没有 302**,正文是一条
    //        <meta http-equiv="refresh" content="1;url=read.php?tid=2973029">
    //        —— 「原生点一下会刷新」的机制本体就是这个 1 秒后自跳的 meta refresh 页。
    //        ★ 关键:这 807 字节里「成功」「失败」两个词**一个都没有**,所以当初若按
    //          「响应里含『购买』就算成功」写,第一次真购买就会误报失败。复读本页是对的。
    //     ② 已购买态 markup(tid-2973029,真买后复读):
    //        <h6 class="quote jumbotron"><span class="s3 f12 fn">此帖售价 0 SP币,已有 9478 人购买</span></h6>
    //        —— 没有 input、**连警告 blockquote 也不渲染**(站点只在「可买」时才提示被骗风险)。
    //   ★ 判据随之改:从「找那枚按钮」改成「这枚 h6 里有没有『此帖售价 N SP币』」;
    //     没有按钮 = 服务端认为已购买 ⇒ 直接画成 ✓已购 卡(data-spp-buy="bought"),
    //     不发任何请求、不挂任何监听。有按钮的那条路一字未改。
    //   ★ 顺带把「已购卡」抽成 paintDone(state),本次买下的(done)与服务端认为已购的
    //     (bought)共用同一套外观 —— 两者只差一个读数标签。
    //   ★★ 落地当轮又抓到并修掉一个自己带出来的坑:警告条原先用纯 CSS 认「紧跟购买块的
    //     blockquote」,而在**已购买**的帖子里,紧跟 h6 的 blockquote 不是警告条、是被解锁的
    //     正文(站点把「购买可见」内容包在 blockquote.blockquote 里)⇒ 会把正文染成小号褐字
    //     还挂个 ⚠。实测:已购页该选择器命中数 1,命中的正是正文块。
    //     改成 JS 按内容认领(同时含「举报」与「封掉ID」才打标 spp-buy__warn),CSS 只认这个标。
    // ★★ 2.53.39:帖子页(read.php)**购买框架** —— 用户「美化一个购物框架」(审核稿
    //   tmp/r1/srv/preview-buy.html 已通过)+「自动购买功能默认开启免费帖子」;用户当场拍板
    //   「不做收费帖那枚开关,收费帖一律手动」。
    //   ★ 现场取证(真实 tid-2973029;探针 tmp/r1/pay2.js + pay3.js,报告 report-pay{2,3}.json):
    //     购买块 = div#read_tpc > h6.quote.jumbotron(807×28 @216,499),两件东西:
    //       span.s3「此帖售价 0 SP币,已有 9474 人购买」
    //       input.btn.btn-danger(onclick = location.href='job.php?action=buytopic&tid=2973029&pid=tpc&verify=c23ac2aa')
    //     紧跟一个**兄弟** blockquote.blockquote(警告条:#f8f8f8 底 + 2.67px #eee 左边线 + #736933 字)。
    //   ★ 那枚按钮是 **GET 跳转** —— 原生点一下整页跳到 job.php,这就是「刷新」的来源;
    //     令牌就在同一页的内联脚本里(var verifyhash = 'c23ac2aa')。
    //   ★ job.php 响应实测(**只走错误路径**、假 tid、零账户副作用):200 + 31,810 字节 HTML、
    //     没有 302、标题「帖子ID非法」⇒ 同源纯 GET、返回纯 HTML ⇒ **能 fetch 而不导航**。
    //   ★ 站点自己的 js/pw_ajax.js 是隐藏 iframe 提交助手、里面没有任何 buy action
    //     ⇒ 官方没有购买 ajax,要免刷新只能自己发那条 GET。
    //   ★ 新段 15g initBuyFramework():① 换成审核稿那张卡(价签 + 人数 + 按钮 + 警告条)
    //     ② 免费帖自动购买(新开关 spp_autobuy_free,默认开)③ 收费帖一律手动,且手动也先弹就地确认条。
    //   ★ 成功判定**不看响应体**:买完再 GET 一次本页,看返回 HTML 里还有没有 action=buytopic
    //     —— 成功页形态仍未实测,任何软判据都可能报「假成功」。
    //   ★ 新开关同时登记进 SPP_PANEL_DEFS 与 buildOverlay 的手写 HTML(2.53.32 栽过一次)。
    // ★★ 2.53.38:markPagerRows 的两条收编路径合成一条(.pages 出发,各自找宿主)。
    //   ★ 2.53.37 是「先从 .t3 收一遍、再从 .pages 补一遍」—— 验证三次里有**一次**出现
    //     「.t3 里那条只中一半」(尺寸中、颜色与 hardenPager 的内联 !important 都没中),
    //     未能复现;但内联 !important 是最高优先级,它没打上说明那一轮那条 row 压根没被处理,
    //     而两条路径的不对称正是最可能的来源。统一入口后不存在「漏一类」的可能。
    //   ★ 行为不变:有 .t3 祖先的 .pages 依旧把 .spp-pgbar 打在 .t3 行上(版块页/帖子页原样),
    //     裸 .pages 依旧就地包一层 div.spp-pgbar(搜索页下半条)。
    // ★★ 2.53.37:搜索页(search.php)的翻页条没被美化。
    //   ★ 取证(tools/probe/search-post.js:搜索结果靠 POST+session,GET 只拿到空壳,必须自己 POST):
    //     结果页有两条 .pages —— div.pages < div.t3 < div#main(上半条,有 .t3)与
    //     div.pages < div#main(**下半条裸挂**,用户红框圈的就是它)。
    //   ★ 两道门把它挡在外面:initBoardToolbar 的 `!PAGE.board && !PAGE.read`(连 CSS_BOARDTOOL 都没注入)
    //     与 markPagerRows 只认「含 .pages 的 .t3 行」。
    //   ⇒ 本版:① 门加 PAGE.search ② CSS 注入条件放行 search ③ markPagerRows 给裸 .pages 就地包一层
    //     div.spp-pgbar(复用同一套规则,不复制 CSS)。
    //   ★ 基线实测:两条翻页条都是站点原样 —— 白线框 rgb(221,221,221) / 黑字 / 当前页灰底白字 /
    //     Go 输入框白底 rgb(255,255,255)。
    // ★★ 2.53.36:两条需求 ——
    //   ①「增加一个功能,可以在悬浮面板中关闭(默认打开)」⇒ 含图楼层悬浮窗总开关
    //      spp_imgidx_show(默认开):关掉后 initImageIndex() 直接不建 #spp-imgidx。
    //   ②「对于白色字体的文本,脚本自动把它转化为加粗黑体(参考网页文本:urusai_cc)」
    //      ⇒ 新段 15f initWhiteTextFix():帖子正文里发帖人写的白字(白压白、对比度 1.00、等于看不见)
    //        自动转成加粗深色 #111。
    //   ★ 取证(tid-2966403):正文 .tpc_content 底色纯白(div.t5),楼主那 6 个图床名都是
    //     <span style="color:#ffffff ">;全楼低对比(<1.6)元素就这 6 个。
    //   ★ 只用内联设色的元素当候选 + 实测对比度判定 —— 不用 [style*="color:#fff"] 字符串匹配:
    //     内联写法太多样会漏,而引用块/深色底里的白字本来就正常、不能一刀切。
    //   ★ 这两枚开关同时登记进 SPP_PANEL_DEFS 与 buildOverlay 的手写 HTML(2.53.32 栽过一次)。
    // ★★ 2.53.35:底部「帖子/版块浏览记录」浮层的关闭按钮 —— 用户拍板「28 px」(热区不变,换图标)。
    //   ★ 线上取证(真实 tid-2966403):关掉 close.gif 拉出来一看,画布 16×16 里
    //     **有效图形只有中间约 7×7**(全图仅 33 个不透明像素,色值 #686868),其余全透明。
    //     ⇒ 2.53.34 把热区做到 28×28、图标恢复 16px 之后用户仍报「还是不够大」,
    //       根因是**图形本身只有 7px**、而且放大会糊 —— 跟热区无关,调数字救不了。
    //   ⇒ 本版:热区仍是 28×28(标题栏高度一点不变),图标换成 span.fr 两个伪元素画的矢量 ×
    //     (2×14 圆头细线,各转 ±45° 交叉居中);站点原图只 display:none,结构一个字没动。
    //   ⇒ hover:底色泛白 + × 提亮到纯白。
    // ★★ 2.53.34:底部「帖子浏览记录 / 版块浏览记录」浮层的**关闭按钮太难点**。
    //   ★ 取证(真实 tid-2966403;tools/probe/read-logclose{,2,3}.js):
    //     浮层 HTML 来自 pw_ajax.php?action=readlog 原始返回,标题行是
    //       <th class="h" onmousedown="read.move(event)"> + <span class="fr" onclick="closep();"><img src="images/close.gif"></span>
    //     ⇒ 挂事件的只有 span.fr,img 自己不挂 ⇒ **热区就是 span 的盒**。
    //   ★ 两条我自己造成的伤害:close.gif 原生 16×16,换皮那行写死 12×12 !important;
    //     span.fr 又被 float 成 block ⇒ 盒收紧到 12×12(144 px²),难怪点不中。
    //   ★ closep 源码 = read.menu.style.display = "none"(不依赖 this/坐标)⇒ 放大 span 的盒就是放大热区。
    //   ⇒ 热区 28×28(图标恢复 16×16 居中),面积 144 → 784 px²;hover 给底与图标双双提亮。
    //     拖拽(th.h 的 read.move)与关闭行为都没动。
    // ★★ 2.53.33:补 2.53.32 漏掉的那一半 —— 「含图楼层靠边自动隐藏」开关的**渲染行**。
    //   ★ 2.53.32 只在 SPP_PANEL_DEFS 里登记了键,忘了 buildOverlay() 里那一行手写 HTML
    //     ⇒ 界面上根本没这枚勾选框,tools/verify/settings-panel.js 当场拦下:
    //     「SPP_PANEL_DEFS 登记了但面板没渲染:spp_imgidx_edge」。
    //   ★ 教训:buildOverlay 是**手写 HTML**、不遍历那张表(803 行注释早就写着这条),
    //     以后每次加开关都要回来补一行。
    // ★★ 2.53.32:含图楼层悬浮窗两处改(用户)——
    //   ① 「不要文本第几张」⇒ 删掉输入框的 placeholder,框内留空。
    //   ② 「增加一个靠边自动部分隐藏的功能(悬浮设置里可以关闭这个功能)」:
    //      贴边时 translateX(calc(100% - 68px)) 缩进去,只留左边 68px(恰好露出绿点 + 「含图楼层」标题),
    //      鼠标移上去 / focus 进去滑回完整面板,过渡 .22s。开关 = SPP_PANEL_DEFS「功能开关」组的
    //      spp_imgidx_edge(def: true,与模块里 SPP_CONFIG.bool 那句逐字同值)。
    //   ★ 设置面板保存后是 location.reload(),所以模块只读一次即可。
    // ★★ 2.53.31:含图楼层面板行太挤(截图里「张」压在右沿上,只剩半个字)。
    //   算账:面板 158 − 内衬 18 = 140px 可用,一行要塞 楼层号 34 + 总数 24 + 输入框 46 + 「张」12
    //   + 3×7 间隙 = 137px,卡着边 ⇒ 加宽到 174px、间隙收到 6px(最宽 133px,留 20px 余量)。
    //   它在右下角是浮层,加宽不挤正文。
    // ★★ 2.53.30:含图楼层悬浮窗两处改(用户)——① 挪到**右下角**(右→左→右下 三次定位终稿);
    //   ② 补上「总数」元素(用户:「楼层和输入框之间还有一个显示总共有多少张图片的数字的元素,这个你遗漏了」)。
    //   · 上一版把总数直接当成输入框的 value ⇒ 行里看不到独立的总数。
    //   · 现在行结构:楼层号 → 总数(静态 / 主色绿 / 等宽数字)→ 输入框(placeholder「第几张」)→ 张;
    //     输入框只管「跳第几张」,回车滚到该层第 k 张。
    //   · 定位改 right:14px; bottom:16px(不再竖向居中):右侧导航面板竖向居中、底边约 530px,
    //     这块从底部往上长,两者不叠。
    // ★★ 2.53.29:含图楼层悬浮窗由右侧改到**左侧**(用户:「这个悬浮窗移动到左侧」)。
    //   只动定位一条:right:62px → left:14px,竖向仍居中;其余(行样式/交互/折叠)一字未改。
    //   ★ 右侧那 62px 偏移原本是为了避开已有的右侧导航面板(#spp-nav-panel),挪到左边后不再需要。
    //   ★ 已知代价:read.php 左栏是楼层头像(约 x 13–163),面板会压住头像列 —— 要挪就改这一条 left。
    // ★★ 2.53.28:read.php 新增「含图楼层索引」右侧悬浮窗(用户从三套预览里选定**方案 B**,
    //   并补充「有多张图片和一张图片的一样要有数字」⇒ 每行都带数字输入框)。
    //   · 行 = 楼层号(GF / B4F …)+ [输入框:该层图片总数] + 张;点行跳该层,改数字回车跳该层第 k 张图。
    //   · 实测(read.php?tid-2966403):31 层里 DOM 带 img 的 7 层,剔掉表情/图标后真的含图只剩 3 层
    //     (GF=20 张 / B4F=1 / B30F=1)。楼层号在 .tiptop > .fl,锚点是 th.r_one 的 id(td_tpc / td_<pid>)。
    //   · ★ 数量按 DOM 里的 img 个数算,**不看 naturalWidth** —— 懒加载的图此刻 0×0,但它确实存在。
    //   · 面板挂 body(不进 form),定位 right:62px 避开已有的右侧导航面板;点标题可折叠。
    //   · 滚动用 IntersectionObserver 联动高亮;MutationObserver(400ms 防抖)只在内容真的变了才重建。
    // ★★ 2.53.27:楼层头像方框**跟着比例长**(用户回访:「你这个显示头像的容器还是没变化」)。
    //   ★ 2.53.26 我做成了「容器锁正方形、只改 object-fit」—— 那正是我表格里「容器锁正方形」的字面,
    //     但用户要的是容器要变 ⇒ 按方案 c 里「竖图最高 200 / 横图最高 150」那半句重做。
    //   ★ 新规则(宽一律 150,表格列宽不动):框高 = clamp(150×高宽比, 150, 200)。
    //     方图 1.000 → 150(与改动前一致);4:3 竖图 1.333 → 200 完整显示;更长的 1.352+ → 封顶 200 + cover 裁切;
    //     横图一并保持 150(不压矮,免得左栏塌),contain 完整显示。
    //   ★ 150↔200 的伸缩是纯 CSS(width 定死 + height:auto + max-height:200),
    //     只有「超限才裁」必须 JS 判 —— object-fit 拿不到图自己的比例。
    //   ★ 门槛取 1.35:内容宽 142 / 内容高 192 ⇒ 192/142 = 1.352 才是真被顶住的点。
    // ★★ 2.53.26:楼层头像按原始长宽比显示(用户从 a/b/c 三案里选定「c 折中」)。
    //   ★ 根问题:站点那枚头像 img 被钉成 width:150px + height:150px ⇒ 非方图一律**拉伸变形**。
    //     实测(read-recon2.js):TD/IMG 计算尺寸都是 150×150,站点图 images/face/a7.gif 自然尺寸也是 150×150。
    //   ★ 方案 c:**容器一个字不动**(150×150,表格列宽与版面零变化),只改 object-fit ——
    //     默认 contain(完整显示,方框本来就白底、留白看不出来);JS 量出 naturalWidth/Height,
    //     高宽比 > 1.4(比 4:3 还细长)或 < 0.71(比 4:3 还扁)才加 .spp-ava-crop 走 cover 裁切。
    //   ★ 为什么必须 JS:object-fit 没法把「图自己的比例」写进条件,CSS 读不到 naturalWidth/Height。
    //   ★ 站点头像带 loading="lazy":没进视口时 naturalWidth=0 ⇒ 挂 load 监听 + 滚动 rAF 节流补扫;
    //     图床挂掉(0×0)时摘掉裁切类退回 contain,不把空图裁成一块色。
    // ★★ 2.53.25:撤销 2.53.24 里那条自己造出来的图层问题(.dropdown{position:relative})。
    //   ★ 实测(tools/probe/read-stack.js):未改动时那枚下拉**本来就在最上层** ——
    //     elementsFromPoint(中心) 栈 = [a.apeacemaker, a.apeacemaker, div.dropdown-content.menu(z=100),
    //     TH#td_tpc.r_one, TABLE.js-post, DIV.t5.t2],它上面只有自己的两个子元素。
    //   ★ 加了 position:relative 后:绝对定位包含块塌缩成 41×19 的触发盒,而父盒 DIV.fr 是 overflow:hidden
    //     ⇒ 下拉被裁掉,中心点量到的是 TABLE.js-post。也就是说「点不到」是 2.53.24 自己引入的。
    //   ★ 现在只给下拉自己提 z-index(3000),父盒定位一律不动。
    // ★★ 2.53.24:read.php 楼层操作条两件事(用户批注:红框撤按钮 / 黄框修下拉)。
    //   ★ 实测(tools/probe/read-recon.js + read-recon2.js,read.php?tid-2974870):
    //     ① 楼层「更多」= a.dropB[id^="td_read_"],共 24 枚,含 41x19,在 .tiptop > .fr > SPAN 里;
    //        页眉那枚「更多」也是 .dropB(#td_u)⇒ 规则必须带 id 前缀,否则一起误伤。
    //     ② 「屏蔽」下拉是**站点/peacemaker 的 CSS hover 下拉**(不是 read.open 万能层):
    //        div.dropdown-content.menu,position:absolute / z-index:100 / radius:12px /
    //        background rgb(14,17,22) 深底;内容两枚 a.apeacemaker(屏蔽此人 / 屏蔽头像)。
    //   ★ 改法:① 红框那枚 display:none;② 下拉改白底(白卡上的暗块)+ z-index 3000/3001,
    //     并把触发盒 .dropdown 抬成一层,免得被正文图片盖住。
    // ★★ 2.53.23:CSS_MENUPOP 改**全站注入**(用户:index.php 这里的还是没变)。
    //   ★ 实测(受控浏览器 · index.php):整页 20 张 <style>,没有一张带 body > div.menu ⇒ 首页没皮;
    //     当场 read.open("menu_u","td_u") 复现弹层:BODY > DIV.menu.pad / bg rgb(255,255,255) / 圆角 0 / 黑字。
    //   ★ 根因:2.53.19–2.53.22 这份皮只有两个入口 —— CSS_BOARDTOOL(版块页/帖子页)与 initTaskPage(任务页),
    //     首页两头都不吃。而站点这枚弹层是全站通用(read.menu 万能层),页眉「更多」每个页族都有。
    //   ★ 改法:脚本顶层无条件 GM_addStyle(CSS_MENUPOP) 一次;另两处入口撤掉,避免同一份 CSS 注入两遍。
    // ★★ 2.53.22:门牌定稿 body > div.menu.pad(第三版,这版是在浏览器里**真的打开量过**的)。
    //   ★ 复现(受控浏览器):win.read.open("menu_u","td_u") —— 站点自己弹层就是 read.open 驱动;
    //     显示出来的节点实测是 BODY > DIV.menu.pad(read.open 把 #menu_u 的 className 与 innerHTML 抄进
    //     万能层 read.menu),#menu_u 自己永远 display:none、只是模板。
    //   ★ 2.53.19 的 body > div.menu:has(> ul) 其实是对的;2.53.20/21 改成 #header > … 反而全错。
    //   ★ 用 .pad 收口:正好把这枚页眉菜单弹层与 sendmsg 建的 readlog 浮层(纯 .menu)分开。
    // ★★ 2.53.21:门牌再收一道 —— #header > div.menu:has(> ul) → #header > div.menu.pad(不依赖 :has())。
    //   ★ 受控浏览器实测:那边已装 2.53.20,#menu_u 量到 bg rgb(13,20,32) / 圆角 14px / 条目 display:flex
    //     ⇒ **脚本这条路是通的**,白的只可能是「浏览器里那份没更上」。
    //   ★ 但 :has() 这条依赖没必要留:页眉 5 枚下拉本来就都带 .pad 类,按类认一样精确。
    // ★★ 2.53.20:修 2.53.19「没生效」——**选择器认错了门牌**。
    //   ★ 现象(用户:脚本我更新了,没有生效 + 一张还是白框的截图)。
    //   ★ 查法:受控浏览器全页 dump(tools/probe/menu-where.js)逐个 matches() 验门牌。
    //   ★ 真相:这枚「更多」下拉不是站点建在 body 下的浮层,而是**加载时就预置在页眉里的静态节点** ——
    //     BODY > DIV#wrapA > DIV#header > DIV#menu_u.menu.pad > ul[style="width:7em;padding-top:.5em"];
    //     页眉里同族还有 menu_hack / menu_skin / menu_profile / menu_sort 四枚,全 display:none 预置、全白底。
    //     2.53.19 的 `body > div.menu:has(> ul)` 对这 5 枚 matches() 全是 false ⇒ 一条没命中。
    //   ★ 我上一轮为什么会认错:探针用 [body > div*, .menu …] 一大串挑「可见的」,
    //     抓到的可见节点其实是 #menu_u 被同一串里的 `.menu` 命中的结果,我把列表里写过的 body > div.menu 当成了它的门牌。
    //     教训:**门牌要逐个 matches() 验,不能用宽松列表抓到的结果反推层级。**
    //   ★ 改法:选择器换成 `#header > div.menu:has(> ul)` —— 指名页眉直系;
    //     送消息建的 readlog 浮层(body > div.menu,表格式、无 ul)与 #main 里的 #menu_cate / #menu_post 都不受影响。
    // ★★ 2.53.19:「更多」下拉(body > div.menu.pad 列表式浮层)暗色适配 —— 用户从五套审核稿里选定【方案 D】。
    //   ★ 实测(plugin.php?H_name-tasks.html 活体):点 #td_u 后站点在 body 下建
    //     <div class="menu pad" style="opacity:.95;left:566px;top:135px;z-index:3000;visibility:visible">
    //       > ul[style="width:7em;padding-top:.5em"] > li > a = 个人首页 / 我的收藏 / 好友近况;
    //     原皮 background:#fff + 1px #bbb + 直角 + 12px 黑字 —— 星空暗底上一坨白方块。
    //   ★ 根因不是「没写皮」,是**皮写错了地方**:那份 body > div.menu 换皮一直在 CSS_BOARDTOOL 里,
    //     而任务页 21 张 <style> 里没有一张带 SPP-BOARDTOOL-BEGIN(活体 dump 证实)⇒ 只有版块页/帖子页吃得到。
    //   ★ 改法:把它提成模块级常量 CSS_MENUPOP(一份定义),CSS_BOARDTOOL 原位置改成 ${CSS_MENUPOP},
    //     再由 initTaskPage() 补一次 GM_addStyle —— 版块页/帖子页/任务页三处同皮,以后改皮不会再漏页族。
    //   ★ 新样式一律挂 body > div.menu:has(> ul):同一个 .menu 还装着「最近浏览的帖子/版块」浮层
    //     (表格式 th.h + tr.tr3),那块皮不能动。
    //   ★ 方案 D 的形状:14px 圆角 + 1px 淡边 + 深投影,条目 flex 8px 14px 内衬、行间 1px 分隔线、
    //     左侧一枚主色小圆点(悬停发光),文字 #cfd9e3 → 悬停 #eafff8 + 行底 7% 白。
    //   ★ ul 的 width:7em / padding-top:.5em 是站点写在**内联 style** 上的 ⇒ 全部 !important 覆盖。
    // ★★ 2.53.18:任务页导航「社区论坛任务」的字重按实测对齐「屏蔽配置」(用户:字体还是有差异)。
    //   只读 dump 同页并排量:屏蔽配置 = Arial 12px / weight 700 / lh 28 / #fff / padding 0 10px;
    //   这枚其余全同、只有字重 400 ⇒ 2.53.17 把「朴素」读成了「不粗」,改回 700。
    //   字体族/字号/行高/内衬本来就已经一致,本版不再动。
    // ★★ 2.53.17:任务页页眉「社区论坛任务」当前页药丸朴素化(用户批注:不要红色 UI 包裹,
    //   像左侧「屏蔽配置」那样只留白字)。贴底色位图(background-image:none)+ 撤边框/投影/字重,
    //   认链接地址([href*="H_name-tasks"])不认 .current;作用域沿用 html.spp-hm-star.spp-hm-task,
    //   只在任务页生效。
    // ★★ 2.53.16:搜索页「提 交 / 重 置」采用**方案 A**(用户从三套审核稿里选定)——
    //   与引用页「提交 / 预览」同一套胶囊语言:提交 = 实心主色胶囊,重置 = 白底主色描边胶囊,
    //   同一度量(26 高 / 0 16 内衬 / 999 圆角 / 12px 600),间隙 10px。
    //   旧那套(2.52.56–2.52.58)只把重置按提交的尺度重做、颜色留成红 —— 两颗两种语言,本轮统一。
    // ★★ 2.53.15:短消息图标定稿 —— 用户:「SVG 图片,只要白色线框亮着就好了」。
    //   stroke 从 currentColor 改成固定 #fff(不再跟主题字色),未读变色那套一并撤掉
    //   ⇒ 页眉上永远是一枚亮着的白色信封线框(形状 = 用户已通过的新稿 A)。
    // ★★ 2.53.14:版块页页眉两件事(用户批注,thread_new.php?fid-9-page-1)。
    //   ① 短消息图标:站点老式 GIF(images/colorImagination/no-shortmail.gif,14×11)
    //      换成内联 SVG 16×12 / stroke=currentColor(用户从对比稿里挑了「新稿 A」,纯信封无线点)。
    //      ★ 关键约束:a#td_msg 是 phpwind 短消息轮询的挂载点 —— <a> 与 <img> 必须原样留着,
    //        img 只 visibility:hidden 让位(保留 17×13 热区),SVG 绝对定位叠上去;
    //        MutationObserver 盯子树:站点重写 innerHTML 就拿回 SVG,img 换成未读那张就点成提醒色。
    //   ② 搜索 / 社区论坛任务两枚链接补 target=_blank + rel=noopener noreferrer(用户定案只做这两枚)。
    // ★★ 2.53.13:快捷键工具栏三行「对齐长度」。
    //   ★ 现象(用户批注,post.php?fid-9 截图):三组快捷键行的右端各停在各处,
    //     与编辑区右缘对不齐(红框画在行右端那条空档上)。
    //   ★ 根因:buildToolbar 里「三行」是用 flex-basis:100% 的占位 span 拼出来的,
    //     行本身不是盒子 ⇒ 行的长度 = 内容长度,三行长度天然不等。
    //   ★ 改法:每行一个真行盒(flex:0 0 100% + width:100% + 下边框,末行去边框)⇒ 三行等长;
    //     另加 syncEditorWidth(),把工具栏与文本框锁到同一个框(左=父级内容左缘,
    //     右=表情栏左缘−14 或父级内容右缘),插入时 + 补两拍 + resize 各量一次。
    // ★★ 2.53.12:修 2.53.11 的「白底画不出来」。
    //   ★ 现象(用户截图):底部选项块下面是深色星空,主题回顾那块根本没变白。
    //   ★ 根因:白底挂在 .t.f_one::after 上,而那张卡自己 computed overflow = hidden
    //     ⇒ top:100% 的伪元素被整块裁掉。上一轮的「验收」只读了计算值
    //     (getComputedStyle(card,'::after').height = 455px)——**计算值不知道裁剪**,
    //     所以量到了、屏幕上却没有。教训:验收必须看图(截图),不能只看数。
    //   ★ 改法:白底改挂 #main::after(#main overflow 是 visible,不裁),
    //     起点 = syncPostFill 量的「卡底 − #main 顶」(写到 #main 的 --spp-white-top),
    //     终点 bottom:0 ⇒ 铺到 #main 底;z-index:-1 + #main{z-index:0} 建层,不盖内容。
    //   ★ 本轮起用 tools/build/land-kit.js 落地(版本三连/断言/发布/归档一次做完),
    //     活体取证走 tools/probe/live.js(自动起服务 + 发布当前脚本 + 截图)。
    // ★★ 2.53.11:引用页收尾(2.53.10 用户截图的三处红批注)。
    //   ★ 先记一条最重要的事实:那张截图里表情栏仍是 195 宽,而 2.53.10 已把它改成 260 ——
    //     ⇒ 那一屏跑的还是【旧版 CSS】(装完没刷新/没生效)。所以 ②(选项块关于轴同心)
    //     与 ③(白卡向下延长)本轮不再重复实现,只补它们各自漏掉的配套:
    //     · ①「美化UI和预览保持一致」:站点提交是 <input type=submit name=Submit>(深红底 +
    //       老式背景位图、直角、13px),脚本那枚 👁 预览是描边胶囊 —— 一深红一浅绿并排。
    //       两条改成同一套尺度(24 高 / 999 圆角 / 12px / 600),主按钮实心、次按钮描边。
    //     · ③ 配套:主题回顾那几张表是站点按**深色星空**配的灰字,白卡铺上去就「看不清」——
    //       把文字/链接/分隔线换成白底可读的一套。作用域只认 #main 的直接子节点(表单之外)。
    //   ★ 这两条都写在 POST_CSS 的 .spp-quote-on 段里(引用页/编辑页限定)。
    // ★★ 2.53.10:引用页(post.php?action-quote / action-modify)三处批注。
    //   ①「加大表情框的大小」:引用页的表情栏实测只有 195×338、网格 170×150(26px 的 5 列),
    //     只用了可用轨道(面板顶 441 → 选项表顶 945 = 504)的三成。两处一起放大:
    //     栏宽 200 → 260(编辑器左内衬 205 → 274,留同样的 14px 间隙 ⇒ 每行 5 → 6 列),
    //     取景高度改成按**轨道**算(网格 150 → 约 309,表情 26 → 30px)。
    //   ②「这里应该是中心对齐」:选项表那条固定的 margin-left:79px 是给非引用页
    //     「编辑器左缘 226」那套量的;引用页表情栏留在左侧,编辑器内容从 218 起,
    //     于是选项表中心 563 对着编辑器中心 728 —— 偏左 165px。改成与编辑器同心:
    //     编辑器左缘 = .t.f_one 左缘 + 274、右缘 = .t.f_one 右缘
    //     ⇒ 编辑器中心 = .t_f_one 中心 + 137 ⇒ 966 宽的表左缘 = 50% − 346。
    //   ③「延长」:白卡(.t.f_one)下面那块是深色星空(主题回顾就浮在上面),
    //     用一枚 z-index:-1 的伪元素把卡底到 #main 底那段铺成同一张白卡
    //     (高度走 --spp-postfill,由 JS 量 —— 主题回顾条数一变那段就不同)。
    //   ★ 三处都只作用于引用页:新加一枚 html.spp-quote-on(applyPostCss 里打),
    //     因为 .spp-post-on 是非引用页与引用页共用的,分不出这两套布局。
    // ★★ 2.53.9:本轮两条需求(用户原话)——
    //   ①「在悬浮设置面板增加一个按钮,用于选择是否使用『用户名 + 彩色背景头像』替换默认头像,默认关闭」;
    //   ②「增加一个功能,用于恢复用户的默认头像」。
    //   ★ ① 是「搬表 + 翻默认值」:spp_hide_default_avatar 这个键自 2.52.63 起一直挂在
    //     SPP_HIDDEN_SWITCHES(只读登记、界面上没有入口),本轮搬进 SPP_PANEL_DEFS 的「功能开关」组,
    //     成为第二枚可勾选布尔行;默认值同步 true → **false**。面板此前从不渲染这个键,
    //     所以绝大多数用户的 GM 存储里根本没有它 ⇒ 翻默认值就是真的「默认关闭」。
    //   ★ ② 是一条「撤标记、不重建节点」的还原链路:站点那枚 <img> 一直在 DOM 里(只是被
    //     .spp-ava-off 藏起来)、彩色块是脚本新建的节点、用户名那行也只是被打了类 ——
    //     所以恢复 = 去类 + 摘块,一个站点节点都不用重建。
    //   ★ 还原入口挂在 15c 的模块级出口 sppAvaRestore 上(与 sppAvaRefit 同一手法),
    //     面板里那枚按钮既没有 data-key 也没有 data-type ⇒ 面板自检与保存循环都自然跳过它,
    //     不必在两处各开一个「排除某一行」的例外口。
    // ★★ 2.53.4:帖子页左栏「默认头像块」与头部那行操作栏(用户三处圈注的收尾一轮)。
    //   ★ 三句原文:①「没有图床或默认图床的用户头像的名字 UI 版块,字体尤其是中文字体太大了,
    //     要自动适配大小、必要时换行」;②「它底部框选的那个名字可以不用了」;
    //     ③ 头部行(楼层号 / 时间 / 只看xx / 屏蔽 / 小中大 / 顶端 / 回复 / 引用 / ⊕)
    //     「需用 UI 框包裹,提升高级感并适配白色背景,避免古早 BBC 论坛的廉价感」。
    //   ★ ① 发生在 2.52.98 那条「ID 放进图片框内」的延长线上:当时按**字数分五档**字号
    //     (is-s2 = 46px 起),宽度够、视觉上却太大 —— 实测 120×120 的框里「芜湖」两个字
    //     46px 高的方块字把整块占满,而 8 字的英文 ID 只有 17px,同一列里两种尺度对不上。
    //     现在改成**按真实排版量出来的自动适配**:JS 只做「二分测出最大可放字号」,
    //     字号上下限与折行门槛仍是 CSS 里的**三个**自定义属性
    //     (--spp-ava-fs-max 上限 / --spp-ava-fs-min 下限 / --spp-ava-fs-one 折行门槛),尺子还在 CSS。
    //     ★ 2.53.4 复校:这里原写「两个」,漏掉了 2.53.4 自己新增的 --spp-ava-fs-one(三把尺都写在
    //       READ_CSS 的 .spp-ava-initial 上,见「2.52.48 首字母头像块」那段)。
    //   ★ ② 站点在头像框下面又把用户名印了一遍(div[align=center] > a > strong);
    //     框里已经写着 ID,这行就是重复 —— 只对**换过块**的楼层摘掉,真头像楼层照旧。
    //   ★ ③ 头部那行站点只有一条 1px 点线(.tiptop{border-bottom:1px dotted #ccc}),
    //     左边是裸的「B10F 2026-10-01 22:53」,右边一串「|」分隔的裸链接。
    //     改法是把整行收进一枚浅灰凹槽(#f8fafc + 1px #edf1f7 + 10px 圆角,与 2.53.2
    //     悬停卡同一套容器语言),楼层号做成一枚白底胶囊,操作项做幽灵胶囊、竖线分隔符
    //     由 JS 包成 .spp-tp-sep 后画成 1px 细线(文本节点 CSS 选不中,必须包一层)。
    // ==================== 运行版本标记(2.53.3)====================
    // ★★ 2.53.3:悬停状态卡「图标行」换掉站点 2008 年的位图 sprite(用户 2.53.3 圈出那 4 枚图标:
    //   「太落后了,有一种古早 BBC 论坛的网页感」→ 出三方案预览对比后拍板 B「图标 + 常驻短标签」)。
    //   ★ 那 4 枚到底是什么(真页面实测,read.php?tid-2948255):images/colorImagination/read/
    //     readicon.png —— **一整张 16×180 / 1318 字节**的 PNG-8 精灵,9 个槽位只用了 4 个:
    //       readprofile 0 -44px 橙框文档 / readmessage 0 -64px 橙色喇叭 /
    //       readonline  0  -4px 绿人像+加号 / readmagic 0 -84px 品红螺旋。
    //   ★ 病根不是「像素低」,是**没有文字**:站点用 text-indent:-24000px 把「作者资料 / 发送短消息 /
    //     加为好友 / 使用道具」四个词踢到屏幕外,只留 li[title] 原生提示 ⇒ 只能逐个悬停猜。
    //   ★ 改法(模块 15e 的 decorateIcons):同一套 1.6px 线性 SVG + currentColor + 常驻短标签
    //     (资料 / 短消息 / 加好友 / 道具);站点位图 div 只加类隐藏、不摘除,<a> 的 href/onclick 不碰。
    //   ★ 真浏览器实测(预览页 6 张卡并排量):卡片 198×242 → **198×267**,图标条 31 → 56px 高;
    //     3 枚图标(茶馆 tid-2948255 实测没有「使用道具」)时四格均分依旧成立。
    //   ★ 短标签为何只留 2-3 字:min-width 198px 四格均分后每格约 43px,10.5px 字号下
    //     「发送短消息」5 字约 53px 会撑破格子(预览实测换行)⇒ 缩成 资料 / 短消息 / 加好友 / 道具。
    // ==================== 运行版本标记(2.53.2)====================
    // ★★ 2.53.2:悬停状态卡「加 UI 包裹」(用户 2.53.2 看着 2.53.1 的成品说:「UI包裹一下,
    //   要不然太廉价了」,并圈出左边那一列裸标签)。
    //   ★ 问题在哪:2.53.1 只把 9 行做成「左标签 / 右值」,但整块仍是**白底裸行** ——
    //     标签与值之间没有分隔、没有任何容器,看着就像临时拼的一堆文字。
    //   ★ 改法(纯 CSS,模块与 DOM 一字未动):
    //     ① 图标行与明细行**各自进一个浅灰凹槽**(#f8fafc + 1px #edf1f7 + 9px 圆角);
    //     ② 明细行之间加一条 1px 内阴影分隔线(inset 0 1px 0 #eaeff7),行距 3.5px;
    //     ③ SP币 那枚值做成金色小胶囊(#fff5e2 底 / #f3e2c2 边 / #a4670f 字),
    //        它本来就是这张卡最该被一眼看到的信息;
    //     ④ 所有值改 tabular-nums(数字等宽,右对齐时每行末位能对齐)。
    //   ★ 卡宽下限 188 → 198px(凹槽左右各多 9px 内边距,不留够会把值挤成换行)。
    // ==================== 运行版本标记(2.53.1)====================
    // ★★ 2.53.1:帖子页「悬停头像弹层」白底化(用户 2.53.1 需求:「鼠标悬停用户头像时,会弹出一个
    //   UI 状态栏,请美化该 UI,适配为白色背景」)。
    //   ★ 站点默认是 1px #bbb 灰边 + 直角 + 零阴影的白盒,9 行状态是**裸文本 + <br>** 拼出来的
    //     ⇒ 光靠 CSS 只能改盒子外观,标签 / 值没法分列、标签也没法单独上色。
    //   ★ 于是新增模块 initUserStatusCard()(15e):把站点那个 120×5 占位**之后**的节点按 <br>
    //     切行、解析成「标签 / 值」,重排成 .spp-uc__rows(左右两列);外观全部交给 READ_CSS
    //     的 2.53.1 段(白底 + 12px 圆角 + 阴影 + 自绘小三角,站点那枚 .co 灰箭头藏掉)。
    //   ★ 恒为白底、不跟星空/毛玻璃主题走:它是一张浮在帖子上的白纸,深底反而糊。
    //   ★ 图标行(作者资料 / 发送短消息 / 加为好友 / 使用道具)与它们的内联 onclick 一个都没动。
    // ==================== 运行版本标记(2.53.0)====================
    // ★★ 2.53.0:新增「自动展开隐藏楼层」(默认开,⚙ 脚本设置 ▸ 功能开关 里可关)。
    //   需求(用户原话):「增加一个功能,默认开启。在设置面板中,通过悬浮按钮弹出的设置面板
    //   可以关闭,功能:自动展开隐藏楼层的功能」。
    //   ★ 站点机制(真页面实测,tid-2948255 / tid-2942976):被折叠的楼层是**服务端直出的
    //     半折叠形态** —— 真正的内容节点(th.r_two > div.user-pic、div.tpc_content、
    //     tr.tr1.r_one 评分行)带 .js-hidepuremark 且带内联 style="display:none";可见的只有
    //     占位 div.fl.bianji.js-puremark-content(内容就是「mark」两个字)与旁边那枚
    //     a[onclick="return expandpuremark(this)"](文字「▼展开」)。
    //   ★ 上一轮已确认:那枚「▼展开」与 expandpuremark() 都出自**站点**内联脚本、不是本脚本
    //     加的(全文搜 expandpuremark 命中 0)。所以本轮做的是「替用户把这一下点掉」。
    //   ★ 不调站点函数:本脚本带 @grant ⇒ 跑在沙箱里(见下面 2.52.102 那段真凶),跨 realm 调
    //     页面函数要绕 unsafeWindow;而这一步只是三个 display 开关,纯 DOM 复刻更稳,也不依赖
    //     站点的 jQuery。站点那两句 .tiptop 内联 .css() 不复刻 —— 本脚本 READ_CSS 对 .tiptop
    //     有 !important 规则,复刻了也看不见(等于写一段永远不生效的代码)。
    //   ★ 幂等:只处理「站点仍是折叠态」的楼层(判据 = 那块 .js-hidepuremark 的 computed
    //     display 还是 none),重复扫描不会二次改动。
    //   ★ 设置项落在 SPP_PANEL_DEFS 的新组「功能开关」(data-type="bool"):本脚本的面板
    //     从 2.52.64 起只渲染两类行(img_json / number),这是撤销该决定后的**第三类行**,
    //     所以渲染、回填、保存、行点击四处都补了 bool 分支(见 tools/verify/settings-panel.js
    //     里「渲染什么就处理什么」那条口径)。
    // ==================== 运行版本标记(2.52.102)====================
    // ★★ 2.52.102:真凶找到了(用户第四次说「脚本没有生效」,前三次我都在修别的东西)。
    //   根因:头部带 @grant ⇒ Tampermonkey 把脚本放进**沙箱**,沙箱里的 window 不是页面 window,
    //   而那套 `read.open / read.close` 是站点在页面上下文建的 ⇒ `window.read` 恒为 undefined ⇒
    //   openNav 第一行就抛、被 catch 吞掉 ⇒ 悬停与点击都不会开面板。
    //   实测证据:在装着本版的真页面上用 CDP 真实点击——按钮的 click 监听器确实跑了(挂上 is-active),
    //   而页面里被包装的 read.open / read.close 一次都没被调用(trace 空)。
    //   ★ 为何四道活体门全绿:探针把源码 new Function 注入【页面上下文】跑,那边的 window.read 是真的
    //     ⇒ 「注入式验证」看不见调用边界问题(经验 1.50)。
    //   修法:加 @grant unsafeWindow + WIN 通道;拿不到就退化成派发 mouseover/mouseout 去触发站点的**内联**
    //   handler(内联 handler 本身就跑在页面上下文,不受沙箱限制),并且那时不再摘掉它。
    // ★★ 2.52.101:房子按钮第四句回访(用户:「点击按钮依旧没有弹出面板」+ 一张圈了版块页
    //    底栏那枚房子图标的截图,附来源 thread_new.php?fid-9-page-1.html)。
    //    真凶不是逻辑而是【页面范围】:这套面板从 2.52.98 起只在 WANTS.readOnly
    //    (read.php)里跑,而用户点的是【版块页】——那里 #td_cate 就是面包屑里那枚 home.gif,
    //    站点只给了内联 onmouseover(悬停能开面板),点击什么都没接。
    //    ⇒ 新增显式意图 WANTS.homeNav = PAGE.read || PAGE.board;按钮样式表从 READ_CSS
    //      整段搬出成 HOME_NAV_CSS,由 initCrumbHomeNav 自己注入 #spp-hbtn-style,
    //      作用域类 html.spp-hbtn-on(帖子页/版块页共用,不再借 html.spp-read-on)。
    //    ★ 连带修掉第二次复发的特化度坑(见 经验 1.44):版块页那条
    //      「#spp-btabs #breadcrumbs #td_cate」(3 个 id)会把热区压成 15×15 ⇒ 同样加
    //      「:not(.spp-hbtn-hit)」互斥。
    // ★★ 2.52.100:房子按钮第三句回访(用户:「这个面板怎么丢了」+ 一张「悬停弹出、点一下面板就没了」\n    //    的截图)。真凶是 2.52.99 那个**无脑 toggle**:悬停已经把面板打开,鼠标还停在按钮上时点一下,\n    //    执行的正是「关闭」分支 ⇒ 面板当场消失,看着就像「丢了」。\n    //    ⇒ 改成「点击=钉住 / 收起」:没开→打开并钉住;悬停开着→第一次点只**钉住**(不移开不收);\n    //    已钉住→再点一次才收;点按钮以外任何地方也收(document 捕获相 click)。\n    //    钉住期间 mouseleave 直接 return,不再跟着悬停收起。\n    //    ★ 实测(read-head-25298.js --live):点击后面板仍开着(display:block 613×351)、\n    //    按钮保持 .is-active;再点一次才回到 none。\n    // ★★ 2.52.99:上一版的房子按钮回访(用户一张小截图 + 两句):①「这个要适配黑色背景」
    //    ②「点击时弹出一个面板,但没有弹出来」。
    //    ① 面包屑那两行都在**深色底**上(顶栏那行是透明地贴在星空页面上、底部那行是本脚本
    //       的 #0e1116 磨砂玻璃条),上一版却给了白色主题(白底 + #e2e8f0 边),
    //       在深色上像贴了张白纸 —— 用户先选了「浅色」,看到实物后改成深色 ⇒ 本轮改用
    //       组件本来就备好的 dark 变体(半透明玻璃底 rgba(255,255,255,.06) + 边框 .18、
    //       图标 #cbd5e1,hover/常亮转 #60a5fa)。两套主题都在 CSS 里,只换按钮上的那个类。
    //    ② 上一版只接了 mouseenter(悬停开、移开收),点击**什么都没绑** —— 用户点了一下
    //       没反应。本轮补 click:开→关(toggle),并让按钮在面板开着时保持 is-active 常亮。
    //       顺带把 button 的 tabIndex:-1 撤掉 —— 点击已是真实动作,键盘 Enter/Space 触发的
    //       也是 click,此时再把它排除在 Tab 序列外就没道理了(多一个制表位是有意的)。
    //    ★ 实测(read-head-25298.js --live,注入当前源码):按钮 26×26、深色主题下
    //      border-color 实得 rgb(255,255,255)/0.18 那层的计算值、点击后面板 display 从 none
    //      变 block(贴合在按钮正下方)、再点一次回到 none。
    // ★★ 2.52.98:用户两张批注截图 = 一个新增功能 + 一批美化。
    //    ①「增加一个功能,鼠标经过房子按钮的时候有面板划出」+「美化按钮:uiverse.io/
    //      mahiatlinux/black-lion-15(适当修改,适配白色主题)」。
    //      ★ 实站量到两枚房子图标待遇不同:顶栏那枚 <img id="td_cate" src=…home.gif> 自带
    //        onmouseover="read.open('menu_cate','td_cate')"(悬停即弹站点原生「版块导航」面板);
    //        底部面包屑那枚 home-old.gif 【没有 id、没有任何事件】—— 用户圈的就是这一枚。
    //      ⇒ 两枚都包进同一个按钮组件(.spp-hbtn-wrap > button.spp-hbtn + 原图当透明热区),
    //        统一驱动站点那套 read.open(面板内容/定位/开合全用站点的,不自己造一份),
    //        并给面板补一次「划出」入场动画(.spp-navout,.24s cubic-bezier)。
    //        按钮 = 该 Uiverse 元素按比例的纯图标形态:26×26、3px 边框、8px 圆角、
    //        左上角勾选圆 hover 时 scale(1) 弹白勾、边框与图标转 #3b82f6(用户选的白色主题)。
    //      ★ 真实浏览器实测(本探针注入当前源码后量):按钮 26×26、热区 26×26 铺满、
    //        面包屑行仍是 34px;悬停后站点面板在按钮正下方展开(btn 13,311 26×26 →
    //        面板 12,336 613×351,内容 206 字含 Comic Market…)、鼠标移开 display:none。
    //      ★★ 踩过一个级联坑:本脚本旧规则 `html.spp-read-on #breadcrumbs #td_cate
    //        {width/height:15px !important; margin:0 9px 0 2px !important}` 带【两个 id】,
    //        特化度 (2,1,1) 压过本段任何 (1,n,n) —— 实测量到热区仍被钉在 15×15,
    //        于是面板贴着旧盒子下缘弹、与 26px 按钮错位 12px。修法是让两条规则【互斥】:
    //        包过的图加 .spp-hbtn-hit,旧规则加 :not(.spp-hbtn-hit)(不再比特化度)。
    //    ②「帖子签名放置区,在'时间'和'只看作者'之间居中」:把站点那枚签名节点(Discuz 惯例
    //      .sign)**搬**(不是克隆)进 .tiptop、插在 .fr 之前,再由 CSS 把这一行改成 flex
    //      三段式(时间 | 签名 flex:1 居中 | 操作按钮)。实测合成签名后中轴 605 vs 605(0px 偏差)、
    //      头部行高 28px(没被撑高)。作者没有签名时不插入占位,这一行保持原样。
    //      ★ 为什么用合成签名验证:该页作者的帖子里本就没有 .sign 节点(探针实测 0 个),
    //        载荷在注入源码前先往正文里挂一个带回读标记的 .sign,才能量到「搬的是不是同一个节点」。
    //    ③「ID放置图片框内,而不是只显示一个'冰'字」:首字母头像块(.spp-ava-initial)改成放整段
    //      用户名,按字数分五档字号(avaSizeBucket → .is-s2/s4/s6/s9/sX,2 字仍是原来的 46px)。
    //      实测「可可萝」= is-s4、块 120×120、scrollHeight 120 ≤ 121 不溢出。
    //    ④ 第二张截图另外五条批注(快速发帖框,独立审计稿 tmp/fr/fastreply.css 已并入 READ_CSS):
    //      左栏复选框+裸文本 → label.spp-fr-opt 胶囊(**必须 JS**:站点那几行标签是裸文本节点,
    //      纯 CSS 选不到);「SP币/HP」两枚只有一个 option 的下拉 → select:has(> option:only-child)
    //      退化成静态文字(**活判据**:站点哪天补上第二个选项,规则自动失效);工具栏与正文框等宽
    //      (实测原来是 735.59 vs 713.50 —— 站点给 textarea 写了行内 width:96%,差 22.09px);
    //      「提交+预览」与「字数检查…」各自成胶囊框(前者站点没给共同父节点,补 span.spp-fr-actions);
    //      整块换白底主题(--spu-* 浅色令牌,不留深色玻璃串场)。
    //      ★ 回复框底色要靠内联逃生口:星空主题那条 html.spp-hm-star #main .t5:not(.spp-t5bar)
    //        带 id,特化度高过本段 —— 实测量到盒子还是 rgba(250,252,255,.93)(星空透进来 7%),
    //        于是由 initFastReply 用 setProperty(…, 'important') 钉白(取值走 --spu-surface 令牌)。
    //      ★ 实测:工具栏/正文框 736 vs 736、左缘 258 vs 258;7/7 复选框进胶囊;2 枚单选项下拉
    //        全部退化;.spp-fr-actions 里 2/2 枚;回复框 rgb(255,255,255) + 12px 圆角。
    //    ★ 本轮的门:tools/probes/read-head-25298.js(静态 32 条契约 + --live 29 条活体)。
    //      活体那两个坑也写进 经验.md 了:① 页面里跑着旧版时**样式注入会被幂等守卫挡回去**
    //      (量到的会是「新 JS + 旧 CSS」);② 旧版的头像扫描挂在 MutationObserver 上,用删/插
    //      节点的清场方式会把它叫醒、它比我们的 500ms 定时器早,只能用属性改动静默清场。
    // ★★ 2.52.97:友情链接卡片底下那行「卡片样式:uiverse.io/... — MIT License © ...」
    //    按用户要求从**界面上**撤掉(截图里绿框圈住 + 一句「这个文本不要」)。
    //    ★ 只是不显示,**MIT 的版权声明一个字没少**:它本来就同时写在
    //      CSS 段头(.spp-fl-* 那一段的注释)与 buildFriendCard 的函数头注释里,
    //      MIT 要求的是"保留声明",不是"印在页面上"。所以:删 DOM、删那条没人再用的
    //      `.spp-fl-foot` 规则,注释里的署名与许可原文照旧。
    // ★★ 2.52.96:首页两行卡片的排版修正(用户截图三条标注,全部命中真实几何)。
    //    1) 侧卡比中间卡高一截 ⇒ 中心没对齐、且侧卡底部圆角被下一组标题条盖住
    //       (标注②③)。真凶是站点全局 box-sizing:content-box:.spp-lt 的 height:100%
    //       只算内容盒,实高 220+28+4 = 252,而槽位 .spp-sc 只有 220。
    //       真实浏览器(OpenCLI,视口 1266)实测:顶行 .spp-lt 实高 256 vs 槽位 220
    //       (溢出 36px)、茶馆行 252(溢出 32px);顶行卡底 565 已越过下一组标题条顶 553。
    //       ⇒ .spp-lt 统一 box-sizing:border-box,槽位与中间卡一起抬到 --sz-card-h:256px
    //         (= 内容盒 224 + 上下内边距 28 + 2px 描边 ×2)。三张卡从此上下齐平。
    //    2) 茶馆卡悬停放大没预留空位(标注①):.spp-tc:hover 是 transform:scale(1.1),
    //       transform 不参与布局 —— 放大量 460×0.1 = 46px、左右各 23px,而这一行 gap 只有
    //       16px ⇒ 每侧压进邻卡 7px。⇒ 本行 gap 改用 --tea-bleed
    //       (= 卡宽 × (放大倍数-1) / 2 + 3px 细缝 = 26px):放大后仍留 3px 细缝,
    //       两侧卡片各窄 10px(正是用户要的「缩短左右两侧卡片的宽度」)。
    //       ★ 第一版把让位写成了容器左右内边距,探针当场否掉:内边距只压窄两侧卡片,
    //         茶馆卡与邻卡之间还是那条 16px gap,放大照样压进去 7px。
    //         让位必须做在【兄弟间距】上 —— 教训见 经验.md 1.41。
    //       卡宽/放大倍数都写进变量,改一处不会漂。
    //    3) 顺带修掉两处陈旧注释与一处重复覆盖:.spp-top4 写着「4 列 / 每张 200px 高」
    //       (实际 3 列),茶馆行那条把 .spp-lt 内边距又覆盖一遍(基类已统一 16/12)。
    // ★★ 2.52.95 两件事:面板里撤掉两组;面板外壳从「多栏 + 跨栏」改成三段式。
    //    1) 撤掉显示的两组(用户 2.52.95 定案:「这两块不该出现在面板里」):
    //       · 「首页 Comic Market 卡片范围 (仅首页)」里的「卡片范围」下拉框
    //       · 2.52.94 才加的「开关状态(只读 · 想改请手改 GM 存储)」整组
    //       ⇒ 面板现在只剩「图片设置 (仅帖子页)」+「数值设置」两组 / 7 个可写参数。
    //       ★ 功能没少:CM 卡片范围仍可从 Userscript 菜单的
    //         「🃏 首页 Comic Market 卡片范围:…」改;那 8 个开关照旧全部生效
    //         (读取逻辑一字未改),只是继续只能手改 GM 存储 —— 与 2.52.63/2.52.64 的定案一致。
    //         ☆ 2.53.62 补记:那条菜单路后来也按用户要求撤掉了(范围写死 'cm')——
    //           本条其余内容仍是 2.52.95 当时的实况,未改。
    //       ★ SPP_HIDDEN_SWITCHES 常量【保留】:它是那 8 个键名唯一的登记处,
    //         文档生成器与 tools/verify/settings-panel.js 都从它抽键名(同 SETTINGS_DEFS 的理由)。
    //    4) 顺手清掉一个「登记表里的假话」:spp_board_polish(版块页卡片 / 列表美化)。
    //       本轮给 settings-panel 守卫加了「每个登记键都必须还有一处 SPP_CONFIG.bool(key, true)
    //       读取点」的断言,当场抓到它:全文只剩两张登记表,没有任何读取点
    //       —— 版块页美化后来拆成了各自独立门控的几块,这个笼统的总开关早就不干活了。
    //       于是从 SETTINGS_DEFS 与 SPP_HIDDEN_SWITCHES 两处删除(存储里的旧值没人读,行为不变)。
    //       两个登记表因此各少一项:SETTINGS_DEFS 7 → 6、SPP_HIDDEN_SWITCHES 9 → 8。
    //    2) 面板外壳重排:原来 box 自己就是 columns:2,按钮组靠 column-span:all 撑整幅。
    //       实测(tools/probes/pw-panel-25295.js):面板高 786px,而它自己声明的 max-height 是
    //       740px(多出 46px;视口 640 / 480 时同样多 46px:638 vs 592、478 vs 432);
    //       同时右栏首组「数值设置」的 top(51) 比标题 top(55) 还高 —— 栏首跑到标题旁边上去
    //       (这一条的几何来自用户真实浏览器,OpenCLI 实测)。
    //       ⇒ 改成「h3 / .spp-gs-cols(唯一的 columns 容器)/ 按钮组」三段、column-span 一处不用,
    //         同一支探针复测:面板高 282px ≤ 上限 740px、各组都在标题之下。
    //         ★ 那 46px 只记「多栏 + 跨栏时实高超上限」这个事实与修后复测,没去证死机制。
    //    3) 标题「脚本设置」的盒子宽只有 68px(本该 902px),分隔线跟着只剩 68px。
    //       ★ 这一条**不是**多栏引起的 —— 去掉 column-span 后症状照旧,于是写了
    //         tools/probes/diag-panel-css.js 去查「到底哪条规则作用在它身上」,真凶是站点自己的
    //         `h3, h2 { display: inline; font-size: 12px }`(页面内联样式表)。
    //         面板那条 `#spp-global-settings-box h3` 赢了字号(17px)、却从没声明过 display,
    //         于是 display 被站点的 inline 拿走 ⇒ h3 变成行内盒:宽按内容收缩,后面的两栏容器
    //         从它的行盒下面开始(380 < 390),看起来就像"组跑到标题上面去了"。
    //         修法:在面板规则里显式写 display:block。
    //       ★ 教训:给浮层元素写样式,不能只写「我关心的那几个属性」—— 页面级规则会设你没设的属性。
    //         见 经验.md 1.40。
    // ★★ 2.52.94:删掉头部 ==UserConfig== 块 —— 篡改猴**不解析**它(实测:本机两个篡改猴
    //    扩展包 12 js + 35 json + 6 html 全文搜 UserConfig 命中 0;那是脚本猫的扩展,见 经验.md 1.38),
    //    同时删掉读数层的「面板值」那一层(没有第二来源就是死代码),并加了只读「开关状态」组
    //    —— 只读组在 2.52.95 按用户要求撤掉。
    // ★★ 2.52.93:头部加了 ==UserConfig== 块 + 读数层 + 删空 @grant GM_setClipboard。
    //    前两项在 2.52.94 被撤回,只有「删空 @grant」保留。
    // ★★ 2.52.92:修掉 POST_CSS 里那句「带反引号」的 CSS 注释。
    //    一个反引号就把模板字面量提前闭合 ⇒ 后面的 CSS 文本被当 JS 解析 ⇒
    //    initBoardTop 抛 TypeError 被 safeInit 吞掉 ⇒ 版块页/帖子页/搜索页/
    //    发帖页/任务页的一整套顶部卡片(用户卡 + 热门版块 + 面包屑板块 +
    //    发帖工作台)在 2.52.91 全部消失,而 node --check 与 boardtest 都是绿的。
    //    详见 POST_CSS 里那段注释、tools/verify/check-backticks.js 与 经验.md 1.34。
    // 「装没装上 / 页面上跑的是哪一版」不该靠肉眼猜 —— 排查 2.52.76 那次
    // 「背景板没加上去」,就是因为我读到的计算样式是上一版的:
    // box-shadow / 圆角是新的规则,background 却还是旧的,说明页面里跑的是 2.52.75。
    // 挂到 window 上之后,控制台敲 __SPP_VERSION 一眼就知道。
    //   ★ 取值优先用 GM_info.script.version(Tampermonkey 报的**真实安装版本**),
    //     这样标记永远不会与 @version 漂移;取不到才退回硬编码兜底。
    //   ★ 整块包 try:一行标记绝不能把整个脚本炸掉(@run-at document-start 时
    //     GM_info 在某些管理器里可能还没就绪)。
    //   ★★ 2.52.79:先把字面量钉死,再尝试用 GM_info 覆盖。原来的写法把「读 GM_info」
    //      和「赋值」绑在同一个三元里 —— 页面实测 __SPP_VERSION 读到 null(脚本其实在跑),
    //      当时判成「那次访问抛了、被 catch 吞掉」。2.52.80 查清:更可能是 sandbox 的
    //      window 与页面 window 不是同一个(见下),两种成因都由 DOM 那份标记一并兜住。
    //   ★★ 2.52.80:每版都改这里。但下面那句「让用户在控制台敲 __SPP_VERSION」其实
    //      常常敲不出来 —— 任务页实测 window.__SPP_VERSION 与 window.__SPP_TASKS 全是
    //      undefined,而脚本注入的 <style> 好端端躺在 DOM 里(脚本确实在跑)。
    //      差别在于 Tampermonkey 的 sandbox 里 window 不是页面那个 window,脚本写的
    //      全局属性落不进页面世界。所以版本标记必须同时钉在 DOM 上(data-spp-version)。
    // ***** SPP-VERSION-MARK-BEGIN *****
    window.__SPP_VERSION = '2.55.35';
    try {
        if (typeof GM_info !== 'undefined' && GM_info && GM_info.script && GM_info.script.version) {
            window.__SPP_VERSION = String(GM_info.script.version);
        }
    } catch (e) { /* 取不到就保持上面那个字面量 */ }
    // DOM 侧那一份(两个世界共享):F12 ▸ Elements 看 <html data-spp-version="…">,
    // 或控制台敲 document.documentElement.dataset.sppVersion —— 用 dataset 访问是
    // camelCase(sppVersion),别写成 spp-version。
    try {
        const pinVersion = function () {
            try { document.documentElement.setAttribute('data-spp-version', String(window.__SPP_VERSION)); } catch (e2) {}
        };
        if (document.documentElement) pinVersion();
        else document.addEventListener('DOMContentLoaded', pinVersion, { once: true });
    } catch (e) { /* 标记绝不能把脚本炸掉 */ }
    // ***** SPP-VERSION-MARK-END *****

    // ==================== 页面类型检测 ====================
    const PATH = location.pathname;
    const SEARCH = location.search;
    const FILE = (PATH.split('/').pop() || '').toLowerCase();

    // ★★ 2.52.92:这里原先是一串 PATH.includes(...) 子串匹配,其中
    //    PATH.includes('read.php') 在 /thread.php 上是【真】—— "t-h-read.php" 里
    //    就含着 "read.php" 这个子串。后果是版块页长期「顺带」跑着帖子页那一串模块:
    //    图片阅读优化 / 一体流 / 操作栏合并 / 默认头像兜底 / READ_CSS 注入 /
    //    隐藏「浏览器收藏|打印」行 / moveCrumb。逐条实站核过(2.52.92):
    //      · 前四个模块在版块页找不到目标节点,等于空跑;
    //      · READ_CSS 的规则全挂在 html.spp-read-on 上,而版块页不加这个类 ⇒ 注入也是死的;
    //      · #main > div.h2 在版块页有 0 个(hideToolsRow 本来就无事可做);
    //      · moveCrumb 在版块页抓到的是【被屏蔽的公告条】那条 .bdbA(站点首个 .bdbA 是
    //        公告栏,2.52.x 已 display:none),打标也是空转;版块页的面包屑另有归属:
    //        initBoardToolbar 把它搬进 #spp-btabs(见那段注释)。
    //    ⇒ 这些「顺带」没有一个是有意为之,也没有一个产生过可见效果;
    //      但它们是「靠巧合工作」的耦合:一旦有人把某条判定改精确,效果会静悄悄地变。
    //      现在判定一律用 FILE 做精确比对,需要跨页的效果写成 WANTS 里的显式意图。
    const PAGE = {
        file:    FILE,
        home:    FILE === '' || FILE === 'index.php',
        board:   FILE === 'thread.php' || FILE === 'thread_new.php',
        read:    FILE === 'read.php',
        post:    FILE === 'post.php' || SEARCH.includes('action-quote') || SEARCH.includes('action-modify'),
        search:  FILE === 'search.php',
        trade:   FILE === 'job.php' && SEARCH.includes('action-trade'),
        profile: FILE === 'profile.php',
        task:    /^\/plugin\.php$/i.test(PATH) && /[?&]H_name[=-]tasks/i.test(SEARCH)
    };

    // 旧名字全部保留(文件里 200+ 处在用),语义统一收紧为「精确判定」
    // 2.52.52:搜索页 —— 用户框选的那一页;顶部结构与版块页同形(见 initBoardTop 里的说明)
    const isReadPage = PAGE.read;
    const isSearchPage = PAGE.search;
    const isThreadListPage = PAGE.board;
    const isTradePage = PAGE.trade;
    const isPostPage = PAGE.post;

    // 显式意图表:一个效果要哪几页,写成名字。读的人不必再去推子串会不会误命中。
    //   ★ 版块页的面包屑【不在】crumbPanel 里:它由 initBoardToolbar 搬进 #spp-btabs,
    //     老代码里那记 .spp-crumb-panel 打在隐藏的公告条上(上述注释第 4 条)。
    const WANTS = {
        crumbPanel: PAGE.read || PAGE.post,   // #main > .bdbA 原地改扮磨砂玻璃板块
        editorUi:   PAGE.read || PAGE.post,   // 预览按钮 / 快捷工具栏 / 表情面板
        readOnly:   PAGE.read,                // 帖子页专属:图片阅读 / 一体流 / 操作栏 / 头像兜底
        // ★ 2.52.101:房子按钮导航面板要跨页 —— 帖子页(read.php 顶部面包屑)与版块页
        //   (thread.php / thread_new.php;站点把面包屑搬进 #spp-btabs 工具条)。
        //   实测版块页:#td_cate 就是面包屑那枚 home.gif,站点只给内联 onmouseover(悬停可开、点击为空)。
        homeNav:    PAGE.read || PAGE.board
    };

    // ==================== 茶馆版块判定(必须同时认两种 URL 形态)====================
    // ★★ 站点对同一版面有两套 URL 写法,实测都存在:
    //      thread.php?fid-9.html       (列表模式)→ 形如 fid-9
    //      thread_new.php?fid=9.html   (卡片模式)→ 形如 fid=9
    //    而版块页卡片美化【只在卡片模式生效】(列表模式没有 #wall ul.stream)——
    //    也就是说:需要这个判定的页面,恰好是旧写法恒为 false 的那个页面。
    //    旧写法 location.href.includes('fid-9') 在卡片模式上是 false,
    //    于是三处「茶馆专属」逻辑全部静默失效:
    //      · groupCards 直接 return 0 → 横/竖/无图 不分、不排、不另起一行
    //      · getReplyColor 落到「其他版块」阈值 → 热度配色不对
    //      · initPageWiden 当成非茶馆 → 页面宽度取错
    //    实测 thread_new.php?fid=9.html:includes('fid-9') === false、
    //    .spp-row-start 数量 0、rank 序列 2012212212222221122220121210222…(无序)。
    //    ★ 教训:「同一个判定」散落在多处子串匹配里,改一处不够 ——
    //      这次统一成一个函数,口径只有一套。
    // ★ 边界必须收在「非数字且非字母」上:
    //   只写 (?!\d) 时 fid=9x 仍会命中(x 不是数字),而 fid=9x 是另一个版面。
    //   实测:用 (?!\d) 时 https://…/thread_new.php?fid=9x.html 被误判为茶馆。
    //   这里用 (?!\w),把字母与下划线一并排除。
    function isTeaFid() {
        return /fid[-=]9(?!\w)/.test(location.href);
    }


    // ==================== 版块页皮肤标记(放大头图用) ====================
    // ★ 为什么用 html 上的一个类,而不是在 CSS 选择器里写 :not(...):
    //   本项目需要按「有版块页卡片网格的页面」来上样式。用
    //   `a[href*="fid-"]:not([href*="fid-9"])` 这类写法时,[href*="fid-9"]
    //   会顺带命中 fid-90、fid-9x 这类版面;而 URL 里同时出现两个 fid 参数时
    //   又会被误判。标记只打一次、判定口径与 isThreadListPage 完全一致
    //   (PATH 上取 thread_new.php / thread.php),不引入第二套逻辑。
    //
    // ★ 为什么能这么早打标:
    //   本脚本是 @run-at document-start,此时 document.documentElement 已存在
    //   (head 都可能还没有),但 html 元素一定在 —— 加类是安全的。
    //   放大头图的 CSS 全部以 html.spp-board-skin 开头,必须在样式生效前打上,
    //   否则首帧会先按 150px 渲染再跳到 16:9,出现一次可见的闪动。
    // ★ 反例(别学这个写法):document.body.classList.add(...) 在 document-start
    //   会抛 TypeError(body 还是 null),整个 IIFE 从第一行就挂掉,后续模块全不执行。
    //
    // ★ 2.47.0 关键变更:门控从「非茶馆」改为「版块页」。
    //   2.46.0 的类名是 spp-not-tea、用 `!location.href.includes('fid-9')` 判定,
    //   把茶馆整版排除在放大之外。用户随后要求茶馆的头图也放大、无图卡一并变大,
    //   所以排除条件已经不存在 —— 继续沿用旧名会让"名字在撒谎",
    //   更糟的是「茶馆不受影响」这条旧守卫会一直绿:它守的是 spp-not-tea 的存在性,
    //   而茶馆现在根本不该有那个类。改名为中性且正确的 spp-board-skin。
    //   注意 PATH 在 document-start 时已可读(location.pathname 同步可得),
    //   不像 document.body 那样是 null,所以这里安全。
    if (PATH.includes('thread_new.php') || PATH.includes('thread.php')) {
        document.documentElement.classList.add('spp-board-skin');
    }

    // ==================== 共享工具函数 ====================
    function getImgSrc(img) {
        return img.getAttribute('data-src')
            || img.getAttribute('data-original')
            || img.getAttribute('data-lazy-src')
            || img.getAttribute('file')
            || img.src || '';
    }

    function isContentImage(img) {
        const src = getImgSrc(img);
        if (!src || src === 'about:blank' || src.startsWith('data:')) return false;
        if (src.includes('/images/post/smile/')) return false;
        if (src.includes('/images/colorImagination/')) return false;
        if (src.includes('noimageavailble')) return false;
        if (img.naturalWidth > 0 && img.naturalWidth < 80 && img.naturalHeight > 0 && img.naturalHeight < 80) return false;
        return true;
    }

    // ★ 2.52.92:DOMReady 回调也包一层。原先只有 safeInit 那层(模块同步初始化),
    //   而绝大多数模块真正的活儿都在回调里 —— 那里抛异常谁都看不见,页面只是「少一块」。
    //   现在:异常记进 SPP_INIT.fail,并照旧写 console.error(真实路径门盯的就是这句)。
    let domReadySeq = 0;
    function onDomReady(fn) {
        const seq = ++domReadySeq;
        const run = function () {
            try {
                fn();
            } catch (e) {
                SPP_INIT.fail.push('DOMReady#' + seq + ' ⇒ ' + errBrief(e));
                console.error('[SPP] DOMReady 回调#' + seq + ' 初始化失败', e);
            }
        };
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', run);
        } else {
            run();
        }
    }

    // 安全插入 head:document-start 时 head 可能还不存在,延迟到 DOMContentLoaded
    function appendToHeadSafe(el) {
        const parent = document.head || document.documentElement || document.body;
        if (parent) {
            parent.appendChild(el);
        } else {
            document.addEventListener('DOMContentLoaded', () => {
                (document.head || document.documentElement || document.body).appendChild(el);
            }, { once: true });
        }
    }

    // 异步等待
    function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }

    // 等待 DOM 元素出现(Promise 版),超时返回 null
    function waitForElement(selector, maxRetries, interval) {
        maxRetries = maxRetries || 30;
        interval = interval || 500;
        return new Promise(function (resolve) {
            var tries = 0;
            (function check() {
                var el = document.querySelector(selector);
                if (el) { resolve(el); return; }
                if (++tries >= maxRetries) { resolve(null); return; }
                setTimeout(check, interval);
            })();
        });
    }

    // ==================== 回复/热度配色(2.50.7 提到顶层) ====================
    // ★ 为什么必须放顶层,放在这里:
    //   本函数原先定义在 initThreadListConvert() 内部。2.50.6 新增的
    //   initAnnounceBoard() 是顶层函数,调用它时拿到 ReferenceError ——
    //   而 safeInit 的 try/catch 会把异常吞成一行 console.error,
    //   页面不报错、只是「公告板块根本没出现」,极难定位。
    //   两个模块都要用同一个配色规则,所以提为共享函数,只此一份。
    //   (放在这里而不是 initThreadListConvert 里,是因为后者在 1200 行之后才开始,
    //     而顶层函数在文件末尾调用,放在共享工具区语义上也更对。)
    function getReplyColor(count) {
        // ★ 2.45.0:最低档从 #1A1A1A 改为 #9AA5BB。
        //   原值是为 2.43.0 的「浅底通栏卡」选的,黑字在浅底上没问题;
        //   2.45.0 把卡片换成深色 #131A2A 后,最低档的标题与回复/热度数字
        //   全部变成近黑字压深底 —— 实测对比度不足 1.2:1,肉眼即「看不见」
        //   (截图里「回复: 热度:」后面是空白)。
        //   本函数同时给标题上色,所以受影响的不只是数据行。
        // 茶馆 (fid=9) 独立阈值
        // ★ 2.49.3:改用 isTeaFid(),旧写法在卡片模式(fid=9)上恒为 false
        // ★ 2.50.1:第二档由 #409EFF(蓝)改为 #67C23A(绿)。
        //   用户要求「禁止蓝色字体」,而原值是本函数里唯一的蓝。
        //   #67C23A 与 #E6A23C / #F56C6C 同属一套语义色(绿/橙/红),档位梯度不变。
        if (isTeaFid()) {
            if (count <= 20) return '#9AA5BB';
            if (count <= 50) return '#67C23A';
            if (count <= 80) return '#E6A23C';
            return '#F56C6C';
        }
        // 其他版块
        if (count <= 50) return '#9AA5BB';
        if (count <= 100) return '#E6A23C';
        return '#F56C6C';
    }

    // 等待 DOM 就绪后往 body 追加元素(带重试)
    function appendToBodySafe(el, maxRetries) {
        maxRetries = maxRetries || 100;
        (function tryAppend(tries) {
            if (document.body) { document.body.appendChild(el); return; }
            if (tries < maxRetries) setTimeout(function () { tryAppend(tries + 1); }, 50);
        })(0);
    }

    // ==================== 模块体检(2.52.92) ====================
    // ★ 起因就是 2.52.91 那次事故(见 经验.md 1.34):一个模块被异常吞掉,页面只是
    //   「顶部那一整套卡片不见了」,只能靠读源码反推是哪个模块、为什么。
    //   现在 safeInit / onDomReady 都把结果记进 SPP_INIT,启动跑完统一落到
    //   DOM 属性 data-spp-init 上 —— 和 data-spp-version 同一套路:sandbox 里
    //   window 不是页面那个 window,DOM 才是两个世界共用的接口。
    //     用法:F12 ▸ Elements 看 <html data-spp-init="ok28/fail0">;
    //           控制台敲 __SPP_INIT 看全量(同为 sandbox 限制,拿不到时才用属性)。
    //   ★ 边界(别把它当万能):只覆盖【模块同步初始化】+【DOMReady 回调】两段。
    //     定时器/事件/异步链路里抛的异常不在此列 —— 那类由真实路径门盯 console 兜住。
    const SPP_INIT = { ok: [], fail: [] };

    function errBrief(e) {
        return (e && e.name ? e.name : 'Error') + ': ' + (e && e.message ? e.message : String(e));
    }

    function publishInitReport() {
        try {
            document.documentElement.setAttribute('data-spp-init',
                'ok' + SPP_INIT.ok.length + '/fail' + SPP_INIT.fail.length +
                (SPP_INIT.fail.length ? ' ⇒ ' + SPP_INIT.fail.join(' ; ') : ''));
        } catch (e) { /* 属性写不进去不该影响任何功能 */ }
        window.__SPP_INIT = SPP_INIT;
    }

    // 模块初始化安全包装:防止一个模块崩溃拖垮全部功能
    function safeInit(name, fn) {
        try {
            fn();
            SPP_INIT.ok.push(name);
        } catch (e) {
            SPP_INIT.fail.push(name + ' ⇒ ' + errBrief(e));
            console.error('[SPP] ' + name + ' 初始化失败', e);
        }
    }

    // ==================== 共享工具函数 END ====================

    // ==================== 设计变量(恒定注入 · 不依赖任何主题层) ====================
    // ★★ 2.52.62 删掉「界面美化」主题层时,这一段必须留下来 —— 它本来只长在 TEA_SKIN 里。
    //   理由:全脚本仍有 84 处 var(--spu-*)【不带兜底值】(2.52.61 实测),
    //   其中 15 处落在模块 4 的「列表模式」卡片样式里,是当前版仍在跑的规则。
    //   var() 无兜底且变量未定义 ⇒ 该条声明在计算值阶段直接作废,属性回落到继承/初始值。
    //   实测(主题层关闭时):getComputedStyle(documentElement).getPropertyValue("--spu-accent") === ""。
    //   所以变量块不能跟着主题层一起删,只能提出来常驻。
    //   ★ 变量清单与 TEA_SKIN 原样一致,一个不多一个不少(12 个)。
    const SPP_DESIGN_VARS = ':root{--spu-bg:#f4f6f8;--spu-surface:#ffffff;--spu-overlay:#eef2f5;--spu-text:#1f2937;--spu-secondary:#475569;--spu-muted:#6b7280;--spu-border:#d8dee7;--spu-accent:#2f6f68;--spu-accent-dim:#255a55;--spu-radius:7px;--spu-shadow-sm:0 1px 2px rgba(15,23,42,0.04);--spu-shadow-md:0 1px 2px rgba(15,23,42,0.04),0 10px 28px rgba(15,23,42,0.06);}';
    (function injectDesignVars() {
        const el = document.createElement('style');
        el.id = 'spp-design-vars';
        el.textContent = SPP_DESIGN_VARS;
        appendToHeadSafe(el);
    })();

    // ==================== 脚本设置读数层(SPP_CONFIG · 2.52.94 定型) ====================
    // ★★ 为什么要有这一层:设置值原先散在十几处各自读 —— 页内面板一套、菜单命令一套、
    //   migrateSettings 一套、各模块各自 GM_getValue(...) 一套。同一条规则(默认值 / 合法区间 /
    //   旧值归一)就抄了三遍:比如「页面宽度默认 1400」这半句同时写在面板、菜单、initPageWiden 里;
    //   「翻页步长默认 1」写在面板与 initNavButtons 两处。改一处忘一处就是静默不一致
    //   (2.52.92 的 PAGE/WANTS 收拾的是同一类毛病:一个判定散在多处子串匹配里)。
    //
    // ★★ 读数口径(2.52.94 定型):**GM 存储 → 内置默认**,外加区间收敛。
    //   2.52.93 曾经在它前面插过一层「==UserConfig== 面板值」,2.52.94 删掉了 ——
    //   那层是死代码:篡改猴根本不解析 ==UserConfig==(实测扩展包全文 0 命中,
    //   见 经验.md 1.38),永远读不到值。留着一个永远为空的来源,比没有更糟:
    //   它让人以为存在一个设置面板,而这个脚本唯一的设置入口是页内那个 ⚙ 面板。
    //   区间收敛的语义仍是「用户填错就拉到最近的合法值」,不是「悄悄改回默认」。
    //
    // ★ 图片参数的键名(maxImgHeight 等)是从 GM 存储里那份 JSON 里取的字段,
    //   不再是任何面板的前缀拼出来的键 —— 所以本层只拼 GM 键,不拼面板键。
    //
    // 图片阅读默认值:原先在页内面板、面板刷新、initImageViewer 里各写了一份(共 3 份),
    // 现在只留这一份。改名 DEFAULTS 会与各处局部变量重名,故加 IMG_ 前缀。
    const IMG_DEFAULTS = { maxImgHeight: 80, maxImgWidth: 100, thumbSize: 64, centerImg: true, wheelAction: 'switch' };
    const IMG_SETTINGS_KEY = 'spp_img_settings';

    // ★★ 2.55.20(用户:「对于茶馆,不同的热度对应不同的颜色,能否在设置面板中由用户
    //   自定义颜色、热度?」→「OK,动手」):茶馆热度配色的**单一登记处**。
    //   为什么是一整份 JSON 而不是 7 个平键:面板的保存循环是**逐行**写的(一行一个
    //   data-key),把 7 个控件装进同一行 ⇒ 一次点击原子落盘,不会出现「改了色没改边界」
    //   这种半新半旧的配置。形状与 IMG_SETTINGS_KEY 同源(一个键 + 里面若干字段)。
    //   字段名:t* 是三档边界(读取时升序排序后使用);c* 是该档的颜色 ——
    //   cCold 管「≤ tCold」、cWarm 管「≤ tWarm」、cHot 管「≤ tHot」,cPeak 管「比 tHot
    //   还高」那一档(它没有上界,所以没有对应的 t)。
    //   ★ HEAT_STYLE_DEFAULTS 就是 2.55.7 / 2.55.8 定下的那套硬编码值,一个数都没改:
    //     1000 / 5000 / 20000 与 白 / 绿 / 橙 / 红。老用户升上来看到的是同一张页面。
    const HEAT_STYLE_KEY = 'spp_heat_style';
    const HEAT_STYLE_MAX = 9999999;
    const HEAT_COLOR_RE = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
    const HEAT_STYLE_DEFAULTS = {
        tCold: 1000, tWarm: 5000, tHot: 20000,
        cCold: '#FFFFFF', cWarm: '#67C23A', cHot: '#E6A23C', cPeak: '#F56C6C'
    };
    /* ★ 面板那一行的说明小字。**刻意只写一份**,两处共用:
       ① 登记表 SPP_PANEL_DEFS 里的 hint(文档生成器与守卫读它);
       ② buildOverlay 里那行的 .spp-gs-hint(用户真正看到的)。
       抄成两份就是下一次漂移的种子 —— 本项目已经吃过「同一句话写两处」的亏。 */
    const HEAT_HINT = '茶馆版块(fid 9)的卡片标题与「回复 / 热度」数字按热度分档上色。三档边界填 0-9999999,读取时自动从小到大排序;四枚取色器依次是「≤ 第一档 / ≤ 第二档 / ≤ 第三档 / 更高」的颜色。色值只认 #rgb / #rrggbb,不合规回落默认色。默认 1000 / 5000 / 20000,白 / 绿 / 橙 / 红';

    // 从一份 JSON 字符串里安全取一个数,取不到返回 null(存储坏了 / 字段缺失 / 不是数)。
    function cfgJsonNum(raw, field) {
        if (raw === null || raw === undefined || raw === '') return null;
        try {
            const o = JSON.parse(String(raw));
            if (!o || typeof o !== 'object') return null;
            const v = Number(o[field]);
            return isFinite(v) ? v : null;
        } catch (_) { return null; }
    }

    // 数值收敛:非法(空 / 非数)返回 null 让调用方走下一层;越界则拉到最近的合法值。
    //   ★ zeroAsMissing:图片参数里 0 是「没设过」的哨兵(尺寸不可能是 0),
    //     而页面宽度 / 翻页步长的 0 同样没有意义,一并按「没设过」处理。
    function cfgFit(v, min, max, zeroAsMissing) {
        if (v === null || v === undefined || v === '') return null;
        const n = Number(v);
        if (!isFinite(n)) return null;
        if (zeroAsMissing && n === 0) return null;
        if (n < min) return min;
        if (n > max) return max;
        return n;
    }

    // ★ 页内设置面板的**分组表**:单一登记处,三个消费点共用它 ——
    //   ① 面板渲染(buildOverlay 按这张表决定分组顺序与标签);
    //   ② 面板打开时的回填(按 type 决定读哪个控件);
    //   ③ tools/build/build-structure-doc.js「九、设置面板」与
    //      tools/verify/settings-panel.js 的「面板行 ↔ 本表」一致性核对。
    //   ★ 改这张表就要同步改 buildOverlay 的 HTML:守卫会比对面板里实际渲染出的
    //     data-key 集合与本表的字段集合,缺一个就报(这正是 2.52.93 那次
    //     「表写了但界面没有」的教训)。
    //   ★★ 2.52.95:「卡片范围」那一组(组内只有一个字段)按用户要求从面板撤掉,本表随之少一组。
    //   ★★ 2.53.62(用户附篡改猴脚本列表截图,红圈圈着「首页 Comic Market 卡片范围:C104 起」那行,
    //      「框选的这个不要显示」;追问后定案「彻底删掉,不要这个开关」):
    //      那枚 Userscript 菜单命令连同模块 16 里那条读数一起删除,范围**写死 'cm'** ——
    //      于是这个设置连同它的存储键在脚本里已经彻底不存在(面板里本来就没有它,这一条不变)。
    const SPP_PANEL_DEFS = [
        {
            group: '图片设置 (仅帖子页)', kind: 'img_json',
            fields: [
                { key: 'maxImgHeight', label: '图片最大高度', type: 'img_json', def: 80, min: 30, max: 100, unit: 'vh' },
                { key: 'maxImgWidth',  label: '图片最大宽度', type: 'img_json', def: 100, min: 20, max: 100, unit: '%' },
                { key: 'thumbSize',    label: '缩略图尺寸',   type: 'img_json', def: 64, min: 32, max: 128, unit: 'px' },
                { key: 'centerImg',    label: '图片居中显示', type: 'img_json', def: true },
                { key: 'wheelAction',  label: '灯箱滚轮行为', type: 'img_json', def: 'switch', values: ['switch', 'zoom'] }
            ]
        },
        {
            /* ★★ 2.55.20:新增「茶馆热度配色」组(面板第六类行 data-type="heat_json")。
               ★ 本组只有**一行**(data-key = spp_heat_style),行里装 7 个控件 ——
                 3 个边界数字框 + 4 个取色器。它把 img_json 走过的「一个 JSON 键 + 行内多个
                 控件」照抄了一遍,区别只在控件种类(多了取色器)与字段名。
               ★ 新增行类型必须同步四处(少一处就是「能看不能点 / 点了不存」):
                 ① 渲染行(buildOverlay 里那一组)② 自检选择器(比 data-key 的那句)
                 ③ 保存分支(.spp-gs-save 的 click 里)④ 打开面板时的回填块;
                 外加 CSS —— 取色器要跟面板其他控件对齐(.spp-heat-* 那几条)。
               ★ def 这里**刻意不填**:本行的默认值不是一个标量,而是一整份
                 HEAT_STYLE_DEFAULTS(7 个字段),由读数层逐字段回落。渲染与回填都问
                 SPP_CONFIG.heatStyle(),不在登记表里抄第二份默认值表(2.53.47 的教训:
                 默认值散成两处,打开面板就会按第二处回填,一保存把配置压平)。 */
            group: '茶馆热度配色 (仅茶馆版块)', kind: 'heat_json',
            fields: [
                { key: 'spp_heat_style', label: '热度分档与配色', type: 'heat_json', hint: HEAT_HINT }
            ]
        },
        {
            // ★★ 2.53.0:新增「功能开关」组。本表自 2.52.64 起只剩 img_json / number 两类行
            //   (布尔开关整批不渲染),本轮为「自动展开隐藏楼层」加回一个**可勾选**的布尔行,
            //   于是引入第三类 data-type="bool"。
            //   ★ 口径是「渲染什么就处理什么」:新类型必须在 buildOverlay 里同步四处 ——
            //     ① 渲染行 ② 面板自检的选择器 ③ 行点击切换 checkbox ④ 保存分支,
            //     外加 openSettingsPanel() 的每次打开回填。少一处就会出现「能看不能点/点了不存」。
            group: '功能开关', kind: 'bool',
            fields: [
                { key: 'spp_auto_expand_hidden', label: '自动展开隐藏楼层', type: 'bool', def: true,
                  hint: '纯 mark 水帖这类被站点折叠的楼层内容直接展开,不再留「▼展开」按钮' },
                // ★★ 2.53.9(用户:「…用于选择是否使用『用户名 + 彩色背景头像』替换默认头像,默认关闭」):
                //   本行是本表第二枚 bool 行。**def: false 是本轮的需求本体** —— 它必须与
                //   15c 里那句 SPP_CONFIG.bool('spp_hide_default_avatar', false) 逐字同值,
                //   tools/verify/settings-panel.js 的 C/D 段就是拿这两处对着核的。
                { key: 'spp_hide_default_avatar', label: '用户名 + 彩色背景头像', type: 'bool', def: false,
                  hint: '没有图床(或头像链接已失效)的用户,用「用户名 + 彩色渐变块」替换站点默认头像。默认关闭' },
                // ★★ 2.53.32 / 2.53.36 / 2.53.68(三代账):2.53.32 加了「靠边自动隐藏」、
                //   2.53.36 又补了一枚「总开关」,同一个面板于是挂了两枚互相打架的勾选项。
                //   2.53.68 用户拍板本意:「勾选它,它就会自动显示;不勾选,它就会自动隐藏到侧边栏」
                //   ⇒ 两枚并成一枚(本表第三枚 bool 行):勾选 = 面板常显;不勾选 = 贴边收起成一条、
                //     鼠标移上去滑回来(**不再是整块不建**,那个门口已撤)。
                //   ★ def: **false**(2.53.70 用户:「含图楼层悬浮窗,这个功能默认为隐藏,即关闭状态」)。
                //     必须与 15a-2 里那句 SPP_CONFIG.bool('spp_imgidx_show', false) 逐字同值
                //     (tools/verify/settings-panel.js 的 EXPECT 拿这两处对着核)。
                //   ★ 键名特意不叫 spp_imgidx:它是另一个键的**前缀**,守卫与文档生成器都在做键名匹配。
                //   ★ 被并掉的那枚键见文件上方那条 2.53.32 的账 —— 老配置里残留的值没人再读,无害。
                { key: 'spp_imgidx_show', label: '含图楼层悬浮窗', type: 'bool', def: false,
                  hint: '勾选:面板常显。不勾选:收起成侧边一条,只留当前那一枚页签(鼠标移上去滑回来)。默认关闭' },
                // ★★ 2.53.94(用户:「这个功能是默认固定的,无需在设置面板中单独勾选开启。但需要勾选
                //   另一个关联功能,以决定该悬贴定位功能是否自动激活。」+「默认不会自动激活」):
                //   ★ 先**撤掉** 2.53.93 那枚 spp_idx_all —— 功能本体(两枚「全贴索引」按钮 + 跨页定位)
                //     固定存在,不再由任何开关决定它出不出现。按钮永远在,点一下才开始抓。
                //   ★ 换成这一枚:它只回答一个问题 —— **打开帖子时要不要自动激活全贴定位**
                //     (自动先吃缓存、缺页才逐页抓,并把视图切到全贴)。
                //   ★ 默认**关闭**(用户口径「默认不会自动激活」)⇒ 不勾就一切照旧,纯手动。
                //   ★ def: false 必须与 15a-2 里那句 SPP_CONFIG.bool('spp_idx_auto', false) 逐字同值
                //     (tools/verify/settings-panel.js 的 EXPECT 拿这两处对着核)。
                { key: 'spp_idx_auto', label: '打开帖子自动索引全贴(含图 / 链接跨页定位)', type: 'bool', def: false,
                  hint: '勾选:打开帖子后自动开始全贴索引(先吃缓存、缺的页才逐页抓)并切到全贴视图,抓的过程可在面板里随时停。不勾选:一切照旧,需要点面板底部那枚「全贴索引」才开始。默认关闭' },
                // ★★ 2.53.36(用户:「对于白色字体的文本,脚本自动把它转化为加粗黑体(参考网页文本:urusai_cc)」):
                //   本表第五枚 bool 行。def: true 与 15f 里那句 SPP_CONFIG.bool('spp_fix_white_text', true) 逐字同值。
                { key: 'spp_fix_white_text', label: '白底白字转加粗深色', type: 'bool', def: true,
                  hint: '帖子里发帖人用白色字体写的字,在白底正文上完全看不见;自动转成加粗的深色字。默认开启' },
                // ★★ 2.53.39(用户:「在悬浮按钮的设置面板中,有一个可启动和关闭的功能,默认是启动的」
                //   +「自动购买功能默认开启免费帖子,而收费帖子则默认关闭」;随后当场拍板
                //   「不做收费帖那枚开关,收费帖一律手动」):本表第六枚 bool 行,也是 15g 唯一那枚开关。
                //   def: true 必须与 15g 里那句 SPP_CONFIG.bool('spp_autobuy_free', true) 逐字同值
                //   (tools/verify/settings-panel.js 的 C/D 段拿这两处对着核)。
                { key: 'spp_autobuy_free', label: '免费帖自动购买', type: 'bool', def: true,
                  hint: '打开帖子若售价为 0 SP币 且还没买过,自动买下 —— 走后台请求,页面不跳转。收费帖一律手动' },
                // ★★ 2.53.41(用户:「A 同族框」+「要改圆角」):本表第七枚 bool 行。
                //   def: true 必须与 15g 里那句 SPP_CONFIG.bool('spp_buy_lift', true) 逐字同值
                //   (tools/verify/settings-panel.js 的 C/D 段拿这两处对着核)。
                { key: 'spp_buy_lift', label: '提亮已购正文', type: 'bool', def: true,
                  hint: '买下之后被解锁的正文,站点原本按引用块显示(12px、偏褐);打开后按正常正文显示(13px、深灰)。默认开启' },
                // ★★ 2.53.45(用户:「鼠标停在某一楼层时候会高亮…这个功能能在设置面板那里
                //   设置为关闭吗?(默认关闭)」):本表第八枚 bool 行,也是本表**唯一默认关闭**的
                //   纯视觉开关 —— def: false 就是本轮需求本体。
                //   ★ 定位见新模块 15h 的长注释:那条高亮是**我们星空皮肤**(spp_home_star_glass)
                //     里的 html.spp-hm-star #main .t5 tr:hover > th/td,不是站点给的。
                //   ★ def: false 必须与 15h 里那句 SPP_CONFIG.bool('spp_floor_hover', false)
                //     逐字同值(tools/verify/settings-panel.js 的 C/D 段拿这两处对着核)。
                { key: 'spp_floor_hover', label: '悬停高亮', type: 'bool', def: false,
                  hint: '鼠标停在某一楼(或其它页面的表格行)上时整行刷一层淡底。默认关闭 —— 关掉后鼠标划过不上色' },
                // ★★ 2.53.47(用户:「网址右侧有一个常驻的翻页悬浮框(设置面板的入口),能否增加
                //   自动半隐藏(靠边隐藏,可以在设置面板关闭,默认关闭)」):本表第九枚 bool 行,
                //   也是第二枚**默认关闭**的纯视觉开关 —— def: false 就是本轮需求本体。
                //   ★ 同款先例是 15a-2 的 spp_imgidx_edge(含图楼层靠边自动隐藏),两处 CSS 结构一致。
                //   ★ def: false 必须与第 2 模块里那句 SPP_CONFIG.bool('spp_nav_edge', false)
                //     逐字同值(tools/verify/settings-panel.js 的 C/D 段拿这两处对着核)。
                //   ★ 键名不能写成 spp_nav:那是 spp_nav_step 的**前缀**,仓库吃过这个亏
                //     (见 spp_imgidx_show 那行的注释)。
                // ★★ 2.55.25(用户:「我希望增加一个黑暗模式,设置面板里面可以打开(默认关闭);
                //   框选的两个区域变成黑色(反光黑玻璃什么的,但是不能影响文本阅读)」):
                //   本表新增一枚 bool,也是第三枚**默认关闭**的纯视觉开关 —— def: false 就是本轮需求本体。
                //   ★ def: false 必须与模块 19 门口那句 SPP_CONFIG.bool('spp_read_dark', false)
                //     逐字同值(tools/verify/settings-panel.js 的 C/D 段拿这两处对着核)。
                //   ★ 它只管帖子页(read.php):模块 19 以 WANTS.readOnly 为门,样式块整体挂在
                //     html.spp-rdark 上 ⇒ 版块页 / 首页那两套皮一个字都不受影响。
                //   ★ 键名不能写成 spp_read:那会与 spp_read_chip 之类同前缀(本仓库吃过这个亏)。
                { key: 'spp_read_dark', label: '黑夜模式', type: 'bool', def: false,
                  hint: '进入黑夜模式即可' },
                { key: 'spp_nav_edge', label: '翻页悬浮框靠边自动隐藏', type: 'bool', def: false,
                  hint: '右侧那块翻页悬浮框贴到屏幕右缘时自动缩进去、只留一条窄边;鼠标移上去自动滑回来。默认关闭' },
                // ★★ 2.53.55(用户:「能否添加一个双箭头的两个按钮,用于滑动到第一页和最后一页?
                //   这两个镜头可以在悬浮设置面板中选择开启或关闭,默认是开启的。」):本表第十枚 bool 行,
                //   也是第三枚**默认开启**的翻页相关开关。
                //   def: true 必须与 15j 里那句 SPP_CONFIG.bool('spp_pg_ends', true) 逐字同值
                //   (tools/verify/settings-panel.js 的 C/D 段拿这两处对着核)。
                //   ★ 键名不能写成 spp_pg:仓库吃过「键名是另一个键的前缀」的亏(见 spp_imgidx_show 那行)。
                { key: 'spp_pg_ends', label: '翻页双箭头(首页 / 末页)', type: 'bool', def: true,
                  hint: '翻页条两端各补一枚 « / » 双箭头:点一下直接到第 1 页 / 最后一页。默认开启 —— 关掉后只剩 ‹ › 单箭头(上一页 / 下一页)' },
                // ★★ 2.53.58(用户:「测试网址 search.php? 能否增加一个功能,将排序方式由时间排序改为人气排序?」):
                //   本表第十一枚可写行。站点搜索表单的「排序方式」是一个 select[name="orderway"],
                //   四个 option 是 postdate / lastpost / replies / hits,HTML 默认选中 postdate
                //   ⇒ 用户搜出来的结果一律按发帖时间排,这里把默认值改成「人气」。
                // ★★ 2.53.59(用户:「你排序的那一个移到悬浮设置面板那里就行了…无需放在网页上」):
                //   结果页那条排序条整套撤掉,本行从「人气 开/关」升级成**下拉框**(四种序)——
                //   这就是本项目第四类行 data-type="select"(前四类:img_json / number / bool / select)。
                //   ★ options 的 v 必须与站点那四个 option 的 value 逐字一致,
                //     与脚本里 SEARCH_ORDER_ALLOWED 同表;读数走 SPP_CONFIG.pick(枚举收敛)。
                { key: 'spp_search_order', label: '搜索结果排序方式', type: 'select', def: 'hits',
                  options: [{ v: 'hits', t: '人气' }, { v: 'replies', t: '回复' },
                            { v: 'postdate', t: '发帖时间' }, { v: 'lastpost', t: '最后回复' }],
                  hint: '搜索表单的「排序方式」默认选中这一种(站点原版默认是「发帖时间」)。只改表单默认值,不动地址上已经写明的那一种。默认:人气' },
                // ★★ 2.53.60(用户:「实时默认渲染这个功能默认是打开的,可以在脚本的悬浮设置面板那里关闭」):
                //   本表第十二枚 bool 行,也是模块 12 那枚预览按钮的**渲染时机开关** ——
                //   def: true 就是本轮需求本体:默认就实时渲染,不必再点那枚按钮。
                //   ★ 这一行的默认值 true 必须与模块 12 门口那句读数调用逐字同值
                //     (tools/verify/settings-panel.js 的 C/D 段拿这两处对着核)——
                //     本注释刻意不抄那句代码:本轮预检要数它出现的次数,抄一次就把数搅乱了。
                //   ★ 它管的是**渲染时机**,不是预览本身:关掉之后模块 12 回到 2.53.59 的老
                //     行为(点一次 👁 预览 渲染一次),预览按钮本身照旧在。
                //   ★ 键名不能写成 spp_preview:那是 spp_preview_btn 的**前缀**
                //     (仓库吃过「键名是另一个键的前缀」的亏,见 spp_imgidx_show 那行)。
                { key: 'spp_preview_live', label: '编辑器实时预览', type: 'bool', def: true,
                  hint: '输入框下方常驻一块预览,边打边渲染(图片 / 表情 / 引用块都算)。默认开启 —— 关掉后回到点一次「预览」渲染一次的旧做法' },
                // ★★ 2.53.72(用户:「OK,根据你的样式修改脚本。」+「可以在设置面板把这个板块关闭,
                //   默认是开启的。」):本表第十三枚 bool 行 —— 页眉那两块淘宝广告位的处置开关。
                //   ★ 这一行的默认值 true 必须与模块 15m(initBrandBar)门口那句读数调用逐字同值
                //     (tools/verify/settings-panel.js 的 C/D 段拿这两处对着核)。
                //   ★ hint 里绝不能出现「def:」字面 —— 守卫抽默认值的正则
                //     (tools/verify/settings-panel.js 的 /\{\s*key:\s*'([^']+)'[\s\S]*?def:\s*([^,}]+)/g)
                //     是非贪婪的,会把 hint 里的那段当成这一行的默认值读走(2.53.47 吃过这个亏)。
                { key: 'spp_brand_bar', label: '南+ 品牌条', type: 'bool', def: true,
                  hint: '把页眉那两块淘宝广告图摘掉跳转、重排成一条品牌条:两图分列左右两端,中间常驻一行 SOUTH-PLUS。默认开启' },
                // ★★ 2.53.96(用户:「对于输入框,我希望在该板块增加两个功能:第一个是自动为
                //   输入框内未进行图床标注的所有链接自动添加图床标符。第二个需求是增加一个 UI
                //   组件:点击后显示下拉框,其中包含多个图床选项,点击即可跳转。」):本表第十四枚
                //   bool 行 —— 管的是「自动」那一半(粘贴进输入框后立刻补标符,幂等)。
                //   手动那一枚是 13 模块工具栏「链接与媒体」组里的『标』按钮,不走开关。
                //   ★★ 2.55.13(用户:「链接自动标注 这个功能默认是关闭的」):默认值 true → false。
                //     四处同值:登记表 / 手写 HTML / 13 模块门口那句读数 / migrateSettings 里的写回。
                //     ★ 光改默认值对老用户无效 —— 面板的保存循环是把每一行都写一遍的,
                //       存过面板的机器里躺着 spp_host_mark=true,所以补了一次性迁移。
                //   ★ 默认值 false 必须与 13 模块门口那句读数调用(键 spp_host_mark)逐字同值
                //     (tools/verify/settings-panel.js 的 EXPECT 拿这两处对着核)。
                //   ★ hint 里绝不能出现「def:」字面 —— 守卫抽默认值的正则是非贪婪的,
                //     会把 hint 里那段当成这一行的默认值读走(2.53.47 吃过这个亏)。
                // ★★ 2.55.12(用户:「修改设置面板中的部分文本」+ 他给的原文):
                //   逐字替换成他写的这一句。原来那句把「六个图床各自的名字 / 域名按首字母
                //   大写」讲得太细,新的一句讲的是**作用范围**(文本输入框)与**结果**
                //   (所有超链接都按图床格式标注)。
                //   ★ 原来尾巴上的「默认开启」按他给的原文去掉了(要留一句话就能加回)。
                { key: 'spp_host_mark', label: '链接自动标注', type: 'bool', def: false,
                  hint: '作用于文本输入框,勾选后会自动将输入框内的所有超链接按图床格式标签进行标注' },
                // ★★ 2.53.99 / 2.54.1:这一格原来登记过一枚 bool 行(键 spp_self_img,
                //   标签「图片链接转 [img]」,默认 true)—— 带图片后缀的链接不补【站点名】,
                //   改成包成 [img]…[/img] 内嵌显示(站内附件 / 六个图床 / 任意外站都算)。
                // ★★ 2.55.13(用户:「img 这个功能默认打开,禁止用户关闭,从设置面板中移除
                //   (这个功能作为默认开启即可,无需在设置面板中显示。)」):**整行撤掉**。
                //   · 两张表一起撤(这里 + buildOverlay 那份手写 HTML)—— 只撤一边,
                //     面板自检当场报「登记了但没渲染 / 渲染了但没登记」。
                //   · 运行时那一侧改成常量 true(13 模块门口那句 SELF_IMG),用户存过的
                //     旧值一律不再读 ⇒「禁止关闭」是硬的:面板上没有控件、存储里写着
                //     false 也不生效。行为与 2.54.1 的默认态逐字相同。
                //   · 键 spp_self_img 从此成为**孤儿键**(没有任何地方读它)。刻意不做
                //     清理:删了没好处,万一以后要加回三选一形态(包 [img] / 补【图床名】/
                //     完全不碰),用户原来的选择还在。
                // ★★ 2.53.97(用户:「能否屏蔽贴吧表情?即鼠标移上去时不显示文本框?」):
                //   本表第十六枚 bool 行 —— 表情格 hover 弹的那个名称小框(浏览器原生 title 提示)。
                //   ★ 默认 **false** 就是本轮需求本体:不勾 = 不弹框(脚本不再给 img 写 title)。
                //   ★★ 2.55.14(用户:「表情名称提示;这个功能默认开启」):默认值 false → true。
                //     四处同值:登记表 / 手写 HTML / 14 模块门口那句读数 / migrateSettings 里的写回。
                //     ★ 老用户存储里那条 false 由一次性迁移翻掉 —— 面板的保存循环把每一行都
                //       写一遍,只改默认值对它们无效(spp_host_mark 那一轮吃过同一口亏)。
                //   ★ 默认值 true 必须与 14 模块门口那句读数调用(键 spp_emoji_tip)逐字同值
                //     (tools/verify/settings-panel.js 的 EXPECT 拿这两处对着核)。
                //   ★ hint 里绝不能出现「def:」字面 —— 守卫抽默认值的正则是非贪婪的,
                //     会把 hint 里那段当成这一行的默认值读走(2.53.47 吃过这个亏)。
                // ★★ 2.55.12(同上,用户给的原文):hint 逐字换成他写的这一句。2.55.14 翻默认值
                //   时 hint 一字未动 —— 那句「为什么它基本没用」是他自己的判断,不替他删。
                { key: 'spp_emoji_tip', label: '表情名称提示', type: 'bool', def: true,
                  hint: '鼠标悬停在表情上会有文本提示,但由于确定表情包对应的文本工作量较大,该功能基本无用' },
                // ★★ 2.55.9(用户:「增加一个设置按钮:决定贴纸是否居中(默认关闭,即不居中)」):
                //   本表第十八枚 bool 行。键 spp_sticker_center,默认 **false** = 不居中。
                //   ★ 默认值必须与 initImageViewer 的 applyDynamicStyles 里那句读数逐字同值。
                //   ★ hint 里绝不能出现「def:」字面 —— 守卫抽默认值的正则非贪婪(2.53.47 吃过亏)。
                { key: 'spp_sticker_center', label: '贴纸居中显示', type: 'bool', def: false,
                  hint: '只管脚本贴进帖子里的表情 / 贴纸(含自带的五套与滑稽那套):勾选 = 居中;不勾选 = 保持行内原样。与上面「图片居中显示」互不干扰 —— 那一条不再管表情 / 贴纸。默认关闭' },
                // ★★ 2.55.4(用户:「在社区任务中,只要用户登录网站,脚本就自动领取奖励…
                //   脚本需支持关闭自动领取功能,但默认开启。」):本表第十七枚 bool 行。
                //   ★ 这个键 2.52.78 就有了,但一直只登记在 SETTINGS_DEFS / SPP_HIDDEN_SWITCHES
                //     两张**只读**表里 —— 界面上根本没有入口,「可关闭」只能手改 GM 存储。
                //     本轮补上真入口:SETTINGS_DEFS 那条照旧留着(它是键名登记处),
                //     SPP_HIDDEN_SWITCHES 那条按 2.53.9 的先例搬走(那张表的语义是「界面没入口」)。
                //   ★ 默认值 true 必须与 initTaskAutoClaim 门口那句读数调用逐字同值
                //     (tools/verify/settings-panel.js 的 C/D 段拿这两处对着核)。
                //   ★ hint 里绝不能出现「def:」字面 —— 守卫抽默认值的正则是非贪婪的,
                //     会把 hint 里那段当成这一行的默认值读走(2.53.47 吃过这个亏)。
                { key: 'spp_task_autoclaim', label: '社区论坛任务自动领取', type: 'bool', def: true,
                  hint: '打开任意南+页面时,脚本在后台把社区任务里「可领取」的奖励领掉(先申请、再领取),页面不跳转;同一个任务领到过一次后 24 小时内不再碰它。默认开启' }
            ]
        },
        {
            group: '数值设置', kind: 'number',
            fields: [
                { key: 'spp_page_width', label: '页面宽度 (px)', type: 'number', def: 1400, min: 1200, max: 1600,
                  hint: '可填 1200-1600(基准 1400),只改容器宽度' },
                { key: 'spp_nav_step', label: '翻页步长', type: 'number', def: 1, min: 1, max: 50,
                  hint: '悬浮按钮每次点击翻几页(1 = 逐页翻)。范围 1-50' },
                // ★★ 2.53.47:本表第三枚 number 行 ——「正文文字大小」。
                //   用户原话:「楼层帖子顶部有设置字体大小的按钮:小、中、大;能否把这个移动到悬浮按钮
                //   的设置面板里面,并且屏蔽楼层帖子顶部的按钮?」
                //   ★ 为什么用 number 而不是 select:三种档位恰好能写成「数值 + 步长 3」,
                //     而 number 是本表**已有**的类型 —— 走 select 要新开第四类 data-type,
                //     得同步改 buildOverlay 四处(渲染行 / 面板自检选择器 / 行点击 / 保存分支)
                //     外加 openSettingsPanel 回填与守卫类型表,约 +60 行,为一个三档开关不划算。
                //   ★ 12 / 14 / 18 不是随手写的数:站点正文底色号就是
                //     `.small{font-size:12px}` / `.middle{font-size:14px}` / `.big{font-size:18px}`
                //     (线上实测读到的三条规则),所以三个档位与站点原生三档逐字对齐。
                //   ★ def: 14 = 站点默认(首帖初始 class 是 .f14);min/max/step 让 spinner
                //     恰好停在 12 / 15 / 18 —— 15 落在站点没有的档位上,但 15 也是合法的字号,
                //     不做否决。**hint 里绝不能出现「def:」字面**:守卫抽默认值的正则
                //     (tools/verify/settings-panel.js 的 /\{\s*key:\s*'([^']+)'[\s\S]*?def:\s*([^,}]+)/g)
                //     是非贪婪的,会把它当成这一行的 def 读走。
                { key: 'spp_font_size', label: '正文文字大小 (px)', type: 'number', def: 14, min: 12, max: 18,
                  hint: '12 = 小、14 = 中(站点默认)、18 = 大。改完刷新后,所有楼层的正文一起变;上方那三枚「小 中 大」按钮会被收掉' },
                // ★★ 2.53.95(用户:「针对链接或图片,如果数量过多,面板会显示大量结果。最好增加翻页
                //   功能,例如每页显示 15 个条目。」+「如果能在设置面板里让用户自定义这个「15」这个
                //   数字,也请增加这个功能。」):本表第四枚 number 行 —— 两栏索引**每页显示几行**。
                //   它管的是面板里那份清单铺几行(含图楼层 / 含链接楼层),**不改抓取范围**:
                //   索引该抓多少页还抓多少页,只是不再一次把 N 百行全铺在那一栏里。
                //   一页装得下时翻页条整条不出现(见 paintPager)。
                //   ★ 读数点就是 15a-2 的 perPage(),两处必须逐字同值
                //     (tools/verify/settings-panel.js 的 EXPECT 拿这两处对着核)。
                { key: 'spp_idx_per', label: '索引面板每页行数', type: 'number', def: 15, min: 5, max: 60,
                  hint: '含图 / 链接两栏每页显示多少行(5-60)。超过一页时面板底部出现翻页条;填大一点就能一栏看全。默认 15' }
            ]
        },
        {
            // ★★ 2.55.1:新增「已读记录 (仅资源区)」组 —— 本轮三句需求落在这里
            //   (开关 / 判定已读的停留秒数 / 按时间清除)。功能本体在模块 4b,这里只是登记处。
            //   ★ 本组是唯一同时含 bool + number + select 三类的组 —— 三类都是面板**已有**的行类型,
            //     不引入任何新 data-type(理由见 buildOverlay 里那段注释)。
            //   ★ 那枚「清除记录」按钮**不在这张表里**:动作行不是参数行(同 2.53.9 的「恢复默认头像」)。
            group: '已读记录 (仅资源区)', kind: 'bool',
            fields: [
                { key: 'spp_read_mark', label: '资源区已读标记', type: 'bool', def: true,
                  hint: '打开资源区的帖子并停留够下面的秒数,该帖在资源区列表的卡片底部就会挂上一枚「已读」标识。茶馆不生效。默认开启' },
                { key: 'spp_read_dwell', label: '判定已读的停留秒数', type: 'number', def: 5, min: 0, max: 600,
                  hint: '帖子页实际停留(只算页面可见的时间)达到这个秒数才算「已读」,可填 0-600。默认 5 秒;填 0 = 打开即算' },
                { key: 'spp_read_purge', label: '清除早于 (小时 / 天)', type: 'number', def: 24, min: 1, max: 100000,
                  hint: '「清除记录」按这个数清理更早的已读记录,可填 1-100000。默认 24' },
                { key: 'spp_read_purge_unit', label: '清除的时间单位', type: 'select', def: 'hour',
                  options: [{ v: 'hour', t: '小时' }, { v: 'day', t: '天' }],
                  hint: '上面那个数的单位:小时或天。默认小时' }
            ]
        },
        {
            /* ★★ 2.55.11(用户:「以后再加套系走哪条路」→ 当场拍板「面板可粘贴」):
               新增「自备表情 / 贴纸」组。
               ★ 本组引入**第五类行 data-type="packs"**(前四类:img_json / number / bool / select)。
                 口径与 2.53.0 加 bool 时一字不差:新类型必须在 buildOverlay 里同步四处 ——
                 ① 渲染行(含配套 CSS)② 面板自检的选择器 ③ 保存分支 ④ openSettingsPanel 的回填,
                 外加守卫的类型表。少一处就会出现「能看不能存」这类静默失败。
               ★ 本行是**唯一**「值是多行文本」的行,所以它不参与任何数值 / 枚举收敛:
                 读数走 SPP_CONFIG.text(),存的是用户原样粘进来的那份文本。
               ★ 解析不在这里 —— 在文件作用域的 sppParseCustomPacks()(模块 3 比本模块先跑,
                 那张 URL 集合要更早拿到)。面板只把它的**小结**摊开(renderPacksFeedback),
                 保证「面板上说的数字」与「表情栏里真出现的东西」同源。
               ★★ 2.55.15(用户:「如果 emoji 和贴纸共用一个自定义栏框 … 会出现两个都无法
                 检索到的情况。我建议分为左右两栏:左栏为 emoji,右栏为贴纸自定义。」):
                 本组第一格拆成**两格两键** —— spp_custom_emoji(左,emoji 页)/
                 spp_custom_sticker(右,贴纸页);旧键 spp_custom_packs 不再登记、不再渲染
                 (它的内容由文件作用域的一次性迁移 sppMigratePacksSplit() 按页拆进两枚新键)。
                 ★ 类型仍是第五类行 data-type="packs":三格要的东西一模一样(一块多行文本框 +
                   下面一行小结 + 原样存字符串),只是 data-key 不同 —— 没为此新增第六类行。 */
            group: '自备表情 / 贴纸 · 自定义图床', kind: 'packs',
            fields: [
                //   ★ 2.55.16:两行的 label / hint 与手写 HTML**逐字同值**(含等长口径:
                //     两格 hint 一模一样,页面名写进 label)—— 面板上那四块能不能横向对齐,
                //     靠的就是这份「等长」,所以这两行必须连左格一起改。
                { key: 'spp_custom_emoji', label: '自备 Emoji(进 emoji 页)', type: 'packs', def: '',
                  hint: '一行套系名 + 若干行图片直链,套系之间空一行;地址后接一段文本 = 那一张的悬停提示(不写就显示「套系名 + 序号」)' },
                { key: 'spp_custom_sticker', label: '自备贴纸(进贴纸页)', type: 'packs', def: '',
                  hint: '一行套系名 + 若干行图片直链,套系之间空一行;地址后接一段文本 = 那一张的悬停提示(不写就显示「套系名 + 序号」)' },
                { key: 'spp_host_extra', label: '自定义图床(粘贴区)', type: 'packs', def: '',
                  hint: '一行一个图床:「名称 首页地址 认链域名」。名称进标注的【名称】,首页地址进「图床 ▾」下拉,认链域名可选' }
            ]
        }
    ];

    // 只读登记:那些「照旧生效、但界面上不显示」的开关(2.52.63/2.52.64 起)。
    //   ★★ 2.52.95:本表**不再被渲染**(2.52.94 那次加的「开关状态(只读)」组按用户要求撤掉了),
    //      另外删掉了 spp_board_polish(详见 SETTINGS_DEFS 上方注释),只剩 8 项。
    //      为什么还留着:它是这 8 个键名【唯一的登记处】—— 文档生成器「九、设置面板」与
    //      tools/verify/settings-panel.js 都从它抽键名,而那个守卫还会逐项核对
    //      「每个键在脚本里仍有一处 SPP_CONFIG.bool(key, true) 读取点」(撤 UI 不许撤功能)。
    //      删掉本表,这两道守卫会一起失效 —— 同 SETTINGS_DEFS 的道理(见下面的注释)。
    //   ★ 为什么不做成可勾选:用户 2.52.64 的定案是「既然灰色且无法勾选,那就隐藏不显示」,
    //     勾选框一旦真能点,等于把当时撤掉的那批 UI 又搬回来。想改就照键名手改 GM 存储。
    const SPP_HIDDEN_SWITCHES = [
        { key: 'spp_hide_bottom',        label: '屏蔽底部图例条与页脚' },
        { key: 'spp_hide_forumrel',      label: '屏蔽「论坛相关」白底面板' },
        { key: 'spp_hide_toptool',       label: '屏蔽顶部空白工具条' },
        { key: 'spp_nav_search_first',   label: '导航「搜索」提为主行动项' },
        // ★★ 2.52.95:spp_board_polish(版块页卡片 / 列表美化)从这里删掉 —— 见 SETTINGS_DEFS
        //   上方那段注释:它在脚本里已经没有任何读取点,属于「登记表写着照旧生效、其实早就不干活」。
        // ★★ 2.55.4:spp_task_autoclaim 从这里**搬走** —— 它在面板「功能开关(续)」组里
        //   有了自己的可勾选行(见 SPP_PANEL_DEFS)。本表的口径是「界面上没有入口、只能
        //   手改 GM 存储」,一个有界面入口的开关留在本表里就是登记处说假话(同 2.53.9
        //   处理 spp_hide_default_avatar 的做法)。8 项 → 7 项,键名仍登记在 SETTINGS_DEFS。
        // ★★ 2.53.9:spp_hide_default_avatar 从这里**搬走**了 —— 它在面板「功能开关」组里有了自己的
        //   可勾选行(见 SPP_PANEL_DEFS)。本表的口径是「界面上没有入口、只能手改 GM 存储」,
        //   一个有界面入口的开关留在本表里就是登记处说假话,所以按 2.52.95 处理 spp_board_polish 的
        //   同一做法移出。8 项 → 7 项(tools/verify/settings-panel.js 的 EXPECT_HIDDEN 同步改)。
        { key: 'spp_home_starfield',     label: '星空背景(全站)' },
        { key: 'spp_home_star_glass',    label: '星空背景的玻璃卡效果' }
    ];

    // ★★ 2.55.20:热度配色整份的**页内缓存**。为什么必须缓存:getTeaHeatColor() 是
    //   **每张卡**都问一次(茶馆一页 242 张卡,标题与两枚数字按钮各问一次),而一次读数
    //   要过 GM_getValue + JSON.parse。为什么缓存不会陈旧:设置面板改完是 location.reload(),
    //   缓存跟着页面一起没 —— 面板那条「保存并刷新」正是这里的同步手段。
    let heatStyleMemo = null;
    const SPP_CONFIG = {
        // 布尔开关:GM 存储 → 内置默认(目前全为 true)。字符串 'false' 也算关。
        bool(key, dflt) {
            const raw = GM_getValue(key, null);
            if (raw === null || raw === undefined || raw === '') return dflt;
            return typeof raw === 'string' ? raw.toLowerCase() !== 'false' : !!raw;
        },
        // 数值:GM 存储 → 内置默认 → 最近的合法值(三者都不合法时取 min)。
        num(key, dflt, min, max) {
            const stored = cfgFit(GM_getValue(key, null), min, max, true);
            if (stored !== null) return stored;
            const d = Number(dflt);
            if (isFinite(d) && d >= min && d <= max) return d;
            return min;
        },
        // 枚举:不在允许集合内(含历史值)就回内置默认。
        pick(key, dflt, allowed) {
            const stored = String(GM_getValue(key, null) || '');
            if (stored && allowed.indexOf(stored) >= 0) return stored;
            return allowed.indexOf(dflt) >= 0 ? dflt : allowed[0];
        },
        // 文本(自备套系粘贴区,★ 2.55.11):GM 存储 → 内置默认;存的不是字符串就当默认值。
        //   ★ 不走任何收敛 —— 这一格装的是用户原样粘进来的多行文本,不是参数。
        text(key, dflt) {
            const raw = GM_getValue(key, null);
            if (raw === null || raw === undefined) return dflt;
            return typeof raw === 'string' ? raw : String(raw);
        },
        // 图片参数(整份存在一个 JSON 字符串里):字段级回退到 IMG_DEFAULTS。
        imgNumber(field, min, max) {
            const stored = cfgFit(cfgJsonNum(GM_getValue(IMG_SETTINGS_KEY, null), field), min, max, true);
            if (stored !== null) return stored;
            const d = Number(IMG_DEFAULTS[field]);
            return isFinite(d) ? d : min;
        },
        imgBool(field) {
            let raw = null;
            try {
                const s = GM_getValue(IMG_SETTINGS_KEY, null);
                if (s) {
                    const o = JSON.parse(s);
                    if (o && typeof o === 'object' && o[field] !== undefined && o[field] !== null) raw = o[field];
                }
            } catch (_) { raw = null; }
            if (raw !== null) return typeof raw === 'string' ? raw.toLowerCase() !== 'false' : !!raw;
            return !!IMG_DEFAULTS[field];
        },
        imgString(field, allowed) {
            let raw = null;
            try {
                const s = GM_getValue(IMG_SETTINGS_KEY, null);
                if (s) { const o = JSON.parse(s); if (o && typeof o === 'object') raw = o[field]; }
            } catch (_) { raw = null; }
            const v = raw === null || raw === undefined ? '' : String(raw);
            if (v && allowed.indexOf(v) >= 0) return v;
            const d = String(IMG_DEFAULTS[field]);
            return allowed.indexOf(d) >= 0 ? d : allowed[0];
        },
        // 图片参数整份(给 initImageViewer / 面板渲染用):逐字段收敛,存储坏了也不会出 NaN。
        imgSettings() {
            return {
                maxImgHeight: this.imgNumber('maxImgHeight', 30, 100),
                maxImgWidth:  this.imgNumber('maxImgWidth', 20, 100),
                thumbSize:    this.imgNumber('thumbSize', 32, 128),
                centerImg:    this.imgBool('centerImg'),
                wheelAction:  this.imgString('wheelAction', ['switch', 'zoom'])
            };
        },
        /* ★★ 2.55.20:茶馆热度配色的读取层(默认值表与字段语义见 HEAT_STYLE_DEFAULTS 上方那笔账)。
           两条收敛都是为了「不让手写坏的配置静默失效」:
             ① 三档边界读出来**升序排序** —— 面板允许把「第一档」填得比「第二档」大,
                若原样使用,中间那一档就永远取不到色(排完最坏也只是两档同色,四色仍可用);
             ② 色值只认 #rgb / #rrggbb —— 这一串最终进 style.setProperty('color', …) 与
                --spp-mbtn-fg,是**存储直通 CSS** 的接口,正则收不到的值一律回落内置默认色。
           数值走 cfgFit:空 / 非数回落默认、越界拉回最近边界(负值拉回 0);其中 0 是**合法档位**
           (zeroAsMissing 必须传 false —— 传 true 会把「第一档填 0」当成没设过)。
           ★ 返回值永远是 7 个字段齐全的对象:调用方(getTeaHeatColor / 面板渲染 / 面板回填)
             都不做二次兜底,缺字段这件事只在这里处理一次。 */
        heatStyle() {
            if (heatStyleMemo) return heatStyleMemo;
            let raw = null;
            try {
                const stored = GM_getValue(HEAT_STYLE_KEY, null);
                if (stored) {
                    const o = JSON.parse(stored);
                    if (o && typeof o === 'object') raw = o;
                }
            } catch (_) { raw = null; }
            const pickColor = (k) => {
                const v = (raw && typeof raw[k] === 'string') ? raw[k].trim() : '';
                return HEAT_COLOR_RE.test(v) ? v : HEAT_STYLE_DEFAULTS[k];
            };
            const bounds = ['tCold', 'tWarm', 'tHot'].map((k) => {
                const v = cfgFit(raw ? raw[k] : null, 0, HEAT_STYLE_MAX, false);
                return v === null ? HEAT_STYLE_DEFAULTS[k] : v;
            }).sort((a, b) => a - b);
            heatStyleMemo = {
                tCold: bounds[0], tWarm: bounds[1], tHot: bounds[2],
                cCold: pickColor('cCold'), cWarm: pickColor('cWarm'),
                cHot: pickColor('cHot'), cPeak: pickColor('cPeak')
            };
            return heatStyleMemo;
        }
        // ★★ 2.53.62:这里原有 cmScope() —— 首页 CM 卡片范围的读数('cm' / 'latest' / 'all' 三档,
        //   并把历史值 'c104' 归一为 'cm')。用户要求把这个开关整条撤掉、范围写死 'cm' ⇒
        //   读数方法与它的存储键一起删除。要恢复就是把它加回这张表,再把模块 16 的调用点
        //   从一个字面量改回一次读数(本轮这一条同时被 settings-panel 守卫盯着)。
    };

    // ==================== 搜索结果排序(2.53.59)====================
    // 面板里那枚下拉框允许的四种序 —— 与站点自己那个 select[name="orderway"] 的四个 option
    //   逐字对应(postdate / lastpost / replies / hits),顺序按「用户最可能想要的在前」。
    // ★ 这份表同时是 SPP_PANEL_DEFS 里 options 的口径来源;读数一律走 searchOrder(),
    //   它用 pick() 做枚举收敛(GM 存储里被手改成野值时回到「人气」,不会把野值带进表单)。
    const SEARCH_ORDER_ALLOWED = ['hits', 'replies', 'postdate', 'lastpost'];
    const SEARCH_ORDER_LABEL = { hits: '人气', replies: '回复', postdate: '发帖时间', lastpost: '最后回复' };
    function searchOrder() {
        return SPP_CONFIG.pick('spp_search_order', 'hits', SEARCH_ORDER_ALLOWED);
    }

    // ==================== 全局设置面板 ====================
    // ★★ 2.52.62:本表由 15 项减到 6 项。删掉的 9 项都是「出现于 <=2.16.0、且已被
    //   新体系取代或压根不干活」的功能 —— 具体理由与实测证据见 2.52.62 的模块注释:
    //     spp_ui_refresh / spp_vis_glass / spp_vis_serif / spp_vis_liftbtn(主题层与视觉三件套)
    //     spp_progress_bar / spp_hover_highlight(进度条与悬停高亮)
    //     spp_content_spots / spp_spots_autoexpand / spp_spots_img_label(内容定位导航三键)
    //     spp_auto_free / spp_auto_sign / spp_domain_redirect(自动购买免费帖 / 自动签到 / 域名锁定)
    //   ★ 旧值仍留在 GM 存储里,但已无人读取;要彻底清掉可手删对应的 spp_* 键。
    const SETTINGS_DEFS = [
        { key: 'spp_hide_bottom',        label: '屏蔽底部图例条与页脚',  default: true },
        { key: 'spp_hide_forumrel',      label: '屏蔽「论坛相关」白底面板', default: true },
        { key: 'spp_hide_toptool',       label: '屏蔽顶部空白工具条',     default: true },
        { key: 'spp_nav_search_first',   label: '导航「搜索」提为主行动项', default: true },
        // ★★ 2.52.95:删掉这一行 —— key 是 spp_board_polish(版块页卡片/列表美化)。
        //   起因:本轮给 settings-panel 守卫加了一条「登记表里的每个键,在脚本里都必须还有一处
        //   SPP_CONFIG.bool(key, true) 读取点」的断言(撤 UI 不许撤功能),当场把它抓出来:
        //   全文搜 spp_board_polish 只命中两张登记表本身,**没有任何读取点**。
        //   原因是版块页美化后来拆成了各自独立门控的几块(本版公告条 / 工具条 / 版块页顶部三键 /
        //   一体流),那个笼统的总开关早就不干活了。留着它等于在登记表里写一句假话
        //   (「这些开关照旧生效」),所以按 2.52.62 处理同类死键的做法直接删除。
        //   ★ 存储里可能有这个旧值,但既然无人读取,删掉登记不影响任何行为。
        { key: 'spp_task_autoclaim',     label: '社区论坛任务自动领取(任意页面打开即尝试)', default: true },
        // ★★ 2.55.4:上面这条**留在这里**(本表是键名登记处),但它 2.55.4 起也有了
        //   SPP_PANEL_DEFS「功能开关」组的可勾选行 —— 所以同轮的 SPP_HIDDEN_SWITCHES
        //   那条被搬走了。默认值 true 与面板那一行、与 initTaskAutoClaim 的读数点三处同值。
        // ★★ 2.53.9:default true → **false**(用户:「…默认关闭」),标签同步改成面板里那枚开关的名字。
        //   它现在有自己的界面入口(SPP_PANEL_DEFS「功能开关」组),所以也从 SPP_HIDDEN_SWITCHES 里搬走了。
        { key: 'spp_hide_default_avatar',label: '用户名 + 彩色背景头像(替换默认头像)', default: false },
        // ★ 2.53.0:新功能的开关登记在 SPP_PANEL_DEFS(它是【面板会渲染】的那张表),
        //   所以不进 SPP_HIDDEN_SWITCHES(那张表的语义是「照旧生效、界面不显示」)。
        //   这里补一条是为了让「开关目录」保持完整:本表是那几个布尔键名的登记处。
        { key: 'spp_auto_expand_hidden', label: '自动展开隐藏楼层(纯 mark 水帖等被站点折叠的楼层)', default: true }
    ];

    let openSettingsPanel = null; // 由 initSettingsPanel 赋值

    function initSettingsPanel() {
        // CSS
        GM_addStyle(`
            #spp-global-settings-overlay {
                display:none; position:fixed; inset:0; z-index:100002;
                background:rgba(15,23,42,0.38); align-items:center; justify-content:center;
                backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px);
            }
            #spp-global-settings-overlay.spp-open { display:flex; }
            #spp-global-settings-box {
                background:var(--spu-surface, #ffffff); border-radius:8px;
                padding:22px 26px; width:min(900px, calc(100vw - 48px)); max-width:none;
                max-height:min(740px, calc(100vh - 48px));
                overflow-y:auto;
                box-shadow:0 24px 70px rgba(15,23,42,0.18), 0 0 0 1px rgba(15,23,42,0.04);
                font-size:14px; color:var(--spu-text,#1f2937);
                border:1px solid var(--spu-border,#d8dee7);
            }
            /* ★★ 2.52.95:两栏只作用在这个内层容器上(box 自己不再是多栏)。
               原来 box 自己就是 columns:2、按钮组靠 column-span:all 撑整幅 ——
               实测那套组合下 box 自己声明的 max-height 会被突破(声明 740px、实高 786px;
               视口 640 / 480 时同样多 46px),且右栏首组会跑到标题旁边上去。
               2.53.56 起这一层已从「多栏」改成「网格」,见下面那几条列分配。 */
            /* ★★ 2.53.56(用户:「请排版一下悬浮设置面板的UI,使其尽量左右对齐。」):多栏 → **两栏 flex**。
               为什么要换:columns:2 是「按高度装箱」自动分栏,实测(900 宽面板、功能开关 10 行)结果是
               左栏 = 图片设置 + 功能开关(~1080px)、右栏 = 数值设置 + 按钮组(~390px)——
               右下角一大片空白,而「保存并刷新」孤零零悬在面板中段,两边都不靠。
               ★ 中途试过 grid,被真机读数否掉:网格里「跨两行的功能开关」会把自己的高度**摊到它跨的每一行**上,
                 左栏那两组之间凭空多出 ~185px 空白(图片设置 h=212,数值设置却从 y=528 才开始)。
                 ⇒ 改成两个真正的列容器,两栏各自独立堆叠,谁高都不影响对方。
               ★ 行内不用动:实测每行控件**本来就右对齐**(所有 checkbox 右缘 658、行右缘 671,
                 各列等宽 428)—— 截图上那种参差是预览缩放的错觉。 */
            #spp-global-settings-box .spp-gs-cols { display:flex; gap:28px; align-items:flex-start; }
            /* flex:1 1 0 + min-width:0:两栏等宽,且允许收缩到比内容更窄
               (只写 flex:1 时 min-width:auto 会让长说明撑破栏宽、把控件挤到栏外)。 */
            #spp-global-settings-box .spp-gs-col { flex:1 1 0; min-width:0; }
            /* 窄屏(面板宽度 = min(900px, 100vw-48px)):两栏挤到每栏 <300px 就没法读了,这一档叠起来。
               ★ 760px 是「面板 ≈712px、两栏各 ≈330px」那条线。 */
            @media (max-width:760px) {
                #spp-global-settings-box .spp-gs-cols { flex-direction:column; gap:0; }
            }
            /* ★★ 2.52.95:display:block 必须显式写。站点自己有一条
               「h3, h2 { display: inline; font-size: 12px }」(页面内联样式表,实测命中),
               而本规则只声明了字号/边距/边框/下划线 —— 字号这边赢了(17px),display 那边没人争,
               于是 h3 变成行内盒:盒子宽按内容收缩到 68px(本该 902px)、分隔线只剩 68px,
               紧随其后的两栏容器还会从它的行盒下面开始(380 < 390),看起来像「组跑到标题上面」。
               ★ 定位手段:tools/probes/diag-panel-css.js(列出所有命中该元素的规则)。
               ★ 注意:这段是 CSS 注释,注释里**不能出现反引号** —— 模板字面量会被提前闭合,
                 整段 CSS 变 JS,症状与 2.52.92 那次一模一样(见 经验.md 1.34)。 */
            #spp-global-settings-box h3 {
                display:block;
                margin:0 0 16px; font-size:17px; color:var(--spu-text,#1f2937);
                border-bottom:1px solid var(--spu-border,#d8dee7); padding-bottom:10px;
                font-weight:650; letter-spacing:0;
            }
            /* ★ 2.53.56:break-inside:avoid 是给多栏用的,多栏撤了就没人认它 —— 一并删掉,别留死声明。 */
            #spp-global-settings-box .spp-gs-group {
                margin-bottom:14px;
            }
            #spp-global-settings-box .spp-gs-group-title {
                font-size:12px; color:var(--spu-muted,#6b7280);
                letter-spacing:0; margin-bottom:8px; padding-left:2px; font-weight:650;
            }
            #spp-global-settings-box .spp-gs-row {
                display:flex; align-items:center; justify-content:space-between;
                padding:8px 10px; border-radius:7px; cursor:pointer;
                transition:background 0.16s ease, color 0.16s ease;
            }
            #spp-global-settings-box .spp-gs-row:hover {
                background:var(--spu-overlay,#f2f5f8);
            }
            #spp-global-settings-box .spp-gs-row label {
                flex:1; cursor:pointer; font-size:13px; user-select:none;
            }
            #spp-global-settings-box .spp-gs-row input[type="checkbox"] {
                width:18px; height:18px; cursor:pointer; accent-color:var(--spu-accent,#2f6f68);
                flex-shrink:0;
            }
            #spp-global-settings-box .spp-gs-row input[type="range"] {
                width:90px; cursor:pointer; accent-color:var(--spu-accent,#2f6f68);
                flex-shrink:0;
            }
            #spp-global-settings-box .spp-gs-row .spp-val {
                font-size:11px; color:var(--spu-muted,#6b7280); min-width:36px; text-align:right;
                font-family:monospace;
            }
            /* ★ 2.52.65:参数行的说明小字。放在 <label> 内部、换行显示在标题下方。
               ★ 必须 display:block:这一行的 .spp-gs-row 是 flex + space-between,
                 写成行内元素会跟标题挤在同一行、被右侧控件压窄后折行成锯齿状。 */
            #spp-global-settings-box .spp-gs-row .spp-gs-hint {
                display:block; font-size:11px; font-weight:400; line-height:1.55;
                color:var(--spu-muted,#6b7280); margin-top:2px;
            }
            #spp-global-settings-box .spp-gs-row input[type="number"] {
                width:70px; padding:5px 8px; border:1px solid var(--spu-border,#d8dee7);
                border-radius:6px; font-size:13px; text-align:center;
                background:#fff; color:var(--spu-text,#1f2937);
            }
            #spp-global-settings-box .spp-gs-btns {
                display:flex; gap:8px; justify-content:flex-end;
                margin-top:16px; padding-top:14px; border-top:1px solid var(--spu-border,#d8dee7);
            }
            #spp-global-settings-box .spp-gs-btns button {
                padding:8px 18px; border:1px solid var(--spu-border,#d8dee7); border-radius:7px;
                cursor:pointer; font-size:13px; background:#fff; color:var(--spu-text,#1f2937);
                transition:background 0.16s ease, transform 0.16s ease, border-color 0.16s ease;
            }
            #spp-global-settings-box .spp-gs-btns button.spp-primary {
                background:var(--spu-accent,#2f6f68); color:#fff; border-color:var(--spu-accent,#2f6f68);
            }
            #spp-global-settings-box .spp-gs-btns button:hover { transform:translateY(-1px); }
            /* ★★ 2.53.9:「恢复默认头像」动作行。它不是 .spp-gs-row(没有参数语义、不参与保存),
               但视觉上要与同组那两行参数行同缩进同节奏 —— 浅灰凹槽 + 圆角,按钮沿用底部按钮组
               那套白底描边语言,只是把右对齐换成本行右端(flex-end 由 .spp-gs-act 自己给)。 */
            #spp-global-settings-box .spp-gs-act {
                display:flex; align-items:center; justify-content:space-between; gap:10px;
                padding:8px 10px; border-radius:7px; background:var(--spu-overlay,#f2f5f8);
            }
            #spp-global-settings-box .spp-gs-act .spp-gs-acthint {
                flex:1; font-size:11px; line-height:1.55; color:var(--spu-muted,#6b7280);
            }
            #spp-global-settings-box .spp-gs-act button {
                flex-shrink:0; padding:6px 14px; border:1px solid var(--spu-border,#d8dee7);
                border-radius:7px; cursor:pointer; font-size:13px; background:#fff;
                color:var(--spu-text,#1f2937);
                transition:background 0.16s ease, border-color 0.16s ease, transform 0.16s ease;
            }
            #spp-global-settings-box .spp-gs-act button:hover { border-color:var(--spu-accent,#2f6f68); transform:translateY(-1px); }
            #spp-global-settings-box .spp-gs-act button:disabled { opacity:0.55; cursor:default; transform:none; }
            /* ★★ 2.55.11:第五类行 data-type="packs" —— 自备套系粘贴区。
               它是本面板唯一「一格里装多行文本」的行,所以三处都跟别的行反着来:
               ① 行本身从「横排 + 两端对齐」改成**竖排** —— 文本框要占满整幅;
               ② 那一格的 label 取消 flex:1 —— 竖排容器里 flex:1 会把标签**纵向**撑开,
                  在标题与文本框之间凭空拉出一道空白(本面板的 .spp-gs-row label 带 flex:1);
               ③ 光标从 pointer 改回 text —— 这一行没有可勾的东西,pointer 会骗人。 */
            #spp-global-settings-box .spp-gs-row[data-type="packs"] {
                flex-direction:column; align-items:stretch; gap:6px; cursor:default;
            }
            #spp-global-settings-box .spp-gs-row[data-type="packs"] label { flex:0 0 auto; cursor:default; }
            #spp-global-settings-box .spp-packs-ta {
                width:100%; box-sizing:border-box; min-height:150px; resize:vertical;
                padding:6px 8px; border:1px solid var(--spu-border,#d8dee7); border-radius:6px;
                font:12px/1.6 Consolas,"Courier New",monospace;
                color:var(--spu-text,#1f2937); background:#fff; outline:none;
                white-space:pre; overflow:auto;
            }
            #spp-global-settings-box .spp-packs-ta:focus { border-color:var(--spu-accent,#2f6f68); }
            #spp-global-settings-box .spp-packs-fb {
                font-size:11px; line-height:1.6; color:var(--spu-muted,#6b7280); word-break:break-all;
            }
            #spp-global-settings-box .spp-packs-fb b { color:var(--spu-accent,#2f6f68); }
            #spp-global-settings-box .spp-packs-fb .spp-bad { color:#C0392B; }
            /* ★★ 2.55.14:第三格粘贴区(自定义图床)那一块的规则 —— 它写的行短、条数少,
               150px 高会白占版面,收一半;其余样式与自备套系那两格共用。 */
            #spp-global-settings-box .spp-gs-row[data-key="spp_host_extra"] .spp-packs-ta { min-height:96px; }
            /* ★★ 2.55.15(用户:「我建议分为左右两栏:左栏为 emoji,右栏为贴纸自定义。」
               /「案例应该在输入框上方,避免出现输入文本后无法查看案例的情况。」):
               ① 自备表情 / 贴纸拆成左右两格并排;② 示例从 textarea 的 placeholder 挪到输入框
                  **上方**成为常驻小块 —— placeholder 一打字就没了,那正是用户说的「看不到案例」。 */
            #spp-global-settings-box .spp-packs-cols { display:flex; gap:14px; align-items:flex-start; }
            /* flex:1 1 0 + min-width:0:两格等宽,且允许收缩到比内容更窄(只写 flex:1 时
               min-width:auto 会让长说明撑破格宽 —— 与上面 .spp-gs-col 同一个坑)。 */
            #spp-global-settings-box .spp-packs-cols > .spp-gs-row { flex:1 1 0; min-width:0; }
            /* 窄屏叠起来(同 .spp-gs-cols 那一档):面板宽 = min(900px, 100vw-48px),
               两格各 <300px 就没法读了。 */
            @media (max-width:760px) {
                #spp-global-settings-box .spp-packs-cols { flex-direction:column; gap:0; }
            }
            /* ★ 2.55.15:左右并排后每格只剩半幅,输入框收一点高度(十来行够用),
               别把面板撑得太长;仍然拖得高(resize:vertical 在 .spp-packs-ta 上)。 */
            #spp-global-settings-box .spp-packs-cols .spp-packs-ta { min-height:120px; }
            /* ★ 2.55.15:输入框上方的常驻示例。左缘一道强调色竖条 + 等宽小字,与真正的输入框
               区分得开,但读起来仍是「照着写」的语感。★ 必须 word-break:break-all ——
               里面是长 URL,不破词会把这一格顶宽。 */
            #spp-global-settings-box .spp-packs-eg {
                font:11px/1.65 Consolas,"Courier New",monospace;
                color:var(--spu-text,#1f2937);
                background:var(--spu-overlay,#f2f5f8);
                border-left:2px solid var(--spu-accent,#2f6f68);
                border-radius:0 5px 5px 0; padding:6px 9px;
                white-space:pre-wrap; word-break:break-all;
            }
            #spp-global-settings-box .spp-packs-eg .spp-eg-note { color:var(--spu-muted,#6b7280); }
            /* ★★ 2.55.20:第六类行 data-type="heat_json"(茶馆热度配色)。
               三处与别的行反着来,理由与 packs 行同源:
               ① 行本身从「横排 + 两端对齐」改成**竖排** —— 4 个格子要占满整幅;
               ② label 取消 flex:1(竖排容器里它会把标签纵向撑开,凭空拉出一道空白);
               ③ 光标从 pointer 改回 default —— 这一行没有可勾的东西。
               ★ 网格 2×2 而不是一排 4 个:右栏约 400px 宽时,一排 4 格每格只剩约 90px,
                 7 位数的边界值(9999999)会被数字框吃掉尾巴(窄屏叠成一栏时同理)。 */
            #spp-global-settings-box .spp-gs-row[data-type="heat_json"] {
                flex-direction:column; align-items:stretch; gap:8px; cursor:default;
            }
            #spp-global-settings-box .spp-gs-row[data-type="heat_json"] label { flex:0 0 auto; cursor:default; }
            #spp-global-settings-box .spp-heat-grid {
                display:grid; grid-template-columns:repeat(2, minmax(0,1fr)); gap:8px;
            }
            #spp-global-settings-box .spp-heat-cell {
                display:flex; flex-direction:column; gap:5px; padding:7px 9px;
                border:1px solid var(--spu-border,#d8dee7); border-radius:7px;
            }
            #spp-global-settings-box .spp-heat-cap {
                font-size:11px; color:var(--spu-muted,#6b7280); white-space:nowrap;
            }
            #spp-global-settings-box .spp-heat-line { display:flex; align-items:center; gap:6px; }
            /* min-width:0 是必须的:flex 子项默认 min-width:auto,窄屏时数字框不肯收缩,
               会把取色器挤出格子(与 .spp-gs-col / .spp-packs-cols 是同一个坑)。 */
            #spp-global-settings-box .spp-gs-row[data-type="heat_json"] input[type="number"] {
                width:100%; min-width:0; box-sizing:border-box; text-align:center;
            }
            #spp-global-settings-box .spp-gs-row[data-type="heat_json"] input[type="color"] {
                flex-shrink:0; width:36px; height:28px; padding:0; cursor:pointer;
                border:1px solid var(--spu-border,#d8dee7); border-radius:6px; background:#fff;
            }
            /* ★★ 2.52.95:2.52.94 为「开关状态(只读)」组加的这批规则(.spp-gs-note /
               .spp-gs-ro / .spp-gs-ro-val / .spp-gs-span)随该组一起删掉 ——
               组没了,规则留着就是死 CSS(而且 2.52.94 那条 column-span:all 的修法
               本身就是这次外壳重排要根治的东西,见上面 .spp-gs-cols 的注释)。 */
        `);

        // ★★ 2.53.9:布尔行的内置默认值 —— **只从登记表读**,回填处不再抄第二份。
        //   起因:打开面板时的回填原写成 SPP_CONFIG.bool(key, true),把「默认 true」硬编码在了
        //   一个与 SPP_PANEL_DEFS 无关的位置。2.53.9 新增的这枚开关默认是 false,
        //   若沿用那句硬编码就会出现「表里 def:false、打开面板却按 true 回填」的静默不一致
        //   (勾选框显示的与实际生效的相反,而两边都查不出错)。
        function panelBoolDef(key) {
            let d = true;
            SPP_PANEL_DEFS.forEach(g => g.fields.forEach(f => {
                if (f.key === key && f.type === 'bool') d = !!f.def;
            }));
            return d;
        }

        /* ★★ 2.55.11:粘贴区那两行下面的「小结」;★★ 2.55.14 起这一格变两格
           (自备套系 / 自定义图床),所以本函数改成**遍历所有 packs 行、按 data-key 分流**。
           小结不改任何东西、也不参与保存 —— 只是把解析结果摊开给人看。
           用的是与保存后那条路径**同一个**解析器 ⇒ 这里说 2 套 137 张,
           刷新后表情栏里就真的是 2 套 137 张(不会出现「面板说有、栏里没有」)。
           ★ 套系名 / 图床名 / 认链域名都是用户自己敲的 ⇒ 插进 innerHTML 前先转义
             (本项目其余面板文案都是写死的常量,只有这几处会碰到外来文本)。 */
        function sppEscHtml(v) {
            return String(v).replace(/[&<>"]/g, function (c) {
                return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c];
            });
        }
        function renderCustomPacksFeedback(ta, fb, key) {
            const esc = sppEscHtml;
            /* ★★ 2.55.15:这一格默认落哪一页由 data-key 定(左格 emoji / 右格 贴纸)。
               ★ 带 [emoji] / [贴纸] 前缀改道的套系照旧算进对面那一页 —— 小结会显示出来。 */
            const isEmoji = key === SPP_CUSTOM_EMOJI_KEY;
            const r = sppParseCustomPacks(ta.value, isEmoji ? 'emoji' : 'sticker');
            if (r.empty) {
                fb.innerHTML = isEmoji
                    ? '这一格留空 = emoji 页只用脚本自带的那几套,不影响任何东西。'
                    : '这一格留空 = 贴纸页只用脚本自带的那几套,不影响任何东西。';
                return;
            }
            const parts = [];
            if (r.packs) {
                parts.push('解析出 <b>' + r.packs + '</b> 套 / <b>' + r.urls + '</b> 张');
                const seg = [];
                if (r.emoji.length) seg.push('emoji 页 ' + r.emoji.length + ' 套(' + r.emoji.map(p => esc(p.name)).join('、') + ')');
                if (r.sticker.length) seg.push('贴纸页 ' + r.sticker.length + ' 套(' + r.sticker.map(p => esc(p.name)).join('、') + ')');
                if (seg.length) parts.push(seg.join(';'));
                const named = r.emoji.concat(r.sticker).reduce(function (n, p) {
                    return n + p.list.filter(function (e) { return e.label && e.label !== e.url; }).length;
                }, 0);
                if (named) parts.push('其中 <b>' + named + '</b> 张带自定义提示文本');
            } else {
                parts.push('没解析出可用的套系');
            }
            if (r.bad.length) {
                const head = r.bad.slice(0, 6).map(b => '第 ' + b.n + ' 行(' + b.why + ')').join('、');
                parts.push('<span class="spp-bad">忽略 ' + r.bad.length + ' 行:' + head +
                    (r.bad.length > 6 ? ' 等' : '') + '</span>');
            }
            parts.push(isEmoji ? '点「保存并刷新」后出现在 emoji 栏' : '点「保存并刷新」后出现在贴纸栏');
            fb.innerHTML = parts.join(' · ');
        }
        function renderCustomHostsFeedback(ta, fb) {
            const esc = sppEscHtml;
            const r = sppParseCustomHosts(ta.value);
            if (r.empty) {
                fb.innerHTML = '这一格留空 = 「图床 ▾」里还是脚本自带的六个,不影响任何东西。';
                return;
            }
            const parts = [];
            if (r.list.length) {
                parts.push('解析出 <b>' + r.list.length + '</b> 个图床:' +
                    r.list.map(function (h) {
                        return esc(h.name) + '(' + esc(h.hosts.join(' / ')) + ')';
                    }).join('、'));
            } else {
                parts.push('没解析出可用的图床');
            }
            if (r.bad.length) {
                const head = r.bad.slice(0, 6).map(b => '第 ' + b.n + ' 行(' + b.why + ')').join('、');
                parts.push('<span class="spp-bad">忽略 ' + r.bad.length + ' 行:' + head +
                    (r.bad.length > 6 ? ' 等' : '') + '</span>');
            }
            parts.push('点「保存并刷新」后出现在「图床 ▾」下拉里,标注时也认这些域名');
            fb.innerHTML = parts.join(' · ');
        }
        function renderPacksFeedback(root) {
            root.querySelectorAll('.spp-gs-row[data-type="packs"]').forEach(function (row) {
                const ta = row.querySelector('.spp-packs-ta');
                const fb = row.querySelector('.spp-packs-fb');
                if (!ta || !fb) return;
                // ★ 2.55.15:自备套系那一格已拆成两格(左 emoji / 右 贴纸)—— 分流时把
                //   data-key 一并交给它,那一格才知道自己的默认归属(第三格仍是图床)。
                if (row.dataset.key === SPP_HOST_KEY) renderCustomHostsFeedback(ta, fb);
                else renderCustomPacksFeedback(ta, fb, row.dataset.key);
            });
        }

        // 构建设置面板 DOM(懒加载)
        function buildOverlay() {            const overlay = document.createElement('div');
            overlay.id = 'spp-global-settings-overlay';

            let html = '<div id="spp-global-settings-box"><h3>脚本设置</h3>';
            /* ★★ 2.53.57(用户:「再排版一下」+三行蓝框 / 「移动」箭头):左栏末尾还要接三枚开关,
               但它们的代码在右栏那一段构建(源码顺序靠后)—— 所以先攒进 leftTail,
               到收尾处再用占位符换进左栏。★ 收尾前替换,保证 innerHTML 赋给 overlay 之前就到位
               (行点击、保存循环都是赋完值再绑的,顺序错了会漏绑那三行)。 */
            let leftTail = '';
            // ★★ 2.52.95:两栏容器从这里开 —— 它是整个面板唯一的 columns 元素。
            //   标题与按钮组都在它【外面】,所以不需要任何 column-span(见 CSS 注释 / 经验.md 1.40)。
            /* ★★ 2.53.56:两栏容器里现在是**两个真正的列**(.spp-gs-col),不再是多栏、也不是网格 ——
               理由见 CSS 里那段注释(网格会把「跨两行的组」的高度摊到每一行上)。 */
            html += '<div class="spp-gs-cols"><div class="spp-gs-col">';

            // 茶纸是唯一保留主题;旧主题值在 migrateSettings() 中自动归一。

            // ★★ 2.52.95:本组连同它的「卡片范围」下拉框一起从面板撤掉 ——
            //   用户 2.52.95 定案:「这两块不该出现在面板里」。
            //   ★ 2.53.62:那个范围随后连 Userscript 菜单入口也一起撤了,范围写死 'cm'
            //     (见模块 16 里调用点上方那段注释)。**面板里始终没有它**,这一条不变。
            //   ★ 历史:本组原有 14 个布尔开关,2.52.64 起【整体不再渲染】(用户定案
            //     「既然灰色且无法勾选,那就隐藏不显示」),当时就只剩这 1 个真参数。

            // ==================== 2.51.0:版块页顶部 ====================
            // ★ 2.52.64:本组的 3 个布尔开关(屏蔽顶部横幅 / 用户信息深色卡 / 右格热门版块)
            //   已整体不渲染 —— 组内没有参数,所以整组一起撤掉,不留空标题。
            //   这些开关的读取与生效逻辑仍在各自模块里,一字未改。

            // ★ 2.52.64:星空背景组的 2 个布尔开关同样整体不渲染(组内无参数,整组撤掉)。


            let imgSettings = SPP_CONFIG.imgSettings();   // ★ 2.52.93 起走集中读数层(GM 存储 → 内置默认)
            html += '<div class="spp-gs-group"><div class="spp-gs-group-title">图片设置 (仅帖子页)</div>';
            html += `<div class="spp-gs-row" data-key="maxImgHeight" data-type="img_json">
                <label>图片最大高度</label>
                <span style="display:flex;align-items:center;gap:6px;">
                    <input type="range" min="30" max="100" step="5" value="${imgSettings.maxImgHeight}" style="width:90px;">
                    <span class="spp-val">${imgSettings.maxImgHeight}vh</span>
                </span>
            </div>`;
            html += `<div class="spp-gs-row" data-key="maxImgWidth" data-type="img_json">
                <label>图片最大宽度</label>
                <span style="display:flex;align-items:center;gap:6px;">
                    <input type="range" min="20" max="100" step="5" value="${imgSettings.maxImgWidth}" style="width:90px;">
                    <span class="spp-val">${imgSettings.maxImgWidth}%</span>
                </span>
            </div>`;
            html += `<div class="spp-gs-row" data-key="thumbSize" data-type="img_json">
                <label>缩略图尺寸</label>
                <span style="display:flex;align-items:center;gap:6px;">
                    <input type="range" min="32" max="128" step="8" value="${imgSettings.thumbSize}" style="width:90px;">
                    <span class="spp-val">${imgSettings.thumbSize}px</span>
                </span>
            </div>`;
            html += `<div class="spp-gs-row" data-key="centerImg" data-type="img_json">
                <label>图片居中显示</label>
                <input type="checkbox" ${imgSettings.centerImg ? 'checked' : ''}>
            </div>`;
            html += `<div class="spp-gs-row" data-key="wheelAction" data-type="img_json">
                <label>灯箱滚轮行为</label>
                <select style="padding:4px 8px;border:1px solid #e8e3d8;border-radius:4px;font-size:12px;background:#fff;color:#2D2318;cursor:pointer;">
                    <option value="switch" ${imgSettings.wheelAction === 'switch' ? 'selected' : ''}>切换图片</option>
                    <option value="zoom" ${imgSettings.wheelAction === 'zoom' ? 'selected' : ''}>缩放图片</option>
                </select>
            </div>`;
            html += '</div>';

            // ★★ 2.53.56:数值设置组**搬到左栏**(原来排在 DOM 末尾,被多栏装进了右栏)。
            //   左栏 = 图片设置 + 数值设置(两组参数),右栏 = 功能开关(那组开关)——
            //   两栏各自独立堆叠,谁高谁矮都不影响对方的行距。
            // ★ 2.52.94:删掉了 2.52.93 加的两行「面板值优先:Tampermonkey ▸ 脚本详情 ▸ 设置…」提示 ——
            //   那句话指向一个不存在的面板(篡改猴不解析 ==UserConfig==,见 经验.md 1.38)。
            //   本页面板就是这个脚本唯一的设置入口,所以不需要任何「谁优先」的说明。
            html += '<div class="spp-gs-group"><div class="spp-gs-group-title">数值设置</div>';
            const pageWidth = SPP_CONFIG.num('spp_page_width', 1400, 1200, 1600);
            html += `<div class="spp-gs-row" data-key="spp_page_width" data-type="number">
                <label>页面宽度 (px)<span class="spp-gs-hint">可填 1200-1600(基准 1400),只改容器宽度</span></label>
                <input type="number" value="${pageWidth}" min="1200" max="1600" step="10">
            </div>`;
            const navStep = SPP_CONFIG.num('spp_nav_step', 1, 1, 50);
            html += `<div class="spp-gs-row" data-key="spp_nav_step" data-type="number">
                <label>翻页步长<span class="spp-gs-hint">悬浮按钮每次点击翻几页(1 = 逐页翻)。范围 1-50</span></label>
                <input type="number" value="${navStep}" min="1" max="50" step="1">
            </div>`;
            // ★★ 2.53.47:第三枚 number 行 ——「正文文字大小」。
            //   读数点与 SPP_PANEL_DEFS 的 def/min/max 必须逐字同值,守卫拿两处对着核。
            const fontSize = SPP_CONFIG.num('spp_font_size', 14, 12, 18);
            html += `<div class="spp-gs-row" data-key="spp_font_size" data-type="number">
                <label>正文文字大小 (px)<span class="spp-gs-hint">12 = 小、14 = 中(站点默认)、18 = 大。改完刷新后,所有楼层的正文一起变;上方那三枚「小 中 大」按钮会被收掉</span></label>
                <input type="number" value="${fontSize}" min="12" max="18" step="3">
            </div>`;
            /* ★★ 2.53.95:第四枚 number 行 —— 索引面板每页行数。读数点与 SPP_PANEL_DEFS 的默认值
               / 区间必须逐字同值(tools/verify/settings-panel.js 拿两处对着核)。 */
            const idxPer = SPP_CONFIG.num('spp_idx_per', 15, 5, 60);
            html += `<div class="spp-gs-row" data-key="spp_idx_per" data-type="number">
                <label>索引面板每页行数<span class="spp-gs-hint">含图 / 链接两栏每页显示多少行(5-60)。超过一页时面板底部出现翻页条;填大一点就能一栏看全。默认 15</span></label>
                <input type="number" value="${idxPer}" min="5" max="60" step="5">
            </div>`;
            html += '</div>';
            /* ★ 2.53.56:左栏到此收口、右栏开始。按钮组在后面、**两栏之外**。 */
            /* ★ 2.53.57:左栏末尾的占位符 —— 三枚开关的 HTML 在下面(右栏那段)才构建出来。 */
            html += '<!--SPP-LEFT-TAIL-->';
            html += '</div><div class="spp-gs-col">';

            // 功能开关组
            // ★★ 2.52.64:这 6 个布尔开关(SETTINGS_DEFS)已在菜单与面板两处全部撤掉显示。
            //   SETTINGS_DEFS 本身【保留】:它是这 6 个键名唯一的登记处,
            //   tools/build/build-structure-doc.js 的「八、设置项」与「必须归零」类断言都从它抽键名。
            //   删掉这个常量会让文档生成器的 declaredSettings === 0 守卫直接拦下(这是有意的)。

            // 功能开关组(★ 2.53.0 新增)
            // ★ 这是面板自 2.52.64 撤掉布尔行之后的第一枚**可勾选**开关:用户本轮明确要求
            //   「增加一个功能,默认开启,在设置面板里可以关闭」。行的结构与 img_json 行一致
            //   (.spp-gs-row + data-key + data-type),只是 type 用 bool。
            const autoExpand = SPP_CONFIG.bool('spp_auto_expand_hidden', true);
            html += '<div class="spp-gs-group"><div class="spp-gs-group-title">功能开关</div>';
            html += `<div class="spp-gs-row" data-key="spp_auto_expand_hidden" data-type="bool">
                <label>自动展开隐藏楼层<span class="spp-gs-hint">纯 mark 水帖这类被站点折叠的楼层内容直接展开,不再留「▼展开」按钮</span></label>
                <input type="checkbox" ${autoExpand ? 'checked' : ''}>
            </div>`;
            // ★★ 2.53.9:第二枚可勾选布尔行(默认关闭)。结构与本组第一行逐字一致 ——
            //   面板自检按 data-key/data-type 比对 SPP_PANEL_DEFS,格式差一点就会报「多 [-]」。
            const avaInit = SPP_CONFIG.bool('spp_hide_default_avatar', false);
            /* ★ 2.53.57:本行按用户标注搬到左栏(见 leftTail 的说明)—— 只改累加目标,行本身一字未动。 */
            leftTail += `<div class="spp-gs-row" data-key="spp_hide_default_avatar" data-type="bool">
                <label>用户名 + 彩色背景头像<span class="spp-gs-hint">没有图床(或头像链接已失效)的用户,用「用户名 + 彩色渐变块」替换站点默认头像。默认关闭</span></label>
                <input type="checkbox" ${avaInit ? 'checked' : ''}>
            </div>`;
            // ★★ 2.53.68:两枚并一枚(见 SPP_PANEL_DEFS 里那条三代账)—— 渲染行随之只留这一条,
            //   语义就是用户要的:勾选 = 常显,不勾选 = 收起成侧边一条。
            //   ★ 再提醒一次这份**手写 HTML**:本函数不遍历 SPP_PANEL_DEFS,新开关必须在两张表里同时登记,
            //     否则 tools/verify/settings-panel.js 当场报「SPP_PANEL_DEFS 登记了但面板没渲染」(2.53.32 就栽在这)。
            const iiShow = SPP_CONFIG.bool('spp_imgidx_show', false);
            /* ★★ 2.55.5:按用户标注搬到左栏(同一批三枚:含图楼层悬浮窗 / 表情名称提示 /
               自动索引全贴)—— 只改累加目标(html → leftTail),行本身一字未动。
               做法与理由见 leftTail 的说明(2.53.57 那笔账)。 */
            leftTail += `<div class="spp-gs-row" data-key="spp_imgidx_show" data-type="bool">
                <label>含图楼层悬浮窗<span class="spp-gs-hint">勾选:面板常显。不勾选:收起成侧边一条,只留当前那一枚页签(鼠标移上去滑回来)。默认关闭</span></label>
                <input type="checkbox" ${iiShow ? 'checked' : ''}>
            </div>`;
            /* ★★ 2.53.96:第十五枚可勾选布尔行 ——「图床链接自动标注」。
               ★★ 2.55.13:默认值 true → false(那一行默认值的说明写在登记表那一侧)。
               又一次提醒这份**手写 HTML**(连同上面那几条老提醒):本函数不遍历
               SPP_PANEL_DEFS,新开关必须在两张表里同时登记,否则
               tools/verify/settings-panel.js 当场报「SPP_PANEL_DEFS 登记了但面板没渲染」。 */
            const hostMark = SPP_CONFIG.bool('spp_host_mark', false);
            html += `<div class="spp-gs-row" data-key="spp_host_mark" data-type="bool">
                <label>链接自动标注<span class="spp-gs-hint">作用于文本输入框,勾选后会自动将输入框内的所有超链接按图床格式标签进行标注</span></label>
                <input type="checkbox" ${hostMark ? 'checked' : ''}>
            </div>`;
            /* ★★ 2.53.97:第十六枚可勾选布尔行 ——「表情名称提示」。
               ★★ 2.55.14:默认值 false → true(那一行默认值的说明写在登记表那一侧)。
               又一次提醒这份**手写 HTML**(连同上面那几条老提醒):本函数不遍历
               SPP_PANEL_DEFS,新开关必须在两张表里同时登记,否则
               tools/verify/settings-panel.js 当场报「SPP_PANEL_DEFS 登记了但面板没渲染」。 */
            const emojiTip = SPP_CONFIG.bool('spp_emoji_tip', true);
            /* ★★ 2.55.5:按用户标注搬到左栏(同一批三枚:含图楼层悬浮窗 / 表情名称提示 /
               自动索引全贴)—— 只改累加目标(html → leftTail),行本身一字未动。
               做法与理由见 leftTail 的说明(2.53.57 那笔账)。 */
            leftTail += `<div class="spp-gs-row" data-key="spp_emoji_tip" data-type="bool">
                <label>表情名称提示<span class="spp-gs-hint">鼠标悬停在表情上会有文本提示,但由于确定表情包对应的文本工作量较大,该功能基本无用</span></label>
                <input type="checkbox" ${emojiTip ? 'checked' : ''}>
            </div>`;
            /* ★★ 2.55.9:第十八枚可勾选布尔行(**默认关闭**)——「贴纸居中显示」。
               又一次提醒这份**手写 HTML**(连同上面那几条老提醒):本函数不遍历
               SPP_PANEL_DEFS,新开关必须在两张表里同时登记,否则
               tools/verify/settings-panel.js 当场报「SPP_PANEL_DEFS 登记了但面板没渲染」。 */
            const stickerCenter = SPP_CONFIG.bool('spp_sticker_center', false);
            leftTail += `<div class="spp-gs-row" data-key="spp_sticker_center" data-type="bool">
                <label>贴纸居中显示<span class="spp-gs-hint">只管脚本贴进帖子里的表情 / 贴纸(含自带的五套与滑稽那套):勾选 = 居中;不勾选 = 保持行内原样。与上面「图片居中显示」互不干扰 —— 那一条不再管表情 / 贴纸。默认关闭</span></label>
                <input type="checkbox" ${stickerCenter ? 'checked' : ''}>
            </div>`;
            /* ★★ 2.55.13:「图片链接转 [img]」这一行**整行撤掉** —— 功能改成硬性常开,
               面板上不再露脸(撤销理由与两侧必须同步的要求写在登记表那一侧)。 */
            const idxAuto = SPP_CONFIG.bool('spp_idx_auto', false);
            /* ★★ 2.55.5:按用户标注搬到左栏(同一批三枚:含图楼层悬浮窗 / 表情名称提示 /
               自动索引全贴)—— 只改累加目标(html → leftTail),行本身一字未动。
               做法与理由见 leftTail 的说明(2.53.57 那笔账)。 */
            leftTail += `<div class="spp-gs-row" data-key="spp_idx_auto" data-type="bool">
                <label>打开帖子自动索引全贴(含图 / 链接跨页定位)<span class="spp-gs-hint">勾选:打开帖子后自动开始全贴索引(先吃缓存、缺的页才逐页抓)并切到全贴视图,抓的过程可在面板里随时停。不勾选:一切照旧,需要点面板底部那枚「全贴索引」才开始。默认关闭</span></label>
                <input type="checkbox" ${idxAuto ? 'checked' : ''}>
            </div>`;
            const wfFix = SPP_CONFIG.bool('spp_fix_white_text', true);
            html += `<div class="spp-gs-row" data-key="spp_fix_white_text" data-type="bool">
                <label>白底白字转加粗深色<span class="spp-gs-hint">发帖人用白色字体写的字在白底正文上完全看不见;自动转成加粗的深色字。默认开启</span></label>
                <input type="checkbox" ${wfFix ? 'checked' : ''}>
            </div>`;
            // ★★ 2.53.39:第六枚可勾选布尔行(默认开启)——「免费帖自动购买」。
            //   再次提醒这份**手写 HTML**:本函数不遍历 SPP_PANEL_DEFS,新开关必须在两张表里同时登记,
            //   否则 tools/verify/settings-panel.js 当场报「SPP_PANEL_DEFS 登记了但面板没渲染」。
            const autobuy = SPP_CONFIG.bool('spp_autobuy_free', true);
            html += `<div class="spp-gs-row" data-key="spp_autobuy_free" data-type="bool">
                <label>免费帖自动购买<span class="spp-gs-hint">打开帖子若售价为 0 SP币 且还没买过,自动买下 —— 走后台请求,页面不跳转。收费帖一律手动</span></label>
                <input type="checkbox" ${autobuy ? 'checked' : ''}>
            </div>`;
            // ★★ 2.53.41:第七枚可勾选布尔行(默认开启)——「提亮已购正文」。
            //   又一次提醒这份**手写 HTML**:本函数不遍历 SPP_PANEL_DEFS,新开关必须在两张表里
            //   同时登记,否则 tools/verify/settings-panel.js 当场报「登记了但面板没渲染」。
            const buyLift = SPP_CONFIG.bool('spp_buy_lift', true);
            html += `<div class="spp-gs-row" data-key="spp_buy_lift" data-type="bool">
                <label>提亮已购正文<span class="spp-gs-hint">买下之后被解锁的正文,站点原本按引用块显示(12px、偏褐);打开后按正常正文显示(13px、深灰)。默认开启</span></label>
                <input type="checkbox" ${buyLift ? 'checked' : ''}>
            </div>`;
            // ★★ 2.53.45:第八枚可勾选布尔行(**默认关闭**)——「悬停高亮」。
            //   又一次提醒这份**手写 HTML**(连同上面那几条老提醒):本函数不遍历
            //   SPP_PANEL_DEFS,新开关必须在两张表里同时登记,否则
            //   tools/verify/settings-panel.js 当场报「SPP_PANEL_DEFS 登记了但面板没渲染」。
            const floorHover = SPP_CONFIG.bool('spp_floor_hover', false);
            /* ★ 2.53.57:按用户标注搬到左栏。 */
            leftTail += `<div class="spp-gs-row" data-key="spp_floor_hover" data-type="bool">
                <label>悬停高亮<span class="spp-gs-hint">鼠标停在某一楼(或其它页面的表格行)上时整行刷一层淡底。默认关闭 —— 关掉后鼠标划过不上色</span></label>
                <input type="checkbox" ${floorHover ? 'checked' : ''}>
            </div>`;
            // ★★ 2.53.47:第九枚可勾选布尔行(**默认关闭**)——「翻页悬浮框靠边自动隐藏」。
            //   又一次提醒这份**手写 HTML**(连同上面那几条老提醒):本函数不遍历
            //   SPP_PANEL_DEFS,新开关必须在两张表里同时登记,否则
            //   tools/verify/settings-panel.js 当场报「SPP_PANEL_DEFS 登记了但面板没渲染」。
            // ★★ 2.55.25:第十九枚可勾选布尔行(**默认关闭**)——「帖子区黑玻璃(暗色)」。
            //   又一次提醒这份**手写 HTML**(连同上面那几条老提醒):本函数不遍历
            //   SPP_PANEL_DEFS,新开关必须在两张表里同时登记,否则
            //   tools/verify/settings-panel.js 当场报「SPP_PANEL_DEFS 登记了但面板没渲染」。
            //   ★ 放左栏:它是纯视觉开关(同上面那几枚),跟着左栏尾走。
            const readDark = SPP_CONFIG.bool('spp_read_dark', false);
            leftTail += `<div class="spp-gs-row" data-key="spp_read_dark" data-type="bool">
                <label>黑夜模式<span class="spp-gs-hint">进入黑夜模式即可</span></label>
                <input type="checkbox" ${readDark ? 'checked' : ''}>
            </div>`;
            const navEdge = SPP_CONFIG.bool('spp_nav_edge', false);
            /* ★ 2.53.57:按用户标注搬到左栏(三枚里最后一枚)。 */
            leftTail += `<div class="spp-gs-row" data-key="spp_nav_edge" data-type="bool">
                <label>翻页悬浮框靠边自动隐藏<span class="spp-gs-hint">右侧那块翻页悬浮框贴到屏幕右缘时自动缩进去、只留一条窄边;鼠标移上去自动滑回来。默认关闭</span></label>
                <input type="checkbox" ${navEdge ? 'checked' : ''}>
            </div>`;
            // ★★ 2.55.4:第十七枚可勾选布尔行(**默认开启**)——「社区论坛任务自动领取」。
            //   又一次提醒这份**手写 HTML**(连同上面那几条老提醒):本函数不遍历
            //   SPP_PANEL_DEFS,新开关必须在两张表里同时登记 —— 好在 2.52.94 起面板
            //   自带一致性自检(缺/多都会记进 SPP_INIT.fail),漏一处当场就能看见。
            //   ★ 放左栏:左栏 577px 比右栏 773px 短一截(2.53.57 的账),这行接在左栏尾。
            const taskAuto = SPP_CONFIG.bool('spp_task_autoclaim', true);
            leftTail += `<div class="spp-gs-row" data-key="spp_task_autoclaim" data-type="bool">
                <label>社区论坛任务自动领取<span class="spp-gs-hint">打开任意南+页面时,脚本在后台把社区任务里「可领取」的奖励领掉(先申请、再领取),页面不跳转;同一个任务领到过一次后 24 小时内不再碰它。默认开启</span></label>
                <input type="checkbox" ${taskAuto ? 'checked' : ''}>
            </div>`;
            // ★★ 2.53.55:第十枚可勾选布尔行(**默认开启**)——「翻页双箭头(首页 / 末页)」。
            //   又一次提醒这份**手写 HTML**(连同上面那几条老提醒):本函数不遍历
            //   SPP_PANEL_DEFS,新开关必须在两张表里同时登记,否则
            //   tools/verify/settings-panel.js 当场报「SPP_PANEL_DEFS 登记了但面板没渲染」。
            const pgEnds = SPP_CONFIG.bool('spp_pg_ends', true);
            html += `<div class="spp-gs-row" data-key="spp_pg_ends" data-type="bool">
                <label>翻页双箭头(首页 / 末页)<span class="spp-gs-hint">翻页条两端各补一枚 « / » 双箭头:点一下直接到第 1 页 / 最后一页。默认开启 —— 关掉后只剩 ‹ › 单箭头(上一页 / 下一页)</span></label>
                <input type="checkbox" ${pgEnds ? 'checked' : ''}>
            </div>`;
            // ★★ 2.53.58:面板第十一枚可写行 ——「搜索结果排序方式」(把站点那个表单下拉框的默认值挪过来)。
            // ★★ 2.53.59:它从 bool 升级成**下拉框**(本项目第四类行 data-type="select")——
            //   用户要求把排序这件事整个搬进面板(页面上不再留控件)。**五处要一起改**:
            //   ① 本处渲染;② 面板自检那个选择器;③ 保存分支;④ openSettingsPanel 里的回填;
            //   ⑤ tools/verify/settings-panel.js 的 EXPECT 表。漏一处守卫当场报。
            //   ★ 这份 HTML 依旧是**手写**的:本函数不遍历 SPP_PANEL_DEFS。
            const searchOrderNow = searchOrder();
            html += `<div class="spp-gs-row" data-key="spp_search_order" data-type="select">
                <label>搜索结果排序方式<span class="spp-gs-hint">搜索表单的「排序方式」默认选中这一种(站点原版默认是「发帖时间」)。只改表单默认值,不动地址上已经写明的那一种。默认:人气</span></label>
                <select style="padding:4px 8px;border:1px solid #e8e3d8;border-radius:4px;font-size:12px;background:#fff;color:#2D2318;cursor:pointer;">
                    <option value="hits" ${searchOrderNow === 'hits' ? 'selected' : ''}>人气</option>
                    <option value="replies" ${searchOrderNow === 'replies' ? 'selected' : ''}>回复</option>
                    <option value="postdate" ${searchOrderNow === 'postdate' ? 'selected' : ''}>发帖时间</option>
                    <option value="lastpost" ${searchOrderNow === 'lastpost' ? 'selected' : ''}>最后回复</option>
                </select>
            </div>`;
            // ★★ 2.53.60:面板第十二枚可勾选布尔行(**默认开启**)——「编辑器实时预览」。
            //   又一次提醒这份**手写 HTML**(连同上面那几条老提醒):本函数不遍历
            //   SPP_PANEL_DEFS,新开关必须在两张表里同时登记,否则
            //   tools/verify/settings-panel.js 当场报「SPP_PANEL_DEFS 登记了但面板没渲染」。
            const previewLive = SPP_CONFIG.bool('spp_preview_live', true);
            // ★★ 2.53.72:第十三枚可勾选布尔行(默认开启)——「南+ 品牌条」。
            //   又一次提醒这份**手写 HTML**(连同上面那几条老提醒):本函数不遍历
            //   SPP_PANEL_DEFS,新开关必须在两张表里同时登记,否则
            //   tools/verify/settings-panel.js 当场报「SPP_PANEL_DEFS 登记了但面板没渲染」。
            //   ★ 这行刻意用字符串拼接、不用模板串:本文件里那两处模板串已经够多,
            //     少一层嵌套就少一处 \${ 转义的地雷(本行的 \${ 一个也不需要)。
            const brandOn = SPP_CONFIG.bool('spp_brand_bar', true);
            html += '<div class="spp-gs-row" data-key="spp_brand_bar" data-type="bool">' +
                '<label>南+ 品牌条<span class="spp-gs-hint">把页眉那两块淘宝广告图摘掉跳转、重排成一条品牌条:两图分列左右两端,中间是「上方三个描边图标 + 下方扫光的 South-Plus」(鼠标移上去看扫光)。默认开启</span></label>' +
                '<input type="checkbox" ' + (brandOn ? 'checked' : '') + '>' +
                '</div>';
            html += `<div class="spp-gs-row" data-key="spp_preview_live" data-type="bool">
                <label>编辑器实时预览<span class="spp-gs-hint">输入框下方常驻一块预览,边打边渲染(图片 / 表情 / 引用块都算)。默认开启 —— 关掉后回到点一次「预览」渲染一次的旧做法</span></label>
                <input type="checkbox" ${previewLive ? 'checked' : ''}>
            </div>`;
            // ★★ 2.53.9(用户:「增加一个功能,用于恢复用户的默认头像」):
            //   这是一枚**动作**行,不是参数行 —— 所以它【没有 data-key、也没有 data-type】:
            //     · 面板自检只收 data-type 为 img_json / number / bool 的行 ⇒ 不会判它「多」;
            //     · 保存循环只按 data-type 分支 ⇒ 不会碰它、也不会因为取不到控件而报错。
            //   刻意不给它 data-type="action":那需要同时改自检的选择器、保存分支与守卫的类型表,
            //   等于为了一枚按钮新开一类行;不给类型就没有任何消费点需要知道它存在。
            html += `<div class="spp-gs-act">
                <span class="spp-gs-acthint">把页面上已经替换成彩色块的头像立刻还原成站点原样,并关闭上面的开关</span>
                <button type="button" class="spp-gs-actbtn">恢复默认头像</button>
            </div>`;
            html += '</div>';
            /* ★★ 2.55.1:资源区「已读记录」组。四行参数 + 一枚动作按钮,逐条对应用户这一轮的话 ——
               · 开关(默认开)=「该功能可以在设置的悬浮面板里关闭,默认是开启的」;
               · 停留秒数(默认 5)=「在设置面板中增加一个 UI 框,用于设定页面停留时长…默认数值
                 为 5秒。可修改」;
               · 清除阈值 + 单位=「允许用户清除指定时间之前的数据,例如一小时或一千小时。
                 它有一个计量单位,可以设置为小时,也可以设置为日期」;
               · 「清除记录」动作按钮=同上一句,点完就地重绘(处理函数在下面)。
               ★ 行类型全部复用面板**已有**的四类(bool / number / select),不新开 data-type:
                 新增一类要同步改本函数四处 + 守卫的类型表,而这里没有一处需要它。
               ★ 按钮【不给 data-key、也不给 data-type】—— 与「恢复默认头像」同一套做法:
                 面板自检只收带 data-type 的行,保存循环只按 data-type 分支,都不认识它。
               ★ 它的类名特意【不叫】.spp-gs-actbtn:那枚是按 overlay.querySelector(取第一个)
                 找的,两枚动作按钮共用一个类名会让「恢复默认头像」的处理函数挂到先出现的那一枚上。
               ★ 文案一律字符串拼接、不用模板串(同 2.53.72 的品牌条那行):本文件里模板串已经够多,
                 少一层嵌套就少一处模板串转义的地雷。 */
            const readMarkOn = SPP_CONFIG.bool('spp_read_mark', true);
            const readDwell  = SPP_CONFIG.num('spp_read_dwell', 5, 0, 600);
            const readPurgeN = SPP_CONFIG.num('spp_read_purge', 24, 1, 100000);
            const readPurgeU = SPP_CONFIG.pick('spp_read_purge_unit', 'hour', READ_PURGE_UNITS);
            html += '<div class="spp-gs-group"><div class="spp-gs-group-title">已读记录 (仅资源区)</div>';
            html += '<div class="spp-gs-row" data-key="spp_read_mark" data-type="bool">' +
                '<label>资源区已读标记<span class="spp-gs-hint">打开资源区的帖子并停留够下面的秒数,该帖在资源区列表的卡片底部就会挂上一枚「已读」标识。茶馆不生效。默认开启</span></label>' +
                '<input type="checkbox" ' + (readMarkOn ? 'checked' : '') + '>' +
                '</div>';
            html += '<div class="spp-gs-row" data-key="spp_read_dwell" data-type="number">' +
                '<label>判定已读的停留秒数<span class="spp-gs-hint">帖子页实际停留(只算页面可见的时间)达到这个秒数才算「已读」,可填 0-600。默认 5 秒;填 0 = 打开即算</span></label>' +
                '<input type="number" value="' + readDwell + '" min="0" max="600" step="1">' +
                '</div>';
            html += '<div class="spp-gs-row" data-key="spp_read_purge" data-type="number">' +
                '<label>清除早于 (小时 / 天)<span class="spp-gs-hint">「清除记录」按这个数清理更早的已读记录,可填 1-100000。默认 24</span></label>' +
                '<input type="number" value="' + readPurgeN + '" min="1" max="100000" step="1">' +
                '</div>';
            html += '<div class="spp-gs-row" data-key="spp_read_purge_unit" data-type="select">' +
                '<label>清除的时间单位<span class="spp-gs-hint">上面那个数的单位:小时或天。默认小时</span></label>' +
                '<select style="padding:4px 8px;border:1px solid #e8e3d8;border-radius:4px;font-size:12px;background:#fff;color:#2D2318;cursor:pointer;">' +
                '<option value="hour"' + (readPurgeU === 'hour' ? ' selected' : '') + '>小时</option>' +
                '<option value="day"' + (readPurgeU === 'day' ? ' selected' : '') + '>天</option>' +
                '</select>' +
                '</div>';
            html += '<div class="spp-gs-act">' +
                '<span class="spp-gs-acthint">左:按上面那条阈值清掉更早的记录;右:清空全部记录(两万条一起没,点两次才真的清)。当前共 <span class="spp-read-count">0</span> 条;清完本页的「已读」标识立刻同步,不需要刷新</span>' +
                '<button type="button" class="spp-read-clear-btn">清除记录</button>' +
                '<button type="button" class="spp-read-wipe-btn">清除全部记录</button>' +
                '</div>';
            html += '</div>';

            /* ★ 2.53.56:右栏到此收口(数值设置那一段已搬到左栏,见上)。 */
            html += '</div></div>';

            /* ★★ 2.55.11:自备套系粘贴区。刻意放在**两栏之外的整幅**位置(与按钮组同一层):
               ① 粘的是多行文本,塞进半幅的列里连一行 URL 都显示不全;
               ② 塞进任意一列都会把那一列拉高一截 —— 用户 2.53.56 / 2.53.57 特意要过
                  「左右对齐」,不能为了这一格把那份对齐又拆了。
               ★ 这一行的初值不在这里写死:文本框由下面那段绑定 / openSettingsPanel
                 的刷新按 GM 存储回填(与 bool / number / select 同口径)。
               ★ 全程字符串拼接、不用模板串(同品牌条那行):这段里有 ${} 形状的字面量,
                 模板串里会踩转义的坑。
               ★★ 2.55.15(用户:「我建议分为左右两栏:左栏为 emoji,右栏为贴纸自定义。」
                 /「案例应该在输入框上方,避免出现输入文本后无法查看案例的情况。建议优化
                 案例文本,使其更易于理解,最好能举例说明。」):
                 本组头两格改成**左右并排**(.spp-packs-cols 包住两枚 packs 行),每格在
                 输入框**上方**多一块常驻示例(.spp-packs-eg)—— 示例原来只靠 placeholder 给,
                 一打字就看不见了,用户要的正是「案例常驻」。
                 ★ 刻意不塞进上面那个 .spp-gs-cols:那两栏装的是开关 / 参数,多插两行会把
                   2.53.56 / 2.53.57 特意对齐过的左右栏高度差又拉出来;这一组自成一行更干净。 */
            html += '<div class="spp-gs-group"><div class="spp-gs-group-title">自备表情 / 贴纸 · 自定义图床</div>';
            html += '<div class="spp-packs-cols">';
            /* ★★ 2.55.16(用户:「尽可能通过空行实现左右两栏四个方块视觉上的大致对齐」):
               左右两格的四块(说明 / 示例 / 输入框 / 小结)要横向对齐。做法是把**会撑高的两块
               写成等长文案**,而不是补 CSS 空行:
                 · 说明那一段两格**逐字相同**(「进 emoji 页 / 进贴纸页」挪进上面那行 label)⇒
                   不论窗口多窄、怎么折行,两格说明永远同高;
                 · 示例块两格只差「我的表情 / 我的贴纸」(都是 4 个字)⇒ 每行长度逐字对齐。
               ★ 为什么不用 min-height 那种「预留 N 行」的写法:面板宽 = min(900px, 100vw-48px),
                 窗口一窄说明的折行数就变;写死行数只在某一个宽度成立,换个窗口又错开。
               ★ 改之前实测:左格说明 3 行 / 右格说明 4 行 ⇒ 右格整体矮 17px(一行),
                 示例、输入框、小结跟着一起错开。 */
            html += '<div class="spp-gs-row" data-key="spp_custom_emoji" data-type="packs">' +
                '<label>自备 Emoji(进 emoji 页)<span class="spp-gs-hint">一行套系名 + 若干行图片直链,套系之间空一行;地址后接一段文本 = 那一张的悬停提示(不写就显示「套系名 + 序号」)</span></label>' +
                '<div class="spp-packs-eg">例:<br>我的表情<br>https://i.example.com/1.png <span class="spp-eg-note">← 悬停弹「我的表情 1」</span><br>https://i.example.com/2.png 坏笑 <span class="spp-eg-note">← 悬停弹「坏笑」</span><br>同一张图两页都要用:两格各写一次即可<br>png / jpg / webp / gif 都行;动图只画静止首帧<br>想改道到另一页,就在套系名前写 [emoji] 或 [贴纸]</div>' +
                '<textarea class="spp-packs-ta" spellcheck="false"></textarea>' +
                '<div class="spp-packs-fb"></div>' +
                '</div>';
            /* ★ 2.55.16:本格与左格**逐字等长**(见左格那段注释)—— 说明完全相同,示例只把
               「我的表情」换成「我的贴纸」(都是 4 个字)。改这两行时请连左格一起改,
               否则两格又会折到不同的行数,四块重新错开。 */
            html += '<div class="spp-gs-row" data-key="spp_custom_sticker" data-type="packs">' +
                '<label>自备贴纸(进贴纸页)<span class="spp-gs-hint">一行套系名 + 若干行图片直链,套系之间空一行;地址后接一段文本 = 那一张的悬停提示(不写就显示「套系名 + 序号」)</span></label>' +
                '<div class="spp-packs-eg">例:<br>我的贴纸<br>https://i.example.com/1.png <span class="spp-eg-note">← 悬停弹「我的贴纸 1」</span><br>https://i.example.com/2.png 坏笑 <span class="spp-eg-note">← 悬停弹「坏笑」</span><br>同一张图两页都要用:两格各写一次即可<br>png / jpg / webp / gif 都行;动图只画静止首帧<br>想改道到另一页,就在套系名前写 [emoji] 或 [贴纸]</div>' +
                '<textarea class="spp-packs-ta" spellcheck="false"></textarea>' +
                '<div class="spp-packs-fb"></div>' +
                '</div>';
            html += '</div>';
            /* ★★ 2.55.14(用户:「在设置面板中增加『图床跳转』功能,允许用户自定义增加
               更多『图床』UI 选项」):第三格粘贴区 —— 它写的不是套系,是**图床**。
               ★ 刻意复用第五类行 data-type="packs":这一格要的东西与上面两格一模一样
                 (一块多行文本框 + 下面一行小结 + 原样存字符串),换个 data-key 就能用;
                 为此新增第六类行得同步动渲染 / 自检选择器 / 保存分支 / 守卫类型表四处,
                 收益只有「类型名更贴切」。真正的差异只在小结那一处(renderPacksFeedback
                 按 data-key 分流)。 */
            html += '<div class="spp-gs-row" data-key="spp_host_extra" data-type="packs">' +
                '<label>自定义图床(粘贴区)<span class="spp-gs-hint">一行一个图床,写成「名称 首页地址 认链域名」:名称会出现在标注的【名称】里,首页地址是「图床 ▾」里点一下要跳去的页,认链域名可选(不写就按首页地址推,逗号分隔多个)。# 开头的行当注释跳过</span></label>' +
                '<div class="spp-packs-eg">例:<br>Postimages https://postimages.org/ postlmg.cc <span class="spp-eg-note">← 标注成【Postimages】,认得 postlmg.cc 的链接</span><br>Freeimage https://freeimage.host/ <span class="spp-eg-note">← 认链域名不写,就按首页域名推</span></div>' +
                '<textarea class="spp-packs-ta" spellcheck="false"></textarea>' +
                '<div class="spp-packs-fb"></div>' +
                '</div>';
            html += '</div>';

            /* ★★ 2.55.20(用户:「不同的热度对应不同的颜色,能否在设置面板中由用户自定义
               颜色、热度?」):茶馆热度配色组 —— 面板第六类行 data-type="heat_json"。
               一行装 7 个控件(3 个边界数字框 + 4 个取色器),一次保存原子落盘。
               ★ 初值直接问读数层(与别的组同口径);打开面板时还会按存储再回填一遍
                 (见 openSettingsPanel 里那段),两处读的都是 SPP_CONFIG.heatStyle()。
               ★ 网格做成 2×2 而不是一排 4 个:右栏在内层面板里只有约 400px 宽,
                 一排 4 格时每格只剩约 90px,7 位数的边界值会被数字框吃掉尾巴。
               ★ 4 个取色器用人话写档位(≤ 第一档 …… 更高),不写 tCold / cPeak 这类
                 内部字段名 —— 字段名只在 data-field 里出现。 */
            const heatCfg = SPP_CONFIG.heatStyle();
            html += '<div class="spp-gs-group"><div class="spp-gs-group-title">茶馆热度配色 (仅茶馆版块)</div>';
            html += '<div class="spp-gs-row" data-key="spp_heat_style" data-type="heat_json">' +
                '<label>热度分档与配色<span class="spp-gs-hint">' + HEAT_HINT + '</span></label>' +
                '<div class="spp-heat-grid">' +
                '<span class="spp-heat-cell"><span class="spp-heat-cap">≤ 第一档</span><span class="spp-heat-line">' +
                '<input type="number" class="spp-heat-n" data-field="tCold" min="0" max="9999999" step="100" value="' + heatCfg.tCold + '">' +
                '<input type="color" class="spp-heat-c" data-field="cCold" value="' + heatCfg.cCold + '" title="热度不超过第一档的颜色(默认白)">' +
                '</span></span>' +
                '<span class="spp-heat-cell"><span class="spp-heat-cap">≤ 第二档</span><span class="spp-heat-line">' +
                '<input type="number" class="spp-heat-n" data-field="tWarm" min="0" max="9999999" step="100" value="' + heatCfg.tWarm + '">' +
                '<input type="color" class="spp-heat-c" data-field="cWarm" value="' + heatCfg.cWarm + '" title="热度不超过第二档的颜色(默认绿)">' +
                '</span></span>' +
                '<span class="spp-heat-cell"><span class="spp-heat-cap">≤ 第三档</span><span class="spp-heat-line">' +
                '<input type="number" class="spp-heat-n" data-field="tHot" min="0" max="9999999" step="100" value="' + heatCfg.tHot + '">' +
                '<input type="color" class="spp-heat-c" data-field="cHot" value="' + heatCfg.cHot + '" title="热度不超过第三档的颜色(默认橙)">' +
                '</span></span>' +
                '<span class="spp-heat-cell"><span class="spp-heat-cap">更高</span><span class="spp-heat-line">' +
                '<input type="color" class="spp-heat-c" data-field="cPeak" value="' + heatCfg.cPeak + '" title="热度高于第三档的颜色(默认红)">' +
                '</span></span>' +
                '</div>' +
                '</div>';
            html += '</div>';

            /* ★ 2.53.56:按钮组挪到**两栏之外** —— 它的上边框自此横跨整幅,
               与标题下方那条分隔线同宽(这才是「左右对齐」)。 */
            html += `<div class="spp-gs-btns">
                <button class="spp-gs-cancel">取消</button>
                <button class="spp-gs-save spp-primary">保存并刷新</button>
            </div></div>`;

            /* ★★ 2.53.57:左栏尾部那三枚开关在这里落位。
               它们是**同一个「功能开关」语义**下的续列 —— 标题写「(续)」,读者一眼知道这几枚和右栏那组是一回事。
               为什么搬:右栏 773px 到底、左栏 577px 就空着(差 341px),用户圈着那三行画了「移动」箭头。
               搬完两栏各自约 640 / 590,底部基本齐平 —— 这就是他要的「左右对齐」。 */
            html = html.replace('<!--SPP-LEFT-TAIL-->',
                '<div class="spp-gs-group"><div class="spp-gs-group-title">功能开关(续)</div>' + leftTail + '</div>');

            overlay.innerHTML = html;
            document.body.appendChild(overlay);

            /* ★★ 2.55.11:粘贴区(2.55.14 起两格:自备套系 / 自定义图床)—— 边打边算,
               小结写在框下面(不必先保存)。算的是同一份解析器,所以小结里的数字与保存后
               真出现的东西同源。 */
            overlay.querySelectorAll('.spp-gs-row[data-type="packs"]').forEach(function (row) {
                const ta = row.querySelector('.spp-packs-ta');
                if (!ta) return;
                ta.value = SPP_CONFIG.text(row.dataset.key, '');
                ta.addEventListener('input', function () { renderPacksFeedback(overlay); });
            });
            renderPacksFeedback(overlay);

            // ==================== 2.52.94:面板 ↔ 登记表的一致性自检 ====================
            // ★ 为什么加:2.52.93 的教训是「表里登记了、界面上却没有」这类**静默不一致** ——
            //   当时登记的是给篡改猴看的 ==UserConfig== 块,而那个块根本不被解析,
            //   于是「交付目标」与「实际交付」差了整整一层,还查不出来。
            //   现在把 SPP_PANEL_DEFS 与真正渲染出来的 data-key 对一遍:
            //   面板少渲染一行、或表里写了界面没有的项,都当场记进 SPP_INIT.fail
            //   (→ <html data-spp-init> 会带上 ⇒ …,真实路径门也会当成失败)。
            try {
                const want = [];
                SPP_PANEL_DEFS.forEach(g => g.fields.forEach(f => want.push(f.key)));
                const got = [];
                //   ★ 2.52.95:选择器只留真正会渲染的两类(img_json / number)——
                //     'raw' 随 CM 卡片范围组一起没了,'readonly' 随只读组没了;
                //     留着查它们等于在查两个不可能出现的东西,反而掩护真缺行。
                //   ★ 2.53.59:加了第四类行 data-type="select"(搜索结果排序方式)。
                //     这里少一类 ⇒ 那行会被判成「SPP_PANEL_DEFS 登记了但面板没渲染」,
                //     而且 SPP_INIT.fail 是启动期写的,刷新页面也只在控制台留一条。
                overlay.querySelectorAll('.spp-gs-row[data-type="img_json"], .spp-gs-row[data-type="number"], .spp-gs-row[data-type="bool"], .spp-gs-row[data-type="select"], .spp-gs-row[data-type="packs"], .spp-gs-row[data-type="heat_json"]')
                    .forEach(r => got.push(r.dataset.key));
                const missing = want.filter(k => got.indexOf(k) < 0);
                const extra = got.filter(k => want.indexOf(k) < 0);
                if (missing.length || extra.length) {
                    SPP_INIT.fail.push('设置面板与 SPP_PANEL_DEFS 不一致:缺 [' + missing.join(', ') +
                        '] 多 [' + extra.join(', ') + ']');
                }
                // 显式把自检结论摆到 window 上,方便离屏核对:
                //   ★ 只看 <html data-spp-init> 看不出来 —— 那属性是启动阶段 publishInitReport() 写的,
                //     而面板是懒加载、打开时早就过了那一刻(实测:面板自检失败时属性仍是 ok38/fail0)。
                window.__SPP_SETTINGS_EARLY = { rows: got.length, hidden: SPP_HIDDEN_SWITCHES.length, fail: SPP_INIT.fail.slice() };
            } catch (e) {
                SPP_INIT.fail.push('设置面板自检失败:' + errBrief(e));
            }

            // 行点击切换 checkbox:适用范围 = 面板里带勾选的行
            //(img_json 类的「图片居中显示」,以及 2.53.0 新增的 bool 类「自动展开隐藏楼层」)。
            overlay.querySelectorAll('.spp-gs-row[data-type="img_json"], .spp-gs-row[data-type="bool"]').forEach(row => {
                const cb = row.querySelector('input[type="checkbox"]');
                if (!cb) return;
                row.addEventListener('click', e => {
                    if (e.target.tagName === 'INPUT') return;
                    cb.checked = !cb.checked;
                });
            });


            // range slider 值实时更新
            overlay.querySelectorAll('input[type="range"]').forEach(slider => {
                slider.addEventListener('input', () => {
                    const valSpan = slider.parentElement.querySelector('.spp-val');
                    if (valSpan) {
                        const row = slider.closest('.spp-gs-row');
                        const key = row ? row.dataset.key : '';
                        const units = { maxImgHeight:'vh', maxImgWidth:'%', thumbSize:'px' };
                        valSpan.textContent = slider.value + (units[key] || '');
                    }
                });
            });

            // 取消
            overlay.querySelector('.spp-gs-cancel').addEventListener('click', () => {
                overlay.classList.remove('spp-open');
            });

            // 点击背景关闭
            overlay.addEventListener('click', e => {
                if (e.target === overlay) overlay.classList.remove('spp-open');
            });

            // 保存
            overlay.querySelector('.spp-gs-save').addEventListener('click', () => {
                overlay.querySelectorAll('.spp-gs-row').forEach(row => {
                    const key = row.dataset.key;
                    const type = row.dataset.type;
                    // ★ 2.52.95:面板里现在只剩两类行(number / img_json),把两段走不到的分支删掉:
                    //   · 'readonly' —— 2.52.94 那段「跳过只读行」的守卫:只读组整组撤了,
                    //     面板里再没有 readonly 行,留着就是守一条不可能走的链路;
                    //   · 'raw' —— 单选下拉框,只服务 CM 卡片范围那一行,那一行撤了。
                    //   ★ 口径是「渲染什么就处理什么」:将来新增行类型必须回来补分支,
                    //     tools/verify/settings-panel.js 会核对面板渲染出的 data-type 集合
                    //     与本处分支集合是否一一对应。
                    if (type === 'number') {
                        const input = row.querySelector('input[type="number"]');
                        const val = parseInt(input.value);
                        if (!isNaN(val)) GM_setValue(key, val);
                    } else if (type === 'bool') {
                        // ★ 2.53.0:布尔行(功能开关组)。写 GM 存储的原始布尔值 ——
                        //   SPP_CONFIG.bool() 两种形态都认(真布尔 / 'false' 字符串)。
                        const cb = row.querySelector('input[type="checkbox"]');
                        if (cb) GM_setValue(key, cb.checked);
                    } else if (type === 'select') {
                        // ★ 2.53.59:第四类行(搜索结果排序方式)。写字符串,读数走 SPP_CONFIG.pick 收敛。
                        const sel = row.querySelector('select');
                        if (sel) GM_setValue(key, sel.value);
                    } else if (type === 'packs') {
                        // ★ 2.55.11:第五类行(粘贴区)。**原样存字符串** ——
                        //   解析交给 sppParseCustomPacks() / sppParseCustomHosts(),存进去的永远
                        //   是用户看到的那份文本,不归一、不吞行:写错了还能改回来,面板上也看
                        //   得见自己写了什么。
                        //   ★ 2.55.14:这一格变两格(自备套系 / 自定义图床);2.55.15 起第一格
                        //     又拆成左右两格(左 emoji / 右 贴纸)—— 本分支一字未动,三格都走
                        //     这里(data-key 不同,值各自落各自的键)。
                        const ta = row.querySelector('.spp-packs-ta');
                        if (ta) GM_setValue(key, ta.value);
                    } else if (type === 'heat_json') {
                        // ★★ 2.55.20:第六类行(茶馆热度配色)。一个键装整份 JSON ——
                        //   行内 7 个控件按 data-field 收成对象,一次原子落盘。
                        //   ★ 数字框留空 / 非数就**不写这一格**(不是写 0):读取层按缺字段
                        //     回落默认值 ⇒ 用户清空一格 = 「这一档回默认」,语义比写 0 清楚,
                        //     也不会因为一次误删把整档压成 0。
                        //   ★ 取色器永远给 #rrggbb,原样存;读取层仍会过一遍正则兜手改存储。
                        //   ★ 局部变量名刻意不叫 heat:本函数(buildOverlay)外层就有一个
                        //     heatCfg,内层再借同名读起来像是同一个东西。
                        const heatObj = {};
                        row.querySelectorAll('input[data-field]').forEach(inp => {
                            if (inp.type === 'color') { heatObj[inp.dataset.field] = inp.value; return; }
                            const n = parseInt(inp.value, 10);
                            if (!isNaN(n)) heatObj[inp.dataset.field] = n;
                        });
                        GM_setValue(key, JSON.stringify(heatObj));
                    } else if (type === 'img_json') {
                        // 图片设置合并写入 JSON(键名走模块级常量:面板 / 阅读优化 / 读数层共用同一个)
                        let current = {};
                        try { current = JSON.parse(GM_getValue(IMG_SETTINGS_KEY, '{}')); } catch (_) {}
                        if (row.querySelector('input[type="checkbox"]')) {
                            current[key] = row.querySelector('input[type="checkbox"]').checked;
                        } else if (row.querySelector('input[type="range"]')) {
                            current[key] = Number(row.querySelector('input[type="range"]').value);
                        } else if (row.querySelector('select')) {
                            current[key] = row.querySelector('select').value;
                        }
                        GM_setValue(IMG_SETTINGS_KEY, JSON.stringify(current));
                    }
                });
                location.reload();
            });

            // ★★ 2.53.9:「恢复默认头像」按钮。三个刻意的取舍:
            //   · **不刷新页面**:用户要的是「恢复」,刷新反而把反馈推迟一整轮;还原链路本身是同步的,
            //     点完立刻能看到站点原图回来,所以就地做完即可(保存按钮那条路径才需要 reload)。
            //   · 按钮点完直接 disabled + 改文案:这一版没有「恢复后再替换回去」的需求,
            //     留着可重复点的按钮只会让人以为它没生效(这个动作本身是幂等的,第二次会得到 0 个)。
            //   · 顺带把上面那枚开关在界面上同步成**未勾选**:出口已经把它落盘写成 false,
            //     界面不跟着改就会出现「显示勾着、实际已关」的静默不一致 —— 那正是保存一次就翻车的地方。
            const actBtn = overlay.querySelector('.spp-gs-actbtn');
            if (actBtn) actBtn.addEventListener('click', () => {
                let n = 0;
                try { n = (typeof sppAvaRestore === 'function') ? sppAvaRestore() : 0; } catch (e) { n = 0; }
                const cb = overlay.querySelector('.spp-gs-row[data-key="spp_hide_default_avatar"] input[type="checkbox"]');
                if (cb) cb.checked = false;
                actBtn.textContent = n > 0 ? ('已恢复 ' + n + ' 个头像') : '已恢复默认头像';
                actBtn.disabled = true;
            });

            // ★★ 2.55.1:「清除记录」按钮。与上面那枚动作按钮的三条取舍一致(不刷新页面、点完给结果、
            //   重复点无害),另加一条本动作特有的:**就地重绘当前页的「已读」标识** ——
            //   被清掉的记录对应的胶囊要立刻消失,否则看起来像按钮没生效。
            //   ★ 阈值取的是面板上【当时】那两个控件的值,不是已保存的值:用户可以改完直接点,
            //     不必先「保存并刷新」(那一步会重载页面,等于把「按时间清一次」拆成两轮操作)。
            //   ★ 读不到控件时按 0 处理 ⇒ readPurgeNow 直接返回 0 条,不会误清。
            const clearBtn = overlay.querySelector('.spp-read-clear-btn');
            if (clearBtn) clearBtn.addEventListener('click', () => {
                const numInput = overlay.querySelector('.spp-gs-row[data-key="spp_read_purge"] input[type="number"]');
                const unitSel = overlay.querySelector('.spp-gs-row[data-key="spp_read_purge_unit"] select');
                const amount = numInput ? parseInt(numInput.value, 10) : 0;
                const unit = unitSel ? unitSel.value : 'hour';
                let res = { removed: 0, left: 0 };
                try { res = readPurgeNow(amount, unit); } catch (e) { res = { removed: 0, left: 0 }; }
                const cnt = overlay.querySelector('.spp-read-count');
                if (cnt) cnt.textContent = String(res.left);
                clearBtn.textContent = res.removed > 0 ? ('已清除 ' + res.removed + ' 条') : '没有更早的记录';
                setTimeout(function () { clearBtn.textContent = '清除记录'; }, 2400);
            });

            // ★★ 2.55.2(用户:「在设置面板的清除功能中,增加一个『清除全部记录』的功能」):
            //   与上面那枚「清除记录」不同,这一枚是【整表清空】—— 两万条记录一点就没,
            //   所以做成两段式:第一次点只把按钮换成「确定清空?」,4 秒内再点一次才真的清;
            //   超时没点就自动退回原文案(按钮不会停在一个「等着你再点」的状态里)。
            //   ★ 刻意不用 confirm()/alert():这套面板里从来没有原生弹窗,
            //     突然弹一个既打断操作,也可能被浏览器静默拦掉 —— 拦掉之后按钮看起来就是「没反应」,
            //     那种失败很难和「按钮坏了」区分开(本项目吃过同类静默失败的亏)。
            //   ★ 阈值与整表清空是**两条独立出口**:前者按上面那两行参数清,后者不看参数。
            let wipeTimer = 0;
            const wipeBtn = overlay.querySelector('.spp-read-wipe-btn');
            if (wipeBtn) wipeBtn.addEventListener('click', () => {
                if (wipeBtn.dataset.sppArmed !== '1') {
                    wipeBtn.dataset.sppArmed = '1';
                    wipeBtn.textContent = '确定清空?';
                    if (wipeTimer) clearTimeout(wipeTimer);
                    wipeTimer = setTimeout(function () {
                        wipeBtn.dataset.sppArmed = '';
                        wipeBtn.textContent = '清除全部记录';
                    }, 4000);
                    return;
                }
                let res = { removed: 0, left: 0 };
                try { res = readWipeNow(); } catch (e) { res = { removed: 0, left: 0 }; }
                wipeBtn.dataset.sppArmed = '';
                if (wipeTimer) { clearTimeout(wipeTimer); wipeTimer = 0; }
                const cnt = overlay.querySelector('.spp-read-count');
                if (cnt) cnt.textContent = String(res.left);
                wipeBtn.textContent = res.removed > 0 ? ('已清空 ' + res.removed + ' 条') : '本来就没有记录';
                setTimeout(function () { wipeBtn.textContent = '清除全部记录'; }, 2400);
            });

            return overlay;
        }

        let overlay = null;
        openSettingsPanel = function () {
            if (!overlay) overlay = buildOverlay();
            // 每次打开时刷新数值
            // ★★ 2.53.47:这里原先是**写死的三元**(`key === 'spp_page_width' ? 1400 : 1`,
            //   上界同理 50)—— 第三个 number 键(spp_font_size)一加进来,它就会被按
            //   「默认 1 / 范围 1-50」回填:打开面板看到的是 1 而不是 14,用户不改就一保存,
            //   字号被静默写成 1px。这正是 经验.md 1.72「自伤会长得和产品缺陷一模一样」那一类。
            //   改成**从 SPP_PANEL_DEFS 逐项读**:以后再加 number 行不用回来改第二处。
            overlay.querySelectorAll('.spp-gs-row[data-type="number"] input').forEach(input => {
                const key = input.closest('.spp-gs-row').dataset.key;
                let meta = null;
                SPP_PANEL_DEFS.forEach(g => g.fields.forEach(f => { if (f.key === key) meta = f; }));
                const d = (meta && meta.def !== undefined) ? meta.def : 1;
                const lo = (meta && meta.min !== undefined) ? meta.min : 1;
                const hi = (meta && meta.max !== undefined) ? meta.max : 50;
                input.value = SPP_CONFIG.num(key, d, lo, hi);
            });
            // 刷新功能开关(★ 2.53.0:bool 行每次打开都按 GM 存储回填,不走 HTML 里那份初值)
            overlay.querySelectorAll('.spp-gs-row[data-type="bool"]').forEach(row => {
                const cb = row.querySelector('input[type="checkbox"]');
                if (cb) cb.checked = SPP_CONFIG.bool(row.dataset.key, panelBoolDef(row.dataset.key));
            });
            // 刷新下拉框行(★ 2.53.59:第四类行 data-type="select")——
            //   允许值与内置默认值都从 SPP_PANEL_DEFS 里逐项读,不在这里写第二份。
            overlay.querySelectorAll('.spp-gs-row[data-type="select"]').forEach(row => {
                const sel = row.querySelector('select');
                if (!sel) return;
                let meta = null;
                SPP_PANEL_DEFS.forEach(g => g.fields.forEach(f => { if (f.key === row.dataset.key) meta = f; }));
                const allowed = ((meta && meta.options) || []).map(o => o.v);
                const dflt = (meta && meta.def !== undefined) ? meta.def : allowed[0];
                sel.value = SPP_CONFIG.pick(row.dataset.key, dflt, allowed);
            });
            // ★★ 2.55.1:「已读记录」那枚动作行里的条数。面板是懒建的(只建一次),
            //   而记录条数每开一次都可能变 ⇒ 每次打开都重读一遍(readLog 已把整表读进内存)。
            const readCnt = overlay.querySelector('.spp-read-count');
            if (readCnt) readCnt.textContent = String(readLog.count());
            // 刷新图片设置(★ 2.52.93:改走 SPP_CONFIG,全脚本只有这一份默认值表)
            const imgSettings = SPP_CONFIG.imgSettings();
            overlay.querySelectorAll('.spp-gs-row[data-type="img_json"]').forEach(row => {
                const key = row.dataset.key;
                const cb = row.querySelector('input[type="checkbox"]');
                if (cb) { cb.checked = !!imgSettings[key]; return; }
                const slider = row.querySelector('input[type="range"]');
                if (slider) {
                    slider.value = imgSettings[key];
                    const valSpan = row.querySelector('.spp-val');
                    if (valSpan) {
                        const units = { maxImgHeight:'vh', maxImgWidth:'%', thumbSize:'px' };
                        valSpan.textContent = imgSettings[key] + (units[key] || '');
                    }
                }
                const select = row.querySelector('select');
                if (select) { select.value = imgSettings[key] || 'switch'; }
            });
            // 刷新粘贴区(★ 2.55.11 第五类行 data-type="packs";2.55.14 起这一格变两格,
            //   2.55.15 起第一格再拆成左右两格:左 emoji / 右 贴纸 / 自定义图床)—— 面板是
            //   懒建的(只建一次),每次打开都按 GM 存储重读一遍,与别的行同口径;小结交给
            //   renderPacksFeedback 统一重算(三格各算各的)。
            overlay.querySelectorAll('.spp-gs-row[data-type="packs"]').forEach(row => {
                const ta = row.querySelector('.spp-packs-ta');
                if (ta) ta.value = SPP_CONFIG.text(row.dataset.key, '');
            });
            renderPacksFeedback(overlay);
            // ★★ 2.55.20:刷新热度配色行(第六类行 data-type="heat_json")—— 一行 7 个控件,
            //   按 data-field 逐字段回填;读数与渲染同源(都走 SPP_CONFIG.heatStyle()),
            //   所以这里不写第二份默认值表。面板是懒建的,每次打开都重读一遍。
            const heatNow = SPP_CONFIG.heatStyle();
            overlay.querySelectorAll('.spp-gs-row[data-type="heat_json"]').forEach(row => {
                row.querySelectorAll('input[data-field]').forEach(inp => {
                    const v = heatNow[inp.dataset.field];
                    if (v !== undefined) inp.value = v;
                });
            });
            // ★ 2.52.95:这里原有「刷新首页卡片范围下拉框」两行 —— 那个下拉框随本组一起撤了,
            //   代码一并删除。★ 2.53.62:那个范围随后连菜单入口也撤了(范围写死 'cm',见模块 16),
            //   它与面板再无任何关系 ⇒ 这里也不会再有「刷新它」的代码。
            overlay.classList.add('spp-open');
        };
    }

    // ==================== 设置迁移(从旧键到新键) ====================
    function migrateSettings() {
        // 图片设置迁移: spiv_settings (v1.x 独立图片脚本) / sp_plus_img_settings (v1.x Plus脚本) → spp_img_settings
        const IMG_NEW = 'spp_img_settings';
        if (GM_getValue(IMG_NEW, null) === null) {
            for (const old of ['sp_plus_img_settings', 'spiv_settings']) {
                const raw = GM_getValue(old, null);
                if (raw !== null) {
                    try {
                        const parsed = JSON.parse(raw);
                        if (parsed && typeof parsed === 'object') {
                            GM_setValue(IMG_NEW, raw);
                            break; // 优先用找到的第一个
                        }
                    } catch (_) { /* skip corrupted */ }
                }
            }
        }
        // 翻页步长迁移: sp_plus_nav_step → spp_nav_step
        const NAV_NEW = 'spp_nav_step';
        if (GM_getValue(NAV_NEW, null) === null) {
            const oldVal = GM_getValue('sp_plus_nav_step', null);
            if (oldVal !== null) GM_setValue(NAV_NEW, oldVal);
        }
        // ★★ 2.53.75(用户:「含图楼层悬浮窗 这个默认关闭」):
        //   这一枚的**默认值**早在 2.53.70 就已经是 false,四处同值(登记表 / 面板渲染行 /
        //   运行时 toggle / 守卫 EXPECT)—— 病根不在默认值,在**存储里那条 true**:
        //   2.53.68 之前这个键是「整块面板建不建」的总开关、默认值恰恰是 **true**,
        //   2.53.68 把语义翻成「勾选 = 常显」、2.53.70 又把默认值翻成 false,
        //   但**已经落盘的旧值没人改**。于是凡是在 2.53.70 之前跑过本脚本的机器,
        //   GM 存储里都躺着 spp_imgidx_show=true ⇒ 悬浮窗照旧常显,看起来就像「默认没关」。
        //   ⇒ 一次性迁移:第一次跑到本版时把该键写回 false,并落一枚标记;
        //     此后用户在面板里怎么勾就是怎么算(迁移只跑一次,不会回头再抹掉人家的选择)。
        const IDX_MIG = 'spp_mig_imgidx_off';
        if (!GM_getValue(IDX_MIG, false)) {
            GM_setValue('spp_imgidx_show', false);
            GM_setValue(IDX_MIG, true);
        }
        // ★★ 2.55.13(用户:「链接自动标注 这个功能默认是关闭的」):
        //   与上一枚是同一口亏 —— 默认值改了,但**已经落盘的旧值没人改**:
        //   面板那枚「保存并刷新」是把每一行都写一遍的,凡是在 2.55.13 之前存过面板的
        //   机器,GM 存储里都躺着 spp_host_mark=true(当时它的默认值就是开),
        //   于是光改默认值对它们毫无效果,看起来就像「没改」。
        //   ⇒ 一次性迁移:第一次跑到本版时把该键写回 false,并落一枚标记;
        //     此后用户在面板里怎么勾就是怎么算(迁移只跑一次,不会回头再抹掉人家的选择)。
        const HM_MIG = 'spp_mig_hostmark_off';
        if (!GM_getValue(HM_MIG, false)) {
            GM_setValue('spp_host_mark', false);
            GM_setValue(HM_MIG, true);
        }
        // ★★ 2.55.14(用户:「表情名称提示;这个功能默认开启」):同一口亏的第三例 ——
        //   这一枚的默认值从 2.53.97 起一直是 false,面板存过一遍的机器里就躺着
        //   spp_emoji_tip=false。翻默认值对它们无效,只能写一次。
        //   ★ 与上一条同样的口径:迁移只跑一次,此后用户在面板里怎么勾就是怎么算。
        const ET_MIG = 'spp_mig_emojitip_on';
        if (!GM_getValue(ET_MIG, false)) {
            GM_setValue('spp_emoji_tip', true);
            GM_setValue(ET_MIG, true);
        }
    }
    migrateSettings();

    // ==================== 1. 页面加宽 ====================
    function initPageWiden() {
        const WIDTH_KEY = 'spp_page_width';

        // ★★ 2.49.5:宽度对【所有版块】共用一套,茶馆不再单独取值。
        //   实测同一窗口(1266px)下,改动前:
        //     茶馆  fid-9  → #main 1200px、列宽 89px、卡宽 392
        //     资源区 fid-14 → #main 1400px、列宽 105.67px、卡宽 459
        //   根因就是下面这一行的三元表达式把茶馆【硬编码】成 1200px:
        //     const W = isTeaHouse ? '1200px' : (GM_getValue(WIDTH_KEY, 1400) + 'px');
        //   而卡片规则本来就只有一套(41 条全以 html.spp-board-skin 开头,
        //   2.47.0 起就不再区分茶馆),网格同为 12 栏、同样按 span 4 / span 3 分流 ——
        //   唯一的差别只是容器宽度。所以这里只改容器宽度,卡片规则一行不动。
        //   ★ 1200 是 2.45.0 三列网格时代的取值(3 列 × 392 + 间隙),
        //     2.49.0 改成 12 栏后这个数字已无对应关系,跟随全局设置即可。
        //   ★ 用户要求:「茶馆的卡片和其他资源区的卡片使用同一套,总体显示宽度也一样。」
        // ★ 2.52.93:本项目历史上这里会「首次访问顺手把 1400 落盘」(savedWidth === null
        //   就 GM_setValue(1400))。撤掉的理由:它把「存储里没这个键」这个信息抹掉了,
        //   于是面板回填时读到的永远是 1400(分不清「用户设过 1400」还是「从没设过」)。
        //   改成:没有这个键就按 1400 算、不落盘 —— 存储里只有用户真正改过的值。
        let savedWidth = SPP_CONFIG.num(WIDTH_KEY, 1400, 1200, 1600);
        // ★★ 2.52.63:范围由 800–2000 收窄到 1200–1600(用户定案)。
        //   理由不是「改了会坏」,而是「改了没用」—— 实测(视口 1692 / 44 张卡):
        //     800 → mainW 800 / 卡宽 259;1400 → 1400 / 459;1600 → 1600 / 525;
        //     2000 与 3000 都被 min(Npx,100%) 收回成 1692,横向溢出 0、出屏卡 0。
        //   而【任何宽度下每行恒为 3 张】(12 栏网格 + 横卡 span 4)——
        //   这个旋钮改不了任何版式关系,只能缩放卡片的像素尺寸(259–556)
        //   与星空背景的尺度。故把可用区间收到设计基准附近,保留微调余地。
        //   ★ 与模块 4 那条注释里写的「范围 1200–1600」对齐(此前代码写的是 800–2000,
        //     注释与实现不一致了整整十几个版本)。
        //   ★ 测量口径必须与写出的规则逐字一致:用裸 `<N>px` 去覆盖着量,会量出
        //     「2000px → 横向溢出 308px、14 张卡出屏」这种【自己造出来的】假故障。
        GM_registerMenuCommand('⚙ 设置页面宽度 (当前: ' + savedWidth + 'px)', () => {
            const input = prompt('版块页页面宽度(px)\n所有版块共用此值,12 栏网格不变,允许 1200–1600(设计基准 1400)\n'
                + '★ 若在 Tampermonkey ▸ 脚本详情 ▸ 设置 里填过「页面宽度」,以那边为准', savedWidth);
            if (input === null) return;
            const val = parseInt(input);
            if (isNaN(val) || val < 1200 || val > 1600) return alert('请输入 1200–1600 之间的整数');
            GM_setValue(WIDTH_KEY, val);
            alert('已设置为 ' + val + 'px,刷新页面生效');
        });

        const W_NUM = SPP_CONFIG.num(WIDTH_KEY, 1400, 1200, 1600);
        const BG_SCALE = Math.round(4000 * W_NUM / 920);

        // ★★ 2.49.6:宽度取 min(设置值, 100%),窄窗口不再把卡片推出屏幕。
        //   2.49.5 只写固定 1400px,实测窗口 1266px 时
        //   #main = 1400 → 横向溢出 142px、44 张卡里 14 张右边缘在视口之外。
        //   收口必须写在【这条共享规则】上:这样任何窗口宽度下,
        //   茶馆与其它版块都仍然完全一致(这正是用户要的「同一套」)。
        //   实测窗口 1266px:mainW 1400→1258、hOverflow 142→0、
        //   cardsBeyondViewport 14→0、卡宽 459→411、每行仍 3 张。
        //   ★ 用 min() 而不是 max-width:站点本来就给这些容器写了固定 width,
        //     再加一条 max-width 会变成两张 !important 声明比书写顺序;
        //     min() 直接算出一个值,没有竞争。
        const W = `min(${W_NUM}px, 100%)`;

        GM_addStyle(`
            body { background-size: ${BG_SCALE}px auto !important; }
            #toptool, #header, #main, #footer, .footer { width: ${W} !important; }
            #header > table { width: 920px !important; margin: 0 auto !important; }
            #header > .c, #mainNav { width: auto !important; }
            #infobox { width: auto !important; padding: 6px !important; }
            .contentc, #content, .contentwrap, .bdbA { width: 100% !important; }
            .t { width: auto !important; }
            .t table { width: 100% !important; }
            .t5.t_one { width: auto !important; min-width: 550px; }
        `);
    }

    // ==================== 2. 悬浮翻页按钮 ====================
    function initNavButtons() {
        const STEP_KEY = 'spp_nav_step';
        let pageStep = SPP_CONFIG.num(STEP_KEY, 1, 1, 50);

        GM_registerMenuCommand('⚙ 设置翻页步长 (当前: ' + pageStep + ')', () => {
            const input = prompt('每次点击翻几页?(1-50)\n'
                + '★ 若在 Tampermonkey ▸ 脚本详情 ▸ 设置 里填过「翻页步长」,以那边为准', pageStep);
            if (input === null) return;
            const val = parseInt(input);
            if (isNaN(val) || val < 1) return alert('请输入大于0的整数');
            GM_setValue(STEP_KEY, val);
            pageStep = val;
            alert('已设置为每次翻 ' + val + ' 页,刷新页面生效');
        });

        function getPageInfoFromUrl() {
            const url = location.href;
            const result = { current: 1, urlPattern: null };

            let m = url.match(/(read\.php\?tid-\d+)-page-(\d+)([^#]*\.html)/);
            if (m) {
                result.current = parseInt(m[2]);
                result.urlPattern = url.replace(/-page-\d+/, '-page-{PAGE}').replace(/#.*$/, '');
                return result;
            }
            m = url.match(/(read\.php\?tid-\d+)(\.html)/);
            if (m) {
                result.current = 1;
                result.urlPattern = m[1] + '-page-{PAGE}' + m[2];
                return result;
            }
            m = url.match(/read\.php\?tid=(\d+).*[&?]page=(\d+)/);
            if (m) {
                result.current = parseInt(m[2]);
                result.urlPattern = 'read.php?tid-' + m[1] + '-page-{PAGE}.html';
                return result;
            }
            m = url.match(/read\.php\?tid=(\d+)/);
            if (m) {
                result.current = 1;
                result.urlPattern = 'read.php?tid-' + m[1] + '-page-{PAGE}.html';
                return result;
            }
            m = url.match(/(thread\.php\?fid-\d+)-page-(\d+)([^#]*\.html)/);
            if (m) {
                result.current = parseInt(m[2]);
                result.urlPattern = url.replace(/-page-\d+/, '-page-{PAGE}').replace(/#.*$/, '');
                return result;
            }
            m = url.match(/(thread\.php\?fid-\d+)(\.html)/);
            if (m) {
                result.current = 1;
                result.urlPattern = m[1] + '-page-{PAGE}' + m[2];
                return result;
            }
            // ★ 2.53.44:搜索页(search.php?…-page-2.html)以前**完全不在这个函数里** ——
            //   面板的 pattern 只能靠 DOM 兜底;那条路一旦读不到(末页 » 没有 href、页面重排、
            //   图墙页 thread_new.php 那种形状),整块面板就静默失效。这里补一条**通用**规则:
            //   任何 `…-page-N.html` 结尾的地址都认,search.php / thread_new.php / 个人页一处收全。
            //   ★ 必须排在 read.php / thread.php 那几条之后:它们先 return,行为一字不变。
            //   ★ 前缀不能写 [^?#]:站点这些漂亮 URL 的 `?` 就在中间(search.php?step-2-…-page-2.html),
            //     `[^?#]*` 当场漏掉(落地首测就踩了这一格,实测 pUrl 返回 null)。
            m = url.match(/^(.*-page-)(\d+)(\.html)([?#].*)?$/);
            if (m) {
                result.current = parseInt(m[2]);
                result.urlPattern = m[1] + '{PAGE}' + m[3];
                return result;
            }
            return result;
        }

        function getPageInfoFromDom() {
            const containers = document.querySelectorAll('.pages, .pagesone, .f_pager');
            for (const c of containers) {
                let current = 0, urlPattern = null;
                const walk = c.querySelectorAll('a, b, strong, span, font');
                for (const el of walk) {
                    const num = parseInt(el.textContent.trim());
                    if (isNaN(num) || num < 1) continue;
                    if (el.tagName === 'A') {
                        if (!urlPattern) {
                            const m = el.href.match(/^(.*)-page-\d+(\.[^#]*)/);
                            if (m) urlPattern = m[1] + '-page-{PAGE}' + m[2];
                        }
                    } else {
                        if (!current) current = num;
                    }
                }
                if (current > 0) return { current, urlPattern };
            }
            return null;
        }

        function getPagePatternFromDom() {
            const containers = document.querySelectorAll('.pages, .pagesone, .f_pager');
            for (const c of containers) {
                const links = c.querySelectorAll('a[href*="page-"]');
                for (const link of links) {
                    const m = link.href.match(/^(.*)-page-\d+(\.[^#]*)/);
                    if (m) return m[1] + '-page-{PAGE}' + m[2];
                }
            }
            return null;
        }

        function getTotalFromDom() {
            // ★ 2.53.44:站点自己写的「Pages: 2/10」才是权威总数 —— 原先只数链接里的最大页码,
            //   而搜索页那枚 »(末页)会被翻页条美化改成「下一页」⇒ 面板若在它之后读数,
            //   10 会读成 5(当时可见的最大页码),上界就形同虚设。
            const one = document.querySelector('.pagesone');
            if (one) {
                const tm = (one.textContent || '').match(/(\d+)\s*\/\s*(\d+)/);
                if (tm) return parseInt(tm[2]);
            }
            const selectors = ['.pages', '.pagesone', '.f_pager', '[class*="page"]'];
            for (const sel of selectors) {
                const container = document.querySelector(sel);
                if (!container) continue;
                let maxPage = 0;
                container.querySelectorAll('a').forEach(link => {
                    const num = parseInt(link.textContent.trim());
                    if (!isNaN(num) && num > maxPage) maxPage = num;
                    const m = link.href.match(/page-(\d+)/);
                    if (m) { const p = parseInt(m[1]); if (p > maxPage) maxPage = p; }
                });
                if (maxPage > 0) return maxPage;
            }
            return 0;
        }

        function buildNavPanel() {
            GM_addStyle(`
                #spp-nav-panel {
                    /* ★★ 2.53.47:多一条 transition:transform —— 「靠边自动半隐藏」的动画。
                       加在这里而不是面板自己那条类上:面板的 transform 是**居中的那一半**
                       (translateY(-50%)),收放态要把它一起重写(见文件末尾 .spp-nav--edge)。
                       把它写在基类上,收/放两个方向都有动画。 */
                    position:fixed; right:12px; top:50%; transform:translateY(-50%);
                    transition:transform .22s ease;
                    z-index:99998;
                    display:flex; flex-direction:column; align-items:stretch; gap:4px;
                    background:rgba(255,255,255,0.9);
                    border:1px solid var(--spu-border,#d8dee7);
                    border-radius:9px;
                    padding:5px;
                    box-shadow:0 10px 30px rgba(15,23,42,0.10),0 0 0 1px rgba(255,255,255,0.7) inset;
                    backdrop-filter:blur(14px); -webkit-backdrop-filter:blur(14px);
                    user-select:none;
                }
                #spp-nav-panel .spp-btn {
                    display:flex; align-items:center; justify-content:center;
                    width:38px; height:34px; border-radius:7px; color:var(--spu-secondary,#475569); font-size:14px;
                    text-decoration:none; cursor:pointer; opacity:1;
                    transition:color 0.16s ease, transform 0.16s ease, background 0.16s ease;
                    box-shadow:none; user-select:none; border:none; outline:none;
                }
                #spp-nav-panel .spp-btn:hover { color:var(--spu-accent,#2f6f68); background:var(--spu-overlay,#eef2f5); transform:translateY(-1px); }
                #spp-nav-panel .spp-btn:active { transform:translateY(0); }
                #spp-nav-panel .spp-btn.spp-enabled { background:rgba(47,111,104,0.10); color:var(--spu-accent,#2f6f68); }
                #spp-nav-panel .spp-btn.spp-enabled:hover { background:var(--spu-accent,#2f6f68); color:#fff; }
                #spp-nav-panel .spp-btn.spp-disabled { background:transparent; color:var(--spu-muted,#6b7280); cursor:not-allowed; opacity:0.38; }
                #spp-nav-panel .spp-btn.spp-disabled:hover { opacity:0.38; transform:none; background:transparent; color:var(--spu-muted,#6b7280); }
                #spp-nav-panel .spp-btn.spp-scroll { background:transparent; color:var(--spu-secondary,#475569); }
                #spp-nav-panel .spp-btn.spp-scroll:hover { background:var(--spu-overlay,#eef2f5); color:var(--spu-accent,#2f6f68); }
                #spp-nav-panel .spp-page-info {
                    display:flex; align-items:center; justify-content:center;
                    text-align:center; color:var(--spu-accent,#2f6f68); font-size:12px; font-weight:650;
                    background:var(--spu-overlay,#eef2f5); border-radius:7px; padding:7px 8px;
                    cursor:pointer; min-width:36px; line-height:1.2; transition:background 0.16s ease,color 0.16s ease;
                    font-family:'SF Mono','Cascadia Code','Consolas',monospace;
                }
                #spp-nav-panel .spp-page-info:hover { background:var(--spu-accent,#2f6f68); color:#fff; }
                #spp-nav-panel .spp-page-input {
                    width:40px; text-align:center; font-size:12px; font-weight:650;
                    border:1px solid var(--spu-accent,#2f6f68); border-radius:7px; padding:5px 6px;
                    outline:none; background:var(--spu-surface,#fff); color:var(--spu-text,#1f2937);
                    font-family:'SF Mono','Cascadia Code','Consolas',monospace;
                }
                #spp-nav-panel .spp-nav-sep {
                    height:1px; background:var(--spu-border,#d8dee7); margin:3px 5px; opacity:0.85;
                }
                /* ---- 2.53.47:靠边自动半隐藏(开关「翻页悬浮框靠边自动隐藏」,默认关)----
                   ★ 手柄 = 那几枚 38px 的按钮(图标居中,落在面板坐标 ≈22-38px 处):
                     收进去 39px、留 24px,正好让图标露出来当「可以点」的线索。
                     留 ≤16px 只剩边框与内衬、看着像一条空白;留满「1/10」要 46px,就不算半隐藏了。
                   ★★ 这里**不能照抄** #spp-imgidx 的 transform:translateX(calc(100% - 68px)):
                     那块面板的基类没有 transform,而这块的基类用它做垂直居中
                     (translateY(-50%))。transform 不叠加 —— 只写 translateX 会把面板甩到下半屏。
                     所以两个函数一起写。
                   ★ 类名带 spp-nav-- 前缀:不叫 .spp-nav(那是 spp_nav_step 的键名前缀,
                     仓库里吃过前缀包含关系的亏)。
                   ★★ 2.53.47 活体首测:只写类、不带 !important 时**不生效** ——
                     实测手动加上类之后,getBoundingClientRect 与 computed transform 都没变。
                     原因是 transform 是**单一属性**、不参与逐条合并:文件里既有
                     .spp-btn 的 transition 声明、又有站点自己的 transform 声明,
                     我们这条非 !important 的规则被整条丢掉。这里两个函数一起加 !important,
                     把「居中的那一半」也一并钉死。 */
                #spp-nav-panel.spp-nav--edge {
                    transform:translateX(calc(100% - 24px)) translateY(-50%) !important;
                }
                #spp-nav-panel.spp-nav--edge:hover,
                #spp-nav-panel.spp-nav--edge:focus-within {
                    transform:translateX(0) translateY(-50%) !important;
                }
                /* 有动效就照顾 prefers-reduced-motion:只去动画、半隐藏本身保留 */
                @media (prefers-reduced-motion: reduce) { #spp-nav-panel { transition:none; } }
            `);

            const pageInfo = getPageInfoFromUrl();
            const totalPages = getTotalFromDom();
            const domInfo = getPageInfoFromDom();
            if (domInfo) {
                pageInfo.current = domInfo.current;
                if (!pageInfo.urlPattern && domInfo.urlPattern) {
                    pageInfo.urlPattern = domInfo.urlPattern;
                }
            }
            if (!pageInfo.urlPattern) {
                const domPattern = getPagePatternFromDom();
                if (domPattern) pageInfo.urlPattern = domPattern;
            }
            const canNavigate = !!pageInfo.urlPattern;
            // ★★ 2.53.44:上下界都要夹 —— 原先只有 Math.max(1, …),末页照样能 ▶。
            //   实测搜索页第 10/10 页:面板自己写着 10/10,▶ 仍是 spp-enabled,
            //   一点就跳到 page-11(站点对这种越界请求给的是「Pages: 11/10」的空表页)。
            //   ★ 为什么现在才敢夹:总数改读站点自己写的「Pages: x/y」(见 getTotalFromDom)。
            const atFirst = pageInfo.current <= 1;
            const atLast = totalPages > 0 && pageInfo.current >= totalPages;

            function buildUrl(pageNum) {
                if (!pageInfo.urlPattern) return null;
                return pageInfo.urlPattern.replace('{PAGE}', pageNum);
            }
            function navigateToPage(targetPage) {
                if (!pageInfo.urlPattern || !isFinite(targetPage)) return;
                // totalPages 拿不到(站点没写 Pages: x/y)⇒ 不夹上界,保持老行为
                const upper = totalPages > 0 ? totalPages : Infinity;
                const clamped = Math.min(Math.max(1, targetPage), upper);
                if (clamped === pageInfo.current) return;   // 到头了:按兵不动,不制造无效跳转
                const url = buildUrl(clamped);
                if (url) window.location.href = url;
            }

            const panel = document.createElement('div');
            panel.id = 'spp-nav-panel';

            function makeBtn(text, cls, onClick) {
                const btn = document.createElement('a');
                btn.className = 'spp-btn ' + cls;
                btn.textContent = text;
                btn.href = 'javascript:void(0)';
                if (onClick) btn.addEventListener('click', onClick);
                return btn;
            }

            function makeSep() {
                const sep = document.createElement('div');
                sep.className = 'spp-nav-sep';
                return sep;
            }

            panel.appendChild(makeBtn('⬆', 'spp-scroll', (e) => {
                e.preventDefault(); window.scrollTo({ top: 0, behavior: 'smooth' });
            }));

            panel.appendChild(makeSep());

            if (canNavigate && !atFirst) {
                panel.appendChild(makeBtn('◀', 'spp-enabled', (e) => {
                    e.preventDefault(); navigateToPage(pageInfo.current - pageStep);
                }));
            } else {
                panel.appendChild(makeBtn('◀', 'spp-disabled'));
            }

            if (canNavigate) {
                const info = document.createElement('div');
                info.className = 'spp-page-info';
                info.textContent = totalPages > 0 ? pageInfo.current + '/' + totalPages : 'P' + pageInfo.current;
                info.title = '点击输入页码跳转';
                info.addEventListener('click', () => {
                    const input = document.createElement('input');
                    input.className = 'spp-page-input';
                    input.type = 'number';
                    input.min = '1';
                    input.value = pageInfo.current;
                    input.title = '输入页码后回车跳转';
                    info.replaceWith(input);
                    input.focus(); input.select();
                    function doJump() {
                        const val = parseInt(input.value);
                        if (!isNaN(val) && val > 0 && val !== pageInfo.current) {
                            navigateToPage(val);
                        } else {
                            input.replaceWith(info);
                        }
                    }
                    input.addEventListener('keydown', (e) => {
                        if (e.key === 'Enter') doJump();
                        if (e.key === 'Escape') input.replaceWith(info);
                    });
                    input.addEventListener('blur', () => {
                        setTimeout(() => { if (document.contains(input)) input.replaceWith(info); }, 150);
                    });
                });
                panel.appendChild(info);
            }

            if (canNavigate && !atLast) {
                panel.appendChild(makeBtn('▶', 'spp-enabled', (e) => {
                    e.preventDefault(); navigateToPage(pageInfo.current + pageStep);
                }));
            } else {
                panel.appendChild(makeBtn('▶', 'spp-disabled'));
            }

            panel.appendChild(makeBtn('⬇', 'spp-scroll', (e) => {
                e.preventDefault(); window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
            }));

            panel.appendChild(makeSep());

            // 设置齿轮按钮
            panel.appendChild(makeBtn('⚙', 'spp-scroll', (e) => {
                e.preventDefault(); openSettingsPanel();
            }));

            // ---- 2.53.47:靠边自动半隐藏(开关「翻页悬浮框靠边自动隐藏」,默认关)----
            //   ★ 读一次即可:面板保存走「保存并刷新」,用户要 reload 才生效
            //     (口径同 15a-2 的 spp_imgidx_edge)。
            //   ★ 纯 CSS 实现(见本函数开头那段样式):贴边时只留 24px,鼠标移上去或
            //     键盘 focus 进去自动滑回完整状态 —— 所以不需要 mouseenter 监听,
            //     「点页码格输入页码」也照旧可用(输入框 focus 会命中 :focus-within)。
            panel.classList.toggle('spp-nav--edge', SPP_CONFIG.bool('spp_nav_edge', false));

            appendToBodySafe(panel);

            // 键盘翻页(不干扰灯箱左右键)
            document.addEventListener('keydown', (e) => {
                if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
                const el = document.activeElement;
                if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT' ||
                    el.isContentEditable === 'true')) return;
                if (document.querySelector('.spp-overlay.spp-open')) return;
                if (!canNavigate) return;
                // ★ 2.53.44:到头了就连按键都不吃 —— 否则「按了没反应」与「按了跳空页」都像坏了
                if (e.key === 'ArrowLeft' ? atFirst : atLast) return;
                e.preventDefault();
                if (e.key === 'ArrowLeft') {
                    navigateToPage(pageInfo.current - pageStep);
                } else {
                    navigateToPage(pageInfo.current + pageStep);
                }
            });
        }

        onDomReady(buildNavPanel);
    }

    // ==================== 2.53.44:搜索索引过期自救 ====================
    // 用户报告:「使用脚本后,检索面板翻到第二页及以后会报错;关闭脚本后,翻页功能恢复正常」。
    // 取证把那句话拆成两半(读数见 2.53.44 段头与 tmp/r1/srv/ 下的探针):
    //   ★ 「报错」是**站点自己**发的那一张 —— 标题「南+ South Plus 提示信息」、正文
    //     「没有查找匹配的内容」。站点按 sid 存一份搜索索引,索引过期/被清掉之后
    //     那个 URL 就只剩这一页;本脚本在搜索页**一个请求都不发**,没有能力让它删索引。
    //   ★ 但这个坑每次都会把人顶在原地,所以本版补上:**过期就地重搜,并停在原来那一页**。
    // 机制(每一步都实测过):
    //   ① 判据:URL 是 search.php 且带 keyword+sid,页面上是站点那条提示信息 ——
    //      **按文字认**,不按结构(站点换 skin / 改 class 都不受影响);
    //   ② 用 URL 里的 keyword + seekfid 原样填回搜索表单字段、POST 一次 search.php? ——
    //      实测 200 / 65,105 B / 拿到新 sid-259614494 / 500 条 10 页(同源 fetch,不导航);
    //   ③ 从响应里认出新索引的翻页 URL 模板与总页数,把目标页号夹进 [1, 总页数],
    //      再 location.replace 过去(replace ⇒ 不往历史里堆一条马上又会过期的记录)。
    // 三条护栏(缺一条都可能变成打转或刷搜索):
    //   · 同一个 URL 30 秒内只救一次、每个标签页最多 3 次(sessionStorage 记账;拿不到就不救);
    //   · 响应认不出翻页模板(不是结果页)⇒ **一个字符都不动**,站点那页原样留着;
    //   · URL 里没有 sid 直接放弃 —— 关键词真搜不到东西时站点也是这一页,那不是「索引过期」。
    //   ★ 只恢复 URL 里带着的条件(关键词 + 版块 + 页码);时间范围不在 URL 里,
    //     只能回落到站点默认值(排序见 2.53.59:跟面板那枚「搜索结果排序方式」下拉框走)——
    //     索引已经没了,这是能给的最好结果。
    function initSearchRescue() {
        if (!PAGE.search) return;
        // 站点两种写法都收:`…-keyword-×-sid-×-seekfid-×-page-N.html`(翻页条那套)与
        // `…?step=2&keyword=×&sid=×&seekfid=×&page=N`(Go 输入框那一套)。
        const Q = SEARCH || '';
        const mKw = Q.match(/[?&\-]keyword-([\s\S]*?)-sid[-=]/) || Q.match(/[?&\-]keyword=([^&]*)/);
        if (!mKw) return;
        let keyword = '';
        try { keyword = decodeURIComponent(String(mKw[1]).replace(/\+/g, ' ')); } catch (e) { return; }
        if (!keyword) return;
        if (!/[?&\-]sid[-=]\d+/.test(Q)) return;      // 没有 sid ⇒ 不是「索引过期」,不碰
        const mFid = Q.match(/[?&\-]seekfid[-=]([^&\-]+)/) || Q.match(/[?&\-]f_fid[-=]([^&\-]+)/);
        const fid = mFid ? mFid[1] : 'all';
        const mPage = Q.match(/(?:[?&]|-|^)page[-=](\d+)/);
        const want = mPage ? parseInt(mPage[1]) : 1;

        onDomReady(function () {
            const box = document.querySelector('#main .t');
            if (!box || (box.textContent || '').indexOf('没有查找匹配的内容') < 0) return;
            // ★ 护栏(定稿):同一个 URL **30 秒内**只救一次,且每个标签页最多救 3 次。
            //   为什么不是「永久只救一次」:location.replace 已经把那条过期 URL 从历史里换掉了,
            //   用户若再从书签/前言进来,那本该救;永久标记会让他觉得「有时灵有时不灵」。
            //   为什么还留 30 秒 + 3 次:万一站点回的不是结果页(我们就不导航),或索引死活
            //   建不起来,这两条能保证它不会退化成刷搜索。拿不到 sessionStorage ⇒ 干脆不救。
            const KEY = 'spp_srescue:' + Q;
            const CNT = 'spp_srescue_n';
            let n = 0;
            try {
                const last = parseInt(sessionStorage.getItem(KEY) || '0', 10);
                n = parseInt(sessionStorage.getItem(CNT) || '0', 10) || 0;
                if (last && Date.now() - last < 30000) return;
                if (n >= 3) return;
            } catch (e) { return; }
            try {
                sessionStorage.setItem(KEY, String(Date.now()));
                sessionStorage.setItem(CNT, String(n + 1));
            } catch (e) { /* 记账写不进去就继续救 —— 读得到就说明存储可用 */ }

            // ---- 用同一组条件重发一次搜索(字段名与站点表单逐一对应)----
            const body = new URLSearchParams();
            body.set('step', '2');
            body.set('keyword', keyword);
            body.set('method', 'OR');
            body.set('pwuser', '');
            body.set('sch_area', '0');
            body.set('digest', '0');
            body.set('f_fid', fid);
            body.set('sch_time', 'all');
            // ★ 2.53.58 起:这里原先写死 lastpost(2.53.44 那版站点默认值)——现在跟着
            //   面板那枚「搜索结果排序方式」走(2.53.59 起它是个下拉框,不再是开/关)。
            body.set('orderway', searchOrder());
            body.set('asc', 'DESC');
            body.set('submit', '提 交');

            fetch('search.php?', { method: 'POST', body: body, credentials: 'include', cache: 'no-store' })
                .then(function (res) { return res && res.ok ? res.text() : ''; })
                .then(function (html) {
                    if (!html) return;
                    const doc = new DOMParser().parseFromString(html, 'text/html');
                    const a = doc.querySelector('.pages a[href*="page-"]');
                    if (!a) return;                              // 认不出 ⇒ 一动不动
                    let abs = '';
                    try { abs = new URL(a.getAttribute('href') || '', location.href).href; } catch (e) { return; }
                    let tpl = '';
                    let m = abs.match(/^(.*-page-)\d+(\.html.*)$/);
                    if (m) tpl = m[1] + '{PAGE}' + m[2];
                    if (!tpl) {                                  // 站点还有 &page=N 那种写法
                        m = abs.match(/^(.*[?&]page=)\d+(.*)$/);
                        if (m) tpl = m[1] + '{PAGE}' + m[2];
                    }
                    if (!tpl) return;
                    let total = 0;
                    const one = doc.querySelector('.pagesone');
                    if (one) {
                        const t = (one.textContent || '').match(/(\d+)\s*\/\s*(\d+)/);
                        if (t) total = parseInt(t[2]);
                    }
                    let to = isFinite(want) && want > 0 ? want : 1;
                    if (total > 0 && to > total) to = total;      // 别跳到不存在的页
                    const url = tpl.replace('{PAGE}', to);
                    console.info('[SPP] 搜索索引已过期,已用同一条件重新搜索,回到第 ' + to + ' 页');
                    location.replace(url);
                })
                .catch(function () { /* 失败 ⇒ 保持站点原样,什么都没改 */ });
        });
    }

    // ==================== 用户自备表情 / 贴纸套系(2.55.9) ====================
    /* ★★ 2.55.9(用户:「C:\Users\AdminUser\Documents\DSH\脚本-南+\表情包\emoji 与
       \\帖纸 这两个文件夹里都有 txt 文本,文本的名字就是对应的贴纸或 emoji 的名字…
       你能不能增加一些 emoji 或是贴纸?」):五套数据从磁盘上那 5 个 txt **原文生成**,
       一个字都没手抄 —— 文件名 = 套系名,一行一个图床链接。
       · emoji/ω(90 张,30×30 PNG)→ 表情栏 emoji 页的第三路数据源
       · 帖纸/ω(90 张,200×200 PNG)、呆呆唯(52)、猫儿小忆(121)、是,首相(100)
         → 表情栏 贴纸页,各占一枚分类胶囊
       · anim:true = 该张是**动画**(动画 webp 或 gif)。实测:呆呆唯 52 张全动、
         猫儿小忆 121 张全动,其余 280 张全是静态 PNG。网格里动画只画静止首帧
         (见 initEmojiPanel 的 staticizeAnimated)。
       · name 只能自动编号 —— txt 只给套系名,每张图没有自己的名字。这个名字只用于
         hover 名称框(spp_emoji_tip,默认关)与 img.alt,不影响看图。
       ★ 为什么放**文件作用域**而不是模块 14 里面:模块 3(图片阅读优化)也要认这 453
         条链接(帖子里的居中豁免),而它 safeInit 在模块 14 之前 —— 挂在模块 14 里
         模块 3 根本读不到。 */
    const PACK_EMOJI_OMEGA = [
        { name:'ω 1', url:'https://p.inari.site/usr/2927/6ac36cc24c8c0.png' },
        { name:'ω 2', url:'https://p.inari.site/usr/2927/6ac36cc2d21cf.png' },
        { name:'ω 3', url:'https://p.inari.site/usr/2927/6ac36cc34a2e9.png' },
        { name:'ω 4', url:'https://p.inari.site/usr/2927/6ac36cc3ac8c8.png' },
        { name:'ω 5', url:'https://p.inari.site/usr/2927/6ac36cc42970b.png' },
        { name:'ω 6', url:'https://p.inari.site/usr/2927/6ac36cc619718.png' },
        { name:'ω 7', url:'https://p.inari.site/usr/2927/6ac36cc676bf6.png' },
        { name:'ω 8', url:'https://p.inari.site/usr/2927/6ac36cc6eeee7.png' },
        { name:'ω 9', url:'https://p.inari.site/usr/2927/6ac36cc75a3dd.png' },
        { name:'ω 10', url:'https://p.inari.site/usr/2927/6ac36cc7c3a9a.png' },
        { name:'ω 11', url:'https://p.inari.site/usr/2927/6ac36cc882341.png' },
        { name:'ω 12', url:'https://p.inari.site/usr/2927/6ac36cc8e1703.png' },
        { name:'ω 13', url:'https://p.inari.site/usr/2927/6ac36cca6d9c6.png' },
        { name:'ω 14', url:'https://p.inari.site/usr/2927/6ac36ccac505a.png' },
        { name:'ω 15', url:'https://p.inari.site/usr/2927/6ac36ccb34078.png' },
        { name:'ω 16', url:'https://p.inari.site/usr/2927/6ac36ccb91aab.png' },
        { name:'ω 17', url:'https://p.inari.site/usr/2927/6ac36ccc1bfe7.png' },
        { name:'ω 18', url:'https://p.inari.site/usr/2927/6ac36ccc7e0c5.png' },
        { name:'ω 19', url:'https://p.inari.site/usr/2927/6ac36ccda6ed4.png' },
        { name:'ω 20', url:'https://p.inari.site/usr/2927/6ac36cce2e768.png' },
        { name:'ω 21', url:'https://p.inari.site/usr/2927/6ac36ccebad95.png' },
        { name:'ω 22', url:'https://p.inari.site/usr/2927/6ac36ccf3c21c.png' },
        { name:'ω 23', url:'https://p.inari.site/usr/2927/6ac36ccfb15e3.png' },
        { name:'ω 24', url:'https://p.inari.site/usr/2927/6ac36cd02d1de.png' },
        { name:'ω 25', url:'https://p.inari.site/usr/2927/6ac36cd0c0dbb.png' },
        { name:'ω 26', url:'https://p.inari.site/usr/2927/6ac36cd1c44d6.png' },
        { name:'ω 27', url:'https://p.inari.site/usr/2927/6ac36cd23cd2f.png' },
        { name:'ω 28', url:'https://p.inari.site/usr/2927/6ac36cd2b18e3.png' },
        { name:'ω 29', url:'https://p.inari.site/usr/2927/6ac36cd359572.png' },
        { name:'ω 30', url:'https://p.inari.site/usr/2927/6ac36cd3d01be.png' },
        { name:'ω 31', url:'https://p.inari.site/usr/2927/6ac370982fb36.png' },
        { name:'ω 32', url:'https://p.inari.site/usr/2927/6ac3709863730.png' },
        { name:'ω 33', url:'https://p.inari.site/usr/2927/6ac37098c7ab0.png' },
        { name:'ω 34', url:'https://p.inari.site/usr/2927/6ac370992ffb4.png' },
        { name:'ω 35', url:'https://p.inari.site/usr/2927/6ac37099a2a20.png' },
        { name:'ω 36', url:'https://p.inari.site/usr/2927/6ac3709a126e7.png' },
        { name:'ω 37', url:'https://p.inari.site/usr/2927/6ac3709a7039d.png' },
        { name:'ω 38', url:'https://p.inari.site/usr/2927/6ac3709ac992f.png' },
        { name:'ω 39', url:'https://p.inari.site/usr/2927/6ac3709b2ee2c.png' },
        { name:'ω 40', url:'https://p.inari.site/usr/2927/6ac3709c503ca.png' },
        { name:'ω 41', url:'https://p.inari.site/usr/2927/6ac3709ca23f6.png' },
        { name:'ω 42', url:'https://p.inari.site/usr/2927/6ac3709d0e382.png' },
        { name:'ω 43', url:'https://p.inari.site/usr/2927/6ac3709d8073c.png' },
        { name:'ω 44', url:'https://p.inari.site/usr/2927/6ac3709de2893.png' },
        { name:'ω 45', url:'https://p.inari.site/usr/2927/6ac3709ec70e9.png' },
        { name:'ω 46', url:'https://p.inari.site/usr/2927/6ac3709f1338c.png' },
        { name:'ω 47', url:'https://p.inari.site/usr/2927/6ac370a04450b.png' },
        { name:'ω 48', url:'https://p.inari.site/usr/2927/6ac370a0a1c2d.png' },
        { name:'ω 49', url:'https://p.inari.site/usr/2927/6ac370a14ccae.png' },
        { name:'ω 50', url:'https://p.inari.site/usr/2927/6ac370a1bb7b9.png' },
        { name:'ω 51', url:'https://p.inari.site/usr/2927/6ac370a265551.png' },
        { name:'ω 52', url:'https://p.inari.site/usr/2927/6ac370a2c5ed3.png' },
        { name:'ω 53', url:'https://p.inari.site/usr/2927/6ac370a341912.png' },
        { name:'ω 54', url:'https://p.inari.site/usr/2927/6ac370a485ebc.png' },
        { name:'ω 55', url:'https://p.inari.site/usr/2927/6ac370a51b680.png' },
        { name:'ω 56', url:'https://p.inari.site/usr/2927/6ac370a5965af.png' },
        { name:'ω 57', url:'https://p.inari.site/usr/2927/6ac370a61865c.png' },
        { name:'ω 58', url:'https://p.inari.site/usr/2927/6ac370a6dcae4.png' },
        { name:'ω 59', url:'https://p.inari.site/usr/2927/6ac370a768392.png' },
        { name:'ω 60', url:'https://p.inari.site/usr/2927/6ac370a7d48b3.png' },
        { name:'ω 61', url:'https://p.inari.site/usr/2927/6ac370e3ee23a.png' },
        { name:'ω 62', url:'https://p.inari.site/usr/2927/6ac370e468a7f.png' },
        { name:'ω 63', url:'https://p.inari.site/usr/2927/6ac370e517bdf.png' },
        { name:'ω 64', url:'https://p.inari.site/usr/2927/6ac370e570a2a.png' },
        { name:'ω 65', url:'https://p.inari.site/usr/2927/6ac370e5f083e.png' },
        { name:'ω 66', url:'https://p.inari.site/usr/2927/6ac370e66de92.png' },
        { name:'ω 67', url:'https://p.inari.site/usr/2927/6ac370e7149ef.png' },
        { name:'ω 68', url:'https://p.inari.site/usr/2927/6ac370e7b23ee.png' },
        { name:'ω 69', url:'https://p.inari.site/usr/2927/6ac370e84d568.png' },
        { name:'ω 70', url:'https://p.inari.site/usr/2927/6ac370ea32451.png' },
        { name:'ω 71', url:'https://p.inari.site/usr/2927/6ac370ec019ab.png' },
        { name:'ω 72', url:'https://p.inari.site/usr/2927/6ac370ec80a50.png' },
        { name:'ω 73', url:'https://p.inari.site/usr/2927/6ac370ed06038.png' },
        { name:'ω 74', url:'https://p.inari.site/usr/2927/6ac370ed676be.png' },
        { name:'ω 75', url:'https://p.inari.site/usr/2927/6ac370ee11d7f.png' },
        { name:'ω 76', url:'https://p.inari.site/usr/2927/6ac370ee68689.png' },
        { name:'ω 77', url:'https://p.inari.site/usr/2927/6ac370eed1d6b.png' },
        { name:'ω 78', url:'https://p.inari.site/usr/2927/6ac370f1827df.png' },
        { name:'ω 79', url:'https://p.inari.site/usr/2927/6ac370f1d6ca9.png' },
        { name:'ω 80', url:'https://p.inari.site/usr/2927/6ac370f252d79.png' },
        { name:'ω 81', url:'https://p.inari.site/usr/2927/6ac370f2cd5af.png' },
        { name:'ω 82', url:'https://p.inari.site/usr/2927/6ac370f338f6d.png' },
        { name:'ω 83', url:'https://p.inari.site/usr/2927/6ac370f397243.png' },
        { name:'ω 84', url:'https://p.inari.site/usr/2927/6ac370f4de5df.png' },
        { name:'ω 85', url:'https://p.inari.site/usr/2927/6ac370f5527c8.png' },
        { name:'ω 86', url:'https://p.inari.site/usr/2927/6ac370f5b1742.png' },
        { name:'ω 87', url:'https://p.inari.site/usr/2927/6ac370f623dce.png' },
        { name:'ω 88', url:'https://p.inari.site/usr/2927/6ac370f6a8f1e.png' },
        { name:'ω 89', url:'https://p.inari.site/usr/2927/6ac370f7230dd.png' },
        { name:'ω 90', url:'https://p.inari.site/usr/2927/6ac37161ac45b.png' },
    ];
    const PACK_STICKER_OMEGA = [
        { name:'ω 1', url:'https://p.inari.site/usr/2927/6ac36ae563b88.png' },
        { name:'ω 2', url:'https://p.inari.site/usr/2927/6ac36ae5f0611.png' },
        { name:'ω 3', url:'https://p.inari.site/usr/2927/6ac36ae6939a9.png' },
        { name:'ω 4', url:'https://p.inari.site/usr/2927/6ac36ae726d6a.png' },
        { name:'ω 5', url:'https://p.inari.site/usr/2927/6ac36ae84d61b.png' },
        { name:'ω 6', url:'https://p.inari.site/usr/2927/6ac36ae8b077c.png' },
        { name:'ω 7', url:'https://p.inari.site/usr/2927/6ac36ae91bc71.png' },
        { name:'ω 8', url:'https://p.inari.site/usr/2927/6ac36ae997b49.png' },
        { name:'ω 9', url:'https://p.inari.site/usr/2927/6ac36ae9ea6c3.png' },
        { name:'ω 10', url:'https://p.inari.site/usr/2927/6ac36aea47207.png' },
        { name:'ω 11', url:'https://p.inari.site/usr/2927/6ac36aea9ee2f.png' },
        { name:'ω 12', url:'https://p.inari.site/usr/2927/6ac36aebaf384.png' },
        { name:'ω 13', url:'https://p.inari.site/usr/2927/6ac36aec7c756.png' },
        { name:'ω 14', url:'https://p.inari.site/usr/2927/6ac36aed61167.png' },
        { name:'ω 15', url:'https://p.inari.site/usr/2927/6ac36aedea684.png' },
        { name:'ω 16', url:'https://p.inari.site/usr/2927/6ac36aee60825.png' },
        { name:'ω 17', url:'https://p.inari.site/usr/2927/6ac36aefabbbb.png' },
        { name:'ω 18', url:'https://p.inari.site/usr/2927/6ac36af0d1e40.png' },
        { name:'ω 19', url:'https://p.inari.site/usr/2927/6ac36af244d4f.png' },
        { name:'ω 20', url:'https://p.inari.site/usr/2927/6ac36af328745.png' },
        { name:'ω 21', url:'https://p.inari.site/usr/2927/6ac36af4496f5.png' },
        { name:'ω 22', url:'https://p.inari.site/usr/2927/6ac36af675c80.png' },
        { name:'ω 23', url:'https://p.inari.site/usr/2927/6ac36af70f070.png' },
        { name:'ω 24', url:'https://p.inari.site/usr/2927/6ac36af7b43b1.png' },
        { name:'ω 25', url:'https://p.inari.site/usr/2927/6ac36af834a37.png' },
        { name:'ω 26', url:'https://p.inari.site/usr/2927/6ac36af89cfab.png' },
        { name:'ω 27', url:'https://p.inari.site/usr/2927/6ac36af9ddce6.png' },
        { name:'ω 28', url:'https://p.inari.site/usr/2927/6ac36afa43307.png' },
        { name:'ω 29', url:'https://p.inari.site/usr/2927/6ac36afaabd04.png' },
        { name:'ω 30', url:'https://p.inari.site/usr/2927/6ac36b8490dbe.png' },
        { name:'ω 31', url:'https://p.inari.site/usr/2927/6ac36b84c0e24.png' },
        { name:'ω 32', url:'https://p.inari.site/usr/2927/6ac36b8528ac1.png' },
        { name:'ω 33', url:'https://p.inari.site/usr/2927/6ac36b8647de5.png' },
        { name:'ω 34', url:'https://p.inari.site/usr/2927/6ac36b86bf5d0.png' },
        { name:'ω 35', url:'https://p.inari.site/usr/2927/6ac36b87317c3.png' },
        { name:'ω 36', url:'https://p.inari.site/usr/2927/6ac36b87dd088.png' },
        { name:'ω 37', url:'https://p.inari.site/usr/2927/6ac36b8848e86.png' },
        { name:'ω 38', url:'https://p.inari.site/usr/2927/6ac36b88bf96e.png' },
        { name:'ω 39', url:'https://p.inari.site/usr/2927/6ac36b8a6ce61.png' },
        { name:'ω 40', url:'https://p.inari.site/usr/2927/6ac36b8ae3bb5.png' },
        { name:'ω 41', url:'https://p.inari.site/usr/2927/6ac36b8be4a68.png' },
        { name:'ω 42', url:'https://p.inari.site/usr/2927/6ac36b8c63cb4.png' },
        { name:'ω 43', url:'https://p.inari.site/usr/2927/6ac36b8cce889.png' },
        { name:'ω 44', url:'https://p.inari.site/usr/2927/6ac36b8d34268.png' },
        { name:'ω 45', url:'https://p.inari.site/usr/2927/6ac36b8d8df27.png' },
        { name:'ω 46', url:'https://p.inari.site/usr/2927/6ac36b8e62ce5.png' },
        { name:'ω 47', url:'https://p.inari.site/usr/2927/6ac36b8fa7f6a.png' },
        { name:'ω 48', url:'https://p.inari.site/usr/2927/6ac36b900b440.png' },
        { name:'ω 49', url:'https://p.inari.site/usr/2927/6ac36b90b3359.png' },
        { name:'ω 50', url:'https://p.inari.site/usr/2927/6ac36b912cb82.png' },
        { name:'ω 51', url:'https://p.inari.site/usr/2927/6ac36b91860de.png' },
        { name:'ω 52', url:'https://p.inari.site/usr/2927/6ac36b91d8d88.png' },
        { name:'ω 53', url:'https://p.inari.site/usr/2927/6ac36b9241fe9.png' },
        { name:'ω 54', url:'https://p.inari.site/usr/2927/6ac36b93af25a.png' },
        { name:'ω 55', url:'https://p.inari.site/usr/2927/6ac36b94aea5b.png' },
        { name:'ω 56', url:'https://p.inari.site/usr/2927/6ac36b953d4cf.png' },
        { name:'ω 57', url:'https://p.inari.site/usr/2927/6ac36b95e6acd.png' },
        { name:'ω 58', url:'https://p.inari.site/usr/2927/6ac36b96e4e23.png' },
        { name:'ω 59', url:'https://p.inari.site/usr/2927/6ac36b9790715.png' },
        { name:'ω 60', url:'https://p.inari.site/usr/2927/6ac36bf6e3270.png' },
        { name:'ω 61', url:'https://p.inari.site/usr/2927/6ac36bf72a1cb.png' },
        { name:'ω 62', url:'https://p.inari.site/usr/2927/6ac36bf796741.png' },
        { name:'ω 63', url:'https://p.inari.site/usr/2927/6ac36bf7ef0b7.png' },
        { name:'ω 64', url:'https://p.inari.site/usr/2927/6ac36bf8de622.png' },
        { name:'ω 65', url:'https://p.inari.site/usr/2927/6ac36bf99fa48.png' },
        { name:'ω 66', url:'https://p.inari.site/usr/2927/6ac36bf9ea4ab.png' },
        { name:'ω 67', url:'https://p.inari.site/usr/2927/6ac36bfb7eb72.png' },
        { name:'ω 68', url:'https://p.inari.site/usr/2927/6ac36bfbcc144.png' },
        { name:'ω 69', url:'https://p.inari.site/usr/2927/6ac36bfc52786.png' },
        { name:'ω 70', url:'https://p.inari.site/usr/2927/6ac36bfcce20a.png' },
        { name:'ω 71', url:'https://p.inari.site/usr/2927/6ac36bfd50bbf.png' },
        { name:'ω 72', url:'https://p.inari.site/usr/2927/6ac36bfda5553.png' },
        { name:'ω 73', url:'https://p.inari.site/usr/2927/6ac36bff409ff.png' },
        { name:'ω 74', url:'https://p.inari.site/usr/2927/6ac36bffe996d.png' },
        { name:'ω 75', url:'https://p.inari.site/usr/2927/6ac36c005ee0d.png' },
        { name:'ω 76', url:'https://p.inari.site/usr/2927/6ac36c01498d6.png' },
        { name:'ω 77', url:'https://p.inari.site/usr/2927/6ac36c017d7b8.png' },
        { name:'ω 78', url:'https://p.inari.site/usr/2927/6ac36c023e981.png' },
        { name:'ω 79', url:'https://p.inari.site/usr/2927/6ac36c0261873.png' },
        { name:'ω 80', url:'https://p.inari.site/usr/2927/6ac36c03146d1.png' },
        { name:'ω 81', url:'https://p.inari.site/usr/2927/6ac36c0400794.png' },
        { name:'ω 82', url:'https://p.inari.site/usr/2927/6ac36c0451954.png' },
        { name:'ω 83', url:'https://p.inari.site/usr/2927/6ac36c04e19c8.png' },
        { name:'ω 84', url:'https://p.inari.site/usr/2927/6ac36c05453bb.png' },
        { name:'ω 85', url:'https://p.inari.site/usr/2927/6ac36c05a0aa3.png' },
        { name:'ω 86', url:'https://p.inari.site/usr/2927/6ac36c0771675.png' },
        { name:'ω 87', url:'https://p.inari.site/usr/2927/6ac36c07d93b7.png' },
        { name:'ω 88', url:'https://p.inari.site/usr/2927/6ac36c08554b0.png' },
        { name:'ω 89', url:'https://p.inari.site/usr/2927/6ac36c08df2c1.png' },
        { name:'ω 90', url:'https://p.inari.site/usr/2927/6ac36c652ff36.png' },
    ];
    const PACK_DAIDAIWEI = [
        { name:'呆呆唯 1', url:'https://p.inari.site/usr/2927/6ac350666ed15.webp', anim: true },
        { name:'呆呆唯 2', url:'https://p.inari.site/usr/2927/6ac350671decb.gif', anim: true },
        { name:'呆呆唯 3', url:'https://p.inari.site/usr/2927/6ac35067c3bfc.webp', anim: true },
        { name:'呆呆唯 4', url:'https://p.inari.site/usr/2927/6ac350686b5db.webp', anim: true },
        { name:'呆呆唯 5', url:'https://p.inari.site/usr/2927/6ac35069d3d0a.gif', anim: true },
        { name:'呆呆唯 6', url:'https://p.inari.site/usr/2927/6ac3506a857dd.webp', anim: true },
        { name:'呆呆唯 7', url:'https://p.inari.site/usr/2927/6ac3506b654ac.gif', anim: true },
        { name:'呆呆唯 8', url:'https://p.inari.site/usr/2927/6ac3506c5e90b.webp', anim: true },
        { name:'呆呆唯 9', url:'https://p.inari.site/usr/2927/6ac3506ce5aa9.gif', anim: true },
        { name:'呆呆唯 10', url:'https://p.inari.site/usr/2927/6ac3506e0292d.webp', anim: true },
        { name:'呆呆唯 11', url:'https://p.inari.site/usr/2927/6ac3506e7e9e8.webp', anim: true },
        { name:'呆呆唯 12', url:'https://p.inari.site/usr/2927/6ac3507243273.webp', anim: true },
        { name:'呆呆唯 13', url:'https://p.inari.site/usr/2927/6ac35072c1eed.webp', anim: true },
        { name:'呆呆唯 14', url:'https://p.inari.site/usr/2927/6ac3507398fef.webp', anim: true },
        { name:'呆呆唯 15', url:'https://p.inari.site/usr/2927/6ac350747599a.webp', anim: true },
        { name:'呆呆唯 16', url:'https://p.inari.site/usr/2927/6ac3507652ef4.webp', anim: true },
        { name:'呆呆唯 17', url:'https://p.inari.site/usr/2927/6ac35076e58c4.webp', anim: true },
        { name:'呆呆唯 18', url:'https://p.inari.site/usr/2927/6ac35077acd52.gif', anim: true },
        { name:'呆呆唯 19', url:'https://p.inari.site/usr/2927/6ac350783ecd5.webp', anim: true },
        { name:'呆呆唯 20', url:'https://p.inari.site/usr/2927/6ac3507926144.gif', anim: true },
        { name:'呆呆唯 21', url:'https://p.inari.site/usr/2927/6ac3507a96404.webp', anim: true },
        { name:'呆呆唯 22', url:'https://p.inari.site/usr/2927/6ac3507c0f6bf.gif', anim: true },
        { name:'呆呆唯 23', url:'https://p.inari.site/usr/2927/6ac3507e4afe0.webp', anim: true },
        { name:'呆呆唯 24', url:'https://p.inari.site/usr/2927/6ac3507f0e92a.webp', anim: true },
        { name:'呆呆唯 25', url:'https://p.inari.site/usr/2927/6ac3507fb4215.gif', anim: true },
        { name:'呆呆唯 26', url:'https://p.inari.site/usr/2927/6ac35080b749d.gif', anim: true },
        { name:'呆呆唯 27', url:'https://p.inari.site/usr/2927/6ac35081ae00a.webp', anim: true },
        { name:'呆呆唯 28', url:'https://p.inari.site/usr/2927/6ac350828d1c1.webp', anim: true },
        { name:'呆呆唯 29', url:'https://p.inari.site/usr/2927/6ac3508364939.webp', anim: true },
        { name:'呆呆唯 30', url:'https://p.inari.site/usr/2927/6ac3508443a00.gif', anim: true },
        { name:'呆呆唯 31', url:'https://p.inari.site/usr/2927/6ac3595ee1d36.gif', anim: true },
        { name:'呆呆唯 32', url:'https://p.inari.site/usr/2927/6ac3595fc8394.webp', anim: true },
        { name:'呆呆唯 33', url:'https://p.inari.site/usr/2927/6ac35960ba869.webp', anim: true },
        { name:'呆呆唯 34', url:'https://p.inari.site/usr/2927/6ac35963a8076.webp', anim: true },
        { name:'呆呆唯 35', url:'https://p.inari.site/usr/2927/6ac3596413db1.webp', anim: true },
        { name:'呆呆唯 36', url:'https://p.inari.site/usr/2927/6ac35964d90ae.webp', anim: true },
        { name:'呆呆唯 37', url:'https://p.inari.site/usr/2927/6ac35966600d9.webp', anim: true },
        { name:'呆呆唯 38', url:'https://p.inari.site/usr/2927/6ac3596706b2c.gif', anim: true },
        { name:'呆呆唯 39', url:'https://p.inari.site/usr/2927/6ac3596949bec.webp', anim: true },
        { name:'呆呆唯 40', url:'https://p.inari.site/usr/2927/6ac3596948de4.webp', anim: true },
        { name:'呆呆唯 41', url:'https://p.inari.site/usr/2927/6ac35974aad63.webp', anim: true },
        { name:'呆呆唯 42', url:'https://p.inari.site/usr/2927/6ac35974e905e.gif', anim: true },
        { name:'呆呆唯 43', url:'https://p.inari.site/usr/2927/6ac35975357c2.webp', anim: true },
        { name:'呆呆唯 44', url:'https://p.inari.site/usr/2927/6ac3597552e05.webp', anim: true },
        { name:'呆呆唯 45', url:'https://p.inari.site/usr/2927/6ac3597597a4a.webp', anim: true },
        { name:'呆呆唯 46', url:'https://p.inari.site/usr/2927/6ac35977e4a2d.webp', anim: true },
        { name:'呆呆唯 47', url:'https://p.inari.site/usr/2927/6ac359788bc2f.webp', anim: true },
        { name:'呆呆唯 48', url:'https://p.inari.site/usr/2927/6ac35988104fb.webp', anim: true },
        { name:'呆呆唯 49', url:'https://p.inari.site/usr/2927/6ac359884ddf7.gif', anim: true },
        { name:'呆呆唯 50', url:'https://p.inari.site/usr/2927/6ac359888e59c.webp', anim: true },
        { name:'呆呆唯 51', url:'https://p.inari.site/usr/2927/6ac35989017f5.webp', anim: true },
        { name:'呆呆唯 52', url:'https://p.inari.site/usr/2927/6ac35989d57e4.webp', anim: true },
    ];
    const PACK_MAOERXIAOYI = [
        { name:'猫儿小忆 1', url:'https://p.inari.site/usr/2927/6ac35d3337cc3.gif', anim: true },
        { name:'猫儿小忆 2', url:'https://p.inari.site/usr/2927/6ac3609050085.webp', anim: true },
        { name:'猫儿小忆 3', url:'https://p.inari.site/usr/2927/6ac3609097720.webp', anim: true },
        { name:'猫儿小忆 4', url:'https://p.inari.site/usr/2927/6ac36090df8d7.webp', anim: true },
        { name:'猫儿小忆 5', url:'https://p.inari.site/usr/2927/6ac3609160b1b.webp', anim: true },
        { name:'猫儿小忆 6', url:'https://p.inari.site/usr/2927/6ac36091c29c6.webp', anim: true },
        { name:'猫儿小忆 7', url:'https://p.inari.site/usr/2927/6ac36092d52b7.webp', anim: true },
        { name:'猫儿小忆 8', url:'https://p.inari.site/usr/2927/6ac36092d56e7.webp', anim: true },
        { name:'猫儿小忆 9', url:'https://p.inari.site/usr/2927/6ac360937822e.webp', anim: true },
        { name:'猫儿小忆 10', url:'https://p.inari.site/usr/2927/6ac360941959e.webp', anim: true },
        { name:'猫儿小忆 11', url:'https://p.inari.site/usr/2927/6ac36094bf620.webp', anim: true },
        { name:'猫儿小忆 12', url:'https://p.inari.site/usr/2927/6ac360955cb95.webp', anim: true },
        { name:'猫儿小忆 13', url:'https://p.inari.site/usr/2927/6ac36095f2e7e.webp', anim: true },
        { name:'猫儿小忆 14', url:'https://p.inari.site/usr/2927/6ac36098c66d0.webp', anim: true },
        { name:'猫儿小忆 15', url:'https://p.inari.site/usr/2927/6ac360996e413.webp', anim: true },
        { name:'猫儿小忆 16', url:'https://p.inari.site/usr/2927/6ac3609a1e840.webp', anim: true },
        { name:'猫儿小忆 17', url:'https://p.inari.site/usr/2927/6ac3609acb9e1.webp', anim: true },
        { name:'猫儿小忆 18', url:'https://p.inari.site/usr/2927/6ac3609b2dedf.webp', anim: true },
        { name:'猫儿小忆 19', url:'https://p.inari.site/usr/2927/6ac3609bd44bf.webp', anim: true },
        { name:'猫儿小忆 20', url:'https://p.inari.site/usr/2927/6ac3609c3f88d.webp', anim: true },
        { name:'猫儿小忆 21', url:'https://p.inari.site/usr/2927/6ac3609cb353a.webp', anim: true },
        { name:'猫儿小忆 22', url:'https://p.inari.site/usr/2927/6ac3609d8f184.webp', anim: true },
        { name:'猫儿小忆 23', url:'https://p.inari.site/usr/2927/6ac3609e2b771.webp', anim: true },
        { name:'猫儿小忆 24', url:'https://p.inari.site/usr/2927/6ac360a09ad64.webp', anim: true },
        { name:'猫儿小忆 25', url:'https://p.inari.site/usr/2927/6ac360a227462.webp', anim: true },
        { name:'猫儿小忆 26', url:'https://p.inari.site/usr/2927/6ac360a36ee8d.webp', anim: true },
        { name:'猫儿小忆 27', url:'https://p.inari.site/usr/2927/6ac360a4d9c02.webp', anim: true },
        { name:'猫儿小忆 28', url:'https://p.inari.site/usr/2927/6ac360a562b4d.webp', anim: true },
        { name:'猫儿小忆 29', url:'https://p.inari.site/usr/2927/6ac360a631b21.webp', anim: true },
        { name:'猫儿小忆 30', url:'https://p.inari.site/usr/2927/6ac360a9ddc4a.webp', anim: true },
        { name:'猫儿小忆 31', url:'https://p.inari.site/usr/2927/6ac360aa4b6b1.webp', anim: true },
        { name:'猫儿小忆 32', url:'https://p.inari.site/usr/2927/6ac36175a4d5b.webp', anim: true },
        { name:'猫儿小忆 33', url:'https://p.inari.site/usr/2927/6ac3617602fcd.webp', anim: true },
        { name:'猫儿小忆 34', url:'https://p.inari.site/usr/2927/6ac361770d4dd.webp', anim: true },
        { name:'猫儿小忆 35', url:'https://p.inari.site/usr/2927/6ac3617792eaa.webp', anim: true },
        { name:'猫儿小忆 36', url:'https://p.inari.site/usr/2927/6ac36177f1ec9.webp', anim: true },
        { name:'猫儿小忆 37', url:'https://p.inari.site/usr/2927/6ac36178b6091.webp', anim: true },
        { name:'猫儿小忆 38', url:'https://p.inari.site/usr/2927/6ac361797b888.webp', anim: true },
        { name:'猫儿小忆 39', url:'https://p.inari.site/usr/2927/6ac36179a701b.webp', anim: true },
        { name:'猫儿小忆 40', url:'https://p.inari.site/usr/2927/6ac3617bed7d1.webp', anim: true },
        { name:'猫儿小忆 41', url:'https://p.inari.site/usr/2927/6ac3617c73eaa.webp', anim: true },
        { name:'猫儿小忆 42', url:'https://p.inari.site/usr/2927/6ac3617d48e5f.webp', anim: true },
        { name:'猫儿小忆 43', url:'https://p.inari.site/usr/2927/6ac3617df2292.webp', anim: true },
        { name:'猫儿小忆 44', url:'https://p.inari.site/usr/2927/6ac3617e71285.webp', anim: true },
        { name:'猫儿小忆 45', url:'https://p.inari.site/usr/2927/6ac3617f7c655.webp', anim: true },
        { name:'猫儿小忆 46', url:'https://p.inari.site/usr/2927/6ac361803c9d7.webp', anim: true },
        { name:'猫儿小忆 47', url:'https://p.inari.site/usr/2927/6ac361823d9d0.webp', anim: true },
        { name:'猫儿小忆 48', url:'https://p.inari.site/usr/2927/6ac36182bc368.webp', anim: true },
        { name:'猫儿小忆 49', url:'https://p.inari.site/usr/2927/6ac3618362e5b.webp', anim: true },
        { name:'猫儿小忆 50', url:'https://p.inari.site/usr/2927/6ac36183ebc9c.webp', anim: true },
        { name:'猫儿小忆 51', url:'https://p.inari.site/usr/2927/6ac3618476d5d.webp', anim: true },
        { name:'猫儿小忆 52', url:'https://p.inari.site/usr/2927/6ac36184df910.webp', anim: true },
        { name:'猫儿小忆 53', url:'https://p.inari.site/usr/2927/6ac3618549edc.webp', anim: true },
        { name:'猫儿小忆 54', url:'https://p.inari.site/usr/2927/6ac36185cf70f.webp', anim: true },
        { name:'猫儿小忆 55', url:'https://p.inari.site/usr/2927/6ac361868b583.webp', anim: true },
        { name:'猫儿小忆 56', url:'https://p.inari.site/usr/2927/6ac361870820d.webp', anim: true },
        { name:'猫儿小忆 57', url:'https://p.inari.site/usr/2927/6ac36189d5cad.webp', anim: true },
        { name:'猫儿小忆 58', url:'https://p.inari.site/usr/2927/6ac3618a5140d.webp', anim: true },
        { name:'猫儿小忆 59', url:'https://p.inari.site/usr/2927/6ac3618b1ce0f.webp', anim: true },
        { name:'猫儿小忆 60', url:'https://p.inari.site/usr/2927/6ac3618c387e5.webp', anim: true },
        { name:'猫儿小忆 61', url:'https://p.inari.site/usr/2927/6ac3618cd9e11.webp', anim: true },
        { name:'猫儿小忆 62', url:'https://p.inari.site/usr/2927/6ac3629e9bb12.webp', anim: true },
        { name:'猫儿小忆 63', url:'https://p.inari.site/usr/2927/6ac3629ee2417.webp', anim: true },
        { name:'猫儿小忆 64', url:'https://p.inari.site/usr/2927/6ac3629f5dc4a.webp', anim: true },
        { name:'猫儿小忆 65', url:'https://p.inari.site/usr/2927/6ac3629fb9c7e.webp', anim: true },
        { name:'猫儿小忆 66', url:'https://p.inari.site/usr/2927/6ac362a01ae37.webp', anim: true },
        { name:'猫儿小忆 67', url:'https://p.inari.site/usr/2927/6ac362a07b9de.webp', anim: true },
        { name:'猫儿小忆 68', url:'https://p.inari.site/usr/2927/6ac362a123db4.webp', anim: true },
        { name:'猫儿小忆 69', url:'https://p.inari.site/usr/2927/6ac362a49a4b4.webp', anim: true },
        { name:'猫儿小忆 70', url:'https://p.inari.site/usr/2927/6ac362a654a1c.webp', anim: true },
        { name:'猫儿小忆 71', url:'https://p.inari.site/usr/2927/6ac362a6caab1.webp', anim: true },
        { name:'猫儿小忆 72', url:'https://p.inari.site/usr/2927/6ac362a73c6a2.webp', anim: true },
        { name:'猫儿小忆 73', url:'https://p.inari.site/usr/2927/6ac362a798a50.webp', anim: true },
        { name:'猫儿小忆 74', url:'https://p.inari.site/usr/2927/6ac362a80ad83.webp', anim: true },
        { name:'猫儿小忆 75', url:'https://p.inari.site/usr/2927/6ac362a869c44.webp', anim: true },
        { name:'猫儿小忆 76', url:'https://p.inari.site/usr/2927/6ac362a9e1e38.webp', anim: true },
        { name:'猫儿小忆 77', url:'https://p.inari.site/usr/2927/6ac362aa3229f.webp', anim: true },
        { name:'猫儿小忆 78', url:'https://p.inari.site/usr/2927/6ac362aa980d7.webp', anim: true },
        { name:'猫儿小忆 79', url:'https://p.inari.site/usr/2927/6ac362ab7cb83.webp', anim: true },
        { name:'猫儿小忆 80', url:'https://p.inari.site/usr/2927/6ac362ac8d08f.webp', anim: true },
        { name:'猫儿小忆 81', url:'https://p.inari.site/usr/2927/6ac362acc92b6.webp', anim: true },
        { name:'猫儿小忆 82', url:'https://p.inari.site/usr/2927/6ac362acd0f5e.webp', anim: true },
        { name:'猫儿小忆 83', url:'https://p.inari.site/usr/2927/6ac362ae0a3e9.webp', anim: true },
        { name:'猫儿小忆 84', url:'https://p.inari.site/usr/2927/6ac362ae71a7f.webp', anim: true },
        { name:'猫儿小忆 85', url:'https://p.inari.site/usr/2927/6ac362af54aa3.webp', anim: true },
        { name:'猫儿小忆 86', url:'https://p.inari.site/usr/2927/6ac362afaa16f.webp', anim: true },
        { name:'猫儿小忆 87', url:'https://p.inari.site/usr/2927/6ac362b041608.webp', anim: true },
        { name:'猫儿小忆 88', url:'https://p.inari.site/usr/2927/6ac362b0943fb.webp', anim: true },
        { name:'猫儿小忆 89', url:'https://p.inari.site/usr/2927/6ac362b109377.webp', anim: true },
        { name:'猫儿小忆 90', url:'https://p.inari.site/usr/2927/6ac362b15853e.webp', anim: true },
        { name:'猫儿小忆 91', url:'https://p.inari.site/usr/2927/6ac362b3285a4.webp', anim: true },
        { name:'猫儿小忆 92', url:'https://p.inari.site/usr/2927/6ac3676ecf480.webp', anim: true },
        { name:'猫儿小忆 93', url:'https://p.inari.site/usr/2927/6ac3676ed3222.webp', anim: true },
        { name:'猫儿小忆 94', url:'https://p.inari.site/usr/2927/6ac3676f04c0e.webp', anim: true },
        { name:'猫儿小忆 95', url:'https://p.inari.site/usr/2927/6ac3676f77a24.webp', anim: true },
        { name:'猫儿小忆 96', url:'https://p.inari.site/usr/2927/6ac3676fc9a42.webp', anim: true },
        { name:'猫儿小忆 97', url:'https://p.inari.site/usr/2927/6ac367703d558.webp', anim: true },
        { name:'猫儿小忆 98', url:'https://p.inari.site/usr/2927/6ac367720a74f.webp', anim: true },
        { name:'猫儿小忆 99', url:'https://p.inari.site/usr/2927/6ac367728253f.webp', anim: true },
        { name:'猫儿小忆 100', url:'https://p.inari.site/usr/2927/6ac36772e5f7f.webp', anim: true },
        { name:'猫儿小忆 101', url:'https://p.inari.site/usr/2927/6ac36773526ca.webp', anim: true },
        { name:'猫儿小忆 102', url:'https://p.inari.site/usr/2927/6ac36773b1371.webp', anim: true },
        { name:'猫儿小忆 103', url:'https://p.inari.site/usr/2927/6ac3677414876.webp', anim: true },
        { name:'猫儿小忆 104', url:'https://p.inari.site/usr/2927/6ac3677475447.webp', anim: true },
        { name:'猫儿小忆 105', url:'https://p.inari.site/usr/2927/6ac36775c5a77.webp', anim: true },
        { name:'猫儿小忆 106', url:'https://p.inari.site/usr/2927/6ac3677634d2e.webp', anim: true },
        { name:'猫儿小忆 107', url:'https://p.inari.site/usr/2927/6ac367769fff8.webp', anim: true },
        { name:'猫儿小忆 108', url:'https://p.inari.site/usr/2927/6ac367770f8e0.webp', anim: true },
        { name:'猫儿小忆 109', url:'https://p.inari.site/usr/2927/6ac3677781a15.webp', anim: true },
        { name:'猫儿小忆 110', url:'https://p.inari.site/usr/2927/6ac367785b425.webp', anim: true },
        { name:'猫儿小忆 111', url:'https://p.inari.site/usr/2927/6ac367795473d.webp', anim: true },
        { name:'猫儿小忆 112', url:'https://p.inari.site/usr/2927/6ac36779ae238.webp', anim: true },
        { name:'猫儿小忆 113', url:'https://p.inari.site/usr/2927/6ac3677a45798.webp', anim: true },
        { name:'猫儿小忆 114', url:'https://p.inari.site/usr/2927/6ac3677a6b8b9.webp', anim: true },
        { name:'猫儿小忆 115', url:'https://p.inari.site/usr/2927/6ac3677aec03b.webp', anim: true },
        { name:'猫儿小忆 116', url:'https://p.inari.site/usr/2927/6ac3677b6646b.webp', anim: true },
        { name:'猫儿小忆 117', url:'https://p.inari.site/usr/2927/6ac3677beb919.webp', anim: true },
        { name:'猫儿小忆 118', url:'https://p.inari.site/usr/2927/6ac3677e05976.webp', anim: true },
        { name:'猫儿小忆 119', url:'https://p.inari.site/usr/2927/6ac3677e3d66e.webp', anim: true },
        { name:'猫儿小忆 120', url:'https://p.inari.site/usr/2927/6ac3677e9570e.webp', anim: true },
        { name:'猫儿小忆 121', url:'https://p.inari.site/usr/2927/6ac3677f0fe5b.webp', anim: true },
    ];
    const PACK_SHISHOUXIANG = [
        { name:'是,首相 1', url:'https://p.inari.site/usr/2927/6ac368b782af0.png' },
        { name:'是,首相 2', url:'https://p.inari.site/usr/2927/6ac368b83e708.png' },
        { name:'是,首相 3', url:'https://p.inari.site/usr/2927/6ac368b91b7f2.png' },
        { name:'是,首相 4', url:'https://p.inari.site/usr/2927/6ac368bc1821c.png' },
        { name:'是,首相 5', url:'https://p.inari.site/usr/2927/6ac368bcc530c.png' },
        { name:'是,首相 6', url:'https://p.inari.site/usr/2927/6ac368bd2c2bd.png' },
        { name:'是,首相 7', url:'https://p.inari.site/usr/2927/6ac368bd8f2c3.png' },
        { name:'是,首相 8', url:'https://p.inari.site/usr/2927/6ac368be0348c.png' },
        { name:'是,首相 9', url:'https://p.inari.site/usr/2927/6ac368bf1e18e.png' },
        { name:'是,首相 10', url:'https://p.inari.site/usr/2927/6ac368bf1e1c2.png' },
        { name:'是,首相 11', url:'https://p.inari.site/usr/2927/6ac368bf46164.png' },
        { name:'是,首相 12', url:'https://p.inari.site/usr/2927/6ac368bfab8f6.png' },
        { name:'是,首相 13', url:'https://p.inari.site/usr/2927/6ac368c0ed4ed.png' },
        { name:'是,首相 14', url:'https://p.inari.site/usr/2927/6ac368c192643.png' },
        { name:'是,首相 15', url:'https://p.inari.site/usr/2927/6ac368c21f7b7.png' },
        { name:'是,首相 16', url:'https://p.inari.site/usr/2927/6ac368c27d4d8.png' },
        { name:'是,首相 17', url:'https://p.inari.site/usr/2927/6ac368c3498ae.png' },
        { name:'是,首相 18', url:'https://p.inari.site/usr/2927/6ac368c3a5a48.png' },
        { name:'是,首相 19', url:'https://p.inari.site/usr/2927/6ac368c42fe88.png' },
        { name:'是,首相 20', url:'https://p.inari.site/usr/2927/6ac368c5b68db.png' },
        { name:'是,首相 21', url:'https://p.inari.site/usr/2927/6ac368c63a843.png' },
        { name:'是,首相 22', url:'https://p.inari.site/usr/2927/6ac368c6cdeeb.png' },
        { name:'是,首相 23', url:'https://p.inari.site/usr/2927/6ac368c77d8bd.png' },
        { name:'是,首相 24', url:'https://p.inari.site/usr/2927/6ac368c7ceb3b.png' },
        { name:'是,首相 25', url:'https://p.inari.site/usr/2927/6ac368c893a0e.png' },
        { name:'是,首相 26', url:'https://p.inari.site/usr/2927/6ac368c9ed228.png' },
        { name:'是,首相 27', url:'https://p.inari.site/usr/2927/6ac368ca78e31.png' },
        { name:'是,首相 28', url:'https://p.inari.site/usr/2927/6ac368cb29796.png' },
        { name:'是,首相 29', url:'https://p.inari.site/usr/2927/6ac368cbae75a.png' },
        { name:'是,首相 30', url:'https://p.inari.site/usr/2927/6ac368ccc0572.png' },
        { name:'是,首相 31', url:'https://p.inari.site/usr/2927/6ac3698588029.png' },
        { name:'是,首相 32', url:'https://p.inari.site/usr/2927/6ac369858f9ca.png' },
        { name:'是,首相 33', url:'https://p.inari.site/usr/2927/6ac36985e00c4.png' },
        { name:'是,首相 34', url:'https://p.inari.site/usr/2927/6ac369864a501.png' },
        { name:'是,首相 35', url:'https://p.inari.site/usr/2927/6ac36986ab4f2.png' },
        { name:'是,首相 36', url:'https://p.inari.site/usr/2927/6ac36987086df.png' },
        { name:'是,首相 37', url:'https://p.inari.site/usr/2927/6ac36987c6953.png' },
        { name:'是,首相 38', url:'https://p.inari.site/usr/2927/6ac369881a3f7.png' },
        { name:'是,首相 39', url:'https://p.inari.site/usr/2927/6ac369893c25e.png' },
        { name:'是,首相 40', url:'https://p.inari.site/usr/2927/6ac369899d3e7.png' },
        { name:'是,首相 41', url:'https://p.inari.site/usr/2927/6ac3698a2cc3c.png' },
        { name:'是,首相 42', url:'https://p.inari.site/usr/2927/6ac3698a9580f.png' },
        { name:'是,首相 43', url:'https://p.inari.site/usr/2927/6ac3698b0b3c2.png' },
        { name:'是,首相 44', url:'https://p.inari.site/usr/2927/6ac3698b5cccd.png' },
        { name:'是,首相 45', url:'https://p.inari.site/usr/2927/6ac3698c3b0a9.png' },
        { name:'是,首相 46', url:'https://p.inari.site/usr/2927/6ac3698c7283d.png' },
        { name:'是,首相 47', url:'https://p.inari.site/usr/2927/6ac3698dba54c.png' },
        { name:'是,首相 48', url:'https://p.inari.site/usr/2927/6ac3698e1d016.png' },
        { name:'是,首相 49', url:'https://p.inari.site/usr/2927/6ac3698e792ce.png' },
        { name:'是,首相 50', url:'https://p.inari.site/usr/2927/6ac3698ed245e.png' },
        { name:'是,首相 51', url:'https://p.inari.site/usr/2927/6ac3698f31807.png' },
        { name:'是,首相 52', url:'https://p.inari.site/usr/2927/6ac3698f7b681.png' },
        { name:'是,首相 53', url:'https://p.inari.site/usr/2927/6ac36990a48b7.png' },
        { name:'是,首相 54', url:'https://p.inari.site/usr/2927/6ac369911ed2a.png' },
        { name:'是,首相 55', url:'https://p.inari.site/usr/2927/6ac3699173a0a.png' },
        { name:'是,首相 56', url:'https://p.inari.site/usr/2927/6ac3699237c0b.png' },
        { name:'是,首相 57', url:'https://p.inari.site/usr/2927/6ac3699294a0d.png' },
        { name:'是,首相 58', url:'https://p.inari.site/usr/2927/6ac36992ec9b7.png' },
        { name:'是,首相 59', url:'https://p.inari.site/usr/2927/6ac369943f480.png' },
        { name:'是,首相 60', url:'https://p.inari.site/usr/2927/6ac3699529e69.png' },
        { name:'是,首相 61', url:'https://p.inari.site/usr/2927/6ac36a129e58f.png' },
        { name:'是,首相 62', url:'https://p.inari.site/usr/2927/6ac36a13a31d9.png' },
        { name:'是,首相 63', url:'https://p.inari.site/usr/2927/6ac36a141a334.png' },
        { name:'是,首相 64', url:'https://p.inari.site/usr/2927/6ac36a14b301b.png' },
        { name:'是,首相 65', url:'https://p.inari.site/usr/2927/6ac36a154b301.png' },
        { name:'是,首相 66', url:'https://p.inari.site/usr/2927/6ac36a15a3b80.png' },
        { name:'是,首相 67', url:'https://p.inari.site/usr/2927/6ac36a1713ff0.png' },
        { name:'是,首相 68', url:'https://p.inari.site/usr/2927/6ac36a178724e.png' },
        { name:'是,首相 69', url:'https://p.inari.site/usr/2927/6ac36a17c99fe.png' },
        { name:'是,首相 70', url:'https://p.inari.site/usr/2927/6ac36a1808846.png' },
        { name:'是,首相 71', url:'https://p.inari.site/usr/2927/6ac36a18c09b5.png' },
        { name:'是,首相 72', url:'https://p.inari.site/usr/2927/6ac36a192b4d3.png' },
        { name:'是,首相 73', url:'https://p.inari.site/usr/2927/6ac36a19ae16e.png' },
        { name:'是,首相 74', url:'https://p.inari.site/usr/2927/6ac36a1b1e04f.png' },
        { name:'是,首相 75', url:'https://p.inari.site/usr/2927/6ac36a1b689b1.png' },
        { name:'是,首相 76', url:'https://p.inari.site/usr/2927/6ac36a1bc963f.png' },
        { name:'是,首相 77', url:'https://p.inari.site/usr/2927/6ac36a1c3db93.png' },
        { name:'是,首相 78', url:'https://p.inari.site/usr/2927/6ac36a1c99f0e.png' },
        { name:'是,首相 79', url:'https://p.inari.site/usr/2927/6ac36a1d15b55.png' },
        { name:'是,首相 80', url:'https://p.inari.site/usr/2927/6ac36a1e40f02.png' },
        { name:'是,首相 81', url:'https://p.inari.site/usr/2927/6ac36a1e8ea49.png' },
        { name:'是,首相 82', url:'https://p.inari.site/usr/2927/6ac36a1ee2cf4.png' },
        { name:'是,首相 83', url:'https://p.inari.site/usr/2927/6ac36a1f3b4f3.png' },
        { name:'是,首相 84', url:'https://p.inari.site/usr/2927/6ac36a1f8d762.png' },
        { name:'是,首相 85', url:'https://p.inari.site/usr/2927/6ac36a1fdae88.png' },
        { name:'是,首相 86', url:'https://p.inari.site/usr/2927/6ac36a2053557.png' },
        { name:'是,首相 87', url:'https://p.inari.site/usr/2927/6ac36a215f59e.png' },
        { name:'是,首相 88', url:'https://p.inari.site/usr/2927/6ac36a21be121.png' },
        { name:'是,首相 89', url:'https://p.inari.site/usr/2927/6ac36a221b26b.png' },
        { name:'是,首相 90', url:'https://p.inari.site/usr/2927/6ac36a227dbe9.png' },
        { name:'是,首相 91', url:'https://p.inari.site/usr/2927/6ac36a539e468.png' },
        { name:'是,首相 92', url:'https://p.inari.site/usr/2927/6ac36a54b29c9.png' },
        { name:'是,首相 93', url:'https://p.inari.site/usr/2927/6ac36a5555560.png' },
        { name:'是,首相 94', url:'https://p.inari.site/usr/2927/6ac36a55ce647.png' },
        { name:'是,首相 95', url:'https://p.inari.site/usr/2927/6ac36a569c65a.png' },
        { name:'是,首相 96', url:'https://p.inari.site/usr/2927/6ac36a56eb3c1.png' },
        { name:'是,首相 97', url:'https://p.inari.site/usr/2927/6ac36a574fcaa.png' },
        { name:'是,首相 98', url:'https://p.inari.site/usr/2927/6ac36a589517c.png' },
        { name:'是,首相 99', url:'https://p.inari.site/usr/2927/6ac36a592d71f.png' },
        { name:'是,首相 100', url:'https://p.inari.site/usr/2927/6ac36a59a3b86.png' },
    ];

    /* ==================== 用户自备套系(面板可粘贴)· 2.55.11 ==================== */
    /* ==================== 自备表情 / 贴纸(2.55.11 起;2.55.15 拆成左右两格) ====================
       设置面板里给两格文本框,粘进来的就是套系本身 —— 一个套系 = 一行套系名 + 若干行
       图床直链,套系之间空一行。左格默认落 emoji 页、右格默认落贴纸页;名字前写
       [emoji] / [贴纸] 仍可临时改道(老用户手里那些带 [emoji] 前缀的文本照旧管用)。
       ★ 解析刻意放在文件作用域、不等表情面板:模块 3(图片阅读优化)比模块 14
         (表情面板)先跑,而「这张图是不是脚本自己贴的表情 / 贴纸」要靠 SPP_PACK_URLS
         认 —— 这些链接必须在模块 3 启动前就进集合,否则用户粘的贴纸会被「图片居中显示」
         当成帖子里的普通图抓去居中(2.55.9 刚修的就是这一类)。
       ★ 一律宽松解析、绝不抛错:粘进来的是用户手写的东西,缺名字、缺链接、贴了非图片
         地址都是常态 —— 能认多少认多少,认不出的行连同行号交给面板显示。
       ★★ 2.55.15(用户:「如果 emoji 和贴纸共用一个自定义栏框 … 会出现两个都无法检索到
          的情况。我建议分为左右两栏」)两处结构性改动:
           ① 存储键一拆为二:spp_custom_emoji / spp_custom_sticker(旧键 spp_custom_packs
              不再渲染,内容由下面的 sppMigratePacksSplit() 按页拆进两枚新键);
           ② 去重表**按页各一张** —— 2.55.11 起那张 seen 是全局的,同一张图既当 emoji
              又当贴纸时,第二处会被判「和前面重复」整行丢掉,那一套随即被判「一个链接都
              没跟」一并删掉 ⇒ 用户看到的就是「两个都搜不到」。
       ★ 口径仍是「同一页里不重复」:左格写过的链接,右格再写一次仍算重复(写重了没意义)。 */
    const SPP_CUSTOM_EMOJI_KEY = 'spp_custom_emoji';
    const SPP_CUSTOM_STICKER_KEY = 'spp_custom_sticker';
    /* 旧键:2.55.15 起只剩「一次性迁移时读一次」这一个用途,面板上不再有它这一格。 */
    const SPP_CUSTOM_KEY = 'spp_custom_packs';
    const SPP_CUSTOM_IMG_RE = /^https?:\/\/[^\s<>"']+\.(png|jpe?g|webp|gif|bmp|avif)(\?[^\s]*)?$/i;
    /* 改道标记:[emoji] / [表情] → emoji 页,[贴纸] / [sticker] → 贴纸页。 */
    const SPP_CUSTOM_PAGE_TAGS = { emoji: 'emoji', '表情': 'emoji', sticker: 'sticker', '贴纸': 'sticker' };
    /* 两格共用的累加器:seen 按页各一张(见上)。
       ★ empty 的初值是 true,两格都空才保持 true —— 单格解析时它就等于「这一格是空的」。 */
    function sppNewPackAcc() {
        return {
            emoji: [], sticker: [], packs: 0, urls: 0, bad: [], empty: true,
            seen: { emoji: new Set(), sticker: new Set() }
        };
    }
    /* 扫一格文本、往 acc 里塞。defaultPage = 这一格默认落哪一页。 */
    function sppPackScan(raw, defaultPage, acc) {
        const text = String(raw === null || raw === undefined ? '' : raw);
        if (!text.trim()) return;
        acc.empty = false;
        let cur = null;
        text.split(/\r?\n/).forEach(function (line, i) {
            const t = line.trim();
            if (!t) { cur = null; return; }                 // 空行 = 一个套系到此为止
            if (t.slice(0, 4).toLowerCase() === 'http') {
                if (!cur) { acc.bad.push({ n: i + 1, why: '上面没有套系名' }); return; }
                /* ★★ 2.55.14(用户:「如果我想为某张贴纸设置弹出的 UI 文本,该如何配置?」):
                   一行可以只写「图片直链」,也可以写成「图片直链 + 一个空白 + 提示文本」。
                   地址里不可能有空白,所以在**第一个空白处切开**是安全的 —— 带 ?query 的地址
                   在切之前整段属于地址那一边,不会被切坏。
                   ★ 没写提示文本的条目照旧退回「套系名 + 序号」;两种写法可以在一套里混用。 */
                const sp = t.search(/\s/);
                const url = sp < 0 ? t : t.slice(0, sp);
                const label = sp < 0 ? '' : t.slice(sp).trim();
                if (!SPP_CUSTOM_IMG_RE.test(url)) { acc.bad.push({ n: i + 1, why: '不是图片直链' }); return; }
                if (acc.seen[cur.page].has(url)) { acc.bad.push({ n: i + 1, why: '这一页里和前面重复' }); return; }
                acc.seen[cur.page].add(url);
                /* 动图只认扩展名:.gif 必动,.webp 可能是动图也可能不是 —— 拿不准的一律
                   按「可能动」处理,交给网格的静止首帧那道工序(静态 webp 画出来与原来一模一样)。 */
                cur.list.push({ name: label, url: url, label: label, anim: /\.(gif|webp)(\?|$)/i.test(url) });
                return;
            }
            // 名字行:默认落本格那一页,带 [emoji] / [贴纸] 前缀的按前缀改道
            let name = t, page = defaultPage;
            const m = t.match(/^\[([^\]]{1,12})\]\s*(.*)$/);
            if (m) {
                const tag = m[1].trim().toLowerCase();
                if (SPP_CUSTOM_PAGE_TAGS[tag]) page = SPP_CUSTOM_PAGE_TAGS[tag];
                name = (m[2] || '').trim() || m[1].trim();
            }
            cur = { key: 'spp-custom-' + page + '-' + acc[page].length, page: page, name: name, list: [] };
            acc[page].push(cur);
        });
    }
    /* 收口:丢掉空套系、给没写提示文本的条目补「套系名 + 序号」、算两个总数。 */
    function sppPackFinish(acc) {
        // 一个链接都没跟的套系不留:留着会在选择区挂一枚 0 张的空胶囊
        ['emoji', 'sticker'].forEach(function (pg) {
            acc[pg] = acc[pg].filter(function (p) { return p.list.length; });
            acc[pg].forEach(function (p) {
                /* ★ 2.55.14:自己写了提示文本的照自己的走,没写才退回「套系名 + 序号」。 */
                p.list.forEach(function (e, i) { e.name = e.label || (p.name + ' ' + (i + 1)); });
            });
        });
        acc.packs = acc.emoji.length + acc.sticker.length;
        acc.urls = acc.seen.emoji.size + acc.seen.sticker.size;
        return acc;
    }
    /* 单格解析(面板那两个小结各算各的):defaultPage 是这一格的默认归属。 */
    function sppParseCustomPacks(raw, defaultPage) {
        const acc = sppNewPackAcc();
        sppPackScan(raw, defaultPage || 'sticker', acc);
        return sppPackFinish(acc);
    }
    /* 两格合并(脚本运行用):左格默认 emoji、右格默认 sticker,共用同一份按页去重表。
       ★ 返回值形状与 2.55.11 的单格解析**一字不差**(emoji / sticker / packs / urls / bad /
         empty)—— 模块 3 与模块 14 都是照这个形状取用的,改形状要同步改那边。 */
    function sppParseCustomPacksAll(emojiRaw, stickerRaw) {
        const acc = sppNewPackAcc();
        sppPackScan(emojiRaw, 'emoji', acc);
        sppPackScan(stickerRaw, 'sticker', acc);
        return sppPackFinish(acc);
    }
    /* ★★ 2.55.15:旧键 → 两枚新键的一次性迁移。
       为什么必须做:面板的保存循环只写**面板上有的行** —— 老机器上那份自备套系全躺在
         spp_custom_packs 里,而本版起那一格不再渲染 ⇒ 不迁的话升级后两格全空,
         表情栏里的自备套系凭空消失(用户会当成「新版把我的东西弄丢了」)。
       ★ 拆法是**重新序列化**而不是按行搬运:借的还是那支解析器(与运行时完全同一条口径),
         [emoji] 套系进左键、其余进右键,逐套写成「套系名 + 若干图直链(带提示文本的照旧
         跟在地址后面)」。写坏的行本来就不参与运行,这里也不会被搬进新键。
       ★ 旧键刻意不删 —— 与 2.55.13 留 spp_self_img 同一口径:真迁错了还能从存储里翻回来。
       ★ 只在两枚新键都为空时才迁:用户已经在新格子里写过东西(或本版已经迁过一次)
         就不该被旧文本盖掉。迁移只跑一次(标记键,同 spp_mig_emojitip_on 那一套)。 */
    function sppMigratePacksSplit() {
        const MIG = 'spp_mig_packsplit_on';
        if (GM_getValue(MIG, false)) return;
        const oldRaw = GM_getValue(SPP_CUSTOM_KEY, '');
        const hasNew = String(GM_getValue(SPP_CUSTOM_EMOJI_KEY, '') || '') ||
                       String(GM_getValue(SPP_CUSTOM_STICKER_KEY, '') || '');
        if (typeof oldRaw === 'string' && oldRaw.trim() && !hasNew) {
            try {
                const parsed = sppParseCustomPacks(oldRaw, 'sticker');
                const dump = function (arr) {
                    return arr.map(function (p) {
                        return p.name + '\n' + p.list.map(function (e) {
                            return e.url + (e.label ? ' ' + e.label : '');
                        }).join('\n');
                    }).join('\n\n');
                };
                GM_setValue(SPP_CUSTOM_EMOJI_KEY, dump(parsed.emoji));
                GM_setValue(SPP_CUSTOM_STICKER_KEY, dump(parsed.sticker));
            } catch (e) { /* 迁移失败不该拦住页面:旧键原封不动,用户手抄一次即可 */ }
        }
        GM_setValue(MIG, true);
    }
    sppMigratePacksSplit();
    /* 解析一次、两处共用:模块 3 的 SPP_PACK_URLS(下面几行)与模块 14 的两张数据表。
       ★ 面板改完点「保存并刷新」⇒ 页面重载 ⇒ 这里重新解析,不会留旧值。
       ★ 迁移就在上面那一行、本行之前完成 —— 本版第一次跑到时读到的已经是拆好的两格。 */
    const SPP_CUSTOM_PACKS = sppParseCustomPacksAll(
        SPP_CONFIG.text(SPP_CUSTOM_EMOJI_KEY, ''),
        SPP_CONFIG.text(SPP_CUSTOM_STICKER_KEY, '')
    );

    /* ==================== 自定义图床(2.55.14) ====================
       用户:「在设置面板中增加『图床跳转』功能,允许用户自定义增加更多『图床』UI 选项。」
       一行一条,写成「名称 首页地址 认链域名,认链域名」:
         · 名称     —— 标符里写的那几个字(【名称】)。必填。
         · 首页地址 —— 「图床 ▾」下拉里点一下要跳去的页。必填、要带 http(s):// 头。
         · 认链域名 —— 可选。不写就按首页地址的域名推;写了就按后缀认子域
                       (一条链接的 host 等于它、或以 .它 结尾都算命中)。
       # 开头的行与空行当注释跳过;认不出的行连同行号交给面板显示,绝不抛错。
       ★ 解析时机:文件作用域(模块 13 与设置面板都要用)。面板改完点「保存并刷新」
         ⇒ 页面重载 ⇒ 这里重新解析,不会留旧值。 */
    const SPP_HOST_KEY = 'spp_host_extra';
    function sppParseCustomHosts(raw) {
        const out = { list: [], bad: [], empty: true };
        const text = String(raw === null || raw === undefined ? '' : raw);
        if (!text.trim()) return out;
        out.empty = false;
        const seen = new Set();
        text.split(/\r?\n/).forEach(function (line, i) {
            const t = line.trim();
            if (!t || t.slice(0, 1) === '#') return;
            const tok = t.split(/\s+/);
            if (tok.length < 2) { out.bad.push({ n: i + 1, why: '要写「名称 首页地址」两段' }); return; }
            /* 名字要能塞进【…】里:方括号与一切空白 / 控制字符都去掉。
               名字里带 【 】会把标注结果切成两半(标符就是 【名称】,2.53.96 定的口径)。 */
            const name = tok[0].replace(/[\[\]【】]/g, '').replace(/[\s\u0000-\u001f]/g, '').slice(0, 24);
            if (!name) { out.bad.push({ n: i + 1, why: '名称是空的' }); return; }
            const site = tok[1];
            if (!/^https?:\/\//i.test(site)) { out.bad.push({ n: i + 1, why: '首页地址要带 http:// 或 https://' }); return; }
            let host0 = '';
            try { host0 = new URL(site).hostname.toLowerCase().replace(/^www\./, ''); } catch (e) { host0 = ''; }
            if (!host0) { out.bad.push({ n: i + 1, why: '首页地址解析不出域名' }); return; }
            /* 第三段起是认链域名,逗号 / 空白 / 顿号都当分隔符;不写就退回首页那个域名。 */
            let hosts = tok.slice(2).join(' ').split(/[\s,,、]+/)
                .map(function (d) {
                    return d.trim().toLowerCase()
                        .replace(/^https?:\/\//, '').replace(/^www\./, '').replace(/\/.*$/, '');
                })
                .filter(Boolean);
            if (!hosts.length) hosts = [host0];
            const dupKey = name + '|' + hosts.join(',');
            if (seen.has(dupKey)) { out.bad.push({ n: i + 1, why: '和前面重复' }); return; }
            seen.add(dupKey);
            out.list.push({ name: name, site: site, hosts: hosts });
        });
        return out;
    }
    const SPP_CUSTOM_HOSTS = sppParseCustomHosts(SPP_CONFIG.text(SPP_HOST_KEY, ''));

    /* 这几套的全部图床地址(脚本自带 453 条 + 面板里粘进来的)。模块 3 用它认
       「这是脚本贴的表情 / 贴纸」,模块 14 用它铺格子。用 Set 而不是每次遍历数组:
       模块 3 每张帖子图都要问一次。 */
    const SPP_PACK_URLS = new Set();
    [PACK_EMOJI_OMEGA, PACK_STICKER_OMEGA, PACK_DAIDAIWEI, PACK_MAOERXIAOYI, PACK_SHISHOUXIANG]
        .forEach(function (arr) { arr.forEach(function (e) { SPP_PACK_URLS.add(e.url); }); });
    /* ★★ 2.55.11:面板里粘进来的自备套系一并认 —— 否则用户粘的贴纸会被模块 3 当成
       「帖子里的普通图」(居中 / 小图豁免全按普通图走,正是 2.55.9 修掉的那一类)。 */
    ['emoji', 'sticker'].forEach(function (pg) {
        SPP_CUSTOM_PACKS[pg].forEach(function (p) {
            p.list.forEach(function (e) { SPP_PACK_URLS.add(e.url); });
        });
    });

    /* 这条链接是不是脚本自己塞进帖子的表情 / 贴纸(含 emoji 页那套)。
       ★ 按**精确 URL** 认,不按域名、也不按尺寸 —— 同一个图床目录下还有用户自己
         发的普通图,按域名认会一并误伤。 */
    function isStickerUrl(u) {
        if (!u) return false;
        try { return SPP_PACK_URLS.has(new URL(u, location.href).href); } catch (e) { return false; }
    }
    // ==================== 3. PC 图片阅读优化(仅 read.php) ====================
    function initImageViewer() {
        if (!WANTS.readOnly) return;

        // ★ 2.52.93:本模块原先自己持有一份 DEFAULTS 与一套 loadSettings —— 现在统一走
        //   SPP_CONFIG.imgSettings()(GM 存储 → 内置默认 + 区间收敛)。存储键用模块级的
        //   IMG_SETTINGS_KEY:页内面板 / 本模块 / 读数层三处指向同一个键。
        function loadSettings() { return SPP_CONFIG.imgSettings(); }
        function saveSettings(s) { GM_setValue(IMG_SETTINGS_KEY, JSON.stringify(s)); }

        let settings = loadSettings();
        let dynamicStyle = null;
        let imgUid = 0;
        let observerStarted = false;

        // ---------- 动态样式 ----------
        function applyDynamicStyles() {
            if (!dynamicStyle) {
                dynamicStyle = document.createElement('style');
                appendToHeadSafe(dynamicStyle);
            }
            dynamicStyle.textContent = `
                .tpc_content img.spp-img {
                    /* ★★ 2.55.9:居中那两条从这条基类里搬出去,改由下面两条条件规则分管 ——
                       基类留着,max-height / 缩放查看 / 图片索引这些照旧。 */
                    max-height:${settings.maxImgHeight}vh !important;
                    max-width:${settings.maxImgWidth}% !important;
                    object-fit:contain !important;
                    cursor:zoom-in !important;
                    transition:opacity 0.2s;
                }
                .tpc_content img.spp-img:hover { opacity:0.85; }
                /* ★★ 2.55.9:①「图片居中显示」只管**不是**脚本表情 / 贴纸的图(:not 那条)
                   ②脚本的表情 / 贴纸由「贴纸居中显示」单独管,默认关 = 不居中(保持行内原样)。
                   两条互不干扰:全局开 + 贴纸关 ⇒ 只有贴纸不居中;全局关 + 贴纸开 ⇒ 只有贴纸居中。 */
                ${settings.centerImg ? '.tpc_content img.spp-img:not(.spp-sticker-img) { display:block !important; margin:0 auto !important; }' : ''}
                ${SPP_CONFIG.bool('spp_sticker_center', false) ? '.tpc_content img.spp-img.spp-sticker-img { display:block !important; margin:0 auto !important; }' : ''}
                .spp-thumb { width:${settings.thumbSize}px !important; height:${settings.thumbSize}px !important; }
            `;
        }

        // ---------- 静态样式(灯箱 + 设置面板) ----------
        function injectStaticStyles() {
            GM_addStyle(`
                .spp-overlay { display:none; position:fixed; inset:0; z-index:99999; background:rgba(0,0,0,0.92); flex-direction:column; align-items:center; justify-content:center; }
                .spp-overlay.spp-open { display:flex; }
                .spp-main { flex:1; display:flex; align-items:center; justify-content:center; width:100%; min-height:0; position:relative; overflow:hidden; }
                .spp-main img { max-width:95vw; max-height:calc(100vh - 100px); object-fit:contain; user-select:none; transition:opacity 0.15s; }
                .spp-arrow { position:absolute; top:50%; transform:translateY(-50%); width:48px; height:80px; background:rgba(255,255,255,0.1); border:none; color:#fff; font-size:28px; cursor:pointer; border-radius:6px; z-index:2; display:flex; align-items:center; justify-content:center; transition:background 0.2s; }
                .spp-arrow:hover { background:rgba(255,255,255,0.25); }
                .spp-arrow-left { left:12px; }
                .spp-arrow-right { right:12px; }
                .spp-close { position:absolute; top:10px; right:16px; width:36px; height:36px; background:rgba(255,255,255,0.12); border:none; color:#fff; font-size:22px; cursor:pointer; border-radius:50%; z-index:3; display:flex; align-items:center; justify-content:center; }
                .spp-close:hover { background:rgba(255,255,255,0.3); }
                .spp-counter { position:absolute; top:14px; left:16px; color:rgba(255,255,255,0.7); font-size:14px; z-index:3; user-select:none; }
                .spp-thumbs { display:flex; gap:4px; padding:6px 10px; overflow-x:auto; max-width:100vw; flex-shrink:0; scrollbar-width:thin; scrollbar-color:rgba(255,255,255,0.3) transparent; }
                .spp-thumbs::-webkit-scrollbar { height:6px; }
                .spp-thumbs::-webkit-scrollbar-thumb { background:rgba(255,255,255,0.3); border-radius:3px; }
                .spp-thumb { flex-shrink:0; object-fit:cover; border:2px solid transparent; border-radius:4px; cursor:pointer; opacity:0.5; transition:opacity 0.15s, border-color 0.15s; }
                .spp-thumb:hover { opacity:0.8; }
                .spp-thumb.spp-active { border-color:#e74c3c; opacity:1; }
            `);
        }

        // ---------- 图片处理 ----------
        function patchImg(img) {
            if (img.dataset.sppUid) return;
            if (!isContentImage(img)) return;

            img.dataset.sppUid = String(++imgUid);
            img.classList.add('spp-img');

            const realSrc = getImgSrc(img);
            if (realSrc && (!img.src || img.src === 'about:blank' || img.src.startsWith('data:'))) {
                img.src = realSrc;
            }

            /* ★★ 2.55.9(用户:「对于功能:图片居中显示,这个不对脚本的 emoji、贴纸生效;
               并且增加一个设置按钮:决定贴纸是否居中(默认关闭,即不居中)」):
               命中脚本那五套图床地址的,单独打一枚 .spp-sticker-img。它**只用来豁免
               「图片居中显示」**那条规则,.spp-img 照旧保留 —— 缩放查看 / 全贴图片索引
               这些依赖 .spp-img 的功能一个字都不受影响。 */
            if (isStickerUrl(realSrc)) img.classList.add('spp-sticker-img');

            // 清除原始事件和尺寸限制
            img.removeAttribute('onload');
            img.onload = null;
            img.removeAttribute('onclick');
            img.onclick = null;
            img.removeAttribute('width');
            img.style.removeProperty('width');

            // 加载后复查是否为小图(表情等)
            /* ★★ 2.55.9 修一处真错:原来整段写在 if (!img.complete) 里面 —— 图**已经
               缓存**时这个分支根本不进,于是 30×30 的 emoji 顶着 .spp-img 被居中,
               而没缓存时又被摘掉 ⇒ 同一个小表情「忽居中忽不居中」。用户报的就是这个。
               现在两种时序都判一次;带 .spp-sticker-img 的图直接跳过尺寸猜测 ——
               它按 URL 精确豁免(见 patchImg 与 applyDynamicStyles)。 */
            function dropSmallImage() {
                if (img.classList.contains('spp-sticker-img')) return;
                if (img.naturalWidth > 0 && img.naturalWidth < 80 && img.naturalHeight > 0 && img.naturalHeight < 80) {
                    img.classList.remove('spp-img');
                    delete img.dataset.sppUid;
                }
            }
            if (img.complete) dropSmallImage();
            else img.addEventListener('load', dropSmallImage, { once: true });
        }

        function processPostImages() {
            document.querySelectorAll('.tpc_content img').forEach(patchImg);
        }

        // ---------- MutationObserver ----------
        function startObserver() {
            if (observerStarted) return;
            observerStarted = true;

            const target = document.getElementById('main') || document.body;
            new MutationObserver(mutations => {
                for (const m of mutations) {
                    if (m.addedNodes.length) {
                        m.addedNodes.forEach(node => {
                            if (node.nodeType !== 1) return;
                            if (node.tagName === 'IMG' && node.closest('.tpc_content')) {
                                patchImg(node);
                            } else if (node.querySelectorAll) {
                                node.querySelectorAll('.tpc_content img').forEach(patchImg);
                            }
                        });
                    }
                    if (m.type === 'attributes' && m.target.tagName === 'IMG') {
                        const img = m.target;
                        if (img.closest('.tpc_content') && !img.dataset.sppUid) {
                            patchImg(img);
                        }
                    }
                }
            }).observe(target, {
                childList: true, subtree: true, attributes: true,
                attributeFilter: ['src', 'data-src', 'data-original', 'data-lazy-src']
            });
        }

        // ---------- 灯箱 ----------
        function initLightbox() {
            const overlay = document.createElement('div');
            overlay.className = 'spp-overlay';
            overlay.innerHTML = `
                <button class="spp-close" title="关闭 (Esc)">✕</button>
                <span class="spp-counter"></span>
                <div class="spp-main">
                    <button class="spp-arrow spp-arrow-left" title="上一张 (←)">‹</button>
                    <img src="" alt="">
                    <button class="spp-arrow spp-arrow-right" title="下一张 (→)">›</button>
                </div>
                <div class="spp-thumbs"></div>
            `;
            document.body.appendChild(overlay);

            const mainImg = overlay.querySelector('.spp-main img');
            const counter = overlay.querySelector('.spp-counter');
            const thumbsBar = overlay.querySelector('.spp-thumbs');
            let images = [], currentIdx = 0;

            function open(imgList, startIdx) {
                images = imgList;
                currentIdx = Math.min(Math.max(startIdx, 0), imgList.length - 1);
                overlay.classList.add('spp-open');
                document.body.style.overflow = 'hidden';
                buildThumbs(); show(currentIdx);
            }
            function close() {
                overlay.classList.remove('spp-open');
                document.body.style.overflow = '';
                mainImg.src = '';
                mainImg.style.transform = ''; // 重置缩放
            }
            function show(idx) {
                if (idx < 0 || idx >= images.length) return;
                currentIdx = idx;
                mainImg.src = images[idx].src;
                mainImg.style.transform = ''; // 切图时重置缩放
                counter.textContent = (idx + 1) + ' / ' + images.length;
                thumbsBar.querySelectorAll('.spp-thumb').forEach((t, i) => {
                    t.classList.toggle('spp-active', i === idx);
                    if (i === idx) t.scrollIntoView({ block: 'nearest', inline: 'center', behavior: 'smooth' });
                });
            }
            function prev() { show((currentIdx - 1 + images.length) % images.length); }
            function next() { show((currentIdx + 1) % images.length); }
            function buildThumbs() {
                thumbsBar.innerHTML = '';
                images.forEach((item, i) => {
                    const thumb = document.createElement('img');
                    thumb.className = 'spp-thumb';
                    thumb.src = item.src;
                    thumb.addEventListener('click', () => show(i));
                    thumbsBar.appendChild(thumb);
                });
            }

            overlay.querySelector('.spp-close').addEventListener('click', close);
            overlay.querySelector('.spp-arrow-left').addEventListener('click', prev);
            overlay.querySelector('.spp-arrow-right').addEventListener('click', next);
            overlay.addEventListener('click', e => {
                if (e.target === overlay || e.target.classList.contains('spp-main')) close();
            });
            document.addEventListener('keydown', e => {
                if (!overlay.classList.contains('spp-open')) return;
                if (e.key === 'Escape') close();
                else if (e.key === 'ArrowLeft') prev();
                else if (e.key === 'ArrowRight') next();
            });

            // 鼠标滚轮(根据设置切换图片或缩放)
            let wheelAccum = 0;
            overlay.addEventListener('wheel', e => {
                if (!overlay.classList.contains('spp-open')) return;
                if (e.target.closest('.spp-thumbs')) return;
                e.preventDefault();

                if (settings.wheelAction === 'zoom') {
                    // 缩放模式:直接缩放主图
                    const scale = e.deltaY < 0 ? 1.15 : 0.87;
                    const current = mainImg.style.transform || 'scale(1)';
                    const curVal = parseFloat(current.replace(/[^0-9.]/g, '')) || 1;
                    const newVal = Math.min(Math.max(curVal * scale, 0.3), 5);
                    mainImg.style.transform = 'scale(' + newVal + ')';
                } else {
                    // 切换模式(默认)
                    wheelAccum += e.deltaY;
                    if (Math.abs(wheelAccum) < 120) return;
                    if (wheelAccum > 0) next();
                    else prev();
                    wheelAccum = 0;
                }
            }, { passive: false });

            // 捕获阶段拦截图片点击
            document.addEventListener('click', e => {
                // 快速路径:非 tpc_content 区域直接跳过
                if (!e.target.closest || !e.target.closest('.tpc_content')) return;
                let img = null;
                if (e.target.tagName === 'IMG') {
                    img = e.target;
                } else if (e.target.tagName === 'A' && e.target.children.length === 1 && e.target.children[0].tagName === 'IMG') {
                    img = e.target.children[0];
                }
                if (!img) return;
                if (!isContentImage(img)) return;

                e.preventDefault();
                e.stopPropagation();
                e.stopImmediatePropagation();

                patchImg(img);
                const postContent = img.closest('.tpc_content');
                postContent.querySelectorAll('img').forEach(patchImg);

                const postImgs = [...postContent.querySelectorAll('img.spp-img[data-spp-uid]')];
                const imgList = postImgs.map(i => ({
                    uid: i.dataset.sppUid,
                    src: getImgSrc(i) || i.src
                })).filter(item => item.src);
                if (!imgList.length) return;

                const clickedUid = img.dataset.sppUid;
                const foundIdx = imgList.findIndex(item => item.uid === clickedUid);
                const startIdx = foundIdx >= 0 ? foundIdx : 0;
                open(imgList, startIdx);
            }, true);
        }

        // ---------- 启动(带重试机制) ----------
        function bootstrap() {
            injectStaticStyles();
            applyDynamicStyles();
            initLightbox();

            waitForElement('.tpc_content', 20, 500).then(function () {
                processPostImages();
                startObserver();
                // 单次延迟兜底:处理 MutationObserver 已启动后的延迟加载图片
                setTimeout(function () { processPostImages(); }, 3500);
            });
        }

        onDomReady(bootstrap);
    }

    // ==================== 4. 图墙无图/失效图床帖列表化(仅 thread_new.php) ====================
    /* 2.52.35:版块页工具条那段 CSS 提成模块级常量 —— 帖子页(read.php) 也要同一套。
       ★ 原先它只随 initThreadListConvert() 的 GM_addStyle 注入,而那个模块帖子页不跑,
         所以帖子页的翻页条一直是站点原样(高 30px / 透明底 / li.f_one 白底)。
         现在版块页仍是 ${CSS_BOARDTOOL} 内联进大盘子,帖子页由 initBoardToolbar 注入同一份,改一处两边同步。 */
    // ==================== 「更多」下拉 / body 直系浮层换皮(2.53.19 提成模块级常量)====================
    /* 原先只有 CSS_BOARDTOOL 里那一份 ⇒ 任务页拿不到、下拉是白的。
       现在一份定义两个入口:CSS_BOARDTOOL(版块页/帖子页)与 initTaskPage()(任务页)。 */
    const CSS_MENUPOP = `
/* ---- E. 站点浮层菜单(body > div.menu)换皮:帖子浏览记录 / 版块浏览记录 弹出的那一块 ----
   ★ 线上实测(直接 fetch pw_ajax.php?action=readlog 的真实返回 + 页面里生成的节点):
     sendmsg() 在 body 下建:
       <div class="menu" style="opacity:.95;left/top;z-index:3000;visibility:visible">
         > div.bor > table[cellpadding=5]
             行1 TH.h:<span class="fr">× close.gif</span> + 标题文字(「最近浏览的帖子」/「最近浏览的版块」)
             行2… TR.tr3.tac > TD(序号) + TD.tal > A[target=_blank](标题)
     站点原皮:背景 rgb(255,255,255)、字色 rgb(31,41,55)、无圆角无内边距 —— 暗色主题下就是一坨白块。
   ★ 站点别处也用 .menu(#menu_cate 版块菜单、#menu_post 发帖菜单…),但它们都在 #main 里;
     这里只认「body 的直接子元素」→ 只命中 sendmsg 这种浮层,不会碰到版块页自己的菜单。
   ★ 拖拽(th.h 上的 onmousedown=read.move)与关闭(× 的 closep())都只吃站点自己的内联绑定,这里不碰结构。 */
body > div.menu {
    background:#0b1119 !important; background-image:none !important; opacity:1 !important;
    border:1px solid rgba(255,255,255,.12) !important;
    border-radius:12px !important;
    box-shadow:0 18px 44px rgba(2,6,23,.62), inset 0 1px 0 rgba(255,255,255,.05) !important;
    padding:6px !important;
    color:#3ef58c !important; font-size:13.5px !important;
    max-height:min(70vh, 460px) !important; overflow:auto !important;
}
body > div.menu > .bor { background:none !important; border:0 !important; padding:0 !important; }
body > div.menu table { background:none !important; border:0 !important; width:100% !important; }
body > div.menu th.h {
    display:table-cell !important;
    background-color:rgba(255,255,255,.05) !important; background-image:none !important;
    color:#3ef58c !important; font-size:13.5px !important; font-weight:700 !important; text-align:left !important;
    padding:8px 10px !important; border:0 !important; border-radius:8px !important;
    position:sticky !important; top:0 !important; z-index:1 !important;
}
/* ★★ 2.53.34:关闭按钮 ×  的**热区**。线上取证(真实 tid-2966403,探针 read-logclose{,2,3}.js):
   · 浮层标题行是 <th class="h" onmousedown="read.move(event)"> 里一枚
     <span class="fr" style="cursor:pointer" onclick="closep();"><img src="images/close.gif"></span>;
     **挂事件的只有 span.fr**,里面的 img 自己不挂 ⇒ 热区 = span 的盒。
   · images/close.gif 原生 16×16,而本站换皮曾写 width/height:12px !important ⇒ 我亲手把它压小了;
     span.fr 又被 float 成 block ⇒ 盒收紧到 12×12,用户报「非常小,关闭起来很困难」。
   · closep 源码 = function closep(){ read.menu.style.display="none"; } —— 不依赖 this/坐标,
     所以**把 span 的盒放大就是单纯放大热区**,关闭行为一个字不变。
   ⇒ 2.53.35 定稿(用户拍板「28 px」):热区就保持 28×28(标题栏高度一点不变),
     把站点那枚图标**整个换成纯 CSS 画的 ×**(两个伪元素各一条 45°/135° 的圆头细线)。
   ★ 为什么非换图标不可:close.gif 画布虽是 16×16,但**有效图形只有中间约 7×7**
     (整张图仅 33 个不透明像素、色值 #686868),显示成 16px 时肉眼看到的只有 7px 的小不点 ——
     用户上轮报「还是不够大」就是这个原因;而且 7×7 位图再放大只会糊,没有出路。
   ★ 站点原图不删、只 display:none(DOM 结构一个字没动,随时能退回位图)。
   ★ span.fr 上的 position:relative 只给那两条伪元素当定位上下文;它仍是 float:right,
     th.h 的 onmousedown(read.move) 拖拽照旧吃整条标题行。 */
body > div.menu th.h .fr {
    float:right !important; margin:0 !important;
    width:28px !important; height:28px !important;
    position:relative !important;
    display:flex !important; align-items:center !important; justify-content:center !important;
    border-radius:8px !important; transition:background-color .18s ease !important;
}
/* 站点那枚 7×7 的位图叉:不删,只藏(留着随时退回) */
body > div.menu th.h .fr img { display:none !important; }
/* 矢量 ×:两条圆头细线,各转 ±45°,在 28×28 的盒里居中交叉 */
body > div.menu th.h .fr::before,
body > div.menu th.h .fr::after {
    content:"" !important; position:absolute !important; left:50% !important; top:50% !important;
    width:2px !important; height:14px !important;
    background:#cfe0f2 !important; border-radius:2px !important;
    transition:background-color .18s ease !important;
}
body > div.menu th.h .fr::before { transform:translate(-50%,-50%) rotate(45deg) !important; }
body > div.menu th.h .fr::after { transform:translate(-50%,-50%) rotate(-45deg) !important; }
body > div.menu th.h .fr:hover { background-color:rgba(255,255,255,.12) !important; }
body > div.menu th.h .fr:hover::before,
body > div.menu th.h .fr:hover::after { background:#fff !important; }
body > div.menu tr.tr3 > td {
    background:none !important; border:0 !important;
    border-bottom:1px solid rgba(255,255,255,.06) !important;
    padding:8px 10px !important; color:#3ef58c !important; font-size:13.5px !important; vertical-align:middle !important;
}
body > div.menu tr.tr3:last-child > td { border-bottom:0 !important; }
body > div.menu tr.tr3:hover > td { background-color:rgba(255,255,255,.05) !important; }
body > div.menu tr.tr3 > td.tal { color:#3ef58c !important; -webkit-text-fill-color:#3ef58c !important; }
body > div.menu a { color:#3ef58c !important; -webkit-text-fill-color:#3ef58c !important; text-decoration:none !important; }
body > div.menu a:hover { color:#9dffc9 !important; -webkit-text-fill-color:#9dffc9 !important; }

/* ---- F. 页眉预置的列表式下拉(#header > div.menu.pad)→ 卡片化行菜单 ----
   ★★ 2.53.20(修正 2.53.19 的认错门牌)。实测(受控浏览器全页 dump):
     这枚下拉**不是**站点临时建在 body 下的浮层,而是**页面加载时就预置在页眉里的静态节点**:
       BODY > DIV#wrapA > DIV#header > DIV#menu_u.menu.pad
         > ul[style="width:7em;padding-top:.5em"] > li > a
           a[href=u.php]                   个人首页
           a[href=u.php?action-favor.html] 我的收藏
           a[href=u.php?action-feed.html]  好友近况
     页眉里同族还有 4 枚:#menu_hack / #menu_skin / #menu_profile / #menu_sort,
     全部 display:none 预置、全部 background rgb(255,255,255),站点靠 JS 把其中一枚显示出来。
   ★ 站点原皮:background:#fff、border:1px #bbb、border-radius:0、条目 padding:3px 15px / 12px 黑字。
   ★ 2.53.19 写的 body > div.menu:has(> ul) 对这 5 枚 **matches() 全是 false** ⇒ 一条没命中、当然是白的。
     (当时探针用 [body > div*, .menu …] 一大串挑「可见的」,抓到的可见节点其实是 #menu_u 被 .menu 命中的,
      我误把它当成 body 直系 —— 教训:门牌要逐个 matches() 验,不能用宽松列表抓到的结果反推层级。)
   ★ 2.53.22:门牌定稿 —— **body > div.menu.pad**。这版是真在浏览器里打开看过的,不再靠猜:
     站点弹层由 read.open() 驱动(源码:read.menu.innerHTML = getObj(id).innerHTML;
     read.menu.className = getObj(id).className;),所以显示出来的那一枚是
     **BODY > DIV.menu.pad**(站点万能层 read.menu 抄了 #menu_u 的 class 与 innerHTML),
     而 #menu_u 自己永远 display:none —— 它只是模板。复现:window.read.open("menu_u","td_u")。
   ★ 为什么用 .pad 而不是 :has(> ul):.pad 是 read.open 抄过来的类,正好把这枚页眉菜单弹层与
     sendmsg() 建的 readlog 浮层(纯 .menu,表格式 th.h + tr.tr3)分开,两边互不打扰。
     页眉那 5 枚下拉本来就都带 .pad(menu_hack / menu_skin / menu_profile / menu_sort / menu_u),
     按类认一样精确,却不必依赖 :has() 的浏览器支持(少一个兼容面)。
   ★ 为什么不会误伤:sendmsg() 建的「最近浏览的帖子/版块」浮层是 body > div.menu(表格式 th.h + tr.tr3、
     没有 ul),不在 #header 里;#main 里的 #menu_cate / #menu_post 也不在 #header 里 —— 两边彻底分家。
     没有 ul),那块皮在本段之前已经写好、不能碰;本轮选择器改成 #header > div.menu 后两边彻底分家。
   ★ ul 的 width:7em / padding-top:.5em 是站点写在 **内联 style** 上的 ⇒ 必须 !important 才盖得掉。
   ★ 本段出处是模块级常量 CSS_MENUPOP:CSS_BOARDTOOL(版块页/帖子页)与 initTaskPage(任务页)各引一次。 */
body > div.menu.pad {
    background:#0d1420 !important; background-image:none !important; opacity:1 !important;
    border:1px solid rgba(255,255,255,.13) !important; border-radius:14px !important;
    box-shadow:0 20px 46px rgba(2,6,23,.66) !important;
    padding:6px 0 !important; min-width:152px !important;
}
body > div.menu.pad > ul {
    width:auto !important; margin:0 !important; padding:0 !important; list-style:none !important;
}
body > div.menu.pad > ul > li { list-style:none !important; }
body > div.menu.pad > ul > li + li { border-top:1px solid rgba(255,255,255,.06) !important; }
body > div.menu.pad > ul > li > a {
    display:flex !important; align-items:center !important; gap:9px !important;
    padding:8px 14px !important; line-height:1.2 !important;
    font-size:13px !important; text-decoration:none !important;
    color:#cfd9e3 !important; -webkit-text-fill-color:#cfd9e3 !important;
    transition:background .16s ease, color .16s ease !important;
}
body > div.menu.pad > ul > li > a::before {
    content:'' !important; flex:none !important;
    width:5px !important; height:5px !important; border-radius:50% !important;
    background:rgba(32,201,151,.55) !important;
    transition:background .16s ease, box-shadow .16s ease !important;
}
body > div.menu.pad > ul > li > a:hover {
    background:rgba(255,255,255,.07) !important;
    color:#eafff8 !important; -webkit-text-fill-color:#eafff8 !important;
}
body > div.menu.pad > ul > li > a:hover::before {
    background:#3ef58c !important; box-shadow:0 0 8px rgba(62,245,140,.55) !important;
}
`;

    // ★★ 2.53.23:这份皮是给**站点全站通用弹层**(read.menu → body > div.menu.pad)用的 ——
    //   页眉「更多」在首页/版块页/帖子页/任务页都存在,所以必须**无条件全站注入一次**。
    //   2.53.19–2.53.22 只在「版块页大盘子」与「任务页」两个入口各引一次,首页两头都不吃 ⇒ 一直没皮。
    GM_addStyle(CSS_MENUPOP);

    const CSS_BOARDTOOL = `
            /* ===== SPP-BOARDTOOL-BEGIN =====
               ==================== 2.52.9:版块页顶部工具条 ====================
               ★ 发新帖改自 uiverse 组件 Button by SelfMadeSystem
                 https://uiverse.io/SelfMadeSystem/evil-seahorse-60 (MIT 许可,署名保留)
                 原版 80×40 独立按钮:四条 2px 渐变光束(.l/.r/.t/.b,--t:-40px,两端渐隐)
                 + blur(4px)/blur(10px) 两层辉光 + 悬停浮出 3px 点阵底纹。
                 本处为「改造」:缩到 30px 行高(--t:-8px)、换本站深底配色、加号用主色绿。
               ★ 实测站点结构(thread_new.php?fid-14,顶/底各一条):
                 DIV.t3 > SPAN.fr.color > A[href=post.php?fid-N.html] > IMG#td_post(83×30 位图,底栏那枚是 #td_post1)
                 页码 = DIV.pages > UL > LI(A 22px 黑字 / B 当前页灰底白字 / LI.pagesone 带 Pages x/y 与 Go 输入框)
                 站点 UL 是 block、LI 靠 float 横排 —— 这里统一 flex 化之后才敢上尺寸。
               ★ 打标由 JS 完成(#main .spp-pgbar 只加在含 .pages 的 .t3 行上),避免误伤站点别的 .t3。
               ★ 2.53.37:搜索页下半条 .pages 没有 .t3 包裹 —— JS 会就地给它包一层同样的 div.spp-pgbar,
                 于是这套规则一字不改地也吃住了它(版块页 / 帖子页 / 搜索页共用同一份)。 */
                        /* ---- 2.52.10:翻页条加大 / 去白框 / 改实底(版块页 + 帖子页共用)----
               ★ ① 翻页「大一些」:行 30→54px、页码 22→32px、Go 输入框 46×22→62×30;
                 ② 去白色线框:站点 .pages 自带 border:1px solid #ddd(框住整组页码)、Go 输入框也有一圈浅边,
                    还有帖子页「下一主题」li 上的内联 border-left:1px solid #eee —— 三处全部归零;
                 ③ 整条要背景不要透明:rgba(9,9,11,.72) → 实底 #0e1116;
                 ④ 帖子页(read.php)顶/底两条 .t3 翻页行套用同一套规则(含「上一主题/下一主题」那对胶囊)。
               ★ .fr 是站点靠 float 贴右的(DOM 里第一枚在最右),flex 化之后用 order 复刻这个视觉序:
                 JS(markPagerRows)给右组打 .spp-pg-mid / .spp-pg-flex 两个标,不靠 :first-child 猜。 */
            #main .spp-pgbar {
                display:flex !important; align-items:center !important; justify-content:flex-start !important;
                box-sizing:border-box !important;                 /* 站点是 content-box:不改的话 min-height 会把整条撑到 72px */
                gap:10px !important; min-height:54px !important; padding:9px 12px !important;
                border-radius:12px !important; border:1px solid rgba(255,255,255,.10) !important;  /* 2.52.44:描边换成自己的一圈半透明白 */
                background-color:rgb(0 0 0 / .86) !important;                                    /* 2.52.44:磨砂黑玻璃(半透明),不再纯色实底;2.55.28:蓝黑 15/21/34 → 中性 29/29/31(苹果 #1d1d1f 66%),全脚本 10 处同步;2.55.30:整族再压一档 → rgb(0 0 0 / .86)(面板从 #252526 压到 #0a0a0a,理由见 2.55.30 段头)*/
                background-image:linear-gradient(152deg, rgba(255,255,255,.03) 0%, rgba(0,0,0,0) 55%) !important;
                -webkit-backdrop-filter:blur(18px) saturate(140%) !important;
                backdrop-filter:blur(18px) saturate(140%) !important;
                box-shadow:0 10px 26px rgba(0,0,0,.34), inset 0 1px 0 rgba(255,255,255,.05) !important;
            }
            #main .spp-pgbar > .fl { order:1 !important; }
            #main .spp-pgbar > .c  { display:none !important; }
            #main .spp-pgbar > .fr { order:2 !important; }
            #main .spp-pgbar > .fr.spp-pg-mid  { order:3 !important; }          /* JS 打标:站点 float 里靠前那枚 → 视觉贴最右 */
            #main .spp-pgbar > .fr.spp-pg-flex { margin-left:auto !important; } /* JS 打标:右组里最左那枚吃掉剩余空间 */
            #main .spp-pgbar .pages {                       /* 站点 .pages 自带 1px 浅灰线框 + 22px 定高 → 都去掉 */
                height:auto !important; line-height:1 !important; margin:0 !important; padding:0 !important;
                border:0 !important; background:none !important; float:none !important;
            }
            #main .spp-pgbar .pages ul {
                display:flex !important; align-items:center !important; gap:6px !important;
                margin:0 !important; padding:0 !important; list-style:none !important;
            }
            #main .spp-pgbar .pages ul li {
                display:inline-flex !important; float:none !important;
                margin:0 !important; padding:0 !important; list-style:none !important;
                border:0 !important;                  /* 帖子页「下一主题」那枚 li 带内联 border-left:#eee */
            }
            #main .spp-pgbar .pages ul li a,
            #main .spp-pgbar .pages ul li b {
                display:inline-flex !important; align-items:center !important; justify-content:center !important;
                box-sizing:border-box !important;                /* 同上:否则 32px 会变成 32+20+2 = 53px */
                height:32px !important; min-width:32px !important; padding:0 10px !important; margin:0 !important;
                font-size:13.5px !important; font-weight:500 !important; line-height:1 !important;
                text-decoration:none !important; border-radius:9px !important;
                border:1px solid rgba(255,255,255,.10) !important;
                background-color:rgba(255,255,255,.05) !important; background-image:none !important;
                color:#dbe4f2 !important; -webkit-text-fill-color:#dbe4f2 !important;
                transition:transform .22s ease, background-color .22s ease, border-color .22s ease, color .22s ease !important;
            }
            #main .spp-pgbar .pages ul li a:hover {
                background-color:rgba(32,201,151,.14) !important; border-color:rgba(32,201,151,.55) !important;
                color:#fff !important; -webkit-text-fill-color:#fff !important; transform:translateY(-1px) !important;
            }
            #main .spp-pgbar .pages ul li b {
                background-color:rgba(32,201,151,.16) !important; border-color:rgba(32,201,151,.70) !important;
                color:#c9f7e4 !important; -webkit-text-fill-color:#c9f7e4 !important; font-weight:700 !important;
            }
            #main .spp-pgbar .pages ul li.pagesone {
                display:inline-flex !important; align-items:center !important; gap:8px !important;
                margin-left:12px !important; padding:0 2px !important;
                color:#9aa8bd !important; -webkit-text-fill-color:#9aa8bd !important;
                font-size:13px !important; white-space:nowrap !important;
            }
            #main .spp-pgbar .pages ul li.pagesone input {
                box-sizing:border-box !important; height:30px !important; width:62px !important; padding:0 6px !important;
                font-size:13px !important; text-align:center !important; border-radius:8px !important;
                border:0 !important; outline:none !important;                  /* ← 输入框白边一起去掉 */
                background-color:#05070b !important; background-image:none !important;
                color:#e6edf7 !important; -webkit-text-fill-color:#e6edf7 !important;
                transition:background-color .22s ease, box-shadow .22s ease !important;
            }
            #main .spp-pgbar .pages ul li.pagesone input:focus {
                background-color:#0b1119 !important;
                box-shadow:0 0 0 2px rgba(32,201,151,.30) !important;
            }
            /* ---- 发新帖(evil-seahorse 改造)---- */
            #main .spp-pgbar .spp-postbtn {
                position:relative !important; display:inline-flex !important; align-items:center !important; gap:7px !important;
                box-sizing:border-box !important;
                height:34px !important; padding:0 16px 0 13px !important; margin-left:auto !important;
                background-color:#09090b !important; background-image:none !important;
                color:#e2e8f0 !important; -webkit-text-fill-color:#e2e8f0 !important;
                font-size:12.5px !important; font-weight:600 !important; line-height:1 !important;
                border:1.58px solid #52525b !important; border-radius:8px !important;
                text-decoration:none !important; cursor:pointer !important; overflow:visible !important;
                transition:color .3s ease, border-color .3s ease, background-color .3s ease !important;
            }
            /* 2.52.29:翻页首尾按钮改成 上一页/下一页 后,到头就置灰不可点 */
            #main .spp-pgbar .pages ul li a.spp-pg-off {
                opacity:.30 !important; pointer-events:none !important; cursor:default !important;
            }
            #main .spp-pgbar .spp-postbtn > img { display:none !important; }
            #main .spp-pgbar .spp-postbtn:hover {
                color:#fff !important; -webkit-text-fill-color:#fff !important;
                border-color:rgba(32,201,151,.55) !important; background-color:#0d0d10 !important;
            }
            #main .spp-pgbar .spp-postbtn .spp-post-ico { position:relative !important; z-index:1 !important; width:11px !important; height:11px !important; }
            #main .spp-pgbar .spp-postbtn .spp-post-ico::before,
            #main .spp-pgbar .spp-postbtn .spp-post-ico::after {
                content:"" !important; position:absolute !important; left:50% !important; top:50% !important;
                transform:translate(-50%,-50%) !important; background-color:#8ef0b8 !important; border-radius:1px !important;
            }
            #main .spp-pgbar .spp-postbtn .spp-post-ico::before { width:11px !important; height:2px !important; }
            #main .spp-pgbar .spp-postbtn .spp-post-ico::after { width:2px !important; height:11px !important; }
            #main .spp-pgbar .spp-postbtn .spp-post-txt { position:relative !important; z-index:1 !important; }
            #main .spp-pgbar .spp-postbtn .spp-post-glow {
                pointer-events:none !important; position:absolute !important; inset:0 !important;
                --w:2px; --t:-8px; --s:calc(var(--t) * -1); --e:calc(100% + var(--t));
                --g:#0000, rgba(255,255,255,.20) var(--s), rgba(255,255,255,.65) var(--s), #fff,
                    rgba(255,255,255,.65) var(--e), rgba(255,255,255,.20) var(--e), #0000;
            }
            #main .spp-pgbar .spp-postbtn .spp-post-glow > i { position:absolute !important; }
            #main .spp-pgbar .spp-postbtn .spp-pb-l { left:-2px !important; top:var(--t) !important; bottom:var(--t) !important;   width:var(--w) !important;  background-image:linear-gradient(var(--g)) !important; }
            #main .spp-pgbar .spp-postbtn .spp-pb-r { right:-2px !important; top:var(--t) !important; bottom:var(--t) !important;  width:var(--w) !important;  background-image:linear-gradient(var(--g)) !important; }
            #main .spp-pgbar .spp-postbtn .spp-pb-t { top:-2px !important; left:var(--t) !important; right:var(--t) !important;    height:var(--w) !important; background-image:linear-gradient(90deg, var(--g)) !important; }
            #main .spp-pgbar .spp-postbtn .spp-pb-b { bottom:-2px !important; left:var(--t) !important; right:var(--t) !important; height:var(--w) !important; background-image:linear-gradient(90deg, var(--g)) !important; }
            #main .spp-pgbar .spp-postbtn .spp-post-glow > i::before {
                content:"" !important; position:absolute !important; inset:0 !important;
                background:inherit !important; filter:blur(4px) !important; z-index:-2 !important;
            }
            #main .spp-pgbar .spp-postbtn .spp-post-glow > i::after {
                content:"" !important; position:absolute !important; inset:0 !important;
                background:inherit !important; filter:blur(10px) !important; opacity:0 !important;
                z-index:-2 !important; transition:opacity .3s ease !important;
            }
            #main .spp-pgbar .spp-postbtn:hover .spp-post-glow > i::after { opacity:1 !important; }
            #main .spp-pgbar .spp-postbtn .spp-pb-backdrop {
                position:absolute !important; inset:0 !important; z-index:-1 !important;   /* 2.52.32:原 -9900% 把元素撑到 16816×6235,页面被顶出 3036px 空白 */
                background-image:radial-gradient(circle at 50% 50%, #0000 0, #0000 20%, rgba(17,17,17,.55) 50%) !important;
                background-size:3px 3px !important;
            }
            #main .spp-pgbar .spp-postbtn::before {
                content:"" !important; position:absolute !important; inset:0 !important; opacity:0 !important;
                border-radius:inherit !important; transition:opacity .3s ease !important;
                background-image:radial-gradient(circle at 50% 50%, #0000 0, #0000 20%, rgba(17,17,17,.67) 50%),
                                 radial-gradient(ellipse 100% 100%, rgba(255,255,255,.85), rgba(255,255,255,0)) !important;
                background-size:3px 3px, auto auto !important;
            }
            #main .spp-pgbar .spp-postbtn:hover::before { opacity:.3 !important; }
            /* ---- 位置(#breadcrumbs 搬进 #spp-btabs 最右)---- */
            /* 站点实测:DIV.bdbA > DIV#breadcrumbs(27px 浅底 248,248,248、12px 字);
               .crumbs-item i 是 7×26 的空占位(无背景图);尾部 A.s3 带内联「红字加粗」。 */
            #spp-btabs #breadcrumbs {
                display:flex !important; align-items:center !important; margin-left:auto !important;
                padding:0 !important; height:auto !important; min-height:0 !important; float:none !important;
                background:none !important; background-color:transparent !important; border:none !important;
                font-size:13px !important; gap:2px !important; min-height:24px !important;
            }
            #spp-btabs #breadcrumbs .crumbs-item {
                display:inline-flex !important; align-items:center !important; padding:0 !important;
                background:none !important; font-size:13px !important; line-height:1.2 !important;
            }
            #spp-btabs #breadcrumbs .crumbs-item i { display:none !important; }
            #spp-btabs #breadcrumbs .crumbs-item.gray3 {
                color:#8b9ab1 !important; -webkit-text-fill-color:#8b9ab1 !important; text-decoration:none !important;
            }
            #spp-btabs #breadcrumbs .crumbs-item.gray3:hover {
                color:#e2e8f0 !important; -webkit-text-fill-color:#e2e8f0 !important;
            }
            /* ★ 2.52.101:当按钮组件已经包过这枚图(.spp-hbtn-hit)时本条不再生效 ——
               3 个 id 会压过新表里那条 (1,3,2),把热区压成 15×15(经验 1.44)。 */
            #spp-btabs #breadcrumbs #td_cate:not(.spp-hbtn-hit) {
                width:15px !important; height:15px !important; margin:0 9px 0 2px !important;
                filter:invert(1) brightness(1.5) opacity(.66) !important; cursor:pointer !important;
            }
            #spp-btabs #breadcrumbs .crumbs-item.current {
                color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important; font-weight:600 !important;
            }
            #spp-btabs #breadcrumbs .crumbs-item.current > strong > a {
                color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
                font-weight:600 !important; text-decoration:none !important;
            }
            #spp-btabs #breadcrumbs .crumbs-item + .crumbs-item::before {
                content:"›" !important; display:inline-block !important;
                margin:0 10px !important; padding:0 !important;
                color:rgba(32,201,151,.72) !important; -webkit-text-fill-color:rgba(32,201,151,.72) !important;
                font-size:16px !important; font-weight:400 !important; line-height:1 !important;
                vertical-align:middle !important; transform:none !important;
            }
            #spp-btabs #breadcrumbs .threadno,
            #spp-btabs #breadcrumbs .threadrss { display:none !important; }
            /* 2.52.31:面包屑与「点击进入列表模式」拉开到 ≥4 个全角空格(实测原 9px ≈ 0.7 个、13px 字)
               ★ 间距 = 本条 margin-left − 末项 .crumbs-item 的 margin-right(-7) + flex gap(2) = ml − 5
                 ⇒ 60px → 55px ≈ 4.2 个全角空格(改这个而不是 #breadcrumbs gap,避免把 › 也撑开)*/
            #spp-btabs #breadcrumbs .fr.gray3 { margin-left:60px !important; float:none !important; }
            #spp-btabs #breadcrumbs a.s3 {
                display:inline-flex !important; align-items:center !important; height:24px !important;
                padding:0 12px !important; border-radius:999px !important; white-space:nowrap !important;
                border:1px solid rgba(32,201,151,.45) !important; background-color:rgba(32,201,151,.12) !important;
                color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
                font-size:12px !important; font-weight:600 !important; text-decoration:none !important;
                transition:background-color .22s ease, border-color .22s ease, color .22s ease !important;
            }
            #spp-btabs #breadcrumbs a.s3:hover {
                background-color:rgba(32,201,151,.20) !important; border-color:rgba(32,201,151,.75) !important;
                color:#fff !important; -webkit-text-fill-color:#fff !important;
            }
            /* ===== 2.52.30:屏蔽站点「类型筛选条」=====
               ★ 就是版块页那条整幅白底细条(DIV.t.z 直接挂在 #main 下):
                 内容为 全部 / 投票 / 活动 / 悬赏 / 商品 / 辩论(onclick=setCurrent('thread_type_all',…)),
                 实测 y502 / h28 / w1258 / 底色 rgba(250,252,255,.93) —— 卡片模式里唯一一条亮底横条。
               ★ 只认【卡片/图墙模式】:列表模式下同一个 DIV.t.z 里装的可能是整张帖子表,
                 无条件 display:none 会把帖子列表一起干掉。判据 = #main 里存在 #thread_img(站点图墙容器)。
               ★ 已注入同款规则实测:命中 1 个、display:none、h 28→0、#wall 高度不变(仅整体上移 28px)。*/
            #main:has(#thread_img) > div.t.z { display:none !important; }

            /* ===== SPP-T5BAR-BEGIN ===== */
            /* ---- B. 2.52.11 提案(.spp-t5bar)★ 这份是交付原文,落地时前缀加回 #main ---- */
            #main .spp-t5bar {
                background-color:#0e1116 !important; background-image:none !important;
                border:0 !important; border-radius:12px !important;
                box-shadow:inset 0 1px 0 rgba(255,255,255,.04) !important;
                margin:6px 0 0 !important; padding:9px 12px !important;
                color:#dbe4f2 !important; font-size:13.5px !important;
                position:relative !important; z-index:40 !important;
            }
            /* ★ 2.52.12 层级修复:站点脚本自己给每行 tr 挂了 .spp-stagger-animate(animation … both)——
               动画填满后 transform 停在 translateY(0)(不是 none),等于给工具条子树建了个层叠上下文,
               于是自绘弹层 z-index 再高也被困在里面,被后面的按钮行/公告面板盖住
               (用户截图里「最后发表」只露出最下面那条「回复」就是这个原因;我上次量到“在上层”是因为我临时注入的
               验证样式顺手 animation:none 了,把坑盖住了)。这里直接把这条动画从工具条上摘掉 + 保留 transform:none 兜底。 */
            #main .spp-t5bar tr { animation:none !important; transform:none !important; }
            #main .spp-t5bar > table.f_one { background:none !important; border:0 !important; padding:0 !important; box-shadow:none !important; }
            #main .spp-t5bar tbody,
            #main .spp-t5bar tr { background:none !important; border:0 !important; }
            #main .spp-t5bar td { border:0 !important; padding:0 !important; vertical-align:middle !important; background:none !important; }
            #main .spp-t5bar tr:hover > td { background:none !important; }
            #main .spp-t5bar form { display:inline-flex !important; align-items:center !important; margin:0 !important; }
            #main .spp-t5bar select {
                -webkit-appearance:none !important; appearance:none !important;
                box-sizing:border-box !important; height:32px !important; margin:0 8px 0 0 !important; padding:0 26px 0 10px !important;
                font-family:inherit !important; font-size:13.5px !important; line-height:1 !important; vertical-align:middle !important;
                color:#dbe4f2 !important; background-color:rgba(255,255,255,.05) !important;
                background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='6'%3E%3Cpath d='M0 0h9L4.5 6z' fill='%239aa8bd'/%3E%3C/svg%3E") !important;
                background-repeat:no-repeat !important; background-position:right 9px center !important; background-size:9px 6px !important;
                border:1px solid rgba(255,255,255,.10) !important; border-radius:9px !important;
                cursor:pointer !important; transition:background-color .25s ease, border-color .25s ease !important;
            }
            #main .spp-t5bar select:hover { background-color:rgba(255,255,255,.09) !important; border-color:rgba(255,255,255,.20) !important; }
            #main .spp-t5bar input.btn {
                box-sizing:border-box !important; height:32px !important; margin:0 0 0 2px !important; padding:0 16px !important;
                font-family:inherit !important; font-size:13.5px !important; font-weight:600 !important; line-height:1 !important; vertical-align:middle !important;
                color:#c9f7e4 !important; background-color:rgba(32,201,151,.16) !important;
                border:1px solid rgba(32,201,151,.70) !important; border-radius:9px !important;
                cursor:pointer !important; transition:background-color .25s ease, border-color .25s ease !important;
            }
            #main .spp-t5bar input.btn:hover { background-color:rgba(32,201,151,.26) !important; border-color:rgba(32,201,151,.92) !important; color:#eafff6 !important; }
            /* ---- 2.52.68:图墙模式下去掉站点筛选行那颗「提 交」----------------------------------
               ★ 它是【站点自带】的按钮,不是本脚本的产物 —— 2.52.11 那段 input.btn 只是顺手给它换了皮
                 (所以它看着像我们的,用户因此误判成「别的面板无故出现的」)。站点原文:
                 <input class="btn" type="button" value="提 交" onclick="this.form.submit();">
               ★ 它所在的 form 固定 POST 到 thread.php?fid-N&page-1,也就是【列表模式】;而图墙页
                 thread_new.php 实测不吃 orderway / asc —— 带
                 thread_new.php?fid=14&search=365&orderway=hits&asc=DESC 请求会被站点归一成 ?fid=14,
                 三枚 select 回落到「1天内的主题 / 发表时间 / 降序」。
               ★ 实测「点一下 = 什么都没发生」:在图墙页把 orderway 选成「人气」再点它,
                 POST 到 thread.php 后【又被本脚本的自动图墙模式(模块 8)弹回 thread_new.php 第 1 页】——
                 落地 URL 与点击前逐字符相同、三枚下拉回到默认值、仍是那 44 条帖子。
                 所以它不是「切去列表模式」,而是【连切都切不过去】的空转(用户会看到一次无意义的刷新)。
                 用户 2026-09-30 指着截图定案「该按钮在此板块毫无意义」,与实测一致。
               ★ 故只在【图墙模式】隐藏它。判据用站点自己打的标 —— 图墙容器 #thread_img
                 (与上面那条隐藏 div.t.z 的 :has(#thread_img) 是同一套判据,不按文字猜)。
                 列表模式(thread.php)没有 #thread_img,那颗「提 交」照旧保留:本脚本的自绘下拉
                 只写值、不提交表单,它是那三枚下拉在列表模式【唯一】的应用入口。
                 (注:装了本脚本后列表模式总要被模块 8 弹回图墙,所以这条保留实际是「万一关掉自动图墙
                   也别把筛选弄废」的保险,不是日常路径。)
               ★ 用 display:none 而不是删节点:站点表单结构原样留着,撤掉本规则即恢复。 */
            #main:has(#thread_img) .spp-t5bar form input.btn { display:none !important; }
            #main .spp-t5bar a.abtn {
                box-sizing:border-box !important; display:inline-flex !important; align-items:center !important;
                height:32px !important; margin:0 0 0 8px !important; padding:0 12px !important;
                font-size:13px !important; line-height:1 !important; vertical-align:middle !important;
                color:#dbe4f2 !important; background-color:rgba(255,255,255,.05) !important;
                border:1px solid rgba(255,255,255,.10) !important; border-radius:9px !important;
                text-decoration:none !important; text-shadow:none !important;
                transition:background-color .25s ease, border-color .25s ease !important;
            }
            #main .spp-t5bar a.abtn:hover { background-color:rgba(255,255,255,.10) !important; border-color:rgba(255,255,255,.20) !important; color:#fff !important; }
            #main .spp-t5bar a.abtn img { display:none !important; }
            #main .spp-t5bar #readlog::after,
            #main .spp-t5bar #threadlog::after {
                content:'▾' !important; margin-left:6px !important; font-size:10px !important; line-height:1 !important; color:#9aa8bd !important;
            }

            /* ★★ 2.53.47:楼层顶部那两条兜底(顶端 / 小中大)**最初写在这里,后来挪走了** ——
               活体实测这个模板在帖子页注入不到(规则进不了 CSSOM),于是「选择器对、规则也在源码里、
               就是不生效」。它们的正确住址是 READ_CSS(帖子页必注入的那一份),
               搜索 span:has(> a[title="顶端"]) 可见,注释里连着这次的教训。 */

            /* ---- D. ④-b 下拉弹层自绘(原生 select 的弹层是系统画的:白底 + 系统蓝高亮,CSS 改不动)
               机制:保留原 <select> 当「表单值载体」(display:none,name 照旧提交),另画 .spp-dd-btn + .spp-dd-pop。
               预览里由页尾那段 JS 现场把 .spp-t5bar 里的三枚 select 换掉;落到脚本时同一段函数 + 本段 CSS 一起搬。 */
            #main .spp-t5bar { overflow:visible !important; }              /* 站点全局 .t,.t5{overflow:hidden!important} 会剪掉弹层 */
            #main .spp-dd { position:relative; display:inline-flex; vertical-align:middle; margin:0 8px 0 0; }
            #main .spp-dd > select { display:none !important; }             /* 原生控件退居后台,只当值载体 */
            #main .spp-dd-btn {
                box-sizing:border-box; display:inline-flex; align-items:center; gap:9px;
                height:32px; padding:0 11px; font-family:inherit; font-size:13.5px; line-height:1;
                color:#dbe4f2; background-color:rgba(255,255,255,.05);
                border:1px solid rgba(255,255,255,.10); border-radius:9px; cursor:pointer;
                transition:background-color .25s ease, border-color .25s ease, color .25s ease;
            }
            #main .spp-dd-btn:hover { background-color:rgba(255,255,255,.09); border-color:rgba(255,255,255,.20); }
            #main .spp-dd.is-open .spp-dd-btn { background-color:#0b1119; border-color:rgba(32,201,151,.62); color:#fff; }
            #main .spp-dd-txt { flex:1 1 auto; text-align:left; }
            #main .spp-dd-caret {
                flex:0 0 auto; width:8px; height:8px;
                border-right:1.6px solid #9aa8bd; border-bottom:1.6px solid #9aa8bd;
                transform:translateY(-2px) rotate(45deg);
                transition:transform .22s ease, border-color .22s ease;
            }
            #main .spp-dd.is-open .spp-dd-caret { transform:translateY(1px) rotate(-135deg); border-color:#8ef0b8; }
            #main .spp-dd-pop {
                position:absolute; top:calc(100% + 7px); left:0; z-index:200;
                min-width:calc(100% + 24px); max-height:286px; overflow:auto; padding:6px;
                background-color:#0b1119; border:1px solid rgba(255,255,255,.12); border-radius:11px;
                box-shadow:0 18px 44px rgba(2,6,23,.62), inset 0 1px 0 rgba(255,255,255,.05);
                opacity:0; visibility:hidden; transform:translateY(-6px) scale(.985); transform-origin:top left;
                transition:opacity .18s ease, transform .18s ease, visibility .18s ease;
            }
            #main .spp-dd.is-open .spp-dd-pop { opacity:1; visibility:visible; transform:translateY(0) scale(1); }
            #main .spp-dd-opt {
                display:flex; align-items:center; gap:10px;
                padding:7px 10px; border-radius:7px; font-size:13px; line-height:1.35;
                color:#3ef58c; white-space:nowrap; cursor:pointer;
                transition:background-color .16s ease, color .16s ease;
            }
            #main .spp-dd-opt.is-hi { background-color:rgba(255,255,255,.07); color:#9dffc9; }
            #main .spp-dd-opt.is-on { color:#3ef58c; background-color:rgba(32,201,151,.14); }
            #main .spp-dd-opt.is-on.is-hi { background-color:rgba(32,201,151,.22); color:#9dffc9; }
            #main .spp-dd-opt.is-on::after { content:'✓' !important; margin-left:auto; font-size:11px; color:#8ef0b8; }
            #main .spp-dd-pop::-webkit-scrollbar { width:9px; }
            #main .spp-dd-pop::-webkit-scrollbar-thumb { background:rgba(255,255,255,.14); border-radius:9px; }

                        /* ===== SPP-T5BAR-END ===== */

            /* ===== SPP-BOARDTOOL-END ===== */
`;

    // ==================== 热门版块卡片样式(2.52.0;2.52.79 提到模块级)====================
    // ★ 为什么提层:它原本只长在 initThreadListConvert() 里 ⇒ 只有版块页能注入。
    //   2.52.79 起「社区论坛任务页」复用同一套顶部(polishUserInfo + buildHot),
    //   而那个页族既不跑 initThreadListConvert、也吃不到这里的样式 ——
    //   不注入就会出现「站点那行热门版块 + 卡片」叠在一起的陀叠字(搜索页 2.52.53 踩过)。
    //   与 2.52.0 把 CSS_UPANEL 提层是同一类问题、同一套修法(闭包作用域错位)。
    const CSS_HOTBOARD = `
/* ===== SPP-HOTBOARD-BEGIN ===== */
            /* ==================== 2.52.0:版块页顶部右框 = 热门版块 ====================
               用户三轮要求:① 屏蔽顶部 logo 横幅 ② 参考主页做左右结构(用户信息在左、
               热门在右)③ 左侧信息栏与原版主页同尺寸、左右两块同尺寸。
               第四轮指定:右框用【主页统计格那套卡片】UI(用户框选了「今日 6796」那张卡)。
               ★ 所以右框【不另配样式】:直接复用 CSS_UPANEL 里的 .spp-hm-stats / .spp-hm-stat,
                 与主页那 6 张卡是同一条规则,改主页样式会一起变。本段只补两件事:
                 ① 卡片要能当链接用(去掉下划线)② 当前所在版面点亮。
               ★ 两格同尺寸不需要新魔数:189 就是 CSS_UPANEL 自己写着的 min-height:189px,
                 也等于线上主页 #user_info 实测高度。但 min-height 对 table-cell 不生效,
                 故由 initBoardTop 另注入一条对 td 有效的 height(只作用于版块页)。 */
            #infobox.spp-up-on td.spp-up-host-td > .hb-wrap { width:100% !important; min-width:0; }
            #infobox.spp-up-on .hb-hd {
                display:flex !important; align-items:center !important; gap:9px !important;
                margin:0 0 11px !important;
            }
            #infobox.spp-up-on .hb-hd__t {
                font-size:13.5px !important; font-weight:700 !important; letter-spacing:.3px !important;
                color:#eef3fb !important; -webkit-text-fill-color:#eef3fb !important;
            }
            #infobox.spp-up-on .hb-hd__n {
                font-size:11px !important; font-weight:700 !important; line-height:1.6 !important;
                color:#dbe6f7 !important; -webkit-text-fill-color:#dbe6f7 !important;
                padding:0 8px !important; border-radius:999px !important;
                background:rgba(120,150,220,.20) !important; border:1px solid rgba(150,180,240,.30) !important;
            }
            #infobox.spp-up-on .hb-hd__hint {
                font-size:11.5px !important; margin-left:auto !important;
                color:#8798a8 !important; -webkit-text-fill-color:#8798a8 !important;
            }
            /* 卡片本体就是 <a>:补去下划线与手型;版块名不让折行 */
            #infobox.spp-up-on .hb-grid .spp-hm-stat { text-decoration:none !important; cursor:pointer; }
            #infobox.spp-up-on .hb-grid .spp-hm-stat .v {
                white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
            }
            /* 当前所在版面点亮:主色描边 + 主色底,字色也提亮(靠底色区分,不靠蓝色) */
            #infobox.spp-up-on .hb-grid .spp-hm-stat.is-cur {
                border-color:rgba(32,201,151,.58) !important;
                background:linear-gradient(180deg, rgba(32,201,151,.20), rgba(255,255,255,.02)) !important;
            }
            #infobox.spp-up-on .hb-grid .spp-hm-stat.is-cur .v { color:#8ef0b8 !important; }
            #infobox.spp-up-on .hb-grid .spp-hm-stat.is-cur .k { color:#a9d8cd !important; }
            /* ★ 站点自带那行「热门版块:」(#fav-fid)只隐藏、不删除:
               站点自己的 shortCut() 仍要 getObj('shortcutforum') 才能「收藏本版」,
               删掉会破坏站点行为(实测该函数会往 #shortcutforum 增删 <li>)。
               隐藏后同一块地方只剩一个入口,正是用户要的效果。 */
            #infobox.spp-up-on td.spp-up-host-td > #fav-fid { display:none !important; }
/* ===== SPP-HOTBOARD-END ===== */
`;

    function initThreadListConvert() {
        if (!PAGE.board) return;
        GM_addStyle(`
            #wall ul.stream > li .inner { overflow: visible !important; height: auto !important; }
            #wall ul.stream > li:not(.spp-tl-list) .section-title {
                font-size:13px !important;
                height:auto !important;
                min-height:50px !important;
                white-space: normal !important;
                overflow: visible !important;
                text-overflow: clip !important;
            }
            #wall ul.stream > li.spp-tl-list {
                float:none !important; clear:both !important; width:100% !important;
                height:auto !important; padding:0 !important; margin:0 !important;
                display:block !important; border-bottom:1px solid #e8e8e8; box-sizing:border-box !important;
            }
            .spp-tl-list > .inner {
                display:flex !important; flex-direction:row !important; flex-wrap:wrap !important;
                align-items:flex-start !important; width:100% !important; height:auto !important;
                padding:30px 20px !important; gap:0 !important; box-sizing:border-box !important;
                position:relative !important;
            }
            .spp-tl-list .section-text div { display:none !important; }
            .spp-tl-list .section-title {
                flex:0 0 100% !important; width:100% !important; min-width:0 !important;
                font-size:15px !important; font-weight:bold !important;
                line-height:1.5 !important; padding:0 !important; margin:0 0 6px 0 !important;
                height:auto !important; display:block !important;
            }
            .spp-tl-list .section-title a { text-decoration:none !important; }
            .spp-tl-list .section-intro, .spp-tl-list .section-intro[style] {
                flex:1 1 auto !important; position:static !important; width:auto !important;
                padding:0 !important; border-radius:0 !important; font-size:12px !important;
                margin:0 !important; height:auto !important;
                background:transparent !important; background-color:transparent !important;
                color:#999 !important; line-height:1.4 !important; order:2 !important;
            }
            .spp-tl-list .section-intro table { width:auto !important; background:transparent !important; background-color:transparent !important; }
            .spp-tl-list .section-intro td, .spp-tl-list .section-intro td[style] {
                padding:0 !important; white-space:nowrap !important; color:#999 !important;
                background:transparent !important; background-color:transparent !important; font-size:12px !important;
            }
            .spp-tl-list .section-intro a, .spp-tl-list .section-intro a[style] { color:#999 !important; text-decoration:none !important; }
            .spp-tl-list .section-text {
                flex:0 0 auto !important; display:flex !important; align-items:center !important;
                gap:6px !important; font-size:12px !important; line-height:1.4 !important;
                white-space:nowrap !important; padding:0 !important; margin:0 !important;
                height:auto !important; color:#999 !important; order:3 !important;
            }
            .spp-tl-list .section-text > span { float:none !important; color:#999 !important; }
            .spp-tl-list .clear { display:none !important; }
            .spp-tl-list .inner > span.s3 { display:none !important; }

            /* ==================== 2.43.0 版块页「卡片 / 列表」美化 ====================
               实测现状(thread_new.php?fid-9):
                 · 带图帖 = #wall ul.stream > li,站点给 width:calc(25% - 12px) + float:left。
                   但这页 42 项里 33 项没有可用图床(被本模块转成 .spp-tl-list 通栏行),
                   剩下 9 张真卡被夹在通栏行之间 —— clear:both 的行把它们全部打断,
                   每行只剩一张 284px 的卡,右侧留下 869px 纯黑空洞(实测 wastedRightEdge_max=869)。
                 · 卡片内 .section-title 被压成 50px 高、13px 字号;.section-intro 是一条
                   绝对定位的暗红条(rgb(153,0,0),10px 白字)。
                 · .spp-tl-list 行高 107px(.inner padding:30px 20px),左侧留白过大。
               做法:带图帖改「通栏横向卡」(左缩略图 + 右标题/数据/作者三行),
                     无图帖保持通栏但收紧到 66px;两类都把数据行统一进 .spp-card-meta。 */
            #wall ul.stream > li:not(.spp-tl-list) {
                float:none !important; clear:both !important;
                width:100% !important; height:auto !important;
                margin:0 0 12px 0 !important; padding:0 !important;
                box-sizing:border-box !important;
            }
            #wall ul.stream > li:not(.spp-tl-list) > .inner {
                display:grid !important;
                grid-template-columns:200px minmax(0,1fr) !important;
                grid-template-areas:"thumb title" "thumb meta" "thumb foot" !important;
                gap:5px 18px !important; padding:15px 18px !important;
                height:auto !important; overflow:visible !important;
                align-content:start !important; box-sizing:border-box !important;
            }
            /* .section-text 里只剩缩略图 div(数据行已被 JS 移入 .spp-card-meta),
               用 display:contents 让那个 div 直接成为 grid 子项,落到 thumb 区 */
            #wall ul.stream > li:not(.spp-tl-list) .section-text { display:contents !important; }
            #wall ul.stream > li:not(.spp-tl-list) .section-text > div { grid-area:thumb !important; overflow:hidden !important; border-radius:6px !important; }
            #wall ul.stream > li:not(.spp-tl-list) .section-text > div img {
                width:100% !important; height:130px !important;
                object-fit:cover !important; object-position:center top !important;
                display:block !important;
            }
            /* 多图帖只留首图,避免把 200px 栏撑爆 */
            #wall ul.stream > li:not(.spp-tl-list) .section-text > div img ~ img { display:none !important; }
            #wall ul.stream > li:not(.spp-tl-list) .section-title {
                grid-area:title !important; margin:0 !important;
                height:auto !important; min-height:0 !important; max-height:none !important;
                font-size:15px !important; font-weight:600 !important; line-height:1.45 !important;
                white-space:normal !important; overflow:visible !important; text-overflow:clip !important;
            }
            #wall ul.stream > li:not(.spp-tl-list) .spp-card-meta {
                grid-area:meta !important; display:flex !important; align-items:center !important;
                flex-wrap:wrap !important; gap:6px 16px !important;
                font-size:12px !important; line-height:1.5 !important;
                color:var(--spu-secondary) !important;
            }
            #wall ul.stream > li:not(.spp-tl-list) .spp-card-meta > span {
                float:none !important; color:var(--spu-secondary) !important; white-space:nowrap !important;
            }
            /* ★ 卡片作者条原本是绝对定位的暗红块(#990000)且 td 带内联 color:white。
               取消底色后若不一起改字色,就变成「白字白底」——实测坐实。 */
            #wall ul.stream > li:not(.spp-tl-list) .section-intro,
            #wall ul.stream > li:not(.spp-tl-list) .section-intro[style] {
                grid-area:foot !important; position:static !important;
                background:transparent !important; background-color:transparent !important;
                width:auto !important; height:auto !important; padding:0 !important;
                color:var(--spu-secondary) !important; font-size:12px !important;
                border-radius:0 !important;
            }
            #wall ul.stream > li:not(.spp-tl-list) .section-intro tbody,
            #wall ul.stream > li:not(.spp-tl-list) .section-intro tr {
                display:flex !important; justify-content:flex-start !important;
                align-items:baseline !important; gap:18px !important; width:100% !important;
                background:transparent !important;
            }
            #wall ul.stream > li:not(.spp-tl-list) .section-intro td,
            #wall ul.stream > li:not(.spp-tl-list) .section-intro td[style] {
                display:block !important; width:auto !important; padding:0 !important;
                background:transparent !important; background-color:transparent !important;
                color:var(--spu-secondary) !important; font-size:12px !important;
            }
            #wall ul.stream > li:not(.spp-tl-list) .clear { display:none !important; }

            /* ---- 无图帖(列表行):107px → 66px ---- */
            #wall ul.stream > li.spp-tl-list > .inner {
                padding:11px 18px !important; align-items:center !important; row-gap:2px !important;
            }
            #wall ul.stream > li.spp-tl-list .section-title {
                font-size:14px !important; margin:0 0 3px 0 !important;
                min-height:0 !important; line-height:1.45 !important;
            }
            /* 数据行移入 .spp-card-meta 后,原 .section-text 只剩隐藏的图片 div → 一并收起 */
            #wall ul.stream > li.spp-tl-list.spp-has-meta .section-text { display:none !important; }
            #wall ul.stream > li.spp-tl-list .spp-card-meta {
                flex:0 0 auto !important; display:flex !important; align-items:center !important;
                gap:14px !important; margin-left:auto !important; order:3 !important;
                font-size:12px !important; line-height:1.5 !important;
                color:var(--spu-secondary) !important;
            }
            #wall ul.stream > li.spp-tl-list .spp-card-meta > span {
                float:none !important; color:var(--spu-secondary) !important; white-space:nowrap !important;
            }
            /* 作者/日期行原本写死 #999(白底 2.77:1,偏淡)→ 提到 --spu-secondary */
            #wall ul.stream > li.spp-tl-list .spp-tl-author-line { color:var(--spu-secondary) !important; }
            #wall ul.stream > li.spp-tl-list .section-intro,
            #wall ul.stream > li.spp-tl-list .section-intro[style] { color:var(--spu-secondary) !important; }

            /* ---- 置顶公告表(#ajaxtable):表头与行距 ----
               实测:表头行是 <tr class="tr2">,但单元格全是 <td>(tr2 里 th 数=0、td 数=5),
               只有 tr3 的第二个单元格才是 <th>。所以表头必须选择 tr.tr2 > td,
               写成 tr.tr2 > th 会一条也匹配不到(踩过)。
               ★ 本注释里【绝不能写反引号】:本段是 GM_addStyle 模板字面量的内容,
                 一个反引号就会提前闭合字面量,导致整份文件 SyntaxError。
                 2.42.0 已在同一处踩过一次,此处复发(连"记录这个坑"的注释本身
                 都又写了一遍反引号)—— 故 build-boardtest.js 加了「模板体内不得
                 出现反引号」的守卫,并把 node --check 前置成第一道门。 */
            /* ★ 选择器必须带上 #main(写成 #main #ajaxtable tr.tr2 > td = 2 id + 1 class + 2 elem)。
               只写 #ajaxtable tr.tr2 > td 是(1 id + 1 class + 1 elem),会输给星空主题的
               html.spp-hm-star #main .t tr > td(1 id + 2 class + 3 elem)——
               实测表头底色被它压成 transparent。特异性按 (id, class, elem) 字典序比较,
               2 id 对 1 id 直接胜出。 */
            #main #ajaxtable tr.tr2 > td {
                background:var(--spu-overlay) !important;
                color:var(--spu-secondary) !important;
                font-size:12px !important; font-weight:600 !important;
                padding:7px 8px !important;
            }
            /* ★ 不要给 tr3 左右加 padding:「最后发表」列只有 138px,
               padding 一涨就把 “2026-09-25 16:00” 挤成两行(实测 51px 行高 + 日期折行)。
               纵向留白 + nowrap 才是安全组合。 */
            #main #ajaxtable tr.tr3 > td, #main #ajaxtable tr.tr3 > th {
                padding:7px 6px !important; vertical-align:middle !important;
                white-space:nowrap !important;
            }
            #main #ajaxtable tr.tr3 > td.tal.y-style.f10 { font-size:11px !important; }

            /* ==================== 2.44.0 版块页卡片「文字收紧」 ====================
               2.43.0 把带图帖做成了通栏横向卡(左缩略图 + 右三行),但实测卡片虽有
               165px 高、三行文字只占 133px 内的一小段,且**每行被拉长**:
                   标题 42.99px / 数据 39.24px / 作者 41.10px
               而这三行真正的文字高度(Range 量到的 ink)只有 17px / 18px / 20px。
               原因:缩略图跨了三行(grid-template-areas 里 thumb 占满三行),
               grid 于是把三个行轨道都撑到图片高度,align-content:start 再把它们
               顶到上边 —— 视觉上就是「三行稀疏地挂在 133px 里」,右半边整片空白。

               做法:让 thumb 行不参与轨道撑高,把行轨道交还给内容(auto),
               再用 align-content:center 把三行作为一个整体垂直居中。
                  · .section-text 高度归零 → 不再撑高轨道
                  · 缩略图 div 绝对定位,left:0 + top:50% + translateY(-50%)
                    居中。★ 这里 left 必须写 0:.section-text 是 position:static,
                    所以 div 的包含块是 .inner 的【padding 盒】,left:0 正好落在
                    内容盒左边缘;写成 left:18px 会把 .inner 的 padding 重复计算
                    一次(实测 leftVsContent 变成 +18px)。
                  · 数据行从「左对齐挤在一起」改为右对齐(justify-content:flex-end)
                    并占满整行宽度 —— 站点原始标记本就是 收藏(float:left) +
                    回复/热度(float:right),2.43.0 的基础样式把两个 float 都清掉了,
                    于是这行缩在标题下方,右半边留下约 400px 空白。
                    行序也调整为 标题 / 作者 / 数据(数据行在最下,视觉上更像页脚)。

               实测(thread_new.php?fid-9,12 张图卡):
                  · grid 行轨道 42.99/39.24/41.10px → 21.75/18/19.85px
                  · 文字簇垂直跨度 134px → 68px,居中偏移 ≤1.7px
                  · 缩略图 200×133,与内容盒左边缘对齐误差 0px、与标题间距 18px
                  · 卡片高 164px(12 张全部一致),整页文档高 5322 → 5168px
                  · 无图帖行高 67px → 62px */
            #wall ul.stream > li:not(.spp-tl-list) > .inner {
                position:relative !important;
                grid-template-rows:auto auto auto !important;
                align-content:center !important; align-items:start !important;
                gap:4px 18px !important;
                min-height:163px !important; height:auto !important;
                overflow:visible !important;
            }
            #wall ul.stream > li:not(.spp-tl-list) .section-text {
                grid-area:thumb !important; position:static !important;
                display:block !important; width:200px !important; height:0 !important;
                overflow:visible !important; margin:0 !important; padding:0 !important;
            }
            #wall ul.stream > li:not(.spp-tl-list) .section-text > span { display:none !important; }
            #wall ul.stream > li:not(.spp-tl-list) .section-text > div {
                position:absolute !important; left:0 !important; top:50% !important;
                transform:translateY(-50%) !important;
                margin:0 !important; padding:0 !important;
                width:200px !important; height:133px !important;
                overflow:hidden !important; border-radius:6px !important;
            }
            #wall ul.stream > li:not(.spp-tl-list) .section-text > div img {
                width:200px !important; max-width:none !important; height:133px !important;
                object-fit:cover !important; object-position:center top !important;
                display:block !important; border:0 !important; padding:0 !important;
            }
            /* 数据行右对齐并占满整行:回收标题下方的中段空白。
               同时把行序改成 标题 / 作者 / 数据 —— 2.43.0 的顺序是
               标题 / 数据 / 作者,作者行被孤零零留在最下面,中间空一截。 */
            #wall ul.stream > li:not(.spp-tl-list) > .inner {
                grid-template-areas:"thumb title" "thumb foot" "thumb meta" !important;
            }
            #wall ul.stream > li:not(.spp-tl-list) .spp-card-meta {
                justify-content:flex-end !important; width:100% !important;
            }

            /* ---- 无图帖列表行:67px → 62px ---- */
            #wall ul.stream > li.spp-tl-list > .inner {
                padding:9px 18px !important; row-gap:1px !important;
            }

            /* ==================== 2.45.0 版块页卡片「长方形网格」 ====================
               用户确认稿 preview/spp-board-star-cards.html 的落地。

               ★ 本段的核心变化:通栏长条 → 三列长方形网格。
                 2.44.0 及以前每张卡都是 width:100% 的通栏横条(1182×164),
                 信息密度低、一屏只看得到 3 条。改成 3 列后单卡约 386×290,
                 一屏可读 6 条,且两类帖(有封面 / 无封面)都是卡片。

               ★ 技法来源(均 MIT,页脚与 README 保留原始版权声明):
                 · 卡片:uiverse.io/ElSombrero2/fresh-wombat-97
                   Copyright 2026 ElSombrero2 (Rakotondrasoa Nirilala)
                   三层结构 —— ::before 旋转渐变长条 / ::after 模糊柔光 / 内容层抬到光之上,
                   于是光只在四周 2px 缝里露出;hover 时 ::before 由 paused 转为 running。
                 · 收藏按钮:uiverse.io/vinodjangid07/heavy-badger-29
                   Copyright 2026 Vinod Jangid
                   图标底座 hover 时横向展开、文字收窄让位。
                 · 作者胶囊:uiverse.io/reglobby/slimy-parrot-21
                   Copyright 2026 reglobby (murphyy)
                   渐变描边 + 内实底,hover 时描边发光。

               ★ 网格与站点标记的真实关系(实测,别照抄别的选择器):
                 #wall > #thread_img.dcsns > .dcsns-content > ul.stream,
                 即 #wall ul.stream(descendant),不是 #wall > ul.stream(匹配 0)。
                 站点 js/thread_new.css 给 .stream li 定了 float:left + width:213px,
                 必须显式覆盖 float 与 width,否则 grid 子项会被站点宽度切碎。

               ★ 无封面卡的「联动流光」:
                 动效层挂在 .spp-tc::before,用 --spp-idx(JS 按卡片序号写入)驱动
                 animation-delay = idx × -0.5s。负延迟让靠后的卡相位超前,
                 同一道波依次掠过整段网格 —— 读起来是一整片流动的光。
                 动效层用 mask 在标题上方收敛,从构造上保证正文可读,
                 不依赖「把透明度调低碰运气」。只跑 transform,走合成器,不触发布局。 */

            /* ---- 网格容器:3 列 ---- */
            #wall ul.stream {
                display:grid !important;
                grid-template-columns:repeat(3, minmax(0, 1fr)) !important;
                gap:12px !important;
                float:none !important;
                width:auto !important;
                margin:0 !important;
                padding:0 !important;
                list-style:none !important;
            }
            /* ★ 选择器必须带上 .spp-tl-list 后缀:
               本文件更早的 2.43.0 段在 #wall ul.stream > li.spp-tl-list(特异性更高)
               上写了 display:block,只写 #wall ul.stream > li 会被它压住 ——
               实测注入后 li 仍计算为 block,空白文本节点照旧生成行盒、多出 51px。 */
            #wall ul.stream > li,
            #wall ul.stream > li.spp-tl-list,
            #wall ul.stream > li:not(.spp-tl-list) {
                float:none !important; clear:none !important;
                width:auto !important; height:auto !important;
                min-width:0 !important;
                margin:0 !important; padding:0 !important;
                border-bottom:0 !important;
                /* ★ 必须是 flex,不能是 block:
                   站点在 <li> 与 .inner 之间夹着空白文本节点(实测 childNodes 为
                   TEXT:"  " / DIV / TEXT:" ")。li 若是 block,这些文本节点会生成
                   行盒,.inner 以基线对齐坐在行盒里,行高再叠加 descender 空间 ——
                   实测 li 高度 308px 而 .inner 只有 257px,多出的 51px 全在下方。
                   改成 flex 后子项不再参与行内布局,空白节点被忽略,高度即 .inner 高度。 */
                display:flex !important;
                align-items:stretch !important;
                box-sizing:border-box !important;
            }

            /* ---- 卡片壳:两类共用的三层技法 ----
               ★ 选择器必须写成 li:not(.spp-tl-list) / li.spp-tl-list 两条,
                 不能用 li > .inner 一把抓:
                 本文件更早的 2.43.0 段在同一位置写了
                 #wall ul.stream > li:not(.spp-tl-list) > .inner { display:grid }
                 —— 其中 :not() 的参数计入特异性,所以它是 (2,2,1),
                 而 li > .inner 只有 (1,2,1)。同权重时靠层叠顺序,这里却是
                 【特异性反超】,我的 display:flex 会被彻底无视。
                 实测:注入后 .inner 仍计算为 grid,卡片维持「左图右文」两栏,
                 与预览确认稿的「上图下文」完全不符。 */
            #wall ul.stream > li:not(.spp-tl-list) > .inner,
            #wall ul.stream > li.spp-tl-list > .inner {
                position:relative !important;
                display:flex !important;
                flex-direction:column !important;
                width:100% !important;
                height:auto !important;
                min-height:0 !important;
                padding:2px !important;
                margin:0 !important;
                border-radius:10px !important;
                background:#131a2a !important;
                box-shadow:0 0 3px 1px #00000088 !important;
                overflow:hidden !important;
                box-sizing:border-box !important;
                transition:box-shadow .3s ease !important;
            }
            /* 旋转渐变条与柔光层同样要按这两条选择器覆盖(2.43.0 的网格规则
               会顺带影响 ::before 的定位上下文) */
            #wall ul.stream > li:not(.spp-tl-list) > .inner::before,
            #wall ul.stream > li.spp-tl-list > .inner::before {
                content:'' !important;
                position:absolute !important;
                top:50% !important; left:50% !important;
                width:80px !important; height:620px !important;
                margin:-310px 0 0 -40px !important;
                background:linear-gradient(#ff2288, #387ef0) !important;
                opacity:0 !important;
                transition:opacity .3s !important;
                animation:spp-card-spin 8s linear infinite !important;
                animation-play-state:paused !important;
                pointer-events:none !important;
            }
            #wall ul.stream > li:not(.spp-tl-list) > .inner::after,
            #wall ul.stream > li.spp-tl-list > .inner::after {
                content:'' !important;
                position:absolute !important; inset:0 !important;
                background:rgba(19,26,42,.62) !important;
                backdrop-filter:blur(50px) !important;
                -webkit-backdrop-filter:blur(50px) !important;
                pointer-events:none !important;
            }
            #wall ul.stream > li:not(.spp-tl-list):hover > .inner::before,
            #wall ul.stream > li.spp-tl-list:hover > .inner::before {
                opacity:1 !important; animation-play-state:running !important;
            }
            #wall ul.stream > li:not(.spp-tl-list):hover > .inner,
            #wall ul.stream > li.spp-tl-list:hover > .inner {
                box-shadow:0 0 24px -2px rgba(61,106,255,.5), 0 0 3px 1px #00000088 !important;
            }
            @keyframes spp-card-spin { 0% { transform:rotate(0) } 100% { transform:rotate(360deg) } }

            /* ---- 内容层:z-index:1 是整套技法的开关 ---- */
            #wall ul.stream > li > .inner > span.section-title,
            #wall ul.stream > li > .inner > span.section-text,
            #wall ul.stream > li > .inner > div.spp-card-meta,
            #wall ul.stream > li > .inner > span.section-intro {
                position:relative !important;
                z-index:1 !important;
            }

            /* ---- 头图区:有封面卡 150px 通栏 ----
               2.43.0 把 .section-text 定为「宽度 200px 的缩略图列 + position:absolute 的图」,
               2.45.0 要的是通栏头图,所以宽度/高度/定位都要按同一特异性反压。 */
            #wall ul.stream > li:not(.spp-tl-list) .section-text {
                display:block !important;
                order:1 !important;
                position:static !important;
                flex:0 0 auto !important;
                width:100% !important;
                height:150px !important;
                margin:0 !important; padding:0 !important;
                overflow:hidden !important;
                border-radius:8px 8px 0 0 !important;
                background:#0b1120 !important;
            }
            #wall ul.stream > li:not(.spp-tl-list) .section-text > div {
                position:static !important;
                width:100% !important; height:100% !important;
                left:auto !important; top:auto !important;
                transform:none !important;
                margin:0 !important; padding:0 !important;
                border-radius:0 !important;
                overflow:hidden !important;
                background:none !important;
            }
            #wall ul.stream > li:not(.spp-tl-list) .section-text > div img {
                width:100% !important; max-width:none !important; height:150px !important;
                object-fit:cover !important; object-position:center !important;
                display:block !important; border:0 !important; padding:0 !important;
            }
            /* 多图帖只留首图 */
            #wall ul.stream > li .section-text > div img ~ img { display:none !important; }

            /* ---- 正文区 ----
               ★ 必须显式改 flex 与 box-sizing:站点 js/thread_new.css 给
                 .section-title 定了 flex:0 0 100%,配合默认 content-box,
                 宽度会按「容器 100%」算再叠加左右 15px 外边距,
                 实测溢出卡片右侧 12px(386.67 + 30 > 388)。
                 改成 flex:1 1 auto + border-box 后,外边距从宽度里扣,
                 标题才会乖乖待在卡内。 */
            #wall ul.stream > li .section-title {
                flex:1 1 auto !important;
                box-sizing:border-box !important;
                width:auto !important;
                max-width:100% !important;
                min-width:0 !important;
            }
            #wall ul.stream > li:not(.spp-tl-list) .section-title {
                order:2 !important;
                margin:13px 15px 0 15px !important;
                height:auto !important; min-height:0 !important;
                max-height:none !important;
                font-size:15.5px !important; font-weight:700 !important; line-height:1.55 !important;
                white-space:normal !important;
                overflow:hidden !important; text-overflow:clip !important;
                display:-webkit-box !important;
                -webkit-line-clamp:2 !important;
                -webkit-box-orient:vertical !important;
                word-break:break-word !important;
            }
            #wall ul.stream > li:not(.spp-tl-list) .section-title a {
                text-decoration:none !important;
            }

            /* ---- 作者胶囊 + 日期胶囊(由 JS 构建 .spp-byline) ---- */
            #wall ul.stream > li:not(.spp-tl-list) .section-intro,
            #wall ul.stream > li:not(.spp-tl-list) .section-intro[style] {
                order:3 !important;
                position:static !important;
                display:block !important;
                grid-area:auto !important;
                background:transparent !important; background-color:transparent !important;
                width:auto !important; height:auto !important;
                padding:0 !important; margin:9px 15px 0 15px !important;
                color:var(--spu-secondary) !important;
                overflow:visible !important;
            }
            #wall ul.stream > li:not(.spp-tl-list) .section-intro table { display:none !important; }

            /* ---- 数据行 ---- */
            #wall ul.stream > li:not(.spp-tl-list) .spp-card-meta {
                order:4 !important;
                grid-area:auto !important;
                margin:auto 15px 0 15px !important;
                padding-top:10px !important;
                display:flex !important; align-items:center !important;
                flex-wrap:nowrap !important; gap:12px !important;
                border-top:1px solid rgba(120,150,220,.10) !important;
                font-size:11.5px !important; line-height:1.5 !important;
                color:var(--spu-secondary) !important;
            }
            #wall ul.stream > li:not(.spp-tl-list) .spp-card-meta > span {
                float:none !important;
                color:var(--spu-secondary) !important;
                white-space:nowrap !important;
            }
            /* 收藏靠左、回复/热度靠右:站点原本就是 float:left / float:right 两个 span */
            #wall ul.stream > li:not(.spp-tl-list) .spp-card-meta > span:first-child { margin-right:auto !important; }

            /* ---- 无封面卡:与卡片同底的「联动流光」----
               ★ 这里必须整体覆盖 ::before / ::after —— 卡片壳那两条是「旋转渐变条 +
                 模糊柔光」,是给有封面卡用的;无封面卡套上去的话,柔光层会盖在
                 极光之上把它洗掉。两类卡各用一套,互不干扰。
               头图区用 padding-top 充当(无封面卡没有 .section-text 可放图片),
               高度 112px,与有封面卡的 150px 图片区同为「头图区」语义。 */
            #wall ul.stream > li.spp-tl-list > .inner {
                padding:2px !important;
                padding-top:114px !important;
                justify-content:flex-start !important;
                background:#131a2a !important;
            }
            #wall ul.stream > li.spp-tl-list > .inner > * { position:relative !important; z-index:1 !important; }

            /* 极光:三团模糊光斑,只跑 transform */
            #wall ul.stream > li.spp-tl-list > .inner::before {
                content:'' !important;
                position:absolute !important;
                left:0 !important; right:0 !important; top:0 !important;
                height:112px !important;
                margin:0 !important;
                background:
                    radial-gradient(34% 40% at 26% 30%, hsl(var(--spp-h1, 220) 78% 58% / .34), transparent 70%),
                    radial-gradient(30% 36% at 68% 22%, hsl(var(--spp-h2, 278) 80% 60% / .27), transparent 72%),
                    radial-gradient(26% 32% at 48% 52%, hsl(var(--spp-h1, 220) 72% 54% / .18), transparent 74%) !important;
                filter:blur(26px) !important;
                backdrop-filter:none !important;
                -webkit-backdrop-filter:none !important;
                opacity:1 !important;
                transform-origin:50% 50% !important;
                animation:spp-aurora 18s ease-in-out infinite !important;
                animation-delay:calc(var(--spp-idx, 0) * -0.5s) !important;
                animation-play-state:running !important;
                -webkit-mask-image:linear-gradient(180deg,#000 0%,#000 42%,transparent 100%) !important;
                mask-image:linear-gradient(180deg,#000 0%,#000 42%,transparent 100%) !important;
                pointer-events:none !important;
            }
            /* 柔光层对无封面卡关闭 */
            #wall ul.stream > li.spp-tl-list > .inner::after {
                content:none !important;
                background:none !important;
                backdrop-filter:none !important;
                -webkit-backdrop-filter:none !important;
                animation:none !important;
            }
            #wall ul.stream > li.spp-tl-list:hover > .inner::before {
                opacity:1 !important;
                animation-play-state:running !important;
            }
            @keyframes spp-aurora {
                0%   { transform:translate3d(-7%,  3%,0) scale(1.00) rotate(0deg) }
                33%  { transform:translate3d( 5%, -5%,0) scale(1.12) rotate(7deg) }
                66%  { transform:translate3d(-3%,  5%,0) scale(1.05) rotate(-5deg) }
                100% { transform:translate3d(-7%,  3%,0) scale(1.00) rotate(0deg) }
            }

            #wall ul.stream > li.spp-tl-list .section-text { display:none !important; }
            #wall ul.stream > li.spp-tl-list .section-title {
                order:1 !important;
                margin:13px 15px 0 15px !important;
                height:auto !important; min-height:0 !important; max-height:none !important;
                font-size:15.5px !important; font-weight:700 !important; line-height:1.55 !important;
                white-space:normal !important; overflow:hidden !important; text-overflow:clip !important;
                display:-webkit-box !important;
                -webkit-line-clamp:2 !important;
                -webkit-box-orient:vertical !important;
                word-break:break-word !important;
            }
            /* 正文与数据行都靠 .spp-card-meta 的 margin-top:auto 顶到底部,
               所以这里要把 .section-intro 之后的弹性交给 meta。
               列表行原本 .inner 用 row-gap 排布,改成 flex 后须显式归零。 */
            #wall ul.stream > li.spp-tl-list > .inner { row-gap:0 !important; gap:0 !important; }
            /* ★ 统一卡高:标题固定占满两行高度。
               不这么做的话,单行标题卡比两行标题卡矮 12px,
               同一行网格里出现两种高度,底部数据行参差不齐(实测 296/308/258 三种)。
               与预览页同法:给标题一个「两行行高」的最小高度。

               ★ 选择器必须与下面两条 .section-title 同特异性或更高:
                 下面那条是 li:not(.spp-tl-list) .section-title,
                 其 :not() 参数计入特异性,所以这里也得带上同样的 :not(),
                 否则 min-height:0 会把它按层叠顺序压回去(实测 titleMinH 读到 0px)。 */
            #wall ul.stream > li:not(.spp-tl-list) .section-title,
            #wall ul.stream > li.spp-tl-list .section-title {
                min-height:calc(15.5px * 1.55 * 2) !important;
            }
            /* 两行固定高度后,整卡高度也随之固定,两类卡各自一致 */
            #wall ul.stream > li:not(.spp-tl-list) > .inner { min-height:295px !important; }
            #wall ul.stream > li.spp-tl-list > .inner { min-height:257px !important; }
            #wall ul.stream > li.spp-tl-list .section-title a { text-decoration:none !important; }
            #wall ul.stream > li.spp-tl-list .section-intro,
            #wall ul.stream > li.spp-tl-list .section-intro[style] {
                order:2 !important;
                position:static !important; display:block !important;
                background:transparent !important; background-color:transparent !important;
                width:auto !important; height:auto !important; padding:0 !important;
                margin:9px 15px 0 15px !important;
                color:var(--spu-secondary) !important;
            }
            #wall ul.stream > li.spp-tl-list .section-intro table { display:none !important; }
            #wall ul.stream > li.spp-tl-list .spp-tl-author-line { display:none !important; }
            #wall ul.stream > li.spp-tl-list .spp-card-meta {
                order:3 !important;
                /* ★★ 2.55.3:这条原本是 14px。有封面卡那条(2.45.0 段)写的是
                   margin:auto 15px 0 15px,于是两类卡的数据行差了整整 14.67px:
                   实测「行底 → 卡底」无图卡 16.67px、有封面卡 2px ——
                   用户看到的正是无图卡那行浮在半空。
                   用户原话:「对于无图床的卡片来说,整体放置的位置需要再靠底边一点。」
                   ⇒ 归零,与有封面卡那条逐字相同;改后 16.67px → 2.67px。
                     (多出来的 0.67px 是站点给 .inner 的 0.667px 边框,两类卡都有。)
                   ★ 卡片高度由内容决定(2.49.7 已放开 min-height),所以这一改
                     卡片会矮 14px:卡底往上收,数据行随之贴上卡底。 */
                margin:auto 15px 0 15px !important;
                padding-top:10px !important;
                display:flex !important; align-items:center !important;
                flex-wrap:nowrap !important; gap:12px !important;
                border-top:1px solid rgba(120,150,220,.10) !important;
                color:var(--spu-secondary) !important;
            }
            #wall ul.stream > li.spp-tl-list .spp-card-meta > span {
                float:none !important; white-space:nowrap !important;
            }
            #wall ul.stream > li.spp-tl-list .spp-card-meta > span:first-child { margin-right:auto !important; }

            /* ---- 作者胶囊:渐变描边 + 内实底(hover 发光) ---- */
            .spp-byline {
                display:flex !important; align-items:center !important; gap:8px !important;
                min-width:0 !important;
                /* ★ 必须不换行:作者的 uid 名有长有短,flex 默认会折行,
                   实测同名胶囊在折行时把 intro 从 21px 撑到 33px,
                   整卡高度随之从 258px 跳到 308px,同一行网格参差不齐。 */
                flex-wrap:nowrap !important;
                white-space:nowrap !important;
                overflow:hidden !important;
            }
            .spp-name {
                position:relative !important;
                display:inline-flex !important; align-items:center !important; gap:2px !important;
                padding:2px 8px 2px 2px !important;
                border-radius:999px !important;
                background:linear-gradient(135deg,
                    hsl(var(--spp-h1, 220) 72% 58% / .40),
                    hsl(var(--spp-h2, 278) 72% 58% / .18)) !important;
                /* 长作者名在窄卡里必须先收缩,不能把日期胶囊挤出卡外 */
                flex:0 1 auto !important;
                max-width:150px !important;
                min-width:0 !important;
                overflow:hidden !important;
                transition:background .28s ease, box-shadow .28s ease !important;
            }
            .spp-name::before {
                content:'' !important;
                position:absolute !important; inset:1px !important;
                border-radius:999px !important;
                background:#1a2233 !important;
                transition:background .28s ease !important;
            }
            #wall ul.stream > li:hover .spp-name {
                background:linear-gradient(135deg,
                    hsl(var(--spp-h1, 220) 82% 62% / .85),
                    hsl(var(--spp-h2, 278) 82% 62% / .45)) !important;
                box-shadow:0 0 12px -3px hsl(var(--spp-h1, 220) 80% 60% / .55) !important;
            }
            #wall ul.stream > li:hover .spp-name::before { background:#202a3e !important; }
            .spp-name-txt {
                position:relative !important; z-index:1 !important;
                font-size:11.5px !important; font-weight:600 !important; letter-spacing:.2px !important;
                color:#b6c1d8 !important;
                overflow:hidden !important; text-overflow:ellipsis !important; white-space:nowrap !important;
                text-decoration:none !important;
                /* ★★ 2.50.4:必须把 margin 归零。
                   站点自己的规则 .stream li .section-intro a { margin-left:32px }
                   会命中本元素(它是个 <a>,且确实在 .section-intro 里),
                   实测计算值就是 32px —— 于是头像与名字之间永远隔着 32px,
                   脚本这边把 flex 的 gap 从 5px 调到 2px 只挪动 3px,肉眼看不出来。
                   (本项目上一轮据此记的「gap 5px → 2px」是对的,但那条改动
                    对观感无效,因为真正的占位者不在这里。此处补齐。) */
                margin:0 !important;
            }
            #wall ul.stream > li:hover .spp-name-txt { color:#e4ecfb !important; }
            .spp-avatar {
                position:relative !important; z-index:1 !important;
                flex:0 0 15px !important; width:15px !important; height:15px !important;
                border-radius:50% !important;
                display:flex !important; align-items:center !important; justify-content:center !important;
                font-size:9px !important; font-weight:800 !important; line-height:1 !important;
                color:#fff !important;
                background:linear-gradient(135deg,
                    hsl(var(--spp-h1, 220) 60% 50%),
                    hsl(var(--spp-h2, 278) 60% 40%)) !important;
                text-transform:uppercase !important;
                user-select:none !important;
            }

            /* ---- 日期胶囊:刻意比作者轻一档(无描边、更暗、字号更小) ---- */
            .spp-date {
                position:relative !important;
                display:inline-flex !important; align-items:center !important;
                padding:2px 8px !important;
                border-radius:999px !important;
                background:rgba(120,150,220,.06) !important;
                font-size:11px !important;
                color:#78839b !important;
                font-variant-numeric:tabular-nums !important;
                letter-spacing:.2px !important;
                white-space:nowrap !important;
                overflow:hidden !important;
                transition:color .28s ease, background .28s ease !important;
            }
            .spp-date::before {
                content:'' !important;
                position:absolute !important; inset:0 !important; z-index:0 !important;
                background:linear-gradient(90deg, hsl(var(--spp-h2, 278) 70% 56% / .34), transparent) !important;
                transform:scaleX(0) !important;
                transform-origin:left center !important;
                transition:transform .34s cubic-bezier(.22,.61,.36,1) !important;
            }
            .spp-date-txt {
                position:relative !important; z-index:1 !important;
                transition:transform .28s ease !important;
            }
            #wall ul.stream > li:hover .spp-date { color:#bccbe4 !important; background:rgba(120,150,220,.10) !important; }
            #wall ul.stream > li:hover .spp-date::before { transform:scaleX(1) !important; }
            #wall ul.stream > li:hover .spp-date-txt { transform:translateX(2px) !important; }

            /* ---- 收藏按钮:图标底座展开、文字收窄 ---- */
            .spp-fav {
                display:inline-flex !important; align-items:center !important;
                padding:3px !important;
                border-radius:999px !important;
                border:1px solid rgba(120,150,220,.22) !important;
                background:rgba(120,150,220,.07) !important;
                font-size:11.5px !important; font-weight:500 !important; letter-spacing:.3px !important;
                color:#8e99b0 !important;
                text-decoration:none !important;
                cursor:pointer !important;
                overflow:hidden !important;
                transition:color .26s ease, background .26s ease, border-color .26s ease, box-shadow .26s ease !important;
            }
            .spp-fav-ico {
                flex:0 0 18px !important; width:18px !important; height:18px !important;
                border-radius:999px !important;
                display:flex !important; align-items:center !important; justify-content:center !important;
                background:linear-gradient(135deg, hsl(220 78% 58%), hsl(255 76% 58%)) !important;
                box-shadow:0 0 0 1px rgba(255,255,255,.16) inset !important;
                overflow:hidden !important;
                transition:width .3s cubic-bezier(.22,.61,.36,1), flex-basis .3s cubic-bezier(.22,.61,.36,1) !important;
            }
            .spp-fav-ico::before {
                content:'' !important;
                width:9px !important; height:11px !important;
                background:#fff !important;
                clip-path:polygon(0 0,100% 0,100% 100%,50% 68%,0 100%) !important;
                opacity:.95 !important;
            }
            .spp-fav-txt {
                padding:0 8px 0 5px !important;
                white-space:nowrap !important;
                max-width:40px !important;
                overflow:hidden !important;
                transition:max-width .3s cubic-bezier(.22,.61,.36,1), opacity .22s ease, padding .3s ease !important;
            }
            .spp-fav:hover {
                color:#e6eeff !important;
                background:rgba(61,106,255,.22) !important;
                border-color:rgba(61,106,255,.66) !important;
                box-shadow:0 0 16px -3px rgba(61,106,255,.8) !important;
            }
            .spp-fav:hover .spp-fav-ico { flex-basis:34px !important; width:34px !important; }
            .spp-fav:hover .spp-fav-txt { max-width:0 !important; opacity:0 !important; padding-left:0 !important; padding-right:0 !important; }
            /* ---- 2.55.1:资源区「已读」标识 ----
               与「收藏」同一套胶囊语言(999px 圆角 + 描边 + 半透底),只换一支绿:
               「已读」是**状态**不是按钮,所以 pointer-events:none —— 点到它等于点到卡片,
               不抢命中层的活。文字左侧那枚对勾由 ::before 给。
               ★ 它不在 .spp-card-meta 的直接子项位置(插在「收藏」那个 span 里),
                 所以上面那条给直接子项写的强制色不会碰它;
                 复合选择器 #wall ul.stream 保证它只在卡片列表页生效。 */
            #wall ul.stream .spp-read-chip {
                display:inline-flex !important; align-items:center !important;
                margin-left:8px !important;
                padding:2px 8px !important;
                border-radius:999px !important;
                border:1px solid rgba(103,194,58,.34) !important;
                background:rgba(103,194,58,.10) !important;
                color:#8ed08e !important;
                font-size:11px !important; font-weight:500 !important; letter-spacing:.3px !important;
                line-height:1.5 !important;
                white-space:nowrap !important;
                vertical-align:middle !important;
                pointer-events:none !important;
                user-select:none !important;
                -webkit-user-select:none !important;
            }
            #wall ul.stream .spp-read-chip::before {
                content:'✓' !important;
                margin-right:4px !important;
                font-size:10px !important;
                line-height:1 !important;
            }

            /* ---- 2.55.2:让「已读」与「收藏」的中轴线真正对齐 ----
               ★ 症状(用户截图红框):「已读」的水平中轴线没有和其他 UI 对齐。
               ★ 根因不是内边距拍脑袋拍的:已读胶囊与「收藏」同住站点那个
                 span[style="float:left"] 里,而它是**普通块盒子** ⇒ 里面两个行内级盒
                 按基线对齐。实测 a.spp-fav 高 25.33、胶囊高 21.83,
                 于是胶囊的垂直中点比「收藏」低 2.53px(4567.63 vs 4565.10)——
                 与「回复 / 热度」那两枚(4565.49)也差 2.14px。
               ★ 修法:把这一格改成 flex + align-items:center,三个盒子的中点由浏览器
                 自己算齐(实测改后三者都是 4565.44,逐位相等)。
               ★ 选择器上挂了 :has(.spp-read-chip) **两道门**:这一格只在真的装着已读胶囊时
                 才改显示方式 —— 茶馆、首页、没有任何已读的卡片上,收藏那一格的盒子模型
                 一个像素都不动(少一道门就变成「为了一枚胶囊去动全站的收藏按钮」)。
               ★ :has() 在本脚本里不是新东西(2.55.0 的 td.f_one:has(#smiliebox) 早就在用),
                 不支持它的浏览器会整条丢掉这条规则 —— 退回 2.55.1 的观感,不会更糟。 */
            #wall ul.stream .spp-card-meta > span:has(a.spp-fav):has(.spp-read-chip) {
                display:inline-flex !important;
                align-items:center !important;
            }

            /* ---- 深色卡上的文字可见性 ----
               ★ 这里【只能覆盖非行内】的颜色。
                 标题与回复/热度数字是脚本用 style.setProperty(..., 'important')
                 写的【行内 !important】,按层叠规则行内 !important 优先于任何
                 外部选择器,CSS 压不过它 —— 那一档由 JS 的 fixDarkTextContrast
                 直接改行内值来处理(见该函数注释)。
                 本段负责的是「站点静态标记带内联色」与「默认继承色」两种。 */
            #wall ul.stream .spp-fav-txt { color:inherit !important; }

            /* ---- 窄屏降级:两列 / 一列 ---- */
            @media (max-width: 1000px) {
                #wall ul.stream { grid-template-columns:repeat(2, minmax(0, 1fr)) !important; }
            }
            @media (max-width: 640px) {
                #wall ul.stream { grid-template-columns:minmax(0, 1fr) !important; }
            }

            /* ==================== 2.47.0 版块页:全站放大头图 + 有图卡靠前 ====================
               本段接在 2.45.0 之后,做两件事:
               ① 头图区从 150px 定高改成 16:9 自适应 —— 2.46.0 只在非茶馆做,
                  2.47.0 起【茶馆一并放大】(用户:「对于茶馆,有图片的展示区也要变大,
                  无图片的一样(图片框内变为特效)」);
               ② 全版块按 横 → 竖 → 无图 分组(纯 JS 重排,见 groupCards;2.49.4 起推广到所有版块)。

               ★ 为什么是 16:9(茶馆实测复核过,不是照搬非茶馆的结论):
                 茶馆 fid-9 首屏 10 张有图卡【加载完成后】的原始比例实测为
                 0.75 / 0.82 / 0.93 / 0.98 / 1.33 / 1.37 / 1.44 / 1.78 / 1.79 / 3.44
                 —— 竖图、方图、宽图混杂,没有任何一个比例能让所有人满意。
                 按 object-fit:cover 的可见比例算(min/max):
                   16:9  均值 67.9%  中位 75.0%  最差 42.2%
                   3:2   均值 71.9%  中位 83.6%  最差 43.6%
                   4:3   均值 73.8%  中位 74.3%  最差 38.8%
                 光看均值会选 4:3,但 4:3 的最差一张只剩 38.8%(那张 3.44 的截图
                 只露三成八);16:9 的最差是 42.2%。真正的决定因素是「框有多高」:
                   16:9 高 218px、3:2 高 258px、4:3 高 291px。
                 用户的原话是要「变大」,且明确「图片框内变为特效」——
                 方块越大特效越可见。所以取 16:9:它比 3:2 矮 40px,比现状 150px
                 仍高出 45%,在「变大」与「别把无图卡撑成一堵墙」之间取得平衡。

               ★★ 已定稿(用户明确确认:「16:9 足够了」):
                 这条不再是待定的可选项。若日后要调大,改下面【两处】
                 aspect-ratio 即可 —— 有图卡的 .section-text 与无图卡的 ::before,
                 只改一处,两类卡立刻不等高。档位:3/2(+18%)、4/3(+34%)。

               ★ 为什么用 aspect-ratio 而不是写死 218px:
                 本脚本的页面宽度是用户可配的(默认 1400px,2.52.63 起收窄为 1200–1600)。
                 卡宽随视口变(1400px 下 392,1600px 下约 450),
                 写死高度会在宽视口下重新产生裁切。aspect-ratio 跟着卡宽走。

               ★ 为什么门控是 html.spp-board-skin(2.46.0 曾叫 html.spp-not-tea):
                 2.46.0 用「非茶馆」做门控,把茶馆整版排除在外。本次用户要求
                 茶馆也放大,排除条件消失 —— 继续用旧名会让类名撒谎,
                 而且「茶馆不应受影响」的旧守卫会一直绿着守一个错误的语义。
                 改名后语义变成「这是一个有版块页卡片网格的页面」,两类版块共用。
                 该类的判定与 isThreadListPage 同口径,在 document-start 就该打上。
                 ★ 2.46.0 那一段 CSS 已被本段【逐条完全覆盖】,且它的门控类
                   spp-not-tea 再无任何代码去添加(匹配条件永假),
                   所以整段删除,而不是像 2.43.0/2.44.0/2.45.0 那样保留 ——
                   那三代是层层叠加、互为前提,删掉任一代都会让后一代失效;
                   2.46.0 则是被完整取代,留着只会变成"grep 得到但从不着色"的死规则。

               ★ 为什么必须整体加 html.spp-board-skin 前缀(不能只靠书写顺序):
                 2.45.0 在同一位置写了 height:150px !important 与
                 img{height:150px !important},两者特异性都是 (1,4,x)。
                 本段每条都多一个 html.spp-board-skin(+1 类 +1 元素),
                 特异性升到 (1,5,y),才能压住它 —— 2.45.0 那两条都带 !important。 */
            html.spp-board-skin #wall ul.stream > li:not(.spp-tl-list) .section-text {
                height:auto !important;
                aspect-ratio:16 / 9 !important;
            }
            html.spp-board-skin #wall ul.stream > li:not(.spp-tl-list) .section-text > div img {
                height:100% !important;
                /* ★★ 2.49.2:同样要解除站点的封顶(理由见本段末尾那条 img 规则)。
                   站点 thread_new.css 的 .stream li.dcsns-rss .section-text img{max-height:270px}
                   会把这里的 height:100% 截回 270px,竖卡图片区因此空出 111px。 */
                max-height:none !important;
            }
            /* 无封面卡的「头图占位」:2.45.0 用 .inner 的 padding-top:114px 顶出一块
               【刻意比图区矮】的区域(114 vs 150)。放大头图后若不动它,
               同一行会出现「有图卡 218、无图卡 114+文字」两种高度 ——
               正是 2.45.0 花大力气消掉的「同行参差」。
               做法:极光伪元素 ::before 从绝对定位改回【在流内】,
               于是它自己就靠 aspect-ratio 撑出同一个 16:9,视觉与占位合成一个东西;
               padding-top 归零,避免与其叠加。

               ★ 为什么这条同时适用于茶馆与非茶馆(不必再分两套):
                 有图卡的头图高 = .section-text 宽 × 9/16;
                 无图卡的占位高 = ::before 宽 × 9/16,而它是 flex:0 0 auto + width:100%,
                 宽度同为 .inner 的 content 宽。两者算出同一个值,同行底部自然对齐。
                 茶馆 42 项里 32 项无图,两类卡同高在这里尤其重要。

               ★ 为什么 ::before 能当 flex 子项:它是 .inner 的第一个生成盒,
                 flex 方向为 column + justify-content:flex-start,
                 所以它正好落在卡片顶部,即「头图区」。 */
            html.spp-board-skin #wall ul.stream > li.spp-tl-list > .inner {
                padding-top:0 !important;
            }
            html.spp-board-skin #wall ul.stream > li.spp-tl-list > .inner::before {
                position:relative !important;
                flex:0 0 auto !important;
                width:100% !important;
                left:auto !important; right:auto !important; top:auto !important;
                height:auto !important;
                aspect-ratio:16 / 9 !important;
                margin:0 !important;
            }

            /* ==================== 2.48.0 版块页:四项细节修正 ====================
               用户在 2.47.0 截图上的四点反馈:
                 ① 回复/热度太贴卡片边缘
                 ② 日期胶囊的特效希望【默认就开】(不必等鼠标移到卡片上)
                 ③ 无图卡「图片框」内的动效要加强
                 ④ 点击卡片任意处即可跳转

               ★★ 本段所有选择器都写成
                  「html.spp-board-skin #wall ul.stream > li... .inner::after」 这种【加深】形态,
                  而不是只写目标元素。原因(本轮实测踩到):
                    2.45.0 段的规则是 「#wall ul.stream > li.spp-tl-list > .inner::after」
                    (1 id + 2 class + 3 elem),我已经多带了 html.spp-board-skin,
                    按说该赢;但 scope() 会把 「html.spp-board-skin」 改写成舞台前缀
                    「#stageAfter」,而 「#wall」 又改写成 「.spp-wall」(class 而非 id)——
                    于是我的最终形态变成 「#stageAfter .spp-wall ...」,
                    与旧规则的权重【恰好相同】,只能靠书写顺序决胜。
                    本代段落虽然在后面,但深度不足时仍会被旧规则的 !important 压住。
                    加深到「li + 目标元素」两级后,权重明确高于旧规则,不再依赖顺序。

               ① 的根因(实测量到,不是猜的):
                  2.44.0 段给数据行写了 justify-content:flex-end + width:100%,
                  2.45.0 段又补了 margin:auto 15px 0 15px。
                  数据行是 .inner(flex column)的子项,默认 align-self:stretch,
                  横向撑满【内容盒】后,左右各 15px 的 margin 把右端推到盒外:
                  实测 .inner 391px、数据行右端 433px、卡内沿 420px → 溢出 13px。
                  ★ 注意 width:100% 是 2.44.0 段写的(带 !important),
                    所以修法不能只补 box-sizing(实测无效,被 !important 压过),
                    必须显式给宽度 discount 掉左右 margin。
                  ★ 为什么是 calc(100% - 30px) 而不是 max-width:
                    实测两者效果相同(都得到 357px),但 calc 对
                    「未来有人改 margin 数值」是自证的 —— 改 margin 必须同步改这里,
                    而单独改 max-width 会在 margin 变小时留下莫名空隙。 */
            html.spp-board-skin #wall ul.stream > li:not(.spp-tl-list) .spp-card-meta,
            html.spp-board-skin #wall ul.stream > li.spp-tl-list .spp-card-meta {
                width:calc(100% - 30px) !important;
                box-sizing:border-box !important;
            }

            /* ② 日期胶囊特效默认常驻。
               2.45.0 写的是:默认 background rgba(.06) + ::before transform:scaleX(0),
               只有 li:hover 才 scaleX(1) 并提亮 —— 也就是「鼠标扫过才亮一下」。
               用户要的是【不经过卡片也开着】。做法:
                 默认态直接给到「亮起来」的那一档,并让 ::before 常驻 scaleX(1);
                 hover 时再叠一层外发光,形成「平时在呼吸、悬停更明确」的两级。
               ★ 保留 ::before 的 scaleX 过渡而不直接删掉:它是 2.45.0 的既定技法,
                 删掉会让 hover 的层次感消失(hover 与默认变得没有差别)。 */
            html.spp-board-skin #wall ul.stream > li .spp-date {
                color:#bccbe4 !important;
                background:rgba(120,150,220,.10) !important;
            }
            html.spp-board-skin #wall ul.stream > li .spp-date::before {
                transform:scaleX(1) !important;
                animation:spp-date-breathe 3.6s ease-in-out infinite !important;
                animation-delay:calc(var(--spp-idx, 0) * -0.28s) !important;
            }
            html.spp-board-skin #wall ul.stream > li:hover .spp-date {
                box-shadow:0 0 10px -2px hsl(var(--spp-h2, 278) 80% 62% / .55) !important;
            }
            @keyframes spp-date-breathe {
                0%,100% { opacity:.55 }
                50%     { opacity:1   }
            }

            /* ③ 无图卡「图片框」内的动效加强。
               2.47.0 的极光是 3 团 blur(26px) 光斑 + 18s 缓慢位移,
               实测能在跑(animation-play-state:running),但观感偏淡:
               blur 26px 把三团糊成一片,实测首屏几乎看不出在动。
               做法(不引入新元素,只加一层 ::after,避免动 DOM):
                 · ::before 提高不透明度并缩短周期,让位移可见;
                 · ::after 新增一道斜向流光扫掠(background-position 驱动,不影响布局)。
               ★ ::after 在 2.45.0 段被写成 content:none(柔光层对无封面卡关闭),
                 本段要重新启用它 —— 所以选择器必须加深到与旧规则同级或更高,
                 否则「写了但不生效」(本轮实测正是这样,预览页如实报了 FAIL)。
               ★ 三团光斑的色相取自卡片自己的 --spp-h1/--spp-h2(每卡不同),
                 保持 2.45.0 的「同底联动」:负延迟让同一道波依次掠过整行。 */
            html.spp-board-skin #wall ul.stream > li.spp-tl-list > .inner::before {
                opacity:1 !important;
                filter:blur(18px) !important;
                animation:spp-aurora 11s ease-in-out infinite !important;
                animation-delay:calc(var(--spp-idx, 0) * -0.5s) !important;
            }
            html.spp-board-skin #wall ul.stream > li.spp-tl-list > .inner::after {
                content:'' !important;
                display:block !important;
                position:absolute !important;
                top:0 !important; left:0 !important; right:0 !important; bottom:auto !important;
                width:100% !important;
                height:auto !important;
                aspect-ratio:16 / 9 !important;
                border-radius:0 !important;
                background-image:linear-gradient(115deg,
                    transparent 30%,
                    hsl(var(--spp-h1, 220) 88% 68% / .30) 45%,
                    hsl(var(--spp-h2, 278) 88% 68% / .22) 55%,
                    transparent 70%) !important;
                background-color:transparent !important;
                background-size:260% 100% !important;
                background-repeat:no-repeat !important;
                filter:none !important;
                backdrop-filter:none !important;
                -webkit-backdrop-filter:none !important;
                box-shadow:none !important;
                opacity:.9 !important;
                transform:none !important;
                animation:spp-sheen 6.5s linear infinite !important;
                animation-delay:calc(var(--spp-idx, 0) * -0.42s) !important;
                animation-play-state:running !important;
                pointer-events:none !important;
            }
            /* 扫掠:只跑 background-position,比 transform 更不易触发重排 */
            @keyframes spp-sheen {
                0%   { background-position:130% 0 }
                100% { background-position:-130% 0 }
            }

            /* ④ 整卡点击跳转的视觉层由 JS 注入(见 initCardClick),
               这里只给宿主一个定位上下文与手型光标。 */
            html.spp-board-skin #wall ul.stream > li { position:relative !important; }
            html.spp-board-skin #wall ul.stream > li.spp-card-clickable { cursor:pointer !important; }
            /* ★★ 交互元素要能接到鼠标,抬的【不是它们自己,而是它们的容器】。
               本轮实测(本地复刻点击层 + elementFromPoint):
                 li > .inner > div.spp-card-meta 被内容层规则定为 position:relative + z-index:1,
                 而 z-index != auto 就【自成堆叠上下文】—— 里面无论给按钮写多高的 z-index,
                 都被锁在这个 z-index:1 的上下文里,永远超不过点击层的 5。
                 实测读数:修复前 over btn = DIV.spp-card-hit;把容器抬到 6 之后
                 over btn = BUTTON.spp-metabtn、over fav = SPAN.spp-fav-txt。
               ⇒ 2.45.0 起那三条「给 .spp-fav / .spp-name-txt / .section-title a 写 z-index:6」
                 的规则其实一直是【无效】的:它们都在 z-index:1 的容器内。
                 收藏按钮此前能用,靠的是点击层的 click 处理器主动让路
                 (见 initCardClick:closest('a,button,…') 命中就 return),不是靠那条 z-index。
                 <button> 也在那个让路名单里,所以【点击】一直正常;
                 缺的是【hover】:鼠标被点击层接走,button:hover 不触发,动效就看不见。
                  ★★ 2.55.24 更正 —— 上面这两句只对【有封面卡】成立,别照它推断无图卡:
                     无图卡(.spp-tl-list)的 .inner **本体**另有 2.50.4 那道玻璃
                     (backdrop-filter)⇒ .inner 自成堆叠上下文,整块压在命中层之下,
                     连「让路」都轮不到:点收藏会去开帖子、:hover 根本不触发。
                     实测 elementFromPoint(收藏中心) 在有 / 无该属性时分别是
                     A.spp-card-hit / SPAN.spp-fav-txt。机制与修法见下面 2.55.24 那段。

               ★ 光把容器抬上去会带来新问题:数据行整条会盖住点击层,
                 于是「点卡片空白处跳转」在数据行这一片失效。
                 所以配 pointer-events 分层:
                   容器 = none(鼠标穿透到点击层,保住整卡点击)
                   交互子元素 = auto(自己重新接住鼠标)
                 pointer-events 是可继承属性:给 <a>/<button> 写 auto,
                 它们的子元素(图标、数字)自动跟着 auto,不必逐个声明。
                 实测:按钮 -> BUTTON.spp-metabtn,作者名 -> A.spp-name-txt。 */
            html.spp-board-skin #wall ul.stream > li > .inner > div.spp-card-meta,
            html.spp-board-skin #wall ul.stream > li > .inner > span.section-intro {
                z-index:6 !important;
                pointer-events:none !important;
            }
            html.spp-board-skin #wall ul.stream > li > .inner > div.spp-card-meta a,
            html.spp-board-skin #wall ul.stream > li > .inner > div.spp-card-meta button,
            html.spp-board-skin #wall ul.stream > li > .inner > span.section-intro a {
                pointer-events:auto !important;
            }
            html.spp-board-skin #wall ul.stream > li .spp-fav,
            html.spp-board-skin #wall ul.stream > li .spp-name-txt,
            html.spp-board-skin #wall ul.stream > li .section-title a { position:relative !important; z-index:6 !important; }

            /* ==================== 2.55.24:无图卡的 .inner 自己也是堆叠上下文 ====================
               ★★ 用户 BUG 反馈(thread_new.php?fid-9-page-1.html):
                    「无图片的卡片样式底部的收藏按钮不能正常点击;
                      回复和热度的动效也消失了」—— 有封面卡一切正常。

               ★ 机制:2.50.4 给无图卡的「黑色玻璃」把 backdrop-filter 写在了 .inner **本体**上。
                 该属性(与 filter / transform / opacity<1 同类)会**为该元素建立堆叠上下文**,
                 而 .inner 的 z-index 是 auto ⇒ 它落在「0 层」,其内部【一切】都被锁在这一层里。
                   · 上面刚抬到 z-index:6 的 .spp-card-meta 照样被封顶;
                   · 更狠的是 .inner 自己整块就在命中层 .spp-card-hit(z-index:5)之下。
                  命中测试先问绘制在上面的元素 ⇒ 收藏按钮与 .spp-metabtn 永远轮不到被指到:
                    点收藏 → 命中层接走 → 变成「打开帖子」(收藏动作丢失);
                    button:hover 不触发 → 「回复 / 热度」那一下弹性回弹动效消失。
                 有封面卡的玻璃写在 .inner::before(伪元素建上下文,不碰 .inner 本体)⇒
                 那边 z-index:6 > 5 依旧成立。这就是「只有无图卡坏」的全部差别。

               ★ 单变量活体对拍(同一张无图卡,只切走过一个属性,DOM 与其它 CSS 一字未动):
                    backdrop-filter:blur(18px) saturate(140%)
                        → elementFromPoint(收藏中心) = A.spp-card-hit
                        → elementFromPoint(回复按钮中心) = A.spp-card-hit
                    同一元素上临时置 none
                        → SPAN.spp-fav-txt / BUTTON.spp-metabtn
                    再移除覆盖(恢复玻璃)
                        → 又回到 A.spp-card-hit
                  对照:有封面卡 .inner 的 backdrop-filter 计算值是 none,两处一直是真元素。
                  (所以 2.52.66 写在上面的「收藏按钮此前能用、【点击】一直正常」只对
                    有封面卡成立;无图卡自 2.50.4 起一直是坏的,那里已就地更正。)

               ★ 修法 = 把上面那套「抬容器 + pointer-events 分层」再抬一级:
                  2.45.0 抬的是 .inner 内部的 .spp-card-meta,本轮要抬的是无图卡的 .inner 本身。
                  给它 z-index:6 站到命中层之上,再整块 pointer-events:none(鼠标穿透回命中层),
                  只让真正的交互元素 a / button 用 pointer-events:auto 自己接回来。
                  pointer-events 可继承:图标、数字、按钮内的 span 自动跟着 auto,不必逐个写。
               ★ 为什么 pointer-events:none 不能省:只抬 z-index 的话 .inner 会整片盖住命中层,
                  「点卡片空白处跳转」在无图卡上直接失效(有封面卡当年正是踩过这个坑,
                  见上面那段长注释)。实测补上之后:
                    收藏中心 → SPAN.spp-fav-txt;回复按钮中心 → BUTTON.spp-metabtn;
                    标题文字 → A(标题链本体,target=_blank,与整卡同地址);
                    卡片空白处 → A.spp-card-hit(整卡点击一分不丢)。
               ★ 只写 .spp-tl-list 一类:有封面卡的 .inner 不是堆叠上下文,不需要它,
                  少写一条就少一条可能与既有规则打架的规则。
               ★ 视觉零变化:本轮只动 z-index 与 pointer-events,不碰颜色 / 尺寸 / 模糊 / 任何节点。 */
            html.spp-board-skin #wall ul.stream > li.spp-tl-list > .inner {
                z-index:6 !important;
                pointer-events:none !important;
            }
            html.spp-board-skin #wall ul.stream > li.spp-tl-list > .inner a,
            html.spp-board-skin #wall ul.stream > li.spp-tl-list > .inner button {
                pointer-events:auto !important;
            }

            .spp-card-hit {
                /* ★ 2.52.66:命中层由 div 改成 <a> —— 它是空链接(无文本、无背景),
                   但站点/全局链接样式可能给 a 设 display、background 或下划线,
                   这里显式钉成"一块透明覆盖层",避免出现可见的链接色块。 */
                display:block !important;
                background:transparent !important;
                text-decoration:none !important;
                position:absolute !important; inset:0 !important;
                z-index:5 !important;
                cursor:pointer !important;
                border-radius:10px !important;
            }

            /* ==================== 2.49.0 版块页:方位卡片(横卡宽矮 / 竖卡窄高) ====================
               用户确认稿 preview/spp-orient-cards-preview.html 的落地。

               ★ 与 2.45.0「三列等高网格」的根本区别:
                 2.45.0 起所有有图卡一律 16:9 头图、同一尺寸,是「整齐但一刀切」;
                 2.49.0 承认【横图与竖图的天然形状不同】,给两类不同的卡片尺寸:
                   横卡(封面 ar ≥ 1):12 栏里占 4 栏 → 一行 3 张,图片区 3:2
                   竖卡(封面 ar < 1):12 栏里占 3 栏 → 一行 4 张,图片区 3:4
                 横卡宽矮、竖卡窄高,各自贴合自己的图,不再互相将就。

               ★ 为什么用 12 栏而不是直接改 grid-template-columns:
                 站点与 2.45.0 都把网格定为 3 列。12 栏是 3 与 4 的公倍数,
                 横卡 span 4 = 老的「一行 3 张」(完全等价,不改变既有观感),
                 竖卡 span 3 = 「一行 4 张」(新增)。这样两代规则共用同一个网格定义,
                 不需要动 2.45.0 的 grid-template-columns,也就不会波及窄屏降级。
                 ★ 窄屏(@media 1000px→2 栏 / 640px→1 栏)下 12 栏仍成立:
                   浏览器会把 span 4 / span 3 放进当前列数里换行,降级照旧生效。

               ★ 图片区高度:横卡从 16:9(218px)改为 3:2(258px)。
                 用户要求「横屏卡片图片显示区域加大」。卡片宽度受容器约束动不了,
                 只能加高。实测 55 张真封面:
                   16:9 可见像素均值 71,711 → 3:2 时 80,977(+12.9%)。
                 注意「可见比例」反而略降(85%→81%,因为框变大、裁得更多),
                 但最终露出的像素更多 —— 这才是「更清晰」的来源。

               ★ 图片填充(用户明确的三条规则):
                 图小于框 → 等比放大到框;图大于框 → 等比缩到框;放不下时顶部对齐。
                 落地即 object-fit:cover + object-position:center top。
                 ★ object-position 必须写完整两值:只写 top 会把横向重置成 0%(左对齐),
                   宽图会被裁掉右边全部、只留最左侧。本站 2.43.0/2.44.0 段的缩略图
                   本来就是 center top(见上文本段 L1244 / L1393),此处与之一致。 */
            html.spp-board-skin #wall ul.stream {
                grid-template-columns:repeat(12, minmax(0, 1fr)) !important;
            }
            /* 横卡:占 4 栏(= 一行 3 张,与 2.45.0 的观感完全一致) */
            html.spp-board-skin #wall ul.stream > li:not(.spp-tl-list):not(.spp-card-portrait) > .inner {
                grid-column:span 4 !important;
            }
            html.spp-board-skin #wall ul.stream > li:not(.spp-tl-list):not(.spp-card-portrait) {
                grid-column:span 4 !important;
            }
            /* ★★ 无图卡也必须显式给 span 4 —— 这是本轮踩到的第二个几何坑:
                 上面两条的 :not(.spp-tl-list) 把无图卡排除在外,竖卡那条也只管
                 .spp-card-portrait,于是无图卡三条规则都不匹配 → grid-column 回落成
                 auto(= span 1),实测卡片只有 89px 宽、被压成一列细条。
                 而 2.45.0 的「三列」是靠 grid-template-columns 定的,不依赖 span,
                 所以老代码里没有这个问题 —— 是我把网格改成 12 栏后才新引入的。
                 ⇒ 教训:把「容器列数」改成「子项 span」时,必须为【每一个】子项类别
                   都写 span,漏掉的那类会静默塌成一栏。 */
            html.spp-board-skin #wall ul.stream > li.spp-tl-list {
                grid-column:span 4 !important;
            }
            /* 竖卡:占 3 栏(= 一行 4 张,卡片收窄,给竖图腾地方) */
            html.spp-board-skin #wall ul.stream > li.spp-card-portrait {
                grid-column:span 3 !important;
            }
            /* ★ 行首:每组第一张强制从第 1 栏开始 = 另起一行(用户要求竖卡、无图卡各自单独起一行)。
               ★ 必须写成 grid-column:1 / span N(起止都写):
                 只写 grid-column-start:1 的话,end 仍是 auto(= span 1),卡片会缩成 1 栏宽。
                 而竖卡是 span 3、横卡与无图卡是 span 4,两个值不同,所以两条分开写。 */
            html.spp-board-skin #wall ul.stream > li.spp-row-start:not(.spp-card-portrait) {
                grid-column:1 / span 4 !important;
            }
            html.spp-board-skin #wall ul.stream > li.spp-row-start.spp-card-portrait {
                grid-column:1 / span 3 !important;
            }
            /* 竖卡图片区 3:4。
               ★ 选择器加深到 > .inner > .section-text:2.47.0 那条是
                 「li:not(.spp-tl-list) .section-text」(同为 1 id / 4 class / 3 elem),
                 不加深就只靠书写顺序决胜 —— 而这个坑 2.48.0 已经在预览里踩过一次
                 (scope() 前缀改写后两条权重打平)。加深一级,不依赖顺序。 */
            html.spp-board-skin #wall ul.stream > li.spp-card-portrait > .inner > .section-text {
                aspect-ratio:3 / 4 !important;
                /* ★★ height:auto 必须一起写 —— 本轮真正踩到的坑:
                   2.45.0 段给 .section-text 定了 height:150px !important,而
                   aspect-ratio 与显式 height 并存时【height 优先】,ratio 改不了已定的高。
                   实测:只加 aspect-ratio:3/2 之后图片区仍是 150px,守卫却全绿 ——
                   因为守卫只在 CSS 文本里搜到了 aspect-ratio 字样,没有一条断言「它真的生效」。
                   2.47.0 之所以没这个问题,正是因为它一并写了 height:auto。 */
                height:auto !important;
            }
            /* 横卡图片区 16:9 → 3:2(同样必须 height:auto,理由见上) */
            html.spp-board-skin #wall ul.stream > li:not(.spp-tl-list):not(.spp-card-portrait) > .inner > .section-text {
                aspect-ratio:3 / 2 !important;
                height:auto !important;
            }
            /* 无图卡的极光占位区跟着横卡一起加高,否则同行的有图卡与无图卡不等高。
               2.47.0 用 aspect-ratio:16/9 让两者同高,现在有图卡变成 3:2,
               占位区必须同步 —— 这正是 2.47.0 注释里「只改一处,两类卡立刻不等高」的兑现。
               ★ 加 :not(.spp-card-portrait) 把权重抬到 5 class,不靠书写顺序。
               ★ 同样要 height:auto:2.45.0 给这个伪元素写过 height:112px。 */
            html.spp-board-skin #wall ul.stream > li.spp-tl-list:not(.spp-card-portrait) > .inner::before {
                aspect-ratio:3 / 2 !important;
                height:auto !important;
            }
            /* 图片填充:等比铺满 + 顶部对齐(用户三条规则)。
               ★ object-position 必须写完整两值:只写 top 会把横向重置成 0%(左对齐),
                 宽图会被裁掉右边全部、只留最左侧。
               ★★ height:100% 必须一起写 —— 这是「height:150px 压住」的第二个化身:
                 2.45.0 段给 img 本身也定了 height:150px !important。我只改了 <img> 的
                 object-fit 而没管它的 height,于是 .section-text 涨到 3:2(258px)
                 而里面的 img 仍是 150px —— 框变大了、图没跟上,表现为「框下半截空掉」。
                 同一个坑在两个层级各出现一次:.section-text(框)与 img(图),
                 每一层都要显式写 height 才生效。
                 2.47.0 用 height:100% 解决过同一问题(见其
                 「.section-text > div img { height:100% }」那条),此处沿用该做法。 */
            html.spp-board-skin #wall ul.stream > li:not(.spp-tl-list) > .inner > .section-text > div img {
                object-fit:cover !important;
                object-position:center top !important;
                height:100% !important;
                /* ★★ 2.49.2 修复「竖卡图片区下半截空掉」:必须显式解除站点的封顶。
                   thread_new.css 里有一条
                     .stream li.dcsns-rss .section-text img { max-height:270px }
                   ★ 为什么 height:100% !important 压不住它:
                     max-height 与 height 不是同一层级的对手 —— max-height 是【封顶】,
                     height(哪怕带 !important)算出的 381px 会被 clamp 回 270px。
                     实测(竖卡 286×381 的框):img 恒为 270px、框下方空 111px,5 张全一致。
                   ★ 这是「框放大了、图没跟上」的第三个化身,也是最隐蔽的一个:
                     前两次是 height 之间互相压制(写 !important 就能赢),
                     这次是 max-height 单方面封顶 —— 只写 height 永远赢不了。
                   ★ 判据:只要给图片区设了 aspect-ratio / 拉高过 height,
                     就必须回来确认 <img> 的最终高度是不是被 max-height 截住了。 */
                max-height:none !important;
            }
            /* 无图卡的极光占位区跟着横卡一起加高,否则同行的有图卡与无图卡不等高。
               2.47.0 用 aspect-ratio:16/9 让两者同高,现在有图卡变成 3:2,
               占位区必须同步 —— 这正是 2.47.0 注释里「只改一处,两类卡立刻不等高」的兑现。
               ★ 加 :not(.spp-card-portrait) 把权重抬到 5 class,不靠书写顺序。
               ★ 同样要 height:auto:2.45.0 给这个伪元素写过 height:112px。 */
            html.spp-board-skin #wall ul.stream > li.spp-tl-list:not(.spp-card-portrait) > .inner::before {
                aspect-ratio:3 / 2 !important;
                height:auto !important;
            }

            /* ---- 窄屏降级必须在本段【重新声明】----
               ★ 上面把网格从 3 列改成 12 列,特异性是 (1 id, 2 class, 2 elem),
                 而 2.45.0 段末尾的 @media(#wall ul.stream,权重 (1,1,1))压不过它 ——
                 不重写的话,窄屏下 12 栏依然生效,降级彻底失效(实测会是这样)。
               12 与 8、4 都是整除关系,所以降级只是换一个栏数,span 值按比例跟着改。 */
            @media (max-width: 1000px) {
                html.spp-board-skin #wall ul.stream { grid-template-columns:repeat(8, minmax(0, 1fr)) !important; }
                html.spp-board-skin #wall ul.stream > li.spp-card-portrait { grid-column:span 4 !important; }
                html.spp-board-skin #wall ul.stream > li.spp-row-start.spp-card-portrait { grid-column:1 / span 4 !important; }
            }
            @media (max-width: 640px) {
                html.spp-board-skin #wall ul.stream { grid-template-columns:repeat(4, minmax(0, 1fr)) !important; }
                html.spp-board-skin #wall ul.stream > li.spp-card-portrait { grid-column:span 4 !important; }
                html.spp-board-skin #wall ul.stream > li.spp-row-start.spp-card-portrait { grid-column:1 / span 4 !important; }
            }

            /* ==================== 2.49.7:无图卡移除「图片显示区域」====================
               用户要求:「对于没有图床的卡片,可以移除其图片显示区域。」

               ★ 那块区域是什么:不是 .section-text(它早就 display:none 了),
                 而是 .inner 上的两个【伪元素】——
                   ::before  极光三光斑,在流内,靠 aspect-ratio:3/2 撑出 270.67px
                   ::after   流光扫掠层,靠 aspect-ratio:16/9 撑出 230.63px
                 历史脉络:2.45.0 用 padding-top:114px 顶出占位 → 2.47.0 把 ::before
                 放回流内、让它自己撑高(与有图卡同行等高)→ 2.48.0 加了 ::after 流光。
                 所以「移除」的正确做法是让这两个伪元素【不再生成】(content:none),
                 而不是继续调它们的 height。

               ★ 为什么连带放开 min-height:
                 2.49.4 起 横/竖/无图 三组【各自另起一行】,无图卡整行都是无图卡 ——
                 不再需要「靠占位区与有图卡同行等高」这个前提。
                 实测原值:li 高 413(标题 48 + 作者行 20 + 数据行 36 ≈ 104),
                 其中约 270px 是空的黑区。放开后卡片由内容决定高度。

               ★ 特异性说明:2.47.0 那条占位规则是
                   #wall ul.stream > li.spp-tl-list > .inner::before
                 本段每条都多 html.spp-board-skin(+1 类 +1 元素),
                 并写在整段【最后】—— 同权重时后写者胜。
                 不到这个权重的话 height / aspect-ratio 仍会生效(实测过)。 */
            html.spp-board-skin #wall ul.stream > li.spp-tl-list > .inner::before,
            html.spp-board-skin #wall ul.stream > li.spp-tl-list > .inner::after {
                content:none !important;
                display:none !important;
                height:auto !important;
                aspect-ratio:auto !important;
                background:none !important;
                animation:none !important;
                filter:none !important;
                opacity:0 !important;
            }
            /* 顺带关掉只服务那块区域的动效(留着会白跑动画) */
            html.spp-board-skin #wall ul.stream > li.spp-tl-list:hover > .inner::before {
                content:none !important;
                display:none !important;
            }
            /* 卡片高度交回内容:标题 + 作者行 + 数据行 */
            html.spp-board-skin #wall ul.stream > li.spp-tl-list > .inner {
                min-height:0 !important;
                padding-top:0 !important;
            }
        `);
        // ==================== 2.49.8:无图卡「极光 + 粒子」与去除卡片银角 ====================
        // ★★ 必须是【独立的一次 GM_addStyle】,不能并进上面那个大盘子里。
        //    原因不是洁癖:tools/build/build-aurora-preview.js 按锚点抽出上面那段,
        //    当作审批预览里的【关闭栏】。极光段若混在里面,两栏就都亮了,
        //    对照失效 —— 第一次落库正是被生成器的「关闭栏不得含新极光关键帧」拦下的。
        //    本段自带 html.spp-board-skin 前缀,因此仍然只在版块页生效。
        GM_addStyle(`
/* ===== SPP-GLASS-BEGIN ===== */
            /* ==================== 2.50.4:无图卡「玻璃质感」====================
               用户要求:「取消茶馆无图片卡片的极光特效,改为玻璃质感的卡片背景」。

               ★ 本代是【撤换】不是【叠加】:
                 2.49.8 的极光(::before 四团柔光)与粒子(::after 星点场)
                 在本代整体撤下 —— 两层都让它 content:none 不再生成。
                 天真的做法是把它们调暗,但调暗之后它仍然是极光;
                 用户要的是「取消」,所以是把这两层【去掉】,不是压暗。

               ★ 为什么「透」要交给 backdrop-filter,而不是画一层浅灰:
                 玻璃之所以是玻璃,是因为它背后有东西且被糊掉。
                 本站卡背是页面的星空背景,用 backdrop-filter 让星星透上来并糊开,
                 得到的才是真玻璃;在卡上刷一层半透明灰只会得到一块塑料。
                 blur 取 16px:这是「看得见背后有东西、但细节化开」的下限,
                 再大就糊成纯色板、玻璃感反而消失。

               ★ 为什么只给顶部一条 1px 内高光、不给整圈描边:
                 整圈描边在深色底上读起来就是「灰线」——
                 2.50.1 刚刚为此删过一次 <li> 上的 1px 边框。
                 玻璃的边缘反光在真实世界里也主要出现在上沿,一条 inset 高光足够。

               ★ 内容层不受影响(与 2.49.8 同一套论证):
                 2.45.0 已把卡内文字子项抬到 z-index:1,本段两层都在 z-index:0,
                 结构上不可能压到任何文字;本段也不写 filter / opacity 到 .inner 上。

               ★ 动效:没有。玻璃是静态材质。
                 本代刻意不再引入任何 @keyframes —— 用户这一轮的诉求是「换成玻璃」,
                 再补一层流光会重新把卡片变成会动的东西,与诉求相反。

               ★ 无障碍:不再有动效,因此本段也不再有 prefers-reduced-motion 分支。 */
            html.spp-board-skin #wall ul.stream > li {
                background:transparent !important;
                border:0 !important;
            }

            /* ★★ 2.50.4 修复:上面这条【本来住在被替换掉的极光段里】,本代必须带过来。
               2.49.8 把「去掉 <li> 白底」与「去掉 <li> 的 1px 灰边框」写在极光段内,
               而本代整段替换极光段时漏掉了它 —— 后果不是「灰线回来了」,
               而是【无图卡整片变白】:.inner 走 backdrop-filter,
               它会把背后那层白(实测 <li> 是 rgba(250,252,255,.94))一起糊进卡里。
               ★ 教训:整段替换一个 CSS 块时,必须逐条确认块内【与本代无关的既有规则】
                 也被继承。这两条既不是极光也不是粒子,却住在极光段里。

               ---- 黑色玻璃底 ----
               ★★ 不能用 background 简写:简写会把 background-color 一并重置为透明,
                  于是 #131a2a 那层「黑」被抹掉、只剩 1.4%~8.5% 的几处白高光 ——
                  那不是玻璃,是一层灰雾(实测整片发白,根因之一)。
                  黑色玻璃 = 【半透黑底】+ 叠加的镜面高光,两者分开写:
                    background-color → 透着星空的半透黑
                    background-image → 斜向 + 上沿的高光
                  blur 把它背后(星空 / 页面)的内容糊开透上来,这才是玻璃。 */
            html.spp-board-skin #wall ul.stream > li.spp-tl-list:not(.spp-card-portrait) > .inner {
                background-color:rgb(0 0 0 / 0.86) !important;
                background-image:linear-gradient(152deg,
                    rgb(255 255 255 / 0.05) 0%,      /* 2.55.30:同族高光一起压暗(.11 → .05),理由见 2.55.30 段头 */
                    rgb(255 255 255 / 0.016) 22%,
                    rgb(255 255 255 / 0.003) 46%,
                    rgb(255 255 255 / 0.000) 62%,
                    rgb(255 255 255 / 0.045) 100%) !important;
                backdrop-filter:blur(18px) saturate(140%) !important;
                -webkit-backdrop-filter:blur(18px) saturate(140%) !important;
                border:1px solid rgb(255 255 255 / 0.09) !important;
                box-shadow:0 12px 32px -14px rgb(0 0 0 / 0.72),
                    inset 0 1px 0 rgb(255 255 255 / 0.22) !important;
            }
            /* hover 时把 2.45.0 那道蓝光【并回来】,而不是让它被上面的 box-shadow 顶掉。
               (本段比 2.45.0 的 hover 规则特异性更高,不显式并回就永久丢失悬停反馈。) */
            html.spp-board-skin #wall ul.stream > li.spp-tl-list:not(.spp-card-portrait):hover > .inner {
                box-shadow:0 0 24px -2px rgb(61 106 255 / 0.5),
                    0 12px 32px -14px rgb(0 0 0 / 0.72),
                    inset 0 1px 0 rgb(255 255 255 / 0.22) !important;
            }
            /* 两层伪元素本代都不生成:极光与粒子整体撤下。
               ★ 必须把 2.49.8 写过的每一项视觉属性都显式复位 ——
                 只写 content:none 时,某些浏览器仍会保留该伪元素的
                 background / animation 参与合成(2.49.7 的注释里记过同类情况)。 */
            html.spp-board-skin #wall ul.stream > li.spp-tl-list:not(.spp-card-portrait) > .inner::before,
            html.spp-board-skin #wall ul.stream > li.spp-tl-list:not(.spp-card-portrait) > .inner::after {
                content:none !important;
                display:none !important;
                background:none !important;
                background-image:none !important;
                background-color:transparent !important;
                animation:none !important;
                filter:none !important;
                backdrop-filter:none !important;
                -webkit-backdrop-filter:none !important;
                box-shadow:none !important;
                opacity:0 !important;
                height:auto !important;
                aspect-ratio:auto !important;
                -webkit-mask-image:none !important;
                mask-image:none !important;
            }
/* ===== SPP-GLASS-END ===== */
        `);

GM_addStyle(`
/* ===== SPP-ANNOUNCE-BEGIN ===== */
            /* ==================== 2.50.6:公告板块(折叠 + 无图卡玻璃质感)====================
               用户要求:「参考无图片的卡片格式,制作一个板块,这个板块内都是截图内的
               链接入口,并且这一个板块属于公告板块,默认情况是折叠的,但是可以手动展开」。

               ★ 来源:页面上的置顶表 #ajaxtable。实测(thread_new.php?fid-9):
                 #ajaxtable 共 14 行 —— 1 行分类 tab、2 行空行、1 行表头 tr2、
                 10 行置顶/公告、1 行空行;**没有任何普通主题行**。
                 也就是说用户截图那张表整体就是「置顶公告表」。
               ★ 为什么不用文字判定(比如标题里带「公告」二字):
                 普通帖的标题同样可能带「公告」二字,会误伤。
                 站点给了两个确切的标记,本段只读这两个:
                   置顶帖   → 行内 img[title="置顶帖标志"](headtopic_N.gif)
                   论坛公告 → a[href*="notice.php"](行首 anc.gif)
                 这两个判据是【上游站点自己打的标】,与脚本里
                 evaluateCardImages / markCardOrientation 的「上游打标、下游读标」同法。
               ★ 为什么元素是 <div> 不是 <a>:
                 整行做 <a> 而内部又有作者名/按钮时,嵌套 <a> 是非法 HTML,
                 浏览器会把外层提前闭合 —— 预览阶段实测过「作者名整段消失、页面不报错」。
                 故采用撑满式链接:行 = div,标题 = 真正的 <a>,其 ::after 铺满整行当热区,
                 按钮抬到 z-index:2 之上。整行可点、中键与右键正常、按钮不误触跳转。
               ★ 折叠为什么用 grid-template-rows 的 0fr↔1fr:
                 不想在 JS 里量高度再写死 px —— 那样每条数变化都要重算,
                 而且动画期间高度变化会引起整页回流。0fr↔1fr 由浏览器自己插值,
                 条数增减都不用改任何数值。
               ★ 玻璃底逐字同步自 SPP-GLASS 段(用户要求「参考无图片的卡片格式」):
                 background-color:rgb(0 0 0 / 0.86) —— 半透黑,星空透得上来;
                 background-image: 斜向高光。两者【必须分开写】,不能用 background 简写
                 (简写会把 background-color 一并重置为透明,2.50.5 刚修过这个坑)。 */
            :is(#spp-announce, #spp-bnotice, #spp-btabs) {
                position:relative !important;
                /* ★ 字体:实测本机 Noto Sans SC 可用(document.fonts.check 为 true),
                   所以维持无衬线优先栈,末尾【显式落到 sans-serif】——
                   不写死 Arial,也不留 serif 回退(宋体在小字号下会发虚)。
                   清晰度的真问题是颜色与字号,不是字形本身。 */
                font-family:"Noto Sans SC","Source Han Sans SC","PingFang SC","Microsoft YaHei",system-ui,sans-serif !important;
                -webkit-font-smoothing:antialiased !important;
                margin:0 0 12px !important;
                border-radius:12px !important;
                overflow:hidden !important;
                background-color:rgb(0 0 0 / 0.86) !important;
                background-image:linear-gradient(152deg,
                    rgb(255 255 255 / 0.05) 0%,      /* 2.55.30:同族高光一起压暗(.11 → .05),理由见 2.55.30 段头 */
                    rgb(255 255 255 / 0.016) 22%,
                    rgb(255 255 255 / 0.003) 46%,
                    rgb(255 255 255 / 0.000) 62%,
                    rgb(255 255 255 / 0.045) 100%) !important;
                backdrop-filter:blur(18px) saturate(140%) !important;
                -webkit-backdrop-filter:blur(18px) saturate(140%) !important;
                border:1px solid rgb(255 255 255 / 0.09) !important;
                box-shadow:0 12px 32px -14px rgb(0 0 0 / 0.72),
                    inset 0 1px 0 rgb(255 255 255 / 0.22) !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-head {
                display:flex !important; align-items:center !important; gap:10px !important;
                width:100% !important;
                padding:11px 14px !important; margin:0 !important;
                background:transparent !important; border:0 !important;
                cursor:pointer !important;
                color:#dfe6f2 !important;
                font-family:inherit !important; font-size:14px !important;
                text-align:left !important;
                transition:background .25s ease !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-head:hover { background:rgba(255,255,255,.05) !important; }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-head:focus-visible {
                outline:2px solid rgb(120 160 255 / .7) !important; outline-offset:-2px !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-title {
                font-size:14.5px !important; font-weight:700 !important; letter-spacing:.4px !important;
                color:#f4f7fd !important; -webkit-text-fill-color:#f4f7fd !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-count {
                font-size:11.5px !important; font-weight:700 !important; line-height:1.6 !important;
                padding:0 8px !important; border-radius:999px !important;
                color:#dbe6f7 !important; -webkit-text-fill-color:#dbe6f7 !important;
                background:rgba(120,150,220,.20) !important;
                border:1px solid rgba(150,180,240,.30) !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-hint {
                font-size:12px !important;
                color:#a9b6cc !important; -webkit-text-fill-color:#a9b6cc !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-chev {
                margin-left:auto !important;
                width:9px !important; height:9px !important; flex:0 0 9px !important;
                border-right:1.6px solid #93a0b8 !important; border-bottom:1.6px solid #93a0b8 !important;
                transform:rotate(45deg) translate(-1px,-1px) !important;
                transition:transform .38s cubic-bezier(.4,0,.2,1) !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs)[data-open="1"] .spp-ann-chev {
                transform:rotate(-135deg) translate(-2px,-2px) !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-body {
                display:grid !important;
                grid-template-rows:0fr !important;
                transition:grid-template-rows .42s cubic-bezier(.4,0,.2,1) !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs)[data-open="1"] .spp-ann-body { grid-template-rows:1fr !important; }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-inner { overflow:hidden !important; }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-list {
                padding:2px 6px 8px !important;
                border-top:1px solid rgba(255,255,255,.07) !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-item {
                position:relative !important;
                display:grid !important;
                grid-template-columns:26px minmax(0,1fr) auto !important;
                grid-template-areas:"dot title meta" "dot byline meta" !important;
                align-items:center !important; column-gap:10px !important; row-gap:2px !important;
                padding:9px 8px !important; margin:1px 0 !important;
                border-radius:9px !important;
                transition:background .22s ease !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-item:hover { background:rgba(255,255,255,.055) !important; }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-dot {
                grid-area:dot !important;
                -webkit-text-fill-color:currentColor !important;
                display:flex !important; align-items:center !important; justify-content:center !important;
                width:22px !important; height:22px !important; border-radius:6px !important;
                font-size:10px !important; font-weight:700 !important; line-height:1 !important;
                border:1px solid transparent !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-dot[data-g="告"] {
                color:#ffb3b3 !important; background:rgba(245,108,108,.16) !important; border-color:rgba(245,108,108,.30) !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-dot[data-g="锁"] {
                color:#bfcbe0 !important; background:rgba(150,170,200,.13) !important; border-color:rgba(150,170,200,.24) !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-dot[data-g="热"] {
                color:#f5c48a !important; background:rgba(230,162,60,.15) !important; border-color:rgba(230,162,60,.30) !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-t {
                grid-area:title !important;
                font-size:14.5px !important; font-weight:600 !important; line-height:1.55 !important;
                color:#eef3fb !important; -webkit-text-fill-color:#eef3fb !important;
                display:-webkit-box !important;
                -webkit-line-clamp:2 !important; -webkit-box-orient:vertical !important;
                overflow:hidden !important; word-break:break-word !important;
            }
            /* ★★ 2.50.8:必须同时写 -webkit-text-fill-color。
               实测:本规则原来只写 color:#e7edf8 !important,但计算值恒为
               rgb(47,111,104)(TEA_SKIN 给全站链接设的墨绿填充色)——
               因为 -webkit-text-fill-color 的优先级【高于】color,
               只写 color 时它纹丝不动,连行内 !important 都改不动。
               这是「浅得看不清」的真根因,不是字号问题。 */
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-t a {
                color:#f2f6fd !important;
                -webkit-text-fill-color:#f2f6fd !important;
                text-decoration:none !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-t a:hover {
                color:#ffffff !important;
                -webkit-text-fill-color:#ffffff !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-t a::after {
                content:'' !important; position:absolute !important; inset:0 !important; border-radius:9px !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-t a:focus-visible::after {
                outline:2px solid rgb(120 160 255 / .75) !important; outline-offset:-1px !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-item:hover .spp-ann-t a { color:#fff !important; }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-meta {
                grid-area:meta !important;
                display:flex !important; align-items:center !important; gap:8px !important;
                margin-left:10px !important;
                position:relative !important; z-index:2 !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-byline {
                grid-area:byline !important;
                display:flex !important; align-items:center !important; gap:8px !important; min-width:0 !important;
            }
            /* ★ 作者名/日期在公告里的专属覆盖:卡片那套选择器带 #wall,公告不在 #wall 内,
               所以这里必须自己写一遍,否则日期只有 4.57:1(偏灰、发虚)。
               同时显式写 -webkit-text-fill-color —— 理由同标题。 */
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-name-txt {
                font-size:12px !important; font-weight:600 !important;
                color:#e2e9f6 !important; -webkit-text-fill-color:#e2e9f6 !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-date-txt {
                font-size:11.5px !important; font-weight:500 !important;
                color:#bccbe4 !important; -webkit-text-fill-color:#bccbe4 !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-nodata {
                font-size:12px !important;
                color:#c3cfe2 !important; -webkit-text-fill-color:#c3cfe2 !important;
                padding:3px 10px !important; border-radius:999px !important;
                border:1px dashed rgba(160,180,210,.42) !important;
            }
            /* ★ 2.52.2:本版公告条里的站点原始富文本(版规)。
               只定「容器 + 文字色 + 链接色」三件事:
                 · 容器:站点 body 字色是 rgb(85,85,85) 深灰,落在深色玻璃底上读不出来,必须提亮;
                 · 链接:必须同时写 -webkit-text-fill-color(TEA_SKIN 给全站链接设了填充色,
                   填充色优先级高于 color,只写 color 纹丝不动 —— 同 2.50.8 那条坑);
                 · 刻意【不】给容器写 -webkit-text-fill-color:它会被子元素继承,
                   而版规用 span[style="color:…"] 做红/蓝强调,写下去会把强调一并刷平。
               内容一个字不改(站点自己写的 b/font/span/br/链接原样搬到条内)。 */
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-raw {
                padding:10px 16px 14px !important;
                border-top:1px solid rgba(255,255,255,.07) !important;
                font-size:13px !important; line-height:1.8 !important;
                color:#d7e0ee !important;
                word-break:break-word !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-raw img {
                max-width:100% !important; height:auto !important; vertical-align:middle !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-raw a {
                color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
                text-decoration:none !important;
            }
            :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-raw a:hover {
                color:#c9ffe4 !important; -webkit-text-fill-color:#c9ffe4 !important;
                text-decoration:underline !important;
            }

            /* ==================== 2.52.6:公告区按钮化(方案 A)====================
               用户定稿:公告条上方一行模板按钮,点按钮把下方共用状态栏切到对应模板并展开,
               状态栏默认折叠;子版块表与版头图从页面原位搬进面板。
               ★ 按钮必须加进通用按钮规则的 :not() 排除链(本文件 4 处已加 :not(.spp-tab)):
                 TEA_SKIN 那条通用按钮规则是 :not(#spp-nav-panel button):not(.spp-btn)…(6 个 id),
                 特异度远超 .spp-tab 这类单类名 —— 不排除就会被刷成主色实底按钮。
               ★ 搬进来的站点表格要还原成透明底:站点那套是白底表格(tr1/tr3 有底色),
                 落在深玻璃上就是一块白板。这里只清 background-color,【绝不用 background 简写】——
                 简写会把版块图标的 background-image 一起清掉(2.50.4 踩过同一个坑)。 */
            #spp-btabs {
                display:flex !important; align-items:center !important; flex-wrap:wrap !important;
                padding:0 8px !important;
            }
            #spp-btabs .spp-tab {
                display:flex !important; align-items:center !important; gap:7px !important;
                padding:7px 14px !important; margin:0 !important;
                background-color:#09090b !important; background-image:none !important;
                border:1.58px solid #52525b !important; cursor:pointer !important;
                color:#e2e8f0 !important; -webkit-text-fill-color:#e2e8f0 !important;
                font-family:inherit !important; font-size:13.5px !important; font-weight:500 !important;
                letter-spacing:.3px !important; border-radius:12px !important;
                box-shadow:0 4px 6px -1px rgba(0,0,0,.40), 0 2px 4px -2px rgba(0,0,0,.40) !important;
                transform:none !important;
                transition:transform .3s ease, box-shadow .3s ease, background-color .3s ease,
                           color .3s ease, border-color .3s ease !important;
            }
            #spp-btabs .spp-tab:hover {
                background-color:#18181b !important;
                color:#fff !important; -webkit-text-fill-color:#fff !important;
                transform:translateY(-.335rem) !important;
                box-shadow:0 20px 25px -5px rgba(0,0,0,.55), 0 8px 10px -6px rgba(0,0,0,.45) !important;
            }
            #spp-btabs .spp-tab.is-on {
                color:#fff !important; -webkit-text-fill-color:#fff !important;
                background-color:rgba(32,201,151,.14) !important;
                border-color:rgba(32,201,151,.80) !important;
                box-shadow:inset 0 -2px 0 rgba(32,201,151,.75) !important;
            }
            #spp-btabs .spp-tab-n {
                font-size:11px !important; font-weight:700 !important; line-height:1.6 !important;
                padding:0 7px !important; border-radius:999px !important;
                color:#dbe6f7 !important; -webkit-text-fill-color:#dbe6f7 !important;
                background-color:rgba(120,150,220,.20) !important;
                border:1px solid rgba(150,180,240,.30) !important;
            }
            #spp-btabs .spp-tab-div {
                width:1px !important; height:20px !important; flex:0 0 1px !important;
                background-color:rgba(255,255,255,.16) !important; margin:0 4px !important;
            }
            /* 按钮行与共用面板拼成一块玻璃:上圆下圆,接缝处一条分隔线 */
            #spp-btabs.spp-paired { margin:0 !important; border-radius:12px 12px 0 0 !important; }
            #spp-announce.spp-paired {
                margin:0 !important; border-radius:0 0 12px 12px !important;
                border-top:1px solid rgba(255,255,255,.10) !important;
            }
            /* 模板面板:一次只显示一个 */
            #spp-announce .spp-pane { display:none !important; }
            #spp-announce .spp-pane.is-on { display:block !important; }
            /* 子版块(站点原表搬进来)*/
            #spp-announce .spp-pane-subs { padding:2px 6px 8px !important; }
            #spp-announce .spp-pane-subs div,
            #spp-announce .spp-pane-subs table,
            #spp-announce .spp-pane-subs tbody,
            #spp-announce .spp-pane-subs tr,
            #spp-announce .spp-pane-subs td,
            #spp-announce .spp-pane-subs th {
                background-color:transparent !important;
                border-color:rgba(255,255,255,.08) !important;
            }
            #spp-announce .spp-pane-subs td,
            #spp-announce .spp-pane-subs th { color:#dfe6f2 !important; }
            #spp-announce .spp-pane-subs a {
                color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
                text-decoration:none !important;
            }
            /* 版头图(站点原图搬进来)*/
            #spp-announce .spp-pane-cover { padding:12px 16px 16px !important; }
            #spp-announce .spp-pane-cover img {
                display:block !important; max-width:100% !important; height:auto !important;
                margin:0 auto !important; border-radius:8px !important;
            }

            /* 窄屏:按钮换到第二行,别把标题挤没 */
            @media (max-width:760px) {
                :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-item {
                    grid-template-columns:26px minmax(0,1fr) !important;
                    grid-template-areas:"dot title" "dot byline" "dot meta" !important;
                }
                :is(#spp-announce, #spp-bnotice, #spp-btabs) .spp-ann-meta { margin-left:0 !important; }
            }
            /* ==================== 2.52.8:按钮皮肤 + 分类面板 ====================
               ★ 按钮皮肤来源:uiverse 组件 Button by thxhtrxn
                 https://uiverse.io/thxhtrxn/warm-baboon-50 (MIT 许可,署名保留)
                 原版:rounded-xl(12px) / border 1.58px zinc-600(#52525b) / bg zinc-950(#09090b)
                       / px-5 py-3 / text-slate-200 / shadow-md / transition-all .3s
                       / hover: translateY(-.335rem) + shadow-xl
                 本处为「改造」而非照搬:padding 由 20/12 缩到状态栏按钮尺度(7px 14px);
                 选中态改用本站主色绿(不引入新色系)。皮肤化之后按钮是独立胶囊,
                 原来那三条 1px 竖分隔线不再需要(与预览一致:隐藏 + 统一 gap)。
               ★ 分类面板:站点分类行 ul.threadlist 原样搬进来,只改外观(不动 href,
                 点击仍由站点自己处理;脚本另在 initWallMode 里保证切图墙不丢筛选)。 */
            #spp-btabs { gap:8px !important; padding:6px 10px !important; }
            #spp-btabs .spp-tab-div { display:none !important; }
            /* 分类面板:站点导航行改成胶囊(有主题分类的版块) */
            #spp-announce .spp-pane-cats { padding:12px 14px 10px !important; }
            #spp-announce .spp-pane-cats ul.threadlist {
                display:flex !important; flex-wrap:wrap !important; align-items:center !important;
                gap:8px !important; margin:0 !important; padding:0 !important;
                position:static !important; bottom:auto !important; height:auto !important;
                overflow:visible !important; float:none !important;
            }
            #spp-announce .spp-pane-cats ul.threadlist > span { display:contents !important; }
            #spp-announce .spp-pane-cats ul.threadlist li {
                display:block !important; float:none !important; list-style:none !important;
                margin:0 !important; padding:0 !important;
            }
            #spp-announce .spp-pane-cats li > a {
                display:inline-flex !important; align-items:center !important; height:28px !important;
                padding:0 13px !important; border-radius:999px !important;
                border:1px solid rgba(255,255,255,.12) !important;
                background-color:rgba(255,255,255,.05) !important;
                color:#c8d6ea !important; -webkit-text-fill-color:#c8d6ea !important;
                font-size:12.5px !important; text-decoration:none !important; white-space:nowrap !important;
            }
            #spp-announce .spp-pane-cats li > a:hover {
                background-color:rgba(255,255,255,.10) !important;
                color:#fff !important; -webkit-text-fill-color:#fff !important;
            }
            #spp-announce .spp-pane-cats li.current > a {
                color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
                background-color:rgba(32,201,151,.14) !important;
                border-color:rgba(32,201,151,.45) !important; font-weight:600 !important;
            }
            /* ==================== 2.52.16:分类行(只有「全部/精华」的版块)搬进状态栏 ====================
               ★ 与「分类」面板那套的区别:不进面板,直接在 #spp-btabs 里当一枚胶囊外壳。
                 a 的全部字色都同时写 color 与 -webkit-text-fill-color —— 全站链接皮肤链带填充色,
                 只写 color 压不住(2.52.15 同一个坑);本行内的 a 早已在该链的 :not(#spp-btabs a) 排除表里。
               ★ 站点 ul.threadlist 自带 position:absolute / bottom / float 那套(面板里也做了同款复位),
                 搬进 flex 行必须逐条复位,否则会脱层漂浮。 */
            #spp-btabs .spp-catrow {
                display:flex !important; align-items:center !important;
                gap:6px !important; margin:0 0 0 2px !important; padding:4px 6px !important;
                border-radius:999px !important; white-space:nowrap !important;
                background-color:rgba(255,255,255,.045) !important;
                border:1px solid rgba(255,255,255,.10) !important;
                box-shadow:inset 0 1px 0 rgba(255,255,255,.06) !important;
            }
            #spp-btabs .spp-catrow ul.threadlist {
                display:flex !important; flex-wrap:wrap !important; align-items:center !important;
                gap:6px !important; margin:0 !important; padding:0 !important;
                position:static !important; bottom:auto !important; height:auto !important;
                overflow:visible !important; float:none !important;
                background:none !important; border:0 !important;
            }
            #spp-btabs .spp-catrow ul.threadlist > span { display:contents !important; }
            #spp-btabs .spp-catrow ul.threadlist li {
                display:block !important; float:none !important; list-style:none !important;
                margin:0 !important; padding:0 !important;
            }
            #spp-btabs .spp-catrow li > a {
                display:inline-flex !important; align-items:center !important; height:26px !important;
                padding:0 12px !important; border-radius:999px !important; line-height:1 !important;
                border:1px solid rgba(255,255,255,.12) !important;
                background-color:rgba(255,255,255,.05) !important;
                color:#c8d6ea !important; -webkit-text-fill-color:#c8d6ea !important;
                font-size:12.5px !important;
                text-decoration:none !important; white-space:nowrap !important;
            }
            #spp-btabs .spp-catrow li > a:hover {
                background-color:rgba(255,255,255,.11) !important;
                color:#fff !important; -webkit-text-fill-color:#fff !important;
            }
            #spp-btabs .spp-catrow li.current > a {
                color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
                background-color:rgba(32,201,151,.14) !important;
                border-color:rgba(32,201,151,.45) !important; font-weight:600 !important;
            }
            ${CSS_BOARDTOOL}
/* ===== SPP-ANNOUNCE-END ===== */
`);

GM_addStyle(CSS_HOTBOARD);



        // ==================== 2.50.0:数据行「回复 / 热度」按钮 ====================
        // ★★ 与 2.49.8 同理,必须是【独立的一次 GM_addStyle】:
        //    @keyframes 只能待在顶层,且预览生成器按标记整段抽取,混进大盘子里会抽不干净。
        GM_addStyle(`
/* ===== SPP-METABTN-BEGIN ===== */
            /* 来源 https://uiverse.io/Shoh2008/hungry-treefrog-88
               原版是一枚独立大按钮(padding:15px 30px / font-size:18px)。
               这里把尺度缩到数据行用(4px 12px / 12px),其余原样保留:
               10px 圆角、#1e1e1e 底、外发光 0 0 10px + inset 0 0 10px、
               0.5s 过渡、hover 跑挤压回弹关键帧,关键帧改名避免与版块页重名。

               ★ 2.50.2:动效改【舒缓】。原版是 scale 0.7↔1.3 的剧烈挤压(0.5s linear),
                 在数据行这种小尺寸上显得躁、且会盖到相邻元素。
                 改成 ±6% / ±5% 的轻微呼吸,1.1s + ease-in-out,落点回到 1,1。
                 幅度与时长都只调这两个值,机制不变。

               ★ 字色【不在这里定】。按脚本既有的颜色标注规则走:
                 JS 把 getReplyColor(回复数) 的返回值写进 --spp-mbtn-fg,本段只取用。
                 这样按钮与「回复 / 热度」数字永远是同一套配色,不会各说各话。

               ★★ 为什么要动 liftBtn 与 TEA_SKIN 那两条通用按钮规则:
                 那两条都写成 button:not(...)…{ … !important },通配全站按钮。
                 liftBtn 的选择器是 (3,3,4),本段是 (1,4,3) —— a 位比它小,
                 同为 !important 时【压不住】,实机按钮会被改成 accent 浅底 +
                 translateY(-1px),挤压回弹与绿光全部丢失。
                 做法与文件既有惯例一致(.spp-btn 当年就是这么排除的):
                 把 .spp-metabtn 加进它们的 :not() 链,让它们【根本选不中】本按钮,
                 而不是去比特异性。 */
            html.spp-board-skin #wall ul.stream > li > .inner .spp-meta-btns {
                display:inline-flex !important;
                gap:6px !important;
                align-items:center !important;
            }
            html.spp-board-skin #wall ul.stream > li > .inner .spp-metabtn {
                padding:4px 12px !important;
                font-size:12px !important;
                line-height:1.5 !important;
                font-family:inherit !important;
                font-weight:400 !important;
                outline:none !important;
                border:none !important;
                border-radius:10px !important;
                background:#1e1e1e !important;
                cursor:pointer !important;
                color:var(--spp-mbtn-fg,#9AA5BB) !important;
                box-shadow:0 0 10px #363636, inset 0 0 10px #363636 !important;
                /* ★ 2.50.9:同「公告板块按钮」那条的理由 —— 常驻 transition 会让
                   首次插入的元素把这些属性从默认值慢慢过渡过来,后台标签页不推进、
                   于是停在起始值。过渡改挂到 :hover 上,首帧立即落值。 */
            }
            html.spp-board-skin #wall ul.stream > li > .inner .spp-metabtn:hover {
                transition:0.5s !important;
                animation:spp-metabtn-pop 1.1s 1 ease-in-out !important;
            }
            @keyframes spp-metabtn-pop {
                0%   { transform:scale(1, 1) }
                22%  { transform:scale(1.06, 0.95) }
                45%  { transform:scale(0.98, 1.03) }
                68%  { transform:scale(1.02, 0.99) }
                100% { transform:scale(1, 1) }
            }
            /* ★★ 2.50.8:上面所有规则都把按钮限定在 #wall 的卡片里
               (html.spp-board-skin #wall ul.stream > li > .inner .spp-metabtn)。
               公告板块挂在 #spp-announce、不在 #wall 内,实测一条都匹配不到
               (btn.matches('#wall … .spp-metabtn') === false),
               于是按钮退化成浏览器默认样式:灰底 rgb(240,240,240)、黑字、
               Arial 13.33px、圆角 0 —— 在深玻璃卡上非常刺眼。
               这里补一组等价规则,尺度与配色来源与卡片按钮完全一致,不另起一套。 */
            #spp-announce .spp-meta-btns {
                display:inline-flex !important;
                gap:6px !important;
                align-items:center !important;
            }
            #spp-announce .spp-metabtn {
                padding:4px 12px !important;
                font-size:12.5px !important;
                line-height:1.5 !important;
                font-family:inherit !important;
                font-weight:500 !important;
                outline:none !important;
                border:none !important;
                border-radius:10px !important;
                background:#1e1e1e !important;
                cursor:pointer !important;
                color:var(--spp-mbtn-fg,#9AA5BB) !important;
                -webkit-text-fill-color:var(--spp-mbtn-fg,#9AA5BB) !important;
                box-shadow:0 0 10px #363636, inset 0 0 10px #363636 !important;
                /* ★★ 2.50.9:常驻规则里【不得】写 transition。
                   2.50.8 在这里写了 transition:0.5s,于是按钮首次插入 DOM 时,
                   background-color / padding ×4 / font-weight / color / box-shadow
                   这些属性会从浏览器默认值【过渡】到我的值 ——
                   实测同时挂起 8 个 CSSTransition。后台标签页不推进过渡,
                   计算值就永远停在起始值:按钮显示为默认灰底 rgb(240,240,240)、
                   黑字、Arial、圆角 0。实测把 transition 关掉后颜色立刻正确。
                   ⇒ 过渡只在交互态挂,首帧立即落值。 */
            }
            #spp-announce .spp-metabtn:hover {
                transition:0.5s !important;
                animation:spp-metabtn-pop 1.1s 1 ease-in-out !important;
            }
/* ===== SPP-METABTN-END ===== */
        `);



        function norm(str) { return str.replace(/ /g, ' ').trim(); }

        function reformatAuthorDate(li) {
            const intro = li.querySelector('.section-intro');
            if (!intro || intro.dataset.sppTlDone) return;
            intro.dataset.sppTlDone = '1';
            intro.style.backgroundColor = 'transparent';
            const tds = intro.querySelectorAll('td');
            let author = '', date = '';
            tds.forEach(td => {
                const text = norm(td.textContent);
                if (/作者/.test(text)) {
                    const a = td.querySelector('a');
                    author = a ? a.textContent.trim() : text.replace(/.*作者[::]\s*/, '');
                } else if (/\d{4}-\d{2}-\d{2}/.test(text)) {
                    const m = text.match(/(\d{4}-\d{2}-\d{2})/);
                    date = m ? m[1] : '';
                }
            });
            if (author || date) {
                const newContent = document.createElement('span');
                newContent.className = 'spp-tl-author-line';
                newContent.textContent = author + (date ? ' - 发布于 ' + date : '');
                newContent.style.cssText = 'color:#999;font-size:12px;';
                // ★ 2.45.0:原表格里的作者链接先寄存在 dataset 上(不渲染)。
                //   本函数会把 intro 整个清空,链接随之消失;若不先存下来,
                //   buildCardByline 只能拿到纯文本,实测 42 项里丢掉 29 个作者链接。
                const aLink = intro.querySelector('td a');
                if (aLink && aLink.getAttribute('href')) {
                    newContent.dataset.sppAuthorHref = aLink.getAttribute('href');
                }
                intro.innerHTML = '';
                intro.appendChild(newContent);
            }
        }

        // ==================== 2.46.0:标题内联颜色的归一 ====================
        // ★ 症状:部分帖子标题是纯蓝 (#0000FF) 或纯品红 (#FF00FF),在深色卡上刺眼。
        //   用户报告「资源链接中的蓝色字体默认改为白色;蓝色字体对眼睛不太友好」。
        //
        // ★ 根因(与任何样式表都无关 —— 这一点花了最久才排除):
        //   颜色来自发帖人【自己写在标题里的标记】:
        //     .section-title a > font[color="#0000FF"]
        //   实测 fid-4 首屏 44 张卡里 34 张带这种 font,全是整段包裹
        //   (coverPct 100%:34 张的 insideFont 与 len 完全相等,0 张是部分包裹)。
        //   为什么之前的探针全都没发现:
        //     1) 遍历样式表找「谁把标题设成蓝色」→ 一条都没有。全站 622 条样式规则里,
        //        命中标题锚点且带 color 的只有 4 条:sheet0 的 `a`(黑)、我们自己的
        //        `a:not(...)`(青)、站点 thread_new.css 的 `.stream li a`(青) 与
        //        `.stream li .section-title a`(#555)。0 条是蓝的。
        //     2) getComputedStyle(a).color 读到的是 rgb(154,165,187) —— 灰的。
        //        因为行内 !important 写在 <a> 上,子元素 <font color> 是【另一个元素】,
        //        它走自己的 UA 呈现规则,父级的 color 对它只是继承基线、被它自己覆盖。
        //     ⇒ 结论:要判定「这段文字实际是什么颜色」,只读 <a> 的计算色是不够的,
        //       必须连【后代元素自己的颜色】一起看。这是本次真正确立的新检查口径。
        //
        // ★ 早前从像素里量到的 rgb(0,0,250) 也是同一件事的旁证:
        //   UA 对 <font color="#0000FF"> 走的是 legacy 呈现色 #0000EE 一族,
        //   再经暗色卡上的抗锯齿/次像素合成偏移,落在 0,0,250 附近。
        //
        // ★ 为什么可以【一律改白】而不担心丢掉作者的强调意图:
        //   实测 34/34 都是整段包裹(insideFont === len),没有一张是「只给标题里的
        //   某几个字上色」。也就是说这个 color 是【整条标题的装饰色】,不是局部强调,
        //   抹平成白色不会丢任何信息 —— 这正是「按亮度归一」那版设计被放弃的原因:
        //   它想保留的「强调」在这份数据里根本不存在,只会让蓝字变成另一种灰蓝。
        const TITLE_WHITE = '#FFFFFF';

        // ★★ 2.55.6:标题墨水色的【唯一口径】。
        //   为什么要收成一处:标题色有两个写入点 —— 归一把作者那层 <font>/行内 color 刷成
        //   一个色,colorTitleByHeat 把档位色写在同一张卡的 <a> 上。两个点各算各的,
        //   一旦算出来的不是同一个色,子元素那层就会压过父级(2.46.0–2.55.5 在茶馆里
        //   就是「归一刷白、父级刷档位色」⇒ 带色标题恒白)。现在两个点都问这里,
        //   结构上不可能再不一致 —— 这就是 2.55.1 在非茶馆那侧用的「两层同色」,
        //   本轮把它推广到茶馆。
        //   ★ 口径没动:判据仍是【回复数】而不是热度(茶馆档位 20/50/80),这是既有设计。
        function readReplyCount(li) {
            const replySpan = li.querySelector('.spp-tl-reply');
            if (replySpan) {
                const n = parseInt(replySpan.textContent, 10);
                if (!isNaN(n)) return n;
            }
            // 兜底:站点原始文本(未被 reformatReplyViews 加工过时才有机会命中)
            const sectionText = li.querySelector('.section-text');
            if (!sectionText) return NaN;
            const m = sectionText.textContent.match(/回复(?:\/人气)?[::]\s*(\d+)/);
            return m ? parseInt(m[1], 10) : NaN;
        }

        // 取不到回复数 → 返回空串:调用方各自保持原行为(不再写色 / 退回白色),不凭空造色。
        // ★★ 2.55.7:茶馆的【热度档位】—— 用户:「直接按热度来划分颜色」。
        //   旧判据是回复数(20/50/80),而卡片上并排显示「回复 x 热度 y」:
        //   回复 20→21 差 1 就换色、热度 2894 与 1510 隔着两档却同色 ⇒ 看着像随机。
        //   实测茶馆 1–5 页 242 张卡的热度分位:
        //     min 9 · p10 528 · p25 816 · p50 2494 · p75 6720 · p90 23590 · p95 59792 · max 374324
        //   阈值取几何级差(×5 / ×4)的圆整数 1000 / 5000 / 20000,落在这批数据上:
        //     白 66 张(27%) · 绿 100 张(41%) · 橙 51 张(21%) · 红 25 张(10%)
        //   ★ 2.55.8:第一档色值由 #9AA5BB 提亮为 #FFFFFF(用户:「当标题为白色时,
        //     如果觉得太暗,能否将其调整为更亮的白色?」)—— 档位边界与其余三档一字未动,
        //     getReplyColor() 也一字未动(其他版块的数字配色、降级路径保持原样)。
        //   —— 绿是主色(中间热度最多),红只留头部 10%(一屏里最烫的那几张才发红)。
        //   备选 2000/6000/20000 会把 44% 压成冷灰(太灰)、1000/3000/10000 让红占 18%
        //   (太吵),都没选。★ 2.55.20 起这三个数与四个色值都搬进了 HEAT_STYLE_DEFAULTS,
        //   面板「茶馆热度配色」组可以随时改(这里不再有可改的常量,改口径去那张表)。

        // ★★ 2.55.20(用户:「不同的热度对应不同的颜色,能否在设置面板中由用户自定义颜色、
        //   热度?」):三档边界与四档色值改为**从设置面板读** —— 形状与两条收敛规则见
        //   SPP_CONFIG.heatStyle()。
        //   ★ 本函数的**分档逻辑一字未动**(仍是 ≤ 边界取该档、否则落到下一档),只是把
        //     2.55.7 定的三个常量与 2.55.8 定的冷档纯白换成一份读数 —— 那两轮的口径仍在,
        //     只是搬进了默认值表。
        function getTeaHeatColor(heat) {
            const s = SPP_CONFIG.heatStyle();
            if (heat <= s.tCold) return s.cCold;   // 默认白(2.55.8 前是 #9AA5BB)
            if (heat <= s.tWarm) return s.cWarm;   // 默认绿
            if (heat <= s.tHot)  return s.cHot;    // 默认橙
            return s.cPeak;                        // 默认红
        }

        // 热度读数:优先已加工的按钮,兜底站点原始文本(两种写法都收,见下)。
        //   ★ 兜底里那条「热度 数字」是给【加工之后】的文本用的:reformatReplyViews
        //     把 span 换成两枚按钮后,.section-text 的文本会变成「回复 45热度 3822」,
        //     此时原始写法「回复/人气:45/3822」已经不存在了。
        function readHeatCount(li) {
            const heatSpan = li.querySelector('.spp-tl-heat');
            if (heatSpan) {
                const n = parseInt(heatSpan.textContent, 10);
                if (!isNaN(n)) return n;
            }
            const sectionText = li.querySelector('.section-text');
            if (!sectionText) return NaN;
            const txt = sectionText.textContent;
            const m = txt.match(/回复\/人气[::]\s*\d+\s*\/\s*(\d+)/) || txt.match(/热度\s*(\d+)/);
            return m ? parseInt(m[1], 10) : NaN;
        }

        function titleInkColor(li) {
            // ★★ 2.55.7:茶馆改成按【热度】分档(用户:「直接按热度来划分颜色」)。
            //   其他版块逐位不动 —— 2.55.1 定案的「非茶馆标题一律纯白」原样保留。
            if (!isTeaFid()) return TITLE_WHITE;
            const h = readHeatCount(li);
            if (!isNaN(h)) return getTeaHeatColor(h);
            // 热度读不到 → 退回旧的回复档位(保守降级,不凭空造色)
            const n = readReplyCount(li);
            return isNaN(n) ? '' : getReplyColor(n);
        }

        function normalizeTitleInlineColor(li) {
            const titleLink = li.querySelector('.section-title a');
            if (!titleLink) return 0;
            // 幂等:MutationObserver 会反复调用
            if (titleLink.dataset.sppTitleNorm) return 0;
            titleLink.dataset.sppTitleNorm = '1';

            // 把所有自带颜色的后代收进来。两种来源都要扫:
            //   · <font color="...">  —— 呈现属性,getAttribute 才读得到
            //   · 任意元素的行内 style="color:..."(含 <b>/<span>/<i>)
            const colored = [];
            titleLink.querySelectorAll('font[color]').forEach(el => {
                if (colored.indexOf(el) < 0) colored.push(el);
            });
            titleLink.querySelectorAll('[style*="color"]').forEach(el => {
                if (colored.indexOf(el) < 0) colored.push(el);
            });
            if (!colored.length) return 0;

            // ★★ 2.55.6:目标色从「一律纯白」改成「标题墨水色」—— 茶馆是回复档位色,
            //   其他版块仍是 TITLE_WHITE(2.55.1 口径一字未改)。
            //   旧写法在这里必刷 TITLE_WHITE,而 <font> 是 <a> 的【子元素】、子元素自己的
            //   颜色压得过父级 ⇒ 茶馆里作者带过色的标题恒为纯白,colorTitleByHeat 写在 <a>
            //   上的档位色根本透不出来(非茶馆那侧 2.55.1 已修,茶馆当时按「不针对茶馆」留着)。
            //   现在两层同色,由 titleInkColor() 一处口径决定。
            const ink = titleInkColor(li) || TITLE_WHITE;
            colored.forEach(el => {
                // font[color] 是【呈现属性】:任何 CSS 都能压过它,所以必须改属性本身,
                // 只写行内 style 在部分浏览器里会被呈现属性的优先级关系搞混。
                if (el.hasAttribute('color')) el.setAttribute('color', ink);
                el.style.setProperty('color', ink, 'important');
            });
            return colored.length;
        }

        function reformatReplyViews(li) {
            const sectionText = li.querySelector('.section-text');
            if (!sectionText) return;
            sectionText.querySelectorAll('span').forEach(span => {
                if (span.dataset.sppTlReplyDone) return;
                const text = norm(span.textContent);
                const match = text.match(/回复\/人气[::]\s*(\d+)\s*\/\s*(\d+)/);
                if (match) {
                    span.dataset.sppTlReplyDone = '1';
                    const replyCount = parseInt(match[1], 10);
                    const heatCount = parseInt(match[2], 10);
                    // ★★ 2.55.7:茶馆的数字跟着标题走(按热度分档 ⇒ 一张卡一个色);
                    //   其他版块仍是 getReplyColor(回复数) —— 2.55.1 定的「热度色留在数字上」照旧。
                    let color = getReplyColor(replyCount);
                    if (isTeaFid()) color = titleInkColor(li) || color;
                    // ★ 2.50.0:两个数字各自包进一枚按钮。
                    //   字色仍走 getReplyColor(),由 --spp-mbtn-fg 传给 CSS —— 按钮文字与数字同色。
                    //   外层的 span 仍是 <span>:buildCardMeta() 只摘 .section-text 的 SPAN 子项,
                    //   换成 div 就摘不走,数据行会留在图片区里(本项目踩过同类坑)。
                    span.classList.add('spp-meta-btns');
                    span.innerHTML =
                        '<button type="button" class="spp-metabtn" style="--spp-mbtn-fg:' + color + '">回复 ' +
                        '<span class="spp-tl-reply" style="color:' + color + ';-webkit-text-fill-color:' + color + ';font-weight:bold">' + replyCount + '</span></button>' +
                        '<button type="button" class="spp-metabtn" style="--spp-mbtn-fg:' + color + '">热度 ' +
                        '<span class="spp-tl-heat" style="color:' + color + ';-webkit-text-fill-color:' + color + ';font-weight:bold">' + heatCount + '</span></button>';
                }
            });
        }

        function colorTitleByHeat(li) {
            const titleLink = li.querySelector('.section-title a');
            if (!titleLink || titleLink.dataset.sppHeatColor) return;
            // ★★ 2.55.6:读数与取色都收进 titleInkColor()(唯一口径),此处不再各写一遍。
            //   语义与旧写法逐条对齐:读不到回复数 → 返回空串 → 这里照旧直接 return。
            const color = titleInkColor(li);
            if (!color) return;
            // ★★ 2.55.1(用户:「我发现一和二的字体颜色为什么不一样?能不能让二的统一字体
            //   效果为一的效果?」+定案「颜色统一为一号」+「不针对茶馆」):
            //   ①② 的差别不是站点给的,是本脚本自己造出来的 ——
            //   normalizeTitleInlineColor 把标题里那层 <font color> 刷白(实测 44 张卡里 31 张带
            //   <font>),而 <font> 是 <a> 的【子元素】、子元素自己的颜色压得过父级 ⇒
            //   那 31 张是白的,另外 13 张(没有 <font>)显示 <a> 上的热度色(最低档 #9AA5BB)。
            //   现在非茶馆分支不再写热度色,改写与 <font> 层**同色**的 TITLE_WHITE ⇒ 两类卡一致。
            //   代价(已与用户对齐):资源区标题的热度梯度消失,热度色仍然留在
            //   「回复 / 热度」两个数字上(reformatReplyViews 那一路一字未改)。
            //   茶馆(fid 9)保持原样 —— 用户口径是「不针对茶馆」。
            // (2.55.6:色值已在上面由 titleInkColor() 给出 —— 茶馆=回复档位色,其他版块=纯白)
            titleLink.style.setProperty('color', color, 'important');
            titleLink.dataset.sppHeatColor = '1';
        }

        let fullTitleMapPromise = null;

        function getTopicId(href) {
            const match = String(href || '').match(/[?&]tid(?:-|=)(\d+)/);
            return match ? match[1] : '';
        }

        function isTruncatedTitle(title) {
            return /(?:\.{2,}|…)$/.test(norm(title));
        }

        function loadFullTitleMap() {
            if (fullTitleMapPromise) return fullTitleMapPromise;

            fullTitleMapPromise = (async function () {
                try {
                    const listUrl = new URL(location.href);
                    listUrl.pathname = listUrl.pathname.replace(/thread_new\.php$/, 'thread.php');

                    const response = await fetch(listUrl.href, { credentials: 'same-origin' });
                    if (!response.ok) throw new Error('HTTP ' + response.status);

                    const html = await response.text();
                    const doc = new DOMParser().parseFromString(html, 'text/html');
                    const titleMap = new Map();

                    doc.querySelectorAll('a[href*="read.php?tid"]').forEach(link => {
                        const tid = getTopicId(link.getAttribute('href'));
                        const title = norm(link.textContent);
                        if (!tid || !title) return;

                        const previous = titleMap.get(tid);
                        if (!previous || title.length > previous.length) {
                            titleMap.set(tid, title);
                        }
                    });

                    return titleMap;
                } catch (error) {
                    console.warn('[SPP] 列表模式完整标题读取失败', error);
                    return new Map();
                }
            })();

            return fullTitleMapPromise;
        }

        function restoreFullTitle(li) {
            const titleLink = li.querySelector('.section-title a');
            if (!titleLink || titleLink.dataset.sppFullTitle) return;

            const currentTitle = norm(titleLink.textContent);
            if (!isTruncatedTitle(currentTitle)) return;

            const tid = getTopicId(titleLink.getAttribute('href'));
            if (!tid) return;

            titleLink.dataset.sppFullTitle = 'loading';
            loadFullTitleMap().then(titleMap => {
                const fullTitle = titleMap.get(tid);
                if (fullTitle && fullTitle.length > currentTitle.length) {
                    titleLink.textContent = fullTitle;
                    titleLink.title = fullTitle;
                    titleLink.dataset.sppFullTitle = 'done';
                } else {
                    titleLink.dataset.sppFullTitle = 'missing';
                }
            });
        }

        const DELETED_IMAGE_MARKERS = [
            // 这些图床删除图片后会返回固定尺寸的占位图,而不是触发 img.error。
            { hostname: 'img.chkaja.com', width: 250, height: 250 },
            { hostname: 'i.ibb.co', width: 250, height: 250 }
        ];

        function isKnownDeletedImage(img) {
            const src = img.currentSrc || getImgSrc(img);
            if (!src) return false;

            try {
                const hostname = new URL(src, location.href).hostname.toLowerCase();
                return DELETED_IMAGE_MARKERS.some(marker =>
                    hostname === marker.hostname
                    && img.naturalWidth === marker.width
                    && img.naturalHeight === marker.height
                );
            } catch (_) {
                return false;
            }
        }

        function getCardImageState(img) {
            const src = getImgSrc(img);
            if (!src || src === 'about:blank' || src.startsWith('data:')) return 'invalid';
            if (src.includes('noimageavailble')) return 'invalid';
            if (img.dataset.sppImageLoadFailed === '1') return 'invalid';

            const deferredSrc = img.getAttribute('data-src')
                || img.getAttribute('data-original')
                || img.getAttribute('data-lazy-src');
            const activeSrc = img.currentSrc || img.src || '';
            if (!img.complete || (deferredSrc && (!activeSrc || activeSrc === 'about:blank' || activeSrc.startsWith('data:')))) {
                return 'pending';
            }

            if (img.naturalWidth === 0 || img.naturalHeight === 0) return 'invalid';
            if (isKnownDeletedImage(img)) return 'invalid';
            if (img.naturalWidth > 50 && img.naturalHeight > 50) return 'valid';
            return 'invalid';
        }

        function convertCardToList(li) {
            if (li.classList.contains('spp-tl-list')) return;
            li.classList.add('spp-tl-list');
            reformatAuthorDate(li);
        }

        function evaluateCardImages(li) {
            const imgs = Array.from(li.querySelectorAll('img'));
            let hasValidImage = false;
            let hasPendingImage = false;

            imgs.forEach(img => {
                if (!img.dataset.sppImageWatch) {
                    img.dataset.sppImageWatch = '1';
                    img.addEventListener('load', function () {
                        delete img.dataset.sppImageLoadFailed;
                        evaluateCardImages(li);
                        // ★ 2.49.0:图加载完才读得到 naturalWidth/naturalHeight,
                        //   也就是「这张封面是横是竖」只有在这一刻才知道。
                        //   所以方向打标必须挂在 load 回调上,不能只在 processItems 里跑一次。
                        markCardOrientation(li);
                    });
                    img.addEventListener('error', function () {
                        img.dataset.sppImageLoadFailed = '1';
                        evaluateCardImages(li);
                    });
                }

                const state = getCardImageState(img);
                if (state === 'valid') hasValidImage = true;
                if (state === 'pending') hasPendingImage = true;
            });

            if (!hasValidImage && !hasPendingImage) {
                convertCardToList(li);
            }
        }

        // ==================== 2.49.0:判定封面是「横」还是「竖」====================
        // 用户确认稿的规则:封面 ar ≥ 1 走横卡(占 4 栏、图片区 3:2),
        // ar < 1 走竖卡(占 3 栏、图片区 3:4)。CSS 只能按类名分流,
        // 所以这里把判定结果写成 li 上的 .spp-card-portrait。
        //
        // ★ 为什么用 naturalWidth/naturalHeight 而不是量 DOM 尺寸:
        //   卡片是定宽网格,li 的宽度恒等于列宽 —— 量它得到的永远是卡片自己的比例,
        //   与封面本身无关。只有 natural* 才是图片文件的真实像素尺寸。
        //
        // ★ 为什么必须处理「已经是竖卡又变回横卡」:
        //   站点的懒加载会先给一个占位图、再换成真图(evaluateCardImages 的
        //   'pending' 分支就是为它写的)。中间态可能是任意比例,若只加不删,
        //   卡片会被永久留在错误的组里。所以这里两个方向都写。
        //
        // ★ 阈值取 1(正方形算横卡):
        //   实测 55 张真封面 ar 从 0.445 到 3.44,1.0 附近是很稀疏的一段
        //   (0.979 与 1.065 之间只有 2 张),阈值的精确位置不敏感。
        //
        // ★ 为什么用「有图卡」做前提:无图卡(.spp-tl-list)的 img 是
        //   站点自带的 noimageavailble 占位图(190×266,ar 0.714),
        //   不排除的话它会被判成竖卡,而它根本没有图片区。
        function markCardOrientation(li) {
            // ★★ 2.49.1 致命修复:写 class 必须先判 contains,否则死循环 ★★
            //   2.49.0 这里是无条件 add/remove。它单独看没错,但因为
            //   ① 本页 Observer 带了 attributes:true + attributeFilter:['class'],
            //   ② Chrome 的 classList.add/remove 在「类名没有实际变化」时
            //      【仍然重写 class 属性并产生一条 attributes 记录】,
            //   两者相乘就成了一条微任务自激链:
            //        markCardOrientation 写 class
            //          → Observer 回调 → processItems → markCardOrientation 写 class
            //            → …(永不回到宏任务,setTimeout 一次都轮不到)
            //   实测:46 张卡 × 每次 33~46 条写,Observer 回调 1000 次/秒,
            //   渲染进程钉在 100% CPU,页面彻底停摆 —— 用户看到的「美化失效」即是此。
            //   ★ 实测数据(Chrome 154):
            //     remove('不存在的类') → 1 条 mutation;add('已存在的类') → 1 条;
            //     先 contains 再 remove   → 0 条。(守卫测试即按这条断言。)
            if (li.classList.contains('spp-tl-list')) {
                if (li.classList.contains('spp-card-portrait')) li.classList.remove('spp-card-portrait');
                return false;
            }
            const img = li.querySelector('.section-text img') || li.querySelector('img');
            if (!img || !img.naturalWidth || !img.naturalHeight) return false;

            const isPortrait = (img.naturalWidth / img.naturalHeight) < 1;
            const hasPortrait = li.classList.contains('spp-card-portrait');
            if (isPortrait && !hasPortrait) li.classList.add('spp-card-portrait');
            else if (!isPortrait && hasPortrait) li.classList.remove('spp-card-portrait');
            return isPortrait;
        }

        // ==================== 2.43.0:数据行独立成 .spp-card-meta ====================
        // 站点把「收藏」与「回复/热度」塞在 .section-text 里,与缩略图 div 混在一处。
        // 新布局需要缩略图独占 grid 的 thumb 区、数据行独占 meta 区,所以这里把
        // .section-text 的【直接子 span】摘出来,移到独立的 .spp-card-meta 容器里;
        // 缩略图 div 留在原处,靠 `.section-text{display:contents}` 变成 grid 子项。
        //
        // ★ 必须幂等:MutationObserver 会对懒加载出来的新项反复调用 processItems,
        //   这里用 dataset 打标,重复调用直接返回(否则会搬走重复节点)。
        function buildCardMeta(li) {
            if (li.dataset.sppMetaDone) return;
            const sectionText = li.querySelector('.section-text');
            if (!sectionText) return;
            const spans = Array.from(sectionText.children).filter(c => c.tagName === 'SPAN');
            if (!spans.length) return;
            li.dataset.sppMetaDone = '1';
            const meta = document.createElement('div');
            meta.className = 'spp-card-meta';
            spans.forEach(s => meta.appendChild(s));
            sectionText.parentNode.insertBefore(meta, sectionText.nextSibling);
            // 列表行没有缩略图,摘走数据行后 .section-text 变空 → 交给 CSS 收起
            if (li.classList.contains('spp-tl-list')) li.classList.add('spp-has-meta');
        }

        // ==================== 2.45.0:深色卡上的文字对比度兜底 ====================
        // ★ 为什么必须用 JS 而不是 CSS:
        //   colorTitleByHeat 用 style.setProperty('color', c, 'important') 写标题色,
        //   那是【行内 !important】。按 CSS 层叠规则,行内 !important 位于
        //   「Author !important」带的最前,任何外部选择器(哪怕同样 !important)
        //   都压不过它。实测:加了 !important 的选择器仍然读回 rgb(26,26,26)。
        //   所以这里直接改行内值本身。
        //
        // 触发的实际症状:卡片底色是 #131A2A 深蓝黑,而最低档配色是 #1A1A1A,
        // 对比度 1.00 —— 15 个标题、38 个回复/热度数字在页面上完全看不见。
        function relativeLuminance(rgb) {
            const m = String(rgb).match(/(\d+(?:\.\d+)?)[,\s]+(\d+(?:\.\d+)?)[,\s]+(\d+(?:\.\d+)?)/);
            if (!m) return null;
            const [r, g, b] = [1, 2, 3].map(i => {
                let v = parseFloat(m[i]) / 255;
                return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
            });
            return 0.2126 * r + 0.7152 * g + 0.0722 * b;
        }

        const CARD_BG_LUM = relativeLuminance('rgb(19,26,42)');   // #131A2A
        const READABLE_MIN = '#9AA5BB';

        function ensureReadable(el, minRatio) {
            if (!el) return false;
            const cs = getComputedStyle(el);
            const fg = relativeLuminance(cs.color);
            if (fg === null) return false;
            const ratio = (Math.max(fg, CARD_BG_LUM) + 0.05) / (Math.min(fg, CARD_BG_LUM) + 0.05);
            if (ratio >= (minRatio || 3)) return false;

            // ★ 必须先掐掉正在跑的 color 过渡,否则设了颜色也读不回来。
            //   本文件的 hover 规则给卡片内的 a / span 定了 `color .18s ease`。
            //   若在该过渡进行中改色(尤其是页面处于后台标签、rAF 不推进时),
            //   transition.currentTime 会停在 0,计算值恒为【起始色】——
            //   实测 42 个标题反复设为 #9AA5BB 后仍读回 rgb(85,85,85),
            //   且 getAnimations() 里那条 CSSTransition 的 keyframes 冻结在
            //   from:#555 → to:#090909。cancel() 之后立刻就能读到新值(实测通过)。
            if (el.getAnimations) {
                el.getAnimations().forEach(anim => {
                    if (anim.transitionProperty === 'color') anim.cancel();
                });
            }
            // 用 important 覆盖,保证压得住站点或旧版脚本留下的任何行内值
            el.style.setProperty('color', READABLE_MIN, 'important');
            el.dataset.sppContrast = '1';
            return true;
        }

        // 各目标的可读性下限(对比度)。
        // 标题 15.5px/700 按 WCAG 大字标准取 3:1;数字与胶囊文字更小,取 3:1 已够看,
        // 但作者名是「辅助但需辨认」的信息,给到 4.5:1 才不糊。
        const CONTRAST_MIN = { title: 3, reply: 3, heat: 3, name: 4.5, date: 3, fav: 3 };

        function fixDarkTextContrast(li) {
            let fixed = 0;
            const bump = (el, min) => { if (ensureReadable(el, min)) fixed++; };
            li.querySelectorAll('.section-title a').forEach(el => bump(el, CONTRAST_MIN.title));
            li.querySelectorAll('.spp-tl-reply').forEach(el => bump(el, CONTRAST_MIN.reply));
            li.querySelectorAll('.spp-tl-heat').forEach(el => bump(el, CONTRAST_MIN.heat));
            li.querySelectorAll('.spp-name-txt').forEach(el => bump(el, CONTRAST_MIN.name));
            li.querySelectorAll('.spp-date-txt').forEach(el => bump(el, CONTRAST_MIN.date));
            li.querySelectorAll('.spp-fav-txt').forEach(el => bump(el, CONTRAST_MIN.fav));
            // ★ 2.46.0:标题里的【后代】元素自带颜色时也要查。
            //   上一版只 bump 了 `.section-title a` 自身,而实测 34/44 张卡的蓝色
            //   来自 <a> 内部的 <font color="#0000FF"> —— 那是另一个元素,
            //   getComputedStyle(a).color 永远是灰的、恒达标,于是这里一个都没修到,
            //   守卫却全绿(「检查了」不等于「检查到了出问题的那个元素」)。
            //   normalizeTitleInlineColor 已把彩色统一成白色,这里是第二道网:
            //   万一有帖子用的是本函数没覆盖的写法,也在此兜住。
            li.querySelectorAll('.section-title a font[color], .section-title a [style*="color"]')
                .forEach(el => bump(el, CONTRAST_MIN.title));
            return fixed;
        }

        function processItems(root) {
            const items = (root || document).querySelectorAll('#wall ul.stream > li:not([data-spp-tl-checked])');
            items.forEach(li => {
                li.setAttribute('data-spp-tl-checked', '1');
                reformatReplyViews(li);
                // ★ 2.46.0:必须夹在 colorTitleByHeat 与 restoreFullTitle 之间。
                //   前者的 style.setProperty('color', ...) 写在 <a> 上,不影响 <font> 子元素;
                //   但 restoreFullTitle 会用 titleMap 改写 titleLink.textContent ——
                //   那一步会【把 <font> 连同它的颜色一起丢掉】,彩色标题在那一刻消失。
                //   放在 restoreFullTitle 之前,是为了让「标题被补全」的帖子也走一遍归一;
                //   放在 colorTitleByHeat 之后,是因为热度的行内色写在同一个 <a> 上,
                //   先归一再写热度色的话,<a> 的白色会被热度色覆盖,两者是不同元素、互不冲突,
                //   但顺序固定在中间能让「谁最后写 <a> 的色」保持唯一(热度色)。
                normalizeTitleInlineColor(li);
                colorTitleByHeat(li);
                restoreFullTitle(li);
                // ★ 必须排在 evaluateCardImages 之前:后者一旦把卡片降级成 .spp-tl-list,
                //   会连带调用 reformatAuthorDate 把 .section-intro 的作者表整个清空,
                //   那时再想读作者链接就只剩纯文本了(实测 42 项里丢 29 个链接)。
                //   作者/日期胶囊在两类卡上都显示,所以此时不必先知道分类结果。
                buildCardByline(li);
                evaluateCardImages(li);
                buildCardMeta(li);
                decorateFavButton(li);
                // ★ 2.55.1:资源区「已读」标识。必须排在 decorateFavButton 之后 ——
                //   胶囊插在「收藏」锚点后面(与收藏同排),锚点得先被建出来。
                applyReadChip(li);
            });
            // 联动流光的相位:按卡片序号写入 --spp-idx,靠后的卡负延迟更大
            const all = document.querySelectorAll('#wall ul.stream > li');
            all.forEach((li, i) => li.style.setProperty('--spp-idx', String(i)));
            // ★ 2.49.0:方向打标必须排在 evaluateCardImages 之后 ——
            //   后者是决定 .spp-tl-list 的地方,而「无图卡不参与横竖分流」
            //   正是靠读它的结论(markCardOrientation 的第一行)。
            //   排在前面的话,首次冷启动时无图卡还没被打上类名,
            //   会被当成有图卡去看它的 noimageavailble 占位图(ar 0.714)→ 误判成竖卡。
            //   ★ 冷启动时图片可能还没加载完,此时 markCardOrientation 会静默返回
            //     false(读不到 natural*),稍后 img 的 load 回调会补跑一次。
            all.forEach(li => markCardOrientation(li));
            // ★ 放在最后:上面的函数会写入行内颜色,对比度兜底必须读到最终值再判定。
            //   本函数只读计算值、只改不合格的那些,幂等无害。
            all.forEach(li => fixDarkTextContrast(li));
            // 再跑一遍:第一遍可能因为 color 过渡尚未落定而读到中间值,
            // 过渡被 cancel 之后第二次判定才是终态。
            all.forEach(li => fixDarkTextContrast(li));
        }

        // ==================== 2.45.0:作者胶囊 + 日期胶囊 ====================
        // 站点原本把作者/日期放在 .section-intro 的 <table> 里(td 带内联 color:white)。
        // 新布局要「作者胶囊(带链接) + 日期胶囊」两枚,所以这里读出原始值后重建。
        // ★ 幂等:MutationObserver 会反复调用,用 dataset 打标。
        function hashHue(str) {
            let h = 7;
            const s = String(str || '');
            for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0;
            return h % 360;
        }

        function buildCardByline(li) {
            if (li.dataset.sppBylineDone) return;
            const intro = li.querySelector('.section-intro');
            if (!intro) return;

            // 作者:优先取已压平的文本行(列表行),否则从原始表格的 <a> 取
            let author = '', authorHref = '', date = '';
            const flat = intro.querySelector('.spp-tl-author-line');
            if (flat) {
                const t = norm(flat.textContent);
                const m = t.match(/^(.*?)\s*-\s*发布于\s*(\d{4}-\d{2}-\d{2})/);
                if (m) { author = m[1].trim(); date = m[2]; }
                else { author = t; }
                // 链接由 reformatAuthorDate 预先寄存在 dataset 上:
                // 那一步会把 intro 清空,此时 DOM 里已经没有 <a> 了。
                authorHref = flat.dataset.sppAuthorHref || '';
            } else {
                Array.from(intro.querySelectorAll('td')).forEach(td => {
                    const text = norm(td.textContent);
                    if (/作者/.test(text)) {
                        const a = td.querySelector('a');
                        author = a ? a.textContent.trim() : text.replace(/.*作者[::]\s*/, '');
                        if (a) authorHref = a.getAttribute('href') || '';
                    } else if (/\d{4}-\d{2}-\d{2}/.test(text)) {
                        const m = text.match(/(\d{4}-\d{2}-\d{2})/);
                        if (m) date = m[1];
                    }
                });
            }
            if (!author && !date) return;
            li.dataset.sppBylineDone = '1';

            const h1 = hashHue(author || date);
            const h2 = (h1 + 58) % 360;

            const byline = document.createElement('span');
            byline.className = 'spp-byline';
            byline.style.setProperty('--spp-h1', String(h1));
            byline.style.setProperty('--spp-h2', String(h2));

            if (author) {
                const name = document.createElement('span');
                name.className = 'spp-name';
                const avatar = document.createElement('span');
                avatar.className = 'spp-avatar';
                avatar.textContent = author[0] || '?';
                const txt = authorHref ? document.createElement('a') : document.createElement('span');
                txt.className = 'spp-name-txt';
                txt.textContent = author;
                if (authorHref) {
                    txt.setAttribute('href', authorHref);
                    txt.setAttribute('target', '_blank');
                }
                name.appendChild(avatar);
                name.appendChild(txt);
                byline.appendChild(name);
            }
            if (date) {
                const d = document.createElement('span');
                d.className = 'spp-date';
                const dt = document.createElement('span');
                dt.className = 'spp-date-txt';
                dt.textContent = date;
                d.appendChild(dt);
                byline.appendChild(d);
            }

            // 清空原始 intro 内容,装入新胶囊组
            intro.innerHTML = '';
            intro.appendChild(byline);
        }

        // ==================== 2.45.0:收藏按钮 ====================
        // 站点原标记是 <a id="favor" onclick="sendmsg(...)">,是链接不是表单控件;
        // 本站 42 项共用一个 id="favor"(站点自身即非法 HTML),
        // 所以这里不碰 id、不打 role,只加类承载外观,onclick 原样保留。
        function decorateFavButton(li) {
            if (li.dataset.sppFavDone) return;
            const meta = li.querySelector('.spp-card-meta');
            if (!meta) return;
            const a = meta.querySelector('a#favor, a[onclick*="action=favor"]');
            if (!a) return;
            li.dataset.sppFavDone = '1';
            a.classList.add('spp-fav');
            a.textContent = '';
            const ico = document.createElement('span');
            ico.className = 'spp-fav-ico';
            ico.setAttribute('aria-hidden', 'true');
            const txt = document.createElement('span');
            txt.className = 'spp-fav-txt';
            txt.textContent = '收藏';
            a.appendChild(ico);
            a.appendChild(txt);
            // 原始内联样式(color:#000)会压过类,清掉后交给 CSS
            a.style.removeProperty('color');
        }

        // ==================== 2.47.0 起:有图卡靠前 → 2.49.0 横/竖/无图 三分组 ====================
        // 用户要求:「对于茶馆同一页,有图的卡片放在一起。」
        // ★★ 已定稿(用户明确确认:「有图卡集中排前,这个是我需求的」)。
        //   代价是「时间序」变成「两段各自有序」——这点已与用户对齐:
        //   当时的组内次序仍是发帖时间序,只有「有图 / 无图」这个分组边界是新增的。
        //   ★ 2.50.4 起组内次序已改为热度优先(见本函数内的三级判据),
        //     上述历史说明【保留不改写】,以免后人把「组内按时间」当成现行规则。
        //   实测茶馆首屏:10 张有图卡集中到最前,其余 32 张无图卡保持原有先后。
        //
        // ★ 为什么必须用 JS 重排,CSS 做不到:
        //   有图卡与无图卡是 ul.stream 的直接子项,顺序就是 DOM 顺序。
        //   CSS 没有「把一个子项移到另一个子项之前」的能力 —— order 只能改
        //   flex/grid 的【视觉】次序,而这里 ul 是 grid 容器、li 是 grid item,
        //   order 确实能改视觉次序,但两类卡的 grid-row 位置并不会因此变得连续,
        //   而且屏幕阅读器与 Tab 焦点顺序仍按 DOM 走,键盘用户会看到
        //   「焦点跳来跳去」。直接调 appendChild 改 DOM 才是唯一一致的做法。
        //
        // ★ 为什么现在【所有版块】都做(2.49.4):
        //   茶馆当初的理由是「有图卡被拆成多段散落」——实测 fid-9 首屏 42 项里
        //   只有 10 项有图,DOM 顺序是 TTCTTTTTTTCC...。
        //   2.49.0 之后横/竖是两种宽度(span 4 / span 3),其它版块同样会交错:
        //   实测 fid-14 首屏 44 项的 rankSeq 是 1110101000…,横竖完全混在一起,
        //   于是同一条网格行里两类卡高低错落。分组不再是茶馆专属的观感优化,
        //   而是「多宽度卡片网格」的必需项。
        //
        // ★ 幂等:MutationObserver 会对懒加载出的新项反复调用 processItems。
        //   若每次都重排,会把用户此刻正在看的卡片抽走。这里的做法是
        //   【排序后与当前顺序逐项比对,完全一致就直接返回】—— 不写 DOM,
        //   因而不会触发下一轮 MutationObserver 回调,自激链在此断开。
        //   (★ 注意:这里【不用】dataset 标记。标记法在「新项插入」时反而要额外
        //     记得清除标记,而逐项比对是自证的:顺序没变就一定不需要写。
        //     早先的注释写成「用 dataset 标记是否排过」,与实现不符,已更正。)
        //
        // ★★ 2.50.4:组内次序由「发帖时间」改为「热度优先」。
        //   用户要求:「横屏、竖屏、无图卡片的排序是根据热度计算,三类卡片分别根据热度
        //   来确定谁在同类中位于第一,如果热度一样就按照回复数来作为第二判断依据。」
        //   ⇒ 三级判据:组别(横 / 竖 / 无图) → 热度【降序】 → 回复数【降序】。
        //   三级全同的两张卡【不再有判据】,交给 Array.prototype.sort 的稳定性兜住
        //   (ES2019 起 sort 是稳定排序),它们维持原有相对顺序 —— 这是特性,不是巧合。
        //
        // ★ 数字的来源是【页面上那两个按钮里已经渲染出来的数】,
        //   不另开一条解析链:reformatReplyViews 已把原始文本拆成
        //   .spp-tl-reply / .spp-tl-heat,而 processItems 一定排在 groupCards 之前
        //   (两处调用点都如此:tryObserve 与 MutationObserver 回调)。
        //   若某张卡的文本格式没匹配上、这两个标记不存在,按 0 计,
        //   它自然排到本组末尾 —— 属于可接受的降级,不会抛错、不会卡住排序。
        function groupCards() {
            // ★★ 2.49.4:本函数对【所有版块页】生效,不再限定茶馆。
            //   用户要求:「对于缩短区域的其他板块,目前竖屏和横屏卡片是混着叠在一起的,
            //   希望调整为先横屏再竖屏。」
            //   实测 thread_new.php?fid-14-page-1.html(44 张卡):
            //     rankSeq = 11101010000110010100101100110011010012010010(横竖完全交错)
            //     sortedByRank = false、rowStart = 0
            //   当年(2.47.0)只做茶馆的理由是「非茶馆有图卡占多数,本来就基本连续」;
            //   2.49.0 引入横/竖分流后这个前提已不成立 —— 横卡 span 4、竖卡 span 3
            //   是【两种宽度】,不分组就会在同一条网格行里高低错落,视觉很乱。
            //   ⇒ 判据:只要卡片有两种以上宽度,就必须按宽度分组,否则网格行永远对不齐。

            const ul = document.querySelector('#wall ul.stream');
            if (!ul) return 0;

            const lis = Array.from(ul.children).filter(c => c.tagName === 'LI');
            if (lis.length < 2) return 0;

            // 判据是 li 上的类名,不是重新去问「有没有图」:
            // evaluateCardImages 已经把「没有可用图」的项标成 .spp-tl-list,
            // 这里读它的结论即可,避免出现第二套「什么算有图」的判定。
            //
            // ★ 2.49.0:从「有图 / 无图」两组改成「横 / 竖 / 无图」三组。
            //   归组顺序 = 用户要求的展示顺序:横卡 → 竖卡 → 无图卡,各占一段。
            //   横竖的判定同样不在这里重新做 —— markCardOrientation 已经把
            //   封面 ar < 1 的项标成 .spp-card-portrait,本函数只读那个结论。
            //   (两处都走「上游打标、下游读标」,避免出现第二套判定标准。)
            const rankOf = li =>
                li.classList.contains('spp-tl-list') ? 2          // 无图:最后
                    : li.classList.contains('spp-card-portrait') ? 1  // 竖图:中间
                        : 0;                                          // 横图:最前
            // 数字读取:先剥掉千分位逗号、空白等非数字字符,再取整。读不到一律算 0。
            const countOf = (li, cls) => {
                const el = li.querySelector('.' + cls);
                if (!el) return 0;
                const n = parseInt(String(el.textContent).replace(/[^\d]/g, ''), 10);
                return isNaN(n) ? 0 : n;
            };
            const heatOf = li => countOf(li, 'spp-tl-heat');
            const replyOf = li => countOf(li, 'spp-tl-reply');
            // ★ 同一组内不存在「按宽度二次分组」的问题:rankOf 已经把
            //   横 / 竖 / 无图 分成三段,三段各自独立按热度排,互不干扰。
            const want = lis.slice().sort((a, b) => {
                const dRank = rankOf(a) - rankOf(b);
                if (dRank) return dRank;                    // 一级:组别(横 → 竖 → 无图)
                const dHeat = heatOf(b) - heatOf(a);
                if (dHeat) return dHeat;                    // 二级:热度【降序】
                return replyOf(b) - replyOf(a);             // 三级:回复数【降序】
            });

            // ★ 行首标记:每组的第一张打上 .spp-row-start,
            //   CSS 用 grid-column:1 / span N 把它强制推到第 1 栏 —— 也就是另起一行。
            //   「竖卡与无图卡各自单独起一行」是用户的明确要求;横卡作为第一组,
            //   它的行首本来就是行首,但一并打标可以让规则统一、不必特判。
            //   ★ 先按目标顺序算好,再与现有标记比对,避免无谓的 DOM 写入。
            const firstOf = {};
            want.forEach(li => {
                const r = rankOf(li);
                if (firstOf[r] === undefined) firstOf[r] = li;
            });
            want.forEach((li, i) => {
                const r = rankOf(li);
                const shouldStart = (firstOf[r] === li);
                const hasStart = li.classList.contains('spp-row-start');
                if (shouldStart && !hasStart) li.classList.add('spp-row-start');
                else if (!shouldStart && hasStart) li.classList.remove('spp-row-start');
            });

            // 先判断"是否已经就位",避免无谓的 DOM 写入触发 MutationObserver 自激
            let same = true;
            for (let i = 0; i < lis.length; i++) {
                if (lis[i] !== want[i]) { same = false; break; }
            }
            if (same) return 0;

            // DocumentFragment 一次性挂载:避免逐个 appendChild 期间反复触发布局
            const frag = document.createDocumentFragment();
            want.forEach(li => frag.appendChild(li));
            ul.appendChild(frag);
            return want.filter(li => !li.classList.contains('spp-tl-list')).length;
        }

        function startTL() {
            processItems();
            // ★ 归组必须排在 processItems 之后:只有跑完 evaluateCardImages,
            //   .spp-tl-list 才被打上,此时才分得清哪张有图。
            //   ★ 此处不必再单独调一次 groupCards():下面的 tryObserve 在拿到
            //     #wall 后会立刻 processItems() + groupCards(),wall 存在与否都由它兜住。
            //     重复调用虽被「已就位即返回」保护,但会让「初次接线」与
            //     「Observer 回调接线」两处守卫难以区分(负向测试 X 正是在这上面暴露了盲区)。
            const obs = new MutationObserver(() => {
                processItems();
                groupCards();
                initCardClick();
            });
            const tryObserve = () => {
                const wall = document.getElementById('wall');
                if (wall) {
                    // ★ 2.49.0:必须监听 attributes,否则冷启动会乱序。
                    //   根因(2.47.0 起就存在、一直没暴露):groupCards 判「横/竖/有图」
                    //   靠的是 li 上的类名,而类名是在图片 load 之后才由
                    //   markCardOrientation / evaluateCardImages 补打的。
                    //   冷启动时图片还没加载完,第一轮排序只能拿到「全是无图卡」的中间态;
                    //   等图片陆续 load 完、类名被改上去时,如果 Observer 只看 childList,
                    //   就【再也不会触发】重排 —— 页面停在一个错误的顺序上。
                    //   实测:刷新一次是乱的,再刷一次才对,正是这个原因。
                    //   ★ attributes 只观察 class 变化,不用 attributeFilter 白名单,
                    //     因为站点自己也会改 <img> 的属性,漏掉任何一类都会让守卫形同虚设。
                    obs.observe(wall, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
                    processItems();
                    groupCards();
                    initCardClick();
                }
                else requestAnimationFrame(tryObserve);
            };
            tryObserve();
        }

        // ==================== 2.48.0:整卡点击跳转 ====================
        // 用户要求:「点击卡片即可实现链接跳转」。
        //
        // 站点原本只有【标题】是 <a>,其余区域点了没反应。实测标题锚点的
        // 面积只占整卡约 10%(392×295 的卡里标题约占 11.5k px²),
        // 鼠标停在封面图、作者行、卡片空白处都点不动 —— 与「卡片」这个视觉隐喻不符。
        //
        // ★ 三条必须同时满足,否则会把站内交互搞坏:
        //   ① 只处理版块页的卡片(#wall ul.stream > li),不动帖子页与首页;
        //   ② 排除卡片内的【其它交互元素】:收藏按钮、作者链接、日期胶囊。
        //      最简单的做法是不去遍历排除,而是在捕获阶段判断
        //      `e.target.closest('a,button,input,textarea,select')` ——
        //      只要点在任何一个真实交互元素上就【不接管】,交给它们自己处理。
        //      这比维护一张排除清单可靠:站点以后加什么控件都不会误伤。
        //   ③ 幂等:MutationObserver 会反复调用(懒加载、重排都会触发),
        //      用 dataset 标记避免给同一张卡重复挂层。
        //
        // ★ 为什么用「铺一层 .spp-card-hit」而不是直接给 li 绑 click:
        //   li 上已经有站点的点击委托(站点用 li 承载 dcsns 逻辑),
        //   直接绑会让「点收藏」也冒泡成「打开帖子」。铺一层透明层并让它
        //   在捕获阶段先行判断,跳转与站内交互的边界就清晰了。
        //   层的 z-index(5) 低于交互元素(6),所以交互元素天然压在它上面。
        //
        // ★★ 2.52.66:跳转目标由【当前页】改为【新标签页】。
        //   用户要求:「点击卡片进入界面时,不应覆盖当前网页,而是应新打开一个页面。」
        //   ★ 这条推翻了 2.48.0 写在这里的旧决策,所以旧注释必须一起改掉 ——
        //     留着它,下一个人会照着「与点标题完全一致(含浏览器历史)」的理由改回 location.href。
        //   ★ 实现方式:命中层由 `div` + JS 跳转换成了【真正的 <a target="_blank">】,
        //     而不是把 location.href 换成 window.open。理由:
        //       · 真链接 ⇒「新标签页」是浏览器默认语义,不受弹窗拦截的 windowFeatures
        //         启发式影响(window.open 一旦带 windowFeatures,Chrome 会按"弹出窗口"
        //         而不是标签页处理);
        //       · 顺带白拿原生能力:Ctrl+点击 / 中键 / 右键「在新标签页中打开」/
        //         悬停显示目标地址 —— 这些在 div 上永远做不出来;
        //       · rel="noopener" 由 HTML 属性保证,不依赖事后置 opener。
        function initCardClick() {
            const wall = document.getElementById('wall');
            if (!wall) return 0;

            const lis = wall.querySelectorAll('ul.stream > li');
            let n = 0;
            lis.forEach(li => {
                if (li.dataset.sppClickDone) return;
                const titleLink = li.querySelector('.section-title a');
                if (!titleLink) return;              // 没有链接的卡不接管
                const href = titleLink.getAttribute('href');
                if (!href) return;

                li.dataset.sppClickDone = '1';
                li.classList.add('spp-card-clickable');

                // 站点的标题链本身没有 target —— 一并补上。
                // ★ 实测说明(别被这一行骗了):标题链目前【点不到】——
                //   本脚本自己给 .section-title 写了 position:relative + z-index:1,
                //   它形成了一个比命中层(z-index:5)更低的堆叠上下文,
                //   于是写在锚点上的 z-index:6 被父级封顶,三条 line-box 中心打点
                //   全部命中 A.spp-card-hit(2.52.66 实测)。
                //   也就是说 2.48.0 起「把标题锚点抬到 6」这条 CSS 一直是空转的。
                //   行为上不影响本次需求(点标题与点卡片都开新标签页、同一个地址),
                //   但保留这一行是为了:堆叠一旦被修正,这里立刻就是对的。
                titleLink.setAttribute('target', '_blank');
                titleLink.setAttribute('rel', 'noopener');

                const hit = document.createElement('a');
                hit.className = 'spp-card-hit';
                hit.href = href;
                hit.target = '_blank';
                hit.rel = 'noopener';
                hit.setAttribute('aria-hidden', 'true');
                // ★ 不进 Tab 序:卡片里已有真标题链,再塞一个同名链接会让键盘用户连续遇到两个。
                hit.tabIndex = -1;
                // 捕获阶段守卫:只处理「落在命中层自己身上」的点击。
                // ★ 必须排除 hit 自己 —— 命中层现在本身就是 <a>,
                //   不排除的话 `t.closest('a,…')` 恒为真、守卫会永远让路、新标签页打不开。
                hit.addEventListener('click', e => {
                    const t = e.target;
                    if (t === hit) return;                      // 交还给 <a> 的原生跳转
                    if (t.closest && t.closest('a,button,input,textarea,select,label')) {
                        e.preventDefault();
                        e.stopPropagation();
                    }
                }, true);
                li.appendChild(hit);
                n++;
            });
            return n;
        }

        onDomReady(startTL);
    }

    // ==================== 4b. 资源区「已读」记录(2.55.1)====================
    // 用户原话(资源区卡片列表,截图里两枚红框标着 ①②):
    //   「我发现一和二的字体颜色为什么不一样?能不能让二的统一字体效果为一的效果?」
    //   「能不能增加一个功能,记录用户打开了哪些网页。该功能对茶馆不生效,仅对资源区生效。」
    //   →「针对颜色变暗的情况,我在想资源区下方能否增加一个『已读』UI 标识。只有点击后才会显示
    //     『已读』…未点击则不显示。颜色变暗或修改颜色时,无需已读,颜色统一为一号。」
    //     (标识位置:卡片底部数据行,与「收藏」同排)
    //   →「在设置面板中增加一个 UI 框,用于设定页面停留时长,即用户在该页面停留多少秒才算作已阅读,
    //      默认数值为 5秒。可修改」+「将此数量改为两万条」+「增加一个清除纪录(…允许用户清除
    //      指定时间之前的数据,例如一小时或一千小时。它有一个计量单位,可以设置为小时,
    //      也可以设置为日期)」
    //
    // ★ 口径(逐条对应上面每一句):
    //   ① 生效面 = 资源区,判定只有一处:readScopeOn()。茶馆(fid 9)一律 false。
    //   ② 已读 = 打开该帖并【实际停留 ≥ spp_read_dwell 秒】(默认 5,0 = 打开即算)。
    //      「实际停留」只累计【页面可见】的时间(document.visibilityState === 'visible')——
    //      后台标签页里流逝的时间不算停留:这正是「停留多久才算读过」的语义,
    //      也顺带解决了「中键连开十几个帖子」把记录刷满的问题(那是本功能的常见用法)。
    //   ③ 记录只占一个 GM 键:spp_read_log = {"<tid>": <毫秒时间戳>},上限两万条(READ_LOG_CAP),
    //      超出时按时间戳淘汰最旧的。
    //   ④ 列表页给已读的卡挂一枚「已读」胶囊(.spp-read-chip,紧贴「收藏」右侧);
    //      未读不挂;茶馆与首页不挂。
    //
    // ★ 为什么【不做】乐观写入 + 回滚:设计稿里那条(点击时先写一笔、离开页面若停留不够再删掉)
    //   没有必要 —— 停留时长在【帖子页】就能判定,本模块只在判定成立的最后一刻写一笔。
    //   于是「没停够就离开」天然什么都不会写:既不需要回滚窗口,也不存在
    //   「浏览器被强杀 ⇒ 回滚没跑到 ⇒ 留一条假已读」这个漏洞。
    //
    // ★ 为什么淘汰最旧的而不是整表清空:用户要的是「两万条」这个**容量上限**,不是「到点全清」;
    //   整表清空会让「已读」在某一天突然全部消失,淘汰最旧的则始终留着最近两万条。
    const READ_LOG_KEY = 'spp_read_log';
    const READ_LOG_CAP = 20000;
    const READ_DWELL_DEF = 5;                 // 秒
    const READ_DWELL_MAX = 600;               // 上限 10 分钟 —— 再长就不叫「停留判定」了
    const READ_PURGE_UNITS = ['hour', 'day'];
    const READ_PURGE_UNIT_MS = { hour: 3600000, day: 86400000 };

    const readLog = {
        _map: null,
        _dirty: false,
        _timer: 0,
        // 读一次进内存:列表页 44 张卡会问它 44 次,不必每次都解析那串几百 KB 的 JSON
        load() {
            if (this._map) return this._map;
            let map = {};
            try {
                const raw = GM_getValue(READ_LOG_KEY, null);
                if (raw) {
                    const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
                    if (parsed && typeof parsed === 'object') map = parsed;
                }
            } catch (_) { map = {}; }
            // 存储被手改坏过也不怕:只收「键是纯数字 tid、值是正数时间戳」的条目
            const clean = {};
            Object.keys(map).forEach(k => {
                const t = Number(map[k]);
                if (/^\d+$/.test(k) && isFinite(t) && t > 0) clean[k] = t;
            });
            this._map = clean;
            return clean;
        },
        has(tid) { return !!this.load()[String(tid)]; },
        count() { return Object.keys(this.load()).length; },
        mark(tid) {
            const k = String(tid || '');
            if (!/^\d+$/.test(k)) return false;       // 不是帖子 id(跳转页 / 广告位)一律不记
            const m = this.load();
            if (m[k]) return false;                   // 记过就不动时间戳:「已读」不需要刷新时间
            m[k] = Date.now();
            const keys = Object.keys(m);
            if (keys.length > READ_LOG_CAP) {
                keys.sort((a, b) => m[a] - m[b]);     // 最旧的排前面
                keys.slice(0, keys.length - READ_LOG_CAP).forEach(x => { delete m[x]; });
            }
            this.later();
            return true;
        },
        // 删掉「早于 amount 个 unit」的记录,返回删掉的条数(unit 见 READ_PURGE_UNIT_MS)
        purgeBefore(amount, unit) {
            const span = (READ_PURGE_UNIT_MS[unit] || READ_PURGE_UNIT_MS.hour) * (Number(amount) || 0);
            if (span <= 0) return 0;
            const cut = Date.now() - span;
            const m = this.load();
            let n = 0;
            Object.keys(m).forEach(k => { if (m[k] < cut) { delete m[k]; n++; } });
            if (n) this.later();
            return n;
        },
        // 整表清空(面板「清除全部记录」出口,2.55.2)。返回清掉的条数。
        //   ★ 与 purgeBefore 分开成两条出口:这一条【不看阈值参数】—— 用户要的就是
        //     「一次把记录清零」,让他先把阈值调小再点,等于把一步操作拆成两步。
        clearAll() {
            const m = this.load();
            const n = Object.keys(m).length;
            if (!n) return 0;
            Object.keys(m).forEach(k => { delete m[k]; });
            this.later();
            return n;
        },
        // 攒 1.2 秒再落盘:连开十几个帖子也只写一次存储
        later() {
            this._dirty = true;
            if (this._timer) return;
            this._timer = setTimeout(() => { this._timer = 0; this.flush(); }, 1200);
        },
        flush() {
            if (!this._dirty || !this._map) return;
            try { GM_setValue(READ_LOG_KEY, JSON.stringify(this._map)); this._dirty = false; } catch (_) {}
        }
    };

    // 当前版块 id:版块页地址上就有(fid-14 与 fid=14 两种写法站点都在用);
    // 帖子页地址上【没有】fid(read.php?tid-…),从面包屑里的版块链兜底 ——
    // 实测 read.php 顶部面包屑是 index.php › thread.php?fid-13.html › thread.php?fid-14.html › 帖子标题,
    // 取最后一枚 thread.php?fid-N 的 N 就是本帖所在版块。
    function readPageFid() {
        const m = location.href.match(/fid[-=](\d+)/);
        if (m) return m[1];
        let fid = '';
        document.querySelectorAll('#breadcrumbs a[href*="thread.php?fid"]').forEach(a => {
            const mm = String(a.getAttribute('href') || '').match(/fid[-=](\d+)/);
            if (mm) fid = mm[1];
        });
        return fid;
    }

    // 卡片列表页判定:地址是 thread.php / thread_new.php 且带 fid —— 首页与帖子页都不是
    function readIsListPage() {
        return /thread(_new)?\.php/.test(location.pathname) && /fid[-=]\d+/.test(location.href);
    }

    // 帖子页判定:read.php?tid-XXXX → 返回 tid(不是帖子页则返回空串)
    function readTidFromUrl() {
        if (!/read\.php/i.test(location.pathname)) return '';
        const m = location.href.match(/[?&]tid[-=](\d+)/);
        return m ? m[1] : '';
    }

    // 生效面:开关开着 + 不是茶馆 + 版块链名里含「资源」
    //   ★ 按**版块名**判定而不是写死一张 fid 白名单:资源区是成片的一族版块
    //     (实测已见到 fid-13「免空资源区」与 fid-14「CG资源」是父子,地址上是两个不同的 fid),
    //     而面包屑与页面标题里始终带着版块名(实测标题「CG资源| 免空资源区 - 南+ South Plus」)。
    //     写死白名单的话,站点新开一个子版块就静默失效;按名字判定则自动跟上。
    //   ★ 茶馆那一条【必须按 fid 判】:茶馆的帖子标题里完全可能出现「资源」两个字,
    //     只按名字判会把茶馆的帖子也算进来 —— 而用户口径是「不针对茶馆」。
    function readScopeOn() {
        if (!SPP_CONFIG.bool('spp_read_mark', true)) return false;
        if (readPageFid() === '9') return false;
        const crumb = document.getElementById('breadcrumbs');
        const text = ((crumb && crumb.textContent) || '') + ' ' + (document.title || '');
        return /资源/.test(text);
    }

    let READ_SCOPE = null;
    function readScope() {
        // 只判一次:开关的改动走「保存并刷新」,同一次页面生命周期里它不会中途变
        if (READ_SCOPE === null) READ_SCOPE = readScopeOn();
        return READ_SCOPE;
    }

    function readTidOfCard(li) {
        const a = li.querySelector('.section-title a') || li.querySelector('a[href*="tid"]');
        const m = a ? String(a.getAttribute('href') || '').match(/tid[-=](\d+)/) : null;
        return m ? m[1] : '';
    }

    // 已读的卡挂一枚胶囊;未读的(或记录已被清掉的)把这枚胶囊摘掉 —— 幂等,随 processItems 反复调用
    //   ★ 插在「收藏」锚点【之后、同一个 span 里】:数据行是 flex + first-child margin-right:auto,
    //     直接塞成 meta 的第三个子项会落到最右端贴着「回复 / 热度」,
    //     而用户口径是「与收藏同排」。
    function applyReadChip(li) {
        if (!readIsListPage() || !readScope()) return 0;
        const tid = readTidOfCard(li);
        if (!tid) return 0;
        const meta = li.querySelector('.spp-card-meta');
        if (!meta) return 0;                       // 数据行还没建出来:下一轮 MutationObserver 再来
        const chip = meta.querySelector('.spp-read-chip');
        if (!readLog.has(tid)) {
            if (chip) chip.remove();
            return 0;
        }
        if (chip) return 1;
        const el = document.createElement('span');
        el.className = 'spp-read-chip';
        el.textContent = '已读';
        const fav = meta.querySelector('a.spp-fav') || meta.querySelector('a#favor, a[onclick*="action=favor"]');
        if (fav && fav.parentNode) fav.parentNode.insertBefore(el, fav.nextSibling);
        else meta.appendChild(el);
        return 1;
    }

    // 记录被清掉后就地重绘当前页的标识(不刷新页面 —— 与「恢复默认头像」同一条取舍)
    function refreshReadChips() {
        document.querySelectorAll('#wall ul.stream > li').forEach(applyReadChip);
    }

    // 面板「清除记录」按钮的出口:按面板上当时的阈值清,清完就地重绘
    function readPurgeNow(amount, unit) {
        const removed = readLog.purgeBefore(amount, unit);
        readLog.flush();
        refreshReadChips();
        return { removed: removed, left: readLog.count() };
    }

    // 面板「清除全部记录」按钮的出口(2.55.2):整表清掉,并就地重绘当前页的标识
    function readWipeNow() {
        const removed = readLog.clearAll();
        readLog.flush();
        refreshReadChips();
        return { removed: removed, left: readLog.count() };
    }

    // 帖子页:累计【可见停留】时长,够了就记一笔(见本模块顶部 ②)
    function initReadDwell() {
        const tid = readTidFromUrl();
        if (!tid || !readScope()) return 0;
        if (readLog.has(tid)) return 0;             // 已经记过:一个定时器都不用起
        const need = SPP_CONFIG.num('spp_read_dwell', READ_DWELL_DEF, 0, READ_DWELL_MAX) * 1000;
        let acc = 0;
        let last = Date.now();
        let timer = 0;
        function tick() {
            const now = Date.now();
            // ★ 只累计可见时间:后台标签页里 now - last 照样在走,但那不是「停留」
            if (document.visibilityState === 'visible') acc += now - last;
            last = now;
            if (acc < need) return;
            readLog.mark(tid);
            stop();
        }
        function stop() {
            if (timer) { clearInterval(timer); timer = 0; }
            document.removeEventListener('visibilitychange', tick);
            readLog.flush();                        // 记完立刻落盘,不等那 1.2 秒的攒批
        }
        timer = setInterval(tick, 500);
        document.addEventListener('visibilitychange', tick);
        window.addEventListener('pagehide', function () { tick(); readLog.flush(); });
        tick();                                     // 停留要求为 0 时,这一步就把它记下
        return 1;
    }

    onDomReady(initReadDwell);


    // ==================== 6b. 屏蔽底部图例条与页脚(全站) ====================
    // 星空背景下,页面底部这两块是【纯白底】,各占满一整行,非常突兀:
    //   ① 图例条  div.t5.t_one   —— 「无新帖版块 / 有新帖版块 / 锁定的版块」1400×56
    //   ② 页脚    div#footer     —— 「Total 0.18s / Powered by PHPWind… 」1400×80
    //
    // ★ 选择器必须写 .t5.t_one,【绝不能只写 .t5】:
    //   .t5 是全站通用类,帖子页上它是【作者信息面板】(那个页面实测有 10 个 .t5)
    //   以及快速发帖框。只写 .t5 会把作者资料和发帖框一起隐藏,论坛直接不可用。
    //   实测 .t_one 只在首页与版块页出现,且恰好就是这条图例 —— 所以两个类一起用才是安全交集。
    //
    // ★ 2.52.3:本条原本是「6. 版规屏蔽」的旁支(那节只作用于版块页,本条要全站生效)。
    //   版规屏蔽整体取消后,本条成为唯一负责隐藏图例条与页脚的规则,继续留在全局启动序列里。
    //
    // 隐藏后 wrapA 会从 6020 收到 5960,页面高度 6113 → 5974,
    // 不会留下空白占位(已实测)。
    function initHideBottom() {
        const KEY = 'spp_hide_bottom';
        let hidden = SPP_CONFIG.bool(KEY, true);   // ★ 2.52.93:走集中读数层(唯一登记处,方便将来做面板)
        const STYLE_ID = 'spp-hide-bottom-style';

        function apply() {
            let el = document.getElementById(STYLE_ID);
            if (hidden) {
                if (!el) {
                    el = document.createElement('style');
                    el.id = STYLE_ID;
                    el.textContent = '.t5.t_one, .rinsp-quick-action-overlay, #footer { display: none !important; }';
                    appendToHeadSafe(el);
                }
            } else {
                if (el) el.remove();
            }
        }

        apply();
    }

    // ==================== 6c. 屏蔽「论坛相关」白底面板(全站) ====================
    // 星空背景下,页面最底部还压着一整块【纯白底板】:
    //   div.t 1400×285  background rgba(250,252,255,.93)
    //   里面是 TABLE > TBODY#cate_info,5 行内容:
    //     tr2「友情链接」 / tr3 本站 banner / tr3 15 条友链
    //     tr2「在线用户」+ <a name="online"> / tr3 管理员·总版主·论坛版主·普通会员 图例
    //
    // ★ 这块【既是视觉残留,又是两个已有功能的数据源】,所以隐藏前必须确认它已无用途:
    //   ① 在线人数徽标 moveOnlineStats() 从这一行的文本里正则取值 →
    //      但它在 DOMContentLoaded 就把徽标插进了统计卡操作条,取值完成后源码行不再被读;
    //      且 display:none 不影响 querySelectorAll / textContent,即使重复执行也仍读得到。
    //   ② 友情链接卡片 pickFriendLinks() 也读 TBODY#cate_info →
    //      同样在渲染时一次性读取,隐藏后 DOM 依然完整。
    //   实测:隐藏该块后 badge 仍在(1 个)、友链仍可读到 15 条,功能无损。
    //
    // ★ 选择器怎么写:块的唯一稳定特征是「含有 #cate_info」,
    //   站点没给宿主任何专属 class / id,所以只能靠 :has() 或运行时打标记。
    //   两条规则【必须分开写】,绝不能合并成一条逗号选择器。
    //   实站实测(用不存在的伪类冒充「不支持 :has() 的浏览器」):
    //     拆成两条独立规则 → 规则数 1,class 兜底生效,块被隐藏   ✓
    //     合并成一条逗号   → 规则数 0,【连 class 兜底一起被丢弃】,块重新显形 ✗
    //   即:不支持 :has() 的浏览器上,合并写法会让整个功能静默失效。
    //   (实测是「整条规则丢弃」,与项在列表中的先后无关 —— 兜底放在前面同样被丢弃。)
    //
    // ★ 绝不能只用 div.t:全页有 6 个 div.t(顶部四栏、#t_39 漫区 2686px、#t_7、#t_1、
    //   隐藏的 #t_123),直接隐藏 div.t 会把整个首页抹掉。
    function initHideForumRel() {
        const KEY = 'spp_hide_forumrel';
        let hidden = SPP_CONFIG.bool(KEY, true);   // ★ 2.52.93:走集中读数层(唯一登记处,方便将来做面板)
        const STYLE_ID = 'spp-hide-forumrel-style';
        const CLS = 'spp-fr-hidden';

        // 支持 :has() 时浏览器自己就命中,不依赖 JS 时机;不支持的浏览器靠这个 class
        function mark(on) {
            const cate = document.getElementById('cate_info');
            if (!cate) return;
            const block = cate.closest('div.t');
            if (!block) return;
            if (on) block.classList.add(CLS);
            else block.classList.remove(CLS);
        }

        function apply() {
            let el = document.getElementById(STYLE_ID);
            if (hidden) {
                if (!el) {
                    el = document.createElement('style');
                    el.id = STYLE_ID;
                    el.textContent =
                        'div.t:has(#cate_info) { display: none !important; }\n' +
                        'div.t.' + CLS + ' { display: none !important; }';
                    appendToHeadSafe(el);
                }
            } else {
                if (el) el.remove();
            }
            mark(hidden);
        }

        apply();
        // document-start 时 #cate_info 还没解析出来,等 DOM 就绪补一次;
        // 站点会 ajax 刷新在线人数(可能重建该表),所以再补两次,与既有约定一致。
        onDomReady(function () { mark(hidden); });
        setTimeout(function () { mark(hidden); }, 1500);
        setTimeout(function () { mark(hidden); }, 2200);
    }

    // ==================== 屏蔽顶部空白工具条(#toptool) ====================
    // 站点在 #mainNav 之上还留了一条 #toptool(实测 1400×14px)。
    // 它自己那三个链接(无图版/帮助/论坛首页)被站点内联写成 display:none,
    // 所以这条带子【实测 0 个可见子元素】,纯粹是 14px 的空白条 ——
    // 却仍然占高度、仍然画底色与底边框(浅灰 #f4f8fc 0.8 + 1px 边)。
    // 实站量点:y=2/5/8/11 处 elementFromPoint 命中的都是 DIV#toptool,
    // 用户箭头指向的正是这一段。
    //
    // ★ 为什么用 display:none 而不是 height:0:
    //   它带 backdrop-filter 与 border-bottom,只压高度仍会留下 1px 边线。
    // ★ 只针对 #toptool 这个站点自带的 id,不碰 #header(#header 里挂着
    //   #mainNav 与 #infobox,写宽了会把整个顶部连内容一起藏掉)。
    function initHideTopTool() {
        const KEY = 'spp_hide_toptool';
        let hidden = SPP_CONFIG.bool(KEY, true);   // ★ 2.52.93:走集中读数层(唯一登记处,方便将来做面板)
        const STYLE_ID = 'spp-hide-toptool-style';

        function apply() {
            let el = document.getElementById(STYLE_ID);
            if (hidden) {
                if (!el) {
                    el = document.createElement('style');
                    el.id = STYLE_ID;
                    // ★ 必须带 !important:主题表里 `#toptool,#header{...}` 与
                    //   星空表的 `html.spp-hm-star #toptool{...}` 都会给它上样式。
                    el.textContent = '#toptool { display: none !important; }';
                    appendToHeadSafe(el);
                }
            } else {
                if (el) el.remove();
            }
        }

        apply();
    }

    // ==================== 导航精简:去掉「讨论区」,把「搜索」提为主行动项 ====================
    // 用户反馈:讨论区不要了,把搜索弄到讨论区那个 UI 框里。
    //
    // 站点结构(实测):
    //   <li id="h_index" class=" current"><a href="//www.south-plus.net">讨论区</a></li>  ← 74×30
    //   <li id="h_search"><a href="search.php">搜索</a></li>                              ← 60×30
    // 首页的「当前项」药丸样式挂在 .current 上,而 .current 在 #h_index 身上。
    // 所以做法是:把 #h_index 从导航里摘掉(ul.guide 是 float:right + li float:left,
    // 摘掉第一项后「搜索」自然补位到最左),再把原来只给 .current 的
    // 主色药丸样式显式给到 #h_search —— 视觉上就是「搜索坐进了讨论区那个框」。
    //
    // ★ 为什么给 #h_search 用 `li#h_search > a`(2 id)而不是复用 .current:
    //   .current 是站点的「当前页」标记,换页会跑到别的 li 上。用它做「搜索」的
    //   样式载体,会在别的页面失效。这里按 id 绑,任何页面都稳定。
    //   2 id 也足以压过主题里的 `#mainNav a{color:...}`(1 id + 1 elem)。
    //   注:主题全站链接规则写的是 `a:not(...):not(#mainNav a)`,#mainNav 的 a
    //   本就被排除在外,不会被它改色。
    function initNavSimplify() {
        const KEY = 'spp_nav_search_first';
        // ★ 2.52.93:这里原先顺手把默认值(1)写回存储 —— 撤掉,理由与 initPageWiden 同一套:
        //   存储里「没这个键」本身就是信息,写一份默认值进去只会让面板回填分不清
        //   「用户设过 1」和「从没设过」(现在没设过就走 SPP_CONFIG 的内置默认 1)。
        let on = SPP_CONFIG.bool(KEY, true);
        const STYLE_ID = 'spp-navsimplify-style';

        function apply() {
            let el = document.getElementById(STYLE_ID);
            if (on) {
                if (!el) {
                    el = document.createElement('style');
                    el.id = STYLE_ID;
                    el.textContent =
                        // 1) 去掉「讨论区」(站点首页导航的第一项)
                        '#mainNav ul.guide > li#h_index { display: none !important; }\n' +
                        // 2) 「搜索」接手主行动项药丸
                        '#mainNav ul.guide > li#h_search > a,\n' +
                        '#mainNav ul.guide > li#h_search > a:hover {\n' +
                        '    background: linear-gradient(180deg,#37837a,#2f6f68) !important;\n' +
                        '    background-image: linear-gradient(180deg,#37837a,#2f6f68) !important;\n' +
                        '    color: #fff !important;\n' +
                        '    font-weight: 700 !important;\n' +
                        '    border-radius: 999px !important;\n' +
                        '    box-shadow: 0 2px 10px -2px rgba(32,201,151,.5), inset 0 1px 0 rgba(255,255,255,.16) !important;\n' +
                        '}';
                    appendToHeadSafe(el);
                }
            } else {
                if (el) el.remove();
            }
        }

        apply();
    }

    // ==================== 页眉:短消息图标换 SVG + 两枚导航新窗口(2.53.14) ====================
    // 用户批注(版块页页眉,thread_new.php?fid-9-page-1):
    //   ①「使用SVG邮件替换这个老旧的」——站点那枚 <a id="td_msg"><img src="…/no-shortmail.gif"></a>
    //     是 14×11 的老式 GIF 位图。换成内联 SVG(16×12,**固定白描边**)——
    //     2.53.15 用户定案:「只要白色线框亮着就好了」⇒ 不跟主题字色、也不做未读变色。
    //   ★ 为什么不能直接把 img 删掉:a#td_msg 的 id 是 phpwind 短消息轮询的挂载点,
    //     有新信时站点自己会换掉里面那张图(也可能整段重写 innerHTML)。所以:
    //       · <a> 与 <img> 原样留着(轮询照旧工作),img 只 visibility:hidden 让出视觉位
    //         —— 用 visibility 而不是 display:保留布局盒,热区仍是 17×13;
    //       · SVG 绝对定位叠在同一格里(pointer-events:none,点击仍走 <a href="message.php">);
    //       · MutationObserver 盯子树:站点重写就拿回 SVG,img 换成非 no-shortmail 那张
    //         就把描边点成提醒色(形状仍是用户已通过的新稿 A)。
    //   ②「点击弹出新窗口,而不是覆盖自身」——只做两枚(用户定案):搜索 / 社区论坛任务。
    //     站点这两枚都没写 target;这里按【文本】认(去空白比对),不按位置/索引。
    function initHeaderBits() {
        const SVG_NS = 'http://www.w3.org/2000/svg';
        const ICON = '<rect x="0.75" y="1" width="14.5" height="10" rx="1.7"/>' +
                     '<path d="M1.5 2.2 L8 6.7 L14.5 2.2"/>';

        // ---- ② 搜索 / 社区论坛任务:新窗口 ----
        function blankLinks() {
            const want = { '搜索': 1, '社区论坛任务': 1 };
            let matched = 0;
            [].slice.call(document.querySelectorAll('#mainNav a, ul.guide a')).forEach(a => {
                const t = (a.textContent || '').replace(/\s+/g, '');
                if (!want[t]) return;
                matched++;
                if (a.getAttribute('target') === '_blank' &&
                    a.getAttribute('rel') === 'noopener noreferrer') return;
                a.setAttribute('target', '_blank');
                a.setAttribute('rel', 'noopener noreferrer');
            });
            return matched;
        }

        // ---- ① 短消息图标:站点 GIF → 内联 SVG ----
        function makeSvg() {
            const svg = document.createElementNS(SVG_NS, 'svg');
            svg.setAttribute('viewBox', '0 0 16 12');
            svg.setAttribute('width', '16');
            svg.setAttribute('height', '12');
            svg.setAttribute('fill', 'none');
            svg.setAttribute('stroke', '#fff');
            svg.setAttribute('stroke-width', '1.2');
            svg.setAttribute('stroke-linecap', 'round');
            svg.setAttribute('stroke-linejoin', 'round');
            svg.setAttribute('class', 'spp-mail-ic');
            svg.setAttribute('aria-hidden', 'true');
            svg.style.cssText = 'position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);pointer-events:none;';
            svg.innerHTML = ICON;
            return svg;
        }
        function mailIcon() {
            const a = document.getElementById('td_msg');
            if (!a) return false;
            const img = a.querySelector('img');
            if (img) img.style.setProperty('visibility', 'hidden', 'important');
            a.style.setProperty('position', 'relative', 'important');
            let svg = a.querySelector('svg.spp-mail-ic');
            if (!svg) { svg = makeSvg(); a.appendChild(svg); }
            // 2.53.15(用户定案):固定白描边 ⇒ 不再按未读态换色,形状与颜色都只这一种。
            if (!a.__sppMailWatch) {
                a.__sppMailWatch = new MutationObserver(() => { mailIcon(); });
                a.__sppMailWatch.observe(a, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] });
            }
            return true;
        }

        let tries = 0;
        (function boot() {
            const icon = mailIcon();
            const n = blankLinks();
            // 页眉是服务端渲染的静态 HTML,但站点可能有脚本重建它 ⇒ 收住后就不再轮询
            if ((icon && n >= 2) || tries++ >= 40) return;
            setTimeout(boot, 300);
        })();
    }

    // ==================== 8. 自动图墙模式(全版块 thread.php / thread_new.php) ====================
    function initWallMode() {
        if (!PAGE.board) return;

        function switchToWall() {
            // 勾选"默认图墙模式"
            const cb = document.querySelector('input[type="checkbox"][name*="wall"]');
            if (cb && !cb.checked) cb.checked = true;

            // 已在图墙模式
            if (document.getElementById('wall')) return;

            // ★ 2.52.8:切图墙必须【保留当前筛选】。站点在列表模式页只给一个固定链接
            //   thread_new.php?fid-N-page-1.html,直接点它会把 search-/type-/special-/page- 段全丢掉:
            //   用户在资源区点「分类」(thread.php?fid-14-type-1.html)后看似没反应,
            //   其实是退回未筛选的第 1 页。实测图墙页吃这些段:
            //   thread_new.php?fid-14-type-1-page-1.html → setCurrent('thread_type_1')。
            function wallUrl(from) {
                const s = String(from);
                const fid = s.match(/fid-(\d+)/) || s.match(/[?&]fid=(\d+)/);
                if (!fid) return '';
                const seg = [];
                ['type', 'special'].forEach(function (k) {
                    const m = s.match(new RegExp(k + '-(\\d+)')) || s.match(new RegExp('[?&]' + k + '=(\\d+)'));
                    if (m) seg.push(k + '-' + m[1]);
                });
                const d = s.match(/search-([a-z]+)/) || s.match(/[?&]search=([a-z]+)/);
                if (d) seg.push('search-' + d[1]);
                const p = s.match(/page-(\d+)/) || s.match(/[?&]page=(\d+)/);
                return location.origin + '/thread_new.php?fid-' + fid[1] +
                    (seg.length ? '-' + seg.join('-') : '') + '-page-' + (p ? p[1] : '1') + '.html';
            }

            // ★ 只认站点自己的模式切换链接(.s3 → thread_new.php 且文字含「图墙」):
            //   原先扫「任意含图墙字样的第一个链接」,侧栏的「图墙模式」版块入口也会被命中。
            let sw = null;
            Array.prototype.some.call(document.querySelectorAll('a.s3[href*="thread_new.php"]'), function (a) {
                if (/图墙/.test(a.textContent || '')) { sw = a; return true; }
                return false;
            });
            if (!sw) return;

            const wall = wallUrl(location.pathname + location.search + location.hash);
            if (wall) { location.href = wall; return; }
            sw.click();
        }

        onDomReady(switchToWall);
    }

    // ==================== 12. BBCode 所见即所得预览(内联切换,不弹窗) ====================
    function initPreviewButton() {
        const KEY = 'spp_preview_btn';
        let enabled = SPP_CONFIG.bool(KEY, true);   // ★ 2.52.93:走集中读数层

        if (!enabled) return;

        // ★★ 2.53.60(用户:「实时默认渲染这个功能默认是打开的,可以在脚本的悬浮设置面板那里关闭」):
        //   本模块从「点一次渲染一次的切换预览」升级成**实时预览**,默认开。
        //   ★ 两个开关是两层意思,别混:spp_preview_btn = 这枚按钮在不在;
        //     spp_preview_live = 渲染是「边打边渲」还是「点一次渲一次」。
        //   ★ 关掉实时之后走的就是 2.53.59 那条老路径(下面所有 LIVE 分支的反面),
        //     所以老代码一行没动 —— 这是本轮唯一能保证「关掉即回到从前」的写法。
        const LIVE = SPP_CONFIG.bool('spp_preview_live', true);

        // 仅在有编辑器的页面生效(read.php 有快速回复框,post.php 有完整编辑器);纯列表页跳过
        if (!WANTS.editorUi) return;
        // —— BBCode → HTML 转换器 ——
        function bbcodeToHTML(bbcode) {
            let html = bbcode;
            html = html.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');

            html = html.replace(/\[b\]([\s\S]*?)\[\/b\]/gi, '<strong>$1</strong>');
            html = html.replace(/\[i\]([\s\S]*?)\[\/i\]/gi, '<em>$1</em>');
            html = html.replace(/\[u\]([\s\S]*?)\[\/u\]/gi, '<u>$1</u>');
            html = html.replace(/\[s\]([\s\S]*?)\[\/s\]/gi, '<del>$1</del>');
            html = html.replace(/\[color=([#\w]+)\]([\s\S]*?)\[\/color\]/gi,
                '<span style="color:$1">$2</span>');
            /* ★★ 2.54.3:预览的 [size] 直接吐 <font size="N"> —— 服务端就是这么吐的,
               浏览器对 <font size> 的映射(本机实测、14px 基准)是
               1:12 2:13 3:16 4:18 5:24 6:32 7:48。
               原来那张自编的 px 表(4→16px、6→22px…)与真机差一大截,预览看着就「不生效」。
               ★ 夹紧照抄服务端实测:7 / 8 / 99 一律变 size="6";小于 1 的按 1 算。 */
            html = html.replace(/\[size=(\d+)\]([\s\S]*?)\[\/size\]/gi, function(_, size, text) {
                let n = parseInt(size, 10);
                if (!isFinite(n) || n < 1) n = 1;
                if (n > 6) n = 6;
                return '<font size="' + n + '">' + text + '</font>';
            });
            html = html.replace(/\[font=([^\]]+)\]([\s\S]*?)\[\/font\]/gi,
                '<span style="font-family:$1">$2</span>');
            html = html.replace(/\[align=(\w+)\]([\s\S]*?)\[\/align\]/gi,
                '<div style="text-align:$1">$2</div>');
            /* ★★ 2.54.1:预览得跟上工具栏(用户:「有序列表不生效、表格、没渲染成功」
               「块引失效」)。这一段原来只认老写法 —— [list] 还是按 [*] 切的,
               [li] / [list=1] / [table] / [blockquote] 一个都没写,于是工具栏插出来的
               东西在实时预览里就是裸标签。下面全部对齐服务端实测的渲染结果。
               ★ 换行仍由最后那句 \n → <br> 负责 —— 所以「空行 / 紧排」两种列表在预览里
                 会原样体现(这正是要看的东西:条目之间到底空不空行)。 */
            html = html.replace(/\[list(=[1aA])?\]([\s\S]*?)\[\/list\]/gi, function (m, kind, content) {
                let inner;
                if (/\[li\]/i.test(content)) {
                    /* 新写法:只把 [li] / [/li] 换成 <li> / </li>,**条目之间的换行原样留着**
                       —— 最后那句 \n → <br> 会把它翻成 <br>,于是「空行 / 紧排」在预览里
                       如实体现(服务端就是这么吐的:
                       <ul><br><li>甲</li><br><li>乙</li><br></ul>)。 */
                    inner = content.replace(/\[li\]/gi, '<li>').replace(/\[\/li\]/gi, '</li>');
                } else {
                    /* 老写法 [list][*]甲[*]乙[/list](站点的老帖子还在用):按 [*] 切 */
                    inner = content.split(/\[\*\]/).filter(function (x) { return x.trim(); })
                        .map(function (x) { return '<li>' + x.trim() + '</li>'; }).join('');
                }
                const ol = !!kind;
                const tag = ol ? 'ol' : 'ul';
                return '<' + tag + (ol ? ' type="' + kind.slice(1) + '"' : '') + '>' + inner + '</' + tag + '>';
            });
            html = html.replace(/\[blockquote\]([\s\S]*?)\[\/blockquote\]/gi,
                '<blockquote class="spp-pv-bq">$1</blockquote>');
            html = html.replace(/\[table(?:=([^\]]+))?\]([\s\S]*?)\[\/table\]/gi, function (m, w, content) {
                const rows = [];
                const rre = /\[tr\]([\s\S]*?)\[\/tr\]/gi;
                let r;
                while ((r = rre.exec(content))) {
                    const cells = [];
                    const cre = /\[td\]([\s\S]*?)\[\/td\]/gi;
                    let c;
                    while ((c = cre.exec(r[1]))) cells.push('<td>' + c[1] + '</td>');
                    rows.push('<tr>' + cells.join('') + '</tr>');
                }
                /* 宽度口径照抄服务端实测:[table=400] → width:400px(站点自己补 px)、
                   [table=50%] 原样、[table] 无宽度 → width:98%。 */
                const ww = !w ? '98%' : (/^\d+$/.test(w) ? w + 'px' : w);
                return '<table style="width:' + ww + '">' + rows.join('') + '</table>';
            });
            html = html.replace(/\[fly\]([\s\S]*?)\[\/fly\]/gi,
                '<marquee behavior="alternate" scrollamount="3" width="100%">$1</marquee>');
            html = html.replace(/\[move\]([\s\S]*?)\[\/move\]/gi,
                '<marquee scrollamount="3">$1</marquee>');
            html = html.replace(/\[(flash|wmv|rm)=([^\]]*)\]([\s\S]*?)\[\/\1\]/gi, function (m, kind, prm, url) {
                return '<span class="spp-pv-media">▶ ' +
                    (kind.toLowerCase() === 'flash' ? '点击播放flash' : '点击播放多媒体') +
                    '(' + prm + ')' + url + '</span>';
            });
            html = html.replace(/\[hr\]/gi, '<hr class="spp-pv-hr">');
            html = html.replace(/\[sup\]([\s\S]*?)\[\/sup\]/gi, '<sup>$1</sup>');
            html = html.replace(/\[sub\]([\s\S]*?)\[\/sub\]/gi, '<sub>$1</sub>');
            html = html.replace(/\[backcolor=([#\w]+)\]([\s\S]*?)\[\/backcolor\]/gi,
                '<span style="background:$1">$2</span>');
            html = html.replace(/\[quote=([^\]]*)\]([\s\S]*?)\[\/quote\]/gi,
                '<blockquote><strong>$1:</strong>$2</blockquote>');
            html = html.replace(/\[quote\]([\s\S]*?)\[\/quote\]/gi, '<blockquote>$1</blockquote>');
            html = html.replace(/\[code\]([\s\S]*?)\[\/code\]/gi, '<pre><code>$1</code></pre>');
            html = html.replace(/\[sp\]([\s\S]*?)\[\/sp\]/gi,
                '<details><summary>展开查看</summary>$1</details>');
            html = html.replace(/\[url=([^\]]+)\]([\s\S]*?)\[\/url\]/gi,
                '<a href="$1" target="_blank" rel="noopener">$2</a>');
            html = html.replace(/\[url\]([\s\S]*?)\[\/url\]/gi,
                '<a href="$1" target="_blank" rel="noopener">$1</a>');
            html = html.replace(/\[email\]([\s\S]*?)\[\/email\]/gi,
                '<a href="mailto:$1">$1</a>');
            // ★★ 2.53.60(实时预览):这条原先把图钉成 max-24px(那是「行内小图」的口径,
            //   给切换式预览用的)—— 实时预览里那等于把用户贴的图缩成一个点,
            //   而用户贴图恰恰是最需要看预览的一类内容。改成按原尺寸、只封顶宽度:
            //   预览块自身宽度就是输入框宽度,max-width:100% 足够,不会撑破表格。
            html = html.replace(/\[img\]([\s\S]*?)\[\/img\]/gi,
                '<img src="$1" style="max-width:100%;height:auto;vertical-align:middle;border-radius:4px" alt="" loading="lazy">');
            // ★ 2.52.91:站点「隐藏内容,回复可见」真正落到 WindCode 的是 [post](实站点出来的),
            //   预览里跟 [hide] 画同一块黄条 —— 否则用新胶囊插进去的 [post] 在预览里是裸标签。
            //   ★★ 2.54.4:工具栏那枚「回复可见」已按用户要求撤掉(「网页不支持」)⇒ 预览这条
            //     规则留着只为**老帖子**与站点原生按钮留下的 [post](不再有新内容用它)。
            html = html.replace(/\[post\]([\s\S]*?)\[\/post\]/gi,
                '<div style="background:#FFF3CD;border:1px dashed #FFC107;padding:8px 12px;border-radius:4px;color:#856404;font-size:13px">🔒 回复可见内容</div>');
            html = html.replace(/\[hide\]([\s\S]*?)\[\/hide\]/gi,
                '<div style="background:#FFF3CD;border:1px dashed #FFC107;padding:8px 12px;border-radius:4px;color:#856404;font-size:13px">🔒 回复可见内容</div>');
            html = html.replace(/\[sell=(\d+)\]([\s\S]*?)\[\/sell\]/gi,
                '<div style="background:#FFF3CD;border:1px dashed #FFC107;padding:8px 12px;border-radius:4px;color:#856404;font-size:13px">💰 出售内容 ($1 SP)</div>');
            // ★★ 2.53.60 修(实时预览暴露出来的):这一行原先把 [s:id] 画成
            //   location.origin + 一条自己拼的路径 —— 实测那条路径是 404,
            //   实时预览里就是一排破图(一打字就看见,比切换式预览显眼得多)。
            //   站点自己的数据源是 face.js 给的 picpath + face[sid][0]:实测
            //   picpath = 'images/post/smile/'、face[718] = ['smallface/face077.gif', ''],
            //   站点 showFace() 与 addsmile() 都照这一套走 —— 后者只往文本框里写字面量
            //   ' [s:' + sid + '] '。⇒ 预览必须与它同源,不能自己拼路径。
            //   ★ 拿不到数据时**原样留着** [s:id] 标签:画一张破图比留个标记更糟。
            html = html.replace(/\[s:(\d+)\]/gi, function (m, sid) {
                const pp = (typeof picpath === 'string' && picpath) ? picpath : 'images/post/smile/';
                const meta = (typeof face !== 'undefined' && face && face[sid]) ? face[sid] : null;
                if (!meta || !meta[0]) return m;
                let src = pp + meta[0];
                // 相对路径按文档基址补全(站点在 /simple/ 与伪静态页会改写 <base>)
                try { src = new URL(src, document.baseURI).href; } catch (e) {}
                return '<img src="' + src + '" style="width:24px;height:24px;vertical-align:middle" alt="[s:' + sid + ']" loading="lazy">';
            });
            /* ★ 2.53.49:id 1(呵呵)在仓库里是**不带编号**的那张(image_emoticon.png)——
               带编号的 image_emoticon1.png 是 14 字节的「404: Not Found」文本。 */
            html = html.replace(/\[tb:(\d+)\]/gi, function (m, id) {
                return '<img src="https://raw.githubusercontent.com/267208/tieba-emoji/master/image_emoticon' +
                    (String(id) === '1' ? '' : id) + '.png" style="width:24px;height:24px;vertical-align:middle" alt="[tb:' + id + ']" loading="lazy">';
            });
            // 保护 <pre>/<code> 块不被 <br> 注入破坏
            const preBlocks = [];
            html = html.replace(/<(pre|code)>[\s\S]*?<\/\1>/gi, function(match) {
                preBlocks.push(match);
                return '%%PRE_' + (preBlocks.length - 1) + '%%';
            });
            html = html.replace(/\n/g, '<br>');
            html = html.replace(/%%PRE_(\d+)%%/g, function(_, i) {
                return preBlocks[parseInt(i)];
            });
            return html;
        }

        // —— 预览面板样式(内联在 textarea 同位置) ——
        GM_addStyle([
            '.spp-preview-pane {',
            '  display:none;',
            '  border:1px solid var(--spu-border,#E8E3D8);',
            '  border-radius:0 0 6px 6px;',
            '  padding:14px 16px; min-height:200px;',
            '  background:var(--spu-surface,#FDFBF7);',
            '  font-size:15px; line-height:1.85;',
            '  color:var(--spu-text,#2D2318);',
            '  word-break:break-word;',
            '  overflow-y:auto;',
            '  scrollbar-width:thin;',
            '  scrollbar-color:var(--spu-border,#ddd) transparent;',
            '}',
            '.spp-preview-pane::-webkit-scrollbar { width:5px; }',
            '.spp-preview-pane::-webkit-scrollbar-thumb {',
            '  background:var(--spu-border,#ddd); border-radius:3px;',
            '}',
            '.spp-preview-pane blockquote {',
            '  border-left:3px solid var(--spu-accent,#C75B39);',
            '  background:color-mix(in srgb,var(--spu-accent,#C75B39) 5%,transparent);',
            '  padding:10px 16px; margin:12px 0; border-radius:0 6px 6px 0;',
            '}',
            '.spp-preview-pane pre {',
            '  background:var(--spu-overlay,#F0EBE1); padding:12px 16px;',
            '  border-radius:6px; overflow-x:auto;',
            '  font-family:"SF Mono","Cascadia Code","Consolas",monospace; font-size:13px;',
            '}',
            '.spp-preview-pane ul, .spp-preview-pane ol { padding-left:24px; }',
            '.spp-preview-pane table { border-collapse:collapse; margin:10px 0; }',
            '.spp-preview-pane table td { border-top:1px solid var(--spu-border,#E8E3D8); border-left:1px solid var(--spu-border,#E8E3D8); padding:4px 10px; }',
            '.spp-preview-pane .spp-pv-bq { border-left:3px solid var(--spu-border,#E8E3D8); padding:6px 12px; margin:10px 0; color:var(--spu-muted,#9B9489); }',
            '.spp-preview-pane .spp-pv-media { display:inline-block; padding:6px 12px; margin:6px 0; border:1px dashed var(--spu-border,#E8E3D8); border-radius:6px; font-size:12px; color:var(--spu-muted,#9B9489); word-break:break-all; }',
            '.spp-preview-pane .spp-pv-hr { border:0; border-top:1px solid var(--spu-border,#E8E3D8); margin:12px 0; }',
            '.spp-preview-pane details {',
            '  border:1px solid var(--spu-border,#E8E3D8); border-radius:6px;',
            '  padding:8px 12px; margin:8px 0;',
            '}',
            '.spp-preview-pane details summary {',
            '  cursor:pointer; color:var(--spu-accent,#C75B39);',
            '  font-weight:600; font-size:13px;',
            '}',
            '.spp-preview-pane img { max-width:100%; border-radius:4px; }',
            '.spp-preview-empty {',
            '  text-align:center; color:var(--spu-muted,#9B9489);',
            '  padding:40px 20px; font-size:14px;',
            '}',
            /* ★★ 2.53.60:实时预览(默认开)—— 同一块 .spp-preview-pane 多挂一个 --live
               修饰类。为什么用修饰类而不是改基类:那枚开关关掉之后要能逐像素回到 2.53.59
               的样子,所以老规则一行不动,实时态的一切差异都写在 --live 里。
               ★ 高度上限 360px + 自己滚:实时态下输入框与预览**并存**,预览不封顶就会把
                 提交行一路推下去(长帖尤其明显)。 */
            '.spp-preview-pane--live {',
            '  display:block;',
            '  min-height:56px;',
            '  max-height:360px;',
            '  overflow-y:auto;',
            '  font-size:14px; line-height:1.8;',
            '}',
            /* 顶部那行「实时预览」用伪元素画:面板每次重渲染都会重写 innerHTML,
               写进去的元素会被冲掉,伪元素不会。 */
            '.spp-preview-pane--live::before {',
            '  content:"实时预览";',
            '  display:block; margin:0 0 8px; padding:0 0 6px;',
            '  border-bottom:1px dashed var(--spu-border,#E8E3D8);',
            '  color:var(--spu-muted,#9B9489); font-size:11px; letter-spacing:1px;',
            '}',
            /* 空内容时不要把 200px 的占位块撑出来(那会让编辑器下方凭空多一大块)。 */
            '.spp-preview-pane--live .spp-preview-empty {',
            '  padding:2px 0; text-align:left; font-size:12px;',
            '}',
        ].join('\n'));

        // —— 内联预览切换 ——
        function tryAttach(attempt) {
            if (attempt === undefined) attempt = 0;
            const textarea = document.querySelector('textarea[name="atc_content"]');
            if (!textarea) { if (attempt < 60) setTimeout(() => tryAttach(attempt + 1), 300); return; }
            if (document.getElementById('spp-preview-btn')) return;

            const submitBtn = document.querySelector('input[type="submit"][name="Submit"]');
            const container = submitBtn ? submitBtn.parentElement : textarea.parentElement;

            // ★★ 2.53.11(用户批注「美化UI和预览保持一致」):提交按钮与下面那枚 👁 预览改成同一套语言。
            //   ★ 为什么走【内联 !important】而不是只写 POST_CSS:实站量过,只写样式表时
            //     提交按钮的底色恒为灰 rgb(187,187,187) —— 站点/旧版脚本对 <input type=submit>
            //     那条规则的权重更高(且可能带背景位图)。这里就在拿到元素的地方直接钉,
            //     权重上谁都不用比(内联 !important 是最高一档),并且 background-image:none
            //     一并写掉:站点那枚按钮的红底是老式四角切图,只改底色会被位图盖住。
            //   ★ 只在引用页/编辑页做(其它页面的提交按钮归「快速发帖框美化」那套管,别抢)。
            if (submitBtn && (SEARCH.indexOf('action-quote') >= 0 || SEARCH.indexOf('action-modify') >= 0)) {
                const skin = [
                    ['height', '24px'], ['padding', '0 14px'], ['border-radius', '999px'],
                    ['border', '1px solid var(--spu-accent,#2f6f68)'],
                    ['background-color', 'var(--spu-accent,#2f6f68)'], ['background-image', 'none'],
                    ['color', '#ffffff'], ['-webkit-text-fill-color', '#ffffff'],
                    ['font-size', '12px'], ['font-weight', '600'], ['font-family', 'inherit'],
                    ['line-height', '1'], ['box-sizing', 'border-box'], ['text-shadow', 'none'],
                    ['vertical-align', 'middle'], ['cursor', 'pointer'],
                ];
                skin.forEach(function (kv) { try { submitBtn.style.setProperty(kv[0], kv[1], 'important'); } catch (_) {} });
            }

            // 创建预览面板(与 textarea 同级,初始隐藏)
            const previewPane = document.createElement('div');
            previewPane.className = 'spp-preview-pane';
            previewPane.id = 'spp-preview-pane';
            textarea.parentElement.insertBefore(previewPane, textarea.nextSibling);

            let previewActive = false;
            // ★★ 2.53.60 实时态的三件运行时状态:
            //     · composing  —— 中文输入法组字期间 input 会连续触发,此时**不渲染**
            //                     (否则每敲一个拼音字母就重建一次预览,滚动与光标都在抖);
            //     · renderedSrc —— 上一次真正渲染过的原文(已 trim),兜底轮询靠它判「值变没变」;
            //     · timer      —— debounce 句柄。
            let composing = false;
            let renderedSrc = null;
            let timer = 0;

            // 渲染一次(幂等:原文没变就直接返回)。
            //   ★ 每次重渲染都要换掉 innerHTML ⇒ 必须自己把滚动位置搬过去,
            //     否则长文预览每敲一个字就被顶回顶部。
            function paint() {
                const raw = textarea.value.trim();
                if (raw === renderedSrc) return;
                renderedSrc = raw;
                const keepTop = previewPane.scrollTop;
                previewPane.innerHTML = raw
                    ? bbcodeToHTML(raw)
                    : '<div class="spp-preview-empty">' + (LIVE ? '预览会随输入实时更新' : '没有内容可预览') + '</div>';
                previewPane.scrollTop = keepTop;
            }

            // debounce:站点允许 1000000 字符 —— 5 万以内 200ms(跟手),
            //   超过走 800ms(仍算实时,只是慢一拍;几十万字每 200ms 跑一遍二十多条正则不划算)。
            function schedulePaint() {
                if (!LIVE || !previewActive) return;
                clearTimeout(timer);
                timer = setTimeout(function () {
                    if (!composing) paint();
                }, textarea.value.length > 50000 ? 800 : 200);
            }

            if (LIVE) {
                previewPane.classList.add('spp-preview-pane--live');
                textarea.addEventListener('input', schedulePaint);
                textarea.addEventListener('compositionstart', function () { composing = true; });
                textarea.addEventListener('compositionend', function () { composing = false; schedulePaint(); });
                textarea.addEventListener('paste', schedulePaint);
                // ★ 兜底轮询:模块 13 的胶囊按钮、表情面板、站点原生工具条都是**直接改
                //   textarea.value**(不派发 input 事件)⇒ 只挂事件会漏掉它们,插进去的标签
                //   要等下一次手敲才会出现在预览里。600ms 比一次字符串,代价可忽略。
                //   ★ 同一处顺带处理「站点自己把输入框藏起来」(post.php 切到「所见即所得」
                //     模式):预览跟着收起来 —— 那块内容渲染的是 WindCode 文本框,
                //     而站点此时给的是编辑器 iframe,留着就是错的。文本框回来预览也回来,
                //     previewActive(用户的意图)不动。
                setInterval(function () {
                    if (document.hidden) return;
                    if (textarea.offsetParent === null) { previewPane.style.display = 'none'; return; }
                    if (previewActive && previewPane.style.display === 'none') previewPane.style.display = 'block';
                    if (composing || !previewActive) return;
                    if (textarea.value.trim() !== renderedSrc) paint();
                }, 600);
            }

            // 创建切换按钮
            const btn = document.createElement('button');
            btn.id = 'spp-preview-btn';
            btn.type = 'button';
            btn.textContent = '👁 预览';
            btn.title = 'BBCode 所见即所得预览 (Ctrl+Shift+P) — 内联切换';
            // ★ 2.52.91「重复按钮」:站点自己那枚 `a.btn3[onclick=gopreview()]` 由 POST_CSS
            //   收掉,本按钮就是提交行里唯一那枚预览 ⇒ 尺寸向 `input[type=submit]` 靠
            //   (原来 7px/16px + 13px 字比「提 交」高出一大截,看着像外来的)。
            btn.style.cssText = [
                'margin-left:10px; padding:0 14px; height:24px; vertical-align:middle;',
                'border:1px solid var(--spu-accent,#2f6f68);',
                'border-radius:5px; background:var(--spu-surface,#fff);',
                'color:var(--spu-accent,#2f6f68); font-size:12px; font-weight:600;',
                'cursor:pointer; transition:all 0.2s ease; font-family:inherit;',
            ].join('');

            btn.addEventListener('mouseenter', function() {
                if (!previewActive) {
                    btn.style.background = 'var(--spu-accent,#C75B39)';
                    btn.style.color = '#fff';
                }
            });
            btn.addEventListener('mouseleave', function() {
                if (!previewActive) {
                    btn.style.background = 'var(--spu-surface,#fff)';
                    btn.style.color = 'var(--spu-accent,#C75B39)';
                }
            });

            // ★★ 2.53.60:把「切换」细化成「设置可见性」,两种形态共用同一个出口 ——
            //   · LIVE(默认):输入框与预览**并存**,切换只是把预览块收起来 / 放出来;
            //   · 老路径(关掉开关):预览时把输入框整块换成预览块 —— 2.53.59 的行为,一行不差。
            function setPaneVisible(on) {
                previewActive = !!on;
                if (previewActive) {
                    if (LIVE) {
                        previewPane.style.display = 'block';
                        paint();
                    } else {
                        // 老路径:先量高度(此时输入框还看得见),再把它换成预览块
                        previewPane.style.minHeight = textarea.offsetHeight + 'px';
                        textarea.style.display = 'none';
                        previewPane.style.display = 'block';
                        renderedSrc = null;   // 老路径每次进来都要重渲染(内容可能已变)
                        paint();
                    }
                    btn.textContent = LIVE ? '✕ 收起预览' : '✏ 编辑';
                    btn.style.background = 'var(--spu-accent,#C75B39)';
                    btn.style.color = '#fff';
                    btn.style.borderColor = 'var(--spu-accent,#C75B39)';
                } else {
                    if (!LIVE) {
                        textarea.style.display = '';
                        textarea.focus();
                    }
                    previewPane.style.display = 'none';
                    btn.textContent = '👁 预览';
                    btn.style.background = 'var(--spu-surface,#fff)';
                    btn.style.color = 'var(--spu-accent,#C75B39)';
                    btn.style.borderColor = 'var(--spu-accent,#C75B39)';
                }
                // 通知表情面板重算高度(预览切换导致 textarea offsetHeight 变化)
                window.dispatchEvent(new CustomEvent('spp-preview-toggle'));
            }

            function togglePreview() { setPaneVisible(!previewActive); }

            btn.addEventListener('click', function(e) {
                e.preventDefault();
                togglePreview();
            });

            if (submitBtn) {
                submitBtn.insertAdjacentElement('afterend', btn);
            } else {
                container.appendChild(btn);
            }

            // Ctrl+Shift+P 切换预览
            document.addEventListener('keydown', function(e) {
                if (e.ctrlKey && e.shiftKey && (e.key === 'P' || e.key === 'p')) {
                    const ta = document.querySelector('textarea[name="atc_content"]');
                    if (ta === document.activeElement || previewActive) {
                        e.preventDefault();
                        togglePreview();
                    }
                }
            });

            // ★★ 2.53.60:实时态的起步 —— 挂完就摆出预览(初始为空时是一行细提示)。
            //   ★ 老路径在这里**什么都不做**,一切照 2.53.59:等用户点那枚按钮。
            if (LIVE) {
                textarea.style.display = '';
                setPaneVisible(true);
            }
        }

        onDomReady(() => setTimeout(tryAttach, 500));
    }

    // ---- 悬浮提示卡(模块 12 / 13 / 14 公用,无独立模块号)----
    // ★★ 2.54.6(用户:「当鼠标悬停在输入框上的按钮 UI 上时,会出现文本。我希望这文本也能
    //   用一个 UI 框包裹起来,进行美化」):原来每枚按钮挂的都是原生 title —— 浏览器弹的是
    //   系统灰框,字号 / 配色 / 圆角全跟本站主题无关,长标题(「表」「媒 ▾」那两条上百字)
    //   还会被拉成一条长带子。这里整套换成自己的卡片:
    //     · SPP_TIP.attach(el, 文字):摘掉 title、改挂 data-spp-tip;
    //     · 鼠标停 180ms 后把卡片摆到元素正下方,放不下就翻到上方,左右夹在视口内;
    //     · 卡片是 pointer-events:none 的 fixed 层(不吃点击),滚动 / 点下去就收掉;
    //     · CSS 只注入一次(靠 id 认门),多个模块共用同一份。
    const SPP_TIP = (function () {
        const STYLE_ID = 'spp-tip-style';
        const BOX_ID = 'spp-tip-box';
        let box = null;
        let timer = null;
        function style() {
            if (document.getElementById(STYLE_ID)) return;
            GM_addStyle(`
                #spp-tip-box {
                    position: fixed; z-index: 2147483000; display: none;
                    max-width: 340px; padding: 8px 12px; border-radius: 10px;
                    background: rgba(26,30,40,0.97); color: #F5F2EC;
                    font-family: 'Noto Sans SC','Source Han Sans SC','PingFang SC','Microsoft YaHei',sans-serif;
                    font-size: 12px; line-height: 1.6; letter-spacing: 0.01em; text-align: left;
                    white-space: normal; word-break: break-word;
                    border: 1px solid rgba(255,255,255,0.10);
                    box-shadow: 0 10px 28px rgba(0,0,0,0.30);
                    pointer-events: none;
                }
                #spp-tip-box.spp-tip-below::before {
                    content: ''; position: absolute; top: -5px; left: 50%; margin-left: -5px;
                    width: 10px; height: 10px; background: inherit; transform: rotate(45deg);
                    border-left: 1px solid rgba(255,255,255,0.10);
                    border-top: 1px solid rgba(255,255,255,0.10);
                }
                #spp-tip-box.spp-tip-above::after {
                    content: ''; position: absolute; bottom: -5px; left: 50%; margin-left: -5px;
                    width: 10px; height: 10px; background: inherit; transform: rotate(45deg);
                    border-right: 1px solid rgba(255,255,255,0.10);
                    border-bottom: 1px solid rgba(255,255,255,0.10);
                }
                /* ★★ 2.55.14(用户:「弹出的文本框需要进行 UI 美化」):表情 / 贴纸格专用的那一枚
                   —— 一张紧凑的胶囊卡。与按钮提示卡共用 #spp-tip-box,只在尺度与材质上收一收:
                   卡片就挨着格子、只写一行名字,用 340px 宽的黑框去装会显得笨重。
                   ★ 用渐变 + 亮一档的描边代替纯色块,是为了在浅色帖子底与深色皮肤(本站皮肤
                     可换)上都能立住;箭头仍由上面那两条 ::before / ::after 接管。 */
                #spp-tip-box.spp-tip-emoji {
                    max-width: 260px; padding: 5px 11px; border-radius: 999px;
                    font-size: 12px; font-weight: 600; letter-spacing: 0.02em;
                    background: linear-gradient(180deg, rgba(40,47,62,0.98), rgba(23,27,37,0.98));
                    border-color: rgba(255,255,255,0.16);
                    box-shadow: 0 6px 18px rgba(0,0,0,0.34);
                }
                #spp-tip-box.spp-tip-emoji.spp-tip-below::before,
                #spp-tip-box.spp-tip-emoji.spp-tip-above::after { background: rgba(23,27,37,0.98); }
            `);
        }
        function node() {
            if (box && box.isConnected) return box;
            box = document.getElementById(BOX_ID);
            if (!box) {
                box = document.createElement('div');
                box.id = BOX_ID;
                document.body.appendChild(box);
            }
            return box;
        }
        function hide() {
            if (box) box.style.display = 'none';
        }
        function place(el, variant) {
            const text = el.getAttribute('data-spp-tip');
            if (!text) return;
            const n = node();
            n.className = '';
            n.textContent = text;
            n.style.visibility = 'hidden';
            n.style.display = 'block';
            n.style.left = '0px';
            n.style.top = '0px';
            const r = el.getBoundingClientRect();
            const w = n.offsetWidth;
            const h = n.offsetHeight;
            const left = Math.max(10, Math.min(r.left + r.width / 2 - w / 2, window.innerWidth - w - 10));
            const below = (r.bottom + 10 + h) <= window.innerHeight - 8;
            n.className = (variant ? variant + ' ' : '') + (below ? 'spp-tip-below' : 'spp-tip-above');
            n.style.left = Math.round(left) + 'px';
            n.style.top = Math.round(below ? r.bottom + 10 : Math.max(8, r.top - h - 10)) + 'px';
            n.style.visibility = 'visible';
        }
        function attach(el, text) {
            if (!el || !text) return el;
            style();
            el.setAttribute('data-spp-tip', text);
            el.removeAttribute('title');
            el.addEventListener('mouseenter', function () {
                clearTimeout(timer);
                timer = setTimeout(function () { place(el); }, 180);
            });
            el.addEventListener('mouseleave', function () { clearTimeout(timer); hide(); });
            el.addEventListener('mousedown', function () { clearTimeout(timer); hide(); });
            el.addEventListener('blur', hide);
            return el;
        }
        // 滚动 / 改窗口就收掉 —— 卡片是 fixed 定位,不跟着元素走,留着会比错位的 tooltip 更碍事
        if (!window.__SPP_TIP_BOUND__) {
            window.__SPP_TIP_BOUND__ = true;
            window.addEventListener('scroll', hide, true);
            window.addEventListener('resize', hide, true);
        }
        /* ★★ 2.55.14(用户:「当鼠标悬停在 emoji 或贴纸上时,弹出的文本框需要进行 UI 美化」):
           表情网格里几百个格子、每次切分类都整片重建 —— 给每个格子挂一对 mouseenter /
           mouseleave 既浪费又容易漏(动图静态化那一步会把 img 换成 canvas)。
           这里加一套**委托**版本:一对 mouseover / mouseout 挂在容器上,靠
           closest('[data-spp-tip]') 找当前格子。
           ★ mouseenter / mouseleave 不冒泡,委托只能用 mouseover / mouseout;
             鼠标在同一个格子里挪动时不重摆(relatedTarget 还在格子内就放过)。
           ★ 只挂一次:root.dataset 上那枚标记挡住第二遍(网格重建不重挂,格子只是普通节点)。 */
        function delegate(root, variant) {
            if (!root || root.dataset.sppTipBound) return;
            root.dataset.sppTipBound = '1';
            style();
            root.addEventListener('mouseover', function (e) {
                const el = e.target && e.target.closest ? e.target.closest('[data-spp-tip]') : null;
                if (!el || el === e.relatedTarget || el.contains(e.relatedTarget)) return;
                clearTimeout(timer);
                timer = setTimeout(function () { place(el, variant); }, 180);
            });
            root.addEventListener('mouseout', function (e) {
                const el = e.target && e.target.closest ? e.target.closest('[data-spp-tip]') : null;
                if (!el || el === e.relatedTarget || el.contains(e.relatedTarget)) return;
                clearTimeout(timer);
                hide();
            });
        }
        return { attach: attach, delegate: delegate, hide: hide };
    })();

    // ==================== 13. 编辑器 BBCode 快捷工具栏(胶囊按钮 + flex 布局) ====================
    function initEditorToolbar() {
        const KEY = 'spp_editor_toolbar';
        let enabled = SPP_CONFIG.bool(KEY, true);   // ★ 2.52.93:走集中读数层

        if (!enabled) return;

        // 仅在有编辑器的页面生效(read.php 有快速回复框,post.php 有完整编辑器);纯列表页跳过
        if (!WANTS.editorUi) return;
        // ★★ 2.53.96:图床这件事拆成两半,这行只读「自动」那一半的开关 ——
        //   粘贴进输入框后立刻把还没标注的图床链接补上标符。关掉之后「链接与媒体」组里的
        //   『标』按钮照旧可用(手动补一遍),下拉那一半与本开关无关(它只开页面、不改文字)。
        //   ★★ 2.53.96 的默认值是开,2.55.13 翻成关(用户:「链接自动标注 这个功能默认是关闭的」)。
        //     老用户存储里那条 true 由 migrateSettings() 里的一次性迁移翻掉 ——
        //     只改这里的默认值对它们无效(保存循环把每一行都写一遍)。
        //   ★ 默认值 false 必须与 SPP_PANEL_DEFS 那一行、buildOverlay 那个 checkbox 同值
        //     (tools/verify/settings-panel.js 的 EXPECT 拿这几处对着核)。
        const MARK_ON = SPP_CONFIG.bool('spp_host_mark', false);
        /* ★★ 2.53.99:图片链接包不包 [img](用户:「正确的标注手段是:[img]…[/img]」)。
           ★★ 2.55.13(用户:「img 这个功能默认打开,禁止用户关闭」):读数改成常量 ——
             这一枚不再是开关,面板上也没有它那一行,永远包。
             ★ 键 spp_self_img 留在存储里当孤儿键,不再有人读它(理由见登记表那一侧)。 */
        const SELF_IMG = true;
        // ── 预设色板 ──
        const COLOR_PRESETS = [
            '#FF0000','#FF4500','#FF6600','#FF8C00','#FFA500',
            '#FFD700','#FFFF00','#ADFF2F','#00FF00','#00FA9A',
            '#00CED1','#00BFFF','#1E90FF','#0000FF','#8A2BE2',
            '#9400D3','#FF00FF','#FF1493','#DC143C','#FF69B4',
            'red','blue','green','orange','purple','brown',
            'teal','navy','maroon','gray','darkred','darkblue',
            '#FFFFFF','#F5F5F5','#D3D3D3','#A9A9A9','#808080','#696969','#2F4F4F','#000000',
        ];

        // ── 图床表(2.53.96)──
        //   name = 标符里写的那几个字(用户给的六个;拉丁名首字母大写);
        //   site = 「图床 ▾」下拉里点一下要跳去的首页;
        //   hosts = 认链用的域名。只写主域名、靠后缀匹配认子域 —— 实际图链大多在子域上
        //           (i.urusai.cc / files.catbox.moe / up.inari.site),写成完整域名会漏一半。
        const IMG_HOSTS_BUILTIN = [
            { name: '厘图片', site: 'https://tupian.li/', hosts: ['tupian.li'] },
            { name: '猫箱', site: 'https://catbox.moe/', hosts: ['catbox.moe'] },
            { name: 'Inari', site: 'https://up.inari.site/', hosts: ['inari.site'] },
            { name: 'Ipfs', site: 'https://beta.fileverse.io/', hosts: ['fileverse.io'] },
            { name: 'Filegarden', site: 'https://filegarden.com/', hosts: ['filegarden.com'] },
            { name: 'Urusai', site: 'https://urusai.cc/', hosts: ['urusai.cc'] },
        ];
        /* ★★ 2.55.14(用户:「在设置面板中增加『图床跳转』功能,允许用户自定义增加更多
           『图床』UI 选项」):把面板「自定义图床(粘贴区)」里那几条接进来。
           ★ 两张顺序是**故意不一样**的:
             · IMG_HOSTS(下拉 / 按钮文案)—— 自带的六个在前,用户加的排后面;
             · IMG_HOSTS_LOOKUP(认链)—— 用户自己写的排**前面**。
           理由:imgHostOf 是「取第一个命中的」,自带那条先命中会把用户特意写的子域规则
           截胡(比如他自己写一条 i.inari.site,自带 inari.site 会先吃掉)。 */
        const IMG_HOSTS = IMG_HOSTS_BUILTIN.concat(SPP_CUSTOM_HOSTS.list);
        const IMG_HOSTS_LOOKUP = SPP_CUSTOM_HOSTS.list.concat(IMG_HOSTS_BUILTIN);

        // ── 按钮定义(按分组) ──
        // type: 'tag'   = 包裹选中文字,无选中则写空标签
        //        'block' = 块级模板(列表等)
        //        'link'  = 链接专用(光标定位到 URL 占位符)
        //        'color' = 颜色弹窗
        // cursor: 光标在 openTag 中的偏移位置,选中该位置占位符
        const BUTTON_GROUPS = [
            {
                label: '基础文本格式',
                buttons: [
                    { label: 'B',  title: '粗体 [b]文字[/b]',              tag: 'b' },
                    { label: 'I',  title: '斜体 [i]文字[/i]',              tag: 'i' },
                    { label: 'U',  title: '下划线 [u]文字[/u]',            tag: 'u' },
                    { label: 'S',  title: '删除线 [s]文字[/s]',            tag: 's' },
                    // ★ 2.52.91:黑框「改为:字色、背景色、字号、字体、水平线」——
                    //   单字标签(色/背/字/体/—)在胶囊里认不出来,换成全称。
                    //   标题里的 windcode 写法一字不动(那才是按钮实际插入的东西)。
                    { label: '字色', title: '字体颜色 (色板选色)',           tag: 'color', type: 'color' },
                    { label: '背景色', title: '背景颜色 (色板选色)',           tag: 'backcolor', type: 'backcolor' },
                    { label: '字号', title: '字号 [size=4][/size](默认 4 号;点完光标就在标签里,直接打字即可 —— 要改号数把光标移到那个 4 上,1–6 有效)', tag: 'size', param: '4', cursor: 6, body: true },
                    { label: '字体', title: '字体选择 (选项表)',             tag: 'font', type: 'font' },
                    { label: '水平线', title: '插入水平线 [hr]',                tag: 'hr', type: 'self' },
                    { label: '上标', title: '上标 [sup][/sup]',                     tag: 'sup' },
                    { label: '下标', title: '下标 [sub][/sub]',                     tag: 'sub' },
                ]
            },
            {
                label: '段落与布局',
                buttons: [
                    { label: '左',   title: '居左 [align=left][/align]',            tag: 'align=left' },
                    { label: '中',   title: '居中 [align=center][/align]',          tag: 'align=center' },
                    { label: '右',   title: '居右 [align=right][/align]',           tag: 'align=right' },
                    /* ★★ 2.54.8(用户:「移除左右平等按钮」):撤掉「平」——
                       [align=justify](两端对齐)这一枚。居左 / 居中 / 居右三枚照旧;
                       预览里 [align=justify] 的规则留着 —— 老帖子与站点原生按钮还在用它。 */
                    /* ★★ 2.54.0:两枚列表按钮原来都是坏的 —— 实测(POST job.php?action=preview)
                       服务端把 [*] 与 [#1] 当**字面文字**吐出来(<ul>[*]甲[*]乙</ul>);真正认的
                       列表项是 [li]…[/li],有序列表也用它,只把外层换成数字型 [list=1]。
                       「引@」([quote=某人])同样实测是字面文字 ⇒ 撤掉,位置让给「块引」;
                       「折」([sp])也是死的 ⇒ 一起撤。 */
                    /* ★★ 2.54.1:两枚列表按钮搬进独立的「列表」组(用户要「空行 / 紧排」四种),
                       这里只留一句位置提示。 */
                    { label: '表',   title: '插入表格 —— 右边三个框依次是「行 × 列 宽」:宽填 400 就是 400px、填 50% 就是 50%、留空则用站点默认(98%);格子里的占位按列字母+行号铺(A1、B1、A2…)', tag: 'table', type: 'table-ui' },
                    { label: '引文', title: '引用 [quote][/quote](站点那枚带「引用」标题的灰框)', tag: 'quote' },
                    { label: '块引', title: '块引用 [blockquote][/blockquote](不带标题的朴素缩进块 —— 站点原生编辑器里那枚「增加缩进」就是它)', tag: 'blockquote' },
                    { label: '码',   title: '代码块 [code][/code]',                  tag: 'code' },
                    { label: '飞',   title: '跑马灯·来回 [fly][/fly](服务端吐 marquee behavior=alternate,宽度撑满)', tag: 'fly' },
                    { label: '移',   title: '跑马灯·单向右滚 [move][/move](服务端吐 marquee,没有 alternate)', tag: 'move' },
                ]
            },
            {
                label: '列表',
                buttons: [
                    /* ★★ 2.54.1(用户:「增加一个是否空行的选项」):把列表拆成两件事 ——
                       「整段」= 起一个列表(占位**只放一条** [li]条目[/li],原来那三条撤了),
                       「条目」= 往列表里补一条。空行 / 紧排 的差别只有换行:
                       服务端把 \n 翻成 <br>,换行落在两个 <li> 之间就是空行(实测 a),
                       全挤一行则一个 <br> 都没有、条目紧挨着(实测 c)。 */
                    /* ★★ 2.54.5(用户:「无序列表:空行列表、空行条目、紧排列表、紧排条目/
                       有序列表:同」):当时按**空行 / 紧排**成对排成两枚一行 ——
                         第一行 = 空行列表、空行条目;第二行 = 紧排列表、紧排条目;
                       无序、有序各两行。★ 2.54.7 已改成一类列表一行(见下一条注释),
                       这里只留档说明这套标记是怎么来的。断行靠标记(渲染循环里处理):
                         · { sub: 文字 }  行内小标签(不换行)
                         · { brk: 文字 }  换行;文字非空就再放一枚小标签(空串 = 只换行)
                         · { brk: 文字, relabel: true }  换行 + 与组标签等宽的隐形占位 */
                    /* ★★ 2.54.7(用户:「列表的排序结构即为你提供的文档结构 —— 有序列表:
                       空行列表、空行条目、紧排列表、紧排条目;无序列表:同上」):
                       上一版把「空行 / 紧排」拆成两行(空行两枚一行、紧排两枚一行),续行又
                       没有左端对齐 ⇒ 四行参差、还容易被读成两组。现在按文档结构走:
                       **一类列表一行**,行内顺序 = 空行列表 → 空行条目 → 紧排列表 → 紧排条目,
                       有序在前、无序在后(与段头那张表逐字一致)。
                       续行用 { brk: 文字, relabel: true } 补一枚与组标签同文本同样式的隐形
                       占位 ⇒ 「无序列表」正对「有序列表」,不用量宽度、不写魔数。 */
                    { sub: '有序列表' },
                    { label: '1. 空行列表', title: '有序列表(条目之间空一行):[list=1] + 每行一条 [li]条目[/li]', tag: 'list', type: 'list', list: 'ol', loose: true },
                    { label: '1. 空行条目', title: '加一条并换行:[li]条目[/li] + 换行 —— 接着写的下一条就和它分开(条目之间空一行)', tag: 'list', type: 'item', list: 'ol', loose: true },
                    { label: '1. 紧排列表', title: '有序列表(条目紧挨着、一个空行都没有):[list=1][li]条目[/li][/list] 一行到底', tag: 'list', type: 'list', list: 'ol', loose: false },
                    { label: '1. 紧排条目', title: '加一条但不换行:[li]条目[/li] —— 光标停在它后面,再点一次就是紧挨着的一条', tag: 'list', type: 'item', list: 'ol', loose: false },
                    { brk: '无序列表', relabel: true },
                    { label: '• 空行列表', title: '无序列表(条目之间空一行):[list] + 每行一条 [li]条目[/li]', tag: 'list', type: 'list', list: 'ul', loose: true },
                    { label: '• 空行条目', title: '加一条并换行:[li]条目[/li] + 换行(无序列表里同样用它)', tag: 'list', type: 'item', list: 'ul', loose: true },
                    { label: '• 紧排列表', title: '无序列表(条目紧挨着、一个空行都没有):[list][li]条目[/li][/list] 一行到底', tag: 'list', type: 'list', list: 'ul', loose: false },
                    { label: '• 紧排条目', title: '加一条但不换行:[li]条目[/li](无序列表里同样用它)', tag: 'list', type: 'item', list: 'ul', loose: false },
                ]
            },

            {
                label: '链接与媒体',
                buttons: [
                    { label: '链',   title: '链接 [url]https://...[/url]',               tag: 'url', type: 'link' },
                    { label: '链文', title: '带文字链接 [url=https://...]文字[/url]',     tag: 'url', type: 'link-text' },
                    { label: '邮',   title: '邮箱 [email][/email]',                       tag: 'email' },
                    { label: '图',   title: '插入图片 [img][/img]',                       tag: 'img' },
                    { label: '媒 ▾', title: '插入多媒体 —— Flash / 视频(wmv) / 音频(mp3) / 音乐(rm) 四种,长宽与自动播放自己填。站点认的四种写法:[flash=长,宽,自动] / [wmv=长,宽,自动] / [wmv=自动] / [rm=长,宽,自动];[media] 是死的(服务端原样吐字面文字)', tag: 'media', type: 'media' },
                    // ★★ 2.53.96(用户:「对于输入框,我希望在该板块增加两个功能」):
                    //   ① 「标」= 给输入框里还没标注过的图床链接补上【图床名】标符(手动补一遍;
                    //      自动那一半是粘贴钩子,开关是设置面板的「图床链接自动标注」)。
                    //   ② 「图床 ▾」= 图床快捷跳转下拉(点一项即在新标签页打开该图床首页)。
                    { label: '标', title: '自动为所有超链接添加图床标签', tag: 'hostmark', type: 'annotate' },
                    /* ★ 2.55.14:括号里那串名字改成从 IMG_HOSTS 现算 —— 写死的话用户加了
                       自定义图床,提示卡里还只报那六个老名字(自相矛盾)。 */
                    { label: '图床 ▾', title: '图床快捷跳转 —— 点开选一个图床,点一下即在新标签页打开(' +
                        IMG_HOSTS.map(function (h) { return h.name; }).join(' / ') + ')', tag: 'hosts', type: 'hosts' },
                ]
            },
        ];

        // ── 当前活动的弹窗(颜色 / 背景色 / 字体) ──
        let activePopover = null;
        /* ★★ 2.54.0(活体实测补的):弹窗的「点外面就收掉」原来是一条 { once: true } 的一次性
           document 监听,可是弹窗里每一行都 stopPropagation(不挡的话点行内就先把自己关了),
           于是那条监听**从来没被消耗**、一直挂在 document 上 —— 下一次再点同一枚按钮开弹窗时,
           按钮的 click 冒泡到 document,刚开出来的弹窗当场被它收掉。真机实测:字体 / 色板 / 媒 ▾
           全是「第一次能开、用过一次之后就再也开不出来」。
           ⇒ 改成登记式:监听器记在 popoverDocCloser 上,closePopover() 负责连它一起摘。 */
        let popoverDocCloser = null;

        function closePopover() {
            if (popoverDocCloser) {
                document.removeEventListener('click', popoverDocCloser);
                popoverDocCloser = null;
            }
            if (activePopover) { activePopover.remove(); activePopover = null; }
        }

        // 给刚挂上来的弹窗装「点外面收掉」:先清旧的、再登记新的(setTimeout 让本次 click 先走完)
        // ★ 再兜一层「点在弹窗自己身上不算点外面」—— 哪一行忘了 stopPropagation 也不会自关。
        function armPopoverClose() {
            if (popoverDocCloser) {
                document.removeEventListener('click', popoverDocCloser);
                popoverDocCloser = null;
            }
            popoverDocCloser = function (ev) {
                if (activePopover && activePopover.contains(ev.target)) return;
                closePopover();
            };
            setTimeout(function () { document.addEventListener('click', popoverDocCloser); }, 0);
        }

        // ── 选中段首尾的空白搬到标签外面(2.54.1)──
        //   用户报:整行选中一个 URL(行尾 \n 也选进去了)再点「图」⇒ [img]url\n[/img],
        //   应该是 [img]url[/img]\n。**不只是难看**:站点那边 [img] 地址末尾带换行就成断链。
        //   ★ 全空白的选择退化成「整段当 core」—— 否则 lead 与 trail 重叠,空白被复制成两份。
        //   ★ 只搬最外沿:多行选中中间那些换行一个字不动。
        function splitSelEdge(raw) {
            let a = 0, b = raw.length;
            while (a < b && /\s/.test(raw.charAt(a))) a++;
            while (b > a && /\s/.test(raw.charAt(b - 1))) b--;
            if (a >= b) return { lead: '', core: raw, trail: '' };
            return { lead: raw.slice(0, a), core: raw.slice(a, b), trail: raw.slice(b) };
        }

        // ── 包裹选中文字,无选中时写空标签 ──
        function insertTag(textarea, tag, param, cursorInOpen, bodyCaret) {
            const s = textarea.selectionStart;
            const e = textarea.selectionEnd;
            const parts = splitSelEdge(textarea.value.substring(s, e));
            const sel = parts.core;
            const openTag = param != null ? '[' + tag + '=' + param + ']' : '[' + tag + ']';
            const closeTag = '[/' + tag.replace(/=.*$/, '') + ']';
            textarea.value = textarea.value.substring(0, s) + parts.lead + openTag + (sel || '') + closeTag + parts.trail + textarea.value.substring(e);

            if (cursorInOpen != null && !bodyCaret) {
                /* 参数**必须填**的(「售」那种):光标选中参数,直接打字就是改数值。 */
                const start = s + parts.lead.length + cursorInOpen;
                const end = start + String(param).length;
                textarea.setSelectionRange(start, end);
            } else if (sel) {
                const at = s + parts.lead.length + openTag.length;
                textarea.setSelectionRange(at, at + sel.length);
            } else {
                /* ★★ 2.54.3:「字号」这类**参数可选**的(按钮上带 body:true)把光标放进
                   标签体内([size=4]|[/size]),与 B/I/U 那批「无选中就 [标签]|[/标签]」
                   完全一致 —— 点完直接打字。
                   用户报的正是这里:原来光标停在参数里,打完号数还在 ] 前面,
                   接着打的字全进了标签 ⇒ AAA[size=5BBB][/size],预览与服务端都当字面文字。 */
                const at = s + parts.lead.length + openTag.length;
                textarea.setSelectionRange(at, at);
            }
            textarea.focus();
        }

        // ── 插入自闭合标签(如 [hr]) ──
        function insertSelfTag(textarea, tag) {
            const s = textarea.selectionStart;
            const e = textarea.selectionEnd;
            const tagStr = '[' + tag + ']';
            textarea.value = textarea.value.substring(0, s) + tagStr + textarea.value.substring(e);
            textarea.setSelectionRange(s + tagStr.length, s + tagStr.length);
            textarea.focus();
        }

        // ── 构建颜色/背景色弹窗 ──
        function buildColorPopover(btn, textarea, tag) {
            closePopover();
            const popover = document.createElement('div');
            popover.className = 'spp-color-popover';
            popover.style.cssText = [
                'position:absolute; z-index:100003;',
                'background:var(--spu-surface,#fff);',
                'border:1px solid var(--spu-border,#e0e0e0);',
                'border-radius:8px; padding:10px;',
                'box-shadow:0 8px 32px rgba(0,0,0,0.18);',
                'display:grid; grid-template-columns:repeat(10,26px); gap:4px;',
                'max-width:320px;',
            ].join('');

            COLOR_PRESETS.forEach(hex => {
                const swatch = document.createElement('div');
                SPP_TIP.attach(swatch, hex);
                swatch.style.cssText = [
                    'width:26px; height:26px; border-radius:4px; cursor:pointer;',
                    'border:2px solid transparent; transition:transform 0.1s,border-color 0.1s;',
                    'background:' + hex + ';',
                    hex === '#FFFFFF' ? 'border:1px solid #ccc;' : '',
                ].join('');
                swatch.addEventListener('mouseenter', () => { swatch.style.transform = 'scale(1.18)'; swatch.style.borderColor = 'var(--spu-accent,#C75B39)'; });
                swatch.addEventListener('mouseleave', () => { swatch.style.transform = 'scale(1)'; swatch.style.borderColor = 'transparent'; });
                swatch.addEventListener('click', e => { e.stopPropagation(); insertTag(textarea, tag, hex); closePopover(); });
                popover.appendChild(swatch);
            });

            const sep = document.createElement('div');
            sep.style.cssText = 'grid-column:1/-1; height:1px; background:var(--spu-border,#eee); margin:4px 0;';
            popover.appendChild(sep);
            const pickerRow = document.createElement('div');
            pickerRow.style.cssText = 'grid-column:1/-1; display:flex; align-items:center; gap:6px;';
            const picker = document.createElement('input');
            picker.type = 'color'; picker.value = '#FF6600';
            picker.style.cssText = 'width:28px; height:24px; border:none; cursor:pointer; padding:0; background:none;';
            const pickerLabel = document.createElement('span');
            const labelText = tag === 'backcolor' ? '取色器 (自定义背景色)' : '取色器 (自定义颜色)';
            pickerLabel.textContent = labelText;
            pickerLabel.style.cssText = 'font-size:11px; color:var(--spu-muted,#999); cursor:pointer;';
            pickerRow.appendChild(picker); pickerRow.appendChild(pickerLabel);
            pickerRow.addEventListener('click', () => picker.click());
            picker.addEventListener('input', () => { insertTag(textarea, tag, picker.value); closePopover(); });
            picker.addEventListener('click', e => e.stopPropagation());
            popover.appendChild(pickerRow);

            armPopoverClose();
            activePopover = popover;
            return popover;
        }

        // ── 构建字体选择弹窗 ──
        const FONT_OPTIONS = [
            '宋体', '仿宋', '黑体', '楷体', '微软雅黑',
            'Arial', 'Verdana', 'Tahoma', 'Times New Roman', 'Georgia',
        ];
        function buildFontPopover(btn, textarea) {
            closePopover();
            const popover = document.createElement('div');
            popover.className = 'spp-font-popover';
            popover.style.cssText = [
                'position:absolute; z-index:100003;',
                'background:var(--spu-surface,#fff);',
                'border:1px solid var(--spu-border,#e0e0e0);',
                'border-radius:8px; padding:6px;',
                'box-shadow:0 8px 32px rgba(0,0,0,0.18);',
                'display:flex; flex-direction:column; gap:2px;',
                'min-width:140px;',
            ].join('');

            FONT_OPTIONS.forEach(name => {
                const item = document.createElement('div');
                item.textContent = name;
                item.style.cssText = [
                    'padding:6px 10px; font-size:13px; cursor:pointer; border-radius:4px;',
                    'font-family:' + name + ';',
                    'transition:background 0.12s;',
                ].join('');
                item.addEventListener('mouseenter', () => { item.style.background = 'var(--spu-hover,#f0ebe1)'; });
                item.addEventListener('mouseleave', () => { item.style.background = 'transparent'; });
                item.addEventListener('click', e => {
                    e.stopPropagation();
                    insertTag(textarea, 'font', name);
                    closePopover();
                });
                popover.appendChild(item);
            });

            armPopoverClose();
            activePopover = popover;
            return popover;
        }

        // ── 链接自动标注(2.53.96 起;2.53.97 起**不再限于六个图床**;2.53.98 起**没写
        //    http:// 的裸域名也认**,并且一条都没补时把原因报出来)──
        //   用户:「自动为输入框内未进行图床标注的所有链接自动添加图床标符」+
        //   「只要检测到超链接,便会自动添加。不局限于我提供的那几个图床网址。」
        //   ★ 名字怎么来:先查 IMG_HOSTS 那六个图像床(用用户给的中文名 / 大写名);
        //     查不到就按域名推一名(siteNameOfHost):去掉子域与公共后缀,剩下那段首字母大写。
        //   ★ 标符长这样:【厘图片】https://tupian.li/xxx.jpg —— 插在链接**前面**、同一行里。
        //     为什么放前面而不是后面:站内帖子的图床名一向写在图/链之上(tid-2966403 那楼就是
        //     「1. urusai_cc」下面跟着一串图),一列排下来前面带标签的行最好扫。
        //   ★ 三条「不碰」的规矩,每一条都对应一种会把原文弄坏的插法:
        //     ① 链接前面已经有同一枚标符 ⇒ 跳过(于是本函数幂等:粘贴后再点一次不会越堆越多);
        //     ② 链接紧跟在 [img] / [media] / [url] / [url= 这类开口标签后面 ⇒ 跳过
        //        (2.54.0 起把 [wmv= / [rm= 一并收进来:「媒 ▾」插出来的正是这两种,粘贴的
        //          URL 就贴在它们后面,不收的话【站点名】会被塞进标签里)
        //        ([img]【厘图片】https://… 会把标符当成图片地址,图直接裂);
        //     ③ [code] 块里的链接 ⇒ 跳过(那是给人看的源码);
        //     ④ URL 落在 HTML 标签**里面**(`<a href="…">` 这种)⇒ 跳过 —— 插进去会把标签
        //        本身插坏(`<a 【Ztube】href=`)。
        //        ★ 2.53.98 收掉一条过宽的:原先「紧跟引号 / 等号(= 属性值)」也一并跳,
        //          于是正文里写 `链接=https://…`、`他说"https://…` 会被误跳过。HTML 标签
        //          那一条已经完整覆盖 `href="…"`,单独判「等号 / 引号」只剩误伤 ⇒ 不判了。
        //   ★ 光标:按插入点把 selectionStart / selectionEnd 往后挪,
        //     否则插在光标之前的标符会把光标顶偏(改完继续打字时最明显)。
        //   ★ 网址字符类刻意不含 CJK 与全角标点:站点帖子常把中文紧贴在链接后面(「链接如下」),
        //     不排掉就会把后面的汉字一起吞进 URL 里。
        const SPP_URL_RE = /https?:\/\/[^\s<>"'【】\u3000-\u303f\u4e00-\u9fff\u3040-\u30ff\uff00-\uffef]+/g;
        const SPP_OPEN_TAG_RE = /\[(?:img|media|url|email|flash|wmv|rm)(?:=[^\]]*)?\]?\s*$/i;

        /* ★★ 2.53.98:没写协议头的裸域名也认(用户:「只要检测到超链接,便会自动添加」)。
           · 形状 = 「(标签.)+TLD」,TLD 走**白名单**(read.php / 2.53.98 / a.jpg 这类形状相似
             的照样不认);
           · m[1] 是「前面那个不是域名的边界字符」,域名本体的起点 = m.index + m[1].length;
           · 边界字符类排掉 @ . / - 与字母数字 ⇒ 邮箱([email protected])、路径片段(/x.com)、
             「https://a.com」里那个 a.com 都不算第二条;
           · m[3] 是可选端口 + 路径尾巴,不含 CJK 与全角标点(与 SPP_URL_RE 同一套排法)。 */
        const SPP_BARE_RE = /(^|[^0-9A-Za-z_@.\/-])((?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,})((?::\d{2,5})?(?:\/[^\s<>"'【】\u3000-\u303f\u4e00-\u9fff\u3040-\u30ff\uff00-\uffef]*)?)/gi;

        /* 认裸域名要的 TLD 白名单 —— 宁可漏认,不可把 read.php / 2.53.98 / a.jpg 这种
           形状相似的当链接补上标符。六个图床用到的 li / moe / site / io / com / cc 都在里面。 */
        const SPP_TLDS = ('com net org edu gov mil int info biz name pro aero coop museum io co me tv cc li moe '
            + 'cn tw hk jp kr sg my th vn ph id in pk au nz uk de fr nl be it es pt se no dk fi is ie ch at pl cz sk hu ro bg gr tr ru ua by kz il sa ae eg za br mx ar cl pe us ca gg la ly to ws pw ne nu tk ml ga cf st su ac sh im '
            + 'top site space xyz club online store shop app dev tech art design live life world today news blog pub win bid loan icu vip wang ren kim work link click press host cloud fun website company network wiki media agency digital studio software mobile tools email zone center city plus love one')
            .split(' ');

        /* ★★ 2.53.99:站内附件要单独认出来 —— 它是**本站自己托管的文件**,不是外站图床。
           用户报:blue-plus.net/attachment/Mon_2610/9_….jpg 被标成了【Blue-plus】;
           他给的正确做法是包成 [img]…[/img](站内图片要内嵌显示)。
           返回 'img' = 图片附件(该包 [img]);'self' = 别的附件文件(zip / rar 等,不碰);
           '' = 不是站内附件(站内普通链接、外站链接都归这里,照旧补【站点名】)。 */
        const SPP_SELF_HOSTS = ['blue-plus.net', 'south-plus.net', 'south-plus.org', 'north-plus.net', 'east-plus.net', 'white-plus.net', 'level-plus.net', 'spring-plus.net', 'summer-plus.net', 'snow-plus.net', 'soul-plus.net', 'imoutolove.me'];
        const SPP_IMG_EXT_RE = /\.(?:jpe?g|png|gif|webp|bmp|avif)$/i;
        const SPP_FILE_EXT_RE = /\.(?:zip|rar|7z|mp4|webm|mkv|txt|pdf)$/i;
        function sppSelfKind(url) {
            let u;
            try { u = new URL(sppAbsUrl(url), location.href); } catch (e) { return ''; }
            const h = u.hostname.toLowerCase().replace(/^www\./, '');
            let self = false;
            for (let i = 0; i < SPP_SELF_HOSTS.length; i++) {
                const d = SPP_SELF_HOSTS[i];
                if (h === d || h.slice(-(d.length + 1)) === '.' + d) { self = true; break; }
            }
            if (!self) return '';
            const p = u.pathname.toLowerCase();
            if (SPP_IMG_EXT_RE.test(p)) return 'img';
            if (p.indexOf('/attachment/') >= 0 || p.indexOf('/thumb/') >= 0 || SPP_FILE_EXT_RE.test(p)) return 'self';
            return '';            /* 站内普通链接(read.php?tid=… 这类)不算附件 */
        }

        /* ★★ 2.54.1:这条链接是不是**图片** —— 不看域名,只看后缀。
           用户报:https://p.inari.site/usr/2927/6ac2bcaa1952f.webp 点「标」变成了
           【Inari】…,而他要的是 [img]…[/img]。理由与站内附件同一条:图片要内嵌显示,
           不需要名字。先前只认「站内」域族(sppSelfKind),图床与外站图片全漏了。
           ★ 只测 pathname:查询串(?w=200)与井号不影响判定。 */
        function sppImgLink(url) {
            let u;
            try { u = new URL(sppAbsUrl(url), location.href); } catch (e) { return false; }
            return SPP_IMG_EXT_RE.test(u.pathname);
        }

        /* 公共后缀是多段的那些(co.uk / com.cn / pages.dev / github.io …)—— 不列出来的话
           tupian.co.uk 会被推成「Co」、yodhcn.pages.dev 会被推成「Pages」。 */
        const SPP_MULTI_SUFFIX = [
            'co.uk', 'org.uk', 'me.uk', 'ac.uk', 'gov.uk', 'com.cn', 'net.cn', 'org.cn', 'gov.cn', 'edu.cn',
            'com.tw', 'org.tw', 'com.hk', 'com.au', 'co.jp', 'co.kr', 'com.sg', 'com.my', 'com.br', 'com.mx',
            'pages.dev', 'github.io', 'gitlab.io', 'netlify.app', 'vercel.app', 'web.app', 'workers.dev',
            'firebaseapp.com', 'blogspot.com', 'herokuapp.com', 'onrender.com', 'glitch.me', 'repl.co',
            'cloudfront.net', 's3.amazonaws.com', 'r2.dev', 'nyc.mn'
        ];

        /* 按域名推一个「站点名」:去掉子域与公共后缀,剩下那一段首字母大写。
           ★ 例:ztube.org → Ztube;www.yuepmaa.space → Yuepmaa;yodhcn.pages.dev → Yodhcn;
             hmoegirl.com → Hmoegirl;koe-koe.com → Koe-koe。
           ★ IP 直用(192.168.1.5 → 192.168.1.5);单段(localhost)也直用。 */
        function siteNameOfHost(host) {
            const h = String(host || '').toLowerCase().replace(/\.$/, '');
            if (!h) return '';
            if (/^\d{1,3}(\.\d{1,3}){3}$/.test(h) || h.indexOf(':') >= 0) return h;
            const parts = h.split('.');
            if (parts.length < 2) return h;
            const last2 = parts.slice(-2).join('.');
            const keep = (SPP_MULTI_SUFFIX.indexOf(last2) >= 0 && parts.length >= 3)
                ? parts[parts.length - 3]
                : parts[parts.length - 2];
            const clean = String(keep || '').replace(/[^a-z0-9-]/g, '');
            if (!clean) return h;
            return clean.charAt(0).toUpperCase() + clean.slice(1);
        }

        /* 这条链接该配什么名字:先查六个图床(用户给的名字),查不到按域名推。
           名字为空 = 这条不标注。 */
        /* ★★ 2.53.98:裸域名(blue-plus.net/x)不能直接丢给 new URL —— 它会被当成**相对
           路径**,解析出来是当前站点的域名(在 blue-plus.net 上把「ztube.org/a」推成了
           「Blue-plus」)。补一个 https:// 再解析。 */
        function sppAbsUrl(url) {
            return /^[a-z][a-z0-9+.-]*:\/\//i.test(url) ? url : 'https://' + url;
        }

        function markNameOf(url) {
            const hit = imgHostOf(url);
            if (hit) return hit.name;
            let host = '';
            try { host = new URL(sppAbsUrl(url), location.href).hostname; } catch (e) { return ''; }
            return siteNameOfHost(host);
        }

        /* 这条链接属于哪个图床?认不出返回 null。子域名靠后缀匹配(www. / i. / files. 都算)。 */
        function imgHostOf(url) {
            let host = '';
            try { host = new URL(sppAbsUrl(url), location.href).hostname.toLowerCase(); } catch (e) { return null; }
            if (!host) return null;
            host = host.replace(/^www\./, '');
            /* ★ 2.55.14:认链走 IMG_HOSTS_LOOKUP(自定义图床排在自带六个**前面**)——
               自带那条先命中会把用户特意写的子域规则截胡,理由见 IMG_HOSTS 上方那段。 */
            for (let i = 0; i < IMG_HOSTS_LOOKUP.length; i++) {
                const h = IMG_HOSTS_LOOKUP[i];
                for (let j = 0; j < h.hosts.length; j++) {
                    const d = h.hosts[j];
                    if (host === d || host.slice(-(d.length + 1)) === '.' + d) return h;
                }
            }
            return null;
        }

        /* 给 ta 里所有「还没标注过」的链接补上【站点名】。返回补了几条(0 = 一条都不用补)。
           ★ 只改 value,不派发 input 事件 —— 与插入标签那几颗按钮同一套做法(站点按钮也这么干)。
           ★★ 2.53.98:认两遍 —— ① 带协议头的 http(s)(SPP_URL_RE)② 没写协议头的裸域名
              (SPP_BARE_RE + TLD 白名单)。第二遍要避开第一遍已经吃掉的区间。
           ★ 第二个参数 stats 可选:填 { skip: {marked,tag,code,attr}, none } ——
              按钮就是靠它把「为什么一条都没补」说出来的。 */
        function annotateHostMark(ta, stats) {
            const src = ta.value || '';
            if (!src) return 0;
            const spans = [];                 /* 待补的位置:{ pos, url } */
            const taken = [];                 /* 第一遍吃掉的区间,第二遍要避开 */
            let m;
            SPP_URL_RE.lastIndex = 0;
            while ((m = SPP_URL_RE.exec(src)) !== null) {
                spans.push({ pos: m.index, url: m[0] });
                taken.push([m.index, m.index + m[0].length]);
            }
            const inTaken = function (at) {
                for (let i = 0; i < taken.length; i++) {
                    if (at >= taken[i][0] && at < taken[i][1]) return true;
                }
                return false;
            };
            SPP_BARE_RE.lastIndex = 0;
            while ((m = SPP_BARE_RE.exec(src)) !== null) {
                const pos = m.index + m[1].length;          /* m[1] 是边界字符,不属于域名 */
                if (inTaken(pos)) continue;                 /* https://a.com 里的 a.com 不算第二条 */
                if (SPP_TLDS.indexOf(m[2].slice(m[2].lastIndexOf('.') + 1).toLowerCase()) < 0) continue;
                spans.push({
                    pos: pos,
                    /* 尾巴上粘着的句读不算链接的一部分(example.com. 这种)。 */
                    url: (m[2] + (m[3] || '')).replace(/[.,;:!?\u3001\u3002\uff0c\uff1b\uff1a]+$/, ''),
                });
            }
            if (!spans.length) { if (stats) { stats.none = 1; } return 0; }
            spans.sort(function (a, b) { return a.pos - b.pos; });
            const skip = { marked: 0, tag: 0, code: 0, attr: 0, self: 0 };
            const marks = [];                 /* 每处补丁写进 out 的位置与长度,用来搬光标 */
            let out = '', last = 0, adds = 0, rms = 0, wraps = 0;
            for (let i = 0; i < spans.length; i++) {
                const sp = spans[i];
                if (sp.pos < last) continue;
                out += src.slice(last, sp.pos);
                /* 跳过判定看「链接之前的所有原文」—— 与旧版比只是把「上一处之后」放宽到整串,
                   尾巴上那几条判定(标符 / 开口标签)本来是锚在结尾的,结论不变。 */
                const cut = src.slice(0, sp.pos).replace(/[ \t\u3000]+$/, '');
                const name = markNameOf(sp.url);
                const tag = name ? '【' + name + '】' : '';
                const hasTag = !!name && cut.slice(-tag.length) === tag;
                /* ★★ 2.54.1:**图片链接一律包 [img]…[/img]、不再补名字** ——
                   站内附件图片、六个图床、任意外站的 .jpg/.png/.webp… 都算
                   (用户:「p.inari.site/…webp 补标符变成【Inari】…,我需要 [img]…[/img]」)。
                   名字推不出来的(IP / 单段域名 / mailto:)也照包 —— 图不需要名字。 */
                const selfKind = sppSelfKind(sp.url);
                const isImg = selfKind === 'img' || sppImgLink(sp.url);
                const wantWrap = isImg && SELF_IMG;
                let why = '';
                if (!name && !wantWrap) why = 'n';                               /* 推不出名字(mailto: 之类) */
                else if (SPP_OPEN_TAG_RE.test(cut)) why = 'tag';                 /* 紧跟 [img] [url= 开口标签 */
                else if (cut.lastIndexOf('[code]') > cut.lastIndexOf('[/code]')) why = 'code';
                else if (cut.lastIndexOf('<') > cut.lastIndexOf('>')) why = 'attr';   /* 还在 HTML 标签内部 */
                /* 旧规矩留下的误标:紧贴图片链接、正好是本函数会生成的那一枚,且没被
                   开口标签 / 代码块 / HTML 挡着 ⇒ 删掉;要包 [img] 的接着包。 */
                const staleTag = hasTag && isImg && !why;
                if (staleTag) {
                    out = out.slice(0, out.length - tag.length);
                    marks.push({ at: out.length, len: -tag.length });
                    rms++;
                    if (wantWrap) { marks.push({ at: out.length, len: 5 }); out += '[img]'; wraps++; }
                } else if (hasTag) {
                    skip.marked++;                                   /* 幂等:已有同一枚标符 */
                } else if (why) {
                    if (why !== 'n') skip[why]++;
                } else if (selfKind === 'self' || (isImg && !wantWrap)) {
                    skip.self++;         /* 站内非图片附件(zip/rar)/ 图片链接但「转 [img]」开关关着 */
                } else if (wantWrap) {
                    /* ★★ 2.53.99:站内附件图片按站点规范包成 [img]…[/img](用户给的正确做法)。 */
                    marks.push({ at: out.length, len: 5 });
                    out += '[img]';
                    wraps++;
                } else {
                    marks.push({ at: out.length, len: tag.length });
                    out += tag;
                    adds++;
                }
                out += src.slice(sp.pos, sp.pos + sp.url.length);
                if (wantWrap && !why) { marks.push({ at: out.length, len: 6 }); out += '[/img]'; }
                last = sp.pos + sp.url.length;
            }
            out += src.slice(last);
            if (stats) { stats.skip = skip; stats.removed = rms; stats.wrapped = wraps; }
            if (!adds && !rms && !wraps) return 0;
            const s = ta.selectionStart, e = ta.selectionEnd;
            ta.value = out;
            const shift = function (p) {
                let d = 0;
                for (let i = 0; i < marks.length; i++) { if (marks[i].at < p) d += marks[i].len; }
                return p + d;
            };
            try { ta.setSelectionRange(shift(s), shift(e)); } catch (err) { }
            return adds;
        }

        // ── 图床快捷跳转(2.53.96):新标签页打开该图床首页 ──
        //   先用 window.open(在用户手势里不会被拦),被拦了就退回「造一枚 <a> 点一下」。
        function openSite(url) {
            try { if (window.open(url, '_blank', 'noopener')) return; } catch (e) { }
            const a = document.createElement('a');
            a.href = url;
            a.target = '_blank';
            a.rel = 'noopener noreferrer';
            document.body.appendChild(a);
            a.click();
            a.parentNode.removeChild(a);
        }

        // ── 构建图床跳转弹窗(2.53.96)──
        //   ★ 与颜色 / 字体那两枚同一套挂法:绝对定位挂进 .spp-editor-toolbar(它是 position:relative),
        //     点一项 → 新标签页打开该图床;点面板外由 closePopover 收掉。
        //   ★ 左边是名字(就是 IMG_HOSTS 的 name,拉丁名首字母大写),右边小灰字是该站域名。
        function buildHostPopover() {
            closePopover();
            const popover = document.createElement('div');
            popover.className = 'spp-host-popover';
            popover.style.cssText = [
                'position:absolute; z-index:100003;',
                'background:var(--spu-surface,#fff);',
                'border:1px solid var(--spu-border,#e0e0e0);',
                'border-radius:8px; padding:6px;',
                'box-shadow:0 8px 32px rgba(0,0,0,0.18);',
                'display:flex; flex-direction:column; gap:2px;',
                'min-width:180px;',
            ].join('');

            IMG_HOSTS.forEach(function (h) {
                const item = document.createElement('div');
                SPP_TIP.attach(item, '打开 ' + h.name);
                item.style.cssText = [
                    'display:flex; align-items:baseline; gap:8px;',
                    'padding:6px 10px; font-size:13px; cursor:pointer; border-radius:4px;',
                    'transition:background 0.12s;',
                ].join('');
                const nm = document.createElement('span');
                nm.textContent = h.name;
                nm.style.cssText = 'font-weight:600; color:var(--spu-text,#333);';
                const dm = document.createElement('span');
                dm.textContent = h.site.replace(/^https?:\/\//, '').replace(/\/$/, '');
                dm.style.cssText = 'margin-left:auto; font-size:11px; color:var(--spu-muted,#9B9489);';
                item.appendChild(nm);
                item.appendChild(dm);
                item.addEventListener('mouseenter', function () { item.style.background = 'var(--spu-hover,#f0ebe1)'; });
                item.addEventListener('mouseleave', function () { item.style.background = 'transparent'; });
                item.addEventListener('click', function (e) {
                    e.stopPropagation();
                    closePopover();
                    openSite(h.site);
                });
                popover.appendChild(item);
            });

            armPopoverClose();
            activePopover = popover;
            return popover;
        }

        // ── 构建多媒体弹窗(2.54.0)──
        //   ★ 四种写法照抄站点 js/wind_c_editor.js 的 insertmedia()(那是权威口径):
        //       case 1 rm    → [rm=长,宽,自动]url[/rm]
        //       case 2 wmv   → [wmv=长,宽,自动]url[/wmv]
        //       case 3 mp3   → [wmv=自动]url[/wmv]      (站点把 mp3 也塞进 wmv 档)
        //       case 4 flash → [flash=长,宽,自动]url[/flash]
        //     尺寸那两个框就是站点弹窗里的「显示尺寸:[长] [宽]」(它默认 314×256,这里 480×360)。
        //   ★ 自动 = 1 时服务端额外补一句 player() 直接起播;0 = 点了才播。
        //   ★ 实话:三个播放器走的都是 NPAPI 插件(flash / mplayer2 / realplayer),今天的
        //     Chrome / Edge / Firefox 都不支持了 ⇒ 多半只看到「点击播放…」占位不会真出画面。
        //     这是站点自己的限制(它的原生弹窗一样),脚本这边只能把**站点认的标签**给对。
        const MEDIA_KINDS = [
            { label: 'Flash 动画', tag: 'flash', dim: true,  note: '[flash=长,宽,自动]' },
            { label: '视频 (wmv)', tag: 'wmv',   dim: true,  note: '[wmv=长,宽,自动]' },
            { label: '音频 (mp3)', tag: 'wmv',   dim: false, note: '[wmv=自动]' },
            { label: '音乐 (rm)',  tag: 'rm',    dim: true,  note: '[rm=长,宽,自动]' },
        ];
        function buildMediaPopover(btn, textarea) {
            closePopover();
            const popover = document.createElement('div');
            popover.className = 'spp-media-popover';
            popover.style.cssText = [
                'position:absolute; z-index:100003;',
                'background:var(--spu-surface,#fff);',
                'border:1px solid var(--spu-border,#e0e0e0);',
                'border-radius:8px; padding:8px;',
                'box-shadow:0 8px 32px rgba(0,0,0,0.18);',
                'display:flex; flex-direction:column; gap:5px;',
                'min-width:206px;',
            ].join('');

            const sizeRow = document.createElement('div');
            sizeRow.style.cssText = 'display:flex; align-items:center; gap:4px; font-size:11px; color:var(--spu-muted,#9B9489);';
            const wLab = document.createElement('span'); wLab.textContent = '显示尺寸';
            const xLab = document.createElement('span'); xLab.textContent = '×';
            const wIn = document.createElement('input');
            const hIn = document.createElement('input');
            wIn.value = '480'; hIn.value = '360';
            [wIn, hIn].forEach(function (el, i) {
                el.type = 'text'; el.inputMode = 'numeric';
                SPP_TIP.attach(el, i === 0 ? '显示尺寸 · 长(第一个参数)' : '显示尺寸 · 宽(第二个参数)');
                el.style.cssText = 'width:48px; height:22px; box-sizing:border-box; padding:0 4px; ' +
                    'border:1px solid var(--spu-border,#E8E3D8); border-radius:5px; text-align:center; ' +
                    'font-size:12px; font-family:inherit; background:var(--spu-surface,#fff); color:var(--spu-text,#333); outline:none;';
                el.addEventListener('click', function (e) { e.stopPropagation(); });
            });
            [wLab, wIn, xLab, hIn].forEach(function (n) { sizeRow.appendChild(n); });
            popover.appendChild(sizeRow);

            const autoRow = document.createElement('label');
            autoRow.style.cssText = 'display:flex; align-items:center; gap:5px; font-size:11px; color:var(--spu-muted,#9B9489); cursor:pointer;';
            const auto = document.createElement('input');
            auto.type = 'checkbox';
            SPP_TIP.attach(auto, '勾上 = 服务端多补一句 player() 直接起播;不勾 = 点了才播');
            auto.addEventListener('click', function (e) { e.stopPropagation(); });
            autoRow.appendChild(auto);
            autoRow.appendChild(document.createTextNode('自动播放'));
            popover.appendChild(autoRow);

            MEDIA_KINDS.forEach(function (k) {
                const item = document.createElement('div');
                SPP_TIP.attach(item, k.note);
                item.style.cssText = 'display:flex; align-items:baseline; gap:8px; padding:6px 8px; ' +
                    'font-size:13px; cursor:pointer; border-radius:4px; transition:background 0.12s;';
                const nm = document.createElement('span');
                nm.textContent = k.label;
                nm.style.cssText = 'font-weight:600; color:var(--spu-text,#333);';
                const tg = document.createElement('span');
                tg.textContent = k.note;
                tg.style.cssText = 'margin-left:auto; font-size:11px; color:var(--spu-muted,#9B9489);';
                item.appendChild(nm); item.appendChild(tg);
                item.addEventListener('mouseenter', function () { item.style.background = 'var(--spu-hover,#f0ebe1)'; });
                item.addEventListener('mouseleave', function () { item.style.background = 'transparent'; });
                item.addEventListener('click', function (e) {
                    e.stopPropagation();
                    insertMediaTag(textarea, k.tag, wIn.value, hIn.value, auto.checked ? 1 : 0, k.dim);
                    closePopover();
                });
                popover.appendChild(item);
            });

            armPopoverClose();
            activePopover = popover;
            return popover;
        }

        // ── 「表」右边那三个输入框(行 × 列 × 宽,2.54.0)──
        //   ★ 这就是「输入框 + 表格按钮」的复合控件:先摆按钮、再把输入框接在它右边
        //     (appendChild 的顺序就是 DOM 顺序,见按钮循环里那一行)。
        //   ★ 输入框里点一下别让它把弹窗关掉(冒泡上来的 click 会触发 closePopover);
        //     在框里按回车等同于点「表」。
        function buildTableUI(btn) {
            const wrap = document.createElement('span');
            wrap.className = 'spp-tb-tblui';
            wrap.style.cssText = 'display:inline-flex; align-items:center; gap:3px;';
            function box(val, ph, title, w) {
                const i = document.createElement('input');
                i.type = 'text';
                i.inputMode = 'numeric';
                i.value = val;
                i.placeholder = ph;
                SPP_TIP.attach(i, title);
                i.style.cssText = 'width:' + w + 'px; height:22px; box-sizing:border-box; padding:0 3px; ' +
                    'border:1px solid var(--spu-border,#E8E3D8); border-radius:5px; text-align:center; ' +
                    'font-size:12px; font-family:inherit; background:var(--spu-surface,#fff); ' +
                    'color:var(--spu-text,#333); outline:none;';
                i.addEventListener('click', function (e) { e.stopPropagation(); });
                i.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); btn.click(); } });
                return i;
            }
            const rows = box('2', '行', '表格行数(1–50)', 26);
            const cols = box('3', '列', '表格列数(1–50)', 26);
            const wide = box('', '宽', '表格宽度:400 = 400px,50% = 50%,留空 = 站点默认(98%)', 40);
            const x = document.createElement('span');
            x.textContent = '×';
            x.style.cssText = 'font-size:11px; color:var(--spu-muted,#9B9489);';
            [rows, x, cols, wide].forEach(function (n) { wrap.appendChild(n); });
            btn._sppRows = rows;
            btn._sppCols = cols;
            btn._sppWide = wide;
            return wrap;
        }

        // ── 插入链接(光标定位到 URL 占位符) ──
        function insertLink(textarea, withText) {
            const s = textarea.selectionStart;
            const e = textarea.selectionEnd;
            const parts = splitSelEdge(textarea.value.substring(s, e));
            const sel = parts.core;
            const placeholder = 'https://...';
            let tag, urlStart;
            if (withText) {
                // [url=https://...]文字[/url]
                tag = parts.lead + '[url=' + placeholder + ']' + (sel || '文字') + '[/url]' + parts.trail;
                urlStart = s + parts.lead.length + 5; // '[url='
            } else {
                // [url]https://...[/url]
                tag = parts.lead + '[url]' + placeholder + '[/url]' + parts.trail;
                urlStart = s + parts.lead.length + 5; // '[url]'
            }
            textarea.value = textarea.value.substring(0, s) + tag + textarea.value.substring(e);
            textarea.setSelectionRange(urlStart, urlStart + placeholder.length);
            textarea.focus();
        }

        // ── 表格:列字母(0→A、25→Z、26→AA)与 windcode 组装(「表」按钮与自检共用)──
        //   ★ 占位文字 = 列字母 + 行号(A1、B1、A2…),与站点原生表格弹窗插出来的空表一一对应。
        //   ★ 宽度口径照抄站点:填数字当 px,带 % 或 px 后缀原样,留空只写 [table]
        //     (服务端把没有宽度的 [table] 渲染成 width:98%)。行/列都夹在 1–50,
        //     空框与非数字回默认(2 行 3 列),填 0 也算 1(不是回默认 —— 0 是有意义的输入)。
        function tblNum(v, def) {
            const n = parseInt(v, 10);
            return isFinite(n) ? Math.max(1, Math.min(50, n)) : def;
        }
        function tblColName(j) {
            let s = '';
            let n = j;
            do { s = String.fromCharCode(65 + (n % 26)) + s; n = Math.floor(n / 26) - 1; } while (n >= 0);
            return s;
        }
        function buildTableTag(rows, cols, wide) {
            const r = tblNum(rows, 2);
            const c = tblNum(cols, 3);
            const w = String(wide == null ? '' : wide).replace(/[^0-9A-Za-z.%]/g, '');
            const lines = ['[table' + (w ? '=' + w : '') + ']'];
            for (let i = 1; i <= r; i++) {
                let line = '[tr]';
                for (let j = 0; j < c; j++) line += '[td]' + tblColName(j) + i + '[/td]';
                lines.push(line + '[/tr]');
            }
            lines.push('[/table]');
            return lines.join('\n');
        }

        // ── 多媒体:按站点口径拼标签(纯函数,自检直接跑它)──
        //   dim=true  → [标签=长,宽,自动]URL[/标签]
        //   dim=false → [标签=自动]URL[/标签](站点「mp3」档走的就是这条:wmv 单参)
        //   ★ 服务端实测:[rm=长,宽] 少写一个参数整条就变字面文字 ⇒ 三参必填;本函数永远写全。
        //   ★ 贴着插入的那条 URL 会被选中(直接打字就是替换它)。
        function insertMediaTag(textarea, tag, w, h, auto, dim) {
            const s = textarea.selectionStart;
            const e = textarea.selectionEnd;
            const parts = splitSelEdge(textarea.value.substring(s, e));
            const sel = parts.core;
            const placeholder = 'https://...';
            const param = dim
                ? (String(w).replace(/[^0-9]/g, '') || '480') + ',' + (String(h).replace(/[^0-9]/g, '') || '360') + ',' + (auto ? 1 : 0)
                : String(auto ? 1 : 0);
            const open = '[' + tag + '=' + param + ']';
            const body = sel || placeholder;
            const tagStr = parts.lead + open + body + '[/' + tag + ']' + parts.trail;
            textarea.value = textarea.value.substring(0, s) + tagStr + textarea.value.substring(e);
            const us = s + parts.lead.length + open.length;
            textarea.setSelectionRange(us, us + body.length);
            textarea.focus();
        }

        // ── 构建工具栏(胶囊按钮 + 分组优化)──
        function buildToolbar(textarea) {
            const bar = document.createElement('div');
            bar.className = 'spp-editor-toolbar';
            bar.style.cssText = [
                'display:flex; flex-wrap:wrap; align-items:center;',
                'gap:5px 10px; padding:8px 12px;',
                'background:var(--spu-overlay,#F0EBE1);',
                'border:1px solid var(--spu-border,#E8E3D8);',
                'border-bottom:none; border-radius:8px 8px 0 0;',
                'position:relative;',
            ].join('');

            // 胶囊按钮基础样式
            const btnBase = [
                'padding:4px 10px; font-size:12px; font-weight:600;',
                'border:1.5px solid transparent; border-radius:14px;',
                'background:var(--spu-surface,#fff); color:var(--spu-text,#333);',
                'cursor:pointer; min-width:28px;',
                'transition:all 0.18s ease; font-family:inherit;',
                'line-height:1.4; letter-spacing:0.02em;',
                'user-select:none;',
            ].join('');

            // 分组标签样式
            const lblStyle = [
                'font-size:10px; color:var(--spu-muted,#9B9489);',
                'white-space:nowrap; letter-spacing:0.05em;',
                'text-transform:uppercase; font-weight:500;',
            ].join('');

            // ★★ 2.53.13(用户批注「默认的长度不一样,修改一下对齐长度」,红框在三组快捷键行的右端):
            //   原来是「一个 flex 容器 + flex-basis:100% 的换行占位 span」拼出视觉上的三行 ——
            //   行本身不是盒子,长度等于内容长度 ⇒ 三行右端各停在各处,永远与编辑区右缘对不齐。
            //   现在每行是一个真正的行盒:flex:0 0 100% + width:100% + 一条下边框,
            //   长度恒等于工具栏内框 ⇒ 三行等长、且都顶到右缘(末行不给边框,工具栏自己那条收尾)。
            const rows = [];
            function newRow() {
                const row = document.createElement('div');
                row.className = 'spp-tb-row';
                row.style.cssText = 'display:flex;flex-wrap:wrap;align-items:center;gap:5px 10px;' +
                    'flex:0 0 100%;width:100%;box-sizing:border-box;' +
                    'padding-bottom:5px;border-bottom:1px solid var(--spu-border,#E8E3D8);';
                bar.appendChild(row);
                rows.push(row);
                return row;
            }
            let row = newRow();

            BUTTON_GROUPS.forEach((group, gi) => {
                // 换行控制:基础文本格式(0)、段落与布局(1)、列表(2)、链接与媒体(3) 各独占一行。
                // ★★ 2.54.6(用户:「移除 PHPWind 专有功能 板块」):第四行原来是「链接与媒体 +
                //   PHPWind 专有功能」共用(两组之间还要挂一道竖分隔),那组撤掉后这行只剩
                //   「链接与媒体」⇒ 共用逻辑与分隔符一起删。
                if (gi === 1 || gi === 2 || gi === 3) row = newRow();

                // 分组标签
                const label = document.createElement('span');
                label.textContent = group.label;
                label.style.cssText = lblStyle + 'margin-right:2px;';
                row.appendChild(label);

                group.buttons.forEach(b => {
                    /* ★★ 2.54.2:显式断行(当时列表那组是两枚一行,2.54.7 起一类列表一行)。
                       b.sub = 行内小标签(不换行);b.brk = 换行(非空串再放一枚小标签)。
                       ★ 整组只在末尾留一条分隔线,不至于每两枚横一道 —— 收的是**上一行**的
                       下边框(2.54.2 收的是新行,那条线就落进组内了,2.54.7 修正)。 */
                    if (b.brk !== undefined || b.sub !== undefined) {
                        if (b.brk !== undefined) {
                            /* ★★ 2.54.7①(用户「列表的排序结构即为你提供的文档结构」):断行时
                               该收的是**上一行**的下边框,而不是新行的 —— 2.54.2 起收的是新行,
                               于是那道线停在组内第一条与第二条之间,整组末尾反倒没线(与当初
                               注释里写的意图正好相反)。现在反着来:旧行收线、新行保留默认边框。 */
                            row.style.borderBottom = 'none';
                            row = newRow();
                            /* ★★ 2.54.7②:续行的左端对齐 —— 补一枚与组标签**同文本、同字体、
                               同字号**的隐形占位,宽度天然与真标签相等(不用量、不写魔数),
                               于是「无序列表」正对「有序列表」、两组第一枚按钮也同轴。 */
                            if (b.relabel) {
                                const gutter = document.createElement('span');
                                gutter.textContent = group.label;
                                gutter.setAttribute('aria-hidden', 'true');
                                gutter.style.cssText = lblStyle + 'margin-right:2px;visibility:hidden;';
                                row.appendChild(gutter);
                            }
                        }
                        const subText = b.brk || b.sub || '';
                        if (subText) {
                            const sub = document.createElement('span');
                            sub.textContent = subText;
                            sub.style.cssText = lblStyle + 'margin-right:2px;';
                            row.appendChild(sub);
                        }
                        return;
                    }
                    const btn = document.createElement('button');
                    btn.type = 'button';
                    btn.textContent = b.label;
                    SPP_TIP.attach(btn, b.title);
                    btn.style.cssText = btnBase;

                    // 特殊按钮:B/I/U/S 用更突出的字体样式
                    if (b.tag === 'b') btn.style.fontWeight = '700';
                    if (b.tag === 'i') btn.style.fontStyle = 'italic';
                    if (b.tag === 'u') btn.style.textDecoration = 'underline';
                    if (b.tag === 's') btn.style.textDecoration = 'line-through';

                    // 悬停效果:填充主题色
                    btn.addEventListener('mouseenter', () => {
                        btn.style.background = 'var(--spu-accent,#C75B39)';
                        btn.style.color = '#fff';
                        btn.style.borderColor = 'var(--spu-accent,#C75B39)';
                        btn.style.transform = 'translateY(-1px)';
                        btn.style.boxShadow = '0 2px 8px rgba(199,91,57,0.2)';
                    });
                    btn.addEventListener('mouseleave', () => {
                        btn.style.background = 'var(--spu-surface,#fff)';
                        btn.style.color = 'var(--spu-text,#333)';
                        btn.style.borderColor = 'transparent';
                        btn.style.transform = 'translateY(0)';
                        btn.style.boxShadow = 'none';
                    });

                    // 点击(active)反馈
                    btn.addEventListener('mousedown', () => {
                        btn.style.transform = 'scale(0.95)';
                        btn.style.transition = 'transform 0.08s ease';
                    });
                    btn.addEventListener('mouseup', () => {
                        btn.style.transform = 'translateY(-1px)';
                        btn.style.transition = 'all 0.18s ease';
                    });

                    btn.addEventListener('click', e => {
                        e.preventDefault();

                        if (b.type === 'color' || b.type === 'backcolor') {
                            closePopover();
                            const tag = b.type === 'backcolor' ? 'backcolor' : 'color';
                            const popover = buildColorPopover(btn, textarea, tag);
                            const btnRect = btn.getBoundingClientRect();
                            const barRect = bar.getBoundingClientRect();
                            popover.style.top = (btnRect.bottom - barRect.top + 6) + 'px';
                            popover.style.left = '0px';
                            bar.appendChild(popover);
                            return;
                        }

                        if (b.type === 'font') {
                            closePopover();
                            const popover = buildFontPopover(btn, textarea);
                            const btnRect = btn.getBoundingClientRect();
                            const barRect = bar.getBoundingClientRect();
                            popover.style.top = (btnRect.bottom - barRect.top + 6) + 'px';
                            popover.style.left = '0px';
                            bar.appendChild(popover);
                            return;
                        }

                        /* ★ 2.53.96:图床两枚 —— 「标」手动补标符,「图床 ▾」开跳转下拉。 */
                        if (b.type === 'annotate') {
                            const st = {};
                            const n = annotateHostMark(textarea, st);
                            tip(annotateTip(n, st));
                            return;
                        }

                        if (b.type === 'hosts') {
                            closePopover();
                            const popover = buildHostPopover();
                            const btnRect = btn.getBoundingClientRect();
                            const barRect = bar.getBoundingClientRect();
                            popover.style.top = (btnRect.bottom - barRect.top + 6) + 'px';
                            popover.style.left = Math.max(0, Math.min(btnRect.left - barRect.left, barRect.width - 190)) + 'px';
                            bar.appendChild(popover);
                            return;
                        }

                        /* ★★ 2.54.0:「媒 ▾」开多媒体弹窗(原来它走 type:"link",插的是死标签 [media])。 */
                        if (b.type === 'media') {
                            closePopover();
                            const popover = buildMediaPopover(btn, textarea);
                            const btnRect = btn.getBoundingClientRect();
                            const barRect = bar.getBoundingClientRect();
                            popover.style.top = (btnRect.bottom - barRect.top + 6) + 'px';
                            popover.style.left = Math.max(0, Math.min(btnRect.left - barRect.left, barRect.width - 216)) + 'px';
                            bar.appendChild(popover);
                            return;
                        }

                        if (b.type === 'link') {
                            insertLink(textarea, false);
                            return;
                        }

                        if (b.type === 'link-text') {
                            insertLink(textarea, true);
                            return;
                        }

                        /* ★★ 2.54.1:列表四种(用户:「整段 / 加条目」×「空行 / 紧排」)。
                           整段 = 起一个列表(占位只放一条),加条目 = 往列表里补一条;
                           空行 / 紧排 只差一个换行(服务端把 \n 翻成 <br>,落在两条之间就是空行)。 */
                        if (b.type === 'list' || b.type === 'item') {
                            const s = textarea.selectionStart;
                            const e2 = textarea.selectionEnd;
                            /* ★ 首尾空白(多半就是行尾那个换行)搬到列表外面 —— 不然
                               最后会多出一条空条目 [li][/li](与 [img]url\n[/img] 同病)。 */
                            const parts = splitSelEdge(textarea.value.substring(s, e2));
                            const sel = parts.core;
                            const open = b.list === 'ol' ? '[list=1]' : '[list]';
                            const one = function (line) { return '[li]' + line + '[/li]'; };
                            const glue = b.loose ? '\n' : '';
                            const body = sel ? sel.split('\n').map(one).join(glue) : one('条目');
                            const inner = b.type === 'item'
                                ? (body + (b.loose ? '\n' : ''))
                                : (b.loose ? open + '\n' + body + '\n[/list]' : open + body + '[/list]');
                            const tag = parts.lead + inner + parts.trail;
                            textarea.value = textarea.value.substring(0, s) + tag + textarea.value.substring(e2);
                            textarea.setSelectionRange(s + tag.length, s + tag.length);
                            textarea.focus();
                            return;
                        }

                        /* ★★ 2.54.0:「表」—— 先在输入框里填「行 × 列」与宽度(默认 2×3、宽度留空),
                           点一下铺一张空表;格子占位 = 列字母 + 行号(A1、B1、A2…),与站点原生
                           表格弹窗插出来的空表一一对应。留空宽度只写 [table],服务端补成 98%。 */
                        if (b.type === 'table-ui') {
                            const rows = btn._sppRows, cols = btn._sppCols, wide = btn._sppWide;
                            const tag = buildTableTag(rows.value, cols.value, wide.value);
                            rows.value = tblNum(rows.value, 2);
                            cols.value = tblNum(cols.value, 3);
                            const s = textarea.selectionStart;
                            const e2 = textarea.selectionEnd;
                            textarea.value = textarea.value.substring(0, s) + tag + textarea.value.substring(e2);
                            textarea.setSelectionRange(s + tag.length, s + tag.length);
                            textarea.focus();
                            tip('已插入 ' + rows.value + '×' + cols.value + ' 表格');
                            return;
                        }

                        if (b.type === 'self') {
                            insertSelfTag(textarea, b.tag);
                            return;
                        }

                        insertTag(textarea, b.tag, b.param, b.cursor, b.body);
                    });
                    /* ★★ 2.54.0(活体截图看出来的):「表」和它那三个输入框是**一个**复合控件,
                       各自 row.appendChild 的话,flex-wrap 会把它们拆到两行去(「表」停在上一行、
                       「2 × 3 宽」掉到下一行,看着像两件不相干的东西)。
                       ⇒ 包进一个 inline-flex 盒子当**一个** flex 项,换行时整体一起走。 */
                    if (b.type === 'table-ui') {
                        const grp = document.createElement('span');
                        grp.className = 'spp-tb-tblgrp';
                        grp.style.cssText = 'display:inline-flex; align-items:center; gap:3px;';
                        grp.appendChild(btn);
                        grp.appendChild(buildTableUI(btn));
                        row.appendChild(grp);
                    } else {
                        row.appendChild(btn);
                    }
                });
            });

            if (rows.length) rows[rows.length - 1].style.borderBottom = 'none';

            /* ★★ 2.53.96:工具栏上的一行小提示 —— 补完标符报个数,2.2 秒自己消失。
               关掉「图床链接自动标注」只影响粘贴那一半,这枚提示与手动补标符照旧。 */
            function tip(msg) {
                const old = bar.querySelector('.spp-tb-tip');
                if (old && old.parentNode) old.parentNode.removeChild(old);
                const el = document.createElement('span');
                el.className = 'spp-tb-tip';
                el.textContent = msg;
                el.style.cssText = 'position:absolute; right:10px; top:7px; padding:2px 9px; ' +
                    'border-radius:10px; font-size:11px; line-height:16px; pointer-events:none; ' +
                    'background:var(--spu-accent,#C75B39); color:#fff;';
                bar.appendChild(el);
                setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 2200);
            }

            /* ★★ 2.53.98:一条都没补时把原因分类说出来 —— 「点了没反应」不再是无解的黑盒。 */
            function missTip(st) {
                const k = (st && st.skip) || {};
                const pa = [];
                if (k.self) pa.push('图片/附件 ' + k.self + ' 条');
                if (k.marked) pa.push('已有标符 ' + k.marked + ' 条');
                if (k.tag) pa.push('在 [img]/[url] 标签里 ' + k.tag + ' 条');
                if (k.code) pa.push('在 [code] 里 ' + k.code + ' 条');
                if (k.attr) pa.push('在 HTML 标签里 ' + k.attr + ' 条');
                if (!pa.length) return '输入框里没找到链接';
                return '没有可标注的链接:' + pa.join('、');
            }

            /* ★★ 2.53.99:一次说全 —— 补了几条 / 包了几条站内附件 / 摘了几条旧误标 /
               为什么一条都没动。 */
            function annotateTip(n, st) {
                const w = (st && st.wrapped) || 0;
                const rm = (st && st.removed) || 0;
                const pa = [];
                if (n) pa.push('已给 ' + n + ' 条链接补上标符');
                if (w) pa.push('已把 ' + w + ' 条图片包成 [img]');
                if (rm) pa.push('摘掉 ' + rm + ' 条旧误标');
                return pa.length ? pa.join(',') : missTip(st);
            }

            /* ★★ 2.53.96:粘贴即补 —— 用户说的「自动」那一半(开关见设置面板的 spp_host_mark)。
               ★ 粘贴事件里读到的 value 还是旧的(这一次粘贴还没落进去),所以推到下一个 tick 再看。
               ★ 钩子只挂一次:dataset 上的标记挡住第二次(万一工具栏被重建)。 */
            if (MARK_ON && !textarea.dataset.sppHostHook) {
                textarea.dataset.sppHostHook = '1';
                textarea.addEventListener('paste', function () {
                    setTimeout(function () {
                        const st = {};
                        const n = annotateHostMark(textarea, st);
                        if (n || st.wrapped || st.removed) tip(annotateTip(n, st));
                    }, 0);
                });
            }

            return bar;
        }

        // ★★ 2.53.13:「宽度」这件事别让每个元素各自算。
        //   站点给的宽度是写死的(编辑器 textarea 行内 width:96%),表情栏又可能落在
        //   编辑区右缘之外 —— 谁按自己的宽度走,右端就会停在半路(用户红框那条空档)。
        //   这里把「可用框」算成一个数,工具栏与文本框都锁到它:
        //     左缘 = 文本框父级的内容左缘;右缘 = 有表情栏时取「表情栏左缘 − 14」,
        //     否则取父级内容右缘(14 是本站「编辑区 ↔ 表情栏」定死的间隙)。
        //   ★ 父级还没布局(隐藏 / 未插入)时不写:量到 0 会把框压没。
        function syncEditorWidth() {
            const ta = document.getElementById('textarea') || document.querySelector('textarea[name="atc_content"]');
            if (!ta || !ta.parentElement) return;
            const host = ta.parentElement;
            const hr = host.getBoundingClientRect();
            if (!hr.width || hr.width < 240) return;
            const hcs = getComputedStyle(host);
            const left = hr.left + (parseFloat(hcs.paddingLeft) || 0);
            let right = hr.right - (parseFloat(hcs.paddingRight) || 0);
            const side = document.getElementById('editor-side');
            if (side) {
                const sr = side.getBoundingClientRect();
                if (sr.width > 40 && sr.left > left + 120) right = Math.min(right, sr.left - 14);
            }
            const w = Math.max(240, Math.round(right - left));
            const bar = document.querySelector('.spp-editor-toolbar');
            if (bar) {
                bar.style.setProperty('width', w + 'px', 'important');
                bar.style.setProperty('box-sizing', 'border-box', 'important');
            }
            ta.style.setProperty('width', w + 'px', 'important');
            ta.style.setProperty('box-sizing', 'border-box', 'important');
        }

        function tryAttach(attempt) {
            if (attempt === undefined) attempt = 0;
            /* ★★ 2.53.98:快速回复框与完整编辑器(post.php 的回复 / 引用 / 发帖)两侧都接住;
               两边都没有(站点改版过的页面)就安静收手 —— 原先这里直接对 querySelector 的
               结果取 dataset,取不到会抛错,整个工具栏模块连出现的机会都没有。 */
            const textarea = document.querySelector('textarea[name="atc_content"]')
                || document.getElementById('textarea');
            if (!textarea || !textarea.parentElement) return;
            textarea.dataset.sppToolbar = '1';
            const bar = buildToolbar(textarea);
            textarea.parentElement.insertBefore(bar, textarea);
            textarea.style.borderRadius = '0 0 6px 6px';
            // ★ 2.53.13:插入当下对齐一次;站点编辑器脚本与表情栏都是稍后才定宽定位的,
            //   所以再补几拍;窗口一变重新量。
            syncEditorWidth();
            [300, 1200].forEach(t => setTimeout(syncEditorWidth, t));
            if (!window.__sppEditorWidthBound) {
                window.__sppEditorWidthBound = 1;
                window.addEventListener('resize', syncEditorWidth);
            }
        }

        onDomReady(() => setTimeout(tryAttach, 400));
    }

    // ==================== 14. 表情面板优化(emoji / 贴纸 两枚 UI + 选择区 + 分组头 + 大网格) ====================
    function initEmojiPanel() {
        const KEY = 'spp_emoji_enhance';
        let enabled = SPP_CONFIG.bool(KEY, true);   // ★ 2.52.93:走集中读数层

        // ★★ 2.53.97(用户:「能否屏蔽贴吧表情?即鼠标移上去时不显示文本框?」):
        //   那个「文本框」是**浏览器对 img[title] 的原生提示**,不是脚本画的浮层 ——
        //   本模块 8423 / 8503 两处给每格写了 title=表情名,鼠标停一下浏览器就弹框。
        //   ⇒ 改成只写 alt(失败兜底照旧)、名称照旧进数据(搜索框按 name 过滤,一点没动);
        //     只有勾了本开关才把名称写回 title。默认**关闭** = 默认不弹框。
        //   ★ 默认值 false 必须与 SPP_PANEL_DEFS 那一行、buildOverlay 那个 checkbox 同值
        //     (tools/verify/settings-panel.js 的 EXPECT 拿这几处对着核)。
        /* ★★ 2.55.14:默认值 false → true(用户:「表情名称提示;这个功能默认开启」)。
           老用户存储里那条 false 由 migrateSettings() 里的一次性迁移翻掉。
           ★ 弹的那一枚同时从**浏览器原生 title** 换成脚本自绘的提示卡(见 appendEmojiImg)——
             原生灰框的配色 / 圆角 / 字号全跟本站主题无关,美化不了,只能自己画。 */
        const EMOJI_TIP = SPP_CONFIG.bool('spp_emoji_tip', true);

        if (!enabled) return;

        // 仅在有编辑器的页面生效(read.php 有快速回复框,post.php 有完整编辑器);纯列表页跳过
        if (!WANTS.editorUi) return;
        // —— 贴吧表情数据(编号 → 含义) ——
        const TIEBA_EMOJI_ALL = [
            /* ★★ 2.53.49:整表按 tmp/emoji-audit/corrected-table.csv 重建(用户第三次提「文本和表情对不上」)。
               那轮审计逐张拉图与贴吧官方套做过比对,结论是 **113 条里 76 条错配**(67.3%),
               根因是本站这套表当年照着**手机版 APK 的编号**抄的,而仓库里的图是经典贴吧编号 ⇒ 全套错位。
               ⇒ 这里把 name 改成「**这张图实际长什么样**」(审计表的 actual 列):
                 例:仓库 image_emoticon14.png 画的是花心(不是钱)、19 是亲亲、24 是可爱、80 是便便…
               ⇒ 跳过的两类:id 1(仓库里那张是 14 字节的「404: Not Found」文本,另用 url 指到不带编号的
                 那张原图)、id 102–124(那是三维角色贴纸,不是经典表情)。
               ⇒ 同名的第二条加 ② 后缀(例如 64 与 93 都画的是摊手)。
               ★ 编号一个都没动 —— 老帖里写死的 [tb:N] 必须继续渲染成同一张图。 */
            { id:1, name:'呵呵', url:'https://raw.githubusercontent.com/267208/tieba-emoji/master/image_emoticon.png' },
            { id:2, name:'哈哈' },{ id:3, name:'吐舌' },{ id:4, name:'啊' },
            { id:5, name:'酷' },{ id:6, name:'怒' },{ id:7, name:'开心' },
            { id:8, name:'汗' },{ id:9, name:'泪' },{ id:10, name:'汗(插伞)' },
            { id:11, name:'鄙视' },{ id:12, name:'不高兴' },{ id:13, name:'真棒' },
            { id:14, name:'花心' },{ id:15, name:'疑问' },{ id:16, name:'阴脸' },
            { id:17, name:'咦' },{ id:18, name:'委屈' },{ id:19, name:'亲亲' },
            { id:20, name:'爱心眼' },{ id:21, name:'斜眼笑' },{ id:22, name:'笑' },
            { id:23, name:'太开心' },{ id:24, name:'可爱' },{ id:25, name:'勉强' },
            { id:26, name:'狂汗' },{ id:27, name:'乖' },{ id:28, name:'睡觉' },
            { id:29, name:'惊哭' },{ id:30, name:'升起' },{ id:31, name:'惊' },
            { id:32, name:'揉脸' },{ id:33, name:'鲁拉' },{ id:34, name:'点头' },
            { id:35, name:'心碎' },{ id:36, name:'玫瑰' },{ id:37, name:'礼物' },
            { id:38, name:'彩虹' },{ id:39, name:'月亮' },{ id:40, name:'太阳' },
            { id:41, name:'钱' },{ id:42, name:'灯泡' },{ id:43, name:'咖啡杯' },
            { id:44, name:'蛋糕' },{ id:45, name:'音乐' },{ id:46, name:'OK' },
            { id:47, name:'V' },{ id:48, name:'真好' },{ id:49, name:'弱' },
            { id:50, name:'OK手势' },{ id:62, name:'扭捏' },{ id:63, name:'右上蹭脸' },
            { id:64, name:'摊手' },{ id:65, name:'打滚' },{ id:66, name:'衰' },
            { id:67, name:'花痴' },{ id:68, name:'打脸' },{ id:69, name:'喷' },
            { id:70, name:'噗' },{ id:71, name:'大哭' },{ id:72, name:'哭瞎' },
            { id:73, name:'感动' },{ id:74, name:'阴' },{ id:75, name:'害羞' },
            { id:76, name:'炸弹' },{ id:77, name:'刀' },{ id:78, name:'沙发' },
            { id:79, name:'香蕉' },{ id:80, name:'便便' },{ id:81, name:'胶囊' },
            { id:82, name:'围巾' },{ id:83, name:'蜡烛' },{ id:84, name:'浮云' },
            { id:85, name:'敲打' },{ id:86, name:'西瓜' },{ id:87, name:'彩虹糖' },
            { id:88, name:'指点' },{ id:89, name:'擦汗' },{ id:90, name:'狗头' },
            { id:91, name:'阴险' },{ id:92, name:'惊恐' },{ id:93, name:'摊手②' },
            { id:94, name:'哈欠' },{ id:95, name:'哈哈哈' },{ id:96, name:'叼玫瑰' },
            { id:97, name:'怪笑' },{ id:98, name:'愤怒' },{ id:99, name:'骷髅(黑)' },
            { id:100, name:'骷髅(大笑)' },{ id:101, name:'举牌' }
        ];

        const TIEBA_CDN_BASE = 'https://raw.githubusercontent.com/267208/tieba-emoji-cdn/master/tieba/image_emoticon';
        const HUAJI_CDN_BASE = 'https://raw.githubusercontent.com/267208/tieba-emoji-cdn/master/huaji/';

        // —— 滑稽表情数据(来自 qwqcode/huaji,上传至自建 CDN) ——
        const HUAZI_EMOJI_ALL = [
            { name:'原味稽', file:'原味稽.png', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E5%8E%9F%E5%91%B3%E7%A8%BD.png' },
            { name:'还是算了', file:'还是算了.png', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E8%BF%98%E6%98%AF%E7%AE%97%E4%BA%86.png' },
            { name:'蓝纹稽', file:'蓝纹稽.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E8%93%9D%E7%BA%B9%E7%A8%BD.jpg' },
            { name:'随稽应变', file:'随稽应变.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E9%9A%8F%E7%A8%BD%E5%BA%94%E5%8F%98.jpg' },
            { name:'蠕动', file:'蠕动.gif', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E8%A0%95%E5%8A%A8.gif' },
            { name:'束手无稽', file:'束手无稽.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E6%9D%9F%E6%89%8B%E6%97%A0%E7%A8%BD.jpg' },
            { name:'微笑默叹以为妙绝', file:'微笑默叹以为妙绝.png', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E5%BE%AE%E7%AC%91%E9%BB%98%E5%8F%B9%E4%BB%A5%E4%B8%BA%E5%A6%99%E7%BB%9D.png' },
            { name:'喝嘤料', file:'喝嘤料.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E5%96%9D%E5%98%A4%E6%96%99.jpg' },
            { name:'暗中观察', file:'暗中观察.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E6%9A%97%E4%B8%AD%E8%A7%82%E5%AF%9F.jpg' },
            { name:'高兴', file:'高兴.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E9%AB%98%E5%85%B4.jpg' },
            { name:'惊稽', file:'惊稽.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E6%83%8A%E7%A8%BD.jpg' },
            { name:'可这和我的帅有什么关系', file:'可这和我的帅有什么关系.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E5%8F%AF%E8%BF%99%E5%92%8C%E6%88%91%E7%9A%84%E5%B8%85%E6%9C%89%E4%BB%80%E4%B9%88%E5%85%B3%E7%B3%BB.jpg' },
            { name:'狱稽', file:'狱稽.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E7%8B%B1%E7%A8%BD.jpg' },
            { name:'梆', file:'梆.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E6%A2%86.jpg' },
            { name:'吃鱼摆摆', file:'吃鱼摆摆.gif', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E5%90%83%E9%B1%BC%E6%91%86%E6%91%86.gif' },
            { name:'跃跃欲试', file:'跃跃欲试.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E8%B7%83%E8%B7%83%E6%AC%B2%E8%AF%95.jpg' },
            { name:'跃跃欲试 2', file:'跃跃欲试 2.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E8%B7%83%E8%B7%83%E6%AC%B2%E8%AF%95_2.jpg' },
            { name:'跃跃欲试 3', file:'跃跃欲试 3.gif', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E8%B7%83%E8%B7%83%E6%AC%B2%E8%AF%95_3.gif' },
            { name:'突然滑稽', file:'突然滑稽.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E7%AA%81%E7%84%B6%E6%BB%91%E7%A8%BD.jpg' },
            { name:'扶墙怂', file:'扶墙怂.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E6%89%B6%E5%A2%99%E6%80%82.jpg' },
            { name:'阔以', file:'阔以.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E9%98%94%E4%BB%A5.jpg' },
            { name:'不得行', file:'不得行.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E4%B8%8D%E5%BE%97%E8%A1%8C.jpg' },
            { name:'少儿不宜', file:'少儿不宜.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E5%B0%91%E5%84%BF%E4%B8%8D%E5%AE%9C.jpg' },
            { name:'稽日可期', file:'稽日可期.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E7%A8%BD%E6%97%A5%E5%8F%AF%E6%9C%9F.jpg' },
            { name:'哎', file:'哎.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E5%93%8E.jpg' },
            { name:'别看丢人', file:'别看丢人.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E5%88%AB%E7%9C%8B%E4%B8%A2%E4%BA%BA.jpg' },
            { name:'地稽', file:'地稽.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E5%9C%B0%E7%A8%BD.jpg' },
            { name:'地稽 2', file:'地稽 2.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E5%9C%B0%E7%A8%BD_2.jpg' },
            { name:'地稽 3', file:'地稽 3.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E5%9C%B0%E7%A8%BD_3.jpg' },
            { name:'地稽委屈', file:'地稽委屈.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E5%9C%B0%E7%A8%BD%E5%A7%94%E5%B1%88.jpg' },
            { name:'地稽抚摸', file:'地稽抚摸.gif', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E5%9C%B0%E7%A8%BD%E6%8A%9A%E6%91%B8.gif' },
            { name:'地稽锤打', file:'地稽锤打.gif', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E5%9C%B0%E7%A8%BD%E9%94%A4%E6%89%93.gif' },
            { name:'老阔有点扣', file:'老阔有点扣.gif', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E8%80%81%E9%98%94%E6%9C%89%E7%82%B9%E6%89%A3.gif' },
            { name:'啊哈哈', file:'啊哈哈.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E5%95%8A%E5%93%88%E5%93%88.jpg' },
            { name:'无稽可奈', file:'无稽可奈.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E6%97%A0%E7%A8%BD%E5%8F%AF%E5%A5%88.jpg' },
            { name:'老实巴交', file:'老实巴交.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E8%80%81%E5%AE%9E%E5%B7%B4%E4%BA%A4.jpg' },
            { name:'紧张', file:'紧张.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E7%B4%A7%E5%BC%A0.jpg' },
            { name:'摇摆稽', file:'摇摆稽.gif', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E6%91%87%E6%91%86%E7%A8%BD.gif' },
            { name:'又不是不能用', file:'又不是不能用.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E5%8F%88%E4%B8%8D%E6%98%AF%E4%B8%8D%E8%83%BD%E7%94%A8.jpg' },
            { name:'一时滑稽', file:'一时滑稽.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E4%B8%80%E6%97%B6%E6%BB%91%E7%A8%BD.jpg' },
            { name:'无法接受', file:'无法接受.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E6%97%A0%E6%B3%95%E6%8E%A5%E5%8F%97.jpg' },
            { name:'嘤雄豪稽', file:'嘤雄豪稽.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E5%98%A4%E9%9B%84%E8%B1%AA%E7%A8%BD.jpg' },
            { name:'相视双稽', file:'相视双稽.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E7%9B%B8%E8%A7%86%E5%8F%8C%E7%A8%BD.jpg' },
            { name:'稽皮发麻', file:'稽皮发麻.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E7%A8%BD%E7%9A%AE%E5%8F%91%E9%BA%BB.jpg' },
            { name:'绝望', file:'绝望.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E7%BB%9D%E6%9C%9B.jpg' },
            { name:'气稽败坏', file:'气稽败坏.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E6%B0%94%E7%A8%BD%E8%B4%A5%E5%9D%8F.jpg' },
            { name:'当场去世', file:'当场去世.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E5%BD%93%E5%9C%BA%E5%8E%BB%E4%B8%96.jpg' },
            { name:'喝酒', file:'喝酒.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E5%96%9D%E9%85%92.jpg' },
            { name:'老衲摆摊算命', file:'老衲摆摊算命.gif', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E8%80%81%E8%A1%B2%E6%91%86%E6%91%8A%E7%AE%97%E5%91%BD.gif' },
            { name:'老哥,稳', file:'老哥,稳.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E8%80%81%E5%93%A5%EF%BC%8C%E7%A8%B3.jpg' },
            { name:'自闭稽', file:'自闭稽.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E8%87%AA%E9%97%AD%E7%A8%BD.jpg' },
            { name:'无话可说', file:'无话可说.jpg', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E6%97%A0%E8%AF%9D%E5%8F%AF%E8%AF%B4.jpg' },
            { name:'满脑子骚操作', file:'满脑子骚操作.gif', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E6%BB%A1%E8%84%91%E5%AD%90%E9%AA%9A%E6%93%8D%E4%BD%9C.gif' },
            { name:'稽之舞', file:'稽之舞.gif', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E7%A8%BD%E4%B9%8B%E8%88%9E.gif' },
            { name:'将稽就稽', file:'将稽就稽.gif', url:'https://cdn.jsdelivr.net/gh/qwqcode/huaji/%E5%B0%86%E7%A8%BD%E5%B0%B1%E7%A8%BD.gif' }
        ];

        // —— CSS 注入 ——
        GM_addStyle(`
            /* 表情 fieldset 整体美化 — 铺满右侧 */
            /* 需要用 text-align/font 重置覆盖主题 body 继承 */
            fieldset.spp-emoji-fieldset {
                border: 1px solid var(--spu-border, #E8E3D8) !important;
                border-radius: var(--spu-radius, 8px) !important;
                padding: 10px 12px 12px !important;
                margin: 0 !important;
                background: var(--spu-surface, #FDFBF7) !important;
                width: auto !important;
                min-width: 260px !important;
                max-height: 100% !important;
                overflow: hidden !important;
                text-align: left !important;
                font-family: 'Noto Sans SC','Source Han Sans SC','PingFang SC','Microsoft YaHei',sans-serif !important;
                font-size: 13px !important;
                line-height: 1.5 !important;
                box-sizing: border-box !important;
            }
            /* ★ 2.52.91「美化排版位置」(2.54.9 重做):这枚 legend 里住的已经是
               两枚 UI(emoji / 贴纸),位置仍在左上。
               ★★ 2.54.9(用户:「两个小 UI 被一个大的胶囊包裹,该胶囊不需要,两个小 UI
               通过左右两侧的实线连接在一起」):legend 自己那层胶囊(描边 + 10% 强调色底
               + 999px)整个撤掉,只留一颗**透明**的 flex 容器 —— 看得见的形状全交给里面
               那两枚 UI:它们共边、左右两端收圆头、中间共用一条竖实线,连成一体。 */
            fieldset.spp-emoji-fieldset > legend {
                display: inline-flex !important;
                align-items: center !important;
                gap: 0 !important;
                padding: 0 !important;
                border: none !important;
                border-radius: 0 !important;
                background: none !important;
                font-size: 11px !important;
                font-weight: 700 !important;
                color: var(--spu-accent, #2f6f68) !important;
                letter-spacing: 0.08em !important;
                line-height: 1.4 !important;
            }
            /* 分类标签容器(2.54.8 起它在 legend 里 —— 那行下边框与下外边距在 legend 里
               只会把胶囊条顶高,所以用 --inlegend 一律复位) */
            .spp-emoji-tabs {
                display: flex !important;
                flex-wrap: wrap !important;
                gap: 3px !important;
                padding: 2px 0 4px !important;
                border-bottom: 1px solid var(--spu-border,#E8E3D8) !important;
                margin-bottom: 4px !important;
            }
            .spp-emoji-tabs.spp-emoji-tabs--inlegend {
                padding: 0 !important;
                border-bottom: none !important;
                margin: 0 !important;
                gap: 0 !important;
            }
            /* ★★ 2.54.9:两枚 UI 连成一体 —— 各自只留 1px 实线描边;左一枚收左圆头、
               右一枚收右圆头,中间那两笔重合成一条竖线(后一枚压掉 1px),
               于是「左右两侧的实线」把它们连成一颗**没有底色**的分段控件。 */
            .spp-emoji-tab {
                position: relative !important;
                padding: 2px 11px !important;
                font-size: 11px !important;
                line-height: 1.5 !important;
                border: 1px solid color-mix(in srgb, var(--spu-accent,#C75B39) 45%, transparent) !important;
                border-radius: 0 !important;
                background: var(--spu-surface,#fff) !important;
                color: var(--spu-muted,#999) !important;
                cursor: pointer !important;
                transition: all 0.15s ease !important;
                user-select: none !important;
                white-space: nowrap !important;
            }
            .spp-emoji-tabs--inlegend .spp-emoji-tab:first-child { border-radius: 999px 0 0 999px !important; }
            .spp-emoji-tabs--inlegend .spp-emoji-tab:last-child {
                border-radius: 0 999px 999px 0 !important;
                margin-left: -1px !important;   /* 中间那条竖线只画一次 */
            }
            .spp-emoji-tab:hover {
                border-color: var(--spu-accent,#C75B39) !important;
                color: var(--spu-accent,#C75B39) !important;
                background: color-mix(in srgb,var(--spu-accent,#C75B39) 8%,transparent) !important;
                z-index: 1 !important;
            }
            .spp-emoji-tab.spp-active {
                background: var(--spu-accent,#C75B39) !important;
                color: #fff !important;
                border-color: var(--spu-accent,#C75B39) !important;
                font-weight: 600 !important;
                z-index: 2 !important;
            }
            /* ★★ 2.54.9(用户:「在两个小 UI 下方增加一个选择区,通过该选择区进行勾选,
               实现分页和跳转」+「里面的选项可能会比较多,要做好支持翻页的准备」):
               选择区 = legend 与网格之间那一条分类胶囊。点一枚 = 勾选(对号 + 强调色
               描边)= 只看这一类(分页),网格同时滚回顶(跳转);「全部」是默认那枚。
               ★ 自带翻页:胶囊超一屏时**在选择区上滚滚轮就翻页**(★ 2.55.17 把页码牌
                 撤掉,改成滚轮 —— 见 --track 与 JS 里 catsBar 的 wheel 处理)。 */
            /* ★★ 2.55.0(用户:「勾选面板尺寸需调大,目前仅有一行,要弄成多行的样式,
               不要显得太紧凑」):选择区从「单行横向滚动」改成**多行换行** ——
               胶囊铺不下就自动落下一行,一行放两三枚,字号与内边距一起放大。
               超过 3 行才限高,并按「可视行」竖着翻。
               ★★ 2.55.17(用户:「无论是 emoji 还是贴纸都通过滚轮翻页,不需要下方的
               1、2 点击翻页」):2.55.10 那排**页码牌整个撤掉**(.spp-emoji-cat-pager /
               .spp-emoji-cat-pnum 一并删干净,不留没人用的选择器),翻页改在选择区本身上
               滚滚轮 —— 一个滚轮格 = 一页。 */
            .spp-emoji-cats {
                /* ★ 2.55.10 起这条是竖排(选项在上、页码在下);★ 2.55.17 把页码那一排
                   撤掉后只剩轨道一条 —— 盒子模型一字不动,少改一处少一处风险。 */
                display: flex !important;
                flex-direction: column !important;
                align-items: stretch !important;
                gap: 4px !important;
                padding: 0 0 5px !important;
                margin: 0 0 5px !important;
                border-bottom: 1px solid var(--spu-border,#E8E3D8) !important;
            }
            .spp-emoji-cats-track {
                display: flex !important;
                flex-wrap: wrap !important;
                align-items: center !important;
                gap: 4px !important;
                flex: 1 1 auto !important;
                min-width: 0 !important;
                overflow: hidden !important;
                scroll-behavior: smooth !important;
                scrollbar-width: none !important;
            }
            .spp-emoji-cats-track::-webkit-scrollbar { height: 0 !important; }
            /* ★★ 2.55.17(用户:「无论是 emoji 还是贴纸都通过滚轮翻页,不需要下方的
               1、2 点击翻页」):.spp-emoji-cat-pager / .spp-emoji-cat-pnum 两条
               规则(那排「1」「2」页码牌)整块撤掉 —— 翻页改由选择区自己的 wheel
               处理接管,页面里不再有任何页码节点(不留没人用的选择器)。
               见 JS 段 catsBar.addEventListener('wheel', …)。 */
            .spp-emoji-cat {
                display: inline-flex !important;
                align-items: center !important;
                gap: 5px !important;
                flex: 0 0 auto !important;
                padding: 3px 10px !important;
                font-size: 11px !important;
                line-height: 1.5 !important;
                color: var(--spu-muted,#9B9489) !important;
                background: var(--spu-surface,#fff) !important;
                border: 1px solid var(--spu-border,#E8E3D8) !important;
                border-radius: 3px !important;
                cursor: pointer !important;
                user-select: none !important;
                white-space: nowrap !important;
                transition: all 0.15s ease !important;
            }
            .spp-emoji-cat > i { font-style: normal !important; opacity: 0.7 !important; }
            .spp-emoji-cat:hover {
                border-color: var(--spu-accent,#C75B39) !important;
                color: var(--spu-accent,#C75B39) !important;
            }
            .spp-emoji-cat.spp-active {
                background: color-mix(in srgb, var(--spu-accent,#C75B39) 14%, #fff) !important;
                border-color: color-mix(in srgb, var(--spu-accent,#C75B39) 55%, #fff) !important;
                color: var(--spu-accent,#C75B39) !important;
                font-weight: 700 !important;
            }
            .spp-emoji-cat.spp-active > b::before { content: '✓ ' !important; }   /* 勾选态 */
            /* ★★ 2.54.6(用户:「去除表情的栏目的搜索功能」):搜索框整条撤掉,
               CSS 一并删干净(不留没人用的选择器)。 */
            /* ★★ 2.54.6(用户截图红框:「UI 重合」):站点那枚「[更多]」的容器
               (<span><a id="td_face">[更多]</a></span>)在 legend 右边摆成一个空胶囊 ——
               站点原生 5 类早就是本栏 emoji 页的一部分,这枚链接没有任何用处。
               它本体是 display:none,空框来自外层这个顶层 span ⇒ 整条撤出布局。
               (原生浮层 body > div.menu 仍由 homeNative() 兜着,与这枚链接无关。) */
            fieldset.spp-emoji-fieldset > span { display: none !important; }
            /* 表情网格 */
            .spp-emoji-grid {
                display: flex !important;
                flex-wrap: wrap !important;
                /* ★★ 2.55.0(用户:「部分表情包的间隔是你人为加长的,这部分我不需要」):
                   网格是定高(min/max 都由 syncEmojiLayout 写死)的 flex 容器,
                   而 align-content 默认是 normal(= stretch)⇒ 行数少时浏览器把
                   每一行**拉伸**填满整个容器,于是行与行之间出现成片空档。
                   改成 flex-start:行紧贴顶部、行距回到 gap 那 2px。 */
                align-content: flex-start !important;
                gap: 2px !important;
                padding: 0 !important;
                overflow-y: auto !important;
                scrollbar-width: thin !important;
                scrollbar-color: var(--spu-border,#ddd) transparent !important;
            }
            .spp-emoji-grid::-webkit-scrollbar { width: 4px !important; }
            .spp-emoji-grid::-webkit-scrollbar-thumb {
                background: var(--spu-border,#ddd) !important; border-radius: 2px !important;
            }
            /* ★★ 2.54.8(用户:「emoji 的分类要能看出来;贴纸的分类也要清晰」):
               网格里每一类的分组头 —— 满宽(flex:0 0 100% 在 flex-wrap 里独占一行)、
               滚动时贴顶(position:sticky 对最近的滚动祖先 = 网格自己生效),
               底下一道细线,图多的时候也知道自己翻到哪一类了。 */
            .spp-emoji-grid .spp-emoji-group {
                flex: 0 0 100% !important;
                position: sticky !important;
                top: 0 !important;
                z-index: 1 !important;
                display: flex !important;
                align-items: center !important;
                gap: 6px !important;
                box-sizing: border-box !important;
                margin: 1px 0 !important;
                padding: 1px 6px !important;
                font-size: 10px !important;
                font-weight: 600 !important;
                letter-spacing: 0.06em !important;
                color: var(--spu-muted,#9B9489) !important;
                background: var(--spu-overlay,#F0EBE1) !important;
                border-bottom: 1px solid var(--spu-border,#E8E3D8) !important;
                border-radius: 3px !important;
            }
            .spp-emoji-grid .spp-emoji-group > i {
                font-style: normal !important;
                font-weight: 500 !important;
                opacity: 0.72 !important;
            }
            /* ★★ 2.55.9:动画格显示的是 canvas(静止首帧),选择器要把 canvas 一起认了。 */
            .spp-emoji-grid img,
            .spp-emoji-grid canvas {
                width: 26px !important; height: 26px !important;
                object-fit: contain !important;
                cursor: pointer !important;
                border-radius: 4px !important;
                border: 2px solid transparent !important;
                padding: 1px !important;
                margin: 0 !important;
                transition: transform 0.12s ease, border-color 0.12s ease, background 0.12s ease !important;
            }
            .spp-emoji-grid img:hover,
            .spp-emoji-grid canvas:hover {
                transform: scale(1.35) !important;
                z-index: 2 !important;
                border-color: var(--spu-accent, #C75B39) !important;
                background: var(--spu-overlay, #F0EBE1) !important;
                box-shadow: 0 3px 10px rgba(0,0,0,0.15) !important;
            }
            .spp-emoji-grid .spp-emoji-name {
                display: none !important;
            }
            /* 空状态 */
            .spp-emoji-empty {
                text-align: center !important;
                padding: 12px !important;
                font-size: 11px !important;
                color: var(--spu-muted,#bbb) !important;
            }
            /* [更多] 链接美化 */
            fieldset.spp-emoji-fieldset > span a#td_face,
            fieldset.spp-emoji-fieldset a#td_face {
                display: inline-block !important;
                font-size: 11px !important;
                color: var(--spu-accent, #C75B39) !important;
                border: 1px dashed var(--spu-border, #ddd) !important;
                border-radius: 4px !important;
                padding: 1px 8px !important;
                margin-top: 4px !important;
                text-decoration: none !important;
                transition: all 0.15s ease !important;
                cursor: pointer !important;
            }
            fieldset.spp-emoji-fieldset > span a#td_face:hover,
            fieldset.spp-emoji-fieldset a#td_face:hover {
                background: var(--spu-accent, #C75B39) !important;
                color: #fff !important;
                border-style: solid !important;
                border-color: var(--spu-accent, #C75B39) !important;
            }
            /* 2.52.51:5 类原生表情已经变成我们自己的标签页,站点那个 [更多] 浮层不再需要 ——
               直接收掉入口,省得栏目里又冒出一整块。
               (homeNative() 保留当保险:万一还有别的路径把它调出来,也不会浮在回复框上。) */
            fieldset.spp-emoji-fieldset > span a#td_face { display:none !important; }
            /* ===== 2.52.49:站点原生表情面板(点 [更多] 弹出的 body > div.menu)搬进表情栏 =====
               ★ 它原本绝对定位在下方的回复框上(实测 left:1189px top:3225px 321x243),还会被回复区裁掉一截。
                 homeNative() 把它 appendChild 进本栏后,这里负责把绝对定位 / 深色底 / 固定宽高改回栏内自适应。
               ★ 选择器必须带 .spp-face-native:主题那条 body > div.menu { background:#0b1119 !important } 是 (0,1,1),
                 而 .spp-face-native 只有 (0,1,0);div.menu.spp-face-native 才是 (0,2,1)。 */
            .spp-face-native-host { display:block !important; margin-top:6px !important; }
            fieldset.spp-emoji-fieldset.spp-face-native-on { overflow-y:auto !important; }
            html body div.menu.spp-face-native {
                position:static !important; left:auto !important; top:auto !important; right:auto !important; bottom:auto !important;
                width:100% !important; max-width:100% !important; min-width:0 !important;
                height:auto !important; max-height:none !important;
                margin:0 !important; padding:0 !important; opacity:1 !important;
                background:none !important; background-image:none !important;
                border:0 !important; border-radius:0 !important; box-shadow:none !important;
                overflow:hidden !important; z-index:auto !important; box-sizing:border-box !important;
                color:var(--spu-text,#333) !important; font-size:12px !important;
            }
            html body div.menu.spp-face-native #buttons {
                display:flex !important; flex-wrap:wrap !important; align-items:center !important; gap:3px !important;
                width:100% !important; height:auto !important; padding:0 !important; overflow:hidden !important;
            }
            html body div.menu.spp-face-native #buttons > span {
                float:none !important; display:inline-flex !important; align-items:center !important;
                margin:0 !important; width:auto !important; cursor:pointer !important;
            }
            html body div.menu.spp-face-native #buttons > span img {
                width:13px !important; height:13px !important; opacity:.7 !important;
                filter:invert(1) brightness(.35) !important;
            }
            html body div.menu.spp-face-native #buttons > span:hover img { opacity:1 !important; }
            html body div.menu.spp-face-native #buttons > div {
                float:none !important; display:inline-block !important; flex:0 0 auto !important;
                width:auto !important; min-width:0 !important; height:22px !important;
                padding:0 8px !important; margin:0 !important; box-sizing:border-box !important;
                font-size:11.5px !important; line-height:20px !important; text-align:center !important;
                color:var(--spu-muted,#6a7b90) !important; background:none !important; background-image:none !important;
                border:1px solid var(--spu-border,#E8E3D8) !important; border-radius:9px !important;
                cursor:pointer !important; white-space:nowrap !important; user-select:none !important;
            }
            html body div.menu.spp-face-native #buttons > div:hover {
                border-color:var(--spu-accent,#C75B39) !important; color:var(--spu-accent,#C75B39) !important;
            }
            html body div.menu.spp-face-native #buttons > div.lian {
                color:#fff !important; background:var(--spu-accent,#C75B39) !important;
                border-color:var(--spu-accent,#C75B39) !important;
            }
            html body div.menu.spp-face-native #showface {
                width:100% !important; max-width:100% !important; height:auto !important; max-height:236px !important;
                box-sizing:border-box !important; overflow-y:auto !important; overflow-x:hidden !important;
                padding:6px 2px 2px !important; margin-top:5px !important; background:none !important;
                border-top:1px dashed var(--spu-border,#E8E3D8) !important;
            }
            html body div.menu.spp-face-native #showface img { max-width:100% !important; height:auto !important; vertical-align:middle !important; }
        `);

        // ---- non-quote:表情栏搬到编辑器【右侧】(2.52.90)----
        // 用户批注(post.php?fid-9.html 截图):蓝框圈住整块「表情」栏、蓝字「移动到右侧」、
        //   蓝箭头从左下绕到右上 —— 表情栏由「编辑器左侧」改到「编辑器右侧」。
        // ★ 实测站点布局(post.php?fid-9):
        //     #editor-side   position:absolute、width:200px、margin-top:22px,但**没有定位祖先**
        //                    (offsetParent 就是 body)⇒ 它的 left:226 其实是「静态位置」
        //                    (包裹层左内缘),并非站点写了 left;
        //     #editor-main   padding-left:265px(本模块 2.51.x 起的覆盖)⇒ 编辑器内容 491..1466。
        //   所以「换边」不能只写 left:auto + right:0:没有定位祖先时 right 是对**视口**解算的,
        //   面板会跑到 1432 那边去。必须先给包裹层 position:relative,right:0 才落在 1466。
        // ★ 包裹层一改成 relative,绝对定位的 #editor-tab / #editor-tool 不受影响 ——
        //   实测它们的 offsetParent 是 #editor-content(自身 relative),跟着编辑器内容框走。
        // ★ #editor-main 那条 padding 换成「左 0 / 右 274」:内容框 226..1192,与右侧面板
        //   (right:0 → 1206..1466)之间留 14px,与原先左栏那份间距同宽。
        // ★ 同一层还有 #postrule(「求物/询问到求物…」提示行,站点自带 margin-left:205px):
        //   不一起改的话它会停在 431 与编辑器错开,并伸到表情栏底下。
        // ★★ 换到右侧【必须同时收高】—— 第一版只换了边,实站量出来才发现压住了控件:
        //   面板自然高 579(图例 17 + 标签 84 + 搜索 24 + 表情网格 420 + 内外边距 34)。
        //   左侧原本一路空到底,右侧却压着「发帖选项」那几行的第三列 —— 实测旧面板 458..1037
        //   会盖住「Wind Code自动转换」「隐藏此帖(回复可见)」两条(文字右端 ~1245 > 面板左缘 1206)。
        //   右侧那条可用轨道 = 面板顶(距包裹层顶 69 = 站点前面两行 margin-left:205px 的 30+17
        //   + 自己的 margin-top:22)到包裹层底,实测 483。所以:
        //     ① 面板 max-height 收到 calc(100% - 69px)(100% = 包裹层高,它已被上面那条定位)
        //        + overflow-y:auto —— 兜住字体/缩放变化:宁可它自己滚,也不许压住下面;
        //     ② 表情网格 420 → 320,让面板落回 483 以内【不触发那条滚动条】:
        //        579 - 420 + 320 = 479 ≤ 483,实测面板底 = 包裹层底 941,与选项行留 13px。
        //    ★★ 收网格必须 max-height 与 min-height【一起收】:syncEmojiLayout() 是
        //       `emojiGrid.style.maxHeight = gridH; emojiGrid.style.minHeight = gridH;` 两条一起写的,
        //       只压 max-height 会被那条行内 min-height:420px 顶回来(CSS 里 min-height > max-height 时
        //       取 min-height)—— 实站第一版正是这样:computed max-height 已经是 320,
        //       实际高度仍是 420,面板照旧溢出、照旧冒滚动条。
        if (PATH.includes("post.php") && !SEARCH.includes("action-quote")) {
            GM_addStyle(`
                #editor-side { width: 260px !important; }
                #editor-side > div { width: 100% !important; }
                div:has(> #editor-side) { position: relative !important; }
                #editor-side { left: auto !important; right: 0 !important; max-height: calc(100% - 52px) !important; overflow-y: auto !important; }
                /* ★★ 2.52.91:这条的 69 变成了 52 —— 本轮把 #postrule 搬出包裹层的
                   文档流之后,面板的静态位置整段上移 17px,包裹层自身也矮了 17px。
                   重量的两个数:面板顶 441、包裹层顶 389 ⇒ 52;可用轨道 924-441 = 483。
                   ★★ 2.54.8(用户:「让它的区域更大一些」):原来这里还有一条
                   「div:has(> #editor-side) .spp-emoji-grid { max-height:316px; min-height:316px }」
                   —— 316 是 2.52.91 为「面板不许压住下面那几行」定死的,它是 !important,
                   会把 syncEmojiLayout 写进来的行内高度整个吃掉(网格永远 316)。
                   现在删掉这条:高度一律由 syncEmojiLayout 按可用轨道算,
                   #editor-side 自己的 max-height + overflow-y:auto 仍是最后那道兜底。 */
                #editor-main { padding-left: 0 !important; padding-right: 274px !important; }
                /* ★ 2.52.91:#postrule 那条「让出右侧 274px」的规则改由 POST_CSS 接手 ——
                   本轮批注要把它【搬进编辑器顶栏那一排】,搬完它就不再是包裹层的子节点,
                   这条选择器自然失效(留着容易让人以为它还在管事)。 */
            `);
        }

        // ---- quote page: narrow emoji panel ----
        if (SEARCH.includes("action-quote")) {
            GM_addStyle(`
                fieldset.spp-emoji-fieldset {
                    width: 100% !important;
                    max-width: 100% !important;
                    min-width: auto !important;
                    box-sizing: border-box !important;
                }
                /* ★★ 2.53.10(用户批注「加大表情框的大小」):引用页的表情栏实测只有 195×338、
                   网格 170×150(26px 的 5 列)—— 而面板顶到尾(选项表顶)有 504 的可用轨道。
                   两处一起放大,数字与非引用页那套对齐(260 / 274 / 14px 间隙):
                     · 栏宽 200 → 260:网格内宽 170 → 233 ⇒ 30px 的表情每行 5 → 6 枚;
                     · 高度不在这里写:网格高度由 syncEmojiLayout() 按【轨道】算(见那个函数)。 */
                #editor-side { width: 260px !important; }
                #editor-side > div { width: 100% !important; }
                #editor-main { padding-left: 274px !important; }
                .spp-emoji-grid img, .spp-emoji-grid canvas { width: 30px !important; height: 30px !important; }
            `);
        }

        // ★★ 2.54.6:那枚搜索框的构建函数随它一起撤(两页内容一眼扫完,打字不如翻)。

        // —— 在 fieldset 中注入增强 UI ——
        function tryPatch(attempt) {
            if (attempt === undefined) attempt = 0;
            if (attempt >= 60) return;
            const fieldsets = document.querySelectorAll('fieldset');
            let emojiFieldset = null;
            for (const fs of fieldsets) {
                const leg = fs.querySelector('legend');
                if (leg && leg.textContent.trim() === '表情') {
                    emojiFieldset = fs;
                    break;
                }
            }
            if (!emojiFieldset) { setTimeout(() => tryPatch(attempt + 1), 300); return; }
            if (emojiFieldset.dataset.sppEmoji) return;
            emojiFieldset.dataset.sppEmoji = '1';
            emojiFieldset.classList.add('spp-emoji-fieldset');
            // 强制 box-sizing: border-box(防止 fieldset 默认 content-box 导致宽度溢出)
            emojiFieldset.style.boxSizing = 'border-box';

            const textarea = document.querySelector('textarea[name="atc_content"]');
            if (!textarea) { setTimeout(() => tryPatch(attempt + 1), 300); return; }

            // 获取原始表情网格
            const menuShow = emojiFieldset.querySelector('#menu_show');
            const moreLink = emojiFieldset.querySelector('a#td_face');

            // ── 单一面网格容器(替换 menuShow 作为唯一表情展示区)──
            const emojiGrid = document.createElement('div');
            emojiGrid.className = 'spp-emoji-grid';
            // 默认表情数据(克隆原始 img,保留原生 onclick 行为)
            const defaultEmojiData = [];
            if (menuShow) {
                menuShow.querySelectorAll('img').forEach(function(origImg) {
                    defaultEmojiData.push({
                        type: 'native',  // 原生表情,直接克隆 DOM
                        el: origImg       // 保留原始元素引用
                    });
                });
                menuShow.style.display = 'none';
            }

            // ── 2.52.51:站点原生 5 个分类的数据源 ──
            //    face.js 给的是 picpath / faces / face 三个全局,站点的 showFace() 只是把
            //    faces[path] 渲染成 <img src=picpath+face[sid][0] onclick=addsmile(sid)>;
            //    这里照抄同一套数据 —— 于是这 5 类可以直接变成我们自己的标签页,不必再弹那个浮层。
            const NATIVE_PACKS = [
                { key: 'smallface', name: '小圆脸' },
                { key: 'miao',      name: '\u00b4\u30fb\u03c9\u30fb\x60' },
                { key: 'kaoani',    name: '颜アニ' },
                { key: 'kaomoji',   name: '颜文字' },
                { key: 'de',        name: '杂项' }
            ];
            /* ★★ 2.55.9(用户:「能不能再增加一些 emoji 或是贴纸?」):emoji 页的
               **第三路**数据源 —— 第三方纯 URL 套系。原来这页只有两路:站点原生 5 类
               (点一下走站点自己的 addsmile(sid))与贴吧表情(点一下插 [img])。
               自备套系属于后者那一类,但数据不在 TIEBA_EMOJI_ALL 里,所以单列一张表。 */
            const CUSTOM_EMOJI_PACKS = [
                { key: 'omega-emoji', name: 'ω', list: PACK_EMOJI_OMEGA }
            ];
            function nativePackData(key) {
                if (typeof faces === 'undefined' || typeof face === 'undefined' || !faces[key]) return null;
                const pp = (typeof picpath === 'string' && picpath) ? picpath : 'images/post/smile/';
                return faces[key].map(function (sid) {
                    const meta = face[sid] || [];
                    return { id: sid, src: pp + (meta[0] || ''), title: String(meta[1] || '') };
                });
            }

            // ── 标签容器 ──
            const tabs = document.createElement('div');
            tabs.className = 'spp-emoji-tabs';

            /* ★★ 2.54.6(用户:「表情的UI改为emoji和贴纸(以这个为准);去除表情的栏目的搜索功能;
               把滑稽原图移动到贴纸分类」):整栏收成两页 ——
                 · emoji  = 站点原生 5 类(小圆脸 / ´・ω・` / 颜アニ / 颜文字 / 杂项)+ 贴吧表情。
                            原生那几类点一下走站点自己的 addsmile(sid);贴吧那套插 [img]。
                            face.js 缺席时退回 #menu_show 里那排默认表情的克隆。
                 · 贴纸   = 滑稽整套(HUAZI_EMOJI_ALL)。原来「滑稽表情」(自建 CDN)与
                            「滑稽原图」(jsdelivr)是**同一张表的两条来路**,现在并成一栏:
                            主源走自建 CDN,那张图挂了就自动换回 jsdelivr 那张原图
                            ——「滑稽原图」于是自然归到贴纸页。
               搜索框整个撤掉(两页就这么点东西,翻一眼比打字快)。 */
            /* ★★ 2.55.9(用户:「动图在表情栏里 —— 这个可以采用静态图」):
               动画 webp / gif 在网格里只画**静止首帧**。做法:照常把那张图载进来,等它
               解码完,把首帧画进 52×52 的 canvas(2× 于 26px 的格子,够 hover 放大),
               再把 <img> 从文档里换掉 —— 图一离开文档,动画就停了,CPU 也还回去了。
               ★ 为什么不让 canvas 变成 data URL 再喂回 <img>:inari 不给 CORS 头,
                 跨域画布是「脏」的,getImageData / toDataURL 会抛 SecurityError;
                 但 drawImage 本身允许,所以 canvas 必须留在 DOM 里当显示元素。
               ★ 真机实测(420 个 = 贴纸页「全部」那一档的最坏格数):29ms 建完、
                 零上下文丢失,420 个全部正常显示首帧。
               ★ 静态图(280 张)完全不走这条路 —— 还是普通 <img>,一个 canvas 都不建。 */
            function staticizeAnimated(img, onClick) {
                const w = img.naturalWidth, h = img.naturalHeight;
                if (!w || !h) return;
                const BOX = 52;
                const k = Math.min(BOX / w, BOX / h);
                const dw = Math.max(1, Math.round(w * k)), dh = Math.max(1, Math.round(h * k));
                const cv = document.createElement('canvas');
                cv.width = BOX; cv.height = BOX;
                cv.className = img.className;
                if (img.title) cv.title = img.title;
                /* ★ 2.55.14:提示文本从原生 title 换成了 data-spp-tip(美化过的卡片),
                   换元素时一并搬走 —— 不搬,动图静态化之后就再也弹不出名字了。 */
                if (img.dataset.sppTip) cv.dataset.sppTip = img.dataset.sppTip;
                const ctx = cv.getContext('2d');
                if (!ctx) return;
                try { ctx.drawImage(img, (BOX - dw) / 2, (BOX - dh) / 2, dw, dh); }
                catch (e) { return; }   /* 画不出来就留着那张会动的 img,别把格子弄没了 */
                cv.addEventListener('click', onClick);
                if (img.parentNode) img.parentNode.replaceChild(cv, img);
                img.removeAttribute('src');   /* 元素已在文档外,再断一次 src 让它尽早被回收 */
            }
            function appendEmojiImg(src, alt, onErr, onClick, isAnim) {
                const img = document.createElement('img');
                img.src = src;
                img.alt = alt;
                /* ★ 2.53.97:不勾就不写提示 ⇒ 不弹名称框。
                   ★★ 2.55.14:弹的那一枚从**浏览器原生 title** 换成脚本自绘的提示卡 ——
                     原生灰框的配色 / 圆角 / 字号全跟本站主题无关,美化不了,只能自己画。
                     挂 data-spp-tip,由网格上那份委托监听(SPP_TIP.delegate)统一摆位。 */
                if (EMOJI_TIP) img.setAttribute('data-spp-tip', alt);
                img.loading = 'lazy';
                img.addEventListener('error', function () {
                    if (onErr && onErr(img)) return;
                    img.style.display = 'none';
                });
                img.addEventListener('click', onClick);
                if (isAnim) img.addEventListener('load', function () { staticizeAnimated(img, onClick); }, { once: true });
                emojiGrid.appendChild(img);
                return img;
            }
            function insertEmojiTag(url) {
                const tag = '[img]' + url + '[/img]';
                const s = textarea.selectionStart;
                const e = textarea.selectionEnd;
                textarea.value = textarea.value.substring(0, s) + tag + textarea.value.substring(e);
                const np = s + tag.length;
                textarea.setSelectionRange(np, np);
                textarea.focus();
            }
            /* ★★ 2.54.8(用户:「贴纸的分类也要清晰,因为后续还会增加贴纸种类」):
               贴纸按**套系**列 —— 加新贴纸只往 STICKER_PACKS 里添一项 { key, name, list },
               分组头与张数自己就出来了,渲染那边一个字都不用改。 */
            /* ★★ 2.55.9:贴纸页多了四套 —— 数据源是文件作用域的 PACK_*(从
               脚本-南+\表情包\帖纸\ 那四个 txt 原文生成)。这套数组本来就是为
               「以后加贴纸只往数组里添一项」设计的:分类胶囊、分组头、张数、「全部」
               的总数全是从它现推的,渲染那边一个字都没改。 */
            const STICKER_PACKS = [
                { key: 'huaji', name: '滑稽', list: HUAZI_EMOJI_ALL },
                { key: 'omega-sticker', name: 'ω', list: PACK_STICKER_OMEGA },
                { key: 'daidaiwei', name: '呆呆唯', list: PACK_DAIDAIWEI },
                { key: 'maoerxiaoyi', name: '猫儿小忆', list: PACK_MAOERXIAOYI },
                { key: 'shishouxiang', name: '是,首相', list: PACK_SHISHOUXIANG }
            ];
            /* ★★ 2.55.11:面板里粘进来的自备套系并进两页 —— 数据在文件作用域已经解析好
               (模块 3 也要用那一份,见 SPP_CUSTOM_PACKS),这里只做合并。
               ★ 默认落贴纸页,带 [emoji] 前缀的落 emoji 页。
               ★ 并进来之后,选择区的胶囊、分组头、张数、两页的「全部」与两张提示卡
                 全部**自动**带上它们 —— 那几处都是从这两张表现推的,一个字都不用改
                 (2.55.9 加那五套时特意留的就是这个口子)。 */
            SPP_CUSTOM_PACKS.emoji.forEach(function (p) { CUSTOM_EMOJI_PACKS.push(p); });
            SPP_CUSTOM_PACKS.sticker.forEach(function (p) { STICKER_PACKS.push(p); });
            /* ★★ 2.54.9(用户:「在两个小 UI 下方增加一个选择区,通过该选择区进行勾选,
               实现分页和跳转」):选择区 = 两页各自那一排**分类胶囊**(住在 legend 与
               网格之间)。点一枚就只看这一类(分页),网格同时滚回顶(跳转);
               「全部」是默认那一枚,这时照旧按分组头把每一类铺满。
               ★ 类的出处与网格同一处:emoji 页取 NATIVE_PACKS + 贴吧表情,
                 贴纸页取 STICKER_PACKS ⇒ 以后加套系 / 加分类,这里自动多一枚。 */
            const CAT_ALL = '__all__';
            /* ★★ 2.55.0(用户:「有一个默认打开选项,emoji 使用的是默认打开的小圆脸,
               贴纸默认打开的是滑稽」):两页各自记一枚默认类 —— 进面板就直接看到
               内容,不必先点一下。「全部」仍在选择区里,只是不再是默认那一枚;
               数据没拿到(face.js 缺席 / 套系为空)时自动退回「全部」,不留白屏。 */
            const CAT_DEF = { emoji: 'smallface', sticker: 'huaji' };
            let curCat = CAT_DEF.emoji;
            const catsBar = document.createElement('div');
            catsBar.className = 'spp-emoji-cats';
            /* ★★ 2.54.9 **翻页预案**(用户:「里面的选项可能会比较多,所以要做好支持翻页的
               准备」)+ ★★ 2.55.17(用户:「框选部分,无论是 emoji 还是贴纸都通过滚轮
               翻页,不需要下方的 1、2 点击翻页」):胶囊超过 3 行时选择区自己限高,
               **滚轮**在上面滚一格翻一页(2.55.10 那排页码牌已撤掉)。
               翻一页 = 一屏「可视行」,并对齐到某一枚胶囊的上缘(不会把一枚切成两半);
               到头了(第 1 页再往上 / 最后一页再往下)就把滚轮还给页面,不把页面焊死。 */
            const catsTrack = document.createElement('div');
            catsTrack.className = 'spp-emoji-cats-track';
            catsBar.appendChild(catsTrack);   // ★ 2.55.17:选择区里只剩轨道一条
            /* ★ 2.55.17:位置反馈走悬停提示(页码牌已撤掉)—— 文案由 catsSync 现算现写,
               这里只把监听挂上(attach 之后改 data-spp-tip 即可:place() 每次现读)。 */
            SPP_TIP.attach(catsBar, '滚轮翻页');
            let catsPages = 0;     // 上一帧算出的总页数:只有它变了才重写悬停提示
            let catsLastPage = 0;  // 上一帧算出的当前页:同上(少写一次 DOM 属性)
            let catsLastCount = -1; // 上一帧的分类数:换页时页数/当前页都可能不变,它必须也进备忘
            let catsSeek = 0;       // 刚点下的目标页(平滑滚动期间不让 scroll 抢回去)
            let catsSeekUntil = 0;
            const CATS_VIS_ROWS = 3;   // ★ 2.55.0:多行选择区最多露出 3 行,再多才翻页
            let catsLastH = 0;
            function catsRowH() {
                const c = catsTrack.children[0];
                return (c ? c.offsetHeight : 20) + 4;   // 胶囊高 + 行距(gap 4)
            }
            function catsRows() {
                return Math.max(1, Math.round(catsTrack.scrollHeight / catsRowH()));
            }
            /* 一帧几何:要不要翻页 / 一页多高 / 共几页 —— catsSync 与滚轮共用一份,
               免得两处各算一遍(2.55.10 起 visH 的口径就是「3 行减去行距 4」)。 */
            function catsGeom() {
                const rows = catsRows();
                const more = rows > CATS_VIS_ROWS;
                const visH = more ? CATS_VIS_ROWS * catsRowH() - 4 : catsTrack.clientHeight;
                return { more: more, visH: visH, pages: more ? Math.ceil(rows / CATS_VIS_ROWS) : 1 };
            }
            /* 当前页(1 基):按「哪一页的目标 scrollTop 离现在的 scrollTop 最近」判 ——
               最后一页常被 scrollHeight 夹住(目标值 < 页序 × visH),用整除算会少算一页。
               ★ 刚滚出去的那一段(catsSeek)优先:平滑滚动途中 scrollTop 还没到位,
                 不听它,否则连滚两格会原地打转(2.55.10 那版就是这样写反过一次)。
               ★★ 2.55.17 改法:到达即清(见 catsSync),另挂 1.5s 安全阀。
               ★ 为什么不能只靠定时(原来是 600ms 过期):平滑滚动在后台页 / 繁忙页上会
                 被节流 —— 实测这台浏览器里一次 80px 的平滑滚动能拖到 2 秒以上;定时一过期
                 就在动画还没落地时报回旧页,连滚两格于是变成「原地打转 + 提示卡在旧页」。 */
            function catsCurPage(g) {
                g = g || catsGeom();
                if (!g.more) return 1;
                if (catsSeek) {
                    if (Date.now() < catsSeekUntil) return catsSeek;
                    catsSeek = 0;                        // 安全阀到点还没滚到 ⇒ 放弃目标,回到实测
                }
                const maxTo = Math.max(0, catsTrack.scrollHeight - g.visH);
                let best = 1;
                let bestD = Infinity;
                for (let i = 0; i < g.pages; i++) {
                    const d = Math.abs(Math.min(maxTo, i * g.visH) - catsTrack.scrollTop);
                    if (d < bestD) { bestD = d; best = i + 1; }
                }
                return best;
            }
            function catsSync() {
                const g = catsGeom();
                catsTrack.style.maxHeight = g.more ? g.visH + 'px' : '';
                /* ★ 2.55.17:目标页「到达即清」—— 平滑滚动一落到目标(±1px)就把 catsSeek
                   交还实测;动画被节流时也不会永久挂着(catsSeekUntil 是安全阀)。 */
                if (catsSeek) {
                    const to = Math.min(Math.max(0, catsTrack.scrollHeight - g.visH), (catsSeek - 1) * g.visH);
                    if (Math.abs(catsTrack.scrollTop - to) <= 1 || Date.now() >= catsSeekUntil) catsSeek = 0;
                }
                /* ★★ 2.55.17:页码牌撤掉之后,位置反馈只剩悬停提示这一处 ——
                   只在「页数 / 当前页 / 分类数」有变时写属性(滚动一路在响,别每帧碰 DOM)。
                   ★ 分类数也必须进备忘:换页(emoji ⇄ 贴纸)时页数与当前页都可能不变
                     (两页都是 2 页、都在第 1 页),只比那两项的话提示卡会一直挂着上一页
                     的「共 12 类」(实测踩到过)。 */
                const nChips = catsTrack.children.length;
                const cur = catsCurPage(g);
                if (g.pages !== catsPages || cur !== catsLastPage || nChips !== catsLastCount) {
                    catsPages = g.pages;
                    catsLastPage = cur;
                    catsLastCount = nChips;
                    if (g.pages > 1) {
                        catsBar.setAttribute('data-spp-tip', '滚轮翻页 · 第 ' + cur + '/' + g.pages +
                            ' 页 · 共 ' + nChips + ' 类');
                    } else {
                        catsBar.removeAttribute('data-spp-tip');   // 一页装得下:不留提示
                    }
                }
                // 兜底那 3 行与多行的总高不一样 ⇒ 选择区高度一变就把网格重算一遍
                if (catsBar.offsetHeight !== catsLastH) {
                    catsLastH = catsBar.offsetHeight;
                    if (typeof syncEmojiLayout === 'function') syncEmojiLayout();
                }
            }
            /* ★★ 2.55.17(用户:「框选部分,无论是 emoji 还是贴纸都通过滚轮翻页,不需要
               下方的 1、2 点击翻页」):翻页改由选择区自己的滚轮接管 —— 一格一页。
               ★ 两页共用这一条:emoji 与贴纸的分类胶囊都铺在同一个 catsTrack 里,翻页
                 走的是同一条路(用户点名「无论是 emoji 还是贴纸」,不必各做一份)。
               ★ 触控板一帧常常只有几 px ⇒ 先攒(|acc| ≥ 20 才算一格),翻一页后 150ms
                 内不再受理:否则一次拨动会连翻三页。
               ★ 滚到两头就把事件**还给页面**(不 preventDefault)—— 选择区不能吃掉整页
                 的滚动;只有「确实还能翻」的时候才拦。
               ★ passive:false 必须显式写:不写的话 Chrome 在部分容器上按被动处理,
                 preventDefault 会失效(滚轮翻页与页面滚动会同时发生)。 */
            let catsWheelAcc = 0;
            let catsWheelLock = 0;
            catsBar.addEventListener('wheel', function (e) {
                const g = catsGeom();
                if (!g.more) return;                       // 一页装得下:滚轮照旧归页面
                /* deltaMode:0 = 像素、1 = 行、2 = 页 —— 先折成像素口径再判方向与攒量 */
                const d = e.deltaMode === 1 ? e.deltaY * 16
                    : (e.deltaMode === 2 ? e.deltaY * Math.max(1, catsTrack.clientHeight) : e.deltaY);
                if (!d) return;
                const cur = catsCurPage(g);
                if ((d > 0 && cur >= g.pages) || (d < 0 && cur <= 1)) return;
                e.preventDefault();
                if (e.stopPropagation) e.stopPropagation();
                const now = Date.now();
                if (now < catsWheelLock) return;
                catsWheelAcc += d;
                if (Math.abs(catsWheelAcc) < 20) return;
                catsWheelAcc = 0;
                catsWheelLock = now + 150;
                catsGoTo(cur + (d > 0 ? 1 : -1));
            }, { passive: false });
            /* 跳到第 p 页(1 基):一页 = CATS_VIS_ROWS 个「可视行」,对齐到某一枚胶囊
               的上缘,不会把一枚切成两半。 */
            function catsGoTo(p) {
                const g = catsGeom();
                if (!g.more) return;
                const idx = Math.max(0, Math.min(g.pages - 1, p - 1));
                const to = Math.min(catsTrack.scrollHeight - g.visH, idx * g.visH);
                catsSeek = idx + 1;                        // 先给反馈,滚动途中不被抢回旧页
                catsSeekUntil = Date.now() + 1500;         // 安全阀:动画被节流时不至于永久挂着
                if (catsTrack.scrollTo) catsTrack.scrollTo({ top: to, behavior: 'smooth' });
                else catsTrack.scrollTop = to;
            }
            catsTrack.addEventListener('scroll', catsSync);
            window.addEventListener('resize', catsSync);
            function catsOf(type) {
                const out = [];
                if (type === 'sticker') {
                    STICKER_PACKS.forEach(function (pk) {
                        if (pk.list && pk.list.length) out.push({ key: pk.key, name: pk.name, count: pk.list.length });
                    });
                } else {
                    NATIVE_PACKS.forEach(function (p) {
                        const l = nativePackData(p.key);
                        if (l && l.length) out.push({ key: p.key, name: p.name, count: l.length });
                    });
                    /* ★★ 2.55.9:自备 emoji 套系也要占一枚胶囊 —— 否则它连被选中的
                       资格都没有(renderCats 会把不在列表里的 curCat 退回「全部」)。 */
                    CUSTOM_EMOJI_PACKS.forEach(function (pk) {
                        if (pk.list && pk.list.length) out.push({ key: pk.key, name: pk.name, count: pk.list.length });
                    });
                    if (TIEBA_EMOJI_ALL.length) out.push({ key: 'tieba', name: '贴吧表情', count: TIEBA_EMOJI_ALL.length });
                }
                let total = 0;
                out.forEach(function (c) { total += c.count; });
                out.unshift({ key: CAT_ALL, name: '全部', count: total });
                return out;
            }
            function catsHas(type, key) {
                const list = catsOf(type);
                for (let i = 0; i < list.length; i++) if (list[i].key === key) return true;
                return false;
            }
            function renderCats(type) {
                // ★ 2.55.0:默认 / 原来的那一类不在这页的列表里 ⇒ 退回「全部」
                if (curCat !== CAT_ALL && !catsHas(type, curCat)) curCat = CAT_ALL;
                catsTrack.innerHTML = '';
                catsOf(type).forEach(function (c) {
                    const chip = document.createElement('span');
                    chip.className = 'spp-emoji-cat' + (c.key === curCat ? ' spp-active' : '');
                    chip.dataset.cat = c.key;
                    const nm = document.createElement('b');
                    nm.textContent = c.name;
                    chip.appendChild(nm);
                    const ct = document.createElement('i');
                    ct.textContent = c.count;
                    chip.appendChild(ct);
                    chip.addEventListener('click', function () {
                        curCat = c.key;      // 勾选:这一类就是下一页
                        renderGrid(type);    // ★ 分页:重画选择区与网格(翻页位置保留)
                        emojiGrid.scrollTop = 0;   // ★ 跳转:换页即回顶
                        syncEmojiLayout();
                    });
                    catsTrack.appendChild(chip);
                });
                // 画完这一帧才知道宽度 / 有没有溢出 ⇒ 隔一帧再决定那三枚要不要露出来
                if (window.requestAnimationFrame) requestAnimationFrame(catsSync); else catsSync();
            }
            /* ★★ 2.54.8:网格里每一类的分组头(满宽 + 滚动贴顶,见 CSS)。
               count 有值就补一枚张数小标 —— 图多的时候「这一类有多少」一眼可见。 */
            function groupHead(name, count) {
                const hd = document.createElement('div');
                hd.className = 'spp-emoji-group';
                hd.textContent = name;
                if (count) {
                    const n = document.createElement('i');
                    n.textContent = count;
                    hd.appendChild(n);
                }
                emojiGrid.appendChild(hd);
                return hd;
            }
            function renderStickerGrid() {
                STICKER_PACKS.forEach(function (pk) {
                    if (!pk.list || !pk.list.length) return;
                    if (curCat !== CAT_ALL && curCat !== pk.key) return;   // ★ 2.54.9:选择区
                    if (curCat === CAT_ALL) groupHead(pk.name, pk.list.length);   // 单类时分组头不重复
                    pk.list.forEach(function (e) {
                        /* ★★ 2.55.9:原来这里写死 HUAJI_CDN_BASE + e.file —— 自备套系没有
                           file 字段、也没有共同的 CDN 前缀,会拼出 …/huaji/undefined。
                           现在 e.url 优先(自备套系只有它);滑稽那套照旧「自建 CDN +
                           jsdelivr」双源 —— 只有同时带 file 与 url 的条目才有回退可言。 */
                        const cdn = e.url || (HUAJI_CDN_BASE + encodeURIComponent(e.file));
                        appendEmojiImg(cdn, e.name, function (img) {
                            if (!e.file || !e.url || img.src === e.url) return false;
                            img.src = e.url;  // 自建 CDN 挂了 ⇒ 换回 jsdelivr 那张原图
                            return true;
                        }, function () { insertEmojiTag(cdn); }, e.anim);
                    });
                });
            }
            function renderEmojiGrid() {
                let native = 0;
                NATIVE_PACKS.forEach(function (p) {
                    const list = nativePackData(p.key);
                    if (!list || !list.length) return;
                    if (curCat !== CAT_ALL && curCat !== p.key) return;   // ★ 2.54.9:选择区
                    if (curCat === CAT_ALL) groupHead(p.name, list.length);   // ★ 2.54.8:每类一枚分组头
                    list.forEach(function (it) {
                        appendEmojiImg(it.src, it.title || String(it.id), null, function () {
                            if (typeof addsmile === 'function') addsmile(it.id);
                        });
                        native += list.length;
                    });
                });
                if (!native && curCat === CAT_ALL) {   // ★ 2.54.9:选了具体一类时不铺兜底
                    // face.js 没拿到:老办法,克隆 #menu_show 里那排默认表情(保留站点 onclick)
                    defaultEmojiData.forEach(function (d) {
                        const clone = d.el.cloneNode(true);
                        /* ★ 2.55.14:站点自己在这些表情上写了 title。勾着提示卡就把那段名字
                           搬到 data-spp-tip 上(原生 title 必须摘掉,否则两个框会一起弹);
                           不勾就照旧摘掉 title,什么都不弹。clone 自带的 onclick 一律留着。 */
                        if (EMOJI_TIP) {
                            const pick = function (el) {
                                const t = el.getAttribute('title');
                                if (t) { el.setAttribute('data-spp-tip', t); el.removeAttribute('title'); }
                            };
                            pick(clone);
                            clone.querySelectorAll('[title]').forEach(pick);
                        } else {
                            clone.removeAttribute('title');
                        }
                        emojiGrid.appendChild(clone);
                    });
                }
                /* ★★ 2.55.9:emoji 页的第三路(自备套系)—— 排在站点原生之后、贴吧表情
                   之前。与贴吧那一路同构:纯 URL,点一下 insertEmojiTag。 */
                CUSTOM_EMOJI_PACKS.forEach(function (pk) {
                    if (!pk.list || !pk.list.length) return;
                    if (curCat !== CAT_ALL && curCat !== pk.key) return;
                    if (curCat === CAT_ALL) groupHead(pk.name, pk.list.length);
                    pk.list.forEach(function (e) {
                        appendEmojiImg(e.url, e.name, null, function () { insertEmojiTag(e.url); }, e.anim);
                    });
                });
                const tiebaOn = (curCat === CAT_ALL || curCat === 'tieba') && TIEBA_EMOJI_ALL.length;
                if (tiebaOn && curCat === CAT_ALL) groupHead('贴吧表情', TIEBA_EMOJI_ALL.length);
                if (tiebaOn) TIEBA_EMOJI_ALL.forEach(function (e) {
                    /* ★ 2.53.49:带 url 的条目走自己的地址(id 1 在仓库里是 404 文本,
                       只能指到不带编号那张)。 */
                    const src = e.url || (TIEBA_CDN_BASE + e.id + '.png');
                    appendEmojiImg(src, e.name, null, function () { insertEmojiTag(src); });
                });
                if (!emojiGrid.childNodes.length) {
                    const empty = document.createElement('div');
                    empty.className = 'spp-emoji-empty';
                    empty.textContent = '这一栏没东西可放(表情数据没拿到)';
                    emojiGrid.appendChild(empty);
                }
            }
            function renderGrid(type) {
                renderCats(type);   // ★ 2.54.9:选择区跟着页走(emoji / 贴纸 各一套)
                emojiGrid.innerHTML = '';
                if (type === 'sticker') renderStickerGrid();
                else renderEmojiGrid();
                /* ★★ 2.55.14:网格每次整片重建,而提示卡那一对监听挂在**容器**上(只挂一次,
                   dataset 挡第二遍)—— 格子只是带 data-spp-tip 的普通节点,重建多少次都不用重挂。 */
                if (EMOJI_TIP) SPP_TIP.delegate(emojiGrid, 'spp-tip-emoji');
            }
            /* ★★ 2.54.8:两枚胶囊各带一张提示卡(SPP_TIP)—— 用户要「emoji 的分类要在这两枚
               UI 上能看出来」,于是把该类包含哪些分类、各多少张写进 title(悬停即见)。 */
            function makeTab(label, type, active, tip) {
                const tab = document.createElement('span');
                tab.className = 'spp-emoji-tab' + (active ? ' spp-active' : '');
                tab.textContent = label;
                tab.dataset.type = type;
                if (tip) SPP_TIP.attach(tab, tip);
                tab.addEventListener('click', function() {
                    tabs.querySelectorAll('.spp-emoji-tab').forEach(function(t) { t.classList.remove('spp-active'); });
                    tab.classList.add('spp-active');
                    curCat = CAT_DEF[type] || CAT_ALL;   // ★ 2.55.0:换页回到本页默认类(emoji 小圆脸 / 贴纸 滑稽)
                    renderGrid(type);
                    /* ★ 2.55.0:换页时选择区(多行后是竖着翻)回到第一页。
                       ★ 2.55.17:连同「目标页 / 攒量」一起复位 —— 换页会把 scrollHeight 改小,
                       上一页可能还有一记平滑滚动在飞,不复位的话位置反馈会挂在旧目标上
                       (实测:从 emoji 切到贴纸后,提示卡还写着上一页的「第 2/2 页」)。 */
                    catsTrack.scrollTop = 0;
                    catsSeek = 0;
                    catsSeekUntil = 0;
                    catsWheelAcc = 0;
                    emojiGrid.scrollTop = 0;
                    syncEmojiLayout();
                });
                return tab;
            }
            // ★★ 2.54.6:两页 —— 「emoji」= 站点原生 5 类 + 贴吧表情;「贴纸」= 滑稽整套(含滑稽原图)。
            //   站点原生那几类不再各占一枚标签(7 枚挤成三行是这一栏最乱的地方),合成一页铺完。
            // ★★ 2.54.8:两枚胶囊各挂一张分类提示卡(用户:「emoji 的分类要在这两枚 UI 上
            //   能看出来」)—— 原生那几类的名字直接从 NATIVE_PACKS 取,张数按当前数据表算,
            //   不写死数字(避免以后加删表情时提示卡说谎)。
            const NATIVE_NAMES = NATIVE_PACKS.map(function (p) { return p.name; }).join(' / ');
            /* ★★ 2.55.9:两张提示卡都不再写死套系名与张数 —— 自备套系加进来之后,
               「贴纸 —— 滑稽 55 张」这种写法当场就开始说谎(2.54.8 那轮自己写的
               「不写死数字,避免以后加删表情时提示卡说谎」,结果这里自己写死了)。
               张数一律从当前数据表现算。 */
            const CUSTOM_EMOJI_N = CUSTOM_EMOJI_PACKS.reduce(function (n, pk) { return n + ((pk.list && pk.list.length) || 0); }, 0);
            const STICKER_N = STICKER_PACKS.reduce(function (n, pk) { return n + ((pk.list && pk.list.length) || 0); }, 0);
            const STICKER_NAMES = STICKER_PACKS.map(function (pk) { return pk.name + ' ' + ((pk.list && pk.list.length) || 0); }).join(' / ');
            tabs.appendChild(makeTab('emoji', 'emoji', true,
                'emoji —— 站点原生 ' + NATIVE_PACKS.length + ' 类(' + NATIVE_NAMES + ')+' +
                (CUSTOM_EMOJI_N ? ' 自备套系 ' + CUSTOM_EMOJI_N + ' 张 +' : '') +
                ' 贴吧表情 ' + TIEBA_EMOJI_ALL.length + ' 张;下面每一类都有自己的分组头,' +
                '点原生那几类走站点自己的 addsmile'));
            tabs.appendChild(makeTab('贴纸', 'sticker', false,
                '贴纸 —— 共 ' + STICKER_N + ' 张:' + STICKER_NAMES +
                ';分组头就是套系名;动图在网格里只显示静止首帧'));

            /* ★★ 2.54.8(用户:「两枚小 UI 需要替换掉表情,表情原本的位置由两个 UI 元素代替」):
               legend 里原本只有站点写的那两个字「表情」,现在把两枚胶囊放进去占它那一行,
               下面那行独立的 .spp-emoji-tabs 整个撤掉 —— 少一行(实测 31px),
               省下的高度全给网格(高度口径见 syncEmojiLayout)。
               ★ legend 拿不到时(结构变了 / 别的页)退回老样子:标签仍旧插在菜单之前,
                 不留白、也不丢这两枚 UI。 */
            const legendEl = emojiFieldset.querySelector('legend');
            if (legendEl) {
                while (legendEl.firstChild) legendEl.removeChild(legendEl.firstChild);  // 撤掉「表情」两字
                legendEl.appendChild(tabs);
                tabs.classList.add('spp-emoji-tabs--inlegend');
            } else if (menuShow) {
                emojiFieldset.insertBefore(tabs, menuShow);
            } else {
                emojiFieldset.appendChild(tabs);
            }
            // 网格接在 legend 之后(有 menuShow 就插在它之前)
            // ★ 2.54.9:选择区就住在 legend 与网格之间(legend 恒在 fieldset 首位,所以插在
            //   网格之前即可),它自己占一条、不参与网格滚动
            if (menuShow) {
                emojiFieldset.insertBefore(catsBar, menuShow);
                emojiFieldset.insertBefore(emojiGrid, menuShow);
            } else {
                emojiFieldset.appendChild(catsBar);
                emojiFieldset.appendChild(emojiGrid);
            }
            // ── 2.52.49:站点原生表情面板(点 [更多] 弹出的 body > div.menu)搬进本栏 ──
            // ★ 只认含 #showface / #buttons.face 的那个浮层 —— body > div.menu 也是「最近浏览的帖子」浮层的形态,
            //   sendmsg() 生成的那个绝不能搬。
            const nativeHost = document.createElement('div');
            nativeHost.className = 'spp-face-native-host';
            emojiFieldset.appendChild(nativeHost);
            function homeNative() {
                const list = document.querySelectorAll('body > div.menu');
                for (let i = 0; i < list.length; i++) {
                    const m = list[i];
                    if (!m.querySelector('#showface, #buttons.face')) continue;
                    if (m.parentElement === nativeHost) return;
                    m.classList.add('spp-face-native');
                    nativeHost.appendChild(m);          // 搬进表情栏:不再压住回复框,也不会被裁
                    emojiFieldset.classList.add('spp-face-native-on');
                    // 栏内空间有限:把我们的表情网格压矮,让原生面板进来(外框改可滚)
                    emojiGrid.style.maxHeight = '160px';
                    emojiGrid.style.minHeight = '160px';
                }
            }
            homeNative();
            new MutationObserver(homeNative).observe(document.body, { childList: true });
            // 站点在 [更多] 的 onclick 里还会给面板重设一次 inline left/top —— 点完补两次搬移(站点先跑)
            document.addEventListener('click', function (e) {
                const t = e.target;
                if (!t || !t.closest || !t.closest('#td_face, #smiliebox')) return;
                setTimeout(homeNative, 0);
                setTimeout(homeNative, 200);
            }, true);

            // 首次渲染(默认 emoji 页)
            renderGrid('emoji');

            // ── 高度自适应:根据页面模式计算网格高度 ──
            const isCompactEmoji = SEARCH.includes('action-quote') || SEARCH.includes('action-modify');
            function syncEmojiLayout() {
                const toolbar = document.querySelector('.spp-editor-toolbar');
                const ta = document.querySelector('textarea[name="atc_content"]');
                if (!ta) return;

                // 预览模式下 textarea 被隐藏,改用预览面板高度
                const previewPane = document.getElementById('spp-preview-pane');
                let contentH = ta.offsetHeight;
                if (contentH <= 0 && previewPane && previewPane.style.display !== 'none') {
                    contentH = previewPane.offsetHeight;
                }
                if (contentH <= 0) return;

                let legendH = 0;
                const leg = emojiFieldset.querySelector('legend');
                if (leg) legendH = leg.offsetHeight;

                /* ★★ 2.54.8:两枚胶囊搬进了 legend ⇒ 独立那行没了。tabsH 只在
                   「legend 没拿到、标签退回老位置」这一种情况下才该加 —— 否则同一份高度
                   会被算两遍(legendH 已经含了那两枚胶囊),网格白白少一截。 */
                const tabsInLegend = !!(leg && tabs && tabs.parentElement === leg);
                const tabsH = (!tabsInLegend && tabs) ? tabs.offsetHeight : 0;
                // ★★ 2.54.9:选择区(.spp-emoji-cats)永远在 legend 与网格之间,
                //   和 tabsInLegend 与否无关 ⇒ 直接量它的高度(+4 = 下外边距)。
                const catsH = catsBar ? catsBar.offsetHeight + 4 : 0;
                // 24px = 上padding(10) + 下padding(12) + border上下(2)
                // ★ 2.54.6:搜索框撤掉后这里少一行(原来的 searchH)
                const chromeH = legendH + catsH + tabsH + 24;

                /* ★★ 2.54.8(用户:「我希望把它变成一个表情贴纸的区域,也就是让它的区域
                   更大一些」):原来两条分支都用 Math.min(420, …) 封顶(编辑页那条还被
                   CSS 的 316 焊死),于是不管窗口多大,网格永远停在 420。
                   现在改成**按可用轨道取景**:从面板顶量到它所在单元格的下缘,留 12px
                   间隙,其余全给网格;上限只留一个防呆的 900。
                   ★ 拿不到单元格时退回「编辑器总高度 × 1.5」的老口径,不留白。 */
                const fsTop = emojiFieldset.getBoundingClientRect().top;
                let avail = 0;
                const cell = emojiFieldset.closest('td.f_one, td, .t_f_one') || emojiFieldset.parentElement;
                if (cell) avail = cell.getBoundingClientRect().bottom - fsTop - 12;
                if (isCompactEmoji) {
                    // ★★ 2.53.10(用户批注「加大表情框的大小」):原来的口径是
                    //   「网格高度 = 文本框高度 − chrome」,实测只给出 150(下限)——
                    //   引用页的文本框本来就矮,网格于是只用了三成空间。
                    //   改成「按**轨道**取景」:面板顶到选项表顶这一段才是真正可用的高度
                    //   (实测 441 → 945 = 504),留 16px 间隙,网格拿剩下的。
                    //   ★ 拿不到轨道时(结构变了 / 别的页)退回原口径,不留白屏。
                    // 引用 / 编辑页:右栏是固定轨道,「选项表上缘」才是准的那个底
                    const optT = document.querySelector('.t.f_one > table:has(#post-option)');
                    if (optT) avail = optT.getBoundingClientRect().top - fsTop - 16;
                    // 再用视口夹一次:面板顶出屏幕就没人看得到了(顶在屏外时不夹)
                    const viewRoom = window.innerHeight - Math.max(0, fsTop) - 16;
                    if (viewRoom > 200) avail = Math.min(avail, viewRoom);
                }
                let gridH;
                if (avail > chromeH + 150) {
                    gridH = Math.max(150, Math.min(900, avail - chromeH));
                } else {
                    // 正常回复:大网格模式,参考编辑器总高度(老口径,拿不到轨道时兜底)
                    const editorH = ((toolbar ? toolbar.offsetHeight : 0) + contentH) * 1.5;
                    gridH = Math.max(320, Math.min(900, editorH - chromeH));
                }
                emojiGrid.style.maxHeight = gridH + 'px';
                emojiGrid.style.minHeight = gridH + 'px';
                emojiGrid.style.overflowY = 'auto';
            }

            syncEmojiLayout();
            // ★ 2.53.10:下面那条 GM_addStyle(引用页加宽)与站点自己的脚本都会改布局,
            //   首帧量到的轨道可能还是旧值 ⇒ 隔一拍再量一次(幂等,不会重复建 DOM)。
            setTimeout(syncEmojiLayout, 300);
            window.addEventListener('resize', syncEmojiLayout);
            // 预览切换时重算(textarea display:none → offsetHeight=0)
            window.addEventListener('spp-preview-toggle', function() {
                // 延迟一帧等 DOM 更新完成
                requestAnimationFrame(syncEmojiLayout);
            });
            if (window.ResizeObserver) {
                const ro = new ResizeObserver(function() { syncEmojiLayout(); });
                const ta = document.querySelector('textarea[name="atc_content"]');
                if (ta) ro.observe(ta);
            }
        }

        onDomReady(() => setTimeout(tryPatch, 500));
    }

    // ==================== 10. 帖子操作栏合并 — tipad → tiptop(仅 read.php) ====================
    function initTipadMerge() {
        if (!WANTS.readOnly) return;

        // 2.53.4:站点在 .fr 里用**裸文本节点**「|」当分隔符(CSS 选不中文本节点,
        //   所以画不成那条 1px 细线,只能是一排竖杠)。这里只做一件事:把它包成
        //   span.spp-tp-sep,外观交给 READ_CSS 的 2.53.4 段。幂等:fr 打 dataset 标记。
        function normalizeSeps(fr) {
            if (!fr || fr.dataset.sppSepDone) return;
            fr.dataset.sppSepDone = '1';
            Array.prototype.slice.call(fr.childNodes).forEach(n => {
                if (n.nodeType !== 3 || n.textContent.trim() !== '|') return;
                const s = document.createElement('span');
                s.className = 'spp-tp-sep';
                s.textContent = '|';
                fr.replaceChild(s, n);
            });
        }

        onDomReady(() => {
            // ★ 先跑一趟全局:不带 .tipad 的楼层(站点某些帖没有那一栏)也要有细线分隔符
            document.querySelectorAll('.t5.t2 .tiptop .fr').forEach(normalizeSeps);
            document.querySelectorAll('.t5.t2').forEach(post => {
                const tiptop = post.querySelector('.tiptop');
                const tipad = post.querySelector('.tipad');
                if (!tiptop || !tipad) return;

                const dingduan = tipad.querySelector('a[title="顶端"]');
                const huifu = tipad.querySelector('.huifu a');
                const yinyong = tipad.querySelector('.yinyong a');
                const gengduo = tipad.querySelector('.dropB');

                let fr = tiptop.querySelector('.fr');
                if (!fr) {
                    fr = document.createElement('div');
                    fr.className = 'fr';
                    fr.style.cssText = 'overflow:hidden;display:flex;align-items:center;gap:6px;flex-wrap:wrap;';
                    tiptop.appendChild(fr);
                } else {
                    fr.style.display = 'flex';
                    fr.style.alignItems = 'center';
                    fr.style.gap = '6px';
                    fr.style.flexWrap = 'wrap';
                }

                normalizeSeps(fr);

                const addSep = () => {
                    const sep = document.createElement('span');
                    sep.className = 'spp-tp-sep';   // 2.53.4:与站点那几枚「|」同一个类,样式只有一处
                    sep.textContent = '|';
                    fr.appendChild(sep);
                };

                if (fr.children.length > 0) addSep();

                [dingduan, huifu, yinyong, gengduo].forEach(el => {
                    if (!el) return;
                    const wrapper = document.createElement('span');
                    wrapper.appendChild(el);
                    fr.appendChild(wrapper);
                    addSep();
                });

                // 去掉末尾多余分隔符
                while (fr.lastChild?.tagName === 'SPAN' && fr.lastChild.textContent.trim() === '|') {
                    fr.removeChild(fr.lastChild);
                }

                tipad.remove();

                // tpc_content 无实质内容 → 整个帖子行隐藏
                const tpc = post.querySelector('.tpc_content');
                if (tpc) {
                    const text = tpc.textContent.replace(/[\s]/g, '');
                    const hasContent = tpc.querySelector('img, a[href], iframe, blockquote, embed, video, .f14')
                        || text.length > 5;
                    if (!hasContent) {
                        const postTr = post.querySelector('.tr1.r_one');
                        if (postTr) postTr.style.display = 'none';
                    }
                }
            });
        });
    }

    // ==================== 10b. 悬赏信息条:数额拆成独立节点(仅 read.php,2.55.23) ====================
    // 为什么要有这一步:站点把两个数额写成 div.tac「最佳答案: 100&nbsp;SP币」这样的**一整段文本**,
    //   CSS 选不到里面的数字 ⇒ 想让「100」比标签大一档,只能拆 DOM(外观全部由 READ_CSS 的
    //   2.55.23 段负责,这里只出结构)。
    // 拆法:按第一个冒号(半角 : 或全角 :)切成「标签 + 数额」两半,标签进 span.spp-bn__k、
    //   数额进 b.spp-bn__v —— 纯文本搬运,站点原文一个字没改(含 100 与 SP币 之间那个 &nbsp;)。
    // 幂等:整块 tips 打 dataset.sppBnDone;一枚都认不出(没有冒号)就整块跳过 ⇒ 页面保持站点原样。
    // 作用域:只认 .t5.t2 帖子里、且只有 div.tac 的那几行 —— 站点别处的 .tips(若有)不碰。
    function initBountyTips() {
        if (!WANTS.readOnly) return;
        onDomReady(() => {
            document.querySelectorAll('.t5.t2 .tips').forEach(tips => {
                if (tips.dataset.sppBnDone) return;
                const rows = tips.querySelectorAll('div.tac');
                if (!rows.length) return;
                tips.dataset.sppBnDone = '1';
                rows.forEach(row => {
                    const m = /^([^::]{1,12})[::]\s*([\s\S]+)$/.exec((row.textContent || '').trim());
                    if (!m) return;
                    const k = document.createElement('span');
                    k.className = 'spp-bn__k';
                    k.textContent = m[1];
                    const v = document.createElement('b');
                    v.className = 'spp-bn__v';
                    v.textContent = m[2];
                    row.textContent = '';
                    row.appendChild(k);
                    row.appendChild(v);
                });
            });
        });
    }

    // ==================== 16. 帖子签名搬进头部行居中(仅 read.php) ====================
    // 用户批注(帖子页截图,白框 + 箭头):「帖子签名放置区,在'时间'和'只看作者'之间居中」。
    // 实站 DOM(read.php 实测):TH.r_one#td_tpc > DIV.tiptop[1040×27] 里
    //   SPAN.fl(引用号,如 B30F)→ SPAN.fl.gray(时间)→ DIV.fr(只看xx/屏蔽/小/中/大/顶端/回复/引用)
    // 签名原本挂在正文里,占一整块纵向空间。修法:把站点那枚签名节点(Discuz 惯例 .sign)
    // **搬**(不是克隆)到 .tiptop 里、DIV.fr 之前,由 CSS 把这一行改成 flex 三段式,
    // 中间那段 flex:1 + text-align:center ⇒ 签名正好落在「时间」和「操作按钮」中间。
    // ★ 为什么搬节点:签名里常有图片/链接,克隆会丢掉站点事件与原节点的其他用处。
    // ★ 站点该帖作者没有签名时什么都不做(`无签名 = 不插入占位`,这一行保持原样)。
    function initPostSignInline() {
        if (!WANTS.readOnly) return;
        onDomReady(() => {
            document.querySelectorAll('.t5.t2').forEach(post => {
                const top = post.querySelector('.tiptop');
                if (!top || top.querySelector('.spp-sign-inline')) return;
                const cell = post.querySelector('#td_tpc') || post;
                const cand = cell.querySelectorAll('.sign, .signature, [class~="sign"]');
                let sig = null;
                for (let i = 0; i < cand.length; i++) {
                    const el = cand[i];
                    if (el.closest('.spp-sign-inline')) continue;
                    if (/(^|\s)spp-/.test(el.className || '')) continue;
                    sig = el;
                    break;
                }
                if (!sig) return;
                const slot = document.createElement('div');
                slot.className = 'spp-sign-inline';
                slot.appendChild(sig);
                const fr = top.querySelector('.fr');
                if (fr) top.insertBefore(slot, fr); else top.appendChild(slot);
            });
        });
    }

    // ==================== 房子按钮样式表(2.52.101 起独立注入) ====================
    // ★ 为什么搬出 READ_CSS:那张表只帖子页注入得到,而版块页也要这枚按钮。
    //   注入方 = initCrumbHomeNav(id: spp-hbtn-style),作用域类 html.spp-hbtn-on。
    // ★ 硬规矩同 READ_CSS:本模板字面量内不得出现反引号与美元符号。
    const HOME_NAV_CSS = `
/* ============================================================================
   ===== 2.52.98 起:房子按钮(面包屑)→ 版块导航面板划出(2.52.101 跨页)=====
   ----------------------------------------------------------------------------
   ★ 2.52.101:这一段已整段搬出 READ_CSS 成 HOME_NAV_CSS,由 initCrumbHomeNav
     自己注入独立样式表 #spp-hbtn-style,作用域类 html.spp-hbtn-on
     —— 因为版块页(thread.php / thread_new.php)也要这枚按钮,而那边不挂 html.spp-read-on,
     原来的选择器在那里全部落空。实站(thread_new.php?fid-9-page-1.html):
     #td_cate 就在 #spp-btabs > #breadcrumbs > .crumbs-item 里,站点只给了内联
     onmouseover(悬停能开、点击是空的)。
   ① 房子按钮的视觉是一段 Uiverse 元素的缩放改编:
        来源 uiverse.io/mahiatlinux/black-lion-15(MIT,© mahiatlinux (Maheswar Kumar) / Darlley)
        原设计=磁贴式开关:3px 边框 + 8px 圆角 + 左上角 ::before 圆形勾选指示器,
        hover 时边框/图标转强调蓝并弹出勾选圆。
        这里按用户要求「适当修改,适配白色主题」→ 缩到 26×26 纯图标形态,
        备了 light(白底 + #e2e8f0 边框 + #334155 图标,hover 转 #3b82f6)与
        dark(半透明玻璃 + rgba(255,255,255,.18) 边框 + #cbd5e1 图标,hover 转 #60a5fa)
        两套主题;2.52.99 起面包屑那两行改用 dark(用户回访:「这个要适配黑色背景」)。
        类名一律 spp-hbtn 前缀。
      ★ MIT 只要求「保留版权声明」,所以声明写在这里(源码里),界面上不印。
   ② 站点给房子图标的原始盒子只有 15×15(实站量测:left 14 / top 316 / 15×15)——
      本段把它换成 26×26 的按钮,原 <img> 退成透明热区(站点内联事件、cursor 全部照旧)。
   ③ 面板「划出」入场动画挂在本段的 .spp-navmenu/.spp-navout 上(keyframes 见段末)。
      ★ 帖子签名那一段(.tiptop 三段式)留在 READ_CSS 里:只有帖子页有那行。
   ============================================================================ */
/* ---- ①-a 基类:几何与交互(不含配色) ---- */
.spp-hbtn {
    box-sizing:border-box !important;
    position:relative !important;
    display:inline-flex !important; align-items:center !important; justify-content:center !important;
    flex:0 0 auto !important;
    width:26px !important; height:26px !important;
    min-width:26px !important; min-height:26px !important;
    max-width:26px !important; max-height:26px !important;
    padding:0 !important; margin:0 !important;
    border-width:3px !important; border-style:solid !important; border-radius:8px !important;
    box-shadow:0 1px 2px 0 rgba(15,23,42,.10) !important;
    transition:all 200ms ease !important;
    overflow:visible !important;
    vertical-align:middle !important; cursor:pointer !important; outline:0 !important;
    -webkit-appearance:none !important; appearance:none !important;
    font-size:0 !important; line-height:0 !important; letter-spacing:0 !important;
    text-decoration:none !important; text-shadow:none !important; text-transform:none !important;
    user-select:none !important; -webkit-user-select:none !important;
    -webkit-tap-highlight-color:transparent !important; touch-action:manipulation !important;
}
/* 内联房子轮廓:描边走 currentColor,跟着 hover / .is-active 一起变蓝 */
.spp-hbtn > svg {
    box-sizing:border-box !important; display:block !important;
    width:20px !important; height:20px !important; max-width:100% !important; max-height:100% !important;
    margin:0 !important; padding:0 !important; fill:none !important;
    stroke:currentColor !important; stroke-width:1.9 !important;
    stroke-linecap:round !important; stroke-linejoin:round !important;
    pointer-events:none !important; transition:all 200ms ease !important;
}
/* 左上角勾选圆:原设计 20px 圆 + 3px 边框 → 缩到 10px(同一比例),常态 opacity:0 + scale(0) */
.spp-hbtn::before {
    box-sizing:border-box !important; content:"" !important;
    position:absolute !important; top:-3px !important; left:-3px !important; z-index:2 !important;
    display:flex !important; align-items:center !important; justify-content:center !important;
    width:10px !important; height:10px !important; margin:0 !important; padding:0 !important;
    border-width:1.5px !important; border-style:solid !important; border-radius:50% !important;
    background:transparent !important;
    font-size:7px !important; font-weight:700 !important; font-style:normal !important; line-height:1 !important;
    opacity:0 !important; transform:scale(0) !important;
    pointer-events:none !important; transition:all 200ms ease !important;
}
.spp-hbtn:hover, .spp-hbtn:focus-visible { box-shadow:0 4px 10px -2px rgba(15,23,42,.20) !important; }
.spp-hbtn.is-active { box-shadow:0 4px 10px -2px rgba(15,23,42,.20) !important; }
.spp-hbtn:hover::before, .spp-hbtn:focus-visible::before { opacity:1 !important; transform:scale(1) !important; }
.spp-hbtn.is-active::before { opacity:1 !important; transform:scale(1) !important; }
.spp-hbtn:focus-visible { outline:2px solid currentColor !important; outline-offset:2px !important; }
/* ---- ①-b light 主题(用户定案:白底浅灰边,适配白色背景) ---- */
.spp-hbtn--light { background:#ffffff !important; border-color:#e2e8f0 !important; color:#334155 !important; }
.spp-hbtn--light::before { background:#ffffff !important; border-color:#cbd5e1 !important; color:#ffffff !important; }
.spp-hbtn--light:hover, .spp-hbtn--light:focus-visible, .spp-hbtn--light.is-active {
    border-color:#3b82f6 !important; color:#3b82f6 !important; background:#ffffff !important;
}
.spp-hbtn--light:hover::before, .spp-hbtn--light:focus-visible::before, .spp-hbtn--light.is-active::before {
    background:#3b82f6 !important; border-color:#3b82f6 !important; color:#ffffff !important;
    content:"\u2713" !important;
}
/* ---- ①-c dark 主题(深色面包屑面板用;本轮默认不上,留着给后续页面) ---- */
.spp-hbtn--dark {
    background:rgba(255,255,255,.06) !important; border-color:rgba(255,255,255,.18) !important;
    color:#cbd5e1 !important; box-shadow:0 1px 2px 0 rgba(0,0,0,.45) !important;
}
.spp-hbtn--dark::before { background:#0e1116 !important; border-color:rgba(255,255,255,.30) !important; color:#ffffff !important; }
.spp-hbtn--dark:hover, .spp-hbtn--dark:focus-visible, .spp-hbtn--dark.is-active {
    border-color:#60a5fa !important; color:#60a5fa !important;
    background:rgba(255,255,255,.10) !important; box-shadow:0 4px 10px -2px rgba(0,0,0,.60) !important;
}
.spp-hbtn--dark:hover::before, .spp-hbtn--dark:focus-visible::before, .spp-hbtn--dark.is-active::before {
    background:#60a5fa !important; border-color:#60a5fa !important; color:#0e1116 !important;
    content:"\u2713" !important;
}
/* ---- ② 面包屑里的挂载点:26×26 定尺 + 原图当透明热区(作用域 html.spp-hbtn-on:帖子页 + 版块页)---- */
/* ★ 2.52.101:补上 box-sizing / line-height / font-size —— 版块页那条
   「#spp-btabs #breadcrumbs .crumbs-item」(2 个 id)带 line-height:1.2,会被 wrap 继承成
   15.6px 行盒。当前靠「26px 定尺 + flex:0 0 auto」算出来是对的,但那是「算出来的安静」,
   这里按与 .spp-hbtn 同一口径钉死(SPEC-AUDIT M1/M2)。 */
html.spp-hbtn-on #breadcrumbs .crumbs-item .spp-hbtn-wrap {
    position:relative !important; box-sizing:border-box !important; display:inline-flex !important;
    align-items:center !important; justify-content:center !important;
    width:26px !important; height:26px !important; flex:0 0 auto !important;
    margin:0 2px 0 0 !important; padding:0 !important; vertical-align:middle !important;
    line-height:1 !important; font-size:0 !important;
}
html.spp-hbtn-on #breadcrumbs .crumbs-item .spp-hbtn-wrap > img {
    position:absolute !important; inset:0 !important; z-index:3 !important;
    width:100% !important; height:100% !important; margin:0 !important; padding:0 !important;
    border:0 !important; opacity:0 !important; cursor:pointer !important;
}
/* 热区(=原图)铺满按钮:悬停/常亮态由 wrap 判定,不信 img 自己的 :hover
   ★ 2.52.99:配色跟着按钮的 dark 变体走(#60a5fa + 半透明玻璃底)—— 上一版这里写的是
     light 那一套(#3b82f6 + 白底),用户看到实物后说「这个要适配黑色背景」。 */
html.spp-hbtn-on #breadcrumbs .crumbs-item .spp-hbtn-wrap:hover .spp-hbtn,
html.spp-hbtn-on #breadcrumbs .crumbs-item .spp-hbtn-wrap.is-active .spp-hbtn {
    border-color:#60a5fa !important; color:#60a5fa !important;
    background:rgba(255,255,255,.10) !important; box-shadow:0 4px 10px -2px rgba(0,0,0,.60) !important;
}
html.spp-hbtn-on #breadcrumbs .crumbs-item .spp-hbtn-wrap:hover .spp-hbtn::before,
html.spp-hbtn-on #breadcrumbs .crumbs-item .spp-hbtn-wrap.is-active .spp-hbtn::before {
    opacity:1 !important; transform:scale(1) !important;
    background:#60a5fa !important; border-color:#60a5fa !important; color:#0e1116 !important;
    content:"\u2713" !important;
}
/* ---- ③ 面板「划出」:站点自己的 read.menu 承载,类由脚本在每次打开时重挂 ---- */
.spp-navmenu.spp-navout { animation:sppNavOut .24s cubic-bezier(.22,1,.36,1) both !important; }
@keyframes sppNavOut {
    from { opacity:0 !important; transform:translateY(-10px) !important; }
    to   { opacity:1 !important; transform:translateY(0) !important; }
}
`;

    // ==================== 17. 面包屑房子按钮 → 版块导航面板(仅 read.php) ====================
    // 用户批注(帖子页底部面包屑截图,红圈圈住房子图标 + 箭头指向版块面板):
    //   「增加一个功能,鼠标经过房子按钮的时候有面板划出」。
    // ★ 实站量到两枚房子图标待遇不同:
    //     顶栏那枚 <img id="td_cate" src="…/home.gif"> 自带
    //       onmouseover="read.open('menu_cate','td_cate')" —— 悬停就能弹站点原生「版块导航」面板
    //       (#menu_cate 内容 = 漫区特设 / Comic Market 10x / 汉化计划 / 综合交流 / 免空资源区 …);
    //     底部面包屑那枚 <img src="…/home-old.gif">【没有 id、没有任何事件】—— 用户圈的是这一枚。
    // ⇒ 修法:两枚都包进同一个按钮组件(.spp-hbtn-wrap > .spp-hbtn + 原图当透明热区),
    //   由本模块统一驱动站点那套 read.open(面板内容/定位/开合全用站点的,不自己造一份),
    //   并给面板补一次「划出」入场动画(.spp-navout)。
    // ★ 为什么摘掉站点那句内联 onmouseover:read.menu.className = 源菜单类名 会把我们加的
    //   动画类冲掉,两边抢时序 ⇒ 收口到一条路:先 read.open,再加动画类。
    function initCrumbHomeNav() {
        if (!WANTS.homeNav) return;
        // ★ 图标路径与 DOM 契约来自本轮 Uiverse 改编件的实拍验收(tmp/btn/house-button-demo.html):
        //   内联 SVG、viewBox 0 0 24 24、不给 width/height 属性(尺寸由 `.spp-hbtn > svg` 的
        //   20px + max-width:100% 决定,这样 20px 图标在 26px 按钮里等比缩放、不会去挤 3px 边框)。
        const HOME_SVG = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">'
            + '<path d="M2.25 12 11.2 3.05a1.2 1.2 0 0 1 1.6 0L21.75 12"></path>'
            + '<path d="M4.8 9.75V19.5A1.5 1.5 0 0 0 6.3 21h2.7v-5.4a1.5 1.5 0 0 1 1.5-1.5h3a1.5 1.5 0 0 1 1.5 1.5V21h2.7a1.5 1.5 0 0 0 1.5-1.5V9.75"></path></svg>';
        let seq = 0;
        let leaveT = 0;
        // ★ 2.52.100(用户:「这个面板怎么丢了」+ 一张「悬停弹出、点一下面板就没了」的截图):
        //   2.52.99 的 click 是无脑 toggle —— 悬停已经把面板打开了,鼠标还在按钮上时点一下,
        //   就执行了「关闭」⇒ 用户看到的是「面板被点没了」。改成**点击只负责钉住/收起**:
        //     · 面板没开 → 点一下=打开 + 钉住;
        //     · 面板开着(不管是悬停开的)→ 第一次点=**钉住**(不移开不收),再点一次才收;
        //     · 钉住期间移开鼠标不收(否则和悬停没区别);点按钮以外的任何地方收。
        //   同一时刻只有一份面板,所以钉住状态用一个模块级变量就够。
        let pinnedHost = null;
        function unpin() {
            if (!pinnedHost) return;
            pinnedHost.classList.remove('is-active');
            pinnedHost = null;
        }
        document.addEventListener('click', (e) => {
            if (pinnedHost && !pinnedHost.contains(e.target)) { const h = pinnedHost; unpin(); closeNav(h); }
        }, true);

        // ★★ 2.52.102(真凶;用户已经报了三次「点击没有弹出面板」,前两轮我修的都是别的东西):
        //   站点那套 read.open / read.close 活在【页面上下文】里,而本脚本头部带 @grant ⇒
        //   Tampermonkey 把脚本放进沙箱执行,沙箱里的 window **不是**页面 window,
        //   window.read 恒为 undefined ⇒ openNav 一进门就抛、被下面的 catch 吞掉,
        //   面板永远不会打开(悬停与点击都一样)。
        //   实测证据(真浏览器 + CDP 真实点击):click 监听器确实跑了(按钮挂上 is-active),
        //   但页面里那层 read.open / read.close 包装函数一次都没被调用(trace 为空)。
        //   ★ 为什么四道活体门全绿却没照出来:探针是 new Function 注入【页面上下文】跑的,
        //     那边的 window.read 是真的 —— 这就是「注入式验证」的盲区(见 经验 1.50)。
        //   修法:头部加 @grant unsafeWindow,这里统一走 WIN;万一拿不到就退化成
        //   「派发 mouseover / mouseout 去触发站点那**内联** handler」(内联 handler 跑在页面上下文)。
        const WIN = (function () {
            try { return (typeof unsafeWindow !== 'undefined' && unsafeWindow) ? unsafeWindow : window; }
            catch (e) { return window; }
        })();
        function pageRead() { try { return WIN.read || null; } catch (e) { return null; } }
        function openNav(img) {
            const R = pageRead();
            if (R && R.open) {
                try {
                    if (!img.id) img.id = 'spp-hcate-' + (++seq);
                    R.open('menu_cate', img.id, 1, 0);
                } catch (e) { return; }
            } else {
                // 退化路径:内联 handler 还在(见下面 removeAttribute 的条件),事件会在页面上下文里触发它
                try { img.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true })); }
                catch (e) { return; }
            }
            const m = R && R.menu;
            if (!m || !m.classList) return;
            m.classList.add('spp-navmenu');
            m.classList.remove('spp-navout');
            void m.offsetWidth;                 // 强制重排:同一枚按钮第二次悬停也能重播动画
            m.classList.add('spp-navout');
        }
        function closeNav(host) {
            const R = pageRead();
            if (R && R.close) { try { R.close(); } catch (e) {} return; }
            const img = host && host.querySelector ? host.querySelector('img') : null;
            if (img) { try { img.dispatchEvent(new MouseEvent('mouseout', { bubbles: true, cancelable: true })); } catch (e) {} }
        }
        // ★ 2.52.101:钉住期间必须摘掉站点挂在原图上的 onmouseout。
        //   站点在 read.open 里写下的是 `getObj(object).onmouseout = function(){ read.close(); … }`
        //   —— 语义是「指针一离开这枚图就关面板」,真实鼠标移开按钮必然触发它,
        //   于是「钉住」在真机上形同虚设:面板 100ms 后就没了。
        //   ★ 为什么上一轮的活体断言没照出来:合成 mouseleave 不冒泡、也不会让 img 收到
        //     mouseout ⇒ 那条「钉住后移开仍开着」在旧实现下是**假绿**(见 经验 1.45)。
        //   面板自己那两个 handler(进面板续命 / 离面板关闭)保持站点原样,不动。
        function holdOpen(img) {
            img.onmouseout = null;
        }

        onDomReady(() => {
            // ★ 2.52.101:样式跟着本模块走 —— 上一版写在 READ_CSS 里,而那张表只帖子页注入,
            //   版块页即使挂上按钮也是裸的(实测:按钮会退成 UA 默认样子)。独立 id 可幂等也好排查。
            if (!document.getElementById('spp-hbtn-style')) {
                const st = document.createElement('style');
                st.id = 'spp-hbtn-style';
                st.textContent = HOME_NAV_CSS;
                appendToHeadSafe(st);
            }
            // 作用域类:帖子页与版块页共用同一套规则(不再借 html.spp-read-on)
            document.documentElement.classList.add('spp-hbtn-on');
            document.querySelectorAll('#breadcrumbs .crumbs-item').forEach(item => {
                const img = item.querySelector('img[src*="home"]');
                if (!img || item.querySelector('.spp-hbtn-wrap')) return;
                const host = document.createElement('span');
                host.className = 'spp-hbtn-wrap';
                const btn = document.createElement('button');
                btn.type = 'button';
                // ★ 2.52.99:面包屑那两行都在深色底上(顶栏=星空页面的透明行、底部=#0e1116
                //   磨砂玻璃条),所以用 dark 变体 —— 两套主题的样式都在 CSS 里,这里只换类。
                btn.className = 'spp-hbtn spp-hbtn--dark';
                btn.setAttribute('aria-label', '版块导航');
                btn.innerHTML = HOME_SVG;
                host.appendChild(btn);
                host.appendChild(img);          // 原图留着当热区:站点光标、后续事件都还在
                item.appendChild(host);
                // ★ 这枚类既是「已包进按钮组件」的标记,也是和旧规则的分界线:
                //   READ_CSS 里那条 `html.spp-read-on #breadcrumbs #td_cate {width:15px !important…}`
                //   有 2 个 id ⇒ 特化度 (2,1,1) 高过本段任何 (1,n,n) 的规则,实测量到
                //   热区仍是 15×15(按钮 26×26,面板因此贴着旧盒子下缘弹、和按钮错位 12px)。
                //   修法:给旧规则加 `:not(.spp-hbtn-hit)` —— 两条规则从此互斥,不再比特化度。
                // ★ 2.52.101:同一条互斥也补到了版块页那条「#spp-btabs #breadcrumbs #td_cate」
                //   (3 个 id,实站会把热区又压回 15×15 —— 经验 1.44 的第二次复发)。
                img.classList.add('spp-hbtn-hit');
                // ★ 2.52.102:只有拿得到页面上下文通路(WIN.read)时才收口摘掉站点的内联 handler;
                //   拿不到就留着它当退化路径 —— 否则这枚按钮在真机上点/悬都没反应(前面三版就是这个下场)。
                if (pageRead()) {
                    img.removeAttribute('onmouseover');
                    img.removeAttribute('onmouseout');
                }
                // ★ 2.52.101:热区几何再加一道**内联 important** 保险。
                //   为什么光靠 `:not(.spp-hbtn-hit)` 还不够:那条 3 个 id 的老规则在
                //   「新版 JS 注入到装着旧版的页面」时会**留在页面里**(旧版那份 CSS 由
                //   GM_addStyle 追加,<style> 没有 id,探针按 id 清场清不掉)⇒ 实测量到
                //   热区仍是 15×15、按钮 26×26(版块页活体探针第一跑的唯一一条 FAIL)。
                //   本项目遇到「打不过读不到的/陈旧的样式表」时的既有做法就是内联 important
                //   (同 initBoardToolbar 的 Go 输入框、快速发帖框底色那两处)。
                //   ★ 只在几何上钉死(位置/尺寸/外边距),观感仍全部走样式表。
                img.style.setProperty('position', 'absolute', 'important');
                img.style.setProperty('inset', '0', 'important');
                img.style.setProperty('width', '100%', 'important');
                img.style.setProperty('height', '100%', 'important');
                img.style.setProperty('margin', '0', 'important');
                img.style.setProperty('padding', '0', 'important');
                img.style.setProperty('border', '0', 'important');

                // 开:悬停整枚按钮(热区就是原图,覆盖按钮全域)→ 站点面板 + 入场动画
                host.addEventListener('mouseenter', () => {
                    clearTimeout(leaveT);
                    host.classList.add('is-active');
                    openNav(img);
                });
                // 关:交给站点那套(read.open 自己给 img 挂 onmouseout→read.close,
                //   面板 mouseover 会清掉它那个 100ms 定时器)—— 我们只负责按钮常亮态:
                //   指针离按钮后留 140ms 宽限,期间进面板则续亮,否则熄灭。
                host.addEventListener('mouseleave', () => {
                    if (pinnedHost === host) return;      // ★ 2.52.100:钉住的(点过)不随移开而收
                    clearTimeout(leaveT);
                    leaveT = setTimeout(() => {
                        const R = pageRead();
                        const m = R && R.menu;
                        if (m && m.classList.contains('spp-navout') && m.matches(':hover')) return;
                        host.classList.remove('is-active');
                        closeNav(host);
                    }, 140);
                });
                // ★ 2.52.99(用户:「点击时弹出一个面板,但没有弹出来」):上一版只接了悬停,
                //   点击什么都没绑 ⇒ 补 click。
                // ★ 2.52.100:把「无脑 toggle」改成「钉住 / 收起」—— 悬停已经打开时点一下
                //   不该把它点没(用户原话:「这个面板怎么丢了」)。三次分支见 initCrumbHomeNav 段头。
                //   热区仍是铺满按钮的原图 ⇒ 点击落在 img 上、冒泡到这里(站点那枚图自己没有
                //   onclick,不会和它抢);键盘 Enter/Space 在 <button> 上也走 click。
                host.addEventListener('click', (ev) => {
                    ev.preventDefault();
                    ev.stopPropagation();
                    clearTimeout(leaveT);
                    const R = pageRead();
                    const m = R && R.menu;
                    const open = !!(m && getComputedStyle(m).display !== 'none' && m.classList.contains('spp-navout'));
                    if (open && pinnedHost === host) {      // 已钉住 → 再点一次=收起
                        unpin();
                        closeNav(host);
                        return;
                    }
                    if (open) {                             // 悬停开着 → 点一下=钉住(不收起)
                        unpin();
                        pinnedHost = host;
                        host.classList.add('is-active');
                        holdOpen(img);                      // ★ 2.52.101:摘掉站点的 onmouseout
                        return;
                    }
                    unpin();                                // 没开 → 打开并钉住
                    pinnedHost = host;
                    host.classList.add('is-active');
                    openNav(img);
                    holdOpen(img);                          // ★ 2.52.101:同上(read.open 每次都会重新挂上它)
                });
            });
        });
    }

    // ==================== 18. 快速发帖框的两种形态(仅 read.php) ====================
    // 用户截图(帖子页底部回复框)里的五条批注,四条能纯 CSS 做(见 READ_CSS 的 2.52.98 段),
    // 只有这两处形态必须由 JS 生成 —— 站点没给它们可选的节点:
    //   ① 左栏每行是「input[type=checkbox] + 紧跟一个【裸文本节点】」,没有 label/span:
    //      纯 CSS 换得掉小方框,却没法把文字装进胶囊里 ⇒ 补一层 label.spp-fr-opt
    //      (顺带把点击区从 13px 的方框扩到整枚胶囊;站点这些 checkbox 自己没有 onclick,包起来不改行为)。
    //   ② 「提 交」与「预览」是那个 DIV 的并列子节点、中间还隔着别的节点,没有共同父元素:
    //      要复刻截图里那两枚独立胶囊框,只能补一层 span.spp-fr-actions。
    // ★ 两处都幂等(dataset 标记 / :scope > 选择器),且 CSS 侧都用「匹配到才生效」的闸门
    //   (label.spp-fr-opt / :has(> .spp-fr-actions))—— 这段 JS 不跑,样式段就是零副作用。
    function initFastReply() {
        if (!WANTS.readOnly) return;
        onDomReady(() => {
            const box = document.querySelector('div.t5:has(textarea[name="atc_content"])');
            if (!box) return;

            // ★ 底色/背景图为什么必须在 JS 里钉(其余属性都由 READ_CSS 那段管):
            //   星空主题那条 `html.spp-hm-star #main .t5:not(.spp-t5bar){background:…!important}`
            //   带一个 id,特化度 (1,3,1) 高过本段的 `html.spp-read-on div.t5:has(…)`(0 个 id)——
            //   实测量到盒子仍是站点那层 rgba(250,252,255,.93)(星空透进来 7%)。
            //   本项目对这类「早已带 !important 的高特化旧规则」的既定解法就是内联逃生口
            //   (同 initReadFlow 段头的理由)。取值走设计令牌,尺子仍只有一把;
            //   CSS 侧那条声明照旧留着(它管其余属性,且星空那条哪天撤了这里就是同值覆盖)。
            // ★★ 2.55.25:这里原先把 --spu-surface 的**当前值读出来当字面量**钉在内联上 ——
            //   那等于把这块玻璃冻死在白色上(内联 !important 谁也盖不住),模块 19 的暗色令牌喂不进来。
            //   改成把**变量本身**写进去,兜底链:--spp-qr-bg(模块 19 在暗色时给)→
            //   --spu-surface(2.53.24 起的白底主题令牌)→ #ffffff(站点原样)。
            //   关态时前两层都没定义 ⇒ 解析出来仍是 #ffffff,与 2.55.24 逐字节一致。
            box.style.setProperty('background-color', 'var(--spp-qr-bg, var(--spu-surface, #ffffff))', 'important');
            box.style.setProperty('background-image', 'var(--spp-qr-bgi, none)', 'important');

            const td = box.querySelector('td.f_one:has(input[name="atc_usesign"])');
            if (td) {
                td.querySelectorAll('input[type="checkbox"]').forEach(cb => {
                    if (cb.dataset.sppFrOpt) return;
                    const next = cb.nextSibling;
                    if (!next || next.nodeType !== 3) return;   // 只认「紧跟文本」的行,其余一律不碰
                    const label = document.createElement('label');
                    label.className = 'spp-fr-opt';
                    cb.parentNode.insertBefore(label, cb);
                    label.appendChild(cb);
                    label.appendChild(next);                    // 文本节点整体搬进去,一个字不改
                    cb.dataset.sppFrOpt = '1';
                });
            }

            const sub = box.querySelector('input[name="Submit"]');
            const pv = document.getElementById('spp-preview-btn');
            if (sub && pv && pv.parentElement === sub.parentElement
                && !sub.parentElement.querySelector(':scope > .spp-fr-actions')) {
                const wrap = document.createElement('span');
                wrap.className = 'spp-fr-actions';
                sub.parentElement.insertBefore(wrap, sub);
                wrap.appendChild(sub);
                wrap.appendChild(pv);                            // 顺序 = 提交、预览
            }
        });
    }

    // ==================== 19. 帖子区 / 快速回复框 黑玻璃(可选,默认关闭,2.55.25)====================
    // ── 2.55.31 本轮增补(四件,用户点名):① 开关改名「黑夜模式」、解释改成「进入黑夜模式即可」;
    //    ② 作用域从「仅帖子页」放宽到「帖子页 + 回贴页(post.php)」—— 见 initReadDark 门口的注释;
    //    ③ 表情栏那两枚 UI(emoji / 贴纸 页签 + 分类胶囊)在黑夜里补实底、勾选态加绿色荧光;
    //    ④ 快捷工具栏的按钮字提白、分类字提白加粗。
    //    下面各段都有就地注释;回贴页那半(区域令牌 / 表单玻璃 / 主题回顾 / 底部按钮)见第 6、7 段。
    // 需求原文与来龙去脉见文件头 2.55.25 段头;这里只写**实现理由**与**为什么非这么写不可**。
    // ---------------------------------------------------------------------------
    // ★ 为什么必须动 JS(活体实测,不是推理):initReadFlow 给六行 .t5.t2 写了内联
    //   background-color:#fff !important;模块 18 给快速回复框写了内联同类声明。
    //   CSS 规范里「内联 !important」优先于任何样式表 !important —— 那两处已按变量化改过
    //   (var(--spp-flow-bg, …) / var(--spp-qr-bg, …)),本段负责在暗色时给值。
    // ★ 令牌为什么只发给这两块、不发 :root:--spu-* 是全脚本自绘 UI 共用的(实测 295 处
    //   var(--spu-*))。挂 :root 会把设置面板 / 灯箱 / 导航面板一起刷黑 —— 超出用户要求
    //   (「框选的两个区域」)。只在两个区域容器上重定义,区域内的自绘控件靠继承自动跟着变。
    // ★ 文本可读性(实测驱动,不是拍脑袋):
    //   ① 容器级**只写 color、不写 -webkit-text-fill-color**:后者是继承属性,一旦在容器上
    //      写成具体色,所有后代都用它绘制 —— 发帖人手写的 color 会当场失效。本版特意避开这个坑。
    //   ② 区域内令牌换浅色 ⇒ 我们自己那些 var(--spu-text/secondary/muted) 的控件整体提亮
    //      (实测:帖子区里 42 处、快速回复框里 35 处深色字都来自这些令牌)。
    //   ③ 站点/脚本里**写死深色**的那几类逐个点名(清单见下面 CSS 段注释,全部来自活体点名)。
    //   ④ CSS 管不了的一类:发帖人**内联**写的深色字(style="color:#000" / <font color=…>),
    //      写法太杂、字符串匹配必漏 ⇒ 照 15f 的老办法按对比度判(见下面的 lift 段)。
    // ★ 为什么选择器里 .spp-rdark 写两遍(不是笔误):READ_CSS 那几条是
    //   html.spp-read-on div.t5:has(textarea[name="atc_content"]) 这种同型 !important 规则;
    //   两条都带 !important 时**比的是特异性**,重复一枚 class 把本方顶上去,就不再依赖
    //   「我们的 <style> 恰好排在 READ_CSS 之后」这种时序条件(同 15h 段里的理由)。
    const CSS_READ_DARK = `
/* ===== SPP-READDARK-BEGIN ===== */
/* 作用域:html.spp-rdark(由 initReadDark 按开关挂 / 摘)。样式块**无条件注入**,
   开 / 关只差 <html> 上那一枚标 —— 与 15h「悬停高亮」同款,将来要做面板内即时生效也不用再动样式。 */

/* ---- 0. 区域令牌(只发给这两块,理由见段头)---- */
html.spp-rdark.spp-rdark #main > form.spp-flow,
html.spp-rdark.spp-rdark div.t5:has(textarea[name="atc_content"]) {
    --spu-surface:#2c2c2e;              /* 2.55.28:蓝黑 → 苹果中性灰(面板里控件的底,跟着面板一起去蓝)*/
    --spu-overlay:#3a3a3c;              /* 2.55.28:蓝黑 → 苹果中性灰(弹出层 / 下拉菜单底)*/
    --spu-text:#f5f5f7;              /* 2.55.29:正文白 → 苹果 rgb(245,245,247)(原 #e3eaf6 偏蓝)*/
    --spu-secondary:#b6c3d6;
    --spu-muted:#93a1b5;
    --spu-border:rgba(255,255,255,.14);
    --spu-accent:#3fbfa8;
    --spu-accent-dim:#2f8f7e;
    --spp-hole:rgba(255,255,255,.06);
    --spp-rk-text:#f5f5f7;                  /* 2.55.29:正文白 → 苹果 rgb(245,245,247) */
    --spp-rk-line:rgba(255,255,255,.12);
    --spp-rk-hair:rgba(255,255,255,.07);
    --spp-rk-link:#8ef0b8;
    --spp-rk-glass:rgb(0 0 0 / .86);          /* 2.55.28:蓝黑 → 苹果 #1d1d1f 66%(共用配方,见 SPP-GLASS 段)*/
    --spp-rk-sheen:linear-gradient(152deg, rgba(255,255,255,.03) 0%, rgba(0,0,0,0) 55%);
    --spp-qr-bg:var(--spp-rk-glass);
    --spp-qr-bgi:var(--spp-rk-sheen);
}
/* 一体流那四个颜色由 initReadFlow 写进内联,这里只负责给值(关态没有这些变量 ⇒ 走原字面量)*/
html.spp-rdark.spp-rdark #main > form.spp-flow {
    --spp-flow-bg:var(--spp-rk-glass);
    --spp-flow-bgi:var(--spp-rk-sheen);
    --spp-flow-edge:var(--spp-rk-line);
    --spp-flow-hair:var(--spp-rk-hair);
}

/* ---- 1. 帖子区:玻璃本体 ----
   ★ 选择器里 .spp-rdark 写三遍、.t5.t2 前面补一个 div,都是为了**特异性**,不是笔误:
     实测(活体逐条算过)本文件 READ_CSS 里两条旧规则各是 (1,5,3) ——
       html.spp-read-on #main > form.spp-flow .t5.t2 th.r_two{background:#fff!important}
       html.spp-read-on #main > form.spp-flow .tiptop > .fl:not(.gray) > a{background:#fff!important}
     而「只写两枚 class」的版本是 (1,5,2),**同 class 数、少一个类型选择器**,正好输给它。
     补到 (1,6,3) 就稳赢,也就不必指望「我们的 <style> 排在 READ_CSS 之后」(那条时序不牢靠)。 */
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2.spp-flow-row {
    color:var(--spp-rk-text) !important;
    -webkit-backdrop-filter:blur(18px) saturate(140%) !important;
    backdrop-filter:blur(18px) saturate(140%) !important;
}
/* 帖内一切浅底扫成透明:作者格白块(185×9901)、头像白底 img(150×150)、用户名白条(150×22)、
   .tiptop 操作条(1190×30 #f8fafc)、a.s3 / a#modify_tpc 两枚白胶囊、站点下拉菜单、弹层凹槽…
   ★ 只扫 background-color,**不扫 background-image** —— 楼层操作条那 6 组图标是
     background-image 画的(2.52.44 那份老配方连 background-image 一起扫,本轮刻意不抄这一半)。
   ★ 扫到 img 也没关系:不透明照片看不出底色变化;透明 PNG 头像露出黑玻璃,本来就是暗色该有的样子。 */
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 * {
    background-color:transparent !important;
}
/* 扫完之后「靠背景色画的线 / 卡」要补回来。
   ★ 这里必须与上面那条扫白**同等特异性再高一档**(3 枚 class + div.t5.t2)——
     实测踩过一次:扫白先按 2 枚 class 写、补线也按 2 枚写,两条同特异性时扫白因为多一个
     类型选择器反而赢,把补回来的分隔条又刷成透明(.spp-tp-sep 量到 rgba(0,0,0,0))。
     差一档就够,不必再往上堆。 */
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 .spp-tp-sep {
    background-color:var(--spp-rk-line) !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 hr {
    background-color:var(--spp-rk-line) !important;
    border-color:var(--spp-rk-line) !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 .user-info.spp-ucard,
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 .user-infoWrap {
    background-color:rgba(17,23,35,.97) !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 .spp-uc__rows,
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 .spp-uc__v {
    background-color:var(--spp-hole) !important;
}

/* ---- 1b. 边框扫白(2.55.26 补,用户第二轮圈选)----
   ★ 1a 那条只扫 background-color,**边框一个都没管** —— 于是深色玻璃上留着一圈圈白线。
     用户圈出来的四处:头像装裱框(1px #e4e9f1) / 楼层号胶囊(1px #e3eaf3) /
     .tiptop 操作条(1px #edf1f7 —— 那一整条 1190px 长横线就是它的上下边) /
     快速回复框里「快速发帖」标题行 td.h2(1px #ddd,站点原样)。
   ★ 为什么这次可以整片扫(不像背景那样按名单点):活体清点两个区域 1261 个元素,
     带 alpha>=.2 的边框一共 16 种,**全部是浅色或浅色 inset 发丝线**,没有一枚是
     承载语义的彩色边(快速回复框里那三枚 #3fbfa8 主色边单独留着,见 3 段)。
     整片扫也顺手覆盖了「悬停才出现、此刻 0×0 量不到边」的两处:屏蔽下拉菜单、悬停头像卡。
   ★ 与背景扫白同理:只写 border-color,不写 border-width / border-style ——
     本来没有边框的元素(绝大多数)仍然没有边框。 */
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 * {
    border-color:var(--spp-rk-hair) !important;
}
/* 扫完把「容器级」的边补回来:头像装裱框 / 工具条卡 / 悬停卡本体 + 它内部两处凹槽 / 屏蔽菜单。
   (与 1a 里 .spp-tp-sep、.user-infoWrap 那几条同一个理由:扫白是一刀切,容器要留形。) */
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 :is(img, .tiptop, .user-infoWrap, .dropdown-content.menu, .spp-uc__rows),
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 .user-info.spp-ucard .listread ul {
    border-color:var(--spp-rk-line) !important;
}
/* 悬停卡里的行分隔线是 **inset 发丝线**(box-shadow,不是 border)⇒ 边框扫白够不到。
   实测这一条同时覆盖 .is-post / .is-sp 两行(它们量到的是同一个值 234,239,247)。 */
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 .spp-uc__row + .spp-uc__row {
    box-shadow:inset 0 1px 0 var(--spp-rk-hair) !important;
}
/* ★ 悬停卡左侧那枚小三角是 ::before 伪元素:星号选择器(*)**根本匹配不到伪元素**
   (1a 那条背景扫白同样匹配不到)⇒ 它会原样留成白三角。这里按卡片底色补回来。 */
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 .user-info.spp-ucard > .user-infoWrap::before {
    background-color:rgba(17,23,35,.97) !important;
    border-color:var(--spp-rk-hair) !important;
}
/* ★★ 两处「1a 名单漏掉、且扫白压不住」的:
   ① 屏蔽下拉菜单 div.dropdown-content.menu 是 **白底**:2.53.24 那条浅色规则是 (1,6,5),
      而 1a 的扫白只有 (1,6,3) —— 同 class 数、少两个类型选择器,正好输给它(实测 bg 仍是
      rgb(255,255,255))。菜单是弹层 ⇒ 用 --spu-overlay(#1c2436)实体色。
   ② 它内部 <a> 的 hover 底色是浅蓝 #eef3f9(那条是 (1,7,5))⇒ 悬停会闪一块浅蓝。
      ★ .menu 写两遍是**故意**的:把本方顶到 8 枚 class,压过 (1,7,5) 的 5 个类型选择器
        (同 1a 里 .spp-rdark 写三遍的理由,同型手法)。 */
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 .dropdown-content.menu {
    background-color:var(--spu-overlay) !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 .dropdown-content.menu.menu > a:hover {
    background-color:rgba(255,255,255,.09) !important;
}
/* 快速回复框本体:星星主题在 .t5 上挂着一条 0.67px rgba(255,255,255,.66) 白描边
   (#spp-starfield-style:html.spp-hm-star #main .t5:not(.spp-t5bar){border:…!important}。
     浅色主题里白底压白边看不出来,换成黑玻璃后就是一圈刺眼白框。)压到与一体流那几条边
   同一个值 --spp-rk-line ⇒ 两块区域一套边框语言。
   ★★ 这里**必须自带 #main**:那条星星规则带 id ⇒ (1,3,1),而「两枚 .spp-rdark + div.t5 + :has(…)"
   只有 (0,4,3) —— 带 !important 的两条比的是 **id 数**,class 再多也压不过(实测第一版就栽在这,
   量出来仍是 .66)。补上 #main 后 (1,4,3) 一比就赢,与 1a 那几条的写法对齐。 */
html.spp-rdark.spp-rdark #main div.t5:has(textarea[name="atc_content"]) {
    border-color:var(--spp-rk-line) !important;
}
/* 快速回复框表格:站点给「快速发帖 / 顶端」那行 td.h2 写死 1px #ddd(同一张表里其余 td 实测无边);
   这里一并按 table/tr/td 扫,免得换个页面 / 状态又冒出来。**故意不扫 label / input** ——
   那三枚 #3fbfa8 是脚本自己的主色边(勾选态),扫掉就没了。 */
html.spp-rdark.spp-rdark div.t5:has(textarea[name="atc_content"]) :is(table, tr, td) {
    border-color:var(--spp-rk-hair) !important;
}

/* ---- 2. 帖子区:写死深色的那几类 → 提亮(清单来自活体点名)---- */
/* 操作条(含它里面那个站点下拉菜单):链接色被星空主题那条超长 a:not(…) 链压着不放 ——
   2.52.43 当年就是栽在这条链上(最后只能给每个 a 写内联 !important)。这里先把选择器顶到
   (1,7,4);实测仍然没亮起来的那几枚,由 initReadDark 的 pin 段用内联逃生口兜底 ——
   与当年的区别是:**只钉对比度实测没过的那几枚**,不是整层每个 a 都钉一遍。 */
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 .tiptop :is(a, b, span:not(.spp-tp-sep)) {
    color:var(--spu-secondary) !important;
    -webkit-text-fill-color:var(--spu-secondary) !important;
}
html.spp-rdark.spp-rdark #main > form.spp-flow > .t5.t2 th.r_two,
html.spp-rdark.spp-rdark #main > form.spp-flow > .t5.t2 th.r_two a {
    color:var(--spp-rk-text) !important;
    -webkit-text-fill-color:var(--spp-rk-text) !important;
}
/* 站点给正文链接写死 #0070af 深蓝(压黑底对比度不足)→ 换成本项目暗色下的绿 */
html.spp-rdark.spp-rdark #main > form.spp-flow > .t5.t2 .tpc_content a {
    color:var(--spp-rk-link) !important;
    -webkit-text-fill-color:var(--spp-rk-link) !important;
}
/* 悬停头像弹层(白底状态卡)里的深色字 */
html.spp-rdark.spp-rdark #main > form.spp-flow > .t5.t2 .user-info.spp-ucard :is(a, span, li, b) {
    color:var(--spu-secondary) !important;
    -webkit-text-fill-color:var(--spu-secondary) !important;
}
html.spp-rdark.spp-rdark #main > form.spp-flow > .t5.t2 .user-info.spp-ucard .spp-uc__v {
    color:var(--spu-text) !important;
    -webkit-text-fill-color:var(--spu-text) !important;
}
html.spp-rdark.spp-rdark #main > form.spp-flow > .t5.t2 .user-info.spp-ucard .spp-uc__row.is-post .spp-uc__v {
    color:var(--spu-accent) !important;
    -webkit-text-fill-color:var(--spu-accent) !important;
}
html.spp-rdark.spp-rdark #main > form.spp-flow > .t5.t2 .user-info.spp-ucard .spp-uc__row.is-sp .spp-uc__v {
    color:#f0b357 !important;
    -webkit-text-fill-color:#f0b357 !important;
}
/* 发帖人内联写的深色字(对比度 < 2.2)—— 由下面的 lift 段挂 class */
html.spp-rdark.spp-rdark #main > form.spp-flow > .t5.t2 .spp-rk-lift {
    color:#eef3fb !important;
    -webkit-text-fill-color:#eef3fb !important;
}

/* ---- 3. 快速回复框:令牌驱动的那一半自动跟着变,下面是**写死 / UA 默认**的那些 ---- */
html.spp-rdark.spp-rdark div.t5:has(textarea[name="atc_content"]) {
    /* ★ 2.55.26 更正:这里原本还有一句 box-shadow:0 0 0 1px var(--spp-rk-line) 想给盒子补一圈描边 ——
       实测**不生效**:星星主题的 html.spp-hm-star #main .t5:not(.spp-t5bar) 带 #main(有 id)且
       !important,是 (1,3,1),而本条只有 (0,4,3) —— 两条都带 !important 时先比 id 数,class 再多也压不过。
       真框改由下面 1b 那条 border-color 接管(同样补 #main 才压得过),这里不再画描边。 */
    -webkit-backdrop-filter:blur(18px) saturate(140%) !important;
    backdrop-filter:blur(18px) saturate(140%) !important;
}
/* 站点控件(文本框 / 下拉 / 两枚金额输入框 / 附件说明框):实测没有任何样式表管它们的底色,
   白色来自 UA 默认值 ⇒ 只有 author !important 压得住 */
html.spp-rdark.spp-rdark div.t5:has(textarea[name="atc_content"]) :is(input:not([type="checkbox"]), textarea, select) {
    background-color:var(--spu-surface) !important;
    border-color:var(--spu-border) !important;
    color:var(--spu-text) !important;
    -webkit-text-fill-color:var(--spu-text) !important;
}
/* WindCode 工具栏:站点没给那 35 枚按钮任何样式(实测 matched=0,全走 UA 白底)*/
html.spp-rdark.spp-rdark div.t5:has(textarea[name="atc_content"]) :is(button, .spp-tb-tblui input) {
    background-color:var(--spp-hole) !important;
    border-color:var(--spu-border) !important;
    /* ★ 2.55.31(用户:「对于输入框,按钮文本采用白色高亮」):--spu-secondary(#b6c3d6)
       在黑玻璃上偏灰、与「高亮」差一档 ⇒ 换 --spu-text。
       ★ 为什么这条能压过按钮自己的行内样式:工具栏那 35 枚按钮的颜色写在 style 里
         (var(--spu-text,#333)),行内**非** !important 永远输给样式表的 !important。 */
    color:var(--spu-text) !important;
    -webkit-text-fill-color:var(--spu-text) !important;
}
html.spp-rdark.spp-rdark div.t5:has(textarea[name="atc_content"]) .spp-tb-row > span {
    /* ★ 2.55.31(用户:「分类文本(如:基础文本格式)采用白色加粗高亮」):
       原来跟按钮一起走 --spu-muted(#93a1b5),比按钮还暗一档,在深底上正好是最难读的那个。 */
    color:var(--spu-text) !important;
    -webkit-text-fill-color:var(--spu-text) !important;
    font-weight:700 !important;
}
/* ★ 2.55.31:上面两条是帖子页快速回复框那半边;回贴页的编辑器走 #editor-content,
   选择器不同 ⇒ 补一条不挑页面的(同一枚 html.spp-rdark 收口,两页共用一套读数)。 */
html.spp-rdark.spp-rdark .spp-editor-toolbar .spp-tb-row > span {
    color:var(--spu-text) !important;
    -webkit-text-fill-color:var(--spu-text) !important;
    font-weight:700 !important;
}
/* 表格骨架与「快速发帖」那条标题行(站点 .h2 是 #ddd 底 + #333 字;底已被星空那条压成透明,字还在)*/
html.spp-rdark.spp-rdark div.t5:has(textarea[name="atc_content"]) :is(tr, td, table) {
    color:var(--spu-secondary) !important;
}
html.spp-rdark.spp-rdark div.t5:has(textarea[name="atc_content"]) td.h2,
html.spp-rdark.spp-rdark div.t5:has(textarea[name="atc_content"]) td.h2 a {
    color:var(--spu-text) !important;
    -webkit-text-fill-color:var(--spu-text) !important;
}
/* 贴纸栏 #smiliebox(白底,但实测**没有任何规则命中它** ⇒ 白色来自 UA 或读不到的样式表)+ 分类签 */
html.spp-rdark.spp-rdark #smiliebox {
    background-color:var(--spu-surface) !important;
    border-color:var(--spu-border) !important;
}
/* ★★ 2.55.31(用户附截图:「两个UI在黑夜模式下为透明的,需要增加背景、勾选的emoji、
   贴纸需要有绿色荧光效果」):那两枚 UI = emoji / 贴纸 页签 + 下面的分类胶囊。
   实测它们原来的底是 --spp-hole(rgba(255,255,255,.06))—— 6% 白压在 #2c2c2e 上
   只出来 56,56,58 三个数(截图里量到的就是它),与底色差 12 档,观感就是「透的」。
   现在给实底 --spu-overlay(#3a3a3c,与面板本体差 14 档)+ 描边,轮廓立起来。
   ★ 选择器不再要求 div.t5:回贴页的 #smiliebox 挂在 #editor-side 下(没有那个 td),
     原来那条只认帖子页 ⇒ 回贴页的表情栏在黑夜里还是白的(实测 fieldset 底 rgb(255,255,255))。 */
html.spp-rdark.spp-rdark #smiliebox :is(.spp-emoji-cat, .spp-emoji-tab) {
    background-color:var(--spu-overlay) !important;
    border-color:var(--spu-border) !important;
    color:var(--spu-secondary) !important;
    -webkit-text-fill-color:var(--spu-secondary) !important;
}
/* 勾选态 = 分类胶囊那枚带 ✓ 的 + 当前那一枚页签(emoji / 贴纸 二选一):绿色荧光。
   ★ 三种「荧光」叠一起才有那个观感:外圈 12px 绿光晕(box-shadow 外投)、
     内圈一道极淡绿(inset 影子)、字上一层绿雾(text-shadow)—— 只用 box-shadow 时
     字是死的,只用 text-shadow 时胶囊是死的。 */
html.spp-rdark.spp-rdark #smiliebox :is(.spp-emoji-cat, .spp-emoji-tab).spp-active {
    background-color:rgba(63,191,168,.16) !important;
    border-color:rgba(63,191,168,.72) !important;
    color:#eafff8 !important;
    -webkit-text-fill-color:#eafff8 !important;
    box-shadow:0 0 12px -2px rgba(63,191,168,.75), inset 0 0 10px rgba(63,191,168,.20) !important;
    text-shadow:0 0 8px rgba(63,191,168,.55) !important;
}
/* ★★ 2.55.33 更正(真机量出来的):2.55.31 换这条时只换了上面那个 #smiliebox 本体,
   这一条**同名的宽选择器规则漏删了**。它比上面那条新规则更具体(多一个 div.t5 与 :has),
   于是把新给的实底又刷回 6% 白 —— 实测 2.55.32 的分类胶囊 / 页签底仍是 rgb(56,56,58),
   与面板本体只差 12 档,用户看到的还是「透的」。规则已合并进上面那一条,这里只留记载。 */
/* 草稿 / 预览两枚站点胶囊按钮(站点 .abtn 写死白底黑字)*/
html.spp-rdark.spp-rdark div.t5:has(textarea[name="atc_content"]) a.abtn {
    background-color:var(--spp-hole) !important;
    border-color:var(--spu-border) !important;
    color:var(--spu-secondary) !important;
    -webkit-text-fill-color:var(--spu-secondary) !important;
}
/* ---- 4. 头像框下沿那枚「ID / 用户名」铭牌(2.55.27 补;2.55.28 已按用户要求改方向)----
   ★ 设计原值(spp-boardtop-style-read 里 2.53.42 那条):86% 白毛玻璃条 + blur(6px) +
     #26303f 深灰字 + 下圆角 14px。黑玻璃上线后它被**两半一起**打坏:
       ① 1a 的扫白 (1,6,3) 把铭牌自己的浅底扫成 transparent;
       ② 第 2 段的 th.r_two 文字规则 (1,5,3) 把它的字刷成浅色。
     ⇒ 只剩浅色字压在「模糊后的亮头像」上(用户截图圈的就是它)。
   ★ 所以这里补的是**被扫掉的那一半**,不是换一套配色:底回 rgba(255,255,255,.86)、
     字回 #26303f。为什么不改成「深底 + 浅字」:铭牌是头像卡的下沿,浅色主题里本来就是
     浅条 + 深字,用户点名要「黑字」—— 还原设计比另起一套更贴原意。
   ★ 选择性 (1,9,5):压过设计那条 (0,6,3)、也压过 1a 的扫白 (1,6,3)(两条都带 !important 时比 id 数)。
   ★ 实测(read.php?tid-2946160,视口内 4 枚铭牌):底 rgba(255,255,255,.86) /
     字 rgb(38,48,63) / 对比度 13.32:1 —— 浅底是实体色,换什么头像都不会反过来。
   ★ 同类漏网已普查过一遍:区域内「声明过浅底」的规则 24 条逐条看过,只有这一对是坏的。
   ── 【2.55.28 取代】上面那套「还原白条 + 深字」被用户第四轮否掉,改成不透明黑底 + 白字。
      理由:铭牌压在头像照片下沿,浅条是整块黑玻璃面板里唯一的亮块。用户原话「头像下面的 ID 应该是
      黑色不透明玻璃 + 白色字体才是正常的」。上面 2.55.27 的记载保留作历史,不删。
   ★ 为什么必须「不透明」:底下是头像照片、明暗随机 —— 半透黑会随头像变深变浅(深色头像上糊成一片、
     浅色头像上泛灰);alpha=1 就没有这个变量。
   ★ 为什么还留一道 background-image:黑底 + 上沿 10% 白高光 = 2.52.44 定下的「玻璃 = 底色 + 镜面
     高光」两段式(底色这轮改成 alpha=1);它同时是铭牌与头像的分界线。
   ★ 实测(read.php?tid-2946160,视口内 5 枚铭牌):底 rgb(0,0,0) / 字 rgb(255,255,255) /
     对比度 21.00:1;backdrop-filter 显式置 none(不透明之后它只是白烧 GPU)。
   ── 【2.55.29 再改】上面这句里的纯白 #ffffff 已并到苹果那支白 rgb(245,245,247) = #f5f5f7,
     理由与实测见 2.55.29 段头(对比度 19.29:1,肉眼不可分,换的是色温)。 */
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 .r_two:not(.spp-ava-tiled) > div[align="center"] > a {
    background-color:#000000 !important;
    background-image:linear-gradient(180deg, rgba(255,255,255,.10) 0%, rgba(255,255,255,0) 100%) !important;
    color:#f5f5f7 !important;                       /* 2.55.29:纯白 → 苹果 rgb(245,245,247),与正文同一支白 */
    -webkit-text-fill-color:#f5f5f7 !important;
    -webkit-backdrop-filter:none !important;
    backdrop-filter:none !important;
}
/* ---- 5. 页面底色搬运苹果(2.55.28 补)----
   ★ 用户:「黑夜模式来说它的黑色背景可以搬运苹果的黑色背景(apple.com/iphone-18-pro),苹果的这个
     黑色背景好很多」。取色不是猜的:把用户那张苹果截图里红框内的空白区按像素统计(Pillow,两个子块
     各 100.0%)全是 #1d1d1f;苹果页 body 是 #000,各 section 在这两支之间交替。
   ★ 于是底色就是苹果原样的那两支:#000 打底、底部一团 #1d1d1f 椭圆。
     为什么保留「椭圆在底」这个形状:原星空本来就是 radial-gradient(ellipse at bottom) 的一团背光,
     只换色标不换形状 —— 页面纵向的明暗节奏一个像素都不变。
   ★ 只挂在 html.spp-rdark 下:星空是另一枚开关(spp_home_starfield),它的夜空是它自己的设计;
     只有黑夜模式打开时才接管这一层,关了立刻还原,对星空单独使用无副作用。
   ★ html 上那条 #000 是兜底:星空开关关掉时页面画布会退回站点浅色 —— 有它才不会出现「黑玻璃挂在
     白底上」。实测它**不会**盖住星空(探针:#spp-starfield rect 仍是整屏 1264×760、渐变色有效):
     它是 z-index:-1 的子元素,负 z-index 画在根背景之上。 */
html.spp-rdark.spp-rdark { background-color:#000000 !important; }
html.spp-rdark.spp-rdark #spp-starfield {
    background:radial-gradient(ellipse at bottom, #1d1d1f 0%, #000000 100%) !important;
}

/* ---- 6. 回贴页(post.php):黑夜模式整页跟着变(2.55.31)----
   ★ 为什么是「整页」而不是像帖子页那样只罩两块:这一页只有一块内容(发帖表单 + 底下的主题回顾),
     没有「帖子区 / 快速回复框」那种分块,用户点名要的就是它黑下来。
   ★ 令牌与帖子页那两块区域**同一个来源**(--spu-* / --spp-rk-*),只是挂的宿主换成 #main ——
     表单里的自绘 UI(快捷工具栏 / 表情栏 / 预览按钮 / 选项表)靠继承自动跟着变,不必逐个点名;
     这一页里本项目那些 var(--spu-*) 能不能生效,全看这一处。
   ★ 实测(真装真跑,post.php?action-reply-fid-9-tid-2978280,2026-10-06):
     改前 .t.f_one 的底 = rgba(250,252,255,.93)(星空那条玻璃规则),正文 #555、控件白底。 */
html.spp-rdark.spp-rdark.spp-post-on #main {
    --spu-surface:#2c2c2e;
    --spu-overlay:#3a3a3c;
    --spu-text:#f5f5f7;
    --spu-secondary:#b6c3d6;
    --spu-muted:#93a1b5;
    --spu-border:rgba(255,255,255,.14);
    --spu-accent:#3fbfa8;
    --spu-accent-dim:#2f8f7e;
    --spp-hole:rgba(255,255,255,.06);
    --spp-rk-text:#f5f5f7;
    --spp-rk-line:rgba(255,255,255,.12);
    --spp-rk-hair:rgba(255,255,255,.07);
    --spp-rk-link:#8ef0b8;
    --spp-rk-glass:rgb(0 0 0 / .86);
    --spp-rk-sheen:linear-gradient(152deg, rgba(255,255,255,.03) 0%, rgba(0,0,0,0) 55%);
}
/* 发帖表单那张卡:把星空给的白玻璃换成与帖子区同一份黑玻璃(同一支 --spp-rk-glass)。
   ★ 特异性:本条 (1,5,1),星空那条 html.spp-hm-star #main .t 是 (1,2,1) —— 两条都带
     !important 时先比 id 数,再比 class 数,稳赢,不必指望样式表先后。 */
html.spp-rdark.spp-rdark.spp-post-on #main .t.f_one {
    box-sizing:border-box !important;
    background-color:var(--spp-rk-glass) !important;
    background-image:var(--spp-rk-sheen) !important;
    color:var(--spp-rk-text) !important;
    border-color:var(--spp-rk-hair) !important;
    -webkit-backdrop-filter:blur(18px) saturate(140%) !important;
    backdrop-filter:blur(18px) saturate(140%) !important;
}
/* 骨架文字:站点 body{color:#555} 一路传下来,压在黑玻璃上读不了 ⇒ 整片提亮。
   ★ 这里只写 color、**故意不写 -webkit-text-fill-color**:后者是继承属性,写在容器上会把
     后代里「发帖人自己写的颜色」一起吃掉 —— 帖子页那条老账(段头 ①)就是这么踩的,不重演。
   ★ 名单里的 p 是骨架段落;正文预览那种内容层由 POST_CSS 的 .spp-preview-pane 自己着色。 */
html.spp-rdark.spp-rdark.spp-post-on #main .t.f_one :is(td, th, b, strong, label, legend, li, dt, dd, small, em, option, optgroup, p) {
    color:var(--spp-rk-text) !important;
}
html.spp-rdark.spp-rdark.spp-post-on #main .t.f_one :is(.gray, .f9) {
    color:var(--spu-muted) !important;
}
/* 站点控件(文本框 / 下拉 / 四个金额输入框):实测没有任何样式表管它们的底,白色来自 UA
   或站点老皮 ⇒ 只有 author !important 压得住。★ 排除 submit / reset / button:
   提交那枚走的是下面「同一套按钮语言」,不在这里刷底。 */
html.spp-rdark.spp-rdark.spp-post-on #main .t.f_one :is(input, textarea, select):not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="reset"]):not([type="button"]) {
    background-color:var(--spu-surface) !important;
    border-color:var(--spu-border) !important;
    color:var(--spu-text) !important;
    -webkit-text-fill-color:var(--spu-text) !important;
}
/* ★ 2.55.33:标题输入框外面还套着一层白壳(站点 div.fl.input.f_one,实测 542×24、
   底 rgb(255,255,255))—— 下面那条控件规则只刷到 input 本体,壳不在名单里,
   深色卡上于是留了一条白带(真机截图里就是它)。
   ★ 为什么用「谁包着标题输入框」而不是认类名:fl / input 都是站点的通用类名,
     :has(> input[name="atc_title"]) 才是一次到位的认法。 */
html.spp-rdark.spp-rdark.spp-post-on #main .t.f_one div:has(> input[name="atc_title"]) {
    background-color:var(--spu-surface) !important;
    border-color:var(--spu-border) !important;
}
/* 编辑器正文框:底由 POST_CSS 那条读 --spu-surface(已经跟着变暗),字色没人管(UA 黑字)*/
html.spp-rdark.spp-rdark.spp-post-on #editor-content #textarea {
    color:var(--spu-text) !important;
    -webkit-text-fill-color:var(--spu-text) !important;
}
/* ★ 2.55.33:这条原来只写 -webkit-text-fill-color —— 站点给 #editor-tool 里那四枚链接
   (字数检查 / 恢复数据 / 存为草稿 / 草稿箱)写死 color:#000,字最终靠 fill-color 画出来是浅的,
   但**计算色仍是黑**(读数与对比度审计都会被它骗)。两处一起写,读数与观感才是同一个数。 */
html.spp-rdark.spp-rdark.spp-post-on #editor-tab > span,
html.spp-rdark.spp-rdark.spp-post-on #editor-tool > span,
html.spp-rdark.spp-rdark.spp-post-on #editor-tool > span a {
    color:var(--spu-secondary) !important;
    -webkit-text-fill-color:var(--spu-secondary) !important;
}
/* ★ 2.55.33:当前那枚编辑器页签(#editor-tab 的 .buttonPressed)在黑夜模式下是
   「实心主色 #3fbfa8 + 白字」= 2.28:1(真机 2.55.32 的对比度审计把它单列出来,只剩它一条)。
   换成与表情栏勾选态同一族的写法:淡青底 + 亮字 + 一圈青光。悬停态同理。 */
html.spp-rdark.spp-rdark.spp-post-on #editor-tab > span.buttonPressed {
    background:rgba(63,191,168,.18) !important;
    color:#eafff8 !important;
    -webkit-text-fill-color:#eafff8 !important;
    box-shadow:0 0 10px -2px rgba(63,191,168,.6), inset 0 1px 0 rgba(255,255,255,.10) !important;
}
html.spp-rdark.spp-rdark.spp-post-on #editor-tab > span:hover,
html.spp-rdark.spp-rdark.spp-post-on #editor-tool > span:hover {
    background:rgba(63,191,168,.14) !important;
    color:#eafff8 !important;
    -webkit-text-fill-color:#eafff8 !important;
}

/* ---- 7. 回贴页「主题回顾」:黑夜底 + 浅字(白昼那半在 POST_CSS,两半合起来才是
   「按黑夜 / 非黑夜增加背景」)----
   ★ 实测根因:这几张表是 #main 的直接子元素、不在任何卡里,而星空那层玻璃只罩卡 ⇒
     它们一直落在夜空上,站点给的字色 #555 压深空。引号页 / 编辑页有 #main::after 铺的白底
     (2.53.10 那条),回贴页从来没人给过 —— 这就是用户说的「底部文本变成透明背景影响阅读」。
   ★ 这里只改「字与线」,不碰 #main::after(那是底色,写一条管两种状态,见 POST_CSS)。 */
html.spp-rdark.spp-rdark.spp-post-on #main > center { color:var(--spu-muted) !important; }
html.spp-rdark.spp-rdark.spp-post-on #main > table[align="center"] { color:var(--spp-rk-text) !important; }
html.spp-rdark.spp-rdark.spp-post-on #main > table[align="center"] a {
    color:var(--spp-rk-link) !important;
    -webkit-text-fill-color:var(--spp-rk-link) !important;
}
html.spp-rdark.spp-rdark.spp-post-on #main > hr {
    background:var(--spp-rk-hair) !important;
    border-color:var(--spp-rk-hair) !important;
}
/* ---- 8. 站点「1px 亮框」总清 + 原生控件转暗(2.55.34,用户第二张截图)----
   ★ 用户原话:「黑夜模式下有很多线条、原始UI按钮被白色线条包裹不美观」。
     量法:遍历 #main 全部元素、逐边读 border-*-width / border-*-color,按 WCAG 相对亮度
     筛出 > 0.35 的「亮线」,再拿 el.matches() 全表反查是哪条规则给的(真机 2.55.33)。
     回贴页整页抓到 59 条,去掉本项目自己的 rgba(255,255,255,.07~.14) 之后只剩三组:
       ① span.editor-button 里那 43 枚精灵按钮 —— 站点 .editor-button span{border:1px solid #fff},
          每枚 18×18 的图标外面套一圈白,就是截图里那排白方框。
          ★ 为什么 2.53.x 那批规则没管到:工具图标那一排**不在 #editor-tool 里**。实测 DOM 是
            #editor-content > #editor-button > div > span.editor-button > span#wy_bold,
            而 #editor-tool 只有 4 个子元素(字数检查 / 恢复数据 / 存为草稿 / 草稿箱),
            所以 html.spp-post-on #editor-tool > span 一条都没命中(kids=4 是实测量出来的)。
       ② .tr3 td / .tr3 th 的 border-bottom:1px solid #eee(9 条)+ th.h 的 #bbb(1 条);
          表格是定宽 966,右端那列是空的 ⇒ 那几条线就横穿空列,看着像乱画的横线。
       ③ 拖拽手柄 img(c_editor/resize_0.gif / _1.gif,21×9)自带 1px #ddd 框。
   ★ 顺带把「原生控件」一并转暗:站点在这些容器上写着 color-scheme:light(白底页是对的,
     但这一页在黑夜模式下是黑玻璃)⇒ 复选框画成白方块、文件按钮画成白胶囊、滚动条也是浅的。
     改 color-scheme 即可,不动 DOM、不碰键盘与读屏行为。
   ★ 精灵图标为什么要 invert:editor-icon.gif 的笔画是**黑的**(像素取样:透明 31571、
     黑 687、白 270、灰三档),白框一撤,黑笔画压在 #2a2a2c 上等于消失。
     invert(1) hue-rotate(180deg) 是配深色底图片的常规组合:黑白对调、色相转回来 ——
     实测彩色图标(表情 / 图片 / 高亮)仍认得出,单色图标(B/I/U、对齐、列表)由几乎看不见
     变成清楚(两版截图对照过)。
   ★ 本条全部挂在 html.spp-rdark 下:白昼模式一个字符都不动。
   ★ color-scheme 那条为什么要两枚 .spp-rdark:要压的是站点自己写的
     color-scheme:light !important((0,3,3)),多这一枚类才到 (0,4,3) 才稳。 */
html.spp-rdark.spp-rdark.spp-post-on #main,
html.spp-rdark.spp-rdark div.t5:has(textarea[name="atc_content"]) {
    color-scheme:dark !important;
}
html.spp-rdark.spp-rdark.spp-post-on #main .editor-button span {
    /* ① 撤白框;② 精灵笔画翻浅 */
    border-color:transparent !important;
    border-radius:4px !important;
    filter:invert(1) hue-rotate(180deg) !important;
}
html.spp-rdark.spp-rdark.spp-post-on #main .editor-button span:hover {
    /* 悬停不叠底色(底色会被元素自身的 filter 一起反相),改用提亮 */
    filter:invert(1) hue-rotate(180deg) brightness(1.3) !important;
}
html.spp-rdark.spp-rdark.spp-post-on #main .editor-button span.buttonDisabled {
    opacity:.45 !important;
}
html.spp-rdark.spp-rdark.spp-post-on #main #editor-button img {
    /* ③ 拖拽手柄那两枚 1px #ddd */
    border-color:transparent !important;
}
html.spp-rdark.spp-rdark.spp-post-on #main :is(.tr3 td, .tr3 th) {
    border-bottom-color:var(--spu-border) !important;
}
html.spp-rdark.spp-rdark.spp-post-on #main th.h {
    border-bottom-color:var(--spu-border) !important;
}
html.spp-rdark.spp-rdark.spp-post-on #main input[type="file"] {
    border-color:var(--spu-border) !important;
    color:var(--spu-text) !important;
}
html.spp-rdark.spp-rdark.spp-post-on #main input[type="file"]::file-selector-button {
    /* 与回贴页那份(搜 file-selector-button,第 23739 行起)同一套语言,配色换黑夜版 */
    box-sizing:border-box !important;
    height:22px !important;
    margin:0 7px 0 0 !important;
    padding:0 11px !important;
    border:1px solid rgba(63,191,168,.42) !important;
    border-radius:6px !important;
    background-color:rgba(63,191,168,.10) !important;
    background-image:none !important;
    color:#9ff0da !important;
    font-family:inherit !important;
    font-size:12px !important;
    font-weight:600 !important;
    cursor:pointer !important;
}
/* ---- 9. 付费模块 + 站内引用块:深色托盘(2.55.35,用户任务 8/9/10)----
   ★ 用户三问:「引用效果显示不清晰」(tid-2978425)/「购买 SP 楼层后的信息显示不清晰」
     (tid-2976788)/「付费贴的提示美化没适配黑夜模式」(tid-1385600,允许花 SP 实测)。
   ★ 根因只有一条(活体逐条反查出来的,不是猜的):第 1 节那条「扫白」把帖内一切
     background-color 压成 transparent,而它是 (1,5,3) —— 带 !important 的两条先比 id 数,
     READ_CSS 里付费模块那批规则全是 (0,1,2)~(0,3,2),一个都赢不了:
       ① 付费卡 .spp-buy__bar 的底色被扫掉,只剩 background-image 那条**白渐变**
          ⇒ 黑夜模式下是一条 926×41 的近白横条(真机读数 rgb(251,254,252)→rgb(243,250,246));
       ② 已购正文框 blockquote.spp-buy__body(提取码 / 被解锁正文)底色被扫掉,字色仍是
          浅色主题的 #3f4a5a ⇒ 深蓝灰压黑玻璃,对比度约 1.9:1(任务 8/9 截图里那团灰字);
       ③ 警告条 blockquote.spp-buy__warn 同理,剩 #8a6512 褐字压黑底,约 2.4:1(任务 10);
       ④ 站内引用块 blockquote.blockquote3(含内层 h6.quote2「引用」与那个 div)底色被扫掉,
          字色是站点写死的 #555 ⇒ 约 2.2:1 —— 任务 8 说的「引用效果不清晰」就是它。
   ★ 治法:只做一件事 —— 把这四件东西按「深色托盘」重画一遍。选择器前缀与那条扫白同源,
     尾部多带一到两枚类 ⇒ 类数多一档就够,不依赖 <style> 的插入顺序。
   ★ 配色沿用本项目黑夜令牌(--spu-surface / --spu-border / --spu-text / --spu-secondary /
     --spp-rk-text / --spp-rk-hair / --spp-rk-link);绿族取 #3fbfa8 的暗色档,琥珀族取 #f0b357。
   ★ 全部挂在 html.spp-rdark 下:白昼模式一个字符都不动(下面每一条都带这枚标)。
   ★ 顺序有意义:通用 blockquote 那组必须排在付费模块各条**之前**,因为付费正文框与警告条
     身上也带着 .blockquote 这个类,两者特异性相同时靠后写的赢。 */

/* 9a. 购买卡 / 通行证条本体 */
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__bar {
    background-color:rgba(44,44,46,.92) !important;
    background-image:none !important;
    border-color:var(--spu-border) !important;
    box-shadow:0 1px 2px rgba(0,0,0,.35) !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__bar.is-pass,
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__bar.is-done {
    /* 已购 = 与白昼同一族的绿,只是把明度反到黑玻璃上 */
    background-color:rgba(63,191,168,.10) !important;
    background-image:none !important;
    border-color:rgba(63,191,168,.34) !important;
    border-left:3px solid rgba(63,191,168,.62) !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__bar.is-pass.is-fail {
    background-color:rgba(224,110,92,.12) !important;
    border-color:rgba(224,110,92,.34) !important;
    border-left-color:rgba(224,110,92,.62) !important;
}

/* 9b. 卡内文字 / 印章 / 胶囊 / 确认条 —— 站点与脚本都写死了浅色主题的字色 */
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__tt,
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__t1 {
    color:var(--spu-text) !important;
    -webkit-text-fill-color:var(--spu-text) !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__t2,
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__facts {
    color:var(--spu-secondary) !important;
    -webkit-text-fill-color:var(--spu-secondary) !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__num { color:#7ee7c4 !important; -webkit-text-fill-color:#7ee7c4 !important; }
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__dot { background-color:rgba(255,255,255,.28) !important; }
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__seal {
    background-color:rgba(63,191,168,.14) !important;
    border-color:rgba(63,191,168,.34) !important;
    color:#9ff0da !important; -webkit-text-fill-color:#9ff0da !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__seal.is-paid {
    background-color:rgba(240,179,87,.14) !important;
    border-color:rgba(240,179,87,.36) !important;
    color:#f0b357 !important; -webkit-text-fill-color:#f0b357 !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__pill {
    background-color:rgba(63,191,168,.14) !important;
    border-color:rgba(63,191,168,.38) !important;
    color:#9ff0da !important; -webkit-text-fill-color:#9ff0da !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__pill.is-paid {
    background-color:rgba(240,179,87,.14) !important;
    border-color:rgba(240,179,87,.36) !important;
    color:#f0b357 !important; -webkit-text-fill-color:#f0b357 !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__pill.is-busy {
    background-color:rgba(255,255,255,.07) !important;
    border-color:var(--spu-border) !important;
    color:var(--spu-muted) !important; -webkit-text-fill-color:var(--spu-muted) !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__pill.is-fail {
    background-color:rgba(224,110,92,.14) !important;
    border-color:rgba(224,110,92,.36) !important;
    color:#ffb3a6 !important; -webkit-text-fill-color:#ffb3a6 !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__pic {
    background-color:rgba(63,191,168,.16) !important;
    border-color:rgba(63,191,168,.40) !important;
    color:#9ff0da !important; -webkit-text-fill-color:#9ff0da !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__pic.is-busy {
    background-color:rgba(255,255,255,.07) !important;
    border-color:var(--spu-border) !important;
    color:var(--spu-muted) !important; -webkit-text-fill-color:var(--spu-muted) !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__pic.is-fail {
    background-color:rgba(224,110,92,.16) !important;
    border-color:rgba(224,110,92,.40) !important;
    color:#ffb3a6 !important; -webkit-text-fill-color:#ffb3a6 !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__btn.is-done {
    background-color:rgba(63,191,168,.14) !important;
    background-image:none !important;
    border-color:rgba(63,191,168,.40) !important;
    color:#9ff0da !important; -webkit-text-fill-color:#9ff0da !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__btn.is-fail {
    background-color:rgba(224,110,92,.14) !important;
    background-image:none !important;
    border-color:rgba(224,110,92,.40) !important;
    color:#ffb3a6 !important; -webkit-text-fill-color:#ffb3a6 !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__fallback { color:#ffb3a6 !important; -webkit-text-fill-color:#ffb3a6 !important; }
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__cf.is-on {
    /* 收费帖点了按钮才出现的就地确认条:白昼是琥珀底褐字,这里换成琥珀暗档 */
    background-color:rgba(240,179,87,.12) !important;
    border-color:rgba(240,179,87,.30) !important;
    color:#f2c883 !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__cf b {
    background-color:rgba(0,0,0,.28) !important;
    border-color:rgba(240,179,87,.34) !important;
    color:#ffd79a !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__no {
    background-color:rgba(255,255,255,.07) !important;
    border-color:var(--spu-border) !important;
    color:var(--spu-text) !important; -webkit-text-fill-color:var(--spu-text) !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.spp-buy .spp-buy__yes {
    background-image:linear-gradient(180deg,#b8493a,#962f24) !important;
    color:#ffffff !important; -webkit-text-fill-color:#ffffff !important;
}

/* 9c. 站内引用块:三枚选择器,按「兜住的面从大到小」排(顺序见段头)
   ★ 站点把 [quote] 渲染成 blockquote.blockquote3 + 内层 h6.quote2「引用」+ 一个 div
     (真机抓到的原始标记:<blockquote class="blockquote3"><h6 class="quote2">引用</h6><div>…</div>
      </blockquote>,tid=2142330),[blockquote] 与「购买可见」正文用的是 blockquote.blockquote。
     ★ 站点原样(白昼实测):blockquote3 = #f8f8f8 底 + 4px #ccc 左边线 + 12px #555 字。 */
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 blockquote,
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 blockquote.blockquote,
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 blockquote.blockquote3 {
    background-color:rgba(255,255,255,.05) !important;
    background-image:none !important;
    border-color:var(--spp-rk-hair) !important;
    border-left:4px solid rgba(63,191,168,.45) !important;
    color:var(--spu-secondary) !important;
    -webkit-text-fill-color:var(--spu-secondary) !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 blockquote.blockquote3 div,
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 blockquote.blockquote div {
    color:var(--spu-secondary) !important;
    -webkit-text-fill-color:var(--spu-secondary) !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 h6.quote2 {
    /* 只换字色:那枚 16×16 的黄绿引号贴图是装饰,留着 */
    background-color:transparent !important;
    color:var(--spu-secondary) !important;
    -webkit-text-fill-color:var(--spu-secondary) !important;
}

/* 9d. 已购正文框(提取码 / 被解锁正文):绿族暗档托盘 + 正文白字
   ★ 必须排在 9c 之后:这枚 blockquote 身上也带着 .blockquote,同特异性时后者赢。 */
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 blockquote.spp-buy__body {
    background-color:rgba(63,191,168,.09) !important;
    background-image:none !important;
    border-color:rgba(63,191,168,.26) !important;
    border-left:3px solid rgba(63,191,168,.55) !important;
    color:var(--spp-rk-text) !important;
    -webkit-text-fill-color:var(--spp-rk-text) !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 blockquote.spp-buy__body a {
    color:var(--spp-rk-link) !important;
    -webkit-text-fill-color:var(--spp-rk-link) !important;
}

/* 9e. 付费提示条(站点原文一字不动,只换外观)——琥珀暗档 */
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 blockquote.spp-buy__warn {
    background-color:rgba(240,179,87,.12) !important;
    background-image:none !important;
    border-color:rgba(240,179,87,.30) !important;
    border-left:3px solid rgba(240,179,87,.60) !important;
    color:#f2c883 !important;
}
html.spp-rdark.spp-rdark.spp-rdark #main > form.spp-flow > div.t5.t2 blockquote.spp-buy__warn b {
    color:#f2c883 !important;
    -webkit-text-fill-color:#f2c883 !important;
}
/* ===== SPP-READDARK-END ===== */
`;
    const RDARK_LIFT = 'spp-rk-lift';

    function initReadDark() {
        /* ★★ 2.55.31(用户:「对于回贴,在黑夜模式下没有变黑、底部文本变成透明背景影响阅读
           (需要根据黑夜、非黑夜增加背景)」):门从「仅帖子页」放宽到「帖子页 + 回贴页(post.php)」。
           活体量的根因:回贴页整页是白的 —— 表单容器 .t.f_one 的底 = rgba(250,252,255,.93)
           (星空那层玻璃规则给的),而这一页的 html 上从来没有 spp-rdark ⇒ 黑夜模式下只有它不跟着变。
           ★ 放宽门不会顺手改到帖子页:这里所有规则的选择器都写着 #main > form.spp-flow /
             div.t5:has(textarea[name="atc_content"]),回贴页一个都不命中;
             回贴页那几条反过来也只在 html.spp-post-on 下生效。 */
        if (!(WANTS.readOnly || PAGE.post)) return;

        /* 样式无条件注入:开 / 关只差 <html> 上那一枚标(理由见段头)*/
        if (!document.getElementById('spp-rdark-style')) {
            const st = document.createElement('style');
            st.id = 'spp-rdark-style';
            st.textContent = CSS_READ_DARK;
            appendToHeadSafe(st);
        }
        if (!SPP_CONFIG.bool('spp_read_dark', false)) return;      // 关态:DOM 上一个字都不写

        document.documentElement.classList.add('spp-rdark');

        /* ④ 发帖人内联写的深色字 → 按对比度提亮(15f 的反向孪生)。
           候选集与 15f 完全一致(只挑「自己声明过颜色」的元素),判据同一套 WCAG 相对亮度,
           阈值也同为 2.2。两段不会互相打架:暗色下背景已经变暗,15f 那条判据自然不成立
           (浅字压深底的对比度远大于 2.2),同时开着也只有一边真正动手。 */
        function lum(rgbText) {
            const m = /rgba?\((\d+),\s*(\d+),\s*(\d+)/.exec(rgbText || '');
            if (!m) return null;
            const ch = function (v) { v = (+v) / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
            return 0.2126 * ch(m[1]) + 0.7152 * ch(m[2]) + 0.0722 * ch(m[3]);
        }
        /* 往上找第一个「不透明」背景色:正文自己通常是 transparent,真正发暗的是行玻璃那一层 */
        function bgLum(el) {
            let e = el;
            while (e && e !== document.documentElement) {
                const bg = getComputedStyle(e).backgroundColor;
                const m = /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?/.exec(bg || '');
                if (m && (m[4] === undefined || +m[4] > 0.5)) return lum(bg);
                e = e.parentElement;
            }
            return lum(getComputedStyle(document.body).backgroundColor);
        }

        let lifted = 0;
        function scan() {
            const boxes = document.querySelectorAll('#main > form.spp-flow > .t5.t2 .tpc_content');
            for (let b = 0; b < boxes.length; b++) {
                const cands = boxes[b].querySelectorAll('[style*="color"], font[color]');
                for (let i = 0; i < cands.length; i++) {
                    const el = cands[i];
                    if (el.classList.contains(RDARK_LIFT)) continue;
                    /* 排除 background-color / border-color 这类同含 "color" 的声明 */
                    const st = el.getAttribute('style') || '';
                    if (!/(^|;)\s*color\s*:/.test(st) && !el.hasAttribute('color')) continue;
                    const L1 = lum(getComputedStyle(el).color);      /* 内联色不可靠,一律取计算值 */
                    if (L1 === null) continue;
                    const L2 = bgLum(el);
                    if (L2 === null) continue;
                    const hi = Math.max(L1, L2), lo = Math.min(L1, L2);
                    if ((hi + 0.05) / (lo + 0.05) < 2.2) { el.classList.add(RDARK_LIFT); lifted++; }
                }
            }
            /* 读数口挂给探针 / 控制台(不参与逻辑,同 15f 的 data-spp-whitefix)*/
            try { document.documentElement.setAttribute('data-spp-darklift', String(lifted)); } catch (e) {}
        }

        /* ⑤ 内联逃生口 —— 只钉**实测没亮起来**的那几枚。
           为什么需要:星空主题给链接写了一条超长 a:not(…) 链(带 !important、特异性很高),
           样式表这侧改不动(2.52.43 的老账);操作条那几枚链接于是留在 #475569 这种深灰上。
           与老账的唯一区别:先按对比度筛,只有真没过的才写内联 —— 一页几十枚,读数挂在
           data-spp-darkpin 上;发帖人自己写的颜色(上一段 lift 管的那些)不在此列,绝不误伤。 */
        let pinned = 0;
        function pinUnreadable() {
            const rows = document.querySelectorAll('#main > form.spp-flow > .t5.t2');
            const SEL = '.tiptop a, .tiptop b, .tiptop span:not(.spp-tp-sep), .user-info.spp-ucard a, .user-info.spp-ucard span';
            for (let r = 0; r < rows.length; r++) {
                const cands = rows[r].querySelectorAll(SEL);
                for (let i = 0; i < cands.length; i++) {
                    const el = cands[i];
                    if (el.dataset.sppRkPin === '1') continue;
                    if (!(el.textContent || '').trim()) continue;             // 没字的(纯图标)不钉
                    const L1 = lum(getComputedStyle(el).color);
                    if (L1 === null) continue;
                    const L2 = bgLum(el);
                    if (L2 === null) continue;
                    const hi = Math.max(L1, L2), lo = Math.min(L1, L2);
                    if ((hi + 0.05) / (lo + 0.05) >= 2.2) continue;           // 已经够亮的一律不碰
                    el.style.setProperty('color', 'var(--spu-secondary, #b6c3d6)', 'important');
                    el.style.setProperty('-webkit-text-fill-color', 'var(--spu-secondary, #b6c3d6)', 'important');
                    el.dataset.sppRkPin = '1';
                    pinned++;
                }
            }
            try { document.documentElement.setAttribute('data-spp-darkpin', String(pinned)); } catch (e) {}
        }

        function pass() { scan(); pinUnreadable(); }

        onDomReady(function () {
            setTimeout(pass, 600);
            setTimeout(pass, 2400);
        });
        /* 楼层被展开 / 动态插入时重扫(防抖 400ms;两段都是幂等的,命中过的会跳过)*/
        onDomReady(function () {
            const host = document.querySelector('#main > form') || document.getElementById('main');
            if (!host || !window.MutationObserver) return;
            let t = 0;
            new MutationObserver(function () {
                clearTimeout(t);
                t = setTimeout(pass, 400);
            }).observe(host, { childList: true, subtree: true });
        });
    }

    // ==================== 15. 默认头像换成 ID 块 + 头像区那行名字恒定显示(仅 read.php) ====================
    // ★★ 2.53.4 校标题:旧标题写「隐藏默认头像,仅显示用户名」,与本模块的实际行为**相反** ——
    //   现在不隐藏任何东西:站点那个 .user-pic 容器留着(首字母块就插在它里面当容器),
    //   站点在框下面印的那行用户名才是被摘的对象(框里已经写着整段 ID 了)。
    //   标题照实现改,免得后人按旧标题去 .r_two 上找 display:none。
    // ★★ 2.53.42 又校一次标题,这回是**范围**:本模块自此兼管「头像区那行名字」的两种活法 ——
    //   换过彩色块的楼层把框下那行收掉,没换的楼层(图床真头像、以及整个开关关掉的情形,
    //   两种都没有块)把名字**叠在头像框下沿**。
    //   为什么落到本模块头上:2.53.7 那条「左栏收口」是页面级 CSS,把没换块楼层那行名字
    //   一起压成了 0 高 —— 用户:「无论开启还是关闭,都希望用户有 ID」。收口的判据天然就是
    //   「框里是否已经写着 ID」,而那枚事实只有本模块知道 ⇒ 由本模块打标记、CSS 照标记分派。
    //   ★ 本模块**没有**新增开关:名字行不给关(那是身份信息,不是装饰)。
    //     开关仍然只管一件事 —— 要不要把站点默认头像换成彩色块。
    // ★★ 2.53.4 修复②的配套出口:本模块把「补量一趟」挂在这个模块级变量上,
    //   供 15d(initAutoExpandHidden)展开折叠楼层后主动喊一声。
    //   为什么需要它:15d 的展开只删一条内联 display(**纯 attributes 变化**),而本模块的
    //   MutationObserver 只监听 childList ⇒ 展开这个动作对本模块是不可见的;不主动喊,
    //   展开后就不会有任何一趟来量,字号会永久停在 CSS 兜底值上被 overflow:hidden 裁掉。
    //   ★ 刻意不给 observer 加 attributes 监听:我们自己每写一次内联 font-size 都会触发它,
    //     那是倍数级的自激(同 2.49.0 与 initReadFlow 注释里记的那个坑)。
    let sppAvaRefit = null;
    // ★★ 2.53.9:还原出口(面板「恢复默认头像」按钮调的就是它)。与 sppAvaRefit 同一手法 ——
    //   模块内部的函数挂到模块级变量上,外部只认这个口,不反向依赖 15c 的内部实现。
    let sppAvaRestore = null;
    function initHideDefaultAvatar() {
        if (!WANTS.readOnly) return;

        const KEY = 'spp_hide_default_avatar';
        // ★★ 2.53.9:默认值 true → **false**(用户:「…默认关闭」)。
        //   ★ 这里必须写成**字面量键名**、不能用上面那个 KEY:tools/verify/settings-panel.js 的 C/D 段
        //     是按 SPP_CONFIG.bool('spp_hide_default_avatar', false) 这串原文去脚本里找读数点的,
        //     换成变量形式那道守卫会判「找不到应存在的调用」。
        let enabled = SPP_CONFIG.bool('spp_hide_default_avatar', false);   // ★ 2.52.93:走集中读数层

        // 2.52.48:不再隐藏 .user-pic(那会把首字母块一起藏掉),改为把 <img> 换成 .spp-ava-initial。
        // 样式统一放在 READ_CSS 的「2.52.48」段里,这里只做逻辑。
        // ★ 用户定的规矩:只有【站点默认头像】或【头像链接已失效】才用名字首字母;
        //   真实上传的头像一律不动 —— 见下面 judgeAva()。
        const AVA_GRADS = [
            'linear-gradient(145deg,#34c39b,#2b8fd6 55%,#8a72e0)',
            'linear-gradient(145deg,#f0a35c,#e2607a 55%,#a262d6)',
            'linear-gradient(145deg,#4aa8e8,#2f7fd0 60%,#3fc49b)',
            'linear-gradient(145deg,#5cc98a,#2fae86 55%,#3fa8d8)',
            'linear-gradient(145deg,#e08a5c,#d4648f 60%,#8f6ad8)',
            'linear-gradient(145deg,#4f8fe0,#5a7fd8 55%,#8f7ae0)'
        ];
        function avaGrad(name) {                       // 同一个名字永远同一块渐变
            let h = 0;
            for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0;
            return AVA_GRADS[h % AVA_GRADS.length];
        }
        // 2.52.98(用户批注「ID放置图片框内,而不是只显示一个'冰'字」):整段 ID 进框。
        // ★★ 2.53.4:字号从「按字数挑档」改成「按真实排版量出来的自动适配」
        //   (用户:「没有图床或默认图床的用户头像的名字 UI 版块,它的字体,尤其是中文字体
        //    太大了,需要自动调整一个合适的大小,并在必要时进行换行」)。
        //   · 旧档位只认字数、认不出字宽与折行:实测同一列里 2 字的「芜湖」46px、
        //     8 字的「lian_798」17px —— 两种尺度在同一列里对不上,用户圈的就是前一种。
        //   · 规则两档,先一行、后折行:
        //     ① 先在 [--spp-ava-fs-min, --spp-ava-fs-max] 里二分出「一行放得下」的最大字号;
        //     ② 若①的结果小于 --spp-ava-fs-one(一行已经小到不好看)⇒ 允许折行再二分,
        //        折行分支只准「不小于」单行分支(Math.max 钳死,理由见下面修复③)。
        //   · 尺子仍只有一把:三个边界写在 READ_CSS 的 .spp-ava-initial 上(--spp-ava-fs-*),
        //     JS 只在这些边界之间二分,自己不写死像素。
        //   · 量的是内层 .spp-ava-txt:块级 + overflow-wrap:anywhere ⇒ 它的 rect 就是折行后的真实文本块。
        //   · 隐藏楼层(站点折叠时 display:none)里量到的是 0×0 ⇒ 不打 sppFit 标记,
        //     留给下一轮扫描补量(见 processPosts 末尾那趟补量)。
        // ★★ 2.53.4 修复①:旧版(≤2.53.3)按字数分档时给块挂的那套字号类 —— is-s1…is-s9 / is-sX。
        //   为什么要清:旧表那条 .spp-ava-initial.is-s9{font-size:17px !important} 比本块基类
        //   多一枚类(特异度 0,6,1 vs 基类 0,5,1),两边都是 !important 时它赢;
        //   页面上残留旧样式时,基类那条 .spp-ava-initial{font-size:32px !important} 整条压不住,
        //   块里字号会被分档类钉死成旧值(实测 12 枚里 10 枚 17px、「芜湖」46px —— 正是用户圈掉的「太大」)。
        //   ⇒ 块上只准留 spp-ava-initial 这一个类:新建路径由 className 一次赋值保证,
        //     残留老块那一半在 refitPending()/补量趟里清(paintInitial 会被 th.dataset.sppAvaDone
        //     挡在门外,老块轮不到它重建,只有补量趟够得着)。
        const LEGACY_BUCKET_CLS = ['is-s1', 'is-s2', 'is-s3', 'is-s4', 'is-s5', 'is-s6', 'is-s7', 'is-s8', 'is-s9', 'is-sX'];
        function fitTile(tile) {
            if (tile.dataset.sppFit) return;
            const txt = tile.firstElementChild;
            if (!txt || !txt.firstChild) return;
            const box = tile.getBoundingClientRect();
            if (box.width < 8 || box.height < 8) return;
            const cs = getComputedStyle(tile);
            // 兜底值与 READ_CSS 那三把尺逐字对齐(改一处必须两处同改)
            const max = parseFloat(cs.getPropertyValue('--spp-ava-fs-max')) || 26;
            const min = parseFloat(cs.getPropertyValue('--spp-ava-fs-min')) || 11;
            const one = parseFloat(cs.getPropertyValue('--spp-ava-fs-one')) || 24;
            const availW = tile.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight);
            const availH = tile.clientHeight - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom);
            // ★★ 2.53.6:折行判据还按行数算(probe 的第二条),但**是否折行**由下面那句 if 决定 ——
            //   口径是用户给的:「只在该 ID 的长度导致无法在一行内显示时,才考虑换行」。
            const probe = (px, maxLines) => {
                tile.style.setProperty('font-size', px + 'px', 'important');   // ★ 必须 !important:本块基类那条 font-size 也是 !important
                const r = txt.getBoundingClientRect();
                const lh = parseFloat(getComputedStyle(txt).lineHeight) || px * 1.12;
                // ★★ 2.53.4 修复③:行数判据从「严格 Math.round(高/行高)」改成 ceil(高/行高 - 0.06)。
                //   那个 0.06 行是**亚像素容差**,不是放松折行上限:两行文本实测 高/行高 往往算成
                //   1.9998 或 2.0003(getBoundingClientRect 是浮点、行高又是 1.12 的倍数),
                //   纯 ceil 会把 2.0003 抬成 3 行、把本该通过的那一档否掉(比例尺越界就掉档);
                //   0.06 远小于 1 行 ⇒ 真的多出一行(比值 ≥ 2.9x)照样判成 3 行,上限没变,只吸掉舍入。
                return r.width <= availW + .5 && r.height <= availH + .5
                    && Math.ceil(r.height / lh - 0.06) <= maxLines;
            };
            const search = (maxLines) => {
                let lo = Math.ceil(min), hi = Math.floor(max), best = 0;
                while (lo <= hi) {
                    const mid = (lo + hi) >> 1;
                    if (probe(mid, maxLines)) { best = mid; lo = mid + 1; } else { hi = mid - 1; }
                }
                return best;
            };
            // ★★ 2.53.6 口径(用户定):「字体最小为 11px;**只有**在该 ID 的长度导致无法在一行内显示时,
            //   才考虑换行」。所以这里的 if 只看一个条件 —— 一行里到底放不放得下(best === 0),
            //   而不是「单行字号已经小于 24」:
            //     · best > 0(一行放得下)⇒ 字号就是它,哪怕它只有 14px(14 字符 ID 的单行上限实测 14px)
            //       —— 14px ≥ 11px 的下限,用户明确要的正是这种「老实缩一点、别折行」;
            //     · best === 0(连 11px 都在一行里放不下)⇒ 才允许折行,折到 3 行再取最大字号。
            //   旧写法 `if (size < one)` 会把「一行放得下但只有 14px」误判成需要折行,
            //   与用户这次给的口径相反(那会让 8 位十六进制 ID 也折成两行)。
            let size = search(1);
            // ★ 折行分支只准「不小于」单行分支:search(3) 不是在大区间上重跑,只是同一区间放宽行数上限,
            //   而 probe 的两条判据(宽/高 + 行数)在浮点边界上并不严格单调,实测能返回更小的值;
            //   一旦取小值就等于「允许折行」反而把字缩小了(与立意相反)。Math.max 钳成「只可能变大或不变」。
            if (!size) size = Math.max(search(3), Math.ceil(min));
            // -one 保留为「一行已经小到不好看」的表态值,供将来调档;当前口径下不参与判断。
            void one;
            tile.style.setProperty('font-size', size + 'px', 'important');
            tile.dataset.sppFit = String(size);
        }
        // 2.53.4:站点在头像框**下面又印了一遍用户名**(div[align=center] 里的 <br> + <a><strong>名字</strong></a>
        //   + 两个 <br>)。框里已经写着 ID,这行就是重复 —— 用户:「底部框选的那个名字可以不用了」。
        //   ★ 只标不摘:那一层里还挂着 span.user-info,它是**悬停卡的数据源**(initUserStatusCard 就是按
        //     showface_N 找它的)、也是站点自己 read.open / showface 的锚点 —— 摘掉节点悬停卡就没数据了,
        //     所以这里只能打类(.spp-avaname-off → CSS display:none),一个节点都不许删。
        //   ★ 只对「换过块」的楼层生效(本函数只由 paintInitial 调用),真头像楼层那行照旧
        //     —— 那种楼层没有块,名字是它唯一的身份显示。
        //   ★ 作用域与 READ_CSS 里 .spp-ava-initial 那套(.t5.t2)对齐:.t5.one 没有那套样式,
        //     要是在那里把名字摘了,左边就什么都不剩 —— 宁可不摘。
        //   ★★ 2.53.4 修复④:从「摘第一个 <a> + 该层所有 <br>」收紧成「只摘认得出的、只收该收的」:
        //     · 那一枚 <a> 必须**确实是去用户主页的链接**(href 里能认到 u.php:实站是
        //       u.php?action-show-uid-203218.html,站点自己的跳转也写成 u.php?action=show&username=…);
        //       认不出来就**一个都不动** —— 这一层里还可能有别的 <a>(站点改版加什么都有可能),
        //       按「第一个 <a>」摘,摘错就等于把左栏唯一的身份信息删了。宁可不摘,别摘错。
        //     · <br> 同理只收「名字那一行」相关的那两枚(紧邻该 <a> 前后各一枚):原来整层扫会把
        //       user-info 那段的换行也收掉,悬停卡的内容于是被挤成一行 —— 无法确定只跟名字相关时,
        //       宁可少收,不收全部。
        function dropNameLine(th) {
            if (!th.closest('.t5.t2')) return;
            const wrap = th.querySelector(':scope > div[align="center"]');
            if (!wrap || wrap.dataset.sppNameOff) return;
            const kids = Array.prototype.slice.call(wrap.children);
            let idx = -1;
            for (let i = 0; i < kids.length; i++) {
                if (kids[i].tagName !== 'A') continue;
                if (/u\.php/i.test(kids[i].getAttribute('href') || '')) { idx = i; break; }
            }
            if (idx < 0) return;            // 认不出用户主页链接 ⇒ 一个都不动(宁可不摘,别摘错)
            wrap.dataset.sppNameOff = '1';
            kids[idx].classList.add('spp-avaname-off');
            for (const d of [-1, 1]) {      // 名字上下的空行一起收(只收紧邻的那两枚 <br>)
                const sib = kids[idx + d];
                if (sib && sib.tagName === 'BR') sib.classList.add('spp-avaname-off');
            }
        }
        // 用户名:取「第一个有文字的 <a>」并排除头像外链(.user-pic 里那枚没有文字)
        function userNameOf(th) {
            const links = th.querySelectorAll('a');
            for (let i = 0; i < links.length; i++) {
                if (links[i].closest('.user-pic')) continue;
                const t = (links[i].textContent || '').trim();
                if (t) return t;
            }
            return '';
        }
        // 把头像换成首字母块:保留原 <a>(含站点 onmouseover 弹窗与跳转),只把 <img> 藏掉
        function paintInitial(th, img) {
            if (th.dataset.sppAvaDone) return;
            th.dataset.sppAvaDone = '1';
            const nm = userNameOf(th);
            const holder = img.closest('a') || img.parentElement;
            if (!holder) return;
            if (holder.querySelector('.spp-ava-initial')) return;
            // ★★ 2.53.42:给这一楼打标记 —— CSS 那两条「头像区名字行」规则全靠它分派:
            //   有这枚类 ⇒ 收口(框里已经写着整段 ID);没有 ⇒ 名字叠在头像框下沿。
            //   ★ 刻意打在这一行(彩色块**真正插进 DOM 之前**的最后一步),而不是 dropNameLine:
            //     dropNameLine 认不出用户主页链接时会主动放弃(宁可重复不可摘错),标记挂它身上
            //     就会出现「块里有 ID、框下却没收口」;挂这里则恒为「块在 ⇒ 收口生效」,
            //     最坏也只是藏掉一行重复信息,永远不会回到「一个 ID 都没有」。
            th.classList.add('spp-ava-tiled');
            const full = String(nm || '').trim() || '?';
            const tile = document.createElement('span');
            // 2.52.98:整段用户名(原先只取首字);2.53.4:字号由 fitTile() 量出来,不再分档
            // ★★ 2.53.4 修复①:这里用**一次 className 赋值**,只留 spp-ava-initial 这一个类 ——
            //   等于顺手把旧版那套字号分档类(is-s1…is-s9 / is-sX)清干净。
            //   为什么非清不可:旧表那条 .spp-ava-initial.is-s9{font-size:17px !important} 比本块基类
            //   多一枚类(特异度 0,6,1 vs 0,5,1),同为 !important 时旧声明赢;页面上残留旧样式表时,
            //   基类 .spp-ava-initial{font-size:32px !important} 整条压不住,块里字号就被分档类钉死成
            //   旧值(实测 12 枚里 10 枚 17px、「芜湖」46px,正是用户圈掉的「太大」)。
            //   ★ 残留老块(旧 JS 建的、带着 is-s9)走不到这一行 —— paintInitial 会被
            //     th.dataset.sppAvaDone 提前挡回;那半边由 processPosts 的补量趟 refitPending() 清。
            tile.className = 'spp-ava-initial';
            const txt = document.createElement('span');
            txt.className = 'spp-ava-txt';
            txt.textContent = full;
            tile.appendChild(txt);
            tile.style.backgroundImage = avaGrad(nm || '?');
            // ★ 不再给整块挂 aria-hidden:2.53.4 起框里的 ID 是这一格**唯一**的名字
            //   (下面那行站点用户名已被摘掉),挂上去等于把这一列读屏信息清空 ——
            //   改挂给外层 <a>,链接的可访问名就是用户名。
            if (!holder.getAttribute('aria-label')) holder.setAttribute('aria-label', full);
            holder.insertBefore(tile, img);
            img.classList.add('spp-ava-off');
            fitTile(tile);          // 量完立刻定字号(在 DOM 里量,插入后同步做)
            dropNameLine(th);       // 框里已有 ID ⇒ 摘掉框下那行重复的用户名
        }
        function judgeAva(th, img) {
            if (th.dataset.sppAvaDone || th.dataset.sppAvaScan) return;
            th.dataset.sppAvaScan = '1';
            const src = img.getAttribute('src') || '';
            const lazy = img.getAttribute('data-src') || img.getAttribute('data-original') || '';
            if (/none\.gif/i.test(src) || /none\.gif/i.test(lazy)) { paintInitial(th, img); return; }
            // 真实头像:只有确认拉不到(naturalWidth 为 0)才兜底,能显示的一律放过
            const chk = () => { if (img.naturalWidth === 0) paintInitial(th, img); else th.dataset.sppAvaDone = '1'; };
            if (img.complete) chk();
            else img.addEventListener('load', chk, { once: true });
            img.addEventListener('error', () => paintInitial(th, img), { once: true });
        }

        // 补量一趟:只量「还没打标记」的块(幂等标记 = 量过就跳过;展开后由 15d 把标记清掉再来)。
        // ★ 先清旧版分档类再量:残留老块不先清类,基类那条 font-size 压不住分档类(见文件上方 ① 的推导)。
        // ★ 末尾把它挂到模块级变量上:15d 展开折叠楼层时是纯 attributes 变化,本模块的 observer
        //   (只听 childList)看不见,必须由 15d 主动喊一声 —— 这就是那个出口。
        function refitPending() {
            document.querySelectorAll('.spp-ava-initial:not([data-spp-fit])').forEach(t => {
                LEGACY_BUCKET_CLS.forEach(c => t.classList.remove(c));
                fitTile(t);
            });
        }

        // ★★ 2.53.9(用户:「增加一个功能,用于恢复用户的默认头像」):
        //   把已经被本脚本换掉的头像还原成站点原样。★ 还原手法只有「撤标记、不重建节点」一条:
        //     ① 站点那枚 <img> **一直在 DOM 里** —— paintInitial 只是给它挂了 .spp-ava-off,
        //        由 READ_CSS 的一条 display:none 藏起来 ⇒ 复原 = 去掉这个类,不必重新造图(也就不会
        //        因为拿不到原始 src / 图床链接失效而恢复失败);
        //     ② 彩色块是脚本新建的节点,站点从来没有它 ⇒ 直接摘掉;
        //     ③ 站点自己的用户名行也只是被打了 .spp-avaname-off ⇒ 去类即复原(那一层一个节点都没删过)。
        //   ★ 同时把开关落盘写成 false:这枚按钮的语义是「恢复默认」,不落盘的话刷新一次
        //     整个页面又被换回去,等于没恢复。
        //   ★ th 上的 sppAvaDone / sppAvaScan 两个标记**不清**:清了之后 MutationObserver 会重扫,
        //     虽然 enabled 已经 false、processPosts 第一步就 return,但留着标记更保险(少一条会漏的路径)。
        //   ★ 返回值 = 本次摘掉的彩色块数量,供面板把按钮文案改成「已恢复 N 个头像」。
        function restoreDefaultAvatars() {
            enabled = false;
            try { GM_setValue(KEY, false); } catch (_) {}
            let n = 0;
            document.querySelectorAll('.spp-ava-initial').forEach(t => {
                const holder = t.parentElement;
                if (holder) {
                    const img = holder.querySelector('img.spp-ava-off');
                    if (img) img.classList.remove('spp-ava-off');   // 站点原图就在这一层,去类即回
                }
                t.remove();
                n++;
            });
            // 兜底两趟:万一以后 DOM 结构变了、图不在块的同级,别把它永远留在藏着的那半边;
            //   用户名那行同理(dropNameLine 是**按类**生效的,去类就复原,与块在不在无关)。
            document.querySelectorAll('img.spp-ava-off').forEach(img => img.classList.remove('spp-ava-off'));
            document.querySelectorAll('.spp-avaname-off').forEach(el => el.classList.remove('spp-avaname-off'));
            // ★ 2.53.42:连「换过块」的标记一起抹掉 —— 少了它,恢复默认头像后那一楼仍被当成
            //   换过块:名字行继续被收口(不叠名字条),实测就是一个没有 ID 的楼层。
            document.querySelectorAll('.spp-ava-tiled').forEach(el => el.classList.remove('spp-ava-tiled'));
            document.querySelectorAll('div[data-spp-name-off]').forEach(w => { delete w.dataset.sppNameOff; });
            return n;
        }
        sppAvaRestore = restoreDefaultAvatars;   // ← 面板那枚按钮的入口

        function processPosts() {
            if (!enabled) return;
            // ★★ 2.53.4 修复②:扫描范围去掉 .t5.one .r_two —— 本块的**全部样式**都只认 .t5.t2
            //   (READ_CSS「2.52.48 首字母头像块」段整段以 .t5.t2 起头),dropNameLine 的作用域判断
            //   也是 th.closest('.t5.t2')。.t5.one 楼层扫进来只会得到一个「有渐变块、却没有任何块样式」
            //   的半成品:块不居中、没定尺/圆角/白字,站点那枚真头像图还在旁边并排显示。宁可不做。
            document.querySelectorAll('.t5.t2 .r_two').forEach(th => {
                const img = th.querySelector('.user-pic img');
                if (!img) return;
                judgeAva(th, img);
            });
            // 2.53.4:换过块、但当时还没上屏(站点折叠着 / 头像图还没解码)的,在这一趟补量一次。
            refitPending();
        }
        sppAvaRefit = refitPending;      // ← 15d 展开楼层后调的就是它(见那个模块的注释)

        // 初始扫描
        onDomReady(() => setTimeout(processPosts, 500));

        // MutationObserver 处理动态加载
        onDomReady(() => {
            const target = document.getElementById('main') || document.querySelector('.t5')?.parentElement || document.body;
            new MutationObserver(() => processPosts()).observe(target, {
                childList: true, subtree: true, attributes: false
            });
        });
    }

    // ==================== 15j. 底部位置栏补「本贴」(仅 read.php,2.53.49)====================
    // 用户(确认过的理解):「底部的位置栏相比顶部的位置栏,缺少了『本贴』的位置」——
    //   帖子正文下方、快速发帖框上方那条面包屑,站点**天生没有最后一节标题**。
    //   ★ 实测(登录态真页,Chrome DevTools MCP 取数,tid-2934530):
    //     顶部 #breadcrumbs:`[img] 南+ South Plus 茶馆 酒店偷拍赛博听墙根2:…`(有标题节,
    //       结构是 …<strong><a href="read.php?tid-N.html">标题</a></strong> + 一枚 rss 小图标);
    //     底部 #breadcrumbs(就是带「帖子浏览记录 / 版块浏览记录」两枚链接那条):
    //       `[img] 南+ South Plus 茶馆 帖子浏览记录 版块浏览记录`(没有标题节)。
    //   ⇒ 把顶部那节 <strong> 原样**克隆**过来(不自己拼字符串 —— 标题里的引号/书名号/全角字符
    //     与 href 都跟着真实节点走),插在右侧链接组(.fr)之前,打 .spp-crumb-cur 标供 CSS 上色。
    //   ★ 幂等:已带 .spp-crumb-cur 就跳过;只有一条 #breadcrumbs(首页/搜索页)时直接返回。
    function initCrumbTitle() {
        if (!WANTS.readOnly) return;
        const all = document.querySelectorAll('#breadcrumbs');
        if (all.length < 2) return;
        /* 找模板:第一条带 <strong><a> 的(真页上就是顶部那条);它自己不能是刚补出来的 */
        let tpl = null;
        for (let i = 0; i < all.length; i++) {
            if (all[i].querySelector('.spp-crumb-cur')) continue;
            const s = all[i].querySelector('strong > a');
            if (s) { tpl = all[i].querySelector('strong'); break; }
        }
        if (!tpl) return;

        let added = 0;
        for (let i = 0; i < all.length; i++) {
            const c = all[i];
            if (c.contains(tpl)) continue;                                   /* 模板自己那条 */
            if (c.querySelector('strong > a') || c.querySelector('.spp-crumb-cur')) continue;
            const node = tpl.cloneNode(true);
            node.className = 'spp-crumb-cur';
            /* rss 小图标不重复搬(底部那行没它的位置)*/
            Array.prototype.forEach.call(node.querySelectorAll('img'), function (im) { im.remove(); });
            const fr = c.querySelector(':scope > .fr') || c.querySelector(':scope > div.fr');
            if (fr) c.insertBefore(node, fr); else c.appendChild(node);
            added++;
        }
        /* 读数口(探针/控制台用,不参与逻辑)*/
        try { document.documentElement.setAttribute('data-spp-crumbtitle', String(added)); } catch (e) {}
    }

    // ==================== 15a-2. 含图楼层索引悬浮窗(仅 read.php,2.53.28)====================
    // 用户需求(原话):「右边有一个悬浮窗,能定位有图片的楼层然后可以通过点击链接跳转,
    //   比如楼 GF、B30;但是对于有多张图在同一楼层的,要有输入框(总计图片数)」;
    //   预览页里定案 **方案 B**,并补充「有多张图片和一张图片的一样要有数字」⇒ 每行都带输入框。
    // 实测(read.php?tid-2966403):31 层里 DOM 带 img 的 7 层,剔掉表情/图标后真的含图只剩 3 层
    //   (GF=20 张 / B4F=1 / B30F=1);楼层号在 .tiptop > .fl,锚点是 th.r_one 的 id(td_tpc / td_<pid>)。
    // ★ 数量按 DOM 里的 img 个数算,**不看 naturalWidth** —— 懒加载的图此刻是 0×0,但它确实存在。
    // ==================== 2.53.72:南+ 品牌条(替换页眉那两块淘宝广告)====================
    /* 用户(附页眉截图):「这两张图片是广告。你需要先去除它们的广告标识符,然后将右侧的一部分
       宽边移至右边侧边,中间空出的区域用来显示内容(south-plus、南+ 等字样;可以添加动态效果)。」
       随后三轮样稿定到 v3:「南+ 以及那个框选的 UI 都不要了;取消字体滚动,改为常驻,确保字体和
       内容显示完整」⇒ 中间只留一行常驻的 SOUTH-PLUS(徽标与其方框已删)。
       ★★ 2.53.76 再次改写中缝(用户:「上方为三个图标,下方为变色字体,字体改为 South-Plus。
       上面短,下面长」+「直接照搬那个网页…不要修改它的动态效果…只是调整大小比例」):
       上行 = Uiverse white-newt-56 的三图标 loader(□ △ ○ 描边 + 红点,原件 CSS 一字未改);
       下行 = Uiverse quick-chicken-50 的扫光字按钮(空心描边,hover 时绿色扫光充填 + 外发光)。
       竖向方程:上带 + 间距 + 下带 = 0.90 × 条高、上带 = 0.90 × 下带 ⇒ 上小下大;
       96px 条 ⇒ 上带 39.5(--ik 0.898)、间距 3、下带 43.9(字号 37.9),合计 86.4。
     ★ 摘掉的是「广告身份」,不是图:外层
       <a href=…taobao/tmall… onclick="ga('send','event','…','clicked','ad')" target="_blank">
       整条拆掉(href / target / onclick / ad 事件标记随之消失),<img> 提到条里当纯装饰图。
     ★ 尺寸:两图 450x96 固定在左右两端;条宽 = **#main 实测宽**(2.53.73 起,不再钉 1260,
       于是条的左右沿与内容列两侧边框对齐);
       中缝 = 条宽 − 900 − 2(边框)⇒ 字号自适应:整词宽 = 8.955 × 字号(字距 .30em,
       线上实测 25px→223.88 / 32px→286.55),取 min(48, (中缝 − 40 − 16) / 8.955)。
       条 1400 ⇒ 中缝 498、可用 458、字号吃满 48px、整词 429.84、余量 28.16,整词完整不裁。
     ★ 末尾那一段字距必须抵掉:letter-spacing 在**末字之后**也生效
       (单字实测:32px 的 "S" 从 19.71 → 29.31),不抵的话整词在居中里右偏半段
       (32px 时 4.8px)—— CSS 里那句 margin-right:-.30em 就是干这个的。
     ★ 开关 spp_brand_bar(默认开,面板「功能开关」组):关掉则页眉一个像素都不碰。
       广告被拦截插件收掉时(找不到那两张图)同样什么都不做,保持站点原样。
     实测(2.53.71 真机 DOM):#header 里一个 inline 写死 width:920px / margin:2px 0 -5px 的 div,
     两个直系 <a> 各包一张 450x96 的图:/images/sswg9.jpg 与 /images/segucrwj4.jpg。 */
    function initBrandBar() {
        if (!SPP_CONFIG.bool('spp_brand_bar', true)) return;

        /* ★ 2.53.89(用户:「避免 ADGuard 插件将其识别为广告并进行屏蔽」):
           隔离动作 = 影子根 + 宿主换血 + 内联锁:
             ① 条的内容与样式全部搬进宿主的 shadowRoot —— 页面级 CSS(站点自己的 + 拦截器注入的
                cosmetic 规则)按规范**穿不过 shadow 边界**,内部节点等于进了保险箱;
             ② 宿主不再复用站点那个广告容器,另起一个没有 id/class 的空白 div 顶在同一位置 ——
                按位置写死的老规则(#header > div:nth-child(N))只能命中已经摘掉的旧槽位;
             ③ 宿主样式一律内联 !important(外观过滤也是 !important,而内联 !important 压过样式表)。
           从此这个模块**不往页面注入任何全局样式**,下面这段只活在影子根里。 */
        var BAR_CSS = `
            #spp-brand {
                position:relative; display:flex; align-items:center; box-sizing:border-box;
                width:100%; height:var(--bh,96px); border-radius:12px; overflow:hidden;
                /* ★ 2.53.83:三图之间留固定空隙,宽度由 fit() 反解条高后算出(JS 的 SP 与此同值) */
                gap:var(--sp,40px);
                border:1px solid rgba(255,255,255,.10);
                /* ★ 2.53.79(用户:「将背景区域改为黑玻璃磨砂背景,不透光。」):
                   纯黑不透明底 + 两层错位细白点阵(磨砂颗粒)+ 顶部玻璃高光;
                   旧底那层蓝黑渐变与中心绿雾撤掉,背后星野一像素都透不上来。
                   没用 backdrop-filter:底色既然不透光,后面没东西可糊,白耗 GPU。 */
                background-color:#05070a;
                background-image:
                    radial-gradient(rgba(255,255,255,.055) .5px, rgba(0,0,0,0) 1.1px),
                    radial-gradient(rgba(255,255,255,.035) .5px, rgba(0,0,0,0) 1.1px),
                    radial-gradient(120% 150% at 50% -12%, rgba(12,227,154,.10), rgba(0,0,0,0) 58%),
                    linear-gradient(180deg, #141a22 0%, #05080c 52%, #03050a 100%);
                background-size: 5px 5px, 9px 9px, 100% 100%, 100% 100%;
                background-position: 0 0, 2px 3px, 0 0, 0 0;
                box-shadow: 0 0 0 1px rgba(0,0,0,.6) inset, 0 1px 0 rgba(255,255,255,.07) inset,
                            0 18px 46px -28px #000;
            }
            #spp-brand::after { content:""; position:absolute; left:0; right:0; top:0; height:1px;
                background: linear-gradient(90deg, transparent, rgba(12,227,154,.6), transparent); }
            #spp-brand .spp-brand__art { position:relative; z-index:1; flex:0 0 var(--aw,450px); min-width:0; height:100%; overflow:hidden; }
            /* ★ 2.53.80:那一侧的图被屏蔽器打死且救不回来时收掉占位,不留空黑带 */
            #spp-brand .spp-brand__art--none { display:none; }
            #spp-brand .spp-brand__art img { display:block; width:100%; height:100%; max-width:none; object-fit:cover; }
            #spp-brand .spp-brand__mid { position:relative; z-index:1; flex:1 1 auto; min-width:0; height:100%;
                display:flex; align-items:center; justify-content:center; }
            /* ★ 2.53.85(用户:「将其分为上下两个区域:上方区域鼠标划过时触发翻页,下方区域
               则触发颜色褪色效果(不触发翻页)」):透视从 .mid 挪到 .stage,卡上压两块透明
               感应区,用兄弟选择器 ~ 驱动后面的卡片 —— 不依赖 :has(),感应区也不在卡里面,
               卡片怎么转都稳稳接住指针(2.53.84 那个「转过 90° 就丢 hover」的坑不再存在)。
               --zd 是分界像素值,由 fit() 按图标行带 + 半间距算出,正好落在图标与文字之间的空隙。 */
            #spp-brand .spp-brand__stage { position:relative; width:var(--cw,320px); max-width:100%; height:100%;
                perspective:900px; }
            #spp-brand .spp-brand__zone { position:absolute; left:0; right:0; z-index:3; }
            #spp-brand .spp-brand__zone--top { top:0; height:var(--zd,50%); cursor:default; }
            #spp-brand .spp-brand__zone--bot { top:var(--zd,50%); bottom:0; cursor:pointer; }
            #spp-brand .spp-brand__flip { position:relative; z-index:1; width:100%; height:100%;
                transform-style:preserve-3d; -webkit-transform-style:preserve-3d;
                transition:transform .8s cubic-bezier(.42,0,.18,1); }
            #spp-brand .spp-brand__zone--top:hover ~ .spp-brand__flip { transform:rotateY(180deg); }
            #spp-brand .spp-brand__zone--top:hover ~ .spp-brand__flip.spp-brand__flip--nocover { transform:none; }
            #spp-brand .spp-brand__face { position:absolute; inset:0; display:flex; align-items:center;
                justify-content:center; backface-visibility:hidden; -webkit-backface-visibility:hidden; }
            #spp-brand .spp-brand__face--cover { transform:rotateY(180deg); }
            #spp-brand .spp-brand__cover { display:block; width:100%; height:100%; object-fit:contain; }
            #spp-brand .spp-brand__unit { display:flex; flex-direction:column; align-items:center;
                justify-content:center; transform-origin:center center; }
            #spp-brand .spp-brand__row { display:flex; align-items:center; justify-content:center; flex:0 0 auto;
                height:calc(44px * var(--ik,0.898)); margin-bottom:var(--gap,3px);
                transform:scale(var(--ik,0.898)); transform-origin:center center; }
            /* ↓↓↓ 照搬 Uiverse white-newt-56(只加 #spp-brand 作用域、keyframes 加前缀) */
            #spp-brand .spp-brand__ld { --path:#fff; --dot:red; --duration:3s;
                width:44px; height:44px; position:relative; }
            #spp-brand .spp-brand__ld:before { content:''; width:6px; height:6px; border-radius:50%;
                position:absolute; display:block; background:var(--dot); top:37px; left:19px;
                transform:translate(-18px,-18px);
                animation:sppBrandDotRect var(--duration) cubic-bezier(0.785,0.135,0.15,0.86) infinite; }
            #spp-brand .spp-brand__ld svg { display:block; width:100%; height:100%; }
            #spp-brand .spp-brand__ld svg rect, #spp-brand .spp-brand__ld svg polygon,
            #spp-brand .spp-brand__ld svg circle { fill:none; stroke:var(--path); stroke-width:10px;
                stroke-linejoin:round; stroke-linecap:round; }
            #spp-brand .spp-brand__ld svg polygon { stroke-dasharray:145 76 145 76; stroke-dashoffset:0;
                animation:sppBrandPathTriangle var(--duration) cubic-bezier(0.785,0.135,0.15,0.86) infinite; }
            #spp-brand .spp-brand__ld svg rect { stroke-dasharray:192 64 192 64; stroke-dashoffset:0;
                animation:sppBrandPathRect 3s cubic-bezier(0.785,0.135,0.15,0.86) infinite; }
            #spp-brand .spp-brand__ld svg circle { stroke-dasharray:150 50 150 50; stroke-dashoffset:75;
                animation:sppBrandPathCircle var(--duration) cubic-bezier(0.785,0.135,0.15,0.86) infinite; }
            #spp-brand .spp-brand__ld--tri { width:48px; }
            #spp-brand .spp-brand__ld--tri:before { left:21px; transform:translate(-10px,-18px);
                animation:sppBrandDotTriangle var(--duration) cubic-bezier(0.785,0.135,0.15,0.86) infinite; }
            #spp-brand .spp-brand__ld { display:inline-block; margin:0 16px; }
            @keyframes sppBrandPathTriangle { 33% { stroke-dashoffset:74; } 66% { stroke-dashoffset:147; } 100% { stroke-dashoffset:221; } }
            @keyframes sppBrandDotTriangle { 33% { transform:translate(0,0); } 66% { transform:translate(10px,-18px); } 100% { transform:translate(-10px,-18px); } }
            @keyframes sppBrandPathRect { 25% { stroke-dashoffset:64; } 50% { stroke-dashoffset:128; } 75% { stroke-dashoffset:192; } 100% { stroke-dashoffset:256; } }
            @keyframes sppBrandDotRect { 25% { transform:translate(0,0); } 50% { transform:translate(18px,-18px); } 75% { transform:translate(0,-36px); } 100% { transform:translate(-18px,-18px); } }
            @keyframes sppBrandPathCircle { 25% { stroke-dashoffset:125; } 50% { stroke-dashoffset:175; } 75% { stroke-dashoffset:225; } 100% { stroke-dashoffset:275; } }
            /* ↑↑↑ white-newt-56 原件 CSS 结束 */
            /* ↓↓↓ 照搬 Uiverse quick-chicken-50(同样只加作用域与前缀) */
            #spp-brand .spp-brand__btn { margin:0; height:auto; background:transparent; padding:0;
                border:none; cursor:pointer; }
            /* ★ 2.53.78 真机补丁:南+ 把 text-align:center 与「长词可断」的 overflow-wrap 一起
               继承进来 —— 扫光层是绝对定位块,内容盒被 border-right:6px 咬掉 6px(内容 295.6 vs
               文字 301.6)⇒ ①那 6px 挤不下时浏览器在字里断行、②单行溢出的居中把整行左移 3px,
               实测底字 695.2 / 亮字 713.1(错位 17.9px)。原件在自家站点靠「正文 uiverse 没有
               断行点」这一个偶然条件才重合;换成 South-Plus 落在这种页面上条件就没了。
               把不断行与左对齐显式钉死 ⇒ 只溢出不断行,溢出那 6px 落在尾随 &nbsp; 上被
               overflow:hidden 吃掉,可见字形一个不缺,两份字形矩形逐像素重合。 */
            #spp-brand .spp-brand__btn, #spp-brand .spp-brand__actual, #spp-brand .spp-brand__sweep {
                white-space:nowrap; text-align:left; }
            /* ★ 2.53.87(用户:「那个字体也不用变成空心了,改成实心的」):这一层
               (.spp-brand__actual)是扫光层收拢之后露出来的那面 —— 它原来继承按钮的
               color:transparent("空心"的来源),补一句填充色就成实心。★ 2.53.89 色值对调后,
               它 = **绿色**那面;描边照旧继承,两面字重一致。两个字幕层几何完全相同。 */
            #spp-brand .spp-brand__actual { color:var(--text-stroke-color); }
            /* ★ 2.53.86(用户:「使其褪色时变为粉色」):原件这里写死 rgba(255,255,255,0.6),
               而它就是「褪色态」那一层(.spp-brand__actual 的空心描边)唯一的颜色来源 ——
               只换这一个值:形态(空心/1px/无辉光)、过渡与所有动画参数一字未动。
               ★ 2.53.87(用户:「我说的粉色是指桃花粉」):色值定为 #FFA3B8 —— 桃花粉那一路
               暖调浅粉(色相 346°、饱和 100%、明度 82%),比洋红系的艳粉暖、比樱花粉沉;
               alpha 提到 0.95,浅粉对比弱,1px 描边不给足不透明度就压不住黑玻璃。 */
            /* ★ 2.53.88(用户:「那个绿色的竖线能不能也去掉?」):那根竖线就是扫光层的
               border-right:6px —— 默认态它贴在文字右端(像光标),褪色态扫光层收拢到 0,
               它就贴到字头成了绿竖线。原件把宽度写成变量,所以这里只把变量改成 0px:
               扫光、过渡、字形位置全不动;内容盒从 295.6 恢复成 301.6,尾随的 &nbsp; 不再被裁,
               可见字形位置逐像素不变。 */
            /* ★ 2.53.89(用户:「默认为粉色,触发后改为绿色」):两个色值对调。别被变量名带偏 ——
                 --animation-color   = **默认**看到的那一层(扫光层 width:100%,盖在最上面)
                 --text-stroke-color = **触发后**看到的那一层(actual 层,扫光层收拢后露出来)
               所以现在是:默认 = 桃花粉实心 + 粉辉光;下半区悬停 = 绿实心(无辉光)。 */
            #spp-brand .spp-brand__btn { --border-right:0px; --text-stroke-color:#37FF8B;
                --animation-color:rgba(255,163,184,0.95); --fs-size:38px; letter-spacing:3px; text-decoration:none;
                font-size:var(--fs-size); font-family:"Arial"; position:relative; text-transform:uppercase;
                color:transparent; -webkit-text-stroke:1px var(--text-stroke-color); }
            /* ★ 2.53.83(用户:「默认显示为绿色高亮,鼠标悬停时才变为褪色效果」):
               只把两个状态的宽度与辉光对调 —— 0.5s 过渡、6px 光标条、描边值一字未动,
               元件行为仍是原件的「扫过去」,只是扫的方向反过来(默认满、悬停收)。 */
            #spp-brand .spp-brand__sweep { position:absolute; box-sizing:border-box;
                content:attr(data-text); color:var(--animation-color); width:100%; inset:0;
                border-right:var(--border-right) solid var(--animation-color); overflow:hidden;
                transition:0.5s; -webkit-text-stroke:1px var(--animation-color);
                filter:drop-shadow(0 0 23px var(--animation-color)); }
            /* ★ 2.53.85:褪色只归下半区(.spp-brand__zone--bot),与翻面彻底分开 ——
               指针在下半区时卡片不翻、绿字朝褪色走;指针移开即退回绿色高亮。
               原件那句 .spp-brand__btn:hover 照旧留着(按钮在下半区里,属于子集,看得出出处)。 */
            #spp-brand .spp-brand__btn:hover .spp-brand__sweep,
            #spp-brand .spp-brand__zone--bot:hover ~ .spp-brand__flip .spp-brand__sweep { width:0%; filter:none; }
            /* ↑↑↑ quick-chicken-50 原件 CSS 结束 */
            /* 本脚本自带的只读动效降级(系统开了「减少动态效果」时冻住,不动原件数值) */
            @media (prefers-reduced-motion: reduce) {
                #spp-brand .spp-brand__ld:before, #spp-brand .spp-brand__ld svg rect,
                #spp-brand .spp-brand__ld svg polygon, #spp-brand .spp-brand__ld svg circle { animation:none; }
                #spp-brand .spp-brand__sweep { transition:none; }
                #spp-brand .spp-brand__flip { transition:none; }
            }
        `;

        // ★★ 2.53.76:竖向预算(样稿实测)——上带 + 间距 + 下带 = STACK × 条高,且
        //   上带 = RATIO × 下带;下带高 = LINE_K × 字号(Arial normal 行高,样稿实测
        //   38px→44.0 / 40px→46.0);上带高 = ICON_H × --ik(原件 .loader 就是 44×44)。
        //   96px 条 ⇒ 间距 3、下带 43.9(字号 37.9)、上带 39.5(--ik 0.898),合计 86.4 = 90%。
        //   横向兜底:文字实测宽 = TXT_UNIT × 字号 + TXT_PAD(12 字 × 3px 字距是常数项,
        //   样稿实测 38px→302.04 / 40px→316.04,两点解出 7.0 与 36),超宽就整组等比缩。
        var FS_MAX = 48, FS_MIN = 12, PAD = 20;
        var STACK = 0.90, RATIO = 0.90, LINE_K = 1.157, ICON_H = 44, TXT_UNIT = 7.0, TXT_PAD = 36;
        // ★★ 2.53.80:站点那两张广告图的原始路径(按左右位置,文件名实测,2.53.72 起就靠它们认宿主)。
        //   屏蔽器把 src 改写成 data:/about:blank 时,靠它兜底再自己取一份。
        // ★★ 2.53.83:铺满算法要用的三个数(都是实测的自然尺寸比,不是估的):
        //   左/右广告 1800×384 ⇒ 4.6875:1;中间封面 300×90 ⇒ 3.3333:1。
        //   条宽 W、图间空隙 SP ⇒ 条高 H = (W − 2·SP) ÷ (4.6875×2 + 3.3333) = (W − 2·SP) ÷ 12.7083。
        //   H_MIN/H_MAX 只是防极端窗口把条拉成怪比例。SP 与 CSS 里 --sp 的默认值必须同值。
        var R_ART = 4.6875, R_CARD = 3.3333, SP = 40, H_MIN = 40, H_MAX = 130;
        // ★★ 2.53.80:站点那两张广告图的原始路径(按左右位置,文件名实测,2.53.72 起就靠它们认宿主)。
        //   ★ 2.53.83:第三项是中间翻面卡的封面(用户给的站点素材路径)。
        var ART_SRC = ['/images/sswg9.jpg', '/images/segucrwj4.jpg', '/images/colorImagination/logo-fall4.png'];

        function hostOf() {
            var header = document.getElementById('header');
            if (!header) return null;
            var kids = header.children, i, j;
            for (i = 0; i < kids.length; i++) {
                var el = kids[i];
                if (el.tagName !== 'DIV') continue;
                if (el.getAttribute('data-spp-brand') === '1') return el;
                var im = el.querySelectorAll('img');
                for (j = 0; j < im.length; j++) {
                    var src = im[j].getAttribute('src') || '';
                    // 站点自己那两张广告图的文件名(实测);换了图也不至于乱认别的 div
                    if (src.indexOf('images/sswg9') >= 0 || src.indexOf('images/segucrwj4') >= 0) return el;
                }
            }
            return null;
        }

        /* ★ 2.53.80(用户:「右侧的图片还是被当成广告屏蔽掉了。」):屏蔽器两条路,分别对付。
           ① 外观过滤 —— 元素还在,被 display:none / visibility:hidden / opacity:0 / max-*:0 藏住。
              内联 !important 顶回去:层叠里内联 !important 只输给动画与过渡,能压过屏蔽器那条规则。
           ② 网络过滤 —— 字节没下来(complete && naturalWidth === 0):自己用 GM_xmlhttpRequest
              取一份换成 blob: URL,屏蔽器按文件名匹配的规则就不再命中;连 src 都被改写成
              data:/about:blank 时,按左右位置回落到 ART_SRC 里的站点原始路径。
           两条都救不回来 ⇒ 收掉那一侧占位(.spp-brand__art--none),不留空黑带。 */
        function unhide(im) {
            im.style.setProperty('display', 'block', 'important');
            im.style.setProperty('visibility', 'visible', 'important');
            im.style.setProperty('opacity', '1', 'important');
            im.style.setProperty('max-width', 'none', 'important');
            im.style.setProperty('max-height', 'none', 'important');
        }
        /* ★ 2.53.82:这里**不能**直接调 fit() —— fit() 是 build() 内部的函数声明,
           refetch() 在模块作用域里够不着,调它就是 ReferenceError(还被同一个 try 吞掉、
           转头走 onFail 把刚救回来的图 display:none,2.53.80/81 就是这么翻的车)。
           改成把结果回调交给调用方:onOk 绑在 img 的 load 上(那一刻一定有像素)。 */
        function refetch(im, idx, onOk, onFail) {
            var fail = function () { if (onFail) onFail(); };
            var raw = im.getAttribute('src') || '';
            var url = /^https?:/i.test(raw) ? raw : (location.origin + (ART_SRC[idx] || ''));
            if (!/^https?:/i.test(url)) { fail(); return; }
            GM_xmlhttpRequest({
                method: 'GET', url: url, responseType: 'blob', timeout: 8000,
                onload: function (r) {
                    if (!r || r.status < 200 || r.status >= 300 || !r.response) { fail(); return; }
                    var u;
                    try { u = URL.createObjectURL(r.response); } catch (e) { fail(); return; }
                    if (onOk) im.addEventListener('load', function () { onOk(); });
                    im.src = u;
                    im.removeAttribute('srcset');
                },
                onerror: fail,
                ontimeout: fail
            });
        }

        function build() {
            var host = hostOf();
            if (!host || host.getAttribute('data-spp-brand') === '1') return;   // 幂等
            // ★ 2.53.80:原来要求「两张图都在」才动手 —— 屏蔽器收掉一张就整条不生效。
            //   现在只要还有一张就搭条(少的那侧收掉占位),并且先把被藏起来的图救回来。
            var imgs = Array.prototype.slice.call(host.querySelectorAll('img')).slice(0, 2);
            if (!imgs.length) return;         // 一张都找不到 ⇒ 不碰,保持站点原样
            imgs.forEach(unhide);
            host.setAttribute('data-spp-brand', '1');

            // ① 摘掉广告身份:整条 <a>(淘宝/天猫跳转 + ga 的 'ad' 事件)拆掉,只留 <img>
            imgs.forEach(function (im) {
                // ★ 2.53.89:把"广告指纹"一起抹掉 —— 规则多半靠这些属性认人,
                //   而 2.53.82 起被救回的图本来就是 blob:,按文件名匹配的规则也一并落空。
                im.removeAttribute('onclick');
                im.removeAttribute('srcset');
                im.removeAttribute('sizes');
                im.removeAttribute('loading');
                im.removeAttribute('referrerpolicy');
                im.removeAttribute('usemap');
                im.setAttribute('alt', '');
                // 带 ad / gg / banner / sponsor / adv / 推广 味道的标识一律摘掉
                var stink = /ad|gg|banner|sponsor|adv|tuiguang/i;
                if (stink.test(im.className || '')) im.removeAttribute('class');
                if (stink.test(im.id || '')) im.removeAttribute('id');
                var a = im.parentNode;
                if (a && a.tagName === 'A') a.parentNode.replaceChild(im, a);
            });

            // ② 搭品牌条:左图 / 中缝 / 右图
            var strip = document.createElement('div');
            strip.id = 'spp-brand';
            var artL = document.createElement('div');
            artL.className = 'spp-brand__art';
            var artR = document.createElement('div');
            artR.className = 'spp-brand__art';
            artL.appendChild(imgs[0]);
            artR.appendChild(imgs[1]);
            var mid = document.createElement('div');
            mid.className = 'spp-brand__mid';
            var unit = document.createElement('div');
            unit.className = 'spp-brand__unit';

            // 上行:三个图标 —— 标记与属性值照抄 white-newt-56(□ 64/8、△ 43 8 79 72 7 72、○ r32)
            var row = document.createElement('div');
            row.className = 'spp-brand__row';
            row.innerHTML =
                '<div class="spp-brand__ld"><svg viewBox="0 0 80 80">' +
                '<rect height="64" width="64" y="8" x="8"></rect></svg></div>' +
                '<div class="spp-brand__ld spp-brand__ld--tri"><svg viewBox="0 0 86 80">' +
                '<polygon points="43 8 79 72 7 72"></polygon></svg></div>' +
                '<div class="spp-brand__ld"><svg viewBox="0 0 80 80">' +
                '<circle r="32" cy="40" cx="40"></circle></svg></div>';

            // 下行:扫光字 —— 结构照抄 quick-chicken-50,正文换成 South-Plus。
            //   ★ 词连接符 &#8288;(U+2060) 是**必须**的:原件正文 "uiverse" 没有任何断行点,
            //     而 South-Plus 的连字符是断行点 —— 悬停层 box-sizing:border-box 又被
            //     border-right:6px 咬掉 6px 内容宽,于是只会在 "-" 处折成两行、
            //     亮字与底字错位(样稿实测错位 62.3px)。加词连接符后两份字形矩形完全一致
            //     (left/top/width 全同),CSS 一字未改。
            var btn = document.createElement('button');
            btn.type = 'button';
            btn.className = 'spp-brand__btn';
            btn.setAttribute('data-text', 'South-Plus');
            btn.innerHTML =
                '<span class="spp-brand__actual">&nbsp;South-&#8288;Plus&nbsp;</span>' +
                '<span aria-hidden="true" class="spp-brand__sweep">&nbsp;South-&#8288;Plus&nbsp;</span>';

            unit.appendChild(row);
            unit.appendChild(btn);

            // ★ 2.53.83:中缝 = 一张翻面卡。两面都塞进 .spp-brand__flip:
            //   --fx 面 = 上面那套图标 + 绿字(默认朝观众 ⇒「默认绿色高亮」看得见);
            //   --cover 面 = 用户给的封面图(站点 DOM 里没有它,自己 new 一个 <img>),
            //   悬停整卡 ⇒ 父层 rotateY(180deg) ⇒ 封面转过来。封面挂了就摘掉翻面(--nocover)。
            var flip = document.createElement('div');
            flip.className = 'spp-brand__flip';
            var faceFx = document.createElement('div');
            faceFx.className = 'spp-brand__face spp-brand__face--fx';
            faceFx.appendChild(unit);
            var faceCover = document.createElement('div');
            faceCover.className = 'spp-brand__face spp-brand__face--cover';
            var cover = document.createElement('img');
            cover.className = 'spp-brand__cover';
            cover.alt = '';
            cover.setAttribute('decoding', 'async');
            cover.src = location.origin + ART_SRC[2];
            cover.addEventListener('load', function () { fit(); });
            cover.addEventListener('error', function () {
                // 与两侧广告同一套兜底:自己取一份换 blob:,按文件名匹配的屏蔽规则就不再命中
                if (cover.getAttribute('data-spp-fetched') === '1') return;
                cover.setAttribute('data-spp-fetched', '1');
                refetch(cover, 2, function () { fit(); }, function () {
                    flip.className += ' spp-brand__flip--nocover';
                    cover.style.display = 'none';
                    fit();
                });
            });
            faceCover.appendChild(cover);
            flip.appendChild(faceFx);
            flip.appendChild(faceCover);

            // ★ 2.53.85:两块透明感应区必须排在 .spp-brand__flip **前面**(兄弟选择器 ~ 只向后找)
            var stage = document.createElement('div');
            stage.className = 'spp-brand__stage';
            var zoneTop = document.createElement('div');
            zoneTop.className = 'spp-brand__zone spp-brand__zone--top';
            var zoneBot = document.createElement('div');
            zoneBot.className = 'spp-brand__zone spp-brand__zone--bot';
            stage.appendChild(zoneTop);
            stage.appendChild(zoneBot);
            stage.appendChild(flip);
            mid.appendChild(stage);
            strip.appendChild(artL);
            strip.appendChild(mid);
            strip.appendChild(artR);

            // ★ 2.53.80:图可能还在路上、也可能已经被屏蔽器打死 —— 各自收尾:
            //   显示得出来就重算尺寸;显示不出来先自己取一份;连取都失败才收掉那一侧占位。
            var settle = function (art, idx) {
                var im = art.firstChild;
                if (!im) { art.className += ' spp-brand__art--none'; return; }
                // ★ 2.53.81:give 先自检 —— 真到这一刻仍然一个像素都没有,才收掉占位。
                //   救图成功后再被异步 error 回调打到时(blob 刚赋值、还没解码,naturalWidth
                //   读成 0),这里只是重算尺寸,不会把救回来的图按死。
                var give = function () {
                    if (im.naturalWidth > 0) { fit(); return; }
                    art.className += ' spp-brand__art--none';
                    fit();
                };
                var ok = function () { fit(); };
                var tried = false;      // 同一侧只发一次 refetch
                var step = function () {
                    if (im.naturalWidth > 0) { fit(); return; }
                    if (tried) return;
                    tried = true;
                    refetch(im, idx, ok, give);
                };
                if (im.complete) step();
                else {
                    im.addEventListener('load', function () { fit(); });
                    im.addEventListener('error', step);
                }
            };
            settle(artL, 0);
            settle(artR, 1);

            // ③ ★ 2.53.89:不再把条挂在站点那个广告容器下,而是另起一个空白 div 顶在同一位置,
            //   把旧容器整个摘掉 —— 按位置写死的老规则命中的将是已经不在页面上的旧槽位;
            //   新宿主没有 id、没有 class、也没有任何广告痕迹,只有脚本自己的 data 标记。
            var slot = document.createElement('div');
            slot.setAttribute('data-spp-brand', '1');
            if (host.parentNode) {
                host.parentNode.insertBefore(slot, host);   // 插在原位置之前,布局一点不动
                host.parentNode.removeChild(host);
            } else {
                slot = host;                                // 退化情况:拿不到父节点就照旧用原容器
            }
            host = slot;

            // ★ 条的全部内容 + 全部样式都进宿主的影子根(页面 CSS 与拦截器的 cosmetic 规则
            //   都穿不过这条边界;站点继承来的 text-align:center / overflow-wrap 也一并挡在外面)
            var shadow = null;
            try { shadow = host.attachShadow({ mode: 'open' }); } catch (e) { shadow = null; }
            if (!shadow) return;                            // 环境不给影子根就别硬来,保持站点原样
            var styleEl = document.createElement('style');
            styleEl.textContent = BAR_CSS;
            shadow.appendChild(styleEl);
            while (host.firstChild) host.removeChild(host.firstChild);
            shadow.appendChild(strip);

            // ★ 2.53.89 兜底:万一整条被规则物理移除(AdGuard 的 {remove:true} 之类),重新挂回
            //   #header 末尾;最多 3 次,用完主动断开 —— 不与用户的拦截器无限对拉。
            try {
                var reAdds = 0;
                var mo = new MutationObserver(function () {
                    if (reAdds >= 3) { mo.disconnect(); return; }
                    if (host.isConnected) return;
                    var hd = document.getElementById('header');
                    if (!hd) return;
                    reAdds++;
                    hd.appendChild(host);
                    fit();
                });
                mo.observe(document.body, { childList: true, subtree: true });
            } catch (e) { }

            // ★★ 2.53.73(用户:「将广告区域向两侧边框对齐,中间空出的区域用于为字体腾出更多空间。」):
            //   原来这里钉着 1260 的上限 ⇒ 条比内容列窄。真机(2.53.72,viewport 1385)实测:
            //   #main 1384.7 从 x=0 起,条却被钉在 1260、居中成 62.3..1322.3 —— 左右各空
            //   62.3 / 61.0 的黑带(正是用户蓝框圈出来的那两条)。图与条沿本身是齐的(各 0.7 = 边框)。
            //   ⇒ 直接取 #main 的实测宽度:条的左右沿与内容列两侧边框对齐,多出来的宽度全进中缝。
            var main = document.getElementById('main');
            var cap = 0;                                     // 0 = 不设上限(退回 100%)
            if (main) {
                var mw = Math.round(main.getBoundingClientRect().width);
                if (mw > 0) cap = mw;                        // 与内容列同宽、同中轴
            }
            // ★ 2.53.89:宿主样式一律内联 + !important —— 拦截器的外观过滤也是 !important,
            //   而层叠里内联 !important 压过任何样式表规则(2.53.80 救图用的同一招)。
            //   锁的全是"藏起来 / 挪走 / 压成 0"的常见手法:显示、可见性、定位、裁剪、
            //   变换、滤镜、指针、盒模型与尺寸。height 不锁(条长高了它得跟着长)。
            host.style.setProperty('box-sizing', 'border-box', 'important');
            host.style.setProperty('display', 'block', 'important');
            host.style.setProperty('position', 'static', 'important');
            host.style.setProperty('visibility', 'visible', 'important');
            host.style.setProperty('opacity', '1', 'important');
            host.style.setProperty('clip-path', 'none', 'important');
            host.style.setProperty('transform', 'none', 'important');
            host.style.setProperty('filter', 'none', 'important');
            host.style.setProperty('pointer-events', 'auto', 'important');
            host.style.setProperty('left', 'auto', 'important');
            host.style.setProperty('top', 'auto', 'important');
            host.style.setProperty('right', 'auto', 'important');
            host.style.setProperty('bottom', 'auto', 'important');
            host.style.setProperty('width', '100%', 'important');
            host.style.setProperty('max-height', 'none', 'important');
            // ★ 2.53.90(2.53.89 活体验收抓到的真漏):模拟拦截器打了一条
            //   [data-spp-brand]{height:0!important;max-height:0!important} —— 影子根里的内容
            //   纹丝不动,可宿主被压成 1400×0,宿主自己又 overflow:hidden ⇒ 整条被裁没。
            //   根因是上一版**故意没锁 height**(怕挡住条长高),而 auto 本来就是"让内容撑高",
            //   锁成 auto 不影响长高,正好把 height:0 挡在外面;顺带把另外几个现代隐藏手法
            //   (zoom / scale / rotate / translate / clip / content-visibility)一起锁死。
            host.style.setProperty('height', 'auto', 'important');
            host.style.setProperty('zoom', '1', 'important');
            host.style.setProperty('scale', '1', 'important');
            host.style.setProperty('rotate', 'none', 'important');
            host.style.setProperty('translate', 'none', 'important');
            host.style.setProperty('clip', 'auto', 'important');
            host.style.setProperty('content-visibility', 'visible', 'important');
            host.style.setProperty('overflow', 'hidden', 'important');
            host.style.setProperty('max-width', (cap > 0 ? cap + 'px' : '100%'), 'important');
            host.style.setProperty('margin', '2px auto -5px', 'important');   // 站点原值,缝隙不变

            // ④ 尺寸自适应(2.53.76):竖向按「上带 + 间距 + 下带 = 0.90 × 条高、上带 = 0.90 × 下带」
            //   反解字号与图标缩放;横向不够时整组等比缩(k)。件件都是实测常数,不猜。
            function fit() {
                // ★ 2.53.83:条高不再写死 96,也不再从自己身上量(量自己 = 自己跟自己较劲)——
                //   它由「三张图等高铺满整条 + 两处空隙」反解:H = (W − 2·SP) ÷ (R_ART×2 + R_CARD)。
                //   三图高度全等就是「等比放大」的全部含义;宽度各自 = 比例 × H,不裁不拉。
                if (!strip.offsetWidth) return;                     // 还没挂进文档,量到的全是 0
                // ★ 2.53.83:窗口变窄时 #main 会变窄,宿主的上限得跟着重取(原来只在 build 里取一次)
                if (main) {
                    var mw2 = Math.round(main.getBoundingClientRect().width);
                    if (mw2 > 0 && mw2 !== cap) { cap = mw2; host.style.setProperty('max-width', cap + 'px', 'important'); }
                }
                var w = strip.clientWidth;
                if (w < 320) return;                                 // 窄到没意义就别算了
                strip.style.setProperty('--sp', SP + 'px');
                var h = Math.floor((w - SP * 2) / (R_ART * 2 + R_CARD));
                if (h > H_MAX) h = H_MAX;
                if (h < H_MIN) h = H_MIN;
                var aw = Math.floor(h * R_ART);
                var cw = Math.min(Math.floor(h * R_CARD), w - aw * 2 - SP * 2);
                strip.style.setProperty('--bh', h + 'px');
                strip.style.setProperty('--aw', aw + 'px');
                strip.style.setProperty('--cw', cw + 'px');
                var availW = cw - PAD;                               // 效果面能用的宽度
                if (availW < 60) return;
                var gap = Math.max(1, Math.round(h * 0.031));       // 96 ⇒ 3,条高变则同时缩
                var hWord = (h * STACK - gap) / (1 + RATIO);        // 96 ⇒ 43.9
                var fs = hWord / LINE_K;                            // 43.9 / 1.157 ⇒ 37.9
                if (fs > FS_MAX) fs = FS_MAX;
                if (fs < FS_MIN) fs = FS_MIN;
                var wWord = TXT_UNIT * fs + TXT_PAD;                // 37.9 ⇒ 301.3
                var k = wWord > availW ? (availW / wWord) : 1;
                strip.style.setProperty('--gap', gap + 'px');
                strip.style.setProperty('--ik', ((hWord * RATIO) / ICON_H).toFixed(4));
                btn.style.setProperty('--fs-size', fs.toFixed(2) + 'px');
                unit.style.transform = 'scale(' + k.toFixed(4) + ')';
                // ★ 2.53.85:上下感应区的分界线 = 整组上边距 + 图标行带 + 半个行距(整组被 k 缩过,
                //   所以按 k 换算到卡里的实际位置)。h=103、k=1 ⇒ 49.1(卡高 47.7% 处)。
                var rowBand = (hWord * RATIO + gap * 0.5) * k;
                var zd = (h - h * STACK * k) / 2 + rowBand;
                strip.style.setProperty('--zd', (Math.round(zd * 10) / 10) + 'px');
            }
            fit();
            if (document.fonts && document.fonts.ready) document.fonts.ready.then(fit);
            var rt = null;
            window.addEventListener('resize', function () {
                if (rt) clearTimeout(rt);
                rt = setTimeout(fit, 160);
            });
        }

        // 页眉那块在 DOM 早期就存在;再兜一次延迟重跑,避开站点自己的脚本补 DOM
        onDomReady(function () { build(); setTimeout(build, 800); });
    }

    function initImageIndex() {
        if (!WANTS.readOnly) return;
        // ★ 2.53.36 那枚「总开关」在 2.53.68 撤了:用户拍板「含图楼层悬浮窗」的本意是
        //   「勾选它,它就会自动显示;不勾选,它就会自动隐藏到侧边栏」—— 不勾选只是**收起成侧边一条**,
        //   不再是整块不建(那枚键现在只驱动 .spp-ii--edge,见下方 toggle 那句)。

        /* 一枚「真图」:排除表情、头像、签名图、小图标。宽高只有加载完才知道 ⇒ 0×0 一律先算真图。 */
        function imgIsReal(im) {
            const src = im.getAttribute('src') || '';
            if (im.closest('.user-pic') || im.closest('.spp-sign-inline')) return false;
            if (/images\/(post|face)\//i.test(src)) return false;      /* 站点表情 / 默认头像 */
            if (/icon|btn|reply|arrow|post\.png|smiley/i.test(src)) return false;
            if (im.naturalWidth && Math.min(im.naturalWidth, im.naturalHeight) < 60) return false;
            return true;
        }
        function realImgs(row) {
            const box = row.querySelector('.tpc_content');
            if (!box) return [];
            return [].slice.call(box.querySelectorAll('img')).filter(imgIsReal);
        }

        GM_addStyle(`
/* ===== SPP-IMGIDX-BEGIN ===== */
#spp-imgidx {
    /* ★ 2.53.48:用户拍板 **左侧中间**(原话「把含图楼层的悬浮面板,从右下角移到左侧中间」)。
       右 → 左 → 右下 → 左侧中间,四次定位的终稿。
       ★ 2.53.30 写的 bottom:calc(16px - 18px) 是让开右侧导航面板 #spp-nav-panel 用的
         (右侧那 62px / -18px 两处错位量都只为它);左侧没有别的常驻浮层 ⇒ 一并去掉。
       ★ 已知代价:read.php 左栏是楼层头像(约 x 13–163),面板宽 174 ⇒ 会压住头像列右缘。
         要避开只能再窄一点或改 left —— 位置已由用户拍板,这里就按拍板落。
       ★ 2.53.51:用户「希望再向上移动一点,因为居中对阅读体验稍差」⇒ 竖向中心 50% → calc(50% - 64px)。
         用固定偏移而不是百分比:百分比随窗口高度飘(900 高的屏上 44% 只挪 54px,1300 高的屏上挪 78px),
         同一块面板在不同窗口里的相对位置就不一致了。64 这个数想再调就改这一行。 */
    position:fixed; left:14px; top:calc(50% - 64px); transform:translateY(-50%); z-index:2999;
    /* ★ 2.53.48:过渡从 transform 改到 width —— 左侧贴边改成「收窄裁剪」(见下方 .spp-ii--edge),
       右侧那套靠 transform 滑出去的写法在左侧会露出面板右端(「张」/ 折叠标记),语义反了。 */
    transition:width .22s ease;
    /* ★ 2.53.51:174 是**展开态**(悬停 / 聚焦,或面板没开「靠边自动隐藏」时一直用它);
       收起成窄条时改用 --spp-ii-ws —— 由 fitWidth() 现算:贴纸块(楼层头像那一栏)的左边
       随窗口浮动(实测 1500 宽在 x≈43、2560 宽在 x≈591),写死任何一个值都会在别的窗口上出错。
       ⇒ 于是「稍微长一点、又不碰贴纸块」由窄条那一态负责,行要的宽度由展开态负责。 */
    box-sizing:border-box; width:174px;
    background:#0d1420; background-image:none;
    border:1px solid rgba(255,255,255,.13); border-radius:14px;
    box-shadow:0 20px 46px rgba(2,6,23,.66);
    padding:9px 9px 7px;
    font:12px/1.5 "Microsoft YaHei","PingFang SC",Arial,Helvetica,sans-serif;
}
/* ★★ 2.53.67(用户附截图:「它的『含图』和『链接』这两个框似乎也没有居中」):
   旧账(面板展开档量的):左到「含图」= 11.67px(内衬 9 + 表头外边距 2),右边空 59.66px ——
   折叠按钮挂着那个「左自动」外边距,把表头剩下的空隙全吃在右边;竖直也不平:内衬 9 / 7,
   折叠态下表头下方没有外边距 ⇒ 上 9 下 7,矮 1px。
   ⇒ ① 表头 position:relative + justify-content:center:两枚页签按内衬居中;
      ② 折叠按钮改成绝对定位钉在右端(right:0 / top:50% / margin-top:-11px 正好是它半个高),
         不再参与居中 —— 窄条那一档它本来就是 display:none,不会和页签抢位置;
      ③ 折叠态补一条下内衬 9px,9/7 变 9/9(见下方 is-fold 那一段)。
   代价:靠边收起那一档悬停时面板从窄条涨回 174,两枚页签会跟着往右挪一点(抽屉拉开的感觉)。 */
/* ★ 2.53.68 更正上一条:2.53.66 在这条规则里插过一句多余的 position:relative(给伪元素当参照),
   而绝对定位那句写在它前面 ⇒ **后者被吃掉**,折叠按钮其实还在流里占着表头的宽度,
   justify-content:center 没有余量可分,两枚页签照旧贴左(真机量到 gapLeft=2 / gapRight=48)。
   本轮把那句 position:relative 删掉;2.53.66 给伪元素留的参照由 absolute 元素的自身盒子担任,
   两根短杠的居中算式一字未动(它们量的是 padding box,与 position 取值无关)。 */
#spp-imgidx .spp-ii__hd {
    position:relative; display:flex; align-items:center; justify-content:center; gap:6px; margin:0 2px 7px;
    font-size:12px; font-weight:700; color:#8ef0b8; cursor:pointer; user-select:none;
}
#spp-imgidx .spp-ii__hd i { width:6px; height:6px; border-radius:50%; background:#3ef58c; box-shadow:0 0 8px rgba(62,245,140,.6); }
/* ★★ 2.53.71(用户:「红色框选的区域比较多余,能否去掉?」+「增大含图链接与展开按钮之间的间隔」):
   展开态内衬框 154.67、两枚页签 104.67 居中 ⇒ 左 25 / 右 25;而折叠按钮 absolute 钉在右端
   (154.33..176.33)⇒ 页签右沿 153.33 与按钮左沿 154.33 只差 **1px**,看着就是「+ 贴在链接上」,
   左边那 25px 又纯是空的。
   ⇒ 表头留一条 24px 跑道(= 按钮 22 + 2 余量),页签在**剩下的** 126.67 里居中:
      左 13 / 右 13 —— 右边那 13 就是「链接」到「+」的间隔,左区也从 25 收到 13,两边仍对称。
   ⇒ 跑道只在按钮真在场的那两档生效:.spp-ii--edge 未悬停时按钮 display:none、条只有 74 宽,
      扣掉 24 会把那一枚页签挤出去。 */
#spp-imgidx:not(.spp-ii--edge) .spp-ii__hd { padding-right:24px; }
#spp-imgidx.spp-ii--edge:hover .spp-ii__hd,
#spp-imgidx.spp-ii--edge:focus-within .spp-ii__hd { padding-right:24px; }
/* ★★ 2.53.65(用户:「绿色框选部分它的折叠按钮不是很明显,点击区域也稍微有点小」):
   原来它只是一枚 12px 灰字(— / +),既不像个能点的东西,命中区也只有字那么大。
   而它是**唯一**的折叠入口(标题栏本身点了只切页签),所以给它一个真的按钮盒子:
   22×22(与页签同高)· 圆角 8 · 浅描边 + 深底 · 字色提亮到 #cfd9e3、字形 14px/700,
   hover 变绿边绿字。命中区就是盒子本身(那点命中余量是本轮唯一的意图)。 */
/* ★★ 2.53.66(用户附截图:「那个加号没有位于灰底框 UI 的中间,而且可以稍微大一点
   (减号可以稍微小一点。),不要和减号的大小差异太多(可以有差异)」):
   上一条只做出了盒子,符号还是字体里的 + 和 —,于是两件事都不是我说了算:
     · 居中:光心跟字体度量走。雅黑在 14px / line-height:1 下,+ 的光心比盒子中心低约 0.7px,
       再叠上半像素抗锯齿 ⇒ 看着就是没对准;换字体又是另一个偏差。
     · 大小:— 是 em 宽、+ 只有半个,天生不等宽,连盒子宽度都跟着字面变(量到过 26 与 22)。
   ⇒ 符号不再用字形,改由 ::before / ::after 两根 2px 圆头短杠画:
      ::before 横杠两态共用(展开态 10×2 就是减号;折叠态加宽到 12×2,当加号的腰),
      ::after 竖杠 2×12 只在折叠态出现 ⇒ 合起来是加号。两态同粗、长短差 1.2 倍,
      正是「可以有差异,别差太多」。
      居中从「靠字体」变成「算术」:left/top:50% 量的是**内衬框**(border-box 22 减去两边
      1px 描边 = 20,50% 即内衬左上 10px,换算回 border-box 恰好 11 = 盒子中心),再用
      偶数尺寸的负 margin 把两根杠各推回半个自身 ⇒ 光心与盒子中心重合在同一个整数像素上,
      与字体、字号无关。
   字面没删:JS 照旧往这一枚里写 + / —(见 applyTab),只是 font-size:0 让它不出墨,
   读屏仍念得到;视觉符号交给上面两根杠。 */
#spp-imgidx .spp-ii__hd .spp-ii__fold {
    position:absolute; right:0; top:50%; margin-top:-11px;
    display:inline-flex; align-items:center; justify-content:center;
    box-sizing:border-box; width:22px; height:22px; padding:0;
    border-radius:8px; cursor:pointer;
    font-size:0;
    color:#cfd9e3; background:rgba(255,255,255,.07); border:1px solid rgba(255,255,255,.16);
    transition:color .16s ease, background .16s ease, border-color .16s ease;
}
/* 两根 2px 圆头短杠:::before 横(两态共用),::after 竖(折叠态才有) */
#spp-imgidx .spp-ii__hd .spp-ii__fold::before,
#spp-imgidx .spp-ii__hd .spp-ii__fold::after {
    content:''; position:absolute; left:50%; top:50%;
    display:block; border-radius:1px; background:currentColor;
}
#spp-imgidx .spp-ii__hd .spp-ii__fold::before { width:10px; height:2px; margin:-1px 0 0 -5px; }
#spp-imgidx .spp-ii__hd .spp-ii__fold::after { width:2px; height:12px; margin:-6px 0 0 -1px; display:none; }
#spp-imgidx.is-fold .spp-ii__hd .spp-ii__fold::before { width:12px; height:2px; margin:-1px 0 0 -6px; }
#spp-imgidx.is-fold .spp-ii__hd .spp-ii__fold::after { display:block; }
#spp-imgidx .spp-ii__hd .spp-ii__fold:hover {
    color:#0ce39a; background:rgba(32,201,151,.16); border-color:rgba(32,201,151,.55);
}
#spp-imgidx .spp-ii__list { max-height:min(62vh, 460px); overflow-y:auto; overflow-x:hidden; }
#spp-imgidx .spp-ii__list::-webkit-scrollbar { width:8px; }
#spp-imgidx .spp-ii__list::-webkit-scrollbar-thumb { background:rgba(255,255,255,.14); border-radius:8px; }
#spp-imgidx .spp-ii__row {
    display:flex; align-items:center; gap:6px; padding:4px 7px; border-radius:8px; cursor:pointer;
    transition:background .14s ease;
}
#spp-imgidx .spp-ii__row:hover { background:rgba(255,255,255,.07); }
#spp-imgidx .spp-ii__row.is-on { background:rgba(32,201,151,.16); box-shadow:inset 2px 0 0 0 #3ef58c; }
#spp-imgidx .spp-ii__tag { font:700 12px/1 Arial,Helvetica,sans-serif; color:#cfd9e3; min-width:34px; }
#spp-imgidx .spp-ii__row.is-on .spp-ii__tag { color:#eafff8; }
#spp-imgidx .spp-ii__num {
    /* ★ 2.53.51:留一条收缩余量(展开态恒为 174,正常情况下用不到;缩得动总比撑破好) */
    box-sizing:border-box; flex:0 1 46px; min-width:30px; text-align:center; outline:none;
    font:700 11.5px/1 Arial,Helvetica,sans-serif; color:#eafff8; -webkit-text-fill-color:#eafff8;
    background:rgba(255,255,255,.08); border:1px solid rgba(255,255,255,.16); border-radius:7px;
    padding:4px 5px; transition:border-color .14s ease, background .14s ease;
}
#spp-imgidx .spp-ii__num:focus { border-color:rgba(32,201,151,.6); background:rgba(32,201,151,.14); }
/* ★ 2.53.30:总数(楼层号与输入框之间)—— 静态显示、主色绿、等宽数字;右对齐交给它 */
#spp-imgidx .spp-ii__total {
    margin-left:auto; min-width:24px; text-align:right;
    font:700 12.5px/1 Arial,Helvetica,sans-serif; color:#3ef58c; font-variant-numeric:tabular-nums;
}
#spp-imgidx .spp-ii__row.is-on .spp-ii__total { color:#8bf2cf; }
#spp-imgidx .spp-ii__num::placeholder { color:#5f7285; font-weight:400; font-size:10.5px; }
#spp-imgidx .spp-ii__u { font-size:10.5px; color:#7f8f9e; }
/* ★ 2.53.32:靠边自动部分隐藏(用户需求)。缩进去只留左边 68px —— 恰好露出绿点 + 「含图楼层」标题;
   鼠标移到那一条上、或键盘 focus 进去,就滑回完整面板。开关是面板里的「含图楼层靠边自动隐藏」。 */
/* ★ 2.53.48:面板挪到**左侧中间**后,贴边这一条也重做了 ——
   ★ 为什么不能照搬右侧那套 transform:右侧版是 translateX(calc(100% - 68px)),面板往右滑,
     留在屏上的是面板**自己那 0–68px**(最左边的绿点 + 标题)。翻面写成 −100% 之后机构依然成立
     (mock 实测:L=-92 / R=82 / 屏上剩 82px / 竖向中心 420.7 不变),但屏上露出的是面板的**右端**
     106–174px —— 每行的「张」「输入框」和标题栏右端的折叠标记;绿点与标题被推出屏幕。语义反了。
   ⇒ 左侧贴边改成**收窄 + 裁剪**:width 174 → 96,同时收起列表与折叠标记。
     96 是算出来的:内衬 9+9 + 外边距 2+2 + 绿点 6 + 间隙 6 + 标题 5 汉字 ×12px = 60 ⇒ 94,留 2px 余量。
   ⇒ 展开方向也顺带变好看:宽度 96→174 过渡,行是「拉开抽屉」那样滑出来,而不是整块跳一下。
   ★ 2.53.51 更正:上面「width 174 → 96」这一条**作废** —— 96px 正是用户截图里「含图」「链接」
     被挤成上下两行的原因(他要求「长度可以稍微扩展一些」)。现在:收起态宽度 = --spp-ii-ws
     (fitWidth() 现算:贴纸块左边 − left:14 − 10px,下限 96 / 上限 174),展开态回到 174。 */
/* ★ 2.53.51:收起态 = 窄条(只藏列表 + 用 --spp-ii-ws 那条宽度),悬停 / 聚焦回全宽。
   窄条的下限 96 是「两枚单字 + 非当前那枚收掉圆点」站得住的宽度。 */
#spp-imgidx.spp-ii--edge { width:var(--spp-ii-ws, 174px); overflow:hidden; }
/* ★★ 2.53.70(用户:「我要求它有一部分隐藏到侧边栏里,但你这个没有实现这一点」):
   2.53.51 起「收起」只是把面板**收窄**成 --spp-ii-ws(两枚页签全都在),等于什么都没藏。
   本轮真藏:收起态只留**当前那一枚页签**(绿点 + 含图 / 链接),非当前那枚连同列表、
   折叠按钮一起留在屏幕外;条宽也随之从「两枚全字所需」改成「一枚页签贴边所需」
   (--spp-ii-ws1,由 fitWidth() 现量真页签算出来,量不到退 74)。
   鼠标移上去照旧整块滑回来(下面那两条 :hover / :focus-within 原样接管)。 */
#spp-imgidx.spp-ii--edge:not(:hover):not(:focus-within) { width:var(--spp-ii-ws1, 74px); }
#spp-imgidx.spp-ii--edge:not(:hover):not(:focus-within) .spp-ii__tab:not(.is-on) { display:none; }
/* ★ 2.53.69:收起(未悬停)那一档里,两枚页签还带着表头那 7px 下外边距 —— 上 9 下 14,
   看着偏上 2.5px(用户截图正是这一档)。收起时把它收掉、下内衬补到 9 ⇒ 上下都是 9。
   悬停 / 键盘聚焦时面板整个展开、列表要出来,这两条让位,恢复 7px 与列表的间距。 */
#spp-imgidx.spp-ii--edge:not(:hover):not(:focus-within) { padding-bottom:9px; }
#spp-imgidx.spp-ii--edge:not(:hover):not(:focus-within) .spp-ii__hd { margin-bottom:0; }
#spp-imgidx.spp-ii--edge .spp-ii__list,
#spp-imgidx.spp-ii--edge .spp-ii__fold { display:none; }
#spp-imgidx.spp-ii--edge:hover,
#spp-imgidx.spp-ii--edge:focus-within { width:174px; overflow:visible; }
#spp-imgidx.spp-ii--edge:hover .spp-ii__list,
#spp-imgidx.spp-ii--edge:focus-within .spp-ii__list { display:block; }
#spp-imgidx.spp-ii--edge:hover .spp-ii__fold,
#spp-imgidx.spp-ii--edge:focus-within .spp-ii__fold { display:inline-flex; }
/* ★ 2.53.48:标题栏 tabindex="0"(见上方 .spp-ii--edge 的说明:收起成一条时键盘得有个落脚点),
   这里给键盘焦点一条可见描边;用 :focus-visible ⇒ 鼠标点击不会冒出描边。 */
#spp-imgidx .spp-ii__hd:focus { outline:none; }
#spp-imgidx .spp-ii__hd:focus-visible { outline:2px solid rgba(62,245,140,.55); outline-offset:2px; border-radius:6px; }
/* ★ 2.53.48:有动效就照顾 prefers-reduced-motion —— 只去动画,收起/展开本身保留(同 #spp-nav-panel)。 */
@media (prefers-reduced-motion: reduce) { #spp-imgidx { transition:none; } }
/* ★ 2.53.50:含图 / 链接 两个页签共用这一块面板(用户拍板「合并成一个面板、分两个页签」)。
   页签即标题:绿点跟着当前页签走 —— 贴边收成 96px 一条时露出的正好是当前那一栏,
   一眼就知道现在索引的是「含图」还是「链接」。 */
#spp-imgidx .spp-ii__tab {
    display:inline-flex; align-items:center; gap:5px; height:22px; padding:0 9px;
    border-radius:8px; cursor:pointer; user-select:none;
    font-size:12px; font-weight:700; line-height:1; white-space:nowrap;
    color:#93a6bb; background:rgba(255,255,255,.045); border:1px solid rgba(255,255,255,.08);
    transition:color .16s ease, background .16s ease, border-color .16s ease;
}
#spp-imgidx .spp-ii__tab > i { background:#3f4b5c; box-shadow:none; }
#spp-imgidx .spp-ii__tab:hover { color:#cfe0f0; background:rgba(255,255,255,.08); }
#spp-imgidx .spp-ii__tab.is-on { color:#8ef0b8; background:rgba(32,201,151,.16); border-color:rgba(32,201,151,.55); }
#spp-imgidx .spp-ii__tab.is-on > i { background:#3ef58c; box-shadow:0 0 8px rgba(62,245,140,.6); }
/* 这一类一层都没有时把页签置灰(不给切),免得点过去看一片空白 */
#spp-imgidx .spp-ii__tab.is-off { opacity:.38; cursor:default; }
#spp-imgidx .spp-ii__tab.is-off:hover { color:#93a6bb; background:rgba(255,255,255,.045); }
#spp-imgidx .spp-ii__tab > b { font:inherit; font-weight:700; }
/* ★ 2.53.51(★ 2.53.65 已整档删除,见下条):页签挂长/短两个字面 —— 默认长字面(含图 / 链接),
   且只有**非当前**那一枚换成短字面(图 / 链)——用户原话「灰色项应显示为图或链单字,
   以便为高亮展开显示为两个字」。条件与上面控制宽度的 :not(:hover):not(:focus-within) 完全一致,
   所以不会出现「宽度已展开、字面还是单字」这种错拍;也不用 JS 去监听 hover。 */
/* ★★ 2.53.65(用户:「去掉只显示单字的选项,横图和链接这两个字显示全,空间是足够的。」):
   2.53.51 挂的那对短字面(图 / 链)连同它的全部 CSS **整档删除** —— 两个页签永远写全「含图 / 链接」,
   不再有「非当前那枚换单字」的形态。下限随之从 108 抬到 128(窄内衬下两枚全字所需);
   代价记在宽度上,不记在字面上。 */
/* ★ 2.53.52:**当前那枚永远两个字** —— 2.53.51 还留着一档「两枚都单字」,用户装上当场指出:
   「如果高亮的是『图』这个字,它应该显示为图片」(原话如此;按上下文=高亮那枚该写成两个字「含图」)。
   高亮项降级等于把「现在索引的是哪一栏」这个信息丢掉 ⇒ 那一档整条删掉;
   窄条下限从 96 提到 108 —— 这一档的代价记在**宽度**上,不记在字面上。
   (这条也正是 2.53.51 里用户那句「以便为高亮展开显示为两个字」的字面意思,当时多留一档属于设计过头。) */
/* 窄档把页签内衬 9→7、圆点间隙 5→4:两枚都写全字所需的宽度从 138 落到 128 */
#spp-imgidx.spp-ii--narrow .spp-ii__tab { padding:0 7px; gap:4px; }
/* ★ 2.53.65:原来这里还有一档 spp-ii--narrow2(非当前那枚连圆点也收掉)—— 随单字档一起删除:
   两枚页签永远全字,圆点(哪一栏、亮不亮)也永远在。 */
/* 链接行:楼层号 + 条数,把「条」顶到最右(只作用于 .is-link,别动含图行那套间距)*/
#spp-imgidx .spp-ii__row.is-link .spp-ii__u { margin-left:auto; }
/* 本层有提取码时那枚小标:琥珀 = 带标签词认到的;青 = 裸码(四字符那套规则认到的)*/
#spp-imgidx .spp-ii__code {
    display:inline-flex; align-items:center; height:16px; padding:0 5px; border-radius:5px;
    font:700 10px/1 Arial,Helvetica,sans-serif; color:#ffd489;
    background:rgba(224,165,58,.16); border:1px solid rgba(224,165,58,.42);
}
#spp-imgidx .spp-ii__code.is-bare { color:#8ef0b8; background:rgba(32,201,151,.14); border-color:rgba(32,201,151,.42); }
/* 链接页签:行自己滚,底部两枚按钮不跟着滚出去 */
#spp-imgidx .spp-ii__list[data-pane="link"] { overflow:visible; max-height:none; }
#spp-imgidx .spp-ii__rows { max-height:min(52vh, 360px); overflow-y:auto; overflow-x:hidden; }
#spp-imgidx .spp-ii__rows::-webkit-scrollbar { width:8px; }
#spp-imgidx .spp-ii__rows::-webkit-scrollbar-thumb { background:rgba(255,255,255,.14); border-radius:8px; }
#spp-imgidx .spp-ii__ft { display:flex; gap:6px; margin:8px 2px 1px; }
#spp-imgidx .spp-ii__btn {
    flex:1 1 0; display:inline-flex; align-items:center; justify-content:center; gap:4px;
    box-sizing:border-box; height:26px; padding:0 6px; cursor:pointer;
    font:700 11.5px/1 "Microsoft YaHei","PingFang SC",Arial,Helvetica,sans-serif;
    color:#cfe0f0; background:rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.14);
    border-radius:8px; transition:color .16s ease, background .16s ease, border-color .16s ease;
}
#spp-imgidx .spp-ii__btn:hover { color:#eafff8; background:rgba(32,201,151,.18); border-color:rgba(32,201,151,.55); }
#spp-imgidx .spp-ii__btn[disabled] { opacity:.45; cursor:default; }
#spp-imgidx .spp-ii__btn[disabled]:hover { color:#cfe0f0; background:rgba(255,255,255,.06); border-color:rgba(255,255,255,.14); }
#spp-imgidx .spp-ii__note {
    margin:7px 2px 0; padding:5px 7px; border-radius:7px;
    font-size:11px; line-height:1.35; color:#a9bccd;
    background:rgba(255,255,255,.05); border:1px solid rgba(255,255,255,.09);
}
#spp-imgidx .spp-ii__note:empty { display:none; }
#spp-imgidx .spp-ii__note.is-ok { color:#8ef0b8; border-color:rgba(32,201,151,.45); background:rgba(32,201,151,.12); }
#spp-imgidx .spp-ii__note.is-bad { color:#ffb4a8; border-color:rgba(255,120,90,.45); background:rgba(255,120,90,.10); }
#spp-imgidx.is-fold .spp-ii__list { display:none; }
#spp-imgidx.is-fold { padding-bottom:9px; }
#spp-imgidx.is-fold .spp-ii__hd { margin-bottom:0; }
html #main > form > .t5.t2 { scroll-margin-top:76px; }
/* ★★ 2.53.93:全贴索引 / 跨页定位新增的几条。命中高亮写在**不限 #spp-imgidx** 的全局段里:
   落点是正文里的图和链接,套不上面板那条前缀(上面那条 scroll-margin-top 就是同一段)。 */
/* ★ 2.53.93 落盘后自查补的第四条(截图复核):链接行多了一个输入框之后,B159F / B161F 这种
   5 个字符的层号被 flex 收缩压得折了两行 ⇒ 层号列不参与收缩、也不许折行,余量由输入框承担
   (它本来就有 46→30 的收缩余量,见 2.53.51 那条)。 */
#spp-imgidx .spp-ii__tag { flex:0 0 auto; white-space:nowrap; }
html .spp-ii-hit { outline:3px solid #3ef58c !important; outline-offset:3px !important; border-radius:6px; }
html a.spp-ii-hit { background:rgba(62,245,140,.18) !important; box-shadow:0 0 0 3px rgba(62,245,140,.18) !important; }
/* 全贴模式下的「第 N 页」分组条:吸在列表顶部,滚到哪都知道这一串楼层属于哪一页 */
#spp-imgidx .spp-ii__pg {
    position:sticky; top:0; z-index:2; margin:6px 0 3px; padding:2px 6px;
    font:700 10.5px/1.5 Arial,Helvetica,sans-serif; color:#8ef0b8;
    background:rgba(13,20,32,.94); border-bottom:1px solid rgba(32,201,151,.28);
}
#spp-imgidx .spp-ii__pg:first-child { margin-top:0; }
/* 非当前页的行:左缘一条淡绿,提醒「点它要换页」 */
#spp-imgidx .spp-ii__row.is-ref { box-shadow:inset 2px 0 0 0 rgba(62,245,140,.45); }
#spp-imgidx .spp-ii__row.is-ref .spp-ii__tag { color:#a9c9b8; }
#spp-imgidx .spp-ii__btn.is-on { color:#0ce39a; background:rgba(32,201,151,.18); border-color:rgba(32,201,151,.55); }
/* 含图栏现在也走 .spp-ii__rows 那条滚动容器(为了给底部按钮留位置)⇒ 外层不再自己滚,
   否则两层都带滚动条。链接栏 2.53.50 起就是这么写的。 */
#spp-imgidx .spp-ii__list[data-pane="img"] { overflow:visible; max-height:none; }
/* ★★ 2.53.95:两栏各自的翻页条(用户:「如果数量过多,面板会显示大量结果。最好增加翻页功能,
   例如每页显示 15 个条目」+「如果能在设置面板里让用户自定义这个「15」这个数字」)。
   每页几行读 spp_idx_per(面板「数值设置」那枚新行)。只有一页可看时整条藏起来。 */
#spp-imgidx .spp-ii__pager {
    display:flex; align-items:center; justify-content:center; gap:5px;
    margin:6px 2px 0; padding-top:6px; border-top:1px solid rgba(255,255,255,.09);
}
/* ★ 必须显式写这条:上一句的 display:flex 会盖过浏览器对 [hidden] 的默认 display:none
   (同一个坑见 2.53.70 那条折叠态说明)—— 少了它,一页装得下时那条空壳照样占一行。 */
#spp-imgidx .spp-ii__pager[hidden] { display:none; }
#spp-imgidx .spp-ii__pgb {
    flex:0 0 auto; box-sizing:border-box; width:22px; height:22px; padding:0; cursor:pointer;
    font:700 13px/1 Arial,Helvetica,sans-serif; color:#cfe0f0;
    background:rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.14); border-radius:7px;
    transition:color .16s ease, background .16s ease, border-color .16s ease;
}
#spp-imgidx .spp-ii__pgb:hover { color:#eafff8; background:rgba(32,201,151,.18); border-color:rgba(32,201,151,.55); }
#spp-imgidx .spp-ii__pgb[disabled] { opacity:.38; cursor:default; }
#spp-imgidx .spp-ii__pgb[disabled]:hover { color:#cfe0f0; background:rgba(255,255,255,.06); border-color:rgba(255,255,255,.14); }
#spp-imgidx .spp-ii__pgt {
    flex:0 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
    font:700 10.5px/1 Arial,Helvetica,sans-serif; color:#93a6bb; font-variant-numeric:tabular-nums;
}
/* ===== SPP-IMGIDX-END ===== */
`);

        /* ==================== 2.53.50:链接楼层 + 提取码 + 两枚复制 ==================== */
        // 用户需求两轮:
        //   ①「增加一个类似的功能,称为『链接楼层』,效果与含图楼层一致,用于定位链接」+
        //     「在该面板上增加两个功能:一是复制本页的全部链接,二是复制本贴的全部链接」;
        //   ②「复制链接还不够,如果楼层中有提取码 + 数字、英文的结构也要复制」→
        //     「密码、验证码、单独存在或者和中文混在一起的四个字符的英文+数字,都按照提取码来看」。
        // ★ 规则与误收面当初先做了一张审核页(严格 / 宽松两种输出并排现算),用户看过之后
        //   点名「修改脚本」,故按**严格档**落:纯 4 字母要有标签词才认,
        //   挡掉 mark / good / note / test 这类词(宽松档会把它们也当码)。
        //   ★ 2.53.64:那张页(tmp/r1/srv/preview-linkpanel.html)按用户要求随本轮清掉,
        //     规则也收口成「散码一档取消」;视觉存档仍在 tmp/r1/shot/preview-linkpanel.png。
        //   ★ 两档与格式都不用改脚本:控制台里
        //     localStorage.setItem('spp_lk_mode','lax')  /  localStorage.setItem('spp_lk_fmt','b')
        //     刷新即生效(读不到就用默认值,不污染设置面板——少一行手写 HTML 就少一次 2.53.32 那种漏改)。
        var CODE_LABEL = /(提取码|提取碼|访问码|訪問碼|解压码|解壓碼|解压密码|解壓密碼|验证码|驗證碼|密码|密碼|pwd|password)\s*[::=]?\s*([A-Za-z0-9]{4,10})/gi;
        var CODE_PARAM = /[?&](?:pwd|password)=([A-Za-z0-9]{4,10})/i;
        // 裸码:4 位字母数字、前后都不是字母数字(用捕获代替 lookbehind,兼容性稳)
        var CODE_BARE = /(^|[^A-Za-z0-9])([A-Za-z0-9]{4})(?![A-Za-z0-9])/g;
        // 只排除三样:楼层号形(B12F)/ 年份(19xx 20xx)/ 已被标签词或 URL 认走(后者在 codesIn 里比)
        var NOT_A_CODE = [/^[A-Za-z]\d{1,3}[Ff]$/, /^(19|20)\d{2}$/];
        // ★★ 2.53.50 真机补:协议名 / 分辨率 / 编码名也长得像「4 位字母数字」,但在资源帖里
        //   几乎不是提取码。实测抓到的第一个误收就是 tid-2934530 的 B41F 层 —— 正文里写着 ed2k
        //   (eMule 协议名),复制出来的第一行成了「- B41F  ed2k(本层无链接)」。
        //   ★ 纯字母的格式名(webp / jpeg / html / mkv…)不必列:严格档本来就要求「纯 4 字母要有标签词」。
        var CODE_STOP = /^(ed2k|m3u8|h264|x265|h265|1080|2160|1440|720p)$/i;

        var lkMode = 'strict', lkFmt = 'a', lkTab = 'img';
        try {
            if (localStorage.getItem('spp_lk_mode') === 'lax') lkMode = 'lax';
            var lkF = localStorage.getItem('spp_lk_fmt');
            if (lkF === 'b' || lkF === 'c') lkFmt = lkF;
            if (localStorage.getItem('spp_lk_tab') === 'link') lkTab = 'link';
        } catch (e) {}

        /* 全角 → 半角:站点里「提取码:」常写成全角冒号,码本身也可能被输入法写成全角 */
        function halfWidth(s) {
            return String(s || '').replace(/[\uFF01-\uFF5E]/g, function (c) {
                return String.fromCharCode(c.charCodeAt(0) - 0xFEE0);
            }).replace(/\u3000/g, ' ');
        }

        /* 把一层正文拆成「文本段 / 链接」交替序列 —— 判「这个码属于哪条链接」全靠它 */
        function splitParts(box) {
            var parts = [];
            (function walk(node) {
                Array.prototype.forEach.call(node.childNodes, function (n) {
                    if (n.nodeType === 3) {
                        var t = halfWidth(n.nodeValue);
                        if (t.trim()) parts.push({ kind: 'text', text: t });
                        return;
                    }
                    if (n.nodeType !== 1) return;
                    /* ★ 2.53.52:跳过站点自己生成的**售价条** —— <h6 class="quote jumbotron">
                       「此帖售价 0 SP币,已有 8550 人购买」里的 8550 是购买人数,被当成裸码收进复制结果里了
                       (真机实例:tid-2717095 的楼主层 GF)。正文在同级的 blockquote 里,不受影响。 */
                    if (n.tagName === 'H6' && n.classList && n.classList.contains('quote')) return;
                    if (n.tagName === 'A') {
                        var href = n.getAttribute('href') || '';
                        if (href && !/^(javascript:|#)/i.test(href)) {
                            parts.push({ kind: 'link', url: n.href || href, el: n });
                            return;
                        }
                    }
                    walk(n);
                });
            })(box);
            return parts;
        }

        function codesIn(text) {
            var out = [], m, i;
            CODE_LABEL.lastIndex = 0;
            while ((m = CODE_LABEL.exec(text))) out.push({ label: m[1], code: m[2] });
            CODE_BARE.lastIndex = 0;
            while ((m = CODE_BARE.exec(text))) {
                var tok = m[2], bad = false;
                for (i = 0; i < NOT_A_CODE.length; i++) if (NOT_A_CODE[i].test(tok)) bad = true;
                if (bad) continue;
                if (CODE_STOP.test(tok)) continue;                                   /* 协议名/分辨率:见上 */
                if (out.some(function (c) { return c.code === tok; })) continue;      /* 标签词已认走 */
                if (lkMode === 'strict' && /[A-Za-z]/.test(tok) && !/\d/.test(tok)) continue;
                out.push({ label: '', code: tok, bare: true });
            }
            return out;
        }

        /* 一层的链接清单:每条链接带上属于它的码 —— 先看**它后面**那段文本,没有再回头看**前面**那段
           (资源帖两种写法都常见:「链接 + 提取码」同行、以及「提取码:xxx」写在链接之前) */
        function scanLinks(box) {
            /* ★ 2.53.64:原来这里还有一套「散码」机器(texts / pool / used)——
               它算的是「本层所有码」减去「被链接认领的码」,差集就是 loose。
               散码一档取消后没人再读这三样,本轮一并清掉(规则收口,不是重写规则)。 */
            var parts = splitParts(box), links = [], i, j;
            for (i = 0; i < parts.length; i++) {
                if (parts[i].kind !== 'link') continue;
                /* ★ 2.53.93:元素与指纹一起带上 —— 前者给页内「跳第 k 条」,后者给跨页认人(见 scanDocFull) */
                var item = { url: parts[i].url, el: parts[i].el || null, fp: fpOf(parts[i].url), code: null };
                var pm = CODE_PARAM.exec(item.url);
                if (pm) item.code = { label: 'pwd', code: pm[1], inUrl: true };
                var after = '', before = '';
                for (j = i + 1; j < parts.length && parts[j].kind !== 'link'; j++) after += parts[j].text;
                for (j = i - 1; j >= 0 && parts[j].kind !== 'link'; j--) before = parts[j].text + before;
                if (!item.code) {
                    var ca = codesIn(after);
                    if (ca.length) item.code = ca[0];
                }
                if (!item.code) {
                    var cb = codesIn(before);
                    if (cb.length) item.code = cb[0];
                }
                links.push(item);
            }
            /* ★ 2.53.51(用户原话):「对于密码、验证码等而言,只有与链接同时存在时,其才生效。
               如果没有链接,即便检测到有验证码,也等同于没有。」
               ⇒ 一层里一条链接都没有时,整层的码一律作废;连带整层不再出现在链接栏
                 (只有码、没有链接的楼层本来也没法「定位链接」,留在栏里只是噪音)。
               ★ 这一刀顺手关掉了 2.53.50 留下的那个悬案:正文里写「6666」的楼层**没有链接**,
                 现在直接不认。
               ★★ 2.53.64(用户:「纯 4 位裸码规则不留,必须要和链接搭配生效」):
                 **「没人认领的码」这一档整个取消** —— 输出里只剩「被某条链接认领的码」。
                 两层含义:① 单枚裸码不再自己成行(「本层无链接」那种行彻底没有了);
                 ② 同层里没配上链接的散码也不再收 —— 售价条那个「已有 8550 人购买」
                    从此是**由规则挡掉**,不再只靠跳过 h6.quote 那一处特判。
                 ★ 裸码识别(CODE_BARE)本身保留:它只是必须贴在链接旁边才算数。 */
            if (!links.length) return { links: [], loose: [] };
            /* ★ 2.53.64:loose 保留为**恒空**字段 —— 下游还有三处读它(面板那枚小标 /
               mergeByTag 的合并 / scanDoc 的进门判断)。要彻底删字段就得连那三处一起改,
               跟本轮的规则收口是两件事 ⇒ 留空壳,并在此注明它恒空。 */
            return { links: links, loose: [] };
        }

        function lkCodeText(c) { return c.label ? (c.label + ' ' + c.code) : c.code; }

        /* 帖子分页地址:站点两种写法都收(…-page-N.html;没有页码段就自己插一段)。
           ★ 2.53.54 修正:page-N 必须换**最后一处**,不能换第一处 —— 站点自己的翻页地址里,
             前面还有一个 fpage-0(fpage 里含 "page"!),末尾才是真正的目标页号。
             旧写法在 …-fpage-0-toread--page-2.html 上把 fpage-0 换成 fpage-3、
             末尾 …--page-2.html 一字不动 ⇒ 抓回来的 9 页全是第 2 页。活体实证:
             …-fpage-3-toread--page-2.html 返回 Pages: 2/10;…-fpage-0-toread--page-3.html 返回 Pages: 3/10。 */
        function pageUrl(n) {
            var base = location.href.split('#')[0];
            /* 贪婪的 [\s\S]* 让匹配一直吃到最后一处 page-N */
            if (/page[-=]\d/i.test(base)) return base.replace(/([\s\S]*)page([-=])\d+/i, '$1page$2' + n);
            if (/\.html(\?|$)/.test(base)) return base.replace(/\.html(\?|$)/, '-page-' + n + '.html$1');
            return base;
        }
        function curPage() {
            var m = location.href.match(/page[-=](\d+)/gi);
            return m ? Number((m[m.length - 1].match(/\d+/) || [1])[0]) : 1;
        }
        /* ★ 2.53.53:总页数原来只有 `.pagesone` 一条路(读不到就当 1 页)—— 用户那边「复制本贴只拿到本页」,
           所以补一条兜底:分页条里的 page-N 链接取最大页号。
           ★ 兜底为什么只是兜底:分页条只显示一个**窗口**(实测 10 页的贴,条上只有 2–5),
             所以它给不出真实的末页 ⇒ 以「Pages: x/y」为准,两者取大。 */
        function totalPages() {
            var one = document.querySelector('.pagesone');
            var m = one ? (one.textContent || '').match(/(\d+)\s*\/\s*(\d+)/) : null;
            var byText = m ? (Number(m[2]) || 1) : 1;
            var byLinks = 1;
            var tid = (location.href.match(/tid[-=](\d+)/i) || [])[1];   /* ★ 只认本贴的翻页链接 */
            Array.prototype.forEach.call(document.querySelectorAll('a[href*="page-"], a[href*="page="]'), function (a) {
                var href = a.getAttribute('href') || '';
                if (!/read\.php/i.test(href)) return;                   /* 版块页 / 上一主题(job.php)都不算 */
                if (tid && href.indexOf(tid) < 0) return;
                var mm = href.match(/page[-=](\d+)/i);
                if (mm) byLinks = Math.max(byLinks, Number(mm[1]) || 1);
            });
            return Math.min(Math.max(byText, byLinks), 60);      /* 封顶 60 页:再长就不该在浮层里抓了 */
        }
        /* 分页信息读到没有 —— 只用来把病因写进提示(见 onCopy 的「本贴只有 1 页」那句) */
        function pagerSeen() {
            var one = document.querySelector('.pagesone');
            return one ? ((one.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 18) || '(空)') : null;
        }

        /* ★ 2.53.53:抓一页。失败**重试一次**(隔 800ms)再把原因抛出去 ——
           原先失败只累加一个计数,用户那边「只抓到本页」时看不出是没网、被拦还是 403。 */
        function fetchPage(n, tries) {
            return fetch(pageUrl(n), { credentials: 'include' }).then(function (res) {
                if (!res.ok) throw new Error('HTTP ' + res.status);
                return res.text();
            }).catch(function (e) {
                if (tries <= 1) throw e;
                return new Promise(function (r) { setTimeout(r, 800); })
                    .then(function () { return fetchPage(n, tries - 1); });
            });
        }

        /* 从一整篇文档里收链接楼层 —— 当前页与 fetch 回来的页共用这一个(判据只有一个)*/
        function scanDoc(doc) {
            var out = [];
            Array.prototype.forEach.call(doc.querySelectorAll('#main > form > .t5.t2'), function (row) {
                var tagEl = row.querySelector('.tiptop > .fl');
                var box = row.querySelector('.tpc_content');
                if (!box) return;
                var lk = scanLinks(box);
                if (!lk.links.length && !lk.loose.length) return;
                out.push({ row: null, tag: tagEl ? (tagEl.textContent || '').trim() : '楼', links: lk.links, loose: lk.loose });
            });
            return out;
        }

        /* 去重:键是「楼层号 + URL」(2.53.52 起)—— 同一层内不重复,**不同层各自保留**。 */
        function dedupeLinks(list) {
            var seen = {}, out = [];
            list.forEach(function (r) {
                (r.links || []).forEach(function (l) {
                    /* ★ 2.53.52:去重键是「楼层号 + URL」,**不是纯 URL**。
                       原来用纯 URL,于是「复制本贴」时同一个 URL 只留给**先入账的那一层**:
                       真机实测(tid-2717095)B15F 与 B73F 各有 acgdb.de / alist.erogame.space,
                       从第 1 页点复制 ⇒ B73F 一条不剩;从第 3 页点 ⇒ B15F 一条不剩。
                       面板本来就是「按层索引链接」,复制出来的也该是每一层的完整清单 ——
                       同一层里重复写同一个 URL 仍旧只留一条(这条去重仍然有用)。 */
                    var key = r.tag + '\u0001' + l.url;
                    if (seen[key]) return;
                    seen[key] = 1;
                    out.push({ tag: r.tag, url: l.url, code: l.code });
                });
                if (r.loose && r.loose.length && !(r.links && r.links.length)) {
                    out.push({ tag: r.tag, url: '', code: r.loose[0] });
                }
            });
            return out;
        }

        /* 三种输出格式:a 一层一行(默认)· b 一层一块 · c 纯 URL */
        function renderCopy(list) {
            if (lkFmt === 'c') return list.map(function (r) { return r.url; }).filter(Boolean).join('\n');
            if (lkFmt === 'b') {
                var o = [];
                list.forEach(function (r, i) {
                    if (i === 0 || list[i - 1].tag !== r.tag) {
                        o.push('- ' + r.tag + (r.code && !r.code.inUrl ? '  ' + lkCodeText(r.code) : ''));
                    }
                    if (r.url) o.push('  ' + r.url);
                });
                return o.join('\n');
            }
            /* ★ 2.53.54:同一层的多条链接收成一行,用「 、 」分隔;码紧跟**它自己那条**链接后面。
               用户原话:「对于一个楼层有多个链接的格式应该是 - B15F  链接1 、 链接2  密码 2 、链接3 、 …」
               ⇒ 一层一行:[tag] + 链接1 、 链接2 密码 X 、 链接3 …(本层有码无链接时那枚码也照样占一格) */
            var groups = [], cur = null;
            list.forEach(function (r) {
                if (!cur || cur.tag !== r.tag) { cur = { tag: r.tag, parts: [] }; groups.push(cur); }
                if (r.url) cur.parts.push(r.url + (r.code && !r.code.inUrl ? ' ' + lkCodeText(r.code) : ''));
                else if (r.code) cur.parts.push(lkCodeText(r.code));
            });
            return groups.map(function (g) {
                return '- ' + g.tag + '  ' + (g.parts.join(' 、 ') || '(本层无链接)');
            }).join('\n');
        }

        /* 剪贴板:**不申请 GM_setClipboard**(2.52.93 明确删过这个 @grant,别再加回来)。
           优先 async clipboard(HTTPS 页面 + 用户手势,条件都满足),失败退到 textarea + execCommand。 */
        function copyText(text, done) {
            var fb = function () {
                try {
                    var ta = document.createElement('textarea');
                    ta.value = text;
                    ta.style.cssText = 'position:fixed;left:-9999px;top:0';
                    document.body.appendChild(ta);
                    ta.select();
                    var ok = document.execCommand('copy');
                    ta.parentNode.removeChild(ta);
                    done(ok);
                } catch (e) { done(false); }
            };
            try {
                if (navigator.clipboard && navigator.clipboard.writeText) {
                    navigator.clipboard.writeText(text).then(function () { done(true); }, fb);
                    return;
                }
            } catch (e) {}
            fb();
        }

        let panel = null, rows = [], linkRows = [], lastKey = '', noteEl = null;
        let activeTab = lkTab, folded = false, tabOk = { img: false, link: false };

        /* ★ 2.53.51:**收起成窄条**时宽度的唯一出口。
           用户两条要求并列:「长度可以稍微扩展一些」+「不能触碰到贴纸板块,避免遮挡阅读」。
           两条都是窗口相关的(贴纸块左边随窗口浮动,见上面的实测),所以不写死值,而是量出
           「左栏贴纸块的左边」、扣掉面板的 left:14 与 10px 呼吸位,取可用的那一段。
           窄条宽度阶梯(2.53.51 / 2.53.52 时定;**下面 116 / 108 那两档 2.53.65 已删**,见本段末尾):
             ≥174  全宽 —— 等价于「不收起」,窗口够宽时保持 2.53.50 的样子
             ≥138  两页签全字(含图 / 链接)         ← 用户点名「能容纳就不用改单字」
             ≥116  当前页签两字、另一枚单字(含图 / 链)← 用户点名要的收缩观感
             ≥108  同上,但把非当前那枚的圆点也收掉(106px 站得住)
             <108  兜底 108 —— **当前那枚依旧是两个字**。
                   ★ 2.53.52:2.53.51 在这里还压过一档「两枚都单字」(下限 96),用户装上后
                     当场指出高亮那枚不该降级;这一档的代价改记在宽度上(96 → 108)。
                   兜底有两种来路:窗口窄到连左留白都没有(贴纸块就在屏幕左边缘),
                   或者页面上量不到贴纸块(不是帖子页 ⇒ 反而用满宽 174,见 fitWidth)。
           ★ 展开态(悬停 / 聚焦)恒为 174:那一刻用户正在用面板,行需要这个宽度。
           收到的起点是 96px 里「含图 / 链接」被挤成上下两行,这就是 2.53.51 改动的由头。
           ★★ 2.53.65 现行阶梯(用户:「去掉只显示单字的选项…空间是足够的」):
             ≥174  全宽(等价于不收起)
             ≥138  两页签全字 · 宽内衬(padding 9 / gap 5)
             <138  窄档(padding 7 / gap 4)—— **两枚依旧全字**,这是现在唯一的窄形态
             <128  兜底 128(两种来路:窗口窄到没有左留白 / 页面上量不到贴纸块)
           116 与 108 两档随「单字」一起删除;narrow2 这个类名也一并消失。 */
        var II_W_LEFT = 14, II_W_GAP = 10, II_W_MAX = 174;
        /* ★★ 2.53.65:下限从 108 抬到 **128** = 窄内衬(padding 7 / gap 4)下「含图 + 链接」
           两枚全字所需(宽内衬是 138)。用户原话:「去掉只显示单字的选项,含图和链接这两个字
           显示全,空间是足够的」⇒ 单字那一档整个删掉,代价记在宽度上(比原来宽 20px,
           仍远窄于展开态 174,也不碰贴纸块那条 2.53.52 的判据)。 */
        var II_W_MIN = 128;
        var II_W_HEAD = 138;    /* 两个页签都写全字所需(宽内衬);≥ 这个值就不必挂窄档 */

        /* 左栏贴纸块的左边。取 .r_two(头像那一格,连底色一起算)—— 比只取图片保守一点。
           ★ 2.53.52:取不到时返回 **null**(不是 0)—— 下面 fitWidth() 要把「量不到」和
             「量到了但可用空间 ≤ 0」分开处理,这两件事的取值方向正好相反。 */
        function contentLeft() {
            var el = document.querySelector('#main > form > .t5.t2 .r_two') ||
                     document.querySelector('#main > form > .t5.t2 .user-pic img') ||
                     document.querySelector('#main > form > .t5.t2 img');
            if (!el) return null;
            var b = el.getBoundingClientRect();
            return b.width ? b.x : null;
        }

        function fitWidth() {
            if (!panel) return;
            var left = contentLeft();
            /* ★ 2.53.52 补:原来把两种情形写成同一条(!(avail > 0) ⇒ 用满宽),于是在 1400 宽的窗口上
               —— 站点根本没有左留白(贴纸块 x=1)—— 面板反而撑到 174。用户的原话是「不能触碰到
               贴纸板块」⇒ **没地方时应当更窄(退到下限),不是更宽**;只有「量不到」(不是帖子页)
               才用满宽。真机读数:1400 宽 ⇒ 128px(2.53.65 抬下限后的值;贴纸块 x=1,本来就让不开,但也不多压)。 */
            var avail = (left === null) ? II_W_MAX : (left - II_W_LEFT - II_W_GAP);
            var w = Math.max(II_W_MIN, Math.min(II_W_MAX, Math.round(avail)));
            panel.style.setProperty('--spp-ii-ws', w + 'px');
            /* ★ 2.53.65:narrow2(连灰页签的圆点也收掉)随单字档一起删除 ——
               两枚页签永远全字,圆点也永远在。 */
            panel.classList.toggle('spp-ii--narrow', w < II_W_HEAD);
            /* ★★ 2.53.70:收起态从「收窄」改成「真藏」之后,条宽不再等于上面那个 w(那是两枚全字
               所需的宽度),而是「一枚页签贴边所需」:内衬 9+9 + 表头外边距 2+2 + 页签本身 + 描边。
               这里**现量**真页签(写在窄档切换之后,量到的就是窄内衬那一版),量不到(面板还没上屏)
               就退回 74 —— 那个数是按窄档页签 49.33 加内外衬算出来的。 */
            var tab0 = panel.querySelector('.spp-ii__tab');
            var tw = tab0 ? tab0.getBoundingClientRect().width : 0;
            panel.style.setProperty('--spp-ii-ws1', (tw > 10 ? Math.ceil(tw) + 24 : 74) + 'px');
        }


        function build() {
            const all = [].slice.call(document.querySelectorAll('#main > form > .t5.t2'));
            const here = curPage();
            totalCache = totalPages();        /* ★ 2.53.93:总页数本轮要反复用,一次算清 */
            const byPage = function (a, b) { return a.page - b.page; };
            const data = [], ldata = [];
            all.forEach(function (row) {
                const tagEl = row.querySelector('.tiptop > .fl');
                const tag = tagEl ? (tagEl.textContent || '').trim() : '楼';
                const pid = rowPid(row);
                /* ★ 2.53.93:本页的图也换成 {el, fp} 记录 —— 与全贴索引里那份(只有指纹、没有元素)
                   同形,下游渲染与跳转不用分两支;el 留在手上,页内「第 k 张」照样精确到张。 */
                const imgs = realImgs(row).map(function (im) {
                    return { el: im, fp: fpOf(im.getAttribute('src')) };
                });
                if (imgs.length) data.push({ row: row, imgs: imgs, tag: tag, pid: pid, page: here });
                /* ★ 2.53.50:同一遍顺手收链接。判据与「含图」共用同一层行 —— 都是 #main > form > .t5.t2 */
                const box = row.querySelector('.tpc_content');
                if (!box) return;
                const lk = scanLinks(box);
                if (lk.links.length || lk.loose.length) {
                    ldata.push({ row: row, tag: tag, pid: pid, page: here, links: lk.links, loose: lk.loose });
                }
            });
            /* ★ 2.53.93:全贴模式把索引里**其它页**的记录并进来。当前页永远以活的 DOM 为准
               (只有它有元素、能就地滚);其它页只有指纹 ⇒ 点行时换页定位(jumpTo)。
               两栏按 page 升序 —— 层号本来就全局递增,排完天然是「第 1 页 → 第 N 页」。 */
            if (idxOn()) {
                /* ★ 2.53.93 落盘后自查补的一条(真机抓到的缺口):索引里也得有**当前页**这一份 ——
                   原来只收「其它页」,于是在第 1 页点完全贴索引、跳到第 2 页之后,第 1 页的楼层
                   整片消失(只剩「第 1 页 · 未索引」),「全贴」就名不副实了。
                   当前页的图/链带着 DOM 元素,而元素进不了 sessionStorage ⇒ 这里摘掉元素、只留指纹,
                   存一份快照;与已存那份逐字比较,没变就不写盘(一次抓取要重绘 N 遍,别每遍都写)。 */
                var snap = {};
                data.forEach(function (d) {
                    if (d.page !== here) return;
                    var e = snap[d.tag] || (snap[d.tag] = { tag: d.tag, pid: d.pid, page: here, imgs: [], links: [] });
                    d.imgs.forEach(function (it) { e.imgs.push({ el: null, fp: it.fp }); });
                });
                ldata.forEach(function (d) {
                    if (d.page !== here) return;
                    var e = snap[d.tag] || (snap[d.tag] = { tag: d.tag, pid: d.pid, page: here, imgs: [], links: [] });
                    d.links.forEach(function (l) { e.links.push({ url: l.url, el: null, fp: l.fp, code: l.code }); });
                });
                var snapArr = Object.keys(snap).map(function (k2) { return snap[k2]; });
                if (JSON.stringify(idxS.pages[here] || null) !== JSON.stringify(snapArr)) {
                    idxS.pages[here] = snapArr;
                    idxWrite();
                }
                Object.keys(idxS.pages).forEach(function (k) {
                    const n = Number(k);
                    if (n === here) return;
                    (idxS.pages[k] || []).forEach(function (r) {
                        if (r.imgs && r.imgs.length) {
                            data.push({ row: null, imgs: r.imgs, tag: r.tag, pid: r.pid, page: n });
                        }
                        if (r.links && r.links.length) {
                            ldata.push({ row: null, tag: r.tag, pid: r.pid, page: n, links: r.links, loose: [] });
                        }
                    });
                });
                data.sort(byPage); ldata.sort(byPage);
            }
            /* ★ 2.53.95:每页行数读一次 —— 键、切片、翻页条三处用的是同一个值。 */
            const per = perPage();
            const key = data.map(function (d) { return d.page + '#' + d.tag + ':' + d.imgs.length; }).join('|') + '#' +
                        ldata.map(function (d) {
                            return d.page + '#' + d.tag + ':' + d.links.length + ':' + d.links.filter(function (x) { return x.code; }).length;
                        }).join('|') + '@' + idxRev() + '%' + pgS.img + '/' + pgS.link + '/' + per;
            /* ★ 2.53.95:上面的 % 段是页码与每页行数 —— 翻页/改设置都必须让键变,
               否则 build() 会以「没变」为由提前返回,点了 ‹ › 界面纹丝不动。 */
            if (key === lastKey && panel) return;      /* 没变就不重建,免得把用户正在输入的框刷掉 */
            lastKey = key;
            rows = data;
            linkRows = mergeByTag(ldata);      /* ★ 2.53.53:同一楼层号只留一行(见 mergeByTag) */

            if (!panel) {
                panel = document.createElement('div');
                panel.id = 'spp-imgidx';
                panel.innerHTML = '<div class="spp-ii__hd">' +
                    /* ★ 2.53.51:每枚页签挂长/短两个字面(含图 / 图),收起成窄条时由 CSS 挑一个显示 */
                    '<span class="spp-ii__tab" data-tab="img" role="button" tabindex="0" title="含图楼层"><i></i><b class="spp-ii__tx">含图</b></span>' +
                    '<span class="spp-ii__tab" data-tab="link" role="button" tabindex="0" title="链接楼层"><i></i><b class="spp-ii__tx">链接</b></span>' +
                    '<span class="spp-ii__fold" title="折叠 / 展开" aria-label="折叠 / 展开"></span></div>' +
                    '<div class="spp-ii__list" data-pane="img">' +
                        '<div class="spp-ii__rows" data-rows="img"></div>' +
                        '<div class="spp-ii__pager" data-pager="img" hidden>' +
                            '<button class="spp-ii__pgb" data-pg="prev" title="上一页">‹</button>' +
                            '<span class="spp-ii__pgt"></span>' +
                            '<button class="spp-ii__pgb" data-pg="next" title="下一页">›</button>' +
                        '</div>' +
                        '<div class="spp-ii__ft">' +
                            '<button class="spp-ii__btn" data-scope="1" title="逐页抓完整贴:列出全贴所有含图楼层,点行跨页跳到那一层">全贴索引</button>' +
                        '</div>' +
                    '</div>' +
                    '<div class="spp-ii__list" data-pane="link">' +
                        '<div class="spp-ii__rows" data-rows="link"></div>' +
                        '<div class="spp-ii__pager" data-pager="link" hidden>' +
                            '<button class="spp-ii__pgb" data-pg="prev" title="上一页">‹</button>' +
                            '<span class="spp-ii__pgt"></span>' +
                            '<button class="spp-ii__pgb" data-pg="next" title="下一页">›</button>' +
                        '</div>' +
                        '<div class="spp-ii__ft">' +
                            '<button class="spp-ii__btn" data-copy="page" title="复制本页所有链接(含提取码)">复制本页</button>' +
                            '<button class="spp-ii__btn" data-copy="thread" title="逐页抓完整贴再复制">复制本贴</button>' +
                        '</div>' +
                        /* ★ 2.53.93:全贴索引 —— 两栏各一枚,驱动同一份索引。单独占一行而不是挤进上面那排:                           面板 174px 里一排四枚按钮实测每枚只剩 48px,「复制本页」四个字就折行了。 */
                        '<div class="spp-ii__ft">' +
                            '<button class="spp-ii__btn" data-scope="1" title="逐页抓完整贴:列出全贴所有含链接楼层,点行跨页跳到那一层">全贴索引</button>' +
                        '</div>' +
                        '<div class="spp-ii__note"></div>' +
                    '</div>';
                document.body.appendChild(panel);
                noteEl = panel.querySelector('.spp-ii__note');
                /* ★ 2.53.50:标题栏换成两枚页签 + 折叠标记。折叠标记自己点(整栏点击会和「切页签」打架)。 */
                Array.prototype.forEach.call(panel.querySelectorAll('.spp-ii__tab'), function (t) {
                    t.addEventListener('click', function (e) {
                        e.stopPropagation();
                        var k = t.getAttribute('data-tab');
                        if (!tabOk[k]) return;
                        activeTab = k;
                        try { localStorage.setItem('spp_lk_tab', k); } catch (er) {}
                        applyTab();
                    });
                    t.addEventListener('keydown', function (e) {
                        if (e.key !== 'Enter' && e.key !== ' ') return;
                        e.preventDefault();
                        t.click();
                    });
                });
                panel.querySelector('.spp-ii__fold').addEventListener('click', function (e) {
                    e.stopPropagation();
                    folded = !folded;
                    applyTab();
                });
                /* ★ 2.53.93:按钮分两类 —— 两枚复制(data-copy)与两枚全贴索引(data-scope)。
                   原来这一句按 .spp-ii__btn 一网打尽,新按钮会被当成复制按钮 ⇒ 选择器收窄。 */
                Array.prototype.forEach.call(panel.querySelectorAll('.spp-ii__btn[data-copy]'), function (b) {
                    b.addEventListener('click', function (e) { e.stopPropagation(); onCopy(b); });
                });
                Array.prototype.forEach.call(panel.querySelectorAll('.spp-ii__btn[data-scope]'), function (b) {
                    b.addEventListener('click', function (e) { e.stopPropagation(); onScope(); });
                });
                /* ★ 2.53.95:两栏各自的翻页条。翻页条本身只建一次(在 panel.innerHTML 里),
                   这里挂一次点击:改页码状态 → 置回顶标记 → build()。页码进了重绘键,
                   所以 build() 不会因为 lastKey 相同而提前返回。 */
                Array.prototype.forEach.call(panel.querySelectorAll('.spp-ii__pager'), function (bar) {
                    bar.addEventListener('click', function (e) {
                        e.stopPropagation();
                        var b = e.target && e.target.closest ? e.target.closest('.spp-ii__pgb') : null;
                        if (!b || b.disabled) return;
                        var d = (b.getAttribute('data-pg') === 'next') ? 1 : -1;
                        var kind = bar.getAttribute('data-pager');
                        pgS[kind] = Math.max(1, (pgS[kind] || 1) + d);
                        pgTop = kind;      /* 翻页才回顶部(普通重绘仍旧恢复原位置) */
                        build();
                    });
                });
            }
            const list = panel.querySelector('.spp-ii__list[data-pane="img"]');
            const listImg = panel.querySelector('.spp-ii__rows[data-rows="img"]');
            const listLink = panel.querySelector('.spp-ii__rows[data-rows="link"]');
            if (!data.length && !ldata.length) { panel.style.display = 'none'; return; }
            panel.style.display = '';
            /* ★ 2.53.32 时这里是「靠边自动隐藏」那枚开关;★ 2.53.68 两枚并一枚之后,
               改由「含图楼层悬浮窗」**取反**驱动:勾选 = 常显(不加 .spp-ii--edge),
               不勾选 = 收起成侧边一条。
               设置面板保存后是 location.reload(),所以这里读一次就够,不必监听变化。 */
            panel.classList.toggle('spp-ii--edge', !SPP_CONFIG.bool('spp_imgidx_show', false));
            fitWidth();          /* ★ 2.53.51:先算出窄条宽度 —— 页签该用长字面还是短字面由它决定 */
            /* ★ 2.53.93:重绘前先记住「滚动位置 + 正在输入的那个框」—— 全贴模式一次要重绘 N 遍,
               不记的话列表每页都弹回顶部、敲到一半的「第 k 张」也没了。 */
            const keepS = [[listImg, 'img'], [listLink, 'link']].map(function (x) {
                const h = x[0];
                const f = document.activeElement;
                const on = f && h.contains(f) && f.className === 'spp-ii__num';
                const top = (pgTop === x[1]) ? 0 : h.scrollTop;   /* 翻页 ⇒ 回顶 */
                return { h: h, top: top, key: on ? (f.getAttribute('data-key') || '') : '', val: on ? f.value : '' };
            });
            headSeq = {};
            /* ★ 2.53.95:先切片再画 —— 页码越界在 pgSpan 里已经收敛过(它把 cur 夹回合法范围)。 */
            const imgSp = pgSpan(data.length, per, pgS.img);
            pgS.img = imgSp.cur;
            const imgSlice = data.slice(imgSp.from, imgSp.to);
            listImg.textContent = '';
            imgSlice.forEach(function (d) {
                headRow(d, listImg);
                const r = document.createElement('div');
                r.className = 'spp-ii__row' + (d.row ? '' : ' is-ref');
                const tag = document.createElement('span');
                tag.className = 'spp-ii__tag';
                tag.textContent = d.tag;
                /* ★ 2.53.30:楼层号与输入框之间补一枚**总数**元素(用户指出上一版遗漏)。
                   总数是静态显示,输入框只管「跳第几张」。 */
                const total = document.createElement('span');
                total.className = 'spp-ii__total';
                total.textContent = String(d.imgs.length);
                const num = document.createElement('input');
                num.className = 'spp-ii__num';
                num.type = 'text';
                num.inputMode = 'numeric';
                num.title = '该层共 ' + d.imgs.length + ' 张图;填第 k 张、回车(或点这一行)跳过去;留空 / 超出总数 = 第 1 张' +
                            (d.row ? '' : '(第 ' + d.page + ' 页:回车会换页跳过去)');
                num.setAttribute('data-key', d.page + '#' + d.tag);
                const u = document.createElement('span');
                u.className = 'spp-ii__u';
                u.textContent = '张';
                r.appendChild(tag); r.appendChild(total); r.appendChild(num); r.appendChild(u);

                r.addEventListener('click', function (e) {
                    if (e.target === num) return;
                    /* ★ 2.55.31:点行也读输入框(口径见 pickIdx)—— 框里没数字就是第 1 张 */
                    const k = pickIdx(num, d.imgs.length);
                    num.value = String(k);
                    go(d, k);
                });
                num.addEventListener('click', function (e) { e.stopPropagation(); });
                num.addEventListener('keydown', function (e) {
                    if (e.key !== 'Enter') return;
                    e.preventDefault();
                    /* ★ 2.55.31:溢出(> 总数)回第 1 张,不再夹到最末一张。 */
                    const n = pickIdx(num, d.imgs.length);
                    num.value = String(n);
                    go(d, n);
                    num.blur();
                });
                d.el = r;
                listImg.appendChild(r);
            });
            /* ★ 2.53.50:两栏各自的可用性 —— 空的置灰、当前栏空了自动换到另一栏 */
            tabOk.img = !!data.length;
            tabOk.link = !!ldata.length;
            if (!tabOk[activeTab]) activeTab = tabOk.img ? 'img' : 'link';

            /* ★ 2.53.95:链接栏同款(两栏各自分页,页码各记各的)。 */
            const lkSp = pgSpan(ldata.length, per, pgS.link);
            pgS.link = lkSp.cur;
            const lkSlice = ldata.slice(lkSp.from, lkSp.to);
            listLink.textContent = '';
            lkSlice.forEach(function (d) {
                const r = document.createElement('div');
                headRow(d, listLink);
                r.className = 'spp-ii__row is-link' + (d.row ? '' : ' is-ref');
                const tag = document.createElement('span');
                tag.className = 'spp-ii__tag';
                tag.textContent = d.tag;
                const total = document.createElement('span');
                total.className = 'spp-ii__total';
                total.textContent = String(d.links.length);
                /* 本层有码就挂一枚小标(琥珀=带标签词 / 青=裸码),一眼看出哪几层要一起抄 */
                /* ★ 2.53.64:散码一档取消后,小标只认「被链接认领过的码」(loose 恒空)*/
                const coded = d.links.filter(function (l) { return l.code; })[0] || null;
                if (coded) {
                    const cb = document.createElement('span');
                    cb.className = 'spp-ii__code' + (coded.bare ? ' is-bare' : '');
                    cb.textContent = coded.bare ? '裸码' : '码';
                    cb.title = coded.label ? (coded.label + ' ' + coded.code) : ('裸码 ' + coded.code);
                    r.appendChild(tag); r.appendChild(total); r.appendChild(cb);
                } else {
                    r.appendChild(tag); r.appendChild(total);
                }
                const u = document.createElement('span');
                u.className = 'spp-ii__u';
                u.textContent = '条';
                r.appendChild(u);
                r.title = (d.row ? '' : ('第 ' + d.page + ' 页 · ')) +
                          d.links.map(function (l) { return l.url; }).slice(0, 4).join('\n') +
                          '\n点行跳到该层;填第 k 条、回车跳到那一条链接';
                /* ★ 2.53.93:链接行也补一枚「第 k 条」输入框 —— 与含图行同一套交互。
                   用户这一轮要的正是**定位到链接位置**;原来只做了「滚到该层顶部」。 */
                const lnum = document.createElement('input');
                lnum.className = 'spp-ii__num';
                lnum.type = 'text';
                lnum.inputMode = 'numeric';
                lnum.setAttribute('data-key', d.page + '#' + d.tag);
                lnum.title = '该层共 ' + d.links.length + ' 条链接;填第 k 条、回车(或点这一行)跳过去;留空 / 超出总数 = 第 1 条' +
                             (d.row ? '' : '(第 ' + d.page + ' 页:回车会换页跳过去)');
                /* 输入框要靠右:原来「条」靠 margin-left:auto 顶到最右,这里把 auto 挪到输入框上
                   (内联胜过类规则),顺序回到「[条数] [第k条框] [条]」,与含图行同形。 */
                lnum.style.marginLeft = 'auto';
                u.style.marginLeft = '0';
                /* ★ 2.53.93 落盘后自查补的第四条(截图复核):用 insertBefore 而不是 appendChild ——
                   追加的话输入框会落在「条」**后面**(本行原来就挂着「条」),一行成了
                   「B58F 1 条 …[框]」,与含图行的「GF 3 [框] 张」不是一个形状。 */
                r.insertBefore(lnum, u);
                lnum.addEventListener('click', function (e) { e.stopPropagation(); });
                lnum.addEventListener('keydown', function (e) {
                    if (e.key !== 'Enter') return;
                    e.preventDefault();
                    /* ★ 2.55.31:溢出回第 1 条(与含图行同一口径)。 */
                    const n = pickIdx(lnum, d.links.length);
                    lnum.value = String(n);
                    goLink(d, n);
                    lnum.blur();
                });
                r.addEventListener('click', function (e) {
                    if (e.target === lnum) return;
                    /* ★ 2.55.31:与含图行同一口径(见 pickIdx) */
                    const k = pickIdx(lnum, d.links.length);
                    lnum.value = String(k);
                    goLink(d, k);
                });
                d.el = r;
                listLink.appendChild(r);
            });
            /* 本页一条链接都没有:说清楚,别让人以为坏了 */
            if (!ldata.length) {
                const em = document.createElement('div');
                em.className = 'spp-ii__note';
                em.textContent = idxOn() ? '本贴没有链接' : '本页没有链接';
                listLink.appendChild(em);
            }
            /* ★ 2.53.95:翻页条按**这一栏**的行数算(不是全贴行数);一页装得下就整条收起来。 */
            paintPager('img', data.length, per, pgS.img);
            paintPager('link', ldata.length, per, pgS.link);
            pgTop = '';      /* 一次性标记用掉了:下面恢复的是「普通重绘」的滚动位置 */
            /* ★ 2.53.93:把保存的现场放回去(滚动位置 + 那个正在输入的框) */
            keepS.forEach(function (k) {
                k.h.scrollTop = k.top;
                if (!k.key) return;
                const f2 = k.h.querySelector('.spp-ii__num[data-key="' + k.key + '"]');
                if (f2) { f2.value = k.val; try { f2.focus(); } catch (e2) {} }
            });
            idxPaint();
            applyTab();
        }

        /* ★ 2.53.53:同一楼层号只保留一行。
           用户报「B15F 生成了两套链接,但只复制了一份」—— 面板按 DOM 行**逐行**建(同一层若在 DOM 里
           出现两次就会出现两行同名),而复制那边按 (层, URL) 去重 ⇒ 正好是「面板两套、复制一份」。
           合并规则:链接按 URL 去重、码按 code 去重,row 保留第一次那个(跳转仍然可用)。 */
        function mergeByTag(list) {
            var byTag = {}, order = [];
            list.forEach(function (d) {
                /* ★ 2.53.93:键从「层号」改成「页码 + 层号」—— 层号本来就全局唯一,加页码更稳,                   免得将来哪一页的引用楼复用同一个层号时被误并。 */
                var kk = d.page + '#' + d.tag;
                var hit = byTag[kk];
                if (!hit) { byTag[kk] = d; order.push(d); return; }
                d.links.forEach(function (l) {
                    if (!hit.links.some(function (x) { return x.url === l.url; })) hit.links.push(l);
                });
                d.loose.forEach(function (c) {
                    if (!hit.loose.some(function (x) { return x.code === c.code; })) hit.loose.push(c);
                });
                if (!hit.row && d.row) hit.row = d.row;
            });
            return order;
        }

        /* ★ 2.53.95:翻页条的外显。每页行数由面板设置 spp_idx_per 决定(默认 15)。
           只有一页可看(行数 ≤ 每页行数)时整条藏起来 —— 面板就 174px 宽,两栏各挂一条
           常驻空条等于白占一行。按钮到边界就地置灰,并把「每页几行」写进 title。 */
        function paintPager(kind, total, per, cur) {
            if (!panel) return;
            var bar = panel.querySelector('.spp-ii__pager[data-pager="' + kind + '"]');
            if (!bar) return;
            var tp = Math.max(1, Math.ceil(total / per));
            if (tp <= 1) { bar.hidden = true; return; }
            bar.hidden = false;
            var tx = bar.querySelector('.spp-ii__pgt');
            if (tx) tx.textContent = cur + '/' + tp + ' · 共' + total + '层';
            var pv = bar.querySelector('.spp-ii__pgb[data-pg="prev"]');
            var nx = bar.querySelector('.spp-ii__pgb[data-pg="next"]');
            if (pv) { pv.disabled = cur <= 1; pv.title = (cur <= 1 ? '已经是第一页' : '上一页') + '(每页 ' + per + ' 行)'; }
            if (nx) { nx.disabled = cur >= tp; nx.title = (cur >= tp ? '已经是最后一页' : '下一页') + '(每页 ' + per + ' 行)'; }
        }

        /* 链接行的高亮与含图行各自独立(两栏都在,但只有一个可见)*/
        function linkMark(d) {
            linkRows.forEach(function (x) { if (x.el) x.el.classList.toggle('is-on', x === d); });
        }

        /* 页签 / 折叠的统一出口:外显全靠它一处写,免得两处状态打架 */
        function applyTab() {
            if (!panel) return;
            panel.classList.toggle('is-fold', folded);
            const foldEl = panel.querySelector('.spp-ii__fold');
            if (foldEl) foldEl.textContent = folded ? '+' : '—';
            Array.prototype.forEach.call(panel.querySelectorAll('.spp-ii__tab'), function (t) {
                const k = t.getAttribute('data-tab');
                t.classList.toggle('is-on', k === activeTab);
                t.classList.toggle('is-off', !tabOk[k]);
                /* ★ 2.53.51:页签字面(长/短)走 CSS,这里不再掺和。
                   ★ 2.53.65:短字面整档删除 ⇒ 这里与 CSS 都不再有「换字」这回事,
                     页签永远写全「含图 / 链接」(见上方那两条 2.53.65 注释)。 */
            });
            Array.prototype.forEach.call(panel.querySelectorAll('.spp-ii__list'), function (p) {
                /* ★ 写内联而不是加类:#spp-imgidx.is-fold .spp-ii__list 那条折叠规则与
                   [data-pane="link"] 的 display:flex 同权,谁后写谁赢 —— 折叠时要能压住它,
                   所以折叠态也在这里一并落成内联 none。 */
                p.style.display = (folded || p.getAttribute('data-pane') !== activeTab) ? 'none' : '';
            });
        }

        function say(text, kind) {
            if (!noteEl) return;
            noteEl.textContent = text;
            noteEl.className = 'spp-ii__note' + (kind ? ' is-' + kind : '');
        }

        /* 两枚复制按钮。本页=读当前 DOM;本贴=按 Pages x/y 逐页 fetch(顺序抓,别打站点)*/
        function onCopy(btn) {
            const isPage = btn.getAttribute('data-copy') === 'page';
            say('');
            if (isPage) {
                const list = dedupeLinks(linkRows);
                if (!list.length) { say('本页没有链接', 'bad'); return; }
                copyText(renderCopy(list), function (ok) {
                    say(ok ? ('已复制 ' + list.filter(function (r) { return r.url; }).length + ' 条链接 · ' +
                              list.filter(function (r) { return r.code && !r.code.inUrl; }).length + ' 个提取码')
                           : '复制失败:浏览器拒绝了剪贴板权限', ok ? 'ok' : 'bad');
                });
                return;
            }
            const total = totalPages(), here = curPage();
            if (total <= 1) {
                /* ★ 2.53.53:这句话原来只有「只有 1 页」,用户分不清是**真的只有一页**还是**没读到分页信息**。
                   现在把读数一并写出来(.pagesone 的内容),下次一眼就能定性。
                   ★★ 2.55.31(用户:「对于图片、链接定位的复制本贴、本页链接;在只有一页的帖子
                      里面,复制本帖等价于复制本页链接」):单页帖不再只回一句话就退出 ——
                      直接按「本页」那一份复制。两枚按钮共用同一段 dedupeLinks + renderCopy,
                      所以结果与「复制本页」逐字相同(这不是「差不多」,是同一段代码同一份输入)。
                      ★ 读数里**必须**把「1 页」的依据写出来:没读到 .pagesone 时也是 1,
                        那种情况下静默只复制本页是最坏的结果,所以照旧标注是「没读到」。 */
                const seen = pagerSeen();
                const list0 = dedupeLinks(linkRows);
                if (!list0.length) { say('本页没有链接', 'bad'); return; }
                copyText(renderCopy(list0), function (ok) {
                    const n0 = list0.filter(function (r) { return r.url; }).length;
                    const c0 = list0.filter(function (r) { return r.code && !r.code.inUrl; }).length;
                    say(ok ? ('已复制 ' + n0 + ' 条链接 · ' + c0 + ' 个提取码 · ' +
                              (seen ? ('本贴只有 1 页(分页显示「' + seen + '」)') :
                                      '没读到分页信息(本页没有 .pagesone)') +
                              ' ⇒ 复制本贴 = 复制本页')
                           : '复制失败:浏览器拒绝了剪贴板权限', ok ? 'ok' : 'bad');
                });
                return;
            }
            /* ★ 2.53.93:全贴索引已经覆盖了其它所有页 ⇒ 复制不必再抓一遍(索引就是抓回来的那份)。
               条件:全贴模式开着、且 1..total 里除本页外每一页都有索引(失败页不算覆盖)。 */
            const missAll = (function () {
                for (let n = 1; n <= total; n++) if (n !== here && !idxS.pages[n]) return true;
                return false;
            })();
            if (idxOn() && !missAll) {
                const fin0 = dedupeLinks(linkRows);
                copyText(renderCopy(fin0), function (ok) {
                    const n0 = fin0.filter(function (r) { return r.url; }).length;
                    const c0 = fin0.filter(function (r) { return r.code && !r.code.inUrl; }).length;
                    say(ok ? ('已复制 ' + n0 + ' 条链接 · ' + c0 + ' 个提取码 · 全贴 ' + total +
                              ' 页(用全贴索引,没再抓一次)')
                           : '复制失败:浏览器拒绝了剪贴板权限', ok ? 'ok' : 'bad');
                });
                return;
            }
            let acc = dedupeLinks(linkRows).map(function (r) {
                return { tag: r.tag, links: r.url ? [{ url: r.url, code: r.code }] : [], loose: r.url ? [] : [r.code] };
            });
            const todo = [];
            for (let n = 1; n <= total; n++) if (n !== here) todo.push(n);
            let i = 0, fail = 0, failWhy = '';
            btn.disabled = true;
            say('正在抓取 0/' + todo.length + ' 页…');
            (function step() {
                if (i >= todo.length) {
                    btn.disabled = false;
                    const fin = dedupeLinks(acc);
                    copyText(renderCopy(fin), function (ok) {
                        const n = fin.filter(function (r) { return r.url; }).length;
                        const c = fin.filter(function (r) { return r.code && !r.code.inUrl; }).length;
                        say((ok ? ('已复制 ' + n + ' 条链接 · ' + c + ' 个提取码') : '复制失败:浏览器拒绝了剪贴板权限') +
                            (fail ? ('(' + fail + ' 页失败:' + failWhy + ')') : '') + ' · 全贴 ' + total + ' 页',
                            ok ? (fail ? 'bad' : 'ok') : 'bad');
                    });
                    return;
                }
                const n = todo[i++];
                fetchPage(n, 2)
                    .then(function (html) {
                        /* ★ 2.53.93:顺手把这一页也收进全贴索引 —— 两处扫的是同一份文档,
                           于是「复制本贴」跑过一次之后,全贴索引再点就是秒出(少抓一轮)。 */
                        const d0 = new DOMParser().parseFromString(html, 'text/html');
                        acc = acc.concat(scanDoc(d0));
                        /* ★ 2.53.94:这里原来挂在 spp_idx_all 上,那枚开关已撤(功能本体固定存在)。 */
                        idxS.pages[n] = scanDocFull(d0, n); idxWrite();
                    })
                    .catch(function (e) {
                        fail++;
                        /* 只留第一条:它最能说明问题(403 / 超时 / HTTP 5xx) */
                        if (!failWhy) failWhy = '第 ' + n + ' 页 ' + ((e && e.message) || '抓取失败');
                    })
                    .then(function () {
                        say('正在抓取 ' + i + '/' + todo.length + ' 页…');
                        step();
                    });
            })();
        }

        /* ★★ 2.55.31(用户:「对于图片、链接定位优化一下逻辑,如果输入框没有数字点击定位,
           定位到第一张图片,如果输入框有数字点击定位,定位到输入框中的数字(如果数字大于
           图片数或者链接数则定位回第一张图、链接)」):
           输入框的读数口径**只有这一处** —— 回车与点行两条路都调它,免得两边慢慢走散。
           规则:空 / 非数字 → 第 1 个;小于 1 → 第 1 个;大于总数 → **也回第 1 个**
           (不是夹到最末一个,这条是用户点名的);其余原样。
           ★ 与旧口径的差别只有两处:① 点行原来一律跳整层顶部(k=0),现在读框(空的跳第 1 张);
             ② 溢出原来 Math.min 夹到总数(= 最后一张),现在回第 1 张。 */
        function pickIdx(input, total) {
            const raw = String((input && input.value) || '').replace(/[^0-9]/g, '');
            const n = raw ? parseInt(raw, 10) : 0;
            if (!n || n < 1 || n > total) return 1;
            return n;
        }

        /* k=0:跳整层顶部;k>=1:跳到该层第 k 张图。
           ★ 2.53.93:d.imgs 的元素从「img 本体」换成 {el, fp} 记录(与全贴索引同形);
             非当前页的行没有 el、也没有 row ⇒ 走 jumpTo() 换页定位。 */
        function go(d, k) {
            if (!d.row) { jumpTo(d, 'img', k); return; }
            const it = d.imgs[k - 1];
            if (it && it.el) { hit(it.el); it.el.scrollIntoView({ block: 'center', behavior: 'smooth' }); }
            else d.row.scrollIntoView({ block: 'start', behavior: 'smooth' });
            mark(d);
        }
        /* ★ 2.53.93:链接行的两档跳转 —— k=0 跳该层顶部,k>=1 跳该层第 k 条链接。 */
        function goLink(d, k) {
            if (!d.row) { jumpTo(d, 'link', k); return; }
            const l = d.links[k - 1];
            if (l && l.el) { hit(l.el); l.el.scrollIntoView({ block: 'center', behavior: 'smooth' }); }
            else d.row.scrollIntoView({ block: 'start', behavior: 'smooth' });
            linkMark(d);
        }
        function mark(d) {
            rows.forEach(function (x) { if (x.el) x.el.classList.toggle('is-on', x === d); });
        }


        /* ==================== 2.53.93:全贴索引 + 跨页定位 ==================== */
        // 用户需求(原话):「我在想,能不能直接定位本贴所有的链接及图片位置?既然能通过点击一键复制
        //   获得整个帖子的链接,那么应该也能定位到整个帖子的图片位置、楼层以及链接位置、楼层。」
        //
        // 三层落法(评估结论 + 本轮在浏览器里的取证;两个坑先写在头里):
        //   ① 收数据:复用「复制本贴」那台机器(fetchPage 逐页抓),把每页的 {层号, pid, 图指纹,
        //      链指纹} 收进一份**按 tid 缓存**的索引(sessionStorage)。当前页永远以活的 DOM 为准 ——
        //      只有它有元素、能就地滚。
        //   ② 页内定位:图的元素留在手上(realImgs → {el, fp}),链接也一样(scanLinks 的 el)
        //      ⇒「第 k 张 / 第 k 条」精确到那一个元素,不再只滚到楼层顶部。
        //   ③ 跨页定位:**按 URL 指纹寻址,不按序号**。理由:img 的尺寸判据
        //      (naturalWidth < 60)在 fetch 回来的文档里恒为 0(那份文档从没渲染过),同一层的
        //      「第 k 张」在两个来源里不是一个东西;指纹(绝对化后的 src / href)不会错位。
        //   ★ 为什么落点不交给 location.hash:#read_<pid> 在**折叠的 mark 楼**里是 display:none
        //     (真机实测 B2F / B11F / B13F / B14F / B15F 全是这种),浏览器滚不过去 ⇒ hash 只当
        //     顺手的加速,真正的落点由目标页的 consumeJump() 负责,落完把 hash 抹掉。
        //   ★ 真机取证(两条测试帖现抓):每层都有唯一 pid(div.f14#read_37496014 +
        //     a[onclick="copyUrl('37496014')"]),两页 pid 交集为空;**GF 的 pid 是字面上的
        //     'tpc'**(#read_tpc / #p_tpc);附件图写在 div#att_481381 里 —— 在 .tpc_content 内、
        //     但**不在** #read_<pid> 内,而表情图反过来 ⇒ 取数范围是整个 .tpc_content 再过滤
        //     (imgIsReal 那套判据正好收下附件、挡掉表情)。
        var TID = (location.href.match(/tid[-=](\d+)/i) || [])[1] || '';
        var IDX_KEY = 'spp_idx_' + TID;   // 全贴索引:按贴存;sessionStorage ⇒ 换页还在、关标签即弃
        var JUMP_KEY = 'spp_jump';        // 一次跨页跳转的待办(目标页读它、处理完立刻删)
        var idxS = { on: false, run: false, stop: false, pages: {}, fail: {}, done: 0, total: 0 };
        var headSeq = {}, totalCache = 1;  // 页头分组游标 / 总页数(build 反复要用,算一次)
        /* ★ 2.53.95:两栏各自的页码(互不干扰)。pgTop = 「这一次重绘后要把哪一栏拉回顶部」
           的一次性标记 —— 只有翻页才回顶,其它重绘(抓取进度、楼层展开)仍旧保持原来的滚动位置。 */
        var pgS = { img: 1, link: 1 }, pgTop = '';

        /* 每页几行(面板「数值设置」里可改;键是 spp_idx_per)。上下界必须与 SPP_PANEL_DEFS
           那一行、以及 buildOverlay 里那个 input 的 min/max 逐字同值。 */
        function perPage() { return SPP_CONFIG.num('spp_idx_per', 15, 5, 60); }

        /* 切片与页码收敛(纯函数,落盘前六组用例自检)。
           ★ 为什么收敛必须在切片**之前**:收回全贴、或换到链接更少的那一页之后,
             原来的页码可能已经越界(第 4/4 页 → 只剩 2 页),直接 slice 会画出一片空白,
             看起来就像「面板坏了」。 */
        function pgSpan(n, per, cur) {
            var tp = Math.max(1, Math.ceil(n / per));
            var c = Math.min(Math.max(1, cur), tp);
            return { tp: tp, cur: c, from: (c - 1) * per, to: c * per };
        }

        /* ★ 2.53.94:原来这里还 && 一枚开关(spp_idx_all)—— 用户拍板「这个功能是默认固定的,
           无需在设置面板中单独勾选开启」⇒ 开关撤掉,只留「用户当前是否处于全贴视图」这一个条件。 */
        function idxOn() { return !!idxS.on; }
        /* 重绘键的一部分:索引一变它就得变,否则边抓边填时界面上看不出动静 */
        function idxRev() {
            return (idxOn() ? 'A' : 'P') + Object.keys(idxS.pages).length + '/' + Object.keys(idxS.fail).length;
        }

        /* 把 URL 归一成「指纹」:绝对化 + 原样保留 path/query/hash。
           ★ 两种来源必须得到同一个字符串,跨页认人才成立:
             活的页面里 im.src / a.href 本来就是绝对的;fetch 回来的文档里 src 是
             //blue-plus.net/attachment/... 这种协议相对的 —— new URL(u, location.href) 两边归一到同一结果
             (真机验证过:DOMParser 出来的文档 baseURI 继承当前页 URL)。 */
        function fpOf(u) {
            if (!u) return '';
            try {
                var x = new URL(u, location.href);
                return x.origin + x.pathname + x.search + x.hash;
            } catch (e) { return String(u); }
        }

        /* 这一层的 pid:站点自己的「复制此楼地址」按钮上写着(楼主层是字面上的 tpc)。
           取不到就退回正文里那两个 id 前缀(p_ / read_)—— 同一份数据换条路。 */
        function rowPid(row) {
            var a = row.querySelector('.tiptop a[onclick*="copyUrl"]');
            var m = a && /copyUrl\(\s*'([^']+)'/.exec(a.getAttribute('onclick') || '');
            if (m) return m[1];
            var el = row.querySelector('.tpc_content [id^="read_"], .tpc_content [id^="p_"]');
            if (el) return (el.id || '').replace(/^(read_|p_)/, '');
            return '';
        }

        /* 一页文档 → 这一页的索引行。图只留指纹、链连提取码一起留。
           与当前页那条路共用 imgIsReal / scanLinks 两个判据 ⇒ 两边的「真图 / 真链接」定义一致。 */
        function scanDocFull(doc, pageNo) {
            var out = [];
            Array.prototype.forEach.call(doc.querySelectorAll('#main > form > .t5.t2'), function (row) {
                var tagEl = row.querySelector('.tiptop > .fl');
                var box = row.querySelector('.tpc_content');
                if (!box) return;
                var tag = tagEl ? (tagEl.textContent || '').trim() : '楼';
                var imgs = [], links = [];
                Array.prototype.forEach.call(box.querySelectorAll('img'), function (im) {
                    if (!imgIsReal(im)) return;
                    imgs.push({ el: null, fp: fpOf(im.getAttribute('src')) });
                });
                var lk = scanLinks(box);
                lk.links.forEach(function (l) {
                    links.push({ url: l.url, el: null, fp: l.fp || fpOf(l.url), code: l.code });
                });
                if (imgs.length || links.length) {
                    out.push({ tag: tag, pid: rowPid(row), page: pageNo, imgs: imgs, links: links });
                }
            });
            return out;
        }

        /* 索引读写。★ 版本对不上就整份作废:解析判据可能随版本变,旧索引当新用会错位。
           ★ 写失败(配额)不吵:内存里那份照样用,只是换页之后要重抓。 */
        function idxRead() {
            try {
                var raw = sessionStorage.getItem(IDX_KEY);
                if (!raw) return;
                var o = JSON.parse(raw);
                if (!o || o.ver !== window.__SPP_VERSION || !o.pages) return;
                idxS.pages = o.pages; idxS.fail = o.fail || {}; idxS.on = !!o.on;
            } catch (e) {}
        }
        function idxWrite() {
            try {
                sessionStorage.setItem(IDX_KEY, JSON.stringify({
                    ver: window.__SPP_VERSION, at: Date.now(), on: idxS.on,
                    pages: idxS.pages, fail: idxS.fail
                }));
            } catch (e) {}
        }

        /* 两枚「全贴索引」按钮的外显:标签即进度(面板只有 174px,另占一行写进度太挤)。 */
        function idxPaint() {
            if (!panel) return;
            var tot = idxS.total || totalCache;
            Array.prototype.forEach.call(panel.querySelectorAll('.spp-ii__btn[data-scope]'), function (b) {
                /* ★ 2.53.94:不再有任何开关决定这两枚按钮出不出现(功能本体固定存在)。 */
                b.classList.toggle('is-on', idxOn());
                if (idxS.run) b.textContent = '停止 ' + idxS.done + '/' + tot;
                else if (idxOn()) {
                    /* ★ 2.53.93 落盘后自查补的第三条(活体):标签要按**索引里真有几页**写。
                       原来一律写总数 —— 抓取被打断或某几页失败时,面板上写着「全贴 33 页」而
                       实际只有 12 页,串页头也是断的,读数与事实不符。 */
                    var have = Object.keys(idxS.pages).length;
                    var f = Object.keys(idxS.fail).length;
                    b.textContent = (have >= tot ? '全贴 ' + tot + ' 页' : '全贴 ' + have + '/' + tot + ' 页') +
                                    (f ? (' · ' + f + ' 失败') : '') + ' · 收回';
                    b.title = have >= tot
                        ? '索引已覆盖全贴 ' + tot + ' 页;点一下收回本页视图(索引留着)'
                        : ('已索引 ' + have + '/' + tot + ' 页' + (f ? (',另有 ' + f + ' 页抓取失败') : '') +
                           '。想接着抓:先收回本页,再点一次「全贴索引」(只补没抓的页)');
                } else b.textContent = '全贴索引';
            });
        }

        /* 全贴索引的主循环:逐页抓、边抓边填、再点一次即停。
           失败只记账不中断 —— 某几页 403 / 超时不该让整轮白跑(复制本贴那条路也是这个口径)。
           ★ 2.53.94:起一轮这件事抽成 idxStart() —— 按钮与「自动激活」两条路都走它,
             免得将来只改一处(用户这一轮要的正是把「要不要自动激活」单独拎出来当开关)。 */
        function idxStart() {
            var total = totalPages(), here = curPage();
            var todo = [];
            for (var n = 1; n <= total; n++) if (n !== here && !idxS.pages[n]) todo.push(n);
            idxS.on = true; idxS.total = total; idxS.done = total - todo.length;
            idxWrite(); build(); idxPaint();          // 先把「第 N 页 · 未索引」的骨架画出来
            if (!todo.length) {
                say('全贴 ' + total + ' 页都已在索引里(缓存 ' + Object.keys(idxS.pages).length + ' 页)', 'ok');
                return;
            }
            if (total <= 1) { say('本贴只有 1 页,没有可索引的其它页', 'bad'); return; }
            idxS.run = true; idxS.stop = false;
            idxPaint();
            say('正在逐页抓取…(再点一次「停止」可中断)', 'ok');
            var i = 0;
            (function step() {
                if (idxS.stop || i >= todo.length) {
                    idxS.run = false; idxPaint(); build();
                    say(idxS.stop ? ('已停止:抓到 ' + i + '/' + todo.length + ' 页')
                                  : ('全贴索引完成:' + total + ' 页'),
                        idxS.stop ? 'bad' : 'ok');
                    return;
                }
                var n = todo[i++];
                fetchPage(n, 2).then(function (html) {
                    idxS.pages[n] = scanDocFull(new DOMParser().parseFromString(html, 'text/html'), n);
                    delete idxS.fail[n];
                }).catch(function (e) {
                    idxS.fail[n] = (e && e.message) || '抓取失败';
                }).then(function () {
                    idxS.done = Math.min(total, idxS.done + 1);
                    idxWrite(); idxPaint(); build();
                    say('已索引 ' + idxS.done + '/' + total + ' 页…', 'ok');
                    step();
                });
            })();
        }

        /* 面板上那枚「全贴索引」按钮的出口:三态 —— 抓取中=停;已全贴=收回本页;否则=起一轮。 */
        function onScope() {
            if (idxS.run) { idxS.stop = true; idxPaint(); return; }        // 再点一次=停
            if (idxOn()) {                                                 // 已经开着 ⇒ 收回本页
                idxS.on = false; idxWrite(); build();
                say('已收回本页', 'ok');
                return;
            }
            idxStart();
        }

        /* ★ 2.53.94:自动激活的唯一入口 —— 开关 spp_idx_auto(**默认关闭**)。
           用户原话:「确认它是否会自动激活?默认不会自动激活。」
           ⇒ 不勾这枚开关,本函数一个请求都不会发出去;勾了才在打开帖子时自动起一轮
             (先吃缓存:索引已经全了的话,这里连一页都不抓,瞬间切到全贴视图)。
           ★ 已经在全贴、或正在抓、或本贴只有一页时,什么都不做(idxStart 内部另有兜底)。 */
        function idxAuto() {
            if (!SPP_CONFIG.bool('spp_idx_auto', false)) return;
            if (idxS.on || idxS.run) return;
            idxStart();
        }

        /* 跨页跳转:把「到哪一页、哪一层、哪一条」写进待办再换页。
           hash 只是顺手(没折叠的楼层上它能省一次滚动),真正的落点是目标页的 consumeJump()。 */
        function jumpTo(d, kind, k) {
            var it = k >= 1 ? (kind === 'img' ? d.imgs[k - 1] : d.links[k - 1]) : null;
            try {
                sessionStorage.setItem(JUMP_KEY, JSON.stringify({
                    tid: TID, tag: d.tag, pid: d.pid || '', kind: kind, k: k || 0,
                    fp: it ? (it.fp || '') : '', ts: Date.now()
                }));
            } catch (e) {}
            say('正在跳到第 ' + d.page + ' 页 ' + d.tag + '…', 'ok');
            location.href = pageUrl(d.page) + (d.pid ? ('#read_' + d.pid) : '');
        }

        /* 落点高亮:目标是正文里的图 / 链接,不在面板里 ⇒ 那条样式写在模块样式表的全局段(.spp-ii-hit) */
        function hit(el) {
            if (!el || !el.classList) return;
            el.classList.add('spp-ii-hit');
            setTimeout(function () { el.classList.remove('spp-ii-hit'); }, 2600);
        }
        function linkish(a) { return !/^(javascript:|#)/i.test(a.getAttribute('href') || ''); }

        /* 「第 k 个」的兜底清单:与 scanLinks 的取数口径对齐(跳 js / 锚点、售价条那块不算) */
        function anchorsIn(box) {
            return [].slice.call(box.querySelectorAll('a[href]')).filter(function (a) {
                if (!linkish(a)) return false;
                if (a.closest && a.closest('h6.quote')) return false;
                return true;
            });
        }
        function imagesIn(box) { return [].slice.call(box.querySelectorAll('img')).filter(imgIsReal); }

        /* 目标页读到待办:先按 pid 找层(退到层号文本),再按指纹找那一条;
           指纹认不出来(图被删 / 链接改过)就退到「第 k 个」,再不行退到该层顶部。
           ★ 折叠的 mark 楼先把正文展开 —— 否则目标在 display:none 里,滚不过去也看不见。 */
        function consumeJump() {
            var p = null;
            try { p = JSON.parse(sessionStorage.getItem(JUMP_KEY) || 'null'); } catch (e) {}
            if (!p) return;
            try { sessionStorage.removeItem(JUMP_KEY); } catch (e) {}
            if (p.tid !== TID) return;                        // 换贴了:这条待办不作数
            if ((Date.now() - p.ts) > 60000) return;          // 60 秒内有效:陈年记录不许再乱跳一次
            var row = null;
            Array.prototype.forEach.call(document.querySelectorAll('#main > form > .t5.t2'), function (r) {
                if (row) return;
                if (p.pid && rowPid(r) === p.pid) { row = r; return; }
                var t = r.querySelector('.tiptop > .fl');
                if (t && (t.textContent || '').trim() === p.tag) row = r;
            });
            if (!row) return;
            var box = row.querySelector('.tpc_content');
            if (box && getComputedStyle(box).display === 'none') {
                var ex = row.querySelector('a[onclick*="expandpuremark"]');
                if (ex) { try { ex.click(); } catch (e) {} }
            }
            setTimeout(function () { landOn(row, box, p); }, 80);
        }
        function landOn(row, box, p) {
            var el = null;
            if (p.fp && box) {
                var pick = (p.kind === 'img') ? imagesIn(box) : anchorsIn(box);
                Array.prototype.forEach.call(pick, function (x) {
                    if (el) return;
                    var raw = (p.kind === 'img') ? x.getAttribute('src') : (x.getAttribute('href') || '');
                    if (fpOf(raw) === p.fp) el = x;
                });
            }
            if (!el && box && p.k >= 1) {                     // 指纹没命中:退到「第 k 个」
                var list = (p.kind === 'img') ? imagesIn(box) : anchorsIn(box);
                el = list[p.k - 1] || null;
            }
            var target = el || row;
            target.scrollIntoView({ block: el ? 'center' : 'start', behavior: 'smooth' });
            hit(el || row);
            /* 图片是懒加载的:它加载完版面会长高,落点可能被顶走 ⇒ 半秒后补一次
               (只在目标确实跑出视野时才补,免得跟正在读的人抢滚动条)。 */
            setTimeout(function () {
                var b = target.getBoundingClientRect();
                if (b.bottom < 40 || b.top > window.innerHeight - 40) {
                    target.scrollIntoView({ block: el ? 'center' : 'start' });
                }
            }, 520);
            try { history.replaceState(null, '', location.pathname + location.search); } catch (e) {}
        }

        /* 全贴模式下按页分组:同一页只在第一行前插一条吸顶的「第 N 页」。
           headSeq 每次 build 清空(见 build 里那句),键用容器的 data-rows —— 两栏各一条链。 */
        function headRow(d, host) {
            if (!idxOn() || totalCache <= 1) return;
            var nm = host.getAttribute('data-rows') || 'img';
            if (headSeq[nm] === d.page) return;
            headSeq[nm] = d.page;
            var h = document.createElement('div');
            h.className = 'spp-ii__pg';
            h.textContent = '第 ' + d.page + ' 页' + (d.row ? ' · 本页'
                : (idxS.pages[d.page] ? ' · 已索引' : (idxS.fail[d.page] ? ' · 抓取失败' : ' · 未索引')));
            host.appendChild(h);
        }

        /* 滚到哪高亮哪 */
        function bindObserver() {
            if (!window.IntersectionObserver || !rows.length) return;
            const io = new IntersectionObserver(function (es) {
                es.forEach(function (e) {
                    if (!e.isIntersecting) return;
                    const hit = rows.filter(function (x) { return x.row === e.target; })[0];
                    if (hit) mark(hit);
                });
            }, { threshold: 0.05, rootMargin: '-20% 0px -60% 0px' });
            /* ★ 2.53.93 落盘后自查补的第二条(活体报错):全贴模式下 rows 里还有「非当前页」的行,
               它们没有 DOM 元素 ⇒ observe(null) 会抛 TypeError,**整条函数当场中断**,
               「滚到哪高亮哪」跟着一起哑掉。这里只对本页真有元素的行挂观察。 */
            rows.forEach(function (x) { if (x.row) io.observe(x.row); });
        }

        onDomReady(function () {
            idxRead();                                  /* ★ 2.53.93:先读索引缓存 —— 换页回来面板要立刻是全贴的 */
            consumeJump();                              /* ★ 2.53.93:跨页跳转的落点由它负责 */
            /* ★ 2.53.94:自动激活(开关 spp_idx_auto,默认关闭)。放在 900ms 之后 —— 让页面自己的
               资源先跑完、也让 build() 先建出面板;用户手动已经开着时它什么都不做(见 idxAuto)。 */
            setTimeout(function () { idxAuto(); }, 900);
            setTimeout(function () { build(); bindObserver(); }, 700);
            setTimeout(function () { build(); }, 2600);
        });
        /* ★ 2.53.51:贴纸块的左边是随窗口浮动的 ⇒ 窗口一改就得重算宽度(防抖 200ms,别在拖拽里狂算) */
        onDomReady(function () {
            var rt = 0;
            window.addEventListener('resize', function () {
                clearTimeout(rt);
                rt = setTimeout(function () { fitWidth(); applyTab(); }, 200);
            });
        });
        /* 楼层被展开 / 动态插入时重建(防抖 400ms,只在行数变化时才真重建) */
        onDomReady(function () {
            const host = document.querySelector('#main > form') || document.getElementById('main');
            if (!host) return;
            let t = 0;
            new MutationObserver(function () {
                clearTimeout(t);
                t = setTimeout(build, 400);
            }).observe(host, { childList: true, subtree: true });
        });
    }

    // ==================== 15f. 白底白字 → 加粗深色(仅 read.php,2.53.36)====================
    // 用户需求(原话):「对于白色字体的文本,脚本自动把它转化为加粗黑体(参考网页文本:urusai_cc)」。
    // ★ 现场取证(read.php?tid-2966403,探针 tools/probe/read-white-text{,2}.js):
    //   · 帖子正文是 div#read_tpc.f14 > .tpc_content,**底色纯白**(往上第一个实心背景是 div.t5 的
    //     rgb(255,255,255);正文自身字色 rgb(85,85,85),对比度 7.46 —— 正常);
    //   · 楼主用白字写了一串图床名:<span style="color:#ffffff ">urusai_cc</span>,同款还有 iili_io /
    //     meee / upload / tupian / fzmxfs —— 白字压白底,对比度 1.00,**等于看不见**;
    //   · 全楼扫下来,正文里对比度 < 1.6 的元素就这 6 个 ⇒ 同类问题只有这一种,范围可以收得很紧。
    // ★ 做法:只加 class + CSS 覆盖;既不动 HTML,也不删发帖人写的内联样式。
    //   ★ 为什么不用纯 CSS 选择器([style*="color:#ffffff"]):
    //     ① 内联写法太杂(#fff / #FFF / rgb(255,255,255) / 尾带空格 / 老标签 <font color>),字符串匹配必漏;
    //     ② **背景未必是白的** —— 引用块、深色底楼层里的白字本来就正常,一刀切会把它们一起做掉。
    //     只有拿实测对比度判才准。
    //   ★ 为什么只挑「发帖人显式设过色」的元素当候选:本例候选只有 6 个,而按计算色全量扫要遍历上千个
    //     元素、还会把站点自己的白色 UI 文字卷进来。继承色的后代不用单独管 —— 祖先加了 class,后代跟着变色。
    function initWhiteTextFix() {
        if (!WANTS.readOnly) return;
        if (!SPP_CONFIG.bool('spp_fix_white_text', true)) return;

        GM_addStyle(`
/* ===== SPP-WHITEFIX-BEGIN ===== */
/* 白底上的白字 → 加粗深色。!important 是必须的:发帖人写的是内联 style="color:#ffffff",
   内联声明的优先级本来就高于普通规则,不吃 !important 盖不住。
   ★ 只改颜色与字重,**不动 font-family** —— 正文本来就是无衬线(Arial/系统 sans),
     换族反而会让这 6 个词的字形和周围文字打架(见本轮回复里的说明)。 */
html.spp-read-on .spp-wf {
    color:#111111 !important; -webkit-text-fill-color:#111111 !important;
    font-weight:700 !important;
}
/* ===== SPP-WHITEFIX-END ===== */
`);

        const FIX = 'spp-wf';
        const MAX_CONTRAST = 2.2;   /* 对比度低于它就认定「看不见」 */
        const MIN_FG_LUM = 0.6;     /* 只处理「浅色字」;深色字压深底是另一回事,本轮不动 */

        function lum(rgbText) {
            const m = /rgba?\((\d+),\s*(\d+),\s*(\d+)/.exec(rgbText || '');
            if (!m) return null;
            const ch = function (v) { v = (+v) / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
            return 0.2126 * ch(m[1]) + 0.7152 * ch(m[2]) + 0.0722 * ch(m[3]);
        }
        /* 往上找第一个「不透明」的背景色:正文自己往往是 transparent,真正发白的是外层的 .t5 */
        function bgLum(el) {
            let e = el;
            while (e && e !== document.documentElement) {
                const bg = getComputedStyle(e).backgroundColor;
                const m = /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?/.exec(bg || '');
                if (m && (m[4] === undefined || +m[4] > 0.5)) return lum(bg);
                e = e.parentElement;
            }
            return lum(getComputedStyle(document.body).backgroundColor);
        }

        let count = 0;

        function scan() {
            const boxes = document.querySelectorAll('.tpc_content');
            for (let b = 0; b < boxes.length; b++) {
                const cands = boxes[b].querySelectorAll('[style*="color"], font[color]');
                for (let i = 0; i < cands.length; i++) {
                    const el = cands[i];
                    if (el.classList.contains(FIX)) continue;
                    /* 排除 background-color / border-color 这类同含 "color" 的声明 */
                    const st = el.getAttribute('style') || '';
                    if (!/(^|;)\s*color\s*:/.test(st) && !el.hasAttribute('color')) continue;
                    const fg = getComputedStyle(el).color;      /* 内联色不可靠(命名色不规范化),一律取计算值 */
                    const L1 = lum(fg);
                    if (L1 === null || L1 < MIN_FG_LUM) continue;
                    const L2 = bgLum(el);
                    if (L2 === null) continue;
                    const hi = Math.max(L1, L2), lo = Math.min(L1, L2);
                    if ((hi + 0.05) / (lo + 0.05) < MAX_CONTRAST) { el.classList.add(FIX); count++; }
                }
            }
            /* 读数口挂给探针/控制台(不参与逻辑)*/
            try { document.documentElement.setAttribute('data-spp-whitefix', String(count)); } catch (e) {}
        }

        onDomReady(function () {
            setTimeout(scan, 500);
            setTimeout(scan, 2200);
        });
        /* 楼层被展开 / 动态插入时重扫(防抖 400ms;scan 幂等,命中过的元素会跳过)*/
        onDomReady(function () {
            const host = document.querySelector('#main > form') || document.getElementById('main');
            if (!host || !window.MutationObserver) return;
            let t = 0;
            new MutationObserver(function () {
                clearTimeout(t);
                t = setTimeout(scan, 400);
            }).observe(host, { childList: true, subtree: true });
        });
    }

    // ==================== 15g. 购买框架(仅 read.php,2.53.39 / 2.53.40 / 2.53.41 / 2.53.46)====================
    // 用户需求:①「美化一个购物框架」(审核稿 tmp/r1/srv/preview-buy.html 已通过)
    //          ②「自动购买功能默认开启免费帖子,而收费帖子则默认关闭」
    //          + 当场拍板:「不做收费帖那枚开关,收费帖一律手动」(Q2 定案)
    //          + 2.53.40:「把已购买态也做成卡,刷新后不再退回灰字」
    //          + 2.53.41:用户圈出截图里「购买后包裹信息的那个 UI 框」⇒ 审核稿 v3/v4 定案
    //            **A 同族框 + 圆角 12px**(与上面那张卡同值),并同上把卡收成「通行证条」
    // ★ 现场取证(真实 tid-2973029;探针 tmp/r1/pay2.js / pay3.js / buy-real.js):
    //   · 未购买态 = div#read_tpc > h6.quote.jumbotron(807×28 @216,499),里面两件东西:
    //       <span class="s3">此帖售价 0 SP币,已有 9477 人购买</span>
    //       <input type="button" class="btn btn-danger" onclick="location.href='job.php?action=buytopic&tid=…&pid=tpc&verify=…'">
    //     紧跟一个**兄弟** blockquote.blockquote(警告条:#f8f8f8 底 + 2.67px #eee 左边线 + #736933 字)。
    //   · **已购买态(2.53.40 真买一单实测)**:同一枚 h6 照旧带 quote jumbotron,但里面**只剩**
    //       <span class="s3 f12 fn">此帖售价 0 SP币,已有 9478 人购买</span>
    //     —— 没有 input、**连那条警告 blockquote 也不渲染了**(站点只在「可买」时才提示被骗风险)。
    //     所以判据不能是「找那枚按钮」,得是「这枚 h6 里有没有『此帖售价 N SP币』」。
    //   · 原生点击是 **GET 跳转**:整页跳到 job.php,这就是「刷新」的来源。令牌在同一页的内联脚本里
    //     (var verifyhash = 'c23ac2aa')。
    //   ★★ 2.53.40 真买一单拿到的成功页形态(**这格悬了两版,现在有读数了**):
    //     200 + **807 字节** HTML、**没有 302**、redirected=false,正文里是一条
    //       <meta http-equiv="refresh" content="1;url=read.php?tid=2973029">
    //     ⇒ 「原生点一下会刷新」的机制本体就是这个 **1 秒后自跳的 meta refresh 页**,不是重定向头。
    //     ★ 而且这份 807 字节里「成功」「失败」两个词**一个都没有** —— 当初若按
    //       「响应里含『购买』就算成功」写,第一次真购买就会误报失败。复读本页那条判据是对的。
    // ★ 本模块四个职责(2.53.43 起):
    //   ① 外观:那行灰底图片按钮(bg_btn.gif + #bbb + 圆角 0 + cursor:default)换成审核稿那张卡;
    //      已购买态(没有按钮)也走同一张卡的「✓已购」形态 —— 否则刷新一下就退回 9.8px 灰字。
    //   ② 免费帖自动购买(开关 spp_autobuy_free,默认开):售价 0 且还能买 → 后台打那条 GET,
    //      成功后就地把卡片改成已购买态。**收费帖一律手动**;手动点也走后台请求(同样不刷新),
    //      并且**先就地展开确认条** —— 脚本永远不静默花 SP币。
    //   ③ 正文框(2.53.41):买完之后包住被解锁内容的那层(站点 blockquote.jumbotron,
    //      公版引用块长相:圆角 0 + 硬边 + 12px 偏褐字)换成与卡同族的圆角框;
    //      同轮把卡与框的左右外沿对齐(站点 h6 自带 2px 外边距 + 正文框自带 13px 内边距)。
    //   ④ 免刷新暴露正文(2.53.43):买完之后被解锁的正文**只有服务端再渲染一次才有**
    //      (实测未购买页里它压根不在 DOM,不是 display:none),而核实成功那一趟复读
    //      本来就把新页 HTML 拿在手里 ⇒ 顺手用它把 #read_tpc 就地换页:
    //      **零新增请求**,正文当场出现、不跳转、不丢滚动位置。
    // ★ 成功判定【不看响应体】:买完再 GET 一次本页,看返回的 HTML 里还有没有那条购买动作。
    //   这是服务端事实(真买一单验过:买完复读,那条 action 消失、人数 9477 → 9478)。
    //   ★★ 2.53.46 起按 **pid** 认(不再整页认):一页可以有好几个购买块,只认自己那一块。
    // ★★ 2.53.46(用户:「测试网址 read.php?tid-2717095-keyword-网址.html / 对于自动购买免费
    //   帖子没生效,此外这个自动购买貌似还是有一些瑕疵」):站点把「购买可见」渲染成
    //     div.tpc_content > div.f14[id^="read_"] > [h6.quote.jumbotron, blockquote…]
    //   —— 首帖那个 div 的 id 恰好叫 read_tpc,**楼层自己出售内容**时叫 read_<pid>
    //   (实测 tid-2717095 的 28 楼 = read_35357708;tid-2217905 的 1 / 2 / 末楼各一块)。
    //   旧版写死 getElementById('read_tpc') ⇒ 这些块连卡都不建,自动购买自然不生效。
    //   ⇒ 本版主体抽成 buildBlock(h6, host, txt, nth),onDomReady 里扫**全页**带「售价 N SP币」
    //     的 h6,宿主取最近的 read_* 祖先:#read_tpc 与 read_<pid> 一次全覆盖。
    //   ★★ 落地前干跑(探针 tmp/r1/srv/buy-multi-25346.js,把新主体抠出来在真页面上跑,
    //     fetch 全打桩 ⇒ 零账户副作用):
    //     · 建卡:那个帖的 28 楼当场出卡(read_tpc 那块已有卡,按块幂等跳过,没重建);
    //     · 自动购买:只发了 2 条请求 —— BUY job.php?action=buytopic&tid=2717095
    //       &pid=35357708&verify=c23ac2aa + 复读本页,URL 与站点原生按钮逐字一致;
    //     · 判成功:买完卡转「✓已购买 / 已解锁」(这一趟 872 = 本地 871+1,服务端当时仍是
    //       871 未购买 —— 真买一单时服务端会给 872,两路一致);
    //     · stillBuyable 单元测试 10 条全对,含本版修掉的那一条:
    //       「整页只剩 B 的购买动作、问 A」= 已买上(旧版整页判会把它读成失败)。
    //   ★★ 落地当轮**真买两单**(用户:「直接跑,看看情况」;探针 tmp/r1/srv/
    //     inject-25346-real.js、inject-25346-real2.js,注入整份载荷、不打桩):
    //     · tid-2717095 第 28 楼(0 SP、871 人购买):当场自动买下 → 卡转「✓已购买 / 已解锁」,
    //       被解锁的正文(https://acgnmap.com/)**不刷新就出现**并被打上 blockquote.spp-buy__body;
    //       服务端复读:那条 action=buytopic 消失、人数 871 → 872。**已购的楼层块**现场 markup =
    //         <h6><span class="s3 f12 fn">此帖售价 0 SP币,已有 872 人购买</span></h6>
    //         <blockquote class="blockquote jumbotron">正文</blockquote>
    //       —— 正是 revealContent 里「纯出售块整段重建 + claimBody 认领正文」那两条假设的现场样本。
    //     · tid-2217905(一页三块:1 楼 0 SP、2 楼 0 SP、末楼 999999999 SP):两块各自买下
    //       (503 → 504、424 → 425),收费那块只出卡、**一次请求都没发**。
    //     ⇒ 同时实测到上面那条竞态(1 楼买成功却被读成失败)⇒ 本版改成退让重试。
    //   ⇒ 同时修掉另外三处「瑕疵」:① 购买控件判据放宽(a[onclick]/a[href] 也算),认不出
    //     购买地址就**整块不动**(旧版会照着空地址建卡,然后永远停在「购买中…」);
    //     ② 成功判定按 pid 认(旧版整页认 ⇒ 一页两块时买下 A 会被 B 的动作判成失败);
    //     ③ 免刷新换页只动**这一块的宿主**,且只在「纯出售块」时整段重建,正文里夹一段
    //        出售的帖子只换售价行 + 补上被解锁的那枚 blockquote。
    function initBuyFramework() {
        if (!WANTS.readOnly) return;

        GM_addStyle(`
/* ===== SPP-BUY-BEGIN ===== */
/* 作用域一律挂在 html.spp-read-on 与 .spp-buy(JS 打上的类)上:没跑 JS、或没认出那个购买块时,
   下面所有规则一条都不匹配 ⇒ 页面保持站点原样(与 2.52.98 的快速发帖框同一套「保持原样」思路)。
   ★ 站点的 .jumbotron 实测:margin 8px 2px 2px / line-height 28px / font-size 9.8px / 无边框,
     那枚 input.btn.btn-danger 是 bg_btn.gif + #bbb + 圆角 0 + cursor:default + 高 14px。 */
html.spp-read-on h6.spp-buy {
    display:block !important;
    margin:10px 0 7px !important;          /* 2.53.41:抹掉那 2px,让卡与下面的正文框左右同宽 */
    padding:0 !important;
    border:0 !important; background:none !important;
    font-size:14px !important; font-weight:400 !important; line-height:normal !important;
    min-height:0 !important;
}
html.spp-read-on h6.spp-buy .spp-buy__bar {
    display:flex !important; align-items:center !important; gap:14px !important; flex-wrap:wrap !important;
    background-color:#ffffff !important;
    background-image:linear-gradient(180deg,#ffffff 0%,#fbfcfe 100%) !important;
    border:1px solid #e6ebf2 !important; border-radius:12px !important;
    padding:11px 13px 11px 12px !important;
    box-shadow:0 1px 2px rgba(15,23,42,.03), 0 6px 18px -12px rgba(15,23,42,.18) !important;
}
/* 已购买态:整卡转绿边 + 浅绿底(与卡内那枚「已解锁」胶囊同一族) */
html.spp-read-on h6.spp-buy .spp-buy__bar.is-done {
    background-color:#fbfefc !important;
    background-image:linear-gradient(180deg,#fbfefc 0%,#f6fcf9 100%) !important;
    border-color:#cfeede !important;
}
html.spp-read-on h6.spp-buy .spp-buy__seal {
    flex:0 0 auto !important; display:flex !important; flex-direction:column !important;
    align-items:center !important; justify-content:center !important;
    width:44px !important; height:44px !important; border-radius:11px !important;
    background-color:#eefaf3 !important; border:1px solid #cfeede !important; color:#1f7a55 !important;
    font-size:15px !important; font-weight:700 !important; line-height:1 !important;
}
html.spp-read-on h6.spp-buy .spp-buy__seal.is-paid {
    background-color:#fff5e2 !important; border-color:#f3e2c2 !important; color:#a4670f !important;
}
html.spp-read-on h6.spp-buy .spp-buy__seal.is-done {
    background-color:#eefaf3 !important; border-color:#cfeede !important; color:#1f7a55 !important;
}
html.spp-read-on h6.spp-buy .spp-buy__seal b { font-weight:700 !important; font-variant-numeric:tabular-nums !important; }
html.spp-read-on h6.spp-buy .spp-buy__seal i {
    font-style:normal !important; font-size:9px !important; letter-spacing:.5px !important;
    margin-top:2px !important; opacity:.8 !important;
}
html.spp-read-on h6.spp-buy .spp-buy__meta { min-width:0 !important; }
html.spp-read-on h6.spp-buy .spp-buy__t1 {
    display:flex !important; align-items:center !important; gap:7px !important; flex-wrap:wrap !important;
    font-size:13.5px !important; font-weight:600 !important; color:#334155 !important; line-height:1.4 !important;
}
html.spp-read-on h6.spp-buy .spp-buy__t2 {
    margin-top:3px !important; font-size:11.5px !important; font-weight:400 !important;
    color:#8a94a6 !important; line-height:1.4 !important;
}
html.spp-read-on h6.spp-buy .spp-buy__num {
    color:#2f6f68 !important; font-weight:700 !important; font-variant-numeric:tabular-nums !important;
}
html.spp-read-on h6.spp-buy .spp-buy__dot {
    display:inline-block !important; width:3px !important; height:3px !important;
    border-radius:50% !important; background-color:#cfd8e3 !important;
    margin:0 7px !important; vertical-align:middle !important;
}
html.spp-read-on h6.spp-buy .spp-buy__pill {
    font-size:10.5px !important; font-weight:600 !important; line-height:1 !important;
    padding:3px 7px !important; border-radius:999px !important;
    background-color:#eefaf3 !important; border:1px solid #cfeede !important; color:#1f7a55 !important;
}
html.spp-read-on h6.spp-buy .spp-buy__pill.is-paid {
    background-color:#fff5e2 !important; border-color:#f3e2c2 !important; color:#a4670f !important;
}
html.spp-read-on h6.spp-buy .spp-buy__pill.is-done {
    background-color:#eefaf3 !important; border-color:#cfeede !important; color:#1f7a55 !important;
}
html.spp-read-on h6.spp-buy .spp-buy__right {
    margin-left:auto !important; display:flex !important; align-items:center !important; gap:8px !important;
}
html.spp-read-on h6.spp-buy .spp-buy__btn {
    display:inline-flex !important; align-items:center !important; gap:7px !important;
    height:34px !important; padding:0 16px !important; box-sizing:border-box !important;
    border:1px solid transparent !important; border-radius:10px !important;
    font-family:inherit !important; font-size:13px !important; font-weight:600 !important;
    line-height:1 !important; white-space:nowrap !important;
    color:#ffffff !important; -webkit-text-fill-color:#ffffff !important;
    background-color:#2f6f68 !important;
    background-image:linear-gradient(180deg,#3f8a80,#2f6f68) !important;
    box-shadow:0 1px 2px rgba(47,111,104,.28), 0 6px 16px -10px rgba(47,111,104,.55) !important;
    cursor:pointer !important;
    /* ★ 常驻规则里【不写】transition:2.50.9 的教训 —— 首次插入的元素会从浏览器默认值
       慢慢过渡过来,后台标签页不推进过渡,于是永远停在起始值(灰底黑字)。 */
}
html.spp-read-on h6.spp-buy .spp-buy__btn:hover {
    background-image:linear-gradient(180deg,#46968b,#2b6660) !important;
    transition:background-image .14s ease, box-shadow .14s ease !important;
}
html.spp-read-on h6.spp-buy .spp-buy__btn:disabled { cursor:progress !important; opacity:.72 !important; }
html.spp-read-on h6.spp-buy .spp-buy__btn.is-done {
    background-color:#eefaf3 !important; background-image:none !important;
    border-color:#cfeede !important; color:#1f7a55 !important; -webkit-text-fill-color:#1f7a55 !important;
    box-shadow:none !important; cursor:default !important; opacity:1 !important;
}
html.spp-read-on h6.spp-buy .spp-buy__btn.is-fail {
    background-color:#fdf0ee !important; background-image:none !important;
    border-color:#f3d4cf !important; color:#b23a2c !important; -webkit-text-fill-color:#b23a2c !important;
    box-shadow:none !important; opacity:1 !important;
}
/* 收费帖的就地确认条:默认整条不显示,只有点了收费帖的按钮才 .is-on(flex-basis 100% 让它独占一行)*/
html.spp-read-on h6.spp-buy .spp-buy__cf { display:none !important; }
html.spp-read-on h6.spp-buy .spp-buy__cf.is-on {
    display:flex !important; flex:1 1 100% !important; align-items:center !important;
    gap:10px !important; flex-wrap:wrap !important;
    margin-top:9px !important; padding:9px 12px !important; box-sizing:border-box !important;
    background-color:#fffaeb !important; border:1px solid #f4e4bb !important; border-radius:10px !important;
    color:#8a6512 !important; font-size:12.5px !important; font-weight:400 !important; line-height:1.5 !important;
}
html.spp-read-on h6.spp-buy .spp-buy__cf b {
    display:inline-block !important; background-color:#ffffff !important;
    border:1px solid #f3e2c2 !important; color:#a4670f !important;
    border-radius:999px !important; padding:1px 8px !important;
    font-weight:700 !important; font-variant-numeric:tabular-nums !important;
}
html.spp-read-on h6.spp-buy .spp-buy__yes,
html.spp-read-on h6.spp-buy .spp-buy__no {
    height:30px !important; padding:0 14px !important; box-sizing:border-box !important;
    border-radius:9px !important; font-family:inherit !important; font-size:12.5px !important;
    font-weight:600 !important; line-height:1 !important; cursor:pointer !important;
    border:1px solid #e6ebf2 !important; background-color:#ffffff !important; background-image:none !important;
    color:#334155 !important; -webkit-text-fill-color:#334155 !important;
}
html.spp-read-on h6.spp-buy .spp-buy__yes {
    margin-left:auto !important; border-color:transparent !important;
    background-color:#b23a2c !important;
    background-image:linear-gradient(180deg,#d9705f,#b23a2c) !important;
    color:#ffffff !important; -webkit-text-fill-color:#ffffff !important;
}
html.spp-read-on h6.spp-buy .spp-buy__fallback {
    flex:1 1 100% !important; margin-top:5px !important;
    font-size:11px !important; color:#8a94a6 !important; text-decoration:underline !important;
    text-underline-offset:2px !important;
}
/* 警告条:站点原文一字不动,只换外观。
   ★★ 选择器为什么不是「h6.spp-buy + blockquote.blockquote」(2.53.40 落地当轮改的):
     已购买之后,紧随 h6 的那个 blockquote **不再是警告条,而是被解锁的帖子正文** ——
     站点把「购买可见」的内容包在 blockquote.blockquote 里。实测已购页该选择器命中数 1,
     命中的正是正文块 ⇒ 会把正文染成小号褐字 + 挂个 ⚠。
   ⇒ 改成 JS 按内容认领(见 initBuyFramework 里那段):文本同时含「举报」与「封掉ID」
     的那条才打标 spp-buy__warn,这里只认这个标。认不出就一个都不动。 */
html.spp-read-on blockquote.spp-buy__warn {
    display:flex !important; align-items:flex-start !important; gap:8px !important;
    margin:0 2px 7px !important; padding:7px 11px 7px 10px !important; box-sizing:border-box !important;
    background-color:#fffaeb !important; background-image:none !important;
    border:1px solid #f4e4bb !important; border-left:3px solid #e8c469 !important;
    border-radius:8px !important;
    color:#8a6512 !important; font-size:11.5px !important; font-weight:400 !important; line-height:1.55 !important;
}
html.spp-read-on blockquote.spp-buy__warn b { font-weight:400 !important; }
html.spp-read-on blockquote.spp-buy__warn::before {
    content:"⚠" !important; flex:0 0 auto !important; font-size:12px !important; line-height:1.45 !important;
}

/* ---------- 2.53.41:正文框 —— 买完之后包住被解锁内容的那一层 ----------
   ★ 站点把它做成「引用块」长相(实测 tid-2973029,823×91):#f8f8f8 底 + 0.667px #eee 四边
     + 2.667px #eee 左线 + **圆角 0** + 内边距 3px 5px 3px 8px + 12px/28px #736933 字。
     用户审核稿(preview-frame.html)定案 A「同族框」:与上面那张卡同一族配色,圆角取 12px
     —— 与卡同值(卡自己就是 12px,卡里那枚 ✓ 印章是 11px)。
   ★ 为什么连 box-sizing 一起改:站点给 h6 与这层 blockquote 的**都是 width:80%**
     (实测:内容列 1008.33 ⇒ 两者都是 806.67),但这层是 content-box、又自带 8+5px 内边距
     与 3.33px 边框 ⇒ 它的**成盒**是 823,比上面那张卡(807)左右各凸 2px / 14px —— 就是
     用户截图里箭头指的那道错位。改成 border-box 之后成盒正好=80%=806.67;再把 h6 那 2px
     外边距抹掉(见上面那条),两者左右外沿严格对齐(实测偏差 0 / 0)。
   ★★ 这里**不能**写 width:auto —— 试过,它会把站点那条 80% 一起顶掉,框当场撑成 100%
     (实测 1008 宽、右边比卡宽 202px)。这一层只能动盒模型,不能动宽度的来源。
   ★ JS 只按**内容**认领这一层(跳过「举报+封掉ID」那条警告条),认不出就不打标 ⇒ 页面原样。 */
html.spp-read-on blockquote.spp-buy__body {
    box-sizing:border-box !important;
    margin:0 0 10px !important;
    background-color:#fbfefc !important; background-image:none !important;
    border:1px solid #d9ece2 !important;
    border-left:3px solid #8ccfae !important;
    border-radius:12px !important;
    padding:11px 14px 12px !important;
}
/* 提亮已购正文(开关 spp_buy_lift,默认开):站点给「付费可见内容」用的是引用块的字号字色
   (12px #736933),花钱买回来的东西按正常正文显示更好读。只认根节点那个标。 */
html.spp-read-on.spp-lift-on blockquote.spp-buy__body {
    color:#3f4a5a !important;
    font-family:inherit !important;
    font-size:13px !important;
    line-height:1.75 !important;
}
html.spp-read-on.spp-lift-on blockquote.spp-buy__body a { color:#0a6ea8 !important; }

/* ---------- 2.53.41:买完之后那张卡收成一条「通行证条」(40px)----------
   ★ 为什么要有它:买完之后这张卡的活儿只剩「报告状态」,原来那张 69px 的卡把「已购买」
     念了三遍(左侧印章 / 标题 / 右侧那枚点不动的绿按钮)。收成一行后正文上移约 29px。
   ★ 自动购买(默认开)那条路上本模块**一上来就用这条的骨架**(is-pass is-busy),
     不再先画一张带大按钮的卡再切走 —— 否则页面上会有一次可见的跳动(审核稿里演过两条路)。
   ★ 不做 transitions:2.50.9 的教训(首次插入的元素会从浏览器默认值慢慢过渡过来,
     后台标签页不推进过渡 ⇒ 永远停在起始值)。 */
html.spp-read-on h6.spp-buy .spp-buy__bar.is-pass {
    height:40px !important;
    padding:0 13px 0 11px !important;
    gap:10px !important;
    flex-wrap:nowrap !important;
    border-color:#d9ece2 !important;
    border-left:3px solid #8ccfae !important;
    border-radius:10px !important;
    background-color:#fbfefc !important;
    background-image:linear-gradient(180deg,#fbfefc 0%,#f3faf6 100%) !important;
    box-shadow:0 1px 2px rgba(15,23,42,.03) !important;
}
/* 失败态:那行「或改用站点原路购买」要能换行落下来(正常态永远一行)*/
html.spp-read-on h6.spp-buy .spp-buy__bar.is-pass.is-fail {
    height:auto !important; min-height:40px !important;
    padding:8px 13px 8px 11px !important; flex-wrap:wrap !important;
    border-color:#f3d4cf !important; border-left-color:#e0a79c !important;
    background-image:linear-gradient(180deg,#fefbfa 0%,#fdf4f2 100%) !important;
}
html.spp-read-on h6.spp-buy .spp-buy__pic {
    flex:0 0 auto !important;
    width:20px !important; height:20px !important; border-radius:50% !important;
    display:inline-flex !important; align-items:center !important; justify-content:center !important;
    background-color:#e3f5eb !important; border:1px solid #c6e8d6 !important;
    color:#1f7a55 !important; -webkit-text-fill-color:#1f7a55 !important;
    font-size:11px !important; font-weight:700 !important; line-height:1 !important;
}
html.spp-read-on h6.spp-buy .spp-buy__pic.is-busy {
    background-color:#f1f4f8 !important; border-color:#e3e8ef !important;
    color:#93a1b5 !important; -webkit-text-fill-color:#93a1b5 !important;
}
html.spp-read-on h6.spp-buy .spp-buy__pic.is-fail {
    background-color:#fdf0ee !important; border-color:#f3d4cf !important;
    color:#b23a2c !important; -webkit-text-fill-color:#b23a2c !important;
}
html.spp-read-on h6.spp-buy .spp-buy__tt {
    font-size:13px !important; font-weight:600 !important; color:#334155 !important;
    -webkit-text-fill-color:#334155 !important; line-height:1 !important; white-space:nowrap !important;
}
html.spp-read-on h6.spp-buy .spp-buy__facts {
    margin-left:auto !important;
    font-size:11.5px !important; font-weight:400 !important; color:#8a94a6 !important;
    -webkit-text-fill-color:#8a94a6 !important; line-height:1.4 !important; white-space:nowrap !important;
}
html.spp-read-on h6.spp-buy .spp-buy__pill.is-busy {
    background-color:#f1f4f8 !important; border-color:#e3e8ef !important; color:#7c8ba1 !important;
}
html.spp-read-on h6.spp-buy .spp-buy__pill.is-fail {
    background-color:#fdf0ee !important; border-color:#f3d4cf !important; color:#b23a2c !important;
}
html.spp-read-on h6.spp-buy .spp-buy__bar.is-pass .spp-buy__fallback { color:#b23a2c !important; }
/* ---------- 2.53.41 新增结束 ---------- */
/* ===== SPP-BUY-END ===== */
`);

        // ---- 「这一块还能不能买」:按 pid 认,不按整页认(2.53.46)----
        //   ★★ 旧代码判的是整页「action=buytopic 有没有出现过」:一页有多个购买块时,买下 A 之后 B 的
        //     购买动作还在 ⇒ **明买成功却被判成失败**(用户说的「瑕疵」之一)。
        //   ★ 按块判的钥匙是 pid:宿主 id 就是 pid(read_<pid>),首帖那个 read_tpc 得从
        //     购买地址里的 pid= 取。两个都取不到才回落到整页判 —— 那正是旧版行为,
        //     比「永远判成功」安全。
        function stillBuyable(html, pid) {
            //   ★ 判据取「一条购买地址的整段 token」(到引号/空白为止),再在这一段里找 pid= ——
            //     这样 pid 写在 action 之前还是之后都认得出,邻块也不会串味。
            if (!pid) return /action=buytopic/i.test(html);
            const re = /action=buytopic[^"'\s<>]*/gi;
            let m, any = false, mine = false;
            while ((m = re.exec(html))) {
                any = true;
                const q = /[?&](?:amp;)?pid=(\d+)/.exec(m[0]);
                if (q && q[1] === String(pid)) mine = true;
            }
            if (!any) return false;          // 一条购买动作都没有 ⇒ 都买过了
            return mine;                     // 自己那块还有动作 ⇒ 没买上
        }

        // ---- 单个购买块:建卡 → 认警告条 → 认正文 →(免费帖)自动购买 ----
        //   ★★ 2.53.46:这一段原本直接写在 onDomReady 里、只认 #read_tpc 一个容器。
        //     抽成函数之后一页可以调 N 次(见下面那个扫描)。
        function buildBlock(h6, host, txt, nth) {
                const pm = /售价\s*([\d,]+)\s*SP币/.exec(txt);
                if (!pm) return;
                const price = parseInt(pm[1].replace(/,/g, ''), 10);
                if (!isFinite(price)) return;
                const bm = /已有\s*([\d,]+)\s*人购买/.exec(txt);
                let buyers = bm ? parseInt(bm[1].replace(/,/g, ''), 10) : 0;
                if (!isFinite(buyers)) buyers = 0;
                const isFree = price === 0;

                // ---- 站点那枚购买控件:判据放宽,认不出地址就**整块不动** ----
                //   ★ 没有控件 = 服务端认为已购买(2.53.40 真买一单实测:买完复读,按钮消失了)。
                //   ★★ 2.53.46 两处:① 判据从「input[onclick*=buytopic]」放宽到 a[onclick]/a[href]
                //     —— 旧版遇到别的形态会把**还能买**的帖画成绿卡「已购买 / 已解锁」;
                //     ② 解析不出购买地址时**直接 return,一个节点都不动** —— 旧版会照着空地址把
                //     卡建出来,然后自动购买那条路永远停在「购买中… / 请稍候」(另一个「瑕疵」)。
                const rawCtl = h6.querySelector(
                    'input[onclick*="buytopic"], a[onclick*="buytopic"], a[href*="buytopic"]');
                const bought = !rawCtl;
                let buyUrl = null;
                if (rawCtl) {
                    const oc = rawCtl.getAttribute('onclick') || '';
                    const hf = rawCtl.getAttribute('href') || '';
                    const um = /location\.href\s*=\s*['"]([^'"]+)['"]/.exec(oc) ||
                        (/buytopic/.test(hf) ? [null, hf] : null);
                    if (um) { try { buyUrl = new URL(um[1], location.href).href; } catch (e) { buyUrl = null; } }
                    if (!buyUrl) return;         // 认不出购买地址 ⇒ 保留站点原样的那一行
                }
                // pid:宿主 id 就是 pid(read_<pid>),首帖那个 read_tpc 得从购买地址里取。
                //   ★ 它是「按块判成功」的钥匙,也是本版修掉「多块同页误判」的关键。
                const hm = /^read_(\d+)$/.exec(host.id);
                const qm = buyUrl ? /[?&]pid=(\d+)/.exec(buyUrl) : null;
                const pid = hm ? hm[1] : (qm ? qm[1] : null);

                // ---- 建卡:外观全在这张卡里;站点那两个节点(span.s3 / input)被替换掉 ----
                h6.classList.add('spp-buy');
                while (h6.firstChild) h6.removeChild(h6.firstChild);

                const bar = document.createElement('div');
                bar.className = 'spp-buy__bar';
                bar.innerHTML =
                    '<div class="spp-buy__seal' + (isFree ? ' is-free' : ' is-paid') + '">' +
                        '<b>' + (isFree ? '免费' : price) + '</b><i>SP</i>' +
                    '</div>' +
                    '<div class="spp-buy__meta">' +
                        '<div class="spp-buy__t1">售价 <b class="spp-buy__num">' + price + '</b> SP币' +
                            '<span class="spp-buy__pill' + (isFree ? ' is-free' : ' is-paid') + '">' +
                                (isFree ? '免费帖' : '需付费') + '</span>' +
                        '</div>' +
                        '<div class="spp-buy__t2">已有 <b class="spp-buy__num spp-buy__cnt">' +
                            buyers.toLocaleString() + '</b> 人购买</div>' +
                    '</div>' +
                    '<div class="spp-buy__right">' +
                        '<button type="button" class="spp-buy__btn">愿意购买,我买,我付钱</button>' +
                    '</div>' +
                    '<div class="spp-buy__cf">' +
                        '<span>确认花费 <b>' + price + ' SP币</b> 购买本帖?</span>' +
                        '<button type="button" class="spp-buy__no">取消</button>' +
                        '<button type="button" class="spp-buy__yes">确认购买</button>' +
                    '</div>';
                h6.appendChild(bar);

                const btn = bar.querySelector('.spp-buy__btn');
                const cf = bar.querySelector('.spp-buy__cf');
                let busy = false;
                let done = false;
                let usePass = false;      // 已经是「通行证条」形态了(见 passHtml)

                // 「通行证条」的三态 HTML(2.53.41,审核稿 A 方案)。买完之后这张卡的活儿只剩
                //   报告状态,所以它从 69px 的大卡收成 40px 的一行:✓ 圆标 + 一句话 + 售价人数。
                //   state: 'busy' 请求中 / 'done' 已购买 / 'fail' 失败(失败那枚胶囊兼作重试提示)。
                function passHtml(state) {
                    const ok = state === 'done';
                    const bad = state === 'fail';
                    return '<span class="spp-buy__pic' + (ok ? '' : bad ? ' is-fail' : ' is-busy') + '">' +
                            (ok ? '✓' : bad ? '!' : '⋯') + '</span>' +
                        '<span class="spp-buy__tt">' + (ok ? '已购买' : bad ? '购买失败' : '购买中…') + '</span>' +
                        '<span class="spp-buy__pill ' + (ok ? 'is-done' : bad ? 'is-fail' : 'is-busy') + '">' +
                            (ok ? '已解锁' : bad ? '点我重试' : '请稍候') + '</span>' +
                        '<span class="spp-buy__facts">售价 <b class="spp-buy__num">' + price + '</b> SP币' +
                            '<i class="spp-buy__dot"></i>已有 <b class="spp-buy__num">' +
                            buyers.toLocaleString() + '</b> 人购买</span>';
                }

                // 卡片切成「已购买」形态(通行证条)。state 只是读数口用的标签('done' = 本次买下的,
                //   'bought' = 服务端本来就认为已购买),两者外观完全一致。
                function paintDone(state) {
                    busy = false; done = true;
                    usePass = true;
                    bar.className = 'spp-buy__bar is-pass';
                    bar.innerHTML = passHtml('done');
                    try { bar.setAttribute('data-spp-buy', state); } catch (e) {}
                }

                function setDone() { buyers++; paintDone('done'); }

                function setFail() {
                    busy = false;
                    // ★ 自动购买那条路用的是通行证条:它的失败态就地换成红色的「点我重试」+
                    //   站点原路的兜底链接(点这条 = 整页跳 job.php,和站点原生按钮同一条路)。
                    //   手动那条路仍旧回到大卡:那枚红按钮本来就是为「失败了让你再试一次」准备的。
                    if (usePass) {
                        bar.className = 'spp-buy__bar is-pass is-fail';
                        bar.innerHTML = passHtml('fail') +
                            (buyUrl ? '<a class="spp-buy__fallback" href="' + buyUrl +
                                '">或改用站点原路购买(会跳转页面)</a>' : '');
                        bar.addEventListener('click', function () {
                            if (bar.getAttribute('data-spp-buy') === 'fail') doBuy();
                        });
                        try { bar.setAttribute('data-spp-buy', 'fail'); } catch (e) {}
                        return;
                    }
                    btn.disabled = false;
                    btn.className = 'spp-buy__btn is-fail';
                    btn.textContent = '购买失败,点我重试';
                    try { bar.setAttribute('data-spp-buy', 'fail'); } catch (e) {}
                    // 兜底退路:站点原路(整页跳)。只在你明确点这行小字时才走。
                    if (buyUrl && !bar.querySelector('.spp-buy__fallback')) {
                        const a = document.createElement('a');
                        a.className = 'spp-buy__fallback';
                        a.href = buyUrl;
                        a.textContent = '或改用站点原路购买(会跳转页面)';
                        bar.appendChild(a);
                    }
                }

                // ---- 动作:后台 GET 过去(不导航)→ 再 GET 本页核实服务端真的记上了 ----
                function doBuy() {
                    if (done || busy) return;
                    if (!buyUrl) { setFail(); return; }   // 建卡前已挡过,这行只是保险
                    busy = true;
                    cf.classList.remove('is-on');
                    btn.disabled = true;
                    btn.className = 'spp-buy__btn';
                    btn.textContent = '购买中…';
                    try { bar.setAttribute('data-spp-buy', 'busy'); } catch (e) {}
                    const pageUrl = location.href.split('#')[0];
                    // 复读本页核实:判据 =「**这一块**(pid)的购买动作没了」(见 stillBuyable)。
                    //   ★★ 2.53.46 落地当轮**真买两单**抓到的竞态:这一趟复读紧跟在购买请求
                    //     后面,服务端可能还没把这一单落库就渲染了页 —— 实测 tid-2217905 的
                    //     1 楼:人数 503 → 504(服务端确实买成功了),卡却停在「购买失败 点我重试」。
                    //   ⇒ 判失败之前先**退让重试两趟**(600ms / 1400ms);成功就用那一趟的 HTML
                    //     换页。竞态窗口只有几百毫秒,两趟退让实测足够(2 楼那块一次就过)。
                    const RETRY = [600, 1400];
                    function verify(step) {
                        return fetch(pageUrl, { credentials: 'include', cache: 'no-store' })
                            .then(function (r) { return r.text(); })
                            .then(function (html) {
                                if (!stillBuyable(html, pid)) {
                                    setDone();
                                    // ★ 2.53.43:把这份**已经到手**的 HTML 接着用起来 ——
                                    //   买完之后被解锁的正文只有服务端再渲染一次才会出现,
                                    //   而我们这一趟正好刚把它取回来(见 revealContent)。
                                    revealContent(html);
                                    return;
                                }
                                if (step >= RETRY.length) { setFail(); return; }
                                return new Promise(function (res) { setTimeout(res, RETRY[step]); })
                                    .then(function () { return verify(step + 1); });
                            });
                    }
                    fetch(buyUrl, { credentials: 'include', cache: 'no-store' })
                        .then(function () { return verify(0); })
                        .catch(function () { setFail(); });
                }

                // ---- 2.53.43:免刷新把买回来的正文摆出来(2.53.46 起**只动这一块**)----
                //   ★ 为什么必须再要一次页面:实测未购买页这一块 = [h6 购买卡, blockquote 警告条],
                //     纯文本 101 字、**没有任何正文块**;已购买页同一位置 =
                //     [正文文本/链接/图…, h6 售价行, 被解锁的 blockquote]。被卖的内容不是被
                //     display:none 藏起来的,是服务端根本不发 ⇒ 不换页就永远看不到。
                //   ★ 而这一趟 GET **本来就在跑**(上面那句复读本页核实成功),所以这个功能
                //     **零新增请求**:只是把原先丢掉的那份 HTML 接着用。实测 364ms / 204KB。
                //   ★ 手法 = 「就地换页」:这一块整段按新页重建,把我们这张卡搬到新结构里 h6 的
                //     位置上(卡带着 data-spp-buy 状态与买家数,不能丢)。
                //   ★★ 2.53.46 三处收紧/放宽:
                //     ① 宿主从写死的 #read_tpc 换成**这一块自己的宿主**(read_tpc / read_<pid>);
                //     ② 只有「纯出售块」(这一块里除售价行只剩 blockquote)才整段重建 ——
                //        正文里夹一段出售的那种(实测 tid-2217905 首帖)**只换售价行 + 补上被解锁
                //        的那枚 blockquote**,绝不把帖子正文整段换掉(那会带走别的模块打过的标);
                //     ③ 站点有时把那枚 h6 套在 <b><span> 里(实测 tid-2217905,包装里只有它)
                //        ⇒ 连包装一起换,免得卡继承了 <b> 的粗体与上下文样式。
                //   ★ 认不出新结构(没有这个宿主、或里面没有 h6)就**一个节点都不动** ——
                //     宁可维持现状(卡是已购买、正文没出来),也绝不把页面掏空。
                function revealContent(html) {
                    let fresh = null;
                    try {
                        fresh = new DOMParser().parseFromString(html, 'text/html').getElementById(host.id);
                    } catch (e) { fresh = null; }
                    const freshH6 = fresh ? fresh.querySelector('h6') : null;
                    if (!fresh || !freshH6) return;
                    // 买家数以新页为准:这一单站点已经算进去了(比我们自己 +1 更准,
                    //   也顺带盖掉「买的同时别人也在买」那点误差)。
                    const fm = /已有\s*([\d,]+)\s*人购买/.exec(freshH6.textContent || '');
                    const fb = fm ? parseInt(fm[1].replace(/,/g, ''), 10) : NaN;
                    if (isFinite(fb) && fb > buyers) { buyers = fb; paintDone('done'); }

                    // 这一块里除售价行还有别的节点吗?(紧邻的 blockquote 不算)
                    const pure = Array.prototype.every.call(host.children, function (c) {
                        return c === h6 || c.tagName === 'BLOCKQUOTE';
                    });
                    if (!pure) {
                        // 正文里夹一段出售:卡留原地,只把「被解锁的正文」那枚 blockquote 补上。
                        let nb = freshH6.nextElementSibling;
                        while (nb && nb.tagName !== 'BLOCKQUOTE') nb = nb.nextElementSibling;
                        if (nb) {
                            let ob = h6.nextElementSibling;
                            while (ob && ob.tagName !== 'BLOCKQUOTE') ob = ob.nextElementSibling;
                            if (ob && ob.parentElement === host) ob.replaceWith(nb);
                            else host.insertBefore(nb, h6.nextSibling);
                        }
                        claimBody();
                        return;
                    }

                    // 把那枚 h6(可能套着 <b><span> 纯包装)换成我们这张卡:只往上拆「包装里除了
                    //   它没别的字」的那几层,包装里还有别的内容就不拆(免得把内容一起带走)。
                    let slot = freshH6;
                    for (let up = 0; up < 3; up++) {
                        const p = slot.parentElement;
                        if (!p || p === fresh) break;
                        if (!/^(b|span|font|i|u|em|strong)$/.test(p.tagName.toLowerCase())) break;
                        if ((p.textContent || '').replace(/\s+/g, '') !==
                            (slot.textContent || '').replace(/\s+/g, '')) break;
                        slot = p;
                    }
                    slot.replaceWith(h6);             // 我们这张卡搬进新结构的售价行位置
                    const kids = Array.prototype.slice.call(fresh.childNodes);
                    while (host.firstChild) host.removeChild(host.firstChild);
                    for (let i = 0; i < kids.length; i++) host.appendChild(kids[i]);
                    claimBody();                      // 换上来的正文框是新节点,重新认领一次
                }

                // ---- 警告条:按**内容**认领,不用相邻兄弟选择器 ----
                //   ★★ 2.53.40 落地当轮抓到的坑:已购买之后,紧随 h6 的那个 blockquote 不再是
                //     「若发现会员采用欺骗的方法获取财富…」警告条,而是**被解锁的帖子正文**
                //     (站点把「购买可见」的内容包在 blockquote.blockquote 里)。实测已购页
                //     h6.spp-buy + blockquote.blockquote 命中数 1,命中的正是正文块 ——
                //     纯 CSS 认邻居会把正文刷成小号褐字还挂个 ⚠。
                //   ⇒ 认领判据:文本里同时有「举报」与「封掉ID」(这句话是站点固定文案)。
                //     认不出就一个都不打标 ⇒ 那条 blockquote 保持站点原样。
                let sib = h6.nextElementSibling;
                for (let k = 0; k < 2 && sib; k++, sib = sib.nextElementSibling) {
                    if (sib.tagName !== 'BLOCKQUOTE') continue;
                    const wt = sib.textContent || '';
                    if (wt.indexOf('举报') >= 0 && wt.indexOf('封掉ID') >= 0) {
                        sib.classList.add('spp-buy__warn');
                        break;
                    }
                }

                // ---- 正文框:买完之后,被解锁的正文被站点包在一枚**兄弟** blockquote 里 ----
                //   ★ 2.53.41:只在「服务端认为已购买」时才认 —— 没买的时候这帖压根没有正文块。
                //   ★ 判据与警告条同源:跳过那条同时含「举报」「封掉ID」的警告条,取其后第一条
                //     非空 blockquote。认不出就一个都不打标 ⇒ CSS 一条都不匹配,保持站点原样。
                // ★ 2.53.43:这一段抽成函数,因为「就地换页」之后必须再跑一遍 ——
                //   换页换上来的那枚被解锁 blockquote 是**新节点**,身上没打过 .spp-buy__body,
                //   不重认的话它就没有那套同族圆角框的样式(只剩站点公告块的秃样子)。
                function claimBody() {
                    let fb = h6.nextElementSibling;
                    for (let k = 0; k < 3 && fb; k++, fb = fb.nextElementSibling) {
                        if (fb.tagName !== 'BLOCKQUOTE') continue;
                        const ft = (fb.textContent || '').replace(/\s+/g, '');
                        if (!ft) continue;
                        if (ft.indexOf('举报') >= 0 && ft.indexOf('封掉ID') >= 0) continue;
                        fb.classList.add('spp-buy__body');
                        return;
                    }
                }
                if (bought) claimBody();

                // ---- 已购买态:直接画完收工(无按钮、无请求、无自动动作)----
                if (bought) { paintDone('bought'); return; }

                btn.addEventListener('click', function () {
                    if (done || busy) return;
                    if (isFree) { doBuy(); return; }        // 免费帖:点一下就是买
                    cf.classList.add('is-on');              // 收费帖:先就地确认,绝不静默扣币
                });
                bar.querySelector('.spp-buy__yes').addEventListener('click', function () { doBuy(); });
                bar.querySelector('.spp-buy__no').addEventListener('click', function () {
                    cf.classList.remove('is-on');
                    btn.focus();
                });

                // ---- 免费帖自动购买(开关默认开)----
                //   ★ 只自动跑一次:失败就停下等你点(setFail 里那行「点我重试」),不重试、不循环。
                //   ★ 延后 600ms 起跑:让一体流(initReadFlow,排在本模块之前)先把楼层容器定下来。
                //   ★★ 2.53.46:一页有多个免费块时按序号各错开 400ms —— 不让同一瞬间并发几条
                //     购买请求,也让每一块的「复读本页」不互相抢(nth = 扫描里的下标)。
                if (isFree && SPP_CONFIG.bool('spp_autobuy_free', true)) {
                    // ★ 2.53.41:这帖一定会被买掉,没必要先闪一张带大按钮的卡再切走 ——
                    //   直接以最终形态的骨架起步,页面上就不会出现那一次 67 → 40 的跳动。
                    //   (手动那条路照旧:大卡 → 点按钮 → 买下后收成通行证条。)
                    usePass = true;
                    bar.className = 'spp-buy__bar is-pass is-busy';
                    bar.innerHTML = passHtml('busy');
                    setTimeout(function () { doBuy(); }, 600 + nth * 400);
                }
        }

        onDomReady(function () {
            // 提亮已购正文(开关 spp_buy_lift,默认开):只给根节点打一个标,CSS 只认这个标。
            //   ★ 与 SPP_PANEL_DEFS / buildOverlay 里那两处是同一个键,三处必须同值。
            if (SPP_CONFIG.bool('spp_buy_lift', true)) {
                document.documentElement.classList.add('spp-lift-on');
            }

            // ---- 找**所有**购买块:判据是**那句话**,不是那枚按钮 ----
            //   (已购买态服务端不渲染按钮,只留这行「此帖售价 N SP币,已有 M 人购买」)
            //   ★★ 2.53.46(用户:「自动购买免费帖子没生效 / 另外这个自动购买貌似还是有一些瑕疵」):
            //     旧版写死 document.getElementById('read_tpc'),只认**首帖**那一块。实测站点把
            //     「购买可见」渲染成 div.tpc_content > div.f14[id^="read_"] > [h6.quote.jumbotron, …]:
            //     首帖那个 div 的 id 恰好叫 read_tpc,**楼层自己出售内容**时叫 read_<pid>
            //     (tid-2717095 的 28 楼 = read_35357708;tid-2217905 的 1 / 2 / 末楼各一块,
            //     其中一块售价 999999999 SP币)。那些块旧版连卡都不建 ⇒ 自动购买自然不生效。
            //   ★ 幂等改成**逐块**判(已经建过卡的 h6 跳过),不再整页 return。
            const blocks = [];
            const allH6 = document.querySelectorAll('h6');
            for (let i = 0; i < allH6.length; i++) {
                const t = (allH6[i].textContent || '').replace(/\s+/g, ' ');
                if (!/售价\s*[\d,]+\s*SP币/.test(t)) continue;
                if (allH6[i].querySelector('.spp-buy__bar')) continue;
                let n = allH6[i].parentElement, hp = null;
                for (let up = 0; n && up < 12; up++, n = n.parentElement) {
                    if (n.id && n.id.indexOf('read_') === 0) { hp = n; break; }
                }
                if (!hp) continue;                 // 认不出宿主:一个节点都不动
                blocks.push({ h6: allH6[i], host: hp, txt: t });
            }
            for (let i = 0; i < blocks.length; i++) {
                buildBlock(blocks[i].h6, blocks[i].host, blocks[i].txt, i);
            }
        });
    }

    // ==================== 15a. 楼层头像按原始长宽比显示(仅 read.php,2.53.26)====================
    // 用户定案「方案 c 折中」:容器锁正方形(150×150,表格列宽与版面一律不动),
    //   图片按自己的比例塞进方框 —— 比例在容忍区间内就完整显示(contain,两侧留白),
    //   超出区间(细长的竖图 / 超宽横幅)才裁切填满(cover)。**任何时候都不拉伸**。
    // ★ 为什么必须 JS:object-fit 没法把「图自己的比例」写进条件 —— CSS 读不到 naturalWidth/Height。
    // ★ 站点头像带 loading="lazy":没进视口时 naturalWidth 是 0,所以先挂 load 监听、加载完再判。
    // ★ 图床挂掉(naturalWidth 0)时**摘掉裁切类**,退回 contain —— 不把一张空图裁成一块色。
    // ★ 容忍区间:高/宽 > 1.4(比 4:3 还细长)或 < 0.71(比 4:3 还扁)才算超限。
    function initAvatarRatio() {
        if (!WANTS.readOnly) return;
        // ★ 2.53.27:门槛按方框的真实上下限算 —— 内容宽 142、内容高 192 ⇒ 比 > 192/142 = 1.352
        //   才会被 max-height:200 顶住。1.35 就是这个点。
        //   横图(比 < 1)一律不裁:框高被 min-height:150 托住,contain 完整显示。
        var UP = 1.35;

        function judge(img) {
            var w = img.naturalWidth, h = img.naturalHeight;
            if (!w || !h) { img.classList.remove('spp-ava-crop'); return; }
            var r = h / w;
            img.classList.toggle('spp-ava-crop', r > UP);
        }
        function scan() {
            document.querySelectorAll('.t5.t2 .r_two .user-pic img:not(.spp-ava-off)').forEach(function (img) {
                if (!img.dataset.sppAvaRatio) {
                    img.dataset.sppAvaRatio = '1';
                    img.addEventListener('load', function () { judge(img); });
                    img.addEventListener('error', function () { img.classList.remove('spp-ava-crop'); });
                }
                if (img.complete) judge(img);
            });
        }
        onDomReady(function () { setTimeout(scan, 600); setTimeout(scan, 2600); });
        // 懒加载:滚动到哪量到哪(passive + rAF 节流,别把滚动拖慢)
        var tick = false;
        window.addEventListener('scroll', function () {
            if (tick) return;
            tick = true;
            requestAnimationFrame(function () { tick = false; scan(); });
        }, { passive: true });
    }

    // ==================== 15i. 楼层顶部行:收掉「小中大」与「顶端」+ 面板里的正文字号(仅 read.php,2.53.47)====================
    // 用户原话(计划目标功能.md 功能2):
    //   「楼层帖子顶部有设置字体大小的按钮:小、中、大;能否把这个移动到悬浮按钮的设置面板里面,
    //     并且屏蔽楼层帖子顶部的按钮?」「能否屏蔽楼层顶部 UI 中的顶端按钮?」
    //
    // ★ 站点实现只有一行(线上实测读到的原文):
    //       function fontsize(text,id){ getObj("read_"+id).className = text; }
    //   三枚 a 的 onclick 分别是 fontsize('small'|'middle'|'big', '<pid>'),
    //   落点是那个 id="read_<pid>" 的正文 div —— **整个 className 被赋值,不是追加**。
    //   站点配套 CSS 也只有三条:.small{12px} / .middle{14px} / .big{18px}。
    //
    // ★ 所以「搬到面板里」这件事没有技术含量:面板给一个 12/14/18 的数值,
    //   我们对页面上所有 div[id^="read_"] 做同一件事。两个必须知道的后果:
    //   ① **全局生效,不再是单楼生效**。站点那三枚是「只改这一楼」,面板只有一枚开关,
    //      语义上必然是全局 —— 用户要的就是「挪到面板里」,所以这是需求本身,不是妥协。
    //   ② **会抹掉正文上别的 class**(`className = text` 是整段覆盖)。站点原本的
    //      id="read_tpc" 初始 class 是 `.f14`(14px),被覆盖后就没有 f14 了 —— 这正是站点
    //      自己那三枚的行为,我们照抄。但为了不去踩**别人**加的类,这里先只清掉
    //      与字号同族的三枚(small/middle/big),其余 class 原样留着。
    //
    // ★ 为什么要动 DOM 而不是纯 CSS:那三枚 a **既无 class 也无 id**,唯一特征就是 onclick 里的
    //   `fontsize(`。CSS 能靠 `[onclick^="fontsize("]` 把它们藏掉(本模块的样式段里就有这条兜底),
    //   但会把 a 留成零宽空元素,它所在的 span 与两旁的 .spp-tp-sep 分隔符都得跟着收 ——
    //   靠 CSS 一条条 :has() 串起来很脆,JS 一次遍历能连「空 span + 多出来的分隔符」一起摘干净。
    function initFloorTopBar() {
        if (!WANTS.readOnly) return;

        GM_addStyle(`
/* ===== SPP-FLOORTOP-BEGIN =====
   1) 正文字号:面板里那枚「正文文字大小」直接复用站点自己的三档类
      (.small/.middle/.big 是站点 CSS,我们一个字都不用重写)。 */
html.spp-read-on div[id^="read_"].spp-fs-small  { font-size: 12px !important; }
html.spp-read-on div[id^="read_"].spp-fs-middle { font-size: 14px !important; }
html.spp-read-on div[id^="read_"].spp-fs-big    { font-size: 18px !important; }
/* ===== SPP-FLOORTOP-END ===== */
        `);

        /* ---- 1. 正文文字大小(0 = 不接管,站点原样)---- */
        const FONT_KEY = 'spp_font_size';
        const FONT_DEF = 14, FONT_MIN = 12, FONT_MAX = 18;
        const FS = [['spp-fs-small', 12], ['spp-fs-middle', 14], ['spp-fs-big', 18]];
        const CLASSES = FS.map(p => p[0]);

        function applyFontSize() {
            const v = SPP_CONFIG.num(FONT_KEY, FONT_DEF, FONT_MIN, FONT_MAX);
            const pick = FS.reduce((best, p) => Math.abs(p[1] - v) < Math.abs(best[1] - v) ? p : best, FS[1]);
            //   站点自己的等价类:面板选了 12/14/18 时顺手把站点那枚类名也写上,
            //   这样「关掉脚本」或「站点自己又调用一次 fontsize」时状态不会莫名跳档。
            const SITE = { 12: 'small', 14: 'middle', 18: 'big' };
            const siteCls = SITE[pick[1]];
            const nodes = document.querySelectorAll('div[id^="read_"]');
            for (let i = 0; i < nodes.length; i++) {
                const el = nodes[i];
                if (el.classList.contains(pick[0]) && (!siteCls || el.classList.contains(siteCls))) continue;
                CLASSES.forEach(c => el.classList.remove(c));
                el.classList.add(pick[0]);
                if (siteCls) el.classList.add(siteCls);
            }
        }

        /* ---- 2. 顶部行:收掉「小 中 大」三枚 + 「顶端」那枚 ----
           ★ 靶子按「种类」认,不写死条目数(站点可能给有的楼层多/少一两项):
             a) onclick 以 fontsize( 开头 → 字号三枚,摘
             b) a[title="顶端"]          → 顶端,摘(CSS 里也有一条兜底,两步都做是为了
                                              「JS 没跑到」与「跑到了」两种情况结果一致)
           ★ 摘完必须清理托底:那三枚原本包在一个裸 <span> 里、两侧各有 .spp-tp-sep 分隔符。
             只摘 <a> 会留下「空 span」与「一串竖线」,比不摘更难看 —— 本仓 2.53.24
             处理「更多」按钮时踩过同一类坑。 */
        function pruneTools() {
            const tops = document.querySelectorAll('.tiptop');
            for (let t = 0; t < tops.length; t++) {
                const bar = tops[t];
                // a) 字号三枚
                const fbtns = bar.querySelectorAll('a[onclick^="fontsize("]');
                for (let i = 0; i < fbtns.length; i++) {
                    const a = fbtns[i].parentElement;
                    fbtns[i].remove();
                    // 空 span(只装过这几枚 a 的那种)一起收:判据 = 没文本、没控件、没图
                    if (a && a !== bar && a.tagName === 'SPAN' &&
                        !(a.textContent || '').trim() && !a.querySelector('input, a, img, select, button')) {
                        a.remove();
                    }
                }
                // b) 顶端
                const topBtn = bar.querySelectorAll('a[title="顶端"]');
                for (let i = 0; i < topBtn.length; i++) {
                    const a = topBtn[i].parentElement;
                    topBtn[i].remove();
                    if (a && a !== bar && a.tagName === 'SPAN' &&
                        !(a.textContent || '').trim() && !a.querySelector('input, a, img, select, button')) {
                        a.remove();
                    }
                }
                // c) 收尾:清掉「首尾多余 / 连续重复」的分隔符。只在摘过东西的楼层做。
                const seps = bar.querySelectorAll('.spp-tp-sep');
                for (let i = seps.length - 1; i >= 0; i--) {
                    const s = seps[i];
                    const prev = s.previousElementSibling;
                    // 连续两枚分隔符 ⇒ 去掉后一枚
                    if (prev && prev.classList && prev.classList.contains('spp-tp-sep')) { s.remove(); continue; }
                    // 紧跟在第一项之前 ⇒ 去掉
                    if (!prev) s.remove();
                }
            }
        }

        onDomReady(() => { applyFontSize(); pruneTools(); });
        // ★ 延后一拍:一体流(15b,排在本模块之前注册)会把楼层容器重排、且它自己的
        //   observer 还在跑;等它静下来再扫,避免我们在它重写内联样式的那一帧里操作 DOM。
        setTimeout(() => { applyFontSize(); pruneTools(); }, 900);
        // ★ 只监听 childList:楼层被展开 / 动态插入时补扫(applyFontSize 与 pruneTools 都幂等,
        //   命中过的元素会因 `continue` 或「找不到靶子」直接跳过,不会反复写)。
        let tick = false;
        const target = document.getElementById('main') || document.body;
        new MutationObserver(() => {
            if (tick) return;
            tick = true;
            setTimeout(() => { tick = false; applyFontSize(); pruneTools(); }, 400);
        }).observe(target, { childList: true, subtree: true });
    }

    // ==================== 15b. 一体流:帖子区合并成一张白卡(仅 read.php) ====================
    // ★ 为什么用 JS 打类而不是纯 CSS:form 里夹着 <a> 锚点与隐藏 .menu,:first-child / :last-of-type
    //   都不可靠(探针实测 form 有 21 个子节点);「末行不要细线」也必须精确命中最后一个 .t5.t2。
    // ★ 为什么结构性属性要写成内联 !important:星空主题
    //   「html.spp-hm-star #main .t5:not(.spp-t5bar){box-shadow:…!important}」(文件 10256 段)会与我们的
    //   规则抢同一批属性,实测同一份 CSS 前后两次量到不同结果 ⇒ 用内联逃生口钉死。
    // ★ 不用 overflow:hidden 裁圆角:站点头像弹窗 span#sf_N 就挂在楼层内部,会被切掉。
    function initReadFlow() {
        if (!WANTS.readOnly) return;
        // ★★ 2.55.25:四处颜色改成「变量 + 原字面量兜底」——这是「帖子区黑玻璃」开关能成立的关键一步。
        //   为什么非改不可:下面这些是 setProperty 写的**内联 !important**,而 CSS 规范里
        //   内联 !important 的优先级高于任何样式表 !important ⇒ 想用 CSS 把帖子区刷黑,
        //   必须先让这里不再写死 #fff。改成 var(--x, 原值) 之后:开关关着时变量没定义,
        //   解析出来就是原来的 #fff / #eaeff7 / #f1f5fa / none,**关态行为逐字节不变**;
        //   开关打开时由模块 19 在 form.spp-flow 上给这四个变量赋值(令牌只有一把尺子)。
        const EDGE = '1px solid var(--spp-flow-edge, #eaeff7)', HAIR = '1px solid var(--spp-flow-hair, #f1f5fa)';

        function tag() {
            const rows = document.querySelectorAll('#main > form > .t5.t2');
            if (!rows.length) return;
            rows[0].parentElement.classList.add('spp-flow');
            for (let i = 0; i < rows.length; i++) {
                const r = rows[i], first = i === 0, last = i === rows.length - 1;
                r.classList.add('spp-flow-row');
                r.classList.toggle('spp-flow-start', first);
                r.classList.toggle('spp-flow-end', last);
                r.style.setProperty('margin', '0', 'important');
                r.style.setProperty('position', 'relative', 'important');
                r.style.setProperty('box-shadow', 'none', 'important');
                r.style.setProperty('transform', 'none', 'important');
                r.style.setProperty('border-top', first ? EDGE : '0', 'important');
                r.style.setProperty('border-bottom', last ? EDGE : HAIR, 'important');
                r.style.setProperty('border-left', EDGE, 'important');
                r.style.setProperty('border-right', EDGE, 'important');
                r.style.setProperty('border-radius', first ? '17px 17px 0 0' : (last ? '0 0 17px 17px' : '0'), 'important');
                r.style.setProperty('background-color', 'var(--spp-flow-bg, #fff)', 'important');
                r.style.setProperty('background-image', 'var(--spp-flow-bgi, none)', 'important');
            }
        }

        onDomReady(() => { tag(); setTimeout(tag, 600); });
        // 只监听 childList(不监听 attributes):避免 2.49.0 那种「自己改类→自己触发」的自激
        onDomReady(() => {
            const target = document.getElementById('main') || document.body;
            new MutationObserver(() => tag()).observe(target, { childList: true, subtree: true });
        });
    }

    // ==================== 15h. 悬停高亮开关(默认关闭,2.53.45)====================
    // ★ 需求(用户 2.53.45):「鼠标停在某一楼层时候会高亮…这个功能能在设置面板那里
    //   设置为关闭吗?(默认关闭)」—— 于是这条高亮从「写死在星空皮肤里」变成一枚可勾选
    //   开关,并且默认就是关的(登记表里 def: false 就是那三个字)。
    // ★ 定位方式是实测,不是猜(真鼠标 hover + getComputedStyle,探针 tmp/r1/srv/hover-src.js):
    //   · 上色那条是**我们自己的**,来自星空玻璃卡那段 CSS:
    //       html.spp-hm-star #main .t5 tr:hover > th / .t5:not(.spp-t5bar) tr:hover > td
    //         { background:rgba(47,111,104,.07) !important }
    //     它跟着 spp_home_star_glass 一起注入,类 spp-hm-star 也是那时才挂到 <html> 上。
    //   · 同一帧读数:被悬停那一楼 th.r_one 由 rgba(0,0,0,0) → rgba(47,111,104,0.07),
    //     其余 30 楼全部保持纯白 ⇒ 用户截图里那层灰就是这条 7% 青灰。
    //     (作者栏 th.r_two 一直不受影响:本文件里 .t5.t2 th.r_two{background:#fff!important}
    //      特异性更高,把它压在白色 —— 上色的只有「楼层头那一行」与正文格。)
    //   · 站点自己没有悬停底色:把那一页所有 :hover 规则逐条与元素对了一遍,其中带
    //     background 的只有我们注入的 <style>(来源逐条可查,不是猜)。
    //   · 2.52.48 一体流还给整行留了一圈 ::after 内描(inset 0 0 0 2px rgba(32,201,151,.15));
    //     它同属「悬停高亮」,一并归这枚开关管(行本体 position:relative,圈画在行内,正常)。
    // ★ 关掉的做法:给 <html> 打 spp-fh-off,用一条**多带一枚 class** 的规则以 !important 压回去。
    //   为什么不做成「在星空那段模板里条件输出 CSS」:那是一整块模板字符串,插 ${} 要动
    //   check-css-tpl / 反引号两道守卫盯着的形态;独立样式块风险更低、也删得干净。
    // ★ 为什么 .spp-fh-off 在每条选择器里写两遍(不是笔误):要压的两条算下来是 (1,3,3) 与
    //   (1,4,3)(`.t5:not(.spp-t5bar)` 里 :not 的实参也算一个 class),两条都带 !important 时
    //   比的是特异性、**不比样式表先后** —— 重复一枚 class 把本方顶到 (1,5,3) 稳赢,
    //   也就不依赖我们的 <style> 恰好排在 #spp-starfield-style 之后。
    const CSS_FLOOR_HOVER = `
/* 悬停高亮关闭态(开关 spp_floor_hover,默认关):把星空那条 7% 青灰底刷回透明 ——
   transparent 正是这些格的默认态(实测默认值 rgba(0,0,0,0)),所以关掉=回到站点原样。 */
html.spp-fh-off.spp-fh-off #main .t tr:hover,
html.spp-fh-off.spp-fh-off #main .t tr:hover > td,
html.spp-fh-off.spp-fh-off #main .t tr:hover > th,
html.spp-fh-off.spp-fh-off #main .t5 tr:hover > td,
html.spp-fh-off.spp-fh-off #main .t5 tr:hover > th,
html.spp-fh-off.spp-fh-off #main .t5:not(.spp-t5bar) tr:hover > td {
    background:transparent !important;
    background-image:none !important;
}
/* 一体流那圈悬停内描(2.52.48)同属「悬停高亮」,一起关 */
html.spp-fh-off.spp-fh-off #main > form.spp-flow > .t5.t2.spp-flow-row:hover::after {
    content:none !important;
}
`;
    function initFloorHoverSwitch() {
        // 样式无条件注入:开 / 关只差 <html> 上那枚标 —— 将来若要做成「面板里即时生效」,
        // 只需增删这枚 class,不必再管样式块在不在。
        if (!document.getElementById('spp-fh-style')) {
            const st = document.createElement('style');
            st.id = 'spp-fh-style';
            st.textContent = CSS_FLOOR_HOVER;
            appendToHeadSafe(st);
        }
        if (SPP_CONFIG.bool('spp_floor_hover', false)) return;   // 开着 = 星空 / 站点原样,一个字都不写
        document.documentElement.classList.add('spp-fh-off');
    }

    // ==================== 15d. 自动展开隐藏楼层(仅 read.php) ====================
    // ★ 需求(2.53.0,用户 2.53.0 定案):默认开启,可在 ⚙ 脚本设置 ▸ 功能开关 里关掉。
    // ★ 站点机制:被折叠的楼层是服务端直出的「半折叠」形态 —— 本体带 .js-hidepuremark 与
    //   内联 style="display:none"(实测命中 th.r_two > div.user-pic、div.tpc_content、
    //   tr.tr1.r_one 评分行),可见的只有占位 .js-puremark-content(「mark」两个字)和它旁边
    //   那枚 a[onclick="return expandpuremark(this)"](「▼展开」)。
    //   站点自己的 expandpuremark() 就是做三件事:本体 show()、占位 hide()、按钮 hide()。
    // ★ 纯 DOM 复刻这三步而不调站点函数:本脚本带 @grant ⇒ 沙箱(见文件顶 2.52.102 那段
    //   真凶),跨 realm 调页面里的 expandpuremark 要绕 unsafeWindow;而这三步只是三个
    //   display 开关,自己写更稳,也不依赖站点的 jQuery。
    //   ★ 站点那两句(.tiptop 的 border-bottom / margin 内联样式)不复刻:本脚本 READ_CSS
    //     对 .tiptop 有 !important 规则,站点这两句本来就会被压掉。
    // ★ 幂等:判据是「本体那块 computed display 还是 none」(即站点仍是折叠态),
    //   展开过的楼层下次扫描直接跳过,不会反复写内联样式。
    // ★ 只监听 childList、不监听 attributes:我们自己的动作就是在写内联 style,
    //   监听 attributes 会自激(同 2.49.0 那个坑,见 initReadFlow 的注释)。
    function initAutoExpandHidden() {
        if (!WANTS.readOnly) return;
        const KEY = 'spp_auto_expand_hidden';
        if (!SPP_CONFIG.bool(KEY, true)) return;      // 默认开;⚙ 脚本设置 ▸ 功能开关 可关

        function expandPost(post) {
            const bodies = post.querySelectorAll('.js-hidepuremark');
            if (!bodies.length) return;
            let folded = false;
            for (let i = 0; i < bodies.length; i++) {
                if (getComputedStyle(bodies[i]).display === 'none') { folded = true; break; }
            }
            if (!folded) return;                       // 已经是展开态(或站点没折叠这一层)
            bodies.forEach(el => el.style.removeProperty('display'));   // ① 还原本体
            post.querySelectorAll('.js-puremark-content')
                .forEach(el => el.style.setProperty('display', 'none'));  // ② 收起「mark」占位
            post.querySelectorAll('a[onclick*="expandpuremark"]').forEach(a => {
                const box = a.parentElement;            // 站点把按钮单独包在一层 div.fl.bianji 里
                if (box) box.style.setProperty('display', 'none');        // ③ 收起「▼展开」
            });
            post.dataset.sppPuremarkOpen = '1';
            // ★★ 2.53.4 修复②:展开是**纯 attributes 变化**(removeProperty('display')),
            //   15 号模块的 observer 只听 childList ⇒ 它永远收不到这一趟。
            //   而 15 号模块给块打的 data-spp-fit 是「量过就跳过」的幂等标记 —— 折叠时量到的是
            //   0×0,fitTile 不计标记(见它的 box.width<8 早退)。两件事叠起来的结果是:
            //   这一层展开后,块里字号永久停在 CSS 兜底值,被 overflow:hidden 裁掉。
            //   所以展开成功后必须主动喊一声,让 15 号模块把「还没量」的块补量一遍。
            if (typeof sppAvaRefit === 'function') sppAvaRefit();
        }

        function sweep() {
            const posts = document.querySelectorAll('table.js-post');
            for (let i = 0; i < posts.length; i++) expandPost(posts[i]);
        }

        onDomReady(() => { sweep(); setTimeout(sweep, 600); });
        onDomReady(() => {
            const target = document.getElementById('main') || document.body;
            new MutationObserver(() => sweep()).observe(target, { childList: true, subtree: true });
        });
    }

    // ==================== 15e. 悬停头像弹层 → 白底状态卡(仅 read.php)====================
    // ★ 需求(用户 2.53.1):「鼠标悬停用户头像时,会弹出一个 UI 状态栏,请美化该 UI,适配为白色背景」。
    // ★ 站点结构(真页面实测):span.user-info#showface_<pid> > .user-infoWrap,里面依次是
    //   span.co(灰箭头)、div.listread(4 枚 16×16 精灵图标 + 内联 onclick)、div.c(120×5 占位)、
    //   以及 9 行**裸文本 + <br>**(「UID: <span class="f12">917911</span><br>精华: …」)。
    // ★ 为什么要动 DOM 而不是纯 CSS:那 9 行的「标签」是裸文本节点,CSS 选不中文本节点 ⇒
    //   标签没法单独上色、也没法做成左右两列的 key/value 行。这里把占位之后的所有节点按 <br>
    //   切行、解析成「标签 / 值」,再重排成 .spp-uc__rows(外观全部交给 READ_CSS 的 2.53.1 段)。
    //   ★ 值的文本原样搬进 .spp-uc__v(不丢字段、不改字),站点那几枚带色的 span 不保留 ——
    //     配色改由我们的 CSS 按字段名给(SP币 / 发帖 两个语义强调,其余走中性色)。
    // ★ 图标行、跳转 <a> 与内联 onclick 一律不动:那是站点的「作者资料 / 短消息 / 加好友 / 道具」
    //   四个入口,搬动它们等于把功能搞坏。
    // ★ 幂等:打 dataset.sppUcDone 标记,重复扫描不再重排(重排一次就够,站点不会重建这层)。
    function initUserStatusCard() {
        if (!WANTS.readOnly) return;
        const ACCENT = { 'SP币': 'is-sp', '发帖': 'is-post' };

        // ==================== 2.53.3:图标行 B 方案(用户拍板「图标 + 常驻短标签」) ====================
        // ★ 为什么必须换掉站点那 4 枚图标(真页面实测,read.php?tid-2948255 / 2942976):
        //   它们是 2008 年的 16×16 位图 sprite —— images/colorImagination/read/readicon.png,
        //   一整张 **16×180 / 1318 字节**(PNG-8 调色板,9 个槽位只用了 4 个),四个槽位四套互不相干的
        //   配色:readprofile 0 -44px 橙框文档 / readmessage 0 -64px 橙色喇叭 / readonline 0 -4px 绿人像+加号
        //   / readmagic 0 -84px 品红螺旋。放大 4 倍就是锯齿方块(用户原话「有一种古早 BBC 论坛的网页感」)。
        // ★ 更要命的是**它们没有文字**:站点用「text-indent:-24000px」把「作者资料 / 发送短消息 / 加为好友 /
        //   使用道具」四个词踢到屏幕外,只留 li[title] 原生提示 ⇒ 用户只能逐个悬停去猜。
        //   本站其它地方(.spp-uc__rows、设置面板)已经是「浅灰凹槽 + 圆角 + 中性色」这套语言,图标行
        //   却还是贴纸风,落差全在这 16×16 里。
        // ★ 改法(B 方案,真浏览器实测:卡片 198×242 → 198×267,图标条 31 → 56):
        //   ① 站点位图 div 只**加类隐藏**、不摘除 —— 站点脚本对它的引用关系保持原样;
        //   ② 注入同一套 1.6px 线性 SVG(16×16 / currentColor)+ 常驻短标签;
        //   ③ <a> 的 href / onclick 一律不碰(那是「作者资料 / 短消息 / 加好友 / 道具」四个真入口)。
        // ★ 数量**会变**:茶馆 tid-2948255 实测只有 3 枚(没有「使用道具」),所以按 class 名映射而不是按序号;
        //   认不出的条目用通用图标 + li[title] 前 5 字兜底,绝不写死 4 格。
        // ★ 短标签为什么缩成 2-3 字:卡片 min-width 198px,四格均分后每格约 43px,
        //   10.5px 字号下「发送短消息」5 字约 53px 会撑破格子(实测换行)⇒ 资料 / 短消息 / 加好友 / 道具。
        const ICON_MAP = {
            readprofile: { k: 'profile', t: '资料' },
            readmessage: { k: 'msg', t: '短消息' },
            readonline: { k: 'friend', t: '加好友' },
            readmagic: { k: 'magic', t: '道具' },
        };
        const ICON_SVG = {
            profile: '<rect x="2.1" y="2.6" width="11.8" height="10.8" rx="2.4"/><circle cx="6.4" cy="6.9" r="1.7"/><path d="M3.7 11.5c.4-1.4 1.5-2.1 2.7-2.1s2.3.7 2.7 2.1"/><path d="M11 6.7h1.4M11 9.3h1.4"/>',
            msg: '<path d="M13.4 8.7a1.7 1.7 0 0 1-1.7 1.7H6.5L3.2 13.1V4.7A1.7 1.7 0 0 1 4.9 3h6.8a1.7 1.7 0 0 1 1.7 1.7z"/><path d="M6.2 6h4M6.2 8.2h2.5"/>',
            friend: '<circle cx="6.5" cy="5.4" r="2.3"/><path d="M2.6 13.3c.5-2.1 2.1-3.3 3.9-3.3.9 0 1.8.3 2.5.8"/><path d="M12.3 9.4v4M10.3 11.4h4"/>',
            magic: '<path d="M3.3 12.7 9.4 6.6"/><path d="M11.5 2.4l.7 1.8 1.8.7-1.8.7-.7 1.8-.7-1.8L8.9 4.9l1.8-.7z"/><path d="M5.3 3.4l.45 1.1 1.1.45-1.1.45-.45 1.1-.45-1.1-1.1-.45 1.1-.45z"/>',
            dot: '<circle cx="8" cy="8" r="2.4"/><path d="M8 2.6v1.6M8 11.8v1.6M2.6 8h1.6M11.8 8h1.6"/>',
        };

        // 站点图标行 → SVG + 常驻标签。幂等:li 打 dataset.sppUcIco。
        function decorateIcons(wrap) {
            if (!wrap) return;
            wrap.querySelectorAll('.listread li').forEach(li => {
                if (li.dataset.sppUcIco) return;
                const a = li.querySelector('a');
                const d = li.querySelector('div');
                if (!a || !d) return;
                let key = '';
                for (const name in ICON_MAP) { if (d.classList.contains(name)) { key = name; break; } }
                const full = (li.getAttribute('title') || d.textContent || '').trim();
                const meta = ICON_MAP[key] || { k: 'dot', t: full.slice(0, 5) || '操作' };
                d.classList.add('spp-uc-old');                 // 只藏,不摘
                const box = document.createElement('span');
                box.className = 'spp-uc__ico';
                box.innerHTML = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + ICON_SVG[meta.k] + '</svg>';
                const lbl = document.createElement('span');
                lbl.className = 'spp-uc__lbl';
                lbl.textContent = meta.t;
                a.appendChild(box);
                a.appendChild(lbl);
                if (!a.getAttribute('aria-label')) a.setAttribute('aria-label', full || meta.t);
                li.dataset.sppUcIco = '1';
            });
        }

        function build(card) {
            if (card.dataset.sppUcDone) return;
            const wrap = card.querySelector('.user-infoWrap');
            if (!wrap) return;
            const spacer = wrap.querySelector(':scope > div.c');   // 站点那个 120×5 的占位边界
            const nodes = [];
            let started = !spacer;
            for (let i = 0; i < wrap.childNodes.length; i++) {
                const n = wrap.childNodes[i];
                if (n === spacer) { started = true; continue; }
                if (started) nodes.push(n);
            }
            if (!nodes.length) return;
            const lines = [[]];
            nodes.forEach(n => {
                if (n.nodeType === 1 && n.tagName === 'BR') { lines.push([]); return; }
                lines[lines.length - 1].push(n);
            });
            const rows = [];
            lines.forEach(line => {
                const text = line.map(n => n.textContent || '').join('');
                const m = text.match(/^\s*([^::]{1,8})[::]\s*([\s\S]*?)\s*$/);
                if (!m) return;
                rows.push({ k: m[1], v: m[2] || '—' });
            });
            if (!rows.length) return;
            const box = document.createElement('div');
            box.className = 'spp-uc__rows';
            rows.forEach(r => {
                const row = document.createElement('div');
                row.className = 'spp-uc__row' + (ACCENT[r.k] ? ' ' + ACCENT[r.k] : '');
                const k = document.createElement('span');
                k.className = 'spp-uc__k';
                k.textContent = r.k;
                const v = document.createElement('span');
                v.className = 'spp-uc__v';
                v.textContent = r.v;
                row.appendChild(k);
                row.appendChild(v);
                box.appendChild(row);
            });
            nodes.forEach(n => { if (n.parentNode === wrap) wrap.removeChild(n); });
            wrap.appendChild(box);
            decorateIcons(wrap);
            card.classList.add('spp-ucard');
            card.dataset.sppUcDone = '1';
        }

        function sweep() {
            document.querySelectorAll('table.js-post span.user-info[id^="showface_"]').forEach(build);
        }

        onDomReady(() => { sweep(); setTimeout(sweep, 700); });
    }

    // ==================== 15c. 用户信息深色卡(首页与版块页共用一份) ====================
    // ★ 为什么提到模块级:2.52.0 起版块页顶部也要用这套卡片(用户要求左右结构与主页一致)。
    //   原先 CSS_UPANEL / esc / polishUserInfo 全锁在 initHomePolish 的闭包里,
    //   而那个函数第一行就 return 掉了非首页 —— 版块页根本取不到。
    //   与 2.50.7 把 getReplyColor 提层是同一类问题(闭包作用域错位)。
    // ★ 本段同时含【用户卡】与【主页统计格 .spp-hm-stat】—— 版块页右框的热门版块
    //   就是复用后者,与主页「今日/昨日/最高日…」那 6 张卡是同一条规则。
    // ==================== 2.52.54:搜索页版块分类弹层(CSS) ====================
    // ★★ 为什么是模块级常量、而不是随手一个 GM_addStyle:
    //   2.52.54 把它挂在 SPP-HOTBOARD 那个 GM_addStyle 后面,而那个大盘子挂在
    //   initThreadListConvert() 内(首行 if (!isThreadListPage) return;)—— 搜索页压根没注入,
    //   结果弹层 DOM 建出来了却没有一行样式(用户截图里成了页面左侧一条没样式的窄条)。
    //   与 2.52.18「个人中心页 CSS 被写进版块页那份 GM_addStyle」是同一个坑,修法也一样:
    //   提成模块级常量,由需要它的模块自己注入(见 initSearchForumPicker 里的 injectCss)。
    const CSS_FPK = `
/* ===== SPP-FPK-BEGIN ===== */
/* 2.52.54 搜索页版块分类弹层(Search Forum PicKer)
   ★ 触发按钮:套站点那个 select 的皮(白底 / 1px #d8dee7 / 38.7px 高 / 深字),
     在原地替身,select 本体只 display:none 保留当表单值载体。 */
.spp-fpk__trig {
    position:relative; box-sizing:border-box; height:38.7px !important; min-height:0 !important; min-width:120px;
    padding:0 34px 0 12px !important; text-align:left; cursor:pointer;
    font-family:inherit; font-size:14px !important; line-height:1.2 !important; font-weight:400 !important; color:#1f2937 !important; -webkit-text-fill-color:#1f2937 !important;
    background-color:#fff; background-image:none;
    border:1px solid #d8dee7; border-radius:8px;
    transition:border-color .18s ease, color .18s ease, box-shadow .18s ease;
}
.spp-fpk__trig:hover { border-color:#20c997; color:#15795f; box-shadow:0 0 0 3px rgba(32,201,151,.12); }
.spp-fpk__trig::after {
    content:""; position:absolute; right:13px; top:50%; margin-top:-3px;
    width:0; height:0; border-left:5px solid transparent; border-right:5px solid transparent;
    border-top:6px solid #6b7280;
}
.spp-fpk__trig-t { display:block; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }

/* 弹层:一块矩形大卡,fixed 居中;一屏放得下就完全不滚动(超出才兜底 overflow:auto) */
.spp-fpk {
    position:fixed; left:50%; transform:translateX(-50%); top:44px; z-index:3400;
    box-sizing:border-box; width:min(1560px, calc(100vw - 32px));
    max-height:calc(100vh - 86px); overflow:auto;
    padding:14px 16px 16px; border-radius:16px;
    background-color:#0b1119;
    background-image:linear-gradient(152deg, rgba(255,255,255,.055) 0%, rgba(0,0,0,0) 62%);
    border:1px solid rgba(255,255,255,.10);
    box-shadow:0 26px 60px rgba(0,0,0,.62);
    color:#dbe6f7; font-size:13px;
    font-family:'Noto Sans SC','Source Han Sans SC','PingFang SC','Microsoft YaHei',sans-serif;
}
.spp-fpk[hidden] { display:none !important; }
.spp-fpk__hd { display:flex; align-items:center; gap:10px; margin:0 0 12px; }
.spp-fpk__t {
    font-size:14.5px; font-weight:700; letter-spacing:.3px;
    color:#eef3fb !important; -webkit-text-fill-color:#eef3fb !important;
}
.spp-fpk__cur {
    margin-left:auto; font-size:12px; color:#7de8b0 !important; -webkit-text-fill-color:#7de8b0 !important;
}
.spp-fpk__x {
    flex:none !important; width:26px !important; height:26px !important; min-height:0 !important; line-height:1 !important; padding:0 !important; cursor:pointer; border-radius:8px; box-shadow:none !important;
    font-size:16px; color:#a9b6c7 !important; -webkit-text-fill-color:#a9b6c7 !important;
    background-color:rgba(255,255,255,.05); background-image:none;
    border:1px solid rgba(255,255,255,.10);
}
.spp-fpk__x:hover { color:#fff !important; -webkit-text-fill-color:#fff !important; background-color:rgba(255,255,255,.12); }

/* 大板块分组 */
.spp-fpk__grp { margin:0 0 12px; }
.spp-fpk__grp:last-child { margin-bottom:0; }
.spp-fpk__grp-t {
    display:inline-flex !important; align-items:center !important; gap:8px !important; margin:0 0 8px !important; padding:0 !important; height:auto !important; min-height:0 !important; box-shadow:none !important;
    cursor:pointer; background:none; border:0;
    font-size:13.5px !important; line-height:20px !important; font-weight:700 !important; letter-spacing:.2px;
    color:#3ef58c !important; -webkit-text-fill-color:#3ef58c !important;
}
.spp-fpk__grp-t:disabled { cursor:default; color:#7de8b0 !important; -webkit-text-fill-color:#7de8b0 !important; }
.spp-fpk__grp-t:hover:not(:disabled) { color:#9dffc9 !important; -webkit-text-fill-color:#9dffc9 !important; }
.spp-fpk__grp-n {
    font-size:10.5px; font-weight:700; line-height:1.6; padding:0 7px; border-radius:999px;
    color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
    background-color:rgba(32,201,151,.16); border:1px solid rgba(32,201,151,.45);
}
.spp-fpk__subs { display:flex; flex-wrap:wrap; gap:6px; }
.spp-fpk__subs--cards { display:grid; grid-template-columns:repeat(auto-fill, minmax(140px, 1fr)); gap:6px; }

/* 届次小卡(只有大板块自己才用) */
.spp-fpk__mini {
    box-sizing:border-box; min-width:0; padding:5px 6px 6px; border-radius:9px;
    background-color:rgba(255,255,255,.035); border:1px solid rgba(255,255,255,.075);
}
.spp-fpk__mini-t {
    display:block !important; margin:0 0 3px !important; padding:0 !important;
    height:auto !important; min-height:0 !important;
    cursor:pointer; background:none !important; border:0 !important; box-shadow:none !important;
    font-size:11px !important; line-height:14px !important; font-weight:700 !important;
    text-align:left; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
    color:#cfdcee !important; -webkit-text-fill-color:#cfdcee !important;
}
.spp-fpk__mini-t:hover { color:#9dffc9 !important; -webkit-text-fill-color:#9dffc9 !important; }
.spp-fpk__mini-t.is-on { color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important; }
.spp-fpk__mini-b { display:block; line-height:0; font-size:0; }
/* ★★ 这三行是踩出来的:首版只给颜色加 !important,结果每个链接被站点/主题的 button 规则
   撑成 32px 高(实测 itemH=32 ⇒ 小卡 109px、弹层 1312px 要滚动)。
   现在度量类属性(padding / height / min-height / line-height / font-size / box-shadow)
   一律 !important —— 与「不常驻 transition」是同一类教训。 */
.spp-fpk__mini-b .spp-fpk__lnk {
    display:inline-block !important; padding:0 !important; margin:0 !important;
    width:auto !important; height:auto !important; min-height:0 !important;
    cursor:pointer; background:none !important; border:0 !important; border-radius:0 !important;
    box-shadow:none !important; transform:none !important;
    font-size:10.5px !important; line-height:14px !important;
    vertical-align:top; white-space:nowrap;
    color:#c8d6e6 !important; -webkit-text-fill-color:#c8d6e6 !important;
}
.spp-fpk__mini-b .spp-fpk__lnk + .spp-fpk__lnk::before {
    content:"·"; margin:0 3px; color:rgba(255,255,255,.24) !important; -webkit-text-fill-color:rgba(255,255,255,.24) !important;
}
.spp-fpk__mini-b .spp-fpk__lnk:hover { color:#fff !important; -webkit-text-fill-color:#fff !important; }
.spp-fpk__mini-b .spp-fpk__lnk.is-on {
    color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important; font-weight:700;
}

/* chip(子板块) */
.spp-fpk__chip {
    box-sizing:border-box; max-width:100%; padding:2px 8px !important; cursor:pointer; border-radius:8px;
    height:auto !important; min-height:0 !important; box-shadow:none !important;
    font-size:11.5px !important; line-height:1.5 !important; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
    color:#c8d6e6 !important; -webkit-text-fill-color:#c8d6e6 !important;
    background-color:rgba(255,255,255,.045); background-image:none;
    border:1px solid rgba(255,255,255,.085);
    transition:background-color .16s ease, border-color .16s ease, color .16s ease;
}
.spp-fpk__chip:hover {
    color:#fff !important; -webkit-text-fill-color:#fff !important;
    background-color:rgba(32,201,151,.16); border-color:rgba(32,201,151,.5);
}
.spp-fpk__chip.is-on,
.spp-fpk__mini-t.is-on + .spp-fpk__mini-b .spp-fpk__chip.is-on {
    color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important; font-weight:700;
    background-color:rgba(32,201,151,.2); border-color:rgba(32,201,151,.62);
}
.spp-fpk__chip--all { margin-left:2px; font-weight:600; }
/* ===== SPP-FPK-END ===== */`;

    const CSS_UPANEL = `
    @property --sppUpAng { syntax:'<angle>'; inherits:false; initial-value:0deg; }

    /* 外层容器转深色 */
    html.spp-hm-star #infobox.spp-up-on {
        background:rgba(14,18,26,0.90) !important;
        border:1px solid rgba(255,255,255,0.075) !important;
        box-shadow:0 16px 46px rgba(2,6,23,0.50), inset 0 1px 0 rgba(255,255,255,0.05) !important;
    }
    /* 用 border-spacing 拉开两格间隙(代替 flex 的 gap) */
    #infobox.spp-up-on > table { border-collapse:separate !important; border-spacing:14px 8px !important; }
    #infobox.spp-up-on td { vertical-align:middle !important; }
    #infobox.spp-up-on td { padding:14px 16px !important; }

    /* ---- 深色卡内的抗锯齿纠正(解决「字体不清晰」的第二个成因)----
       全局 body 被设为 -webkit-font-smoothing:antialiased(见 THEME 的 body 规则),
       该值在【浅色底】上更细腻,但在【深色底】上会让笔画变细、发虚。
       深色卡内改回 subpixel-antialiased,笔画立刻变实 —— 不放大字号、
       不改变任何盒模型尺寸,因此不影响已冻结的宽度预算。 */
    #infobox.spp-up-on,
    #user_info.spp-up-card,
    #infobox.spp-up-on td.spp-up-host-td {
        -webkit-font-smoothing:subpixel-antialiased !important;
        -moz-osx-font-smoothing:auto !important;
    }

    /* ---- 左格:用户信息卡(旋转渐变描边,主视觉) ---- */
    #user_info.spp-up-card {
        position:relative !important;
        min-height:189px;
        border-radius:14px !important;
        overflow:hidden;
        border:2px solid transparent !important;
        background-image:
            radial-gradient(150% 190% at 104% -22%, var(--spp-up-a1,rgba(32,201,151,.26)), transparent 58%),
            linear-gradient(135deg,#222732 0%,#191d25 58%,#12161d 100%),
            conic-gradient(from var(--sppUpAng),
                var(--spp-up-c1,#20c997) 0deg, var(--spp-up-c2,#2f6f68) 120deg,
                var(--spp-up-c3,#387ef0) 240deg, var(--spp-up-c1,#20c997) 360deg);
        background-origin:border-box;
        background-clip:padding-box, padding-box, border-box;
        animation:sppUpSpin 8s linear infinite;
        box-shadow:0 1px 25px rgba(255,255,255,.07), 0 18px 44px -20px rgba(0,0,0,.9) !important;
        transition:box-shadow .28s ease, transform .28s ease;
    }
    #user_info.spp-up-card:hover {
        transform:translateY(-2px);
        box-shadow:0 1px 30px rgba(255,255,255,.12), 0 22px 52px -20px rgba(0,0,0,.95) !important;
    }
    @keyframes sppUpSpin { to { --sppUpAng:360deg; } }

    /* 顶部内高光(只做 inset 高光,不填色,不遮文字) */
    #user_info.spp-up-card::after {
        content:''; position:absolute; inset:2px; z-index:1; border-radius:12px;
        pointer-events:none;
        box-shadow:inset 0 1px 0 rgba(255,255,255,.12);
        background:linear-gradient(180deg, rgba(255,255,255,.07), transparent 36%);
    }

    /* 内容层:绝对定位铺满 padding 盒子并垂直居中
       (#user_info 是 td,高度会被同行的统计格撑高,不居中会在下方留一大块空白) */
    #user_info.spp-up-card .spp-up__real {
        position:absolute; inset:14px 16px; z-index:3;
        display:grid;
        grid-template-columns:60px minmax(0,1fr) auto;
        grid-template-rows:auto auto auto;
        align-content:center;
        column-gap:16px; row-gap:9px;
        align-items:center;
        overflow:hidden;
    }

    /* 头像:旋转光环保留给伪元素,否则头像会跟着转 */
    #user_info.spp-up-card .spp-up__av {
        grid-column:1; grid-row:1 / span 2;
        position:relative; width:60px; height:60px; justify-self:start;
        box-sizing:border-box;   /* 不加的话 padding 会把外框撑到 66px */
        border-radius:50%; padding:3px;
        box-shadow:0 6px 18px -8px rgba(0,0,0,.85);
    }
    #user_info.spp-up-card .spp-up__av::before {
        content:''; position:absolute; inset:0; border-radius:50%; z-index:0;
        background:conic-gradient(from 140deg,
            var(--spp-up-c1,#20c997), var(--spp-up-c2,#2f6f68),
            var(--spp-up-c3,#387ef0), var(--spp-up-c1,#20c997));
        animation:sppUpRing 9s linear infinite;
    }
    @keyframes sppUpRing { to { transform:rotate(360deg); } }
    #user_info.spp-up-card:hover .spp-up__av::before { animation-duration:2.6s; }

    /* 头像本体:实站内联 style="float:left;margin-top:.4em;width:40px;clip:rect(0,40px,40px,0);position:absolute"
       → 全部要 !important 才能盖掉内联。
       position 用 relative(不是 static),否则 z-index 不生效,会被上面的光环盖住 */
    #user_info.spp-up-card .spp-up__av img {
        position:relative !important;
        z-index:1;
        width:100% !important; height:100% !important;
        object-fit:cover; border-radius:50%;
        display:block; background:#232a33;
        clip:auto !important;
        float:none !important; margin:0 !important;
    }

    /* 第一行:等级 / 帖子 / 操作 */
    #user_info.spp-up-card .spp-up__row1 {
        grid-column:2; grid-row:1;
        display:flex; align-items:center; flex-wrap:wrap; gap:7px; min-width:0;
    }
    /*   ★★ 2.52.82(用户红框:「字体不再同一条线上」):chip 原来是 align-items:baseline ——
       固定 24px 高的容器里 baseline 对齐会让内容整体【顶】在胶囊上沿,而右侧「我的主题 /
       我的回复 / 更多」那排胶囊是 align-items:center(文字垂直居中)。实站 Range 实测:
       chip 文字底 113px vs 胶囊文字底 118px,差 4~6px,肉眼就是「两行字」。
       修法与胶囊同一机制:chip 改 center,k/v 给 line-height:24px(行盒=胶囊高度),
       改后 chip 文字底 116/117px,与胶囊的 118px 差 1~2px(来自 13px 值与 12px 标签的
       字号差,属正常排版)。这张卡是全页族共用的一份,首页/版块页/帖子页同步受益。 */
    #user_info.spp-up-card .spp-up__chip {
        display:inline-flex; align-items:center; gap:5px;
        height:24px; padding:0 10px; border-radius:8px;
        background:rgba(255,255,255,.055);
        border:1px solid rgba(255,255,255,.105);
        font-size:12px; color:#a9bccb; white-space:nowrap;
        transition:transform .18s ease, background .18s ease, border-color .18s ease;
    }
    #user_info.spp-up-card .spp-up__chip:hover {
        transform:translateY(-1px); background:rgba(32,201,151,.16); border-color:rgba(32,201,151,.42);
    }
    #user_info.spp-up-card .spp-up__chip .k { font-size:11px; color:#8798a8; line-height:24px; }
    #user_info.spp-up-card .spp-up__chip .v {
        font-weight:700; font-size:13px; color:#e8eef6;
        font-variant-numeric:tabular-nums; letter-spacing:-.01em;
        line-height:24px;
    }
    #user_info.spp-up-card .spp-up__chip--lv .v {
        background:linear-gradient(45deg,#5ff0c0,#63b8ff);
        -webkit-background-clip:text; background-clip:text; color:transparent;
    }
    #user_info.spp-up-card .spp-up__acts { display:inline-flex; align-items:center; gap:5px; margin-left:2px; }
    #user_info.spp-up-card .spp-up__acts a,
    #user_info.spp-up-card .spp-up__more {
        display:inline-flex; align-items:center; gap:4px;
        height:24px; padding:0 10px; border-radius:999px;
        font-size:12px; text-decoration:none !important;
        color:#a9d8cd !important; background:rgba(255,255,255,.07) !important;
        border:1px solid rgba(255,255,255,.14);
        transition:background .16s ease, transform .16s ease, border-color .16s ease, color .16s ease;
    }
    #user_info.spp-up-card .spp-up__acts a:hover,
    #user_info.spp-up-card .spp-up__more:hover {
        background:rgba(32,201,151,.22) !important; border-color:rgba(32,201,151,.55);
        color:#d8fff5 !important; transform:translateY(-1px);
    }
    /* 原「更多」是 <a id="td_u" class="dropB"> + 背景图图标,
       站点用 .dropB{text-indent:-24000px; overflow:hidden} 把文字藏掉、只露图标。
       这里要把三样都还原:文字缩进、overflow、背景图;并放开被 .dropB 限死的 width。 */
    #user_info.spp-up-card #td_u {
        float:none !important; clear:none !important;
        width:auto !important; height:24px !important;
        font-size:12px !important; color:#a9d8cd !important;
        background-image:none !important;
        text-indent:0 !important;
        overflow:visible !important;
        line-height:1 !important;
        display:inline-flex !important; align-items:center;
    }
    #user_info.spp-up-card #td_u::after { content:'▼'; font-size:9px; margin-left:4px; opacity:.8; }

    /* 第二行:签名(把原生 #honor 挪进来,站点 honor() 仍写这个节点) */
    #user_info.spp-up-card .spp-up__sign {
        grid-column:2 / span 2; grid-row:2;
        display:flex; align-items:center; gap:8px; min-width:0;
        padding:8px 12px 8px 13px; border-radius:10px;
        background:linear-gradient(100deg, rgba(255,255,255,.07), rgba(255,255,255,.028));
        border-left:3px solid var(--spp-up-c2,#2f6f68);
        box-shadow:inset 0 1px 0 rgba(255,255,255,.08);
        transition:background .22s ease, border-color .22s ease;
    }
    #user_info.spp-up-card:hover .spp-up__sign {
        background:linear-gradient(100deg, rgba(32,201,151,.16), rgba(56,126,240,.08));
    }
    #user_info.spp-up-card #honor {
        flex:1; min-width:0;
        font-size:13.5px !important; color:#cfd9e3 !important;
        font-style:italic; letter-spacing:.2px;
        white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
    }
    /* 原「编辑」是 <a id="sethonor" onclick="sendmsg(...)"> + gif 图标 */
    #user_info.spp-up-card #sethonor {
        flex:none; display:inline-flex; align-items:center; gap:4px;
        font-size:11.5px !important; color:#8b9bab !important; text-decoration:none !important;
        padding:2px 8px; border-radius:6px;
        transition:background .16s ease, color .16s ease;
    }
    #user_info.spp-up-card #sethonor:hover { background:rgba(56,126,240,.22); color:#bcd6ff !important; }
    #user_info.spp-up-card #sethonor img { width:11px; height:11px; vertical-align:middle; opacity:.8; }

    /* 第三行:账户明细(把原生 hover 面板 #showface 里的信息提到明面) */
    #user_info.spp-up-card .spp-up__row3 {
        grid-column:1 / span 3; grid-row:3;
        display:flex; align-items:center; flex-wrap:wrap; gap:6px 4px;
        padding-top:9px; margin-top:1px;
        border-top:1px dashed rgba(255,255,255,.14);
    }
    #user_info.spp-up-card .spp-up__m {
        display:inline-flex; align-items:baseline; gap:5px;
        padding:2px 9px 2px 0; font-size:11.5px; color:#93a3b4;
        border-right:1px solid rgba(255,255,255,.13);
        font-variant-numeric:tabular-nums;
    }
    #user_info.spp-up-card .spp-up__m:last-child { border-right:0; padding-right:0; }
    #user_info.spp-up-card .spp-up__m b { font-size:12px; font-weight:700; color:#dbe5ef; }
    #user_info.spp-up-card .spp-up__m--sp b { color:#ffc36b; }
    #user_info.spp-up-card .spp-up__m--ip b {
        font-family:'SF Mono','Cascadia Code',Consolas,monospace; font-weight:500; color:#a9bccb;
    }

    /* 原生 #sf / #showface 的容器:整体隐藏但留在 DOM 里,
       免得头像上的内联 onmouseover="read.open('showface','sf','4')" 找不到节点 */
    #user_info.spp-up-card .spp-up__orig { display:none !important; }

    /* ---- 右格:统计条(从属卡,刻意不给动画描边,不跟左格抢焦点) ---- */
    #infobox.spp-up-on td.spp-up-host-td {
        padding:14px 16px !important;
        border-radius:14px !important;
        vertical-align:middle !important;
        border:1px solid rgba(255,255,255,.075) !important;
        background:linear-gradient(160deg,#1b2029 0%,#171b22 60%,#14181e 100%) !important;
        box-shadow:inset 0 1px 0 rgba(255,255,255,.05), 0 10px 30px -24px rgba(0,0,0,.9) !important;
    }
    #infobox.spp-up-on td.spp-up-host-td > .spp-hm-host {
        width:100%; box-sizing:border-box;
        float:none !important; text-align:left !important; padding:0 !important;
    }
    #infobox.spp-up-on .spp-hm-stats { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:9px; }
    #infobox.spp-up-on .spp-hm-stat {
        display:flex; flex-direction:column; gap:3px;
        padding:9px 13px 10px;
        border-radius:10px;
        border:1px solid rgba(255,255,255,.075) !important;
        background:linear-gradient(180deg, rgba(255,255,255,.055), rgba(255,255,255,.018)) !important;
        box-shadow:inset 0 1px 0 rgba(255,255,255,.06) !important;
        transition:transform .18s ease, box-shadow .18s ease, border-color .18s ease, background .18s ease;
    }
    #infobox.spp-up-on .spp-hm-stat:hover {
        transform:translateY(-2px);
        border-color:rgba(32,201,151,.42) !important;
        background:linear-gradient(180deg, rgba(32,201,151,.14), rgba(255,255,255,.02)) !important;
        box-shadow:inset 0 1px 0 rgba(255,255,255,.10), 0 8px 22px -10px rgba(0,0,0,.9) !important;
    }
    #infobox.spp-up-on .spp-hm-stat .k {
        display:inline-flex; align-items:center; gap:5px;
        font-size:11px !important; color:#8798a8 !important; letter-spacing:.3px;
        font-weight:450 !important;
    }
    /* 标签前一颗 4px 主色小点,给 6 格建立节奏(不依赖额外 DOM) */
    #infobox.spp-up-on .spp-hm-stat .k::before {
        content:''; width:4px; height:4px; border-radius:50%;
        background:var(--spp-up-c1,#20c997);
        box-shadow:0 0 6px var(--spp-up-a1,rgba(32,201,151,.26)); flex:none;
    }
    #infobox.spp-up-on .spp-hm-stat .v {
        font-size:16px !important; font-weight:700 !important; color:#eaf2f9 !important;
        font-variant-numeric:tabular-nums; letter-spacing:-.015em;
        text-shadow:0 1px 2px rgba(0,0,0,.6);
    }
    #infobox.spp-up-on .spp-hm-acts {
        display:flex; flex-wrap:wrap; gap:6px; justify-content:flex-end; align-items:center;
        margin-top:11px; padding-top:10px;
        border-top:1px dashed rgba(255,255,255,.12);
    }
    #infobox.spp-up-on .spp-hm-acts a {
        position:relative; overflow:hidden; isolation:isolate;
        font-size:12px !important; font-weight:650 !important; letter-spacing:.01em;
        padding:5px 14px !important; border-radius:30px !important;
        background:#1c222b !important; color:#d9e7e2 !important;
        border:1.5px solid rgba(255,255,255,.18) !important;
        text-decoration:none !important; cursor:pointer;
        display:inline-flex !important; align-items:center; gap:6px;
        box-shadow:0 1px 2px rgba(0,0,0,.35), inset 0 1px 0 rgba(255,255,255,.05);
        transition:all .4s ease !important;
    }
    /* 原版那颗径向眩光:默认 scale(0),悬停 scale(4) —— 取自 uiverse iZOXVL/wise-goat-75 */
    #infobox.spp-up-on .spp-hm-acts a::after {
        content:''; position:absolute; inset:0; z-index:-1;
        background:radial-gradient(circle, rgba(255,255,255,.25) 0%, rgba(255,255,255,0) 70%);
        transform:scale(0); transition:transform .5s ease; pointer-events:none;
    }
    #infobox.spp-up-on .spp-hm-acts a:hover::after { transform:scale(4); }
    #infobox.spp-up-on .spp-hm-acts a:hover {
        background:#253040 !important; border-color:rgba(32,201,151,.55) !important;
        color:#eafff8 !important; transform:translateY(-1px);
        box-shadow:0 6px 18px -6px rgba(32,201,151,.45), inset 0 1px 0 rgba(255,255,255,.09);
    }
    #infobox.spp-up-on .spp-hm-acts a:active { transform:translateY(0) scale(.985); transition-duration:.08s !important; }
    /* 主行动项「新会员」:唯一带主色底的按钮,作视觉落点 */
    #infobox.spp-up-on .spp-hm-acts a[href*="show-username"] {
        background:linear-gradient(180deg,#2b7d6f,#22655b) !important;
        border-color:rgba(32,201,151,.45) !important; color:#f0fffa !important;
    }
    #infobox.spp-up-on .spp-hm-acts a[href*="show-username"]:hover {
        background:linear-gradient(180deg,#33907f,#2a7a6b) !important;
        border-color:rgba(32,201,151,.8) !important;
    }

    /* ---- 在线人数徽标(深色环境覆盖;浅色版在 CSS_STATS 里)----
       实站 599px 可用宽度下:11px + 26-08-06 = 223px,余 63px 单行成立。
       ⚠ 12px + 完整日期 = 282px 只余 4px,最后一个 RSS 会掉到第二行
         (操作条 40px → 75px),左右两格随即不等高,禁止使用。 */
    #infobox.spp-up-on .spp-onb {
        border-color:rgba(255,255,255,.10) !important;
        background:linear-gradient(90deg, rgba(32,201,151,.15), rgba(255,255,255,.045) 64%) !important;
        box-shadow:inset 0 1px 0 rgba(255,255,255,.06) !important;
    }
    #infobox.spp-up-on .spp-onb__dot {
        background:var(--spp-up-c1,#20c997) !important;
        box-shadow:0 0 7px var(--spp-up-a1,rgba(32,201,151,.26)) !important;
        animation:sppOnPulse 2.4s ease-in-out infinite;
    }
    @keyframes sppOnPulse {
        0%,100% { opacity:1; transform:scale(1); }
        50%     { opacity:.55; transform:scale(.8); }
    }
    #infobox.spp-up-on .spp-onb__k,
    #infobox.spp-up-on .spp-onb__peak { color:#8798a8 !important; }
    #infobox.spp-up-on .spp-onb__v {
        color:#7ef0c4 !important; text-shadow:0 0 12px rgba(32,201,151,.28);
    }
    #infobox.spp-up-on .spp-onb__peak b { color:#dfe7f0 !important; }
    /* 日期原为 #5f7285,在 #0e121a 上实测仅 3.99:1(11px 小字远低于 AA 4.5:1),
       故提到与「在线/峰值」同色 #8798a8 = 6.69:1,视觉层级仍低于数值。
       ★ 只改颜色,绝不加字号:徽标宽度实测 226px 是硬约束,
         12px + 完整日期 = 282px 会把 RSS 挤到第二行、左右两格不等高。 */
    #infobox.spp-up-on .spp-onb__date { color:#8798a8 !important; opacity:1; }`;

    function esc(s) {

    return String(s).replace(/[&<>"]/g, function (c) {
    return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c];
    });
    }

    function polishUserInfo(onUPanel) {
        if (!onUPanel) return;   // 入参:首页传 GM 开关、版块页传 true
        const ib = document.getElementById('infobox');
        if (!ib || ib.classList.contains('spp-up-on')) return;
        const ui = document.getElementById('user_info');
        if (!ui) return;
        // 防重入:initHomePolish 会在 0s / 1.2s 各跑一次,
        // 没有这道守卫时「编辑」按钮会被插入多个 SVG(实测出现 2 个)
        if (ui.dataset.sppUpDone) return;
        ui.dataset.sppUpDone = '1';

        const tbl = ib.querySelector('table');
        const tds = tbl ? [].slice.call(tbl.querySelectorAll('tr > td')) : [];
        const hostTd = tds.filter(function (t) { return t !== ui; })[0];
        if (!tbl || tds.length !== 2 || !hostTd) return;   // 结构不符就整体不动

        // ---- 采集原生信息(先读后写,避免读到自己刚写的节点)----
        const face = ui.querySelector('#showface');
        function grab(re) {
            if (!face) return null;
            const m = (face.textContent || '').match(re);
            return m ? m[1].trim() : null;
        }
        const uid  = grab(/UID\s*[::]?\s*([\w]+)/i);
        const sp   = grab(/SP币\s*[::]?\s*([\d,]+\s*G)/i);
        const hp   = grab(/HP\s*[::]?\s*([\d,]+\s*点)/i);
        const po   = grab(/魄\s*[::]?\s*([\d,]+\s*点)/i);
        const last = grab(/最后登录\s*[::]?\s*([\d-]+)/i);
        const ip   = grab(/登录IP\s*[::]?\s*([\d.]+)/i);

        const lvTxt = (ui.textContent || '').match(/等级\s*[::]\s*(Lv\.?\s*\d+)/i);
        // \d[\d,]*\d 收尾必须是数字,否则「帖子:182,」会把后面的逗号也吃进来
        const poTxt = (ui.textContent || '').match(/帖子\s*[::]?\s*(\d[\d,]*\d|\d)/);
        const avatar = ui.querySelector('a[href="u.php"] img');

        const links = [].slice.call(ui.querySelectorAll('a.link5'));
        const mineTopics = links.filter(function (a) { return /我的主题/.test(a.textContent); })[0];
        const minePosts  = links.filter(function (a) { return /我的回复/.test(a.textContent); })[0];
        const more       = ui.querySelector('#td_u');
        const honor      = ui.querySelector('#honor');
        const setHonor   = ui.querySelector('#sethonor');
        const editImg    = setHonor ? setHonor.querySelector('img') : null;

        // 原生那两层容器(#sf 是 read.open 的锚点、#showface 是内容),
        // 一起塞进隐藏壳保留在 DOM 里,别删
        const sf = ui.querySelector('#sf');

        // ---- 组装 ----
        const real = document.createElement('div');
        real.className = 'spp-up__real';

        const av = document.createElement('div');
        av.className = 'spp-up__av';
        if (avatar) {
            const img = avatar.cloneNode(true);       // 克隆:原 <a> 还带 onmouseover,克隆体不复制事件
            img.removeAttribute('style');             // 去掉内联裁剪/定位,交给 CSS
            img.removeAttribute('onmouseover');
            img.removeAttribute('onmouseout');
            av.appendChild(img);
        }
        real.appendChild(av);

        const row1 = document.createElement('div');
        row1.className = 'spp-up__row1';
        if (lvTxt) {
            row1.innerHTML += '<span class="spp-up__chip spp-up__chip--lv"><span class="k">等级</span>' +
                '<span class="v">' + esc(lvTxt[1].replace(/\s+/g, '')) + '</span></span>';
        }
        if (poTxt) {
            row1.innerHTML += '<span class="spp-up__chip"><span class="k">帖子</span>' +
                '<span class="v">' + esc(poTxt[1]) + '</span></span>';
        }
        const acts = document.createElement('span');
        acts.className = 'spp-up__acts';
        if (mineTopics) acts.appendChild(mineTopics);   // 直接搬原节点 → 保留 href/class
        if (minePosts)  acts.appendChild(minePosts);
        if (more)       acts.appendChild(more);
        row1.appendChild(acts);
        real.appendChild(row1);

        const sign = document.createElement('div');
        sign.className = 'spp-up__sign';
        if (honor) sign.appendChild(honor);
        if (setHonor) sign.appendChild(setHonor);
        real.appendChild(sign);

        const row3 = document.createElement('div');
        row3.className = 'spp-up__row3';
        const cells = [
            ['UID', uid, ''], ['SP币', sp, ' spp-up__m--sp'], ['HP', hp, ''],
            ['魄', po, ''], ['最后登录', last, ''], ['IP', ip, ' spp-up__m--ip']
        ].filter(function (c) { return c[1]; });
        row3.innerHTML = cells.map(function (c) {
            return '<span class="spp-up__m' + c[2] + '">' + esc(c[0]) + ' <b>' + esc(c[1]) + '</b></span>';
        }).join('');
        real.appendChild(row3);

        // 原生容器整体搬进隐藏壳(含 #sf / #showface,保住 read.open 锚点)
        const orig = document.createElement('div');
        orig.className = 'spp-up__orig';
        if (face) orig.appendChild(face);
        if (sf) orig.appendChild(sf);
        const origWrap = ui.querySelector('div[style*="margin-left"]');
        if (origWrap) orig.appendChild(origWrap);
        const avatarLink = ui.querySelector('a[href="u.php"]');
        if (avatarLink) orig.appendChild(avatarLink);

        // ---- 落地 ----
        ui.classList.add('spp-up-card');
        ui.innerHTML = '';
        ui.appendChild(real);
        ui.appendChild(orig);

        hostTd.classList.add('spp-up-host-td');
        ib.classList.add('spp-up-on');

        // 编辑按钮的原生 img 是 gif 图标,换成内联 SVG 更配深色底(失败则保留原图)
        if (setHonor && editImg) {
            // 先移除可能已存在的替换 SVG,避免重复执行时叠加出多个图标
            setHonor.querySelectorAll('svg.spp-up__edit-ico').forEach(function (s) { s.remove(); });
            const svg = '<svg class="spp-up__edit-ico" viewBox="0 0 24 24" width="12" height="12" fill="currentColor" style="opacity:.85;flex:none">' +
                '<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 0 0 0-1.41' +
                'l-2.34-2.34a1 1 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>';
            editImg.insertAdjacentHTML('afterend', svg);
            editImg.style.display = 'none';
        }
    }


    // ==================== 16. 首页美化(index.php · 漫区特设 Comic Market 卡片) ====================
    function initHomePolish() {
        // 仅首页生效:/ 、/index.php
        if (!(PATH === '/' || PATH === '' || /\/index\.php$/.test(PATH))) return;

        const HP_KEY    = 'spp_home_polish';    // 总开关
        const HP_STATS  = 'spp_home_stats';     // 统计条卡片化
        const HP_NOTICE = 'spp_home_notice';    // 公告胶囊化
        const HP_BOARDS = 'spp_home_boards';    // 版块区块清爽化
        const HP_CM     = 'spp_home_cm_card';   // 漫区特设 Comic Market 卡片
        const HP_HJ     = 'spp_home_hj_card';   // 综合交流卡片(ACG/轻小说/原创/求物/AI)
        const HP_RM     = 'spp_home_rm_card';   // 人民囧府卡片(事务受理)
        const HP_FL     = 'spp_home_fl_card';   // 友情链接卡片(人民囧府右侧)
        const HP_ONLINE = 'spp_home_online';    // 在线人数徽标(统计卡操作条左端)
        const HP_SUBCARD = 'spp_home_subcard';  // 子版卡片区(茶馆三栏 + 免空/网赚)
        const HP_TOPCARD = 'spp_home_topcard';  // 顶部四栏板块卡片化
        const HP_HIDE_NOTICE = 'spp_home_hide_notice'; // 屏蔽公告栏
        const HP_HDR    = 'spp_home_hide_header'; // 屏蔽顶部横幅
        const HP_UPANEL = 'spp_home_upanel';    // 用户信息区深色卡
        // ★★ 2.53.62:这里原有 HP_SCOPE(首页 CM 卡片范围的存储键)。用户要求把那个开关
        //   整条撤掉(连 Userscript 菜单那一行也不留),范围写死 'cm' ⇒
        //   常量与本文件里那两行读数一起删除,存储里哪怕还留着旧值也不再被读。

        if (!GM_getValue(HP_KEY, true)) return;
        const onStats  = GM_getValue(HP_STATS, true);
        const onNotice = GM_getValue(HP_NOTICE, true);
        const onBoards = GM_getValue(HP_BOARDS, true);
        const onCard   = GM_getValue(HP_CM, true);
        const onHjCard = GM_getValue(HP_HJ, true);
        const onRmCard = GM_getValue(HP_RM, true);
        const onFlCard = GM_getValue(HP_FL, true);
        const onOnline = GM_getValue(HP_ONLINE, true);
        const onSub    = GM_getValue(HP_SUBCARD, true);
        const onTopCard = GM_getValue(HP_TOPCARD, true);
        const onHideNotice = GM_getValue(HP_HIDE_NOTICE, true);
        const onHdr    = GM_getValue(HP_HDR, true);
        const onUPanel = GM_getValue(HP_UPANEL, true);
        // ★★ 2.53.62:写死「C104 起(含最新)」。这里原有两行 —— 读存储、并把历史档位 c104
        //   归一为 cm;那个开关整条撤掉之后,范围只剩下面渲染调用点里的一个字面量。

        const STYLE_ID = 'spp-home-polish-style';

        // ---------- CSS ----------
        const CSS_STATS = `
    #infobox .spp-hm-host { float:none !important; text-align:left !important; padding:6px 8px !important; }
    .spp-hm-stats { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:8px; }
    .spp-hm-stat {
        display:flex; flex-direction:column; gap:1px; padding:8px 12px;
        border-radius:10px; border:1px solid var(--spu-border,#d8dee7);
        background:linear-gradient(180deg,rgba(255,255,255,.95),rgba(238,242,245,.92));
        box-shadow:0 1px 2px rgba(15,23,42,.04);
        transition:transform .18s ease,box-shadow .18s ease,border-color .18s ease;
    }
    .spp-hm-stat:hover { transform:translateY(-2px); box-shadow:0 8px 20px rgba(15,23,42,.09); border-color:var(--spu-accent,#2f6f68); }
    .spp-hm-stat .k { font-size:11px; color:var(--spu-muted,#6b7280); }
    .spp-hm-stat .v { font-size:15px; font-weight:700; color:var(--spu-text,#1f2937); font-variant-numeric:tabular-nums; letter-spacing:-.01em; }
    .spp-hm-acts { display:flex; flex-wrap:wrap; gap:6px; justify-content:flex-end; align-items:center; margin-top:8px; }
    .spp-hm-acts a {
        font-size:12px !important; padding:4px 11px; border-radius:999px;
        background:rgba(47,111,104,.08); color:var(--spu-accent,#2f6f68) !important;
        text-decoration:none !important; transition:background .16s ease,transform .16s ease;
    }
    .spp-hm-acts a:hover { background:rgba(47,111,104,.18); transform:translateY(-1px); }

    /* ★ 新会员链接必须封顶宽度,否则徽标会随时折行。
       「新会员:<用户名>」里的用户名是【变长用户内容】:
       实测 aa_123=112px、highest_plenary=162px,长度不可预期。
       操作条一行总需 = 徽标226 + 新会员 + 标记已读71 + 最新帖子71 + RSS45 + 4×6(gap)
       用户名 162px 时正好 599px(超 1px 即折行)。封顶后最坏总需 577px,
       恒定留出 22px 余量,与用户名长度无关 —— 这是唯一的硬保证。
       ★ 必须写 box-sizing:border-box:默认 content-box 时 max-width 只管内容盒,
         加上左右各 11px 内边距,实际仍是 162px,封顶形同虚设(踩过一次)。
       只裁显示、不删内容,完整名字仍可点进去看到。 */
    #infobox .spp-hm-acts a[href*="show-username"] {
        box-sizing:border-box;
        max-width:140px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
    }

    /* ---- 在线人数徽标(源数据:论坛相关表「在线用户 - 共 N 人在线,最多 M 人发生在 ...」)----
       放在操作条【左端空白】处:实站该处实测空闲 282px,而 4 个按钮只占 317px。
       margin-right:auto 是关键 —— 它把后面的按钮组照原样推回右端,不改变原布局。
       ★ 类名必须带自己的前缀(spp-onb):
         站内已有 html.spp-hm-on 这个【根元素标记类】(applyHomeStyle 里
         document.documentElement.classList.add('spp-hm-on')),若这里也叫 .spp-hm-on,
         这条 display:inline-flex 会直接命中 <html>,整个页面会塌掉。
         同理 .spp-hm-on__x 也会被 html 继承——一律避开 spp-hm-on 前缀。 */
    .spp-onb {
        display:inline-flex; align-items:center; gap:7px;
        margin-right:auto; min-width:0; max-width:100%;
        padding:3px 12px 3px 10px; border-radius:999px;
        border:1px solid var(--spu-border,#d8dee7);
        background:rgba(47,111,104,.08);
        font-size:11px; line-height:1.55; white-space:nowrap;
        /* 收窄时自身先让步,核心数字用 flex:none 保住 */
        flex:0 1 auto;
    }
    .spp-onb__dot {
        width:6px; height:6px; border-radius:50%; background:var(--spu-accent,#2f6f68); flex:none;
    }
    .spp-onb__k { color:var(--spu-muted,#6b7280); flex:none; }
    .spp-onb__v { color:var(--spu-text,#1f2937); font-variant-numeric:tabular-nums; letter-spacing:-.01em; flex:none; }
    .spp-onb__peak { color:var(--spu-muted,#6b7280); flex:none; }
    .spp-onb__peak b { color:var(--spu-text,#1f2937); font-variant-numeric:tabular-nums; }
    /* 日期信息量最低:空间不够时它先被压缩/裁掉(flex-shrink 99 远大于其他项),
       而不是整条徽标折行撑高整张卡(左侧用户卡只有 ~29px 余量)。
       min-width:0 + overflow:hidden 才能让 flex 项真的收缩,否则 min-content 兜底 */
    .spp-onb__date {
        color:var(--spu-muted,#6b7280); opacity:.75; font-variant-numeric:tabular-nums;
        flex:0 99 auto; min-width:0; overflow:hidden; text-overflow:ellipsis;
    }
    `;

        // ---------- 用户信息区深色卡(#infobox 两格)----------
        // 实站 #infobox 是 table 布局:td#user_info(53%) + td(统计条)。
        // 两格都做成深色卡,靠 #infobox > table 的 border-spacing 拉出 14px 间隙;
        // 表格同一行的 td 天然等高,所以左右卡高度一定一致(预览里用 flex 做不到这点)。
        // 描边手法:@property 注册 --sppUpAng + conic-gradient(from var(--sppUpAng))
        //           + background-clip: padding-box, border-box → 渐变压成 2px 边框。
        //           ★ 第 1 层必须【不透明】才能把锥形挤进边框;用半透明会整格漫色。

        const CSS_NOTICE = `
    #notice { overflow:visible !important; }
    #notice0 { height:auto !important; overflow:visible !important; }
    #notice > div[style*="height:36px"] { display:none !important; }
    #notice0 ul.spp-hm-notice-list { display:flex !important; flex-wrap:wrap; gap:8px; margin:0 !important; padding:0 !important; list-style:none !important; }
    #notice0 ul.spp-hm-notice-list > li {
        display:flex; align-items:center; gap:8px; padding:6px 12px;
        border-radius:999px; background:#fff; border:1px solid var(--spu-border,#d8dee7);
        box-shadow:0 1px 2px rgba(15,23,42,.04);
        transition:transform .16s ease,box-shadow .16s ease,border-color .16s ease;
    }
    #notice0 ul.spp-hm-notice-list > li:hover { transform:translateY(-1px); border-color:var(--spu-accent,#2f6f68); box-shadow:0 6px 16px rgba(15,23,42,.09); }
    #notice0 ul.spp-hm-notice-list > li a { text-decoration:none !important; font-size:12.5px !important; }
    #notice0 ul.spp-hm-notice-list > li .f9 { font-size:11px !important; opacity:.6; }
    `;

        const CSS_BOARDS = `
    .spp-hm-on .t > table { border-collapse:separate !important; border-spacing:0 !important; }
    .spp-hm-on .t > table > tbody > tr > th.h {
        background:linear-gradient(180deg,#ffffff,#f0f4f7) !important;
        border-bottom:1px solid var(--spu-border,#d8dee7) !important;
        padding:9px 14px !important;
    }
    .spp-hm-on .t > table > tbody > tr > th.h h2 a.cfont {
        font-size:14.5px !important; font-weight:700 !important;
        color:var(--spu-text,#1f2937) !important; text-decoration:none !important;
    }
    .spp-hm-on .t > table > tbody > tr > th.h h2 a.cfont::before {
        content:''; display:inline-block; width:3px; height:13px; margin-right:8px;
        vertical-align:-1px; border-radius:2px;
        background:linear-gradient(180deg,var(--spu-accent,#2f6f68),#7fb3ac);
    }
    .spp-hm-on .t > table > tbody > tr.tr2 > td {
        background:linear-gradient(180deg,#ffffff,#f3f6f9) !important;
        border-bottom:1px solid var(--spu-border,#d8dee7) !important;
        padding:8px 10px !important;
    }
    .spp-hm-on .t > table > tbody > tr.tr2 > td > span.b { font-weight:650 !important; font-size:12.5px !important; color:var(--spu-text,#1f2937) !important; }
    .spp-hm-on .t > table > tbody > tr.tr2 > td > span.b::before {
        content:''; display:inline-block; width:3px; height:11px; margin-right:6px;
        vertical-align:-1px; border-radius:2px; background:var(--spu-accent,#2f6f68);
    }
    .spp-hm-on .t > table > tbody > tr.tr3 > td.icon img { border-radius:9px !important; opacity:.9; }
    .spp-hm-on .t > table > tbody > tr.tr3 > th { padding:10px 8px !important; }
    .spp-hm-on .t > table > tbody > tr.tr3 > th h3.b > a { font-size:13.5px !important; font-weight:600 !important; }
    .spp-hm-on .t > table > tbody > tr.tr3 > td.tal.y-style.e { padding:10px 12px !important; line-height:1.95 !important; }
    `;

    // ====================================================================
    // 漫区特设 Comic Market 卡片 · v2 版式
    // 视觉内核移植自 Uiverse / ElSombrero2 (fresh-wombat-97, MIT):
    //   __spin  = .card::before  80x360 渐变条 8s 无限旋转 → 悬停时的描边流光
    //   ::before = 静态渐变兜底(不悬停时也有极淡彩边,悬停时让位给旋转条)
    //   __inner  = .card::after 磨砂遮罩,blur 由 50px 降至 14px 以保证小字可读
    // 原样式 MIT 许可声明:
    //   Copyright - 2026 ElSombrero2 (Rakotondrasoa Nirilala)
    //   Permission is hereby granted, free of charge, to any person obtaining
    //   a copy of this software ... (MIT License, 全文见项目根 README)
    // 版块映射:徽章+标题=Comic Market N / 数据带=主题·帖数·今日 /
    //           板块入口=同人志&CG、同人志&CG(图墙模式) / 发言条=最后发言·最后发言时间
    // ====================================================================
    const CSS_CARD = `
    .spp-cm-row > td { padding:14px 10px !important; border:none !important; background:transparent !important; }
    .spp-cm-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(330px,1fr)); gap:16px; }
    .spp-cm-grid.is-single { grid-template-columns:minmax(0,540px); justify-content:center; }
    /* 分组之间留出间距(CM 届次组 ↔ 常设大区组) */
    .spp-cm-grid + .spp-cm-grid { margin-top:16px; }
    /* 分组标题条:横跨整行,左右各一条细线,中间是标签与说明 */
    .spp-cm-group-hd {
        grid-column:1 / -1;
        display:flex; align-items:center; gap:10px;
        margin:2px 0 0; font-size:12px; letter-spacing:.02em;
        color:#94a3b8;
    }
    .spp-cm-group-hd::before, .spp-cm-group-hd::after {
        content:""; flex:1; height:1px; background:rgba(148,163,184,.22);
    }
    /* 第一组(紧随卡片行顶部)不加 before 线,避免多一条横线 */
    .spp-cm-group-hd.is-first::before { display:none; }
    .spp-cm-group-hd b { color:#cbd5e1; font-weight:650; font-size:12.5px; }
    .spp-cm-group-hd i { font-style:normal; color:#64748b; font-size:11px; }
    .spp-cm-group-hd em {
        font-style:normal; font-weight:600; font-size:10.5px;
        padding:2px 8px; border-radius:999px;
        background:rgba(56,126,240,.16); color:#93c5fd;
    }
    .spp-cm-group-hd.is-std em { background:rgba(255,34,136,.16); color:#f9a8d4; }
    .spp-cm-group-hd.is-pin em { background:rgba(12,227,154,.16); color:#6ee7b7; }

    .spp-cm-card {
        position:relative; padding:1.5px; border-radius:14px; overflow:hidden; isolation:isolate;
        background:#0b1120; cursor:pointer;
        /* 卡片是 grid item,会被拉伸到整行最高那张的高度。用 flex 列 + __inner:flex:1
           让内板填满,否则内容矮的卡(如只有 1 个子版入口的)底部会露出 ::before 渐变 */
        display:flex; flex-direction:column;
        box-shadow:0 18px 40px -18px rgba(2,6,23,.9);
        transition:transform .32s cubic-bezier(.2,.7,.3,1), box-shadow .32s ease;
        font-family:'Noto Sans SC','Source Han Sans SC','PingFang SC','Microsoft YaHei',sans-serif;
    }
    .spp-cm-card:hover { transform:translateY(-4px); box-shadow:0 26px 54px -20px rgba(99,102,241,.55); }

    .spp-cm-card__spin {
        position:absolute; top:50%; left:50%; width:120px; height:420px; margin:-210px 0 0 -60px;
        background:linear-gradient(180deg,#ff2288,#387ef0);
        opacity:0; z-index:0; pointer-events:none;
        animation:spp-cm-spin 8s linear infinite; animation-play-state:paused;
        transition:opacity .3s ease;
    }
    .spp-cm-card:hover .spp-cm-card__spin { opacity:1; animation-play-state:running; }
    @keyframes spp-cm-spin { from { transform:rotate(0deg); } to { transform:rotate(360deg); } }

    .spp-cm-card::before {
        content:""; position:absolute; inset:0; border-radius:14px; z-index:0;
        background:linear-gradient(120deg,rgba(56,126,240,.55),rgba(168,85,247,.4),rgba(255,34,136,.55));
        opacity:.45; transition:opacity .3s ease;
    }
    .spp-cm-card:hover::before { opacity:0; }

    .spp-cm-card__inner {
        position:relative; z-index:1; display:flex; flex-direction:column; gap:14px;
        flex:1 1 auto;   /* 撑满 .spp-cm-card,避免短卡底部露出 ::before 渐变 */
        padding:16px; border-radius:13px;
        background:linear-gradient(180deg,rgba(2,6,23,.97),rgba(2,6,23,.92));
        -webkit-backdrop-filter:blur(14px); backdrop-filter:blur(14px);
    }

    .spp-cm-card__head { display:flex; align-items:flex-start; gap:10px; }
    .spp-cm-card__badge {
        flex:0 0 auto; display:flex; flex-direction:column; align-items:center; justify-content:center;
        width:46px; height:46px; border-radius:11px; line-height:1;
        background:linear-gradient(145deg,#387ef0,#a855f7);
        box-shadow:0 6px 16px -6px rgba(56,126,240,.8);
    }
    .spp-cm-card__badge em { font-style:normal; font-size:9px; font-weight:600; letter-spacing:.08em; opacity:.85; margin-bottom:3px; }
    .spp-cm-card__badge b { font-size:17px; font-weight:800; font-variant-numeric:tabular-nums; color:#fff; }

    .spp-cm-card__titles { flex:1 1 auto; min-width:0; display:flex; flex-direction:column; gap:5px; padding-top:1px; }
    .spp-cm-card__title {
        font-size:15px !important; font-weight:650 !important; color:#fff !important; text-decoration:none !important;
        white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
    }
    .spp-cm-card__title:hover { color:#c7d2fe !important; }
    .spp-cm-card__meta { display:flex; flex-wrap:wrap; align-items:center; gap:2px 6px; font-size:11px; color:#64748b; }
    .spp-cm-card__meta b { color:#94a3b8; font-weight:600; font-variant-numeric:tabular-nums; }

    .spp-cm-card__pill {
        flex:0 0 auto; display:inline-flex; align-items:center; gap:5px; padding:3px 9px;
        border-radius:999px; font-size:10.5px; font-weight:600; white-space:nowrap;
        background:rgba(16,185,129,.12); color:#10b981;
    }
    .spp-cm-card__pill.is-idle { background:rgba(100,116,139,.16); color:#94a3b8; }
    .spp-cm-card__dot { width:6px; height:6px; border-radius:50%; background:currentColor; }
    .spp-cm-card__pill:not(.is-idle) .spp-cm-card__dot { animation:spp-cm-pulse 1.8s ease-in-out infinite; }
    @keyframes spp-cm-pulse { 0%,100% { opacity:1; transform:scale(1); } 50% { opacity:.35; transform:scale(.8); } }

    .spp-cm-card__stats {
        display:grid; grid-template-columns:1fr 1fr 1.15fr; border-radius:10px; overflow:hidden;
        background:rgba(15,23,42,.6); border:1px solid rgba(148,163,184,.1);
    }
    .spp-cm-card__stat { display:flex; flex-direction:column; gap:2px; padding:9px 11px; position:relative; }
    .spp-cm-card__stat + .spp-cm-card__stat::before {
        content:""; position:absolute; left:0; top:18%; bottom:18%; width:1px; background:rgba(148,163,184,.14);
    }
    .spp-cm-card__stat .k { font-size:10.5px; color:#64748b; letter-spacing:.02em; }
    /* 数值栏:.v 一律不折行(数量级大的板块如「汉化本发布」2,588,429 会撑爆 72px 内容宽);
       超过宽度时由 buildCmCard 给 .spp-cm-card__stats 挂 .is-wide 缩字号,
       而不是让数字断成两行 */
    .spp-cm-card__stat .v { font-size:16px; font-weight:700; color:#fff; font-variant-numeric:tabular-nums; letter-spacing:-.01em; white-space:nowrap; }
    .spp-cm-card__stats.is-wide .spp-cm-card__stat { padding:9px 8px; }
    .spp-cm-card__stats.is-wide .spp-cm-card__stat .v { font-size:13px; letter-spacing:-.02em; }
    .spp-cm-card__stat .v small { font-size:11px; font-weight:600; color:#94a3b8; margin-left:2px; }
    .spp-cm-card__stat .d { font-size:10px; color:#64748b; }
    .spp-cm-card__stat.is-hot .v { color:#10b981; }
    .spp-cm-card__stat.is-hot .d { color:#10b981; opacity:.75; }

    .spp-cm-card__boards { display:flex; flex-direction:column; gap:8px; }
    .spp-cm-card__boards-k { font-size:10.5px; color:#64748b; letter-spacing:.06em; display:flex; align-items:center; gap:8px; }
    .spp-cm-card__boards-k::after { content:""; flex:1; height:1px; background:rgba(148,163,184,.14); }

    .spp-cm-card__board {
        display:flex; align-items:center; gap:10px; padding:9px 11px; border-radius:10px;
        background:rgba(30,41,59,.5); border:1px solid rgba(148,163,184,.1); text-decoration:none !important;
        transition:background .18s ease,border-color .18s ease,transform .18s ease;
    }
    .spp-cm-card__board:hover { background:rgba(56,126,240,.16); border-color:rgba(56,126,240,.45); transform:translateX(2px); }
    .spp-cm-card__board-ico {
        flex:0 0 30px; width:30px; height:30px; border-radius:8px;
        display:flex; align-items:center; justify-content:center;
        background:rgba(56,126,240,.16); color:#93c5fd;
    }
    .spp-cm-card__board.is-wall .spp-cm-card__board-ico { background:rgba(255,34,136,.16); color:#f9a8d4; }
    .spp-cm-card__board-ico svg { width:15px; height:15px; }
    .spp-cm-card__board-txt { flex:1 1 auto; min-width:0; }
    .spp-cm-card__board-txt b { display:block; font-size:12.5px; font-weight:600; color:#e2e8f4; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
    .spp-cm-card__board-txt i { display:block; font-style:normal; font-size:10.5px; color:#64748b; margin-top:1px; }
    .spp-cm-card__board-go { flex:0 0 auto; color:#64748b; display:flex; }
    .spp-cm-card__board-go svg { width:13px; height:13px; }
    .spp-cm-card__board:hover .spp-cm-card__board-go { color:#c7d2fe; }

    .spp-cm-card__last {
        display:flex; align-items:center; gap:10px; padding:10px 11px; border-radius:10px;
        background:linear-gradient(90deg,rgba(56,126,240,.12),rgba(2,6,23,0));
        border-left:2px solid rgba(56,126,240,.6); text-decoration:none !important;
    }
    .spp-cm-card__last:hover { background:linear-gradient(90deg,rgba(56,126,240,.22),rgba(2,6,23,0)); }
    .spp-cm-card__avatar {
        flex:0 0 26px; width:26px; height:26px; border-radius:50%;
        display:flex; align-items:center; justify-content:center;
        font-size:11px; font-weight:700; color:#fff;
        background:linear-gradient(145deg,#387ef0,#ff2288);
    }
    .spp-cm-card__last-txt { flex:1 1 auto; min-width:0; }
    .spp-cm-card__last-txt b { display:block; font-size:12px; font-weight:500; color:#dbe4f2; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
    .spp-cm-card__last-txt i { display:block; font-style:normal; font-size:10.5px; color:#64748b; margin-top:2px; }
    .spp-cm-card__last-time {
        flex:0 0 auto; display:flex; flex-direction:column; align-items:flex-end; gap:2px;
        font-size:10.5px; color:#94a3b8; font-variant-numeric:tabular-nums;
    }
    .spp-cm-card__last-time b { font-size:11px; color:#cbd5e1; font-weight:600; }
    .spp-cm-card__last-time.is-fresh b { color:#10b981; }

    /* margin-top:auto 把底部条推到卡片最下沿:同排卡片高度不一时(只有 1 个入口的短卡),
       按钮仍对齐在底部,而不是浮在内容后面留一片空白 */
    .spp-cm-card__foot { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-top:auto; padding-top:2px; }
    .spp-cm-card__cta {
        flex:0 0 auto; display:inline-flex; align-items:center; gap:4px; padding:5px 11px; border-radius:8px;
        background:linear-gradient(90deg,#387ef0,#a855f7); color:#fff !important;
        font-size:11px !important; font-weight:600 !important; text-decoration:none !important;
        transition:filter .2s ease;
    }
    .spp-cm-card__cta:hover { filter:brightness(1.15); }
    .spp-cm-card__cta svg { width:12px; height:12px; }

    .spp-cm-card__subs { display:flex; flex-wrap:wrap; gap:6px; min-width:0; }
    .spp-cm-card__subs a {
        font-size:10.5px !important; padding:3px 8px; border-radius:6px;
        background:rgba(51,65,85,.55); color:#cbd5e1 !important; text-decoration:none !important;
        transition:background .16s ease,color .16s ease;
    }
    .spp-cm-card__subs a:hover { background:rgba(99,102,241,.4); color:#fff !important; }

    /* ==================================================================
       渐变描边按钮 · 移植自 Uiverse / nima-mollazadeh (terrible-panda-97, MIT)
       机制:本体铺 45° 渐变,::before 用 inset:1px 的深色内板盖住中间 →
             只露出 1px 渐变边;hover 时内板半透明 + ::after 模糊辉光
       ================================================================== */
    .spp-uvbtn {
        position:relative !important; isolation:isolate;
        background:linear-gradient(45deg,#0ce39a,#69007f,#fc0987) !important;
        border:none !important; border-radius:11px !important;
        box-shadow:0 8px 22px -14px rgba(105,0,127,.85);
        overflow:visible;
    }
    .spp-uvbtn::before {
        content:""; position:absolute; inset:1px; border-radius:10px;
        background:#272727; z-index:-1; pointer-events:none;
        transition:opacity .5s ease;
    }
    .spp-uvbtn:hover::before { opacity:.72; }
    .spp-uvbtn::after {
        content:""; position:absolute; inset:0; border-radius:10px;
        background:linear-gradient(45deg,#0ce39a,#69007f,#fc0987);
        opacity:0; filter:blur(20px); z-index:-2; pointer-events:none;
        transition:opacity .5s ease;
    }
    .spp-uvbtn:hover::after { opacity:1; }
    .spp-uvbtn > * { position:relative; z-index:1; }

    /* 漫区特设 分类标题行套用上面的按钮样式(覆盖 CSS_BOARDS 里的浅色底) */
    html.spp-hm-on .t > table > tbody > tr > th.h.spp-uvbtn {
        background:linear-gradient(45deg,#0ce39a,#69007f,#fc0987) !important;
        border-bottom:none !important;
        padding:10px 14px !important;
    }
    .spp-hm-on .t > table > tbody > tr > th.h.spp-uvbtn h2 a.cfont {
        color:#fff !important; text-shadow:0 1px 6px rgba(0,0,0,.55);
    }
    .spp-hm-on .t > table > tbody > tr > th.h.spp-uvbtn h2 a.cfont::before {
        background:linear-gradient(180deg,#0ce39a,#fc0987) !important;
    }
    .spp-hm-on .t > table > tbody > tr > th.h.spp-uvbtn a.closeicon img { filter:invert(1); opacity:.7; }
    .spp-hm-on .t > table > tbody > tr > th.h.spp-uvbtn a.closeicon:hover img { opacity:1; }

    /* ==================================================================
       存档分区(旧物仓库):独立标题条 + 子版小卡网格
       ================================================================== */
    .spp-cm-sec { display:flex; flex-direction:column; gap:14px; }
    .spp-cm-sec__hd {
        display:flex; align-items:center; gap:12px; padding:11px 16px; flex-wrap:wrap;
    }
    .spp-cm-sec__bar {
        width:3px; height:15px; border-radius:2px; flex:0 0 auto;
        background:linear-gradient(180deg,#0ce39a,#fc0987);
    }
    .spp-cm-sec__title {
        font-size:14.5px !important; font-weight:700 !important;
        color:#fff !important; text-decoration:none !important;
        text-shadow:0 1px 6px rgba(0,0,0,.55);
    }
    .spp-cm-sec__title:hover { color:#a7f3d0 !important; }
    .spp-cm-sec__meta {
        font-size:11.5px; color:#cbd5e1; font-variant-numeric:tabular-nums;
    }
    .spp-cm-sec__count {
        margin-left:auto; font-size:11px; font-weight:600; padding:3px 10px; border-radius:999px;
        background:rgba(12,227,154,.14); color:#6ee7b7;
    }

    .spp-cm-sec__grid {
        display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:10px;
    }
    .spp-cm-mini {
        position:relative; display:flex; align-items:center; padding:10px 14px 10px 16px;
        border-radius:10px; overflow:hidden;
        /* 芯片直接坐在论坛的浅色内容区(#fafcff)上,底色必须不透明 ——
           否则半透明深色会被浅底冲成中灰,和深色预览页对不上 */
        background:#141d2f; border:1px solid rgba(148,163,184,.24);
        text-decoration:none !important;
        transition:border-color .18s ease, background .18s ease, transform .18s ease;
    }
    .spp-cm-mini::before {
        content:""; position:absolute; left:0; top:0; bottom:0; width:3px;
        background:linear-gradient(180deg,#0ce39a,#69007f,#fc0987);
        opacity:.8; transition:opacity .18s ease;
    }
    .spp-cm-mini:hover {
        background:#1d2a44; border-color:rgba(12,227,154,.55); transform:translateY(-2px);
    }
    .spp-cm-mini:hover::before { opacity:1; }
    .spp-cm-mini__nm {
        font-size:13px; font-weight:600; color:#e2e8f4; letter-spacing:.01em;
        white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
    }

    /* ==================================================================
       友情链接卡片 · 移植自 uiverse.io/gharsh11032000/tasty-frog-49
       Copyright - 2026 gharsh11032000 (Harsh Gupta) — MIT License
       机制:本体深色底;::before 铺 45° 渐变、比本体大 5px 一圈 → 只露出渐变边;
             hover 时该渐变块旋转 90° 并拉伸;::after 同色系模糊块,hover 时
             blur 20→30px 形成外发光。
       ★ 本体不能设 isolation / overflow:hidden —— ::before 用 z-index:-10,
         一裁就只剩普通描边方块、动效全丢。
       ★★ hover 变换的 scaleX/scaleY 必须按实测尺寸算,不能照抄原版的 1.34/0.77:
          原版卡 190×254,::before 为 inset:-5px 后约 200×264。rotate(-90deg) 会把
          宽高对调,所以要先压宽、拉高,旋转后才回到「与卡片同形」。反算公式:
              sy = k·卡宽/(卡高+10) ,sx = k·卡高/(卡宽+10) ,k = 1.06
          代回原版 (190,254):sx = 1.346 / sy = 0.763 —— 正是原版的 1.34 / 0.77(误差<1%),
          说明这就是原作者当初的算法。而我们的横版面板代进去是 sx≈0.49 / sy≈2.19;
          若照抄 1.34/0.77,旋转后会得到 292×1061 的竖长条、溢出整屏(已实测复现)。
          数值由 syncFriendCardScale() 写入 --spp-fl-sx / --spp-fl-sy。
       ================================================================== */
    .spp-fl-card {
        position:relative; background:#0b0f1a; border-radius:8px; cursor:default;
        display:flex; flex-direction:column; gap:12px; padding:14px 16px 16px;
    }
    .spp-fl-card::before {
        content:''; position:absolute; inset:-5px; border-radius:10px;
        background:linear-gradient(-45deg,#e81cff 0%,#40c9ff 100%);
        z-index:-10; pointer-events:none;
        transition:all .6s cubic-bezier(.175,.885,.32,1.275);
    }
    .spp-fl-card::after {
        content:""; position:absolute; inset:0; border-radius:8px;
        background:linear-gradient(-45deg,#fc00ff 0%,#00dbde 100%);
        transform:translate3d(0,0,0) scale(.95); filter:blur(20px);
        z-index:-1;
    }
    .spp-fl-card:hover::after { filter:blur(30px); }
    .spp-fl-card:hover::before {
        transform:rotate(-90deg) scale(var(--spp-fl-sx,1.34), var(--spp-fl-sy,.77));
    }

    .spp-fl-head { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
    .spp-fl-head .ttl { font-size:20px; font-weight:700; color:#fff; letter-spacing:.01em; }
    .spp-fl-head .cnt {
        font-size:11px; font-weight:600; padding:2px 9px; border-radius:999px;
        background:rgba(232,28,255,.16); color:#f0abfc;
    }
    .spp-fl-head .hint { font-size:11.5px; color:#64748b; margin-left:auto; }

    /* 网格列数:紧凑 142px / 舒适 196px */
    .spp-fl-grid { display:grid; gap:7px; grid-template-columns:repeat(auto-fill,minmax(142px,1fr)); }
    .spp-fl-card.is-loose .spp-fl-grid { gap:10px; grid-template-columns:repeat(auto-fill,minmax(196px,1fr)); }

    .spp-fl-item {
        display:flex; align-items:center; gap:9px; text-decoration:none !important;
        padding:9px 11px; border-radius:9px; min-width:0;
        background:rgba(30,41,59,.55); border:1px solid rgba(148,163,184,.12);
        transition:background .18s ease, border-color .18s ease, transform .18s ease;
    }
    .spp-fl-item:hover {
        background:rgba(232,28,255,.14); border-color:rgba(232,28,255,.45);
        transform:translateY(-2px);
    }
    .spp-fl-dot {
        flex:0 0 auto; width:24px; height:24px; border-radius:7px;
        display:grid; place-items:center; font-size:11px; font-weight:800; color:#fff;
        background:linear-gradient(145deg,#e81cff,#40c9ff);
        box-shadow:0 4px 12px -5px rgba(232,28,255,.8);
    }
    .spp-fl-txt { min-width:0; display:flex; flex-direction:column; gap:1px; }
    .spp-fl-txt b {
        font-size:12.5px; font-weight:600; color:#fff;
        white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
    }
    .spp-fl-txt i {
        font-style:normal; font-size:10.5px; color:#64748b;
        white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
    }
    .spp-fl-item:hover .spp-fl-txt i { color:#a5b4fc; }

    /* .spp-fl-foot(卡片底下的uiverse署名脚注)在 2.52.97 随那段文本一起删除 —— 界面要干净,
       MIT 声明留在注释里(见 CSS 段头与 buildFriendCard 上方)。 */

    /* 方案A:人民囧府区块内两栏。
       align-items:stretch 让右栏友链卡与左栏事务卡底边对齐
       (实测 782×369 时两卡等高 369px;用 start 会错开一条边)。
       .spp-cm-card__inner 自带 flex:1,能自然吃下多出的高度。 */
    .spp-fl-two { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1.15fr); gap:16px; align-items:stretch; }
    .spp-fl-two > * { min-width:0; }
    .spp-fl-two > .spp-cm-grid { display:flex; flex-direction:column; }
    .spp-fl-two > .spp-cm-grid > .spp-cm-card { flex:1 1 auto; }
    @media (max-width:1000px) { .spp-fl-two { grid-template-columns:minmax(0,1fr); } }
    /* ★ 站点全局 CSS 给 .t 加了 overflow:hidden!important(见 CSS_BASE 里的
       「.t,.t5,table.t_one{…overflow:hidden!important…}」那条),会把 ::before 那圈
       渐变与 ::after 的辉光在卡片边缘裁掉。这两条把它放开(仅友链卡所在宿主)。 */
    .spp-hm-on .t.spp-fl-host, .spp-hm-on .t.spp-fl-host > table,
    .spp-hm-on .t.spp-fl-host > table > tbody,
    .spp-hm-on .t.spp-fl-host > table > tbody > tr,
    .spp-hm-on .t.spp-fl-host > table > tbody > tr > td { overflow:visible !important; }
    `;

        // 顶部四栏卡片化:3 列网格(GALGAME汉化区 / 蜜柑计划 / 提问求物),
        // 与茶馆行同高(--sz-card-h),复用 .spp-sc / .spp-lt 皮肤
        const CSS_TOPCARD = `
    .spp-top4 {
        /* ★ 2.52.96:卡片总高 = 内容盒 224 + 上下内边距 28 + 2px 描边 x2 = 256。
           站点是 box-sizing:content-box(实测 body/html 都是),所以高度必须由【槽位】给死;
           槽位与茶馆行共用同一个 --sz-card-h,两行三张卡才是同高(详解见 CSS_SUBCARD 顶注)。 */
        --sz-card-h:256px;
        display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:16px;
        margin:0 0 16px;
    }
    .spp-top4 > .spp-sc { height:var(--sz-card-h); }
    /* ★ 2.53.63(用户截图红框:「红色框选部分需要保持一定距离,太贴近了,看起来像是已经重叠」):
       给宿主一个属于自己的上边距。站点在 #header 末尾放了一块 99px 高、mb:-5px 的广告位,
       广告拦截插件把它收掉之后 #main 就直接顶到信息区下沿 —— 线上实测:
       信息区 bottom = 277、卡片 top = 278,缝隙只有 1px,看着就是叠在一起;
       有广告时那道缝由广告位撑着(97px),所以这条只在装了拦截插件的机器上现形。
       加 16px 之后有没有广告都恒定有呼吸(16 = 本模块卡片间距,同 .spp-top4 的 gap)。
       ★ 为什么不用 #main{padding-top}:那会连版块页/帖子页的整页首块一起挪,本轮不碰。 */
    html.spp-hm-on .t.spp-top4-host { margin-top:16px !important; }
    .spp-top4 .spp-lt__hd { font-size:14px; color:#f0f5ff; letter-spacing:1.4px; }
    .spp-top4 .spp-lt__hd em { display:none; }
    .spp-top4 .spp-lt__hd a.spp-top4__hd {
        color:#f0f5ff !important; text-decoration:none !important;
        transition:color .16s ease;
    }
    .spp-top4 .spp-lt__hd a.spp-top4__hd:hover { color:#0ce39a !important; }
    .spp-top4 .spp-lt__list a { height:29px; font-size:13.5px; }

    /* ==================================================================
       蜜柑计划卡:Uiverse / ElSombrero2 / tricky-robin-67 —— 3D 翻转
       原版卡为 190x254 竖版;这里按版面改成与同排卡片一致的横版(由 .spp-top4 定高)。
       默认显示「背面」(.spp-mk__back:黑白小电视 + 旋转橙带),hover 翻出错面(信息面)。
       ================================================================== */
    .spp-sc--mikan { perspective:900px; }
    .spp-mk {
        position:absolute; inset:0;
        transform-style:preserve-3d;
        transition:transform 300ms;
        box-shadow:0 0 10px 1px #000000ee;
        border-radius:20px;
    }
    .spp-sc--mikan:hover .spp-mk { transform:rotateY(180deg); }
    .spp-mk__back, .spp-mk__front {
        position:absolute; inset:0; overflow:hidden; border-radius:20px;
        background-color:#151515;
        backface-visibility:hidden; -webkit-backface-visibility:hidden;
    }
    /* ---- 第一面(默认可见):旋转的橙色光带 + 黑白小电视 ---- */
    .spp-mk__back { display:grid; place-items:center; }
    .spp-mk__back::before {
        position:absolute; content:''; display:block;
        width:46%; height:300%;
        background:linear-gradient(90deg,transparent,#ff9966,#ff9966,#ff9966,#ff9966,transparent);
        animation:sppMkSpin 5000ms infinite linear;
    }
    @keyframes sppMkSpin {
        0%   { transform:rotateZ(0deg); }
        100% { transform:rotateZ(360deg); }
    }
    .spp-mk__core {
        position:absolute; inset:1px; z-index:1; border-radius:19px;
        background-color:#151515;
        display:flex; flex-direction:column; align-items:center; justify-content:center; gap:16px;
        color:#fff;
    }
    .spp-mk__core strong {
        font-size:15px; font-weight:700; letter-spacing:3px; color:#f4f4f4;
        text-shadow:0 1px 3px rgba(0,0,0,.75);
    }
    /* ---- 第二面(翻转后):信息面 ---- */
    .spp-mk__front { transform:rotateY(180deg); color:#fff; }
    .spp-mk__orbs { position:absolute; inset:0; z-index:1; pointer-events:none; }
    .spp-mk__orbs span {
        position:absolute; border-radius:50%; filter:blur(15px);
        animation:sppMkFloat 2600ms infinite linear;
    }
    .spp-mk__orbs .c1 { width:120px; height:120px; background:#ffbb66; left:10%;  top:46%; }
    .spp-mk__orbs .c2 { width:44px;  height:44px;  background:#ff2233; right:13%; top:8%;  animation-delay:-1800ms; }
    .spp-mk__orbs .c3 { width:150px; height:150px; background:#ff8866; left:32%;  top:88%; animation-delay:-800ms; }
    @keyframes sppMkFloat {
        0%   { transform:translateY(0); }
        50%  { transform:translateY(10px); }
        100% { transform:translateY(0); }
    }
    .spp-mk__frontcore {
        position:relative; z-index:2; height:100%; padding:14px 16px;
        box-sizing:border-box; overflow:hidden;
        display:flex; flex-direction:column; justify-content:space-between; gap:10px;
    }
    .spp-mk__badge {
        align-self:flex-start; flex:0 0 auto;
        background-color:rgba(0,0,0,.34); backdrop-filter:blur(2px);
        padding:3px 11px; border-radius:10px;
        font-size:11px; letter-spacing:1.6px; color:#ffd9c2;
        max-width:100%; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
    }
    .spp-mk__desc {
        flex:0 1 auto; min-height:0;
        background-color:rgba(0,0,0,.62); backdrop-filter:blur(5px);
        border-radius:10px; padding:10px 13px;
        box-shadow:0 0 10px 5px rgba(0,0,0,.53);
    }
    .spp-mk__title {
        display:flex; align-items:center; justify-content:space-between; gap:10px;
        font-size:13px; font-weight:700; color:#fff; line-height:1.3;
    }
    .spp-mk__title span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
    .spp-mk__title svg { flex:0 0 auto; fill:#20c997; }
    .spp-mk__foot {
        margin:5px 0 0; font-size:11px; line-height:1.45;
        color:rgba(255,255,255,.66);
        /* 最多两行,超出省略 —— 简介可能很长,绝不能撑破卡片 */
        display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
        overflow:hidden;
    }
    /* 外链提示行(蜜柑计划指向站外站点) */
    .spp-mk__foot--host {
        display:block; margin-top:3px; -webkit-line-clamp:none;
        color:rgba(255,153,102,.82); letter-spacing:.4px;
    }
    /* 整卡热区:盖住两面,点哪都能进板块 */
    .spp-mk__link { position:absolute; inset:0; z-index:300; display:block; }

    /* ---- 黑白小电视图标:默认 19px;蜜柑卡第一面放大到 64px ---- */
    .spp-bili {
        flex:0 0 auto; width:19px; height:19px;
        fill:none; stroke:currentColor; stroke-width:1.7;
        stroke-linecap:round; stroke-linejoin:round;
        color:#f4f4f4; vertical-align:middle;
        filter:drop-shadow(0 0 5px rgba(255,255,255,.22));
        transition:color .16s ease, filter .16s ease;
    }
    .spp-mk__core .spp-bili {
        width:64px; height:64px; stroke-width:1.5;
        filter:drop-shadow(0 0 9px rgba(255,255,255,.28));
    }
    .spp-sc--mikan:hover .spp-bili { color:#fff; filter:drop-shadow(0 0 8px rgba(255,255,255,.5)); }
    `;

        // 屏蔽公告栏:公告条本身是 #main 的第一个 div.bdbA(内含 #breadcrumbs)。
        // 它是 #main 的直系子元素,不参与版块表格布局,整条隐藏不影响后续版块。
        const CSS_HIDE_NOTICE = `
    #main > .bdbA { display:none !important; }
    `;

        // 屏蔽顶部横幅:即 #header 里第一个 <table>(td.banner 内的季节 logo + 淘宝广告位)。
        // 注意 #header 是个大容器,里面还挂着 #mainNav 导航条(28px) 与 #infobox 统计条(203px),
        // 所以只能精确命中它的 table 子元素,不能整个 display:none。
        const CSS_HDR = `
    #header > table { display:none !important; }
    `;

        // ---------- 卡片区去白底 ----------
        // 星空模块给「容器」铺了浅色毛玻璃底(html.spp-hm-star #main .t 那条),
        // 深色卡片嵌在里面会露出一圈浅边。这里把承载卡片的容器改透明,让星空直接透出。
        // ★ 坑:#t_39(漫区特设)的父级是 .contentwrap,【不是】#main,
        //   所以必须用后代选择器 #main div.t,写成 #main > div.t 匹配不到它。
        // 只在首页、且卡片区确实挂载时生效(html.spp-hm-on 由 applyHomeStyle 加),
        // 避免影响帖子页等其他页面的页头观感。
        const CSS_CLEARBG = `
    html.spp-hm-on.spp-hm-star #header {
        background:transparent !important;
        backdrop-filter:none !important;
        -webkit-backdrop-filter:none !important;
        box-shadow:none !important;
    }
    /* 顶部三栏卡片的宿主:去掉白底与投影,网格自己撑起视觉 */
    html.spp-hm-on.spp-hm-star #main div.t:has(> .spp-top4) {
        background:transparent !important;
        border-color:transparent !important;
        box-shadow:none !important;
    }
    /* 漫区特设 #t_39:CM 卡片 + 常设大区卡 + 旧物仓库存档区都在它内部,
       去掉这块底板,卡片直接坐在星空上(旧物仓库那一段也随之干净) */
    html.spp-hm-on.spp-hm-star #main div.t:has(#cate_39) {
        background:transparent !important;
        border-color:transparent !important;
        box-shadow:none !important;
    }
    /* 综合交流 #t_7:ACG交流等 6 个板块成卡后,这块底板同样去掉。
       注意它的宿主是 #t_7,父级是 .contentwrap z(与 #t_39 同),
       所以仍须用后代选择器 #main div.t,写成 #main > div.t 匹配不到 */
    html.spp-hm-on.spp-hm-star #main div.t:has(#cate_7) {
        background:transparent !important;
        border-color:transparent !important;
        box-shadow:none !important;
    }
    /* 人民囧府 #t_1:事务受理成卡后同样去掉底板(宿主 #t_1,父级也是 .contentwrap z) */
    html.spp-hm-on.spp-hm-star #main div.t:has(#cate_1) {
        background:transparent !important;
        border-color:transparent !important;
        box-shadow:none !important;
    }
    /* :has() 兜底:老浏览器不支持时用脚本给宿主加的这些 class */
    html.spp-hm-on.spp-hm-star #main div.t.spp-top4-host,
    html.spp-hm-on.spp-hm-star #main div.t.spp-clear-host {
        background:transparent !important;
        border-color:transparent !important;
        box-shadow:none !important;
    }
    `;

        function applyHomeStyle() {
            let el = document.getElementById(STYLE_ID);
            if (!el) {
                el = document.createElement('style');
                el.id = STYLE_ID;
                appendToHeadSafe(el);
            }
            el.textContent = (onHdr ? CSS_HDR : '') +
                             (onHideNotice ? CSS_HIDE_NOTICE : '') +
                             (onStats ? CSS_STATS : '') +
                             (onUPanel ? CSS_UPANEL : '') +
                             ((onTopCard || onSub || onCard || onHjCard || onRmCard) ? CSS_CLEARBG : '') +
                             (onNotice ? CSS_NOTICE : '') +
                             (onBoards ? CSS_BOARDS : '') + ((onCard || onFlCard) ? CSS_CARD : '') +
                             (onTopCard ? CSS_TOPCARD : '') +
                             ((onSub || onTopCard) ? CSS_SUBCARD : '');
            if (onBoards || onCard || onSub || onTopCard || onUPanel || onHjCard || onRmCard) document.documentElement.classList.add('spp-hm-on');
        }

        // ==================================================================
        // 子版卡片区(首页 · 插在 #content > .contentwrap 之首 → 漫区特设之前)
        // ==================================================================
        const CSS_SUBCARD = `
    /* ---- 外层容器:铺满内容区宽度 ---- */
    .spp-sz { margin:0 0 8px !important; }
    .spp-sz-group { margin-bottom:20px; }
    .spp-sz-group:last-child { margin-bottom:0; }

    /* ---- 大板块标题条(沿用渐变描边按钮 .spp-uvbtn) ---- */
    .spp-sz-hd {
        position:relative; isolation:isolate;
        display:flex; align-items:center; gap:14px;
        background:linear-gradient(45deg,#0ce39a,#69007f,#fc0987) !important;
        border-radius:11px; padding:9px 16px; margin-bottom:14px;
        box-shadow:0 8px 22px -14px rgba(105,0,127,.85);
    }
    .spp-sz-hd::before {
        content:""; position:absolute; inset:1px; border-radius:10px;
        background:#272727; z-index:-1; pointer-events:none; transition:opacity .5s ease;
    }
    .spp-sz-hd:hover::before { opacity:.72; }
    .spp-sz-hd__t {
        position:relative; z-index:1; margin:0; font-size:17px; font-weight:700;
        color:#fff !important; text-decoration:none !important;
        text-shadow:0 1px 6px rgba(0,0,0,.55);
    }
    .spp-sz-hd__meta {
        margin-left:auto; position:relative; z-index:1;
        font-size:12px; letter-spacing:.6px; color:rgba(255,255,255,.55); white-space:nowrap;
    }
    .spp-sz-hd__meta b { color:#0ce39a; font-weight:700; }
    .spp-sz-hd__cnt {
        position:relative; z-index:1; font-size:11px; letter-spacing:2px;
        color:rgba(255,255,255,.4); padding-left:12px;
        border-left:1px solid rgba(255,255,255,.16);
    }

    /* ---- 子版卡片网格(默认 5 列) ---- */
    .spp-sz-grid {
        --scols:5;
        display:grid; grid-template-columns:repeat(var(--scols),1fr); gap:16px;
    }
    /* ★ 2.52.96:用户截图标注的三条,两条出在「高度」、一条出在「让位」,都在这张 flex 行上。
       ①「卡片扩展时空间不足」:.spp-tc:hover 是 transform:scale(1.1),transform 不参与布局
         —— 放大量只能由两侧卡片预先让出。放大量 = 460 × 0.1 = 46px、左右各 23px,
         而本行 gap 只有 16px ⇒ 每侧实打实压进邻卡 7px。这里把让位量算成 --tea-bleed 交给
         本行的 gap(两侧卡片因此各窄 10px,正是用户要的「缩短左右两侧卡片的宽度」)。
       ②③「上下卡片应一致 / 中心卡与左右卡中心对齐」:站点是 box-sizing:content-box(实测
         body/html 都是),.spp-lt 的 height:100% 只算内容盒 ⇒ 实高 220+28+4 = 252,
         而槽位(.spp-sc)只有 220 ⇒ 侧卡比中间卡高 32px,多出的那截正好被下一组标题条
         (.spp-sz-hd)盖住,圆角就看不见了(真实浏览器实测:顶行卡底 565 > 下一组顶 553)。
         修法:.spp-lt 统一 border-box + 槽位与中间卡一起抬到 --sz-card-h ⇒ 三卡上下齐平。 */
    .spp-sz-group.is-center .spp-sz-grid {
        --sz-card-h:256px;      /* 同 .spp-top4:内容盒 224 + 上下内边距 28 + 描边 4 */
        --tea-w:460px;          /* 茶馆卡宽:卡与槽位同源,改一处就够 */
        --tea-grow:1.1;         /* 必须与 .spp-tc:hover 的 scale 一致 */
        /* 悬停放大后左右各多占 (460 × 0.1) / 2 = 23px,再留 3px 细缝 */
        --tea-bleed:calc(var(--tea-w) * (var(--tea-grow) - 1) / 2 + 3px);
        display:flex; justify-content:center;
        /* ★ 让位必须做在【兄弟间距】上,不能只做容器内边距:实测内边距只把两侧卡片压窄,
           茶馆卡与邻卡之间仍是那条 16px 的 gap,放大后照样压进去 7px(探针实测)。
           这里直接让 gap = --tea-bleed(26px):放大后仍剩 3px 细缝,两侧卡片各窄 10px。 */
        gap:var(--tea-bleed);
        /* 上下 16px:给放大后的 12.8px 纵向溢出留余量(原来 14px 只剩 1px) */
        padding:16px 0;
    }
    .spp-sz-group.is-center .spp-sz-grid > .spp-sc:not(.spp-sc--tea):not(.spp-latest) {
        width:calc((100% - (var(--scols) - 1) * 16px) / var(--scols)); flex:0 0 auto;
    }

    /* ---- 普通子版卡:Uiverse / 00Kubi / cowardly-eagle-56 ---- */
    .spp-sc {
        position:relative; display:block; height:172px;
        text-decoration:none !important; color:inherit;
        transition:200ms;
        -webkit-touch-callout:none; -webkit-user-select:none;
        -moz-user-select:none; -ms-user-select:none; user-select:none;
    }
    .spp-sc:active { filter:brightness(.94); }
    .spp-sc__canvas {
        position:absolute; inset:0; z-index:200; perspective:800px;
        display:grid;
        grid-template-columns:1fr 1fr 1fr 1fr 1fr;
        grid-template-rows:1fr 1fr 1fr 1fr 1fr; gap:0 0;
        grid-template-areas:
            "a1 a2 a3 a4 a5"
            "b1 b2 b3 b4 b5"
            "c1 c2 c3 c4 c5"
            "d1 d2 d3 d4 d5"
            "e1 e2 e3 e4 e5";
    }
    .spp-sc__tr { position:absolute; z-index:200; width:100%; height:100%; }
    .spp-sc__tr:nth-child(1)  { grid-area:a1; --rx:20deg;  --ry:-10deg; }
    .spp-sc__tr:nth-child(2)  { grid-area:a2; --rx:20deg;  --ry:-5deg;  }
    .spp-sc__tr:nth-child(3)  { grid-area:a3; --rx:20deg;  --ry:0deg;   }
    .spp-sc__tr:nth-child(4)  { grid-area:a4; --rx:20deg;  --ry:5deg;   }
    .spp-sc__tr:nth-child(5)  { grid-area:a5; --rx:20deg;  --ry:10deg;  }
    .spp-sc__tr:nth-child(6)  { grid-area:b1; --rx:10deg;  --ry:-10deg; }
    .spp-sc__tr:nth-child(7)  { grid-area:b2; --rx:10deg;  --ry:-5deg;  }
    .spp-sc__tr:nth-child(8)  { grid-area:b3; --rx:10deg;  --ry:0deg;   }
    .spp-sc__tr:nth-child(9)  { grid-area:b4; --rx:10deg;  --ry:5deg;   }
    .spp-sc__tr:nth-child(10) { grid-area:b5; --rx:10deg;  --ry:10deg;  }
    .spp-sc__tr:nth-child(11) { grid-area:c1; --rx:0deg;   --ry:-10deg; }
    .spp-sc__tr:nth-child(12) { grid-area:c2; --rx:0deg;   --ry:-5deg;  }
    .spp-sc__tr:nth-child(13) { grid-area:c3; --rx:0deg;   --ry:0deg;   }
    .spp-sc__tr:nth-child(14) { grid-area:c4; --rx:0deg;   --ry:5deg;   }
    .spp-sc__tr:nth-child(15) { grid-area:c5; --rx:0deg;   --ry:10deg;  }
    .spp-sc__tr:nth-child(16) { grid-area:d1; --rx:-10deg; --ry:-10deg; }
    .spp-sc__tr:nth-child(17) { grid-area:d2; --rx:-10deg; --ry:-5deg;  }
    .spp-sc__tr:nth-child(18) { grid-area:d3; --rx:-10deg; --ry:0deg;   }
    .spp-sc__tr:nth-child(19) { grid-area:d4; --rx:-10deg; --ry:5deg;   }
    .spp-sc__tr:nth-child(20) { grid-area:d5; --rx:-10deg; --ry:10deg;  }
    .spp-sc__tr:nth-child(21) { grid-area:e1; --rx:-20deg; --ry:-10deg; }
    .spp-sc__tr:nth-child(22) { grid-area:e2; --rx:-20deg; --ry:-5deg;  }
    .spp-sc__tr:nth-child(23) { grid-area:e3; --rx:-20deg; --ry:0deg;   }
    .spp-sc__tr:nth-child(24) { grid-area:e4; --rx:-20deg; --ry:5deg;   }
    .spp-sc__tr:nth-child(25) { grid-area:e5; --rx:-20deg; --ry:10deg;  }
    .spp-sc__tr:hover { cursor:pointer; }
    .spp-sc__card {
        position:absolute; inset:0; z-index:0;
        display:flex; justify-content:center; align-items:center;
        border-radius:20px; transition:700ms;
        background:linear-gradient(45deg,#1a1a1a,#262626);
        border:2px solid rgba(255,255,255,.1);
        overflow:hidden;
        box-shadow:0 0 20px rgba(0,0,0,.3), inset 0 0 20px rgba(0,0,0,.2);
    }
    .spp-sc__card::before {
        content:""; position:absolute; left:50%; top:50%;
        width:150%; height:150%;
        transform:translate(-50%,-50%);
        background:radial-gradient(circle at center,
            rgba(0,255,170,.1) 0%, rgba(0,162,255,.05) 50%, transparent 100%);
        filter:blur(20px); opacity:0; transition:opacity .3s ease;
    }
    .spp-sc__tr:hover ~ .spp-sc__card {
        transition:125ms ease-in-out;
        transform:rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg));
        filter:brightness(1.1);
    }
    .spp-sc__tr:hover ~ .spp-sc__card::before { opacity:1; }
    .spp-sc__tr:hover ~ .spp-sc__card .spp-sc__glows > div { opacity:1; }
    .spp-sc__tr:hover ~ .spp-sc__card .spp-sc__particles span { animation:sppScFloat 2s infinite; }
    .spp-sc__tr:hover ~ .spp-sc__card .spp-sc__name { transform:translateY(-4px); }
    .spp-sc__body {
        position:relative; width:100%; height:100%;
        padding:16px 18px 12px;
        display:flex; flex-direction:column; align-items:center; justify-content:center;
    }
    .spp-sc__glare {
        position:absolute; inset:0; pointer-events:none; opacity:0; transition:opacity 300ms;
        background:linear-gradient(125deg,
            rgba(255,255,255,0) 0%, rgba(255,255,255,.05) 45%,
            rgba(255,255,255,.1) 50%, rgba(255,255,255,.05) 55%, rgba(255,255,255,0) 100%);
    }
    .spp-sc:hover .spp-sc__glare { opacity:1; }
    .spp-sc__lines span {
        position:absolute; pointer-events:none;
        background:linear-gradient(90deg,transparent,rgba(92,103,255,.2),transparent);
    }
    .spp-sc__lines span:nth-child(1) { top:20%; left:0;  width:100%; height:1px; transform:scaleX(0); transform-origin:left;  animation:sppScLine 3s linear infinite; }
    .spp-sc__lines span:nth-child(2) { top:40%; right:0; width:100%; height:1px; transform:scaleX(0); transform-origin:right; animation:sppScLine 3s linear infinite 1s; }
    .spp-sc__lines span:nth-child(3) { top:60%; left:0;  width:100%; height:1px; transform:scaleX(0); transform-origin:left;  animation:sppScLine 3s linear infinite 2s; }
    .spp-sc__lines span:nth-child(4) { top:80%; right:0; width:100%; height:1px; transform:scaleX(0); transform-origin:right; animation:sppScLine 3s linear infinite 1.5s; }
    .spp-sc__glows > div {
        position:absolute; pointer-events:none; width:100px; height:100px; border-radius:50%;
        background:radial-gradient(circle at center,rgba(0,255,170,.3) 0%,rgba(0,255,170,0) 70%);
        filter:blur(15px); opacity:0; transition:opacity .3s ease;
    }
    .spp-sc__glows .g1 { top:-20px; left:-20px; }
    .spp-sc__glows .g2 { top:50%; right:-30px; transform:translateY(-50%); }
    .spp-sc__glows .g3 { bottom:-20px; left:30%; }
    .spp-sc__particles span {
        position:absolute; pointer-events:none; width:3px; height:3px;
        background:#00ffaa; border-radius:50%; opacity:0; transition:opacity .3s ease;
    }
    .spp-sc__particles span:nth-child(1) { --x:1;   --y:-1; top:40%; left:20%; }
    .spp-sc__particles span:nth-child(2) { --x:-1;  --y:-1; top:60%; right:20%; }
    .spp-sc__particles span:nth-child(3) { --x:.5;  --y:1;  top:20%; left:40%; }
    .spp-sc__particles span:nth-child(4) { --x:-.5; --y:1;  top:80%; right:40%; }
    .spp-sc__particles span:nth-child(5) { --x:1;   --y:.5; top:30%; left:60%; }
    .spp-sc__particles span:nth-child(6) { --x:-1;  --y:.5; top:70%; right:60%; }
    .spp-sc__corners span {
        position:absolute; pointer-events:none; width:15px; height:15px;
        border:2px solid rgba(92,103,255,.3); transition:all .3s ease;
    }
    .spp-sc__corners span:nth-child(1) { top:10px; left:10px;  border-right:0; border-bottom:0; }
    .spp-sc__corners span:nth-child(2) { top:10px; right:10px; border-left:0;  border-bottom:0; }
    .spp-sc__corners span:nth-child(3) { bottom:10px; left:10px;  border-right:0; border-top:0; }
    .spp-sc__corners span:nth-child(4) { bottom:10px; right:10px; border-left:0;  border-top:0; }
    .spp-sc:hover .spp-sc__corners span {
        border-color:rgba(92,103,255,.8); box-shadow:0 0 10px rgba(92,103,255,.5);
    }
    .spp-sc__scan {
        position:absolute; inset:0; pointer-events:none;
        background:linear-gradient(to bottom,transparent,rgba(92,103,255,.1),transparent);
        transform:translateY(-100%); animation:sppScScan 2s linear infinite;
    }
    @keyframes sppScFloat {
        0%   { transform:translate(0,0); opacity:0; }
        50%  { opacity:1; }
        100% { transform:translate(calc(var(--x,0) * 30px),calc(var(--y,0) * 30px)); opacity:0; }
    }
    @keyframes sppScLine {
        0%   { transform:scaleX(0); opacity:0; }
        50%  { transform:scaleX(1); opacity:1; }
        100% { transform:scaleX(0); opacity:0; }
    }
    @keyframes sppScScan {
        0%   { transform:translateY(-100%); }
        100% { transform:translateY(100%); }
    }
    .spp-sc__name {
        position:relative; z-index:5; margin:0; text-align:center;
        font-size:20px; font-weight:800; letter-spacing:2px; line-height:1.25;
        background:linear-gradient(45deg,#8dffdd,#6fd0ff);
        -webkit-background-clip:text; background-clip:text;
        -webkit-text-fill-color:transparent;
        filter:drop-shadow(0 1px 1px rgba(0,0,0,.95)) drop-shadow(0 0 6px rgba(0,255,170,.22));
        -webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale;
        text-rendering:optimizeLegibility;
        transition:transform 300ms ease-in-out; word-break:break-word;
    }
    .spp-sc.is-long  .spp-sc__name { font-size:16px;   letter-spacing:1px; }
    .spp-sc.is-xlong .spp-sc__name { font-size:13.5px; letter-spacing:.5px; }

    /* ---- 茶馆卡:Uiverse / Smit-Prajapati / spicy-rat-83 ----
       槽位高与左右侧卡同源(--sz-card-h),卡填满槽位(height:100%)⇒ 三张卡同高,
       悬停放大只往 --tea-bleed 让出的空位里长。 */
    .spp-sz-group.is-center .spp-sz-grid > .spp-sc--tea {
        flex:0 0 var(--tea-w); width:var(--tea-w); height:var(--sz-card-h);
    }
    .spp-tc {
        position:relative; width:var(--tea-w); height:100%;
        background:#243137; display:grid; place-content:center;
        border-radius:10px; overflow:hidden; transition:all .5s ease-in-out;
        -webkit-user-select:none; -moz-user-select:none; user-select:none;
    }
    .spp-tc__border {
        position:absolute; inset:0; border:2px solid #bd9f67;
        opacity:0; transform:rotate(10deg); transition:all .5s ease-in-out; pointer-events:none;
    }
    .spp-tc__bottom {
        position:absolute; left:50%; bottom:13px; transform:translateX(-50%);
        font-size:9px; text-transform:uppercase; padding:0 5px 0 8px;
        color:#bd9f67; background:#243137;
        opacity:0; letter-spacing:7px; transition:all .5s ease-in-out;
        white-space:nowrap; pointer-events:none;
    }
    .spp-tc__content { transition:all .5s ease-in-out; }
    .spp-tc__logo {
        height:46px; position:relative; width:48px;
        overflow:hidden; transition:all 1s ease-in-out;
    }
    .spp-tc__logo1, .spp-tc__logo2 {
        height:100%; position:absolute; top:0;
        display:flex; align-items:center; color:#bd9f67; white-space:nowrap;
    }
    .spp-tc__logo1 { left:0; font-size:22px; font-weight:700; letter-spacing:1px;
        transition:opacity .35s ease, transform .5s ease; }
    .spp-tc__logo2 { left:48px; font-size:15px; letter-spacing:1px;
        transition:left .5s ease-in-out; }
    .spp-tc__trail {
        position:absolute; right:0; height:100%; width:100%; opacity:0; pointer-events:none;
    }
    .spp-tc__sub {
        position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
        margin-top:44px; color:#bd9f67; padding-left:8px; font-size:11px;
        opacity:0; letter-spacing:0; transition:all .5s ease-in-out .5s;
        white-space:nowrap; pointer-events:none;
    }
    .spp-tc:hover { border-radius:0; transform:scale(1.1); }
    .spp-tc:hover .spp-tc__logo { width:356px; animation:sppTcFlash 1s ease-in-out; }
    .spp-tc:hover .spp-tc__logo1 { opacity:0; transform:translateX(-14px); }
    .spp-tc:hover .spp-tc__logo2 { left:0; }
    .spp-tc:hover .spp-tc__border { inset:15px; opacity:1; transform:rotate(0); }
    .spp-tc:hover .spp-tc__bottom { letter-spacing:3px; opacity:1; }
    .spp-tc:hover .spp-tc__sub { opacity:1; letter-spacing:9.5px; }
    .spp-tc:hover .spp-tc__trail { animation:sppTcTrail 1s ease-in-out; }
    @keyframes sppTcFlash {
        0%   { border-right:1px solid transparent; }
        10%  { border-right:1px solid #bd9f67; }
        80%  { border-right:1px solid #bd9f67; }
        100% { border-right:1px solid transparent; }
    }
    @keyframes sppTcTrail {
        0%  { background:linear-gradient(90deg,rgba(189,159,103,0) 90%,rgb(189,159,103) 100%); opacity:0; }
        30% { background:linear-gradient(90deg,rgba(189,159,103,0) 70%,rgb(189,159,103) 100%); opacity:1; }
        70% { background:linear-gradient(90deg,rgba(189,159,103,0) 70%,rgb(189,159,103) 100%); opacity:1; }
        95% { background:linear-gradient(90deg,rgba(189,159,103,0) 90%,rgb(189,159,103) 100%); opacity:0; }
    }

    /* ---- 侧栏(首页「最新讨论 / 最新回复」搬到茶馆两侧):沿用普通卡片视觉,不装 5×5 热区 ---- */
    .spp-sz-group.is-center .spp-sz-grid > .spp-latest {
        flex:1 1 0; width:auto; height:var(--sz-card-h);
    }
    /* .spp-lt 的内边距已在基类统一成 16px 18px 12px(2.52.96),这里不再单独覆盖 */
    .spp-sz-group.is-center .spp-sz-grid > .spp-latest .spp-lt__list a { height:29px; font-size:13.5px; }
    .spp-sz-group.is-center .spp-sz-grid > .spp-latest .spp-lt__hd { font-size:14px; }
    .spp-lt {
        /* ★ 2.52.96:站点是 content-box,height:100% 只算内容盒 ⇒ 实高 220+28+4 = 252,
           比槽位高 32px(侧卡比中间卡高一截的真凶)。统一 border-box 后 height:100%
           就是整张卡的高度;内边距同时从 18/14 收到 16/12(内容盒 224 放得下 220 内容,
           留 4px 余量,一个字都不裁)。 */
        box-sizing:border-box;
        position:relative; height:100%; padding:16px 18px 12px;
        display:flex; flex-direction:column;
        border-radius:20px;
        background:linear-gradient(45deg,#1a1a1a,#262626);
        border:2px solid rgba(255,255,255,.1);
        box-shadow:0 0 20px rgba(0,0,0,.3), inset 0 0 20px rgba(0,0,0,.2);
        overflow:hidden;
    }
    .spp-lt__glare {
        position:absolute; inset:0; pointer-events:none; opacity:0; transition:opacity 300ms;
        background:linear-gradient(125deg,
            rgba(255,255,255,0) 0%, rgba(255,255,255,.05) 45%,
            rgba(255,255,255,.1) 50%, rgba(255,255,255,.05) 55%, rgba(255,255,255,0) 100%);
    }
    .spp-lt:hover .spp-lt__glare { opacity:1; }
    .spp-lt__lines span {
        position:absolute; pointer-events:none;
        background:linear-gradient(90deg,transparent,rgba(92,103,255,.2),transparent);
    }
    .spp-lt__lines span:nth-child(1) { top:22%; left:0;  width:100%; height:1px; transform:scaleX(0); transform-origin:left;  animation:sppScLine 3s linear infinite; }
    .spp-lt__lines span:nth-child(2) { top:42%; right:0; width:100%; height:1px; transform:scaleX(0); transform-origin:right; animation:sppScLine 3s linear infinite 1s; }
    .spp-lt__lines span:nth-child(3) { top:62%; left:0;  width:100%; height:1px; transform:scaleX(0); transform-origin:left;  animation:sppScLine 3s linear infinite 2s; }
    .spp-lt__lines span:nth-child(4) { top:82%; right:0; width:100%; height:1px; transform:scaleX(0); transform-origin:right; animation:sppScLine 3s linear infinite 1.5s; }
    .spp-lt__glows > div {
        position:absolute; pointer-events:none; width:100px; height:100px; border-radius:50%;
        background:radial-gradient(circle at center,rgba(0,255,170,.3) 0%,rgba(0,255,170,0) 70%);
        filter:blur(15px); opacity:0; transition:opacity .3s ease;
    }
    .spp-lt__glows .g1 { top:-20px; left:-20px; }
    .spp-lt__glows .g2 { top:50%; right:-30px; transform:translateY(-50%); }
    .spp-lt__glows .g3 { bottom:-20px; left:30%; }
    .spp-lt:hover .spp-lt__glows > div { opacity:1; }
    .spp-lt__corners span {
        position:absolute; pointer-events:none; width:10px; height:10px;
        border:2px solid rgba(92,103,255,.3); transition:all .3s ease;
    }
    .spp-lt__corners span:nth-child(1) { top:8px; left:8px;  border-right:0; border-bottom:0; }
    .spp-lt__corners span:nth-child(2) { top:8px; right:8px; border-left:0;  border-bottom:0; }
    .spp-lt__corners span:nth-child(3) { bottom:8px; left:8px;  border-right:0; border-top:0; }
    .spp-lt__corners span:nth-child(4) { bottom:8px; right:8px; border-left:0;  border-top:0; }
    .spp-lt:hover .spp-lt__corners span {
        border-color:rgba(92,103,255,.8); box-shadow:0 0 10px rgba(92,103,255,.5);
    }
    .spp-lt__scan {
        position:absolute; inset:0; pointer-events:none;
        background:linear-gradient(to bottom,transparent,rgba(92,103,255,.1),transparent);
        transform:translateY(-100%); animation:sppScScan 2s linear infinite;
    }
    .spp-lt__hd {
        position:relative; z-index:5;
        display:flex; align-items:center; gap:8px;
        padding-bottom:9px; margin-bottom:8px;
        border-bottom:1px solid rgba(148,163,184,.16);
        font-size:13px; font-weight:700; letter-spacing:1px; color:#e8eefc;
    }
    .spp-lt__hd i {
        flex:0 0 auto; width:3px; height:13px; border-radius:2px;
        background:linear-gradient(180deg,#00ffaa,#00a2ff);
        box-shadow:0 0 8px rgba(0,255,170,.5);
    }
    .spp-lt__hd em {
        margin-left:auto; font-style:normal; font-weight:400;
        font-size:10.5px; letter-spacing:2px; color:rgba(255,255,255,.34);
    }
    .spp-lt__list {
        position:relative; z-index:5; list-style:none; margin:0; padding:0;
        display:flex; flex-direction:column; flex:1; min-height:0;
    }
    .spp-lt__list a {
        display:flex; align-items:center; gap:9px; height:25px;
        text-decoration:none !important; font-size:13px; font-weight:500;
        color:rgba(255,255,255,.88) !important;
        text-shadow:0 1px 2px rgba(0,0,0,.8);
        -webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale;
        transition:color .15s ease, transform .15s ease;
    }
    .spp-lt__list a i {
        flex:0 0 auto; width:5px; height:5px; border-radius:50%;
        background:rgba(0,255,170,.75);
    }
    .spp-lt__list a span {
        flex:1 1 auto; min-width:0;
        white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
    }
    .spp-lt__list a:hover { color:#0ce39a !important; transform:translateX(2px); }
    .spp-lt__list a:hover i { background:#00ffaa; box-shadow:0 0 8px rgba(0,255,170,.8); }
    `;

        function comma(n) { return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ','); }
        // 相对时间:把 'YYYY-MM-DD HH:mm' 转成「3 分钟前 / 8 个月前」这类可读文案
        function relTime(s) {
            if (!s) return '';
            const t = new Date(String(s).replace(' ', 'T')).getTime();
            if (!t || isNaN(t)) return '';
            const diff = Date.now() - t;
            if (diff < 0) return '刚刚';
            const m = Math.floor(diff / 60000);
            if (m < 1) return '刚刚';
            if (m < 60) return m + ' 分钟前';
            const h = Math.floor(m / 60);
            if (h < 24) return h + ' 小时前';
            const d = Math.floor(h / 24);
            if (d < 30) return d + ' 天前';
            const mo = Math.floor(d / 30);
            if (mo < 12) return mo + ' 个月前';
            return Math.floor(mo / 12) + ' 年前';
        }

        // ---------- A. 统计条卡片化 ----------
        function polishStats() {
            if (!onStats) return;
            const host = document.querySelector('#infobox div.fr.tar');
            if (!host || host.dataset.sppHm) return;
            host.dataset.sppHm = '1';
            const txt = (host.textContent || '').replace(/\s+/g, ' ');
            const pick = function (label) {
                const m = txt.match(new RegExp(label + '\\s*[::]\\s*([\\d,]+)'));
                return m ? m[1] : null;
            };
            const items = [
                ['今日', pick('今日')], ['昨日', pick('昨日')], ['最高日', pick('最高日')],
                ['主题', pick('主题')], ['帖子', pick('帖子')], ['会员', pick('会员')]
            ].filter(function (i) { return i[1]; });
            const acts = [].slice.call(host.querySelectorAll('a')).filter(function (a) {
                return /^(标记已读|最新帖子|RSS)$/.test(a.textContent.trim());
            });
            const newbie = host.querySelector('a[href*="u.php?action-show-username"]');
            host.classList.add('spp-hm-host');
            host.innerHTML = '<div class="spp-hm-stats">' +
                items.map(function (i) {
                    return '<div class="spp-hm-stat"><span class="k">' + i[0] + '</span><span class="v">' + i[1] + '</span></div>';
                }).join('') +
                '</div><div class="spp-hm-acts">' +
                (newbie ? '<a href="' + newbie.getAttribute('href') + '" target="_blank">新会员:' + esc(newbie.textContent.trim()) + '</a>' : '') +
                acts.map(function (a) {
                    return '<a href="' + a.getAttribute('href') + '"' +
                        (a.getAttribute('target') ? ' target="' + a.getAttribute('target') + '"' : '') + '>' +
                        esc(a.textContent.trim()) + '</a>';
                }).join('') + '</div>';
        }

        // ---------- A2. 用户信息区深色卡 ----------
        // 原生 #infobox → table:[td#user_info(用户区)] [td(统计条)]
        // 重排思路:只把「旧结构里已有的节点」搬位置,不重建语义节点,
        // 这样站点自己的内联行为脚本(honor() 写 #honor、#sethonor 的 sendmsg、
        // 头像 onmouseover 的 read.open('showface','sf'))全部继续有效。

        function polishNotice() {
            if (!onNotice) return;
            const ul = document.querySelector('#notice0 ul');
            if (ul) ul.classList.add('spp-hm-notice-list');
        }

        // ---------- C0. 顶部四栏板块卡片化:实现见「子版卡片区」之后 ----------

        // ---------- C. 漫区特设 Comic Market 卡片 ----------
        // 真实行结构(南+ 首页 #cate_39):
        //   td.icon | th[0] 版块名 h3.b>a + span.s3.f10 今日发帖"(9)" + span.smalltxt 简介
        //           |  td.tal "主题 / 帖数"  |  th[1] a.a2 末帖标题 + span.f12 用户名 + span.f9 [时间]
        //   子版写在 th[0] 内的 <div>子版:<a>…</a> | <a>…</a></div>
        function parseBoardRow(tr) {
            const a = tr.querySelector('th h3.b > a');
            if (!a) return null;
            const ths = tr.querySelectorAll('th');
            const nameTh = ths[0];
            const statTd = tr.querySelector('td.tal');
            const m = (statTd ? statTd.textContent : '').match(/(\d+)\s*\/\s*(\d+)/);
            const cm = a.textContent.trim().match(/^Comic Market\s+(\d+)/i);
            // 今日发帖:版块名右侧的 span.s3.f10,形如 "(9)";解析不到记 null(区别于真实的 0)
            const todayEl = nameTh ? nameTh.querySelector('span.s3.f10') : null;
            const todayM = todayEl ? todayEl.textContent.match(/\((\d+)\)/) : null;
            const subWrap = [].slice.call(tr.querySelectorAll('th div')).filter(function (d) {
                return /子版/.test(d.textContent);
            })[0];
            const subs = subWrap ? [].slice.call(subWrap.querySelectorAll('a')).map(function (x) {
                return { name: x.textContent.trim(), href: x.href };
            }) : [];
            const lastCell = ths[ths.length - 1];
            const lastA = lastCell ? lastCell.querySelector('a.a2') : null;
            const lastUserEl = lastCell ? lastCell.querySelector('span.f12') : null;
            const lastT = lastCell ? (lastCell.textContent.match(/\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2})\]/) || [])[1] : null;
            const dateEl = tr.querySelector('span.smalltxt');
            return {
                tr: tr, name: a.textContent.trim(), href: a.href,
                no: cm ? parseInt(cm[1], 10) : null,
                topics: m ? +m[1] : 0, posts: m ? +m[2] : 0,
                today: todayM ? parseInt(todayM[1], 10) : null,
                date: dateEl ? dateEl.textContent.trim() : '',
                lastTitle: lastA ? lastA.textContent.trim() : '',
                lastUser: lastUserEl ? lastUserEl.textContent.trim() : '',
                lastTime: lastT || '',
                subs: subs
            };
        }

        // 卡片上的每个数字都来自本版块自身,不做任何跨版块聚合(旧「同期占比」因分母取自别的版块而废弃)
        function buildCmCard(info) {
            const card = document.createElement('div');
            card.className = 'spp-cm-card';
            const avg = info.topics ? (info.posts / info.topics) : 0;
            const avgTxt = avg ? '帖均 ' + avg.toFixed(1) : '暂无数据';
            const hasToday = (typeof info.today === 'number');
            const todayTxt = hasToday ? (info.today > 0 ? '+' + info.today : '0') : '—';
            const fresh = info.lastTime ? (Date.now() - new Date(info.lastTime.replace(' ', 'T')).getTime() < 7 * 864e5) : false;
            const rel = relTime(info.lastTime);
            const pill = info.lastTime
                ? '<span class="spp-cm-card__pill' + (fresh ? '' : ' is-idle') + '"><span class="spp-cm-card__dot"></span>' + (fresh ? '近期活跃' : '已沉淀') + '</span>'
                : '<span class="spp-cm-card__pill is-idle"><span class="spp-cm-card__dot"></span>暂无更新</span>';
            // 子版入口:只列【真实存在】的子版,绝不在缺失时回落到版块自身。
            // 回落会造成两种错:① 没有常规子版时造出一个「版块首页」,与底部「进入板块」
            // 完全同址(サンクリ / Comic1☆ / 其他同人志 / 例大祭&红楼梦);
            // ② 没有图墙子版时凭空造出「图墙模式」,指向板块自身却写「需在板块内切换」
            //(一般漫画只有「原版交流」)。所以两个入口都按真实子版分别生成。
            const subs = info.subs || [];
            const wallSub = subs.filter(function (s) { return /图墙/.test(s.name); })[0];
            const listSub = subs.filter(function (s) { return !/图墙/.test(s.name); })[0];
            const otherSubs = subs.filter(function (s) { return s !== wallSub && s !== listSub; });
            // 描述文案只在确实有该子版时才用到
            const listDesc = /同人志|CG/.test(listSub ? listSub.name : '') ? '列表模式 · 按最后回复排序' : '列表模式';
            const wallDesc = '封面墙 · 缩略图瀑布流';
            // 徽章文案:默认按漫区语义(CM 届次给数字、常设大区给名字前两字)。
            // 综合交流的板块 no 也全是 null,但它们是「交流」不是「漫区」,
            // 所以允许调用方用 info.badgeEm / info.badgeNo 覆盖,避免徽章写错。
            const badgeEm = info.badgeEm || (info.no === null ? '漫区' : 'CM');
            const badgeNo = info.badgeNo || (info.no === null ? String(info.name || '?').trim().slice(0, 2) : String(info.no));
            const wallIco = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
                '<path stroke-linecap="round" stroke-linejoin="round" d="M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM14 14h7v7h-7z"/></svg>';
            const listIco = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
                '<path stroke-linecap="round" stroke-linejoin="round" d="M4 5h16v14H4zM4 9h16M9 5v14"/></svg>';
            const goIco = '<span class="spp-cm-card__board-go">' +
                '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
                '<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/></svg></span>';
            const initial = (info.lastUser || info.lastTitle || info.name || '?').trim().charAt(0).toUpperCase();
            // 数值栏宽度是固定的(三栏 flex),而「汉化本发布」这类板块的帖数可达
            // 2,588,429(7 位 + 2 个千分位逗号 ≈ 9 字形),在 72px 内容宽里会折行。
            // 这里按最长那个数值的字形数预判:超过 6 个字符就整体缩一档字号。
            const statVals = [comma(info.topics), comma(info.posts), todayTxt];
            const wideStats = statVals.some(function (s) { return String(s).length >= 7; });
            card.innerHTML =
                '<span class="spp-cm-card__spin"></span>' +
                '<div class="spp-cm-card__inner">' +
                    '<div class="spp-cm-card__head">' +
                        '<span class="spp-cm-card__badge"><em>' + esc(badgeEm) + '</em><b>' + esc(badgeNo) + '</b></span>' +
                        '<div class="spp-cm-card__titles">' +
                            '<a class="spp-cm-card__title" href="' + info.href + '" target="_blank" rel="noopener">' + esc(info.name) + '</a>' +
                            (info.date ? '<div class="spp-cm-card__meta"><span>' + esc(info.date) + '</span></div>' : '') +
                        '</div>' + pill +
                    '</div>' +
                    '<div class="spp-cm-card__stats' + (wideStats ? ' is-wide' : '') + '">' +
                        '<div class="spp-cm-card__stat"><span class="k">主题</span><span class="v">' + comma(info.topics) +
                            '</span><span class="d">' + esc(avgTxt) + '</span></div>' +
                        '<div class="spp-cm-card__stat"><span class="k">帖数</span><span class="v">' + comma(info.posts) +
                            '</span><span class="d">' + (subs.length ? '子版 ' + subs.length + ' 个' : '无子版') + '</span></div>' +
                        '<div class="spp-cm-card__stat is-today' + (hasToday && info.today > 0 ? ' is-hot' : '') + '">' +
                            '<span class="k">今日</span><span class="v">' + esc(todayTxt) + '</span><span class="d">今日发帖</span></div>' +
                    '</div>' +
                    '<div class="spp-cm-card__boards">' +
                        '<div class="spp-cm-card__boards-k">板块入口</div>' +
                        (listSub
                            ? '<a class="spp-cm-card__board" href="' + listSub.href + '" target="_blank" rel="noopener">' +
                                '<span class="spp-cm-card__board-ico">' + listIco + '</span>' +
                                '<span class="spp-cm-card__board-txt"><b>' + esc(listSub.name) + '</b><i>' + esc(listDesc) + '</i></span>' +
                                goIco +
                              '</a>'
                            : '') +
                        (wallSub
                            ? '<a class="spp-cm-card__board is-wall" href="' + wallSub.href + '" target="_blank" rel="noopener">' +
                                '<span class="spp-cm-card__board-ico">' + wallIco + '</span>' +
                                '<span class="spp-cm-card__board-txt"><b>' + esc(wallSub.name) + '</b><i>' + esc(wallDesc) + '</i></span>' +
                                goIco +
                              '</a>'
                            : '') +
                        // 一个子版都没有(理论上不该发生)时,至少给条直达版块的入口,
                        // 免得「板块入口」下面空白一片
                        (!listSub && !wallSub
                            ? '<a class="spp-cm-card__board" href="' + info.href + '" target="_blank" rel="noopener">' +
                                '<span class="spp-cm-card__board-ico">' + listIco + '</span>' +
                                '<span class="spp-cm-card__board-txt"><b>进入板块</b><i>浏览全部主题</i></span>' +
                                goIco +
                              '</a>'
                            : '') +
                    '</div>' +
                    (info.lastTime || info.lastTitle
                        ? '<a class="spp-cm-card__last" href="' + info.href + '" target="_blank" rel="noopener">' +
                            '<span class="spp-cm-card__avatar">' + esc(initial) + '</span>' +
                            '<span class="spp-cm-card__last-txt">' +
                                '<b>' + esc(info.lastTitle || info.name + ' · 暂无新帖') + '</b>' +
                                '<i>最后发言 <em>' + esc(info.lastUser || info.name) + '</em></i>' +
                            '</span>' +
                            '<span class="spp-cm-card__last-time' + (fresh ? ' is-fresh' : '') + '">' +
                                '<b>' + esc(info.lastTime || '—') + '</b><span>' + esc(rel) + '</span>' +
                            '</span>' +
                          '</a>'
                        : '') +
                    '<div class="spp-cm-card__foot">' +
                        '<div class="spp-cm-card__subs">' + otherSubs.slice(0, 3).map(function (s) {
                            return '<a href="' + s.href + '" target="_blank" rel="noopener">子版:' + esc(s.name) + '</a>';
                        }).join('') + '</div>' +
                        '<a class="spp-cm-card__cta" href="' + info.href + '" target="_blank" rel="noopener">进入板块' +
                        '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
                        '<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"></path></svg></a>' +
                    '</div>' +
                '</div>';
            return card;
        }

        // 置顶版块:无视当前范围一律纳入,并按此顺序排在卡片网格最前。调整置顶只需改这个数组
        const CM_PINNED = ['汉化本发布', '询问&求物'];
        // ★ 2.53.63(用户:「把求物这个卡片放到汉化版发布旁边」):置顶行多收一位**隔壁**板块。
        //   「询问&求物」属综合交流 #cate_7,不在漫区特设(#cate_39)的表里,所以光进这张
        //   置顶名单还不够 —— 渲染前要先从它自己那个分类里把行捞过来(见 pickForeignPins),
        //   并且要从 HJ_KEEP / HJ_BADGE 里摘掉:搬家不是复制,两个区块各出现一张就错了。
        //   badgeEm / badgeNo 写死在下面:不写的话 buildCmCard 会按「漫区 + 询问」渲染,
        //   等于给这张卡换了块牌子;这里保留它在综合交流时的语义(交流 / 求物)。
        const CM_FOREIGN = [
            { name: '询问&求物', cateId: 'cate_7', badgeEm: '交流', badgeNo: '求物' }
        ];
        // 存档版块:从主卡片网格中剔除,单独成一个分区(含它的全部子版小卡)
        const CM_ARCHIVE = '旧物仓库';

        // 常设大区:没有届次号(no === null)的固定板块
        // (サンクリ / Comic1☆ / 其他同人志 / 一般漫画 / 例大祭&红楼梦 …)。
        // 它们不属于任何一届 CM,所以 scope 只管「届次范围」,常设大区与 scope 正交、
        // 始终成卡 —— 这样切到「仅最新一届」也不会把它们丢掉。
        function isStdBoard(r) { return r.no === null; }

        // 依据范围与置顶规则挑出要渲染成卡片的版块:置顶优先,其余保持页面原顺序
        function pickCmTargets(all, scope, series) {
            let targets = all;
            if (scope === 'latest' && series.length) targets = all.filter(function (r) { return r.no === series[0].no; });
            else if (scope === 'cm') targets = all.filter(function (r) { return r.no !== null && r.no >= 104; });

            // 并入常设大区(按名字去重,顺序仍是页面上原有顺序)
            const merged = [];
            const seen = {};
            targets.concat(all.filter(isStdBoard)).forEach(function (r) {
                if (seen[r.name]) return;
                seen[r.name] = 1;
                merged.push(r);
            });

            const pinned = CM_PINNED.map(function (n) {
                return all.filter(function (r) { return r.name === n; })[0];
            }).filter(Boolean);
            return pinned.concat(merged.filter(function (r) { return pinned.indexOf(r) === -1; }))
                .filter(function (r) { return r.name !== CM_ARCHIVE; });
        }

        // ★ 2.53.63:「借来置顶」的外板块 —— 从别的分类(CM_FOREIGN.cateId)里按名字捞一行,
        //   复制一份并写上它自己的徽章语义(不复制行对象本身:原行的 tr 还要留着被隐藏)。
        //   捞不到(站点改了版块名 / 那个分类不在本页)就返回空数组,置顶行退化成只有汉化本发布,
        //   卡片区照常渲染 —— 不抛错、不留半张卡。
        function pickForeignPins() {
            return CM_FOREIGN.map(function (f) {
                const cate = document.getElementById(f.cateId);
                if (!cate) return null;
                const row = [].slice.call(cate.querySelectorAll('tr.f_one[id^="fid_"]'))
                    .map(parseBoardRow).filter(Boolean)
                    .filter(function (r) {
                        return r.name === f.name || String(r.name).indexOf(f.name) === 0;
                    })[0];
                if (!row) return null;
                return Object.assign({}, row, { badgeEm: f.badgeEm, badgeNo: f.badgeNo });
            }).filter(Boolean);
        }

        // ==================================================================
        // 漫区卡片的「分组换行」
        // ==================================================================
        // 问题:CM 届次(Comic Market 108…104)与常设大区(サンクリ/Comic1☆…)
        //   原本塞进同一个 grid,会流式连排 —— 「サンクリ」紧贴在 C104 右边,
        //   两组混在一起看不出分界。
        // 做法:拆成独立 grid 竖排。每个 grid 的首个子项是横跨整行的组标题
        //   (靠 CSS 的 grid-column:1/-1),于是组与组之间强制换行。
        // ★ 置顶板块(汉化本发布)的 no 也是 null,若只按 isStdBoard 分组它会掉进
        //   「常设大区」组、失去「置顶第一张」的语义,所以单独成一组排在最前。
        // 只在有多组时才画标题,避免只剩一组时多出一条无意义的横线。
        function buildCmGroups(targets) {
            const pinned  = targets.filter(function (r) { return CM_PINNED.indexOf(r.name) !== -1; });
            const rest    = targets.filter(function (r) { return CM_PINNED.indexOf(r.name) === -1; });
            const cmGroup  = rest.filter(function (r) { return !isStdBoard(r); });
            const stdGroup = rest.filter(isStdBoard);
            const out = [];
            const gcount = (pinned.length ? 1 : 0) + (cmGroup.length ? 1 : 0) + (stdGroup.length ? 1 : 0);
            const many = gcount > 1;

            function grid(list, headerEl) {
                const g = document.createElement('div');
                // 只有一张卡时用 is-single 居中;有组标题时不套用,否则标题会被一起压窄
                g.className = 'spp-cm-grid' + (list.length === 1 && !headerEl ? ' is-single' : '');
                if (headerEl) g.appendChild(headerEl);
                list.forEach(function (t) { g.appendChild(buildCmCard(t)); });
                return g;
            }
            function head(text, note, count, cls, isFirst) {
                const h = document.createElement('div');
                h.className = 'spp-cm-group-hd' + (cls ? ' ' + cls : '') + (isFirst ? ' is-first' : '');
                h.innerHTML = '<b>' + esc(text) + '</b><em>' + esc(count + ' 个') + '</em>' +
                              '<i>' + esc(note) + '</i>';
                return h;
            }

            let first = true;
            if (pinned.length) {
                out.push(grid(pinned, many
                    ? head('置顶', '常驻最前', pinned.length, 'is-pin', first) : null));
                first = false;
            }
            if (cmGroup.length) {
                out.push(grid(cmGroup, many
                    ? head('CM 届次', '按届次倒序 · 最新在左', cmGroup.length, '', first) : null));
                first = false;
            }
            if (stdGroup.length) {
                out.push(grid(stdGroup, many
                    ? head('常设大区', '每届通用的固定板块', stdGroup.length, 'is-std', first) : null));
            }
            return out;
        }

        // 存档分区:标题条(沿用渐变描边按钮)+ 子版小卡网格
        function buildCmArchive(info) {
            const subs = info.subs || [];
            const wrap = document.createElement('div');
            wrap.className = 'spp-cm-sec';
            wrap.innerHTML =
                '<div class="spp-cm-sec__hd spp-uvbtn">' +
                    '<span class="spp-cm-sec__bar"></span>' +
                    '<a class="spp-cm-sec__title" href="' + info.href + '" target="_blank" rel="noopener">' + esc(info.name) + '</a>' +
                    '<span class="spp-cm-sec__meta">' + comma(info.topics) + ' 主题 · ' + comma(info.posts) + ' 帖' +
                        (typeof info.today === 'number' ? ' · 今日 ' + (info.today > 0 ? '+' + info.today : '0') : '') +
                    '</span>' +
                    (subs.length ? '<span class="spp-cm-sec__count">' + subs.length + ' 个存档会场</span>' : '') +
                '</div>' +
                (subs.length ? '<div class="spp-cm-sec__grid">' + subs.map(function (s) {
                    return '<a class="spp-cm-mini" href="' + s.href + '" target="_blank" rel="noopener">' +
                        '<span class="spp-cm-mini__nm">' + esc(s.name) + '</span></a>';
                }).join('') + '</div>' : '') ;
            return wrap;
        }

        // ==================================================================
        // 子版卡片区
        // ==================================================================
        // 三个大板块(都在 #cate_7 综合交流里):茶馆 / 免空资源区 / 网赚资源区
        // 卡片对象是它们的**子版**;茶馆只有 1 个子版,直接用大板块本身成卡
        const SZ_GROUPS = ['茶馆', '免空资源区', '网赚资源区'];
        const SZ_TEA    = '茶馆';
        // 茶馆两侧的两栏直接用首页 DOM 里的「最新讨论 / 最新回复」,
        // 数据由 collectTopCols() 提供(同一份快照),不再走 new.php 异步请求。

        // 建一个子版卡片(Uiverse / 00Kubi / cowardly-eagle-56:5×5 热区 → 3D 倾斜)
        function buildSubCard(sub) {
            const a = document.createElement('a');
            a.className = 'spp-sc';
            const n = sub.name || '';
            if (n.length >= 14) a.className += ' is-xlong';
            else if (n.length >= 9) a.className += ' is-long';
            a.href = sub.href;
            a.target = '_blank';
            a.rel = 'noopener';
            a.innerHTML =
                '<div class="spp-sc__canvas">' +
                    new Array(25).join('<div class="spp-sc__tr"></div>') + '<div class="spp-sc__tr"></div>' +
                    '<div class="spp-sc__card"><div class="spp-sc__body">' +
                        '<div class="spp-sc__glare"></div>' +
                        '<div class="spp-sc__lines"><span></span><span></span><span></span><span></span></div>' +
                        '<div class="spp-sc__glows"><div class="g1"></div><div class="g2"></div><div class="g3"></div></div>' +
                        '<div class="spp-sc__particles"><span></span><span></span><span></span><span></span><span></span><span></span></div>' +
                        '<div class="spp-sc__corners"><span></span><span></span><span></span><span></span></div>' +
                        '<div class="spp-sc__scan"></div>' +
                        '<h4 class="spp-sc__name">' + esc(n) + '</h4>' +
                    '</div></div>' +
                '</div>';
            return a;
        }

        // 建茶馆卡(Uiverse / Smit-Prajapati / spicy-rat-83:hover 展开诗句,「茶馆」二字淡出)
        const SZ_TEA_POEM = '休对故人思故国,且将新火试新茶,诗酒趁年华。';
        function buildTeaCard(info) {
            const a = document.createElement('a');
            a.className = 'spp-sc spp-sc--tea';
            a.href = info ? info.href : 'thread.php?fid-9.html';
            a.target = '_blank';
            a.rel = 'noopener';
            a.innerHTML =
                '<div class="spp-tc">' +
                    '<div class="spp-tc__border"></div>' +
                    '<div class="spp-tc__content">' +
                        '<div class="spp-tc__logo">' +
                            '<div class="spp-tc__logo1">茶馆</div>' +
                            '<div class="spp-tc__logo2">' + SZ_TEA_POEM + '</div>' +
                            '<span class="spp-tc__trail"></span>' +
                        '</div>' +
                        '<span class="spp-tc__sub">SOUTH-PLUS</span>' +
                    '</div>' +
                    '<span class="spp-tc__bottom">苏轼《望江南》</span>' +
                '</div>';
            return a;
        }

        // 渲染整个子版卡片区,插到 #content > .contentwrap 最前(即漫区特设 #t_39 之前)
        function renderSubCards() {
            if (!onSub) return;
            const cw = document.querySelector('#content > .contentwrap');
            if (!cw || cw.querySelector('.spp-sz')) return;
            const cate = document.getElementById('cate_7');
            if (!cate) return;
            const all = [].slice.call(cate.querySelectorAll('tr.f_one[id^="fid_"]'))
                .map(parseBoardRow).filter(Boolean);
            if (!all.length) return;

            const zone = document.createElement('div');
            zone.className = 'spp-sz';

            // 茶楼 → 茶馆:子版名叫「茶馆」时用大板块本身,卡片文字也写「茶馆」
            const picked = SZ_GROUPS.map(function (name) {
                return all.filter(function (r) { return r.name === name; })[0];
            }).filter(Boolean);
            if (!picked.length) return;

            picked.forEach(function (info) {
                const g = document.createElement('div');
                const isTea = info.name === SZ_TEA;
                g.className = 'spp-sz-group' + (isTea ? ' is-center' : '');

                const hd = document.createElement('div');
                hd.className = 'spp-sz-hd spp-uvbtn';
                hd.innerHTML =
                    '<a class="spp-sz-hd__t" href="' + info.href + '" target="_blank" rel="noopener">' + esc(info.name) + '</a>' +
                    '<span class="spp-sz-hd__meta">' + comma(info.topics) + ' 主题 · ' + comma(info.posts) + ' 帖' +
                        (typeof info.today === 'number' ? ' · 今日 <b>' + (info.today > 0 ? '+' + info.today : '0') + '</b>' : '') +
                    '</span>' +
                    '<span class="spp-sz-hd__cnt">' + info.subs.length + ' 子版</span>';
                g.appendChild(hd);

                const grid = document.createElement('div');
                grid.className = 'spp-sz-grid';

                if (isTea) {
                    // 两侧改为从首页 DOM 搬来的「最新讨论 / 最新回复」(不再异步 fetch)。
                    // 用的是 renderTopCards 那份同一快照(collectTopCols 有缓存)。
                    const snap = collectTopCols();
                    const pick = function (name) {
                        return snap ? snap.cols.filter(function (c) { return c.title === name; })[0] : null;
                    };
                    const l = pick('最新讨论'), r = pick('最新回复');
                    if (l) grid.appendChild(buildTopCard(l.title, l.links, l.href || null));
                    grid.appendChild(buildTeaCard(info));
                    if (r) grid.appendChild(buildTopCard(r.title, r.links, r.href || null));
                } else {
                    info.subs.forEach(function (sub) { grid.appendChild(buildSubCard(sub)); });
                }
                g.appendChild(grid);
                zone.appendChild(g);
            });

            if (!zone.children.length) return;
            cw.insertBefore(zone, cw.firstChild);
        }

        // 顶部四栏板块的原始数据(在 renderTopCards 里采集,供茶馆两侧复用)。
        // 三处渲染(顶部 3 栏 / 茶馆左右 2 栏)都读这一份快照,所以必须先采集再渲染。
        let TOP_SNAPSHOT = null;

        // 采集 #main > div.t 的四栏(标题 + 5 条链接)。只读 DOM,不发网络请求。
        // 返回 {title, links:[<a>原始节点引用], tag} 的数组;结构不符返回 null。
        function collectTopCols() {
            if (TOP_SNAPSHOT) return TOP_SNAPSHOT;
            const wrap = document.querySelector('#main > div.t');
            if (!wrap) return null;
            const tbl = wrap.querySelector('table');
            if (!tbl || tbl.rows.length < 2) return null;
            const headRow = tbl.rows[0], bodyRow = tbl.rows[1];
            if (headRow.cells.length !== 4 || bodyRow.cells.length !== 4) return null;
            const cols = [];
            for (let i = 0; i < 4; i++) {
                const title = headRow.cells[i].textContent.replace(/\s+/g, ' ').trim();
                const links = [].slice.call(bodyRow.cells[i].querySelectorAll('a'))
                    .filter(function (a) { return (a.textContent || '').trim(); });
                if (!title || !links.length) return null;   // 结构不符就整体放弃,绝不半改
                cols.push({ title: title, links: links });
            }
            TOP_SNAPSHOT = { wrap: wrap, tbl: tbl, cols: cols };
            return TOP_SNAPSHOT;
        }

        // 顶部四栏板块 → 3 张卡片(GALGAME汉化区 / 蜜柑计划 / 提问求物)。
        // 「蜜柑计划」不在这个 table 里,它是 #cate_123 下的独立大板块(只有 1 行),
        // 所以取它的板块名与链接单独成卡,并把原块整块隐藏。
        const TOP_KEEP = ['GALGAME汉化区', '提问求物'];   // 保留的原始栏(顺序以 KEEP 为准)
        const TOP_MIKAN = '蜜柑计划';
        // 蜜柑计划是外部站点(不是站内板块),卡片整卡外链到这里
        const MIKAN_URL = 'https://mikan.tangbai.cc/';

        // 内联单色「小电视」图标(黑白线稿,不引外链图,不受 CORS 限制)。
        // 用 currentColor 描边,颜色交给 CSS,hover 时可跟随主题变色。
        const SZ_BILI_SVG =
            '<svg class="spp-bili" viewBox="0 0 24 24" aria-hidden="true" focusable="false">' +
                '<path d="M6.4 6.6 4.1 4.2"/>' +
                '<path d="M17.6 6.6 19.9 4.2"/>' +
                '<rect x="2.6" y="6.6" width="18.8" height="13.6" rx="3.4"/>' +
                '<path d="M8.2 11.4v2.8"/>' +
                '<path d="M15.8 11.4v2.8"/>' +
            '</svg>';

        // opts: { variant:'mikan', icon:svg, href, name, desc }
        //   带 variant 时改用 3D 翻转结构(Uiverse / ElSombrero2 / tricky-robin-67),
        //   否则沿用普通 .spp-lt 面板。两张面带 backface-visibility:hidden 互斥:
        //   默认见「背面」(黑白小电视),hover 翻出「正面」(信息面)。
        function buildTopCard(title, links, href, opts) {
            opts = opts || {};
            const wrap = document.createElement('div');
            wrap.className = 'spp-sc spp-latest' + (opts.variant ? ' spp-sc--' + opts.variant : '');

            if (opts.variant === 'mikan') {
                const link = opts.href || '#';
                wrap.innerHTML =
                    '<div class="spp-mk">' +
                        // 第一面(默认):旋转橙带 + 黑白小电视
                        '<div class="spp-mk__back">' +
                            '<div class="spp-mk__core">' +
                                (opts.icon || '') +
                                '<strong>' + esc(title) + '</strong>' +
                            '</div>' +
                        '</div>' +
                        // 第二面(翻转):badge + 描述
                        '<div class="spp-mk__front">' +
                            '<div class="spp-mk__orbs"><span class="c1"></span><span class="c2"></span><span class="c3"></span></div>' +
                            '<div class="spp-mk__frontcore">' +
                                '<span class="spp-mk__badge">' + esc(opts.name || title) + '</span>' +
                                '<div class="spp-mk__desc">' +
                                    '<div class="spp-mk__title"><span>' + esc(title) + '</span>' +
                                        '<svg height="15px" width="15px" viewBox="0,0,256,256" aria-hidden="true">' +
                                            '<path d="M25,27l-9,-6.75l-9,6.75v-23h18z"></path>' +
                                        '</svg>' +
                                    '</div>' +
                                    (opts.desc ? '<p class="spp-mk__foot">' + esc(opts.desc) + '</p>' : '') +
                                    (opts.host ? '<p class="spp-mk__foot spp-mk__foot--host">' + esc(opts.host) + ' ↗</p>' : '') +
                                '</div>' +
                            '</div>' +
                        '</div>' +
                        '<a class="spp-mk__link" href="' + esc(link) + '" target="_blank" rel="noopener" aria-label="' + esc(title) + '"></a>' +
                    '</div>';
                return wrap;
            }

            const box = document.createElement('div');
            box.className = 'spp-lt';
            const head = href
                ? '<a class="spp-top4__hd" href="' + esc(href) + '" target="_blank" rel="noopener">' + esc(title) + '</a>'
                : '<span class="spp-top4__hd">' + esc(title) + '</span>';
            box.innerHTML =
                '<div class="spp-lt__glare"></div>' +
                '<div class="spp-lt__lines"><span></span><span></span><span></span><span></span></div>' +
                '<div class="spp-lt__glows"><div class="g1"></div><div class="g2"></div><div class="g3"></div></div>' +
                '<div class="spp-lt__corners"><span></span><span></span><span></span><span></span></div>' +
                '<div class="spp-lt__scan"></div>' +
                '<div class="spp-lt__hd"><i></i>' + (opts.icon || '') + head + '</div>';
            const ul = document.createElement('ul');
            ul.className = 'spp-lt__list';
            links.forEach(function (a) {
                const li = document.createElement('li');
                const na = document.createElement('a');
                na.href = a.getAttribute('href') || '#';
                na.target = '_blank';
                na.rel = 'noopener';
                const txt = (a.textContent || '').replace(/\s+/g, ' ').trim();
                na.innerHTML = '<i></i><span>' + esc(txt) + '</span>';
                li.appendChild(na);
                ul.appendChild(li);
            });
            box.appendChild(ul);
            wrap.appendChild(box);
            return wrap;
        }

        // 从 #cate_123 取「蜜柑计划」板块(名 + 链接 + 简介 + 统计)
        function collectMikan() {
            const cate = document.getElementById('cate_123');
            if (!cate) return null;
            const row = cate.querySelector('tr.f_one[id^="fid_"]');
            const tbl = cate.closest('table');
            if (!row) return null;
            const a = row.querySelector('th h3.b > a');
            if (!a) return null;
            // 简介:th 里的 span.smalltxt(形如「蜜柑计划一周年大更新!…」)
            const descEl = row.querySelector('span.smalltxt');
            // 统计:td.tal 的「主题 / 帖数」
            const statTd = row.querySelector('td.tal');
            const st = (statTd ? statTd.textContent : '').match(/(\d+)\s*\/\s*(\d+)/);
            return {
                block: document.getElementById('t_123') || (tbl ? tbl.parentNode : null),
                title: TOP_MIKAN,
                name: a.textContent.trim(),
                // 卡片整卡外链到蜜柑计划官网,不指向站内板块
                href: MIKAN_URL,
                // 站内板块链接保留,供其它用途(如以后加「站内讨论」入口)
                boardHref: a.getAttribute('href'),
                desc: descEl ? descEl.textContent.replace(/\s+/g, ' ').trim() : '',
                topics: st ? Number(st[1]) : null,
                posts: st ? Number(st[2]) : null,
                links: [a]
            };
        }

        // 顶部 3 栏:GALGAME汉化区 · 蜜柑计划 · 提问求物
        function renderTopCards() {
            if (!onTopCard) return;
            const snap = collectTopCols();
            if (!snap || snap.wrap.querySelector('.spp-top4')) return;
            const mikan = collectMikan();

            const grid = document.createElement('div');
            grid.className = 'spp-top4';
            // 按 中文序:GALGAME汉化区 → 蜜柑计划 → 提问求物
            const keep = TOP_KEEP.map(function (name) {
                return snap.cols.filter(function (c) { return c.title === name; })[0];
            }).filter(Boolean);
            if (keep.length !== TOP_KEEP.length) return;      // 缺栏就整体放弃

            const seq = [keep[0], mikan, keep[1]].filter(Boolean);
            if (seq.length !== 3) return;
            seq.forEach(function (c) {
                const isMikan = c === mikan;
                grid.appendChild(buildTopCard(c.title, c.links, c.href || null,
                    isMikan ? {
                        variant: 'mikan',
                        icon: SZ_BILI_SVG,
                        href: mikan.href,
                        name: mikan.name,
                        desc: mikan.desc,
                        host: 'mikan.tangbai.cc'
                    } : null));
            });
            snap.tbl.style.display = 'none';
            if (mikan && mikan.block) mikan.block.style.display = 'none';
            // 给宿主加 class,供 :has() 不支持时的兜底规则命中
            snap.wrap.classList.add('spp-top4-host');
            snap.wrap.appendChild(grid);
        }

        function renderCmCards() {
            if (!onCard) return;
            const cate = document.getElementById('cate_39');
            if (!cate || cate.querySelector('.spp-cm-row')) return;
            const all = [].slice.call(cate.querySelectorAll('tr.f_one[id^="fid_"]'))
                .map(parseBoardRow).filter(Boolean);
            if (!all.length) return;
            // ★ 2.53.63:「借来置顶」的外板块(询问&求物)并进池子 —— 置顶判定与分组都走这一份。
            //   下面那个 anchor 仍只用 all:卡片行要插在漫区自己那张表的行位上。
            const pool = all.concat(pickForeignPins());
            const series = all.filter(function (r) { return r.no !== null; })
                .sort(function (a, b) { return b.no - a.no; }).slice(0, 5);

            // 列头行(版块/主题/文章/最后发表/版主)在卡片模式下没有意义,直接隐藏
            const headRow = cate.querySelector('tr.tr2');
            if (headRow) headRow.style.display = 'none';
            // 分类标题行套用 Uiverse 渐变描边按钮样式
            const tbl = cate.closest('table');
            const headTh = tbl ? tbl.querySelector('th.h') : null;
            if (headTh) headTh.classList.add('spp-uvbtn');

            // ★★ 2.53.62:第二个实参从读数变量改成字面量 'cm' —— 范围开关已整条撤掉。
            //   pickCmTargets 的三档判定保持原样:它是纯函数,两个测试生成器也还按 'cm' 调它。
            const targets = pickCmTargets(pool, 'cm', series);
            // 给漫区宿主加 class,供 :has() 不支持时的兜底规则命中
            // (宿主是 #t_39,它的父级是 .contentwrap 而非 #main,故 CSS 用后代选择器)
            const hostT = cate.closest('.t');
            if (hostT) hostT.classList.add('spp-clear-host');
            // 卡片行插在「被替换的版块行」中 DOM 最靠前的那个位置,保持原有版面位置
            const anchor = all.filter(function (r) { return targets.indexOf(r) !== -1; })[0];
            if (targets.length && anchor) {
                const tr = document.createElement('tr');
                tr.className = 'spp-cm-row';
                const td = document.createElement('td');
                td.colSpan = 5;
                // 两组卡片竖排(CM 届次组在上、常设大区组在下),实现见 buildCmGroups
                buildCmGroups(targets).forEach(function (node) { td.appendChild(node); });
                tr.appendChild(td);
                anchor.tr.parentNode.insertBefore(tr, anchor.tr);
                targets.forEach(function (t) { t.tr.style.display = 'none'; });
            }

            // 存档分区:单独成区,占用原行位置
            const archive = all.filter(function (r) { return r.name === CM_ARCHIVE; })[0];
            if (archive) {
                const atr = document.createElement('tr');
                atr.className = 'spp-cm-row spp-cm-archive-row';
                const atd = document.createElement('td');
                atd.colSpan = 5;
                atd.appendChild(buildCmArchive(archive));
                atr.appendChild(atd);
                archive.tr.parentNode.insertBefore(atr, archive.tr);
                archive.tr.style.display = 'none';
            }
        }

        // ==================================================================
        // 综合交流卡片(#cate_7) / 人民囧府卡片(#cate_1)
        // ==================================================================
        // 这一族「板块成卡」区块共用同一套骨架:宿主去白底 → 标题行套渐变描边
        // → 隐藏列头行 → 插入卡片行 → 隐藏被接管的原生行。
        // ★ 骨架必须只有一份。2.34.0 里标题行的 .spp-uvbtn 就是因为在综合交流那份
        //   里被抄漏,CSS 里 `th.h.spp-uvbtn` 整组规则一条都命中不了 —— 而且不报错。
        //   所以这里抽成 renderBoardCards(),各区块只提供「选区 + 挑板块 + 徽章语义」。
        // ★ 2.53.63:「询问&求物」搬去漫区特设的置顶行、与「汉化本发布」并排(用户要求),
        //   本组随之只剩 5 张卡。它不在本区块的名单里,原行仍由 hideRest 收掉。
        const HJ_KEEP = ['ACG交流', '轻小说', '原创绘画', '原创小说区', 'AI交流'];

        // 徽章上的短标签。不能简单取名字前两字 —— 「原创绘画」与「原创小说区」
        // 会双双变成「原创」,两张卡的徽章一模一样,失去区分作用。
        // ★ 2.53.63:「询问&求物」的短标签随卡片一起搬去 CM_FOREIGN(交流 / 求物),
        //   本表只剩这 5 个 —— 不留没人用的键。
        const HJ_BADGE = {
            'ACG交流':    'AC',
            '轻小说':      '轻小',
            '原创绘画':    '绘画',
            '原创小说区':  '小说',
            'AI交流':      'AI'
        };

        // 按 HJ_KEEP 的顺序挑板块;「AI交流 (beta)」带后缀,用前缀匹配兜住
        function pickHjTargets(all) {
            return HJ_KEEP.map(function (k) {
                return all.filter(function (r) {
                    return r.name === k || String(r.name).indexOf(k) === 0;
                })[0];
            }).filter(Boolean);
        }

        // 纯函数:给一组板块 info 返回卡片 grid(不碰 DOM 全局,便于预览页复用同一份代码)
        // badge 由调用方给出语义(em=徽章上排小字,map=短标签表),默认沿用漫区口径。
        function buildBoardGrid(targets, badgeEm, badgeMap, keepList) {
            const grid = document.createElement('div');
            // 只有一张卡时沿用漫区的 is-single 约定(居中、限宽 540px)——
            // 否则单张卡会孤零零地贴在 4 列网格的最左边。
            grid.className = 'spp-cm-grid' + (targets.length === 1 ? ' is-single' : '');
            targets.forEach(function (t) {
                // 这些板块的 no 同样是 null,不覆盖的话 buildCmCard 会按漫区默认
                // 写成「漫区」+ 名字前两字,语义就错了;短标签走别名表,
                // 避免「原创绘画/原创小说区」双双取成「原创」而撞车。
                const keys = keepList || [];
                const key = keys.filter(function (k) {
                    return t.name === k || String(t.name).indexOf(k) === 0;
                })[0] || t.name;
                const info = Object.assign({}, t, {
                    badgeEm: badgeEm,
                    badgeNo: (badgeMap && badgeMap[key]) || String(t.name).trim().slice(0, 2)
                });
                grid.appendChild(buildCmCard(info));
            });
            return grid;
        }

        // 兼容旧调用(综合交流预览页 / 测试脚本按这个名字取)
        function buildHjGrid(targets) {
            return buildBoardGrid(targets, '交流', HJ_BADGE, HJ_KEEP);
        }

        // 通用骨架:选区 id、挑板块函数、卡片行 class、徽章语义、是否连非目标行一并隐藏
        // hideRest=true 会收掉该 tbody 内【所有】原生板块行 —— 适用于「整个区块都成卡」
        //(漫区 / 综合交流 / 人民囧府),这样列头行也才敢隐藏;若有板块要保持原生行,
        // 传 false,此时列头行必须保留(否则原生行会失去表头)。
        function renderBoardCards(opt) {
            const cate = document.getElementById(opt.cateId);
            if (!cate || cate.querySelector('.' + opt.rowClass)) return;
            const all = [].slice.call(cate.querySelectorAll('tr.f_one[id^="fid_"]'))
                .map(parseBoardRow).filter(Boolean);
            if (!all.length) return;

            const targets = opt.pick(all);
            if (!targets.length) return;
            const rest = all.filter(function (r) { return targets.indexOf(r) === -1; });
            if (rest.length && !opt.hideRest) return;   // 有残留行却不允许隐藏 → 保持原样

            // 宿主加 class,供 :has() 不支持时的兜底规则命中
            const hostT = cate.closest('.t');
            if (hostT) hostT.classList.add('spp-clear-host');

            // 与漫区保持同一套标题样式:给标题行套渐变描边按钮。
            // 缺了它 CSS 里 `th.h.spp-uvbtn` 那组规则全都命中不了(且不报错)。
            const tbl = cate.closest('table');
            const headTh = tbl ? tbl.querySelector('th.h') : null;
            if (headTh) headTh.classList.add('spp-uvbtn');

            // 列头行(版块 / 主题·文章 / 最后发表 / 版主)在卡片模式下没有意义,直接隐藏。
            // 只有在 hideRest 时才敢这么做 —— 否则保留的原生行会丢失表头。
            if (opt.hideRest) {
                const headRow = cate.querySelector('tr.tr2');
                if (headRow) headRow.style.display = 'none';
            }

            // 卡片行插在「被替换的版块行」中 DOM 最靠前的那个位置,保持原有版面位置
            const anchor = all.filter(function (r) { return targets.indexOf(r) !== -1; })[0];
            if (!anchor) return;
            const tr = document.createElement('tr');
            tr.className = 'spp-cm-row' + (opt.rowClass === 'spp-cm-row' ? '' : ' ' + opt.rowClass);
            const td = document.createElement('td');
            td.colSpan = 5;
            td.appendChild(buildBoardGrid(targets, opt.badgeEm, opt.badgeMap, opt.keepList));
            tr.appendChild(td);
            anchor.tr.parentNode.insertBefore(tr, anchor.tr);
            // 被接管的原生行必须隐藏,否则裸露在星空上、与列头行一起形成一条孤零零的表格尾巴
            (opt.hideRest ? all : targets).forEach(function (r) { r.tr.style.display = 'none'; });
        }

        // ---- 综合交流:5 个板块成卡(整个区块都成卡 → 非目标行一并收掉)----
        //   ★ 2.53.63:原本 6 个,「询问&求物」搬去漫区特设的置顶行(见 CM_FOREIGN)。
        function renderHjCards() {
            if (!onHjCard) return;
            return renderBoardCards({
                cateId: 'cate_7',
                rowClass: 'spp-hj-row',
                pick: pickHjTargets,
                badgeEm: '交流',
                badgeMap: HJ_BADGE,
                keepList: HJ_KEEP,
                hideRest: true
            });
        }

        // ---- 人民囧府:只管「事务受理」一个板块(同上,整块成卡)----
        // 该区块只有 1 行,成卡后与漫区/综合交流的卡片皮肤完全一致。
        const RM_KEEP = ['事务受理'];
        const RM_BADGE = { '事务受理': '受理' };
        function pickRmTargets(all) {
            return RM_KEEP.map(function (k) {
                return all.filter(function (r) {
                    return r.name === k || String(r.name).indexOf(k) === 0;
                })[0];
            }).filter(Boolean);
        }
        function renderRmCards() {
            if (!onRmCard) return;
            return renderBoardCards({
                cateId: 'cate_1',
                rowClass: 'spp-rm-row',
                pick: pickRmTargets,
                badgeEm: '服务',
                badgeMap: RM_BADGE,
                keepList: RM_KEEP,
                hideRest: true
            });
        }

        // ---------- A3. 在线人数 → 统计卡操作条左端 ----------
        // 源数据在「论坛相关」表 #cate_info 的一行里:
        //   <tr class="tr2"><td>在线用户<a name="online"></a> - 共 28534 人在线,最多 40652 人发生在 2026-08-06 16:24</td></tr>
        // ★ 这一行不是独立数据条,而是紧随其后的【图例行】
        //   (管理员 / 总版主 / 论坛版主 / 普通会员)的小标题,并且带一个 <a name="online"> 锚点。
        //   所以只摘掉「- 共 … 发生在 …」这段数字尾巴,保留「在线用户」四字与那个锚点原地不动:
        //   图例行仍然有标题,页内锚点也不会失效。整行删除会同时破坏这两点。
        function moveOnlineStats() {
            if (!onOnline || !onStats) return;   // 徽标寄居在统计条的操作条里
            const host = document.querySelector('#infobox div.fr.tar');
            if (!host || host.querySelector('.spp-onb')) return;
            const acts = host.querySelector('.spp-hm-acts');
            if (!acts) return;

            const cate = document.getElementById('cate_info');
            if (!cate) return;
            const src = [].slice.call(cate.querySelectorAll('tr')).filter(function (tr) {
                return /在线用户/.test(tr.textContent) && /人在线/.test(tr.textContent);
            })[0];
            if (!src) return;

            const td = src.querySelector('td');
            const txt = (td ? td.textContent : src.textContent).replace(/\s+/g, ' ').trim();
            // 「在线」与「最多」两句分开取:千分位逗号可选,数字里可能带逗号
            const cur  = txt.match(/共\s*([\d,]+)\s*人在线/);
            const peak = txt.match(/最多\s*([\d,]+)\s*人发生在\s*([\d-]+)[ T]?([\d:]*)/);
            if (!cur) return;                    // 认不出就不动,绝不猜

            // 日期带年份:2026-08-06 16:24 → 26-08-06(完整时间留给 title)
            let dateTxt = '';
            let peakAt = '';
            if (peak) {
                peakAt = peak[2] + (peak[3] ? ' ' + peak[3] : '');
                const m = peak[2].match(/^(\d{4})-(\d{2})-(\d{2})$/);
                if (m) dateTxt = m[1].slice(2) + '-' + m[2] + '-' + m[3];
            }

            const badge = document.createElement('span');
            badge.className = 'spp-onb';
            badge.title = '共 ' + cur[1] + ' 人在线' +
                          (peak ? ',最多 ' + peak[1] + ' 人发生在 ' + peakAt : '');
            badge.innerHTML =
                '<span class="spp-onb__dot"></span>' +
                '<span class="spp-onb__k">在线</span>' +
                '<b class="spp-onb__v">' + esc(cur[1]) + '</b>' +
                (peak ? '<span class="spp-onb__peak">峰值 <b>' + esc(peak[1]) + '</b></span>' : '') +
                (dateTxt ? '<span class="spp-onb__date">· ' + esc(dateTxt) + '</span>' : '');
            // 插到操作条最前,靠 CSS 的 margin-right:auto 把按钮组推回右端
            acts.insertBefore(badge, acts.firstChild);

            // 摘掉数字尾巴:只改【文本节点】,保留 <a name="online"> 锚点
            // (用 innerHTML 重写会把站点自己的锚点一并抹掉)
            if (td) {
                [].slice.call(td.childNodes).forEach(function (n) {
                    if (n.nodeType === 3 && /人在线|人发生在/.test(n.nodeValue)) {
                        n.nodeValue = n.nodeValue.replace(/\s*[-—–]?\s*共\s*[\d,]+\s*人在线.*$/, '');
                    }
                });
            }
        }

        // ==================================================================
        // 友情链接卡片(人民囧府 #t_1 右侧一栏 · 方案A)
        // ==================================================================
        // 卡片样式移植自 uiverse.io/gharsh11032000/tasty-frog-49
        //   Copyright - 2026 gharsh11032000 (Harsh Gupta) — MIT License
        //
        // 数据:站点把友链放在 #cate_info 里的一行 ——
        //   <tbody id="cate_info">
        //     <tr class="tr2"><td>友情链接</td></tr>
        //     <tr class="tr3">…<a>[琉璃神社]</a><a>[绅士之庭]</a>…</tr>
        //     <tr class="tr2">在线用户 - 共 N 人在线…</tr>
        //   </tbody>
        // ★ 那一行共 19 个 <a>,其中前 3 个是本站 banner(两个图片链接 + 一个文字链接,
        //   都指向本站根域)、末尾 1 个是空锚点(分隔符残留)。三者都不是友链,
        //   必须剔掉 —— 判据用「文字是否形如 [站名]」+「href 是否本站域」双保险,
        //   只按其中一条都会漏(空锚点无文字、banner 文字不含方括号但 href 指向本站)。
        const FL_SEL = '#cate_info tr';
        // 本站域名判据:脚本的 @match 覆盖多个镜像域,这里按「与当前 host 同域」判断
        function isSameSite(href) {
            try {
                const h = new URL(href, location.href).hostname.replace(/^www\./, '');
                const cur = location.hostname.replace(/^www\./, '');
                if (h === cur) return true;
                // 镜像域之间也算本站(south-plus.net / blue-plus.net / …)
                return /(^|\.)(south-plus\.net|blue-plus\.net|south-plus\.org|north-plus\.net|east-plus\.net|white-plus\.net|level-plus\.net|spring-plus\.net|summer-plus\.net|snow-plus\.net|soul-plus\.net|imoutolove\.me)$/.test(h);
            } catch (e) { return false; }
        }
        function pickFriendLinks() {
            const cate = document.getElementById('cate_info');
            if (!cate) return [];
            // 找到那个「含方括号链接最多」的行 —— 比按 class 认更稳(tr2/tr3 只是隔行底色)
            let best = null, bestN = 0;
            [].slice.call(cate.querySelectorAll('tr')).forEach(function (tr) {
                const n = [].slice.call(tr.querySelectorAll('a')).filter(function (a) {
                    return /^\s*\[.+\]\s*$/.test(a.textContent);
                }).length;
                if (n > bestN) { bestN = n; best = tr; }
            });
            if (!best || bestN < 2) return [];
            const seen = {};
            return [].slice.call(best.querySelectorAll('a')).map(function (a) {
                const text = a.textContent.replace(/\s+/g, ' ').trim();
                if (!/^\[.+\]$/.test(text)) return null;          // 只认 [站名]
                if (!a.href) return null;
                if (isSameSite(a.href)) return null;              // 剔本站 banner
                const name = text.replace(/^\[|\]$/g, '');
                if (seen[name]) return null;                      // 去重
                seen[name] = 1;
                let host = '';
                try { host = new URL(a.href).hostname.replace(/^www\./, ''); } catch (e) { host = a.href; }
                return { name: name, href: a.href, host: host };
            }).filter(Boolean);
        }

        // hover 变换的尺寸适配(推导见 CSS_CARD 里的注释)
        // 实站卡片宽度由 grid 决定、随窗口变化,所以每次渲染与 resize 都要重算。
        const FL_K = 1.06;
        function syncFriendCardScale(card) {
            const r = card.getBoundingClientRect();
            if (!r.width || !r.height) return;
            const W = r.width + 10, H = r.height + 10;   // ::before 为 inset:-5px
            card.style.setProperty('--spp-fl-sx', (FL_K * r.height / W).toFixed(4));
            card.style.setProperty('--spp-fl-sy', (FL_K * r.width / H).toFixed(4));
        }
        function syncAllFriendCardScales() {
            [].slice.call(document.querySelectorAll('.spp-fl-card')).forEach(syncFriendCardScale);
        }

        function initialOf(name) {
            const s = String(name || '?').trim();
            // 中文取首字、英文站点(ASMR Online / Morian / JoyACG)取首字母大写
            return /[A-Za-z]/.test(s.charAt(0)) ? s.charAt(0).toUpperCase() : s.charAt(0);
        }

        // 纯函数:给一组友链返回卡片节点(不碰全局 DOM,便于预览页复用同一份代码)
        function buildFriendCard(links, opt) {
            opt = opt || {};
            const card = document.createElement('div');
            card.className = 'spp-fl-card' + (opt.dense === 'loose' ? ' is-loose' : '');
            card.innerHTML =
                '<div class="spp-fl-head">' +
                    '<span class="ttl">友情链接</span>' +
                    '<span class="cnt">' + links.length + ' 个站点</span>' +
                    '<span class="hint">点击直达 · 均为站外</span>' +
                '</div>' +
                '<div class="spp-fl-grid">' + links.map(function (l) {
                    return '<a class="spp-fl-item" href="' + l.href + '" target="_blank" rel="noopener" title="' + esc(l.href) + '">' +
                        '<span class="spp-fl-dot">' + esc(initialOf(l.name)) + '</span>' +
                        '<span class="spp-fl-txt"><b>' + esc(l.name) + '</b><i>' + esc(l.host) + '</i></span>' +
                    '</a>';
                }).join('') + '</div>';
            // ★ 2.52.97:这里原有一行「卡片样式:uiverse.io/... — MIT License © ...」的
            //   署名脚注(.spp-fl-foot),按用户要求从界面撤掉。
            //   ★ MIT 声明照旧保留 —— 上面的段落注释与本函数上方那段署名都还在,
            //     许可要的是"随源码保留声明",不是"印在页面上"。
            return card;
        }

        function renderFriendCards() {
            if (!onFlCard || !onRmCard) return;   // 它寄居在人民囧府那一行里
            const host = document.querySelector('.spp-rm-row td');
            if (!host) return;
            if (host.querySelector('.spp-fl-two')) return;   // 已渲染
            const links = pickFriendLinks();
            if (!links.length) return;

            // 把人民囧府那张卡与友链卡并排(两栏)
            const grid = host.querySelector('.spp-cm-grid');
            if (!grid) return;
            const two = document.createElement('div');
            two.className = 'spp-fl-two';
            host.insertBefore(two, grid);
            two.appendChild(grid);
            two.appendChild(buildFriendCard(links, { dense: 'tight' }));

            // 宿主放开 overflow(站点给 .t 加了 overflow:hidden!important,会裁掉卡边渐变)
            const t = host.closest('.t');
            if (t) t.classList.add('spp-fl-host');

            // 尺寸就绪后算一次 hover 变换的缩放;图片/字体回流会改变高度,故再补一次
            syncAllFriendCardScales();
            setTimeout(syncAllFriendCardScales, 60);
        }

        // 窗口尺寸变化 → 卡片宽度变 → 缩放要重算(带节流)
        let _flRzTimer = null;
        window.addEventListener('resize', function () {
            if (_flRzTimer) clearTimeout(_flRzTimer);
            _flRzTimer = setTimeout(syncAllFriendCardScales, 120);
        });

        applyHomeStyle();
        onDomReady(function () {
            safeInit('首页美化·统计条', polishStats);
            // 顺序要紧:徽标要插进统计条刚建好的 .spp-hm-acts 里,必须等 polishStats 先跑完
            safeInit('首页美化·在线人数', moveOnlineStats);
            safeInit('首页美化·用户信息区', function () { polishUserInfo(onUPanel); });
            safeInit('首页美化·公告', polishNotice);
            // 顺序要紧:茶馆两侧的「最新讨论 / 最新回复」复用顶部那四栏的快照,
            // 所以必须先渲染顶部(采集快照)再渲染子版卡片区。
            safeInit('首页美化·顶部四栏', renderTopCards);
            safeInit('首页美化·漫区卡片', renderCmCards);
            safeInit('首页美化·综合交流卡片', renderHjCards);
            safeInit('首页美化·人民囧府卡片', renderRmCards);
            // 顺序要紧:友链卡要插进人民囧府那一行的 td 里,必须等它先渲染完
            safeInit('首页美化·友情链接卡片', renderFriendCards);
            safeInit('首页美化·子版卡片', renderSubCards);
            // 站点可能异步刷新版块列表(折叠/展开、ajax 换页),补一次
            setTimeout(function () {
                safeInit('首页美化·顶部四栏(延迟)', renderTopCards);
                safeInit('首页美化·子版卡片(延迟)', renderSubCards);
            }, 1200);
            setTimeout(function () { safeInit('首页美化·漫区卡片(延迟)', renderCmCards); }, 1200);
            setTimeout(function () { safeInit('首页美化·综合交流卡片(延迟)', renderHjCards); }, 1200);
            setTimeout(function () { safeInit('首页美化·人民囧府卡片(延迟)', renderRmCards); }, 1200);
            setTimeout(function () { safeInit('首页美化·友情链接卡片(延迟)', renderFriendCards); }, 1400);
            // 站点异步刷新在线人数,补一次(polishStats 有 dataset 守卫,重复调用是安全的)
            setTimeout(function () { safeInit('首页美化·在线人数(延迟)', moveOnlineStats); }, 1600);
        });

        // ★★ 2.53.62:这里原有第三枚 Userscript 菜单命令 ——
        //   「🃏 首页 Comic Market 卡片范围:…」:点一次在 仅最新一届 / C104 起(含最新)/
        //   漫区特设全部 之间循环,写存储后 reload。用户附篡改猴脚本列表截图(红圈圈着那一行)
        //   说「框选的这个不要显示」,追问后定案「彻底删掉,不要这个开关」⇒
        //   注册连同三档循环一起删除,范围由上面那处调用点的字面量 'cm' 决定。
        //   ★ 另外两枚菜单命令(⚙ 设置页面宽度 / ⚙ 设置翻页步长)照旧,一字未改。
    }


    // ==================== 16b. 星空背景(全站 · uiverse amir_6539/red-duck-82 移植) ====================
    // 星点坐标取自原 CSS,压缩为 "x y x y ..." 数字串,运行时还原为 box-shadow。
    const SPP_STAR_LAYERS = [
        { key: 's1', size: 1, dur: 50 },
        { key: 's2', size: 2, dur: 100 },
        { key: 's3', size: 3, dur: 150 }
    ];

    const SPP_STAR_PTS = {
        s1: '501 811 1450 1324 1093 1780 1469 678 904 741 1160 781 1841 1962 1630 1667 1788 676 367 1734 1343 156 1283 1142 1062 378 1395 467 1017 1891 137 1114 1767 1403 1543 11 1078 181 1189 1574 1697 1551 439 472 1491 677 1364 599 34 382 1221 1584 1266 1499 169 1907 1219 1125 659 18 1731 1959 332 1216 1913 788 80 712 326 1605 574 1502 473 1653 404 975 322 1797 425 1321 1121 1797 731 647 891 1584 1523 109 1379 244 865 1064 493 956 624 1380 440 619 1630 767 955 1196 62 729 126 946 1256 896 1444 256 661 1628 1078 1716 300 737 1734 413 1296 129 1771 1678 977 1764 1879 549 665 1531 89 701 1084 1183 1597 1576 1354 1774 554 1471 1469 287 887 106 1962 766 638 805 1651 741 1517 1826 24 1152 507 558 1262 652 246 1048 1077 421 1866 1847 1986 1561 704 632 1991 1875 1227 395 45 1116 247 786 890 607 787 1235 557 524 1582 1285 1725 1366 952 747 251 458 1500 1250 1999 1734 1336 1955 1705 1464 728 697 594 510 1345 1990 1919 1803 1117 966 1629 97 1046 1196 810 1092 722 976 406 18 1665 1860 1758 1628 1183 463 564 239 13 1767 1482 1472 1700 347 1362 244 1141 1708 22 885 374 1309 1034 1037 1725 1086 1343 1921 596 903 1061 478 18 1409 729 1364 264 911 677 1442 123 33 1303 646 1945 792 1305 938 918 1536 620 948 183 646 695 687 881 272 1521 1212 1423 1022 1545 1271 1393 348 685 1910 1446 856 73 1201 736 999 673 796 469 850 1912 142 1278 664 184 1990 1173 1312 782 1879 323 1035 611 908 565 1449 748 1713 1047 490 1040 1872 1818 1659 1806 1327 386 575 1550 463 148 687 651 1683 1588 1194 1831 2 581 876 1396 1743 1212 1810 421 1920 658 1461 1859 1809 1456 388 186 1627 1528 1145 171 97 674 1072 676 1052 1165 1131 1088 781 1231 948 330 257 426 1046 549 652 1338 74 1749 364 931 369 383 1428 1558 389 927 133 234 1888 1785 1617 556 643 401 275 406 1644 1253 1852 1599 883 744 1721 524 1297 1226 1177 1679 55 874 1811 838 790 1241 430 1676 652 1191 568 53 1990 1163 237 61 223 592 456 1844 271 1324 1488 1373 717 1822 709 1464 941 1445 1118 991 1414 1964 1076 108 172 641 1722 1539 427 1697 45 1301 1353 1060 329 967 1396 493 301 1228 1406 1211 1653 444 1822 1746 353 1449 381 671 887 650 138 30 1839 1094 1405 273 796 1618 1964 1045 1849 1472 1155 1529 1312 728 448 44 1908 691 818 254 293 1981 1133 1307 375 196 316 1241 1975 1138 1706 1769 463 1768 1428 1730 590 1780 523 1862 1526 1613 909 1266 1781 470 352 699 1682 1002 614 1209 133 1842 518 1422 1836 1720 1901 470 1788 1355 1387 146 1162 933 80 681 1063 313 1341 740 1498 168 1014 345 1355 1498 1562 1626 1358 890 403 663 562 1481 168 22 719 774 1041 1899 829 430 158 430 361 1592 1334 224 323 1639 1131 7 271 1646 1514 1605 1444 1820 1665 1549 1641 1609 1377 486 1098 229 613 542 1694 318 256 1861 918 889 892 442 1524 19 422 1935 1908 828 109 862 1248 1275 560 906 63 337 1605 1691 918 1414 679 1726 749 1540 1149 1337 1466 446 430 676 1616 840 326 976 977 1840 642 1273 804 1071 928 1292 1675 29 1148 1585 135 1007 563 1035 78 1174 574 120 1304 845 1292 861 540 234 232 1940 1367 759 639 1775 1381 906 372 1104 1165 1524 911 1882 330 1389 700 300 1629 220 1614 563 140 1611 1586 793 1316 325 1070 1722 1462 1406 1120 1169 1768 1956 1053 959 1587 585 1566 370 204 1606 1416 443 1606 1499 1102 1943 105 1121 1594 1512 32 871 1425 433 100 294 1471 1688 1755 1666 591 1034 300 734 1178 1342 313 1616 1590 1763 1472 632 1935 1708 872 1871 915 1829 1020 1599 578 42 585 1163 1382 1744 1272 984 1426 1786 1584 1813 379 1867 1127 97 567 626 988 1178 79 1703 211 961 1785 110 975 953 1941 1027 1790 1665 107 11 964 1718 1147 21 1728 1358 1922 872 65 1191 1635 762 681 1519 1033 906 566 1074 657 1093 415 51 198 1075 1418 1547 1070 225 920 850 1974 981 595 1425 131 460 917 56 495 714 428 920 493 470 1521 532 821 1905 71 883 1501 294 196 381 1999 332 793 1246 408 233 149 315 231 1594 1302 696 1585 791 136 479 199 1627 1413 1824 924 1631 342 1251 1151 284 1781 497 1052 204 1161 646 1499 1762 558 854 1833 883 945 44 982 1101 834 515 1748 1578 1435 819 1258 776 670 115 385 1478 434 885 20 192 1513 78 1129 1774 1105 955 1149 1817 1929 1106 1832 1107 1997 94 23 243 982 43 1972 1798 673 1131 1589 841 14 826 345 687 56 1084 32 1887 1878 153 526 1828 253 1947 1105 886 700 1307 1723 1274 651 1530 837 1699 1637 1703 1331 1929 1557 1763 737 1118 1680 1545 692 1462 1092 208 1667 1393 859 186 1794 351 1199 642 1995 1061 1726 1708 115 1233 1305 637 1786 1730 603 75 1240 1704 1326 584 346 438 1554 561 513 1382 225 467 1674 1403 815 1546 1835 127 1119 276 591 688 1458 765 646 474 984 171 361 94 1480 1962 1666 909 1037 1725 222 253 1355 1892 1901 275 1847 28 1184 1725 1382 882 647 1935 1046 10 344 292 1328 127 1352 752 929 1589 384 284 1829 381 820 1229 1125 777 429 1811 1499 1573 287 295 756 389 616 781 41 1092 333 794 1588 386 1847 1802 710 662 60 640 264 463 746 1859 799 763 37 639 396 357 1071 1190 1430 1814 257 1382 235 606 1304 1939 1470 1124 349 307 1567 310 1323 1145 922 1196 1922 1647 544 788 1337 257 632 1413 414 590 620 582 794 1702 1481 1055 53 157 346 50 1901 1038 1369 796 1941 215 194 1567 1538 367 800 1044 489 1109 1712 524 327 525 1252 1475 1240 529 436 795 834 122 1371 79 482 520 1249 336 1878 188 944 325 1259 1491 1942 620 1054 1606 1153 1448 502 53 1381 107 1670 1380 618 967 1557 1116 1722 1174 1044 1805 717 663 394 1848 1007 389 802 49 392 1650 852 1678 1012 335 1009 1818 1631 1568 742 1162 1991 52 1190 1401 928 119 1549 537 1529 2 1709 122 387 543 2 27 1971 507 1377 1362 1080 1031 1544 1631 1174 1603 312 1626 1422 1430 615 1958 1431 1946 1412 1848 247 984 1808 1396 225 319 717 1252 875 1619 156 951 1971 386 355 1406 1151 273 1538 844 1570 947 151 1363 525 209 307 1923 1718 993 1741 1513 353 1353 61 664 352 1382 359 1487 1707 657 1045 1107 490 1834 1176 837 1438 1947 448 1196 333 151 555 18 992 458 748 1801 890 1093 1012 315 1101 194 323 754 292 1737 7 40 840 1170 805 176 1753 805 1148 1578 1271 367 1494 363 1111 1955 243 1451 1093 375 617 1223 720 1178 13 1456 865 1440 49 186 1569 320 1853 300 539 1559 509 1985 1108 1588 828 525 1432 831 363 141 281 1319 402 40 456 1955 478 1758 818 1924 688 1030 953 1982 210 917 1401 1051 1837 1045 463 1744 573 529 1530 542 469 1982 324 1902 1422 1968 782 1666 1561 955 304 323 778 272 443 485 581 1353 1058 1257 131 434 98 1587 1953 1749 68 1984 839 1518 183 1071 855 1662 1994 1111 106 1954 838',
        s2: '1925 1320 693 1778 1016 711 1171 563 661 1919 1610 44 1275 140 1208 1802 1473 1587 11 1117 853 1757 1149 937 1353 428 270 279 258 1404 417 1188 286 561 393 1765 147 881 666 1097 1425 1278 806 156 1252 561 218 52 1371 1980 171 745 1424 89 137 244 939 1922 137 1080 1757 50 904 536 1938 1001 1172 440 72 1475 102 121 804 1671 1314 270 440 1341 1216 511 1061 1523 97 274 704 1318 52 1872 1962 296 111 289 1157 1236 1347 1451 820 286 1389 1169 644 841 1286 522 955 659 428 1805 237 557 1689 1058 636 1882 1349 1664 1548 432 1841 504 302 252 827 1765 620 123 207 748 1454 1234 1967 1790 542 33 742 1214 255 1402 74 1772 699 475 980 1253 534 1676 909 202 1498 1251 1796 120 1409 1263 1627 995 969 710 1674 676 1832 759 1623 563 251 1790 96 1688 886 239 778 150 1767 430 765 1259 1189 877 444 1629 1560 324 1952 1097 712 1173 541 911 827 1420 1233 285 784 546 645 285 1273 1255 1821 174 221 1795 1004 456 1298 941 274 387 174 376 1491 258 1489 1946 1134 1382 1289 1145 464 358 1249 1842 1665 831 1982 84 541 774 1994 523 762 1644 1730 867 1951 1287 911 1691 1454 725 1287 1940 70 564 1980 638 1674 1774 1720 116 1747 182 1040 450 1795 375 857 1471 1326 1730 915 274 1224 358 1808 60 43 1870 1810 1536 1564 1719 731 1388 1953 1967 1744 1119 794 1384 959 714 18 1932 1358 1437 355 939 1355 1648 608 719 383 758 1164 1681 1045 253 424 1279 1899 359 379 488 214 465 179 905 830 1993 448 1077 1880 1354 1973 347 745 1025 788 1007 1377 883 6 290 1312 407 1398 622 1405 339 1198 1709 988 1226 87 1459 1113 1698 997 732 708 331 1876 1112 1729 1797 719 703 1295 522 758 1061 1309 1014 1327 1365 854 1317 531 1001 1751 1040 1354 190 800 1538 88 1455 668 39 1379 41 892 524 54 649 1289 730 727 488 181 842 1230 64 3 857 292 1201 1343 673 1096 1412 1520 292 104 1683 934 1387 314 739',
        s3: '200 981 1731 521 132 1039 1888 1547 899 1226 1887 580 1548 1092 1626 689 254 1072 1684 1211 672 1267 939 668 1969 645 1126 983 457 568 476 876 829 1896 1364 1846 1507 1120 936 1948 1833 832 1424 285 1377 1596 432 153 1348 1410 1529 954 1102 387 264 297 811 977 1931 673 1734 978 1772 1567 1197 1400 764 282 1103 822 872 1803 1057 1763 52 1299 1312 1236 235 1082 299 1086 1017 1602 1950 626 1306 132 1358 1618 1873 1718 1447 940 1888 1195 1704 1765 872 1357 1555 1120 250 1415 450 415 492 901 170 1641 56 1129 627 1514 1221 500 324 1895 1397 1775 1966 598 1550 763 326 1605 261 969 890 281 736 544 589 1262 1581 368 1900 1132 1914 585 1864 1517 241 217 859 787 996 1729 741 121 418 414 142 967 387 896 703 562 968 1136 1682 332 1287 846 256 1427 1885 432 1739 1458 345 1769 1140 1612 192 1921 920 471 834 881 917 1803 466 1266 483 1108 689 986 1279 786 458 910 1250 870 785 1654 1543 1757 287 1272'
    };

    function sppStarShadow(pts) {
        const a = pts.split(' ');
        const out = [];
        for (let i = 0; i + 1 < a.length; i += 2) out.push(a[i] + 'px ' + a[i + 1] + 'px #fff');
        return out.join(',');
    }

    function initStarfield() {
        const KEY = 'spp_home_starfield';
        const GLASS = 'spp_home_star_glass';
        if (!SPP_CONFIG.bool(KEY, true)) return;
        const glass = SPP_CONFIG.bool(GLASS, true);

        const CSS = `
    #spp-starfield {
        position:fixed; inset:0; z-index:-1; overflow:hidden; pointer-events:none;
        background:radial-gradient(ellipse at bottom, #1b2735 0%, #090a0f 100%);
    }
    #spp-starfield > i {
        position:absolute; top:0; left:0; display:block; background:transparent;
        box-shadow:var(--spp-star-sh);
        animation:spp-star-move linear infinite;
        will-change:transform;
    }
    #spp-starfield > i::after {
        content:" "; position:absolute; top:2000px; left:0;
        width:var(--spp-star-size); height:var(--spp-star-size);
        background:transparent; box-shadow:var(--spp-star-sh);
    }
    @keyframes spp-star-move {
        from { transform:translateY(0); }
        to   { transform:translateY(-2000px); }
    }
    ` + (glass ? `
    /* 页面底色让位给星空 */
    html.spp-hm-star, html.spp-hm-star body {
        background-color:transparent !important; background-image:none !important;
    }
    html.spp-hm-star #wrapA, html.spp-hm-star #main,
    html.spp-hm-star #content, html.spp-hm-star .contentc,
    html.spp-hm-star #wall, html.spp-hm-star #wall ul.stream {
        background:transparent !important;
    }
    /* 内容容器 → 毛玻璃,星空从背后透出(首页区块 / 帖子 / 表格 / 用户信息框)
       注意:#infobox 若已挂上 .spp-up-on(用户信息区深色卡),下面这条会把深色底刷回浅色,
       所以用 :not(.spp-up-on) 把它排除掉。
       ★ 2.52.78:#infobox:not(.spp-up-on) 已从这份【浅色】清单里移出 —— 见下面
         「社区论坛任务页」那一段。任务页的 #infobox 就是用户圈出来的浅灰横条,
         它必须是深色横条板块,不能再走浅色毛玻璃。 */
    html.spp-hm-star #main .t, html.spp-hm-star #main .t5:not(.spp-t5bar),
    html.spp-hm-star table.t_one {
        background:rgba(250,252,255,0.93) !important;
        border-color:rgba(255,255,255,0.66) !important;
        box-shadow:0 16px 46px rgba(2,6,23,0.46) !important;
    }
    html.spp-hm-star #main .t:hover, html.spp-hm-star #main .t5:hover {
        box-shadow:0 20px 56px rgba(2,6,23,0.56) !important;
    }
    /* 图墙卡片(数量可达上百张,不加 backdrop-filter,避免合成层开销) */
    html.spp-hm-star #wall ul.stream > li {
        background:rgba(250,252,255,0.94) !important;
        border-color:rgba(255,255,255,0.66) !important;
        box-shadow:0 10px 30px rgba(2,6,23,0.42) !important;
    }
    html.spp-hm-star #wall ul.stream > li:hover {
        background:rgba(250,252,255,0.98) !important;
        box-shadow:0 16px 40px rgba(2,6,23,0.55) !important;
    }
    /* 表格内部一律透明,避免出现"白行/灰行"拼接 */
    html.spp-hm-star #main .t table,
    html.spp-hm-star #main .t tbody,
    html.spp-hm-star #main .t tr,
    html.spp-hm-star #main .t tr > td,
    html.spp-hm-star #main .t tr > th,
    html.spp-hm-star #main .t5 table,
    html.spp-hm-star #main .t5 tbody,
    html.spp-hm-star #main .t5 tr,
    html.spp-hm-star #main .t5 tr > td,
    html.spp-hm-star #main .t5 tr > th,
    html.spp-hm-star table.t_one tbody,
    html.spp-hm-star table.t_one tr,
    html.spp-hm-star table.t_one tr > td,
    html.spp-hm-star table.t_one tr > th {
        background:transparent !important;
    }
    html.spp-hm-star #main .t tr:hover,
    html.spp-hm-star #main .t tr:hover > td,
    html.spp-hm-star #main .t tr:hover > th,
    html.spp-hm-star #main .t5:not(.spp-t5bar) tr:hover > td,
    html.spp-hm-star #main .t5 tr:hover > th {
        background:rgba(47,111,104,0.07) !important;
    }
    /* 顶栏 / 页头 —— 2.52.76:整条换成「深空玻璃背景板」
       ★ 原来是 rgba(244,248,252,0.80) 的**浅色**毛玻璃(站点自己的浅底配方)。
         深空星空页面上,这块浅底就是顶栏里那条灰带 —— 必须一起换掉,
         否则导航条那条半透明玻璃(下面 #mainNav 规则)会透出浅底,又亮回去。 */
    html.spp-hm-star #toptool, html.spp-hm-star #header {
        background-color:rgba(9,13,21,0.62) !important;
        background-image:
            linear-gradient(180deg, rgba(255,255,255,0.10) 0%, rgba(255,255,255,0.02) 40%, rgba(255,255,255,0) 74%) !important;
        background-repeat:no-repeat !important;
        backdrop-filter:blur(18px) saturate(150%) !important;
        -webkit-backdrop-filter:blur(18px) saturate(150%) !important;
    }
    html.spp-hm-star #mainNav {
        /* 2.52.76:导航条本该是「深空玻璃背景板」,站点却在这块上贴了一张
           bg_topnav 渐变图(顶部浅灰 → 底黑)。在深空页面上,那张图的顶部就是
           一条横贯全宽的灰带 —— 用户圈的正是它(实测顶端 rgb(58,59,59)、往下收到黑)。
           这里把底图整个换掉,自绘一块玻璃板:深底 + 顶部一道高光(玻璃反光)
           + 左上一抹冷色反光;投影与底部圆角沿用原配方。 */
        background-color:rgba(9,13,21,0.62) !important;
        background-image:
            linear-gradient(180deg, rgba(255,255,255,0.10) 0%, rgba(255,255,255,0.02) 40%, rgba(255,255,255,0) 74%),
            radial-gradient(140% 220% at 10% -90%, rgba(120,190,255,0.16) 0%, rgba(0,0,0,0) 62%) !important;
        background-repeat:no-repeat !important;
        box-shadow:0 6px 24px rgba(2,6,23,0.5) !important;
        border-radius:0 0 8px 8px;
        /*   ★★ 2.52.84(用户红色双箭头 + 红框圈「搜索」胶囊:「已基本贴合,适当增加宽度」):
           导航条与下方面板的元素边是贴死的 —— 实测 #mainNav 底=28、#infobox 顶=28(零间距),
           可视卡片顶=43 只剩 15px 缓冲,「搜索」胶囊的辉光几乎蹭到面板框。
           加 14px 呼吸间距;导航条在各页族同构,这一条全站生效,
           下方内容整体随之下移(页高 +14px)。 */
        margin-bottom:14px !important;
    }
    html.spp-hm-star #footer { color:#8fa0b5 !important; }

    /* ---------- 2.52.78:社区论坛任务页(plugin.php?H_name-tasks*) ----------
       用户批注(截图 + 圈注):「参考主页美化」「单独一个横条板块,美化」「UI包裹、美化」。
       该页实测有三条站点默认浅灰横条(截图取色 → CSS 坐标):
         #header      y   5.. 91  采样 rgb(192,192,204)             ← 2.52.76 已换深空玻璃板
         #infobox     y 119..176  bgc rgba(250,252,255,.93)        ← 浅灰横条 1(用户绿框)
         #breadcrumbs y 184..211  bgc rgb(248,248,248) + bg_tr.gif ← 浅灰横条 2(用户蓝箭头)
       根类名 spp-hm-task 由 initTaskPage() 挂在 <html> 上,因此本段只在任务页生效:
       不碰个人中心页(那边 2.52.73 已定稿「去灰底、留文字」),也不碰版块页。
       ★ 一律用 inset 阴影代替 border —— 加 border 会改盒模型,把 #main 整体下推。
       ★★ 2.52.79:#infobox 那条现在是【未登录时的兜底】。登录态下任务页已并入
          initBoardTop(与版块页/帖子页/搜索页同一套),polishUserInfo(true) 会给 #infobox
          挂上 .spp-up-on,主页那张深色用户卡接管,:not(.spp-up-on) 自然失配。
          未登录时 #user_info 是登录表单、polishUserInfo 会整体退回不动 ——
          这条至少保证它还是一条深色横条,而不是亮回浅灰。 */
    html.spp-hm-star.spp-hm-task #infobox:not(.spp-up-on) {
        background-color:rgba(14,18,26,0.82) !important;
        background-image:linear-gradient(180deg, rgba(255,255,255,0.085) 0%, rgba(255,255,255,0.015) 44%, rgba(255,255,255,0) 78%) !important;
        background-repeat:no-repeat !important;
        border-radius:12px !important;
        box-shadow:0 16px 46px rgba(2,6,23,0.50), inset 0 0 0 1px rgba(255,255,255,0.075), inset 0 1px 0 rgba(255,255,255,0.06) !important;
        backdrop-filter:blur(18px) saturate(150%) !important;
        -webkit-backdrop-filter:blur(18px) saturate(150%) !important;
        color:#c7d3e3 !important;
    }
    html.spp-hm-star.spp-hm-task #infobox:not(.spp-up-on) a { color:#8fb6ee !important; }
    html.spp-hm-star.spp-hm-task #infobox:not(.spp-up-on) a:hover { color:#d5e6ff !important; }
    html.spp-hm-star.spp-hm-task #infobox:not(.spp-up-on) .gray,
    html.spp-hm-star.spp-hm-task #infobox:not(.spp-up-on) .f10 { color:#8b98a8 !important; }
    html.spp-hm-star.spp-hm-task #infobox:not(.spp-up-on) b,
    html.spp-hm-star.spp-hm-task #infobox:not(.spp-up-on) strong { color:#e3ecf7 !important; }

    /* 面包屑:单独成一条「横条板块」
       (原来是与正文分离的 1400×27 浅灰条 + 1px #ddd 描边 + bg_tr.gif 位图) */
    html.spp-hm-star.spp-hm-task #breadcrumbs {
        background-color:rgba(14,18,26,0.82) !important;
        background-image:linear-gradient(180deg, rgba(255,255,255,0.075) 0%, rgba(255,255,255,0.012) 46%, rgba(255,255,255,0) 80%) !important;
        background-repeat:no-repeat !important;
        border-color:rgba(255,255,255,0.075) !important;
        border-radius:9px !important;
        box-shadow:0 10px 30px rgba(2,6,23,0.42), inset 0 1px 0 rgba(255,255,255,0.05) !important;
        backdrop-filter:blur(16px) saturate(145%) !important;
        -webkit-backdrop-filter:blur(16px) saturate(145%) !important;
        color:#b6c3d3 !important;
        padding:0 14px !important;
        line-height:30px !important;
    }
    html.spp-hm-star.spp-hm-task #breadcrumbs a { color:#8fb6ee !important; }
    html.spp-hm-star.spp-hm-task #breadcrumbs a:hover { color:#d5e6ff !important; }
    html.spp-hm-star.spp-hm-task #breadcrumbs img { opacity:.72; vertical-align:middle; }
    /*   ★★ 2.52.82(用户蓝箭头:「当前所在位置应该是绿色」):面包屑的结构是
       img + a(南+ South Plus → index.php) + 「 » » + a(社区论坛任务 → hack.php?H_name-tasks…)。
       最后这个链接就是「当前位置」,原来跟着 a 的通用配色走蓝(#8fb6ee),和祖先链一个颜色,
       分不出「我在哪」。给 a:last-of-type 单独给绿色 —— 色相同页内强调竖线 / 悬停高光的
       那支青绿(#20c997 家族),不是另起一支。 */
    html.spp-hm-star.spp-hm-task #breadcrumbs a:last-of-type { color:#3fd9a4 !important; }
    html.spp-hm-star.spp-hm-task #breadcrumbs a:last-of-type:hover { color:#8bf2cf !important; }

    /* 「UI包裹」:该插件的外壳是一张裸 <table>(两个 td 分左右栏),不像其它页那样
       有 .t 卡片兜底 —— 于是左右两栏之间的空档在星空页上直接露出深空,看起来像破了个洞。
       这里把整张表包成一块深空玻璃工作台(星空从背后透出),两栏在台面上留 14px 内边距。 */
    html.spp-hm-star.spp-hm-task #main > table {
        background-color:rgba(13,18,27,0.72) !important;
        background-image:linear-gradient(180deg, rgba(255,255,255,0.055) 0%, rgba(255,255,255,0.008) 40%, rgba(255,255,255,0) 82%) !important;
        background-repeat:no-repeat !important;
        border-collapse:separate !important;
        border-radius:14px !important;
        box-shadow:0 18px 54px rgba(2,6,23,0.52), inset 0 0 0 1px rgba(255,255,255,0.065) !important;
        backdrop-filter:blur(16px) saturate(145%) !important;
        -webkit-backdrop-filter:blur(16px) saturate(145%) !important;
        overflow:hidden !important;
    }
    html.spp-hm-star.spp-hm-task #main > table > tbody > tr > td {
        padding:14px !important;
        vertical-align:top !important;
    }
    /* 台面上的任务卡:星空浅色毛玻璃早已命中,但 border-radius:0、表头 td.h 被
       「表格内部一律透明」那条刷成无底 → 整块像一张平铺的浅灰纸。这里补圆角 + 表头条
       + 分隔线 + 文字配色。表头用 inset 阴影画强调竖线,不写 border(不改盒模型高度)。 */
    /*   ★★ 2.52.80(用户第二轮批注:「这里的白色面板还是优化的不好,要用UI包裹」):
       2.52.78 只把表头 td.h 刷深了,卡身 div.t 本身还是站点那块 rgba(250,252,255,.93)
       浅色毛玻璃 —— 于是深色工作台上摆着四张白纸。实测卡身底色挂在 div.t 上
       (不在 table / tr / td 那几层),所以换底必须换在 div.t 这一层,再把内部
       table / tr / td 压成透明(站点 .tr3 / .f_one 还会各自再刷一层浅色)。
       配色同时要反过来:2.52.78 给 b / font[color] 配的是照浅底挑的深色
       (#22303f / #2f7d5f),深底上直接读不出来。 */
    /*   ★★ 2.52.81(用户指定 UI):「用这个按钮UI包裹作为一种美化框 ——
       https://uiverse.io/adamgiebl/wise-moth-35」(MIT)。已用浏览器把它源码取回来
       (tools/probes/get-uiverse.js),它的三层结构原样复刻,站点这边纯 CSS,
       不往站点 DOM 里塞它的 <span>/<div>:
         div.t          ← .button          外圈银渐变 56→36→41 + 2px 内边距当银边 + 12px 圆角
         div.t > table  ← .button span     暗面 rgb(29,29,29) + 三枚 radial 高光 + 10px 圆角
         div.t::after   ← .button-overlay  拉丝纹理 repeating-conic,opacity 10%
       ★ 加 padding 会改盒模型 → 同时钉 box-sizing:border-box(2.52.78 起一律用 inset
         阴影代替 border,图的就是不改盒模型;这里同理)。
       ★ 站点自己在 .t 上有一条白描边(实测 border ≈0.67px solid rgba(255,255,255,.66))
         —— 只改颜色、不改宽度:既不推布局,银边外也不会再套一圈白线。
       ★★ 2.52.82:左栏三块(功能简介 / 操作 / 版权)【退出本配方】—— 用户第三轮批注指认
         这圈银边在深底上读作「白色线框」,黄箭头要求移除;下方有一条 td:first-child > div.t
         的平替规则把它换回深空玻璃,并撤掉本条的 padding 内衬与拉丝伪元素。
         右栏任务卡(用户没圈)原样保留 uiverse 框。 */
    html.spp-hm-star.spp-hm-task #main > table div.t {
        position:relative;
        z-index:0;
        box-sizing:border-box !important;
        padding:2px !important;
        background-color:rgb(36,36,36) !important;
        background-image:linear-gradient(180deg, rgb(56,56,56) 0%, rgb(36,36,36) 66%, rgb(41,41,41) 100%) !important;
        background-repeat:no-repeat !important;
        border-color:rgb(56,56,56) !important;
        border-radius:12px !important;
        overflow:hidden !important;
        box-shadow:0 14px 34px rgba(2,6,23,0.50) !important;
        color:rgb(218,218,218) !important;
    }
    /* .button-overlay:那层银色拉丝。用伪元素,pointer-events:none 保证链接照样点得到。
       父级钉了 z-index:0(自成层叠上下文),伪元素 z-index:1 才会盖在内容之上而不是之下。 */
    html.spp-hm-star.spp-hm-task #main > table div.t::after {
        content:'' !important;
        position:absolute !important;
        inset:0 !important;
        z-index:1 !important;
        pointer-events:none !important;
        background:repeating-conic-gradient(rgb(48,47,47) 0.0000001%, rgb(51,51,51) 0.000104%) 60% 60%/600% 600% !important;
        opacity:.10;
    }
    /* 卡身内部:table 这一层现在是 uiverse 的暗面(.button span 的对应物,不能再压透明),
       tr / td 仍然压平 —— 站点 .tr3 / .f_one 会各自再刷一层浅色。 */
    html.spp-hm-star.spp-hm-task #main > table div.t > table {
        background-color:rgb(29,29,29) !important;
        background-image:radial-gradient(at 95% 89%, rgb(15,15,15) 0px, transparent 50%),
                         radial-gradient(at 0% 100%, rgb(17,17,17) 0px, transparent 50%),
                         radial-gradient(at 0% 0%, rgb(29,29,29) 0px, transparent 50%) !important;
        border-radius:10px !important;
        color:rgb(218,218,218) !important;
    }
    html.spp-hm-star.spp-hm-task #main > table div.t > table > tbody > tr,
    html.spp-hm-star.spp-hm-task #main > table div.t > table > tbody > tr > td {
        background-color:transparent !important;
        background-image:none !important;
    }
    html.spp-hm-star.spp-hm-task #main > table div.t > table > tbody > tr > td.h {
        background-color:rgba(17,23,33,0.97) !important;
        background-image:linear-gradient(180deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0) 74%) !important;
        background-repeat:no-repeat !important;
        color:#dfe8f3 !important;
        box-shadow:inset 3px 0 0 rgba(32,201,151,0.85), inset 0 -1px 0 rgba(255,255,255,0.08) !important;
        /* 2.52.81:暗面(table)是 10px 圆角,表头贴在它顶上 —— 不跟着圆角,
           方角会把 table 的圆角盖掉(表格背景本身是按 border-radius 裁的)。 */
        border-radius:10px 10px 0 0 !important;
        /* uiverse 原文 .button span 上的 text-shadow,只加在表头上当标题光晕 */
        text-shadow:0 0 20px rgba(75,75,75,0.9);
        font-weight:650 !important;
        letter-spacing:.02em;
        padding:2px 12px 0 !important;
    }
    /* ★★ 2.52.81:用户箭头指的那条「白色虚线」,实测是【站点自己】画的
       border-bottom:1px dashed #ddd(挂在 <td class="li"> 上,不是我那条渐变)。
       用户要「移除」—— 那就连同我 2.52.78/2.52.80 画的那条冷白渐变一起去掉:
       两行之间只留留白(td.li 那 3px 高度还在,不会塌),分组交给卡片外框。
       ★ 只把 border 写成 none、宽度不动会留 0.67px 的缝,所以显式 width/style 一起写。 */
    html.spp-hm-star.spp-hm-task #main > table div.t tr.f_one > td.li {
        border-bottom:0 none !important;
        background-image:none !important;
        background-color:transparent !important;
    }
    /*   ★★ 2.52.83(用户第三轮回访:「白线没移除」+ 三处新圈注):上一轮拆的是 uiverse 银边,
       但用户圈的白线另有其人 —— 站点自己给这张插件表画的【表格网格线】:
         每个表头 td.h      → border-bottom: 0.67px solid rgb(187,187,187)(#bbb)
         每个内容行 td      → border-bottom: 0.67px solid rgb(238,238,238)(#eee)
       (装机态 computed 实测;截图采样亮度 175-238 与这两个色值逐一对上 ——
         功能简介/操作/版权的表头下、操作四行每行下、版权行下、右栏表头下,全是它。)
       浅色主题里这是正常表格网格;2.52.80 的压平只清了 background(.tr3/.f_one 的底),
       没清 border —— 经验 1.10「换底是两件事」漏了第三件。这里整页一次清零:
       行间分隔交给已有的自绘元素(操作行发丝线 / 表头深色底+圆角 / 留白)。 */
    html.spp-hm-star.spp-hm-task #main > table div.t > table > tbody > tr > td {
        border-bottom: 0 none !important;
    }
    html.spp-hm-star.spp-hm-task #main > table div.t tr.f_one:hover > td { background-color:rgba(47,111,104,0.10) !important; }
    html.spp-hm-star.spp-hm-task #main > table div.t font[color] { color:#7cc6ff !important; }
    html.spp-hm-star.spp-hm-task #main > table div.t b { color:#e8f0f9 !important; }
    html.spp-hm-star.spp-hm-task #main > table div.t a { color:#8fb6ee !important; }
    html.spp-hm-star.spp-hm-task #main > table div.t a:hover { color:#d5e6ff !important; }
    /* 「操作」那四行是导航入口,给成浅底胶囊 —— 只在任务页、只认指向本插件自己的链接,
       版权那两条外链保持普通文字链接的样子(否则正文里平白多两个药丸)。 */
    html.spp-hm-star.spp-hm-task #main > table div.t a[href^="plugin.php?H_name-tasks"] {
        display:inline-block;
        padding:2px 11px;
        border-radius:6px;
        background-color:rgba(255,255,255,0.045);
        box-shadow:inset 0 0 0 1px rgba(255,255,255,0.055);
        text-decoration:none !important;
        transition:background-color .15s ease, color .15s ease;
    }
    html.spp-hm-star.spp-hm-task #main > table div.t a[href^="plugin.php?H_name-tasks"]:hover {
        background-color:rgba(47,111,104,0.24);
        color:#d5e6ff !important;
    }
    /* ★★ 2.53.17(用户批注):「社区论坛任务」这枚**当前页药丸不要红底包裹** ——
       要像它左边的「屏蔽配置」那样:无底色、无边框、无投影,只留白字,风格朴素。
       ★ 只认【链接地址】不认 .current:站点的「当前页」类会随所在页跑到别的 li 上,
         按 id/类名认迟早认错项;
       ★ background-image:none 必须写:这层底是**背景位图**(本项目在「重 置」那颗按钮上量过同一坑),
         只写 background-color 删不掉;
       ★ 不动 padding / 字号 / 尺寸:底色撤掉后文字位置不变,导航不跳位。
       ★★ 2.53.18(用户:字体还是有差异):实测同页并排量两枚 —— 屏蔽配置 a#peacemakerconfig 是
         Arial 12px / **weight 700** / lh 28px / #fff / padding 0 10px;这枚其余全同、只有字重是 400。
         2.53.17 把「朴素」读成了「不粗」,这版按实测改回 700。 */
    html.spp-hm-star.spp-hm-task #mainNav ul.guide > li:has(> a[href*="H_name-tasks"]),
    html.spp-hm-star.spp-hm-task #mainNav ul.guide > li:has(> a[href*="H_name-tasks"]) > a,
    html.spp-hm-star.spp-hm-task #mainNav ul.guide a[href*="H_name-tasks"],
    html.spp-hm-star.spp-hm-task #mainNav ul.guide a[href*="H_name-tasks"]:hover {
        background:none !important;
        background-image:none !important;
        background-color:transparent !important;
        border:0 !important;
        border-radius:0 !important;
        box-shadow:none !important;
        text-shadow:none !important;
        color:#fff !important;
        font-weight:700 !important;
    }
    /*   ★★ 2.52.82(第三轮批注,五处圈注里的三处):上一版按用户指定的 uiverse 框把四块面板
       都包了银边,这一轮用户拿回截图:左栏三块(功能简介 / 操作 / 版权)的银边在近黑的工作台
       上读作一圈「白色线框」(像素实测就是那条 0.67px border rgb(56,56,56) + 银渐变的外沿,
       采样 rgb(56,56,56) 与 CSS 完全对上),黄箭头:「移除这些白色线框」。
       处置:左栏三块退出 uiverse 配方(右栏任务卡保留,用户没圈它),换回深空玻璃平替 ——
       撤银边内衬(padding:2px→0)、撤拉丝伪元素、内层 table 不再刷 rgb(29,29,29) 暗面,
       卡身换成与 #breadcrumbs 同族的深空玻璃。选择器用 td:first-child > div.t 收口,
       只命中左栏那三块,右栏任务卡的 uiverse 框原样保留。 */
    html.spp-hm-star.spp-hm-task #main > table > tbody > tr > td:first-child > div.t {
        padding:0 !important;
        background-color:rgba(16,22,32,0.88) !important;
        background-image:linear-gradient(180deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0) 62%) !important;
        background-repeat:no-repeat !important;
        border-color:rgba(255,255,255,0.06) !important;
        box-shadow:0 12px 30px rgba(2,6,23,0.45) !important;
        color:#c7d3e3 !important;
    }
    html.spp-hm-star.spp-hm-task #main > table > tbody > tr > td:first-child > div.t::after {
        content:none !important;
    }
    html.spp-hm-star.spp-hm-task #main > table > tbody > tr > td:first-child > div.t > table {
        background-color:transparent !important;
        background-image:none !important;
        border-radius:11px !important;
    }
    /*   ★★ 2.52.84(用户黄色框 ×3:「去掉绿色装饰」):表头左边那根 3px 青绿竖线
       (2.52.78 给 td.h 画的 inset 3px 0 0 强调条)在左栏三块的表头上被用户点名拆掉。
       只拆被圈的左栏(td:first-child 收口),右栏任务卡的表头竖线用户没圈、原样保留
       (与 2.52.82 拆银边同一条范围判据:被圈的动作、没被圈的保留)。
       用 box-shadow 覆盖而不是另写 border —— 竖线本来就是 inset 阴影画的。 */
    html.spp-hm-star.spp-hm-task #main > table > tbody > tr > td:first-child > div.t > table > tbody > tr > td.h {
        box-shadow:inset 0 -1px 0 rgba(255,255,255,0.08) !important;
    }
    /*   ★★ 2.52.82(用户白箭头:「可以填充一下表格,让它看起来更美观」):「操作」面板的四行
       导航(新任务选择 / 进行中 / 已完成 / 已失败)在 2.52.80 起内部全层透明,只剩四颗悬浮的
       小胶囊,整块表格像没填。给每一行(td 里直接装着指向本插件任务的 <a>)填上格子:
       淡底 + 行间发丝线,胶囊照旧居中其上。:has() 从行内链接反认行,不用数面板序号;
       悬停时仍由 tr.f_one:hover 那条刷成青绿,格子只是底色、不抢交互。 */
    html.spp-hm-star.spp-hm-task #main > table div.t tr.f_one > td:has(> a[href*="H_name-tasks"]) {
        background-color:rgba(255,255,255,0.035) !important;
        background-image:none !important;
        padding:6px 0 !important;
    }
    html.spp-hm-star.spp-hm-task #main > table div.t tr.f_one + tr.f_one > td:has(> a[href*="H_name-tasks"]) {
        box-shadow:inset 0 1px 0 rgba(255,255,255,0.05);
    }
    /*   ★★ 2.52.82(用户绿框:「用一个头像框包裹起来,更有层次感」):周常 / 日常那两张
       64×64 头像(站点点图就是 hack/tasks/image/*.png,白底方块)原先裸贴在深色卡上,
       和文字之间没有层次。给它们一个与左格用户卡头像同族的渐变像框:padding 当框沿、
       135° 青绿→蓝渐变当框面、外圈 1px 冷白环 + 深色投影,img 本体圆角 9px。
       纯 CSS(框画在 img 自己的 padding/background 上,不往站点 DOM 里包 wrapper);
       选择器钉死「tr.f_one 的第一格里直接装的 img」—— 描述行第一格装的是文字,不会误伤。 */
    html.spp-hm-star.spp-hm-task #main > table div.t tr.f_one > td:first-child > img {
        padding:3px;
        background-image:linear-gradient(135deg, rgba(95,240,192,0.55) 0%, rgba(99,184,255,0.32) 55%, rgba(255,255,255,0.14) 100%);
        border-radius:9px;
        box-shadow:0 0 0 1px rgba(255,255,255,0.10), 0 10px 26px rgba(2,6,23,0.55);
    }

    /* ---------- 2.52.85:个人主页(u.php?uid-*,根类 spp-hm-uprofile) ----------
       用户批注(u.php?uid-1515110.html 截图,四色圈注):红框屏蔽 logo 横幅 /
       蓝框面包屑「当前所在我的位置」高亮(与任务页一致)/ 黄箭头间距与上方一致 + 板块适当加宽 /
       绿框主卡创意美化(点名可从 uiverse 取样式)。
       结构实测:#header > table(920px logo 横幅,任务页/版块页早已屏蔽,此页门控没罩到);
       .bdbA > #breadcrumbs(rgb(248,248,248) 浅条,当前位 = a.crumbs-item.cu);
       #u-wrap > #u-wrap2.cc(整幅白卡,rgb(255,255,255));内层 table/tr/td 全透明零边框,
       白底只挂在 #u-wrap2 一层 —— 换底只需一层,但仍按 1.16 把 border/text 一起成对翻。
       与 profile.php(个人中心,2.52.69「去灰底留文字」定稿)是两个页面族:那边的定稿不受影响。
       横幅屏蔽沿用 initBoardTop 同一条规则文本,但这里的门控是「URL 是 /u.php」——
       不能进 initBoardTop:那边强依赖 #infobox 的 2 个 td 结构,本页根本没有 #infobox。 */
    html.spp-hm-star.spp-hm-uprofile #header > table { display:none !important; }

    /* 板块适当加宽:本页族 #main / #header 提到设置体系 sanctioned 上限 1600
       (黄箭头「板块宽度适当增加」;全站 spp_page_width 设置 1400 不动,只宽这一族) */
    html.spp-hm-star.spp-hm-uprofile #main,
    html.spp-hm-star.spp-hm-uprofile #header { width:min(1600px, 100%) !important; }

    /* 面包屑:任务页同款深空玻璃横条;当前位(a.crumbs-item.cu)沿用同一支绿 #3fd9a4(蓝框) */
    html.spp-hm-star.spp-hm-uprofile #breadcrumbs {
        background-color:rgba(14,18,26,0.82) !important;
        background-image:linear-gradient(180deg, rgba(255,255,255,0.075) 0%, rgba(255,255,255,0.012) 46%, rgba(255,255,255,0) 80%) !important;
        background-repeat:no-repeat !important;
        border-color:rgba(255,255,255,0.075) !important;
        border-radius:9px !important;
        box-shadow:0 10px 30px rgba(2,6,23,0.42), inset 0 1px 0 rgba(255,255,255,0.05) !important;
        backdrop-filter:blur(16px) saturate(145%) !important;
        -webkit-backdrop-filter:blur(16px) saturate(145%) !important;
        color:#b6c3d3 !important;
        padding:0 14px !important;
        line-height:30px !important;
        margin:0 0 10px !important;
    }
    html.spp-hm-star.spp-hm-uprofile #breadcrumbs a { color:#8fb6ee !important; }
    html.spp-hm-star.spp-hm-uprofile #breadcrumbs a:hover { color:#d5e6ff !important; }
    html.spp-hm-star.spp-hm-uprofile #breadcrumbs a.crumbs-item.current { color:#3fd9a4 !important; }
    html.spp-hm-star.spp-hm-uprofile #breadcrumbs .crumbs-item { background:none !important; border-color:transparent !important; }

    /* 主卡:#u-wrap2 深空玻璃(外层 #u-wrap 的 u-wrap.gif 位图底一并清掉) */
    html.spp-hm-star.spp-hm-uprofile #u-wrap { background:none !important; }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 {
        box-sizing:border-box !important;
        background-color:rgba(16,22,32,0.88) !important;
        background-image:linear-gradient(180deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0) 62%) !important;
        background-repeat:no-repeat !important;
        border-radius:14px !important;
        box-shadow:0 16px 46px rgba(2,6,23,0.50), inset 0 0 0 1px rgba(255,255,255,0.065) !important;
        /* 2.55.21:卡片继承色 #c7d3e3 → #eaf2ff(凡是没有专属规则的正文都吃这一条) */
        color:#eaf2ff !important;
        margin:0 0 14px !important;
    }
    /* 换底三件套(经验 1.16):内层压平(本页实测本来就透明,显式钉住防站点改版)+ 边框与文字成对翻 */
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 table,
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 tbody,
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 tr,
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 td {
        background-color:transparent !important;
        background-image:none !important;
    }
    /*   ★★ 2.52.85 补丁(用户回访:「UI框和它背后的面板是否大小适配」):左栏(头像 / 等级 /
       发短消息)那块白面板是 #u-sidebar > div.bgA —— rgb(255,255,255),压平清单第一版漏了它。
       清掉之后头像像框直接坐在深卡上,不存在「框与面板对不齐」的问题。 */
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 .bgA { background:none !important; }
    /*   ★★ 2.52.85 补丁第二批(视觉复核扫出的残余白底):#u-portrait(头像容器)、
       h5.u-h5 的白色渐变位图(images/colorImagination/*)、「更多」链接的白底 ——
       一并压平;a 的 background:none 不伤页签(页签规则特异性更高且排在后面) */
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 #u-portrait { background:none !important; }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 a { background:none !important; }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 table,
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 tr,
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 td,
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 div,
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 dl,
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 dt,
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 li {
        border-color:rgba(255,255,255,0.08) !important;
    }
    /*   ★★ 2.55.21(用户两张截图,红框圈住个人首页与回复页的整幅内容区:
       「字体修改为白亮色」/「一样,字体修改为白亮色」)—— 个人主页这一族(u.php 全族)
       的文字色统一收成**两档白**,不再有中间那几档「算得出对比度、读起来还是灰」的颜色:
         一级(链接 / 列表与卡片标题 / 页签名 / 用户卡按钮 / 小节「更多」药丸 /
              收藏页工具条 / 资料页字段值)= #ffffff;
         二级(正文 td 与 th、.gray / .gray3 / .f9 / dt、签名 #honor、日期、作者、
              等级与 Lv.、翻页文字、资料页字段名)= #eaf2ff。
       改前那几档(#c3dcff 链接 / #c6d5e8 次级 / #d3dce8 正文 / #a9bccb 页签 /
       #c7d3e3 卡片继承色 / #93a7bd 资料页字段名)在近黑底上 12px 都被读成「灰」。
       ★ 只换色:这一族里没有一条规则的盒模型 / 尺寸 / 定位被本轮碰过。
       ★ 唯一的例外是「更多」药丸的字重:#c3dcff 那条链接的基色是纯白之后,站点写死的
         font-weight:100 仍然把它压成发丝 —— 与 2.55.17 在收藏页列表标题上遇到的是同一条
         站点规则,照同一档补 500(字号与盒子不动)。
       ★ 层级改由字号、粗细与底色承担(标题 12px 纯白;次级 10~12px 的 #eaf2ff;
         页签与药丸自带底与描边)—— 下面几条注释里记的「链接偏蓝、次级偏灰」是
         2.52.86~2.52.88 的历史口径,注释原样保留,免得后人以为那些值还是当前值。 */
    /*   ★★ 2.52.85 补丁(用户回访:「黑色背景搭配灰色字体基本不可取」):站点给卡内一批元素
       写死了显式深色(td/签名行 #555、日期 span.gray3.f9 #333、空态 dt #555),继承色救不了
       它们 —— 显式色必须显式翻。正文 td 全部提亮,次级信息用 9fb0c4(对比度 ≈7:1),
       不再留任何 51/85 级的暗灰。 */
    /*   ★★ 2.52.86(用户回访:「字体与黑底融合过紧,黑底暗灰字不符美学」):整档再提亮 ——
       链接 8fb6ee→9dc2ff、正文 c7d3e3→d3dce8、次级 9fb0c4→b0c0d4(含 #honor 签名)。
       12px 小字号在深底上的可读下限比大字高,宁亮勿灰。 */
    /*   ★★ 2.55.21:正文 / 用户名 / 链接三档一起上提。链接的悬停色跟着从
       #eef5ff 换成 #d8fff5 —— 基色已经是纯白,悬停再往白里加就看不出来了,
       换成本族通用的 teal 白(与页签、药丸、翻页条的悬停同一档)。 */
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 td,
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 th { color:#eaf2ff !important; }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 h1.u-h1 { color:#ffffff !important; }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 a { color:#ffffff !important; }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 a:hover { color:#d8fff5 !important; }
    /*   ★★ 2.52.88(回复页批注「蓝色不够亮,太暗了。」,橙框圈住整列):链接蓝
       9dc2ff→c3dcff、次级灰蓝 b0c0d4→c6d5e8。回复列表整列的标题就是这批链接,
       12px 的小字在近黑底上「算得出对比度、读起来还是暗」—— 这一档按实站点名的
       方向再提一级,同时保住两级层次(链接偏蓝、次级偏灰)。 */
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 td.gray3,
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 .gray,
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 .gray3,
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 .f9,
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 dt { color:#eaf2ff !important; }   /* 2.55.21:#c6d5e8 → #eaf2ff */
    /* 小节标题(好友近况 / 最新回复 / 最新主题):teal 文字 + 底缘发丝线
       (2.52.84 刚拆过表头左缘的青绿竖条,这里刻意不再用左缘竖线这个刚被否的形)
       ★★ 2.52.88(首页批注「字体颜色太暗,不够明显。」,两根箭头分别指向
          「好友近况」与「最新回复」):12.6px / #3fd9a4 在近黑底上只有约 8:1 的计算对比度,
          但青绿在低亮度底上会被周边星点背景「吃掉」—— 用户读到的就是发暗。提亮到
          #8bf7cd + 加一点点同色辉光(辉光只加在文字上,不给盒子加边),字号 12.6→13.5px。
          ★ 为什么提亮而不是加粗:加粗在同一字号下只增笔画密度,暗底小字的第一瓶颈是亮度。 */
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 h5.u-h5 {
        background:none !important;
        color:#8bf7cd !important;
        font-size:13.5px !important;
        font-weight:650 !important;
        letter-spacing:.02em;
        padding-bottom:5px;
        text-shadow:0 0 12px rgba(139,247,205,0.30);
        box-shadow:inset 0 -1px 0 rgba(255,255,255,0.08);
    }
    /* 标题文字的白色衬底挂在 h5 的子 span 上(站点 h5 span 白底)—— 只剥 span 的底 */
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 h5.u-h5 span {
        background:none !important;
        color:#8bf7cd !important;
    }
    /*   ★★ 2.52.88(首页批注「这主要使用 UI 按钮的样式进行包裹。」,三只绿框圈住三条
       「更多」):站点的「更多」是 a.r.gray3(38×18、无底无边、12px 灰蓝),在标题行右端
       像掉在角落的一行杂物。套成与左栏按钮 / 页签同族的深空药丸(同一份 padding / 圆角 /
       inset 描边),悬停转 teal。「更多」在标题行里是 float:right(class .r),
       只换皮、不动位。 */
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 h5.u-h5 a {
        display:inline-flex;
        align-items:center;
        padding:4px 11px !important;
        /*   2.55.21:站点给 .gray3 写死 font-weight:100 —— 「更多」两字是整页最细的一处,
           改纯白之后仍然发丝。这与 2.55.17 在收藏页列表标题上遇到的是**同一条站点规则**,
           照同一档补(500):不动字号、不动盒子,只把笔画密度拉回正常。 */
        font-weight:500 !important;
        border-radius:7px;
        background-color:rgba(255,255,255,0.055) !important;
        color:#ffffff !important;   /* 2.55.21:#c3dcff → 纯白 */
        text-decoration:none !important;
        box-shadow:inset 0 0 0 1px rgba(255,255,255,0.10);
        transition:background-color .15s ease, color .15s ease;
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 h5.u-h5 a:hover {
        background-color:rgba(32,201,151,0.18) !important;
        color:#d8fff5 !important;
    }
    /* 签名(#honor,站点写死 #333)—— 黑底灰字的最后一处 */
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 #honor { color:#eaf2ff !important; }   /* 2.55.21:#c6d5e8 → #eaf2ff */
    /* 动态列表行(dl.feed-list / dl.article-list 的 dt):行距与悬停底 */
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 dl.feed-list dt,
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 dl.article-list dt {
        padding:6px 4px !important;
        border-radius:6px;
        transition:background-color .15s ease;
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 dl.feed-list dt:hover,
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 dl.article-list dt:hover {
        background-color:rgba(255,255,255,0.045) !important;
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 a.link1 { color:#ffffff !important; }   /* 2.55.21:#c3dcff → 纯白 */
    /* 头像:与左格用户卡同族的旋转 conic 渐变像框(@property + keyframes 本块自带,
       本页不吃 CSS_UPANEL)—— 绿框「创意美化」的主体之一 */
    @property --sppUfAng { syntax:'<angle>'; inherits:false; initial-value:0deg; }
    @keyframes sppUfSpin { to { --sppUfAng:360deg; } }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 td:first-child img {
        box-sizing:border-box;
        border:3px solid transparent !important;
        border-radius:16px !important;
        background:
            linear-gradient(#161b26, #12161d) padding-box,
            conic-gradient(from var(--sppUfAng),
                rgba(95,240,192,0.95), rgba(99,184,255,0.55), rgba(255,255,255,0.16), rgba(95,240,192,0.95)) border-box;
        animation:sppUfSpin 8s linear infinite;
        box-shadow:0 12px 30px rgba(2,6,23,0.55);
    }
    /* 页签行(ul.b,激活 = li.current)——
       出处:uiverse「Yaya12085/rude-mouse-79」(MIT,radio-inputs 分段控件),已按 1.13 取源对齐:
       圆角轨道 8px + 0.25rem 内衬 + 0 0 0 1px 环 + 段内 6px 圆角 + 激活段反色加粗 + .15s ease-in-out;
       配色翻成深空(轨道 6% 白)+ 青绿激活,与本站既有强调族一致 */
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 ul.b {
        display:inline-flex;
        flex-wrap:wrap;
        gap:2px;
        padding:4px !important;
        border-radius:8px;
        background-color:rgba(255,255,255,0.06) !important;
        box-shadow:0 0 0 1px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.06) !important;
        list-style:none;
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 ul.b li,
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 ul.b li a {
        box-sizing:border-box !important;
        height:auto !important;
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 ul.b li a {
        display:flex;
        align-items:center;
        justify-content:center;
        line-height:1.2 !important;
        padding:6px 14px !important;
        border-radius:6px;
        /* ★ 2.52.85 补丁(用户回访:「黑主题不要白底」):站点给这里的 a 刷了 #eee 底,
           不显式盖掉的话非激活页签就是一排白盒灰字。给深底淡面,悬停加亮一档。 */
        background-color:rgba(255,255,255,0.045) !important;
        color:#ffffff !important;   /* 2.55.21:#a9bccb(全页最暗的一档)→ 纯白 */
        text-decoration:none !important;
        transition:all .15s ease-in-out;
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 ul.b li a:hover {
        background-color:rgba(255,255,255,0.10) !important;
        color:#d8fff5 !important;   /* 2.55.21:#e8eef6 → teal 白(基色已是纯白,白上加白看不出悬停) */
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 ul.b li.current a {
        background-color:rgba(32,201,151,0.16) !important;
        color:#d8fff5 !important;
        font-weight:600;
        box-shadow:inset 0 0 0 1px rgba(32,201,151,0.42);
    }

    /*   ★★ 2.52.87(用户第六轮:全 tab 8 张截图,「居中」+「移动位置」两条批注)——
       站点本族是两个 float 顶两端:#u-sidebar 200 float:left、#u-content 720 float:right;
       内容区再分主栏(首页 #u-contentmain / 资料页 .u-content-66)与右栏
       (#u-contentside / 资料页 .u-content-33)。窗口一宽,主栏贴左、右栏贴最右,
       中间永远是几百像素的死空间。2.52.86 只是把主栏拉满,用户这一轮点名要
       「整页居中」,并选定 B 方案:**右栏搬到主内容下方,全页一栏、居中**。
       实现:六个内容块(含 #u-top 页眉)统一 width:min(960px,100%) + 左右 auto 居中;
       右栏去掉 float 之后按源码顺序(main 在前、side 在后)自然落到主内容下方 ——
       不改 DOM、不用 order。 */
    html.spp-hm-star.spp-hm-uprofile #u-sidebar { float:left !important; width:200px !important; }
    html.spp-hm-star.spp-hm-uprofile #u-content {
        float:none !important;
        width:auto !important;
        margin-left:216px !important;
        overflow:hidden !important;
    }
    /*   ★ float 元素上 width:auto 是【收缩到内容宽】而不是填满(2.52.86 踩过:收藏页
       只剩 240px、列表被挤在左边),所以这里必须显式给宽度,不能靠 auto。
       ★ 960 的取值:用户视口实测 ≈1280、内容区 ≈1064 —— 960 两侧各留约 52px,
         居中看得出来;再窄就会压到商品页那个五列表头。
       ★ #u-top 也要收进同宽:页签槽 #u-top-nav 是它的绝对定位子元素,不同宽的话
         页签左缘会和内容左缘错开。 */
    html.spp-hm-star.spp-hm-uprofile #u-content > #u-top,
    html.spp-hm-star.spp-hm-uprofile #u-content > #u-contentmain,
    html.spp-hm-star.spp-hm-uprofile #u-content > #u-contentside,
    html.spp-hm-star.spp-hm-uprofile #u-content > .u-content-66,
    html.spp-hm-star.spp-hm-uprofile #u-content > .u-content-33,
    html.spp-hm-star.spp-hm-uprofile #u-content > .u-profile {
        float:none !important;
        box-sizing:border-box !important;
        width:min(960px, 100%) !important;
        margin-left:auto !important;
        margin-right:auto !important;
    }
    /*   ★ 2.52.87(商品页「控制间距」绿批注):页签槽是绝对定位的,不撑 #u-top 的高度 ——
       实测 ul.b(高 38,从 y=115 起)底在 153,而 #u-top 只到 145,内容块紧接着就是
       负间距,页签压着正文。24px = 页眉底 145 + 24 = 169,距药丸底 153 留 16px 呼吸。 */
    html.spp-hm-star.spp-hm-uprofile #u-content > #u-contentmain,
    html.spp-hm-star.spp-hm-uprofile #u-content > .u-content-66,
    html.spp-hm-star.spp-hm-uprofile #u-content > .u-profile { margin-top:24px !important; }
    /* 右栏下移后它是「第二节」而不是并排侧栏,间距给大一档。 */
    html.spp-hm-star.spp-hm-uprofile #u-content > #u-contentside,
    html.spp-hm-star.spp-hm-uprofile #u-content > .u-content-33 { margin-top:20px !important; }
    html.spp-hm-star.spp-hm-uprofile #u-content table { width:100% !important; }
    /*   ★ 2.52.86 补丁(用户第五轮:「白色线条不需要,可以换成其他表达形式」):站点把行分隔
       画成 border-bottom:0.67px **dotted** —— 列表标题列是 <th>(#eee)、正文列是 <td>(#ddd)。
       我方 2.52.85 的 border-color 覆盖名单漏了 th(所以标题列一直挂着 #eee 白点线);
       且点线在深空底上呈现为「白色虚线」,正是用户点名要换掉的形。统一换成
       6% 白的 1px 细实线(发丝线),宽度不变、不影响布局。 */
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 th,
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 td {
        border-bottom-style:solid !important;
        border-bottom-color:rgba(255,255,255,0.06) !important;
    }

    /*   ★★ 2.52.86(收藏页):表单控件全部暗化 —— 「白色背景不要」的名称输入框、
       红渐变位图按钮(.btn,bg_btn.gif)、目录转换下拉。工具条原本挤在一起
       (radio 行与按钮行的 margin 只有 1em 且同处一个 <center>),显式拉开。 */
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 form center {
        margin:12px 0 4px !important;
        line-height:2 !important;
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 form center > div { margin-top:8px !important; }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 input[type=text],
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 input:not([type]) {
        background-color:rgba(255,255,255,0.06) !important;
        color:#eaf2ff !important;   /* 2.55.21:#d3dce8 → #eaf2ff */
        border:1px solid rgba(255,255,255,0.14) !important;
        border-radius:6px;
        padding:3px 8px !important;
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 select {
        background-color:#1a2029 !important;
        color:#eaf2ff !important;   /* 2.55.21:#d3dce8 → #eaf2ff */
        border:1px solid rgba(255,255,255,0.14) !important;
        border-radius:6px;
        padding:2px 6px !important;
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 input.btn {
        background:rgba(32,201,151,0.16) !important;
        color:#d8fff5 !important;
        border:1px solid rgba(32,201,151,0.42) !important;
        border-radius:6px;
        padding:3px 12px !important;
        font-weight:600;
        cursor:pointer;
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 input.btn:hover {
        background:rgba(32,201,151,0.30) !important;
    }

    /*   ★★ 2.52.86(绿框:「发短消息 / 加为好友」要么 SVG 图标要么移除,改 UI 按钮包裹文本):
       两颗按钮是裸 <a> 里塞位图 <img> —— 位图直接藏掉,文字套深空药丸(teal 描边悬停)。 */
    /* 图标是 td 里与 a 平级的兄弟 <img>(mail-icon.gif 等),不在 a 内。容器在两种页面上
       不一样:子页包在 div.bgA 里、首页包在 div.bdbA 里 —— 所以按容器收口会漏一半。
       改按「排除头像」收口:#u-portrait 里的头像 img 带 class="pic",其余位图一律藏掉。 */
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 #u-sidebar img:not(.pic) { display:none !important; }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 #u-sidebar td a {
        display:inline-flex;
        align-items:center;
        padding:5px 12px !important;
        border-radius:7px;
        background-color:rgba(255,255,255,0.055) !important;
        color:#ffffff !important;   /* 2.55.21:#c3dcff → 纯白 */
        text-decoration:none !important;
        box-shadow:inset 0 0 0 1px rgba(255,255,255,0.10);
        transition:background-color .15s ease, color .15s ease;
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 #u-sidebar td a:hover {
        background-color:rgba(32,201,151,0.18) !important;
        color:#d8fff5 !important;
    }

    /*   ★★ 2.52.87(资料页批注:「使用卡片包裹项目」,另附一张手绘样张
       「卡片样式 ▸ UID:xxxx」)—— 一行一张卡。
       结构实测:个人信息 / 论坛资历两张表都是 <tr><td>标签</td><th width="75%">值</th></tr>,
       值那格是 75% 宽的表头格,于是「UID」和「1515110」被 500px 的空档拉开。
       改法:把表拆成【行 = flex 卡片】(table/tbody display:block、tr display:flex),
       标签格钉 6em(最长标签「平均日发帖」5 字,6em 够)+ nowrap,值格 flex:1 左对齐 ——
       读起来就是样张上那句「UID:1515110」,而且十行的值仍然对齐在同一条竖线上。
       ★ 为什么不用 border-spacing + 圆角格:那样两格各自成面,中间会留一条内缝;
         行级卡片只有一个盒子,圆角/底色/高光都是一套。 */
    html.spp-hm-star.spp-hm-uprofile #u-profile > table.u-table,
    html.spp-hm-star.spp-hm-uprofile #u-profile-s > table.u-table { display:block !important; }
    html.spp-hm-star.spp-hm-uprofile #u-profile > table.u-table > tbody,
    html.spp-hm-star.spp-hm-uprofile #u-profile-s > table.u-table > tbody {
        /*   ★★ 2.52.88(资料页蓝框,另附「这里和上面的蓝色框选排版一样」指向论坛资历):
           2.52.87 是「一行一张卡」—— 每行 960px 只装「UID 1515110」这么一小段内容,
           用户圈出来的正是这个:把两个短项并排、一行两张卡。
           表结构不改(tr 仍是 flex 卡片),只把 tbody 换成两列网格,行宽自动各半。 */
        display:grid !important;
        grid-template-columns:repeat(2, minmax(0, 1fr)) !important;
        gap:8px !important;
    }
    html.spp-hm-star.spp-hm-uprofile #u-profile > table.u-table > tbody > tr,
    html.spp-hm-star.spp-hm-uprofile #u-profile-s > table.u-table > tbody > tr {
        display:flex !important;
        align-items:center !important;
        box-sizing:border-box !important;
        /* 行距交给 tbody 的 gap(原来靠 tr 的下外边距,进网格后会与 gap 叠加成 16px) */
        margin:0 !important;
        padding:9px 14px !important;
        border-radius:10px !important;
        background-color:rgba(255,255,255,0.05) !important;
        background-image:linear-gradient(180deg, rgba(255,255,255,0.045) 0%, rgba(255,255,255,0) 72%) !important;
        box-shadow:inset 0 1px 0 rgba(255,255,255,0.06) !important;
    }
    html.spp-hm-star.spp-hm-uprofile #u-profile > table.u-table > tbody > tr > td,
    html.spp-hm-star.spp-hm-uprofile #u-profile-s > table.u-table > tbody > tr > td {
        display:block !important;
        flex:0 0 auto !important;
        width:6em !important;
        min-width:6em !important;
        white-space:nowrap !important;
        color:#eaf2ff !important;   /* 2.55.21:#93a7bd(全族最暗的一档)→ #eaf2ff */
        border-bottom:0 none !important;
    }
    html.spp-hm-star.spp-hm-uprofile #u-profile > table.u-table > tbody > tr > th,
    html.spp-hm-star.spp-hm-uprofile #u-profile-s > table.u-table > tbody > tr > th {
        display:block !important;
        flex:1 1 auto !important;
        width:auto !important;
        min-width:0 !important;
        text-align:left !important;
        font-weight:400 !important;
        color:#ffffff !important;   /* 2.55.21:#dce6f2 → 纯白 */
        border-bottom:0 none !important;
    }
    /*   ★★ 2.52.88:「自我简介」是个人信息表里唯一的长项(手绘样张上它没画第二个蓝框),
       跨满两列;论坛资历 11 行、没有长项,末行照常占左格。 */
    html.spp-hm-star.spp-hm-uprofile #u-profile > table.u-table > tbody > tr:last-child {
        grid-column:1 / -1 !important;
    }

    /*   ★★ 2.52.87(回复页批注:「这里要用卡片化,参考茶馆无图片的卡片形式」)——
       回复列表 50 行、一行一条发丝线,改成一行一张玻璃卡。
       ★ 玻璃配方逐字同步自 SPP-GLASS 段(半透黑底 + blur18 + 上沿内高光 + 12px 圆角),
         但【底色渐变改竖向】:卡片由 th / td 两格拼成,斜向/径向渐变会在接缝处断开,
         竖向渐变两格完全一致 —— 接缝于是看不见(这也是不用 border 画描边的原因:
         两格各画一圈就是一条内缝,改用整行一个盒子的 inset 描边)。
       ★ 本段靠 .spp-up-post 收口:CSS 读不到 query string,页型类由 JS 挂在 html 上。
       ★ 第一行是站点的占位空行(<td><br></td>),卡片化之后它会变成一张空卡,删掉。 */
    html.spp-hm-star.spp-hm-uprofile.spp-up-post #u-contentmain > table.u-table { display:block !important; }
    html.spp-hm-star.spp-hm-uprofile.spp-up-post #u-contentmain > table.u-table > tbody {
        /*   ★★ 2.52.88(回复页批注:橙框圈住整列 + 「可以参考这种排版,但颜色可以忽略。」
           指向一张三列卡片网格):2.52.87 的一行一卡是整幅 960px 一条 —— 50 条回复
           摊成 50 条横带,右半边全是空。改成响应式卡片网格:minmax(280px,1fr) 在 960px
           宽下正好落三列(3×280 + 2×10 = 860 ≤ 960;第四列要 1150,落不下来),
           与参考图的三列同构。窄窗口自动降到两列/一列,不需要断点。 */
        display:grid !important;
        grid-template-columns:repeat(auto-fill, minmax(280px, 1fr)) !important;
        gap:10px !important;
    }
    html.spp-hm-star.spp-hm-uprofile.spp-up-post #u-contentmain > table.u-table > tbody > tr:first-child { display:none !important; }
    html.spp-hm-star.spp-hm-uprofile.spp-up-post #u-contentmain > table.u-table > tbody > tr {
        display:flex !important;
        /* 参考图里每张卡是「标题在上、作者/日期压底」的竖排 —— 原来的横排(标题左、
           作者右)在 300px 宽的格里会把标题挤成两三个字一折。 */
        flex-direction:column !important;
        align-items:stretch !important;
        box-sizing:border-box !important;
        margin:0 !important;
        padding:11px 14px !important;
        border-radius:12px !important;
        background-color:rgb(0 0 0 / 0.86) !important;
        background-image:linear-gradient(180deg, rgba(255,255,255,0.06) 0%, rgba(255,255,255,0.014) 44%, rgba(255,255,255,0) 100%) !important;
        backdrop-filter:blur(18px) saturate(140%) !important;
        -webkit-backdrop-filter:blur(18px) saturate(140%) !important;
        box-shadow:0 12px 32px -14px rgba(0,0,0,0.72),
            inset 0 1px 0 rgba(255,255,255,0.20),
            inset 0 0 0 1px rgba(255,255,255,0.09) !important;
        transition:box-shadow .15s ease;
    }
    html.spp-hm-star.spp-hm-uprofile.spp-up-post #u-contentmain > table.u-table > tbody > tr:hover {
        box-shadow:0 0 24px -2px rgba(61,106,255,0.5),
            0 12px 32px -14px rgba(0,0,0,0.72),
            inset 0 1px 0 rgba(255,255,255,0.20),
            inset 0 0 0 1px rgba(255,255,255,0.09) !important;
    }
    html.spp-hm-star.spp-hm-uprofile.spp-up-post #u-contentmain > table.u-table > tbody > tr > th {
        display:block !important;
        flex:1 1 auto !important;
        width:auto !important;
        min-width:0 !important;
        /* ★ 2.55.31(用户:「主题文字居中、变大、变白、增亮」):2.52.88 那版是左对齐,
           这一轮按用户点的改回居中;标题自己那条在下面(th > a:not(.gray))。 */
        text-align:center !important;
        font-weight:400 !important;
        border-bottom:0 none !important;
    }
    html.spp-hm-star.spp-hm-uprofile.spp-up-post #u-contentmain > table.u-table > tbody > tr > td {
        /*   ★★ 2.52.88:作者那一格从「右侧对齐的尾巴」改成卡片的页脚 —— 卡宽 300px 时
           「作者:78f573ba」右对齐会看着像溢出的边注,落到卡片底缘、左对齐、加一条
           1px 白线的内上沿,才与参考图的「标题 / 元信息」两段式同形。
           th 是 flex:1(撑满剩余高度)→ 同一行里卡片等高时,页脚自然全部压在同一条线上。 */
        /* ★ 2.55.31:2.52.88 是 block;现在改成 flex —— 页脚要「作者靠左、板块/时间靠右」
           两端对齐,.spp-upc-meta 那枚 margin-left:auto 才有地方顶。 */
        display:flex !important;
        align-items:baseline !important;
        flex:0 0 auto !important;
        text-align:left !important;
        margin-top:8px !important;
        padding-top:8px !important;
        box-shadow:inset 0 1px 0 rgba(255,255,255,0.07) !important;
        border-bottom:0 none !important;
    }
    /* ★★ 2.55.31(用户:「对于主页回复,把板块、时间放在右下角」):这两枚本来是标题那格
       th 里的行内兄弟,由 initUprofilePostCards 搬进这格页脚、裹成 .spp-upc-meta ——
       margin-left:auto 把它顶到右下角,与左端的「作者:xxx」同一行。
       ★ 为什么必须动 DOM 而不是纯 CSS:两格之间隔着 th / td 的边界,纯 CSS 只能用绝对定位
         +猜另一枚的宽度,日期一换格式(站点现在是 [YYYY-MM-DD])就错位。 */
    html.spp-hm-star.spp-hm-uprofile.spp-up-post #u-contentmain > table.u-table > tbody > tr > td > .spp-upc-meta {
        margin-left:auto !important;
        display:inline-flex !important;
        align-items:center !important;
        gap:6px !important;
        white-space:nowrap !important;
        line-height:1.4 !important;
    }
    html.spp-hm-star.spp-hm-uprofile.spp-up-post #u-contentmain > table.u-table > tbody > tr > td > .spp-upc-meta > a,
    html.spp-hm-star.spp-hm-uprofile.spp-up-post #u-contentmain > table.u-table > tbody > tr > td > .spp-upc-meta > span {
        color:#c3d4ea !important;
        font-size:10px !important;
        text-decoration:none !important;
    }
    html.spp-hm-star.spp-hm-uprofile.spp-up-post #u-contentmain > table.u-table > tbody > tr > td > .spp-upc-meta > a:hover {
        color:#ffffff !important;
    }
    /* 主题文字:居中(th 那条)+ 变大(12 → 14px)+ 更白(#fff)+ 增亮
       (一圈冷白光晕 + 一层极淡的暗投影 —— 只加光晕时小字会「糊」,加投影才有边界)。 */
    html.spp-hm-star.spp-hm-uprofile.spp-up-post #u-contentmain > table.u-table > tbody > tr > th > a:not(.gray) {
        display:block !important;
        text-align:center !important;
        font-size:14px !important;
        line-height:1.45 !important;
        font-weight:600 !important;
        color:#ffffff !important;
        -webkit-text-fill-color:#ffffff !important;
        letter-spacing:.01em !important;
        text-shadow:0 0 14px rgba(255,255,255,.34), 0 1px 0 rgba(0,0,0,.40) !important;
    }

    /*   ★★ 2.55.17(用户:「对于网址:https://blue-plus.net/u.php?action=favor
       字体改为亮白色、美化删除 UI」)—— 收藏页专属。页型类 .spp-up-favor 由 JS 挂在
       html 上(见 initUprofilePage:等号与连字符两种写法都认)。
       ★ 这一页的表格在 #u-contentmain 里还套着一层 <form>(不是直接子元素),
         所以选择器用后代而不是 >(.spp-up-post 那段是直接子元素,不能照抄)。 */
    /*   ① 亮白:列表标题本来是 #c3dcff,且站点给标题那格写死 font-weight:100 —— 深空底
       上 12px 的极细字看着就是「灰」;改成纯白 + 600。作者列 500 + #eaf2ff(比标题低
       一档,两级层次仍在);表头(标题 / 作者 / 目录)也提亮,并靠字距把三列分开。 */
    html.spp-hm-star.spp-hm-uprofile.spp-up-favor #u-contentmain table.u-table > tbody > tr:not(:first-child) > th > a {
        color:#ffffff !important;
        font-weight:600 !important;
    }
    html.spp-hm-star.spp-hm-uprofile.spp-up-favor #u-contentmain table.u-table > tbody > tr:not(:first-child) > td > a {
        color:#eaf2ff !important;
        font-weight:500 !important;
    }
    html.spp-hm-star.spp-hm-uprofile.spp-up-favor #u-contentmain table.u-table > tbody > tr:not(:first-child):hover {
        background-color:rgba(255,255,255,0.045) !important;
    }
    html.spp-hm-star.spp-hm-uprofile.spp-up-favor #u-contentmain table.u-table > tbody > tr:first-child > td {
        color:#f0f6ff !important;
        font-weight:600 !important;
        letter-spacing:.06em;
    }
    /*   ② 勾选框(工具条「删除」要删的就是它们):站点原生方块在深空底上是两块纯白,
       改成与主题同族的方片;勾选态 teal 实心 + 深色勾(勾是内联 SVG,不引外部图)。 */
    html.spp-hm-star.spp-hm-uprofile.spp-up-favor #u-contentmain table.u-table input[type=checkbox] {
        -webkit-appearance:none !important;
        appearance:none !important;
        width:15px !important;
        height:15px !important;
        margin:0 !important;
        padding:0 !important;
        border-radius:4px;
        background-color:rgba(255,255,255,0.07);
        box-shadow:inset 0 0 0 1px rgba(255,255,255,0.22);
        cursor:pointer;
        transition:background-color .15s ease, box-shadow .15s ease;
    }
    html.spp-hm-star.spp-hm-uprofile.spp-up-favor #u-contentmain table.u-table input[type=checkbox]:hover {
        box-shadow:inset 0 0 0 1px rgba(32,201,151,0.65);
    }
    html.spp-hm-star.spp-hm-uprofile.spp-up-favor #u-contentmain table.u-table input[type=checkbox]:checked {
        background-color:rgba(32,201,151,0.9);
        background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.4 8.5l3.1 3.1 6.1-6.6' fill='none' stroke='%230a1a14' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
        background-repeat:no-repeat;
        background-position:center;
        background-size:11px 11px;
        box-shadow:inset 0 0 0 1px rgba(32,201,151,1), 0 0 10px -3px rgba(32,201,151,0.75);
    }
    /*   ③ 目录那一串「×」(td.fav > a.fr,站点是 16px 位图 tips-close.png 装在 40×26
       的空盒里):位图藏掉,换成 22×22 的自绘方片,悬停转红。
       ★ 选择器必须带上 #u-wrap2 —— 两条链的 id 数相同才压得住 #u-wrap2
         #u-contentside td a 那条药丸规则(少一个 id 就会被它按特异性顶掉)。 */
    html.spp-hm-star.spp-hm-uprofile.spp-up-favor #u-wrap2 #u-contentside td.fav > a.fr {
        display:inline-flex !important;
        align-items:center !important;
        justify-content:center !important;
        box-sizing:border-box !important;
        width:22px !important;
        height:22px !important;
        padding:0 !important;
        margin:2px 2px 0 8px !important;
        border-radius:7px;
        background-color:rgba(255,255,255,0.055) !important;
        color:#eaf2ff !important;   /* 2.55.21:#b9c6d6 → #eaf2ff */
        font-size:12px !important;
        line-height:1 !important;
        text-decoration:none !important;
        box-shadow:inset 0 0 0 1px rgba(255,255,255,0.12);
        transition:background-color .15s ease, color .15s ease, box-shadow .15s ease;
    }
    html.spp-hm-star.spp-hm-uprofile.spp-up-favor #u-wrap2 #u-contentside td.fav > a.fr img { display:none !important; }
    html.spp-hm-star.spp-hm-uprofile.spp-up-favor #u-wrap2 #u-contentside td.fav > a.fr::before { content:'✕' !important; }
    html.spp-hm-star.spp-hm-uprofile.spp-up-favor #u-wrap2 #u-contentside td.fav > a.fr:hover {
        background-color:rgba(255,86,86,0.22) !important;
        color:#ffd9d9 !important;
        box-shadow:inset 0 0 0 1px rgba(255,120,120,0.6);
    }
    /*   ④ 工具条那对 radio(由 initFavorPageUI 包成 <label class="spp-fav-job">):
       「目录转换」无害 ⇒ teal;「删除」是破坏性动作 ⇒ 红调;选中「删除」时提交键
       跟着转红(center.spp-fav-danger)。★ 裸文本节点 CSS 选不到 ⇒ 包成 label
       是唯一的路(点整枚胶囊即选中,原生 label 行为,不必自己写 JS 点击)。 */
    html.spp-hm-star.spp-hm-uprofile.spp-up-favor #u-contentmain center { line-height:2.2 !important; }
    html.spp-hm-star.spp-hm-uprofile.spp-up-favor #u-contentmain .spp-fav-job {
        display:inline-flex;
        align-items:center;
        gap:6px;
        padding:3px 12px 3px 8px !important;
        margin:0 2px !important;
        border-radius:999px;
        font-size:12px !important;
        line-height:1.7 !important;
        color:#ffffff !important;   /* 2.55.21:#d7e3f4 → 纯白 */
        background-color:rgba(255,255,255,0.055) !important;
        box-shadow:inset 0 0 0 1px rgba(255,255,255,0.12);
        cursor:pointer;
        user-select:none;
        vertical-align:middle;
        transition:background-color .15s ease, color .15s ease, box-shadow .15s ease;
    }
    html.spp-hm-star.spp-hm-uprofile.spp-up-favor #u-contentmain .spp-fav-job > input[type=radio] {
        -webkit-appearance:none !important;
        appearance:none !important;
        flex:0 0 auto;
        width:13px !important;
        height:13px !important;
        margin:0 !important;
        border-radius:50%;
        background-color:rgba(255,255,255,0.06);
        box-shadow:inset 0 0 0 1.5px rgba(255,255,255,0.30);
        cursor:pointer;
    }
    html.spp-hm-star.spp-hm-uprofile.spp-up-favor #u-contentmain .spp-fav-job:hover { background-color:rgba(255,255,255,0.09) !important; }
    html.spp-hm-star.spp-hm-uprofile.spp-up-favor #u-contentmain .spp-fav-job.is-on {
        background-color:rgba(32,201,151,0.16) !important;
        color:#d8fff5 !important;
        box-shadow:inset 0 0 0 1px rgba(32,201,151,0.55);
    }
    html.spp-hm-star.spp-hm-uprofile.spp-up-favor #u-contentmain .spp-fav-job.is-on > input[type=radio] {
        background-image:radial-gradient(circle at 50% 50%, #20c997 0 3.2px, transparent 3.6px);
        box-shadow:inset 0 0 0 1.5px rgba(32,201,151,0.85);
    }
    html.spp-hm-star.spp-hm-uprofile.spp-up-favor #u-contentmain .spp-fav-job--del:hover {
        background-color:rgba(255,86,86,0.14) !important;
        color:#ffd9d9 !important;
    }
    html.spp-hm-star.spp-hm-uprofile.spp-up-favor #u-contentmain .spp-fav-job--del.is-on {
        background-color:rgba(255,86,86,0.18) !important;
        color:#ffe0e0 !important;
        box-shadow:inset 0 0 0 1px rgba(255,110,110,0.6);
    }
    html.spp-hm-star.spp-hm-uprofile.spp-up-favor #u-contentmain .spp-fav-job--del.is-on > input[type=radio] {
        background-image:radial-gradient(circle at 50% 50%, #ff6b6b 0 3.2px, transparent 3.6px);
        box-shadow:inset 0 0 0 1.5px rgba(255,110,110,0.9);
    }
    html.spp-hm-star.spp-hm-uprofile.spp-up-favor #u-contentmain center.spp-fav-danger input.btn[type=submit] {
        background:rgba(255,86,86,0.20) !important;
        color:#ffe3e3 !important;
        border-color:rgba(255,110,110,0.55) !important;
        box-shadow:0 0 16px -6px rgba(255,86,86,0.9);
    }

    /*   ★★ 2.52.87(商品页黄批注「对齐」):左栏两行本来各排各的 ——
       .bgA(等级 / Lv.1)自带 .6em 1.2em 内衬,.bdbA(发短消息 / 加为好友)的左右内衬
       是 0(内联 style="padding:.5em 0"),于是两颗按钮的列起点和上面 等级 / Lv.1 两列对不上。
       统一成同一张两列表:两处都 table-layout:fixed + 两列各 50% + 同一份左右内衬,
       等级 与 发短消息 同列、Lv.1 与 加为好友 同列。 */
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 #u-sidebar .bgA table,
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 #u-sidebar .bdbA table {
        table-layout:fixed !important;
        width:100% !important;
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 #u-sidebar .bgA td,
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 #u-sidebar .bdbA td {
        width:50% !important;
        /*   ★★ 2.52.88(首页黄批注「中心对齐」,两个黄框分别圈住「等级 / Lv.1」与
           「发短消息 / 加为好友」两行):2.52.87 把两行对齐到同一条列线之后,格内文字
           仍是 left —— 每格 86px、文字 30~72px,于是两行的内容都靠在各自格的左缘,
           读起来像两张左对齐的小表。改格内居中:两格本就以侧栏中线互为中心,
           居中之后「等级」与「Lv.1」以自己的格心对称落在头像中轴上。
           ★ 两个 <a> 是 inline-flex(在行内级),父格 text-align:center 即可把它们带正,
             不必给 a 加左右 auto —— 那反而会因为 a 是 flex 项外的行内块而失效。 */
        text-align:center !important;
    }
    /*   ★ 2.52.87 补丁(实站量出来的根因):站点给 .bdbA 写死了 width:200px 且是
       content-box —— 只加左右内衬会让它的盒子涨到 228.8px、内容框仍是 200,表格跟着
       宽 200,第二列跑到 x=160.4,与 Lv.1 的 x=146 差 14.4px。必须同时改 border-box,
       内容框才回到 171.2,与 .bgA(auto 宽,天然 171.2)一致。 */
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 #u-sidebar .bdbA {
        box-sizing:border-box !important;
        padding:.5em 1.2em !important;
    }

    /*   ★★ 2.52.88(收藏页 / 商品页批注「红色框选内容需基于蓝色进行中心对齐。」——
       红框圈住用户名 + 页签那一行,蓝框圈住下面的内容表)——
       实站量出来的根因:#u-top 自己已经居中(960 + 左右 auto),但它内部有两层结构 ——
       用户名的 table 满宽(948,两缘与内容表齐),页签槽 #u-top-nav 却是
       position:absolute + left:12px + 宽度收缩到内容(492px),于是页签整行贴在内容列的
       左缘。内容列越宽,这一行看着越像「没跟上居中」。用户看到的正是这个。
       改法:绝对定位的 left 换成 50%,再用 translateX(-50%) 把自己拉回正中。
       ★ 同时必须 width:max-content —— 绝对定位盒「收缩到内容」的算式是
         min(max(首选最小, 可用空间), 首选),可用空间 = 960 - left(=480) = 480px,
         而页签那一排的自然宽是 492px → 不写 width 就会被压到 480、页签折成两行
         (实站第一版就是这个样子:消息/设置被挤到第二行)。max-width:100% 兜住窄窗口,
         窗口很窄时才允许折行,不再出现「被 leftover 空间挤折」。 */
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 #u-top-nav {
        left:50% !important;
        width:max-content !important;
        max-width:100% !important;
        transform:translateX(-50%) !important;
    }

    /*   ★★ 2.52.88(收藏页黄框 + 「文本要用UI包裹。」):收藏分类目录 / 商品的分类项是
       td.fav > a(裸链接,13px 高的光文字、无底无边)。套成与左栏按钮、页签同族的
       深空药丸;当前分类(td.fav.current,站点 setCurrent 脚本挂的)用 teal 实心档 ——
       与页签选中态同一套色,读者一眼能看出「点开的是哪一档」。 */
       /* ★★ 2.55.17 补一处**少了开头的注释符**(2.52.88 起就在,一直在静默吞规则):
          这一行被浏览器当成选择器,撞上它末尾的注释结束符之后,下面那条
          #u-wrap2 #u-contentside table.u-table —— width:100% + margin-left:0 ——
          整条被吞掉:实测第二张分类表的 margin-left 仍是行内那 5%(47px),比上一张
          右移 47px,每个自定义目录那枚「×」也跟着跑到容器外 47px(收藏页 / 商品页同病)。
          补上开头的注释符,规则回归,两张表的左缘与「×」的右缘各自对齐。
          ★ 教训:CSS 那几段住在模板串里,注释符漏一个不会报语法错,只会静默少一条规则
            (node --check 永远看不见)—— 所以要靠 probe-css.js 那种体检,不能靠肉眼。 */
       /* ▲ 为什么用 UI 包裹而不是加间距:td.fav 是 31px 高的整行格,裸链接的点击区只有 13px
         高、而且视觉上「分类」两个字和标题行分不开;药丸把点击区撑到 23px 且自带边界。 */
    /*   ★★ 2.52.88:第二张分类表(默认分类那一张)站点给了行内 width="95%" +
       style="margin-left:5%",于是它的药丸比上一张右移 5% —— 同一组分类左右错开一格。
       两份行内声明都压掉,整组左缘对齐。 */
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 #u-contentside table.u-table {
        width:100% !important;
        margin-left:0 !important;
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 #u-contentside td a {
        display:inline-flex;
        align-items:center;
        padding:5px 12px !important;
        border-radius:7px;
        background-color:rgba(255,255,255,0.055) !important;
        color:#ffffff !important;   /* 2.55.21:#c3dcff → 纯白 */
        text-decoration:none !important;
        box-shadow:inset 0 0 0 1px rgba(255,255,255,0.10);
        transition:background-color .15s ease, color .15s ease;
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 #u-contentside td a:hover {
        background-color:rgba(32,201,151,0.18) !important;
        color:#d8fff5 !important;
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 #u-contentside td.fav.current a {
        background-color:rgba(32,201,151,0.16) !important;
        color:#d8fff5 !important;
        font-weight:600;
        box-shadow:inset 0 0 0 1px rgba(32,201,151,0.42);
    }

    /*   ★★ 2.52.89(首页批注「红色框选区域相对于黄色框选区域,以轴中轴对称对齐。」——
       红框圈住 签名+编辑,黄框圈住页签行,两根黄箭头分别指着两者的中轴)——
       实测结构:#u-top > table(内联 float:left、948px)里两格 ——
         TD1 = 258px 装 <h1>用户名</h1>;TD2 = 690px(内联 padding-left:1em)装 #honor 签名 + 编辑。
       签名在 TD2 里左对齐,于是它的中轴落在 893,而页签行(已居中)的中轴是 954 —— 差 61px。
       改法:把这张表变成定宽 100% + table-layout:fixed 的两格表,**名字格压到 1px**
       (名字用 nowrap 溢出显示,它只有 ~70px 宽,离居中后的签名还有 260px 空档,不会叠),
       签名格于是独占整幅 960、text-align:center 就把它摆在内容列中轴上(实测偏差 0px)。
       ★ 为什么不让整行居中(名字+签名一起):批注圈的是**签名**那一块,箭头标的是签名的中轴;
         整行居中会让签名的中轴落到 1050(偏右 96px),反而更不齐。 */
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 #u-top > table {
        table-layout:fixed !important;
        width:100% !important;
    }
    /*   ★ 2.52.89 实站量出来的第二层偏移:#u-top 自带 padding-left:12px,
       表 width:100% 于是只有 948(486..1434)、中轴 960,而内容列中轴是 954 —— 签名居中后仍偏 6px。
       这一格左内衬只是给用户名的缩进,归零不影响别处(页签槽是绝对定位,containing block 是 padding box,
       不受 padding-left 影响:实测归零前后它都落在 954)。 */
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 #u-top { padding-left:0 !important; }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 #u-top > table > tbody > tr > td:first-child {
        width:1px !important;
        white-space:nowrap !important;
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 #u-top > table > tbody > tr > td:last-child {
        text-align:center !important;
        padding-left:0 !important;
    }

    /*   ★★ 2.52.89(首页批注「按钮和划线相交了」——红框圈住右栏那三只「更多」)——
       实测:小节标题行的发丝线画在 h5 的 border-box 底边(y=205.5),
       而「更多」药丸是 position:absolute、按静态位置落在内容行顶(y=183),
       药丸 26px 比行高 18px 高 8px → 一路压到 y=209,**探出发丝线 3.5px**。
       改法:给药丸 top:50% + translateY(-50%),让它在标题行里垂直居中 ——
       底边落到 200.5,离发丝线留 5px(不动行高、不动 float/right 定位)。 */
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 h5.u-h5 a {
        top:50% !important;
        transform:translateY(-50%) !important;
    }

    /*   ★★ 2.52.89(回复页红框「变大一点」,箭头指向底部分页条)——
       u.php 的翻页条是 #u-contentmain 里一只裸 div.pages(12px 字、22px 行、li 靠 float 横排),
       没吃到 2.52.10 给版块页/帖子页那套(那套收口在 #main .spp-pgbar,而 .spp-pgbar 由 JS
       只打在含 .pages 的 .t3 行上,u.php 没有 .t3 包裹)—— 所以它在 u.php 里一直是原样。
       这里按**同一套数字**补:页码 22→32px、字号 12→13.5px、当前页 teal 实心、
       Go 输入框 57×21→62×30、ul 统一 flex 化(站点 li 是 float,不 flex 化撑不开高度)。 */
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 .pages {
        float:none !important;
        height:auto !important;
        line-height:1 !important;
        margin:14px 0 0 !important;
        padding:7px 10px !important;
        border:0 !important;
        border-radius:12px !important;
        background-color:rgb(0 0 0 / 0.86) !important;
        box-shadow:0 10px 26px rgba(0,0,0,0.34), inset 0 0 0 1px rgba(255,255,255,0.09) !important;
        font-size:13.5px !important;
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 .pages ul {
        display:flex !important;
        align-items:center !important;
        flex-wrap:wrap !important;
        gap:6px !important;
        margin:0 !important;
        padding:0 !important;
        list-style:none !important;
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 .pages ul li {
        display:inline-flex !important;
        float:none !important;
        align-items:center !important;
        margin:0 !important;
        padding:0 !important;
        list-style:none !important;
        border:0 !important;
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 .pages ul li a,
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 .pages ul li b {
        display:inline-flex !important;
        align-items:center !important;
        justify-content:center !important;
        box-sizing:border-box !important;
        height:32px !important;
        min-width:32px !important;
        padding:0 10px !important;
        margin:0 !important;
        font-size:13.5px !important;
        line-height:1 !important;
        text-decoration:none !important;
        border-radius:9px !important;
        background-color:rgba(255,255,255,0.05) !important;
        color:#ffffff !important;   /* 2.55.21:#c3dcff → 纯白 */
        box-shadow:inset 0 0 0 1px rgba(255,255,255,0.10);
        transition:background-color .15s ease, color .15s ease;
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 .pages ul li a:hover {
        background-color:rgba(32,201,151,0.18) !important;
        color:#d8fff5 !important;
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 .pages ul li b {
        background-color:rgba(32,201,151,0.16) !important;
        color:#d8fff5 !important;
        font-weight:600 !important;
        box-shadow:inset 0 0 0 1px rgba(32,201,151,0.42);
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 .pages ul li.pagesone {
        gap:8px !important;
        margin-left:10px !important;
        color:#eaf2ff !important;   /* 2.55.21:#c6d5e8 → #eaf2ff */
        font-size:13px !important;
        white-space:nowrap !important;
    }
    html.spp-hm-star.spp-hm-uprofile #u-wrap2 .pages ul li.pagesone input[type=text] {
        box-sizing:border-box !important;
        width:62px !important;
        height:30px !important;
        padding:0 9px !important;
        font-size:13px !important;
        border-radius:9px !important;
    }
    ` : '');

        let el = document.getElementById('spp-starfield-style');
        if (!el) {
            el = document.createElement('style');
            el.id = 'spp-starfield-style';
            appendToHeadSafe(el);
        }
        el.textContent = CSS;

        function mount() {
            if (document.getElementById('spp-starfield')) return;
            const wrap = document.createElement('div');
            wrap.id = 'spp-starfield';
            wrap.setAttribute('aria-hidden', 'true');
            SPP_STAR_LAYERS.forEach(function (L) {
                const d = document.createElement('i');
                d.style.width = L.size + 'px';
                d.style.height = L.size + 'px';
                d.style.setProperty('--spp-star-size', L.size + 'px');
                d.style.animationDuration = L.dur + 's';
                d.style.setProperty('--spp-star-sh', sppStarShadow(SPP_STAR_PTS[L.key]));
                wrap.appendChild(d);
            });
            const host = document.body || document.documentElement;
            host.insertBefore(wrap, host.firstChild);
            if (glass) document.documentElement.classList.add('spp-hm-star');
        }
        onDomReady(mount);

    }

    // ==================== 2.52.78:社区论坛任务页(深色横条 + UI 包裹 + 自动领取)====================
    // 用户批注(plugin.php?H_name-tasks.html 截图,三处圈注):
    //   绿框「参考主页美化」            → #infobox(用户信息条)要像主页那样是深色横条板块
    //   蓝箭头「单独一个横条板块,美化」 → #breadcrumbs 独立成一条深色横条
    //   黄框「分析网页结构,增加一个功能登入自动领取」+「UI包裹、美化」
    //
    // 结构实测(tools/probes/probe-tasks-struct.js / -style.js / -inner.js):
    //   #main > table(裸表:无 .t 外壳)> tbody > tr > td{280,1120}
    //     td1 > div.t ×3(功能简介 / 操作 / 版权)    td2 > div.t ×1(任务列表)
    //   #infobox    154,119,1400,57  bgc rgba(250,252,255,.93)   ← 浅灰横条 1
    //   #breadcrumbs 154,184,1400,27 bgc rgb(248,248,248)+bg_tr.gif+1px #ddd ← 浅灰横条 2
    //   div.t 的 td.h 计算样式 bgc 透明(站点那张渐变表头被星空的「表格内部一律透明」刷掉了)
    //   右栏任务卡高 194,左栏三卡合计 444 → 右栏下方留 250px 空档,星空直接透出来(「破洞」)
    //
    // 自动领取协议:站点自己的领取按钮走的就是这个(hack/tasks/image/index.js 全文 380B):
    //   function startjob(id){
    //       ajax.send('plugin.php?H_name=tasks&action=ajax&actions=job&cid='+id, ...
    //   返回 <ajax><![CDATA[success\t任务领取完成...]]></ajax>;被拒时首段是 confirm:
    //   ★ 实测(2.52.78,冷却期内):cid=14 → confirm\t拒离上次申请[周常]还没超过 158 小时
    //     —— 冷却期调用只会拿到一句拒绝说明,绝不会误领,所以后台预探是安全的。
    //   ★ 领取按钮只在「可领取」时才渲染,容器 id 固定是 p_<cid>(startjob 的回调写回 p_<cid>),
    //     所以扫描器两种写法都认:id="p_<cid>" 与 onclick="startjob(<cid>)"。
    //   ★ 入口:window.__SPP_TASKS = { run, scan } —— 控制台敲 await __SPP_TASKS.run() 可手动跑一次。
    //
    // ★★ 2.55.4:上面这段 2.52.78 的账里有一处**致命误读**,本轮查明并修掉 ——
    //   「站点自己的领取按钮走的就是这个」只对了一半:站点有**两个** ajax 口,
    //     · job  = 申请(首页 p_<cid> 的 sh.png,源码 hack/tasks/image/index.js)
    //     · job2 = 领取(进行中页 both_<cid> 的 god.png,源码 hack/tasks/image/newtasks.js)
    //   2.52.78 把两者当成一个,claim() 一律打 job ⇒ 每次都拿到
    //   「confirm 您申请过[日常]还未完成,不需要重新申请!」,被记成 rejected 就收工 ——
    //   也就是说这个功能从上线起**一次都没领成过**(控制台那行「扫描 1 枚按钮 / ok:0」就是它)。
    //   实测证据(2026-10-05 活体,用户账号):
    //     · job2 → success …\t15,日常随即进「已完成任务」,完成时间 2026-10-05 16:39:14;
    //     · 从茶馆页(非任务页)发同样的 job2,返回与任务页逐字相同 ⇒ **不看 Referer**;
    //     · 无需 verify 校验、GET 即可(站点自己用 POST + verify,但服务端并不强制);
    //     · job 在冷却期的拒绝语里带剩余小时:「拒离上次申请[周常]还没超过 158 小时」;
    //     · ★ 日常领完当天,首页立刻又渲染出「申请任务」⇒ 站点对日常**不设**冷却,
    //       所以脚本必须自己上闸(GUARD = 24h),否则每 3 小时就能再薅一次 +2 SP。
    function isTaskPage() {
        return PAGE.task;   // 2.52.92:判定搬进 PAGE(口径不变,仍是 plugin.php + H_name-tasks 精确匹配)
    }

    // 只挂一个根类名:所有任务页专属的 CSS 都靠它收口,不牵连个人中心页/版块页
    function initTaskPage() {
        if (!isTaskPage()) return;
        document.documentElement.classList.add('spp-hm-task');
    }

    // ==================== 2.52.85:个人主页(u.php?uid-*)====================
    // 用户批注(u.php?uid-1515110.html 截图,四色圈注):屏蔽 logo 横幅 / 面包屑当前位高亮 /
    // 板块适当加宽 / 主卡创意美化(点名可从 uiverse 取样式)。
    // 结构实测(tools/probes/…):这页与 profile.php(个人中心,2.52.69 定稿)不同族 ——
    //   #infobox 不存在、无 #set-wrap,此前所有页型门控都罩不到它,整页还是站点原样:
    //   #header > table(920px logo 横幅,红框)、.bdbA > #breadcrumbs(rgb(248,248,248) 浅条,
    //   当前位 = a.crumbs-item.current b(探针里 className 截成 cu,别被它骗),蓝框)、#u-wrap > #u-wrap2.cc(整幅白卡,绿框)。
    // 内层实测干净:table/tr/td 全透明零边框,白底只挂在 #u-wrap2 一层 —— 重刷比任务页简单。
    function isUprofilePage() {
        return /^\/u\.php$/i.test(location.pathname);
    }

    // 同任务页的做法只挂一个根类名,CSS 全靠它收口(含 ?action-* 子页,同族同主题)
    function initUprofilePage() {
        if (!isUprofilePage()) return;
        const root = document.documentElement;
        root.classList.add('spp-hm-uprofile');
        //   ★★ 2.52.87(用户第六轮:回复页「这里要用卡片化」):子页类型也挂成根类 ——
        //   「回复列表卡片化」只该在这一页生效,而 CSS 读不到 query string(只有 html
        //   上的类能带进样式表)。个人首页没有 action-*,落到 spp-up-index。
        //   白名单字符集,避免把 query 里的任意串直接当类名。
        //   ★★ 2.52.88(用户在收藏 / 商品两页批注「红色框选内容需基于蓝色中心对齐」,
        //      并顺带量出来):站点会把 query 重排成 uid-…-action-… 的形态
        //      (?action-post-uid-1515110.html ⇄ ?uid-1515110-action-post.html 两种都真实存在),
        //      而原来的 [?&]action- 只认前者 —— 直接把地址栏那种 uid-…-action-… 粘进去时
        //      匹配不上,页型恒为 spp-up-index,「回复列表卡片化」整条永不生效。
        //      改成只认 action- 子串,两种形态通吃。
        //   ★★ 2.55.17:还有第三种形态 —— **等号写法**(?action=favor,用户给的收藏页
        //      地址就是这个)。它一个 action- 都没有,于是页型恒为 spp-up-index:
        //      「回复列表卡片化」(.spp-up-post)与本次的收藏页样式(.spp-up-favor)
        //      在这种写法下整条永不生效。分隔符改成 [-=],两种写法通吃。
        const m = /action[-=]([a-z_]+)/i.exec(location.search);
        root.classList.add('spp-up-' + (m ? m[1].toLowerCase() : 'index'));
    }

    /* ==================== 2.55.31:主页回复卡片「板块 / 时间 → 右下角」====================
       用户原话:「对于主页回复,把板块、时间放在右下角;主题文字居中、变大、变白、增亮」。
       实测这一页的卡片结构(u.php?action-post-uid-1515110,2026-10-06):
         tr > th  = 标题 <a> + <br> + <a class="gray">茶馆</a> + <span class="gray f9">[2026-10-06]</span>
         tr > td  = 作者:<a class="gray">xxx</a>            ← 卡片页脚(CSS 那条 inset 发丝线)
       ★ 为什么必须动 DOM(CSS 做不到):板块/时间与页脚分属两格,纯 CSS 只能用绝对定位
         +猜另一枚的宽度;搬进页脚后一条 margin-left:auto 就够(样式在 2.52.88 那一节)。
       ★ 只搬节点、不改站点结构:两枚元素是**移动**(不是克隆),href / class 一字未改,
         站点自己的跳转照常;只在回复页跑,只在没搬过时动手(dataset 挡第二遍)。 */
    function isUprofilePostPage() {
        return isUprofilePage() && /action[-=]post\b/i.test(location.search);
    }
    function initUprofilePostCards() {
        if (!isUprofilePostPage()) return;
        onDomReady(function () {
            const rows = document.querySelectorAll('#u-contentmain > table.u-table > tbody > tr');
            for (let i = 0; i < rows.length; i++) {
                const tr = rows[i];
                if (tr.dataset.sppUpc) continue;
                const th = tr.querySelector(':scope > th');
                const td = tr.querySelector(':scope > td');
                if (!th || !td) continue;
                const board = th.querySelector('a.gray');           // 板块
                const date = th.querySelector('span.gray.f9');      // 时间
                if (!board || !date) continue;
                const box = document.createElement('span');
                box.className = 'spp-upc-meta';
                box.appendChild(board);
                box.appendChild(date);
                td.appendChild(box);
                tr.dataset.sppUpc = '1';
            }
        });
    }

    // ==================== 2.55.17:收藏页(u.php?action=favor)工具条 ====================
    /* 用户原话:「对于网址:https://blue-plus.net/u.php?action=favor 字体改为亮白色、
       美化删除 UI」。亮白那一半在 CSS(.spp-up-favor 段),这里做 CSS 做不了的那一半:
       站点工具条是「radio + 紧跟一个【裸文本节点】」——
         <input type="radio" name="job" value="change" checked>&nbsp;目录转换 <select …>
         <input type="radio" name="job" value="clear">&nbsp;删除
       裸文本节点没有选择器能选中(CSS 选不到文本),所以把每一对包成 <label> 胶囊:
       点整枚胶囊即选中(原生 label 行为),选中态上色 —— 「删除」红调,与无害的
       「目录转换」区分开;选中「删除」时提交键同步转红(破坏性动作先给个警示)。
       ★ 只搬动 radio 与它后面那个文本节点:name / value / form / onclick 一字未改,
         站点自己的 CheckAll(全选) 与 Checkinfo(删除确认) 照常工作。
       ★ 只在收藏页、只在没包过时动手(center.dataset 挡第二遍)。 */
    function isFavorPage() {
        return isUprofilePage() && /action[-=]favor\b/i.test(location.search);
    }
    function initFavorPageUI() {
        if (!isFavorPage()) return;
        onDomReady(function () {
            const center = document.querySelector('#u-contentmain > form > center');
            if (!center || center.dataset.sppFavJob) return;
            const radios = center.querySelectorAll('input[type="radio"][name="job"]');
            if (radios.length < 2) return;   // 结构变了就别动:宁可不上色,也别把表单弄坏
            center.dataset.sppFavJob = '1';
            function paintFavJob() {
                Array.prototype.forEach.call(center.querySelectorAll('.spp-fav-job'), function (l) {
                    const inp = l.querySelector('input[type="radio"]');
                    l.classList.toggle('is-on', !!(inp && inp.checked));
                });
                const del = center.querySelector('.spp-fav-job--del input[type="radio"]');
                center.classList.toggle('spp-fav-danger', !!(del && del.checked));
            }
            Array.prototype.forEach.call(radios, function (r) {
                const label = document.createElement('label');
                label.className = 'spp-fav-job' + (r.value === 'clear' ? ' spp-fav-job--del' : '');
                r.parentNode.insertBefore(label, r);
                label.appendChild(r);
                /* 紧跟其后的文本节点(「&nbsp;目录转换 」/「&nbsp;删除」)搬进胶囊 ——
                   一路吃到第一个「去掉空白后还有字」的文本节点为止;再往后就不是这一项的
                   文字了。中间那些纯 &nbsp; 留在原地:它只是一段行内空白,无害。 */
                let n = label.nextSibling;
                while (n && n.nodeType === 3) {
                    const nxt = n.nextSibling;
                    const t = String(n.nodeValue || '').replace(/[\s\u00a0]+/g, ' ').trim();
                    label.appendChild(n);
                    if (t) break;
                    n = nxt;
                }
                r.addEventListener('change', paintFavJob);
            });
            paintFavJob();
            /* 目录那一串「×」是站点的 16px 位图(tips-close.png),CSS 里已经把它藏掉、
               换成自绘方片;这里把悬停提示也换成脚本自己的卡片(原生 title 的样式改不了)。
               ★ 提示文案照实写:那个 onclick 走的是站点 Checkinfo(),它就是一句 confirm。 */
            if (SPP_TIP && SPP_TIP.attach) {
                Array.prototype.forEach.call(
                    document.querySelectorAll('#u-contentside td.fav > a.fr'),
                    function (a) { SPP_TIP.attach(a, '删除这个分类目录(会先弹一次确认框)'); }
                );
            }
        });
    }

    function initTaskAutoClaim() {
        // ★★ 2.55.4:读数点写成**字面量**(不再套 KEY 变量)—— 面板守卫与文档生成器都是按
        //   SPP_CONFIG.bool('spp_task_autoclaim', true) 这串原文去脚本里找读数点的;
        //   本项目自 2.53.94 起就是这个口径(见 spp_hide_default_avatar 那行)。
        //   驱动它的变量(旧名 KEY)本轮撤掉:它只在这一行用过。
        if (!SPP_CONFIG.bool('spp_task_autoclaim', true)) return;

        GM_addStyle(
            '#spp-task-toast {' +
            'position:fixed; right:18px; bottom:18px; z-index:100004;' +
            'max-width:min(370px, calc(100vw - 36px)); padding:12px 15px; border-radius:12px;' +
            'background-color:rgba(10,14,22,0.94);' +
            'background-image:linear-gradient(180deg, rgba(255,255,255,0.07) 0%, rgba(255,255,255,0) 70%);' +
            'background-repeat:no-repeat; color:#dbe4ef; border:1px solid rgba(255,255,255,0.10);' +
            'box-shadow:0 20px 54px rgba(2,6,23,0.6), inset 0 1px 0 rgba(255,255,255,0.06);' +
            'backdrop-filter:blur(14px) saturate(140%); -webkit-backdrop-filter:blur(14px) saturate(140%);' +
            'font-size:12.5px; line-height:1.75; opacity:0; transform:translateY(10px);' +
            'pointer-events:none; transition:opacity .3s ease, transform .3s ease; }' +
            '#spp-task-toast.spp-on { opacity:1; transform:translateY(0); }' +
            '#spp-task-toast .spp-tt-h { color:#eaf2fb; font-weight:650; margin-bottom:3px; }' +
            '#spp-task-toast .spp-tt-b { color:#9fb0c4; }'
        );

        const LAST = 'spp_task_claim_at';
        const LOG = 'spp_task_claimed';
        const GAP = 3 * 60 * 60 * 1000;   // 非任务页:3 小时内最多后台探一次(任务冷却 18~24h,够用)
        // ★★ 2.55.4 本地冷却闸门:站点对「日常」这一档**不设**冷却 —— 实测领完当天再打开
        //   任务页,首页立刻又渲染出「申请任务」按钮。脚本若只认「有按钮就领」,每 3 小时
        //   就能再薅一次 +2 SP,那是在刷分。所以自己给自己上一道闸:同一 cid 领成功过一次
        //   之后 24 小时内不再碰它(周常那种 158 小时的冷却由站点那侧挡着,不靠这条)。
        const GUARD = 24 * 60 * 60 * 1000;
        const TASK_URL = 'plugin.php?H_name-tasks.html';
        const RUN_URL = 'plugin.php?H_name-tasks-actions-newtasks.html.html';
        // ★★ 2.55.4:站点那两个动作是**两个不同的口**(2.52.78 只认了第一个 ⇒ 从来没领成过):
        //   job  = 申请,job2 = 领取。两步都走同一个 ajax 口,只有 actions 不同。
        const JOB = 'plugin.php?H_name=tasks&action=ajax&actions=job&cid=';
        const JOB2 = 'plugin.php?H_name=tasks&action=ajax&actions=job2&cid=';
        const ON_TASK = isTaskPage();
        const now = Date.now();
        if (!ON_TASK && now - (Number(GM_getValue(LAST, 0)) || 0) < GAP) return;
        GM_setValue(LAST, now);

        // 从一段 HTML 里抽「可领取」的 cid:只认站点自己那两种渲染方式
        function cidsFrom(html) {
            const out = [];
            let m;
            const reJob = /startjob\(\s*(\d+)\s*\)/g;
            while ((m = reJob.exec(html))) if (out.indexOf(m[1]) < 0) out.push(m[1]);
            if (!out.length && html.indexOf('startjob') >= 0) {
                const reId = /\bid=["']p_(\d+)["']/g;
                while ((m = reId.exec(html))) if (out.indexOf(m[1]) < 0) out.push(m[1]);
            }
            return out;
        }
        function scanDoc() {
            return Promise.resolve(cidsFrom(document.documentElement.innerHTML));
        }
        function scanUrl(url) {
            const u = url + (url.indexOf('?') < 0 ? '?' : '&') + '_spp=' + Date.now();
            return fetch(u, { credentials: 'same-origin' })
                .then(function (r) { return r.ok ? r.text() : ''; })
                .then(cidsFrom)
                .catch(function () { return []; });
        }
        // ★★ 2.55.4:两页一起扫、取**并集**。2.52.78 是「首页有按钮就不看进行中页」——
        //   于是卡在「已申请、还没领取」的 cid(只有进行中页会渲染它)永远扫不到。
        function scanAll() {
            const jobs = [scanUrl(TASK_URL), scanUrl(RUN_URL)];
            if (ON_TASK) jobs.push(scanDoc());
            return Promise.all(jobs).then(function (rs) {
                const list = [];
                rs.forEach(function (a) {
                    (a || []).forEach(function (c) { if (list.indexOf(c) < 0) list.push(c); });
                });
                return list;
            });
        }
        // 打一次任务插件的 ajax 口,把 <![CDATA[success\t…\t<cid>]]> 拆成三段
        function post(url) {
            return fetch(url + '&nowtime=' + Date.now(),
                { credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
                .then(function (r) { return r.text(); })
                .then(function (txt) {
                    const cd = /<!\[CDATA\[([\s\S]*?)\]\]>/.exec(txt);
                    const parts = (cd ? cd[1] : txt).split('\t');
                    return { ok: parts[0] === 'success', msg: (parts[1] || '').trim(), tid: (parts[2] || '').trim() };
                })
                .catch(function () { return { ok: false, msg: '请求失败' }; });
        }
        // ★★ 2.55.4:领取 = 先申请、再领取(成败一律以 job2 为准)。
        //   · 已经申请过时 job 会回「还未完成,不需要重新申请」——实测**不会**重置进度,无害;
        //   · job 的拒绝里可能带「拒离上次申请[X]还没超过 N 小时」——把 N 记进闸门,
        //     省得冷却期内每 3 小时回来敲一次门。
        function claim(cid) {
            return post(JOB + cid).then(function (ap) {
                const h = /还没超过\s*(\d+)\s*小时/.exec(ap.msg || '');
                return post(JOB2 + cid).then(function (g) {
                    g.cid = cid;
                    g.step = ap.msg;
                    g.wait = h ? Number(h[1]) * 3600 * 1000 : 0;
                    return g;
                });
            });
        }
        // 本地闸门:{ "<cid>": { at: 上次领成功, until: 站点报的冷却到期 } }
        function gateRead() {
            try { return JSON.parse(GM_getValue(LOG, '{}')) || {}; } catch (_) { return {}; }
        }
        function gateLeft(g, cid) {
            const r = g[cid];
            if (!r) return 0;
            const until = Math.max((Number(r.at) || 0) + GUARD, Number(r.until) || 0);
            return until - Date.now();
        }
        function gateSave(g) {
            try { GM_setValue(LOG, JSON.stringify(g)); } catch (_) { }
        }
        function toast(title, lines) {
            if (!document.body) { onDomReady(function () { toast(title, lines); }); return; }
            let box = document.getElementById('spp-task-toast');
            if (!box) { box = document.createElement('div'); box.id = 'spp-task-toast'; document.body.appendChild(box); }
            box.textContent = '';
            const h = document.createElement('div');
            h.className = 'spp-tt-h';
            h.textContent = title;
            box.appendChild(h);
            lines.forEach(function (t) {
                const d = document.createElement('div');
                d.className = 'spp-tt-b';
                d.textContent = t;
                box.appendChild(d);
            });
            void box.offsetHeight;
            box.classList.add('spp-on');
            if (box.__sppT) clearTimeout(box.__sppT);
            box.__sppT = setTimeout(function () { box.classList.remove('spp-on'); }, 9000);
        }

        let running = false;
        function run() {
            if (running) return Promise.resolve({ skipped: 'already-running' });
            running = true;
            const gate = gateRead();
            const res = {};
            const got = [];
            const found = [];   // ★ 本轮在两页上扫到的全部 cid(含被闸门挡下的)—— 看日志用
            let scanned = 0;
            // 两轮:第一轮把两页上渲染出来的 cid 全试一遍;
            // 1.5s 后再扫一次,只补第一轮「试过、没成、站点也没报冷却」的那几枚
            //(站点偶发是「先申请、后领取」两拍,第二拍那枚按钮要等一会儿才渲染出来)。
            function round(n) {
                return scanAll().then(function (cids) {
                    cids.forEach(function (c) { if (found.indexOf(c) < 0) found.push(c); });
                    const todo = cids.filter(function (c) {
                        if (gateLeft(gate, c) > 0) return false;
                        if (n === 0) return true;
                        const r = res[c];
                        return !!(r && r.round === 0 && !r.ok && !r.wait);
                    });
                    if (!todo.length) return;
                    scanned += todo.length;
                    return todo.reduce(function (p, c) {
                        return p.then(function () {
                            return claim(c).then(function (g) {
                                g.round = n;
                                res[c] = g;
                                got.push(g);
                                // ★ 每成功一枚就立刻落盘:万一这中间页面被关掉,
                                //   闸门也不会丢(丢了就等于下次又能领一次)。
                                if (g.ok) { gate[c] = { at: Date.now() }; gateSave(gate); }
                                else if (g.wait) { gate[c] = { until: Date.now() + g.wait }; gateSave(gate); }
                            });
                        });
                    }, Promise.resolve()).then(function () {
                        if (n > 0) return;
                        return new Promise(function (x) { setTimeout(x, 1500); }).then(function () { return round(1); });
                    });
                });
            }
            return round(0).then(function () {
                running = false;
                gateSave(gate);
                const ok = got.filter(function (g) { return g.ok; });
                const no = got.filter(function (g) { return !g.ok; });
                const out = { found: found, scanned: scanned, ok: ok.length, rejected: no.length, results: got };
                if (ok.length) {
                    toast('社区论坛任务 · 自动领取 ' + ok.length + ' 项',
                        ok.map(function (g) { return '✓ ' + (g.msg || ('cid ' + g.cid)); }));
                    console.info('[SPP] 社区论坛任务自动领取成功:', out);
                } else {
                    console.info('[SPP] 社区论坛任务自动领取:本次没有可领取的任务(尝试 ' + scanned + ' 枚按钮)', out);
                }
                return out;
            }).catch(function (e) {
                running = false;
                console.warn('[SPP] 社区论坛任务自动领取异常:', e);
                return { error: String(e) };
            });
        }

        window.__SPP_TASKS = {
            run: run,
            scan: function () { return scanAll(); },
            urls: [TASK_URL, RUN_URL]
        };

        // ★ 2.52.80:同一个出口的 DOM 版本 —— sandbox 里挂在 window 上的东西,页面控制台
        //   取不到(实测 __SPP_TASKS 是 undefined)。控制台改敲:
        //     document.dispatchEvent(new Event('spp-tasks-run'))
        //   document 是两个世界共享的,事件一定收得到。
        document.addEventListener('spp-tasks-run', function () { run(); });

        if (document.readyState === 'interactive' || document.readyState === 'complete') {
            setTimeout(function () { run(); }, 500);
        } else {
            onDomReady(function () { setTimeout(function () { run(); }, 500); });
        }
    }

    // ==================== 2.52.6:公告区按钮化(方案 A · 用户定稿) ====================
    // 用户要求:「在公告状态栏上方额外添加几个 UI 按钮,选中按钮后,状态栏切换至对应的信息模板;
    // 同时,公告栏默认处于折叠状态;将页面内的子板块移至 UI 按钮栏目,并在子板块、公告及本板块公告
    // 之间添加分隔线。图片内容也转换为 UI 按钮,放置于公告栏区域内(切换到该按钮时,状态栏内显示的
    // 是本板块的图片)」—— 在 preview\版块页公告区按钮化.html 的两方案里定稿 A。
    //
    // ★ 结构:按钮行(#spp-btabs) + 共用面板(#spp-announce),两个 id 与旧的本版公告条(#spp-bnotice)
    //   共用同一份 CSS(根选择器 :is(...) 里三个 id)。面板由公告板块建、本模块只往里塞内容。
    // ★ 时序:面板是 initAnnounceBoard 在 onDomReady 里建的(它把 run() 放在 onDomReady),
    //   本模块在它之后注册,onDomReady 回调按注册顺序走 —— 所以本段也必须 onDomReady,否则
    //   同步执行时看到的 panel 还是 null,会误走兜底分支。
    // ★ 三份内容的容器(线上实测 thread_new.php?fid-9-page-1.html):
    //     本版公告 → #cate_thread > 第一个 .tr1 面板里、版头图之外的那些节点
    //     版头图   → 同一个面板里的 DIV[align] > IMG(900×506)
    //     子版块   → #cate_children(TBODY) > TABLE > 外层 DIV.t.z(与 #ajaxtable 的外层不是同一个)
    //   搬空之后 #cate_thread 会变空,它头上那行 UL.gongul「版块公告」图例就没有意义了,一并收起。
    // ★ 幂等:建过 #spp-btabs 就不再建;兜底分支用 #spp-bnotice 判重。
    // ===== SPP-T5BAR-JS-BEGIN =====
    // ==================== 2.52.11:工具条下拉改自绘 ====================
    // ① 原生 <select> 的弹层由系统绘制(白底 + 系统蓝高亮 + 圆角都改不动),CSS 碰不到它 —— 所以保留
    //    <select> 当「表单值载体」(display:none 也照旧按 name/value 提交),另画 .spp-dd-btn + .spp-dd-pop。
    // ② 只在传入的 root(#main .spp-t5bar)范围内动手,别处的 select 一概不碰;已处理过的用 dataset.sppDd 判重。
    // ③ 键位:↑↓ 移动高亮、Enter 选中、Esc 关闭;点页面空白处关闭(文档级监听只绑一次)。
    const sppDrops = [];
    function sppDropsClose(except) {
        sppDrops.forEach(function (d) { if (d !== except) d.close(); });
    }
    document.addEventListener('click', function () { sppDropsClose(null); });
    document.addEventListener('keydown', function (e) { if (e.key === 'Escape') sppDropsClose(null); });

    function initDropdowns(root) {
        if (!root) return 0;
        let n = 0;
        Array.prototype.forEach.call(root.querySelectorAll('select'), function (sel) {
            if (sel.dataset.sppDd) return;
            sel.dataset.sppDd = '1';
            n++;

            const wrap = document.createElement('span');
            wrap.className = 'spp-dd';
            sel.parentNode.insertBefore(wrap, sel);
            wrap.appendChild(sel);

            const btn = document.createElement('button');
            btn.type = 'button';
            btn.className = 'spp-dd-btn';
            btn.setAttribute('aria-haspopup', 'listbox');
            btn.setAttribute('aria-expanded', 'false');
            const txt = document.createElement('span');
            txt.className = 'spp-dd-txt';
            const car = document.createElement('span');
            car.className = 'spp-dd-caret';
            car.setAttribute('aria-hidden', 'true');
            btn.appendChild(txt);
            btn.appendChild(car);

            const pop = document.createElement('div');
            pop.className = 'spp-dd-pop';
            pop.setAttribute('role', 'listbox');
            const opts = [];
            Array.prototype.forEach.call(sel.options, function (o, i) {
                const d = document.createElement('div');
                d.className = 'spp-dd-opt';
                d.setAttribute('role', 'option');
                d.dataset.i = String(i);
                // 站点的 option 文本带换行/制表缩进,统一压成单空格
                d.textContent = String(o.text).replace(/\s+/g, ' ').trim();
                pop.appendChild(d);
                opts.push(d);
            });
            wrap.appendChild(btn);
            wrap.appendChild(pop);

            let hi = 0;
            function paint() {
                opts.forEach(function (d, i) {
                    d.classList.toggle('is-on', i === sel.selectedIndex);
                    d.classList.toggle('is-hi', i === hi);
                });
            }
            function reveal() {
                const on = opts[hi];
                if (on && on.scrollIntoView) on.scrollIntoView({ block: 'nearest' });
            }
            function sync() {
                txt.textContent = opts[sel.selectedIndex] ? opts[sel.selectedIndex].textContent : '';
                paint();
            }
            const api = {
                isOpen: function () { return wrap.classList.contains('is-open'); },
                close: function () {
                    wrap.classList.remove('is-open');
                    btn.setAttribute('aria-expanded', 'false');
                },
                open: function () {
                    sppDropsClose(api);
                    wrap.classList.add('is-open');
                    btn.setAttribute('aria-expanded', 'true');
                    hi = Math.max(0, sel.selectedIndex);
                    paint();
                    reveal();
                },
                move: function (dy) {
                    hi = Math.min(opts.length - 1, Math.max(0, hi + dy));
                    paint();
                    reveal();
                },
                take: function () {
                    sel.selectedIndex = hi;
                    sel.dispatchEvent(new Event('change', { bubbles: true }));
                    sync();
                    api.close();
                    btn.focus();
                }
            };
            btn.addEventListener('click', function (e) {
                e.preventDefault();
                e.stopPropagation();
                if (api.isOpen()) api.close(); else api.open();
            });
            btn.addEventListener('keydown', function (e) {
                if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
                    e.preventDefault();
                    if (api.isOpen()) api.move(e.key === 'ArrowDown' ? 1 : -1); else api.open();
                } else if (e.key === 'Enter' || e.key === ' ') {
                    e.preventDefault();
                    if (api.isOpen()) api.take(); else api.open();
                } else if (e.key === 'Escape') {
                    api.close();
                }
            });
            opts.forEach(function (d, i) {
                d.addEventListener('mouseenter', function () { hi = i; paint(); });
                d.addEventListener('click', function (e) { e.stopPropagation(); hi = i; api.take(); });
            });
            sppDrops.push(api);
            sync();
        });
        return n;
    }
    // ===== SPP-T5BAR-JS-END =====

    function initBoardNotice() {
        if (!PAGE.board) return;
        onDomReady(function () {
            if (document.getElementById('spp-btabs')) return;      // 幂等

            const cat = document.getElementById('cate_thread');
            const panel = document.getElementById('spp-announce');   // 多模板面板(公告板块建的)

            // ---- 1. 找出要收进面板的三份内容 ----
            let rules = null, cover = null, pan = null;
            if (cat) {
                pan = Array.from(cat.children).find(function (c) {
                    return c.classList && c.classList.contains('tr1') &&
                        (c.textContent || '').replace(/\s+/g, '').length >= 40;
                });
                if (pan) {
                    const nodes = [];
                    // ★ 必须用 childNodes,不能用 children:站点那段版规里夹着大量【裸文本节点】
                    //   (<b>/<font> 之间的文字、独立成段的纯文本)—— 只搬元素会把半截文字留在原地。
                    //   2.52.6 就是换成 children 才漏的(实测面板里元素 0 个、却还剩 549 字)。
                    Array.from(pan.childNodes).forEach(function (n) {
                        if (n.nodeType === 1) {
                            // 版头大图:站点结构是 DIV[align] > IMG
                            if (!cover && n.tagName === 'DIV' && n.hasAttribute('align') && n.querySelector('img')) {
                                cover = n;
                                return;
                            }
                            nodes.push(n);
                        } else if (n.nodeType === 3 && n.textContent.trim()) {
                            nodes.push(n);
                        }
                    });
                    if (nodes.length) rules = nodes;
                }
            }
            let subs = null;
            const subBody = document.getElementById('cate_children');
            if (subBody) {
                const tb = subBody.closest('table');
                subs = (tb && tb.parentElement) || tb;
            }
            // 分类行(站点导航 ul.threadlist:全部 / 精华 / thread_type_N…)
            // ★ 只在版块真有主题分类时才收:只有「全部/精华」的版块(如免空资源区)不做这一格,
            //   否则面板里只有两项、按钮也没意义。
            let cats = null, catsWrap = null;
            const catRow = document.querySelector('#ajaxtable ul.threadlist') || document.querySelector('ul.threadlist');
            if (catRow && Array.prototype.some.call(catRow.querySelectorAll('li[id^="thread_type_"]'), function (li) {
                return li.id !== 'thread_type_all' && li.id !== 'thread_type_digest';
            })) {
                cats = catRow;
                catsWrap = catRow.closest('.hthread') || catRow.parentElement;   // 搬走后要收起的空外壳
            }

            // ---- 2. 没有「公告」面板(该页没有置顶/公告条目)→ 退回旧形态:只出一条本版公告条 ----
            if (!panel) {
                if (!rules || document.getElementById('spp-bnotice')) return;
                const anchor = document.createComment('spp-bnotice');
                rules[0].parentNode.insertBefore(anchor, rules[0]);

                const box = document.createElement('section');
                box.id = 'spp-bnotice';
                box.dataset.open = '0';                 // 默认折叠
                const head = document.createElement('button');
                head.type = 'button';
                head.className = 'spp-ann-head';
                head.setAttribute('aria-expanded', 'false');
                const t = document.createElement('span');
                t.className = 'spp-ann-title';
                t.textContent = '本版公告';
                const h = document.createElement('span');
                h.className = 'spp-ann-hint';
                h.textContent = '版规与注意事项';
                const cv = document.createElement('span');
                cv.className = 'spp-ann-chev';
                cv.setAttribute('aria-hidden', 'true');
                head.appendChild(t); head.appendChild(h); head.appendChild(cv);
                const body = document.createElement('div');
                body.className = 'spp-ann-body';
                const inner = document.createElement('div');
                inner.className = 'spp-ann-inner';
                const raw = document.createElement('div');
                raw.className = 'spp-ann-raw';
                rules.forEach(function (n) { raw.appendChild(n); });
                inner.appendChild(raw);
                body.appendChild(inner);
                box.appendChild(head);
                box.appendChild(body);
                anchor.parentNode.replaceChild(box, anchor);
                head.addEventListener('click', function () {
                    const open = box.dataset.open === '1';
                    box.dataset.open = open ? '0' : '1';
                    head.setAttribute('aria-expanded', open ? 'false' : 'true');
                });
                return;
            }

            // ---- 3. 方案 A:三份内容塞进三个面板,再在面板上方插一行按钮 ----
            const panes = {};
            Array.prototype.forEach.call(panel.querySelectorAll('.spp-pane'), function (p) {
                panes[p.dataset.pane] = p;
            });

            function put(key, wrapCls, nodes) {
                const pane = panes[key];
                if (!pane) return false;
                const wrap = document.createElement('div');
                wrap.className = wrapCls;
                nodes.forEach(function (n) { wrap.appendChild(n); });
                pane.appendChild(wrap);
                return true;
            }

            const countEl = panel.querySelector('.spp-ann-count');
            const tabs = [];
            // ★ 2.52.9:没有置顶/公告条目的页面(第 2 页起)不排「公告」这一格
            const noticeNum = countEl ? String(countEl.textContent).trim() : '';
            if (panes.notice && noticeNum !== '0') {
                tabs.push({ key: 'notice', name: '公告', hint: '置顶与论坛公告', num: noticeNum });
            }
            if (rules && put('rules', 'spp-ann-raw', rules)) {
                tabs.push({ key: 'rules', name: '本版公告', hint: '版规与注意事项' });
            }
            if (subs && put('subs', 'spp-pane-subs', [subs])) {
                tabs.push({ key: 'subs', name: '子版块', hint: '本版子版块' });
            }
            if (cover && put('cover', 'spp-pane-cover', [cover])) {
                tabs.push({ key: 'cover', name: '版头图', hint: '本板块图片' });
            }
            // ★ 用户定稿:分类放在版头图【后面】
            if (cats && put('cats', 'spp-pane-cats', [cats])) {
                tabs.push({
                    key: 'cats', name: '分类',
                    hint: Array.prototype.map.call(cats.querySelectorAll('li'), function (li) {
                        return (li.textContent || '').replace(/\s+/g, '');
                    }).filter(Boolean).join(' / ')
                });
                if (catsWrap && !catsWrap.children.length) {
                    catsWrap.style.setProperty('display', 'none', 'important');
                }
            }
            if (tabs.length < 2) return;                 // 只有一个模板就没必要做按钮行

            // ---- 按钮行(按钮之间 1px 竖分隔线)----
            const bar = document.createElement('section');
            bar.id = 'spp-btabs';
            bar.className = 'spp-paired';
            const btns = tabs.map(function (it, i) {
                const b = document.createElement('button');
                b.type = 'button';
                b.className = 'spp-tab' + (i === 0 ? ' is-on' : '');
                b.dataset.pane = it.key;
                b.dataset.name = it.name;
                b.dataset.hint = it.hint || '';
                b.dataset.num = it.num || '';
                b.appendChild(document.createTextNode(it.name));
                if (it.num) {
                    const n = document.createElement('span');
                    n.className = 'spp-tab-n';
                    n.textContent = String(it.num);
                    b.appendChild(n);
                }
                return b;
            });
            btns.forEach(function (b, i) {
                if (i) {
                    const d = document.createElement('span');
                    d.className = 'spp-tab-div';
                    d.setAttribute('aria-hidden', 'true');
                    bar.appendChild(d);
                }
                bar.appendChild(b);
            });

            // ---- 点按钮:切面板 + 同步面板标题 + 展开 ----
            const titleEl = panel.querySelector('.spp-ann-title');
            const hintEl = panel.querySelector('.spp-ann-hint');
            function activate(btn, open) {
                btns.forEach(function (b) { b.classList.toggle('is-on', b === btn); });
                Array.prototype.forEach.call(panel.querySelectorAll('.spp-pane'), function (p) {
                    p.classList.toggle('is-on', p.dataset.pane === btn.dataset.pane);
                });
                if (titleEl) titleEl.textContent = btn.dataset.name;
                if (countEl) {
                    countEl.textContent = btn.dataset.num || '';
                    countEl.style.display = btn.dataset.num ? '' : 'none';
                }
                if (hintEl) hintEl.textContent = btn.dataset.hint || '';
                if (open) panel.dataset.open = '1';
            }
            btns.forEach(function (b) {
                b.addEventListener('click', function () { activate(b, true); });
            });

            // 与面板拼成一块玻璃(上圆下圆 + 接缝分隔线),插在面板正上方
            panel.classList.add('spp-paired');
            panel.parentElement.insertBefore(bar, panel);

            // ★ 2.52.9:初始显示的那一格必须跟第一枚按钮一致 —— 第 2 页起没有「公告」格,
            //   若沿用 initAnnounceBoard 里写死的 notice.is-on,会先亮出一个空面板。
            activate(btns[0], false);

            // ---- 2.52.16:只有「全部/精华」的版块,把站点分类行包成胶囊搬进这一行 ----------------
            // 用户要求:「用 UI 将框选的两个元素包裹起来,并移动到箭头指向的状态栏」。
            // 2.52.8 只处理「有主题分类」的版块(收进面板当「分类」那一格);只有「全部/精华」的
            // 版块按当时的设计不做那一格,于是那条站点原皮灰条一直留在面板下面。现在照样收,
            // 但不进面板 —— 包一层 .spp-catrow 胶囊,直接插在状态栏按钮之后。
            // ★ 面包屑由 initBoardToolbar 稍后 append 进同一行(它带 margin-left:auto),所以
            //   我们的胶囊落在「按钮 ↔ 面包屑」之间,正是箭头指的位置。
            // ★ ul 原样搬、href 一个字符不改,点击仍由站点自己处理(与分类面板同一套做法)。
            if (!cats && catRow) {
                const catBox = document.createElement('div');
                catBox.className = 'spp-catrow';
                catBox.appendChild(catRow);
                bar.appendChild(catBox);

                // 搬空之后收起那一行(实测是 #ajaxtable 里分类行 tr > td > ul)。
                // 只在这一行真的没别的可见内容时才藏:写死 closest('.hthread') 会误伤,
                // 而不藏又会在面板下面留一条空灰条。
                const holder = catRow.parentElement;
                const rowEl = (holder && holder.closest('tr')) || holder;
                if (rowEl && !rowEl.querySelector('a, img, input, table, button') &&
                    !(rowEl.textContent || '').replace(/\s+/g, '').length) {
                    rowEl.style.setProperty('display', 'none', 'important');
                }
            }

            // ---- 2.52.11:页脚那条工具条(站点 DIV.t5)抬到「翻页行 ↔ 按钮行」之间 ----------------
            // 站点实测:它是 #main 的直接子元素、且是唯一含 table.f_one 的 .t5(另一个 div.t5.t_one 是图例条,页面上是 display:none)。
            // 原皮:rgba(250,252,255,.93) 浅玻璃 + .667px 白边 + radius 7px + 内联 margin:2px 0 —— 整页唯一没被换皮的白条。
            // 打标 .spp-t5bar(CSS 只认这个类、不写 .t5,避免误伤帖子页的 .t5.t2 作者面板与快速发帖框)。
            // 位置取「紧贴翻页行下方」:按钮行与公告面板是上圆下圆的「拼接玻璃」,工具条插在中间会把这层拼接拆开。
            const t5bar = Array.prototype.filter.call(
                document.querySelectorAll('#main > div.t5'),
                function (el) {
                    return !el.classList.contains('t_one') && !!el.querySelector('table.f_one');
                }
            )[0];
            if (t5bar && !t5bar.classList.contains('spp-t5bar')) {
                t5bar.classList.add('spp-t5bar');
                bar.parentNode.insertBefore(t5bar, bar);
                initDropdowns(t5bar);
            }


            // ---- 搬空之后收拾残局:空的 #cate_thread 与它头上那行「版块公告」图例 ----
            // ★ 判据只看「被搬的那个面板是不是真的空了」+「所有 .tr1 面板是不是都空了」:
            //   不用 querySelector('img') 那种间接判据 —— 别的面板里也可能有图,会误伤。
            const panels = cat ? Array.from(cat.querySelectorAll('.tr1')) : [];
            const emptied = !!pan && panels.length > 0 && panels.every(function (p) {
                return p.children.length === 0 && !p.textContent.replace(/\s+/g, '').length;
            });
            if (emptied) {
                cat.style.setProperty('display', 'none', 'important');
                const gg = cat.closest('.gonggao');
                const legend = gg ? gg.querySelector('ul.gongul') : null;
                if (legend) legend.style.setProperty('display', 'none', 'important');
            }
        });
    }

    // ==================== 2.50.6:公告板块(置顶/论坛公告 → 折叠玻璃卡) ====================
    // 用户要求:「参考无图片的卡片格式,制作一个板块,这个板块内都是截图内的链接入口,
    // 并且这一个板块属于公告板块,默认情况是折叠的,但是可以手动展开」。
    //
    // ★ 判据(只用站点自己打的标,不按文字猜):
    //     置顶帖   → 行内 img[title="置顶帖标志"]
    //     论坛公告 → 行内 a[href*="notice.php"]
    //   实测 thread_new.php?fid-9 的 #ajaxtable 共 14 行:1 分类 tab + 2 空行 + 1 表头 tr2
    //   + 10 置顶/公告 + 1 空行,**没有普通主题行** —— 那张表整体就是置顶公告表。
    //
    // ★ 搬走原行:用户确认「从原列表移除,只在公告板块里出现」(避免同一条出现两次)。
    //   搬空之后若表里再没有普通主题行,就把表头 tr2 与空行一并隐藏,
    //   免得留下一张空表;分类 tab 行保留(它是站点导航,不属于公告)。
    //
    // ★ 幂等:本模块只跑一次(下面用 dataset 打标),且 #ajaxtable 是首屏就有的静态表,
    //   不涉及懒加载新行,所以不需要 MutationObserver。
    function initAnnounceBoard() {
        if (!PAGE.board) return;

        const clean = x => String(x == null ? '' : x).replace(/\s+/g, ' ').trim();

        function rowIsPinned(tr) {
            if (tr.querySelector('img[title="置顶帖标志"]')) return true;
            if (tr.querySelector('a[href*="notice.php"]')) return true;
            // 论坛公告那行的图标是行首 td.tac 里的 anc.gif(老版面可能没有 notice 链接)
            if (tr.querySelector('td.tac img[src*="anc.gif"]')) return true;
            return false;
        }

        // 从一行里抽出条目需要的全部字段。抽不到就返回 null(宁可不搬,也不搬半个)。
        function readRow(tr) {
            const cells = Array.from(tr.querySelectorAll('td, th'));
            const h3 = tr.querySelector('h3');
            const a = h3 ? h3.querySelector('a[href]') : null;
            if (!a) return null;
            const href = a.getAttribute('href') || '';
            if (!href) return null;

            const isNotice = !!tr.querySelector('a[href*="notice.php"]');
            const iconEl = tr.querySelector('td.tac img[src*="thread/"]') || tr.querySelector('td img');
            const icon = iconEl ? (iconEl.getAttribute('src') || '').split('/').pop() : '';

            let author = '', postDate = '', reply = null, heat = null, lastDate = '', lastBy = '';
            // 末三格固定是 作者 / 回复-人气 / 最后发表(对着站点模板取的位,不按表头文字找)
            if (cells.length >= 5) {
                const cAuthor = clean(cells[cells.length - 3].textContent);
                const cStats = clean(cells[cells.length - 2].textContent);
                const cLast = clean(cells[cells.length - 1].textContent);
                const mA = cAuthor.match(/^(.*?)\s*(\d{4}-\d{2}-\d{2})$/);
                if (mA) { author = clean(mA[1]); postDate = mA[2]; }
                else author = cAuthor;
                const mS = cStats.match(/(\d+)\s*\/\s*(\d+)/);
                if (mS) { reply = parseInt(mS[1], 10); heat = parseInt(mS[2], 10); }
                const mL = cLast.match(/(\d{4}-\d{2}-\d{2}(?:\s+\d{2}:\d{2})?)/);
                if (mL) lastDate = mL[1];
                if (cLast.indexOf('by:') >= 0) lastBy = clean(cLast.split('by:')[1]);
            }
            return {
                isNotice: isNotice,
                icon: icon,
                title: clean(h3.textContent) || clean(a.textContent),
                href: href,
                author: author, postDate: postDate,
                reply: reply, heat: heat,
                lastDate: lastDate, lastBy: lastBy
            };
        }

        // 与脚本 getReplyColor() 同一套规则(这里直接复用,不另起一套配色)
        function glyphOf(it) {
            if (it.isNotice) return '告';
            if (/lock/.test(it.icon || '')) return '锁';
            return '热';
        }

        function buildItem(it) {
            const row = document.createElement('div');
            row.className = 'spp-ann-item';

            const dot = document.createElement('span');
            dot.className = 'spp-ann-dot';
            dot.setAttribute('aria-hidden', 'true');
            const g = glyphOf(it);
            dot.dataset.g = g;
            dot.textContent = g;

            const t = document.createElement('span');
            t.className = 'spp-ann-t';
            const a = document.createElement('a');
            a.href = it.href;
            a.target = '_blank';
            a.rel = 'noopener';
            a.textContent = it.title;
            t.appendChild(a);

            // 作者行:胶囊样式直接复用站点卡片那套类名(.spp-name / .spp-avatar / .spp-date)
            const byline = document.createElement('span');
            byline.className = 'spp-ann-byline';
            if (it.author) {
                const nm = document.createElement('span');
                nm.className = 'spp-name';
                const av = document.createElement('span');
                av.className = 'spp-avatar';
                av.textContent = (it.author || '?').charAt(0);
                const tx = document.createElement('span');
                tx.className = 'spp-name-txt';
                tx.textContent = it.author;
                nm.appendChild(av); nm.appendChild(tx);
                byline.appendChild(nm);
            }
            const addDate = (text) => {
                if (!text) return;
                const d = document.createElement('span');
                d.className = 'spp-date';
                const dt = document.createElement('span');
                dt.className = 'spp-date-txt';
                dt.textContent = text;
                d.appendChild(dt);
                byline.appendChild(d);
            };
            addDate(it.postDate);
            addDate(it.lastDate ? '最后 ' + it.lastDate : '');

            // 数据行:有回复/热度就给两枚按钮(与卡片数据行同款),没有就给一枚虚线小标签。
            // ★ 不硬塞 0:像「论坛公告: Contact / DMCA」本来就没有回复数,
            //   塞 0 会让它看起来像一条没人理的帖子。
            const meta = document.createElement('span');
            meta.className = 'spp-ann-meta';
            if (it.reply === null) {
                const nd = document.createElement('span');
                nd.className = 'spp-ann-nodata';
                nd.textContent = it.isNotice ? '论坛公告' : '置顶';
                meta.appendChild(nd);
            } else {
                const color = getReplyColor(it.reply);
                const wrap = document.createElement('span');
                wrap.className = 'spp-meta-btns';
                [['回复', it.reply, 'spp-tl-reply'], ['热度', it.heat, 'spp-tl-heat']].forEach(pair => {
                    const b = document.createElement('button');
                    b.type = 'button';
                    b.className = 'spp-metabtn';
                    b.style.setProperty('--spp-mbtn-fg', color);
                    b.appendChild(document.createTextNode(pair[0] + ' '));
                    const num = document.createElement('span');
                    num.className = pair[2];
                    num.textContent = String(pair[1]);
                    b.appendChild(num);
                    wrap.appendChild(b);
                });
                meta.appendChild(wrap);
            }

            row.appendChild(dot);
            row.appendChild(t);
            row.appendChild(byline);
            row.appendChild(meta);
            return row;
        }

        function run() {
            if (document.getElementById('spp-announce')) return 0;   // 幂等
            const table = document.querySelector('#main #ajaxtable') || document.querySelector('#ajaxtable');
            if (!table) return 0;

            const rows = Array.from(table.querySelectorAll('tr'));
            const pinned = [];
            rows.forEach(tr => {
                if (rowIsPinned(tr)) {
                    const it = readRow(tr);
                    if (it) pinned.push({ row: tr, item: it });
                }
            });
            // ★ 2.52.9:站点只把置顶/公告行放在第 1 页,所以「没有置顶就整块不出现」会让第 2 页起
            //   连按钮行/版头图/分类一起消失(用户实测「翻页后就失效」)。现在只要页面上还有别的可收
            //   内容(版块公告 #cate_thread / 分类行 ul.threadlist)就把面板建出来;公告那一格由
            //   initBoardNotice 按条数决定排不排(见那里的 noticeNum !== '0')。
            if (!pinned.length && !document.querySelector('#cate_thread') && !document.querySelector('ul.threadlist')) return 0;

            // ---- 组装板块 ----
            const box = document.createElement('section');
            box.id = 'spp-announce';
            box.dataset.open = '0';                 // 默认折叠

            const head = document.createElement('button');
            head.type = 'button';
            head.className = 'spp-ann-head';
            head.setAttribute('aria-expanded', 'false');

            const title = document.createElement('span');
            title.className = 'spp-ann-title';
            title.textContent = '公告';

            const count = document.createElement('span');
            count.className = 'spp-ann-count';
            count.textContent = String(pinned.length);

            const hint = document.createElement('span');
            hint.className = 'spp-ann-hint';
            hint.textContent = '置顶与论坛公告';

            const chev = document.createElement('span');
            chev.className = 'spp-ann-chev';
            chev.setAttribute('aria-hidden', 'true');

            head.appendChild(title); head.appendChild(count);
            head.appendChild(hint);  head.appendChild(chev);

            const body = document.createElement('div');
            body.className = 'spp-ann-body';
            const inner = document.createElement('div');
            inner.className = 'spp-ann-inner';
            const list = document.createElement('div');
            // ★ 2.52.6:本条从「唯一内容」降级为「第一个模板面板」——公告列表是 notice 面板,
            //   另外三个(rules / subs / cover)留空壳,由 initBoardNotice 填内容。
            list.className = 'spp-ann-list spp-pane is-on';
            list.dataset.pane = 'notice';
            pinned.forEach(p => list.appendChild(buildItem(p.item)));
            inner.appendChild(list);
            ['rules', 'subs', 'cover', 'cats'].forEach(function (k) {
                const pane = document.createElement('div');
                pane.className = 'spp-pane';
                pane.dataset.pane = k;
                inner.appendChild(pane);
            });
            body.appendChild(inner);

            box.appendChild(head);
            box.appendChild(body);

            // ---- 插到置顶表的位置上(表 -> table 的祖先 DIV.t.z 之前)----
            // ★ 插入点取「表自己的外层 DIV.t.z」而不是 #main 的第一个子元素:
            //   实测 #main 里前面还有 script / style / 分类菜单,插到最前面会被它们挤开。
            const wrapper = table.parentElement || table;
            if (wrapper.parentElement) wrapper.parentElement.insertBefore(box, wrapper);
            else table.parentNode.insertBefore(box, table);

            // ---- 把原行搬走 ----
            pinned.forEach(p => p.row.remove());

            // ---- 表里若再无普通主题行,连表头与空行一起收起,避免留一张空表 ----
            const leftover = Array.from(table.querySelectorAll('tr')).filter(tr =>
                tr.querySelector('a[id^="a_ajax_"], h3 a[href*="read.php"]'));
            if (!leftover.length) {
                Array.from(table.querySelectorAll('tr')).forEach(tr => {
                    // 分类 tab 行留着(站点导航),其余收起
                    if (tr.querySelector('#menu_special, #menu_cate, .hthread, .threadlist')) return;
                    tr.style.setProperty('display', 'none', 'important');
                });
            }

            // ---- 折叠交互 ----
            head.addEventListener('click', () => {
                const open = box.dataset.open === '1';
                box.dataset.open = open ? '0' : '1';
                head.setAttribute('aria-expanded', open ? 'false' : 'true');
            });

            return pinned.length;
        }

        // #ajaxtable 在首屏就有,但分类 tab 里的「全部/精华」链接是站点脚本后填的:
        // 分类 tab 归站点管、本段只读置顶行,所以 onDomReady 后跑一次即可,不必等懒加载。
        onDomReady(() => {
            run();
            // 兜底:分类菜单是异步渲染的,它可能把 #ajaxtable 重建一次,届时补跑。
            // run() 自带 id 幂等,重复调用安全。
            setTimeout(run, 800);
        });
    }

    // ==================== 2.51.0:版块页顶部改写(屏蔽 logo 横幅 + 热门版块玻璃卡) ====================
    // 用户要求见 SPP-HOTBOARD 段的注释。这里只讲实现上必须注意的三点:
    //
    // ★ 为什么屏蔽横幅要用 JS 建 <style> 而不是静态写进大 CSS:
    //   本脚本的版块页大 CSS 走 GM_addStyle 一次性注入,任何页面都会带上。
    //   而 #header > table 这条只该在有横幅的页面上生效;写进大盘子里虽然也不会误伤,
    //   但会与首页那条 CSS_HDR 重复。这里按模块自己的开关管理,与其他「屏蔽类」模块同法
    //   (见 initHideTopTool / initHideRule 的实现)。
    //
    // ★ 为什么热门版块要【现读 DOM】而不是写死名单:
    //   站点会调热门版块。写死一份名单,站点改了这里就过期。
    //   数据源是 #fav-fid > ul#shortcutforum > li > a(站点自己渲染的)。
    //
    // ★ 为什么卡片插在 #infobox 之后而不是替换它:
    //   用户明确「用户信息区保持现状不改」。所以是【新增一块】在它下面,
    //   不动 #infobox 的任何结构。
    function initBoardTop() {
        // 2.52.33:帖子页(read.php)一并处理 —— 屏蔽 logo 横幅 + 用户卡深色卡 + 热门版块玻璃卡
        //   ★ 实测 read.php 的结构与版块页一致:#infobox 里正好 2 个 td(#user_info / #fav-fid 宿主)、
        //     #showface 与 #shortcutforum 都在 ⇒ polishUserInfo 的两道结构守卫与 buildHot 都能过。
        // 2.52.52:搜索页(search.php)也一并处理 —— 用户框的蓝/红/黄三处正是这一块:
        //   蓝框 #header > table(logo 横幅)+ #header 浅灰底 → 屏蔽
        //   红框 #mainNav + #infobox 用户信息条 → 主页那套深色卡(applyCard / polishUserInfo / buildHot)
        //   黄框 #main > .bdbA 里那条面包屑 → 搬进卡片右格(见 moveCrumb / SEARCH_CSS)
        //   ★ 实测 search.php 与版块页同形:#infobox 正好 2 个 td、#showface 在、#shortcutforum 6 条、
        //     #main > .bdbA 里只有 #breadcrumbs 一个孩子(搬走即空,收起不留 27px 灰条)。
        // 2.52.79:社区论坛任务页(plugin.php?H_name-tasks*)也走同一套 ——
        //   ★ 结构实测与该页族【同形】,所以这里只是「搬过来」,不另画一套:
        //     #infobox 里正好 2 个 td;#user_info 内有 #showface / a.link5×2(我的主题·我的回复)/
        //     #td_u(更多)/ #honor(签名)/ #sethonor(编辑)/ #sf / a[href="u.php"] img(头像);
        //     右格 td 内有 #fav-fid > ul#shortcutforum(6 条热门版块)。
        //   ⇒ polishUserInfo 的两道结构守卫与 buildHot 全部能过,用户要的正是主页那张深色卡。
        // 2.52.90:发帖页(post.php)也一并处理 —— 用户批注的红/黄两处正是这一块:
        //   红框 #header > table(logo 横幅)→ 屏蔽
        //   黄框 #infobox 用户信息条 + #main > .bdbA 面包屑 →「使用之前的模版」
        //        (= 帖子页/版块页那张深色卡 + 磨砂玻璃面包屑板块,样式见 POST_CSS)
        //   ★ 实测 post.php?fid-9 与帖子页【同形】:#infobox 里正好 2 个 td(#user_info / #fav-fid 宿主)、
        //     #showface 与 #shortcutforum(6 条热门版块)都在、#main > .bdbA 里只有 #breadcrumbs 一个孩子
        //     ⇒ polishUserInfo 的两道结构守卫与 buildHot 都能过,用户点名要的就是这套现成模版。
        if (!PAGE.board && !PAGE.read && !PAGE.search && !PAGE.post && !PAGE.task) return;

        const HIDE_KEY = 'spp_board_hide_banner';
        const CARD_KEY = 'spp_board_upcard';
        const HOT_KEY  = 'spp_board_hotboards';
        const STYLE_ID = 'spp-boardtop-style';

        // ---------- 两格等高到主页尺寸 ----------
        // ★★ 为什么光靠 CSS_UPANEL 不够:它给 #user_info 写的是 min-height:189px,
        //    而 min-height/min-width 对 display:table-cell **不生效**(CSS 规范如此),
        //    实测左格只有 126px。主页那个 189px 其实是【右格的 6 个统计格】把左格撑上去的。
        //    表格单元格要用 height —— table-cell 的 height 按最小高度语义应用。
        //    189 不是新魔数:就是 CSS_UPANEL 自己写着的那个值,也等于线上主页实测高度。
        //    ★ 这段只拼进版块页自己的 style,不动主页。
        const EQ_CSS = '#infobox.spp-up-on > table > tbody > tr > td { height:189px !important; }';

        // ---------- 屏蔽 logo 横幅 ----------
        // 与首页 CSS_HDR 同一条规则,只是这里按自己的开关管理。
        function applyBanner() {
            const on = SPP_CONFIG.bool(HIDE_KEY, true);
            let el = document.getElementById(STYLE_ID + '-banner');
            if (on) {
                if (!el) {
                    el = document.createElement('style');
                    el.id = STYLE_ID + '-banner';
                    // ★ 只能精确命中 #header 的第一个 table(td.banner 内的季节 logo + 广告)。
                    //   整个 #header 藏掉会把 #mainNav 导航条与 #infobox 用户条一起抹掉。
                    el.textContent = '#header > table { display:none !important; }';
                    appendToHeadSafe(el);
                }
            } else if (el) {
                el.remove();
            }
        }

        // ---------- 左格用户卡 ----------
        // 注入的就是与首页同一份 CSS_UPANEL(含用户卡与主页统计格那一套)。
        function applyCard() {
            const on = SPP_CONFIG.bool(CARD_KEY, true);
            let el = document.getElementById(STYLE_ID);
            if (!on) { if (el) el.remove(); return; }
            if (!el) {
                el = document.createElement('style');
                el.id = STYLE_ID;
                appendToHeadSafe(el);
            }
            el.textContent = CSS_UPANEL + EQ_CSS;
        }

        // ---------- 右格:热门版块(主页统计格那套卡片,样式来自 CSS_UPANEL) ----------
        function buildHot(td) {
            if (!SPP_CONFIG.bool(HOT_KEY, true)) return 0;
            if (td.querySelector('.hb-wrap')) return 0;                       // 幂等
            const ul = document.getElementById('shortcutforum');
            if (!ul) return 0;                                                 // 没有就不建(不造空壳)
            // ★★ fid 必须在【读数据的这一刻】就解析好随对象带走:
            //   卡片里的 <a> 是新建元素,事后回 DOM 找 closest('li') 恒为 null(2.51.0 踩过)。
            const links = Array.from(ul.querySelectorAll('li > a'))
                .map(a => {
                    const li = a.closest('li');
                    const fromId = li && li.id ? (li.id.match(/scf_?(\d+)/) || [])[1] : '';
                    const fromHref = (a.getAttribute('href') || '').match(/fid[-=](\d+)/);
                    return {
                        name: (a.textContent || '').trim(),
                        href: a.getAttribute('href') || '',
                        fid: fromId || (fromHref ? fromHref[1] : '')
                    };
                })
                .filter(x => x.name && x.href);
            if (!links.length) return 0;

            // 当前所在版面:站点 li 的 scf_<fid> 与地址栏 fid 比,不按名称比
            const curFid = (location.href.match(/fid[-=](\d+)/) || [])[1] || '';

            const wrap = document.createElement('div');
            wrap.className = 'hb-wrap';

            const hd = document.createElement('div');
            hd.className = 'hb-hd';
            const t = document.createElement('span');
            t.className = 'hb-hd__t'; t.textContent = '热门版块';
            const n = document.createElement('span');
            n.className = 'hb-hd__n'; n.textContent = String(links.length);
            const hint = document.createElement('span');
            hint.className = 'hb-hd__hint'; hint.textContent = '常用入口';
            hd.appendChild(t); hd.appendChild(n); hd.appendChild(hint);

            // ★ 复用主页统计格那套:外层 .spp-hm-stats 负责 3 列网格,卡片 .spp-hm-stat
            const grid = document.createElement('div');
            grid.className = 'spp-hm-stats hb-grid';
            links.forEach(item => {
                const a = document.createElement('a');
                a.className = 'spp-hm-stat' + (curFid && item.fid === curFid ? ' is-cur' : '');
                a.href = item.href;
                const k = document.createElement('span');
                k.className = 'k';                    // 空标签:绿点由 .k::before 生成
                const v = document.createElement('span');
                v.className = 'v'; v.textContent = item.name;
                a.appendChild(k); a.appendChild(v);
                grid.appendChild(a);
            });

            wrap.appendChild(hd); wrap.appendChild(grid);
            // ★ 只【追加】,不动站点原有的 #fav-fid 子树:
            //   站点 shortCut() 还要靠 #shortcutforum 增删 <li>,删了会破坏站点行为。
            //   重复入口由 CSS 把 #fav-fid 隐藏掉(见 SPP-HOTBOARD 段末)。
            td.appendChild(wrap);
            return links.length;
        }

        function buildTop() {
            if (!SPP_CONFIG.bool(CARD_KEY, true)) return 0;
            // 未登录时 #user_info 是登录表单,没有这些节点 —— 不套卡,按站点原样留着
            const ui = document.getElementById('user_info');
            if (!ui || !ui.querySelector('#showface')) return 0;
            // 先把 #infobox 那一行做成主页同款深色卡(左格用户卡 + 右格宿主 td)
            polishUserInfo(true);
            const info = document.getElementById('infobox');
            if (!info) return 0;
            const tbl = info.querySelector('table');
            if (!tbl) return 0;
            const td = [].slice.call(tbl.querySelectorAll('tr > td'))
                .filter(function (x) { return x.id !== 'user_info'; })[0];
            if (!td) return 0;
            return buildHot(td);
        }

        // ---------- 2.52.34:帖子页(read.php)顶部收尾 / 2.52.60:面包屑独立板块 ----------
        // ① 站点那条「热门版块: …」(#fav-fid)hide 掉 —— 它 height 被压成 0 但内容 inline 溢出照样可见;
        // ② 2.52.34~2.52.59 把面包屑搬进右格、2.52.59 又解锁 3 行折行 —— 实测长标题一折行右格就被拉高,
        //    而 table 同一行两格等高、左卡内容只有 189px,左右底边错位、两列比例也被顶歪
        //    (用户截图「不对称大小分配」)。2.52.60 按用户定案整条撤出右格:面包屑留在 #main 顶部
        //    自己的 .bdbA 行里原地改扮独立板块(moveCrumb 只负责打 .spp-crumb-panel 标,不再搬格),
        //    折行只在板块内部发生,顶栏恢复「正常大小分配」。
        // ★ 样式必须走本模块自己的 <style>:实测 initThreadListConvert() 那个大盘子在帖子页不注入。
        const READ_CSS = `
html.spp-read-on #header { background:none !important; }
html.spp-read-on #main > .bdbA { background:none !important; border:0 !important; }
/* ===== 2.52.60:面包屑板块 —— 与翻页条同一份磨砂黑玻璃配方(2.52.44),原地改扮 .bdbA 行 =====
   ★ 不新建节点:#breadcrumbs 本来就住在 #main 顶部的 .bdbA 行里(顶栏正下方、翻页条正上方),
     装面包屑的板块就该站在这个位置 —— 把这一行改扮即可,站点 DOM 一个不动。
   ★ 板块高度随内容走(这就是「预留三行」的兑现方式):单行 ≈48px,标题折满 3 行 ≈105px,
     见段末的折行预算;底部的「浏览记录」条(也是 #main > .bdbA)不吃这套皮,见下面的还原规则。 */
html.spp-read-on #main > .bdbA.spp-crumb-panel {
    box-sizing:border-box !important;
    margin:10px 0 8px !important; padding:6px 12px !important;
    border-radius:12px !important; border:1px solid rgba(255,255,255,.10) !important;
    background-color:rgb(0 0 0 / .86) !important;
    background-image:linear-gradient(152deg, rgba(255,255,255,.03) 0%, rgba(0,0,0,0) 55%) !important;
    -webkit-backdrop-filter:blur(18px) saturate(140%) !important;
    backdrop-filter:blur(18px) saturate(140%) !important;
    box-shadow:0 10px 26px rgba(0,0,0,.34), inset 0 1px 0 rgba(255,255,255,.05) !important;
}
/* #breadcrumbs 退成板块里的透明内容行(2.52.34~2.52.59 时代它自己背着 #0e1116 深条皮) */
html.spp-read-on #breadcrumbs {
    display:flex !important; align-items:center !important; gap:2px !important;
    box-sizing:border-box !important; min-height:34px !important; height:auto !important;
    margin:0 !important; padding:0 !important;
    background:none !important; background-image:none !important;
    border:0 !important; border-radius:0 !important; box-shadow:none !important;
    font-size:13px !important; color:#cfdcee !important; overflow:hidden !important;
}
/* 底部「浏览记录」条不是板块:补回 2.52.34 的深条皮,观感与 2.52.59 一字不差
   (这条必须排在上面透明规则之后,靠更高特异性赢回去) */
html.spp-read-on #main > .bdbA:not(.spp-crumb-panel) > #breadcrumbs {
    margin:6px 0 0 !important; padding:0 12px !important;
    background-color:#0e1116 !important; background-image:none !important;
    border:0 !important; border-radius:12px !important;
    box-shadow:inset 0 1px 0 rgba(255,255,255,.04) !important;
}
/* ★ 2.53.49(用户:「底部的位置栏相比顶部的位置栏,缺少了『本贴』的位置」):
   initCrumbTitle() 把顶部那节 <strong><a>标题</a></strong> 克隆到这一行里了,这里只管它的样子 ——
   与顶部当前位同色(主色绿)+ 加粗,并在前面补一枚 › 分隔符。
   ★ 为什么要单独写分隔符:顶部那几枚靠 .crumbs-item + .crumbs-item::before 画 ›,
     而这枚是克隆来的裸 <strong>、不带 .crumbs-item,蹭不上那条规则。 */
html.spp-read-on #main > .bdbA:not(.spp-crumb-panel) > #breadcrumbs > strong.spp-crumb-cur {
    font-weight:700 !important;
}
html.spp-read-on #main > .bdbA:not(.spp-crumb-panel) > #breadcrumbs > strong.spp-crumb-cur::before {
    content:'›' !important; margin:0 .5em 0 .15em !important; color:#5f7285 !important;
    font-weight:400 !important;
}
html.spp-read-on #main > .bdbA:not(.spp-crumb-panel) > #breadcrumbs > strong.spp-crumb-cur > a {
    color:#3ef58c !important; -webkit-text-fill-color:#3ef58c !important;
    text-decoration:none !important; white-space:nowrap !important;
}
html.spp-read-on #breadcrumbs .crumbs-item {
    display:inline-flex !important; align-items:center !important; padding:0 !important;
    background:none !important; background-image:none !important;
    font-size:13px !important; line-height:1.2 !important;
    min-width:0 !important; overflow:hidden !important;
    white-space:nowrap !important; text-overflow:ellipsis !important;
}
html.spp-read-on #breadcrumbs .crumbs-item i { display:none !important; }
html.spp-read-on #breadcrumbs .crumbs-item > img,
/* ★ 2.52.98:这枚图的尺寸/滤镜只对「还没包进按钮组件」的原图生效 —— 包过之后
   热区尺寸由 .spp-hbtn-wrap > img 那条说了算(一处尺寸一处声明,见 经验 1.42)。
   ★ 为什么要 :not:本站写法里「#breadcrumbs #td_cate」带两个 id(特化度 2,1,1),
   比本段任何 html.spp-read-on #breadcrumbs .x .y > img(1,n,n)都高 ——
   实测没加 :not 时热区仍被钉在 15×15,面板于是贴着旧盒子下缘弹、与 26px 按钮错位 12px。 */
html.spp-read-on #breadcrumbs #td_cate:not(.spp-hbtn-hit) {
    width:15px !important; height:15px !important; margin:0 9px 0 2px !important;
    filter:invert(1) brightness(1.5) opacity(.66) !important;
}
html.spp-read-on #breadcrumbs .crumbs-item.gray3 {
    color:#8b9ab1 !important; -webkit-text-fill-color:#8b9ab1 !important; text-decoration:none !important;
}
html.spp-read-on #breadcrumbs .crumbs-item.gray3:hover {
    color:#e2e8f0 !important; -webkit-text-fill-color:#e2e8f0 !important;
}
html.spp-read-on #breadcrumbs .crumbs-item + .crumbs-item::before {
    content:"›" !important; display:inline-block !important; flex:none !important;
    margin:0 8px !important; padding:0 !important;
    color:rgba(32,201,151,.72) !important; -webkit-text-fill-color:rgba(32,201,151,.72) !important;
    font-size:16px !important; font-weight:400 !important; line-height:1 !important;
    vertical-align:middle !important; transform:none !important;
}
html.spp-read-on #breadcrumbs .crumbs-item.current {
    flex:0 1 auto !important; min-width:0 !important; overflow:hidden !important;
    white-space:nowrap !important; text-overflow:ellipsis !important;
    color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important; font-weight:600 !important;
}
html.spp-read-on #breadcrumbs .crumbs-item.current > strong,
html.spp-read-on #breadcrumbs .crumbs-item.current > strong > a {
    display:inline-block !important; min-width:0 !important; max-width:100% !important;
    overflow:hidden !important; white-space:nowrap !important; text-overflow:ellipsis !important;
    color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
    font-weight:600 !important; text-decoration:none !important;
}
/* 2.52.60:右格里那两条 .spp-crumb-line 规则随「面包屑撤出右格」一并撤销
   (面包屑不再进右格;搜索页仍搬格,同款规则在 SEARCH_CSS 里自己的作用域下) */
html.spp-read-on #infobox.spp-up-on td.spp-up-host-td > #fav-fid { display:none !important; }
/* ---- 2.52.35 ① 站点那条「| 浏览器收藏 | 打印」(#main > div.h2,底色 rgb(221,221,221)):屏蔽 ---- */
html.spp-read-on .spp-hide-row { display:none !important; }
/* ---- 2.52.35 ② 翻页行里的 li(站点给 li.f_one 画白底,胶囊其实在 a 上)---- */
html.spp-read-on #main .spp-pgbar .pages ul li,
html.spp-read-on #main .spp-pgbar .pages ul li.f_one { background:none !important; }
/* ---- 2.52.35 ③ 右格卡标题(热门版块 N 常用入口):帖子页缺这段样式 ⇒ 深底黑字看不见 ---- */
html.spp-read-on #infobox.spp-up-on .hb-hd {
    display:flex !important; align-items:center !important; gap:9px !important; margin:0 0 11px !important;
}
html.spp-read-on #infobox.spp-up-on .hb-hd__t {
    font-size:13.5px !important; font-weight:700 !important; letter-spacing:.3px !important;
    color:#eef3fb !important; -webkit-text-fill-color:#eef3fb !important;
}
html.spp-read-on #infobox.spp-up-on .hb-hd__n {
    font-size:11px !important; font-weight:700 !important; line-height:1.6 !important;
    color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
    padding:0 8px !important; border-radius:999px !important;
    background:rgba(32,201,151,.16) !important; border:1px solid rgba(32,201,151,.45) !important;
}
html.spp-read-on #infobox.spp-up-on .hb-hd__hint {
    font-size:11.5px !important; margin-left:auto !important;
    color:#8798a8 !important; -webkit-text-fill-color:#8798a8 !important;
}
/* ---- 2.52.35 ④ 站点白底弹出面板(#menu_cate = home 图标的 onmouseover 面板)→ 磨砂黑玻璃 ---- */
html.spp-read-on div.menu {
    box-sizing:border-box !important; padding:6px !important;
    background-color:#0e1116 !important;
    background-image:linear-gradient(152deg, rgba(255,255,255,.06) 0%, rgba(0,0,0,0) 60%) !important;
    border:1px solid rgba(255,255,255,.10) !important; border-radius:12px !important;
    box-shadow:0 18px 40px rgba(0,0,0,.55) !important;
}
html.spp-read-on div.menu > div { max-height:62vh !important; overflow-y:auto !important; }
html.spp-read-on div.menu .ul3 { padding:0 !important; }
html.spp-read-on div.menu .ul3 > h2 { margin:7px 0 2px !important; }
html.spp-read-on div.menu a {
    display:block !important; padding:5px 12px !important; border-radius:8px !important;
    color:#cfdcee !important; -webkit-text-fill-color:#cfdcee !important;
    font-size:12.5px !important; text-decoration:none !important;
}
html.spp-read-on div.menu .ul3 > h2 > a {
    padding:5px 12px !important; color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
    font-weight:700 !important;
}
html.spp-read-on div.menu a:hover {
    background-color:rgba(32,201,151,.14) !important;
    color:#fff !important; -webkit-text-fill-color:#fff !important;
}
/* ---- 2.52.35 ⑤ 面包屑尾部那枚 RSS(<img class="threadrss" src="…kong.png"> 空图)→ 绿色胶囊 ---- */
html.spp-read-on #breadcrumbs .crumbs-item.current > a[href^="rss.php"] {
    display:inline-flex !important; align-items:center !important; height:20px !important;
    margin-left:8px !important; padding:0 8px !important; border-radius:999px !important;
    background-color:rgba(32,201,151,.14) !important; border:1px solid rgba(32,201,151,.45) !important;
    color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
    font-size:11px !important; font-weight:700 !important; letter-spacing:.4px !important;
    text-decoration:none !important; vertical-align:middle !important;
}
html.spp-read-on #breadcrumbs .crumbs-item.current > a[href^="rss.php"] img { display:none !important; }
html.spp-read-on #breadcrumbs .crumbs-item.current > a[href^="rss.php"]::after { content:"RSS" !important; }
html.spp-read-on #breadcrumbs .crumbs-item.current > a[href^="rss.php"]:hover {
    background-color:rgba(32,201,151,.26) !important; border-color:rgba(32,201,151,.75) !important;
    color:#fff !important; -webkit-text-fill-color:#fff !important;
}
/* ---- 2.52.36 ② 回帖(站点 83×30 位图按钮)→ 深色玻璃胶囊 + ✎ 回帖 ---- */
html.spp-read-on #main .spp-pgbar a[href*="action-reply"] {
    position:relative !important; display:inline-flex !important; align-items:center !important; gap:6px !important;
    box-sizing:border-box !important; height:34px !important; padding:0 14px !important; margin:0 !important;
    border-radius:8px !important; border:1px solid rgba(255,255,255,.16) !important;
    background-color:rgba(255,255,255,.055) !important; background-image:none !important;
    color:#dbe4f2 !important; -webkit-text-fill-color:#dbe4f2 !important;
    font-size:12.5px !important; font-weight:600 !important; line-height:1 !important; text-decoration:none !important;
    transition:background-color .22s ease, border-color .22s ease, color .22s ease !important;
}
html.spp-read-on #main .spp-pgbar a[href*="action-reply"] > img { display:none !important; }
html.spp-read-on #main .spp-pgbar a[href*="action-reply"]::before {
    content:"✎" !important; font-size:12px !important; line-height:1 !important;
    color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
}
html.spp-read-on #main .spp-pgbar a[href*="action-reply"]::after { content:"回帖" !important; }
/* 2.52.37:同上 —— 回帖 / 发新帖 的底与描边改用 box-shadow(background/border 被站点压住)*/
html.spp-read-on #main .spp-pgbar a[href*="action-reply"] {
    box-shadow:inset 0 0 0 1px rgba(255,255,255,.16), inset 0 0 0 100px rgba(255,255,255,.055) !important;
}
html.spp-read-on #main .spp-pgbar a[href*="action-reply"]:hover {
    box-shadow:inset 0 0 0 1px rgba(32,201,151,.55), inset 0 0 0 100px rgba(32,201,151,.16) !important;
}
html.spp-read-on #main .spp-pgbar .spp-postbtn {
    box-shadow:inset 0 0 0 1px rgba(32,201,151,.55), inset 0 0 0 100px rgba(32,201,151,.14),
               inset 0 1px 0 rgba(255,255,255,.06) !important;
}
html.spp-read-on #main .spp-pgbar .spp-postbtn:hover {
    box-shadow:inset 0 0 0 1px rgba(32,201,151,.85), inset 0 0 0 100px rgba(32,201,151,.22),
               inset 0 1px 0 rgba(255,255,255,.10) !important;
}
/* ---- 2.52.38:帖子页的「发新帖」是站点位图(.spp-postbtn 只存在于版块页)—— 按 href 认,
        藏图 + 自绘 + 号 + 文字;注意排除掉回帖那条(它的 href 也是 post.php 开头) ---- */
html.spp-read-on #main .spp-pgbar a[href^="post.php"]:not([href*="action-reply"]) {
    position:relative !important; display:inline-flex !important; align-items:center !important; gap:7px !important;
    box-sizing:border-box !important; height:34px !important; padding:0 16px !important; margin:0 0 0 auto !important;
    border:0 !important; background:none !important; border-radius:8px !important;
    color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
    font-size:12.5px !important; font-weight:600 !important; line-height:1 !important; text-decoration:none !important;
    box-shadow:inset 0 0 0 1px rgba(32,201,151,.55), inset 0 0 0 100px rgba(32,201,151,.14) !important;
    transition:box-shadow .22s ease, color .22s ease !important;
}
html.spp-read-on #main .spp-pgbar a[href^="post.php"]:not([href*="action-reply"]) > img { display:none !important; }
html.spp-read-on #main .spp-pgbar a[href^="post.php"]:not([href*="action-reply"])::before {
    content:"" !important; position:relative !important; flex:none !important;
    width:11px !important; height:11px !important;
    background-image:linear-gradient(#8ef0b8,#8ef0b8), linear-gradient(#8ef0b8,#8ef0b8) !important;
    background-size:11px 2px, 2px 11px !important;
    background-position:center center, center center !important; background-repeat:no-repeat !important;
}
html.spp-read-on #main .spp-pgbar a[href^="post.php"]:not([href*="action-reply"])::after { content:"发新帖" !important; }
html.spp-read-on #main .spp-pgbar a[href^="post.php"]:not([href*="action-reply"]):hover {
    color:#fff !important; -webkit-text-fill-color:#fff !important;
    box-shadow:inset 0 0 0 1px rgba(32,201,151,.85), inset 0 0 0 100px rgba(32,201,151,.22) !important;
}
html.spp-read-on #main .spp-pgbar a[href*="action-reply"]:hover {
    background-color:rgba(32,201,151,.14) !important; border-color:rgba(32,201,151,.55) !important;
    color:#fff !important; -webkit-text-fill-color:#fff !important;
}
/* ---- 2.52.36 ③ 发新帖(已有 uiverse 件)→ 主色按钮,和「回帖」分主次 ---- */
html.spp-read-on #main .spp-pgbar .spp-postbtn {
    border-color:rgba(32,201,151,.55) !important;
    background-color:rgba(32,201,151,.14) !important;
    color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
}
html.spp-read-on #main .spp-pgbar .spp-postbtn .spp-post-ico::before,
html.spp-read-on #main .spp-pgbar .spp-postbtn .spp-post-ico::after { background-color:#8ef0b8 !important; }
html.spp-read-on #main .spp-pgbar .spp-postbtn:hover {
    border-color:rgba(32,201,151,.85) !important; background-color:rgba(32,201,151,.22) !important;
    color:#fff !important; -webkit-text-fill-color:#fff !important;
}
/* ===== 2.52.45:帖子区恢复白底 —— 2.52.42/2.52.44 那套磨砂黑玻璃 + 深底浅字 + 绿链接整体撤掉 =====
   ★ 用户:「帖子的背景修改为白色,不要黑玻璃背景。」玻璃只留在「上一/下一主题 + 回帖 + 发新帖」那条 .spp-pgbar 上。
   ★ 站点自己的帖子底是 rgba(250,252,255,.93),这里按用户要求显式压成纯白;帖内浅底(作者格/引用框)也不再动。 */
html.spp-read-on #main > form > .t5.t2 { background-color:#fff !important; background-image:none !important; }
/* ---- 2.52.36 顺手:右簇里站点残余的空占位(:empty 才隐藏,有内容的绝不动)---- */
html.spp-read-on #main .spp-pgbar > span:empty,
html.spp-read-on #main .spp-pgbar > div:empty { display:none !important; }
/* ★★ 2.54.9(用户:「下方的 UI 出现了折叠重叠的情况,请修改一下,往下移一移」):
   站点给帖子页底下那条翻页行写了内联 style="margin-top:-.4em"(12px 字号 ⇒ -4.8px),
   把它硬塞进上一张帖子卡片的底缘里 —— 卡片最后一道下边框正好被这条玻璃压住,
   看上去就是「折叠重叠」。这里用 !important 顶掉那个负外边距,再留 8px 空隙:
   翻页行整个落到卡片下面,卡片底缘那条线完整露出来。
   (!important 的样式表声明能压过非 important 的内联声明 —— 正是这里要的。) */
html.spp-read-on #main .t3.spp-pgbar { margin-top: 8px !important; }
/* ★★ 2.55.0(用户截图黄框:「上方框选区域的留白是不需要的」):发帖那一行是三格 ——
   选项 / 编辑器 / 表情栏,行高由**最高那格**(右侧表情栏,900+ 高)决定;
   而站点把编辑器那格写成 vertical-align:middle ⇒ 整段编辑器被垂直居中,
   标题上面于是空出 177px(实测:单元格顶 7553、标题行 7730)。
   三格一律改成 top —— 编辑器贴到行顶,留白消失;顺带表情栏也抬到行顶,
   两枚 UI 那行与标题行大致齐平(用户上一条箭头批注「水平高度应大致相同」)。 */
html.spp-read-on #main td.f_one:has(.spp-editor-toolbar),
html.spp-read-on #main td.f_one:has(textarea[name="atc_content"]),
html.spp-read-on #main td.f_one:has(#smiliebox) { vertical-align: top !important; }
/* ===== 2.52.46:底部面包屑条(#main > .bdbA)右簇那两个「浏览记录」按钮 =====
   ★ 站点给的是 A.abtn + 一枚 menu-down.gif 位图(原生白底黑字、19px 高、两枚几乎贴在一起),
     还有 home 图标头顶的内联 margin-top:6px。这里统一成与翻页条同套的深色玻璃胶囊,
     并拉开彼此间距(用户:「美化一下框选位置的 UI、UI 之间的间距」)。 */
html.spp-read-on #main > .bdbA #breadcrumbs .fr {
    display:flex !important; align-items:center !important; gap:9px !important;
    margin-left:auto !important;   /* ← 2.52.47:flex 化之后站点那句 float:right 失效,用 auto 贴回最右 */
    height:auto !important; line-height:1 !important; padding:0 !important;
}
html.spp-read-on #main > .bdbA #breadcrumbs .fr .abtn {
    display:inline-flex !important; align-items:center !important; gap:6px !important;
    box-sizing:border-box !important; height:26px !important; padding:0 10px !important; margin:0 !important;
    /* ★★ 2.53.47(用户:「底部的『帖子浏览记录』『版块浏览记录』的字体需要参考顶部按钮的
       『社区论坛任务』的字体类型」):
       ★ 先纠正一个位置误判 —— 这两枚**不在** .spp-t5bar 里(实测它们住在
         #main > .bdbA > #breadcrumbs > .fr,即面包屑那一行的右侧;页面上 .spp-t5bar 的数量是 0)。
         所以给 .spp-t5bar 那条规则加字体是白费功夫,真正生效的是本规则。
       ★ 二是找到真凶:站点有一条全局 .abtn{font-family:simsun}(内联样式表里实测读到原文),
         宋体 + 无抗锯齿 ⇒ 中文又细又皱;而本规则原先只写了 font-weight:500,没碰字体族,
         于是宋体一路赢下来。参照物「社区论坛任务」实测是
             Arial, Helvetica, sans-serif / 12px / weight 700,
         而这一行父容器(.bdbA)的字体栈本来就是 Arial 那套 ⇒ 只要 font-family:inherit 就同源。
       ★ font-weight 从 500 提到 700 是照用户那句「参考顶部按钮的字体类型」直接对齐的;
         失真担忧不成立:本页 .abtn 只有这两枚,且它们在我们的深色玻璃胶囊里,
         字重提到 700 反而与胶囊的对比更立得住。 */
    font-family:inherit !important; font-weight:700 !important;
    font-size:12.5px !important; line-height:1 !important;
    text-decoration:none !important; border-radius:9px !important;
    border:1px solid rgba(255,255,255,.10) !important;
    background-color:rgba(255,255,255,.05) !important; background-image:none !important;
    color:#dbe4f2 !important; -webkit-text-fill-color:#dbe4f2 !important;
    transition:background-color .2s ease, border-color .2s ease, color .2s ease !important;
}
html.spp-read-on #main > .bdbA #breadcrumbs .fr .abtn:hover {
    background-color:rgba(32,201,151,.14) !important; border-color:rgba(32,201,151,.55) !important;
    color:#fff !important; -webkit-text-fill-color:#fff !important;
}
html.spp-read-on #main > .bdbA #breadcrumbs .fr .abtn > img {
    width:12px !important; height:6px !important; margin:0 !important;
    filter:invert(1) brightness(1.8) opacity(.72) !important;
}
html.spp-read-on #main > .bdbA #breadcrumbs .fr .abtn:hover > img {
    filter:invert(1) brightness(2.2) opacity(1) !important;
}
/* 左簇:home 图标那枚内联 margin-top:6px 让它在深条上吊在半空 —— !important 能盖非 important 的内联 */
html.spp-read-on #main > .bdbA #breadcrumbs .crumbs-item > img {
    margin:0 8px 0 2px !important;
    filter:invert(1) brightness(1.5) opacity(.66) !important;
}
/* ===== 2.52.48 一体流:整个帖子区合并成一张白卡,楼层之间只剩 1px 细线 =====
   ★ 用户挑了提案里的 s7「一体流」:没有卡片间距、没有投影,长贴阅读视觉噪音最小。
   ★ 外框逐行画(左右 1px + 行间 1px 细线,首末行补上下边与 17px 圆角),不画在 form 上:
     form 首位各有一个行盒(<a name> 20px + 尾部空白文本 23.8px),画在 form 上会多出白带。
   ★ 这里是「可读的规格说明 + 首帧兜底」;真正的强制在 initReadFlow.tag() 里用内联 !important
     (星空主题 html.spp-hm-star #main .t5:not(.spp-t5bar) 会抢 box-shadow / border)。 */
html.spp-read-on #main > form.spp-flow > .t5.t2.spp-flow-row {
    margin:0 !important;
    border:0 !important; border-radius:0 !important;
    border-left:1px solid #eaeff7 !important;
    border-right:1px solid #eaeff7 !important;
    border-bottom:1px solid #f1f5fa !important;
    background-color:#fff !important; background-image:none !important;
    box-shadow:none !important;
    transform:none !important;
}
html.spp-read-on #main > form.spp-flow > .t5.t2.spp-flow-start {
    border-top:1px solid #eaeff7 !important;
    border-top-left-radius:17px !important; border-top-right-radius:17px !important;
}
html.spp-read-on #main > form.spp-flow > .t5.t2.spp-flow-end {
    border-bottom:1px solid #eaeff7 !important;
    border-bottom-left-radius:17px !important; border-bottom-right-radius:17px !important;
}
/* 悬停:一体流不浮起,只在行内描一圈主色。★ 用 ::after 而不是行本身的 box-shadow ——
   行本体已被内联钉成 box-shadow:none,伪元素则完全不受星空那条规则影响。 */
html.spp-read-on #main > form.spp-flow > .t5.t2.spp-flow-row:hover {
    background-color:#fff !important; background-image:none !important;
}
html.spp-read-on #main > form.spp-flow > .t5.t2.spp-flow-row:hover::after {
    content:"" !important; position:absolute !important; inset:0 !important;
    border-radius:inherit !important; pointer-events:none !important;
    box-shadow:inset 0 0 0 2px rgba(32,201,151,.15) !important;
}
/* 左侧作者栏同一片白:站点给的偶数帖浅灰 / 楼主帖浅绿都归零 */
html.spp-read-on #main > form.spp-flow .t5.t2 th.r_two,
html.spp-read-on #main > form.spp-flow .t5.t2.spp-post-even th.r_two,
html.spp-read-on #main > form.spp-flow .t5.t2.spp-post-op th.r_two {
    background:#fff !important; background-image:none !important;
}
/* ---- 2.52.48 首字母头像块(仅默认头像 / 头像链接失效时由 JS 插入)----
   ★ .user-pic 救回:更早的版本用「.r_two.spp-no-avatar .user-pic{display:none}」藏过它,
     升级到本版的那一刻页面上可能还留着那个类(或 data-spp-no-avatar 属性),会把首字母块一起藏掉。 */
html.spp-read-on .t5.t2 .r_two .user-pic { display:block !important; }
html.spp-read-on .t5.t2 .r_two .user-pic img.spp-ava-off { display:none !important; }
html.spp-read-on .t5.t2 .r_two .spp-ava-initial {
    /* ★★ 2.53.4:这三个是「尺子」,fitTile() 只在这个区间里二分,不自己写死像素 ——
       · --spp-ava-fs-max:一行放得下时允许的最大字号(2.52.98 的 46px 就是被用户圈掉的那个「太大」);
       · --spp-ava-fs-min:再挤也不小于它;
       · --spp-ava-fs-one:一行字号若低于它 ⇒ 允许折行(最多 3 行,长 ID 因此不会缩成细线)。
       下面那条 font-size 只是 JS 量之前的第一帧兜底,量完由内联样式(!important)接管。 */
    /* ★★ 2.53.6 定案(用户口径,三次给死后落在这三个数上):
       · 上限 26px:用户定(2.53.5 的 28px 仍偏高,直接点名「改成二十六」);
         —— 一个汉字在 26px 下文本块 27×30px,可用区 100×120px,物理上还能更大,
            所以这一档是被**口径**封顶的,不是被宽度封顶的;
       · 下限 11px:用户定「字体最小为 11px」;
       · 折行门槛 24px:「只有在该 ID 的长度导致无法在一行内显示时,才考虑换行」——
         所以先在一行内尽量放大(最高 26),只有当一行里连 11px 都放不下时,才允许折到 3 行再放大。
       ★ 这条口径把「一行放得下就一行」放在「字号不小于某个下限」之前,实量(26px 上限下重跑矩阵):
         郦/芜湖 26px 单行(文本 27×29 / 53×29,可用 100×120);
         xxiii633 25px、blazblue/lian_798 23px、8 位十六进制 20px(文本 92-94px)、
         7 字汉字 13px(文本 93×15)、14 字符 ID 12px —— **全部单行,没有任何一个折行**,
         因为它们在一行里的上限都还在 11px 之上(这正是「只有一行放不下才折行」的判据)。 */
    --spp-ava-fs-max:32px; --spp-ava-fs-min:11px; --spp-ava-fs-one:24px;
    display:flex !important; align-items:center !important; justify-content:center !important;
    box-sizing:border-box !important;
    /* ★★ 2.53.8:尺寸 120 → **150**。用户批注(同一张截图两条箭头):
       「蓝色箭头指向的色块区域大小,应与红色箭头指向的头像大小一致(指色块区域,而非字体大小)」。
       真页面实测两者差多少:真头像 img 渲染 **157×157**(原图 150×150),色块只有 120×120;
       而按图片自然尺寸取整,站点的头像基准就是 **150**(157 = 150 + 站点给 img 的 3.5px 边框×2 + …)。
       ⇒ 色块与真头像统一到 150;字号上限 26 → 32 是**同一比例**的换算(150/120 = 1.25,
          26 × 1.25 = 32.5,取 32),否则框大了字没大,块里会显空。
       ★ 内边距 12 → 8:可用宽 = 150-16 = 134px(原来 100px),长 ID 的单行上限随之抬一档。 */
    width:150px !important; height:150px !important; margin:0 !important;
    padding:0 8px !important; overflow:hidden !important;
    border-radius:14px !important;
    color:#fff !important; font-size:32px !important; font-weight:700 !important;
    line-height:1.12 !important; letter-spacing:.02em !important; user-select:none !important;
    text-shadow:0 1px 3px rgba(16,24,40,.28) !important;
    box-shadow:0 8px 20px -12px rgba(16,24,40,.4), inset 0 0 0 1px rgba(255,255,255,.5) !important;
}
/* ★★ 2.53.4:内层文本块 —— 它是 fitTile() 量高度用的那把尺子。
   ★ 必须是块级(flex 项会自动缩到容器宽,长 ID 才会折行而不是溢出),
     折行由 overflow-wrap:anywhere 放开(它同时把 min-content 宽降到单字,flex 才收得动)。 */
html.spp-read-on .t5.t2 .r_two .spp-ava-txt {
    display:block !important; min-width:0 !important; max-width:100% !important;
    white-space:normal !important; overflow-wrap:anywhere !important; word-break:break-word !important;
    text-align:center !important;
}
/* 2.53.4:框里写着 ID,站点在框下面又印的那行用户名就是重复的(用户:「那个名字可以不用了」)——
   ★ 类由 JS 只打在「换过块」的楼层上(真头像楼层那行照旧,那是它唯一的身份显示);
   ★ 收掉的不只是名字链接,还有它上下的 <br>(不然会剩三行空白)。 */
html.spp-read-on .t5.t2 .r_two .spp-avaname-off { display:none !important; }
/* ---- 2.53.8 真头像的「白底头像框」(用户:「为带有图床链接的头像添加一个适配白色背景、美观精致的头像框」)
   ★ 为什么真头像需要框:站点那枚 img 是**裸图**(border:1px solid #eee + 直角 + 无内衬,实测渲染 157×157),
     直接贴在白色行底上 —— 图片的四周没有「被装裱」的痕迹,而色块那侧有 16px 圆角 + 双阴影,
     同一列里两种观感。用户截图里两支箭头指的就是这对落差(尺寸差 + 观感差)。
   ★ 做法与色块**对齐同一套容器语言**:3px 白内衬(图片不顶到边)+ 1px #e4e9f1 发丝边 +
     14px 圆角(比色块的 16px 小 2px:图片是实心内容,圆角在图上会被读成"裁切",收一点更稳)+
     与色块同源的双层阴影。
   ★ box-sizing:border-box + 固定 150×150 是关键:站点那张图**没有**尺寸声明、只有 width="150" 属性,
     边框与内衬会把它撑到 157(实测)⇒ 不钉尺寸的话,"框"会让它比色块又大一圈。
   ★ 作用域只认 .user-pic img:not(.spp-ava-off):被本脚本换掉的默认头像图(spp-ava-off)不套框,
     否则框会叠在色块外面再出来一层。 */
html.spp-read-on .t5.t2 .r_two .user-pic img:not(.spp-ava-off) {
    box-sizing:border-box !important;
    width:150px !important;
    /* ★ 2.53.27:高不再钉死 —— 150×150 会把非方图**拉伸**;改成 height:auto 由内在比例推,
       下限 150、上限 200。方图仍算出 150(与改动前逐像素一致),竖图最高长到 200。 */
    height:auto !important; min-height:150px !important; max-height:200px !important;
    padding:3px !important;
    border:1px solid #e4e9f1 !important; border-radius:14px !important;
    background-color:#ffffff !important;
    box-shadow:0 1px 2px rgba(15,23,42,.04), 0 8px 20px -14px rgba(16,24,40,.28) !important;
}

/* ---- 2.53.27(方案 c 折中·定稿):方框宽 150、高按比例 150~200 伸缩,超限才裁 ----
   ★ 方框一个字不动(150×150 + 3px 内衬 + 1px 边):非方图被拉伸才是原来的根问题。
   ★ 默认 contain:完整显示,方框里两侧留白(方框本来就是白底,留白看不出来)。
   ★ 超出容忍区间的细长图(JS 量出 naturalWidth/Height 后打 .spp-ava-crop)才 cover 裁切填满。
   ★ 不能用纯 CSS:object-fit 没法拿「图自己的比例」当条件,CSS 读不到 naturalWidth/Height。 */
html.spp-read-on .t5.t2 .r_two .user-pic img:not(.spp-ava-off) {
    object-fit:contain !important;
}
html.spp-read-on .t5.t2 .r_two .user-pic img:not(.spp-ava-off).spp-ava-crop {
    object-fit:cover !important;
}

/* ---- 2.53.7 左栏收口:把「头像块下面那一圈空行」真收干净 ----
   ★ 用户批注(截图圈住楼层底部那条灰带):「评论区的楼层似乎分成了上下两块,下面那块是什么?
     能否将其移除,只保留上面的部分?**将剩余空间给上方**」。
   ★ 真页面实测(read.php?tid-2974870 的 B10F):那「下面一块」**不是一个功能块**,是两件事叠出来的:
     ① 站点 TABLE.js-post 的**末行是空行**(tr.tr1.r_one > th > div.tpc_content,正文其实在上一行,
        这一行只剩站点自己的正文锚点 div#w_<pid>)—— 它自己占 34px;
     ② 左栏比右栏高:左 154(<br>13 + 头像表单 126 + div[align=center] 14 + 名字行)
        vs 右 120(tiptop 30 + 正文 56 + 站点给正文的 28px 下内边距)——差的那 34 正落在底部显影。
   ★ 所以「移除那一块」单独做是没用的:行藏掉后那 34px 会变成同宽的空白(A/B 实测:藏行 ⇒ 帖子仍 155,
     右栏反被撑到 154)。真正消掉它的钥匙在**左栏**:头像块已经写着 ID,框下那圈只剩空白占位。
   ★ 下面的数字全是 A/B 实测(tmp/r1/probe29-final.js):
     收口前 帖高 155 / 左 154 / 右 120 / 末行 34;收口后 **帖高 127 / 左 126 / 右 126 / 末行 0**。
   ★ 只压高度,**不动结构**:span.user-info#showface_<pid>(悬停卡本体)与站点那两个定位占位
     <span id="sf_<pid>"> 一个都不摘 —— 实测悬停卡节点仍在、.spp-uc__rows 仍是 9 行
     (改坏了就等于把 2.53.1~2.53.3 三轮做的卡一起废掉)。
   ★ 分组里的第一、二条是「占位 <br>」:.user-pic 里那枚在头像表单**之前**(13px 行盒)、
     div[align="center"] 之后那枚在名字行**之后**,两者都是纯占位。 */
   ★ 2.53.7 立此段时的教训(1.31 的第三次复发):这层 CSS 是**模板字面量**的正文,
     注释里出现反引号会直接**提前闭合整份脚本**(node --check 第一次就红在 12837 行)。
     ⇒ 本段及以下所有注释里,选择器一律不加反引号引号,改用普通书写。 */
html.spp-read-on .t5.t2 .r_two .user-pic > br { display:none !important; }
html.spp-read-on .t5.t2 .r_two > div[align="center"] ~ br { display:none !important; }
/* ---- 2.53.42:头像区那行名字 —— 两种楼层,两种活法 ----
   ★ 用户原文:「关闭的时候用户 ID 都消失;开启的时候无图床头像的用户有 ID、有图床头像的
     用户无 ID。无论开启还是关闭,都希望用户有 ID。」
   ★ 查下来是两个症状一个根因:上面那条收口原先**不带条件**(只认 .spp-read-on,不认开关),
     凡是没换成彩色块的楼层,名字就被它一起压成 0 高。实测 tid-2973029 全部 31 楼:
     容器 rect 高 0、名字链接 offsetHeight 0、字号被继承成 0px —— 节点一直都在,只是被藏住。
   ★ 修法两半,同一枚标记类 .spp-ava-tiled(由 15c 在彩色块真正插进 DOM 的那一行打在 .r_two 上):
     ① 换过彩色块的楼层 ⇒ 收口照旧(框里已经写着整段 ID,框下那圈确实只剩空行);
     ② 没换块的楼层 ⇒ 名字叠在头像框下沿,**零增高**。
   ★ 为什么选「叠」而不是「把名字行放回来」:放回来实测 +42~45px/楼(本页 29 个短楼合计
     +1263px),就算把名字上下的空 <br> 收掉也要 +625px;而叠加实测 31 楼**总高 5061 → 5061,
     一分没涨**,同时每个用户都有且只有一个 ID。
   ★ 叠加怎么做到零增高(三个数字,缺一条就错位):
     · 条高 22 = 3px 上下内边距 + 16px 行高(下面那条 a 的 padding/line-height);
     · 头像框底距 3px(实测:站点把头像表放进 .user-pic 后,框的**外底边**离单元格底 3px,
       方图 150 高与非方图 200 高都一样是 3);
     · 于是 margin-top 取 -(22+3) = -25px,条底正好压在框的外底边上(实测差 0)。
   ★★ 那个 margin-bottom:3px 不是凑数:单元格高度 = 头像表 + 名字层的**外边距盒**,
     只写 -25px 时单元格会从 156 缩到 153(实测),头像框随之丢掉底下那 3px 呼吸位;
     补回 3px 之后 31 楼的前 6 楼逐像素回到 266/206/156/156/156/156。
   ★ 为什么用负外边距而不是 position:absolute:绝对定位的 bottom:0 认的是**单元格**底边,
     而顶楼那种「正文很长把左栏撑到 266px」的楼层,头像只占顶部 156px —— 名字条会掉到
     单元格最下面去。负外边距走的是**文档流**(紧跟在头像表后面),与单元格多高无关。
   ★ 条宽 150 = 头像框的外宽,margin:0 auto 居中(实测左偏 0.5px,与框自身的居中误差同源);
     圆角 0 0 14px 14px 与框的 14px 外圆角对齐。
   ★ 长 ID 让它换行、**不截断**:本页 31 个名字最宽 72px(可用 134px),够用;
     真遇到超长名宁可让这一楼长高十几像素,也不做省略号 —— 用户要的正是「ID 要看得见」。
   ★ 真头像的**图**一个字没动:不裁、不缩、不换 src(2.53.27 的比例规则照旧),
     只是框的下沿浮了一枚名字条 —— 这条放宽是用户 2.53.42 拍板选的 ④。 */
html.spp-read-on .t5.t2 .r_two.spp-ava-tiled > div[align="center"] {
    display:block !important; height:0 !important; max-height:0 !important; overflow:hidden !important;
    margin:0 !important; padding:0 !important; line-height:0 !important; font-size:0 !important;
}
/* ② 没换块的楼层:名字层贴着头像框下沿浮起来(负外边距,不动单元格高度)*/
html.spp-read-on .t5.t2 .r_two:not(.spp-ava-tiled) > div[align="center"] {
    position:relative !important; z-index:2 !important;
    display:block !important; height:auto !important; max-height:none !important; overflow:visible !important;
    margin:-25px 0 3px !important;
    padding:0 !important;
    line-height:normal !important; font-size:12px !important;
    text-align:center !important;
}
/* 名字条本体:站点那行名字就是这一枚 <a>(外面那三枚空 <br> 全部收掉)*/
html.spp-read-on .t5.t2 .r_two:not(.spp-ava-tiled) > div[align="center"] > br { display:none !important; }
html.spp-read-on .t5.t2 .r_two:not(.spp-ava-tiled) > div[align="center"] > a {
    display:block !important; box-sizing:border-box !important;
    width:150px !important; margin:0 auto !important; padding:3px 8px !important;
    background-color:rgba(255,255,255,.86) !important;
    -webkit-backdrop-filter:blur(6px) !important; backdrop-filter:blur(6px) !important;
    border-radius:0 0 14px 14px !important;
    color:#26303f !important; -webkit-text-fill-color:#26303f !important;
    font-size:12px !important; font-weight:600 !important; line-height:16px !important;
    text-decoration:none !important;
    white-space:normal !important; overflow-wrap:anywhere !important; word-break:break-word !important;
}
/* 头像容器与它里面的表单:站点那层是 inline 上下文,行盒的 descent 会在表单上下各留 2px
   (实测 126 → 122 的差就是这么来的);line-height:0 把行盒压掉,表单自己决定高度。 */
html.spp-read-on .t5.t2 .r_two .user-pic { line-height:0 !important; font-size:0 !important; }
html.spp-read-on .t5.t2 .r_two .user-pic table { margin:0 auto !important; line-height:0 !important; }
/* 末行:站点那行**正文锚点**(div.tpc_content 里只有 div#w_<pid>,没有文字)。
   ★ 只在左栏收口生效的前提下才藏 —— 它是纯空行,藏着不丢内容;上面 ① 那条实测就是它占的 34px。
   ★★ 2.55.23 修:这条原先不带条件 ⇒ 把**悬赏 / 活动 / 商品帖**的悬赏框一起藏了。
      站点把悬赏框(div.tips:此帖悬赏中 / 最佳答案 / 热心助人剩余点数)就放在
      **这一末行的 div.tpc_content 里**,紧跟正文锚点 —— 真机实测 tid-2952377:
      该行 getComputedStyle(tr).display === "none"、div.tips 的 rect 是 0 x 0
      (内容一直在 DOM 里,只是被这条规则藏了;用户看到的就是「脚本没有显示悬赏金额」)。
      ⇒ 加 :not(:has(.tips)) 一个词就够:含悬赏框的末行照常显示(下一段给它换皮),
        其余 30 层的空末行照旧藏掉(2.53.7 收掉的那 34px 一分不丢)。 */
html.spp-read-on .t5.t2 > table.js-post > tbody > tr:last-child:not(:has(.tips)) { display:none !important; }

/* ===== 2.55.23:悬赏信息条(read.php 的悬赏 / 活动 / 商品帖,站点 .tips)=====
   ★ 站点原始形态(实测 tid-2952377):177 x 95 的居中三行小框,坐落在帖子表末行里 ——
       div.tips > span.s3「此帖悬赏中(剩余时间:已结束)...」
                 + div.tac「最佳答案: 100 SP币」
                 + div.tac「热心助人剩余点数: 10 SP币」
   ★ 改法:整块变一条**琥珀卡片**,三行摊成一排胶囊(flex + wrap)——
     状态一枚实色琥珀胶囊,两个数额各一枚白底胶囊;数额由 initBountyTips 拆成独立节点后
     单独提亮。配色与购买卡那条琥珀警告条同源(#fffaf0 底 / #f2e3c2 描边 / #e8c469 左线 /
     #8a6512 字),落在白卡上不抢正文的眼。
   ★ 站点原文一个字没改,只换外观;JS 只做 DOM 拆分(没跑时卡片照旧成立,见上一段的规矩)。
   ★ 为什么 inline-flex + max-width:站点给 div.tips 写了内联 width:auto,而它在 th 里是块级 ——
     留块级会让这条卡横铺满整行(实测内容列 1214px),三枚胶囊稀稀拉拉。收紧到内容宽度、
     左对齐挂在正文下方,读起来像一条「票据」;窗口窄了 gap 与 wrap 会在卡片内折行。 */
html.spp-read-on .t5.t2 > table.js-post > tbody > tr:last-child:has(.tips) > th {
    padding:0 !important; border:0 !important; vertical-align:top !important;
}
html.spp-read-on .t5.t2 > table.js-post > tbody > tr:last-child:has(.tips) > th > div.tpc_content {
    margin:0 !important;
    /* ★ 只压上下内衬,左右那 28px 原样留着 —— 站点给正文框的正是同一条 padding
       (实测正文行 .tpc_content padding-left 28px ⇒ 正文与图片起于 x=360)。
       留着它,卡片左沿就**自动**与正文对齐(实测卡片 left 360 = #read_tpc left 360);
       前一轮把它一并压成 0,卡片会左凸 28px 落在正文外沿(截图里一眼看得出)。
       上下压掉是为了收掉站点给这块的 28px 下内衬 —— 卡片自己的 9px 下外边距说了算。 */
    padding-top:0 !important; padding-bottom:0 !important;
}
html.spp-read-on .t5.t2 .tips {
    display:inline-flex !important; flex-wrap:wrap !important; align-items:center !important;
    gap:6px 8px !important; box-sizing:border-box !important; max-width:100% !important;
    margin:2px 0 9px !important; padding:8px 10px !important;
    background-color:#fffaf0 !important;
    background-image:linear-gradient(180deg,#fffdf8 0%,#fff5e1 100%) !important;
    border:1px solid #f2e3c2 !important; border-left:3px solid #e8c469 !important;
    border-radius:10px !important;
    box-shadow:0 1px 2px rgba(15,23,42,.03) !important;
    color:#8a6512 !important;
    font-size:12px !important; line-height:1.6 !important; text-align:left !important;
}
/* ① 状态(站点那枚 span.s3)→ 实色琥珀胶囊 */
html.spp-read-on .t5.t2 .tips > span.s3 {
    flex:0 0 auto !important; display:inline-block !important;
    margin:0 !important; padding:2px 9px !important; border-radius:999px !important;
    background-color:#f6e5be !important; color:#7a5a0d !important;
    font-size:11.5px !important; font-weight:600 !important;
}
/* ② 两个数额(站点 .tac 是居中块)→ 白底胶囊并排:标签淡、数额亮 */
html.spp-read-on .t5.t2 .tips > div.tac {
    flex:0 0 auto !important; display:inline-flex !important; align-items:baseline !important; gap:5px !important;
    margin:0 !important; padding:2px 10px !important; border-radius:999px !important;
    background-color:#ffffff !important; border:1px solid #f2e3c2 !important;
    color:#8a6512 !important; font-size:11.5px !important;
    text-align:left !important; white-space:nowrap !important;
}
html.spp-read-on .t5.t2 .tips > div.tac > span.spp-bn__k { color:#a08a5e !important; font-weight:400 !important; }
html.spp-read-on .t5.t2 .tips > div.tac:first-of-type > b.spp-bn__v {
    color:#b45309 !important; font-size:13px !important; font-weight:700 !important;
    font-variant-numeric:tabular-nums !important; letter-spacing:.2px !important;
}
html.spp-read-on .t5.t2 .tips > div.tac + div.tac > b.spp-bn__v {
    color:#8a7333 !important; font-size:12px !important; font-weight:600 !important;
    font-variant-numeric:tabular-nums !important;
}
/* ===== 2.52.60:折行预算(预留三行)—— 当前项最多折到 3 行,前几段单行不参与收缩 =====
   ★ 站点内联样式把 #breadcrumbs{height:26px} 与 #breadcrumbs .crumbs-item{height:26px} 写死了:
     不禁掉这两条高度,标题折到第二行会直接从 26px 的盒子里溢出(条高不跟着长)。
   ★ 站点结构:<span class="crumbs-item current"><i></i><strong><a>标题</a></strong> <a rss>…</a></span>
     只让 current 这一枚收缩折行;前面几段(南+ South Plus › 版块 › 子版)flex:none,保持单行。
   ★ 不写 flex-wrap:wrap:整行换行会把 RSS 胶囊甩到第二行(2.52.59 实测白多 26px),
     让 current 自己缩、自己折行才对。
   ★ 作用域钉在 .spp-crumb-panel 上 —— 底部「浏览记录」条同款结构不吃折行(它没有长标题)。 */
html.spp-read-on .spp-crumb-panel #breadcrumbs .crumbs-item.gray3,
html.spp-read-on .spp-crumb-panel #breadcrumbs .crumbs-item:first-child { flex:none !important; }
html.spp-read-on .spp-crumb-panel #breadcrumbs .crumbs-item.current {
    height:auto !important; min-height:0 !important;
    flex:1 1 auto !important; align-items:center !important;
    white-space:normal !important; overflow:visible !important; text-overflow:clip !important;
}
html.spp-read-on .spp-crumb-panel #breadcrumbs .crumbs-item.current > strong {
    display:block !important; flex:1 1 auto !important;
    min-width:0 !important; max-width:none !important;
    white-space:normal !important; overflow:hidden !important;
}
html.spp-read-on .spp-crumb-panel #breadcrumbs .crumbs-item.current > strong > a {
    display:-webkit-box !important; -webkit-box-orient:vertical !important; -webkit-line-clamp:3 !important;
    max-width:100% !important; overflow:hidden !important;
    white-space:normal !important; overflow-wrap:anywhere !important; word-break:break-word !important;
    line-height:1.45 !important;
}
html.spp-read-on .spp-crumb-panel #breadcrumbs .crumbs-item.current > a[href^="rss.php"] {
    flex:none !important; align-self:center !important;
}
/* ===== 2.52.61:RSS 胶囊右缘被削 7px =====
   ★ 根因(实站量测):站点给每枚 .crumbs-item 挂 margin-right:-7px —— 设计意图是让相邻项的
     分隔图标互相咬合(flex 里前一项的 -7 外缘正好被后一项压住,看不出来)。但收尾的「当前项」
     右边没有下一项可咬合:外缘(margin box)被 flex 收在内容缘,边框盒却悬在内容缘外 7px,
     RSS 胶囊贴在边框盒右端 —— #breadcrumbs 的 overflow:hidden(2.52.34 起)沿内容缘裁切,
     胶囊于是被削掉一条。
   ★ 修法只动收尾一枚:负 margin 归零后 flex 自动把它的边框盒收回内容缘内,
     胶囊右缘 = 板块内容缘(与左端 home 图标的 12px 内缩对称);中间项的咬合间距原样保留。
     不用 :last-child:语义上「当前项」就是最后一枚面包屑,站点往后追加什么也不受影响。 */
html.spp-read-on .spp-crumb-panel #breadcrumbs .crumbs-item.current { margin-right:0 !important; }
/* ===== 2.52.60:撤销 2.52.59 的 32%/68% 钉死 =====
   ★ 钉死当时是为了压住「折行把右列最小内容宽顶到 907px」的列宽漂移;面包屑整条搬出右格后
     右列只剩热门版块网格(.v 自带省略号可收缩),漂移源头不复存在。
   ★ 实测站点原生 td#user_info 就带着 width="53%"(与主页同款)—— 不再干预,自动回到
     主页那套「正常大小分配」(左 53 / 右 47),两格由 EQ_CSS 的 height:189px 保持等高。 */
/* ★ 用户批注「移除」(2.52.59):右格那行「热门版块 6 … 常用入口」整行去掉(保持移除)。 */
html.spp-read-on #infobox.spp-up-on .hb-wrap .hb-hd { display:none !important; }

/* ★ 2.52.101:房子按钮那一段(① 按钮视觉 / ② 面包屑挂载点 / ③ 面板划出动画)已搬出本表,
   改由 initCrumbHomeNav 注入独立的 #spp-hbtn-style —— 它现在要同时服务帖子页与版块页,
   作用域是 html.spp-hbtn-on(帖子页不再依赖 html.spp-read-on)。常量见 HOME_NAV_CSS。 */
/* ---- ④ 帖子头部行:楼层号 / 时间 / 签名(居中)/ 操作按钮 ----
   ★★ 2.53.4:整行收进一枚「工具条」卡片(用户圈住整行:「需用 UI 框包裹,以提升高级感并
     适配白色背景,避免呈现出类似几十年前 BBC 论坛的古老廉价网页感」)。
   ★ 站点原样只有一条 1px 点线(.tiptop{border-bottom:1px dotted #ccc}):左边是裸文本
     「B10F 2026-10-01 22:53」,右边一串「|」隔开的裸链接 —— 与 2.53.1/2.53.2 起
     悬停状态卡那套「浅灰凹槽 + 圆角 + 中性色」的容器语言完全对不上。
   ★ 容器与悬停卡同源:#f8fafc 底 / 1px #edf1f7 边 / 10px 圆角(悬停卡是 9px 的凹槽,
     这里是整行,取 10px)。行高 27 → 30px(站点 height:27px 必须让开,否则内边距会顶破)。 */
html.spp-read-on #main > form.spp-flow .tiptop {
    display:flex !important; align-items:center !important; flex-wrap:nowrap !important;
    gap:10px !important;
    box-sizing:border-box !important; min-height:30px !important; height:auto !important;
    padding:0 10px !important;
    background-color:#f8fafc !important; background-image:none !important;
    border:1px solid #edf1f7 !important; border-radius:10px !important;
    box-shadow:none !important;
}
/* 站点那枚 <div class="c"> 是浮动清除器(clear:both/height:0);在 flex 里只会多出一段 gap */
html.spp-read-on #main > form.spp-flow .tiptop > .c { display:none !important; }
html.spp-read-on #main > form.spp-flow .tiptop > .fl { float:none !important; }
html.spp-read-on #main > form.spp-flow .tiptop > .fr { float:none !important; margin-left:auto !important; }
/* 楼层号(B10F,站点那枚 copyUrl 链接):白底胶囊 —— 这一行里唯一的「锚点」信息。
   ★ 站点全文都是裸文本,这里给它一枚容器:白底 + 发丝边 + 999px 圆角。
   ★ 认 .fl:not(.gray):站点的时间那枚带 .gray,不能靠 :first-child(那行里有空白文本节点)。 */
html.spp-read-on #main > form.spp-flow .tiptop > .fl:not(.gray) > a {
    display:inline-flex !important; align-items:center !important;
    box-sizing:border-box !important; height:18px !important; padding:0 8px !important;
    margin:0 !important;                 /* 站点 .tiptop a{margin:0 .2em} 让位给 flex gap */
    border:1px solid #e3eaf3 !important; border-radius:999px !important;
    background-color:#ffffff !important; background-image:none !important;
    color:#475569 !important; font-size:11px !important; font-weight:600 !important;
    letter-spacing:.03em !important; line-height:1 !important; text-decoration:none !important;
    transition:color .14s ease, border-color .14s ease !important;
}
html.spp-read-on #main > form.spp-flow .tiptop > .fl:not(.gray) > a:hover {
    color:#2f6f68 !important; border-color:#cfe3df !important;
}
/* 时间:站点给的 .fl.gray 是 #888 —— 在白卡上再压淡一档,数字等宽(每层的列能对齐) */
html.spp-read-on #main > form.spp-flow .tiptop > .fl.gray {
    color:#94a3b8 !important; font-size:12px !important; font-variant-numeric:tabular-nums !important;
}
/* 操作项(只看xx / 屏蔽 / 小 中 大 / 顶端 / 回复 / 引用 / 更多):幽灵胶囊 —— 默认无底色,
   悬停才浮出浅蓝底 + 主色字。△ 站点那几枚 <a> 都带内联 onclick,这里只动外观。 */
html.spp-read-on #main > form.spp-flow .tiptop > .fr > a,
html.spp-read-on #main > form.spp-flow .tiptop > .fr > span > a,
html.spp-read-on #main > form.spp-flow .tiptop > .fr > div.dropdown > a {
    display:inline-flex !important; align-items:center !important;
    box-sizing:border-box !important; height:19px !important; padding:0 8px !important;
    margin:0 !important;                 /* 站点 .tiptop a{margin:0 .2em} 让位给 flex gap */
    border:1px solid transparent !important; border-radius:999px !important;
    color:#4b5563 !important; font-size:12px !important; line-height:1 !important;
    text-decoration:none !important;
    transition:background-color .14s ease, color .14s ease, border-color .14s ease !important;
}
html.spp-read-on #main > form.spp-flow .tiptop > .fr > a:hover,
html.spp-read-on #main > form.spp-flow .tiptop > .fr > span > a:hover,
html.spp-read-on #main > form.spp-flow .tiptop > .fr > div.dropdown > a:hover {
    background-color:#eef3f9 !important; color:#2f6f68 !important; border-color:#e2ebf3 !important;
}
/* 「更多」那枚站点是 16×16 位图(dropB-cion.gif,还配 font:0/0 + text-indent:-2000em 藏字)——
   在这里把它掰回一枚正常的文字胶囊(背景图去掉、字号与缩进还给文字)。 */
html.spp-read-on #main > form.spp-flow .tiptop a.dropB {
    background:none !important; width:auto !important; height:19px !important;
    text-indent:0 !important; font-size:12px !important; line-height:1 !important;
    overflow:visible !important; float:none !important;
}

/* ---- 2.53.24(用户批注 read.php 红框 + 黄框)----
   ① 红框:楼层那枚「更多」撤掉 —— 用户定案「没有这个功能」,留着只占位。
      ★ 必须带 id 前缀限定:页眉的「更多」也是 .dropB(#td_u),不加限定会一起误伤。
   ② 黄框:操作条里「屏蔽」那个下拉(div.dropdown-content.menu,两枚 a.apeacemaker:
      「屏蔽此人 / 屏蔽头像」)。实测它的 position:absolute + z-index:100 + 深底 rgb(14,17,22)。
      a) 底色:帖子区是白卡,深底压上去很突兀 ⇒ 白底 + 浅边 + 轻阴影,与操作条胶囊同一族语言。
      b) 图层:z-index 只有 100,正文图片一浮上来就盖住它 ⇒ 提到 3000;并把触发盒 .dropdown
         自己抬成一层(position:relative),免得它被祖先的层序压住。 */
html.spp-read-on #main > form.spp-flow .tiptop a.dropB[id^="td_read_"] {
    display:none !important;
}

/* ★ 2.53.25:这里**故意不给** .dropdown 加 position:relative ——
   加了它,绝对定位的包含块会从更高的祖先塌缩成那枚 41×19 的触发盒,
   而父盒 DIV.fr 是 overflow:hidden,下拉当场被裁掉(实测 elementsFromPoint 从 dd 变成 TABLE)。
   实测未改动的页面上这枚下拉本来就在最上层(栈里它上面只有自己的两个子元素),
   所以只需要给它自己提 z-index,别动父盒的定位。 */
html.spp-read-on #main > form.spp-flow .tiptop > .fr > div.dropdown > div.dropdown-content.menu {
    z-index:3001 !important;
    background-color:#ffffff !important; background-image:none !important;
    border:1px solid #e6ebf2 !important; border-radius:10px !important;
    box-shadow:0 10px 26px rgba(15,23,42,.14), 0 2px 6px rgba(15,23,42,.05) !important;
    padding:4px !important;
}
html.spp-read-on #main > form.spp-flow .tiptop > .fr > div.dropdown > div.dropdown-content.menu > a {
    display:block !important;
    box-sizing:border-box !important;
    padding:5px 10px !important; margin:0 !important;
    border:0 !important; border-radius:7px !important;
    background:none !important; text-decoration:none !important; white-space:nowrap !important;
    color:#334155 !important; -webkit-text-fill-color:#334155 !important;
    font-size:12px !important; line-height:1.5 !important;
    transition:background-color .14s ease, color .14s ease !important;
}
html.spp-read-on #main > form.spp-flow .tiptop > .fr > div.dropdown > div.dropdown-content.menu > a:hover {
    background-color:#eef3f9 !important;
    color:#2f6f68 !important; -webkit-text-fill-color:#2f6f68 !important;
}
/* 分隔符:站点在 .fr 里插的是**裸文本节点**「|」(CSS 选不中,画不成线),
   JS(模块 10 的 normalizeSeps)把它包成 .spp-tp-sep 后,在这里画成 1px 细线;
   字号 0 把那个「|」字形藏掉,只留背景那一条。 */
html.spp-read-on #main > form.spp-flow .tiptop .spp-tp-sep {
    display:block !important; flex:0 0 auto !important; align-self:center !important;
    width:1px !important; height:12px !important;
    margin:0 3px !important; padding:0 !important; border:0 !important;
    background-color:#e3eaf3 !important; background-image:none !important;
    color:transparent !important; font-size:0 !important; line-height:0 !important;
    overflow:hidden !important;
}
html.spp-read-on #main > form.spp-flow .tiptop > .spp-sign-inline {
    flex:1 1 auto !important; min-width:0 !important;
    display:flex !important; align-items:center !important; justify-content:center !important;
    max-height:24px !important; overflow:hidden !important;
    text-align:center !important; font-size:12px !important; line-height:1.4 !important;
    color:#8a94a6 !important;
}
html.spp-read-on #main > form.spp-flow .tiptop > .spp-sign-inline img {
    max-height:20px !important; width:auto !important; vertical-align:middle !important;
}

/* ===== 2.52.98:快速发帖框(帖子页底部回复框)美化 —— 用户截图里那五条批注 =====
   ★ 这一段是独立审计稿(tmp/fr/fastreply.css,含实测基线与逐条判据)的并入版:
     每条规则「为什么这么写」的推导都在原稿注释里保留着,原稿与 REPORT.md 也留在库里。
   ★ 作用域全部收在 div.t5:has(textarea[name="atc_content"])(= 回复框整块)里;发帖页那个
     编辑器(#editor-content,由 POST_CSS 负责)不受影响。浏览器不支持 :has() 时整段不生效
     —— 结果是「保持站点原样」,不会把别处改坏。
   ★ 两处形态由 JS 生成、CSS 只管样式(没跑 JS 时这些选择器匹配数为 0、零副作用):
     ① label.spp-fr-opt     —— 左栏「复选框 + 紧随文本」的胶囊形态(裸文本节点纯 CSS 选不到)
     ② span.spp-fr-actions  —— 「提交 + 预览」那两枚按钮的独立胶囊框(站点没给它们共同父节点)
   ★ 三条别踩的坑(原稿实测):
     · width:100% 必须配 box-sizing:border-box —— content-box 下会比工具栏【更宽】(735.59+内边距);
     · #spp-preview-btn 段故意不写 background/color/border-color —— 脚本用内联样式表达 hover 与
       「预览激活=实心」,!important 会把它盖住,按钮会看起来像坏了;
     · select:has(> option:only-child) 是活判据:站点哪天给这两枚下拉补上第二个选项,规则自动失效。
   ========================================================================== */
/* ============================================================================
   快速发帖区(read.php 帖子页底部回复框)美化草案 v1
   ----------------------------------------------------------------------------
   产出物:纯 CSS 片段(未并入主脚本,仅供评审)
   作用域:html.spp-read-on(帖子页)
           div.t5:has(textarea[name="atc_content"])   ← 快速发帖框整块
           (发帖页 post.php 的编辑器不在此列:那是 #editor-content,由 POST_CSS 负责)
   硬规矩:本文件内【不得出现反引号与美元符号】(将来整段贴进 READ_CSS 模板字面量
           时会提前闭合 / 被当 JS 解析);每条声明均带 !important。
   实测基准:blue-plus.net/read.php?tid=2975415(客人态,html.spp-hm-star.spp-read-on)
   ----------------------------------------------------------------------------
   三段作用域在下面反复出现,为便于阅读分别简称(CSS 里全部展开写,不用 :is() 缩写):
     [左栏] div.t5:has(textarea[name="atc_content"]) td.f_one:has(input[name="atc_usesign"])
     [中栏] div.t5:has(textarea[name="atc_content"]) td.f_one:has(textarea[name="atc_content"])
     [提交行] [中栏] > div:has(input[name="Submit"])
   为什么用 :has() 而不是 :nth-child():三个 TD.f_one 与站点列序无关,
   认「这一栏里装的是什么控件」比认「它是第几格」稳(站点改列序时不会误伤)。
   浏览器不支持 :has() 时本文件整体不生效 —— 结果是【保持站点原样】,不会把别处改坏。
   ============================================================================ */


/* ==========================================================================
   0. 白底主题底座(批注 5)
   ========================================================================== */
html.spp-read-on div.t5:has(textarea[name="atc_content"]) {
    color-scheme: light !important;                         /* 白底页,禁用 UA 深色渲染(原生控件/滚动条) */
    background-color: var(--spu-surface,#ffffff) !important; /* 原 rgba(250,252,255,.93),会把星空透进来 */
    background-image: none !important;
    border: 1px solid var(--spu-border,#d8dee7) !important;
    border-radius: 12px !important;
    box-shadow: 0 1px 2px rgba(15,23,42,.04), 0 12px 30px rgba(15,23,42,.08) !important;
    color: var(--spu-text,#1f2937) !important;
    font-size: 12.5px !important;
}


/* ==========================================================================
   1. 左栏「内容 / HTML 代码不可用 / 使用签名 / Wind Code 自动转换 / 匿名帖 /
          隐藏此帖 / 隐藏附件 / 出售 … / 加密 …」(批注 1)
   --------------------------------------------------------------------------
   站点结构(实测 td.f_one 的 childNodes 顺序):
     B[内容] BR TXT[HTML 代码不可用] BR
     INPUT[checkbox][atc_usesign]  TXT[使用签名] BR
     INPUT[checkbox][atc_convert]  TXT[Wind Code自动转换] BR
     INPUT[checkbox][atc_anonymous] TXT[匿名帖] BR
     INPUT[checkbox][atc_hide]      TXT[隐藏此帖] BR
     INPUT[checkbox][atc_hideatt]   TXT[隐藏附件] BR
     INPUT[checkbox][atc_requiresell] TXT[出售] INPUT.input[atc_money] SELECT[atc_credittype] BR
     INPUT[checkbox][atc_requireenhide] TXT[加密] INPUT.input[atc_rvrc] SELECT[atc_enhidetype] BR
   ★★ 关键事实:每行的标签是【裸文本节点】,没有 label / span 包着 ——
      纯 CSS 能把「小方框」换成主题化勾选块,但【无法把文字装进胶囊里】,
      也【无法逐行改文字颜色】(文本节点选不到)。要真正的「胶囊按钮(文字在胶囊内)」
      必须由 JS 把「复选框 + 紧随的文本节点」包成一个 label(草案见 REPORT.md 第 4.1 节,
      本文件第 5 节的 label.spp-fr-opt 段就是给那一版准备的样式,未跑 JS 时完全不生效)。
   ★★ 「HTML 代码不可用」那一行没有控件、也没有元素包裹,纯 CSS 无法单独取到
      (客人态下它就是一句提示文字),本草案不处理。
   ========================================================================== */

/* ---- 1a. 复选框:主题化方形勾选块(纯 CSS 版,无需 JS) ---- */
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(input[name="atc_usesign"]) input[type="checkbox"] {
    -webkit-appearance: none !important;
    appearance: none !important;
    accent-color: var(--spu-accent,#2f6f68) !important;   /* appearance:none 失效时的兜底 */
    box-sizing: border-box !important;
    display: inline-block !important;
    /* ★ 尺寸与边距是【成套】的,别单独改其中一个:
       站点原值 = 高 13 + marginTop 3 + marginBottom 1 = 17 的 margin 盒,
       实测每行节距 19.4px(= 17 + 文本 descent 2.4)。
       这里 15 + 1 + 1 仍是 17 ⇒ 行距不变(改错了整栏会变高/挤在一起)。 */
    width: 15px !important;
    height: 15px !important;
    margin: 1px 7px 1px 0 !important;
    border: 1.5px solid var(--spu-border,#d8dee7) !important;
    border-radius: 5px !important;
    background-color: var(--spu-surface,#ffffff) !important;
    background-image: none !important;
    box-shadow: none !important;
    vertical-align: -2px !important;
    cursor: pointer !important;
    transition: background-color .16s ease, border-color .16s ease, box-shadow .16s ease !important;
}
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(input[name="atc_usesign"]) input[type="checkbox"]:hover {
    border-color: var(--spu-accent,#2f6f68) !important;
    box-shadow: 0 0 0 3px rgba(47,111,104,.10) !important;
}
/* 勾选态:底色换主题色 + 白色对勾(用 data:URI 的 SVG,不依赖 input 上的伪元素支持) */
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(input[name="atc_usesign"]) input[type="checkbox"]:checked {
    background-color: var(--spu-accent,#2f6f68) !important;
    background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.4 8.5l3.1 3.1 6.1-6.6' fill='none' stroke='%23ffffff' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") !important;
    background-repeat: no-repeat !important;
    background-position: center center !important;
    background-size: 11px 11px !important;
    border-color: var(--spu-accent,#2f6f68) !important;
    box-shadow: 0 2px 8px rgba(47,111,104,.22) !important;
}
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(input[name="atc_usesign"]) input[type="checkbox"]:focus-visible {
    outline: 2px solid rgba(47,111,104,.45) !important;
    outline-offset: 1px !important;
}

/* ---- 1b. 选项文字 + 小标题「内容」 ---- */
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(input[name="atc_usesign"]) {
    color: var(--spu-secondary,#475569) !important;
}
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(input[name="atc_usesign"]) > b {
    color: var(--spu-text,#1f2937) !important;
    font-size: 12.5px !important;
    font-weight: 700 !important;
    letter-spacing: .04em !important;
}

/* ---- 1c. 「出售 / 加密」两个金额输入框(站点类名就叫 input) ---- */
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(input[name="atc_usesign"]) input.input {
    box-sizing: border-box !important;
    width: 40px !important;                 /* 原 34.3px;40 是留出数字位,可回退 */
    height: 20px !important;                /* 原 19.3px */
    margin: 0 4px !important;
    padding: 0 4px !important;
    border: 1px solid var(--spu-border,#d8dee7) !important;
    border-radius: 6px !important;
    background-color: var(--spu-surface,#ffffff) !important;
    background-image: none !important;
    color: var(--spu-text,#1f2937) !important;
    font-family: inherit !important;
    font-size: 12px !important;
    line-height: 1 !important;
    text-align: center !important;
    vertical-align: middle !important;
    outline: none !important;
    box-shadow: none !important;
}
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(input[name="atc_usesign"]) input.input:focus {
    border-color: var(--spu-accent,#2f6f68) !important;
    box-shadow: 0 0 0 3px rgba(47,111,104,.12) !important;
}


/* ==========================================================================
   2. 「SP币 ▼ / HP ▼」两枚只有一个选项的下拉:退化成静态文字(批注 2)
   --------------------------------------------------------------------------
   实测:SELECT#attmode_2[name=atc_credittype] 的 options = ["SP币=money"](1 个)
         SELECT#attmode_1[name=atc_enhidetype] 的 options = ["HP=rvrc"](1 个)
   判据用 :has(> option:only-child) —— 只有一个 option 时它就是 :only-child。
   ★ 好处:将来站点给这两枚下拉补上第二个选项,本规则【自动失效】、恢复成正常下拉,
     不需要任何 JS 记账;反过来若写成固定 id 选择器,站点加选项后就成了「点不动的假文本」。
   ★ 视觉动作:appearance:none 收掉系统箭头,边框/底色全撤 ⇒ 就是一行文字;
     pointer-events:none 让它彻底不可交互(原生弹层没法被 CSS 改,索性不给入口)。
     value/name 原样保留在表单里,提交行为一字不变(appearance 不影响表单值)。
   ========================================================================== */
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(input[name="atc_usesign"]) select:has(> option:only-child) {
    -webkit-appearance: none !important;
    appearance: none !important;
    box-sizing: content-box !important;
    width: auto !important;
    height: auto !important;
    min-height: 0 !important;
    margin: 0 2px !important;
    padding: 0 2px !important;
    border: 0 !important;
    border-radius: 0 !important;
    background: transparent none !important;
    background-image: none !important;
    box-shadow: none !important;
    outline: none !important;
    color: var(--spu-secondary,#475569) !important;
    font-family: inherit !important;
    font-size: 12px !important;
    font-weight: 600 !important;
    line-height: 1.5 !important;
    text-align: left !important;
    vertical-align: middle !important;
    cursor: default !important;
    pointer-events: none !important;
}


/* ==========================================================================
   3. 工具栏(WindCode 标签行)必须与正文框严格等宽(批注 3)
   --------------------------------------------------------------------------
   实测(read.php 快速发帖框,中栏 TD.f_one:内容左缘 257.53 / 内容宽 735.59):
     工具栏 DIV.spp-editor-toolbar  257.53 .. 993.13  宽 735.59
     正文框 TEXTAREA                257.53 .. 971.03  宽 713.50
     ⇒ 右缘差 22.09px(工具栏比文本框宽),左缘本来就是齐的。
   根因:站点给 textarea 写了【行内 style="width:96%"】(96% × 735.59 − 内边距/边框 = 713.5),
        而工具栏是我们的脚本产物(buildToolbar,无 width ⇒ 块级自动撑满 735.59)。
   两解都成立,本草案选「让文本框也撑满」——
     (a) 文本框 width:100% + box-sizing:border-box ⇒ 两者都等于 735.59(本草案)
     (b) 工具栏收窄到 713.5px(写死,站点一改 textarea 宽度就再次错位)
   ★ box-sizing 必须和 width:100% 一起写:content-box 下 width:100% 会变成 735.59+内边距+边框,
     比工具栏【更宽】,反而错得更远。
   ★ resize 必须从 both 改成 vertical:站点原值是 resize:both,用户横向拖一下
     宽度就再也不等于工具栏(且不可恢复)。
   ★ display:block:站点那个 textarea 是 inline-block,会多出 ~1px 的基线间隙;
     改块级后与工具栏严丝合缝(实测现在工具栏底 1482.04 / 文本框顶 1483,差 0.96px)。
   ★ 圆角/边框改成「工具栏留底边、文本框留顶边」= 两者拼成一张卡;上下圆角都取 10px
     (原来工具栏 8px、文本框 6px,拼接处对不上)。
   ========================================================================== */
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(textarea[name="atc_content"]) > .spp-editor-toolbar {
    box-sizing: border-box !important;
    display: flex !important;
    flex-wrap: wrap !important;
    align-items: center !important;
    width: auto !important;                  /* 块级自动撑满中栏内容宽 735.59 */
    max-width: 100% !important;
    min-width: 0 !important;
    margin: 0 !important;
    border: 1px solid var(--spu-border,#d8dee7) !important;
    border-bottom: 1px solid var(--spu-border,#d8dee7) !important;
    border-radius: 10px 10px 0 0 !important;
    background-color: var(--spu-overlay,#eef2f5) !important;
    color: var(--spu-secondary,#475569) !important;
}
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(textarea[name="atc_content"]) > textarea[name="atc_content"] {
    box-sizing: border-box !important;
    display: block !important;
    width: 100% !important;                  /* ★ 等宽的关键一条(覆盖站点行内 width:96%) */
    max-width: 100% !important;
    min-width: 0 !important;
    margin: 0 !important;
    resize: vertical !important;             /* 原 both:横向拖拽会破坏等宽 */
    padding: 7px 10px !important;
    border: 1px solid var(--spu-border,#d8dee7) !important;
    border-top: 0 !important;
    border-radius: 0 0 10px 10px !important;
    background-color: var(--spu-surface,#ffffff) !important;
    background-image: none !important;
    color: var(--spu-text,#1f2937) !important;
    font-family: inherit !important;
    font-size: 13px !important;
    line-height: 1.7 !important;
    outline: none !important;
}
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(textarea[name="atc_content"]) > textarea[name="atc_content"]:focus {
    border-color: var(--spu-accent,#2f6f68) !important;
    box-shadow: 0 0 0 3px rgba(47,111,104,.10) !important;
}
/* ===== 2.53.47:附件区的「选择文件」按钮(用户:「网址底部输入框的附件区域的
   『选择文件』按钮 UI 需要美化」)=====
   ★ 实测现状:站点全局 .input{appearance:none} 把 <input type=file> 的外壳剥掉了
     (computed: appearance:none / border:1px solid #ddd / border-radius:0 / height:24px),
     而**内层那枚「选择文件」按钮仍是 Chrome 原生的灰方框** —— 容器被美化、按钮没被碰,
     所以看着像「一个贴片里嵌了块系统控件」。
   ★ 皮肤直接沿用设置页那份(搜 file-selector-button,设置页第 17953 行起),
     只改配色:那边是**深色页**(#set-wrap 是深底),这边是白卡,所以换成浅色玻璃。
   ★ ::file-selector-button 是标准选择器(Chrome 89+);旧版不认时整条规则被丢弃,
     输入框退回原样,不会更糟。
   ★ 不给 input 加 width:站点靠 size="10" 定的宽(实测 236.67px),
     本规则只改内衬与边框,宽度沿用站点,避免把那一行撑破。 */
html.spp-read-on :is(div.t5:has(textarea[name="atc_content"]), .spp-t5bar) input[type="file"] {
    box-sizing: border-box !important;
    height: 26px !important;
    padding: 1px 6px 1px 1px !important;
    border: 1px solid var(--spu-border,#d8dee7) !important;
    border-radius: 7px !important;
    background-color: var(--spu-surface,#ffffff) !important;
    background-image: none !important;
    color: var(--spu-muted,#6b7280) !important;
    font-family: inherit !important;
    font-size: 12.5px !important;
    vertical-align: middle !important;
    transition: border-color .16s ease, box-shadow .16s ease !important;
}
html.spp-read-on :is(div.t5:has(textarea[name="atc_content"]), .spp-t5bar) input[type="file"]:hover {
    border-color: var(--spu-accent,#2f6f68) !important;
    box-shadow: 0 0 0 3px rgba(47,111,104,.10) !important;
}
html.spp-read-on :is(div.t5:has(textarea[name="atc_content"]), .spp-t5bar) input[type="file"]::file-selector-button {
    box-sizing: border-box !important;
    height: 22px !important;
    margin: 0 7px 0 0 !important;
    padding: 0 11px !important;
    border: 1px solid rgba(47,111,104,.42) !important;
    border-radius: 6px !important;
    background-color: rgba(47,111,104,.09) !important;
    background-image: none !important;
    color: var(--spu-accent,#2f6f68) !important;
    font-family: inherit !important;
    font-size: 12px !important;
    font-weight: 600 !important;
    cursor: pointer !important;
    transition: background-color .16s ease, color .16s ease, border-color .16s ease !important;
}
html.spp-read-on :is(div.t5:has(textarea[name="atc_content"]), .spp-t5bar) input[type="file"]::file-selector-button:hover {
    background-color: var(--spu-accent,#2f6f68) !important;
    border-color: var(--spu-accent,#2f6f68) !important;
    color: #fff !important;
}

/* ===== 2.53.47:楼层顶部行 —— 收掉「小 中 大」三枚与「顶端」=====
   ★★ 这两条**为什么住在 READ_CSS 里**:最开始它们写在「版块页工具条」那个 CSS 模板里
      (那段里有 .bdbA / #breadcrumbs 的面包屑规则,看着很像该放的地方),
      但活体实测 **帖子页压根注入不到那一份** —— 于是规则漂亮、选择器也对、就是不生效。
      READ_CSS 是「帖子页一定会注入」的那一份(applyReadCss 注入、id 以 -read 结尾),
      所以收在这段里。(同类的坑:本文件上方写着「READ_CSS 的规则全挂在 html.spp-read-on 上,
      而版块页不加这个类 ⇒ 注入也是死的」——正好是这条的镜像。)
   ★★ 靶子必须是**包着它们的元素**,不能是 <a> 自己:只写 a[title="顶端"]{display:none} 时,
      规则进了 CSSOM、computed 却仍是 inline-flex —— 因为脚本自己另一条规则
      (#main > form.spp-flow .tiptop > .fr > a { display:inline-flex !important })
      把**父级**点亮了;父级一 inline-flex,被 display:none 的子 a 只是「不画自己」,
      在 flex 行里仍然占位。所以用 :has() 直接掐父级。
   ★★ 两个父级**不是同一种东西**(第二处实测纠偏):
        ·「顶端」的父级是裸 <span>          → span:has(> a[title="顶端"])
        ·「小 中 大」的父级**就是 DIV.fr**  → .tiptop > .fr:has(> a[onclick^="fontsize("])
      给小中大写 span:has(…) 时五种写法实测 count 全为 0(它们外面没有 span)。
   ★ 这两条是**纯 CSS 兜底**;15i 里那段 JS 是加强版,还会把因此变空的分隔符链收拾干净。 */
html.spp-read-on .tiptop span:has(> a[title="顶端"]) { display:none !important; }
html.spp-read-on .tiptop > .fr:has(> a[onclick^="fontsize("]) { display:none !important; }
/* 预览面板与文本框同位置同宽(脚本切换预览时显示这一块)——
   几何/边框跟文本框对齐,display 与 min-height 由脚本内联控制,这里一个都不碰 */
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(textarea[name="atc_content"]) > .spp-preview-pane {
    box-sizing: border-box !important;
    width: 100% !important;
    max-width: 100% !important;
    min-width: 0 !important;
    margin: 0 !important;
    border: 1px solid var(--spu-border,#d8dee7) !important;
    border-top: 0 !important;
    border-radius: 0 0 10px 10px !important;
    background-color: var(--spu-surface,#ffffff) !important;
    color: var(--spu-text,#1f2937) !important;
}


/* ==========================================================================
   4. 提交行:「提 交 / 预览」与「字数检查 / 恢复数据 / 存为草稿 / 草稿箱」
          的 UI 包裹(批注 4)
   --------------------------------------------------------------------------
   站点结构(实测,中栏 TD.f_one 的最后一个 DIV,行内 style="margin:5px 0",
             盒子 257.53..993.13  宽 735.59  高 24):
     DIV[float:right 宽 290.36]  > A.abtn 字数检查 / A.abtn 恢复数据 / A.abtn 存为草稿
                                 / A.abtn#newdraft 草稿箱
     DIV[float:left  宽 131.02]  > 裸文本「按 Ctrl+Enter 直接提交」(站点行内是 #FF0000 红字)
     INPUT.btn[name="Submit"]    宽 65.34  高 19.67   x 388.55..453.90
     BUTTON#spp-preview-btn      宽 68.68  高 24      x 463.90..532.57(脚本产物,插在提交按钮之后)
   ★★ 「提交 + 预览」【没有共同的父元素】(两者是那个 DIV 的直接子节点,夹在提示 div 后面),
      所以纯 CSS 只能把【整行】做成一块 UI 托盘(本草案),做不到给这一对单独套一个框。
      要两个独立框必须 JS 包一层(草案见 REPORT.md 第 4.2 节,样式见本文件 5b 段)。
   ★★ 托盘用 overflow:hidden 收住两个浮动子块(不然托盘底色不被撑开),
      同时它不会剪到表情面板的弹层(#menu_face 挂在右栏,不在本行内)。
   ========================================================================== */
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(textarea[name="atc_content"]) > div:has(input[name="Submit"]) {
    box-sizing: border-box !important;
    display: block !important;
    overflow: hidden !important;
    width: 100% !important;
    max-width: 100% !important;
    height: auto !important;
    margin: 10px 0 0 !important;             /* 原行内 margin:5px 0 */
    padding: 5px 8px !important;
    background-color: var(--spu-overlay,#eef2f5) !important;
    background-image: none !important;
    border: 1px solid var(--spu-border,#d8dee7) !important;
    border-radius: 10px !important;
    color: var(--spu-secondary,#475569) !important;
    font-size: 12px !important;
}

/* ---- 4a. 右侧那一簇(字数检查…):收成一条内联胶囊组 ---- */
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(textarea[name="atc_content"]) > div:has(input[name="Submit"]) > div:has(> a.abtn) {
    display: inline-flex !important;         /* 浮动块会 blockify,仍是 flex,宽度收缩适应 */
    align-items: center !important;
    gap: 6px !important;
    float: right !important;
    height: auto !important;
    margin: 0 !important;
    padding: 0 !important;
    border: 0 !important;
    background: transparent none !important;
    box-shadow: none !important;
}

/* ---- 4b. 四枚功能胶囊 ---- */
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(textarea[name="atc_content"]) > div:has(input[name="Submit"]) > div:has(> a.abtn) > a.abtn {
    box-sizing: border-box !important;
    display: inline-flex !important;
    align-items: center !important;
    justify-content: center !important;
    height: 26px !important;
    min-height: 26px !important;
    margin: 0 !important;
    padding: 0 12px !important;
    border: 1px solid var(--spu-border,#d8dee7) !important;
    border-radius: 999px !important;
    background-color: var(--spu-surface,#ffffff) !important;
    background-image: none !important;
    color: var(--spu-secondary,#475569) !important;
    font-family: inherit !important;
    font-size: 12px !important;
    font-weight: 600 !important;
    line-height: 1 !important;
    text-decoration: none !important;
    text-shadow: none !important;
    white-space: nowrap !important;
    cursor: pointer !important;
    box-shadow: none !important;
    transition: background-color .18s ease, border-color .18s ease, color .18s ease !important;
}
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(textarea[name="atc_content"]) > div:has(input[name="Submit"]) > div:has(> a.abtn) > a.abtn:hover {
    background-color: color-mix(in srgb, var(--spu-accent,#2f6f68) 10%, transparent) !important;
    border-color: var(--spu-accent,#2f6f68) !important;
    color: var(--spu-accent,#2f6f68) !important;
    text-decoration: none !important;
}

/* ---- 4c. 「提 交」:主行动按钮(站点原皮是 187,187,187 灰底白字) ---- */
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(textarea[name="atc_content"]) > div:has(input[name="Submit"]) > input[name="Submit"] {
    box-sizing: border-box !important;
    display: inline-block !important;
    height: 26px !important;
    min-width: 78px !important;
    margin: 0 0 0 6px !important;
    padding: 0 18px !important;
    border: 1px solid var(--spu-accent,#2f6f68) !important;
    border-radius: 999px !important;
    background-color: var(--spu-accent,#2f6f68) !important;
    background-image: none !important;
    color: #ffffff !important;
    font-family: inherit !important;
    font-size: 12.5px !important;
    font-weight: 600 !important;
    line-height: 1 !important;
    letter-spacing: .08em !important;
    text-align: center !important;
    text-shadow: none !important;
    vertical-align: middle !important;
    cursor: pointer !important;
    box-shadow: 0 2px 8px rgba(47,111,104,.22) !important;
    transition: background-color .18s ease, border-color .18s ease, box-shadow .18s ease !important;
}
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(textarea[name="atc_content"]) > div:has(input[name="Submit"]) > input[name="Submit"]:hover {
    background-color: var(--spu-accent-dim,#255a55) !important;
    border-color: var(--spu-accent-dim,#255a55) !important;
    box-shadow: 0 3px 12px rgba(47,111,104,.30) !important;
}

/* ---- 4d. 「预览」按钮:只统一几何形状,颜色一律留给脚本内联 ----
   ★★ 这一条【故意不写 background / color / border-color】:
      脚本在 mouseenter/leave 与「预览激活」时改的是【内联样式】,
      而本文件全是 !important ⇒ 只要我这里写了颜色,就会把脚本的
      「已激活(实心)」状态一起盖成白色,按钮会看着像没反应。
      这里只覆盖 border-radius / height / padding / 字体(内联的 5px 圆角被 999px 盖掉)。 */
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(textarea[name="atc_content"]) > div:has(input[name="Submit"]) > #spp-preview-btn {
    box-sizing: border-box !important;
    display: inline-block !important;
    height: 26px !important;
    margin: 0 0 0 6px !important;
    padding: 0 16px !important;
    border-radius: 999px !important;
    font-family: inherit !important;
    font-size: 12.5px !important;
    font-weight: 600 !important;
    line-height: 1 !important;
    letter-spacing: .04em !important;
    text-shadow: none !important;
    vertical-align: middle !important;
    cursor: pointer !important;
}

/* ---- 4e. 「按 Ctrl+Enter 直接提交」提示:站点行内是 #FF0000 红字,改成低对比度说明文字 ---- */
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(textarea[name="atc_content"]) > div:has(input[name="Submit"]) > div:not(:has(a)) {
    float: left !important;
    height: 26px !important;
    line-height: 26px !important;
    margin: 0 !important;
    padding: 0 !important;
    color: var(--spu-muted,#6b7280) !important;
    font-size: 11.5px !important;
}


/* ==========================================================================
   5. 附录 A 的 JS 版样式(批注 1 的「文字装进胶囊」形态)
   --------------------------------------------------------------------------
   只有跑了 REPORT.md 第 4.1 节那段 JS(把每个「复选框 + 紧随文本节点」包成
   label.spp-fr-opt)之后才会命中;没跑 JS 时本段选择器匹配数为 0,零副作用。
   ★ 行距提醒:胶囊高 20 + marginTop 1 = 21 的 margin 盒 ⇒ 每行节距从 19.4 涨到 23.4
     (+4px/行 × 6 行 ≈ 整块高 24px)。要零增长的把 height 改成 18px。
   ========================================================================== */
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(input[name="atc_usesign"]) label.spp-fr-opt {
    box-sizing: border-box !important;
    display: inline-flex !important;
    align-items: center !important;
    gap: 6px !important;
    height: 20px !important;
    margin: 1px 6px 1px 0 !important;
    padding: 0 10px 0 7px !important;
    border: 1px solid var(--spu-border,#d8dee7) !important;
    border-radius: 999px !important;
    background-color: var(--spu-surface,#ffffff) !important;
    background-image: none !important;
    color: var(--spu-secondary,#475569) !important;
    font-size: 12px !important;
    line-height: 1 !important;
    white-space: nowrap !important;
    cursor: pointer !important;
    user-select: none !important;
    transition: background-color .16s ease, border-color .16s ease, color .16s ease !important;
}
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(input[name="atc_usesign"]) label.spp-fr-opt:hover {
    border-color: var(--spu-accent,#2f6f68) !important;
    color: var(--spu-accent,#2f6f68) !important;
}
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(input[name="atc_usesign"]) label.spp-fr-opt:has(input:checked) {
    border-color: var(--spu-accent,#2f6f68) !important;
    background-color: color-mix(in srgb, var(--spu-accent,#2f6f68) 10%, transparent) !important;
    color: var(--spu-accent,#2f6f68) !important;
    font-weight: 600 !important;
}
/* 胶囊内的勾选块:缩到 14px、去掉外边距(1a 那条的 margin 在这里必须清掉) */
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(input[name="atc_usesign"]) label.spp-fr-opt > input[type="checkbox"] {
    width: 14px !important;
    height: 14px !important;
    margin: 0 !important;
    vertical-align: middle !important;
    flex: 0 0 auto !important;
}


/* ---- 5b. 第 4.2 节那个函数的样式:给「提 交 + 预览」单独套一个框(变体 B)----
   ★★ 只有跑了那段 JS(生成 span.spp-fr-actions)之后才会命中。
      命中后:整行托盘退成透明(4a 的底色/内边距让位),左右两簇各自成为一个胶囊框 ——
      这正是用户截图里两个黄框的样子。没跑 JS 时 4a 的单托盘形态保留。
   ★ 这一组选择器比 4a/4b 多了一层 :has(> .spp-fr-actions),同权重下靠后 + 特指更高 ⇒ 覆盖 4a。 */
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(textarea[name="atc_content"]) > div:has(input[name="Submit"]):has(> .spp-fr-actions) {
    padding: 0 !important;
    border: 0 !important;
    background: transparent none !important;
}
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(textarea[name="atc_content"]) > div:has(input[name="Submit"]):has(> .spp-fr-actions) > div:has(> a.abtn) {
    height: 26px !important;
    padding: 0 6px 0 10px !important;
    border: 1px solid var(--spu-border,#d8dee7) !important;
    border-radius: 999px !important;
    background-color: var(--spu-surface,#ffffff) !important;
}
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(textarea[name="atc_content"]) > div:has(input[name="Submit"]) > span.spp-fr-actions {
    box-sizing: border-box !important;
    display: inline-flex !important;
    align-items: center !important;
    gap: 6px !important;
    height: 26px !important;
    margin: 0 0 0 8px !important;
    padding: 0 5px 0 0 !important;
    border: 1px solid var(--spu-border,#d8dee7) !important;
    border-radius: 999px !important;
    background-color: var(--spu-surface,#ffffff) !important;
    vertical-align: middle !important;
}
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(textarea[name="atc_content"]) > div:has(input[name="Submit"]) > span.spp-fr-actions > input[name="Submit"] {
    margin: 0 0 0 8px !important;
}
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(textarea[name="atc_content"]) > div:has(input[name="Submit"]) > span.spp-fr-actions > #spp-preview-btn {
    margin: 0 !important;
}
html.spp-read-on div.t5:has(textarea[name="atc_content"]) td.f_one:has(textarea[name="atc_content"]) > div:has(input[name="Submit"]):has(> .spp-fr-actions) > div:not(:has(a)) {
    height: 26px !important;
    line-height: 26px !important;
}


/* ==========================================================================
   2.53.1:帖子页「悬停头像弹层」(用户状态卡)—— 站点默认是 1px 灰边 + 直角 + 零阴影的白盒
   --------------------------------------------------------------------------
   ★ 站点结构(真页面实测,thread 2948255):
       span.user-info#showface_<pid>
         └ .user-infoWrap
             ├ span.co                       ← 10×20 的 user-info-co.gif 灰箭头
             ├ div.listread > ul > li×4       ← 作者资料 / 发送短消息 / 加为好友 / 使用道具
             ├ div.c  (内联 width:120;height:5)  ← 站点自己的占位 spacer
             └ 裸文本行「UID: <span class=f12>…</span><br>…」×9
   ★ 那 9 行是**裸文本 + <br>** 拼的,CSS 没法只给「标签」上色 ⇒ 由 initUserStatusCard()
     把它们重排成 .spp-uc__row(左标签 / 右值),本段只负责外观。
   ★ 为什么把站点那枚 .co 箭头藏掉:圆角卡配 1px 灰箭头很脏,改用 ::before 画一枚同色小三角,
     仍指向左侧头像那一列,视觉上「卡片是从头像长出来的」这件事没丢。
   ★ 全部按 var(--spu-*) 之外的固定浅色写:本卡必须**恒为白底**(用户 2.53.1 定案),
     不跟随星空/毛玻璃主题的深底 —— 它是浮动在帖子上的白纸,深底反而糊。
   ★★ 关键实测(2.53.1 第一次活体自检当场抓到的,写在这里免得下次又踩):
      站点 read.open() 显示的**不是**那个 SPAN 本体,而是它在 <body> 末尾新建的浮层
      **div.user-info**(内联 opacity:.95 / left / top / z-index:3000 / visibility:visible),
      内容是本体 DOM 的克隆。于是选择器**绝对不能写 span.**:
        · 本体 = span.user-info#showface_<pid>(躲在 th.r_two 里,只当数据源与 read.open 的锚点)
        · 显示 = div.user-info(克隆,classList 与本体现状一致 ⇒ 也带 .spp-ucard)
      第一版只写 span. 时的症状:悬停出来的那张卡仍是站点原样(直角 / 灰边 / 标签值与值挤在一起),
      而屏幕左下角还杵着一张被我们美化过的**本体**,看着像「只生效了一半」。
   ★ 克隆是**悬停那一刻**从本体现状克隆的 ⇒ 我们的重排只需做在本体上,克隆自动带上 .spp-uc__rows,
      不需要 show 时再跑一遍(实测克隆内 .spp-uc__row 同样是 9 行)。
   ========================================================================== */
html.spp-read-on .user-info.spp-ucard {
    z-index:9999 !important;
    opacity:1 !important;          /* 压掉克隆体那行内联 opacity:.95,白卡要够白 */
}
html.spp-read-on .user-info.spp-ucard > .user-infoWrap {
    box-sizing:border-box !important;
    width:auto !important; min-width:198px !important; max-width:280px !important;
    margin:0 !important; padding:8px 12px 10px !important;
    border:1px solid #e6ecf5 !important; border-radius:12px !important;
    background-color:#ffffff !important; background-image:none !important;
    box-shadow:0 10px 26px rgba(15,23,42,.14), 0 2px 6px rgba(15,23,42,.06) !important;
    font-family:inherit !important; font-size:12px !important; line-height:1 !important;
    color:#334155 !important; position:relative !important;
}
html.spp-read-on .user-info.spp-ucard > .user-infoWrap > .co { display:none !important; }
html.spp-read-on .user-info.spp-ucard > .user-infoWrap::before {
    content:"" !important; position:absolute !important; left:-5.5px !important; top:15px !important;
    width:9px !important; height:9px !important; background-color:#ffffff !important;
    border-left:1px solid #e6ecf5 !important; border-bottom:1px solid #e6ecf5 !important;
    transform:rotate(45deg) !important; border-radius:0 0 0 2px !important;
}
html.spp-read-on .user-info.spp-ucard > .user-infoWrap > div.c { display:none !important; }
html.spp-read-on .user-info.spp-ucard .listread { height:auto !important; }
html.spp-read-on .user-info.spp-ucard .listread ul {
    display:flex !important; align-items:stretch !important; gap:2px !important;
    height:auto !important; margin:0 0 8px !important; padding:3px !important;
    list-style:none !important;
    background-color:#f8fafc !important; border:1px solid #edf1f7 !important; border-radius:9px !important;
}
/* ===== 2.53.3:图标行 = 线性 SVG + 常驻短标签(B 方案,用户拍板) =====
   ★ 这四条取代了 2.53.1 的「li 自身带 padding + hover 底色」写法:
     位图换成 SVG 后每格要「图标在上、文字在下」,靠 padding 撑不出居中的两行,
     所以把 padding 与 hover 都挪到 <a> 上(<a> 撑满整格 ⇒ 热区没变小)。
   ★ 特异性提醒(预览阶段真踩过):基线是
     html.spp-read-on .user-info.spp-ucard .listread li > a{(0,4,3)}
     —— 想再往上盖一条别的布局,光加 !important 没用,必须把方案类挂到卡片根上凑到 (0,5,3)。 */
html.spp-read-on .user-info.spp-ucard .listread li {
    display:block !important; float:none !important; flex:1 1 0 !important; min-width:0 !important;
    width:auto !important; height:auto !important;
    margin:0 !important; padding:0 !important; border-radius:7px !important;
}
html.spp-read-on .user-info.spp-ucard .listread li > a {
    display:flex !important; flex-direction:column !important; align-items:center !important;
    justify-content:center !important; gap:3px !important;
    padding:6px 2px 5px !important; border-radius:7px !important; cursor:pointer !important;
    color:#475569 !important; transition:background-color .14s ease, color .14s ease !important;
}
html.spp-read-on .user-info.spp-ucard .listread li:hover > a {
    background-color:#eef4fb !important; color:#2563eb !important;
}
/* 站点那枚 2008 年位图(readicon.png 16×180 精灵):只藏不摘,保住站点脚本的引用关系 */
html.spp-read-on .user-info.spp-ucard .listread li > a > div.spp-uc-old { display:none !important; }
html.spp-read-on .user-info.spp-ucard .listread .spp-uc__ico {
    display:block !important; width:16px !important; height:16px !important; flex:0 0 auto !important;
}
html.spp-read-on .user-info.spp-ucard .listread .spp-uc__ico svg {
    display:block !important; width:16px !important; height:16px !important;
}
html.spp-read-on .user-info.spp-ucard .listread .spp-uc__lbl {
    font-size:10.5px !important; line-height:1 !important; letter-spacing:.2px !important;
    white-space:nowrap !important; overflow:hidden !important; text-overflow:ellipsis !important;
    max-width:100% !important; color:inherit !important; text-indent:0 !important;
}
html.spp-read-on .user-info.spp-ucard .spp-uc__rows {
    display:flex !important; flex-direction:column !important; gap:0 !important;
    padding:4px 9px !important;
    background-color:#f8fafc !important; border:1px solid #edf1f7 !important; border-radius:9px !important;
}
html.spp-read-on .user-info.spp-ucard .spp-uc__row {
    display:flex !important; align-items:baseline !important;
    justify-content:space-between !important; gap:12px !important;
    padding:3.5px 0 !important;
}
html.spp-read-on .user-info.spp-ucard .spp-uc__row + .spp-uc__row {
    box-shadow:inset 0 1px 0 #eaeff7 !important;
}
html.spp-read-on .user-info.spp-ucard .spp-uc__k {
    color:#94a3b8 !important; font-size:11.5px !important; letter-spacing:.2px !important; white-space:nowrap !important;
}
html.spp-read-on .user-info.spp-ucard .spp-uc__v {
    color:#1f2937 !important; font-size:12px !important; font-weight:600 !important; text-align:right !important;
    font-variant-numeric:tabular-nums !important;
}
html.spp-read-on .user-info.spp-ucard .spp-uc__row.is-sp .spp-uc__v {
    color:#a4670f !important;
    background-color:#fff5e2 !important; border:1px solid #f3e2c2 !important;
    border-radius:999px !important; padding:1px 7px !important;
}
html.spp-read-on .user-info.spp-ucard .spp-uc__row.is-post .spp-uc__v { color:#2f6f68 !important; }


/* ==========================================================================
   6. 未采用 / 待用户拍板(写在这里免得被当成漏项)
   --------------------------------------------------------------------------
   (1) 「HTML 代码不可用」那一行:无控件、无元素包裹,纯 CSS 取不到,本次不动。
   (2) 左右两栏宽 249.86 / 中栏 749.59 是站点表格布局,本次不动(批注没提)。
   (3) 右栏表情面板 fieldset#smiliebox.spp-emoji-fieldset 实测宽 260、左缘 x1003.1,
       右缘 1263.1 —— 比右栏(1000.13..1250)【溢出 13.1px】。批注没圈它,
       本次只记录不改(改它是另一轮的事:要么收窄到 235,要么让它自适应)。
   (4) 顶部那行 TD.h2「快速发帖 / 顶端」沿用站点原样,本次不动。
   ========================================================================== */
`;

        // ---------- 2.52.52:搜索页顶部的收尾样式 ----------
        // ★ 只写搜索页真正缺的几条,不搬帖子页那一整套(spp-read-on):
        //   ① #header 的浅灰底去掉(横幅本身由 applyBanner 的 display:none 收掉)——
        //      蓝框里「灰底 + logo」两样一起去掉,才算真的屏蔽干净;
        //   ② #main > .bdbA(黄框那条 27px 灰带)去底去边 —— 面包屑搬走后它就是空壳,由 moveCrumb 收起;
        //   ③ 面包屑进了深色卡右格,站点那套浅底黑字会看不见,要换成浅字 + 主色分隔符。
        //      ★ 与帖子页同款观感(帖子页那几条写在 READ_CSS 里、由 html.spp-read-on 开关)。
        const SEARCH_CSS = `
html.spp-search-on #header { background:none !important; }
html.spp-search-on #main > .bdbA { background:none !important; border:0 !important; }
html.spp-search-on #infobox.spp-up-on td.spp-up-host-td > .spp-crumb-line { margin:0 0 9px !important; min-width:0 !important; }
html.spp-search-on .spp-crumb-line > #breadcrumbs {
    display:flex !important; align-items:center !important; gap:2px !important;
    box-sizing:border-box !important; min-height:20px !important; height:auto !important;
    margin:0 !important; padding:0 !important;
    background:none !important; background-image:none !important; border:0 !important;
    box-shadow:none !important; border-radius:0 !important;
    font-size:12.5px !important; color:#cfdcee !important; overflow:hidden !important;
}
html.spp-search-on .spp-crumb-line .crumbs-item {
    display:inline-flex !important; align-items:center !important; padding:0 !important;
    background:none !important; background-image:none !important;
    font-size:12.5px !important; line-height:1.2 !important;
    min-width:0 !important; overflow:hidden !important;
    white-space:nowrap !important; text-overflow:ellipsis !important;
}
html.spp-search-on .spp-crumb-line .crumbs-item i { display:none !important; }
html.spp-search-on .spp-crumb-line .crumbs-item.gray3 {
    color:#8b9ab1 !important; -webkit-text-fill-color:#8b9ab1 !important; text-decoration:none !important;
}
html.spp-search-on .spp-crumb-line .crumbs-item.gray3:hover {
    color:#e2e8f0 !important; -webkit-text-fill-color:#e2e8f0 !important;
}
html.spp-search-on .spp-crumb-line .crumbs-item + .crumbs-item::before {
    content:"›" !important; display:inline-block !important; flex:none !important;
    margin:0 7px !important; padding:0 !important;
    color:rgba(32,201,151,.72) !important; -webkit-text-fill-color:rgba(32,201,151,.72) !important;
    font-size:15px !important; font-weight:400 !important; line-height:1 !important;
    vertical-align:middle !important;
}
html.spp-search-on .spp-crumb-line .crumbs-item.current {
    flex:0 1 auto !important; min-width:0 !important; overflow:hidden !important;
    white-space:nowrap !important; text-overflow:ellipsis !important;
    color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important; font-weight:600 !important;
}
html.spp-search-on .spp-crumb-line .crumbs-item.current > strong {
    color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important; font-weight:600 !important;
}
/* ---- 2.52.53:站点那行「热门版块: 茶馆 › 询问&求物 › …」的字色 + 卡片标题换行 ----
   ★ 这行为什么在搜索页是【露着】的:把它藏起来的那条
       #infobox.spp-up-on td.spp-up-host-td > #fav-fid { display:none }
     写在 SPP-HOTBOARD 那个大盘子里,而它挂在 initThreadListConvert() 内、非版块页不注入
     (帖子页也是因为这个才在 READ_CSS 里另写了一份同样的规则)⇒ 搜索页两份样式都吃不到。
   ★ 它为什么又和卡片标题叠在一行:行内有 float:left 的 span#shortcutname,容器高度塌陷成
     只剩 padding-top 的 5px,紧随其后的 .hb-wrap / .hb-hd 便贴着它的行盒排。
   ★ 用户要求:这行的字改亮绿(原来是主题压过的暗绿 rgb(47,111,104)),卡片标题也换同一枚亮绿并另起一行。 */
html.spp-search-on #infobox.spp-up-on td.spp-up-host-td > #fav-fid {
    display:block !important;
    padding:0 !important; margin:0 0 10px !important;
    overflow:hidden !important;        /* 建 BFC:把内部浮动收进容器,这行从此独占一整行 */
}
html.spp-search-on #infobox.spp-up-on td.spp-up-host-td > #fav-fid #shortcutname {
    color:#7de8b0 !important; -webkit-text-fill-color:#7de8b0 !important;
}
html.spp-search-on #infobox.spp-up-on td.spp-up-host-td > #fav-fid #shortcutforum > li > a {
    color:#3ef58c !important; -webkit-text-fill-color:#3ef58c !important;
    font-weight:600 !important; text-decoration:none !important;
}
html.spp-search-on #infobox.spp-up-on td.spp-up-host-td > #fav-fid #shortcutforum > li > a:hover {
    color:#9dffc9 !important; -webkit-text-fill-color:#9dffc9 !important;
}
/* 卡片自己的标题/网格:搜索页同样吃不到 SPP-HOTBOARD 那份样式(它在 initThreadListConvert 里),
   帖子页当初也遇到同一个缺口(READ_CSS 里补的就是这几条)—— 这里按同一办法补齐。
   不补的话标题三个 span 会挤成一坨「热门版块6常用入口」贴在行尾,正是用户截图里那陀叠字。 */
html.spp-search-on #infobox.spp-up-on td.spp-up-host-td > .hb-wrap {
    width:100% !important; min-width:0 !important; clear:both !important;
}
html.spp-search-on #infobox.spp-up-on .hb-hd {
    display:flex !important; align-items:center !important; gap:9px !important; margin:0 0 11px !important;
}
html.spp-search-on #infobox.spp-up-on .hb-hd__n {
    font-size:11px !important; font-weight:700 !important; line-height:1.6 !important;
    color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
    padding:0 8px !important; border-radius:999px !important;
    background:rgba(32,201,151,.16) !important; border:1px solid rgba(32,201,151,.45) !important;
}
html.spp-search-on #infobox.spp-up-on .hb-grid .spp-hm-stat { text-decoration:none !important; }
html.spp-search-on #infobox.spp-up-on .hb-grid .spp-hm-stat .v {
    white-space:nowrap !important; overflow:hidden !important; text-overflow:ellipsis !important;
}
/* 卡片标题:换行到独立一行 + 同一枚亮绿(用户另指:字体要更明显) */
html.spp-search-on #infobox.spp-up-on .hb-hd__t {
    font-size:14px !important;
    color:#3ef58c !important; -webkit-text-fill-color:#3ef58c !important;
}
html.spp-search-on #infobox.spp-up-on .hb-hd__hint {
    color:#7de8b0 !important; -webkit-text-fill-color:#7de8b0 !important;
}
/* ---- 2.53.16(用户选定「方案 A」):搜索页「提 交 / 重 置」= 与引用页「提交 / 预览」同一套胶囊语言 ----
   ★ 站点原样:提交走主题那条通用按钮(绿底 / 7px 圆角 / 13px),重置是一张红底位图
     (images/colorImagination/bg_btn.gif)—— 两颗一大一小、一新一旧。
     2.52.56–2.52.58 只把重置按提交的尺度重做,颜色仍是「一绿一红」。
     方案 A 把两颗收进同一套度量:26px 高 / 0 16px 内衬 / 999 胶囊 / 12px 600。
   ★ 为什么必须写 background-image:none:站点的底色是**背景位图**(2.53.11 在提交按钮上踩过同一个坑),
     只写 background-color 会被位图盖住。
   ★ 为什么 -webkit-text-fill-color 两颗都要写:提交要 #fff、重置要主色;不写会被上一层白字规则串色。
   ★ 提交的选择器不写 :has(...) 锁行:站点这张表单里提交与重置可能不在同一个 div,
     写死行内关系时另一颗会漏网(留成主题绿)。这里按「#main 内该表单的提交」认,表单里只有这一枚。
   ★ 过渡只挂 :hover(常驻 transition 会让后台标签页的计算值停在起始值,本文件踩过两次)。 */
html.spp-search-on #main form input[type="submit"],
html.spp-search-on #main > form > div > input[type="reset"] {
    box-sizing:border-box !important;
    height:26px !important; min-height:0 !important;
    padding:0 16px !important;
    border-radius:999px !important;
    background-image:none !important;
    font-family:inherit !important; font-size:12px !important;
    font-weight:600 !important; line-height:1 !important;
    letter-spacing:normal !important; text-indent:0 !important; text-shadow:none !important;
    vertical-align:middle !important; cursor:pointer !important;
    opacity:1 !important; filter:none !important;
}
html.spp-search-on #main form input[type="submit"] {
    border:1px solid var(--spu-accent,#2f6f68) !important;
    background-color:var(--spu-accent,#2f6f68) !important;
    color:#fff !important; -webkit-text-fill-color:#fff !important;
}
html.spp-search-on #main > form > div > input[type="reset"] {
    margin-left:10px !important;
    border:1px solid var(--spu-accent,#2f6f68) !important;
    background-color:var(--spu-surface,#fff) !important;
    color:var(--spu-accent,#2f6f68) !important;
    -webkit-text-fill-color:var(--spu-accent,#2f6f68) !important;
}
html.spp-search-on #main form input[type="submit"]:hover {
    transition:all .18s ease !important;
    background-color:color-mix(in srgb, var(--spu-accent,#2f6f68) 84%, #000) !important;
    border-color:color-mix(in srgb, var(--spu-accent,#2f6f68) 84%, #000) !important;
    transform:translateY(-1px) !important;
    box-shadow:0 4px 12px -3px rgba(47,111,104,.5) !important;
}
html.spp-search-on #main > form > div > input[type="reset"]:hover {
    transition:all .18s ease !important;
    background-color:#eef6f4 !important;
    border-color:color-mix(in srgb, var(--spu-accent,#2f6f68) 84%, #000) !important;
    transform:translateY(-1px) !important;
}
html.spp-search-on #main form input[type="submit"]:active,
html.spp-search-on #main > form > div > input[type="reset"]:active {
    transition:all .06s !important;
    transform:translateY(0) !important;
}
`;

        function applySearchCss() {
            if (!PAGE.search) return;
            if (document.getElementById(STYLE_ID + '-search')) return;   // 只注入一次(本模块会重跑)
            const el = document.createElement('style');
            el.id = STYLE_ID + '-search';
            el.textContent = SEARCH_CSS;
            appendToHeadSafe(el);
        }

        function applyReadCss() {
            if (!WANTS.readOnly) return;
            if (document.getElementById(STYLE_ID + '-read')) return;      // 只注入一次(本模块会重跑)
            const el = document.createElement('style');
            el.id = STYLE_ID + '-read';
            el.textContent = READ_CSS;
            appendToHeadSafe(el);
        }

        // ---------- 2.52.79:任务页要补的只有「热门版块卡片」这一段 ----------
        // ★ 卡片网格本体(.spp-hm-stats / .spp-hm-stat)与用户卡都在 CSS_UPANEL 里,
        //   applyCard() 已经注入;这里要补的是 .hb-wrap / .hb-hd 那几条 +
        //   #fav-fid 的隐藏 —— 它们原本只长在 initThreadListConvert() 里(版块页专属)。
        //   不补就会出现「站点那行热门版块」与卡片叠在一起的陀叠字(搜索页 2.52.53 踩过)。
        function applyHotCss() {
            // 2.52.90:发帖页也吃这一段(.hb-wrap/.hb-hd + #fav-fid 的隐藏)——
            //   不然站点那条「热门版块: …」会与卡片叠成陀叠字(搜索页 2.52.53 踩过)。
            if (!PAGE.task && !PAGE.post) return;
            if (document.getElementById(STYLE_ID + '-hot')) return;       // 只注入一次(本模块会重跑)
            const el = document.createElement('style');
            el.id = STYLE_ID + '-hot';
            el.textContent = CSS_HOTBOARD;
            appendToHeadSafe(el);
        }

        // ---------- 2.52.90:发帖页(post.php)顶部「使用之前的模版」----------
        // 黄框那条批注(post.php?fid-9)指的是:本页的 #infobox 用户条 + 面包屑,
        // 要长得跟帖子页/版块页那张现成的卡一样。所以这里【不新画样式】——只把 READ_CSS
        // 里那两段(页面底色 + 面包屑磨砂玻璃板块)在本页自己的 scope 下重写一遍;
        // 卡本体走 CSS_UPANEL、热门版块那段走 CSS_HOTBOARD,两者的选择器本来就是
        // 页面无关的(html.spp-hm-star #infobox.spp-up-on …),不必也不能再抄一份。
        // ★ 为什么是「重复」而不是把两条 scope 并成一个选择器:两页各留各的 scope,
        //   改一边不会波及另一边,回归面最清楚;SEARCH_CSS 当初也是照此办理(见上面那段注释)。
        // ★ 内容与 READ_CSS 逐条对应,只抄本页有对应元素的:翻页条 / 回帖·发新帖按钮 /
        //   顶部浅灰工具条(浏览器收藏·打印)/ 站点白底弹出面板 这几段本页没有,略过。
        const POST_CSS = `
/* 面包屑那一行(#main > .bdbA)原先是站点 27px 浅灰条,卡片化后这一层底色要退掉 */
html.spp-post-on #main > .bdbA { background:none !important; border:0 !important; }
/* ===== 面包屑板块 —— 与帖子页同一份磨砂黑玻璃配方(2.52.60 定案)===== */
html.spp-post-on #main > .bdbA.spp-crumb-panel {
    box-sizing:border-box !important;
    margin:10px 0 8px !important; padding:6px 12px !important;
    border-radius:12px !important; border:1px solid rgba(255,255,255,.10) !important;
    background-color:rgb(0 0 0 / .86) !important;
    background-image:linear-gradient(152deg, rgba(255,255,255,.03) 0%, rgba(0,0,0,0) 55%) !important;
    -webkit-backdrop-filter:blur(18px) saturate(140%) !important;
    backdrop-filter:blur(18px) saturate(140%) !important;
    box-shadow:0 10px 26px rgba(0,0,0,.34), inset 0 1px 0 rgba(255,255,255,.05) !important;
}
/* #breadcrumbs 退成板块里的透明内容行 */
html.spp-post-on #breadcrumbs {
    display:flex !important; align-items:center !important; gap:2px !important;
    box-sizing:border-box !important; min-height:34px !important; height:auto !important;
    margin:0 !important; padding:0 !important;
    background:none !important; background-image:none !important;
    border:0 !important; border-radius:0 !important; box-shadow:none !important;
    font-size:13px !important; color:#cfdcee !important; overflow:hidden !important;
}
html.spp-post-on #breadcrumbs .crumbs-item {
    display:inline-flex !important; align-items:center !important; padding:0 !important;
    background:none !important; background-image:none !important;
    font-size:13px !important; line-height:1.2 !important;
    min-width:0 !important; overflow:hidden !important;
    white-space:nowrap !important; text-overflow:ellipsis !important;
}
html.spp-post-on #breadcrumbs .crumbs-item i { display:none !important; }
html.spp-post-on #breadcrumbs .crumbs-item > img,
html.spp-post-on #breadcrumbs #td_cate {
    width:15px !important; height:15px !important; margin:0 9px 0 2px !important;
    filter:invert(1) brightness(1.5) opacity(.66) !important;
}
html.spp-post-on #breadcrumbs .crumbs-item.gray3 {
    color:#8b9ab1 !important; -webkit-text-fill-color:#8b9ab1 !important; text-decoration:none !important;
}
html.spp-post-on #breadcrumbs .crumbs-item.gray3:hover {
    color:#e2e8f0 !important; -webkit-text-fill-color:#e2e8f0 !important;
}
html.spp-post-on #breadcrumbs .crumbs-item + .crumbs-item::before {
    content:"›" !important; display:inline-block !important; flex:none !important;
    margin:0 8px !important; padding:0 !important;
    color:rgba(32,201,151,.72) !important; -webkit-text-fill-color:rgba(32,201,151,.72) !important;
    font-size:16px !important; font-weight:400 !important; line-height:1 !important;
    vertical-align:middle !important; transform:none !important;
}
html.spp-post-on #breadcrumbs .crumbs-item.current {
    flex:0 1 auto !important; min-width:0 !important; overflow:hidden !important;
    white-space:nowrap !important; text-overflow:ellipsis !important;
    color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important; font-weight:600 !important;
}
html.spp-post-on #breadcrumbs .crumbs-item.current > strong,
html.spp-post-on #breadcrumbs .crumbs-item.current > strong > a {
    display:inline-block !important; min-width:0 !important; max-width:100% !important;
    overflow:hidden !important; white-space:nowrap !important; text-overflow:ellipsis !important;
    color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
    font-weight:600 !important; text-decoration:none !important;
}
/* 站点那条「热门版块: …」:只隐藏、不删除(站点 shortCut() 还要用它增删 <li>) */
html.spp-post-on #infobox.spp-up-on td.spp-up-host-td > #fav-fid { display:none !important; }
/* 面包屑尾部那枚 RSS(站点空图)→ 绿色胶囊(帖子页同款) */
html.spp-post-on #breadcrumbs .crumbs-item.current > a[href^="rss.php"] {
    display:inline-flex !important; align-items:center !important; height:20px !important;
    margin-left:8px !important; padding:0 8px !important; border-radius:999px !important;
    background-color:rgba(32,201,151,.14) !important; border:1px solid rgba(32,201,151,.45) !important;
    color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
    font-size:11px !important; font-weight:700 !important; letter-spacing:.4px !important;
    text-decoration:none !important; vertical-align:middle !important;
}
html.spp-post-on #breadcrumbs .crumbs-item.current > a[href^="rss.php"] img { display:none !important; }
html.spp-post-on #breadcrumbs .crumbs-item.current > a[href^="rss.php"]::after { content:"RSS" !important; }
html.spp-post-on #breadcrumbs .crumbs-item.current > a[href^="rss.php"]:hover {
    background-color:rgba(32,201,151,.26) !important; border-color:rgba(32,201,151,.75) !important;
    color:#fff !important; -webkit-text-fill-color:#fff !important;
}
/* ================= 2.52.91 发帖工作台 =================
   本轮批注(post.php?fid-9):红框=正文框(参照轴)、蓝框=选项/附件/提交
   「相对于红框的中心轴对称对齐」、两处「UI包裹」(WindCode 标签行 / 字数检查那一行)、
   黑箭头「移动」(求物提示行)、黄箭头「美化排版位置」(表情 legend,见表情模块)、
   「重复按钮」(预览×2)、黑框「改为:字色…」(见 BUTTON_GROUPS)、
绿圈「增加功能」(站点 #wy_post → [post];其中 [post] 那一枚 2.54.4 已按用户要求撤掉)。
   ★★ 蓝框为什么「偏轴」:站点在发帖表单里给「标签列」留了 200/205px 缩进
      (标题行 / 选项行 / 附件块 / 提交块的内层 div 都带 style="margin-left:20Xpx"),
      选项表自己又是 #main 通宽 1399 —— 实测内容左缘 347、中心 906,
      而正文框(2.52.90 起已是通栏)是 226..1192、中心 709。
      ⇒ 把这份缩进全撤,选项表收到 966 并与正文框同左缘,两轴自然重合。
   ★★ 顶栏两块卡(标签行 / 字数检查行)用 bottom:100% 挂在 #editor-content 上沿,
      而不是写 top:-22px:站点原来的 top:-22px 是配它自己 21.67px 的行高效的,
      行高一变(这里 22 → 28)就得连 top 一起改,写 bottom 则天然贴顶。 */
html.spp-post-on .t.f_one [style*="margin"][style*="205px"]:not(#postrule),
html.spp-post-on .t.f_one [style*="margin-left:200px"] {
    margin-left:0 !important;
    /* ★ 别写 width:966px:这几个 div 里还有表格(附件那张),写死 966 会把
       单元格撑到 990(966 + 单元格左右 padding 24),选项表跟着涨到 991、
       整块右缘跑到 1217 —— 实站第一版就是这么偏出 25px 的。
       表里那几条本来就该「占满父容器」,撤掉缩进就够了。 */
    width:auto !important;
    box-sizing:border-box !important;
}
/* ★ 选择器为什么要 [style*="margin"] + [style*="205px"] 两个条件:
   标题行写的是【四值省略式】margin:.5em auto 0 205px(没有 margin-left 字样),
   只按 [style*="margin-left:205px"] 一个条件去匹配会把它漏掉 —— 实站第一版量出来
   标题行还停在 431、与 226 起的正文框错开 205px。
   加 margin 这个条件是为了排掉整页星空装饰的 <I>(它的行内样式里带 200px
   但没有 margin;匹配上的话 width:auto 会把那些小点整片打散)。
   ★★ 这段注释里【绝对不能出现反引号】(2.52.91 就是这么炸的):本段是模板字面量
      的内容,一个反引号就会把它提前闭合,后面的 CSS 文本被当 JS 解析 ——
      而且 node --check 照样通过(源文件整体仍语法合法)、boardtest 的注释剥离
      也照样通过,只在运行期抛 TypeError: Assignment to constant variable,
      initBoardTop 整个模块被 safeInit 吞掉。守卫见 tools/verify/check-backticks.js。 */
html.spp-post-on .t.f_one > table:has(#post-option) {
    width:966px !important;
    margin:12px 0 0 79px !important;
    border:1px solid var(--spu-border,#d8dee7) !important;
    border-radius:12px !important;
    overflow:hidden !important;
    background:var(--spu-surface,#fff) !important;
    box-shadow:0 1px 2px rgba(15,23,42,.05), 0 12px 30px rgba(15,23,42,.08) !important;
    /* ★ 这条表是「单列布局表」,固定布局才能乖乖守 966:自动布局下
       单元格的最小内容宽度(附件内表 966 + padding)会把表顶到 991。 */
    table-layout:fixed !important;
}
html.spp-post-on .t.f_one > table:has(#post-option) > tbody > tr > td,
html.spp-post-on .t.f_one > table:has(#post-option) > tbody > tr > th {
    padding:7px 12px !important;
}
html.spp-post-on .t.f_one > table:has(#post-option) table { width:100% !important; }
html.spp-post-on .t.f_one > table:has(#post-option) .gray { color:var(--spu-muted,#6b7280) !important; }
html.spp-post-on #editor-content {
    border:1px solid var(--spu-border,#d8dee7) !important;
    border-radius:12px !important;
    background:var(--spu-surface,#fff) !important;
    box-shadow:0 1px 2px rgba(15,23,42,.05), 0 12px 30px rgba(15,23,42,.08) !important;
}
html.spp-post-on #editor-content #editor-button {
    border-radius:11px 11px 0 0 !important;
    background:var(--spu-overlay,#eef2f5) !important;
    border-bottom:1px solid var(--spu-border,#d8dee7) !important;
}
html.spp-post-on #editor-content .spp-editor-toolbar {
    border:0 !important;
    border-radius:0 !important;
    border-bottom:1px solid var(--spu-border,#d8dee7) !important;
    background:var(--spu-surface,#fff) !important;
}
html.spp-post-on #editor-content #textarea,
html.spp-post-on #editor-content .spp-preview-pane {
    border-radius:0 0 11px 11px !important;
    background:var(--spu-surface,#fff) !important;
}
html.spp-post-on #editor-tab,
html.spp-post-on #editor-tool {
    top:auto !important;
    bottom:100% !important;
    margin:0 !important;
    width:auto !important;
    height:28px !important;
    display:flex !important;
    align-items:center !important;
    padding:3px 5px !important;
    box-sizing:border-box !important;
    border:1px solid var(--spu-border,#d8dee7) !important;
    border-bottom:0 !important;
    /* 11px = 编辑器卡那 12px 圆角减掉它自己的 1px 边框 —— 两块卡上沿能连成一条弧。
       ★ 不要写成 10px 10px 0 0:2.52.81 的「表头圆角」用的是同一个字符串,
         负向用例 BJ 靠 replace(第一处) 定位,重了就会替错地方(本轮实测踩过)。 */
    border-radius:11px 11px 0 0 !important;
    background:var(--spu-overlay,#eef2f5) !important;
}
html.spp-post-on #editor-tab { left:0 !important; right:auto !important; gap:4px !important; }
html.spp-post-on #editor-tool { left:auto !important; right:0 !important; gap:2px !important; }
/* ★ 站点在 #editor-tab 里塞了个 .c(width:540 的清除浮动块),它一变成 flex item
   就把标签卡顶到 735px 宽(实测),把后面的提示行整段盖住 ⇒ 直接收掉(0 高,纯占位)。 */
html.spp-post-on #editor-tab > .c { display:none !important; }
html.spp-post-on #editor-tab > span,
html.spp-post-on #editor-tool > span {
    flex:0 0 auto !important;
    float:none !important;
    display:inline-flex !important;
    align-items:center !important;
    justify-content:center !important;
    width:auto !important;
    height:22px !important;
    margin:0 !important;
    padding:0 12px !important;
    border:0 !important;
    border-radius:7px !important;
    background:none !important;
    background-image:none !important;
    color:var(--spu-secondary,#475569) !important;
    font-size:12px !important;
    font-weight:600 !important;
    line-height:1 !important;
    cursor:pointer !important;
    white-space:nowrap !important;
    user-select:none !important;
    transition:background .16s ease, color .16s ease !important;
}
html.spp-post-on #editor-tab > span:hover,
html.spp-post-on #editor-tool > span:hover {
    background:color-mix(in srgb,var(--spu-accent,#2f6f68) 10%,transparent) !important;
    color:var(--spu-accent,#2f6f68) !important;
}
html.spp-post-on #editor-tab > span.buttonPressed {
    background:var(--spu-accent,#2f6f68) !important;
    color:#fff !important;
    box-shadow:0 2px 8px rgba(47,111,104,.26) !important;
}
/* 「移动」后的求物提示行:DOM 由 movePostRule() 搬进 #editor-content(见下方注释),
   这里只管把它钉在顶栏那一排、右端让开「字数检查…」那块卡(实测 243 + 8 间隙)。 */
html.spp-post-on #editor-content > #postrule {
    position:absolute !important;
    top:auto !important;
    bottom:100% !important;
    left:auto !important;
    /* 右端让开「字数检查…」那块卡:实测该卡 293 宽(四个胶囊 72/72/72/60 + 内边距),
       不是站点原来的 243 —— 第一版照 243 写 251,实站量出来压了 42px。 */
    right:301px !important;
    width:auto !important;
    max-width:430px !important;
    height:28px !important;
    margin:0 !important;
    padding:0 2px !important;
    display:flex !important;
    align-items:center !important;
    gap:6px !important;
    box-sizing:border-box !important;
    background:none !important;
    border:0 !important;
    box-shadow:none !important;
    font-size:12px !important;
    line-height:1.2 !important;
    color:var(--spu-muted,#6b7280) !important;
}
html.spp-post-on #editor-content > #postrule > div {
    min-width:0 !important;
    overflow:hidden !important;
    white-space:nowrap !important;
    text-overflow:ellipsis !important;
}
html.spp-post-on #editor-content > #postrule a {
    color:var(--spu-accent,#2f6f68) !important;
    font-weight:600 !important;
    text-decoration:none !important;
}
html.spp-post-on #editor-content > #postrule a:hover { text-decoration:underline !important; }
/* 「重复按钮」:站点自带的预览(a.btn3 → gopreview())收掉,提交行只留 👁 预览 */
html.spp-post-on a.btn3[onclick*="gopreview"] { display:none !important; }

/* ================= 2.53.10 引用页(action-quote / action-modify)=================
   见文件上方「运行版本标记(2.53.10)」那段的实测数字。两条都只在 html.spp-quote-on 下生效。
   ★ 为什么不能直接改上面那条 margin-left:79px:那是非引用页的布局(编辑器通栏、左缘 226),
     引用页的表情栏留在左侧(#editor-main 的左内衬 274),两套布局的左缘差 205px。
   ★ 为什么用 calc(50% - 346px) 而不是写死像素:页面宽度可调(1200–1600),
     编辑器右缘跟着 #main 走,只有相对中心写才在任何宽度下都同心。 */
html.spp-post-on.spp-quote-on .t.f_one > table:has(#post-option) {
    /* ★★ 2.54.1(用户截图「没对齐」):calc(50% - 346px) 是按**引用页**的左缘调的 ——
       引用页的表情栏留在左侧、正文卡被内衬顶出去 274,而 spp-quote-on 这枚标在
       action-modify 页**也打**(applyPostCss 里是 quote || modify),编辑页没有那道
       内衬、正文卡贴 .t_f_one 左缘 ⇒ 选项表被推右整整 274px(实测:正文卡 226..1192,
       选项表 500..1466)。⇒ 改读 --spp-opt-indent(syncPostFill 量的
       「正文卡左缘 − .t_f_one 左缘」),两页、任何页宽都同心;JS 没跑到时退回老口径。 */
    margin-left: var(--spp-opt-indent, calc(50% - 346px)) !important;
}
/* 白卡向下延长:把「卡底 → #main 底」这段(主题回顾所在)铺成白底。
   ★★ 2.53.12 换宿主:原来挂在 .t.f_one::after 上,那张卡自己 overflow:hidden,
     top:100% 的伪元素被整块裁掉 —— 计算值量得到 455px,屏幕上一片深色(用户截图抓到)。
     现在挂 #main::after:探针实测 #main 的 overflow 是 visible,不裁。
   ★ 起点走 --spp-white-top(syncPostFill 量「卡底 − #main 顶」写进 #main)——
     不能写 top:100%、也不能写死像素:卡上方那条面包屑行的高度会随页面变。
   ★ z-index:-1 + #main{z-index:0} 建层:白底只压页面底色,回顾那几张表是非定位元素,
     照旧画在白底【之上】。 */
html.spp-post-on.spp-quote-on #main { position:relative !important; z-index:0 !important; }
/* ---- ①「美化UI和预览保持一致」:提交与预览同一套按钮语言 ----
   ★ 为什么必须写 background-image:none:站点那枚提交是 <input type=submit>,它的红底是
     **背景位图**(老式 4 角切图),只写背景色会被位图盖住 —— 实测仍是一块带白边的深红方块。
   ★ 圆角用 999 而不是 12:它俩高度只有 24,12 圆角在小高度上看着像削角,999 才是胶囊。 */
/* ★ 2.55.31(用户:「底部两个UI按钮没有统一、美化」):这一套皮肤原来只挂在
   .spp-quote-on(引号页 / 编辑页)上,回贴页那两枚还是「站点红底切图 19.67px 高」
   +「我们的内联胶囊 24px 高」两种形状并排 —— 实测高度差 4.3px、圆角 0 vs 5px、
   字体 Arial vs inherit。本轮把同一套皮肤放到所有 post.php 页上(下面四条一起)。 */
html.spp-post-on input[type="submit"][name="Submit"],
html.spp-post-on #spp-preview-btn {
    height:24px !important;
    padding:0 14px !important;
    border-radius:999px !important;
    font-size:12px !important;
    font-weight:600 !important;
    font-family:inherit !important;
    line-height:1 !important;
    box-sizing:border-box !important;
    vertical-align:middle !important;
    cursor:pointer !important;
    text-shadow:none !important;
    transition:background .18s ease, border-color .18s ease, color .18s ease, transform .18s ease !important;
}
html.spp-post-on input[type="submit"][name="Submit"] {
    border:1px solid var(--spu-accent,#2f6f68) !important;
    background-color:var(--spu-accent,#2f6f68) !important;
    background-image:none !important;
    color:#fff !important;
    -webkit-text-fill-color:#fff !important;
}
html.spp-post-on input[type="submit"][name="Submit"]:hover {
    background-color:color-mix(in srgb, var(--spu-accent,#2f6f68) 84%, #000) !important;
    border-color:color-mix(in srgb, var(--spu-accent,#2f6f68) 84%, #000) !important;
    transform:translateY(-1px) !important;
}
html.spp-post-on #spp-preview-btn {
    border:1px solid var(--spu-accent,#2f6f68) !important;
    background:var(--spu-surface,#fff) !important;
    color:var(--spu-accent,#2f6f68) !important;
    margin-left:8px !important;
}
/* ★★ 2.54.1(用户截图「没有美化UI」):编辑页那枚「直接删除」是
   <input class="btn" type="submit" value="直接删除"> —— 它**没有 name**,
   上面那套提交钮皮肤是按 input[type=submit][name="Submit"] 挑的,于是它一直
   还是站点那张红底切图(实测背景是 bg_btn.gif)。
   ★ 认 value 不认位置:只有值里带「删除」的才上皮肤,其它 .btn 一个都不碰;
     用危险色(与设置页「重置」同一枚 #c0392b),别让人误以为是主操作。 */
html.spp-post-on input.btn[type="submit"][value*="删除"] {
    height:24px !important;
    padding:0 14px !important;
    border-radius:999px !important;
    border:1px solid rgba(192,57,43,.9) !important;
    background-color:#c0392b !important;
    background-image:none !important;
    color:#fff !important;
    -webkit-text-fill-color:#fff !important;
    font-size:12px !important;
    font-weight:600 !important;
    font-family:inherit !important;
    line-height:1 !important;
    box-sizing:border-box !important;
    text-shadow:none !important;
    vertical-align:middle !important;
    cursor:pointer !important;
}
html.spp-post-on input.btn[type="submit"][value*="删除"]:hover {
    background-color:#d14836 !important;
    border-color:#d14836 !important;
}
/* 提交行:站点把这两枚放在 ta=center 的 div 里,选项表同心之后它自然同心 ——
   这里只把行高与上下留白收齐,免得两枚胶囊各偏一行。 */
html.spp-post-on.spp-quote-on .t.f_one > table:has(#post-option) > tbody > tr > td > div:has(> input[name="Submit"]) {
    display:flex !important;
    align-items:center !important;
    justify-content:center !important;
    gap:0 !important;
    line-height:1 !important;
}
/* ---- ③ 配套:白底上的「主题回顾」区(站点文字是按深色星空配的灰字,白底看不清)----
   ★ 作用域只认 #main 的直接子节点:回顾区是站点渲染在 form 之后的兄弟节点,
     这样绝不会碰到表单里的任何东西(选项表、编辑器、附件行都在 form 内)。 */
html.spp-post-on.spp-quote-on #main > center {
    color:var(--spu-secondary,#475569) !important;
    font-size:13px !important;
    font-weight:600 !important;
}
html.spp-post-on.spp-quote-on #main > table,
html.spp-post-on.spp-quote-on #main > table font,
html.spp-post-on.spp-quote-on #main > table span,
html.spp-post-on.spp-quote-on #main > table td {
    color:var(--spu-text,#1f2937) !important;
    -webkit-text-fill-color:var(--spu-text,#1f2937) !important;
    text-shadow:none !important;
}
html.spp-post-on.spp-quote-on #main > table { font-size:12.5px !important; line-height:1.6 !important; }
html.spp-post-on.spp-quote-on #main > table a,
html.spp-post-on.spp-quote-on #main > table a * {
    color:var(--spu-accent,#2f6f68) !important;
    -webkit-text-fill-color:var(--spu-accent,#2f6f68) !important;
    font-weight:600 !important;
    text-decoration:none !important;
}
html.spp-post-on.spp-quote-on #main > table a:hover { text-decoration:underline !important; }
html.spp-post-on.spp-quote-on #main > hr {
    height:0 !important;
    border:0 !important;
    border-top:1px solid var(--spu-border,#e6ebf2) !important;
}
html.spp-post-on.spp-quote-on #main::after {
    content:"" !important;
    position:absolute !important;
    left:0 !important;
    right:0 !important;
    top:var(--spp-white-top, 100%) !important;
    bottom:0 !important;
    background:var(--spu-surface,#fff) !important;
    z-index:-1 !important;
    pointer-events:none !important;
}

/* ---- 2.55.31:回贴页黑夜下的预览钮(提交钮不用另写:它的主色走 --spu-accent,
   在黑夜里已经是 #3fbfa8 那支绿)。上面那条给的是「白底 + 主色字」,
   深色卡上白底太跳 ⇒ 换成同主色的淡底胶囊。 */
html.spp-rdark.spp-rdark.spp-post-on #spp-preview-btn {
    border-color:rgba(63,191,168,.55) !important;
    background:rgba(63,191,168,.14) !important;
    color:#eafff8 !important;
    -webkit-text-fill-color:#eafff8 !important;
}
html.spp-rdark.spp-rdark.spp-post-on #spp-preview-btn:hover {
    background:rgba(63,191,168,.24) !important;
    border-color:rgba(63,191,168,.85) !important;
}

/* ================= 2.55.31 回贴页「主题回顾」:白昼也要有底 =================
   用户:「底部文本变成透明背景影响阅读(需要根据黑夜、非黑夜增加背景)」。
   ★ 实测:星空那层玻璃(html.spp-hm-star #main .t = rgba(250,252,255,.93))只罩卡,
     主题回顾这几张表是 #main 的直接子元素 ⇒ 它们**一直**落在夜空上(黑模式开不开都一样 ——
     星空是另一枚开关、默认开)。站点给的字色 #555 压深空 = 读不了。
   ★ 白昼这一半照 2.53.10 给引号页做的办法:#main::after 把「卡底 → #main 底」这段铺成底;
     区别只在本条**不要求 .spp-quote-on**(那枚标只给 action-quote / action-modify 打,
     回贴页从来没这层底)。黑夜那半(字色 / 分隔线)在 CSS_READ_DARK 的第 7 段。
   ★ 底色写一份变量管两种状态:--spp-rk-glass 只在黑夜模式下被 CSS_READ_DARK 定义在 #main 上
     (黑玻璃),白昼没有定义 ⇒ 退回 fallback 的 #fff(白卡)。
   ★ 起点走 --spp-white-top(syncPostFill 量的「卡底 − #main 顶」):不能写 top:100%,
     卡上方那条面包屑行的高度会随页面变。
   ★ z-index:-1 + #main{z-index:0} 建层:底只压页面底色,回顾那几张表是非定位元素,
     照旧画在底之上。 */
html.spp-post-on:not(.spp-quote-on) #main { position:relative !important; z-index:0 !important; }
html.spp-post-on:not(.spp-quote-on) #main::after {
    content:"" !important;
    position:absolute !important;
    left:0 !important;
    right:0 !important;
    top:var(--spp-white-top, 100%) !important;
    bottom:0 !important;
    background:var(--spp-rk-glass,#fff) !important;
    z-index:-1 !important;
    pointer-events:none !important;
}
html.spp-post-on:not(.spp-quote-on) #main > center { color:var(--spu-muted,#6b7280) !important; }
html.spp-post-on:not(.spp-quote-on) #main > table[align="center"] { color:var(--spu-text,#1f2937) !important; }
html.spp-post-on:not(.spp-quote-on) #main > table[align="center"] a { color:var(--spu-accent,#2f6f68) !important; }
html.spp-post-on:not(.spp-quote-on) #main > hr {
    height:1px !important;
    border:0 !important;
    background:var(--spu-border,#e6ebf2) !important;
}
`;

        // 2.52.91「移动」:把「求物/询问到求物/询问区(点击进入),发在本版会删除」这一行
        //   搬进编辑器顶栏那一排(批注:黑框圈住它 + 黑箭头「移动」→ 箭头尖落在
        //   #editor-tab 右侧那块空白,x≈688@y≈451)。
        //   ★ 为什么要动 DOM:#editor-tab / #editor-tool 是 #editor-content 的绝对定位子节点
        //     (#editor-content 自己 position:relative),而 #postrule 原本挂在包裹层
        //     div:has(> #editor-side) 上。不搬就只能相对包裹层写死 top,而「包裹层顶 →
        //     正文框顶」这段距离正好会被本轮自己改动(提示行一撤,整段上移 ~17px)。
        //     搬进同一个定位祖先后,CSS 一条 bottom:100% 就贴住正文框上沿。
        //   ★ 幂等:站点若重建 #editor-content,下一轮 initBoardTop 会再搬一次。
        function movePostRule() {
            if (!PAGE.post) return false;
            const host = document.getElementById('editor-content');
            const rule = document.getElementById('postrule');
            if (!host || !rule || rule.parentElement === host) return false;
            host.appendChild(rule);
            return true;
        }

        // ★★ 2.53.10:白卡向下延长段的高度(③)。
        //   为什么必须量、不能写死:那段就是「主题回顾」的高度,条数一变就不同
        //   (实测本页 379;有的帖只有一两行、有的十几行)。量的口径是
        //   「#main 底 − 卡底」,即这页剩下的全部内容高度。
        function syncPostFill() {
            if (!PAGE.post) return;
            const card = document.querySelector('.t.f_one');
            const main = document.getElementById('main');
            if (!card || !main) return;
            const mr = main.getBoundingClientRect();
            const cr = card.getBoundingClientRect();
            // ★★ 2.53.12:这两个数都写到 **#main** 上(不再是卡上)——
            //   白底改挂 #main::after,变量得跟着宿主走,否则 var() 取不到。
            //   --spp-white-top = 卡底相对 #main 顶的距离(白底起点)。用 top 而不是
            //   「卡高 + 间距」:卡上面那条面包屑行的高度会随页面变。
            main.style.setProperty('--spp-white-top', Math.round(cr.bottom - mr.top) + 'px');
            main.style.setProperty('--spp-postfill', Math.max(0, Math.round(mr.bottom - cr.bottom) + 1) + 'px');
            /* ★★ 2.54.1:选项表与正文卡同左缘(用户截图「没对齐」)。
               引用页 / 编辑页的正文卡左缘**不一样**(引用页表情栏留在左侧、卡被内衬顶出去),
               写死一个 calc(50% - 346px) 只能对一页成立 ⇒ 就地量差值写进 --spp-opt-indent,
               给 POST_CSS 那条 margin-left 用。两页、任何页宽都同心,resize 时也会重量。 */
            const cardEl = document.getElementById('editor-content');
            if (cardEl) {
                main.style.setProperty('--spp-opt-indent',
                    Math.round(cardEl.getBoundingClientRect().left - cr.left) + 'px');
            }
        }

        function applyPostCss() {
            if (!PAGE.post) return;
            movePostRule();                                               // 先搬,再注入样式(顺序无关,但先搬免得首帧闪)
            // ★★ 2.53.10:引用页/编辑页在 <html> 上打一枚标 —— POST_CSS 里那几条只该在这两页生效
            //   (选项表居中 / 白卡延长 / 表情栏放大),而 .spp-post-on 是非引用页与引用页共用的。
            //   ★ 写在下面那条「只注入一次」的提前返回【之前】:样式只注入一次,标记却要每次都刷。
            document.documentElement.classList.toggle('spp-quote-on',
                SEARCH.indexOf('action-quote') >= 0 || SEARCH.indexOf('action-modify') >= 0);
            if (!window.__sppPostFillBound) {
                window.__sppPostFillBound = 1;
                window.addEventListener('resize', syncPostFill);
                // 主题回顾是服务端渲染的静态表,但站点可能有别的脚本往里插行 ⇒ 隔几拍各量一次兜住。
                [120, 400, 1200].forEach(function (t) { setTimeout(syncPostFill, t); });
            }
            syncPostFill();
            if (document.getElementById(STYLE_ID + '-post')) return;      // 只注入一次(本模块会重跑)
            const el = document.createElement('style');
            el.id = STYLE_ID + '-post';
            el.textContent = POST_CSS;
            appendToHeadSafe(el);
        }

        // 2.52.35 ①:屏蔽站点那条「| 浏览器收藏 | 打印」(#main > div.h2,底色 #ddd)
        // ★ 认内容不认类名:.h2 在站点里是通用小标题条,别的页面上还可能是「快速回复」那种,不能一刀切。
        function hideToolsRow() {
            // 2.52.35 ①:屏蔽站点那条「| 浏览器收藏 | 打印」(#main > div.h2,底色 #ddd)
            // 2.52.92:判定收紧成精确 PAGE.read。实站在版块页量过 —— #main > div.h2 是 0 个,
            //   原先靠 'thread.php'.includes('read.php') 误命中多跑的这一次本来就是空转。
            if (!PAGE.read) return 0;
            let n = 0;
            [].slice.call(document.querySelectorAll('#main > div.h2')).forEach(function (el) {
                if (el.classList.contains('spp-hide-row')) return;
                if (!/浏览器收藏|打印/.test(el.textContent || '')) return;
                el.classList.add('spp-hide-row');
                n++;
            });
            return n;
        }

        function moveCrumb() {
            // 2.52.60:帖子页不再搬 —— 面包屑整条留在 #main 顶部自己的 .bdbA 行里(顶栏正下方、
            //   翻页条正上方,板块本来就该站的位置),这里只给该行打 .spp-crumb-panel 标,
            //   READ_CSS 据此把它原地改扮成磨砂黑玻璃板块(折行预算见 READ_CSS 段末)。
            //   2.52.34~2.52.59 搬进右格的方案退役:长标题一折行就把右格拉高、左卡跟不上,
            //   左右底边错位(用户截图「不对称大小分配」)。
            // 2.52.90:发帖页(post.php)走同一条分支 —— 它的 #main > .bdbA 里同样只有 #breadcrumbs
            //   一个孩子,「南+ South Plus › 茶馆」那条即黄框里的第二行。
            if (WANTS.crumbPanel) {
                const row = document.querySelector('#main > .bdbA');
                if (!row || !row.querySelector('#breadcrumbs')) return 0;
                if (row.classList.contains('spp-crumb-panel')) return 0;      // 幂等
                row.classList.add('spp-crumb-panel');
                return 1;
            }
            // 2.52.52:搜索页仍搬进右格(黄框那条就是它)
            if (!PAGE.search) return 0;
            const host = document.querySelector('#infobox.spp-up-on td.spp-up-host-td');
            if (!host) return 0;
            const bdbA = document.querySelector('#main > .bdbA');
            const bc = bdbA ? bdbA.querySelector('#breadcrumbs') : null;
            if (!bc) return 0;
            if (bc.parentElement && bc.parentElement.classList.contains('spp-crumb-line')) return 0;   // 幂等
            let line = host.querySelector(':scope > .spp-crumb-line');
            if (!line) {
                line = document.createElement('div');
                line.className = 'spp-crumb-line';
                host.insertBefore(line, host.firstChild);
            }
            line.appendChild(bc);
            // 搬空的 .bdbA 收起(站点那条 27px 浅灰底不留空位)
            if (bdbA && !bdbA.children.length) bdbA.style.setProperty('display', 'none', 'important');
            return 1;
        }

        onDomReady(() => {
            // 2.52.52:搜索页先挂自己的页面类(不借帖子页的 spp-read-on)
            if (PAGE.search) document.documentElement.classList.add('spp-search-on');
            // 2.52.90:发帖页同样挂自己的页面类(POST_CSS 全靠它开关)
            if (PAGE.post) document.documentElement.classList.add('spp-post-on');
            applyBanner();
            applyCard();
            buildTop();
            applyReadCss();
            applySearchCss();
            applyPostCss();       // 2.52.90 发帖页:面包屑板块 + 页面底色(卡本体在 applyCard)
            applyHotCss();        // 2.52.79 任务页 / 2.52.90 发帖页:热门版块卡片(版块页当初由 initThreadListConvert 注入)
            moveCrumb();          // 2.52.34 帖子页 / 2.52.52 搜索页:安置面包屑(2.52.60 起帖子页不搬格,只打 .spp-crumb-panel 标)
            hideToolsRow();       // 2.52.35 ①:屏蔽「浏览器收藏 | 打印」那条浅灰行
            // 兜底:#infobox 与 #shortcutforum 都是站点脚本后填的,可能晚于本模块。
            // 各处都幂等(banner/applyCard 按 style id、buildHot 按 .hb-wrap、
            // polishUserInfo 按 data-spp-up-done),重复调用安全。
            setTimeout(() => { applyBanner(); applyCard(); buildTop(); applyReadCss(); applySearchCss(); applyPostCss(); applyHotCss(); moveCrumb(); hideToolsRow(); }, 800);
        });
    }

    // ==================== 2.52.54:搜索页「全部版块分类」→ 卡片弹层 ====================
    // 结构(探针实测 select[name="f_fid"]):194 个 option、没有 optgroup,层级是文本前缀拼的 ——
    //   value=all          「全部版块分类」
    //   ">> 漫区特设"      大板块(前缀 >> )
    //   " &nbsp;|- 同人志&CG"   子板块(前缀 |- ,前导 &nbsp; 表缩进)
    // 五个大板块:漫区特设(150) / 蜜柑计划(1) / 综合交流(14) / 人民囧府(1) / 隐藏版块(2),
    // 其中「隐藏版块」那条 value 是空白 ⇒ 只当标题、不可点。
    // ★ 漫区特设那 150 项是「届次」成组的(Comic Market 108 → 同人志&CG / 图墙模式 …),
    //   平铺 150 枚 chip 会又长又糊;再切一层「届次小卡」后高度从 ~900px 压到 ~300px,
    //   加其余三组一共约 460px —— 一屏放得下,正是用户要的「无需滚动」。
    function initSearchForumPicker() {
        if (!PAGE.search) return;

        // 「同人志&CG / 图墙模式 / 游戏 / 音乐 / Cosplay&其他 / 广播剧…」都是【上一届次】的续行。
        // ★ 必须【整名相等】才算续行:首版用前缀匹配(/^游戏|^同人音声/)时,综合交流里的
        //   「游戏资源」「同人音声」也被当成续行,于是那一组被拆成 21 张莫名其妙的卡。
        var CONT_NAMES = {
            '同人志&CG': 1, '同人志&CG (图墙模式)': 1, '图墙模式': 1, '游戏': 1, '音乐': 1,
            'Cosplay&其他': 1, '同人音声&Cosplay&其他': 1, '广播剧&Cosplay&其他': 1
        };
        function isCont(t) { return !!CONT_NAMES[String(t).replace(/s*(Cd+)$/, '')]; }
        // 只有子板块特别多的大板块(漫区特设 159 项)才值得拆届次卡;综合交流 25 项平铺 2 行就够
        var SPLIT_MIN = 40;
        var ALL_LABEL = '全部版块分类';
        var TEA_LABEL = '茶馆';

        function txt(o) { return (o.textContent || '').replace(/\u00a0/g, ' ').replace(/\s+/g, ' ').trim(); }
        // 小卡里 chip 只留能区分的部分(完整名字进 title,悬停可看)
        function short(label) {
            return String(label)
                .replace(/\s*\(C\d+\)$/, '')
                .replace(/^同人志&CG\s*\(图墙模式\)$/, '图墙模式')
                .replace(/^同人志&CG$/, '同人志')
                .replace(/^Cosplay&其他$/, 'Cosplay')
                .replace(/^新视界区\s*\(临时\)$/, '新视界');
        }

        function parse(sel) {
            var out = { all: null, groups: [] }, cur = null;
            [].slice.call(sel.options).forEach(function (o) {
                var t = txt(o);
                if (!t) return;
                if (o.value === 'all' || t === ALL_LABEL) { out.all = { v: o.value, t: t }; return; }
                if (t.indexOf('>>') === 0) {
                    cur = { name: t.replace(/^>>\s*/, ''), fid: o.value, subs: [] };
                    out.groups.push(cur);
                    return;
                }
                if (!cur) { cur = { name: ALL_LABEL, fid: o.value, subs: [] }; out.groups.push(cur); }
                cur.subs.push({ v: o.value, t: t.replace(/^\|-\s*/, '') });
            });
            // 子板块特别多的大板块(漫区特设)拆成一张张届次小卡
            out.groups.forEach(function (g) {
                if (g.subs.length <= SPLIT_MIN) return;
                var cards = [];
                g.subs.forEach(function (s) {
                    if (cards.length && isCont(s.t)) cards[cards.length - 1].items.push(s);
                    else cards.push({ head: s, items: [] });
                });
                g.cards = cards;
            });
            return out;
        }

        // 幂等注入(本模块可能在 onDomReady 与 900ms 兜底里各跑一次)
        function injectCss() {
            if (document.getElementById('spp-fpk-style')) return 0;
            var st = document.createElement('style');
            st.id = 'spp-fpk-style';
            st.textContent = CSS_FPK;
            appendToHeadSafe(st);
            return 1;
        }

        function build() {
            var sel = document.querySelector('select[name="f_fid"]');
            if (!sel || sel.dataset.sppFpk) return 0;
            injectCss();                     // ★ 必须在这里注入:见 CSS_FPK 那段注释
            var data = parse(sel);

            // ---- 触发按钮:原位替身 ----
            var trig = document.createElement('button');
            trig.type = 'button';
            trig.className = 'spp-fpk__trig';
            var trigT = document.createElement('span');
            trigT.className = 'spp-fpk__trig-t';
            trig.appendChild(trigT);
            var wpx = Math.round(sel.getBoundingClientRect().width);
            if (wpx > 80) trig.style.width = wpx + 'px';
            sel.parentNode.insertBefore(trig, sel);
            sel.dataset.sppFpk = '1';
            // ★ select 只隐藏不删除:表单提交仍然是 f_fid(站点的搜索表单是 GET)
            sel.style.setProperty('display', 'none', 'important');

            // ---- 弹层 ----
            var panel = document.createElement('div');
            panel.className = 'spp-fpk';
            panel.setAttribute('hidden', 'hidden');

            var hd = document.createElement('div');
            hd.className = 'spp-fpk__hd';
            var ttl = document.createElement('span');
            ttl.className = 'spp-fpk__t';
            ttl.textContent = '选择版块分类';
            var allChip = document.createElement('button');
            allChip.type = 'button';
            allChip.className = 'spp-fpk__chip spp-fpk__chip--all';
            allChip.textContent = ALL_LABEL;
            if (data.all) allChip.dataset.fid = data.all.v;
            var curTxt = document.createElement('span');
            curTxt.className = 'spp-fpk__cur';
            var xBtn = document.createElement('button');
            xBtn.type = 'button';
            xBtn.className = 'spp-fpk__x';
            xBtn.textContent = '×';
            xBtn.title = '关闭';
            hd.appendChild(ttl); hd.appendChild(allChip); hd.appendChild(curTxt); hd.appendChild(xBtn);

            var body = document.createElement('div');
            body.className = 'spp-fpk__body';

            function chipOf(item, compact) {
                var c = document.createElement('button');
                c.type = 'button';
                c.className = compact ? 'spp-fpk__lnk' : 'spp-fpk__chip';
                c.textContent = item.short ? short(item.t) : item.t;
                c.title = item.t;
                if (item.v && String(item.v).trim()) c.dataset.fid = item.v;
                else c.disabled = true;
                return c;
            }

            data.groups.forEach(function (g) {
                if (!g.subs.length) return;
                var sec = document.createElement('section');
                sec.className = 'spp-fpk__grp';
                var gt = document.createElement('button');
                gt.type = 'button';
                gt.className = 'spp-fpk__grp-t';
                gt.textContent = g.name;
                gt.title = '选中「' + g.name + '」这个大板块';
                if (g.fid && String(g.fid).trim()) gt.dataset.fid = g.fid;
                else gt.disabled = true;
                var gn = document.createElement('span');
                gn.className = 'spp-fpk__grp-n';
                gn.textContent = String(g.subs.length);
                gt.appendChild(gn);
                sec.appendChild(gt);

                var box = document.createElement('div');
                box.className = 'spp-fpk__subs';
                if (g.cards) {
                    box.classList.add('spp-fpk__subs--cards');
                    g.cards.forEach(function (card) {
                        var mini = document.createElement('div');
                        mini.className = 'spp-fpk__mini';
                        var mt = document.createElement('button');
                        mt.type = 'button';
                        mt.className = 'spp-fpk__mini-t';
                        mt.textContent = short(card.head.t);
                        mt.title = card.head.t;
                        if (card.head.v && String(card.head.v).trim()) mt.dataset.fid = card.head.v;
                        else mt.disabled = true;
                        mini.appendChild(mt);
                        var mb = document.createElement('div');
                        mb.className = 'spp-fpk__mini-b';
                        card.items.forEach(function (it) {
                            it.short = true;
                            mb.appendChild(chipOf(it, true));
                        });
                        mini.appendChild(mb);
                        box.appendChild(mini);
                    });
                } else {
                    g.subs.forEach(function (it) { box.appendChild(chipOf(it)); });
                }
                sec.appendChild(box);
                body.appendChild(sec);
            });

            panel.appendChild(hd);
            panel.appendChild(body);
            document.body.appendChild(panel);

            // ---- 状态与交互 ----
            function nodes() { return [].slice.call(panel.querySelectorAll('[data-fid]')); }
            function labelOf(v) {
                for (var i = 0; i < sel.options.length; i++) {
                    if (sel.options[i].value === v) return txt(sel.options[i]).replace(/^>>\s*/, '').replace(/^\|-\s*/, '');
                }
                return ALL_LABEL;
            }
            function paint() {
                var v = sel.value;
                nodes().forEach(function (el) {
                    var on = String(el.dataset.fid) === String(v);
                    el.classList.toggle('is-on', on);
                });
                var lb = labelOf(v);
                trigT.textContent = lb;
                trig.title = '当前:' + lb + '(点击选择版块分类)';
                curTxt.textContent = '当前:' + lb;
            }
            function open() { panel.removeAttribute('hidden'); paint(); }
            function shut() { panel.setAttribute('hidden', 'hidden'); }
            function pick(fid) {
                if (!fid || !String(fid).trim()) return;
                sel.value = fid;
                try { sel.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) { }
                paint();
                shut();
            }
            trig.addEventListener('click', function (e) {
                e.preventDefault();
                if (panel.hasAttribute('hidden')) open(); else shut();
            });
            xBtn.addEventListener('click', function (e) { e.preventDefault(); shut(); });
            panel.addEventListener('click', function (e) {
                e.preventDefault();
                var el = e.target && e.target.closest ? e.target.closest('[data-fid]') : null;
                if (el && !el.disabled) pick(el.dataset.fid);
            });
            document.addEventListener('keydown', function (e) { if (e.key === 'Escape') shut(); });
            document.addEventListener('click', function (e) {
                if (panel.hasAttribute('hidden')) return;
                if (panel.contains(e.target) || trig.contains(e.target)) return;
                shut();
            }, true);

            // ---- 默认勾选「茶馆」(GM 开关 spp_fpk_default_tea,默认开)----
            if (GM_getValue('spp_fpk_default_tea', true)) {
                [].slice.call(sel.options).some(function (o) {
                    var t = txt(o);
                    if (t.indexOf(TEA_LABEL) < 0 || t.indexOf('>>') === 0 || o.value === 'all') return false;
                    if (!String(o.value).trim()) return false;
                    sel.value = o.value;
                    return true;
                });
            }
            // 2.52.56:站点这颗「重 置」会把表单打回 HTML 默认值(f_fid 回到 all),
            //   而触发按钮的文案是自绘的、不会跟着变 —— 挂一次 reset 监听补 paint()
            if (sel.form) {
                sel.form.addEventListener('reset', function () { setTimeout(paint, 0); });
            }
            paint();
            return 1;
        }

        onDomReady(function () {
            if (!build()) return;
            // 兜底:站点表单/select 有可能晚于本模块(与 2.52.34 那套同因)
            setTimeout(function () { if (!document.querySelector('.spp-fpk__trig')) build(); }, 900);
        });
    }

    // ==================== 2.53.59:搜索排序(在悬浮设置面板里选,页面上不留控件) ====================
    // 用户(2.53.58 交付后):「你排序的那一个移到悬浮设置面板那里就行了,在悬浮设置面板中设置即可,
    //   无需放在网页上」 ⇒ 2.53.58 加在结果页的那条排序条**整套撤掉**(连它那条 SPP-SSORT 样式段、
    //   结果表反推、以及那套「记下表单条件、摘掉 sid 重发搜索」的机制),
    //   改成面板里一枚下拉框(人气 / 回复 / 发帖时间 / 最后回复)。
    // 保留的两件事,都不在页面上留控件:
    //   ① 搜索表单页:把站点那个 select[name="orderway"] 预设成面板里选的那一种(默认「人气」);
    //   ② 索引过期自救(2.53.44):重搜时也用同一种(那一版写死的是 lastpost)。
    // 站点事实(2.53.58 实测,读数见 README 的 2.53.58 专项):
    //   · 站点那个 select 的四个 option 是 postdate / lastpost / replies / hits,HTML 默认选中
    //     postdate(发帖时间)—— 用户说的「时间排序」是这个默认值,不是脚本的行为;
    //   · URL 里显式带 orderway 时按 URL 走:面板只改**表单的默认值**,不覆盖地址上写明的那一种。
    // ★ 为什么不再从结果页换序:站点结果页没有排序入口(select 数量 0),要换序只能自己摘掉 sid
    //   重发一次搜索(实测带 sid 时 orderway 会被服务端忽略)。既然在面板里改一次就够,
    //   就没必要在页面上多一行控件,更没必要为它去记表单条件。
    function initSearchSort() {
        if (!PAGE.search) return;
        function presetForm() {
            const sel = document.querySelector('select[name="orderway"]');
            if (!sel || sel.dataset.sppSsort) return 0;
            sel.dataset.sppSsort = '1';
            const m = String(SEARCH || '').match(/[?&]orderway=([a-z]+)/i);
            if (m && SEARCH_ORDER_LABEL[m[1]]) {
                // 地址里显式写了 orderway ⇒ 照地址(面板管的是表单默认值,不是地址)
                sel.value = m[1];
            } else {
                const want = searchOrder();
                sel.value = want;
                // 「人气 / 回复」要「最多的在前」:站点默认就是 DESC,只有被改成 ASC 时才纠回来
                if (want === 'hits' || want === 'replies') {
                    const asc = document.querySelector('input[name="asc"][value="ASC"]');
                    const desc = document.querySelector('input[name="asc"][value="DESC"]');
                    if (asc && asc.checked && desc) desc.checked = true;
                }
            }
            // 站点那颗「重 置」会把表单打回 HTML 默认值(发帖时间)—— 与 2.52.56 那颗按钮同一处,
            //   补一次,让面板里选的那一种在重置之后依然生效。
            if (sel.form) {
                sel.form.addEventListener('reset', function () { setTimeout(function () { sel.value = searchOrder(); }, 0); });
            }
            return 1;
        }
        onDomReady(function () {
            presetForm();
            // 兜底:站点表单有可能晚于本模块(与 2.52.54 / 2.52.34 那两处同因)
            setTimeout(presetForm, 700);
        });
    }

    // ==================== 2.52.9:版块页工具条(翻页 / 发新帖 / 位置) ====================
    // 用户要求:「美化绿色框选的翻页和发新贴按钮(发新贴按钮使用样式:uiverse evil-seahorse-60)
    //   把红色框选的'位置'移动到状态栏的右侧,并且美化」+「[点击进入列表模式] 修改为:点击进入列表模式」。
    // 样式在 SPP-BOARDTOOL 段,这里只负责三件 DOM 事(都不动站点链接本身):
    //   ① 给翻页行打 .spp-pgbar(CSS 只认这个类,站点别的 .t3 不受影响)——
    //      ★ 2.53.37:除了「含 .pages 的 .t3 行」,搜索页那条**裸挂在 #main 下的 .pages** 也会被
    //        就地包进一层 div.spp-pgbar,好共用同一套规则;
    //   ② 发新帖那枚 83×30 位图 → 光束结构(原位新增 span,img 只隐藏不删);
    //   ③ #breadcrumbs 搬进 #spp-btabs 最右;方括号文案去掉;搬空的 .bdbA 收起。
    // ★ 幂等:打标用 classList / dataset,重复 run() 安全(面板是异步建的,所以带一次兜底重跑)。
    function initBoardToolbar() {
        // 2.52.10:帖子页(read.php)顶/底两条 .t3 翻页行也套用同一套规则 —— 以前只认列表页,
        // 帖子页翻页一直是站点原样(.pages 白框 + 白底 Go 输入框)。
        // ★ 2.53.37:搜索页(search.php)也要这套 —— 它的翻页条跟版块页/帖子页是同一套站点结构,
        //   却因为这道门一直没被处理(用户:「这个框选的翻页按钮没有被脚本美化」)。
        //   实测结果页有两条 .pages:上半条在 .t3 里、下半条裸挂在 #main 下,两条都得收。
        if (!PAGE.board && !PAGE.read && !PAGE.search) return;
        // 2.52.33:帖子页的面包屑搬不进 #spp-btabs(那是版块页的工具条),就地美化 —— 用 html 类当开关,
        //   免得这条 CSS 落到首页那条同样叫 .bdbA 的公告栏上
        // 2.52.92:判定收紧成精确 PAGE.read 之后,「&& !isThreadListPage」这道防子串
        //   误命中的补丁就不需要了(原先正是为了挡 /thread.php 命中 'read.php' 才加的)。
        if (PAGE.read) document.documentElement.classList.add('spp-read-on');

        // 2.52.35:帖子页也要版块页那套工具条样式(该段 CSS 原先只在版块页那个大盘子里注入)
        if ((PAGE.read || PAGE.search) && !document.getElementById('spp-boardtool-style')) {
            const bt = document.createElement('style');
            bt.id = 'spp-boardtool-style';
            bt.textContent = CSS_BOARDTOOL;
            appendToHeadSafe(bt);
        }

        // ★ 实测:站外还有别的注入样式(读到一条 input[type=text]{border:1px solid var(--spu-border)!important}
        //   这种全局 !important),当前页 <b> 与 Go 输入框的背景/边框被压在下面 —— 光靠选择器打不过
        //   读不到的样式表,所以这两处用【内联 important】钉死(内联 important 只有更重要的内联才能盖)。
        function hardenPager(row) {
            const cur = row.querySelector('.pages ul li b');
            if (cur) {
                cur.style.setProperty('box-sizing', 'border-box', 'important');
                cur.style.setProperty('background-color', 'rgba(32,201,151,.16)', 'important');
                cur.style.setProperty('border-color', 'rgba(32,201,151,.70)', 'important');
                cur.style.setProperty('color', '#c9f7e4', 'important');
                cur.style.setProperty('-webkit-text-fill-color', '#c9f7e4', 'important');
            }
            const go = row.querySelector('.pagesone input');
            if (go) {
                go.style.setProperty('box-sizing', 'border-box', 'important');
                go.style.setProperty('height', '30px', 'important');
                go.style.setProperty('width', '62px', 'important');
                go.style.setProperty('padding', '0 6px', 'important');
                go.style.setProperty('font-size', '13px', 'important');
                go.style.setProperty('border-radius', '8px', 'important');
                go.style.setProperty('background-color', '#05070b', 'important');
                go.style.setProperty('border-width', '0', 'important');
                go.style.setProperty('outline', 'none', 'important');
                go.style.setProperty('color', '#e6edf7', 'important');
                go.style.setProperty('-webkit-text-fill-color', '#e6edf7', 'important');
            }
        }

        // 2.52.29:「首页 « / 末页 »」→「上一页 / 下一页」
        // 判据:按钮按【文本】找(站点改 class 也不受影响)——这几天已经栽过两次「按猜的元素写选择器」;
        // 页号模板取行内任一含 page-N / page=N 的链接,目标页号 = 地址栏里的当前页 ±1。
        function retargetEndPager(row) {
            /* ★★ 2.53.49 三刀才到位(每一刀的读数都写在 land-25349*.js 里):
               同一行里有**两块 .pages** —— ① <span class="fl pages">「上一主题 / 下一主题」
               (href 是 job.php?…-fpage-N-goto-…,也匹配 page[-=]\d+),② <div class="pages"> 真正的
               翻页块(‹ 1 2 3 › + Pages x/y + Go),且 ① 在 DOM 里更靠前。
               ⇒ 这里**只挑「同时含 ‹ 与 › 的那一块」**,模板与箭头都从这一块里取;找不到就整体不动。 */
            let box = null;
            const boxes = row.querySelectorAll('.pages');
            for (let b = 0; b < boxes.length; b++) {
                const t = boxes[b].textContent || '';
                if (/[«‹]/.test(t) && /[»›]/.test(t)) { box = boxes[b]; break; }
            }
            if (!box) return 0;
            const anchors = Array.from(box.querySelectorAll('a'));
            /* ★★ 2.53.55 修(真机读数抓到的自伤):本函数会反复跑,而新加的 « / » 就在同一块 .pages 里、
               字形也匹配这两条正则 ⇒ 第二次跑时 prevBtn 会**认领到自己刚建的那一枚**,
               紧接着那句 prevBtn.textContent = '‹' 就把 « 改成了 ‹。
               所以认领站点按钮前显式跳过自家按钮(data-spp-pg-end)。 */
            const byText = (re) => anchors.find(function (a) {
                return !a.hasAttribute('data-spp-pg-end') && re.test((a.textContent || '').trim());
            });
            const prevBtn = byText(/^[«‹]+$/);
            const nextBtn = byText(/^[»›]+$/);
            /* ★ 模板同一条道理:自家按钮的 href 也含 page-N,别拿它当模板(虽然形状一样,但那是**我们**的产物)。 */
            const tmplA = anchors.find(function (a) {
                return !a.hasAttribute('data-spp-pg-end') && /page[-=]\d+/i.test(a.getAttribute('href') || '');
            });
            if (!prevBtn || !nextBtn || !tmplA) return 0;
            const tmpl = tmplA.getAttribute('href') || '';
            /* ★ 2.53.49:同上一处 —— 地址栏里也可能带 fpage-N(点过首尾按钮之后就是那种地址),
               取第一处会把 fpage 当成当前页号,于是「上一页/下一页」算错一整格。 */
            const hereHits = (location.pathname + location.search).match(/page[-=](\d+)/gi);
            const hereLast = hereHits ? hereHits[hereHits.length - 1].match(/(\d+)/) : null;
            const cur = hereLast ? Number(hereLast[1]) : 1;
            const one = row.querySelector('.pagesone');
            const tm = one ? (one.textContent || '').match(/(\d+)\s*\/\s*(\d+)/) : null;
            const total = tm ? Number(tm[2]) : 0;
            /* ★ 2.53.49 修(用户:「左右翻页按钮功能失效」):旧写法 replace(/page([-=])\d+/i, …)
               命中的是**第一处** page[-=]N,而站点这串地址前头还有个 fpage-0(fpage 里也含 "page")
               ⇒ 改完变成 …-fpage-N-toread--page-1.html,末尾真正的目标页号一直是 1,两个按钮都回第 1 页。
               实测:read.php?tid-2934530-fpage-2-toread--page-1.html → Pages: 1/3。改成只换最后一处。 */
            const build = function (n) {
                const re = /page([-=])\d+/gi;
                let m, last = null;
                while ((m = re.exec(tmpl))) last = m;
                if (!last) return tmpl;
                return tmpl.slice(0, last.index) + 'page' + last[1] + n + tmpl.slice(last.index + last[0].length);
            };
            const set = function (a, n, ok) {
                if (a.dataset.sppPgTo === String(n) && ok) return;
                a.dataset.sppPgTo = String(n);
                if (ok) {
                    a.setAttribute('href', build(n));
                    a.classList.remove('spp-pg-off');
                    a.removeAttribute('aria-disabled');
                } else if (a.getAttribute('href')) {
                    a.removeAttribute('href');
                    a.classList.add('spp-pg-off');
                    a.setAttribute('aria-disabled', 'true');
                }
            };
            set(prevBtn, cur - 1, cur - 1 >= 1);
            set(nextBtn, cur + 1, !total || cur + 1 <= total);
            /* ★ 2.53.49(用户:「将那个带有两个箭头的图标换成一个箭头」):站点这两枚是连体双箭头
               « / »(看着像「首页 / 末页」),而它们已经被本函数改成「上一页 / 下一页」
               ⇒ 字形跟着换成单箭头 ‹ / ›。上面两条 byText 正则本来就同时认两种字形,改字不影响识别。 */
            prevBtn.textContent = '‹';
            nextBtn.textContent = '›';
            /* ★★ 2.53.55(用户:「能否添加一个双箭头的两个按钮,用于滑动到第一页和最后一页?
               这两个镜头可以在悬浮设置面板中选择开启或关闭,默认是开启的。」):
               在 ‹ / › 两侧各补一枚连体双箭头 « / »(首页 / 末页)。站点原本那两枚就是 «/»,
               2.52.29 起被本函数改成「上一页 / 下一页」⇒ 这里**新建**两枚,不动那两枚的字形。
               · 纯 <a href> 跳转(复用同一个 build 模板改写),不挂 JS 点击 —— 与站点自己的翻页同一种行为,
                 中键 / 新窗口 / 键盘访问都还能用;
               · 到边界照 ‹/› 的老规矩摘掉 href + .spp-pg-off(灰掉、不可点)——「已经在第 1 页」不该看着能点;
               · 幂等:以 data-spp-pg-end="first|last" 认领,重跑只改 href;
               · 读不到总页数时**只建首页那枚**(末页没得算,宁可不给也不给一个错的);
               · 开关关掉时把已建的两枚摘掉(面板保存本来就会 reload,这里只是兜底)。
               ★ 结构:站点是 <ul><li><a>‹</a></li>…</ul>,所以新按钮照原样包一层 <li>
                 (cloneNode(false) 保留该 <li> 自己的属性);万一站点改成 <a> 直接挂 <ul>,
                 就退化成直接插 <a>,不硬套。 */
            const endsOn = SPP_CONFIG.bool('spp_pg_ends', true);
            const clearEnds = function () {
                Array.prototype.forEach.call(row.querySelectorAll('a[data-spp-pg-end]'), function (a) {
                    const p = a.parentElement;
                    if (p && p.tagName === 'LI' && p.querySelectorAll('a').length === 1) p.remove();
                    else a.remove();
                });
            };
            if (!endsOn) clearEnds();
            const mkEnd = function (side, glyph, n, ok) {
                const host = (side === 'first') ? prevBtn.parentElement : nextBtn.parentElement;
                if (!host || !host.parentElement) return;
                const wrap = host.parentElement;
                let a = wrap.querySelector('a[data-spp-pg-end="' + side + '"]');
                if (!a) {
                    a = prevBtn.cloneNode(true);
                    a.setAttribute('data-spp-pg-end', side);
                    a.removeAttribute('data-spp-pg-to');
                    if (host.tagName === 'LI') {
                        const li = host.cloneNode(false);
                        li.appendChild(a);
                        wrap.insertBefore(li, (side === 'first') ? host : host.nextSibling);
                    } else {
                        wrap.insertBefore(a, (side === 'first') ? host : host.nextSibling);
                    }
                }
                /* ★ 2.53.55 修:字形**每次都由这里写定**(不只是新建时)—— 这样即便别处改脏了也能自愈。 */
                a.textContent = glyph;
                if (ok) {
                    a.setAttribute('href', build(n));
                    a.classList.remove('spp-pg-off');
                    a.removeAttribute('aria-disabled');
                    a.setAttribute('title', (side === 'first') ? '第 1 页' : ('末页(第 ' + n + ' 页)'));
                } else {
                    a.removeAttribute('href');
                    a.classList.add('spp-pg-off');
                    a.setAttribute('aria-disabled', 'true');
                    a.setAttribute('title', (side === 'first') ? '已经在第 1 页' : '已经在末页');
                }
            };
            if (endsOn) {
                mkEnd('first', '«', 1, cur > 1);
                if (total) mkEnd('last', '»', total, cur < total);
            }
            return 1;
        }

        function markPagerRows() {
            let n = 0;
            const rows = [];
            /* ★ 2.53.38:起点统一成「遍历所有 .pages,各自找宿主」。
               2.53.37 是「先从 .t3 收一遍、再从 .pages 补一遍」,两条路径不对称 ——
               验证时见过一次「.t3 里那条只中一半(尺寸中、颜色与 hardenPager 都没中)」,
               没能复现,但不对称本身就是隐患:只有一个入口,就不可能再漏掉某一类。 */
            /* ★ 2.53.37:搜索结果页下半条 .pages 是**裸挂在 #main 下**的(div.pages < div#main,
               没有 .t3 包裹)—— 上面那条老路子整条漏掉,用户红框圈的就是它。
               这里就地给它**包一层 div.spp-pgbar**(纯装饰、不带任何样式),好让同一套
               #main .spp-pgbar .pages 规则原样吃上 —— 比复制十來条 CSS 少一处维护点。
               ★(上面这行原本给选择器套了反引号,被 tools/verify/check-backticks.js 当场拦下 ——
                 它只按「行首是不是 // 、/* 、*」判断注释,块注释中间那几行一样会被当成代码行。改掉。)
               ★ 幂等:包过一次之后 parentElement 就带类了,重跑时直接跳过、不会再套一层。
               ★ 为什么不是给 .pages 自己打类:CSS 要的是「祖先带 .spp-pgbar」,自己带不算。 */
            Array.prototype.forEach.call(document.querySelectorAll('.pages'), function (p) {
                const t3 = p.closest('.t3');                       // 有 .t3 包裹 → 标打在 .t3 行上(与 2.52.10 一致)
                if (t3) { if (rows.indexOf(t3) < 0) rows.push(t3); return; }
                const host = p.parentElement;
                if (host && host.classList.contains('spp-pgbar')) { rows.push(host); return; }
                const wrap = document.createElement('div');
                wrap.className = 'spp-pgbar';
                host.insertBefore(wrap, p);
                wrap.appendChild(p);
                rows.push(wrap);
            });
            rows.forEach(function (row) {
                if (!row.classList.contains('spp-pgbar')) row.classList.add('spp-pgbar');
                // 2.52.10:站点靠 float 把 .fr 贴右(DOM 第一枚在最右)。flex 化之后用 order 复刻这个视觉序,
                // 并让右组里「视觉最左」那枚吃掉剩余空间 —— 否则帖子页的「回帖 / 发新帖」会左右颠倒。
                const frs = row.querySelectorAll(':scope > .fr');
                for (let k = 0; k < frs.length; k++) {
                    frs[k].classList.add(k < frs.length - 1 ? 'spp-pg-mid' : 'spp-pg-flex');
                }
                hardenPager(row);
                retargetEndPager(row);          // 2.52.29:末页/首页 → 下一页/上一页
                n++;
            });
            return n;
        }

        // 发新帖:实测结构 SPAN.fr.color > A[href=post.php?fid-N.html] > IMG#td_post(底栏 #td_post1)
        function dressPostButtons() {
            if (!PAGE.board) return 0;   // 帖子页那两枚站点位图(发新帖 / 回帖)不换皮
            let n = 0;
            Array.prototype.forEach.call(document.querySelectorAll('.spp-pgbar a[href*="post.php"]'), function (a) {
                if (a.dataset.sppPost) return;
                a.dataset.sppPost = '1';
                a.classList.add('spp-postbtn');

                const glow = document.createElement('span');
                glow.className = 'spp-post-glow';
                glow.setAttribute('aria-hidden', 'true');
                ['l', 'r', 't', 'b'].forEach(function (k) {     // 四条光束 .l/.r/.t/.b
                    const i = document.createElement('i');
                    i.className = 'spp-pb-' + k;
                    glow.appendChild(i);
                });
                const bd = document.createElement('span');
                bd.className = 'spp-pb-backdrop';               // 点阵底纹(悬停时浮出)
                glow.appendChild(bd);

                const ico = document.createElement('span');     // “+” 号
                ico.className = 'spp-post-ico';
                ico.setAttribute('aria-hidden', 'true');

                const txt = document.createElement('span');
                txt.className = 'spp-post-txt';
                txt.textContent = '发新帖';                      // 与站点位图上的文案一致

                a.appendChild(glow); a.appendChild(ico); a.appendChild(txt);
                n++;
            });
            return n;
        }

        // 用户要求:「[点击进入列表模式] 修改为:点击进入列表模式」——去方括号与「搬不搬得进按钮行」
        // 无关,所以单独一步、无条件执行(图墙页那枚是 [点击进入图墙模式],同样处理,图墙识别不受影响)。
        function stripBrackets() {
            const s3 = document.querySelector('#breadcrumbs a.s3');
            if (!s3) return 0;
            const m = (s3.textContent || '').trim().match(/^\[(.+)\]$/);
            if (m) s3.textContent = m[1];
            return 1;
        }

        function moveCrumbs() {
            const bar = document.getElementById('spp-btabs');
            const bc = document.getElementById('breadcrumbs');
            if (!bar || !bc) return 0;                          // 没按钮行就不搬(例如没面板的页面)
            if (bc.parentElement !== bar) bar.appendChild(bc);   // 按钮行最右(CSS 用 margin-left:auto)
            const wrap = bc.closest('.bdbA');
            if (wrap && !wrap.children.length) wrap.style.setProperty('display', 'none', 'important');
            return 1;
        }

        function run() {
            markPagerRows();
            dressPostButtons();
            stripBrackets();
            moveCrumbs();
        }

        onDomReady(function () {
            run();
            // 兜底:按钮行由 initBoardNotice 在 onDomReady 里建,站点也可能晚一步填 #breadcrumbs
            setTimeout(run, 800);
        });
    }

    // ==================== 2.52.19:个人中心页(profile.php)权限表勾选标记 ====================
    // 只做一件事:把权限表里文本是「√」的单元格打上 .spp-tick(CSS 才能上亮绿)——
    // CSS 没有「按文本选元素」的能力,这个类名是唯一钩子;判据只看单元格自身文本,不动站点结构。
    // 幂等:打过标的用 dataset 跳过(重复 run 安全)。
    function initProfileSkin() {
        // ★ 2.52.72:判据从「URL 含 profile.php」换成「页面上有 #set-wrap」。
        //   userpay.php(积分管理)用的是同一套外壳(#set-wrap / #set-content / #set-side-wrap),
        //   但 URL 不含 profile.php → 原先这个模块整个不跑,那边页签的当前项就一直是站点的白底。
        //   CSS 那边本来就用 body:has(#set-wrap) 门控,现在两边同一套判据。
        //   ★ 必须放在 onDomReady 里判:#set-wrap 在 document-start 时还不存在。
        onDomReady(function () {
            if (!document.getElementById('set-wrap')) return;
            // ★ 站点在【读不到的样式表】里给这些链接写了同权重的 !important,光靠选择器打不过
            //   (实测:抬高到 4 个 id 仍被 #333 压住)——与 initBoardToolbar 的 Go 输入框同一处境,
            //   这里按项目既有做法用内联 important 钉死字色与左内边距(hover 的底色与左侧竖条仍走 CSS)。
            // ★ 站点那层浅底画在 <tr> 上,光钉 <td> 会从下面透上来(实测:列头行仍发白)。
            //   先把整表每一行钉成透明,再钉单元格式样 —— 这样玻璃与星空的层次才留得住。
            // ★ 白线框(页签描边、区块间横线、内层容器的框)同样是站点的 border,
            //   与列头行的浅底一个处境 —— 选择器可能被读不到的样式表压住,这里一并内联钉死。
            Array.prototype.forEach.call(
                document.querySelectorAll('#set-content div, #set-content .set-tab-table td, #set-content .set-tab-table table, #set-content-wrap'),
                function (el) {
                    el.style.setProperty('border', '0', 'important');
                    el.style.setProperty('outline', '0', 'important');
                    el.style.setProperty('box-shadow', 'none', 'important');
                }
            );
            Array.prototype.forEach.call(
                document.querySelectorAll('#set-content table.set-table2 tr'),
                function (tr) {
                    tr.style.setProperty('background-color', 'transparent', 'important');
                    tr.style.setProperty('background-image', 'none', 'important');
                }
            );
            // ★ 页签与列头行的浅底灰字也来自读不到的样式表,同样内联 important 钉死:
            //   页签(版块权限那枚白框)、列表小节标题行、列头行三处一起改,免得半深半浅。
            [
                ['#set-content .set-tab-table td', 'rgba(255,255,255,.06)', '#e6edf7'],
                ['#set-content table.set-table2 tr.tr32:first-child > td', 'rgba(255,255,255,.045)', '#8ef0b8'],
                ['#set-content table.set-table2 tr.tr2 > td', 'rgba(255,255,255,.055)', '#9fb0c8']
            ].forEach(function (it) {
                Array.prototype.forEach.call(document.querySelectorAll(it[0]), function (el) {
                    el.style.setProperty('background-color', it[1], 'important');
                    el.style.setProperty('background-image', 'none', 'important');
                    el.style.setProperty('color', it[2], 'important');
                    el.style.setProperty('-webkit-text-fill-color', it[2], 'important');
                });
            });
            Array.prototype.forEach.call(document.querySelectorAll('#set-menu > li > a'), function (a) {
                a.style.setProperty('color', '#cfdcee', 'important');
                a.style.setProperty('-webkit-text-fill-color', '#cfdcee', 'important');
                a.style.setProperty('padding-left', '16px', 'important');
            });
            Array.prototype.forEach.call(
                document.querySelectorAll('#set-content table td'),
                function (td) {
                    if (td.dataset.sppTick) return;
                    const t = (td.textContent || '').trim();
                    if (t === '√' || t === '✓' || t === '✔') {
                        td.classList.add('spp-tick');
                        td.dataset.sppTick = '1';
                        td.style.setProperty('color', '#3ef58c', 'important');
                        td.style.setProperty('-webkit-text-fill-color', '#3ef58c', 'important');
                    }
                }
            );
        });
    }

    // ==================== 2.52.69:个人中心页「卡片化 + 排版重做」(方案 B) ====================
    // 与 2.52.17 的老规矩一致:凡是「要一块边框/底色」的元素,都必须内联钉死 ——
    // 站点在读不到的样式表里给 #set-content div 写了同权重的 border/outline/box-shadow !important,
    // 上面那圈 border:0 就是为它准备的;新加的两块托盘同理,只能靠内联 important 才赢(与 Go 输入框同一处境)。
    function initProfileLayout() {
        onDomReady(function () {
            if (!document.getElementById('set-wrap')) return;   // 判据同 initProfileSkin(见上)
            const CARDS = [
                '#set-content .set-content-left > div:first-child',   // 资料区托盘(头像 + 名字 + 签名 + 快捷入口)
                '#set-content .set-content-right > div:not(.c)'       // 右侧两块面板(短消息 / 收藏)
            ];
            CARDS.forEach(function (sel) {
                Array.prototype.forEach.call(document.querySelectorAll(sel), function (el) {
                    el.style.setProperty('border', '1px solid rgba(255,255,255,.075)', 'important');
                    el.style.setProperty('border-radius', '14px', 'important');
                    el.style.setProperty('background-color', 'rgba(255,255,255,.022)', 'important');
                    el.style.setProperty('background-image',
                        'linear-gradient(to top, rgba(0,0,0,.30) 0%, rgba(255,255,255,.05) 100%)', 'important');
                    el.style.setProperty('box-shadow',
                        'inset 0 2px 5px rgba(0,0,0,.45), inset 0 -1px 0 rgba(255,255,255,.06)', 'important');
                });
            });

            // ★ 统计区:站点把 10 项数据用 <br> 折成两列文本塞进两个 <td>。CSS 没有「按 <br> 切分」的能力,
            //   所以这里按行拆出来重排成一格一项的网格;每个格子【上下两行】—— 标签在上、数值在下(用户 2026-09-30 定案)。
            //   原先是左右并排,遇到「在线时间 579 小时 / 注册时间 2021-12-12」这种长值就在格子里折行。
            //   幂等:拆过的表用 dataset 标记,且表已不在 DOM 里,重复 run 自然短路。
            (function buildProfileStats() {
                const host = document.querySelector('#set-content .set-content-left');
                const table = host ? host.querySelector('table.set-table1') : null;
                if (!table || table.dataset.sppStatsDone) return;
                table.dataset.sppStatsDone = '1';
                const items = [];
                Array.prototype.forEach.call(table.querySelectorAll('td'), function (td) {
                    String(td.innerHTML).split(/<br\s*\/?>/i).forEach(function (seg) {
                        const t = seg.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
                        if (!t) return;
                        const m = t.match(/^([^::]+)[::]\s*(.*)$/);
                        items.push(m ? { k: m[1].trim(), v: m[2].trim() } : { k: t, v: '' });
                    });
                });
                if (!items.length) return;
                const box = document.createElement('div');
                box.className = 'spp-stats';
                items.forEach(function (it) {
                    const cell = document.createElement('span');
                    cell.className = 'spp-stat' + (/^(SP币|会员头衔)$/.test(it.k) ? ' is-accent' : '');
                    const em = document.createElement('em');   // 上行:标签
                    em.textContent = it.k;
                    const b = document.createElement('b');     // 下行:数值
                    b.textContent = it.v || '—';
                    cell.appendChild(em);
                    cell.appendChild(b);
                    box.appendChild(cell);
                });
                table.replaceWith(box);
            })();

            // ★ 侧栏「当前项」:站点没给 #set-menu 打任何当前态标记,这里只用站点自己给的 href 判据(?action-xxx),
            //   不按文字猜(经验 8.6)。首页(profile.php 无 action)本来就没有对应项,保持全都不点亮。
            Array.prototype.forEach.call(document.querySelectorAll('#set-menu > li'), function (li) {
                if (li.dataset.sppCurDone) return;
                li.dataset.sppCurDone = '1';
                const a = li.querySelector('a');
                if (!a) return;
                const m = String(a.getAttribute('href') || '').match(/action-([a-z]+)/);
                if (m && SEARCH.indexOf('action-' + m[1]) >= 0) li.classList.add('spp-cur');
            });

            // ★ 2.52.70:左列尾巴上那个「只有 padding 的空块」(内含空 table.set-table1)——
            //   实测它是 24px 的纯占位,卡底那段空就是它 + 两列高度差。收起它。
            //   ★ 判据必须收紧:只看「没有文字、没有图、也没有非空表格行」的那一个,
            //     否则权限页(左列同样是最后一个子块、但里面有整张权限表)会被一起藏掉。
            (function dropTrailingEmpty() {
                const left = document.querySelector('#set-content .set-content-left');
                if (!left) return;
                const blocks = [...left.children].filter(function (d) { return !d.classList.contains('c'); });
                const last = blocks[blocks.length - 1];
                if (!last || last.dataset.sppEmptyDone) return;
                const txt = (last.textContent || '').replace(/\s+/g, '');
                const hasMedia = !!last.querySelector('img, input, a, select, textarea');
                last.dataset.sppEmptyDone = '1';
                if (!txt && !hasMedia) last.style.setProperty('display', 'none', 'important');
            })();
            // ★ 2.52.71:表单控件暗色化 —— 这三组属性【只能在 JS 里内联钉死】。
            //   实测把 CSS 打到「html body:has(#set-wrap) #set-content input.input」(2 个 id)之后:
            //   border-radius / min-height / width / text-align 都生效了,
            //   但 background-color / border / font-family 三条仍被站点压住 —— 与 2.52.17 那批链接同一处境
            //   (站点在读不到的样式表里写了同权重 !important,把选择器权重抬到 4 个 id 也赢不了)。
            //   所以这几条(含跟着一起被压的 color)走内联 important。
            const FIELD_SEL = '#set-content input.input, #set-content input[type="text"], #set-content input[type="password"], ' +
                '#set-content input[type="email"], #set-content input[type="number"], #set-content input[type="url"], ' +
                '#set-content input[type="search"], #set-content input[type="tel"], #set-content textarea';
            Array.prototype.forEach.call(document.querySelectorAll(FIELD_SEL), function (el) {
                el.style.setProperty('background-color', 'rgba(9,13,21,.55)', 'important');
                el.style.setProperty('background-image',
                    'linear-gradient(to top, rgba(0,0,0,.30) 0%, rgba(255,255,255,.045) 100%)', 'important');
                el.style.setProperty('border', '1px solid rgba(255,255,255,.10)', 'important');
                el.style.setProperty('color', '#eaf2fb', 'important');
                el.style.setProperty('-webkit-text-fill-color', '#eaf2fb', 'important');
                el.style.setProperty('font-family', 'inherit', 'important');
            });
            // 文件选择(修改头像 → 头像上传)
            Array.prototype.forEach.call(document.querySelectorAll('#set-content input[type="file"]'), function (el) {
                el.style.setProperty('background-color', 'rgba(9,13,21,.40)', 'important');
                el.style.setProperty('background-image', 'none', 'important');
                el.style.setProperty('border', '1px dashed rgba(255,255,255,.16)', 'important');
                el.style.setProperty('color', '#9fb0c8', 'important');
                el.style.setProperty('-webkit-text-fill-color', '#9fb0c8', 'important');
                el.style.setProperty('font-family', 'inherit', 'important');
            });
            // 提交 / 重置按钮:站点给的是 bg_btn.gif 位图 + #bbb 底 + Arial
            Array.prototype.forEach.call(
                document.querySelectorAll('#set-content input.btn[type="submit"], #set-content input.btn[type="button"], #set-content button.btn'),
                function (el) {
                    el.style.setProperty('background-image', 'none', 'important');
                    el.style.setProperty('background-color', '#188a67', 'important');
                    el.style.setProperty('border', '1px solid rgba(32,201,151,.85)', 'important');
                    el.style.setProperty('color', '#eafff6', 'important');
                    el.style.setProperty('-webkit-text-fill-color', '#eafff6', 'important');
                    el.style.setProperty('font-family', 'inherit', 'important');
                    el.style.setProperty('font-size', '13.5px', 'important');
                }
            );
            Array.prototype.forEach.call(document.querySelectorAll('#set-content input.btn[type="reset"]'), function (el) {
                el.style.setProperty('background-image', 'none', 'important');
                el.style.setProperty('background-color', '#c0392b', 'important');
                el.style.setProperty('border', '1px solid rgba(192,57,43,.9)', 'important');
                el.style.setProperty('color', '#fff', 'important');
                el.style.setProperty('-webkit-text-fill-color', '#fff', 'important');
                el.style.setProperty('font-family', 'inherit', 'important');
            });
        });
    }

    // ==================== 启动全部功能(safeInit 包装,防止单模块崩溃拖垮全部) ====================
    safeInit('首页美化',          initHomePolish);
    safeInit('星空背景',          initStarfield);
    safeInit('任务页外壳与横条',    initTaskPage);
    safeInit('个人主页外壳',        initUprofilePage);
    // ★ 2.55.31:主页回复卡片的页脚重排(自带 onDomReady,与上面那条只差顺序,互不依赖)。
    safeInit('主页回复卡片排版',    initUprofilePostCards);
    // ★ 2.55.17:收藏页工具条(radio 胶囊 + 目录「×」的悬停提示)—— 排在页型类之后,
    //   它自己用 onDomReady 等 DOM(safeInit 是 document-start 就跑的)。
    safeInit('收藏页工具条',        initFavorPageUI);
    safeInit('社区论坛任务自动领取', initTaskAutoClaim);
    safeInit('页面加宽',          initPageWiden);
    safeInit('悬浮翻页按钮',      initNavButtons);
    safeInit('图片阅读优化',      initImageViewer);
    safeInit('操作栏合并',        initTipadMerge);
    // ★ 2.55.23:悬赏信息条。排在 initReadFlow(一体流)之前没问题 —— 它只拆 div.tips 里的
    //   两枚 div.tac(站点服务端渲染的静态节点),与楼层容器重排互不相干;自带 onDomReady。
    safeInit('悬赏信息条',        initBountyTips);
    safeInit('签名进头部行',      initPostSignInline);
    safeInit('房子按钮导航面板',  initCrumbHomeNav);
    safeInit('图墙无图/失效图床帖列表化', initThreadListConvert);
    safeInit('公告板块',          initAnnounceBoard);
    safeInit('版块页本版公告条',  initBoardNotice);
    safeInit('版块页工具条',      initBoardToolbar);
    safeInit('版块页顶部',        initBoardTop);
    safeInit('搜索页版块分类',    initSearchForumPicker);
    // ★ 2.53.58:排在「搜索页版块分类」之后、救援之前 —— 它建的那条排序条就在结果表上方,
    //   与自救(索引过期重搜)互不相干;两个模块都自带 onDomReady + 兜底重跑。
    safeInit('搜索排序人气优先',  initSearchSort);
    safeInit('搜索索引过期自救',  initSearchRescue);
    safeInit('底部图例与页脚屏蔽', initHideBottom);
    safeInit('论坛相关白底面板屏蔽', initHideForumRel);
    safeInit('顶部空白工具条屏蔽', initHideTopTool);
    safeInit('导航精简',          initNavSimplify);
    safeInit('页眉短消息图标与新窗口', initHeaderBits);
    safeInit('自动图墙模式',      initWallMode);
    safeInit('隐藏默认头像',      initHideDefaultAvatar);
    safeInit('一体流',            initReadFlow);
    safeInit('悬停高亮开关',      initFloorHoverSwitch);
    // ★ 2.53.47:排在 initReadFlow 之后 —— 一体流会重排楼层容器,等它把 DOM 定下来
    //   再收顶部按钮;本模块内部还额外延后 900ms 补扫一次(它自己的 observer 也在跑)。
    safeInit('楼层顶部行',        initFloorTopBar);
    safeInit('楼层头像比例',      initAvatarRatio);
    safeInit('含图楼层索引',      initImageIndex);
    safeInit('南+ 品牌条',        initBrandBar);
    safeInit('底部位置栏补本贴',  initCrumbTitle);
    safeInit('白底白字转深色',    initWhiteTextFix);
    // ★ 2.55.25:排在 15f 之后 —— 两段都按对比度判定,暗色下 15f 自然不动手(见模块 19 段头);
    //   万一同一元素被两边同时判中,本段的选择器更晚也更具体,提亮结果以本段为准。
    safeInit('黑夜模式',          initReadDark);
    // ★ 2.53.39:购买框架。排在一体流(initReadFlow)之后 —— 它要改的是 div#read_tpc 里
    //   那一块,先让一体流把楼层容器重排完;内部还额外延后 600ms 才起跑自动购买。
    safeInit('购买框架',          initBuyFramework);
    // ★ 2.53.0:排在 initReadFlow 之后 —— 一体流会重排楼层容器,先让它把 DOM 定下来再展开,
    //   否则展开动作可能作用在随后被搬走的节点上(那就是「有时展开、有时没展开」)。
    safeInit('自动展开隐藏楼层',  initAutoExpandHidden);
    // ★ 2.53.1:悬停弹层重排要在 DOM 定下来之后(同 15d 的理由),排在最后两个。
    safeInit('悬停弹层白底化',    initUserStatusCard);
    safeInit('设置面板',          initSettingsPanel);
    safeInit('BBCode预览按钮',    initPreviewButton);
    safeInit('编辑器快捷工具栏',  initEditorToolbar);
    safeInit('表情面板优化',      initEmojiPanel);
    safeInit('个人中心页美化',      initProfileSkin);
    safeInit('个人中心页排版',      initProfileLayout);
    // ★ 排在最末:它要等 initPreviewButton 把 #spp-preview-btn 建出来才包得住「提交 + 预览」
    safeInit('快速发帖框美化',      initFastReply);

    // 模块体检结果挂到 DOM 上(页面世界可见)——见 safeInit 上方那段注释
    publishInitReport();
GM_addStyle(`
/* ===== SPP-PROFILE-BEGIN ===== */
            /* ==================== 2.52.17:用户中心 / 版块权限查看页(profile.php)====================
               ★ 需求:屏蔽顶部 logo 横幅;内容区换磨砂黑玻璃并把表格网格化;左侧菜单按
                 uiverse 侧边栏组件(mobinkakei/silent-kangaroo-88)的思路重做 —— 悬停左滑 +
                 左侧竖条点亮;原组件是浅底配色,这里整套改成深底(描边/字色/高光全部换掉)。
               ★ 用 :has(#set-wrap) 把整套锁死在个人中心页:#set-wrap 只在 profile.php 出现,
                 不必新增开关,也不会碰版块页/首页(那边的 #header > table 由各自开关管)。
               ★ 玻璃配方逐字同步自 SPP-ANNOUNCE / SPP-GLASS 段(半透黑 + 斜向高光 + blur18)。
               ★ 本页的 a 已从全站链接皮肤链排除(文件上方 :not(#set-wrap a)),所以这里必须
                 给 a 显式写 color 与 -webkit-text-fill-color,否则会掉回站点旧链接色。 */
            html body:has(#set-wrap) #header > table { display:none !important; }

            /* ---- 页头底色(2.52.75)----
               ★ 用户第三张截图圈的那条「灰色线」就是这条:
                 站点 #header 是一块 **30px 高、浅底 rgba(244,248,252,.8)** 的容器,里面
                 #mainNav(黑)只有 28px、横幅 table 又被上面那条规则藏了 —— 于是底部剩下
                 **2px 浅灰条**(实测 rgb(138,140,149),跨满 1400px),在深色页面上就是一条灰线。
               ★ 版块页/首页不受影响:那边的 #infobox 从 y=28 起铺了 1400×250 的深色卡,
                 正好把这段浅底盖住(探针实测 elementsFromPoint(1200,29) = #infobox)。
                 本页族(profile / userpay / …)没有 #infobox,所以必须自己把底色压透明。
               ★ 只动底色,不动高度:横幅在别的页面上若可见,仍按站点原本的排布。 */
            html body:has(#set-wrap) #header {
                background-color:transparent !important; background-image:none !important;
            }

            /* ---- 面包屑行(站点本来是 #f8f8f8 白灰条)----
               ★ 2.52.73:用户截图圈出这一行 ——「箭头指向一个灰色板块」。
               成因就是下面这版皮肤:满宽 27px 的浅底 + 1px 描边 + 10px 圆角,
               而里面只有最左几个字,宽窗口下整条读起来像一块无意义的空面板。
               这里只压掉「面板感」(底色 / 描边 / 圆角 / 投影 / 内边距),
               面包屑文字与配色照旧保留 —— 用户选的是「保留文字、去掉灰底」。 */
            html body:has(#set-wrap) #main > .bdbA {
                background:none !important; background-color:transparent !important; background-image:none !important;
                border:0 !important; border-radius:0 !important; box-shadow:none !important;
                padding:0 !important; margin:0 0 8px !important; min-height:0 !important;
            }
            html body:has(#set-wrap) #main > .bdbA #breadcrumbs { background:none !important; border:0 !important; }
            html body:has(#set-wrap) #breadcrumbs .crumbs-item,
            html body:has(#set-wrap) #breadcrumbs .crumbs-item a {
                color:#9fb0c8 !important; -webkit-text-fill-color:#9fb0c8 !important; font-size:13px !important;
            }
            html body:has(#set-wrap) #breadcrumbs .crumbs-item:hover,
            html body:has(#set-wrap) #breadcrumbs .crumbs-item:hover a { color:#fff !important; -webkit-text-fill-color:#fff !important; }
            html body:has(#set-wrap) #breadcrumbs .crumbs-item.current,
            html body:has(#set-wrap) #breadcrumbs .crumbs-item.current a,
            html body:has(#set-wrap) #breadcrumbs .crumbs-item.current strong {
                color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important; font-weight:600 !important;
            }
            /* 实测:这一行面包屑上有两枚分隔 —— 站点 <i>(细竖线)+ 站点 ::before 的字形,
               所以只换 <i> 会出现「一大一小两枚箭头」。这里把 <i> 整个收掉,
               统一由 .crumbs-item + .crumbs-item::before 画一枚箭头(本来没有 ::before 也会因此生成)。 */
            html body:has(#set-wrap) #breadcrumbs .crumbs-item i { display:none !important; }
            /* ★ 实测(CDP 读到的计算样式):真正的箭头来自 .crumbs-item 自身的背景精灵
               url("https://blue-plus.net/images/colorI…"),既不是 <i> 也不是 ::before ——
               只换 <i>(2.52.24)或只收 <i>(2.52.25)都会剩下这枚精灵箭头。这里把它清掉。 */
            html body:has(#set-wrap) #breadcrumbs .crumbs-item {
                background-image:none !important; background-color:transparent !important;
                padding-left:0 !important;
            }
            html body:has(#set-wrap) #breadcrumbs .crumbs-item + .crumbs-item::before {
                content:'›' !important;
                display:inline-block !important; visibility:visible !important; opacity:1 !important;
                width:auto !important; height:auto !important; min-width:0 !important;
                margin:0 9px 0 1px !important; padding:0 !important; float:none !important;
                background:none !important; background-image:none !important;
                border:0 !important; border-radius:0 !important; transform:none !important;
                color:rgba(32,201,151,.78) !important; -webkit-text-fill-color:rgba(32,201,151,.78) !important;
                font-family:inherit !important; font-size:14px !important; font-weight:700 !important;
                line-height:1 !important; vertical-align:middle !important;
                transition:color .18s ease !important;
            }
            html body:has(#set-wrap) #breadcrumbs .crumbs-item:hover::before {
                color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
            }

            /* ---- 两栏磨砂黑玻璃 ---- */
            html body:has(#set-wrap) #set-side-wrap,
            html body:has(#set-wrap) #set-content {
                background-color:rgb(0 0 0 / 0.86) !important;
                background-image:linear-gradient(152deg,
                    rgb(255 255 255 / 0.05) 0%,      /* 2.55.30:同族高光一起压暗(.11 → .05),理由见 2.55.30 段头 */
                    rgb(255 255 255 / 0.016) 22%,
                    rgb(255 255 255 / 0.003) 46%,
                    rgb(255 255 255 / 0.000) 62%,
                    rgb(255 255 255 / 0.045) 100%) !important;
                border:1px solid rgb(255 255 255 / 0.09) !important;
                border-radius:14px !important;
                box-shadow:0 12px 32px -14px rgb(0 0 0 / 0.72),
                    inset 0 1px 0 rgb(255 255 255 / 0.22) !important;
                backdrop-filter:blur(18px) saturate(140%) !important;
                -webkit-backdrop-filter:blur(18px) saturate(140%) !important;
                overflow:hidden !important;
                color:#dfe7f3 !important;
            }
            html body:has(#set-wrap) #set-wrap #set-side-wrap *,
            html body:has(#set-wrap) #set-wrap #set-content * { color:#dfe7f3 !important; -webkit-text-fill-color:#dfe7f3 !important; }
            html body:has(#set-wrap) #set-content a { color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important; }

            /* ---- 左侧:标题 + 菜单(悬停左滑 + 左侧竖条点亮)---- */
            html body:has(#set-wrap) #set-side-wrap h2.set-h2 {
                background-color:transparent !important; background-image:none !important;
                border-bottom:1px solid rgba(255,255,255,.10) !important;
                padding:12px 14px !important; margin:0 !important; font-size:14px !important;
            }
            html body:has(#set-wrap) #set-side-wrap h2.set-h2 a {
                color:#fff !important; -webkit-text-fill-color:#fff !important;
                font-weight:700 !important; letter-spacing:.4px !important;
            }
            html body:has(#set-wrap) #set-menu { list-style:none !important; margin:0 !important; padding:8px !important; }
            html body:has(#set-wrap) #set-menu > li { list-style:none !important; margin:0 0 2px !important; padding:0 !important; }
            html body:has(#set-wrap) #set-wrap #set-menu > li > a {
                position:relative !important; display:block !important;
                padding:9px 12px 9px 16px !important; border-radius:9px !important;
                font-size:13px !important; text-decoration:none !important;
            }
            html body:has(#set-wrap) #set-wrap #set-menu > li > a::before {
                content:'' !important; position:absolute !important;
                left:6px !important; top:50% !important; width:3px !important; height:0 !important;
                border-radius:999px !important; background-color:#20c997 !important;
                transform:translateY(-50%) !important; opacity:0 !important;
            }
            html body:has(#set-wrap) #set-wrap #set-menu > li > a:hover {
                transition:background-color .22s ease, color .22s ease, padding-left .22s ease !important;
                background-color:rgba(255,255,255,.07) !important;
                color:#fff !important; -webkit-text-fill-color:#fff !important;
                padding-left:20px !important;
            }
            html body:has(#set-wrap) #set-wrap #set-menu > li > a:hover::before { transition:height .22s ease, opacity .22s ease !important; height:58% !important; opacity:1 !important; }
            html body:has(#set-wrap) #set-menu > li.current > a,
            html body:has(#set-wrap) #set-menu > li.on > a,
            html body:has(#set-wrap) #set-menu > li.selected > a,
            html body:has(#set-wrap) #set-menu > li > a.cur,
            html body:has(#set-wrap) #set-menu > li > a.on {
                background-color:rgba(32,201,151,.14) !important;
                color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important; font-weight:600 !important;
            }
            html body:has(#set-wrap) #set-menu > li.current > a::before,
            html body:has(#set-wrap) #set-menu > li.on > a::before,
            html body:has(#set-wrap) #set-menu > li.selected > a::before,
            html body:has(#set-wrap) #set-menu > li > a.cur::before,
            html body:has(#set-wrap) #set-menu > li > a.on::before { height:58% !important; opacity:1 !important; }

            /* ---- 右侧:标题条 + 表格网格化 ---- */
            html body:has(#set-wrap) #set-content .set-tab-table {
                background-color:transparent !important; background-image:none !important;
                border-bottom:1px solid rgba(255,255,255,.10) !important;
                padding:12px 16px !important; font-size:14px !important; font-weight:700 !important;
            }
            html body:has(#set-wrap) #set-content .set-tab-table * { color:#fff !important; -webkit-text-fill-color:#fff !important; }
            html body:has(#set-wrap) #set-content .set-tab-box { padding:14px 16px 18px !important; }
            /* ★ 站点在内容区留了几道白线:页签 td 的 1px 描边、区块之间的横线、内层 div 的框线。
               这里只清边框/阴影,不动 table.set-table2 里的格线(那是我们要的网格)。 */
            html body:has(#set-wrap) #set-content div,
            html body:has(#set-wrap) #set-content .set-tab-table,
            html body:has(#set-wrap) #set-content .set-tab-table table,
            html body:has(#set-wrap) #set-content .set-tab-table td,
            html body:has(#set-wrap) #set-content-wrap {
                border:0 !important; border-width:0 !important;
                outline:0 !important; box-shadow:none !important;
            }
            /* 小节标题行(实测:table.set-table2 > tr.tr32:first-child > td[colspan] > b)*/
            html body:has(#set-wrap) #set-content table.set-table2 tr.tr32:first-child > td {
                background-color:rgba(255,255,255,.045) !important;
                border-left:3px solid rgba(32,201,151,.85) !important;
                padding:11px 14px !important; text-align:left !important;
                font-size:13.5px !important; font-weight:700 !important; letter-spacing:.2px !important;
                color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
            }
            /* 列头行(实测:tr.tr2.tac > td)*/
            html body:has(#set-wrap) #set-content table.set-table2 tr.tr2 > td {
                background-color:rgba(255,255,255,.05) !important;
                color:#9fb0c8 !important; -webkit-text-fill-color:#9fb0c8 !important;
                font-weight:600 !important; letter-spacing:.3px !important;
            }
            /* 数据行(tr.tr32 首行之外 / tr.tr3):首列主色绿,其余近白 */
            html body:has(#set-wrap) #set-content table.set-table2 tr.tr32:not(:first-child) > td:not(.spp-tick),
            html body:has(#set-wrap) #set-content table.set-table2 tr.tr3 > td:not(.spp-tick) {
                color:#dfe7f3 !important; -webkit-text-fill-color:#dfe7f3 !important;
            }
            html body:has(#set-wrap) #set-content table.set-table2 tr.tr32:not(:first-child) > td:first-child:not(.spp-tick),
            html body:has(#set-wrap) #set-content table.set-table2 tr.tr3 > td:first-child:not(.spp-tick) {
                text-align:left !important; font-weight:600 !important;
                color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
            }
            /* 权限勾(√):CSS 不能按文本选中,由 initProfileSkin() 打 .spp-tick */
            html body:has(#set-wrap) #set-content table.set-table2 td.spp-tick {
                font-size:15px !important; font-weight:700 !important;
                color:#3ef58c !important; -webkit-text-fill-color:#3ef58c !important;
            }
            html body:has(#set-wrap) #set-content table.set-table2 {
                width:100% !important; max-width:100% !important; margin:0 !important;
                border-collapse:separate !important; border-spacing:0 !important;
                border:1px solid rgba(255,255,255,.10) !important; border-radius:10px !important;
                overflow:hidden !important; background-color:rgba(255,255,255,.02) !important;
            }
            html body:has(#set-wrap) #set-content table.set-table2 td,
            html body:has(#set-wrap) #set-content table.set-table2 th {
                border:0 !important; border-right:1px solid rgba(255,255,255,.07) !important;
                border-bottom:1px solid rgba(255,255,255,.07) !important;
                background-color:transparent !important;
                padding:11px 12px !important; text-align:center !important;
                font-size:13px !important; font-weight:400 !important;
            }
            html body:has(#set-wrap) #set-content table.set-table2 tr > td:last-child,
            html body:has(#set-wrap) #set-content table.set-table2 tr > th:last-child { border-right:0 !important; }
            html body:has(#set-wrap) #set-content table.set-table2 tr:last-child > td,
            html body:has(#set-wrap) #set-content table.set-table2 tr:last-child > th { border-bottom:0 !important; }
            html body:has(#set-wrap) #set-content table.set-table2 tr:hover > td { background-color:rgba(32,201,151,.07) !important; }

            /* ==================== 2.52.69:个人中心页排版重做(方案 B · 暗色浮雕)====================
               ★ 来由:用户指着实站截图提三件事 ——
                 ① 顶部那条面包屑「长度不匹配」(绿框);② 三块内容(设置中心侧栏 / 资料区 / 右侧面板)
                 「字体排版不对,要用 UI 框包裹」;③ 样式参考 uiverse lenfear23/empty-lion-5(MIT)。
               ★ 方案 B = 「同一套浮雕手法,色值改暗」:外层仍是既有磨砂黑玻璃,内层做成内凹托盘
                 (上高光 / 下暗影 / inset 凹槽),点缀色照旧 #20c997 / #8ef0b8。
                 为什么不用那款组件的原色:它是浅色金属板(linear-gradient(#D8D9DB→#fff) + #8F9092 边),
                 搬进星空黑底会像贴上去的另一套系统;浮雕的「手法」留下了,色值换成本站体系。
                 预览见 preview/个人中心页美化.html 的方案 B 舞台(那边保留了提案当时的左右并排统计格)。

               ---- ① 绿框:面包屑条「长度不匹配」------------------------------------------------
               ★ 实测:.bdbA 实宽 1425、右缘 1578;而 #mainNav / #set-wrap / #set-content 右缘都是 1554。
                 根因在【本脚本自己】:站点给 .bdbA 写了 width:100%!,2.52.17 又给它加了 padding:0 12px,
                 而它是 content-box → 1400 + 24 = 1424,整条往右凸出 24px。
                 ★ 修法是补 box-sizing,不动站点那句 width(动 width 会连带影响它内部的 #breadcrumbs)。 */
            html body:has(#set-wrap) #main > .bdbA { box-sizing:border-box !important; width:100% !important; }
            html body:has(#set-wrap) #main > .bdbA #breadcrumbs { box-sizing:border-box !important; width:100% !important; margin:0 !important; }

            /* ---- ② 内容区留白:内外两层壳不再各留一份 ----
               ★ 2.52.70 修一个 2.52.69 自己引入的「左上大片空」:
                 站点给 .set-content-left 写了 margin:0 320px 0 0 —— 那 320px 正是给右列(float:right 300px)
                 让位用的。2.52.69 图省事把它清零,于是左列的 content 宽变成 1202,
                 而左列里那个托盘是【float:left; width:100%】(站点自己写的行内样式)——
                 一个 100% 宽的浮动挤不进 300px 浮动的旁边,按 float 排布规则被顶到右列下方,
                 左上就空出一整块(用户 2026-09-30 截图圈的就是它,实测托盘 y 从 96 掉到 400+)。
                 ★ 修法:把站点那 320px 让位显式写回来,不动别的东西。 */
            html body:has(#set-wrap) #set-content-wrap { padding:14px !important; }
            html body:has(#set-wrap) #set-content .set-content-left { margin:0 320px 0 0 !important; padding:0 !important; }
            html body:has(#set-wrap) #set-content .set-content-right { padding:0 !important; }

            /* ---- ③ 资料区:内凹托盘 + 头像底座 + 名字行 + 签名 + 快捷入口 ----
               ★ 托盘那层(> div:first-child)的卡框由 initProfileSkin() 内联钉死 —— 站点在读不到的
                 样式表里给 #set-content div 写了同权重的 border/box-shadow !important,选择器打不过它。 */
            html body:has(#set-wrap) #set-content .set-content-left > div:first-child {
                box-sizing:border-box !important; padding:16px 18px 14px !important;
            }
            html body:has(#set-wrap) #set-content .set-content-left td.tac { width:118px !important; padding-right:16px !important; }
            html body:has(#set-wrap) #set-content .set-content-left img.pic {
                width:104px !important; height:104px !important; object-fit:cover !important;
                border-radius:18px !important; border:1px solid rgba(255,255,255,.14) !important;
                padding:3px !important; background-color:rgba(255,255,255,.045) !important;
                box-shadow:inset 0 1px 0 rgba(255,255,255,.18), 0 10px 22px -12px rgba(0,0,0,.9) !important;
            }
            html body:has(#set-wrap) #set-content .set-content-left td.tac > div { padding:9px 0 0 !important; }
            html body:has(#set-wrap) #set-content .set-content-left td.tac a.gray3 {
                display:inline-block !important; padding:4px 11px !important; border-radius:999px !important;
                background-color:rgba(255,255,255,.055) !important; border:1px solid rgba(255,255,255,.10) !important;
                color:#cfe0f5 !important; -webkit-text-fill-color:#cfe0f5 !important;
                font-size:11.5px !important; letter-spacing:.3px !important; text-decoration:none !important;
            }
            html body:has(#set-wrap) #set-content .set-content-left td.tac a.gray3:hover {
                transition:background-color .2s ease, border-color .2s ease !important;
                background-color:rgba(32,201,151,.18) !important; border-color:rgba(32,201,151,.5) !important;
                color:#eafff6 !important; -webkit-text-fill-color:#eafff6 !important;
            }
            /* 名字行:名字 + (数字ID:…) 胶囊 + 编辑资料按钮,一行排开 */
            html body:has(#set-wrap) #set-content .set-content-left .bdbdash:first-child > div:first-child {
                display:flex !important; align-items:center !important; flex-wrap:wrap !important; gap:9px !important;
                padding:0 0 2px !important;
            }
            html body:has(#set-wrap) #set-content .set-content-left span[style*="bold"] {
                font-family:inherit !important; font-size:19px !important; font-weight:700 !important;
                letter-spacing:.4px !important; color:#fff !important; -webkit-text-fill-color:#fff !important;
            }
            html body:has(#set-wrap) #set-content .set-content-left span.gray3 {
                padding:3px 9px !important; border-radius:999px !important;
                background-color:rgba(255,255,255,.055) !important; border:1px solid rgba(255,255,255,.10) !important;
                font-size:11px !important; letter-spacing:.3px !important;
                color:#9fb0c8 !important; -webkit-text-fill-color:#9fb0c8 !important;
            }
            html body:has(#set-wrap) #set-content .set-content-left a.link5 {
                display:inline-flex !important; align-items:center !important; gap:5px !important;
                padding:5px 12px !important; border-radius:999px !important;
                background-color:rgba(32,201,151,.13) !important; border:1px solid rgba(32,201,151,.42) !important;
                color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
                font-size:12.5px !important; font-weight:600 !important; letter-spacing:.2px !important;
                text-decoration:none !important;
            }
            html body:has(#set-wrap) #set-content .set-content-left a.link5:hover {
                transition:background-color .2s ease, border-color .2s ease, color .2s ease !important;
                background-color:rgba(32,201,151,.24) !important; border-color:rgba(32,201,151,.85) !important;
                color:#eafff6 !important; -webkit-text-fill-color:#eafff6 !important;
            }
            /* 个性签名:单独一条内凹条,左侧亮绿竖条 */
            html body:has(#set-wrap) #set-content .set-content-left span#honor {
                display:block !important; box-sizing:border-box !important;
                margin:2px 0 10px !important; padding:9px 13px !important; border-radius:10px !important;
                background-color:rgba(255,255,255,.028) !important;
                background-image:linear-gradient(to top, rgba(0,0,0,.26) 0%, rgba(255,255,255,.04) 100%) !important;
                border-left:3px solid rgba(32,201,151,.55) !important;
                box-shadow:inset 0 2px 4px rgba(0,0,0,.40) !important;
                font-size:13.5px !important; line-height:1.65 !important;
                color:#cbd6e6 !important; -webkit-text-fill-color:#cbd6e6 !important;
            }
            html body:has(#set-wrap) #set-content .set-content-left a#sethonor { margin-left:6px !important; }
            html body:has(#set-wrap) #set-content .set-content-left a#sethonor img { display:none !important; }
            /* 快捷入口那一行:我的首页 / 主题 / 回复 / 收藏 —— 各自一枚胶囊,不再是 &nbsp; 挤成一串 */
            html body:has(#set-wrap) #set-content .set-content-left .bdbdash:first-child > div:last-child {
                display:flex !important; flex-wrap:wrap !important; gap:8px !important; padding:2px 0 0 !important;
            }
            html body:has(#set-wrap) #set-content .set-content-left .bdbdash:first-child > div:last-child a.link5 {
                background-color:rgba(255,255,255,.055) !important; border:1px solid rgba(255,255,255,.10) !important;
                color:#cfe0f5 !important; -webkit-text-fill-color:#cfe0f5 !important; font-weight:500 !important;
            }
            html body:has(#set-wrap) #set-content .set-content-left .bdbdash:first-child > div:last-child a.link5:hover {
                background-color:rgba(255,255,255,.10) !important; border-color:rgba(255,255,255,.22) !important;
                color:#fff !important; -webkit-text-fill-color:#fff !important;
            }

            /* ---- ④ 统计区:站点用 <br> 把 10 项数据折成两列文本塞进两个 <td>,这里重排成网格 ----
               ★ 每个格子【上下两行】:标签在上、数值在下(用户 2026-09-30 定案)。
                 原先是左右并排,遇到「在线时间 579 小时 / 注册时间 2021-12-12」这种长值就会在格子里折行
                 (用户截图里「注册时 间」「2021-12-」「12」断成两行)。
               ★ 结构与类名由 initProfileLayout() 里的 buildProfileStats() 现场生成(CSS 不能按 <br> 切分)。 */
            html body:has(#set-wrap) #set-content .set-content-left .spp-stats {
                display:grid !important; grid-template-columns:repeat(auto-fill, minmax(150px, 1fr)) !important;
                gap:8px !important; margin:0 !important; padding:0 !important;
            }
            html body:has(#set-wrap) #set-content .set-content-left .spp-stat {
                box-sizing:border-box !important;
                display:flex !important; flex-direction:column !important; align-items:flex-start !important; gap:2px !important;
                padding:9px 12px !important; border-radius:10px !important;
                background-color:rgba(9,13,21,.55) !important;
                background-image:linear-gradient(to top, rgba(0,0,0,.35) 0%, rgba(255,255,255,.035) 100%) !important;
                border:1px solid rgba(255,255,255,.06) !important;
                box-shadow:inset 0 2px 4px rgba(0,0,0,.55), inset 0 -1px 0 rgba(255,255,255,.05) !important;
            }
            html body:has(#set-wrap) #set-content .set-content-left .spp-stat em {
                font-style:normal !important; font-size:11.5px !important; line-height:1.5 !important; letter-spacing:.3px !important;
                white-space:nowrap !important; color:#93a4bc !important; -webkit-text-fill-color:#93a4bc !important;
            }
            html body:has(#set-wrap) #set-content .set-content-left .spp-stat b {
                font-size:14.5px !important; font-weight:600 !important; line-height:1.5 !important; letter-spacing:.2px !important;
                white-space:nowrap !important; color:#eaf2fb !important; -webkit-text-fill-color:#eaf2fb !important;
            }
            html body:has(#set-wrap) #set-content .set-content-left .spp-stat.is-accent b { color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important; }

            /* ---- ⑤ 右侧两块面板:各自成卡(卡框同样由 JS 内联钉死)---- */
            html body:has(#set-wrap) #set-content .set-content-right > div:not(.c) {
                box-sizing:border-box !important; overflow:hidden !important;
            }
            html body:has(#set-wrap) #set-content .set-content-right .set-tab-table {
                padding:0 !important; border-bottom:1px solid rgba(255,255,255,.08) !important;
                background-color:rgba(255,255,255,.03) !important;
            }
            html body:has(#set-wrap) #set-content .set-content-right .set-tab-table table { width:100% !important; }
            html body:has(#set-wrap) #set-content .set-content-right .set-tab-table td {
                background-color:transparent !important; padding:10px 12px !important; vertical-align:middle !important;
            }
            html body:has(#set-wrap) #set-content .set-content-right .set-tab-table td.current {
                position:relative !important; text-align:left !important; padding-left:22px !important;
                font-size:13px !important; font-weight:700 !important; letter-spacing:.3px !important;
                color:#fff !important; -webkit-text-fill-color:#fff !important;
            }
            html body:has(#set-wrap) #set-content .set-content-right .set-tab-table td.current::before {
                content:'' !important; position:absolute !important; left:11px !important; top:50% !important;
                width:3px !important; height:14px !important; margin-top:-7px !important;
                border-radius:999px !important; background-color:#20c997 !important;
            }
            html body:has(#set-wrap) #set-content .set-content-right .set-tab-table td:not(.current) { text-align:right !important; }
            html body:has(#set-wrap) #set-content .set-content-right .set-tab-table td:not(.current) a.link5 {
                display:inline-block !important; padding:4px 11px !important; border-radius:999px !important;
                background-color:rgba(32,201,151,.12) !important; border:1px solid rgba(32,201,151,.38) !important;
                color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
                font-size:11.5px !important; font-weight:500 !important; text-decoration:none !important;
            }
            html body:has(#set-wrap) #set-content .set-content-right .set-tab-table td:not(.current) a.link5:hover {
                transition:background-color .2s ease !important; background-color:rgba(32,201,151,.24) !important;
                color:#eafff6 !important; -webkit-text-fill-color:#eafff6 !important;
            }
            html body:has(#set-wrap) #set-content .set-content-right .set-tab-box { padding:6px 12px 10px !important; }
            html body:has(#set-wrap) #set-content .set-content-right table.set-table1 td.gray3 {
                padding:8px 2px !important; font-size:11.5px !important; letter-spacing:.4px !important;
                border-bottom:1px solid rgba(255,255,255,.055) !important;
                color:#8b9bb2 !important; -webkit-text-fill-color:#8b9bb2 !important;
            }

            /* ---- ⑥ 侧栏:标题竖条 + 菜单「当前项」(当前项由 JS 按站点自己的 href 判据打)---- */
            html body:has(#set-wrap) #set-side-wrap h2.set-h2 {
                position:relative !important; padding-left:20px !important; letter-spacing:.5px !important;
            }
            html body:has(#set-wrap) #set-side-wrap h2.set-h2::before {
                content:'' !important; position:absolute !important; left:12px !important; top:50% !important;
                width:3px !important; height:15px !important; margin-top:-7.5px !important;
                border-radius:999px !important; background-color:#20c997 !important;
            }
            html body:has(#set-wrap) #set-menu > li > a { padding:10px 12px 10px 16px !important; font-size:13px !important; }
            html body:has(#set-wrap) #set-menu > li > a::after {
                content:'›' !important; position:absolute !important; right:11px !important; top:50% !important;
                transform:translateY(-50%) !important; font-size:14px !important; line-height:1 !important;
                color:rgba(159,176,200,.55) !important; -webkit-text-fill-color:rgba(159,176,200,.55) !important;
            }
            html body:has(#set-wrap) #set-menu > li > a:hover::after { color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important; }
            html body:has(#set-wrap) #set-wrap #set-menu > li.spp-cur > a {
                background-color:rgba(32,201,151,.14) !important;
                color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important; font-weight:600 !important;
            }
            html body:has(#set-wrap) #set-wrap #set-menu > li.spp-cur > a::before { height:58% !important; opacity:1 !important; }

            /* ==================== 2.52.71:个人中心页表单控件(输入框 / 下拉 / 单选 / 文件 / 按钮)====================
               ★ 用户指着左侧菜单(编辑资料 / 论坛设置 / 积分转换 / 特殊组购买 …)说:
                 「点进去后,若需填写信息,部分区域会出现白底填空栏,此处需适配黑色背景」。
               ★ 实测根因有两层(profile.php?action-modify-info_type-face.html):
                 ① 站点给表单控件写的是 background-color:rgb(255,255,255)、border:.667px #ddd、
                    border-radius:0、font-family:Arial —— 白色空框;
                 ② 2.52.17 那条「#set-wrap #set-content * { color:#dfe7f3 !important }」把控件里的文字
                    也一起钉成了近白 —— 于是变成【白底白字】,看着就是一排发白的空框(字几乎看不见)。
               ★ 作用域仍锁在 body:has(#set-wrap):profile.php 的**所有子页**(编辑资料 / 论坛设置 /
                 积分转换 / 特殊组购买 / 修改头像…)都带 #set-wrap,所以这一套一次覆盖,不必逐页加。
               ★ 配色沿用本站暗色体系:暗玻璃底 + 内凹阴影 + 9px 圆角;聚焦走亮绿环(#20c997 / #8ef0b8)。 */
            html body:has(#set-wrap) #set-content input.input,
            html body:has(#set-wrap) #set-content select.input,
            html body:has(#set-wrap) #set-content textarea.input,
            html body:has(#set-wrap) #set-content input[type="text"],
            html body:has(#set-wrap) #set-content input[type="password"],
            html body:has(#set-wrap) #set-content input[type="email"],
            html body:has(#set-wrap) #set-content input[type="number"],
            html body:has(#set-wrap) #set-content input[type="url"],
            html body:has(#set-wrap) #set-content input[type="search"],
            html body:has(#set-wrap) #set-content input[type="tel"],
            html body:has(#set-wrap) #set-content textarea,
            html body:has(#set-wrap) #set-content select {
                box-sizing:border-box !important;
                background-color:rgba(9,13,21,.55) !important;
                background-image:linear-gradient(to top, rgba(0,0,0,.30) 0%, rgba(255,255,255,.045) 100%) !important;
                border:1px solid rgba(255,255,255,.10) !important;
                border-radius:9px !important;
                color:#eaf2fb !important; -webkit-text-fill-color:#eaf2fb !important;
                font-family:inherit !important; font-size:13px !important; line-height:1.45 !important;
                padding:6px 10px !important; height:auto !important; min-height:32px !important;
                box-shadow:inset 0 2px 4px rgba(0,0,0,.45) !important;
                outline:none !important;
                transition:border-color .2s ease, box-shadow .2s ease, background-color .2s ease !important;
            }
            html body:has(#set-wrap) #set-content input.input:hover,
            html body:has(#set-wrap) #set-content textarea:hover,
            html body:has(#set-wrap) #set-content select:hover { border-color:rgba(255,255,255,.20) !important; }
            html body:has(#set-wrap) #set-content input.input:focus,
            html body:has(#set-wrap) #set-content textarea:focus,
            html body:has(#set-wrap) #set-content select:focus {
                border-color:rgba(32,201,151,.78) !important;
                background-color:rgba(9,13,21,.72) !important;
                box-shadow:inset 0 2px 4px rgba(0,0,0,.35), 0 0 0 3px rgba(32,201,151,.16) !important;
            }
            html body:has(#set-wrap) #set-content input.input::placeholder,
            html body:has(#set-wrap) #set-content textarea::placeholder { color:#7d8ca3 !important; -webkit-text-fill-color:#7d8ca3 !important; }
            html body:has(#set-wrap) #set-content input.input:disabled,
            html body:has(#set-wrap) #set-content input.input[readonly],
            html body:has(#set-wrap) #set-content textarea:disabled,
            html body:has(#set-wrap) #set-content textarea[readonly] {
                color:#8b9bb2 !important; -webkit-text-fill-color:#8b9bb2 !important;
                background-color:rgba(255,255,255,.03) !important; box-shadow:none !important;
                cursor:not-allowed !important;
            }
            /* 小数值格(头像宽度/高度、生日…):站点靠 size=2/3 定宽 → 44px 放不下「150」 */
            html body:has(#set-wrap) #set-content input.input[size="1"],
            html body:has(#set-wrap) #set-content input.input[size="2"],
            html body:has(#set-wrap) #set-content input.input[size="3"],
            html body:has(#set-wrap) #set-content input.input[size="4"] {
                width:78px !important; text-align:center !important; padding:6px 8px !important;
            }
            /* 下拉:原生箭头在暗底上几乎看不见,自绘一枚(渐变底与箭头分两层写) */
            html body:has(#set-wrap) #set-content select,
            html body:has(#set-wrap) #set-content select.input {
                -webkit-appearance:none !important; appearance:none !important;
                padding-right:28px !important; cursor:pointer !important;
                background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='6'%3E%3Cpath d='M0 0h9L4.5 6z' fill='%239aa8bd'/%3E%3C/svg%3E"), linear-gradient(to top, rgba(0,0,0,.30) 0%, rgba(255,255,255,.045) 100%) !important;
                background-repeat:no-repeat, no-repeat !important;
                background-position:right 10px center, 0 0 !important;
                background-size:9px 6px, auto !important;
            }
            /* 文本域:给足最小高度与行高 */
            html body:has(#set-wrap) #set-content textarea,
            html body:has(#set-wrap) #set-content textarea.input {
                min-height:78px !important; line-height:1.65 !important; resize:vertical !important;
            }
            /* 单选 / 复选:原生控件用 accent-color 上色(比自绘安全,键盘与读屏不受影响) */
            html body:has(#set-wrap) #set-content input[type="radio"],
            html body:has(#set-wrap) #set-content input[type="checkbox"] {
                accent-color:#20c997 !important;
                width:14px !important; height:14px !important;
                vertical-align:middle !important; margin:0 5px 0 0 !important; cursor:pointer !important;
            }
            /* 文件选择(修改头像 → 头像上传) */
            html body:has(#set-wrap) #set-content input[type="file"] {
                box-sizing:border-box !important; padding:5px 8px !important; min-height:0 !important;
                border:1px dashed rgba(255,255,255,.16) !important; border-radius:9px !important;
                background-color:rgba(9,13,21,.40) !important; background-image:none !important;
                color:#9fb0c8 !important; -webkit-text-fill-color:#9fb0c8 !important; font-size:12.5px !important;
            }
            html body:has(#set-wrap) #set-content input[type="file"]::file-selector-button {
                margin-right:10px !important; padding:4px 11px !important; border-radius:7px !important;
                border:1px solid rgba(255,255,255,.12) !important; background-color:rgba(255,255,255,.07) !important;
                color:#dfe7f3 !important; font-family:inherit !important; font-size:12.5px !important; cursor:pointer !important;
            }
            /* 提交 / 重置按钮:站点给它的是 bg_btn.gif 位图 + #bbb 底 —— 与全站按钮不是一套皮 */
            html body:has(#set-wrap) #set-content input.btn[type="submit"],
            html body:has(#set-wrap) #set-content input.btn[type="button"],
            html body:has(#set-wrap) #set-content button.btn {
                box-sizing:border-box !important; height:34px !important; min-height:0 !important;
                padding:0 20px !important; border-radius:9px !important;
                background-image:none !important; background-color:#188a67 !important;
                border:1px solid rgba(32,201,151,.85) !important;
                color:#eafff6 !important; -webkit-text-fill-color:#eafff6 !important;
                font-family:inherit !important; font-size:13.5px !important; font-weight:600 !important;
                letter-spacing:.3px !important; text-shadow:none !important;
                box-shadow:0 6px 16px -8px rgba(32,201,151,.75), inset 0 1px 0 rgba(255,255,255,.22) !important;
                cursor:pointer !important;
            }
            html body:has(#set-wrap) #set-content input.btn[type="submit"]:hover,
            html body:has(#set-wrap) #set-content input.btn[type="button"]:hover,
            html body:has(#set-wrap) #set-content button.btn:hover { background-color:#1f9d76 !important; }
            /* 重置走 2.52.57 那套语义色(次级/清空动作 = 实底红) */
            html body:has(#set-wrap) #set-content input.btn[type="reset"] {
                box-sizing:border-box !important; height:34px !important; min-height:0 !important;
                padding:0 18px !important; border-radius:9px !important;
                background-image:none !important; background-color:#c0392b !important;
                border:1px solid rgba(192,57,43,.9) !important;
                color:#fff !important; -webkit-text-fill-color:#fff !important;
                font-family:inherit !important; font-size:13px !important; font-weight:500 !important;
                cursor:pointer !important;
            }
            html body:has(#set-wrap) #set-content input.btn[type="reset"]:hover { background-color:#d14836 !important; }

            /* ==================== 2.52.72:用户权限表 + 单栏子页正文托盘(积分管理等)====================
               ★ 用户两处反馈:① profile.php?action-permission.html 的权限表整片发白;
                 ② userpay.php(积分管理)页签当前项是白底,且面板里的文字要「用 UI 包裹」。
               ★ 白底源头(实测):
                 · 权限表**没有 .set-table2 类**(「table」 无类),站点给它交替刷了
                   「tr.tr3」 = rgb(238,238,238) / 「tr.tr32」 = rgb(255,255,255) ——
                   而 2.52.17/2.52.19 那些网格规则全都挂在 「table.set-table2」 上,于是一条都没命中;
                 · userpay.php 的页签当前项 「TD#pay_begin.current」 是 rgb(255,255,255),
                   压平它的内联钉死在 initProfileSkin 里,可那个模块原先被 URL 门控挡在 userpay.php 之外
                   (2.52.72 已把门控改成「页面上有 #set-wrap」)。
               ★ 复用 set-table2 那套网格配方,避免同一张表两套皮。 */
            html body:has(#set-wrap) #set-content table tr.tr3,
            html body:has(#set-wrap) #set-content table tr.tr32 {
                background-color:transparent !important; background-image:none !important;
            }
            html body:has(#set-wrap) #set-content table tr.tr3 > td,
            html body:has(#set-wrap) #set-content table tr.tr32 > td {
                background-color:transparent !important; background-image:none !important;
                border-bottom:1px solid rgba(255,255,255,.07) !important;
                border-right:1px solid rgba(255,255,255,.07) !important;
                padding:9px 12px !important; text-align:center !important;
                font-size:13px !important; font-weight:400 !important;
                color:#dfe7f3 !important; -webkit-text-fill-color:#dfe7f3 !important;
            }
            /* 分组标题行(「基本权限 / 帖子权限 / 附件权限」那种整行说明)*/
            html body:has(#set-wrap) #set-content table tr.tr3 > td:first-child {
                text-align:left !important; font-weight:600 !important; letter-spacing:.3px !important;
                color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
            }
            /* 表格外框:与 set-table2 同一套圆角玻璃边 */
            html body:has(#set-wrap) #set-content table:has(tr.tr32) {
                width:100% !important; max-width:100% !important; margin:0 0 10px !important;
                border-collapse:separate !important; border-spacing:0 !important;
                border:1px solid rgba(255,255,255,.10) !important; border-radius:10px !important;
                overflow:hidden !important; background-color:rgba(255,255,255,.02) !important;
            }
            html body:has(#set-wrap) #set-content table:has(tr.tr32) tr > td:last-child { border-right:0 !important; }
            html body:has(#set-wrap) #set-content table:has(tr.tr32) tr:last-child > td { border-bottom:0 !important; }
            /* 勾选/叉号:权限表里若是纯文本,交给 initProfileSkin 打 .spp-tick;这里给颜色兜底 */
            html body:has(#set-wrap) #set-content table td.spp-tick {
                font-size:15px !important; font-weight:700 !important;
                color:#3ef58c !important; -webkit-text-fill-color:#3ef58c !important;
            }

            /* ---- 单栏子页(积分管理 / 积分转换 / 特殊组购买 / 论坛设置 …)----
               ★ 这些页没有 .set-content-left / .set-content-right 两栏,正文直接铺在玻璃卡上;
                 用户要求「用 UI 包裹文本」。这里把正文区(页签行之外的兄弟节点)包成一块内凹托盘。
               ★ 2.52.74:托盘**不能套在 <form> 上**。站点把这类表单写成 display:inline,
                 而 inline 元素的边框/内边距是围绕着**行盒碎片**画的 —— 于是 2.52.72 的这套托盘
                 在编辑资料页(#creator 就是那个 form)只画出一枚 27×38 的圆角小药丸,
                 挂在面板左上角,正是用户圈出的「那个灰色边框」。:not(form) 一刀切掉这一路。*/
            html body:has(#set-wrap) #set-content-wrap:not(:has(.set-content-left)) > div > *:not(.set-tab-table):not(.c):not(script):not(form) {
                box-sizing:border-box !important;
                border-radius:12px !important;
                background-color:rgba(255,255,255,.02) !important;
                background-image:linear-gradient(to top, rgba(0,0,0,.26) 0%, rgba(255,255,255,.045) 100%) !important;
                border:1px solid rgba(255,255,255,.07) !important;
                box-shadow:inset 0 2px 5px rgba(0,0,0,.40) !important;
                padding:12px 14px !important; margin:0 0 10px !important;
                color:#dfe7f3 !important; -webkit-text-fill-color:#dfe7f3 !important;
            }
            /* 托盘里的表格不再自带外框,免得套两层 */
            html body:has(#set-wrap) #set-content-wrap:not(:has(.set-content-left)) > div > table {
                border:0 !important; background-color:transparent !important; box-shadow:none !important;
            }

/* ===== SPP-PROFILE-END ===== */
`);

// ==================== 2.55.18:写短消息页(message.php?action-write.html)白底换掉 ====================
/* 用户 2026-10-05 截图圈的是「其他」那一行的两枚白块(添加表情 / 草稿箱),并把范围收窄成
   「把白色底边换掉,让它看起来清楚一点就 OK」。整页已经是深色星空玻璃,只剩这几处还留着
   站点原皮的白底 —— 深底上的白块、白底上的近白字,都读不清。本模块只做一件事:把这些白底
   换成本站体系的暗底,字色自然就清楚了。**不动布局、不挪一个盒子、不加图标、不碰 DOM。**

   实测(受控浏览器读到的计算样式,2.55.17 原样):
     ① 两枚 A.abtn「添加表情 / 草稿箱」:站点规则(可读样式表、无 !important)
        .abtn{cursor:pointer;border:1px solid #eee;color:#000;padding:2px 8px;
        font-family:simsun;background:#fff;margin:2px;white-space:nowrap}
        而字色被本页皮肤继承成 rgb(223,231,243) ⇒ 白底 + 近白字 ≈ 看不见(实测 69×19)。
     ② 「内容」那行的编辑器图标条:16 枚 GIF **自带不透明白底**(实测 bold.gif 23×22、
        整张不透明、四角 rgb(235,235,227),所以它确实是一条米白带)—— ★★ 2.55.19 按
        **用户口径撤掉不动**:2.55.18 试过 filter:invert(1) hue-rotate(180deg)(实测确实
        翻成暗底浅笔画),用户看过截图后说「这里不用动」。本模块**不留任何 filter 规则**,
        这一排保持站点原样(连圆角也不加)。
     ③ 点「添加表情」弹出的表情面板:站点 read.open() 把 #menu_face 的 innerHTML + class
        抄进 body 下那枚万能浮层(实测 BODY > DIV.menu,已吃到 CSS_MENUPOP 的暗色玻璃),
        但浮层里那两块仍是原生白:DIV#buttons.face(url(th1.png) + rgb(224,240,249))与
        DIV#showface(站点 JS 写的行内 style="background:rgb(255,255,255)" ⇒ 只能 !important 压)。
        ★ 门牌用 :has(> #buttons.face):同一枚 BODY > DIV.menu 还兼作帖子页的「最近浏览」浮层
          (th.h + tr.tr3 表格式)与页眉那几枚 .pad 下拉 —— 三者互不相干,这里只命中表情面板。
        ★ 表情格子本身仍是站点 GIF 的白底小图(21×19,白底烤在位图里;反色只会把黄脸翻成
          蓝脸),所以只给它们圆角与悬停底色,让白格读成一格格贴纸,不再和面板糊成一整片白。
   ★ 本模块**纯 CSS**:不动 DOM、不绑事件。站点那套拖拽(.face 上的 cursor:move /
     read.move)、切页签(showTab)、关闭(closep)、插入表情(addsmile)全部照旧。 */
GM_addStyle(`
/* ===== SPP-MSGWRITE-BEGIN ===== */

/* ---- ① 两枚白块 → 暗底玻璃胶囊 ---- */
html body:has(#set-wrap) #set-content:has(#td_face) a.abtn {
    display:inline-flex !important; align-items:center !important; box-sizing:border-box !important;
    height:26px !important; padding:0 12px !important; margin:0 6px 0 0 !important;
    border:1px solid rgba(255,255,255,.14) !important; border-radius:8px !important;
    background-color:rgba(255,255,255,.07) !important; background-image:none !important;
    color:#dfe7f3 !important; -webkit-text-fill-color:#dfe7f3 !important;
    font-family:inherit !important; font-size:12.5px !important; font-weight:500 !important;
    line-height:1 !important; white-space:nowrap !important; cursor:pointer !important;
    text-decoration:none !important;
    transition:background-color .18s ease, border-color .18s ease, color .18s ease !important;
}
html body:has(#set-wrap) #set-content:has(#td_face) a.abtn:hover {
    background-color:rgba(32,201,151,.18) !important;
    border-color:rgba(32,201,151,.55) !important;
    color:#eafff6 !important; -webkit-text-fill-color:#eafff6 !important;
}

/* ---- ③ 表情面板(BODY > DIV.menu 里那两块白)---- */
body > div.menu:has(> #buttons.face) > #buttons.face {
    background-color:transparent !important; background-image:none !important;
    border-bottom:1px solid rgba(255,255,255,.09) !important;
    border-radius:8px 8px 0 0 !important;
}
body > div.menu:has(> #buttons.face) > #buttons.face div {
    border-radius:7px !important;
    color:#cfe0f5 !important; -webkit-text-fill-color:#cfe0f5 !important;
    transition:background-color .18s ease !important;
}
body > div.menu:has(> #buttons.face) > #buttons.face div:hover { background-color:rgba(255,255,255,.09) !important; }
body > div.menu:has(> #buttons.face) > #buttons.face div.lian {
    background-color:rgba(32,201,151,.22) !important; background-image:none !important;
    color:#8ef0b8 !important; -webkit-text-fill-color:#8ef0b8 !important;
}
body > div.menu:has(> #buttons.face) > #showface {
    background-color:transparent !important; background-image:none !important;
}
body > div.menu:has(> #buttons.face) > #showface img { border-radius:5px !important; }
body > div.menu:has(> #buttons.face) > #showface img:hover { background-color:rgba(32,201,151,.28) !important; }
/* 关闭那张位图(images/close.gif)是**透明底 + 深灰笔画**(实测满图只有 33 个不透明像素、
   色值 rgb(104,104,104)),在暗面板上几乎看不见 —— 只反色,不换图、不动 span.fr 的热区与
   closep 绑定。左右箭头那两枚(btLeft / btRight.gif)本来就是白色笔画,不用动。 */
body > div.menu:has(> #buttons.face) img[src*="close.gif"] { filter:invert(1) !important; }

/* ===== SPP-MSGWRITE-END ===== */
`);


})();