/* 可复用组件。尺寸全部走 tokens.css 的流式变量，因此这里不写媒体查询。 */

/* --------------------------------------------------------------------------- 按钮 */

.btn {
  min-height: var(--tap);
  padding: 0 var(--gap);
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text);
  font-size: var(--fs-base);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.btn:active {
  opacity: 0.75;
}

/* 点不动的按钮要看得出来。**灰但不锁**是最糟的一种：用户会一直点，
   而指令根本没发出去（设备离线时的「启动」就是这么撞见的）。
   与 .recipe-ops .btn:disabled 同一个观感 */
.btn:disabled {
  opacity: 0.4;
}

.btn-primary {
  background: var(--primary);
  border-color: var(--primary);
  color: var(--on-accent);
  font-weight: 600;
}

.btn-block {
  width: 100%;
}

.btn-danger {
  color: var(--st-error);
  border-color: var(--st-error);
  background: transparent;
}

.btn-link {
  min-height: auto;
  border: 0;
  background: none;
  color: var(--primary);
  padding: var(--gap-xs);
}

/* 卡片里的小按钮（状态面板右侧那类动作，如「报警复位」）：
   比整行按钮矮一档，宽度只占内容 */
.btn-compact {
  min-height: 34px;
  padding: 0 var(--gap-sm);
  font-size: var(--fs-sm);
  flex: none;
}

/* 顶栏图标按钮（返回、右上角动作） */
.icon-btn {
  min-width: var(--tap);
  min-height: var(--tap);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-lg);
  color: var(--text);
  border-radius: var(--radius-sm);
}

.icon-btn:active {
  background: var(--surface-2);
}

/* --------------------------------------------------------------------------- 表单 */

.field {
  display: grid;
  gap: var(--gap-xs);
}

.field > label {
  font-size: var(--fs-sm);
  color: var(--text-dim);
}

.input {
  min-height: var(--tap);
  padding: 0 var(--gap-sm);
  width: 100%;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  font-size: var(--fs-base);
  outline: none;
}

.input:focus {
  border-color: var(--primary);
}

.input::placeholder {
  color: var(--text-dim);
}

.field-error {
  font-size: var(--fs-xs);
  color: var(--st-error);
  min-height: 1em;
}

/* --------------------------------------------------------------------------- 卡片 */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--gap);
}

.card + .card {
  margin-top: var(--gap);
}

.card-title {
  font-size: var(--fs-sm);
  color: var(--text-dim);
  margin-bottom: var(--gap-sm);
}

/* 控件栅格。列数由 render/controls.js 按「分组内控件数量 + 视口宽度」
   算好后以行内样式给出（手机最多 2 列、宽屏最多 4 列），这里只定默认值。 */
.ctl-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--gap-sm);
}

/* span=2 的控件占满一行 */
.ctl.span-2 {
  grid-column: 1 / -1;
}

/* 卡片级垂直居中（模板 `cardValign: "center"`）：一行里几块控件高低不一时，
   矮的那块上下居中，不贴着顶 */
.ctl-grid.vcenter {
  align-items: center;
}

.ctl {
  display: grid;
  gap: var(--gap-xs);
  align-content: start;
}

.ctl-label {
  font-size: var(--fs-sm);
  color: var(--text-dim);
}

/* --------------------------------------------------------------- 状态面板 */

/* 一组只读字段：标签小、值显眼，两列铺开 */
.status-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--gap-sm) var(--gap);
}

/* 模板 `valign: "center"`：每格内容在格子里**上下居中**。
   同一行里有的格是「标签 + 值」、有的是两行的复合格，不居中的话上下沿参差不齐 */
.status-grid.vcenter > .status-cell {
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* 带右侧动作按钮的面板（模板的 `action`，现在是「报警复位」）。
   网格吃掉剩余宽度、按钮固定在最右边 —— 按钮不会压住报警文字。 */
.status-panel {
  display: flex;
  align-items: center;
  gap: var(--gap-sm);
}

.status-panel > .status-grid {
  flex: 1;
  min-width: 0;
}

/* `stretch`：面板撑满这一列的高度、内部改成**竖排**。
   用在两个面板并排、行数不一样时（隔板一列五格在左，右边目标温度 + 一个真空度小框）——
   不撑的话格子全挤在顶上，右边空一大片，两块也拼不成一个方块。

   `height: 100%` 是必须的：.ctl-grid 的行高由高的那一列撑着，
   这一列得把自己的高度吃到满，内部才有的可分。

   注：这里把 grid 换成 flex —— 上面 style 里的 gridTemplateColumns 就失效了。
   目前只有这一个竖向面板用它，够用；要是有多列的 stretch 面板，得另想办法 */
.status-grid.stretch {
  display: flex;
  flex-direction: column;
  height: 100%;
}

/* 普通格把剩余高度分掉 */
.status-grid.stretch > .status-cell {
  flex: 1 1 0;
}

/* 框起来的那些**按内容高、贴着底**（margin-top:auto 就是"剩下的都留在我上面"）*/
.status-grid.stretch > .status-box {
  flex: 0 0 auto;
  margin-top: auto;
}

/* 一组字段的小框（模板里给同一个 `box` 名就装进同一个框）。

   **哪几条边画线由模板的 `box.sides` 决定**（渲染器算好 border-width 写在内联样式里）——
   这个默认值只是没写 sides 时的兜底。
   异形外框就是靠它拼的：左边那块不画右边、缺口那块只画左边，合起来是个 Γ 形 */
.status-box {
  display: grid;
  gap: var(--gap-sm);
  padding: var(--gap-sm);
  border-style: solid;
  border-color: var(--border);
  border-width: 1px;
  border-radius: var(--radius-sm);
}

/* 跨行的框（左边通到底、右边上下两块）*/
.status-box.span-2 { grid-row: span 2; }
.status-box.span-3 { grid-row: span 3; }

/* `frame`：面板自己带一圈**完整的方框**。
   外面这一圈是"完整的方框"，里面的 `box` 只画分隔线（`sides` 给单边）——
   两者合起来就是用户要的「一个方形 + 里面一个 Γ」 */
.status-grid.framed {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

.status-cell.span-2 {
  grid-column: 1 / -1;
}

.status-label {
  font-size: var(--fs-sm);
  color: var(--text-dim);
}

/* 「一上一下」的复合格（模板字段的 `stack`）：每组一行「小标签 + 值」。
   现在放 段状态 / 当前段状态 —— 两行挤在一格里，比拆成两格省一排 */
.status-stack {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 2px;
}

.stack-row {
  display: flex;
  align-items: baseline;
  gap: var(--gap-xs);
  white-space: nowrap;
  /* 行高收紧：两行合起来的高度与旁边「标签 + 值」那一格相当，
     居中对齐后才不会比旁边的格子高一截、上下错开 */
  line-height: 1.2;
}

/* 复合格整块左移 10px、右边留 10px 白：
   标签跟「跳段」按钮对齐（都从 242 起），破折号不贴着卡片右边（到 343 为止） */
.status-cell.stack-cell {
  margin-left: -10px;
  padding-right: 10px;
}

.stack-label {
  /* 和卡片里别的标签同一档；原来是 fs-xs，用户 2026-10-18 说太小 */
  font-size: var(--fs-sm);
  color: var(--text-dim);
}

.stack-value {
  /* 与其它读数同一档（fs-lg），只比标签稍小一点就看不出来了 */
  font-size: var(--fs-lg);
  font-weight: 600;
  /* 值**靠右对齐**：两行标签长短不一样（设备状态 / 当前段状态），
     不推右的话两个值会从不同位置起排，看着歪 */
  margin-left: auto;
}

/* 读数用等宽字体：tabular-nums 让每个数字占同样宽度，
   否则倒计时跳秒时整行会左右抖 */
.status-value,
.readonly .val {
  font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
}

.status-value {
  margin-top: 2px;
  font-size: var(--fs-lg);
  font-weight: 600;
}

/* 模板字段 `big: true`：数值再放大一档（目标温度 / 冷阱温度 / 真空度） */
.status-value.big {
  font-size: calc(var(--fs-lg) * 1.3);
}

/* 模板字段 `valueAlign: "center"`：数值在自己的框里**水平+垂直居中**，
   标签仍在左上角不动。格子拉满框高，数值吃掉剩下的高度 */
.status-cell.center-value {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.status-cell.center-value > .status-value.center {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
}

/* --------------------------------------------------------------- 跑马灯 */

/* 报警那一格可能是一串（多条报警拼起来的，见 `设备端对接文档.md` 4.2）：
   一行放不下 —— 截断看不全，换行会把整块卡片撑高，所以让它滚（用户 2026-10-16 定的）。
   ⚠️ **放得下就不滚**（`scrolling` 由 JS 按实际宽窄加，见 controls.js 的 marqueePaint）：
   一个「无」字在那儿来回晃，比截断还难受 */
.status-value.marquee {
  overflow: hidden;
  white-space: nowrap;
}

/* ⚠️ **`inline-block` 必须无条件加上，不能只在 `scrolling` 时才给。**
   量"放不放得下"靠的是这一层元素自己的宽度（见 controls.js 的 marqueePaint），
   而 `inline` 元素量出来的是**折行之后的一段**（真机量到 327，正好等于容器宽）——
   于是永远判"放得下"，那一格就再也不滚了。
   加上它之后，放不下时量到的是整串的真实宽度 ✓ */
.status-value.marquee > .marquee-text {
  display: inline-block;
}

.status-value.marquee.scrolling > .marquee-text {
  /* 从右边外面进来、滚完从左出去 —— 这样一圈接一圈，中间留个空档当分隔 */
  padding-left: 100%;
  /* 时长由 JS 按字数给（串越长越慢，走 --marquee-dur；没给就 10s），这里管轨迹 */
  animation: marquee-scroll var(--marquee-dur, 10s) linear infinite;
}

@keyframes marquee-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-100%); }
}

/* --------------------------------------------------------------- 七段数码管 */

/* 数字用七段拼出来（不是换字体）。段位排布：
        aaaa
       f    b
       f    b
        gggg
       e    c
       e    c
        dddd                                              */
.led-text {
  display: inline-flex;
  align-items: flex-end;
  gap: 3px;
}

.led {
  position: relative;
  display: inline-block;
  width: 0.74em;
  height: 1.18em;
}

/* 灭段不画：只有亮起来的段可见。画上暗灰的灭段会像给数字垫了一层影子 */
.led .seg {
  position: absolute;
  background: transparent;
  border-radius: 1px;
}

.led .seg.on {
  background: currentColor;    /* 亮段跟随字段配色 */
}

.led .a, .led .g, .led .d {
  left: 8%;
  width: 84%;
  height: 12%;
}

.led .a { top: 0; }
.led .g { top: 44%; }
.led .d { bottom: 0; }

.led .b, .led .c, .led .e, .led .f {
  width: 12%;
  height: 40%;
}

.led .b { right: 0; top: 4%; }
.led .c { right: 0; bottom: 4%; }
.led .e { left: 0; bottom: 4%; }
.led .f { left: 0; top: 4%; }

/* 小数点与单位：数码管拼不出这些，用普通字符贴着底部对齐 */
.led-dot {
  width: 0.14em;
  height: 0.14em;
  margin-bottom: 0.06em;
  border-radius: 50%;
  background: currentColor;
}

/* 单位（℃）用普通字体跟在数码管后面，额外留一点左间距 ——
   否则会贴着最后一位数字，看着像叠在一起 */
.led-plain {
  font-size: 0.55em;
  line-height: 1;
  padding-bottom: 0.05em;
  margin-left: 2px;
}

/* 有值时转告警色（正常时保持正文色） */
.status-value.danger {
  color: var(--st-error);
}

/* 数值配色。**按颜色取名** —— 模板里写的就是"这一格要什么颜色"，
   用语义名（cold/warm）反而对不上（一个真空度设定值叫 cold 说不通）。

   **只作用在数值上，标签一律统一**（用户 2026-10-13 定的）——
   渲染器只把 tone 类挂在数值那个元素上，所以这里的 .status-label 不受影响。

   下面三条等号左边是 v4~v6 老模板在用的语义名，保留成同一套颜色。 */
.tone-blue,   .tone-cold   { color: var(--tone-blue); }
.tone-yellow, .tone-warm   { color: var(--st-connecting); }
.tone-green,  .tone-target { color: var(--st-connected); }

/* 指示灯：绿=运行、灰=停止。比「运行中/停止」几个字好扫得多 */
.status-dot {
  width: 14px;
  height: 14px;
  margin-top: 6px;
  border-radius: 50%;
  background: var(--st-disconnected);
}

.status-dot.on {
  background: var(--st-connected);
  box-shadow: 0 0 8px var(--st-connected);
}

/* 挂在标签后面的灯（隔板那种：标签 + 通断指示）。
   灯跟在**标签**后面而不是数值后面 —— 数值宽度不等，跟数值就会一行一个位置 */
.status-dot.inline {
  margin-top: 0;
  flex: none;
}

/* 标签里放了灯的，标签自己排成一行 */
.status-label.has-dot {
  display: flex;
  align-items: center;
  gap: var(--gap-xs);
}

/* 隔板灯的亮色是**红**（用户 2026-10-13 定的：灰=0、红=1）——
   与「运行中」那种绿点区分开：这里说的是某一路通断，不是好/坏 */
.status-dot.dot-red.on {
  background: var(--st-error);
  box-shadow: 0 0 8px var(--st-error);
}

/* 居中对齐的控件（配方名）：水平靠 justify-content + 值内部 text-align，
   垂直靠 align-items —— 只有水平居中的话，值会贴在格子顶部 */
.ctl.align-center .readonly,
.ctl.align-center .ctl-value {
  justify-content: center;
  align-items: center;
  /* 高度沿用 .readonly 的 --tap，与旁边的按钮同档，两块底衬一样高。
     这里不能再写 min-height: 100%：行高是内容撑开的，百分比算不出来会
     退化成 auto，只读框缩成一行字高，就比按钮矮一截 */
}

.ctl.align-center .readonly .val,
.ctl.align-center .ctl-value .val {
  width: 100%;
  text-align: center;
}

/* 同一行里的按钮跟着旁边的只读框一样高，否则一个高一个矮，看着不齐 */
.ctl .btn-block {
  height: 100%;
}

/* --------------------------------------------------------------- 启动 / 停止 */

/* 两种状态用不同底色，字号也放大一档：这是主页上最主要的操作按钮，
   远远瞄一眼就得看清设备在不在跑、以及该按哪个 */
.btn-start,
.btn-stop {
  font-size: var(--fs-lg);
  font-weight: 600;
}

.btn-start {
  background: var(--st-connected);
  border-color: var(--st-connected);
  color: var(--on-accent);
}

.btn-stop {
  background: var(--st-error);
  border-color: var(--st-error);
  color: var(--on-accent);
}

/* --------------------------------------------------------------- 跳段 */

/* 内部 2 列，**与卡片第 2、3 列同宽**（外层跨两列、列间距相同，所以能严丝合缝）：
   标签和输入框在第 1 列，按钮在第 2 列 —— 按钮正好落在「段状态」那一列下面。
   列间距必须是同一个 `--gap`，否则上下两张卡片的列会错开 */
.stage-jump {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: var(--gap);
  row-gap: var(--gap-xs);
  align-items: center;
}

.stage-jump-label {
  grid-column: 1;
  grid-row: 1;
  color: var(--text-dim);
  font-size: var(--fs-sm);
  white-space: nowrap;
}

.stage-jump-input {
  grid-column: 1;
  grid-row: 2;
  /* 段号最多两位（1~30）：框只开那么大，字体放大到与读数同档 */
  width: 2.8em;
  /* 比标准输入框矮几档（44 → 26）：它只是个数字格，和右边 44 高的按钮并排时
     一样高会显得笨重（用户 2026-10-18 定的） */
  min-height: 26px;
  height: 26px;
  padding: 0 var(--gap-xs);
  text-align: center;
  font-size: var(--fs-lg);
  font-weight: 600;
}

/* 按钮跨两行、在两行中间垂直居中 —— 跟左边的「标签 + 输入框」当一个整体看，
   而不是只在第二行、比标签低半截 */
.stage-jump-btn {
  grid-column: 2;
  grid-row: 1 / 3;
  align-self: center;
  /* 往左多出 10px（与上面「设备状态」标签对齐）、右边保持贴卡片右缘 ——
     左移 + 拉长，而不是整体右移 */
  margin-left: -10px;
  width: calc(100% + 10px);
  padding: 0 var(--gap-xs);
}

/* --------------------------------------------------------------- 静态文字 */

/* `type: "text"`：画面里的大标题，现在是化霜画面的「化霜中」 */
.ctl-text {
  padding: var(--gap-sm) 0;
  font-size: var(--fs-xl);
  font-weight: 600;
  text-align: center;
}

.ctl-text.tone-primary { color: var(--primary); }
.ctl-text.tone-danger  { color: var(--st-error); }

/* --------------------------------------------------------------------------- 开关 */

.switch {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
}

.switch input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.switch .track {
  width: 52px;
  height: 30px;
  border-radius: 15px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  position: relative;
  transition: background 0.15s, border-color 0.15s;
}

.switch .knob {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--text-dim);
  transition: transform 0.15s, background 0.15s;
}

.switch input:checked + .track {
  background: var(--primary);
  border-color: var(--primary);
}

.switch input:checked + .track .knob {
  transform: translateX(22px);
  background: var(--on-accent);
}

.switch input:disabled + .track {
  opacity: 0.45;
}

/* --------------------------------------------------------------------------- 数值步进器 */

.stepper {
  display: grid;
  grid-template-columns: var(--tap) 1fr var(--tap);
  align-items: center;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg);
  overflow: hidden;
}

.stepper button {
  height: var(--tap);
  font-size: var(--fs-lg);
  color: var(--primary);
}

.stepper button:disabled {
  color: var(--text-dim);
  opacity: 0.5;
}

.stepper .val {
  text-align: center;
  font-size: var(--fs-lg);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.stepper .unit {
  font-size: var(--fs-xs);
  color: var(--text-dim);
  margin-left: 2px;
  font-weight: 400;
}

/* --------------------------------------------------------------------------- 枚举分段控件 */

.segment {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--bg);
}

.segment button {
  min-height: var(--tap);
  font-size: var(--fs-sm);
  color: var(--text-dim);
  border-left: 1px solid var(--border);
}

.segment button:first-child {
  border-left: 0;
}

.segment button.active {
  background: var(--primary);
  color: var(--on-accent);
  font-weight: 600;
}

/* --------------------------------------------------------------------------- 只读显示 */

.readonly {
  min-height: var(--tap);
  display: flex;
  align-items: baseline;
  gap: 2px;
  padding: 0 var(--gap-sm);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}

.readonly .val {
  font-size: var(--fs-lg);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.readonly .unit {
  font-size: var(--fs-xs);
  color: var(--text-dim);
}

/* 可点的只读值（模板里带 navigate，如配方名）：右侧给一个引导符，
   让用户知道这块能点开。引导符必须绝对定位 —— 放进流里会把居中的值挤偏 */
.readonly.tappable {
  position: relative;
  cursor: pointer;
  /* 给右侧引导符让位 */
  padding-right: var(--gap-lg);
}

/* 居中的只读值左右留一样宽，文字才仍然是正中间（只让右边会让它偏左） */
.ctl.align-center .readonly.tappable {
  padding-left: var(--gap-lg);
}

.readonly.tappable .go {
  position: absolute;
  right: var(--gap-sm);
  top: 50%;
  transform: translateY(-50%);
  color: var(--primary);
  font-size: var(--fs-lg);
  line-height: 1;
}

.readonly.tappable:active {
  opacity: 0.75;
}

/* 服务端下发了 App 不认识的控件类型时的降级占位 */
.ctl-unsupported {
  min-height: var(--tap);
  display: flex;
  align-items: center;
  padding: 0 var(--gap-sm);
  border: 1px dashed var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-dim);
  font-size: var(--fs-sm);
}

/* --------------------------------------------------------------------------- 状态点 / 徽标 */

.dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  display: inline-block;
  background: var(--st-disconnected);
}

.dot.online {
  background: var(--st-connected);
}

.dot.offline {
  background: var(--st-disconnected);
}

.badge {
  display: inline-block;
  padding: 1px 6px;
  border-radius: 4px;
  font-size: var(--fs-xs);
  background: var(--surface-2);
  color: var(--text-dim);
}

.badge-ok {
  color: var(--st-connected);
  background: var(--ok-bg);
}

.badge-warn {
  color: var(--st-connecting);
  background: var(--warn-bg);
}

.badge-err {
  color: var(--st-error);
  background: var(--err-bg);
}

/* --------------------------------------------------------------------------- 空态 / 加载 */

.empty {
  padding: var(--gap-lg) var(--gap);
  text-align: center;
  color: var(--text-dim);
}

/* 内联 SVG 图标：颜色跟随所在元素的 color */
.icon {
  display: block;
  flex: none;
}

/* 图标按钮/图标位：原来放的是文字，靠 line-height 对齐；换成 SVG 后要显式居中 */
.icon-btn,
.tabbar .ico,
.trash-icon,
.device-head .dev-icon {
  display: flex;
  align-items: center;
  justify-content: center;
}

.empty .big {
  /* 图标从文字换成 SVG 后不再有字号撑开，居中得自己来 */
  display: flex;
  justify-content: center;
  margin-bottom: var(--gap-sm);
  opacity: 0.6;
}

/* 加载指示**整个删掉了**（含 `.spinner`、`@keyframes spin`）。

   它的历史：最早是铺满全屏、带暗底的遮罩（请求慢一点整个界面就点不动，
   "转圈不能操作"就是它）→ 改成顶栏下的小胶囊 → 现在**彻底不要**。

   用户 2026-10-13 定的：**加载就该静默进行** —— 页面照常能看能点，数据回来
   自己填上，切页时也不该闪一下圆圈。`App.ui.loading()` 保留成空实现
   （三十多处调用不用动），它**不再渲染任何东西**。
   **要恢复记得**：别只加回样式，`ui.loading` 里那段创建 DOM 的也要加回来。 */

/* --------------------------------------------------------------------------- toast */

/* 固定在**顶部、顶栏下方**（用户 2026-10-18 定的，原来贴底）。
   贴底会压在页面的操作区上；顶部这一条只盖住几行，不挡正在看的画面。
   位置写死，不再随底部导航栏（body.has-tabbar）上下跑。 */
.toast-layer {
  position: fixed;
  left: 0;
  right: 0;
  top: calc(var(--topbar-h) + env(safe-area-inset-top, 0) + var(--gap-sm));
  display: grid;
  justify-items: center;
  gap: var(--gap-xs);
  z-index: 60;
  pointer-events: none;
}

.toast {
  max-width: calc(100% - var(--gap) * 2);
  padding: var(--gap-sm) var(--gap);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  font-size: var(--fs-sm);
  box-shadow: var(--shadow);
}

.toast.error {
  border-color: var(--st-error);
}

/* 弹窗出现：从上沿淡入。**只是一次淡入，中间不要回暗** ——
   原来在 72% 处压到 opacity 0.3 想"闪一下"表示切换，真机上看起来像
   同一条弹窗消失后又弹了一次（用户 2026-10-18 报）。
   连续触发时旧条淡出 → 0.2 秒间隔 → 新条进来，切换本身已经看得清 */
@keyframes toast-in {
  0%   { opacity: 0; transform: translateY(-6px); }
  100% { opacity: 1; transform: translateY(0); }
}

.toast.toast-in {
  animation: toast-in 0.42s ease-out 1;
}

.toast.ok {
  border-color: var(--st-connected);
}

/* --------------------------------------------------------------------------- 底部弹层（确认框） */

.sheet-layer {
  position: fixed;
  inset: 0;
  background: var(--overlay);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  z-index: 50;
}

.sheet {
  width: 100%;
  max-width: var(--max-w);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  padding: var(--gap);
  padding-bottom: calc(var(--gap) + env(safe-area-inset-bottom, 0));
  display: grid;
  gap: var(--gap-sm);
  animation: sheet-up 0.18s ease-out;
}

@keyframes sheet-up {
  from {
    transform: translateY(16px);
    opacity: 0;
  }
}

.sheet h3 {
  font-size: var(--fs-lg);
}

.sheet .sheet-text {
  color: var(--text-dim);
  font-size: var(--fs-sm);
}

/* --------------------------------------------------------------------------- 设置类列表行 */

.rows {
  display: grid;
}

.row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--gap-sm);
  min-height: var(--tap);
  padding: var(--gap-xs) 0;
}

.row + .row {
  border-top: 1px solid var(--border);
}

.row .row-label {
  font-size: var(--fs-base);
}

.row .row-value {
  color: var(--text-dim);
  font-size: var(--fs-sm);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  max-width: 45vw;
}

/* ---------------------------------------------------------------------------
   机型图片 / 视频（机型详情页）
   两列缩略图；视频盖一个播放三角，点开后在 #layer 里全屏看/播。
   --------------------------------------------------------------------------- */
.asset-gallery {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--gap-sm);
}

.asset-tile {
  display: flex;
  flex-direction: column;
  padding: 0;
  overflow: hidden;
  text-align: left;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: inherit;
}

.asset-tile img,
.asset-tile .asset-media video {
  display: block;
  width: 100%;
  height: 120px;
  object-fit: cover;
  background: #000;
}

.asset-media { position: relative; }

/* 自定义图片卡片（产品详情页排版里可加）：整宽大图，点开全屏 */
.product-image-card {
  display: block;
  width: 100%;
  padding: 0;
  margin-bottom: 12px;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: inherit;
}

.product-image-card img {
  display: block;
  width: 100%;
  max-height: 280px;
  object-fit: cover;
}

/* 分组卡片（点开二级页面）与里面的文件行 */
.product-group-card {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 12px;
  margin-bottom: 12px;
  text-align: left;
  color: inherit;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.product-group-cover {
  flex: 0 0 auto;
  width: 72px;
  height: 72px;
  object-fit: cover;
  border-radius: 10px;
  background: #000;
}

.product-group-main { flex: 1; min-width: 0; }
.product-group-title { font-weight: 600; font-size: 15px; }
.product-group-summary { margin-top: 2px; color: var(--dim); font-size: 12px; }

.product-file-row {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 12px;
  margin-bottom: 12px;
  text-align: left;
  color: inherit;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.product-file-icon { font-size: 22px; }
.product-file-main { flex: 1; min-width: 0; }
.product-file-name { font-weight: 600; }
.product-file-meta {
  font-size: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.product-video-card { margin-bottom: 12px; padding: 0; overflow: hidden; }
.product-video-card video {
  display: block;
  width: 100%;
  max-height: 280px;
  background: #000;
}

.asset-play {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: #fff;
  font-size: 28px;
  text-shadow: 0 2px 8px rgba(0, 0, 0, .65);
  pointer-events: none;
}

.asset-caption {
  padding: 6px 8px;
  color: var(--text-dim);
  font-size: var(--fs-xs);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.asset-viewer {
  position: fixed;
  inset: 0;
  z-index: 1200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  background: rgba(0, 0, 0, .94);
}

.asset-viewer-media {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

.asset-viewer-close {
  position: absolute;
  top: calc(12px + env(safe-area-inset-top, 0px));
  right: 14px;
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: rgba(255, 255, 255, .16);
  color: #fff;
  font-size: 24px;
  line-height: 1;
}

/* ---------------------------------------------------------------------------
   扩展控件：滑块 / 下拉 / 文本 / 进度 / 趋势
   --------------------------------------------------------------------------- */
.slider { display: flex; align-items: center; gap: var(--gap-sm); }
.slider input[type="range"] {
  flex: 1;
  min-width: 0;
  height: var(--tap);
  accent-color: var(--primary);
}
.slider-value {
  display: flex;
  align-items: baseline;
  justify-content: flex-end;
  gap: 2px;
  min-width: 56px;
}
.slider-value .val { font-size: var(--fs-lg); font-weight: 600; }
.slider-value .unit { color: var(--text-dim); font-size: var(--fs-xs); }

.ctl-select {
  width: 100%;
  min-height: var(--tap);
  padding: 8px 10px;
  font: inherit;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.ctl-input {
  width: 100%;
  min-height: var(--tap);
  padding: 8px 10px;
  font: inherit;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  outline: none;
}
.ctl-input:focus { border-color: var(--primary); }
.ctl-textarea { min-height: 88px; resize: vertical; line-height: 1.5; }

.progress { display: flex; align-items: center; gap: var(--gap-sm); }
.progress-track {
  flex: 1;
  height: 8px;
  overflow: hidden;
  background: var(--surface-2);
  border-radius: 999px;
}
.progress-fill {
  width: 0;
  height: 100%;
  background: var(--primary);
  border-radius: 999px;
  transition: width .2s;
}
.progress-text {
  min-width: 56px;
  color: var(--text-dim);
  font-size: var(--fs-sm);
  text-align: right;
}

.trend { display: grid; gap: 6px; }
.trend-canvas {
  display: block;
  width: 100%;
  height: 120px;
  background: var(--surface-2);
  border-radius: var(--radius-sm);
}
.trend-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  color: var(--text-dim);
  font-size: var(--fs-xs);
}
.trend-key { display: inline-flex; align-items: center; gap: 4px; }
.trend-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; }
