/* ai-chat-base.css */

/* ============================================
   Global Color Variables
   ============================================ */
:root {
  --color-text: #1a1a1a;
  --color-text-secondary: rgba(0,0,0,0.4);
  --color-text-tertiary: rgba(0,0,0,0.35);
  --color-bg-card: rgba(0,0,0,0.06);
  --color-bg-hover: rgba(0,0,0,0.04);
  --color-bg-panel: #fff;
  --color-bg-surface: #f0f0f0;
  --color-border: rgba(0,0,0,0.10);          /* ★ 全站唯一标准边线（卡片/容器/横条/图片轮廓）— 所有边线都必须用它，改这一处全站生效 */
  /* ★"在线/活着"唯一绿(本机连着、将来别处同义)。只标示【在】,不用它报警 —— 报警是另一回事。
     两档同一个亮绿:它是一盏灯,不是正文 —— 白天用深绿(曾 #1f8a4c)灯体发暗、光晕成黑影
     (owner 2026-08-28 实拍两次)。 */
  --color-live: #3ecf8e;
  /* ★"活着"那道光的唯一写法(本地手在线灯 · agent 干活中的头像)。两层 = 近处实、远处虚;
     三档同源:静止 / 呼吸低点 / 呼吸高点。数值只此一份,消费方别各写一遍 drop-shadow。 */
  --live-glow: drop-shadow(0 0 1px var(--color-live)) drop-shadow(0 0 3px var(--color-live));
  --color-warn: #c77700;                     /* ★"在做/等着"唯一黄。★它不是错:红(--color-danger)才表示要你动手 */
  /* ★"危险/要你动手"唯一红。原先它以 #e5484d 的字面量散在 6 处、注释还写着"全站唯一"却没有源;
     现在这里是唯一一份(2026-08-09 提取)。停止中的 agent、删除按钮、错误文字都喝这口。 */
  --color-danger: #e5484d;
  --color-border-light: var(--color-border);  /* 别名=标准边线；历史遗留写 var(--color-border-light) 的都归到这 */
  --footer-btn-box: 32px;                     /* ★ 消息 footer 图标按钮唯一盒尺寸(msg-iconbtn + variant-chevron);行预留带 = 它 + 6px 间距,改这一处全跟着走 */
  --header-btn-box: 36px;                     /* ★ 顶栏/浮动图标按钮唯一盒尺寸(.chat-header-btn)。给它让位的布局必须 calc 引用它,别照抄数字——设置弹窗侧栏就是抄了 16+36 写死 52,于是关闭钮与第一个 tab 的 hover 严丝合缝零间隙 */
  /* ★48px app 图标瓦片(品牌 / 模板 / 作品集图标框)的圆角【唯一源】—— 与图片缩略图的 --img-radius
     不是一回事:那是图片卡,这是图标框。比例按 iOS app 图标(≈0.22×边长)定,因为超椭圆就是照这个
     比例画的;比例一大它就退化成柠檬形(owner 2026-08-30 在 0.44 上看到的"超椭圆")。
     48 × 0.22 ≈ 11px = squircle 档要的值,所以基数 = 11 / 1.5。放大的同族(编辑器 80px 大预览)
     按边长比乘,保证"同一个形状放大"而不是"另一个圆角"。 */
  --tile-radius: calc(7.33px * var(--corner-k));
  /* ★同一个形状的【比例】版:定值圆角只在它算出来的那个尺寸上对(48px)。瓦片一被拉大,
     11px 就只剩一成,看着是方的(owner 2026-09-02 在系列瓦片上、2026-09-04 在 Files 网格上各报过一次)。
     22.4% = 手机 app 图标那个比例,跟着盒子走。凡是尺寸随格子变的方瓦片都用它。 */
  --tile-radius-pct: 22.4%;
  --img-radius: calc(14px * var(--corner-k));                         /* ★ 全站图片缩略图【圆角】唯一源（参考图画廊/agent图条/生成图容器）— 改此处全站生效 */
  --img-gap: 10px;                            /* ★ 全站图片【间距】唯一源（参考图画廊/agent图条）— 改此处全站生效 */
  --md-para-gap: 0.9em;                       /* ★ 全站 markdown 正文【段落间距】唯一源（气泡正文/草稿卡）— 2026-08-06 owner 定 0.9em(原0.5em 段落块感太弱)，改此处全站生效 */
  --menu-radius: calc(8px * var(--corner-k));                         /* ★ 全站【下拉菜单面】圆角唯一源（样板 .sidebar-action-menu）— owner 2026-08-06:菜单面统一无外边线,存量菜单路过就迁 */
  --menu-shadow: 0 4px 12px rgba(0,0,0,0.1);  /* ★ 全站【下拉菜单面】阴影唯一源，与上一条同族 */
  --modal-radius: calc(16px * var(--corner-k));
  /* ★ 浮层层级【唯一源】—— z-index 到了 100000 这一带的东西只许引用这五档,别在自己文件里写数字。
     档位的顺序由【谁能从谁里面打开】决定,每一档都写出逼出它的那个场景:
       --z-panel     面板/编辑器(.project-modal 家族:项目设置、品牌编辑器、维度面板)
       --z-viewer    全屏查看器 —— 文件是【从面板里】点开的,所以必须压过面板
       --z-selectbar 选中操作条 —— 长按选东西就在查看器里的网格上做,压不过查看器=看着像多选坏了
       --z-dialog    从查看器/操作条里叫出来的对话框(改名、删除确认、提示、移到项目) —— 必须压过叫它们的人
       --z-float     菜单/提示/取色器/toast —— 不管上面开着什么,它们永远在最上面
     以前这五档各自把数字写在自己的样式表里,谁被压住谁就把自己抬一级,抬完又压住别人
     (2026-09-05 查看器抬到 100003,同时盖住了改名弹窗和选中操作条)。右键菜单 999999 仍在全部之上。 */
  --z-panel: 100002;
  --z-viewer: 100003;
  --z-selectbar: 100004;
  --z-dialog: 100005;
  --z-float: 100006;
  /* 输入框胶囊的倒角/阴影唯一源(composer-pill 与从它拉起的 ask-choice 面板同值 —— 两个要读成一件东西) */
  --composer-pill-radius: 25px;
  --composer-pill-shadow: 0 1px 6px rgba(32, 33, 36, 0.28);
                     /* ★ 全站【弹窗/对话框窗口】倒角唯一源（project-modal-content/pdf-viewer-dialog/camera-modal/settings-modal）— 改此处全站生效。锚定下拉/popover 与移动上拉 sheet(--duo-sheet-radius) 属另类，不用此源 */
  /* ★ 可悬停元素的圆角三档唐一真源（按用途分，不按模块）：
     menu-row  = 下拉菜单里的行（.action-item / .project-chat-menu-item）
     list-row  = 列表行（侧栏聊天项 / 查看器图层行）
     icon-btn  = 图标按钮的 hover 底（工具条 / 三点 / 关闭）
     新增可悬停元素先对号入座，别再写数字 */
  --radius-menu-row: calc(8px * var(--corner-k) - 4px)   /* menu 8*k - padding 4 */;
  --radius-list-row: calc(8px * var(--corner-k));
  --radius-icon-btn: calc(10px * var(--corner-k));   /* owner 2026-08-06: 定 10(实测多数派=.msg-iconbtn 全族);原 6 只有 file-preview 一个消费方 */
  /* ★胶囊被【拉满宽】时的尺寸档唯一源(变体类 .project-modal-btn-block)。胶囊真源那套
     7px/12px 是给【内容宽】按钮定的,拉成整行后同样的高度会读成一条被拉长的输入框。
     消费方:设置弹窗手机版主按钮 · Studio 上传落点。数值只此一份,别在消费方再写一遍。 */
  /* 两个按钮尺寸都在这里取名。胶囊 = 弹窗/设置行用的标准那颗;块 = 窗口底部动作行加高的那一档
     (下面那张选择器清单说明哪些容器算底部动作行)。 */
  --btn-pill-pad-y: 7px;
  --btn-pill-pad-x: 12px;
  --btn-pill-font: 12px;
  --btn-block-pad-y: 12px;
  --btn-block-font: 14px;
  --meta-text: #9ca3af;                       /* ★ 回复上方【过程/元信息行】文字色唯一源（思考条 .agent-seg-head、记忆通知 .memory-notice-header）— 同一族必须同色，改此处全站生效 */
  --avatar-bg: #1a1a1a;                       /* ★ 头像(圆形缩写/节点圆圈)背景唯一源 — 全站头像都用它，改此处全站生效 */
  --avatar-fg: #ffffff;                       /* ★ 头像文字色唯一源 */
  /* ★头像家族里那一档【更亮的实色】唯一源:成员组的 "+N" 圆、训练维度栈里你正在的那一颗。
     两个消费方读同一处 —— 深浅要调就调这里。它两色模式同值(和 "+N" 一直以来一样),所以没有暗色覆盖。 */
  --avatar-bg-lit: #6b7280;
  --chevron-trigger-size: var(--icon-size);               /* ★ 下拉触发 chevron 字形唯一源（版本/图片筛选+项目排序/设置语言/分享角色+访问/账户语言）— 改此处全站生效。不含行内导航›、分组折叠、翻页‹› */
  --color-bg-page: #fff;
  --duo-header-h: 60px;   /* 顶栏统一高度(单一真源):sidebar / Studio 架构区 / 会议室 header 共用,三条分割线对齐 */
  --duo-scrollbar-w: 8px; /* ★滚动条宽度唯一真源:.duo-scroll 画它,容器外的邻居(不滚动的 header/footer)要预留同样一条才对得齐右缘 */
  --duo-window-gap: 4px;  /* ★"圆角窗口"离屏幕边的缝隙唯一真源:Studio(四周+中缝,品牌色从这透出)与无痕模式共用 —— 改这一处两边一起变 */
  --duo-drop-tint: rgba(201,178,124,0.12);   /* 拖放高亮金色蒙层(单一真源):【仅主聊天拖图】用金色 */
  --studio-drop-tint: rgba(0,0,0,0.07);        /* ★Studio 拖放蒙层(拖品牌卡/拖 agent):去色中性、不带金色品牌属性;亮色=深灰淡覆盖,暗色见 .dark-mode 覆盖 */
  --color-shadow: rgba(0,0,0,0.15);
  /* ★"变暗/淡显"唯一真源:全站只有这一个值(owner 2026-08-08 定死"统一一个值 0.4")。
     凡是要把某样东西压下去 —— 拖起来的源件、不能点的控件、这一轮不参与的、已定稿只读的、
     空态图标与次要说明字、上传时的忙碌遮罩 —— 一律 var(--dim),不分档、不写裸数字、
     也不给暗色开特例。收口前它散在九个样式表里,同一个状态能有三个深浅。
     ★唯一一处不同量纲:忙碌遮罩用它当 backdrop-filter 的 brightness(压背后的真实像素,
       所以进度扇形能挖洞露出原色),其余是元素自己的 opacity。同一个数,两种用法。 */
  --dim: 0.4;
  --press-hover: 0.93;    /* 指针悬停时的压暗量(filter: brightness) */
  --press-active: 0.88;   /* 按下时再深一档,与 hover 成对出现 */
  --press-overlay: rgba(0,0,0,0.12);   /* the same darkening as an overlay, for elements that must not gain a layer on press (iOS) */
  --icon-size: 20px;      /* every ordinary icon (owner 2026-09-15) */
  --icon-size-sm: 16px;   /* icons beside small text: row chevrons, checks, inline marks */
  --icon-stroke: 1.75px;  /* every line icon's stroke, on screen, at any size it is shown (owner 2026-09-13) */
  --duo-scrim-blur: 6px;  /* ★ how blurred the page is behind an open window. 20px hid it completely (owner 2026-09-16); at 6px the page still reads through. One value for every scrim below. */
}
/* ── The scrim behind an open window: one place says how blurred the page is ──────────
   Each scrim keeps its own tint where it is declared (a dialog dims less than a viewer);
   what is shared is the blur, so the app cannot end up with two strengths of it.
   Reduced transparency turns it off by zeroing the token - the tint alone still separates
   the window from the page, and there is no second copy of this list to keep in step. */
.settings-modal-overlay,
.project-modal-backdrop,
.settings-memory-backdrop,
#projectSettingsModal .project-settings-sheet-backdrop,
.wechat-qr-overlay,
.pdf-viewer-overlay,
.ai-image-viewer .viewer-backdrop,
.camera-modal,
.sources-sheet-overlay.visible .sources-sheet-backdrop {
  -webkit-backdrop-filter: blur(var(--duo-scrim-blur));
          backdrop-filter: blur(var(--duo-scrim-blur));
}
@media (prefers-reduced-transparency: reduce) {
  :root { --duo-scrim-blur: 0px; }
}
/* A line icon (class duo-icon) is shown at 11px here and 24px there: a stroke-width in viewBox units scales with it,
   so the same icon read heavier or lighter by where it sat. non-scaling-stroke measures the stroke in screen px
   instead, so one token sets every line icon. Gate: scripts/ui/icon-stroke-weight.test.mjs */
.duo-icon :is(path, line, polyline, polygon, circle, ellipse, rect) { vector-effect: non-scaling-stroke; stroke-width: var(--icon-stroke); }
/* Every UI icon is one of two sizes (owner 2026-09-15). A place that shows the small one says so: its own size rule
   reads --icon-size-sm, or the icon carries duo-icon-sm. A width attribute on the svg no longer decides anything. */
.duo-icon { width: var(--icon-size); height: var(--icon-size); }
.duo-icon.duo-icon-sm { width: var(--icon-size-sm); height: var(--icon-size-sm); }
/* An app icon is a whole app's mark, ground and all (class duo-app-icon, ground + mark named inside it). On dark
   paper it turns over - light ground, dark letter (owner 2026-09-17) - so the mark reads with the same weight on
   either side. Gate: scripts/ui/app-icon-dark.test.mjs */
.duo-app-icon { border-radius: var(--tile-radius-pct); overflow: hidden; }
.duo-app-icon .app-icon-ground { fill: #1c1917; }
.duo-app-icon .app-icon-mark { fill: #f5f1ea; }
.dark-mode .duo-app-icon .app-icon-ground,
.dark-mode-active .duo-app-icon .app-icon-ground { fill: #f5f1ea; }
.dark-mode .duo-app-icon .app-icon-mark,
.dark-mode-active .duo-app-icon .app-icon-mark { fill: #1c1917; }
/* ★ Bottom action rows of EVERY window - desktop and phone alike - take the block size (the pair of tokens
   above), the same size the phone sheets give their last row (owner 2026-08-28). Scoped to the footer
   containers only: pills inside the body (Add a note, compare, chips) keep the standard size. Add a new
   footer container here, never re-declare the size beside it. */
.project-modal-actions .project-modal-btn,
.project-modal-footer .project-modal-btn,
.share-footer-invite .project-modal-btn,
.invite-modal-actions .project-modal-btn,
.studio-bench-actions .project-modal-btn,
.studio-ability-foot .project-modal-btn,
.tp-obj-row .project-modal-btn,
.edit-profile-btn-cancel,
.edit-profile-btn-save,
.share-copy-btn { padding: var(--btn-block-pad-y) 14px; font-size: var(--btn-block-font); }

/* Insertion caret everywhere in the app = the theme ink (white in dark, black in light). Without this,
   Android Chrome paints the caret with the OS accent colour, not the text colour (owner 2026-08-27). */
input, textarea, [contenteditable="true"] { caret-color: var(--color-text); }
.dark-mode,
.dark-mode-active {
  --color-text: #e5e5e5;
  --color-text-secondary: rgba(255,255,255,0.4);
  --color-text-tertiary: rgba(255,255,255,0.35);
  --studio-drop-tint: rgba(255,255,255,0.10);   /* ★Studio 拖放蒙层暗色:浅灰淡覆盖(去色中性) */
  --color-warn: #f0a92e;                      /* 同理:暗底上土黄会闷掉 */
  --color-bg-card: rgba(255,255,255,0.08);
  --color-bg-hover: rgba(255,255,255,0.06);
  --color-bg-panel: #2a2a2a;
  --color-bg-surface: #333;
  --meta-text: #8a8a8a;                       /* ★ 暗色过程/元信息行文字色（思考条与记忆通知同色） */
  --color-border: rgba(255,255,255,0.10);     /* ★ 暗色标准边线，与亮色同等视觉分量 */
  --color-border-light: var(--color-border);
  --color-bg-page: #1a1a1a;
  --avatar-bg: #444444;                       /* ★ 暗色头像背景（与头像规范一致） */
  --color-shadow: rgba(0,0,0,0.4);
  --menu-shadow: 0 4px 24px rgba(0,0,0,0.4);  /* ★ 暗色菜单面阴影:亮色的黑10%在深底上不可见,取基准侧栏菜单的实测暗色值 */
}

/* ============================================
   ★ 全站唯一滚动条标准 —— 任何可滚动容器加 class="duo-scroll" 即调用此模板。
   改滚动条(宽度/颜色/圆角)只改这一处，全站生效。
   ⚠️ 只用 ::-webkit-scrollbar 控制，【禁止】在此再加标准属性 scrollbar-width / scrollbar-color：
      Chrome 121+ 一旦看到标准 scrollbar-width，就会忽略 ::-webkit-scrollbar 的自定义 width，
      导致宽度怎么改都没反应（本 bug 的根因）。想要精确 px 宽度只能走 webkit。
      代价：Firefox 回退原生滚动条（Firefox 本就不支持任意 px 宽度）。
   Exception: deliberately hidden HORIZONTAL strips (composer-attachments / images-editor-thumbs
   use scrollbar-width:none) write their own rule and skip .duo-scroll.
   Mobile pull-up sheets are NOT an exception (owner 2026-08-27): they use this template as-is and
   only start lower - the one track-inset rule lives in ai-chat-sheet.css. The four private copies
   that used to hide it (settings / card sheet / share) are gone; do not bring them back.
   ============================================ */
/* ★ 全屏浮层/查看器关闭键单一真源:36 圆 + 16 图标(底色/颜色/定位由各场景自带)。用:图片编辑器/图片灯箱/相机 */
.duo-close-lg {
  width: 36px; height: 36px;
  border: none; border-radius: 50%;
  corner-shape: round;
  cursor: pointer; padding: 0;
  display: flex; align-items: center; justify-content: center;
  transition: background 0.15s, color 0.15s;
}
.duo-close-lg svg { width: var(--icon-size-sm); height: var(--icon-size-sm); }

.duo-scroll::-webkit-scrollbar { width: var(--duo-scrollbar-w); height: var(--duo-scrollbar-w); }
.duo-scroll::-webkit-scrollbar-track { background: transparent; }
/* ★滑块的深浅对齐【系统原生滚动条】(Chrome 亮 #c1c1c1 ≈1.80:1 / 暗 #6b6b6b ≈2.66:1)——
   人眼对滚动条的深浅是被原生校准过的,比原生淡就会读成"没有滚动条"。
   原来两档都是 0.10 = 和发丝边线 --color-border 同一个值(把控件当线画),实测对比度只有
   1.34:1(暗)/1.25:1(亮),表格这种【横向滚动是唯一位置线索】的地方等于没有(用户 2026-07-29 实测)。 */
.duo-scroll::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.24); border-radius: calc(3px * var(--corner-k)); }
.dark-mode .duo-scroll::-webkit-scrollbar-thumb,
.dark-mode-active .duo-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.28); }
html:not([data-input="touch"]) .duo-scroll::-webkit-scrollbar-thumb:hover{ background: rgba(0,0,0,0.38); }
html:not([data-input="touch"]) .dark-mode .duo-scroll::-webkit-scrollbar-thumb:hover,
html:not([data-input="touch"]).dark-mode-active .duo-scroll::-webkit-scrollbar-thumb:hover{ background: rgba(255,255,255,0.45); }
/* ★两条滚动条同时出现时,右下角那块【角】和 resizer 若不给色,会退回浏览器默认的浅色 = 一个白点。
   模板以前只覆盖 scrollbar/track/thumb,是因为全站在此之前【没有一个两轴都滚的容器】,
   直到 xlsx 表格(2026-07-29 用户实测)。这里补齐整套伪元素,以后任何两轴容器都不会再冒白点。 */
.duo-scroll::-webkit-scrollbar-corner,
.duo-scroll::-webkit-resizer { background: transparent; }

/* Phase D 按需拉：被 evict 的老会话拉取失败/空的友好占位（--color-text-secondary 自动适配暗色） */
.chat-load-placeholder {
  padding: 24px 20px;
  text-align: center;
  font-size: 13px;
  line-height: 1.5;
  color: var(--color-text-secondary);
}

.ai-design-section {
  display: flex;
  height: 100vh;
  height: 100dvh; /* iOS: 键盘弹出时收缩到可见区域，防止欢迎页 composer 被遮挡 */
  width: 100vw;
  overflow: hidden;
  overscroll-behavior: none; /* 内层滚动到边界不外链、app 根不回弹（配合 theme.liquid 的 html/body 视口锁）*/
  background-color: var(--color-bg-page);
  letter-spacing: 0.02em; /* Jost 在小字号下默认 tracking 偏紧，统一加 0.02em 让 sidebar/下拉/按钮等更松弛 */
}

/* Main Chat Area */
.chat-main {
  flex-grow: 1;
  display: flex;
  flex-direction: column;
  position: relative;
  height: 100%;
  width: 100%;
  overflow: hidden;
}

/* ============================================
   Chat Header
   ============================================ */
.chat-header {
  display: flex;
  align-items: center;
  /* 从右边排:右边那组(电脑·无痕·动作)谁显示谁隐藏都贴右,首帧即终态;唯一的左手件(项目面包屑)
     用 margin-right:auto 留在左边。曾是 space-between + 电脑按钮的 margin-left:auto 把无痕推过去,
     而那颗按钮要问过 worker 才出现 → 无痕先在最左、0.1s 后跳右(owner 2026-09-06)。 */
  justify-content: flex-end;
  height: 60px;
  padding: 0 16px;
  background: var(--color-bg-panel);
  border-bottom: 1px solid var(--color-border);
  flex-shrink: 0;
  position: fixed;
  top: 0;
  left: var(--sidebar-width, 240px); /* ✅ Follow sidebar */
  right: 0;
  z-index: 100;
  transition: left 0.3s cubic-bezier(0.2, 0, 0, 1); /* ✅ Sync with sidebar */
}

/* ✅ When sidebar is collapsed */
.chat-sidebar.collapsed ~ .chat-main .chat-header {
  left: var(--sidebar-width-collapsed, 60px);
}








/* 展开时箭头翻转:Lab 版本选择器 + Studio 品牌名下拉共用同一条 */
.studio-org-brand-wrap.open .version-chevron {
  transform: rotate(180deg);
}

.version-dropdown {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  min-width: 220px;
  background: var(--color-bg-panel);
  border-radius: calc(12px * var(--corner-k));
  box-shadow: var(--menu-shadow);
  padding: 8px 0;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-4px);
  transition: opacity .15s ease, transform .15s ease, visibility .15s;
  z-index: 200;
}

/* 展开态:DuoHand 版本选择器 + Studio 架构 header 品牌名下拉共用同一条(别再复制一份声明) */
.studio-org-brand-wrap.open .version-dropdown {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.version-dropdown-title {
  padding: 8px 16px;
  font-size: 14px;
  font-weight: 500;
  color: var(--color-text-secondary);
  user-select: none;
}

.version-dropdown-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 16px;
  font-size: 14px;
  color: #111;
  cursor: default;
  transition: background .1s ease;
}
html:not([data-input="touch"]) .version-dropdown-item:hover{
  background: var(--color-bg-hover);
}
.version-dropdown-item:active {
  background: var(--color-bg-hover);
}

.version-dropdown-check {
  display: none;
  align-items: center;
  color: #111;
  line-height: 0;
  flex-shrink: 0;
}
.version-dropdown-item.selected .version-dropdown-check { display: inline-flex; }

/* 下拉项：双行布局（名称 + 描述） */
.version-dropdown-label { display: flex; flex-direction: column; gap: 2px; }
.version-dropdown-desc {
  font-size: 12px;
  color: var(--color-text-secondary);
  font-weight: 400;
}

.chat-header-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  position: relative;
}

.chat-header-btn {
  width: var(--header-btn-box);
  height: var(--header-btn-box);
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  border-radius: calc(6px * var(--corner-k));
  cursor: pointer;
  color: rgba(0, 0, 0, 0.6);
  transition: background 0.15s, color 0.15s;
}
html:not([data-input="touch"]) .chat-header-btn:hover{
  background: rgba(0, 0, 0, 0.05);
  color: rgba(0, 0, 0, 0.85);
}
/* 触屏按下反馈:hover 的配对 active(5.3 规范)。暗色那半在 ai-chat-sidebar.css 早就有,
   亮色一直缺 —— 手机限定按钮(sheet 关闭钮)只有这条能给反馈 */
html:not([data-input="touch"]) .chat-header-btn:active{
  background: rgba(0, 0, 0, 0.05);
  color: rgba(0, 0, 0, 0.85);
}
.chat-header-actions:has(.chat-header-menu.active) .chat-header-btn {
  background: rgba(0, 0, 0, 0.05);
  color: rgba(0, 0, 0, 0.85);
}

/* 返回所属项目面包屑(聊天属于某项目时显示,点击客户端切换回项目页)。
   数值全取 .chat-header-btn 基准:36px 高/6px 圆角/0.6 灰/hover 0.05+0.85。 */
/* The left slot's geometry, named here because two things take turns in it: the crumb (chevron + name) inside a
   project, the chat's own name outside one. Their names must start at the same x, so the title reads these. */
:root {
  --crumb-pad-left: 6px;     /* 左侧 chevron 补偿视觉对称(5.1 可点击 padding 规范) */
  --crumb-icon-w: var(--icon-size);   /* the chevron box is one icon wide - measured 20, the icon system size */
  --crumb-gap: 6px;
  --crumb-slot-left: 0px;    /* the left edge of the header's own padding: the slot the model picker held is empty
                                since 2026-09-06, and an empty run in front of the name is just an empty run */
  --title-pad-x: 12px;        /* the chat name button's own breathing room, taken back out of its margin so the
                                LETTERS line up with the crumb's name, not the button edge */
}
.chat-project-crumb {
  display: flex;
  align-items: center;
  gap: var(--crumb-gap);
  margin-right: auto;   /* the one left-hand thing in a flex-end header */
  max-width: 240px;
  height: 36px;
  padding: 0 10px 0 var(--crumb-pad-left);
  border: none;
  background: transparent;
  border-radius: calc(6px * var(--corner-k));
  cursor: pointer;
  color: rgba(0, 0, 0, 0.85);            /* the crumb's own value (it inherited it from the header model label that sat in this slot until 2026-09-06) */
  font-size: var(--text-h4);             /* 逐字节继承版本标签字号基准 */
  letter-spacing: 0.05em;                /* the crumb's own value (it inherited it from the header model label that sat in this slot until 2026-09-06) */
  font-family: "Flair", "Jost Fallback", "PingFang SC", "Microsoft YaHei", sans-serif;
  transition: background 0.15s, color 0.15s;
}
.chat-project-crumb[hidden] { display: none; }

/* The chat's own name in the same slot, and the same shape as the crumb - one is for projects, the other for plain
   chats, and they never show together. No chevron, so no left compensation. */
/* The name and the chevron beside it are one block: the block takes the header's left slot and the auto margin,
   so the two move together and the name button stays a box around the name alone. */
.chat-header-name {
  display: flex;
  align-items: center;
  gap: 2px;
  margin-right: auto;
  margin-left: var(--crumb-slot-left);
  min-width: 0;
}
.chat-header-name[hidden] { display: none; }
/* What the chevron opens is the chat's own menu (share / rename / add to project / mark final / delete) - the same
   one the sidebar row's dots open. It is a separate button from the name, because the name is the rename target. */
.chat-header-title-menu {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  flex-shrink: 0;
  border: none;
  background: transparent;
  border-radius: calc(6px * var(--corner-k));
  corner-shape: round;
  cursor: pointer;
  color: var(--color-text-secondary);
  padding: 0;
}
html:not([data-input="touch"]) .chat-header-title-menu:hover { background: var(--color-bg-hover); }
.chat-header-title-menu svg { width: var(--icon-size-sm); height: var(--icon-size-sm); }
.chat-header-title {
  display: flex;
  align-items: center;
  max-width: 280px;
  height: 36px;
  padding: 0 var(--title-pad-x);
  border: none;
  background: transparent;
  border-radius: calc(6px * var(--corner-k));
  corner-shape: round;
  cursor: pointer;
  font: inherit;
  color: var(--color-text);
}
html:not([data-input="touch"]) .chat-header-title:hover { background: var(--color-bg-hover); }
.chat-header-title[hidden] { display: none; }
/* The editable span is built by DuoInlineRename inside the button; it takes the size from the button, so the
   phone's own .chat-header-title rule (13px) still decides there. */
.chat-header-title { font-size: 14px; }
/* The inline-rename base pulls itself 4px left (margin -4 / padding-left 4) so it can sit flush with running text
   elsewhere. Inside this button that stole 4px from the left side only - measured 8 left against 12 right. Here the
   name fills the button, so the pull is cancelled and both sides come from --title-pad-x alone. */
.chat-header-title .duo-inline-rename { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-left: 0; padding-left: 0; padding-right: 0; }
/* While it is being edited it stops being a button and reads as a field: the ring is the focus ring the rest of the
   site uses, so the name sits in a rounded box with its text selected. */
.chat-header-title:has(.is-editing) { cursor: text; background: var(--color-bg-hover); }
/* The crumb takes the header's left slot (35px, where the model picker sat until 2026-09-06; the picker is on the composer now) */
.chat-header.has-project-crumb .chat-project-crumb { margin-left: var(--crumb-slot-left); }
@media (max-width: 768px), (max-height: 500px) {
  /* 箭头与汉堡几何级对齐:汉堡=viewport left 12px 的 40px 盒、图形居中(ai-chat-sidebar.css 739)。
     header 左 padding 16px → margin -4px 使 crumb 盒左缘落 12px;图标容器撑成 40px 盒居中
     → 两个图形同盒同中心,光学必然一致(不受各自 viewBox 内缩差异影响)。 */
  .chat-header.has-project-crumb .chat-project-crumb { margin-left: -4px; padding-left: 0; gap: 2px; }
  .chat-header.has-project-crumb .chat-project-crumb-icon { width: 40px; justify-content: center; }
  /* On the phone the chevron sits in a 40px box aligned to the hamburger, and the crumb's own padding is dropped
     (margin -4px, padding-left 0) - the title starts where that leaves the name. */
  /* the phone drops the slot push and the crumb's own padding (margin -4px, padding-left 0), and widens the
     chevron box to 40px to line up with the hamburger - the name starts after that box and the gap */
  /* Here the hamburger, not a chevron, holds the left run when there is no crumb: it is a fixed 40px box at
     viewport left 12px and it sits ON TOP of the header, so the button starts where that box ends - measured, its
     own tint used to slide 10px underneath it. */
  .chat-header-name { margin-left: calc(12px + 40px - 16px); }   /* the hamburger box, less the header padding it is measured from */
  /* 左箭头替换汉堡(Claude 式)。!important 是必要对抗:汉堡基准自带
     display:flex!important(ai-chat-sidebar.css 739)+position:fixed 浮在 header 上,
     不同级压制必重影——非偷懒,存量 !important 的对等反制。 */
  body.duo-project-crumb .mobile-menu-trigger { display: none !important; }
}
.chat-project-crumb-icon { display: flex; align-items: center; }
/* 箭头用 icon-chevron-left-large(自带 22px 内联尺寸)= 与汉堡图标同尺寸,不另定值。
   ★chevron 家族(2026-07-26 命名降级后只剩两档):
     icon-chevron-*        —— 不带内联尺寸、带 .chevron-icon class,尺寸交给 CSS(--chevron-trigger-size 那一族下拉触发)
     icon-chevron-*-large  —— 自带 22px 内联尺寸(面包屑/手机记忆/图片灯箱/PDF 上下页/查看器返回) */
/* The name takes the size of a label, not of body copy: with no size of its own it inherited the 16px the
   messages use, and the crumb read as a page heading (owner 2026-09-17) */
.chat-project-crumb-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; }
html:not([data-input="touch"]) .chat-project-crumb:hover{ background: rgba(0, 0, 0, 0.05); }
.chat-project-crumb:active { background: rgba(0, 0, 0, 0.05); }

.chat-header-btn svg {
  width: var(--icon-size);
  height: var(--icon-size);
}
/* The two that carry a state - the computer link and the incognito ghost - sit a touch bigger than the rest of the
   row: both are read for "is it on", not for their drawing (owner 2026-09-17). One rule for the pair, so they stay
   the same size as each other; registered in scripts/ui/icon-size-keyline.test.mjs as a place with its own size. */
#chatBenchBtn svg,
#btnIncognito svg {
  width: 22px;
  height: 22px;
}

/* 主聊天顶栏那个"电脑在不在":外观与状态整套复用 .studio-bench-btn(24px 墨迹、亮绿、光晕
   都只在那一处定义)。
   它什么时候出现取决于一次网络往返(问过 worker 才知道配没配电脑),所以它绝不能左右别人的位置:
   header 是 flex-end,右边这组各自贴右,它来不来无痕都在原地。Studio 那条 margin-left:auto /
   margin-right:-14px 在这里归零;右边 8px = .chat-header-actions 本来就用的按钮间距,header 没有
   gap,不给就会贴着无痕那个幽灵。 */
#chatBenchBtn.studio-bench-btn { margin-left: 0; margin-right: 8px; }

/* Dropdown menu */
.chat-header-menu {
  display: none;
  position: absolute;
  right: 0;
  top: 100%;
  margin-top: 8px;
  background: var(--color-bg-panel);
  box-shadow: var(--menu-shadow);
  border-radius: calc(8px * var(--corner-k));
  padding: 4px;
  min-width: 180px;
  z-index: 1000;
}

.chat-header-menu.active {
  display: block;
}

.chat-menu-title {
  padding: 8px 12px 4px 16px;
  font-size: 12px;
  font-weight: 400;
  color: rgba(0, 0, 0, 0.45);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 220px;
}
/* Full-bleed menu divider utility (owner 2026-08-06: 分割线一律贯穿容器).
   New menus use this class; containers with horizontal padding set --menu-pad to it. */
.menu-divider {
  height: 1px;
  background: var(--color-border);
  margin: 4px calc(-1 * var(--menu-pad, 0px));
}

.chat-menu-divider {
  height: 1px;
  background: rgba(0, 0, 0, 0.08);
  margin: 4px -4px;   /* full-bleed: cancels the menu's 4px padding */
}

.chat-menu-item {
  padding: 8px 12px;
  font-size: 14px;
  color: rgba(0, 0, 0, 0.85);
  cursor: pointer;
  border-radius: calc(8px * var(--corner-k) - 4px);
  white-space: nowrap;
  transition: background 0.15s;
  display: flex;
  align-items: center;
  gap: 8px;
}
html:not([data-input="touch"]) .chat-menu-item:hover{
  background: var(--color-bg-hover);
}

.chat-menu-item--danger {
  color: #d32f2f;
}
.chat-menu-item--danger .action-icon,
.chat-menu-item--danger .action-icon svg {
  color: #d32f2f;
  stroke: #d32f2f;
}
html:not([data-input="touch"]) .chat-menu-item--danger:hover{
  background: rgba(211, 47, 47, 0.08);
}

/* Mobile responsive */
@media (max-width: 768px), (max-height: 500px) {
  /* ✅ Reset header to full width on mobile (sidebar is drawer) */
  .chat-header {
    left: 0 !important;
  }


  .chat-header-title {
    font-size: 13px;
  }

  .chat-title-text {
    max-width: 200px;
  }
}

.chat-container {
  display: flex;
  flex-direction: column;
  height: 100%;
  width: 100%;
  margin: 0;
  padding: 0;
  padding-top: 60px; /* Header height */
  justify-content: center; /* 默认垂直居中 */
}

/* 当有消息时，切换为从顶部开始布局 */
.chat-container.has-messages {
  justify-content: flex-start;
}

/* 欢迎页标题字体（owner 2026-09-06）：拉丁 = 产品同一款 Jost、字重 300（比正文小一档）；中文 = 苹方(Mac)→微软雅黑(Win)→Noto。
   300 这一档的 @font-face 在 theme.liquid head 与 custom-styles.css；不闪靠 head 里 preload + 页面门控（duohand-app-ui.liquid） */
#initial-message .h1 {
  font-family: "Fraunces", "PingFang SC", "Microsoft YaHei", "Noto Sans SC", "Noto Sans JP", serif !important;
  font-weight: 400 !important;
  font-variation-settings: "SOFT" 70, "WONK" 0, "opsz" 40;
}

/* 欢迎页标题与副标题间距（覆盖 theme.css 的 prose 子元素间距） */
#initial-message .prose > .h1 + div {
  margin-top: 6px !important;
}
/* 副标题与输入框间距 */
#initial-message {
  padding-bottom: 24px;
}

/* JS 添加此类后，初始消息将隐藏 */
.chat-container.has-messages #initial-message {
  display: none;
}
.chat-container:not(.has-messages) .chat-messages {
  display: none; /* 初始状态下隐藏空的消息容器 */
}

/* ★这类窗口(消息列表:主聊天 / 会议室 / 训练台)里【不许有任何东西比窗口宽】(owner 2026-09-03)。
   靠的是每个产生点自己守规矩(宽表格/代码块在自己的盒子里滚、窄了跟着面板收),不是拿 overflow-x
   把已经溢出的东西藏起来 —— 藏了内容会被裁掉,而且下一个超宽的东西照样没人发现。 */
.chat-messages {
  flex-grow: 1;
  overflow-y: auto;
  padding: 1rem;
  padding-top: 4rem; /* 为顶部图标留出空间 */
  scroll-padding-top: 4rem; /* scrollIntoView 时避开 fixed header */
}

.chat-messages .msg {
  display: flex;
  flex-direction: column;
  margin-bottom: 1rem;
  margin-left: auto;
  margin-right: auto;
  max-width: 800px;
  align-items: flex-start; /* Default for assistant */
}

.chat-messages .msg.user {
  align-items: flex-end;
}

.chat-messages .bubble {
  max-width: 85%;
  padding: 10px 14px;
  border-radius: calc(18px * var(--corner-k));
  font-size: 14px;
  line-height: 1.6;
  white-space: pre-wrap;
  word-break: break-word;
  border: 1px solid var(--color-border);
  background: var(--color-bg-panel);
  width: fit-content;
  display: inline-block;
}

/* ✅ Assistant bubble: no decoration, full width (ChatGPT-style) */
.chat-messages .msg.assistant .bubble {
  max-width: 100%;
  width: 100%;
  background: transparent;
  border: none;
  border-radius: 0;
  padding: 0;
  box-shadow: none;
}

.chat-messages .msg.user .bubble {
  background: #f9fafb;
  border-color: transparent;
}

/* Ensure bubble text alignment is correct */
.chat-messages .bubble,
.chat-messages .bubble .bubble-content,
.chat-messages .bubble .bubble-text {
  text-align: left !important;
}

.composer-bar {
  padding: 1rem;
  padding-top: 0;
}

/* =========================
   Dark mode
   ========================= */
.dark-mode .ai-design-section { background-color: #212121; }
.dark-mode .chat-header { background: #212121; border-bottom-color: var(--color-border); }
.dark-mode .version-dropdown-item { color: rgba(255,255,255,0.9); }
.dark-mode .version-dropdown-check { color: #fff; }
.dark-mode .chat-header-btn { color: rgba(255,255,255,0.6); }
.dark-mode .chat-project-crumb { color: rgba(255,255,255,0.85); }
/* 同暗色版本标签 */
html:not([data-input="touch"]) .dark-mode .chat-project-crumb:hover{ background: rgba(255,255,255,0.08); }
.dark-mode .chat-project-crumb:active { background: rgba(255,255,255,0.08); }
html:not([data-input="touch"]) .dark-mode .chat-header-btn:hover{ background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.85); }
.dark-mode .chat-header-actions:has(.chat-header-menu.active) .chat-header-btn {
  background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.85);
}
.dark-mode .chat-menu-title { color: rgba(255,255,255,0.4); }
.dark-mode .chat-menu-divider { background: rgba(255,255,255,0.08); }
.dark-mode .chat-menu-item { color: rgba(255,255,255,0.85); }
.dark-mode .chat-menu-item--danger { color: #ef5350; }
.dark-mode .chat-menu-item--danger .action-icon,
.dark-mode .chat-menu-item--danger .action-icon svg { color: #ef5350; stroke: #ef5350; }
.dark-mode .chat-messages .bubble { border-color: var(--color-border); }
.dark-mode .chat-messages .msg.assistant .bubble { background: transparent; border: none; }
.dark-mode .chat-messages .msg.user .bubble { background: #303030; border-color: transparent; }

/* ── DuoHand 取色器（window.DuoColorPicker）—— 替代无法被 CSS 触及的原生 <input type=color> 弹窗 ──
   append 到 body(脱离 transform 祖先);圆角/边线/阴影全走全站 token;亮暗双套。
   float 档:只从品牌弹窗里打开,挂 body 就得压过开着的弹窗,否则开在弹窗背面 */
.duo-cp {
  position: fixed; z-index: var(--z-float); width: 220px; padding: 10px;
  box-sizing: border-box; border-radius: calc(14px * var(--corner-k));
  background: var(--color-bg-page); border: 1px solid var(--color-border);
  box-shadow: 0 8px 24px var(--color-shadow);
  display: flex; flex-direction: column; gap: 10px;
  /* touch-callout 与 user-select 是两套机制:后者管划选,前者管 iOS 长按弹的 Copy/翻译。
     它挂在 body 上,查看器/弹窗那些按子树写的防护都罩不到,得自己带。 */
  user-select: none; -webkit-user-select: none; touch-action: none;
  -webkit-touch-callout: none;
}
.duo-cp-sv {
  position: relative; height: 120px; border-radius: 0; cursor: crosshair;   /* 拖色面板直角(用户指定);色相条与弹层圆角不变 */
  border: 1px solid var(--color-border);
  background:
    linear-gradient(to top, #000, rgba(0,0,0,0)),
    linear-gradient(to right, #fff, rgba(255,255,255,0)),
    var(--cp-hue, #f00);
}
.duo-cp-row { display: flex; align-items: center; gap: 8px; }
.duo-cp-row .duo-cp-hue { flex: 1; min-width: 0; }
/* 吸管按钮(原生 EyeDropper API;不支持的浏览器不渲染) */
.duo-cp-eyedropper {
  width: 26px; height: 26px; flex-shrink: 0; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--color-border); border-radius: calc(6px * var(--corner-k));
  background: transparent; color: var(--color-text); cursor: pointer;
  transition: background 0.15s;
}
html:not([data-input="touch"]) .duo-cp-eyedropper:hover{ background: var(--color-bg-card); }
.duo-cp-eyedropper:active { background: var(--color-bg-card); }
.duo-cp-hue {
  position: relative; height: 12px; border-radius: 6px; corner-shape: round; cursor: pointer;
  border: 1px solid var(--color-border);
  background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
}
.duo-cp-sv-handle, .duo-cp-hue-handle {
  position: absolute; width: 12px; height: 12px; border-radius: 50%;
  corner-shape: round;
  border: 2px solid #fff; box-shadow: 0 0 0 1px rgba(0,0,0,0.35);
  transform: translate(-50%, -50%); pointer-events: none;
}
.duo-cp-hue-handle { top: 50%; background: transparent; }
.duo-cp-rgb { display: flex; gap: 6px; }
.duo-cp-rgb label { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px; }
.duo-cp-rgb input {
  width: 100%; box-sizing: border-box; text-align: center;
  padding: 5px 2px; font-family: inherit; font-size: 12px; font-weight: 400;
  color: var(--color-text); background: var(--color-bg-card);
  border: 1px solid var(--color-border); border-radius: calc(8px * var(--corner-k)); outline: none;
}
.duo-cp-rgb input:focus { border-color: var(--color-text-tertiary); }
.duo-cp-rgb span { font-size: 11px; color: var(--color-text-secondary); }

/* Drag-drop overlay — 主聊天拖图 = 金色蒙层(--duo-drop-tint) */
.chat-main.drag-over::after {
  content: '';
  position: fixed;
  inset: 0;
  background: var(--duo-drop-tint);
  z-index: 9999;
  pointer-events: none;
}
/* Studio 拖品牌卡到操作区 = 去色中性蒙层(--studio-drop-tint,不带金色品牌属性;随白天/夜间) */
.studio-main.brand-drop-hover::after {
  content: '';
  position: fixed;
  inset: 0;
  background: var(--studio-drop-tint);
  z-index: 9999;
  pointer-events: none;
}
/* ── 就地改名（DuoInlineRename，全站单一真源）：点标题直接改、失焦即存 ──
   不另造输入框：编辑态就是标题文字本身（字号/字重不变=零跳动），只用极轻的底色+插入符提示可编辑。
   ★ inline-block = 只包住文字：JS 只把标题里的【文字】包成 span 做可编辑，块级标题本身不参与，
     否则底色会铺满整行（"改名这个条太长了"）。 */
.duo-inline-rename { display: inline-block; max-width: 100%; cursor: text; border-radius: calc(6px * var(--corner-k)); margin-left: -4px; padding-left: 4px; padding-right: 4px; transition: background 0.12s; }
html:not([data-input="touch"]) .duo-inline-rename:hover{ background: var(--color-bg-hover); }
.duo-inline-rename:active { background: var(--color-bg-hover); }
/* Editing reads as a field, not as highlighted text: a rounded box around the name (owner 2026-09-17), in the
   site's own corner language and border colour. The 2px inset keeps the letters off the line. */
.duo-inline-rename.is-editing {
  background: var(--color-bg-hover);
  outline: none;
  cursor: text;
  border-radius: calc(6px * var(--corner-k));
  corner-shape: round;
  box-shadow: 0 0 0 1px var(--color-border-strong, var(--color-border));
  /* The ring is drawn outside the box (box-shadow takes no space), so the letters keep their place and nothing
     beside the name shifts when editing starts - no padding, no negative margin to cancel it. */
}
/* 扩展名:不可编辑，淡一档表明"这段改不了"（改名只改名字，不改格式） */
.duo-inline-rename-suffix { color: var(--color-text-secondary); cursor: text; }

/* ── 全站唯一轻提示(window.showToast,见 ui-chat-toast.js)────────────────────
   位置:顶栏之下、居中。底部是 composer,提示盖住正在打字的输入框最讨嫌;
   离顶距离复用 --duo-header-h(顶栏高度单一真源),顶栏改高它自动跟着走。
   float 档:压过一切开着的浮层(提示要报告弹窗/查看器里那次操作的结果),仍低于右键菜单(999999)。
   亮暗不写第二套 —— 颜色全走 :root / .dark-mode-active 上的变量,挂在 body 下照样继承。 */
.duo-toast-host {
  position: fixed; z-index: var(--z-float);
  top: calc(var(--duo-header-h) + 12px); left: 50%; transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  pointer-events: none;                 /* 不可点(自动消失),也绝不挡住下面的界面 */
  max-width: min(92vw, 420px);
}
.duo-toast {
  padding: 10px 14px; box-sizing: border-box;
  border: 1px solid var(--color-border); border-radius: calc(10px * var(--corner-k));
  background: var(--color-bg-panel); color: var(--color-text);
  font-size: 14px; line-height: 1.4; text-align: center;
  box-shadow: 0 4px 12px rgba(0,0,0,0.14);
  opacity: 0; transform: translateY(-6px);
  transition: opacity 0.18s ease-out, transform 0.18s ease-out;
}
.duo-toast.is-in { opacity: 1; transform: translateY(0); }
/* 错误只用【文字颜色】区分,不再叠图标/彩边/彩底(设计宪法第八条:同一件事别编码两遍)。
   #e5484d = 全站危险色,与 .duo-viewer-menu .action-item.is-danger 同源。 */
.duo-toast.is-error { color: var(--color-danger); }
@media (prefers-reduced-motion: reduce) {
  .duo-toast { transition: none; transform: none; }
  .duo-toast.is-in { transform: none; }
}


/* ── 上传中:全站唯一画法 ─────────────────────────────────────────────────────
   压暗宿主 + 中心进度环,【跟着已有的 data-uploading 标记走】—— 调用点一行都不用改,
   也不会出现"忘了清理"的残留环(标记一去,环随之消失)。
   原先五处各写各的:chip 自绘 SVG、聊天里的文件卡只 opacity:0.7(看着像坏了)、
   Studio 作品集/模板两套 is-uploading、字体只把按钮字改成 Uploading…。改这里 = 全站一起变。
   进度值:JS 知道真实百分比时写 data-upload-pct + --duo-upload-pct;
   不知道就别写 —— 没有该属性 = 转圈,绝不编一个假百分比。 */
/* ★进度不是【画上去】的,是【擦出来】的:整块压暗,再按百分比像钟表指针那样把遮罩挖掉一个扇形,
   露出图标本来的颜色。所以扇形没有自己的颜色 —— 它就是图标本身。
   (我先前画了一个白扇形盖上去,那是另一种东西:白的永远是白的,和图标什么颜色无关。) */
/* 【只有这一种动作】:扇形从 0 扫到 100。还没拿到百分比的那一小段(读文件、等第一个进度回调)
   就停在 0 —— 不另起一段转圈动画。同一件事先转一下再改成扫,看着像是两个东西接在一起。 */
[data-uploading] { position: relative; pointer-events: none; }
/* ★压暗+进度环【只适用文件卡】(owner 2026-08-11):图片/视频是替换元素,遮罩要挂父级 —— 但发图
   本就该直接显示、不该盖一层加载圈,所以媒体不走这条,只有非媒体(文件卡 div)画环。
   data-uploading 属性仍留在图上(invariant/save 守卫在读),去掉的只是它的视觉环。 */
/* ★三个半径要能动,必须先 @property 注册:没有类型的自定义属性浏览器不插值,动画会直接跳到终值 ——
   开场的长出和收尾的张开就都看不见了。 */
@property --duo-upload-inner { syntax: "<length-percentage>"; inherits: false; initial-value: 0%; }
@property --duo-upload-outer { syntax: "<length-percentage>"; inherits: false; initial-value: 0%; }
@property --duo-upload-open  { syntax: "<length-percentage>"; inherits: false; initial-value: 0%; }
@keyframes duoUploadGrow { from { --duo-upload-inner: 0%; --duo-upload-outer: 0%; } }

[data-uploading]:not(img):not(video)::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  corner-shape: inherit;
  z-index: 4;
  pointer-events: none;
  /* ★压暗是【自己铺的一层黑】,不是 backdrop-filter(2026-09-17 换掉)。换的原因不是好看:
     全站 corner-shape 是 squircle,而 squircle 瓦片里 Chrome 会把整块压暗、直接忽略 mask ——
     环一帧都画不出来,卡片只是变黑然后一直黑着。实测(白底 dpr2、--dim 0.4,洞=255、全压暗=102):
     方角 backdrop-filter 扇形处 255(对),squircle 下 102(环没了);换成这层黑后 squircle 下也是 255。
     iOS 本身就是这么做的 —— 洞的位置【没有任何一层】,露出的是图标自己的像素,
     而不是一圈在什么图标上都一样的白。 */
  background: rgb(0 0 0 / calc(1 - var(--dim)));
  /* 百分比→角度。★满格乘 3.62deg 而不是 3.6:100% 时让扇形首尾重叠一点,正好相接会留一条缝。 */
  --duo-upload-sweep: calc(var(--duo-upload-pct, 0) * 3.62deg);
  /* 半径都是【短边一半】的比例:mask-size 给整块 + circle closest-side,100% 正好是短边的一半。
     清空的环带 0.6→0.7;0.6 以内按进度从 12 点顺时针擦扇形;0.7 以外一直压着。 */
  --duo-upload-inner: 60%;
  --duo-upload-outer: 70%;
  --duo-upload-open: 0%;
  animation: duoUploadGrow 0.333s ease-out;            /* 开场:两个半径从 0 长出来 */
  transition: --duo-upload-open 0.4s ease-in;          /* 收尾:张开到盖过四角 */
  /* 洞 = 张开的圆 ∪ 环 ∪ (内盘 ∩ 扇形);整块减掉这个并集。
     mask-image 按【上→下】写、合成自下而上算,所以 composite 是
     subtract(整块) / add(张开) / add(环) / intersect(内盘) —— 最后一层的算子被忽略。
     ★内盘要比环的内沿【多伸出 1px】(+0.5px / +1.5px)。两条 1px 软边正好对接会留下一道弧:
     mask-composite 的 add 是 source-over 不是相加,各自半透明处并起来是 0.5 + 0.5×0.5 = 0.75,
     还剩四分之一压暗没被减掉(实测就在短边一半的 60% 处读到灰 223,那里本该是洞=255)。
     ★扫描边摊开 ±0.6deg:conic 的硬色标在斜边上只能整像素取舍,不摊开就是一排台阶。 */
  --duo-upload-holes:
    radial-gradient(circle farthest-corner,
      #000 var(--duo-upload-open), transparent calc(var(--duo-upload-open) + 1px)),
    radial-gradient(circle closest-side,
      transparent calc(var(--duo-upload-inner) - 0.5px),
      #000 calc(var(--duo-upload-inner) + 0.5px),
      #000 calc(var(--duo-upload-outer) - 0.5px),
      transparent calc(var(--duo-upload-outer) + 0.5px)),
    radial-gradient(circle closest-side,
      #000 calc(var(--duo-upload-inner) + 0.5px),
      transparent calc(var(--duo-upload-inner) + 1.5px)),
    conic-gradient(from 0deg,
      #000 0deg,
      #000 calc(var(--duo-upload-sweep) - 0.6deg),
      transparent calc(var(--duo-upload-sweep) + 0.6deg),
      transparent 360deg);
  -webkit-mask-image: linear-gradient(#000, #000), var(--duo-upload-holes);
          mask-image: linear-gradient(#000, #000), var(--duo-upload-holes);
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  mask-composite: subtract, add, add, intersect, add;
}
/* ★收尾不是淡出:清空的圆一直长到盖过四角(farthest-corner 的 100% 就是到角的距离),把压暗一起带走。
   调用方必须【等这一下播完】再把占位换成真卡 —— 立刻换的话,环在开场画出第一帧之前就被拿走了。
   写入口 DuoUploadProgress.finish() 会等,门 upload-ring-finish 保证每个调用点都调它。 */
[data-uploading][data-upload-done]::before { --duo-upload-open: calc(100% + 1px); }
/* 关掉动效时开场和收尾都不播,直接到位。transitionend 因此不会来 ——
   finish() 那边的超时不是在兜错,是这条路本来就没有过渡事件。 */
@media (prefers-reduced-motion: reduce) {
  [data-uploading]:not(img):not(video)::before { animation: none; transition: none; }
}
/* ★分钟级的"在做"(件卡图标)在扇形之上再走一条流光(owner 2026-08-27:光有扇形看不出活着)。
   光带样式与 thinking 气泡同一份(动画复用 bubbles 的 bubbleShimmer 真源,不另造第二条);
   秒级上传仍是纯扇形 —— 这条只挂件卡的图标盒,别扩散。 */
.bubble-doc-icon[data-uploading] { overflow: hidden; }
.bubble-doc-icon[data-uploading]::after {
  content: "";
  position: absolute;
  top: 0; left: -140%;
  width: 140%; height: 100%;
  pointer-events: none;
  background: linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.85) 45%, rgba(255,255,255,0) 80%);
  animation: bubbleShimmer 1.1s ease-in-out infinite;
}
/* 没有真实进度 → 同一瓣扇形绕着转,别假装知道百分比。转的是【遮罩的起始角】(@property 让角度可动画),
   不是转元素 —— 元素一转,那层铺满的压暗也跟着转,四角就露白了。 */

/* ============================================
   Beta badge - THE one capsule that marks a surface as not finished (owner 2026-09-06).
   Markup: {%- render 'duo-beta' -%} in liquid, or '<span class="duo-beta" data-i18n="common.beta">Beta</span>' in JS.
   Tokens only, so dark mode follows on its own; the capsule radius is a shape, not a corner (exempt from --corner-k).
   Sits after a label: inline, centred on the text line; the host decides the gap (margin-left 6px is the default).
   ============================================ */
/* A name wearing a badge is one object: the name takes what width there is (wrapping if it must) and the badge
   stays beside it, instead of dropping to a line of its own when the line runs out - a chip alone under the name
   reads as a stray, and in a narrow panel it can be pushed off the row entirely (measured 2026-09-17).
   The distance between them is still the badge's own margin; this only keeps them on one line. */
.settings-row-label:has(> .duo-beta) { display: flex; align-items: center; }
.settings-row-label:has(> .duo-beta) > :first-child { min-width: 0; }
.duo-beta {
  display: inline-flex;
  align-items: center;
  height: 18px;
  padding: 0 8px;
  margin-left: 6px;
  border-radius: 999px; corner-shape: round;   /* a capsule stays a capsule under the site's squircle switch */
  background: var(--color-bg-card);
  color: var(--color-text-secondary);
  font-family: inherit;
  font-size: 11px;
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.02em;
  text-transform: none;
  white-space: nowrap;
  vertical-align: middle;
  flex: none;
}
