/* 正本：Linear 【Infra】LYNCA Design Standard（镜像 lynca-agent-runtime/docs/standards/）。本文件只做应用层实现。
   Load order: app/styles/fonts.css → nocturne.css → lynca-tokens.css → asset-img.css →
   listing-copilot.css → workbench-v2.css → commercial-ui.css → this file (last sheet wins).

   ═══ Type ═══════════════════════════════════════════════════════════════════
   The two static Latin faces below are kept as the desk's fast path: they are
   the same family the variable Source Serif 4 in app/styles/fonts.css declares,
   subset to Latin and 100KB in total, and on the writer desk an exact 400/600
   match takes them over the variable file (same origin, §5.5 self-hosted).
   Source Serif is served from this origin, not from Google. Two reasons, both
   of which made the linked version render as nothing at all: the site's CSP is
   style-src 'self', so the stylesheet was refused outright -- document.fonts on
   production held zero faces and every heading fell back to whatever the OS had
   -- and the writers are in China, where fonts.googleapis.com does not resolve,
   so loosening the CSP would only have traded a blocked request for a hanging
   one. Latin subset, two weights, 100KB total.

   CJK stays on the system serif. Noto Serif SC is several megabytes; on a page
   whose whole point is how fast a title comes back, that is not a trade worth
   making, and 宋体 is already what macOS and Windows reach for.
   ══════════════════════════════════════════════════════════════════════════ */
@font-face {
  font-family: "Source Serif 4";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/app/fonts/source-serif-4-400.woff2") format("woff2");
}

@font-face {
  font-family: "Source Serif 4";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/app/fonts/source-serif-4-600.woff2") format("woff2");
}

/* Runtime batch table wearing Listing Copilot chrome.
   DOM and event IDs stay in app.js; this layer only restyles them. */

.commercial-workbench {
  min-height: 100vh;
  color: var(--theme-ink, var(--wb-ink, var(--color-text)));
}

.commercial-workbench .page-shell {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  padding-bottom: 0;
}

.commercial-workbench header.app-header {
  min-height: 72px;
  height: auto;
}

.commercial-workbench #registerLink {
  color: inherit;
  text-decoration: none;
}

.commercial-workbench nav#batches {
  display: flex;
  gap: 8px;
  padding: 12px 20px;
  border-bottom: 1px solid var(--wb-line, var(--theme-line, var(--color-neutral-800)));
  background: transparent;
  overflow: auto;
}

.commercial-workbench .batch {
  background: var(--wb-surface, var(--theme-surface, var(--color-surface)));
  color: var(--wb-ink, var(--theme-ink, var(--color-text)));
  border: 1px solid var(--wb-line, var(--theme-line, var(--color-neutral-800)));
}

.commercial-workbench .batch.active {
  border-color: var(--wb-accent, var(--theme-accent-bright, var(--color-accent)));
  box-shadow: 0 0 0 1px var(--color-accent);
}

.commercial-workbench .batch small {
  color: var(--wb-muted, var(--theme-muted, var(--color-neutral-400)));
}

.commercial-workbench main.runtime-table-workspace.workspace {
  display: grid;
  grid-template-columns: 270px minmax(560px, 1fr) 280px;
  width: 100%;
  max-width: none;
  flex: 1;
  min-height: 0;
  margin: 0;
  padding: 0;
  background: transparent;
}

.commercial-workbench main.runtime-table-workspace > aside,
.commercial-workbench main.runtime-table-workspace > section {
  background: var(--color-surface);
  border-right: 1px solid var(--wb-line, var(--theme-line, var(--color-neutral-800)));
  color: inherit;
}

.commercial-workbench main.runtime-table-workspace h1,
.commercial-workbench main.runtime-table-workspace h2,
.commercial-workbench main.runtime-table-workspace h3 {
  color: var(--wb-ink, var(--theme-ink, var(--color-text)));
}

.commercial-workbench .section-title h1 {
  margin: 4px 0 20px;
  font-size: 20px;
  letter-spacing: -0.02em;
}

.commercial-workbench .section-title small,
.commercial-workbench aside label,
.commercial-workbench .hint,
.commercial-workbench .batch-context-label,
.commercial-workbench dt,
.commercial-workbench .file-drop span,
.commercial-workbench .pair-card small {
  color: var(--wb-muted, var(--theme-muted, var(--color-neutral-400)));
}

.commercial-workbench input,
.commercial-workbench textarea,
.commercial-workbench select,
.commercial-workbench #batchContext,
.commercial-workbench #descriptionText {
  background: var(--color-surface);
  color: var(--wb-ink, var(--theme-ink, var(--color-text)));
  border: 1px solid var(--wb-line, var(--theme-line, var(--color-neutral-800)));
}

/* A blanket rule painted every button with a solid accent fill and near-black
   text. It reads fine on the primary action and nowhere else: the queue chips
   are buttons too, and on their translucent fill this produced near-black text
   on a near-black panel. Scope the filled treatment to the buttons that want it
   and let the rest inherit readable ink. */
.commercial-workbench button {
  background: transparent;
  color: var(--wb-ink, var(--theme-ink, var(--color-text)));
}

.commercial-workbench button.primary-button,
.commercial-workbench button.primary,
.commercial-workbench button[type="submit"] {
  background: transparent;
  color: var(--wb-accent, var(--theme-accent, var(--color-accent)));
  border: 1px solid var(--wb-accent, var(--theme-accent, var(--color-accent)));
}

.commercial-workbench button.secondary {
  background: transparent;
  color: var(--wb-ink, var(--theme-ink, var(--color-text)));
  border: 1px solid var(--wb-line-strong, var(--theme-line-strong, var(--color-neutral-700)));
}

.commercial-workbench .file-drop {
  border: 1px dashed var(--wb-line-strong, var(--theme-line-strong, var(--color-neutral-700)));
  background: var(--color-surface);
  color: var(--wb-ink, var(--theme-ink, var(--color-text))) !important;
}

.commercial-workbench .hint.legibility {
  background: var(--color-generated-900);
  color: var(--wb-warning, var(--theme-warning, var(--color-generated)));
}

.commercial-workbench .pair-status {
  background: var(--color-surface);
  color: var(--wb-muted, var(--theme-muted, var(--color-neutral-400)));
}

.commercial-workbench .pair-status.ready {
  background: var(--color-verified-900);
  color: var(--wb-success, var(--theme-success, var(--color-verified)));
}

.commercial-workbench .pair-status.error {
  background: var(--color-rejected-900);
  color: var(--wb-danger, var(--theme-danger, var(--color-rejected-300)));
}

.commercial-workbench .pair-card {
  background: var(--color-surface);
  border-color: var(--wb-line, var(--theme-line, var(--color-neutral-800)));
}

.commercial-workbench .upload-step > b {
  background: var(--color-accent-800);
  color: var(--wb-accent, var(--theme-accent-bright, var(--color-accent)));
}

.commercial-workbench .table-wrap {
  border-color: var(--wb-line, var(--theme-line, var(--color-neutral-800)));
  background: var(--color-surface);
}

.commercial-workbench th {
  color: var(--wb-muted, var(--theme-muted, var(--color-neutral-400)));
  background: var(--color-surface);
}

.commercial-workbench td {
  border-bottom-color: var(--wb-line, var(--theme-line, var(--color-neutral-800)));
}

.commercial-workbench tbody tr.selected {
  background: var(--color-accent-900);
}

.commercial-workbench .aspects td {
  border-top-color: var(--wb-line, var(--theme-line, var(--color-neutral-800)));
}

.commercial-workbench .glass-box td,
.commercial-workbench .glass-box th {
  border-top-color: var(--wb-line, var(--theme-line, var(--color-neutral-800)));
}

.commercial-workbench footer {
  background: var(--wb-header, var(--theme-header-glass, var(--color-surface)));
  border-top: 1px solid var(--wb-line, var(--theme-line, var(--color-neutral-800)));
  color: inherit;
}

.commercial-workbench .legal-nav,
.commercial-workbench .legal-nav a {
  color: var(--wb-muted, var(--theme-muted, var(--color-neutral-400)));
}

.commercial-auth .tos-accept,
.commercial-auth .auth-check-row {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 4px 0 0;
  color: var(--theme-muted, var(--color-neutral-400));
  font-size: 14px;
  line-height: 1.6;
  font-weight: 600;
}

.commercial-auth .tos-accept input,
.commercial-auth .auth-check-row input {
  width: auto;
  min-height: 0;
  margin-top: 3px;
  flex: none;
}

.commercial-auth .tos-accept a,
.commercial-auth .auth-check-row a {
  color: var(--theme-accent-bright, var(--color-accent));
  text-decoration: underline;
}

@media (max-width: 1000px) {
  .commercial-workbench main.runtime-table-workspace.workspace {
    grid-template-columns: 240px 1fr;
  }
}

.commercial-workbench footer.workbench-legal {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
  padding: 12px 32px 20px;
  border-top: 1px solid var(--wb-line, var(--theme-line, var(--color-neutral-800)));
  color: var(--wb-muted, var(--theme-muted, var(--color-neutral-400)));
}

.commercial-workbench footer.workbench-legal a {
  color: inherit;
}

.commercial-workbench .workspace-queue-heading {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.commercial-workbench .workspace-queue-heading strong {
  color: var(--wb-ink, var(--theme-ink, var(--color-text)));
  font-size: 14px;
  font-weight: 700;
}

.commercial-workbench .workspace-queue-heading p {
  margin: 0;
  color: var(--wb-muted, var(--theme-muted, var(--color-neutral-400)));
  font-size: 14px;
}

.commercial-workbench .asset-board,
.commercial-workbench .asset-preview-list {
  max-width: 100%;
  overflow-x: hidden;
}

.commercial-workbench .workspace[data-batch-state="ready"] .workspace-queue-heading strong {
  display: none;
}

/* Session-ready must not hide 从卡片到世界. Production /api/session can hang,
   leaving data-session-ready="false"; the hero is already in the HTML. */

/* Landing slogan — exact copy in #workbenchTitle. Later type-scale and
   520px rules shrank it to 22–26px, so the header read as the product and
   the slogan sat under Writer Workbench. Empty desk only; ready hides intro. */
.commercial-workbench .workspace[data-batch-state="empty"] .workspace-intro,
.commercial-workbench .workspace[data-session-ready="false"] .workspace-intro {
  display: grid;
  visibility: visible;
  opacity: 1;
}

.commercial-workbench .workspace[data-batch-state="empty"] .workspace-intro h1,
.commercial-workbench .workspace[data-session-ready="false"] .workspace-intro h1 {
  display: block;
  visibility: visible;
  opacity: 1;
  font-size: clamp(36px, 6vw, 56px);
  font-weight: 600;
  text-align: center;
}

@media (max-width: 520px) {
  .commercial-workbench .workspace[data-batch-state="empty"] .workspace-intro h1,
  .commercial-workbench .workspace[data-session-ready="false"] .workspace-intro h1 {
    font-size: clamp(32px, 8vw, 40px);
  }
}

/* The toolbar only restates 队列总览 / 正反面完整显示 — already on the pair
   board. Empty-state CSS hid it; ready showed a leftover strip. */
.commercial-workbench .workspace-toolbar {
  display: none;
}

/* The title tray is empty until identify writes something. An empty
   "识别出的标题会汇总在这里" column next to the first card is leftover chrome. */
.commercial-workbench .batch-titles-panel:has(.batch-empty) {
  display: none;
}

.commercial-workbench .control-panel {
  display: block;
}

.commercial-workbench .control-panel #processButton,
.commercial-workbench #processButton {
  display: none;
}

/* The images had 57% of the card and the title 38%. The writer's job here is to
   read a title and correct it; the images are reference for that, not the work
   itself. The two cards need enough width to be legible and no more -- they cap
   at 224px each in the preview rules above -- so the rest belongs to the text
   the writer is actually editing. */
.commercial-workbench .asset-row-card {
  grid-template-columns: minmax(0, 0.72fr) minmax(0, 1.28fr);
  max-width: 100%;
}

/* Main pair on the review card — list thumbs were capped at 220px while this
   column still followed 52vh / uncapped aspect boxes. */
.commercial-workbench .asset-row-card .preview-images {
  max-width: 448px;
}

/* Left as the floor for narrow viewports, but the two bounds are equal now:
   220 tall against 224 wide is the same mismatch that made a landscape card
   read larger than a portrait one, just smaller. */
.commercial-workbench .asset-row-card .preview-images > .thumb-button,
.commercial-workbench .asset-row-card .preview-images > .thumb-button .thumb {
  max-height: 220px;
  max-width: 220px;
}

.commercial-workbench .thumb-button {
  overflow: hidden;
  max-height: 220px;
}

/* The height cap alone left a 1014px-wide frame around a portrait card: with
   object-fit: contain a 2.8/4 image inside it renders about 154px wide and the
   rest is empty box. Cap the frame to what the image can actually fill and
   centre the pair, so the preview reads as two cards rather than two bands. */
.commercial-workbench .preview-images {
  justify-content: center;
}

.commercial-workbench .preview-images > .thumb-button {
  width: 100%;
  max-width: calc(320px * 2.8 / 4);
  margin-inline: auto;
}

.commercial-workbench .preview-images > .thumb-button,
.commercial-workbench .preview-images > .thumb-button .thumb {
  max-height: 220px;
}

.commercial-workbench .preview-images.mixed-orientation > .thumb-button.is-landscape {
  max-width: calc(320px * 4 / 2.8);
}

.commercial-workbench .thumb,
.commercial-workbench .result-thumb {
  width: 100%;
  max-width: 100%;
  height: auto;
  max-height: 220px;
  aspect-ratio: 2.8 / 4;
  object-fit: contain;
}



.commercial-workbench .preview-images.mixed-orientation {
  grid-template-columns: minmax(0, 1fr);
  gap: 12px;
}

.commercial-workbench .preview-images.mixed-orientation .thumb-button.is-landscape .thumb {
  width: 100%;
  height: auto;
}

@media (max-width: 980px) {
  .commercial-workbench .asset-row-card {
    grid-template-columns: 1fr;
  }
}


/* dd tiles always contain "0" / "—", so :has(dd:not(:empty)) never hid the
   right column. Empty desk is one centered intake; ready desk keeps the grid. */
.commercial-workbench .workspace[data-batch-state="empty"] .intake-grid {
  display: flex;
  justify-content: center;
  width: min(760px, 100%);
  margin-inline: auto;
  grid-template-columns: none;
}

.commercial-workbench .workspace[data-batch-state="empty"] .batch-summary {
  display: none;
}

.commercial-workbench .workspace[data-batch-state="empty"] .drop-zone {
  justify-content: center;
  justify-self: center;
  margin-inline: auto;
}

.commercial-workbench .drop-zone-copy {
  text-align: left;
}

.commercial-workbench .workspace[data-batch-state="empty"] .drop-zone,
.commercial-workbench .workspace[data-batch-state="empty"] .drop-zone-copy {
  justify-content: center;
  text-align: center;
}

/* The queue rail sets overflow-x: auto but rendered 3451px wide inside a 1265px
   viewport, taking the whole document with it. A flex item will not shrink below
   its content unless told to, so the scroll container never had a width to
   scroll within. min-width: 0 is the whole fix; the overflow rule was already
   right. */
.commercial-workbench .batch-navigation,
.commercial-workbench .batch-navigation-controls,
.commercial-workbench .batch-rail {
  min-width: 0;
  max-width: 100%;
}

.commercial-workbench .batch-rail {
  overflow-x: auto;
  scrollbar-width: thin;
  overscroll-behavior-x: contain;
}

.commercial-workbench .batch-rail-item {
  flex: 0 0 auto;
}

/* 9px and 10px were doing real work in this UI -- field labels, section kickers,
   the header buttons. Below about 11px Chinese glyphs lose their internal
   strokes on a standard display, and these are the labels that say what a number
   means. */
.commercial-workbench .eyebrow,
.commercial-workbench .section-kicker,
.commercial-workbench .thumb-label,
.commercial-workbench .ghost-button,
.commercial-workbench .batch-summary dt {
  font-size: 12px;
}

.commercial-workbench .batch-summary dt {
  letter-spacing: 0.01em;
}

/* Footer links were 19px tall. Pointer targets need enough height to be hit
   without aiming. */
.commercial-workbench footer a,
.commercial-workbench .legal-links a {
  display: inline-flex;
  min-height: 28px;
  align-items: center;
}

/* /api/listing-image-thumbnail answers 404 runtime_thumbnail_not_found for a
   restored session's derived images. Without an error path the browser renders
   the alt text, so the writer read "front-recognition-thumbnail.jpg" in place of
   a card. Show the frame as a quiet placeholder: the card is still selectable
   and its title still editable, which is what the writer is here for. */
.commercial-workbench .asset-img__well[data-thumb-missing="true"] {
  display: grid;
  min-height: 132px;
  place-items: center;
  box-shadow: none;
  border: 1px dashed var(--color-neutral-700);
}

.commercial-workbench .asset-img__well[data-thumb-missing="true"]::after {
  content: "图片待补";
  color: var(--muted);
  font-size: 12px;
  letter-spacing: 0.01em;
}

/* Queue chips carry the batch status in a colour-coded label. On the dark panel
   the fill has to stay translucent and the ink stay light, or the status word
   disappears into the chip. */
.commercial-workbench .batch-rail-item {
  border: 1px solid var(--wb-line-strong, var(--theme-line-strong));
  background: var(--color-neutral-900);
  color: var(--wb-ink, var(--theme-ink));
}

.commercial-workbench .batch-rail-item.is-visible,
.commercial-workbench .batch-rail-item.is-selected {
  border-color: var(--wb-accent, var(--theme-accent));
  background: var(--color-accent-900);
  color: var(--wb-ink, var(--theme-ink));
}

.commercial-workbench .batch-rail-item small {
  color: var(--wb-muted, var(--theme-muted));
}

.commercial-workbench .batch-rail-item[data-batch-status="saved"] small {
  color: var(--wb-success, var(--theme-success));
}

.commercial-workbench .batch-rail-item[data-batch-status="failed"] small {
  color: var(--wb-danger, var(--theme-danger));
}

.commercial-workbench .batch-rail-item[data-batch-status="rejected"] small {
  color: var(--wb-warning, var(--theme-warning));
}

/* ── Surface treatment ────────────────────────────────────────────────────────
   Flat fills read as plastic. Every raised surface here gets the four layers the
   lynca-app shell uses: a gradient instead of one colour, a hairline of light on
   the top edge, a deep soft shadow, and blur behind. Ported rather than invented
   so the workbench and the agent shell are recognisably the same product. */

.commercial-workbench .batch-summary,
.commercial-workbench .asset-row-card,
.commercial-workbench .title-output,
.commercial-workbench .thumb-button,
.commercial-workbench .batch-navigation,
.commercial-workbench .queue-overview,
/* .asset-workbench holds the rail and the card; both carry their own surface,
   so filling it as well drew a box around boxes -- a full-width rectangle a
   shade lighter than the page, with hard edges, which is the band that kept
   coming back. Deleted here rather than overridden further down: three separate
   rules were painting this one element, and adding a fourth is how it survived
   two attempts at removing it. */
.commercial-workbench .asset-workbench {
  border: 0;
  background: none;
}

/* The drop zone keeps its dashed edge -- that is what says "put files here" --
   but sits on the same material as everything else. */

/* The card under review is the one surface that should feel closer to the
   reader, so it takes the denser core and the deeper shadow. */
.commercial-workbench .title-output {
  background: var(--color-surface);
  box-shadow: var(--shadow-md);
}

.commercial-workbench .batch-rail-item {
  border-radius: var(--glass-radius, 9px);
  box-shadow: none;
}

.commercial-workbench .batch-rail-item.is-visible,
.commercial-workbench .batch-rail-item.is-selected {
  box-shadow: var(--shadow-sm);
}

/* Two blurred colour fields behind the page. Without them a dark UI is a flat
   sheet; with them the panels have something to sit on and the blur has
   something to pick up. Non-interactive and behind everything. */
.commercial-workbench .workspace {
  position: relative;
  isolation: isolate;
}

.commercial-workbench .workspace::before,
.commercial-workbench .workspace::after {
  position: absolute;
  z-index: -1;
  border-radius: 50%;
  content: "";
  filter: blur(64px);
  pointer-events: none;
}

/* Two glows covered 860px of a 4892px page, so 82% of the panels had pure black
   behind them. backdrop-filter samples what is actually there: over black it
   returns black, the white fill lands on top, and white over black is grey. That
   is the dust -- not the fill value, the absence of anything to sample.

   A single fixed field behind the whole workspace gives every panel something to
   pick up, at any scroll position, for one paint. */
.commercial-workbench .workspace::before {
  position: fixed;
  inset: 0;
  width: auto;
  height: auto;
  border-radius: 0;
  filter: none;
  background: var(--color-surface);
}

.commercial-workbench .workspace::after {
  content: none;
}

/* The header floats over that field rather than capping it with a solid bar. */
.commercial-workbench .app-header,
.commercial-workbench header {
  background: var(--color-surface);
  box-shadow: none;
}

@media (prefers-reduced-transparency: reduce) {
  .commercial-workbench .batch-summary,
  .commercial-workbench .asset-row-card,
  .commercial-workbench .title-output,
  .commercial-workbench .thumb-button,

  .commercial-workbench .workspace::before,
  .commercial-workbench .workspace::after {
    display: none;
  }
}

/* ── Press feedback ───────────────────────────────────────────────────────────
   The commercial skin had no :active state anywhere, so every pressable thing
   in the workbench — queue chips, 上一组/下一组, 复制/已保存/拒绝 — took a click
   with no acknowledgement. A button that does not answer the press reads as
   broken even when it worked. Subtle scale, fast, on the transform only. */
.commercial-workbench button,
.commercial-workbench .batch-rail-item,
.commercial-workbench .thumb-button,
.commercial-workbench [role="button"] {
  transition: transform 160ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)),
              background 160ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)),
              border-color 160ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1));
}

.commercial-workbench button:active:not(:disabled),
.commercial-workbench .batch-rail-item:active,
.commercial-workbench [role="button"]:active {
  transform: scale(0.97);
}

/* The image frames are larger, so the same 0.97 reads as a bigger jump; go
   lighter on them. */
.commercial-workbench .thumb-button:active {
  transform: scale(0.99);
}

@media (prefers-reduced-motion: reduce) {
  .commercial-workbench button,
  .commercial-workbench .batch-rail-item,
  .commercial-workbench .thumb-button,
  .commercial-workbench [role="button"] {
    transition: none;
  }

  .commercial-workbench button:active:not(:disabled),
  .commercial-workbench .batch-rail-item:active,
  .commercial-workbench .thumb-button:active,
  .commercial-workbench [role="button"]:active {
    transform: none;
  }
}

/* The intake grid stretched both columns to the taller one: 当前批次 needs 190px,
   the drop zone needs 84, and the remaining 104px rendered as dead space under a
   dashed box that looked twice the size it needed to be. Let each column take
   its own height. */
.commercial-workbench .intake-grid {
  align-items: start;
}

/* The dashed box measured 863x84 around 236x39 of content -- 34% filled across,
   47% down. A target that large around a label that small does not read as
   generous, it reads as unfinished. Size it to its content and centre it, the
   way the agent shell sizes its action pills. */

.commercial-workbench .workspace[data-batch-state="empty"] .drop-zone {
  min-width: min(520px, 100%);
  padding: 20px 32px;
}

/* With the box no longer stretching, the panel around it has nothing left to
   pad out. */
.commercial-workbench .control-panel {
  display: grid;
  align-content: start;
}

/* margin-inline: auto was being overridden by the grid's own placement, leaving
   204px of gap on one side and 240 on the other. Place it explicitly. */
.commercial-workbench .control-panel {
  justify-items: center;
}

/* The left column was sized 1.55fr for a box that now wants 420px, so the drop
   zone floated in the middle of a column far wider than itself. Size the columns
   to what they hold: the intake target, then the batch panel. */
.commercial-workbench .intake-grid {
  grid-template-columns: minmax(0, 1fr) minmax(300px, 340px);
}

/* Standard §8: one column on phones. The unconditional rule above used to win over the
   earlier sheets' breakpoints, which left a 90px drop zone with vertical text at 390px. */
@media (max-width: 640px) {
  .commercial-workbench .intake-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* Two earlier layers both set the drop zone's display and disagreed about it:
   one wanted a two-column grid, the other place-items: center start. The result
   put the icon and copy left of the box's own centre, so a geometrically
   centred box still looked off. State it once: icon and copy as one centred
   row. */

/* justify-items on the panel left a 36px imbalance -- the panel carries a
   view-transition-name, which gives it its own containing block and shifts what
   auto margins resolve against. Place the box against the grid track itself. */
.commercial-workbench .control-panel > .drop-zone {
  justify-self: center;
}

/* Queue chips carried the status as a word: 23 of 45 read 已入库, so the rail was
   mostly one repeated phrase competing with the numbers that actually differ.
   The word stays on aria-label and title; the eye gets a dot. */
.commercial-workbench .batch-rail-item {
  display: grid;
  min-width: 46px;
  justify-items: center;
  gap: 4px;
  padding: 8px 8px;
  font-variant-numeric: tabular-nums;
}

.commercial-workbench .batch-rail-item > span {
  font-size: 14px;
  font-weight: 600;
  line-height: 1.2;
}

.commercial-workbench .batch-rail-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--wb-muted, var(--theme-muted));
  opacity: 0.55;
}

.commercial-workbench .batch-rail-item[data-batch-status="saved"] .batch-rail-dot {
  background: var(--wb-success, var(--theme-success));
  opacity: 1;
}

.commercial-workbench .batch-rail-item[data-batch-status="failed"] .batch-rail-dot {
  background: var(--wb-danger, var(--theme-danger));
  opacity: 1;
}

.commercial-workbench .batch-rail-item[data-batch-status="rejected"] .batch-rail-dot {
  background: var(--wb-warning, var(--theme-warning));
  opacity: 1;
}

/* Colour alone should not be the only carrier. A failed card also gets a ring,
   so the three states stay apart for anyone who cannot separate the hues. */
.commercial-workbench .batch-rail-item[data-batch-status="failed"] .batch-rail-dot {
  box-shadow: 0 0 0 1px var(--color-rejected-500);
}

/* The intake target is the primary action on an empty desk and was sized like a
   caption. Give it room. */

.commercial-workbench .drop-zone-icon {
  width: 52px;
  height: 52px;
}

.commercial-workbench .drop-zone-icon svg {
  width: 24px;
}

.commercial-workbench .drop-zone-copy strong {
  font-size: 14px;
}

/* ── Intake target ────────────────────────────────────────────────────────────
   A dashed outline says "drop here"; a filled panel says "this is a surface".
   Having both made the dashed box read as a card with a border drawn on it. The
   target keeps the outline and loses the fill. */

/* It should answer the pointer, which is also what stops it reading as empty:
   the target lights rather than sits there. */

.commercial-workbench .drop-zone-icon {
  border-color: var(--color-accent-700);
  background: var(--color-accent-900);
  box-shadow: none;
  color: var(--theme-accent-bright);
}

/* ── Status dots ──────────────────────────────────────────────────────────────
   A flat 6px circle is a dot; these should read as an indicator. Same recipe as
   the panels: a light source above, a soft halo below, a hairline containing
   it. */
.commercial-workbench .batch-rail-dot {
  position: relative;
  width: 7px;
  height: 7px;
  background: var(--theme-muted);
  box-shadow: none;
}

.commercial-workbench .batch-rail-item[data-batch-status="saved"] .batch-rail-dot {
  background: var(--theme-success);
  box-shadow: 0 0 0 1px var(--color-accent);
}

.commercial-workbench .batch-rail-item[data-batch-status="failed"] .batch-rail-dot {
  background: var(--theme-danger);
  box-shadow: 0 0 0 1px var(--color-rejected-500);
}

.commercial-workbench .batch-rail-item[data-batch-status="rejected"] .batch-rail-dot {
  background: var(--theme-warning);
  box-shadow: 0 0 0 1px var(--color-generated-500);
}

/* ── Type scale ───────────────────────────────────────────────────────────────
   The hero carries a 54px headline with -0.055em tracking, and it is hidden the
   moment a batch loads -- so the working state, which is where a writer spends
   all their time, had nothing above 16px and read as one flat sheet of small
   text. Give the section headings the same treatment at working scale. */
.commercial-workbench .workspace[data-batch-state="ready"] .queue-overview strong,
.commercial-workbench .asset-board-title,
.commercial-workbench .batch-navigation-summary strong {
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.2;;
}

.commercial-workbench .section-kicker,
.commercial-workbench .eyebrow {
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  opacity: 0.68;
}

/* The batch numbers are the one place a glance lands; let them carry weight. */
.commercial-workbench .batch-summary dd {
  font-size: 26px;
  font-weight: 600;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

/* Touch reports a hover on tap, so any hover that moves an element leaves it
   stuck in the hovered state after a finger lifts. Gate movement on a real
   pointer; colour and border changes are fine everywhere. */
@media (hover: hover) and (pointer: fine) {
  .commercial-workbench .thumb-button:hover {
    border-color: var(--theme-line-strong);
  }
}

/* Six curves, three of them near-identical, and eleven durations between 100
   and 300ms with no intent behind the differences. One set, named by role. */
:root {
  /* Motion (§7): durations are the standard tokens; easings come from lynca-tokens.css. */
  --duration-press: var(--motion-fast);
  --duration-fast: var(--motion-fast);
  --duration-base: var(--motion-standard);
  --duration-slow: var(--motion-standard);
}

/* ── Buttons and glass, de-greyed ─────────────────────────────────────────────
   Two separate causes of the same "dusty" look.

   1. The action buttons rendered on a solid white background — a light-mode
      style surviving into a dark surface, which on this palette is a plate of
      white sitting in the middle of the card.
   2. The glass fill layered plain white over near-black. White on black is grey;
      it never reads as glass no matter how much blur sits under it. The
      reference fill carries a violet component (rgba(116,105,196,.032)) and,
      more importantly, its panels have a lit background to pick up. Ours mostly
      do not, so the fill has to supply its own colour. */
.commercial-workbench .title-actions .copy-button,
.commercial-workbench .copy-button,
.commercial-workbench .secondary-button {
  /* Nocturne .btn-secondary: neutral outline on the panel, no fill; hover lifts one ramp step. */
  border: 1px solid var(--color-neutral-700);
  background: transparent;
  color: var(--theme-ink);
  box-shadow: none;
}

.commercial-workbench .title-actions .copy-button:hover,
.commercial-workbench .copy-button:hover,
.commercial-workbench .secondary-button:hover {
  border-color: var(--color-neutral-500);
  background: var(--color-neutral-900);
}

.commercial-workbench .copy-button:disabled {
  border-color: var(--color-neutral-900);
  background: transparent;
  color: var(--theme-subtle);
}

/* The tinted fill was wrong. A colour census of the reference shell says its
   surfaces are white at .018-.09 over near-black -- 40+ declarations of it --
   and violet appears only as border, shadow and glow, at .055-.25, with a
   different value almost every time. The purple is punctuation, not the paper.
   Painting the fill violet made every panel the same lilac wash, which is why
   it read as flat: nothing was left to accent against.

   So the fill goes back to white, kept low, with the faintest violet only at the
   far end of the gradient. What makes it read as glass is the edge and the light
   above it, not the body. */
:root {
  /* Standard §5.3: a panel is one flat --color-surface with a --shadow-sm hairline (COS-77). */
  --glass-fill: var(--color-surface);
  --glass-core: var(--color-surface);
  --glass-line: var(--color-neutral-800);
}

/* With the column wider, the editing field should use it: one line of an eBay
   title is 80 characters and it was wrapping into a 469px box three lines deep. */
.commercial-workbench .title-output textarea,
.commercial-workbench [data-title-input] {
  min-height: 66px;
  line-height: 1.4;
}

/* ── Typography ───────────────────────────────────────────────────────────────
   Claude pairs a serif reading face with a UI sans, and the serif is what gives
   it the narrative quality. Same split here: the serif carries anything the
   writer reads as language -- headings, and the title they are editing -- while
   numbers, labels and controls stay sans, because those are scanned rather than
   read and a serif slows that down.

   In Chinese a serif is 宋体, which is precisely the voice being asked for. The
   CJK glyphs come from whatever serif the reader's system has -- Noto Serif SC
   if it is installed, otherwise Songti SC on macOS and SimSun on Windows. None
   of them is downloaded: see the @font-face note at the top of this file.

   Weights come down with it. The audit found 27 declarations at 700-850 against
   10 below 560; that heaviness is most of why the page read as blunt. A serif at
   600 carries more authority than a sans at 800, and needs less. */
:root {
  --font-serif: var(--font-display);   /* lynca-tokens.css: Source Serif 4 + CJK serif fallbacks */
  --font-sans: var(--font-body);       /* Inter + CJK sans fallbacks */
}

.commercial-workbench .workspace-intro h1,
.commercial-workbench .queue-overview strong,
.commercial-workbench .asset-board-title,
.commercial-workbench .batch-navigation-summary strong,
.commercial-workbench .batch-summary-heading strong,
.commercial-workbench h1,
.commercial-workbench h2,
.commercial-workbench h3 {
  font-family: var(--font-serif);
  font-weight: 600;
  letter-spacing: -0.01em;
}

.commercial-workbench .workspace-intro h1 {
  font-weight: 600;
  letter-spacing: -0.04em;
  line-height: 1.2;
}

/* The title under edit is the one long string a writer actually reads on this
   page. It gets the serif and the room to breathe. */
.commercial-workbench .title-output textarea,
.commercial-workbench [data-title-input] {
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: 16px;
  line-height: 1.6;
  letter-spacing: 0.0em;
}

.commercial-workbench .workspace-intro .subtitle,
.commercial-workbench .queue-overview p,
.commercial-workbench .drop-zone-copy > span {
  font-family: var(--font-serif);
  font-weight: 400;
  line-height: 1.6;
}

.commercial-workbench .drop-zone-copy strong {
  font-family: var(--font-serif);
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* Scanned, not read: figures, field labels, chips, buttons. These stay sans and
   keep tabular figures so columns of numbers line up. */
.commercial-workbench .batch-summary dd,
.commercial-workbench .batch-rail-item,
.commercial-workbench button,
.commercial-workbench .section-kicker,
.commercial-workbench .eyebrow,
.commercial-workbench .batch-summary dt,
.commercial-workbench .confidence-badge,
.commercial-workbench .thumb-label {
  font-family: var(--font-sans);
}

/* Lighter across the board. 700-850 was doing the work that size and a serif
   should be doing. */
.commercial-workbench .batch-summary dd {
  font-weight: 600;
}

.commercial-workbench .confidence-badge,
.commercial-workbench .section-kicker,
.commercial-workbench .eyebrow {
  font-weight: 600;
}

.commercial-workbench button {
  font-weight: 600;
}

/* ═══ Design system ═══════════════════════════════════════════════════════════
   An audit of the rendered page found 27 distinct spacing values (3,4,5,6,8,9,
   10,11,12,13,14,15,16,18,24,28…), six border radii, and letter-spacing that
   contradicts itself at the same size — four different tracking values at 9px,
   five at 12px. That is the actual reason the page reads as undesigned. Claude
   and Apple do not look considered because of decoration; they look considered
   because every value sits on a scale somebody can defend.

   Two scales, and everything snaps to them. */
:root {
  /* Spacing: the six local steps snap to the Nocturne scale (standard §5.4). */
  --s1: var(--space-1);
  --s2: var(--space-2);
  --s3: var(--space-4);
  --s4: var(--space-6);
  --s5: var(--space-12);
  --s6: var(--space-16);

  /* Radius: three, by surface size, from the Nocturne radius tokens. */
  --r-chip: var(--radius-md);
  --r-panel: var(--radius-lg);
  --r-sheet: var(--radius-lg);
}

/* Tracking: the desk is Chinese UI, and the standard (§5.5) sets letter-spacing 0 for
   Chinese text -- one value, no size-specific tightening or opening. */
.commercial-workbench :is(h1, h2, h3, p, button, textarea, .sub, .subtitle, .section-kicker, .eyebrow,
  .batch-rail-item, .confidence-badge, .thumb-label, .batch-summary dt, .batch-summary dd) { letter-spacing: 0; }

/* Leading tracks size inversely (Apple §15). */
.commercial-workbench .workspace-intro h1 { line-height: 1.2; }
.commercial-workbench :is(h2, h3, .queue-overview strong) { line-height: 1.2; }
.commercial-workbench :is(p, .sub, .subtitle) { line-height: 1.6; }

/* Radius by surface size, replacing the six ad-hoc values. */
.commercial-workbench :is(.batch-rail-item, button, .confidence-badge, .drop-zone-icon) { border-radius: var(--r-chip); }
.commercial-workbench :is(.batch-summary, .asset-row-card, .title-output, .batch-navigation, .thumb-button, .drop-zone) { border-radius: var(--r-panel); }
.commercial-workbench :is(.modal, .image-modal, dialog) { border-radius: var(--r-sheet); }

/* Spacing on the scale, at the seams that carry the page's rhythm. */
.commercial-workbench .intake-grid { gap: var(--s4); }
.commercial-workbench .batch-summary { padding: var(--s4); gap: var(--s3); }
.commercial-workbench .asset-row-card { gap: var(--s4); padding: var(--s4); }
.commercial-workbench .batch-navigation { padding: var(--s4); gap: var(--s3); }
.commercial-workbench .batch-rail { gap: var(--s2); }
.commercial-workbench .title-output { padding: var(--s4); gap: var(--s3); }
.commercial-workbench .title-actions { gap: var(--s2); }
.commercial-workbench .preview-images { gap: var(--s3); }

/* ═══ Surface layers ══════════════════════════════════════════════════════════
   The rendered page carried 16 background colours and 10 border colours. Four of
   the borders were rgba(196,190,255) at .14 / .18 / .30 plus rgba(178,168,255)
   at .16 -- indistinguishable to the eye, yet enough that no two panels matched.
   That is what makes a page feel unresolved: not a wrong hue, but the absence of
   a repeating one.

   Three layers, the way Apple stacks materials (§12): the page, the panels that
   sit on it, and the controls that sit on those. Each has exactly one fill and
   one edge. */
:root {
  --layer-0: var(--color-bg);                        /* the page */
  --layer-1-fill: var(--color-surface);              /* panels */
  --layer-1-edge: var(--color-neutral-800);
  --layer-2-fill: var(--color-neutral-900);          /* controls on panels */
  --layer-2-edge: var(--color-neutral-700);
  --layer-active-fill: var(--color-accent-900);      /* the one selected thing */
  --layer-active-edge: var(--color-accent);
}

.commercial-workbench :is(.batch-summary, .asset-row-card, .batch-navigation, .title-output, .thumb-button, .queue-overview) {
  border: 1px solid var(--layer-1-edge);
  background: var(--layer-1-fill);
}

.commercial-workbench :is(.batch-rail-item, button, .copy-button, .secondary-button, .drop-zone-icon) {
  border: 1px solid var(--layer-2-edge);
  background: var(--layer-2-fill);
}

.commercial-workbench :is(.batch-rail-item.is-visible, .batch-rail-item.is-selected, .primary-button) {
  border-color: var(--layer-active-edge);
  background: var(--layer-active-fill);
}

/* The primary action is the one accent-coloured control on the page: Nocturne
   .btn-primary, an outline and accent text, never a fill (standard §6). */
.commercial-workbench .primary-button {
  background: transparent;
  color: var(--theme-accent);
  border-color: var(--theme-accent);
}

.commercial-workbench .primary-button:hover:not(:disabled) {
  background: var(--color-accent-900);
}

/* Never stack a light translucent surface on another (Apple §12): a control
   inside a panel reads against the panel, so its edge is the only thing that has
   to separate it. */
.commercial-workbench .title-output button,
.commercial-workbench .batch-navigation button {
}

/* The drop target is an outline, not a surface -- it has no fill on any layer. */



/* ═══ Intake ══════════════════════════════════════════════════════════════════
   One rule. This element had eleven, and the shipped result took three of them
   stacked to work out: a glass panel, then dashed, then the fill stripped back
   off -- which is why removing "the box" last time only removed half of it.

   No border at all now. A dashed rectangle is a placeholder convention, not a
   surface, and it competed with the panels around it while a batch was running.
   What says "drop here" is the icon, the label, and a target big enough to hit;
   the outline only appears when a file is actually over it. Once cards are on
   the desk the intake stops being the headline and shrinks to a control, so the
   work is what fills the screen.
   ══════════════════════════════════════════════════════════════════════════ */
.commercial-workbench .drop-zone {
  position: relative;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: var(--s4);
  width: 100%;
  max-width: min(560px, 100%);
  margin-inline: auto;
  padding: var(--s5) var(--s6);
  border: 1px solid transparent;
  border-radius: var(--r-sheet);
  background: transparent;
  box-shadow: none;
  text-align: left;
  transition: border-color 160ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)),
              background 160ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1));
}

@media (hover: hover) and (pointer: fine) {
  .commercial-workbench .drop-zone:hover {
    background: var(--color-accent-900);
  }
}

.commercial-workbench .drop-zone.is-dragging,
.commercial-workbench .drop-zone.dragover {
  border-color: var(--theme-accent);
  background: var(--color-accent-900);
}

/* Working state: cards are on the desk, so the intake is no longer the thing
   the page is about. */
.commercial-workbench .workspace:not([data-batch-state="empty"]) .drop-zone {
  max-width: min(380px, 100%);
  padding: var(--s3) var(--s4);
  gap: var(--s3);
}


/* ═══ Card preview ════════════════════════════════════════════════════════════
   Every thumbnail was framed at 2.8/4 -- portrait -- whichever way the card
   actually is. A landscape card inside that frame with object-fit: contain is a
   band across the middle of an empty box, and two of them side by side get half
   the column each on top of that. The card ends up smaller than its own label.

   Orientation drives the frame now, and a pair that is landscape stacks instead
   of splitting the row, so each card is as wide as the column allows.
   ══════════════════════════════════════════════════════════════════════════ */
.commercial-workbench .thumb-button.is-landscape .thumb {
  aspect-ratio: 4 / 2.8;
}

.commercial-workbench .preview-images.all-landscape {
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s3);
}

.commercial-workbench .preview-images.all-landscape > .thumb-button {
  width: 100%;
  max-width: 220px;
}

.commercial-workbench .preview-images.all-landscape > .thumb-button,
.commercial-workbench .preview-images.all-landscape > .thumb-button .thumb {
  max-height: 220px;
}


/* The UA stylesheet gives strong/b font-weight: bolder, which is relative -- in
   a parent already at 700 it computes to 900, a weight this page never declares
   and the variable face has to synthesise. Pin it so the scale holds. */
.commercial-workbench :is(strong, b),
.commercial-auth :is(strong, b) {
  font-weight: 600;
}


/* On an empty desk the intake is the only thing to do, so it carries weight --
   through the size of the target and the type on it, not through a rectangle
   drawn around it. A dashed box reads as a placeholder waiting to be replaced;
   a large, quiet, generously spaced control reads as the thing itself. */
.commercial-workbench .workspace[data-batch-state="empty"] .drop-zone {
  max-width: min(560px, 100%);
  padding: var(--s5) var(--s6);
  gap: var(--s4);
}

.commercial-workbench .workspace[data-batch-state="empty"] .drop-zone-icon {
  width: 60px;
  height: 60px;
}

.commercial-workbench .workspace[data-batch-state="empty"] .drop-zone-copy strong {
  font-size: 20px;
}

.commercial-workbench .workspace[data-batch-state="empty"] .drop-zone-copy > span,
.commercial-workbench #statusText {
  font-size: 14px;
  white-space: normal;
}


/* Busy is carried by the progress text and the ::after sweep. Do not also
   outline the drop zone — that box came back the moment identify started. */
.commercial-workbench .drop-zone.status-busy {
  border-color: var(--layer-2-edge);
  background: transparent;
}

/* commercial-ui and workbench-v2 hide 清空批次 below 760/640 to save header
   space. That leaves a phone writer with no way to clear a stuck batch. */
@media (max-width: 760px) {
  .commercial-workbench .reset-button {
    display: inline-flex;
  }

  .commercial-workbench .session-account > span {
    display: inline;
  }
}

@media (max-width: 640px) {
  .commercial-workbench .thumb,
  .commercial-workbench .result-thumb {
    height: auto;
    max-height: 220px;
  }
}

/* Static HTML leftover from the eight-card window. The rail already says
   how many cards are on the desk; a 1-card pair must not read 每屏 8 张. */
.commercial-workbench .section-state {
  display: none;
}

/* §12 判断项 · 中文字距为 0. Every page is lang="zh-CN", so the size-specific
   tightening above (-0.04em on the display h1, -0.02em on numerals) and the opened
   kickers (0.06em on 当前批次 / 当前任务) resolve to the standard's one value here,
   ahead of the desk lock, which stays the last block of the last sheet. The only
   tracked text left is the Latin brand mark. */
.commercial-workbench:lang(zh)
  :is(h1, h2, h3, h4, p, strong, small, span, dt, dd, label, button, input, textarea,
      .eyebrow, .section-kicker, .header-context, #assetBoardTitle):not(.brand-mark) {
  letter-spacing: 0;
}

/* --- writer desk lock (sweep 3; last rules in last sheet) ---
   listing-copilot → workbench-v2 → commercial-ui → this file.
   Earlier sheets share login/auth and must not shrink the slogan, un-center
   the empty intake, uncap the pair, or clip the title the writer edits. */
.commercial-workbench .workspace[data-batch-state="empty"] .workspace-intro,
.commercial-workbench .workspace[data-session-ready="false"] .workspace-intro {
  display: grid;
  visibility: visible;
  opacity: 1;
}

.commercial-workbench .workspace[data-batch-state="empty"] .workspace-intro h1,
.commercial-workbench .workspace[data-session-ready="false"] .workspace-intro h1 {
  display: block;
  visibility: visible;
  opacity: 1;
  font-size: clamp(36px, 6vw, 56px);
  font-weight: 600;
  text-align: center;
}

@media (max-width: 520px) {
  .commercial-workbench .workspace[data-batch-state="empty"] .workspace-intro h1,
  .commercial-workbench .workspace[data-session-ready="false"] .workspace-intro h1 {
    font-size: clamp(32px, 8vw, 40px);
  }
}

.commercial-workbench .workspace[data-batch-state="empty"] .workspace-intro .subtitle {
  display: block;
  justify-self: center;
  text-align: center;
}

.commercial-workbench .workspace[data-batch-state="empty"] .drop-zone,
.commercial-workbench .workspace[data-batch-state="empty"] .drop-zone-copy {
  justify-content: center;
  text-align: center;
}

/* The images column is 1.2fr of the row -- about 715px -- and these caps threw
   most of it away, so a card rendered at 220px. A player photo survives that;
   the year, set, card number and serial do not, which is why reading a card
   meant opening the thumbnail first. The column is there, so use it. */
.commercial-workbench .asset-row-card .preview-images {
  max-width: 100%;
}



/* Stacked, each landscape card has the whole column instead of half of it. */


.commercial-workbench .title-output textarea,
.commercial-workbench [data-title-input] {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  overflow: auto;
  max-height: none;
}

/* File input must cover the dashed/label hit target. Earlier sheets used a
   1×1 clipped control; some browsers then skip change after a label click. */
.commercial-workbench .drop-zone input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  opacity: 0;
  cursor: pointer;
  pointer-events: auto;
  overflow: visible;
  clip: auto;
  clip-path: none;
  z-index: 1;
}

.commercial-workbench .drop-zone.status-error #statusText,
.commercial-workbench #statusText.status-error {
  color: var(--wb-danger, var(--theme-danger, var(--color-rejected-300)));
  font-weight: 600;
  white-space: normal;
}

.commercial-workbench .title-output-error.intake-reject {
  color: var(--wb-danger, var(--theme-danger, var(--color-rejected-300)));
}

.commercial-workbench .intake-preview-missing-back {
  display: grid;
  place-items: center;
  min-height: 120px;
  border: 1px dashed var(--wb-danger, var(--theme-danger, var(--color-rejected-500)));
  color: var(--wb-danger, var(--theme-danger, var(--color-rejected-300)));
}



/* ═══ Surfaces: fewer of them, and the ones that stay carry hue ═════════════
   Every panel was rgba(255,255,255,0.063) over rgb(5,6,13) -- composited
   rgb(21,22,28). Neutral grey, 16 levels above the page: no hue for the eye to
   read as material, not enough separation to read as a layer. That is the grey.

   White is the wrong fill for a dark UI. Glass reads as glass when it tints
   what is behind it; white over near-black is grey no matter how much blur sits
   under it. But tinting everything violet was worse -- every panel became the
   same lilac wash and nothing stood out. So: the card under review is the only
   thing that gets material, and the containers around it stop competing. */

.commercial-workbench .asset-row-card,
.commercial-workbench .title-output {
  background: linear-gradient(155deg, var(--color-accent-900) 0%, var(--color-accent-900) 52%, var(--color-surface) 100%);
  border-color: var(--color-accent-700);
  box-shadow: var(--shadow-sm);
}

/* The image is the content. A frame around it is one more grey rectangle. */
.commercial-workbench .thumb-button {
  border-color: transparent;
  background: none;
  box-shadow: none;
}

/* Containers hold; they do not need to be seen. A hairline is enough to say
   where one ends, and space says the rest. */
.commercial-workbench .batch-summary,
.commercial-workbench .batch-navigation,
.commercial-workbench .queue-overview {
  background: none;
  box-shadow: none;
  border-color: var(--color-accent-700);
}

/* Choosing a card scrolls its top up; keep that clear of the sticky header. */
.commercial-workbench .asset-row-card {
  scroll-margin-top: 88px;
}

/* ═══ Intake motion ════════════════════════════════════════════════════════
   Recognition takes about 25 seconds. The desk went from empty to a full board
   in one frame and then sat still for all of it, so the writer had nothing to
   read as progress. Cards now arrive one at a time, which spends part of that
   wait and shows the batch building.

   Values are from the animation playbook, not invented: entering uses the
   strong ease-out, on-screen movement the strong ease-in-out, and nothing here
   animates a layout property -- the intake target travels by FLIP (transform
   only) and the cards enter on transform + opacity. */

.commercial-workbench .drop-zone {
  will-change: auto;
}

/* Bolt sliding home: in from the left, settling rather than popping. Starts at
   scale(0.96) -- nothing in the world appears from nothing, so never scale(0). */
@keyframes lynca-card-bolt-in {
  from {
    opacity: 0;
    transform: translate3d(-28px, 0, 0) scale(0.96);
  }
  to {
    opacity: 1;
    transform: translate3d(0, 0, 0) scale(1);
  }
}

.commercial-workbench .asset-row-card.is-entering {
  opacity: 0;
  animation: lynca-card-bolt-in 260ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)) forwards;
  /* Each card follows the one before it. 55ms is short enough that eight cards
     land in under half a second, long enough to read as one-after-another. */
  animation-delay: calc(var(--enter-index, 0) * 55ms);
}

/* The board has more room to work in once a batch is on it. */
.commercial-workbench .workspace[data-batch-state="ready"] .asset-workbench {
  padding-block: var(--s5);
}

@media (prefers-reduced-motion: reduce) {
  /* Keep the state change legible, drop the travel. */
  .commercial-workbench .asset-row-card.is-entering {
    animation-duration: 160ms;
    animation-delay: 0ms;
    animation-name: lynca-card-fade-in;
  }
  @keyframes lynca-card-fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
  }
}


/* ═══ Master-detail ════════════════════════════════════════════════════════
   The batch stands to the left of the card being read, and the card gets the
   rest. Rows of small cards meant the writer could see the batch or read a
   card, never both -- and reading one meant opening the thumbnail. Here the
   whole batch is always in view as a list you can name cards from, and the one
   card in hand is as large as the board allows.

   The cost, stated so it is a choice and not an accident: 268px is spent on
   navigation at all times, and comparing two cards side by side is not
   possible -- that is what the rail's titles are for. */

.commercial-workbench #assetPreviewList {
  display: grid;
  grid-template-columns: 268px minmax(0, 1fr);
  gap: var(--s5);
  align-items: start;
}

.commercial-workbench #assetPreviewList > .batch-navigation {
  grid-column: 1;
  grid-row: 1 / -1;
  position: sticky;
  top: 88px;
  display: flex;
  flex-direction: column;
  /* workbench-v2.css sets flex-wrap: wrap on this element, which was right when
     it was a horizontal strip. As a bounded column it means the moment the rail
     no longer fits the max-height -- eight cards in -- it wraps into a SECOND
     column beside the first, outside the 268px grid track, where overflow:
     hidden clips it. The rail measured x=283 against a nav ending at x=292,
     sharing a y with the summary: laid out to its right, not below it. Setting
     the direction without the wrap left half the rule behind. */
  flex-wrap: nowrap;
  gap: var(--s3);
  max-height: calc(100vh - 120px);
  min-width: 0;
  padding-right: var(--s4);
  border-right: 1px solid var(--layer-1-edge);
  border-radius: 0;
  background: none;
  box-shadow: none;
  overflow: hidden;
}

.commercial-workbench #assetPreviewList > .asset-review-group {
  grid-column: 2;
}

/* The rail is a list, not a strip of buttons. */
/* The rail is the scrolling part of the navigation column. Without flex: 1 and
   min-height: 0 it refuses to shrink, so the rows past the column's height were
   clipped by the overflow: hidden above rather than scrolled -- which is why a
   batch stopped being navigable somewhere around the tenth card. It holds a
   hundred now because the list scrolls instead of being cut off. */
/* A window with a floor and a ceiling, not a list that grows with the batch.
   The bound is expressed in rows -- a row is 58px plus the 4px gap -- and lands
   on a half row on purpose: a cut-off row is what tells a reader there is more
   below, the way a picker shows the next value peeking past the selection. A
   list that simply ended flush at the eighth card looked complete. */
.commercial-workbench #assetPreviewList .batch-rail {
  --rail-row: 62px;
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  flex: 0 1 auto;
  min-height: calc(var(--rail-row) * 3);
  max-height: calc(var(--rail-row) * 7.5);
  overflow-y: auto;
  overflow-x: hidden;
  min-width: 0;
  overscroll-behavior: contain;
}

/* scrollbar-width: thin was the wrong instrument: in Chrome it swaps the
   overlay scrollbar for a classic one, which then sits there permanently in
   full system grey against a near-black column. The rail keeps an overlay
   scrollbar and dresses it in the palette, resting at almost nothing and
   surfacing while the reader is in the list. */
.commercial-workbench #assetPreviewList .batch-rail::-webkit-scrollbar {
  width: 6px;
}

.commercial-workbench #assetPreviewList .batch-rail::-webkit-scrollbar-track {
  background: transparent;
}

.commercial-workbench #assetPreviewList .batch-rail::-webkit-scrollbar-thumb {
  border-radius: 999px;
  background: var(--color-neutral-900);
  transition: background 180ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1));
}

@media (hover: hover) and (pointer: fine) {
  .commercial-workbench #assetPreviewList .batch-rail:hover::-webkit-scrollbar-thumb {
    background: var(--color-neutral-700);
  }
}

/* Firefox has no thumb selector; the pair is colour-only and already subtle. */
@supports (scrollbar-color: transparent transparent) {
  .commercial-workbench #assetPreviewList .batch-rail {
    scrollbar-color: var(--color-neutral-700) transparent;
  }
}

/* The selected card scrolls itself into that window when the batch moves on, so
   the writer never has to hunt for where they are. */
.commercial-workbench #assetPreviewList .batch-rail-item {
  scroll-margin-block: var(--s2);
}

.commercial-workbench #assetPreviewList .batch-rail-item {
  display: grid;
  /* The trailing track is fixed, not auto: an auto track after 1fr is pushed
     open by its content, which is how the rail overflowed its column before. */
  grid-template-columns: 18px 44px minmax(0, 1fr) 10px;
  align-items: center;
  gap: var(--s2);
  width: 100%;
  height: auto;
  min-width: 0;
  padding: var(--s2);
  text-align: left;
  border-color: transparent;
  background: none;
}

.commercial-workbench #assetPreviewList .batch-rail-item.is-selected {
  border-color: var(--theme-accent);
  background: var(--layer-active-fill);
}

.commercial-workbench .batch-rail-index {
  font-size: 12px;
  color: var(--theme-subtle);
  min-width: 18px;
}

/* COS-82: the rail thumb is the standard .asset-img (span variant, because the row is a
   <button>). No frame of its own: padding and surface come off, the well is the 40px square. */
.commercial-workbench .batch-rail-thumb {
  padding: 0;
  gap: 2px;
  background: transparent;
  border-radius: 0;
}

.commercial-workbench .batch-rail-thumb .asset-img__well {
  width: 40px;
  height: 40px;
  padding: 2px;
}

.commercial-workbench .batch-rail-thumb .asset-img__caption {
  display: none;
}

.commercial-workbench .batch-rail-copy {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.commercial-workbench .batch-rail-title {
  font-size: 12px;
  color: var(--theme-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.commercial-workbench .batch-rail-state {
  font-size: 12px;
  color: var(--theme-subtle);
}

/* One card in hand. The others stay in the DOM -- identify, confirm and the
   title editor all act on rendered rows -- they are simply not the card being
   read right now. */
.commercial-workbench #assetPreviewList .asset-row-card {
  display: none;
}

.commercial-workbench #assetPreviewList .asset-row-card.is-focused {
  display: grid;
}

.commercial-workbench #assetPreviewList .asset-review-group:not(:has(.asset-row-card.is-focused)) {
  display: none;
}

/* With one card on screen it can have the room: images left, title right, both
   larger than a row of eight ever allowed. */
.commercial-workbench #assetPreviewList .asset-row-card.is-focused .preview-images > .thumb-button {
  max-height: 460px;
}

@media (max-width: 1100px) {
  .commercial-workbench #assetPreviewList {
    grid-template-columns: minmax(0, 1fr);
  }
  .commercial-workbench #assetPreviewList > .batch-navigation {
    grid-column: 1;
    grid-row: auto;
    position: static;
    max-height: 260px;
    border-right: 0;
    border-bottom: 1px solid var(--layer-1-edge);
    padding-right: 0;
    padding-bottom: var(--s3);
  }
  .commercial-workbench #assetPreviewList > .asset-review-group {
    grid-column: 1;
  }
}


/* ═══ A pair reads as one size ═════════════════════════════════════════════
   Equal frames were the wrong idea. A portrait card fills a square's height and
   two thirds of its width; a landscape card fills the width and two thirds of
   the height. Put them in the same box and each wastes a different corner --
   which is exactly what 双竖和横竖还是很灾难 was describing.

   Height is the shared measure. Give both images the same height and let width
   follow the photo: a portrait card comes out 238x340, a landscape one 486x340,
   and the eye reads them as the same card seen two ways. Nothing is cropped,
   nothing is squeezed, and neither leaves a hole. */

.commercial-workbench .asset-row-card .preview-images {
  display: flex;
  flex-wrap: nowrap;
  align-items: flex-start;
  justify-content: center;
  gap: var(--s4);
  max-width: 100%;
}

.commercial-workbench .asset-row-card .preview-images > .thumb-button {
  flex: 0 1 auto;
  width: auto;
  max-width: none;
  /* Bounded: an unbounded thumb frame is what let storage-first originals blow
     the board out, and a guard test pins that. */
  max-height: 420px;
  aspect-ratio: auto;
  min-width: 0;
  display: grid;
  place-items: center;
}

/* Matching on height made the landscape card twice the area of the portrait one
   -- 486x340 against 238x340 -- so they still did not read as the same card.
   The measure that does is the LONGEST side, because a trading card is 2.5 by
   3.5 inches whichever way up it is. Cap both dimensions at the same number and
   a portrait comes out 266x380, a landscape 380x266: same card, turned ninety
   degrees, same area.

   The cap is fluid so the pair always fits the detail pane beside the rail and
   never pushes the title below the fold -- two landscape cards side by side are
   the widest case at twice the cap. */
.commercial-workbench .asset-row-card {
  /* The detail pane is roughly 930px beside the rail, and the widest case is
     two landscape cards at the cap plus a gap -- so the ceiling can go to 480
     and still fit, which is where the room the founder could see was. */
  /* Measured on production: at a 1280 viewport the detail pane is 932 and the
     image row 890, so two landscape cards plus the gap fit while the cap stays
     under (890 - 20) / 2 = 435. 34vw lands on 435.2 and overflows by a fifth of
     a pixel; 33vw leaves the margin. The 480 ceiling then applies from about
     1450 up, where the row has grown to carry it. */
  --card-long-edge: clamp(220px, 33vw, 480px);
}

.commercial-workbench .asset-row-card .preview-images > .thumb-button .thumb {
  width: auto;
  height: auto;
  max-width: min(var(--card-long-edge), 100%);
  max-height: var(--card-long-edge);
  aspect-ratio: auto;
  object-fit: contain;
}

.commercial-workbench .asset-row-card .preview-images.single {
  justify-content: center;
}

/* Landscape pairs sit side by side like every other pair. Stacking them was a
   fix for the old height-matched sizing, where each came out 486 wide; on the
   long edge they are 380, so the two fit the row -- and stacked they had grown
   tall enough to push the title and the buttons off the bottom of the screen,
   which is the opposite of being able to see the card. */
.commercial-workbench .asset-row-card .preview-images.all-landscape {
  flex-direction: row;
  align-items: flex-start;
}

/* ═══ COS-82: the collectible sits in the standard .asset-img ═══════════════
   asset-img.css (a byte copy of the standard) owns the mounting: surface card,
   --color-bg well with a hairline, contain, no radius / filter / hover zoom on
   the image, front left of back through .asset-pair. This block only decides
   how much room the desk gives it, using the same long-edge cap as before so a
   pair still fits beside the rail and the title stays above the fold. A pair's
   wells share the 5:7 card aspect (asset-img.css), so both read as one size. */
.commercial-workbench .asset-row-card .preview-images {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  justify-items: center;
  align-items: start;
  gap: var(--space-4);
  max-width: 100%;
}

.commercial-workbench .asset-row-card .preview-images.asset-pair {
  grid-template-columns: repeat(2, minmax(0, calc(var(--card-long-edge) * 5 / 7 + var(--space-4) * 2)));
  justify-content: center;
}

.commercial-workbench .asset-row-card .preview-images > .asset-img {
  width: 100%;
  max-width: calc(var(--card-long-edge) + var(--space-4) * 2);
  min-width: 0;
}

/* Chrome sizes an auto grid row by the image's natural height, so the image's
   max-height: 100% never binds and a tall photo runs straight out of a fixed-height
   or 5:7 well and over the caption (measured with the 700x1265 fixture: every
   preset in asset-img.css does it). A minmax(0, 1fr) row makes the row the well;
   the inspect button is a grid of its own and needs the same. Nothing is clipped —
   the image is bounded, not cut. Reported upstream for asset-img.css; this stays
   until the snapshot carries it. */
.commercial-workbench .asset-img__well {
  min-height: 0;
  grid-template-rows: minmax(0, 1fr);
  grid-template-columns: minmax(0, 1fr);
}

.commercial-workbench .asset-img__inspect {
  min-height: 0;
  min-width: 0;
  grid-template-rows: minmax(0, 1fr);
  grid-template-columns: minmax(0, 1fr);
}

.commercial-workbench .asset-row-card .preview-images > .asset-img .asset-img__img {
  max-width: min(var(--card-long-edge), 100%);
  max-height: var(--card-long-edge);
}

.commercial-workbench .asset-row-card .preview-images.asset-pair > .asset-img .asset-img__img {
  max-width: 100%;
  max-height: 100%;
}

/* The intake preview reuses the review-card layout before the upload has an id. */
.commercial-workbench .intake-preview-thumb {
  pointer-events: none;
}

@media (max-width: 599px) {
  .commercial-workbench .asset-row-card .preview-images.asset-pair {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ═══ The detail reads top to bottom ═══════════════════════════════════════
   Images across the top, then the title under them, the way the direction was
   drawn. Side by side, the title column was fighting the card for width and
   both lost; stacked, the images get the full width of the detail pane and the
   title gets a line long enough to read without wrapping mid-phrase. */

.commercial-workbench #assetPreviewList .asset-row-card.is-focused {
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s4);
}

.commercial-workbench #assetPreviewList .asset-row-card.is-focused .asset-source {
  min-width: 0;
}

/* Belt and braces on the rail: the row is a grid with a 1fr title cell, and a
   long title must clip inside it rather than draw across its neighbours. */
.commercial-workbench #assetPreviewList .batch-rail-item {
  overflow: hidden;
}

.commercial-workbench .batch-rail-copy,
.commercial-workbench .batch-rail-title,
.commercial-workbench .batch-rail-state {
  min-width: 0;
  max-width: 100%;
}


/* ═══ The intake target ════════════════════════════════════════════════════
   Outlined in every state, busy included, and big enough to read as the place
   cards go rather than a button tucked in a corner. It is the only affordance
   on an empty desk and the one a writer reaches for again mid-batch, so it does
   not fade out once work starts. What does not belong is the cyan busy tint
   from listing-copilot.css: a target whose border changes colour reads as a
   state change on the target itself rather than on the batch. */

.commercial-workbench .drop-zone {
  border: 1px solid var(--layer-2-edge);
  /* Full width of the intake column rather than a fixed box: the target for a
     batch of cards should be the size of the space it is dropped into. */
  min-width: min(880px, 100%);
  width: 100%;
  padding: var(--s6) var(--s6);
  gap: var(--s5);
}

.commercial-workbench .workspace[data-batch-state="empty"] .drop-zone {
  min-width: min(960px, 100%);
  padding: calc(var(--s6) * 1.4) var(--s6);
}

.commercial-workbench .drop-zone-icon {
  width: 76px;
  height: 76px;
}

.commercial-workbench .drop-zone-icon svg {
  width: 34px;
  height: 34px;
}

.commercial-workbench .drop-zone-copy strong {
  font-size: 20px;
}

/* The shortcut is only useful if the writer knows it is there, so it rides on
   the button rather than in a legend somewhere. Quiet enough that the word
   stays the thing you read. */
.commercial-workbench .title-action-key {
  margin-left: var(--s2);
  padding: 1px 5px;
  border: 1px solid var(--color-neutral-700);
  border-radius: 4px;
  font-family: inherit;
  font-size: 12px;
  line-height: 1.2;
  color: var(--theme-muted);
  opacity: 0.85;
}

.commercial-workbench button:disabled .title-action-key {
  display: none;
}

/* The page is stale, not broken: it sits at the corner, it does not cover the
   desk, and it waits. A modal here would interrupt a writer mid-title. */
.deploy-stale-notice {
  position: fixed;
  right: clamp(12px, 2vw, 28px);
  bottom: clamp(12px, 2vw, 28px);
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: min(420px, calc(100vw - 32px));
  padding: 12px 14px;
  border-radius: 12px;
  border: 1px solid var(--surface-border, var(--color-neutral-800));
  background: var(--surface-raised, var(--color-bg));
  color: var(--text-primary, var(--color-text));
  box-shadow: var(--shadow-sm);
  font-size: 13px;
  line-height: 1.45;
  animation: deploy-stale-in 260ms cubic-bezier(0.23, 1, 0.32, 1) both;
}

.deploy-stale-notice button {
  flex: 0 0 auto;
  white-space: nowrap;
}

@keyframes deploy-stale-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .deploy-stale-notice { animation: none; }
}

/* The title list grows one row per card with nothing bounding it, so a real
   batch pushed the page to 5300px: the header ended up floating in the middle
   of a column of empty space and the desk itself was somewhere off-screen. The
   queue rail was given a window for exactly this reason; this list never was.

   Bounding the board rather than the panel on purpose. Four stylesheets hide
   this panel under scoped selectors, and a bare `display` here would be one
   more rule arguing with rules it cannot see -- which is how an earlier fix in
   this file landed on the wrong element entirely. */
.batch-titles-panel > .batch-titles-board {
  display: flex;
  flex-direction: column;
  max-height: min(52vh, 620px);
  overflow: hidden;
}

.commercial-workbench .batch-title-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}

.commercial-workbench .batch-titles-foot {
  flex: 0 0 auto;
  padding: 12px 20px 16px;
  border-top: 1px solid var(--line);
}

.commercial-workbench .copy-all-button {
  width: 100%;
  min-height: 40px;
  padding: 0 16px;
  /* Nocturne .btn-primary: accent outline and text, never a fill (§6). */
  border: 1px solid var(--color-accent);
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--color-accent);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.01em;
  cursor: pointer;
  box-shadow: none;
}

.commercial-workbench .copy-all-button:hover:not(:disabled) {
  border-color: var(--color-accent);
  background: var(--color-accent-900);
}

.commercial-workbench .copy-all-button:disabled {
  border-color: var(--color-neutral-900);
  background: transparent;
  color: var(--theme-subtle);
}

/* The board title is an h1 so the page still has a top-level heading once the
   intro is hidden (see index.html). It must not inherit the slogan's type
   scale -- it is a section label, and it looked like one as an h2. */
.commercial-workbench h1#assetBoardTitle {
  font-size: 14px;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: -0.02em;
  margin: 0;
}

/* 复制全部 beside the 全部标题 heading -- where it used to live -- running
   against the full-width one at the foot of the same panel. It takes the
   compact shape the section head expects; the foot version's width:100% would
   stretch it across the heading row. */
.commercial-workbench .copy-all-head {
  width: auto;
  white-space: nowrap;
}
