/* ════════════════════════════════════════════════════════════════════════════
   app.css — Klado application styles.

   Colours come from theme.css tokens only; see that file for the palette.
   Structure: shared components first, then one section per page.
   ════════════════════════════════════════════════════════════════════════════ */

/* ─── Icon sizing helpers ───────────────────────────────── */
.ri {
  font-size: inherit;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

i[class^="ri-"], i[class*=" ri-"] { font-size: inherit; }

/* ─── Language spans (long formatted prose) ───────────────
   For paragraphs whose <b> structure makes node-by-node `中文 / English`
   pairing unnatural: keep each language as its own complete fragment and
   show exactly one. Short copy must still use the pair convention. The
   `data-lang` attribute on <html> is set by i18n.js `apply()`. */
html[data-lang="en"] [data-lang-zh],
html[data-lang="zh"] [data-lang-en] { display: none; }

/* ─── Top Nav ───────────────────────────────────────────── */
.nav {
  height: var(--nav-h);
  background: var(--surface);
  display: flex;
  align-items: center;
  padding: 0 20px;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 200;
  border-bottom: 1px solid var(--border);
  box-shadow: 0 1px 3px rgba(15,23,42,.04);
}

.nav-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 1rem;
  font-weight: 800;
  color: var(--text-1);
  letter-spacing: 0.08em;
  padding-right: 20px;
  border-right: 1px solid var(--border);
  white-space: nowrap;
  min-width: 200px;
  cursor: pointer;
  /* the brand is the way home, like every site */;
}

.nav-brand:hover { color: var(--accent); }

.nav-brand:focus-visible {
  outline: 2px solid rgba(37, 99, 235, .5);
  outline-offset: 3px;
  border-radius: 6px;
}

.nav-brand-icon {
  /* logo.png 是横版 lockup（云 + "Klado"），透明底，按原比例显示，不裁切不铺底。
     云的高度已对齐到 wordmark 的 cap height（与 "K" 等高、顶底齐平）。

     ⚠️ 30px → 22px（2026-10-04，用户要求「logo 放小一点」）。这个高度是整个 lockup
     的高度，wordmark 的 cap height 只占其中一部分；30px 时字标大到压过顶栏其余内容
     的字号，整条 nav 的视觉重心偏左。22px 保留可读性，又把左半边让回给模块标签。
     ⚠️ 改这个值必须同时量 nav 有没有挤掉第一个顶栏标签 —— 顶栏标签少一个就等于
     模块「不见了」，见 AGENTS.md「模块标签不许少」。 */
  width: auto;
  height: 22px;
  aspect-ratio: 1219 / 257;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

/* The company's own name, after the product's: "Klado x HZ".
 *
 * ⚠️ 字标是烤进 PNG 的，所以「和 Klado 一样」只能靠把网页这一侧调到最接近，而不是
 * 真的同字体。实际量过字形：几何无衬线、**平头端点**、**双层 'a'**、近圆的 'o'、
 * 笔画很粗、**紧字距** —— Montserrat / Proxima Nova 一类。Poppins 与 Nunito 被排除
 * （单层 'a'、圆头端点），Helvetica / Inter 被排除（'o' 没那么圆，属 neo-grotesque）。
 *
 * ⚠️ 两个具体的坑：
 * ① `letter-spacing` 必须**紧**。`.nav-brand` 自己写的是 `0.08em`（松），那是给一个
 *    曾经存在的文字版 logo 调的，字标本身是紧的 —— 继承松字距正是「怎么配都不像」的
 *    原因，这里显式覆盖。
 * ② 不继承 `--font`（Roboto Condensed，窄体）：窄体 + 粗字重会读成另一种东西，
 *    和旁边那张几何字标摆在一起像两套品牌。
 *
 * 字体没随包发（api/fonts/ 只有 README），所以栈里先写首选族，再落到系统里最接近的
 * Avenir Next / Century Gothic。装了 Montserrat 的机器会自动用上首选。 */
/* The wordmark's own family, bundled.
 *
 * ⚠️ Why this file exists at all, given Avenir Next was already within a few percent: the
 * fallback was never a *match*, it was a *coincidence of this Mac*. Avenir Next is a
 * system font — absent on Windows and on Linux, where the stack fell through to
 * Century Gothic or plain `system-ui` and the brand lockup rendered differently per
 * machine. Bundling makes the lockup the same picture everywhere.
 *
 * ⚠️ The measurement, because "close enough" is a claim that has to be earned. Rendered
 * Montserrat 600 and measured the ink: cap-height ratio **0.7050**, stem/cap **0.1844**
 * against the wordmark's 0.1983 ⇒ 0.93×. Avenir Next measured 0.7100 and 0.189, so the
 * 26px / weight-600 sizing in the rule below is unchanged by the switch — it was derived
 * from the wordmark, and the two families happen to agree on the numbers that set size.
 *
 * ⚠️ Latin subset only, on purpose. An organization's name may well be Chinese, and that
 * falls through to PingFang / Microsoft YaHei below — which is correct: a Latin geometric
 * face has no CJK glyphs, so pretending otherwise would only produce tofu. The face is
 * 16KB, not the 70KB+ full charset.
 *
 * ⚠️ `font-display: block` (matching the remixicon face) rather than `swap`: the brand is
 * above the fold and in the same place on every page, so a swap would show Avenir for a
 * frame and then reflow the top-left corner of the whole app. Blocking hides the name for
 * a moment instead, which nobody reads as a glitch.
 *
 * ⚠️ Only ONE weight is bundled — the one this lockup uses. A request for a weight that
 * is not here falls back to the *nearest available* rather than to the next family, so a
 * stray `font-weight: 500` would silently render at 600 and look like a bug. The separator
 * below therefore opts out of this family on purpose instead of asking for a 400.
 */
@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 600;
  font-display: block;
  src: url(vendor/fonts/montserrat-latin-600.woff2) format('woff2');
}

.nav-brand-org {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  margin-left: 10px;
  min-width: 0;
  font-family: 'Montserrat', 'Avenir Next', 'Century Gothic', 'Segoe UI', system-ui, sans-serif;
  /* ⚠️ 字号是**量**出来的，不是试出来的。直接量了 logo.png：字标里 "K" 的墨迹占
     第 14–250 行 = 237px，而整张图 257px 高 —— 字母高是 lockup 的 **92%**。
     lockup 渲染成 22px 高时，"Klado" 的 cap height = 20.3px；要让网页这一侧的字
     大小看过去是同一个字，字号就得 ≈ 29px（cap ≈ 0.70em 的几何体）。
     之前用的 .78rem(12.5px) 只有 cap 8.8px，不到一半 —— 字体对、字号不对，摆在一起
     就是「两套东西」，而不是一个 lockup。取 26px（cap ≈ 18.2px，字标的 90%）：
     留一点从属关系，又不至于大过顶栏里的模块标签。改这个值必须重新量 nav 有没有被挤坏。 */
  font-size: 26px;
  /* ⚠️ 字重同样是量出来的。量两边同一件事：竖笔画宽度 ÷ 大写字母高度。
     字标侧用 "l"（x 606..653）—— 几何体里的纯竖干，六个高度上都是恒定的 47px，
     除以 "K" 的 cap 237px ⇒ **0.198**。web 侧用 "H" 横杠**以上**那一段（横杠会把
     两根竖干连成一整条，量到的会是 H 的总宽，那是第一版量错的地方）。
     Avenir Next 在本机的实测：600 → 0.189（0.95×，几乎重合）、700 → 0.257、
     800 → 0.351（**1.77×**）。所以 800 就是「看着明显更粗」的原因 —— 不是错觉。
     900 与 800 相同，Avenir Next 到 800 为止，system-ui 也拿不出更粗的。 */
  font-weight: 600;
  letter-spacing: .005em;
  color: var(--text-3);
  line-height: 1;
  /* ⚠️ 1.2px 是**量**出来的，不是调出来的。把 nav 截图按墨迹切开量：字标的 cap
     占 20.20px、'HZ' 占 18.00px（Montserrat 26px 应有的 18.33px，差的 0.33 是 2x 抗
     锯齿），两者基线差了 +1.20px —— 名字浮得比 wordmark 高。一个 1px 的错位说不出
     哪里怪，但一个 lockup 里就是「排版没对齐」。
     ⚠️ 用 transform 而不是 margin：这是纯视觉校正，一旦用 margin/margin-top 就会
     参与 flex 布局，把顶栏第一个模块标签推走 —— 而顶栏标签少一个等于模块「不见了」。 */
  transform: translateY(1.2px);
}

/* ⚠️ The ellipsis has to be on the element that actually contains the text, which is
 * NOT the flex box above. `text-overflow` is inert on a flex container whose text sits
 * in a child: the container gets `overflow: hidden` and clips, but never draws the "…",
 * and the child keeps its full width — so a long company name pushes the whole top bar
 * sideways instead of shortening. Measured with a 21-character name: 295px of brand and
 * no ellipsis at all. */
.nav-brand-org-name {
  min-width: 0;
  max-width: 260px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The separator. `i` because it is a glyph, not a word — and it inherits nothing:
 * a bare <i> with no rule takes the browser's italic, which tilts it off the
 * baseline the company name sits on.
 *
 * ⚠️ It opts OUT of Montserrat, deliberately. Only weight 600 is bundled, and a request
 * for a weight that is not bundled resolves to the nearest *available* one rather than
 * to the next family — so inheriting this stack at weight 400 would silently draw the
 * `x` at 600, exactly as heavy as the company name it is supposed to sit between. A
 * multiplication sign is near-identical in any sans, so the app's own stack is the right
 * answer here and costs no second 16KB face. */
.nav-brand-org i {
  font-family: var(--font);
  font-style: normal;
  font-weight: 400;
  font-size: .62em;          /* scales with the name, so the pair stays one lockup */
  color: var(--text-3);
  opacity: .65;
  flex-shrink: 0;
}

/* logo.png 是白底深字的成品图，深色主题下 wordmark（#1A2737）压不住深色底，
   实测对比度仅 1.09:1。改用 logo-dark.png（浅色字 + 主题 accent 云朵），
   两张图叠在一起，按 data-theme 显隐 —— 不用 filter，蓝色云朵保持原色。 */
.nav-brand-icon img, .auth-brand-logo img,
.wl-brand-ico img, .wl-foot-ico img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

.nav-brand-icon img.logo-dark, .auth-brand-logo img.logo-dark,
.wl-brand-ico img.logo-dark, .wl-foot-ico img.logo-dark {
  display: none;
}

:root[data-theme="dark"] .nav-brand-icon img.logo-light,
:root[data-theme="dark"] .auth-brand-logo img.logo-light,
:root[data-theme="dark"] .wl-brand-ico img.logo-light,
:root[data-theme="dark"] .wl-foot-ico img.logo-light {
  display: none;
}

:root[data-theme="dark"] .nav-brand-icon img.logo-dark,
:root[data-theme="dark"] .auth-brand-logo img.logo-dark,
:root[data-theme="dark"] .wl-brand-ico img.logo-dark,
:root[data-theme="dark"] .wl-foot-ico img.logo-dark {
  display: block;
}

.nav-center {
  flex: 1;
  display: flex;
  align-items: center;
  padding: 0 20px;
  gap: 2px;
}

.nav-tab {
  padding: 6px 14px;
  font-size: 0.82rem;
  color: var(--text-3);
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.15s;
  white-space: nowrap;
}

.nav-tab:hover { color: var(--text-1); background: var(--hover); }

.nav-tab.active {
  color: var(--accent);
  background: var(--accent-soft);
  font-weight: 600;
}

#nav-tab-reports, #nav-tab-knowledge, #nav-tab-calendar, #nav-tab-inbox, #nav-tab-datacenter,
#nav-tab-dashboard, #nav-tab-workspace, .nav-tab-dash {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

/* ⚠️ 每个带 <img class="workspace-mark"> 的 nav tab 都必须有上面那条 display:inline-flex：
   .workspace-mark 是 display:block，落在普通 inline <span> 里就自己占一行、文字被挤到下一行
   （#nav-tab-calendar 就是这么装上去的：实测 tab 高 52.8px vs 邻居 34.0px，图标与文字垂直差 23px）。
   api/tests/verify_calendar_ui.py 有一条断言盯着"图标与文字在同一行"，别再漏。 */
.workspace-mark {
  display: block;
  width: 22px;
  height: 22px;
  object-fit: contain;
  flex-shrink: 0;
}

.nav-right {
  display: flex;
  align-items: center;
  gap: 14px;
  padding-left: 16px;
  border-left: 1px solid var(--border);
}

.nav-status-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ok);
}

.nav-status-text { font-size: 0.76rem; color: var(--text-3); }

.nav-avatar {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--accent-fill);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.76rem;
  font-weight: 700;
  color: var(--text-on-ink);
  cursor: pointer;
  transition: box-shadow .15s ease;
}

/* The avatar is the only door into preferences now, so it has to read as a control. */
.nav-avatar:hover { box-shadow: 0 0 0 3px var(--accent-soft); }

#nav-clock {
  font-size: 0.76rem;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ─── Shell ─────────────────────────────────────────────── */
.shell {
  display: flex;
  height: calc(100vh - var(--nav-h));
  margin-top: var(--nav-h);
  overflow: hidden;
}

/* ══ LEFT RAIL ═══════════════════════════════════════════
   Hidden until the pointer reaches the left edge. Three pieces, and the split matters:

   1. `.rail-hotzone` — a 14px strip pinned at x=0, always in the viewport.
      ⚠️ The rail itself CANNOT be the hover trigger. It is translated to
      `translateX(-100%)`, so it has no hover region at all and can never be
      reached — the classic "hover to open" drawer that never opens.
   2. `.rail` — the panel. It overlays the workspace (`position: fixed`) rather than
      taking flex width, because a flex sibling would reflow the whole app every
      time it slid out.
   3. `body.rail-open` — the one state class. Everything else keys off it, so
      "is it open" has exactly one answer in the DOM.

   ⚠️ Colours are semantic tokens only (AGENTS.md): a literal here is a rail that
   is right in one theme and unreadable in the other.

   ⚠️ Every piece of the rail starts at `--nav-h`, never at 0, for the same reason
   `.rpt-viewer` and `#datacenter-page` do: the top bar is `position: fixed` at
   `z-index: 200` and is the one thing that must never be covered. The hot zone used
   to be `top: 0`, which put a 14px strip of `z-index: 8600` — 43× the nav's — across
   the nav's own left padding. Nothing on the nav lives in those 14px today (it has
   20px of padding), so no button was actually lost, and `elementFromPoint(2..13, 26)`
   measured `div#rail-hotzone`. It is fixed anyway: a global overlay outranking the nav
   is the exact family of bug that hid the nav twice already, and the cost of being
   wrong here is a drawer popping open over the navigation. */

.rail-hotzone {
  position: fixed;
  left: 0;
  top: var(--nav-h);
  bottom: 0;
  width: 14px;
  z-index: 8600;
  /* No background and no pointer-events of its own would make it inert; it has to
     be a real hit target. Transparent is deliberate: the strip must not be visible. */
  background: transparent;
}

.rail {
  position: fixed;
  left: 0;
  top: var(--nav-h);
  bottom: 0;
  width: 248px;
  z-index: 8700;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border-right: 1px solid var(--border);
  box-shadow: 8px 0 24px rgba(0, 0, 0, 0.10);
  transform: translateX(-100%);
  transition: transform 180ms ease;
  overflow: hidden;
}

/* An embedded document renders no chrome at all (see the `app-embedded` rules), so
   a rail there would be a floating panel over somebody's report. */
html.app-embedded .rail,
html.app-embedded .rail-hotzone { display: none !important; }

body.rail-open .rail { transform: translateX(0); }

/* While the rail is out, a transparent sheet catches the pointer leaving it, so
   moving right closes it instead of the panel staying open over the content. It is
   *behind* the rail (z-index 8699 < 8700) so the rail itself stays clickable. */
.rail-scrim {
  position: fixed;
  inset: var(--nav-h) 0 0 0;
  z-index: 8699;
  background: transparent;
}
body:not(.rail-open) .rail-scrim { display: none; }

.rail-head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 10px 10px 6px;
}

.rail-new-folder {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 8px;
  border: 1px dashed var(--border);
  border-radius: 8px;
  background: transparent;
  color: var(--text-3);
  font-size: 12px;
  cursor: pointer;
  transition: color 120ms, border-color 120ms;
}
.rail-new-folder:hover { color: var(--accent); border-color: var(--accent); }
/* ⚠️ The <i> must not shrink. It is an icon FONT, so a squeezed box renders the
   glyph as a tofu box — which reads as "the font failed to load" and sends somebody
   looking for a missing webfont instead of at a one-line flex rule. */
.rail-new-folder i { flex: 0 0 auto; font-size: 15px; line-height: 1; }

.rail-modules {
  flex: 0 0 auto;
  overflow-y: auto;
  padding: 2px 8px 6px;
}

.rail-sep {
  flex: 0 0 auto;
  height: 1px;
  margin: 6px 12px;
  background: var(--border);
}

.rail-folders {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 2px 8px 16px;
}

.rail-empty {
  padding: 12px 14px;
  color: var(--text-3);
  font-size: 12px;
}

/* ── a module row: the short title the user renames ── */
.rail-mod {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 7px 8px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--text-1);
  font-size: 13px;
  text-align: left;
  cursor: pointer;
  transition: background 120ms;
}
.rail-mod:hover { background: var(--surface-2); }
.rail-mod.on { background: var(--accent-fill); color: var(--text-on-accent); }
.rail-mod.on .rail-mod-icon { opacity: 1; }
.rail-mod-icon { width: 18px; flex: 0 0 18px; display: flex; opacity: 0.7; }
.rail-mod-icon i, .rail-folder > i, .rail-folder-item > i {
  /* Same reason as `.rail-new-folder i`: an icon font in a shrinking box is a tofu
     box, and tofu looks like a missing webfont rather than a layout bug. */
  flex: 0 0 auto;
  font-size: 14px;
  line-height: 1;
}
.rail-mod-name {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rail-mod-rename {
  flex: 1;
  min-width: 0;
  padding: 2px 5px;
  border: 1px solid var(--accent);
  border-radius: 5px;
  background: var(--bg);
  color: var(--text-1);
  font-size: 13px;
  outline: none;
}
.rail-mod-acts {
  flex: 0 0 auto;
  display: none;
  gap: 2px;
}
.rail-mod:hover .rail-mod-acts,
.rail-mod:focus-within .rail-mod-acts { display: flex; }
.rail-mod-btn {
  display: flex;
  padding: 2px 4px;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: inherit;
  cursor: pointer;
  opacity: 0.75;
}
.rail-mod-btn:hover { opacity: 1; background: rgba(127, 127, 127, 0.16); }

/* ── the folder tree ── */
.rail-folder {
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 5px 6px;
  border-radius: 7px;
  color: var(--text-1);
  font-size: 13px;
  cursor: pointer;
  transition: background 120ms;
}
.rail-folder:hover { background: var(--surface-2); }
.rail-folder.open { background: var(--surface-2); }
.rail-folder.drop { background: var(--accent-fill); color: var(--text-on-accent); }
.rail-folder-caret {
  flex: 0 0 12px;
  display: flex;
  justify-content: center;
  font-size: 11px;
  color: var(--text-3);
  transition: transform 140ms;
}
.rail-folder.open > .rail-folder-caret { transform: rotate(90deg); }
.rail-folder-name {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rail-folder-acts {
  flex: 0 0 auto;
  display: none;
  gap: 2px;
}
.rail-folder:hover .rail-folder-acts,
.rail-folder:focus-within .rail-folder-acts { display: flex; }
.rail-folder-btn {
  display: flex;
  padding: 2px 4px;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: inherit;
  cursor: pointer;
  opacity: 0.7;
}
.rail-folder-btn:hover { opacity: 1; background: rgba(127, 127, 127, 0.16); }

.rail-children { margin-left: 14px; }

/* A card on the wall being dragged, so the user can see what they picked up. */
body.dragging-rail .rpt-card.drag-src,
body.dragging-rail .rail-folder-item.drag-src { opacity: 0.45; }

.rail-folder-item {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 6px 4px 22px;
  border-radius: 6px;
  color: var(--text-2);
  font-size: 12.5px;
  cursor: pointer;
}
.rail-folder-item:hover { background: var(--surface-2); color: var(--text-1); }
.rail-folder-item.gone { color: var(--text-3); text-decoration: line-through; cursor: default; }
.rail-folder-item-name {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ══ CENTER WORKSPACE ════════════════════════════════════ */

/* ── the rail's own right-click menu and folder picker ── */

/* ⚠️ Its own element, NOT the shared `.rpt-menu` the card walls use: that one is
   closed by each wall's `closeCardMenu()` and by its own document listener, so
   sharing it means opening one menu closes the other with the wrong slug on it. */
.rail-menu {
  position: fixed;
  z-index: 9800;
  /* Above `.rail` (8700) so it is not clipped or covered by the panel it belongs to. */
  min-width: 200px;
  padding: 4px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 10px 32px rgba(0, 0, 0, 0.18);
}
.rail-menu[hidden] { display: none; }
.rail-menu button {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 7px 9px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--text-1);
  font-size: 13px;
  text-align: left;
  cursor: pointer;
}
.rail-menu button:hover { background: var(--surface-2); }
.rail-menu button.danger { color: var(--danger); }
.rail-menu hr { margin: 4px 6px; border: 0; border-top: 1px solid var(--border); }

/* The folder chooser. Reuses `.rpt-modal`'s look (12px radius, same border and
   shadow) so it does not read as a different generation of dialog. */
.rail-picker-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 320px;
  overflow-y: auto;
  margin: 8px 0;
}
.rail-picker-row,
.rail-picker-new {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 8px 10px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--text-1);
  font-size: 13px;
  text-align: left;
  cursor: pointer;
}
.rail-picker-row:hover,
.rail-picker-new:hover { background: var(--surface-2); }
/* The nesting is shown by indentation set inline (padding-left per depth), so there
   is no `text-indent` to fight with and the icon stays put. */
.rail-picker-row i,
.rail-picker-new i { flex: 0 0 auto; font-size: 14px; color: var(--text-3); }
.rail-picker-new { color: var(--accent); }
.rail-picker-empty {
  padding: 12px 10px;
  color: var(--text-3);
  font-size: 13px;
}
.workspace {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--bg);
  min-width: 0;
}

.workspace-bar {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  padding: 0 20px;
  display: flex;
  align-items: center;
  height: 44px;
  gap: 10px;
  flex-shrink: 0;
}

.workspace-bar .breadcrumb {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 0.82rem;
  color: var(--text-3);
  line-height: 1;
  margin: 0;
  flex-wrap: nowrap;
}

.workspace-bar .breadcrumb i {
  display: flex;
  align-items: center;
  line-height: 1;
}

.breadcrumb-sep {
  color: var(--border-dk);
  font-size: 1rem;
  display: flex;
}

.breadcrumb-current { color: var(--text-1); font-weight: 600; }

.workspace-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
}

.btn {
  height: 30px;
  padding: 0 12px;
  border-radius: 5px;
  font-size: 0.82rem;
  font-weight: 500;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  transition: all 0.15s;
  border: 1px solid var(--border);
  font-family: var(--font);
}

.btn i { font-size: 1rem; }

.btn-ghost { background: transparent; color: var(--text-2); }

.btn-ghost:hover { background: var(--bg); border-color: var(--border-dk); }

.btn-primary, .rpt-modebtn.on, .rpt-fchip.on {
  background: var(--accent-fill);
  color: var(--text-on-ink);
  border-color: var(--accent-fill);
}

.btn-primary:hover { background: var(--accent-fill); }

.workspace-body {
  flex: 1;
  overflow-y: auto;
  padding: 20px;
}

.workspace-body::-webkit-scrollbar { width: 6px; }

.workspace-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }

/* Panels */
.panels {
  display: grid;
  grid-template-columns: 1fr;
  gap: 14px;
}

.panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}

/* ─── Mobile phone layout ─────────────────────────────────── */
@media (max-width: 768px) {
  /* Nav: show only brand icon + name */
  .nav-center { display: none !important;
  }
  .nav-right  { display: none !important;
  }
  .nav-brand  { border-right: none !important;
  min-width: 0;
  cursor: pointer;
  }
  /* Hide sidebar */
  .sidebar { display: none !important;
  }
  .sidebar-open-zone { display: none !important;
  }
  /* Overview: 1 container per row (already 1fr at 1180px, keep it) */
  .home-overview-grid { grid-template-columns: 1fr !important;
  }
  /* KPI and tracker cards: 2 per row */
  #home-kpi-grid,
  #kt-tracker-row { grid-template-columns: repeat(2,1fr) !important;
  }
  /* Data freshness cards: 2 per row */
  .dcard-row { grid-template-columns: repeat(2,1fr) !important;
  }
  /* SKU Calendar: 2 per row */
  .skucal-grid { grid-template-columns: repeat(2,1fr) !important;
  }
  /* Quick Module tiles: 2 per row */
  .tile-grid { grid-template-columns: repeat(2,1fr) !important;
  }
  /* Channel sell-in bars: horizontal scroll instead of overflow */
  #ch-sellin-bars { overflow-x: auto;
  flex-wrap: nowrap;
  }
  /* workspace body: tighter padding */
  .workspace-body { padding: 12px;
  }
  /* workspace-bar: hide non-essential parts
     ⚠️ `:not(:has(.ws-action-btn))` and NOT a plain `display:none` on the bar. The bar
     was empty on every page until 2026-10-06, so this rule hid an empty box and cost
     nothing. It now holds the Settings page's only Refresh, and an unconditional hide
     would take that away below 769px — leaving a four-card page with no way to reload
     it on exactly the screens least able to afford a reload. A page's own control is
     not one of the "non-essential parts"; the breadcrumb next to it still wraps. */
  .workspace-actions:not(:has(.ws-action-btn)) { display: none !important;
  };
}

.tile {
  border: 1px solid var(--border);
  border-radius: 7px;
  padding: 12px;
  cursor: pointer;
  transition: all 0.16s;
  background: var(--surface);
  display: flex;
  flex-direction: column;
  gap: 6px;
  position: relative;
  overflow: hidden;
}

.tile::after {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  background: var(--tile-c, var(--blue));
  opacity: 0;
  transition: opacity 0.16s;
}

.tile:hover {
  border-color: var(--tile-c, var(--blue));
  transform: translateY(-1px);
  box-shadow: 0 2px 10px rgba(0,0,0,0.06);
}

.tile:hover::after { opacity: 1; }

.tile.wip {
  opacity: 0.38;
  cursor: default;
  pointer-events: none;
}

@keyframes dbSlideIn {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}

/* ═══ DATA CENTER v2 ══════════════════════════════════════════ */
/* The Data Center is an overlay rather than a real page in the shell, because the file
   browser is a three-pane layout that does not want the workspace's own scroll. That
   choice is fine; painting over the top of the screen is not. `.nav` is itself
   `position: fixed; z-index: 200`, and this used to be `inset: 0` at the same z-index
   while coming later in the document — so it covered the global navigation and Data
   Center was the one page in the app with no nav bar at all. Starting below
   `--nav-h` puts it exactly where a shell page's content starts. */
      #datacenter-page {
  display: none;
  position: fixed;
  top: var(--nav-h);
  left: 0;
  right: 0;
  bottom: 0;
  background: var(--bg);
  z-index: 200;
  flex-direction: column;
  font-family: inherit;
}

#datacenter-page.visible { display: flex; }

/* The per-section half of the page toolbar. Files owns the location breadcrumb and
   its three file actions; Datasets owns the search box and the count. Only one group
   is shown at a time, so the page has ONE toolbar, the way Workspace and Inbox do. */
.dc-tools {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

/* The breadcrumb used to head its own full-width bar, where `flex: 1` pushed the
   action buttons to the right edge. In the toolbar it shares a row with them, so it
   takes only the width it needs. */
.dc-tools .dc-fb-breadcrumb { flex: 0 1 auto; }

.dc-stat-pill {
  display: flex;
  align-items: center;
  gap: 6px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 20px;
  padding: 4px 12px;
  font-size: 11px;
  color: var(--text-3);
}

.dc-stat-pill b { color: var(--text-1); font-size: 12px; }

.dc-stat-pill i { font-size: 13px; color: var(--blue); }

/* Body */
/* No padding. The 32px inset this used to carry was the whole reason the Data Center
   did not line up with the rest of the app: it opened a visible empty band under the
   toolbar, and it pushed the three panes 32px right of the toolbar's own 16px gutter,
   so "FILES" sat visibly off from the "Files" tab directly above it. Every other page
   runs its content straight off the toolbar's bottom border and lets each row carry
   its own padding; this does the same now. */
      .dc-body {
  flex: 1;
  overflow-y: auto;
  padding: 0;
}

.dc-step-line {
  flex: 1;
  height: 2px;
  background: var(--border);
  margin: 0 10px;
  border-radius: 1px;
  min-width: 40px;
  transition: background .3s;
}

.dc-step-line.done { background: var(--ok); }

/* Drop zone */
      .dc-dropzone {
  border: 2px dashed var(--border-strong);
  border-radius: 14px;
  padding: 60px 40px;
  text-align: center;
  cursor: pointer;
  background: linear-gradient(145deg, var(--surface-2) 0%, var(--accent-soft) 100%);
  transition: all .2s;
}

.dc-dropzone:hover, .dc-dropzone.dragover {
  border-color: var(--accent-fill);
  background: linear-gradient(145deg, var(--accent-soft) 0%, var(--accent-soft) 100%);
}

.dc-dropzone h4 {
  margin: 0 0 8px;
  font-size: 17px;
  font-weight: 700;
  color: var(--text-1);
}

.dc-dropzone p {
  margin: 0;
  color: var(--text-3);
  font-size: 13px;
}

/* Tables */
      .dc-table { width: 100%; border-collapse: collapse; }

.dc-table th {
  background: var(--surface-2);
  padding: 10px 16px;
  font-size: 11px;
  font-weight: 700;
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: .6px;
  border-bottom: 1px solid var(--border);
  text-align: left;
  white-space: nowrap;
}

.dc-table td {
  padding: 10px 16px;
  border-bottom: 1px solid var(--surface-3);
  font-size: 13px;
  color: var(--text-1);
  vertical-align: middle;
}

.dc-table tr:last-child td { border-bottom: none; }

.dc-table input[type=text] {
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 5px 10px;
  font-size: 12px;
  color: var(--text-1);
  outline: none;
  width: 140px;
}

.dc-table input[type=text]:focus { border-color: var(--accent-fill); }

.dc-table select {
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 5px 8px;
  font-size: 12px;
  color: var(--text-1);
  background: var(--surface);
  outline: none;
}

/* Badges */
      .dc-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 8px;
  border-radius: 20px;
  font-size: 11px;
  font-weight: 600;
}

.dc-badge-pg { background: var(--accent-soft); color: var(--accent-hover); }

.dc-badge-sales { background: var(--warn-soft); color: var(--warn); }

.dc-badge-market { background: var(--ok-soft); color: var(--ok); }

/* Result rows */
      .dc-result-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 20px 28px;
}

.dc-result-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 10px;
}

.dc-result-icon {
  width: 36px;
  height: 36px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 17px;
  flex-shrink: 0;
}

.dc-result-icon.ok { background: var(--ok-soft); color: var(--ok); }

.dc-result-icon.error { background: var(--danger-soft); color: var(--danger); }

.dc-result-icon.skip { background: var(--surface-3); color: var(--text-3); }

.dc-result-name, .dc-file-item-name, .dc-fb-folder-name {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-1);
}

.dc-result-msg, .dc-file-item-meta, .dc-wz-stat-lbl {
  font-size: 11px;
  color: var(--text-3);
  margin-top: 2px;
}

.dc-result-rows {
  margin-left: auto;
  font-size: 12px;
  color: var(--text-3);
  white-space: nowrap;
}

/* Buttons */
      .dc-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 9px 20px;
  border-radius: 8px;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  border: 1px solid transparent;
  transition: all .15s;
}

.dc-btn-primary {
  background: var(--accent-fill);
  color: var(--text-on-ink);
  border-color: var(--accent-fill);
}

.dc-btn-primary:hover { background: var(--accent-fill); }

.dc-btn-success {
  background: var(--ok);
  color: var(--text-on-ink);
  border-color: var(--ok);
}

.dc-btn-success:hover { background: var(--ok); }

.dc-btn-secondary {
  background: var(--surface);
  color: var(--text-1);
  border-color: var(--border);
}

.dc-btn-secondary:hover { background: var(--surface-2); }

.dc-btn:disabled { opacity: .45; cursor: not-allowed; }

/* ── DATASETS ── */
/* Full width, like the Inbox list. The old 1100px cap existed to keep a GRID of cards
   from stretching; a row list wants the width, because the numbers and the actions
   sit on one horizontal line and a narrow column pushes them into a second one.
   The 16px gutter is the toolbar's own, so the rows start under the section tabs. */
      .dc-datasets-wrap { max-width: none; margin: 0; padding: 0 16px; }

/* The Datasets section's own toolbar is gone for the same reason the Files one is —
   its search box, refresh and count moved up into the page toolbar. `.auth-brand`
   keeps the rule it always shared. */
.auth-brand {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 20px;
}

/* Now that the search box sits in the page toolbar rather than in a padded bar of its
   own, it is sized like the Workspace search input beside it. */
.dc-search {
  width: 190px;
  flex: 0 0 auto;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 6px 12px 6px 32px;
  font-size: 12px;
  color: var(--text-1);
  background: var(--surface);
  outline: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='15' height='15' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.35-4.35'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: 11px center;
}

.dc-search:focus { border-color: var(--accent-fill); }

.dc-ds-count {
  font-size: 12px;
  color: var(--text-3);
}

/* ── datasets: a list, not a wall of cards ──────────────────────────────────
   Datasets are read in the same way as Inbox items — scan the names, compare the
   numbers, act on one. Cards gave every dataset its own box and a drop shadow, so
   twenty of them turned into a grid of competing frames and the eye had nowhere to
   land. This is the Inbox idiom instead: full-width rows, a hairline between them,
   a hover tint, and a left accent bar marking the row you are pointing at.

   The layout is deliberately a plain block flow, not a grid: rows must span the full
   width so the divider reads as one continuous rule down the page.

   ⚠️ Before "fixing" a rule here, confirm it is the one that wins. This file is long and
   the card-era rules used to live in a second block much further down; a leftover
   `.dc-dataset-footer { display: grid; grid-template-columns: repeat(4,1fr) }` there had
   the same specificity as the flex rule here and simply came later, so it won — the row
   was a 4-column grid that pushed the fifth button onto a second line while the
   computed `display` said `grid` and every flex tweak in this block did nothing. Two
   things catch it: `getComputedStyle(el).display` (it will not be what you wrote) and
   `grep -c '^\s*\.dc-dataset-footer' app.css` (it will be more than one hit). */
/* ── dataset cards ────────────────────────────────────────────────────────
   A GRID OF CARDS. This reverses the column-of-rows that was here before —
   `api/tests/shot_datacenter_list_ui.py` was written to lock THAT in, and
   recorded two reasons for it. Both reasons are answered differently here
   rather than dismissed:

   · "six action buttons in one line wrap, and a wrapped Delete on its own
     line makes every row twice as tall as its content." A card is a block
     and has the height for a footer that wraps. What the row had to avoid
     at any cost costs nothing here.
   · "a row one button short drags the whole stat block sideways, so
     ROWS/COLS stop lining up and the list stops being comparable at a
     glance." The goal was comparability, and every card being the same
     width with its stats in the same place inside it gives that for free.

   `.rpt-card` is the in-app card shape and this follows it: same radius, same
   border, same hover lift. In a grid every card in a row is already stretched
   to the tallest one, so the cards line up without any height bookkeeping. */
/* The dataset WALL: a grid of folder cards shaped like the Workspace project cards
   (the `.dc-proj` selectors are merged into the `.kb-proj, .wr-proj` groups above
   rather than restated — a second copy of these declarations is a second copy to
   drift).

   ⚠️ 225px on purpose, and it is the same track `.wr-wall-grid` uses, because the
   reader asked for the Workspace folder look (2026-10-05). An earlier version used
   360px to fit a member row on the card — which meant the card had to show what was
   inside it, which meant it was a list wearing a card costume. Clicking a folder and
   showing the list INSIDE it is what lets the card be the size of a folder.

   ⚠️ Nothing in this grid may set `grid-column: 1 / -1` on a card. It was set on the
   container card on 2026-10-05 and it forced every card onto its own line while
   THESE tracks stayed at their full count — so a read of this rule says "grid" and
   `getBoundingClientRect()` says "one per row". Measure the children's `left`, not
   this declaration. */
.dc-datasets-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(225px, 1fr));
  gap: 14px;
  align-content: start;
  border-top: 0;
  padding-top: 2px;
}

/* The list INSIDE an open dataset folder. It replaces the wall in the same grid
   container, so both span the full width: a list that inherits a 225px track would
   put one table per line. */
.dc-ds-list-head,
.dc-ds-list-acts,
.dc-ds-list {
  grid-column: 1 / -1;
}

.dc-ds-list-head {
  padding: 2px 2px 0;
}

.dc-ds-list-head h3 {
  margin: 0 0 3px;
  font-size: .95rem;
  font-weight: 600;
  color: var(--text-1);
}

.dc-ds-list-head p {
  margin: 0 0 4px;
  font-size: .78rem;
  color: var(--text-2);
}

.dc-ds-list-acts {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 10px 2px 12px;
}

.dc-ds-list {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  overflow: hidden;
}

.dc-dataset-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0;
  padding: 14px;
  background: var(--surface);
  border: 1px solid var(--border);
  /* The 3px left edge is the selected marker and stays, so "which one is
     selected" still reads without a colour-only cue. */
  border-left: 3px solid transparent;
  border-radius: 12px;
  transition: box-shadow .16s, transform .16s, border-color .16s;
}

.dc-dataset-card:hover {
  transform: translateY(-2px);
  border-color: var(--accent-border);
  box-shadow: var(--shadow-2);
}

.dc-dataset-card.is-selected {
  background: var(--accent-soft);
  border-color: var(--accent-border);
  border-left-color: var(--accent-fill);
}

.dc-dataset-card-top {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

/* The icon drops to the Inbox's size. At 44px with an 11px rounded tile it was the
   loudest thing in the row, louder than the dataset name. */
.dc-dataset-icon {
  width: 30px;
  height: 30px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  flex-shrink: 0;
}

.dc-dataset-icon.pg { background: var(--accent-soft); }

.dc-dataset-icon.sales { background: var(--warn-soft); }

.dc-dataset-icon.market { background: var(--ok-soft); }

.dc-dataset-meta {
  flex: 1;
  min-width: 0;
}

.dc-dataset-name-wrap {
  display: flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
}

.dc-dataset-name {
  font-size: 13.5px;
  font-weight: 700;
  color: var(--text-1);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 1;
  min-width: 0;
}

.dc-rename-btn {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-3);
  padding: 2px 3px;
  border-radius: 4px;
  line-height: 1;
  font-size: 12px;
  flex-shrink: 0;
  opacity: 0;
  transition: opacity .15s;
}

.dc-dataset-card:hover .dc-rename-btn { opacity: 1; }

.dc-rename-btn:hover { color: var(--accent); background: var(--accent-soft); }

.dc-rename-input {
  font-size: 13.5px;
  font-weight: 700;
  color: var(--text-1);
  border: 1.5px solid var(--accent-fill);
  border-radius: 4px;
  padding: 1px 6px;
  outline: none;
  flex: 1;
  min-width: 0;
  background: var(--surface);
}

.dc-dataset-tablename {
  font-size: 11px;
  font-weight: 400;
  color: var(--text-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-family: monospace;
}

/* Inline after the name, not in a corner. It was absolutely positioned on the
   old 310px card, which put it on top of a long dataset name; in a card the
   name truncates instead (`.dc-dataset-name` has the ellipsis) and the badge
   never lands on top of text. */
.dc-dataset-badge-corner {
  position: static;
  flex-shrink: 0;
}

/* The numbers get a hairline above them: identity on top, figures below, so
   the card reads top-to-bottom instead of left-to-right across a wide row. */
.dc-dataset-stats {
  display: flex;
  gap: 18px;
  flex-shrink: 0;
  align-items: baseline;
  margin-top: 12px;
  padding-top: 11px;
  border-top: 1px solid var(--border);
}

.dc-stat-item {
  display: flex;
  align-items: baseline;
  gap: 4px;
  min-width: 64px;
}

.dc-stat-val {
  font-size: 13px;
  font-weight: 700;
  color: var(--text-1);
  font-variant-numeric: tabular-nums;
}

.dc-stat-lbl {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: .5px;
  color: var(--text-3);
}

/* Actions sit at the BOTTOM of the card, pinned there by `margin-top: auto` so
   a long name does not push them halfway down the card while a short one leaves
   them floating — that was the "row one button short drags the layout" problem in
   its other form, and a card can solve it outright.
   `flex-wrap: wrap` is the deliberate reversal: six labelled buttons do not fit
   268px in English, and on a card a two-line footer is a footer. */
.dc-dataset-footer {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  margin-top: auto;
  padding-top: 13px;
  white-space: nowrap;
}
/* Unshrinkable: a narrow card should move a whole button to the next line rather
   than squeeze one label out of shape. */
.dc-dataset-footer > .dc-icon-btn { flex: 0 0 auto; }

.dc-icon-btn {
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 5px 11px;
  border-radius: 6px;
  font-size: 11px;
  font-weight: 600;
  color: var(--text-3);
  background: var(--surface-2);
  border: 1px solid var(--border);
  cursor: pointer;
  transition: all .15s;
}

.dc-icon-btn i { font-size: 13px; }

.dc-icon-btn:hover { background: var(--surface-3); color: var(--text-1); }

.dc-icon-btn.danger:hover {
  background: var(--danger-soft);
  color: var(--danger);
  border-color: var(--danger-border);
}

.dc-icon-btn.primary { color: var(--accent-hover); }

.dc-icon-btn.primary:hover {
  background: var(--accent-soft);
  color: var(--accent-hover);
  border-color: var(--accent-border);
}

/* Update modal file list */
      .dc-file-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  cursor: pointer;
  border-bottom: 1px solid var(--surface-3);
  transition: background .15s;
}

.dc-file-item:last-child { border-bottom: none; }

.dc-file-item:hover { background: var(--surface-2); }

.dc-file-item.selected { background: var(--accent-soft); }

.dc-file-item-icon {
  font-size: 20px;
  color: var(--ok);
  flex-shrink: 0;
}

.dc-file-item-badge {
  margin-left: auto;
  font-size: 11px;
  background: var(--ok-soft);
  color: var(--ok);
  padding: 2px 8px;
  border-radius: 10px;
  font-weight: 600;
  white-space: nowrap;
}

.dc-file-item-badge.match { background: var(--warn-soft); color: var(--warn); }

.dc-modal-head {
  display: flex;
  align-items: center;
  padding: 16px 20px;
  border-bottom: 1px solid var(--surface-3);
}

.dc-modal-head h3 {
  margin: 0;
  font-size: 15px;
  font-weight: 700;
  color: var(--text-1);
  flex: 1;
}

.dc-modal-foot {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 14px 20px;
  border-top: 1px solid var(--surface-3);
  flex-shrink: 0;   /* the actions must never be what gets scrolled away */
}

/* ── File Browser (3-pane) ──
   The panes are NOT cards. They sit directly on the page background and are told apart
   by a single hairline each, which is the Inbox idiom: a white panel floating on the
   grey page reads as a box you could pick up, and a list you are scanning does not
   work that way. Everything below that used to paint a pane `var(--surface)` now
   paints it `var(--bg)`.

   The row rule changed for the same reason and it was the sharper half of the problem:
   it was `var(--surface-2)`, which in the light theme is #f8fafc on a #ffffff row —
   a contrast of about 1.03:1, i.e. a line that is not really there. The rows floated
   free of each other, which is exactly what "put it in a card" looks like without
   having drawn one. `--border` is the same hairline the Inbox rows use. */
      .dc-fb-page {
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow: hidden;
  background: var(--bg);
}

/* The Files section used to head its own bar — breadcrumb on the left, Refresh /
   New Folder / Upload on the right — and then the breadcrumb moved into the page
   toolbar, where it was a chain of `/`-separated chips. All of that is gone: the
   location is `.dc-crumbs` now, a row of its own under the toolbar.

   ⚠️ This comment used to say it sits "the way the knowledge page's does, with the
   same markup and the same rules". Both halves were wrong and the first is now
   wronger. The knowledge page had NO `.kb-crumbs` rule at all — grep the stylesheets
   for `kb-crumb` and the only hit was the `[hidden]` half — so the two rows never
   shared anything but a class name, and that row has since been deleted outright.
   `.dc-crumbs` is now the only breadcrumb in the app. Do not assume a sibling. */

.dc-fb-progress {
  padding: 6px 14px;
  background: var(--accent-soft);
  border-bottom: 1px solid var(--accent-border);
  font-size: 12px;
  color: var(--accent);
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}

/* The folder TREE is gone. A tree that shows every folder at once is the right
   control for a filesystem you navigate by location; this page opens on a wall of
   cards and moves by opening one, so the tree duplicated the cards and added a
   third place to be. `.dc-fb-move-item.selected` survives because the move dialog
   is still a list — it just is not a tree any more. */
.dc-fb-move-item.selected {
  background: var(--accent-soft);
  color: var(--accent-hover);
  font-weight: 600;
}

/* ══ Data Center: the wall and the folder view ══════════════════════════════
   Data Center opens on a wall of folder cards, the same shape the knowledge page
   opens on, and a folder is 目录 on the left and the table on the right. The
   three-pane browser this replaced (a folder TREE, a file LIST, a preview PANE)
   had three ways to move between folders at once — the tree, the breadcrumb, and
   clicking through rows — and no way to see the whole picture at the top.

   ⚠️ Nothing here writes a colour literal: every value is a token from theme.css,
   so both themes are covered by construction. The cover reuses `--cover-gradient`,
   which is what the knowledge cards and the Workspace cards use, so the three
   walls are read with one eye. */

/* ⚠️ No `display` on this rule, and that is deliberate. The wall and the folder view
   are toggled with the `hidden` attribute, and the browser's `[hidden] { display:
   none }` is a DEFAULT that any author `display` would beat. `.dc-crumbs` above
   DOES need `display: flex` to lay its parts out, so it carries its own explicit
   `[hidden]` rule; this one and `.dc-fb-body` below are laid out by their own
   context, so they must never grow a `display` — the day one does, the wall and the
   directory render on top of each other and the attribute silently stops working. */
.dc-wall {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 18px 20px 28px;
}

.dc-wall-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill,minmax(225px,1fr));
  gap: 12px;
  align-content: start;
}

.dc-wall-loading {
  grid-column: 1 / -1;
  padding: 30px;
  text-align: center;
  color: var(--text-3);
  font-size: .8rem;
}

.dc-wall-loading i { font-size: 22px; }

/* One card shape, used for the root card, for every folder, and for the "new
   folder" tile. They are deliberately the same box: a wall that ends in a
   differently-sized button is a wall the eye has to search before it can create
   anything. */
.dc-folder {
  position: relative;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  overflow: hidden;
  cursor: pointer;
  text-align: left;
  padding: 0;
  font: inherit;
  color: inherit;
  transition: box-shadow .18s, border-color .18s, transform .18s;
}

.dc-folder:hover { border-color: var(--accent); transform: translateY(-1px); }
.dc-folder:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.dc-folder-cover {
  display: block;
  position: relative;
  aspect-ratio: 16/9;
  overflow: hidden;
  background: var(--cover-gradient);
}

/* The placeholder in a cover: an icon with a line of text under it, which is what
   the knowledge cards and the Workspace cards do. The TEXT is the load-bearing
   half — a 16:9 band with one icon floating in the middle of it reads as a card
   whose picture failed to load, and no icon size fixes that; the line of text is
   what turns the band into "this is a folder, there is no cover".

   ⚠️ `i.` prefix on the icon: the remixicon rules are an inline `<style>` at the top
   of index.html and `app.css` loads below it, so `.ri { font-size: inherit }` has
   the same specificity as a bare class here and wins on ORDER. A 26px icon measured
   14px in a real browser with the rule sitting in the file saying 26px. Same trap
   as `.dc-fb-icon`; measure `getComputedStyle(icon).fontSize` before simplifying. */
.dc-folder-cover-ph {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  color: var(--text-3);
}

.dc-folder-cover-ph i { font-size: 26px; }
.dc-folder-cover-ph span { font-size: .7rem; }

.dc-folder-body { display: block; padding: 9px 11px 11px; }

.dc-folder-title {
  display: block;
  margin: 0 0 3px;
  font-size: .86rem;
  font-weight: 600;
  line-height: 1.35;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Both counts, because they answer different questions: a folder can hold many
   source files and no dataset yet (nothing imported), or one dataset and no loose
   files. Showing only files would say the second folder is empty while it holds a
   table. */
.dc-folder-sub {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: .72rem;
  color: var(--text-3);
}

.dc-folder-new {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 150px;
  border: 1px dashed var(--border);
  border-radius: 10px;
  background: none;
  color: var(--text-3);
  font: inherit;
  cursor: pointer;
}

.dc-folder-new:hover { border-color: var(--accent); color: var(--accent); }
.dc-folder-new:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.dc-folder-new i { font-size: 22px; }
.dc-folder-new span { font-size: .78rem; }

/* 数据目录 / <folder>. Same reason as the knowledge page's breadcrumb: leaving a
   folder is one click from the header, not a hunt for a back arrow. */
/* One header height for both panes, read by the breadcrumb row and the table pane's
   title bar. It was a fact about three numbers happening to be typed the same; it is
   now a fact about the stylesheet. */
#datacenter-page {
  --pane-hd-h: 40px;
  --pane-hd-fs: 13px;
  --pane-hd-pad: 0 10px;
}

/* ⚠️ NO `display` here, and that is the whole reason for the note.
   This rule used to cite `.kb-crumbs` as its cautionary example — "that one sets
   `display: flex`, so the breadcrumb never actually hides on the knowledge page".
   Do not go looking for it: no `.kb-crumbs` rule with a `display` ever existed in
   this stylesheet, and the knowledge page's breadcrumb row has since been deleted
   along with it. The trap this note describes is real, but `.dc-crumbs` is now the
   only row it can happen to, which is why the example has to be its own sibling.

   `.dc-crumbs` is only ever toggled, never permanently on, so it must not set
   `display` at all. Its own `X[hidden] { display: none }` below is the explicit
   half of the same contract: a rule that would otherwise win, stated on purpose. */
.dc-crumbs[hidden] { display: none; }

.dc-crumbs {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 20px;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  font-size: .78rem;
  color: var(--text-3);
}

.dc-crumb {
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  color: var(--accent);
  cursor: pointer;
}

.dc-crumb:hover { color: var(--accent-hover); text-decoration: underline; }
.dc-crumb[aria-current="page"] { color: var(--text-1); cursor: default; font-weight: 600; }
.dc-crumb[aria-current="page"]:hover { text-decoration: none; }

/* ⚠️ The `[hidden]` half is not optional and not decoration. `.dc-fb-body` is a
   flex row (it has to be, to put 目录 and the table side by side), and `display: flex`
   BEATS the browser's `[hidden] { display: none }` — that rule is a default, not a
   priority. Without the line below, `dcShowWall()` setting `body.hidden = true` does
   nothing visible: the directory stays exactly where it was, on top of the cards the
   wall just drew. No error, no console message — the page just stops working in the
   way that is hardest to notice, because both views are individually correct. */
.dc-fb-body[hidden] { display: none; }

/* Two panes: what is in this folder on the left, the table on the right. The
   left pane's width is the knowledge page's (`.kb-toc` is 284px) so the two
   directory views in the app line up with each other. */
.dc-fb-body {
  display: flex;
  flex: 1;
  overflow: hidden;
  min-height: 0;
}

.dc-toc {
  width: 284px;
  flex-shrink: 0;
  border-right: 1px solid var(--border);
  background: var(--surface);
  overflow-y: auto;
  padding: 8px 8px 20px;
  position: relative;
}

.dc-toc::-webkit-scrollbar { width: 6px; }
.dc-toc::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }

/* The drop target. ⚠️ `.dc-toc-drop` sets no `display` of its own beyond `relative`
   positioning, so the hint — which is `position:absolute; inset:8px` — is the only
   thing that changes on drag. Anything with `display` here would beat nothing and
   shift every row down by the height of the hint. */
.dc-toc-drop {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.dc-fb-drop-hint {
  display: none;
  position: absolute;
  inset: 8px;
  background: var(--accent-soft);
  border: 2px dashed var(--accent-border);
  border-radius: 10px;
  z-index: 10;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 10px;
  font-size: 13px;
  color: var(--accent);
}

.dc-toc-drop.dragging .dc-fb-drop-hint { display: flex; }

/* A row, not a table row. The three kinds (subfolder, file, dataset) share one
   row shape so the list does not change height when the reader clicks through
   them, and each row is a `role="button"` with a keydown handler because the
   thing a reader clicks here IS a button — it was a `<tr>` with an onclick,
   which is focusable by accident and not at all by keyboard. */
.dc-fb-row {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 6px 10px;
  border-radius: 6px;
  border-bottom: 1px solid var(--border);
  font-size: 12px;
  cursor: pointer;
  transition: background .1s;
}

.dc-fb-row:hover { background: var(--surface-3); }
.dc-fb-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.dc-fb-row.selected {
  background: var(--accent-soft);
  box-shadow: inset 3px 0 0 var(--accent-fill);
}

.dc-fb-row.ctx-active { background: var(--accent-soft); }

/* ⚠️ `i.` prefix for the same reason as `.dc-folder-cover-icon`: the inline
   remixicon block sets `.ri { font-size: inherit }` and loads BEFORE this file, so
   a single class loses to it on order and the icon renders at whatever the parent
   happens to be. The row icons are 15px and the cover icon is 46px; both are
   invisible if these lose. */
i.dc-fb-icon { font-size: 15px; flex-shrink: 0; }
i.dc-fb-folder-row .dc-fb-icon { color: var(--warn); }
i.dc-fb-icon--ds { color: var(--accent); }

/* ⚠️ The name is what a reader chooses the row by, so it is the only part of a row
   that is allowed to be as long as it needs. Everything else is metadata: it gives
   up space first.

   This was not true at first. The row carried a "from 2026-10" chip, a "Dataset"
   chip and a "25 rows" count, and in English those three together took more width
   than a 284px pane has — so the name ellipsised to two characters ("销售…")
   while the metadata stayed whole. The list became a column of badges with the one
   word that identifies each row squeezed out of it.

   So the name is `flex: 1` with no min-width fight, and each chip is allowed to
   shrink and hide rather than push: `flex-shrink` is high on the chips, the arrow
   is `flex: 0 0 auto` so "there is a folder inside" is never the thing that
   disappears, and the count is the last to go. */
.dc-fb-name {
  color: var(--text-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  display: block;
  flex: 1 1 auto;
  min-width: 4em;
}

.dc-fb-kind {
  flex-shrink: 8;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 10px;
  color: var(--text-3);
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--surface-3);
}

.dc-fb-badge {
  flex-shrink: 8;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 10px;
  color: var(--accent);
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--accent-soft);
}

.dc-fb-go { flex: 0 0 auto; color: var(--text-3); font-size: 13px; }

.dc-fb-meta {
  /* Last to give up space, and the reason the name above is allowed to be long. */
  flex-shrink: 4;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-3);
  font-size: 11px;
  padding: 0;
}

.dc-fb-empty {
  padding: 50px 16px;
  text-align: center;
  color: var(--text-3);
}

.dc-fb-empty i {
  font-size: 36px;
  display: block;
  margin-bottom: 8px;
}

/* right: the table */
.dc-table-pane {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--bg);
}

.dc-fb-preview-empty {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: var(--text-3);
  gap: 10px;
  text-align: center;
  padding: 20px;
}

.dc-fb-preview-empty i { font-size: 44px; }
.dc-fb-preview-empty span { font-size: 13px; max-width: 340px; line-height: 1.5; }

.dc-fb-preview-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  height: var(--pane-hd-h);
  padding: var(--pane-hd-pad);
  border-bottom: 1px solid var(--border);
  background: var(--bg);
  flex-shrink: 0;
}

.dc-fb-preview-bar .dc-fb-pv-name {
  font-size: var(--pane-hd-fs);
  font-weight: 600;
  color: var(--text-1);
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dc-fb-preview-bar .dc-fb-pv-size { font-size: 11px; color: var(--text-3); }

.dc-fb-pv-table-wrap {
  flex: 1;
  overflow: auto;
  background: var(--bg);
}

/* ⚠️ The table is `width: max-content` inside a scrolling pane, not `width: 100%`:
   a dataset with 40 columns squeezed into the pane width renders 40 unreadable
   12px columns, and the reader scrolls sideways to read the first row instead of
   scrolling down to read the rows. The first column stays put by being the one
   with a min-width the pane respects. */
.dc-fb-pv-table {
  border-collapse: collapse;
  font-size: 12px;
  white-space: nowrap;
}

.dc-fb-pv-table th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface-2);
  color: var(--text-2);
  font-weight: 600;
  text-align: left;
  padding: 7px 10px;
  border-bottom: 1px solid var(--border);
  border-right: 1px solid var(--border);
}

.dc-fb-pv-table td {
  padding: 5px 10px;
  border-bottom: 1px solid var(--border);
  border-right: 1px solid var(--border);
  color: var(--text-2);
  max-width: 320px;
  overflow: hidden;
  text-overflow: ellipsis;
}

.dc-fb-pv-table tbody tr:hover td { background: var(--surface-3); }

.dc-fb-preview-content {
  flex: 1;
  overflow: auto;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 16px;
}


/* image preview */
      .dc-fb-pv-img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: 6px;
  box-shadow: 0 2px 16px rgba(0,0,0,.1);
}

/* pdf preview */
      .dc-fb-pv-iframe {
  width: 100%;
  height: 100%;
  border: none;
  border-radius: 4px;
}

/* ⚠️ `.dc-fb-pv-table-wrap` is declared ONCE, above, with `flex: 1; overflow: auto`.
   It used to be declared a second time here with `width: 100%`, inside a comment
   that said "table preview" — the same selector, same context, a different value.
   Same specificity, so the later one silently won: the wrap stopped being a flex
   child that fills the pane, and the sheet-tab row above it stopped sitting on a
   fixed-height header. It is deleted rather than merged, because "the pane fills
   the remaining height" is the whole job and the second rule contradicted it. */

.dc-fb-pv-sheet-tabs {
  display: flex;
  gap: 4px;
  padding: 0 14px 0;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
  flex-shrink: 0;
}

.dc-fb-pv-sheet-tab {
  padding: 6px 12px;
  font-size: 12px;
  color: var(--text-3);
  cursor: pointer;
  border-bottom: 2px solid transparent;
}

.dc-fb-pv-sheet-tab.active {
  color: var(--accent-hover);
  border-bottom-color: var(--accent-fill);
  font-weight: 600;
}

/* context menu */
      .dc-fb-ctx {
  position: fixed;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 6px 24px rgba(0,0,0,.13);
  padding: 4px 0;
  z-index: 9999;
  min-width: 168px;
}

.dc-fb-ctx-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 14px;
  font-size: 13px;
  color: var(--text-1);
  cursor: pointer;
  white-space: nowrap;
}

.dc-fb-ctx-item:hover { background: var(--surface-3); }

.dc-fb-ctx-item.danger { color: var(--danger); }

.dc-fb-ctx-item.danger:hover { background: var(--danger-soft); }

.dc-fb-ctx-sep { border-top: 1px solid var(--surface-3); margin: 3px 0; }

/* The dataset card's menu names what it acts on. A menu that opens on a card
   titled "华东 2026" and offers four verbs with no subject makes the reader
   re-read the wall to find out which card is now armed — and a menu that opened
   on the WRONG card is the worst kind of ambiguous. One line, not a header. */
.dc-at-local {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}

/* The status after picking a local file. It is the only place the reader learns
   whether the upload landed and how many sheets came with it — without it a
   failed upload is a dialog that looks exactly like an idle one. */
.dc-at-local-hint {
  font-size: 12px;
  color: var(--text-3);
}

.dc-ds-ctx-title {
  padding: 5px 14px 6px;
  font-size: 11px;
  color: var(--text-3);
  max-width: 240px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  border-bottom: 1px solid var(--border);
  margin-bottom: 3px;
}

.dc-fb-list-bar .dc-icon-btn {
  width: 28px;
  height: 28px;
  font-size: 14px;
}

/* shared small modal */
      .dc-sm-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(15,23,42,.4);
  z-index: 10000;
  align-items: center;
  justify-content: center;
}

.dc-sm-overlay.open { display: flex; }

.dc-sm-box {
  background: var(--surface);
  border-radius: 12px;
  width: 380px;
  max-width: 95vw;
  box-shadow: 0 20px 60px rgba(0,0,0,.18);
  overflow: hidden;
}

.dc-sm-head {
  padding: 14px 18px;
  border-bottom: 1px solid var(--surface-3);
  display: flex;
  align-items: center;
  gap: 10px;
}

.dc-sm-head h3 {
  font-size: 14px;
  font-weight: 700;
  color: var(--text-1);
  flex: 1;
  margin: 0;
}

.dc-sm-body {
  padding: 16px 18px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.dc-sm-body label {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-3);
  display: block;
  margin-bottom: 4px;
}

.dc-sm-body input {
  width: 100%;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 7px;
  font-size: 13px;
  box-sizing: border-box;
  outline: none;
}

.dc-sm-body input:focus { border-color: var(--accent-fill); box-shadow: 0 0 0 3px rgba(59,130,246,.12); }

.dc-sm-foot {
  padding: 12px 18px;
  border-top: 1px solid var(--surface-3);
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

/* confirm modal */
      .dc-sm-confirm-icon {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 22px;
  flex-shrink: 0;
}

.dc-sm-confirm-icon.danger { background: var(--danger-soft); color: var(--danger); }

.dc-sm-confirm-msg {
  font-size: 13px;
  color: var(--text-2);
  line-height: 1.5;
  margin: 0;
}

.dc-sm-confirm-filename {
  font-weight: 600;
  color: var(--text-1);
  word-break: break-all;
}

/* move modal folder list */
      .dc-fb-move-tree {
  max-height: 220px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 8px;
  margin-top: 2px;
}

.dc-fb-move-item {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 7px 12px;
  cursor: pointer;
  font-size: 13px;
  color: var(--text-1);
  transition: background .1s;
}

.dc-fb-move-item:hover { background: var(--surface-3); }

.dc-fb-move-item i { color: var(--warn); flex-shrink: 0; }

.dc-fb-move-item.root i { color: var(--text-3); }

/* ── Import Wizard ─────────────────────────────────────────────── */
      .dc-wz-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(15,23,42,.5);
  z-index: 10000;
  align-items: center;
  justify-content: center;
}

.dc-wz-overlay.open { display: flex; }

.dc-wz-box {
  background: var(--surface);
  border-radius: 14px;
  width: 92vw;
  max-width: 1100px;
  height: 88vh;
  max-height: 800px;
  box-shadow: 0 24px 80px rgba(0,0,0,.25);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* step bar */
      .dc-wz-stepbar {
  display: flex;
  align-items: center;
  padding: 0 24px;
  border-bottom: 1px solid var(--surface-3);
  background: var(--surface);
  flex-shrink: 0;
  gap: 0;
}

.dc-wz-step {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 14px 18px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-3);
  cursor: default;
  border-bottom: 2px solid transparent;
  transition: all .15s;
}

.dc-wz-step.active { color: var(--accent); border-bottom-color: var(--accent-fill); }

.dc-wz-step.done { color: var(--ok); }

.dc-wz-step-num {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--border);
  color: var(--text-3);
  font-size: 11px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.dc-wz-step.active .dc-wz-step-num { background: var(--accent-fill); color: var(--text-on-ink); }

.dc-wz-step.done  .dc-wz-step-num { background: var(--ok); color: var(--text-on-ink); }

.dc-wz-step-sep {
  flex: 1;
  height: 1px;
  background: var(--border);
  margin: 0 4px;
}

/* panels */
      .dc-wz-panel {
  flex: 1;
  overflow: hidden;
  display: none;
  flex-direction: column;
}

.dc-wz-panel.active { display: flex; }

/* panel inner layout */
      .dc-wz-phead { padding: 16px 24px 10px; flex-shrink: 0; }

.dc-wz-phead h4 {
  margin: 0;
  font-size: 14px;
  font-weight: 700;
  color: var(--text-1);
}

.dc-wz-phead p {
  margin: 4px 0 0;
  font-size: 12px;
  color: var(--text-3);
}

.dc-wz-pbody {
  flex: 1;
  overflow-y: auto;
  padding: 0 24px 16px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

/* step1 — config */
      .dc-wz-form-row { display: flex; gap: 14px; }

.dc-wz-form-group {
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex: 1;
}

.dc-wz-form-group label {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-3);
}

.dc-wz-form-group input, .dc-wz-form-group select {
  padding: 7px 10px;
  border: 1px solid var(--border);
  border-radius: 7px;
  font-size: 13px;
  outline: none;
}

.dc-wz-form-group input:focus, .dc-wz-form-group select:focus { border-color: var(--accent-fill); box-shadow: 0 0 0 3px rgba(99,102,241,.1); }

/* raw preview table in step1 */
      .dc-wz-preview-wrap {
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: auto;
  max-height: 240px;
  flex-shrink: 0;
}

.dc-wz-tbl {
  border-collapse: collapse;
  font-size: 11px;
  white-space: nowrap;
}

.dc-wz-tbl th {
  background: var(--surface-2);
  padding: 5px 10px;
  font-weight: 700;
  color: var(--text-3);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
}

.dc-wz-tbl td {
  padding: 4px 10px;
  border-bottom: 1px solid var(--surface-2);
  color: var(--text-1);
  max-width: 160px;
  overflow: hidden;
  text-overflow: ellipsis;
}

.dc-wz-tbl tr:hover td { background: var(--surface-2); }

.dc-wz-col-type {
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 3px 5px;
  font-size: 11px;
  color: var(--text-1);
  outline: none;
  width: 76px;
  flex-shrink: 0;
}

.dc-wz-col-trim { flex-shrink: 0; }

/* right panel: filters + replace */
      .dc-wz-ops {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
  overflow-y: auto;
}

.dc-wz-filter-row, .dc-wz-rep-row {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 12px;
}

.dc-wz-filter-row select, .dc-wz-filter-row input, .dc-wz-rep-row select, .dc-wz-rep-row input {
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 4px 7px;
  font-size: 12px;
  outline: none;
}

.dc-wz-filter-row select:focus, .dc-wz-filter-row input:focus, .dc-wz-rep-row select:focus, .dc-wz-rep-row input:focus { border-color: var(--accent-fill); }

.dc-wz-filter-col { width: 120px; flex-shrink: 0; }

.dc-wz-filter-op { width: 110px; flex-shrink: 0; }

.dc-wz-der-col { width: 110px; flex-shrink: 0; }

.dc-wz-der-regex {
  flex: 1;
  min-width: 0;
  font-family: ui-monospace,Menlo,monospace;
}

.dc-wz-filter-val { flex: 1; min-width: 60px; }

.dc-wz-rep-col { width: 110px; flex-shrink: 0; }

.dc-wz-rep-find, .dc-wz-rep-with { flex: 1; min-width: 40px; }

.dc-wz-add-btn {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  color: var(--accent);
  cursor: pointer;
  border: none;
  background: none;
  padding: 0;
  font-weight: 600;
}

.dc-wz-add-btn:hover { text-decoration: underline; }

.dc-wz-del-btn {
  border: none;
  background: none;
  color: var(--text-3);
  cursor: pointer;
  font-size: 14px;
  padding: 0 2px;
  flex-shrink: 0;
}

.dc-wz-del-btn:hover { color: var(--danger); }

.dc-wz-stat-row {
  display: flex;
  gap: 12px;
  flex-shrink: 0;
}

.dc-wz-stat {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 16px;
  flex: 1;
  text-align: center;
}

.dc-wz-stat-val, .dc-detail-summary-val {
  font-size: 22px;
  font-weight: 700;
  color: var(--text-1);
}

.dc-wz-stat.green .dc-wz-stat-val { color: var(--ok); }

/* footer */
      .dc-wz-foot {
  display: flex;
  align-items: center;
  padding: 12px 24px;
  border-top: 1px solid var(--surface-3);
  gap: 10px;
  flex-shrink: 0;
  background: var(--surface);
}

.dc-wz-foot-info {
  flex: 1;
  font-size: 12px;
  color: var(--text-3);
}

/* global options row */
      .dc-wz-toggle-row {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--text-1);
}

.dc-wz-toggle-row input[type=checkbox] {
  width: 14px;
  height: 14px;
  cursor: pointer;
}

/* ── Interactive table (step 2) ────────────────────────────────── */
      .dc-wz-itable-wrap {
  flex: 1;
  min-height: 0;
  overflow: auto;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
}

.dc-wz-itable { border-collapse: collapse; font-size: 12px; }

.dc-wz-itable td {
  padding: 3px 10px;
  border-bottom: 1px solid var(--surface-2);
  white-space: nowrap;
  color: var(--text-1);
  max-width: 180px;
  overflow: hidden;
  text-overflow: ellipsis;
}

.dc-wz-itable tr:hover td { background: var(--surface); }

.dc-wz-ith {
  background: var(--surface-3);
  padding: 0;
  vertical-align: top;
  border-right: 1px solid var(--border);
  border-bottom: 2px solid var(--border);
  min-width: 120px;
  position: relative;
  user-select: none;
}

.dc-wz-ith.excluded { background: var(--danger-soft); }

.dc-wz-ith.excluded .dc-wz-ith-name { text-decoration: line-through; color: var(--danger-border); }

.dc-wz-ith.vfiltered { border-bottom-color: var(--accent-fill); }

.dc-wz-ith-row1 {
  display: flex;
  align-items: center;
  gap: 3px;
  padding: 5px 6px 2px;
}

.dc-wz-ith-name {
  flex: 1;
  font-size: 12px;
  font-weight: 700;
  color: var(--text-1);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  cursor: text;
  outline: none;
  min-width: 0;
}

.dc-wz-ith-name[contenteditable=true] {
  background: var(--surface);
  border-radius: 3px;
  padding: 1px 3px;
  box-shadow: 0 0 0 2px var(--accent);
}

.dc-wz-col-del {
  width: 18px;
  height: 18px;
  border: none;
  background: transparent;
  cursor: pointer;
  color: var(--text-3);
  font-size: 14px;
  line-height: 1;
  padding: 0;
  flex-shrink: 0;
  border-radius: 3px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.dc-wz-col-del:hover { background: var(--danger-soft); color: var(--danger); }

.dc-wz-ith.excluded .dc-wz-col-del { color: var(--danger); }

.dc-wz-col-flt {
  width: 20px;
  height: 18px;
  border: none;
  background: transparent;
  cursor: pointer;
  color: var(--text-3);
  font-size: 11px;
  padding: 0;
  flex-shrink: 0;
  border-radius: 3px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.dc-wz-col-flt:hover { background: var(--accent-soft); color: var(--accent); }

.dc-wz-col-flt.active { color: var(--accent); background: var(--accent-soft); }

.dc-wz-ith-row2 {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 2px 6px 5px;
}

.dc-wz-col-type-sel {
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 2px 4px;
  font-size: 10px;
  color: var(--text-3);
  outline: none;
  background: var(--surface);
  cursor: pointer;
  flex-shrink: 0;
}

.dc-wz-col-trim-lbl {
  font-size: 10px;
  color: var(--text-3);
  display: flex;
  align-items: center;
  gap: 2px;
  cursor: pointer;
}

.dc-wz-itable td.excluded { color: var(--border); }

/* Value filter popup */
      .dc-wz-vflt {
  position: fixed;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 8px 28px rgba(0,0,0,.15);
  z-index: 20000;
  width: 200px;
  max-height: 300px;
  display: flex;
  flex-direction: column;
}

.dc-wz-vflt-head {
  padding: 7px 10px;
  border-bottom: 1px solid var(--surface-3);
  display: flex;
  gap: 6px;
  align-items: center;
}

.dc-wz-vflt-search {
  flex: 1;
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 3px 7px;
  font-size: 12px;
  outline: none;
}

.dc-wz-vflt-search:focus { border-color: var(--accent-fill); }

.dc-wz-vflt-all {
  font-size: 11px;
  color: var(--accent);
  cursor: pointer;
  border: none;
  background: none;
  padding: 0;
  white-space: nowrap;
}

.dc-wz-vflt-list { overflow-y: auto; flex: 1; }

.dc-wz-vflt-item {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 5px 10px;
  cursor: pointer;
  font-size: 12px;
  color: var(--text-1);
}

.dc-wz-vflt-item:hover { background: var(--surface-2); }

.dc-wz-vflt-item input { flex-shrink: 0; cursor: pointer; }

.dc-wz-vflt-foot {
  padding: 6px 10px;
  border-top: 1px solid var(--surface-3);
  display: flex;
  justify-content: flex-end;
  gap: 6px;
}

.dc-wz-ops-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 12px;
  color: var(--text-1);
  cursor: pointer;
  background: var(--surface);
  user-select: none;
  flex-shrink: 0;
}

.dc-wz-ops-toggle:hover { background: var(--surface-2); }

.dc-wz-ops-collapse { display: none; }

.dc-wz-ops-collapse.open { display: block; }

.dc-fb-pv-tbl {
  border-collapse: collapse;
  font-size: 12px;
  white-space: nowrap;
}

.dc-fb-pv-tbl th {
  padding: 5px 10px;
  background: var(--surface-3);
  border: 1px solid var(--border);
  font-weight: 600;
  color: var(--text-2);
  position: sticky;
  top: 0;
  z-index: 1;
}

.dc-fb-pv-tbl td {
  padding: 4px 10px;
  border: 1px solid var(--surface-3);
  color: var(--text-2);
}

.dc-fb-pv-tbl tr:hover td { background: var(--surface-2); }

/* info preview (unsupported types) */
      .dc-fb-pv-info {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 40px 20px;
  text-align: center;
}

.dc-fb-pv-info i { font-size: 56px; color: var(--text-3); }

.dc-fb-pv-info .pv-info-name {
  font-size: 15px;
  font-weight: 600;
  color: var(--text-1);
}

.dc-fb-pv-info .pv-info-meta { font-size: 12px; color: var(--text-3); }

.dc-fb-preview-actions {
  padding: 10px 14px;
  border-top: 1px solid var(--border);
  display: flex;
  gap: 6px;
  background: var(--surface);
  flex-shrink: 0;
}

/* Empty state */
      .dc-empty {
  text-align: center;
  padding: 80px 40px;
  color: var(--text-3);
}

.dc-empty-icon {
  width: 80px;
  height: 80px;
  border-radius: 50%;
  background: var(--surface-3);
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 auto 20px;
  font-size: 36px;
  color: var(--text-3);
}

.dc-empty h4 {
  margin: 0 0 8px;
  font-size: 16px;
  color: var(--text-3);
}

.dc-empty p { margin: 0; font-size: 13px; }

/* Modal */
      .dc-modal-overlay {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 300;
  background: rgba(15,27,45,.65);
  backdrop-filter: blur(4px);
  align-items: center;
  justify-content: center;
}

.dc-modal-overlay.open { display: flex; }

.dc-modal {
  background: var(--surface);
  border-radius: 16px;
  width: min(92vw, 980px);
  max-height: 82vh;
  display: flex;
  flex-direction: column;
  box-shadow: 0 24px 64px rgba(0,0,0,.25);
  overflow: hidden;
}

.dc-modal-hdr {
  padding: 18px 24px;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-shrink: 0;
}

.dc-modal-hdr h3 {
  margin: 0;
  font-size: 15px;
  font-weight: 700;
  color: var(--text-1);
}

.dc-modal-close {
  width: 30px;
  height: 30px;
  border-radius: 7px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-3);
  border: none;
  cursor: pointer;
  font-size: 16px;
  color: var(--text-3);
}

.dc-modal-close:hover { background: var(--border); }

/* ⚠️ `min-height: 0` is stated, not inherited. A flex item's automatic minimum
   size is 0 only while its `overflow` is not `visible`; that is the reason this rule
   happens to work today, and the reason it would quietly stop working the first time
   somebody changes `overflow` or copies the declaration into a new class. Saying it
   costs nothing and makes the contract the thing the next reader can check. */
.dc-modal-body { flex: 1; min-height: 0; overflow: auto; }

/* ⚠️ The scroll container a tall dialog needs, and the reason it exists.
   `.dc-modal` is `max-height: 82vh` with `overflow: hidden`, so a body that is
   neither `flex: 1` nor scrollable is not merely "long" — it is CLIPPED, and the
   footer below it goes with it. The "Import from a database" dialog was unfinishable
   for exactly that reason: the mode radios sat at the cut line and Cancel/Import
   were never on screen.
   `min-height: 0` is the part that is easy to leave out: a flex child defaults to
   `min-height: auto`, so it refuses to shrink below its content and the overflow
   rule above never engages. */
.dc-modal-pad {
  flex: 1;
  min-height: 0;
  overflow: auto;
  padding: 20px;
}

/* A hairline between the groups of a long form. The dialog grew to four blocks
   (connect / choose a table / name it / keep it fresh) and spacing alone stopped
   saying where one ended. */
.dc-modal-group {
  border-top: 1px solid var(--surface-3);
  margin-top: 18px;
  padding-top: 2px;
}
.dc-modal-group:first-child { border-top: none; margin-top: 0; padding-top: 0; }

/* Saved external-database connections. Same row shape as the table picker
   (`.dc-at-row`) so the two lists read as one form, minus its `cursor: pointer`:
   picking a table is a choice, managing a connection is not. */
.dc-conn-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  margin-bottom: 5px;
  font-size: 12px;
  color: var(--text-1);
  background: var(--surface);
}
.dc-conn-row:hover { background: var(--surface-2); }
.dc-conn-row.is-current { border-color: var(--accent); }
/* ⚠️ `display: flex; flex-direction: column` and not just `flex: 1`. The name and
   the host line are `<span>`s, so inline they run together on ONE line —
   "probe sourcelocalhost:5432/klado" — and the row is 30px tall, so the text wraps
   at an arbitrary point instead of stacking. Only real data ever showed this: the
   empty state is the first thing a person sees. */
.dc-conn-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.dc-conn-name { font-weight: 600; }
.dc-conn-sub {
  font-size: 11px;
  color: var(--text-3);
  overflow-wrap: anywhere;   /* a long host must not push the buttons off the row */
  margin-top: 1px;
}
.dc-conn-actions { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.dc-conn-tag {
  font-size: 10px;
  color: var(--text-3);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 1px 8px;
  white-space: nowrap;
}

/* The empty states. A bare empty box reads as "broken"; a dashed one reads as
   "nothing here yet", which is what it is. */
.dc-conn-empty {
  border: 1px dashed var(--border);
  border-radius: 8px;
  padding: 14px 12px;
  font-size: 12px;
  color: var(--text-3);
  text-align: center;
}

/* Toast */
      .dc-toast {
  position: fixed;
  bottom: 24px;
  right: 24px;
  z-index: 999;
  background: var(--surface);
  border-radius: 10px;
  padding: 12px 18px;
  box-shadow: 0 4px 24px rgba(0,0,0,.15);
  font-size: 13px;
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: 340px;
  border-left: 4px solid var(--accent-fill);
  animation: dcToastIn .25s ease;
  pointer-events: none;
}

.dc-toast.success { border-left-color: var(--ok); }

.dc-toast.error { border-left-color: var(--danger); }

@keyframes dcToastIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}

@keyframes dcSpin {
to { transform: rotate(360deg); }
}

.rpt-frow, .cal-todo-line {
  display: flex;
  gap: 6px;
  align-items: center;
}

/* Settings panel */

@keyframes monSpin { to{transform:rotate(360deg)}; }

/* ── Detail modal styles ── */
  .dc-detail-loading {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 40px;
  justify-content: center;
  color: var(--text-3);
}

.dc-detail-empty {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 40px;
  justify-content: center;
  color: var(--text-3);
  flex-direction: column;
}

.dc-detail-summary {
  display: grid;
  grid-template-columns: repeat(3,1fr);
  border-bottom: 1px solid var(--surface-3);
  padding: 20px 24px;
}

.dc-detail-summary-item { text-align: center; }

.dc-detail-summary-lbl {
  font-size: 11px;
  color: var(--text-3);
  margin-top: 2px;
  text-transform: uppercase;
  letter-spacing: .04em;
}

.dc-detail-col-row { padding: 10px 24px 4px; }

.dc-detail-col-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11px;
  color: var(--accent);
  background: var(--accent-soft);
  padding: 2px 8px;
  border-radius: 4px;
}

.dc-detail-rows {
  padding: 12px 24px 20px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.dc-detail-row {
  display: grid;
  grid-template-columns: 72px 1fr 80px 28px;
  gap: 8px;
  align-items: center;
}

.dc-detail-del-btn {
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
  line-height: 1;
  color: var(--text-3);
  border-radius: 4px;
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: color .15s,background .15s;
}

.dc-detail-del-btn:hover { color: var(--danger); background: var(--danger-soft); }

.dc-detail-row-label {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-2);
}

.dc-detail-row-bar-wrap {
  height: 8px;
  background: var(--surface-3);
  border-radius: 99px;
  overflow: hidden;
}

.dc-detail-row-bar {
  height: 100%;
  background: linear-gradient(90deg,var(--accent-fill),var(--accent-2));
  border-radius: 99px;
  transition: width .4s ease;
}

.dc-detail-row-cnt {
  font-size: 12px;
  color: var(--text-3);
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* ── Period stat: consistent vertical alignment ── */
  .dc-stat-val {
  min-height: 28px;
  display: flex;
  align-items: center;
}

.dc-stat-val--period {
  font-size: 12px !important;
  font-weight: 600;
  color: var(--text-1);
}

@keyframes sku-flash {
0% { transform: scale(.98); opacity: .7; }
100% { transform: scale(1); opacity: 1; }
}

.st-active { background: rgba(0,163,224,.15); color: var(--accent); }

.st-phasein { background: rgba(52,211,153,.15); color: var(--ok); }

.st-phaseout { background: rgba(251,146,60,.15); color: var(--warn); }

.st-phased { background: rgba(148,163,184,.12); color: var(--text-3); }

.plat-edit-modal-ft .btn-primary { background: var(--accent-fill); border-color: var(--accent-fill); }

.plat-edit-modal-ft .btn-primary:hover { background: var(--accent-fill); border-color: var(--accent-fill); }

td.ov-mix-band {
  background: var(--bg-page) !important;
  color: var(--accent);
  font-weight: 600;
  font-size: 0.70rem;
  vertical-align: middle;
  text-align: left;
  border-right: 2px solid var(--accent);
  padding: 4px 6px;
  white-space: normal;
  line-height: 1.3;
}

td.ov-mix-ms { color: var(--accent); font-weight: 600; }

td.ov-mix-tot-lbl {
  color: var(--text-2) !important;
  font-style: italic;
  font-weight: 400 !important;
  font-size: 0.70rem !important;
}

td.ov-mix-ghost-lbl { font-style: italic !important; font-weight: 400 !important; }

/* modal */
.sku-modal, /* modal */
.sku-modal {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  width: 780px;
  max-width: 97vw;
  max-height: 90vh;
  display: flex;
  flex-direction: column;
  box-shadow: 0 20px 60px rgba(5,28,44,.18),0 2px 8px rgba(5,28,44,.10);
}

.sku-modal-x, .sku-modal-x {
  background: transparent;
  border: none;
  color: var(--text-3);
  cursor: pointer;
  font-size: 1.2rem;
  padding: 4px;
  border-radius: 6px;
  display: flex;
  align-items: center;
  transition: background .15s,color .15s;
  line-height: 1;
}

.sku-modal-x:hover { background: rgba(0,0,0,.045); color: var(--text-1); }

#sku-modal-bg .sku-modal { width: 960px; }

.sku-modal-x:hover { background: rgba(0,0,0,.045); color: var(--text-1); }

#sku-modal-bg .sku-modal { width: 960px; }

#sku-add-modal-bg .sku-modal-x { color: var(--text-3); }

#sku-add-modal-bg .sku-modal-x:hover { background: var(--hover); color: var(--text-1); }

#sku-add-modal-bg .sku-modal-ft .btn-primary { background: var(--accent-fill); border-color: var(--accent-fill); }

#sku-add-modal-bg .sku-modal-ft .btn-primary:hover { background: var(--accent-fill); border-color: var(--accent-fill); }

/* settings modal */
.ap-settings-bg {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,.48);
  z-index: 9200;
  display: flex;
  align-items: center;
  justify-content: center;
}

.ap-settings-modal {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  width: 580px;
  max-width: 95vw;
  max-height: 82vh;
  display: flex;
  flex-direction: column;
  box-shadow: 0 8px 32px rgba(13,27,42,.20);
}

.ap-settings-hd {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 20px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}

.ap-settings-title {
  font-size: .9rem;
  font-weight: 700;
  color: var(--text-1);
}

.gpc-kpi .k {
  font-size: 10px;
  font-weight: 800;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--text-3);
}

.gpc-kpi .v {
  margin-top: 3px;
  font-size: 21px;
  font-weight: 800;
  color: var(--ink);
  letter-spacing: -.4px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.gpc-row .k { color: var(--text-3); white-space: nowrap; }

.gpc-row .v {
  color: var(--ink-soft);
  font-weight: 650;
  text-align: right;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.gpc-row .v.wrap {
  white-space: normal;
  line-height: 1.35;
  max-height: 2.7em;
  overflow: hidden;
}

.gpc-row .v.na { color: var(--text-3); font-weight: 600; }

.gpc-trend-h .t {
  font-size: 12.5px;
  font-weight: 800;
  color: var(--ink-soft);
}

.gpc-trend-h .s {
  font-size: 10.5px;
  font-weight: 600;
  color: var(--text-3);
}

/* ── System Settings Page ── */
/* ⚠️ 2026-10-06: `.rpt-title`, `.ss-toolbar` and `.ss-toolbar-inner` were all deleted
   with the settings toolbar they styled. The page has no row of its own any more — its
   Refresh is declared in `PAGE_ACTIONS` and drawn in the breadcrumb bar — so all three
   selectors had no element left to match. `.rpt-spacer` STAYS: nine other toolbars
   (Workspace, Inbox, Data Center, …) still use it, and it is not a settings thing. */

/* The icon-only button a page puts in the breadcrumb bar. `.btn` on its own gives it
   12px of padding either side of a 16px glyph, so it reads as a 40px slab with a small
   mark floating in it; a square is what an icon-only control has to be. */
.workspace-actions .btn { width: 30px; padding: 0; }

.ss-lbl {
  display: block;
  font-size: 0.76rem;
  font-weight: 600;
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: .05em;
  margin-bottom: 4px;
}

.ss-input {
  width: 100%;
  height: 30px;
  border-radius: 6px;
  border: 1px solid var(--border);
  padding: 0 8px;
  font-size: 0.82rem;
  color: var(--text-1);
  background: var(--surface);
  outline: none;
  font-family: var(--font);
  box-sizing: border-box;
}

.ss-input:focus { border-color: var(--blue); }

.btn-sm {
  height: 28px !important;
  padding: 0 10px !important;
  font-size: 0.76rem !important;
}

/* ── Smart Import (Quick Import redesign) ── */
#dc-qi-box {
  background: var(--surface);
  border-radius: 16px;
  width: 940px;
  max-width: 97vw;
  height: 580px;
  max-height: 92vh;
  display: flex;
  flex-direction: column;
  box-shadow: 0 24px 64px rgba(0,0,0,.22);
  overflow: hidden;
}

.dc-qi-hd {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 13px 18px;
  border-bottom: 1px solid var(--surface-3);
  flex-shrink: 0;
}

.dc-qi-cols {
  display: flex;
  flex: 1;
  min-height: 0;
  position: relative;
  overflow: hidden;
}

.dc-qi-col {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.dc-qi-col-sheets {
  width: 230px;
  border-right: 1px solid var(--surface-3);
  flex-shrink: 0;
}

.dc-qi-col-datasets {
  width: 260px;
  border-left: 1px solid var(--surface-3);
  flex-shrink: 0;
}

.dc-qi-col-mid {
  flex: 1;
  position: relative;
  background: var(--surface-2);
  display: flex;
  align-items: center;
  justify-content: center;
}

.dc-qi-col-hd {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 7px 10px;
  font-size: 11px;
  font-weight: 700;
  color: var(--text-2);
  border-bottom: 1px solid var(--surface-3);
  flex-shrink: 0;
  text-transform: uppercase;
  letter-spacing: .04em;
}

.dc-qi-count-badge {
  font-size: 10px;
  background: var(--surface-3);
  color: var(--text-3);
  padding: 1px 6px;
  border-radius: 10px;
  font-weight: 600;
}

.dc-qi-item-list {
  flex: 1;
  overflow-y: auto;
  padding: 5px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.dc-qi-sheet-row {
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 6px 8px;
  border-radius: 7px;
  cursor: pointer;
  border: 1px solid transparent;
  transition: all .12s;
  position: relative;
}

.dc-qi-sheet-row:hover { background: var(--surface-2); border-color: var(--border); }

.dc-qi-sheet-row.selected { background: var(--accent-soft); border-color: var(--accent-border); }

.dc-qi-sheet-row.connected { background: var(--ok-soft); border-color: var(--ok-border); }

.dc-qi-sheet-row.importing { background: var(--warn-soft); border-color: var(--warn-border); }

.dc-qi-sheet-row.done {
  background: var(--ok-soft);
  border-color: var(--ok-border);
  opacity: .75;
}

.dc-qi-sheet-row.error { background: var(--danger-soft); border-color: var(--danger-border); }

.dc-qi-sheet-name {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-1);
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dc-qi-dot-r, .dc-qi-dot-l {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: 2px solid var(--border-strong);
  background: var(--surface);
  flex-shrink: 0;
  transition: all .12s;
}

.dc-qi-sheet-row.connected .dc-qi-dot-r { border-color: var(--ok); background: var(--ok); }

.dc-qi-sheet-row.selected  .dc-qi-dot-r, .dc-qi-ds-row.target    .dc-qi-dot-l {
  border-color: var(--accent-fill);
  background: var(--accent-fill);
  box-shadow: 0 0 0 3px rgba(59,130,246,.2);
}

.dc-qi-sheet-row.error     .dc-qi-dot-r { border-color: var(--danger); background: var(--danger); }

.dc-qi-ds-row {
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 6px 8px;
  border-radius: 7px;
  cursor: pointer;
  border: 1px solid transparent;
  transition: all .12s;
}

.dc-qi-ds-row:hover { background: var(--surface-2); border-color: var(--border); }

.dc-qi-ds-row.connected { background: var(--ok-soft); border-color: var(--ok-border); }

.dc-qi-ds-row.target { background: var(--accent-soft); border-color: var(--accent-border); }

.dc-qi-ds-row.connected .dc-qi-dot-l { border-color: var(--ok); background: var(--ok); }

.dc-qi-ds-name {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-1);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dc-qi-ds-sub {
  font-size: 10px;
  color: var(--text-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  margin-top: 1px;
}

.dc-qi-ds-info {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.dc-qi-disc {
  flex-shrink: 0;
  border: none;
  background: none;
  color: var(--danger);
  cursor: pointer;
  font-size: 13px;
  padding: 0 2px;
  line-height: 1;
  opacity: .7;
}

.dc-qi-disc:hover { opacity: 1; }

.dc-qi-mode-btn {
  flex-shrink: 0;
  border: none;
  border-radius: 4px;
  font-size: 10px;
  font-weight: 700;
  padding: 2px 6px;
  cursor: pointer;
  line-height: 1.4;
  letter-spacing: .02em;
}

.dc-qi-mode-btn.append { background: var(--accent-soft); color: var(--accent-hover); }

.dc-qi-mode-btn.replace { background: var(--danger-soft); color: var(--danger); }

.dc-qi-mode-btn:hover { filter: brightness(.93); }

.dc-qi-ds-search {
  display: block;
  width: 100%;
  box-sizing: border-box;
  padding: 4px 8px;
  border: 1px solid var(--border);
  border-radius: 5px;
  font-size: 11px;
  outline: none;
  color: var(--text-1);
  margin-top: 3px;
  background: var(--surface);
}

.dc-qi-ds-search:focus { border-color: var(--accent-fill); }

.dc-qi-ft {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 16px;
  border-top: 1px solid var(--surface-3);
  flex-shrink: 0;
}

.dc-qi-mid-hint {
  font-size: 11px;
  color: var(--text-3);
  text-align: center;
  padding: 10px 16px;
  pointer-events: none;
  line-height: 1.7;
}

.pf-row2 .panel {
  display: flex;
  flex-direction: column;
  height: 370px;
}

input[type=number]::-webkit-outer-spin-button, input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

input[type=number] { -moz-appearance: textfield; appearance: textfield; }

/* ── Workspace: private HTML documents and public snapshots ─────────────────
   Cards in the Workspace; each card opens the document itself in a
   full-viewport viewer. All classes are namespaced `rpt-` (the vendored Tabler
   CSS in this app owns generic utility names, so no bare class names here). */
#page-reports { overflow: hidden; }

.workspace.workspace-page-compact > .workspace-bar { display: none; }

.rpt-toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  padding: 9px 16px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}

/* The area tabs are deliberately NOT a segmented control: they sit directly on the
   toolbar as plain buttons, and the active one is marked by colour alone
   (`.rpt-tab.on`). The grey pill container this used to be made the Workspace page
   look different from the Knowledge base page, which has always been flat — the two
   are the same kind of switcher and must not read as two different controls.
   The wrapper is kept for `role="tablist"` only, so it carries no visual style. */
.rpt-tabs {
  display: inline-flex;
  align-items: center;
  gap: 3px;
}

.rpt-tab {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text-3);
  padding: 6px 10px;
  font: inherit;
  font-size: .74rem;
  font-weight: 700;
  cursor: pointer;
  white-space: nowrap;
}

.rpt-tab.on {
  color: var(--accent-hover);
  background: var(--surface);
  box-shadow: 0 1px 3px rgba(15,23,42,.12);
}

.rpt-tab:hover { color: var(--accent); }

.rpt-toolbar-btn {
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  white-space: nowrap;
}

.rpt-toolbar-btn.rpt-refresh { width: 30px; padding: 0; }

.rpt-viewer-mode {
  font-size: .67rem;
  color: var(--text-3);
  white-space: nowrap;
}

.rpt-viewer-pull {
  border: 1px solid var(--accent-border);
  background: var(--accent-soft);
  color: var(--accent-hover);
  border-radius: 6px;
  padding: 5px 9px;
  font: inherit;
  font-size: .7rem;
  font-weight: 700;
  cursor: pointer;
}

/* Same affordance as Pull to edit (a one-shot action that creates a new card), so it
   deliberately reuses that exact spec — no size modifier of its own. */
.rpt-viewer-tostatic {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border: 1px solid var(--accent-border);
  background: var(--accent-soft);
  color: var(--accent-hover);
  border-radius: 6px;
  padding: 5px 9px;
  font: inherit;
  font-size: .7rem;
  font-weight: 700;
  cursor: pointer;
}

.rpt-viewer-tostatic:hover { background: var(--accent-soft); }

.rpt-viewer-tostatic[disabled] { opacity: .55; cursor: default; }

.rpt-spacer { flex: 1; }

.rpt-input, .rpt-select {
  height: 28px;
  padding: 2px 8px;
  font-size: .75rem;
  color: var(--text-1);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  outline: none;
}

.rpt-input { width: 210px; }

.rpt-input:focus, .rpt-select:focus { border-color: var(--accent-border); }

.rpt-count {
  font-size: .71rem;
  color: var(--text-3);
  white-space: nowrap;
}

.rpt-body {
  flex: 1;
  overflow-y: auto;
  padding: 14px 16px;
  background: var(--bg);
}

.rpt-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill,minmax(225px,1fr));
  gap: 12px;
  align-content: start;
}

.rpt-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  cursor: pointer;
  overflow: hidden;
  transition: box-shadow .16s, transform .16s, border-color .16s;
}

.rpt-card:hover {
  transform: translateY(-2px);
  border-color: var(--accent-border);
  box-shadow: 0 9px 22px rgba(15,23,42,.11);
}

/* the cover box is 16:9 — the same shape the generator is told to produce, so the
   image fills it without letterboxing */
.rpt-cover {
  position: relative;
  aspect-ratio: 16/9;
  overflow: hidden;
  background: var(--cover-gradient);
}

.rpt-cover img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform .5s cubic-bezier(.2,.7,.3,1);
}

.rpt-card:hover .rpt-cover img { transform: scale(1.035); }

.rpt-cover-ph {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  color: var(--text-3);
}

.rpt-cover-ph i { font-size: 1.5rem; opacity: .75; }

.rpt-cover-ph span {
  font-size: .6rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  font-weight: 700;
}

.rpt-cover-badges {
  position: absolute;
  z-index: 2;
  top: 8px;
  left: 8px;
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  max-width: calc(100% - 48px);
}

.rpt-chip {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  min-width: 0;
  font-size: .58rem;
  font-weight: 700;
  line-height: 1.25;
  padding: 3px 7px;
  border-radius: 999px;
  color: var(--text-on-ink);
  background: rgba(13,27,42,.72);
  backdrop-filter: blur(6px);
  white-space: nowrap;
}

.rpt-chip.rpt-chip-interactive { background: rgba(29,78,216,.9); }

/* A dynamic report's slices are chosen in the viewer's rail — say so on the card, or a
   reader who never opens it cannot tell it apart from a static snapshot. */
.rpt-chip.rpt-chip-dynamic { background: rgba(124,58,237,.92); }

.rpt-chip.rpt-chip-filters { background: var(--accent-border); }

.rpt-chip.rpt-chip-filters i { font-size: .62rem; }

/* A document card (pptx / xlsx / pdf / docx) — the deliverable IS the file. */
.rpt-chip.rpt-chip-doc { background: rgba(15,118,110,.92); }

.rpt-chip.rpt-chip-doc i { font-size: .66rem; }

.rpt-chip.rpt-chip-shared { background: rgba(5,122,85,.9); }

.rpt-chip.rpt-chip-draft { background: rgba(180,83,9,.9); }

.rpt-chip.rpt-chip-archived { background: rgba(71,98,127,.9); }

.rpt-card-acts {
  position: absolute;
  z-index: 3;
  top: 7px;
  right: 7px;
  display: flex;
  gap: 2px;
  opacity: 0;
  transition: opacity .16s ease;
}

.rpt-card:hover .rpt-card-acts, .rpt-card:focus-within .rpt-card-acts { opacity: 1; }

.rpt-card-acts .rpt-iconbtn {
  color: var(--text-on-ink);
  background: rgba(13,27,42,.55);
  backdrop-filter: blur(6px);
}

.rpt-card-acts .rpt-iconbtn:hover { background: rgba(255,255,255,.24); color: var(--text-on-ink); }

@media(hover:none) {
.rpt-card-acts { opacity: 1; }
}

.rpt-export-status {
  margin: 8px 16px 0;
  padding: 9px 12px;
  border-radius: 7px;
  background: var(--danger-soft);
  border: 1px solid var(--danger-border);
  color: var(--danger);
  font-size: .74rem;
}

.rpt-export-status.busy {
  background: var(--accent-soft);
  border-color: var(--accent-border);
  color: var(--accent-hover);
}

.rpt-card-body {
  padding: 9px 10px 10px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.rpt-card-title {
  margin: 0;
  height: 2.72em;
  font-size: .77rem;
  font-weight: 700;
  line-height: 1.36;
  color: var(--text-1);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* The summary is the one part of a card that used to grow. There was no rule here at
   all, so a four-line description made its card ~60px taller and stepped the whole
   grid row out of line with the one above it — a wall whose rows are ragged reads as
   broken even when every card is fine. `.rpt-card-title` already clamps at two lines;
   this is the same clamp on the paragraph, with the same arithmetic (line-height × 2
   = the height) so "two lines" is one number both elements agree on.

   ⚠️ `margin: 0` is load-bearing, not tidying. A bare `<p>` carries the UA's
   `margin-block-end: 1em` — 16px of invisible padding under every summary, on every
   card, which is most of why the bodies were taller than their content. */
.rpt-card-sum {
  margin: 0;
  height: 2.857em;
  font-size: .875rem;
  line-height: 1.4286;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* A wall is only a wall if the cards agree. The cover is already fixed (16:9 at an
   equal column width) and the summary is clamped above, so this pins the last
   variable — and makes the invariant explicit rather than emergent. It is what a
   summary-less card needs: without a summary it is 46px short, which a grid row hides
   but a single-column wall would show. Everything inside scales with the root font
   size (rem, em), so the same 7.5rem holds at any zoom.

   `margin-top: auto` parks the byline on the bottom edge, so whatever slack is left
   reads as breathing room above it rather than a gap under the summary. */
#dsh-grid .rpt-card-body { height: 7.5rem; }

#dsh-grid .rpt-card-sub { margin-top: auto; }

.rpt-card-sub {
  min-width: 0;
  font-size: .61rem;
  color: var(--text-3);
  display: grid;
  grid-template-columns: minmax(0,1fr) auto;
  align-items: center;
  gap: 6px;
}

/* document cards: the FILE name is the useful second line (the title is often a summary) */
.rpt-card-file {
  display: flex;
  align-items: center;
  gap: 5px;
  min-width: 0;
  margin-top: 3px;
  font-size: .63rem;
  color: var(--text-3);
}

.rpt-card-file i { flex-shrink: 0; opacity: .85; }

.rpt-card-file span, .rpt-share-person span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rpt-card-file em {
  font-style: normal;
  flex-shrink: 0;
  color: var(--text-3);
  opacity: .8;
}

.rpt-card-sub .rpt-who {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-2);
}

.rpt-card-sub time { white-space: nowrap; font-variant-numeric: tabular-nums; }

.rpt-card-sub .rpt-who i {
  margin-right: 3px;
  font-size: .68rem;
  opacity: .8;
}

.rpt-menu {
  position: fixed;
  z-index: 8100;
  min-width: 190px;
  padding: 5px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 9px;
  box-shadow: 0 12px 34px rgba(15,23,42,.2);
}

.rpt-menu[hidden] { display: none; }

.rpt-menu button {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  border: 0;
  border-radius: 5px;
  padding: 8px 10px;
  background: transparent;
  color: var(--text-1);
  text-align: left;
  font: inherit;
  font-size: .74rem;
  cursor: pointer;
}

.rpt-menu button:hover, .rpt-menu button:focus-visible { background: var(--accent-soft); outline: none; }

.rpt-menu button.danger { color: var(--danger); }

.rpt-menu button.danger:hover, .rpt-menu button.danger:focus-visible { background: var(--danger-soft); }

.rpt-menu hr, .kb-ctx hr, .cal-ctx hr, .ibx-ctx hr {
  border: 0;
  border-top: 1px solid var(--border);
  margin: 4px 6px;
}

.rpt-menu button i, .kb-ctx button i, .cal-ctx button i, .ibx-ctx button i {
  width: 15px;
  text-align: center;
  font-size: .86rem;
}

.rpt-share-box { width: 560px; }

.rpt-share-bd {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 16px;
  overflow-y: auto;
}

.rpt-share-note {
  margin: 0;
  color: var(--text-3);
  font-size: .75rem;
  line-height: 1.5;
}

.rpt-share-list {
  display: flex;
  flex-direction: column;
  gap: 5px;
  max-height: 180px;
  overflow-y: auto;
}

.rpt-share-person {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 7px 9px;
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: .75rem;
}

.rpt-share-person button {
  border: 0;
  background: none;
  color: var(--danger);
  cursor: pointer;
}

.rpt-share-input {
  width: 100%;
  min-width: 0;
  padding: 7px 9px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--text-1);
  font: inherit;
  font-size: .76rem;
}

.rpt-share-input:focus { outline: 1px solid var(--accent); }

.rpt-share-url { font-family: ui-monospace,SFMono-Regular,Menlo,monospace; font-size: .7rem; }

/* ── colleague picker ───────────────────────────────────────────────────────────
   Shared by the Workspace report dialog and the knowledge-base page dialog (see
   kldColleaguePicker). The dropdown is a fixed-position layer hanging off <body> and
   placed from the input's own rect, NOT a child of the dialog: `.rpt-share-bd` scrolls and
   `.rpt-modal-box` is `overflow:hidden`, so an absolutely positioned list inside the modal
   gets silently clipped — measured 75px cut off (the last row and the keyboard hint), with
   no scrollbar to reach it, because an absolute child never enters scrollHeight. */
.colleague-pop {
  /* ⚠️ Above every surface that can host an input it is attached to — not just
     `.rpt-modal` (8100): the calendar's event overlay is `.cal-modal` (9000) and its row
     menu `.cal-ctx` (9600). At the old 8200 the dropdown opened *under* the calendar
     panel, so the rows were visible but every click landed on the panel behind them
     (caught by `verify_event_todo_ui.py`, which is why the Partners box is a picker now). */
  position: fixed;
  z-index: 9650;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 12px 28px rgba(15,23,42,.16);
  padding: 4px;
  overflow-y: auto;
}

.colleague-pop[hidden] { display: none; }

.colleague-pop .cp-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 9px;
  border-radius: 6px;
  cursor: pointer;
  font-size: .76rem;
  color: var(--text-1);
}

.colleague-pop .cp-row:hover, .colleague-pop .cp-row.on { background: var(--accent-soft); }

.colleague-pop .cp-mail {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.colleague-pop .cp-name {
  color: var(--text-3);
  font-size: .72rem;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.colleague-pop .cp-tag {
  margin-left: auto;
  flex-shrink: 0;
  font-size: .62rem;
  font-weight: 700;
  letter-spacing: .04em;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent-hover);
}

.colleague-pop .cp-tag.cp-granted { background: rgba(5,150,105,.12); color: var(--ok); }

.colleague-pop .cp-msg {
  padding: 7px 9px;
  color: var(--text-3);
  font-size: .73rem;
}

.colleague-pop .cp-foot {
  margin-top: 3px;
  padding: 6px 9px 4px;
  border-top: 1px solid var(--border);
  color: var(--text-3);
  font-size: .68rem;
}

.colleague-pop .cp-foot kbd {
  font: inherit;
  font-weight: 700;
  color: var(--text-2);
  border: 1px solid var(--border);
  border-bottom-width: 2px;
  border-radius: 4px;
  padding: 0 4px;
}

.rpt-iconbtn {
  border: none;
  background: none;
  padding: 3px 5px;
  border-radius: 5px;
  color: var(--text-3);
  cursor: pointer;
  font-size: .85rem;
  line-height: 1;
}

.rpt-iconbtn:hover { background: var(--accent-soft); color: var(--accent); }

.rpt-iconbtn.rpt-danger:hover { background: var(--danger-soft); color: var(--danger); }

.rpt-empty {
  grid-column: 1 / -1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 56px 20px;
  text-align: center;
  color: var(--text-3);
}

.rpt-empty i {
  font-size: 2rem;
  color: var(--text-3);
  opacity: .5;
}

.rpt-empty .workspace-mark { width: 44px; height: 44px; }

.rpt-empty-t {
  font-size: .88rem;
  font-weight: 700;
  color: var(--text-2);
}

.rpt-empty-s {
  font-size: .74rem;
  line-height: 1.6;
  max-width: 520px;
}

.rpt-empty code {
  font-size: .7rem;
  background: var(--hover);
  border-radius: 4px;
  padding: 1px 5px;
}

.rpt-viewer-page {
  font-size: .7rem;
  font-weight: 700;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Replaces the deck's own floating "全部页面" button, which stays hidden while
   embedded (see report-deck.js). Same pill language as .rpt-lang, sized to the
   bar's other controls. */
.rpt-modebtn {
  height: 22px;
  padding: 0 9px;
  font-size: .66rem;
  font-weight: 700;
  color: var(--text-3);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  cursor: pointer;
  white-space: nowrap;
}

.rpt-modebtn:hover { color: var(--accent); border-color: var(--accent-border); }

/* full-viewport report viewer — carries its own close affordance so the global
   ESC handler dismisses it through reportsPage.closeViewer() */
.rpt-viewer {
  position: fixed;
  /* ⚠️ Starts BELOW the top bar, and that is the whole point of the rule.
     `.nav` is `position: fixed; z-index: 200`; this used to be `inset: 0` at
     z-index 8000 with an opaque `background: var(--bg)`, so opening ANY report —
     from Workspace, Inbox, the knowledge base or a rail folder — painted a
     full-screen box over the navigation and the nav was simply gone. The user
     asked for the nav to exist on every page, which makes "the viewer is
     full-bleed" the bug rather than the feature.
     Same fix as `#datacenter-page` (see its comment): the content area starts at
     `--nav-h`, so a shell page and a viewer occupy identical screen real estate.
     `.rpt-viewer-main` is `flex: 1; min-height: 0`, so the shorter box flows
     straight into the iframe — and the document inside measures ITS OWN viewport,
     so a deck that scales to fit re-fits itself rather than getting clipped. */
  inset: var(--nav-h) 0 0 0;
  z-index: 8000;
  display: none;
  flex-direction: column;
  background: var(--bg);
}

.rpt-viewer.open { display: flex; }

.rpt-viewer-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 46px;
  padding: 0 12px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}

.rpt-back {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 28px;
  padding: 0 10px;
  font-size: .75rem;
  font-weight: 600;
  color: var(--text-1);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
}

.rpt-back:hover { border-color: var(--accent-border); color: var(--accent); }

.rpt-viewer-title {
  font-size: .82rem;
  font-weight: 700;
  color: var(--text-1);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.rpt-viewer-meta, .rpt-frow > span {
  font-size: .68rem;
  color: var(--text-3);
  white-space: nowrap;
}

.rpt-viewer-bar .rpt-iconbtn { font-size: 1rem; }

/* ══ EDIT MODE ══════════════════════════════════════════════════════════════
   Two controls and a panel: the Save button, the "you have not kept this yet"
   chip (one piece of state, shown twice), and the page outline beside the
   document.

   ⚠️ The Office-shaped ribbon is GONE — see the long comment on `#rpt-viewer-main`
   in index.html for why a save that writes only text cannot honour a bold button.
   Its CSS went with it rather than being left behind: dead rules for elements
   that no longer exist are how a stylesheet grows a second answer to "what does
   the edit bar look like", and `test_stale_css_rules.py` treats a rule with no
   markup behind it as exactly the thing worth catching. */

/* ── Save, and the "you have not kept this yet" chip ────────────────────────
   These two are one piece of state shown twice: the chip says WHICH state, the
   button changes it. Both live at the right end of the one bar, so the reader
   never has to look somewhere else to find out whether their work is safe. */
.rpt-viewer-save {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border: 1px solid var(--accent-border);
  background: var(--accent-fill);
  color: var(--text-on-accent);
  border-radius: 6px;
  padding: 5px 10px;
  font: inherit;
  font-size: .7rem;
  font-weight: 700;
  cursor: pointer;
  white-space: nowrap;
}
.rpt-viewer-save[hidden] { display: none; }
.rpt-viewer-save .ti { font-size: .9rem; }
.rpt-viewer-save:hover:not(:disabled) { filter: brightness(1.08); }
/* ⚠️ Disabled, not hidden. A Save the reader can see and cannot press is the
   control that answers "can I keep this?" — and it is visible from the moment
   the document opens, so the answer is there before they start typing rather
   than after they wonder. It is also why the label is never greyed out of
   recognition: only the fill drops away. */
.rpt-viewer-save:disabled {
  background: var(--surface-3);
  border-color: var(--border);
  color: var(--text-3);
  cursor: default;
}
.rpt-viewer-save.is-dirty:not(:disabled) { box-shadow: 0 0 0 2px var(--warn-soft); }
.rpt-viewer-save.is-saving { cursor: progress; }
.rpt-viewer-save.is-saving .ti { opacity: .5; }

.rpt-dirty {
  font-size: .64rem;
  font-weight: 600;
  color: var(--warn);
  background: var(--warn-soft);
  border: 1px solid var(--warn-border);
  border-radius: 999px;
  padding: 2px 9px;
  white-space: nowrap;
}
.rpt-dirty[hidden] { display: none; }

/* ── the toolbar ──────────────────────────────────────────────────────────
   Five controls, one row, above the document and below the outline. A `.on` state
   is a READOUT of the reader's current selection, not a mode: the same button is
   lit when the cursor is inside bold text and dark when it is not, which is what
   lets a reader see the state of a selection they cannot see highlighted. */
.rpt-tools {
  display: flex;
  align-items: center;
  gap: 2px;
  flex: 0 0 auto;
  padding: 5px 8px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  /* ⚠️ `hidden` needs its own rule: this one sets `display: flex`, which beats
   * the browser's own `[hidden] { display: none }`. */
}
.rpt-tools[hidden] { display: none; }
.rpt-tools-g { display: flex; align-items: center; gap: 1px; }
.rpt-tools-g + .rpt-tools-g {
  margin-left: 8px;
  padding-left: 8px;
  border-left: 1px solid var(--border);
}
.rpt-tool {
  width: 30px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid transparent;
  border-radius: 5px;
  background: none;
  color: var(--text-2);
  font-size: 1.05rem;
  cursor: pointer;
}
.rpt-tool:hover { background: var(--hover); border-color: var(--border); color: var(--text-1); }
.rpt-tool:active { background: var(--surface-3); }
.rpt-tool:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* The reader must be able to tell which one is on without hovering, and must be
   able to see it in dark mode too — so this is the accent FILL with the
   on-accent ink, not a tint of the text colour. */
.rpt-tool.on {
  background: var(--accent-fill);
  border-color: var(--accent-border);
  color: var(--text-on-accent);
}
.rpt-tools-hint {
  margin-left: auto;
  padding-right: 4px;
  font-size: .68rem;
  color: var(--text-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The two controls that are LETTERS rather than glyphs. ⚠️ `line-height: 1` and
   a fixed height on the span, because the icons are a webfont on a 28px box and a
   text glyph on the same box sits a pixel lower and reads as misaligned — the
   toolbar's whole job is to be scannable, and a row of letters bouncing by a
   pixel is not. */
.rpt-tool-txt {
  display: block;
  line-height: 1;
  font-size: .82rem;
  font-weight: 600;
  letter-spacing: -.01em;
}

/* The colour button's anchor, so the palette can be positioned without measuring
   anything. `position: relative` on the wrapper rather than on the whole bar is
   what keeps the palette tied to ITS button when the bar reflows. */
.rpt-tool-anchor { position: relative; display: inline-flex; }

/* ⚠️ The button shows the CURRENT colour as a bar under the A, so the reader can
   see what the palette is set to without opening it. The hex values are written
   out one per `data-current` rather than passed through a custom property: these
   are the palette's own CONTENT colours (the values a reader writes into a
   document), not theme tokens, and one selector per value is the only form in
   which a stylesheet can name a colour it is not theming with. */
.rpt-tool-color { position: relative; }
.rpt-tool-color::after {
  content: "";
  position: absolute;
  left: 7px; right: 7px; bottom: 4px;
  height: 3px;
  border-radius: 2px;
  background: #1f2328;
  pointer-events: none;
}
.rpt-tool-color[data-current="334155"]::after { background: #334155; }
.rpt-tool-color[data-current="6B7280"]::after { background: #6b7280; }
.rpt-tool-color[data-current="C0392B"]::after { background: #c0392b; }
.rpt-tool-color[data-current="B45309"]::after { background: #b45309; }
.rpt-tool-color[data-current="1E7A46"]::after { background: #1e7a46; }
.rpt-tool-color[data-current="0B5CAD"]::after { background: #0b5cad; }
.rpt-tool-color[data-current="6B3FA0"]::after { background: #6b3fa0; }

.rpt-swatches {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 30;
  display: grid;
  grid-template-columns: repeat(4, 26px);
  gap: 5px;
  padding: 7px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: var(--shadow-3);
}
.rpt-swatches[hidden] { display: none; }
.rpt-swatch {
  width: 26px;
  height: 26px;
  border-radius: 5px;
  border: 1px solid var(--border);
  cursor: pointer;
  padding: 0;
}
.rpt-swatch:hover { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ── the outline panel ───────────────────────────────────────────────────────
   The one part of edit mode that is REAL: it is read out of the rendered
   document in the same-origin iframe, so the count and the labels are this
   document's, not a fixture. */
.rpt-outline {
  flex: 0 0 208px;
  width: 208px;
  display: flex;
  flex-direction: column;
  min-width: 0;
  background: var(--surface-2);
  border-right: 1px solid var(--border);
}
.rpt-outline[hidden] { display: none; }
.rpt-outline-hd {
  display: flex; align-items: center; gap: 6px;
  padding: 7px 8px 7px 10px;
  border-bottom: 1px solid var(--border);
  font-size: .68rem; font-weight: 600; color: var(--text-2);
}
.rpt-outline-hd .ti { font-size: .85rem; color: var(--text-3); }
.rpt-outline-x {
  margin-left: auto; border: 0; background: none; cursor: pointer;
  color: var(--text-3); padding: 2px 4px; border-radius: 4px; line-height: 1;
}
.rpt-outline-x:hover { background: var(--hover); color: var(--text-1); }
.rpt-outline-list { flex: 1; overflow-y: auto; padding: 5px 6px 10px; }
.rpt-ol {
  display: flex; align-items: center; gap: 7px;
  width: 100%; text-align: left;
  border: 1px solid transparent; border-radius: 6px;
  background: none; font: inherit; cursor: pointer;
  padding: 4px 6px; margin-bottom: 2px;
  color: var(--text-1);
}
.rpt-ol:hover { background: var(--hover); }
.rpt-ol.on { background: var(--accent-soft); border-color: var(--accent-border); color: var(--accent-hover); }
.rpt-ol-n {
  flex: 0 0 20px; text-align: right;
  font-size: .62rem; color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.rpt-ol-t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: .7rem; }
.rpt-ol-empty { padding: 12px 8px; font-size: .68rem; color: var(--text-3); }

/* ── the document, while editing ─────────────────────────────────────────────
   ⚠️ DELIBERATELY NOT HERE. The preview is a separate document in a same-origin
   iframe and author CSS does not cross an iframe boundary, so `.pv-editing`
   rules in THIS file match nothing inside it — they would read as correct and do
   nothing on screen, which is the exact failure `verify_edit_mode_ui.py` measures
   `getComputedStyle` inside the frame to catch.

   The editor's styling is injected into the frame by
   `reportsPage.injectEditStyles()` (the constant `EDIT_FRAME_CSS` in index.html),
   which also copies `--accent` / `--accent-border` / `--surface-3` in from this
   app's theme, so theme.css stays the only place a colour is written down. */

.rpt-frame, .ibx-frame {
  flex: 1;
  width: 100%;
  border: 0;
  background: var(--surface);
}

/* ── dynamic report filter rail ──────────────────────────────────────────────
   The ONLY control surface a dynamic report has: the document itself is read-only
   and shows no filter UI at all (api/routers/reports.py `_inline_filter_runtime`).
   The rail is also read-only about the SCHEMA — an agent defines the filters and a
   reader only picks values — so nothing here creates, renames or removes a filter. */
.rpt-viewer-main {
  flex: 1;
  display: flex;
  min-height: 0;
}

.rpt-viewer-main > .rpt-frame { min-width: 0; }

.rpt-filters {
  flex: 0 0 276px;
  width: 276px;
  display: flex;
  flex-direction: column;
  min-width: 0;
  background: var(--surface);
  border-right: 1px solid var(--border);
}

.rpt-filters[hidden], .rpt-filters-toggle[hidden] { display: none; }

.rpt-filters-hd {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 11px 12px;
  border-bottom: 1px solid var(--border);
}

.rpt-filters-hd > i {
  color: var(--accent);
  font-size: 1rem;
  line-height: 1.3;
}

.rpt-filters-hd strong {
  display: block;
  font-size: .78rem;
  color: var(--text-1);
  line-height: 1.3;
}

.rpt-filters-hd span {
  display: block;
  font-size: .63rem;
  color: var(--text-3);
  line-height: 1.45;
}

.rpt-filters-body {
  flex: 1;
  overflow-y: auto;
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.rpt-filters-body .rpt-fl {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}

.rpt-fl > label {
  font-size: .62rem;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--text-3);
}

.rpt-fsel, .rpt-fnum, .rpt-fdate {
  width: 100%;
  height: 30px;
  padding: 0 8px;
  font: inherit;
  font-size: .75rem;
  color: var(--text-1);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
}

.rpt-fsel:focus, .rpt-fnum:focus, .rpt-fdate:focus {
  outline: none;
  border-color: var(--accent-border);
  background: var(--surface);
}

.rpt-frow > .rpt-fnum, .rpt-frow > .rpt-fdate { flex: 1; min-width: 0; }

.rpt-fchips {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}

.rpt-fchip {
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text-3);
  border-radius: 999px;
  padding: 4px 10px;
  font: inherit;
  font-size: .71rem;
  font-weight: 600;
  cursor: pointer;
}

.rpt-fchip:hover { border-color: var(--accent-border); color: var(--accent); }

.rpt-filters-ft {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 12px;
  border-top: 1px solid var(--border);
}

.rpt-filters-ft .rpt-freset {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 26px;
  padding: 0 9px;
  font: inherit;
  font-size: .7rem;
  font-weight: 600;
  color: var(--text-1);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
}

.rpt-filters-ft .rpt-freset:hover { border-color: var(--accent-border); color: var(--accent); }

.rpt-filters-msg {
  flex: 1;
  min-width: 0;
  font-size: .65rem;
  color: var(--text-3);
  text-align: right;
}

.rpt-filters-note {
  font-size: .66rem;
  color: var(--text-3);
  line-height: 1.5;
}

.rpt-filters-empty {
  font-size: .72rem;
  color: var(--text-3);
  padding: 6px 0;
}

.rpt-filters-err {
  font-size: .72rem;
  color: var(--danger);
  line-height: 1.5;
}

.rpt-iconbtn.rpt-filters-toggle.on { background: var(--accent-soft); color: var(--accent); }

/* publish dialog */
.rpt-modal {
  position: fixed;
  inset: 0;
  z-index: 8100;
  display: none;
  align-items: center;
  justify-content: center;
  background: rgba(15,23,42,.45);
}

.rpt-modal.open { display: flex; }

.rpt-modal-box {
  display: flex;
  flex-direction: column;
  width: 760px;
  max-width: 94vw;
  max-height: 90vh;
  background: var(--surface);
  border-radius: 12px;
  box-shadow: 0 24px 60px rgba(15,23,42,.28);
  overflow: hidden;
}

.rpt-modal-hd {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
}

.rpt-modal-bd {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  padding: 14px 16px;
  overflow-y: auto;
}

.rpt-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}

.rpt-field.rpt-wide { grid-column: 1 / -1; }

.rpt-field > label, .kld-dlg-field > label {
  font-size: .64rem;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--text-3);
}

.rpt-field input, .rpt-field select, .rpt-field textarea {
  padding: 6px 8px;
  font-size: .76rem;
  font-family: inherit;
  color: var(--text-1);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  outline: none;
}

.rpt-field input:focus, .rpt-field textarea:focus { border-color: var(--accent-border); }

.rpt-field textarea {
  min-height: 210px;
  resize: vertical;
  font-family: ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size: .69rem;
  line-height: 1.45;
  white-space: pre;
}

.rpt-modal-ft {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 16px;
  border-top: 1px solid var(--border);
}

.rpt-msg {
  flex: 1;
  font-size: .72rem;
  color: var(--text-3);
}

.rpt-msg.err { color: var(--danger); }

.rpt-msg.ok { color: var(--ok); }

/* language chooser — shown only for reports that ship more than one language */
.rpt-langpick { background: rgba(15, 23, 42, .5); }

.rpt-langpick-box {
  width: 430px;
  max-width: 92vw;
  background: var(--surface);
  border-radius: 14px;
  padding: 18px 18px 14px;
  box-shadow: 0 24px 60px rgba(15, 23, 42, .3);
}

.rpt-langpick-hd {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  margin-bottom: 14px;
}

.rpt-langpick-t {
  font-size: .84rem;
  font-weight: 700;
  color: var(--text-1);
  line-height: 1.35;
}

.rpt-langpick-s {
  font-size: .68rem;
  color: var(--text-3);
  margin-top: 2px;
}

.rpt-langpick-btns {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 10px;
}

.rpt-langbtn {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  padding: 12px 14px;
  cursor: pointer;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  text-align: left;
  transition: border-color .14s, background .14s;
}

.rpt-langbtn:hover { border-color: var(--accent-border); }

.rpt-langbtn.on {
  border-color: var(--accent-fill);
  background: var(--accent-soft);
  box-shadow: inset 0 0 0 1px rgba(37, 99, 235, .35);
}

.rpt-langbtn:focus-visible { outline: 2px solid rgba(37, 99, 235, .6); outline-offset: 1px; }

.rpt-langbtn-name {
  font-size: .86rem;
  font-weight: 700;
  color: var(--text-1);
}

.rpt-langbtn-code {
  font-size: .64rem;
  color: var(--text-3);
  letter-spacing: .06em;
}

.rpt-langpick-ft {
  margin-top: 12px;
  border-top: 1px solid var(--border);
  padding-top: 10px;
}

/* report language switch in the viewer bar (rendered from the deck state) */
.rpt-lang {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  border-radius: 999px;
  background: var(--bg);
  border: 1px solid var(--border);
}

.rpt-lang:empty { display: none; }

.rpt-lang button {
  border: 0;
  background: none;
  color: var(--text-3);
  cursor: pointer;
  font: inherit;
  font-size: .66rem;
  font-weight: 700;
  letter-spacing: .04em;
  padding: 2px 8px;
  border-radius: 999px;
}

.rpt-lang button:hover { color: var(--accent); }

.rpt-lang button.on { background: var(--accent-fill); color: var(--text-on-ink); }

/* ── Business knowledge base (the wiki) ───────────────────────────────────────
   Two panes: the page list (目录) on the left, the page on the right. Reading
   and editing share one pane so the layout never jumps; the editor is a
   contenteditable block list, so markdown syntax is never shown to the author. */
.kb-body, .wr-body {
  flex: 1;
  display: flex;
  min-height: 0;
}

.kb-toc, .wr-toc {
  width: 284px;
  flex-shrink: 0;
  border-right: 1px solid var(--border);
  background: var(--surface);
  overflow-y: auto;
  padding: 8px 8px 20px;
}

.kb-toc::-webkit-scrollbar, .wr-toc::-webkit-scrollbar, .kb-main::-webkit-scrollbar, .wr-main::-webkit-scrollbar { width: 6px; }

.kb-toc::-webkit-scrollbar-thumb, .wr-toc::-webkit-scrollbar-thumb, .kb-main::-webkit-scrollbar-thumb, .wr-main::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }

.kb-link {
  display: flex;
  align-items: baseline;
  gap: 7px;
  width: 100%;
  text-align: left;
  border: 0;
  background: none;
  font: inherit;
  cursor: pointer;
  padding: 6px 9px;
  border-radius: 6px;
  color: var(--text-2);
  font-size: .78rem;
  line-height: 1.35;
}

.kb-link:hover { background: var(--accent-soft); color: var(--accent-hover); }

.kb-link.on {
  background: var(--accent-soft);
  color: var(--accent-hover);
  font-weight: 700;
}

.kb-link.ctx-active { background: var(--accent-soft); }

/* One line per page: the six-digit date, then the title. The title ellipsises instead of
   wrapping — rows that change height cannot be scanned — and the full title, the summary
   and the submitter live in the hover tooltip. */
.kb-when {
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .01em;
  color: var(--text-3);
}

.kb-link:hover .kb-when, .kb-link.on .kb-when { color: inherit; }

.kb-link-t, .cal-attach-row > span {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 启用开关（2026-09-30）：星在日期前。亮 = ri-star-fill 金色（参与检索）；
   不亮 = ri-star-line 灰描边（已停用：页面仍可读、仍在列表）。remix 库图标。 */
.kb-star {
  flex: 0 0 auto;
  border: 0;
  background: none;
  padding: 0 1px;
  font-size: 12px;
  line-height: 1;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  border-radius: 3px;
  color: var(--gold);
  transition: color .15s, opacity .15s;
}

.kb-star.off { color: var(--text-3); opacity: .6; }

.kb-star:focus-visible { outline: 2px solid rgba(37, 99, 235, .5); outline-offset: 1px; }

/* ■ 项目 card wall (2026-10-03) ■
   "My knowledge" opens on this. The card shape is the Workspace card's — a 16:9
   cover on top, a title and a count below — deliberately, so the two walls are read
   with one eye. The placeholder when a project has no cover reuses `.rpt-cover`'s
   tokens rather than inventing a second gradient that could drift from it.
   ⚠️ Nothing here writes a colour literal: every value is a token from theme.css,
   so both themes are covered by construction. */
.kb-wall, .wr-wall {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 18px 20px 28px;
}

.kb-wall-grid, .wr-wall-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill,minmax(225px,1fr));
  gap: 12px;
  align-content: start;
}

.kb-proj, .wr-proj, .dc-proj {
  position: relative;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  overflow: hidden;
  cursor: pointer;
  text-align: left;
  padding: 0;
  font: inherit;
  color: inherit;
  transition: box-shadow .18s, border-color .18s, transform .18s;
}

.kb-proj:hover, .wr-proj:hover, .dc-proj:hover { border-color: var(--accent); transform: translateY(-1px); }
.kb-proj:focus-visible, .wr-proj:focus-visible, .dc-proj:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.kb-proj-cover, .wr-proj-cover, .dc-proj-cover {
  display: block;
  position: relative;
  aspect-ratio: 16/9;
  overflow: hidden;
  background: var(--cover-gradient);
}

.kb-proj-cover img, .wr-proj-cover img { display: block; width: 100%; height: 100%; object-fit: cover; }

.kb-proj-cover-ph, .wr-proj-cover-ph, .dc-proj-cover-ph {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  color: var(--text-3);
}

.kb-proj-cover-ph i, .wr-proj-cover-ph i, .dc-proj-cover-ph i { font-size: 26px; }
.kb-proj-cover-ph span, .wr-proj-cover-ph span, .dc-proj-cover-ph span { font-size: .7rem; }

/* The system 未归档 card's cover, drawn by the app (see systemCoverSvg()). A
   system container is not user content, so it is never given an uploaded or
   generated image: the picture is chrome, and chrome follows the theme. */
.kb-proj-cover-sys { position: absolute; inset: 0; display: block; width: 100%; height: 100%; }
.kb-sys-slot { fill: var(--accent-fill); }
.kb-sys-rim { fill: none; stroke: var(--surface); stroke-width: 2; stroke-linecap: round; opacity: .38; }
/* ⚠️ The dashed target is drawn in `--text-3`, not `--border`: the border token is
   so faint on the light cover gradient that the outline vanished, and the one
   element that says "there is a place for these" is exactly the one that must not
   disappear in half the themes. */
.kb-sys-target { fill: none; stroke: var(--text-3); stroke-width: 1.5; stroke-dasharray: 4 4; }
.kb-sys-sheet rect { fill: var(--surface); stroke: var(--border); stroke-width: 1.5; }
.kb-sys-rule { fill: none; stroke: var(--text-3); stroke-width: 2.5; stroke-linecap: round; }

.kb-proj-body, .wr-proj-body, .dc-proj-body { display: block; padding: 9px 11px 11px; }

.kb-proj-title, .wr-proj-title, .dc-proj-title {
  margin: 0 0 3px;
  font-size: .86rem;
  font-weight: 600;
  line-height: 1.35;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The count line says what is inside, so a reader can choose a card without
   opening it. A shared project says so here rather than only on hover. */
.kb-proj-sub, .wr-proj-sub, .dc-proj-sub {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: .72rem;
  color: var(--text-3);
}

/* The two reserved description lines on a Workspace project card.
   ⚠️ The two lines are RESERVED whether or not there is anything to put in them, and
   that is the part that matters: a card that grows when a description is added is a
   wall that re-flows under the reader. `-webkit-line-clamp` is what does the reserving
   — it caps the box at two lines — so an empty slot and a four-line note come out the
   same height, which is the ragged-grid defect `.rpt-card-sum` was written to kill, one
   level up.

   ⚠️ `height` rather than `min-height` is a **measured** choice, and the note says so
   because a mutation harness found the two indistinguishable here: the clamp already
   caps at two lines and `line-height × 2` is exactly this `height`, so the two compute
   the same box for every input. `height` is kept because it stays true if the font
   size moves and the two stop agreeing, where `min-height` would then let a two-line
   block grow. It is not load-bearing today, and claiming it was would be the same
   mistake as a comment that names a risk the code does not have.

   ⚠️ `-webkit-line-clamp` over `line-clamp` on purpose, and the same reason
   `.rpt-card-title` uses it: the unprefixed property is not honoured by the engine this
   ships in, and a clamp that silently does nothing is worse than no clamp — it lets a
   four-line note push the grid out of line again. */
.wr-proj-sum, .dc-proj-sum {
  margin: 0 0 4px;
  height: 2.9em;
  font-size: .72rem;
  line-height: 1.45;
  color: var(--text-2);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
}

.kb-proj-badge, .wr-proj-badge {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: .68rem;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
}

/* The "make a project" tile. Same box as a card, so the wall does not end in a
   differently-sized button the eye has to find. */
.kb-proj-new, .wr-proj-new, .dc-proj-new {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 150px;
  border: 1px dashed var(--border);
  border-radius: 10px;
  background: none;
  color: var(--text-3);
  font: inherit;
  cursor: pointer;
}

.kb-proj-new:hover, .wr-proj-new:hover, .dc-proj-new:hover { border-color: var(--accent); color: var(--accent); }
.kb-proj-new:focus-visible, .wr-proj-new:focus-visible, .dc-proj-new:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.kb-proj-new i, .wr-proj-new i, .dc-proj-new i { font-size: 22px; }
.kb-proj-new span, .wr-proj-new span, .dc-proj-new span { font-size: .78rem; }

/* Breadcrumb: 全部项目 / <项目>. The first part is a button so leaving a project
   is one click from anywhere in the header, not a hunt for a back arro.kb-sec[hidden], .wr-sec[hidden] { display: none; }

/* The move picker's select. Styled here rather than left to the platform because the
   unstyled control renders at a different height per browser, and the dialog's OK
   button is aligned to the field. */
.kb-move-label, .wr-move-label { display: block; margin-bottom: 5px; font-size: .78rem; color: var(--text-3); }

.kb-move-sel, .wr-move-sel {
  display: block;
  width: 100%;
  min-height: 34px;
  padding: 6px 8px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text-1);
  font: inherit;
  font-size: .84rem;
}

/* The folder rail, above the page list in the same left column. 「未归档」 sits
   first because it is where an unfiled page is, and the whole point of the feature
   is that a reader never has to wonder where something went. */
.kb-folders, .wr-folders { padding: 2px 0 6px; }

.kb-sec, .wr-sec {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 7px 10px 4px;
  font-size: .66rem;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-3);
}

.kb-sec::after, .wr-sec::after { content: ''; flex: 1; height: 1px; background: var(--border); }

/* ⚠️ An explicit `[hidden]` rule, stated on purpose rather than left to chance.
 * `.kb-body` is `display: flex` (shared rule above), and a normal author `display`
 * beats the browser's `[hidden] { display: none }` default — so without this line
 * the wall and the directory view would render on top of each other. The vendor
 * sheets happen to ship `[hidden]{display:none!important}` (verified in
 * bootstrap.min.css and tabler.min.css) that would rescue it today; this rule is
 * here so the contract does not depend on a vendor escape hatch. Same contract as
 * `.dc-crumbs[hidden]` above.
 *
 * ⚠️ `.wr-body[hidden]` is deliberately absent. The Workspace body shares the same
 * `display: flex` rule and relies on the vendor `!important` instead of repeating
 * this line. That is a difference in the two modules' history, NOT a statement that
 * `.wr-body` carries no author `display` — an earlier version of this comment said
 * exactly that and was wrong. (The `.kb-crumbs[hidden]` half of this block is gone
 * with the knowledge breadcrumb row.) */
.kb-body[hidden] { display: none; }

/* ⚠️ The Workspace's way back to the project wall, and the only one — it used to
   be the `Projects` crumb in the row above this column. A chevron and the open
   project's name, in the rail, because that is where a reader's eye already goes
   for "the level above this".
   `[hidden]` needs its own rule for the same reason as above: this rule sets
   `display: flex`. */
.wr-back {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  margin: 0 0 6px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--surface);
  padding: 5px 9px;
  font: inherit;
  font-size: .78rem;
  color: var(--text-1);
  text-align: left;
  cursor: pointer;
}
.wr-back[hidden] { display: none; }
.wr-back:hover { background: var(--accent-soft); color: var(--accent-hover); }
.wr-back:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.wr-back i { flex: 0 0 auto; color: var(--text-3); }
.wr-back:hover i { color: var(--accent); }
.wr-back span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.kb-folder, .wr-folder {
  display: flex;
  align-items: center;
  gap: 7px;
  width: 100%;
  border: 0;
  border-radius: 7px;
  background: none;
  padding: 5px 9px;
  font: inherit;
  font-size: .8rem;
  color: var(--text-1);
  text-align: left;
  cursor: pointer;
}

.kb-folder:hover, .wr-folder:hover { background: var(--accent-soft); }
.kb-folder.on, .wr-folder.on { background: var(--accent-soft); color: var(--accent-hover); font-weight: 600; }
.kb-folder:focus-visible, .wr-folder:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.kb-folder i { flex: 0 0 auto; color: var(--text-3); }
.kb-folder.on i { color: var(--accent); }
.kb-folder-t, .wr-folder-t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kb-folder-n, .wr-folder-n { flex: 0 0 auto; font-size: .7rem; color: var(--text-3); }
.kb-folder-shared { flex: 0 0 auto; font-size: .68rem; color: var(--accent); }

/* ── Workspace (the `.wr-` half) — the same wall and the same directory ───────
   The card, the folder rail and the two-pane body are the knowledge page's own
   components, so their rules above are comma-joined rather than restated here.
   Only the three things the Workspace has that the wiki does not live below.

   ⚠️ The split is by role, not by resemblance: `.wr-filters` and `.wr-row` belong
   to the report list (the wiki's `.kb-link` renders a page row, a report row has to
   carry a status and a document type), and the inline viewer is a different
   *container* for the same reader. */

.wr-filters { display: flex; flex-direction: column; gap: 5px; padding: 2px 0 8px; }
.wr-filters[hidden] { display: none; }

.wr-filters .rpt-input, .wr-filters .rpt-select { width: 100%; }

.wr-row {
  display: flex;
  align-items: center;
  gap: 7px;
  width: 100%;
  text-align: left;
  border: 0;
  border-radius: 7px;
  background: none;
  font: inherit;
  color: var(--text-1);
  cursor: pointer;
  padding: 6px 9px;
}

.wr-row:hover { background: var(--accent-soft); color: var(--accent-hover); }
.wr-row.on { background: var(--accent-soft); color: var(--accent-hover); font-weight: 600; }
.wr-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.wr-row-icon { flex: 0 0 auto; opacity: .8; }

/* ⚠️ The row is ONE line (2026-10-04). `.wr-row-main` is a row, not a column: the title
   takes the slack and truncates, the size sits after it and never shrinks. Both need
   `min-width: 0` for the same reason — without it a long title refuses to shrink and
   pushes the size and the badge out of the 284px pane instead of truncating. */
.wr-row-main { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; }
.wr-row-t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: .8rem; }

/* The size is not the title said twice: "2.0 MB" is the one number a title cannot imply,
   and it survived the removal of the second line on purpose. */
.wr-row-size { flex: 0 0 auto; font-style: normal; font-size: .63rem; color: var(--text-3); }
.wr-row.on .wr-row-size { color: inherit; }

/* ⚠️ The icon says which kind of file this is, and it says it by COLOUR (2026-10-04).
 *
 * The row lost its second line and with it the only remaining written copy of the file
 * format, so the icon went from decoration to the type itself. Grey, it identified
 * nothing — there are four of them and they differ by a few pixels of glyph.
 *
 * ⚠️ Every colour here is a token from theme.css and the mapping is by `data-doctype`,
 * which `rowHtml` writes. Do NOT put a hex in this file, and do not switch on the
 * `ri-file-*` class names instead: a fifth format would then have to be remembered in
 * two files, and the class names are the icon set's business, not the palette's.
 *
 * ⚠️ The full opacity is deliberate. `.wr-row-icon` above keeps `opacity: .8` for the
   neutral report icon; these four drop it, because 0.8 opacity over the surface is a
 * different colour than the token and the contrast below was measured on the token. */
.wr-row-icon[data-doctype] { opacity: 1; }
.wr-row-icon[data-doctype="pptx"] { color: var(--doc-pptx); }
.wr-row-icon[data-doctype="xlsx"] { color: var(--doc-xlsx); }
.wr-row-icon[data-doctype="docx"] { color: var(--doc-docx); }
.wr-row-icon[data-doctype="pdf"]  { color: var(--doc-pdf); }

.wr-row-b { flex: 0 0 auto; font-size: .64rem; color: var(--text-3); font-weight: 600; text-transform: uppercase; letter-spacing: .03em; }
.wr-row.on .wr-row-b { color: inherit; }
.wr-row-draft { color: var(--warn); }

/* The right pane. `.kb-main` scrolls because it holds a rendered markdown page; the
   Workspace pane holds a *live document* in an iframe, which must never be inside a
   scrolling box — the deck scales to its own viewport, and a scrolled parent would
   clip it. `overflow: hidden` + `min-height: 0` is what lets the inline viewer
   below fill the pane exactly. */
.wr-main { overflow: hidden; display: flex; flex-direction: column; }

.wr-empty {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  text-align: center;
  color: var(--text-3);
  padding: 24px;
}

.wr-empty i { font-size: 30px; }
.wr-empty-t { font-size: .88rem; font-weight: 600; color: var(--text-1); }
.wr-empty-s { font-size: .76rem; max-width: 340px; line-height: 1.55; }

/* ── the report viewer, inline ────────────────────────────────────────────────
   `.rpt-viewer` is a full-viewport overlay (`position: fixed; inset: var(--nav-h) 0
   0 0; z-index: 8000`) because the top bar must stay reachable. The directory view
   wants the SAME reader — deck paging, dynamic-report filters, the language pair,
   notes, export — just filling the right pane instead of the screen, so this is a
   modifier on the one element rather than a second reader that would have to keep
   re-implementing the first one's features.

   ⚠️ `position: static` + `inset: auto` are both required. Leaving `inset` in place
   on a static element is harmless but leaving `position: fixed` would take the
   reader out of the pane and put it back over the top bar — which is precisely the
   bug AGENTS.md records for the overlay. And the class is added and removed by the
   two functions that own the element, so the two modes can never be half-applied. */
.rpt-viewer.inline {
  position: static;
  inset: auto;
  z-index: auto;
  flex: 1;
  min-height: 0;
  width: 100%;
  border-radius: 0;
}

.rpt-viewer.inline.open { display: flex; }

/* The empty state and the viewer are mutually exclusive in the same pane, and both
   are toggled by class/attribute — so both need the explicit `display` reset. See
   the `hidden` rule note in AGENTS.md: the browser's own `[hidden]` is only a
   default and any author `display` beats it. */
.wr-empty[hidden] { display: none; }

/* Right-click menu on a page row. */
.kb-ctx {
  position: fixed;
  z-index: 70;
  min-width: 176px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 5px;
  box-shadow: 0 12px 30px rgba(15, 23, 42, .18);
}

.kb-ctx button {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  border: 0;
  background: none;
  font: inherit;
  font-size: .76rem;
  color: var(--text-2);
  text-align: left;
  cursor: pointer;
  padding: 7px 9px;
  border-radius: 6px;
}

.kb-ctx button:hover { background: var(--accent-soft); color: var(--accent-hover); }

.kb-ctx button.danger { color: var(--danger); }

.kb-ctx button.danger:hover { background: var(--danger-soft); }

/* Hover card for a page row — replaces the browser's own tooltip. `pointer-events:none` so
   the card can never steal the pointer away from the row it describes (which would hide it
   the instant it appeared). */
/* ⚠️ `box-shadow` was `0 14px 34px rgba(15, 23, 42, .18)` — a hand-written colour, and
   a light-mode one: in the dark theme it was a pale grey haze at the edge of a dark
   card, which read as a smudge rather than a lift. `--shadow-3` is the same shape in
   light (`0 12px 32px rgba(15,23,42,.18)`) and a real one in dark
   (`0 14px 36px rgba(0,0,0,.55)`). This card is `position: fixed`, so it sits on top of
   whatever is under it in BOTH themes and a wrong shadow is visible in both. */
.kb-tip {
  position: fixed;
  z-index: 69;
  width: 306px;
  max-width: 46vw;
  pointer-events: none;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow-3);
  padding: 11px 13px;
  opacity: 0;
  transition: opacity .12s ease;
}

.kb-tip.on { opacity: 1; }

.kb-tip-t, .cal-tip-t {
  font-size: .82rem;
  font-weight: 800;
  line-height: 1.35;
  color: var(--text-1);
}

.kb-tip-s, .cal-tip-s {
  margin-top: 7px;
  font-size: .73rem;
  line-height: 1.55;
  color: var(--text-2);
}

.kb-tip-f, .cal-tip-f {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 9px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
  font-size: .68rem;
  color: var(--text-3);
}

.kb-tip-f i { font-size: .8rem; }

.kb-tip-date { margin-left: auto; font-variant-numeric: tabular-nums; }

/* ■ WORKSPACE ROW HOVER CARD — `#wr-tip` reuses every `.kb-tip-*` rule above ■
   ⚠️ The z-index is the one thing it does NOT inherit, and it has to be its own rule.
   `.kb-tip` sits at 69 because it lives INSIDE `#page-knowledge`: everything it can
   overlap is a sibling within that page. This one is at body level and describes rows
   that sit beside `.rpt-viewer` (8000, opaque, full-screen) — at 69 it would be a
   perfectly laid-out card drawn UNDERNEATH the document, which is the same silent
   failure the nav bar had: correct DOM, correct box, nothing on screen.

   8100 is the `#auth-menu` slot (8050) rounded up: over the viewer, under
   `.rpt-modal` 8100+/`.cal-modal` 9000/`kldDialog` 9700, so a dialog still covers it. */
#wr-tip { z-index: 8100; }

/* A document's full filename, which the row itself does not print (and which used to
   live only in a native `title` that this card replaced). Same weight as the byline
   above it — it is provenance, not headline — so it sits one step quieter. */
.wr-tip-file {
  margin-top: 8px;
  font-size: .68rem;
  line-height: 1.5;
  color: var(--text-3);
  overflow-wrap: anywhere;
}

.wr-tip-file i { font-size: .78rem; vertical-align: -1px; }

/* ■ CALENDAR — one row per month, one cell per day, events as gantt bars ■
   The grid is 31 columns wide for EVERY month so a given day sits at the same x in every
   row: a bar that starts on the 12th lines up with the 12th of the month above it, which is
   the whole reason to read six months at once. Months shorter than 31 days leave the tail
   cells switched off. */
.cal-wrap {
  flex: 1;
  min-height: 0;
  overflow: auto;
  padding: 12px 14px 44px;
}

/* ── the kind filter: one tile per event type, all of them on by default ──────
   Lane packing gets crowded the moment several events share a week, and the first thing a
   reader wants is to drop the kinds they are not looking at. A tile carries the SAME colour
   the bar of that kind will be, so the filter doubles as the legend the calendar never had
   (the two were one problem: "what are these grey bars").
   Sized like `.rpt-tab` on purpose — the toolbar keeps one rhythm, no size modifiers. */
.cal-kinds {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex-wrap: wrap;
}

.cal-kind {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  /* ⚠️ OFF is borderless + muted, ON is a bordered tinted chip. The difference is FORM, not
     just hue, on purpose: `other` is slate grey, and a grey "on" tile next to the grey
     unselected tabs read as inactive (caught in review 2026-09-30). Hue alone cannot carry
     the state for a grey kind, so shape does. */
  border: 1px solid transparent;
  border-radius: 6px;
  background: none;
  padding: 5px 9px;
  font: inherit;
  font-size: .71rem;
  font-weight: 700;
  color: var(--text-3);
  cursor: pointer;
  white-space: nowrap;
  opacity: .55;
}

.cal-kind i { font-size: .8rem; }

/* ⚠️ Hover must NOT borrow the ON look. The first version gave every hovered tile a solid
   border + dark text, so an OFF tile under the pointer looked exactly like an ON one — a
   reviewer read the tile it had just switched off as "on" (2026-09-30). Hover only lifts the
   text and adds a faint wash; the border belongs to the ON state alone. */
.cal-kind:hover {
  opacity: 1;
  color: var(--text-1);
  background: var(--hover);
}

/* On: a solid chip in the kind's OWN colours — the same fill, edge and ink as one of its bars
   (asked for 2026-10-01: the row of pale outlines above a row of solid bars read as two
   different palettes; the tile IS the legend, so it has to be made of the same stuff).
   ⚠️ That also settles the grey `other` tile, whose "on" state could not be told from an
   inactive tab by hue alone — a solid body is a difference in FORM, which no hue can fake.
   One variable per job, all from the type: `--kind`      → the body;
     `--kind-edge` → the border (the bar's own 1px edge colour);
     `--kind-ink`  → the glyph and the label. */
.cal-kind.on {
  opacity: 1;
  color: var(--kind-ink);
  border-color: var(--kind-edge);
  font-weight: 800;
  background: var(--kind);
}

.cal-kind.on:hover { background: color-mix(in srgb, var(--kind) 82%, var(--surface)); }

.cal-kind.on i { color: var(--kind-ink); }

.cal-kind:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.cal-grid {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.cal-month {
  display: flex;
  align-items: stretch;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--surface);
  overflow: hidden;
}

.cal-mlabel {
  flex: 0 0 104px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 1px;
  padding: 6px 9px;
  background: var(--bg);
  border-right: 1px solid var(--border);
}

.cal-mlabel b {
  font-size: .76rem;
  font-weight: 800;
  color: var(--text-1);
}

.cal-mlabel small { font-size: .63rem; color: var(--text-3); }

.cal-days {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
}

.cal-cells {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: repeat(31, minmax(0, 1fr));
}

.cal-cell {
  border-right: 1px solid var(--hover);
  padding: 1px 0 0 3px;
  font-size: .58rem;
  line-height: 1.4;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  overflow: hidden;
}

.cal-cell.wk { background: rgba(15, 23, 42, .03); }

.cal-cell.off { background: transparent; border-right-color: transparent; }

.cal-cell.today {
  background: var(--accent-soft);
  color: var(--accent-hover);
  font-weight: 800;
}

.cal-bar {
  position: absolute;
  height: 20px;
  border-radius: 5px;
  padding: 0 6px;
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: .67rem;
  font-weight: 700;
  /* ⚠️ `border-box` is global, so this 1px edge is INSIDE the 20px the lane maths and the
     milestone triangle both assume (`BAR_H`);
  a content-box border would make the bar 22px and
     drop every triangle 2px below its bar. */
  border: 1px solid transparent;
  /* Both are fallbacks: every bar is given its type's own `ink` and `edge` inline (see
     `KIND_META`) — white text on the light fills measures ~2:1. */
  color: var(--text-on-ink);
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  box-shadow: 0 1px 2px rgba(15, 23, 42, .18);
}

/* ⚠️ Per-type bar colours, resolved through the per-theme tokens in theme.css — the only
   place in this app allowed to name a colour. A bar carries `k-<kind>` and no inline
   colour at all: `-fill` is the body, `-ink` the label, `-edge` the 1px rule that is
   what gives the body a shape. `.cal-bar.cancelled` still wins on its own `!important`.
   A milestone keeps its own `background: none !important` above, so it is unaffected. */
.cal-bar.k-meeting {
  background: var(--cal-meeting-fill);
  color: var(--cal-meeting-ink);
  border-color: var(--cal-meeting-edge);
}
.cal-bar.k-review {
  background: var(--cal-review-fill);
  color: var(--cal-review-ink);
  border-color: var(--cal-review-edge);
}
.cal-bar.k-launch {
  background: var(--cal-launch-fill);
  color: var(--cal-launch-ink);
  border-color: var(--cal-launch-edge);
}
.cal-bar.k-promotion {
  background: var(--cal-promotion-fill);
  color: var(--cal-promotion-ink);
  border-color: var(--cal-promotion-edge);
}
.cal-bar.k-campaign {
  background: var(--cal-campaign-fill);
  color: var(--cal-campaign-ink);
  border-color: var(--cal-campaign-edge);
}
.cal-bar.k-media {
  background: var(--cal-media-fill);
  color: var(--cal-media-ink);
  border-color: var(--cal-media-edge);
}
.cal-bar.k-content {
  background: var(--cal-content-fill);
  color: var(--cal-content-ink);
  border-color: var(--cal-content-edge);
}
.cal-bar.k-offline {
  background: var(--cal-offline-fill);
  color: var(--cal-offline-ink);
  border-color: var(--cal-offline-edge);
}
.cal-bar.k-training {
  background: var(--cal-training-fill);
  color: var(--cal-training-ink);
  border-color: var(--cal-training-edge);
}
.cal-bar.k-research {
  background: var(--cal-research-fill);
  color: var(--cal-research-ink);
  border-color: var(--cal-research-edge);
}
.cal-bar.k-planning {
  background: var(--cal-planning-fill);
  color: var(--cal-planning-ink);
  border-color: var(--cal-planning-edge);
}
.cal-bar.k-other {
  background: var(--cal-other-fill);
  color: var(--cal-other-ink);
  border-color: var(--cal-other-edge);
}

.cal-bar:hover { filter: brightness(1.08); }

.cal-bar.sel { outline: 2px solid rgba(15, 23, 42, .35); outline-offset: 1px; }

/* ⚠️ 0.68, not 0.55: at 0.55 a light fill over the white grid practically disappeared, and
   "draft" has to read as quiet, not as absent. */
.cal-bar.draft { opacity: .68; }

.cal-bar.cancelled {
  background: var(--border-strong) !important;
  border-color: var(--border-strong) !important;
  color: var(--text-1) !important;
  text-decoration: line-through;
}

/* A milestone: the diamond says "this DAY is the event", the chip beside it says which.
   `width:auto` on purpose — its geometry is a point, not a span, so it must not inherit
   the bar's day-proportional width. */
.cal-bar.milestone {
  width: auto;
  background: none !important;
  box-shadow: none;
  overflow: visible;
  gap: 5px;
  padding: 0;
}

.cal-ms-dot {
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  margin-left: 3px;
  border-radius: 2px;
  transform: rotate(45deg);
  box-shadow: 0 1px 2px rgba(15, 23, 42, .28);
}

.cal-ms-label {
  font-size: .67rem;
  font-weight: 700;
  color: var(--text-1);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 1px 6px;
  max-width: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
}

.cal-bar.milestone:hover .cal-ms-label { border-color: var(--accent-fill); color: var(--accent-hover); }

.cal-bar.milestone.sel .cal-ms-label { border-color: var(--accent-fill); }

.cal-bar i { font-size: .74rem; flex: 0 0 auto; }

.cal-bar span { overflow: hidden; text-overflow: ellipsis; }

/* ── a milestone: a vertical white dashed line, not a chip ────────────────────
   ⚠️ Replaced 2026-10-01 (asked for: "甘特图里的 milestone 用竖的白色虚线来表示吧，放在甘特图内
   文本的下一个层级，不要挡着文本"). What it replaced was a purple diamond sitting in its own
   22px strip at the bottom of the month row — a band separated from the bars by a gap, with no
   label (the label would collide the moment two dues landed on the same day, which on a 31-day
   grid is normal). It read as "a mystery marker floating under the chart" rather than as "this
   day". A line through the row says the same thing in the place the eye already is.

   Three properties, in order of how easy they are to break:
   1. `pointer-events: none` — a full-height line OVER the bars would swallow the click and the
      double-click that open an event. It must be invisible to the mouse.
   2. Drawn above the bar FILL but below the bar TEXT: the line is `z-index: 1`, and
      `.cal-bar > i / > span` are lifted to `z-index: 2`. Without that lift the white dashes
      would cross the titles (a bar is one stacking layer when it has no z-index of its own).
   3. White with a thin dark halo, not plain white: the month background is white, so a bare
      white line is invisible on a day with no bar — and a milestone on a quiet day would then
      be invisible altogether. The halo makes it a faint grey dash there and a white dash where
      it crosses a bar.
   `done` to-dos keep the same line, dotted — still findable, visibly finished. */
/* ── a milestone: a small equilateral triangle sitting ON the chart's bottom edge ──
   ⚠️ Replaced 2026-10-01 (asked for: "把细线换成等边小三角。三角形的下面的边与甘特图的底边
   重合。高度是3px"). Height 5px, so the base is 2×5/√3 = 5.77px — 2.89px on each side.
   The colour is the STATUS, not the type: green = done (on time or early), red = overdue, WHITE = not due and not done (asked for 2026-10-01 — it was amber). That is the one thing a
   type colour could never say. `pointer-events: none` stays: nothing here should ever take a
   click away from the chart. */
.cal-ms-tri {
  /* ⚠️ The OPEN state is WHITE (asked for 2026-10-01: "那个三角，当没有到时间也没有完成的时候
     改成白色"). It was an amber yellow, which read as a warning;
  white reads as "nothing to
     report yet" — the same thing the type colours now say at a lighter key.
     ⚠️ White on a light bar is ~1.6:1, so the mark carries its own hairline. It cannot be a
     `border`: `clip-path` clips the element AND its descendants, so a real border (or a child
     element) would be cut away with everything else outside the triangle. `drop-shadow` with
     zero blur and a sub-pixel offset is drawn from the CLIPPED shape, which is exactly the
     outline wanted, and it costs one paint. */
  --ms-color: var(--ms-todo);
  --ms-edge: rgba(15, 23, 42, .55);
  /* ⚠️ `clip-path`, not a border triangle: a transparent border of a fractional px gets rounded
     to a whole pixel by layout, so the border version measured 2px wide against 3px tall — not
     the equilateral triangle that was asked for. 5px tall means a base of 5 × 2/√3 = 5.77px, and
     `translateX(-50%)` puts that base's CENTRE on the day, not its left edge. */
  position: absolute;
  width: 5.77px;
  height: 5px;
  background: var(--ms-color);
  clip-path: polygon(50% 0%, 100% 100%, 0% 100%);
  transform: translateX(-50%);
  filter: drop-shadow(0.6px 0 0 var(--ms-edge)) drop-shadow(-0.6px 0 0 var(--ms-edge));
  pointer-events: none;
  z-index: 1;
}

/* Done and overdue are dark enough to stand on a light bar unaided, so they keep a crisp edge
   (no halo) — and a green/red triangle is a statement, not a placeholder. */
.cal-ms-tri.is-done { --ms-color: var(--ms-done); filter: none; }

/* finished */
.cal-ms-tri.is-late { --ms-color: var(--ms-late); filter: none; }

/* its day has passed, not finished */
/* The event's own deadline is the same triangle on the same rail. It has no "done", so it is
   red once the day has passed and white before — never green. */
.cal-ms-tri.is-deadline.is-late { --ms-color: var(--ms-late); }

.cal-bar > i, .cal-bar > span { position: relative; z-index: 2; }

/* Right-click menu and hover card: same anatomy as the wiki's, so one menu reads like the
   other. ⚠️ `hr` keeps Tabler's 2rem margins unless it is pinned down here — that is what
   made the wiki menu look "spaced far apart" on 2026-09-30. */
/* ⚠️ Above `.cal-modal` (9000) and `.cal-pick` (9500): the overlay's own Export / Cover
   menus open from the toolbar and are drawn in this same element. At z-index 70 they opened
   *behind* the modal's scrim — the menu existed and could not be seen. */
.cal-ctx {
  position: fixed;
  z-index: 9600;
  min-width: 190px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 9px;
  padding: 5px;
  box-shadow: 0 12px 30px rgba(15, 23, 42, .18);
}

.cal-ctx button {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  border: 0;
  background: none;
  font: inherit;
  font-size: .74rem;
  color: var(--text-2);
  text-align: left;
  cursor: pointer;
  padding: 8px 10px;
  border-radius: 5px;
}

.cal-ctx button:hover { background: var(--accent-soft); color: var(--accent-hover); }

.cal-ctx button.danger { color: var(--danger); }

.cal-ctx button.danger:hover { background: var(--danger-soft); }

/* ── the MONTH view ──────────────────────────────────────────────────────────
   Seven weekday columns and one row per week, against the timeline's "one row per month,
   31 day columns". Two shapes for the same events, so everything here is deliberately
   narrow: the reader picks a view with the toolbar tabs and the filters, the kind colours and
   the chips themselves are the SAME objects as the timeline's bars, and `.cal-bar` is reused
   rather than reinvented so click / double-click / hover / right-click keep working.
   ⚠️ Every colour is a token. `theme.css` owns the palette; a literal here would only ever
   be right in one of the two themes. */
/* ⚠️ THIS IS WHAT MAKES THE GRID FILL THE PAGE, and it is one line for a reason.
   `#cal-grid` is `display: block` with an AUTO height, so a `min-height: 100%` on the card
   inside it has no definite height to resolve against and computes to nothing — the month
   then shrinks to its content (measured: 286px of an 863px window, 37%) and the reader gets
   a 160px strip at the top of an otherwise empty screen. Giving THIS element `min-height:
   100%` puts the percentage one level up, where `.cal-wrap` (a flex item with a real
   `clientHeight`) can answer it, and `display: flex` then lets the card take what is left. */
#cal-grid.is-month {
  display: flex;
  flex-direction: column;
  min-height: 100%;
}

.cal-mv {
  /* One lane of bars, and the day-number strip above them. Both are named so a week row's
     minimum height and the renderer's `grid-template-rows` cannot drift apart — the row
     writes `auto repeat(N, minmax(var(--cal-mv-lane), 1fr))`, and the number of day-number
     pixels is declared here rather than spelled into the min-height a second time.
     ⚠️ 18 → 26px on 2026-10-06 ("甘特图的高度能不能调高一点"). At 18 the chip was a hairline
     inside a cell that had just been given 140px to live in — the grid stopped looking
     cramped only because the lanes around it grew, while the events themselves stayed thin. */
  --cal-mv-lane: 26px;
  --cal-mv-days: 20px;
  display: flex;
  flex-direction: column;
  /* `flex: 1`, not `min-height: 100%` — see the note on `#cal-grid.is-month` above for why
     the percentage could not be resolved from here. */
  flex: 1 1 auto;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--surface);
  overflow: hidden;
}

.cal-mv-h {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 7px 10px;
  border-bottom: 1px solid var(--border);
}

/* ⚠️ A fixed width, so paging from January to February does not make the arrows jump
   sideways under the pointer — the title is centred in a box that does not resize. */
.cal-mv-title {
  min-width: 200px;
  text-align: center;
  font-size: .82rem;
  font-weight: 800;
  color: var(--text-1);
}

.cal-mv-nav { padding: 4px 8px; }

.cal-mv-wk {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}

.cal-mv-wk span {
  padding: 5px 7px;
  border-right: 1px solid var(--border);
  font-size: .62rem;
  font-weight: 800;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-3);
}

.cal-mv-wk span:last-child { border-right: none; }

.cal-mv-grid { display: flex; flex-direction: column; flex: 1 1 auto; }

/* ⚠️ One grid per week, NOT one grid for the month. A bar is placed with an explicit
   `grid-column: start / end`, so it can span the days it covers — and because the day
   numbers sit in row 1 and every lane below is a bar row, the days still line up across all
   the weeks. One month-wide grid cannot do both: a spanning bar would have to be placed in
   the same rows as the cells it crosses. */
.cal-mv-week {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  /* Row 1 is the day-number strip and row 2 is the first lane of bars. With no `row-gap`
     they were flush, so the first bar read as part of the header — 「甘特图不要紧挨着日期」.
     This is the air BETWEEN lanes; the 26px lane floor and the bar's own height are
     untouched, so the grid gains 6px per row and nothing else. Measured: the first bar's
     top sits 7px below the day-number strip (6px gap + the 1px cell rule). */
  row-gap: 6px;
  flex: 1 1 auto;
  min-height: calc(var(--cal-mv-days) + var(--cal-mv-lane));
  border-bottom: 1px solid var(--border);
}

.cal-mv-week:last-child { border-bottom: none; }

.cal-mv-cell {
  grid-row: 1;
  display: flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
  padding: 1px 5px;
  border-right: 1px solid var(--border);
  font-size: .66rem;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}

/* The last COLUMN of a week, not the last cell: a six-day month leaves a trailing `out`
   cell after the 31st, and `:last-child` would then keep a border on Saturday while Monday
   through Friday lost theirs. */
.cal-mv-cell:nth-child(7n) { border-right: none; }

.cal-mv-cell.wk { background: var(--hover); }

.cal-mv-cell.out { background: var(--bg); }

.cal-mv-cell.today {
  background: var(--accent-soft);
  color: var(--accent-hover);
  font-weight: 800;
}

.cal-mv-num { flex: 0 0 auto; }

.cal-mv-marks {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  margin-left: auto;
  min-width: 0;
}

/* The timeline rides these triangles on the chart's bottom edge, absolutely positioned at a
   day's column. Inside a day cell they are just a row of glyphs on the right — only the
   POSITIONING is reset here. The geometry, the halo that keeps the white "not due yet" state
   visible on a white cell, and the done / overdue colours all stay the same object, so a
   green mark means the same thing in both views. */
.cal-mv .cal-ms-tri { position: static; transform: none; }

.cal-mv .cal-bar {
  /* ⚠️ `.cal-bar` carries gantt geometry a grid item must not have: `position: absolute`,
     and a day-proportional width the timeline writes inline. Both are neutralised here, at a
     specificity the single class cannot outrank. The colour, the border and the ink stay
     inline per kind, exactly as the timeline sets them. */
  position: static;
  width: auto;
  height: auto;
  min-height: calc(var(--cal-mv-lane) - 2px);
  margin: 1px 2px;
  padding: 0 7px;
  border-radius: 5px;
  gap: 5px;
  font-size: .7rem;
  z-index: 1;
  /* ⚠️ `start`, not the default `stretch`. The lanes now grow to fill a tall cell, and a
     stretched chip would become a 70px slab; a compact one sitting at the TOP of its lane
     is what a month grid is supposed to look like — day number, then the events, then air. */
  align-self: start;
}

/* A piece that continues into the next week drops its left corner, so a campaign that wraps
   reads as one bar rather than two chips that happen to touch. The margin goes with it —
   keeping it would leave a 2px notch of background down the seam. */
.cal-mv .cal-bar.cont-l {
  margin-left: 0;
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}

.cal-mv .cal-bar.cont-r {
  margin-right: 0;
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}

/* ⚠️ A retired `milestone` draws as a bare diamond in the timeline, so `.cal-bar.milestone`
   carries `background: none !important`. An author `!important` beats an inline value, so the
   month view's milestone chip CANNOT be styled from the renderer and has to win here — a
   milestone in the grid would otherwise be a title floating on the cell background with no
   chip under it, and no diamond beside it either (the class hides the padding too). */
.cal-mv .cal-bar.milestone {
  background: var(--surface) !important;
  border-color: var(--border) !important;
  color: var(--text-1) !important;
  padding: 0 5px;
  gap: 4px;
  box-shadow: none;
}

.cal-mv .cal-ms-dot {
  width: 12px;
  height: 12px;
  margin-left: 0;
  border-radius: 1.5px;
}

/* A month grid is denser than a gantt row: at narrow widths the type is the first thing to
   go, because a truncated title is still a title and a truncated ICON is a mystery glyph. */
@media (max-width: 900px) {
  .cal-mv .cal-bar { font-size: .62rem; padding: 0 5px; }
  .cal-mv .cal-bar i { display: none; }
  .cal-mv-title { min-width: 150px; font-size: .74rem; }
}

.cal-tip {
  position: fixed;
  z-index: 69;
  width: 320px;
  max-width: 46vw;
  pointer-events: none;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 14px 34px rgba(15, 23, 42, .18);
  padding: 11px 13px;
  opacity: 0;
  transition: opacity .12s ease;
}

.cal-tip-cover {
  width: 100%;
  height: 96px;
  object-fit: cover;
  border-radius: 6px;
  margin-bottom: 8px;
  display: block;
}

.cal-tip.on { opacity: 1; }

.cal-tip-d {
  margin-top: 4px;
  font-size: .7rem;
  font-weight: 700;
  color: var(--accent-hover);
}

/* The event's own 16:9 page, in a reader overlay. */
.cal-modal {
  position: fixed;
  inset: 0;
  z-index: 9000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(15, 23, 42, .55);
  padding: 22px;
}

.cal-modal[hidden] { display: none; }

/* ⚠️ 1500×900 with a 360px panel left the form squeezed into a quarter of the dialog: long
     summaries were cut off and a to-do with an owner and a date had no room (2026-09-30).
     The page still gets the larger share — the panel only takes what the form needs. */
  .cal-modal-bd {
  display: flex;
  flex-direction: column;
  width: min(1800px, 97vw);
  height: min(960px, 94vh);
  background: var(--surface);
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 24px 60px rgba(15, 23, 42, .35);
}

.cal-modal-h {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--border);
}

.cal-modal-h b {
  font-size: .88rem;
  font-weight: 800;
  color: var(--text-1);
}

.cal-modal-h .cal-modal-dates { font-size: .72rem; color: var(--text-3); }

.cal-modal-h .cal-modal-dates { margin-left: auto; }

.cal-modal-tools {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-left: 4px;
}

.cal-cover-thumb {
  width: 40px;
  height: 23px;
  object-fit: cover;
  border-radius: 4px;
  border: 1px solid var(--border);
  flex: 0 0 auto;
}

.cal-modal-body, .ibx-split {
  flex: 1;
  min-height: 0;
  display: flex;
}

.cal-frame-wrap {
  flex: 1;
  min-height: 0;
  background: var(--ink);
}

.cal-frame-wrap iframe {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

/* The event's own fields, beside its page. The 16:9 page stays visible while editing:
   "the deadline is wrong" is a judgement you make while looking at the page. */
.cal-edit {
  flex: 0 0 min(460px, 34vw);
  min-width: 0;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border-left: 1px solid var(--border);
}

.cal-edit[hidden] { display: none; }

.cal-edit-h, .cal-pick-h {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--border);
}

.cal-edit-h b { font-size: .8rem; font-weight: 800; }

.cal-edit-body {
  flex: 1;
  min-height: 0;
  overflow: auto;
  padding: 12px 14px 20px;
  display: flex;
  flex-direction: column;
  gap: 11px;
}

.cal-edit-f {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
  padding: 10px 14px;
  border-top: 1px solid var(--border);
}

.cal-fld {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.cal-fld > label {
  font-size: .68rem;
  font-weight: 700;
  color: var(--text-3);
}

.cal-fld input[type="text"], .cal-fld input[type="date"], .cal-fld input[type="email"], .cal-fld select, .cal-fld textarea {
  width: 100%;
  padding: 6px 8px;
  font-size: .76rem;
  font-family: inherit;
  color: var(--text-1);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
}

.cal-fld textarea { resize: vertical; min-height: 54px; }

.cal-fld input:focus, .cal-fld select:focus, .cal-fld textarea:focus {
  outline: none;
  border-color: var(--accent-fill);
  background: var(--surface);
}

.cal-fld-row { display: flex; gap: 8px; }

.cal-fld-row > .cal-fld { flex: 1 1 0; min-width: 0; }

/* ── the editor panel: one SECTION per question ──────────────────────────────
     It used to be one long column of controls with no grouping, so unrelated fields sat as
     close to each other as related ones, and every text field was a single line (a summary
     that wrapped was simply cut off). Sections give each group a heading and a rule between them. */
  .cal-sec {
  display: flex;
  flex-direction: column;
  gap: 9px;
  padding: 12px 0;
  border-top: 1px solid var(--border);
}

.cal-sec:first-of-type { border-top: 0; padding-top: 2px; }

/* The heading line carries the section name, and — where one sentence saves a mistake — a
     single short note on the right of it, so the form does not need a paragraph under every
     field (the first version's "文字太多，看起来太累"). */
  .cal-sec-h {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: .68rem;
  font-weight: 800;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--text-3);
}

.cal-sec-note {
  margin-left: auto;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  font-size: .68rem;
  color: var(--text-3);
}

/* A pair of one-line fields (the summaries) shares a row; anything that wraps does not. */
  .cal-fld-row-tight > .cal-fld { flex: 1 1 0; }

/* Multiline fields: a summary or a description that wraps must stay readable — an `input`
     cannot wrap and silently hides the rest of the line behind the caret. */
  .cal-fld textarea {
  min-height: 56px;
  line-height: 1.45;
  font-size: .78rem;
  resize: vertical;
}

.cal-attach {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 2px;
}

.cal-attach-row {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 5px 8px;
  font-size: .72rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
}

.cal-attach-row > i { flex: 0 0 auto; color: var(--text-3); }

.cal-attach-row button, .cal-todo-del {
  flex: 0 0 auto;
  border: 0;
  background: none;
  cursor: pointer;
  color: var(--text-3);
  padding: 2px;
  border-radius: 4px;
}

.cal-attach-row button:hover { color: var(--danger); background: var(--danger-soft); }

.cal-attach-add {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.cal-attach-add .btn { font-size: .72rem; }

.cal-attach-empty { font-size: .68rem; color: var(--text-3); }

/* A second line under a field label, in the label's own colour but not bold: it explains
   what the field feeds (a to-do's owner/date put the line on the calendar). */
.cal-fld-note {
  display: block;
  font-weight: 400;
  color: var(--text-3);
}

/* ── the to-do editor ─────────────────────────────────────────────────────────
   One card per line, two controls wide: what has to happen on top, who and when below.
   Same shape as `.cal-attach-row` so the panel keeps one visual language. */
.cal-todos {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 2px;
}

/* Its own class, not `.cal-attach-add`: reusing that one made the attachments assertion
   (".cal-attach-add button") count the to-do button as a fourth attachment source. */
.cal-todo-add {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.cal-todo-add .btn { font-size: .72rem; }

.cal-todo-row {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 7px 8px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
}

/* ⚠️ A to-do row is its own component (`.cal-todos`), and it sits in a `cal-sec`, NOT inside a
   `.cal-fld`. So the panel-wide `.cal-fld input[type="text"|"date"] { width: 100%; }

` rule does
   not reach it — which is exactly why these controls need their own styling here. While they
   relied on that ancestor, the date input fell back to Chrome's intrinsic 115px and `flex`
   to its initial `0 1 auto`, so the owner field — the only one that shrinks — was crushed to
   **18px**: an empty square nobody could type into, which reads as "a to-do has no
   responsible field at all" (reported 2026-09-30, reintroduced by the section refactor). */
.cal-todos .cal-todo-line > input[type="text"], .cal-todos .cal-todo-line > input[type="date"] {
  min-width: 0;
  padding: 6px 8px;
  font-size: .76rem;
  font-family: inherit;
  color: var(--text-1);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
}

/* ⚠️ No `width: 100%` above on purpose: `input[type="date"]` there is (0, 3, 1) and would
   out-rank `.cal-todo-due` (0, 3, 0), handing the whole line back to the date box. Width is
   owned by the two flex rules below, one control each. */
.cal-todos .cal-todo-line > input:focus {
  outline: none;
  border-color: var(--accent-fill);
  background: var(--surface);
}

.cal-todos .cal-todo-line > .cal-todo-text, .cal-todos .cal-todo-line > .cal-todo-who {
  flex: 1 1 auto;
  width: auto;
  min-width: 0;
}

.cal-todo-done {
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  margin: 0;
  cursor: pointer;
  accent-color: var(--accent);
}

.cal-todo-del:hover { color: var(--danger); background: var(--danger-soft); }

.cal-todos .cal-todo-line > .cal-todo-due { flex: 0 0 auto; width: 132px; }

/* One "pick an existing thing" dialog for reports and wiki pages alike. */
.cal-pick {
  position: fixed;
  inset: 0;
  z-index: 9500;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(15, 23, 42, .55);
}

.cal-pick[hidden] { display: none; }

.cal-pick-bd {
  display: flex;
  flex-direction: column;
  width: min(560px, 94vw);
  max-height: min(620px, 88vh);
  background: var(--surface);
  border-radius: 12px;
  box-shadow: 0 24px 60px rgba(15, 23, 42, .3);
  overflow: hidden;
}

.cal-pick-h b {
  flex: 1 1 auto;
  font-size: .8rem;
  font-weight: 800;
}

.cal-pick-search {
  margin: 10px 14px 0;
  padding: 7px 9px;
  font-size: .78rem;
  font-family: inherit;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--bg);
  color: var(--text-1);
}

.cal-pick-search:focus {
  outline: none;
  border-color: var(--accent-fill);
  background: var(--surface);
}

.cal-pick-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  padding: 10px 14px 14px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.cal-pick-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 9px;
  cursor: pointer;
  border: 1px solid transparent;
  border-radius: 7px;
  font-size: .76rem;
}

.cal-pick-row:hover { background: var(--bg); border-color: var(--border); }

.cal-pick-row i { color: var(--text-3); }

.cal-pick-row b { font-weight: 700; }

.cal-pick-row small {
  margin-left: auto;
  color: var(--text-3);
  font-size: .68rem;
}

.cal-pick-empty {
  padding: 24px 8px;
  text-align: center;
  color: var(--text-3);
  font-size: .74rem;
}

@media (max-width: 900px) {
.cal-edit { flex: 1 1 auto; }
}

/* ── the one dialog + toasts (`kldDialog`) ──────────────────────────────────────────────
   Same anatomy as `.rpt-modal` on purpose: one dialog look for the whole app. z-index sits
   above `.rpt-modal` (8100) and `.cal-modal` (9000) so a confirm triggered *from* a modal
   (revoke a share, withdraw a report) is visible instead of hidden behind it. */
.kld-dlg {
  position: fixed;
  inset: 0;
  z-index: 9700;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(15, 23, 42, .45);
}

.kld-dlg[hidden] { display: none; }

.kld-dlg-bd {
  display: flex;
  flex-direction: column;
  width: min(460px, 94vw);
  max-height: 88vh;
  background: var(--surface);
  border-radius: 12px;
  box-shadow: 0 24px 60px rgba(15, 23, 42, .28);
  overflow: hidden;
}

.kld-dlg-h { padding: 13px 18px; border-bottom: 1px solid var(--border); }

.kld-dlg-h b {
  font-size: .84rem;
  font-weight: 800;
  color: var(--text-1);
}

.kld-dlg-bd-body {
  padding: 16px 18px;
  overflow-y: auto;
  font-size: .78rem;
  line-height: 1.6;
  color: var(--text-2);
}

.kld-dlg-text { white-space: pre-line; }

.kld-dlg-text + .kld-dlg-field { margin-top: 12px; }

.kld-dlg-field {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.kld-dlg-field input, .kld-dlg-field textarea {
  padding: 7px 9px;
  font-size: .78rem;
  font-family: inherit;
  color: var(--text-1);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 7px;
  outline: none;
}

.kld-dlg-field input:focus, .kld-dlg-field textarea:focus { border-color: var(--accent-border); background: var(--surface); }

.kld-dlg-field textarea { min-height: 74px; resize: vertical; }

.kld-dlg-note {
  margin-top: 8px;
  font-size: .68rem;
  color: var(--text-3);
}

/* What a delete takes — the confirm box names real tables, so the names have to look
   like identifiers and the row counts like metadata. It scrolls with `.kld-dlg-bd-body`,
   which is already `overflow-y: auto`, so a 30-table dataset does not push the buttons
   off the dialog. No `max-height` of its own: a second scrollbar inside a scrolling
   body is worse than a long body. */
.dc-del-head { margin-bottom: 8px; color: var(--text-1); }

.dc-del-list {
  margin: 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--border);
  border-radius: 9px;
  overflow: hidden;
}

.dc-del-list li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 6px 10px;
  background: var(--surface-2);
  font-size: .74rem;
}

.dc-del-list li + li { border-top: 1px solid var(--border); }

.dc-del-name {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .72rem;
  color: var(--text-1);
  /* Generated table names carry the original file name, so they are long and have no
     spaces to break on — without this one name overflows the 460px dialog. */
  word-break: break-all;
}

.dc-del-rows { flex: none; color: var(--text-3); font-variant-numeric: tabular-nums; }

.dc-del-note { margin-top: 8px; font-size: .7rem; color: var(--text-3); }

.kld-dlg-f {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  padding: 11px 18px;
  border-top: 1px solid var(--border);
}

.kld-dlg-f .btn { font-size: .78rem; }

/* Destructive actions read as destructive: the same button, in red. */
#kld-dlg-ok.danger {
  background: var(--danger);
  border-color: var(--danger);
  color: var(--text-on-ink);
}

#kld-dlg-ok.danger:hover { background: var(--danger); border-color: var(--danger); }

.kld-toasts {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 9800;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
  pointer-events: none;
}

.kld-toast {
  pointer-events: auto;
  max-width: min(420px, 88vw);
  padding: 10px 14px;
  border-radius: 9px;
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: 0 12px 30px rgba(15, 23, 42, .18);
  font-size: .76rem;
  line-height: 1.5;
  color: var(--text-1);
  white-space: pre-line;
  cursor: pointer;
  animation: kld-toast-in .16s ease-out;
}

.kld-toast.err {
  border-color: var(--danger-border);
  color: var(--danger);
  background: var(--danger-soft);
}

.kld-toast.ok {
  border-color: rgba(5, 150, 105, .45);
  color: var(--ok);
  background: var(--ok-soft);
}

@keyframes kld-toast-in {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
.kld-toast { animation: none; }
}

/* A bilingual page renders one section per language; the switch sits in the meta row. */
.kb-langswitch {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--bg);
}

.kb-langswitch button {
  border: 0;
  background: none;
  font: inherit;
  cursor: pointer;
  font-size: .68rem;
  font-weight: 700;
  padding: 2px 9px;
  border-radius: 5px;
  color: var(--text-3);
}

.kb-langswitch button.on {
  background: var(--surface);
  color: var(--accent-hover);
  box-shadow: 0 1px 2px rgba(15, 23, 42, .08);
}

.kb-main, .wr-main {
  flex: 1;
  min-width: 0;
  overflow-y: auto;
}

.kb-doc {
  max-width: 940px;
  margin: 0 auto;
  padding: 24px 34px 96px;
}

.kb-doc-head {
  border-bottom: 1px solid var(--border);
  padding-bottom: 12px;
  margin-bottom: 20px;
}

.kb-doc-title {
  margin: 0 0 7px;
  font-size: 1.45rem;
  font-weight: 800;
  line-height: 1.25;
  color: var(--text-1);
}

.kb-doc-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 7px;
  font-size: .69rem;
  color: var(--text-3);
}

.kb-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  border-radius: 20px;
  background: var(--bg);
  color: var(--text-2);
  font-size: .65rem;
  font-weight: 600;
}

.kb-chip.kb-pub { background: rgba(16, 185, 129, .14); color: var(--ok); }

.kb-chip.kb-shared { background: var(--accent-soft); color: var(--accent-hover); }

.kb-acts {
  display: flex;
  gap: 6px;
  margin-left: auto;
}

.kb-acts .btn {
  height: 27px;
  display: inline-flex;
  align-items: center;
  gap: 5px;
}

.kb-empty {
  padding: 68px 30px;
  text-align: center;
  color: var(--text-3);
}

.kb-empty i {
  font-size: 2rem;
  display: block;
  margin-bottom: 10px;
  opacity: .5;
}

.kb-empty-t {
  font-size: .92rem;
  font-weight: 700;
  color: var(--text-2);
  margin-bottom: 5px;
}

.kb-empty-s { font-size: .74rem; line-height: 1.6; }

/* reading mode — rendered markdown, never the source */
.kb-render {
  font-size: .88rem;
  line-height: 1.75;
  color: var(--text-1);
}

.kb-render > :first-child { margin-top: 0; }

.kb-render h1 {
  font-size: 1.3rem;
  font-weight: 800;
  margin: 26px 0 10px;
}

.kb-render h2 {
  font-size: 1.1rem;
  font-weight: 800;
  margin: 24px 0 9px;
}

.kb-render h3 {
  font-size: .96rem;
  font-weight: 700;
  margin: 20px 0 8px;
}

.kb-render h4, .kb-render h5, .kb-render h6 {
  font-size: .88rem;
  font-weight: 700;
  margin: 16px 0 6px;
}

.kb-render p { margin: 0 0 12px; }

.kb-render ul, .kb-render ol { margin: 0 0 12px; padding-left: 22px; }

.kb-render li { margin: 3px 0; }

.kb-render blockquote {
  margin: 0 0 12px;
  padding: 6px 14px;
  border-left: 3px solid var(--accent-border);
  background: var(--accent-soft);
  color: var(--text-2);
  border-radius: 0 6px 6px 0;
}

.kb-render code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .82em;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 1px 5px;
}

.kb-render pre {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px 14px;
  overflow-x: auto;
  margin: 0 0 14px;
  /* ⚠️ The colour MUST be set here. Tabler styles `pre` as a dark surface with
     `color: var(--tblr-light)` (#f6f8fb);
  overriding only the background left the
     text near-white on light grey — measured contrast ratio 1.00, i.e. code blocks
     rendered as empty boxes. Override a surface's background and you own its text
     colour too. */
  color: var(--text-1);
}

.kb-render pre code {
  background: none;
  border: 0;
  padding: 0;
  font-size: .8rem;
  line-height: 1.6;
  color: inherit;
}

/* function */
/* A ```mermaid block becomes an SVG in place. The library is loaded lazily, and if
   it fails (or the diagram is invalid) the source block stays visible. */
.kb-render .kb-mermaid {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 14px;
  margin: 0 0 14px;
  overflow-x: auto;
  text-align: center;
}

.kb-render .kb-mermaid svg { max-width: 100%; height: auto; }

.kb-render pre.kb-mermaid-failed { border-color: rgba(185, 28, 28, .45); }

.kb-render table {
  border-collapse: collapse;
  width: 100%;
  margin: 0 0 14px;
  font-size: .8rem;
}

.kb-render th, .kb-render td {
  border: 1px solid var(--border);
  padding: 7px 10px;
  text-align: left;
}

.kb-render th { background: var(--bg); font-weight: 700; }

.kb-render hr {
  border: 0;
  border-top: 1px solid var(--border);
  margin: 22px 0;
}

.kb-render a {
  color: var(--accent-hover);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.kb-render img { max-width: 100%; border-radius: 8px; }

/* ── Embedded (iframe) mode ─────────────────────────────────────────────
   The Inbox preview boots this same SPA in an iframe, but the reader asked for
   the DOCUMENT, not the app a second time — so when booted embedded, the shell
   chrome (top nav, sidebar, its resize handle, breadcrumb bar) is not rendered
   at all and the page content fills the frame. */
html.app-embedded .nav, html.app-embedded .sidebar, html.app-embedded .resize-handle-l, html.app-embedded .workspace-bar { display: none !important; }

/* The wiki is the one document that boots INSIDE the shell, so dropping the nav and the
   sidebar still leaves the app on screen a second time: its scope toolbar (My knowledge /
   Public / Shared with me · New page) and its page list are navigation, not the page. An
   embedded reader followed ONE page, so only the document pane survives — and the
   page-management buttons go with them, because the Inbox pane tells the reader to use
   "Open page" to work on the document (a Delete / Publish button under that sentence is a
   trap). Scoped to app-embedded, so the top-level page keeps every one of them. */
html.app-embedded #page-knowledge > .rpt-toolbar, html.app-embedded #page-knowledge .kb-toc, html.app-embedded #page-knowledge #kb-doc-meta .kb-acts { display: none !important; }

/* ── Inbox ───────────────────────────────────────────────────────────────
   Two panes: messages on the left, a read-only document preview on the right.
   A message is ONE English sentence — actor first, the document name bold and
   clickable — with the time at the end. Nothing else. */
.nav-tab-badge {
  display: inline-block;
  min-width: 16px;
  margin-left: 5px;
  padding: 1px 4px;
  border-radius: 999px;
  background: var(--danger);
  color: var(--text-on-ink);
  font-size: 10px;
  font-weight: 700;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.nav-tab-badge[hidden] { display: none; }

.ibx-list {
  flex: 0 0 400px;
  width: 400px;
  overflow-y: auto;
  border-right: 1px solid var(--border);
  padding: 6px 0 40px;
}

.ibx-item {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  padding: 11px 14px;
  border-bottom: 1px solid var(--border);
  border-left: 3px solid transparent;
  cursor: pointer;
}

.ibx-item:hover { background: var(--bg); }

.ibx-item.is-selected { background: var(--accent-soft); border-left-color: var(--accent-fill); }

.ibx-line {
  flex: 1;
  min-width: 0;
  font-size: .84rem;
  line-height: 1.55;
  color: var(--text-1);
}

.ibx-item.is-unread .ibx-line { font-weight: 700; }

.ibx-actor { font-weight: 700; }

.ibx-title-link {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  font-weight: 700;
  color: var(--accent-hover);
  cursor: pointer;
  text-decoration: none;
}

.ibx-item.is-unread .ibx-title-link { color: var(--accent-hover); }

.ibx-title-link:hover { text-decoration: underline; text-underline-offset: 2px; }

.ibx-time {
  flex: 0 0 auto;
  font-size: .68rem;
  color: var(--text-3);
  white-space: nowrap;
  margin-top: 3px;
  font-variant-numeric: tabular-nums;
}

.ibx-dot {
  flex: 0 0 auto;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--danger);
  margin-top: 7px;
}

.ibx-item:not(.is-unread) .ibx-dot { background: transparent; }

.ibx-empty {
  text-align: center;
  padding: 64px 20px;
  color: var(--text-3);
}

.ibx-empty i {
  font-size: 34px;
  display: block;
  margin-bottom: 10px;
  color: var(--border);
}

.ibx-filtered {
  text-align: center;
  padding: 40px 20px;
  color: var(--text-3);
  font-size: .82rem;
}

.ibx-preview {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  background: var(--bg);
}

.ibx-preview-empty {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  color: var(--text-3);
  text-align: center;
  padding: 30px;
}

.ibx-preview-empty i { font-size: 38px; color: var(--border); }

.ibx-preview-hint {
  font-size: .74rem;
  color: var(--text-3);
  max-width: 320px;
}

.ibx-preview-pane {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.ibx-preview-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 12px;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}

.ibx-preview-title {
  flex: 1;
  min-width: 0;
  font-size: .8rem;
  font-weight: 700;
  color: var(--text-1);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Right-click menu on a message row. Same anatomy as the wiki's and the calendar's,
   so one menu reads like the others: the shared `button i` and `hr` rules live up
   front with the rest of the component layer.
   ⚠️ `hidden` needs its own rule ONLY because this one declares `display` — and here
   it does not, so the browser's own `[hidden] { display: none }` is the whole story.
   That default is a default: adding a `display` to the block below without adding the
   `[hidden]` line would make the menu permanently visible on every page.
   z-index 70 is `.kb-ctx`'s number, and it is safe here for one reason: the menu only
   ever opens BELOW the cursor, and the list it belongs to already starts under the
   top bar — so it can never be drawn under the nav (200). Everything that must cover
   it is far higher: the modals are 8100–9000 and `kldDialog` is 9700. */
.ibx-ctx {
  position: fixed;
  z-index: 70;
  min-width: 180px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 5px;
  box-shadow: var(--shadow-3);
}

.ibx-ctx button {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  border: 0;
  background: none;
  font: inherit;
  font-size: .76rem;
  color: var(--text-2);
  text-align: left;
  cursor: pointer;
  padding: 7px 9px;
  border-radius: 6px;
}

.ibx-ctx button:hover { background: var(--accent-soft); color: var(--accent-hover); }

.ibx-ctx button.danger { color: var(--danger); }

.ibx-ctx button.danger:hover { background: var(--danger-soft); }

/* editing mode — a block list; every block is directly editable */
.kb-editor {
  font-size: .88rem;
  line-height: 1.75;
  color: var(--text-1);
}

.kb-bar {
  position: sticky;
  top: 0;
  z-index: 6;
  display: flex;
  flex-wrap: wrap;
  gap: 3px;
  align-items: center;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 5px 6px;
  margin-bottom: 14px;
}

.kb-bar button {
  border: 0;
  background: none;
  font: inherit;
  cursor: pointer;
  min-width: 28px;
  height: 26px;
  border-radius: 5px;
  color: var(--text-2);
  font-size: .74rem;
  font-weight: 700;
  padding: 0 7px;
}

.kb-bar button:hover { background: var(--accent-soft); color: var(--accent-hover); }

.kb-bar button.on { background: var(--accent-soft); color: var(--accent-hover); }

.kb-bar .kb-bar-sep {
  width: 1px;
  height: 18px;
  background: var(--border);
  margin: 0 4px;
}

.kb-block {
  border-radius: 6px;
  padding: 3px 8px;
  margin: 2px 0;
  position: relative;
}

.kb-block:hover { background: var(--accent-soft); }

.kb-block:focus {
  outline: none;
  background: var(--accent-soft);
  box-shadow: 0 0 0 1px rgba(37, 99, 235, .18);
}

.kb-block[data-kind="h1"] { font-size: 1.3rem; font-weight: 800; }

.kb-block[data-kind="h2"] { font-size: 1.1rem; font-weight: 800; }

.kb-block[data-kind="h3"] { font-size: .96rem; font-weight: 700; }

.kb-block[data-kind="code"] {
  background: var(--bg);
  border: 1px solid var(--border);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .8rem;
  white-space: pre-wrap;
}

.kb-block[data-kind="quote"] { border-left: 3px solid var(--accent-border); background: var(--accent-soft); }

.kb-block[data-kind="hr"] {
  border-top: 1px solid var(--border);
  height: 0;
  padding: 0;
  margin: 14px 0;
}

/* A language marker (`<!-- lang:en -->`) shows as a chip in the editor, not as the
   handful of punctuation the author would otherwise have to recognise. */
.kb-block[data-kind="lang"] {
  display: inline-block;
  margin: 14px 0 4px;
  padding: 2px 11px;
  border-radius: 20px;
  background: var(--accent-soft);
  color: var(--accent-hover);
  font-size: .63rem;
  font-weight: 800;
  letter-spacing: .07em;
}

.kb-block[data-kind="lang"]:hover { background: var(--accent-soft); }

.kb-list { margin: 2px 0; padding-left: 24px; }

.kb-list li { border-radius: 6px; padding: 2px 6px; }

.kb-list li:hover { background: var(--accent-soft); }

.kb-list li:focus { outline: none; background: var(--accent-soft); }

.kb-editor table {
  border-collapse: collapse;
  width: 100%;
  margin: 6px 0;
  font-size: .8rem;
}

.kb-editor th, .kb-editor td { border: 1px solid var(--border); padding: 6px 9px; }

.kb-editor th { background: var(--bg); }

.kb-props {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 8px;
  margin-bottom: 12px;
}

.kb-field {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.kb-field label {
  font-size: .64rem;
  font-weight: 800;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-3);
}

.kb-field input, .kb-field select {
  height: 30px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text-1);
  font: inherit;
  font-size: .78rem;
  padding: 0 9px;
}

.kb-status {
  margin-left: auto;
  font-size: .7rem;
  color: var(--text-3);
}

.kb-status.err { color: var(--danger); }

.kb-status.ok { color: var(--ok); }

/* ══ AUTH GATE · USER MENU · ADMIN PAGE ══════════════════════════════════════ */
.nav-user-wrap {
  position: relative;
  display: flex;
  align-items: center;
}

/* ⚠️ This menu is `position: fixed` at BODY level and `placeMenu()` gives it `top` /
   `left` from the avatar's rect. It used to be `position: absolute; right: 0` nested
   inside `.nav` — which cannot work from that spot: `.nav` is z-index 200 and creates
   a stacking context, so the menu could never rise above `.rpt-viewer` (8000) and
   simply vanished behind an open report. Body level is the same pattern `.rpt-modal`,
   `.cal-modal` and `kldDialog` already use, and the markup comment says so.

   z-index is deliberately between the report viewer (8000) and the overlays that are
   *supposed* to cover everything (`.rpt-modal` 8100, `.cal-modal` 9000,
   `.auth-gate` 9000, `kldDialog` 9700).

   `display: flex` is set HERE, not only by the inline `style.display` that
   `setMenu()` writes — the old rule declared `flex-direction: column` with no
   `display` at all, so the column only existed by accident of the inline style. */
.nav-user-menu {
  position: fixed;
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 236px;
  max-width: calc(100vw - 16px);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-3);
  padding: var(--sp-1);
  z-index: 8050;
  animation: au-in .12s ease-out;
}

@keyframes au-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .nav-user-menu { animation: none; }
}

/* ⚠️ NO `display` in the rule itself, and the `[hidden]` override is EXPLICIT. These
   three are toggled with the `hidden` attribute, and the UA's `[hidden] { display:none }`
   is only a default: any `display` declared here outranks it, which would leave an
   always-on header shape or a hairline in a menu that has no account section. */
.nav-user-menu .au-head[hidden],
.nav-user-menu .au-sep[hidden],
.nav-user-menu #au-acct[hidden] { display: none; }

.nav-user-menu .au-sep {
  height: 1px;
  margin: var(--sp-1) 0;
  background: var(--border);
}

.nav-user-menu .au-head {
  padding: var(--sp-2) var(--sp-2) var(--sp-3);
}

/* An account-less deployment (AUTH_ENABLED=false) must not leave a header-shaped hole
   where the name would be. */
.nav-user-menu .au-head:empty { display: none; }

.nav-user-menu .au-name {
  font-size: .84rem;
  font-weight: 700;
  color: var(--text-1);
  line-height: 1.3;
}

.nav-user-menu .au-mail {
  font-size: .72rem;
  color: var(--text-3);
  margin-top: 2px;
  word-break: break-all;
}

/* The account rows live in a wrapper so they can be hidden as a unit. `display: flex`
   makes its buttons stack and stretch exactly like the preference rows above, and the
   explicit `[hidden]` rule above is what lets the whole group disappear. */
.nav-user-menu #au-acct {
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.nav-user-menu .au-item {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  width: 100%;
  padding: 7px var(--sp-2);
  border: 0;
  border-radius: var(--r-md);
  background: none;
  font-family: var(--font);
  font-size: .82rem;
  color: var(--text-2);
  text-align: left;
  cursor: pointer;
  transition: background .12s ease, color .12s ease;
}

.nav-user-menu .au-item:hover,
.nav-user-menu .au-item:focus-visible {
  background: var(--hover);
  color: var(--text-1);
  outline: none;
}

/* Keyboard focus still has to be visible — a menu reachable only by mouse is half a
   menu, and `:hover` alone would leave a keyboard user with no position at all. */
.nav-user-menu .au-item:focus-visible {
  box-shadow: inset 0 0 0 1px var(--accent-border);
}

.nav-user-menu .au-item i {
  font-size: 1rem;
  color: var(--text-3);
  flex-shrink: 0;
}

.nav-user-menu .au-label { flex: 1; min-width: 0; }

/* The chip answers "what am I in right now" — the icon and tooltip next to it answer
   "what happens if I click". Mixing the two in one place is what made the old nav
   buttons unreadable at a glance. */
.nav-user-menu .au-chip {
  flex-shrink: 0;
  padding: 1px 8px;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  background: var(--surface-3);
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .01em;
  color: var(--text-3);
}

.nav-user-menu .au-item.au-danger { color: var(--danger); }

.nav-user-menu .au-item.au-danger:hover,
.nav-user-menu .au-item.au-danger:focus-visible { background: var(--danger-soft); color: var(--danger); }

.nav-user-menu .au-item.au-danger:hover i,
.nav-user-menu .au-item.au-danger:focus-visible i { color: var(--danger); }

/* ── The office-avatar picker ──────────────────────────────────────────────
   ⚠️ This is the only part of the user menu that is a GRID rather than a row, and
   it is the reason the six columns are `1fr` and not a fixed width. The menu is
   `min-width: 236px` and was already — six animals across at a fixed 34px plus
   gaps needs 243px, which is more than the menu has, so a fixed width would have
   made the row scroll and the sixth animal would be something a reader had to go
   looking for. `1fr` gives each cell ~33px and all six are always on screen.

   The thumbnails are real `<svg>` from office.js's `beastHead()`, not icons —
   an icon set has no tiger, and a bespoke glyph would be a second drawing of the
   same animal. */
.nav-user-menu .au-avatar { padding: 6px 8px 8px; }
.nav-user-menu .au-avatar-hd {
  font-size: .68rem; color: var(--text-3); margin-bottom: 5px;
}
.nav-user-menu .au-avatar-grid {
  display: grid; grid-template-columns: repeat(6, 1fr); gap: 3px;
}
.nav-user-menu .au-avatar-cloths { grid-template-columns: repeat(5, 1fr); margin-top: 4px; }
.nav-user-menu .au-avatar-cell,
.nav-user-menu .au-cloth-cell {
  display: block; padding: 0; cursor: pointer;
  border: 1px solid transparent;
  transition: background .15s ease, border-color .15s ease;
}
/* ⚠️ The two cell types share a hover/focus treatment and NOTHING else. They used
   to share `background` and `border-radius` too, and each then re-declared both —
   which `test_stale_css_rules.py` reports as conflicting duplicates, correctly:
   the second declaration silently wins and the first is a lie. The rounded bar
   and the shirt fill are what make a swatch a swatch, so they belong to the cloth
   cell alone. */
.nav-user-menu .au-avatar-cell {
  height: 34px; background: transparent; border-radius: var(--r-sm);
}
.nav-user-menu .au-avatar-cell:hover,
.nav-user-menu .au-cloth-cell:hover { background: var(--hover); }
.nav-user-menu .au-avatar-cell:focus-visible,
.nav-user-menu .au-cloth-cell:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.nav-user-menu .au-avatar-cell.is-on {
  border-color: var(--accent-border); background: var(--accent-soft);
}
/* The swatch is a rounded bar in the shirt colour, not a filled circle: five
   circles in a row is a row of dots, and a dot has no shirt in it. */
.nav-user-menu .au-cloth-cell {
  height: 20px; border-radius: var(--r-pill); background: var(--c);
}
.nav-user-menu .au-cloth-cell.is-on { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
/* A pending write dims the whole block rather than spinning anything: the picker
   has eleven targets and a spinner on the one that was pressed is the only
   honest way to say "this one, not the others". */
.nav-user-menu .au-avatar.is-saving { opacity: .6; pointer-events: none; }

body.auth-gate-open > .nav, body.auth-gate-open > .shell { display: none !important; }

.auth-gate {
  position: fixed;
  inset: 0;
  z-index: 9000;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 28px 20px;
  background: var(--bg-gradient);
  overflow-y: auto;
}

.auth-gate-card {
  width: 100%;
  max-width: 436px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 16px;
  box-shadow: 0 24px 60px rgba(15,23,42,.16), 0 2px 8px rgba(15,23,42,.06);
  padding: 28px 30px 24px;
}

.auth-brand-logo {
  /* 同 nav-brand-icon：横版 lockup，透明底，按原比例显示 */
  width: auto;
  height: 42px;
  aspect-ratio: 1219 / 257;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.auth-brand-name {
  font-size: 1.04rem;
  font-weight: 800;
  letter-spacing: .06em;
  color: var(--text-1);
}

.auth-brand-sub {
  font-size: .73rem;
  color: var(--text-3);
  margin-top: 2px;
}

.auth-tabs {
  display: flex;
  gap: 4px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 9px;
  padding: 3px;
  margin-bottom: 18px;
}

.auth-tab {
  flex: 1;
  height: 32px;
  border: 0;
  background: none;
  border-radius: 6px;
  font-family: var(--font);
  font-size: .82rem;
  font-weight: 600;
  color: var(--text-3);
  cursor: pointer;
}

.auth-tab.on {
  background: var(--surface);
  color: var(--accent);
  box-shadow: 0 1px 3px rgba(15,23,42,.12);
}

.auth-field { margin-bottom: 14px; }

.auth-label {
  display: block;
  font-size: .76rem;
  font-weight: 600;
  color: var(--text-2);
  margin-bottom: 6px;
}

.auth-input {
  width: 100%;
  height: 40px;
  padding: 0 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  color: var(--text-1);
  font-family: var(--font);
  font-size: .88rem;
  outline: none;
  transition: background .15s, border-color .15s, box-shadow .15s;
  box-sizing: border-box;
}

.auth-input:focus {
  background: var(--surface);
  border-color: var(--accent-fill);
  box-shadow: 0 0 0 3px rgba(37,99,235,.12);
}

.auth-submit {
  width: 100%;
  height: 40px;
  justify-content: center;
  font-size: .88rem;
  font-weight: 600;
}

.auth-msg {
  margin-top: 14px;
  font-size: .79rem;
  line-height: 1.55;
  border-radius: 8px;
  padding: 10px 12px;
  display: none;
}

.auth-msg.err {
  display: block;
  background: var(--danger-soft);
  border: 1px solid var(--danger-soft);
  color: var(--danger);
}

.auth-msg.ok {
  display: block;
  background: rgba(22,163,74,.07);
  border: 1px solid rgba(22,163,74,.22);
  color: var(--ok);
}

.auth-hint {
  margin-top: 12px;
  font-size: .74rem;
  color: var(--text-3);
  line-height: 1.55;
}

.auth-foot {
  margin-top: 16px;
  font-size: .72rem;
  color: var(--text-3);
  text-align: center;
  line-height: 1.55;
}

/* 备案号（《非经营性互联网信息服务备案管理办法》第十三条：主页底部中央位置）。
   刻意与 .auth-foot 同构、排在它下面 —— 法规要的是"底部"，两个块叠在卡片底部
   才读得出那个位置；`.auth-foot` 本身会被 JS 整体覆盖，所以是独立元素。 */
.auth-beian {
  margin-top: 10px;
  font-size: .72rem;
  color: var(--text-3);
  text-align: center;
  line-height: 1.55;
}

.auth-beian a {
  color: var(--text-3);
  text-decoration: none;
}

.auth-beian a:hover {
  color: var(--text-2);
  text-decoration: underline;
}

/* The card stack, inside the page's own `.rpt-body` scroller.
   ⚠️ No `overflow` and no bottom padding of its own: the body scrolls, and a second
   scroller inside it is the "which bar do I drag" problem. The old `40px` of bottom
   padding existed to clear a nested scroller's end, and it left a band of empty page
   under the last card instead. */
.adm-scroll {
  display: flex;
  flex-direction: column;
  gap: 16px;
  max-width: 1180px;
  margin: 0 auto;
}

/* The roster search, as one control: an icon, a field and its button read as three
   loose objects sitting in the card header otherwise, and the header is a flex row
   that wraps on a narrow window — which used to drop the button onto its own line,
   halfway down the header. */
.adm-search {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
}

.adm-search-icon {
  color: var(--text-3);
  font-size: .86rem;
  line-height: 1;
  flex: 0 0 auto;
  pointer-events: none;
}

/* The field itself drops its own border: the icon lives INSIDE it, so a second
   outline around the text would draw a box within a box. */
.adm-input-search {
  width: 230px;
  padding-left: 26px;
  background: var(--bg);
}

.adm-input-search:focus {
  border-color: var(--accent-fill);
}

.adm-btn-go { flex: 0 0 auto; }

.adm-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
}

.adm-hd {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
  flex-wrap: wrap;
}

.adm-hd-title {
  font-size: .9rem;
  font-weight: 700;
  color: var(--text-1);
  display: flex;
  align-items: center;
  gap: 6px;
}

.adm-hd-title i { color: var(--accent); font-size: 1.05rem; }

.adm-count { font-size: .73rem; color: var(--text-3); }

.adm-spacer { flex: 1; }

.adm-input, .adm-select {
  height: 30px;
  padding: 0 10px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--surface);
  color: var(--text-1);
  font-family: var(--font);
  font-size: .8rem;
  outline: none;
}

.adm-input:focus, .adm-select:focus { border-color: var(--accent-fill); box-shadow: 0 0 0 3px rgba(37,99,235,.12); }

.adm-body { max-height: 440px; overflow: auto; }

.adm-table {
  width: 100%;
  border-collapse: collapse;
  font-size: .78rem;
}

.adm-table th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface);
  text-align: left;
  font-weight: 700;
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: .04em;
  font-size: .67rem;
  padding: 9px 12px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

.adm-table td {
  padding: 9px 12px;
  border-bottom: 1px solid var(--border);
  color: var(--text-2);
  white-space: nowrap;
}

.adm-table tr:last-child td { border-bottom: 0; }

.adm-table tbody tr:hover td { background: var(--bg); }

.adm-empty {
  padding: 26px;
  text-align: center;
  color: var(--text-3);
  font-size: .82rem;
}

.adm-badge {
  display: inline-flex;
  align-items: center;
  height: 20px;
  padding: 0 8px;
  border-radius: 999px;
  font-size: .67rem;
  font-weight: 700;
  border: 1px solid transparent;
}

.adm-badge.ok {
  background: rgba(22,163,74,.1);
  color: var(--ok);
  border-color: rgba(22,163,74,.22);
}

.adm-badge.off {
  background: var(--danger-soft);
  color: var(--danger);
  border-color: var(--danger-soft);
}

/* A closed account. Deliberately NOT `.adm-badge.off`: pending deletion is a state
   with a deadline and a way back, not a failure — and the two are told apart by more
   than colour. */
.adm-badge.warn {
  background: var(--warn-soft);
  color: var(--warn);
  border-color: var(--warn-border);
}

/* ── closing an account: the impact dialog ─────────────────────────────────────
   The dialog an operator reads before deleting a person. Its job is to answer "what
   will this break", so the rows that name OTHER people's pages are given the visual
   weight and the ones that merely count this account's own rows are not. */
.adm-impact-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 6px 0;
  border-bottom: 1px solid var(--border);
}
.adm-impact-row:last-of-type { border-bottom: 0; }

.adm-impact-label {
  color: var(--text-2);
  font-size: .82rem;
  min-width: 92px;
}

.adm-impact-val {
  color: var(--text-1);
  font-weight: 700;
  font-size: .82rem;
}

.adm-impact-items {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 6px;
  min-width: 0;
}
.adm-impact-items code,
.adm-impact-list code {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 1px 5px;
  font-size: .72rem;
  color: var(--text-2);
}
.adm-impact-more {
  color: var(--text-3);
  font-size: .72rem;
  align-self: center;
}

/* The block that matters: whose pages stop answering. */
.adm-impact-warn {
  margin-top: 12px;
  padding: 8px 10px;
  border-radius: 8px;
  background: var(--warn-soft);
  border: 1px solid var(--warn-border);
  color: var(--warn);
  font-size: .8rem;
  font-weight: 600;
}

.adm-impact-list {
  margin: 6px 0 0;
  padding-left: 18px;
  color: var(--text-2);
  font-size: .8rem;
}
.adm-impact-list li { margin: 2px 0; }
.adm-impact-owner {
  color: var(--text-3);
  font-size: .74rem;
}

.adm-impact-note {
  margin-top: 10px;
  color: var(--text-3);
  font-size: .78rem;
  line-height: 1.5;
}

/* The impact markup is injected as HTML into the shared dialog's note slot, so it has
   to look right inside a 12px-radius `.rpt-modal`-shaped box. */
.kld-dlg-note .adm-impact-row:first-child { padding-top: 0; }

.adm-badge.admin {
  background: var(--accent-soft);
  color: var(--accent-hover);
  border-color: var(--accent-border);
}

.adm-badge.muted {
  background: var(--bg);
  color: var(--text-3);
  border-color: var(--border);
}

.adm-acts { display: flex; gap: 6px; }

/* The company a roster belongs to, next to the count. ⚠️ It used to be appended to the
   table as a bare `· Northwind` line, which reads as a list bullet and as a rendering
   leftover — and the most useful fact on the card was the least legible thing on it. */
.adm-scope {
  font-size: .74rem;
  color: var(--text-3);
  max-width: 320px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.adm-scope:empty { display: none; }

.adm-btn {
  height: 26px;
  padding: 0 9px;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-2);
  font-family: var(--font);
  font-size: .72rem;
  font-weight: 600;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.adm-btn:hover { background: var(--bg); }

.adm-btn.danger { color: var(--danger); border-color: var(--danger-border); }

.adm-btn.danger:hover { background: var(--danger-soft); }

/* The selected policy preset. `primary` rather than `aria-pressed` alone: a pressed
   state nobody can see is a state nobody knows they chose, and these three buttons set
   BOTH sharing and publication at once. */
.adm-btn.primary {
  background: var(--accent-fill);
  border-color: var(--accent-fill);
  color: var(--text-on-accent);
}

.adm-btn.primary:hover { background: var(--accent-hover); color: var(--text-on-accent); }

/* A <select> in the Actions cell, sized to the roster rather than to the column. The
   old mail form carried the class without a rule, so it rendered at the browser's
   default width and pushed every other button in the row out of alignment. */
.adm-select {
  height: 26px;
  padding: 0 6px;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-2);
  font-family: var(--font);
  font-size: .72rem;
  font-weight: 600;
  cursor: pointer;
}

.adm-select:disabled { opacity: .5; cursor: default; }

.adm-role { min-width: 84px; }

.adm-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .88rem;
  font-weight: 700;
  letter-spacing: .16em;
  color: var(--text-1);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 2px 8px;
}

.adm-code.none {
  color: var(--text-3);
  letter-spacing: normal;
  font-weight: 400;
  background: none;
  border: 0;
  padding: 0;
}

/* A full agent code is 53 characters (`klado_agent_` + 43), and this cell used to hold the
   12-character prefix — so it is the one value in the admin table that has to wrap. The
   letter-spacing of .adm-code is for 6-digit verification codes and reads as noise at this
   length, hence the override. (2026-09-27) */
.adm-code-full {
  font-size: .72rem;
  letter-spacing: .02em;
  word-break: break-all;
  user-select: all;
}

.adm-note {
  padding: 10px 16px;
  font-size: .74rem;
  color: var(--text-3);
  border-top: 1px solid var(--border);
  background: var(--bg);
  line-height: 1.55;
}

/* Invite strip + the expandable per-account detail (2026-09-26). The detail row is a
   real <tr> so it stays inside the table's sticky-header scroll container. */
.adm-invite {
  padding: 12px 16px 14px;
  border-top: 1px solid var(--border);
  background: var(--surface);
}

.adm-invite-lbl {
  font-size: .74rem;
  color: var(--text-3);
  line-height: 1.6;
}

.adm-invite-row {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-top: 9px;
}

.adm-invite-row .adm-input { flex: 0 0 280px; }

.adm-invite-status {
  min-height: 18px;
  font-size: .78rem;
  color: var(--text-3);
  line-height: 1.6;
  margin-top: 8px;
}

/* The table's feedback line sits directly above the invite strip's top border, so it needs
   its own bottom padding — otherwise the last line of text reads as glued to that divider
   (2026-09-26, from the user's screenshot). While empty it keeps the plain 8px gap, so the
   table's bottom border and the strip's divider do not collide. */
#adm-code-status { padding: 0 16px 16px; }

#adm-code-status:empty { padding: 0; }

/* ⚠️ The ONE place `.adm-invite-status` is used somewhere other than on its own line, and
   the offset has to come off. Its `margin-top: 8px` is the gap under a stacked block (the
   invite row, the table's feedback line). The Enterprise-settings save strip instead puts
   the badge INSIDE an `align-items: center` flex row next to the Save button — and a
   margin still applies before alignment, so the badge was pushed 8px down and its centre
   ended up 4px below the button's. Measured, not eyeballed: 26px button vs 20.6px badge,
   centres 4.0px apart. Small enough to pass a screenshot and large enough that a row of
   buttons and a badge beside them read as two separate things.

   Scoped by id on purpose: the class belongs to the stacked layout, and two of its three
   users are stacked. Fixing the class instead would have re-broken those. */
#adm-org-status { margin-top: 0; }

/* Icon-only copy button: same 26px height as every other .adm-btn (a size modifier in an
   Actions cell is what made one of three buttons look wrong before), just tighter sides. */
.adm-copy { padding: 0 7px; }

/* ⚠️ Every button in the Actions column is a plain `.adm-btn`. A smaller "toggle"
   variant used to sit on Details, which made one of three buttons 22px tall next to
   26px ones — measured, not guessed (2026-09-26). Don't reintroduce a size modifier
   for one button in that cell. The one control in that cell that is NOT a button is
   the role <select>, which `.adm-select` sizes to the same 26px for the same reason. */

/* ─── Dashboard ───────────────────────────────────────────────────
   The card grid is `.rpt-card`, already shared with Workspace — a dashboard is
   read the same way, so it gets the same card and only adds its own pieces:
   the placement dialog and the landing-page container. */
/* ⚠️ `.nav-tab-dash` 没有 id（是 syncPromotedNav() 动态建的），所以它进的是上面那条
   inline-flex 规则的**类**选择器那一段，不是 #id 那一段。少了它，`.workspace-mark` 这个
   display:block 的图就会自己占一行、把仪表盘标题挤到下一行。 */
.nav-tab-dash { max-width: 190px; }
.nav-tab-dash span {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.dsh-place-box { max-width: 560px; }
.dsh-place-row {
  display: flex; gap: 10px; align-items: flex-start;
  padding: 10px 12px; margin-bottom: 8px; cursor: pointer;
  border: 1px solid var(--border); border-radius: var(--r-lg);
  background: var(--surface-2);
}
.dsh-place-row:hover { border-color: var(--border-strong); }
.dsh-place-row input { margin-top: 3px; }
.dsh-place-row span { display: flex; flex-direction: column; gap: 2px; }
.dsh-place-row strong { font-size: .86rem; color: var(--text-1); }
.dsh-place-row em { font-style: normal; font-size: .76rem; color: var(--text-2); }
.dsh-place-field {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; margin: 10px 0; font-size: .82rem; color: var(--text-2);
}
.dsh-place-field .rpt-input { width: 88px; }
.dsh-place-datasets { margin-top: 12px; }
.dsh-place-datasets > span {
  display: block; font-size: .78rem; color: var(--text-2); margin-bottom: 6px;
}
.dsh-place-datasets-list { display: flex; flex-wrap: wrap; gap: 6px; }
.dsh-place-datasets-list em { color: var(--text-3); font-size: .8rem; }
.dsh-dataset-chip {
  display: inline-block; padding: 2px 8px; border-radius: 999px;
  background: var(--accent-soft); color: var(--accent);
  font-size: .76rem; font-family: var(--font-mono, ui-monospace, monospace);
}

/* Landing-page containers. The page inside is a live /d/{slug} document, so the
   frame is sized by aspect ratio rather than by content — an iframe has no
   intrinsic height and would otherwise collapse to its 150px default. */
.home-card {
  border: 1px solid var(--border); border-radius: var(--r-lg);
  background: var(--surface); margin: 0 0 14px; overflow: hidden;
  box-shadow: var(--shadow-1);
}
.home-card-hd {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 12px; border-bottom: 1px solid var(--border);
  background: var(--surface-2);
}
.home-card-hd h2 {
  margin: 0; font-size: .92rem; font-weight: 600; color: var(--text-1);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.home-card-spacer { flex: 1; }
.home-card-toggle {
  border: 0; background: none; cursor: pointer; color: var(--text-2);
  padding: 2px 4px; border-radius: 6px; line-height: 1;
}
.home-card-toggle:hover { background: var(--surface); color: var(--text-1); }
.home-card[data-collapsed="1"] .home-card-toggle i { transform: rotate(-90deg); }
.home-card-toggle i { transition: transform .15s ease; display: inline-block; }
/* Collapsed really means collapsed: the frame is removed from the layout, not just
   hidden, so a live page inside stops costing anything (its timers keep running
   either way — toggleHomeCard() clears its src). */
.home-card[data-collapsed="1"] .home-card-body { display: none; }
.home-card-body { padding: 0; }
.home-card-frame {
  display: block; width: 100%; height: 460px; border: 0;
  background: var(--surface);
}

/* ─── Member modules (Settings → Organization members) ─────────────────────
   The trigger is a button on the member row; the control itself is a checkbox list in
   the shared `kldDialog`. It used to be a nine-column matrix pinned under the roster, so
   changing one person's modules meant finding their row in a different table than the
   one you were reading. There is no third state to encode here, and no glyph: a checkbox
   is the one control whose two states are both legible without a legend. */

/* The row button. `.mod-btn-set` is the only signal that anything was ever decided for
   this person — deliberately a border, not a coloured dot, because a dot next to an
   icon reads as "this module is on", which is a claim about ONE module and this is a
   claim about four. */
.mod-btn {
  position: relative;
  /* ⚠️ 26px, the same height as `.adm-btn`, not a round 30. The Actions cell holds a
     role select and two buttons; one control a few pixels taller than its neighbours
     makes the whole row read as misaligned, and it is the kind of thing a screenshot
     review catches and nobody can name. */
  width: 30px; height: 26px;
  padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: .92rem;
}
.mod-btn-set { border-color: var(--accent); color: var(--accent); }

/* The list inside the dialog. A `<label>` per row, so clicking the icon or the name
   toggles the box — a 14px target that only responds when you hit it exactly is not a
   checkbox, it is a test of aim. */
/* The wrapper. A different class from the row on purpose: sharing one gave the list a
   border, a background and a hover of its own — a box drawn around the whole thing — and
   made `.mod-pick` match N+1 elements for N modules. */
.mod-pick-list { display: flex; flex-direction: column; gap: 7px; }

.mod-pick {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 11px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  cursor: pointer;
  user-select: none;
}
.mod-pick:hover { border-color: var(--border-strong); background: var(--surface-2); }

/* ⚠️ The real `<input>`, not a drawn box. It is the only control in the product that
   survives being read by a screen reader, printed, and selected with the keyboard, and
   re-implementing it with a div throws all three away for a border-radius. It is
   enlarged rather than hidden: `appearance:none` with a zero-size input is invisible to
   a pointer hit-test on some engines, which is how a row ends up looking enabled and
   doing nothing. */
.mod-pick-box {
  appearance: none; -webkit-appearance: none;
  width: 17px; height: 17px; margin: 0; flex: 0 0 auto;
  border: 1.5px solid var(--border-strong);
  border-radius: 4px;
  background: var(--bg);
  cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
}
.mod-pick-box:checked {
  background: var(--accent-fill);
  border-color: var(--accent-fill);
}
/* The tick, drawn with borders rather than a glyph: this app ships no icon font, and a
   `content: "✓"` on an unchecked box would still be laid out (and would read as a mark
   in forced-colours mode). */
.mod-pick-box:checked::after {
  content: "";
  width: 4px; height: 8px;
  margin-top: -2px;
  border: solid var(--text-on-accent);
  border-width: 0 2px 2px 0;
  transform: rotate(43deg);
}
.mod-pick-box:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.mod-pick-ico {
  width: 26px; height: 26px; flex: 0 0 auto;
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--text-2);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: .92rem;
}
/* The icon follows the box, so the row reads as one thing rather than as a checkbox
   with a picture next to it. `--accent` is the TEXT token here on purpose: the icon
   sits on a light surface in both themes, and `--accent-fill` would be a dark-on-dark
   block in the dark one. */
.mod-pick:has(.mod-pick-box:checked) .mod-pick-ico {
  background: var(--accent-soft);
  color: var(--accent);
}
.mod-pick-name { font-size: .84rem; color: var(--text-1); }
.mod-pick-empty { font-size: .84rem; color: var(--text-2); line-height: 1.6; }

/* ─── Dataset sharing (Data Center) ──────────────────────────────────
   The shared-in list sits under the owned grid and is visually distinct: it is
   somebody else's data, and the difference should be visible before you click. */
.dc-shared-wrap { margin-top: 22px; }
.dc-shared-hd {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 10px 2px 12px; color: var(--text-1);
  font-size: .84rem; font-weight: 600;
  border-bottom: 1px solid var(--border); margin-bottom: 14px;
}
.dc-shared-hd i { color: var(--accent); font-size: 1rem; }
.dc-shared-note {
  flex-basis: 100%; font-weight: 400; font-size: .76rem; color: var(--text-2);
}
/* Same list idiom as the owned datasets above — a shared dataset is a row, not a
   smaller card. The two sections are told apart by their HEADING and by what the
   rows do, not by a second visual language. */
.dc-shared-grid {
  display: block;
  border-top: 1px solid var(--border);
}
.dc-shared-note-below { display: block; margin-top: 10px; }
.dc-dataset-card--shared .dc-dataset-name { color: var(--text-2); }

.dc-share-target {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: .82rem; color: var(--text-1);
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--r-md); padding: 8px 10px; margin-bottom: 10px;
  word-break: break-all;
}
.dc-share-note { font-size: .78rem; color: var(--text-2); margin: 0 0 12px; line-height: 1.6; }
.dc-share-list {
  display: flex; flex-direction: column; gap: 6px;
  max-height: 190px; overflow-y: auto; margin-bottom: 10px;
}
.dc-share-item {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 8px; border: 1px solid var(--border);
  border-radius: var(--r-md); background: var(--surface-2);
  font-size: .8rem; color: var(--text-1);
}
.dc-share-item > span { flex: 1; overflow: hidden; text-overflow: ellipsis; }
.dc-share-empty { font-size: .78rem; color: var(--text-3); padding: 8px 2px; }
.dc-share-row { display: flex; gap: 8px; }
.dc-share-row .dc-input { flex: 1; }
.dc-share-hint { font-size: .74rem; color: var(--text-3); margin-top: 8px; }
.dc-share-msg { font-size: .78rem; color: var(--text-2); margin-right: auto; }
.dc-share-msg.err { color: var(--danger); }

/* Data Center restores separate resource views with Workspace-style scopes. */
#dc-section-upload[hidden], #dc-section-datasets[hidden], #dc-tools-files[hidden], #dc-shared-files[hidden] { display: none; }
.dc-scope { width: auto; }
#dc-section-datasets { padding: 20px; }
#dc-section-datasets .dc-section-header { display: flex; gap: 12px; align-items: center; margin-bottom: 16px; }

/* ════════════════════════════════════════════════════════════════════════════
   Agent 办公室 — the landing page (2026-10-05)

   Twelve isometric desks. An agent that connects takes one; the card floating
   over its head says what it is doing right now, and the animation on the card
   and at the desk is that same state, so the picture is readable without
   reading a word of it.

   ⚠️ WHY THE LABELS ARE HTML AND NOT SVG `<text>`. The room is ~950 user units
   wide, so a 10-unit font inside the viewBox lands at ~6 real pixels once the
   SVG is scaled to a column — the geometry shrinks, the type must not. Every
   label is therefore a positioned HTML element, and `office.js` keeps the two
   coordinate systems in step by placing them in % of the viewBox (the stage
   carries the same aspect-ratio, so the mapping survives a resize).

   ⚠️ The colours are NOT here. Every material face and every state colour is a
   `--off-*` token in theme.css, because a literal in this file is a surface
   that stays light when the theme flips.
   ═══════════════════════════════════════════════════════════════════════════ */

.off-wrap { display: flex; flex-direction: column; gap: 14px; }

/* ── The four counters ───────────────────────────────────────────────────
   ⚠️ These used to sit at the right end of an `.off-head` row that also held an
   `<h1>`, a sentence and the demo note, so they wrapped onto a second line as
   soon as any of that got long. All of it is gone: the page is named in the
   breadcrumb above. The counters are now the only thing in this row, and
   `nowrap` is explicit so a later edit that puts a sibling back here cannot
   silently reintroduce the wrap. */
.off-stats { display: flex; flex-wrap: nowrap; gap: 10px; }
.off-stat {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md);
  padding: 10px 18px; min-width: 100px; box-shadow: var(--shadow-1);
}
.off-stat b { display: block; font-size: 1.2rem; font-weight: 600; line-height: 1.2; color: var(--text-1); }
.off-stat span { font-size: .7rem; color: var(--text-3); white-space: nowrap; }
.off-stat.is-accent b { color: var(--accent); }

/* ── Layout: the room, and the roster beside it ────────────────────────── */
.off-grid { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 16px; align-items: start; }
@media (max-width: 1180px) { .off-grid { grid-template-columns: 1fr; } }

/* ── Stage ─────────────────────────────────────────────────────────────── */
.off-stage {
  position: relative; width: 100%; overflow: hidden;
  background: radial-gradient(ellipse at 50% 42%, var(--off-stage-light), var(--off-stage-bg)); border-bottom: 1px solid var(--line-soft, var(--border));
  touch-action: none; cursor: grab;
}
.off-stage.dragging { cursor: grabbing; }
.off-stage > svg { display: block; width: 100%; height: 100%; }
.off-labels { position: absolute; inset: 0; pointer-events: none; }

/* Nothing here is a text label on the drawing, so the stage never has to be
   measured to place one — but the ROOM still needs an explicit ratio, and it
   is the same one office.js writes into the viewBox. */
/* ⚠️ `max-height` is set by office.js (`capStage`) as the space actually left
   under the stage on THIS screen. It is what keeps the whole room on one screen:
   the ratio alone makes the stage tall enough on a laptop to push the bottom row
   of desks below the fold, and the cap makes the drawing letterbox into the
   space instead of overflowing it. The default is deliberately absent — until
   office.js runs, an uncapped stage is the old behaviour, not a broken one. */
.off-stage[data-ratio="wide"] { aspect-ratio: 1000 / 660; max-height: var(--off-stage-cap, none); }
.off-stage[data-ratio="tall"] { aspect-ratio: 1000 / 860; max-height: var(--off-stage-cap, none); }

.off-hint {
  position: absolute; left: 10px; bottom: 9px; display: flex; gap: 10px;
  font-size: .68rem; color: var(--text-3); background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--r-pill);
  padding: 3px 10px; opacity: .9;
}

/* ── The floating card over an agent's head ────────────────────────────── */
/* ⚠️ The card is scaled to the room, and the `transform-origin` is bottom-centre
   so the scale grows the card UPWARD from the stalk, not sideways from the head.
   The scale itself is set by office.js (`fitCards`) from the measured
   pixels-per-viewBox-unit: the cards are HTML at a fixed pixel size and the room
   is viewBox units, and those two only balanced at one room size. The default of
   1 is what the first frame uses, before any card exists to measure. */
.off-label {
  position: absolute; transform: translate(-50%, -100%) scale(var(--off-card-scale, 1));
  transform-origin: 50% 100%;
  background: var(--surface); border: 1px solid var(--border);
  border-left: 3px solid var(--c, var(--off-st-idle));
  border-radius: var(--r-md); padding: 4px 8px 5px;
  /* Narrower than the column pitch (109px) on purpose — see the PITCH note in
     office.js. A card wider than the pitch is a card that has to be lifted
     clear of its neighbour, and a lifted card collides with the one above it. */
  box-shadow: var(--shadow-2); min-width: 126px; max-width: 126px; text-align: left;
  pointer-events: auto; cursor: default;
  transition: opacity .3s ease, border-color .3s ease;
  --stalk: 14px;
}
.off-label::after {            /* the little stalk down to the head */
  content: ""; position: absolute; left: 50%; top: 100%;
  width: 1px; height: var(--stalk); background: var(--c, var(--off-st-idle)); opacity: .45;
}
.off-label .n { display: flex; align-items: center; gap: 5px; font-size: .76rem; font-weight: 600; color: var(--text-1); }
.off-label .n em { font-style: normal; width: 6px; height: 6px; border-radius: 50%; background: var(--c); flex: none; }
.off-label .n i { font-style: normal; font-size: .8rem; color: var(--c); line-height: 1; flex: none; }
.off-label .who { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.off-label .t { font-size: .68rem; color: var(--text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; }
/* The seat number and the clock sit on the name's row, right-aligned. */
.off-label .m { font-size: .64rem; color: var(--text-3); font-family: var(--font-mono, ui-monospace, monospace); flex: none; }
.off-label .bar { height: 2px; border-radius: 2px; background: var(--surface-3); margin-top: 4px; overflow: hidden; }
.off-label .bar i { display: block; height: 100%; background: var(--c); border-radius: 2px; width: 0; transition: width .5s ease; }
.off-label.is-sel { border-color: var(--accent-border); box-shadow: var(--shadow-3); }

/* An empty desk gets no card at all. A card that says "nobody" twelve times is
   noise; the roster below already lists the free seats, and the pulsing ring on
   the chair is the in-scene cue. */
.off-label.is-off { opacity: 0; pointer-events: none; }

/* ── Roster ────────────────────────────────────────────────────────────── */
.off-roster-hd { display: flex; align-items: baseline; justify-content: space-between; padding: 11px 13px; border-bottom: 1px solid var(--border); }
.off-roster-hd h2 { margin: 0; font-size: .84rem; font-weight: 600; color: var(--text-1); }
.off-roster-hd span { font-size: .72rem; color: var(--text-3); }
.off-rows { padding: 5px; max-height: 260px; overflow-y: auto; }
.off-row {
  display: grid; grid-template-columns: 40px minmax(0, 1fr) auto; gap: 9px; align-items: center;
  width: 100%; text-align: left; background: transparent; border: 0; border-radius: var(--r-md);
  padding: 6px 8px; cursor: pointer; font-family: var(--font); color: var(--text-1);
  transition: background .15s ease;
}
.off-row:hover { background: var(--hover); }
.off-row.is-sel { background: var(--accent-soft); box-shadow: inset 0 0 0 1px var(--accent-border); }
.off-row .no {
  width: 22px; height: 22px; border-radius: var(--r-sm); background: var(--surface-3);
  color: var(--text-3); font-size: .68rem; font-weight: 600;
  display: grid; place-items: center;
}
.off-row.on .no { background: var(--surface-2); color: var(--text-1); }
.off-row .who { min-width: 0; }
.off-row .n { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; font-size: .78rem; font-weight: 600; }
.off-row .n em { font-style: normal; width: 6px; height: 6px; border-radius: 50%; background: var(--c); flex: none; }
.off-row .n small { font-weight: 400; font-size: .68rem; color: var(--text-3); }
.off-row .t { font-size: .7rem; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.off-row .r { display: flex; align-items: center; gap: 6px; font-size: .68rem; color: var(--text-3); }
.off-row .r i { font-style: normal; font-size: .92rem; color: var(--c); }
/* ⚠️ The per-row add / remove button is gone with the manual "seat an agent"
   action it performed. A row is now a readout, not a control: the holder's name,
   the presence state and when the agent was last there. Nothing on it is
   clickable except selecting the desk in the room. */
.off-row.free .n { color: var(--text-2); font-weight: 500; }
.off-note { padding: 9px 13px; border-top: 1px solid var(--border); font-size: .7rem; color: var(--text-3); line-height: 1.55; }

/* ── Legend ────────────────────────────────────────────────────────────── */
.off-legend { display: flex; flex-wrap: wrap; gap: 6px; padding: 10px 13px; border-top: 1px solid var(--border); }
.off-lg {
  display: inline-flex; align-items: center; gap: 5px; font-size: .7rem; color: var(--text-2);
  border: 1px solid var(--border); border-radius: var(--r-pill); padding: 2px 9px 2px 7px;
  background: var(--surface-2);
}
.off-lg i { width: 6px; height: 6px; border-radius: 50%; background: var(--c); flex: none; }
.off-lg b { font-weight: 600; color: var(--text-1); }

/* ══════════════════════════════════════════════════════════════════════════
   State motion. Every keyframe is `off-` prefixed and every rule hangs off a
   state class on ONE wrapper, so a state change is a single class swap and the
   previous state's animation cannot survive it.

   The rule the whole set follows: motion must be legible as that state with
   the sound off and from across the room. A 2px bob reads as "idle"; a
   travelling dash reads as "something is coming back from the queue"; a
   single red pulse that fires twice reads as "this needs a person".
   ══════════════════════════════════════════════════════════════════════════ */
.off-agent .inner { animation: off-breathe 4.4s ease-in-out infinite; }
@keyframes off-breathe { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-.8px); } }

/* Walking in: a bob plus a leg swing, and the two together are what make a
   figure sliding along a floor line read as a person arriving rather than a
   sprite being repositioned. */
.off-agent.is-walking .inner { animation: off-walkbob .46s ease-in-out infinite alternate; }
@keyframes off-walkbob { from { transform: translateY(0); } to { transform: translateY(-2.4px); } }
.off-agent.is-walking .leg-a { animation: off-leg-a .46s ease-in-out infinite alternate; }
.off-agent.is-walking .leg-b { animation: off-leg-b .46s ease-in-out infinite alternate; }
@keyframes off-leg-a { from { transform: rotate(11deg); } to { transform: rotate(-11deg); } }
@keyframes off-leg-b { from { transform: rotate(-11deg); } to { transform: rotate(11deg); } }
.off-agent .leg-a, .off-agent .leg-b { transform-origin: 50% 0%; }

/* coding — the keyboard, then the screen. Both are the same fact twice: the
   hands are moving AND the output is scrolling.
   ⚠️ These hang off `.off-desk`, not `.off-agent`: the keyboard and the monitor
   are furniture and live in the desk's group. A rule written against the agent
   would match nothing, and the room would sit perfectly still while nine
   agents worked — which is the failure this file exists to prevent. */
/* ⚠️ These rules are KEPT but currently INERT, and that is deliberate rather than
   leftover. Each one is a real animation for a work state — hands on a keyboard, a
   magnifier sweeping, a tick drawing itself — and they are the thing a future
   self-reported work state would switch back on, one selector at a time.

   They are switched off because no state class named here is ever applied: the
   room only has `never` / `idle` / `active`, and an animation that is attached to
   `.st-coding` when nothing can ever be `coding` is dead code pretending to be a
   feature. The naming convention they encode is also load-bearing and worth
   keeping: the keyboard and the monitor hang off `.off-desk`, not `.off-agent`,
   because they are furniture and live in the desk's group. A rule written against
   the agent matched nothing, and the room sat perfectly still while nine agents
   supposedly worked. Whoever re-enables these has to keep that distinction. */
.off-desk.st-coding .key { animation: off-key 1.05s steps(1) infinite; animation-delay: calc(var(--i, 0) * 70ms); }
@keyframes off-key { 0%, 44% { opacity: .2; } 50%, 94% { opacity: 1; } 100% { opacity: .2; } }
.off-desk.st-coding .cline { animation: off-cline 2.6s linear infinite; animation-delay: calc(var(--i, 0) * .22s); }
@keyframes off-cline { 0%, 100% { transform: translateY(0); opacity: .3; } 45% { opacity: 1; } 80% { transform: translateY(-7px); opacity: .25; } }
.off-desk.st-coding .caret { animation: off-blink 1.05s steps(1) infinite; }
@keyframes off-blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }

/* searching — a magnifier that sweeps. The floor wire that used to sit here is
   gone for good: it meant "an answer is coming back from the task queue", and
   nothing on this page retrieves anything. */
.off-agent.st-search .mag { animation: off-sweep 1.9s ease-in-out infinite; }
@keyframes off-sweep { 0%, 100% { transform: translateX(-2.5px); } 50% { transform: translateX(2.5px); } }

/* writing — lines growing left to right, then a caret. */
.off-agent.st-writing .wline { animation: off-grow 1.7s ease-in-out infinite; animation-delay: calc(var(--i, 0) * .16s); }
@keyframes off-grow { 0%, 100% { transform: scaleX(.25); opacity: .3; } 40% { transform: scaleX(1); opacity: .95; } }
.off-agent.st-writing .caret { animation: off-blink .9s steps(1) infinite; }

/* waiting — the halo, the sand timer, and the "?". */
.off-agent.st-waiting .halo { animation: off-halo 1.8s ease-out infinite; }
@keyframes off-halo { 0% { opacity: .5; transform: scale(.62); } 70%, 100% { opacity: 0; transform: scale(1.5); } }
.off-agent.st-waiting .qmark { animation: off-bob 1.5s ease-in-out infinite; }
.off-agent.st-waiting .sand { animation: off-flip 2.6s ease-in-out infinite; }
@keyframes off-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-2px); } }
@keyframes off-flip { 0%, 100% { transform: rotate(0deg); } 50% { transform: rotate(180deg); } }

/* done — the tick draws itself once and holds, and a +1 leaves. */
.off-agent.st-done .tick { stroke-dasharray: 22; stroke-dashoffset: 22; animation: off-draw .55s ease-out forwards; }
@keyframes off-draw { to { stroke-dashoffset: 0; } }
.off-agent.st-done .plus1 { animation: off-rise 2.1s ease-out infinite; }
@keyframes off-rise { 0% { transform: translateY(0); opacity: 0; } 18% { opacity: 1; } 100% { transform: translateY(-20px); opacity: 0; } }
.off-agent.st-done .glowp { animation: off-glow 2.4s ease-out infinite; }
@keyframes off-glow { 0% { opacity: .55; transform: scale(.7); } 100% { opacity: 0; transform: scale(2.1); } }

/* error — two quick pulses, not one slow one, and a shake on the mark. */
.off-agent.st-error .ring { animation: off-alarm 1.5s ease-out infinite; }
@keyframes off-alarm { 0% { opacity: .85; transform: scale(.6); } 60%, 100% { opacity: 0; transform: scale(1.7); } }
.off-agent.st-error .bang { animation: off-shake 1.5s ease-in-out infinite; }
@keyframes off-shake { 0%, 88%, 100% { transform: rotate(0deg); } 91% { transform: rotate(-11deg); } 94% { transform: rotate(9deg); } 97% { transform: rotate(-4deg); } }

/* The transform-origin every scale-from-centre animation above needs. Without
   it SVG scales about the element's own origin in user space, which is not the
   middle of the circle, and the "pulse" visibly slides off. */
.off-agent .halo, .off-agent .glowp, .off-agent .ring,
.off-agent .qmark, .off-agent .bang, .off-agent .plus1 { transform-box: fill-box; transform-origin: 50% 50%; }

/* idle — steam off a cup. ⚠️ Inert for the same reason as the rest: `st-idle` means
   "this account's agent was here and is not now", and a person sitting at a desk
   with a steaming cup says nothing about whether they are. Re-enable it when
   `idle` stops meaning absence. */
.off-agent.st-idle .steam { stroke-dasharray: 5 3.4; animation: off-steam 3.4s linear infinite; }
@keyframes off-steam { to { stroke-dashoffset: -17; } }

/* ── active: the ONE animation the room is allowed to run ───────────────────
   `active` is the only state backed by a fact — this account's agent credential
   was used in the last few minutes — so it is the only one that may move. A slow
   breathing pulse on the desk's glow says "occupied" without claiming a task, and
   it is gated behind `@media (prefers-reduced-motion)` with everything else. */
.off-desk.st-active .glowp { animation: off-presence-glow 3.6s ease-in-out infinite; }
@keyframes off-presence-glow { 0%, 100% { opacity: .28; } 50% { opacity: .6; } }


/* An empty chair, asking to be taken. */
.off-emptyring { animation: off-spin 9s linear infinite; }
@keyframes off-spin { to { stroke-dashoffset: -64; } }

/* Ambience: the light band crossing the floor, the server LEDs, the plants. */
.off-sweep { animation: off-sweepmove 16s linear infinite; }
@keyframes off-sweepmove { 0% { transform: translateX(-62%); } 55%, 100% { transform: translateX(62%); } }
.off-led { animation: off-blink-led 1.6s ease-in-out infinite; animation-delay: calc(var(--i, 0) * .32s); }
@keyframes off-blink-led { 0%, 100% { opacity: .25; } 50% { opacity: 1; } }
.off-plant { transform-box: fill-box; transform-origin: 50% 100%; animation: off-sway 6.5s ease-in-out infinite; }
@keyframes off-sway { 0%, 100% { transform: rotate(-1.6deg); } 50% { transform: rotate(1.6deg); } }

@media (prefers-reduced-motion: reduce) {
  .off-agent *, .off-desk *, .off-sweep, .off-led, .off-plant, .off-emptyring {
    animation: none !important;
  }
}

/* Narrow: the cards come off entirely, and this is a geometry decision rather
   than a simplification. At 400px the whole ~1000-unit viewBox is squeezed into
   the width, so a column pitch of 109px lands at ~40px on screen while a card is
   still ~100px: every card would sit on top of the next one and the room would
   read as a smear. The roster is directly below, full width, and says the same
   thing without the collision — so on a phone the roster IS the list, and the
   room is the picture. */
@media (max-width: 720px) {
  .off-label { display: none; }
  .off-stats { width: 100%; }
  .off-stat { flex: 1; min-width: 0; }
}

/* The office's own toolbar. `.off-toolbar` deliberately reuses `.btn` / `.btn-ghost`
   rather than restating them — a second definition of a button is how two buttons
   stop agreeing. The 1×/2×/4× speed switch that used to sit here is gone with the
   simulation it sped up: those buttons controlled a clock that no longer exists,
   and leaving a control that does nothing is worse than not having it. */
.off-toolbar { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
/* ⚠️ The load-failure line. It is in the DOM from the start and EMPTY on purpose:
   a permanent "loading" that never resolves is indistinguishable from a room that
   really is empty, and an empty room is a real, correct state on a fresh deploy. */
.off-toolbar-note { font-size: .74rem; color: var(--text-3); }
.off-toolbar-note:empty { display: none; }
/* ⚠️ "More desks" at the ceiling. It stays in the layout and stays legible — it
   is the control the reader was promised, and a button that vanishes once the
   room is big enough is a button that has to be hunted for every time. Only its
   ACTION goes away, and the label has already changed to say why. */
#off-more.is-max { opacity: .55; cursor: default; }
#off-more[disabled] { pointer-events: none; }

/* ⚠️ Said out loud rather than hidden. When the seat list is longer than the room
   can hold, the room is drawn from a capped list and the reader is told so here —
   quietly drawing twelve desks for fourteen accounts would read as "those two
   people do not exist". */
.off-trunc { padding: 0 13px 10px; font-size: .72rem; color: var(--warn); }
.off-trunc:empty { display: none; }

/* The legend's footnote: what "在用" actually means numerically. The number is
   rendered from the value the SERVER sent, so the caption cannot drift away from
   the rule that produced the count. */
.off-lg-note { flex-basis: 100%; font-size: .68rem; color: var(--text-3); line-height: 1.5; }

/* One table, as a row in the list inside a dataset folder. `border-top` on every
   row but the first rather than `border-bottom` on the list: with a flex column the
   last row's border would sit right on top of the list's own bottom rule and read
   as a double line. */
.dc-group-table {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 16px;
  flex-wrap: wrap;
}

.dc-group-table + .dc-group-table {
  border-top: 1px solid var(--border);
}

.dc-group-table-name {
  flex: 1;
  min-width: 180px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-1);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dc-group-table-stats {
  display: flex;
  gap: 10px;
  font-size: 12px;
  color: var(--text-3);
  white-space: nowrap;
}

/* The count is its own element on purpose — see `dcTableStats()` in index.html.
   A translation walker that keeps one side of a pair deletes the other, and when
   the number is inside that side the number goes with it. */
/* The gap lives here, not as a leading space in the markup: the translation
   walker trims the side it keeps, so " 行 / Rows" becomes "Rows" flush against
   the number. */
.dc-stat b {
  color: var(--text-1);
  font-weight: 600;
  margin-right: 4px;
}

.dc-group-table-actions {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin-left: auto;
}

.dc-group-empty {
  padding: 20px 16px;
  font-size: 13px;
  color: var(--text-3);
}

/* ── The three container dialogs ───────────────────────────────────────────
   `.dc-modal-overlay` / `.dc-modal` / `.dc-modal-head` / `.dc-modal-foot` already
   exist for the update dialog; only the fields and the pickable rows below are
   new, so the dialogs share one look with the rest of Data Center. */
.dc-field {
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin-bottom: 12px;
}

.dc-field-lbl {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-3);
}

.dc-field input,
.dc-field select {
  width: 100%;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 14px;
  background: var(--surface);
  color: var(--text-1);
}

.dc-at-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 14px;
}

.dc-at-sec {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: .5px;
  margin: 16px 0 8px;
}

.dc-at-hint {
  font-size: 12px;
  line-height: 1.55;
  color: var(--text-3);
  margin: 8px 0;
}

.dc-at-slug {
  font-weight: 400;
  color: var(--text-3);
  font-size: 13px;
  /* ⚠️ The span sits inline right after the title text, so without this the slug
     renders glued to it ("…from a database<slug>"). Only this dialog titles a
     modal this way, which is why it went unnoticed. */
  margin-left: 8px;
}

/* ⚠️ No `display` in the rule below that these elements toggle with `hidden`.
   A `[hidden]` element must not carry an author `display`, or it never hides.
   The row itself is a flex container, so the hide/unhide path is `[hidden]`. */
.dc-at-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  margin-bottom: 6px;
  cursor: pointer;
  font-size: 13px;
  color: var(--text-1);
}

.dc-at-row:hover {
  background: var(--surface-2);
}

.dc-at-rows {
  margin-left: auto;
  font-size: 12px;
  color: var(--text-3);
  white-space: nowrap;
}

.dc-at-go {
  font-size: 12px;
  color: var(--accent);
  white-space: nowrap;
}

.dc-at-modes {
  display: flex;
  gap: 8px;
}

.dc-at-modes label {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 1;
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
  font-size: 13px;
  color: var(--text-1);
}

/* ⚠️ Deliberately BELOW `.dc-at-modes label`, and anchored on the WHOLE rule above
   rather than a prefix of it. Two mistakes fit in here:
   · source order. Both selectors are one class plus one element, so specificity
     ties and the later one wins. Written above it, this was silently overridden
     and the two radio cards stayed stretched across the whole dialog.
   · the anchor. A first attempt matched only up to `flex: 1;` and inserted the
     rule INSIDE the one it was meant to follow — the browser swallowed
     `.dc-ie-modes label` as a declaration, the CSSOM never had it, and the rest of
     `.dc-at-modes label`'s own declarations (padding, border, cursor) were left
     stranded after it. Nothing errored; `test_stale_css_rules.py` cannot see it
     either, because it looks for one selector declared twice and this was two. */
.dc-ie-modes label { flex: 0 0 auto; min-width: 150px; }

/* ── Import from a database: one screen, two columns ─────────────────────────
   ⚠️ 2026-10-06 用户原话：「所有的信息在一屏里面显示，不希望滑动。信息尽量紧凑点。」
   The dialog grew from `max-width:680px` to two columns, and every list below carries
   its OWN `max-height`: a long one scrolls INSIDE its card instead of pushing the
   footer off the dialog. `verify_dc_modal_layout_ui.py` asserts the body itself does
   not scroll at 1440x900 or 1280x800 — a dialog that only fits because its lists are
   clipped is the bug this replaces. */
.dc-modal-wide { width: min(94vw, 1080px); }

.dc-ie-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 0 20px;
  align-items: start;
}
.dc-ie-col { min-width: 0; }
.dc-ie-col + .dc-ie-col {
  border-left: 1px solid var(--surface-3);
  padding-left: 20px;
}

/* The shared section label carries a 16px top margin for a stacked form, which is
   too much air once the groups sit side by side. */
.dc-ie-grid .dc-at-sec { margin: 10px 0 6px; }
.dc-ie-col > .dc-at-sec:first-child { margin-top: 0; }

/* Section label with an action on the same line. The label is a block in the
   shared `.dc-at-sec` rule, and a button parked on its own row underneath cost a
   whole line of a dialog that has to fit without scrolling. */
.dc-ie-secline { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.dc-ie-secline .dc-at-sec { margin-bottom: 6px; }

.dc-ie-list { max-height: 96px; overflow: auto; margin-bottom: 8px; }
/* The schedule list gets more room than the default cap, not because it is exempt
   from one — a 150px cap still bounds it. At the shared 96px a two-row list was cut
   through the middle of row two, which reads as a clipped card rather than as "there
   is more below". 150px shows two whole rows and keeps the guarantee. */
#dc-ie-jobs { max-height: 150px; }
.dc-ie-list:empty { display: none; }

.dc-ie-fields { gap: 0 12px; }
.dc-ie-fields .dc-field { margin-bottom: 7px; gap: 3px; }
.dc-ie-fields .dc-field input { padding: 6px 9px; font-size: 13px; }

.dc-ie-hint { margin: 2px 0 6px; font-size: 11px; line-height: 1.45; }

.dc-ie-btnrow { display: flex; gap: 8px; align-items: center; }
.dc-btn-sm { padding: 5px 12px; font-size: 12px; }

/* The schedule switch is a REAL checkbox, drawn: `appearance: none` keeps it a form
   control (focus, keyboard, and `.checked` in JS) while the knob is a ::before, so
   the on/off state never has to be mirrored in a second variable. */
.dc-ie-switchrow { display: inline-flex; align-items: center; gap: 7px; cursor: pointer; }
.dc-ie-switchrow input {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  width: 30px;
  height: 17px;
  margin: 0;
  flex-shrink: 0;
  border-radius: 999px;
  background: var(--border);
  cursor: pointer;
  transition: background .15s;
}
.dc-ie-switchrow input::before {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  /* `--text-on-accent`, not a literal: the knob sits on the accent FILL, and the
     fill/ink pair is what stays readable in both themes. */
  background: var(--text-on-accent);
  transition: transform .15s;
}
.dc-ie-switchrow input:checked { background: var(--accent-fill); }
.dc-ie-switchrow input:checked::before { transform: translateX(13px); }
.dc-ie-switchrow input:disabled { opacity: .45; cursor: not-allowed; }
.dc-ie-switchrow.is-off { cursor: not-allowed; }
.dc-ie-switchlbl { font-size: 12px; color: var(--text-1); }

/* Interval pills. Five radio buttons drawn as one segmented control — a native
   <select> was what the screenshot complained about, and a bilingual pair like
   "每 15 分钟 / every 15 min" is far too wide for five of them, so the faces are
   the language-neutral `15m / 1h / 6h / 1d / 1w` and the full pair rides on `title`.
   The input is not `display:none`: it stays focusable so the group is keyboard
   operable, and the focus ring is drawn on the visible span. */
.dc-ie-pills { display: inline-flex; gap: 4px; }
.dc-ie-schedbar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; }
.dc-ie-pill { position: relative; cursor: pointer; }
.dc-ie-pill input {
  position: absolute;
  inset: 0;
  opacity: 0;
  margin: 0;
  cursor: pointer;
}
.dc-ie-pill span {
  display: inline-block;
  padding: 4px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 12px;
  color: var(--text-2);
  background: var(--surface);
  user-select: none;
}
.dc-ie-pill:hover span { border-color: var(--accent); }
.dc-ie-pill input:checked + span {
  border-color: var(--accent);
  color: var(--accent);
  font-weight: 600;
  background: var(--surface-2);
}
.dc-ie-pill input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 1px; }
.dc-ie-pill input:disabled + span { opacity: .45; cursor: not-allowed; }

/* A schedule row. A dot for the last outcome beats a word here: the row is 30px
   tall and the words were what made it 60. */
.dc-job-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  margin-bottom: 5px;
  font-size: 12px;
  color: var(--text-1);
}
.dc-job-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--border); flex-shrink: 0; }
.dc-job-dot.is-ok { background: var(--ok); }
.dc-job-dot.is-bad { background: var(--danger); }
.dc-job-dot.is-off { background: var(--text-3); }
.dc-job-name { font-weight: 600; }
.dc-job-sub { font-size: 11px; color: var(--text-3); overflow-wrap: anywhere; margin-top: 1px; }
.dc-job-when { font-size: 11px; color: var(--text-3); white-space: nowrap; }
.dc-job-actions { display: flex; align-items: center; gap: 10px; flex-shrink: 0; margin-left: auto; }

/* The "schedule an existing copy" form. `#dc-ie-sched-form` is toggled with the
   `hidden` attribute, so this rule must not carry a `display` — and the explicit
   `[hidden]` line below is the belt to that braces, per the project rule. */
.dc-ie-form {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 12px 8px;
  background: var(--surface-2);
  margin-top: 6px;
}
.dc-ie-form[hidden] { display: none; }
.dc-ie-chips { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 7px; }
.dc-ie-chips:empty { display: none; }
.dc-ie-chip {
  font-size: 11px;
  color: var(--text-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 1px 8px;
  background: var(--surface);
  white-space: nowrap;
}
.dc-ie-chips .dc-ie-chip { cursor: pointer; }
.dc-ie-chips .dc-ie-chip:hover { border-color: var(--accent); color: var(--accent); }
.dc-ie-formfoot { display: flex; align-items: center; gap: 8px; }
.dc-ie-formfoot .dc-ie-chip { margin-right: auto; }

/* Narrow windows get one column. The two-column split is the whole point of the
   redesign, but a 700px-wide window cannot carry it, and the lists keep their own
   caps so the fallback still fits. */
@media (max-width: 900px) {
  .dc-ie-grid { grid-template-columns: minmax(0, 1fr); }
  .dc-ie-col + .dc-ie-col { border-left: none; padding-left: 0; }
}

.dc-at-modal-body {
  max-height: 60vh;
  overflow-y: auto;
}

/* Office activity: explicitly reported work, separate from presence. */
.off-detail { border-top: 1px solid var(--border); padding: 18px 16px; max-height: 480px; overflow-y: auto; overflow-wrap: anywhere; }
.off-detail h3 { font-size: .9rem; margin: 0 0 12px; color: var(--text-1); }
.off-profile { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
.off-profile > svg { width: 70px; height: 80px; flex: none; border-radius: 16px; background: var(--surface-2); }
.off-profile h3 { margin-bottom: 5px; }
.off-profile p { font-size: .72rem; margin: 0; color: var(--text-3); }
.off-row .off-portrait { width: 40px; height: 44px; background: var(--surface-2); border: 0; }
.off-portrait > svg { width: 36px; height: 42px; }
.off-work-status { color: var(--c); font-size: .72rem; font-weight: 600; }
.off-task-title { margin: 8px 0; font-size: .9rem; line-height: 1.5; }
.off-work-summary { font-size: .78rem; color: var(--text-2); line-height: 1.6; white-space: pre-wrap; }
.off-history-title { font-size: .72rem; margin: 22px 0 12px; color: var(--text-3); }
.off-event { border-left: 2px solid var(--c); padding: 0 0 2px 12px; margin: 0 0 16px; }
.off-event-time { display: block; font-size: .65rem; color: var(--text-3); margin-bottom: 4px; }
.off-event strong { font-size: .76rem; font-weight: 500; color: var(--text-1); }
.off-event p { font-size: .72rem; color: var(--text-2); margin: 4px 0 0; white-space: pre-wrap; }
.off-empty-copy { color: var(--text-3); font-size: .75rem; line-height: 1.7; margin: 8px 0; }
.off-connect { padding: 12px 16px; border-top: 1px solid var(--border); font-size: .7rem; color: var(--text-3); }
.off-connect summary { cursor: pointer; color: var(--text-2); }
.off-connect p { margin: 10px 0; line-height: 1.6; }
.off-connect pre { white-space: pre-wrap; font-size: .65rem; padding: 8px; background: var(--surface-2); }
.off-desk.has-work .cline { animation: off-cline 2.6s linear infinite; animation-delay: calc(var(--i, 0) * .22s); }
.off-label:focus-visible, .off-row:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
@media (max-width: 720px) {
  .off-stat { padding: 8px; }
  .off-stat span { font-size: .62rem; white-space: normal; }
  .off-toolbar { gap: 4px; }
  .off-toolbar .btn { font-size: .68rem; padding: 5px 7px; }
  .off-detail { max-height: none; }
}

.off-toolbar .btn { color: var(--text-2); background: var(--surface); border-color: var(--border); }

/* An away character remains solid against furniture; only its color is muted. */
.off-agent.st-idle .off-person { filter: saturate(.4); }

/* ════════════════════════════════════════════════════════════════════════════
   WELCOME — the product landing page (`frontend/out/welcome.html`, served at
   `/welcome`). A standalone document, so it does NOT get the app shell: there is
   no `.nav`, no `.rail` and no `.shell` here, and the top-bar rule that holds
   the nav on every app page does not apply to it. It is reached before anyone
   has an account, which is exactly why it must stand on its own.

   ⚠️ `overflow: hidden` on `html, body` (theme.css) is what makes the app a
   fixed shell with inner scroll containers. Inherited here it would clip this
   page to one viewport with no way to scroll past the hero — so the landing
   document opts back out, and `html.wl-root` is what makes the override win on
   specificity. **A second `wl-root` page inherits the same trap.**
   ════════════════════════════════════════════════════════════════════════════ */
html.wl-root, html.wl-root body { height: auto; overflow: visible; }
html.wl-root body { background: var(--bg); }

.wl-wrap { width: 100%; max-width: 1180px; margin: 0 auto; padding: 0 24px; }

/* ── Header ────────────────────────────────────────────────────────────────
   Sticky, and its own frame: the app's `.nav` is 52px with a fixed position
   over a shell, neither of which means anything here. */
.wl-head {
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}
.wl-head-in { display: flex; align-items: center; gap: 14px; height: 60px; }
.wl-brand { display: flex; align-items: center; gap: 10px; text-decoration: none; color: var(--text-1); }
/* ⚠️ `logo.png` already contains the wordmark — rendering "Klado" next to it says
   the name twice, which is how the first draft of this header read. The wrapper
   is in the `.nav-brand-icon` / `.auth-brand-logo` selector lists, so this page
   gets the same light/dark pair for free instead of a second copy of the rule. */
.wl-brand-ico { flex: none; height: 26px; }
.wl-foot-ico { flex: none; height: 20px; }
.wl-head-spacer { flex: 1; }
.wl-head-acts { display: flex; align-items: center; gap: 8px; }
/* The language chip follows the app's own control: the label states the language
   you are IN, the tooltip states the one a click would switch to. `i18n.js`
   writes both (`syncToggle`), so the markup carries no text of its own. */
.wl-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 32px;
  padding: 0 10px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--surface);
  color: var(--text-2);
  font-family: var(--font);
  font-size: .78rem;
  font-weight: 500;
  cursor: pointer;
  transition: all .15s;
}
.wl-toggle:hover { border-color: var(--border-strong); color: var(--text-1); }
.wl-toggle i { font-size: 1rem; color: var(--text-3); }

/* ── Hero ────────────────────────────────────────────────────────────────── */
.wl-hero { position: relative; overflow: hidden; background: var(--bg-gradient); border-bottom: 1px solid var(--border); }
/* A blurred ellipse in `--accent-soft`, not a gradient: a gradient would need a
   transparent stop, which is a colour literal, and this repository keeps every
   colour in `theme.css`. `--accent-soft` is already translucent, and `blur()`
   gives the falloff a `radial-gradient` would have needed. */
.wl-hero-glow {
  position: absolute;
  top: -220px;
  left: 50%;
  width: 900px;
  height: 480px;
  margin-left: -450px;
  background: var(--accent-soft);
  filter: blur(90px);
  border-radius: 50%;
  pointer-events: none;
}
.wl-hero-in { position: relative; padding: 84px 0 76px; text-align: center; }
.wl-badge {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 13px;
  border: 1px solid var(--accent-border);
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: .74rem;
  font-weight: 600;
}
.wl-badge i { font-size: .92rem; }
.wl-h1 {
  margin: 22px auto 0;
  max-width: 17em;
  font-size: 3rem;
  line-height: 1.16;
  font-weight: 800;
  letter-spacing: -.6px;
  color: var(--text-1);
}
.wl-sub { margin: 20px auto 0; max-width: 40em; font-size: 1.02rem; line-height: 1.78; color: var(--text-2); }
.wl-cta { display: flex; align-items: center; justify-content: center; gap: 12px; margin-top: 34px; flex-wrap: wrap; }
.wl-stats { display: flex; align-items: center; justify-content: center; gap: 14px; margin-top: 46px; flex-wrap: wrap; }
.wl-stat {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 9px 16px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  box-shadow: var(--shadow-1);
  color: var(--text-2);
  font-size: .78rem;
}
.wl-stat i { color: var(--accent); font-size: 1rem; }

/* ── Buttons ───────────────────────────────────────────────────────────────
   `.wl-btn` rather than the app's `.btn`: that one is 30px tall and sized for a
   toolbar, and a hero call to action has to read at 46px. Same radius (5px),
   same tokens, same hover behaviour. */
.wl-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 46px;
  padding: 0 24px;
  border: 1px solid transparent;
  border-radius: 5px;
  font-family: var(--font);
  font-size: .92rem;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  transition: all .15s;
}
.wl-btn i { font-size: 1.05rem; }
/* ⚠️ The FILL is `--accent-fill` and the text is `--text-on-ink`. Writing the
   fill as `var(--accent)` drops white-on-blue to 2.5:1 in the dark theme,
   because `--accent` is the *text/icon* token and brightens there. */
.wl-btn-primary { background: var(--accent-fill); border-color: var(--accent-fill); color: var(--text-on-ink); }
.wl-btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: var(--text-on-ink); }
.wl-btn-ghost { background: var(--surface); border-color: var(--border-strong); color: var(--text-1); }
.wl-btn-ghost:hover { background: var(--surface-2); color: var(--accent); border-color: var(--accent-border); }
.wl-btn-sm { height: 34px; padding: 0 15px; font-size: .8rem; }

/* ── Sections ────────────────────────────────────────────────────────────── */
.wl-sec { padding: 78px 0; }
.wl-sec-alt { background: var(--surface-2); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
.wl-sec-head { max-width: 46em; margin: 0 auto 44px; text-align: center; }
.wl-eyebrow {
  display: inline-block;
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--accent);
}
.wl-h2 { margin: 10px 0 0; font-size: 1.95rem; line-height: 1.28; font-weight: 750; letter-spacing: -.3px; color: var(--text-1); }
.wl-sec-sub { margin: 14px 0 0; font-size: .95rem; line-height: 1.75; color: var(--text-2); }

/* ── Cards (pain points and modules share one card) ──────────────────────── */
.wl-grid { display: grid; gap: 18px; }
.wl-grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.wl-grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.wl-card {
  padding: 26px 24px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  box-shadow: var(--shadow-1);
  transition: box-shadow .18s, transform .18s, border-color .18s;
}
.wl-card:hover { box-shadow: var(--shadow-2); transform: translateY(-2px); border-color: var(--border-strong); }
.wl-card-ico {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  border-radius: 10px;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 1.3rem;
}
.wl-card h3 { margin: 16px 0 0; font-size: 1rem; font-weight: 700; color: var(--text-1); }
.wl-card p { margin: 9px 0 0; font-size: .86rem; line-height: 1.78; color: var(--text-2); }

/* ── Pricing ─────────────────────────────────────────────────────────────── */
/* ⚠️ No `align-items: start` here. The team plan lists one more feature than the
   individual plan, so with independent heights the two cards measured 505px and
   547px and their buttons sat 42px apart — the row read as two things that were
   never meant to be compared. Stretch is what makes the shared baseline a
   baseline; the card is a flex column and the button keeps `margin-top: auto`, so
   both land on the bottom edge. */
.wl-grid-plans { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px; align-items: stretch; }
.wl-plan {
  display: flex;
  flex-direction: column;
  padding: 30px 28px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface);
  box-shadow: var(--shadow-1);
}
/* The team plan is the one people are choosing, so it is the one that carries the
   accent edge. `--accent` (not `--accent-fill`) because this is a BORDER, not a
   fill under white text. */
.wl-plan-featured { border-color: var(--accent-border); box-shadow: var(--shadow-2); position: relative; }
.wl-plan-flag {
  position: absolute;
  top: -11px;
  left: 28px;
  padding: 3px 11px;
  border-radius: 999px;
  background: var(--accent-fill);
  color: var(--text-on-ink);
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .3px;
}
.wl-plan-name { font-size: 1.06rem; font-weight: 750; color: var(--text-1); }
.wl-plan-for { margin: 6px 0 0; font-size: .8rem; color: var(--text-3); }
.wl-price { display: flex; align-items: baseline; gap: 5px; margin: 20px 0 0; }
.wl-price b { font-size: 2.5rem; font-weight: 800; letter-spacing: -1px; color: var(--text-1); }
.wl-price span { font-size: .82rem; color: var(--text-3); }
.wl-plan-desc { margin: 12px 0 0; font-size: .84rem; line-height: 1.72; color: var(--text-2); }
.wl-feats { margin: 22px 0 26px; padding: 0; list-style: none; }
.wl-feats li { display: flex; gap: 10px; padding: 7px 0; font-size: .84rem; line-height: 1.6; color: var(--text-2); }
.wl-feats li i { color: var(--ok); font-size: 1.02rem; margin-top: 1px; flex: none; }
.wl-plan .wl-btn { width: 100%; margin-top: auto; }
.wl-plan-note { margin: 14px 0 0; font-size: .74rem; line-height: 1.6; color: var(--text-3); text-align: center; }

/* ── Footer ──────────────────────────────────────────────────────────────── */
.wl-foot { padding: 34px 0; border-top: 1px solid var(--border); background: var(--surface); }
.wl-foot-in { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.wl-foot-spacer { flex: 1; }
.wl-foot-note { font-size: .76rem; color: var(--text-3); }

/* 备案号（《非经营性互联网信息服务备案管理办法》第十三条：主页底部中央位置）。
   单独一行居中，而不是塞进上面那条 flex 行 —— 那一行是左 logo / 右隐私政策，
   备案号挤进去既不居中、也读不出「底部中央」这个位置。 */
.wl-beian-row {
  margin-top: 12px;
  display: flex;
  justify-content: center;
}

.wl-beian {
  font-size: .74rem;
  color: var(--text-3);
  text-decoration: none;
}

.wl-beian:hover { color: var(--text-2); text-decoration: underline; }
.wl-foot a { color: var(--text-3); font-size: .76rem; text-decoration: none; }
.wl-foot a:hover { color: var(--accent); }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

@media (max-width: 900px) {
  .wl-grid-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .wl-h1 { font-size: 2.3rem; }
}
@media (max-width: 640px) {
  .wl-wrap { padding: 0 18px; }
  .wl-hero-in { padding: 58px 0 54px; }
  .wl-sec { padding: 56px 0; }
  .wl-grid-2, .wl-grid-3, .wl-grid-plans { grid-template-columns: minmax(0, 1fr); }
  .wl-h1 { font-size: 1.9rem; }
  .wl-h2 { font-size: 1.5rem; }
  .wl-btn { width: 100%; }
  .wl-cta { flex-direction: column; align-items: stretch; }
  .wl-head-in { height: 56px; }
}
