html, body { height: 100%; }
body { margin: 0; background: var(--color-bg); color: var(--color-text); font-family: var(--font-body); }
#app { min-height: 100%; }

.satu-authside { position: relative; padding: var(--space-8); display: flex; flex-direction: column; align-items: center; justify-content: space-between; background: var(--color-surface); overflow: hidden; }
.gw-login { min-height: 100vh; display: grid; grid-template-columns: 1fr 1fr; background: var(--color-bg); font-family: var(--font-body); color: var(--color-text); }
.gw-flash { margin: 0; padding: 10px var(--space-3); border-radius: var(--radius-sm); font-size: 13px; line-height: 1.6; }
.gw-flash-err { color: var(--color-accent-800); background: var(--color-accent-100); }
.gw-flash-ok { color: var(--color-accent-2-800); background: var(--color-accent-2-100); }
/* 带 × 的那种（flashRow 画的）才排成一行。选择器问的是「有没有那颗按钮」，因为
   同一个 .gw-flash 还被几处当纯说明块用（下面那条 note、机器失联那条），它们没有
   按钮，也就不该被改成 flex——那会把里面并排的字和小按钮重新排一遍。 */
.gw-flash:has(.gw-flash-x) { display: flex; align-items: flex-start; gap: var(--space-2); }
.gw-flash-text { flex: 1; min-width: 0; }
/* × 只借提示条自己的颜色：这条本来就是一块有色的面，再给按钮描边填底就是两层。
   平时压半透明，指上去才实——它是「读完了可以撤掉」，不是这条提示的主角。 */
.gw-flash-x {
  flex: none;
  margin: 1px -4px 0 0;
  padding: 2px;
  display: inline-flex;
  align-items: center;
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  color: inherit;
  opacity: 0.6;
  cursor: pointer;
}
.gw-flash-x:hover { opacity: 1; }
/* 既不是错也不是成功，是一句说明。搜索模式那条用它——用红的会被读成「出事了」。 */
.gw-flash-note { color: var(--foreground); background: var(--color-neutral-200); }

.satu-provrow { cursor: pointer; }
.satu-provrow[aria-selected="true"] { background: var(--color-accent-100); }
.satu-provrow[aria-selected="true"]:hover { background: var(--color-accent-100); }

.gw-roles { position: sticky; top: 0; z-index: 3; display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); padding: var(--space-2) 0 var(--space-4); background: var(--color-neutral-100); }
.satu-modelhead, .satu-modelrow { grid-template-columns: 2fr 1fr 1.1fr 0.9fr 1.1fr 1.1fr 168px; }
.gw-caps { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.satu-modelrow .satu-rowactions { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }

.gw-credlist .satu-provhead,
.gw-credlist .satu-provrow { grid-template-columns: minmax(160px, 2fr) 72px 96px minmax(140px, 1.4fr) minmax(300px, auto); }
.gw-credlist .satu-provrow { cursor: default; }
.gw-secret { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
.gw-secret .input { border-radius: var(--radius-md); padding: 8px 12px; }
.gw-provactions { display: flex; align-items: center; justify-content: flex-end; gap: var(--space-2); flex-wrap: wrap; }
/* 测试结论独占一行。跟按钮抢横向空间的话，一出结果整排按钮就会换行错位。 */
.gw-provactions .gw-testline { flex-basis: 100%; text-align: right; }

.gw-page { flex: 1; min-height: 0; overflow-y: auto; padding: var(--space-6); }
.gw-page-inner { max-width: 980px; margin: 0 auto; display: flex; flex-direction: column; gap: var(--space-6); }
.gw-page-inner.narrow { max-width: 860px; }
.gw-page-inner.gw-profile { max-width: 760px; }

.satu-userrow { width: 100%; }
.gw-logout { flex: none; padding: 0; border: 0; background: transparent; font-family: var(--font-body); font-size: 12px; font-weight: 600; color: var(--color-accent-700); cursor: pointer; }
.gw-logout:hover { color: var(--color-accent-600); }

@media (max-width: 900px) {
  .gw-login { grid-template-columns: 1fr; }
  .satu-authside { display: none; }
  .gw-roles { grid-template-columns: 1fr; }
}

.gw-modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 25;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-6);
  background: color-mix(in srgb, var(--color-neutral-900) 42%, transparent);
}
.gw-modal {
  width: 100%;
  max-width: 440px;
  box-sizing: border-box;
  background: var(--popover);
  color: var(--color-text);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.gw-modal .btn-primary {
  background: var(--color-accent-500);
  border-color: var(--color-accent-500);
}

.satu-menuitem:disabled { opacity: 0.45; cursor: not-allowed; }
@media (max-width: 800px) {
  .gw-page-inner > div[style*="repeat(4, 1fr)"] { grid-template-columns: 1fr 1fr !important; }
}

/* main：两行（header / 正文）两列（内容 / 右栏）。右栏跨两行，所以能顶到 header
   那一行去——做成 header 下面的兄弟节点就顶不上去。列宽由 appView 内联给出。 */
.gw-main { min-width: 0; height: 100%; min-height: 0; box-sizing: border-box; overflow: hidden;
  background: var(--color-neutral-100); border: 1px solid var(--color-divider); border-radius: var(--radius-md);
  display: grid; grid-template-rows: auto minmax(0, 1fr); }
.gw-head { grid-row: 1; grid-column: 1; display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3) var(--space-6); border-bottom: 1px solid var(--color-divider); }
.gw-body { grid-row: 2; grid-column: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
.gw-aside { grid-row: 1 / span 2; grid-column: 2; position: relative; min-width: 0; overflow: hidden;
  border-left: 1px solid var(--color-divider); display: flex; flex-direction: column; }
/* 右栏没有自己的 header：开关在对话 header 上，所以这里不会有一条和左边对不齐的
   横线。整栏从顶上直接开始，顶部内边距和 .gw-head 一致，标题看起来就在同一行。 */
.gw-aside-body { flex: 1; min-height: 0; overflow-y: auto; padding: var(--space-3) var(--space-4) var(--space-4);
  display: flex; flex-direction: column; gap: var(--space-3); }
/* 「运行环境」那一叠：右栏的常态。点开一条日常任务时它整个 hidden，但**不从 DOM 里
   拿掉**——里面那块屏是一条 VNC 连接，卸一次就断一次（见 render.js 的 pageAside）。 */
.gw-aside-stack { display: flex; flex-direction: column; gap: var(--space-3); }
/* `display: flex` 会盖掉 [hidden] 自带的 display:none（作者样式赢 UA 样式）。少了
   这一行，点开一条日常任务时那一叠还在原地，两屏叠着画。 */
.gw-aside-stack[hidden] { display: none; }
.gw-aside-stack > h3 { margin: 0; font-size: 12px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--muted-foreground); line-height: 30px; }
/* 抓手压在左边框上：可视只有 1px 的边框，但有 7px 的命中区，不然很难抓准。 */
.gw-aside-grip { position: absolute; left: -3px; top: 0; bottom: 0; width: 7px; cursor: col-resize; z-index: 2; }
.gw-aside-grip:hover { background: color-mix(in srgb, var(--color-accent-500) 35%, transparent); }
/* 预览摆在右栏时整栏让给它：不要外层的滚动（预览自己的内容框会滚），也不要外层的内边距
   （预览那一块自己留）。外层也滚的话，一份长 PDF 是两层滚动条套在一起。 */
.gw-aside[data-preview] .gw-aside-body { overflow: hidden; padding: 0; }
/* 拖右栏的时候 iframe 不接鼠标，不然一划进预览里的 PDF / HTML，mousemove 就收不到了。 */
.gw-dragging iframe { pointer-events: none; }
.gw-aside .satu-kv { display: block; padding: 0; border: 0; }
.gw-aside .satu-kv > span:first-child { display: block; font-size: 11px; color: var(--muted-foreground); }
/* 这些值是机器生成的长串（uid、路径、版本号），必须能断行，否则会把整栏撑开。 */
.gw-aside .satu-kv > span:last-child { display: block; font-size: 12px; word-break: break-all; line-height: 1.5; }

.gw-chat { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
/* 窄屏收掉右边那栏：环境信息是辅助的，对话本身不能被挤扁。 */
/* 这些值是机器生成的长串（uid、路径、版本号），必须能断行，否则会把整栏撑开。 */
/* flex: 1 不能少。.gw-chat 从 grid 改成 flex 列之后，没有它主区就只有内容高度，
   输入框会被顶到对话正文底下而不是贴着页面底部。 */
.gw-chat-main { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
/* 消息流、气泡、输入区在 chat.css。这里只留对话页的外框。 */
.gw-chat-empty, .gw-chat-empty-main { margin: 0; padding: var(--space-4); font-size: 14px; color: var(--muted-foreground); line-height: 1.6; }
.gw-chat-empty-main { flex: 1; display: flex; align-items: center; justify-content: center; }
/* ── 侧栏顶层的 Bot 名单 ──────────────────────────────────────────────
   做成聊天列表的样子：头像、名字、最近回复的时间、一行摘要。它们是这个产品的主体，
   不该藏在一个还要先点开的导航项底下。 */
/* 名单占满剩余高度、自己滚。Bot 多的时候滚的是名单，而不是把下面的入口顶出屏幕。 */
.satu-botlist { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; }
/* 「新建 Bot」。贴在名单下沿、跟名单一样宽——它是名单的动作，不是又一个导航项，
   所以用虚线边框跟上面那些实心的行区分开。收成窄栏（.satu-rail）时只剩那个加号。 */
.satu-newbot {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  box-sizing: border-box;
  margin-top: 4px;
  padding: 8px var(--space-3);
  border: 1px dashed var(--color-divider);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--muted-foreground);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}
.satu-newbot:hover { color: var(--color-text); border-color: var(--color-text); }
.satu-rail .satu-newbot { justify-content: center; }
.satu-rail .satu-newbot span { display: none; }
/* ── 桌面端有新版本（shell.js 的 desktopUpdateInner）──────────────────
   沉在侧栏底部、头像那一行上面。实心的强调色底：它是这一栏里唯一一条「要你去点」的东西，
   跟虚线的「新建 Bot」、灰色的导航都不一样。空槽不占高度。 */
.satu-dupdate-slot:empty { display: none; }
.satu-dupdate-slot { flex: none; margin-top: var(--space-2); }
.satu-dupdate {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  box-sizing: border-box;
  padding: 8px var(--space-3);
  border: 1px solid color-mix(in srgb, var(--primary) 35%, transparent);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--primary) 10%, transparent);
  color: var(--color-text);
  font: inherit;
  text-align: left;
}
button.satu-dupdate { cursor: pointer; }
button.satu-dupdate:hover { background: color-mix(in srgb, var(--primary) 16%, transparent); }
.satu-dupdate-icon { flex: none; display: flex; color: var(--primary); }
.satu-dupdate-text { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 3px; line-height: 1.25; }
.satu-dupdate-text strong { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.satu-dupdate-text small { font-size: 11px; color: var(--muted-foreground); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.satu-dupdate-text small[data-error="true"] { color: var(--color-accent-700); }
.satu-dupdate-bar { display: block; height: 4px; border-radius: 999px; background: color-mix(in srgb, var(--primary) 18%, transparent); overflow: hidden; }
.satu-dupdate-bar i { display: block; height: 100%; background: var(--primary); border-radius: inherit; transition: width .3s ease; }
.satu-dupdate-bar[data-indeterminate="true"] i { animation: satu-dupdate-slide 1.2s ease-in-out infinite; }
@keyframes satu-dupdate-slide { from { transform: translateX(-100%); } to { transform: translateX(340%); } }
/* 窄栏只剩图标，居中；字藏掉，悬停看 title。 */
.satu-rail .satu-dupdate { justify-content: center; padding: 8px 0; }
.satu-rail .satu-dupdate-text { display: none; }

/* 「新建 Bot」和它右边那颗「更多」并排一行。
   **position: relative 是给弹出菜单当锚的**（.satu-menu 是 absolute）：少了它，菜单
   会跑去贴最近的定位祖先，在这一列里就是整个视口。 */
.satu-newbotrow { position: relative; flex: none; display: flex; align-items: stretch; gap: 4px; }
.satu-newbotrow .satu-newbot { flex: 1; min-width: 0; }
/* 「更多」那颗。**选择器要带上 .satu-newbotrow**：上面那条给行内每颗按钮写了
   `flex: 1`，权重比单独一个类名高，光写 .satu-newbot-more 的话 `flex: none` 压不过
   它——两颗于是平分一行，「新建 Bot」被挤成半行。 */
.satu-newbotrow .satu-newbot-more { flex: none; width: 34px; justify-content: center; padding: 8px 0; }
/* 导轨那一档不用菜单，摊成两行（见 render.js 里那段）。所以这两颗平时藏着。 */
.satu-newbot-rail { display: none; }
.satu-rail .satu-newbotrow { flex-direction: column; gap: 0; }
.satu-rail .satu-newbotrow .satu-newbot-more { display: none; }
.satu-rail .satu-newbot-rail { display: flex; width: 100%; }
/* 菜单项带图标的那种：和它收起来之前那两颗按钮长得一样，人才认得出是同两条。 */
.satu-menuitem-icon { display: flex; align-items: center; gap: var(--space-2); }
.satu-menuitem-icon svg { flex: none; opacity: 0.75; }
/* 页面入口沉在底部，不跟着名单滚。 */
.satu-navfoot { flex: none; display: flex; flex-direction: column; gap: 2px; padding-top: 2px; }
/* 没有 Bot 名单的那一侧（平台管理员）——菜单就是这一列的全部内容。十几条再加上分组
   标题，矮一点的屏就装不下了，而 aside 是 overflow:hidden，装不下等于**直接裁掉**，
   最底下那几条谁都点不到。所以这一侧让它自己滚。
   有名单时不能这么写：那时 navfoot 必须 flex:none 沉在底部，一旦可压缩就会被名单
   挤扁。两种情形靠「有没有名单」区分，正是 :has() 问的那件事。 */
aside:not(:has(.satu-botlist)) .satu-navfoot { flex: 0 1 auto; min-height: 0; overflow-y: auto; }
.satu-botrow {
  display: flex;
  /* 头像跨两行，所以竖直居中——只有一行（还没说过话的 Bot）时也一样居中，
     行高从 34 塌到 34，看着不会跳。 */
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  box-sizing: border-box;
  padding: 8px var(--space-3);
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  cursor: pointer;
  font-family: var(--font-body);
  text-align: left;
  color: color-mix(in srgb, var(--color-text) 78%, transparent);
}
.satu-botrow:hover { background: color-mix(in srgb, var(--color-text) 6%, transparent); color: var(--color-text); }
.satu-botrow[aria-current="true"] { background: var(--color-accent); color: var(--color-bg); }
.satu-botrow[aria-current="true"]:hover { background: var(--color-accent-600); }
.satu-botrow:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.satu-botavatar { flex: none; display: flex; align-items: center; }
/* min-width: 0 是省略号能生效的前提——不写的话 flex 子项按内容宽度算，长名字会把
   时间挤出侧栏。 */
.satu-botmain { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.satu-botline { display: flex; align-items: baseline; gap: 7px; min-width: 0; }

/* 本地 Bot 是运行边界，不是一句次要说明。徽标在侧栏和会话顶栏复用，使用有底色、有
   描边的形状，避免像原来那行 10px 灰字一样和摘要混在一起。 */
.satu-localtag {
  display: inline-flex;
  align-items: center;
  flex: none;
  padding: 2px 7px;
  border: 1px solid var(--color-accent-300);
  border-radius: 999px;
  background: var(--color-accent-100);
  color: var(--color-accent-800);
  font-size: 10.5px;
  font-weight: 700;
  line-height: 1.2;
  vertical-align: middle;
  white-space: nowrap;
}
.satu-localtag[data-compact] { padding: 1px 5px; font-size: 9.5px; }
.satu-botrow[aria-current="true"] .satu-localtag {
  border-color: color-mix(in srgb, var(--color-bg) 58%, transparent);
  background: color-mix(in srgb, var(--color-bg) 18%, transparent);
  color: var(--color-bg);
}

/* ── 状态点 ────────────────────────────────────────────────────────
   三态靠**形状**分，不只靠颜色：空闲是空心圈、执行中是实心并闪、待人工是实心琥珀。
   只用颜色的话，色觉障碍的人看到的是三个一模一样的点；而且侧栏选中行会整条染成
   品牌色，压在上面的纯色差别更难分。 */
.satu-botdot {
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 999px;
  border: 1.5px solid currentColor;
  opacity: 0.4;
  align-self: center;
  transition: opacity 120ms ease, background 120ms ease;
}
.satu-botdot[data-state="busy"] {
  background: currentColor;
  opacity: 1;
  animation: satu-blink 1.15s ease-in-out infinite;
}
.satu-botdot[data-state="review"] {
  background: var(--color-warn-500);
  border-color: var(--color-warn-500);
  opacity: 1;
  animation: none;
}
@keyframes satu-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.22; } }
@media (prefers-reduced-motion: reduce) {
  .satu-botdot[data-state="busy"] { animation: none; }
}
/* 收起侧栏时整行只剩头像，点跟着收掉——它挤在图标旁边只会让图标看着歪。 */
.satu-rail .satu-botdot { display: none; }

/* ── 机器通联指示灯 ──────────────────────────────────────────────────
   和上面的 botdot 同一条规矩：**四态靠形状分，不只靠颜色**。在线是实心、迟了是实心
   带闪、失联是空心粗环、没配对是空心虚线。色觉障碍的人只看颜色会看到四个一样的点，
   而这盏灯的全部意义就是「一眼扫过去哪台不对」。

   灯旁边永远跟着一句状态文字，那才是权威——灯只负责让人扫得快。 */
.satu-linkdot {
  flex: none;
  width: 9px;
  height: 9px;
  border-radius: 999px;
  border: 2px solid var(--muted-foreground);
  align-self: center;
}
.satu-linkdot[data-link="online"] {
  background: var(--color-ok-500);
  border-color: var(--color-ok-500);
}
.satu-linkdot[data-link="stale"] {
  background: var(--color-warn-500);
  border-color: var(--color-warn-500);
  animation: satu-blink 1.6s ease-in-out infinite;
}
/* 失联是**空心**：实心配深色在浅背景上反而不显眼，而空心粗环和上面两个实心点在
   形状上一眼分得开。 */
.satu-linkdot[data-link="offline"] {
  background: transparent;
  border-color: var(--destructive);
}
.satu-linkdot[data-link="unpaired"] {
  background: transparent;
  border-style: dashed;
  border-color: var(--muted-foreground);
}
/* 说不上来的时候就画得说不上来：实心灰，既不像在线的绿，也不像没配对的虚线。 */
.satu-linkdot[data-link="unknown"] {
  background: var(--muted-foreground);
  border-color: var(--muted-foreground);
  opacity: 0.45;
}
@media (prefers-reduced-motion: reduce) {
  .satu-linkdot[data-link="stale"] { animation: none; }
}

/* ── 占用条（CPU / 内存 / 磁盘 / journal）────────────────────────────
   **叫 gauge 不叫 meter**：shell.css 里早就有一套 `.satu-meter` / `.satu-meterfill`
   （Bot 统计页那几条按比例撑满宽度的进度条）。同名而不同形状，晚来的这套会顺着
   层叠盖上去——把那边的进度条从「撑满容器」压成 72px 的一小截，而那一页谁也没动过。
   三档颜色，门槛 75% 和 90%。**颜色不是唯一信息**：条子旁边永远跟着一个百分数，
   跟通联灯那条规矩一样——只靠颜色的话，色觉障碍的人看到的是三根一模一样的条。

   门槛定在 75/90 而不是 90/95：这一页的用处是在机器出事**之前**看见它，而一块盘
   从 90% 到写满往往只要几个小时，那时 bot 起不来、连日志都写不进去。 */
.satu-gauge {
  flex: none;
  display: inline-block;
  width: 72px;
  height: 6px;
  border-radius: 999px;
  background: var(--secondary);
  overflow: hidden;
  vertical-align: middle;
}
.satu-gauge > span {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--color-ok-500);
}
.satu-gauge[data-level="warn"] > span { background: var(--color-warn-500); }
.satu-gauge[data-level="hot"] > span { background: var(--destructive); }
/* 算不出百分比（还没采到、或者压根没有上限可比）：空心虚线槽。**不能画成实心的
   0%**——那是「量到了，就是 0」，和「没量到」是完全相反的两个结论，跟通联灯里
   「失联是空心、没配对是虚线」同一条规矩。 */
.satu-gauge[data-level="none"] {
  background: transparent;
  border: 1px dashed var(--border);
  opacity: 0.6;
}

/* 机器卡片的头一行：灯、编号、短码、状态文字。编号和短码用等宽体——它们是用来
   照着念和比对的，比例字体下 6a9434e2 里的 0/o、1/l 分不清。 */
.satu-machinehead {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-bottom: var(--space-2);
}
.satu-machineno {
  font-weight: 600;
}
.satu-machineid {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--muted-foreground);
}
/* 刷新推到最右。头一行是 flex-wrap 的，窄屏上它会自己换行到下一行的右端——
   比固定定位好：绝对定位到卡片右上角，窄屏时会压在状态文字上。 */
.satu-machinerefresh {
  margin-left: auto;
}
.satu-botname {
  flex: 1;
  min-width: 0;
  font-size: 13.5px;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.satu-botrow[aria-current="true"] .satu-botname { font-weight: 600; }
/* 「有事等你」的图标。**点之外还要有它**：8px 的状态点在一列名字里太容易滑过去，
   而这一格说的是「你不去点，那一轮就一直停在席位上等着」。

   **画成一颗实心徽章，不是一个描边图形。** 13px 的细描边压在奶油底上，和旁边那颗
   同样琥珀的状态点糊成一团——远看就是两个小点，人扫过去根本不会停。填满之后它是这
   一列里唯一的色块。
   
   底色用 500、图形用 100：这套色阶在深色主题里是**翻过来的**（100 是深的、800 是浅
   的），所以 500 底 + 100 图形是唯一一组两个主题下都够对比的搭配——写死白色的话，
   深色主题里就是浅琥珀底压白图形。 */
.satu-botneed {
  /* 底色和图形色走两个变量：选中的那一行要整组换掉（见下面那条），而光晕的颜色
     跟着底色走——写死 warn-500 的话，翻过来的那一行会甩出一圈琥珀光晕。 */
  --need-ink: var(--color-warn-500);
  --need-glyph: var(--color-warn-100);
  flex: none;
  align-self: center;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  border-radius: 999px;
  background: var(--need-ink);
  color: var(--need-glyph);
  animation: satu-need-pulse 1.5s ease-in-out infinite;
}
/**
 * **选中的那一行要翻过来。**
 *
 * 那一行整条染成品牌赭红（`[aria-current]`），琥珀压在上面几乎看不见——而人正看着的
 * 那颗 Bot 恰恰最可能是在等他的那一颗。翻成浅底 + 琥珀图形，两种底色下都立得住。
 * 这就是那颗状态点的注释里说的「压在品牌色上的纯色差别更难分」，同一个坑。
 */
.satu-botrow[aria-current="true"] .satu-botneed {
  --need-ink: var(--color-bg);
  --need-glyph: var(--color-warn-500);
}
.satu-botneed[hidden] { display: none; }
.satu-botneed svg { display: block; width: 10px; height: 10px; }
/**
 * 一圈往外推的光晕 + 一档很浅的呼吸。
 *
 * **不做「闪没」那种闪烁**：透明度掉到看不见的那半拍里，这颗徽章等于不存在，而人的
 * 视线正好可能在那半拍扫过去。亮度只压到 0.72，一直看得见，动的是外面那圈光晕——
 * 会动的东西在余光里比会消失的东西显眼得多。
 */
@keyframes satu-need-pulse {
  0%, 100% { opacity: 1; box-shadow: 0 0 0 0 color-mix(in srgb, var(--need-ink) 55%, transparent); }
  70% { opacity: 0.72; box-shadow: 0 0 0 5px color-mix(in srgb, var(--need-ink) 0%, transparent); }
}
/* 关了动效的人**不能因此少看见一件事**：动画去掉，徽章本身照旧是实心亮色。 */
@media (prefers-reduced-motion: reduce) {
  .satu-botneed { animation: none; }
}
/* 侧栏收起来只剩头像，这个图标跟着收——挤在图标旁边只会让头像看着歪（同 botdot）。 */
.satu-rail .satu-botneed { display: none; }
.satu-bottime { flex: none; font-size: 11px; opacity: 0.6; font-variant-numeric: tabular-nums; }
/* 摘要只给一行。多行会让每一行的高度随内容跳，整份名单看着就散了。 */
.satu-botsnip {
  font-size: 12px;
  opacity: 0.62;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.satu-botsnip[hidden] { display: none; }
/* 收起侧栏时只剩头像，名字、时间、摘要一起收掉。 */
.satu-rail .satu-botrow { justify-content: center; padding: 8px 0; }
.satu-rail .satu-botmain { display: none; }
.gw-chat-main > .gw-flash { margin: var(--space-3) var(--space-6) 0; }

/* 金额输入框：$ 压在输入框左内边距里，输入内容相应右移让位。 */
.satu-money { position: relative; display: block; }
.satu-money > span {
  position: absolute; left: 16px; top: 50%; transform: translateY(-50%);
  font-size: 14px; color: var(--muted-foreground); pointer-events: none;
}
.satu-money .input { padding-left: 30px; }

/* 统计表。列比供应商表多，单独一套。 */
.satu-stathead, .satu-statrow { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr 1fr 1fr 1fr; gap: var(--space-3); align-items: center; padding: var(--space-3) var(--space-4); }
.satu-stathead { padding: 10px var(--space-4); background: var(--card); border-bottom: 1px solid var(--border); font-size: 11.5px; font-weight: 600; letter-spacing: 0.04em; color: var(--muted-foreground); }
/* 除第一列外都是数字列：右对齐，并且要**等宽数字**——比例数字下 `1` 比 `7` 窄，
   右对齐只保证末位齐，上下两行的千分位仍然错开，一列数扫下来是歪的。 */
.satu-stathead > *:not(:first-child), .satu-statrow > *:not(:first-child) { text-align: right; font-variant-numeric: tabular-nums; }
/* 一行里的格子按**首行基线**对齐，不按盒子居中：带副行的格子（公司名下面的时间、
   输入下面的缓存、金额下面的角标）比别的格子高，居中会把它的首行顶上去，于是同一
   行的几个数字落在三个高度上。基线对齐让数字都停在同一条线上，副行统一往下挂。 */
.satu-statrow { align-items: baseline; border-bottom: 1px solid var(--border); min-width: 0; }
.satu-statrow:last-child { border-bottom: 0; }
.satu-statrow:hover { background: var(--card); }
.satu-stathead:first-child { border-top-left-radius: var(--radius-lg); border-top-right-radius: var(--radius-lg); }

/* ── 首页（没登录的人看到的 `/`，见 ui/pages-landing.js）──────────────────
   版式是**一张图纸**，不是一叠卡片：一条 1px 的栏线从顶栏一直画到页脚，每一块
   内容都落在这条线圈出的格子里，格与格之间也只用一条线隔开——不描卡片框、不投
   影、不收圆角。原来那版是「白卡片 + 圆角 + 阴影」堆出来的，八张卡片八个漂浮的
   方块，整页看下来没有一条线是对齐的。

   颜色一个没换，全走 theme.css 的 token（所以暗色主题不用再写一遍）。换掉的只有
   三样：**圆角收成直角、投影换成线、标题从 600 改到 300**——这三样合起来就是那
   种「技术图纸」的样子，而奶油 + 赭红仍然是我们自己的。

   这一屏没有外壳（没有侧栏顶栏），滚的是文档本身。 */
.satu-lp {
  /* 让**文档本身**滚，不套一层 overflow 容器：顶栏那条 sticky 才是黏在视口上的，
     而且移动端的地址栏收放也只在文档滚动时才对。工作台那边（appView）是反过来的
     ——它锁死 100vh 由内部各栏自己滚，那是应用；这一屏是网页。 */
  min-height: 100vh;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);

  /* 这一屏自己的几个量。**不进 theme.css**：它们只在这套图纸版式里成立，工作台
     那边仍然是圆角 + 柔和面。 */
  --lp-line: var(--color-divider);
  --lp-dim: color-mix(in srgb, var(--color-text) 62%, transparent);
  --lp-panel: var(--popover);
  /* 小样底下垫的那层暖面。比页面底色深一点点，够把白色的小样托起来，又不至于
     在页面上切出一块色斑。 */
  --lp-tint: color-mix(in srgb, var(--color-accent-100) 55%, var(--color-bg));
  /* 深色那一条带。**写死值，不用 --color-neutral-900**：那个 token 在暗色主题下
     是浅的（整阶翻转，见 theme.css），跟着它走的话暗色下这条带会变成一块白。 */
  --lp-dark: #1a1917;
  --lp-dark-ink: #f3f0e9;
  --lp-dark-line: #3a382f;
}
:root[data-theme='dark'] .satu-lp {
  /* 暗色下页面本身已经很深，这条带要**比页面还深一档**才还是一条带。 */
  --lp-dark: #0e0d11;
  --lp-dark-ink: #eae7e0;
  --lp-dark-line: #2e2c35;
}

/* 那条栏线圈出来的宽度。1240 而不是原来的 1120：现在左右两边各画着一条线，线离
   内容太近的话，整页看着像被夹住的。 */
.satu-lp-wrap { width: 100%; max-width: 1240px; margin: 0 auto; padding: 0 clamp(18px, 3.4vw, 40px); box-sizing: border-box; }
/* 带栏线的那几段。上下相接，两条竖线于是从头连到尾——这就是整张图纸的骨架。 */
.satu-lp-rail { border-left: 1px solid var(--lp-line); border-right: 1px solid var(--lp-line); }

/* ── 顶栏 ──────────────────────────────────────────────────────────────
   右边那几样各占一格，格与格之间一条竖线，最后那格「登录」是实心赭红。顶栏不是
   一排飘着的按钮，而是一条被切成几格的横梁。 */
.satu-lp-top {
  position: sticky;
  top: 0;
  z-index: 5;
  background: color-mix(in srgb, var(--color-bg) 92%, transparent);
  backdrop-filter: blur(10px);
  /* 底下这条横线**通栏**（它是顶栏和页面之间的那道界），但横梁本身收在栏线里
     ——底色和边框各管各的，所以这条不受下面那个 max-width 影响。 */
  border-bottom: 1px solid var(--lp-line);
}
/* 横梁**和内容同宽**：自己带一对栏线，左右两端正好落在那两条竖线上，于是页面从
   最顶上就是一张连着的图纸。
   内边距**走 .satu-lp-wrap 那一份**（不再归零）：顶栏里那几颗现在是圆钮而不是
   拉满高的格子，贴着栏线摆会像被线切了一刀。这一份内边距也让 logo 和页脚那枚
   logo、和别处所有走 wrap 的正文左沿对在同一条竖线上。 */
.satu-lp-topin {
  border-left: 1px solid var(--lp-line);
  border-right: 1px solid var(--lp-line);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  height: 64px;
}
/* 法务两页借的是同一条顶栏，可它们底下**没有栏线**（正文是一份文档，不是图纸）。
   那对边框在那儿就成了吊在顶栏两头的两截短竖线，下面接不上任何东西——收掉。 */
.satu-lg .satu-lp-topin { border-left: 0; border-right: 0; }
.satu-lp-brand {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font-family: var(--font-heading);
  font-size: 19px;
  font-weight: 500;
  letter-spacing: -0.015em;
  cursor: pointer;
}
.satu-lp-brand[data-static] { cursor: default; font-size: 16px; }
.satu-lp-brand img { border-radius: var(--radius-sm); object-fit: contain; }
.satu-lp-brand .satu-lp-home-logo { width: 40px; height: 40px; flex: none; }
/* 顶栏右边是**一排圆钮**，不是被竖线切开的几格。格子那版和登录页对不上：人从这儿
   点「登录」过去，落地那一屏上所有的按钮和输入框都是圆头的药丸。 */
.satu-lp-topact { display: flex; align-items: center; gap: var(--space-3); }
/* 源码那颗。不描边不填底——它和右边那组语言开关、登录按钮是三级不同的东西，
   给它也画个框的话，顶栏右边就成了三个抢同样注意力的方块。 */
.satu-lp-ghlink {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  flex: none;
  border-radius: 999px;
  color: var(--color-text);
  opacity: 0.55;
  transition: opacity 120ms ease, background 120ms ease;
}
.satu-lp-ghlink:hover { opacity: 1; background: var(--secondary); color: var(--color-text); }
.satu-lp-ghlink:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
/* 「联系销售」。btn-secondary 的描边款：它和右边那颗实心的「登录」是两件事——
   一个是「先问问」，一个是「我有账号」，实心只该有一颗。 */
.satu-lp-sales { flex: none; gap: 6px; }
.satu-lp-sales svg { flex: none; }
.satu-lp-topsign { flex: none; }
/* 语言开关：两颗贴在一起的小按钮，选中的那颗压一层底色。做成一段而不是下拉，
   因为只有两个值——点一下就到，不该先展开再选。 */
.satu-lp-lang { display: inline-flex; flex: none; padding: 2px; border: 1px solid var(--border); border-radius: 999px; background: var(--popover); }
.satu-lp-lang button {
  padding: 4px 11px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--muted-foreground);
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}
.satu-lp-lang button[aria-pressed="true"] { background: var(--secondary); color: var(--secondary-foreground); }

/* ── 通用零件 ──────────────────────────────────────────────────────────── */
/* 小字标签。这一屏所有的「眉题」「格子标题」「按钮字」都是这一款：小、大写、
   拉开字距。它是这套版式里承担「技术感」的那一样——正文和标题都不带装饰，全靠
   这一档小字把页面钉成图纸。 */
.satu-lp-kicker {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-size: 11.5px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-accent-600);
}
/* 眉题前面那颗小十字。和格子角上那颗是同一个记号（.satu-lp-tick），它把「这是
   一张图纸」这件事从版式落到细节上。 */
.satu-lp-kicker::before {
  content: '';
  width: 9px;
  height: 9px;
  flex: none;
  background:
    linear-gradient(currentColor, currentColor) center / 100% 1px no-repeat,
    linear-gradient(currentColor, currentColor) center / 1px 100% no-repeat;
}
/* 格子左上角那颗十字。图纸上的定位记号：它标的是这一格从哪儿起。 */
.satu-lp-tick {
  position: absolute;
  top: 14px;
  left: 14px;
  width: 10px;
  height: 10px;
  opacity: 0.5;
  background:
    linear-gradient(currentColor, currentColor) center / 100% 1px no-repeat,
    linear-gradient(currentColor, currentColor) center / 1px 100% no-repeat;
}

/* 按钮。**这一屏不另起一套**，就用 theme.css 里那套圆头药丸（.btn / .btn-primary /
   .btn-secondary）——登录页、工作台里的每一颗按钮都是它，首页自己造一套方的，人从
   「登录 Satuwork」点过去，落地那一屏的按钮忽然换了个形状。

   原先这儿有一套 .satu-lp-btn：直角、大写、拉字距。那套照着 orderful 来，摆在首页上
   自成一体，可它和它自己引向的那一屏对不上——**一个按钮的形状是跨页的承诺**，不是
   单页的排版选择。所以这儿只剩一件事：把药丸放大一档，首屏那颗主 CTA 该有的分量。 */
.satu-lp-ctas .btn { padding: 12px 22px; font-size: 15px; }
/* 深色带里那颗。btn-primary 自己带的底色在深底上照样立得住（赭红两边都够亮），
   所以只有描边款需要翻——目前带里只有主 CTA 一颗，这条是留给以后加的。 */
.satu-lp-band .btn-secondary {
  background: transparent;
  border-color: color-mix(in srgb, var(--lp-dark-ink) 45%, transparent);
  color: var(--lp-dark-ink);
}
.satu-lp-band .btn-secondary:hover { background: color-mix(in srgb, var(--lp-dark-ink) 12%, transparent); }

/* ── 首屏 ──────────────────────────────────────────────────────────────
   只有一列文案，不分左右。 */
.satu-lp-hero {
  padding-left: 0;
  padding-right: 0;
}
.satu-lp-heroleft {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-4);
  justify-content: center;
  padding: clamp(36px, 5vw, 64px) clamp(28px, 3.6vw, 56px);
}
.satu-lp-eyebrow { margin-bottom: var(--space-2); }
/* 这一屏的门面。**300 的字重**是整套样子的来源：同样一句话，600 是「我在喊」，
   300 是「我在写」。配上 1.02 的行距和负字距，它读起来像印在纸上的一行标题。
   clamp 而不是媒体查询：标题是这一屏唯一真正大的东西，它得跟着窗口连续变，在断点
   上「啪」地跳一档比一直偏小还显眼。
   keep-all：中文默认「哪儿都能断」，于是标题会在「交给一 / 个同事」中间折，把一个
   词劈成两半。加上它之后只在标点和空格处折——英文那版靠空格断行，不受影响。 */
.satu-lp-hero h1 {
  margin: 0;
  font-size: clamp(34px, 5vw, 66px);
  font-weight: 300;
  line-height: 1.04;
  letter-spacing: -0.028em;
  word-break: keep-all;
}
.satu-lp-lead { margin: 0; max-width: 44ch; font-size: 16px; line-height: 1.75; color: var(--lp-dim); }
.satu-lp-ctas { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-top: var(--space-2); }
.satu-lp-fine { margin: 0; font-size: 12px; letter-spacing: 0.02em; line-height: 1.6; color: var(--muted-foreground); }
/* 首屏那行小字里的「下载桌面端」。是句子中间的一条链接，不是一颗按钮——它和旁边那
   两颗 CTA 抢不得，所以只有下划线。 */
.satu-lp-finelink { padding: 0; border: 0; background: transparent; color: var(--color-accent-700); font: inherit; text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
.satu-lp-finelink:hover { color: var(--color-accent-600); }
.satu-lp-finelink:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; border-radius: 2px; }

/* 状态点（日常任务小样里那几颗）：忙=赭红，待人工=琥珀，空闲=灰，和侧栏名册同义。 */
.satu-lp-shotdot { width: 7px; height: 7px; flex: none; border-radius: 999px; background: var(--color-neutral-500); }
.satu-lp-shotdot[data-state="busy"] { background: var(--color-accent-500); }
.satu-lp-shotdot[data-state="review"] { background: var(--color-warn-500); }

/* ── 首屏下面那段演示 ──────────────────────────────────────────────────
   两个 AI 员工轮流干一件活（pages-landing.js 的 lpFilmView）。背后一层 three.js 点阵，
   只铺在上半截、往下淡掉——播放器压在它淡掉的那一截上面，像是从里面浮出来的。 */
.satu-lp-film {
  position: relative;
  overflow: hidden;
  padding-top: clamp(40px, 5vw, 72px);
  padding-bottom: clamp(40px, 5vw, 72px);
  border-top: 1px solid var(--lp-line);
}
.satu-lp-film-bg {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: min(640px, 100%);
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, #000 45%, transparent 92%);
  mask-image: linear-gradient(to bottom, #000 45%, transparent 92%);
}
.satu-lp-film > :not(canvas) { position: relative; }
.satu-lp-filmhead { margin-bottom: clamp(24px, 3vw, 36px); }
/* 标题两句各占一行（见 lpFilmView），不受 .satu-lp-sechead 那个 30ch 的宽度限制。 */
.satu-lp-sechead.satu-lp-filmhead { max-width: none; }
.satu-lp-filmhead h2 span { display: block; }

/* 两位的标签 + 播放键。每张标签底下一条进度，正在播的那位走着。 */
.satu-lp-filmtabs {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
  gap: var(--space-3);
  max-width: 1040px;
  margin: 0 auto var(--space-4);
}
.satu-lp-filmtab {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
  padding: 12px 14px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md, 10px);
  background: color-mix(in srgb, var(--lp-panel) 88%, transparent);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  color: inherit;
  font-family: var(--font-body);
  text-align: left;
  cursor: pointer;
  overflow: hidden;
  transition: border-color 160ms ease, box-shadow 160ms ease;
}
.satu-lp-filmtab:hover { border-color: color-mix(in srgb, var(--color-accent-500) 45%, var(--border)); }
.satu-lp-filmtab[aria-pressed='true'] { border-color: var(--color-accent-500); box-shadow: var(--shadow-sm); }
.satu-lp-filmtab:focus-visible, .satu-lp-filmplay:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.satu-lp-filmface { flex: none; display: flex; }
.satu-lp-filmtext { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.satu-lp-filmtext b { font-size: 14px; font-weight: 500; }
.satu-lp-filmtext span { font-size: 12px; line-height: 1.45; color: var(--lp-dim); }
.satu-lp-filmbar { position: absolute; left: 0; right: 0; bottom: 0; height: 3px; background: color-mix(in srgb, var(--color-text) 8%, transparent); }
.satu-lp-filmbar i { display: block; width: 0; height: 100%; background: var(--color-accent-500); }
.satu-lp-filmplay {
  align-self: center;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--lp-panel);
  color: var(--color-text);
  cursor: pointer;
}
.satu-lp-filmplay:hover { border-color: var(--color-accent-500); }
.satu-lp-filmplay[data-playing='true'] svg:last-child, .satu-lp-filmplay[data-playing='false'] svg:first-child { display: none; }

/* 播放器外框：高度只靠 aspect-ratio（宽屏 2:1、窄屏 4:3），舞台按宽度整体缩放进来（lpFilmFit）。
   **别在 JS 里写死 height**：aspect-ratio 碰上定高会反过来按高度算宽度，窗口从窄拉宽时框就卡在窄的那个宽度上。 */
.satu-lp-filmview {
  position: relative;
  max-width: 1040px;
  margin: 0 auto;
  aspect-ratio: 960 / 480;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--lp-panel);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}
.satu-lp-filmview[data-narrow] { aspect-ratio: 640 / 480; }

/* ── 舞台（960×480 的设计稿；窄屏 640×480，只剩屏幕） ── */
.satu-lpf-stage {
  position: absolute;
  top: 0;
  left: 0;
  display: grid;
  grid-template-columns: 320px 640px;
  width: 960px;
  height: 480px;
  transform-origin: 0 0;
  font-family: var(--font-body);
  font-size: 12px;
  line-height: 1.5;
}
.satu-lpf-stage[data-narrow] { grid-template-columns: 640px; width: 640px; }
.satu-lpf-stage[data-narrow] .satu-lpf-chat { display: none; }

/* 出场 / 退场。默认是「会淡进淡出、占着位置」；对话、工具痕迹和终端那几行是往下长的，
   没到那一步就不占地方。 */
.satu-lpf-stage [data-at], .satu-lpf-stage [data-off] { transition: opacity 360ms ease, transform 360ms ease, visibility 360ms; }
.satu-lpf-stage [data-at]:not([data-on]), .satu-lpf-stage [data-off]:not([data-on]) { opacity: 0; visibility: hidden; transform: translateY(6px) scale(0.98); }
.satu-lpf-stage .satu-lpf-msg:not([data-on]),
.satu-lpf-stage .satu-lpf-chip:not([data-on]),
.satu-lpf-stage .satu-lpf-termbody > :not([data-on]) { display: none; }
.satu-lpf-stage .satu-lpf-file[data-off]:not([data-on]) { transform: translateY(-14px) scale(0.6); }

/* 左边：Satuwork 的对话。用的是界面自己的令牌，深浅主题跟着走。 */
.satu-lpf-chat { display: flex; flex-direction: column; min-height: 0; border-right: 1px solid var(--lp-line); background: var(--color-surface); }
.satu-lpf-chathead { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--lp-line); }
.satu-lpf-chathead b { font-size: 13px; font-weight: 500; }
.satu-lpf-face { display: flex; }
.satu-lpf-badge { margin-left: auto; padding: 2px 9px; border-radius: 999px; font-size: 10.5px; font-weight: 500; letter-spacing: 0.04em; background: var(--color-accent-2-200); color: var(--color-accent-2-800); }
.satu-lpf-badge[data-kind='local'] { background: var(--color-ok-100); color: var(--color-ok-800); }
.satu-lpf-thread { flex: 1; min-height: 0; overflow: hidden; display: flex; flex-direction: column; justify-content: flex-end; gap: 10px; padding: 14px; }
.satu-lpf-msg { display: flex; }
.satu-lpf-msg[data-role='user'] { justify-content: flex-end; }
.satu-lpf-bubble { max-width: 86%; padding: 8px 11px; border-radius: 12px; font-size: 12px; line-height: 1.6; }
.satu-lpf-msg[data-role='user'] .satu-lpf-bubble { background: var(--primary); color: var(--primary-foreground); border-bottom-right-radius: 4px; }
.satu-lpf-msg[data-role='assistant'] .satu-lpf-bubble { background: var(--card); color: var(--card-foreground); border: 1px solid var(--border); border-bottom-left-radius: 4px; }
.satu-lpf-chips { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 7px; }
.satu-lpf-chip { display: inline-flex; align-items: center; gap: 3px; padding: 1px 8px; border-radius: 999px; background: var(--secondary); color: var(--secondary-foreground); font-size: 10.5px; white-space: nowrap; }
.satu-lpf-compose { display: flex; align-items: center; justify-content: space-between; margin: 0 14px 14px; padding: 0 5px 0 14px; height: 38px; border: 1px solid var(--input-border, var(--border)); border-radius: 999px; color: var(--muted-foreground); font-size: 12px; }
.satu-lpf-compose i { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 999px; background: var(--primary); color: var(--primary-foreground); }
.satu-lpf-compose svg { width: 14px; height: 14px; }

/* 右边：那台电脑的屏幕。**颜色写死**——这是一张「屏幕截图」，不是这个站的界面，
   不跟着主题翻。 */
.satu-lpf-screen { position: relative; width: 640px; height: 480px; overflow: hidden; color: #1f1d1a; }
.satu-lpf-os { position: absolute; inset: 0; }
.satu-lpf-os[data-os='remote'] { background: radial-gradient(120% 90% at 20% 10%, #3a4f66 0%, #243140 55%, #1a222c 100%); }
.satu-lpf-os[data-os='local'] { background: radial-gradient(120% 100% at 80% 0%, #f6dcc3 0%, #e0a882 50%, #b8704e 100%); }
.satu-lpf-osbar { display: flex; align-items: center; gap: 10px; height: 26px; padding: 0 12px; font-size: 11px; color: #f5f2ec; background: rgba(12, 14, 18, 0.55); }
.satu-lpf-os[data-os='local'] .satu-lpf-osbar { color: #2b2320; background: rgba(255, 255, 255, 0.55); }
.satu-lpf-osbar b { font-weight: 600; }
.satu-lpf-osbar span { opacity: 0.75; }
.satu-lpf-osdot { width: 8px; height: 8px; border-radius: 999px; background: #e95420; opacity: 1 !important; }
.satu-lpf-apple { width: 10px; height: 12px; border-radius: 5px 5px 4px 4px; background: #2b2320; opacity: 1 !important; }
.satu-lpf-osconn { margin-left: auto; display: inline-flex; align-items: center; gap: 6px; }
.satu-lpf-osconn::before { content: ''; width: 6px; height: 6px; border-radius: 999px; background: #3ecf8e; }

.satu-lpf-win { position: absolute; display: flex; flex-direction: column; border-radius: 8px; background: #fbfaf8; box-shadow: 0 18px 40px -12px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(0, 0, 0, 0.08); overflow: hidden; }
.satu-lpf-winbar { display: flex; align-items: center; gap: 5px; height: 28px; padding: 0 10px; flex: none; background: #efece7; border-bottom: 1px solid #e0dcd5; font-size: 11px; color: #6b655c; }
.satu-lpf-winbar > i { width: 9px; height: 9px; border-radius: 999px; background: #d9d4cc; }
.satu-lpf-winbar > i:first-child { background: #ec6a5e; }
.satu-lpf-winbar > i:nth-child(2) { background: #f4bf4f; }
.satu-lpf-winbar > i:nth-child(3) { background: #61c554; }
.satu-lpf-winbar > span { margin-left: 8px; }

/* 远程：浏览器。 */
.satu-lpf-url { flex: 1; height: 18px; margin-left: 10px !important; padding: 0 10px; border-radius: 999px; background: #fff; border: 1px solid #e0dcd5; display: flex; align-items: center; font-size: 10.5px; color: #3a352f; overflow: hidden; white-space: nowrap; }
/* 地址一个字一个字打出来：宽度按字数走 steps。 */
.satu-lpf-type { display: inline-block; overflow: hidden; white-space: nowrap; vertical-align: bottom; }
.satu-lpf-type[data-on] { animation: satu-lpf-type 1.1s steps(28, end) both; transform: none !important; }
@keyframes satu-lpf-type { from { max-width: 0; } to { max-width: 220px; } }
.satu-lpf-page { position: relative; flex: 1; padding: 14px 16px; }
.satu-lpf-skel { position: absolute; inset: 14px 16px; display: flex; flex-direction: column; gap: 10px; }
.satu-lpf-skel i { height: 12px; border-radius: 4px; background: #ece8e1; }
.satu-lpf-skel i:first-child { width: 40%; height: 16px; }
.satu-lpf-pagehead { display: flex; justify-content: space-between; margin-bottom: 10px; font-size: 13px; font-weight: 600; }
.satu-lpf-pagehead span { font-weight: 400; color: #8a8378; }
.satu-lpf-table { border: 1px solid #e6e2db; border-radius: 6px; overflow: hidden; }
.satu-lpf-row { display: grid; grid-template-columns: 22px 56px 1fr 90px; align-items: center; height: 30px; padding: 0 10px; font-size: 11px; border-top: 1px solid #efebe5; font-variant-numeric: tabular-nums; }
.satu-lpf-row:first-child { border-top: 0; }
.satu-lpf-row span:last-child { text-align: right; }
.satu-lpf-more { display: block; line-height: 30px; color: #8a8378; }
.satu-lpf-check { width: 11px; height: 11px; border: 1.5px solid #b9b2a7; border-radius: 3px; }
/* 第 3 步全选：勾都打上。 */
.satu-lpf-os[data-os='remote'] .satu-lpf-browser .satu-lpf-page > [data-on] .satu-lpf-check { transition: background 200ms ease 900ms, border-color 200ms ease 900ms; background: #c2410c; border-color: #c2410c; }
.satu-lpf-btn { position: absolute; right: 16px; bottom: 14px; padding: 6px 14px; border-radius: 6px; background: #c2410c; color: #fff; font-size: 11px; font-weight: 500; }
.satu-lpf-toast { position: absolute; right: 12px; top: 40px; display: flex; align-items: center; gap: 6px; padding: 7px 12px; border-radius: 8px; background: #26231f; color: #f5f2ec; font-size: 11px; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25); }

/* 远程：文件管理器。 */
.satu-lpf-list { padding: 6px 0; }
.satu-lpf-li { display: flex; align-items: center; gap: 8px; height: 30px; padding: 0 14px; font-size: 11px; }
.satu-lpf-li span { margin-left: auto; color: #8a8378; }
.satu-lpf-li em { width: 26px; padding: 1px 0; border-radius: 3px; text-align: center; font-style: normal; font-size: 8.5px; font-weight: 700; color: #fff; background: #d64541; }
.satu-lpf-li em[data-ext='xlsx'] { background: #1f8f4e; }
.satu-lpf-dim { color: #8a8378; }
.satu-lpf-new { margin: 2px 6px; border-radius: 6px; background: #fff3c4; font-weight: 600; }

/* 本地：访达。 */
.satu-lpf-folders { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; padding: 12px 14px 6px; min-height: 76px; border-bottom: 1px solid #eee9e2; }
.satu-lpf-folder { position: relative; display: flex; flex-direction: column; align-items: center; gap: 4px; }
.satu-lpf-folder i { position: relative; width: 44px; height: 34px; border-radius: 4px; background: linear-gradient(#78b7f0, #4b95dc); }
.satu-lpf-folder i::before { content: ''; position: absolute; left: 0; top: -5px; width: 18px; height: 8px; border-radius: 3px 3px 0 0; background: #5aa2e2; }
.satu-lpf-folder em { font-style: normal; font-size: 10.5px; white-space: nowrap; }
.satu-lpf-folder small { position: absolute; top: -6px; right: 14px; min-width: 18px; padding: 0 5px; border-radius: 999px; background: #c2410c; color: #fff; font-size: 9.5px; font-weight: 600; text-align: center; line-height: 16px; }
.satu-lpf-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 10px 4px; padding: 12px 10px; }
.satu-lpf-file { display: flex; flex-direction: column; align-items: center; gap: 4px; min-width: 0; }
.satu-lpf-file i { position: relative; width: 30px; height: 38px; border-radius: 3px; background: #fff; box-shadow: 0 0 0 1px #ddd6cc, 0 2px 4px rgba(0, 0, 0, 0.08); }
.satu-lpf-file i::after { content: ''; position: absolute; top: 0; right: 0; border-style: solid; border-width: 0 9px 9px 0; border-color: #efe9e0 #f7f3ee; }
.satu-lpf-file b { position: absolute; left: -4px; bottom: 6px; padding: 0 3px; border-radius: 2px; font-size: 7.5px; font-weight: 700; color: #fff; background: #8a8378; }
.satu-lpf-file[data-ext='pdf'] b { background: #d64541; }
.satu-lpf-file[data-ext='png'] b { background: #3b82c4; }
.satu-lpf-file[data-ext='docx'] b { background: #2f5fb3; }
.satu-lpf-file[data-ext='xlsx'] b { background: #1f8f4e; }
.satu-lpf-file[data-ext='zip'] b { background: #9a7b3f; }
.satu-lpf-file em { max-width: 100%; font-style: normal; font-size: 9.5px; color: #4a453e; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* 本地：终端。 */
.satu-lpf-term { background: #1c1b19; }
.satu-lpf-term .satu-lpf-winbar { background: #2a2825; border-color: #37342f; color: #b9b2a7; }
.satu-lpf-termbody { display: flex; flex-direction: column; gap: 3px; padding: 10px 12px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10.5px; color: #e7e3da; white-space: nowrap; overflow: hidden; }
.satu-lpf-termbody b { color: #e8845c; font-weight: 400; }
.satu-lpf-out { color: #9fd49a; }
.satu-lpf-note { position: absolute; top: 36px; right: 12px; display: flex; flex-direction: column; gap: 2px; width: 210px; padding: 10px 12px; border-radius: 12px; background: rgba(250, 248, 245, 0.92); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); font-size: 11px; }
.satu-lpf-note b { font-size: 11px; }
.satu-lpf-localtag { position: absolute; left: 20px; bottom: 16px; display: inline-flex; align-items: center; gap: 6px; padding: 5px 11px; border-radius: 999px; background: rgba(28, 27, 25, 0.78); color: #f5f2ec; font-size: 10.5px; }

/* 光标：走到下一步的位置要 0.7 秒；点一下是一圈扩开的涟漪。 */
.satu-lpf-cursor { position: absolute; top: 0; left: 0; z-index: 5; width: 18px; height: 18px; transition: transform 700ms cubic-bezier(0.45, 0, 0.2, 1); filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.3)); }
.satu-lpf-cursor::after { content: ''; position: absolute; left: -9px; top: -9px; width: 20px; height: 20px; border-radius: 999px; border: 2px solid #c2410c; opacity: 0; }
.satu-lpf-cursor[data-click]::after { animation: satu-lpf-click 520ms ease-out; }
@keyframes satu-lpf-click { from { opacity: 0.9; transform: scale(0.4); } to { opacity: 0; transform: scale(1.8); } }

/* 字幕：只在窄屏（对话那一栏收起时）露出来，一次一句。 */
.satu-lpf-caps { display: none; }
.satu-lpf-stage[data-narrow] .satu-lpf-caps { position: absolute; left: 0; right: 0; bottom: 0; z-index: 6; display: block; height: 96px; background: linear-gradient(transparent, rgba(0, 0, 0, 0.72)); }
.satu-lpf-caps span { position: absolute; left: 24px; right: 24px; bottom: 20px; color: #fff; font-size: 24px; line-height: 1.35; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); }

@media (prefers-reduced-motion: reduce) {
  .satu-lpf-stage *, .satu-lpf-stage *::after { transition: none !important; animation: none !important; }
}

@media (max-width: 760px) {
  /* 两张标签叠起来，播放键缩到右上角。 */
  .satu-lp-filmtabs { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .satu-lp-filmplay { display: none; }
  .satu-lp-filmtext span { display: none; }
  .satu-lp-filmtab { padding: 10px 12px 12px; }
}

/* ── 能力带 ────────────────────────────────────────────────────────────
   四样单开一张卡不值当（说一句就完），可整个不提又缺一块。排成一条被竖线切成四
   格的窄带，人扫过去知道「还有这些」就够了。 */
.satu-lp-spec {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0;
  padding-left: 0;
  padding-right: 0;
  border-top: 1px solid var(--lp-line);
}
.satu-lp-specitem {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  padding: 20px clamp(12px, 1.6vw, 22px);
  font-size: 12.5px;
  letter-spacing: 0.02em;
  color: var(--secondary-foreground);
}
.satu-lp-specitem + .satu-lp-specitem { border-left: 1px solid var(--lp-line); }
.satu-lp-chipicon { display: inline-flex; flex: none; color: var(--color-accent-600); }

/* ── 分节 ──────────────────────────────────────────────────────────────
   节头**居中**（和 orderful 一样）：一句眉题、一行大标题、一句说明。大标题同样是
   300 的字重——整页只有一档字重在说话，别的都是那一档小字。 */
.satu-lp-sec { padding-top: clamp(56px, 6vw, 88px); padding-bottom: clamp(56px, 6vw, 88px); border-top: 1px solid var(--lp-line); }
.satu-lp-sechead { max-width: 30ch; margin: 0 auto clamp(36px, 4vw, 56px); text-align: center; }
.satu-lp-sechead .satu-lp-kicker { margin-bottom: var(--space-4); }
.satu-lp-sechead h2 { margin: 0 0 var(--space-3); font-size: clamp(27px, 3.4vw, 42px); font-weight: 300; line-height: 1.12; letter-spacing: -0.025em; word-break: keep-all; }
.satu-lp-sechead p { margin: 0; font-size: 15px; line-height: 1.7; color: var(--lp-dim); }

/* 四张能力卡排成一行，**只靠竖线隔开**：不描框、不投影、不收圆角。原来是四张带
   框带影的白卡，四个方块浮在页面上，谁也不挨着谁。 */
.satu-lp-cards {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  border-top: 1px solid var(--lp-line);
  border-bottom: 1px solid var(--lp-line);
}
.satu-lp-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-width: 0;
  padding: clamp(24px, 2.6vw, 34px) clamp(16px, 1.8vw, 26px);
}
.satu-lp-card + .satu-lp-card { border-left: 1px solid var(--lp-line); }
.satu-lp-cardshot {
  height: 168px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
  box-sizing: border-box;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  /* 小样底下垫一层比页面略深的暖面：小样自己是白的，直接贴在白底上会糊成一片。 */
  background: var(--lp-tint);
  overflow: hidden;
}
.satu-lp-cardshot > * { width: 100%; max-width: 340px; }
.satu-lp-card h3 { margin: 0; font-size: 16px; font-weight: 500; letter-spacing: -0.01em; }
.satu-lp-card p { margin: 0; font-size: 13px; line-height: 1.65; color: var(--muted-foreground); }

/* 小样里的窗口壳：三颗灯 + 标题。工作区、日常任务、账本三块共用。 */
.satu-lp-win { border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--lp-panel); box-shadow: var(--shadow-sm); overflow: hidden; }
.satu-lp-winbar { display: flex; align-items: center; gap: 5px; padding: 7px 9px; border-bottom: 1px solid var(--lp-line); background: var(--color-surface); }
.satu-lp-winbar i { width: 6px; height: 6px; border-radius: 999px; background: var(--color-neutral-400); }
.satu-lp-winbar i:first-child { background: var(--color-accent-400); }
.satu-lp-winbar span { margin-left: 5px; font-size: 10.5px; letter-spacing: 0.04em; color: var(--muted-foreground); }
.satu-lp-winbody { display: flex; flex-direction: column; gap: 5px; padding: 9px; }

.satu-lp-file { display: flex; align-items: center; gap: 7px; font-size: 11px; color: var(--color-text); }
.satu-lp-file svg { flex: none; color: var(--color-accent-600); }
.satu-lp-file span { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.satu-lp-file em { flex: none; font-style: normal; font-size: 10px; color: var(--muted-foreground); }
/* 终端那一行：整块小样里唯一的等宽字，它一出现「这是一台机器」就不用再解释了。 */
.satu-lp-term { margin-top: 3px; padding: 7px 8px; border-radius: var(--radius-sm); background: var(--lp-dark); color: #e7e3da; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10.5px; white-space: nowrap; overflow: hidden; }
.satu-lp-term b { color: var(--color-accent-400); font-weight: 400; }
.satu-lp-caret { display: inline-block; width: 5px; height: 10px; margin-left: 3px; vertical-align: -1px; background: var(--color-accent-400); }

.satu-lp-appr { padding: var(--space-3); border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--lp-panel); box-shadow: var(--shadow-sm); }
.satu-lp-apprtag { display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px; border-radius: 999px; background: var(--color-warn-100); color: var(--color-warn-800); font-size: 10.5px; font-weight: 500; letter-spacing: 0.04em; }
.satu-lp-appr p { margin: var(--space-2) 0 var(--space-3); font-size: 12.5px; line-height: 1.5; }
.satu-lp-apprbtns { display: flex; gap: 7px; }
.satu-lp-apprbtns span { padding: 5px 14px; border: 1px solid var(--border); border-radius: 999px; background: var(--lp-panel); font-size: 11px; font-weight: 500; color: var(--secondary-foreground); }
.satu-lp-apprbtns span[data-yes] { border-color: transparent; background: var(--color-accent-500); color: var(--primary-foreground); }

.satu-lp-cron { display: flex; align-items: center; gap: 8px; font-size: 11px; }
.satu-lp-cronwhen { flex: none; padding: 2px 7px; border-radius: var(--radius-sm); background: var(--color-accent-2-100); color: var(--color-accent-2-800); font-weight: 500; }
.satu-lp-cronwhat { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--muted-foreground); }

.satu-lp-ledger { gap: 9px; }
/* 柱子的高度是排版，不是数据（见 lpMockLedger）——所以不标刻度，也不写坐标轴。 */
.satu-lp-bars { display: flex; align-items: flex-end; gap: 5px; height: 58px; }
.satu-lp-bars i { flex: 1; border-radius: 3px 3px 0 0; background: var(--color-accent-300); }
.satu-lp-bars i:nth-child(6) { background: var(--color-accent-500); }
.satu-lp-ledgerfoot { display: flex; align-items: baseline; gap: 8px; }
.satu-lp-ledgerfoot b { font-family: var(--font-heading); font-size: 19px; font-weight: 400; }
.satu-lp-ledgerfoot span { font-size: 10px; color: var(--muted-foreground); }

/* ── 「怎么开始」那块深色 ────────────────────────────────────────────
   **整页唯一一块深色**。它落在页面快到底的地方：前面全是奶油色，一路读下来到这儿
   忽然暗一档，人自然会停一下——而要他停的正是这一段（三步 + 那颗 CTA）。
   深色只铺在栏线以内（底色在 -bandin 上，不在外层 section 上），两边栏线外仍是奶油底。 */
.satu-lp-band { color: var(--lp-dark-ink); }
.satu-lp-bandin { background: var(--lp-dark); padding-top: clamp(56px, 6vw, 88px); padding-bottom: clamp(56px, 6vw, 88px); }
.satu-lp-band .satu-lp-kicker { color: var(--color-accent-400); }
.satu-lp-band .satu-lp-sechead p { color: color-mix(in srgb, var(--lp-dark-ink) 66%, transparent); }

/* 三步：一个格子一步，角上一颗定位十字，序号是 300 字重的大字。 */
.satu-lp-steps {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--lp-dark-line);
  border-radius: var(--radius-lg);
  /* 圆角要真的切得到最外那一圈：三格的底色和分隔线都压在这个圆角里，不裁的话
     左右两格的直角会从圆角底下探出来。 */
  overflow: hidden;
}
.satu-lp-step {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
  padding: clamp(28px, 3vw, 40px) clamp(20px, 2.2vw, 30px) clamp(24px, 2.6vw, 34px);
}
.satu-lp-step + .satu-lp-step { border-left: 1px solid var(--lp-dark-line); }
.satu-lp-stepnum { font-family: var(--font-heading); font-size: 38px; font-weight: 200; line-height: 1; letter-spacing: -0.03em; color: var(--color-accent-400); font-variant-numeric: tabular-nums; }
.satu-lp-step h3 { margin: 0; font-size: 15px; font-weight: 400; line-height: 1.5; color: var(--lp-dark-ink); }

.satu-lp-tail { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-4); margin-top: clamp(32px, 3.6vw, 48px); }
.satu-lp-tail span { font-size: 12.5px; letter-spacing: 0.02em; color: color-mix(in srgb, var(--lp-dark-ink) 62%, transparent); }

/* ── 页脚 ──────────────────────────────────────────────────────────────
   顶上那条 3px 的赭红是收尾的一笔：整页只有两处实心赭红（顶栏末端那格、这一条），
   一头一尾把这张图纸夹住。 */
.satu-lp-foot { border-top: 3px solid var(--primary); background: var(--color-bg); }
.satu-lp-footin { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-3); padding-top: var(--space-6); padding-bottom: var(--space-6); font-size: 11.5px; letter-spacing: 0.04em; color: var(--muted-foreground); }

/* 弹窗：二维码那张卡。 */
.satu-lp-salesbox { display: flex; flex-direction: column; gap: var(--space-4); max-width: 380px; }
/* 二维码**底色写死白的**，暗色主题下也不反——反色的码有相当一部分扫码器读不出来。
   所以给它一层白底框，在深色页面上看着是一张卡片，而不是一块没适配的白斑。 */
.satu-lp-qr {
  align-self: center;
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background: #ffffff;
  box-shadow: var(--shadow-sm);
  line-height: 0;
}
.satu-lp-qr img { display: block; width: 180px; height: 180px; }
.satu-lp-salesnum { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-3); }
.satu-lp-salesnum > span { font-family: var(--font-heading); font-size: 16px; font-variant-numeric: tabular-nums; }
.satu-lp-salesnum .btn { gap: 6px; text-decoration: none; }
/* 「示例二维码」那句。琥珀不是灰：它不是一句补充说明，是一件还没做完的事。 */
.satu-lp-salesnote { margin: 0; padding: 9px 12px; border-radius: var(--radius-sm); background: var(--color-warn-100); color: var(--color-warn-800); font-size: 12px; line-height: 1.6; }

@media (max-width: 1040px) {
  /* 四张卡排一行到这个宽度每张只剩 200 出头，小样里的文件名开始一个个省略号。
     收成 2×2：四张小样摆成一个方块，仍然是一整块。 */
  .satu-lp-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .satu-lp-card:nth-child(odd) { border-left: 0; }
  .satu-lp-card:nth-child(n + 3) { border-top: 1px solid var(--lp-line); }
}

@media (max-width: 900px) {
  .satu-lp-heroleft { padding: 32px clamp(20px, 4vw, 32px); }
  /* 能力带收成 2×2：四格排一行到这儿每格只剩五六个字宽。 */
  .satu-lp-spec { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .satu-lp-specitem:nth-child(odd) { border-left: 0; }
  .satu-lp-specitem:nth-child(n + 3) { border-top: 1px solid var(--lp-line); }
  /* 三步竖着排。 */
  .satu-lp-steps { grid-template-columns: 1fr; }
  .satu-lp-step + .satu-lp-step { border-left: 0; border-top: 1px solid var(--lp-dark-line); }
  /* 顶栏那颗「登录」和首屏的主 CTA 说的是同一件事，窄屏上一前一后隔不到一屏，
     留一颗就够——留下面那颗大的。 */
  .satu-lp-topsign { display: none; }
}

@media (max-width: 760px) {
  .satu-lp-cards { grid-template-columns: 1fr; }
  .satu-lp-card + .satu-lp-card { border-left: 0; border-top: 1px solid var(--lp-line); }
  .satu-lp-ctas { width: 100%; }
  .satu-lp-ctas .btn { flex: 1; min-width: 180px; }
}

@media (max-width: 560px) {
  .satu-lp-brand .satu-lp-home-logo { width: 36px; height: 36px; }
  /* 「联系销售」收成一颗只有图标的圆钮：顶栏那一行在这个宽度上已经排着 logo、
     GitHub、语言开关三样了，再加四个字就换行。**不能跟着「登录」一起藏**（那条在
     900 那段里）——藏了的话页面上再没有第二处能联系到人。
     900 到 560 之间不收：那一档「登录」已经让出了位置，四个字放得下。 */
  .satu-lp-sales { padding: 8px; border-radius: 999px; }
  .satu-lp-sales span { display: none; }
  /* 能力带竖着排：两格一行到 375 宽时「Skill 与 MCP 分两层」也要折行。 */
  .satu-lp-spec { grid-template-columns: 1fr; }
  .satu-lp-specitem { border-left: 0; }
  .satu-lp-specitem + .satu-lp-specitem { border-top: 1px solid var(--lp-line); }
}

/* ── 隐私政策与服务条款（/privacy、/terms，见 ui/pages-legal.js）─────────
   外壳（顶栏、页脚、那条 wrap）借首页那一套，这里只管正文：一列目录 + 一列文本。
   两页共用，所以别往这儿写任何一页专有的东西。 */
.satu-lg-head { padding-top: 56px; padding-bottom: var(--space-6); border-bottom: 1px solid var(--color-divider); }
.satu-lg-head h1 { margin: 0 0 var(--space-3); font-size: clamp(28px, 3.4vw, 40px); letter-spacing: -0.02em; }
.satu-lg-lead { margin: 0; max-width: 60ch; font-size: 15.5px; line-height: 1.7; color: color-mix(in srgb, var(--color-text) 72%, transparent); }
.satu-lg-meta { margin: var(--space-4) 0 0; font-size: 13px; color: var(--muted-foreground); font-variant-numeric: tabular-nums; }
/* 「还没过法务」那条横条。和首页那句「示例二维码」同一身打扮（警告色、小字），
   因为它们是同一种东西：一件没做完的事在页面上留下的唯一痕迹。 */
.satu-lg-note { margin: var(--space-4) 0 0; max-width: 72ch; padding: 10px 13px; border-radius: var(--radius-sm); background: var(--color-warn-100); color: var(--color-warn-800); font-size: 12.5px; line-height: 1.65; }

.satu-lg-grid { display: grid; grid-template-columns: 232px minmax(0, 1fr); gap: var(--space-8); align-items: start; padding-top: var(--space-8); padding-bottom: 64px; }

/* 目录黏在顶栏底下。top 要躲开那条 62px 高的 sticky 顶栏，否则跳到某一节之后，
   目录自己被压在顶栏底下半截。 */
.satu-lg-toc { position: sticky; top: 78px; display: flex; flex-direction: column; gap: 1px; }
.satu-lg-toc a {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  color: var(--muted-foreground);
  text-decoration: none;
  font-size: 13px;
  line-height: 1.5;
}
.satu-lg-toc a:hover { background: var(--secondary); color: var(--color-text); }
.satu-lg-toc a:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; }
.satu-lg-toc a span { flex: none; width: 16px; font-variant-numeric: tabular-nums; font-size: 11.5px; opacity: 0.7; }

.satu-lg-body { max-width: 72ch; }
/* 锚点跳过来时，标题不要顶在视口最上沿被 sticky 顶栏盖住。 */
.satu-lg-sec { scroll-margin-top: 80px; }
.satu-lg-sec + .satu-lg-sec { margin-top: var(--space-8); }
.satu-lg-sec h2 { display: flex; align-items: baseline; gap: var(--space-3); margin: 0 0 var(--space-4); font-size: 19px; }
.satu-lg-secnum { flex: none; font-size: 13px; color: var(--muted-foreground); font-variant-numeric: tabular-nums; }
.satu-lg-sec p { margin: 0 0 var(--space-3); font-size: 14.5px; line-height: 1.85; color: color-mix(in srgb, var(--color-text) 80%, transparent); }
.satu-lg-sec ul { margin: 0 0 var(--space-3); padding-left: 1.15em; }
.satu-lg-sec li { margin-bottom: var(--space-2); font-size: 14.5px; line-height: 1.8; color: color-mix(in srgb, var(--color-text) 80%, transparent); }
.satu-lg-sec > *:last-child { margin-bottom: 0; }

/* 页脚那几条（首页、登录页、这两页共用）。首页那边是按钮（站内跳转走 data-act），
   登录那三屏是真链接（开新标签页，见 shell.js 的 authAside）——两种都要把默认打扮
   撤掉，否则页脚上会冒出两个灰框、或者两条蓝字。 */
.satu-lg-links { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--space-4); }
.satu-lg-links button,
.satu-lg-links a {
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);
  cursor: pointer;
}
.satu-lg-links button:hover,
.satu-lg-links a:hover { color: var(--color-text); text-decoration-color: currentColor; }
.satu-lg-links button:focus-visible,
.satu-lg-links a:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; border-radius: 2px; }

/* ── 下载桌面端：首页那一段（lpDownload）和应用内那一页（downloadPage），见 ui/pages-landing.js ──
   外框（栏线、上下留白、居中的标题块）借首页 .satu-lp-sec 那一套，这里只管里面：
   左边一张卡装当前平台的包，右边一列说明。
   scroll-margin 让出顶栏那一条：首屏那行小字、页脚和老的 /download 地址都是滚到这儿的，
   不让的话标题正好被吸顶的顶栏盖住。 */
.satu-lp-dl { scroll-margin-top: 64px; }
/* 标题块比别的段宽：底下那句说明有三行长，塞进 30ch 会折成一根细柱子。 */
.satu-lp-dlhead { max-width: 62ch; }
/* 那一段带 tabindex="-1"，点首屏小字 / 页脚跳过来时焦点落在它身上（见 app.js 的
   landing-download）。整段描一圈框没有意义，里面的按钮自己有焦点环。 */
.satu-lp-dl:focus { outline: none; }
/* 「总是最新版 · 历次版本」那行是 .satu-lg-meta（法律页那一行）本身，这里只补链接的样子。
   它是 <div> 不是 <p>，`.satu-lp-sechead p` 够不着它（见 pages-landing.js 的 dlMeta）。 */
.satu-dl-meta a { color: var(--color-accent-700); text-decoration: underline; text-underline-offset: 3px; }
.satu-dl-meta a:hover { color: var(--color-accent-600); }
.satu-dl-grid { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); gap: var(--space-8); align-items: start; }

.satu-dl-card { border: 1px solid var(--color-divider); border-radius: var(--radius-lg); padding: var(--space-6); background: var(--color-bg); }

/* 平台切换：两颗挨在一起的分段按钮。打扮照首页顶栏那对语言键（.satu-lp-lang）来，
   它们是同一种东西——一组互斥的小开关。 */
.satu-dl-tabs { display: inline-flex; gap: 2px; padding: 2px; border: 1px solid var(--border); border-radius: 999px; background: var(--secondary); }
.satu-dl-tabs button {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 6px 14px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--muted-foreground);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}
.satu-dl-tabs button svg { flex: none; width: 15px; height: 15px; }
.satu-dl-tabs button[aria-pressed="true"] { background: var(--color-bg); color: var(--color-text); box-shadow: var(--shadow-sm); }
.satu-dl-tabs button:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }

.satu-dl-head { display: flex; align-items: flex-start; gap: var(--space-4); margin-top: var(--space-6); }
.satu-dl-osicon { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; border-radius: var(--radius-md); background: var(--secondary); color: var(--color-text); }
.satu-dl-head h3 { margin: 0 0 4px; font-size: 20px; }
.satu-dl-head p { margin: 0; font-size: 13px; line-height: 1.6; color: var(--muted-foreground); }

.satu-dl-gets { display: flex; flex-direction: column; gap: var(--space-3); margin-top: var(--space-6); }
/* 下载那颗**是个 `<a>`**（右键存储为、复制地址都要能用，见 pages-landing.js 的 dlRow），
   而 .btn 那套是按 <button> 调的：行高和内边距在链接上对不齐，所以这里补一档。 */
.satu-dl-get { display: flex; align-items: center; justify-content: flex-start; gap: var(--space-3); height: auto; padding: 12px 16px; text-align: left; text-decoration: none; }
.satu-dl-get svg { flex: none; }
.satu-dl-getin { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.satu-dl-getin b { font-size: 14px; font-weight: 600; }
.satu-dl-getin em { font-style: normal; font-size: 11.5px; opacity: 0.75; }

.satu-dl-fine { margin: var(--space-4) 0 0; font-size: 12.5px; line-height: 1.7; color: var(--muted-foreground); }
/* 「直接用网页版登录」那条：夹在一句话中间，所以要看着是链接而不是一颗按钮。 */
.satu-dl-weblink { padding: 0; border: 0; background: transparent; color: var(--color-accent-700); font: inherit; text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
.satu-dl-weblink:hover { color: var(--color-accent-600); }
.satu-dl-weblink:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; border-radius: 2px; }

/* 服务器地址那条。等宽 + 可选中：这一行是要被抄进桌面端第一屏的。 */
.satu-dl-server { margin: var(--space-4) 0 0; font-size: 12.5px; color: var(--muted-foreground); }
.satu-dl-server code { display: inline-block; margin-left: 6px; padding: 2px 7px; border-radius: var(--radius-sm); background: var(--secondary); color: var(--color-text); font-family: var(--font-mono, ui-monospace, monospace); font-size: 12px; user-select: all; overflow-wrap: anywhere; }

.satu-dl-note + .satu-dl-note { margin-top: var(--space-4); padding-top: var(--space-4); border-top: 1px solid var(--color-divider); }
.satu-dl-note h3 { margin: 0 0 var(--space-2); font-size: 14.5px; }
.satu-dl-note p { margin: 0; font-size: 13.5px; line-height: 1.8; color: color-mix(in srgb, var(--color-text) 78%, transparent); }
/* 要贴进终端的那一行。点一下全选（user-select: all），窄屏上可以在任意处折行——一条
   横向滚出去的命令在手机上看不全，人会照着前半截抄。 */
.satu-dl-cmd { display: block; margin-top: var(--space-3); padding: 8px 10px; border-radius: var(--radius-sm); background: var(--secondary); color: var(--color-text); font-family: var(--font-mono, ui-monospace, monospace); font-size: 12px; line-height: 1.6; user-select: all; overflow-wrap: anywhere; }

@media (max-width: 900px) {
  /* 说明挪到卡片底下。**别撤掉**——「第一次打开会被拦」那条是这一段里最要紧的一句，
     手机上照样得看得见（人常常是用手机扫码打开首页、再转给自己的电脑）。 */
  .satu-dl-grid { grid-template-columns: 1fr; gap: var(--space-6); }
  .satu-dl-card { padding: var(--space-4); }
}

/* 「点击『…』即表示你同意…」那句（shell.js 的 authConsent）：注册和接受邀请两屏的
   提交按钮底下各一份。压在按钮下面、比正文小一号，但两条链接要**看得出是链接**——
   这句话的全部意义就在于那两条点得开。 */
.satu-authconsent { margin: calc(var(--space-2) * -1) 0 0; font-size: 12px; line-height: 1.65; color: var(--muted-foreground); }
.satu-authconsent a { color: var(--color-accent-700); text-decoration: underline; text-underline-offset: 3px; }
.satu-authconsent a:hover { color: var(--color-accent-600); }

@media (max-width: 900px) {
  /* 窄屏上目录横过来摆在正文前面：竖着那一列到这个宽度已经没地方放，而整个撤掉的话
     这两页就只剩一根长长的滚动条——法律文本恰恰是要跳着看的。 */
  .satu-lg-grid { grid-template-columns: 1fr; gap: var(--space-6); }
  .satu-lg-toc {
    position: static;
    flex-direction: row;
    flex-wrap: wrap;
    gap: var(--space-2);
    padding-bottom: var(--space-4);
    border-bottom: 1px solid var(--color-divider);
  }
  .satu-lg-toc a { padding: 4px 10px; border: 1px solid var(--border); border-radius: 999px; font-size: 12.5px; }
  .satu-lg-head { padding-top: 36px; }
  /* 顶栏那颗「登录」在窄屏上是藏起来的（见上面 900 那段），因为首页底下还有一颗更大的
     主 CTA。**这两页没有那颗**——跟着藏的话，手机上从这儿再也走不进产品。 */
  .satu-lg .satu-lp-topsign { display: inline-flex; }
}

/* 审计详情的评分明细：项目 · 得分 / 满分 · 原因。窄屏把原因挪到下一行。 */
.satu-audit-score { display: grid; grid-template-columns: minmax(140px, 1fr) 96px minmax(0, 2.4fr); gap: var(--space-4); align-items: start; padding: var(--space-3) 0; border-top: 1px solid var(--border); }
.satu-audit-score:first-child { border-top: 0; }
@media (max-width: 760px) {
  .satu-audit-score { grid-template-columns: 1fr 96px; }
  .satu-audit-score > :last-child { grid-column: 1 / -1; }
}

/* ══════════════════════════════════════════════════════════════════════
   手机那一档（≤760px，和 prefs.js 的 narrowMq 是同一个数）。

   外壳在宽屏上是「侧栏 | 内容 | 右栏」三列，列宽全写在 appView 的内联样式里——所以
   这一段到处是 !important，盖的就是那几条内联。

   - 侧栏变成从左边盖上来的抽屉（顶栏最左那颗变汉堡，见 appView）。常驻的话 248 宽
     的侧栏在 375 的屏上只给对话剩一条缝。
   - 右栏不再占一列，而是盖在对话正文那一格上；顶栏留在原处，切屏和收起那几颗照样
     点得到——右栏自己没有 header，盖住顶栏就没地方关了。
   ══════════════════════════════════════════════════════════════════════ */
.gw-drawer-mask { display: none; }
@media (max-width: 760px) {
  .gw-shell { grid-template-columns: minmax(0, 1fr) !important; gap: 0 !important; padding: 0 !important; }
  .gw-side {
    /* 压在弹窗（.gw-modal-backdrop 是 25）底下：「新建 Bot」「插件」这几个弹窗就是从抽屉里
       点出来的，抽屉压在上面的话弹窗开了也看不见。 */
    position: fixed; top: 0; bottom: 0; left: 0; z-index: 24;
    width: min(300px, 86vw); height: auto !important;
    border-radius: 0 var(--radius-md) var(--radius-md) 0 !important;
    box-shadow: var(--shadow-lg);
    transform: translateX(-105%);
    transition: transform 200ms ease;
  }
  .gw-shell[data-drawer="open"] .gw-side { transform: none; }
  .gw-shell[data-drawer="open"] .gw-drawer-mask {
    display: block; position: fixed; inset: 0; z-index: 23;
    background: color-mix(in srgb, var(--color-neutral-900) 42%, transparent);
  }
  /* 抽屉里的行要按得准：桌面上 9px 的上下内边距在指头底下太矮。 */
  .gw-side .satu-nav { padding-top: 11px; padding-bottom: 11px; }

  .gw-main { grid-template-columns: minmax(0, 1fr) !important; border: 0; border-radius: 0; }
  .gw-head { gap: var(--space-2); padding: var(--space-2) var(--space-3); }
  .gw-aside { grid-row: 2; grid-column: 1; z-index: 5; border-left: 0; background: var(--color-neutral-100); }
  .gw-aside-grip { display: none; }
  .gw-page { padding: var(--space-4); }
  .gw-page-inner { gap: var(--space-4); }

  /* 顶栏一行要挤下汉堡、头像、名字、待办铃和三四颗切屏：头像让出来给名字。 */
  .gw-head .sw-convo-avatar { display: none; }
  .gw-head .satu-localtag { display: none; }

  /* 页面里那几处内联写死的两列 / 三列表单（个人设置、公司资料、弹窗里的表单），以及
     卡片网格那几条 minmax(300px+)：343 宽的内容区放两列，每个输入框只剩一百出头。
     数字小卡（概览、用量顶上那一排）反过来：auto-fit 到这个宽度会一张一行，四个数字
     要滚一整屏，收成两两一排。 */
  .gw-shell [style*="grid-template-columns: 1fr 1fr;"],
  .gw-shell [style*="grid-template-columns: repeat(3, 1fr)"],
  .gw-shell [style*="minmax(300px, 1fr)"],
  .gw-shell [style*="minmax(320px, 1fr)"] { grid-template-columns: minmax(0, 1fr) !important; }
  .gw-shell [style*="grid-template-columns: repeat(4, 1fr)"],
  .gw-shell [style*="minmax(190px, 1fr)"],
  .gw-shell [style*="minmax(150px, 1fr)"] { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
  .gw-shell [style*="grid-template-columns: 150px 1fr;"] { grid-template-columns: 96px minmax(0, 1fr) !important; }

  /* iOS Safari 碰到字号小于 16px 的输入框，一聚焦就把整页放大，放大了还不缩回去。 */
  .satu-prompt, .input { font-size: 16px; }

  .gw-modal-backdrop { padding: var(--space-4); }
  .gw-modal { padding: var(--space-4); max-height: calc(100dvh - var(--space-8)); overflow-y: auto; }
}

/* 表格 → 卡片（窄屏）。哪些是表、每格叫什么，由 render.js 的 cardifyTables 认出来、
   打上 gw-tt-* 的记号；这里只管画。行上的 grid 列宽是各页内联写的，所以要 !important。
   卡片的样子：第一格是标题，右上角是操作，下面每格一行「列名 … 值」。 */
@media (max-width: 760px) {
  .gw-tt-head { display: none !important; }
  /* 带上 .gw-shell：上面那几条按内联 grid 写法改列宽的规则也是两段选择器，同级的话
     后写的赢，这里要稳赢。 */
  .gw-shell .gw-tt-row {
    display: grid !important;
    grid-template-columns: minmax(0, 1fr) auto !important;
    gap: 6px var(--space-3) !important;
    align-items: center !important;
    padding: var(--space-3) var(--space-4) !important;
    min-height: 0 !important;
    text-align: left;
  }
  .gw-tt-row > * { grid-column: 1 / -1; min-width: 0; }
  .gw-tt-row > [data-tt-cell="title"] { grid-column: 1; grid-row: 1; }
  .gw-tt-row > [data-tt-cell="act"] { grid-column: 2; grid-row: 1; justify-self: end; }
  .gw-tt-cell { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); min-width: 0; }
  .gw-tt-cell::before {
    content: attr(data-label);
    flex: none; max-width: 45%;
    font-size: 12px; font-weight: 400; color: var(--muted-foreground);
  }
  /* 值那一侧靠右；长的允许折行，不再截成省略号——卡片里竖着有的是地方。 */
  .gw-tt-cell > * { min-width: 0; max-width: 100%; text-align: right; overflow-wrap: anywhere; white-space: normal; }
  .gw-tt-cell > :empty:only-child { display: none; }
  .gw-tt-cell:has(> :empty:only-child) { display: none; }
}

@media (max-width: 760px) and (prefers-reduced-motion: reduce) {
  .gw-side { transition: none; }
}

/* ══════════════════════════════════════════════════════════════════════
   手机壳（mobile/）那几条。

   index.html 的 viewport 带 viewport-fit=cover：页面铺到刘海和 Home 条底下，
   env(safe-area-inset-*) 才有值。浏览器里这几个值都是 0，下面等于没写——所以不按
   壳子分支，照 ≤760px 那一档写。

   - 外壳把安全区让出来（上面那段把 padding 写成 0 !important，这里要再盖一次）。
     让出来的那一圈是外壳自己的底色，刘海两侧看着就是页面的一部分。
   - 抽屉是 fixed 的、顶到底，自己也要让；弹窗的蒙版同理。
   - 登录那一屏不走外壳。
   ══════════════════════════════════════════════════════════════════════ */
@media (max-width: 760px) {
  .gw-shell { padding: env(safe-area-inset-top) 0 env(safe-area-inset-bottom) !important; }
  .gw-side {
    padding-top: calc(var(--space-4) + env(safe-area-inset-top)) !important;
    padding-bottom: calc(var(--space-2) + env(safe-area-inset-bottom)) !important;
  }
  .gw-login { box-sizing: border-box; padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom); }
  .gw-modal-backdrop {
    padding-top: calc(var(--space-4) + env(safe-area-inset-top));
    padding-bottom: calc(var(--space-4) + env(safe-area-inset-bottom));
  }
}

/* 软键盘：shell.js 的 startMobileShellFit 在键盘弹起时把可视高度写进 --satu-vvh，外壳跟着缩，
   正文自己让位。没写（键盘收了）就回到 100dvh。html.satu-mobile 只有手机壳会加。
   外壳的高度是 appView 内联写的，所以 !important。页面本身不该再弹（overscroll）：
   应用布局里能滚的是各栏，整页一弹就是一截底色露出来。 */
html.satu-mobile .gw-shell { height: var(--satu-vvh, 100dvh) !important; }
/* 键盘弹着时 Home 条那一截被键盘盖着，底下的安全区不用再让；不去掉就是键盘上沿一道底色（实测）。 */
html.satu-kbd .gw-shell { padding-bottom: 0 !important; }
html.satu-mobile, html.satu-mobile body { overscroll-behavior: none; }
