/* system.css — Develop base + component layer (v3).
 *
 * v3 evolves v2 with the 漫剧工坊 component language on the paper-default
 * palette: `.btn-primary` is the SOLID ACCENT CTA (amber; `.btn-accent` stays
 * as an alias), `.btn-secondary` is the neutral outline sibling, ghost stays
 * the quiet tool button, and every selected/active state (chip / segmented /
 * card / sidebar rail / tree) speaks the accent tonally. /v2 is frozen.
 *
 * Explicit @layer order: a consumer's own UNLAYERED rules always win over every
 * lib layer (opt-in, non-breaking); within lib, utilities > components > reset.
 * Tokens come from v3/tokens.css. Compact 12px base; one shared focus ring; one
 * global reduced-motion guard. Components reference SEMANTIC tokens only. */

@layer lib.reset, lib.components, lib.utilities;

@layer lib.reset {
  *, *::before, *::after { box-sizing: border-box; }

  html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; tab-size: 4; }
  img, svg, video, canvas { max-width: 100%; height: auto; }
  svg { display: block; }
  /* An svg that declares its size via width/height ATTRIBUTES (the inline-icon
     pattern) must not be fluid-capped: inside a shrink-wrapped flex parent
     (e.g. an icon-only .btn) `max-width:100%` makes parent↔child sizing
     circular — Chrome resolves it to the attribute size, WebKit/iOS Safari
     collapses the icon to 0×0 (the "copy button vanished on mobile" bug).
     Explicit attributes mean the author already fixed the size; honor it. */
  svg[width][height] { max-width: none; }
  button, input, textarea, select { font: inherit; color: inherit; }
  :where(p, li, h1, h2, h3, h4, h5, h6) { overflow-wrap: break-word; }

  body {
    margin: 0;
    font-family: var(--font-sans);
    font-size: var(--text-body);
    line-height: var(--leading-body);
    background: var(--background);
    color: var(--foreground);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
  }

  h1, h2, h3, h4, h5, h6 {
    margin: 0;
    font-weight: var(--weight-semibold);
    line-height: var(--leading-heading);
    letter-spacing: var(--tracking-tight);
    color: var(--foreground);
  }
  h1 { font-size: var(--text-display); }
  h2 { font-size: var(--text-subheading); }
  h3 { font-size: var(--text-heading); }
  h4 { font-size: var(--text-title); }
  h5 { font-size: var(--text-body); }
  h6 { font-size: var(--text-micro); }
  p { margin: 0; line-height: var(--leading-body); }
  /* Complete the margin/padding normalization so consumers never need their own
     `*{margin:0;padding:0}` reset (an UNLAYERED consumer reset would beat every
     lib component's padding — see top comment). :where() keeps specificity 0 so
     any product/component rule trivially overrides. */
  :where(ul, ol, menu) { margin: 0; padding: 0; }
  :where(figure, blockquote, dl, dd, fieldset) { margin: 0; }

  a { color: inherit; }

  /* Custom-element FOUC guards (components.js is deferred). */
  dev-site-header:not(:defined) { display: block; min-height: 48px; background: var(--background); border-bottom: 1px solid var(--border); }
  dev-site-footer:not(:defined) { display: block; min-height: 120px; background: var(--background); border-top: 1px solid var(--border); }
  /* Async custom elements with slotted application content stay hidden until
     upgrade; otherwise drawer bodies and option labels dump into normal page
     flow on a cold/slow components.js load. Important is intentional: the
     reset-layer guard must also beat the contextual drawer's utility-layer
     display rule before that element is defined. */
  dev-dialog:not(:defined),
  dev-drawer:not(:defined),
  dev-select:not(:defined) { display: none !important; }

  /* 减少动效的全局守卫搬去 recipes.css 了 —— 页面样式表进不了 shadow DOM,
     那条 `*` 规则于是只覆盖页面这一层,组件内部的动效一概不受约束(实测 9 个
     带动效的组件一个都没被压住)。recipes.css 是两侧共用的那一份。 */
}

@layer lib.components {
  /* ---- Buttons ---- 已抽到 styles/v3/recipes.css(shadow DOM 也要用同一份)。
     构建时会把 recipes.css 拼进这份文件,消费方看到的仍是一个 /v3/system.css。 */
  /* ---- Inputs ---- */
  .input, .textarea, select.input {
    box-sizing: border-box; width: 100%; padding: 0 var(--sp-3);
    font-family: inherit; font-size: var(--text-body); line-height: var(--leading-body);
    color: var(--foreground); background: var(--c-input-bg);
    border: 1px solid var(--c-input-border); border-radius: var(--radius);
    outline: none; caret-color: var(--ui-accent);   /* branded text cursor — a subtle refined detail */
    transition: border-color var(--duration-fast) var(--ease-smooth), box-shadow var(--duration-fast) var(--ease-smooth);
  }
  .input, select.input { min-height: var(--c-input-h); }
  .input-number { width: 4rem; min-width: 4rem; }
  .textarea { padding: var(--sp-2) var(--sp-3); resize: vertical; }
  .input:hover, .textarea:hover, select.input:hover { border-color: var(--border-strong); }
  /* Focus with the BRAND accent — the field a user is typing in reads as intentional, and
     ties the form to the same accent the buttons use (resting state stays neutral). */
  .input:focus-visible, .textarea:focus-visible, select.input:focus-visible,
  .input:focus, .textarea:focus, select.input:focus { border-color: var(--ui-accent); outline: var(--focus-outline-brand); outline-offset: var(--focus-outline-offset); }
  .input::placeholder, .textarea::placeholder { color: var(--placeholder); }
  .input:disabled, .textarea:disabled, select.input:disabled { opacity: var(--state-disabled-opacity); cursor: not-allowed; background: var(--muted); }
  .input-invalid, .input[aria-invalid="true"], .textarea[aria-invalid="true"] { border-color: var(--danger); }
  /* 无效态聚焦给红环,不是品牌琥珀环 —— 环的颜色要跟着控件此刻的意图走。 */
  .input-invalid:focus, .input[aria-invalid="true"]:focus,
  .textarea[aria-invalid="true"]:focus, select.input[aria-invalid="true"]:focus {
    border-color: var(--danger); outline: var(--focus-outline-danger); outline-offset: var(--focus-outline-offset);
  }
  .input-sm { min-height: 1.75rem; font-size: var(--text-caption); }
  .input-lg { min-height: 2.5rem; }
  select.input {
    appearance: none; cursor: pointer; padding-right: var(--sp-8);
    background-image: var(--c-select-caret);
    background-repeat: no-repeat; background-position: right var(--sp-3) center;
  }

  /* ---- Form field scaffold ---- */
  .form-field { display: grid; gap: var(--sp-1); }
  /* Horizontal field: label on the left (fixed column so inputs align across a
     stack), control on the right. Pair with .form-label. Override the label
     column width per-form with --field-label-w. */
  .form-field-row { display: grid; grid-template-columns: var(--field-label-w, 5rem) 1fr; align-items: start; gap: var(--sp-2); }
  /* The label is a box the height of ONE input, top-aligned in the grid, so it lines up with the
     control even when the control column carries a help line beneath the input (align-items:center
     used to sink the label to the middle of input+help — the row-misalignment bug). */
  /* A label longer than the column WRAPS inside it (balanced, so an 8-character
     CJK label splits 4/4 instead of orphaning one glyph). `nowrap` made it spill
     LEFT out of the grid cell instead: in a dialog the label escaped the panel's
     own padding box (16px past it, 5px from the border), and in a multi-column
     filter grid it came within 1px of the neighbouring column's input. */
  .form-field-row > .form-label { white-space: normal; overflow-wrap: break-word; text-wrap: balance; text-align: end; min-block-size: var(--c-input-h); display: inline-flex; align-items: center; justify-content: flex-end; }
  .form-label { font-size: var(--text-caption); font-weight: var(--weight-medium); color: var(--foreground); }
  .form-label .required { color: var(--danger-text); margin-left: 2px; }
  .form-help { font-size: var(--text-micro); color: var(--muted-foreground); }
  /* 小圆圈 "?" 帮助点:啰嗦的说明文字不平铺在界面上,收进 <dev-tooltip> 的触发器
     (operator 2026-08-05:「这些文字做成小圆圈的?,鼠标放上去再显示」)。用法:
     <dev-tooltip text="…"><span class="help-dot" tabindex="0">?</span></dev-tooltip>
     tabindex 让键盘 focus 也能唤出提示(dev-tooltip 监听 focusin)。 */
  .help-dot { display: inline-flex; align-items: center; justify-content: center; inline-size: 14px; block-size: 14px; border-radius: 50%; border: 1px solid var(--border); color: var(--muted-foreground); background: var(--muted, transparent); font-size: var(--text-micro); line-height: 1; font-weight: var(--weight-medium); cursor: help; user-select: none; flex: 0 0 auto; vertical-align: middle; }
  .help-dot:hover, .help-dot:focus-visible { color: var(--foreground); border-color: var(--muted-foreground); }
  /* 11px error copy = normal text, so it takes the INK step (--danger was
     4.34:1 on the paper canvas). */
  .form-error { font-size: var(--text-micro); color: var(--danger-text); }

  /* ---- Checkbox / radio (native, tinted) ---- */
  .checkbox, .radio { width: 1rem; height: 1rem; accent-color: var(--primary); cursor: pointer; }
  .checkbox:focus-visible, .radio:focus-visible { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); }

  /* ---- Tags / badges ---- */
  .tag {
    display: inline-flex; align-items: center; gap: var(--sp-1);
    padding-block: 0.1875rem; padding-inline: var(--sp-2);
    font-size: var(--text-micro); font-weight: var(--weight-medium); line-height: 1.4;
    border-radius: var(--c-tag-radius); background: var(--secondary); color: var(--secondary-foreground); white-space: nowrap;
  }
  /* Soft intent tints communicate category through background + inset border;
     text stays on the guaranteed readable foreground token. Saturated intent
     ink on a 10–14% tint fell below normal-text contrast, especially warning. */
  .tag-success { background: var(--success-bg); color: var(--foreground); box-shadow: inset 0 0 0 1px var(--success-border); }
  .tag-warning { background: var(--warning-bg); color: var(--foreground); box-shadow: inset 0 0 0 1px var(--warning-border); }
  .tag-danger  { background: var(--danger-bg);  color: var(--foreground); box-shadow: inset 0 0 0 1px var(--danger-border); }
  .tag-info    { background: var(--info-bg);    color: var(--foreground); box-shadow: inset 0 0 0 1px var(--info-border); }
  .tag-brand   { background: var(--brand-soft); color: var(--foreground); box-shadow: inset 0 0 0 1px var(--brand-soft-hover); }
  .tag-solid   { background: var(--primary); color: var(--primary-foreground); }
  /* High-contrast filled semantic tags (saturated bg + readable foreground) —
     for a coloured LABEL that must stay legible where the soft .tag-* (light bg
     + saturated text) reads too faintly. */
  .tag-solid-info    { background: var(--info);    color: var(--info-foreground); }
  .tag-solid-warning { background: var(--warning); color: var(--warning-foreground); }
  .tag-solid-success { background: var(--success); color: var(--success-foreground); }
  .tag-solid-danger  { background: var(--danger);  color: var(--danger-foreground); }

  /* ---- Cards ---- */
  /* 卡片配方搬到 recipes.css:组件里也要用它(<dev-card> 以前自抄一份,圆角用
     --radius、阴影用 --shadow-card、内距写死 20px —— 和页面上的 .card 并排放着
     就是两种卡片)。build.mjs 会把 recipes.css 拼进 /v3/system.css,页面侧看到的
     还是同一个文件。 */
  /* Whole-card actions: use with .card + layout utilities. This keeps channel
     landing pages semantic and keyboard accessible without private reset CSS. */
  .action-card { appearance: none; inline-size: 100%; box-sizing: border-box; text-align: start; cursor: pointer; font: inherit; }
  .action-card:focus-visible { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); border-color: var(--ring); }
  .action-card:disabled { cursor: not-allowed; opacity: var(--state-disabled-opacity); }
  /* Small, reusable onboarding flow. Content and actions remain channel-owned;
     lib owns only the responsive visual relationship. */
  .workflow-steps { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-3); }
  .workflow-step { display: flex; align-items: flex-start; gap: var(--sp-3); min-inline-size: 0; padding: var(--sp-3); border: 0; border-radius: var(--radius); background: var(--surface-sunken); color: var(--foreground); }
  .workflow-step-index { display: inline-grid; place-items: center; flex: 0 0 1.75rem; inline-size: 1.75rem; block-size: 1.75rem; border-radius: var(--radius-full); background: var(--primary); color: var(--primary-foreground); font-size: var(--text-micro); font-weight: var(--weight-bold); }
  /* Leading-edge colour accent — a 3px semantic bar on a card's start edge to
     category-code it (e.g. one card per vendor). Generic + reusable. */
  .card-accent-info    { border-inline-start: 3px solid var(--info); }
  .card-accent-warning { border-inline-start: 3px solid var(--warning); }
  .card-accent-success { border-inline-start: 3px solid var(--success); }
  .card-accent-danger  { border-inline-start: 3px solid var(--danger); }
  .card-accent-brand   { border-inline-start: 3px solid var(--brand); }

  /* ---- Alert ---- */
  .alert { display: flex; gap: var(--sp-2); padding: var(--sp-3); border-radius: var(--radius); border: 1px solid var(--border); background: var(--surface-sunken); color: var(--foreground); font-size: var(--text-caption); }
  .alert-success { background: var(--success-bg); border-color: var(--success-border); }
  .alert-warning { background: var(--warning-bg); border-color: var(--warning-border); }
  .alert-danger  { background: var(--danger-bg);  border-color: var(--danger-border); }
  .alert-info    { background: var(--info-bg);    border-color: var(--info-border); }

  /* ---- Progress ---- */
  .progress { height: 6px; border-radius: var(--radius-full); background: var(--muted); overflow: hidden; }
  .progress-bar { height: 100%; border-radius: inherit; background: var(--brand-gradient); transition: width var(--duration-slow) var(--ease-out); }
  .progress-indeterminate .progress-bar { width: 40%; animation: lib-indeterminate 1.1s var(--ease-smooth) infinite; }

  /* ---- Spinner ---- */
  .spinner { display: inline-block; width: var(--icon-md); height: var(--icon-md); border: 2px solid var(--border); border-right-color: var(--foreground); border-radius: 50%; animation: lib-spin 0.7s linear infinite; }

  /* ---- Skeleton ---- */
  .skeleton { background: linear-gradient(90deg, var(--muted) 25%, var(--surface-sunken) 50%, var(--muted) 75%); background-size: 200% 100%; border-radius: var(--radius-sm); animation: lib-shimmer 1.3s linear infinite; }

  /* ---- Empty state ---- */
  /* align-items 是承重的:空态几乎总是和 .stack 一起写(要自定 gap),而 .stack 在
     本文件里排得更靠后,会把 display 从 grid 翻成 flex —— justify-items 于是彻底
     失效,图标和按钮全部靠左,只有文字因为 text-align 还居中。两种 display 下都
     居中,才不用每个调用点各记一次(线上 404 页与资产库空态都栽在这)。 */
  .empty-state { display: grid; justify-items: center; align-items: center; gap: var(--sp-2); padding: var(--sp-8) var(--sp-4); text-align: center; color: var(--muted-foreground); }
  .empty-state-icon { font-size: var(--text-display); opacity: 0.7; }
  .empty-state-title { font-size: var(--text-title); font-weight: var(--weight-semibold); color: var(--foreground); }

  /* ---- Misc primitives ---- */
  .divider { border: none; border-top: 1px solid var(--border); margin: var(--sp-3) 0; }
  .kbd { font-family: var(--font-mono); font-size: var(--text-micro); padding: 1px var(--sp-1); border: 1px solid var(--border); border-bottom-width: 2px; border-radius: var(--radius-sm); background: var(--surface-raised); color: var(--foreground); }
  .code { font-family: var(--font-mono); font-size: 0.95em; padding: 1px var(--sp-1); border-radius: var(--radius-sm); background: var(--surface-sunken); color: var(--foreground); }
  .avatar { display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; border-radius: var(--radius-full); background: var(--secondary); color: var(--secondary-foreground); font-size: var(--text-caption); font-weight: var(--weight-semibold); overflow: hidden; }
  .avatar img { width: 100%; height: 100%; object-fit: cover; }

  /* ---- Overlay (for non-dev-dialog modals) ---- */
  .overlay { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; background: oklch(0.2 0 0 / 0.4); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); z-index: var(--z-modal); animation: lib-fade-in var(--duration-normal) var(--ease-out); }
  .modal-card { background: var(--surface-overlay); color: var(--card-foreground); border: 1px solid var(--border); border-radius: var(--radius-xl); box-shadow: var(--elevation-4); max-width: min(560px, 92vw); max-height: 86vh; overflow: auto; animation: lib-scale-in var(--duration-normal) var(--ease-out); }
  /* Wide variant for content-heavy modals (a form + a data table); pair with .modal-card. */
  .modal-card.modal-lg { max-width: min(900px, 94vw); width: 100%; }

  /* ---- Scrollbar ---- */
  /* 搬去 recipes.css 了:组件在 shadow root 里够不着 system.css,于是每个有滚动区的
     <dev-*> 都露着一条系统默认的粗滚动条(课程栏、对话框正文、下拉列表…)。
     recipes.css 是两侧共用的那一份,页面这边由 build 拼回同一个文件。 */
}

@layer lib.utilities {
  .stack { display: flex; flex-direction: column; gap: var(--stack-gap, var(--sp-4)); }
  .cluster { display: flex; flex-wrap: wrap; align-items: center; gap: var(--cluster-gap, var(--sp-2)); }
  .container, .center { margin-inline: auto; max-width: var(--container-max, 72rem); padding-inline: var(--container-pad, var(--sp-6)); }
  .text-balance { text-wrap: balance; }

  /* ---- Finite layout / spacing utilities (token-driven, RTL-safe logical
     properties, NO JIT — this is what makes "link + use" cover app screens). ---- */
  .flex { display: flex; } .inline-flex { display: inline-flex; }
  .grid { display: grid; gap: var(--grid-gap, var(--sp-4)); }
  .block { display: block; } .inline-block { display: inline-block; }
  .hidden { display: none !important; }
  .flex-col { flex-direction: column; } .flex-row { flex-direction: row; }
  .flex-wrap { flex-wrap: wrap; } .flex-nowrap { flex-wrap: nowrap; }
  .items-start { align-items: flex-start; } .items-center { align-items: center; }
  .items-end { align-items: flex-end; } .items-stretch { align-items: stretch; }
  /* label/数值行要按文字基线对齐(规格事实行、版本条标题):没有这一档时
     消费方写了 items-baseline 落回默认 stretch,一直没人发现(幽灵类审计 2026-08-06)。 */
  .items-baseline { align-items: baseline; }
  .self-start { align-self: flex-start; } .self-end { align-self: flex-end; } .self-center { align-self: center; }
  .justify-start { justify-content: flex-start; } .justify-center { justify-content: center; }
  .justify-end { justify-content: flex-end; } .justify-between { justify-content: space-between; }
  .flex-1 { flex: 1 1 0%; min-width: 0; } .flex-auto { flex: 1 1 auto; }
  .shrink-0 { flex-shrink: 0; } .grow { flex-grow: 1; }
  /* 三段式工具栏要「中间那段真居中」时用:左右两段 grow basis-0 各占一半,
     中间那段就落在容器正中,与两侧内容多寡无关(顶栏的视图切换器就靠它)。 */
  .basis-0 { flex-basis: 0; }
  .grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .grid-auto { grid-template-columns: repeat(auto-fill, minmax(var(--grid-min, 16rem), 1fr)); }
  .grid-rail { grid-template-columns: minmax(13rem, 17rem) minmax(0, 1fr); }
  /* 设置类页面(.grid-rail-nav)的目录列锁定:右列长内容滚动时目录留在原地,
     不随滚动丢失(xshort 设置页 2026-08-05 设计稿要点)。窄屏规则(下方
     media 块)会把它折成水平条,继续生效。
     封顶按**真实滚动视口**算:页面滚动容器在 dev-site-header(56px)之下,
     按 100dvh 封顶的话,矮窗口里目录自己的滚动条永远不启用、末尾几项被裁在
     折叠线下够不到(2026-08-05 评审);站头高度变了就同步 --rail-header-offset。 */
  .grid-rail.grid-rail-nav > .sidebar { position: sticky; inset-block-start: 0; max-block-size: calc(100dvh - var(--rail-header-offset, 56px)); overflow: auto; align-self: start; }
  /* Mirrored rail: content first, metadata/actions rail at the end (viewers). */
  .grid-rail-end { grid-template-columns: minmax(0, 1fr) minmax(13rem, 17rem); }
  /* Two-pane editing workspace. Desktop keeps a stable rail and a fill-height
     editor; phones stack full-width, independently usable panes inside the
     surrounding dialog/page scroller. */
  .workspace-split { display: grid; grid-template-columns: minmax(13rem, 17rem) minmax(0, 1fr); gap: var(--sp-4); block-size: 65dvh; min-block-size: 30rem; }
  .workspace-split > * { min-inline-size: 0; }
  /* Main document + contextual end Inspector. Pair the surrounding page or
     dialog with .cq so the split responds to its usable width, not only the
     viewport. The rail stays visible while both panes own their scrolling. */
  .workspace-split.workspace-split-end { grid-template-columns: minmax(0, 1fr) minmax(22rem, 26rem); align-items: start; block-size: calc(100dvh - 11rem); min-block-size: 38rem; }
  .workspace-split.workspace-split-end > * { min-block-size: 0; block-size: 100%; }
  /* 侧栏用**标准栏宽**(与 .grid-rail-end 同一档):同一个共享面板在不同页面里
     必须一样宽,否则同一个组件会因为列宽不同排出不同的列数,看着就是两个东西。 */
  .workspace-split.workspace-split-rail { grid-template-columns: minmax(0, 1fr) minmax(13rem, 17rem); }
  .workspace-split.workspace-split-end > :last-child { position: sticky; inset-block-start: var(--sp-4); }
  /* Fill variant: inside a fixed app-shell main (flex column) the split grows to
     the exact remaining height instead of guessing with 100dvh math, so a
     bottom-anchored composer really touches the viewport edge. */
  .workspace-split.workspace-split-fill { flex: 1 1 0%; block-size: auto; min-block-size: 0; align-items: stretch; }
  .workspace-split.workspace-split-fill > :last-child { position: static; }
  /* Opt-in contextual end rail. A dev-drawer is the second workspace column on
     desktop, then becomes its normal modal drawer below 48rem. The trigger and
     in-panel close button are real buttons; consumers only wire open()/close(). */
  .workspace-context-toggle, .workspace-context-close { display: none; }
  .workspace-split.workspace-split-end > dev-drawer.workspace-context-drawer {
    position: relative; inset: auto; z-index: auto; display: block;
    min-inline-size: 0; min-block-size: 0; block-size: 100%;
  }
  /* AI-assisted long-form workspace: light outline, one continuous document,
     and a persistent end rail. Each column owns its vertical scrolling and the
     shell never creates a page-level horizontal scrollbar. */
  .document-workspace-shell { display: flex; flex-direction: column; min-inline-size: 0; min-block-size: 38rem; block-size: calc(100dvh - 7rem); overflow: hidden; background: var(--background); }
  .document-workspace-shell[data-fullscreen] { block-size: 100dvh; min-block-size: 0; }
  /* min-block-size:同一产品里单行标题与两行标题的 topbar 必须一样高 ——
     否则切视图时整条工具行(和它下面的一切)上下跳 13px(2026-08-06 像素巡检)。 */
  .document-workspace-topbar { flex: none; display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); min-inline-size: 0; min-block-size: 3.75rem; padding: var(--sp-2) var(--sp-3); border-block-end: 1px solid var(--border); background: var(--background); }
  .document-workspace-topbar-start,.document-workspace-topbar-actions,.document-workspace-title { min-inline-size: 0; }
  /* The action row stays on one line while it fits — that's the desktop toolbar.
     Owned here rather than by a .flex-nowrap utility on the product, because the
     phone rule below has to be able to turn it back on (utilities are the last
     layer, so a utility would win over the media query and pin the row to one
     line at 390px — where it overflows LEFT past x=0 and eats the first control). */
  .document-workspace-topbar-actions { flex-wrap: nowrap; }
  .document-workspace-body { flex: 1 1 auto; display: grid; grid-template-columns: minmax(11rem,14rem) minmax(38rem,1fr) minmax(24rem,28rem); min-inline-size: 0; min-block-size: 0; overflow: hidden; }
  .document-workspace-body > * { min-inline-size: 0; min-block-size: 0; }
  .document-workspace-outline,.document-workspace-main { overflow: auto; overscroll-behavior: contain; }
  .document-workspace-outline { border-inline-end: 1px solid var(--border); background: var(--surface-sunken); }
  .document-workspace-main { background: var(--surface-sunken); }
  .document-workspace-rail { display: flex; flex-direction: column; overflow: hidden; border-inline-start: 1px solid var(--border); background: var(--card); overscroll-behavior: contain; }
  .document-workspace-body > dev-drawer.workspace-context-drawer { min-inline-size: 0; min-block-size: 0; block-size: 100%; }
  .document-workspace-body > dev-drawer.workspace-context-drawer .document-workspace-rail { block-size: 100%; }
  .document-workspace-rail-scroll { flex: 1 1 auto; min-block-size: 0; overflow: auto; }
  .document-workspace-rail-head { flex: none; padding: var(--sp-3); border-block-end: 1px solid var(--border); background: linear-gradient(180deg,color-mix(in oklab,var(--brand-soft) 58%,var(--card)),var(--card)); }
  .document-workspace-agent-mark { display: inline-grid; place-items: center; inline-size: var(--control-h); block-size: var(--control-h); flex: none; border: 1px solid color-mix(in oklab,var(--brand) 40%,var(--border)); border-radius: var(--radius-md); background: var(--brand-gradient); color: var(--brand-gradient-fg); box-shadow: var(--elevation-1); }
  .document-workspace-context-card { padding: var(--sp-2); border: 1px solid color-mix(in oklab,var(--brand) 24%,var(--border)); border-radius: var(--radius-md); background: color-mix(in oklab,var(--brand-soft) 60%,var(--background)); }
  .document-workspace-context-summary { display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 3; }
  .document-workspace-context-actions { display: flex; flex-wrap: wrap; gap: var(--sp-1); }
  .document-workspace-action-chip { --action-tone:var(--brand);--action-bg:var(--brand-soft);display:inline-flex;align-items:center;gap:var(--sp-1);min-block-size:var(--control-h-sm);padding-inline:var(--sp-2);border:1px solid color-mix(in oklab,var(--action-tone) 34%,var(--border));border-radius:var(--radius-full);background:color-mix(in oklab,var(--action-bg) 72%,var(--card));color:color-mix(in oklab,var(--action-tone) 72%,var(--foreground));font:inherit;font-size:var(--text-micro);font-weight:var(--weight-semibold);cursor:pointer;transition:background var(--duration-fast) var(--ease-standard),border-color var(--duration-fast) var(--ease-standard),transform var(--duration-fast) var(--ease-standard)}
  .document-workspace-action-chip:hover,.document-workspace-action-chip[aria-pressed="true"]{background:var(--action-bg);border-color:color-mix(in oklab,var(--action-tone) 62%,var(--border))}
  .document-workspace-action-chip:active{transform:translateY(1px)}
  .document-workspace-action-chip:focus-visible{outline:var(--focus-outline);outline-offset:var(--focus-outline-offset)}
  .document-workspace-action-chip[data-tone="image"],.document-workspace-action-chip[data-tone="reference"]{--action-tone:var(--success);--action-bg:var(--success-bg)}
  .document-workspace-action-chip[data-tone="video"]{--action-tone:var(--info);--action-bg:var(--info-bg)}
  .document-workspace-action-chip[data-tone="audio"],.document-workspace-action-chip[data-tone="settings"]{--action-tone:var(--warning);--action-bg:var(--warning-bg)}
  .document-workspace-action-chip[data-tone="history"]{--action-tone:var(--muted-foreground);--action-bg:var(--muted)}
  .document-workspace-reference-strip { flex: none; padding: var(--sp-2) var(--sp-3); border-block-end: 1px solid var(--border); background: var(--background); }
  .document-workspace-reference-list { display: flex; gap: var(--sp-2); max-inline-size: 100%; overflow-x: auto; padding-block-end: var(--sp-1); overscroll-behavior-inline: contain; scrollbar-width: thin; }
  .document-workspace-reference-item { --reference-tone:var(--info);position:relative;display:grid;grid-template-rows:calc(var(--sp-8) + var(--sp-2)) auto auto;inline-size:calc(var(--sp-12) + var(--sp-4));min-inline-size:calc(var(--sp-12) + var(--sp-4));padding:0;overflow:hidden;border:1px solid color-mix(in oklab,var(--reference-tone) 42%,var(--border));border-radius:var(--radius-md);background:var(--card);color:var(--foreground);font:inherit;text-align:start;box-shadow:var(--elevation-1)}
  /* KNOWN GAP (measured, not fixed here): --reference-tone is used as INK on its
     own 10% tint by .document-workspace-reference-kind below — 2.86:1 for scene
     (success) and 2.05:1 for prop (warning) in light. Closing it needs an ink
     step for warning/info too (amber needs ~55% toward --foreground, far heavier
     than the 65% the success/danger pair uses), which is a palette decision, not
     a rename. Do it with --warning-text/--info-text + a --reference-ink knob. */
  .document-workspace-reference-item[data-tone="scene"]{--reference-tone:var(--success)}
  .document-workspace-reference-item[data-tone="prop"]{--reference-tone:var(--warning)}
  .document-workspace-reference-item[data-tone="style"]{--reference-tone:var(--brand)}
  .document-workspace-reference-item[data-tone="result"],.document-workspace-reference-item[data-tone="video"]{--reference-tone:var(--brand)}
  .document-workspace-reference-item>img,.document-workspace-reference-item>video{inline-size:100%;block-size:100%;min-block-size:0;object-fit:cover;background:var(--surface-sunken)}
  .document-workspace-reference-kind{display:flex;align-items:center;gap:var(--sp-1);padding:var(--sp-0_5) var(--sp-1);border-block-start:var(--sp-0_5) solid var(--reference-tone);background:color-mix(in oklab,var(--reference-tone) 10%,var(--card));color:var(--reference-tone);font-size:var(--text-micro);font-weight:var(--weight-semibold)}
  .document-workspace-reference-label{padding:var(--sp-1);font-size:var(--text-micro);font-weight:var(--weight-medium)}
  .document-workspace-reference-lock { position:absolute;inset-block-start:var(--sp-1);inset-inline-end:var(--sp-1);display:grid;place-items:center;inline-size:var(--control-h-sm);block-size:var(--control-h-sm);padding:0;border:1px solid color-mix(in oklab,var(--card) 65%,transparent);border-radius:var(--radius-full);background:color-mix(in oklab,var(--foreground) 72%,transparent);color:var(--background);cursor:pointer }
  /* Pressed = the ACCENT pair, not --brand. Two reasons, and the second is the
     hard one: (1) v3's component language says selected/active speaks the
     accent and --brand* is decorative (see .card-active / .chip.is-selected);
     (2) --brand-gradient-fg on --brand is 4.00:1 in light BUT 2.93:1 in dark —
     under the 3:1 floor WCAG 1.4.11 requires even for a bare icon, so the
     pressed lock glyph is failing today, not hypothetically. The accent pair is
     already contract-bound to ≥4.5:1 in every skin × theme by
     tests/v3-contrast.test.mjs. */
  .document-workspace-reference-lock[aria-pressed="true"]{background:var(--ui-accent);color:var(--ui-accent-fg)}
  .document-workspace-reference-play { position:absolute;inset-block-start:calc(var(--sp-4) - var(--sp-1));inset-inline-start:50%;display:grid;place-items:center;transform:translateX(-50%);color:var(--brand-gradient-fg);filter:drop-shadow(0 1px 2px color-mix(in oklab,var(--foreground) 70%,transparent));pointer-events:none }
  .document-workspace-result-peek { overflow:hidden;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--surface-sunken) }
  .document-workspace-result-card { flex:0 1 22rem;min-block-size:12rem;overflow:auto;padding:var(--sp-2) var(--sp-3);border-block-end:1px solid var(--border);background:var(--surface-sunken);overscroll-behavior:contain }
  .document-workspace-tool-panel { flex:0 1 45%;min-block-size:10rem;display:flex;flex-direction:column;overflow:hidden;border-block-end:1px solid var(--border);background:var(--surface-sunken) }
  .document-workspace-tool-head { flex:none;padding:var(--sp-2) var(--sp-3);border-block-end:1px solid var(--border);background:var(--card) }
  .document-workspace-tool-scroll { flex:1 1 auto;min-block-size:0;overflow:auto }
  .document-workspace-thread { flex:1 1 auto;min-block-size:0;display:flex;flex-direction:column;overflow:hidden;background:var(--card) }
  .document-workspace-thread-scroll { flex:1 1 auto;min-block-size:0;overflow:auto;overscroll-behavior:contain;scrollbar-width:thin }
  .document-workspace-composer { flex: none; padding: var(--sp-3); border-block-start: 1px solid color-mix(in oklab,var(--brand) 24%,var(--border)); background: var(--card); box-shadow:var(--elevation-2); }
  .document-workspace-composer-input { resize:none;background:var(--background);border-color:color-mix(in oklab,var(--brand) 24%,var(--border)) }
  .document-workspace-composer-input:focus { border-color:var(--ui-accent);outline:var(--focus-outline-brand);outline-offset:var(--focus-outline-offset) }
  /* Send / preview = the SOLID ACCENT CTA, not the decorative --brand-gradient.
     The gradient is amber→teal→blue: its label contrast varies per pixel and
     bottoms out at 3.62:1 light / 2.48:1 dark against --brand-gradient-fg, so
     an 11px label on it can never be AA. tokens.css already draws the line —
     --brand* is hero art, --ui-accent is the action color — and both consumers
     already carry .btn-primary, so this now matches every other primary CTA. */
  .document-workspace-send,.document-workspace-preview-action { border-color:transparent;background:var(--btn-solid-bg);color:var(--btn-solid-fg);box-shadow:var(--elevation-1) }
  .document-workspace-send:hover:not(:disabled),.document-workspace-preview-action:hover:not(:disabled){background:var(--btn-solid-bg-hover)}
  .document-workspace-outline-item { --outline-tone:var(--info);border-inline-start:var(--sp-0_5) solid transparent }
  .document-workspace-outline-item[data-tone="success"]{--outline-tone:var(--success)}
  .document-workspace-outline-item[data-tone="warning"]{--outline-tone:var(--warning)}
  .document-workspace-outline-item[data-tone="accent"]{--outline-tone:var(--brand)}
  .document-workspace-outline-item:hover{border-inline-start-color:color-mix(in oklab,var(--outline-tone) 48%,transparent);background:color-mix(in oklab,var(--outline-tone) 7%,var(--surface-sunken))}
  .document-workspace-outline-item.is-active{border-inline-start-color:var(--outline-tone);background:color-mix(in oklab,var(--outline-tone) 13%,var(--surface-sunken));color:var(--foreground)}
  .document-workspace-outline-toggle,.document-workspace-agent-dock { display: none; }
  .m-3{margin:var(--sp-3)} .m-4{margin:var(--sp-4)} .overflow-x-auto{overflow-x:auto} .overflow-y-auto{overflow-y:auto}
  /* 表单控件收回内容宽(.input 默认 100%);网格行占满整行。都是消费方已在用
     却不存在的意图类(幽灵类审计 2026-08-06)。 */
  .w-auto{inline-size:auto} .col-span-full{grid-column:1/-1}
  .w-full { width: 100%; } .h-full { height: 100%; } .min-w-0 { min-width: 0; } .min-h-0 { min-height: 0; }
  .max-w-prose { max-width: 65ch; } .max-w-field { max-width: 24rem; } .max-w-full { max-width: 100%; } .max-inline-none { max-inline-size: none; }
  .relative { position: relative; } .sticky { position: sticky; }
  /* 长对话框 / 长表单里,主动作不许跟着正文滚出视口:这条把动作行钉在滚动容器的底边。
     用在动作行本身;它的滚动祖先负责滚动(dev-dialog 的正文区就是)。
     实测过的病:整集交付的「批准 / 退回」落在 982-1038px,视口只有 1000px,按钮被切在边缘外。 */
  .action-bar-sticky { position: sticky; inset-block-end: 0; z-index: var(--z-raised-2); padding-block: var(--sp-2); background: var(--card); border-block-start: 1px solid var(--border); }
  .overflow-auto { overflow: auto; } .overflow-hidden { overflow: hidden; }
  .border-0 { border: 0; }
  .text-pre-wrap { white-space: pre-wrap; overflow-wrap: anywhere; }
  .page-pad { padding: var(--sp-5) var(--sp-7) var(--sp-8); }
  .fieldset-reset { min-inline-size: 0; margin: 0; padding: 0; border: 0; }
  /* 长串指纹 / URL 要能断行,否则撑破卡片 */
  .text-break { overflow-wrap: anywhere; word-break: break-word; }
  /* 品牌色文字(强调某个值,而不是整段) */
  .text-brand { color: var(--ui-accent-text, var(--primary)); }
  .text-start { text-align: start; } .text-center { text-align: center; } .text-end { text-align: end; }
  /* padding (logical: px=inline, py=block, ps/pe=inline start/end) */
  .p-0{padding:0} .p-1{padding:var(--sp-1)} .p-2{padding:var(--sp-2)} .p-3{padding:var(--sp-3)} .p-4{padding:var(--sp-4)} .p-6{padding:var(--sp-6)} .p-8{padding:var(--sp-8)}
  .px-0{padding-inline:0} .px-1{padding-inline:var(--sp-1)} .px-2{padding-inline:var(--sp-2)} .px-3{padding-inline:var(--sp-3)} .px-4{padding-inline:var(--sp-4)} .px-6{padding-inline:var(--sp-6)} .px-8{padding-inline:var(--sp-8)}
  .py-0{padding-block:0} .py-1{padding-block:var(--sp-1)} .py-2{padding-block:var(--sp-2)} .py-3{padding-block:var(--sp-3)} .py-4{padding-block:var(--sp-4)} .py-6{padding-block:var(--sp-6)} .py-8{padding-block:var(--sp-8)}
  /* Edge-specific padding is intentionally reset-only. Keep the public surface
     small; non-zero edge spacing should use a semantic role or a nested stack. */
  .pt-0{padding-block-start:0} .pt-1{padding-block-start:var(--sp-1)} .pt-2{padding-block-start:var(--sp-2)} .pt-3{padding-block-start:var(--sp-3)} .pt-4{padding-block-start:var(--sp-4)} .pt-6{padding-block-start:var(--sp-6)} .pt-8{padding-block-start:var(--sp-8)}
  .pb-0{padding-block-end:0} .pb-1{padding-block-end:var(--sp-1)} .pb-2{padding-block-end:var(--sp-2)} .pb-3{padding-block-end:var(--sp-3)} .pb-4{padding-block-end:var(--sp-4)} .pb-6{padding-block-end:var(--sp-6)} .pb-8{padding-block-end:var(--sp-8)}
  /* margin */
  .m-0{margin:0} .mx-auto{margin-inline:auto}
  .mt-0{margin-block-start:0} .mt-1{margin-block-start:var(--sp-1)} .mt-2{margin-block-start:var(--sp-2)} .mt-3{margin-block-start:var(--sp-3)} .mt-4{margin-block-start:var(--sp-4)} .mt-6{margin-block-start:var(--sp-6)} .mt-8{margin-block-start:var(--sp-8)} .mt-auto{margin-block-start:auto} .ms-auto{margin-inline-start:auto}
  .mb-0{margin-block-end:0} .mb-1{margin-block-end:var(--sp-1)} .mb-2{margin-block-end:var(--sp-2)} .mb-3{margin-block-end:var(--sp-3)} .mb-4{margin-block-end:var(--sp-4)} .mb-6{margin-block-end:var(--sp-6)} .mb-8{margin-block-end:var(--sp-8)}
  /* gap */
  .gap-0{gap:0} .gap-1{gap:var(--sp-1)} .gap-2{gap:var(--sp-2)} .gap-3{gap:var(--sp-3)} .gap-4{gap:var(--sp-4)} .gap-6{gap:var(--sp-6)} .gap-8{gap:var(--sp-8)}
  .gap-x-2{column-gap:var(--sp-2)} .gap-x-4{column-gap:var(--sp-4)} .gap-y-2{row-gap:var(--sp-2)} .gap-y-4{row-gap:var(--sp-4)}
  .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .line-clamp-2 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
  .text-left { text-align: left; } .text-center { text-align: center; } .text-right { text-align: right; }
  .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
  .sr-only-focusable:focus, .sr-only-focusable:focus-within { position: static; width: auto; height: auto; margin: 0; overflow: visible; clip: auto; white-space: normal; }
  .mono { font-family: var(--font-mono); }
  /* 数字位:等宽 + 表格数字。金额/延迟/计数逐行对齐,否则一列数字在视觉上
     锯齿状,读的人得逐个数位数。 */
  .num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
  .dim { color: var(--muted-foreground); }
  /* Type-role utilities — bind text to the role scale (so channels never write
     a raw px size). SELF-CONTAINED: each role carries its canonical font-weight
     too, so it renders identically on ANY element (span / div / h*) and never
     depends on heading defaults — which a host CSS reset (e.g. Tailwind
     preflight) would otherwise strip. Heading-rank roles default to semibold;
     text roles to normal. Override weight only when truly needed via an inline
     style — a "title that isn't bold" usually means the wrong role. */
  .text-display { font-size: var(--text-display); font-weight: var(--weight-semibold); line-height: var(--leading-heading); letter-spacing: var(--tracking-tight); }
  .text-heading { font-size: var(--text-heading); font-weight: var(--weight-semibold); line-height: var(--leading-heading); }
  .text-title { font-size: var(--text-title); font-weight: var(--weight-semibold); line-height: var(--leading-heading); }
  .text-body { font-size: var(--text-body); font-weight: var(--weight-normal); line-height: var(--leading-body); }
  .text-caption { font-size: var(--text-caption); font-weight: var(--weight-normal); line-height: var(--leading-body); }
  .text-micro { font-size: var(--text-micro); font-weight: var(--weight-normal); line-height: var(--leading-body); }
  /* Marketing poster scale — fluid hero/section type beyond --text-display (22). */
  .text-poster-sm { font-size: var(--text-poster-sm); font-weight: var(--weight-bold); line-height: 1.1; letter-spacing: var(--tracking-tight); }
  .text-poster-md { font-size: var(--text-poster-md); font-weight: var(--weight-bold); line-height: 1.05; letter-spacing: var(--tracking-tight); }
  .text-poster-lg { font-size: var(--text-poster-lg); font-weight: var(--weight-bold); line-height: 1.02; letter-spacing: var(--tracking-tight); }
  .brand-gradient-text { background-image: var(--brand-gradient); -webkit-background-clip: text; background-clip: text; color: transparent; }
  .animate-spin { animation: lib-spin 0.9s linear infinite; }
  /* Slow "living gradient" sweep — pair with .brand-gradient-text on a headline. */
  .animate-shine { background-size: 200% auto; animation: lib-shine 5.5s ease-in-out infinite; }
  /* Keep page transitions opacity-only. A transformed page becomes the
     containing block for fixed-position dropdowns/dialogs, which offsets
     floating panels by the app sidebar and can push them off-screen. */
  .page-enter { animation: lib-fade-in 0.24s var(--ease-out); }
  /* Element-level entrance (chat bubbles, media cards). Element scope only —
     never on a page/panel container (transform would trap fixed children,
     see .page-enter note above). `both` keeps the pre-animation frame hidden. */
  .fade-up { animation: lib-fade-up var(--duration-slow) var(--ease-out) both; }
  .stagger-1 { animation-delay: 0.05s; } .stagger-2 { animation-delay: 0.1s; }
  .stagger-3 { animation-delay: 0.15s; } .stagger-4 { animation-delay: 0.2s; }
  /* Responsive overrides must stay in the same cascade layer as the desktop
     utility. A lower-priority component layer cannot override utilities even
     when its media query matches. */
  @media (max-width: 48rem) {
    .grid-rail, .grid-rail-end { grid-template-columns: minmax(0, 1fr); }
    /* Settings and other dense section navigation can opt into a compact,
       horizontally scrollable rail. Keeping the rail in normal flow avoids
       a full-height sidebar overlapping the stacked content on phones. */
    .grid-rail.grid-rail-nav > .sidebar {
      flex-direction: row;
      block-size: calc(var(--control-h) + var(--sp-4));
      min-block-size: calc(var(--control-h) + var(--sp-4));
      inline-size: 100%;
      max-inline-size: 100%;
      overflow-x: auto;
      overflow-y: hidden;
      padding-block: var(--sp-2);
      border-inline-end: 0;
      border-block-end: 1px solid var(--border);
      overscroll-behavior-inline: contain;
      scroll-snap-type: x proximity;
      scroll-padding-inline: var(--sp-2);
      -webkit-overflow-scrolling: touch;
    }
    .grid-rail.grid-rail-nav > .sidebar > .sidebar-label { display: none; }
    .grid-rail.grid-rail-nav > .sidebar > .sidebar-item {
      flex: 0 0 auto;
      inline-size: auto;
      white-space: nowrap;
      scroll-snap-align: start;
      scroll-margin-inline: var(--sp-2);
    }
    .grid-rail.grid-rail-nav > .sidebar > .sidebar-item.is-active,
    .grid-rail.grid-rail-nav > .sidebar > .sidebar-item[aria-current="page"] { scroll-snap-align: center; }
    .grid-rail.grid-rail-nav > .sidebar > .sidebar-item::before {
      inset-inline: var(--sp-2);
      inset-block-start: auto;
      inset-block-end: 0;
      inline-size: auto;
      block-size: 2px;
      border-radius: var(--radius-full);
    }
    .workspace-split { grid-template-columns: minmax(0, 1fr); block-size: auto; min-block-size: 0; }
    .workspace-split > * { min-block-size: 30rem; }
    .workspace-split.workspace-split-end { grid-template-columns: minmax(0, 1fr); block-size: auto; min-block-size: 0; }
    .workspace-split.workspace-split-end > * { min-block-size: 30rem; block-size: auto; }
    .workspace-split.workspace-split-end > :last-child:not(.workspace-context-drawer) { position: static; }
    .workspace-split.workspace-split-fill { flex: 0 0 auto; }
    .workspace-split.workspace-split-end > dev-drawer.workspace-context-drawer[data-context-modal] {
      position: fixed; inset: 0; z-index: var(--z-modal); display: none;
      min-block-size: 0; block-size: auto;
    }
    .workspace-split.workspace-split-end > dev-drawer.workspace-context-drawer[data-context-modal][open] { display: block; }
    /* These controls exist only in the phone drawer flow, so their touch
       target must not depend on pointer-capability detection. Width emulation,
       hybrid devices and mobile browsers can all report a fine pointer. */
    .workspace-context-toggle, .workspace-context-close {
      display: inline-flex;
      min-block-size: var(--sp-11);
    }
    .page-pad { padding-inline: var(--sp-3); }
    .workflow-steps { grid-template-columns: minmax(0, 1fr); }
  }
  @container (max-width: 56rem) {
    .workspace-split.workspace-split-fill { flex: 0 0 auto; }
    .workspace-split.workspace-split-end { grid-template-columns: minmax(0, 1fr); block-size: auto; min-block-size: 0; }
    .workspace-split.workspace-split-end > * { min-block-size: 30rem; block-size: auto; }
    .workspace-split.workspace-split-end > :last-child:not(.workspace-context-drawer) { position: static; }
    .workspace-split.workspace-split-end > dev-drawer.workspace-context-drawer[data-context-modal] {
      position: fixed; inset: 0; z-index: var(--z-modal); display: none;
      min-block-size: 0; block-size: auto;
    }
    .workspace-split.workspace-split-end > dev-drawer.workspace-context-drawer[data-context-modal][open] { display: block; }
    .workspace-context-toggle, .workspace-context-close {
      display: inline-flex;
      min-block-size: var(--sp-11);
    }
    .document-workspace-shell { block-size: calc(100dvh - 4rem); min-block-size: 0; }
    .document-workspace-topbar { padding-inline: var(--sp-2); }
    .document-workspace-body { display: block; overflow: hidden; }
    .document-workspace-main { block-size: 100%; padding-block-end: calc(var(--sp-11) + var(--sp-4)); }
    .document-workspace-body > dev-drawer.workspace-context-drawer[data-context-modal] {
      position: fixed; inset: 0; z-index: var(--z-modal); display: none;
      min-block-size: 0; block-size: auto;
    }
    .document-workspace-body > dev-drawer.workspace-context-drawer[data-context-modal][open] { display: block; }
    .document-workspace-agent-dock {
      position: fixed; z-index: var(--z-sticky); inset-inline: var(--sp-3); inset-block-end: var(--sp-3);
      display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
      min-block-size: var(--sp-11); padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border);
      border-radius: var(--radius-full); background: var(--card); color: var(--card-foreground,var(--foreground));
      box-shadow: var(--elevation-3);
    }
  }

  @media (max-width: 80rem) {
    .document-workspace-body { grid-template-columns: minmax(38rem,1fr) minmax(22rem,24rem); }
    .document-workspace-outline { display: none; }
    .document-workspace-outline-toggle { display: inline-flex; }
  }

  @media (max-width: 56rem) {
    .document-workspace-shell { block-size: calc(100dvh - 4rem); min-block-size: 0; }
    .document-workspace-topbar { padding-inline: var(--sp-2); }
    .document-workspace-body { display: block; overflow: hidden; }
    .document-workspace-main { block-size: 100%; padding-block-end: calc(var(--sp-11) + var(--sp-4)); }
    .document-workspace-agent-dock { position: fixed; z-index: var(--z-sticky); inset-inline: var(--sp-3); inset-block-end: var(--sp-3); display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); min-block-size: var(--sp-11); padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius-full); background: var(--card); color: var(--card-foreground,var(--foreground)); box-shadow: var(--elevation-3); }
  }

  @media (max-width: 30rem) {
    /* Phone: ONE row. Wrapping used to be the escape hatch for "more controls
       than a 390px row holds", but two stacked rows cost 100px of a 844px screen
       on a surface whose whole point is the area BELOW the toolbar (a canvas /
       a document). The controls that don't fit belong in an overflow menu — the
       consumer swaps its wide action row (`.md-only`) for a `<dev-menu>`
       (`.md-hidden`) at the same container breakpoint. Keep the row nowrap so a
       consumer that forgets sees its overflow immediately instead of paying a
       silent second row on every phone. */
    .document-workspace-topbar { flex-wrap: nowrap; gap: var(--sp-1); }
    .document-workspace-subtitle { display: none; }
  }
}

/* Keyframes (outside @layer — not subject to the cascade). */
@keyframes lib-spin { to { transform: rotate(360deg); } }
@keyframes lib-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes lib-scale-in { from { opacity: 0; transform: scale(0.97); } to { opacity: 1; transform: scale(1); } }
@keyframes lib-fade-up { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@keyframes lib-shimmer { to { background-position: -200% 0; } }
@keyframes lib-indeterminate { 0% { margin-left: -40%; } 100% { margin-left: 100%; } }
@keyframes lib-marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@keyframes lib-shine { 0% { background-position: 0% center; } 50% { background-position: 120% center; } 100% { background-position: 0% center; } }
@media (prefers-reduced-motion: reduce) { .animate-shine { animation: none; } }

/* ---- Forced-colors / Windows High Contrast Mode. box-shadow (our focus ring)
   is stripped here, so restore real borders + a system-colored focus outline,
   and keep functional fills painted. Top-level (not layered) so it wins. ---- */
@media (forced-colors: active) {
  .btn, .input, .textarea, select.input, .tag, .card, .alert, .modal-card,
  .kbd, .code, .avatar { border: 1px solid ButtonText; }
  .btn-ghost { border-color: transparent; }
  :focus-visible { outline: 2px solid Highlight; outline-offset: 2px; }
  .progress-bar, .skeleton, .spinner { forced-color-adjust: none; }
}

/* ---- Scrim robustness: opaque fallback where backdrop-filter is unsupported,
   and respect prefers-reduced-transparency. ---- */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .overlay { background: rgba(20, 20, 22, 0.62); }
}
@media (prefers-reduced-transparency: reduce) {
  .overlay { background: var(--background); -webkit-backdrop-filter: none; backdrop-filter: none; }
}

/* ===== vP5 — structural components (pure CSS; same @layer merges) ===== */
@layer lib.components {
  /* Tabs (visual; consumer wires roving-tabindex / panels) */
  .tabs { display: flex; gap: var(--sp-1); border-block-end: 1px solid var(--border); }
  .tabs.tabs-scroll { overflow-x: auto; scrollbar-width: thin; overscroll-behavior-inline: contain; scroll-snap-type: x proximity; scroll-padding-inline: var(--sp-2); -webkit-overflow-scrolling: touch; }
  .tabs.tabs-scroll > .tab { flex: 0 0 auto; scroll-snap-align: start; scroll-margin-inline: var(--sp-2); }
  .tab { appearance: none; background: none; border: none; cursor: pointer; padding: var(--sp-2) var(--sp-3); font: inherit; font-size: var(--text-caption); color: var(--muted-foreground); border-block-end: 2px solid transparent; margin-block-end: -1px; }
  .tabs.tabs-compact > .tab { padding-inline: var(--sp-2); }
  .tab:hover { color: var(--foreground); background: var(--state-hover); border-start-start-radius: var(--radius-sm); border-start-end-radius: var(--radius-sm); }
  .tab[aria-selected="true"], .tab.is-active { color: var(--foreground); border-block-end-color: var(--ui-accent); font-weight: var(--weight-semibold); }
  .tabs.tabs-scroll > .tab[aria-selected="true"], .tabs.tabs-scroll > .tab.is-active { scroll-snap-align: center; }
  .tab:focus-visible { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); border-radius: var(--radius-sm); }

  /* Table */
  .table { width: 100%; border-collapse: collapse; font-size: var(--text-caption); font-variant-numeric: tabular-nums; }
  .table th, .table td { padding: var(--sp-2) var(--sp-3); text-align: start; border-block-end: 1px solid var(--border); }
  .table thead th { font-size: var(--text-micro); font-weight: var(--weight-semibold); text-transform: uppercase; letter-spacing: var(--tracking-wide); white-space: nowrap; color: var(--muted-foreground); background: var(--surface-sunken); border-block-end-color: var(--border-strong); }
  .table tbody tr:hover { background: var(--state-hover); }
  .table-zebra tbody tr:nth-child(even) { background: color-mix(in oklab, var(--foreground) 3%, transparent); }
  .table tr[aria-selected="true"], .table tr.is-selected { background: var(--state-selected); }
  .table-sticky thead th { position: sticky; inset-block-start: 0; z-index: var(--z-raised); }
  /* Dense operational tables with many columns should preserve readable cells
     and let .table-wrap provide horizontal scrolling instead of crushing every
     column into a few characters. */
  .table-wide { min-inline-size: 72rem; }
  /* Wrap a wide .table in this so it scrolls horizontally on a narrow screen
     instead of overflowing the viewport (clipping the 操作/copy buttons on the
     right). The wrapper carries the scroll; the table keeps its desktop layout. */
  .table-wrap { inline-size: 100%; max-inline-size: 100%; overflow-x: auto; scrollbar-width: thin; -webkit-overflow-scrolling: touch; overscroll-behavior-inline: contain; }

  /* Input group + addons */
  .input-group { display: flex; align-items: stretch; }
  .input-group > .input, .input-group > .input-addon { border-radius: 0; }
  .input-group > :first-child { border-start-start-radius: var(--radius); border-end-start-radius: var(--radius); }
  .input-group > :last-child { border-start-end-radius: var(--radius); border-end-end-radius: var(--radius); }
  .input-group > :not(:first-child) { margin-inline-start: -1px; }
  .input-addon { display: inline-flex; align-items: center; padding: 0 var(--sp-3); white-space: nowrap; background: var(--secondary); border: 1px solid var(--border); color: var(--muted-foreground); font-size: var(--text-caption); }

  /* Button group (segmented) */
  .button-group { display: inline-flex; }
  .button-group > .btn { border-radius: 0; }
  .button-group > .btn:first-child { border-start-start-radius: var(--radius); border-end-start-radius: var(--radius); }
  .button-group > .btn:last-child { border-start-end-radius: var(--radius); border-end-end-radius: var(--radius); }
  .button-group > .btn:not(:first-child) { margin-inline-start: -1px; }

  /* Breadcrumb */
  .breadcrumb { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); margin: 0; padding: 0; list-style: none; font-size: var(--text-caption); color: var(--muted-foreground); }
  .breadcrumb li { display: flex; align-items: center; gap: var(--sp-2); }
  .breadcrumb li:not(:last-child)::after { content: "/"; color: var(--border-strong); }
  .breadcrumb a { color: var(--muted-foreground); text-decoration: none; }
  .breadcrumb a:hover { color: var(--foreground); }
  .breadcrumb [aria-current="page"] { color: var(--foreground); font-weight: var(--weight-medium); }

  /* Pagination */
  .pagination { display: flex; align-items: center; gap: var(--sp-1); margin: 0; padding: 0; list-style: none; }
  .pagination .page { display: inline-flex; align-items: center; justify-content: center; min-width: var(--control-h); height: var(--control-h); padding: 0 var(--sp-2); border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface-raised); color: var(--foreground); font-size: var(--text-caption); text-decoration: none; cursor: pointer; }
  .pagination .page:hover { border-color: var(--border-strong); background: color-mix(in oklab, var(--foreground) 5%, var(--surface-raised)); }
  .pagination .page:active { background: color-mix(in oklab, var(--foreground) 9%, var(--surface-raised)); }
  .pagination .page[aria-current="page"] { background: var(--primary); color: var(--primary-foreground); border-color: transparent; }

  /* List */
  .list { display: flex; flex-direction: column; overflow: hidden; background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-lg); }
  .list-header { padding: var(--sp-2) var(--sp-3); font-size: var(--text-micro); font-weight: var(--weight-semibold); text-transform: uppercase; letter-spacing: var(--tracking-wide); color: var(--muted-foreground); background: var(--surface-sunken); border-block-end: 1px solid var(--border); }
  /* A row inside .list. USE `.list-row` — `.list-item` is kept only for the
     existing non-Tailwind consumers.
     Why two names: `list-item` is ALSO a Tailwind display utility
     (`display: list-item`). A Tailwind consumer that merely writes lib's class
     name in JSX makes Tailwind emit `.list-item{display:list-item}` into its
     utilities layer — which sits above lib's — so the row silently stops being
     a flex box: `justify-between` goes inert and label/value collapse into one
     run of text. Observed live on chat.develop.cc 2026-07-30. */
  .list-row, .list-item { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3); border-block-end: 1px solid var(--border); }
  .list-row:last-child, .list-item:last-child { border-block-end: none; }
  .list-row:hover, .list-item:hover { background: var(--state-hover); }
  .list-row.list-row-bordered, .list-item.list-item-bordered { border: 1px solid var(--border); border-radius: var(--radius); }
  /* Read-only list — rows that are DATA (a spec sheet's label/value pairs), not
     choices. Kills the row hover: a highlight under the cursor promises a click
     that never happens. Opt-in on the container so every existing interactive
     list keeps its hover untouched. */
  .list.list-static > :is(.list-row, .list-item):hover { background: transparent; }
  /* Single-line title cell for any row (list row, sidebar row, picker option).
     Titles here are DATA (a chat session name, a file name) — they arrive at
     any length, and wrapping them turns a 50-row rail into ragged, unscannable
     blocks. min-inline-size:0 is the load-bearing part: without it the flex
     item refuses to shrink below its text and the ellipsis never appears. */
  .row-title { flex: 1; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

  /* Banner (page-level vs inline .alert) */
  .banner { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-4); font-size: var(--text-caption); color: var(--foreground); background: var(--surface-sunken); border-block-end: 1px solid var(--border); }
  .banner-success { background: var(--success-bg); }
  .banner-warning { background: var(--warning-bg); }
  .banner-danger { background: var(--danger-bg); }
  .banner-info { background: var(--info-bg); }

  /* Accordion (native <details>) */
  .accordion { overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-lg); }
  .accordion > details { border-block-end: 1px solid var(--border); }
  .accordion > details:last-child { border-block-end: none; }
  /* Header rule is scoped to the accordion's OWN summary (both markup shapes:
     `<details class="accordion">` and `<div class="accordion"><details>`). A
     descendant selector leaked this 12px flex header — and its far-right caret —
     onto every inline <details> that happened to live inside an accordion body
     (a "原始错误详情" disclosure inside a card rendered 249×41 with its marker
     171px away from its own label). */
  .accordion > summary,
  .accordion > details > summary { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3); font-weight: var(--weight-medium); cursor: pointer; list-style: none; }
  .accordion > summary:hover,
  .accordion > details > summary:hover { background: var(--state-hover); }
  .accordion > summary:focus-visible,
  .accordion > details > summary:focus-visible { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); border-radius: var(--radius-sm); }
  .accordion > summary::-webkit-details-marker,
  .accordion > details > summary::-webkit-details-marker { display: none; }
  .accordion > summary::after,
  .accordion > details > summary::after { content: "▸"; color: var(--muted-foreground); transition: transform var(--duration-fast) var(--ease-smooth); }
  /* Open state must rotate for BOTH shapes; keying only on a descendant
     `details[open]` left `<details class="accordion" open>` showing a collapsed
     caret while its body was expanded. */
  .accordion[open] > summary::after,
  .accordion > details[open] > summary::after { transform: rotate(90deg); }
  .accordion-body { padding: 0 var(--sp-3) var(--sp-3); font-size: var(--text-caption); color: var(--muted-foreground); }

  /* Single advanced disclosure. Channels often need one optional filter or
     expert-settings block without constructing a full accordion wrapper. */
  .advanced-filter { overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--surface-raised); }
  .advanced-filter > summary { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); padding: var(--sp-3); font-weight: var(--weight-medium); cursor: pointer; list-style: none; }
  .advanced-filter > summary:hover { background: var(--state-hover); }
  .advanced-filter > summary:focus-visible { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); border-radius: var(--radius-sm); }
  .advanced-filter > summary::-webkit-details-marker { display: none; }
  .advanced-filter > summary::after { content: "▸"; color: var(--muted-foreground); transition: transform var(--duration-fast) var(--ease-smooth); }
  .advanced-filter[open] > summary { border-block-end: 1px solid var(--border); }
  .advanced-filter[open] > summary::after { transform: rotate(90deg); }
}

/* ===== vP6 — component breadth: batch 1 (separator / label / skeleton-text /
   stat-card / segmented). Pure CSS, same @layer merges. ===== */
@layer lib.components {
  /* Separator (explicit h/v line; .divider stays the margin'd <hr>) */
  .separator { flex: none; border: none; background: var(--border); }
  .separator:not(.separator-vertical) { block-size: 1px; inline-size: 100%; }
  .separator-vertical { inline-size: 1px; align-self: stretch; min-block-size: 1em; }
  /* Dashed variant — soft boundary for collapsible detail regions (e.g. a
     composer's advanced-params fold). Draws with a dashed border, not bg. */
  .separator-dashed:not(.separator-vertical) { block-size: 0; background: transparent; border-block-start: 1px dashed var(--border); }
  .separator-dashed.separator-vertical { inline-size: 0; background: transparent; border-inline-start: 1px dashed var(--border); }

  /* Fit-content input/select — for bare inline controls in a toolbar row where
     the default width:100% would blow the flex line. select.input-fit matches
     select.input's specificity so the width:100% base loses. */
  .input-fit, select.input-fit { width: auto; min-width: 0; }

  /* Standalone label (complements grid-context .form-label) */
  .label { display: inline-flex; align-items: center; gap: var(--sp-1); font-size: var(--text-caption); font-weight: var(--weight-medium); color: var(--foreground); }
  .label .required { color: var(--danger-text); }
  .label-muted { color: var(--muted-foreground); font-weight: var(--weight-normal); }

  /* Skeleton text line (paragraph placeholders; last line is shorter) */
  .skeleton-text { display: block; block-size: 0.7em; margin-block: 0.35em; border-radius: var(--radius-sm); background: linear-gradient(90deg, var(--muted) 25%, var(--surface-sunken) 50%, var(--muted) 75%); background-size: 200% 100%; animation: lib-shimmer 1.3s linear infinite; }
  .skeleton-text:last-child { inline-size: 70%; }

  /* NOTE: .stat-card intentionally NOT defined here — the clipboard channel
     already uses that class name for its own boxes. It will return once that
     channel adopts lib's version (see docs/lib-design-system-progress.md). */

  /* Segmented control (visual radio / mode switch). Works with aria-pressed
     buttons OR <label><input type=radio>…</label> via :has().
     段也可以是 <a>:同页换视图用 button,跨页去另一个视图必须是真链接 ——
     否则中键 / ⌘点击 / 右键复制链接全没了。当前段用 aria-current 表达。 */
  .segmented { display: inline-flex; gap: 2px; padding: 2px; background: var(--surface-sunken); border: 1px solid var(--border); border-radius: var(--radius); }
  .segmented > button, .segmented > label, .segmented > a { appearance: none; text-decoration: none; border: none; background: none; cursor: pointer; font: inherit; font-size: var(--text-caption); color: var(--muted-foreground); padding: 0 var(--sp-3); min-block-size: calc(var(--control-h) - 6px); display: inline-flex; align-items: center; justify-content: center; border-radius: calc(var(--radius) - 2px); white-space: nowrap; transition: background-color 120ms var(--ease-smooth), color 120ms var(--ease-smooth); }
  .segmented > button:hover, .segmented > label:hover, .segmented > a:hover { color: var(--foreground); }
  /* v3: the active segment carries the accent tonally (amber wash + ink). */
  .segmented > button[aria-pressed="true"], .segmented > [aria-selected="true"], .segmented > [aria-current], .segmented > .is-active, .segmented > label:has(input:checked) { background: var(--btn-tonal-selected-bg); color: var(--btn-tonal-fg); box-shadow: var(--shadow-card); font-weight: var(--weight-medium); }
  .segmented > label > input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
  .segmented > button:focus-visible, .segmented > a:focus-visible, .segmented > label:focus-within { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); }
  .segmented > button:disabled, .segmented > label:has(input:disabled) { opacity: var(--state-disabled-opacity); cursor: not-allowed; }

  /* Menu / dropdown surface — styles the slotted content of <dev-menu>, and
     stands alone for pure-CSS menus. */
  .menu { display: flex; flex-direction: column; min-inline-size: 11rem; max-block-size: var(--c-popover-max-h); overflow: auto; overscroll-behavior: contain; padding: var(--sp-1); background: var(--popover); color: var(--popover-foreground); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--elevation-3); }
  .menu-item { display: flex; align-items: center; gap: var(--sp-2); inline-size: 100%; box-sizing: border-box; padding: var(--sp-1) var(--sp-2); border: none; background: none; border-radius: var(--radius-sm); font: inherit; font-size: var(--text-caption); color: var(--foreground); text-align: start; cursor: pointer; }
  .menu-item:hover { background: var(--state-hover); }
  .menu-item:focus-visible { background: var(--state-hover); outline: var(--focus-outline); outline-offset: calc(var(--focus-outline-offset) * -1); }
  .menu-item.is-active { background: var(--state-selected); box-shadow: inset 2px 0 0 var(--ui-accent); }
  .menu-item:disabled, .menu-item[aria-disabled="true"] { opacity: var(--state-disabled-opacity); pointer-events: none; }
  /* Hover paints --danger-bg under this label, so the ink step is mandatory
     here too — the fill tone on its own tint is 3.74:1. */
  .menu-item-danger { color: var(--danger-text); }
  .menu-item-danger:hover, .menu-item-danger:focus-visible { background: var(--danger-bg); }
  .menu-item .menu-shortcut { margin-inline-start: auto; padding-inline-start: var(--sp-4); color: var(--muted-foreground); font-size: var(--text-micro); }
  .menu-sep { block-size: 1px; background: var(--border); margin: var(--sp-1) calc(-1 * var(--sp-1)); }
  .menu-label { padding: var(--sp-1) var(--sp-2); font-size: var(--text-micro); font-weight: var(--weight-semibold); text-transform: uppercase; letter-spacing: var(--tracking-wide); color: var(--muted-foreground); }
  /* Anchored popover variant: pair with .menu inside a position:relative anchor
     (e.g. a composer's @-mention layer). Fixed compact width, capped height,
     floats above the anchor; interior scrolling belongs to a .scroll-area child. */
  .menu-float { position: absolute; inset-block-end: 100%; inset-inline-start: 0; z-index: var(--z-dropdown); inline-size: min(26.25rem, 100%); max-block-size: 25rem; margin-block-end: var(--sp-2); border-radius: var(--radius-lg); overflow: hidden; }

  /* ---- picker: the platform's ONE "choose a model / preset / variant" panel ----
     Left = the index (families / groups), right = the chosen entry's variants and
     the facts about it. Lives inside a .modal-card or a popover; the channel
     supplies only rows and labels.
     Why a template and not per-channel CSS: AI对话 had a two-pane popover for
     image models and a one-column 560px card list for video models — two looks
     for one job. The index column is fixed-width and the detail pane carries
     min-inline-size:0 so a long model id wraps INSIDE the column instead of
     stretching the whole panel. */
  .picker { display: flex; gap: var(--sp-3); align-items: stretch; }
  .picker-index { flex: none; inline-size: var(--picker-index-w, 10.5rem); display: flex; flex-direction: column; gap: 2px; padding-inline-end: var(--sp-3); border-inline-end: 1px solid var(--border); }
  .picker-detail { flex: 1; min-inline-size: 0; display: flex; flex-direction: column; gap: var(--sp-3); }
  /* An index row. Block (not flex) so a name + its sub-line stack; text-align
     start because a <button>'s UA default is centered. */
  .picker-option { appearance: none; display: block; inline-size: 100%; padding: var(--sp-2); border: none; background: none; font: inherit; color: inherit; border-radius: var(--radius-sm); text-align: start; cursor: pointer; transition: background-color var(--duration-fast) var(--ease-smooth); }
  .picker-option:hover { background: var(--state-hover); }
  /* Both spellings: aria-pressed is the valid one on a <button> (aria-selected
     needs an option/row role), aria-selected fits a listbox/row markup. */
  .picker-option[aria-pressed="true"], .picker-option[aria-selected="true"], .picker-option.is-selected { background: var(--state-selected); }
  .picker-option:focus-visible { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); }
  /* Phone: two panes do not fit. Measured on a 390px viewport — the detail pane
     lands at 153px, the tier .segmented overflows (the last tier is clipped) and
     the panel grows a horizontal scrollbar. Stack instead: index on top with a
     hairline under it, detail below at full width. lib owns this, so no channel
     re-invents a breakpoint for its own picker. */
  @media (max-width: 30rem) {
    .picker { flex-direction: column; }
    .picker-index { inline-size: auto; padding-inline-end: 0; padding-block-end: var(--sp-2); border-inline-end: none; border-block-end: 1px solid var(--border); }
  }

  /* ---- definition: 标签 / 值 的事实清单 ----
     用途:一组「名字 → 值」的陈述,读的人按标签找值。操作手册顶部的接口地址 /
     认证 / 协议 / 耗时,渠道详情的规格,作品的元信息,都是这一个形状。

     为什么进 lib 而不是各频道自己写:同一件事已经出现过三种做法 —— 横排等宽格
     (把「接口地址」压成五分之一屏宽,而它恰恰是来这一页要看的第一件事)、
     两列 table(语义不对,读屏软件当成数据表)、以及每处各写一份 grid-template。
     标签列定宽,值列自适应;窄屏堆成两行,标签在上。

     用法:
       <dl class="definition">
         <dt>接口地址</dt><dd><code class="code">https://…</code></dd>
       </dl>
     `dt`/`dd` 是语义正确的那对标签;也接受 span 子元素(某些框架不便生成 dl)。 */
  .definition { display: grid; grid-template-columns: var(--definition-label-w, 7rem) 1fr; gap: var(--sp-2) var(--sp-4); align-items: baseline; margin: 0; }
  .definition > dt, .definition > .definition-label { font-size: var(--text-micro); color: var(--muted-foreground); margin: 0; }
  .definition > dd, .definition > .definition-value { margin: 0; min-inline-size: 0; display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
  /* 窄屏:两列挤不下一个地址 + 一个复制键。堆成两行,标签在上。 */
  @media (max-width: 40rem) {
    .definition { grid-template-columns: 1fr; gap: var(--sp-1); }
    .definition > dt, .definition > .definition-label { margin-block-start: var(--sp-2); }
  }

  /* ---- batch 3: scroll-area (drawer/lightbox are web components).
     NOTE: .dropzone intentionally NOT defined — xsvg already uses that class
     name; it returns once that channel adopts lib's version. ---- */
  .scroll-area { overflow: auto; max-block-size: var(--scroll-max, 16rem); scrollbar-width: thin; scrollbar-color: var(--border) transparent; overscroll-behavior: contain; }
  .scroll-area::-webkit-scrollbar { width: 8px; height: 8px; }
  .scroll-area::-webkit-scrollbar-track { background: transparent; }
  .scroll-area::-webkit-scrollbar-thumb { background: var(--border); border-radius: var(--radius-full); border: 2px solid transparent; background-clip: content-box; }
  .scroll-area::-webkit-scrollbar-thumb:hover { background: var(--border-strong); background-clip: content-box; }

  /* ---- batch 4: slider / stepper / chip (combobox + collapsible are WCs) ---- */
  .slider { -webkit-appearance: none; appearance: none; inline-size: 100%; max-inline-size: 18rem; block-size: 1.25rem; background: transparent; cursor: pointer; }
  .slider::-webkit-slider-runnable-track { block-size: 4px; border-radius: var(--radius-full); background: var(--muted); }
  .slider::-moz-range-track { block-size: 4px; border-radius: var(--radius-full); background: var(--muted); }
  .slider::-moz-range-progress { block-size: 4px; border-radius: var(--radius-full); background: var(--primary); }
  .slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; inline-size: 14px; block-size: 14px; margin-block-start: -5px; border-radius: 50%; background: var(--background); border: 2px solid var(--primary); box-shadow: var(--shadow-card); transition: box-shadow 120ms var(--ease-smooth); }
  .slider::-moz-range-thumb { inline-size: 14px; block-size: 14px; border: 2px solid var(--primary); border-radius: 50%; background: var(--background); box-shadow: var(--shadow-card); }
  .slider:focus-visible { outline: none; }
  .slider:focus-visible::-webkit-slider-thumb { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); }
  .slider:focus-visible::-moz-range-thumb { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); }
  .slider:disabled { opacity: var(--state-disabled-opacity); cursor: not-allowed; }

  .stepper { display: inline-flex; align-items: stretch; border: 1px solid var(--c-input-border); border-radius: var(--radius); overflow: hidden; background: var(--c-input-bg); }
  .stepper > button { appearance: none; border: none; background: var(--secondary); color: var(--foreground); inline-size: var(--control-h); cursor: pointer; font: inherit; font-size: var(--text-body); display: inline-flex; align-items: center; justify-content: center; }
  .stepper > button:hover { background: var(--state-hover); }
  .stepper > button:disabled { opacity: var(--state-disabled-opacity); cursor: not-allowed; }
  .stepper > input { inline-size: 3rem; border: none; text-align: center; background: transparent; color: inherit; font: inherit; font-size: var(--text-body); -moz-appearance: textfield; appearance: textfield; outline: none; }
  .stepper > input::-webkit-outer-spin-button, .stepper > input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
  .stepper:focus-within { border-color: var(--ring); outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); }

  .chip { display: inline-flex; align-items: center; gap: var(--sp-1); padding-block: 0.1875rem; padding-inline: var(--sp-2); font-size: var(--text-micro); border: 1px solid var(--border); border-radius: var(--radius-full); background: var(--secondary); color: var(--foreground); white-space: nowrap; }
  .chip-clickable { cursor: pointer; transition: background-color 120ms var(--ease-smooth), border-color 120ms var(--ease-smooth); }
  .chip-clickable:hover { border-color: var(--border-strong); background: var(--state-hover); }
  /* v3: selected chip = accent TONAL (amber wash + amber ink), not a solid
     fill — solid is reserved for the one primary CTA. */
  .chip.is-selected { background: var(--btn-tonal-selected-bg); color: var(--btn-tonal-fg); border-color: var(--btn-tonal-border); }
  /* Tinted semantic chips — coloured bg + matching border, but KEEP .chip's
     readable --foreground text (unlike the soft .tag-*, whose saturated text can
     read faintly). For many small coloured chips that must each stay legible. */
  .chip-info    { background: var(--info-bg);    border-color: var(--info-border);    }
  .chip-warning { background: var(--warning-bg); border-color: var(--warning-border); }
  .chip-success { background: var(--success-bg); border-color: var(--success-border); }
  .chip-danger  { background: var(--danger-bg);  border-color: var(--danger-border);  }
  .chip-brand   { background: var(--brand-soft); border-color: var(--brand-soft-hover); }
  .chip-remove { display: inline-flex; align-items: center; justify-content: center; inline-size: 1rem; block-size: 1rem; margin-inline-end: -3px; padding: 0; border: none; border-radius: 50%; background: none; color: inherit; cursor: pointer; font-size: 0.9em; line-height: 1; opacity: 0.65; }
  .chip-remove:hover { opacity: 1; background: color-mix(in oklab, currentColor 18%, transparent); }

  /* ---- batch 5: sidebar / carousel / command-item (tabs + command are WCs) ---- */
  /* Items in a section are tight (2px); each .sidebar-label opens a new section —
     the first hugs the top, the rest get a hairline rule + breathing room so the
     groups (e.g. Console vs Admin) are unmistakably separate. Items carry a
     leading icon slot (any inline svg) that follows the row colour. */
  /* min-block-size:100% so the rail (and its border-inline-end divider) fills a
     stretched parent all the way down instead of stopping at content height —
     null-effect when the parent has no definite height, so it never over-stretches
     a non-app-shell usage. */
  .sidebar { display: flex; flex-direction: column; min-block-size: 100%; gap: 2px; inline-size: var(--sidebar-w, 15rem); padding: var(--sp-3) var(--sp-2); background: var(--sidebar, var(--surface-sunken)); color: var(--sidebar-foreground, var(--foreground)); border-inline-end: 1px solid var(--border); }
  .sidebar-label { padding: var(--sp-1) var(--sp-2); font-size: var(--text-micro); font-weight: var(--weight-bold); text-transform: uppercase; letter-spacing: var(--tracking-wide); color: var(--muted-foreground); }
  .sidebar-label:first-child { margin-block-start: 0; }
  .sidebar-label:not(:first-child) { margin-block-start: var(--sp-3); padding-block-start: var(--sp-4); border-block-start: 1px solid var(--border); }
  /* Item text uses --text-menu (13px / medium) — the platform-wide left-nav
     standard shared with the AI对话 and AI短剧 sidebars. */
  .sidebar-item { position: relative; display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); border: none; background: none; border-radius: var(--radius); font: inherit; font-size: var(--text-menu, 0.8125rem); font-weight: var(--weight-medium); color: inherit; text-decoration: none; text-align: start; cursor: pointer; transition: background-color var(--duration-fast) var(--ease-smooth), color var(--duration-fast) var(--ease-smooth); }
  /* 复合行(项目行、生成任务行)里那块「主点击区」要撑满整行高度。默认
     align-items:center 会把它缩到文字高度(实测 42px 的行里只有中间 20px 可点),
     上下 padding 成了死区 —— 看着是一整行,点上去一半没反应
     (operator 2026-08-09「其他 UI 细节」)。 */
  .sidebar-item > .btn-bare { align-self: stretch; align-items: center; }
  .sidebar-item > svg { inline-size: 1rem; block-size: 1rem; flex-shrink: 0; opacity: 0.8; transition: opacity var(--duration-fast) var(--ease-smooth), color var(--duration-fast) var(--ease-smooth); }
  .sidebar-item:hover { background: var(--state-hover); color: var(--foreground); }
  .sidebar-item:hover > svg { opacity: 0.95; }
  /* Row-hover-revealed inline action (delete/rename on list rows): invisible
     until the row is hovered or the control is keyboard-focused; always
     faintly visible on touch devices where hover cannot reveal it. */
  /* Bare button — a clickable region with ZERO native chrome, for rows/cards
     whose visual identity comes from the parent (.sidebar-item project rows). */
  /* text-decoration 也要清:.btn-bare 常被套在 <a> 上当「一行里的主点击区」
     (侧栏的项目行、生成任务行都是),不清的话那一行就带着浏览器默认下划线,
     混在一排菜单项里像个外来的链接 —— 线上实测「生成任务」就是这样
     (operator 2026-08-09「其他 UI 细节」)。 */
  .btn-bare { appearance: none; border: none; background: none; padding: 0; margin: 0; font: inherit; color: inherit; text-align: inherit; text-decoration: none; cursor: pointer; }
  .row-action { opacity: 0; transition: opacity var(--duration-fast) var(--ease-smooth); }
  :hover > .row-action, .row-action:focus-visible { opacity: 1; }
  @media (hover: none) { .row-action { opacity: 0.55; } }
  .sidebar-item:focus-visible { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); }
  /* Active item — the v3 design language (paper mockup): a soft tonal block
     with the icon speaking the accent color. NO leading rail bar (that was
     the v2 tell); the rounded wash itself is the active marker. */
  .sidebar-item.is-active, .sidebar-item[aria-current="page"] { background: var(--accent); color: var(--foreground); font-weight: var(--weight-semibold); }
  .sidebar-item.is-active > svg, .sidebar-item[aria-current="page"] > svg { opacity: 1; color: var(--ui-accent); }
  /* 悬停「当前页」时不能退回中性悬停色 —— 那会让当前项在鼠标下丢掉自己的身份。
     当前态在悬停时只是更重一点。 */
  .sidebar-item.is-active:hover, .sidebar-item[aria-current="page"]:hover {
    background: color-mix(in oklab, var(--ui-accent) 6%, var(--accent)); color: var(--foreground);
  }
  .sidebar-item > .tag, .sidebar-item > .chip { margin-inline-start: auto; }

  /* ---- sidebar template: fixed head / scrolling middle / fixed foot ----
     For a rail whose middle section is a LIST OF DATA (chat sessions, drama
     episodes, saved queries) that can reach dozens of rows. Scrolling the whole
     rail is the bug this replaces: the channel entries and the primary "new …"
     action scroll away with the 50th row, so the rail's navigation disappears
     exactly when a long history makes it most needed.
     Requires a definite height on the rail — inside .app-shell-fixed it has one;
     .sidebar-scroll is a no-op (content-height) rail otherwise, which is the
     right degradation for a document-scrolling page. */
  .sidebar-head { flex: none; display: flex; flex-direction: column; gap: 2px; }
  .sidebar-scroll { flex: 1; min-block-size: 0; display: flex; flex-direction: column; gap: 2px; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
  .sidebar-scroll::-webkit-scrollbar { width: 8px; }
  .sidebar-scroll::-webkit-scrollbar-track { background: transparent; }
  .sidebar-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: var(--radius-full); border: 2px solid transparent; background-clip: content-box; }
  /* margin-block-start:auto keeps the account/identity block pinned to the
     bottom even when the middle section is short (a fresh account with 2 rows). */
  .sidebar-foot { flex: none; margin-block-start: auto; padding-block-start: var(--sp-2); border-block-start: 1px solid var(--border); display: flex; flex-direction: column; gap: 2px; }

  /* ---- canonical app-shell — the ONE full-page layout every product emits ----
     Gives the lib .sidebar a definite full height so its border-inline-end divider
     runs edge-to-edge to the bottom, without each product re-inventing min-h-screen /
     grid / items-start glue. Document-scroll preserving (NO internal scroll, NO pinned
     footer): the shell is min 100dvh and GROWS with tall content. Structure:
       .app-shell > [header] + .app-shell-body( .sidebar + .app-main ) + [footer / mobile-nav]
     Short page → the body-row fills the viewport (rail reaches the bottom); tall page →
     it grows to content height (rail reaches the content's bottom) and the page scrolls
     normally. 100dvh is mobile-URL-bar correct; logical props keep it writing-mode safe. */
  .app-shell { display: flex; flex-direction: column; min-block-size: 100dvh; }
  /* The row fills the height left under the header (flex:1) but never shrinks below its
     content — so the rail reaches the bottom on short AND tall pages. */
  .app-shell-body { display: flex; flex: 1; }
  .app-shell-body > .sidebar, .app-shell-body > .app-sidebar { align-self: stretch; }
  /* Content pane: fills the row inline; min-inline-size:0 lets long content shrink
     instead of overflowing. Padding is opt-in via --app-main-pad (default 0 so
     full-bleed pages and padded consoles both compose without per-product overrides). */
  .app-main { flex: 1; min-inline-size: 0; padding: var(--app-main-pad, 0); }

  /* Opt-in fixed-viewport console/editor shell. The header stays in flow while
     the body consumes the remaining viewport and only the main pane scrolls.
     Products select the behavior; they do not reimplement height/overflow CSS. */
  .app-shell.app-shell-fixed { block-size: 100dvh; min-block-size: 0; overflow: hidden; }
  .app-shell.app-shell-fixed > .app-shell-body { min-block-size: 0; overflow: hidden; }
  .app-shell.app-shell-fixed > .app-shell-body > .app-main { display: flex; flex-direction: column; min-block-size: 0; overflow: auto; }

  /* ---- responsive app-shell — phone sidebar drawer (< the header's 768px PC
     breakpoint) ----------------------------------------------------------------
     Desktop: .sidebar is the in-flow rail. Phone: it becomes an off-canvas drawer
     toggled by a product-rendered hidden checkbox (.app-nav-toggle-input) — pure
     CSS via :has(), NO JS, and picking a nav item (which re-renders the shell)
     closes it. Products emit, as children of .app-shell-body: the hidden checkbox,
     .sidebar, <main.app-main> (whose first child is a label.app-nav-toggle
     hamburger), then a label.app-nav-backdrop. All the off-canvas chrome is hidden
     on desktop, so adding the markup is inert above the breakpoint. */
  /* ---- desktop rail: collapse to icons + drag to resize ----------------------
     Orthogonal to the phone drawer below (that engages only ≤48rem). Collapsing
     narrows the rail to an icon strip instead of removing it: navigation stays
     reachable and the re-open handle is still where the rail was — a rail that
     vanishes takes its own toggle with it. Width is the single --sidebar-w knob
     the rail already reads, so <dev-rail-resizer> writes one property and
     nothing else in the shell needs to know. */
  .app-shell-body[data-nav="collapsed"] > :is(.sidebar, .app-sidebar) { --sidebar-w: var(--sidebar-rail-w, 3.25rem); padding-inline: var(--sp-1); overflow: hidden; }
  .app-shell-body[data-nav="collapsed"] > :is(.sidebar, .app-sidebar) .sidebar-item { justify-content: center; padding-inline: 0; gap: 0; }
  /* NO transition on the rail's inline-size. It looks nice and it breaks the drag:
     with a running width transition the dragged value never lands (measured in
     production — the rail visibly resized, then went back to 240px), and every
     frame of it relayouts the whole canvas. Collapse/expand snaps instead. */
  /* The grabber is a 10px hit area with a 2px visual — wide enough to grab,
     thin enough not to read as a divider of its own. 它同时是**收起/展开的唯一入口**
     (双击),所以收起状态下绝不能消失 —— 藏了就再也展不开(operator 2026-08-09
     把收起按钮撤掉后,这条线就是那颗按钮)。 */
  .rail-resizer { flex: 0 0 auto; inline-size: 10px; margin-inline-start: -5px; cursor: col-resize; background: transparent; position: relative; z-index: var(--z-raised); touch-action: none; }
  .rail-resizer::after { content: ""; position: absolute; inset-block: 0; inset-inline-start: 4px; inline-size: 2px; background: var(--ui-accent); opacity: 0; transition: opacity var(--duration-fast, 0.12s) var(--ease-smooth, ease); }
  .rail-resizer:hover::after, .rail-resizer:focus-visible::after, .rail-resizer.is-dragging::after { opacity: 1; }
  .rail-resizer:focus-visible { outline: none; }
  /* 收起态:线还在原地,但拖不动了(JS 侧也挡着),所以光标不再是「可拖宽」。 */
  .app-shell-body[data-nav="collapsed"] .rail-resizer { cursor: pointer; }
  @media (max-width: 48rem) { .rail-resizer, dev-rail-resizer { display: none; } }
}

/* The rail's collapsed state has to WIN over the layout utilities that put those
   rows on screen (.cluster/.stack are `display:flex`), and `lib.utilities` is the
   last layer — inside a later layer even a one-class rule beats any specificity
   from an earlier one. So these two live here, next to what they override,
   instead of in lib.components with the rest of the shell. */
@layer lib.utilities {
  .app-shell-body[data-nav="collapsed"] > :is(.sidebar, .app-sidebar) :is(.sidebar-label, .rail-hide) { display: none; }
  /* Keep the icon, drop the words: everything after the leading glyph is text. */
  .app-shell-body[data-nav="collapsed"] > :is(.sidebar, .app-sidebar) .sidebar-item > *:not(svg):not(.rail-keep) { display: none; }

  /* Chrome 面:与 <dev-site-header> 同一张纸。
     频道自己的页头贴在平台顶栏正下方时,必须和它共面 —— 否则叠出「顶栏 --card /
     页头 --background / 侧栏 --card」三级近白,中间那条最暗,整片 chrome 看着脏
     (operator 2026-08-04 在 AI短剧 上点名)。`.document-workspace-topbar` 的
     `--background` 是给**整页文档工作台**用的(顶栏坐在文档底色上);贴着平台顶栏
     的那种页头加这个类即可,不必让每个频道各自写一遍背景色。
     必须在 lib.utilities 里:层序是 reset → components → utilities,放进
     components 会被 utilities 整层压掉(2026-08-04 线上实测)。 */
  .surface-chrome { background: var(--card); }
}

@layer lib.components {

  .app-nav-toggle-input { position: absolute; inline-size: 1px; block-size: 1px; margin: -1px; opacity: 0; pointer-events: none; }
  .app-nav-toggle { display: none; align-self: flex-start; align-items: center; justify-content: center; gap: var(--sp-2); block-size: 2.25rem; min-inline-size: 2.25rem; padding: 0 var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface-raised); color: var(--foreground); font: inherit; font-size: var(--text-caption); font-weight: var(--weight-medium); cursor: pointer; user-select: none; }
  .app-nav-toggle:hover { background: var(--state-hover); }
  .app-nav-toggle > svg { inline-size: 1.1rem; block-size: 1.1rem; flex: none; }
  .app-nav-backdrop { display: none; }
  @media (max-width: 48rem) {
    /* Strictly OPT-IN: the off-canvas behavior only engages when the page emits
       the .app-nav-toggle-input checkbox (+ a .app-nav-toggle hamburger). A shell
       without the toggle keeps its in-flow sidebar unchanged, so this is safe for
       every channel that hasn't adopted the drawer yet. */
    .app-shell-body:has(> .app-nav-toggle-input) > .sidebar,
    .app-shell-body:has(> .app-nav-toggle-input) > .app-sidebar {
      position: fixed; inset-block: 0; inset-inline-start: 0; z-index: var(--z-dropdown);
      inline-size: min(17rem, 82vw); max-inline-size: 82vw;
      transform: translateX(-100%);
      transition: transform var(--duration-medium, 0.22s) var(--ease-smooth, ease);
      box-shadow: var(--shadow-float, 0 10px 30px rgba(0, 0, 0, 0.22));
      overflow-y: auto;
    }
    .app-shell-body:has(> .app-nav-toggle-input:checked) > .sidebar,
    .app-shell-body:has(> .app-nav-toggle-input:checked) > .app-sidebar { transform: translateX(0); }
    .app-shell-body:has(> .app-nav-toggle-input) .app-nav-backdrop { display: block; position: fixed; inset: 0; z-index: calc(var(--z-dropdown) - 5); border: 0; background: rgba(0, 0, 0, 0.45); opacity: 0; pointer-events: none; transition: opacity var(--duration-medium, 0.22s) var(--ease-smooth, ease); }
    .app-shell-body:has(> .app-nav-toggle-input:checked) .app-nav-backdrop { opacity: 1; pointer-events: auto; }
    /* The hamburger is phone-only, so it always receives the shared 44px touch
       floor even when desktop devtools emulate width without a coarse pointer. */
    .app-nav-toggle { display: inline-flex; min-block-size: var(--sp-11); }
    /* 桌面那条「收起」在抽屉里没有意义 —— 抽屉本身就是收起态。收起入口现在是
       分割线(.rail-resizer),它在窄屏本来就已经整条隐藏(见上面的 48rem 规则),
       所以这里不需要再单独藏什么。 */
  }

  .carousel { display: flex; gap: var(--sp-3); overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: thin; padding-block-end: var(--sp-2); overscroll-behavior-inline: contain; }
  .carousel > * { scroll-snap-align: start; flex: 0 0 auto; }
  .carousel-marquee { overflow: hidden; }
  .carousel-marquee > .carousel-track { display: flex; gap: var(--sp-3); inline-size: max-content; animation: lib-marquee 24s linear infinite; }
  .carousel-marquee:hover > .carousel-track { animation-play-state: paused; }

  .cmd-item { display: flex; align-items: center; gap: var(--sp-2); inline-size: 100%; box-sizing: border-box; padding: var(--sp-2) var(--sp-3); border: none; background: none; border-radius: var(--radius-sm); font: inherit; font-size: var(--text-caption); color: var(--foreground); text-align: start; cursor: pointer; }
  /* 键盘当前项 ≠ 鼠标悬停。它们必须能**同时存在而不混淆**:鼠标停在一行、方向键
     又在另一行时,两行同色就看不出回车会落到哪。悬停只给底色,键盘游标额外多一道
     内侧强调条 —— 分量比悬停重一档,且两者叠加时仍分得清。 */
  .cmd-item:hover { background: var(--state-hover); }
  .cmd-item.active { background: var(--state-selected); box-shadow: inset 2px 0 0 var(--ui-accent); }
  .cmd-item .menu-shortcut { margin-inline-start: auto; }

  /* ---- batch 6: tree + diff (rich-textarea/calendar/image-annotate are WCs) ---- */
  .tree, .tree-group { list-style: none; margin: 0; padding: 0; }
  .tree { font-size: var(--text-caption); }
  .tree-group { padding-inline-start: var(--sp-4); }
  [role="treeitem"][aria-expanded="false"] > .tree-group { display: none; }
  .tree-item { display: flex; align-items: center; gap: var(--sp-1); padding: var(--sp-1) var(--sp-2); border-radius: var(--radius-sm); color: var(--foreground); cursor: pointer; }
  .tree-item:hover { background: var(--state-hover); }
  .tree-item:focus-visible { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); }
  [role="treeitem"][aria-selected="true"] > .tree-item { background: var(--ui-accent-soft); color: var(--accent-foreground, var(--foreground)); font-weight: var(--weight-medium); }
  .tree-twisty { display: inline-flex; inline-size: 1rem; justify-content: center; flex: none; color: var(--muted-foreground); transition: transform 150ms var(--ease-smooth); }
  [role="treeitem"][aria-expanded="false"] > .tree-item .tree-twisty { transform: rotate(-90deg); }
  [role="treeitem"]:not([aria-expanded]) > .tree-item .tree-twisty { visibility: hidden; }

  .diff { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; font-family: var(--font-mono); font-size: var(--text-caption); line-height: 1.55; }
  /* 左右两栏原本各自堆行、各自滚动 —— 任何一行折行,从那里往下整篇就错位了。
     现在一行一对、共用同一条网格:.diff-row 用 display:contents 把两个单元格
     直接交给网格,同一行的左右天然等高;滚动也只剩一条(挂在 .diff 上)。 */
  .diff { max-block-size: 22rem; overflow: auto; }
  .diff-row { display: contents; }
  .diff-pane { background: var(--card); }
  .diff-pane-head { padding: var(--sp-1) var(--sp-3); font-family: var(--font-sans); font-size: var(--text-micro); font-weight: var(--weight-semibold); color: var(--muted-foreground); background: var(--surface-sunken); border-block-end: 1px solid var(--border); position: sticky; inset-block-start: 0; }
  .diff-line { display: block; background: var(--card); padding-inline: var(--sp-3); white-space: pre-wrap; word-break: break-word; }
  .diff-add { background: var(--success-bg); }
  .diff-del { background: var(--danger-bg); }
  .diff-empty { background: color-mix(in oklab, var(--muted) 60%, transparent); min-block-size: 1.55em; }

  /* ---- vP7: channel-migration gaps (collision-safe names) ---- */
  /* Metric tile — generic key/value stat (replaces channels' bespoke
     .stat-card/.metrics/.buy-stats; the bare .stat-card name stays withheld). */
  .metric { display: grid; gap: var(--sp-1); padding: var(--c-card-pad); border: 1px solid var(--border); border-radius: var(--c-card-radius); background: var(--card); min-inline-size: 0; }
  .metric-label { font-size: var(--text-micro); font-weight: var(--weight-medium); text-transform: uppercase; letter-spacing: var(--tracking-wide); color: var(--muted-foreground); }
  .metric-value { font-size: var(--text-title); font-weight: var(--weight-semibold); line-height: 1.1; color: var(--foreground); font-variant-numeric: tabular-nums; }
  .metric-delta { display: inline-flex; align-items: center; gap: 2px; font-size: var(--text-micro); color: var(--muted-foreground); }
  /* 11px delta figures are TEXT on a card — the fill tones read 3.14:1 (green)
     and 4.60:1 (red) there, so both take the ink step. */
  .metric-delta.is-up { color: var(--success-text); }
  .metric-delta.is-down { color: var(--danger-text); }
  /* Dashboard-grade KPI variant — the number carries the tile. */
  .metric-lg .metric-value { font-size: var(--text-display); }
  /* horizontal key:value pill variant (e.g. timing rows) */
  .metric-row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface-sunken); min-inline-size: 0; }
  .metric-row .metric-value { font-size: var(--text-body); text-align: end; }

  /* Dark-glass badge for labeling content OVER media (theme-independent,
     legible on any image — token .tag/.chip are invisible over photos). */
  .media-badge { display: inline-flex; align-items: center; gap: var(--sp-1); min-block-size: 1.5rem; padding: 0.3125rem 0.625rem; border-radius: var(--radius-full); border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.6); color: #fff; font-size: var(--text-micro); line-height: 1; letter-spacing: 0.02em; -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); }

  /* Generic media-library composition. Consumers provide only media and data;
     lib owns the preview ratio, selection state, card spacing, and actions. */
  .media-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--media-card-min, 14rem), 1fr)); gap: var(--sp-4); align-items: start; }
  .media-card { position: relative; min-inline-size: 0; overflow: hidden; padding: 0; border: 1px solid var(--border); border-radius: var(--radius-lg); appearance: none; background: var(--card); color: var(--card-foreground); box-shadow: var(--shadow-card); font: inherit; text-align: inherit; }
  button.media-card { cursor: pointer; }
  .media-card[aria-selected="true"], .media-card.is-selected { border-color: var(--ui-accent); box-shadow: 0 0 0 2px color-mix(in oklab, var(--ui-accent) 26%, transparent); }
  .media-preview { position: relative; display: grid; place-items: center; aspect-ratio: var(--media-aspect, 16 / 10); overflow: hidden; background: var(--surface-sunken); border-block-end: 1px solid var(--border); }
  .media-preview.media-preview-video { --media-aspect: 16 / 9; }
  .media-preview.media-preview-portrait { --media-aspect: 3 / 4; }
  .media-preview > img, .media-preview > video { inline-size: 100%; block-size: 100%; object-fit: cover; }
  .media-preview.media-preview-contain > img, .media-preview.media-preview-contain > video { object-fit: contain; }
  .media-preview > audio { inline-size: calc(100% - (2 * var(--sp-3))); max-inline-size: 100%; }
  .media-preview-placeholder { display: grid; place-items: center; inline-size: 100%; block-size: 100%; color: var(--muted-foreground); font-size: var(--text-title); }
  .media-preview > .media-badge { position: absolute; inset-block-start: var(--sp-2); inset-inline-end: var(--sp-2); transition: opacity var(--duration-fast, 120ms) var(--ease-smooth, ease); }
  /* 类型徽章与放大角标同住右上角:hover(角标浮现)时徽章暂时让位 ——
     与画布节点的候选徽章同一规矩,两个标叠在一起谁都看不清。 */
  .media-preview:has(.media-zoom):hover > .media-badge { opacity: 0; }
  /* Anything wired to the singleton <dev-lightbox> (declarative data-lightbox attr
     or a channel click handler) advertises the full-view affordance. */
  [data-lightbox] { cursor: zoom-in; }
  .media-player { inline-size: 100%; max-block-size: var(--media-player-max, 24rem); border-radius: var(--radius); background: var(--surface-sunken); }
  .media-card-select { position: absolute; inset-block-start: var(--sp-2); inset-inline-start: var(--sp-2); z-index: var(--z-raised); display: inline-flex; padding: var(--sp-1); border-radius: var(--radius-sm); background: color-mix(in oklab, var(--card) 86%, transparent); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); }
  .media-card-body { display: flex; flex-direction: column; gap: var(--sp-2); padding: var(--sp-3); }
  .media-card-title { min-inline-size: 0; overflow: hidden; color: var(--foreground); font-size: var(--text-body); font-weight: var(--weight-semibold); text-overflow: ellipsis; white-space: nowrap; }
  .media-card-meta { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1); color: var(--muted-foreground); font-size: var(--text-micro); }
  .media-card-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1); padding-block-start: var(--sp-1); border-block-start: 1px solid var(--border); }
  /* Compact thumbnail for horizontal asset rows (asset rails, mention lists).
     Size/aspect are tokens: override --media-thumb-w / --media-aspect per list. */
  .media-thumb { position: relative; flex: none; inline-size: var(--media-thumb-w, 3.75rem); aspect-ratio: var(--media-aspect, 3 / 4); display: grid; place-items: center; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface-sunken); color: var(--muted-foreground); font-size: var(--text-micro); }
  .media-thumb > img, .media-thumb > video { inline-size: 100%; block-size: 100%; object-fit: cover; }
  /* 需要看清「这是哪一张」的地方(资产栏、聊天回显)用 contain:cover 会把竖版
     画面裁成中间一条。列表默认仍是 cover —— 那里要的是整齐,不是完整。 */
  .media-thumb-contain > img, .media-thumb-contain > video { object-fit: contain; }
  /* 缩略图网格:等宽自动折行,格子宽度由 --tile-w 调。有了它,一列资产栏里的
     「图 + 名字」不必各自算宽度,名字也才有个边界可以 truncate。 */
  .tile-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--tile-w, 4.5rem), 1fr)); gap: var(--sp-2); }
  .tile-grid > * { min-inline-size: 0; }
  /* 侧栏里的「大格子」:约 8rem 一格,21rem 宽的资产栏正好两列。 */
  /* 7rem:17rem 宽的侧栏正好两列(8rem 会退化成一列,线上实测)。 */
  .tile-grid-lg { --tile-w: 7rem; }
  /* 缩略图撑满它所在的格子(默认是 3.75rem 的固定小图)。 */
  .media-thumb-fill { --media-thumb-w: 100%; }
  /* 空的素材位:虚线框 + 居中的一句「添加」,点它去挑素材(上传 / 图库)。
     每个有素材位的频道都在自己那边画过一遍这个方格(图片的参考图、视频的参考图与
     首尾帧),形状还各不相同 —— 收进 lib,和已挂素材的 .media-thumb 天然等大等圆角。 */
  .media-thumb-add { border-style: dashed; cursor: pointer; background: none; }
  .media-thumb-add:hover { border-color: var(--primary); color: var(--foreground); }
  .media-thumb-add:focus-visible { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); }
  .media-thumb-add:disabled { opacity: var(--state-disabled-opacity); pointer-events: none; }
  /* 画幅形状小框(与 AI对话 AspectShape 同款):regular 卡片/规格键上按真实比例
     画的小矩形,宽高由宿主内联 size 给,这里只管描边。圆角写死 2px:系统圆角在
     19×11 的小框上会变成胶囊,看不出是个画幅。 */
  .aspect-shape { display: block; flex: none; border: 1.5px solid currentColor; border-radius: 2px; opacity: .75; }
  /* 可放大的缩略图(挂 data-lightbox 的 .media-thumb / .media-card 图区):hover 时
     右上角浮出一个「放大 / 播放」小圆标,提示点击可看大图。纯 CSS 角标
     (pointer-events:none)—— 点击仍由 dev-lightbox 的全局委托接管,页面零脚本。
     图标是 lucide zoom-in / play 的白描边 SVG(不手画,见 lib-frontend-spec)。 */
  .media-thumb[data-lightbox]::after, .media-card [data-lightbox]::after, .media-zoom {
    position: absolute; inset-block-start: 4px; inset-inline-end: 4px;
    inline-size: 1.375rem; block-size: 1.375rem; border-radius: var(--radius-full, 9999px); border: 0; padding: 0;
    background: rgba(0, 0, 0, 0.55) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3Cline x1='11' y1='8' x2='11' y2='14'/%3E%3Cline x1='8' y1='11' x2='14' y2='11'/%3E%3C/svg%3E") center / 0.875rem no-repeat;
    opacity: 0; transition: opacity var(--duration-fast, 120ms) var(--ease-smooth, ease);
  }
  .media-thumb[data-lightbox]::after, .media-card [data-lightbox]::after { content: ""; pointer-events: none; }
  /* 真按钮版:缩略图本体的点击另有语义(选版本 / 挑素材)时,角标自己就是
     lightbox 触发器 —— <button class="media-zoom" data-lightbox> 放进 .media-thumb,
     大图源自动取同一宿主里的 img/video(dev-lightbox 委托的宿主回溯)。 */
  .media-zoom { cursor: zoom-in; }
  .media-zoom:focus-visible { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); opacity: 1; }
  .media-thumb[data-lightbox]:hover::after, .media-card [data-lightbox]:hover::after,
  .media-thumb:hover .media-zoom, :hover > .media-zoom, .media-zoom:hover { opacity: 1; }
  /* 视频缩略图的角标换成播放三角(声明式 type 或宿主里就是 <video> 都认)。 */
  .media-thumb[data-lightbox][data-lightbox-type="video"]::after,
  .media-thumb[data-lightbox]:has(video)::after,
  .media-card [data-lightbox][data-lightbox-type="video"]::after,
  .media-card [data-lightbox]:has(video)::after,
  .media-zoom[data-lightbox-type="video"], .media-thumb:has(video) > .media-zoom {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='6 3 20 12 6 21 6 3'/%3E%3C/svg%3E");
  }
  /* 列表里的小圆点标记:比图标轻,又比纯文字有锚点。 */
  /* 侧栏面板的底:自带卡面与一条起始分隔线。共享组件的外观必须由组件自己带,
     宿主只决定它放在哪 —— 一个宿主传 card、另一个不传,同一个组件就会长成两个样子。 */
  .surface-panel { background: var(--card); border-inline-start: 1px solid var(--border); }
  /* 消费方长期在用、lib 却没有的几档 —— 写了等于没写,补齐: */
  .surface-muted { background: var(--surface-sunken); border-radius: var(--radius); }
  /* 全出血面板(surface-panel)套沉底色时只要「色」不要「圆角」:贴边面板带
     圆角会把四角剪出缺口(xshort 项目资产栏,2026-08-06 像素巡检)。 */
  .surface-panel.surface-muted { border-radius: 0; }
  .card-pad-sm { padding: var(--sp-2) var(--sp-3); }
  .media-card-thumb { inline-size: 100%; aspect-ratio: var(--media-aspect, 3 / 4); object-fit: cover; border-radius: var(--radius) var(--radius) 0 0; display: block; }

  .dot-mark { inline-size: .375rem; block-size: .375rem; border-radius: 999px; background: var(--muted-foreground); opacity: .45; }
  /* 状态点变体:检视面板头部「已就绪/待处理」一类的 6px 状态灯(画布右栏首用)。 */
  .dot-mark-ok { background: var(--success); opacity: 1; }
  .dot-mark-warn { background: var(--warning, var(--accent)); opacity: 1; }
  .media-thumb-duration { position: absolute; inset-block-end: var(--sp-1); inset-inline-end: var(--sp-1); padding: 0 var(--sp-1); border-radius: var(--radius-sm); background: color-mix(in oklab, var(--foreground) 75%, transparent); color: var(--background); font-size: var(--text-micro); }

  /* Compact media editor slot used wherever one immutable image/video input
     owns upload, generate, edit, and history actions. This is deliberately a
     generic role: channels provide the media and commands, lib owns layout,
     overlay contrast, and narrow-container sizing. */
  .media-slot { display: flex; flex-direction: column; gap: var(--sp-2); inline-size: min(100%, var(--media-slot-width, 12.5rem)); flex: none; }
  .media-slot-preview { position: relative; display: grid; place-items: center; inline-size: 100%; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface-sunken); }
  .media-slot-preview > img, .media-slot-preview > video { inline-size: 100%; block-size: 100%; object-fit: cover; }
  .media-overlay-action { position: absolute; inset-block-start: var(--sp-1); inset-inline-start: var(--sp-1); z-index: var(--z-raised); display: inline-flex; align-items: center; justify-content: center; inline-size: var(--control-h-sm); block-size: var(--control-h-sm); padding: 0; border: 0; border-radius: var(--radius-sm); background: rgba(0,0,0,0.55); color: #fff; cursor: pointer; }
  .media-overlay-action:hover { background: rgba(0,0,0,0.76); }
  .media-overlay-action:focus-visible { outline: 2px solid transparent; box-shadow: 0 0 0 2px #fff; }
  .media-progress-overlay { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--sp-2); background: rgba(0,0,0,0.55); color: #fff; }
  .media-progress-overlay > .spinner { border-color: rgba(255,255,255,0.35); border-right-color: #fff; }
  .media-preview-action { position: absolute; inset: 0; display: grid; place-items: center; inline-size: 100%; block-size: 100%; padding: 0; border: 0; background: rgba(0,0,0,0.22); color: #fff; cursor: pointer; transition: background var(--duration-fast) var(--ease-standard); }
  .media-preview-action:hover { background: rgba(0,0,0,0.45); }
  .media-preview-action:focus-visible { outline: 2px solid transparent; box-shadow: inset 0 0 0 3px #fff; }

  /* Fill-height card — same surface as .card but stretches in a flex/grid row
     (dev-card's shadow body is content-height and cannot). */
  .card-stretch { display: flex; flex-direction: column; min-block-size: 0; background: var(--card); color: var(--card-foreground, var(--foreground)); border: 1px solid var(--border); border-radius: var(--c-card-radius); box-shadow: var(--c-card-shadow); overflow: hidden; }
  .card.card-selected { background: var(--ui-accent-soft); }

  /* Conversation messages are generic content roles. Compose with .card and
     the type/spacing utilities; the channel supplies only message content. */
  .chat-message { inline-size: min(82%, 42rem); }
  .chat-message-user { align-self: flex-end; background: var(--state-selected); }
  .chat-message-assistant { align-self: flex-start; }

  /* Conversation workbench bubbles — accent-tonal user speech with a keyed
     corner, a square AI mark, and entity reference chips. Skin decides the
     hues (paper: amber speech / teal references). */
  .chat-bubble-user { max-inline-size: 78%; align-self: flex-end; background: color-mix(in oklab, var(--p-accent) 16%, var(--surface-raised)); border: 1px solid color-mix(in oklab, var(--p-accent) 34%, var(--surface-raised)); color: color-mix(in oklab, var(--p-accent) 28%, var(--foreground)); padding: var(--sp-3) var(--sp-4); border-radius: var(--radius-xl) var(--radius-xl) calc(var(--radius) - 6px) var(--radius-xl); line-height: 1.8; white-space: pre-wrap; font-size: var(--text-body); }
  .chat-avatar-mark { display: inline-flex; align-items: center; justify-content: center; inline-size: 1.75rem; block-size: 1.75rem; flex: 0 0 1.75rem; border-radius: var(--radius-sm); background: linear-gradient(135deg, var(--p-gray-7), var(--p-gray-5)); border: 1px solid var(--border-strong); color: var(--p-accent); font-size: var(--text-micro); font-weight: var(--weight-bold); }
  .chip-ref { background: color-mix(in oklab, var(--chart-2) 13%, var(--surface-raised)); border-color: color-mix(in oklab, var(--chart-2) 36%, var(--surface-raised)); color: color-mix(in oklab, var(--chart-2) 72%, var(--foreground)); }
  .chip-ref:hover { background: color-mix(in oklab, var(--chart-2) 22%, var(--surface-raised)); }
  /* Conversation typing indicator — three pulsing accent dots. */
  .typing-dots { display: inline-flex; align-items: center; gap: 5px; padding-block: var(--sp-2); }
  .typing-dots > span { inline-size: 7px; block-size: 7px; border-radius: var(--radius-full); background: var(--p-accent); animation: lib-dot-pulse 1.2s infinite; }
  .typing-dots > span:nth-child(2) { animation-delay: 0.2s; }
  .typing-dots > span:nth-child(3) { animation-delay: 0.4s; }
  @keyframes lib-dot-pulse { 0%, 80%, 100% { opacity: 0.25; transform: scale(0.85); } 40% { opacity: 1; transform: none; } }
  /* Chat body copy breathes looser than tool-grade text. */
  .leading-chat { line-height: 1.8; }
  /* Identity swatches — small colored initial squares for list identity. */
  .swatch { display: inline-flex; align-items: center; justify-content: center; inline-size: 1.625rem; block-size: 1.625rem; flex: 0 0 1.625rem; border-radius: var(--radius-sm); color: var(--primary-foreground); font-size: var(--text-micro); font-weight: var(--weight-bold); }
  .swatch-1 { background: var(--chart-1); } .swatch-2 { background: var(--chart-2); } .swatch-3 { background: var(--chart-3); } .swatch-4 { background: var(--chart-4); } .swatch-5 { background: var(--chart-5); }

  /* Structured editable document: one framed surface with selectable blocks
     and seam-free shared editors. Product channels provide only content. */
  .document-surface { margin: 0 var(--sp-3) var(--sp-3); overflow: auto; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface-raised); }
  .document-block { position: relative; overflow: hidden; border-block-end: 1px solid var(--border); background: var(--surface-raised); transition: background var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); }
  .document-block:last-child { border-block-end: 0; }
  .document-block.is-selected { background: color-mix(in srgb, var(--brand) 6%, var(--surface-raised)); box-shadow: inset var(--sp-1) 0 0 var(--brand); }
  .document-block:focus-visible { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); }
  .editor-seamless { inline-size: 100%; margin: 0; }
  .editor-seamless::part(box) { border-inline: 0; border-radius: 0; background: transparent; }

  /* <dev-captcha> — light-DOM host for the vendored ALTCHA widget. ALTCHA
     injects its stylesheet into document.head, which cannot cross a shadow
     boundary, so the widget MUST live in light DOM; these rules map the
     design tokens onto ALTCHA's theming variables (light/dark follow the
     host palette automatically). min-block-size reserves the widget's
     resting height so the form doesn't jump when the lazy vendor bundle
     arrives. */
  dev-captcha { display: block; min-block-size: 52px; }
  dev-captcha altcha-widget {
    --altcha-color-base: var(--card);
    --altcha-color-base-content: var(--card-foreground, var(--foreground));
    --altcha-color-neutral: var(--muted-foreground);
    --altcha-color-neutral-content: var(--card);
    --altcha-color-primary: var(--primary);
    --altcha-color-primary-content: var(--primary-foreground);
    --altcha-color-error: var(--danger);
    --altcha-color-success: var(--success);
    --altcha-border-color: var(--border);
    --altcha-border-radius: var(--radius);
    --altcha-input-background-color: var(--background);
    --altcha-input-color: var(--foreground);
    --altcha-max-width: 100%;
  }
}

/* Coarse-pointer target floor. Compact desktop controls keep their 26–40px
   visual rhythm; touch input gets the platform 44px spacing step without any
   product breakpoint or one-off override. */
@layer lib.components {
  @media (pointer: coarse) {
    /* .btn-bare belongs here too: it IS the tap target of a composite row (a
       project row's name area next to its row-actions). Without the floor it
       renders at its own 36px text height while the .btn siblings in the same
       row are 44 — i.e. the biggest target in the row is the smallest one. */
    :is(
      .btn, .btn-bare, .input, .textarea, select.input, .tab, .sidebar-item,
      .menu-item, .cmd-item, .pagination .page, .segmented > button,
      .segmented > label, .stepper > button, .chip-clickable,
      .app-nav-toggle, .workspace-context-toggle, .workspace-context-close,
      .action-card, .media-overlay-action
    ) { min-block-size: var(--sp-11); }
    /* Width matters as much as height for a two-glyph chip: 文字/图片/视频 come out
       40px wide and sit 4px apart, which is the miss-tap geometry. */
    .btn-icon, .pagination .page, .media-overlay-action, .chip-clickable { min-inline-size: var(--sp-11); }
  }
}

/* ===== vP4 — container-query responsive (no JIT, no viewport breakpoints).
   Opt in with .cq on a wrapper; descendants use md-/lg-prefixed classes that
   react to the wrapper's width, not the viewport. ===== */
.cq { container-type: inline-size; }
@container (min-width: 30rem) {
  .md-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .md-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .md-row { flex-direction: row; }
  .md-flex { display: flex; }
  .md-hidden { display: none; }
  .md-block { display: block; }
}
/* `.md-only` is the exact mirror of `.md-hidden`: present only once the
   container is wide enough. A responsive toolbar needs BOTH halves — the wide
   inline row and the narrow overflow menu — and exactly one of them may exist
   at a time, in the accessibility tree as well as on screen. `.hidden` can't
   play this role (it is `!important`, so no `md-` rule can bring the element
   back), and a bare `display:none` on the product side is channel CSS. */
@container (width < 30rem) {
  .md-only { display: none; }
}
@container (width < 48rem) {
  .lg-only { display: none; }
}
@container (min-width: 48rem) {
  .lg-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .lg-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .lg-hidden { display: none; }
}

/* ── 状态点 / 24 格健康条 ──────────────────────────────────────────────
   监控类页面共用的两个原子。放在 lib 而不是某个产品里:图片监控、渠道监控、
   节点健康问的是同一件事(「现在什么状态」「这一天什么形状」),各写一份的结果
   是同一个绿在三个页面上是三个绿。 */

.status-dot {
  inline-size: .5rem; block-size: .5rem; border-radius: 99px;
  display: inline-block; flex: none;
  background: var(--muted-foreground);
}
.status-dot-ok { background: var(--success); }
.status-dot-degraded { background: var(--warning); }
.status-dot-fail { background: var(--destructive); }
/* 「没测过」刻意不是绿色 —— 把未验证渲染成健康是监控页最容易犯的谎。 */
.status-dot-idle { background: var(--muted-foreground); opacity: .45; }

.bar24 { display: flex; gap: 2px; align-items: center; }
.bar24 > i {
  inline-size: 5px; block-size: 16px; border-radius: 1px;
  background: var(--success);
}
.bar24 > i.bar24-warn { background: var(--warning); }
.bar24 > i.bar24-bad { background: var(--destructive); }
/* 没流量的那一格留成中性色而不是消失 —— 压缩掉空档,一段断服在时间轴上会
   被挤没,而「什么时候开始坏的」正是看这条的唯一理由。 */
.bar24 > i.bar24-none { background: var(--border); }

/* ── 状态开关 ──────────────────────────────────────────────────────────
   一颗胶囊**既是「现在是什么状态」也是「点一下就切」**。首个落地面是图片监控的
   状态列(渠道启停 / 模型上下架),形状却是通用的:渠道监控、节点健康、任何
   「一行一个实体,这一列写它开着还是关着」的表问的都是同一件事。

   进 lib 而不是各页自己拼,是因为「一个状态标签 + 一颗动作按钮」这种两件套会
   自相矛盾 —— 图片监控上一版就在同一格里出现过「启用」配「已自动停用」两个说法。
   一颗胶囊只有一个真相:文字写现状,`title` 写点下去会发生什么。

   三条交互规矩,写在这里免得下一个人「优化」掉:
   - **可点的悬停只加深边框,宽度绝不变**。悬停换文案(「启用」→「点击停用」)
     会让整列跟着抖,鼠标扫过一遍表格就是一次全表重排。
   - **动作与解释一律进 `title`**,不占第二行 —— 行高一涨,一屏能看见的对象就
     少一批,而这是一张用来扫的表。
   - **状态不是按钮时不许伪装成按钮**:派生的、点了不会有任何改变的状态用
     `<span class="state-switch state-switch-static">`,不用 `<button>`。一颗点了
     没反应的按钮比不给按钮更糟。

   用法:`<button class="state-switch state-switch-on" title="点击停用"><i></i>启用</button>`
   —— `<i>` 是状态点,与 `.status-dot` 同一套色觉(success / 琥珀 / 中性),小一号。 */
.state-switch {
  --state-switch-border: var(--border);
  --state-switch-border-hover: var(--border-hover);
  --state-switch-dot: var(--muted-foreground);
  display: inline-flex; align-items: center; gap: 5px;
  block-size: 22px; padding: 0 var(--sp-2);
  border: 1px solid var(--state-switch-border); border-radius: var(--radius-full);
  background: var(--card); color: var(--foreground);
  font-size: var(--text-caption); line-height: 1; white-space: nowrap;
  cursor: pointer;
}
.state-switch > i {
  inline-size: 6px; block-size: 6px; border-radius: var(--radius-full);
  flex: none; background: var(--state-switch-dot);
}
/* 悬停只动边框的颜色 —— 尺寸 / 内距 / 字重 / 文案一个都不许变(见上)。 */
.state-switch:not(.state-switch-static):hover { border-color: var(--state-switch-border-hover); }
.state-switch:focus-visible { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); }

/* 在售 / 启用。 */
.state-switch-on {
  --state-switch-border: var(--success-border);
  --state-switch-border-hover: var(--success);
  --state-switch-dot: var(--success);
  background: var(--success-bg);
}
/* 琥珀 = **非常态,要人知道**:被策略自动停用的渠道、一条活渠道都没有 / 一行价
   都没配的模型。它和「人自己关掉的」长得一样的话,运维会守着一条永远不会自己
   回来的通路等它自愈。(默认那身中性皮才是「人关的」。) */
.state-switch-warn {
  --state-switch-border: var(--warning-border);
  --state-switch-border-hover: var(--warning);
  --state-switch-dot: var(--warning);
  background: var(--warning-bg);
}
/* 只是一句陈述,不是开关。配 `<span>` 用(见上);光标不变手型、悬停不反应,
   免得读的人以为它点得动。 */
.state-switch-static { cursor: default; }
/* 危险方向的第二段:点一下先变成「确认下架?」,再点才执行 —— 超时还原由页面
   自己计时(那是业务节奏,不是样式)。 */
.state-switch-confirm {
  --state-switch-border: var(--danger-border);
  --state-switch-border-hover: var(--danger);
  --state-switch-dot: var(--danger);
  background: var(--danger-bg); color: var(--danger-text);
}
/* 写在飞:点不动 + 状态点换成转圈。这一格会宽 2px(8px 的圈比 6px 的点大),
   与悬停那条铁律不冲突 —— 悬停是鼠标扫过整列的高频动作,而这只发生在人自己
   点下去之后的那一两秒。转圈本身**不随 reduced-motion 停掉**,只降速:一个
   冻住的忙态圈让人分不清「在跑」和「挂了」(降速名单在 recipes.css)。 */
.state-switch-busy { opacity: .45; pointer-events: none; }
.state-switch-busy > i {
  inline-size: 8px; block-size: 8px; background: none;
  border: 1.5px solid var(--border); border-right-color: var(--foreground);
  animation: lib-spin 0.7s linear infinite;
}

/* recipes.css — 可注入 shadow DOM 的共享配方。
 *
 * 起因:shadow DOM 拿不到 system.css,于是每个 <dev-*> 组件把 .btn / .card /
 * 表头这些配方**各抄一份**,抄的过程中丢交互态、丢几何、词表还对不上 ——
 * 逐组件评审里条目最多的一条根因就是它。
 *
 * 这份文件是**唯一来源**:
 *   - 页面侧:build.mjs 把它拼进 /v3/system.css(消费方看到的仍是同一个文件)
 *   - 组件侧:esbuild 以 text 形式内联,组件用 adoptRecipes(shadowRoot) 采纳
 * 两侧吃的是同一段字节,所以不会再漂。
 *
 * 只有「组件内部也需要的」配方放这里。页面独有的排版(.page-pad、.app-shell…)
 * 留在 system.css。 */

@layer lib.components {
  /* ---- Buttons ---- */
  /* Default button = NEUTRAL OUTLINE (reference-console discipline: hue is
     reserved for the primary CTA + selected states; every other button stays
     quiet so a screen full of actions doesn't read as a wall of color). */
  .btn {
    box-sizing: border-box;
    display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
    min-height: var(--c-btn-h); padding: 0 var(--c-btn-px);
    border: 1px solid var(--btn-outline-border); border-radius: var(--c-btn-radius);
    background: var(--btn-outline-bg); color: var(--btn-outline-fg);
    box-shadow: var(--elevation-1);
    font-family: inherit; font-size: var(--text-caption); font-weight: var(--weight-medium);
    line-height: 1.2; letter-spacing: var(--tracking-normal); white-space: nowrap;
    text-decoration: none; cursor: pointer;
    transition: transform 100ms var(--ease-out),
                box-shadow 120ms var(--ease-out),
                background-color 120ms var(--ease-smooth),
                border-color 120ms var(--ease-smooth),
                filter 120ms var(--ease-smooth);
  }
  .btn:not(:disabled):hover { background: var(--btn-outline-bg-hover); border-color: var(--btn-outline-border-hover); }
  /* Press = 1px sink (the reference press affordance), not a scale. */
  .btn:not(:disabled):active { transform: translateY(1px); box-shadow: none; }
  .btn:focus-visible { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); }
  .btn:disabled, .btn[aria-disabled="true"] { opacity: var(--state-disabled-opacity); cursor: not-allowed; }
  .btn[aria-busy="true"] { cursor: progress; color: transparent; position: relative; }
  .btn[aria-busy="true"]::after {
    content: ""; position: absolute; inset: 0; margin: auto;
    width: 1em; height: 1em; border-radius: 50%;
    border: 2px solid currentColor; border-right-color: transparent;
    color: var(--btn-outline-fg); animation: lib-spin 0.7s linear infinite;
  }
  /* Primary CTA — v3: the SOLID ACCENT fill (the skin's voice color; baseline
     paper = amber), per the 漫剧工坊 definitive design. ONE decisive action
     per surface carries it (send, save, generate); everything else stays a
     quiet outline/ghost so the screen doesn't shout. `.btn-accent` is kept as
     a verbatim ALIAS for v2-era markup. */
  .btn-primary, .btn-accent { background: var(--ui-accent); color: var(--ui-accent-fg); border: 1px solid transparent; font-weight: var(--weight-semibold); box-shadow: var(--elevation-1); }
  .btn-primary:not(:disabled):hover, .btn-accent:not(:disabled):hover { background: var(--ui-accent-hover); color: var(--ui-accent-fg); }
  .btn-primary:not(:disabled):active, .btn-accent:not(:disabled):active { transform: translateY(1px); box-shadow: none; }
  .btn-primary[aria-busy="true"]::after, .btn-accent[aria-busy="true"]::after { color: var(--ui-accent-fg); }
  /* Accent-outlined secondary action (save-as-asset, view). The LABEL takes
     --ui-accent-text, not --ui-accent: the latter is the FILL tone (it is
     tuned to carry white ON it, so as ink on a light canvas it only reached
     3.70:1). --ui-accent-text is the accent already mixed toward --foreground
     for exactly this job, and it inverts correctly in dark mode. Same reason
     hover keeps the text token instead of --ui-accent-hover (the darker FILL,
     which on a dark surface is accent-on-accent). */
  .btn-outline-accent { background: transparent; color: var(--ui-accent-text); border: 1px solid color-mix(in oklab, var(--ui-accent) 45%, var(--surface-raised)); }
  .btn-outline-accent:hover:not(:disabled) { background: color-mix(in oklab, var(--ui-accent) 14%, var(--surface-raised)); color: var(--ui-accent-text); }
  /* Secondary — the neutral OUTLINE sibling (v3: same family as the default
     .btn; hierarchy comes from the one solid primary vs everything bordered). */
  .btn-secondary { background: var(--btn-outline-bg); color: var(--btn-outline-fg); border: 1px solid var(--btn-outline-border); box-shadow: var(--elevation-1); }
  .btn-secondary:not(:disabled):hover { background: var(--btn-outline-bg-hover); border-color: var(--btn-outline-border-hover); }
  /* Ghost — the QUIET, neutral button (cancel / toolbar / icon). Neutral fg so
     it does NOT carry the brand-blue accent; only the solid/selected states do. */
  .btn-ghost { background: transparent; border-color: transparent; box-shadow: none; color: var(--secondary-foreground); }
  .btn-ghost:not(:disabled):hover { background: var(--state-hover); border-color: transparent; box-shadow: none; color: var(--foreground); }
  .btn-ghost:not(:disabled):active { background: var(--state-pressed); transform: none; box-shadow: none; }
  /* Danger — SOFT destructive (tinted fill, no shouting border); the solid
     variant below stays for the truly terminal action.
     The label is --danger-TEXT, not --danger: this is a tonal button, so its
     ink sits on its own 12%/20% tint, exactly where the fill tone is unreadable
     (3.74:1 resting, 3.38:1 hover). Its neutral sibling .btn[aria-pressed] has
     always used the ink step (--btn-tonal-fg) for the same reason. */
  .btn-danger { background: var(--danger-bg); color: var(--danger-text); border-color: transparent; box-shadow: none; }
  .btn-danger:not(:disabled):hover { background: color-mix(in oklab, var(--danger) 20%, var(--surface)); border-color: transparent; }
  .btn-danger:focus-visible, .btn-danger-solid:focus-visible { outline: var(--focus-outline-danger); }
  .btn-danger-solid { background: var(--danger); color: var(--danger-foreground); border: 1px solid transparent; box-shadow: var(--shadow-card); }
  .btn-danger-solid:not(:disabled):hover { background: var(--danger); filter: brightness(0.95); }
  .btn-danger-solid:not(:disabled):active { filter: brightness(0.9); }
  .btn[aria-pressed="true"], .btn.is-selected { background: var(--btn-tonal-selected-bg); border-color: var(--btn-tonal-border-hover); color: var(--btn-tonal-fg); box-shadow: none; }
  .btn-primary[aria-pressed="true"], .btn-primary.is-selected,
  .btn-accent[aria-pressed="true"], .btn-accent.is-selected { background: var(--ui-accent-hover); border-color: transparent; color: var(--ui-accent-fg); }
  .btn-sm { min-height: var(--control-h-sm); padding: 0 0.625rem; font-size: var(--text-micro); }
  .btn-lg { min-height: var(--control-h-lg); padding: 0 var(--sp-4); font-size: var(--text-body); }
  .btn-icon { padding: 0; width: var(--c-btn-h); aspect-ratio: 1; }
  .btn-icon.btn-sm { width: var(--control-h-sm); }
  .btn-icon.btn-lg { width: var(--control-h-lg); }
  .btn-icon:not(:disabled):hover { transform: none; box-shadow: none; }

  /* ---- Cards ---- */
  .card { border: 1px solid var(--border); border-radius: var(--c-card-radius); background: var(--card); color: var(--card-foreground); box-shadow: var(--c-card-shadow); transition: border-color var(--duration-normal) var(--ease-out), box-shadow var(--duration-normal) var(--ease-out), translate var(--duration-normal) var(--ease-out); }
  .card-pad { padding: var(--c-card-pad); }
  .card-header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); border-block-end: 1px solid var(--border); background: var(--surface-sunken); }
  .card-body { padding: var(--sp-4); }
  /* Interactive cards lift 1px on hover (translate, not transform — composes
     with any consumer transform) with a soft mid shadow; the old overlay-grade
     float shadow read as heavy next to the reference consoles. */
  .card-hover:hover { border-color: var(--border-strong); box-shadow: var(--elevation-2); translate: 0 -1px; }
  /* v3: the active card is ringed in the accent, not the neutral gray ring. */
  .card-active { border-color: var(--ui-accent); box-shadow: 0 0 0 2px color-mix(in oklab, var(--ui-accent) 26%, transparent); }

  /* ---- 主对话框 (composer) ----
     全平台只有一个「说点什么」的盒子:AI对话、AI短剧、以后任何频道都用这一份。
     形状由 lib 说了算,频道只往里放内容 —— 每个频道自己画一个盒子,就是 operator
     看到的「样式还是不一样」。

       <div class="composer">
         <div class="composer-lead">…chips / 附件…</div>        (可选,排在最上)
         <textarea class="composer-input">                      (或任意输入组件)
         <div class="composer-toolbar">
           <div class="composer-toolbar-group">…模式 / 模型 / 参数…</div>
           <div class="composer-toolbar-group">…引用 / 发送…</div>
         </div>
       </div>

     盒子不投影、不给自己内边距:内边距在输入区和工具条上,这样输入区的可点区域
     一直顶到盒子边缘 —— 点“空白”也能开始打字。 */
  .composer { display: flex; flex-direction: column; inline-size: 100%; min-inline-size: 0; background: var(--c-composer-bg); border: 1px solid var(--border); border-radius: var(--c-composer-radius); transition: border-color var(--duration-fast) var(--ease-standard); }
  .composer:focus-within { border-color: var(--ring); }
  .composer-lead { order: -1; display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1); padding: var(--sp-3) var(--sp-4) 0; }
  /* 输入区:透明、无边框、不可拖拽改大小 —— 盒子才是那个控件。
     `field-sizing: content` 让它随内容长高到 max 后再滚(支持的浏览器上)。 */
  .composer-input { flex: 1 1 auto; min-block-size: var(--c-composer-min-h); max-block-size: var(--c-composer-max-h); padding: 14px var(--sp-4) 6px; border: 0; background: transparent; color: var(--foreground); font: inherit; font-size: var(--c-composer-font); line-height: var(--leading-body); resize: none; field-sizing: content; overflow: auto; }
  .composer-input:focus, .composer-input:focus-visible { outline: none; box-shadow: none; }
  .composer-input::placeholder { color: var(--muted-foreground); }
  .composer-toolbar { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-1); padding: var(--sp-2) var(--sp-3) var(--sp-3); }
  .composer-toolbar-group { display: flex; align-items: center; gap: var(--sp-1); min-inline-size: 0; }
  /* 发送键是圆的 —— 工具条上唯一一个实心圆,眼睛不用找。尺寸写死在 token 上:
     默认的图标按钮跟着 --c-btn-h 走(30px),比 AI对话 那颗小两像素,并排一看就是
     两个设计。 */
  .composer-send { border-radius: var(--radius-full); inline-size: var(--c-composer-send-size); block-size: var(--c-composer-send-size); min-block-size: var(--c-composer-send-size); padding: 0; }
}

@layer lib.utilities {
  /* ---- Scrollbar(opt-in) ----
     放在共享配方里,组件采纳后就能用同一条细滚动条 —— 否则 shadow root 里够不着
     system.css,每个有滚动区的组件都露一条系统默认的粗条。 */
  .scroll-thin { scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
  .scroll-thin::-webkit-scrollbar { inline-size: 6px; block-size: 6px; }
  .scroll-thin::-webkit-scrollbar-track { background: transparent; }
  .scroll-thin::-webkit-scrollbar-thumb { background: var(--border); border-radius: var(--radius-full); }
  .scroll-thin::-webkit-scrollbar-thumb:hover { background: var(--border-strong); }
}

@layer lib.reset {
  /* ONE global reduced-motion guard — neutralizes lib motion DURATION for users
     who ask for it (a11y). Leaves end-states intact so layout doesn't break. */
  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
      animation-duration: 0.001ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.001ms !important;
    }
    /* 但**状态指示性**的动效不能一起冻住:转圈、不定进度、忙态圆环停下来之后,
       页面看起来就是卡死的 —— 用户分不清「在跑」和「挂了」。这类降速循环、
       不取消。装饰性动效才是这条守卫要压的东西。 */
    .spinner, .progress-indeterminate, .progress-indeterminate::after,
    .btn[aria-busy="true"]::after, .skeleton, .skeleton-text,
    .state-switch-busy > i {
      animation-duration: 1.6s !important;
      animation-iteration-count: infinite !important;
    }
  }
}

@layer lib.reset {
  /* ---- 选中文字 / 原生控件配色 ----
     两件都是「不设就退回浏览器默认」的东西,而且都跨不进 shadow DOM(::selection
     不继承;组件内部的原生控件也读不到页面那层的设置)。放在共享配方里,页面与
     组件吃同一份。
     · 选中态:默认是操作系统的蓝色高亮,压在这套暖纸配色上格格不入。
       用 28% 强调色混纸色 —— 量过正文墨色在上面对比度 10.09,读起来毫无压力。
     · accent-color:复选框 / 单选 / 进度条不设就是系统蓝(macOS),与产品强调色不一致。 */
  :where(:root, :host) { accent-color: var(--ui-accent); }
  ::selection { background: color-mix(in oklab, var(--ui-accent) 28%, var(--surface-raised)); color: var(--foreground); }
}
