/* ============================================================================
   ICResG — Components: toasts, alerts, reveals, page transitions, layout rails
   ========================================================================== */

/* ================================================================ toasts === */
.toast-stack {
  position: fixed;
  top: calc(var(--nav-h) + var(--sp-3));
  right: var(--sp-4);
  z-index: 2000;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  width: min(400px, calc(100vw - var(--sp-4) * 2));
  pointer-events: none;
}
@media (max-width: 575.98px) {
  .toast-stack {
    top: auto;
    bottom: var(--sp-4);
    left: var(--sp-3);
    right: var(--sp-3);
    width: auto;
    flex-direction: column-reverse;
  }
}

.toast-item {
  --toast-accent: var(--teal);
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  padding: var(--sp-4);
  padding-left: calc(var(--sp-4) + 4px);
  border-radius: var(--radius);
  background: var(--paper);
  border: 1px solid var(--hairline);
  box-shadow: var(--shadow-lg);
  pointer-events: auto;
  overflow: hidden;
  /* Entrance is driven by an animation so it plays once, then the element
     settles; dismissal adds .is-leaving. */
  animation: toast-in var(--dur-slow) var(--ease-spring) both;
}
@media (max-width: 575.98px) { .toast-item { animation-name: toast-in-up; } }

.toast-item::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 4px;
  background: var(--toast-accent);
}
.toast-item.is-leaving {
  animation: toast-out var(--dur) var(--ease-in-out) both;
}
@keyframes toast-in  { from { opacity: 0; transform: translateX(110%) scale(.96); } to { opacity: 1; transform: none; } }
@keyframes toast-in-up { from { opacity: 0; transform: translateY(110%) scale(.96); } to { opacity: 1; transform: none; } }
@keyframes toast-out { to { opacity: 0; transform: translateX(110%) scale(.94); max-height: 0; padding-top: 0; padding-bottom: 0; margin-bottom: calc(var(--sp-3) * -1); } }

.toast-item__ico { --ico-size: 1.3rem; flex: none; margin-top: .05em; color: var(--toast-accent); }
.toast-item__body { flex: 1; min-width: 0; }
.toast-item__title {
  font-family: var(--head);
  font-weight: 700;
  font-size: var(--fs-sm);
  color: var(--ink);
  line-height: 1.35;
}
.toast-item__msg {
  font-size: var(--fs-sm);
  color: var(--ink-soft);
  line-height: 1.5;
  margin-top: .12em;
  overflow-wrap: anywhere;
}
.toast-item__close {
  flex: none;
  width: 26px; height: 26px;
  display: grid; place-items: center;
  border: 0; background: none;
  border-radius: 50%;
  color: var(--ink-faint);
  cursor: pointer;
  transition: background var(--dur-fast), color var(--dur-fast), transform var(--dur-fast) var(--ease-spring);
}
.toast-item__close:hover { background: var(--paper-sunk); color: var(--ink); transform: rotate(90deg); }
.toast-item__close .ico { --ico-size: .9rem; }

/* Auto-dismiss countdown rail */
.toast-item__timer {
  position: absolute;
  left: 4px; right: 0; bottom: 0;
  height: 2px;
  background: var(--toast-accent);
  opacity: .35;
  transform-origin: left;
  animation: toast-timer linear both;
  animation-duration: var(--toast-ttl, 5500ms);
}
@keyframes toast-timer { from { transform: scaleX(1); } to { transform: scaleX(0); } }
.toast-item:hover .toast-item__timer { animation-play-state: paused; }

.toast-item--success { --toast-accent: #2e9e5b; }
.toast-item--danger  { --toast-accent: #d1453b; }
.toast-item--warning { --toast-accent: var(--amber); }
.toast-item--info    { --toast-accent: #2b7fd4; }

/* ================================================================ alerts === */
/* Inline, in-flow alerts (form errors, page-level notices). */
.alert-ds {
  --alert-accent: var(--teal);
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  padding: var(--sp-4);
  border-radius: var(--radius);
  border: 1px solid var(--hairline);
  border-left: 4px solid var(--alert-accent);
  background: var(--paper-alt);
  color: var(--ink);
  font-size: var(--fs-sm);
  line-height: 1.55;
  animation: alert-in var(--dur-slow) var(--ease-out) both;
}
@keyframes alert-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
.alert-ds__ico { --ico-size: 1.2rem; flex: none; margin-top: .1em; color: var(--alert-accent); }
.alert-ds--success { --alert-accent: #2e9e5b; background: #f0f9f3; }
.alert-ds--danger  { --alert-accent: #d1453b; background: #fdf1f0; }
.alert-ds--warning { --alert-accent: var(--amber); background: var(--amber-soft); }
.alert-ds--info    { --alert-accent: #2b7fd4; background: #eff6fd; }

/* ============================================== scroll-reveal animations === */
/* Opt-in: add data-reveal to any element. JS adds .is-in when it enters view. */
[data-reveal] {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity var(--dur-page) var(--ease-out),
              transform var(--dur-page) var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
  will-change: opacity, transform;
}
[data-reveal="left"]  { transform: translateX(-26px); }
[data-reveal="right"] { transform: translateX(26px); }
[data-reveal="scale"] { transform: scale(.94); }
[data-reveal="fade"]  { transform: none; }
[data-reveal].is-in { opacity: 1; transform: none; will-change: auto; }

/* Stagger a whole group: put data-reveal-group on the container and every
   direct child animates in sequence. JS adds .is-in to the container once it
   enters the viewport, so the children never need individual attributes. */
[data-reveal-group] > * {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity var(--dur-page) var(--ease-out),
              transform var(--dur-page) var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}
[data-reveal-group].is-in > * { opacity: 1; transform: none; }

[data-reveal-group] > *:nth-child(1)  { --reveal-delay: 0ms; }
[data-reveal-group] > *:nth-child(2)  { --reveal-delay: 70ms; }
[data-reveal-group] > *:nth-child(3)  { --reveal-delay: 140ms; }
[data-reveal-group] > *:nth-child(4)  { --reveal-delay: 210ms; }
[data-reveal-group] > *:nth-child(5)  { --reveal-delay: 280ms; }
[data-reveal-group] > *:nth-child(6)  { --reveal-delay: 350ms; }
[data-reveal-group] > *:nth-child(7)  { --reveal-delay: 420ms; }
[data-reveal-group] > *:nth-child(8)  { --reveal-delay: 490ms; }
[data-reveal-group] > *:nth-child(n+9) { --reveal-delay: 560ms; }

/* ============================================== page enter / exit motion === */
/* A short, restrained cross-fade. Long enough to feel deliberate, short enough
   that it never delays a click. Skipped entirely under reduced-motion. */
@media (prefers-reduced-motion: no-preference) {
  main#main { animation: page-in var(--dur-page) var(--ease-out) both; }
  @keyframes page-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
  body.is-leaving main#main { opacity: 0; transform: translateY(-6px); transition: opacity 180ms var(--ease-in-out), transform 180ms var(--ease-in-out); animation: none; }
}

/* Top-of-page loading rail shown during a navigation. */
.route-bar {
  position: fixed;
  top: 0; left: 0;
  height: 3px;
  width: 100%;
  z-index: 3000;
  transform: scaleX(0);
  transform-origin: left;
  background: linear-gradient(90deg, var(--teal), var(--amber));
  box-shadow: 0 0 12px rgba(20, 176, 160, .6);
  opacity: 0;
  pointer-events: none;
}
.route-bar.is-active { opacity: 1; animation: route-progress 1.4s var(--ease-out) forwards; }
@keyframes route-progress {
  0%   { transform: scaleX(0); }
  55%  { transform: scaleX(.72); }
  100% { transform: scaleX(.94); }
}

/* ================================================= layout rails & rhythm === */
/* One vertical rhythm for every section, fluid across the whole range. */
.section-y   { padding-block: var(--sp-7); }
.section-y-s { padding-block: var(--sp-6); }
.stack > * + * { margin-top: var(--sp-4); }
.stack-s > * + * { margin-top: var(--sp-2); }

/* Auto-fitting grid: no breakpoint maths, correct from 360px to 4K.
   --col sets the ideal minimum column width. */
.auto-grid {
  display: grid;
  gap: var(--sp-4);
  grid-template-columns: repeat(auto-fit, minmax(min(var(--col, 280px), 100%), 1fr));
}
.auto-grid--sm { --col: 210px; }
.auto-grid--lg { --col: 360px; }

/* Cap the *content* on ultra-wide displays even when the container is fluid,
   so a 4K screen reads as a designed page and not a stretched one. */
.measure    { max-width: 72ch; }
.measure-lg { max-width: 90ch; }

/* Cards lift on hover; the whole card is the hit target. */
.card-lift {
  transition: transform var(--dur) var(--ease-out),
              box-shadow var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out);
}
.card-lift:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-lg);
  border-color: rgba(20, 176, 160, .35);
}

/* Anything wide (tables, code, diagrams) scrolls inside itself rather than
   forcing the page to scroll sideways on a phone. */
.scroll-x {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
}
.table-responsive, .scroll-x { overscroll-behavior-x: contain; }

/* ==================================================== push-permission chip == */
.push-chip {
  position: fixed;
  left: var(--sp-4);
  bottom: var(--sp-4);
  z-index: 1500;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  max-width: min(380px, calc(100vw - var(--sp-4) * 2));
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--radius-lg);
  background: var(--navy);
  color: #fff;
  border: 1px solid rgba(120, 175, 205, .25);
  box-shadow: var(--shadow-xl);
  font-size: var(--fs-sm);
  animation: toast-in-up var(--dur-slow) var(--ease-spring) both;
  animation-delay: 1.2s;
}
.push-chip .ico { --ico-size: 1.2rem; color: var(--teal); flex: none; }
.push-chip__actions { display: flex; gap: var(--sp-2); margin-left: auto; flex: none; }
.push-chip__btn {
  border: 1px solid rgba(255, 255, 255, .22);
  background: rgba(255, 255, 255, .08);
  color: #fff;
  border-radius: var(--radius-pill);
  padding: .35em .9em;
  font-family: var(--head);
  font-size: var(--fs-xs);
  font-weight: 600;
  cursor: pointer;
  transition: background var(--dur-fast), transform var(--dur-fast) var(--ease-spring);
}
.push-chip__btn:hover { background: rgba(255, 255, 255, .18); transform: translateY(-1px); }
.push-chip__btn--primary { background: var(--teal); border-color: var(--teal); }
.push-chip__btn--primary:hover { background: var(--teal-dark); }

/* ================================================ card grids that adapt === */
/* Drop-in for a Bootstrap `.row` of cards. Replaces breakpoint-stepped columns
   with an auto-fitting track list, so the same markup gives 1 column on a
   phone and 5-6 on a 4K panel without any breakpoint maths. Set --col to
   change the ideal card width. */
.grid-cards {
  display: grid !important;
  /* The track floor grows with the viewport but stops, so column count rises
     with the screen without the cards ever becoming cramped slivers. */
  --col: clamp(260px, 12vw + 100px, 400px);
  grid-template-columns: repeat(auto-fit, minmax(min(var(--col), 100%), 1fr));
  gap: var(--sp-4);
  margin: 0 !important;
}
.grid-cards > [class*="col-"],
.grid-cards > * {
  width: auto !important;
  max-width: none !important;
  flex: none !important;
  padding-left: 0 !important;
  padding-right: 0 !important;
  margin-top: 0 !important;
}
.grid-cards--sm { --col: clamp(200px, 8vw + 90px, 300px); }
.grid-cards--lg { --col: clamp(300px, 16vw + 120px, 520px); }
/* Cards inside a grid track fill their cell, so rows always line up. */
.grid-cards > * > .card,
.grid-cards > * > .card-plain { height: 100%; }

/* ============================================== auth: single-panel card === */
/* The verify / closed pages have no split-brand column, so the card centres
   on one measure rather than stretching a lone panel across the full width. */
.auth-card--single {
  grid-template-columns: 1fr !important;
  max-width: 620px;
  margin-inline: auto;
}
.auth-card--single .auth-form { padding: clamp(2rem, 1.6rem + 2vw, 3.5rem); }

.verify-mark {
  display: grid;
  place-items: center;
  width: 72px; height: 72px;
  margin: 0 auto var(--sp-4);
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent-dark);
  border: 1px solid var(--accent);
}
.verify-mark .ico { --ico-size: 2rem; stroke-width: 1.6; }
.verify-mark--warn {
  background: var(--gold-soft);
  color: var(--gold);
  border-color: var(--gold);
}

/* ================================================== portal: project form === */
.pf-head {
  display: flex; flex-wrap: wrap; align-items: flex-end;
  justify-content: space-between; gap: var(--sp-3);
  padding-bottom: var(--sp-4); margin-bottom: var(--sp-4);
  border-bottom: 1px solid var(--hairline);
}
.pf { max-width: 900px; }
.pf-block {
  padding: clamp(1.1rem, 1rem + .7vw, 1.8rem);
  margin-bottom: var(--sp-4);
  background: var(--raise);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
}
.pf-legend {
  font-family: var(--body);
  font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 var(--sp-3);
  padding-bottom: .6rem;
  border-bottom: 1px solid var(--hairline);
}
/* The recruiting switch is the decision that changes who sees the project,
   so it is given its own weight rather than sitting as one checkbox in a row. */
.pf-switch {
  padding: var(--sp-3);
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  border: 1px solid var(--tag-ok-bd);
}
.pf-switch .form-check-label { font-size: var(--fs-sm); }
.pf-actions {
  display: flex; flex-wrap: wrap; gap: var(--sp-3);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--hairline);
}

/* ============================================== OAuth provider buttons === */
.oauth-split {
  display: flex; align-items: center; gap: var(--sp-3);
  margin: var(--sp-4) 0 var(--sp-3);
  font-size: var(--fs-xs); color: var(--ink-faint);
  text-transform: uppercase; letter-spacing: .14em; font-weight: 700;
}
.oauth-split::before, .oauth-split::after {
  content: ""; flex: 1; height: 1px; background: var(--hairline);
}
.oauth-row { display: grid; gap: var(--sp-2); grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
.oauth-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .6em;
  padding: .8em 1.1em;
  border: 1.5px solid var(--hairline);
  border-radius: var(--radius-pill);
  background: var(--raise);
  color: var(--ink);
  font-family: var(--body); font-weight: 700; font-size: var(--fs-sm);
  text-decoration: none;
  transition: border-color var(--dur) var(--ease-out),
              transform var(--dur) var(--ease-spring),
              box-shadow var(--dur) var(--ease-out);
}
.oauth-btn:hover {
  color: var(--ink); text-decoration: none;
  border-color: var(--accent);
  transform: translateY(-2px);
  box-shadow: var(--shadow-sm);
}
.oauth-btn__mark { --ico-size: 1.15rem; }
/* ORCID's own green, so the mark is recognisable at a glance. */
.oauth-btn--orcid .oauth-btn__mark { color: #A6CE39; }
.oauth-btn.is-pending {
  opacity: .55;
  cursor: not-allowed;
}
.oauth-btn.is-pending:hover {
  transform: none;
  box-shadow: none;
  border-color: var(--hairline);
}

/* ================================================================ buttons ===
   One button system for the whole site, matching the hero.

   What was wrong: primary buttons carried `color: var(--navy-3)` — near-black
   text on saturated blue — left over from the jade palette, and they were
   8px-radius while the hero used pills, so no two calls to action agreed.
   ========================================================================== */
.btn {
  --btn-bg: transparent;
  --btn-fg: var(--ink);
  --btn-bd: transparent;
  --btn-shadow: none;

  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .5em;
  font-family: var(--body);
  font-weight: 700;
  font-size: var(--fs-sm);
  line-height: 1.2;
  letter-spacing: .005em;
  padding: .72em 1.5em;
  border-radius: var(--radius-pill);
  border: 1.5px solid var(--btn-bd);
  background: var(--btn-bg);
  color: var(--btn-fg);
  box-shadow: var(--btn-shadow);
  text-decoration: none;
  white-space: nowrap;
  transition: transform var(--dur) var(--ease-spring),
              background var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out),
              box-shadow var(--dur) var(--ease-out),
              color var(--dur) var(--ease-out);
}
.btn:hover { transform: translateY(-2px); text-decoration: none; color: var(--btn-fg); }
.btn:active { transform: translateY(0); }
.btn:disabled, .btn.disabled { opacity: .55; transform: none; box-shadow: none; cursor: not-allowed; }
.btn .ico { --ico-size: 1.05em; }

.btn-sm { padding: .5em 1.1em; font-size: var(--fs-xs); }
.btn-lg { padding: .9em 1.9em; font-size: var(--fs-base); }

/* Primary — the one blue. White text, never dark. */
.btn-primary, .btn-teal {
  --btn-bg: var(--accent);
  --btn-fg: #fff;
  --btn-bd: var(--accent);
  --btn-shadow: 0 6px 20px rgba(37, 99, 235, .34);
}
/* Hover DARKENS. Lightening to #3B82F6 dropped white text to 3.68:1 — only
   AA-large — where darkening lifts it to 7.4:1 and reads more deliberate. */
.btn-primary:hover, .btn-teal:hover,
.btn-primary:focus-visible, .btn-teal:focus-visible {
  --btn-bg: var(--accent-dark);
  --btn-bd: var(--accent-dark);
  --btn-fg: #fff;
  --btn-shadow: 0 10px 30px rgba(37, 99, 235, .45);
}

/* Secondary — outlined, for the second action in a pair. */
.btn-outline-primary {
  --btn-fg: var(--accent-dark);
  --btn-bd: rgba(37, 99, 235, .45);
  --btn-bg: transparent;
}
.btn-outline-primary:hover, .btn-outline-primary:focus-visible {
  --btn-bg: var(--accent);
  --btn-bd: var(--accent);
  --btn-fg: #fff;
  --btn-shadow: 0 8px 24px rgba(37, 99, 235, .3);
}

/* Quiet — tertiary actions, filters, table rows. */
.btn-outline-secondary {
  --btn-fg: var(--ink-soft);
  --btn-bd: var(--hairline);
  --btn-bg: var(--raise);
}
.btn-outline-secondary:hover, .btn-outline-secondary:focus-visible {
  --btn-fg: var(--ink);
  --btn-bd: var(--ink-faint);
  --btn-bg: var(--paper-alt);
}

/* On a dark band. */
.btn-outline-light {
  --btn-fg: #fff;
  --btn-bd: rgba(255, 255, 255, .34);
  --btn-bg: transparent;
}
.btn-outline-light:hover, .btn-outline-light:focus-visible {
  --btn-bd: #fff;
  --btn-bg: rgba(255, 255, 255, .1);
  --btn-fg: #fff;
}

/* Standing / merit. Amber carries dark text because amber is a light hue —
   white on amber fails contrast. */
.btn-amber {
  --btn-bg: var(--gold);
  --btn-bd: var(--gold);
  --btn-fg: #23180A;
  --btn-shadow: 0 6px 20px rgba(217, 119, 6, .30);
}
.btn-amber:hover, .btn-amber:focus-visible {
  --btn-bg: #B45309;
  --btn-bd: #B45309;
  --btn-fg: #fff;
}

/* Destructive. */
.btn-danger, .btn-outline-danger {
  --btn-fg: var(--clay);
  --btn-bd: rgba(220, 38, 38, .4);
  --btn-bg: transparent;
}
.btn-danger:hover, .btn-outline-danger:hover,
.btn-danger:focus-visible, .btn-outline-danger:focus-visible {
  --btn-bg: var(--clay);
  --btn-bd: var(--clay);
  --btn-fg: #fff;
}

.btn-link {
  --btn-fg: var(--accent-dark);
  --btn-bd: transparent;
  padding-inline: .35em;
  box-shadow: none;
}
.btn-link:hover { --btn-fg: var(--ink); text-decoration: underline; }

/* On the Signal surface the same classes must read on a dark ground. */
[data-surface="signal"] .btn-outline-secondary {
  --btn-fg: var(--ink-soft);
  --btn-bd: var(--hairline);
  --btn-bg: transparent;
}
[data-surface="signal"] .btn-outline-secondary:hover {
  --btn-fg: var(--ink);
  --btn-bg: rgba(226, 234, 236, .08);
  --btn-bd: var(--ink-faint);
}
[data-surface="signal"] .btn-primary,
[data-surface="signal"] .btn-teal {
  --btn-bg: var(--accent);
  --btn-bd: var(--accent);
  --btn-fg: #07100E;                 /* aqua is light — dark text is correct */
  --btn-shadow: 0 6px 20px rgba(53, 224, 200, .26);
}
[data-surface="signal"] .btn-primary:hover,
[data-surface="signal"] .btn-teal:hover {
  --btn-bg: #5DE3D0;
  --btn-bd: #5DE3D0;
  --btn-fg: #07100E;
}
[data-surface="signal"] .btn-outline-primary {
  --btn-fg: var(--accent);
  --btn-bd: rgba(53, 224, 200, .42);
}
[data-surface="signal"] .btn-outline-primary:hover {
  --btn-bg: var(--accent);
  --btn-fg: #07100E;
  --btn-bd: var(--accent);
}

@media (prefers-reduced-motion: reduce) {
  .btn:hover, .btn:active { transform: none; }
}

/* ================================================= legacy component lift ===
   54 `.card-plain`, 38 `.badge-type` and 8 `.icon-chip` instances remain in
   the older templates. Restyling the classes lifts every one of them without
   touching that markup — and avoids a large, risky find-and-replace.
   ========================================================================== */

/* .card-plain used `background: var(--paper)` — the PAGE background — so
   cards had no separation from the ground at all. */
.card-plain {
  background: var(--raise);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  padding: clamp(1.15rem, 1rem + .7vw, 1.6rem);
  box-shadow: var(--shadow-sm);
  transition: transform var(--dur) var(--ease-out),
              box-shadow var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out);
}
a.card-plain:hover,
.card-plain.card-hover-bar:hover {
  transform: translateY(-3px);
  border-color: rgba(37, 99, 235, .3);
  box-shadow: var(--shadow-lg);
}

/* ================================================= small tags, one family ==
   design-system.css already carries a proper `.tag`: one set of metrics driven
   by --tag-bg / --tag-fg / --tag-bd, the same shape the buttons use. The rest
   of the site's little labels had each grown their own — hand-copied metrics
   here, legacy teal and 999px radii in app.css, and a couple that stacked a
   number over a tiny caption in a box (the publication year over the word
   YEAR, the citation count over CITED). Those read as form fields, not tags.

   Everything below now inherits the tag metrics and only declares its colour.
   ========================================================================== */
.badge-type, .pub-badge, .badge-soft, .badge-amber {
  display: inline-flex;
  align-items: center;
  gap: .38em;
  font-family: var(--body);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
  line-height: 1;
  padding: .42em .8em;
  border-radius: var(--radius-pill);
  border: 1px solid var(--tag-bd, var(--hairline));
  background: var(--tag-bg, var(--paper-alt));
  color: var(--tag-fg, var(--ink-soft));
  white-space: nowrap;
  vertical-align: middle;
}
.badge-type, .pub-badge {
  --tag-bg: var(--accent-soft);
  --tag-bd: rgba(37, 99, 235, .18);
  --tag-fg: var(--accent-dark);
}
.badge-amber {
  --tag-bg: var(--gold-soft);
  --tag-bd: rgba(217, 119, 6, .22);
  --tag-fg: #92400E;
}
.badge-soft {
  --tag-bg: var(--paper-alt);
  --tag-bd: var(--hairline);
  --tag-fg: var(--ink-soft);
}

/* Keyword pills stay sentence case — they carry a person's research interests,
   which have to stay readable; only the chrome is brought into line. */
.pill, .tag-cloud .tag {
  display: inline-flex;
  align-items: center;
  font-family: var(--body);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  line-height: 1.3;
  padding: .4em .85em;
  margin: 0 .35rem .4rem 0;
  border-radius: var(--radius-pill);
  background: var(--paper-alt);
  border: 1px solid var(--hairline);
  color: var(--ink-soft);
}
.pill-teal, .tag-cloud .tag {
  background: var(--accent-soft);
  border-color: rgba(37, 99, 235, .18);
  color: var(--accent-dark);
}
.tag-cloud { display: flex; flex-wrap: wrap; gap: .4rem; }
.tag-cloud .tag { margin: 0; }

/* The citation count: same "big number over a tiny caption" box as the old
   publication year. One tag, the count and its unit on one line. */
.pub-cite {
  flex-shrink: 0;
  display: inline-flex;
  align-items: baseline;
  gap: .3em;
  min-width: 0;
  padding: .38em .7em;
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  border: 1px solid rgba(37, 99, 235, .18);
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.pub-cite:hover { background: #DBEAFE; border-color: rgba(37, 99, 235, .38); text-decoration: none; }
.pub-cite .pc-num {
  display: inline;
  font-family: var(--body);
  font-weight: 700;
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: var(--accent-dark);
}
.pub-cite .pc-lbl {
  display: inline;
  margin: 0;
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  line-height: 1;
  color: var(--ink-faint);
}

/* Section marks. */
.icon-chip {
  width: 44px; height: 44px;
  border-radius: var(--radius);
  display: inline-flex; align-items: center; justify-content: center;
  margin-bottom: var(--sp-3);
  background: var(--accent-soft);
  border: 1px solid rgba(37, 99, 235, .16);
  color: var(--accent-dark);
}
.icon-chip .ico { --ico-size: 1.25rem; }

/* Rule-separated lists get consistent rhythm with the rest of the system. */
.list-rule > * { border-bottom: 1px solid var(--hairline); padding: var(--sp-4) 0; }
.list-rule > *:last-child { border-bottom: 0; }
.list-rule > *:first-child { padding-top: 0; }

/* Form controls, so inputs match the buttons they sit beside. */
.form-control, .form-select {
  border-radius: var(--radius-sm);
  border-color: var(--hairline);
  background: var(--raise);
  color: var(--ink);
  font-family: var(--body);
  padding: .62rem .85rem;
  transition: border-color var(--dur-fast), box-shadow var(--dur-fast);
}
.form-control:focus, .form-select:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(37, 99, 235, .16);
}
.form-control::placeholder { color: var(--ink-faint); }
.form-label {
  font-family: var(--body);
  font-weight: 700;
  font-size: var(--fs-xs);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: .35rem;
}
.form-text { font-size: var(--fs-xs); color: var(--ink-faint); }

/* ============================================== registration form layout ===
   Grouped into three short fieldsets rather than one long column, so the form
   reads as three small asks instead of nine fields. */
.auth-form__title {
  font-family: var(--head);
  font-weight: 700;
  font-size: clamp(1.5rem, 1.3rem + 0.72vw, 1.72rem);
  letter-spacing: -.035em;
  color: var(--ink);
  margin: .3rem 0 .3rem;
}
.auth-form__sub { font-size: var(--fs-sm); color: var(--ink-soft); margin: 0 0 var(--sp-4); }
.auth-form__form { margin-top: var(--sp-3); }
.auth-form__foot {
  margin: var(--sp-4) 0 0;
  text-align: center;
  font-size: var(--fs-xs);
  color: var(--ink-faint);
  line-height: 1.6;
}

.field-set { border: 0; padding: 0; margin: 0 0 var(--sp-4); }
.field-set__legend {
  float: left;                 /* legend + fieldset spacing is unreliable */
  width: 100%;
  font-family: var(--body);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  padding: 0 0 .55rem;
  margin: 0 0 .75rem;
  border-bottom: 1px solid var(--hairline);
}
.field-set__legend + * { clear: both; }

.field-grid { display: grid; gap: var(--sp-3); grid-template-columns: 1fr; }
.field + .field, .field-grid + .field, .field + .field-grid { margin-top: var(--sp-4); }
/* ...but never between the columns of a grid. `.field + .field` was matching
   the 2nd and 3rd column too, so each field in a row started a step lower
   than the one before it and the whole form came out as a staircase. The
   grid's own `gap` is the only spacing a row needs. */
.field-grid > .field + .field { margin-top: 0; }
/* Boxes in a row start at the top of their cell, so a cell whose help text
   makes it taller does not drag its neighbour's input down to the bottom. */
.field-grid { align-items: start; }
.field-grid > .field > .field__err,
.field-grid > .field > .form-text { margin-top: .4rem; }
@media (min-width: 560px) {
  .field-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 720px) {
  /* First / Other / Last sit on one line where there is room; the middle
     field is narrower because it is the optional one. */
  .field-grid--3 { grid-template-columns: 1fr .85fr 1fr; }
}
.field { min-width: 0; }
.field__err { margin: .35rem 0 0; font-size: var(--fs-xs); color: var(--clay); }
.req { color: var(--clay); font-weight: 700; }

.consent {
  display: flex;
  align-items: flex-start;
  gap: .7rem;
  margin-top: var(--sp-4);
  padding: var(--sp-3);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--paper-alt);
  font-size: var(--fs-sm);
  line-height: 1.55;
  color: var(--ink-soft);
  cursor: pointer;
}
.consent input { margin-top: .2em; flex: none; width: 1.05em; height: 1.05em; accent-color: var(--accent); }
.consent.is-invalid { border-color: var(--clay); background: #FEF2F2; }

/* When the providers lead the form they get equal billing with the fields. */
.oauth-row--top { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
.oauth-row--top .oauth-btn { padding: .9em 1.1em; }

/* ============================================== affiliation cards, mobile ===
   The footer's country and Visit link were centred while the name and blurb
   were left-aligned, so each card had two competing axes. Everything now
   hangs off one left rail, and the footer only splits into two columns when
   there is room for it.
   ========================================================================== */
.aff-card {
  display: flex;
  flex-direction: column;
  gap: .7rem;
  height: 100%;
  text-align: left;
}
.aff-head { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; }
.aff-logo {
  width: 46px; height: 46px; flex: none;
  border-radius: var(--radius-sm);
  object-fit: contain;
  background: var(--paper-alt);
  padding: 4px;
}
.aff-mark {
  display: grid; place-items: center;
  background: linear-gradient(155deg, #3B82F6, var(--accent-dark));
  color: #fff;
  font-family: var(--head);
  font-weight: 700;
  font-size: .95rem;
  letter-spacing: -.02em;
  padding: 0;
}
.aff-name {
  font-family: var(--head);
  font-weight: 700;
  font-size: clamp(.98rem, .94rem + .3vw, 1.12rem);
  letter-spacing: -.025em;
  line-height: 1.25;
  color: var(--ink);
  margin: 0;
  min-width: 0;
}
.aff-desc {
  margin: 0;
  font-size: var(--fs-sm);
  line-height: 1.6;
  color: var(--ink-soft);
}
.aff-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: .5rem var(--sp-3);
  margin-top: auto;
  padding-top: .8rem;
  border-top: 1px solid var(--hairline);
  text-align: left;
}
.aff-foot small { display: inline-flex; align-items: center; color: var(--ink-faint); font-size: var(--fs-xs); }
.aff-visit {
  font-family: var(--body);
  font-weight: 700;
  font-size: var(--fs-xs);
  color: var(--accent-dark);
  text-decoration: none;
  white-space: nowrap;
}
.aff-visit:hover { color: var(--ink); text-decoration: underline; }

@media (max-width: 575.98px) {
  /* One column on a phone, so each card gets the full width rather than being
     squeezed into a half that cannot hold an institution's name. */
  .aff-card { gap: .6rem; }
  .aff-logo { width: 40px; height: 40px; }
}

/* =========================================== auth pages, rebuilt =========
   The registration form was fighting itself: the split card gave the form
   half of 920px, so three name columns had ~110px each and every label
   wrapped. Two changes fix it — the card is wider and the form side gets the
   larger share, and the field grids now respond to the FORM's width via a
   container query rather than to the viewport, so the columns collapse
   exactly when the column they sit in runs out of room, not before.
   ========================================================================== */
.auth-wrap { display: block; padding-block: clamp(1.5rem, 1.2rem + 2.4vw, 4rem); }
.auth-card {
  display: grid;
  grid-template-columns: 1fr;
  max-width: 1120px;
  margin-inline: auto;
  background: var(--raise);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-lg);
}
@media (min-width: 992px) {
  /* Split only where both halves can actually hold their content. Below
     this the brand panel is hidden and the form takes the full measure. */
  .auth-card { grid-template-columns: minmax(0, 0.78fr) minmax(0, 1.22fr); }
}

.auth-brand {
  display: none;
  padding: clamp(2rem, 1.6rem + 1.6vw, 3rem);
  color: #fff;
  background:
    radial-gradient(60% 55% at 20% 8%, rgba(37, 99, 235, .32) 0%, transparent 68%),
    linear-gradient(165deg, var(--surface-dark-2) 0%, var(--surface-dark) 100%);
}
@media (min-width: 992px) { .auth-brand { display: block; } }
/* The registration form is long, so centring the brand copy in the column put
   it two-thirds of the way down the page where nobody scrolling the form
   would ever see it. It rides along at the top instead. */
.auth-brand__inner { position: sticky; top: clamp(1.5rem, 1.2rem + 2vw, 3rem); }
.auth-brand img { margin-bottom: var(--sp-4); }
.auth-brand h2 {
  font-family: var(--head); font-weight: 700;
  font-size: clamp(1.3rem, 1.15rem + .7vw, 1.75rem);
  letter-spacing: -.035em; line-height: 1.15;
  color: #fff; margin: 0 0 .4rem;
}
.auth-brand > p {
  font-family: var(--body); font-style: normal;
  font-size: var(--fs-sm); color: #93B4F5; margin: 0;
}
.auth-points { list-style: none; padding: 0; margin: clamp(1.4rem, 1.2rem + .8vw, 2rem) 0 0; display: grid; gap: .8rem; }
.auth-points li {
  display: flex; align-items: flex-start; gap: .6rem;
  font-size: var(--fs-sm); line-height: 1.5;
  color: rgba(255, 255, 255, .78);
  margin: 0;
}
.auth-points .ico { --ico-size: 1.05rem; color: #60A5FA; flex: none; margin-top: .12em; }

.auth-form {
  padding: clamp(1.5rem, 1.2rem + 2vw, 3rem);
  container-type: inline-size;   /* the field grids below size off THIS */
  min-width: 0;
}
.auth-form .small-caps { margin-bottom: .1rem; }

/* --- field grids, sized to the form column ------------------------------ */
/* The @media versions above are the no-container-query fallback; these win
   wherever @container is supported, which is every current browser. */
@container (min-width: 420px) {
  .field-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@container (min-width: 520px) {
  /* Other names is the optional one, so it takes the narrow middle track. */
  .field-grid--3 { grid-template-columns: 1fr .8fr 1fr; }
}
@container (max-width: 519.98px) {
  .field-grid--3 { grid-template-columns: 1fr; }
}
@container (max-width: 419.98px) {
  .field-grid--2 { grid-template-columns: 1fr; }
}

.field__aside { margin: .4rem 0 0; font-size: var(--fs-xs); }
.field__aside a { color: var(--ink-faint); }
.field__aside a:hover { color: var(--accent-dark); }

.auth-form .form-label {
  font-family: var(--body); font-weight: 600;
  font-size: var(--fs-xs); letter-spacing: .01em;
  color: var(--ink); margin-bottom: .3rem;
}
.auth-form .form-text { font-size: var(--fs-xs); line-height: 1.5; margin-top: .35rem; }

/* Provider buttons carry a full label now ("Continue with Google"), so give
   them room to sit side by side and let them stack when they cannot. */
.oauth-row, .oauth-row--top { grid-template-columns: repeat(auto-fit, minmax(min(215px, 100%), 1fr)); }
.oauth-btn span { white-space: nowrap; }
.oauth-btn--google:hover { border-color: #4285F4; }
.oauth-btn--orcid:hover { border-color: #A6CE39; }

/* ================================== cards keep their left rail on phones ===
   Bootstrap's `.text-center` and a handful of hand-written mobile rules used
   to centre card contents below 768px. Centred blocks of running text are
   harder to scan — the eye loses the left edge on every line — and it made
   the phone layout read as a different design from the desktop one. Cards
   now hang off one left rail at every width.
   ========================================================================== */
@media (max-width: 767.98px) {
  .card-plain, .pcard, .person, .aff-card, .partner-card, .pillar,
  .spot__body, .grid-cards > *, .ws-card, .pb-card, .tk-card,
  .card-plain *, .pcard *, .person *, .aff-card *, .partner-card * {
    text-align: left;
  }
  /* A lone icon or badge that is centred inside its own round holder is a
     layout box, not running text, so those keep centring. */
  .card-plain .verify-mark, .card-plain .sec__ico,
  .person__portrait, .partner-mark, .aff-mark, .pillar__ico {
    text-align: center;
  }
}

/* ================================= homepage partners / affiliations strip ===
   These were two-up on a phone with the logo BESIDE the name, which left the
   name about 90px and broke most institutions across four lines. Below 576px
   the logo now sits above the name so the full card width is available for
   the text; from 576px up the horizontal row returns.
   ========================================================================== */
.partner-card {
  display: flex;
  align-items: center;
  gap: .85rem;
  padding: .9rem 1rem;
  height: 100%;
  text-align: left;
  text-decoration: none;
  color: inherit;
}
a.partner-card:hover { text-decoration: none; color: inherit; transform: translateY(-3px); box-shadow: var(--shadow-md); }
.partner-logo { flex: 0 0 auto; display: block; width: 52px; height: 52px; }
.partner-logo img {
  width: 52px; height: 52px;
  object-fit: contain;
  border-radius: var(--radius);
  background: var(--paper-alt);
  padding: 4px;
  display: block;
}
.partner-mark {
  width: 52px; height: 52px;
  border-radius: var(--radius);
  display: grid; place-items: center;
  background: linear-gradient(155deg, #3B82F6, var(--accent-dark));
  color: #fff;
  font-family: var(--head); font-weight: 700; font-size: 1.05rem;
  letter-spacing: -.02em;
}
.partner-meta { min-width: 0; display: flex; flex-direction: column; gap: .15rem; }
.partner-name {
  font-family: var(--head); font-weight: 700;
  font-size: var(--fs-sm);
  letter-spacing: -.02em;
  color: var(--ink);
  line-height: 1.3;
}
.partner-meta small { font-size: var(--fs-xs); color: var(--ink-faint); line-height: 1.3; }

/* The stacked mobile variant that used to live here is gone. It made sense
   when these cards were two to a phone row and only ~164px wide; they are full
   width now, so the horizontal layout fits and stacking just made them taller.
   It was also fighting the align-items on the rebuilt card and producing a
   centred column. */

/* ============================ portal: membership & profile, one form ======
   The application and the profile were separate forms asking the same
   questions. They are one page now, so it needs to read as a sequence of
   short sections rather than a wall of inputs: each card states what it is
   for, and the status of the application is answered before anything else.
   ========================================================================== */
.pp-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-3) var(--sp-4);
  margin-bottom: var(--sp-3);
}
.pp-title {
  font-family: var(--head); font-weight: 700;
  font-size: clamp(1.4rem, 1.2rem + 0.72vw, 1.72rem);
  letter-spacing: -.035em; line-height: 1.12;
  color: var(--ink); margin: .2rem 0 .4rem;
}
.pp-lead { max-width: 62ch; color: var(--ink-soft); font-size: var(--fs-sm); line-height: 1.6; margin: 0; }

/* --- profile progress: reports, never gates ----------------------------- */
.mprog {
  display: grid;
  gap: var(--sp-3);
  background: var(--raise);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  padding: clamp(1rem, .9rem + .6vw, 1.5rem);
  margin-bottom: var(--sp-4);
}
.mprog__bar {
  height: 8px;
  border-radius: var(--radius-pill);
  background: var(--paper-alt);
  overflow: hidden;
}
.mprog__fill {
  display: block; height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, #3B82F6, var(--accent-dark));
  transition: width var(--dur-slow) var(--ease-out);
}
.mprog--done .mprog__fill { background: linear-gradient(90deg, #34D399, #059669); }
.mprog__head {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .2rem .7rem;
  margin: 0 0 .35rem;
  font-size: var(--fs-sm);
}
.mprog__head strong { font-family: var(--head); font-weight: 700; letter-spacing: -.02em; color: var(--ink); }
.mprog__head .text-soft { font-size: var(--fs-xs); }
.mprog__note {
  display: flex; align-items: center; gap: .45em;
  margin: 0; font-size: var(--fs-xs); color: var(--ink-soft);
}
.mprog--done .mprog__note { color: #047857; }
.mprog__note .ico { --ico-size: 1rem; flex: none; }
.mprog__list {
  display: flex; flex-wrap: wrap; gap: .4rem;
  list-style: none; padding: 0; margin: .6rem 0 0;
}
.mprog__list li {
  padding: .3em .8em;
  border-radius: var(--radius-pill);
  background: var(--paper-alt);
  border: 1px solid var(--hairline);
  font-size: var(--fs-xs);
  color: var(--ink-soft);
}

.mform { display: grid; gap: var(--sp-4); }
.mcard {
  background: var(--raise);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  padding: clamp(1.15rem, 1rem + .8vw, 1.9rem);
  container-type: inline-size;   /* the field grids size off the card */
}
.mcard__head {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  padding-bottom: var(--sp-3);
  margin-bottom: var(--sp-4);
  border-bottom: 1px solid var(--hairline);
}
.mcard__ico {
  display: grid; place-items: center;
  width: 40px; height: 40px; flex: none;
  border-radius: var(--radius);
  background: var(--accent-soft);
  border: 1px solid rgba(37, 99, 235, .16);
  color: var(--accent-dark);
}
.mcard__ico .ico { --ico-size: 1.15rem; }
.mcard__title {
  font-family: var(--head); font-weight: 700;
  font-size: clamp(1.02rem, .96rem + .4vw, 1.2rem);
  letter-spacing: -.03em; color: var(--ink);
  margin: 0 0 .15rem;
}
.mcard__sub { margin: 0; font-size: var(--fs-xs); line-height: 1.55; color: var(--ink-faint); max-width: 68ch; }
.mcard input[readonly] { background: var(--paper-alt); color: var(--ink-faint); cursor: not-allowed; }

.mform__actions {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-3) var(--sp-4);
  margin-top: var(--sp-2);
}
.mform__actions .btn { flex: 0 0 auto; }
.mform__hint { margin: 0; font-size: var(--fs-xs); color: var(--ink-faint); }
@media (max-width: 479.98px) {
  .mform__actions { flex-direction: column; align-items: stretch; }
}

/* ================================================= form controls, designed ==
   The inputs were Bootstrap's with the colours swapped: 10px tall, a hairline
   border, and labels set in tracked uppercase. On a nine-field registration
   form that reads as shouting at someone who has not agreed to anything yet,
   and the fields themselves disappeared into the card behind them.

   What changes: the field is a real object — a tinted ground, a 44px+ target,
   a visible edge — and the label goes back to sentence case so the eye reads
   "First name", not "F I R S T  N A M E". The focus ring is one style
   everywhere, including on the file and select controls Bootstrap leaves bare.
   ========================================================================== */
.form-label {
  font-family: var(--body);
  font-weight: 600;
  font-size: .82rem;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink);
  margin-bottom: .4rem;
  display: block;
}
.form-control, .form-select {
  width: 100%;
  border: 1.5px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--paper-alt);
  color: var(--ink);
  font-family: var(--body);
  font-size: .95rem;
  line-height: 1.5;
  padding: .72rem .9rem;
  transition: border-color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}
.form-control:hover:not(:focus):not([readonly]),
.form-select:hover:not(:focus) { border-color: #CBD5E1; }
.form-control:focus, .form-select:focus {
  background: var(--raise);
  border-color: var(--accent);
  box-shadow: 0 0 0 4px rgba(37, 99, 235, .14);
  outline: none;
}
.form-control::placeholder { color: var(--ink-faint); opacity: 1; }
textarea.form-control { min-height: 6rem; resize: vertical; line-height: 1.6; }

/* Bootstrap's caret is a fixed-colour data URI, so it has to be restated to
   follow the palette. Padding on the right keeps text clear of it. */
.form-select {
  padding-right: 2.6rem;
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none'%3e%3cpath stroke='%23475569' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 6l4 4 4-4'/%3e%3c/svg%3e");
  background-repeat: no-repeat;
  background-position: right .95rem center;
  background-size: 15px 12px;
}

/* The file input's button half is a separate box; style it as a button. */
.form-control[type="file"] { padding: .4rem .5rem .4rem .4rem; }
.form-control[type="file"]::file-selector-button {
  margin-right: .8rem;
  padding: .5rem .9rem;
  border: 0;
  border-radius: calc(var(--radius) - 3px);
  background: var(--ink);
  color: #fff;
  font-family: var(--body); font-weight: 700; font-size: var(--fs-xs);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out);
}
.form-control[type="file"]::file-selector-button:hover { background: var(--accent-dark); }

.form-control[readonly] { background: var(--paper); color: var(--ink-faint); border-style: dashed; }
.form-control[readonly]:focus { box-shadow: none; border-color: var(--hairline); }

.form-control.is-invalid, .form-select.is-invalid {
  border-color: var(--clay);
  background-image: none;   /* no Bootstrap warning glyph over the caret */
}
.form-select.is-invalid {
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none'%3e%3cpath stroke='%23DC2626' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 6l4 4 4-4'/%3e%3c/svg%3e");
}
.form-control.is-invalid:focus, .form-select.is-invalid:focus {
  box-shadow: 0 0 0 4px rgba(220, 38, 38, .14);
}
.form-text { font-size: var(--fs-xs); color: var(--ink-faint); line-height: 1.5; }

/* The consent checkbox is a target, not a decoration: give it a real box and
   make the whole row clickable. */
.consent {
  display: flex;
  align-items: flex-start;
  gap: .7rem;
  padding: .9rem 1rem;
  border: 1.5px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--paper-alt);
  cursor: pointer;
  font-size: var(--fs-sm);
  line-height: 1.55;
  color: var(--ink-soft);
  transition: border-color var(--dur-fast) var(--ease-out);
}
.consent:hover { border-color: #CBD5E1; }
.consent.is-invalid { border-color: var(--clay); background: #FEF2F2; }
.consent input[type="checkbox"] {
  flex: none;
  width: 1.15rem; height: 1.15rem;
  margin-top: .1em;
  accent-color: var(--accent);
  cursor: pointer;
}
.consent:focus-within { border-color: var(--accent); box-shadow: 0 0 0 4px rgba(37, 99, 235, .14); }

/* Repeatable education rows sit inside the same field language. */
.edu-row {
  display: flex; align-items: flex-start; gap: .6rem;
  padding: .85rem;
  border: 1.5px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--paper-alt);
}
.edu-row + .edu-row { margin-top: .6rem; }
.edu-grid { flex: 1 1 auto; display: grid; gap: .7rem; grid-template-columns: 1fr; min-width: 0; }
@container (min-width: 560px) { .edu-grid { grid-template-columns: 1.4fr 1.4fr .6fr .6fr; } }
@media (min-width: 992px) { .edu-grid { grid-template-columns: 1.4fr 1.4fr .6fr .6fr; } }
.edu-grid .form-control { background: var(--raise); }
.edu-lbl { font-size: var(--fs-xs); color: var(--ink-faint); font-weight: 600; }
.edu-del {
  flex: none;
  display: grid; place-items: center;
  width: 38px; height: 38px;
  margin-top: 1.5rem;
  border: 1.5px solid var(--hairline);
  border-radius: var(--radius-sm);
  background: var(--raise);
  color: var(--ink-faint);
  cursor: pointer;
  transition: color var(--dur-fast), border-color var(--dur-fast);
}
.edu-del:hover { color: var(--clay); border-color: var(--clay); }

/* app.css stacks a set of "label left / actions right" rows below 576px, and
   .aff-foot was swept up in that: it became a centred column of country over
   Visit, fighting the left-aligned name and blurb above it. It is a short row
   of two short things — it stays a row, and it stays on the left rail. */
@media (max-width: 575.98px) {
  .aff-foot {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: .5rem var(--sp-3);
  }
  .aff-foot > * { text-align: left; }
}

/* The hero above these reads left; the stat band under it was centred, so the
   two halves of the same dark panel disagreed about where the page starts.
   They share the left rail on phones now, and the divider between columns is
   dropped because it only makes sense on a single row. */
@media (max-width: 767.98px) {
  .stat { text-align: left; padding-left: 0; padding-right: 0; }
  .stat-band .col:not(:last-child) .stat::after { display: none; }
}

/* =================================================== FAQ accordion, tidied ==
   Brought onto the design tokens: the panel and its items were still using
   the legacy --paper/--navy/--teal names and a hard-coded teal caret, so the
   accordion looked like it belonged to a different site than the cards around
   it. Same shapes, current palette.
   ========================================================================== */
.faq-panel {
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  background: var(--raise);
  box-shadow: var(--shadow-sm);
  padding: clamp(1.15rem, 1rem + .7vw, 1.75rem);
}
.faq-panel h2 { color: var(--ink); }
.faq-accordion .accordion-item {
  border: 1px solid var(--hairline);
  border-radius: var(--radius) !important;
  margin-bottom: .6rem;
  overflow: hidden;
  background: var(--paper-alt);
  transition: border-color var(--dur) var(--ease-out);
}
.faq-accordion .accordion-item:last-child { margin-bottom: 0; }
.faq-accordion .accordion-item:has(.accordion-button:not(.collapsed)) { border-color: rgba(37, 99, 235, .3); }
.faq-accordion .accordion-button {
  font-family: var(--body);
  font-weight: 700;
  font-size: var(--fs-sm);
  line-height: 1.45;
  color: var(--ink);
  background: transparent;
  padding: .95rem 1.1rem;
  box-shadow: none;
}
.faq-accordion .accordion-button:not(.collapsed) {
  color: var(--accent-dark);
  background: var(--accent-soft);
  box-shadow: none;
}
.faq-accordion .accordion-button:focus-visible {
  box-shadow: 0 0 0 4px rgba(37, 99, 235, .14);
  border-color: var(--accent);
}
.faq-accordion .accordion-button::after {
  width: 1rem; height: 1rem; background-size: 1rem;
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='%23475569' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 6l4 4 4-4'/%3e%3c/svg%3e");
  filter: none;
}
.faq-accordion .accordion-button:not(.collapsed)::after {
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='%231D4ED8' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 6l4 4 4-4'/%3e%3c/svg%3e");
}
.faq-accordion .accordion-body {
  color: var(--ink-soft);
  font-size: var(--fs-sm);
  padding: .2rem 1.1rem 1.1rem;
  line-height: 1.7;
}

/* ======================== admin tables become cards on a phone ============
   An admin list is a wide table by nature — Members ran to 1472px and
   Publications to 1637px. Horizontal scrolling inside a 375px viewport is a
   poor way to read a record and a worse way to reach the Edit button at the
   far right, so below 768px each row is re-laid as a card: the column heading
   moves in front of its value (carried on the cell's data-label), the header
   row is dropped, and the actions get a full-width strip of their own.

   The table markup is untouched — this is all presentational, so the desktop
   table and the phone cards stay the same one source of truth.
   ========================================================================== */
@media (max-width: 767.98px) {
  .admin-table { overflow: visible; border: 0; background: none; box-shadow: none; }
  .admin-table > table,
  .admin-table > table > tbody { display: block; width: 100%; }
  .admin-table > table > thead { display: none; }   /* the labels move into the cells */

  .admin-table > table > tbody > tr {
    display: block;
    background: var(--raise);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    box-shadow: var(--shadow-xs);
    padding: .35rem .9rem .9rem;
    margin-bottom: .7rem;
  }
  .admin-table > table > tbody > tr:last-child { margin-bottom: 0; }

  .admin-table > table > tbody > td,
  .admin-table > table > tbody > tr > td {
    display: grid;
    grid-template-columns: minmax(5.5rem, 38%) minmax(0, 1fr);
    align-items: baseline;
    gap: var(--sp-3);
    width: auto;
    padding: .5rem 0;
    border: 0;
    border-bottom: 1px solid var(--hairline);
    white-space: normal;
    overflow-wrap: anywhere;
    text-align: left;
  }
  .admin-table > table > tbody > tr > td:last-child { border-bottom: 0; }
  .admin-table > table > tbody > tr > td::before {
    content: attr(data-label);
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--ink-faint);
    line-height: 1.5;
  }
  /* A cell with no heading behind it (a spacer column) must not print "" and
     leave a hanging indent. */
  .admin-table > table > tbody > tr > td:not([data-label]) { grid-template-columns: 1fr; }

  /* Actions: a full-width strip, and every control a real 40px target. */
  .admin-table > table > tbody > tr > td.admin-table__actions,
  .admin-table > table > tbody > tr > td:last-child:has(.btn) {
    grid-template-columns: 1fr;
    gap: .5rem;
    padding-top: .8rem;
  }
  .admin-table > table > tbody > tr > td.admin-table__actions::before { margin-bottom: -.2rem; }
  .admin-table__actions .btn,
  .admin-table > table > tbody > tr > td:last-child .btn { min-height: 40px; }
  .admin-table__actions form,
  .admin-table > table > tbody > tr > td:last-child form { display: inline-block; }
  .admin-table__actions > *,
  .admin-table > table > tbody > tr > td:last-child > * { margin-right: .4rem; margin-bottom: .1rem; }

  /* The empty state spans the card, not a phantom column. */
  .admin-table > table > tbody > tr > td[colspan] { grid-template-columns: 1fr; text-align: left; }
  .admin-table > table > tbody > tr > td[colspan]::before { content: none; }
}

/* ============================ admin tables follow their surface ===========
   Bootstrap paints every cell with --bs-table-bg (#fff) and --bs-table-color
   (#212529) via an inset box-shadow, and nothing was overriding it — so the
   admin list tables rendered as a white sheet with black text sitting on the
   dark Signal shell. Handing those variables to the design tokens makes the
   table take whichever surface it is on, light or dark, with no per-surface
   rules to keep in sync.
   ========================================================================== */
.admin-table > table {
  --bs-table-bg: transparent;
  --bs-table-color: var(--ink);
  --bs-table-border-color: var(--hairline);
  --bs-table-striped-bg: var(--paper-alt);
  --bs-table-striped-color: var(--ink);
  --bs-table-hover-bg: var(--paper-alt);
  --bs-table-hover-color: var(--ink);
  --bs-table-accent-bg: transparent;
  color: var(--ink);
  margin-bottom: 0;
}
.admin-table > table > tbody > tr > td,
.admin-table > table > thead > tr > th {
  background-color: transparent;
  color: inherit;
  border-color: var(--hairline);
}
.admin-table > table > tbody > tr:hover > td { background-color: var(--paper-alt); }

/* Headings were wrapping mid-word ("MEMBE / R ID") because the tracked
   uppercase label is wider than the column it names. They stay on one line
   and the table scrolls instead — that is what the scroll container is for. */
.admin-table th {
  white-space: nowrap;
  background: var(--paper-alt) !important;
  color: var(--ink-faint) !important;
  border-bottom: 1px solid var(--hairline) !important;
  font-family: var(--body);
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
}
.admin-table td { color: var(--ink); vertical-align: middle; }

/* Row actions: one wrapping row of equal-height controls, not a squeezed
   column of five stacked buttons. */
.admin-table__actions { white-space: nowrap; }
.admin-table__actions .btn { vertical-align: middle; }
@media (min-width: 768px) {
  .admin-table__actions {
    display: flex;
    flex-wrap: wrap;
    gap: .3rem;
    justify-content: flex-end;
    align-items: center;
    /* Five actions in a 166px column wrapped into a stack five rows tall and
       made every row of the table that height. The column gets the width it
       needs and the table scrolls, which it is already set up to do. */
    min-width: 22rem;
  }
  .admin-table__actions form { display: contents; }
}

/* The dashboard's own tables (.dt) get the same phone treatment. They scroll
   rather than clip, so they were never unreachable — but a three-column table
   read at 375px through a horizontal scrollbar is still the wrong shape for
   the panel it sits in. */
@media (max-width: 767.98px) {
  .table-responsive:has(> table.dt) { overflow: visible; }
  table.dt, table.dt > tbody { display: block; width: 100%; }
  table.dt > thead { display: none; }
  table.dt > tbody > tr {
    display: block;
    padding: .55rem 0 .7rem;
    border-bottom: 1px solid var(--hairline);
  }
  table.dt > tbody > tr:last-child { border-bottom: 0; padding-bottom: 0; }
  table.dt > tbody > tr > td {
    display: grid;
    grid-template-columns: minmax(5rem, 34%) minmax(0, 1fr);
    align-items: baseline;
    gap: var(--sp-3);
    padding: .3rem 1.15rem;
    border: 0;
    white-space: normal;
    overflow-wrap: anywhere;
  }
  table.dt > tbody > tr > td::before {
    content: attr(data-label);
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--ink-faint);
  }
  table.dt > tbody > tr > td[data-label=""] { grid-template-columns: 1fr; }
  table.dt > tbody > tr > td[data-label=""]::before { content: none; }
}

@media (max-width: 767.98px) {
  /* A cell with no value printed its heading and nothing else — an orphan
     "VENUE" label with a blank line after it. In a table the empty cell is
     invisible; as a card row it has to go. */
  .admin-table > table > tbody > tr > td:empty,
  table.dt > tbody > tr > td:empty { display: none; }
}

/* Bootstrap's dismiss control is 31px — under the 44px a finger needs and
   under the 40px this site uses everywhere else. */
.btn-close {
  min-width: 40px;
  min-height: 40px;
  background-position: center;
  opacity: .75;
}
.btn-close:hover { opacity: 1; }

/* Bootstrap's checkbox is 1em square — around 16px, well under a finger.
   The site's own .consent control already uses 1.15rem; match it. */
.form-check-input {
  width: 1.15rem;
  height: 1.15rem;
  margin-top: .18em;
  accent-color: var(--accent);
  cursor: pointer;
}
.form-check-input:focus-visible { box-shadow: 0 0 0 4px rgba(37, 99, 235, .14); }
.form-check-label { cursor: pointer; padding-left: .15rem; }

/* ==================== the button variants the admin actually emits ========
   action_button_class() returns btn-success for activate/approve/publish and
   btn-outline-warning for suspend/unpublish/reject — neither of which existed
   in this button system. They fell through to Bootstrap's defaults, which the
   token layer neutralises, so "Activate" and "Suspend" rendered as bare text
   with no chrome at all. Every class that helper can produce is defined here.
   ========================================================================== */
.btn-success {
  --btn-bg: #059669;
  --btn-fg: #fff;
  --btn-bd: #059669;
  --btn-shadow: 0 6px 20px rgba(5, 150, 105, .30);
}
.btn-success:hover, .btn-success:focus-visible {
  --btn-bg: #047857;
  --btn-bd: #047857;
  --btn-fg: #fff;
  --btn-shadow: 0 10px 30px rgba(5, 150, 105, .40);
}
.btn-outline-success {
  --btn-fg: #047857;
  --btn-bd: rgba(5, 150, 105, .45);
  --btn-bg: transparent;
}
.btn-outline-success:hover, .btn-outline-success:focus-visible {
  --btn-fg: #fff; --btn-bg: #059669; --btn-bd: #059669;
}

.btn-warning {
  --btn-bg: var(--gold);
  --btn-fg: #1F1400;
  --btn-bd: var(--gold);
  --btn-shadow: 0 6px 20px rgba(217, 119, 6, .28);
}
.btn-warning:hover, .btn-warning:focus-visible {
  --btn-bg: #B45309; --btn-bd: #B45309; --btn-fg: #fff;
}
.btn-outline-warning {
  --btn-fg: #B45309;
  --btn-bd: rgba(217, 119, 6, .45);
  --btn-bg: transparent;
}
.btn-outline-warning:hover, .btn-outline-warning:focus-visible {
  --btn-fg: #fff; --btn-bg: #B45309; --btn-bd: #B45309;
}
/* On the dark portal/admin shell the muted browns above go murky; the tokens
   there are already tuned for a dark ground. */
[data-surface="signal"] .btn-outline-success { --btn-fg: #34D399; --btn-bd: rgba(52, 211, 153, .45); }
[data-surface="signal"] .btn-outline-warning { --btn-fg: #FBBF24; --btn-bd: rgba(251, 191, 36, .45); }
[data-surface="signal"] .btn-outline-warning:hover,
[data-surface="signal"] .btn-outline-warning:focus-visible { --btn-bg: #FBBF24; --btn-fg: #1F1400; --btn-bd: #FBBF24; }
[data-surface="signal"] .btn-success { --btn-bg: #34D399; --btn-fg: #05231A; --btn-bd: #34D399; }
[data-surface="signal"] .btn-success:hover,
[data-surface="signal"] .btn-success:focus-visible { --btn-bg: #6EE7B7; --btn-bd: #6EE7B7; --btn-fg: #05231A; }

/* Actions on a phone: a tidy full-width stack on the card's left rail. The
   cell carries Bootstrap's .text-end for the desktop table, which is
   !important and would otherwise drag the whole strip to the right. */
@media (max-width: 767.98px) {
  .admin-table > table > tbody > tr > td.admin-table__actions { text-align: left !important; }
  .admin-table__actions .btn { width: 100%; }
  .admin-table__actions > *,
  .admin-table__actions form { margin: 0; width: 100%; display: block; }
}

/* The admin list toolbar. Its buttons wrap rather than compress, and on a
   phone each one takes the full width so no label is ever clipped. */
.res-actions { align-items: center; }
@media (max-width: 575.98px) {
  .res-actions { flex-direction: column; align-items: stretch; }
  .res-actions > *, .res-actions form { width: 100%; }
  .res-actions .btn { width: 100%; }
}

/* A row's negative gutter margins must never exceed the padding of the
   container it sits in, or the page scrolls sideways by the difference.
   Bootstrap's .g-5 pulls -24px a side while .container gives 12-16px, so
   /, /membership and /contact each scrolled 8px. Capping the HORIZONTAL
   gutter fixes it; --bs-gutter-y is untouched, so the vertical rhythm of
   those rows is exactly as before. */
.row.g-5, .row.g-lg-5, .row.gx-5, .row.gx-lg-5 { --bs-gutter-x: 1.5rem; }
@media (min-width: 1200px) {
  /* Wide enough that the container's own padding covers a bigger gutter. */
  .row.g-5, .row.g-lg-5, .row.gx-5, .row.gx-lg-5 { --bs-gutter-x: 2.5rem; }
}

/* ============================================ project proposal review =====
   The one approval an administrator makes. Each proposal is a card with
   enough of the pitch to decide on, the full text behind a disclosure, and
   the decision attached to it — so accepting or declining never means
   holding the details in your head across a page.
   ========================================================================== */
.prev-section {
  display: flex; align-items: center; gap: .6rem;
  font-family: var(--head); font-weight: 700;
  font-size: var(--fs-md); letter-spacing: -.02em;
  color: var(--ink); margin: 0 0 var(--sp-4);
}
.prev-count {
  display: inline-grid; place-items: center;
  min-width: 1.7em; padding: .18em .5em;
  border-radius: var(--radius-pill);
  background: var(--accent-soft); border: 1px solid rgba(37, 99, 235, .2);
  font-size: var(--fs-xs); font-weight: 700; color: var(--accent-dark);
}
.prev-list { display: grid; gap: var(--sp-4); }
.prev-card {
  background: var(--raise);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  padding: clamp(1.1rem, 1rem + .7vw, 1.75rem);
}
.prev-card__head {
  display: flex; flex-wrap: wrap; align-items: flex-start;
  justify-content: space-between; gap: var(--sp-3);
  margin-bottom: .6rem;
}
.prev-card__meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: .45rem;
  margin: 0 0 .25rem;
  font-size: var(--fs-xs); color: var(--ink-faint);
}
.prev-card__meta .ico { --ico-size: .9rem; }
.prev-card__title {
  font-family: var(--head); font-weight: 700;
  font-size: var(--fs-md); letter-spacing: -.025em; line-height: 1.25;
  color: var(--ink); margin: 0;
}
.prev-card__summary { margin: 0 0 var(--sp-3); font-size: var(--fs-sm); line-height: 1.7; color: var(--ink-soft); }

.prev-facts { display: flex; flex-wrap: wrap; gap: var(--sp-3) var(--sp-5); margin: 0 0 var(--sp-3); }
.prev-facts dt {
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: .07em;
  text-transform: uppercase; color: var(--ink-faint);
}
.prev-facts dd { margin: .15rem 0 0; font-size: var(--fs-sm); color: var(--ink); }

.prev-more { margin-bottom: var(--sp-3); }
.prev-more summary {
  cursor: pointer; font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: .05em; text-transform: uppercase; color: var(--accent-dark);
  padding: .35rem 0;
}
.prev-more[open] summary { margin-bottom: .5rem; }
.prev-more .prose { font-size: var(--fs-sm); color: var(--ink-soft); }

.prev-note {
  margin: 0 0 var(--sp-3); padding: .7rem .9rem;
  border-radius: var(--radius); background: var(--paper-alt);
  border-left: 3px solid var(--gold);
  font-size: var(--fs-sm); color: var(--ink-soft);
}
.prev-decide { border-top: 1px solid var(--hairline); padding-top: var(--sp-4); margin-top: var(--sp-3); }
.prev-decide__actions { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-top: var(--sp-3); }
@media (max-width: 575.98px) {
  .prev-decide__actions { flex-direction: column; align-items: stretch; }
  .prev-decide__actions .btn { width: 100%; }
}
.prev-empty {
  display: flex; align-items: center; gap: var(--sp-3);
  color: var(--ink-soft);
}
.prev-empty .ico { --ico-size: 1.3rem; color: #059669; flex: none; }

.tag--ok { --tag-bg: #ECFDF5; --tag-fg: #047857; --tag-bd: rgba(5, 150, 105, .3); }
.tag--no { --tag-bg: var(--gold-soft); --tag-fg: #92400E; --tag-bd: rgba(217, 119, 6, .3); }
[data-surface="signal"] .tag--ok { --tag-bg: rgba(52, 211, 153, .14); --tag-fg: #34D399; --tag-bd: rgba(52, 211, 153, .35); }
[data-surface="signal"] .tag--no { --tag-bg: rgba(251, 191, 36, .14); --tag-fg: #FBBF24; --tag-bd: rgba(251, 191, 36, .35); }

/* ================================================ back to top (footer) ====
   Sits in the footer's bottom bar rather than floating over the page, so it
   never covers content and cannot collide with the mobile navigation.
   ========================================================================== */
.to-top {
  display: inline-flex;
  align-items: center;
  gap: .45em;
  padding: .45rem .9rem;
  border-radius: var(--radius-pill);
  border: 1px solid rgba(255, 255, 255, .18);
  background: rgba(255, 255, 255, .04);
  color: rgba(255, 255, 255, .78);
  font-family: var(--body);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out),
              color var(--dur) var(--ease-out),
              transform var(--dur) var(--ease-spring);
}
.to-top:hover,
.to-top:focus-visible {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  text-decoration: none;
  transform: translateY(-2px);
}
.to-top:focus-visible { outline: none; box-shadow: 0 0 0 4px rgba(37, 99, 235, .3); }
.to-top .ico { --ico-size: .95rem; }
.to-top:hover .ico { animation: to-top-nudge .5s var(--ease-out); }

@keyframes to-top-nudge {
  0%   { transform: translateY(0); }
  45%  { transform: translateY(-3px); }
  100% { transform: translateY(0); }
}

@media (max-width: 575.98px) {
  /* Three items on one line is too many at this width. The copyright takes
     the row, and the motto and the button share the next one. */
  .footer-bottom .container { justify-content: center; text-align: center; }
  .footer-bottom__motto { flex: 1 1 100%; }
  /* 40px, matching every other touch target on the site. */
  .to-top { min-height: 40px; padding-inline: 1.1rem; }
}
@media (prefers-reduced-motion: reduce) {
  .to-top, .to-top .ico { transition: none; }
  .to-top:hover { transform: none; }
  .to-top:hover .ico { animation: none; }
}

/* ================================ homepage: affiliations & partners =======
   These read as a logo stranded on one line with the name orphaned beneath it,
   inside a 121px card that was mostly empty. The cause was specificity, not
   layout: `a.card-plain { display: block }` (0,1,1) beats
   `.partner-card { display: flex }` (0,1,0) whatever the source order, and a
   partner card is an <a> whenever it has a website — so the intended
   horizontal layout never applied to a single one of them.

   Rebuilt as a compact horizontal card: mark on the left, name and country
   stacked beside it, everything on one baseline grid. Selectors here are
   specific enough to hold against a.card-plain.
   ========================================================================== */
a.partner-card,
div.partner-card {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  /* Tall enough for a two-line name plus the country, so a long institution
     name does not make its row taller than the one below it. The name is
     clamped to two lines, so this is the ceiling as well as the floor. */
  min-height: 86px;
  height: 100%;
  padding: .8rem .95rem;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--raise);
  text-align: left;
  text-decoration: none;
  color: inherit;
  transition: transform var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out),
              box-shadow var(--dur) var(--ease-out);
}
a.partner-card:hover {
  transform: translateY(-2px);
  border-color: rgba(37, 99, 235, .32);
  box-shadow: var(--shadow-md);
  text-decoration: none;
}

/* The mark: a fixed square so a row of cards lines up regardless of whether a
   partner supplied a logo or falls back to initials. */
.partner-card .partner-logo {
  flex: none;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--paper-alt);
  border: 1px solid var(--hairline);
  padding: 0;
}
.partner-card .partner-logo img {
  width: 100%; height: 100%;
  object-fit: contain;
  padding: 5px;
  border-radius: 0;
  box-shadow: none;
  display: block;
}
.partner-card .partner-mark {
  display: grid; place-items: center;
  width: 100%; height: 100%;
  background: linear-gradient(150deg, #3B82F6, var(--accent-dark));
  color: #fff;
  font-family: var(--head);
  font-weight: 700;
  font-size: .82rem;
  letter-spacing: .01em;
  border: 0;
}

.partner-card .partner-meta {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: .12rem;
  min-width: 0;
}
.partner-card .partner-name {
  font-family: var(--head);
  font-weight: 700;
  font-size: var(--fs-sm);
  line-height: 1.3;
  letter-spacing: -.015em;
  color: var(--ink);
  /* Two lines is enough for the longest institution name in the set; a third
     would make one card taller than the rest of its row. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
a.partner-card:hover .partner-name { color: var(--accent-dark); }
.partner-card .partner-meta small {
  font-size: var(--fs-xs);
  line-height: 1.35;
  color: var(--ink-faint);
}
