/* Dark Heritage (default). Text alphas are tuned so every pair clears
   WCAG AA 4.5:1 against --surface, the lightest ground text sits on. */
:root {
  --bg:          #0e1f16;
  --bg-raised:   #152c1e;
  --surface:     rgba(255,255,255,0.04);
  --surface-hover: rgba(255,255,255,0.07);
  --ink:         #f0e8c8;
  --muted:       rgba(240,232,200,0.72);
  --line:        rgba(255,255,255,0.14);
  --accent:      #d4a048;
  --accent-ink:  #e0b264;
  --accent-soft: rgba(212,160,72,0.16);
  --accent-rule: rgba(212,160,72,0.3);
  --green:       #56a06b;
  --green-soft:  rgba(86,160,107,0.25);

  --field-bg:    rgba(255,255,255,0.04);
  --dropdown-bg: #152c1e;
  --panel-solid: #142a1d;
  --map-void:    #0a1a10;
  --map-from:    20,55,35;
  --map-to:      86,160,107;
  --map-border:  rgba(255,255,255,0.10);
  --shadow:      rgba(0,0,0,0.4);
  --scroll-thumb: rgba(212,160,72,0.35);
  --header-bg:   rgba(14,31,22,0.92);
  --hero-glow:   rgba(61,122,80,0.4);
  /* Every source image is a JPEG on a near-white ground (measured ~249-255
     per channel across the set), so the plate behind them is near-white in
     BOTH themes — matching the artwork's own background means the
     letterboxing disappears instead of framing each image in a visibly
     mismatched tinted bar. */
  --plate:       #fdfdfb;

  /* ---- System tokens ----------------------------------------------------
     The stylesheet had grown 18 font sizes (13 of them under 1rem, 11 within
     0.14rem of each other) and ~25 spacing values. That is jitter, not
     hierarchy. Everything below draws from these six type steps and a 4px
     spacing scale; nothing should introduce a new one. */
  --t-micro: 0.6875rem;  /* 11px — mono labels: the ONLY small size */
  --t-small: 0.8125rem;  /* 13px — secondary text, chips */
  --t-body:  0.9375rem;  /* 15px */
  --t-lead:  1.125rem;   /* 18px */
  --t-h3:    1.625rem;   /* 26px — Instrument Serif */
  --t-h1:    clamp(2.25rem, 5vw, 3.5rem);

  --s1: 0.25rem;  --s2: 0.5rem;   --s3: 0.75rem;  --s4: 1rem;
  --s5: 1.5rem;   --s6: 2rem;     --s7: 3rem;

  --radius: 2px;
  --dur: 0.15s;
  --media-h: 380px;

  --path-fill:  rgba(212,160,72,0.16);
  --path-line:  #d4a048;
  --sib-fill:   rgba(255,255,255,0.075);
  --sib-line:   rgba(255,255,255,0.16);
  --sib-ink:    rgba(240,232,200,0.72);
  /* Sibling species must NOT reuse the accent — amber is reserved for the
     selected lineage, or every leaf on screen reads as highlighted. */
  --dino-stroke: rgba(86,160,107,0.55);
  --dino-ink:   #6fb885;
  --dino-fill:  rgba(86,160,107,0.10);

  /* Chart palettes live here (not in JS) so they re-theme for free. */
  --chart-herb:    #56a06b;
  --chart-carn:    #d4a048;
  --chart-omni:    rgba(240,232,200,0.62);
  --chart-unknown: rgba(255,255,255,0.42);
  --chart-1: #56a06b;  --chart-2: #d4a048;  --chart-3: #8f9fc4;
  --chart-4: #c07a5e;  --chart-5: #8acb9d;  --chart-6: #e0b264;
  --chart-7: #6b7f9e;  --chart-8: #a5654c;  --chart-9: rgba(240,232,200,0.62);
  /* Two extra hues so the Type chart's 9-colour palette (below) never has to
     reuse --chart-1/--chart-2 — those are also --chart-herb/--chart-carn,
     and reusing them for a type would make that type's swatch identical to
     Herbivorous or Carnivorous in the Diet chart sitting right next to it. */
  --chart-type-8: #b98bd8;  --chart-type-9: #5fb6b6;
}

/* Light Heritage — same identity in daylight: parchment ground, deep forest
   ink, and a darkened amber that holds 4.5:1 on a light surface. */
:root[data-theme="light"] {
  --bg:          #f4efe1;
  --bg-raised:   #ffffff;
  --surface:     rgba(255,255,255,0.72);
  --surface-hover: rgba(20,45,30,0.06);
  --ink:         #16281d;
  --muted:       rgba(22,40,29,0.72);
  --line:        rgba(22,40,29,0.16);
  --accent:      #8a5f14;
  --accent-ink:  #6d4b10;
  --accent-soft: rgba(138,95,20,0.12);
  --accent-rule: rgba(138,95,20,0.28);
  --green:       #2f6340;
  --green-soft:  rgba(47,99,64,0.2);

  --field-bg:    #ffffff;
  --dropdown-bg: #ffffff;
  --panel-solid: #faf7ef;
  --map-void:    #e6e0cd;
  --map-from:    206,224,206;
  --map-to:      47,99,64;
  --map-border:  rgba(22,40,29,0.18);
  --shadow:      rgba(22,40,29,0.18);
  --scroll-thumb: rgba(138,95,20,0.4);
  --header-bg:   rgba(244,239,225,0.92);
  --hero-glow:   rgba(47,99,64,0.18);
  --plate:       #fdfdfb;

  --path-fill:  rgba(138,95,20,0.12);
  --path-line:  #8a5f14;
  --sib-fill:   rgba(22,40,29,0.05);
  --sib-line:   rgba(22,40,29,0.20);
  --sib-ink:    rgba(22,40,29,0.72);
  --dino-stroke: rgba(47,99,64,0.5);
  --dino-ink:   #2f6340;
  --dino-fill:  rgba(47,99,64,0.07);

  --chart-herb:    #2f6340;
  --chart-carn:    #8a5f14;
  --chart-omni:    rgba(22,40,29,0.6);
  --chart-unknown: rgba(22,40,29,0.3);
  --chart-1: #2f6340;  --chart-2: #8a5f14;  --chart-3: #4a5c86;
  --chart-4: #93503a;  --chart-5: #52976a;  --chart-6: #a5751f;
  --chart-7: #6b7f9e;  --chart-8: #b9724f;  --chart-9: rgba(22,40,29,0.55);
  --chart-type-8: #6d5a9e;  --chart-type-9: #2f6b6b;
}

* { box-sizing: border-box; }

/* Closed for good, site-wide: any class that forces its own `display`
   (flex/grid/inline-flex — .btn, .news-card, and now the mobile nav/filter
   toggles below) beats the UA stylesheet's [hidden]{display:none} at equal
   specificity, so the `hidden` attribute silently stops working on it. Hit
   this bug three separate times before adding this; !important is the only
   way a single rule can guarantee it can't happen a fourth. The narrower
   .btn[hidden]/.news-card[hidden] rules elsewhere are now redundant but
   left in place as documentation of where it bit before. */
[hidden] { display: none !important; }

/* Lets the header's in-page nav (Explore/Timeline/Quiz/News) and the skip
   link animate to their target instead of snapping there instantly. The
   existing prefers-reduced-motion block above already forces this back to
   `auto`, so this doesn't need its own reduced-motion guard. */
html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: 'Inter', system-ui, sans-serif;
  line-height: 1.5;
}

h1, h2, h3 { margin: 0 0 0.5rem; }

/* Header */
.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.25rem 2rem;
  background: var(--header-bg);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  /* Above Leaflet, whose panes and controls reach z-index 1000. */
  z-index: 1100;
  flex-wrap: wrap;
}

.wordmark {
  font-family: 'Instrument Serif', serif;
  font-style: italic;
  font-size: 1.3rem;
  font-weight: 400;
  color: var(--ink);
  letter-spacing: 0.01em;
}

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

/* Hidden on desktop; the media query near the bottom of this file swaps this
   and .header-nav's roles below ~640px. */
.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: transparent;
  cursor: pointer;
}
/* The span itself is the middle bar, sitting in normal flow — its own
   position:relative is what gives the other two bars (::before/::after,
   absolutely placed against it) a containing block to measure `top` from.
   Previously ::before/::after were only position:relative, so their `top`
   offsets shifted them from their own normal-flow position *inside* a
   parent box fixed at 2px tall — which overlapped/clipped them down to what
   read as two bars instead of three. */
.nav-toggle__bars { position: relative; display: block; width: 18px; height: 2px; }
.nav-toggle__bars,
.nav-toggle__bars::before,
.nav-toggle__bars::after {
  width: 18px;
  height: 2px;
  background: var(--ink);
  border-radius: 1px;
  transition: transform var(--dur), opacity var(--dur), top var(--dur);
}
.nav-toggle__bars::before, .nav-toggle__bars::after { content: ''; position: absolute; left: 0; }
.nav-toggle__bars::before { top: -6px; }
.nav-toggle__bars::after { top: 6px; }
/* The X: middle bar disappears, the other two move to the centre and rotate
   to cross there. */
.nav-toggle[aria-expanded="true"] .nav-toggle__bars { background: transparent; }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars::before { top: 0; transform: rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars::after { top: 0; transform: rotate(-45deg); }

.header-nav { display: flex; gap: 1.5rem; }
.header-nav a {
  color: var(--muted);
  text-decoration: none;
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  transition: color var(--dur);
}
.header-nav a:hover { color: var(--accent); }

.header-nav { align-items: center; }
.theme-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: 2px;
  background: transparent;
  color: var(--muted);
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  cursor: pointer;
  transition: border-color var(--dur), color var(--dur);
}
.theme-toggle:hover { border-color: var(--accent); color: var(--accent); }
.theme-toggle__icon {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--accent);
  /* Crescent in dark mode, filled disc in light mode. */
  box-shadow: inset -3px 0 0 0 var(--bg);
}
:root[data-theme="light"] .theme-toggle__icon { box-shadow: none; }

/* Keyboard focus was invisible on every control; one ring covers them all. */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Hero */
/* The hero spans main's content box so the filter bar's edges land exactly on
   the panel grid below it. Only the prose inside is held to a reading measure —
   previously five different max-widths (1200/900/900/760/520) meant nothing on
   the page lined up vertically as you scrolled. */
.hero {
  padding: var(--s7) var(--s6) var(--s6);
  max-width: 1200px;
  margin: 0 auto;
  text-align: center;
  background: radial-gradient(ellipse at 50% -20%, var(--hero-glow) 0%, transparent 65%);
}

.hero-eyebrow {
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.2em;
  color: var(--accent);
  margin: 0 0 1rem;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
}
.hero-eyebrow::before,
.hero-eyebrow::after {
  content: '';
  display: inline-block;
  width: 24px;
  height: 1px;
  background: var(--accent-rule);
}

.hero h1 {
  font-family: 'Instrument Serif', serif;
  font-size: var(--t-h1);
  font-weight: 400;
  color: var(--ink);
  line-height: 1.02;
  letter-spacing: -0.02em;
  margin: 0 0 0.75rem;
}
.hero h1 .accent {
  font-style: italic;
  color: var(--accent);
}
.hero-subtitle {
  color: var(--muted);
  font-size: var(--t-lead);
  max-width: 52ch;
  margin: 0 auto;
  line-height: 1.6;
}

/* Search */
.hero-search-row {
  display: flex;
  align-items: stretch;
  gap: var(--s3);
  margin: 1.5rem auto 0;
  max-width: 520px;
}

.hero-search {
  display: flex;
  flex: 1;
  min-width: 0;
  gap: 0;
  border: 1px solid var(--accent-rule);
  border-radius: 2px;
  background: var(--field-bg);
}

.search-field { position: relative; flex: 1; }

.hero-search input {
  width: 100%;
  padding: 0.75rem 2.5rem 0.75rem 1rem;
  border: none;
  background: transparent;
  font-size: var(--t-body);
  font-family: 'Inter', system-ui, sans-serif;
  color: var(--ink);
  border-radius: 0;
}
.hero-search input::placeholder { color: var(--muted); }
/* The wrapper already carries a border; an inset ring 1px inside it read as a
   double outline. The whole control lights up instead. */
.hero-search:focus-within { border-color: var(--accent); }
.hero-search input:focus { outline: none; }
.hero-search input::-webkit-search-cancel-button { display: none; }

.search-clear {
  position: absolute;
  right: 0.5rem;
  top: 50%;
  transform: translateY(-50%);
  border: none;
  background: none;
  font-size: 1.25rem;
  line-height: 1;
  color: var(--muted);
  cursor: pointer;
  padding: 0 0.25rem;
  border-radius: 2px;
}
.search-clear:hover { color: var(--ink); }

.search-suggestions {
  position: absolute;
  z-index: 1200;
  top: calc(100% + 4px);
  left: -1px;
  right: -1px;
  margin: 0;
  padding: 0.25rem;
  list-style: none;
  background: var(--dropdown-bg);
  border: 1px solid var(--accent-rule);
  border-radius: 2px;
  box-shadow: 0 12px 28px var(--shadow);
  max-height: 300px;
  overflow-y: auto;
  text-align: left;
}
.search-suggestions li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.5rem 0.7rem;
  border-radius: 2px;
  cursor: pointer;
  font-size: var(--t-body);
  color: var(--ink);
}
.search-suggestions li:hover,
.search-suggestions li[aria-selected="true"] { background: var(--accent-soft); }
.search-suggestions .suggestion-kind {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-family: 'JetBrains Mono', monospace;
  color: var(--accent);
  border: 1px solid var(--accent-rule);
  border-radius: 2px;
  padding: 0.1rem 0.45rem;
  flex-shrink: 0;
}
.search-suggestions mark {
  background: transparent;
  color: var(--accent);
  font-weight: 700;
}

/* Result count + active filter chips */
.result-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.5rem 0.75rem;
  margin: 1rem auto 0;
  max-width: 1136px;
  min-height: 1.6rem;
}
.result-count {
  margin: 0;
  font-size: var(--t-small);
  color: var(--muted);
  font-family: 'JetBrains Mono', monospace;
}
.result-count strong { color: var(--ink); }

.active-filters { display: flex; flex-wrap: wrap; gap: 0.4rem; justify-content: center; }
.active-filter {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.2rem 0.35rem 0.2rem 0.65rem;
  border-radius: 2px;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: var(--t-micro);
  font-family: 'JetBrains Mono', monospace;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 500;
}
.active-filter button {
  border: none;
  background: none;
  color: inherit;
  cursor: pointer;
  font-size: var(--t-body);
  line-height: 1;
  padding: 0 0.15rem;
  border-radius: 2px;
}
.active-filter button:hover { background: var(--accent); color: var(--bg); }

.list-empty { padding: 1.5rem 0.5rem; text-align: center; }
.list-empty .empty-state { margin: 0 0 0.5rem; }
.did-you-mean { margin: 0 0 0.9rem; font-size: var(--t-body); color: var(--muted); }
.did-you-mean button {
  border: none;
  background: none;
  color: var(--accent);
  font-weight: 700;
  cursor: pointer;
  text-decoration: underline;
  font-size: inherit;
  padding: 0;
}

/* Shared box for every CTA-style button on the page (Search, Next question,
   Clear all, View all articles, Reset everything, Clear selection, the
   All/Saved tabs) — one padding, one radius, one type scale, so a button
   reads the same size and shape wherever it turns up, and only its colour
   says "primary" or "secondary". Applied alongside each button's own
   colour class rather than replacing it. */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  padding: 0.6rem 1.1rem;
  border-radius: var(--radius);
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  line-height: 1;
  cursor: pointer;
  transition: border-color var(--dur), color var(--dur), background var(--dur);
}
/* display:inline-flex above beats the UA stylesheet's [hidden]{display:none}
   at equal specificity (author rules win regardless of source order) — every
   .btn toggled via the `hidden` property (Next question, View all articles,
   Show all dinosaurs) needs this to actually disappear. Same fix already
   applied to .news-card[hidden] below. */
.btn[hidden] { display: none; }
.btn-accent {
  border: 1px solid var(--accent-rule);
  background: var(--accent-soft);
  color: var(--accent);
}
/* The one button flush against another control (the search field) rather
   than freestanding. Borderless: the search-field wrapper it's glued to
   already draws the full outline around both of them together, so a border
   here would double up on three of its four sides — only the divider
   between input and button (the left edge) is this button's own to draw.
   The deliberate exception to both the shared radius and the shared border,
   for the one button that isn't actually freestanding. */
.btn--attached {
  border: none;
  border-left: 1px solid var(--accent-rule);
  border-radius: 0 var(--radius) var(--radius) 0;
}
/* Was background:--accent-soft, i.e. its own resting value — the only change
   was text going from accent to ink, which *reduced* emphasis on hover. */
.btn-accent:hover {
  background: var(--accent);
  color: var(--bg);
}

/* Filter bar */
/* --- Bookmarks ("My list") ------------------------------------------- */
/* All / Saved: a segmented switch sitting on the Results panel's own header,
   so the control is adjacent to the thing it changes and its selected state
   is read as "which view am I in", not "is this option on". */
.list-panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  flex-wrap: wrap;
}
.list-panel-head .section-label { margin-bottom: 0; }
.list-panel-head .section-label::after { display: none; }
.list-scope {
  display: inline-flex;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-hover);
  padding: 2px;
}
/* Same padding/type scale as .btn, kept as its own rule rather than sharing
   the class: these two are one glued-together control, not a pair of
   freestanding buttons, so only the inner tab gets its own small radius
   (nested inside .list-scope's own square corners) instead of .btn's full
   corner treatment. */
.list-scope__tab {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  border: none;
  border-radius: var(--radius);
  background: none;
  padding: 0.6rem 1.1rem;
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  cursor: pointer;
  transition: background var(--dur), color var(--dur);
}
.list-scope__tab svg { width: 1em; height: 1em; fill: none; stroke: currentColor; stroke-width: 1.6; }
.list-scope__tab:hover { color: var(--accent); }
.list-scope__tab.is-active {
  background: var(--surface);
  color: var(--accent-ink);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
}
#scope-saved.is-active svg { fill: var(--accent); stroke: var(--accent); }
.list-scope__count {
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}
.list-scope__tab.is-active .list-scope__count { color: var(--accent); }
/* Nothing saved yet: the tab stays visible so the feature is discoverable,
   but dimmed rather than removed — switching to an empty view teaches less
   than the star on each row does. */
.list-scope__tab.is-empty { opacity: 0.55; }

/* One star per list row, filled gold when saved. stopPropagation on click
   keeps it from also selecting the dinosaur underneath it. */
.dino-list li { position: relative; }
.bookmark-star {
  flex-shrink: 0;
  margin-left: auto;
  border: none;
  background: none;
  padding: 0.25rem;
  cursor: pointer;
  color: var(--line);
  transition: color var(--dur), transform 0.15s ease;
}
.bookmark-star svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.5; }
.bookmark-star:hover { color: var(--accent); transform: scale(1.1); }
.bookmark-star.is-active { color: var(--accent); }
.bookmark-star.is-active svg { fill: var(--accent); }

.detail-name-row { display: flex; align-items: center; gap: var(--s3); }
.detail-name-row #detail-name { margin: 0; }
.detail-name-row .bookmark-star svg { width: 22px; height: 22px; }

.filter-bar {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr)) 1.15fr 1.15fr auto auto;
  align-items: end;
  gap: 0.75rem 1rem;
  margin: var(--s5) auto 0;
  /* Aligned to main's content box (1200 - 2*2rem padding) so the filter bar's
     edges sit exactly over the panel grid below rather than 148px inside it. */
  max-width: 1136px;
  padding: var(--s4) var(--s5);
  border: 1px solid var(--line);
  border-top: 1px solid var(--accent-rule);
  border-radius: 2px;
  background: var(--surface);
  text-align: left;
}

.filter-field {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  min-width: 0;
}

.filter-field__label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.4rem;
  font-size: var(--t-micro);
  font-family: 'JetBrains Mono', monospace;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
}

.filter-field__label output {
  color: var(--accent-ink);
  letter-spacing: 0.04em;
  text-transform: none;
}

.filter-field select {
  width: 100%;
  padding: 0.45rem 1.6rem 0.45rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: 2px;
  background-color: var(--field-bg);
  font-size: var(--t-small);
  font-family: 'Inter', system-ui, sans-serif;
  color: var(--ink);
  text-transform: capitalize;
  cursor: pointer;
  /* Native arrow varies per-platform and ignores our palette; draw our own. */
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--accent) 50%),
                    linear-gradient(135deg, var(--accent) 50%, transparent 50%);
  background-position: calc(100% - 0.9rem) 55%, calc(100% - 0.6rem) 55%;
  background-size: 0.3rem 0.3rem;
  background-repeat: no-repeat;
  transition: border-color var(--dur);
}
.filter-field select:hover { border-color: var(--accent-rule); }
.filter-field select option { background: var(--dropdown-bg); color: var(--ink); }

/* A select holding a non-default value reads as active, matching the chips below. */
.filter-field select.is-active {
  border-color: var(--accent-rule);
  background-color: var(--accent-soft);
  color: var(--accent-ink);
}

.filter-field--range input[type="range"] {
  width: 100%;
  height: 1.55rem;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}

.filter-clear {
  border: 1px solid var(--line);
  background: transparent;
  color: var(--muted);
}

/* Mobile-only trigger for the filter bar below — CSS-hidden above ~640px,
   where the filter bar it controls is simply always visible. Docked beside
   the search field as an icon-only square, matching the common search+filter
   pairing rather than sitting as its own full-width text button underneath. */
.filters-toggle {
  display: none;
  position: relative;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 44px;
  padding: 0;
  margin: 0;
}
.filters-toggle__icon {
  width: 20px;
  height: 20px;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  fill: none;
}
.filters-toggle__icon circle { fill: currentColor; stroke: none; }
.filters-toggle__count {
  position: absolute;
  top: -6px;
  right: -6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.3em;
  height: 1.3em;
  padding: 0 0.25em;
  border-radius: 999px;
  background: var(--accent);
  color: var(--bg);
  font-size: 0.7rem;
  line-height: 1;
}
.filter-clear:hover { border-color: var(--accent); color: var(--accent); }

main { max-width: 1200px; margin: 0 auto; padding: 1.5rem 2rem 3rem; }

.section-label {
  display: flex;
  align-items: center;
  gap: var(--s3);
  font-size: var(--t-micro);
  font-weight: 500;
  font-family: 'JetBrains Mono', monospace;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--accent);
  /* Owns the space below itself, replacing the identical margin-top that
     #world-map, .dino-list, .chart-row and .detail-top each repeated. */
  margin: 0 0 var(--s3);
}
/* Was --accent-soft, which at 12% amber is invisible against the light theme;
   dividers on this page are --line. */
.section-label::after { content: ""; flex: 1; height: 1px; background: var(--line); }

.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--s5);
}

/* Map + list overview */
.overview-grid {
  display: grid;
  grid-template-columns: 2fr 1fr;
  /* Grid items default to min-width:auto, so the Leaflet canvas's intrinsic
     width blew the single-column mobile layout out to 600px inside a 375px
     viewport. Every panel here must be free to shrink below its content. */
  gap: 1.5rem;
  margin-bottom: 1.5rem;
  align-items: stretch;
}
.overview-grid > *, .charts-grid > * { min-width: 0; }

/* Leaflet gives its panes and controls z-index 400–1000. Without a stacking
   context here those values compete in the root context and paint the map over
   the sticky header. `isolation` traps them inside this panel. */
.list-panel { display: flex; flex-direction: column; min-height: 0; }

.map-panel {
  position: relative;
  isolation: isolate;
  z-index: 0;
}

#world-map {
  height: var(--media-h);
  width: 100%;
  border-radius: 2px;
  overflow: hidden;
  background: var(--map-void);
}

.map-legend {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin-top: var(--s3);
  font-size: var(--t-micro);
  font-family: 'JetBrains Mono', monospace;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}
.map-legend__title { flex-shrink: 0; }
/* A continuous gradient bar, because the choropleth ramp is continuous —
   discrete chips implied bins that do not exist. */
.map-legend__ramp {
  flex: 1;
  min-width: 60px;
  height: 8px;
  border-radius: 1px;
  border: 1px solid var(--map-border);
}
.map-legend__min, .map-legend__max { font-variant-numeric: tabular-nums; }

/* Leaflet ships its own white tooltip with dark text. Setting only the text
   colour left cream-on-white — about 1.15:1, effectively invisible in the dark
   theme. The whole tooltip is themed, arrow included. --panel-solid rather than
   --surface because it sits over the map and must be opaque. */
.leaflet-tooltip {
  background: var(--panel-solid);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 2px 10px var(--shadow);
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  letter-spacing: 0.04em;
  padding: 0.3rem 0.5rem;
}
.leaflet-tooltip-top::before { border-top-color: var(--line); }
.leaflet-tooltip-bottom::before { border-bottom-color: var(--line); }
.leaflet-tooltip-left::before { border-left-color: var(--line); }
.leaflet-tooltip-right::before { border-right-color: var(--line); }

.country-tooltip { font-weight: 600; color: var(--ink); }

.dino-list {
  list-style: none;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  flex: 1 1 auto;
  min-height: 0;
  /* Bounded, or the list stops being a scroller: the overview grid sizes its
     row to the tallest item, so an uncapped 75-row list defines the row height
     and renders in full instead of the map defining it. Derived from --media-h
     (the map's height) plus the map legend's row, so the two panels end up the
     same height and neither carries dead space. */
  max-height: calc(var(--media-h) + 1.25rem);
  border-top: 1px solid var(--line);
}

.dino-list li {
  display: flex;
  gap: 0.75rem;
  align-items: center;
  padding: 0.6rem 0.4rem;
  border-bottom: 1px solid var(--line);
  cursor: pointer;
  transition: background var(--dur);
}
.dino-list li:hover { background: var(--surface-hover); }
.dino-list li.selected { background: var(--accent-soft); }

/* The source images are all 400px wide with heights from 166 to 714 — aspect
   ratios spanning 0.56 to 2.41. `cover` in a square box therefore cropped the
   head and tail off most of them, which are exactly the parts that identify a
   dinosaur. `contain` inside a 3:2 plate (near the 1.43 median ratio) shows
   every image whole; the tinted ground makes the letterboxing read as a
   specimen card rather than a mistake. */
.dino-list img {
  width: 66px;
  aspect-ratio: 3 / 2;
  height: auto;
  object-fit: contain;
  padding: 3px;
  border-radius: var(--radius);
  background: var(--plate);
  flex-shrink: 0;
  border: 1px solid var(--line);
  transition: border-color var(--dur);
}
.dino-list li.selected img { border-color: var(--accent); }
.dino-list .list-item-text { min-width: 0; }
.dino-list h3 {
  margin: 0;
  font-family: 'Instrument Serif', serif;
  font-size: var(--t-body);
  font-weight: 400;
  font-style: italic;
  color: var(--ink);
}
/* Row metadata: three short facts, not a truncated sentence. Sentence case and
   muted, so the row's name stays the thing the eye lands on — full-width
   uppercase accent text was competing with it. */
.dino-list .list-item-meta {
  margin: 0.15rem 0 0;
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  letter-spacing: 0.04em;
  color: var(--muted);
  overflow: hidden;
  white-space: nowrap;
}
.dino-list .list-item-meta span { overflow: hidden; text-overflow: ellipsis; }
.dino-list .list-item-meta i { color: var(--sib-line); font-style: normal; }
.dino-list li.selected .list-item-meta { color: var(--accent); }

.empty-state { color: var(--muted); font-style: italic; }

/* Charts */
.chart-panel { display: flex; flex-direction: column; container-type: inline-size; }
.chart-panel .chart-row { flex: 1; }

.charts-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s5);
  margin-bottom: var(--s5);
}

/* Reversed: the chart is the content and the legend is the key, but the pie sat
   at 110px in a ~540px panel — a stamp in a field of legend. */
.chart-row { display: flex; align-items: center; gap: var(--s5); }
/* Shrinks with the panel instead of overflowing into the neighbouring chart:
   flex-shrink alone isn't enough here because the two-column legend below has
   its own min-content floor, so the pie has to give way first. */
.chart-row svg { width: 150px; height: 150px; aspect-ratio: 1 / 1; flex-shrink: 1; min-width: 50px; }

.chart-legend { list-style: none; padding: 0; margin: 0; flex: 1; }
/* Nine types in one column outran the pie's height; two columns keep the row
   balanced and let the chart set the panel's height. Gated on the *panel's*
   own width via container query, not the viewport: charts-grid halves the
   panel at 861px viewport width, well before a viewport-width media query
   would've turned this off, and the resulting two-column min-content floor
   was pushing the pie out past the panel edge into the next chart.
   The threshold itself has to clear the two-column legend's own min-content
   width (2 * the longest label, ~163px, plus the column gap — about 342px)
   with enough left over for the pie's chart-row gap and a non-zero pie —
   360px didn't, so the split used to fire before there was room for it and
   the pie got crushed to its 50px floor and still overflowed. */
@container (min-width: 460px) {
  .chart-legend--split { columns: 2; column-gap: var(--s4); }
}
.chart-legend li {
  display: flex;
  align-items: center;
  gap: var(--s1);
  padding: 0.2rem 0;
  color: var(--muted);
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  break-inside: avoid;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.chart-legend .swatch { width: 10px; height: 10px; border-radius: 0; display: inline-block; flex-shrink: 0; }

/* Standalone colour key under a full-width chart (timeline, size), as opposed
   to the pie legends, which are a column beside their chart. Wraps along one
   row so it reads as a strip of swatches rather than a second list competing
   with the chart above it. */
.chart-key {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s4);
  margin-top: var(--s3);
}
.chart-key li {
  padding: 0.1rem var(--s1);
  border: 1px solid transparent;
  border-radius: var(--radius);
  transition: border-color var(--dur);
}

/* Detail panel */
.detail-top {
  display: grid;
  grid-template-columns: 240px 1fr;
  gap: var(--s6);
}
.detail-image-col, .detail-text-col { min-width: 0; }

.detail-image-col img {
  width: 100%;
  aspect-ratio: 3 / 2;
  height: auto;
  object-fit: contain;
  padding: 0.75rem;
  border-radius: var(--radius);
  background: var(--plate);
  border: 1px solid var(--line);
}

.fact-pills { margin: 1rem 0 0; display: flex; flex-direction: column; gap: 0.4rem; }
.fact-pills .fact-pill {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  font-size: var(--t-small);
  padding: 0.3rem 0;
  border-bottom: 1px solid var(--line);
}
.fact-pills dt {
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-weight: 500;
  color: var(--accent);
}
.fact-pills dd { margin: 0; color: var(--muted); text-align: right; text-transform: capitalize; }

.detail-text-col h3 {
  font-family: 'Instrument Serif', serif;
  font-size: var(--t-h3);
  font-weight: 400;
  font-style: italic;
  color: var(--ink);
  letter-spacing: -0.01em;
  margin-bottom: 0.75rem;
}
.field-label {
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--accent);
  font-weight: 500;
  margin-top: 1rem;
  margin-bottom: 0.2rem;
}
.detail-text-col p:not(.field-label) { color: var(--muted); line-height: 1.6; margin: 0; max-width: 68ch; }

/* Real excavation coordinates from the Paleobiology Database, one level more
   specific than the museum's own country-only foundIn field. Kept as a short
   list here rather than only as map pins because selecting a dinosaur
   scrolls straight to this panel — the map is off-screen at that exact
   moment, so a pin nobody scrolls back up to see may as well not exist. */
.dig-sites { display: flex; flex-direction: column; gap: 0.35rem; }
.dig-site-list { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0; padding: 0; list-style: none; }
.dig-site-item {
  display: inline-flex;
  align-items: baseline;
  gap: 0.35em;
  padding: 0.2rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface-hover);
  font-size: var(--t-small);
  color: var(--ink);
}
.dig-site-item .dig-site-count {
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  color: var(--muted);
}
.dig-sites-empty { color: var(--muted); font-size: var(--t-small); margin: 0; }
.dig-sites-jump {
  align-self: flex-start;
  margin-top: 0.15rem;
  border: none;
  background: none;
  padding: 0;
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--accent);
  cursor: pointer;
  border-bottom: 1px solid var(--accent-rule);
}
.dig-sites-jump:hover { color: var(--ink); border-bottom-color: var(--ink); }

/* The "Taxonomy" heading used to sit at the bottom of the two-column .detail-top
   row while the ladder itself was a full-width block below it — so the label
   promised content immediately, and the content arrived wherever the taller
   column happened to end (sometimes a full image-column's height later). The
   heading now sits directly above the thing it names. */
.taxonomy-tree-wrap { margin-top: var(--s5); padding-top: var(--s4); border-top: 1px solid var(--line); }
.taxonomy-tree-wrap .field-label { margin-top: 0; }

/* Full lineage in one line, for the reader who only wants the answer. */
.tx-crumb {
  /* Flex so the segments wrap: they are separated by markup, not whitespace,
     so as inline text the line had no break opportunities and ran off-page. */
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  line-height: 1.9;
  color: var(--sib-ink);
  margin-bottom: 1rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--line);
}
.tx-crumb i { color: var(--sib-line); margin: 0 0.4rem; font-style: normal; }
.tx-crumb__self { color: var(--accent); font-weight: 500; }

/* The ladder: one rank per row. The lineage runs down the left edge as a
   continuous rule, siblings wrap to the right. Height scales with depth,
   width never scales with branching factor. */
.tx-ladder { list-style: none; margin: 0; padding: 0; position: relative; }
.tx-ladder::before {
  content: '';
  position: absolute;
  left: 5px;
  top: 0.6rem;
  bottom: 0.6rem;
  width: 1px;
  background: var(--path-line);
  opacity: 0.45;
}

.tx-rung {
  display: grid;
  grid-template-columns: minmax(0, auto) minmax(0, 1fr);
  gap: 0.4rem 0.9rem;
  /* Top-aligned, not centred: a rank with nine siblings wraps to several lines,
     and the clade must stay level with the first of them so it is obvious which
     row the wrapped chips belong to. */
  align-items: start;
  padding: 0.28rem 0 0.28rem 1.4rem;
  position: relative;
}
.tx-rung::before {
  content: '';
  position: absolute;
  left: 1px;
  /* Fixed offset rather than 50%, so the marker tracks the clade box (which is
     top-aligned) instead of drifting down as siblings wrap. */
  top: 0.75rem;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--surface);
  border: 1px solid var(--path-line);
}
.tx-rung--self::before { background: var(--accent); border-color: var(--accent); }

.tx-clade {
  font-family: 'Inter', system-ui, sans-serif;
  font-size: var(--t-small);
  font-weight: 500;
  letter-spacing: 0;
  color: var(--accent);
  background: var(--path-fill);
  border: 1px solid var(--path-line);
  border-radius: var(--radius);
  padding: 0.3rem 0.55rem;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
}
.tx-clade--self {
  font-family: 'Instrument Serif', Georgia, serif;
  font-style: italic;
  font-size: var(--t-body);
  letter-spacing: 0;
  background: none;
  border-color: transparent;
  padding-left: 0;
}

/* Siblings wrap instead of spreading — this is what bounds the width. */
.tx-sibs { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.tx-sib {
  font-family: 'Inter', system-ui, sans-serif;
  font-size: var(--t-small);
  color: var(--sib-ink);
  background: var(--sib-fill);
  border: 1px solid var(--sib-line);
  border-radius: var(--radius);
  padding: 0.22rem 0.45rem;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}
.tx-sib--dino { font-family: 'Instrument Serif', Georgia, serif; font-style: italic; font-size: var(--t-small); border-color: var(--dino-stroke); background: var(--dino-fill); }
.tx-count { font-family: 'JetBrains Mono', monospace; font-size: var(--t-micro); color: var(--muted); font-variant-numeric: tabular-nums; }

.tx-more {
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  color: var(--accent);
  background: none;
  border: 1px dashed var(--sib-line);
  border-radius: var(--radius);
  padding: 0.22rem 0.45rem;
  cursor: pointer;
  white-space: nowrap;
}

/* Ranks where the lineage had no alternative — no classification decision was
   made there, so they collapse by default. */
.tx-fold { position: relative; padding: 0.15rem 0 0.15rem 1.4rem; }
.tx-fold__toggle {
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  background: none;
  border: none;
  border-left: 1px dashed var(--sib-line);
  padding: 0.35rem 0 0.35rem 0.6rem;
  margin-left: -0.6rem;
  cursor: pointer;
}
.tx-fold__toggle::before { content: '▸ '; }
.tx-fold__toggle[aria-expanded="true"]::before { content: '▾ '; }
.tx-fold__toggle:hover { color: var(--accent); }
.tx-fold__body { list-style: none; margin: 0; padding: 0; }

.result-note {
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  margin-left: 0.5rem;
}

.site-footer {
  text-align: center;
  padding: 2.5rem 2rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s3);
  border-top: 1px solid var(--line);
  margin-top: 2rem;
}
.site-footer__links { display: flex; gap: 1.5rem; }
.site-footer__links a {
  color: var(--muted);
  text-decoration: none;
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  transition: color var(--dur);
}
.site-footer__links a:hover { color: var(--accent); }
/* Fulfils the CC BY 4.0 requirement on the PBDB occurrence data and the
   Wellcome engravings now that neither has a dedicated panel of its own to
   carry the credit in-page (the excavation atlas that used to hold the PBDB
   line was removed) — this is the one place left where every non-dataset
   source gets named. */
.site-footer__note {
  max-width: 62ch;
  margin: 0;
  font-size: var(--t-micro);
  line-height: 1.7;
  color: var(--muted);
}
.site-footer__note a { color: inherit; text-decoration: underline; text-decoration-color: var(--line); }
.site-footer__note a:hover { color: var(--accent); text-decoration-color: var(--accent); }
/* An authorship claim, not a sourcing footnote — ink instead of muted, and its
   own weight, so it doesn't read as one more line in the same register as the
   licence credits directly below it. */
.site-footer__ownership {
  max-width: 62ch;
  margin: 0;
  font-size: var(--t-small);
  line-height: 1.7;
  color: var(--ink);
}
.site-footer__ownership a { color: var(--accent); text-decoration: underline; text-decoration-color: var(--accent-rule); }
.site-footer__ownership a:hover { text-decoration-color: var(--accent); }
.site-footer__copyright {
  margin: var(--s2) 0 0;
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  letter-spacing: 0.04em;
  color: var(--muted);
}

/* Scrollbar tinting for dark theme */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--scroll-thumb); border-radius: var(--radius); }
::-webkit-scrollbar-thumb:hover { background: var(--accent); }

@media (max-width: 860px) {
  .overview-grid, .charts-grid { grid-template-columns: 1fr; }
  .detail-top { grid-template-columns: 1fr; }
  .filter-bar { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .filter-bar .filter-clear { grid-column: 1 / -1; justify-self: stretch; justify-content: center; }
}

@media (max-width: 640px) {
  .site-header, main { padding-left: 1.25rem; padding-right: 1.25rem; }
  .hero { padding: 2rem 1.25rem 2rem; }
  .hero-search-row { max-width: 100%; }
  .filter-bar { grid-template-columns: 1fr; padding: 0.9rem 1rem; }

  /* Header nav becomes a dropdown anchored under the (sticky, so already
     positioned) header bar, opened by the hamburger that only exists at
     this width. Collapsed by default; JS adds .is-open on click. */
  /* 44px, not the 36px it inherits from desktop's inline placement next to
     four other links — the only thing next to it now is the wordmark, and
     44px is the touch-target floor most guidelines (WCAG, Apple HIG) use. */
  .nav-toggle { display: inline-flex; width: 44px; height: 44px; }
  .header-nav {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: var(--s2);
    background: var(--header-bg);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border-bottom: 1px solid var(--line);
  }
  .header-nav.is-open { display: flex; }
  .header-nav a {
    padding: var(--s3) var(--s2);
    border-radius: var(--radius);
  }
  .header-nav a:hover { background: var(--surface-hover); }
  .theme-toggle { margin: var(--s2) var(--s2) 0; justify-content: center; }

  /* Filter bar becomes an on-demand panel behind the Filters button — five
     controls plus two buttons, each full-width in this single column, used
     to run the hero on for a full screen of scrolling before the first
     dinosaur was visible. JS adds .is-open on click and keeps the button's
     count badge in step with the same active-filter state the chips use. */
  .filters-toggle { display: flex; min-height: 44px; }
  .filter-bar { display: none; }
  .filter-bar.is-open { display: grid; }

  /* Three dot-separated fields (diet · type · period) forced onto one line
     were ellipsizing to 3-4 characters each — "her…", "pro…", "Early…" — on
     every one of the 74 rows, at a width this constrained. There's no field
     here that's safe to drop (all three are why someone picked this row over
     the next one), so this trades the single line for two: full words,
     wrapped, instead of three unreadable fragments. */
  .dino-list .list-item-meta {
    flex-wrap: wrap;
    white-space: normal;
    row-gap: 0.1rem;
  }
  .dino-list .list-item-meta span { overflow: visible; text-overflow: clip; }
}

/* Shown in place of the whole app when the dataset can't be loaded at all —
   previously this case rendered an empty page with no explanation. */
.load-error { margin-top: 2rem; }
.load-error p { color: var(--muted); line-height: 1.6; }
.load-error code {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.8em;
  background: var(--surface-hover);
  padding: 0.1rem 0.35rem;
  border-radius: var(--radius);
  color: var(--accent);
}

/* Narrow screens: siblings get the full width under their clade rather than
   being squeezed into a second column a few chips wide. */
@media (max-width: 640px) {
  /* Legend items are short (type/diet names), so they don't need the full
     row width — keeping the pie beside them uses the space that stacking
     left empty under a narrow list. */
  .chart-row { align-items: flex-start; gap: var(--s3); }
  .chart-row svg { width: 110px; height: 110px; aspect-ratio: 1 / 1; flex-shrink: 1; min-width: 50px; }
  .tx-rung { grid-template-columns: minmax(0, 1fr); }
  .tx-sibs { padding-left: 0.2rem; }
}

/* Chart dimming moved out of inline styles set on every re-render. */
.chart-legend li, #type-chart [data-key], #diet-chart [data-key] {
  cursor: pointer;
  transition: opacity var(--dur);
}
.is-dimmed { opacity: 0.3; }

/* Rows are the primary interaction and are now focusable; the ring is inset so
   it doesn't collide with the row divider. */
.dino-list li:focus-visible { outline-offset: -2px; }

/* --- Accessibility and motion ------------------------------------------- */

/* Reaching the results by keyboard meant tabbing through the nav, the theme
   toggle, the search field, five filters and every active filter chip. */
.skip-link {
  position: absolute;
  left: var(--s2);
  top: -3rem;
  z-index: 1200;
  padding: var(--s2) var(--s4);
  background: var(--accent);
  color: var(--bg);
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  text-decoration: none;
  border-radius: var(--radius);
  transition: top var(--dur);
}
.skip-link:focus { top: var(--s2); }

/* The sticky header used to swallow the heading of whatever we scrolled to. */
#detail-panel, .overview-grid, .result-bar, #news-panel,
#timeline-panel, #quiz-panel { scroll-margin-top: 5rem; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* --- Detail panel: a way out, and an honest stale state ------------------ */
.detail-clear {
  margin-left: auto;
  flex-shrink: 0;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--muted);
}
.detail-clear:hover { border-color: var(--accent); color: var(--accent); }
/* --- Dino news --------------------------------------------------------
   Real headlines pulled at build time from ScienceDaily and Phys.org's own
   RSS feeds (tools/fetch-news.py) — title, short summary, date and an
   outbound link, which is exactly what RSS syndication is for. Nothing here
   is generated or republished beyond that. */
.news-panel { margin-bottom: 1.5rem; }
.news-panel .section-label::after { display: none; }
/* Three items now share this row (title, date, toggle). Without wrapping, a
   narrow panel squeezes the title until "Dino news" breaks across two lines
   while the controls stay whole; nowrap + wrap moves whichever item does not
   fit down to its own line intact instead. */
.news-panel .section-label { flex-wrap: wrap; row-gap: 0.5rem; white-space: nowrap; }
.news-intro {
  margin: 0.3rem 0 var(--s4);
  color: var(--muted);
  font-size: var(--t-small);
  max-width: 62ch;
}
/* One dedicated card per story. A slim colour bar along the top edge carries
   the source's identity (so ScienceDaily vs Phys.org reads at a glance
   without another badge competing with the headline). A topic badge above
   the headline is inferred from the headline's own wording (fossil find /
   evolution / discovery) — real signal, not a decorative label. Every card
   is the same size — nothing is promoted above the rest, so the grid reads
   as one consistent set rather than a hierarchy. Only a handful show by
   default — the "View all" button appends
   the rest into the same grid rather than a separate list, so revealing
   more never changes the layout language, just the count. */
.news-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: var(--s4);
  align-items: stretch;
}
/* The grid only fits 3 across once there's room for 3 * 280px cards plus
   gaps inside the page/panel padding (992px viewport and up, measured
   against this page's actual layout). Below that it's 2 columns, where 3
   cards would leave a lone 3rd stranded on its own row — so only the first
   2 show there, a single even row instead. */
@media (max-width: 991px) {
  .news-list:not(.is-expanded) .news-card:nth-child(3) { display: none; }
}
.news-card {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding: var(--s4);
  border: 1px solid var(--line);
  border-top: 3px solid var(--news-source-color, var(--accent));
  border-radius: var(--radius);
  /* Faint glow in the corner, same recipe as the quiz panel's background —
     a small bit of colour so the card doesn't read as a flat form field. */
  background:
    radial-gradient(220px 140px at 100% 0%, var(--accent-soft), transparent 70%),
    var(--surface);
  text-decoration: none;
  color: inherit;
  transition: border-color var(--dur), transform 0.18s ease, box-shadow var(--dur);
}
.news-card:hover {
  border-color: var(--news-source-color, var(--accent));
  transform: translateY(-4px);
  box-shadow: 0 14px 28px -16px rgba(0, 0, 0, 0.45);
}
/* display:flex above beats the UA stylesheet's [hidden]{display:none} on
   specificity, so a card marked hidden (everything past NEWS_CARDS_VISIBLE)
   would otherwise render anyway — this reasserts it. */
.news-card[hidden] { display: none; }


.news-card__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
}
.news-card__topic {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--news-source-color, var(--accent));
}
.news-card__topic svg { width: 1.1em; height: 1.1em; flex-shrink: 0; }
.news-card__new {
  flex-shrink: 0;
  padding: 0.1em 0.55em;
  border-radius: 999px;
  background: var(--accent);
  color: var(--bg);
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.6rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.news-card__title {
  margin: 0;
  font-family: 'Instrument Serif', serif;
  font-style: italic;
  font-size: 1.2rem;
  line-height: 1.3;
  color: var(--ink);
  transition: color var(--dur);
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.news-card:hover .news-card__title { color: var(--news-source-color, var(--accent)); }
.news-card__summary {
  margin: 0;
  flex: 1 1 auto;
  font-size: var(--t-small);
  color: var(--muted);
  line-height: 1.55;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.news-card__foot {
  display: flex;
  align-items: center;
  gap: 0.4em;
  margin-top: 0.1rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--line);
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  color: var(--muted);
}
.news-card__source { color: var(--news-source-color, var(--accent)); font-weight: 600; }
.news-card__dot { opacity: 0.6; }

.news-toggle {
  margin-left: auto;
  flex-shrink: 0;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--muted);
}
.news-toggle:hover { border-color: var(--accent); color: var(--accent); }
.news-empty { color: var(--muted); font-size: var(--t-small); }

/* One-time marker where the page shifts from reading the dataset to playing
   with it / reading about it elsewhere — deliberately not a .panel and not a
   .section-label, so it doesn't read as a seventh data panel among the six
   above it. Flanking rules reuse the .section-label::after rule colour so
   the vocabulary matches; the label itself sits in the gap other panels
   leave empty, so the 24px --s5 rhythm around it is untouched. */
.section-break {
  display: flex;
  align-items: center;
  gap: var(--s4);
  margin: var(--s7) 0;
  color: var(--accent);
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.16em;
}
.section-break::before,
.section-break::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--line);
}

.quiz-panel {
  margin-bottom: 1.5rem;
  position: relative;
  overflow: hidden;
  /* A faint radial glow behind the question, purely decorative — gives the
     panel some presence instead of reading as a plain form. */
  background:
    radial-gradient(560px 260px at 15% 0%, var(--accent-soft), transparent 65%),
    var(--surface);
}
.quiz-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s4);
}
.quiz-top .section-label { flex: 1 1 auto; }
.quiz-score {
  margin-left: var(--s3);
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  gap: var(--s2);
  padding: 0.2em 0.7em;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--surface-hover);
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  letter-spacing: 0.08em;
  color: var(--muted);
  text-transform: none;
  white-space: nowrap;
  transition: border-color var(--dur), color var(--dur);
}
.quiz-streak {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  color: var(--accent-ink);
  font-weight: 600;
}
.quiz-streak svg { width: 0.85em; height: 0.85em; fill: var(--accent); }
.quiz-score.on-fire { border-color: var(--accent-rule); color: var(--accent-ink); }

/* Countdown ring: an SVG circle whose stroke-dashoffset is transitioned from
   0 (full loop, i.e. full time left) to its own circumference (empty) over
   exactly QUIZ_TIME_MS, linearly — CSS does the ticking, JS only starts it
   and swaps a colour class in the final few seconds. Rotated -90deg so the
   sweep starts at 12 o'clock like a clock face, not 3 o'clock. */
.quiz-timer {
  position: relative;
  flex-shrink: 0;
  width: 2.75rem;
  height: 2.75rem;
}
.quiz-timer svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.quiz-timer-track { fill: none; stroke: var(--line); stroke-width: 3; }
.quiz-timer-ring {
  fill: none;
  stroke: var(--accent);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-dasharray: 119.4;
  stroke-dashoffset: 0;
  transition: stroke-dashoffset linear, stroke var(--dur);
}
.quiz-timer.danger .quiz-timer-ring { stroke: #c0564a; }
.quiz-timer.danger .quiz-timer-num { color: #c0564a; }
.quiz-timer-num {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  font-weight: 500;
  color: var(--muted);
}
.quiz-timer.stopped .quiz-timer-ring { transition: none; }

.quiz-question {
  font-family: 'Instrument Serif', serif;
  font-style: italic;
  font-size: var(--t-h3);
  color: var(--ink);
  margin: var(--s4) 0 var(--s4);
  animation: quizRise 0.4s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
.quiz-options {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--s3);
}
/* Four fixed colours (reusing the same palette the type/diet charts use, so
   the quiz feels like part of the same dataset rather than a bolted-on
   widget) rather than a colour derived from the answer's own data — the
   whole point of a lettered/coloured tile grid is that shape+colour let you
   spot an option before reading it, so the mapping has to be stable, not
   meaningful. */
.quiz-option {
  display: flex;
  align-items: center;
  gap: var(--s3);
  text-align: left;
  padding: var(--s4);
  border: none;
  border-radius: var(--radius);
  border-left: 4px solid var(--quiz-tile-color, var(--line));
  background: var(--quiz-tile-bg, var(--surface-hover));
  color: var(--ink);
  font-family: 'Inter', sans-serif;
  font-weight: 600;
  font-size: var(--t-body);
  cursor: pointer;
  transition: background var(--dur), transform 0.15s ease, box-shadow var(--dur);
  animation: quizRise 0.4s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
.quiz-option:nth-child(1) {
  animation-delay: 0.03s;
  --quiz-tile-color: var(--chart-1);
  --quiz-tile-bg: color-mix(in srgb, var(--chart-1) 14%, var(--surface-hover));
}
.quiz-option:nth-child(2) {
  animation-delay: 0.07s;
  --quiz-tile-color: var(--chart-2);
  --quiz-tile-bg: color-mix(in srgb, var(--chart-2) 14%, var(--surface-hover));
}
.quiz-option:nth-child(3) {
  animation-delay: 0.11s;
  --quiz-tile-color: var(--chart-3);
  --quiz-tile-bg: color-mix(in srgb, var(--chart-3) 14%, var(--surface-hover));
}
.quiz-option:nth-child(4) {
  animation-delay: 0.15s;
  --quiz-tile-color: var(--chart-4);
  --quiz-tile-bg: color-mix(in srgb, var(--chart-4) 14%, var(--surface-hover));
}
.quiz-option-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 2em;
  height: 2em;
  border-radius: 50%;
  background: var(--quiz-tile-color, var(--muted));
  color: #fff;
}
.quiz-option-icon svg { width: 1.05em; height: 1.05em; fill: currentColor; }
.quiz-option:hover:not(:disabled) {
  transform: translateY(-3px);
  box-shadow: 0 8px 20px -10px rgba(0, 0, 0, 0.35);
}
.quiz-option:active:not(:disabled) { transform: translateY(-1px) scale(0.98); }
.quiz-option:disabled { cursor: default; }
.quiz-option.correct {
  border-left-color: #4a8c5f;
  background: rgba(74, 140, 95, 0.18);
  color: #326b45;
  animation: quizPop 0.45s cubic-bezier(0.3, 1.6, 0.4, 1);
  box-shadow: 0 0 0 3px rgba(74, 140, 95, 0.16);
}
.quiz-option.correct .quiz-option-icon { background: #4a8c5f; color: #fff; }
.quiz-option.wrong {
  border-left-color: #c0564a;
  background: rgba(192, 86, 74, 0.16);
  color: #963e33;
  animation: quizShake 0.4s ease;
}
.quiz-option.wrong .quiz-option-icon { background: #c0564a; color: #fff; }
.quiz-option:disabled:not(.correct):not(.wrong) { opacity: 0.5; }
.quiz-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  margin-top: var(--s4);
  min-height: 2.5rem;
}
.quiz-feedback {
  margin: 0;
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  letter-spacing: 0.06em;
  color: var(--muted);
  animation: quizRise 0.3s ease both;
}
.quiz-feedback.is-correct { color: #4a8c5f; }
.quiz-feedback.is-wrong { color: #c0564a; }
/* The "why", shown once a question is answered: every option's actual value
   for the field just asked about, not only the one the reader picked. Sits
   below the footer rather than inside the feedback line itself — four rows
   of data don't fit an inline sentence, and this way it reads as a small
   reference table rather than more prose. */
.quiz-answer-detail {
  margin-top: var(--s3);
  padding-top: var(--s3);
  border-top: 1px solid var(--line);
  animation: quizRise 0.3s ease both;
}
.quiz-answer-detail__label {
  margin: 0 0 var(--s2);
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--accent);
}
.quiz-answer-detail__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.4rem var(--s3);
}
.quiz-answer-detail__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s2);
  padding: 0.35rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-hover);
  font-size: var(--t-small);
}
.quiz-answer-detail__name { color: var(--ink); }
.quiz-answer-detail__value {
  flex-shrink: 0;
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  color: var(--muted);
  text-align: right;
}
/* The one row matching the correct answer, picked out the same green the
   correct tile above already uses — the two are the same fact, so they get
   the same colour. */
.quiz-answer-detail__row.is-correct {
  border-color: #4a8c5f;
  background: rgba(74, 140, 95, 0.12);
}
.quiz-answer-detail__row.is-correct .quiz-answer-detail__value { color: #326b45; font-weight: 600; }
@media (max-width: 640px) {
  .quiz-answer-detail__list { grid-template-columns: 1fr; }
}
.quiz-next {
  animation: quizRise 0.3s ease both;
}

@keyframes quizRise {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}
@keyframes quizPop {
  0% { transform: scale(1); }
  40% { transform: scale(1.035); }
  100% { transform: scale(1); }
}
@keyframes quizShake {
  10%, 90% { transform: translateX(-1px); }
  20%, 80% { transform: translateX(2px); }
  30%, 50%, 70% { transform: translateX(-4px); }
  40%, 60% { transform: translateX(4px); }
}
@media (prefers-reduced-motion: reduce) {
  .quiz-question, .quiz-option, .quiz-feedback, .quiz-next { animation: none; }
  .quiz-timer-ring { transition: none; }
}
@media (max-width: 640px) {
  .quiz-options { grid-template-columns: 1fr; }
}

/* The label already carries the meaning; ::after would put a rule between the
   heading and the button. */
.detail-panel .section-label::after { display: none; }
/* Missing entirely before: every other panel in <main> sets its own
   margin-bottom (overview-grid, timeline-panel, size-panel...), but this one
   didn't, so it sat flush against "When they lived" with a 0px gap — their
   borders touched, reading as one merged card with an internal seam rather
   than two separate panels. */
.detail-panel { margin-bottom: var(--s5); }

.detail-stale {
  margin: 0 0 var(--s4);
  padding: var(--s2) var(--s3);
  border-left: 2px solid var(--accent);
  background: var(--accent-soft);
  color: var(--ink);
  font-size: var(--t-small);
}

#detail-name:focus-visible { outline-offset: 4px; }

/* --- Touch targets: the filter-removal and taxonomy controls are the
       recovery paths, and all sat under the 24px minimum. ---------------- */
.search-clear, .active-filter button {
  min-width: 24px;
  min-height: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.tx-more { min-height: 28px; }
.chart-legend li { min-height: 24px; }

/* Legend entries are the accessible control for cross-filtering; the pie is a
   pointer shortcut to the same thing. */
.chart-legend button {
  display: flex;
  align-items: center;
  gap: var(--s1);
  width: 100%;
  padding: 0.2rem var(--s1);
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  /* At the 2-column split, the longest label ("armoured dinosaur (5)") comes
     within a fraction of a pixel of its column's width — enough that some
     browsers round down and wrap it, while every shorter label sits with
     visible room to spare. There's no real overflow to solve, so nowrap alone
     (not truncation) is the fix. */
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--dur), border-color var(--dur);
}
.chart-legend button:hover { background: var(--surface-hover); }
/* The active category is marked, not merely left undimmed. */
.chart-legend button.is-active {
  border-color: var(--accent);
  color: var(--ink);
}
/* The selected dinosaur's slice, same dark outline the timeline bar and size
   dot use for the same purpose — separate from is-active (a filter) since
   selecting one animal shouldn't read as filtering by its whole category. */
path[data-key].is-selected-key { stroke: var(--ink); stroke-width: 2; }
.chart-legend button.is-selected-key {
  border-color: var(--ink);
  color: var(--ink);
}
/* Same marker on the timeline/size charts' static type key, so the selected
   dot's colour can be looked up there too, not just spotted in the chart —
   and the same border-around-the-whole-row treatment as the pie legend
   above, not just a ring on the swatch, so the two read as one convention. */
.chart-key li.is-selected-key {
  border-color: var(--ink);
  color: var(--ink);
}

/* --- Deep-time chart ------------------------------------------------------ */
.timeline-panel { margin-bottom: var(--s5); }
.timeline-hint {
  margin-left: auto;
  flex-shrink: 0;
  color: var(--muted);
  text-transform: none;
  letter-spacing: 0.02em;
}
.timeline-panel .section-label::after { display: none; }
/* Sized down from the shared .btn scale to sit comfortably inline in a
   section-label heading next to a serif title, rather than at the same
   weight as a page-level CTA. */
.timeline-reset {
  margin-left: auto;
  flex-shrink: 0;
  padding: 0.3rem 0.7rem;
}
#timeline { width: 100%; }
.tl-svg { display: block; touch-action: none; cursor: crosshair; }

/* Epoch bands are context, not content: they must never compete with the bars. */
.tl-band { fill: var(--surface-hover); opacity: 0.5; cursor: pointer; }
.tl-band--alt { fill: transparent; }
.tl-band:hover { opacity: 0.9; }
.tl-band-label {
  font-family: 'JetBrains Mono', monospace;
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  fill: var(--muted);
  pointer-events: none;
}

.tl-bar { cursor: pointer; }
/* Out-of-filter bars stay on screen so the brush has something to drag
   against — the chart is the dataset, the filters are a lens over it. */
.tl-bar--out { opacity: 0.13; }
/* Lighter than --out: this mark is still a real result, just not the one
   selected, so it fades rather than disappears the way a filtered-out mark
   does. */
.tl-bar--dim { opacity: 0.35; }
.tl-bar--sel { stroke: var(--ink); stroke-width: 1.5; opacity: 1; }
.tl-bar:hover { stroke: var(--ink); stroke-width: 1; }

.tl-brush {
  fill: var(--accent);
  opacity: 0.16;
  stroke: var(--accent);
  stroke-width: 1;
  pointer-events: none;
}

.tl-axis, .tl-tick { stroke: var(--line); stroke-width: 1; }
.tl-tick-label, .tl-axis-label {
  font-family: 'JetBrains Mono', monospace;
  font-size: 9px;
  fill: var(--muted);
}
.tl-axis-label { letter-spacing: 0.08em; text-transform: uppercase; }

.timeline-note {
  margin: var(--s3) 0 0;
  font-size: var(--t-micro);
  line-height: 1.6;
  color: var(--muted);
  max-width: 78ch;
}

@media (max-width: 640px) {
  .timeline-hint { display: none; }
}
.tl-band-edge { stroke: var(--line); stroke-width: 1; pointer-events: none; }

/* --- Size chart ----------------------------------------------------------- */
.size-panel { margin-bottom: var(--s5); }
.size-panel .section-label::after { display: none; }
#size-chart { width: 100%; }
.sz-svg { display: block; touch-action: none; cursor: crosshair; }

.sz-dot { cursor: pointer; }
/* Same convention as the timeline: filtered-out points stay on screen so the
   brush always has the full distribution to drag against. */
.sz-dot--out { opacity: 0.13; }
/* Lighter than --out: this dot is still a real result, just not the one
   selected, so it fades rather than disappears the way a filtered-out dot
   does. */
.sz-dot--dim { opacity: 0.35; }
.sz-dot--sel { stroke: var(--ink); stroke-width: 2; opacity: 1; }
.sz-dot:hover { stroke: var(--ink); stroke-width: 1; }

/* Drawn over the dots, not behind them — it is the reference the whole chart
   is read against. */
.sz-human line {
  stroke: var(--ink);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  opacity: 0.75;
}
.sz-human text {
  font-family: 'JetBrains Mono', monospace;
  font-size: 9px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  fill: var(--muted);
}

.sz-missing {
  margin: var(--s2) 0 0;
  font-size: var(--t-micro);
  color: var(--muted);
  font-family: 'JetBrains Mono', monospace;
}

/* A configured view is a citable object — this is how you take it with you. */
.filter-clear--link { white-space: nowrap; }
@media (max-width: 860px) {
  .filter-clear--link { grid-column: 1 / -1; justify-self: stretch; }
}

/* Leaflet's zoom controls ship white with dark glyphs, which read as a pair of
   bright chips punched into a dark map. Themed to match the panel vocabulary. */
.leaflet-bar a,
.leaflet-bar a:hover {
  background: var(--panel-solid);
  color: var(--ink);
  border-bottom-color: var(--line);
}
.leaflet-bar a:hover { background: var(--surface-hover); color: var(--accent); }
.leaflet-bar {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 2px 10px var(--shadow);
}
.leaflet-bar a:first-child { border-radius: var(--radius) var(--radius) 0 0; }
.leaflet-bar a:last-child { border-radius: 0 0 var(--radius) var(--radius); }

.map-note {
  margin: var(--s2) 0 0;
  font-size: var(--t-micro);
  line-height: 1.6;
  color: var(--muted);
}
.map-note a { color: var(--accent); }

/* Dig-site markers sit above the choropleth fill. */
.dig-site-note { color: var(--accent); }

/* --- Hero showcase ---------------------------------------------------------
   Two elements per slide, modelled on the Harvard Museum of Natural History
   reference: an engraved habitat dome, and the animal isolated on transparent
   background standing in front of it. The animal deliberately breaks the top
   edge of the dome — that overlap is what makes it read as standing ON a
   mound rather than being pasted inside a frame, and it's why these have to be
   cut-outs rather than the dataset's flat JPEGs. */
.showcase {
  /* One shared duotone recipe for both the engraving and the digital cut-out:
     flattened to grayscale first (removes the cut-out's smooth painted
     colour), then a single warm ink tint reapplied to both, so the two very
     different source techniques land on the same limited parchment/ink
     palette instead of clashing. */
  --showcase-duotone: grayscale(1) sepia(0.35) saturate(1.4) contrast(1.08) brightness(0.95);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0 var(--s5);
  margin: var(--s6) auto 5rem;
  max-width: 1650px;
}

.showcase-nav {
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--muted);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  transition: border-color var(--dur), color var(--dur);
}
.showcase-nav:hover { border-color: var(--accent); color: var(--accent); }

.showcase-stage {
  position: relative;
  flex: 1 1 auto;
  max-width: 1400px;
  /* Shows a shallow cap of a much larger circle, plus headroom above the rim.
     A full semicircle would be 2:1, but these animals are ~2.8:1, so one
     sized to span a semicircle top-to-bottom would have to be twice the
     dome's width. A shallower arch lets the animal cover the whole visible
     dome and still break well out of the top, as in the reference. */
  /* Geometry is expressed as ratios from here down, not fixed pixels, so the
     whole composition scales as one unit at any width and needs no
     per-breakpoint retuning. The ratios are the ones that tested best:
     circle = 0.70 x stage width, dino = 1.20 x circle, rim at 0.10 x circle.
     Height follows from those — it has to clear the animal's feet, which is
     what was cropping them when the stage was set independently. */
  /* Height = rim offset (0.10D) + visible cap (0.48D) = 0.58D, and D is 0.70
     of this width, so H = 0.406W. The cap is deliberately just UNDER the
     circle's radius: at exactly 0.50D you reach the equator, and any deeper
     the sides curve back inward so the shape stops reading as a dome — and
     the disc's far pole rises high enough that the *other* dinosaur's feet
     appear over the horizon. */
  aspect-ratio: 1000 / 406;
  height: auto;
  overflow: hidden;
  /* Soft fade at the horizon instead of a hard clip line, so the dinosaur
     sinking below it thins out rather than getting guillotined. Kept shallow
     so it never reaches the feet of the animal standing in front. */
  -webkit-mask-image: linear-gradient(to bottom, #000 97%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 97%, transparent 100%);
}

/* One flat disc, spun purely in the plane of the screen — rotate(), not
   rotateY/rotateX, no perspective, no third axis. Two dinosaurs are fixed to
   opposite poles of that disc (.showcase-wheel-dino-a at the top, -b at the
   bottom, baked in already rotated 180° from -a) the way two pegs sit on
   opposite sides of a wheel-of-fortune. The disc's own backdrop — the
   engraving — is part of the same rigid object, so it spins with them: one
   continuous circular surface, like a globe, that simply carries a different
   animal at a different point along its rim.
   .showcase-wheel-mount only centres that disc so its equator sits on the
   stage's horizon line (half of it always below, clipped by the stage's
   overflow); .showcase-wheel-spin is the piece Web Animations actually
   rotates. As it turns 180°, whichever dinosaur was above the horizon arcs
   down and under it while the other arcs up from underneath — same motion,
   same object, so the current animal is never cut or crossfaded, only
   carried out of and into view along the disc's own curve. */
.showcase-wheel-mount {
  position: absolute;
  left: 50%;
  /* Anchored from the top so the rim sits at a known height and the headroom
     above it is predictable; the rest of the disc runs off the bottom of the
     stage and is clipped, leaving a shallow, wide arch. */
  /* Raised so the arch shows a deeper slice of the circle rather than just
     its crown. How much of each engraving is visible scales 1:1 with this
     depth, so it is the main lever on "the background looks cropped" — the
     limit is that content compresses toward the disc's centre, by
     (1 - capDepth/radius) at the base of the arch. */
  /* 0.10 x the circle's own diameter, expressed against the stage height. */
  top: 17.2%;
  /* Never wider than the stage that clips it. The stage has overflow:hidden
     to make the horizon, and a disc wider than the stage gets its sides
     sliced off by that same clip — two straight vertical cuts through the
     arch. Capping at 100% means the whole circle always fits across, so only
     its bottom is ever cut, which is the horizon we actually want. */
  width: 70%;
  aspect-ratio: 1;
  transform: translateX(-50%);
}

.showcase-wheel-spin {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  will-change: transform;
}
/* The landscape is a wide band laid across the disc's rim, not a texture
   stretched over the whole disc: `cover` on a 940px circle blew a 3.3:1
   engraving up to roughly double size and cropped away everything legible,
   so it fits to the disc's *width* instead and sits at the pole, which is
   the only part the stage ever reveals.
   It's drawn twice — ::before at the top pole, ::after flipped to the bottom
   one — because the disc turns 180°: whatever the rim shows now, the
   opposite pole has to show after the spin, exactly like the two dinosaurs.
   Both carry the same ink/parchment duotone as the animals so the engraving
   and the cut-outs read as one object rather than two techniques. */
/* Each plate fills exactly half the disc, unwarped: ::before the top half,
   ::after the bottom half via a 180deg turn, so each is upright when its own
   pole is at the top. `100% 50%` on a plate cropped to exactly 2:1 means it
   fits that half at its true aspect — no stretching.
   This replaces an earlier polar warp that wrapped the engraving around the
   full circumference. The warp guaranteed ground at every angle, but it bent
   the plate's straight hatching into concentric arcs, which is what made the
   backdrop read as an odd woven texture rather than a landscape. Between the
   two halves the disc is now fully covered, so nothing goes blank mid-turn;
   the cost is that at intermediate angles the scenery is simply rotated,
   which is what a real spinning disc would do anyway. */
.showcase-wheel-spin::before,
.showcase-wheel-spin::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background-repeat: no-repeat;
  background-size: 100% 50%;
  background-position: center top;
  filter: var(--showcase-duotone);
}
.showcase-wheel-spin::before {
  background-image: url('../images/specimens/habitat.jpg');
}
.showcase-wheel-spin::after {
  background-image: url('../images/specimens/habitat2.jpg');
  transform: rotate(180deg);
}

.showcase-wheel-dino {
  position: absolute;
  /* Above both halves of the disc. ::after is generated as the spin element's
     last child, so without this it paints straight over the animal. */
  z-index: 1;
  left: 50%;
  /* Nearly as wide as the disc itself — in the reference the animal is the
     subject and the engraving is its setting, not the other way round. */
  /* 1.20 x the circle: the animal is wider than the mound it stands on, which
     is what makes it read as the subject rather than as scenery. */
  /* --showcase-scale is set per-specimen from SHOWCASE_SPECIMENS in app.js and
     defaults to 1. object-fit: contain means shrinking the box shrinks the
     animal inside it without distortion, so this is a straight size dial for
     specimens whose crop reads too large next to the rest of the set. */
  width: calc(105% * var(--showcase-scale, 1));
  /* Capped so a tall/narrow animal (portrait crop, aspect < ~1.3) can't blow
     the box out vertically. Each pole's box sits mostly off the visible cap
     (mount is much taller than the stage that clips it) — but only if the
     box stays short enough that its far edge doesn't creep back into the
     stage. Past ~66% of the mount's diameter the far pole's box overlaps the
     near pole's visible area and its feet start showing up in the other
     animal's scene. object-fit: contain then shrinks any image that would
     exceed either the width or this height, instead of stretching it. */
  height: calc(48% * var(--showcase-scale, 1));
  object-fit: contain;
  /* Deliberately NOT duotoned. The engraving stays monochrome and the animal
     stays in full colour — that contrast is what makes it read as the subject
     rather than part of the scenery, and it is what the reference does: a
     colour painting standing on a black-and-white plate. Only the shadow ties
     it to the ground. */
  filter: drop-shadow(0 18px 20px rgba(0, 0, 0, 0.45));
}
/* Straddling the rim, as in the reference: most of the animal stands over
   the illustrated ground inside the dome, while its head and back break out
   above the top edge. Anchored at the pole and lifted by about a third of
   its own height, so roughly a third clears the rim and two thirds overlap
   the landscape. Upright when the wheel is unrotated. */
.showcase-wheel-dino-a {
  top: 0;
  /* Lifted just clear of the rim so the animal's feet land on the dome's
     baseline rather than below it: it covers the dome top to bottom and
     breaks out over the top edge, without spilling past the horizon. */
  transform: translate(
    calc(-50% + var(--showcase-shift, 0%)),
    calc(-8% + var(--showcase-drop, 0%))
  );
}
/* The antipodal peg: the exact point-reflection of -a through the disc's
   centre (same offset, mirrored, plus the 180° bake-in), so once the wheel
   itself turns 180° this one arrives in precisely the position -a started
   in. Until then it's the upside-down animal on the far, hidden side of the
   disc — the "opposite side" the wheel carries around. */
.showcase-wheel-dino-b {
  bottom: 0;
  /* --showcase-drop and --showcase-shift are negated here: this peg is the
     point-reflection of -a, so a nudge that reads as "down the dome" or
     "to the right" once it rotates up into view is a nudge the other way in
     its own unrotated frame. */
  transform: translate(
      calc(-50% - var(--showcase-shift, 0%)),
      calc(8% - var(--showcase-drop, 0%))
    )
    rotate(180deg);
}

/* Name, meta and the view-details link get their own centred row below. */
.showcase-caption {
  flex-basis: 100%;
  text-align: center;
  margin-top: var(--s3);
}
.showcase-name {
  font-family: 'Instrument Serif', serif;
  font-style: italic;
  font-size: var(--t-h3);
  color: var(--ink);
  margin: 0;
}
.showcase-meta {
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  letter-spacing: 0.06em;
  color: var(--muted);
  margin: var(--s1) 0 0;
}
.showcase-view {
  margin-top: var(--s2);
  border: none;
  background: none;
  padding: 0;
  font-family: 'JetBrains Mono', monospace;
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--accent);
  cursor: pointer;
  border-bottom: 1px solid var(--accent-rule);
}
.showcase-view:hover { color: var(--ink); border-bottom-color: var(--ink); }

@media (max-width: 860px) {
  /* Geometry is ratio-based now and scales on its own; only the chrome needs
     adjusting here. */
}

@media (max-width: 640px) {
  .showcase { gap: 0 var(--s2); }
  .showcase-nav { width: 36px; height: 36px; }

}
