/* App layout/components — tokens live in tokens.css (Hedui §4). */

*,
*::before,
*::after { box-sizing: border-box; }

/* The UA [hidden] rule loses to any class that sets display (e.g. .button),
   which left hidden-by-JS controls visible. Component rules that must show a
   [hidden] element still win on specificity. */
[hidden] { display: none !important; }

html { -webkit-font-smoothing: antialiased; }

body {
  margin: 0;
  min-height: 100vh;
  font-family: var(--font-sans);
  font-size: var(--text-15);
  font-weight: 400;
  line-height: 1.47;
  letter-spacing: 0;
  color: var(--text);
  background: var(--bg);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.tabular-nums,
td.num,
th.num,
.stat,
.matrix-table td:not(.sticky-col) {
  font-variant-numeric: tabular-nums;
}

td.num,
th.num,
.matrix-table td:not(.sticky-col) {
  text-align: right;
}

/* —— shared shell (P1) —— */
.app-shell {
  position: sticky;
  top: 0;
  z-index: 40;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  color: var(--text);
}

.app-shell__top {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  min-height: 48px;
  padding: 0 var(--space-4);
}

.app-shell__branding {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
  flex: 0 1 auto;
}

.app-shell__brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-15);
  font-weight: 600;
  color: var(--text);
  text-decoration: none;
  white-space: nowrap;
}

.app-shell__brand-mark {
  width: 1.25rem;
  height: 1.25rem;
  display: block;
  flex: 0 0 auto;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .app-shell__brand-mark {
    filter: invert(1);
  }
}

:root[data-theme="dark"] .app-shell__brand-mark {
  filter: invert(1);
}

.app-shell__subject {
  font-size: var(--text-13);
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 10rem;
}

.app-shell__nav {
  display: flex;
  align-items: stretch;
  gap: var(--space-3);
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
}

.app-shell__nav-link {
  display: inline-flex;
  align-items: center;
  height: 48px;
  padding: 0 2px;
  font-size: var(--text-13);
  color: var(--text-muted);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  white-space: nowrap;
}

.app-shell__nav-link:hover { color: var(--text); }

.app-shell__nav-link.is-active {
  color: var(--text);
  border-bottom-color: var(--text);
  font-weight: 600;
}

.app-shell__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-left: auto;
  flex: 0 0 auto;
}

.app-shell__icon-btn,
.app-shell__text-btn {
  appearance: none;
  margin: 0;
  border: 1px solid transparent;
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--text-13);
  line-height: 1;
  padding: 6px 8px;
  border-radius: var(--r-control);
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
}

.app-shell__icon-btn:hover,
.app-shell__text-btn:hover { color: var(--text); border-color: transparent; }

/* Section links on the right share the left nav's active contract (.app-shell__nav-link);
   the theme toggle and logout stay borderless actions. */
.app-shell__actions > .app-shell__nav-link { margin: 0 var(--space-1); }

.snapshot-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  min-height: 32px;
  padding: 4px var(--space-4);
  background: var(--surface-sunken);
  border-top: 1px solid var(--line);
  font-size: var(--text-12);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.snapshot-bar__range { margin-left: auto; }

.app-shell--anon .app-shell__top { min-height: 44px; }

/* Responsive fix: the single-row shell (branding + nav + actions) cannot
   fit the full nav contract (总览/临床时间线/全部指标/数据表/源报告) alongside
   the right-side actions (上传/待确认/设置/主题/退出) once available width
   drops below ~900px — `.app-shell__brand`'s own `white-space: nowrap`
   gives it no shrink floor, so squeezing the row only ever shrinks
   `.app-shell__subject` (the username), collapsing it to an ellipsis
   ("徐...") well before the row actually fits, and past that point the
   row's combined minimum content width exceeds the viewport, causing
   page-level horizontal overflow rather than a contained overflow inside
   `.app-shell__nav`'s own `overflow-x: auto`. 900px was chosen with
   headroom above the ~750-800px minimum content width the current brand/
   nav/actions content actually needs, so the two-row layout below kicks in
   before any clipping/ellipsis/overflow can occur — not merely at the
   width where the owner happened to observe it break.

   CSS-only two-row layout via flex `order` — no markup change, no
   hamburger, no JS: row 1 keeps branding + actions, row 2 becomes the
   full-width nav (still horizontally scrollable via its existing
   overflow-x:auto if the five links don't all fit one line). Vertical
   padding is added on the container here (not as a one-off margin on
   `病历系统`) so the wrapped first row gets breathing room above/below;
   `row-gap` (spacing between row 1 and row 2) and the shell's bottom
   divider (`.app-shell { border-bottom }`) are both untouched. */
@media (max-width: 900px) {
  .app-shell__top {
    flex-wrap: wrap;
    row-gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
  }
  .app-shell__branding { order: 1; }
  .app-shell__actions { order: 2; }
  .app-shell__nav { order: 3; flex-basis: 100%; }
}

/* Theme switching: no transition (§4.6). */
html,
body,
.app-shell,
.snapshot-bar {
  transition: none;
}

/* Foundation + drawer motion (§4.6 / P2). */
.inspector-root {
  position: fixed;
  inset: 0;
  z-index: 60;
  pointer-events: none;
}

.inspector-root.is-open {
  pointer-events: auto;
}

.inspector-mask {
  position: absolute;
  inset: 0;
  background: rgb(0 0 0 / 0.32);
  opacity: 0;
  transition: opacity var(--motion-mask);
}

.inspector-root.is-open .inspector-mask {
  opacity: 1;
}

.inspector-drawer {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  width: min(560px, 92vw);
  background: var(--surface);
  border-left: 1px solid var(--line);
  box-shadow: var(--shadow-drawer);
  display: flex;
  flex-direction: column;
  transform: translateX(100%);
  transition: transform var(--motion-drawer);
}

.inspector-root.is-open .inspector-drawer {
  transform: translateX(0);
}

.inspector-drawer__chrome {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--line);
}

.inspector-drawer__title {
  margin: 0;
  font-size: var(--text-17);
  font-weight: 600;
  color: var(--text);
}

.inspector-drawer__close {
  appearance: none;
  border: 1px solid transparent;
  background: transparent;
  color: var(--text-muted);
  border-radius: var(--r-control);
  font: inherit;
  font-size: var(--text-13);
  padding: 6px 10px;
  cursor: pointer;
}
.inspector-drawer__close:hover { border-color: transparent; background: transparent; color: var(--accent); }

.inspector-drawer__body {
  overflow: auto;
  padding: var(--space-4);
  flex: 1 1 auto;
  overscroll-behavior: contain;
}

.inspect-catalog { display: none; }

.inspect-record__head { margin-bottom: var(--space-3); }
.inspect-record__title {
  margin: 0 0 var(--space-1);
  font-size: var(--text-17);
  font-weight: 600;
}
.inspect-record__id {
  margin: 0;
  font-size: var(--text-11);
  line-height: 1.5;
  color: var(--text-faint);
  overflow-wrap: anywhere;
}

.inspect-record__id + .inspect-record__id { margin-top: 1px; }

.limitation-caveat {
  border-left: 3px solid var(--st-limit);
  background: var(--surface-sunken);
  padding: var(--space-2) var(--space-3);
  margin: 0 0 var(--space-3);
}

.limitation-caveat__label {
  margin: 0 0 4px;
  font-size: var(--text-12);
  color: var(--st-limit);
  font-weight: 600;
}

.limitation-caveat__codes {
  margin: 0;
  font-size: var(--text-13);
  color: var(--text);
}

.observation-block {
  margin-bottom: var(--space-4);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: var(--space-3);
  background: var(--surface);
}

.observation-block__grid {
  margin: 0;
  display: grid;
  grid-template-columns: 72px 1fr;
  gap: 8px 12px;
  font-size: var(--text-13);
}

.observation-block__grid dt {
  margin: 0;
  color: var(--text-muted);
}

.observation-block__grid dd { margin: 0; color: var(--text); }

/* /charts observation inspector — normal / actionable / technical hierarchy (§P5 Pass B1). */
.inspect-actionable {
  border-left: 3px solid var(--st-review);
  background: var(--surface-sunken);
  padding: var(--space-2) var(--space-3);
  margin: 0 0 var(--space-3);
}

.inspect-actionable__title {
  margin: 0 0 4px;
  font-size: var(--text-12);
  color: var(--st-review);
  font-weight: 600;
}

.inspect-actionable__list {
  margin: 0;
  padding-left: 1.1em;
  font-size: var(--text-13);
  color: var(--text);
}

/* Observation-detail key/value tables reuse the Dashboard table visual
   language (§P5 Owner UI Round 3 §8) — compact table/th/td rules above,
   scoped down to a two-column layout that fits the drawer. */
.inspect-table {
  width: 100%;
  margin: 0 0 var(--space-3);
  font-size: var(--text-13);
}

.inspect-table th,
.inspect-table td {
  padding: 6px var(--space-2);
}

.inspect-table th {
  width: 96px;
  font-weight: 500;
  color: var(--text-muted);
  white-space: nowrap;
}

.inspect-table td {
  color: var(--text);
  overflow-wrap: anywhere;
}

@media (max-width: 480px) {
  .inspect-table th { width: 76px; white-space: normal; }
}

.inspect-technical summary {
  cursor: pointer;
  font-size: var(--text-12);
  font-weight: 600;
  color: var(--text-muted);
  padding: var(--space-2) 0;
}

.inspect-table--technical {
  margin: var(--space-2) 0 0;
  font-size: var(--text-11);
  color: var(--text-faint);
}

.inspect-table--technical th { color: var(--text-faint); }
.inspect-table--technical td { color: var(--text-faint); }

/* Series-level detail source list (P5 Owner UI Round 5 §14-15). */
.inspect-sources__title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-12);
  font-weight: 600;
  color: var(--text-muted);
}

.inspect-sources__list { list-style: none; margin: 0; padding: 0; }

.inspect-sources__item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--line);
  font-size: var(--text-13);
}

.inspect-sources__item:last-child { border-bottom: 0; }

.inspect-sources__meta { min-width: 0; overflow-wrap: anywhere; color: var(--text); }

/* Compact observation rows inside 观测与来源详情 (P5 Round 6 §3-13) — one
   scannable row per observation instead of a repeated full key/value table.
   No vertical grid lines; a horizontal border separates observations. */
.obs-list { list-style: none; margin: 0; padding: 0; }

.obs-row {
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--line);
}

.obs-row:last-child { border-bottom: 0; }

/* Main content (left: value/metadata) vs. the date/time column (right) — a
   real two-region structural layout, not padding offsets, so it wraps
   sensibly at narrow widths (P5 Round 6 owner re-check §5/§9). `center`
   vertically centers the (single-line) right block against the FULL height
   of the left block, which may carry a second metadata line and a third
   caution line (Part E) — not merely against the value line's top. */
.obs-row__main {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.obs-row__content {
  flex: 1 1 240px;
  min-width: 0;
}

.obs-row__value {
  font-size: var(--text-15);
  font-weight: 600;
  color: var(--text);
}

.obs-row__meta {
  margin: 2px 0 0;
  font-size: var(--text-12);
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

/* Imaging-presentation fix: an imaging finding is narrative content, not a
   record-title — it must not reuse .obs-row__value's bold/large treatment.
   The exam context (when present) is a small kicker line above it, shown
   only once per contiguous run of findings that share it. */
.obs-row__context {
  margin: 0 0 2px;
  font-size: var(--text-11);
  font-weight: 600;
  color: var(--text-faint);
}

.obs-row__finding {
  margin: 0;
  font-size: var(--text-13);
  font-weight: 400;
  color: var(--text);
  line-height: 1.5;
  overflow-wrap: break-word;
}

.obs-row__caution {
  margin: 4px 0 0;
  font-size: var(--text-12);
  color: var(--st-review);
}

/* P6 Phase 3 owner round 2 §14-§19: default human-facing record summary —
   visible before 详情, a compact label/value list (not the full drawer
   table). Rows are added only when the underlying fact exists (§15). */
.tl-record-summary {
  margin: var(--space-2) 0 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.tl-record-summary > div {
  display: flex;
  gap: var(--space-2);
  font-size: var(--text-12);
}

.tl-record-summary dt {
  flex: 0 0 auto;
  width: 4.5em;
  color: var(--text-faint);
}

.tl-record-summary dd {
  margin: 0;
  min-width: 0;
  color: var(--text);
  overflow-wrap: anywhere;
}

/* Right-side column: date/time only (P5 Round 6 owner re-check, part 2) —
   打开源报告 moved down into the shared bottom action row alongside 详情. */
.obs-row__side {
  flex: 0 0 auto;
  text-align: right;
}

.obs-row__time {
  font-size: var(--text-12);
  color: var(--text-muted);
  white-space: nowrap;
}

/* Bottom action row (P5 Round 6 owner re-check, Part D): 详情 and
   打开源报告 are true SIBLING elements, not one nested inside the other's
   clickable area — nesting the button inside <summary> made Safari lay the
   native disclosure marker and the flex content on separate lines (a real
   engine quirk with a block-level box as a <summary>'s principal box), and
   any nested-interactive-in-summary structure risks the button click also
   toggling the disclosure. Two siblings in one flex row is simple, avoids
   both problems, and still renders as one visual action row: 详情's
   <details> grows to fill the remaining width (flex: 1 1 auto) so its own
   expanded content (below its <summary>, native behavior, untouched) has
   the same width the row itself has — not squeezed into a narrow column
   beside the button. */
.obs-row__actions {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-top: var(--space-2);
}

.obs-row__detail {
  flex: 1 1 auto;
  min-width: 0;
}

.obs-row__detail summary {
  cursor: pointer;
  font-size: var(--text-12);
  font-weight: 600;
  color: var(--text-muted);
  padding: var(--space-1) 0;
}

.obs-row__detail[open] summary { color: var(--accent); }

.obs-row__detail .inspect-table { margin-top: var(--space-2); }

.obs-row__review {
  margin: var(--space-2) 0 0;
  font-size: var(--text-12);
  color: var(--text-muted);
}

.evidence-panel {
  border-top: 1px solid var(--line-strong);
  padding-top: var(--space-3);
}

.evidence-panel__title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-13);
  font-weight: 600;
  color: var(--text-muted);
}

.evidence-panel__list { margin: 0; }

.evidence-panel__row {
  display: grid;
  grid-template-columns: 72px 1fr;
  gap: 8px 12px;
  padding: 8px 0;
  border-bottom: 1px solid var(--line);
  font-size: var(--text-13);
}

.evidence-panel__row dt {
  margin: 0;
  color: var(--text-muted);
}

.evidence-panel__row dd { margin: 0; color: var(--text); }

.evidence-panel__source-link {
  display: inline-block;
  margin-left: var(--space-2);
  font-size: var(--text-12);
}

.evidence-panel__ref { color: var(--text-faint); }

.evidence-review--needs {
  font-weight: 700;
  color: var(--st-review);
}

.evidence-review--ok { color: var(--text-muted); }
.evidence-review--unknown { color: var(--text-faint); }

.qc-chip {
  display: inline-block;
  margin: 0 0 0 4px;
  padding: 1px 6px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-chip);
  font-size: var(--text-11);
  font-family: var(--font-mono);
  color: var(--st-qc);
  background: var(--surface-sunken);
}

.limitation-inline {
  border-left: 3px solid var(--st-limit);
  padding-left: var(--space-2);
}

.status-glyph--verified { color: var(--st-verified); }
.status-glyph--qc { color: var(--st-qc); }
.status-glyph--blocked { color: var(--st-blocked); }
.status-glyph--muted { color: var(--text-muted); }
.status-glyph--faint { color: var(--text-faint); }
.status-glyph--compare { color: var(--text-muted); }

.mono { font-family: var(--font-mono); }

.empty-state__message {
  margin: 0;
  font-size: var(--text-15);
  color: var(--text-muted);
}

.empty-state__action {
  margin: var(--space-2) 0 0;
  font-size: var(--text-13);
}

.source-meta {
  margin: 0;
  display: grid;
  gap: 8px;
  font-size: var(--text-13);
}

.source-meta > div {
  display: grid;
  grid-template-columns: 72px 1fr;
  gap: 8px 12px;
}

.source-meta dt { margin: 0; color: var(--text-muted); }
.source-meta dd { margin: 0; }

@media (prefers-reduced-motion: reduce) {
  .drawer,
  .drawer-mask,
  [data-drawer],
  .inspector-mask,
  .inspector-drawer {
    transition: none !important;
    animation: none !important;
  }
}

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

a:active { opacity: 0.85; }

code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.92em;
}

/* —— legacy global-nav (superseded by .app-shell; kept inert) —— */
.global-nav { display: none; }

.global-nav a {
  color: var(--text);
  font-size: var(--text-12);
  font-weight: 400;
  line-height: 1;
  opacity: 0.88;
}

.global-nav a:active { opacity: 1; }

.global-nav__brand {
  font-weight: 600;
  opacity: 1 !important;
  letter-spacing: -0.2px;
}

.global-nav__links {
  display: flex;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
}

.global-nav .button-dark-utility {
  margin: 0;
}

/* —— sub-nav-frosted —— */
.sub-nav {
  position: sticky;
  top: 44px;
  z-index: 30;
  height: 52px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  padding: 0 22px;
  background: rgba(245, 245, 247, 0.8);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}

.sub-nav__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: 21px;
  font-weight: 600;
  line-height: 1.19;
  letter-spacing: 0.231px;
  color: var(--color-ink);
}

.sub-nav__meta {
  font-size: 14px;
  letter-spacing: -0.224px;
  color: var(--color-ink-muted-48);
}

/* —— layout —— */
.page {
  max-width: var(--content-max);
  margin: 0 auto;
  padding: var(--space-5) var(--space-5) var(--space-8);
}

.page--wide { max-width: var(--content-wide); }
.page--app { max-width: var(--content-app); }

@media (min-width: 1100px) {
  .page { padding-left: var(--space-6); padding-right: var(--space-6); }
}

.page-hero,
.page-head {
  margin: 0 0 var(--space-4);
}

.page-hero h1,
.page-head__title {
  margin: 0;
  font-size: var(--text-21);
  font-weight: 600;
  line-height: 1.25;
  color: var(--text);
}

.page-hero .lead,
.page-head__lead {
  margin: var(--space-1) 0 0;
  font-size: var(--text-13);
  font-weight: 400;
  line-height: 1.5;
  color: var(--text-muted);
}

.section,
.section-gap { margin-top: var(--space-6); }

.section-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  flex-wrap: wrap;
  padding-bottom: var(--space-2);
  margin-bottom: var(--space-4);
  border-bottom: 1px solid var(--line);
}

.section-title {
  margin: 0;
  font-size: var(--text-15);
  font-weight: 600;
  line-height: 1.3;
  color: var(--text);
}

.section-meta {
  margin: 0;
  font-size: var(--text-12);
  color: var(--text-faint);
}

.section-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.section-link { font-size: var(--text-12); }

.section-foot {
  margin: var(--space-3) 0 0;
  font-size: var(--text-12);
  line-height: 1.6;
  color: var(--text-faint);
}

.section-foot p { margin: 0; }

.caption {
  font-size: var(--text-12);
  line-height: 1.5;
  color: var(--text-muted);
}

.caption--spaced { margin: var(--space-3) 0 var(--space-4); }
.caption--top { margin-top: var(--space-4); }

.fine-print {
  font-size: var(--text-11);
  color: var(--text-faint);
}

.error { color: var(--st-qc); }
.ok { color: var(--st-verified); }
.muted { color: var(--text-faint); }

/* —— buttons —— */
button,
.button,
.button-primary,
.button--primary,
.button-secondary-pill,
.button-dark-utility,
.button-pearl-capsule {
  appearance: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 28px;
  padding: 0 var(--space-3);
  font-family: var(--font-sans);
  font-size: var(--text-12);
  font-weight: 500;
  line-height: 1;
  text-decoration: none;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-control);
  background: var(--surface);
  color: var(--text);
}

button:hover,
.button:hover,
.button-pearl-capsule:hover,
.button-secondary-pill:hover { border-color: var(--text-faint); }

.button-primary,
.button--primary,
.button-dark-utility {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--color-on-primary);
}

.button-primary:hover,
.button--primary:hover,
.button-dark-utility:hover { filter: brightness(1.06); }

/* Shared disabled state (P5 Owner UI Round 5 §18) — a disabled action is a
   <span>, never an <a> with a live href (so keyboard/click cannot navigate
   through it); reduced contrast, no hover affordance, not-allowed cursor. */
.button.is-disabled,
.button[aria-disabled="true"] {
  color: var(--text-faint);
  border-color: var(--line);
  background: var(--surface-sunken);
  cursor: not-allowed;
  pointer-events: none;
}

button:focus-visible,
.button:focus-visible,
.button-primary:focus-visible,
.button--primary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

button:disabled { cursor: default; color: var(--text-faint); }

.inline,
.inline-form { display: inline-flex; }

/* —— forms —— */
.form-stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.form-stack label,
.form-row label {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-size: var(--text-12);
  font-weight: 500;
  color: var(--text-muted);
}

.form-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: end;
}

.search-input,
select,
textarea,
input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]):not([type="range"]) {
  font-family: var(--font-sans);
  font-size: var(--text-13);
  font-weight: 400;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-control);
  padding: 0 var(--space-3);
  height: 28px;
  min-width: 0;
}

textarea { height: auto; padding: var(--space-2) var(--space-3); line-height: 1.5; }

select { padding-right: var(--space-5); }

input[type="file"] {
  height: auto;
  padding: var(--space-3);
  width: 100%;
  max-width: 420px;
}

input[type="checkbox"] { accent-color: var(--accent); }

.search-input:focus,
select:focus,
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):focus,
textarea:focus {
  outline: 2px solid var(--accent);
  outline-offset: 0;
  border-color: var(--accent);
}

/* —— store-utility-card grid —— */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 20px;
}

.utility-card {
  background: var(--color-canvas);
  border: 1px solid var(--color-hairline);
  border-radius: var(--rounded-lg);
  padding: var(--space-lg);
}

.utility-card h2,
.utility-card h3 {
  margin: 0;
  font-size: 17px;
  font-weight: 600;
  line-height: 1.24;
  letter-spacing: 0;
}

.utility-card h2 a {
  color: var(--color-ink);
}

.utility-card h2 a:active {
  color: var(--color-primary);
}

.utility-card .stat {
  margin: var(--space-xs) 0 0;
  font-family: var(--font-display);
  font-size: 34px;
  font-weight: 600;
  letter-spacing: 0;
  line-height: 1.1;
  color: var(--color-ink);
}

.utility-card .stat-label {
  margin-top: var(--space-xxs);
  font-size: 14px;
  color: var(--color-ink-muted-48);
  letter-spacing: -0.224px;
}

/* —— chips / tabs —— */
.chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0 0 var(--space-4);
}

.chip,
.chip-check {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-control);
  padding: 0 var(--space-3);
  min-height: 26px;
  font-size: var(--text-12);
}

.chip.is-selected {
  border-color: var(--accent);
  color: var(--accent);
}

/* —— tables —— */
.table-shell {
  overflow: auto;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
}

.table-shell--scroll { max-height: 34rem; }

table {
  border-collapse: collapse;
  width: 100%;
  font-size: var(--text-13);
  line-height: 1.45;
}

th,
td {
  padding: var(--space-2) var(--space-3);
  text-align: left;
  /* P5 Round 6 §15: site-wide data-table cells vertically center content,
     including wrapped timestamps and inline action buttons. */
  vertical-align: middle;
  border-bottom: 1px solid var(--line);
}

th {
  font-weight: 600;
  color: var(--text);
}

thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface);
  font-size: var(--text-12);
  font-weight: 600;
  color: var(--text-muted);
  border-bottom: 1px solid var(--line-strong);
  white-space: nowrap;
  /* P5 Owner UI Round 5 §8: every header cell centers its content on the
     same baseline regardless of whether it holds plain text or a `.th-row`
     control group — overrides the generic th,td { vertical-align: top }. */
  vertical-align: middle;
}

.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

td.is-empty { color: var(--text-faint); }

tbody tr:last-child td,
tbody tr:last-child th { border-bottom: 0; }

/* Compact per-column sort/filter controls (P5 Owner UI Round 4 §3/§4) — title
   and its control(s) share ONE header line, visually subordinate to the
   title; no second control row, no dedicated table toolbar row (§6, reset
   moved to the page-level section header instead). */
.table-controls-empty {
  margin: 0;
  padding: var(--space-3);
  font-size: var(--text-12);
  color: var(--text-faint);
}

.th-row {
  display: flex;
  align-items: center;
  gap: 4px;
  /* Genuine narrow-width fallback only (P5 Round 6 owner re-check, Part B):
     with correct column-width allocation, header content fits on one line
     at ordinary desktop widths and this never triggers there. */
  flex-wrap: wrap;
}

/* Right-aligned columns (num) still use the same .th-row structure for
   consistent vertical rendering (§8) even with no controls of their own. */
.th-row--end { justify-content: flex-end; width: 100%; }

.th-label { flex: 0 0 auto; }

.col-filter-input {
  font-size: var(--text-11);
  height: 20px;
  width: 4.5rem;
  min-width: 0;
  flex: 0 1 auto;
}

/* Compact combined up/down glyph, single upward/downward arrow when active —
   never a combined icon plus a separate large triangle (owner explicitly
   rejected that treatment). The button's own text is the whole indicator. */
.col-sort-btn {
  appearance: none;
  border: none;
  background: none;
  color: var(--text-faint);
  cursor: pointer;
  font-size: var(--text-12);
  line-height: 1;
  padding: 2px;
  flex: 0 0 auto;
}

.col-sort-btn:hover { color: var(--text); }

th[data-col].is-sorted-asc .col-sort-btn,
th[data-col].is-sorted-desc .col-sort-btn { color: var(--accent); font-weight: 700; }


/* —— flash —— */
.flash {
  list-style: none;
  margin: 0 0 var(--space-4);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.flash li {
  padding: var(--space-1) 0 var(--space-2);
  border-bottom: 1px solid var(--line);
  font-size: var(--text-12);
  color: var(--text-muted);
}

.flash .success { color: var(--st-verified); }
.flash .error { color: var(--st-qc); }

/* —— login tile —— */
.page--login {
  min-height: calc(100vh - 52px);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  padding: var(--space-6) var(--space-4) var(--space-4);
}

.login-panel {
  width: 100%;
  max-width: 360px;
  margin: auto 0;
  text-align: left;
}

.login-panel h1 {
  margin: 0 0 var(--space-2);
  font-family: var(--font-sans);
  font-size: var(--text-21);
  font-weight: 600;
  color: var(--text);
  line-height: 1.25;
}

.login-panel .lead {
  margin: 0 0 var(--space-5);
  font-size: var(--text-13);
  color: var(--text-muted);
}

.login-panel form {
  text-align: left;
}

.login-panel .button-primary {
  width: 100%;
  min-height: 32px;
  margin-top: var(--space-3);
}

.login-panel .error {
  margin: 0 0 var(--space-3);
  font-size: var(--text-13);
  color: var(--st-blocked);
}

/* Login beian footer — zeoseven-style: 国徽 + 公安 | ICP. */
.login-footer {
  flex: 0 0 auto;
  width: 100%;
  padding: var(--space-4) 0 var(--space-2);
  text-align: center;
  color: var(--text-muted);
  font-size: var(--text-12);
  line-height: 1.5;
}

.login-footer__beian {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  margin: 0;
  gap: 0;
}

.login-footer__beian-icon {
  width: 14px;
  height: 16px;
  margin-right: 4px;
  vertical-align: middle;
  flex: 0 0 auto;
}

.login-footer__sep {
  margin: 0 var(--space-2);
  color: var(--text-muted);
  opacity: 0.7;
}

.login-footer__link {
  color: inherit;
  text-decoration: none;
  transition: color 0.2s ease;
}

.login-footer__link:hover {
  color: var(--accent);
}

/* —— global states (P3) —— */
.state-panel {
  max-width: 36rem;
  margin: var(--space-8) auto;
  padding: var(--space-4) 0;
}

.state-panel__title {
  margin: 0 0 var(--space-3);
  font-size: var(--text-21);
  font-weight: 600;
  color: var(--text);
}

.state-panel__action {
  margin: 0;
  font-size: var(--text-13);
}

.snapshot-bar--invalid {
  align-items: flex-start;
  gap: var(--space-2);
  color: var(--text);
  background: var(--surface-sunken);
  border-top: 1px solid var(--line-strong);
}

.snapshot-bar__glyph {
  flex: 0 0 auto;
  color: var(--st-blocked);
}

.snapshot-bar__retry {
  white-space: nowrap;
}

.page--state {
  min-height: 40vh;
}

.chart-canvas-slot {
  min-height: 220px;
  display: block;
}

.chart-canvas-slot .trend-canvas {
  width: 100%;
  display: block;
}

.source-frame-slot {
  position: relative;
  min-height: 75vh;
}

.source-loading-slot {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  z-index: 1;
}

.source-loading-slot[hidden] {
  display: none;
}

.source-loading-slot__message {
  margin: 0;
  font-size: 13px;
  color: var(--text-muted);
}

.source-frame-slot .source-frame {
  position: relative;
  z-index: 0;
}

/* —— chart —— */
.chart-shell {
  background: var(--color-canvas);
  border: 1px solid var(--color-hairline);
  border-radius: var(--rounded-lg);
  padding: var(--space-lg);
}

.chart-shell canvas {
  width: 100% !important;
  max-height: 460px;
}

/* —— footer —— */
.site-footer {
  background: var(--color-canvas-parchment);
  color: var(--color-ink-muted-80);
  padding: 48px 22px 64px;
  border-top: 1px solid rgba(0, 0, 0, 0.08);
}

.site-footer .inner {
  max-width: var(--content-max);
  margin: 0 auto;
  font-size: 12px;
  letter-spacing: -0.12px;
  color: var(--color-ink-muted-48);
}

/* —— responsive —— */
@media (max-width: 833px) {
  .global-nav__links { gap: 14px; }
  .page-hero h1 { font-size: 34px; letter-spacing: 0; }
  .section-title { font-size: 28px; }
  .sub-nav__title { font-size: 17px; letter-spacing: 0; }
}

@media (max-width: 640px) {
  .page-hero h1 { font-size: 28px; }
  .card-grid { grid-template-columns: 1fr 1fr; gap: 12px; }
  .utility-card { padding: 16px; }
  .utility-card .stat { font-size: 28px; }
}

@media (max-width: 419px) {
  .card-grid { grid-template-columns: 1fr; }
  .global-nav { padding: 0 14px; }
  .sub-nav { padding: 0 14px; }
}

/* —— v1 病历系统增补 —— */
.global-nav__branding { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.nav-h1 { margin: 0; font-size: 17px; font-weight: 600; color: inherit; }
.nav-h2 { margin: 0; font-size: 12px; font-weight: 400; color: var(--color-ink-muted-48); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 160px; }
.stat-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; }
.stat-item { background: var(--color-canvas); border: 1px solid var(--color-hairline); border-radius: var(--rounded-md); padding: 14px 16px; }
.stat-item .stat { margin: 4px 0 0; font-size: 28px; font-weight: 600; }
.stat-item .stat--text { font-size: 18px; }
.stat-item .stat-label { margin: 0; font-size: 12px; color: var(--color-ink-muted-48); }

/* P4 Dashboard */
.status-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0;
  margin: 0 0 var(--space-5);
  padding: var(--space-2) 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

.status-strip__item {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-4);
  border-right: 1px solid var(--line);
}

.status-strip__item:first-child { padding-left: 0; }
.status-strip__item:last-child { border-right: 0; }

.status-strip__label {
  font-size: var(--text-12);
  color: var(--text-muted);
}

.status-strip__value {
  font-size: var(--text-15);
  font-weight: 600;
  color: var(--text);
}

.tabular { font-variant-numeric: tabular-nums; }

.ts { display: inline-block; white-space: nowrap; }
.ts__date { display: block; color: inherit; }
.ts__time { display: block; font-size: var(--text-11); color: var(--text-faint); }
.ts--none { color: var(--text-faint); }

.snapshot-range-form {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.snapshot-range-form label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-12);
  color: var(--text-muted);
}

.snapshot-range-form select,
.snapshot-range-form input {
  height: 24px;
  font-size: var(--text-12);
}

/* metric chooser (§8) — compact popover instead of a full checkbox cloud */
.metric-picker { position: relative; }

.metric-picker__summary {
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 28px;
  padding: 0 var(--space-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-control);
  background: var(--surface);
  color: var(--text);
  font-size: var(--text-12);
  font-weight: 500;
  cursor: pointer;
}

.metric-picker__summary::-webkit-details-marker { display: none; }
.metric-picker__summary::after { content: "▾"; color: var(--text-faint); }
.metric-picker__summary:hover { border-color: var(--text-faint); }
.metric-picker__summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.metric-picker[open] > .metric-picker__summary { border-color: var(--accent); color: var(--accent); }
.metric-picker[open] > .metric-picker__summary::after { content: "▴"; }

.metric-picker__count { color: var(--text-muted); font-variant-numeric: tabular-nums; }

.metric-picker__panel {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 30;
  width: min(380px, 88vw);
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-card);
}

.metric-picker__search {
  padding: var(--space-2);
  border-bottom: 1px solid var(--line);
}

.metric-picker__search input { width: 100%; }

.metric-picker__list {
  list-style: none;
  margin: 0;
  padding: var(--space-1) 0;
  max-height: 19rem;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.metric-option[hidden] { display: none; }

.metric-option__label {
  display: grid;
  grid-template-columns: auto 1fr auto;
  column-gap: var(--space-2);
  align-items: baseline;
  padding: var(--space-1) var(--space-3);
  cursor: pointer;
}

.metric-option__label:hover { background: var(--surface-sunken); }
.metric-option__label:focus-within { background: var(--accent-weak); }

.metric-option__label input {
  grid-column: 1;
  grid-row: 1 / span 2;
  align-self: center;
  margin: 0;
}

.metric-option__name {
  grid-column: 2;
  grid-row: 1;
  font-size: var(--text-13);
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.metric-option__unit {
  grid-column: 3;
  grid-row: 1;
  font-size: var(--text-11);
  color: var(--text-muted);
}

.metric-option__id {
  grid-column: 2 / span 2;
  grid-row: 2;
  font-size: var(--text-11);
  color: var(--text-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.metric-option__label input:disabled ~ .metric-option__name,
.metric-option__label input:disabled ~ .metric-option__unit { color: var(--text-faint); }

.metric-picker__empty {
  margin: 0;
  padding: var(--space-4) var(--space-3);
  font-size: var(--text-12);
  color: var(--text-faint);
}

.metric-picker__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-top: 1px solid var(--line);
}

.metric-picker__hint {
  margin: 0;
  font-size: var(--text-11);
  color: var(--text-faint);
}

.metric-picker__hint.is-limit { color: var(--st-review); }

.metric-picker__foot-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 0 0 auto;
}

/* Shared date-range control (§P5 Owner UI Round 2 §9) — compact popover, same
   visual language as the metric picker; every option is a plain GET link/form. */
.range-picker { position: relative; }

.range-picker__summary {
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 28px;
  padding: 0 var(--space-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-control);
  background: var(--surface);
  color: var(--text);
  font-size: var(--text-12);
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
}

.range-picker__summary::-webkit-details-marker { display: none; }
.range-picker__summary:hover { border-color: var(--text-faint); }
.range-picker__summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.range-picker[open] > .range-picker__summary { border-color: var(--accent); color: var(--accent); }

.range-picker__summary-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 12px;
  height: 12px;
  line-height: 0;
}

/* Functional icons: one CSS-mask mechanism so the glyph always takes the
   containing control's `color` (an external <img> cannot). */
.fn-icon {
  display: block;
  flex: none;
  width: 12px;
  height: 12px;
  background-color: currentColor;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: contain;
  mask-size: contain;
}
.fn-icon--time { -webkit-mask-image: url("icons/time.svg"); mask-image: url("icons/time.svg"); }
.fn-icon--filter { -webkit-mask-image: url("icons/filter.svg"); mask-image: url("icons/filter.svg"); }
.fn-icon--trash { -webkit-mask-image: url("icons/trash.svg"); mask-image: url("icons/trash.svg"); }

.range-picker__summary-label { font-variant-numeric: tabular-nums; }

/* Icon-only trigger for compact contexts, e.g. a table column header
   (P5 Owner UI Round 4 §7). */
.range-picker--compact .range-picker__summary {
  min-height: 20px;
  padding: 0 4px;
}

.range-picker__summary-flag {
  color: var(--st-review);
  font-weight: 700;
}

.range-picker__panel {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 30;
  width: max-content;
  max-width: min(320px, 88vw);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-drawer);
}

.range-picker__presets {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.range-picker__preset {
  display: inline-block;
  padding: 4px 10px;
  border: 1px solid var(--line);
  border-radius: var(--r-control);
  font-size: var(--text-12);
  color: var(--text);
  text-decoration: none;
  white-space: nowrap;
}

.range-picker__preset:hover { background: var(--surface-sunken); }
.range-picker__preset.is-active { background: var(--accent-weak); color: var(--accent); border-color: var(--accent); font-weight: 500; }

.range-picker__custom {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--line);
}

.range-picker__field {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: var(--text-11);
  color: var(--text-muted);
}

.range-picker__field input { height: 26px; font-size: var(--text-12); }

.range-picker__actions { display: flex; gap: var(--space-2); }

.range-picker__error {
  margin: 0;
  font-size: var(--text-11);
  color: var(--st-review);
}

/* Responsive fix: the metric/range picker panels are position:absolute
   relative to their own (narrow) <details> trigger. At mobile width that
   trigger can sit anywhere in a wrapped action row, so a fixed max-width
   like 88vw does not guarantee the panel stays inside the viewport. Move
   the positioning context to the WIDER row the trigger lives in instead —
   Dashboard's `.section-actions`/`.metric-picker`/`.range-picker` become
   position:static so `.section-head` (spanning the full content width)
   becomes the nearest positioned ancestor; the charts range picker anchors
   to its own full-width toolbar and aligns from the right, since its
   trigger sits flush right there. Only these three named controls are
   touched — the compact table-header range picker is untouched. */
@media (max-width: 640px) {
  .section-head { position: relative; }
  .section-actions .metric-picker,
  .section-actions .range-picker { position: static; }
  .section-head .metric-picker__panel,
  .section-head .range-picker__panel {
    max-width: calc(100vw - 2 * var(--space-4));
  }

  .charts-detail__toolbar { position: relative; }
  .charts-detail__toolbar .range-picker { position: static; }
  .charts-detail__toolbar .range-picker__panel {
    left: auto;
    right: 0;
    max-width: calc(100vw - 2 * var(--space-4));
  }
}

/* Compact sort control (§P5 Owner UI Round 2 §5) — same popover pattern as
   the date-range control, sized down to an icon-only trigger. */
.sort-picker { position: relative; }

.sort-picker__summary {
  list-style: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-control);
  background: var(--surface);
  color: var(--text-muted);
  cursor: pointer;
}

.sort-picker__summary::-webkit-details-marker { display: none; }
.sort-picker__summary:hover { border-color: var(--text-faint); color: var(--text); }
.sort-picker__summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.sort-picker[open] > .sort-picker__summary { border-color: var(--accent); color: var(--accent); }
.sort-picker__summary > span[aria-hidden="true"] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  line-height: 0;
}

.sort-picker__panel {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 30;
  width: max-content;
  min-width: 8rem;
  list-style: none;
  margin: 0;
  padding: var(--space-1) 0;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-drawer);
}

/* P5 Round 6 (owner re-check, Part A visual match): this menu-row style is
   shared by two element types -- the existing sort options (<a>, no native
   chrome) and the newer comparability filter options (<button>, which DOES
   carry native browser button chrome -- border/background/appearance/
   centered text -- unless explicitly reset). Without the reset below, a
   <button class="sort-picker__option"> renders as an individually bordered,
   rounded button instead of a plain full-width menu row, which is exactly
   why the two menus looked like different components. The reset is a no-op
   for the existing <a> options (anchors have none of these by default). */
.sort-picker__option {
  display: block;
  width: 100%;
  appearance: none;
  background: none;
  border: none;
  border-radius: 0;
  padding: var(--space-1) var(--space-3);
  font: inherit;
  font-size: var(--text-13);
  color: var(--text);
  text-align: left;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
}

.sort-picker__option:hover { background: var(--surface-sunken); }
.sort-picker__option.is-active { background: var(--accent-weak); color: var(--accent); font-weight: 500; }

/* Compact variant for the same sort-picker markup reused as a table-header
   filter trigger (P5 Round 6 owner re-check, Part A) -- the 28px page-level
   sort icon is oversized for a cramped column header; scoping by .th-row
   ancestry keeps the /charts page's own sort-picker untouched. */
.th-row .sort-picker__summary { width: 20px; height: 20px; font-size: var(--text-11); }

/* Non-default filter selected (Part A §4): the trigger must communicate an
   active filter even while collapsed, not only while the menu is open --
   reuses the exact same accent-border treatment as the open state. */
.sort-picker.is-filter-active > .sort-picker__summary { border-color: var(--accent); color: var(--accent); }
.range-picker.is-filter-active > .range-picker__summary { color: var(--accent); }

/* Table-header time/filter controls are header actions in the same family as
   `.col-sort-btn`: borderless, same inactive (--text-faint) and hover (--text)
   foreground; their .fn-icon follows via `currentColor`. `:where()` keeps the
   colours at low specificity so [open] / .is-filter-active (accent) still win;
   the border/background reset is intentionally stronger so no chrome returns
   in any state — active/open stay distinguishable by the accent foreground.
   State lives on the ACTION icon only: a `.th-label` never changes colour. */
:where(.th-row) .range-picker__summary,
:where(.th-row) .sort-picker__summary { color: var(--text-faint); }
:where(.th-row) .range-picker__summary:hover,
:where(.th-row) .sort-picker__summary:hover { color: var(--text); }
.th-row .range-picker .range-picker__summary,
.th-row .sort-picker .sort-picker__summary { border-color: transparent; background: transparent; }

/* trend grid (§8) */
.chart-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
}

@media (min-width: 1024px) {
  .chart-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 1680px) {
  .chart-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.chart-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
  padding: var(--space-3) var(--space-4) var(--space-2);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
}

.chart-card__head { position: relative; display: flex; flex-direction: column; gap: 2px; min-width: 0; padding-right: 5.5rem; }

@media (max-width: 480px) {
  /* Sensible wrapping instead of overlaying the title on narrow/mobile
     layouts (P5 Owner UI Round 5 §4). */
  .chart-card__head { padding-right: 0; }
  .chart-card__head .chart-reset-view { position: static; align-self: flex-start; margin-top: 2px; }
}

.chart-card__title {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin: 0;
  min-width: 0;
  font-size: var(--text-17);
  font-weight: 600;
  line-height: 1.3;
  color: var(--text);
}

.chart-card__unit {
  font-size: var(--text-12);
  font-weight: 400;
  color: var(--text-muted);
  white-space: nowrap;
}

.chart-card__sub {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
  font-size: var(--text-12);
  color: var(--text-muted);
}

.chart-card__sub-sep { color: var(--text-faint); }

.chart-card__availability-note {
  margin: 0;
  font-size: var(--text-12);
  color: var(--text-muted);
}

/* Restrained reference-range / missing-connection legend lines (§P5 Owner UI Round 2 §3-4). */
.chart-card__legend {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  font-size: var(--text-11);
  color: var(--text-faint);
}

.chart-card__legend--muted { color: var(--text-muted); }

.chart-card__legend-swatch {
  display: inline-block;
  width: 14px;
  height: 8px;
  flex: 0 0 auto;
  border-radius: 2px;
}

.chart-card__legend-swatch--band { background: var(--chart-band); border: 1px solid var(--line-strong); }

.chart-card__legend-swatch--line {
  width: 10px;
  height: 0;
  border-top: 2px solid var(--line-strong);
}

.chart-canvas-slot {
  position: relative;
  height: 190px;
  min-height: 0;
}

/* Single-column grid: give the plot more vertical room so it is not a flat band. */
@media (max-width: 1023px) {
  .chart-canvas-slot { height: 260px; }
}

.chart-canvas-slot .trend-canvas {
  position: absolute;
  inset: 0;
  width: 100% !important;
  height: 100% !important;
  display: block;
}

/* Compact hover/inspection panel (§11.5) — external Chart.js tooltip so
   same-day clusters can list each observation as an individually clickable row. */
/* Unobtrusive reset-view control (P5 Owner UI Round 4 §16) — only shown once
   the chart's zoom/pan viewport differs from the initial x-domain. Anchored
   to the chart-card header (title row), never the plot area (§4). */
/* Positioning + compactness only — `.button` (shared, §7) supplies border/
   background/color/cursor/hover/focus. */
.chart-reset-view {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 3;
  min-height: 22px;
  padding: 0 8px;
  font-size: var(--text-11);
}

/* Shared floating-tooltip surface (Metrics chart + Timeline navigator).
   Theme via tokens only — do not fork light/dark skins per surface. */
.chart-tooltip,
.tl-nav__tooltip {
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-control);
  box-shadow: var(--shadow-drawer);
  font-size: var(--text-12);
  color: var(--text);
  padding: var(--space-2) var(--space-3);
}

.chart-tooltip {
  position: absolute;
  z-index: 4;
  min-width: 160px;
  max-width: 260px;
  pointer-events: auto;
  transform: translateY(-50%);
}

.chart-tooltip__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  padding: 2px 0;
}

.chart-tooltip__row--head {
  font-weight: 600;
  border-bottom: 1px solid var(--line);
  margin-bottom: 2px;
  padding-bottom: 4px;
}

.chart-tooltip__row--muted { color: var(--text-muted); font-size: var(--text-11); }

.chart-tooltip__row--clickable {
  cursor: pointer;
  border-radius: var(--r-chip);
}

.chart-tooltip__row--clickable:hover { background: var(--surface-sunken); }

.chart-card__foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-top: auto;
  padding-top: var(--space-2);
  border-top: 1px solid var(--line);
}

.chart-card__id {
  font-size: var(--text-11);
  color: var(--text-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chart-card__note {
  font-size: var(--text-11);
  color: var(--st-limit);
  text-align: right;
}

.chart-card__context {
  font-size: var(--text-11);
  color: var(--text-faint);
  text-align: center;
  white-space: nowrap;
}

/* longitudinal index (§8) */
/* P5 Round 6 (owner re-check, Part B): Owner-mandated final three columns
   (可比状态/潜在问题/待人工复核) at 8% each -- the 可比状态 icon-only filter
   trigger (Part A) needs far less width than the old native <select>, which
   frees width back to the first five columns versus the previous allocation.
   指标 keeps enough room for typical display names (long names still wrap
   rather than truncate); 单位 is compact; 时间跨度 keeps the most width among
   the flexible content columns since its body shows two date/time endpoints
   plus an arrow, and its header carries a range-filter icon + sort button. */
.series-index th:nth-child(1), .series-index td:nth-child(1) { width: 26%; }
.series-index th:nth-child(2), .series-index td:nth-child(2) { width: 10%; }
.series-index th:nth-child(3), .series-index td:nth-child(3) { width: 6%; }
.series-index th:nth-child(4), .series-index td:nth-child(4) { width: 10%; }
.series-index th:nth-child(5), .series-index td:nth-child(5) { width: 8%; }
.series-index th:nth-child(6), .series-index td:nth-child(6) { width: 18%; }
.series-index th:nth-child(7), .series-index td:nth-child(7) { width: 8%; }
.series-index th:nth-child(8), .series-index td:nth-child(8) { width: 6%; }
.series-index th:nth-child(9), .series-index td:nth-child(9) { width: 8%; }

/* Column alignment invariant (Part C): 指标 stays left (default), 最近值
   stays right (.num + .th-row--end, unchanged); 单位 and every remaining
   column are horizontally centered -- the header's whole control group, not
   just its label, and the body content including the 时间跨度 flex pair.
   Higher specificity than the single-class .num so it wins the text-align
   cascade on the numeric-formatted columns that are also centered. */
.series-index th.col-center,
.series-index td.col-center { text-align: center; }
.series-index th.col-center .th-row { justify-content: center; }
.series-index td.col-center .series-index__span-inner { justify-content: center; }
.series-index .col-unit {
  white-space: nowrap;
  font-size: var(--text-12);
  color: var(--text-muted);
}

/* P5 Round 6 (owner final bugfix, Bug 1): `thead th` is `position: sticky`
   with its own `z-index: 1` (above), which makes EVERY header cell its own
   stacking context with an opaque background. An open popover's
   `z-index: 30` (.range-picker__panel / .sort-picker__panel) only wins
   against elements INSIDE that same stacking context -- it can never climb
   past its own <th>'s context to paint over a SIBLING <th>'s opaque
   background. Ties between sibling stacking contexts fall back to DOM
   order, so 时间跨度's rightward-opening panel (an earlier sibling) loses
   to 可比状态's <th> (a later sibling, painted on top) -- the exact
   "rectangular interruption" the Owner sees. (可比状态's own popover opens
   LEFTWARD over an EARLIER sibling, which already wins that DOM-order tie,
   which is why it never showed this symptom.) Fix: while a header's own
   popover is open, bump that <th>'s stacking order above its siblings --
   closed-state header appearance is completely untouched. */
.series-index thead th:has(.range-picker[open]),
.series-index thead th:has(.sort-picker[open]) {
  z-index: 2;
}

.series-index__metric { font-weight: 400; }

.series-index__name { font-size: var(--text-13); color: var(--text); }

/* P5 Owner UI Round 5 §10: `display: flex` directly on a <td> makes it stop
   rendering as a normal table cell in some browsers, breaking that column's
   row-separator continuity. The flex layout now lives on an inner wrapper;
   the <td> itself stays a plain table cell. */
.series-index__span-inner { display: flex; align-items: flex-start; gap: var(--space-2); }
.series-index__span-arrow { color: var(--text-faint); line-height: 1.45; }

.series-index__row.is-inspectable { cursor: pointer; }

.series-index__row.is-inspectable:hover,
.series-index__row.is-inspectable:focus-visible {
  background: var(--accent-weak);
  outline: none;
}

.dash-footer-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-5);
}

/* P5 Round 6 (owner final bugfix, Bug 2): a `min-height` on `.section-head`
   alone was insufficient because 待确认's header mixes text (title/count)
   with a `.button` inside an `align-items: baseline`-aligned flex row --
   the button has no real text baseline, so its SYNTHESIZED baseline (per
   spec, an inline-block-like box without an in-flow line box baselines from
   its margin-box bottom edge) combines with the title's actual text
   baseline in a way that inflates 待确认's natural line height well past
   any single min-height guess, while 近期处理 (title only, a genuine text
   baseline, no synthesis) stays short -- min-height only raises the
   shorter box's floor, it can never make the two computed heights equal.
   Subgrid makes them structurally equal instead of approximately equal:
   both `.dash-footer-grid__col`s share the PARENT's row tracks, so the
   header row's height becomes the max of both columns' actual header
   content -- whatever that content turns out to be -- and the content row
   starts at the same Y in both columns, empty 近期处理 or not. */
@media (min-width: 1100px) {
  .dash-footer-grid {
    grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
    grid-template-rows: auto 1fr;
    column-gap: var(--space-5);
    row-gap: 0;
  }
  .dash-footer-grid__col {
    display: grid;
    grid-template-rows: subgrid;
    grid-row: span 2;
  }
  /* P5 Round 6 (final pre-review patch, item 1): row 1 (the header) keeps
     the DEFAULT `align-items: stretch` -- that is exactly what makes a
     naturally-shorter header (近期处理) stretch to the row's auto-computed
     height (driven by the taller 待确认 header) so both border-bottoms land
     at the same Y. Row 2 (the content block -- whichever element it is:
     the pending-review list/empty-state on the left, the table-shell on
     the right) must NOT inherit that same stretch: `1fr` on row 2 gives it
     room to grow, and default stretch was filling that room even when a
     column's own content (an empty 近期处理 table) is much shorter than its
     sibling. `align-self: start` lets each content block size to its own
     content and start at the top of row 2, instead of being stretched to
     match 待确认's height. */
  .dash-footer-grid__col > :nth-child(2) {
    align-self: start;
  }
}

.dash-footer-grid__col { min-width: 0; }

/* Compact 操作 column — keep rows dense; reuse shared `.button`. */
.dash-footer-grid .data-table__action {
  width: 1%;
  white-space: nowrap;
  vertical-align: middle;
}
.dash-footer-grid .data-table__action .button {
  padding: 0.2rem 0.55rem;
  font-size: var(--text-12);
}

/* 待确认: 问题 | 类型 | 状态 | 页码 | 操作 — structured fields already on the item. */
.dash-footer-reviews .col-problem { text-align: left; width: 42%; min-width: 0; }
.dash-footer-reviews .col-kind,
.dash-footer-reviews .col-date,
.dash-footer-reviews .col-page,
.dash-footer-reviews .col-action { text-align: center; width: 1%; white-space: nowrap; }
.dash-footer-reviews .col-problem {
  overflow-wrap: anywhere;
  word-break: break-word;
}

/* 近期处理: 报告 | 状态 | 更新时间 | 操作 | 说明 */
.dash-footer-tasks .col-report,
.dash-footer-tasks .col-note { text-align: left; }
.dash-footer-tasks .col-status,
.dash-footer-tasks .col-updated,
.dash-footer-tasks .col-action { text-align: center; }
.dash-footer-tasks .col-status,
.dash-footer-tasks .col-updated,
.dash-footer-tasks .col-action { width: 1%; white-space: nowrap; }
.dash-footer-tasks .col-report { width: 42%; min-width: 0; }
.dash-footer-tasks .col-note,
.dash-footer-tasks td.col-note.is-empty {
  width: 28%;
  min-width: 0;
  text-align: left;
  overflow-wrap: anywhere;
  word-break: break-word;
}

/* The parser-blocking helper restores/reveals this list before its first
   visible frame. `visibility` preserves final geometry while preventing a
   default-top flash. */
.charts-index__list[data-scroll-pending] { visibility: hidden; }

/* Shared Dashboard record-row primitive (P5 Round 6): multiline info block
   stays stacked; the whole block and the row action are centered on the
   full row height — not baseline-aligned to the metadata line. */
.record-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}
.record-row__body { min-width: 0; flex: 1 1 auto; }
.record-row__action { flex: 0 0 auto; align-self: center; }


/* 近期处理 empty-state cell: keep content centered in the row. */
.data-table td.is-empty,
.data-table tr.is-empty-row td {
  vertical-align: middle;
  text-align: center;
}

/* Dedicated metrics surface (P5 §11) — compact browse list + one full detail card. */
.charts-layout {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-5);
  align-items: start;
}

@media (min-width: 1100px) {
  .charts-layout { grid-template-columns: minmax(220px, 280px) 1fr; }
}

/* P5 Round 6 (owner re-check, Part C): the left sidebar previously stacked
   its toolbar/list/note children via each child's own ad hoc margin-top,
   while the right workspace already used flex + a shared gap
   (.charts-detail-col below). Two different spacing mechanisms made the
   two columns' toolbar-to-content gap only coincidentally close, not
   guaranteed equal, so the first metric row and the chart card started at
   different Y positions. Putting BOTH columns on the SAME mechanism --
   flex column + one shared gap, no per-child margin-top -- guarantees the
   list and the chart card start at the same row once their toolbar rows
   share the same height (both search-input/sort-icon and range-picker
   controls already share a 28px min-height). */
.charts-index {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.charts-index__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.charts-index__tools {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.charts-index__search { flex: 1; min-width: 0; }

.charts-index__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  overflow: hidden;
  max-height: 70vh;
  overflow-y: auto;
}

.charts-index__item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  background: var(--surface);
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-13);
  color: var(--text);
  text-decoration: none;
}

.charts-index__item:hover { background: var(--surface-sunken); }
.charts-index__item.is-active { background: var(--accent-weak); color: var(--accent); font-weight: 500; }

.charts-index__name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.charts-index__unit { font-size: var(--text-11); color: var(--text-faint); }
.charts-index__count { font-size: var(--text-11); color: var(--text-faint); }

.charts-index__singleton-mark {
  margin-left: 1px;
  color: var(--text-faint);
}

.charts-index__note {
  margin: 0;
  padding: 0 var(--space-1);
  font-size: var(--text-11);
  color: var(--text-faint);
}

/* Right column (P5 Owner UI Round 3 §9): its own toolbar row sits at the same
   grid row as the left search/sort toolbar, with deliberate space before the
   chart card below — never visually attached to the chart border. */
.charts-detail-col {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
}

.charts-detail__toolbar {
  display: flex;
  justify-content: flex-end;
}

.charts-detail .chart-card { min-height: 360px; }
.charts-detail .chart-canvas-slot { height: 320px; }

.measure-detail { position: fixed; top: 0; right: 0; width: min(360px, 100%); height: 100%; background: var(--color-canvas); border-left: 1px solid var(--color-hairline); padding: 24px; overflow: auto; z-index: 50; }
.measure-detail dl { display: grid; grid-template-columns: 110px 1fr; gap: 8px 12px; }
.measure-detail dt { color: var(--color-ink-muted-48); font-size: 13px; }
.measure-detail dd { margin: 0; font-size: 14px; }
.timeline { list-style: none; padding: 0; margin: 0; }
.timeline li { display: flex; flex-wrap: wrap; gap: 10px 16px; padding: 10px 0; border-bottom: 1px solid var(--color-divider-soft); align-items: center; }
.badge { display: inline-block; font-size: 12px; color: var(--color-error); border: 1px solid currentColor; border-radius: 4px; padding: 1px 6px; }
/* P7 document-table redesign: Source Reports as a restrained document
   browser (structural reference only, no borrowed branding/colors) — a
   dominant title column plus muted secondary columns, not Timeline's
   .obs-row event anatomy. Header and rows share ONE grid-template-columns
   declaration so cells always line up; .doc-row__meta uses `display:
   contents` at desktop so its three children act as three independent grid
   columns, then becomes one merged, muted line at mobile (§ media query
   below) — same three <span> elements, no markup duplication. */
/* P7 table-controls round: outer border/radius/background come straight
   from the shared `.table-shell` (same primitive 指标概览 uses) — .doc-table
   only adds the grid, no separate surface styling. Header/rows share ONE
   grid-template-columns so cells line up; sort (`.col-sort-btn`, reused
   verbatim from the Metrics table) and column filters
   (`column_filter_picker()`, the same `.range-picker--compact` shape as
   Metrics' 时间跨度 column filter) are real GET links/popovers, not
   client-side table_controls.js state — Source Reports needs reproducible
   URLs and Back-context restoration, Metrics' own JS-driven sort/filter
   does not apply here. */
/* Owner fix §5: .doc-table itself carries NO padding — outer border/radius
   is `.table-shell`'s job, horizontal breathing room is each row/head's OWN
   padding, so a row's hover/focus background (painted on .doc-row__link,
   which spans the full width) reaches the shell's inner edges exactly like
   a real <table>'s <tr> background does, instead of stopping short inside
   an outer padded wrapper. */
.doc-table__head,
.doc-row__link {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(72px, 0.8fr) minmax(64px, 0.7fr) minmax(72px, 0.7fr) minmax(92px, 0.9fr);
  align-items: center;
  gap: var(--space-3);
}

/* Mobile round: `.doc-table__group` (head) / `.doc-row__doc` (body, below)
   are `display: contents` at desktop — their children keep acting as the
   SAME independent desktop grid columns above, unchanged. At <=640px they
   become the actual 3-column grid cells instead (§ media query below) —
   one semantic DOM tree, no duplicated report data. */
@media (min-width: 641px) {
  .doc-table__group { display: contents; }
}

/* Owner fix §1: restore normal header breathing room — matches the
   generic `th, td { padding: var(--space-2) var(--space-3); }` table
   convention used elsewhere (Metrics included) instead of a 0px top. */
.doc-table__head {
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--line-strong);
  font-size: var(--text-12);
  font-weight: 600;
  color: var(--text-muted);
}

/* Active-sort color reuses the exact Metrics convention/values
   (`th[data-col].is-sorted-*`) — restated against this div-based header
   instead of a literal <th> (no ARIA table role is claimed, since a table
   row here would need to itself be a real link). Active-FILTER indication
   is the canonical `.sort-picker.is-filter-active` trigger treatment
   (column_filter_picker()) — no separate `.th-label` color rule needed. */
.doc-table__col.is-sorted-asc .col-sort-btn,
.doc-table__col.is-sorted-desc .col-sort-btn { color: var(--accent); font-weight: 700; }

/* Owner fix §2C: the filter/sort popovers must overlay the table body, not
   be clipped by it. `.table-shell` defaults to `overflow: auto` (needed
   elsewhere for horizontal/vertical table scrolling), which would clip an
   open popover once it extends past the shell's own content height —
   scoped to `.doc-table` only, since no other `.table-shell` usage needs
   this override. */
.table-shell.doc-table { overflow: visible; }

.doc-table__body { list-style: none; margin: 0; padding: 0; }

.doc-row { border-bottom: 1px solid var(--line); }
.doc-row:last-child { border-bottom: 0; }

.doc-row__link {
  padding: var(--space-3);
  color: inherit;
  text-decoration: none;
}

.doc-row:hover .doc-row__link,
.doc-row__link:focus-visible {
  background: var(--accent-weak);
}

.doc-row__link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.doc-row__doc { display: contents; }

.doc-row__title {
  min-width: 0;
  font-size: var(--text-13);
  font-weight: 400;
  color: var(--text);
  overflow-wrap: anywhere;
}

.doc-row__title mark { background: var(--accent-weak); }

.doc-row__meta { display: contents; }

.doc-row__type,
.doc-row__process {
  font-size: var(--text-12);
  color: var(--text-muted);
}

/* Polish fix (§3/§4): semantic text treatment is a VALUE-based class, not
   a viewport-based one, so 桌面/移动端 always mean the same thing for the
   same status. Reuses ONLY existing tokens — no new colors:
   已处理 = default muted (no extra class); 待处理 = --st-limit (the same
   "worth noting, not urgent" caveat token already used for limitation
   notices); 处理失败 = --st-qc (the app's existing error/danger token,
   already `--color-error` and `.error`'s color); 待确认 = --st-review
   (already the review-attention token used elsewhere, e.g. Timeline's
   .tl-card__warning); 无需确认 = --text-faint (restrained default, not
   --text-muted, so it visibly recedes relative to an active status). */
.doc-row__process--pending { color: var(--st-limit); }
.doc-row__process--failed { color: var(--st-qc); }
.doc-row__review { font-size: var(--text-12); color: var(--text-faint); }
.doc-row__review--pending { color: var(--st-review); }

.doc-row__time { display: flex; flex-direction: column; gap: 2px; }
.doc-row__date { font-size: var(--text-12); color: var(--text-muted); }
.doc-row__date--pending { color: var(--text-faint); }
/* 使用文件日期 flags that the shown date falls back to file metadata
   instead of a clinical date — a caveat worth noticing, not an error, so
   it reuses the SAME --st-limit token as 待处理 rather than --text-faint. */
.doc-row__hint { font-size: var(--text-11); color: var(--st-limit); }

/* Mobile round: Source Reports stays visibly a TABLE at <=640px — header
   never hidden, 3 columns (报告 | 状态 | 报告日期), header/body share the
   SAME grid geometry so values align under their headings. */
@media (max-width: 640px) {
  .doc-table__head,
  .doc-row__link {
    grid-template-columns: minmax(0, 1fr) minmax(60px, auto) minmax(76px, auto);
    gap: var(--space-2);
    align-items: start;
  }

  .doc-row__link { padding: var(--space-2); }

  .doc-row__doc,
  .doc-row__meta {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
  }

  .doc-row__title {
    font-size: var(--text-13);
    font-weight: 400;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
  }

  .doc-row__type { font-size: var(--text-11); }
  .doc-row__process { font-size: var(--text-11); }
  .doc-row__review { font-size: var(--text-11); }

  .doc-row__date { font-size: var(--text-12); font-variant-numeric: tabular-nums; }
  .doc-row__hint { font-size: var(--text-11); }
}

/* Owner fix §1: page title stands alone; the count reuses the exact
   `.charts-index__head`/`.section-meta` primitive Metrics uses above its
   own search row. `.cases-count-row` only adds the vertical breathing room
   between the page-head and the count — additive, so it never touches
   `.charts-index__head` itself (Metrics' own spacing is untouched). */
.cases-count-row { margin-top: var(--space-4); }

/* Owner fix §3: search reuses `.charts-index__search` (same input, same
   flex:1 sizing) inside a real <form> — `display: contents` lets that
   input join the SAME `.charts-index__tools` flex row as the sibling
   range_picker()/reset, exactly like Metrics' search + sort_picker pair,
   with no nested-form concerns. `.source-controls` only adds wrapping for
   the extra controls Metrics' own row doesn't carry. */
.source-search { display: contents; }
.source-controls { flex-wrap: wrap; }

/* Layout invariant fix: this gap must hold regardless of how many rows
   `.source-controls` wraps into (one row, or 排序/时间范围 + a wrapped
   重置排序/筛选 row) — it belongs to the sibling relationship between the
   whole controls block and the table, not to any one control inside it, so
   it is no longer scoped to a desktop-only breakpoint. */
.source-controls + .doc-table {
  margin-top: var(--space-3);
}

/* Polish fix: heading aligns with the option text's own left inset
   (`.sort-picker__option { padding: var(--space-1) var(--space-3); }`) via
   matching horizontal MARGIN (so the divider border below, which follows
   the margin-box width, lines up with that same inset too, never running
   to the panel's outer edge); normal weight, same restrained/faint
   treatment as other secondary metadata (e.g. the `203 份报告` count). One
   divider sits directly under EACH heading — the gap before the NEXT
   heading (its own margin-top) is what separates the two groups, so there
   is no second, redundant divider between them. */
.range-picker__group-label {
  margin: var(--space-2) var(--space-3) 4px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--line);
  font-size: var(--text-11);
  font-weight: 400;
  color: var(--text-faint);
}
.range-picker__group-label:first-child { margin-top: 0; }

.sort-picker__group-list { list-style: none; margin: 0; padding: 0; }

/* Mobile round §8/§9: ONE doc_sort_picker element — hidden at desktop
   (column-header ⇅ icons remain the sole desktop sort UI, unchanged),
   visible at <=640px as the second control-row item so it wraps onto its
   own row below the full-width search input alongside 时间范围/重置. */
.source-controls__mobile-sort { display: none; }

@media (max-width: 640px) {
  .source-controls .charts-index__search { flex-basis: 100%; }
  .source-controls__mobile-sort { display: contents; }
  /* Match Dashboard's already-correct narrow-width containment fix (see
     `.section-actions .range-picker { position: static; }` +
     `.section-head { position: relative; }` above): the panel is
     `position: absolute`, so anchoring it to the narrow `<details>`
     trigger itself (the default `.range-picker { position: relative; }`)
     just flips which trigger-relative edge it can overflow past once the
     trigger sits mid-row instead of at either row edge. Promoting
     `.source-controls` (the full-width control row, same primitive as
     Dashboard's `.section-head`) to the positioned ancestor instead lets
     the panel's own base `left: 0` anchor to that wide row — which cannot
     itself overflow the viewport — so a capped max-width is then enough to
     keep it fully contained on both sides. Scoped to this control row only,
     so Dashboard/Charts/tables' own range pickers are untouched. */
  .source-controls { position: relative; }
  .source-controls > .range-picker { position: static; }
  .source-controls > .range-picker > .range-picker__panel {
    max-width: calc(100vw - 2 * var(--space-4));
  }
}

/* Mobile round §6/§7: table header stays visible at <=640px with 3 groups
   (报告/状态/报告日期). No sort icons in the mobile header (single
   doc_sort_picker above owns sorting there) — only the filter triggers
   remain. `.doc-table__group--status` swaps its two desktop cells for one
   combined status_filter_picker() trigger; `.doc-table__group--report`
   keeps 类型's filter icon but drops its own redundant text label, since
   the group is now represented by the single "报告" label. */
.doc-table__mobile-only { display: none; }

@media (max-width: 640px) {
  .doc-table__group {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    min-width: 0;
  }
  .doc-table__group .col-sort-btn { display: none; }
  .doc-table__group--report .doc-table__sub-label { display: none; }
  .doc-table__group--status .doc-table__col { display: none; }
  .doc-table__mobile-only {
    display: flex;
    align-items: center;
    gap: var(--space-1);
  }

  /* Popover edge safety (§11): the leftmost (报告/类型) trigger stays
     anchored from the left so it can't push past the left viewport edge;
     the rightmost (报告日期) trigger already opens from the right
     (`.sort-picker__panel { right: 0; }`, unchanged) so it stays inside
     the right edge. Both get a viewport-relative max-width. */
  .doc-table__group--report .sort-picker__panel {
    left: 0;
    right: auto;
  }
  .doc-table__head .sort-picker__panel,
  .source-controls__mobile-sort .range-picker__panel {
    max-width: calc(100vw - 2 * var(--space-4));
  }
}

.case-tabs { display: none; gap: 8px; margin: 16px 0; }
.case-tab { border: 1px solid var(--color-hairline); background: var(--color-canvas); border-radius: var(--rounded-pill); padding: 8px 14px; }
.case-tab.is-active { background: var(--color-ink); color: #fff; }
.case-split { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; min-height: 60vh; }
.case-pane { min-width: 0; }
.source-image, .source-frame { width: 100%; max-height: 75vh; object-fit: contain; border: 1px solid var(--color-hairline); border-radius: var(--rounded-md); background: #fff; }
.source-frame { height: 75vh; }
.transcript-page + .transcript-page { margin-top: 20px; }
.transcript-page__text { max-width: 100%; max-height: 75vh; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
.drop-zone { display: block; border: 2px dashed var(--color-hairline); border-radius: var(--rounded-lg); padding: 36px 16px; text-align: center; background: var(--color-canvas); cursor: pointer; }
.drop-zone.is-dragover { border-color: var(--color-primary); background: #f0f7ff; }
.drop-zone input[type=file] { display: block; margin: 12px auto 0; }
.form-stack { display: grid; gap: 14px; max-width: 560px; }
.form-stack label, .filters label { display: grid; gap: 6px; font-size: 14px; }
.form-stack input, .form-stack select, .filters input, .filters select { padding: 10px 12px; border: 1px solid var(--color-hairline); border-radius: var(--rounded-sm); font: inherit; }
.check-row { display: flex !important; align-items: flex-start; gap: 8px; grid-template-columns: none; }
.confirm-box.is-attention { outline: 2px solid var(--color-error); padding: 10px; border-radius: var(--rounded-sm); }
.chip-check { display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; border: 1px solid var(--color-hairline); border-radius: var(--rounded-pill); margin: 4px; font-size: 13px; }
.chip-row--wrap { flex-wrap: wrap; }
.table-shell--matrix { overflow-x: auto; }
.matrix-table .sticky-col { position: sticky; left: 0; background: var(--color-canvas); z-index: 1; }
.settings-form { max-width: 640px; }

@media (max-width: 900px) {
  .case-tabs { display: flex; }
  .case-split { display: block; grid-template-columns: none; }
  .case-pane { display: none !important; }
  .case-pane.is-active { display: block !important; }
}

@media (min-width: 901px) {
  .case-tabs { display: none; }
  .case-pane { display: block !important; }
  .case-pane[hidden] { display: block !important; }
}

/* —— P6 Clinical Timeline —— */
/* Horizontal Clinical Timeline Navigator (P6 Phase 1) — year/month
   hierarchical axis + true-elapsed-time daily activity waveform, with the
   selected-range overlay drawn directly on the waveform (not a separate
   slider strip). Every layer (axis segments, bars, dim/selection overlay,
   range-input thumbs) shares one coordinate system: a percentage of
   `span_days`, computed server-side (views_data._build_timeline_navigator) —
   never client-side Date.parse() or equal-spacing-by-index. */
.tl-nav {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  /* Extra bottom padding clears the longer handle tips (top/bottom: -12px). */
  padding: var(--space-3) var(--space-3) var(--space-5);
  margin-bottom: var(--space-5);
  /* Pointer-drag min pill separation: center gap = grip width + edge gap. */
  --tl-nav-grip-w: 6px;
  --tl-nav-grip-gap: 2px;
}

.tl-nav.is-panning,
.tl-nav.is-panning * { cursor: grabbing; }

/* Year reads as the coarser tier (spans its own extent with an underline),
   month as the finer tier (a boundary tick at its true-time start). Both are
   lighter than the waveform below them (P6 §10). */
.tl-nav__axis {
  position: relative;
  height: 16px;
  margin-bottom: 1px;
}

.tl-nav__axis-seg {
  position: absolute;
  top: 0;
  height: 100%;
  overflow: hidden;
  white-space: nowrap;
  font-size: var(--text-11);
  line-height: 15px;
  color: var(--text-faint);
  box-sizing: border-box;
}

.tl-nav__axis--year .tl-nav__axis-seg {
  font-weight: 600;
  color: var(--text-muted);
  border-bottom: 1px solid var(--line);
}

.tl-nav__axis--month .tl-nav__axis-seg {
  border-left: 1px solid var(--line);
  padding-left: 4px;
}

/* Right-aligned action group: order always
   定位所选范围 → 重置视图 → 清除范围; [hidden] removes from flex. */
.tl-nav__actions {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  z-index: 5;
  display: flex;
  flex-direction: row;
  justify-content: flex-end;
  align-items: center;
  gap: var(--space-2);
  pointer-events: none;
}

.tl-nav__action {
  pointer-events: auto;
  min-height: 22px;
  height: 22px;
  padding: 0 var(--space-2);
  font-size: var(--text-11);
}

/* Clip axis labels to navigator content bounds without clipping handles/tooltips.
   Axes share the same pan surface as the waveform (grab + no text selection). */
.tl-nav__axes {
  overflow: hidden;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  touch-action: pan-y;
}

.tl-nav__axis--day {
  height: 16px;
}

.tl-nav__axis--day .tl-nav__axis-seg--day {
  border-left: 1px solid var(--line);
  padding-left: 2px;
  font-variant-numeric: tabular-nums;
  font-size: var(--text-11);
  line-height: 15px;
}

/* Waveform strip */
.tl-nav__stage {
  position: relative;
  height: 52px;
  margin: var(--space-5) 0 0;
  border-bottom: 1px solid var(--line);
  overflow: visible;
  outline: none;
  cursor: grab;
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
}

.tl-nav__plot {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}

.tl-nav__plot .tl-nav__bar { pointer-events: auto; }

/* Dims the waveform OUTSIDE the selection, so it must sit above the bars
   (z 2) but below the handles (z 4). */
.tl-nav__dim {
  position: absolute;
  top: 0;
  bottom: 0;
  background: color-mix(in srgb, var(--surface) 68%, transparent);
  pointer-events: none;
  z-index: 3;
}

.tl-nav__dim--start { left: 0; }
.tl-nav__dim--end { right: 0; }

/* Selection is expressed by dimming what is outside it (P6 §6/§15), never
   by drawing a bordered rectangle over the waveform. Kept in the DOM as the
   geometry carrier for the handles. */
/* Selected-range band: same --chart-band for FULL DOMAIN and partial range;
   geometry alone changes (full = entire stage; partial = between handles).
   Outside dim remains a separate layer and is empty at full domain. */
.tl-nav__selection {
  position: absolute;
  /* Vertically taller than the bars so the band reads as a time window;
     left/width remain the continuous handle coordinates (unchanged). */
  top: -8px;
  bottom: -8px;
  background: var(--chart-band);
  pointer-events: none;
  z-index: 1;
}

/* Visible boundary = thin accent line spanning the stage plus a compact grip;
   the surrounding box is the transparent pointer/touch target (P6 §2/§3).
   Positioned from the CONTINUOUS domain coordinate, so at full range the line
   sits exactly on the stage edges instead of a thumb-width inset (P6 §5). */
.tl-nav__handle {
  position: absolute;
  /* Extend the thin boundary ~9px past the stage so the line reads past the
     waveform without enlarging the capsule grip (still centered at 50%). */
  top: -12px;
  bottom: -12px;
  width: 18px;
  transform: translateX(-50%);
  background: none;
  cursor: ew-resize;
  touch-action: none;
  z-index: 4;
}

.tl-nav__handle::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: 2px;
  margin-left: -1px;
  background: var(--accent);
}

.tl-nav__grip {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--tl-nav-grip-w);
  height: 14px;
  margin: -7px 0 0 calc(var(--tl-nav-grip-w) / -2);
  background: var(--accent);
  border-radius: 3px;
}

/* Keyboard/programmatic q0==q1 fallback only (pointer drag never reaches
   pill overlap). Lines share semantic left%; hit boxes split L/R. */
.tl-nav__handle--start.is-coincident {
  transform: translateX(-100%);
  z-index: 5;
}
.tl-nav__handle--start.is-coincident::before {
  left: auto;
  right: 0;
  margin-left: 0;
}
.tl-nav__handle--start.is-coincident .tl-nav__grip {
  left: auto;
  right: 2px;
  margin-left: 0;
}

.tl-nav__handle--end.is-coincident {
  transform: translateX(0);
  z-index: 5;
}
.tl-nav__handle--end.is-coincident::before {
  left: 0;
  margin-left: 0;
}
.tl-nav__handle--end.is-coincident .tl-nav__grip {
  left: 2px;
  margin-left: 0;
}

/* Focus is projected from the (visually absent) native input onto its own
   handle, so only that one boundary lights up — never the whole navigator.
   The inputs precede the handles inside .tl-nav__stage, so `~` matches. */
[data-tl-range-start]:focus-visible ~ .tl-nav__handle--start::before,
[data-tl-range-end]:focus-visible ~ .tl-nav__handle--end::before {
  width: 3px;
  margin-left: -1.5px;
}

[data-tl-range-start]:focus-visible ~ .tl-nav__handle--start .tl-nav__grip,
[data-tl-range-end]:focus-visible ~ .tl-nav__handle--end .tl-nav__grip {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 35%, transparent);
}

/* One bar per ACTIVE day only (P6 §6/§8) — height encodes ONLY
   sqrt(event_count) normalized against the full-domain max, never
   renormalized by the current selection. Horizontal position is a
   server-computed true-elapsed-time percentage. */
/* The button is a transparent, comfortably wide hit target (P6 §8); the
   visible waveform bar is its ::before, narrow and bottom-aligned. */
.tl-nav__bar {
  position: absolute;
  bottom: 0;
  top: 0;
  width: 13px;
  transform: translateX(-6.5px);
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  min-width: 0;
  cursor: pointer;
  appearance: none;
  z-index: 2;
}

.tl-nav__bar::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 0;
  width: 5px;
  margin-left: -2.5px;
  min-height: 3px;
  height: calc(4px + var(--tl-h, 0) * 46px);
  background: var(--text-muted);
  border-radius: 2px 2px 0 0;
}

.tl-nav__bar:hover::before,
.tl-nav__bar:focus-visible::before {
  background: var(--accent);
}

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

/* Two overlapping native range inputs (P6 §10) — native keyboard/ARIA
   semantics, but visually integrated: the track is fully transparent and
   ignores pointer events, only the styled thumb (the actual handle) is
   interactive, so it reads as one boundary marker sitting on the waveform
   rather than a generic separate slider. */
.tl-nav__range {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  background: transparent;
  appearance: none;
  pointer-events: none;
  z-index: 4;
}

.tl-nav__range::-webkit-slider-runnable-track { background: transparent; }
.tl-nav__range::-moz-range-track { background: transparent; border: none; }

/* The native inputs stay for keyboard/ARIA only: invisible, non-interactive,
   and with NO whole-element focus ring (that ring was the blue rectangle
   around the entire waveform). Focus is shown on the matching custom handle
   instead — see .tl-nav__handle.is-focused (P6 §4). */
.tl-nav__range::-webkit-slider-thumb {
  appearance: none;
  width: 1px;
  height: 1px;
  border: none;
  background: transparent;
}

.tl-nav__range::-moz-range-thumb {
  width: 1px;
  height: 1px;
  border: none;
  background: transparent;
}

.tl-nav__range:focus,
.tl-nav__range:focus-visible {
  outline: none;
  border-color: transparent;
}

/* Timeline navigator tooltip: same shared surface as `.chart-tooltip`; only
   positioning/hit-testing differ (hover-only, above the bar). Row hierarchy
   reuses `.chart-tooltip__row*`. */
.tl-nav__tooltip {
  position: absolute;
  z-index: 10;
  max-width: 280px;
  pointer-events: none;
  transform: translateY(-100%);
}

/* P6 Phase 3 — date -> source/report card grid (replaces the obsolete
   one-event-one-card vertical presentation). Chronology is CONTENT-spaced
   (§22): a fixed inter-group gap, never scaled by actual elapsed days —
   true elapsed-time distance is the horizontal navigator's job. */
.tl-chronology { list-style: none; margin: 0; padding: 0; }

.tl-date-group {
  display: grid;
  grid-template-columns: minmax(5.5rem, 6.5rem) 20px minmax(0, 1fr);
  column-gap: var(--space-3);
  padding-top: var(--space-5);
  /* P6 Phase 3 owner round 3 §4-§6: day-click scrolls this element to the
     viewport's geometric top via scrollIntoView({block:"start"}), but the
     sticky .app-shell header (min-height 48px) then covers that top strip —
     scroll-margin-top reserves that space plus a small breathing margin so
     the date label/node land BELOW the sticky header, not under it. This is
     a layout-native fix; scrollToDay() itself is unchanged. */
  scroll-margin-top: calc(48px + var(--space-4));
}

.tl-chronology > .tl-date-group:first-child { padding-top: 0; }

/* P6 Phase 3 owner round 1 §4/§5, round 3 §7/§8: date is a first-level
   structural heading, right-aligned toward the node. line-height is set
   explicitly (not left to the browser default) so the node's margin-top
   below can be DERIVED from it rather than guessed: label vertical center
   = padding-top(2px) + line-height/2(10px) = 12px from the row's top edge. */
.tl-date-label {
  grid-column: 1;
  padding-top: 2px;
  line-height: 20px;
  font-size: var(--text-15);
  font-weight: 600;
  color: var(--text);
  text-align: right;
  white-space: nowrap;
}

.tl-date-rail {
  grid-column: 2;
  position: relative;
  display: flex;
  justify-content: center;
  align-items: flex-start;
}

/* One continuous line: each group's segment overlaps into the gap above it
   so the rail reads as unbroken, not per-group dashes. */
.tl-date-rail::before {
  content: "";
  position: absolute;
  top: calc(-1 * var(--space-5));
  bottom: calc(-1 * var(--space-5));
  width: 2px;
  background: var(--line-strong);
}

.tl-chronology > .tl-date-group:first-child .tl-date-rail::before { top: 0; }

/* The rail must span the FULL rendered height of its date group, including
   the last one (P6 Phase 3 round 4 §B). The previous `bottom: 50%` cut the
   line at the vertical middle of the group, which on a tall final day left
   most of the cards with no rail beside them. The last VISIBLE group ends
   the line flush with its own bottom instead of overhanging into the gap.
   [data-tl-last-visible] is maintained by timeline.js because selection
   filtering hides groups without changing DOM order, so :last-child alone
   would mark a hidden element; the :not([hidden]) form is the no-JS
   fallback. */
.tl-chronology > .tl-date-group:last-child:not([hidden]) .tl-date-rail::before,
.tl-chronology > .tl-date-group[data-tl-last-visible] .tl-date-rail::before { bottom: 0; }

/* P6 Phase 3 owner round 3 §7/§8: node center DERIVED from .tl-date-label's
   declared box model (global box-sizing: border-box, so this 10px node's
   own box height is exactly 10px including its border) — not a guessed
   pixel offset. Label center = 12px from row top (see .tl-date-label
   comment above); this node's own center = margin-top + 10/2. Solving
   margin-top + 5 = 12 gives margin-top = 7px. If either the label's
   padding-top/line-height or this node's width/height ever change, this
   margin-top must be recomputed from the same two formulas — never
   re-guessed by eye. */
.tl-date-node {
  position: relative;
  z-index: 1;
  width: 10px;
  height: 10px;
  margin-top: 7px;
  border-radius: 50%;
  background: var(--surface);
  border: 2px solid var(--accent);
}

/* P6 Phase 3 owner round 1 §3: auto-fill (not auto-fit) with a fixed max
   keeps cards at a compact "report card" width instead of stretching a
   sparse row's cards to fill the remaining page width — unused tracks stay
   empty rather than being removed, so cards pack from the left. */
.tl-source-grid {
  grid-column: 3;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 18rem), 22rem));
  justify-content: start;
  gap: var(--space-3);
  align-items: start;
}

/* P6 Phase 3 round 4 §A: `grid-template-rows: masonry` is NOT supported by
   any current mainstream engine (verified in-browser: CSS.supports(...) is
   false in Firefox, Safari and Chromium-family), so the old @supports block
   never applied and the grid stayed row-locked — every card in a row padded
   to the tallest card. Real masonry is therefore done by timeline.js, which
   reads the column tracks the rules above already resolved and absolutely
   positions each card into the currently-shortest column, then writes the
   measured height back onto the container so the date-group row (and its
   rail) still sizes correctly. The declarations above remain the single
   source of truth for column count/width and stay in force verbatim when
   JS is unavailable or the grid is single-column. */
.tl-source-grid[data-tl-masonry] {
  position: relative;
  display: block;
}

.tl-source-grid[data-tl-masonry] > * {
  position: absolute;
  top: 0;
  left: 0;
  margin: 0;
}

/* Card visual language (P6 Phase 3 §15): a persistent, more spacious
   version of the approved shared tooltip surface (.chart-tooltip /
   .tl-nav__tooltip) — same background/border/radius/text tokens, but NO
   elevation/shadow and no floating-overlay feel, since this is a page
   surface, not a transient overlay. */
.tl-card {
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-control);
  padding: var(--space-3);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
  cursor: pointer;
}

/* P6 Phase 3 owner round 1 §9/§10: the card face itself is the inspector
   trigger (data-inspect-open on the .tl-card); reuse the SAME restrained
   hover/focus affordance already accepted for .series-index__row.is-inspectable
   — a background tint, no shadow/elevation/scale. */
.tl-card:hover,
.tl-card:focus-visible {
  background: var(--accent-weak);
  outline: none;
}

.tl-card__head { display: flex; flex-direction: column; gap: 2px; }

.tl-card__heading {
  margin: 0;
  font-size: var(--text-13);
  font-weight: 600;
  color: var(--text);
  overflow-wrap: anywhere;
}

.tl-card__facts {
  list-style: none;
  margin: 0;
  padding: var(--space-2) 0 0;
  border-top: 1px solid var(--line);
  font-size: var(--text-12);
  color: var(--text);
}

.tl-card__facts li { padding: 1px 0; overflow-wrap: anywhere; }
.tl-card__facts-more { color: var(--text-faint); }

/* P6 Phase 3 owner round 2 §8-§10: compact label/value row, a lighter
   relative of the drawer's summary table — no cell borders, no grid lines.
   A label-only fact (no separable value) simply has one child, so it sits
   at the row's start with nothing to justify against. */
.tl-card__fact {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-2);
}

.tl-card__fact-label { min-width: 0; overflow-wrap: anywhere; }

/* Responsive fix: flex:0 0 auto refused to shrink, so a long metric name
   or unit-bearing value could escape the card at mobile width — allow the
   value to shrink and wrap like the label already does. */
.tl-card__fact-value {
  flex: 0 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
  color: var(--text-muted);
  text-align: right;
}

/* Imaging-presentation fix: a finding is narrative CONTENT, not a
   label|value pair — normal word wrapping (never overflow-wrap:anywhere,
   which is what squeezed a short repeated exam name into a vertical
   single-character column beside a long finding). A shared exam CONTEXT
   renders once as a small kicker line, not repeated per finding. */
.tl-card__fact-content { overflow-wrap: normal; word-break: normal; }
.tl-card__fact-context {
  overflow-wrap: normal;
  word-break: normal;
  color: var(--text-faint);
  font-size: var(--text-11);
  padding-top: var(--space-2);
  border-top: 1px solid var(--line);
}
.tl-card__fact-context:first-child { padding-top: 0; border-top: none; }

.tl-card__foot { display: flex; flex-direction: column; gap: var(--space-2); margin-top: auto; }
.tl-card__meta { margin: 0; font-size: var(--text-11); color: var(--text-faint); }
.tl-card__meta-sep { margin: 0 4px; }
.tl-card__warning { color: var(--st-review); }

/* P6 Phase 3 owner round 1 §8: no more 详情 button sharing this row — just
   the single real 打开源报告 action, aligned to the end. */
.tl-card__actions { display: flex; align-items: center; justify-content: flex-end; gap: var(--space-2); }

.tl-unassociated { margin-top: var(--space-6); }
.tl-unassociated > summary {
  cursor: pointer;
  font-size: var(--text-13);
  font-weight: 600;
  color: var(--text-muted);
  padding: var(--space-2) 0;
}
.tl-unassociated > .table-controls-empty {
  padding-left: 0;
  padding-right: 0;
}

@media (max-width: 640px) {
  /* Existing 44px touch-target convention (P6 §19) for the stage/handles. */
  .tl-nav__stage { height: 44px; }
  .tl-nav__bar::before { height: calc(4px + var(--tl-h, 0) * 38px); }
  .tl-nav__handle { width: 26px; }
  /* Narrower date column, single-column card grid (P6 Phase 3 §20/§21). */
  .tl-date-group { grid-template-columns: minmax(4.5rem, 5.5rem) 20px minmax(0, 1fr); }
  .tl-source-grid { grid-template-columns: 1fr; }
}

/* Responsive fix: the two-row .app-shell__top (see .app-shell__nav above)
   now switches in below 900px, not 640px — adds a full nav row (same 48px
   .app-shell__nav-link height as desktop) plus its own vertical padding
   beneath the branding/actions row, so the sticky header is taller here
   than on desktop. scroll-margin-top must grow by that same amount, over
   this SAME width range, or the date label/node lands under the taller
   header. */
@media (max-width: 900px) {
  .tl-date-group { scroll-margin-top: calc(48px + 48px + var(--space-4)); }
}

/* —— 待确认：任务列表与裁定详情 —— */

/* ---------------------------------------------------------------------------
   Review Workspace (§B/§C/§E/§N). Three intentionally different density
   levels share the application's existing visual language rather than a new
   design system: the Dashboard preview stays a plain `.data-table`, /reviews
   reuses the Source Reports `.doc-table` document-browser primitive verbatim
   (only its column geometry differs), and the detail page is a stacked
   evidence/decision surface built from the same `--surface`/`--line`/
   `--r-card` tokens. No navbar, Source Reports, Timeline, charts, Shared
   Inspector or `.series-index` rule is touched.
   ------------------------------------------------------------------------ */

/* Dashboard preview: the whole row navigates, but a table row cannot itself
   be an <a>, so the single real focusable link in 涉及内容 is stretched over
   the row — keyboard focus and the accessible name stay on that one <a>. */
.dash-footer-reviews .dash-review-row { position: relative; }
.dash-review-row__link { color: inherit; text-decoration: none; font-weight: 400; }
.dash-review-row__link::after { content: ""; position: absolute; inset: 0; }
.dash-review-row:hover { background: var(--accent-weak); }
.dash-review-row__link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Two columns: the source report gets the dominant width (it is the row's
   identity here), the choice summary stays a compact muted trailing cell. */
.dash-footer-reviews .col-source { width: auto; min-width: 0; }
.dash-footer-reviews .col-choice { width: 1%; white-space: nowrap; color: var(--text-muted); }
.dash-footer-reviews .dash-review-row__link {
  display: block;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* /reviews: same head/row grid contract as `.doc-table`, six columns. */
.doc-table--reviews .doc-table__head,
.doc-table--reviews .doc-row__link {
  grid-template-columns: minmax(72px, 0.7fr) minmax(0, 2fr) minmax(92px, 1.1fr) minmax(0, 1.3fr) minmax(0, 1.6fr) minmax(92px, 0.9fr);
  align-items: start;
}

.doc-table--reviews .doc-row__link { align-items: center; }

.rev-cell { min-width: 0; display: flex; flex-direction: column; gap: 2px; font-size: var(--text-13); }
/* Row hierarchy: 待确认事项 (task) and 需要确认 (action) are primary; 涉及内容 is normal-weight context. */
.rev-cell--category,
.rev-cell--choice .rev-cell__primary { color: var(--text); }
.rev-cell--affected .rev-cell__primary { font-weight: 400; }
.rev-cell--source .rev-cell__primary {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rev-cell__sub { font-size: var(--text-11); color: var(--text-faint); }
.rev-cell__sub--review { color: var(--st-review); }
.rev-cell__sub--error { color: var(--st-qc); }
.rev-cell__saved { font-size: var(--text-11); color: var(--st-verified); }

@media (max-width: 640px) {
  /* Two mobile columns: the task itself, then its source/date — the same
     "stay a table, merge the muted columns" convention Source Reports uses. */
  .doc-table--reviews .doc-table__head,
  .doc-table--reviews .doc-row__link {
    grid-template-columns: minmax(0, 1fr) minmax(96px, auto);
    gap: var(--space-2);
  }

  .doc-table--reviews .doc-table__group {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
  }

  .doc-table--reviews .rev-group--source { text-align: right; }
}

/* Breadcrumb shared by Review Detail and Source Report Detail: the link keeps
   the global link colour (no underline at rest), the current item is muted. */
.crumbs {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
  font-size: var(--text-12);
  color: var(--text-muted);
}
.crumbs a:hover { text-decoration: underline; }
.crumbs a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.crumbs__sep { color: var(--text-faint); }

.review-shell {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-top: var(--space-4);
  max-width: 820px;
}

.review-panel {
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  background: var(--surface);
  padding: var(--space-3) var(--space-4);
}
.review-panel__title { margin: 0 0 var(--space-2); font-size: var(--text-13); font-weight: 600; color: var(--text); }
.review-panel__hint { margin: 0 0 var(--space-2); font-size: var(--text-12); color: var(--text-muted); }
/* Owner UI feedback round 3 §2: main explanatory content (what the owner
   needs to read to decide) is normal body text, never muted — muted is
   reserved for supporting/secondary metadata. Distinct from
   `.review-panel__hint` (a smaller --text-12 aside used elsewhere on this
   page) so that fix stays scoped to this semantic, not a global re-tuning. */
.review-panel__body { margin: 0 0 var(--space-2); font-size: var(--text-13); color: var(--text); }
.review-panel__body--muted { color: var(--text-muted); }
.review-panel textarea { width: 100%; }

.review-affected { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-1); font-size: var(--text-13); }

.review-source__title { margin: 0; font-size: var(--text-13); font-weight: 600; }
.review-source__meta { display: flex; flex-wrap: wrap; gap: var(--space-3); margin: var(--space-1) 0 var(--space-2); font-size: var(--text-12); color: var(--text-muted); }
.review-source__hint { color: var(--st-limit); }

.review-form { display: flex; flex-direction: column; gap: var(--space-3); }
.review-form__note { margin: 0; }

/* Optional note: a routine review must not have to write prose, so this is a
   plain compact field rather than another panel card competing with the
   decision itself. */
.review-note { display: flex; flex-direction: column; gap: var(--space-1); }
.review-note__label { font-size: var(--text-12); color: var(--text-muted); }
.review-note textarea { width: 100%; font-size: var(--text-12); }

.review-samevalue {
  margin: 0 0 var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-left: 3px solid var(--st-limit);
  background: var(--accent-weak);
  font-size: var(--text-12);
  color: var(--text);
}

.review-candidates { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2); }
.review-candidate__label {
  display: flex;
  gap: var(--space-3);
  align-items: flex-start;
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
}
.review-candidate__label:hover { border-color: var(--accent); }
.review-candidate__label:focus-within { outline: 2px solid var(--accent); outline-offset: 1px; }
.review-candidate__label:has(input:checked) { border-color: var(--accent); background: var(--accent-weak); }
.review-candidate__body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.review-candidate__role { font-size: var(--text-13); font-weight: 600; }
.review-candidate__value { font-size: var(--text-13); color: var(--text-muted); }
.review-candidate__meta { display: flex; flex-wrap: wrap; gap: var(--space-3); font-size: var(--text-11); color: var(--text-faint); }
.review-candidates__shared { margin: var(--space-2) 0 0; font-size: var(--text-12); color: var(--text-muted); }

.review-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-3); }
.review-reject-hint { margin: var(--space-2) 0 0; }

.review-correction__fields { display: flex; flex-wrap: wrap; gap: var(--space-3); }
.review-correction__fields label { display: flex; flex-direction: column; gap: var(--space-1); font-size: var(--text-12); }

.review-saved { border-color: var(--st-verified); }
.review-technical { margin-top: var(--space-1); }

/* Owner UI feedback round 1 §4: containment body text was inheriting the
   page's default (~--text-15) instead of the existing --text-13 grammar
   `.review-affected`/`.inspect-table` already use elsewhere on this page. */
.review-containment-proposal { font-size: var(--text-13); }
/* Owner UI feedback round 3 §3: subsection labels (当前记录/建议整理为/变化说明)
   must read as a smaller, muted grouping label — distinct from both the
   card title (`.review-panel__title`) and fact titles (`.fact-compact__title`,
   normal color, same --text-13), which otherwise all look like the same
   weight/size. No underline (link affordance); stronger top spacing groups
   each subsection visually instead. */
.review-containment-proposal h3 { margin: var(--space-4) 0 var(--space-2); font-size: var(--text-12); font-weight: 600; color: var(--text-muted); }
.review-containment-list { margin: 0 0 var(--space-2); padding-left: var(--space-4); display: grid; gap: var(--space-2); }

/* Owner UI feedback round 2 §2: the "compact" FactPresentation context — a
   fact nested inside a list item (or, later, any other surface) — needs its
   OWN small intrinsic label width, not `.inspect-table`'s full-width column
   (that's the "block" context: _macros.html fact_block/fact_compact). Same
   markup regardless of field count; only the rendering context differs. */
.fact-compact { font-size: var(--text-13); }
.fact-compact__title { margin: 0 0 var(--space-1); font-weight: 600; }
.fact-compact__fields { margin: 0; display: grid; gap: 2px; }
.fact-compact__fields > div { display: flex; gap: var(--space-2); }
.fact-compact__fields dt { flex: 0 0 auto; min-width: 4.5em; color: var(--text-muted); font-weight: 500; }
.fact-compact__fields dd { margin: 0; color: var(--text); overflow-wrap: anywhere; }

/* ---------------------------------------------------------------------------
   Notification Center + toast. Built from the existing popover/panel language
   (same `<details>` + popover.js behaviour the workspace filters use) and the
   existing surface/line/severity tokens — not a separate widget library.
   ------------------------------------------------------------------------ */

.notif { position: relative; display: inline-flex; }
.notif__trigger { position: relative; list-style: none; display: inline-flex; align-items: center; }
.notif__trigger::-webkit-details-marker { display: none; }
.notif[open] .notif__trigger { color: var(--text); }
.notif__trigger:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.notif__icon {
  display: block;
  width: 16px;
  height: 16px;
  background-color: currentColor;
  -webkit-mask: url("icons/bell.svg") center / contain no-repeat;
  mask: url("icons/bell.svg") center / contain no-repeat;
}

.notif__badge {
  position: absolute;
  top: -1px;
  right: -1px;
  min-width: 15px;
  padding: 0 3px;
  border-radius: 999px;
  background: var(--st-qc);
  color: #fff;
  font-size: var(--text-11);
  line-height: 15px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.notif__panel {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 60;
  width: min(340px, calc(100vw - 2 * var(--space-3)));
  max-height: min(70vh, 520px);
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-pop, 0 8px 24px rgba(0, 0, 0, 0.12));
  padding: var(--space-2);
  text-align: left;
}

.notif__head { display: flex; align-items: baseline; gap: var(--space-2); padding: var(--space-1) var(--space-2); }
.notif__title { font-size: var(--text-15); font-weight: 600; }
/* Count of CURRENTLY VISIBLE notifications — secondary to the title and
   deliberately not badge-shaped: the unread signal stays the bell badge. */
.notif__count { font-size: var(--text-12); color: var(--text-faint); font-variant-numeric: tabular-nums; }
.notif__head .notif__link { margin-left: auto; }
.notif__link {
  appearance: none; border: 0; background: transparent; cursor: pointer;
  color: var(--text-muted); font: inherit; font-size: var(--text-11); padding: 0;
}
.notif__link:hover { color: var(--accent); }

.notif__section + .notif__section { margin-top: var(--space-2); border-top: 1px solid var(--line); padding-top: var(--space-2); }
.notif__section-title { margin: 0 0 var(--space-1); padding: 0 var(--space-2); font-size: var(--text-11); font-weight: 600; color: var(--text-muted); }
.notif__list { list-style: none; margin: 0; padding: 0; }
.notif__empty { margin: 0; padding: var(--space-1) var(--space-2) var(--space-2); font-size: var(--text-12); color: var(--text-faint); }

.notif__item {
  display: flex;
  align-items: flex-start;
  gap: var(--space-1);
  font-size: var(--text-12);
}
/* The hover highlight is painted on the CONTENT region only, so the clear
   action sits beside it rather than inside the highlighted area. */
.notif__item-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-2);
  border-radius: var(--r-control);
}
.notif__item:hover .notif__item-main,
.notif__item:focus-within .notif__item-main { background: var(--accent-weak); }
.notif__item-title { font-size: var(--text-12); color: var(--text); }
/* Unread distinction is weight only — no extra badge per row. */
.notif__item.is-unread .notif__item-title { font-weight: 600; }
.notif__item--error .notif__item-title { color: var(--st-qc); }
.notif__item-meta { display: flex; flex-wrap: wrap; gap: var(--space-2); font-size: var(--text-11); color: var(--text-faint); }
.notif__item-time { font-variant-numeric: tabular-nums; }
.notif__item-body { font-size: var(--text-11); color: var(--text-muted); }

/* `需要处理` is durable recovery state, so it stays visible and actionable
   even when every notification below it has been cleared. */
.notif__item--action {
  background: var(--accent-weak);
  align-items: center;
  padding: var(--space-2);
  border-radius: var(--r-control);
}
.notif__item--action .notif__item-main { padding: 0; }
.notif__item--action:hover .notif__item-main,
.notif__item--action:focus-within .notif__item-main { background: transparent; }

.notif__action,
.notif__dismiss {
  appearance: none; border: 1px solid var(--line); background: var(--surface);
  color: var(--text); font: inherit; font-size: var(--text-11);
  padding: 3px 8px; border-radius: var(--r-control); cursor: pointer; flex-shrink: 0;
}
.notif__action { border-color: var(--line-strong); }
.notif__action:hover { border-color: var(--text-faint); }
.notif__action[disabled] { opacity: 0.5; cursor: default; }

/* Clear control: its own quiet surface, a comfortable hit target, and it
   stays out of the way until the row is hovered — but never when it has
   keyboard focus. Sized for either a glyph or an icon asset. */
.notif__dismiss {
  align-self: center;
  border-color: transparent;
  background: transparent;
  color: var(--text-faint);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  font-size: var(--text-13);
  line-height: 1;
  opacity: 0;
  transition: opacity 120ms ease-out;
}
/* The glyph is a currentColor mask (an <img> ignores `color`): row hover only
   reveals the button; only the button's own :hover darkens the glyph. */
.notif__dismiss .fn-icon { width: 14px; height: 14px; }
.notif__item:hover .notif__dismiss,
.notif__item:focus-within .notif__dismiss,
.notif__dismiss:focus-visible { opacity: 1; }
.notif__dismiss:hover { background: transparent; border-color: transparent; color: var(--text); }
.notif__dismiss:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

@media (hover: none) {
  /* Touch has no hover: the clear control is always visible there. */
  .notif__dismiss { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .notif__dismiss { transition: none; }
}

@media (max-width: 640px) {
  /* Same information architecture, docked to the viewport so nothing
     overflows horizontally on a narrow screen. */
  .notif__panel {
    position: fixed;
    top: auto;
    left: var(--space-2);
    right: var(--space-2);
    bottom: var(--space-2);
    width: auto;
    max-height: 70vh;
  }
}

/* —— toast —— */
.toast-host {
  position: fixed;
  top: var(--space-3);
  right: var(--space-3);
  z-index: 90;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  width: min(340px, calc(100vw - 2 * var(--space-3)));
  pointer-events: none;
}

.toast {
  pointer-events: auto;
  display: flex;
  /* Primary content and the controls share one centred baseline, so a
     one-line and a multi-line toast lay out the same way. */
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--text-faint);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-pop, 0 8px 24px rgba(0, 0, 0, 0.12));
  font-size: var(--text-12);
  animation: toast-in 180ms ease-out;
}
/* Semantic type reads from the left accent, but never from colour alone:
   each toast also carries a visually-hidden type word and the matching
   status/alert role. */
.toast--success { border-left-color: var(--st-verified); }
.toast--warning { border-left-color: var(--st-limit); }
.toast--error { border-left-color: var(--st-qc); }
.toast__text { margin: 0; flex: 1; min-width: 0; line-height: 1.45; align-self: center; }
.toast__action,
.toast__close { align-self: center; }
.toast__action {
  appearance: none; border: 1px solid var(--line); background: transparent;
  color: var(--accent); font: inherit; font-size: var(--text-11);
  padding: 2px 8px; border-radius: var(--r-control); cursor: pointer; flex-shrink: 0;
}
.toast__close {
  appearance: none; border: 1px solid transparent; background: transparent;
  color: var(--text-faint); font: inherit; font-size: var(--text-13); line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; padding: 0; border-radius: var(--r-control);
  cursor: pointer; flex-shrink: 0;
}
.toast__close svg,
.toast__close img { width: 14px; height: 14px; display: block; }
.toast__close:hover { background: var(--accent-weak); border-color: var(--line); color: var(--text); }
.toast__close:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.toast.is-leaving { opacity: 0; transition: opacity 180ms ease-out; }

@keyframes toast-in {
  from { opacity: 0; transform: translateY(-6px); }
  to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; }
  .toast.is-leaving { transition: none; }
}

@media (max-width: 640px) {
  .toast-host { left: var(--space-2); right: var(--space-2); width: auto; }
}

@media (max-width: 640px) {
  /* Title / count / 全部清除 must not collide in a narrow panel. */
  .notif__head { flex-wrap: wrap; row-gap: 2px; }
  .notif__item-main { padding: var(--space-2) var(--space-1); }
}

/* —— upload current progress —— */
.upload-progress__list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-1); }
.upload-progress__item {
  display: flex; justify-content: space-between; gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--line); border-radius: var(--r-card); background: var(--surface);
  font-size: var(--text-12);
}
.upload-progress__status { color: var(--text-muted); flex-shrink: 0; }

/* —— review application state —— */
.review-applied--success { border-color: var(--st-verified); }
.review-applied--error { border-color: var(--st-qc); }
.review-applied--pending { border-color: var(--st-limit); }
.review-applied p { margin: 0 0 var(--space-2); }
.review-applied p:last-child { margin-bottom: 0; }
.review-applied__body { font-size: var(--text-13); }
.review-applied__remaining {
  margin-top: var(--space-4) !important;
  color: var(--text);
  font-size: var(--text-13);
  font-weight: 600;
}
