/* ai-chat-file-preview.css — 全站【文件内联预览】视觉单一真源(配 ui-chat-file-preview.js 及各格式渲染器)。
 *
 * ★尺寸由【宿主容器】决定,渲染器不写死:同一套渲染器要同时服务
 *   Studio 弹窗(收缩包裹,860×78vh)与主聊天全屏查看器(铺满)。宿主改这两个变量即可。
 *   默认值 = Studio 现状(改这里会同时动 Studio,别当成"只是主聊天的事")。
 */
:root {
  --preview-w: min(860px, 88vw);
  --preview-h: 78vh;
}

/* ★.duo-preview 是【所有】预览的基类:img(图片)/ iframe(PDF、.ai)/ canvas(3D) 都挂它。
   所以基类上【只许】放三者通用的东西,尺寸一律写在具体类上 ——
   否则给图片写的 width/max-width 会以更高特异度压过 .duo-preview-frame 的 var(--preview-w),
   而 iframe/canvas 的 CSS 默认固有宽度是 300px → 三种预览一起塌成 300px 且与窗口宽度无关。 */
.duo-preview { display: block; }
/* 图片:按自身尺寸显示(不撑满 → 横图矮/竖图高,弹窗无留白),★本身不带圆角 */
img.duo-preview { max-width: min(820px, 88vw); max-height: var(--preview-h); object-fit: contain; }
/* PDF 内嵌 / 3D 画布 / 加载占位:三者尺寸【必须一致】,否则开窗会"先小后大"跳一下 */
.duo-preview-frame { width: var(--preview-w); height: var(--preview-h); border: none; background: #fff; }

/* ★画布类渲染面【永远不是文字】,一律不可选:3D 每转一下都是一次拖拽,浏览器会顺手划选
   周围的东西,手机上还会弹 Copy / Look Up 盖住画面。
   这是【按渲染器】定的,不是笼统关掉整个内容区 —— PDF 正文要能选能复制,那是真功能。
   以后新增画布渲染器(canvas 的)往这条加一个类名。 */
.duo-psd-canvas{
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}
/* ★还要单独钉在 <canvas> 自己身上:上面那条 -webkit-touch-callout 是【继承】属性、能传下去,
   但 -webkit-user-drag 【不继承】—— 而 iOS 上"按住先浮起一个半透明泡泡、松手才弹 Copy"
   的那个泡泡,正是拖拽抬起预览,归 user-drag 管。写在父元素上到不了画布,必须写在画布上。
   画布不是图片也不是文字:3D 每转一下都是拖拽,长按拿走一张"截图"没有任何意义。 */
canvas.duo-psd-canvas{
  -webkit-user-drag: none;
  -webkit-touch-callout: none;
  user-select: none;
  -webkit-user-select: none;
}

.duo-preview-loading { width: var(--preview-w); height: var(--preview-h); display: flex; align-items: center; justify-content: center; }
/* 不能预览时:文件类型图标 + 说明(+ 可选的"新标签打开") */
.duo-preview-icon { width: 72px; height: 72px; border-radius: calc(16px * var(--corner-k)); display: flex; align-items: center; justify-content: center; color: #fff; }
.duo-preview-icon svg { width: 40px; height: 40px; }
/* the 3D module's loading line wears the platform's class name; it is our UI (owner 2026-09-15) */
.duo-preview-hint, .flairui-preview-hint { font-size: 12px; color: var(--color-text-secondary); text-align: center; }
/* 查看器里的失败态(如手机上太大的件):图标在上、一句话在下,比卡片里那对 72px 的大一档
   (owner 2026-08-27,照 kuribaby)。只作用于查看器正文;卡片缩略图不动。 */
.pdf-viewer-body:has(> .duo-preview-icon) { flex-direction: column; gap: 18px; }
.pdf-viewer-body > .duo-preview-icon { width: 120px; height: 120px; border-radius: calc(26px * var(--corner-k)); }
.pdf-viewer-body > .duo-preview-icon svg { width: 66px; height: 66px; }
/* 一句话给它两行的宽度(owner:一行太挤):左右留 1.5rem,超过就折 */
.pdf-viewer-body > .duo-preview-hint { font-size: 14px; line-height: 1.5; max-width: 22em; padding: 0 1.5rem; }

/* 全屏查看器(主聊天)里的预览容器:预览铺满它(--preview-w/h 由 JS 设 100%),内容居中。
   与 Studio 弹窗共用同一套渲染器,只是宿主给的尺寸不同。 */
.pdf-viewer-body { display: flex; align-items: center; justify-content: center; overflow: auto; position: relative; }   /* relative:3D 工具列贴右缘定位用 */

/* ── 整块 app 图标(pdf/ai/psd/3dm):图标自带底色与圆角 → 铺满方块、不再套彩色底 + 小图标 ──
   全站唯一定义:任何显示文件类型的方块(模板卡/作品缩略图/架构行 chip/手机选择器/预览兜底)
   由 DuoFilePreview.applyTypeIcon 加上 .is-app-icon,尺寸就跟着它的宿主方块走。 */
/* ★底色【由 CSS 盒画】,SVG 只画前景标记:CSS 盒的绘制会被吸附到整设备像素,SVG 内容不会 ——
   元素落在小数位置时(实测 chip x=237.22),画在 SVG 里的满幅底板远端那列只有 22% 覆盖,
   看着就是"缺右下一条边"(用户 2026-07-27)。底色改由 el.style.background 给(见 applyTypeIcon),
   与 typeStyle 里本来就有的每类底色天然吻合。 */
.is-app-icon { border: none !important; padding: 0; overflow: hidden; }
.is-app-icon svg { width: 100% !important; height: 100% !important; display: block; }

/* ── 3D 预览工具列(复位 / 光影 / 背景)────────────────────────────────
   贴在预览区右缘;宿主容器需 position:relative(下面给 .studio-pf-preview-main / .pdf-viewer-body 补上)。
   按钮沿用全站图标按钮基准:24×24 图标 + 6px 圆角方形 hover(不是圆形)。 */
/* ★全站预览工具条【唯一】样式:PDF(翻页/缩放)、图片(缩放)、3D(视图/光影/背景)共用同一条。
   由 ctx.tools(items) 统一构建(见 ui-chat-file-preview.js),渲染器不自己画按钮 ——
   位置/尺寸/圆角/暗色/手机 touch 全在一处,新格式接进来自动一致。
   宿主容器需 position:relative(.studio-pf-preview-main / .pdf-viewer-body 已补)。 */
.duo-preview-tools {
  position: absolute; top: 8px; right: 8px; z-index: 2;
  display: flex; flex-direction: column; gap: 4px; align-items: center;
}
.duo-preview-tool {
  width: 32px; height: 32px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  /* ★裸图标不画框(用户 2026-07-27:"这个网站的图标都是不带方框的"):
     全站图标按钮(三点/返回/侧栏动作)都是无边框无底色,反馈只靠 hover/active 背景。
     6px 圆角留着 —— 那是 hover 背景的形状基准,不是可见的框。 */
  border: none; border-radius: var(--radius-icon-btn);
  /* 颜色 = 全站顶栏图标基准 .chat-header-btn(ai-chat-base.css):亮 0.6 黑 / 暗 0.6 白;别用 --color-text(那是正文,比同类图标重一档) */
  background: transparent; color: rgba(0, 0, 0, 0.6);
  cursor: pointer; transition: background 0.12s;
}
.dark-mode .duo-preview-tool, .dark-mode-active .duo-preview-tool { color: rgba(255,255,255,0.6); }
html:not([data-input="touch"]) .dark-mode .duo-preview-tool:hover, html:not([data-input="touch"]) .dark-mode-active .duo-preview-tool:hover { color: rgba(255,255,255,0.85); }
/* ★浮动列是例外,底板必须留:它悬在【图片内容】上(Studio 小预览没有顶栏,工具贴在图右上角),
   底下什么颜色都可能,裸图标在浅色图上直接看不见。用户说的"不带方框"指的是顶栏那种
   有稳定底色的场合 —— 同一个 class 两种处境,按处境分,不是一刀切。 */
.duo-preview-tools.is-floating .duo-preview-tool {
  border: 1px solid var(--color-border);
  background: var(--color-bg-panel);
}
.duo-preview-tool svg { width: var(--icon-size); height: var(--icon-size); }
/* The folder glyph fills 18 x 17.1 of its 24 box, so at 18px its ink is 13.5 x 12.83 - under the layers glyph beside
   it (13.65 x 13.6). At 20px: 15.0 x 14.25. Even box in the 32px button = whole-pixel centre. Only this button. */
html:not([data-input="touch"]) .duo-preview-tool:hover{ background: var(--color-bg-hover); }

.duo-preview-tool:active { background: var(--color-bg-hover); }
.duo-preview-tool.is-active { background: var(--color-bg-hover); }
.duo-preview-tool.is-disabled { opacity: 0.35; cursor: default; }
/* 只读数字(页码 3/46、缩放 120%):与按钮同宽,字号取全站微型档 */
/* ★只读数字不给框(用户 2026-07-27):框 = "这里能点/能改"的暗示,而 "/ 45" 只是总页数。
   能改的那格(.duo-preview-field)保留框,一有框一无框,两者的可交互性一眼可分。 */
.duo-preview-readout {
  padding: 0 2px; box-sizing: border-box;
  color: var(--color-text-secondary);
  font-size: 11px; line-height: 1.3; text-align: center;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.duo-preview-tools-sep { width: 20px; height: 1px; background: var(--color-border); margin: 2px 0; }

/* ── 工具条两种落位(同一套按钮,只是宿主不同)──────────────────────────────
   is-topbar   = 全屏 viewer 的顶栏,Chrome PDF 工具条那个位置(横排、居中)
   is-floating = Studio 小预览里贴主舞台右上角的浮动列(竖排)
   ★两种落位的按钮尺寸/圆角/图标大小完全一致 —— 换文件类型只换按钮,位置和观感不变。 */
.duo-preview-tools.is-topbar {
  position: static; top: auto; right: auto;
  flex-direction: row; justify-content: center; gap: 4px;
}
.duo-preview-tools.is-floating { position: absolute; top: 8px; right: 8px; flex-direction: column; }

/* ── 右侧 rail:同一个位置装不同东西(PDF=页缩略图 / 3D·PSD=图层 / 图片=胶片条)──
   外框、滚动、选中态由壳给;条目内容由渲染器声明。宽度恒定 → 换文件不改主舞台尺寸(不跳)。 */
.duo-preview-rail-item {
  display: flex; align-items: center; gap: 6px;
  padding: 2px; border-radius: calc(6px * var(--corner-k)); cursor: pointer;
  border: 1px solid transparent; box-sizing: border-box;
}
.duo-preview-rail-item.is-active { border-color: var(--color-text); }
/* ★带缩略图的条目:选中框【只框图】,页码落在框外(用户 2026-07-27)。
   所以框(内距+边线)挂在图的包裹层上,整行自己不再画框 —— 靠特异性压过上面那条,不用 !important。 */
.duo-preview-rail-item.has-thumb { padding: 0; }
.duo-preview-rail-item.has-thumb.is-active { border-color: transparent; }
.duo-preview-rail-thumbbox {
  width: 100%; padding: 3px; box-sizing: border-box;   /* 3px 内距 + 1px 边线 = 每边比图大 4px(用户 2026-07-27) */
  border: 1px solid transparent; border-radius: calc(6px * var(--corner-k));
}
.duo-preview-rail-item.is-active .duo-preview-rail-thumbbox { border-color: var(--color-text); }
/* ★hover 与选中框必须落在【同一层】:选中框只框图(挂 thumbbox),而 hover 原来挂在整行,
   于是 hover 把下面的页码也罩进去了,两个框一大一小(用户 2026-07-28)。
   带缩略图的条目把 hover 改挂到 thumbbox;不带缩略图的行(3D/PSD 图层)整行 hover 不变。 */
html:not([data-input="touch"]) .duo-preview-rail-item:hover{ background: var(--color-bg-hover); }
html:not([data-input="touch"]) .duo-preview-rail-item.has-thumb:hover{ background: transparent; }
html:not([data-input="touch"]) .duo-preview-rail-item.has-thumb:hover .duo-preview-rail-thumbbox{ background: var(--color-bg-hover); }

.duo-preview-rail-item:active { background: var(--color-bg-hover); }
.duo-preview-rail-item.has-thumb:active { background: transparent; }
.duo-preview-rail-item.has-thumb:active .duo-preview-rail-thumbbox { background: var(--color-bg-hover); }
/* 缩略图沿用全站图片缩略图基准(圆角/边线/正方 cover),别自造 */
/* ★rail 缩略图不要圆角也不要边线(用户 2026-07-26 定):这里一列几十张,圆角+描边会糊成一片噪点,
   而且页缩略图本身白底、边界已经很清楚。全站图片缩略图那套(--img-radius + 边线)只用在【内容区】。 */
.duo-preview-rail-thumb { width: 100%; height: auto; display: block; border-radius: 0; border: none; }
/* ★1:1 cover 只对【图片】缩略图成立(胶片条);PDF 页缩略图是 canvas,裁成正方就看不出是哪一页了。
   同今天"给图片写的规则别用基类捞走非图片"那条教训。 */
img.duo-preview-rail-thumb { aspect-ratio: 1 / 1; object-fit: cover; }
.duo-preview-rail-label { font-size: 12px; color: var(--color-text); flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 材质色点:跟在图层名后面,浅色点靠极淡内描边出边界 */
.duo-preview-rail-dot {
  /* 尺寸/圆形/内距走 .project-color-dot 真源(owner 2026-08-06:与一般材质选择同大)。
     本类只留行内摆位与描边 —— 在这里再写宽高就是第二真源,而本表加载晚必然赢 */
  flex: 0 0 auto;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.22);
}
.duo-preview-rail-dot.is-btn { cursor: pointer; }
.duo-preview-rail-dot:not(.is-btn) { cursor: default; }
/* 基准自带 pointer,不可点的点不许撒谎 */
html:not([data-input="touch"]) .duo-preview-rail-dot.is-btn:hover{ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.22), 0 0 0 3px var(--color-bg-hover); }

.duo-preview-rail-dot.is-btn:active { box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.22), 0 0 0 3px var(--color-bg-hover); }
/* on the dark panel a dark swatch (onyx, a black pearl) needs a light edge instead */
.dark-mode .duo-preview-rail-dot { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.22); }
html:not([data-input="touch"]) .dark-mode .duo-preview-rail-dot.is-btn:hover{ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.22), 0 0 0 3px var(--color-bg-hover); }
.dark-mode .duo-preview-rail-dot.is-btn:active { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.22), 0 0 0 3px var(--color-bg-hover); }
.duo-preview-rail-badge { font-size: 10px; color: var(--color-text-secondary); font-variant-numeric: tabular-nums; }
/* 图层显隐(3D / PSD):同全站图标按钮基准,关闭态压低存在感 */
.duo-preview-rail-eye {
  width: 24px; height: 24px; padding: 0; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  border: none; border-radius: calc(6px * var(--corner-k)); background: transparent; color: var(--color-text);
  cursor: pointer;
}
.duo-preview-rail-eye svg { width: var(--icon-size-sm); height: var(--icon-size-sm); }
.duo-preview-rail-eye.is-off { opacity: 0.3; }
html:not([data-input="touch"]) .duo-preview-rail-eye:hover{ background: var(--color-bg-hover); }

.duo-preview-rail-eye:active { background: var(--color-bg-hover); }

/* The site-wide corner knob (the * block in theme.css) expects circles and pills to declare corner-shape: round.
   The 3D renderer's stylesheet (flair3d.css) declares none, so the exemption is ours to give - but only to its own
   pieces: the picker's dots, pills and play button under the model, and the material balls (owner 2026-09-15: the
   platform keeps render tool buttons, material styles and thumbnails). The material panel and the bottom sheet are
   our UI and follow the knob like every other sheet. */
[class*="flair3d-pick"], [class*="flair3d-dot"], [class*="flair3d-matball"] { corner-shape: round; }

/* -- The material panel (a layer's colour dot in the 3D viewer, or the picker's +, opens it) ------------------
   Ours: ui-chat-file-preview-3d.js draws it through the renderer's materials handle (owner 2026-09-15: windows and
   panels belong to each platform). */
/* 材质面板(胶囊里的 + 打开):全部材质按金属/宝石分组,勾选决定哪几种留在底部那一行。
   桌面浮在 + 左边(宽度取全站侧边面板基准),手机走底部上拉 sheet、高度与图层 sheet 同一个真源。 */
.duo-matsheet {
  position: fixed; box-sizing: border-box;
  width: var(--sidebar-width, 250px); max-height: 60vh; overflow: hidden;
  display: flex; flex-direction: column;
  padding: 8px;
  background: var(--color-bg-panel); border: 1px solid var(--color-border);
  border-radius: var(--modal-radius); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
}
/* 桌面端 = 与右侧图层栏对称的【左侧栏】:同宽、同高、同一条滑入曲线,只是从左边进来。
   几何取图层栏那套(它才是这个位置的基准),别自己另定一套。 */
.duo-matsheet.is-side {
  position: absolute; left: 0; top: 0; bottom: 0; right: auto;
  width: var(--sidebar-width, 250px); max-height: none;
  border: none; border-right: 1px solid var(--color-border);
  /* 三个角是直的,只有左下角跟着【窗口】的圆角走:侧栏贴着窗口左缘通到底,方角会戳出
     窗口的圆边、露出后面。半径取窗口那个单一源,窗口改圆角这里自动跟着改。 */
  border-radius: 0 0 0 var(--viewer-corner, 0px);
  box-shadow: none; z-index: 3;
  display: flex; flex-direction: column; gap: 0;
  animation: duoSideSlideIn 0.3s cubic-bezier(0.2, 0, 0, 1);
}
@keyframes duoSideSlideIn { from { transform: translateX(-100%); } to { transform: translateX(0); } }
/* ★回程必须是【另一个名字】:滑入那条已经播完,同名动画只更新参数不重启 —— reverse+forwards
   会直接定格在反向终点帧(屏幕外),看着就是瞬间消失。只写 to、不写 from:起点取当前位置,
   滑入中途关闭也能顺滑接上。 */
@keyframes duoSideSlideOut { to { transform: translateX(-100%); } }
.duo-matsheet.is-side.is-closing { animation: duoSideSlideOut 0.25s cubic-bezier(0.2, 0, 0, 1) forwards; }
/* ── 材质面板:族图标竖排 + 球试样网格 ──
   竖排是面板的目录(策展=浏览哪族;指派=这层归哪族的开关本体);按钮走全站图标按钮基准。 */
.duo-matsheet-strip {
  position: absolute; left: 0; top: 0; bottom: 0; width: 52px; z-index: 2;
  /* 族按钮与放大镜同一按钮语言(chat-header-btn 36 盒),整列必须同轴:放大镜在 left:16,
     列宽 52 + 右贴齐 = 按钮占 16..52,中轴 34 与放大镜严格重合 —— 居中(轴 22)就是那个
     "怎么看怎么歪"的 12px offset */
  display: flex; flex-direction: column; align-items: flex-end; gap: 4px;
  /* 不画到顶的分隔线:遮罩会把线头斩断,看着像结构断了;图标列+间距本身就能分区
     (面板与舞台之间已有边线,里面再画一条是双重分隔)。选中胶囊承担"在哪族"的表达。 */
  padding: 56px 0 12px;
}
/* 选中族常亮:与 duo-preview-tool.is-active 同一个值(全站"开着=常亮"的既有语言) */
.duo-matsheet-strip .chat-header-btn.is-active { background: var(--color-bg-hover); }
.duo-matsheet.has-strip .duo-matsheet-body { margin-left: 52px; }
/* 有竖排时整个侧栏加宽一条竖排的量:内容区保持与右侧图层栏同宽(对称不破),不然三列网格被挤出横向滚动条 */
.duo-matsheet.is-side.has-strip { width: calc(var(--sidebar-width, 250px) + 52px); }
.duo-matgrid {
  /* auto-fill:列数跟宽度走 —— 桌面窄面板自然 3 列,手机全宽 sheet 自然 4-5 列,不写死数字 */
  display: grid; grid-template-columns: repeat(auto-fill, minmax(62px, 1fr)); gap: 8px 4px; padding: 4px 2px;
}
.duo-matsearch { flex: 0 0 auto; margin: 0 2px 8px; box-sizing: border-box; width: auto; padding: 5px 12px; }   /* 比基准矮一档(owner):侧栏里 33px 偏高;手机 in-bar 版高度另有 36px 锁定不受影响 */
/* 材质 sheet 的关闭钮:真源字形(duo-sheet-close),只在手机 sheet 形态出现 —— 桌面侧栏
   点空白/切层就是关,右上角再来一颗 X 是双入口 */
.duo-matsheet-x { display: none; }
/* 搜索放大镜同为手机限定:桌面搜索框还在内容区顶部,再来一颗入口是双真源 */
.duo-matsearch-btn { display: none; }
/* The panel's buttons (close, search, family icons) take the viewer tools' ink, not chat-header-btn's 60% grey.
   :active is pinned too, and neither the colour nor :active sits behind the touch gate: phones are where the
   sheet form appears. */
.duo-matsheet .chat-header-btn,
.duo-matsheet .chat-header-btn:active{ color: var(--color-text); }
html:not([data-input="touch"]) .duo-matsheet .chat-header-btn:hover{ color: var(--color-text); }
.duo-matgrid-hint { grid-column: 1 / -1; padding: 16px 10px; text-align: left; }
/* 内层才滚：关闭钮与顶部渐变需要一个不随内容走的层（同手机版 settings 的分层） */
/* scrollbar-gutter:切换族时有的溢出有的不溢出,滚动条一出一消吃宽度,网格跟着晃
   (5.4 规范指定修法:动态内容滚动容器常驻预留)。手机是 overlay 滚动条不占布局宽,预留=0 */
.duo-matsheet-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; scrollbar-gutter: stable; }
@media (max-width: 768px), (max-height: 500px) {
  /* 顶部渐变：内容在碰到关闭钮之前先化掉（同 settings 手机版）。
     高度按【本面板右上角那颗的盒子】推,不照抄 settings 的 80/70 —— 那是 92dvh 的 sheet,
     这块 sheet 不满屏:OK 胶囊约 26 高、占 16..42，不透明段(60%)要盖到 42 → 42 / 0.6 = 70 */
  .duo-matsheet::before {
    content: ""; position: absolute; top: 0; left: 0; right: 0; height: 70px;
    background: linear-gradient(to bottom, var(--color-bg-panel) 60%, transparent);
    z-index: 1; pointer-events: none;
    border-radius: var(--duo-sheet-radius) var(--duo-sheet-radius) 0 0;
  }
  /* 渐变的起点必须是【本面板自己的底色】:设置页那条在暗色里写死 #212121 是它自家 sheet 的值,
     本面板底色是 var(--color-bg-panel)(暗色 #2a2a2a)—— 照搬字面值就会在顶部盖出一条更深的带。
     用变量则亮暗两套自动吻合,不需要暗色覆盖。 */
  /* 标题钉在壳上、压在渐变【之上】(z-index 比渐变高一级),列表从它下面滚过去 ——
     两侧各让出 60px(OK 胶囊 + 16 边距 + 呼吸),标题才真居中;与 OK 同一条基线 */
  .duo-matsheet-x { display: flex; position: absolute; top: 16px; right: 16px; z-index: 2; }
  .duo-matsearch-btn { display: flex; position: absolute; top: 16px; left: 16px; z-index: 2; }
  /* 搜索态:标题让位,输入框在标题栏【原地】现身(两侧 44 = 36 按钮 + 8 呼吸),不占内容高度;
     sheet 同时升满屏档 —— 键盘弹起后仍有大半屏放结果网格。清空失焦由 JS 摘 is-search 还原 */
  .duo-matsearch.is-in-bar { display: none; flex: 1 1 auto; min-width: 0; height: 36px; margin: 0 44px; }
  .duo-matsheet.is-search .duo-matsearch.is-in-bar { display: block; }
  .duo-matsheet.is-search .settings-memory-subpanel-title { display: none; }
  .duo-matsheet.duo-sheet.is-search { height: var(--duo-sheet-h-tall); max-height: var(--duo-sheet-h-tall); }
  /* ★内容从渐变【完全透明处】(70px)再留 4 起,不是从关闭钮下沿(42)起:渐变 42..70 还在
     淡出,球试样有 40px 高,从 46 起的话第一行大半截泡在渐变里(文字行时代只擦到行顶,看不出)。
     族竖排同理。顶部留白由内层给,渐变才能贴着壳顶起画。 */
  .duo-matsheet.duo-sheet { padding-top: 0; }
  .duo-matsheet-body { padding-top: 74px; }
  .duo-matsheet-strip { padding-top: 74px; }
}
/* 手机端形态与上拉动画全部走 .duo-sheet 那套真源(class 已挂在元素上),
   这里只改它该矮多少、内距与边线 —— 复合选择器压过真源的 92dvh,不用 !important */
@media (max-width: 768px), (max-height: 500px) {
  .duo-matsheet.duo-sheet {
    top: auto;
    /* 宽度必须在这里显式放开:桌面那条给了 250px 的定宽,与真源的 width:auto 特异性相同,
       靠文件先后决定胜负 —— 本表在 sheet 表之后加载,不写就是 250px 一条贴在左边 */
    left: 0; right: 0; width: auto; max-width: none;
    /* 同上:桌面那条的 --modal-radius 是四角都圆,与真源的"只圆上面两角"同特异性、又在它之后加载。
       贴着屏幕底边的面板下面两角必须是直角,否则底下露出两个缺口 */
    border-radius: var(--duo-sheet-radius) var(--duo-sheet-radius) 0 0;
    height: var(--duo-sheet-h-short); max-height: var(--duo-sheet-h-short);
    padding: 8px 16px 16px;
    border: none; border-top: 1px solid var(--color-border);
  }
}
/* The material sheet on a phone is mounted on <body> as a bottom sheet. It is called up from the viewer, so it
   sits where every such dialog sits (owner 2026-09-11) - this is its one layer; the rules above declare none. */
.duo-matsheet,
.duo-matsheet.duo-sheet { z-index: var(--z-dialog); }   /* the phone form re-declares z at two classes; match it */

/* 选中 = 外圈描环 + 右下角对号圆圈。这颗勾是【全站唯一一份】:材质球与作品集选图器挂的是
   同一个 .duo-select-tick(几何/配色/描环再继承角标真源 .studio-pf-stack-count),
   各场景只负责"什么时候现身"。DOM 由 DuoFilePreview.buildSelectTick() 组装,别手搓第二份。
   双级选择器压特异性:file-preview.css 先于 studio.css 加载,同特异性会被基准的 top/right 反压。 */
/* 出角的量【不自己发明】:取角标真源那两个 inset,纵向镜像到下面 —— 右上角标出多少,
   右下对号就出多少(owner 2026-08-08:"参考图片堆叠右上角的位置,虽然这个是右下角")。 */
.studio-pf-stack-count.duo-select-tick {
  display: none; top: auto; right: var(--badge-inset-x); bottom: var(--badge-inset-y); padding: 0;
  /* 本处两点偏离真源(owner 定):①外圈不用页底色挖空 —— 它坐在选中环上,同色才像
     嵌进环里,页色会在环上切一道口子;②整体收 1px,16 在球上偏大一像素 */
  min-width: 15px; height: 15px;
  box-shadow: 0 0 0 2px var(--color-text);
}
/* 12px(owner 2026-08-06 定);生效前提 = JS 已摘掉图标自带的内联 16px。勾的墨迹在 viewBox
   里本身居中(偏差 <0.1px @8px),所以几何居中即视觉居中,不需要光学补偿值 */
.studio-pf-stack-count.duo-select-tick svg { width: 12px; height: 12px; }
/* 圆宿主的例外(方块那套出角量在这里不成立,不是"差不多"):球面在对角线上往回缩,
   照搬 -6/-5 实测只咬进球 2px = 勾吊在球外、还压到名字。要维持的关系是"勾坐在选中环上、
   像嵌进去",在 40px 的圆上那个解是 -2。方块宿主(选图器/角标)仍走上面的真源变量。 */
.flair3d-matball .duo-select-tick { right: -2px; bottom: -2px; }

.flair3d-matball.is-on .duo-select-tick { display: flex; }
   

/* 预览器里的 sheet 关闭钮补【场景色】:真源只管几何(盒36/字形14/1.5),颜色跟所在面走 ——
   chat-header-btn 自带 60% 灰配的是 header 邻居,预览器工具图标全是全色 var(--color-text),
   同一面上一灰一黑读起来像坏了。hover 一并钉住,防 base 的 0.85 灰阶插进来。 */
/* 材质 sheet 手机端挂 document.body(不在 .duo-viewer 里),要单独点名;
   祖先用 matSheet 自己,压过 base 的 .dark-mode .chat-header-btn。
   :active 一并钉住:按下反馈由背景给,颜色不许跳灰。
   选 .chat-header-btn 整类而不是逐颗点名:关闭钮/放大镜都在这张面上,同一条规矩 */
/* ★底色与 :active 不进触屏门:那道门是给 :hover 的。整族一起挂进去的话,触屏上这些按钮
   的颜色规则整条失效(手机上变灰),而手机恰恰是 sheet 形态最常出现的地方。 */
.duo-viewer .duo-sheet-close,
.duo-viewer.dark-mode .duo-sheet-close,
.duo-viewer .duo-sheet-close:active,
.duo-viewer.dark-mode .duo-sheet-close:active{ color: var(--color-text); }
html:not([data-input="touch"]) .duo-viewer .duo-sheet-close:hover,
html:not([data-input="touch"]) .duo-viewer.dark-mode .duo-sheet-close:hover{ color: var(--color-text); }

/* 标题栏走 .duo-sheet-bar 真源(ai-chat-sheet.css);PDF 页缩略图抽屉不是 sheet 形态,不出标题 */
.duo-viewer-rail.is-thumbs .duo-viewer-rail-bar { display: none; }

/* ── 全站文件查看器外壳(ui-chat-viewer.js)──────────────────────────────
   顶栏三段:左=返回/面包屑 · 中=渲染器工具条(Chrome PDF 工具条那个位置)· 右=最大化/更多/关闭。
   ★几何恒定:rail 定宽、舞台吃剩余空间 → 换文件类型、开合 rail 都不改主舞台尺寸(不跳)。 */
/* 窗口把【自己此刻的倒角】声明出来,贴着它边缘的子层(画布、材质侧栏)一律读这个值。
   最大化与手机全屏时窗口是方角(见下面两条),子层不跟着变方就会在方角窗口上切出缺口。 */
/* 最大化不做尺寸动画(owner 2026-08-06 定):width/height 是布局属性,过渡=每帧全查看器重排
   + 3D/PDF 画布逐帧 setSize 重渲,必卡;瞬时切换物理上不可能卡(Chrome PDF 同款) */
.duo-viewer .pdf-viewer-dialog { --viewer-corner: var(--modal-radius); }
/* 桌面端:上下 5vh + 左右 20vh(基类原上下边距;基类左右是 960px 上限,不等距) */
@media (min-width: 769px) and (min-height: 501px) {
  .duo-viewer .pdf-viewer-dialog:not(.is-max) {
    width: calc(100% - 30vh);
    max-width: none;
    height: 90vh;
  }
}
.duo-viewer .pdf-viewer-dialog.is-max { width: 100%; max-width: 100%; height: 100%; border-radius: 0; --viewer-corner: 0px; }
/* ★相邻图标按钮全站只用【一个】间距 4px:顶栏分三段(面包屑|工具条|窗口按钮)纯粹是布局手段,
   不该在视觉上变成一种分组。原来段与段是 8px,于是"笔 ↔ rail 开关"这一对跨段的图标
   视觉空白 22px、而同段内相邻图标只有 18px(按钮 32 图标 18 → 每侧自带 7px)——
   差的正是这 4px(用户 2026-07-27 实测"间距过大")。
   顶栏因此只剩【两个】间距值,规则一句话说清:
     · 相邻图标按钮 = 4px(工具条内、右段内、跨段全部一致)
     · 图标 ↔ 文字   = 6px(返回键↔面包屑走 nav 的 gap;面包屑↔页码格 = 段间 4 + nav padding-right 2)
   文字与图标之间要更宽的呼吸,是【不同类元素之间】的有意区别,与"相邻图标"那条基准不冲突;
   但它自己也必须只有一个值 —— 原来左边 6px、右边 8px,同一类边界两套数就是 bug。 */
/* ★默认全关、按需开口(白名单)。之前是逐个元素点名关闭,漏了三轮(顶栏 → 画布 → 材质面板
   → 手机上长按模型仍弹 Copy/Look Up):iOS 那个是【文字】选择菜单,壳里任何一处漏网文字都会
   把它招出来,一个个堵堵不完。真要能选的只有 PDF 文字层和输入框,在下面开口即可。
   后两个是 3D 的材质面板/浮层:它们 append 到 document.body、不在壳的子树里,必须单独点名。 */
.duo-viewer,
.duo-matsheet {
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}
/* 开口一:PDF 正文 —— 这是真功能(选中/复制/系统查词),整层就为它存在。
   开口二:壳里能输入的东西(rail 可编辑字段、缩放百分比框)。祖先 user-select:none 在 Safari
   会连输入框里的文字一起锁住 —— 能打字但选不中、改不了,那是真回归。同 studio 侧栏那条。 */
.duo-pdf-textlayer, .duo-pdf-textlayer span,
.duo-viewer input, .duo-viewer textarea,
.duo-viewer [contenteditable="true"], .duo-viewer [contenteditable="plaintext-only"] {
  user-select: text;
  -webkit-user-select: text;
  -webkit-touch-callout: default;
}
.duo-viewer-bar { gap: 4px; }
.duo-viewer-nav { display: flex; align-items: center; gap: 6px; min-width: 0; flex: 1 1 0; padding-right: 2px; }
/* ★字号基准 = .pdf-viewer-title(ai-chat-bubbles.css)——本壳的 overlay/header 本来就复用
   .pdf-viewer-* 那一族,标题当然跟同一族走:16px / 500 / DuoHand 字族 / 省略号。
   原来取 --text-h4 是 clamp(16px…18px):手机上正好 16、桌面涨到 18,所以只有电脑端显大
   (用户 2026-07-27)。换成这条定值后【手机维持原样】,桌面收回 16。
   字距不带 0.05em —— 那是 DuoHand 字标的处理,文件名不是字标。 */
.duo-viewer-crumb {
  font-family: "Flair", "Jost Fallback", "PingFang SC", "Microsoft YaHei", sans-serif;
  font-size: 16px; font-weight: 400;
  color: var(--color-text-secondary); cursor: pointer; white-space: nowrap;
  /* ★可收缩:nowrap 的文字若不给 overflow,flex 压不下去 —— 它会直接顶到右边的页码格上
     (用户 2026-07-27:"标题跟页码输入打架")。让它省略号截断,空间归工具条。 */
  overflow: hidden; text-overflow: ellipsis; min-width: 0;
}
/* 当前项 = 文件名:与上级面包屑的区别【只用颜色】表达。
   原来还叠了 font-weight:500 —— 同一件事编码两遍是冗余不是决策,而且这一条顶栏其它文字全是 400,
   单把文件名加粗会让它读起来像标题(用户 2026-07-27 提出改 400)。 */
.duo-viewer-crumb.is-current { color: var(--color-text); cursor: default; overflow: hidden; text-overflow: ellipsis; }
/* 文字看着比同排图标高:三个盒子的中心其实是重合的(实测 back 67.7+32 / svg 72.7+22 / title 70.5+26.4,
   中心都 83.7),偏的是【字在盒子里的位置】—— 字体的 ascent/descent 不对称(Jost 实测 17/6),
   半行距把这块居中后基线落在盒中心下方,而拉丁文件名墨迹上 13 下 0,墨迹中心就跑到上面去了。
   交给浏览器按【当前字体自己的度量】裁掉基线以上/大写高度以外的空隙,盒子从此贴着字 ——
   换字体、换语言都自动跟着变,不写任何补偿数值。
   竖向内距对称(不改居中),只为让下伸笔画不被上面那条 overflow:hidden 裁掉。 */
.duo-viewer-crumb, .duo-viewer-title {
  text-box-trim: trim-both;
  text-box-edge: cap alphabetic;
  padding-block: 0.25em;
}
/* 文字比同排图标高 1px:盒子三者中心是重合的(实测 back 67.7+32 / svg 72.7+22 / title 70.5+26.4,
   中心都是 83.7),偏的是字 —— Jost 的 ascent/descent = 17/6 不对称,半行距把这块居中后基线落在
   盒中心【下方 5.5px】,而文件名这类拉丁文墨迹上 13 下 0,墨迹中心就跑到盒中心上方 1px。
   图标墨迹对称,一比就露。按这组实测度量把文字压回 1px;用位移不用 padding —— padding 会改盒高、
   flex 居中后只补回一半。 */
.duo-viewer-crumb, .duo-viewer-title { transform: translateY(1px); }
.duo-viewer-crumb-sep { font-size: 16px; color: var(--color-text-secondary); opacity: 0.5; }
.duo-viewer-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 工具条槽:居中,不抢左右两段的宽度 */
.duo-viewer-tools-slot { flex: 0 0 auto; display: flex; align-items: center; }
/* ★只让 nav 增长(它 flex:1 1 0 是唯一增长者),工具槽与本段都按内容定宽 ——
   工具与窗口按钮自然贴右,剩余宽度【全部】归面包屑/文件名(用户 2026-07-27:"给文件名腾出空间")。
   原来本段也 flex:1 1 0,于是它和 nav 平分空档,工具槽被夹在中间居中。 */
.duo-viewer-actions { display: flex; align-items: center; gap: 4px; flex: 0 0 auto; }
/* 关闭键沿用 .project-modal-close,但同一排图标必须同色:按 .chat-header-btn 基准(亮 0.6 黑 / 暗 0.6 白),特异性压过 .dark-mode .project-modal-close */
.duo-viewer-actions .duo-viewer-close { color: rgba(0, 0, 0, 0.6); }
.dark-mode .duo-viewer-actions .duo-viewer-close,
.dark-mode-active .duo-viewer-actions .duo-viewer-close { color: rgba(255,255,255,0.6); }
html:not([data-input="touch"]) .dark-mode .duo-viewer-actions .duo-viewer-close:hover,
html:not([data-input="touch"]) .dark-mode-active .duo-viewer-actions .duo-viewer-close:hover { color: rgba(255,255,255,0.85); }
/* ★整窗关掉浏览器缩放:只在 PDF 滚动区关不够 —— 手指落在顶栏/rail 上捏合仍走页面缩放,
   于是"两种捏合"并存、行为不一致(用户 2026-07-27)。查看器是自绘的,缩放归它自己管。 */
.duo-viewer { touch-action: pan-x pan-y; }
.duo-viewer-body { flex: 1 1 auto; min-height: 0; display: flex; position: relative; }
/* ★舞台自己不滚:滚动归渲染器内部(PDF 有自己的 scroller)。两层 overflow:auto 会出双滚动条。 */
.duo-viewer-stage { flex: 1 1 auto; min-width: 0; min-height: 0; overflow: hidden; }
/* The viewer is a host that gives its picture the whole stage, shown whole (owner 2026-09-13: "why does the image
   not fill the window"). The 820px cap on the base rule is the Studio popup's shrink-wrap; hosts set the size. */
.duo-viewer-stage > img.duo-preview { width: 100%; height: 100%; max-width: none; max-height: none; }
/* overview state (breadcrumb one level up): the caller supplies the grid layout class (.studio-pf-picker-grid); this only fills the stage, scrolls, pads */
/* the stage is a centring flex box (.pdf-viewer-body): without width/stretch the grid shrinks to one centred column */
/* The Files page has ONE rail, and the title, the toolbar and the grid all start on it. The rail is a value per
   breakpoint (the overview's own horizontal padding), declared once here and read by both - hardcoding the desktop
   number is how the phone came out 10px off. The title adds the toggle button's optical inset
   ((34px button - 22px glyph) / 2 = 6px) so its icon starts on the same pixel as the 9-grid icon.
   Only padding-LEFT is overridden: the title's vertical rhythm (its distance from the top) is the images page's
   own, per breakpoint, and must keep coming from there - a padding shorthand here overrode that too and left the
   Files title 200px down a phone screen where Images sits at 120px. */
#filesViewPage { --files-rail: 24px; }
#filesViewPage .duo-viewer-overview { padding-left: var(--files-rail); padding-right: var(--files-rail); }
#filesViewPage .images-title-row { width: auto; max-width: none; margin: 0; padding-left: calc(var(--files-rail) + (34px - var(--icon-size)) / 2); }
/* 清空回收站:标题行右端,与网格右缘同一条竖线(标题行的右内距本来是 0,所以这里补上同一条 rail) */
#filesViewPage .images-title-row { padding-right: var(--files-rail); }
.files-empty-trash { margin-left: auto; flex-shrink: 0; }
@media (max-width: 768px), (max-height: 500px) {
  #filesViewPage { --files-rail: 14px; }
}
.duo-viewer-overview { width: 100%; align-self: stretch; flex: 1 1 auto; height: 100%; box-sizing: border-box; overflow-y: auto; padding: 16px 24px; display: flex; flex-direction: column; gap: 14px; }
/* toolbar: density toggle (images page component) on the left, the three dropdowns (settings language-picker component) on the right */
.duo-viewer-overview-bar { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; flex-shrink: 0; }
.duo-viewer-overview-bar .settings-lang-dropdown { margin-left: auto; }
.duo-viewer-overview-bar .settings-lang-dropdown ~ .settings-lang-dropdown { margin-left: 0; }
.duo-viewer-overview-bar .settings-lang-icon { display: flex; align-items: center; flex-shrink: 0; }
.duo-viewer-overview-bar .settings-lang-icon svg { width: var(--icon-size); height: var(--icon-size); }
/* phones: the bar stays one row (owner 2026-08-23) - the dropdowns drop their words and keep icon + chevron */
@media (max-width: 768px), (max-height: 500px) {
  .duo-viewer-overview { padding: 12px 14px; }
  .duo-viewer-overview-bar { flex-wrap: nowrap; gap: 10px; }
  .duo-viewer-overview-bar .images-view-toggle { width: auto; flex-shrink: 0; }   /* the images page stretches its toggle to 100% on phones; in this bar it shares the row */
  .duo-viewer-overview-bar .settings-lang-current { display: none; }
  .duo-viewer-overview-bar .settings-lang-trigger { gap: 2px; }
}
/* the picker grid caps itself at 52vh for a modal; here it fills the stage. Its 4px padding is the modal's room for a selected
   tile's ring, which a clipping scroll box would cut; this grid clips nothing (the host scrolls, with its own padding), and
   the padding moved every tile 4px off the edges the sizes below are measured from. */
.duo-viewer-overview-grid.studio-pf-picker-grid { max-height: none; margin: 0; padding: 0; align-content: start; }
/* -- Tile sizes and spacing: flair.design's springboard grid (owner 2026-09-15, relayed by Flair 设计). Every overview grid
   (Files, Portfolio, Templates, Training) is this one grid. The compact 9-grid mode is the small tile, .images-grid--large
   (4-grid) the large one.
   Phones: the columns meet with no gap and each tile is inset inside its column, so the room between two tiles is
   twice that inset and a name still has its whole column to wrap in (it clamps at two lines, and the library's names
   are long). The inset used to be a share of the column, which left the outermost tiles short of the page's rail by
   3.5% of the grid - and since the tiles are the biggest, most repeated shape on the page, the eye read THEIR edge as
   the page's edge and everything actually on the rail looked pushed out (owner 2026-09-17). It is a length now, and
   the grid steps out by exactly that length, so a tile's outer edge lands on the rail while its column keeps its
   width. ★The inset can never exceed that rail: the grid can only step out as far as the padding it steps out of,
   and a pixel more made the overview's own scroller run sideways by that pixel (measured at 320/360/390 on both the
   Files page and Studio). minmax(0, 1fr), never bare 1fr: a 1fr column cannot be narrower than its longest
   unbreakable word, so one long name widened its column. Gate: scripts/ui/overview-grid-rail.test.mjs. */
@media (max-width: 768px), (max-height: 500px) {
  .duo-viewer-overview-grid.studio-pf-picker-grid { --duo-tile-inset: 13px; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 26px 0; margin-inline: calc(-1 * var(--duo-tile-inset)); }
  .duo-viewer-overview-grid.studio-pf-picker-grid.images-grid--large { --duo-tile-inset: 14px; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 30px 0; }
  .duo-viewer-overview-grid .studio-pf-thumbwrap { width: calc(100% - 2 * var(--duo-tile-inset)); padding-bottom: calc(100% - 2 * var(--duo-tile-inset)); }
}
/* Desktop: the phone's pixel sizes, 80px and 112px, as many per row as fit, the first and last tile flush with the edges
   (space-between spreads what is left; the gap is the minimum). At most 10 and 8 per row: once the grid is wide enough
   for that many at the minimum gap, the gap grows instead of another column appearing - (100% - 10 * 80px) / 9 is the
   gap at which exactly ten fill the row. flair.design switches to fixed counts at a 1184px window, which is where its
   own grid reaches that width; here the grid's own width decides, because it sits in a viewer window or beside the
   sidebar, narrower than the window. */
@media (min-width: 769px) and (min-height: 501px) {
  .duo-viewer-overview-grid.studio-pf-picker-grid { grid-template-columns: repeat(auto-fill, 80px); row-gap: 26px; column-gap: max(22px, calc((100% - 800px) / 9)); justify-content: space-between; }
  .duo-viewer-overview-grid.studio-pf-picker-grid.images-grid--large { grid-template-columns: repeat(auto-fill, 112px); row-gap: 36px; column-gap: max(32px, calc((100% - 896px) / 7)); }
  .duo-viewer-overview-grid .studio-pf-thumbwrap { width: 80px; padding-bottom: 80px; }
  .duo-viewer-overview-grid.images-grid--large .studio-pf-thumbwrap { width: 112px; padding-bottom: 112px; }
}
/* square tiles: aspect-ratio is only a preference on an auto-height box - a tall image would stretch it; the image fills the box absolutely instead */
/* min-height:0 removes the content-based minimum that let a tall image stretch the box; with the ratio-derived height definite,
   the image's height:100% resolves and object-fit crops it. No absolute positioning (iOS clips absolute children inside a
   rounded overflow:hidden box unreliably, which showed up as square corners after a density switch). */
/* iOS resolves the image's height:100% against the natural image inside an aspect-ratio box (tiles went tall). So the square
   comes from the wrap (padding-bottom:100% = width), and the rounded box fills it absolutely - the box itself is rounded, the
   image stays in flow inside it, which every engine clips correctly. */
.duo-viewer-overview-grid .studio-pf-thumbwrap { height: 0; position: relative; }   /* the square's size (width = padding-bottom) is set per breakpoint above */
/* 瓦片在这里被拉到格子那么大(48px 那档的定值圆角在这个尺寸上只剩一成 = 看着是方的,
   owner 2026-09-04:"files里的文件倒角都太小了"),所以走比例那一档。 */
.duo-viewer-overview-grid .studio-portfolio-thumb { position: absolute; inset: 0; width: auto; height: auto; overflow: hidden; border-radius: var(--tile-radius-pct); }
/* 九宫格那颗自带的是【卡片场景】的尺寸(width:100% + aspect-ratio:1 + 百分比内距)。进了网格瓦片,
   尺寸归瓦片管 —— 不交出来它就按自己的比例缩成 70 而瓦片是 88(owner 2026-09-05 实测)。内距仍是百分比,
   所以格子照样跟着盒子走。 */
.duo-viewer-overview-grid .studio-portfolio-thumb.studio-training-mosaic { width: auto; height: auto; aspect-ratio: auto; }
/* A folder tile (a collection, a training object): up to nine small thumbnails placed the way an iPhone places them in a
   folder icon, measured off a real home screen - a 204px folder holds 39px icons 9px apart, 34.5px in from each edge
   (owner 2026-09-15). As shares of the tile's outer width: each 19.1%, at 16.9%, 40.4% and 63.9% across and down.
   They are placed against the tile itself: a percentage padding or gap would be taken from the column, which the tile
   only partly fills. A percentage here is taken inside the tile's border while the shares are of its outer edge, so the
   border (the thumb's own 1px, ai-chat-studio-markup.css) is added back: share s of the outer width = s * inner +
   (2s - 1) * edge. Only in the overview grid: the Training cards keep their own mosaic. */
.duo-viewer-overview-grid .studio-portfolio-thumb.studio-training-mosaic { --mosaic-edge: 1px; display: block; padding: 0; }
.duo-viewer-overview-grid .studio-training-mosaic > .studio-portfolio-thumb {
  position: absolute; right: auto; bottom: auto; aspect-ratio: auto;
  width: calc(19.1% + 0.382 * var(--mosaic-edge)); height: calc(19.1% + 0.382 * var(--mosaic-edge));
}
.duo-viewer-overview-grid .studio-training-mosaic > .studio-portfolio-thumb:nth-child(3n + 1) { left: calc(16.9% - 0.662 * var(--mosaic-edge)); }
.duo-viewer-overview-grid .studio-training-mosaic > .studio-portfolio-thumb:nth-child(3n + 2) { left: calc(40.4% - 0.192 * var(--mosaic-edge)); }
.duo-viewer-overview-grid .studio-training-mosaic > .studio-portfolio-thumb:nth-child(3n) { left: calc(63.9% + 0.278 * var(--mosaic-edge)); }
.duo-viewer-overview-grid .studio-training-mosaic > .studio-portfolio-thumb:nth-child(-n + 3) { top: calc(16.9% - 0.662 * var(--mosaic-edge)); }
.duo-viewer-overview-grid .studio-training-mosaic > .studio-portfolio-thumb:nth-child(n + 4):nth-child(-n + 6) { top: calc(40.4% - 0.192 * var(--mosaic-edge)); }
.duo-viewer-overview-grid .studio-training-mosaic > .studio-portfolio-thumb:nth-child(n + 7) { top: calc(63.9% + 0.278 * var(--mosaic-edge)); }
.duo-viewer-overview-grid .studio-portfolio-thumb img { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; corner-shape: inherit; }
/* Every MARK-type glyph in the overview (brief/deck/3D/folder: a mark on a coloured tile) is drawn at tile scale -
   40% of the square - so they share one size in both densities, not the 28px card glyph or the folder's 22px sidebar
   size (owner 2026-08-28). Full-bleed app icons (.is-app-icon: pdf/ai/psd/3dm) already fill the tile and stay as they are. */
.duo-viewer-overview-grid .studio-portfolio-thumb.has-filetype:not(.is-app-icon) svg { width: 40%; height: 40%; }
.duo-viewer-overview-grid .studio-portfolio-card { width: 100%; }
/* the grid never scrolls on its own (the picker's overflow:auto would give it a second, untemplated scrollbar); the host scrolls with .duo-scroll */
.duo-viewer-overview-grid.studio-pf-picker-grid { overflow: visible; flex: none; }
/* names: the sidebar card's 11px inline style is for 48px tiles; here tiles are big, names take the 13px tier and the whole cell width */
/* (0,3,0) on purpose: the sidebar card rule in ai-chat-studio.css (.studio-portfolio-card .studio-portfolio-name, 60px for a 48px thumb) is (0,2,0) and loads later - at equal specificity it won and every overview name was cut at 60px. */
/* up to two lines at a roomier leading, a name without spaces breaking inside its column rather than being cut (owner 2026-09-15);
   the same size in both densities - toggling changes the tiles, never the text */
.duo-viewer-overview-grid .studio-portfolio-card .studio-portfolio-name { font-size: 13px; color: var(--color-text); max-width: 100%; text-align: center; white-space: normal; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; line-height: 1.45; overflow-wrap: anywhere; }
/* The density toggle needs no colours here: it takes app-wide tokens (ai-chat-images-page.css), which are
   declared on :root / .dark-mode-active and so are in scope inside the viewer too. It used to carry its own
   light+dark set here, which is how the same three buttons ended up with two colour schemes. */
/* ★rail 是【浮层】,不挤压舞台:它一挤压,画布尺寸就在收合动画的每一帧变一次,
   而屏幕空间效果的缓冲(遮蔽图/上一帧反射图/G-Buffer)按画布尺寸建 —— 尺寸一变就重建,
   重建后未填充的那一帧内容为 0:遮蔽读 0 = 全遮蔽、反射读 0 = 黑,镜面金属整块发黑。
   浮层化后画布尺寸全程不变,这一类问题从源头消失。收合走位移(位移不触发重排)。 */
.duo-viewer-rail {
  position: absolute; top: 0; right: 0; bottom: 0; width: 0;
  overflow: hidden; z-index: 3;
  background: var(--color-bg-panel);
  transition: transform 0.3s cubic-bezier(0.2, 0, 0, 1);
}
/* ★右内距交给里面的滚动容器,不写在面板上:写在面板上,滚动条会被挤到离右缘 8px 的地方
   浮着;交出去之后滚动条贴着面板右缘,而内容仍靠 body 自己的 padding-right 不贴边。
   滚动条样式本身走全站唯一模板(.duo-scroll,由 JS 挂在 rail-body 上),这里只管位置。 */
.duo-viewer.has-rail .duo-viewer-rail { width: 132px; padding: 8px 0 8px 8px; border-left: 1px solid var(--color-border); box-sizing: border-box; display: flex; flex-direction: column; }
/* 面板分两层:顶栏常驻(手机端才显形)、内容自己滚 —— 渲染器每次填内容都会清空它的宿主,
   关闭钮待在宿主里会被一起清掉,所以宿主只能是【内层】 */
.duo-viewer-rail-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; padding-right: 8px; }
/* 关闭钮：手机端浮在右上角（位置照手机版 settings，尺寸走 .project-modal-close 真源）；
   桌面是常驻抽屉，顶栏那个图层开关就是它的开合入口，不出这颗 */
.duo-viewer-rail-close { display: none; }
/* 文字行侧栏(3D/PSD 图层):宽度取全站侧边面板基准 --sidebar-width。
   132px 是缩略图侧栏的宽度(缩略图 width:100% 跟着它走),图层名 + 色圆 + 眼睛塞不下。
   手机端不套用:那边 rail 是底部上拉 sheet,宽度归它自己 */
@media (min-width: 769px) and (min-height: 501px) {
  .duo-viewer.has-rail .duo-viewer-rail:has(> .duo-viewer-rail-body.is-list) { width: var(--sidebar-width, 250px); }
}
/* rail 收起:整块滑出右缘(比 :not(:empty) 那条特异性高,不必 !important) */
.duo-viewer.rail-off .duo-viewer-rail { transform: translateX(100%); }

/* rail 里的条目排版:一行一项、行间分割线,规格取 .settings-row(16px 上下内距/14px 字/底线) */
.duo-viewer-rail .duo-preview-rail-item {
  flex: 0 0 auto; width: 100%; box-sizing: border-box;
  /* 分割线走行容器:高亮本身仍是 6px 圆角方形(同全站行项基准) */
  padding: 12px 10px; margin: 0; border: none; border-radius: var(--radius-list-row);
  font-size: 14px;
}
/* ★文字行的行高取全站侧栏行基准 --sidebar-item-height(项目列表那一档),两侧面板同一个值 ——
   左边材质栏与右边图层栏是对称的两块,行距必须一样。缩略图行不套用:那种行的高度由图自己决定。
   行距 = 行高本身(去掉 gap):原来右边多 6px 间隙,同样的内距也会看着比左边松一档。 */
.duo-viewer-rail-body.is-list { gap: 0; }
.duo-viewer-rail-body.is-list .duo-preview-rail-item {
  min-height: var(--sidebar-item-height, 44px); padding: 0 10px;
}
/* ★分割线画在【直的伪元素】上,不能挂在行本身:行为了 hover 高亮带 8px 圆角,
   而 box-shadow / border 都会沿圆角走 —— 线的两头就翘起来。全站其余分割线都长在无圆角的元素上,
   这里的行两样都要(圆角底 + 直线),所以把线单独拆出来。 */
.duo-viewer-rail .duo-preview-rail-item { position: relative; }
.duo-viewer-rail .duo-preview-rail-item + .duo-preview-rail-item::before {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 1px;
  background: var(--color-border);
}
.duo-viewer-rail .duo-preview-rail-label { font-size: 14px; }

/* ── The shell's sibling FILE LIST (.is-files: portfolio / collection siblings) ──
   A file list, not a page-thumb strip: one row per file = a small thumb + the name, readable. On the phone it is the
   bottom sheet the list family already is (title "Files", close works); on the desktop the drawer widens so names
   fit instead of being clipped at 132px (owner 2026-08-29). */
.duo-viewer.has-rail .duo-viewer-rail.is-files { width: 240px; }
.duo-viewer-rail.is-files .duo-preview-rail-item.has-thumb {
  display: flex; flex-direction: row; align-items: center; gap: 10px;
  min-height: var(--sidebar-item-height, 44px); padding: 6px 10px;
}
.duo-viewer-rail.is-files .duo-preview-rail-thumbbox { flex: 0 0 40px; width: 40px; height: 40px; overflow: hidden; border-radius: 8px; }
.duo-viewer-rail.is-files .duo-preview-rail-thumb { width: 100%; height: 100%; object-fit: cover; }
.duo-viewer-rail.is-files .duo-preview-rail-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left; }

.duo-viewer-menu .action-item.is-danger { color: var(--color-danger); }
@media (max-width: 768px), (max-height: 500px) {
  /* 手机:直接满屏(小窗没有意义) */
  .duo-viewer .pdf-viewer-dialog { width: 100%; max-width: 100%; height: 100dvh; border-radius: 0; --viewer-corner: 0px; }
  /* ★手机端 rail = 底部上拉 sheet(不是右侧抽屉):窄屏从右边挤进来只剩一条,竖着看更顺手。
     圆角与高度都取 sheet 的唯一真源、滑动同一条曲线(比会议室那种满屏 sheet 矮,留住画面)。 */
  /* ★:not(.is-thumbs) —— 手机上只有【文字行】侧栏(3D/PSD 图层)变底部 sheet;
     PDF 的页缩略图仍走右侧抽屉:一长条缩略图躺进矮 sheet 只剩两三页可见,反而更难用。 */
  .duo-viewer.has-rail .duo-viewer-rail:not(.is-thumbs) {
    /* 升起/落下交给 sheet 真源那对关键帧(与材质面板同一套),自身的位移过渡在这端要让开,
       否则过渡和动画同时改 transform、两个写者打架 */
    transition: none;
    top: auto; left: 0; right: 0; bottom: 0;
    width: auto; height: var(--duo-sheet-h-short);
    border-left: none; border-top: 1px solid var(--color-border);
    border-radius: var(--duo-sheet-radius) var(--duo-sheet-radius) 0 0;
    flex-direction: column;
    /* 右内距同样交给滚动容器(见桌面那条的理由),这里的量是 16 不是 8 */
    padding: 8px 0 16px 16px;
  }
  .duo-viewer-rail-body { gap: 0; }
  .duo-viewer-rail:not(.is-thumbs) .duo-viewer-rail-body { padding-right: 16px; }
  .duo-viewer-rail:not(.is-thumbs) .duo-viewer-rail-close { display: flex; position: absolute; top: 16px; right: 16px; z-index: 2; }
  /* 顶部渐变与让位：同材质面板那套，高度按【本面板关闭钮的盒子】推——
     钮占 16..52，不透明段(60%)要盖到 52 → 52 / 0.6 ≈ 88；内容从 56 起 */
  .duo-viewer-rail:not(.is-thumbs)::before {
    content: ""; position: absolute; top: 0; left: 0; right: 0; height: 88px;
    background: linear-gradient(to bottom, var(--color-bg-panel) 60%, transparent);
    z-index: 1; pointer-events: none;
    border-radius: var(--duo-sheet-radius) var(--duo-sheet-radius) 0 0;
  }
  .duo-viewer.has-rail .duo-viewer-rail:not(.is-thumbs) { padding-top: 0; }
  /* 同材质 sheet 那课:内容从渐变【完全透明处】(88)再留 4 起,52..88 的淡出段不许压内容 */
  .duo-viewer-rail:not(.is-thumbs) .duo-viewer-rail-body { padding-top: 92px; }
  .duo-viewer.rail-off .duo-viewer-rail:not(.is-thumbs) { transform: translateY(100%); }
  /* 缩略图侧栏在手机上比桌面再窄一档(改成 sheet 之前就是 96px),其余几何沿用基类的右侧抽屉 */
  .duo-viewer.has-rail .duo-viewer-rail.is-thumbs { width: 96px; }
}

/* ── PDF 自绘(ui-chat-file-preview-pdf.js)──────────────────────────────
   尺寸沿用宿主变量(与 iframe 时代一致):全屏 viewer 给 100%,Studio 小预览给默认框。
   ★页占位框在渲染前就给准尺寸 → 滚动条长度稳定,不会边渲染边跳。 */
.duo-pdf-scroller {
  /* ★关掉容器内的浏览器捏合缩放:捏合 = 页面缩放,会把所有 canvas 按新倍率重新光栅化 →
     iOS 内存超限直接杀标签页(用户 2026-07-27 实测"一捏就崩")。缩放改由本渲染器接管、重排重渲。 */
  touch-action: pan-x pan-y;
  width: var(--preview-w); height: var(--preview-h);
  overflow: auto; background: var(--color-bg-page);
  /* ★永远预留滚动条槽:首次布局时内容还没排完、竖滚动条尚未出现,clientWidth 会把它将来要占的宽度
     也算进去 → 按此算出的"适应宽度"等滚动条一出现就比可视区宽一截(用户 2026-07-26:"默认略宽")。
     stable 让 clientWidth 在有/没有滚动条两种状态下【相同】,一次算准,不必事后再量一遍纠正。
     注:scrollbar-gutter 与 .duo-scroll 的 ::-webkit-scrollbar 自定义宽度不冲突
     (会冲突的是标准属性 scrollbar-width,那条另有记录)。 */
  scrollbar-gutter: stable;
}
/* 页面层:排版落在这里,滚动留给外面的 scroller —— 捏合预览要变换的正是这一层
   (变换加在滚动容器上会把滚动搞坏)。可用宽度仍按 scroller.clientWidth − 16 算,
   因为 8px 内距只是从 scroller 挪到了这里,总量没变。 */
.duo-pdf-pages {
  display: flex; flex-direction: column; gap: 8px;
  padding: 8px; box-sizing: border-box;
  /* ★放大到比容器宽时,居中会让页面【向左右两侧同时溢出】,而左边那半永远滚不到:
     scrollLeft 最小是 0 = 容器左内边缘,页面却起始于负坐标。safe 在溢出时自动退回
     start 对齐,不再吞掉左半边(同 .studio-org-agents 那条)。
     两条都写:不认 safe 的浏览器会让整条失效退回 stretch,页面被拉满宽。 */
  align-items: center;
  align-items: safe center;
}
.duo-pdf-page { position: relative; flex: 0 0 auto; background: #fff; box-shadow: 0 1px 4px rgba(0,0,0,0.12); }
.duo-pdf-canvas { display: block; }
.duo-pdf-thumb { width: 100%; height: auto; display: block; background: #fff; }
/* 上一页 = 复用 chevronDown 翻转(禁止为方向再画一个图标) */
.duo-preview-tool.is-flip svg { transform: rotate(180deg); }

/* 可输入的小方格(页码):与 readout 同一副外观,靠光标/聚焦表示"能改",不另造一种样式 */
.duo-preview-field {
  /* ★正方 32×32 + 6px 圆角:全站没有"长方形圆角"这种元素,方形 6px 是图标按钮的既有基准。
     写在【基准规则】里而不是手机 media query 里 —— 桌面同样不该出现长方格。 */
  width: 32px; height: 32px; padding: 0; box-sizing: border-box; text-align: center;
  border: 1px solid var(--color-border); border-radius: calc(6px * var(--corner-k));
  background: var(--color-bg-panel); color: var(--color-text);
  font-size: 11px; line-height: 1.3; font-family: inherit;
  font-variant-numeric: tabular-nums;
}
.duo-preview-field:focus { outline: none; border-color: var(--color-text); }

/* ── 缩放浮层(ctx.zoom,PDF/PSD 共用):点放大镜才出。横向滑块 + 够大的百分比输入框 ── */
.duo-preview-zoompop {
  display: none; position: fixed; z-index: 100001;   /* 高于查看器 overlay */
  align-items: center; gap: 8px;
  padding: 8px 10px; border-radius: calc(10px * var(--corner-k));
  background: var(--color-bg-panel); border: 1px solid var(--color-border);
  box-shadow: 0 4px 12px rgba(0,0,0,0.14);
}
.duo-preview-zoompop.is-open { display: flex; }
/* ★不吃浏览器默认外观:原生 range 的轨道在暗色 UI 上就是一条白杠,晃眼
   (用户 2026-07-27:"zoom 的白条在夜间模式太赤眼")。accent-color 只染滑块、染不了轨道,
   所以根治是 appearance:none 后自绘 —— 轨道用 --color-border、滑块用 --color-text,
   两个变量本身 theme-aware,亮暗各自成立,不需要再写一份暗色覆盖。 */
.duo-preview-zoomslider {
  width: 150px; height: 20px; margin: 0;
  -webkit-appearance: none; appearance: none;
  background: transparent; cursor: pointer;
}
.duo-preview-zoomslider::-webkit-slider-runnable-track {
  height: 4px; border-radius: 999px; corner-shape: round; background: var(--color-border);
}
.duo-preview-zoomslider::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 12px; height: 12px; border-radius: 50%; corner-shape: round; border: none;
  background: var(--color-text);
  margin-top: -4px;                 /* (轨道 4 − 滑块 12) / 2:把滑块压回轨道中线 */
}
.duo-preview-zoomslider::-moz-range-track {
  height: 4px; border-radius: 999px; corner-shape: round; background: var(--color-border);
}
.duo-preview-zoomslider::-moz-range-thumb {
  width: 12px; height: 12px; border-radius: 50%; corner-shape: round; border: none;
  background: var(--color-text);
}
.duo-preview-zoominput {
  /* ★正好三位数:上限 400、下限也不会到四位,所以 3ch 就是这个框的全部需求。
     ch = 字符"0"的宽度,配 tabular-nums 每个数字等宽 → 三位数严丝合缝,不靠拍一个 52px。
     再加 padding 12px + 边框 2px(border-box 要把它们算进来)。 */
  width: calc(3ch + 14px); padding: 4px 6px; box-sizing: border-box; text-align: right;
  border: 1px solid var(--color-border); border-radius: calc(6px * var(--corner-k));
  background: var(--color-bg-panel); color: var(--color-text);
  font-size: 14px; font-family: inherit; font-variant-numeric: tabular-nums;
}
.duo-preview-zoominput:focus { outline: none; border-color: var(--color-text); }
.duo-preview-zoompct { font-size: 12px; color: var(--color-text-secondary); }

/* ── 画线浮层:点画笔先出【调色 + 形状】两排,不直接给笔(用户 2026-07-27)──
   第一排颜色圆圈整套复用项目命名的 .project-color-picker/.project-color-dot(设计宪法第三条:
   同一用途只允许一个基准),第二排是形状。选中任一项才真正进入画线态。 */
.duo-pdf-drawpop {
  display: none; position: fixed; z-index: 100001;
  flex-direction: column; gap: 8px;
  padding: 10px; border-radius: calc(10px * var(--corner-k));
  background: var(--color-bg-panel); border: 1px solid var(--color-border);
  box-shadow: 0 4px 12px rgba(0,0,0,0.14);
}
.duo-pdf-drawpop.is-open { display: flex; }
/* ★两排必须一一对应(用户 2026-07-27):颜色圆 26px、工具按钮 32px,直接并排排不齐。
   给两排同一套 6 列栅格 —— 各自保留自己的基准尺寸,只让【列距】一致,居中对齐即成列。 */
.duo-pdf-drawpop .project-color-picker,
.duo-pdf-drawshapes {
  display: grid; grid-template-columns: repeat(6, 32px); gap: 4px; justify-items: center; align-items: center;
}
/* ★选中圆环按底色取【对比色】:黑墨用白环,其余彩色用黑环 —— 白环压在黄色上几乎看不见。
   项目命名那套一律白环是因为那边底色都偏深;这里处境不同,按处境分,不是两套随意值。 */
.duo-pdf-drawpop .project-color-ring { border-color: #1a1a1a; }
.duo-pdf-drawpop .project-color-dot[data-ink="default"] .project-color-ring { border-color: #fff; }

/* ── PDF 文字层:透明字覆在页上,让文字能选能复制(canvas 里只有像素)──
   叠放用显式 z-index 定死,不靠 DOM 到达顺序:这三层都是异步铺的,谁先到不一定。
   链接必须在文字层【之上】,否则整页被透明字盖住、链接点不动。 */
.duo-pdf-textlayer {
  position: absolute; inset: 0; overflow: hidden; z-index: 1;
  line-height: 1; text-align: initial; text-size-adjust: none;
  forced-color-adjust: none; transform-origin: 0 0;
}
.duo-pdf-textlayer span, .duo-pdf-textlayer br {
  color: transparent; position: absolute; white-space: pre;
  cursor: text; transform-origin: 0% 0%;
}
/* ── PDF 超链接层:canvas 只是像素,链接要单独铺可点的 <a> ── */
.duo-pdf-links { position: absolute; inset: 0; z-index: 2; }
.duo-pdf-link { position: absolute; display: block; cursor: pointer; }
html:not([data-input="touch"]) .duo-pdf-link:hover,
html:not([data-input="touch"]) .duo-pdf-link:active{ background: rgba(64,128,255,0.16); }

/* ── 临时标注层(画线;不落库)── */
.duo-pdf-ink { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 3; }
.duo-pdf-scroller.is-drawing .duo-pdf-ink { cursor: crosshair; touch-action: none; }
.duo-pdf-scroller:not(.is-drawing) .duo-pdf-ink { pointer-events: none; }   

/* 顶栏工具条兜底:即使某个渲染器声明了很多常驻工具,也只横向滚动、绝不把顶栏撑爆 */
.duo-preview-tools.is-topbar { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; max-width: 100%; }
.duo-preview-tools.is-topbar::-webkit-scrollbar { height: 0; }
.duo-viewer-menu-sep { height: 1px; background: var(--color-border); margin: 4px 6px; }
@media (max-width: 768px), (max-height: 500px) {
  /* ★手机端不出"最大化":窗口本来就是满屏(见上面的 100dvh 规则),再给个最大化按钮既无效又占宽度 */
  .duo-viewer-actions .duo-preview-tool[data-role="max"] { display: none; }
  .duo-viewer-nav .duo-viewer-crumb { display: none; }   /* 面包屑让位给工具条,返回键仍在 */
}

/* ── rail 条目:带缩略图的走【竖排】,页码落在图下方居中 ──────────────────
   横排时缩略图 width:100% 会吃掉整行,页码徽标拿不到宽度 → 溢出被 rail 裁掉
   (手机上 1 位数贴边、2 位数缺一半,用户 2026-07-27 实测)。竖排后徽标有自己的一行。
   带文字的条目(3D/PSD 图层)仍走横排,不受影响。 */
.duo-preview-rail-item.has-thumb { flex-direction: column; gap: 2px; }
.duo-preview-rail-item.has-thumb .duo-preview-rail-badge { text-align: center; width: 100%; }

@media (max-width: 768px), (max-height: 500px) {
  /* ★靠右不靠弹簧,靠顶栏本来的三段布局:nav | 工具槽 | actions。
     只让 nav 增长(它 flex:1 1 0,是唯一的增长者),另两段按内容定宽 —— 工具与 ⋯ 自然贴右,
     剩下的宽度【全部】归标题(用户 2026-07-27:"只要不到页码输入 都可以显示")。
     上一版把工具槽也设成可增长,它就和标题【平分】剩余空间,标题被压到 4 个字。 */
  /* 顶栏腾出来的位置显示【所在文件夹】:面包屑只留父级,当前文件名那段收起
     (文件名已在下面的内容里/⋯ 菜单里可改名,顶栏重复没意义)。 */
  .duo-viewer-nav .duo-viewer-crumb { display: inline; }
  .duo-viewer-nav .duo-viewer-crumb.is-current,
  .duo-viewer-nav .duo-viewer-crumb-sep { display: none; }
}

/* ── xlsx 自绘(ui-chat-file-preview-xlsx.js)────────────────────────────
   画的是【表格本身】:列标 A/B/C + 行号 + 网格,位置能和 Excel 里对上。
   颜色全走变量 → 暗色自动成立;表头必须【不透明】(--color-bg-surface),
   半透明的话滚动时下面的单元格会从吸顶表头里透出来。 */
.duo-xlsx-scroller {
  width: var(--preview-w); height: var(--preview-h);
  overflow: auto; background: var(--color-bg-page);
  scrollbar-gutter: stable;
}
.duo-xlsx-table {
  border-collapse: collapse;
  /* fixed:列宽只认 colgroup(= 文件里的列宽);auto 会让长文本把列撑开,和 Excel 里的版面对不上 */
  table-layout: fixed;
  font-size: 12px; color: var(--color-text); background: var(--color-bg-panel);
}
.duo-xlsx-cell, .duo-xlsx-colhead, .duo-xlsx-rowhead, .duo-xlsx-corner {
  border: 1px solid var(--color-border);
  padding: 3px 6px; box-sizing: border-box;
  white-space: pre; overflow: hidden; text-overflow: ellipsis;
  vertical-align: bottom;   /* Excel 的默认竖向对齐就是底对齐 */
}
.duo-xlsx-cell.is-wrap { white-space: pre-wrap; word-break: break-word; }
.duo-xlsx-cell.is-bold { font-weight: 500; }     /* 字重阶梯只有 400/500,粗体一律落 500 */
.duo-xlsx-cell.is-italic { font-style: italic; }
.duo-xlsx-colhead, .duo-xlsx-rowhead, .duo-xlsx-corner {
  background: var(--color-bg-surface); color: var(--color-text-secondary);
  font-weight: 400; font-size: 11px; text-align: center; vertical-align: middle;
  position: sticky;
}
.duo-xlsx-colhead { top: 0; z-index: 2; }
.duo-xlsx-rowhead { left: 0; z-index: 2; }
.duo-xlsx-corner { top: 0; left: 0; z-index: 3; }
.duo-xlsx-col-num { width: 44px; }
.duo-xlsx-note { padding: 10px 12px; text-align: left; }

/* ── PSD 自绘(ui-chat-file-preview-psd.js)────────────────────────────
   画布 + (需要时的)一行说明竖排。gap 沿用 .duo-pdf-pages 的 8px,不另定数值。 */
.duo-psd-view { width: var(--preview-w); height: var(--preview-h); display: flex; flex-direction: column; min-width: 0; }
.duo-psd-scroller {
  flex: 1 1 auto; min-height: 0; overflow: auto; display: flex;
  background: var(--color-bg-page); scrollbar-gutter: stable;
  /* 关掉浏览器自带的捏合缩放:那是【整页】缩放,会把 canvas 按新倍率重新光栅化(iOS 上直接吃满内存),
     而且对 canvas 只是拉伸模糊。捏合改由渲染器接管、真的换倍率重画。 */
  touch-action: pan-x pan-y;
}
/* ★内边距由 JS 给(= 视口宽减去要留住的那一条):它就是【把图推离中心】的余量,图因此总是
   大于视口 → 居中改由滚动位置表达(见 centerView)。auto 边距留着当首帧兜底:JS 还没来得及
   量出视口时内容仍是居中的,且它在空间为负时自动退成 0,永远不会像 flex 居中那样裁掉溢出的一半。 */
.duo-psd-stage { margin: auto; padding: 8px; }
.duo-psd-canvas { display: block; height: auto; }
.duo-psd-view > .duo-preview-hint { flex: 0 0 auto; padding: 6px 10px; }

/* ── 通用:预览里的一行说明(截断提示 / 条目数 / "由 DuoHand 自行渲染")────────────── */
.duo-preview-note { padding: 10px 14px; font-size: 12px; color: var(--color-text-secondary); }

/* ── 文本 / 代码 / Markdown(ui-chat-file-preview-text.js)────────────────────────
   代码字体栈 = 聊天气泡代码块那一套(ai-chat-bubbles.css),不另起一份。 */
.duo-text-view { width: var(--preview-w); height: var(--preview-h); overflow: auto; background: var(--color-bg-page); scrollbar-gutter: stable; }
.duo-text-pre {
  margin: 0; padding: 16px 20px;
  font-family: "SF Mono", "Menlo", "Monaco", "Consolas", "PingFang SC", "Microsoft YaHei", monospace;
  font-size: 13px; line-height: 1.55; color: var(--color-text);
  white-space: pre-wrap; word-break: break-word; tab-size: 4;
}
.duo-md-view { max-width: 820px; margin: 0 auto; padding: 20px 28px; font-size: 16px; line-height: 1.6; color: var(--color-text); }
.duo-md-view h1 { font-size: 24px; } .duo-md-view h2 { font-size: 20px; } .duo-md-view h3 { font-size: 18px; }
.duo-md-view h1, .duo-md-view h2, .duo-md-view h3, .duo-md-view h4 { font-weight: 500; margin: 1.2em 0 0.5em; }
.duo-md-view p, .duo-md-view ul, .duo-md-view ol { margin: 0 0 0.9em; }
.duo-md-view pre { padding: 12px 14px; overflow: auto; background: var(--color-bg-surface); border-radius: calc(8px * var(--corner-k)); font-size: 13px; }
.duo-md-view code { font-family: "SF Mono", "Menlo", "Monaco", "Consolas", "PingFang SC", "Microsoft YaHei", monospace; font-size: 0.9em; }
.duo-md-view img { max-width: 100%; height: auto; }
.duo-md-view table { border-collapse: collapse; }
.duo-md-view td, .duo-md-view th { border: 1px solid var(--color-border); padding: 4px 8px; }

/* ── 视频 / 音频(ui-chat-file-preview-media.js)─────────────────────────────────── */
.duo-media-view { width: var(--preview-w); height: var(--preview-h); display: flex; align-items: center; justify-content: center; background: var(--color-bg-page); }
.duo-media-video { max-width: 100%; max-height: 100%; background: #000; }
.duo-media-audio { width: min(520px, 90%); }

/* ── zip 目录清单(ui-chat-file-preview-zip.js)─────────────────────────────────── */
.duo-zip-view { width: var(--preview-w); height: var(--preview-h); overflow: auto; background: var(--color-bg-page); scrollbar-gutter: stable; font-size: 13px; color: var(--color-text); }
.duo-zip-row { display: flex; gap: 12px; align-items: baseline; padding: 6px 14px; border-bottom: 1px solid var(--color-border); }
.duo-zip-row.is-dir { color: var(--color-text-secondary); }
.duo-zip-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.duo-zip-size { flex: 0 0 auto; color: var(--color-text-secondary); font-variant-numeric: tabular-nums; }

/* ── Word(ui-chat-file-preview-docx.js)─────────────────────────────────────────
   文档 = 一张白纸,暗色下也是白纸(同 PDF 页:.duo-pdf-page 的底色与投影照抄),周围才是面板底。
   纸内的表格线是【文档内容】不是界面边线,所以不走 --color-border。 */
/* 纵向 flex:短文档的纸也撑满视口(flex:1),说明行永远钉在底部(margin-top:auto);长文档照常滚 */
.duo-doc-view { width: var(--preview-w); height: var(--preview-h); overflow: auto; background: var(--color-bg-page); scrollbar-gutter: stable; display: flex; flex-direction: column; }
.duo-doc-page {
  flex: 1 0 auto; width: 100%; max-width: 820px; box-sizing: border-box;
  background: #fff; color: #111; margin: 16px auto 0; padding: 48px 56px;
  box-shadow: 0 1px 4px rgba(0,0,0,0.12); font-size: 14px; line-height: 1.6;
}
.duo-doc-view > .duo-preview-note { margin-top: auto; flex: 0 0 auto; }
.duo-doc-page h1 { font-size: 24px; } .duo-doc-page h2 { font-size: 20px; } .duo-doc-page h3 { font-size: 18px; }
.duo-doc-page h1, .duo-doc-page h2, .duo-doc-page h3, .duo-doc-page h4 { font-weight: 500; margin: 1em 0 0.4em; }
.duo-doc-page p { margin: 0 0 0.8em; }
.duo-doc-page img { max-width: 100%; height: auto; }
.duo-doc-page table { border-collapse: collapse; margin: 0 0 1em; }
.duo-doc-page td, .duo-doc-page th { border: 1px solid #d4d4d4; padding: 4px 8px; vertical-align: top; }

/* ── PowerPoint(ui-chat-file-preview-pptx.js)────────────────────────────────────
   库自己按给定宽度把每页画成一个块并竖向排开;这里只管外框与滚动。 */
.duo-ppt-view { width: var(--preview-w); height: var(--preview-h); overflow: auto; background: var(--color-bg-page); scrollbar-gutter: stable; padding: 16px 0 0; box-sizing: border-box; }
.duo-ppt-host { margin: 0 auto; }

/* Who made the file being shown: the agent's avatar + name, parked on the stage's bottom-right corner (owner 2026-08-29).
   .project-avatar is the site avatar base (ink, shape, portrait-fill); here only the size and the corner seat. */
.duo-viewer-by { position: absolute; right: 16px; bottom: 16px; z-index: 3; display: flex; align-items: center; gap: 8px; pointer-events: none; }
.duo-viewer-by[hidden] { display: none; }
.duo-viewer-by-avatar { width: 28px; height: 28px; font-size: 11px; border-color: var(--color-bg-panel); }
.duo-viewer-by-name { font-size: 12px; color: var(--color-text-secondary); white-space: nowrap; }
@media (max-width: 768px), (max-height: 500px) {
  .duo-viewer-by { right: 12px; bottom: 12px; }
  .duo-viewer-by-name { display: none; }   /* the phone stage is tight: the portrait alone says who */
}
