*,
*::before,
*::after {
  box-sizing: border-box;
}

/* 流体根字号（demo theme.css 的 html 规则原样搬过来）。blkswn 整站随视口缩放：
   1440px → 14.4px、1920px → 18px（封顶）、≤1200px → 12px（保底）。

   第二轮之前生产没设根字号（= UA 默认 16px），而第一轮把 demo 的 rem 按 1440px
   冻结成 px 写进来，于是同一份 CSS 里混着两套标定：`.primary-nav` 是 25px(×14.4)、
   `.primary-nav a` 却是 0.9rem(×16) —— 两边都对不上 demo，这是首页字号没落地的根因。
   上了流体根字号后，rem 值自己就对了，冻结的 px 才是需要换回 rem 的那批。

   作用域正好是第二轮范围：app.css 只被 layouts/app.blade.php 加载 ——
   admin(admin.css) 与 3 个自定义布局旗舰页(tokens+自身 CSS) 都不吃这条。

   pixel-grid-guide 例外：它是旗舰豁免页却走标准 tool-layout、且没有自己的 CSS，
   所以它的字号会跟着缩放，**没法钉回**（rem 按定义相对根元素，子元素改 font-size 也拦不住）。
   要真豁免只能在 <html> 上挂类分支。它的豁免范围本就只是 v3 版式(hero markup，
   HomePageTest 有钉)，皮肤层它一直跟着 app.css 走，故此处按现状处理。 */
html {
  font-size: clamp(12px, 1vw, 18px);
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}

[hidden] {
  display: none !important;
}

#tool, #package, #sizes, #faq, #how, #use-cases, #intro, #output-spec {
  scroll-margin-top: 80px;
}

body {
  margin: 0;
  min-height: 100vh;
  font-family: var(--sans);
  color: var(--text);
  /* blkswn 是纯白平底：原先两层品牌蓝/橙的 radial 淡染在此体系里没有位置 */
  background: var(--bg);
  line-height: 1.6;
  /* demo 的 body 全局字距（theme.css `body { letter-spacing: 0.02rem }`）。
     漏了这一条，全站每个继承字距的元素都比 demo 紧 —— 单看某处像"差不多"，
     整页排下来就是"字体间距不一样"。根字号两边同为 14.4px，故 rem 可 1:1 照搬。 */
  letter-spacing: 0.02rem;
}

/* UA 默认粗体的重置 —— 两个自托管字体都只到 500（Oswald 可变上限 500，
   Zen Kaku 只取了 latin 400/500 两片），而浏览器对 h1-h6/th/legend 默认 bold(700)、
   对 strong/b 默认 bolder（相对父级再加粗，套在 th 里能到 900）。
   缺字形时浏览器会**伪造**粗体：字形被算法加粗，边缘发糊。实测证实
   （font-synthesis:none 一关，700 的渲染就塌回 500 的字形）。
   demo 的字重词汇只有 300/400/500，从不用 600/700，故一律收进 500。

   整条用 :where() 包住 → 特异性为 0，这一点别去掉。作者样式本就压得过 UA 默认，
   无需靠特异性；压到 0 才能让任何类规则都盖得过它（如 .exif-table th 的 mono 600
   要保住真粗体）。

   曾经还有第二处作用域 `body:not(.image-to-ascii-page)`，用来放过自带字体命名空间的
   自定义布局旗舰页（那些字体有真 700 字形）。pixel-art(2026-07-19)、image-split 与
   image-to-ascii(2026-07-20) 陆续重设计并入本体系后，豁免对象一个不剩，:not() 已删。
   --mono 不受影响（系统等宽有真 600/700）。 */
:where(h1, h2, h3, h4, h5, h6, th, legend, strong, b) {
  font-weight: 500;
}

a {
  color: var(--accent);
  text-decoration: none;
}

/* 荧光黄选中 —— accent 在 blkswn 里就是「标记」信号 */
::selection {
  background: var(--accent-light);
  color: var(--text);
}

a:hover {
  color: var(--accent-hover);
  text-decoration: underline;
}

/* v3 版式：与 demo 的 .bs-wrap 同宽。
   demo 是 `max-width: 84rem; padding: 0 2rem`，按本文档「四、rem/clamp」的换算
   （1440 视口下根字号 = clamp(12px,1vw,18px) = 14.4px，不是 16px）：
     84rem × 14.4 = 1209.6px   2rem × 14.4 = 28.8px   →   内容宽 1152px、边距 28.8px
   此前是 1120px / 边距 16px —— 窄 32px，round 1 沿用了旧宽度没做这步换算。
   用 width 而非 max-width+padding 复刻：.wrap 常与设了 `padding` 简写的类连用
   （.tool-page-tail 等设了 padding 简写的类），横向 padding 会被它们重置成 0。 */
/* demo 的 .bs-wrap 是 `max-width:84rem; padding:0 2rem`（外框 84rem、内容 80rem）。
   这里刻意**不照搬 padding 写法**：有 18 个类跟 .wrap 连用（.wrap.home-main、.wrap.content-sections …），
   其中任何一个用 padding 简写都会把横向留白重置掉。改用等值的 width-calc，
   内容盒同为 80rem、两侧留白同为 2rem，视觉一致但不给别人的 padding 可乘之机。 */
.wrap {
  width: min(80rem, calc(100% - 4rem));
  margin: 0 auto;
}

/* ── Category color scopes ── */
.cat-convert { --cat: var(--c-convert); --cat-soft: var(--c-convert-soft); }
.cat-edit { --cat: var(--c-edit); --cat-soft: var(--c-edit-soft); }
.cat-optimize { --cat: var(--c-optimize); --cat-soft: var(--c-optimize-soft); }
.cat-creative { --cat: var(--c-creative); --cat-soft: var(--c-creative-soft); }

/* ── Header ── */
.site-promo {
  background: var(--accent-light);
  color: var(--accent-light-text);
}

.site-promo-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 0.35rem;
  font-size: max(14px, 0.95rem);
  line-height: 1.5;
}

.site-promo-copy {
  min-width: 0;
  margin: 0;
}

.site-promo-copy strong {
  margin-inline-end: 0.6em;
}

.site-promo-link,
.site-promo-link:hover {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  flex-shrink: 0;
  min-height: 44px;
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.site-promo-link:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}

/* demo .bs-header 是**实心白 + 一条发丝线**，没有磨砂。半透明+blur 是 cream/blue 时代的语汇，
   与单色发丝线体系冲突（模糊会把 1px 线糊掉），按「旧设计可以不要」去掉。 */
.site-header-bar {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}

/* demo .bs-header-inner：`height:4rem; gap:2rem`。
   这里用 min-height 而非 height —— 移动端菜单（阶段 B）落地前，窄视口仍靠 flex-wrap 兜底，
   写死 height 会让换行的内容溢出。阶段 B 接管窄视口后这两行会收敛成 demo 原样。 */
.site-header {
  min-height: 4rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;
  flex-wrap: wrap;
}

.brand {
  display: flex;
  align-items: center;
  gap: 14px;
}

.brand-mark {
  width: 40px;
  height: 40px;
  border-radius: 0;
  background: var(--accent);
  display: grid;
  place-items: center;
  font-family: var(--display);
  font-weight: 500;
  color: var(--accent-text);
  font-size: 14px;
  box-shadow: var(--shadow);
}

.brand-logo {
  width: 40px;
  height: 40px;
  flex-shrink: 0;
  object-fit: contain;
  border-radius: 0;
  display: block;
}

/* 站点 logo 是**纯黑单色**图标、底透明（/storage/site/8LgmbJ….png），暗底上几乎看不见。
   invert 把黑描边翻成白，透明像素不受影响 —— 单色标这么处理是等价的，不用另做一张暗色图。
   ⚠ 后台换成彩色 logo 的话这条要撤掉，彩色反相会变鬼片。 */
:root[data-theme='dark'] .brand-logo {
  filter: invert(1);
}

/* 没有 data-theme 就是没点过开关（或没 JS）—— 跟随系统。 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .brand-logo {
    filter: invert(1);
  }
}

/* v3 版式：品牌名与小字**同一行并排**（demo 的 .bs-brand 里 <small> 是 inline +
   margin-left:0.6em），不是上下堆叠。图片 logo 按要求保留在最左。
   字号按「四、rem/clamp」换算：demo 1.35rem × 14.4 ≈ 19px、0.75rem × 14.4 ≈ 11px
   —— 不用 rem，生产的 rem 是按 16px 标定的。 */
.brand-lockup-text {
  display: flex;
  align-items: baseline;
  /* demo 是 `.bs-brand small { margin-left: 0.6em }`，em 相对 small 自己的 0.75rem
     → 6.48px。这里的 gap 若写 0.6em，em 相对本容器（继承 body 14.4px）= 8.64px。
     直接写死等价值。 */
  gap: 0.45rem;
}

/* demo .bs-brand 原值（此前是按 1440px 冻结的 19px）。 */
.brand-name {
  display: block;
  margin: 0;
  font-family: var(--display);
  text-transform: uppercase;
  letter-spacing: 0.04rem;
  font-size: 1.35rem;
  font-weight: 500;
  color: var(--text);
}

/* demo .bs-brand small 原值（此前冻结成 11px，且漏了字距）。 */
/* demo 的 `.bs-brand small` 自己只写了 weight/color/size/letter-spacing —— 字体与大写是
   从父级 .bs-brand 继承来的。生产的副标在 .brand-lockup-text 里，继承不到 .brand-name
   的 Oswald+uppercase，于是落回 body 的 Zen Kaku 原样大小写。显式补上。 */
.brand-tagline {
  margin: 0;
  font-family: var(--display);
  text-transform: uppercase;
  font-size: 0.75rem;
  font-weight: 300;
  letter-spacing: 0.08rem;
  color: var(--muted);
  white-space: nowrap;
}

/* ── 长文案语种的页头预算（2026-08-01）─────────────────────────────
   现象：切到 pt-BR，页头挤成两行。

   ⚠ **这是比例问题，不是视口问题，所以断点解决不了。** 根字号是
   `clamp(12px, 1vw, 18px)`、`.wrap` 是 `80rem` —— 字号与容器宽**同步**随视口缩放，
   于是「文案宽 / 可用宽」这个比值在任何视口下都恒定：pt-BR 在 1280 撑爆，
   在 1920 一样撑爆。加 @media 只会让人误以为修好了。

   页头文案合计字符数（All tools + 两个板块名 + 收藏 + Get started）：
   en 49 · de 45 · id 41 · ms 51 · vi 52 · fr 55 · th 68 · es 76 · **pt-BR 78**。
   超过 en 约 35% 就放不下，故按语种收紧 —— 命中 `<html lang>`，
   其余语种渲染一个像素都不变（`FluidTypeScaleTest` 钉着那几个 demo 原值）。

   ⚠ **新增语种要回来看这里**：进 `supported` 目录后先数一遍页头字符数，
   超过 ~65 就把它加进下面两个选择器。 */
html[lang='pt-BR'] .brand-tagline,
html[lang='es'] .brand-tagline,
html[lang='th'] .brand-tagline {
  display: none;
}

/* 副标腾出的约 110px 通常就够了；再收三层 gap 是留给「板块变多」的余量。
   ≤720px 不参与：那时导航已进抽屉，跟页头横向预算无关。 */
@media (min-width: 721px) {
  html[lang='pt-BR'] .site-header,
  html[lang='es'] .site-header,
  html[lang='th'] .site-header {
    gap: 1.25rem;
  }

  html[lang='pt-BR'] .header-actions,
  html[lang='es'] .header-actions,
  html[lang='th'] .header-actions,
  html[lang='pt-BR'] .header-utilities,
  html[lang='es'] .header-utilities,
  html[lang='th'] .header-utilities,
  html[lang='pt-BR'] .primary-nav,
  html[lang='es'] .primary-nav,
  html[lang='th'] .primary-nav {
    gap: 1.1rem;
  }
}

/* v3 版式：导航是纯文字 + 下划线，不是灰底方块。
   黑字、宽间距、hover/当前项加下划线 —— 对齐 demo 的 .bs-nav。 */
.primary-nav {
  display: flex;
  align-items: center;
  gap: 1.75rem; /* demo .bs-nav 原值 */
  flex-wrap: wrap;
  min-width: 0;
}

.primary-nav a {
  font-size: 0.9rem;
  color: var(--text);
  text-decoration: none;
  transition: none;
  /* 单个条目**绝不能自己折行**：pt-BR 的「Todas as ferramentas」一断，
     页头就从「导航挤成两行」变成「整条页头高度翻倍」。 */
  white-space: nowrap;
}

/* 下划线跟随当前页：命中 aria-current="page" 的链接（由 site-header 按路径打标），
   而不是无条件钉在 .nav-home 上 —— 否则切到 About 下划线仍停在 All tools。 */
.primary-nav a:hover,
.primary-nav a[aria-current='page'] {
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-thickness: 1px;
}

/* ── 首页 hero（批次6-P5：采用 demo .bs-hero 结构）──
   满幅外壳 + 内层 .wrap 定宽，故 border-bottom 天然通栏（0..1439），
   不再需要把那条线转嫁给 .keywords 的 border-top —— 旧写法是因为 .home-hero
   自带 .wrap、只有 1152 宽，线会卡在内容宽里。 */
/* 2026-07-30：上留白 5rem → 3rem。首屏文字整体上移，把省下的高度让给上传面板
   —— 改前面板顶边在 375px（1440 视口，根字号 14.4px），近半个首屏花在文字与留白上。 */
.bs-hero {
  padding: 3rem 0 3.5rem;
  border-bottom: 1px solid var(--bs-border);
}

/* 照 demo，这条也作用到 .bs-hero-kicker（它也是 <p>）：kicker 的字号与下留白因此
   跟着这里走，而不是 .bs-hero-kicker 里写的 clamp/1.5rem —— 那正是 demo 的实际渲染
   （`.bs-hero p`(0,1,1) 压过 `.bs-hero-kicker`(0,1,0)）。旧的 .home-hero .hero-kicker
   曾把这两个值手抄一遍，现在由结构本身提供。 */
.bs-hero p {
  max-width: 38rem;
  margin: 0 0 2.25rem;
  font-size: 1rem;
  color: var(--bs-text-sub);
}

.bs-hero h1 {
  margin: 0 0 1.25rem;
  font-family: var(--bs-font-condensed);
  font-weight: 500;
  /* 2026-07-26：首屏主角让给上传面板，标题整体降一档（原 clamp(2.6rem, 7vw, 6rem)）。
     2026-07-30 再降一档（原 clamp(2rem, 4.2vw, 3.5rem)，1440 下 50.4px → 39.6px）：
     标题是写死两行的（title_html 带 <br>），字号每降 1px 首屏就省 2.16px。
     FluidTypeScaleTest 钉着这个值，改这里要同步改那条断言。 */
  font-size: clamp(1.75rem, 3.2vw, 2.75rem);
  line-height: 1.08;
  letter-spacing: 0;
  text-transform: uppercase;
  color: var(--bs-text);
}

/* 首屏水平居中（2026-07-26）。刻意只点名 hero wrap 的**直接**子块，不写
   `.bs-hero { text-align: center }` —— 那会继承进面板内部，把结果态的文件名、
   体积读数、控件标签一起推到中间；面板内部该居中的视图自己已经居中了
   （.home-panel-view--center / .home-panel-drop）。 */
.bs-hero > .wrap > h1,
.bs-hero > .wrap > p {
  margin-left: auto;
  margin-right: auto;
  text-align: center;
}

/* 2026-07-30 首屏上移：kicker 与描述的下留白收紧（原都是 `.bs-hero p` 给的 2.25rem）。
   刻意走**直接子块**选择器，不去动 `.bs-hero p` 本身 —— 后者 (0,1,1) 会漏进面板内部，
   §十二「附带修掉的特异度坑」记的就是它把面板里那几个 <p> 压出行距 bug 的经过。
   kicker 比描述再紧一档：它只是一行小字帽，跟 h1 贴近才成组。 */
.bs-hero > .wrap > p {
  margin-bottom: 1.5rem;
}

.bs-hero > .wrap > .bs-hero-kicker {
  margin-bottom: 1rem;
}

/* 强调词（no upload, no sign-up）＝ demo 的 .bs-mark：荧光黄记号笔高亮。
   2026-07-18 还原 v3 配色后黄底回归，`padding: 0 0.3em` 也一并跟上
   （P5a 曾因「黄底已删」把它登记为刻意不跟，那个前提没了）。 */
.bs-hero p em {
  font-style: normal;
  background: var(--accent-light);
  /* 黄底恒定，字色也必须恒定 —— 不写的话它继承 .bs-hero p 的 --muted，暗色下 2.23:1。 */
  color: var(--accent-light-text);
  padding: 0 0.3em;
}

/* ── 工具页 hero（批次6-P1：采用 demo .bs-page-hero 结构，吃 --bs-* → 生产 token）──
   .bs-page-hero 满幅、自带通栏底线（不再需要 ::after 100vw）；内层复用 .wrap 定宽；
   依次是面包屑 + kicker + h1 + 描述 + 标签。旧 .tool-hero* 平行类整块删除。 */
.bs-page-hero {
  padding: 3.5rem 0 2.5rem;
  border-bottom: 1px solid var(--bs-border);
}

.bs-page-hero h1 {
  font-family: var(--bs-font-condensed);
  font-weight: 500;
  font-size: clamp(2rem, 5vw, 3.5rem);
  line-height: 1.1;
  letter-spacing: 0;
  text-transform: uppercase;
  margin: 0 0 1rem;
  color: var(--bs-text);
}

/* 描述段。照 demo 也作用到 .bs-hero-kicker（kicker 也是 <p>）：其 margin:0 会压掉
   .bs-hero-kicker 的下边距 —— 这正是 demo 的实际渲染（kicker→h1 紧贴），故照搬。 */
.bs-page-hero p {
  max-width: 40rem;
  margin: 0;
  color: var(--bs-text-sub);
}

.bs-hero-kicker {
  font-size: clamp(10px, 0.7vw, 14px);
  letter-spacing: 0.04rem;
  color: var(--bs-text-sub);
  text-transform: uppercase;
  margin: 0 0 1.5rem;
}

/* 2026-07-30：标签与 hero 操作区（收藏星标）同一行，星标靠右。
   上留白从 .bs-hero-tags 提到这一行 —— 没有标签的工具页只剩星标时，
   留白仍然由这条给，不会贴着描述。 */
.bs-hero-tagrow {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-top: 1rem;
}

.bs-hero-actions {
  flex: none;
  margin-left: auto;
}

.bs-hero-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.bs-hero-tags span {
  font-size: clamp(9px, 0.65vw, 12px);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 0.15rem 0.6rem;
  border: 1px solid var(--bs-border);
  color: var(--bs-text-sub);
}

/* ── 工具页正文区块（批次6-P2：采用 demo .bs-section 结构）──
   区块间距用 demo 的机制：每区自带 padding-bottom，容器是普通块级流
   （demo 的 <main class="bs-wrap bs-main"> 无 flex 无 gap）。demo 每个 section 写
   style="padding-top:0"，这里用选择器表达。容器的 display 见下方 .content-sections。 */
.bs-section {
  padding: 3.5rem 0;
}

.bs-section[id] {
  scroll-margin-top: 5rem;
}

/* demo 每个 section 都写 style="padding-top:0"，区块间距全靠 padding-bottom。
   正文区与 tail 两处容器同理。 */
.content-sections > .bs-section,
.tool-page-tail > .bs-section {
  padding-top: 0;
}

.bs-section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 2rem;
}

.bs-section-head h2 {
  font-family: var(--bs-font-condensed);
  font-weight: 500;
  font-size: 1.5rem;
  letter-spacing: 0.04rem;
  text-transform: uppercase;
  margin: 0;
  color: var(--bs-text);
}

/* 负上边距与 .bs-section-head 的 margin-bottom:2rem 耦合，净得 0.75rem。批次5 时生产
   没有 head 包裹、照抄这个负值会让正文压到 h2 上；P2 补上包裹后这组耦合才成立。 */
.bs-cat-desc {
  margin: -1.25rem 0 1.5rem;
  max-width: 42rem;
  font-size: 0.9rem;
  color: var(--bs-text-sub);
}

/* how-to 步骤 / 场景卡（demo 两者同一个 .bs-steps，后者只是不给 .bs-step-num）。
   发丝线**由每格自己描边**，不用 demo 的「容器涂底色＋子格盖白」——
   同 .bs-grid 处的长注释：末行填不满时，空格子没有子元素去盖，容器底色直接漏成灰块。
   demo 能那么写是因为它每个网格的格数写死且填满；生产的格数来自文案数据，
   **且窄视口下会换行**——实测 640px 起 scenarios(4格) 末行 1/3、480px 起 how(3格) 末行 1/2，
   照搬 demo 会在右侧漏出整片灰块（就是 9070ebc 修掉的首页那个 840px 灰块，同一个机制）。
   注：0 偏移 0 模糊的 box-shadow 是描边不是投影，不违反「blkswn 无投影」。 */
.bs-steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: 1px;
}

.bs-steps > div {
  background: var(--bs-surface);
  padding: 1.5rem;
  box-shadow: 0 0 0 1px var(--bs-border);
}

/* 荧光黄强调格：原 .content-card.highlight 的等价物（image-to-icon 的特性卡由
   文案数据 feature.highlight 驱动）。黄本就是 v3 的强调色，直接沿用。 */
.bs-steps > .is-highlight {
  background: var(--accent-light);
}

.bs-steps h3 {
  font-family: var(--bs-font-condensed);
  font-size: 1rem;
  text-transform: uppercase;
  margin: 0 0 0.5rem;
}

.bs-steps p {
  margin: 0;
  font-size: 0.85rem;
  color: var(--bs-text-sub);
}

.bs-step-num {
  font-family: var(--bs-font-condensed);
  font-weight: 300;
  font-size: 2rem;
  color: var(--bs-text-sub);
  margin-bottom: 0.5rem;
}

/* demo 的 .bs-table-wrap 在面板里用 margin:0 1.5rem 1.5rem；正文区里 demo 页面
   自己覆写成 margin:0 0 1.5rem，这里取正文那个值。 */
.bs-table-wrap {
  overflow-x: auto;
  border: 1px solid var(--bs-border);
  margin: 0 0 1.5rem;
}

.bs-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.85rem;
}

.bs-table th,
.bs-table td {
  padding: 0.6rem 1rem;
  text-align: left;
  border-bottom: 1px solid var(--bs-border);
}

.bs-table tr:last-child td {
  border-bottom: 0;
}

.bs-table th {
  font-size: clamp(10px, 0.7vw, 12px);
  text-transform: uppercase;
  letter-spacing: 0.04rem;
  color: var(--bs-text-sub);
  font-weight: 500;
  white-space: nowrap;
}

/* demo 的 .bs-table 只给 th 上灰字，td 继承正文黑 —— P2 当时多写了一条 td 灰，
   被对照器的全局配色跳过挡住了；2026-07-18 撤掉跳过后当场暴露，删。 */

/* ── Tool shell ── */
.tool-shell {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  position: relative;
}

/* 这里曾有一条 .tool-shell[class*='cat-']::before 的 3px 分类色渐变条，和一条
   .tool-shell--v3 把它关掉的覆盖 —— v3 版式里面板只有一条发丝线边框，没有渐变条
   （demo 无对应物，分类信息面包屑的 chip 已经给了）。2026-07-20 最后一个豁免页
   pixel-grid-guide 并入 v3 后，--v3 修饰类成了全体，两条一并删除。 */

/* ── 工具面板抬头（标题 + ☞ 隐私注）──
   此前只有三个独立页有这块，且各写各的：image-split/image-to-ascii 的 .bs-panel-head
   定义在各自的 *-theme.css 里（app.css 够不着），pixel-art 另起 .pa-head/.pa-lbl。
   这里把几何值原样搬进 app.css，好让 x-tool-layout 的 43 个页面也有。
   .tool-shell 无内边距，故这条 border-bottom 天然通栏；外层 overflow:hidden 负责切圆角。 */
.bs-panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1rem 1.5rem;
  border-bottom: 1px solid var(--border);
}

.bs-panel-head h2 {
  font-family: var(--display);
  font-weight: 500;
  font-size: 1.1rem;
  letter-spacing: 0.04rem;
  text-transform: uppercase;
  margin: 0;
}

/* 独立页此前拿 .bs-hero-kicker 当这条注，再内联 style="margin:0" 压掉它的下边距；
   这里给它自己的类，免掉那个内联覆盖。 */
.bs-panel-note {
  font-size: clamp(10px, 0.7vw, 13px);
  letter-spacing: 0.04rem;
  text-transform: uppercase;
  color: var(--muted);
  text-align: right;
}

/* 窄屏放不下并排，改上下堆叠，注文回到左对齐。 */
@media (max-width: 720px) {
  .bs-panel-head {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.35rem;
  }

  .bs-panel-note {
    text-align: left;
  }
}

.tool-shell[class*='cat-'] .upload-drop:hover {
  border-color: var(--cat, var(--accent));
  background: var(--cat-soft, var(--accent-light));
}

/* 2026-07-18 删掉两条 cat-* 的 .btn-primary 覆盖（cream/blue 时代残留）。
   分类色统一收进单色后 `--cat` 就是纯黑，这两条只做了两件事：
   ① 常态背景重复设成黑（.btn-primary 本来就是）；
   ② **hover 背景被钉死成黑，而 .btn-primary:hover 又把字色设成 --text 黑
      → 黑底黑字、对比度 1.00，鼠标一悬上传按钮的字就没了**（全部工具页中招）。
   删掉后按钮回到干净的黑白反转：黑底白字 ⇄ hover 白底黑字。 */

[hidden] {
  display: none !important;
}

/* ── Upload stage (initial view only) ── */
.upload-stage {
  padding: 28px;
}

.upload-drop {
  position: relative;
  cursor: pointer;
  border-radius: var(--radius);
  /* demo 的 .bs-dropzone 是发丝线虚框（1px），不是 2px 粗框 */
  border: 1px dashed var(--border);
  background: var(--surface-2);
  transition: border-color 0.2s, background 0.2s;
}

.upload-drop:hover {
  border-color: var(--accent);
  background: var(--accent-light);
  box-shadow: inset 0 0 0 1px var(--border);
}

.upload-drop.is-dragging {
  border-color: var(--accent);
  border-style: solid;
  background: var(--accent-light);
}

.upload-drop input[type='file'] {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.upload-drop-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  min-height: 300px;
  padding: 40px 28px;
  text-align: center;
}

.upload-visual {
  width: 88px;
  height: 88px;
  border-radius: var(--radius-pill);
  background: var(--surface);
  border: 1px solid var(--border);
  display: grid;
  place-items: center;
  color: var(--accent);
  box-shadow: var(--shadow);
  margin-bottom: 4px;
}

.upload-icon {
  width: 56px;
  height: 56px;
}

.upload-arrow {
  opacity: 0.7;
}

.upload-title {
  margin: 0;
  /* 依 demo 的 .bs-dropzone strong：走正文体 */
  font-family: var(--sans);
  font-size: clamp(1.25rem, 3vw, 1.55rem);
  font-weight: 500;
  color: var(--text);
}

.upload-sub {
  margin: 0;
  color: var(--muted);
  font-size: 0.92rem;
}

.upload-formats {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: center;
}

.upload-formats span {
  font-family: var(--mono);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--muted);
}

.upload-btn {
  margin-top: 4px;
  min-width: 148px;
}

/* ── Editor workspace (after upload) ── */
.tool-workspace {
  animation: workspace-in 0.35s ease;
}

@keyframes workspace-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* 编辑区左右分栏：缝隙即分隔线（demo 的 .bs-panel-body） */
.editor-panel {
  display: grid;
  grid-template-columns: 1fr 270px;
  gap: 1px;
  background: var(--border);
}

.editor-panel > * {
  background: var(--surface);
}

/* 分栏线由 .editor-panel 的 `gap:1px` 提供，这里再加 border-right 就是第二条
   （实测交界处 x=1007/1008 两列都是 #cccccc，2px 粗）。发丝线只能有一条。
   移动端塌成单栏时 border-right 还会变成右边缘的竖线，本就不该由格子自己画。 */
.editor-main {
  padding: 22px;
  background: var(--surface);
}

.file-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 18px;
  padding: 11px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 0.85rem;
}

.file-bar .name {
  font-weight: 500;
  word-break: break-all;
}

.file-bar .meta {
  color: var(--muted);
  font-family: var(--mono);
  font-size: 0.72rem;
  margin-top: 2px;
}

.file-actions {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
}

.preview-heading {
  margin: 0 0 14px;
  font-size: 0.72rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
}

/* ── Device previews (realistic classic palettes) ── */
.device-previews {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 16px;
  margin-bottom: 22px;
}

.device-mock {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px 12px 10px;
  background: var(--surface-2);
}

.device-mock h4 {
  margin: 0 0 10px;
  font-size: 0.7rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--muted);
}

.mock-caption {
  margin: 8px 0 0;
  font-size: 0.68rem;
  color: var(--muted);
  font-family: var(--mono);
}

.device-previews.is-empty canvas {
  opacity: 0.2;
}

/* Chrome — classic light theme */
.chrome-window {
  border-radius: 10px;
  overflow: hidden;
  border: 1px solid #c4c7c5;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}

.chrome-tabstrip {
  display: flex;
  align-items: flex-end;
  gap: 0;
  background: #dee1e6;
  padding: 8px 8px 0;
  min-height: 38px;
}

.chrome-tab {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 7px 12px;
  font-size: 0.7rem;
  max-width: 150px;
  border-radius: 8px 8px 0 0;
  color: #3c4043;
}

.chrome-tab.inactive {
  background: transparent;
  color: #5f6368;
  max-width: 100px;
}

.chrome-tab.active {
  background: #ffffff;
  box-shadow: 0 -1px 0 #fff;
  position: relative;
  z-index: 1;
}

.chrome-tab.active canvas {
  width: 16px;
  height: 16px;
  image-rendering: pixelated;
  flex-shrink: 0;
  border-radius: 2px;
}

.tab-favicon {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  flex-shrink: 0;
}

.tab-favicon-muted {
  background: #9aa0a6;
}

.tab-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 500;
}

.tab-title-muted {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tab-close {
  color: #5f6368;
  margin-left: 4px;
  font-size: 0.9rem;
  line-height: 1;
}

.chrome-toolbar {
  background: #f1f3f4;
  padding: 8px 10px;
  border-bottom: 1px solid #dadce0;
}

.chrome-omnibox {
  display: flex;
  align-items: center;
  gap: 8px;
  background: #ffffff;
  border: 1px solid #dadce0;
  border-radius: 20px;
  padding: 7px 14px;
  font-size: 0.72rem;
  color: #202124;
  font-family: var(--mono);
}

.omnibox-lock {
  flex-shrink: 0;
}

.chrome-page {
  height: 28px;
  background: #ffffff;
}

/* iOS — classic blue-purple wallpaper */
.iphone-shell {
  position: relative;
  border-radius: 16px;
  overflow: hidden;
  border: 2px solid #1a1a1a;
  min-height: 168px;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15);
}

.ios-status {
  position: relative;
  z-index: 2;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 6px 14px 4px;
  font-size: 0.62rem;
  font-weight: 500;
  color: #fff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
}

.ios-status-icons {
  font-size: 0.55rem;
  letter-spacing: 0.05em;
}

.ios-wallpaper {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 80% 60% at 20% 10%, rgba(120, 180, 255, 0.9), transparent 55%),
    radial-gradient(ellipse 70% 50% at 80% 20%, rgba(200, 120, 220, 0.85), transparent 50%),
    radial-gradient(ellipse 90% 70% at 50% 100%, rgba(40, 60, 120, 0.95), transparent 60%),
    linear-gradient(165deg, #5b9fd4 0%, #7b6cb0 35%, #c77dba 60%, #2d3a6e 100%);
}

.ios-grid {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 10px 8px;
  padding: 8px 14px 52px;
}

.ios-slot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}

.ios-slot span,
.ios-slot canvas {
  width: 44px;
  height: 44px;
  border-radius: 22%;
  display: block;
}

.ios-slot.ghost span {
  background: linear-gradient(145deg, #4a90d9, #2d6cb5);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
}

.ios-slot.ghost:nth-child(2) span {
  background: linear-gradient(145deg, #34c759, #248a3d);
}

.ios-slot.ghost:nth-child(3) span {
  background: linear-gradient(145deg, #ff9500, #c93400);
}

.ios-slot.ours canvas {
  width: 44px;
  height: 44px;
  border-radius: 22%;
  image-rendering: auto;
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.35);
}

.ios-slot i {
  font-size: 0.52rem;
  font-style: normal;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
  max-width: 52px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: center;
}

.ios-dock {
  position: absolute;
  bottom: 8px;
  left: 12px;
  right: 12px;
  z-index: 2;
  display: flex;
  justify-content: space-around;
  padding: 6px 10px;
  background: rgba(255, 255, 255, 0.22);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-radius: 18px;
  border: 1px solid rgba(255, 255, 255, 0.25);
}

.ios-dock span {
  width: 32px;
  height: 32px;
  border-radius: 22%;
  background: linear-gradient(145deg, rgba(255, 255, 255, 0.45), rgba(255, 255, 255, 0.15));
}

/* Android — Material light + bottom sheet */
.android-shell {
  position: relative;
  border-radius: 12px;
  overflow: hidden;
  border: 1px solid #dadce0;
  min-height: 168px;
  background: #e8eaed;
}

.android-chrome-bar {
  background: #ffffff;
  padding: 8px 12px;
  border-bottom: 1px solid #e0e0e0;
  font-size: 0.68rem;
  color: #5f6368;
  font-family: var(--mono);
  text-align: center;
}

.android-page {
  height: 36px;
  background: linear-gradient(180deg, #f8f9fa, #e8eaed);
}

.android-sheet {
  background: #ffffff;
  border-radius: 14px 14px 0 0;
  padding: 10px 14px 12px;
  box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15);
}

.sheet-handle {
  width: 32px;
  height: 4px;
  background: #dadce0;
  border-radius: 2px;
  margin: 0 auto 10px;
}

.pwa-banner {
  display: flex;
  align-items: center;
  gap: 12px;
}

.pwa-icon-canvas {
  width: 44px;
  height: 44px;
  border-radius: 12px;
  flex-shrink: 0;
  image-rendering: auto;
  border: 1px solid #e8eaed;
}

.pwa-copy {
  min-width: 0;
}

.pwa-copy strong {
  display: block;
  font-size: 0.82rem;
  color: #202124;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pwa-copy > span {
  font-size: 0.68rem;
  color: #5f6368;
}

.sheet-desc {
  margin: 10px 0 0;
  font-size: 0.75rem;
  color: #3c4043;
}

.pwa-actions {
  display: flex;
  justify-content: flex-end;
  gap: 16px;
  margin-top: 10px;
  font-size: 0.75rem;
  font-weight: 500;
}

.pwa-cancel {
  color: #1a73e8;
}

.pwa-add {
  color: #1a73e8;
}

/* Windows 11 — dark Start menu acrylic */
.win-shell {
  background: #2b2b2b;
  border-radius: 10px;
  padding: 12px;
  border: 1px solid #3d3d3d;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}

.win-search {
  display: flex;
  align-items: center;
  gap: 8px;
  background: #3a3a3a;
  border-radius: 6px;
  padding: 8px 12px;
  font-size: 0.68rem;
  color: #9a9a9a;
  margin-bottom: 10px;
}

.win-section-label {
  margin: 0 0 8px;
  font-size: 0.62rem;
  font-weight: 500;
  color: #9a9a9a;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.win-tiles {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 8px;
}

.win-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
}

.win-tile span,
.win-tile canvas {
  width: 36px;
  height: 36px;
  border-radius: var(--radius);
  display: block;
}

.win-tile.ghost span {
  background: linear-gradient(145deg, #0078d4, #005a9e);
}

.win-tile.ghost:nth-child(2) span {
  background: linear-gradient(145deg, #107c10, #0b5a0b);
}

.win-tile.ghost:nth-child(4) span {
  background: linear-gradient(145deg, #8764b8, #5c2d91);
}

.win-tile.ours canvas {
  width: 36px;
  height: 36px;
  border-radius: var(--radius);
  image-rendering: pixelated;
  background: rgba(255, 255, 255, 0.08);
}

.win-tile i {
  font-size: 0.55rem;
  font-style: normal;
  color: #e0e0e0;
  max-width: 48px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: center;
}

/* Bottom preview row */
.preview-bottom {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 18px;
  align-items: start;
  padding-top: 18px;
  border-top: 1px solid var(--border);
}

.main-preview {
  width: 110px;
  /* 透明区棋盘格：功能性图案，随主题改为中性灰（原为 cream #e4d9cc / #f5f0e8） */
  background:
    linear-gradient(45deg, var(--border) 25%, transparent 25%),
    linear-gradient(-45deg, var(--border) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, var(--border) 75%),
    linear-gradient(-45deg, transparent 75%, var(--border) 75%);
  background-size: 10px 10px;
  background-position: 0 0, 0 5px, 5px -5px, -5px 0;
  background-color: var(--surface-2);
  border-radius: var(--radius);
  border: 1px solid var(--border);
  padding: 10px;
  text-align: center;
}

.main-preview canvas {
  width: 100%;
  height: auto;
  image-rendering: pixelated;
  border-radius: var(--radius);
}

.main-preview-label {
  display: block;
  margin-top: 6px;
  font-family: var(--mono);
  font-size: 0.62rem;
  color: var(--muted);
}

.preview-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.preview-cell {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 9px;
  background: var(--surface-2);
  border-radius: var(--radius);
  border: 1px solid var(--border);
}

.preview-cell canvas {
  width: 26px;
  height: 26px;
  image-rendering: pixelated;
  border-radius: var(--radius);
  flex-shrink: 0;
}

.preview-cell span {
  font-family: var(--mono);
  font-size: 0.65rem;
  color: var(--muted);
}

/* Sidebar */
/* v3 版式：右栏是白的，分栏靠 .editor-panel 的 `gap:1px` 透出发丝线（demo 的
   .bs-panel-body 同一手法）。此前这里用 --surface-2 覆盖掉了父级 `.editor-panel > *`
   给的白底 —— 两条特指度相同，靠排在后面取胜，灰底正是这么来的。 */
.editor-sidebar {
  padding: 22px 18px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.control-group {
  border: 0;
  margin: 0;
  padding: 0;
}

.control-group legend {
  font-size: 0.72rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--label);
  margin-bottom: 8px;
  padding: 0;
}

.control-group .field-hint {
  margin: -2px 0 8px;
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--label);
}

.form-field {
  margin: 0;
  padding: 0;
  border: 0;
}

.form-field + .form-field,
.form-row + .form-field,
.form-field + .form-row {
  margin-top: 0;
}

.segmented {
  display: flex;
  gap: 3px;
  background: var(--surface);
  padding: 3px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border);
}

.segmented label {
  flex: 1;
  cursor: pointer;
}

.segmented input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.segmented span {
  display: block;
  text-align: center;
  padding: 6px 4px;
  border-radius: var(--radius-pill);
  font-size: 0.78rem;
  font-weight: 500;
  color: var(--muted);
  transition: background 0.12s, color 0.12s;
}

/* 选中态走荧光黄（demo 的 .bs-choice.is-selected）—— accent 在 blkswn 里就是选中信号，
   与阶段 1 已改的 .preset-row/.position-grid .btn.is-active 保持一致 */
.segmented input:checked + span {
  background: var(--accent-light);
  color: var(--text);
}

.range-label {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 0.84rem;
  margin-bottom: 6px;
}

.range-label span:last-child {
  font-family: var(--mono);
  color: var(--accent);
  font-size: 0.78rem;
}

input[type='range'] {
  width: 100%;
  accent-color: var(--accent);
}

.color-control {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 8px;
  font-size: 0.84rem;
}

.color-control input[type='color'] {
  width: 40px;
  height: 28px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  cursor: pointer;
}

/* Buttons */
/* 胶囊按钮（对应 demo 的 .bs-btn）。
   高度 44px = demo 的 3rem × 14.4（走查视口 1440 下 demo 根字号为 14.4px），
   正好压中 44px 触控下限。
   transition: none 是 blkswn 原样 —— 黑白瞬时反转，刻意不做渐变，勿加回来。 */
.btn {
  appearance: none;
  font: inherit;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 44px;
  border-radius: var(--radius-pill);
  border: 1px solid transparent;
  padding: 0 28px;
  font-weight: 500;
  font-size: 0.88rem;
  transition: none;
}

.btn:active {
  transform: scale(0.98);
}

/* 主按钮：黑底白字 → hover 反转为白底黑字（demo 的 .bs-btn / .bs-btn:hover） */
.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--surface);
}

.btn-primary:hover {
  background: var(--surface);
  color: var(--text);
}

/* 次按钮：白底黑字 → hover 反转为黑底白字（demo 的 .bs-btn--secondary） */
.btn-secondary {
  background: var(--surface);
  color: var(--text);
  border-color: var(--border-strong);
}

.btn-secondary:hover {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--surface);
}

/* ghost 是小号次要按钮（Replace/Remove 之类）：保持矮身，但同样是胶囊 */
.btn-ghost {
  background: var(--surface);
  color: var(--muted);
  border-color: var(--border);
  height: 32px;
  padding: 0 14px;
  font-size: 0.78rem;
}

.btn-ghost:hover {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--surface);
}

.btn-danger {
  color: var(--danger);
  border-color: transparent;
  background: transparent;
}

.download-actions {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: auto;
}

.snippet-block {
  padding: 22px;
  border-top: 1px solid var(--border);
  background: var(--surface-2);
}

.snippet-block h3 {
  margin: 0 0 8px;
  font-family: var(--display);
  text-transform: uppercase;
  letter-spacing: 0.04rem;
  font-size: 1rem;
}

.snippet-block p {
  margin: 0 0 12px;
  color: var(--muted);
  font-size: 0.85rem;
}

.snippet-row {
  display: flex;
  gap: 8px;
}

.snippet-row textarea {
  flex: 1;
  min-height: 88px;
  resize: vertical;
  font-family: var(--mono);
  font-size: 0.74rem;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
}

/* Content sections。
   曾是 flex column + gap:3.5rem，再由上面一条 `.content-sections:has(> .bs-section)`
   靠 :has() 的特异性压成块级流 —— 那个 :has() 是为了让当时还用旧 .content-block 的
   两个豁免页继续吃 flex gap。两页 2026-07-20 并入 v3 后它没有消费者了，直接在源头
   写成块级流：区块间距由每个 .bs-section 自己的 padding 提供（demo 的机制）。
   注意别改回裸 `.content-sections{display:block}` 放在上面 —— 同特异性、位置更靠前，
   会被这里的声明反压回去。 */
.content-sections {
  padding: 52px 0 36px;
  display: block;
}

/* demo 的 .bs-hero-kicker（hero 与正文区共用）：正文 sans、常规字重、灰字。
   用户 2026-07-18 改口：正文 kicker 要和 v3 一样，不再保留 mono。 */

.split-layout {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 28px;
  align-items: start;
}

.package-board {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow);
}

.board-header {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 10px 14px;
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--muted);
}

.board-header span {
  width: 8px;
  height: 8px;
  border-radius: 50%;
}

/* macOS 窗口红黄绿灯:与设备预览区(471-909)同理,是 OS 仿真,硬编码即设计正确 —— 勿收进单色 */
.board-header span:nth-child(1) { background: #ff5f57; }
.board-header span:nth-child(2) { background: #febc2e; }
.board-header span:nth-child(3) { background: #28c840; }

.file-list {
  list-style: none;
  margin: 0;
  padding: 6px 0;
}

.file-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 14px;
  font-size: 0.82rem;
  border-bottom: 1px solid var(--border);
}

.file-list li:last-child {
  border-bottom: 0;
}

.fname {
  font-family: var(--mono);
  font-size: 0.74rem;
}

.fbadge {
  font-family: var(--mono);
  font-size: 0.6rem;
  font-weight: 700;
  padding: 2px 7px;
  border-radius: var(--radius-pill);
  flex-shrink: 0;
}

.fbadge.src,
.fbadge.ico,
.fbadge.png,
.fbadge.json,
.fbadge.txt { background: var(--surface-2); color: var(--text); }

.table-wrap {
  overflow-x: auto;
  margin: 0 0 1.5rem; /* demo mt 0 / mb 21.6px */
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
}

.tip-grid {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.tip-card {
  padding: 16px;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--surface);
}

/* good/warn 原来是 Material 的语义色（#edf7ed 绿 / #fff8e1 米黄 + 同族描边），
   是全页唯一两处越出 v3 调色板的底色 —— 扫描器每次都报。2026-07-20 收成单色：
   白底 + 发丝线，好/坏改由**标题前的记号**区分（☞ / ✕，同全站的记号语汇），
   记号本身用荧光黄底压一下，保证不靠颜色也读得出来（不违反 WCAG 1.4.1）。 */
.tip-card strong {
  display: block;
  margin-bottom: 6px;
  font-size: 0.85rem;
}

.tip-card strong::before {
  display: inline-block;
  margin-right: 0.4em;
  padding: 0 0.3em;
  background: var(--accent-light);
  font-family: var(--mono);
}

.tip-card.good strong::before { content: "☞"; }
.tip-card.warn strong::before { content: "✕"; }

.tip-card p {
  margin: 0;
  color: var(--muted);
  font-size: 0.82rem;
}

.folder-tree {
  margin: 0;
  padding: 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: var(--mono);
  font-size: 0.76rem;
  color: var(--muted);
  overflow-x: auto;
  box-shadow: var(--shadow);
}

.folder-tree code {
  white-space: pre;
}

/* ── FAQ（批次6-P3：采用 demo .bs-faq* 结构）──
   容器一条顶线、每项一条底线，问题带 ☞ 前缀、hover 灰底。
   demo 的 .bs-faq-q 是 <button>，生产是 <summary>（保留原生 <details> 折叠，无 JS）——
   故这里比 demo 多几条 summary 专属重置，其余值逐条同 demo。 */
.bs-faq {
  border-top: 1px solid var(--bs-border);
}

.bs-faq-item {
  border-bottom: 1px solid var(--bs-border);
}

.bs-faq-q {
  width: 100%;
  text-align: left;
  padding: 1rem 0.25rem;
  font-weight: 500;
  font-size: 1rem;
  cursor: pointer;
  color: var(--bs-text);
  /* summary 专属：去原生三角 + 重置从正文继承来的行高/字距（demo 的 button 天然没有） */
  list-style: none;
  line-height: normal;
  letter-spacing: normal;
}

.bs-faq-q::-webkit-details-marker {
  display: none;
}

.bs-faq-q::before {
  content: '☞ ';
  color: var(--bs-text-sub);
}

/* demo 是 [aria-expanded="true"]（button+JS）；原生折叠用 [open]，同一状态。
   demo 给展开态的 ☞ 加荧光黄底 —— 2026-07-18 还原 v3 配色后照 demo 原样。 */
.bs-faq-item[open] > .bs-faq-q::before {
  background: var(--accent-light);
  color: var(--text);
}

.bs-faq-q:hover {
  background: var(--bs-bg-sub);
}

.bs-faq-a {
  padding: 0 0.25rem 1rem;
  font-size: 0.9rem;
  color: var(--bs-text-sub);
}

.bs-faq-a p {
  margin: 0;
}

/* 页脚：全幅黑底反转块 + 巨型压缩体字标 + 四栏（对应 demo 的 .bs-footer）。
   尺寸按 demo rem × 14.4（走查视口 1440 下 demo 根字号 14.4px）。 */
/* 页脚整体照 demo 的 .bs-footer 系列落值。原先这里的 px 是按 1440px 冻结的近似值
   （58 ≈ 4rem×14.4=57.6），且多了一条 demo 没有的 `font-size: 0.82rem` —— demo 的
   页脚吃 body 的 1rem，那 0.82rem 把整个页脚（栏目、链接、底栏）都压小了一号。 */
/* 批次6-P5b：类名与 demo 统一后，反转色也改吃 --bs-invert-*（P0 映射层里就是
   ink #111827 / #fff，与原来的 --accent / --surface 同值，零视觉变化）。 */
.bs-footer {
  margin-top: 4rem;
  padding: 4rem 0 2rem;
  background: var(--bs-invert-bg);
  color: var(--bs-invert-text);
}

.bs-footer-brand {
  font-family: var(--display);
  font-weight: 500;
  font-size: clamp(2.4rem, 6vw, 5rem);
  line-height: 1;
  letter-spacing: 0;
  text-transform: uppercase;
  margin: 0 0 3rem;
}

.bs-footer-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  gap: 2rem;
  padding-bottom: 2.5rem;
  border-bottom: 1px solid var(--accent-border);
}

/* 栏目标题是**灰色小标签**、走正文字体（demo 的 .bs-footer-cols h5 没写 font-family，
   继承 body 的 Zen Kaku）。原先写成了 Oswald + 白色 —— 那是把标题和链接的颜色写反了：
   demo 是「标题灰、链接白」，读起来才是标签在上、内容在下。
   font-weight 保持 500 不跟 demo 的 700：Zen Kaku 只有 400/500 字形，700 会被浏览器
   伪造成合成粗体而发糊 —— demo 那里就是合成粗体，626d3ec 专门修过这类，不跟。 */
.bs-footer-cols h5 {
  font-size: clamp(10px, 0.7vw, 13px);
  text-transform: uppercase;
  letter-spacing: 0.04rem;
  color: var(--accent-muted);
  margin: 0 0 0.9rem;
}

.bs-footer-cols a {
  display: block;
  padding: 0.2rem 0;
  font-size: 0.9rem;
  color: var(--surface);
  text-decoration: none;
}

/* demo 的 hover 是加下划线，不是变色（链接本来就是白的） */
.bs-footer-cols a:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.bs-footer-legal {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 1rem;
  padding-top: 1.5rem;
  font-size: 0.8rem;
  color: var(--accent-muted);
}

.bs-footer > p {
  margin: 0;
}

.toast {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%) translateY(120px);
  background: var(--text);
  color: var(--bg);
  border-radius: 0;
  padding: 10px 20px;
  font-size: 0.85rem;
  font-weight: 500;
  box-shadow: var(--shadow-lg);
  z-index: 100;
  transition: transform 0.25s ease;
  pointer-events: none;
}

.toast.show {
  transform: translateX(-50%) translateY(0);
}

/* Header actions + language menu */
/* v3 版式：品牌在最左，其余（导航 + 登录/CTA）作为**一整组**靠右 ——
   demo 的 .bs-header-inner 是 brand vs nav 两组 space-between。
   此前 header-actions 是 `flex:1` + space-between，把导航顶到品牌旁、
   登录甩到最右，中间一个大洞。 */
.header-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 1.75rem;
  min-width: 0;
}

/* 移动端抽屉开关（demo 的 .bs-nav-toggle）。桌面隐藏，≤720px 由下面的断点显示。 */
.nav-toggle {
  display: none;
  align-items: center;
  height: 2.25rem;
  padding: 0 1.1rem;
  font-family: var(--display);
  font-weight: 400;
  font-size: 0.85rem;
  letter-spacing: 0.08rem;
  text-transform: uppercase;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--text);
  border-radius: var(--radius-pill);
  cursor: pointer;
}

.site-header.is-open .nav-toggle {
  background: var(--text);
  color: var(--bg);
}

/* ── 页头搜索按钮（见 docs/首页搜索改造计划.md）──
   它是 .site-header 的**直接子元素**：.header-actions 在 ≤720px 会变成全屏抽屉，
   按钮放进去就意味着窄屏得先点 Menu 才能搜索。
   margin-right:auto 让它紧贴 brand —— .site-header 是 space-between，三个可见子元素
   会把它甩到正中。
   形态沿用旧首屏搜索框的语汇：胶囊 + ☞ 字形 + kbd 提示（首页那只框在批 S3 删除）。 */
.header-search {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  height: 2.25rem;
  margin-right: auto;
  padding: 0 0.5rem 0 1rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  color: var(--muted);
  font-family: inherit;
  font-size: 0.85rem;
  line-height: 1;
  cursor: pointer;
}

.header-search:hover {
  border-color: var(--text);
  color: var(--text);
}

/* 放大镜（用户 2026-07-27 指定，不用 ☞）。尺寸与页头 ▾ 同级，颜色跟随按钮的 color
   —— hover 时按钮整体转 --text，图标白拿。 */
.header-search-icon {
  width: 1rem;
  height: 1rem;
  flex-shrink: 0;
}

.header-search-kbd {
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(10px, 0.7vw, 13px);
  letter-spacing: 0.04rem;
  color: var(--muted);
  padding: 0.2rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  white-space: nowrap;
}

/* ── 命令面板弹窗 ──
   原生 <dialog> + showModal()：焦点陷阱、Esc 关闭、::backdrop、背景 inert 全是白送的。
   ⚠ display 只能写在 [open] 上 —— 写进基础规则会盖掉 UA 的 `dialog:not([open]){display:none}`，
   弹窗就永远开着。 */
.tool-search {
  width: min(40rem, calc(100% - 2.5rem));
  max-height: min(30rem, calc(100vh - 8rem));
  /* dialog 默认 `margin:auto` 垂直居中；命令面板的惯例是靠上，视线不用下移。 */
  margin: 4.5rem auto auto;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--text);
  border-radius: var(--radius);
  overflow: hidden;
}

.tool-search[open] {
  display: flex;
  flex-direction: column;
}

.tool-search::backdrop {
  background: rgba(0, 0, 0, 0.45);
}

.tool-search-head {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-shrink: 0;
  padding: 0 1rem 0 1.25rem;
  height: 3.5rem;
  border-bottom: 1px solid var(--border);
}

.tool-search-icon {
  width: 1.15rem;
  height: 1.15rem;
  flex-shrink: 0;
  color: var(--muted);
}

.tool-search-input {
  flex: 1;
  min-width: 0;
  height: 100%;
  padding: 0;
  border: 0;
  outline: none;
  background: transparent;
  font-family: inherit;
  font-size: 1rem;
  color: var(--text);
}

.tool-search-input::placeholder {
  color: var(--muted);
}

.tool-search-close {
  flex-shrink: 0;
  font-family: var(--display);
  font-size: clamp(10px, 0.7vw, 13px);
  letter-spacing: 0.04rem;
  text-transform: uppercase;
  color: var(--muted);
  background: var(--bg);
  padding: 0.2rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  cursor: pointer;
}

.tool-search-close:hover {
  color: var(--text);
  border-color: var(--text);
}

.tool-search-results {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

/* 结果行：分类胶囊 + 标题/描述 + ☞，与工具卡同一套语汇，只是横排。 */
.tool-search-item {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  padding: 0.7rem 1.25rem;
  text-decoration: none;
  color: inherit;
  border-bottom: 1px solid var(--border);
}

.tool-search-item:last-child {
  border-bottom: 0;
}

/* 键盘活动项与鼠标 hover 同一个视觉 —— 命令面板里两者是同一个「当前项」概念。 */
.tool-search-item:hover,
.tool-search-item.is-active {
  background: var(--surface-2);
  text-decoration: none;
}

.tool-search-item-cat {
  flex-shrink: 0;
  width: 5.5rem;
  font-family: var(--display);
  font-size: clamp(10px, 0.7vw, 13px);
  letter-spacing: 0.04rem;
  text-transform: uppercase;
  color: var(--muted);
}

.tool-search-item-main {
  flex: 1;
  min-width: 0;
}

.tool-search-item-title {
  display: block;
  font-size: 0.95rem;
}

.tool-search-item-desc {
  display: block;
  font-size: 0.8rem;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tool-search-item-arrow {
  flex-shrink: 0;
  font-family: var(--display);
  color: var(--muted);
}

.tool-search-item.is-active .tool-search-item-arrow {
  color: var(--text);
}

.tool-search-empty {
  margin: 0;
  padding: 2.5rem 1.25rem;
  text-align: center;
  color: var(--muted);
}

.tool-search-foot {
  display: flex;
  flex-shrink: 0;
  gap: 1.25rem;
  padding: 0.6rem 1.25rem;
  border-top: 1px solid var(--border);
  font-size: clamp(10px, 0.7vw, 13px);
  color: var(--muted);
}

.tool-search-foot kbd {
  font-family: var(--display);
  font-size: inherit;
  padding: 0 0.35rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

/* demo 的 .bs-nav-auth 是 `display: contents` —— Log in / Get started / 语言菜单
   全都直接吃 .bs-nav 的 `gap: 1.75rem`，与其余导航项同一个节奏。生产是两层结构
   （.header-actions > .primary-nav + .header-utilities），contents 会打断抽屉的
   grid 与 flex-shrink，故取等价做法：把本层 gap 对齐成同一个 1.75rem。
   原先的 6px 让 Log in 几乎贴着 Get started（实测 6px vs demo 25px）。 */
.header-utilities {
  display: flex;
  align-items: center;
  gap: 1.75rem;
  flex-shrink: 0;
}

/* 页头登录/注册：对应 demo 的 .bs-nav-login / .bs-btn--small，同为胶囊 */
/* Log in：demo 桌面端就是一条**纯文字导航链接**（.bs-nav-login 只带 .bs-nav-secondary）。
   描边胶囊是 demo 在 @media(max-width:720px) 里才给的形态 —— 桌面不该有框。 */
.header-auth-link {
  display: inline-flex;
  align-items: center;
  color: var(--text);
  font-size: 0.9rem;
  text-decoration: none;
  white-space: nowrap;
  transition: none;
}

.header-auth-link:hover {
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-thickness: 1px;
}

/* Get started：demo 的 .bs-btn--accent + .bs-btn--small ——
   荧光黄底 + 黑字黑边，hover 反转为黑底黄字。
   --accent-light 就是 #fdf497；--accent 在单色 token 下是纯黑，用它 CTA 会消失在页头里。 */
.header-auth-link--primary {
  height: 2.25rem;
  padding: 0 1.25rem;
  /* demo 的 .bs-btn--small 写的是 0.85rem，但它在页头里**渲染不出这个值** ——
     `.bs-nav a { font-size: 0.9rem }`(0,1,1) 压过 `.bs-btn--small`(0,1,0)。
     照抄字面值就会比 demo 小一号。同 .home-hero .hero-kicker 那处的坑。 */
  font-size: 0.9rem;
  border: 1px solid var(--text);
  border-radius: var(--radius-pill);
  background: var(--accent-light);
  color: var(--text);
}

.header-auth-link--primary:hover {
  background: var(--text);
  color: var(--accent-light);
  border-color: var(--text);
  text-decoration: none;
}

/* ── 移动端导航抽屉（demo 的 @media(max-width:720px) 顶栏折叠） ──
   结构映射：demo 是单层 .bs-nav 直接装链接 + .bs-nav-auth + 语言菜单；
   生产是 .header-actions 里包着 .primary-nav 与 .header-utilities 两层。
   所以抽屉本体 = .header-actions，大字链接 = .primary-nav a，
   登录/注册两列 = .header-utilities（语言切换器也在其中，排到末尾整行）。 */
@media (max-width: 720px) {
  .site-header .brand-tagline {
    display: none;
  }

  /* demo 的 .bs-wrap 在此断点收窄到 `padding: 0 1.25rem`。
     原先是 ≤640px 的 `min(1080px, calc(100% - 24px))` —— 断点和留白都不是 demo 的值，
     且那个 1080px 上限在 640 以下永远轮不到，是旧版式的残留。 */
  .wrap {
    width: min(80rem, calc(100% - 2.5rem));
  }

  .nav-toggle {
    display: inline-flex;
  }

  /* 窄屏收成图标态（只留 ☞），与 Menu 并排常驻 —— 它不在抽屉里，见组件注释。 */
  .header-search {
    gap: 0;
    padding: 0 0.9rem;
  }

  .header-search-label,
  .header-search-kbd {
    display: none;
  }

  /* 触屏没有 ↑↓/Enter/Esc，键位提示栏无意义；面板同时贴近顶部、吃满可用高度。 */
  .tool-search {
    margin-top: 3.25rem;
    max-height: calc(100vh - 4.5rem);
  }

  .tool-search-foot {
    display: none;
  }

  .tool-search-item-cat {
    display: none;
  }

  body.nav-open {
    overflow: hidden;
  }

  .header-actions {
    position: fixed;
    top: var(--drawer-top, calc(4rem + 1px));
    left: 0;
    right: 0;
    bottom: 0;
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    gap: 0;
    padding: 0.75rem 1.25rem 2rem;
    background: var(--bg);
    overflow-y: auto;
    visibility: hidden;
    opacity: 0;
    transform: translateY(-0.75rem);
    transition: opacity 0.35s cubic-bezier(0.19, 1, 0.22, 1), transform 0.55s cubic-bezier(0.19, 1, 0.22, 1),
      visibility 0s linear 0.35s;
  }

  .site-header.is-open .header-actions {
    visibility: visible;
    opacity: 1;
    transform: none;
    transition: opacity 0.35s cubic-bezier(0.19, 1, 0.22, 1), transform 0.55s cubic-bezier(0.19, 1, 0.22, 1);
  }

  /* 行项目错峰淡入（demo 的 .bs-nav > * + nth-child 延时） */
  .header-actions > * {
    opacity: 0;
    transform: translateY(0.75rem);
    transition: opacity 0.45s cubic-bezier(0.19, 1, 0.22, 1), transform 0.65s cubic-bezier(0.19, 1, 0.22, 1);
  }

  .site-header.is-open .header-actions > * {
    opacity: 1;
    transform: none;
  }

  .site-header.is-open .primary-nav > *:nth-child(1) { transition-delay: 0.05s; }
  .site-header.is-open .primary-nav > *:nth-child(2) { transition-delay: 0.1s; }
  .site-header.is-open .primary-nav > *:nth-child(3) { transition-delay: 0.15s; }
  .site-header.is-open .primary-nav > *:nth-child(4) { transition-delay: 0.2s; }
  .site-header.is-open .primary-nav > *:nth-child(5) { transition-delay: 0.25s; }

  .primary-nav {
    flex-direction: column;
    align-items: stretch;
    gap: 0;
  }

  /* demo 的抽屉链接是编辑部风格大字 */
  .primary-nav a {
    padding: 1.1rem 0;
    font-family: var(--display);
    font-weight: 500;
    font-size: 1.9rem;
    line-height: 1.1;
    letter-spacing: 0.02rem;
    text-transform: uppercase;
    border-bottom: 1px solid var(--border);
    /* ⚠ 必须撤掉基类那条 nowrap：它是给桌面一行导航防折行的，
       抽屉里字号 1.9rem，pt-BR 的「Todas as ferramentas」单条就有 ~380px，
       在 375px 的手机上会直接横向溢出。这里要的正是折行。 */
    white-space: normal;
  }

  /* 抽屉里的工具区（收藏 / 账户 / 语言）不在这里 —— 见文件末尾同断点的那个
     「抽屉工具区」@media。那些规则必须排在 .header-dropdown-* / .user-menu* /
     .lang-* / .header-fav 的基类**之后**才压得住：媒体查询不加特异度，同为
     (0,1,0) 时靠源码顺序定胜负，而那些基类全都写在本块下方。 */
}

@media (prefers-reduced-motion: reduce) {
  .header-actions,
  .header-actions > * {
    transition: none !important;
  }
}

.header-dropdown-chevron {
  width: 14px;
  height: 14px;
  color: var(--muted);
  flex-shrink: 0;
  transition: transform 0.18s ease;
}

details[open] > summary .header-dropdown-chevron {
  transform: rotate(180deg);
}

.header-dropdown-panel {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  z-index: 50;
  min-width: 180px;
  padding: 6px;
  background: var(--surface);
  border: 0;
  border-radius: 0;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12), 0 2px 6px rgba(0, 0, 0, 0.06);
  opacity: 0;
  transform: translateY(-6px);
  pointer-events: none;
  transition: opacity 0.16s ease, transform 0.16s ease;
}

.header-dropdown-panel::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: -10px;
  height: 10px;
}

details[open] .header-dropdown-panel {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

.header-dropdown-item {
  display: block;
  width: 100%;
  padding: 9px 12px;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 0.84rem;
  font-weight: 500;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease;
}

.header-dropdown-item:hover {
  background: var(--accent-light);
  color: var(--accent);
}

.header-dropdown-item.is-active,
.header-dropdown-item.is-current {
  background: var(--accent-light);
  color: var(--accent);
}

.header-dropdown-item--danger {
  color: #9a3412;
}

.header-dropdown-item--danger:hover {
  background: #fff7ed;
  color: #c2410c;
}

.user-menu {
  position: relative;
  font-size: 0.8rem;
}

.user-menu summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 8px 4px 4px;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: var(--text);
  font-weight: 500;
  max-width: 200px;
  transition: color 0.12s ease;
}

.user-menu summary::-webkit-details-marker {
  display: none;
}

.user-menu summary:hover {
  color: var(--accent);
}

.user-menu-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--accent-light);
  color: var(--accent);
  font-size: 0.78rem;
  font-weight: 500;
  flex-shrink: 0;
}

.user-menu-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.85rem;
}

/* 抽屉专用（≤720px 才显示）。桌面页头的 summary 只放头像 + 名字。 */
.user-menu-email {
  display: none;
}

.user-menu-panel {
  min-width: 200px;
}

.user-menu-divider {
  height: 1px;
  margin: 4px 8px;
  background: var(--border);
}

.user-menu-logout {
  margin: 0;
}

.user-menu-logout button {
  width: 100%;
}

/* v3 版式：语言切换是一行纯文字 + 文字 ▾（demo 的 .bs-menu summary）。
   此前是 cream/blue 时代的样式：地球仪 SVG + 箭头 SVG + 灰底小字 —— 91ebe66
   「去掉页头图标」那批没扫到它，因为当时前台 en-only、这个菜单是隐藏的，
   放开 zh 之后才露出来。demo 的页头通篇无图标。 */
/* ── 板块下拉（Image Tools ▾ / SVG Tools ▾，2026-08-01）─────────────
   在 .primary-nav 里，跟 .nav-secondary 那些纯文字链接并排，所以 summary 要跟
   `.primary-nav a` 长得一样（同字号、hover 同下划线）—— 它是 <summary> 不是 <a>，
   基类那两条选择器都命中不了，只能在这儿抄一份。
   面板改 left 对齐：基类 .header-dropdown-panel 是 right:0（给页头右侧的账户/语言用的），
   在左半边的导航里会把菜单甩到触发器左外侧。 */
.section-menu {
  position: relative;
}

.section-menu > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 0.35rem;
  /* summary 不是 <a>，`.primary-nav a` 那两条基类命中不了它 —— 字号与 nowrap 都得自己写，
     漏了的话长语种下板块名会自己折行。 */
  font-size: 0.9rem;
  white-space: nowrap;
  color: var(--text);
}

.section-menu > summary::-webkit-details-marker {
  display: none;
}

.section-menu > summary:hover,
.section-menu > summary[aria-current='page'] {
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-thickness: 1px;
}

.section-menu-panel {
  left: 0;
  right: auto;
  min-width: 200px;
}

/* 「Image Tools · 46 tools」那条总入口：比下面的分类条目重一档。 */
.section-menu-all {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
}

.section-menu-count {
  font-family: var(--display);
  font-weight: 300;
  font-size: 0.8rem;
  color: var(--muted);
}

.lang-menu {
  position: relative;
}

.lang-menu summary {
  list-style: none;
  cursor: pointer;
  font-size: 0.9rem; /* demo .bs-menu summary */
  user-select: none;
  white-space: nowrap;
}

.lang-menu summary::-webkit-details-marker {
  display: none;
}

/* demo 的 ▾ 是**文字**不是 SVG（.bs-menu summary::after） */
.lang-menu summary::after {
  content: ' ▾';
  color: var(--muted);
  font-size: 0.75em;
}

/* demo 的 .bs-menu[open] summary —— 展开时加下划线，不是变色 */
.lang-menu[open] summary {
  text-decoration: underline;
  text-underline-offset: 4px;
}

.lang-current-code {
  white-space: nowrap;
}

/* 抽屉里那一行语言触发条（桌面隐藏；≤720px 由「抽屉工具区」那个 @media 打开）。 */
.lang-drawer-trigger {
  display: none;
}

.lang-panel {
  max-height: min(70vh, 420px);
  overflow-y: auto;
}

.lang-group + .lang-group {
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
}

.lang-group-label {
  font-size: 0.62rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  padding: 4px 8px 6px;
}

.lang-option.is-active {
  background: var(--accent);
  color: var(--accent-text);
}

.lang-option.is-active:hover {
  background: var(--accent-hover);
  color: var(--accent-text);
}

.lang-panel a.lang-option {
  text-decoration: none;
}

/* ── 移动端语言弹窗（用户 2026-07-31 指定，见 docs/移动端抽屉版式.md）──
   原生 <dialog> + showModal()，形制沿用 .tool-search 那只命令面板。
   ⚠ display 只能写在 [open] 上 —— 写进基础规则会盖掉 UA 的
   `dialog:not([open]) { display: none }`，弹窗就永远开着。 */
.lang-dialog {
  width: min(22rem, calc(100% - 2.5rem));
  max-height: min(32rem, calc(100vh - 6rem));
  /* 保持 dialog 默认的 margin:auto 居中。tool-search 靠上是命令面板的惯例
     （视线不用下移继续打字），语言表没有这个前提。 */
  margin: auto;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--text);
  border-radius: var(--radius);
  overflow: hidden;
}

.lang-dialog[open] {
  display: flex;
  flex-direction: column;
}

.lang-dialog::backdrop {
  background: rgba(0, 0, 0, 0.45);
}

.lang-dialog-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  flex-shrink: 0;
  height: 3.5rem;
  padding: 0 0.75rem 0 1.25rem;
  border-bottom: 1px solid var(--border);
}

.lang-dialog-title {
  margin: 0;
  font-family: var(--display);
  font-weight: 500;
  font-size: 0.95rem;
  letter-spacing: 0.04rem;
  text-transform: uppercase;
}

.lang-dialog-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  color: var(--muted);
  font-size: 1.35rem;
  line-height: 1;
  cursor: pointer;
}

.lang-dialog-close:hover {
  border-color: var(--text);
  color: var(--text);
}

/* 这里滚是对的：弹窗是独立面板，不套在抽屉的滚动里（那才是之前要避开的滚动套滚动）。 */
.lang-dialog-list {
  overflow-y: auto;
}

.lang-dialog-option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 1rem 1.25rem;
  border-bottom: 1px solid var(--border);
  color: var(--text);
  font-size: 1rem;
  text-decoration: none;
}

.lang-dialog-option:last-child {
  border-bottom: 0;
}

.lang-dialog-option:hover {
  background: var(--surface-2);
}

.lang-dialog-option.is-active {
  font-weight: 600;
}

.lang-dialog-check {
  color: var(--accent);
}

/* RTL */
html[dir='rtl'] .site-header,
html[dir='rtl'] .header-actions,
html[dir='rtl'] .file-bar,
html[dir='rtl'] .pwa-banner,
html[dir='rtl'] .snippet-row {
  flex-direction: row-reverse;
}

html[dir='rtl'] .editor-panel {
  direction: rtl;
}

html[dir='rtl'] .editor-main {
  border-right: 0;
  border-left: 1px solid var(--border);
}

html[dir='rtl'] .lang-panel {
  right: auto;
  left: 0;
}

html[dir='rtl'] .primary-nav {
  flex-direction: row-reverse;
}

/* Responsive */
@media (max-width: 900px) {
  .editor-panel {
    grid-template-columns: 1fr;
  }

  .editor-main {
    border-right: 0;
    border-left: 0;
    border-bottom: 1px solid var(--border);
  }

  html[dir='rtl'] .editor-main {
    border-left: 0;
  }

  .split-layout {
    grid-template-columns: 1fr;
  }

  .device-previews {
    grid-template-columns: 1fr;
  }

  .preview-bottom {
    grid-template-columns: 1fr;
  }

  .content-sections {
    padding: 36px 0 28px;
    gap: 32px;
  }

  .tool-page-content .content-sections {
    padding: 20px 0 14px;
    gap: 24px;
  }

}

@media (max-width: 640px) {
  .content-sections {
    padding: 24px 0 20px;
    gap: 24px;
  }

  .tool-page-content .content-sections {
    padding: 16px 0 12px;
    gap: 20px;
  }



  .tip-grid {
    grid-template-columns: 1fr;
  }

  .folder-tree {
    font-size: 0.72rem;
  }

  .tool-page-tail {
    gap: 12px;
    padding: 10px 0 6px;
  }
}

@media (max-width: 520px) {
  .file-bar {
    flex-direction: column;
    align-items: flex-start;
  }

  .snippet-row {
    flex-direction: column;
  }

  .ios-grid {
    grid-template-columns: repeat(4, 1fr);
    gap: 6px;
  }

  .win-tiles {
    grid-template-columns: repeat(4, 1fr);
  }

  html[dir='rtl'] .file-bar {
    align-items: flex-end;
  }
}

/* ── OneImageTool extensions (F0) ── */
.oit-toast-root {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  z-index: 200;
  pointer-events: none;
}

.oit-toast {
  background: var(--text);
  color: var(--bg);
  border-radius: var(--radius-pill);
  padding: 10px 20px;
  font-size: 0.85rem;
  font-weight: 500;
  box-shadow: var(--shadow-lg);
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 0.2s, transform 0.2s;
  margin-top: 8px;
}

.oit-toast.is-visible {
  opacity: 1;
  transform: translateY(0);
}

.oit-toast--error { background: var(--danger); }
.oit-toast--success { background: var(--success); }

.ad-slot {
  min-height: 0;
  margin: 24px auto;
  max-width: 728px;
  /* Adcash 的 banner 是**固定尺寸**的，不像 AdSense 的自适应单元会撑满容器。
     `margin:auto` 只居中位置本身，居中不了里面的 iframe —— 而 --in-content-2 是
     全屏通栏（1920 视口下 1920px 宽），不加这条的话 728×90 会孤零零贴在最左边。 */
  text-align: center;
}

/* 广告位 div 始终在 DOM，但未被广告脚本填充时收起 —— 不占位、不产生 flex gap（用户 2026-07-18）。
   :empty 管的是还没配 slot ID 的位置（AdSlot 渲染成空 aside）。 */
.ad-slot:empty {
  display: none;
}

/* 配了 slot ID 但 AdSense 这一次没有广告可投时，它会给 <ins> 打上
   data-ad-status="unfilled"。此时 .ad-slot 已经不是 :empty 了，上面那条管不着，
   而 --below-tool / --in-content-* 的 min-height 会留下一条 90~250px 的空白。 */
.ad-slot:has(> .adsbygoogle[data-ad-status='unfilled']) {
  display: none;
}

/* Adcash 走的位置里恒有一个 <script> 子节点，所以上面两条**都管不着它**：
   :empty 因为有子节点不成立，unfilled 是 AdSense 的私有属性、Adcash 什么都不打。
   反过来判「除了 <script> 还有别的子元素才占位」，一条覆盖两种情况，
   且不依赖 aclib 到底注入的是 iframe 还是 div。
   ⚠ 若实测发现 aclib 把广告注在 <aside> **外面**（作为兄弟节点），这条会让位置
   一直收着、广告出现在它下方 —— 位置略偏但仍可见，届时改成给容器加标记类即可。 */
.ad-slot[data-ad-net='adcash']:not(:has(> :not(script))) {
  display: none;
}

/* ⚠ 这两条 min-height 是给 AdSense 的自适应单元占位用的。Adcash 是固定尺寸，
   高度由 iframe 自己决定，再叠 250px 就会在 728×90 下方留 160px 空白 —— 归零。
   代价是填充成功的瞬间会有一次 CLS，换来的是不填充时一条空白都没有
   （与用户 2026-07-18 定的「不占位」一致）。 */
.ad-slot[data-ad-net='adcash'] { min-height: 0; }

.ad-slot--below-tool { min-height: 90px; }

.ad-slot--in-content-1,
.ad-slot--in-content-2 { min-height: 250px; max-width: 100%; }

/* Generic tool page — tighter vertical rhythm between sections */

/* hero → 功能区的间距。demo 里由 `.bs-section` 的 `padding-top:2rem` 提供（44 个 demo
   工具页一致），生产这层是 .tool-page-main 且此前无上边距 —— 于是 .bs-page-hero 的
   border-bottom 与 .tool-shell 的 border-top 直接贴成一条双线。取 demo 同值补上。
   刻意用 padding-top 而非 padding 简写：.wrap 的横向留白是 width-calc 出来的（见文件
   开头注释），用简写会把它重置掉。 */
.tool-page-main {
  padding-top: 2rem;
}

.tool-page-main ~ .ad-slot--below-tool {
  margin: 12px auto;
}

.tool-page-content .content-sections {
  padding: 24px 0 16px;
  gap: 28px;
}

.tool-page-content .ad-slot {
  margin: 16px auto;
}

/* Post-tool sections (FAQ, related tools)。批次6-P7：FAQ(P3) 与 related(P4) 都已是
   .bs-section，故 tail 与正文区一样改吃 demo 的间距机制 —— 每区自带 padding-bottom、
   容器不再用 flex gap（P3 里欠的那笔）。末区的 padding-bottom 即到页脚的间距。 */
.tool-page-tail {
  padding-top: 12px;
}

/* 发丝线网格。批次6-P4 采用 demo 的类名与尺寸值，但**发丝线机制刻意不采用**：
   线由子元素各自描边（.bs-card 的 box-shadow），不用 demo 的容器涂底色。
   原做法是给容器 background: var(--border) 再让子元素盖白 —— 缝隙露出的那 1px
   即成线。构造很巧，但末行填不满时，空格子没有子元素去盖，整块容器底色就直接
   露出来变成一片灰：首页 17 项 / 4 轨 = 840px 宽的灰块（四个分类段全中）。
   demo 里看不出来，因为设计稿的格子是写死填满的；生产的工具数是动态的，还能被
   后台禁用，永远不可能保证是轨数的整数倍。
   改用子元素描边后：相邻卡片的 1px 描边在 1px 缝隙里重叠成一条线（同色不叠深），
   外圈描边即原来的容器 border，而空格子什么都不画 —— 与项目数无关。
   注：这里的 box-shadow 是 0 偏移 0 模糊的描边，不是投影，与阶段 6「blkswn 无投影」
   不冲突（同理于焦点环的 0 0 0 3px）。 */
/* demo .bs-grid 的 minmax(15rem,1fr)：15rem@1440px = 216px，1152 内容宽正好排 5 列。
   此前是 255px（旧值），只排得下 4 列 —— 这是首页与 demo 最显眼的版式差。 */
/* demo 的 .bs-grid 没有 margin-top：首页分类段的上留白全部来自
   .home-category-desc 的 `margin: -1.25rem 0 1.5rem`（已与 demo 一致）。 */
.bs-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 1px;
}

/* 相关工具 / 404：本就自带 margin-top:16px 覆盖掉基类原来的 18px，
   所以基类去掉 margin-top 对这两处零影响（实测前后都是 16px）。 */
.bs-grid--compact {
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  margin-top: 16px;
}

.bs-card {
  position: relative;
  display: flex;
  flex-direction: column;
  /* demo .bs-card：gap 0.75rem / padding 1.5rem / min-height 12rem（撑齐一行卡片的高度）。 */
  gap: 0.75rem;
  min-height: 12rem;
  padding: 1.5rem;
  background: var(--surface);
  border: 0;
  border-radius: var(--radius);
  box-shadow: 0 0 0 1px var(--border); /* 发丝线：见 .bs-grid 处的说明 */
  text-decoration: none;
  color: inherit;
  overflow: hidden;
  transition: background-color 0.15s ease; /* demo .bs-card 原样 */
}

.bs-card::before {
  content: '';
  position: absolute;
  inset: 0 auto 0 0;
  width: 3px;
  background: var(--cat, var(--accent));
  opacity: 0;
  transition: opacity 0.15s;
}

/* demo .bs-card:hover 只做一件事 —— 底色转次级灰。
   原先的 border-color 自阶段 3 给卡片 border:0 起就是死声明；
   box-shadow: var(--shadow-lg) 自阶段 6 起是 none，留着会把上面那条发丝线在 hover 时抹掉；
   translateY(-2px) 是高度语汇，与阶段 6 去投影同因去掉。 */
.bs-card:hover {
  background: var(--surface-2);
  text-decoration: none;
}

.bs-card:hover::before {
  opacity: 1;
}

.bs-card--convert { --cat: var(--c-convert); --cat-soft: var(--c-convert-soft); }
.bs-card--edit { --cat: var(--c-edit); --cat-soft: var(--c-edit-soft); }
.bs-card--optimize { --cat: var(--c-optimize); --cat-soft: var(--c-optimize-soft); }
.bs-card--creative { --cat: var(--c-creative); --cat-soft: var(--c-creative-soft); }

.bs-card-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

/* 卡片编号（demo 的 .bs-card-num）。用 CSS counter 而非在 Blade 里写死：
   序号跟着实际渲染出的卡片走，禁用某个工具后不会留空号，也无需改标记。
   v3 版式：全站连号 01–46（demo 是 Convert 01–17、Edit 18–35、Optimize 36–37、
   Creative 38–46），所以 counter-reset 挂在所有网格的共同祖先 .home-main 上，
   而不是每个 .bs-grid 各自重置（那样每个分类都从 01 重新开始）。 */
.home-main {
  counter-reset: bs-card;
}

/* Related tools 与 404 的网格是独立的一组，各自从 01 起数（demo 同样如此）。
   显式重置：不写的话它们会退到隐式根计数器，眼下结果碰巧也是 01 起，但那不是保证。 */
.bs-grid--compact {
  counter-reset: bs-card;
}

.bs-card-top::before {
  counter-increment: bs-card;
  content: counter(bs-card, decimal-leading-zero);
  order: -1; /* demo 里编号在左（.bs-card-num），图标顺势移到右侧（demo 那里是分类胶囊） */
  font-family: var(--display);
  font-weight: 300;
  font-size: 1.6rem;
  line-height: 1;
  color: var(--muted);
}

/* 相关工具传入显式全站号时，关掉 counter，改用 .bs-card-num（同 demo .bs-card-num 的样子）。 */
.bs-card-top--fixed-num::before {
  content: none;
  counter-increment: none;
}

.bs-card-num {
  order: -1;
  font-family: var(--display);
  font-weight: 300;
  font-size: 1.6rem;
  line-height: 1;
  color: var(--muted);
}

/* v3 版式：分类是发丝线胶囊里的名称，没有分类色（demo 的 .bs-card-cat 一律 border+灰字）。
   尺寸按「四、rem/clamp」换算：padding 0.1rem/0.8rem × 14.4 ≈ 1px/12px；
   font-size clamp(10px,0.7vw,13px) 在 1440 下 = 10px。 */
/* demo .bs-card-cat 原值（此前 padding/字号都是冻结的 px）。 */
.bs-card-cat {
  padding: 0.1rem 0.8rem;
  font-size: clamp(10px, 0.7vw, 13px);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  color: var(--muted);
  background: var(--bg);
  white-space: nowrap;
}

/* ☞ 母题：卡片右下角，hover 右移（demo 的 .bs-card-arrow）。1.1rem × 14.4 ≈ 16px。 */
.bs-card-arrow {
  align-self: flex-end;
  margin-top: auto;
  font-size: 1.1rem; /* demo .bs-card-arrow */
  color: var(--text);
  transition: transform 0.6s cubic-bezier(0.19, 1, 0.22, 1);
}

.bs-card:hover .bs-card-arrow {
  transform: translateX(4px);
}

.bs-card.is-coming {
  opacity: 0.65;
  pointer-events: none;
}

/* h3 与 h4 同一套：卡片标题的级别随上文变（见 x-tool-card 的 level prop），
   首页/板块页/404 是 h4，related-tools 与收藏页是 h3 —— 视觉必须完全一致。 */
.bs-card h3,
.bs-card h4 {
  margin: 0;
  /* 依 demo 的 .bs-card h3：卡片标题走正文体，不用压缩体、不大写 */
  font-family: var(--sans);
  font-weight: 500;
  font-size: 1.1rem;
  line-height: 1.4;
}

/* demo .bs-card p 的 `flex:1` 把描述撑开，箭头才会被推到卡片底部对齐。 */
/* demo 的 .bs-card p 不写 line-height，吃 body 的 1.6 —— 这里原先多了一条 1.5 */
.bs-card p {
  margin: 0;
  flex: 1;
  font-size: 0.85rem;
  color: var(--muted);
}

.bs-card .badge {
  align-self: flex-start;
  font-size: 0.68rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 4px 8px;
  border-radius: var(--radius-pill);
  background: var(--surface-2);
  color: var(--muted);
}

/* demo .bs-section-head h2 原值。demo 里「IMAGE TOOLS」与各分类标题（CONVERT/EDIT…）
   同为 .bs-section-head h2、同 1.5rem；生产保留 h2/h3 的语义分级（文档大纲更正确），
   只把字号对齐 —— 视觉一致，markup 不动。 */
.home-section-title {
  margin: 0;
  font-family: var(--display);
  text-transform: uppercase;
  letter-spacing: 0.04rem;
  font-size: 1.5rem;
  font-weight: 500;
}

/* ── Home page ── */
/* 首屏搜索框（.home-search*）已于 2026-07-27 整块移除：搜索迁到页头的 .header-search
   + .tool-search 弹窗（全站可用）。这是生产**刻意偏离 demo v3** 的第一处结构 ——
   demo 首页有 .bs-search、页头无搜索，对照器报差异时不要当回归修回去。
   见 docs/首页搜索改造计划.md。 */

.trust-row {
  display: flex;
  justify-content: center;
  gap: 0.5rem 1.75rem;
  flex-wrap: wrap;
  font-size: clamp(10px, 0.7vw, 14px);
  letter-spacing: 0.04rem;
  color: var(--muted);
}

/* demo .bs-trust span::before：对勾是**字形**不是 SVG（首页通篇无图标）。 */
.trust-row span::before {
  content: "✓ ";
  color: var(--text);
}

/* ── 首页万能面板 ────────────────────────────────────────────────
   idle 态是服务端渲染的静态 HTML；处理逻辑等首个文件落地才动态 import
   （见 home.blade.php 的引导脚本）。三态共用 .home-panel-view 的固定
   min-height，切换不改变文档高度 —— 首页 CLS 必须保持 0。
   2026-07-26：首屏改水平居中，面板是主角 —— 宽度从 38rem（原来跟 .bs-hero p 同列宽、
   左缘对齐）放大到 44rem 并左右 auto 居中。
   2026-07-30 再放大到 52rem（1440 下 748.8px，.wrap 是 1152px，左右各余 200px）。
   ⚠ 面板变宽不会让 result 态的对比预览跟着变大 —— 它由 home-panel.js 的
   PREVIEW_MAX_W/H 封顶（那个高度上限是刻意的首屏预算）。.compare-container 是
   justify-content:center，故富余的宽度是对称留白，不会把预览甩到一边。 */
.home-panel {
  position: relative;
  max-width: 52rem;
  margin: 0 auto 2rem;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  transition: border-color 0.2s, background 0.2s;
}

/* 拖拽高亮沿用 .upload-drop 的 is-dragging 语汇，与工具页上传台一致 */
.home-panel.is-dragging {
  border-color: var(--accent);
  border-style: solid;
  background: var(--accent-light);
}

/* 出结果后不再是「投放区」，收掉虚线 */
.home-panel[data-state='result'] {
  border-style: solid;
  background: var(--surface);
}

.home-panel-file {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.home-panel-view {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  /* 2026-07-26：投放区加高（原 10rem）。三态共用这一个值，切换仍不改文档高度。
     2026-07-30 再加高到 16rem —— 上面 hero 收紧省出的高度直接落到这里；
     同日二次加高到 19rem。这个值只对 idle/busy 生效（result 态内容本就超过它），
     而 idle 正是「上传框」本身。 */
  min-height: 19rem;
  /* 上留白由 .home-panel-modes 提供，这里只补下半部分 */
  padding: 1rem 1.75rem 1.75rem;
}

.home-panel-view--center {
  align-items: center;
  justify-content: center;
  text-align: center;
}

.home-panel-drop {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  cursor: pointer;
  text-align: center;
}

/* 带前缀同 -hint：idle 的主文案是 <span>（免疫），busy 的是 <p> —— 不修它，busy 态
   「Working on it…」下面就会挂着 `.bs-hero p` 的 2.25rem，且字号比 idle 小一档。 */
.home-panel .home-panel-title {
  margin: 0;
  font-family: var(--display);
  /* 标题降档后面板成为首屏视觉重心，主文案跟着上调（原 1.05rem） */
  font-size: 1.2rem;
  letter-spacing: 0.02rem;
  text-transform: uppercase;
  color: var(--text);
}

/* 带 .home-panel 前缀同 .home-panel-name/-stat：idle 态的两行提示是 <span>（免疫），
   但 busy 态的隐私落款是 <p>，会被 `.bs-hero p`(0,1,1) 撑成 1rem 并多 2.25rem 下边距。 */
.home-panel .home-panel-hint {
  margin: 0;
  font-size: clamp(10px, 0.7vw, 13px);
  letter-spacing: 0.04rem;
  color: var(--muted);
}

/* 移动端无拖拽与剪贴板语义：换文案、隐藏 Ctrl+V 提示 */
.home-panel-only-mobile {
  display: none;
}

.home-panel-spinner {
  width: 1.75rem;
  height: 1.75rem;
  border: 2px solid var(--border);
  border-top-color: var(--text);
  border-radius: 50%;
  animation: home-panel-spin 0.8s linear infinite;
}

@keyframes home-panel-spin {
  to {
    transform: rotate(360deg);
  }
}

.home-panel-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
}

/* 这两条与下面的 .home-panel-stat 刻意带上 .home-panel 前缀（0,2,0）：它们是 <p>，
   会被 hero 的 `.bs-hero p`(0,1,1) 的 `margin: 0 0 2.25rem` 压过 —— 单类的 `margin: 0`
   一直没生效，result 态这两行的行距因此偏松。间距统一由 .home-panel-view 的 gap 给。 */
.home-panel .home-panel-name {
  margin: 0;
  overflow: hidden;
  color: var(--text);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.home-panel-again {
  flex: none;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: clamp(10px, 0.7vw, 13px);
  color: var(--muted);
  text-decoration: underline;
  cursor: pointer;
}

.home-panel-again:hover {
  color: var(--text);
}

.home-panel .home-panel-stat {
  margin: 0;
  font-family: var(--display);
  font-size: 1.15rem;
  color: var(--text);
}

/* 模式 pill：三个 view 之外，idle 与 result 都可见 —— result 态切模式即重算同一张图 */
.home-panel-modes {
  display: flex;
  flex-wrap: wrap;
  /* 2026-07-26：首屏居中后这行也跟着居中（三态恒定，落文件时不会跳位） */
  justify-content: center;
  gap: 0.4rem;
  padding: 1rem 1.75rem 0;
}

.home-panel-mode {
  padding: 0.3rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: transparent;
  font: inherit;
  font-size: clamp(10px, 0.7vw, 13px);
  letter-spacing: 0.04rem;
  color: var(--muted);
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s, background 0.15s;
}

.home-panel-mode:hover {
  border-color: var(--text);
  color: var(--text);
}

.home-panel-mode[aria-pressed='true'] {
  border-color: var(--text);
  background: var(--text);
  color: var(--bg);
}

/* ── 面板文件类型切换（Image / SVG）─────────────────────────────
   排在模式 pill **之上**：先选文件类型，再选对该类型做什么。
   SVG 侧只做分流（列出板块工具入口），刻意不在面板内解析 —— SVG 是活代码，
   面板内渲染要先有 S1 的 parse.js 沙箱，见 docs/新板块设计-SVG.md §4.2。 */
.home-panel-types {
  display: flex;
  justify-content: center;
  gap: 0.4rem;
  padding: 1.25rem 1.75rem 0;
}

.home-panel-type {
  padding: 0.25rem 1rem;
  border: 0;
  border-bottom: 2px solid transparent;
  background: transparent;
  font-family: var(--display);
  font-size: 0.85rem;
  font-weight: 500;
  letter-spacing: 0.06rem;
  text-transform: uppercase;
  color: var(--muted);
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s;
}

.home-panel-type:hover {
  color: var(--text);
}

.home-panel-type[aria-pressed='true'] {
  border-bottom-color: var(--text);
  color: var(--text);
}

/* 两种类型共用 idle/busy/result 三个 view，差异只在**文案与部分控件**上，
   靠这一对类按 data-type 二选一。刻意不做第四个 view：home-panel.js 的三态
   开关（setView 里那三行 hidden）会因此多一个分支，且切类型时两套 hidden
   互相覆盖，很难不出 bug。 */
/* idle 区里这两个是**包一组文案的壳**，得自己复刻 .home-panel-drop 的列式排布，
   否则标题与提示会挤成一行。刻意不用 display:contents —— 它在部分浏览器上会
   把元素从无障碍树里摘掉，而这里包的正是要读出来的说明文字。
   ⚠ 只挑 .home-panel-drop 的直接子元素：同名类还挂在两套倍率按钮上，
   给 <button> 套这份 flex 会把它撑变形。 */
.home-panel-drop > .home-panel-for-image,
.home-panel-drop > .home-panel-for-svg {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
}

.home-panel[data-type='image'] .home-panel-for-svg,
.home-panel[data-type='svg'] .home-panel-for-image {
  display: none;
}

/* SVG 不出模式 pill：矢量没有「压缩」可言，前后对比也永远是两张一样的图 ——
   面板在 SVG 态固定只做光栅化（格式 + 输出尺寸），模式选择就没有意义了。 */
.home-panel[data-type='svg'] .home-panel-modes {
  display: none;
}

.home-panel-controls {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.home-panel-field {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
}

.home-panel-field input[type='range'] {
  flex: 1;
  min-width: 8rem;
}

.home-panel-field input[type='number'] {
  width: 5.5rem;
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  font: inherit;
  font-size: 0.9rem;
  color: var(--text);
}

.home-panel-field input[type='color'] {
  width: 2.5rem;
  height: 1.75rem;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  cursor: pointer;
}

.home-panel-field .listbox {
  flex: 1;
  min-width: 7rem;
}

.home-panel-label,
.home-panel-out {
  font-size: clamp(10px, 0.7vw, 13px);
  letter-spacing: 0.04rem;
  color: var(--muted);
}

.home-panel-out {
  min-width: 2.5rem;
  color: var(--text);
  text-align: right;
}

.home-panel-size {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.home-panel-subrow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  font-size: clamp(10px, 0.7vw, 13px);
  color: var(--muted);
}

.home-panel-check {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  cursor: pointer;
}

/* 复选框跟随全站单色调（range 在全局已有同款设定） */
.home-panel-check input[type='checkbox'] {
  accent-color: var(--accent);
}

.home-panel-percents {
  display: inline-flex;
  gap: 0.3rem;
}

.home-panel-pct {
  padding: 0.2rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: transparent;
  font: inherit;
  font-size: inherit;
  color: var(--muted);
  cursor: pointer;
}

.home-panel-pct:hover {
  border-color: var(--text);
  color: var(--text);
}

/* Level 3：<details> 原生折叠，零 JS */
.home-panel-advanced > summary {
  font-size: clamp(10px, 0.7vw, 13px);
  letter-spacing: 0.04rem;
  color: var(--muted);
  cursor: pointer;
}

.home-panel-advanced > summary:hover {
  color: var(--text);
}

.home-panel-advanced[open] > summary {
  margin-bottom: 0.75rem;
}

.home-panel-advanced > *:not(summary) {
  margin-top: 0.6rem;
}

/* 对比预览：整套 .compare-* 复用 compress 工具页样式，这里只收口留白 */
.home-panel-compare {
  padding: 8px;
}

/* 同上。它是面板最后一个元素，那 2.25rem 下边距会直接叠在 .home-panel-view 的
   下 padding 上。max-width 也一起收回：`.bs-hero p` 的 38rem 比面板内容宽（40.5rem）窄，
   会让这排链接提前折行。 */
.home-panel .home-panel-more {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
  margin: 0;
  max-width: none;
  font-size: clamp(10px, 0.7vw, 13px);
}

.home-panel-more-links {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

.home-panel-more-links a {
  color: var(--muted);
  text-decoration: underline;
}

.home-panel-more-links a:hover {
  color: var(--text);
}

@media (max-width: 640px) {
  .home-panel-view {
    padding: 1rem 1.25rem 1.25rem;
  }

  .home-panel-modes {
    padding: 1rem 1.25rem 0;
  }

  .home-panel-only-desktop {
    display: none;
  }

  .home-panel-only-mobile {
    display: block;
  }

  /* Level 2 控件单列堆叠：窄屏下 label + 输入并排会挤成两三个字一行 */
  .home-panel-field {
    align-items: flex-start;
    flex-direction: column;
    gap: 0.35rem;
  }

  .home-panel-field input[type='range'],
  .home-panel-field input[type='number'],
  .home-panel-field .listbox {
    width: 100%;
  }

  /* 质量那行的读数本来在最右，堆叠后跟回标签同一行更省高度 */
  .home-panel-out {
    text-align: left;
  }

  .home-panel-subrow {
    justify-content: flex-start;
  }
}

@media (prefers-reduced-motion: reduce) {
  .home-panel-spinner {
    animation-duration: 2.4s;
  }
}

.home-main {
  padding-bottom: 24px;
}

/* 关键词跑马灯（demo 的 .bs-keywords）。序列 ×2 + translateX(-50%) 实现无缝循环。 */
/* 批次6-P5 起与 demo 同构：上面那条线由 .bs-hero 的 border-bottom 提供（它现在是
   满幅外壳 + 内层 .wrap，线天然通栏），本元素只留 border-bottom，同 demo .bs-keywords。
   此前上线写在这里，是因为旧的 .home-hero 自带 .wrap、只有 1152 宽，
   线会缩成内容宽（实测 144..1295 vs demo 的 0..1439）—— 那个前提已消失。 */
.keywords {
  overflow: hidden;
  padding: 1.25rem 0; /* demo .bs-keywords */
  border-bottom: 1px solid var(--border);
}

.keywords-track {
  display: flex;
  gap: 0.6rem; /* demo .bs-keywords-track */
  width: max-content;
  animation: marquee 40s linear infinite;
}

.keywords:hover .keywords-track {
  animation-play-state: paused;
}

/* demo .bs-tag 原值。 */
.tag {
  display: inline-block;
  padding: 0.25rem 1rem;
  font-size: clamp(10px, 0.7vw, 14px);
  line-height: 1.6;
  white-space: nowrap;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  text-decoration: none;
}

.tag:hover {
  background: var(--accent);
  color: var(--surface);
}

@keyframes marquee {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

/* v3 版式：标题行（Image tools … 46 tools，两端对齐、独占一行）在上，
   分类 chips 另起一行 —— demo 是 .bs-section-head 后跟 .bs-cat-nav 两个块。
   此前是标题与 chips 挤在同一行 space-between。gap = demo 的 1.25rem × 14.4。 */
.home-toolbar {
  display: flex;
  flex-direction: column;
  gap: 18px;
  margin-top: 16px;
}

.home-cat-nav {
  display: flex;
  gap: 0.5rem; /* demo .bs-cat-nav */
  flex-wrap: wrap;
}

/* demo .bs-cat-chip 原值（此前 gap/padding 是冻结 px、字重 500 也非 demo 值）。 */
.cat-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.85rem;
  padding: 0.35rem 1.1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--text); /* demo .bs-cat-chip 是 var(--bs-text) 纯黑，不是 --label 的灰 */
  text-decoration: none;
  transition: border-color 0.15s, color 0.15s, background 0.15s;
}

.cat-chip:hover {
  border-color: var(--cat, var(--accent));
  color: var(--cat, var(--accent));
  background: var(--cat-soft, var(--accent-light));
  text-decoration: none;
}

.cat-chip svg {
  width: 14px;
  height: 14px;
  color: var(--cat, var(--accent));
}

/* demo .bs-cat-chip .bs-cat-count：压缩体细字裸数字，没有灰底方块也没有内距。 */
.cat-chip-count {
  font-family: var(--display);
  font-size: 0.8rem;
  font-weight: 300;
  color: var(--muted);
}

.cat-chip--convert { --cat: var(--c-convert); --cat-soft: var(--c-convert-soft); }
.cat-chip--edit { --cat: var(--c-edit); --cat-soft: var(--c-edit-soft); }
.cat-chip--optimize { --cat: var(--c-optimize); --cat-soft: var(--c-optimize-soft); }
.cat-chip--creative { --cat: var(--c-creative); --cat-soft: var(--c-creative-soft); }

/* 首页顶部这排 chip 指向的是**板块**（不是分类）。首页 2026-08-01 起是全站集合页，
   板块/分类两层都出 chip 会糊成一片，分类 chip 让给板块页（section.blade.php 那边还在）。
   排版对齐板块标题：压缩体 + 大写，跟工具页那种分类 chip 一眼能分开。 */
.cat-chip--section {
  font-family: var(--display);
  text-transform: uppercase;
  letter-spacing: 0.04rem;
}

/* ── 首页板块块（板块 → 分类 两层分组）─────────────────────────────
   ⚠ .home-section-title 与 .home-category-title 基类同为 1.5rem（demo 里两者本就同级，
   那时首页只有分类一层）。两层嵌套后必须拉开，否则读者分不清层级 —— 板块层加通栏粗线
   + 大一号字，只在 .home-section 内作用域生效，板块页与 Guide 段的同名类不受影响。 */
.home-section {
  margin-top: 64px;
  padding-top: 28px;
  border-top: 2px solid var(--text);
  scroll-margin-top: 90px;
}

.home-section > .home-section-head {
  margin-bottom: 0.6rem;
}

.home-section > .home-section-head .home-section-title {
  font-size: 1.9rem;
}

/* 板块标题本身就是板块页入口 —— 不另做「查看全部」按钮，省一个 ×12 的翻译键。 */
.home-section-link {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  color: inherit;
  text-decoration: none;
}

.home-section-link:hover {
  text-decoration: underline;
  text-underline-offset: 6px;
  text-decoration-thickness: 1px;
}

.home-section-arrow {
  font-size: 1.1rem;
  color: var(--muted);
  transition: transform 0.15s, color 0.15s;
}

.home-section-link:hover .home-section-arrow {
  color: var(--text);
  transform: translateX(3px);
}

.home-section-desc {
  margin: 0;
  max-width: 42rem;
  font-size: 0.95rem;
  color: var(--muted);
}

/* 板块块里的第一个分类不吃 .home-category 的 44px —— 上面已有标题与简介垫着。 */
.home-section > .home-category:first-of-type {
  margin-top: 32px;
}

@media (prefers-reduced-motion: reduce) {
  .home-section-arrow {
    transition: none;
  }
}

.home-category {
  margin-top: 44px;
  scroll-margin-top: 90px;
}

/* v3 版式：分区标题与右侧计数两端对齐（demo 的 .bs-section-head + .bs-count）。 */
/* demo .bs-section-head 原值。 */
.home-section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 2rem;
}

/* demo 只给「Image tools」那一个区标题写了内联的 margin-bottom:1.25rem
   （各分类标题仍是默认的 2rem）—— 它下面紧跟分类导航，收得更紧。 */
.home-toolbar .home-section-head {
  margin-bottom: 1.25rem;
}

.home-count {
  font-family: var(--display);
  font-weight: 300;
  color: var(--muted);
  font-size: 1rem; /* demo .bs-count 原值 */
  white-space: nowrap;
}

.home-category-title {
  margin: 0;
  font-family: var(--display);
  text-transform: uppercase;
  letter-spacing: 0.04rem;
  font-size: 1.5rem; /* 同 .home-section-title：demo 两者同为 .bs-section-head h2 */
  font-weight: 500;
}

/* demo .bs-cat-desc：负上边距把描述收回到分区标题下方（抵消 .bs-section-head 的 2rem）。 */
.home-category-desc {
  margin: -1.25rem 0 1.5rem;
  max-width: 42rem;
  font-size: 0.9rem;
  color: var(--muted);
}

.home-category--convert { --cat: var(--c-convert); --cat-soft: var(--c-convert-soft); }
.home-category--edit { --cat: var(--c-edit); --cat-soft: var(--c-edit-soft); }
.home-category--optimize { --cat: var(--c-optimize); --cat-soft: var(--c-optimize-soft); }
.home-category--creative { --cat: var(--c-creative); --cat-soft: var(--c-creative-soft); }

/* Guide 段 —— demo 的 `.bs-section > .bs-section-head + .bs-seo`。
   demo 的段落本身不带边线（`.bs-section { padding: 3.5rem 0 }` 而已），
   分隔线全部来自 .bs-seo 自己的发丝线方框。 */
.seo-strip {
  padding: 3.5rem 0;
}

/* 发丝线方框：`gap:1px` + 容器涂 --border + 格子盖白 —— 缝隙即分隔线，
   与首页工具网格、工作区分栏是同一个手法（demo 的 .bs-seo 原样）。
   注意这里能用容器涂色的老办法：Guide 是 4 个写死的格子，不像工具网格那样
   数量随后台启用状态变（阶段 7 那次漏灰正是因为格子数不定，见 9070ebc）。 */
.seo-strip-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: 1px;
  background: var(--border);
  border: 1px solid var(--border);
}

.seo-strip-cols > div {
  background: var(--surface);
  padding: 1.5rem;
}

/* ⚠ 作用域必须是 **.seo-strip-cols**，不能写成 `.seo-strip h2`。
   这条 1rem 本来只想管四个专栏标题，但 `.seo-strip h2` 的 (0,1,1) 会顺带压过
   区标题「Guide」身上 .home-section-title 的 (0,1,0) —— 于是 Guide 长期被渲染成
   1rem，而它跟「Image Tools」「SVG Tools」一样裹在 .home-section-head 里、
   本该是 1.5rem 的区标题。收窄作用域即恢复设计意图。
   （专栏标题 2026-08-01 已从 h2 降为 h3，见 blade 注释。） */
.seo-strip-cols h3 {
  margin: 0 0 0.75rem;
  font-family: var(--display);
  text-transform: uppercase;
  letter-spacing: 0.04rem;
  font-size: 1rem;
}

.seo-strip p {
  margin: 0;
  font-size: 0.85rem;
  color: var(--muted);
}

/* demo 的 .bs-seo a：正文里的链接是黑字加下划线，不是 accent 色无下划线 */
/* 正文/散文链接 = demo 的 .bs-link：**墨色** + 下划线，hover 出 ☞（见下方 ::before）。
   2026-07-18 还原 demo v3 配色，撤回第三轮的蓝色链接。选择器范围保留（批次6-P2/P4
   加的 .bs-section/.content-block 等是结构性补齐，与配色无关）；基础 a（导航/卡片/
   关键词 tag，text-decoration:none）本就是墨色，不受影响。 */
.seo-strip a,
.bs-section a:not(.bs-card),
.legal-section a {
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}

/* ── Tool page breadcrumb ── */
/* demo 把面包屑放在 .bs-page-hero 内、hero padding-top 3.5rem 提供「与顶部的间距」；
   生产面包屑是 hero 前的独立 .wrap，故这里承担那段顶部间距，再用下边距接到 hero。 */
/* ── 面包屑（批次6-P1：采用 demo .bs-breadcrumb，在 .bs-page-hero>.wrap 内）──
   顶部间距由 .bs-page-hero 的 padding-top 提供（不再压在面包屑自己身上）。 */
.bs-breadcrumb {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  font-size: clamp(10px, 0.7vw, 13px);
  color: var(--bs-text-sub);
  margin-bottom: 1.25rem;
}

.bs-breadcrumb a {
  color: var(--bs-text);
  text-decoration: none;
}

.bs-breadcrumb a:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.cat-tag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 10px;
  border-radius: 0;
  background: var(--cat-soft, var(--accent-light));
  color: var(--cat, var(--accent));
  font-weight: 500;
}

.cat-tag svg {
  width: 11px;
  height: 11px;
}

.cat-tag--convert { --cat: var(--c-convert); --cat-soft: var(--c-convert-soft); }
.cat-tag--edit { --cat: var(--c-edit); --cat-soft: var(--c-edit-soft); }
.cat-tag--optimize { --cat: var(--c-optimize); --cat-soft: var(--c-optimize-soft); }
.cat-tag--creative { --cat: var(--c-creative); --cat-soft: var(--c-creative-soft); }

.tool-page-tail[class*='cat-'] .bs-section,
.wrap.content-sections {
  --cat: inherit;
}

.content-sections.cat-convert,
.tool-page-content.cat-convert,
.tool-page-tail.cat-convert { --cat: var(--c-convert); --cat-soft: var(--c-convert-soft); }
.content-sections.cat-edit,
.tool-page-content.cat-edit,
.tool-page-tail.cat-edit { --cat: var(--c-edit); --cat-soft: var(--c-edit-soft); }
.content-sections.cat-optimize,
.tool-page-content.cat-optimize,
.tool-page-tail.cat-optimize { --cat: var(--c-optimize); --cat-soft: var(--c-optimize-soft); }
.content-sections.cat-creative,
.tool-page-content.cat-creative,
.tool-page-tail.cat-creative { --cat: var(--c-creative); --cat-soft: var(--c-creative-soft); }

td strong {
  font-family: var(--mono);
  color: var(--cat, var(--accent));
}

/* ── Resize tool ── */
.resize-preview-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 16px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  min-height: 200px;
}

.resize-preview-canvas {
  max-width: 100%;
  height: auto;
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.resize-preview-meta {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--muted);
}

.resize-dim-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}

/* ⚠ 选择器必须挂在 .resize-dim-row 上，不能挂在 .resize-controls 上。
   后者只有图片 resizer 的 <aside> 有，而 svg-resizer / svg-crop 复用的是
   .resize-dim-row —— 挂错那层的话这两页的数字框一条样式都吃不到，
   直接露出浏览器默认框（Arial 13px / 2px 边框 / 0 圆角 / 高 21px）。
   .resize-dim-row 才是三页真正共用的那一层。 */
.resize-dim-row .field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 0.78rem;
  font-weight: 500;
  color: var(--muted);
}

.resize-dim-row .field input {
  width: 100%;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-family: var(--mono);
  font-size: 0.9rem;
}

.check-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
  font-size: 0.85rem;
  cursor: pointer;
}

.preset-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.btn-sm {
  padding: 6px 12px;
  font-size: 0.78rem;
}

/* ── Form controls ── */
.text-input {
  display: block;
  width: 100%;
  height: 36px; /* demo .bs-input 的 2.5rem × 14.4（转写规则，非 ×16） */
  padding: 0 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: 0.9rem;
  line-height: 1.45;
  color: var(--text);
  background: var(--surface);
}

.text-input:hover {
  border-color: var(--border-strong);
}

.text-input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-light);
}

.text-input::placeholder {
  color: var(--muted);
  opacity: 0.8;
}

.text-input:disabled {
  opacity: 0.65;
  cursor: not-allowed;
  background: var(--surface-2);
}

textarea.text-input {
  height: auto; /* 覆盖 .text-input 的 36px 行高定高 */
  min-height: 96px;
  padding: 10px 12px;
  resize: vertical;
}

input[type='number'].text-input {
  font-family: var(--mono);
  font-size: 0.88rem;
}

/* 自定义 listbox（批次 4，替换原生 <select>）—— trigger 沿用 .text-input 的框体样式 + 下拉箭头；
   下拉菜单靠发丝线分层、无投影；选中项走 §2.2 A（淡蓝底 + 蓝字），键盘/hover 焦点走 C 类灰。 */
.listbox {
  position: relative;
}

.listbox-trigger {
  display: flex;
  align-items: center;
  width: 100%;
  height: 36px;
  padding: 0 36px 0 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: 0.9rem;
  line-height: 1.45;
  color: var(--text);
  text-align: left;
  background-color: var(--surface);
  cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M2.5 4.5L6 8l3.5-3.5' stroke='%23666666' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 12px;
}

.listbox-trigger:hover {
  border-color: var(--border-strong);
}

.listbox-trigger:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-light);
}

.listbox-trigger:disabled {
  opacity: 0.65;
  cursor: not-allowed;
  background-color: var(--surface-2);
}

.listbox-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 菜单浮层。用 Popover API 提到 top layer（见 listbox.js），故不被任何祖先的
   overflow/z-index/transform 裁切或遮挡；位置与上/下翻转由 JS 按视口空间计算。
   position:fixed 同时兜住不支持 popover 的旧浏览器（仍逃出 overflow 裁切）。 */
.listbox-menu {
  position: fixed;
  z-index: 1000;
  max-height: 260px;
  margin: 0;
  padding: 4px;
  overflow-y: auto;
  list-style: none;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.16);
}

/* popover 未显示时 UA 会 display:none；[hidden] 兜住无 popover 支持的回退路径。 */
.listbox-menu[hidden] {
  display: none;
}

.listbox-option {
  padding: 7px 10px;
  border-radius: var(--radius-sm);
  font-size: 0.9rem;
  color: var(--text);
  cursor: pointer;
}

.listbox-option.is-active {
  background: var(--surface-2);
}

.listbox-option[aria-selected='true'] {
  background: var(--accent-light);
  color: var(--text);
  font-weight: 500;
}

.color-input {
  display: block;
  width: 100%;
  height: 36px;
  padding: 4px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  cursor: pointer;
}

.color-input:hover {
  border-color: var(--border-strong);
}

.color-input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-light);
}

.form-row {
  display: grid;
  gap: 14px;
}

.form-row-2 {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.file-picker-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  min-height: 44px; /* 同 .btn 的胶囊触控下限（阶段 3） */
  padding: 10px 12px;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--muted);
  font-size: 0.86rem;
  font-weight: 500;
  cursor: pointer;
}

.file-picker-btn:hover {
  border-color: var(--accent);
  color: var(--accent);
  background: var(--accent-light);
}

.field-meta {
  margin: 8px 0 0;
  padding: 8px 10px;
  font-size: 0.78rem;
  line-height: 1.4;
  color: var(--muted);
  background: var(--surface);
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  word-break: break-all;
}

.field-meta:empty {
  display: none;
}

.control-group input[type='checkbox'] {
  width: 16px;
  height: 16px;
  margin: 0;
  accent-color: var(--accent);
  flex-shrink: 0;
}

@media (max-width: 900px) {
  .form-row-2 {
    grid-template-columns: 1fr;
  }
}

.quality-row {
  margin-top: 12px;
}

.quality-row label {
  display: block;
  font-size: 0.78rem;
  font-weight: 500;
  color: var(--muted);
  margin-bottom: 6px;
}

.quality-row input[type='range'] {
  width: 100%;
}

.btn-block {
  width: 100%;
  margin-top: 8px;
}

@media (max-width: 900px) {
  .resize-panel.editor-panel {
    grid-template-columns: 1fr;
  }

  .resize-panel .editor-main {
    border-right: none;
    border-bottom: 1px solid var(--border);
  }

  .compress-panel.editor-panel {
    grid-template-columns: 1fr;
  }

  .compress-panel .editor-main {
    border-right: none;
    border-bottom: 1px solid var(--border);
  }
}

/* ── Compress tool ── */
.compare-container {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  padding: 12px;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: repeating-conic-gradient(var(--border) 0% 25%, var(--surface-2) 0% 50%) 50% / 16px 16px;
}

.compare-stage {
  position: relative;
  display: inline-block;
  width: fit-content;
  max-width: 100%;
  line-height: 0;
  touch-action: none;
  cursor: ew-resize;
  user-select: none;
  overflow: hidden;
  -webkit-tap-highlight-color: transparent;
}

.compare-stage.is-comparing {
  cursor: ew-resize;
}

.compare-canvas {
  display: block;
  vertical-align: top;
}

.compare-before {
  position: relative;
  z-index: 1;
}

.compare-after {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 2;
  pointer-events: none;
}

.compare-divider {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 1px;
  background: rgba(255, 255, 255, 0.92);
  z-index: 3;
  box-shadow: none;
  pointer-events: none;
}

.compare-stage.is-comparing .compare-divider {
  width: 1px;
  box-shadow: none;
}

.compare-handle {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 4;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.96);
  /* 手柄和标签压在用户照片上、底色写死白 —— 字色与描边也要写死（发版流程 §2.6 第 3 条）。
     原来用 var(--accent) / var(--text)，暗色模式下两者翻成近白，白底白字，手柄箭头和标签字都看不见
    （2026-09-13 sharpen-image 截图发现，compress / compress-to-size 同病） */
  border: 1px solid #111111;
  color: #111111;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 2px;
  /* 原有 0 1px 5px rgba(0,0,0,.16)：字面值，阶段 6 的 --shadow:none 够不着。
     手柄浮在照片上要读得出，但它已有一圈纯黑发丝线描边 —— 靠线不靠高度。 */
  pointer-events: none;
}

.compare-handle-chevron {
  display: block;
  flex-shrink: 0;
  width: 4px;
  height: 7px;
}

.compare-tag {
  position: absolute;
  top: 8px;
  z-index: 5;
  font-family: var(--sans);
  font-size: 0.6rem;
  font-weight: 500;
  letter-spacing: 0.02em;
  text-transform: none;
  padding: 2px 7px;
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, 0.9);
  color: #111111;
  /* 标签浮在任意照片上：原靠 #ccc 边 + 0 1px 3px 投影（字面值，token 够不着）。
     投影去掉，边收到纯黑 —— #ccc 压在深色照片上本就读不出，线比影管用。 */
  border: 1px solid #111111;
  pointer-events: none;
  line-height: 1.35;
}

.compare-tag-before {
  left: 8px;
}

.compare-tag-after {
  right: 8px;
}

.compare-hint {
  margin: 10px 0 0;
  font-size: 0.8rem;
  color: var(--muted);
  text-align: center;
}

.size-stats {
  margin: 0;
  display: grid;
  gap: 10px;
}

.size-stats > div {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.size-stats dt {
  margin: 0;
  font-size: 0.78rem;
  font-weight: 500;
  color: var(--muted);
}

.size-stats dd {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.88rem;
  font-weight: 600;
}

.saved-pct.is-good {
  color: var(--success);
}

.quality-readout {
  margin: 8px 0 0;
  text-align: center;
  font-family: var(--mono);
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--accent);
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ── Convert tool ── */
.convert-preview-wrap,
.crop-preview-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 16px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  min-height: 200px;
  width: 100%;
  overflow: hidden;
}

.convert-preview-canvas {
  display: block;
  max-width: 100%;
  height: auto;
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  pointer-events: none;
}

.crop-preview-canvas {
  display: block;
  vertical-align: top;
  pointer-events: none;
}

.convert-preview-meta,
.crop-preview-meta {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--muted);
}

/* 社媒尺寸表里的像素值 —— 等宽 + 荧光黄标记，同工具面板的信号色 */
.crop-social-size {
  margin: 2px 0 4px;
  font-family: var(--mono);
  font-size: 0.82rem;
  color: var(--text);
}

.convert-controls .field,
.crop-controls .field {
  margin-bottom: 10px;
}

@media (max-width: 900px) {
  .convert-panel.editor-panel,
  .crop-panel.editor-panel {
    grid-template-columns: 1fr;
  }

  .convert-panel .editor-main,
  .crop-panel .editor-main {
    border-right: none;
    border-bottom: 1px solid var(--border);
  }
}

/* ── Crop tool ── */
.crop-stage {
  position: relative;
  display: block;
  line-height: 0;
  touch-action: none;
  user-select: none;
  overflow: hidden;
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.crop-shade {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  pointer-events: none;
  z-index: 1;
}

.crop-box {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 2;
  border: 2px solid rgba(255, 255, 255, 0.95);
  box-shadow: none;
  cursor: move;
  touch-action: none;
}

/* ⚠ 白底 + 黑边**两个都写死**，不走 token：手柄压在用户自己的图片上，与站点主题无关。
   跟着 --accent 走的话，暗色模式下边框会变成近白色，白手柄配白边框在浅色照片上直接消失。 */
.crop-handle {
  position: absolute;
  z-index: 3;
  width: 12px;
  height: 12px;
  background: #fff;
  border: 2px solid #000;
  border-radius: var(--radius);
  box-sizing: border-box;
}

.crop-handle-nw { top: -7px; left: -7px; cursor: nwse-resize; }
.crop-handle-ne { top: -7px; right: -7px; cursor: nesw-resize; }
.crop-handle-sw { bottom: -7px; left: -7px; cursor: nesw-resize; }
.crop-handle-se { bottom: -7px; right: -7px; cursor: nwse-resize; }
.crop-handle-n { top: -7px; left: 50%; margin-left: -6px; cursor: ns-resize; }
.crop-handle-s { bottom: -7px; left: 50%; margin-left: -6px; cursor: ns-resize; }
.crop-handle-w { left: -7px; top: 50%; margin-top: -6px; cursor: ew-resize; }
.crop-handle-e { right: -7px; top: 50%; margin-top: -6px; cursor: ew-resize; }

/* 目标尺寸徽标：叠在截取框内左上角，切换预设时更新。
   同比例的预设（如 1 寸 / 2 寸 都是 5:7）截取框形状不变，靠它给出可见反馈。 */
.crop-size-badge {
  position: absolute;
  top: 6px;
  left: 6px;
  z-index: 4;
  padding: 2px 7px;
  font-family: var(--mono);
  font-size: 0.68rem;
  font-weight: 600;
  line-height: 1.35;
  color: #fff;
  background: rgba(0, 0, 0, 0.72);
  border-radius: 4px;
  white-space: nowrap;
  pointer-events: none;
}

.preset-row .btn.is-active {
  border-color: var(--accent);
  color: var(--accent);
  background: var(--accent-light);
}

/* ── Base64 tool ── */
.base64-panel.editor-panel {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: stretch;
}

.base64-panel .editor-main {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.base64-preview-wrap {
  flex: 1;
  min-height: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

.base64-preview-img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.base64-controls.editor-sidebar {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.base64-output-wrap {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

.base64-output {
  flex: 1;
  width: 100%;
  min-height: 200px;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: var(--mono);
  font-size: 0.72rem;
  line-height: 1.45;
  resize: none;
  background: var(--surface);
  color: var(--text);
}

.base64-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-top: 12px;
}

.base64-actions .btn {
  width: 100%;
  justify-content: center;
  text-align: center;
}

@media (max-width: 900px) {
  .base64-panel.editor-panel {
    grid-template-columns: 1fr;
  }

  .base64-panel .editor-main {
    border-right: none;
    border-bottom: 1px solid var(--border);
  }

  .base64-preview-wrap {
    flex: none;
    min-height: 200px;
    max-height: 320px;
  }
}

.field-label {
  display: block;
  font-size: 0.72rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--label);
  margin-bottom: 8px;
}

.field-hint {
  margin: 0 0 10px;
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--label);
}

/* ── EXIF viewer ── */
.exif-preview-wrap {
  display: flex;
  justify-content: center;
  padding: 16px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.exif-preview-img {
  max-width: 100%;
  max-height: 280px;
  height: auto;
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  object-fit: contain;
}

.exif-table-wrap {
  max-height: 420px;
  overflow: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.exif-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.8rem;
}

.exif-table th,
.exif-table td {
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
  text-align: left;
  vertical-align: top;
}

.exif-table th {
  width: 42%;
  font-weight: 600;
  color: var(--muted);
  font-family: var(--mono);
  font-size: 0.72rem;
}

.exif-table td {
  word-break: break-word;
}

.exif-empty {
  margin: 0 0 10px;
  font-size: 0.85rem;
  color: var(--muted);
}

/* EXIF 的主内容是右栏的元数据表，给它更宽的空间：左侧预览收成定宽，其余全给右栏。
   （默认 .editor-panel 是 1fr 270px，右栏太窄，长字段值挤成多行。） */
.exif-panel.editor-panel {
  grid-template-columns: minmax(260px, 360px) 1fr;
}

/* 左栏收窄后 file-bar 横排会把文件名和按钮挤到一起：改竖排 —— 文件信息在上、
   替换/移除按钮在下。（.file-bar 是全站共用类，此处只作用于 EXIF 的窄左栏。） */
.exif-panel .file-bar {
  flex-direction: column;
  align-items: stretch;
  gap: 10px;
}

.exif-actions {
  display: flex;
  gap: 8px;
  margin-bottom: 14px;
}

.exif-actions .btn {
  flex: 1;
}

@media (max-width: 900px) {
  .exif-panel.editor-panel,
  .watermark-panel.editor-panel {
    grid-template-columns: 1fr;
  }

  .exif-panel .editor-main,
  .watermark-panel .editor-main {
    border-right: none;
    border-bottom: 1px solid var(--border);
  }
}

/* ── Text watermark ── */
.watermark-preview-wrap {
  display: flex;
  justify-content: center;
  padding: 16px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.watermark-preview-canvas {
  display: block;
  max-width: 100%;
  height: auto;
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.position-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px;
  max-width: 132px;
}

.position-btn {
  min-width: 0;
  padding: 8px 0;
  font-size: 0.9rem;
  line-height: 1;
}

.position-grid .btn.is-active {
  border-color: var(--accent);
  color: var(--accent);
  background: var(--accent-light);
}

/* ── Sign PDF ── */
.sign-pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  margin-bottom: 8px;
  font-variant-numeric: tabular-nums;
}

/* 画布与签名框共用这一层的坐标：框按百分比定位，所以它必须紧贴画布大小 */
.sign-stage {
  position: relative;
  display: inline-block;
  max-width: 100%;
  line-height: 0;
  touch-action: none;
  cursor: crosshair;
}

/* 压在用户的 PDF 页面上 —— 颜色写死，不跟主题翻（发版流程 §2.6 第 3 条） */
.sign-box {
  position: absolute;
  outline: 1.5px dashed #2563eb;
  outline-offset: 2px;
  cursor: move;
  touch-action: none;
}

.sign-box:focus-visible {
  outline: 2px solid #2563eb;
}

.sign-box img {
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;
  user-select: none;
}

/* 签名板的底色写死白色：用户是在「纸」上签，暗色模式下黑墨也得看得见 */
.sign-pad {
  display: block;
  width: 100%;
  aspect-ratio: 5 / 2;
  background: #ffffff;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  touch-action: none;
  cursor: crosshair;
}

/* ── Fill PDF form ── */
/* 手机上页面不再往下缩（600px 起），改为在这个框里横向滚 —— 缩到 280px 的表单字只有 4px，没法填 */
.pdf-form-pages {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  overflow-x: auto;
  gap: 16px;
  padding: 16px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* 输入框按百分比叠在页面画布上，所以这一层必须紧贴画布。
   字号用 cqw 跟着页面宽度缩放 —— 手机上页面缩到三百多像素，写死 px 的字会撑出框。
   ⚠ inline-size 容器的宽度不能靠内容撑，所以宽度要写明，画布再铺满它 */
.pdf-form-page {
  position: relative;
  width: clamp(600px, 100%, 820px);
  flex-shrink: 0;
  margin-inline: auto;
  container-type: inline-size;
  line-height: 0;
  box-shadow: var(--shadow);
}

.pdf-form-page canvas {
  display: block;
  width: 100%;
  height: auto;
}

/* 压在用户的 PDF 上 —— 颜色写死，不跟主题翻（发版流程 §2.6 第 3 条）。
   淡蓝底是阅读器里「这里可以填」的通用暗示 */
.pdf-form-field {
  position: absolute;
  /* 原生复选框/下拉在暗色下会变黑，而它们压在白纸上 */
  color-scheme: light;
  box-sizing: border-box;
  margin: 0;
  padding: 0 2px;
  border: 1px solid rgba(37, 99, 235, 0.45);
  border-radius: 2px;
  background: rgba(219, 234, 254, 0.55);
  color: #000000;
  font: inherit;
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  line-height: 1.15;
}

.pdf-form-field:focus {
  outline: 2px solid #2563eb;
  background: rgba(255, 255, 255, 0.9);
}

.pdf-form-field:disabled {
  background: transparent;
  border-color: transparent;
}

textarea.pdf-form-field {
  resize: none;
  padding: 2px;
}

.pdf-form-field-checkbox,
.pdf-form-field-radio {
  appearance: auto;
  accent-color: #1d4ed8;
  cursor: pointer;
}

.pdf-form-empty {
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--text);
}

/* ── Annotate image ── */
.annotate-stage {
  position: relative;
  display: inline-block;
  max-width: 100%;
  line-height: 0;
  cursor: crosshair;
  touch-action: none;
  user-select: none;
}

.annotate-stage[data-tool='text'] {
  cursor: text;
}

.annotate-stage canvas {
  display: block;
  max-width: 100%;
  max-height: 75vh;
  height: auto;
}

/* 压在用户截图上 —— 颜色写死（发版流程 §2.6 第 3 条） */
.annotate-text {
  position: absolute;
  min-width: 8em;
  margin: 0;
  padding: 2px 4px;
  border: 1.5px dashed #2563eb;
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.85);
  font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
  font-weight: 600;
  line-height: 1.25;
  resize: none;
  color-scheme: light;
}

.annotate-tools {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 6px;
}

.annotate-tool {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 8px;
}

.annotate-tool svg {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
}

.annotate-tools .btn.is-active,
.annotate-sizes .btn.is-active {
  border-color: var(--accent);
  color: var(--accent-light-text);
  background: var(--accent-light);
}

.annotate-fill {
  display: flex;
  margin-top: 8px;
}

.annotate-colors {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* 色块本身是标注色，写死；选中态用双圈，亮暗主题下都看得见 */
.annotate-swatch {
  width: 30px;
  height: 30px;
  padding: 0;
  border: 1px solid rgba(0, 0, 0, 0.35);
  /* 内圈一道浅线：黑色色块在暗色侧栏上、白色色块在亮色侧栏上都看得见轮廓 */
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.45);
  border-radius: 50%;
  cursor: pointer;
}

.annotate-swatch.is-active {
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.45), 0 0 0 2px var(--surface), 0 0 0 4px var(--text);
}

.annotate-sizes,
.annotate-history {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.annotate-history {
  margin-bottom: 12px;
}

/* ── Straighten image ── */
.straighten-stage {
  position: relative;
  display: inline-block;
  max-width: 100%;
  line-height: 0;
  cursor: crosshair;
  touch-action: none;
  user-select: none;
}

.straighten-stage canvas {
  display: block;
  max-width: 100%;
  max-height: 70vh;
  height: auto;
}

/* 网格与画线都压在用户照片上 —— 颜色写死（发版流程 §2.6 第 3 条）。
   网格用黑白双线，亮暗照片上都看得见 */
.straighten-grid {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image:
    linear-gradient(to right, rgba(255, 255, 255, 0.55) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(255, 255, 255, 0.55) 1px, transparent 1px),
    linear-gradient(to right, rgba(0, 0, 0, 0.25) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(0, 0, 0, 0.25) 1px, transparent 1px);
  background-size: 12.5% 12.5%, 12.5% 12.5%, 12.5% 12.5%, 12.5% 12.5%;
  background-position: 0 0, 0 0, 1px 1px, 1px 1px;
}

.straighten-line {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  overflow: visible;
}

.straighten-line line {
  stroke: #ffd400;
  stroke-width: 2;
  stroke-dasharray: 6 4;
  filter: drop-shadow(0 0 1px #000000);
}

.straighten-fine {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin: 8px 0;
}

.straighten-check {
  display: flex;
}

/* ── Sharpen image ── */
/* 对比条共用 .compare-*，但那边画布按容器宽算好像素尺寸；这里画布是原图 1:1 的块，
   窄屏上要跟着容器缩，不然被 overflow 裁掉一半 */
.sharpen-workspace .compare-before,
.cvd-workspace .compare-before {
  max-width: 100%;
  height: auto;
}

.sharpen-workspace .compare-after,
.cvd-workspace .compare-after {
  width: 100%;
  height: 100%;
}

/* ── Color blindness simulator / image diff：预览画布最长 1400px，高图别撑出一屏 ── */
.cvd-workspace .compare-before,
.diff-canvas {
  width: auto;
  max-height: 70vh;
}

.diff-canvas {
  display: block;
  max-width: 100%;
  height: auto;
}

.diff-need-b {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 40px 16px;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  color: var(--muted);
  text-align: center;
}

.diff-slot {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
  min-width: 0;
}

.diff-slot-tag {
  flex: none;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--accent-text);
  font-size: 0.75rem;
  font-weight: 700;
  line-height: 24px;
  text-align: center;
}

.diff-slot-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.8rem;
  color: var(--muted);
}

.diff-warning {
  color: var(--text);
}

/* ── Video to JPG（帧卡片共用 .gif-frame-*）── */
.video-jpg-card img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius);
  background: var(--surface);
}

.video-jpg-actions {
  display: flex;
  gap: 6px;
}

.video-jpg-actions .btn:first-child {
  flex: 1;
}

.video-jpg-capture {
  margin-top: 12px;
}

.sharpen-thumb {
  position: relative;
  display: inline-block;
  max-width: 100%;
  line-height: 0;
  cursor: crosshair;
  touch-action: none;
  user-select: none;
}

.sharpen-thumb canvas {
  display: block;
  max-width: 100%;
  height: auto;
  border-radius: 4px;
}

/* 压在用户照片上 —— 颜色写死（发版流程 §2.6 第 3 条） */
.sharpen-lens {
  position: absolute;
  box-sizing: border-box;
  border: 2px solid #ffffff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6), 0 0 0 9999px rgba(0, 0, 0, 0.35);
  pointer-events: none;
}

.sharpen-thumb {
  overflow: hidden;
}

/* ── QR scanner（卡片、按钮行与录屏共用 .rec-card / .rec-actions）── */
.scan-idle,
.scan-live,
.scan-done {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.scan-stage {
  position: relative;
  width: 100%;
  max-width: 560px;
  margin-inline: auto;
  overflow: hidden;
  border-radius: var(--radius);
  background: #000000;
  line-height: 0;
}

.scan-video {
  display: block;
  width: 100%;
  height: auto;
}

/* 取景框：压在摄像头画面上，颜色写死（发版流程 §2.6 第 3 条）。
   外面一圈用超大 box-shadow 压暗，框内保持原亮度 */
.scan-frame {
  position: absolute;
  top: 50%;
  left: 50%;
  /* 按画面高度定尺寸再限宽：横向画面上按宽度算会比画面还高（390px 手机上实测上下被切），
     竖向画面上 max-width 兜住，最多变成竖长方形 */
  height: 62%;
  max-width: 62%;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  border: 3px solid #ffffff;
  border-radius: 12px;
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.35);
  pointer-events: none;
}

.scan-hint {
  text-align: center;
}

.scan-kind {
  margin: 0;
  color: var(--text);
}

.scan-found {
  font-weight: 600;
}

.scan-output {
  width: 100%;
  resize: vertical;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  word-break: break-all;
}

.scan-wifi {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 4px 16px;
  margin: 0;
  color: var(--text);
}

.scan-wifi dt {
  font-weight: 600;
}

.scan-wifi dd {
  margin: 0;
  word-break: break-all;
}

/* ── GIF to MP4 ── */
.g2m-compare {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 16px;
}

.g2m-pane {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0;
  min-width: 0;
}

/* 棋盘格底：透明 GIF 的透明处要看得出来，才知道底色选对没有 */
.g2m-media {
  display: block;
  width: 100%;
  max-height: 420px;
  object-fit: contain;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: repeating-conic-gradient(#e5e5e5 0% 25%, #ffffff 0% 50%) 50% / 16px 16px;
}

/* ── Screen recorder ── */
.rec-card {
  display: flex;
  flex-direction: column;
  gap: 16px;
  max-width: 720px;
  margin-inline: auto;
  padding: 24px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

.rec-setup,
.rec-live,
.rec-done {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* 两个勾选项各占一行 —— .barcode-check-label 本身是行内的，并排会读成一句话 */
.rec-setup .barcode-check-label {
  display: flex;
}

.rec-setup .barcode-check-label + .barcode-check-label {
  margin-top: 8px;
}

.rec-unsupported {
  margin: 0;
  color: var(--text);
}

.rec-start {
  align-self: flex-start;
}

.rec-status {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  color: var(--text);
}

.rec-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--danger);
  animation: rec-blink 1.2s ease-in-out infinite;
}

.rec-live.is-paused .rec-dot {
  animation: none;
  background: var(--text-muted, var(--border));
}

@keyframes rec-blink {
  50% { opacity: 0.25; }
}

@media (prefers-reduced-motion: reduce) {
  .rec-dot { animation: none; }
}

.rec-timer {
  margin: 0;
  font-size: clamp(2.5rem, 8vw, 4rem);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: var(--text);
}

.rec-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.rec-video {
  width: 100%;
  max-height: 60vh;
  background: #000000;
  border-radius: var(--radius);
}

/* ── Redact PDF ── */
/* 页面栈与 fill-pdf-form 同一套：手机上最小 600px、在框里横向滚 */
.redact-pages {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 16px;
  padding: 16px;
  overflow-x: auto;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* 懒渲染没到时靠 aspect-ratio 占位，白底就是纸 */
.redact-sheet {
  position: relative;
  width: clamp(600px, 100%, 820px);
  flex-shrink: 0;
  margin-inline: auto;
  background: #ffffff;
  box-shadow: var(--shadow);
  cursor: crosshair;
  /* 竖划滚动、横拖画框（见 redact.js bindSheet） */
  touch-action: pan-y;
  user-select: none;
}

.redact-sheet canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* 压在用户的 PDF 上 —— 颜色写死（发版流程 §2.6 第 3 条）。
   预览就是纯黑：导出的效果就是这样，半透明的预览会让人以为底下的字会留着 */
.redact-box {
  position: absolute;
  box-sizing: border-box;
  background: #000000;
  outline: 1px solid #ef4444;
  cursor: default;
}

.redact-box.is-draft {
  background: rgba(0, 0, 0, 0.55);
  outline: 1.5px dashed #ef4444;
  pointer-events: none;
}

.redact-box:focus-visible {
  outline: 2px solid #ef4444;
  outline-offset: 2px;
}

.redact-box-del {
  position: absolute;
  top: -10px;
  right: -10px;
  width: 20px;
  height: 20px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: #ef4444;
  color: #ffffff;
  font-size: 14px;
  line-height: 20px;
  cursor: pointer;
}

.sign-pad-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 8px 0;
}

/* 没挂 .color-input，靠这条 ID 规则单独成型 —— 高度跟上统一的 36px 输入框 */
#watermarkColor {
  width: 100%;
  height: 36px;
  padding: 4px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

/* ── Rotate tool ── */
.rotate-preview-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 16px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  min-height: 200px;
}

.rotate-preview-canvas {
  max-width: 100%;
  height: auto;
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.rotate-preview-meta {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--muted);
}

.rotate-action-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

@media (max-width: 900px) {
  .rotate-panel.editor-panel {
    grid-template-columns: 1fr;
  }

  .rotate-panel .editor-main {
    border-right: none;
    border-bottom: 1px solid var(--border);
  }
}

/* ── 404 page ── */
.error-page {
  padding: 24px 0 56px;
}

.error-panel {
  max-width: 560px;
  margin: 0 auto 40px;
  text-align: center;
  padding: 32px 20px;
}

.error-kicker {
  margin: 0 0 8px;
  font-family: var(--mono);
  font-size: 0.85rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent);
}

.error-panel h2 {
  margin: 0 0 12px;
  font-size: clamp(1.75rem, 4vw, 2.25rem);
  line-height: 1.15;
}

.error-desc {
  margin: 0 0 24px;
  color: var(--muted);
  font-size: 1rem;
  line-height: 1.6;
}

.error-tools {
  max-width: 960px;
  margin: 0 auto;
}

@media (prefers-reduced-motion: reduce) {
  .keywords-track,
  .tool-workspace,
  .oit-toast,
  .bs-card {
    animation: none !important;
    transition: none !important;
  }
}

/* padding-top 与 .tool-page-main 同值：这三页补上 .bs-page-hero 外壳后，hero 的
   border-bottom 会与首个内容块直接贴住（about/pricing 是卡片边框，legal 是
   .legal-updated 文字）—— 同 9dfd3c9 给工具页处理的双线问题。 */
/* 正文左边缘与 .bs-page-hero 内层（.wrap）对齐：此前 720px 阅读列宽是钉在 main 自己
   身上的，.wrap 的 `margin:0 auto` 便按 720 居中，正文整体比 hero 标题右缩 152px。
   改法是**让 main 保持一个纯粹的 .wrap**（宽度、左右留白全部继承），把 720 的阅读列宽
   下放给子块 —— 这样左边缘天然等于 .wrap 的左边缘，无需在这里复算留白。
   曾试过 margin-left 复算（max(2rem, (100% - 80rem)/2)）：桌面对齐但窄屏差 9px，因为
   .wrap 在 ≤720px 断点收窄到 2.5rem（见上），复算等于把那套数学抄两份、改一处就漏。
   横向 padding 也归零：.wrap 的左右留白是 width-calc 出来的，再加 20px 会把正文推离 hero。 */
.about-page,
.pricing-page,
.legal-page {
  margin: 0 auto 48px;
  padding: 2rem 0 0;
}

.about-page > *,
.pricing-page > *,
.legal-page > * {
  max-width: 720px;
}

.about-stats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
  margin: 0 0 32px;
  padding: 0;
  list-style: none;
}

.about-stat {
  padding: 16px 12px;
  text-align: center;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

.about-stat strong {
  display: block;
  font-size: 1.5rem;
  color: var(--accent);
  line-height: 1.2;
}

.about-stat span {
  display: block;
  margin-top: 4px;
  font-size: 0.78rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--label);
}

.about-pillars {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 16px;
  margin-bottom: 32px;
}

.about-pillar {
  padding: 20px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

.about-pillar h2 {
  margin: 0 0 8px;
  font-size: 1.05rem;
}

.about-pillar p {
  margin: 0;
  color: var(--label);
  line-height: 1.6;
  font-size: 0.92rem;
}

.about-section,
.pricing-card {
  margin-bottom: 28px;
  padding: 20px 0;
  border-bottom: 1px solid var(--border);
}

.about-section h2,
.pricing-card h2 {
  margin: 0 0 10px;
  font-size: 1.25rem;
}

.about-section p {
  margin: 0;
  color: var(--label);
  line-height: 1.65;
}

/* 同一节里的第二段（About 页的联系邮箱）—— 上面那条 margin:0 是按「每节只有一段」写的。 */
.about-section p + p {
  margin-top: 10px;
}

.about-legal,
.legal-links {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  margin: 24px 0 8px;
}

.about-legal a,
.legal-links a {
  font-weight: 500;
  font-size: 0.9rem;
}

.about-cta {
  margin-top: 32px;
}

.legal-updated {
  margin: 0 0 12px;
  font-size: 0.82rem;
  font-weight: 500;
  color: var(--label);
}

.legal-intro {
  margin: 0 0 28px;
  line-height: 1.65;
  color: var(--text);
}

.legal-section {
  margin-bottom: 28px;
  padding-bottom: 24px;
  border-bottom: 1px solid var(--border);
}

.legal-section h2 {
  margin: 0 0 12px;
  font-size: 1.1rem;
}

.legal-section p {
  margin: 0 0 12px;
  line-height: 1.65;
  color: var(--label);
}

.legal-section ul {
  margin: 0 0 12px;
  padding-left: 1.25rem;
  color: var(--label);
  line-height: 1.65;
}

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

/* 阅读列宽与左对齐都交给上面的 .pricing-page > * 统一管：原先这里的 max-width:960px
   其实够不着 —— 父级 .pricing-page 当时钉死 720px，960 永远轮不到；父级放开成 .wrap 后
   它才生效，把卡片撑到 960 并重新居中，正是要消掉的错位。margin 的 auto 同理去掉。 */
.pricing-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 20px;
  margin: 0 0 48px;
}

.pricing-card {
  padding: 24px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel, #fff);
}

.pricing-card.is-primary {
  border-color: var(--accent);
}

.pricing-price {
  margin: 0 0 16px;
  font-size: 1.1rem;
}

.pricing-card ul {
  margin: 0 0 20px;
  padding-left: 1.2rem;
  color: var(--muted);
  line-height: 1.6;
}

/* ── Pixel grid guide ── */
.grid-guide-samples {
  margin-top: 20px;
  padding-top: 18px;
  border-top: 1px solid var(--border);
}

.grid-guide-samples-title {
  margin: 0 0 10px;
  font-size: 0.78rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}

.grid-guide-samples-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
}

.grid-guide-sample {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 6px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
  font-family: inherit;
  font-size: 0.72rem;
  font-weight: 500;
  color: var(--muted);
}

.grid-guide-sample img {
  width: 72px;
  height: 72px;
  object-fit: cover;
  border-radius: var(--radius);
}

/* hover 与选中拆开：blkswn 靠底色区分，不靠抬升（原 translateY(-1px) 是高度语汇，
   与阶段 6 去投影同因去掉；box-shadow: var(--shadow) 自阶段 6 起已是 none，属死声明）。 */
.grid-guide-sample:hover {
  border-color: var(--border-strong);
  background: var(--surface-2);
  color: var(--text);
}

.grid-guide-sample.is-active {
  border-color: var(--accent);
  background: var(--accent-light);
  color: var(--text);
}

.grid-guide-panel.editor-panel {
  grid-template-columns: minmax(0, 1fr) 260px;
}

.grid-guide-previews {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
}

.grid-guide-previews figure {
  margin: 0;
}

.grid-guide-previews figcaption {
  margin-bottom: 8px;
  font-size: 0.72rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}

.grid-guide-canvas-wrap {
  display: flex;
  justify-content: center;
  padding: 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: auto;
}

.grid-guide-canvas-wrap canvas {
  image-rendering: pixelated;
  max-width: 100%;
  height: auto;
}

.grid-guide-canvas-wrap--scroll {
  max-height: min(70vh, 520px);
}

.grid-guide-meta {
  margin: 12px 0 0;
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--muted);
}

.grid-guide-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.grid-guide-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 28px;
  height: 28px;
  padding: 0 6px;
  border: 1px solid var(--border); /* 原 rgba(0,0,0,.15)：发丝线却没走 --border,偏离单色阶 */
  border-radius: var(--radius-pill);
  font-family: var(--mono);
  font-size: 0.68rem;
  font-weight: 700;
}

.chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.chip {
  display: inline-flex;
  align-items: center;
  padding: 7px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
  font-size: 0.82rem;
  font-weight: 500;
  color: var(--muted);
  cursor: pointer; /* demo 的 .bs-choice 无过渡 —— 同阶段 3 的胶囊按钮,黑白瞬时切换 */
}

.chip-radio input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

/* 选中 = 荧光黄（demo .bs-choice.is-selected）。原为黑底白字 —— 那是**按钮**的反转语汇，
   不是「选中」。同类单选组（.segmented / .preset-row / .position-grid / .passport-preset）
   全站都已是荧光黄，这里是漏网的一个。 */
.chip-radio:has(input:checked) {
  border-color: var(--accent);
  background: var(--accent-light);
  color: var(--text);
}

.chip-radio:hover {
  border-color: var(--accent);
  color: var(--text);
}

@media (max-width: 900px) {
  .grid-guide-panel.editor-panel {
    grid-template-columns: 1fr;
  }

  .grid-guide-panel .editor-main {
    border-right: none;
    border-bottom: 1px solid var(--border);
  }

  .grid-guide-previews {
    grid-template-columns: 1fr;
  }
}

/* ── Content tables ── */
.table-scroll {
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.table-scroll table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
}

.table-scroll th,
.table-scroll td {
  padding: 12px 16px;
  text-align: left;
  border-bottom: 1px solid var(--border);
}

.table-scroll th {
  background: var(--surface-2);
  color: var(--text);
  font-size: 0.78rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.table-scroll tr:last-child td {
  border-bottom: none;
}

.table-note {
  margin-top: 10px;
  font-size: 0.85rem;
  color: var(--muted);
  line-height: 1.55;
}

/* ── Passport photo ── */
.passport-preset-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.passport-preset {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  text-align: left;
  padding: 10px 12px;
}

.passport-preset-label {
  font-weight: 600;
}

/* 原为 color-mix(--accent 12%, --surface)：--accent 从蓝变黑后这个淡染变成了灰，
   选中态读起来像禁用态。收进全站统一的荧光黄选中语汇（同 .preset-row .btn.is-active）。 */
.passport-preset.is-active {
  border-color: var(--accent);
  background: var(--accent-light);
  color: var(--text);
}

.passport-preset-sub {
  font-size: 0.72rem;
  font-family: var(--mono);
  color: var(--muted);
  font-weight: 500;
  white-space: nowrap;
}

.passport-photo-controls input[type='color'] {
  width: 100%;
  height: 40px;
  padding: 4px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
}

.passport-photo-controls .check-row {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 10px 0;
  font-size: 0.82rem;
  color: var(--muted);
  cursor: pointer;
}

.passport-photo-controls .check-row input {
  margin-top: 3px;
  accent-color: var(--accent);
}

.passport-output-preview {
  display: flex;
  justify-content: center;
  padding: 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.passport-output-preview canvas {
  max-width: 100%;
  height: auto;
  border: 1px solid var(--border);
}

.passport-output-meta {
  margin: 8px 0 0;
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--muted);
  text-align: center;
}

/* ── Batch compress ── */
.batch-compress-panel.editor-panel {
  grid-template-columns: minmax(0, 1fr) 260px;
}

.batch-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 16px;
  flex-wrap: wrap;
}

.batch-summary {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--muted);
}

.batch-toolbar-actions {
  display: flex;
  gap: 8px;
}

.batch-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.batch-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.batch-row-main {
  min-width: 0;
}

.batch-row-name {
  display: block;
  font-weight: 500;
  font-size: 0.85rem;
  word-break: break-all;
}

.batch-row-meta {
  display: block;
  margin-top: 2px;
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--muted);
}

.batch-row-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}

.batch-row-status {
  font-size: 0.72rem;
  font-weight: 500;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.batch-row--processing .batch-row-status {
  color: var(--accent);
}

.batch-row--done .batch-row-status {
  color: var(--success);
}

.batch-row--error .batch-row-status {
  color: var(--danger, #c0392b);
}

.pro-gate {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: rgba(0, 0, 0, 0.45);
}

.pro-gate[hidden] {
  display: none;
}

.pro-gate-card {
  width: min(420px, 100%);
  padding: 24px;
  border-radius: var(--radius);
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
}

.pro-gate-card h3 {
  margin: 0 0 10px;
  font-size: 1.15rem;
}

.pro-gate-card p {
  margin: 0 0 16px;
  color: var(--muted);
  line-height: 1.55;
}

.pro-gate-actions {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

@media (max-width: 900px) {
  .batch-compress-panel.editor-panel {
    grid-template-columns: 1fr;
  }

  .batch-compress-panel .editor-main {
    border-right: none;
    border-bottom: 1px solid var(--border);
  }
}

/* ── GIF to frames ── */
.gif-frames-panel.editor-panel {
  grid-template-columns: minmax(0, 1fr) 220px;
}

.gif-frame-summary {
  margin: 0 0 14px;
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--muted);
}

.gif-frame-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 12px;
  max-height: min(70vh, 640px);
  overflow: auto;
  padding: 2px;
}

.gif-frame-card {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.gif-frame-card canvas {
  width: 100%;
  height: auto;
  image-rendering: pixelated;
  border-radius: var(--radius);
  background: var(--surface);
}

.gif-frame-meta {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--muted);
}

@media (max-width: 900px) {
  .gif-frames-panel.editor-panel {
    grid-template-columns: 1fr;
  }

  .gif-frames-panel .editor-main {
    border-right: none;
    border-bottom: 1px solid var(--border);
  }
}

/* ── Images to GIF ── */
.images-to-gif-panel.editor-panel {
  grid-template-columns: minmax(0, 1fr) 240px;
}

.gif-frame-list .gif-frame-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
}

.gif-frame-thumb {
  width: 48px;
  height: 48px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface-2);
}

.gif-preview-wrap {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 180px;
  padding: 16px;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.gif-preview-empty {
  margin: 0;
  text-align: center;
  color: var(--muted);
  font-size: 0.92rem;
}

.gif-preview-image {
  max-width: 100%;
  max-height: 320px;
  height: auto;
  border-radius: var(--radius-sm);
}

@media (max-width: 900px) {
  .images-to-gif-panel.editor-panel {
    grid-template-columns: 1fr;
  }

  .images-to-gif-panel .editor-main {
    border-right: none;
    border-bottom: 1px solid var(--border);
  }
}

/* ── QR Decoder ── */
.qr-decode-status {
  margin: 0 0 12px;
  font-family: var(--mono);
  font-size: 0.82rem;
}

.qr-decode-status--ok {
  color: var(--success);
}

.qr-decode-status--miss {
  color: var(--danger);
}

/* ── QR Cleaner ── */
.qr-clean-panel.editor-panel {
  grid-template-columns: minmax(0, 1fr) 240px;
}

.qr-compare-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
}

.qr-compare-col .base64-preview-wrap,
.qr-clean-preview-wrap {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 200px;
  padding: 12px;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.qr-clean-canvas {
  max-width: 100%;
  height: auto;
  image-rendering: pixelated;
}

.qr-payload-text {
  margin: 6px 0 0;
  font-family: var(--mono);
  font-size: 0.82rem;
  line-height: 1.45;
  word-break: break-all;
  color: var(--text);
}

@media (max-width: 900px) {
  .qr-clean-panel.editor-panel {
    grid-template-columns: 1fr;
  }

  .qr-clean-panel .editor-main {
    border-right: none;
    border-bottom: 1px solid var(--border);
  }

  .qr-compare-grid {
    grid-template-columns: 1fr;
  }
}

/* ── Barcode Generator ── */
.barcode-generator-panel.editor-panel {
  grid-template-columns: minmax(0, 1fr) 260px;
}

.barcode-preview-wrap {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 220px;
  padding: 20px;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.barcode-preview-empty {
  margin: 0;
  text-align: center;
  color: var(--muted);
  font-size: 0.92rem;
}

.barcode-preview-canvas {
  max-width: 100%;
  height: auto;
}

.barcode-check-label {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  font-weight: 500;
}

@media (max-width: 900px) {
  .barcode-generator-panel.editor-panel {
    grid-template-columns: 1fr;
  }

  .barcode-generator-panel .editor-main {
    border-right: none;
    border-bottom: 1px solid var(--border);
  }
}

/* ── Image Adjust ── */
.image-adjust-panel.editor-panel {
  grid-template-columns: minmax(0, 1fr) 260px;
}

.adjust-preview-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 12px;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.adjust-preview-canvas {
  max-width: 100%;
  height: auto;
}

.adjust-preview-meta {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.82rem;
  color: var(--muted);
  text-align: center;
}

@media (max-width: 900px) {
  .image-adjust-panel.editor-panel {
    grid-template-columns: 1fr;
  }

  .image-adjust-panel .editor-main {
    border-right: none;
    border-bottom: 1px solid var(--border);
  }
}

/* ── Photo Filters ── */
.photo-filters-panel.editor-panel {
  grid-template-columns: minmax(0, 1fr) 260px;
}

.filter-preset-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

/* 选中 = 荧光黄，同全站其余单选组（.segmented / .preset-row / .position-grid /
   .passport-preset / .chip-radio）。这块落在文档给阶段 7 划的行号区间(2732–4398)之外，
   而它其实是工具块 —— 边界按行号切，把 4452–4626 这几个工具漏在了两阶段之间。 */
.filter-preset-btn.is-active {
  border-color: var(--accent);
  background: var(--accent-light);
  color: var(--text);
}

.placeholder-preview-wrap {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  overflow: auto;
  max-height: 520px;
}

/* 原有 0 2px 12px rgb(0 0 0 / 8%)：字面投影，阶段 6 的 --shadow:none 是 token,够不着。
   blkswn 靠发丝线分层不靠高度 —— 外面的 .placeholder-preview-wrap 已有一圈虚线边。 */
.placeholder-preview-canvas {
  max-width: 100%;
  height: auto;
}

.placeholder-image-panel.editor-panel,
.photo-filters-panel.editor-panel {
  grid-template-columns: minmax(0, 1fr) 260px;
}

@media (max-width: 900px) {
  .photo-filters-panel.editor-panel,
  .placeholder-image-panel.editor-panel {
    grid-template-columns: 1fr;
  }

  .photo-filters-panel .editor-main,
  .placeholder-image-panel .editor-main {
    border-right: none;
    border-bottom: 1px solid var(--border);
  }
}

/* ── Pixelate / long split / vCard ── */
.pixelate-canvas {
  cursor: none;
  touch-action: none;
  max-width: 100%;
  height: auto;
}

/* 圆形笔刷光标：随粉笔刷大小缩放，位置由 JS 更新 */
.image-pixelate-workspace .adjust-preview-wrap {
  position: relative;
}

.pixelate-cursor {
  position: absolute;
  top: 0;
  left: 0;
  border: 2px solid var(--accent-light);
  border-radius: 50%;
  background: rgb(253 244 151 / 12%);
  box-shadow: 0 0 4px rgb(0 0 0 / 35%);
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: 2;
}

.long-split-original {
  max-height: 280px;
  overflow: auto;
  margin-bottom: 16px;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.long-split-original-img {
  display: block;
  width: 100%;
  height: auto;
}

.vcard-payload {
  margin: 0;
  padding: 12px;
  font-family: var(--mono);
  font-size: 0.75rem;
  line-height: 1.45;
  white-space: pre-wrap;
  word-break: break-word;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  max-height: 220px;
  overflow: auto;
}

.perspective-stage {
  position: relative;
  display: inline-block;
  line-height: 0;
  touch-action: none;
  user-select: none;
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: visible;
}

.perspective-source-canvas,
.perspective-result-canvas {
  display: block;
  max-width: 100%;
  height: auto;
}

.perspective-handle {
  position: absolute;
  z-index: 3;
  width: 14px;
  height: 14px;
  margin: -7px 0 0 -7px;
  padding: 0;
  /* 同 .crop-handle：压在用户图片上，白底黑边写死，不跟主题。 */
  background: #fff;
  border: 2px solid #000;
  border-radius: 50%;
  box-sizing: border-box;
  cursor: grab;
  touch-action: none;
}

.perspective-handle:active {
  cursor: grabbing;
}

.perspective-result-wrap {
  margin-top: 8px;
  padding: 12px;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

/* ── Auth pages ── */
.auth-page {
  max-width: 440px;
  margin: 0 auto;
  padding: 32px 0 56px;
}

.auth-card {
  padding: 32px 28px;
  border: 1px solid var(--border);
  border-radius: 0;
  background: var(--surface);
  box-shadow: var(--shadow);
}

.auth-card-header {
  margin-bottom: 24px;
}

.auth-card-header h1 {
  margin: 16px 0 0;
  font-family: var(--display);
  text-transform: uppercase;
  letter-spacing: 0.04rem;
  font-size: 1.5rem;
  line-height: 1.2;
  color: var(--text);
}

.auth-brand .brand-mark {
  border-radius: 0;
  background: var(--accent);
  box-shadow: var(--shadow);
}

/* 这里原有三条覆盖，全部删掉 —— 它们把 auth 页锁在了改造之前：
   1) `.btn { border-radius: 0 }` 把阶段 3 的胶囊显式压回方角。阶段 3 把
      `border-radius:0` 一律归为「与 --radius:0 一致的无害冗余」，但压在 .btn 上
      一点也不无害：胶囊从没到达过登录/注册页。
   2) `.btn-primary { background: var(--accent) }` 是重复声明（.btn-primary 本就是 accent）。
   3) `.btn-primary:hover { background: var(--accent-hover) }` 把 hover **变深**
      —— 那是旧语汇；阶段 3 定的是黑白**瞬时反转**（demo 的 .bs-btn:hover）。
   删掉后三者都回落到全局 .btn 规则。 */

/* 同 .upgrade-card .btn-block：block 会丢掉 .btn 的 flex 垂直居中。
   本页多数 btn-block 是 <button>（自带标签居中，看不出问题），但开了 Google OAuth 后
   那颗「Continue with Google」是 <a>，就会文字贴顶。 */
.auth-page .btn-block {
  display: flex;
  width: 100%;
}

/* 「Continue with Google」：四色 G 在文案前。整体仍居中，图标与文字一起当一个块。 */
.btn-oauth {
  gap: 10px;
}

.btn-oauth .google-logo {
  flex: none;
}

.auth-form {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.auth-field label {
  display: block;
  margin-bottom: 6px;
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--label);
}

.auth-field input[type='email'],
.auth-field input[type='password'],
.auth-field input[type='text'] {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 0;
  background: var(--bg);
  font: inherit;
  color: var(--text);
}

.auth-field input:focus {
  outline: 2px solid var(--accent-light);
  border-color: var(--accent);
}

.auth-checkbox {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.88rem;
  color: var(--label);
  cursor: pointer;
}

.auth-errors {
  margin: 0 0 16px;
  padding: 12px 14px;
  list-style: none;
  border: 1px solid #fecaca;
  border-radius: 0;
  background: #fef2f2;
  color: var(--danger);
  font-size: 0.88rem;
}

.auth-errors li + li {
  margin-top: 4px;
}

.auth-oauth {
  margin-bottom: 4px;
}

.auth-divider {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 20px 0;
  color: var(--muted);
  font-size: 0.82rem;
  text-transform: lowercase;
}

.auth-divider::before,
.auth-divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--border);
}

.auth-footer {
  margin: 24px 0 0;
  text-align: center;
  font-size: 0.9rem;
  color: var(--muted);
}

.auth-footer a {
  color: var(--accent);
  font-weight: 500;
  text-decoration: none;
}

.auth-footer a:hover {
  color: var(--accent-hover);
  text-decoration: underline;
}

.auth-lead {
  margin: 0 0 16px;
  color: var(--muted);
  font-size: 0.9rem;
  text-align: center;
}

.auth-code-input {
  text-align: center;
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  font-size: 1.1rem;
  letter-spacing: 0.25em;
}

.auth-page .captcha-widget {
  margin-top: 4px;
}

/* 验证码只预留空间，不放「加载中」占位条 —— 两家 widget 自带加载态，外面再套一层就是两个
   spinner 叠着；而且占位一旦要「撤」就得有就绪判据，Turnstile 的 iframe 在**闭合 shadow root**
   里，`:has(iframe)` 穿不透，占位会永远撤不掉（旧实现的真实故障，2026-08-03 拆掉）。
   ⚠ **别再加回 `.captcha-widget-loading` 那类需要撤的元素。**
   min-height 用官方尺寸：Turnstile normal 300×65、reCAPTCHA v2 304×78 ——
   目的是 widget 落地时不顶动下面的提交按钮。 */
.captcha-widget {
  min-height: 65px;
}

.captcha-widget[data-captcha-provider='recaptcha'] {
  min-height: 78px;
}

/* 未拿到验证码 token 前提交按钮被 x-captcha 的脚本禁掉：必须看得出来是禁用态，
   否则按钮看着能点却没反应，比原来直接报错更糟。 */
.auth-form .btn:disabled {
  opacity: 0.65;
  cursor: not-allowed;
}

@media (max-width: 480px) {
  .auth-card {
    padding: 24px 20px;
  }
}

/* ── Account dashboard ── */
.account-shell {
  display: grid;
  grid-template-columns: 240px 1fr;
  gap: 28px;
  padding: 32px 0 56px;
  align-items: start;
}

.account-sidebar {
  position: sticky;
  top: 24px;
  border: 1px solid var(--border);
  border-radius: 0;
  background: var(--surface);
  box-shadow: var(--shadow);
  overflow: hidden;
}

.account-sidebar-header {
  padding: 20px 18px 16px;
  border-bottom: 1px solid var(--border);
}

.account-sidebar-header h2 {
  margin: 0;
  font-family: var(--display);
  text-transform: uppercase;
  letter-spacing: 0.04rem;
  font-size: 1rem;
}

.account-sidebar-header p {
  margin: 4px 0 0;
  font-size: 0.78rem;
  color: var(--muted);
  word-break: break-word;
}

.account-nav {
  padding: 8px;
}

.account-nav a {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: 0;
  font-size: 0.88rem;
  font-weight: 500;
  color: var(--label);
  text-decoration: none;
  border: 1px solid transparent;
}

.account-nav a:hover {
  background: var(--surface-2);
  color: var(--text);
}

.account-nav a.is-active {
  background: var(--accent-light);
  border-color: var(--border-strong);
  color: var(--accent);
}

.account-nav svg {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  opacity: 0.75;
}

.account-nav a.is-active svg {
  opacity: 1;
}

.upgrade-card {
  margin: 8px;
  padding: 14px;
  border: 1px solid var(--border);
  border-radius: 0;
  /* 原为 linear-gradient(135deg, --accent-light, --bg)。blkswn 是平的 —— 整个设计稿
     没有一处渐变；且荧光黄在这里是「标记/选中」信号（同侧栏 .account-nav a.is-active
     就是黄的，两块黄会打架）。收成次级底色，靠发丝线分层。 */
  background: var(--surface-2);
}

.upgrade-card p {
  margin: 0 0 10px;
  font-size: 0.82rem;
  color: var(--label);
  line-height: 1.45;
}

.upgrade-card--pro p {
  margin: 0;
}

/* 用 flex 而非 block 撑满：.btn 是 inline-flex + align-items:center，靠它把文字在
   固定的 44px 高里垂直居中。改成 display:block 会把这层居中一并丢掉 —— 文字贴着
   顶边（实测上方 2px、下方 26px），text-align 只管得了水平方向。 */
.upgrade-card .btn-block {
  display: flex;
  width: 100%;
}

.account-main {
  min-width: 0;
}

.page-header {
  margin-bottom: 24px;
}

.page-header h1 {
  margin: 0 0 6px;
  font-family: var(--display);
  text-transform: uppercase;
  letter-spacing: 0.04rem;
  font-size: 1.75rem;
  line-height: 1.2;
}

.page-header p {
  margin: 0;
  color: var(--muted);
  font-size: 0.95rem;
}

.stats-row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 16px;
  margin-bottom: 20px;
}

.stat-card {
  padding: 18px 20px;
  border: 1px solid var(--border);
  border-radius: 0;
  background: var(--surface);
  box-shadow: var(--shadow);
}

.stat-card dt {
  margin: 0 0 6px;
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

.stat-card dd {
  margin: 0;
  font-family: var(--display);
  font-size: 1.35rem;
  font-weight: 500;
  line-height: 1.2;
}

.panel {
  border: 1px solid var(--border);
  border-radius: 0;
  background: var(--surface);
  box-shadow: var(--shadow);
  padding: 24px;
}

.panel + .panel {
  margin-top: 20px;
}

.panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 16px;
}

.panel-header h2 {
  margin: 0;
  font-family: var(--display);
  text-transform: uppercase;
  letter-spacing: 0.04rem;
  font-size: 1.1rem;
}

.panel-desc {
  margin: 0 0 16px;
  color: var(--muted);
  font-size: 0.92rem;
}

.badge {
  display: inline-flex;
  align-items: center;
  padding: 4px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.badge--pro {
  border-color: var(--accent);
  background: var(--accent-light);
  color: var(--accent);
}

.badge--free {
  background: var(--surface-2);
  color: var(--muted);
}

.badge--linked {
  border-color: #86efac;
  background: #f0fdf4;
  color: #166534;
}

.badge--soon {
  background: var(--surface-2);
  color: var(--muted);
}

.details {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px 24px;
}

.detail dt {
  margin: 0 0 4px;
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

.detail dd {
  margin: 0;
  font-size: 0.98rem;
  word-break: break-word;
}

.quick-links {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 16px;
}

/* 可点即胶囊（阶段 3）。这些不是 .btn，所以胶囊规则从没够到它们 ——
   原是灰底直角方块，在全站胶囊里显得像另一个时代的东西。 */
.quick-links a {
  display: inline-flex;
  align-items: center;
  height: 32px; /* 同 .btn-ghost 的矮身 —— 它们是次要动作 */
  padding: 0 14px;
  font-size: 0.85rem;
  font-weight: 500;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--label);
  text-decoration: none;
}

.quick-links a:hover {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--surface);
  text-decoration: none;
}

.quick-links a:hover {
  border-color: var(--accent);
  background: var(--accent-light);
  color: var(--accent);
  text-decoration: none;
}

.panel-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 16px;
}

.panel-actions .hint {
  flex: 1 1 100%;
  margin: 0;
  color: var(--muted);
  font-size: 0.82rem;
}

.account-status {
  margin: 0 0 16px;
  padding: 10px 14px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--label);
  font-size: 0.88rem;
}

.payment-history-wrap {
  overflow-x: auto;
}

.payment-history {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.88rem;
}

.payment-history th,
.payment-history td {
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
  text-align: left;
}

.payment-history th {
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

.payment-history code {
  font-size: 0.78rem;
}

.placeholder-block {
  padding: 20px;
  border: 1px dashed var(--border);
  border-radius: 0;
  background: var(--surface-2);
  color: var(--muted);
  font-size: 0.88rem;
  text-align: center;
}

.placeholder-block strong {
  display: block;
  margin-bottom: 4px;
  color: var(--label);
}

.oauth-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 16px;
  border: 1px solid var(--border);
  border-radius: 0;
  background: var(--surface-2);
}

.oauth-info {
  display: flex;
  align-items: center;
  gap: 12px;
}

.oauth-icon {
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 0;
}

.oauth-email {
  display: block;
  font-size: 0.82rem;
  color: var(--muted);
}

.oauth-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

.session-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.session-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 0;
  border-bottom: 1px solid var(--border);
}

.session-item:last-child {
  border-bottom: none;
}

.session-item strong {
  display: block;
  font-size: 0.9rem;
}

.session-item span {
  font-size: 0.82rem;
  color: var(--muted);
}

.two-factor-setup {
  display: grid;
  gap: 16px;
  margin-bottom: 16px;
}

.two-factor-qr {
  width: min(200px, 100%);
  padding: 12px;
  border: 1px solid var(--border);
  background: #fff;
}

.two-factor-qr svg {
  display: block;
  width: 100%;
  height: auto;
}

.two-factor-secret {
  margin: 0;
  font-size: 0.85rem;
  color: var(--muted);
}

.two-factor-secret code {
  display: block;
  margin-top: 6px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  font-size: 0.9rem;
  word-break: break-all;
}

.two-factor-actions {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin-top: 4px;
}

.two-factor-actions .hint {
  flex: 1 1 100%;
  margin: 0;
  color: var(--muted);
  font-size: 0.82rem;
}

.two-factor-secondary-action {
  margin: 0 0 8px;
}

.two-factor-disable-panel {
  margin-top: 16px;
}

.two-factor-disable-trigger {
  list-style: none;
  cursor: pointer;
  display: inline-block;
  width: auto;
}

.two-factor-disable-trigger::-webkit-details-marker {
  display: none;
}

.two-factor-disable-panel[open] > .two-factor-disable-trigger {
  display: none;
}

.two-factor-setup-actions {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

.two-factor-disable {
  margin-top: 0;
  padding-top: 0;
  border-top: none;
}

.two-factor-disable-hint {
  margin: 0 0 16px;
  font-size: 0.85rem;
  color: var(--muted);
}

.two-factor-recovery {
  margin: 16px 0;
  padding: 16px;
  border: 1px solid #fde68a;
  background: #fffbeb;
  color: #92400e;
  font-size: 0.88rem;
}

.two-factor-recovery strong {
  display: block;
  margin-bottom: 6px;
  color: #78350f;
}

.recovery-code-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px 16px;
  margin: 12px 0 0;
  padding: 0;
  list-style: none;
}

.recovery-code-list code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  font-size: 0.85rem;
}

.account-status {
  margin: 0 0 20px;
  padding: 12px 14px;
  border: 1px solid #bbf7d0;
  border-radius: 0;
  background: #f0fdf4;
  color: #166534;
  font-size: 0.88rem;
}

.account-status-badge {
  display: inline-flex;
  align-items: center;
  padding: 3px 9px;
  border: 1px solid var(--border);
  border-radius: 0;
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

.account-status-badge--active,
.account-status-badge--trialing {
  border-color: #86efac;
  background: #f0fdf4;
  color: #166534;
}

.account-status-badge--pending {
  border-color: #fde68a;
  background: #fffbeb;
  color: #92400e;
}

.account-status-badge--cancelled,
.account-status-badge--canceled,
.account-status-badge--expired,
.account-status-badge--suspended {
  border-color: var(--border);
  background: var(--surface-2);
  color: var(--muted);
}

.account-status-badge--past_due {
  border-color: #fecaca;
  background: #fef2f2;
  color: #991b1b;
}

/* 同 .auth-page 处的说明：这里原有的 `.btn { border-radius: 0 }` 把胶囊压回方角，
   `.btn-primary` 是重复声明，`.btn-primary:hover` 与 `.btn-secondary(:hover)` 则把
   阶段 3 的黑白瞬时反转改回了「变深/淡染」的旧语汇。全部删掉，回落到全局 .btn。 */

.account-shell .btn-ghost {
  background: transparent;
  color: var(--muted);
  border-color: var(--border);
}

.account-shell .btn-ghost:hover {
  background: var(--surface-2);
  color: var(--text);
}

/* 原为一套 cream 时代的橙（#9a3412/#fdba74/#fff7ed/#c2410c）—— 而全局 .btn-danger
   早已走 var(--danger)。危险按钮是 chrome 不是状态提示，收进 token；
   （阶段 1「保留 21 种状态色」指的是错误/成功/警告**提示块**，不含这里。） */
.account-shell .btn-danger {
  background: transparent;
  color: var(--danger);
  border-color: var(--danger);
}

.account-shell .btn-danger:hover {
  background: var(--danger);
  color: var(--surface);
  border-color: var(--danger);
}

.account-shell .panel-actions .btn:disabled {
  opacity: 0.65;
  cursor: not-allowed;
}

.account-form {
  display: flex;
  flex-direction: column;
  gap: 14px;
  max-width: 420px;
}

.account-field label {
  display: block;
  margin-bottom: 6px;
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--label);
}

.account-field input[type='password'] {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 0;
  background: var(--surface);
  font: inherit;
}

.account-field input:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.field-error,
.account-error {
  color: #9a3412;
  font-size: 0.82rem;
}

/* ☞ 母题：blkswn 用它给错误提示与正文链接 hover 打标记（demo 仅这几处用，不滥用） */
.field-error::before,
.account-error::before {
  content: '☞ ';
  background: var(--accent-light);
}

.bs-section a:not(.bs-card):hover::before,
.legal-section a:hover::before {
  content: '☞ ';
}

.account-error {
  padding: 12px 14px;
  border: 1px solid #fdba74;
  background: #fff7ed;
  margin-bottom: 16px;
}

.inline-form {
  display: inline;
}

.checkout-form {
  margin-top: 8px;
}

.checkout-plans {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 12px;
  margin-bottom: 16px;
}

/* demo 的 .bs-choice：未选中 = 白底 + 1px 发丝线，选中才是荧光黄。
   原为灰底 + 2px 边 —— 灰底让未选中的方案读起来像被禁用，而 2px 粗框
   在全站 1px 发丝线里也是个异类。过渡按阶段 3 去掉（选中是瞬时的）。 */
.checkout-plan {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 18px;
  border: 1px solid var(--border);
  background: var(--surface);
  cursor: pointer;
}

.checkout-plan input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.checkout-plan.is-selected {
  border-color: var(--accent);
  background: var(--accent-light);
}

.checkout-plan-name {
  font-weight: 500;
  font-size: 0.95rem;
}

.checkout-plan-price {
  font-size: 1.35rem;
  font-weight: 500;
}

.checkout-plan-price small {
  display: block;
  font-size: 0.78rem;
  font-weight: 500;
  color: var(--muted);
}

.checkout-plan-badge {
  font-size: 0.72rem;
  font-weight: 500;
  color: var(--accent);
}

.billing-cancel-page {
  padding: 48px 0 80px;
  max-width: 560px;
}

.billing-feedback {
  text-align: center;
  padding: 32px 24px;
}

.billing-feedback-icon {
  width: 56px;
  height: 56px;
  margin: 0 auto 16px;
  display: grid;
  place-items: center;
  font-size: 1.5rem;
  font-weight: 500;
  border: 2px solid var(--border);
}

.billing-feedback--success .billing-feedback-icon {
  color: #166534;
  border-color: #86efac;
  background: #f0fdf4;
}

.billing-feedback--pending .billing-feedback-icon {
  color: #92400e;
  border-color: #fcd34d;
  background: #fffbeb;
}

.billing-feedback--cancel .billing-feedback-icon {
  color: #9a3412;
  border-color: #fdba74;
  background: #fff7ed;
}

.billing-feedback .page-header {
  margin-bottom: 12px;
}

.billing-feedback .panel-actions {
  justify-content: center;
  margin-top: 20px;
}

@media (max-width: 860px) {
  .account-shell {
    grid-template-columns: 1fr;
    gap: 20px;
  }

  .account-sidebar {
    position: static;
  }

  .account-nav {
    display: flex;
    gap: 4px;
    overflow-x: auto;
    padding-bottom: 4px;
  }

  .account-nav a {
    white-space: nowrap;
  }

  .upgrade-card {
    display: none;
  }

  .details {
    grid-template-columns: 1fr;
  }

  .stats-row {
    grid-template-columns: 1fr;
  }
}

.cutout-canvas {
  background:
    linear-gradient(45deg, #ddd 25%, transparent 25%) 0 0 / 16px 16px,
    linear-gradient(-45deg, #ddd 25%, transparent 25%) 0 8px / 16px 16px,
    linear-gradient(45deg, transparent 75%, #ddd 75%) 8px -8px / 16px 16px,
    linear-gradient(-45deg, transparent 75%, #ddd 75%) 8px 0 / 16px 16px,
    #eee;
}

/* ── 工具收藏（docs/新功能设计-工具收藏.md）───────────────────────── */

/* 页头收藏入口。语汇对齐 .header-auth-link（同一条工具栏上的邻居）。 */
.header-fav {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--text);
  font-size: 0.9rem;
  text-decoration: none;
  white-space: nowrap;
}

.header-fav:hover {
  text-decoration: underline;
  text-underline-offset: 4px;
}

.header-fav.is-current {
  font-weight: 500;
}

.header-fav-icon {
  width: 16px;
  height: 16px;
  flex: none;
  fill: none;
}

.header-fav.is-current .header-fav-icon {
  fill: currentColor;
}

/* ── 主题开关（页头工具条）────────────────────────────────────────────
   默认不显示：data-theme 由 AppLayout 的 head 脚本首帧前写上，没 JS 就没有这个
   属性 —— 那种情况下按钮点了也没用，不如不出。lockLight 的两个工具页发的是
   data-theme-lock，同样不出（它们的 blkswn-v3-theme.css 写死亮色，切了是半成品）。 */
.theme-toggle {
  display: none;
}

:root[data-theme]:not([data-theme-lock]) .theme-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0;
  background: transparent;
  border: 0;
  color: var(--text);
  font: inherit;
  cursor: pointer;
}

.theme-toggle-icon {
  width: 16px;
  height: 16px;
  flex: none;
}

/* 图标表示**当前**状态（暗色下是太阳），与 aria-pressed 同义。 */
:root[data-theme='dark'] .theme-icon-moon,
:root:not([data-theme='dark']) .theme-icon-sun {
  display: none;
}

/* 桌面只出图标 —— 页头这条工具条已经很挤（pt-BR/es/th 还要另外收 gap）。 */
.theme-toggle-label {
  display: none;
}

/* ── 抽屉工具区（≤720px，2026-07-31 重做）─────────────────────────────
   ⚠ 位置是刻意的：这些规则要压过 .header-dropdown-* / .user-menu* / .lang-* /
   .header-fav 的桌面基类，而媒体查询**不加特异度** —— 同为 (0,1,0) 时只看源码顺序，
   那些基类全在上面。搬回页头那个 @media (max-width: 720px) 里就会静默失效。
   （旧版就栽在这儿：那边写的 `.lang-panel { position: static }` 一直被下面的
   `.header-dropdown-panel { position: absolute }` 盖掉，抽屉里的下拉其实是浮层。） */
@media (max-width: 720px) {
  /* 抽屉工具区（2026-07-31 重做）。此前是一个 1fr 1fr 网格，靠 order:99 把语言/账户
     甩到最后 —— 结果自动排布留下空洞：未登录时主 CTA「Get started」半宽独占一行、
     右边空着；已登录时收藏也是。现在只有登录/注册两枚胶囊共享一行，其余全部整行。
     登录/注册一行两列仍是 demo 的 .bs-nav-auth。 */
  .header-utilities {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.75rem;
    margin-top: 0;
  }

  /* 收藏是「去某一页」，在抽屉里就该和上面的导航同一档，而不是缩成 0.9rem 小字
     挤在 3rem 高的登录胶囊旁边。整行 + 同一套大字 + 同一条发丝线，
     接着 .primary-nav 往下排（故 .header-utilities 的 margin-top 归 0）。 */
  .header-fav {
    grid-column: 1 / -1;
    gap: 0.6rem;
    padding: 1.1rem 0;
    font-family: var(--display);
    font-weight: 500;
    font-size: 1.9rem;
    line-height: 1.1;
    letter-spacing: 0.02rem;
    text-transform: uppercase;
    border-bottom: 1px solid var(--border);
  }

  .header-fav:hover {
    text-decoration: none;
  }

  .header-fav-icon {
    width: 1.5rem;
    height: 1.5rem;
  }

  /* Log in 在抽屉里才变回描边胶囊 —— 这正是 demo 把 .bs-nav-login 的框
     写在移动端断点里的原因（桌面是纯文字链接）。 */
  .header-auth-link {
    justify-content: center;
    height: 3rem;
    margin-top: 1rem;
    border: 1px solid var(--text);
    border-radius: var(--radius-pill);
    font-size: 0.95rem;
  }

  .header-auth-link:hover {
    text-decoration: none;
  }

  .header-auth-link--primary {
    height: 3rem;
    padding: 0;
    font-size: 0.95rem;
  }

  /* ── 抽屉里的账户区：不折叠，进来就是身份块 + 直排菜单（用户 2026-07-31 指定）──
     details 由 nav-drawer.js 在开抽屉时置 open（桌面仍是下拉，见那边的注释）。 */
  .user-menu {
    grid-column: 1 / -1;
    order: 99;
    margin-top: 1.5rem;
  }

  /* summary 不再是开关而是身份卡：头像跨两行在左，名字/邮箱堆在右。
     pointer-events 关掉 —— 否则点身份卡等于 toggle details，整块菜单会收起来。 */
  .user-menu summary {
    display: grid;
    grid-template-columns: auto 1fr;
    column-gap: 0.9rem;
    align-items: center;
    max-width: none;
    padding: 0 0 1.25rem;
    pointer-events: none;
  }

  .user-menu-avatar {
    grid-row: 1 / 3;
    width: 3rem;
    height: 3rem;
    font-size: 1.1rem;
  }

  .user-menu-name {
    font-family: var(--display);
    font-weight: 500;
    font-size: 1.15rem;
    letter-spacing: 0.02rem;
    text-transform: uppercase;
  }

  .user-menu-email {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 0.85rem;
    color: var(--muted);
  }

  /* 不折叠就没有「展开箭头」可言 */
  .user-menu .header-dropdown-chevron {
    display: none;
  }

  /* 两块面板在抽屉里都不是浮层：脱掉定位、底色与投影，就地平铺。 */
  .lang-panel,
  .user-menu-panel {
    position: static;
    min-width: 0;
    margin: 0;
    padding: 0;
    background: transparent;
    box-shadow: none;
    opacity: 1;
    transform: none;
    pointer-events: auto;
  }

  /* 浮层时代用来接住鼠标的 10px 桥，静态面板里是块无用的绝对定位盒。 */
  .lang-panel::before,
  .user-menu-panel::before {
    content: none;
  }

  /* 菜单条目对齐 .primary-nav 的节奏（整行 + 发丝线），不再是 13px 的嵌套手风琴。 */
  .user-menu-panel .header-dropdown-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 1rem 0;
    font-family: var(--display);
    font-weight: 500;
    font-size: 1.05rem;
    letter-spacing: 0.04rem;
    text-transform: uppercase;
    border-bottom: 1px solid var(--border);
  }

  /* 触屏没有 hover，底色块只会在点击后残留成「选中」的错觉；反馈交给文字色。
     ⚠ 只挑链接改文字色：退出登录那条是 .header-dropdown-item--danger，
     用不带 a 的选择器会把它的警示色一并盖成 accent。底色则两者都要收掉。 */
  .user-menu-panel .header-dropdown-item:hover {
    background: transparent;
  }

  .user-menu-panel a.header-dropdown-item:hover {
    color: var(--accent);
  }

  /* 当前页同样只用文字色标识，不铺底色 —— 与下面的语种胶囊同一套处理，
     否则基类的 .is-current 会在这套通篇发丝线的抽屉里留下唯一一块实色。 */
  .user-menu-panel .header-dropdown-item.is-current {
    background: transparent;
    color: var(--accent);
  }

  /* 只给链接条目加箭头 —— 退出登录是 <button>，不是「去某处」。 */
  .user-menu-panel a.header-dropdown-item::after {
    content: '→';
    color: var(--muted);
    font-size: 0.9em;
  }

  /* 每行自带发丝线，分隔线多余了 */
  .user-menu-divider {
    display: none;
  }

  /* 抽屉里跟语言那条一样是整行元信息，不是导航条目 —— 图标旁边把文字放出来，
     否则一枚 16px 图标孤零零占半格。order 排在语言之前。 */
  :root[data-theme]:not([data-theme-lock]) .theme-toggle {
    grid-column: 1 / -1;
    order: 99;
    justify-content: space-between;
    width: 100%;
    margin-top: 1.5rem;
    padding: 1rem 0;
  }

  .theme-toggle-label {
    display: inline;
    font-size: clamp(10px, 0.7vw, 14px);
    letter-spacing: 0.08rem;
    text-transform: uppercase;
    color: var(--muted);
  }

  .theme-toggle-icon {
    width: 1.1rem;
    height: 1.1rem;
  }

  /* ── 抽屉里的语言切换：一行触发条 + 弹窗（用户 2026-07-31 指定）──
     此前试过两版都不成：details 折叠 + max-height 内滚是滚动套滚动；改平铺胶囊网格
     则语种一多就把抽屉撑长。现在选项整体搬进 #langDialog，抽屉里永远只占一行。 */
  .lang-menu {
    display: none;
  }

  .lang-drawer-trigger {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    grid-column: 1 / -1;
    order: 100;
    width: 100%;
    /* 上方无论是账户菜单的最后一行还是登录胶囊，都自带下边界，这里只留呼吸位；
       再补一条 border-top 会和账户菜单末行的 border-bottom 叠成双线。 */
    margin-top: 1.5rem;
    padding: 1rem 0;
    background: transparent;
    border: 0;
    color: var(--text);
    font: inherit;
    text-align: left;
    cursor: pointer;
  }

  /* 语汇同 .bs-hero-kicker：这一行读起来是「语言：English」的元信息，不是导航条目。 */
  .lang-drawer-trigger-label {
    font-size: clamp(10px, 0.7vw, 14px);
    letter-spacing: 0.08rem;
    text-transform: uppercase;
    color: var(--muted);
  }

  .lang-drawer-trigger-value {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
  }

  /* → 表示「开一层新界面」，与账户菜单里链接条目的箭头同义。 */
  .lang-drawer-trigger-value::after {
    content: '→';
    color: var(--muted);
  }

  /* ── 抽屉里的板块下拉（2026-08-01）──
     ⚠ 必须写在这里而不是页头那个 @media：要压过 .header-dropdown-panel 的
     `position: absolute`，而媒体查询不加特异度，那条基类在本块上方。
     账户区在抽屉里是**展开**的（由 nav-drawer.js 置 open），板块下拉刻意仍保持折叠
     —— 图片板块四个分类全铺开会把 Pricing/About 挤出首屏，而它本来就是导航层级，
     手风琴是合理形态。 */
  .section-menu > summary {
    justify-content: space-between;
    padding: 1.1rem 0;
    font-family: var(--display);
    font-weight: 500;
    font-size: 1.9rem;
    line-height: 1.1;
    letter-spacing: 0.02rem;
    text-transform: uppercase;
    border-bottom: 1px solid var(--border);
    /* 同 .primary-nav a：基类那条 nowrap 是给桌面用的，抽屉里必须让它折行 */
    white-space: normal;
  }

  .section-menu > summary:hover,
  .section-menu > summary[aria-current='page'] {
    text-decoration: none;
  }

  .section-menu .header-dropdown-chevron {
    width: 1.25rem;
    height: 1.25rem;
    color: var(--text);
  }

  /* 同 .lang-panel / .user-menu-panel：抽屉里不是浮层，就地平铺。 */
  .section-menu-panel {
    position: static;
    min-width: 0;
    margin: 0;
    padding: 0 0 0 1rem;
    background: transparent;
    box-shadow: none;
    opacity: 1;
    transform: none;
    pointer-events: auto;
  }

  .section-menu-panel::before {
    content: none;
  }

  /* 二级条目：比板块标题小一档，但仍是整行 + 发丝线的抽屉节奏。 */
  .section-menu-panel .header-dropdown-item {
    padding: 0.9rem 0;
    font-family: var(--display);
    font-weight: 500;
    font-size: 1.05rem;
    letter-spacing: 0.04rem;
    text-transform: uppercase;
    border-bottom: 1px solid var(--border);
  }

  /* 触屏没有 hover，底色块只会残留成「选中」的错觉 —— 同账户区的处理。 */
  .section-menu-panel .header-dropdown-item:hover {
    background: transparent;
    color: var(--accent);
  }
}

/* 星标按钮。工具页 hero 里是「☆ + 文案」的胶囊，收藏页卡片上是 --compact 的纯图标。
   尺寸在两态间恒定 —— 服务端渲染成未收藏、JS 再校正，尺寸变了就会闪。 */
/* 刻意不带 margin-top：星标现在总是活在一个自带留白的容器里
   （hero 是 .bs-hero-tagrow 的 margin-top，卡片上是绝对定位）。
   留着它会把 .bs-hero-tagrow 撑高 18px，并让星标比标签低 9px —— align-items:center
   对着含外边距的项算的是**外边距盒**，看起来就像"没居中"。 */
.fav-star {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 0.4rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: transparent;
  font: inherit;
  font-size: 0.85rem;
  color: var(--muted);
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s;
}

.fav-star:hover {
  border-color: var(--text);
  color: var(--text);
}

.fav-star[aria-pressed='true'] {
  border-color: var(--text);
  color: var(--text);
}

.fav-star-icon {
  width: 18px;
  height: 18px;
  flex: none;
  fill: none;
}

.fav-star[aria-pressed='true'] .fav-star-icon {
  fill: var(--accent);
  stroke: var(--accent);
}

/* 卡片右上角的快捷收藏（首页 / 相关工具 / 404 / 收藏页共用）。
   它是 .bs-card 的**兄弟**不是后代 —— .bs-card 是整个 <a>，内嵌 <button> 非法。
   常驻可见（不是 hover 才出）：这是快捷入口，藏起来就没人知道它存在。 */
/* 与 .bs-card-cat 胶囊竖直居中对齐。
   做法是**让两者的盒模型逐项相同**，而不是拿一个偏移量去凑：
   字号同（同一个 clamp）、行高同、竖直 padding 同（0.1rem）、边框同（1px）。
   图标高度写 1.6em —— 等于胶囊在同一字号下的一个行盒。于是两者高度恒等，
   `top: 1.5rem`（＝卡片上内边距，也就是 .bs-card-top 的顶）就天然对齐。

   ⚠ 别退回「1.5rem 减几个 px」那种写法：星标原来的高度是固定的
   （16px 图标 + 5px 内边距，全是 px），而胶囊高度随根字号缩放（1440 下 21px、
   1920 下 26.4px）——差值不是常数，一个偏移量不可能在所有视口都对。
   实测那版在 1920 偏上 3.4px、在 1024 偏下 0.8px。 */
.fav-star--compact {
  position: absolute;
  top: 1.5rem;
  right: 1.5rem;
  z-index: 1;
  padding: 0.1rem;
  /* 1px 透明边框是**盒模型的一部分**（胶囊也有 1px 边框），删了高度就对不上了。
     但它永远不显形 —— 见下面把三个状态的 border-color 全钉成 transparent。
     radius 也归 0：这个盒子不等边（约 21×23），套 --radius-pill(60px) 会渲染成椭圆。 */
  border: 1px solid transparent;
  border-radius: 0;
  background: transparent;
  font-size: clamp(10px, 0.7vw, 13px);
  line-height: 1.6;
  color: var(--muted);
  /* 最后一步：胶囊并不贴着行顶 —— .bs-card-top 的行高是由**编号**撑的
     （::before 是 1.6rem / line-height 1，宽视口下比胶囊高），胶囊在其中居中。
     所以星标要对齐的是**这一行**，不是胶囊本身。
     min-height: 1.6rem 让星标高度 = max(1.6rem, 胶囊高) = 行高，
     配合 top: 1.5rem（行顶）与 .fav-star 的 align-items: center，
     图标中线就恒等于行中线 —— 也就恒等于胶囊中线，与谁更高无关。 */
  min-height: 1.6rem;
}

/* 盒子跟着胶囊走之后按钮只有 ~21px，低于触控目标下限。
   用伪元素把可点区域向外扩到 ~33px —— 它不参与布局，故不破坏上面的对齐。 */
.fav-star--compact::after {
  content: '';
  position: absolute;
  inset: -6px;
}

/* ⚠ hover 与选中态都必须显式收掉边框：基类的 .fav-star:hover / [aria-pressed='true']
   都是 (0,2,0) 且写在前面，会给出一圈描边 —— 在这个不等边的小盒子上就是一枚椭圆
   （选中态还会配上实心星糊成黑徽章）。这两条同为 (0,2,0) 且在其后，夺回声明权。
   紧凑星标是纯图标控件，不是 chip：反馈只靠图标颜色，任何状态都不出边框。 */
.fav-star--compact:hover {
  border-color: transparent;
  color: var(--text);
}

.fav-star--compact[aria-pressed='true'] {
  border-color: transparent;
  color: var(--accent);
}

/* 键盘焦点仍要看得见。用 outline 而不是 border —— outline 不占位，
   不会把上面那套「与胶囊等高」的盒模型算坏。 */
.fav-star--compact:focus-visible {
  outline: 2px solid var(--text);
  outline-offset: 2px;
}

/* 1.6em = 胶囊在同一字号下的一个行盒高度，两者的内容盒因此等高。 */
.fav-star--compact .fav-star-icon {
  width: 1.6em;
  height: 1.6em;
}

/* 卡片壳（x-tool-card 的外层）。星标绝对定位要它当参照，
   卡片则要撑满，否则同行卡片不等高。 */
.tool-card {
  position: relative;
  display: flex;
}

.tool-card > .bs-card {
  flex: 1;
}

/* 给星标让位：.bs-card-top 是 space-between，分类胶囊本来就贴右缘，
   不留出这段右 padding 两者会叠在一起。
   2.25rem ＝ 星标宽（约 1.6em + 0.2rem + 2px）再加一段呼吸位；用 rem 跟着根字号缩放，
   写死 px 会在窄视口把水平间隙挤到 1px（上一版的实测值）。 */
.tool-card .bs-card-top {
  padding-right: 2.25rem;
}

/* ⚠ 这一段的间距 2026-07-31 由写死的 px 改成 rem。全站根字号是流体的
   （html 的 clamp(12px,1vw,18px)，见 FluidTypeScaleTest），px 值不跟着缩放 ——
   宽视口下整页都放大了，只有这几处纹丝不动。别改回 px。 */
.fav-page {
  padding-bottom: 3rem;
}

/* 计数占 kicker 的位置（标题之上，同首页 .bs-hero-kicker），见 blade 里的说明。
   带 .bs-page-hero 前缀(0,2,1) 才压得过 `.bs-page-hero p`(0,1,1) 的 `margin: 0` ——
   单类的 .fav-count(0,1,0) 输给它，留白会静默失效（同设计稿 §十二 记的那类特异度坑）。 */
.bs-page-hero .fav-count {
  margin: 0 0 1.5rem;
  /* 空着也要占住一行：内容是 JS 填的，不预留高度就会在落位时把整页推下去。 */
  min-height: 1.6em;
  font-size: clamp(10px, 0.7vw, 14px);
  letter-spacing: 0.04rem;
  color: var(--muted);
  text-transform: uppercase;
}

/* JS 落位前先占住高度，避免卡片进来时把页脚顶下去（首页面板那轮定的闸门是 CLS ≤ 0.02）。
   空态与网格共用这一个容器，两者切换不改变文档高度。 */
.fav-region {
  min-height: 18rem;
  padding-top: 2rem;
  /* 卡片编号与首页同一套 CSS counter 机制。不重置的话收藏页会退到隐式根计数器上
     （眼下碰巧也从 01 起，但那不是保证 —— 同 .bs-grid--compact 处的说明）。 */
  counter-reset: bs-card;
}

/* 收藏页没有首页那种按分类分区，是一张平铺的短清单：列宽从首页的 15rem 收到 13rem，
   同宽视口多放一列，短清单不至于一行两张就断。卡片本身仍与首页逐字相同
   （2026-07-30 拍板的「与首页工具项完全一致」说的是卡片，不是列宽）。 */
.fav-region .bs-grid {
  grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
}

/* 空态：与卡片同一套发丝线面板语汇（原来是一块没有边界的居中文字，
   落在空荡荡的 18rem 区域里像是没加载完）。 */
.fav-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.75rem;
  padding: 4rem 1.5rem;
  text-align: center;
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: 0 0 0 1px var(--border); /* 发丝线：同 .bs-card，不用 border */
}

/* 空心星当视觉锚点。用**字形**不用 SVG —— 同 .bs-card-arrow 的 ☞，这套版式通篇如此。 */
.fav-empty::before {
  content: '☆';
  font-size: 2.75rem;
  line-height: 1;
  color: var(--muted);
}

.fav-empty h2 {
  margin: 0;
  font-family: var(--display);
  font-size: 1.3rem;
  text-transform: uppercase;
}

.fav-empty p {
  margin: 0;
  max-width: 28rem;
  color: var(--muted);
}

/* 提示条改成左侧重线的旁注（原来四面描边的灰盒子，和下面的卡片抢同一种「面板」读法）。 */
.fav-warning {
  margin: 1.5rem 0 0;
  padding: 0.75rem 1rem;
  border: 0;
  border-left: 2px solid var(--text);
  background: var(--surface-2);
  font-size: 0.85rem;
  color: var(--muted);
}

/* 手机上收回单列。上面那条 13rem 是为了宽视口多放一列，但根字号在这儿只有 12px，
   13rem=156px 会让 375px 的屏挤出两列各 ~172px —— 而首页的 15rem 在同一处正好塌成单列。
   收藏页不该比首页更挤，故在这个断点跟它对齐。 */
@media (max-width: 480px) {
  .fav-region .bs-grid {
    grid-template-columns: 1fr;
  }
}

/* ── 首页 hero 底部一行：Product Hunt 徽章 + 打赏入口 ──────────────
   紧贴 .trust-row 下方，两者水平居中并排。都关掉时这一行不渲染，
   首屏高度回到原样。 */
.hero-links {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.75rem 1.75rem;
  margin-top: 1.25rem;
}

/* 徽章是后台粘进来的整段 PH 官方代码，尺寸由它自己的 width/height + 内联 style 定
   （标准 250×54）。这里只兜住溢出，**不去覆盖它的尺寸** —— 内联 style 优先级更高，
   写了也打不过，只会让两套规则互相打架。

   gap 是必需的：这个字段能粘进多枚徽章（PH 的 Featured / Review / Top Post 是三段
   独立的 <a><img></a>）。inline-flex 下，粘贴时代码之间的换行属于纯空白文本节点，
   flex 不会为它生成匿名 item，直接丢掉；line-height:0（本是为消掉 <img> 基线下那条缝）
   又把行内兜底间距也清了 —— 两枚徽章因此会完全贴死。
   列间距收到 1rem（小于外层 .hero-links 的 1.75rem）：同类徽章读起来是一组，
   与旁边的打赏入口拉开层级。 */
.hero-link-ph {
  display: inline-flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.75rem 1rem;
  line-height: 0;
  max-width: 100%;
}

.hero-link-ph img {
  max-width: 100%;
}

/* 打赏入口的语汇对齐上一行的 .trust-row（同字号、同字距、同灰度），
   两行因此读起来是一组，而不是硬塞进来的一块。 */
.hero-link-tip {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: clamp(10px, 0.7vw, 14px);
  letter-spacing: 0.04rem;
  color: var(--muted);
  text-decoration: none;
}

.hero-link-tip:hover {
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 4px;
}

/* 板块主页在工具落地之前的空态（S0：pdf / utility）。 */
.section-empty {
  margin: 2rem 0 0;
  color: var(--muted);
}

/* ── SVG Viewer（S1）───────────────────────────────────────────────
   舞台 = 固定高的取景框，图片在里面被 transform 缩放/平移。
   ⚠ 缩放走 transform 而不是改 width/height：后者每帧触发重排，
   而 transform 只走合成层，8× 下拖动才不卡。 */
/* 预览区跟右侧控件栏一起拉高。
   .editor-panel 是 grid，两列本来就等高 —— 问题出在 .editor-main **内部**：
   舞台原先写死 `height: 26rem`，而侧栏（缩放 + 背景 + 信息 + 源码）实际约 42rem，
   于是舞台下方长期空出一大截。改成 .editor-main 列式 flex、舞台 flex:1 自己长齐。
   ⚠ **每一层都要 min-height: 0**：flex 子项默认 `min-height: auto`，不清掉的话
   里面的内容会把容器顶破，flex:1 反而不生效（源码 pre 那种可滚动块尤其明显）。
   ⚠ 作用域必须带 .svg-viewer-workspace：.editor-main / .editor-panel 是 40 多个
   工具页共用的结构类，在基类上改会波及全站。 */
.svg-viewer-workspace .editor-main {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.svg-viewer-workspace #previewBlock {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

.svg-viewer-stage {
  position: relative;
  flex: 1;
  /* ≤900px 塌成单栏后两列不再并排，flex:1 没有可撑的高度，靠这条兜底。 */
  min-height: 18rem;
  overflow: hidden;
  display: grid;
  /* 同 .svgo-pane-stage：隐式 auto 行会让 .svg-viewer-img 的 `max-height: 90%`
     失效（当 none），高图初始就不是「适应视图」而是原尺寸被裁掉大半。
     这里 overflow:hidden 只是把症状藏起来，问题照样在。 */
  grid-template: minmax(0, 1fr) / minmax(0, 1fr);
  place-items: center;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: grab;
  touch-action: none; /* 否则触屏上拖动会被浏览器当成滚动页面 */
}

.svg-viewer-stage.is-panning {
  cursor: grabbing;
}

/* 四种底纹。棋盘格复用 .compare-container 那份 repeating-conic-gradient 的写法，
   自定义色经 --viewer-bg 下发（由 JS 写在 style 上）。 */
.svg-viewer-stage[data-bg='checker'] {
  background: repeating-conic-gradient(var(--border) 0% 25%, var(--surface-2) 0% 50%) 50% / 16px 16px;
}

.svg-viewer-stage[data-bg='white'] { background: #fff; }
.svg-viewer-stage[data-bg='black'] { background: #000; }
.svg-viewer-stage[data-bg='custom'] { background: var(--viewer-bg, var(--surface)); }

/* transform-origin 居中：从中心缩放才符合直觉（左上角缩放会让图往右下跑）。
   max-* 让初始 100% 时大图也完整可见，之后由 zoom 放大。 */
.svg-viewer-img {
  /* ⚠ 脱离文档流是刻意的：图留在流里时，它的固有尺寸会成为 .svg-viewer-stage 的
     内容贡献 —— 一张 400×1600 的 SVG 能把预览区顶到 1600px 高，`flex: 1`「跟右侧
     控件栏拉齐」的意图（bb60b97）就作废了。abspos + inset:0 + margin:auto 是替换
     元素的经典居中写法，`transform` 的缩放/平移照常生效。 */
  position: absolute;
  inset: 0;
  margin: auto;
  max-width: 90%;
  max-height: 90%;
  transform-origin: center center;
  /* 不加过渡：拖动时每帧都在改 transform，加了会拖尾。 */
  user-select: none;
  -webkit-user-drag: none;
}

.svg-viewer-hint {
  margin: 0.5rem 0 0;
  font-size: 0.8rem;
  color: var(--muted);
}

.svg-viewer-zoom {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
}

.svg-viewer-zoom .btn {
  flex: none;
  min-width: 2.25rem;
}

.svg-viewer-zoom-value {
  flex: 1;
  text-align: center;
  font-family: var(--display);
  font-size: 0.95rem;
}

.svg-viewer-bgs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-bottom: 0.75rem;
}

.svg-viewer-bg {
  padding: 0.25rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: transparent;
  font: inherit;
  font-size: 0.8rem;
  color: var(--muted);
  cursor: pointer;
}

.svg-viewer-bg:hover {
  border-color: var(--text);
  color: var(--text);
}

.svg-viewer-bg[aria-pressed='true'] {
  border-color: var(--text);
  background: var(--text);
  color: var(--bg);
}

/* 信息条：术语（viewBox 等）不翻译也不换行，值可以断。 */
.svg-viewer-info {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.35rem 1rem;
  margin: 0;
  font-size: 0.85rem;
}

.svg-viewer-info dt {
  color: var(--muted);
  white-space: nowrap;
}

.svg-viewer-info dd {
  margin: 0;
  overflow-wrap: anywhere;
}

.svg-viewer-source,
.svg-viewer-error-body {
  max-height: 14rem;
  margin: 0 0 0.75rem;
  padding: 0.75rem;
  overflow: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  font-size: 0.78rem;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* 解析失败态。取代预览显示 —— 解析不出来就没有可预览的东西，
   摆个空画布只会让人以为是白图。 */
.svg-viewer-error {
  padding: 1.25rem;
  border: 1px solid var(--danger, #c0392b);
  border-radius: var(--radius);
  background: var(--surface);
}

.svg-viewer-error-title {
  margin: 0 0 0.35rem;
  font-family: var(--display);
  font-weight: 500;
  font-size: 1.05rem;
  color: var(--danger, #c0392b);
}

.svg-viewer-error-hint {
  margin: 0 0 0.75rem;
  font-size: 0.85rem;
  color: var(--muted);
}

/* ── SVGO 三页（optimizer / minifier / beautifier）─────────────────
   前后并排预览：设计稿 §4.3 要求「肉眼核对没有清坏」—— 优化器最怕的是悄悄清掉了
   该留的东西，只给体积数字是看不出来的。 */
.svgo-compare {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1rem;
}

.svgo-pane {
  margin: 0;
  min-width: 0; /* grid 子项默认 min-width:auto，长文件名/大图会把列撑破 */
}

.svgo-pane-stage {
  display: grid;
  /* ⚠ 必须显式给轨道尺寸，不能只靠 `place-items: center` 的隐式行。
     隐式行是 auto（尺寸依赖内容），子项身上的 `max-height: 100%` 就没有确定的
     百分比基准，浏览器直接把它当 none —— 实测一张 400×1600 的 SVG 在 202px 高的
     框里被渲染成 1547px，整个撑破工作区。`minmax(0, 1fr)` 让轨道变成确定尺寸，
     百分比才解析得出来。**别把这行删了改回 place-items 单打**。 */
  grid-template: minmax(0, 1fr) / minmax(0, 1fr);
  place-items: center;
  height: 14rem;
  padding: 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  /* 棋盘格：SVG 多半带透明底，纯白背景下看不出边界 */
  background: repeating-conic-gradient(var(--border) 0% 25%, var(--surface-2) 0% 50%) 50% / 16px 16px;
}

.svgo-pane-stage img {
  max-width: 100%;
  max-height: 100%;
}

.svgo-pane figcaption {
  margin-top: 0.5rem;
  font-size: 0.72rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
  text-align: center;
}

/* 体积对比是 minifier 那页的主角，给它压缩体大字。 */
.svgo-stat {
  margin: 1.25rem 0 0;
  font-family: var(--display);
  font-size: 1.15rem;
  letter-spacing: 0.02rem;
  text-align: center;
}

.svgo-stat:empty {
  margin: 0;
}

/* svg-color-changer 的色板列表（S3c）。行数随文件变，由 recolor.js 生成。 */
.recolor-list {
  max-height: 18rem;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  list-style: none;
}

.recolor-row {
  display: grid;
  /* 色块定宽、色值撑开、次数靠右 —— 色值长短不一（#ffffff 与 currentColor），
     用 1fr 吃掉差额，右侧的次数才不会跳来跳去。 */
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 0.6rem;
  padding: 0.35rem 0;
}

.recolor-row + .recolor-row {
  border-top: 1px solid var(--border);
}

/* 原生 color 输入在各家浏览器里默认样式差得远，统一成一个方色块。 */
.recolor-swatch {
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: calc(var(--radius) / 2);
  background: none;
  cursor: pointer;
}

.recolor-swatch::-webkit-color-swatch-wrapper { padding: 2px; }
.recolor-swatch::-webkit-color-swatch { border: none; border-radius: 2px; }
.recolor-swatch::-moz-color-swatch { border: none; border-radius: 2px; }

.recolor-label {
  font-family: var(--mono);
  font-size: 0.85rem;
  /* currentColor 这种长值不该把次数挤出去。 */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.recolor-count {
  font-size: 0.78rem;
  color: var(--muted);
}

.svgo-result {
  max-height: 16rem;
  margin: 0 0 0.75rem;
  padding: 0.75rem;
  overflow: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  font-size: 0.75rem;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

@media (max-width: 560px) {
  /* 窄屏两栏各只剩 ~130px，图小到看不出差异 —— 改上下叠放 */
  .svgo-compare {
    grid-template-columns: 1fr;
  }
}

/* ── 代码输出类工具（code-workspace partial：svg-to-base64，S3 起还有 react/vue） ── */

.code-block {
  margin-top: 1.25rem;
}

.code-block:first-of-type {
  margin-top: 1rem;
}

.code-block-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.5rem;
}

.code-block-head h4 {
  margin: 0;
  font-size: 0.72rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
}

/* 结果块比 svgo 那边矮一档：这页一屏要放四块，每块都 16rem 会把页面撑爆。 */
.code-workspace .svgo-result {
  max-height: 9rem;
  margin-bottom: 0;
}

/* 侧栏的预览是配角，给个固定高度别让它跟着图片比例乱跳。 */
.code-workspace .svgo-pane-stage {
  height: 9rem;
}

/* 这页的 stat 是两个体积的对照，不是 minifier 那种主角大字。 */
.code-workspace .svgo-stat {
  margin-top: 0.75rem;
  font-size: 0.82rem;
  color: var(--muted);
}

/* ── svg-crop（S4a）──────────────────────────────────────────────
   裁剪框与遮罩整套复用图片 crop 工具的 `.crop-stage` / `.crop-shade` / `.crop-box`，
   这里只补「舞台里装的是 <img> 而不是 <canvas>」那点差异。 */
.svg-crop-stage {
  /* 舞台宽高由脚本按 viewBox 比例写死，图片撑满即可。
     ⚠ 图必须 absolute：留在文档流里时它的固有尺寸会反过来撑舞台，
     裁剪框的坐标基准就跟着变了（svg-viewer 那次踩的是同一条）。 */
  position: relative;
  /* SVG 多半透明底，棋盘格才看得出边界与裁剪范围 */
  background: repeating-conic-gradient(var(--border) 0% 25%, var(--surface-2) 0% 50%) 50% / 16px 16px;
}

.svg-crop-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
}

/* ── svg-merge（S4c）的文件列表 ────────────────────────────────
   行数随上传的文件变，由 merge.js 生成；顺序即排版顺序，故有上下移动按钮。 */
.merge-list {
  max-height: 14rem;
  margin: 0 0 1.25rem;
  padding: 0;
  overflow-y: auto;
  list-style: none;
}

.merge-row {
  display: grid;
  /* 文件名撑开、尺寸定宽、按钮组靠右 —— 文件名长短差别很大，让它独占弹性列 */
  grid-template-columns: 1fr auto auto;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0;
}

.merge-row + .merge-row {
  border-top: 1px solid var(--border);
}

.merge-name {
  min-width: 0; /* grid 子项默认 min-width:auto，长文件名会把整行撑破 */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.85rem;
}

.merge-size {
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--muted);
}

.merge-actions {
  display: flex;
  gap: 0.25rem;
}

/* ── svg-split（S4d）的分块列表 ────────────────────────────────
   每行 = 勾选框 + 缩略图 + 名称 + 标签名；缩略图与导出同源（同一份 blob URL），
   看到什么就下到什么。行由 split.js 生成。 */
.split-list {
  max-height: 22rem;
  margin: 0 0 1rem;
  padding: 0;
  overflow-y: auto;
  list-style: none;
}

.split-row + .split-row {
  border-top: 1px solid var(--border);
}

.split-pick {
  display: grid;
  grid-template-columns: auto auto 1fr auto;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0;
  cursor: pointer;
}

.split-thumb {
  width: 40px;
  height: 40px;
  object-fit: contain;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  /* 棋盘格：拆出来的块多半是透明底的图形 */
  background: repeating-conic-gradient(var(--border) 0% 25%, var(--surface-2) 0% 50%) 50% / 10px 10px;
}

.split-label {
  min-width: 0; /* grid 子项默认 min-width:auto，长 id 会把整行撑破 */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--mono);
  font-size: 0.8rem;
}

.split-tag {
  font-size: 0.72rem;
  color: var(--muted);
}

/* ── PDF 板块（P1）────────────────────────────────────────────────
   三块：merge 的文件列表、split 的计划表、delete/extract/rotate 共用的页面网格。 */

/* merge-pdf 的文件列表。顺序即合并顺序，故每行都有上下移动按钮（同 .merge-row 的思路，
   但这里多一列首页缩略图 —— PDF 光看文件名认不出是哪一份）。 */
.pdf-file-list {
  max-height: 22rem;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  list-style: none;
}

.pdf-file-row {
  display: grid;
  /* 缩略图定宽、元信息撑开、按钮组靠右 */
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0;
}

.pdf-file-row + .pdf-file-row {
  border-top: 1px solid var(--border);
}

.pdf-file-thumb {
  display: grid;
  /* 缩略图是后到的（先画行、图慢慢补），定死尺寸免得图一到整份列表跳一下 */
  grid-template: minmax(0, 1fr) / minmax(0, 1fr);
  width: 36px;
  height: 48px;
  border: 1px solid var(--border);
  background: var(--surface-2);
}

/* canvas = PDF 首页缩略图（merge）；img = 图片本身（from-images，P2c）。
   两者同一个框，故同一条规则。 */
.pdf-file-thumb canvas,
.pdf-file-thumb .pdf-thumb-img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.pdf-file-meta {
  min-width: 0; /* grid 子项默认 min-width:auto，长文件名会把整行撑破 */
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.pdf-file-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.85rem;
}

.pdf-file-pages {
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--muted);
}

.pdf-file-actions {
  display: flex;
  gap: 0.25rem;
}

/* split-pdf 的计划表 —— 导出前把「会得到哪几份、各含哪些页」摆出来 */
.pdf-plan-list {
  max-height: 20rem;
  margin: 0 0 0.75rem;
  padding: 0;
  overflow-y: auto;
  list-style: none;
}

.pdf-plan-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 0.75rem;
  padding: 0.4rem 0;
}

.pdf-plan-row + .pdf-plan-row {
  border-top: 1px solid var(--border);
}

.pdf-plan-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--mono);
  font-size: 0.8rem;
}

.pdf-plan-pages {
  font-size: 0.75rem;
  color: var(--muted);
}

/* ── PDF 右栏（.editor-sidebar.control）的表单版式 ─────────────────
   ⚠ 站内没有全局 <input> 样式：文本 / 数字框不套 .text-input 就直接露浏览器
     默认框（Arial 13px / 2px 边框 / 0 圆角 / 高 21px），和同栏 36px 的
     .listbox-trigger 完全对不齐 —— S3 的 svg-crop·svg-resizer 踩过同一个坑。
     框体样式一律走 .text-input，这里只补版式，不重复定义边框圆角。
   ⚠ .editor-sidebar 的 `gap:16px` 只作用于它的直接子元素，管不到
     <fieldset class="control-group"> 内部 —— 组里的字段与相邻 listbox 原本
     是零间距贴死的（image-to-pdf 那两组各两个 listbox 最明显）。 */
.pdf-tool-form .form-field + .form-field,
.pdf-tool-form .form-field + .listbox,
.pdf-tool-form .listbox + .form-field,
.pdf-tool-form .listbox + .listbox {
  margin-top: 12px;
}

/* legend 说的是「这一组管什么」，字段 label 说的是「这一格设什么」——
   而 .field-label 与 .control-group legend 的排版本来一模一样（都是 0.72rem
   全大写 + .07em 字距 + --label），两级叠在一起会糊成一片，"PAGE / PAGE SIZE"
   看着像重复了一遍。字段这级改回句式大小写 + 正文色，层级才立得住。 */
.pdf-tool-form .field-label {
  margin-bottom: 6px;
  font-size: 0.78rem;
  font-weight: 500;
  text-transform: none;
  letter-spacing: normal;
  color: var(--text);
}

/* 字段内部的提示 / 报错由字段自己控距。必须盖掉 .control-group .field-hint
   的负上边距 —— 那条是给「紧贴 listbox 的整组说明」用的，落到字段里会把提示
   吸到输入框上。两者特指度相同，靠排在后面取胜。 */
.pdf-tool-form .form-field .field-hint,
.pdf-tool-form .form-field .field-error {
  margin: 6px 0 0;
}

/* 状态行（.svgo-stat）是 1.15rem 的展示体，那条 -2px 落在它下面就是两行字贴死。
   ⚠ 居中大字本身不动 —— svg-split / code-workspace 的右栏一直是这个样子，
   改了是 PDF 单方面偏离 SVG 板块，不是「对齐」。 */
.pdf-tool-form .svgo-stat + .field-hint {
  margin-top: 10px;
}

/* 连着两条 hint 说的是两件事（organize 是「怎么拖」与「键盘怎么按」），
   原本 6px 会糊成一整段 */
.pdf-tool-form .field-hint + .field-hint {
  margin-top: 10px;
}

/* 组末尾的 hint 不再自己留下边距，组间距一律交给 .editor-sidebar 的 gap ——
   否则以 hint 收尾的组比以字段收尾的多出 8px，九个 PDF 右栏彼此对不齐 */
.pdf-tool-form .control-group > .field-hint:last-child {
  margin-bottom: 0;
}

/* delete / extract / rotate 共用的页面网格 */
.pdf-page-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 12px;
  max-height: 30rem;
  overflow-y: auto;
  padding: 2px;
}

.pdf-cell {
  position: relative;
  display: block;
  width: 100%;
  padding: 6px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
}

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

/* ⚠ 方格必须是**正方形**：旋转的缩略图靠 CSS transform 转，
   一个能塞进正方形的框转 90° 之后仍然塞得进（长宽都 ≤ 边长），不必再算缩放。 */
.pdf-cell-stage {
  /* ⚠ 显式轨道尺寸，不用 place-items —— 隐式 auto 行下子项的百分比 max-height
     没有基准、被当 none 处理，一张高图能把格子撑破（SvgPreviewStageCssTest 守着）。 */
  display: grid;
  grid-template: minmax(0, 1fr) / minmax(0, 1fr);
  position: relative;
  aspect-ratio: 1;
  background: var(--surface-2);
}

.pdf-cell-img {
  position: absolute;
  inset: 0;
  margin: auto;
  max-width: 100%;
  max-height: 100%;
  display: block;
  /* 转的是缩略图不是内容 —— 重渲一遍要走完整条 PDF.js 管线，点四下就是四次全页渲染 */
  transition: transform 0.15s;
}

.pdf-cell[data-rot='90'] .pdf-cell-img { transform: rotate(90deg); }
.pdf-cell[data-rot='180'] .pdf-cell-img { transform: rotate(180deg); }
.pdf-cell[data-rot='270'] .pdf-cell-img { transform: rotate(270deg); }

.pdf-cell-num {
  display: block;
  margin-top: 4px;
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--muted);
  text-align: center;
}

.pdf-cell-badge {
  position: absolute;
  top: 2px;
  right: 2px;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  border-radius: var(--radius-pill);
  background: var(--surface-2);
  color: var(--muted);
  font-size: 0.68rem;
  line-height: 18px;
  text-align: center;
}

.pdf-cell.is-picked {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}

.pdf-cell.is-picked .pdf-cell-badge {
  background: var(--accent);
  color: var(--text);
}

/* delete 页：选中 = 待删，用记号而不只是高亮 —— 「选中」在两个页面上是**相反的意思**，
   光靠颜色区分不了，用户很容易把 extract 的操作习惯带过来。 */
.pdf-grid-workspace[data-mode='delete'] .pdf-cell.is-picked .pdf-cell-img {
  opacity: 0.4;
}

/* 角标从页号换成 ✕：`font-size: 0` 把页号收掉，伪元素自己再把字号要回来 */
.pdf-grid-workspace[data-mode='delete'] .pdf-cell.is-picked .pdf-cell-badge {
  font-size: 0;
}

.pdf-grid-workspace[data-mode='delete'] .pdf-cell.is-picked .pdf-cell-badge::after {
  content: '✕';
  font-size: 0.7rem;
}

.svgo-stat.is-warning {
  color: #9a3412;
}

/* ── organize-pdf（P2a）─────────────────────────────────────────
   同一套 .pdf-page-grid，但格子本身可以被搬走，所以是 div 不是 button
  （button 里不能再套 button，而每格要带自己的 ↻ / ✕）。 */
.pdf-cell-organize {
  /* 拖拽时别让浏览器接管手势：触摸屏上不写这条，拖页会变成滚动页面 */
  touch-action: none;
  user-select: none;
  cursor: grab;
}

.pdf-cell-organize:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.pdf-cell-organize.is-dragging {
  cursor: grabbing;
  opacity: 0.55;
  border-color: var(--accent);
}

/* 键盘搬运（空格抓起）要有与拖拽同样明显的态，否则按了空格看不出发生了什么 */
.pdf-cell-organize.is-grabbed {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent);
}

/* 删页是**标记**不是真删：格子留在原位、灰掉，再点一次能撤回 */
.pdf-cell-organize.is-removed .pdf-cell-stage {
  opacity: 0.3;
}

.pdf-cell-organize.is-removed {
  border-style: dashed;
}

.pdf-cell-actions {
  position: absolute;
  top: 2px;
  right: 2px;
  display: flex;
  gap: 2px;
}

.pdf-cell-act {
  width: 20px;
  height: 20px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--muted);
  font-size: 0.7rem;
  line-height: 1;
  cursor: pointer;
}

.pdf-cell-act:hover {
  background: var(--accent);
  color: var(--text);
}

@media (max-width: 900px) {
  .pdf-page-grid {
    grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
  }
}

/* ── image-color-picker（2026-09-09 新增） ────────────────────────── */

/* 取色要看得见指针落在哪一格上。 */
.picker-canvas {
  display: block;
  max-width: 100%;
  height: auto;
  cursor: crosshair;
  border-radius: var(--radius);
  border: 1px solid var(--border);
}

.picked-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

/* ⚠ 色块的边框不是装饰：纯白的取色结果压在浅色面板上，没有边框就等于消失了
   （暗色下纯黑同理）。用户的颜色是任意的，站点的底色不是。 */
.picked-chip,
.palette-chip {
  border: 1px solid var(--border);
  border-radius: calc(var(--radius) / 2);
  flex: none;
}

.picked-chip {
  width: 3rem;
  height: 3rem;
}

.picked-values {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  font-family: var(--mono);
  font-size: 0.85rem;
  min-width: 0;
}

.picked-values .rgb,
.picked-values .hsl {
  color: var(--muted);
  font-size: 0.78rem;
}

.palette-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: 0.5rem;
  margin: 0.5rem 0 0.75rem;
}

/* 文字**不压在色块上** —— 压上去就要为任意底色算对比度，而底色是用户的图给的。 */
.palette-swatch {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  text-align: left;
  cursor: pointer;
}

.palette-swatch:hover {
  background: var(--surface-2);
}

.palette-chip {
  width: 1.75rem;
  height: 1.75rem;
}

.palette-code {
  font-family: var(--mono);
  font-size: 0.8rem;
}

.palette-share {
  margin-left: auto;
  color: var(--muted);
  font-size: 0.72rem;
}
