/* =============================================================================
   Atlassian Spacing Tokens  —  8px base unit
   https://atlassian.design/foundations/spacing
   ============================================================================= */
:root {
  --space-025:   2px;
  --space-050:   4px;
  --space-075:   6px;
  --space-100:   8px;
  --space-150:  12px;
  --space-200:  16px;
  --space-250:  20px;
  --space-300:  24px;
  --space-400:  32px;
  --space-500:  40px;
  --space-600:  48px;
  --space-800:  64px;
  --space-1000: 80px;

  /* ── Figma colour tokens ── */
  --c-bg:         #ffffff;
  --c-dark:       #0d0d0c;          /* rgb(13,13,12)   */
  --c-light-bg:   #f8f7f4;          /* rgb(248,247,244)*/
  --c-accent:     #5533ee;          /* rgb(85,51,238)  */
  --c-red:        #f03d1f;          /* rgb(240,61,31)  */
  /* Deeper red-orange — the gradient's closing stop, promoted to a token
     of its own. Link text on a white/light ground uses this: --c-red
     only reaches ~3.9:1 against white, this clears 4.5:1. --c-red stays
     the brand accent for fills, borders, icons, tags and labels. */
  --c-red-dark:   #df3611;
  --c-gray:       #888680;          /* rgb(136,134,128)*/
  --c-white:      #ffffff;

  /* Paragraph+Background gradient (orbital overlay)
     stop 0 = #5533ee, stop 81% = #df3611 (rgb 223,54,17)
     angle ≈ 150 deg based on Figma handle positions              */
  --c-gradient: linear-gradient(150deg, #5533ee 0%, var(--c-red-dark) 81%);

  /* ── Figma typography tokens ── */
  /* Two families, no third — and the split is by what the text IS, not by
     heading level. --f-serif: page titles, and titles of works or names of
     people (a post title, a paper title, a person's name), plus pull-quote
     copy. --f-sans: section headings, card and UI labels, running prose,
     metadata, controls and uppercase micro-labels.
     The one that trips people up: a SECTION HEADING IS NOT SERIF. Serif
     marks a thing that has its own name; sans marks the furniture around
     it. See README -> Typography. */
  --f-serif: 'EB Garamond', Georgia, 'Times New Roman', serif;
  --f-sans:  'DM Sans', system-ui, -apple-system, sans-serif;
  /* -- Type scale (sitewide -- see README) --------------------------------
     Fluid: each clamp() interpolates between the mobile floor and the
     desktop ceiling, so tablet needs no separate breakpoint override.
     12px is the hard floor; nothing on the site ships smaller.         */
  --fs-display:     clamp(36px, 22.7px + 3.70vw, 76px);
  --fs-h1:          clamp(36px, 29.3px + 1.85vw, 56px);
  --fs-h2:          clamp(26px, 21.3px + 1.30vw, 40px);
  --fs-h3:          clamp(20px, 18.7px + 0.37vw, 24px);
  --fs-lede:        clamp(18px, 17.3px + 0.19vw, 20px);
  --fs-body:        16px;
  --fs-small:       14px;
  --fs-small-serif: 15px;   /* EB Garamond runs ~1px small beside DM Sans */
  --fs-micro:       12px;   /* uppercase labels, tags, meta -- the floor */

  --lh-display: 1.05;
  --lh-heading: 1.15;
  --lh-lede:    1.26;
  --lh-title:   1.3;
  --lh-body:    1.55;

  /* ── Page layout ── */
  --max-w: 1440px;
  --pad-x: var(--space-1000);   /* 80px side padding — page-level rhythm */
}

/* Side padding scales down on smaller screens — same breakpoints as
   /home, /grants, /events and /team-leadership. */
@media (max-width: 900px) {
  :root { --pad-x: var(--space-400); }  /* 32px */
}
@media (max-width: 480px) {
  :root { --pad-x: var(--space-250); }  /* 20px */
}

/* =============================================================================
   External Link Arrow Badge
   Black circle + white arrow that signals "opens an external site." Used
   on school/partner/center logo cards; host element needs position:
   relative. On hover, the badge switches to a continuously sliding
   purple-to-orange gradient (the brand gradient's own colors) rather
   than a flat color swap — one consistent motion effect across every
   section instead of a different static hover color per section.
   ============================================================================= */
.card-arrow {
  position: absolute;
  top: var(--space-100);
  right: var(--space-100);
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--c-dark);
  color: var(--c-white);
  font-size: var(--fs-micro);
  line-height: 1;
  transition: transform 0.2s ease;
  pointer-events: none;
}
@keyframes card-arrow-slide {
  0%   { background-position: 0% 50%; }
  100% { background-position: 200% 50%; }
}
a:hover .card-arrow {
  transform: scale(1.15) translate(2px, -2px);
  background: linear-gradient(100deg, var(--c-accent), var(--c-red), var(--c-accent));
  background-size: 220% 100%;
  animation: card-arrow-slide 1.2s linear infinite;
}

/* =============================================================================
   Reset
   ============================================================================= */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
/* overflow-anchor: none avoids a browser "scroll anchoring" bug where the
   page silently scrolls itself down a bit after load — the page's top
   heading sits right at the very top with only 20px of padding above it,
   and when the web fonts swap in a beat after first paint, the heading's
   height shifts slightly. The browser tries to nudge the scroll position
   in the viewport to compensate, which crops the heading. Scrolling up
   manually reveals it fine — this just stops the auto-scroll from
   happening in the first place. (Found and fixed first in the
   `team-leadership` repo; backported here per "keeping the repos in
   sync.") Set on both html and body since scroll anchoring can anchor to
   either. */
html { scroll-behavior: smooth; overflow-anchor: none; }
body {
  font-family: var(--f-sans);
  color: var(--c-dark);
  background: var(--c-bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
  overflow-anchor: none;
}
ul, ol { list-style: none; }
a { color: inherit; text-decoration: none; }
img { max-width: 100%; height: auto; display: block; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0;
  margin: -1px; overflow: hidden; clip: rect(0,0,0,0);
  white-space: nowrap; border-width: 0;
}

/* =============================================================================
   Shared inner container
   ============================================================================= */
.mission__inner,
.related-centers__inner,
.orbital__inner,
.partners__inner {
  width: 100%;
  max-width: var(--max-w);
  margin-inline: auto;
  padding-inline: var(--pad-x);
}

/* =============================================================================
   Scroll-triggered reveal
   Progressive enhancement: the opacity/transform rule only applies once
   the page's own script has confirmed it can run (js-reveal-ready on
   <html>), so a page with JS disabled or blocked never gets stuck with
   invisible content — everything just renders normally instead.

   Uses the standalone `translate` property (not `transform`) for the
   entrance slide, on purpose: the school/center/partner tiles' hover lift
   uses `transform: translateY(-4px)` with its own fast 0.15s transition
   (see e.g. .school-block__link:hover below) — and `transform` was also
   being used here for the entrance. Since a `transition` shorthand
   replaces the whole list per property, the more specific reveal rule was
   silently overriding transform's transition to this reveal's slower
   duration even after the reveal had long finished, making hover lifts
   feel sluggish instead of crisp. `translate` and `transform` are
   independent, separately-transitionable properties, so splitting them
   here removes that conflict entirely. Also switched off the generic
   `ease` curve to a smoother ease-out (cubic-bezier), and dropped the
   added `scale()` and reduced the travel distance — the combination of a
   long 56px slide, a scale change, and a slow linear-ish ease was reading
   as heavy/sluggish rather than smooth. (Found and fixed first in the
   `team-leadership` repo — see that repo's styles.css/README for the
   same writeup — then backported here per "keeping the repos in sync.")
   ============================================================================= */
.js-reveal-ready .reveal {
  opacity: 0;
  translate: 0 24px;
  transition: opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1), translate 0.7s cubic-bezier(0.16, 1, 0.3, 1);
}
.js-reveal-ready .reveal.is-visible {
  opacity: 1;
  translate: none;
}

/* Grid tiles and stat items get their own staggered reveal, cued off
   their parent section's .is-visible class — each item fades/lifts in a
   beat after the one before it instead of the whole grid appearing as a
   single flat block, so the reveal reads as more deliberate/noticeable. */
.js-reveal-ready .school-block,
.js-reveal-ready .center-block,
.js-reveal-ready .partner-card {
  opacity: 0;
  translate: 0 20px;
  transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), translate 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
.reveal.is-visible .school-block,
.reveal.is-visible .center-block,
.reveal.is-visible .partner-card {
  opacity: 1;
  translate: none;
}
.school-block:nth-child(1),  .center-block:nth-child(1),  .partner-card:nth-child(1)  { transition-delay: 0.05s; }
.school-block:nth-child(2),  .center-block:nth-child(2),  .partner-card:nth-child(2)  { transition-delay: 0.11s; }
.school-block:nth-child(3),  .center-block:nth-child(3),  .partner-card:nth-child(3)  { transition-delay: 0.17s; }
.school-block:nth-child(4),  .center-block:nth-child(4)  { transition-delay: 0.23s; }
.school-block:nth-child(5),  .center-block:nth-child(5)  { transition-delay: 0.29s; }
.school-block:nth-child(6),  .center-block:nth-child(6)  { transition-delay: 0.35s; }
.school-block:nth-child(7),  .center-block:nth-child(7)  { transition-delay: 0.41s; }
.school-block:nth-child(8),  .center-block:nth-child(8)  { transition-delay: 0.47s; }
.center-block:nth-child(9)  { transition-delay: 0.53s; }
.center-block:nth-child(10) { transition-delay: 0.59s; }
.center-block:nth-child(11) { transition-delay: 0.65s; }

/* =============================================================================
   MISSION STATEMENT  (chapter)
   Figma: 1434×580, fills transparent (opacity=0) → inherits white from parent
   ============================================================================= */
.mission {
  position: relative;
  background: var(--c-bg);
  /* Top of page: 80px from the site menu to the page title, the same on
     every page (--space-1000, the documented page-level rhythm). */
  padding-top: var(--space-1000);
  padding-bottom: var(--space-1000); /* 80px — page-level spacing, matches Orbital/Partners/Related Centers */
}


/* Tighter, symmetric padding for the standalone GNI/Jameson quote
   sections — the standard 20/80 split (tuned for Mission Statement's
   heading + prose) left way too much empty space around a single short
   quote. */
.mission--tight {
  padding-top: var(--space-600);
  padding-bottom: var(--space-600);
}

.mission__inner {
  display: flex;
  flex-direction: column;
  gap: 0;
}

/* Body copy + quote box side by side, quote box to the right */
.mission__row {
  display: flex;
  align-items: center;     /* vertically centers the quote box against the body copy */
  gap: var(--space-600);   /* 48px between body copy and quote box */
  flex-wrap: wrap;         /* stacks on narrow viewports */
}

/* ch-title: EB Garamond SemiBold 48px, fill rgb(85,51,238), ls=0, lh=50.4 */
.mission__title {
  font-family: var(--f-serif);
  font-size: var(--fs-h1);
  font-weight: 600;
  line-height: var(--lh-display);
  letter-spacing: 0;
  color: var(--c-accent);
  margin-bottom: var(--space-250);  /* 20px below heading */
}

/* p: DM Sans Light 20px, fill rgb(13,13,12), ls=0, lh=25.2 */
.mission__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-300);           /* 24px between paragraphs — rhythm */
  flex: 1 1 0;                     /* grows evenly against the quote box below, rather than consuming all leftover space next to a fixed-width sibling */
}

.mission__body p {
  font-family: var(--f-sans);
  font-size: var(--fs-lede);
  font-weight: 300;
  line-height: var(--lh-lede);
  letter-spacing: 0;
  color: var(--c-dark);
}

/* First (bold) paragraph — runs the section's full original width, above
   the row, rather than sharing row space with the quote box. Only the
   second (unbolded) paragraph sits in .mission__row and vertically
   centers against the quote. */
.mission__lead {
  font-family: var(--f-sans);
  font-size: var(--fs-lede);
  font-weight: 600;
  line-height: var(--lh-lede);
  letter-spacing: 0;
  color: var(--c-dark);
  margin-bottom: var(--space-300);   /* 24px — same rhythm as the old .mission__body gap */
}

/* anno-quote-text: EB Garamond Regular 24px, fill rgb(13,13,12), lh=23.25 */
.mission__quote {
  max-width: 1153px;
  margin-top: var(--space-800);  /* 64px above quote */
}

.mission__quote blockquote {
  position: relative;
  padding-left: var(--space-400);
  border-left: 2px solid var(--c-red);
  margin-bottom: var(--space-200);
}

.mission__quote blockquote p {
  position: relative;
  z-index: 1;
}

.mission__quote blockquote p {
  font-family: var(--f-serif);
  font-size: var(--fs-h3);
  font-weight: 400;
  line-height: var(--lh-display);
  letter-spacing: 0;
  color: var(--c-dark);
}

/* anno-quote-attr: EB Garamond Medium 18px, fill rgb(136,134,128), ls=1.2, lh=21.6
   Serif to match the quote text above it, not the sans body copy elsewhere. */
.mission__quote figcaption {
  font-family: var(--f-serif);
  font-size: var(--fs-lede);
  font-weight: 500;
  line-height: var(--lh-lede);
  letter-spacing: 1.2px;           /* Figma ls=1.2 */
  color: var(--c-gray);
  padding-left: var(--space-400);
}

/* ─── Quote box variant ───
   No background card — just the red accent bar as a left border, with
   smaller/italic text. Used two ways: beside the Mission Statement body
   copy (inside .mission__row, a row-direction flex context — the width
   there is set via `flex` so it holds a fixed size next to the text),
   and standalone/centered for the GNI and Jameson quotes (inside a plain
   .mission__inner--center column, where `width` + auto margins center
   it, since flex-basis in a column container would size height instead
   of width). */
/* Card treatment matches the blog repo's `.pull-quote` component
   (https://pennmediated.github.io/blog/#gni-membership): a tinted
   background + left accent bar wrapping both the quote and its
   attribution as one card, rather than just a bare left border on the
   blockquote alone. Applied here on the `<figure>` itself so it wraps
   both the `<blockquote>` and the `<figcaption>` below it in one box. */
.mission__quote--box {
  max-width: none;
  width: 400px;
  margin-top: 0;
  margin-inline: auto;   /* centers it when it's the sole item in a column context */
  background: var(--c-light-bg);
  border-left: 4px solid var(--c-red);
  padding: var(--space-250) var(--space-300);
}

.mission__row .mission__quote--box {
  flex: 1 1 0;            /* grows evenly against .mission__body, so the two sides split the row roughly 50/50 instead of the quote staying a fixed 400px against a much wider paragraph */
  margin-inline: 0;      /* positioned by the row layout instead, not centered */
}

/* GNI/Jameson (standalone, centered quotes) get their own fixed width,
   independent of the Gutmann quote's 1120px breakpoint below — otherwise
   they'd shrink from a wide centered box down to the narrow 400px
   side-by-side size once the screen crosses 1120px, which only makes
   sense for the Gutmann quote's row layout. */
.mission__inner--center .mission__quote--box {
  width: clamp(min(280px, 100%), 75%, 860px);   /* matches the Gutmann quote's stacked width exactly */
  max-width: 860px;
}

.mission__quote--box blockquote {
  position: relative;
  padding-left: 0;    /* cancels .mission__quote blockquote's own accent-bar padding — the card's own border-left (on the figure) is the only accent bar now */
  border-left: none;  /* cancels .mission__quote blockquote's own border — avoids a doubled line, since both classes apply to the same element */
  margin-bottom: var(--space-150);
}

.mission__quote--box blockquote p {
  position: relative;
  z-index: 1;
}

.mission__quote--box blockquote p {
  font-size: var(--fs-h3);
  line-height: var(--lh-body);
}

.mission__quote--box figcaption {
  font-family: var(--f-serif);
  font-size: var(--fs-small);
  font-weight: 600;
  font-style: normal;
  letter-spacing: 0;
  padding-left: 0;   /* cancels .mission__quote figcaption's own padding — the card's own padding (on the figure) already insets it */
}

/* Below 1120px the body copy and the quote box stop sharing a line and
   stack instead.

   This used to be done by widening the quote's flex-basis to 75% and
   trusting `flex-wrap: wrap` to push it onto its own line. It didn't
   wrap: the row is a single flex line until its contents hit min-content,
   which doesn't happen until ~760px. So between 1120px and ~900px the
   quote sat beside a .mission__body that had been starved down to about
   140px — a one-or-two-word-per-line sliver of text — because the body's
   `flex: 1 1 0` gives it a zero basis while the quote asked for 75%.

   Switching the row to `flex-direction: column` makes the stack explicit
   rather than emergent, so the body always gets the full width. Note the
   quote is sized with `width`, not `flex-basis`: in a column container
   flex-basis sizes the main axis, which is now height. */
@media (max-width: 1120px) {
  .mission__row {
    flex-direction: column;
    align-items: stretch;   /* body copy runs full width; the quote's own
                               width + auto margins centre it below */
  }

  .mission__quote--box {
    width: clamp(min(280px, 100%), 75%, 860px);
    max-width: 860px;
    margin-inline: auto;   /* centers the box on its own line */
  }
  /* Same specificity as the row's own ".mission__row .mission__quote--box
     { margin-inline: 0; }" rule above, so this (later in the cascade)
     wins — otherwise that more-specific rule keeps pinning the box left. */
  .mission__row .mission__quote--box {
    flex: 0 0 auto;
    margin-inline: auto;
  }
}

/* Centers content within a standalone quote section (GNI/Jameson), where
   .mission__inner has no adjacent body copy to sit beside — both
   horizontally (align-items) and vertically (justify-content, using the
   min-height below to give it room to center within). */
.mission__inner--center {
  align-items: center;
  justify-content: center;
  min-height: 80px;
}

/* =============================================================================
   RELATED CENTERS AT PENN  (Section)
   Solid red-orange band background — full section, including the card
   grid, matching the Orbital/Partners color-block treatment.
   ============================================================================= */
.related-centers {
  background: var(--c-red);
  padding-block: var(--space-1000);  /* 80px */
  margin-bottom: var(--space-1000);
}

.related-centers__inner {
  display: flex;
  flex-direction: column;
  gap: var(--space-300);             /* 24px heading -> lede, matches /grants' rhythm */
}

.related-centers__title {
  font-family: var(--f-sans);
  font-size: var(--fs-h2);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: var(--lh-heading);
  color: var(--c-white);
}

.related-centers__lede {
  font-family: var(--f-sans);
  font-size: var(--fs-lede);
  font-weight: 300;
  line-height: var(--lh-lede);
  color: var(--c-white);
  margin-bottom: var(--space-300);   /* 24px before the grid */
}

/* 4:3 tile, sized off its own width (aspect-ratio) — same reasoning as
   .school-block__link above. Since height tracks width rather than a
   fixed px value, --tight's smaller padding no longer needs to be offset
   by anything to keep it the same height as its neighbors; it's already
   guaranteed by sharing the same grid column width. */
/* =============================================================================
   INTERDISCIPLINARY HUB  (orbital)
   Figma: 1440×652, fills IMAGE ref=29d9b9807cc2 (FILL scale)
   Child Paragraph+Background: GRADIENT_LINEAR 150deg #5533ee→#df3611
   ============================================================================= */
.orbital {
  position: relative;
  overflow: hidden;
  padding-block: var(--space-1000);  /* 80px */
  background-image: url(assets/orbital-bg.png);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

/* Paragraph+Background gradient overlay — fully opaque, covers background photo */
.orbital::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--c-gradient);
  pointer-events: none;
  z-index: 0;
}

.orbital__inner {
  position: relative;
  z-index: 1;
}

/* orbital-title: DM Sans Medium 48px, fill white, lh=50.4 */
.orbital__title {
  font-family: var(--f-sans);
  font-size: var(--fs-h2);
  font-weight: 600;
  line-height: var(--lh-heading);
  letter-spacing: 0;
  color: var(--c-white);
  margin-bottom: var(--space-300);
}

.orbital__title em {
  font-style: normal;   /* the <em> stays for semantic emphasis, but reads upright, not italic */
}

/* orbital-body: DM Sans Light 18px, fill white, lh=22.75 */
.orbital__lead {
  font-family: var(--f-sans);
  font-size: var(--fs-lede);
  font-weight: 300;
  line-height: var(--lh-lede);
  letter-spacing: 0;
  color: var(--c-white);
  margin-bottom: var(--space-500);   /* 40px before list */
}

/* =============================================================================
   LOGO TILE SYSTEM
   One grid, one tile, one logo cap — shared by the school grid (Orbital), the
   related-centres grid and the external-partners cards. These three sections
   are the same component; keeping them as three near-identical rule sets is
   what let their logo caps drift to 128/128/152px and let the partners' flex
   row behave differently from the other two. A new logo section is markup
   only: .logo-grid > .logo-tile > .logo-tile__img.
   ============================================================================= */
.logo-grid {
  display: grid;
  /* Intrinsic column count: the track floor decides how many fit, so there
     are no breakpoints to keep in sync and adding tiles needs no CSS change.
     min(260px, 100%) rather than a bare 260px — a bare minmax floor cannot
     shrink below itself and overflows viewports narrower than the track.
     auto-fill, not auto-fit: empty trailing tracks are what keep a 2-tile
     section the same tile size as an 8-tile one instead of stretching. */
  grid-template-columns: repeat(auto-fill, minmax(min(260px, 100%), 1fr));
  gap: var(--space-200);
}

/* 4:3 tile, sized off its own column width rather than a fixed px height, so
   every tile in a section matches its neighbours automatically at every width
   without a single height value to maintain. */
.logo-tile {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 4 / 3;
  padding: var(--space-300) var(--space-200);
  background: var(--c-white);
  border: 1px solid rgba(13, 13, 12, 0.08);
  text-decoration: none;
  transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s;
}

.logo-tile:hover {
  box-shadow: 0 12px 24px rgba(13, 13, 12, 0.16);
  transform: translateY(-4px);
}

/* The sizing is on the WIDTH axis on purpose: 213px is 160 x 4/3, so for a 4:3
   asset it resolves the height to 160 without depending on a percentage height.
   aspect-ratio yields to content, and a percentage max-height goes indefinite
   in some containers, so a height-side value lets a logo force its own tile
   taller than 4/3. A width-side value resolves against the track's definite
   width everywhere.

   width, not max-width: a cap can only shrink. Every asset here is wider than
   213px intrinsically, so a cap happened to size them all identically and hid
   that — until hci-logo.svg, a 71px-wide export, which a cap left at 71px in a
   260px tile while its neighbours filled theirs. Setting width makes the tile
   width govern in both directions, so a small export scales up to match.
   max-height stays as the guard for a portrait logo: when it binds, the
   replaced-element sizing rules recompute the width from the ratio, so the
   logo shrinks proportionally rather than distorting. */
.logo-tile__img {
  max-height: 100%;
  width: min(100%, 213px);   /* 213 = 160 x 4/3 */
  height: auto;
}

/* Phone sizing for every logo section at once (schools, partners,
   centres all share this component).

   600px is not an arbitrary breakpoint: it is exactly where the track
   floor above stops fitting two columns (2 x 260px + 16px gap = 536px of
   content, plus the 32px side padding either side), so below it every
   tile went full-bleed — 350px wide and, at 4/3, 263px tall to hold a
   logo capped at 213 x 160. That left more empty tile than logo and made
   the three sections ~7,000px of scrolling on a 390px phone.

   Two fixed columns rather than a smaller auto-fill floor: below this
   width the intrinsic count only ever resolves to 1 or 2, so stating 2
   is both what actually happens and easier to reason about. The shorter
   3/2 ratio and tighter padding then take the remaining slack out of the
   box, so the logo — not the white space around it — is what scales. */
@media (max-width: 600px) {
  .logo-grid {
    /* minmax(0, 1fr), not bare 1fr: a bare 1fr track can't shrink below a
       logo's natural width, and Safari counts that width, so wide logos
       pushed the second column off the right edge of a phone screen. */
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-150);   /* 12px */
  }

  .logo-tile {
    aspect-ratio: 3 / 2;
    /* Horizontal padding stays at 16px: it is what keeps a full-width
       logo clear of the external-link badge in the top-right corner.
       Only the vertical padding comes down. */
    padding: var(--space-150) var(--space-200);
  }

  /* The 213px cap is inert at this size (100% of the tile is well under
     it), so the logo is free to use the full width the tile now gives it. */
  .logo-tile__img {
    /* Centred absolutely inside the tile so its caps resolve against the
       tile's own 3:2 box. In flow, Safari ignored max-height: 100% here and
       let 4:3 logos stretch their tile taller than its row neighbours. */
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    max-width: calc(100% - 2 * var(--space-200));
    max-height: calc(100% - 2 * var(--space-150));
  }

  /* Badge scales with the smaller tile — at 26px it read as oversized
     against a 111px-tall card and crowded the wider logos. */
  .card-arrow {
    top: var(--space-075);     /* 6px */
    right: var(--space-075);
    width: 22px;
    height: 22px;
  }
}

/* School logo grid — white crest tiles floating on the gradient, same
   card pattern as the /grants repo's eligible-schools grid. */
.school-grid { margin-bottom: var(--space-400); }   /* 32px — this section only */

/* 4:3 tile, sized off its own width (aspect-ratio) rather than a fixed
   px height — stays 4:3 at every grid breakpoint (4/2/1 columns) since
   height always tracks whatever the column width happens to be, and
   every tile in the section shares that same column width, so they stay
   the same height as each other automatically too. */
/* =============================================================================
   EXTERNAL PARTNERS
   Purple block, white text — cards stay white so the partner logos still
   read clearly against the purple background.
   ============================================================================= */
.partners {
  background: var(--c-accent);
  padding-block: var(--space-1000);
}

.partners__inner {
  display: flex;
  flex-direction: column;
  gap: var(--space-300);             /* 24px heading -> lede, matches /grants' heading-to-body rhythm */
}

/* Sans-serif heading, semibold rather than the heaviest weight — matches
   /grants' .rfp__title family/scale but pulled back from 700 to 600. */
.partners__title {
  font-family: var(--f-sans);
  font-size: var(--fs-h2);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: var(--lh-heading);
  color: var(--c-white);
}

.partners__lede {
  font-family: var(--f-sans);
  font-size: var(--fs-lede);
  font-weight: 300;
  line-height: var(--lh-lede);
  color: var(--c-white);
  margin-bottom: var(--space-300);   /* 24px before the cards */
  /* No max-width: fills the full padded width so left/right space stays
     equal, same fix as /grants' body-copy blocks. */
}

.partners__panel {
  background: transparent;
}

/* A plain grid item now. The tile width comes from the shared track floor,
   so these come out the same size as the school and centre tiles without
   any width arithmetic to update when a partner is added. Block, not a
   flex column: a column flex container sizes itself from its item, which
   let the tile inside drift off 4/3 by a few pixels at some widths. */
.partner-card {
  display: block;
}


/* ── Page end ──────────────────────────────────────────────────────────
   Sitewide (Sept 2026): the last section sits flush on the global footer,
   which opens with its own 132px white signal band. That band is the gap
   before the footer on every page. The old 80px trailing space on top of it
   left 200px+ of white at the end of some pages. */
body { padding-bottom: 0; }
.related-centers { margin-bottom: 0; }

/* ── Trailing-margin guard ─────────────────────────────────────────────────
   These pages are embedded in an iframe that is resized to the height this
   page reports, and that height is measured from document.body.

   A bottom margin on the last element COLLAPSES THROUGH body and out to
   <html>. It never appears in body.scrollHeight, but it does extend
   documentElement.scrollHeight — so the frame gets sized short of the real
   document, the embedded page is left with a few px it can scroll, and it
   swallows the visitor's wheel events: the outer page then only scrolls when
   the cursor is over its scrollbar. It has bitten twice (home, 64px;
   grants-overview, 80px) and it is invisible in the builder.

   display: flow-root makes body a block formatting context, so descendant
   margins can no longer escape it and the measured height is always the whole
   document. Cheap, and it removes a whole class of bug rather than one
   instance. If a page grows here, something was relying on a margin escaping
   — convert that margin to padding rather than removing this.
   ====================================================================== */
body { display: flow-root; }
