/* ==========================================================================
   SWAMP LAUNCH — DESIGN SYSTEM
   Palette: deep space black + UF blue/orange + a single swamp-green glow
   accent used only for the vine/circuit motif. Everything else stays cold,
   quiet, and mostly black — the "product" (the rocket, the team, the work)
   does the talking. This file is the whole design system: change the
   variables in :root and the entire site updates.
   ========================================================================== */

:root {
  /* ---- Color: EDIT HERE to retheme the whole site ---- */
  --black: #000000;
  --bg-elevated: #0a0d10;      /* slightly-raised panels/cards */
  --bg-elevated-2: #10141a;
  --uf-blue: #0021A5;
  --uf-blue-light: #3454e0;
  --uf-orange: #d33901;
  --uf-orange-light: #ff6a2e;  /* hover tint, derived from the base above */
  --swamp-green: #39ff8a;      /* used ONLY for the vine motif + tiny glows */
  --chrome: #c7cdd6;
  --text: #f4f6f8;
  --text-dim: #8992a3;
  --text-faint: #545c6b;
  --line: rgba(255, 255, 255, 0.08);

  /* ---- Type ---- */
  --font-display: "Orbitron", "Eurostile", "Arial Narrow", sans-serif;
  --font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-hero: "Nasalization", "Orbitron", "Eurostile", sans-serif; /* SWAMP / LAUNCH wordmark only */

  /* ---- Rhythm ---- */
  --section-pad: clamp(4rem, 10vw, 9rem);
  --edge-pad: clamp(1.5rem, 6vw, 6rem);
  --radius: 14px;

  /* set + read by script.js — do not hand-edit */
  --launch-progress: 0; /* 0→1 across the whole hero scroll track */
  --arrive-p: 0;         /* 0→1: rocket traveling from its low start up to the words */
  --local-p: 0;          /* 0→1: everything AFTER arrival — the "default" sequence below */
  --split-p: 0;         /* 0→1: SWAMP/LAUNCH pushing apart, rocket lifting off */
  --grow-p: 0;           /* 0→1: thrust plume growing large */
  --overtake-p: 0;       /* 0→1: text fades away / clouds roll in, late in the scroll */
  --blackout-p: 0;       /* 0→1: quick final cut to black, right before the vine section */
  --rumble-x: 0px;       /* set by script.js: tiny shake on the non-wordmark hero text while scrolling */
  --rumble-y: 0px;
}

/* ---- Reset ---- */
* { box-sizing: border-box; margin: 0; padding: 0; }
/* Visually hidden but still announced by screen readers -- used for
   headings that exist purely to label a dialog/section for assistive
   tech (aria-labelledby) where a visible title would be redundant. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
html { scroll-behavior: smooth; background: var(--black); }
body {
  background: var(--black);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.6;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  /* This reads as a designed interface, not a document -- clicking or
     drag-selecting body copy shouldn't flip the cursor to an I-beam and
     highlight text the way it would on a plain page. Form fields opt
     back in below, since typed input still needs to be selectable. */
  -webkit-user-select: none;
  user-select: none;
}
img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
ul { list-style: none; }
button { font: inherit; cursor: pointer; }
input, textarea {
  -webkit-user-select: text;
  user-select: text;
}

::selection { background: var(--uf-orange); color: var(--black); }

/* ==========================================================================
   CRT / GLITCH ATMOSPHERE
   A site-wide neon wash plus faint scanlines, with occasional flickers so
   the whole page reads as slightly unstable rather than flat. Both live on
   body pseudo-elements, so every page picks them up with no markup
   changes.

   Everything here is pointer-events:none and sits above the content but
   below the nav's z-index:100 stacking, so nothing becomes unclickable.
   Only opacity and transform animate — no filters — to keep it off the
   main thread and away from the jank that blur animations caused
   earlier in the hero.
   ========================================================================== */
body::before,
body::after {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 90;
}
/* Neon wash: a barely-there green/blue tint that drifts across the page,
   tying the whole thing to the swamp-green accent. */
body::before {
  background:
    /* Center pulled down to 14% (was 0%, right on the top edge) --
       at 0% the gradient's brightest point sat exactly along the very
       top strip of the viewport, which on an iPhone 14 Pro is where the
       Dynamic Island/camera cutout lives, so that strip read as a
       visible green fill instead of black on load. Pulling the peak
       down keeps the same wash lower on the page while leaving the very
       top edge on the gradient's faded tail. */
    radial-gradient(120% 80% at 12% 14%, rgba(57, 255, 138, .07), transparent 60%),
    radial-gradient(100% 70% at 88% 100%, rgba(0, 33, 165, .10), transparent 62%);
  mix-blend-mode: screen;
  animation: neon-drift 14s ease-in-out infinite alternate;
}
@keyframes neon-drift {
  from { opacity: .55; transform: translate3d(-1%, 0, 0); }
  to   { opacity: 1;   transform: translate3d(1%, -1%, 0); }
}
/* Scanlines + the glitch flicker itself. The lines are a 3px repeating
   gradient, kept very low-alpha; the animation nudges them and blinks
   the layer's opacity at irregular keyframe positions so the
   interference reads as erratic instead of a steady pulse. */
body::after {
  background: repeating-linear-gradient(
    to bottom,
    rgba(255, 255, 255, .012) 0px,
    rgba(255, 255, 255, .012) 1px,
    transparent 1px,
    transparent 4px
  );
  animation: glitch-flicker 7s steps(1, end) infinite;
}
@keyframes glitch-flicker {
  0%, 100%  { opacity: .5;  transform: translate3d(0, 0, 0); }
  8%        { opacity: .9;  transform: translate3d(-1px, 0, 0); }
  9%        { opacity: .35; transform: translate3d(1px, 0, 0); }
  10%       { opacity: .6;  transform: translate3d(0, 0, 0); }
  46%       { opacity: .55; transform: translate3d(0, 0, 0); }
  47%       { opacity: 1;   transform: translate3d(2px, 0, 0); }
  48%       { opacity: .4;  transform: translate3d(-1px, 0, 0); }
  49%       { opacity: .6;  transform: translate3d(0, 0, 0); }
  81%       { opacity: .85; transform: translate3d(-2px, 0, 0); }
  82%       { opacity: .5;  transform: translate3d(0, 0, 0); }
}

/* Chromatic aberration on the display type — a green ghost to one side
   and an orange one to the other, offset by a single pixel, so headings
   look very slightly mis-registered like a bad signal. The wordmark gets
   its own stronger treatment further down. */
.section-title,
.org-name,
.hero-eyebrow,
.hero-milestone {
  text-shadow:
    -1px 0 rgba(57, 255, 138, .28),
     1px 0 rgba(250, 70, 22, .22);
}

/* ---- Starfield backdrop (space is black, not empty) ----
   The stars are split across three layers (the element plus its two
   pseudo-elements), each holding a different handful of them and
   twinkling on its own period and phase. A single layer could only ever
   pulse every star in lockstep; with three drifting against each other,
   some stars brighten while others dim at any given moment. Periods are
   intentionally non-multiples (7s / 9s / 11s) so the groups never fall
   back into sync, and the opacity swings are small — this should read
   as barely-there shimmer, not blinking. */
.starfield {
  position: fixed;
  /* Oversized on purpose: the parallax drift below shifts these layers
     up as you scroll, and at inset:0 that would expose a bare strip
     along the bottom edge. Also extended horizontally — the dots are
     tiled at fixed offsets inside each tile, so wherever the layer's
     edge falls mid-tile it can cut off just before that tile's dot,
     leaving an empty column down the side of the screen. Pushing the
     edges well outside the viewport moves that partial tile out of
     sight.

     vw/vh, NOT percent: percentage inset on a position:fixed element
     that extends past the viewport edge is a known trigger for iOS
     Safari to expand the LAYOUT viewport (window.innerWidth) to fit it,
     rather than just letting it overflow past the edge as intended —
     which is what made the whole page load "pre-zoomed out" requiring
     a pinch to see it at actual size. vw/vh don't trigger that. */
  inset: -30vh -15vw;
  z-index: 0;
  pointer-events: none;
  /* Each gradient is TILED (a px background-size) rather than stretched
     across the whole viewport at 100% 100%. The old version drew exactly
     one dot per gradient — nine stars total, floating in a lot of empty
     black — which is what read as low-res. Tiling repeats each dot into
     a dense field instead.
     The dots also fade to transparent over their full radius rather than
     stopping hard at `#fff 100%, transparent 0`; that hard stop produced
     an aliased, chunky little square, while a soft falloff gives a
     round, anti-aliased point at any pixel density. Sizes and alphas
     vary per gradient so the field has some natural depth to it. */
  background-image:
    radial-gradient(circle 1.1px at 34px 22px, rgba(255,255,255,.95), transparent),
    radial-gradient(circle .8px at 118px 86px, rgba(255,255,255,.7), transparent),
    radial-gradient(circle 1px at 72px 148px, rgba(206,226,255,.8), transparent),
    radial-gradient(circle .7px at 176px 42px, rgba(255,255,255,.6), transparent),
    radial-gradient(circle .9px at 14px 196px, rgba(255,255,255,.75), transparent),
    radial-gradient(circle .85px at 8px 64px, rgba(255,255,255,.7), transparent),
    radial-gradient(circle 1px at 96px 12px, rgba(232,240,255,.8), transparent);
  background-size: 190px 190px, 240px 240px, 210px 210px, 280px 280px,
                   260px 260px, 150px 150px, 170px 170px;
  opacity: .5;
  /* Parallax, slowest layer — these are the "farthest" stars, so they
     barely move. --scroll-y comes from script.js; the pseudo-element
     layers below add their own drift ON TOP of this one (they're
     children, so transforms compound), making each successive group
     travel faster and read as nearer. */
  transform: translateY(calc(var(--scroll-y, 0px) * -.04));
  will-change: transform;
  /* Two animations: a one-shot fade up from pure black on first load,
     then the ongoing twinkle. Both target opacity; the later-listed
     twinkle wins once it starts, which is exactly when the fade ends.
     Run over 2s (longer than the hero content's 1.1s) on a gentle
     ease-out so the sky drifts in rather than snapping to full
     brightness — and the twinkles below are delayed to match, so the
     handoff lands mid-fade instead of interrupting it. */
  animation:
    starfield-in 2s cubic-bezier(.25, .5, .3, 1) both,
    twinkle-a 16s ease-in-out 2s infinite backwards;
}
.starfield::before,
.starfield::after {
  content: "";
  position: absolute;
  inset: 0;
}
/* Mid-depth layer: a little extra drift on top of the parent's. */
.starfield::before {
  background-image:
    radial-gradient(circle 1.3px at 96px 54px, rgba(255,255,255,.95), transparent),
    radial-gradient(circle .9px at 28px 132px, rgba(226,238,255,.75), transparent),
    radial-gradient(circle 1px at 168px 108px, rgba(255,255,255,.8), transparent),
    radial-gradient(circle .8px at 132px 208px, rgba(255,246,230,.7), transparent),
    radial-gradient(circle 1.1px at 58px 246px, rgba(255,255,255,.85), transparent),
    radial-gradient(circle .9px at 12px 34px, rgba(255,255,255,.7), transparent);
  background-size: 230px 230px, 300px 300px, 265px 265px, 320px 320px,
                   285px 285px, 195px 195px;
  transform: translateY(calc(var(--scroll-y, 0px) * -.05));
  /* "backwards" so each layer already sits at its twinkle's 0% value
     while the parent is still fading up from black — without it, these
     jumped straight from their default full opacity to that starting
     value the instant the twinkle kicked in, which popped. */
  animation: twinkle-b 21s ease-in-out 2s infinite backwards;
}
/* Nearest layer: the most additional drift, so these stars visibly
   outrun the two behind them — and the largest/brightest dots, since
   nearer stars read bigger. */
.starfield::after {
  background-image:
    radial-gradient(circle 1.6px at 62px 88px, rgba(255,255,255,1), transparent),
    radial-gradient(circle 1.2px at 214px 36px, rgba(214,232,255,.9), transparent),
    radial-gradient(circle 1.4px at 122px 262px, rgba(255,255,255,.95), transparent),
    radial-gradient(circle 1px at 286px 172px, rgba(255,250,238,.8), transparent),
    radial-gradient(circle 1.2px at 18px 120px, rgba(255,255,255,.9), transparent);
  background-size: 340px 340px, 390px 390px, 360px 360px, 420px 420px, 250px 250px;
  transform: translateY(calc(var(--scroll-y, 0px) * -.11));
  animation: twinkle-c 27s ease-in-out 2s infinite backwards;
}
@keyframes starfield-in {
  from { opacity: 0; }
  to   { opacity: .5; }
}
/* Toned right back down: one slow rise-and-fall per cycle instead of the
   previous two deep dips toward zero, which read as the whole sky
   blinking. The swings are now narrow enough to be a gentle shimmer, and
   no layer ever drops far enough to visibly go out. Group A rides the
   parent's own opacity so it hovers around the .5 the fade-in lands on;
   B and C are relative to that parent, hence their higher values. Their
   peaks stay staggered so the groups still drift against each other
   rather than breathing in unison. */
@keyframes twinkle-a {
  0%   { opacity: .5; }
  45%  { opacity: .38; }
  100% { opacity: .5; }
}
@keyframes twinkle-b {
  0%   { opacity: .88; }
  35%  { opacity: 1; }
  70%  { opacity: .74; }
  100% { opacity: .88; }
}
@keyframes twinkle-c {
  0%   { opacity: .95; }
  55%  { opacity: .78; }
  100% { opacity: .95; }
}

/* ==========================================================================
   NAV
   ========================================================================== */
.nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1.1rem var(--edge-pad);
  /* Directional scrim instead of a flat panel — solid-ish right at the
     top where it sits over the hero, fading to fully transparent by the
     bottom of the bar, so it reads as a soft gradient wash rather than a
     hard-edged bar (matching the reference site's nav treatment). */
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0));
  transition: background .4s ease, backdrop-filter .4s ease, opacity .35s ease;
}
.nav.is-scrolled {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
/* Toggled by script.js based on scroll direction: fades out once you've
   scrolled down a short way, fades back in the moment you scroll back
   up (or return near the top). pointer-events:none while hidden so the
   invisible links can't be clicked. */
.nav.nav-hidden {
  opacity: 0;
  pointer-events: none;
}
.nav-brand { display: flex; align-items: center; gap: .6rem; }
.nav-brand img { height: 30px; width: auto; }
.nav-brand span {
  font-family: var(--font-display);
  font-size: .8rem;
  letter-spacing: .12em;
  color: var(--text);
}
.nav-links {
  display: flex;
  gap: clamp(1rem, 2.5vw, 2.4rem);
  align-items: center;
}
.nav-links a {
  font-size: .82rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-dim);
  transition: color .25s ease;
  position: relative;
}
.nav-links a:hover { color: var(--text); }
.nav-links a::after {
  content: "";
  position: absolute;
  left: 0; bottom: -6px;
  width: 0; height: 1px;
  background: var(--uf-orange);
  transition: width .25s ease;
}
.nav-links a:hover::after { width: 100%; }
.nav-links a.is-active { color: var(--text); }
.nav-links a.is-active::after { width: 100%; background: var(--uf-orange); }

.btn-donate {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  transition: transform .25s cubic-bezier(.2,.7,.2,1), filter .3s ease;
  filter: drop-shadow(0 0 0 rgba(250, 70, 22, 0));
}
.btn-donate img {
  height: 34px;
  width: auto;
}
.btn-donate:hover {
  transform: rotate(-4deg) translateY(-1px);
  filter: drop-shadow(0 0 14px rgba(250, 70, 22, .6));
}

/* Base (desktop) state — hidden. The actual mobile behavior, including
   the .nav-menu override, lives in the aspect-ratio media query at the
   end of this file: it has to come after .nav-menu's own hover-dropdown
   rules (defined later, around the org chart section) to win the
   cascade, since a media query doesn't add specificity by itself, only
   source order does. */
.nav-toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 34px;
  height: 34px;
  padding: 0;
  background: none;
  border: none;
  flex-shrink: 0;
  z-index: 101;
  cursor: pointer;
}
.nav-toggle span {
  display: block;
  width: 100%;
  height: 2px;
  background: var(--text);
  border-radius: 1px;
  transition: transform .3s cubic-bezier(.2,.7,.2,1), opacity .2s ease;
}
.nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* ==========================================================================
   HERO — "scroll to launch"
   A tall scroll track with a sticky viewport inside it. As the user scrolls
   through the track, --launch-progress goes 0 → 1 (set by script.js) and
   every transform below reads that single variable. To change how far/fast
   the rocket flies, change --launch-track height.
   ========================================================================== */
.hero-track {
  position: relative;
  height: 176vh; /* scroll distance for the whole launch sequence, before the vine section (20% shorter) */
}
.hero-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
}

.hero-glow {
  position: absolute;
  bottom: -10%;
  left: 50%;
  width: 60vw;
  height: 60vw;
  transform: translateX(-50%);
  background: radial-gradient(circle, rgba(0,33,165,.35), transparent 65%);
  filter: blur(10px);
  z-index: 1;
}

/* ==========================================================================
   PAGE MATERIALIZE — the whole above-the-fold view resolves out of a blur
   in under half a second on first load. Runs on page load (no scroll or
   JS trigger needed), unlike the .reveal-up observer used further down
   the page.

   Deliberately animates ONLY opacity and filter, never transform: the
   hero's wordmark, rocket rig and copy all have live transforms driven
   by --launch-progress / --rumble-*, and an animation on transform would
   override those outright for its duration and fight the scroll
   sequence.

   Applied to the rocket's WRAPPER (.rocket-rig), not to .rocket-img /
   .thrust-img themselves — those carry drop-shadow filters, and running
   a filter animation directly on them would wipe those shadows out.

   Animating filter:blur() was the source of the jagginess in an earlier
   pass — a blur has to be recomputed from scratch every single frame,
   and on the full-size wordmark plus a viewport-wide particle layer
   that can't hold 60fps. Opacity is compositor-only, so the fade here
   is driven purely by opacity (with will-change hinting it onto its own
   layer up front) and runs longer to stay soft. */
@keyframes materialize-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.nav,
.hero-copy,
.hero-title-row,
.rocket-rig,
.hero-sub,
.hero-milestone,
.scroll-cue {
  /* fill-mode is "backwards", NOT "both": backwards applies the from
     state during the animation's delay, then hands the property back to
     normal CSS once the animation finishes. "both" would keep the
     animation's own end state (opacity:1) applied forever, and since
     animations outrank regular declarations, that permanently overrode
     the scroll-driven `opacity: calc(1 - overtake-p ...)` fades on the
     hero text — the copy simply never faded out on scroll. */
  animation: materialize-in 1.1s cubic-bezier(.33, 0, .2, 1) backwards;
  will-change: opacity;
}
/* Gentle stagger so it reads as settling into place rather than one
   flat cross-fade. */
.hero-title-row { animation-delay: .1s; }
.rocket-rig     { animation-delay: .18s; }
.hero-sub,
.hero-milestone { animation-delay: .26s; }
.scroll-cue     { animation-delay: .34s; }

/* The particle cloud itself: a scatter of tiny dots over the hero that
   flashes in and dissolves almost immediately, so the content reads as
   condensing out of it. pointer-events:none keeps it from ever
   intercepting a click on the nav or scroll cue underneath. */
.hero-stage::before {
  content: "";
  position: absolute;
  inset: -10%;
  z-index: 6;
  pointer-events: none;
  background-image:
    radial-gradient(1.5px 1.5px at 12% 22%, rgba(57,255,138,.9) 100%, transparent 0),
    radial-gradient(1.5px 1.5px at 78% 14%, rgba(255,255,255,.85) 100%, transparent 0),
    radial-gradient(2px 2px at 34% 68%, rgba(57,255,138,.7) 100%, transparent 0),
    radial-gradient(1.5px 1.5px at 63% 47%, rgba(255,255,255,.8) 100%, transparent 0),
    radial-gradient(1.5px 1.5px at 21% 84%, rgba(255,255,255,.7) 100%, transparent 0),
    radial-gradient(2px 2px at 88% 72%, rgba(57,255,138,.75) 100%, transparent 0),
    radial-gradient(1.5px 1.5px at 47% 8%, rgba(255,255,255,.75) 100%, transparent 0),
    radial-gradient(1.5px 1.5px at 55% 90%, rgba(57,255,138,.8) 100%, transparent 0);
  background-size: 240px 240px, 300px 300px, 360px 360px, 280px 280px,
                   320px 320px, 260px 260px, 340px 340px, 300px 300px;
  /* Blur is a STATIC value here, not animated — a fixed filter is
     rasterized once, whereas animating it would re-blur this
     viewport-sized layer every frame. Only opacity and transform (both
     compositor-only) actually animate. */
  filter: blur(2px);
  will-change: opacity, transform;
  animation: particle-dissolve 1.3s cubic-bezier(.33, 0, .2, 1) both;
}
@keyframes particle-dissolve {
  from { opacity: .85; transform: scale(1.05); }
  to   { opacity: 0;   transform: scale(1); }
}

/* ---- Final beat: a quick cut to black, handing off to the next section ---- */
.hero-overtake {
  position: absolute;
  inset: 0;
  z-index: 5;
  background: var(--black);
  opacity: var(--blackout-p);
  pointer-events: none;
}

.hero-copy {
  position: relative;
  z-index: 4;
  text-align: center;
  opacity: calc(1 - (var(--overtake-p) * 1.3));
  transform: translate(var(--rumble-x), var(--rumble-y));
  transition: transform 0.1s linear;
}
.hero-eyebrow {
  font-size: .78rem;
  letter-spacing: .35em;
  text-transform: uppercase;
  color: var(--uf-orange);
  margin-bottom: 1.1rem;
  display: block;
}

/* ---- SWAMP [rocket] LAUNCH ----
   SWAMP/LAUNCH sit close together at rest now (the rocket lives lower down,
   not between them) — they only get pushed apart once the rocket arrives. */
.hero-title-row {
  position: relative;
  z-index: 4;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(1.5rem, 5vw, 4rem);
  padding: 0 clamp(1rem, 4vw, 3rem);
}
.hero-word {
  font-family: var(--font-hero);
  font-weight: 400;
  /* Capped by BOTH vw and vh, not vw alone — .hero-stage is a fixed
     100vh box with overflow:hidden, so a short, wide window (vw scaling
     only) could size the wordmark taller than the box has room for and
     get clipped. min() keeps it scaled to whichever dimension is
     actually tighter. */
  font-size: clamp(3.4rem, min(15vw, 18vh), 12rem);
  line-height: .85;
  letter-spacing: .01em;
  white-space: nowrap;
  background: linear-gradient(180deg, #ffffff 0%, var(--chrome) 55%, var(--uf-blue-light) 145%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  opacity: calc(1 - (var(--overtake-p) * 1.3));
  /* Neon bloom around the wordmark. A STATIC filter (rasterized once)
     rather than an animated one — and drop-shadow instead of
     text-shadow, since the glyphs are a clipped background with
     transparent fill, which text-shadow would ghost behind rather than
     hug. */
  filter:
    drop-shadow(0 0 22px rgba(57, 255, 138, .20))
    drop-shadow(0 0 4px rgba(0, 33, 165, .35));
  will-change: transform;
  transition: transform 0.5s cubic-bezier(.16,.7,.24,1), opacity 0.6s ease-out;
}
/* Early on, --split-p pulls them apart a little (the rocket parting them).
   Once --overtake-p kicks in (same window as the text fade), they get an
   additional big push so they clear the screen exactly as the fade finishes. */
.word-swamp { transform: translateX(calc((var(--split-p) * -10vw) + (var(--overtake-p) * -70vw))); }
.word-launch { transform: translateX(calc((var(--split-p) * 10vw) + (var(--overtake-p) * 70vw))); }

/* Container is sized to match the rocket's own aspect ratio (tall/narrow) so
   its natural height stays in proportion to the text next to it, instead of
   the image overflowing far above the row.

   It now lives directly in .hero-stage (not inside the title row) so it can
   start low — down between "Launched into the unknown." and "Scroll to
   launch" — and travel UP to meet the words. --arrive-p (0→1 over the first
   slice of the scroll) drives that trip; once it hits 1, the rocket is
   docked at its old centered spot and everything else (--split-p, --grow-p,
   --overtake-p, all now driven by --local-p) plays out exactly as before. */
.rocket-rig {
  position: absolute;
  left: 50%;
  top: 50%;
  transform:
    translate(-50%, -50%)
    translateY(calc((1 - var(--arrive-p)) * 22vh));
  z-index: 4;
  width: clamp(30px, 4vw, 64px);
  height: clamp(110px, 14vw, 210px);
  overflow: visible;
}
.rocket-rig img {
  position: absolute;
  left: 50%;
  width: 100%;
  height: auto;
}
/* Rocket starts BELOW the text line at rest, then rises up through the gap
   between SWAMP/LAUNCH as you scroll — it's what parts them, along its own
   flight path. --rocket-drift is shared with the thrust below so the two
   stay physically attached at the same point as the rocket climbs. Driven
   by --local-p (post-arrival) rather than raw --launch-progress. */
:root { --rocket-drift: calc(5vh - (var(--local-p) * 27vh)); }
.rocket-img {
  bottom: 0;
  transform: translateX(-50%) translateY(var(--rocket-drift));
  /* Soft offset shadow to lift it off the flat background a bit, plus a
     touch of contrast so the silhouette reads with a little more depth. */
  filter: drop-shadow(1.5px 3px 4px rgba(0, 0, 0, .5)) contrast(1.08);
  z-index: 3;
  will-change: transform;
}
/* Thrust doesn't exist at rest (scaleY 0 = invisible). It stays anchored at
   the launch-pad point (--rocket-drift, NOT the rocket's own exit path) and
   grows ONLY on the Y axis (transform-origin: top) — so it shoots out longer
   and longer, but its width can never exceed the rocket's own width. No
   scaleX, ever: the source of the thrust cannot balloon past the aft.

   The scale has three terms, so the plume is stretching at every point
   in the scroll rather than only during two isolated beats:
     --arrive-p  gates it in as the rocket climbs (first 20% of scroll),
                 taking it to 3x so it extends right away.
     --local-p   a steady, continuous term across the ENTIRE
                 post-arrival track. This is the one that matters — with
                 only the two staged values, the plume sat frozen in the
                 gap before --grow-p opens (local-p .35) and again after
                 it closes (local-p .75), which is the stall you'd see
                 mid-scroll.
     --grow-p    still layers the big expansion on top through the
                 middle beat. */
.thrust-img {
  top: 100%;
  width: 95%; /* matches the rocket's aft/body width, not the flared fins, plus a touch wider */
  transform-origin: 50% 0%;
  transform: translateX(-50%) translateY(var(--rocket-drift)) scaleY(calc(var(--arrive-p) * (3 + (var(--local-p) * 5) + (var(--grow-p) * 6))));
  /* Once it's grown in (near the very end of the local sequence), dissolve it
     away rather than cutting off abruptly right as you move into the vine section. */
  opacity: calc(1 - clamp(0, (var(--local-p) - 0.95) / 0.05, 1));
  filter: drop-shadow(0 0 2.5px rgba(250, 70, 22, .32)) drop-shadow(0 0 7px rgba(250, 70, 22, .2));
  z-index: 2;
  will-change: transform, opacity;
  transition: opacity 0.6s ease-out;
}

.hero-sub {
  position: relative;
  z-index: 4;
  margin-top: 1.4rem;
  color: var(--text-dim);
  font-family: var(--font-display);
  font-size: clamp(.95rem, 1.6vw, 1.15rem);
  max-width: 34ch;
  margin-inline: auto;
  text-align: center;
  opacity: calc(1 - (var(--overtake-p) * 1.3));
  transform: translate(var(--rumble-x), var(--rumble-y));
  transition: transform 0.1s linear;
}
.hero-milestone {
  position: relative;
  z-index: 4;
  display: block;
  margin-top: 1.6rem;
  font-family: var(--font-display);
  font-size: .78rem;
  letter-spacing: .68em;
  text-transform: uppercase;
  color: var(--chrome);
  text-align: center;
  /* letter-spacing adds its gap AFTER every character, including the
     last — so the trailing space sits inside the centered box and pushes
     the four glyphs left of true center, which is why the plume (dead
     center of the viewport) didn't land in the R|E gap. The nudge here
     compensates for that (half the letter-spacing would be .34em) plus a
     little extra to the right. */
  transform: translate(var(--rumble-x), var(--rumble-y)) translateX(.46em);
  transition: transform 0.1s linear;
  opacity: calc(var(--split-p) * (1 - var(--overtake-p)));
}

.scroll-cue {
  position: absolute;
  bottom: 2.6rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .5rem;
  font-size: .72rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--text-faint);
  opacity: calc(1 - (var(--launch-progress) * 4));
}
.scroll-cue .chevron {
  width: 14px; height: 14px;
  border-right: 1px solid var(--text-faint);
  border-bottom: 1px solid var(--text-faint);
  transform: rotate(45deg);
  animation: bob 1.8s ease-in-out infinite;
}
@keyframes bob {
  0%, 100% { transform: rotate(45deg) translate(0,0); }
  50% { transform: rotate(45deg) translate(4px, 4px); }
}

/* ==========================================================================
   VINE / CIRCUIT DIVIDER — the one organic swamp motif, used sparingly.
   Draws itself in (stroke-dashoffset) when scrolled into view.
   ========================================================================== */
.vine-divider {
  position: relative;
  width: 100%;
  padding: 2.5rem 0;
  display: flex;
  justify-content: center;
  overflow: hidden;
}
.vine-divider svg {
  width: min(1100px, 90%);
  height: auto;
  overflow: visible;
}
.vine-divider path {
  fill: none;
  stroke: var(--swamp-green);
  stroke-width: 1.4;
  stroke-linecap: round;
  filter: drop-shadow(0 0 6px rgba(57,255,138,.55));
  stroke-dasharray: 1400;
  stroke-dashoffset: 1400;
  transition: stroke-dashoffset 1.8s cubic-bezier(.2,.7,.2,1);
}
.vine-divider circle {
  fill: var(--swamp-green);
  opacity: 0;
  filter: drop-shadow(0 0 5px rgba(57,255,138,.8));
  transition: opacity .6s ease .9s;
}
.vine-divider.in-view path { stroke-dashoffset: 0; }
.vine-divider.in-view circle { opacity: .85; }

/* ==========================================================================
   MILESTONE TIMELINE — "the vine" branches out as it draws itself in to
   reveal the year's milestones (Sacred-Timeline style). Trunk draws first;
   each branch + its node + its card follow, staggered so they appear roughly
   as the trunk "reaches" that point. All gated on .in-view (same
   IntersectionObserver as the vine dividers, via the shared .reveal-up hook).
   ========================================================================== */
.milestone-timeline {
  padding-top: 1rem;
}
.milestone-timeline .section-inner {
  text-align: center;
  padding-bottom: 1rem;
}
.milestone-timeline .section-title { margin-bottom: 0; }

/* Spacing above/below the SVG lives in MARGIN, not padding — the
   milestone cards are positioned with percentages measured against this
   element's padding box, and the SVG's own node coordinates (as
   fractions of its 1200x320 viewBox) assume that box is exactly the
   SVG's rendered size. Padding here would get folded into that
   percentage basis and push every card further from its node than the
   viewBox math intends; margin sits outside the box entirely, so it
   doesn't. */
.timeline-track {
  position: relative;
  width: min(1200px, 94%);
  margin: clamp(2rem, 6vw, 4rem) auto clamp(3rem, 8vw, 5rem);
}
.timeline-svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

.trunk-line,
.branch-line {
  fill: none;
  stroke: var(--swamp-green);
  stroke-width: 2;
  stroke-linecap: round;
  filter: drop-shadow(0 0 6px rgba(57, 255, 138, .55));
}
.trunk-line {
  stroke-dasharray: 2600;
  stroke-dashoffset: 2600;
  transition: stroke-dashoffset 3s cubic-bezier(.2, .7, .2, 1);
}
.branch-line {
  stroke-width: 1.6;
  stroke-dasharray: 260;
  stroke-dashoffset: 260;
  transition: stroke-dashoffset .9s cubic-bezier(.2, .7, .2, 1);
}
.branch-node {
  fill: var(--swamp-green);
  opacity: 0;
  transform-box: fill-box;
  transform-origin: center;
  transform: scale(0);
  filter: drop-shadow(0 0 6px rgba(57, 255, 138, .85));
  transition: opacity .53s ease, transform .53s cubic-bezier(.34, 1.56, .64, 1);
}

/* Timing is 1.5x the original pacing (gaps between each branch/node/card
   reveal roughly .6s apart instead of .4s) — slower than the original,
   but faster than the first pass, so each point still reads distinctly. */
.milestone-timeline.in-view .trunk-line { stroke-dashoffset: 0; }
.milestone-timeline.in-view .branch-1 { stroke-dashoffset: 0; transition-delay: .9s; }
.milestone-timeline.in-view .branch-2 { stroke-dashoffset: 0; transition-delay: 1.5s; }
.milestone-timeline.in-view .branch-3 { stroke-dashoffset: 0; transition-delay: 2.1s; }
.milestone-timeline.in-view .branch-4 { stroke-dashoffset: 0; transition-delay: 2.7s; }
.milestone-timeline.in-view .node-1 { opacity: .9; transform: scale(1); transition-delay: 1.58s; }
.milestone-timeline.in-view .node-2 { opacity: .9; transform: scale(1); transition-delay: 2.18s; }
.milestone-timeline.in-view .node-3 { opacity: .9; transform: scale(1); transition-delay: 2.78s; }
.milestone-timeline.in-view .node-4 { opacity: .9; transform: scale(1); transition-delay: 3.38s; }

.milestone-card {
  position: absolute;
  transform: translate(-50%, 0) translateY(10px);
  width: max-content;
  max-width: 32ch;
  opacity: 0;
  transition: opacity .75s ease, transform .75s cubic-bezier(.2, .7, .2, 1);
  pointer-events: none;
}
.milestone-card.is-above { transform: translate(-50%, -100%) translateY(4px); }
.milestone-card.is-below { transform: translate(-50%, 0) translateY(-4px); }
/* GBM #1's card doubles as a trigger for the recruit popup (see
   script.js) -- the only one of the four with [data-modal-open], so the
   pointer cursor only shows up where there's actually something to
   click. */
.milestone-card[data-modal-open] { cursor: pointer; }
.milestone-card[data-modal-open]:hover .milestone-title { color: var(--uf-orange); }
.milestone-timeline.in-view .milestone-card {
  opacity: 1;
  pointer-events: auto;
}
.milestone-timeline.in-view .milestone-card.is-above { transform: translate(-50%, -100%) translateY(-4px); }
.milestone-timeline.in-view .milestone-card.is-below { transform: translate(-50%, 0) translateY(4px); }
.milestone-timeline.in-view .card-1 { transition-delay: 1.65s; }
.milestone-timeline.in-view .card-2 { transition-delay: 2.25s; }
.milestone-timeline.in-view .card-3 { transition-delay: 2.85s; }
.milestone-timeline.in-view .card-4 { transition-delay: 3.45s; }

/* Category tag — which program a milestone belongs to (IREC, Development,
   etc.). Placed first, above the date, matching the eyebrow-before-heading
   pattern used everywhere else on the site rather than introducing a new
   convention. Small and dim so it reads as context, not competing with
   the date for attention. */
.milestone-tag {
  display: block;
  font-size: .6rem;
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--swamp-green);
  margin-bottom: .25rem;
}
.milestone-date {
  display: block;
  font-size: .68rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--uf-orange);
  margin-bottom: .3rem;
}
.milestone-title {
  font-family: var(--font-display);
  font-size: .92rem;
  font-weight: 700;
  color: var(--text);
  white-space: normal;
}

@media (max-width: 860px) {
  /* The percentage-positioned vine/branch layout can't reflow for narrow
     screens — the cards are placed by absolute x/y coordinates matched
     to specific SVG node points, so shrinking the width just crams them
     into each other. Below this width the vine is dropped entirely and
     the milestones become a plain, full-width vertical list in the same
     chronological order the cards already appear in the DOM.

     This used to trigger at 700px, but the cramped-overlap failure mode
     described above was still happening on tablet-width phones between
     700-860px — raised to match the site's other mobile breakpoints
     (the org chart above uses 860px for the same "absolute layout
     doesn't survive narrow" reason) so nothing in that gap still gets
     the vine layout. margin-top pulls the section up slightly, closer
     to the hero above it, now that it's a compact list instead of the
     taller vine graphic. */
  .timeline-svg { display: none; }
  .timeline-track {
    display: flex;
    flex-direction: column;
    gap: 1.75rem;
    margin: -1.5rem auto 0;
    padding: 1rem 0 0;
  }
  .milestone-card {
    position: static;
    max-width: none;
    width: 100%;
    text-align: left;
    opacity: 1;
    transform: none !important;
    pointer-events: auto;
    padding-left: 1rem;
    border-left: 2px solid var(--swamp-green);
  }
  .milestone-title { font-size: .95rem; }
}

/* ==========================================================================
   SECTION BASE
   ========================================================================== */
section { position: relative; z-index: 2; }
.section-inner {
  max-width: 1200px;
  margin-inline: auto;
  padding: var(--section-pad) var(--edge-pad);
}
.eyebrow {
  font-size: .74rem;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: var(--uf-orange);
  margin-bottom: 1rem;
  display: block;
}
h2.section-title {
  font-family: var(--font-display);
  font-size: clamp(1.8rem, 4vw, 3rem);
  font-weight: 700;
  letter-spacing: .02em;
  color: var(--text);
  margin-bottom: 1.2rem;
}
.section-lede {
  color: var(--text-dim);
  max-width: 60ch;
  font-size: 1.05rem;
}

/* ==========================================================================
   TEAM / MISSION — two-column split
   ========================================================================== */
.split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
.split-card {
  background: var(--bg-elevated);
  padding: clamp(2rem, 4vw, 3.5rem);
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  min-height: 420px;
  position: relative;
}
.split-card.has-image {
  justify-content: flex-end;
}
/* The photo now lives on ::before (layered under its own gradient) instead
   of as the element's own background-image. That makes it a self-contained
   layer we can blur/darken on hover without touching the text above it —
   set per-card via the --card-image custom property in the inline style. */
.split-card.has-image::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(
      180deg,
      rgba(0, 0, 0, 0) 0%,
      rgba(0, 0, 0, .18) 30%,
      rgba(0, 0, 0, .6) 55%,
      rgba(0, 0, 0, .88) 72%,
      rgba(0, 0, 0, .94) 100%
    ),
    var(--card-image, none);
  background-size: 100% 100%, cover;
  background-position: center, center;
  background-repeat: no-repeat, no-repeat;
  transition: filter .45s ease;
}
.split-card > * { position: relative; z-index: 1; }
/* Video tiles: the video sits behind the ::before gradient instead of
   above it. Slightly dimmed/desaturated so busy, bright footage doesn't
   fight with the text, and it darkens/blurs further on hover. */
.split-card-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
  filter: brightness(.72) saturate(.85);
  transition: filter .45s ease;
}
/* Hover: push the background further out of the way of the text as a
   stylistic flourish — darker and softly blurred, text stays crisp since
   it's a separate layer on top. */
.split-card:hover.has-image::before {
  filter: brightness(.6) blur(3px);
}
.split-card:hover .split-card-video {
  filter: brightness(.45) saturate(.7) blur(5px);
}
.split-card h3 {
  font-family: var(--font-display);
  font-size: 1.5rem;
  margin-bottom: .8rem;
  letter-spacing: .02em;
  text-shadow: 0 2px 10px rgba(0, 0, 0, .55);
}
.split-card p { color: var(--chrome); margin-bottom: 1.4rem; text-shadow: 0 1px 8px rgba(0, 0, 0, .6); }
/* The mission statement on the homepage tile is styled exactly like the
   Team tile's copy beside it — same face, size, and colour — so the two
   cards read as a matched pair. It only keeps text-wrap:balance, which
   evens out the line lengths of a three-clause sentence. */
.split-card-statement {
  text-wrap: balance;
}
.link-arrow {
  color: var(--text);
  font-size: 1.02rem;
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  border-bottom: 1.5px solid var(--uf-orange);
  padding-bottom: 5px;
  /* max-content instead of fit-content: Safari has a history of sizing
     fit-content wrong on a flex item's child (it was reading as roughly
     3x the actual text width in testing, stretching the underline way
     past the label), where max-content behaves predictably. */
  display: inline-block;
  width: max-content;
  transition: color .25s ease, border-color .25s ease, gap .25s ease;
}
.link-arrow:hover { color: var(--uf-orange-light); }

/* ==========================================================================
   FEATURED LAUNCH — Apple-style: one huge image, almost no text.
   Used twice on the homepage now, stacked back to back (IREC, then
   Development) instead of once with a single "latest launch" caption.
   ========================================================================== */
.feature-photo {
  position: relative;
  height: 100vh;
  min-height: 560px;
  display: flex;
  align-items: flex-end;
}
.feature-photo img,
.feature-photo video {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: center 30%;
}
/* Same staging footage used on the Development page, desaturated the
   same way there -- grayscale so it reads as archival/technical rather
   than competing in color with the rest of the homepage. */
.feature-video-grayscale { filter: grayscale(1) contrast(1.05); }
.feature-photo::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(0deg, rgba(0,0,0,.9) 0%, rgba(0,0,0,.15) 45%, rgba(0,0,0,.35) 100%);
}
.feature-caption {
  position: relative;
  z-index: 1;
  padding: var(--section-pad) var(--edge-pad);
}
.feature-caption .eyebrow { color: var(--chrome); }
.feature-caption h2 { margin-bottom: .6rem; }

/* ==========================================================================
   MERCH TEASER
   ========================================================================== */
.merch {
  background: var(--bg-elevated);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.merch .section-inner {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 2.5rem;
}
.merch-icon {
  width: 64px; height: 64px;
  border-radius: 50%;
  border: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center;
  color: var(--uf-orange);
}
.merch-icon svg { width: 28px; height: 28px; }
.merch-text h2 { margin-bottom: .4rem; }
.merch-text p { color: var(--text-dim); }
.btn-outline {
  /* background:none is only load-bearing now that this class is also
     used on a real <button> (the contact-form trigger) -- an <a> never
     had a UA background to begin with, but a <button> does. */
  background: none;
  border: 1px solid var(--chrome);
  color: var(--text);
  padding: .8rem 1.6rem;
  border-radius: 999px;
  font-size: .82rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  white-space: nowrap;
  transition: background .25s ease, color .25s ease, border-color .25s ease;
}
.btn-outline:hover {
  background: var(--text);
  color: var(--black);
  border-color: var(--text);
}

/* ==========================================================================
   CONTACT MODAL — popup triggered by the footer's "Contact form" button.
   Sends via mailto: (see script.js) rather than a hosted form backend,
   so it needs no server/account setup, but that also means it opens the
   visitor's own mail app instead of sending silently in the background.
   ========================================================================== */
.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--edge-pad);
  background: rgba(0, 0, 0, .7);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  opacity: 0;
  visibility: hidden;
  transition: opacity .25s ease, visibility 0s linear .25s;
}
.modal-overlay.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity .25s ease, visibility 0s linear 0s;
}
.modal {
  position: relative;
  width: 100%;
  max-width: 420px;
  background: var(--bg-elevated);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 2.2rem;
  transform: translateY(12px);
  transition: transform .25s cubic-bezier(.2,.7,.2,1);
}
.modal-overlay.is-open .modal { transform: translateY(0); }
.modal-close {
  position: absolute;
  top: 1rem;
  right: 1rem;
  width: 32px;
  height: 32px;
  border: none;
  background: none;
  color: var(--text-dim);
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
  transition: color .2s ease;
}
.modal-close:hover { color: var(--text); }
.modal h3 {
  font-family: var(--font-display);
  font-size: 1.3rem;
  margin-bottom: 1.4rem;
}
.modal-form { display: flex; flex-direction: column; gap: 1.1rem; }
.modal-form label {
  display: flex;
  flex-direction: column;
  gap: .4rem;
  font-size: .78rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.modal-form input,
.modal-form textarea {
  background: var(--bg-elevated-2);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: .7rem .9rem;
  color: var(--text);
  font-size: .92rem;
  text-transform: none;
  letter-spacing: normal;
  font-family: inherit;
  resize: vertical;
}
.modal-form input:focus,
.modal-form textarea:focus { outline: none; border-color: var(--uf-orange); }
.modal-form button {
  margin-top: .4rem;
  align-self: flex-start;
}

/* ==========================================================================
   RECRUIT MODAL — the Supersonic Fall / GBM #1 popup. Shows itself once
   automatically before the event (see script.js), and can be re-opened
   anytime via the same generic [data-modal-open] mechanism the contact
   modal above uses. Poster-first: the image carries the actual event
   details (date/time/room), so the card around it stays minimal.
   ========================================================================== */
.modal-recruit {
  /* Sized well past the other modals on purpose -- this is a poster,
     meant to actually be readable, not a form. Capped by vh, not a
     fixed pixel max-height: the poster below is constrained the same
     way (max-height, width:auto), so together they settle on whatever
     size fills the viewport without ever needing the page (or the
     modal) to scroll, on any screen -- a phone's shorter vh just
     produces a smaller popup automatically, the same rule handles both. */
  width: auto;
  max-width: min(94vw, 640px);
  max-height: 92vh;
  padding: 1.4rem 1.4rem 1.8rem;
  text-align: center;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.recruit-poster {
  display: block;
  width: auto;
  max-width: 100%;
  max-height: 74vh;
  border-radius: calc(var(--radius) - 4px);
  margin-bottom: 1.1rem;
}
.recruit-note {
  /* Explicitly Inter, not inherited var(--font-display) some modal
     headings use -- this is body copy under a poster, not a title. */
  font-family: var(--font-body);
  color: var(--chrome);
  font-size: 1.05rem;
  line-height: 1.5;
  max-width: 42ch;
}

/* ==========================================================================
   SPONSORS — atmosphere-layer tiers
   ========================================================================== */
.atmosphere {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 1px;
  margin-top: 3rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
.atmosphere-tier {
  background: var(--bg-elevated);
  padding: 2rem 1.2rem;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  min-height: 220px;
  position: relative;
  transition: background .3s ease;
}
.atmosphere-tier:hover { background: var(--bg-elevated-2); }
.atmosphere-tier .tier-name {
  font-family: var(--font-display);
  font-size: .82rem;
  letter-spacing: .06em;
  color: var(--text);
  margin-bottom: .3rem;
}
.atmosphere-tier .tier-alt {
  font-size: .72rem;
  color: var(--text-faint);
}
/* ascending glow bar behind each tier — taller = higher tier */
.atmosphere-tier::before {
  content: "";
  position: absolute;
  left: 1.2rem; right: 1.2rem; bottom: 0;
  height: var(--h, 20%);
  background: linear-gradient(0deg, var(--uf-blue), transparent);
  opacity: .35;
  z-index: 0;
}
.atmosphere-tier:nth-child(1)::before { --h: 16%; }
.atmosphere-tier:nth-child(2)::before { --h: 30%; }
.atmosphere-tier:nth-child(3)::before { --h: 46%; }
.atmosphere-tier:nth-child(4)::before { --h: 64%; }
.atmosphere-tier:nth-child(5)::before { --h: 84%; background: linear-gradient(0deg, var(--uf-orange), transparent); }
.atmosphere-tier > * { position: relative; z-index: 1; }

.sponsor-cta { margin-top: 2.5rem; display: flex; gap: 1rem; flex-wrap: wrap; }

/* ==========================================================================
   FOOTER
   ========================================================================== */
footer {
  border-top: 1px solid var(--line);
  padding: var(--section-pad) var(--edge-pad) 2.5rem;
}
.footer-grid {
  max-width: 1200px;
  margin-inline: auto;
  display: grid;
  grid-template-columns: 1.4fr 1fr 1fr 1.2fr;
  gap: 3rem;
}
/* A bare `fr` track's implicit minimum is its content's min-content
   width, not 0 — so a column holding the email input or the "Contact
   form" button couldn't shrink past whatever those need, and forced
   the whole grid (and with it, the real page width) wider than the
   viewport on narrow screens. This is what was making the ENTIRE site
   load "pre-zoomed out": one overflowing element anywhere is enough to
   make mobile browsers widen the whole layout viewport to fit it,
   rather than just clipping that one element. min-width:0 lets the
   track (and everything in it) actually shrink to the column's real
   available space. */
.footer-brand, .footer-col { min-width: 0; }
.footer-brand img { height: 34px; margin-bottom: 1rem; }
.footer-brand p { color: var(--text-dim); max-width: 32ch; font-size: .92rem; }
.footer-col h4 {
  font-size: .78rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: 1rem;
}
/* The brand mark in the first footer column. Without an explicit size it
   renders at the HQ artwork's natural dimensions — the global
   `img { max-width: 100% }` only stops it overflowing the column, which
   still leaves it far too large. */
.footer-col img {
  width: 64px;
  height: auto;
  margin-bottom: 1.1rem;
}
.footer-col a, .footer-col p {
  display: block;
  color: var(--text-dim);
  font-size: .92rem;
  margin-bottom: .6rem;
  transition: color .2s ease;
}
.footer-col a:hover { color: var(--text); }
/* Stacked (input above button) at every width, not just mobile: the
   footer column this sits in is only ~1fr of a 4-column grid (see
   .footer-grid), so it stays narrow even on a wide desktop window —
   narrow enough that the un-shrinkable "Subscribe to our newsletter"
   button squeezed the input to a sliver regardless of viewport size.
   A side-by-side pill was never going to fit this column at any width;
   stacking sidesteps the fight over space entirely. */
.newsletter-form {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  margin-top: .5rem;
}
.newsletter-form input {
  min-width: 0;
  background: transparent;
  border: none;
  padding: .75rem 1rem;
  color: var(--text);
  font-size: .88rem;
  text-align: center;
}
.newsletter-form input:focus { outline: none; }
.newsletter-form button {
  width: 100%;
  background: var(--uf-blue);
  color: var(--text);
  border: none;
  padding: .85rem 1.2rem;
  font-size: .78rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  /* The label is a full sentence now rather than one word, so it must not
     be allowed to wrap mid-phrase or squeeze the input to nothing. */
  white-space: nowrap;
  flex-shrink: 0;
}

/* ---- Contact ----
   Sits directly under the newsletter form in the same footer column. */
.footer-contact { margin-top: 1.8rem; }
.footer-contact a {
  color: var(--text-dim);
  font-size: .88rem;
  transition: color .2s ease;
}
.footer-contact a:hover { color: var(--uf-orange); }
/* The button overrides the plain-link colour above, and needs to be
   inline-block so it sizes to its text — .footer-col a sets display:block,
   which would otherwise stretch it across the whole column. */
.footer-form-btn {
  display: inline-block;
  margin-top: .9rem;
  padding: .55rem 1.1rem;
  font-size: .78rem;
  color: var(--text);
}
.footer-form-btn:hover { color: var(--black); }
.footer-bottom {
  max-width: 1200px;
  margin: 3rem auto 0;
  padding-top: 1.6rem;
  border-top: 1px solid var(--line);
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem;
  font-size: .8rem;
  color: var(--text-faint);
}
/* Real icon links instead of plain brand-name text. Kept dark/muted
   (--text-faint, same as the copyright line beside them) rather than
   full-color brand marks, so they sit quietly in the footer instead of
   competing with the rest of the page for attention; they brighten on
   hover as the only sign they're interactive. */
.footer-socials {
  display: flex;
  align-items: center;
  gap: 1rem;
}
.footer-socials a {
  color: var(--text-faint);
  transition: color .2s ease;
}
.footer-socials a:hover { color: var(--text); }
.footer-socials svg {
  width: 22px;
  height: 22px;
  display: block;
}

/* ==========================================================================
   TEAM PAGE — org chart
   Same "trickle down" idea as the milestone vine: a center trunk forks out
   tier by tier. Connector lines are plain swamp-green CSS lines (no SVG
   needed here since the shapes are just straight/right-angle branches),
   glowing the same way the vine does. Cards expand in place on click via
   team.js, which just toggles .is-expanded / .has-expanded classes —
   all the actual animation is CSS transitions below.
   ========================================================================== */
.team-hero {
  position: relative;
  z-index: 2;
  padding: calc(var(--section-pad) + 3.5rem) var(--edge-pad) 1rem;
}
.team-hero .section-inner { padding: 0; max-width: 900px; }
.team-hero-title {
  font-family: var(--font-display);
  font-size: clamp(2rem, 5.5vw, 3.6rem);
  line-height: 1.08;
  letter-spacing: .01em;
  margin-bottom: 1.3rem;
}
.team-hero-stat {
  margin-top: 1.1rem;
  font-size: .95rem;
  color: var(--text-dim);
  letter-spacing: .02em;
}
.team-hero-stat strong {
  font-family: var(--font-display);
  color: var(--swamp-green);
  font-size: 1.2em;
  margin-right: .35em;
}

.org-chart { padding-bottom: 2rem; }
.org-chart .section-inner {
  max-width: 1300px;
  padding-top: 1.5rem;
}
/* Everything — President down through the Lead Engineer grid down to Ops
   & Outreach — is one single centered column. */
.org-main {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
  width: 100%;
}

.org-tier {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: 1.1rem 1.4rem;
}
/* Ops & Outreach's 4 cards used to just be a flat flex row with no
   connecting line at all. Rebuilt as a 2x2 grid (matching the subteam
   lead-grids' own layout) so org-ops-spine below has two real rows to
   connect, with extra top/bottom breathing room from the tiers before
   and after it. */
.org-tier-ops {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: 1.6rem;
  row-gap: 2rem;
  justify-items: center;
  align-items: start;
  width: 100%;
  max-width: 340px;
  margin-inline: auto;
  padding-top: 3rem;
  margin-bottom: 2rem;
}
.org-tier-ops .org-card { width: 100%; max-width: 148px; z-index: 1; }
/* Same absolutely-positioned trick as .org-lead-spine — sized to the
   grid's own box so it doesn't take up a cell — but overflow:visible so
   the trunk's stub (drawn above y:0 in its viewBox) can actually poke up
   past the grid's top edge and reach the label above instead of getting
   clipped at the box's boundary. Deliberately its own class, NOT also
   .org-lead-spine: the subteam spines' own staged reveal is targeted by
   ".org-lead-spine path:nth-of-type(1/3/5/7)" further down, which (being
   3 classes + a pseudo-class + an element) is more specific than this
   file's own ".org-ops-spine .org-spine-trunk/-bar" delay rules — so if
   this SVG also carried .org-lead-spine, that rule would silently win
   and reset its trunk back to the subteam timing (4.8s) no matter what
   delay got set below. */
.org-ops-spine {
  position: absolute;
  top: 30px;
  left: 0;
  right: 0;
  bottom: 0;
  width: 100%;
  height: calc(100% - 30px);
  z-index: 0;
  pointer-events: none;
  overflow: visible;
}

/* Fork connectors between tiers: simple hand-drawn SVGs (a trunk splitting
   into 3 branches), not the Illustrator vine art. Uses default
   "xMidYMid meet" scaling (uniform, so stroke width never distorts) rather
   than stretching to fill — each sits in its own full-width row and
   "grows in" via stroke-dasharray, gated on .org-chart.in-view. */
.org-vine-wrap {
  position: relative;
  width: 100%;
  display: flex;
  justify-content: center;
  pointer-events: none;
}
.org-fork { display: block; width: 100%; height: auto; }

/* Stubs are drawn long on purpose (see viewBox in team.html) and a big
   negative bottom margin pulls the next tier up to swallow the extra
   length, so the line visually plugs into the middle of each card instead
   of stopping short with a gap above it. The tier's cards sit later in the
   DOM (both are position:relative with no z-index), so they paint on top
   and hide the overlapped portion of the line. */
.org-vine-exec { max-width: 549px; margin: -6px 0 -70px; z-index: 0; }
.org-vine-tripartite { max-width: 1300px; margin: -6px 0 -75px; z-index: 0; }

/* Same fix as org-tier-2 below: cancels most of org-vine-exec's -70px
   bottom margin so the branch tips land at the top of the VP/PM/
   Treasurer cards instead of buried deep inside them. */
.org-tier-1 { margin-top: 50px; }
/* One-directional stretch: Owen (2nd card) should match Eli's or Jacob's
   height when either of THEM expands, but Eli/Jacob should stay their
   own natural height when OWEN is the one who expands — not the
   symmetric "everyone matches the tallest" behavior .org-tier-2 uses.
   .org-tier-1 itself stays flex-start (from the shared .org-tier rule),
   so Eli/Jacob never stretch; align-self here overrides that just for
   Owen's own card, pulling only it to match whichever sibling is
   tallest. */
.org-tier-1 .org-card:nth-child(2) { align-self: stretch; }

/* Chiefs row spans the same full width as org-tier-3 below (no more
   760px cap), and each chief sits in its own flex:1 slot — identical
   column math to .org-lead-group — so Connell/Rodriguez/Taylor center
   exactly over Development/Vehicle/Payload's trees instead of just being
   spread evenly within a separately-sized box. */
.org-tier-2 {
  width: 100%;
  gap: 1.6rem;
  /* stretch, not flex-start — Development's title wraps to two lines
     while Vehicle's and Payload's don't, so Development is naturally
     the tallest card even before anything is clicked. Stretch pulls all
     three chief cards to match whichever is tallest (Development at
     rest, or whichever one is later expanded), so every card's bottom
     edge always lines up with where the vine below actually starts —
     no gap on load, and the same continuity holds once a click makes
     one card taller still. */
  align-items: stretch;
  /* Shifts the whole chiefs row down so the branch endpoints (y:110 of the
     fork's 120-tall viewBox) land right at the top of the cards instead of
     disappearing deep into them. org-vine-tripartite's margin-bottom is
     -75px; this cancels most of that so only ~20px of overlap remains
     (a small safety margin so the tips definitely reach the card rather
     than falling just short). */
  margin-top: 55px;
}
.org-chief-slot {
  display: flex;
  justify-content: center;
  /* stretch, matching .org-tier-2 above — the slot itself being taller
     does nothing visible unless the actual bordered card inside it
     (this slot's only child) also grows to fill it. */
  align-items: stretch;
  flex: 1 1 0;
  min-width: 0;
}
/* Connell and Taylor nudged in 40px from their column centers; Rodriguez
   (middle) stays put. Tripartite fork's outer branch endpoints are
   updated to match (208->248, 1092->1052) so the lines still land on
   the cards. */
.org-chief-slot:first-child { transform: translateX(40px); }
.org-chief-slot:last-child { transform: translateX(-40px); }

/* Connectors follow Structure EX.jpeg's overall layout but each branch is
   now drawn as ONE continuous curve from its shared origin straight to
   its destination card, instead of separate trunk/bus/stub segments
   meeting at hard right angles — that's what was reading as sharp 90°
   corners even after the segments themselves got wavy, since two
   perpendicular pieces meeting (or crossing) at a point still looks like
   a corner/cross regardless of each piece's own curve. A single branch
   curve has no such seam. Round caps/joins reinforce the soft, vine-like
   feel. Lead spines keep this same "branch from a shared point" structure
   (trunk segment -> fork into left/right/share branches -> next trunk
   segment), just repeated at each row. Waves stay modest under
   preserveAspectRatio="none" (lead spines) since that non-uniform stretch
   would otherwise visibly warp a bigger amplitude. */
.org-fork-path {
  fill: none;
  stroke: var(--swamp-green);
  stroke-width: 2.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 0 5px rgba(57, 255, 138, .55));
  stroke-dasharray: 55;
  stroke-dashoffset: 55;
  /* Linear, not eased — the old cubic-bezier(.2,.7,.2,1) front-loads all
     the motion early and visually "finishes" drawing well before the
     transition officially ends, so the tile at the end (which correctly
     waits for the real end) looked like it was lagging behind a vine
     that appeared done already. Linear keeps the line actually moving
     for the full duration, so the tile really does appear the instant
     the line visually arrives. */
  transition: stroke-dashoffset .7s linear;
}
/* Dasharray needs to sit CLOSE to each path's real length — too small and
   it redraws mid-line, but too large (the earlier bug: everything used
   160-500 regardless of actual length) means the reveal window doesn't
   reach the visible part of the path until late in the transition, so the
   stem looks like it just pops in near the end instead of visibly
   extending out of the trunk the whole time. Base 55 fits fork-sm's short
   stub (~42 units); everything longer gets its own tighter override
   below. */
.org-fork-branch-mid { stroke-dasharray: 135; stroke-dashoffset: 135; }
/* The long diagonal branches (exec fork's VP/Treasurer, tripartite fork's
   Connell/Taylor) are much longer than everything else on the page — and
   very different lengths from each other — so each gets its own fitted
   dasharray instead of sharing one oversized value. */
.org-vine-exec .org-fork-branch-long { stroke-dasharray: 260; stroke-dashoffset: 260; }
.org-vine-tripartite .org-fork-branch-long { stroke-dasharray: 500; stroke-dashoffset: 500; }
.org-fork-node {
  fill: var(--swamp-green);
  opacity: 0;
  transform-box: fill-box;
  transform-origin: center;
  transform: scale(0);
  filter: drop-shadow(0 0 6px rgba(57, 255, 138, .85));
  transition: opacity .2s ease, transform .2s cubic-bezier(.34, 1.56, .64, 1);
}
.org-chart.in-view .org-fork-path { stroke-dashoffset: 0; }
.org-chart.in-view .org-fork-node { opacity: .9; transform: scale(1); }

/* Full sequence, zero gaps, cards fade .25s (no traced outline step), run
   at 2x the original pace throughout:
   President forms (0s, done .25s) -> exec vine grows (.25s, done .95s) ->
   officers form (.95s, done 1.2s) -> tripartite vine grows (1.2s, done
   1.9s) -> chiefs form (1.9s, done 2.15s) -> stems into the lead-grids
   (2.15s, done 2.85s) -> subteam trunks grow all the way down (2.85s,
   done 4.65s) -> row 1 branches (4.65s, done 5.35s) + tiles (5.35s, done
   5.6s) -> row 2 branches (5.6s, done 6.3s) + tiles (6.3s, done 6.55s) ->
   row 3 branches (6.55s, done 7.25s) + tiles (7.25s, done 7.5s) -> Ops
   (7.5s, done 7.75s). Each stage's delay is the previous stage's delay
   plus its own transition-duration, so the next thing starts the instant
   the last thing finishes. */
.org-chart.in-view .org-vine-exec .org-fork-path { transition-delay: .25s; }
.org-chart.in-view .org-vine-exec .org-fork-node { transition-delay: .25s; }
.org-chart.in-view .org-vine-tripartite .org-fork-path { transition-delay: 1.2s; }
.org-chart.in-view .org-vine-tripartite .org-fork-node { transition-delay: 1.2s; }

.org-branch-label {
  width: 100%;
  text-align: center;
  font-family: var(--font-display);
  font-size: .74rem;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--swamp-green);
  opacity: 0;
  margin: 1.6rem 0 -.4rem;
  /* .45s, slower than the .25s the cards use — this heading eases in
     rather than snapping on. */
  transition: opacity .45s ease;
}
/* Starts at 4.1s and finishes at 4.55s — exactly when the ops spine's
   trunk below begins drawing — so the heading is fully there before its
   stem starts reaching down toward the tiles. */
.org-chart.in-view .org-branch-label { opacity: .8; transition-delay: 4.1s; }

/* Ops spine: trunk starts the instant the "Operations & Outreach"
   heading above it has finished fading in (4.55s), then the bars follow
   once the trunk itself is done (4.85s). */
.org-chart.in-view .org-ops-spine .org-spine-trunk { transition-delay: 4.55s; }
.org-chart.in-view .org-ops-spine .org-spine-bar { transition-delay: 4.85s; }

.org-card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  width: 168px;
  background: var(--bg-elevated);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1.1rem .9rem;
  color: var(--text);
  transition: transform .3s ease, border-color .3s ease, box-shadow .3s ease,
              flex-grow .45s cubic-bezier(.2,.7,.2,1), opacity .35s ease;
}
.org-card-sm { width: 148px; padding: .85rem .7rem; }
.org-card:hover {
  transform: translateY(-3px);
  border-color: rgba(57, 255, 138, .35);
  box-shadow: 0 8px 24px rgba(0, 0, 0, .4);
}

/* Card entrance: no more traced outline — the whole card (background,
   border, avatar, name, title) just fades in together, fast, the instant
   its stage's delay hits. The .org-card-outline SVG markup is still in
   the HTML but hidden via display:none so it has no visual effect.
   Each tier's delay starts the instant its own vine/spine segment
   finishes growing (see the tier-specific delays below), so cards still
   form in the wake of the line reaching them, just without a separate
   drawing step first. */
.org-chart .org-card {
  background: transparent;
  border-color: transparent;
  transition: transform .3s ease, border-color .25s ease, box-shadow .3s ease,
              background-color .25s ease, flex-grow .45s cubic-bezier(.2,.7,.2,1);
}
.org-chart.in-view .org-card {
  background: var(--bg-elevated);
  border-color: var(--line);
}
/* Academic Advisor tile — a plain div, not a button (see team.html), so
   it was never going to be clickable in the first place; this just
   also strips the hover lift/glow so it doesn't visually invite a click
   like the real interactive tiles do. team.js's card selector also
   explicitly excludes this class so it's skipped entirely, not just
   inert-looking. */
.org-card-static:hover {
  transform: none;
  border-color: var(--line);
  box-shadow: none;
}
.org-card-outline { display: none; }
.org-avatar, .org-name, .org-title {
  opacity: 0;
  transition: opacity .25s ease;
}
.org-chart.in-view .org-avatar,
.org-chart.in-view .org-name,
.org-chart.in-view .org-title {
  opacity: 1;
}

/* Full staged sequence, tightened so each stage's delay is exactly the
   previous stage's delay + its own transition-duration — zero dead time
   anywhere. Cards fade in (background/border/avatar/name/title together)
   over .25s; each vine segment starts the instant the card feeding it is
   done, and the next card starts the instant its vine arrives. See the
   vine comment above for the matching vine-side timestamps. */
.org-chart.in-view .org-tier-0 .org-card,
.org-chart.in-view .org-tier-0 .org-avatar,
.org-chart.in-view .org-tier-0 .org-name,
.org-chart.in-view .org-tier-0 .org-title { transition-delay: 0s; }

.org-chart.in-view .org-tier-1 .org-card,
.org-chart.in-view .org-tier-1 .org-avatar,
.org-chart.in-view .org-tier-1 .org-name,
.org-chart.in-view .org-tier-1 .org-title { transition-delay: .95s; }

.org-chart.in-view .org-tier-2 .org-card,
.org-chart.in-view .org-tier-2 .org-avatar,
.org-chart.in-view .org-tier-2 .org-name,
.org-chart.in-view .org-tier-2 .org-title { transition-delay: 1.9s; }

/* Executive tiles — President down through the 3 Chief Engineers (all 7
   exec-tier positions) — are marked as the leadership chain by a single
   light sweep that crosses the whole group, with their green outline
   emerging in its wake.

   The glimmer is ONE element on the group itself, not a per-card
   ::after: seven separately-animated overlays each sweeping their own
   little box (at their own local angle and speed) read as seven
   unrelated flickers rather than one pass of light, which is what
   looked glitchy. A single wide gradient traveling across the whole
   group gives every card a consistent, continuous wavefront. */
/* The light itself lives on each card (clipped by overflow:hidden) so it
   only ever passes over the tiles, never the empty background or the
   vines between them. Cohesion comes from timing instead: every card
   uses the SAME duration and easing, with only a small stagger by
   column, so the highlights read as one wavefront crossing the group
   left-to-right rather than seven unrelated flickers (which is what the
   earlier per-tier version, staggered seconds apart, looked like). */
.org-tier-0 .org-card,
.org-tier-1 .org-card,
.org-tier-2 .org-card {
  position: relative;
  overflow: hidden;
  /* Above the fork SVGs (z-index:0) so each branch disappears behind the
     card it connects to and reads as originating from underneath it. DOM
     order alone wasn't enough: a fork sits AFTER the tier above it, so
     the exec fork painted over the President's card even though the
     tier-1 cards below correctly covered it. */
  z-index: 2;
}
.org-tier-0 .org-card::after,
.org-tier-1 .org-card::after,
.org-tier-2 .org-card::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: -120%;
  width: 90%;
  background: linear-gradient(105deg, transparent, rgba(255, 255, 255, .38), transparent);
  opacity: 0;
  pointer-events: none;
  z-index: 5;
}
.org-chart.in-view .org-tier-0 .org-card::after,
.org-chart.in-view .org-tier-1 .org-card::after,
.org-chart.in-view .org-tier-2 .org-card::after {
  animation: exec-glimmer .75s cubic-bezier(.4, 0, .5, 1) 2.25s 1 forwards;
}
/* Column stagger — left cards catch the light first, right cards last,
   so the pass travels across the row. */
.org-chart.in-view .org-tier-1 .org-card:nth-child(1)::after { animation-delay: 2.15s; }
.org-chart.in-view .org-tier-1 .org-card:nth-child(2)::after { animation-delay: 2.25s; }
.org-chart.in-view .org-tier-1 .org-card:nth-child(3)::after { animation-delay: 2.35s; }
.org-chart.in-view .org-tier-2 .org-chief-slot:nth-child(1) .org-card::after { animation-delay: 2.15s; }
.org-chart.in-view .org-tier-2 .org-chief-slot:nth-child(2) .org-card::after { animation-delay: 2.25s; }
.org-chart.in-view .org-tier-2 .org-chief-slot:nth-child(3) .org-card::after { animation-delay: 2.35s; }
@keyframes exec-glimmer {
  0% { left: -120%; opacity: 0; }
  15% { opacity: 1; }
  80% { opacity: 1; }
  100% { left: 130%; opacity: 0; }
}

/* Outline arrives WITH the sweep rather than being on from the start —
   delayed to 2.4s (just behind the light) and eased in slowly, so it
   reads as the glimmer leaving the green behind it. background-color
   keeps each tier's ORIGINAL staged delay (0s / .95s / 1.9s) so the
   cards themselves still fade in when they always did — an earlier
   version of this rule set one shared transition for all properties,
   which wiped those delays and made every exec card appear instantly at
   page load. */
.org-chart.in-view .org-tier-0 .org-card,
.org-chart.in-view .org-tier-1 .org-card,
.org-chart.in-view .org-tier-2 .org-card {
  border-color: rgba(57, 255, 138, .45);
  box-shadow: 0 0 18px rgba(57, 255, 138, .22);
}
.org-chart.in-view .org-tier-0 .org-card {
  transition: background-color .25s ease 0s, border-color .6s ease 2.4s,
              box-shadow .6s ease 2.4s, transform .3s ease;
}
.org-chart.in-view .org-tier-1 .org-card {
  transition: background-color .25s ease .95s, border-color .6s ease 2.4s,
              box-shadow .6s ease 2.4s, transform .3s ease;
}
.org-chart.in-view .org-tier-2 .org-card {
  transition: background-color .25s ease 1.9s, border-color .6s ease 2.4s,
              box-shadow .6s ease 2.4s, transform .3s ease;
}

/* "Executive Board" — a vertical side label running alongside President
   through the Chief Engineers, same green as the glimmering cards next
   to it. .org-exec-group shrinks to fit its widest tier (fit-content)
   instead of spanning org-main's full width, so the label sits just
   outside the actual card group's edge instead of way out near the
   page's own margin. Fades in at 4.3s — the instant tier-2 (the last of
   the 7 exec positions) finishes fading in — and glimmers the same way
   the cards do once visible. */
/* Must stay full width, NOT fit-content — .org-tier-2 inside it is
   width:100% and sizes its three chief columns off this box, so
   shrink-to-fit here squeezed the chiefs into tier-1's narrower width
   and broke their alignment with the subteams below. The label is
   instead offset from center using tier-1's own half-width (549/2 =
   ~274px) plus a gap, so it still sits just outside the cards. */
.org-exec-group {
  position: relative;
  width: 100%;
  /* Mirrors .org-main's own flex centering — this group sits between
     org-main and the tiers it wraps, and as a plain block it left-aligned
     the fixed-width exec fork (.org-vine-exec, max-width:549px with
     margin-inline:0) instead of centering it under the President. */
  display: flex;
  flex-direction: column;
  align-items: center;
}
.org-exec-label {
  position: absolute;
  top: 0;
  bottom: 0;
  /* Flush to the exec group's own left edge (the group spans the full
     chart width, i.e. as wide as the Chief Engineers row) rather than
     tucked in beside the narrower officers row — sitting inside tier-2's
     span made the label read as belonging only to the middle row instead
     of to all 7 exec positions. */
  left: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-family: var(--font-display);
  font-size: .74rem;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: var(--swamp-green);
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  /* Slow, drawn-out fade (.9s vs the .25s the cards use) so the label
     eases in over the glimmer rather than snapping on with it. */
  transition: opacity .9s ease;
}
.org-chart.in-view .org-exec-label {
  opacity: .85;
  transition-delay: 2.25s;
}

/* Lead tiles, staged row by row. Cards carry an explicit data-row="1|2|3"
   attribute (set directly in team.html) instead of being targeted by
   nth-child position — nth-child chains had to vary per group (Development
   has 6 cards, Vehicle/Payload have 5) and any mismatch left a card
   unmatched, falling back to the base (undelayed) opacity/background
   rules above and popping in immediately instead of staying hidden until
   its turn. data-row is unambiguous and identical across all three
   groups. */

/* Row 1: Propulsion+Staging (Dev), Composites (Vehicle), Payload
   Mechanical (Payload), plus the shared Testing tile. */
/* NOTE — everything from the E-board's completion (2.15s) onward runs at
   a further 2x speed on top of that (4x the original pace overall): each
   stage's delay is now 2.15s + (old_offset / 4), and the matching
   transition-durations below are quartered to match. Chain:
   stem 2.15 -> label/trunk 2.275 -> row1 branches 2.575 -> row1 tiles
   2.925 -> row2 branches 3.05 -> row2 tiles 3.4 -> row3 branches 3.525
   -> row3 tiles 3.875 -> ops trunk 4.125 -> ops bars 4.425 -> ops tiles
   4.775 -> advisor label 4.9 -> advisor tile 5.025 (page total ~5.15s,
   down from ~10.3s). */
.org-chart.in-view .org-lead-grid > .org-card[data-row="1"],
.org-chart.in-view .org-lead-grid > .org-card[data-row="1"] .org-avatar,
.org-chart.in-view .org-lead-grid > .org-card[data-row="1"] .org-name,
.org-chart.in-view .org-lead-grid > .org-card[data-row="1"] .org-title,
.org-chart.in-view .org-shared-lead .org-card,
.org-chart.in-view .org-shared-lead .org-avatar,
.org-chart.in-view .org-shared-lead .org-name,
.org-chart.in-view .org-shared-lead .org-title { transition-delay: 3.125s; }

/* Row 2: Tooling+Composites (Dev), Structures+Recovery (Vehicle),
   Controls Mechanical+Payload Software (Payload). */
.org-chart.in-view .org-lead-grid > .org-card[data-row="2"],
.org-chart.in-view .org-lead-grid > .org-card[data-row="2"] .org-avatar,
.org-chart.in-view .org-lead-grid > .org-card[data-row="2"] .org-name,
.org-chart.in-view .org-lead-grid > .org-card[data-row="2"] .org-title { transition-delay: 3.75s; }

/* Row 3: Aerosciences+Instrumentation (Dev), Avionics+Flight Dynamics
   (Vehicle), Controls Software+Payload Electrical (Payload). */
.org-chart.in-view .org-lead-grid > .org-card[data-row="3"],
.org-chart.in-view .org-lead-grid > .org-card[data-row="3"] .org-avatar,
.org-chart.in-view .org-lead-grid > .org-card[data-row="3"] .org-name,
.org-chart.in-view .org-lead-grid > .org-card[data-row="3"] .org-title { transition-delay: 4.375s; }

/* Ops cards wait for the ops-spine's own bars to finish (4.85s start +
   .35s draw = 5.2s) — the tree fully connects before the tiles it's
   pointing at show up. */
.org-chart.in-view .org-tier-ops .org-card,
.org-chart.in-view .org-tier-ops .org-avatar,
.org-chart.in-view .org-tier-ops .org-name,
.org-chart.in-view .org-tier-ops .org-title { transition-delay: 5.2s; }

/* Academic Advisor — the very last thing in the chart, starting right
   as the ops cards above it finish fading in (4.775s start + .125s =
   4.9s). Label (own class, not the shared .org-branch-label timing meant
   for "Operations & Outreach") lands at 4.9s, just before the card below
   it — same "label arrives just before its tiles" pattern used
   everywhere else on this page. */
.org-tier-advisor { margin-top: 4rem; gap: 1.6rem; }
/* These two cards carry a role line ABOVE the name, unlike every other
   tile — .org-name's own margin-bottom does nothing here since nothing
   follows it, so the title needs its own. */
.org-tier-advisor .org-title { margin-bottom: .2rem; }
.org-chart.in-view .org-branch-label-advisor { transition-delay: 4.875s; }
.org-chart.in-view .org-tier-advisor .org-card,
.org-chart.in-view .org-tier-advisor .org-avatar,
.org-chart.in-view .org-tier-advisor .org-name,
.org-chart.in-view .org-tier-advisor .org-title { transition-delay: 5.325s; }

/* Hover lift for every non-exec tile.
   The staged reveal rules above each set a single transition-delay
   value, which CSS cycles across ALL of the card's transitioned
   properties — including transform. So a row-3 lead card carried an
   11.2s delay on its hover lift too, and the card simply never appeared
   to move. (The exec tiles didn't have this problem because their own
   rules spell out a per-property delay for each one.) These rules
   restate the transition per group: background-color/border-color keep
   that group's staged reveal delay, while transform and box-shadow are
   pinned to 0s so the hover responds immediately, matching the exec
   tiles' feel. */
.org-chart.in-view .org-lead-grid > .org-card[data-row="1"],
.org-chart.in-view .org-shared-lead .org-card {
  transition: background-color .175s ease 3.125s, border-color .175s ease 3.125s,
              transform .3s ease 0s, box-shadow .3s ease 0s;
}
.org-chart.in-view .org-lead-grid > .org-card[data-row="2"] {
  transition: background-color .175s ease 3.75s, border-color .175s ease 3.75s,
              transform .3s ease 0s, box-shadow .3s ease 0s;
}
.org-chart.in-view .org-lead-grid > .org-card[data-row="3"] {
  transition: background-color .175s ease 4.375s, border-color .175s ease 4.375s,
              transform .3s ease 0s, box-shadow .3s ease 0s;
}
.org-chart.in-view .org-tier-ops .org-card {
  transition: background-color .125s ease 5.2s, border-color .125s ease 5.2s,
              transform .3s ease 0s, box-shadow .3s ease 0s;
}
.org-chart.in-view .org-tier-advisor .org-card {
  transition: background-color .125s ease 5.325s, border-color .125s ease 5.325s,
              transform .3s ease 0s, box-shadow .3s ease 0s;
}

/* Card text/photo fades for the accelerated stretch after the E-board,
   matching each group's background-color duration above: .175s for the
   lead-engineer tiles under the Chiefs (that section runs a bit slower
   than the rest), .125s for ops and the advisor. */
.org-chart.in-view .org-lead-grid .org-avatar,
.org-chart.in-view .org-lead-grid .org-name,
.org-chart.in-view .org-lead-grid .org-title,
.org-chart.in-view .org-shared-lead .org-avatar,
.org-chart.in-view .org-shared-lead .org-name,
.org-chart.in-view .org-shared-lead .org-title { transition-duration: .175s; }
.org-chart.in-view .org-tier-ops .org-avatar,
.org-chart.in-view .org-tier-ops .org-name,
.org-chart.in-view .org-tier-ops .org-title,
.org-chart.in-view .org-tier-advisor .org-avatar,
.org-chart.in-view .org-tier-advisor .org-name,
.org-chart.in-view .org-tier-advisor .org-title { transition-duration: .125s; }

/* Square photo badge — bigger than the original 44px icon, sized to fit
   the narrowest card (the 148px-wide lead-engineer tiles) without
   touching any card's own width, since card width is load-bearing for
   the vine alignment math throughout this page. */
.org-avatar {
  width: 100px; height: 100px;
  margin-bottom: .7rem;
  border-radius: 8px;
  background: var(--bg-elevated-2);
  border: 1px solid var(--line);
  flex-shrink: 0;
  position: relative;
  background-size: cover;
  background-repeat: no-repeat;
  /* opacity has to be listed here too (not left to the separate
     .org-avatar, .org-name, .org-title rule above) — transition is a
     single shorthand property, and since this selector has the same
     specificity and comes later in the file, it would otherwise replace
     that rule's transition entirely, silently killing the opacity fade
     and its staged delay so avatars popped in instantly instead of
     waiting their turn. */
  transition: width .35s cubic-bezier(.2,.7,.2,1),
              height .35s cubic-bezier(.2,.7,.2,1),
              opacity .25s ease;
}
/* Clicking a card enlarges its photo in place — the tile itself grows
   wider (see .org-card.is-expanded below) to comfortably encompass the
   bigger square, so the photo stays inside the tile's own bordered
   shape instead of popping out over the rest of the page.
   transition-delay is forced to 0 here, !important — the avatar still
   carries whatever transition-delay was set by its tier's staged reveal
   rule above (e.g. 11.7s for a row-3 tile), and since that delay is a
   single value cycled across all listed transition-properties, the
   enlarge would otherwise silently wait out that same multi-second delay
   before doing anything, making a click look like it did nothing. */
.org-card.is-expanded .org-avatar {
  width: 200px;
  height: 200px;
  transition-delay: 0s !important;
}
/* Once a real photo is set (via inline background-image, see team.html),
   this class drops the placeholder person-icon so the photo shows
   cleanly instead of the icon painting on top of it. */
.org-avatar.has-photo::before { display: none; }
.org-avatar::before {
  content: "";
  position: absolute;
  inset: 0;
  background-color: var(--text-dim);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'/%3E%3Ccircle cx='12' cy='7' r='4'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'/%3E%3Ccircle cx='12' cy='7' r='4'/%3E%3C/svg%3E");
  -webkit-mask-size: 60%;
  mask-size: 60%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
}

.org-name {
  font-size: .88rem;
  font-weight: 600;
  margin-bottom: .2rem;
}
.org-title {
  font-size: .72rem;
  color: var(--text-dim);
  letter-spacing: .02em;
}

/* Detail panel is now a true in-flow part of the card, not a floating
   popover — clicking a card grows the tile itself downward to reveal
   the bio/contact, so it reads as one continuous shape expanding
   instead of a separate dropdown box appearing below it. */
.org-detail {
  width: 100%;
  max-height: 0;
  opacity: 0;
  overflow: hidden;
  pointer-events: none;
  text-align: left;
  transition: max-height .45s cubic-bezier(.2,.7,.2,1), opacity .3s ease,
              padding-top .45s cubic-bezier(.2,.7,.2,1);
}
.org-bio {
  font-size: .82rem;
  color: var(--text-dim);
  margin-top: .9rem;
}
.org-contact {
  display: flex;
  gap: 1rem;
  margin-top: .8rem;
  justify-content: center;
}
.org-contact a {
  font-size: .72rem;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--uf-orange-light);
  border-bottom: 1px solid currentColor;
  padding-bottom: 2px;
}

/* Expand behavior: the clicked card grows both wider and taller in
   place — wider so its own bordered shape actually encompasses the
   bigger photo (200px square, see .org-avatar above) with room to
   spare, taller as the bio/contact info reveals below it. Both grow
   from the same tile, not a separate box or an overlay elsewhere on the
   page. Other tiles are left alone — no dimming, no blur. width/max-width
   use !important since this needs to reliably beat whatever width rule
   applies to this specific card (a plain 168px .org-card, or a 148px
   .org-lead-grid .org-card, or the 100%-wide .org-card-sm ones) without
   having to chase every one of those selectors' specificity individually.
   z-index needs !important for the same reason — .org-lead-grid .org-card
   sets z-index:1 further down this file, and since that selector has the
   exact same specificity as this one, being declared later let it quietly
   win and reset an expanded lead tile's z-index back down to 1, same as
   every sibling around it — which is why the expanded tile was losing
   the overlap instead of sitting on top of everything else. */
.org-card.is-expanded {
  width: 260px !important;
  max-width: 260px !important;
  border-color: rgba(57, 255, 138, .5);
  box-shadow: 0 0 0 1px rgba(57, 255, 138, .15), 0 12px 32px rgba(0, 0, 0, .5);
  z-index: 100 !important;
  /* Forced to 0 here, !important — this rule's own transition shorthand
     (below) replaces the card's base transition wholesale (same
     specificity, later in the file wins), which means the tier reveal
     rule's single transition-delay value (e.g. 11.7s for a row-3 card)
     would otherwise cycle onto width/max-width too and silently delay
     the expand-click by that same multi-second amount. */
  transition-delay: 0s !important;
  transition: width .35s cubic-bezier(.2,.7,.2,1), max-width .35s cubic-bezier(.2,.7,.2,1),
              transform .3s ease, border-color .5s ease, box-shadow .3s ease,
              background-color .5s ease;
}
.org-card.is-expanded .org-detail {
  max-height: 300px;
  opacity: 1;
  padding-top: .9rem;
  pointer-events: auto;
}

/* Collapsing (clicking off, or clicking a different card) should snap
   back down fast, unlike the more deliberate opening pace above.
   team.js briefly adds .is-closing to a card the instant it starts
   collapsing (see team.js for why) — this is the only place that class
   is ever present, so forcing duration/delay here can't ever bleed into
   the scroll-triggered reveal animation elsewhere on the page. */
.org-card.is-closing,
.org-card.is-closing .org-avatar,
.org-card.is-closing .org-detail {
  transition-duration: .15s !important;
  transition-delay: 0s !important;
}

/* Tier 2 (the Chiefs) uses the same 6-column grid math as the Lead
   Engineer groups below it — each chief spans the 2 columns of their own
   leads — so Development/Vehicle/Payload line up directly under
   Connell/Rodriguez/Taylor and spread just as wide as their own columns. */
.org-grid {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  column-gap: 1.6rem;
  row-gap: 2rem;
  width: 100%;
  justify-items: center;
}
.org-grid .org-card { width: 100%; max-width: 168px; }

/* Tier 3 (Lead Engineers): three independent groups (Development, Vehicle,
   Payload), each its own small 2-column grid with a vine growing straight
   down from its Chief above — same trunk+branch language as the homepage
   milestone timeline, just vertical. Equal flex-basis + matching gap keeps
   them lined up with tier 2's 6-column grid above. */
.org-tier-3 {
  gap: 1.6rem;
  /* flex-start, not stretch — otherwise the moment one lead-group's grid
     grows taller (because a card in it expanded), the other two
     lead-groups would stretch to match, same issue as .org-tier-2. */
  align-items: flex-start;
}
.org-lead-group {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
  flex: 1 1 0;
  min-width: 0;
}
.org-fork-sm {
  display: block;
  width: 100%;
  height: 40px;
}

/* Shared lead role (e.g. Testing Lead Engineer) that sits in the gap
   between two org-lead-groups instead of inside either one's grid — fed
   by a .org-spine-share stub reaching out from each neighbor's spine.
   left/top are estimated (2/3 across org-tier-3, row-1 height); nudge
   once visible. */
.org-shared-lead {
  position: absolute;
  transform: translate(-50%, -50%);
  z-index: 2;
}
.org-grid-label {
  width: 100%;
  font-family: var(--font-display);
  font-size: .74rem;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--swamp-green);
  opacity: 0;
  text-align: center;
  margin-top: .6rem;
  margin-bottom: .8rem;
  transition: opacity .25s ease;
}
/* Label sits between the chief-to-grid stem and the cards, so it should
   only fade in once that stem (.org-fork-sm) has actually finished
   drawing down to it, not before — matching the "fade in after the vine
   reaches them" rule used for the cards. Stem starts at 2.15s and takes
   .5s to draw, so 2.65s is right when it arrives (close to the instant
   the subteam trunk below starts growing). */
.org-chart.in-view .org-grid-label { opacity: .8; transition-delay: 2.275s; }
/* Simple 2-column grid of lead cards under each Chief. */
.org-lead-grid {
  position: relative;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: 1.6rem;
  row-gap: 2rem;
  justify-items: center;
  /* Without this, grid's default align-items:stretch makes every card
     in a row stretch to match its tallest sibling — so the instant one
     card expands (taller, for the revealed bio), its row-mate stretches
     to match too, even though its own content didn't grow, leaving an
     empty, elongated tile next to the one actually being viewed. */
  align-items: start;
  width: 100%;
}
.org-lead-grid .org-card { width: 100%; max-width: 148px; z-index: 1; }
/* When a card expands, its row-mate stays at align-items:start (top of
   the now-taller row) while the spine's crossbar for that row shifts
   down along with the grid's new overall height — so the crossbar ends
   up aimed at the blank space below the row-mate instead of the card
   itself. Centering the row-mate within its row when a sibling is
   expanded moves it down to meet the branch roughly where it now lands,
   instead of leaving it stranded at the top pointing at nothing. */
.org-lead-grid:has(.org-card.is-expanded) .org-card:not(.is-expanded) {
  align-self: center;
}

/* Lead spine — a single vertical trunk with a horizontal crossbar at each
   row, matching the hand-drawn reference: one straight line down, ticked
   off at every row instead of separate per-lead branches. Pulled out of
   grid flow (position:absolute) so it doesn't consume a cell, and
   preserveAspectRatio="none" is safe here since every segment is
   axis-aligned — no distortion risk like the old curved vines had. Cards
   are position:relative and come after it in the DOM, so they paint on
   top and the crossbars appear to run behind/into each card. */
.org-lead-spine {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
}
/* Trunk is one continuous path per spine again (not 3 segments meeting at
   junctions) so it draws as a single unbroken sweep top-to-bottom instead
   of visibly separate pieces; dasharray covers its full ~344-unit length
   and it gets its own longer draw duration. Branches still depart from
   the trunk's center at each row — they just no longer require the trunk
   itself to be broken there. */
.org-spine-trunk {
  stroke-dasharray: 400;
  stroke-dashoffset: 400;
  transition-duration: .6s;
}
/* Regular branches are short (~80 units); a tight dasharray means they
   visibly grow out of the trunk across the whole transition instead of
   popping in near the end. Share-bars are much longer (~195 units), so
   they need their own bigger value — this rule must come after
   .org-spine-bar since both classes are applied together in the markup. */
.org-spine-bar { stroke-dasharray: 90; stroke-dashoffset: 90; }
.org-spine-share { stroke-dasharray: 210; stroke-dashoffset: 210; }

/* The stem from the Chief down into the lead-grid (fork-sm) — the vine
   that runs right under the Development/Vehicle/Payload label — draws in
   just .125s now (faster). It starts the instant the Chiefs finish
   fading in (2.15s), and the subteam spine trunk starts the instant the
   stem finishes drawing (2.4s) — no gap between any of these. */
.org-fork-sm .org-fork-path { transition-duration: .125s; }
.org-chart.in-view .org-lead-group > .org-fork-sm .org-fork-path { transition-delay: 2.15s; }
.org-chart.in-view .org-lead-group .org-fork-path { transition-delay: 2.275s; }

/* Each spine is 7 paths: trunk, then 2 branches per row (rows 1/2/3). The
   trunk grows as ONE continuous sweep from 2.275s (right after the stem
   above it finishes) over .4s, all the way to the bottom — matching
   "vines from them down all the way" as its own complete stage. Once
   that finishes (2.675s) row 1's branches fire (done 3.125s); row 2 and
   row 3 each start the instant their preceding row's tiles finish fading
   in (see the card timing rules above), not partway through the trunk. */
.org-chart.in-view .org-lead-spine path:nth-of-type(1) { transition-delay: 2.275s; }
.org-chart.in-view .org-lead-spine path:nth-of-type(2),
.org-chart.in-view .org-lead-spine path:nth-of-type(3) { transition-delay: 2.675s; }
.org-chart.in-view .org-lead-spine path:nth-of-type(4),
.org-chart.in-view .org-lead-spine path:nth-of-type(5) { transition-delay: 3.3s; }
.org-chart.in-view .org-lead-spine path:nth-of-type(6),
.org-chart.in-view .org-lead-spine path:nth-of-type(7) { transition-delay: 3.925s; }
/* Draw durations for the post-E-board stretch. The subteam spines under
   the Chiefs run a touch slower than the ops spine below them (bars
   .45s vs .35s, trunks .4s vs .3s) — this is the whole lead-engineer
   section easing off the 2x pace slightly. Scoped to the lead/ops
   spines so the exec forks above keep their original timing. */
.org-chart .org-lead-spine .org-fork-path { transition-duration: .45s; }
.org-chart .org-lead-spine .org-spine-trunk { transition-duration: .4s; }
.org-chart .org-ops-spine .org-fork-path { transition-duration: .35s; }
.org-chart .org-ops-spine .org-spine-trunk { transition-duration: .3s; }

.team-join {
  text-align: center;
  border-top: 1px solid var(--line);
  padding: var(--section-pad) var(--edge-pad);
}
.team-join .section-inner { padding: 0; display: flex; flex-direction: column; align-items: center; }
.team-join .btn-outline { margin-top: 1.5rem; }

/* ==========================================================================
   NAV DROPDOWN
   Used by the Mission item. The parent stays a real link (so touch and
   keyboard both still get to the page), and the submenu is revealed on
   hover or keyboard focus within.
   ========================================================================== */
.nav-has-menu { position: relative; }
/* Orange caret after the parent item. Drawn as a rotated square with two
   borders rather than a glyph, so it stays crisp at any size and matches
   the chevron in the scroll cue. It has to be ::before — ::after on
   .nav-links a is already the orange underline sweep. The link gets right
   padding so the caret doesn't overlap the neighbouring nav item. */
.nav-has-menu > a { padding-right: 14px; }
.nav-has-menu > a::before {
  content: "";
  position: absolute;
  right: 0;
  top: 50%;
  width: 5px;
  height: 5px;
  border-right: 1.5px solid var(--uf-orange);
  border-bottom: 1.5px solid var(--uf-orange);
  transform: translateY(-75%) rotate(45deg);
  /* Only present on hover/focus — the padding-right above is reserved
     unconditionally, so it fades in without shifting the nav items. */
  opacity: 0;
  transition: opacity .2s ease;
}
.nav-has-menu:hover > a::before,
.nav-has-menu:focus-within > a::before {
  opacity: 1;
}
/* No panel — just the bare list sitting on the page, left-aligned under
   the parent item. */
.nav-menu {
  position: absolute;
  top: 100%;
  left: 0;
  transform: translateY(-6px);
  padding: 1rem 0 0;
  min-width: 150px;
  display: flex;
  flex-direction: column;
  gap: .55rem;
  /* Hidden via opacity rather than display:none so it can transition;
     visibility keeps it out of the tab order while closed. */
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity .2s ease, transform .2s ease, visibility .2s;
}
/* A transparent bridge over the gap between the nav item and the menu —
   without it the menu closes as the pointer travels down to it. */
.nav-has-menu::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  height: 18px;
}
.nav-has-menu:hover .nav-menu,
.nav-has-menu:focus-within .nav-menu {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translateY(0);
}
.nav-menu a {
  display: block;
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  /* Full-strength white, not the dimmed nav colour — with no panel behind
     them these need the contrast to stay legible over whatever is on the
     page underneath. */
  color: var(--text);
  white-space: nowrap;
  transition: color .2s ease;
}
.nav-menu a::after { display: none; } /* no underline sweep inside the menu */
.nav-menu a:hover { color: var(--uf-orange); }

/* ==========================================================================
   MISSION PAGE
   ========================================================================== */

/* ---- Statement: alone on a full screen ---- */
.mission-hero {
  position: relative;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: calc(var(--section-pad) + 3rem) var(--edge-pad) var(--section-pad);
}
.mission-hero-inner { position: relative; z-index: 2; max-width: 1100px; }
.mission-hero-logo {
  position: relative;
  width: clamp(140px, 17vw, 240px);
  height: auto;
  margin: 0 auto 2rem;
  /* Same green bloom the exec tiles and vines use, so the mark reads as
     part of the page's lighting rather than a pasted-on PNG. */
  filter: drop-shadow(0 0 26px rgba(57, 255, 138, .18));
}
/* Glimmer on load: a band of light sweeps across the mark once. The
   wrapper carries it rather than the <img> itself — replaced elements
   like <img> can't host pseudo-elements. */
/* The wrapper is a circle (the mark is round) and clips the sweep, so the
   light stays inside the logo instead of crossing a square box.
   Deliberately NOT mask-image with the logo PNG: if that URL fails to
   resolve, the overlay is masked out entirely and the glimmer silently
   never appears — a clip has no such failure mode. */
.mission-hero-logo-wrap {
  position: relative;
  display: block;
  width: fit-content;
  margin: 0 auto 2rem;
  border-radius: 50%;
  overflow: hidden;
  isolation: isolate;
}
.mission-hero-logo-wrap .mission-hero-logo { margin: 0; display: block; }
.mission-hero-logo-wrap::after {
  content: "";
  position: absolute;
  top: -20%;
  bottom: -20%;
  left: -70%;
  width: 55%;
  pointer-events: none;
  background: linear-gradient(100deg,
    transparent, rgba(255, 255, 255, .85), transparent);
  transform: rotate(8deg);
  opacity: 0;
  /* One slow pass, starting once the page has settled. Ends at 1.4 + 2.4
     = 3.8s, which is where the statement pulses pick up. */
  animation: logo-glimmer 2.4s ease-in-out 1.4s 1 forwards;
}
@keyframes logo-glimmer {
  0%   { left: -70%; opacity: 0; }
  12%  { opacity: 1; }
  85%  { opacity: 1; }
  100% { left: 120%; opacity: 0; }
}
.mission-hero .eyebrow { margin-bottom: 1.6rem; }
.mission-statement {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(1.7rem, 4.4vw, 3.6rem);
  line-height: 1.25;
  letter-spacing: .01em;
  color: var(--text);
  text-wrap: balance;
}
/* Each clause on its own line at desktop width — the three-part rhythm is
   the whole point of the statement, so it shouldn't wrap arbitrarily. */
.mission-statement span {
  display: block;
  transform-origin: center;
  /* Picks up the instant the logo glimmer ends (1.4s start + 2.4s pass =
     3.8s), then each clause pulses in turn so the three-part rhythm of
     the statement is played out rather than just sitting there. Each
     pulse lingers 1.8s, staggered .75s apart so they overlap and read as
     one slow wave travelling down the lines. */
  animation: statement-pulse 1.8s cubic-bezier(.34, 1.2, .5, 1) backwards;
}
.mission-statement span:nth-child(1) { animation-delay: 3.8s; }
.mission-statement span:nth-child(2) { animation-delay: 4.55s; }
.mission-statement span:nth-child(3) { animation-delay: 5.3s; }
/* The closing clause stays the same white as the rest of the statement —
   its green now only shows up as the glow during its pulse (see
   statement-pulse-green below), so the colour reads as a moment rather
   than a permanent state. */

/* Layered shadows — a tight core plus a wide halo — read as light coming
   off the text rather than a flat blur behind it. The glow peaks early
   and decays slowly (holding through ~60%) so it lands as a pulse of
   light instead of a symmetrical fade in and out. */
@keyframes statement-pulse {
  0% {
    transform: scale(1);
    text-shadow: none;
  }
  30% {
    transform: scale(1.04);
    text-shadow:
      0 0 12px rgba(255, 255, 255, .95),
      0 0 34px rgba(255, 255, 255, .7),
      0 0 70px rgba(190, 220, 255, .45);
  }
  60% {
    transform: scale(1.012);
    text-shadow:
      0 0 8px rgba(255, 255, 255, .5),
      0 0 26px rgba(255, 255, 255, .3);
  }
  100% {
    transform: scale(1);
    text-shadow: none;
  }
}
/* The closing clause is the green one, so its flash is green too. */
@keyframes statement-pulse-green {
  0% {
    transform: scale(1);
    text-shadow: none;
  }
  30% {
    transform: scale(1.05);
    text-shadow:
      0 0 12px rgba(255, 255, 255, .8),
      0 0 30px rgba(57, 255, 138, .95),
      0 0 78px rgba(57, 255, 138, .6);
  }
  60% {
    transform: scale(1.015);
    text-shadow:
      0 0 10px rgba(57, 255, 138, .55),
      0 0 30px rgba(57, 255, 138, .35);
  }
  /* Settles to a permanent low green glow rather than back to nothing —
     the flash passes, the ember stays. Paired with animation-fill-mode
     `both` below so this end state is held after the animation finishes;
     with `backwards` alone it would snap back to unlit. */
  100% {
    transform: scale(1);
    text-shadow:
      0 0 8px rgba(255, 255, 255, .35),
      0 0 18px rgba(57, 255, 138, .8),
      0 0 44px rgba(57, 255, 138, .55),
      0 0 90px rgba(57, 255, 138, .3);
  }
}
.mission-statement span:last-child {
  animation-name: statement-pulse-green;
  animation-fill-mode: both;
}
.mission-hero .scroll-cue { opacity: .75; }

/* ---- Alternating full-bleed sections ----
   The media is not a tile in a column — it fills the entire section, edge
   to edge and top to bottom, with the copy laid over one side of it. The
   only thing that alternates is which side the copy sits on, and which
   way the black fade runs. */
.mission-row {
  position: relative;
  display: flex;
  align-items: center;
  min-height: 100vh;
  padding: var(--section-pad) var(--edge-pad);
  overflow: hidden;
}
/* Copy sits opposite the "open" side of the image: right by default, left
   on reversed rows. margin-inline handles it rather than flex order, since
   the media is out of flow entirely. */
.mission-copy { margin-left: auto; }
.mission-row.is-reversed .mission-copy { margin-left: 0; margin-right: auto; }

/* The media covers ~70% of the section's width and is flush to the top,
   bottom and outer edge. The remaining ~30% is left as bare page — the
   site's own black/starfield background, not a painted panel. */
.mission-media {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 70%;
  z-index: 0;
  overflow: hidden;
  border-radius: 0;
  /* A MASK, not a black overlay on top: masking fades the image itself
     out to nothing, so the starfield and page background show through the
     falloff. A black gradient would instead paint over the stars and
     leave a visible dark rectangle against the rest of the page.
     The image is fully gone by 82% of its own 70% width — i.e. ~57% into
     the section — which lands clear of the copy column starting at ~66%,
     so the text always sits on plain background with nothing behind it. */
  -webkit-mask-image: linear-gradient(to right, #000 40%, transparent 82%);
  mask-image: linear-gradient(to right, #000 40%, transparent 82%);
}
.mission-row.is-reversed .mission-media {
  left: auto;
  right: 0;
  -webkit-mask-image: linear-gradient(to left, #000 40%, transparent 82%);
  mask-image: linear-gradient(to left, #000 40%, transparent 82%);
}
.mission-media img,
.mission-media video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* A gentle overall dim so white copy stays readable where it overlaps the
   tail of the image. Deliberately uniform — the directional falloff is
   handled by the mask above, not here. */
.mission-media::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: rgba(0, 0, 0, .18);
}

/* IREC row exception: that team photo is a wide group shot with people
   running nearly edge to edge, so the standard falloff started eating
   into the group. Narrower media (62%) with the fade pushed to the very
   end, rather than the standard early falloff — the image still clears
   the copy column, but almost none of the group is lost to the
   gradient. */
#irec .mission-media {
  width: 62%;
  -webkit-mask-image: linear-gradient(to right, #000 76%, transparent 100%);
  mask-image: linear-gradient(to right, #000 76%, transparent 100%);
}

.mission-copy {
  position: relative;
  z-index: 2;
  /* Sized to sit in the ~30% of the section the media doesn't cover,
     with a floor so it never collapses to a sliver on narrower screens. */
  width: min(46ch, 34%);
  max-width: none;
}
.mission-copy .eyebrow { color: var(--swamp-green); margin-bottom: .9rem; }
.mission-copy h2 {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.6rem, 3.2vw, 2.7rem);
  line-height: 1.12;
  margin-bottom: 1.1rem;
  text-wrap: balance;
}
.mission-copy p {
  color: var(--text-dim);
  font-size: clamp(.95rem, 1.4vw, 1.08rem);
  margin-bottom: 1.6rem;
}

@media (max-width: 860px) {
  /* Too narrow to hold copy beside an image. The old approach kept the
     image absolutely positioned at a fixed 70%-of-row height with
     object-fit:cover — cropping it to whatever that box's aspect ratio
     happened to be, which for a wide group photo meant losing people
     off the sides or top. Simpler and safer below this width: drop the
     absolute positioning and the crop entirely. The row becomes a
     plain centered column — image first, sized to its own natural
     aspect ratio so nothing is ever cut off, then the copy centered
     below it, both in normal document flow instead of stacked via
     position tricks. */
  .mission-row,
  .mission-row.is-reversed {
    flex-direction: column;
    align-items: center;
    min-height: 0;
    gap: 2rem;
  }
  .mission-copy,
  .mission-row.is-reversed .mission-copy {
    position: static;
    width: 100%;
    max-width: 46ch;
    margin: 0;
    text-align: center;
  }
  .mission-row .mission-media,
  .mission-row.is-reversed .mission-media {
    position: static;
    width: 100%;
    height: auto;
    aspect-ratio: auto;
    border-radius: var(--radius);
    mask-image: none;
    -webkit-mask-image: none;
  }
  .mission-media img,
  .mission-media video {
    height: auto;
    object-fit: contain;
  }
  /* The dim overlay existed to keep copy readable where it crossed the
     image's fade — with the copy now living below the image instead of
     overlapping it, there's nothing left for it to do. */
  .mission-media::after { display: none; }
  #irec .mission-media {
    width: 100%;
    mask-image: none;
    -webkit-mask-image: none;
  }
  .mission-statement span { display: inline; }
}

/* ==========================================================================
   SPONSORS PAGE
   Built in stages. Step A only: the sky itself, scrollable.
   ========================================================================== */
.sponsors-page {
  /* Matches the darkest band of the background art, so any overscroll or
     rubber-banding at the top of the page reads as more sky rather than
     as the site's default black showing through. */
  background: #000000;
}
/* The shared starfield backdrop is not wanted here — this page has its own
   painted atmosphere. */
.sponsors-page .starfield { display: none; }

.sky {
  position: relative;
  z-index: 1;
}
/* Everything painted — the background image and every absolutely
   positioned overlay on top of it (clouds, rocket, plumes, ground
   title, foreground foliage) — lives inside this wrapper, separate from
   the tier sections that follow it in .sky. Percentages on those
   overlays (`top`, `bottom`) are measured against THIS element's
   height, not .sky's, so they keep tracking the art's own bands
   regardless of how much tier content flows after it. */
.sky-art {
  position: relative;
  overflow: hidden;
  /* Crops the dead water off the bottom of the artwork so the page lands
     on the treeline instead of on empty swamp.

     The art is 6375 x 18750. Setting an aspect-ratio here that is SHORTER
     than the art's own makes this box the page's real height, and
     overflow:hidden trims the excess off the bottom. 18375 keeps the top
     98% of the image — raise that number to show more water, lower it to
     cut in tighter on the trees. The width stays 6375 either way, so the
     scale of the art never changes. */
  aspect-ratio: 6375 / 18375;
}
/* Anchored to the top of the crop box, full width. display:block removes
   the inline-element baseline gap that would otherwise leave a sliver
   under it. */
.sky-bg {
  display: block;
  width: 100%;
  height: auto;
}

/* ---- Cloud decks ----
   Each sits on the boundary between two colour bands of the background,
   matching the placement in "Full sponsor page base.png".

   `top` is a percentage of .sky, which is the full (uncropped) art
   height, so these stay locked to their bands at any viewport width.
   translateY(-50%) centres each deck on its boundary instead of letting
   it hang below the line.

   z-index 6 puts them in FRONT of the rocket (which will sit at 4), so
   the vehicle flies behind the weather. */
.cloud-layer {
  position: absolute;
  left: 0;
  width: 100%;
  height: auto;
  z-index: 6;
  pointer-events: none;
  transform: translateY(-50%);
}
/* Zoom uses scale() rather than a wider element: it grows from the
   centre, so the extra size spills equally off both edges with no offset
   needed, and .sky clips the overflow. */

/* light blue -> mid blue: the thick cumulus deck */
.cloud-1 {
  top: 64.5%;
  transform: translateY(-50%) scale(1.12);
}
/* mid blue -> navy: thinner, higher cloud */
.cloud-2 { top: 48.75%; }
/* navy -> dark navy: the aurora band */
.cloud-3 {
  top: 29%;
  transform: translateY(-50%) scale(1.16);
}

/* ---- Exosphere starfield ----
   Sits in the black band at the top of the climb. Purpose-built rather
   than reusing the site-wide .starfield, which is fixed to the viewport
   and carries its own inset, transform and parallax — inheriting all of
   that and overriding it piecemeal is what made the earlier attempt
   misbehave.

   Masked so the stars fade out downward, thinning as the atmosphere
   thickens instead of stopping on a hard line. */
.exo-stars {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: var(--exo-height, 28%);
  z-index: 2;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, #000 55%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 55%, transparent 100%);
}
/* Three layers, each tiled at its own scale and twinkling on its own
   period. A single layer could only pulse every star in lockstep; with
   three drifting against each other, some brighten while others dim.
   The periods are deliberately non-multiples so they never fall back
   into sync. */
.exo-star-layer {
  position: absolute;
  inset: 0;
  background-repeat: repeat;
}
.exo-star-1 {
  background-image:
    radial-gradient(circle 1.1px at 34px 22px, rgba(255,255,255,.95), transparent),
    radial-gradient(circle .8px at 118px 86px, rgba(255,255,255,.7), transparent),
    radial-gradient(circle 1px at 72px 148px, rgba(206,226,255,.8), transparent),
    radial-gradient(circle .7px at 176px 42px, rgba(255,255,255,.6), transparent);
  background-size: 190px 190px, 240px 240px, 210px 210px, 280px 280px;
  animation: exo-twinkle-a 16s ease-in-out infinite;
}
.exo-star-2 {
  background-image:
    radial-gradient(circle 1.3px at 96px 54px, rgba(255,255,255,.95), transparent),
    radial-gradient(circle .9px at 28px 132px, rgba(226,238,255,.75), transparent),
    radial-gradient(circle 1px at 168px 108px, rgba(255,255,255,.8), transparent);
  background-size: 230px 230px, 300px 300px, 265px 265px;
  animation: exo-twinkle-b 21s ease-in-out infinite;
}
.exo-star-3 {
  background-image:
    radial-gradient(circle 1.6px at 62px 88px, rgba(255,255,255,1), transparent),
    radial-gradient(circle 1.2px at 214px 36px, rgba(214,232,255,.9), transparent),
    radial-gradient(circle 1.4px at 122px 262px, rgba(255,255,255,.95), transparent);
  background-size: 340px 340px, 390px 390px, 360px 360px;
  animation: exo-twinkle-c 27s ease-in-out infinite;
}
@keyframes exo-twinkle-a {
  0%   { opacity: .75; }
  45%  { opacity: .45; }
  100% { opacity: .75; }
}
@keyframes exo-twinkle-b {
  0%   { opacity: .9; }
  35%  { opacity: 1; }
  70%  { opacity: .6; }
  100% { opacity: .9; }
}
@keyframes exo-twinkle-c {
  0%   { opacity: .95; }
  55%  { opacity: .7; }
  100% { opacity: .95; }
}

/* ==========================================================================
   THE VEHICLE
   Fixed dead centre of the viewport, so it holds still while the sky
   scrolls past it. z-index 4 sits above the painted background and below
   the cloud decks (6), so the rocket passes behind the weather.
   ========================================================================== */
.rocket {
  position: fixed;
  top: 50%;
  left: 50%;
  z-index: 4;
  width: clamp(16px, 1.9vw, 32px);
  pointer-events: none;
  /* Stage 2 sits directly on top of stage 1, base to nose, so the two
     read as one vehicle. line-height:0 kills the inline baseline gap
     that would otherwise open a seam between them. */
  display: flex;
  flex-direction: column;
  align-items: center;
  line-height: 0;
  /* --launch runs 0 (parked low, hidden behind the water) to 1 (arrived
     at flight position, dead centre), set by sponsors.js from how far
     you have scrolled off the bottom. Past 1 the rocket simply holds
     position and the sky does all the moving — it no longer flies off
     the top at the end of the climb.

     --summit still exists and still drives the wipe on the summit copy;
     it just no longer moves the vehicle. */
  transform: translate(
    -50%,
    calc(-50% + ((1 - var(--launch, 0)) * 62vh))
  );
  /* A short transition on the transform, matching the homepage's hero
     elements. Each scroll frame sets a new target and the rocket eases
     toward it rather than snapping, which is what smooths the motion and
     gives the slight trailing blur as it moves. */
  transition: transform .12s linear;
}

.rocket-stage {
  display: block;
  width: 100%;
  height: auto;
}
/* ---- Plume ----
   Hangs off the base of the stack and travels with it. Sized as a
   multiple of the rocket's own width so it stays proportional at any
   viewport, and long enough that its tail runs off the bottom of the
   screen rather than ending visibly mid-air.

   z-index:-1 puts it behind both stages. That is safe here because
   .rocket carries its own z-index and therefore its own stacking
   context — the negative value only pushes the plume behind its
   siblings inside that context, not behind the sky.

   Locked to the background, not to the vehicle: it is a child of .sky and
   sized as a percentage of the artwork, so it scrolls with the sky and
   scales with it at any window width.

   Sizing it against .rocket was the reason it kept coming out small —
   that box is only ~32px wide, so every percentage was measured against
   it. .sky is the full page width, which is why a plain percentage
   works here. */
.thrust-trail {
  position: absolute;
  /* Anchored from the BOTTOM rather than the top, so the tail always
     sits in the water regardless of how tall the artwork renders — the
     plume emerges from the swamp and climbs from there. A top-based
     offset would drift away from the waterline as the page scales. */
  bottom: var(--thrust-bottom, -.3%);
  left: 50%;
  width: var(--thrust-width, 50%);
  /* The global reset caps every image at `max-width: 100%`, which was
     silently clamping this element — 500%, 200% and 100% all rendered at
     exactly the same size. Releasing the cap is what makes the width
     value actually mean anything here. */
  max-width: none;
  height: auto;
  transform: translateX(-50%);
  /* Behind the rocket (4) and, more importantly, behind the foreground
     foliage (7) — so the base of the plume is hidden by the water it is
     rising out of instead of sitting on top of it. */
  z-index: 3;
  pointer-events: none;
  /* Only the part of the plume BELOW the rocket is drawn. --plume-cut is
     the fraction to remove from the top, set by sponsors.js from the
     vehicle's live position, so the trail is uncovered by the rocket's
     own ascent rather than being there from the start. inset() takes the
     amount to cut from each edge, so 1 hides all of it and 0 shows the
     lot. */
  clip-path: inset(calc(var(--plume-cut, 1) * 100%) 0 0 0);
  /* Both trails thin away as the climb continues — see --plume-fade in
     sponsors.js. */
  opacity: var(--plume-fade, 1);
}

/* ---- Upper plume ----
   A second trail higher in the climb, running from the cloud 2 deck at
   the foot of the Mesosphere up to the aurora. Pinned at BOTH ends,
   unlike the one below it, so it spans exactly that stretch: the values
   are the same percentages those two decks sit at (48.75% and 29% of
   .sky), with the tail expressed from the bottom — 100% - 48.75%.

   Pinning both edges only works with height:auto removed, or the image
   would size from its own aspect ratio and ignore the bottom edge. */
.thrust-trail-high {
  /* Both edges shifted by the same amount, so the trail moves up the page
     as a whole rather than changing length. */
  top: var(--thrust-high-top, 4.5%);
  bottom: var(--thrust-high-bottom, 85.5%);
  height: revert;
  /* Fades out as the climb continues: --plume-fade is 1 while the trail
     is still relevant and 0 by the foot of the Exosphere, so the plume
     thins away rather than persisting all the way to the top. */
  opacity: var(--plume-fade, 1);
  /* Its own cut value, not the one the lower plume uses. Both are
     uncovered by the rocket, but the fraction to hide depends on where
     each trail sits relative to the vehicle — sharing --plume-cut would
     clip this one using the water plume's geometry and it would never
     line up. */
  clip-path: inset(calc(var(--plume-cut-high, 1) * 100%) 0 0 0);
}

/* Small alignment nudge so stage 1's top seats properly under stage 2.
   margin-top pulls the join tighter; the two are stacked in flow, so
   this moves stage 1 up without disturbing stage 2 above it. */
.rocket-stage-1 {
  transform: translateX(-2.5%);
  margin-top: -3%;
}

/* Separation at the Mesosphere: stage 2 continues with the rig while the
   booster is left behind. Because the rig itself keeps moving with the
   scroll, "staying put" means travelling back down the screen at the same
   rate the sky does — hence the large downward translate rather than a
   simple stop. */
/* No opacity delay here — this is the REATTACH direction, so the booster
   should fade back in as it rises rather than staying invisible for over
   a second and then popping into place. The delay belongs only on the
   separation, below. */
.rocket-stage-1 {
  transition: transform 2.4s cubic-bezier(.3, 0, .7, 1),
              opacity 1.6s ease;
}
/* The nudge above is repeated here: transform is a single property, so
   omitting translateX would drop it the instant staging fires and the
   booster would jump sideways before falling. */
.is-staged .rocket-stage-1 {
  transform: translateX(-2.5%) translateY(120vh) rotate(14deg);
  opacity: 0;
  /* The fade is held back on the way out, so the booster is still
     visible as it falls clear before disappearing. Its delay scales with
     the fall — pushed out to match the longer drop, or the booster would
     vanish while still high in frame. */
  transition: transform 5s cubic-bezier(.3, 0, .7, 1),
              opacity 2.4s ease 2.8s;
}

/* ---- Tiers ----
   Each block is pinned to the centre of its own colour band. The `top`
   percentages are band midpoints taken from the artwork and divided by
   .98 to account for the 2% crop on .sky, so they track the bands rather
   than the cropped box.

   z-index 5 puts them above the background and rocket (4) but below the
   cloud decks (6) and foreground foliage (7) — text passes behind the
   weather the same way the vehicle does. */
.tier {
  position: absolute;
  left: 0;
  width: 100%;
  z-index: 5;
  display: grid;
  grid-template-columns: 1.05fr 1fr;
  gap: clamp(2rem, 6vw, 6rem);
  align-items: center;
  padding-inline: var(--edge-pad);
  transform: translateY(-50%);
  pointer-events: none;
}
.tier-exosphere    { top: 8.2%; }
.tier-thermosphere { top: 22.4%; }
.tier-mesosphere   { top: 39.8%; }
.tier-stratosphere { top: 57.7%; }
.tier-troposphere  { top: 74.5%; }

.tier-copy { max-width: 46ch; }
.tier-level {
  display: block;
  font-family: var(--font-display);
  font-size: .72rem;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: var(--uf-orange);
  margin-bottom: .9rem;
}
.tier-name {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(2rem, 5.4vw, 4.2rem);
  line-height: 1;
  letter-spacing: .02em;
  margin-bottom: .7rem;
  text-shadow: 0 4px 34px rgba(0, 0, 0, .45);
}
.tier-line {
  font-size: clamp(1rem, 1.8vw, 1.35rem);
  color: var(--text);
  margin-bottom: .9rem;
}
.tier-desc {
  color: var(--chrome);
  font-size: clamp(.9rem, 1.3vw, 1.02rem);
  text-shadow: 0 1px 8px rgba(0, 0, 0, .4);
}

/* Each tier's call to action. pointer-events is re-enabled here because
   .tier turns it off wholesale — the blocks sit over the artwork and
   should not intercept anything except their own links. */
.tier-cta {
  margin-top: 1.5rem;
  pointer-events: auto;
}

/* ---- Summit ----
   The top of the climb, above the Exosphere block, where the rocket
   finally comes to rest. */
.summit {
  position: absolute;
  top: 3%;
  left: 0;
  width: 100%;
  z-index: 8;
  padding-inline: var(--edge-pad);
  text-align: center;
  /* Wipe reveal. --summit runs 0 to 1 as the top of the page is reached
     (set by sponsors.js). clip-path uncovers the block from the bottom
     up, so it appears in the wake of the departing rocket rather than
     being there all along. inset() takes the amount to CUT from each
     edge, so 100% hides it entirely and 0% shows all of it. */
  clip-path: inset(calc((1 - var(--summit, 0)) * 100%) 0 0 0);
  opacity: var(--summit, 0);
  transition: opacity .3s linear;
}
.summit-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.4rem, 3.4vw, 2.6rem);
  letter-spacing: .02em;
  margin-bottom: 1.4rem;
  color: var(--text);
}
.summit-cta { margin-inline: auto; }

/* Sponsor spots. An auto-fit CSS grid here left each tier's logos
   pinned to the left edge of however many columns that row happened to
   fill, which is exactly what read as "jumbled" — a lone sponsor sat
   flush left instead of centered, and different tiers' logos never
   lined up with each other because each row's column count differed.
   Flexbox with justify-content:center fixes both at once: the filled
   logos in a row center as a group regardless of count, whether that's
   one (MAE, Relativity), two (Ansys/Diab — wrap centers them as a
   balanced pair rather than one hugging each edge), or a full row.
   Empty slots are taken out of the flow entirely (display:none) rather
   than reserved as invisible spacers — reserving their space is what
   pinned everything left of center in the first place. */
.tier-slots {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 1.5rem;
}
.sponsor-spot { display: none; }
.sponsor-spot.is-filled {
  display: flex;
  flex: 0 1 140px;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  min-height: 0;
  border: none;
  background: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  pointer-events: auto;
  transition: transform .2s ease;
}
.sponsor-spot.is-filled:hover,
.sponsor-spot.is-filled:focus-visible {
  transform: translateY(-3px);
}
.sponsor-spot.is-filled img {
  width: 100%;
  max-height: 64px;
  object-fit: contain;
  /* Every transparent logo here is dark-on-clear (navy, black, deep
     blue/orange) — none are light-colored — so a soft white glow is a
     safe universal way to keep them legible over both the black/navy
     tier bands (mobile) and the painted sky (desktop) without putting
     a card behind them. Two stacked drop-shadows approximate a soft
     outline rather than one blurry halo. */
  filter:
    drop-shadow(0 0 2px rgba(255, 255, 255, .85))
    drop-shadow(0 0 7px rgba(255, 255, 255, .5));
}
.sponsor-spot-name {
  font-family: var(--font-display);
  font-size: .68rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--text);
  text-shadow: 0 2px 10px rgba(0, 0, 0, .6);
}

/* Logo size scales with how crowded a tier's row actually is -- a tier
   with only one or two sponsors has the room to run each logo bigger
   than the shared 64px default, while a full row (Troposphere's six)
   needs the base size just to fit without crowding. These are keyed to
   each tier's CURRENT sponsor count, not automatic -- if a tier's count
   changes, its size here should move to match the next bracket down
   (or up). Exosphere (1: MAE) and Thermosphere (1: Relativity) both
   get the largest size; Mesosphere (2: Ansys, Diab) and Stratosphere
   (3: Anthropic, Autodesk, Monster) step down from there. */
/* The image's own width:100% (above) means the FLEX ITEM's width is
   what actually bounds a logo before object-fit:contain ever gets to
   use the extra max-height — widening the item alongside the height
   is what actually makes these read as bigger, not just taller. */
.tier-exosphere .sponsor-spot.is-filled,
.tier-thermosphere .sponsor-spot.is-filled {
  /* Wider than Exosphere's MAE strictly needs (it maxes out its own
     120px height around 240px wide) because Thermosphere's Relativity
     wordmark is much wider relative to its height (~3.5:1) -- at 240px
     it was width-bound well short of the same 120px, only reaching
     ~70px. object-fit:contain never upscales past max-height, so the
     extra width here is free for MAE (just more side padding) and is
     what Relativity actually needs to reach the same target height. */
  flex-basis: 340px;
}
.tier-exosphere .sponsor-spot.is-filled img,
.tier-thermosphere .sponsor-spot.is-filled img {
  max-height: 120px;
}
.tier-mesosphere .sponsor-spot.is-filled { flex-basis: 220px; }
.tier-mesosphere .sponsor-spot.is-filled img {
  max-height: 92px;
}
.tier-stratosphere .sponsor-spot.is-filled { flex-basis: 160px; }
.tier-stratosphere .sponsor-spot.is-filled img {
  max-height: 80px;
}

/* ---- Mobile ----
   Each .tier is absolutely positioned at a fixed percentage down the
   giant background image, matched to that band's midpoint on desktop,
   where the 2-column layout keeps every block short enough to fit in
   its ~15-18% slice. On a narrow phone width there's no second column
   to put .tier-slots in, .tier-name's font-size clamp bottoms out at a
   fixed rem floor regardless of viewport, and the resulting single
   squeezed column runs taller than the band has room for — so
   neighboring tiers' text physically overlapped. There's no font-size
   fix for that; the fundamental content doesn't fit in that band's
   pixel height at any reasonable mobile type size. Below this width the
   bands are abandoned and tiers become a plain stacked list in their
   existing DOM order (space to ground), same treatment as the
   milestones and IREC-page fixes above. */
@media (max-width: 860px) {
  /* The rocket's animated climb is driven entirely by JS math built for
     the desktop start-at-bottom/scroll-up interaction (see sponsors.js
     — that whole tracker is skipped on mobile now). Rather than leave
     it position:fixed at a frozen default (which would pin it to the
     viewport and have it float weirdly over the tier text as you
     scroll past), it's anchored into the scene itself here — same
     bottom-relative-to-.sky-art placement .ground-title and
     .sky-foreground already use — so it scrolls away naturally with the
     artwork it's part of instead of persisting on screen. */
  .rocket {
    position: absolute;
    top: auto;
    left: 50%;
    bottom: 5%;
    width: clamp(20px, 4vw, 32px);
    transform: translateX(-50%);
    transition: none;
  }
  /* The painted sky itself doesn't survive going static either -- it's
     one giant image cropped and positioned for the desktop scroll
     experience, and stretches/tiles oddly once .tier stops being
     absolutely positioned against it. Drop it, and give each tier its
     own flat background instead, sampled from that band's actual color
     in assets/sponsors/sky-background.png (troposphere's light blue up
     through exosphere's near-black) -- so the page still reads as
     ascending through the atmosphere, just without the artwork. */
  .sky-art { display: none; }
  .tier {
    position: static;
    transform: none;
    display: block;
    width: 100%;
    grid-template-columns: none;
    padding: 3rem var(--edge-pad);
    border-top: 1px solid rgba(255, 255, 255, .12);
  }
  .tier-exosphere    { background: #000000; }
  .tier-thermosphere { background: #0e142c; }
  .tier-mesosphere   { background: #1c2b59; }
  .tier-stratosphere { background: #1e5092; }
  .tier-troposphere  { background: #679bcd; }
  .tier-copy { max-width: none; }
  .tier-name { font-size: clamp(1.9rem, 8vw, 2.8rem); }
  .tier-slots { margin-top: 1.75rem; }
  .summit {
    position: static;
    clip-path: none;
    opacity: 1;
    padding: 3rem var(--edge-pad);
    background: rgba(0, 0, 0, .6);
  }
}

/* ---- Scroll cue ----
   Fixed to the viewport rather than parked at the bottom of the document,
   so it is on screen the moment the page opens on the ground. It fades
   out in step with the rocket's climb: --launch is 0 on the pad and 1
   once the vehicle reaches flight position, so the cue is gone by the
   time the ascent is under way.

   z-index 8 keeps it above the foreground foliage (7), which otherwise
   covers this part of the screen at ground level. */
.sky-cue {
  position: fixed;
  bottom: 2.4rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 8;
  /* Normal column order: the chevron is first in the markup, so it lands
     above the text. (column-reverse would flip them.) */
  flex-direction: column;
  gap: .55rem;
  max-width: min(90vw, 640px);
  text-align: center;
  line-height: 1.5;
  color: var(--text);
  text-shadow: 0 1px 10px rgba(0, 0, 0, .55);
  /* Tied to --ground, not --launch: the cue should survive as long as the
     water is still on screen, rather than vanishing the instant the
     rocket lifts off. */
  opacity: calc(1 - var(--ground, 0));
}
.sky-cue-lead {
  font-family: var(--font-display);
  font-size: .9rem;
  letter-spacing: .26em;
}
/* Quieter and un-tracked, so the two lines read as a headline and a
   caption rather than competing. */
.sky-cue-sub {
  font-size: .72rem;
  letter-spacing: .1em;
  text-transform: none;
  color: var(--chrome);
}
/* Points up, since the journey from here is upward — and rotating the
   arrow means the bob animation has to be re-pointed too, or it would
   drift down-right while the arrow points up-left. */
.sky-cue .chevron {
  transform: rotate(225deg);
  animation: bob-up 1.8s ease-in-out infinite;
}
@keyframes bob-up {
  0%, 100% { transform: rotate(225deg) translate(0, 0); }
  50%      { transform: rotate(225deg) translate(4px, 4px); }
}

/* ---- Ground title ----
   Sits in the clearing between the two stands of trees, with the two
   lines on DIFFERENT layers: "SPONSORS" in front of the foliage, "Fueled
   by You" behind it so the grass clips its base.

   The wrapper deliberately has no z-index of its own. A positioned
   element with z-index:auto does not create a stacking context, which
   means these two children can be layered independently against the
   foliage outside — set a z-index on the wrapper and they would both be
   trapped inside it, unable to straddle that layer.

   Positioned in percentages of .sky so it stays locked to the treeline
   at any window width, and fades out on --launch alongside the scroll
   cue once the climb begins. */
.ground-title {
  position: absolute;
  left: 50%;
  bottom: 12.5%;
  transform: translateX(-50%);
  width: min(72vw, 620px);
  text-align: center;
  pointer-events: none;
  /* Same fade as the scroll cue — holds while the swamp is on screen. */
  opacity: calc(1 - var(--ground, 0));
}
/* In front of the foliage (7). */
.ground-title h1 {
  position: relative;
  z-index: 8;
  font-family: var(--font-display);
  font-weight: 900;
  font-size: clamp(1.8rem, 5.6vw, 4.2rem);
  letter-spacing: .08em;
  line-height: 1;
  color: var(--text);
  text-shadow: 0 6px 40px rgba(0, 0, 0, .45);
}
/* The subtitle is drawn TWICE, on either side of the foliage layer:
   .ground-sub sits behind it (z-index 5) and .ground-sub-ghost sits in
   front (z-index 8) at low opacity.

   Where the trees don't cover the line, the back copy reads as fully
   solid and the faint copy on top barely registers. Where the grass does
   cover it, only the faint copy survives — so the overlap alone appears
   semi-transparent, which is not something a single element could do,
   since opacity applies to a whole element rather than to just the part
   another layer happens to cross. */
.ground-sub-wrap {
  position: relative;
  display: block;
  margin-top: .6rem;
}
.ground-sub,
.ground-sub-ghost {
  display: block;
  font-family: var(--font-display);
  font-size: clamp(.9rem, 2vw, 1.4rem);
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--swamp-green);
}
/* Behind the foliage (7), above the background and rocket (4). */
.ground-sub {
  position: relative;
  z-index: 5;
}
/* In front of the foliage, faint — this is the part that shows through
   the grass. Raise the opacity for a stronger show-through. */
.ground-sub-ghost {
  position: absolute;
  inset: 0;
  z-index: 8;
  opacity: .3;
  pointer-events: none;
}

/* ---- Foreground swamp ----
   A transparent copy of the foliage and water that already sits baked
   into the bottom of the background art, layered on top of the rocket so
   the vehicle is hidden on the pad.

   Anchored to the ARTWORK's bottom edge, not the crop box's: .sky trims
   the last 2% of the image, so bottom:0 here would ride ~2% high and the
   two copies of the foliage would show as doubled. -2.04% is that same
   2% expressed against .sky's own (98%-tall) height. */
.sky-foreground {
  position: absolute;
  left: 0;
  bottom: -2.04%;
  width: 100%;
  height: auto;
  z-index: 7;
  pointer-events: none;
}

/* ---- Top sponsors spotlight ----
   Sits just below the ground-title, in the same initial view a visitor
   lands on before they ever scroll — so the biggest sponsors get seen
   without anyone having to climb the whole ladder first. Same
   bottom-percentage-of-.sky-art anchoring and the same --ground fade as
   .ground-title, so the two rise and fall together as one unit. */
.top-sponsors {
  position: absolute;
  left: 50%;
  bottom: 10.5%;
  transform: translateX(-50%);
  width: min(90vw, 640px);
  text-align: center;
  /* Behind the foliage (7), same depth as .ground-sub — so hanging moss
     and grass can cross in front of it instead of it floating on top of
     the whole scene like a UI overlay. */
  z-index: 5;
  opacity: calc(1 - var(--ground, 0));
  pointer-events: none;
}
.top-sponsors-label {
  display: block;
  font-family: var(--font-display);
  font-size: .68rem;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: var(--text);
  text-shadow: 0 2px 14px rgba(0, 0, 0, .55);
  margin-top: .7rem;
}
.top-sponsors-row {
  display: flex;
  justify-content: center;
  gap: clamp(.3rem, 1vw, .6rem);
}
.top-sponsors-row .sponsor-spot.is-filled {
  flex: 0 1 110px;
  min-height: 0;
}
.top-sponsors-row .sponsor-spot.is-filled img { max-height: 34px; }
.top-sponsors-row .sponsor-spot-name { font-size: .58rem; }

@media (max-width: 860px) {
  /* The whole painted ground scene is gone on mobile (.sky-art display:
     none above), so this has nothing left to anchor to — it gets its own
     plain-flow placement instead, directly under "Fueled by You". */
  .top-sponsors {
    position: static;
    width: 100%;
    transform: none;
    opacity: 1;
    /* Clears the fixed .nav (which otherwise overlaps whatever sits first
       in flow here — fine for the hero text it's designed to wash over,
       not for small logo marks that just go illegible under it). */
    margin-top: 6.5rem;
    padding: 0 var(--edge-pad);
  }
}

/* ==========================================================================
   DONATE PAGE
   Single full-bleed photo hero, dark scrim underneath the copy for
   legibility anywhere on the frame. Everything is one centered column —
   this page has exactly one job (get the donor to the UF giving page,
   and make sure they know to email their receipt), so there is
   deliberately nothing else on it to dilute that.
   ========================================================================== */
.donate-hero {
  position: relative;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  padding: 7rem var(--edge-pad) 4rem;
}
.donate-hero-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
}
/* Darkest at the edges (where the nav and page bottom sit) and through
   the middle where the text lives; the sky in the source photo is
   brightest at the top, so this leans harder on the top scrim than a
   symmetric vignette would. */
.donate-hero-scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  background:
    linear-gradient(to bottom, rgba(0,0,0,.75) 0%, rgba(0,0,0,.45) 30%, rgba(0,0,0,.55) 70%, rgba(0,0,0,.85) 100%);
}
.donate-inner {
  position: relative;
  z-index: 2;
  max-width: 640px;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.donate-title {
  font-family: var(--font-hero);
  font-weight: 400;
  font-size: clamp(3rem, 8vw, 6rem);
  line-height: 1;
  letter-spacing: .01em;
  color: var(--text);
  margin: .4rem 0 1.2rem;
  text-shadow: 0 0 40px rgba(255, 255, 255, .3), 0 4px 24px rgba(0,0,0,.6);
}
.donate-sub {
  color: var(--text-dim);
  font-size: 1.05rem;
  max-width: 46ch;
  text-shadow: 0 2px 12px rgba(0,0,0,.7);
}

/* The receipt requirement — this is the one thing on the page that MUST
   NOT be missed, so it's its own loud, bordered block instead of a line
   of body copy: heavy orange border + glow, bold label, and the
   "otherwise" consequence called out in orange on its own line. */
.donate-warning {
  margin-top: 2.5rem;
  width: 100%;
  border: 2px solid var(--uf-orange);
  border-radius: var(--radius);
  background: rgba(0, 0, 0, .55);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  padding: 1.6rem 1.8rem;
  box-shadow: 0 0 0 1px rgba(211, 57, 1, .25), 0 0 40px rgba(211, 57, 1, .35);
}
.donate-warning-label {
  display: block;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--uf-orange-light);
  margin-bottom: .7rem;
}
.donate-warning p {
  color: var(--text);
  font-size: 1.05rem;
  line-height: 1.5;
}
.donate-warning a {
  color: var(--uf-orange-light);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.donate-warning-otherwise {
  margin-top: .6rem;
  font-weight: 700;
  color: var(--uf-orange-light);
}

/* The CTA — big enough to be the obvious next action on the page, not
   just another button among several. */
.donate-cta {
  display: inline-block;
  margin-top: 2.2rem;
  width: 100%;
  padding: 1.5rem 2rem;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.3rem;
  letter-spacing: .04em;
  text-transform: uppercase;
  text-align: center;
  color: var(--black);
  background: linear-gradient(135deg, var(--uf-orange-light), var(--uf-orange));
  border-radius: var(--radius);
  box-shadow: 0 8px 30px rgba(211, 57, 1, .45);
  transition: transform .2s cubic-bezier(.2,.7,.2,1), box-shadow .2s ease;
}
.donate-cta:hover {
  transform: translateY(-3px);
  box-shadow: 0 14px 40px rgba(211, 57, 1, .6);
}
.donate-thanks {
  margin-top: 1.6rem;
  font-family: var(--font-display);
  font-size: 1rem;
  letter-spacing: .04em;
  color: var(--text-dim);
  text-shadow: 0 2px 12px rgba(0,0,0,.7);
}

/* ==========================================================================
   IREC PAGE
   ========================================================================== */
.irec-page { background: var(--black); }

/* ---- The critical sequence ----
   Every asset is exported on the same 3300x2550 canvas, so they are all
   stacked at inset:0 inside a box of that exact ratio. Nothing needs
   positioning: they stay in register with each other automatically, at
   any window width. */
/* Sized to the viewport, not to its content: the whole sequence has to be
   on screen at load with nothing to scroll to. Fixing the height at 100vh
   (rather than min-height) stops the stage pushing the page taller than
   the window. */
.irec-hero {
  position: relative;
  /* Above .irec-body, so the outline in that section can never paint over
     the sequence even if it overflows its own box. */
  z-index: 2;
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* Trimmed right down: every rem of padding is height the artwork
     cannot use. */
  padding: 4.5rem var(--edge-pad) 2rem;
  overflow: hidden;
}
.irec-titles {
  /* Positioned + given a real z-index so it wins the stacking order
     against .irec-stage below: .irec-stage is also `position:relative`,
     and a positioned element always paints above a plain static one
     regardless of DOM order UNLESS the static one is lifted out too —
     needed now that the negative margin below deliberately overlaps the
     two, and the title has to stay legible on top of the trajectory
     art, not get buried under it. */
  position: relative;
  z-index: 3;
  text-align: center;
  margin-top: 3vh;
  margin-bottom: -9vh;
}
.irec-title {
  font-family: var(--font-hero);
  /* Explicitly 400. An h1 is bold by default, and Nasalization ships no
     bold cut — so the browser synthesises one by smearing the glyphs,
     which is why it looked heavy and slightly mushy in Chrome. */
  font-weight: 400;
  /* Big and grand on purpose — this is the page's title card, and it's
     meant to overlay the top of the trajectory art below it, not just
     sit politely above it. */
  font-size: clamp(3.5rem, 13vh, 10rem);
  line-height: 1;
  letter-spacing: .02em;
  color: var(--text);
  text-shadow: 0 0 40px rgba(255, 255, 255, .35), 0 0 90px rgba(255, 255, 255, .18);
}
.irec-subtitle {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(.75rem, 1.6vh, 1.2rem);
  letter-spacing: .06em;
  color: var(--uf-orange);
}

/* Driven by HEIGHT first, with width following from the aspect ratio —
   the opposite of the usual approach. Sizing by width would let a wide
   window make the artwork taller than the screen, which is exactly what
   forced scrolling. The vw term keeps it from overflowing sideways on
   short, wide windows. */
.irec-stage {
  position: relative;
  height: min(90vh, 73vw);
  width: auto;
  aspect-ratio: 3300 / 2550;
}
.seq {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0;
}

/* Point markers snap in; the rockets take noticeably longer, so each
   vehicle arrives as its own beat before its trajectory draws. */
.seq-point  { animation: irec-fade .5s ease forwards; }
.seq-rocket { animation: irec-fade 1.4s ease forwards; }

/* The trajectories are "drawn" with a left-to-right clip wipe. The arcs
   run monotonically left to right — launch at the left, apogee in the
   middle, landing at the right — so a horizontal wipe uncovers each one
   in true flight order. inset() takes the amount to cut from each edge,
   so it starts cutting 100% off the right and ends cutting none. */
.seq-path {
  opacity: 1;
  clip-path: inset(0 100% 0 0);
  animation: irec-draw 1.2s cubic-bezier(.4, 0, .3, 1) forwards;
}
@keyframes irec-draw {
  to { clip-path: inset(0 0 0 0); }
}
@keyframes irec-fade {
  to { opacity: 1; }
}

/* ---- Sequence timing ----
   Three flights in turn. Within each: rocket, then trajectory, then the
   four points along it, then the label. Delays are absolute so the whole
   run reads as one continuous sequence rather than three loops. */
.s1-rocket  { animation-delay: .3s; }
.s1-path    { animation-delay: .9s; }
.s1-ascent  { animation-delay: 2.1s; }
.s1-apogee  { animation-delay: 2.4s; }
.s1-descent { animation-delay: 2.7s; }
.s1-landed  { animation-delay: 3.0s; }
.label-1    { animation-delay: 3.3s; }

.s2-rocket  { animation-delay: 3.7s; }
.s2-path    { animation-delay: 4.3s; }
.s2-ascent  { animation-delay: 5.5s; }
.s2-apogee  { animation-delay: 5.8s; }
.s2-descent { animation-delay: 6.1s; }
.s2-landed  { animation-delay: 6.4s; }
.label-2    { animation-delay: 6.7s; }

.s3-rocket  { animation-delay: 7.1s; }
.s3-path    { animation-delay: 7.7s; }
.s3-ascent  { animation-delay: 8.9s; }
.s3-apogee  { animation-delay: 9.2s; }
.s3-descent { animation-delay: 9.5s; }
.s3-landed  { animation-delay: 9.8s; }
.label-3    { animation-delay: 10.1s; }

/* Labels are live text, not the exported artwork, so they stay crisp and
   editable. Positioned as percentages of the same canvas. */
.flight-label {
  position: absolute;
  transform: translateX(-50%);
  text-align: center;
  opacity: 0;
  /* Longhand, NOT the `animation` shorthand. This rule sits after the
     .label-1/2/3 delays in the file, and the shorthand resets every
     unspecified sub-property — including animation-delay — so it was
     wiping those delays and firing all three labels at once, on top of
     each other, the moment the page loaded. */
  animation-name: irec-fade;
  animation-duration: .5s;
  animation-timing-function: ease;
  animation-fill-mode: forwards;
  /* Each label gets its own column of space so the three can never
     collide, however long the wording gets. */
  width: 17%;
}
.label-1 { left: 48.4%; top: 76%; }
.label-2 { left: 66.6%; top: 76%; }
.label-3 { left: 86.4%; top: 76%; }
.flight-when {
  display: block;
  font-family: var(--font-display);
  font-size: clamp(.5rem, .85vw, .8rem);
  font-weight: 700;
  letter-spacing: .08em;
  color: var(--uf-orange);
  margin-bottom: .3em;
}
.flight-what {
  display: block;
  font-family: var(--font-display);
  font-size: clamp(.7rem, 1.6vw, 1.5rem);
  line-height: 1.15;
  color: var(--text);
}

.irec-cue { position: absolute; bottom: 1.2rem; }

/* Subsystems still awaiting real copy. The placeholder is visible while
   building so the gaps are obvious, and disappears the moment text is
   written into the element. */
.needs-copy::before {
  content: "Description to come";
  font-style: italic;
  color: rgba(255, 255, 255, .28);
}

/* ---- IREC logo strip ----
   Simple centered mark between the flight sequence and the goals. Width
   is capped rather than fluid-scaled to the viewport — it's a wide
   horizontal wordmark, and letting it stretch with the page would make
   it dominate the section on large screens. */
.irec-logo-strip {
  position: relative;
  z-index: 1;
  display: flex;
  justify-content: center;
  padding: 2rem var(--edge-pad);
}
.irec-logo-strip img {
  width: min(360px, 60vw);
  height: auto;
}

/* ---- The goals ----
   Three columns between the flight sequence and the vehicle breakdown.
   align-items:start keeps each block anchored to the top, so the three
   headings sit on one line even though the paragraphs run to different
   lengths. */
.irec-goals {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: start;
  max-width: 1400px;
  margin-inline: auto;
  padding: var(--section-pad) var(--edge-pad);
}
/* Each column's text follows its own position in the row: the left block
   stays left-aligned, the middle centres, the right block aligns right.
   Scoped to .irec-goals with nth-child so it applies to this section
   only, and inherits down to the eyebrow, heading and body together. */
.irec-goals .irec-goal:nth-child(2) { text-align: center; }
.irec-goals .irec-goal:nth-child(3) { text-align: right; }

.irec-goal .eyebrow { display: block; margin-bottom: .3rem; }
.irec-goal h2 {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(1.5rem, 3vw, 2.5rem);
  line-height: 1.05;
  margin-bottom: .9rem;
  color: var(--text);
}
.irec-goal p {
  color: var(--chrome);
  font-size: clamp(.85rem, 1.1vw, .96rem);
  line-height: 1.65;
}

@media (max-width: 860px) {
  .irec-goals { grid-template-columns: 1fr; }
}

/* ---- The build: rocket outline with subsystems alternating down it ----
   The outline art itself is the full 2550x6000 canvas, but .irec-body is
   deliberately given MORE height than that ratio (2550/7200) — the tight
   9%-pitch spacing between the 11 subsystem rows left no slack for their
   text to wrap onto extra lines at some viewport widths, which made
   neighboring rows collide (e.g. Controls Mechanical into Testing). The
   extra ~20% height stretches every row's percentage-based gap into more
   actual pixels, fixing that everywhere at once instead of shuffling
   individual rows. */
.irec-body {
  /* position:relative is load-bearing — it makes this the containing
     block for the absolutely positioned outline and subsystem nodes
     inside it. Without it they resolve against the page instead, and the
     outline balloons to the height of the whole document and paints
     across the hero. overflow:hidden is a second guard on the same
     failure. */
  position: relative;
  z-index: 1;
  overflow: hidden;
  width: 100%;
  max-width: 1400px;
  margin-inline: auto;
  aspect-ratio: 2550 / 7200;
}
.irec-outline {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Faded and fixed in place — this reads as a backdrop behind the
     subsystem copy, not an element competing for attention. */
  opacity: .2;
}
.irec-outline img {
  width: 100%;
  height: 100%;
  /* object-position:top keeps the art pinned to where the top few rows
     expect it — .irec-body is now taller than the art's own ratio, and
     without this the leftover height would split evenly above and below,
     drifting the outline out of register with every row instead of just
     leaving slack at the bottom where it's harmless. */
  object-fit: contain;
  object-position: top;
}

.irec-node {
  position: absolute;
  width: 30%;
  opacity: 0;
  transform: translateY(20px);
  transition: opacity .7s ease, transform .7s cubic-bezier(.2, .7, .2, 1);
}
.irec-node.in-view { opacity: 1; transform: none; }
/* Left-hand nodes sit against the left margin; right-hand ones mirror it
   and align their text to the right, so both read as running outward from
   the rocket in the centre. */
.irec-node.is-left  { left: 1%; text-align: left; }
.irec-node.is-right { right: 1%; text-align: right; }

.irec-node h3 {
  font-family: var(--font-display);
  font-size: clamp(.85rem, 1.5vw, 1.35rem);
  font-weight: 500;
  color: var(--text);
  margin-bottom: .5em;
}
.irec-node p {
  font-size: clamp(.62rem, .95vw, .85rem);
  line-height: 1.55;
  color: var(--chrome);
}
/* Imagery sits OPPOSITE its text, on the far side of the rocket, and
   shares the same `top` so the pair reads as one row across the
   airframe. Positioned exactly like .irec-node so the two always line
   up. */
.irec-photo {
  position: absolute;
  width: 26%;
  aspect-ratio: 4 / 3;
  border-radius: var(--radius);
  border: 1px dashed rgba(255, 255, 255, .2);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: .6rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .3);
  opacity: 0;
  transform: translateY(20px);
  transition: opacity .7s ease, transform .7s cubic-bezier(.2, .7, .2, 1);
}
.irec-photo.in-view { opacity: 1; transform: none; }
.irec-photo.is-left  { left: 3%; }
.irec-photo.is-right { right: 3%; }
.irec-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: var(--radius);
}

@media (max-width: 860px) {
  /* The outline spine does not survive a narrow screen, so the nodes drop
     into a single column and the rocket becomes a faint backdrop. */
  .irec-body { aspect-ratio: auto; padding: var(--section-pad) var(--edge-pad); }
  .irec-node {
    position: static;
    width: 100%;
    margin-bottom: 3rem;
    text-align: left;
  }
  .irec-node.is-right { text-align: left; }
  /* .irec-photo was left position:absolute here, still using its
     desktop `top` percentage against .irec-body's now content-driven
     (not fixed-aspect) height — so it landed at an effectively random
     point over whichever text happened to be there. Static + full width
     puts each photo placeholder right after the node it belongs to. */
  .irec-photo {
    position: static;
    width: 100%;
    max-width: 340px;
    margin: -1.5rem 0 3rem;
  }
}

/* ==========================================================================
   HISTORY PAGE
   ========================================================================== */
.history-page { background: var(--black); }

.history-hero {
  position: relative;
  z-index: 1;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: calc(var(--section-pad) + 2rem) var(--edge-pad) var(--section-pad);
}
.history-titles { margin-bottom: clamp(1.5rem, 4vh, 3rem); }
.history-title {
  font-family: var(--font-hero);
  /* Explicit 400: an h1 is bold by default and Nasalization has no bold
     cut, so the browser would synthesise one and smear the glyphs. */
  font-weight: 400;
  font-size: clamp(2.6rem, 8vw, 6.5rem);
  line-height: 1;
  letter-spacing: .02em;
  color: var(--text);
  text-shadow: 0 0 40px rgba(255, 255, 255, .3), 0 0 90px rgba(255, 255, 255, .15);
}
.history-subtitle {
  margin-top: .8rem;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(.85rem, 2vw, 1.4rem);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--uf-orange);
}

/* The garden runs wide — it is the point of the page, so it is given the
   full width rather than being boxed into the usual content column. */
.rocket-garden {
  width: min(100%, 1600px);
  height: auto;
  max-width: none;
}

.history-note {
  margin-top: clamp(1.5rem, 4vh, 3rem);
  max-width: 46ch;
  color: var(--chrome);
  font-size: clamp(.9rem, 1.3vw, 1.05rem);
  line-height: 1.7;
}

/* ==========================================================================
   ALUMNI PAGE
   Same minimal treatment as History: a centered hero card with nothing
   below it yet -- both pages are placeholders for content that's coming
   later (a scrollable rocket garden there, featured alumni placements
   here), not stripped-down versions of a finished page.
   ========================================================================== */
.alumni-page { background: var(--black); }
.alumni-hero {
  position: relative;
  z-index: 1;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: calc(var(--section-pad) + 2rem) var(--edge-pad) var(--section-pad);
}
.alumni-titles { margin-bottom: clamp(1.5rem, 4vh, 3rem); }
.alumni-title {
  font-family: var(--font-hero);
  font-weight: 400;
  font-size: clamp(2.6rem, 8vw, 6.5rem);
  line-height: 1;
  letter-spacing: .02em;
  color: var(--text);
  text-shadow: 0 0 40px rgba(255, 255, 255, .3), 0 0 90px rgba(255, 255, 255, .15);
}
.alumni-subtitle {
  margin-top: .8rem;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(.85rem, 2vw, 1.4rem);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--uf-orange);
}
.alumni-note {
  margin-top: clamp(1.5rem, 4vh, 3rem);
  max-width: 46ch;
  color: var(--chrome);
  font-size: clamp(.9rem, 1.3vw, 1.05rem);
  line-height: 1.7;
}

/* ==========================================================================
   SRAD PAGE
   Same minimal placeholder treatment as Alumni/History, plus one real
   photo (borrowed from the IREC Controls Mechanical subsystem, which is
   itself SRAD hardware) standing in for the display that's coming later.
   ========================================================================== */
.srad-page { background: var(--black); }
.srad-hero {
  position: relative;
  z-index: 1;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: calc(var(--section-pad) + 2rem) var(--edge-pad) var(--section-pad);
}
.srad-titles { margin-bottom: clamp(1.5rem, 4vh, 3rem); }
.srad-title {
  font-family: var(--font-hero);
  font-weight: 400;
  font-size: clamp(2.6rem, 8vw, 6.5rem);
  line-height: 1;
  letter-spacing: .02em;
  color: var(--text);
  text-shadow: 0 0 40px rgba(255, 255, 255, .3), 0 0 90px rgba(255, 255, 255, .15);
}
.srad-subtitle {
  margin-top: .8rem;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(.85rem, 2vw, 1.4rem);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--uf-orange);
}
.srad-photo {
  margin-top: clamp(2rem, 5vh, 3.5rem);
  width: min(100%, 640px);
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--radius);
}
.srad-note {
  margin-top: clamp(1.5rem, 4vh, 3rem);
  max-width: 46ch;
  color: var(--chrome);
  font-size: clamp(.9rem, 1.3vw, 1.05rem);
  line-height: 1.7;
}

/* ==========================================================================
   RESPONSIVE
   ========================================================================== */
@media (max-width: 860px) {
  .nav-links { display: none; }
  .hero-word { font-size: clamp(2.6rem, 13vw, 5rem); }
  .word-swamp { transform: translateX(calc((var(--split-p) * -3vw) + (var(--overtake-p) * -55vw))); }
  .word-launch { transform: translateX(calc((var(--split-p) * 3vw) + (var(--overtake-p) * 55vw))); }
  .rocket-rig { width: clamp(24px, 6vw, 40px); height: clamp(90px, 22vw, 140px); }
  .split { grid-template-columns: 1fr; }
  .merch .section-inner { grid-template-columns: 1fr; text-align: center; }
  .merch-icon { margin-inline: auto; }
  .atmosphere { grid-template-columns: repeat(2, 1fr); }
  .footer-grid { grid-template-columns: 1fr 1fr; }
  .org-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .org-tier-2.org-grid .org-card { grid-column: span 2 !important; }
  .org-detail { max-width: min(80vw, 260px); }
  .org-tier-3 { flex-direction: column; align-items: center; gap: 2.6rem; }
  .org-lead-group { max-width: 320px; width: 100%; }
  .org-vine-exec { max-width: 412px; }

  /* Org chart, mobile: the hand-drawn vine/fork connectors read as
     scroll-triggered draw-ins tuned for a wide desktop layout, and once
     every tier below stretches or wraps to fit a phone width the lines
     no longer land on the cards they're meant to connect. Simpler to
     drop them outright here and let the tiers speak for themselves --
     rows of tiles you swipe through, labelled by the group heading
     already sitting above each one, rather than a connector trying (and
     failing) to point at eight things going down a single column. */
  .org-vine-wrap,
  .org-fork-sm,
  .org-lead-spine,
  .org-ops-spine {
    display: none !important;
  }

  /* President (org-tier-0) is already a single centered card, untouched.
     Chief Engineers stay centered and stacked -- explicitly asked to
     read as their own row, not folded into a swipeable strip with
     everyone else -- with the nudges that spread them across a 3-column
     desktop row cancelled out. */
  .org-tier-2 {
    flex-direction: column;
    align-items: center;
    margin-top: 2.6rem;
  }
  .org-chief-slot {
    /* Was flex:1 1 0 for three equal-width columns in a row -- in
       .org-tier-2's new column direction that basis applies to height
       instead, which would force all three chiefs to match whichever
       one is tallest. Natural height, full width, centered. */
    flex: 0 0 auto;
    width: 100%;
  }
  .org-chief-slot:first-child,
  .org-chief-slot:last-child {
    transform: none;
  }

  /* Everyone else -- the officers under the President, each Chief's own
     leads, Ops & Outreach, and the Advisors -- becomes a single
     horizontally-scrolling row of tiles instead of wrapping to a second
     line or a 2-column grid. flex:0 0 auto stops the cards from
     shrinking to fit; the row scrolls instead. */
  .org-tier-1,
  .org-tier-ops,
  .org-tier-advisor,
  .org-lead-grid {
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-start;
    overflow-x: auto;
    overflow-y: visible;
    -webkit-overflow-scrolling: touch;
    width: 100%;
    max-width: 100%;
    padding: .3rem .3rem 1rem;
    margin-inline: 0;
  }
  .org-tier-1 .org-card,
  .org-tier-ops .org-card,
  .org-tier-advisor .org-card,
  .org-lead-grid .org-card {
    flex: 0 0 auto;
    width: 148px;
  }

  /* Testing Lead Engineer is a role shared between Vehicle and Payload,
     drawn on desktop with position:absolute + percentage coordinates so
     it visually sits between their two spines. Those coordinates are
     tuned for the desktop layout and land somewhere arbitrary once
     everything above it reflows to a phone width -- it was overlapping
     Development's lead row. It's the last thing in .org-tier-3's DOM
     order (after all three lead-groups), so falling back to a plain
     centered block just places it at the end of the stack instead of
     literally between two specific groups -- not a perfect match for
     "shared", but correct and readable, which the absolute position
     wasn't on a narrow screen. */
  .org-shared-lead {
    position: static;
    transform: none;
    display: flex;
    justify-content: center;
    margin-top: 1rem;
  }
}
@media (max-width: 480px) {
  .atmosphere { grid-template-columns: 1fr; }
  .footer-grid { grid-template-columns: 1fr; }
  .org-card, .org-card-sm { width: 128px; }
  .org-grid { grid-template-columns: 1fr; }
  .org-tier-2.org-grid .org-card { grid-column: 1 !important; }
  /* Kept at .org-lead-grid's own default 2-column grid (matching how
     Operations & Outreach already displays) rather than collapsing to
     one — the vine's crossbars are drawn assuming two leads per row, so
     a single column left them pointing at empty space beside each card
     instead of at it, and it read as needlessly tall besides. */
}

/* ==========================================================================
   PRINT / PDF EXPORT
   Lets the team page be saved straight to PDF from the browser
   (Ctrl/Cmd-P -> Save as PDF, with "Background graphics" enabled) while
   keeping the black starry look of the live page. Screen styles are
   untouched — everything here is inside @media print.

   Three things have to be corrected for paper:
   1. The whole chart is revealed by a scroll observer, and its cards/
      vines start at opacity 0 / stroke-dashoffset. Printing doesn't
      scroll, so anything not already revealed would come out blank.
      Every one of those start states is forced to its finished value.
   2. Animations and transitions are disabled outright, so nothing prints
      mid-fade.
   3. position:fixed layers (the starfield, the nav, the glitch overlays)
      are unreliable in print — most engines paint them on page one only.
      The starfield becomes absolute so it covers the full flow, the nav
      goes static, and the glitch/scanline overlays are dropped since they
      would just muddy the page.
   ========================================================================== */
@media print {
  /* A3 landscape: the chiefs row and subteam grid span ~1300px, which
     needs a wide sheet to avoid being scaled down to illegibility. */
  @page {
    size: 420mm 297mm;
    margin: 10mm;
  }

  /* Browsers strip backgrounds when printing unless told not to — without
     this the "black starry" page comes out white. */
  html, body {
    background: var(--black) !important;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }

  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
  }

  /* Starfield: fixed -> absolute so it tiles down the whole document
     rather than only the first sheet. */
  .starfield {
    position: absolute !important;
    inset: 0 !important;
    height: 100% !important;
    opacity: .5 !important;
    transform: none !important;
  }
  .starfield::before { opacity: .88 !important; transform: none !important; }
  .starfield::after  { opacity: .95 !important; transform: none !important; }

  /* Screen-only chrome that has no place in a printed org chart. */
  body::before,
  body::after,
  .nav,
  .team-join,
  footer,
  .scroll-cue { display: none !important; }

  /* Force the fully-revealed state of every staged element. */
  .org-card {
    background: var(--bg-elevated) !important;
    border-color: var(--line) !important;
  }
  .org-tier-0 .org-card,
  .org-tier-1 .org-card,
  .org-tier-2 .org-card {
    border-color: rgba(57, 255, 138, .45) !important;
    box-shadow: none !important;
  }
  .org-avatar,
  .org-name,
  .org-title { opacity: 1 !important; }
  .org-grid-label,
  .org-branch-label { opacity: .8 !important; }
  .org-exec-label { opacity: .85 !important; }
  .org-fork-path { stroke-dashoffset: 0 !important; }
  .org-fork-node { opacity: .9 !important; transform: scale(1) !important; }
  /* The one-shot glimmer sweep would otherwise freeze mid-page. */
  .org-tier-0 .org-card::after,
  .org-tier-1 .org-card::after,
  .org-tier-2 .org-card::after { display: none !important; }
  /* Expanded-card detail panels stay collapsed — the printout is the
     chart, not one opened profile. */
  .org-detail { display: none !important; }

  /* Keep tiers and cards from being sliced across a page boundary. */
  .org-tier,
  .org-lead-group,
  .org-card,
  .team-hero { break-inside: avoid; page-break-inside: avoid; }

  .team-hero { padding-top: 0 !important; }
  .org-chart { padding-bottom: 0 !important; }
}

/* ==========================================================================
   MOBILE NAV
   Gated on aspect ratio, not a fixed width breakpoint: the hamburger
   takes over whenever the viewport is taller than it is wide, so a tall
   narrow desktop window gets exactly the same treatment as a phone in
   portrait. Below this width the header bar simply doesn't have room to
   lay .nav-links out horizontally next to the brand and donate button.
   Placed at the end of the file (not next to .nav-links/.nav-menu
   directly above) so it wins the cascade over their plain, unconditional
   desktop rules — a media query doesn't add specificity, only source
   order decides when two rules tie.
   ========================================================================== */
@media (max-aspect-ratio: 1/1) {
  .nav-toggle { display: flex; }

  /* The older `@media (max-width: 860px)` block above sets
     `.nav-links { display: none; }` — a leftover from before any
     hamburger menu existed, back when there was simply nowhere for the
     links to go on mobile. Equal specificity, so whichever rule comes
     later in the file wins for `display`; this block runs after that
     one, but only overrides properties it explicitly sets, and it
     never touched `display` before now — so that old `none` was
     winning and the panel could never render at all, regardless of the
     .is-open class, aria-expanded, or anything else toggling correctly. */
  .nav-links {
    display: flex;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 99;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    background: var(--black);
    border-bottom: 1px solid var(--line);
    padding: 5.5rem 1.5rem 2rem;
    max-height: 100vh;
    overflow-y: auto;
    transform: translateY(-100%);
    opacity: 0;
    visibility: hidden;
    transition: transform .35s cubic-bezier(.2,.7,.2,1), opacity .3s ease,
                visibility 0s linear .35s;
  }
  .nav-links.is-open {
    transform: translateY(0);
    opacity: 1;
    visibility: visible;
    transition: transform .35s cubic-bezier(.2,.7,.2,1), opacity .3s ease,
                visibility 0s linear 0s;
  }
  .nav-links li { border-bottom: 1px solid var(--line); }
  .nav-links a {
    display: block;
    padding: .95rem 0;
    font-size: 1.05rem;
  }
  /* The underline-sweep hover effect doesn't read at full width in a
     stacked list — it's a desktop hover affordance, not needed once
     every link is already a full-width tap target. */
  .nav-links a::after { display: none; }

  /* Mission's submenu becomes a plain, always-visible nested list rather
     than a hover dropdown — there's no hover on touch, and tapping the
     parent link would navigate away before a hover menu ever had a
     chance to appear. */
  .nav-has-menu > a::before { display: none; }
  .nav-menu {
    position: static;
    transform: none;
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    padding: .25rem 0 .5rem 1.25rem;
    gap: 0;
  }
  .nav-menu a { padding: .7rem 0; font-size: .95rem; color: var(--text-dim); }

  .btn-donate img { height: 30px; }
}

/* ==========================================================================
   DEVELOPMENT PAGE
   Rebuilt as two separate crop windows into the same source artwork
   rather than one continuous 4250x7500 canvas -- see the long comment in
   development.html above <main> for why (short version: a single
   fixed-timeline reveal across a canvas several screens tall falls out
   of sync with actual scroll position; splitting into shorter,
   independently-triggered sections keeps each one small enough that a
   one-shot .reveal-up stays in sync).

   Both .dev-hero-canvas and .dev-roadmap-canvas hold the FULL set of
   layers at their exact original canvas position (nothing here is
   hand-positioned) -- each is just a differently-sized, differently-
   offset "window" into where that canvas would otherwise be. See the
   worked math in the two crop-window rules below.
   ========================================================================== */
.development-page { background: var(--black); }

.dev-hero {
  position: relative;
  z-index: 2;
  text-align: center;
  padding: 7rem var(--edge-pad) 1rem;
}
.dev-title {
  font-family: var(--font-hero);
  font-weight: 400;
  /* vw-based, not vh-based like .irec-title -- "Development" is eleven
     characters, and sizing off viewport HEIGHT let it run wider than a
     narrow-but-tall phone screen before line-height ever noticed. Tying
     it to width instead keeps the word inside the viewport at any
     aspect ratio. */
  font-size: clamp(2.2rem, 9vw, 7rem);
  line-height: 1;
  letter-spacing: .02em;
  color: var(--text);
  text-shadow: 0 0 40px rgba(255, 255, 255, .35), 0 0 90px rgba(255, 255, 255, .18);
  margin-bottom: 1rem;
}
.dev-subtitle {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(.85rem, 1.8vw, 1.15rem);
  letter-spacing: .04em;
  line-height: 1.6;
  color: var(--chrome);
}

.dev-layer, .dev-star-group {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
}
.dev-layer img, .dev-star { width: 100%; height: 100%; }
.dev-star { position: absolute; inset: 0; }

/* ---- Hero crop: source canvas rows 8%-38% (stars/gator, the full
   Kármán line, the rocket) ----
   .dev-hero-stage is the visible window: overflow:hidden, sized to just
   those 30 percentage-points of the source's 7500px height. Full-bleed
   (no max-width) so it fills the screen edge to edge rather than sitting
   boxed in a centered column with dead space either side on a wide
   monitor.
   .dev-hero-canvas inside it is the full-size source canvas (same
   4250/7500 ratio every layer was exported at) shifted up by
   -(8 / 30 * 100)% so that source row 8% lands at the window's own
   row 0%. */
.dev-hero-stage {
  position: relative;
  z-index: 1;
  width: 100%;
  overflow: hidden;
  aspect-ratio: 4250 / 2250;
}
.dev-hero-canvas {
  position: absolute;
  left: 0;
  width: 100%;
  aspect-ratio: 4250 / 7500;
  top: -26.67%;
}

@keyframes devStarsCycle {
  0%   { opacity: 0; }
  30%  { opacity: 1; }
  62%  { opacity: 1; }
  100% { opacity: 0; }
}
@keyframes devFadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes devFadeUp {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes devRiseIn {
  from { opacity: 0; transform: translateY(18vh); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes devMarkIn {
  from { opacity: 0; transform: scale(.9); }
  to   { opacity: 1; transform: scale(1); }
}

.dev-gator {
  animation-fill-mode: forwards;
  animation-timing-function: ease;
}
/* Only the stars-into-gator constellation is an animated sequence now --
   stars rise and hold, then the gator (the line-drawing connecting them)
   crossfades in over them. The Kármán line and rocket beneath it are
   static art from here on, just like the roadmap below -- shown plainly
   rather than staged in on their own delay. */
.dev-hero-stage.in-view .dev-star-group { animation: devStarsCycle 1.3s ease forwards; }
.dev-hero-stage.in-view .dev-gator      { animation-name: devFadeUp; animation-duration: .9s; animation-delay: .7s; }
.dev-karman, .dev-rocket { opacity: 1; }

/* Kármán line copy -- dropped below the rocket art entirely (it ends
   by row ~77% of this crop) rather than over its narrow waist: sitting
   right on the rocket's own silhouette read as an overlap, not a "gap",
   at actual reading size. Wide (up to 62ch) so it wraps to fewer, wider
   lines and fits in the remaining ~20 rows of the crop without hitting
   .dev-hero-stage's own overflow:hidden bottom edge. Positioned against
   .dev-hero-stage (the visible crop window), not .dev-hero-canvas (the
   full source canvas), so its `top` is a plain percentage of what the
   reader can actually see. */
.dev-hero-copy {
  position: absolute;
  z-index: 2;
  left: 50%;
  top: 80%;
  transform: translateX(-50%);
  width: min(92%, 74ch);
  text-align: center;
  font-size: clamp(.6rem, .95vw, .8rem);
  line-height: 1.5;
  color: var(--chrome);
  opacity: 1;
}
/* Mobile's copy of the same text -- hidden here, shown instead in the
   mobile media query below, ahead of the first branch (see the markup
   comment in development.html for why it needs its own element rather
   than reusing .dev-hero-copy). */
.dev-hero-copy-mobile { display: none; }

/* ---- Roadmap crop: source canvas rows 38%-86% (the propulsion trunk
   fanning into every leg) ----
   Same crop-window technique, picking up exactly where the hero crop
   left off (38%) so the trunk reads as one continuous shape across the
   two sections, and ending at 86% where the fanned lines actually taper
   off (not 100% -- the source canvas has a stretch of empty space below
   that no layer ever draws into).

   Unlike the hero, this art is a single always-on backdrop rather than
   a timed sequence -- see the file-level comment for why a multi-leg
   timed chain doesn't hold up across a section this size. It gets one
   plain fade-in (.reveal-up, triggered the ordinary way) and then just
   sits there; all of the actual "revealing as you scroll" is carried by
   the text blocks beside it, each with its own ordinary scroll-triggered
   fade-in. */
.dev-roadmap {
  /* Carries the crop window's own aspect-ratio (not .dev-roadmap-stage),
     so it has a real, well-defined height -- that's what .dev-branch
     below positions itself against with plain top:%, and what
     .dev-roadmap-stage fills via inset:0. */
  position: relative;
  z-index: 1;
  width: 100%;
  aspect-ratio: 4250 / 3600;
}
.dev-roadmap-stage {
  position: absolute;
  inset: 0;
  overflow: hidden;
  opacity: 1;
}
.dev-roadmap-canvas {
  position: absolute;
  left: 0;
  width: 100%;
  aspect-ratio: 4250 / 7500;
  top: -79.17%;
}
.dev-roadmap-canvas .dev-layer { opacity: 1; }
/* Nudged left off its native canvas position -- it was sitting close
   enough to ATLAS's text (once that branch itself moved left to clear
   the propulsion trunk, see .dev-branch-atlas) that the two overlapped. */
.dev-mark-atlas { transform: translateX(-6%); }
/* Orpheus's mark natively sits just right of its own text (right:7%-13%,
   vs. the text box's own 60%-84% span) -- fine on its own, but asked to
   read as sitting to the text's LEFT instead (toward the trunk, not the
   page edge, unlike Atlas's mark). Shifted across so its span lands
   ~51%-57%, just clear of the text box's 60% left edge. */
.dev-mark-orpheus { transform: translateX(-23%); }

/* Separate, tightly-cropped copies of just the two marks' own artwork
   (not part of the shared canvas stack above), shown only on mobile --
   see .dev-branch-marked in the mobile block below. Hidden here by
   default so they don't also render (doubled, at the wrong scale) on
   desktop, which already shows both marks via the shared canvas. */
.dev-branch-mark-mobile { display: none; }

/* Copy blocks -- live text rather than exported artwork, positioned as
   percentages of .dev-roadmap (i.e. of the visible crop window, not the
   full source canvas), each beside the leg it belongs to. Revealed by
   the site's ordinary scroll-reveal (script.js' IntersectionObserver):
   each has its own distinct `top`, so unlike the stacked art layers it
   can be told apart by an observer without any extra machinery. */
.dev-branch {
  position: absolute;
  width: 30%;
  opacity: 1;
}
.dev-branch.is-left  { left: 1%; text-align: left; }
.dev-branch.is-right { right: 1%; text-align: right; }
/* ATLAS/Orpheus's own position, as real classes rather than inline
   left/width -- an inline style wins the cascade over literally any
   class selector regardless of specificity or source order, which was
   silently defeating the mobile media query's own .dev-branch rule
   below (width stuck at the desktop value even once position:static
   should have taken over). As classes, source order alone decides, and
   the mobile rule -- appearing later in this file -- correctly wins. */
.dev-branch-atlas   { left: 18%; width: 20%; }
.dev-branch-orpheus { right: 16%; width: 24%; }

.dev-eyebrow {
  display: block;
  font-family: var(--font-display);
  font-size: clamp(.6rem, .9vw, .78rem);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text);
  margin-bottom: .4em;
}
.dev-alt {
  display: block;
  font-size: clamp(.62rem, .95vw, .8rem);
  letter-spacing: .08em;
  color: var(--text-dim);
  margin-bottom: .3em;
}
.dev-branch h3 {
  font-family: var(--font-display);
  font-size: clamp(1rem, 1.8vw, 1.6rem);
  font-weight: 500;
  line-height: 1.15;
  color: var(--text);
  margin-bottom: .3em;
}
.dev-branch p, .dev-note {
  font-size: clamp(.62rem, .95vw, .85rem);
  line-height: 1.55;
  color: var(--chrome);
}

/* Full-bleed staging footage, desaturated so it reads as texture behind
   the roadmap rather than competing in color with it. */
.dev-video-section {
  position: relative;
  width: 100%;
  line-height: 0;
}
.dev-video {
  display: block;
  width: 100%;
  height: 70vh;
  min-height: 340px;
  object-fit: cover;
  filter: grayscale(1) contrast(1.05);
}
/* The nav's own background is a soft gradient tuned for sitting over dark
   hero art (see .nav) -- it isn't enough on its own to keep the nav
   legible over this footage, which is bright daylight aerial video
   unlike anything else behind the nav elsewhere on the site. A top scrim
   here, the same technique .feature-photo::after already uses, keeps the
   logo/links readable without touching the nav itself. */
.dev-video-section::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 140px;
  background: linear-gradient(to bottom, rgba(0, 0, 0, .75), rgba(0, 0, 0, 0));
  z-index: 1;
  pointer-events: none;
}
.dev-video-caption {
  text-align: center;
  font-size: .82rem;
  letter-spacing: .04em;
  color: var(--text-faint);
  padding: 1rem var(--edge-pad) 0;
}
.dev-video-stat {
  text-align: center;
  font-size: .82rem;
  letter-spacing: .04em;
  color: var(--text-faint);
  padding: .3rem var(--edge-pad) 0;
}

@media (max-width: 860px) {
  /* .dev-hero-stage needs no fallback here -- its crop is already a
     short, wide band (2250 tall against a 4250 width, not the old
     single canvas's full 7500-tall portrait), so it keeps its own
     aspect-ratio and just scales down with the viewport like any other
     full-bleed image.

     .dev-roadmap is the one that doesn't survive a narrow screen: five
     text blocks' worth of copy doesn't fit inside its crop's own
     (much shorter, nearly-square) proportions at any readable type
     size. Same fallback .irec-body uses -- it goes back to a
     content-driven height, and the art stretches to fill whatever that
     turns out to be as a faint backdrop (object-fit:contain keeps every
     layer at the same scale so they stay in register with each other,
     the same way a single .irec-outline img does) rather than a
     fixed-to-viewport watermark. The copy itself keeps alternating
     left/right instead of collapsing to one column, so it still reads
     as running down either side of the trunk in the backdrop. */
  .dev-roadmap { aspect-ratio: auto; padding: var(--section-pad) var(--edge-pad); }
  /* .dev-hero-copy: a full paragraph doesn't fit inside the hero crop's
     own short band at any readable size on a narrow screen
     (overflow:hidden on .dev-hero-stage would just clip it), and the
     same copy already lives on mission.html's own Kármán line section
     where mobile readers can read it properly, so it's simplest to drop
     it here rather than fight the crop for room. */
  .dev-hero-copy { display: none; }
  /* .in-view still gets added right on schedule (it's the same generic
     observer everywhere else), but it must NOT win the cascade here --
     the whole point on mobile is a constant faint backdrop, not a fade
     to full opacity. */
  .dev-roadmap-stage,
  .dev-roadmap-stage.in-view {
    position: absolute;
    inset: 0;
    opacity: .16;
    transition: none;
  }
  .dev-roadmap-canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    aspect-ratio: auto;
    top: 0;
  }
  .dev-roadmap-canvas .dev-layer { object-fit: contain; }
  /* Shown here instead of .dev-hero-copy (hidden above) -- centered,
     ahead of the first branch in normal flow so it can't overlap it. */
  .dev-hero-copy-mobile {
    display: block;
    position: relative;
    text-align: center;
    color: var(--chrome);
    font-size: .8rem;
    line-height: 1.6;
    margin: 0 auto 2.4rem;
    max-width: 46ch;
  }
  .dev-branch {
    position: static;
    width: 100%;
    margin-bottom: 3rem;
  }
  .dev-branch.is-right { text-align: right; }
  .dev-mark { display: none; }

  /* ATLAS and Orpheus each carry a standalone, tightly-cropped copy of
     their own constellation mark (the shared-canvas one above is
     hidden along with the rest of .dev-mark) -- shown here, large, on
     the side opposite their own text-align, per explicit request.
     Order in the DOM (not flex-direction) decides left vs. right: ATLAS
     is text-then-mark (text-align:left, mark ends up on the right),
     Orpheus is mark-then-text (text-align:right, mark ends up on the
     left) -- see the HTML comments in development.html. */
  .dev-branch-marked {
    display: flex;
    align-items: center;
    gap: 1.1rem;
  }
  .dev-branch-text { flex: 1; min-width: 0; }
  .dev-branch-mark-mobile {
    display: block;
    width: 34%;
    max-width: 130px;
    flex-shrink: 0;
  }
}

/* ==========================================================================
   FILM GRAIN OVERLAY
   A cheap, seamless noise texture (SVG feTurbulence baked into a data URI,
   no external file) laid over the whole viewport at low opacity. Fixed +
   pointer-events:none so it never blocks clicks or scrolls with the page;
   the position is nudged every frame of a long, slightly-off-loop keyframe
   animation so the grain flickers instead of sitting static, which is what
   actually reads as "grain" instead of just a dirty screen.
   ========================================================================== */
.grain-overlay {
  position: fixed;
  inset: -10%;
  z-index: 999;
  pointer-events: none;
  opacity: .05;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
  animation: grainShift 8s steps(8) infinite;
}
@keyframes grainShift {
  0%   { transform: translate(0, 0); }
  12%  { transform: translate(-2%, 2%); }
  25%  { transform: translate(2%, -1%); }
  37%  { transform: translate(-1%, -2%); }
  50%  { transform: translate(2%, 2%); }
  62%  { transform: translate(-2%, 1%); }
  75%  { transform: translate(1%, -2%); }
  87%  { transform: translate(-1%, 1%); }
  100% { transform: translate(0, 0); }
}
