/* ===========================================================================
   09GUA ADMIN V2-A.2 — THE DESIGN SYSTEM
   ===========================================================================

   WHY ONE FILE OF TOKENS AND COMPONENTS
   -------------------------------------
   §十二 asks for this directly, and the Owner's screenshot shows what its
   absence costs: every page had grown its own spacing, its own idea of what a
   "badge" is, and inline colours in templates. The result reads as a dozen
   pages rather than one product — which is a large part of what 「像十几年前
   的 2G 管理页」 actually describes.

   So: every colour, every radius and every gap below is a TOKEN, declared
   once at :root. A template may name a component (.card, .badge, .btn) and
   may not name a colour. There is no `style="..."` in any V2 template.

   THE VISUAL BRIEF (§十三)
   ------------------------
   现代 / 克制 / 专业 / 高信息密度. Explicitly NOT: 巨大圆角, 渐变, 霓虹,
   炫技动画, 大号 Hero. This is a tool somebody uses for an hour a day, so the
   budget goes to legibility and density, not to decoration. Radii are 6–10px,
   shadows are one hairline, and the only animation is a 120ms colour fade on
   interactive elements.

   Dark sidebar, light content (§十三). The sidebar is chrome — it should
   recede and stay stable; the content is the work.
   =========================================================================== */

:root {
  /* -- surface ---------------------------------------------------------- */
  --bg:             #f2f4f7;
  --surface:        #ffffff;
  --surface-muted:  #f7f8fa;
  --surface-hover:  #f0f3f7;
  --border:         #e3e7ed;
  --border-strong:  #cbd2dc;

  /* -- text ------------------------------------------------------------- */
  --text:           #1a2230;
  --text-soft:      #4d5766;
  --text-muted:     #7b8595;
  --text-invert:    #ffffff;

  /* -- brand & status ---------------------------------------------------
     Four status colours and no more. §二十五 maps them to meanings once:
     绿=ready 黄=needs attention 红=failed 灰=not started. A fifth colour
     would need a fifth meaning, and there are only four. */
  --primary:        #2f6fdd;
  --primary-strong: #245bbb;
  --primary-soft:   #e8f0fd;

  --success:        #1c8a4d;
  --success-soft:   #e3f5ea;
  --warning:        #a4700a;
  --warning-soft:   #fdf2dc;
  --danger:         #c0392b;
  --danger-soft:    #fbe6e3;
  --neutral:        #74808f;
  --neutral-soft:   #eceff3;

  /* -- sidebar ---------------------------------------------------------- */
  --side-bg:        #182031;
  --side-bg-soft:   #212b3f;
  --side-text:      #c3ccdb;
  --side-text-dim:  #7b8699;
  --side-active:    #2f6fdd;

  /* -- spacing scale (4px base) ----------------------------------------- */
  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 20px; --s6: 24px; --s7: 32px; --s8: 40px;

  /* -- shape ------------------------------------------------------------ */
  --r-sm: 4px; --r-md: 6px; --r-lg: 10px; --r-pill: 999px;
  --shadow-1: 0 1px 2px rgba(16, 24, 40, .06);
  --shadow-2: 0 4px 14px rgba(16, 24, 40, .10);

  /* -- type (§十四) ------------------------------------------------------ */
  --fs-page:  26px;
  --fs-sec:   18px;
  --fs-body:  14px;
  --fs-table: 13px;
  --fs-meta:  12px;

  /* -- layout ----------------------------------------------------------- */
  --sidebar-w: 232px;
  --header-h:  56px;
  /* §十一 — on a 1700px monitor an unbounded table sprays 8 columns across
     the whole width and the eye loses the row. 1600 is the ceiling. */
  --main-max: 1600px;

  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
          "Hiragino Sans GB", "Microsoft YaHei", "Source Han Sans SC",
          "Noto Sans CJK SC", sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

/* ===========================================================================
   RESET / BASE
   =========================================================================== */
*, *::before, *::after { box-sizing: border-box; }

html, body { height: 100%; }

body {
  margin: 0;
  font-family: var(--font);
  font-size: var(--fs-body);
  line-height: 1.55;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

a { color: var(--primary); text-decoration: none; }
a:hover { text-decoration: underline; }

/* §四十九 — focus must be VISIBLE, and not only on the element the mouse
   happens to be over. :focus-visible keeps it out of the way of mouse users
   while guaranteeing a keyboard user can always see where they are. */
:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

h1, h2, h3, h4 { margin: 0; font-weight: 600; }

.mono { font-family: var(--mono); font-size: var(--fs-meta); }
.muted { color: var(--text-muted); }
.soft  { color: var(--text-soft); }
.nowrap { white-space: nowrap; }
.warn  { color: var(--danger); }
.right { text-align: right; }
.pre { white-space: pre-wrap; word-break: break-word; }
.hide { display: none !important; }
/* The only spacing utilities. Kept to two so that a template still cannot
   express an arbitrary margin — the alternative to `style="margin-top:24px"`
   must not be a utility framework. */
.mt-6 { margin-top: var(--s6); }
.mt-4 { margin-top: var(--s4); }

/* ===========================================================================
   APP SHELL — sidebar + header + main  (§七 / §八 / §十 / §十一)
   =========================================================================== */
.app { min-height: 100vh; }

/* -- SIDEBAR -------------------------------------------------------------- */
.sidebar {
  position: fixed;
  inset: 0 auto 0 0;
  width: var(--sidebar-w);
  background: var(--side-bg);
  color: var(--side-text);
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  overscroll-behavior: contain;
  z-index: 40;
}

.sidebar-brand {
  display: flex; flex-direction: column; gap: 2px;
  padding: var(--s4) var(--s5);
  border-bottom: 1px solid rgba(255,255,255,.07);
}
.sidebar-brand .mark {
  font-size: 17px; font-weight: 700; letter-spacing: .06em;
  color: var(--text-invert);
}
.sidebar-brand .sub { font-size: var(--fs-meta); color: var(--side-text-dim); }
.sidebar-brand a { color: inherit; text-decoration: none; }

.sidebar-nav { flex: 1; padding: var(--s3) 0 var(--s6); }

.nav-group { padding: var(--s2) 0; }
.nav-group + .nav-group { border-top: 1px solid rgba(255,255,255,.07); }

.nav-heading {
  padding: var(--s2) var(--s5) var(--s1);
  font-size: 11px; font-weight: 600; letter-spacing: .1em;
  color: var(--side-text-dim); text-transform: uppercase;
}

.nav-item {
  display: flex; align-items: center; gap: var(--s2);
  padding: 7px var(--s5);
  color: var(--side-text);
  font-size: var(--fs-body);
  border-left: 3px solid transparent;
  transition: background .12s ease, color .12s ease;
}
.nav-item:hover { background: var(--side-bg-soft); color: var(--text-invert); text-decoration: none; }
.nav-item.is-active {
  background: var(--side-bg-soft);
  color: var(--text-invert);
  border-left-color: var(--side-active);
  font-weight: 600;
}
.nav-item .nav-label {
  flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.nav-item.nav-sub { padding-left: var(--s7); font-size: 13.5px; }
.nav-item.is-off .nav-label { opacity: .55; }

/* §九 — the badge is 待审核 + 分类异常, i.e. WORK. Zero renders nothing at
   all rather than a grey 0: a row of zeroes trains the eye to skip the
   column that is supposed to attract it. */
.nav-badge {
  min-width: 20px; padding: 0 6px;
  border-radius: var(--r-pill);
  background: rgba(255,255,255,.14);
  color: var(--text-invert);
  font-size: 11px; font-weight: 600; text-align: center; line-height: 18px;
}
.nav-badge.is-warn { background: var(--danger); }
.nav-badge.is-test {
  background: transparent; color: var(--side-text-dim);
  border: 1px solid var(--side-text-dim); font-weight: 500;
}

/* -- HEADER --------------------------------------------------------------- */
.header {
  position: sticky; top: 0; z-index: 30;
  height: var(--header-h);
  display: flex; align-items: center; gap: var(--s4);
  padding: 0 var(--s6);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}
.header-crumb { display: flex; align-items: baseline; gap: var(--s2); min-width: 0; }
.header-crumb .crumb-parent { font-size: var(--fs-meta); color: var(--text-muted); }
.header-crumb .crumb-title {
  font-size: 15px; font-weight: 600; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.header-spacer { flex: 1; }

.header-search { position: relative; }
.header-search input {
  width: 260px; padding: 6px 10px 6px 30px;
  border: 1px solid var(--border-strong); border-radius: var(--r-md);
  background: var(--surface-muted); font-size: 13px;
}
.header-search input:focus { background: var(--surface); }
.header-search .icon {
  position: absolute; left: 9px; top: 50%; transform: translateY(-50%);
  color: var(--text-muted); font-size: 13px; pointer-events: none;
}

.header-user {
  display: flex; align-items: center; gap: var(--s2);
  font-size: 13px; color: var(--text-soft);
}
.header-user .who {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: var(--r-pill);
  background: var(--primary-soft); color: var(--primary-strong);
  font-weight: 600; font-size: 12px;
}

.sidebar-toggle {
  display: none;
  border: 1px solid var(--border-strong); background: var(--surface);
  color: var(--text); border-radius: var(--r-md);
  padding: 5px 9px; font-size: 15px; line-height: 1; cursor: pointer;
}

/* -- MAIN ----------------------------------------------------------------- */
.shell { margin-left: var(--sidebar-w); min-width: 0; }
.main  { padding: var(--s6); }
.container { max-width: var(--main-max); margin: 0 auto; min-width: 0; }

.page-head {
  display: flex; align-items: flex-start; gap: var(--s4);
  flex-wrap: wrap; margin-bottom: var(--s5);
}
.page-head .grow { flex: 1; min-width: 240px; }
.page-title { font-size: var(--fs-page); line-height: 1.25; letter-spacing: -.01em; }
.page-sub { margin-top: var(--s1); font-size: var(--fs-body); color: var(--text-muted); }
.page-actions { display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap; }

.section-title {
  font-size: var(--fs-sec); margin: var(--s7) 0 var(--s3);
  display: flex; align-items: baseline; gap: var(--s2);
}
.section-title:first-child { margin-top: 0; }
.section-title .count { font-size: var(--fs-meta); color: var(--text-muted); font-weight: 400; }

/* ===========================================================================
   CARD / GRID
   =========================================================================== */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-1);
}
.card-pad { padding: var(--s5); }
.card-head {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3) var(--s5);
  border-bottom: 1px solid var(--border);
}
.card-head h2 { font-size: 15px; }
.card-head .grow { flex: 1; }
.card + .card { margin-top: var(--s4); }

.grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: var(--s4); }
.stack  { display: flex; flex-direction: column; gap: var(--s4); }
.row    { display: flex; gap: var(--s3); align-items: center; flex-wrap: wrap; }

/* ===========================================================================
   METRIC CARDS (§十五)
   ===========================================================================
   Six numbers, each one a LINK into the filter that produced it. A metric the
   Owner cannot click is a metric that makes them go and find the rows by
   hand, which is the trip §二 is trying to delete. */
.metrics {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: var(--s3);
}
.metric {
  display: block; padding: var(--s4) var(--s4) var(--s3);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); box-shadow: var(--shadow-1);
  color: inherit; transition: border-color .12s ease, box-shadow .12s ease;
}
a.metric:hover { text-decoration: none; border-color: var(--border-strong); box-shadow: var(--shadow-2); }
/* The three lines are BLOCKS. As inline spans the value and its caption
   ran together — 「0 今天新到的内容」 on one line — which is the exact
   opposite of a metric card: the number stops being the thing you see. */
.metric .m-label {
  display: block;
  font-size: var(--fs-meta); color: var(--text-muted);
}
.metric .m-value {
  display: block;
  font-size: 30px; font-weight: 650; line-height: 1.15; letter-spacing: -.02em;
  margin-top: var(--s1); font-variant-numeric: tabular-nums;
}
.metric .m-foot {
  display: block;
  font-size: var(--fs-meta); color: var(--text-muted); line-height: 1.35;
}
.metric.is-zero .m-value { color: var(--text-muted); font-weight: 500; }
.metric.is-warn .m-value { color: var(--warning); }
.metric.is-danger .m-value { color: var(--danger); }
.metric.is-good .m-value { color: var(--success); }

/* ===========================================================================
   BADGES (§二十四 / §二十五)
   ===========================================================================
   ONE badge component, four status tones, and a `.badge-dim` outline variant
   for provenance labels (人工 / 来源默认) which are facts rather than states.
   §四十九: every tone also carries a WORD, so colour is never the only
   channel — the same rule that makes 分类✓ and 分类! distinguishable in
   greyscale. */
.badge {
  display: inline-flex; align-items: center; gap: 3px;
  padding: 1px 8px; border-radius: var(--r-pill);
  font-size: var(--fs-meta); line-height: 18px; font-weight: 500;
  background: var(--neutral-soft); color: var(--text-soft);
  white-space: nowrap;
}
.badge-ok      { background: var(--success-soft); color: var(--success); }
.badge-warn    { background: var(--warning-soft); color: var(--warning); }
.badge-danger  { background: var(--danger-soft);  color: var(--danger); }
.badge-info    { background: var(--primary-soft); color: var(--primary-strong); }
.badge-off, .badge-idle { background: var(--neutral-soft); color: var(--text-muted); }
.badge-dim {
  background: transparent; color: var(--text-muted);
  border: 1px solid var(--border-strong); font-weight: 400;
}
.badge-test {
  background: transparent; color: var(--warning);
  border: 1px dashed var(--warning); font-weight: 600; letter-spacing: .04em;
}
.badge-sm { font-size: 11px; padding: 0 6px; line-height: 16px; }

/* The status strip in a queue row: badges laid out inline, wrapping. */
.badge-row { display: flex; flex-wrap: wrap; gap: 3px; }

/* ===========================================================================
   BUTTONS (§四十九 — a button is a <button>, never a styled <div>)
   =========================================================================== */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s1);
  padding: 6px 12px;
  font-family: inherit; font-size: 13px; font-weight: 500; line-height: 20px;
  border: 1px solid var(--border-strong); border-radius: var(--r-md);
  background: var(--surface); color: var(--text);
  cursor: pointer; text-decoration: none; white-space: nowrap;
  transition: background .12s ease, border-color .12s ease, color .12s ease;
}
.btn:hover { background: var(--surface-hover); text-decoration: none; }
.btn:disabled, .btn[aria-disabled="true"] { opacity: .5; cursor: not-allowed; }

.btn-primary { background: var(--primary); border-color: var(--primary); color: var(--text-invert); }
.btn-primary:hover { background: var(--primary-strong); border-color: var(--primary-strong); }
.btn-danger  { background: var(--danger); border-color: var(--danger); color: var(--text-invert); }
.btn-danger:hover { background: #a5321c; border-color: #a5321c; }
.btn-ghost   { background: transparent; border-color: transparent; color: var(--text-soft); }
.btn-ghost:hover { background: var(--surface-hover); color: var(--text); }
.btn-sm { padding: 3px 9px; font-size: 12px; line-height: 18px; }
.btn-icon { padding: 4px 8px; font-size: 15px; line-height: 16px; }

.btn-link {
  background: none; border: none; padding: 0;
  color: var(--primary); font-size: var(--fs-meta); cursor: pointer;
  font-family: inherit; text-align: left;
}
.btn-link:hover { text-decoration: underline; }

.btn-group { display: inline-flex; gap: var(--s1); align-items: center; }

/* -- overflow menu (§二十七) ----------------------------------------------
   <details>/<summary>, not a JS popover: it keeps working with JS disabled,
   it is keyboard-operable for free, and it cannot get stuck open on a row
   that has been re-rendered. */
.menu { position: relative; display: inline-block; }
.menu > summary {
  list-style: none; cursor: pointer;
  padding: 4px 8px; border: 1px solid var(--border-strong);
  border-radius: var(--r-md); background: var(--surface);
  font-size: 14px; line-height: 18px; color: var(--text-soft);
}
.menu > summary::-webkit-details-marker { display: none; }
.menu > summary:hover { background: var(--surface-hover); }
.menu-body {
  position: absolute; right: 0; top: calc(100% + 4px); z-index: 20;
  min-width: 190px; padding: var(--s1);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-md); box-shadow: var(--shadow-2);
}
.menu-body a, .menu-body button {
  display: block; width: 100%; text-align: left;
  padding: 6px 10px; border: none; background: none;
  font-family: inherit; font-size: 13px; color: var(--text); cursor: pointer;
  border-radius: var(--r-sm); text-decoration: none;
}
.menu-body a:hover, .menu-body button:hover { background: var(--surface-hover); text-decoration: none; }
.menu-body hr { border: none; border-top: 1px solid var(--border); margin: var(--s1) 0; }

/* P7-AI1 §二十四..§二十七 — DROP-UP INSIDE THE BOTTOM BAR.
   The Owner could open the ⋯ menu on the Content detail screen and could not
   see it. Nothing clipped it and no z-index was wrong: `.actionbar` is
   `position: fixed; bottom: 0`, so `top: calc(100% + 4px)` lays the panel out
   BELOW the bottom edge of the viewport — off-screen, and unreachable because
   a fixed bar does not scroll.

   The rule is scoped to `.actionbar` on purpose. The queue table's row menu is
   in normal flow with space beneath it and must keep opening downwards; and
   §二十六 forbids buying the fix by removing the sticky bar. `bottom: 100%`
   measures from the summary's own box, so the panel sits directly above the
   button whatever the bar's height turns out to be. */
.actionbar .menu-body {
  top: auto; bottom: calc(100% + 4px);
  z-index: 40;   /* above .actionbar's own 35, inside its stacking context */
}

/* ===========================================================================
   FORMS
   =========================================================================== */
input, select, textarea {
  font-family: inherit; font-size: 13px; color: var(--text);
  padding: 6px 9px;
  border: 1px solid var(--border-strong); border-radius: var(--r-md);
  background: var(--surface);
  max-width: 100%;
}
textarea { line-height: 1.6; resize: vertical; }
input:disabled, select:disabled, textarea:disabled { background: var(--surface-muted); color: var(--text-muted); }

.field { display: flex; flex-direction: column; gap: var(--s1); min-width: 0; }
.field > label, .field-label {
  font-size: var(--fs-meta); font-weight: 500; color: var(--text-soft);
}
.field .hint { font-size: var(--fs-meta); color: var(--text-muted); }
.field input, .field textarea, .field select { width: 100%; }
.field-row { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: var(--s4); }

.check { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; cursor: pointer; }
.check input { width: auto; }

.inline-form { display: inline-flex; gap: var(--s1); align-items: center; margin: 0; }

/* -- FILTER BAR (§十九) ---------------------------------------------------
   One bar, one row, everything in it. §十九 names the failure it replaces:
   「一个 checkbox 飘在页面中间，一个『只看分类异常』文字单独掉一行」. */
.toolbar {
  display: flex; align-items: center; gap: var(--s3);
  flex-wrap: wrap;
  padding: var(--s3) var(--s4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-1);
  margin-bottom: var(--s4);
}
.toolbar .grow { flex: 1; min-width: 160px; }
.toolbar form { display: contents; }
.toolbar .toolbar-search { flex: 1; min-width: 200px; max-width: 380px; }
.toolbar .divider { width: 1px; align-self: stretch; background: var(--border); }

/* ===========================================================================
   TABS / PILLS (§十八)
   =========================================================================== */
.tabs {
  display: flex; gap: var(--s1); flex-wrap: wrap;
  margin-bottom: var(--s4);
  border-bottom: 1px solid var(--border);
}
.tab {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 14px; margin-bottom: -1px;
  border: 1px solid transparent; border-bottom: none;
  border-radius: var(--r-md) var(--r-md) 0 0;
  font-size: 13.5px; color: var(--text-soft);
  transition: background .12s ease, color .12s ease;
}
.tab:hover { background: var(--surface-hover); color: var(--text); text-decoration: none; }
.tab.is-active {
  background: var(--surface); color: var(--text); font-weight: 600;
  border-color: var(--border); border-bottom: 1px solid var(--surface);
}
.tab .tab-count {
  min-width: 20px; padding: 0 6px; border-radius: var(--r-pill);
  background: var(--neutral-soft); color: var(--text-soft);
  font-size: 11px; line-height: 17px; text-align: center;
  font-variant-numeric: tabular-nums;
}
.tab.is-active .tab-count { background: var(--primary-soft); color: var(--primary-strong); }
.tab.is-warn .tab-count { background: var(--danger-soft); color: var(--danger); }

/* ===========================================================================
   TABLES (§二十 / §二十九)
   =========================================================================== */
/* §二十九 + §三十七 — the TABLE scrolls, in BOTH axes, and the page body
   never does.
   
   This container is deliberately the scroll context. `overflow-x: auto` alone
   already makes it one (a browser resolves the other axis to `auto` too), and
   that has a consequence which is easy to get wrong and was: a `position:
   sticky` <th> inside it resolves its `top` against THIS box, not the
   viewport. With `top: var(--header-h)` the header was pushed 56px down into
   its own table and sat on top of the first row — a header that hides the row
   it is labelling.
   
   So the offset is 0 and the box is given a height, which is what makes the
   sticky header actually engage: the rows scroll under it, inside the card,
   with the page's own header still visible above. */
.table-wrap {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-1);
  overflow: auto;
  max-height: calc(100vh - 210px);
  overscroll-behavior: contain;
}
table.tbl { width: 100%; border-collapse: collapse; font-size: var(--fs-table); }
table.tbl th, table.tbl td {
  padding: 10px var(--s3);
  border-bottom: 1px solid var(--border);
  text-align: left; vertical-align: top;
}
table.tbl thead th {
  position: sticky; top: 0; z-index: 10;   /* §二十九 — see .table-wrap */
  background: var(--surface-muted);
  font-size: var(--fs-meta); font-weight: 600; color: var(--text-soft);
  letter-spacing: .02em; white-space: nowrap;
  border-bottom: 1px solid var(--border);
}
table.tbl tbody tr:last-child td { border-bottom: none; }
table.tbl tbody tr:hover { background: var(--surface-muted); }
table.tbl td.num { font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
table.tbl th.num { text-align: right; }
table.tbl .empty-cell { padding: var(--s8) var(--s4); text-align: center; color: var(--text-muted); }

/* -- QUEUE ROW (§二十一 .. §二十七) --------------------------------------
   Column widths are DECLARED, not left to the browser. §二十 names 「表格列宽
   失控」 as one of the failures, and an auto-laid-out table with a 60-char
   caption in it will always give that column the width and starve the rest. */
/* The widths are BALANCED against the narrowest supported content area:
   1366px minus the 232px rail minus 2×16px padding leaves ~1100px, and the
   fixed columns below sum to 740 — so 内容 keeps ~360px at 1366 and grows
   from there. The actions column is sized to what it actually holds
   (发布 + 查看 + ⋯ plus two gaps); the first cut of this table gave it 132px
   and clipped the overflow menu off the right edge, which is the same class
   of defect as §二十's 列宽失控, just self-inflicted. */
table.queue { table-layout: fixed; min-width: 1040px; }
table.queue col.c-check   { width: 36px; }
table.queue col.c-content { width: auto; }
table.queue col.c-source  { width: 130px; }
table.queue col.c-section { width: 140px; }
table.queue col.c-status  { width: 168px; }
table.queue col.c-publish { width: 110px; }
table.queue col.c-actions { width: 156px; }
table.queue td { vertical-align: top; }

.cell-content { display: flex; gap: var(--s3); min-width: 0; }
.cell-content .thumb-link { flex: none; }
.cell-content .body { min-width: 0; flex: 1; }
.cell-content .title {
  display: block; font-size: 13.5px; font-weight: 600; color: var(--text);
  line-height: 1.4;
  overflow: hidden; text-overflow: ellipsis;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.cell-content .title:hover { color: var(--primary); text-decoration: none; }
.cell-content .excerpt {
  margin-top: 2px; font-size: var(--fs-meta); color: var(--text-muted);
  line-height: 1.45;
  overflow: hidden; text-overflow: ellipsis;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.cell-content .meta {
  margin-top: var(--s1); font-family: var(--mono); font-size: 11px;
  color: var(--text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* §二十一 — 120×68 at 16:9. A placeholder that is the same SHAPE as a real
   cover keeps the row height identical whether or not a poster exists; a
   shorter fallback makes the table jitter as you scroll. */
.thumb, .thumb-empty {
  display: block; width: 120px; height: 68px;
  border-radius: var(--r-md); object-fit: cover;
  background: var(--surface-muted);
}
.thumb-empty {
  display: flex; align-items: center; justify-content: center;
  border: 1px dashed var(--border-strong);
  color: var(--text-muted); font-size: 18px;
}

.cell-source .name { font-size: 13px; color: var(--text); }
.cell-source .handle,
.cell-source .when { font-size: 11px; color: var(--text-muted); font-family: var(--mono); }

/* P9.1 时间口径 —— 三行时间各占一行，不换行折断。
   `white-space: nowrap` 是必需的：一个 `2026-09-15 10:06` 被折成两行之后，
   相邻两行的时间会在视觉上串在一起，而这一列的全部意义是让三个时间**分得
   开**。列宽给到内容宽度，窄屏由外层表格的 overflow-x 兜住 —— 既有登记：
   用 scrollWidth 判溢出会说谎，所以这里让表格横向滚，不让单元格挤。 */
.cell-time { white-space: nowrap; }
.cell-time .when {
  font-size: 11px; color: var(--text); font-family: var(--mono);
  line-height: 1.6;
}
.cell-time .when.muted { color: var(--text-muted); }

.cell-section .name { font-size: 13px; font-weight: 500; }
.cell-section .name.is-unset { color: var(--text-muted); font-weight: 400; }
.cell-section .why { margin-top: 3px; }
.cell-section select { width: 100%; font-size: 12px; padding: 3px 6px; }
/* §二十三 — a nudge is a LINE, not a paragraph, and only appears on
   disagreement. Agreement renders nothing at all. */
.cell-section .nudge {
  margin-top: 4px; font-size: 11px; line-height: 1.4;
  color: var(--warning); word-break: break-word;
}
.cell-section .nudge.is-anomaly { color: var(--danger); }

.cell-publish .state { font-size: 13px; font-weight: 500; }
.cell-publish .sub { font-size: 11px; color: var(--text-muted); }

.cell-actions { display: flex; gap: var(--s1); align-items: flex-start; flex-wrap: nowrap; }
.cell-actions .btn { padding: 3px 7px; }
.cell-actions .menu > summary { padding: 3px 7px; }

tr.is-nudge   > td:first-child { box-shadow: inset 3px 0 0 var(--warning); }
tr.is-anomaly > td:first-child { box-shadow: inset 3px 0 0 var(--danger); }
tr.is-selected { background: var(--primary-soft) !important; }

/* ===========================================================================
   BATCH ACTION BAR (§二十八)
   ===========================================================================
   Hidden until something is selected. §二十八 is explicit that an always-on
   row of batch buttons is noise: with nothing ticked every one of them is a
   button that cannot do anything. */
.batchbar {
  position: sticky; bottom: 0; z-index: 25;
  display: none;
  align-items: center; gap: var(--s3); flex-wrap: wrap;
  margin-top: var(--s4);
  padding: var(--s3) var(--s5);
  background: var(--side-bg); color: var(--text-invert);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-2);
}
.batchbar.is-open { display: flex; }
.batchbar .count { font-weight: 600; }
.batchbar .count b { font-size: 16px; }
.batchbar .grow { flex: 1; }
.batchbar .note { font-size: var(--fs-meta); color: var(--side-text-dim); }
.batchbar .btn { border-color: rgba(255,255,255,.28); background: transparent; color: var(--text-invert); }
.batchbar .btn:hover { background: rgba(255,255,255,.12); }
.batchbar .btn-primary { background: var(--primary); border-color: var(--primary); }
.batchbar .btn-primary:hover { background: var(--primary-strong); }

/* ===========================================================================
   PAGINATION (§三十)
   =========================================================================== */
.pager {
  display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap;
  margin-top: var(--s4);
}
.pager .summary { font-size: var(--fs-meta); color: var(--text-muted); }
.pager .grow { flex: 1; }
.pager-links { display: flex; gap: var(--s1); align-items: center; }
.pager-links a, .pager-links span {
  min-width: 32px; padding: 4px 9px; text-align: center;
  border: 1px solid var(--border); border-radius: var(--r-md);
  background: var(--surface); font-size: 13px; color: var(--text-soft);
}
.pager-links a:hover { background: var(--surface-hover); text-decoration: none; color: var(--text); }
.pager-links .is-current {
  background: var(--primary); border-color: var(--primary); color: var(--text-invert); font-weight: 600;
}
.pager-links .is-disabled { opacity: .45; }
.pager .size { display: flex; align-items: center; gap: 6px; font-size: var(--fs-meta); color: var(--text-muted); }

/* ===========================================================================
   EMPTY STATE / FLASH / BANNER
   =========================================================================== */
.empty {
  padding: var(--s8) var(--s5); text-align: center;
  color: var(--text-muted);
  background: var(--surface); border: 1px dashed var(--border-strong);
  border-radius: var(--r-lg);
}
.empty .empty-title { font-size: 15px; color: var(--text-soft); margin-bottom: var(--s1); }

.flash {
  display: flex; gap: var(--s2); align-items: flex-start;
  padding: 10px var(--s4); margin-bottom: var(--s3);
  border-radius: var(--r-md); font-size: 13.5px;
  border: 1px solid transparent; word-break: break-word;
}
.flash-ok    { background: var(--success-soft); color: var(--success); border-color: #bfe6cd; }
.flash-error { background: var(--danger-soft);  color: var(--danger);  border-color: #f2c7c1; }
.flash-warn  { background: var(--warning-soft); color: var(--warning); border-color: #ecd9a8; }

.banner {
  padding: 10px var(--s4); margin-bottom: var(--s4);
  border-radius: var(--r-md); font-size: 13.5px;
  border: 1px solid transparent;
}
.banner-danger { background: var(--danger-soft); color: var(--danger); border-color: #f2c7c1; }
.banner-warn   { background: var(--warning-soft); color: var(--warning); border-color: #ecd9a8; }
.banner-info   { background: var(--primary-soft); color: var(--primary-strong); border-color: #c9dcfa; }

/* ===========================================================================
   KEY/VALUE TABLE — the detail page's right rail and every settings screen
   =========================================================================== */
table.kv { width: 100%; border-collapse: collapse; font-size: 13px; }
table.kv th, table.kv td {
  padding: 7px 0; border-bottom: 1px solid var(--border);
  text-align: left; vertical-align: top;
}
table.kv th {
  width: 40%; font-weight: 500; color: var(--text-muted);
  padding-right: var(--s3);
}
table.kv tr:last-child th, table.kv tr:last-child td { border-bottom: none; }
table.kv th small { font-weight: 400; opacity: .8; }

/* ===========================================================================
   CONTENT DETAIL (§三十一)
   =========================================================================== */
.detail {
  display: grid;
  grid-template-columns: minmax(0, 65fr) minmax(300px, 35fr);
  gap: var(--s5);
  align-items: start;
  padding-bottom: 84px;              /* room for the sticky action bar */
}
.detail-rail { position: sticky; top: calc(var(--header-h) + var(--s5)); }
.detail-media img, .detail-media video { width: 100%; border-radius: var(--r-md); background: #000; display: block; }
.detail-media .placeholder {
  padding: var(--s8); text-align: center; color: var(--text-muted);
  background: var(--surface-muted); border-radius: var(--r-md);
  border: 1px dashed var(--border-strong);
}
.media-strip { display: flex; gap: var(--s2); flex-wrap: wrap; }
.media-strip .cell { width: 108px; }
.media-strip .cell img, .media-strip .cell .thumb-empty { width: 108px; height: 61px; }
.media-strip .cell .cap { font-size: 11px; color: var(--text-muted); font-family: var(--mono); }

.actionbar {
  position: fixed; bottom: 0; right: 0; left: var(--sidebar-w); z-index: 35;
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3) var(--s6);
  background: var(--surface); border-top: 1px solid var(--border);
  box-shadow: 0 -2px 10px rgba(16,24,40,.06);
}
.actionbar .inner {
  max-width: var(--main-max); margin: 0 auto; width: 100%;
  display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
}
.actionbar .grow { flex: 1; }

/* Caption provenance — carried over from 5B-2.1, restyled to the tokens. */
.caption-raw, .caption-body, .caption-footer {
  white-space: pre-wrap; word-break: break-word;
  border-left: 3px solid transparent; padding-left: var(--s2);
}
.caption-raw    { border-left-color: var(--border-strong); }
.caption-body   { border-left-color: var(--success); }
.caption-footer { border-left-color: var(--danger); opacity: .75; }

/* ===========================================================================
   LOGIN
   =========================================================================== */
.login-wrap { min-height: 100vh; display: grid; place-items: center; padding: var(--s5); }
.login-box {
  width: 100%; max-width: 360px; padding: var(--s7);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); box-shadow: var(--shadow-2);
}
.login-box .mark { font-size: 20px; font-weight: 700; letter-spacing: .06em; }
.login-box .sub { color: var(--text-muted); font-size: 13px; margin-bottom: var(--s5); }
.login-box form { display: flex; flex-direction: column; gap: var(--s3); }

/* ===========================================================================
   LEGACY CLASSES — the engineering screens (§三十三)
   ===========================================================================
   pool.html / detail.html / content_pool.html keep their markup. They are not
   redesigned this round (§四十一 lists them as 保留/降级), but they must not
   look like a different product either, so their class names are re-pointed
   at the same tokens. No engineering template was edited to achieve this. */
.data-table, .grid {
  width: 100%; border-collapse: collapse; font-size: var(--fs-table);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); overflow: hidden;
}
.data-table th, .data-table td, .grid th, .grid td {
  padding: 8px var(--s3); border-bottom: 1px solid var(--border);
  text-align: left; vertical-align: top;
}
.data-table thead th, .grid thead th {
  background: var(--surface-muted); font-size: var(--fs-meta);
  font-weight: 600; color: var(--text-soft); white-space: nowrap;
}
.data-table .truncate { max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.data-table .empty, .grid .empty { text-align: center; color: var(--text-muted); padding: var(--s6); }

.kv-table { width: 100%; border-collapse: collapse; background: var(--surface);
            border: 1px solid var(--border); border-radius: var(--r-lg); overflow: hidden; }
.kv-table th, .kv-table td { padding: 8px var(--s3); border-bottom: 1px solid var(--border);
                             font-size: var(--fs-table); text-align: left; }
.kv-table th { width: 220px; color: var(--text-muted); font-weight: 500; }

.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--s3); }
.stat { background: var(--surface); border: 1px solid var(--border);
        border-radius: var(--r-lg); padding: var(--s4); box-shadow: var(--shadow-1); }
.stat-value { font-size: 24px; font-weight: 650; font-variant-numeric: tabular-nums; }
.stat-label { font-size: var(--fs-meta); color: var(--text-muted); margin-top: 2px; }
.stat-danger .stat-value { color: var(--danger); }

.filter-form { display: flex; gap: var(--s3); align-items: flex-end; flex-wrap: wrap;
               margin-bottom: var(--s4); padding: var(--s3) var(--s4);
               background: var(--surface); border: 1px solid var(--border);
               border-radius: var(--r-lg); }
.filter-form label, .filters label { display: flex; flex-direction: column; gap: 3px;
                                     font-size: var(--fs-meta); color: var(--text-soft); }
.filters { display: flex; gap: var(--s3); align-items: center; flex-wrap: wrap;
           margin-bottom: var(--s4); padding: var(--s3) var(--s4);
           background: var(--surface); border: 1px solid var(--border);
           border-radius: var(--r-lg); }
.add-source-box { background: var(--surface); border: 1px solid var(--border);
                  border-radius: var(--r-lg); padding: var(--s3) var(--s4); margin-bottom: var(--s4); }
.detail-grid { display: grid; grid-template-columns: minmax(280px, 420px) 1fr;
               gap: var(--s5); align-items: start; }
.preview-video, .preview-poster { width: 100%; border-radius: var(--r-md); background: #000; }
.preview-placeholder { padding: var(--s8) var(--s5); text-align: center;
                       background: var(--surface-muted); border-radius: var(--r-md); color: var(--text-muted); }
.actions { display: flex; gap: var(--s2); margin-top: var(--s4); flex-wrap: wrap; }
.error-text { color: var(--danger); }
.album-order { margin-bottom: var(--s4); }
.asset-chip { display: inline-block; padding: 1px 8px; margin-right: 4px;
              border-radius: var(--r-pill); background: var(--neutral-soft);
              font-family: var(--mono); font-size: var(--fs-meta); }
.status-oversize { color: var(--warning); font-weight: 600; }
.status-missing  { color: var(--danger); }
.status-ready    { color: var(--success); }
.link-btn { background: none; border: none; padding: 0; color: var(--primary);
            cursor: pointer; text-decoration: underline; font-size: var(--fs-meta);
            font-family: inherit; }
.err { color: var(--danger); font-size: 11px; }
.counters { display: flex; flex-wrap: wrap; gap: var(--s3); margin: var(--s3) 0; }
.counters a, .counters > span {
  display: flex; flex-direction: column; align-items: center; min-width: 86px;
  padding: var(--s2) var(--s3); border-radius: var(--r-lg);
  background: var(--surface); border: 1px solid var(--border);
  text-decoration: none; color: inherit; }
.counters .n { font-size: 20px; font-weight: 650; line-height: 1.15; font-variant-numeric: tabular-nums; }
.counters .k { font-size: var(--fs-meta); color: var(--text-muted); }
.pill { display: inline-block; min-width: 18px; padding: 0 6px; margin-left: 4px;
        border-radius: var(--r-pill); background: var(--success);
        color: var(--text-invert); font-size: 11px; }
.pill-warn { background: var(--danger); }

/* ===========================================================================
   RESPONSIVE (§三十七)
   ===========================================================================
   Tested at 1920 / 1440 / 1366 / 1024. The breakpoints below are chosen so
   that each of those four widths lands in a layout that was designed for it
   rather than in the tail of the one above. */

/* 1600–: the container caps and centres; nothing stretches further. */

@media (max-width: 1500px) {
  /* 1440: six metric cards still fit but want less air. */
  :root { --s6: 20px; }
  .header-search input { width: 210px; }
}

@media (max-width: 1366px) {
  /* 1366 with a 232px sidebar leaves ~1100px of content. Six across is still
     legible; the metric value drops a step so it cannot collide. */
  .metric .m-value { font-size: 26px; }
  .header-search input { width: 180px; }
}

@media (max-width: 1200px) {
  .metrics { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .detail { grid-template-columns: minmax(0, 1fr); }
  .detail-rail { position: static; }

  /* The queue tightens rather than truncating. At 1024 — the narrowest width
     §三十七 requires — the untightened table needs 1040px and the 操作 column
     ends up behind the container's horizontal scroll, which is exactly
     「按钮挤掉」. Shaving the fixed columns and the cell padding brings the
     requirement to ~900px, which fits once the rail has collapsed. */
  table.queue { min-width: 900px; font-size: 12.5px; }
  table.queue col.c-source  { width: 108px; }
  table.queue col.c-section { width: 128px; }
  table.queue col.c-status  { width: 150px; }
  table.queue col.c-publish { width: 96px; }
  table.queue col.c-actions { width: 148px; }
  table.tbl th, table.tbl td { padding: 8px var(--s2); }
  .thumb, .thumb-empty { width: 96px; height: 54px; }
}

@media (max-width: 1024px) {
  /* §三十七 — below 1024 the sidebar becomes a drawer. It must OVERLAY, never
     push: pushing re-flows the table under it and the row you were reading
     moves. The toggle is a real <button> with aria-expanded. */
  .sidebar {
    transform: translateX(-100%);
    transition: transform .18s ease;
    box-shadow: var(--shadow-2);
  }
  body.nav-open .sidebar { transform: translateX(0); }
  body.nav-open .scrim { display: block; }
  .scrim {
    display: none; position: fixed; inset: 0; z-index: 35;
    background: rgba(16, 24, 40, .45);
  }
  .shell { margin-left: 0; }
  .sidebar-toggle { display: inline-flex; }
  .actionbar { left: 0; }
  .main { padding: var(--s4); }
  .metrics { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .field-row, .grid-2 { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 720px) {
  .metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .header-search { display: none; }   /* §十 — no dead controls; search moves
                                         into the page's own filter bar */
  .page-title { font-size: 21px; }
  .table-wrap { max-height: calc(100vh - 150px); }
}

@media print {
  .sidebar, .header, .batchbar, .actionbar, .toolbar, .pager { display: none !important; }
  .shell { margin-left: 0; }
}

/* VIDEO V2 §7/§8 —— 发布前必须看得见的三件事：清点、不完整警告、切分可信度。
   复用既有 .warn 的配色变量，不新建第二套告警视觉。 */
.media-tally { margin-bottom: 8px; font-size: 13px; }

.media-incomplete-notice {
  margin: 8px 0 12px;
  padding: 10px 12px;
  border: 1px solid var(--warn, #b8860b);
  border-left-width: 4px;
  border-radius: 6px;
  background: rgba(184, 134, 11, .08);
}
.media-incomplete-notice strong { display: block; margin-bottom: 4px; }
.media-incomplete-notice form { margin-top: 8px; }

/* 确认按钮刻意不是 .btn-primary：它与「发布到 WordPress」是两个不同的决定，
   看起来就不该一样。 */
.btn-warn {
  border-color: var(--warn, #b8860b);
  color: var(--warn, #b8860b);
  background: transparent;
}
.btn-warn:hover { background: rgba(184, 134, 11, .12); }

.batchbar .batch-confirm {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-right: 10px;
  font-size: 13px;
  white-space: nowrap;
}

/* TELEGRAM MEDIA LIBRARY V1 §50 —— 可复制的命令块。
   
   媒体库的凭据 Gate 要在屏幕上给出一条 Owner 能直接复制的 sudo 命令
   （§51：不要让 Owner 把任何 Telegram secret 发到对话里，操作步骤必须
   长在后台上）。而 <pre> 默认 white-space: pre —— 一条 110 字符的命令
   在 390px 窄屏上把 <body> 自己撑宽了 182px，也就是整个页面（含侧栏、
   页头）跟着左右晃。实测出来的就是这个数。
   
   pre-wrap + break-all 让它换行而不是撑宽；overflow-x: auto 是兜底，
   万一将来放进来一条不含空格的长串（比如一个 URL），滚动的仍然是这个
   块本身，不是页面。 */
pre.code {
  white-space: pre-wrap;
  word-break: break-all;
  overflow-x: auto;
  max-width: 100%;
  margin: var(--s2) 0;
  padding: var(--s3);
  background: var(--surface-muted);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--fs-meta);
  line-height: 1.5;
}

/* LIVE CANARY FIX —— 素材行里的「原文」折叠。
   
   主显示是 caption_clean（切掉了频道固定广告尾注）；原文没有丢，用一个原生
   <details> 折叠起来备查。用原生元素而不是 modal，是因为它不需要一行 JS，
   而这一轮不该为了一个 provenance 入口引入一套弹窗。 */
.raw-caption { margin-top: var(--s1); }
.raw-caption > summary {
  cursor: pointer;
  font-size: var(--fs-meta);
  list-style: none;
}
.raw-caption > summary::before { content: "▸ "; }
.raw-caption[open] > summary::before { content: "▾ "; }
.raw-caption pre.code { max-height: 14em; overflow-y: auto; }

/* LIVE CANARY FIX —— toolbar 里的项目要能收缩。
   
   .toolbar 是一条 flex 行，而 flex item 的 min-width 默认是 auto —— 也就是
   「不许比内容窄」。一个「长文字 + <select>」的 <label> 因此是一个不可断开
   的整体，在 390px 窄屏上把整张卡片撑到 480px，进而把 <body> 撑宽 107px。
   实测出来的就是这个数（媒体库的「重推范围」那一行）。
   
   min-width:0 让它可以收缩，flex-wrap 让它换行而不是溢出。两条都要：
   只加 wrap，单个 item 仍然可能比容器宽。 */
.toolbar { flex-wrap: wrap; }
.toolbar > label,
.toolbar > .check { min-width: 0; max-width: 100%; }
.toolbar select,
.toolbar input[type="search"],
.toolbar input[type="text"] { max-width: 100%; }

/* OPERATOR CLOSURE §二十一 —— header 上的时区注记。
   一次性说明，所以窄屏直接收起：它不值得挤掉面包屑或搜索框。 */
.header-tz {
  font-size: 12px;
  opacity: .75;
}

@media (max-width: 900px) {
  .header-tz { display: none; }
}
