/* Resource Menu — environmental epidemiology policy & advocacy resources */

:root {
  /* This page is light-only by design — see the note below the palette
     for why there's no dark-mode variant of it. Telling the browser that
     explicitly also stops it from dark-moding form controls/scrollbars
     on top of that. */
  color-scheme: light;

  --bg: #ffffff;
  --surface: #ffffff;
  --surface-2: #f1f3f6;
  --border: #e2e6ec;
  --border-strong: #cfd6e0;
  --text: #131720;
  --text-2: #5a6472;
  --text-3: #8a93a2;

  /* Brand palette (ISEE-aligned) — fixed navy/slate. Used directly (not
     through --accent below) by the header bar and the search input,
     which both intentionally keep their original navy styling even
     though the general accent has moved to teal. */
  --brand-navy: #0b6fa1;
  --brand-navy-deep: #044768;
  --brand-green: #3a95a0;
  --brand-slate: #5b84a8;
  --search-border: var(--brand-slate);
  --search-border-focus: var(--brand-navy);
  --search-ring: 0 0 0 3px rgba(27, 58, 92, 0.28);

  /* General accent — buttons, links, selected/focus states, Organization
     pills, etc. site-wide. A muted teal/sage rather than the brand navy
     above, which stays reserved for the header bar and search input
     specifically (see --search-* above). */
  --accent: #0b6fa1;
  --accent-2: #3a95a0;
  --accent-soft: color-mix(in srgb, var(--accent) 10%, white);
  --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.05);
  --shadow-md: 0 4px 12px rgba(16, 24, 40, 0.08);
  --shadow-lg: 0 14px 34px rgba(16, 24, 40, 0.14);
  --radius: 5px;
  --radius-sm: 8px;
  --ring: 0 0 0 3px rgba(46, 96, 88, 0.28);
  --type-s: 62%;
  --type-l: 42%;
  --type-tint-l: 96%;
  /* Deliberately darker than --type-l: this one's specifically for white
     text sitting directly on a solid hue fill (see .pill[data-filter=
     "type"] below), which needs more headroom than a dot or a stripe
     does. --type-l's 42% clears WCAG AA (4.5:1) for white text on most
     of app.js's HUES but falls short on the lighter/warmer ones (e.g.
     hue 70, a yellow-green, lands around 3.7:1) — 28% clears all of them
     with room to spare. */
  --type-fill-l: 28%;

  /* Dedicated accent for "Another random resource" (see .btn-random) —
     the same pink/magenta hue used on some Type tags (hue 340 in app.js's
     HUES list), so the playful random-pick feature has its own on-brand
     color distinct from the teal used for primary/neutral actions
     elsewhere. */
  --random-h: 340;
  --random-s: 68%;
  --random-l: 38%;

  /* Type scale — every font-size on the site maps to one of these seven
     steps, chosen for what the text IS, not which element happens to
     render it. Two elements serving the same purpose (a card title, a
     filter pill, a caption) always share the same variable, on both
     pages. This replaced ~18 one-off px values (10px-20px, plus three
     slightly different clamp() formulas) that had accumulated with no
     shared reasoning behind them.
       --fs-page-title  the <h1> site title (Policy Menu, Research
                         Resources, Privacy Policy)
       --fs-heading      a section or modal heading one level down from
                         the page title (modal titles/subtitles, "Start
                         Here", an empty-state heading, policy page h2s)
       --fs-card-title   a resource card's own title (.card h3)
       --fs-control      a primary, standalone interactive control's own
                         text — currently just the search input and the
                         "New to advocacy?" toggle
       --fs-body         paragraph/description text meant to be read,
                         not scanned — card descriptions, intro
                         paragraphs, modal/tooltip copy, error text
       --fs-label        short, scannable UI text: pill/tag labels, the
                         result count, breadcrumbs, nav links, toast
       --fs-meta         the smallest tier — captions, counts-on-counts
                         (a pill's own number badge), filter section
                         labels (TYPE/CREATOR), footer/timestamp text
     Mobile-only overrides that compress one of these for a tight
     viewport (search input, some pill/tag padding) are left as their
     own literal values rather than routed through a smaller variable —
     they're a responsive adjustment to an already-tokenized size, not a
     distinct purpose of their own. */
  --fs-page-title: clamp(20px, 3vw, 28px);
  --fs-heading: 17px;
  --fs-card-title: 16px;
  --fs-control: 15px;
  --fs-body: 14px;
  --fs-label: 13px;
  --fs-meta: 11px;
}

/* Deliberately no @media (prefers-color-scheme: dark) override anywhere
   in this file — the design has only ever been built and tested in light
   mode, so a visitor with a dark-mode OS/browser setting used to get an
   unintended, untested mix of colors instead of it. */

* { box-sizing: border-box; }

/* The browser's own [hidden] { display: none } rule lives in the UA
   stylesheet, which loses to *any* author rule setting display on the
   same element regardless of specificity (e.g. .btn's display:inline-flex)
   — origin beats specificity in the cascade. Without this, toggling the
   hidden attribute via JS silently does nothing on anything that also
   has a display-setting class, like #back-to-all (.btn). */
[hidden] { display: none !important; }

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto,
    "Helvetica Neue", Arial, sans-serif;
  font-size: var(--fs-body);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

/* 1600px keeps line length and card width readable on ultra-wide monitors
   instead of stretching content edge-to-edge; the fluid side padding scales
   continuously with viewport width rather than jumping at one breakpoint. */
.wrap {
  max-width: 1600px;
  margin: 0 auto;
  padding: 14px clamp(16px, 4vw, 40px) 96px;
}


/* ---------- Defaults for hyperlinks --------- */
a {
  color: var(--brand-green);
  text-decoration-color: var(--brand-green); /* keeps underline same color */
}

a:visited {
  color: var(--brand-green);
}

a:hover,
a:focus-visible {
  color: var(--brand-navy);            /* optional: darker on hover */
  text-decoration-color: var(--brand-navy);
}

/* ---------- Header / nav bar ---------- */

/* White banner with a navy bottom border standing in for the navy fill
   the bar itself used to have — separates it from the page content
   below without needing a solid navy block to do it. */
.site-header {
  background: #fff;
  color: var(--brand-navy-deep);
  /* border-bottom: 3px solid var(--brand-navy); */
}

.site-header-inner {
  max-width: 1600px;
  margin: 0 auto;
  /* Vertical padding upped to give the larger logo below room to breathe
     without cramping the bar. */
  padding: 30px clamp(16px, 4vw, 40px);
  /* Fixed across every page that shares this header (index.html,
     privacy-policy.html, research-resources.html) so navigating between
     them never jumps in height just because one page's subtitle wraps
     to a different number of lines than another's. Sized to comfortably
     fit a two-line subtitle (the tallest case) without clipping, with
     align-items:center below keeping a shorter one-line subtitle
     vertically centered rather than top-aligned in the extra space. */
  min-height: 150px;
  display: flex;
  align-items: center;
  /* .brand on the left, the Start Here pill (see below) pinned to the
     far right, opposite it. */
  justify-content: space-between;
  gap: 16px;
}

.brand {
  display: flex;
  align-items: center;
  gap: 16px;
  min-width: 0;
}

/* No card/background needed here now that the bar itself is white — the
   source PNG's own background is already opaque white (confirmed by
   sampling its corners), so it sits flush and seamless directly on the
   bar. The old white card was specifically to stay legible against the
   navy fill this bar no longer has. Sized by height with width auto so
   it scales proportionally and never stretches. */
.brand-logo {
  height: 74px;
  width: auto;
  flex: none;
  display: block;
  object-fit: contain;
}

.brand-text {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}

.brand-name {
  margin: 0;
  font-size: var(--fs-page-title);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.25;
  color: var(--brand-navy-deep);
}

/* Stacked directly under the title now (previously its own centered
   paragraph below the header entirely) — a plain subdued line rather
   than the old centered/accented treatment, reading as a subtitle to
   the heading above it rather than a standalone statement. Capped
   width keeps it from stretching edge-to-edge next to a short title on
   very wide screens. */
.brand-subtitle {
  margin: 0;
  max-width: 640px;
  color: var(--brand-navy-data);
  opacity: 0.82;
  font-size: var(--fs-body);
  line-height: 1.45;
}

/* ---------- Start Here ---------- */

/* Holds both header pills (Start Here + quick-read), stacked vertically
   at the header's far right, opposite .brand — replaces
   .start-here-toggle's old position as a lone flex:none child of
   .site-header-inner now that there are two. align-items: stretch (not
   flex-end) makes both pills the same width: a column flex container
   with auto width sizes itself to its widest child's content, then
   stretches every child to that width — so the shorter-text pill grows
   to match the longer one instead of each hugging its own text. */
.header-ctas {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 10px;
  flex: none;
}

/* Pill living inside the header bar itself (see .site-header-inner's
   justify-content:space-between), opposite the logo/title, rather than
   its own full-width bar below it. Solid --accent fill (the site's teal
   accent) reads clearly against the header's white background.
   Collapsed by default on every load (app.js sets no persisted state),
   so this is the only way back into Start Here once dismissed. Shared
   by quick-read-toggle below it — same color/size/chevron treatment,
   just a second instance of this class rather than a variant.
   justify-content: center keeps its text+chevron centered once
   .header-ctas stretches it wider than its own content (see above) —
   inline-flex's default flex-start would otherwise left-hug the text
   inside the now-wider pill instead of centering it. */
.start-here-toggle {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 11px 27px;
  font: inherit;
  font-size: var(--fs-control);
  font-weight: 700;
  color: #fff;
  white-space: nowrap;
  background: var(--brand-green);
  border: 1px var(--brand-green);
  border-radius: 999px;
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: background 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
}

.start-here-toggle:hover,
.start-here-toggle:focus-visible {
  background: var(--brand-navy);
  box-shadow: var(--shadow-md);
  transform: translateY(-1px);
}
.start-here-toggle:focus-visible { outline: none; box-shadow: var(--ring); }

.start-here-toggle .chevron {
  flex: none;
  width: 15px;
  height: 15px;
  transition: transform 0.2s ease;
}

/* Reads the button's own aria-expanded (set in app.js alongside the
   section's hidden attribute) rather than a separate class. */
.start-here-toggle[aria-expanded="true"] .chevron {
  transform: rotate(180deg);
}

/* Positioning context for start-here-section below — same role as
   .quick-read-wrap further down, just wider since this one has to fit
   the four-card grid rather than a couple lines of text. */
.start-here-wrap {
  position: relative;
  display: flex;
  flex-direction: column;
}

/* start-here-toggle's dropdown menu — floats directly under the button
   (like a real nav dropdown) instead of pushing page content down.
   right:0 (not left:0 like quick-read-panel) anchors it to the button's
   right edge and lets it grow leftward, since .header-ctas sits at the
   header's far right and a left-anchored panel this wide would run off
   the viewport. Width is capped both by a fixed max (comfortably fits
   the grid's four 220–340px cards in 2–3 columns) and by viewport width
   (calc(100vw - 32px)) so it never overflows a narrow screen. */
.start-here {
  position: absolute;
  z-index: 45;
  top: calc(100% + 8px);
  right: 0;
  width: min(720px, calc(100vw - 32px));
  margin: 0;
  background: var(--accent-soft);
  border: 1px solid color-mix(in srgb, var(--brand-green) 22%, var(--border));
  border-radius: calc(var(--radius) + 4px);
  padding: 10px 14px 12px;
  box-shadow: var(--shadow-md);
}

/* Positioning context for the dropdown panel below — a plain wrapper
   (not .header-ctas itself) so the panel anchors to this one button
   specifically rather than spanning both stacked pills. display:flex
   with the default align-items:stretch lets the button inside still
   fill the width .header-ctas stretches this wrapper to (see
   .header-ctas above) — the panel itself doesn't count toward that
   width since position:absolute pulls it out of normal flow. */
.quick-read-wrap {
  position: relative;
  display: flex;
  flex-direction: column;
}

/* quick-read-toggle's dropdown menu — floats directly under the button
   (like a real nav dropdown) instead of pushing page content down.
   Same light accent-fill box treatment as .start-here above, just
   narrower (matches the trigger button's own width, via left/right:0
   against .quick-read-wrap) and stacked column-wise since that width
   is too narrow for the description and link to sit side by side. */
.quick-read-panel {
  position: absolute;
  z-index: 45;
  top: calc(100% + 8px);
  left: 0;
  right: 0;
  background: var(--accent-soft);
  border: 1px solid color-mix(in srgb, var(--brand-green) 22%, var(--border));
  border-radius: calc(var(--radius) + 4px);
  padding: 14px 16px;
  box-shadow: var(--shadow-md);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 10px;
}

.quick-read-panel-desc {
  margin: 0;
  color: var(--text);
  font-size: var(--fs-body);
  line-height: 1.5;
}

.quick-read-panel .btn { justify-content: center; }

.start-here-title {
  margin: 0 0 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  font-size: var(--fs-heading);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--brand-navy);
  text-align: center;
}

.start-here-icon { display: inline-flex; flex: none; }

/* Fixed 2 columns (not auto-fit) so the panel's four cards always land
   2x2 rather than however many 220px columns happen to fit the
   dropdown's ~720px width (auto-fit was giving 3-then-1). */
.start-here-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: clamp(6px, 1vw, 10px);
}

@media (min-width: 721px) {
  .start-card { max-width: 340px; }
}

.start-card {
  text-align: left;
  font: inherit;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 10px 12px;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 3px;
  box-shadow: var(--shadow-sm);
  transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}

.start-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); border-color: var(--accent-2); }
.start-card:focus-visible { outline: none; box-shadow: var(--ring); }

.start-card-title { font-size: var(--fs-body); font-weight: 640; letter-spacing: -0.005em; }
.start-card-desc { font-size: var(--fs-meta); color: var(--text-2); line-height: 1.5; }

.start-card[aria-pressed="true"] {
  border-color: var(--accent);
  background: var(--accent-soft);
  box-shadow: var(--shadow-sm), inset 0 0 0 1px var(--accent);
}

/* ---------- Controls ---------- */

/* Only the compact search row lives here — not the Type/Subtype/
   Organization filter area (that's .filter-bar, a plain sibling below,
   never sticky). Keeping .controls small like this is deliberate: a
   sticky element reserves layout space equal to its own box size
   regardless of any transform applied to it, so hiding a *tall* sticky
   element via transform leaves its full height reserved as a blank gap
   (that was the original bug). A compact search row's reserved height is
   small enough that this is a non-issue, which is what makes a simple
   transform-based hide safe here without resorting to animating height. */
.controls {
  position: sticky;
  top: 0;
  z-index: 30;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: saturate(1.6) blur(10px);
  -webkit-backdrop-filter: saturate(1.6) blur(10px);
  border-bottom: 1px solid transparent;
  padding: 6px 0 5px;
  margin-bottom: 2px;
}

.controls.is-stuck {
  border-bottom-color: var(--border);
  box-shadow: 0 6px 18px -12px rgba(16, 24, 40, 0.5);
}

.searchrow {
  display: flex;
  gap: 10px;
  align-items: stretch;
  flex-wrap: wrap;
}

.searchbox {
  position: relative;
  flex: 1 1 320px;
  min-width: 0;
}

/* Class-based (not ID-based) so the exact same search-bar look is
   shared by every page's search input, not just the Policy Menu's own
   #search — see .search-icon/.search-input on the Research Resources
   page. Scoped to .search-icon specifically (not a blanket .searchbox
   svg) — the random-hint button below also has an svg inside
   .searchbox (the dice icon), which needs to stay in normal inline
   flow within its own button rather than inheriting this leading
   icon's absolute positioning. */
.search-icon svg {
  position: absolute;
  left: 13px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--text-3);
  pointer-events: none;
}

/* Deliberately the most visually prominent control on the page — a
   thicker accent-toned border plus a standing shadow (not just on focus)
   so it reads as the primary way in, ahead of Match/filter categories
   below it (see .filter-group-box, .segmented). */
/* Border/focus intentionally use the dedicated --search-* variables
   (brand navy) rather than the general --accent, which keeps this the
   one prominent control that still reads as navy-branded even as the
   rest of the page's accent color has moved to teal. */
.search-input {
  width: 100%;
  /* Right padding sized to clear .search-hints below (clear-search +
     the random-resource hint, now the last/rightmost item there). */
  padding: 12px 144px 12px 40px;
  font: inherit;
  font-size: var(--fs-control);
  color: var(--text);
  background: var(--surface);
  border: 1.5px solid var(--search-border);
  border-radius: 11px;
  outline: none;
  box-shadow: var(--shadow-sm);
  transition: border-color 0.16s ease, box-shadow 0.16s ease;
}

.search-input::placeholder { color: var(--text-3); }
.search-input:focus { border-color: var(--search-border-focus); box-shadow: var(--search-ring); }

/* Houses the clear-search (×) button and the random-resource hint, left
   to right — random-hint last/rightmost so it sits at the far right
   edge of the search bar. (The "/" keyboard-shortcut kbd that used to
   live here too has been dropped — see the footer's own "press / to
   search" mention for that hint now.) */
.search-hints {
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  gap: 8px;
}

kbd {
  font: 600 11px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--text-3);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-bottom-width: 2px;
  border-radius: 5px;
  padding: 4px 6px;
}

/* Small, muted hint replacing the old standalone "Random resource"
   button — deliberately subtle (light, no fill/border by default) so
   it reads as a hint alongside clear/slash rather than competing with
   the search input for attention. font-style intentionally left at
   the inherited normal (not italic) so it reads as the same font as
   the search placeholder beside it, not a visually distinct one.
   Padding well beyond the visible icon+text gives it a real hit
   target despite the small footprint; the matching negative margin
   cancels that padding back out of .search-hints' layout so it
   doesn't visually push the other hints further right than the
   icon+text alone would. */
.random-hint {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border: 0;
  background: none;
  padding: 8px 5px;
  margin: -8px -5px;
  cursor: pointer;
  color: var(--text-3);
  font: inherit;
  font-size: var(--fs-meta);
  white-space: nowrap;
  border-radius: 6px;
  transition: color 0.15s ease, background 0.15s ease;
}

.random-hint:hover,
.random-hint:focus-visible {
  color: var(--accent-2);
  background: var(--accent-soft);
}
.random-hint:focus-visible { outline: none; box-shadow: var(--ring); }

.random-hint:disabled {
  opacity: 0.4;
  cursor: default;
  pointer-events: none;
}

/* Fixed to --accent (not inherited from .random-hint's own light/hover
   text color) — the same teal/sage the "New to advocacy?" header pill
   uses, via the same variable rather than a separately-hardcoded
   value, so the two stay in sync if --accent ever changes again. Fixed
   regardless of hover/focus state, unlike the text beside it. */
.random-hint svg {
  flex: none;
  width: 19px;
  height: 19px;
  color: var(--accent);
}

.clear-search {
  border: 0;
  background: var(--surface-2);
  color: var(--text-2);
  width: 22px;
  height: 22px;
  border-radius: 50%;
  cursor: pointer;
  font-size: var(--fs-body);
  line-height: 1;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 0;
}

.clear-search:hover { background: var(--border-strong); color: var(--text); }
.searchbox.has-value .clear-search { display: flex; }

.btn {
  font: inherit;
  font-weight: 560;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 11px;
  padding: 11px 16px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  white-space: nowrap;
  transition: transform 0.12s ease, box-shadow 0.16s ease,
    background 0.16s ease, border-color 0.16s ease;
}

.btn:hover:not(:disabled) { border-color: var(--accent-2); color: var(--accent-2); box-shadow: var(--shadow-sm); }
.btn:active:not(:disabled) { transform: translateY(1px); }
.btn:focus-visible { outline: none; box-shadow: var(--ring); }

.btn:disabled {
  opacity: 0.42;
  cursor: default;
  color: var(--text-3);
}

/* Several .btn instances are <a href> elements (back-link, the feedback
   CTAs, the homepage research callout below), not <button>s — without
   this, the browser's default :visited style would recolor their text
   once clicked, which reads as broken/faded rather than intentional. */
.btn:visited { color: var(--text); }

/* Primary CTA (Random resource) — solid accent teal, darker teal on hover */
.btn-primary {
  color: #fff;
  background: var(--accent);
  border-color: var(--accent);
}

.btn-primary:hover:not(:disabled) {
  background: var(--accent-2);
  border-color: var(--accent-2);
  color: #fff;
}

/* Overrides .btn:visited above for the solid-fill variant — same
   specificity, so it must come after in source order to win. */
.btn-primary:visited { color: #fff; }

/* "Another random resource" — the primary, repeatable action while a
   pick is showing, styled distinctly from the neutral "Back to all
   resources" beside it: pill-shaped rather than the standard rounded-
   rectangle .btn, filled with a dedicated pink/magenta (--random-h, tied
   to the same hue used on some Type tags) instead of the teal every
   other primary action uses, so it reads as its own playful thing tied
   to the random-pick feature specifically. */
.btn-random {
  border-radius: 999px;
  color: #fff;
  background: hsl(var(--random-h) var(--random-s) var(--random-l));
  border-color: hsl(var(--random-h) var(--random-s) var(--random-l));
}

.btn-random:hover:not(:disabled) {
  background: hsl(var(--random-h) var(--random-s) calc(var(--random-l) - 7%));
  border-color: hsl(var(--random-h) var(--random-s) calc(var(--random-l) - 7%));
  color: #fff;
}

.badge {
  font-size: var(--fs-meta);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  background: var(--accent);
  color: #fff;
  border-radius: 999px;
  min-width: 18px;
  padding: 1px 5px;
  text-align: center;
}

.badge:empty { display: none; }

/* Clear-filters lights up once something is actually filtering */
#clear-filters:not(:disabled) {
  border-color: var(--border-strong);
  color: var(--text);
}

#clear-filters:not(:disabled):hover {
  border-color: #e5484d;
  color: #e5484d;
  background: color-mix(in srgb, #e5484d 8%, var(--surface));
}

/* ---------- Filter groups ---------- */

/* .match-row, .filter-bar-tags, and .filter-actions are placed by name
   (grid-area, declared with each of them below) rather than relying on
   their order in index.html, so each can move independently per
   breakpoint: desktop keeps Match spanning the top with tags and the
   action column sharing the row below it (the action column's height
   following whichever side is taller, per grid's default stretch, while
   its buttons stay top-aligned rather than stretching to fill it);
   mobile (see the media query) instead stacks Browse/Clear right under
   Match, ahead of the tags — permanently reachable near the top instead
   of trailing after a pill wall that can run much longer than one
   screen. */
.filter-bar {
  margin-top: 6px;
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas:
    "match match"
    "tags actions";
  align-items: start;
  gap: 6px 16px;
}

/* Hidden until "Browse filters" is actually open — Match all/any only
   means something once there's a pill wall in view to apply it to, so
   there's nothing useful to show before that (or worth clicking
   Clear filters just to make it go away again after). Toggle and info
   icon sit on one line at every width (unlike .fgroup, which stacks on
   mobile) — there's nothing here that needs the room a stacked layout
   buys, and staying inline keeps this row's footprint minimal once it
   does appear. */
.match-row { display: none; }

.filter-bar.filters-open .match-row {
  grid-area: match;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 10px;
  /* Positioning context for .info-btn .tooltip on mobile (see that
     media-query override) — the row's own width is always safely within
     the viewport, unlike the tiny 23px info-btn itself, which the tooltip
     is anchored to at every other width. */
  position: relative;
}

/* "MATCH" spelled out is redundant with the pill labels themselves
   ("Match all" / "Match any") right next to it, at every width — visually-
   hidden rather than removed so .segmented's aria-labelledby (see
   index.html) still has real text to announce. Matches .visually-hidden's
   technique (see that rule). */
.match-row .flabel {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* Small circular (i) button that reveals the Match all/any explanation
   on hover, keyboard focus, or tap. */
.info-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 23px;
  height: 23px;
  flex: none;
  padding: 0;
  border: 1.5px solid var(--accent-2);
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent-2);
  cursor: pointer;
}

.info-btn:hover,
.info-btn:focus-visible {
  color: #fff;
  background: var(--accent);
  border-color: var(--accent);
}
.info-btn:focus-visible { outline: none; box-shadow: var(--ring); }

/* .tooltip's own cosmetic rule (position/size/colors) is defined once,
   shared with .nav-item-wrap's tab tooltips, though reveal triggers
   differ (.info-btn also gets tap-to-open; nav tabs are hover/focus-
   only) — see "Nav tab tooltips" further down (kept there since that
   section also needs to explain the nav-specific overflow handling). */

/* The boxed groups (Type+Subtype, Topic, Organization), stacked. */
.filter-bar-tags {
  grid-area: tags;
  min-width: 0;
  display: grid;
  gap: 6px;
}

.filter-actions {
  grid-area: actions;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.filter-actions .btn { justify-content: center; white-space: nowrap; }

/* Type+Subtype, Topic, and Organization each get their own bordered
   box so the four filter categories read as three distinct groups
   rather than one continuous wall of tags. */
/* Muted by default — blends toward --surface-2 (the page background,
   --bg, is the same white as --surface itself now, so it can't supply
   this contrast the way it used to) rather than standing out as a
   bordered card — so it reads as a supporting section next to the
   search bar above. Interacting with anything inside it (hover or
   focus, e.g. tabbing to a pill) restores the clearer card treatment,
   since at that point the user's attention is already there. */
.filter-group-box {
  border: 1px solid transparent;
  border-radius: var(--radius);
  padding: 6px 10px;
  background: color-mix(in srgb, var(--surface) 55%, var(--surface-2));
  transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
}

.filter-group-box:hover,
.filter-group-box:focus-within {
  border-color: var(--border);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
}

/* A small always-visible preview of the top pills in a box (plus whatever's
   already active) so first-time visitors see *something* without opening
   the full wall. Swaps out for the full .filters panel — rather than
   sitting above it — once "Browse filters" is open, so the same pills
   never render twice. */
.filters-preview { display: grid; gap: 5px; }
.filter-bar.filters-open .filters-preview { display: none; }

/* TRYING: clamp each preview row to a single line, backstopping the
   reduced PREVIEW_TYPE/SUB/ORG_COUNT constants in app.js in case a
   viewport is narrow enough that even that trimmed count would still
   wrap. Base .pill is 13px/1.2 line-height + 6px vertical padding + 1px
   border each side ≈ 30px tall with a 4px row-gap, so row 2 would start
   at 34px — 32px sits in that gap, showing exactly one clean row. */
.filters-preview .pills {
  max-height: 32px;
  overflow: hidden;
}

/* Collapsed by default so first-time visitors aren't hit with the full
   pill wall — "Browse filters" reveals it via filters-open. */
.filters { display: none; gap: 5px; }
.filter-bar.filters-open .filters { display: grid; }

/* Organization's mobile-only disclosure button — see the mobile media
   query below for its actual layout/behavior. Hidden outright here so
   it never renders (or gets focus) above 720px, where Organization
   keeps Type/Subtype's existing preview + shared Browse-filters
   treatment untouched. */
.org-mobile-toggle { display: none; }

/* Type's mobile-only disclosure button — same reasoning as Organization's
   above. Its show/hide interaction with Subtype (which shares its panel)
   is handled in app.js, not here; this only keeps the button itself
   invisible/inert above 720px. */
.type-mobile-toggle { display: none; }

.fgroup {
  display: grid;
  /* Wide enough for the uppercase, letter-spaced "ORGANIZATION" label
     without wrapping or overflowing, same column as Type/Subtype/Match. */
  grid-template-columns: 118px 1fr;
  gap: 10px;
  align-items: start;
}

/* Plain .flabel (not just .fgroup > .flabel) so the same label style
   works in .match-row too, which isn't a .fgroup. */
.flabel {
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-3);
}

.fgroup > .flabel {
  /* Aligns the label's baseline with the first row of pills beside it,
     which only matters for .fgroup's grid layout — .match-row centers
     its items instead, so it needs no such offset. */
  padding-top: 7px;
}

/* Wraps a .flabel together with an .info-btn (Research Resources' "Is
   it still online" row) so the pair sits as one unit in .fgroup's label
   column. Carries the baseline-alignment padding-top itself, since the
   nested .flabel is no longer a direct child of .fgroup and so no
   longer matches the rule above. */
.fgroup-label-row {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding-top: 4px;
}

.fgroup-label-row .flabel {
  padding-top: 0;
}

/* row-gap trimmed separately from column-gap: shrinks the vertical space
   between wrapped pill lines without crowding pills within the same line. */
.pills { display: flex; flex-wrap: wrap; row-gap: 4px; column-gap: 7px; }

.pill {
  font: inherit;
  font-size: var(--fs-label);
  line-height: 1.2;
  color: var(--text-2);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 6px 11px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  transition: background 0.15s ease, border-color 0.15s ease,
    color 0.15s ease, transform 0.12s ease, opacity 0.15s ease,
    filter 0.15s ease;
}

.pill .dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: hsl(var(--type-h, 220) var(--type-s) var(--type-l));
  flex: none;
}

.pill .n {
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-meta);
  color: var(--text-3);
  background: var(--surface-2);
  border-radius: 999px;
  padding: 1px 6px;
  transition: background 0.15s ease, color 0.15s ease;
}

.pill:hover { border-color: var(--border-strong); transform: translateY(-1px); }
.pill:focus-visible { outline: none; box-shadow: var(--ring); }

.pill[aria-pressed="true"] {
  color: #fff;
  background: hsl(var(--type-h, 220) var(--type-s) var(--type-l));
  border-color: hsl(var(--type-h, 220) var(--type-s) var(--type-l));
  font-weight: 560;
}

.pill[aria-pressed="true"] .n {
  background: rgba(255, 255, 255, 0.22);
  color: #fff;
}

.pill[aria-pressed="true"] .dot { background: rgba(255, 255, 255, 0.85); }

/* Subtype and Topic pills stay a lighter, thinner gray outline at
   regular weight — no colored dot (never added for either kind — see
   pillHTML in app.js/research.js), no hue-tinted fill, selected or
   not — so next to Type's always-on solid fill below, both read as
   quieter secondary refinements rather than a second primary category.
   Without this, a selected pill of either kind would otherwise fall
   through to the generic .pill[aria-pressed="true"] rule above and
   pick up its hsl(var(--type-h, 220) ...) fallback — an unrelated blue
   tint neither kind has any other reason to show. More specific than
   that generic rule, so this wins the cascade for just this one
   property set; font-weight stays at the base .pill's regular
   (unbolded) weight, since this rule doesn't touch it and Type's
   bold-weight rule below is scoped to data-filter="type" only.
   Organization pills are left as they were. Same selectors at every
   width, so this holds on mobile too. */
.pill[data-filter="sub"][aria-pressed="true"],
.pill[data-filter="topic"][aria-pressed="true"] {
  color: var(--text);
  background: var(--surface-2);
  border-color: var(--border-strong);
}

.pill[data-filter="sub"][aria-pressed="true"] .n {
  background: var(--surface);
  color: var(--text-2);
}

/* Type pills read as the primary category at a glance, before any
   interaction — solid, per-value hue fill (same hue as the card stripe/
   .tag.type) with bold white text, always on rather than only once
   selected. Equal specificity to the generic .pill[aria-pressed="true"]
   above, so source order (this comes after) is what makes this rule win
   for pressed Type pills too, rather than the generic accent-colored
   fill; unpressed Type pills need no such tie-break since the generic
   rule doesn't apply to them at all. */
.pill[data-filter="type"] {
  color: #fff;
  background: hsl(var(--type-h) var(--type-s) var(--type-fill-l));
  border-color: hsl(var(--type-h) var(--type-s) var(--type-fill-l));
  font-weight: 600;
}

.pill[data-filter="type"] .dot { background: rgba(255, 255, 255, 0.85); }

.pill[data-filter="type"] .n {
  background: rgba(255, 255, 255, 0.22);
  color: #fff;
}

/* Hover's generic border-color (var(--border-strong), a neutral gray)
   would otherwise sit oddly against a saturated fill — keep it on-hue,
   just a touch darker. */
.pill[data-filter="type"]:hover {
  border-color: hsl(var(--type-h) var(--type-s) calc(var(--type-fill-l) - 6%));
}

/* The fill above is now Type's permanent look, so it can no longer by
   itself signal "this is the active filter" the way it used to when
   pressed was the only thing that turned a pill solid. An inner ring —
   same box-shadow: inset technique .tag[aria-pressed="true"] already
   uses for card tags — layers "currently selected" on top without
   touching the fill color, keeping the two signals (Type vs. Subtype,
   and selected vs. not) independent of each other. */
.pill[data-filter="type"][aria-pressed="true"] {
  box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, 0.9);
}

/* box-shadow doesn't stack across separate rules — without this, tabbing
   to an already-selected Type pill would win on specificity alone (0-3-0
   beats .pill:focus-visible's 0-2-0) and silently swallow the focus
   ring. Layer both explicitly so a keyboard user still sees where focus
   is even on a pill that's already showing the selected-state ring. */
.pill[data-filter="type"][aria-pressed="true"]:focus-visible {
  box-shadow: var(--ring), inset 0 0 0 1.5px rgba(255, 255, 255, 0.9);
}

/* Match-any-only emphasis (see pillHTML in app.js for exactly when this
   class gets added): once a Type is active, mute the *other* Type pills'
   color/opacity so the active selection reads clearly against the rest,
   rather than sitting in a wall of equally-bold fills. Purely visual —
   no change to layout, hit target, or the count/hide logic above; the
   pill stays exactly as clickable as ever. filter (not just opacity)
   pulls the saturation down too, since these pills are a solid hue fill
   rather than plain text — opacity alone would just look translucent,
   not "muted." */
.pill[data-filter="type"].pill-type-muted {
  opacity: 0.45;
  filter: saturate(0.35);
}

/* Organization gets its own soft, muted teal identity — distinct from
   Type's per-value hue coding and Subtype's neutral gray — built on
   --accent/--accent-2 (the same pair .seg-btn[aria-pressed="true"]
   already uses for a deliberately muted selected tone, rather than a
   solid fill) so it reads as "on brand" and stays in sync if the accent
   pair ever changes again, rather than an arbitrary new color. Text
   stays --accent in both states rather than the lighter --accent-2 —
   against a background this pale, --accent-2 comes in under 4:1
   contrast, short of WCAG AA's 4.5:1 for this size text; --accent clears
   it with comfortable room in both states. Selected just deepens the
   tint and switches the border to --accent-2, rather than any bold/
   saturated fill — a modest escalation to stay legible as "this one's
   picked," not a different color language. */
.pill[data-filter="org"] {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: color-mix(in srgb, var(--accent-2) 32%, transparent);
}

.pill[data-filter="org"] .n {
  background: color-mix(in srgb, var(--accent-2) 16%, var(--surface));
  color: var(--accent);
}

.pill[data-filter="org"][aria-pressed="true"] {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 16%, white);
  border-color: var(--accent-2);
  font-weight: 560;
}

.pill[data-filter="org"][aria-pressed="true"] .n {
  background: color-mix(in srgb, var(--accent) 14%, white);
  color: var(--accent);
}

/* "Is it still online" filter pills (Research Resources page only) —
   these reuse .status-badge/.status-online/.status-offline/.status-
   unknown directly (defined once, further down, and shared with the
   read-only badge on each resource card) rather than a second color
   definition, per .pill.status-badge below. */
.pill.is-empty { opacity: 0.34; }
.pill.is-empty:hover { transform: none; }

/* Trailing "+N more" pill in a preview row — same shape/size as a real
   filter pill, but dashed and accent-colored so it reads as an action
   ("there's more, tap to see it") rather than a selectable value. */
.pill-more {
  color: var(--accent-2);
  border-style: dashed;
  font-weight: 600;
}

.pill-more:hover {
  color: var(--accent);
  border-color: var(--accent);
  background: var(--accent-soft);
}

/* ---------- Match-mode toggle (Type ↔ Subtype: all vs. any) ---------- */

.segmented {
  /* Without this, the grid item stretches to fill the .fgroup column on
     wide screens, turning the pill into an oversized bar with a big empty
     gap next to the two buttons. */
  justify-self: start;
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: 999px;
  /* No padding/gap: the two buttons fill the pill edge-to-edge and meet
     at a center divider (below), reading as one split pill rather than
     two separate button shapes. */
  /* Muted by default, same reasoning as .filter-group-box (including the
     --surface-2 blend, not --bg) — a supporting control, not competing
     with the search bar for attention. */
  background: color-mix(in srgb, var(--surface) 55%, var(--surface-2));
  transition: border-color 0.15s ease, background 0.15s ease;
}

.segmented:hover,
.segmented:focus-within {
  border-color: var(--border-strong);
  background: var(--surface);
}

.seg-btn {
  font: inherit;
  font-size: var(--fs-label);
  font-weight: 560;
  color: var(--text-2);
  background: transparent;
  border: none;
  padding: 7px 15px;
  cursor: pointer;
  position: relative;
  transition: background 0.15s ease, color 0.15s ease;
}

/* Radius only on the outer edge each button touches, so together the
   pair still traces the container's single pill outline. */
.seg-btn:first-child { border-radius: 999px 0 0 999px; }
.seg-btn:last-child { border-radius: 0 999px 999px 0; }

/* Center divider — the "split down the middle" line the two halves meet
   at. A plain border (not overflow:hidden clipping) so the focus ring on
   either button still renders in full, uncut. */
.seg-btn:last-child::before {
  content: "";
  position: absolute;
  left: 0;
  top: 22%;
  bottom: 22%;
  width: 1px;
  background: var(--border-strong);
}

.seg-btn:hover { color: var(--text); }
.seg-btn:focus-visible { outline: none; box-shadow: var(--ring); z-index: 1; }

/* Muted selected tone (accent-soft + accent text) instead of a solid
   navy fill — the old solid --accent/white combo visually outweighed
   the search bar and everything else around it. */
.seg-btn[aria-pressed="true"] {
  color: var(--accent);
  background: var(--accent-soft);
  font-weight: 640;
}


/* ---------- Status bar / breadcrumbs ---------- */

.statusbar {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin: 6px 0 8px;
  min-height: 30px;
}

.count { font-size: var(--fs-label); color: var(--text-2); }
.count strong { color: var(--text); font-variant-numeric: tabular-nums; }

.crumbs { display: flex; flex-wrap: wrap; gap: 7px; align-items: center; }

.crumb {
  font: inherit;
  font-size: var(--fs-label);
  color: var(--text);
  background: var(--accent-soft);
  border: 1px solid transparent;
  border-radius: 999px;
  padding: 4px 6px 4px 11px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  animation: pop 0.18s ease both;
  transition: background 0.15s ease, border-color 0.15s ease;
}

.crumb .k { color: var(--text-2); }
.crumb .x {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 17px;
  height: 17px;
  border-radius: 50%;
  background: rgba(127, 127, 127, 0.18);
  font-size: var(--fs-meta);
  line-height: 1;
}
.crumb:hover { background: var(--surface); border-color: var(--border-strong); }
.crumb:hover .x { background: #e5484d; color: #fff; }
.crumb:focus-visible { outline: none; box-shadow: var(--ring); }

@keyframes pop {
  from { opacity: 0; transform: scale(0.9); }
  to { opacity: 1; transform: scale(1); }
}

/* ---------- Grid + cards ---------- */

/* auto-fit (not auto-fill) so a sparse result set — e.g. after filtering
   down to one or two cards on a wide screen — reflows into the freed
   columns instead of leaving them as dead empty tracks. The upper bound
   must stay a flexible 1fr, not a fixed length: auto-fit/auto-fill uses
   that upper bound (when it's a definite length) to decide how many
   columns fit at all, so a fixed cap there quietly collapses tablet/
   laptop widths to fewer columns than the 288px minimum alone would
   allow. The 420px cap on .card below does the "don't stretch edge to
   edge" job instead, without touching the column count. */
/* stretch (the grid default) rather than start: every card in a row
   fills that row's height to match its tallest member, instead of
   sizing to its own content and leaving a ragged bottom edge. .meta's
   margin-top:auto (below) then anchors Organization to the bottom of
   that stretched box, so the leftover space collapses into the gap
   between the description and Organization rather than trailing empty
   space after everything. No-op on mobile: the grid is forced to a
   single column there, so each "row" only ever has one card to match. */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(288px, 1fr));
  gap: clamp(14px, 1.6vw, 20px);
  align-items: stretch;
}

.card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 17px 17px 15px;
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  overflow: hidden;
  animation: fadeUp 0.34s cubic-bezier(0.22, 1, 0.36, 1) both;
  animation-delay: var(--d, 0ms);
  transition: transform 0.2s cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 0.2s ease, border-color 0.2s ease, opacity 0.2s ease;
}

/* Caps how wide a card can stretch when its grid track ends up unusually
   wide (a sparse filtered result on a wide screen). Scoped above the
   forced-single-column breakpoint so a lone phone-width card still fills
   the full row instead of leaving a gap. */
@media (min-width: 721px) {
  .card { max-width: 420px; }
}

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

/* Type colour stripe */
.card::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 4px;
  background: hsl(var(--type-h) var(--type-s) var(--type-l));
  opacity: 0.9;
  transition: width 0.2s ease;
}

.card:hover,
.card:focus-within {
  transform: translateY(-3px);
  box-shadow: var(--shadow-lg);
  border-color: hsl(var(--type-h) var(--type-s) var(--type-l) / 0.55);
  z-index: 2;
}

.card:hover::before { width: 6px; }
.card:focus-visible { outline: none; box-shadow: var(--ring), var(--shadow-lg); }

.card-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}

.card h3 {
  margin: 0 0 9px;
  font-size: var(--fs-card-title);
  font-weight: 640;
  letter-spacing: -0.01em;
  line-height: 1.34;
  padding-left: 6px;
}

.card h3 a {
  color: inherit;
  text-decoration: none;
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 0% 1px;
  background-repeat: no-repeat;
  background-position: 0 100%;
  transition: background-size 0.25s ease;
}

.card:hover h3 a { background-size: 100% 1px; }

.tagrow {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 10px;
  padding-left: 6px;
}

.tag {
  font: inherit;
  font-size: var(--fs-meta);
  font-weight: 560;
  line-height: 1.2;
  border-radius: 999px;
  padding: 3.5px 9px;
  cursor: pointer;
  border: 1px solid transparent;
  transition: transform 0.12s ease, filter 0.15s ease, box-shadow 0.15s ease;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tag.type {
  color: hsl(var(--type-h) var(--type-s) calc(var(--type-l) - 6%));
  background: hsl(var(--type-h) var(--type-s) var(--type-tint-l));
  border-color: hsl(var(--type-h) var(--type-s) var(--type-l) / 0.28);
}

.tag.sub {
  color: var(--text-2);
  background: var(--surface-2);
  border-color: var(--border);
}

.tag:hover { transform: translateY(-1px); filter: brightness(1.04); box-shadow: var(--shadow-sm); }
.tag:focus-visible { outline: none; box-shadow: var(--ring); }
.tag[aria-pressed="true"] { box-shadow: inset 0 0 0 1.5px currentColor; }

.desc {
  margin: 0;
  padding-left: 6px;
  color: var(--text-2);
  font-size: var(--fs-body);
  line-height: 1.58;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Uncapped-height growth here would feed back into row sizing: with
   .grid's align-items:stretch, every card's height is now driven by the
   tallest card in its row, so an unbounded reveal on one hover (some
   descriptions run 4-6x the 3-line clamp) would drag every sibling in
   that row taller along with it — a jarring "everyone changed size
   because I hovered one card" flicker. Capping at ~7 lines with its own
   scroll keeps growth modest and predictable, while still fully
   revealing the large majority of descriptions with no scrolling at all
   (--fs-body's 14px x 1.58 line-height x 7 lines ≈ 155px). */
.card:hover .desc,
.card:focus-within .desc {
  /* display:block fully exits the -webkit-box clamp model (same reset
     the touch media query below already uses) rather than layering
     max-height/overflow-y:auto on top of it, which is safer than relying
     on scroll behavior within the legacy -webkit-box display mode. */
  display: block;
  -webkit-line-clamp: unset;
  line-clamp: unset;
  max-height: 155px;
  overflow-y: auto;
}

/* Secondary metadata (Organization) — always visible, not just on hover;
   same spot and style at the bottom of the card. margin-top:auto (rather
   than a fixed value) anchors it to the bottom of the card's box, so on
   a row-stretched card (see .grid) any leftover space collapses into the
   gap above it instead of trailing empty space below it. */
.meta {
  padding-left: 6px;
  margin-top: auto;
  padding-top: 11px;
}

.meta-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 12px;
  padding-top: 10px;
  border-top: 1px dashed var(--border);
  font-size: var(--fs-label);
  color: var(--text-3);
}

.meta-item { display: inline-flex; align-items: center; gap: 5px; min-width: 0; }
.meta-item svg { flex: none; opacity: 0.75; }
.meta-item .v { color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Copy-link button — appears on hover */
.copy {
  flex: none;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-2);
  border-radius: 8px;
  width: 30px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
  opacity: 0;
  transform: scale(0.86);
  transition: opacity 0.18s ease, transform 0.18s ease, color 0.15s ease,
    border-color 0.15s ease, background 0.15s ease;
}

.card:hover .copy,
.card:focus-within .copy,
.copy:focus-visible { opacity: 1; transform: none; }

.copy:hover { color: var(--accent-2); border-color: var(--accent-2); background: var(--accent-soft); }
.copy:focus-visible { outline: none; box-shadow: var(--ring); }
.copy.copied { color: #0a7d55; border-color: #a9e3cb; background: #e3f7ee; }

/* Touch devices have no hover, so nothing may hide behind it: show the copy
   button and the full description outright (.meta is always visible now,
   on every input type, so it needs no override here). */
@media (hover: none) {
  .copy { opacity: 1; transform: none; }
  .desc { -webkit-line-clamp: unset; line-clamp: unset; display: block; overflow: visible; }
  .card:active { transform: scale(0.985); box-shadow: var(--shadow-md); }
}

/* Search-match highlight */
mark {
  background: #fff2a8;
  color: inherit;
  border-radius: 3px;
  padding: 0 1px;
  font-weight: 620;
}

/* Tag-hover cross-highlighting */
.grid.dimming .card { opacity: 0.32; filter: saturate(0.5); }
.grid.dimming .card.tag-match {
  opacity: 1;
  filter: none;
  border-color: hsl(var(--type-h) var(--type-s) var(--type-l) / 0.7);
  box-shadow: var(--shadow-md);
}

/* Random-pick flicker: each intermediate candidate is a brand-new DOM
   node (render() replaces the grid's innerHTML every tick — see
   pickRandom() in app.js), so it only exists for ~90ms. Suppressing the
   base .card entrance animation on it is what makes the swap read as a
   crisp strobe instead of a blur of overlapping fade-ins. */
.card.cycling {
  animation: none;
}

/* The final landed pick: a quick scale/fade-in distinct from the plain
   .card entrance the rest of the grid uses, plus the glow pulse below. */
.card.settle-in {
  animation: settleIn 0.28s cubic-bezier(0.22, 1, 0.36, 1) both, flash 1.5s ease 0.05s;
}

@keyframes settleIn {
  from { opacity: 0; transform: scale(0.9); }
  to { opacity: 1; transform: scale(1); }
}

@keyframes flash {
  0%, 100% { box-shadow: var(--shadow-sm); }
  18% { box-shadow: 0 0 0 4px hsl(var(--type-h) var(--type-s) var(--type-l) / 0.4), var(--shadow-lg); }
  70% { box-shadow: 0 0 0 4px hsl(var(--type-h) var(--type-s) var(--type-l) / 0.25), var(--shadow-md); }
}

/* ---------- Empty + loading states ---------- */

.state {
  text-align: center;
  padding: 66px 20px;
  animation: fadeUp 0.3s ease both;
}

.state .icon {
  width: 54px;
  height: 54px;
  margin: 0 auto 16px;
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--text-3);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* font-weight 700, not the 640 this used before — every other heading
   sharing --fs-heading (Start Here, Feedback, policy h2, onboarding
   modal) is 700; this was the one inconsistent outlier. */
.state h2 { margin: 0 0 7px; font-size: var(--fs-heading); font-weight: 700; }
.state p { margin: 0 auto 18px; color: var(--text-2); max-width: 46ch; }

/* 404 page's two CTA buttons, inside a .state block (see 404.html) —
   same flex/gap/wrap pattern as .onboarding-modal-actions and
   .feedback-cta-actions elsewhere (10px gap matches both), just
   centered rather than stretched or flush against sibling content,
   since this row is the .state block's only content. */
.notfound-actions {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 10px;
}

/* Equal-width buttons regardless of their text length ("Go to Policy
   Menu" vs. the longer "Go to Research Resources") — same flex-grow/
   justify-content/min-height pattern .onboarding-modal-actions .btn
   already uses for its own two-button row, just with a taller
   flex-basis to match this pair's longer label text. */
.notfound-actions .btn {
  flex: 1 1 160px;
  justify-content: center;
  min-height: 44px;
}

.skeleton-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(288px, 1fr));
  gap: clamp(14px, 1.6vw, 20px);
}

.skel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 17px;
  height: 172px;
}

.skel span {
  display: block;
  height: 11px;
  border-radius: 6px;
  margin-bottom: 11px;
  background: linear-gradient(90deg, var(--surface-2) 25%, var(--border) 37%, var(--surface-2) 63%);
  background-size: 400% 100%;
  animation: shimmer 1.3s ease-in-out infinite;
}

.skel span:nth-child(1) { width: 62%; height: 15px; }
.skel span:nth-child(2) { width: 34%; }
.skel span:nth-child(3) { width: 100%; }
.skel span:nth-child(4) { width: 92%; }
.skel span:nth-child(5) { width: 71%; }

@keyframes shimmer {
  from { background-position: 100% 0; }
  to { background-position: 0 0; }
}

.error-box {
  border: 1px solid #f0c0c0;
  background: #fdf1f1;
  color: #8a2020;
  border-radius: var(--radius);
  padding: 18px 20px;
  font-size: var(--fs-body);
}

.error-box code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  background: rgba(0, 0, 0, 0.06);
  padding: 1px 5px;
  border-radius: 4px;
}

/* Toast */
.toast {
  position: fixed;
  left: 50%;
  bottom: 26px;
  transform: translate(-50%, 14px);
  background: #131720;
  color: #fff;
  font-size: var(--fs-label);
  font-weight: 540;
  padding: 10px 17px;
  border-radius: 999px;
  box-shadow: var(--shadow-lg);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.22s ease, transform 0.22s ease;
  z-index: 100;
}

.toast.show { opacity: 1; transform: translate(-50%, 0); }

/* Feedback popup — shown once, ~45s after load, unless already
   dismissed this session (see setupFeedbackModal in app.js). [hidden]
   keeps it fully out of layout/tab order until then; app.js removes
   that attribute first and adds .show on the next frame so the
   fade/scale below actually has a starting state to transition from
   (display can't itself be animated). Sits above the toast (a higher
   z-index), since a popup asking for attention shouldn't be hideable
   behind one. */
.feedback-modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(19, 23, 32, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  z-index: 200;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
}

.feedback-modal-backdrop.show {
  opacity: 1;
  pointer-events: auto;
}

.feedback-modal {
  position: relative;
  width: 100%;
  max-width: 380px;
  /* Belt-and-suspenders on short viewports (mobile landscape especially)
     — scroll the modal itself rather than letting it overflow the
     backdrop or the screen, even though the compact content here is in
     practice never close to tall enough to need it. */
  max-height: calc(100vh - 40px);
  overflow-y: auto;
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: 30px 24px 24px;
  text-align: center;
  transform: translateY(10px) scale(0.98);
  transition: transform 0.2s cubic-bezier(0.22, 1, 0.36, 1);
}

.feedback-modal-backdrop.show .feedback-modal { transform: none; }

.feedback-modal-close {
  position: absolute;
  top: 12px;
  right: 12px;
  width: 30px;
  height: 30px;
  flex: none;
  border: 0;
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--text-2);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
  transition: background 0.15s ease, color 0.15s ease;
}

.feedback-modal-close:hover { background: var(--border-strong); color: var(--text); }
.feedback-modal-close:focus-visible { outline: none; box-shadow: var(--ring); }

.feedback-modal-title {
  margin: 0 0 8px;
  font-size: var(--fs-heading);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--text);
}

.feedback-modal-text {
  margin: 0 0 20px;
  color: var(--text-2);
  font-size: var(--fs-body);
  line-height: 1.5;
}

/* Separates the email signup (primary ask above) from the existing
   feedback-survey link (secondary ask below) so the two don't read as
   one long form. */
.feedback-modal-divider {
  margin: 20px 0 16px;
  border: 0;
  border-top: 1px solid var(--border);
}

/* Second section's heading — same size as .feedback-modal-title above
   (both are --fs-heading), but a lighter weight instead of a smaller
   size to still read as a subsection of this one modal rather than a
   second dialog. */
.feedback-modal-subtitle {
  margin: 0 0 8px;
  font-size: var(--fs-heading);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text);
}

.feedback-modal-btn { width: 100%; justify-content: center; }

/* ---------- Responsive ---------- */

@media (max-width: 720px) {
  /* Belt-and-suspenders against horizontal scroll/pan on mobile: any
     descendant that's even a little wider than the viewport (the
     .info-btn tooltip below was one concrete case) would otherwise let
     the whole page shift sideways on a diagonal touch-scroll. overflow-x
     only — overflow-y is left at its default (visible/auto) so normal
     vertical scrolling, and pinch-to-zoom (governed by the viewport meta
     tag, untouched here), both keep working exactly as before. Set on
     both html and body since iOS Safari and Chrome don't agree on which
     of the two is "the" scrolling element. */
  html, body { overflow-x: hidden; }

  /* Horizontal padding stays governed by the fluid clamp() on .wrap so it
     keeps scaling smoothly instead of jumping at this one breakpoint. */
  .wrap { padding-top: 10px; padding-bottom: 72px; }

  /* Start Here: tighter box padding. No margin override here (unlike the
     old in-flow layout) since the panel is absolutely positioned now —
     margin would just offset it from its top/right anchor instead of
     doing anything useful. .start-here-grid's own 2-column layout and
     gap already apply unchanged from the base rule above, so no override
     needed here. */
  .start-here { padding: 8px 10px 10px; }
  /* font-size no longer overridden here — desktop's --fs-heading (17px)
     already equals what this mobile rule used to force it down to. */
  .start-here-title { margin: 0 0 6px; }
  .start-card { padding: 9px 10px; }

  /* Same tighter treatment as .start-here just above. */
  .quick-read-panel { padding: 10px 12px; }

  /* Header's two stacked pills both go full-width here, same as
     .start-here-toggle already did alone before quick-read-toggle
     existed (see that rule further down) — .header-ctas itself just
     needs to claim the full wrapped row for them to stretch into. */
  .header-ctas { width: 100%; align-items: stretch; }

  .fgroup { grid-template-columns: 1fr; gap: 6px; }
  .fgroup > .flabel { padding-top: 0; }

  /* Organization on mobile: no preview tags/count by default, just this
     label+chevron row — Organization has by far the most values and the
     longest labels of the three facets, so even a capped preview row ate
     the most vertical space for the facet visitors reach for least
     first. Decoupled entirely from "Browse filters": the
     #org-filter-box/#org-mobile-toggle id selectors below outrank
     .filter-bar.filters-open .filters in specificity, so Organization
     stays collapsed even while Browse filters is open, and only this
     button's own aria-expanded reveals it. Type gets the same collapsed-
     by-default treatment further down (shares its panel with Subtype,
     so it's coordinated in app.js instead of pure CSS); Subtype itself
     is the only facet still following the plain preview + Browse-filters
     behavior all three used to share. */
  .org-mobile-toggle,
  .type-mobile-toggle {
    display: flex;
    align-items: center;
    gap: 6px;
    width: 100%;
    padding: 7px 0;
    border: 0;
    background: none;
    cursor: pointer;
    text-align: left;
  }
  .org-mobile-toggle .chevron,
  .type-mobile-toggle .chevron {
    flex: none;
    width: 14px;
    height: 14px;
    color: var(--text-3);
    transition: transform 0.2s ease;
  }
  .org-mobile-toggle[aria-expanded="true"] .chevron,
  .type-mobile-toggle[aria-expanded="true"] .chevron { transform: rotate(180deg); }

  #org-filter-box .filters-preview { display: none; }
  #org-filter-box .filters { display: none; }
  #org-mobile-toggle[aria-expanded="true"] ~ .filters { display: grid; }
  /* #org-pills' own "Organization" flabel would otherwise repeat right
     below the toggle button's identical label when expanded. Safe to
     hide outright rather than just visually: #org-pills keeps
     aria-labelledby="lbl-org", and per the accessible-name spec a
     labelledby target's text still counts even while display:none, so
     screen readers still get "Organization" as the group's name. */
  #org-fgroup > .flabel { display: none; }

  /* Type: same collapsed-by-default treatment as Organization, but its
     preview/full panels are shared with Subtype (#filters-preview-typesub
     / #filters-typesub), so a `~`-sibling CSS rule like Organization's
     can't reach it — Type's fgroup is nested inside that shared panel,
     not a direct sibling of this button. #type-fgroup-preview is hidden
     unconditionally (CSS-only, matching Organization, so there's no
     flash of the old preview before JS runs); #type-fgroup's own
     show/hide and the shared panel's visibility are instead driven by
     app.js (syncTypeSubtypeMobilePanel) via inline styles, which always
     win over these stylesheet rules regardless of specificity — so
     Subtype's own visibility here (governed entirely by
     .filter-bar.filters-open, untouched) can't be exposed as a side
     effect of Type's toggle opening the shared panel. */
  #type-fgroup-preview { display: none; }
  /* Same duplicate-label fix as Organization's #org-fgroup, applied
     preemptively here too. */
  #type-fgroup > .flabel { display: none; }

  /* Desktop's Match-any Type emphasis (.pill-type-muted, right above —
     same class, same condition) only dims the rest; mobile hides them
     outright instead. Vertical space is tighter here, and once a Type is
     picked the Subtype section can appear right below it (see
     renderFilters in app.js) — a whole wall of every other unpicked Type
     competes with that for room in a way it doesn't on a wide desktop
     screen. No JS change needed for "bring them back": pillHTML only
     adds this class while the condition holds, so deselecting the Type
     or clearing filters removes the class (and this rule with it) on
     the very next render, same as the desktop mute already does. */
  .pill[data-filter="type"].pill-type-muted {
    display: none;
  }

  /* The desktop tooltip anchors to .info-btn itself (23px wide) and
     extends up to 320px rightward from whatever x-position the icon
     lands at — on a full-width desktop row that's harmless, but on a
     narrow phone the icon usually sits two-thirds of the way across the
     row (after "Match all"/"Match any"), so the tooltip would run far
     past the right edge of the viewport, forcing a horizontal scrollbar
     even while the tooltip itself is invisible (opacity:0, but still
     part of layout). Re-anchoring to .match-row instead of .info-btn —
     and centering rather than left-aligning — keeps it within a box
     that's already guaranteed to fit the viewport, regardless of where
     the icon itself falls inside that row. */
  .info-btn { position: static; }
  .info-btn .tooltip {
    left: 50%;
    right: auto;
    transform: translateX(-50%) translateY(-4px);
    max-width: min(300px, calc(100% - 24px));
  }
  .info-btn:hover .tooltip,
  .info-btn:focus-visible .tooltip,
  .info-btn.info-open .tooltip {
    transform: translateX(-50%) translateY(0);
  }

  /* Two rows on mobile, not one (matches fitPreviewRow's maxRows in
     app.js, which measures against this same budget) — a single long
     top-ranked label (some Subtype/Organization values run 40+
     characters) can already fill an entire narrow-phone row alone,
     which meant capping at 1 row often left those two categories with
     zero visible pills before "+N more" while Type, whose top labels
     are shorter, still showed one. Mobile's own .pill override further
     down renders pills at 36px tall (vs. 30px on desktop) with the same
     4px row-gap: row 2 runs 40-76px, row 3 would start at 80px; 78px
     sits in that gap, showing exactly two clean rows. */
  .filters-preview .pills {
    max-height: 78px;
    overflow: hidden;
  }
  .grid, .skeleton-grid { grid-template-columns: 1fr; }

  /* 16px minimum stops iOS Safari zooming the page on focus. Right
     padding trimmed from desktop's — just the random-resource hint
     (itself a bit smaller — see .random-hint below) and the clear
     button share this end now. */
  .search-input { font-size: 16px; padding: 12px 116px 12px 40px; }

  .searchbox { flex: 1 1 100%; }

  /* font-size no longer overridden here — desktop's --fs-meta (11px)
     already equals what this mobile rule used to shrink it to. */
  .random-hint { gap: 4px; }
  .random-hint svg { width: 17px; height: 17px; }

  /* Hide the (small, compact) search bar on scroll-down, reveal it on
     scroll-up — mobile only, gated by this media query rather than a JS
     width check, so no amount of class-toggling in app.js has any visual
     effect above 720px. Transform + opacity only, never height/padding:
     a pure compositor animation with no layout recalculation, which is
     what keeps it smooth. app.js throttles + thresholds the scroll
     listener so this class only flips on a real directional scroll, not
     on every pixel of jitter. */
  .controls {
    transition: transform 0.2s cubic-bezier(0.22, 1, 0.36, 1),
      opacity 0.2s cubic-bezier(0.22, 1, 0.36, 1),
      border-color 0.2s ease, box-shadow 0.2s ease;
    will-change: transform, opacity;
  }

  .controls.controls-hidden {
    transform: translateY(-100%);
    opacity: 0;
    pointer-events: none;
  }

  /* Single column, and Browse/Clear filters move up to sit right under
     Match — permanently reachable near the top on every load, collapsed
     or expanded, rather than trailing after a pill wall that can run
     much longer than one screen (their .filter-actions styling itself,
     row direction and button sizing, comes from the rule further down —
     this only changes where the row sits). */
  .filter-bar {
    grid-template-columns: 1fr;
    grid-template-areas:
      "match"
      "actions"
      "tags";
  }

  .filter-actions {
    flex-direction: row;
    flex-wrap: wrap;
  }

  .filter-actions .btn {
    flex: 1 1 0;
    min-width: 0;
    padding: 11px 10px;
    font-size: var(--fs-label);
  }

  /* A "0" count means no matches given the *other* active filters —
     hide the tag outright rather than showing it grayed out, EXCEPT
     for Subtype/Organization tags while Match all is active (see
     pillHTML in app.js for the full reasoning: those need the
     .pill-may-hide class present too, which app.js only adds there in
     Match any, or for Type in either mode). Re-renders on every filter
     or match-mode change, so which tags are eligible to hide updates
     live right along with everything else. */
  .pill.is-empty.pill-may-hide { display: none; }

  /* Comfortable tap targets (~40px) */
  .pill { padding: 9px 14px; font-size: 13.5px; }
  .tag { padding: 6.5px 12px; font-size: 12px; }
  /* font-size no longer overridden here — desktop's --fs-label (13px)
     already equals what this mobile rule used to bump it up to. */
  .crumb { padding: 7px 8px 7px 13px; }
  .crumb .x { width: 20px; height: 20px; }
  .copy { width: 38px; height: 38px; }
  .clear-search { width: 26px; height: 26px; }

  .card { padding: 16px 15px 14px; }

  /* Now that the pill (below) is full-width on its own wrapped row here,
     rather than squeezed onto the same line as a shrunk-down brand
     block. */
  .site-header-inner { padding: 14px 14px; gap: 12px; flex-wrap: wrap; }
  .brand { gap: 10px; }
  .brand-logo { height: 46px; }
  /* Small enough that the full title settles into 2 lines instead of
     wrapping awkwardly across 3. */
  .brand-name { font-size: 15px; line-height: 1.22; }
  .brand-subtitle { font-size: 12px; line-height: 1.35; }

  /* Full-width on its own row below .brand rather than squeezed beside
     it, so it still has clean room to breathe even at this smaller
     size. */
  .start-here-toggle {
    width: 100%;
    justify-content: center;
    padding: 11px 16px;
    font-size: 11px;
    gap: 5px;
  }
  .start-here-toggle .chevron { width: 12px; height: 12px; }

  /* Wrap the creator rather than truncating it — there's room in one column */
  .meta-item .v { white-space: normal; }

  /* Centered and stacked rather than the desktop's side-by-side
     space-between, which would otherwise squeeze the button into a
     cramped second line under just a sliver of leftover width. */
  .feedback-cta { flex-direction: column; text-align: center; padding: 14px 16px; }
  .feedback-cta-actions { flex-direction: column; width: 100%; }
  .feedback-cta-btn { width: 100%; justify-content: center; }

  /* Less outer padding and a touch less inner padding so the card has
     room to breathe on the narrowest phones without needing to shrink
     its own max-width. */
  .feedback-modal-backdrop { padding: 14px; }
  .feedback-modal { padding: 26px 20px 20px; }
}

/* ---------- No-JavaScript fallback ---------- */

.nojs-note {
  border: 1px solid var(--border-strong);
  background: var(--surface);
  border-radius: var(--radius);
  padding: 15px 17px;
  margin-bottom: 20px;
  color: var(--text-2);
  font-size: var(--fs-body);
  line-height: 1.6;
}

.nojs-note strong { color: var(--text); }

/* Long titles must never push the layout sideways */
.card h3, .desc { overflow-wrap: anywhere; }

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

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

footer.foot {
  margin-top: 40px;
  padding-top: 18px;
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* Feedback call-to-action — a light accent-tinted card so it reads as
   its own inviting block rather than blending into the muted hint row
   below it, without competing with the primary search/filter controls
   above the fold. */
.feedback-cta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
  padding: 14px 18px;
  background: color-mix(in srgb, var(--accent) 6%, var(--surface));
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.feedback-cta-text {
  color: var(--text-2);
  font-size: var(--fs-label);
}

/* Holds both CTA buttons side by side, right-aligned as a single unit
   against .feedback-cta-text via the parent's justify-content:
   space-between — same role .header-ctas plays for the two header
   pills. */
.feedback-cta-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: none;
  flex-wrap: wrap;
}

.feedback-cta-btn { flex: none; white-space: nowrap; }

.foot-hint {
  color: var(--text-3);
  font-size: var(--fs-meta);
  display: flex;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}

/* ---------- Privacy policy page ---------- */

/* Prose column, capped narrower than the app's own 1600px .wrap so lines
   of text stay a comfortable reading length instead of stretching wide. */
.policy {
  max-width: 640px;
  margin: 0 auto;
  padding: 8px 0 20px;
}

.policy .policy-updated {
  margin: 0 0 28px;
  color: var(--text-3);
  font-size: var(--fs-meta);
}

.policy h2 {
  margin: 28px 0 8px;
  font-size: var(--fs-heading);
  font-weight: 700;
  color: var(--text);
}

.policy p,
.policy li {
  color: var(--text-2);
  font-size: var(--fs-body);
  line-height: 1.65;
}

.policy ul {
  margin: 8px 0;
  padding-left: 20px;
}

.policy li + li { margin-top: 4px; }

.policy .back-link {
  display: inline-flex;
  margin-bottom: 22px;
}

/* ---------------------------------------------------------------------
   First-time-visitor onboarding (see onboarding-tour.js): a "New here?"
   prompt shown once, 5s after load, offering a step-by-step guided tour
   of the filters and the two curated-resource shortcuts. Entirely
   separate from the feedback popup above — own classes, own
   sessionStorage key, own timer — so the two can never interfere.

   Sizing throughout uses clamp()/min()/vw rather than fixed pixel
   breakpoints, so the popup and tour tooltip scale fluidly across
   phone/tablet/desktop and both orientations instead of jumping at a
   couple of fixed widths. -------------------------------------------- */

.onboarding-modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(19, 23, 32, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(14px, 4vw, 20px);
  z-index: 210;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
}

.onboarding-modal-backdrop.show {
  opacity: 1;
  pointer-events: auto;
}

.onboarding-modal {
  width: 100%;
  max-width: min(340px, 100%);
  max-height: calc(100vh - 40px);
  overflow-y: auto;
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: clamp(20px, 5vw, 28px) clamp(18px, 5vw, 24px) clamp(18px, 4vw, 22px);
  text-align: center;
  transform: translateY(10px) scale(0.98);
  transition: transform 0.2s cubic-bezier(0.22, 1, 0.36, 1);
}

.onboarding-modal-backdrop.show .onboarding-modal { transform: none; }

.onboarding-modal-title {
  margin: 0 0 8px;
  font-size: var(--fs-heading);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--text);
}

/* One-line description of the site, above the tour question: same size,
   darker and slightly heavier so it reads as the lead line. */
.onboarding-modal-desc {
  margin: 0 0 8px;
  color: var(--text);
  font-size: var(--fs-body);
  font-weight: 500;
  line-height: 1.5;
}

.onboarding-modal-text {
  margin: 0 0 20px;
  color: var(--text-2);
  font-size: var(--fs-body);
  line-height: 1.5;
}

.onboarding-modal-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

/* min 44px tall and flex-wrap above so both buttons stay comfortably
   tappable — stacking full-width instead of squeezing side by side —
   on the narrowest phone widths. */
.onboarding-modal-actions .btn {
  flex: 1 1 120px;
  justify-content: center;
  min-height: 44px;
}

/* ---------------- guided tour ---------------- */

/* Full-viewport layer, above everything else on the page (including the
   feedback/onboarding popups' own backdrops). Deliberately has no click
   handler and no background image of its own — the dark scrim comes
   entirely from .tour-highlight's box-shadow below — but simply sitting
   on top with default pointer-events (auto) already blocks taps on the
   page underneath, so tapping outside the tooltip is a no-op by design,
   not an accidental way to lose your place in the tour. Only
   Back/Skip/Next/"Got it", the × button, or Escape end the tour. */
.tour-overlay {
  position: fixed;
  inset: 0;
  z-index: 300;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
}

.tour-overlay.show {
  opacity: 1;
  pointer-events: auto;
}

/* The "cutout": a transparent box sized/positioned in JS to match the
   current step's target element, with a huge box-shadow spread that
   paints the dark scrim over the rest of the viewport around it — one
   element to keep in sync instead of four separate backdrop panels.
   Repositions with a short transition as the tour moves between steps
   or the viewport resizes/scrolls. */
.tour-highlight {
  position: fixed;
  border-radius: 8px;
  box-shadow: 0 0 0 9999px rgba(15, 19, 27, 0.62);
  pointer-events: none;
  transition: top 0.25s ease, left 0.25s ease, width 0.25s ease, height 0.25s ease;
}

.tour-tooltip {
  position: fixed;
  width: min(320px, calc(100vw - 24px));
  max-height: calc(100vh - 24px);
  overflow-y: auto;
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: clamp(18px, 5vw, 22px) clamp(16px, 5vw, 20px) clamp(14px, 4vw, 16px);
  transition: top 0.25s ease, left 0.25s ease;
}

.tour-close {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 34px;
  height: 34px;
  flex: none;
  border: 0;
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--text-2);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
  transition: background 0.15s ease, color 0.15s ease;
}

.tour-close:hover { background: var(--border-strong); color: var(--text); }
.tour-close:focus-visible { outline: none; box-shadow: var(--ring); }

.tour-step-count {
  margin: 0 0 6px;
  padding-right: 26px;
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-3);
}

.tour-tooltip-text {
  margin: 0 0 18px;
  padding-right: 26px;
  color: var(--text-2);
  font-size: var(--fs-body);
  line-height: 1.5;
}

.tour-tooltip-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

/* min 44px tall, generous horizontal padding and gap above — comfortable
   tap targets that don't invite mis-taps between Back/Skip/Next. */
.tour-tooltip-actions .btn {
  min-height: 44px;
  padding: 10px 16px;
}

/* Pushes Skip/Next to the row's right edge when Back is present (steps
   2+); on step 1, Back is [hidden] and Skip/Next simply start the row. */
#tour-back { margin-right: auto; }

/* ---------- Site nav (shared across every page) ---------- */

/* A slim second row under the main header bar, present on every page
   (index.html, privacy-policy.html, research-resources.html) so
   Research Resources is reachable from anywhere, not just the homepage.
   Two-layer split (bar + centered inner), same pattern .site-header/
   .site-header-inner already uses: .site-nav itself stays full-bleed so
   its background/border still span the whole viewport, while
   .site-nav-inner carries the same max-width/centering/side-padding as
   .wrap and .site-header-inner, so the tab links line up with the
   actual content column below (search bar, cards) instead of the bare
   viewport edge — the two only coincided before on viewports narrow
   enough that .wrap's own 1600px cap never kicked in. */
.site-nav {
  background: var(--surface-2);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}

.site-nav-inner {
  max-width: 1600px;
  margin: 0 auto;
  display: flex;
  gap: 20px;
  padding: 10px clamp(16px, 4vw, 40px);
}

.site-nav a {
  font-size: var(--fs-label);
  font-weight: 640;
  color: var(--text-2);
  text-decoration: none;
}

.site-nav a:hover,
.site-nav a:focus-visible {
  color: var(--brand-navy);
}

.site-nav a[aria-current="page"] {
  color: var(--brand-navy-deep);
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 4px;
}

/* ---------- Nav tab tooltips + Research Resources nav nudge ---------- */

/* Wraps each nav tab <a> (Policy Menu, Research Resources — on every
   page sharing .site-nav) so it has somewhere to anchor its own
   describing .tooltip (see below), and — for Research Resources only —
   .nav-nudge right after it on the same row. Otherwise a plain grouping
   box: no sizing of its own beyond position:relative, so the <a>'s own
   rendered box (and the tour's getBoundingClientRect highlight around
   '.site-nav a[href="research-resources.html"]') is unaffected. */
.nav-item-wrap {
  position: relative;
  display: inline-flex;
  align-items: center;
}

/* Same .tooltip cosmetic rule used by .info-btn elsewhere (Match all/
   any, "Is it still online"), but reveal is hover/keyboard-focus ONLY
   here — deliberately no tap-to-open. .nav-item-wrap's <a> is a real
   navigation link, not a decorative disclosure button like .info-btn,
   so it never gets the .info-open tap-toggle treatment (see
   setupInfoTooltips in shared-ui.js): tapping a nav tab on touch
   navigates immediately, exactly like a plain link, with no tooltip
   step in between. Describing text is each page's own header subtitle
   (see index.html/research-resources.html/privacy-policy.html) rather
   than new copy. The tab link itself needs no tabindex/role changes:
   <a> is natively focusable, and :has(a:focus-visible) mirrors
   .info-btn's own :focus-visible trigger without needing the tooltip to
   be a child of the focusable element itself (it's a sibling here, not
   a child, since nesting it inside the <a> would make clicking the
   tooltip's own text trigger navigation). */
.info-btn .tooltip,
.nav-item-wrap .tooltip {
  position: absolute;
  z-index: 40;
  left: 0;
  top: calc(100% + 8px);
  width: max-content;
  max-width: min(320px, 80vw);
  background: var(--text);
  color: var(--bg);
  font-size: var(--fs-body);
  font-weight: 400;
  line-height: 1.45;
  text-align: left;
  padding: 8px 11px;
  border-radius: 8px;
  box-shadow: var(--shadow-md);
  opacity: 0;
  transform: translateY(-4px);
  pointer-events: none;
  transition: opacity 0.15s ease, transform 0.15s ease;
}

.info-btn:hover .tooltip,
.info-btn:focus-visible .tooltip,
.info-btn.info-open .tooltip,
.nav-item-wrap:hover .tooltip,
.nav-item-wrap:has(a:focus-visible) .tooltip {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

/* Research Resources is the nav's last/rightmost tab — a left-anchored
   tooltip (the default above, fine for Policy Menu near the nav's left
   edge) would grow rightward from well into the row and can overflow a
   narrow viewport, same risk .nav-nudge already accounts for. Anchoring
   from the wrap's right edge instead keeps it growing leftward, safely
   within the viewport regardless of how far right the tab itself sits. */
.site-nav-inner > .nav-item-wrap:last-child .tooltip {
  left: auto;
  right: 0;
}

/* Small, quiet, persistent pointer at the Research Resources tab,
   replacing the old full-width .research-callout banner — a bare
   arrow + caption sitting inline right after the link, on the nav's
   own row, rather than a badge/banner or anything stacked below it.
   Purely decorative (the link text already says where it goes) —
   aria-hidden and pointer-events:none so it never affects the link's
   own click target or screen-reader reading order. color is set once
   here and inherited by both the arrow (stroke="currentColor") and
   the caption below, so they're guaranteed the same value rather than
   two separate declarations that could drift apart. */
.nav-nudge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-left: 8px;
  /* Same maroon/burgundy hue "Database" (a Type pill elsewhere on the
     site) currently renders at — .tag.type has no fixed per-type
     color of its own (each Type's hue is assigned algorithmically
     from its alphabetical position in the live sheet's Type list, see
     typeHue in app.js), so there's no shared --color variable to
     point at; this reuses the exact same hsl()/--type-s/--type-l
     formula .tag.type itself uses, just with that type's current hue
     (340) fixed in directly, rather than a second hardcoded color
     unrelated to the site's type-color system. */
  color: hsl(340 var(--type-s) calc(var(--type-l) - 6%));
  pointer-events: none;
}

.nav-nudge-arrow {
  flex: none;
  width: 12px;
  height: 10px;
}

.nav-nudge-text {
  font-size: var(--fs-meta);
  font-style: italic;
  line-height: 1;
  white-space: nowrap;
}

/* "Research Resources" + the arrow + the full caption measures out to
   ~330px of content (SF Pro 13px/11px) — comfortably fits every nav
   width down to ~370px including .site-nav's own side padding, which
   covers every phone from a regular-size iPhone/Android up through
   desktop. Below that only the smallest phones (old iPhone SE-class,
   ~320-360px) run out of room, so the caption drops there and just
   the small arrow remains — a dedicated, narrower cutoff than the
   site's general 720px mobile breakpoint (used for much bigger layout
   changes elsewhere), chosen from this component's own actual width,
   not reused from an unrelated rule. */
@media (max-width: 400px) {
  .nav-nudge-text {
    display: none;
  }
  .nav-nudge {
    margin-left: 6px;
  }
}

/* ---------- Research Resources page ---------- */

/* First real content in .wrap now that the back-link/tour-trigger row
   above it is gone — .wrap's own top padding provides the gap down
   from the header, matching the tighter 6-8px rhythm the search/
   filter-bar/statusbar stack below it already uses. */
.rr-intro {
  margin: 0 0 12px;
  color: var(--text-2);
  font-size: var(--fs-body);
  line-height: 1.6;
}

/* Widens the label column for this page's filter rows (Is it still
   online / Type / Creator) so "IS IT STILL ONLINE?" — much longer than
   "ORGANIZATION", which the shared 118px .fgroup column was sized for
   — fits on one line. Scoped to this page's filter bar rather than the
   shared .fgroup rule, so the Policy Menu's own Type/Subtype/
   Organization columns are untouched. Desktop-only: mobile already
   stacks the label above its pills via the generic .fgroup override in
   that breakpoint, which this must not fight. */
@media (min-width: 721px) {
  #rr-filter-bar .fgroup {
    grid-template-columns: 190px 1fr;
  }
}

/* Type/Subtype tags on a research card are informational, not clickable
   filters (the pill rows above do that job) — same look as the Policy
   Menu's own .tag, minus the pointer/hover affordances that would
   otherwise imply they do something on click. */
.tag-static {
  cursor: default;
}

.tag-static:hover {
  transform: none;
  filter: none;
  box-shadow: none;
}

/* Research cards' own compactness pass — every rule below is scoped to
   .rr-card (never the bare .card/.tagrow/.desc/.meta/.topic-pills base
   rules those classes share with the Policy Menu) so none of this
   touches that page's cards. Tightens the margin/padding between each
   section of the card — title, Type tag, description, the two dashed
   dividers (above the creator/level/location meta row and above the
   topic pills row), and the meta block itself — plus the card's own
   top/bottom padding, without changing the Type tag's own look, the
   left-edge color stripe, the status badge, the dividers' own dashed
   style, any font size/line-height, or .grid's align-items:stretch
   (still stretches every card in a row to the tallest one — this only
   shrinks how tall that tallest card is in the first place). .desc's
   -webkit-line-clamp:3 has no minimum-height effect of its own — a
   short description just renders at its own natural (shorter) height;
   the empty space visible on short-description cards is the stretched
   row height plus .meta's margin-top:auto (deliberately kept, per
   above) absorbing the slack above the meta block, not a separate
   reservation to remove here.
   Card's own top/bottom padding — sides left alone. */
.card.rr-card {
  padding-top: 13px;
  padding-bottom: 12px;
}

/* Title -> Type tag gap. .card.rr-card (not .rr-card alone) so this
   unambiguously outranks the base .card h3's specificity rather than
   relying on a same-specificity source-order tiebreak. */
.card.rr-card h3 {
  margin-bottom: 5px;
}

/* Type tag -> description gap (was 10px shared, then 8px here). */
.rr-card .tagrow {
  margin-bottom: 5px;
}

/* Divider 1: gap above the creator/level/location row. */
.rr-card .meta {
  padding-top: 7px;
}
.rr-card .meta-inner {
  padding-top: 6px;
}

/* Divider 2: gap above the topic pills row. */
.rr-card .topic-pills {
  margin-top: 7px;
  padding-top: 6px;
}

.topic-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 11px;
  padding-top: 10px;
  border-top: 1px dashed var(--border);
}

.topic-pill {
  font-size: var(--fs-meta);
  font-weight: 560;
  line-height: 1.2;
  color: var(--brand-navy-deep);
  background: color-mix(in srgb, var(--brand-navy) 9%, white);
  border-radius: 999px;
  padding: 3px 9px;
  white-space: nowrap;
}

.status-badge {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: 0.02em;
  border-radius: 999px;
  padding: 4px 10px 4px 8px;
  white-space: nowrap;
}

.status-badge::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
}

.status-online {
  color: #0a7d55;
  background: #e3f7ee;
}

.status-offline {
  color: #b3261e;
  background: #fdecea;
}

.status-unknown {
  color: var(--text-2);
  background: var(--surface-2);
}

/* Filter-pill use of the status badge above (class="pill status-badge
   status-online status-filter", built in pillHTML in research.js) —
   color, dot, shape, and padding all come from .status-badge/.status-*
   as-is, completely unduplicated. .pill is kept only for the mechanics
   a clickable filter needs (click delegation, the shared .pills flex
   row, the "+N more" preview-row measurement) — every visual property
   it would otherwise contribute is reset or outranked below so the
   rendered pill is indistinguishable from the card's own badge except
   for being clickable and carrying a count. */
.pill.status-badge {
  border: none;
  cursor: pointer;
}

/* Locks in the correct status color in the selected (aria-pressed)
   state too — .pill[aria-pressed="true"] (defined earlier, for the
   Organization pill) would otherwise repaint this element with the
   generic accent fill instead. Equal specificity, declared later, so
   this wins in both the pressed and unpressed states without needing a
   separate pressed-state duplicate of these two colors. */
.pill.status-online {
  color: #0a7d55;
  background: #e3f7ee;
}

.pill.status-offline {
  color: #b3261e;
  background: #fdecea;
}

/* Selection shown as a thin ring in the badge's own color (same inset-
   ring technique already used for the Type pill's selected state)
   rather than any fill/color change, so the badge reads identically to
   the card's whether or not it's currently an active filter. */
.status-badge.status-filter[aria-pressed="true"] {
  box-shadow: inset 0 0 0 1.5px currentColor;
}

.status-badge.status-filter[aria-pressed="true"]:focus-visible {
  box-shadow: var(--ring), inset 0 0 0 1.5px currentColor;
}

/* Count badge — same shape as the Type/Creator filter pills' own .n
   (tabular numerals, small pill), but tinted from this pill's own
   status color via color-mix rather than a flat neutral overlay — the
   same mechanism .pill[data-filter="org"] .n already uses to derive
   its teal tint from --accent-2, just fed #0a7d55/#b3261e instead of
   an accent variable. (Type's own .n, by contrast, is a translucent
   *white* overlay — right for its solid hue fill, but this pill's
   background is a pale tint like Organization's, not a solid fill, so
   a white overlay here would barely register against it.) */
.pill.status-online .n {
  background: color-mix(in srgb, #0a7d55 18%, var(--surface));
  color: #0a7d55;
}

.pill.status-offline .n {
  background: color-mix(in srgb, #b3261e 18%, var(--surface));
  color: #b3261e;
}
