/* Researchgen — shared primitives: reset, type, nav, buttons, inputs, cards,
   tables, dialog, toast, menu, chips, banners, marketing/auth/admin layout and
   chart chrome. Every surface-specific rule lives in app.css or dash.css.

   Colour, size, radius and motion come from theme.css ONLY: there is no literal
   hex in this file, so both themes are correct by construction.

   CSP: no inline stylesheet elements, no inline style attributes, no inline script
   elements. CSSOM writes
   via el.style.* are permitted and are used for exactly three things — the theme
   attribute, the confirm countdown bar, and real-pixel chart sizing. */

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

* {
  margin: 0;
  padding: 0;
}

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--t-17);
  line-height: var(--lh-body);
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  min-height: 100dvh;
}

/* The codebase's `hidden`-plus-flex idiom: a component that sets display:flex on
   its own selector silently defeats [hidden]. Restoring it here once means no
   downstream sheet has to remember. */
[hidden] {
  display: none !important;
}

img,
svg,
video {
  display: block;
  max-width: 100%;
}

svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
}

button {
  cursor: pointer;
}

button:disabled {
  cursor: default;
}

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

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

/* ── type ──────────────────────────────────────────────────────────────────── */
h1,
h2,
h3,
h4 {
  font-weight: 600;
  line-height: var(--lh-tight);
  /* Apple stops at semibold; there is no 700 anywhere in this design. */
}

h1 {
  font-size: var(--t-40);
  line-height: var(--lh-display);
  letter-spacing: var(--tr-tight);
}

h2 {
  font-size: var(--t-28);
  letter-spacing: var(--tr-tight);
}

h3 {
  font-size: var(--t-21);
}

h4 {
  font-size: var(--t-17);
}

p {
  max-width: var(--read-w);
}

small,
.meta {
  font-size: var(--t-12);
  color: var(--text-3);
}

.sub {
  font-size: var(--t-17);
  color: var(--text-2);
}

code,
kbd,
pre,
.mono {
  font-family: var(--font-mono);
  font-size: var(--tm-13);
}

pre {
  background: var(--code-bg);
  border-radius: var(--r-8);
  padding: var(--sp-12);
  overflow: auto;
  /* A huge tool payload used to push the whole page down. */
  max-height: 320px;
}

/* Visually hidden but reachable by assistive tech. */
.vh {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── focus ─────────────────────────────────────────────────────────────────
   One rule, everything. :focus-visible (not :focus) so a mouse click on a
   button does not leave a ring behind. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  box-shadow: var(--focus-ring);
  border-radius: var(--r-8);
}

/* On an accent-FILLED control the accent ring is invisible against the fill, so
   it flips to the page background. */
.btn.primary:focus-visible,
.btn.accent-fill:focus-visible {
  outline-color: var(--bg);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 55%, transparent);
}

/* ── skip link — the FIRST focusable node on every page ─────────────────────── */
.skip-link {
  position: fixed;
  top: var(--sp-8);
  left: var(--sp-8);
  z-index: 100;
  padding: var(--sp-8) var(--sp-16);
  background: var(--bg-elev);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--r-8);
  box-shadow: var(--shadow-md);
  font-size: var(--t-14);
  transform: translateY(-200%);
  transition: transform var(--dur) var(--ease);
}

.skip-link:focus-visible {
  transform: translateY(0);
  text-decoration: none;
}

/* ── nav — 48px sticky frosted bar ─────────────────────────────────────────── */
.nav {
  position: sticky;
  top: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  gap: var(--sp-16);
  height: var(--nav-h);
  padding: 0 var(--sp-16);
  background: var(--nav-bg);
  border-bottom: 1px solid var(--border);
}

/* The blur is applied ONLY where it is supported; theme.css has already swapped
   --nav-bg for the opaque value in the fallback and reduced-transparency cases,
   so contrast over the bar is guaranteed in exactly the branch that needs it. */
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  @media not (prefers-reduced-transparency: reduce) {
    .nav {
      -webkit-backdrop-filter: saturate(180%) blur(20px);
      backdrop-filter: saturate(180%) blur(20px);
    }
  }
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-8);
  font-size: var(--t-17);
  font-weight: 600;
  color: var(--text);
  letter-spacing: var(--tr-tight);
}

.brand:hover {
  text-decoration: none;
  color: var(--text);
}

.brand-mark {
  width: 18px;
  height: 18px;
  color: var(--accent);
  flex: none;
}

.nav-spacer {
  flex: 1 1 auto;
}

.nav-link {
  font-size: var(--t-14);
  color: var(--text-2);
  padding: var(--sp-8);
  border-radius: var(--r-8);
}

.nav-link:hover {
  color: var(--text);
  text-decoration: none;
}

.nav-link[aria-current="page"] {
  color: var(--text);
  font-weight: 500;
}

/* Size-RESERVED so shared.js filling it causes zero layout shift. The frosted
   bar, its height and the brand all exist in the initial HTML; only the parts
   that genuinely drift (theme button, user menu) are injected. */
#nav-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--sp-4);
  min-width: 120px;
  min-height: 32px;
}

/* ── footer ────────────────────────────────────────────────────────────────── */
.footer {
  padding: var(--sp-32) var(--sp-16);
  border-top: 1px solid var(--border);
  color: var(--text-3);
  font-size: var(--t-12);
  text-align: center;
}

.footer a {
  color: var(--text-2);
}

/* ── buttons ───────────────────────────────────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-8);
  /* >=40px hit target everywhere, phones included. */
  min-height: 40px;
  padding: 0 var(--sp-16);
  border-radius: var(--r-pill);
  font-size: var(--t-14);
  font-weight: 500;
  color: var(--text);
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease);
}

.btn.primary {
  background: var(--accent);
  /* NEVER #fff: see theme.css --d-accent-ink. */
  color: var(--accent-ink);
}

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

.btn.ghost {
  border: 1px solid var(--border);
}

.btn.ghost:hover:not(:disabled) {
  background: var(--bg-alt);
}

.btn.danger {
  color: var(--danger);
  border: 1px solid color-mix(in srgb, var(--danger) 40%, transparent);
}

.btn.danger:hover:not(:disabled) {
  background: color-mix(in srgb, var(--danger) 10%, transparent);
}

.btn.icon {
  width: 40px;
  height: 40px;
  min-height: 40px;
  padding: 0;
  border-radius: var(--r-8);
  color: var(--text-2);
}

.btn.icon:hover:not(:disabled) {
  background: var(--bg-alt);
  color: var(--text);
}

.btn.icon svg {
  width: 20px;
  height: 20px;
}

.btn.sm {
  min-height: 32px;
  padding: 0 var(--sp-12);
  font-size: var(--tm-13);
}

.btn.block {
  width: 100%;
  min-height: 48px;
  font-size: var(--t-17);
}

.btn:disabled {
  background: var(--bg-alt);
  color: var(--text-3);
  border-color: transparent;
}

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-8);
  align-items: center;
}

/* ── inputs ────────────────────────────────────────────────────────────────── */
.field {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  margin-bottom: var(--sp-16);
}

label {
  font-size: var(--t-14);
  font-weight: 500;
  color: var(--text);
}

.input,
input[type="text"],
input[type="email"],
input[type="password"],
input[type="search"],
select,
textarea {
  min-height: 48px;
  padding: var(--sp-12);
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-8);
  font-size: var(--t-17);
  width: 100%;
}

textarea {
  resize: vertical;
  line-height: var(--lh-body);
}

.input.mono,
input.mono {
  font-family: var(--font-mono);
  font-size: var(--tm-13);
  letter-spacing: .06em;
  text-transform: uppercase;
}

::placeholder {
  color: var(--text-3);
  opacity: 1;
}

/* A password field with a show/hide button reserves room for it. */
.field-wrap {
  position: relative;
  display: flex;
}

.field-wrap .reveal {
  position: absolute;
  top: 50%;
  right: var(--sp-4);
  transform: translateY(-50%);
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-8);
  color: var(--text-2);
}

.field-wrap:has(.reveal) .input {
  padding-right: 52px;
}

.helper {
  font-size: var(--t-12);
  color: var(--text-2);
}

/* role="alert" + aria-live="assertive" in the markup; hidden while empty so it
   does not announce an empty string on every render. */
.error-text {
  font-size: var(--t-14);
  color: var(--danger);
}

.error-text:empty {
  display: none;
}

/* Requirement list: the check is never colour-only — data-ok drives a glyph. */
.rules {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  font-size: var(--t-14);
  color: var(--text-2);
}

.rules li {
  display: flex;
  align-items: center;
  gap: var(--sp-8);
}

.rules li::before {
  content: "\2022";
  width: 16px;
  text-align: center;
  color: var(--text-3);
}

.rules li[data-ok="true"] {
  color: var(--text);
}

.rules li[data-ok="true"]::before {
  content: "\2713";
  color: var(--success);
}

/* ── card ──────────────────────────────────────────────────────────────────── */
.card {
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--r-18);
  padding: var(--sp-24);
}

.card + .card {
  margin-top: var(--sp-24);
}

.card-head {
  display: flex;
  align-items: center;
  gap: var(--sp-12);
  flex-wrap: wrap;
  margin-bottom: var(--sp-16);
}

.card-head h3 {
  margin-right: auto;
}

.card-sub {
  font-size: var(--t-14);
  color: var(--text-2);
  margin-bottom: var(--sp-16);
}

/* ── table, with a CSS-only stacked-card mode ──────────────────────────────── */
.table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--t-14);
}

.table th,
.table td {
  text-align: left;
  padding: var(--sp-12) var(--sp-8);
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}

.table th {
  font-size: var(--t-12);
  font-weight: 500;
  color: var(--text-2);
  white-space: nowrap;
}

.table tbody tr:hover {
  background: var(--bg-alt);
}

.table td.num,
.table th.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.table th[aria-sort] {
  cursor: pointer;
}

.table th[aria-sort="ascending"]::after {
  content: " \2191";
}

.table th[aria-sort="descending"]::after {
  content: " \2193";
}

/* Below 700px every row becomes a card and each cell grows its own label from
   the data-label attribute. No inline styles, no JS branch, no second markup. */
@media (max-width: 699px) {
  .table thead {
    display: none;
  }

  .table,
  .table tbody,
  .table tr,
  .table td {
    display: block;
    width: 100%;
  }

  .table tr {
    border: 1px solid var(--border);
    border-radius: var(--r-12);
    padding: var(--sp-8);
    margin-bottom: var(--sp-12);
  }

  .table td {
    border-bottom: none;
    display: flex;
    justify-content: space-between;
    gap: var(--sp-16);
    padding: var(--sp-4) var(--sp-4);
    text-align: left;
  }

  .table td::before {
    content: attr(data-label);
    font-size: var(--t-12);
    color: var(--text-2);
    flex: none;
  }

  .table td.num {
    text-align: right;
  }
}

/* ── dialog ────────────────────────────────────────────────────────────────── */
dialog {
  background: var(--bg-elev);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--r-18);
  padding: var(--sp-32);
  width: min(420px, calc(100vw - 32px));
  box-shadow: var(--shadow-lg);
}

dialog::backdrop {
  background: var(--scrim);
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
}

dialog h3 {
  display: flex;
  align-items: center;
  gap: var(--sp-12);
  margin-bottom: var(--sp-12);
}

.dialog-icon {
  width: 40px;
  height: 40px;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-circle);
  background: color-mix(in srgb, var(--warn) 14%, transparent);
  color: var(--warn);
}

dialog.destructive .dialog-icon {
  background: color-mix(in srgb, var(--danger) 14%, transparent);
  color: var(--danger);
}

.dialog-body {
  font-size: var(--t-14);
  color: var(--text-2);
  margin-bottom: var(--sp-24);
}

.dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--sp-8);
}

/* Width is set with el.style.setProperty (CSSOM, CSP-safe) — never an inline
   style attribute, which style-src 'self' would block. */
.countdown {
  height: 2px;
  background: var(--warn);
  border-radius: var(--r-pill);
  width: 100%;
  transition: width var(--dur) linear;
}

/* ── toast ─────────────────────────────────────────────────────────────────── */
#toast {
  position: fixed;
  bottom: var(--sp-24);
  left: 50%;
  z-index: 90;
  transform: translate(-50%, 0);
  max-width: min(420px, calc(100vw - 32px));
  padding: var(--sp-12) var(--sp-24);
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  box-shadow: var(--shadow-lg);
  font-size: var(--t-14);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-slow) var(--ease), transform var(--dur-slow) var(--ease);
}

#toast.show {
  opacity: 1;
  transform: translate(-50%, calc(-1 * var(--sp-8)));
}

/* ── menu primitive (role="menu") — theme picker and every row ⋯ menu ──────── */
.menu-wrap {
  position: relative;
}

.menu {
  position: absolute;
  top: calc(100% + var(--sp-4));
  right: 0;
  z-index: 50;
  min-width: 180px;
  padding: var(--sp-4);
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--r-12);
  box-shadow: var(--shadow-lg);
}

.menu button,
.menu a {
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  width: 100%;
  min-height: 40px;
  padding: 0 var(--sp-12);
  border-radius: var(--r-8);
  font-size: var(--t-14);
  color: var(--text);
  text-align: left;
}

.menu button:hover,
.menu a:hover {
  background: var(--bg-alt);
  text-decoration: none;
}

.menu button:disabled {
  color: var(--text-3);
  background: none;
}

.menu hr {
  border: none;
  border-top: 1px solid var(--border);
  margin: var(--sp-4) 0;
}

.menu .check {
  margin-left: auto;
  width: 16px;
  color: var(--accent);
  visibility: hidden;
}

.menu [aria-checked="true"] .check {
  visibility: visible;
}

/* ── segmented control ─────────────────────────────────────────────────────── */
.segmented {
  display: inline-flex;
  padding: 2px;
  background: var(--bg-alt);
  border-radius: var(--r-pill);
}

.segmented label {
  position: relative;
  display: inline-flex;
}

.segmented input {
  position: absolute;
  opacity: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  cursor: pointer;
}

.segmented span {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  padding: 0 var(--sp-12);
  border-radius: var(--r-pill);
  font-size: var(--tm-13);
  color: var(--text-2);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}

.segmented input:checked + span {
  background: var(--bg-elev);
  color: var(--text);
  box-shadow: var(--shadow-sm);
}

.segmented input:focus-visible + span {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  box-shadow: var(--focus-ring);
}

/* ── chip / badge / banner / caveat ────────────────────────────────────────── */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-4);
  min-height: 32px;
  padding: 0 var(--sp-12);
  background: var(--bg-alt);
  border-radius: var(--r-pill);
  font-size: var(--t-14);
  color: var(--text);
}

button.chip:hover {
  background: var(--bg-elev);
  box-shadow: var(--shadow-sm);
}

.chip.selected {
  background: var(--accent-tint);
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-4);
  padding: 2px var(--sp-8);
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  font-size: var(--tm-11);
  color: var(--text-2);
  white-space: nowrap;
}

/* Status badges always carry an icon AND a word — colour is never the only
   channel, so these rules only tint what a glyph already says. */
.badge.good {
  color: var(--success);
}

.badge.bad {
  color: var(--danger);
}

.badge.warn {
  color: var(--warn);
}

.banner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-12);
  min-height: var(--banner-h);
  padding: var(--sp-12) var(--sp-16);
  background: var(--accent-tint);
  border-left: 2px solid var(--accent);
  border-radius: var(--r-12);
  font-size: var(--t-14);
}

.banner .banner-text {
  flex: 1 1 320px;
}

.banner .btn-row {
  margin-left: auto;
}

.banner.danger {
  background: color-mix(in srgb, var(--danger) 10%, transparent);
  border-left-color: var(--danger);
}

/* Shown above score charts whenever coverage < 60%. */
.caveat {
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  padding: var(--sp-8) var(--sp-12);
  background: color-mix(in srgb, var(--warn) 10%, transparent);
  border-left: 2px solid var(--warn);
  border-radius: var(--r-8);
  font-size: var(--t-14);
  color: var(--text);
  margin-bottom: var(--sp-16);
}

.notice {
  padding: var(--sp-12) var(--sp-16);
  background: var(--accent-tint);
  border-radius: var(--r-12);
  font-size: var(--t-14);
}

.empty {
  padding: var(--sp-24);
  font-size: var(--t-14);
  line-height: var(--lh-body);
  color: var(--text-2);
}

.empty .btn {
  margin-top: var(--sp-12);
}

/* ── marketing layout ──────────────────────────────────────────────────────── */
.wrap {
  max-width: var(--marketing-w);
  margin: 0 auto;
  /* 16px side gutter at phone width, no horizontal page scroll anywhere. */
  padding: 0 var(--sp-16);
}

.hero {
  padding: var(--sp-96) 0 var(--sp-80);
  text-align: center;
}

.hero h1 {
  font-size: var(--t-56);
  margin-bottom: var(--sp-24);
}

.hero .sub {
  max-width: 640px;
  margin: 0 auto var(--sp-32);
}

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--sp-24);
  padding-bottom: var(--sp-80);
}

.tile {
  padding: var(--sp-32);
  background: var(--bg-alt);
  border-radius: var(--r-18);
}

.tile h3 {
  margin-bottom: var(--sp-8);
}

.tile p {
  font-size: var(--t-14);
  color: var(--text-2);
}

.cta-band {
  padding: var(--sp-80) var(--sp-16);
  margin-bottom: var(--sp-64);
  background: var(--bg-alt);
  border-radius: var(--r-18);
  text-align: center;
}

@media (max-width: 699px) {
  .hero {
    padding: var(--sp-48) 0 var(--sp-32);
  }

  .hero h1 {
    font-size: var(--t-40);
  }

  .cta-band {
    padding: var(--sp-48) var(--sp-16);
  }
}

/* ── auth layout ───────────────────────────────────────────────────────────── */
.auth-shell {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: calc(100dvh - var(--nav-h));
  padding: var(--sp-32) var(--sp-16);
}

.auth-card {
  width: 100%;
  max-width: var(--card-w);
  padding: var(--sp-32);
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--r-18);
  box-shadow: var(--shadow-sm);
}

.auth-card h1 {
  font-size: var(--t-28);
  margin-bottom: var(--sp-24);
}

.auth-alt {
  margin-top: var(--sp-16);
  font-size: var(--t-14);
  text-align: center;
  color: var(--text-2);
}

/* The transparency paragraph on /register. Plain prose, not a dismissible
   banner: an analyst must not be able to close it before reading it. */
#manager-visibility-note {
  font-size: var(--t-14);
  color: var(--text-2);
  padding: var(--sp-12);
  background: var(--bg-alt);
  border-radius: var(--r-8);
  margin-bottom: var(--sp-16);
}

/* ── admin layout helpers ──────────────────────────────────────────────────── */
.admin-shell {
  max-width: 1280px;
  margin: 0 auto;
  padding: var(--sp-24) var(--sp-16) var(--sp-64);
}

.admin-tabs {
  display: flex;
  gap: var(--sp-4);
  overflow-x: auto;
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--sp-16);
}

.admin-tabs a {
  padding: var(--sp-12);
  font-size: var(--t-14);
  color: var(--text-2);
  border-bottom: 2px solid transparent;
  white-space: nowrap;
}

.admin-tabs a:hover {
  color: var(--text);
  text-decoration: none;
}

.admin-tabs a[aria-current="page"] {
  color: var(--text);
  font-weight: 500;
  border-bottom-color: var(--accent);
}

/* ONE filter row, above everything it scopes — never per-chart, never inside a
   card. This is what makes Overview, Analysts and Conversations agree. */
.filter-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-8);
  padding: var(--sp-12) 0;
  margin-bottom: var(--sp-16);
  border-bottom: 1px solid var(--border);
}

.filter-row .computed {
  margin-left: auto;
  font-size: var(--t-12);
  color: var(--text-3);
}

.grid {
  display: grid;
  gap: var(--sp-16);
}

.grid.tiles-4 {
  grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
}

.grid.cols-3 {
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}

/* ── chart chrome ──────────────────────────────────────────────────────────
   charts.js replaces the children of figure.viz; every rule here is about the
   box around the SVG, so a chart can never introduce layout shift. */
figure.viz {
  position: relative;
  margin: 0;
  padding: var(--sp-16);
  background: var(--viz-surface);
  border: 1px solid var(--border);
  border-radius: var(--r-12);
  /* Reserved BEFORE JS runs. Each chart type overrides with its own --viz-h-*. */
  min-height: var(--viz-h-tile);
  /* No skeleton on refetch: the old numbers hold, dimmed. A skeleton would make
     a 200ms refresh look like a page load. */
  transition: opacity var(--dur-fast) var(--ease);
}

figure.viz[data-loading="true"] {
  opacity: .5;
}

figure.viz figcaption {
  font-size: var(--t-12);
  color: var(--viz-ink-2);
  margin-bottom: var(--sp-8);
}

figure.viz .viz-title {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  font-size: var(--t-14);
  font-weight: 500;
  color: var(--viz-ink);
}

figure.viz .viz-value {
  font-size: var(--t-28);
  font-weight: 600;
  color: var(--viz-ink);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tr-tight);
}

figure.viz[data-hero="true"] {
  min-height: var(--viz-h-hero);
}

/* 48px, stepping down to 40px under 420px, per a2-design-dashboard §B.1: at 375
   the 48px figure plus its delta chip does not fit on one line. Both rules were
   --t-40 before, which made the media query dead and the hero 8px too small. */
figure.viz[data-hero="true"] .viz-value {
  font-size: var(--t-48);
}

@media (max-width: 419px) {
  figure.viz[data-hero="true"] .viz-value {
    font-size: var(--t-40);
  }
}

figure.viz[data-type="line"] {
  min-height: var(--viz-h-line);
}

figure.viz[data-type="stack"] {
  min-height: var(--viz-h-stack);
}

figure.viz[data-type="heat"] {
  min-height: var(--viz-h-heat);
}

figure.viz svg text {
  /* Text NEVER wears a series colour. Enforced here as well as in charts.js. */
  fill: var(--viz-ink-2);
  font-family: var(--font-mono);
  font-size: var(--tm-11);
  stroke: none;
}

figure.viz svg .grid-line {
  stroke: var(--viz-grid);
  stroke-width: 1;
}

figure.viz svg .axis-line {
  stroke: var(--viz-axis);
  stroke-width: 1;
}

figure.viz svg [tabindex]:focus-visible {
  outline: none;
}

figure.viz svg .focus-rect {
  stroke: var(--viz-accent);
  stroke-width: 2;
  fill: none;
}

/* Per-chart live region. Deliberately NOT the page-level #sr-status: a focus
   move in one chart must not stomp another chart's announcement. */
.viz-readout {
  font-size: var(--t-12);
  color: var(--viz-ink-2);
  margin-top: var(--sp-8);
  min-height: 1.5em;
}

/* The table-view twin. Always in the DOM, keyboard-reachable with zero JS, so
   tooltips ENHANCE and never gate. */
.viz-table {
  margin-top: var(--sp-8);
  font-size: var(--t-12);
}

.viz-table summary {
  cursor: pointer;
  color: var(--viz-ink-2);
  min-height: 40px;
  display: flex;
  align-items: center;
}

.viz-table table {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}

.viz-table th,
.viz-table td {
  padding: var(--sp-4);
  text-align: left;
  border-bottom: 1px solid var(--viz-grid);
}

.viz-table caption {
  text-align: left;
  color: var(--viz-ink-muted);
  padding-bottom: var(--sp-4);
}

/* Legend appears IFF >= 2 series. A one-swatch legend restating the title is a
   bug, and ui-check.mjs asserts against it. */
.viz-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-12);
  margin-top: var(--sp-8);
  font-size: var(--t-12);
  color: var(--viz-ink-2);
}

.viz-legend li {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  list-style: none;
}

/* A line stroke, not a filled box: a 2px rule reads as "this series' line". */
.viz-legend .swatch {
  width: 14px;
  height: 2px;
  border-radius: var(--r-pill);
  background: currentColor;
}

.viz-delta {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-4);
  font-size: var(--t-12);
  color: var(--viz-ink-2);
}

/* ── definition tooltip — 60 buttons, ONE popover ─────────────────────────── */
.def {
  width: 20px;
  height: 20px;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-circle);
  color: var(--text-3);
  font-size: var(--tm-11);
}

.def:hover {
  color: var(--accent);
  background: var(--bg-alt);
}

/* position:fixed and repositioned on demand via CSSOM, so 60 definitions cost
   60 buttons and one node. #def-tip is one of only two selectors permitted to
   carry a runtime inline style attribute (the other is .viz-*). */
#def-tip {
  position: fixed;
  z-index: 80;
  top: 0;
  left: 0;
  max-width: 320px;
  padding: var(--sp-12);
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--r-12);
  box-shadow: var(--shadow-lg);
  font-size: var(--t-12);
  line-height: var(--lh-body);
  color: var(--text);
}

#def-tip .def-label {
  font-weight: 500;
  margin-bottom: var(--sp-4);
}

#def-tip .def-derivation {
  color: var(--text-2);
  margin-top: var(--sp-8);
}

/* ── forced colors ────────────────────────────────────────────────────────── */
@media (forced-colors: active) {
  .btn,
  .card,
  .chip,
  .badge,
  figure.viz {
    border: 1px solid ButtonBorder;
  }

  figure.viz svg [fill] {
    fill: CanvasText;
  }

  figure.viz svg [stroke]:not(text) {
    stroke: CanvasText;
  }

  /* The texture channel is not implemented; the table view IS the fallback, so
     it must be OPEN and not merely reachable.

     `display` on the child cannot do that. A closed <details> hides its content
     through the UA's `content-visibility: hidden` on the ::details-content
     pseudo-element, and content-visibility is not a display value, so
     `.viz-table > table { display: table }` renders nothing at all. Unsetting it
     on the pseudo-element is the CSS-only half of the fix; charts.js owns the
     other half (it must set `details.open = true` behind
     `matchMedia("(forced-colors: active)")` and on `beforeprint`), because
     ::details-content only landed in Chrome 131 / Safari 18.4 and the table view
     is the accessibility fallback — it cannot depend on a recent engine.
     Contract §9.3 rule 5. */
  .viz-table::details-content {
    content-visibility: visible;
    block-size: auto;
  }

  .viz-table {
    display: block;
  }

  .viz-table > table {
    display: table;
  }
}

@media print {
  .nav,
  #toast,
  #def-tip {
    display: none;
  }

  /* Same reason as forced-colors above: on paper there are no tooltips, so the
     table view is the only way the numbers reach the page. */
  .viz-table::details-content {
    content-visibility: visible;
    block-size: auto;
  }

  .viz-table > table {
    display: table;
  }
}
