/* ==========================================================================
   Components
   ========================================================================== */

/* ---- Eyebrow ---------------------------------------------------------- */
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font-family: var(--font-mono);
  font-size: var(--fs-eyebrow);
  font-weight: 500;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--accent);
  margin: 0 0 var(--s-4);
}
/* Robyn, 4 Sep: the lead-in rule reads as an em dash. It is replaced by the
   SharkMakers dorsal fin. Drawn as a mask rather than an <img> so it inherits
   currentColor and stays correct in both skins and on the dark hero scrim.
   Defined once here - it applies to all 99 kickers across the site. */
.eyebrow::before {
  content: "";
  flex: none;
  width: 1.15em;
  height: 1.15em;
  background: currentColor;
  -webkit-mask: var(--fin) no-repeat center / contain;
          mask: var(--fin) no-repeat center / contain;
}

.lead {
  font-size: var(--fs-lead);
  line-height: 1.55;
  color: var(--text-muted);
  max-width: 58ch;
}

/* ---- Buttons ---------------------------------------------------------- */
.btn {
  --_bg: var(--accent);
  --_fg: var(--accent-ink);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  padding: 0.9rem 1.6rem;
  min-height: 48px; /* touch target */
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  background: var(--_bg);
  color: var(--_fg);
  font-family: var(--font-body);
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  text-decoration: none;
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-out),
    filter var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}
.btn:hover {
  filter: brightness(1.08);
  transform: translateY(-2px);
}
.btn:active {
  transform: translateY(0);
}
/* The one button on the site allowed to break the blue-primary rule, and the
   pack's own reason for it: amber is reserved for money and impact, and
   spending it on decoration is how a donate button stops standing out. Ink is
   --accent-ink in both skins - #06080b on #ffb84d is 10.9:1 dark, #ffffff on
   #b04a12 is 5.3:1 light. Shipped inline on /donate until now; it is a class
   because two separate buttons need it and a third will. */
.btn--give {
  --_bg: var(--signal);
  --_fg: var(--accent-ink);
  font-size: 1.0625rem;
  font-weight: 700;
  padding: 1.1rem 2.1rem;
  min-height: 56px;
}


.btn-secondary {
  --_bg: transparent;
  --_fg: var(--text);
  border-color: var(--border-strong);
}
.btn-secondary:hover {
  --_bg: var(--surface-2);
  filter: none;
}

.btn-ghost {
  --_bg: transparent;
  --_fg: var(--accent);
  padding-inline: 0;
  min-height: 32px;
}
.btn-ghost:hover {
  filter: none;
  transform: none;
  text-decoration: underline;
  text-underline-offset: 4px;
}

.btn .arrow {
  transition: transform var(--dur-fast) var(--ease-out);
}
.btn:hover .arrow {
  transform: translateX(3px);
}

/* ---- Header ----------------------------------------------------------- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--border);
}
.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
  min-height: 72px;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  text-decoration: none;
  color: var(--text);
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: -0.03em;
  font-size: 1.0625rem;
  line-height: 1.1;
}
.brand__mark {
  width: 34px;
  height: 34px;
  border-radius: 9px;
  background: var(--accent);
  color: var(--accent-ink);
  display: grid;
  place-items: center;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  flex: none;
}
.brand__sub {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 400;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-top: 2px;
}

/* ---- Brand lockup (real logo art) -------------------------------------
   The Connect-IT 360 file is only 398 px wide, so it is never set above
   172 px. Both logos use the trimmed exports, so the light and dark
   variants sit in the same optical position.
   ---------------------------------------------------------------------- */
.brand-lockup {
  display: inline-flex;
  align-items: center;
  gap: 11px;
  text-decoration: none;
  flex: none;
}
/* The [hidden] guard that used to sit here is now global, in base.css. */
.brand-lockup img {
  filter: none;
}

/* Endorsement, lockup study option E: beside the mark, behind a hairline.
   Keeps the header one line tall, which is what makes it work on a phone. */
.brand-lockup__rule {
  width: 1px;
  align-self: stretch;
  margin-block: 3px;
  background: var(--border-strong);
  flex: none;
}
.brand-lockup__by {
  font-family: var(--font-body);
  font-style: italic;
  font-size: 10px;
  line-height: 1.35;
  letter-spacing: 0.005em;
  color: var(--text-muted);
  max-width: 13ch;
}
.brand-lockup__by b {
  display: block;
  font-weight: 500;
  font-style: italic;
  color: var(--text);
}
@media (max-width: 420px) {
  .brand-lockup img {
    width: 132px !important;
  }
  .brand-lockup {
    gap: 9px;
  }
}

/* Both colour variants ship; CSS shows the one that suits the ground.
   A filter would not work here - inverting the white art would also invert
   the blue fin, which is a fixed brand colour in both variants. */
.brand-lockup .on-light {
  display: none;
}
:root[data-skin="light"] .brand-lockup .on-dark {
  display: none;
}
:root[data-skin="light"] .brand-lockup .on-light {
  display: block;
}

.nav {
  display: none;
  align-items: center;
  gap: var(--s-5);
}
@media (min-width: 900px) {
  .nav {
    display: flex;
  }
}
.nav a {
  color: var(--text-muted);
  text-decoration: none;
  font-size: 0.9375rem;
  font-weight: 500;
  padding: var(--s-2) 0;
}
.nav a:hover,
.nav a[aria-current="page"] {
  color: var(--text);
}
.nav a[aria-current="page"] {
  box-shadow: inset 0 -2px 0 var(--accent);
}

.nav-toggle {
  display: inline-grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: transparent;
  color: var(--text);
  cursor: pointer;
}
@media (min-width: 900px) {
  .nav-toggle {
    display: none;
  }
}
.nav-mobile {
  display: none;
  border-top: 1px solid var(--border);
  padding: var(--s-4) 0 var(--s-5);
}
.nav-mobile[data-open="true"] {
  display: block;
}
.nav-mobile a {
  display: block;
  padding: var(--s-3) 0;
  color: var(--text);
  text-decoration: none;
  font-weight: 500;
  border-bottom: 1px solid var(--border);
}

/* ---- "A programme of" notice bar -------------------------------------- */
.org-notice {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  font-size: var(--fs-small);
  color: var(--text-muted);
}
.org-notice__inner {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-4);
  align-items: center;
  justify-content: center;
  padding-block: var(--s-3);
  text-align: center;
}
.org-notice strong {
  color: var(--text);
  font-weight: 600;
}

/* ---- Hero + parallax --------------------------------------------------- */
.hero {
  /* The hero is cinematic in BOTH skins: dark photography, light type.
     So it declares its own text tokens rather than inheriting the skin's. */
  --text: #ffffff;
  --text-muted: rgba(255, 255, 255, 0.82);
  --border-strong: rgba(255, 255, 255, 0.45);
  color: #ffffff;
  position: relative;
  min-height: clamp(560px, 92vh, 900px);
  display: flex;
  align-items: flex-end;
  overflow: hidden;
  background: var(--bg-deep);
  isolation: isolate;
}
/* The light skin's accent (#1b4de4) only clears 2.3:1 on the dark hero plate,
   so the hero borrows the dark skin's accent. Checked: 8.8:1. */
:root[data-skin="light"] .hero {
  --accent: #6fb1ff;
  --accent-ink: #06080b;
}
/* No space before .brand-sharkmakers: the skin attribute and the brand class
   sit on the SAME <html> element, so a descendant combinator never matches. */
:root[data-skin="light"].brand-sharkmakers .hero,
:root[data-skin="light"] .hero.brand-sharkmakers {
  --accent: #6fb1ff;
  --accent-ink: #06080b;
}

.hero__media {
  position: absolute;
  inset: -12% 0 -12% 0;
  z-index: -2;
  will-change: transform;
}
.hero__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: var(--photo-treatment);
}
.hero__scrim {
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--hero-scrim);
}
.hero__grid {
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: linear-gradient(var(--grid-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
  background-size: 72px 72px;
  mask-image: radial-gradient(ellipse at 50% 40%, #000 20%, transparent 78%);
}
.hero__inner {
  position: relative;
  padding-block: var(--s-9) var(--s-8);
  width: 100%;
}
.hero h1 {
  font-size: var(--fs-display-xl);
  max-width: 16ch;
  margin-bottom: var(--s-5);
}
.hero .lead {
  max-width: 52ch;
  color: var(--text);
  opacity: 0.86;
}
.hero__actions {
  margin-top: var(--s-6);
}

.parallax-layer {
  will-change: transform;
}

/* ---- Stat band --------------------------------------------------------- */
.statband {
  border-block: 1px solid var(--border);
  background: var(--surface);
}
.statband__grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1px;
  background: var(--border);
}
@media (min-width: 900px) {
  .statband__grid {
    grid-template-columns: repeat(4, 1fr);
  }
}
.stat {
  background: var(--surface);
  padding: var(--s-6) var(--s-5);
}
.stat__value {
  font-family: var(--font-display);
  font-size: clamp(2rem, 4.4vw, 3.25rem);
  font-weight: 700;
  letter-spacing: -0.04em;
  line-height: 1;
  color: var(--text);
  display: block;
}
.stat__value.is-money {
  color: var(--signal);
}
.stat__label {
  display: block;
  margin-top: var(--s-3);
  font-size: var(--fs-small);
  color: var(--text-muted);
  line-height: 1.4;
}

/* ---- Cards ------------------------------------------------------------- */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--s-6);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  transition: border-color var(--dur-base) var(--ease-out),
    transform var(--dur-base) var(--ease-out);
}
.card:hover {
  border-color: var(--border-strong);
  transform: translateY(-3px);
}
.card h3 {
  margin: 0;
}
.card p {
  color: var(--text-muted);
  margin: 0;
  font-size: 0.9688rem;
}
.card__num {
  font-family: var(--font-mono);
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--tracking-eyebrow);
  color: var(--accent);
}
.card--link {
  text-decoration: none;
  color: inherit;
}

.card-media {
  border-radius: var(--r-lg);
  overflow: hidden;
  border: 1px solid var(--border);
  background: var(--surface);
}
.card-media img {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  filter: var(--photo-treatment);
  transition: transform var(--dur-slow) var(--ease-out);
}
.card-media:hover img {
  transform: scale(1.03);
}
.card-media__body {
  padding: var(--s-5);
}
.card-media__body h3 {
  margin-bottom: var(--s-2);
}
.card-media__body p {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.9375rem;
}
/* Media cards size to their own content. Stretching them to the tallest
   sibling leaves a block of dead surface under the short ones, which on the
   /news grid is most of them. Was an inline style on that one grid; it is a
   property of the component, so it lives with the component. */
.grid:has(> .card-media) {
  align-items: start;
}

/* ---- Ladder (Inventor -> Entrepreneur) --------------------------------- */
.ladder {
  display: grid;
  gap: var(--s-4);
  counter-reset: rung;
}
@media (min-width: 900px) {
  .ladder {
    grid-template-columns: repeat(4, 1fr);
  }
}
.ladder__step {
  position: relative;
  padding: var(--s-6) var(--s-5) var(--s-5);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
  overflow: hidden;
}
.ladder__step::after {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  height: 3px;
  width: var(--_w, 25%);
  background: var(--accent);
}
.ladder__step:nth-child(2)::after {
  --_w: 50%;
}
.ladder__step:nth-child(3)::after {
  --_w: 75%;
}
.ladder__step:nth-child(4)::after {
  --_w: 100%;
}
.ladder__step h3 {
  margin-bottom: var(--s-2);
}
.ladder__step p {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.9375rem;
}

/* ---- Timeline ---------------------------------------------------------- */
.timeline {
  list-style: none;
  padding: 0;
  margin: 0;
  border-left: 2px solid var(--border);
}
.timeline li {
  position: relative;
  padding: 0 0 var(--s-7) var(--s-6);
  margin: 0;
}
.timeline li::before {
  content: "";
  position: absolute;
  left: -7px;
  top: 8px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--bg);
}
.timeline__year {
  font-family: var(--font-mono);
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--tracking-eyebrow);
  color: var(--accent);
  display: block;
  margin-bottom: var(--s-2);
}
.timeline h3 {
  margin-bottom: var(--s-2);
  font-size: 1.125rem;
}
.timeline p {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.9375rem;
}

/* ---- Partner wall ------------------------------------------------------ */
/* Two tiers. Principal partners get a bordered band and their own mark;
   everyone else keeps the name chip, which now reads as the quieter tier
   because it sits under a smaller label and carries no ground of its own. */
.partner-wall__label {
  margin: 0 0 var(--s-4);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.partner-wall__label--sub {
  margin-top: var(--s-7);
}

.partner-logos {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-5) var(--s-7);
  padding: var(--s-6) var(--s-6);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
}
.partner-logo {
  display: flex;
  align-items: center;
}
.partner-logo img {
  height: var(--cap, 32px);
  width: auto;
  max-width: 200px;
  object-fit: contain;
}
/* One ink colour for the whole wall. A set of five brand palettes side by side
   reads as a ransom note, and half of them vanish on the opposite skin. */
[data-logos="mono"] .partner-logo img {
  filter: var(--logo-filter);
}
@media (max-width: 640px) {
  .partner-logos {
    gap: var(--s-5);
    padding: var(--s-5) var(--s-4);
  }
  .partner-logo img {
    height: calc(var(--cap, 32px) * 0.78);
    max-width: 140px;
  }
}

.partners {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
}
.partner {
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  padding: 0.55rem 1rem;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--text-muted);
  background: transparent;
}

/* ---- Photo strip ------------------------------------------------------- */
.photo-strip {
  display: grid;
  gap: var(--s-3);
  grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 900px) {
  .photo-strip {
    grid-template-columns: repeat(4, 1fr);
  }
}
.photo-strip img {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  filter: var(--photo-treatment);
}

.figure {
  margin: 0;
}
.figure img {
  border-radius: var(--r-lg);
  border: 1px solid var(--border);
  width: 100%;
  filter: var(--photo-treatment);
}
.figure figcaption {
  margin-top: var(--s-3);
  font-size: var(--fs-small);
  color: var(--text-muted);
}

/* ---- Split section ----------------------------------------------------- */
/* Top-aligned, not centred. Centring two columns of very different height
   pushes the short one into the middle of the tall one's run and opens a gap
   above and below it - 521px of it in /about's history split at 1440x900.
   Start-aligned, the two columns begin on the same line, which is what the
   layout is for, and it is also the only alignment a sticky child can work
   inside: a centred grid item is stretched to nothing to centre it, and
   position:sticky then has no track to travel. */
.split {
  display: grid;
  gap: var(--s-7);
  align-items: start;
}
@media (min-width: 900px) {
  .split {
    grid-template-columns: 1fr 1fr;
    gap: var(--s-8);
  }
  .split--wide-left {
    grid-template-columns: 1.15fr 0.85fr;
  }
}

/* ---- Dense heroes ------------------------------------------------------
   Two hero photographs are a wall of lit faces rather than one subject on a
   dark backdrop: /schools (a thirty-student cohort) and /about (the season
   showcase group). The type clears contrast over them, but it lands across a
   dozen faces and reads as caption dumped on a crowd snapshot, and at 390px
   the eyebrow sits on a cheek where the scrim is only ~0.35.

   The answer is local darkening, not a heavier --hero-scrim. Robyn shoots on
   dark backdrops and the scrim is deliberately light so the students stay
   visible; deepening it globally would flatten every other hero to
   silhouettes. So this is a second layer, shaped like the text block: a wedge
   from the left at desktop, where the headline lives, fading out well before
   the right of the frame; a bottom-up gradient on a phone, where the text
   stacks over the lower half. Faces outside the type's path keep the light
   they were shot with. */
.hero--dense .hero__scrim::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, rgba(6, 8, 11, 0.72) 0%,
              rgba(6, 8, 11, 0.55) 38%, transparent 68%);
}
@media (max-width: 720px) {
  .hero--dense .hero__scrim::after {
    background: linear-gradient(180deg, transparent 0%,
                rgba(6, 8, 11, 0.68) 34%, rgba(6, 8, 11, 0.8) 100%);
  }
}

/* ---- Callout / notice -------------------------------------------------- */
.callout {
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  background: var(--accent-soft);
  border-radius: var(--r-md);
  padding: var(--s-5);
  font-size: 0.9375rem;
}
.callout p:last-child {
  margin-bottom: 0;
}
/* Amber is reserved for money and impact figures. A legal note is neither,
   so it takes the brand accent; spending --signal here is what stopped the
   donate button reading as the loudest thing on the page. */
.callout--legal {
  border-left-color: var(--accent);
  background: var(--surface);
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  line-height: 1.65;
  text-transform: none;
}

/* Season status. Amber because it is the one thing on the page that changes
   between seasons, and somebody who reads nothing else has to read it. */
.callout--status {
  border-left-color: var(--signal);
  background: color-mix(in srgb, var(--signal) 8%, transparent);
}
.callout--status strong {
  color: var(--text);
}

.pill {
  display: inline-block;
  padding: 0.3rem 0.7rem;
  border-radius: var(--r-pill);
  background: var(--accent-soft);
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-weight: 500;
}

/* ---- CTA band ---------------------------------------------------------- */
.cta-band {
  background: var(--accent);
  color: var(--accent-ink);
}
.cta-band h2 {
  color: var(--accent-ink);
  max-width: 18ch;
}
.cta-band p {
  color: var(--accent-ink);
  opacity: 0.82;
  max-width: 52ch;
}
.cta-band .btn {
  --_bg: var(--accent-ink);
  --_fg: var(--accent);
}
.cta-band .btn-secondary {
  --_bg: transparent;
  --_fg: var(--accent-ink);
  border-color: color-mix(in srgb, var(--accent-ink) 40%, transparent);
}

/* ---- Footer ------------------------------------------------------------ */
.site-footer {
  background: var(--bg-deep);
  border-top: 1px solid var(--border);
  padding-block: var(--s-8) var(--s-6);
  font-size: 0.9375rem;
}
.site-footer__top {
  display: grid;
  gap: var(--s-7);
  padding-bottom: var(--s-7);
  border-bottom: 1px solid var(--border);
}
@media (min-width: 900px) {
  .site-footer__top {
    grid-template-columns: 1.3fr 0.8fr 1fr 1fr 1fr;
  }
}
.site-footer h2 {
  font-size: 0.75rem;
  font-family: var(--font-mono);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: var(--s-4);
}
.site-footer ul {
  list-style: none;
  padding: 0;
  margin: 0;
}
.site-footer li {
  margin-bottom: var(--s-3);
}
.site-footer a {
  color: var(--text-muted);
  text-decoration: none;
}
.site-footer a:hover {
  color: var(--text);
  text-decoration: underline;
}
.site-footer .socials {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-4);
  margin-top: var(--s-4);
}
.site-footer .socials li {
  margin-bottom: 0;
}

.legal-identity {
  margin-top: var(--s-6);
  color: var(--text-muted);
  font-size: var(--fs-small);
  line-height: 1.65;
}
.legal-identity strong {
  color: var(--text);
}
.legal-identity .ein {
  font-family: var(--font-mono);
  font-size: 0.8125rem;
}
.fl-legend {
  margin-top: var(--s-5);
  padding: var(--s-4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  line-height: 1.7;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.01em;
}

/* ---- Legal page template ----------------------------------------------- */
.legal-head {
  border-bottom: 1px solid var(--border);
  padding-block: var(--s-8) var(--s-7);
  background: var(--surface);
}

/* Twelve of the eighteen pages open on this flat banner rather than a
   full-height hero, and a flat banner plus a full --section-y (128px at
   desktop) put roughly 200px of empty ground in the middle of the opening
   frame. A cinematic hero earns that much air because it fills the screen
   on its own; a 250px banner does not. Halve the gap so the fold carries
   the first row of real content. The section's own bottom padding is left
   alone - the rhythm further down the page is fine, it is only the join
   under the banner that reads as a page that failed to load. */
.legal-head + .section {
  padding-block-start: var(--s-7);
}

/* A section whose entire payload is one callout is a footnote, not a band,
   and does not need a full --section-y on each side of it. This is what put
   another 160px of nothing between the report list and the reporting-period
   note on /impact-reports, and the same again on /calendar. */
.section:has(> .container > .callout:only-child) {
  padding-block: var(--s-7);
}
.legal-meta {
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  color: var(--text-muted);
}
.legal-toc {
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
  padding: var(--s-5);
  margin-bottom: var(--s-7);
}
.legal-toc h2 {
  font-size: 0.75rem;
  font-family: var(--font-mono);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: var(--s-3);
}
.legal-toc ol {
  margin: 0;
  padding-left: 1.2em;
  columns: 2;
  column-gap: var(--s-6);
}
@media (max-width: 640px) {
  .legal-toc ol {
    columns: 1;
  }
}
.legal-toc li {
  margin-bottom: var(--s-2);
  font-size: 0.9375rem;
}

table.data {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9375rem;
  min-width: 520px;
}
table.data th,
table.data td {
  text-align: left;
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}
table.data th {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
  font-weight: 500;
}

/* ---- Reveal animation ---------------------------------------------------
   The resting state is VISIBLE, deliberately. .reveal on its own is a marker
   saying "this MAY animate in", not a hiding rule, so a page with the script
   blocked, or an element already in the opening frame, renders normally.
   motion.js adds .reveal--armed to the elements that are genuinely below the
   fold at boot, and only those start hidden. See the note over initReveal. */
.reveal {
  transition: opacity var(--dur-slow) var(--ease-out),
    transform var(--dur-slow) var(--ease-out);
}
.reveal--armed {
  opacity: 0;
  transform: translateY(22px);
}
.reveal--armed.in {
  opacity: 1;
  transform: none;
}
.reveal[data-delay="1"] {
  transition-delay: 90ms;
}
.reveal[data-delay="2"] {
  transition-delay: 180ms;
}
.reveal[data-delay="3"] {
  transition-delay: 270ms;
}

/* ---- Skin switcher (PREVIEW ONLY - remove before production) ----------- */
.skin-switch {
  position: fixed;
  right: var(--s-4);
  bottom: var(--s-4);
  z-index: 150;
  display: flex;
  align-items: center;
  gap: var(--s-1);
  padding: var(--s-1);
  border-radius: var(--r-pill);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow);
}
.skin-switch button {
  border: 0;
  background: transparent;
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 0.6rem 0.9rem;
  min-height: 40px;
  border-radius: var(--r-pill);
  cursor: pointer;
}
.skin-switch button[aria-pressed="true"] {
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 600;
}

/* ---- Preview-only annotation ------------------------------------------- */
.preview-note {
  border: 1px dashed var(--signal);
  border-radius: var(--r-md);
  padding: var(--s-4);
  background: color-mix(in srgb, var(--signal) 8%, transparent);
  font-size: var(--fs-small);
  color: var(--text);
}
.preview-note strong {
  color: var(--signal);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  display: block;
  margin-bottom: var(--s-2);
}

/* ---- Placeholder marker ------------------------------------------------
   build.py's TBD() wraps an unresolved fact in <span class="ph">. The amber
   dashed chip exists so an unknown is impossible to miss while we are still
   working - and it is exactly the wrong thing to show a donor or a parent.
   Thirty-six of them were live on fourteen public pages.

   So the chip is now opt-in. Bare `.ph` is inert: the fallback prose renders
   as ordinary body text, indistinguishable from the sentence around it. The
   chip only paints inside `.is-preview`.

   THE SWITCH: put `is-preview` on <body>. Nothing else turns it on, and a
   production build that does nothing gets no chips. Whoever gates build.py
   should set the body class from the same flag that gates TBD() itself, so
   the two can never disagree.
   ---------------------------------------------------------------------- */
.is-preview .ph {
  color: var(--text);
  background: color-mix(in srgb, var(--signal) 15%, transparent);
  border-bottom: 1px dashed var(--signal);
  padding: 0 0.25em;
  border-radius: 2px;
  font-family: var(--font-mono);
  font-size: 0.9em;
}

/* Lockup art wrapper - holds the two colour variants stacked in the flow. */
.brand-lockup__art {
  display: block;
  flex: none;
}

/* Relationship statement in the footer legal block. */
.legal-identity .relationship {
  margin-top: 0.75rem;
}

/* ==========================================================================
   Migration components - added for the audience, org and news pages that
   absorb the old WordPress URLs. Token-driven, so both skins are covered.
   ========================================================================== */

/* ---- FAQ ---------------------------------------------------------------- */
.faq {
  border-top: 1px solid var(--border);
  margin-top: var(--s-6);
}
.faq__item {
  border-bottom: 1px solid var(--border);
}
.faq__item > summary {
  cursor: pointer;
  list-style: none;
  padding: var(--s-5) 0;
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: 500;
  letter-spacing: -0.015em;
  color: var(--text);
  display: flex;
  align-items: flex-start;
  gap: var(--s-4);
}
.faq__item > summary::-webkit-details-marker { display: none; }
.faq__item > summary::after {
  content: "+";
  margin-left: auto;
  flex: none;
  font-family: var(--font-mono);
  font-size: 1.35rem;
  line-height: 1;
  color: var(--accent);
  transition: transform var(--dur-fast) var(--ease-out);
}
.faq__item[open] > summary::after { transform: rotate(45deg); }
.faq__item > summary:hover { color: var(--accent); }
.faq__item > div {
  padding-bottom: var(--s-5);
  max-width: var(--prose);
  color: var(--text-muted);
}
.faq__item > div > p { margin-top: 0; }

/* ---- Pull quote --------------------------------------------------------- */
.quote {
  margin: 0;
  padding: var(--s-5) 0 var(--s-5) var(--s-5);
  border-left: 2px solid var(--accent);
}
.quote p {
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: 500;
  line-height: var(--lh-snug);
  letter-spacing: -0.015em;
  color: var(--text);
  margin: 0;
}
.quote__cite {
  display: block;
  margin-top: var(--s-4);
  font-size: var(--fs-small);
  font-style: normal;
  color: var(--text-muted);
}
.quote__cite strong { color: var(--text); font-weight: 600; }

/* ---- People grid (board, mentors, alumni) ------------------------------- */
.people {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: var(--s-5);
  margin-top: var(--s-6);
  padding: 0;
  list-style: none;
}
.person {
  /* A floor, so a one-line card in a mixed row does not collapse next to a
     three-line neighbour. */
  min-height: 76px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: var(--s-5);
  background: var(--surface);
  display: flex;
  align-items: center;
  gap: var(--s-4);
}
/* Initials rather than an empty frame waiting for a headshot. Seven identical
   "photo" placeholders read as a broken page; a monogram reads as a decision,
   and the one outstanding asset is stated once above the grid instead. */
.person__mono {
  flex: 0 0 auto;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 0.82rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
}
.brand-sharkmakers .person__mono {
  background: var(--accent-sm-soft);
  color: var(--accent-sm);
}
.person__photo {
  flex: 0 0 auto;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 50%;
  object-fit: cover;
  /* The sources are a mixed bag - one is a circle cropped onto white, the rest
     are square on assorted backgrounds. The ring gives them a shared edge so
     the row reads as one set rather than seven pasted images. */
  box-shadow: 0 0 0 1px var(--border-strong);
  background: var(--bg-deep);
}
.person__body { min-width: 0; }
.person h3 {
  font-size: 1.02rem;
  margin: 0;
  letter-spacing: -0.01em;
  overflow-wrap: break-word;
}
.person p {
  margin: var(--s-2) 0 0;
  font-size: var(--fs-small);
  color: var(--text-muted);
}

/* ---- Board portraits (/about, /leadership) -----------------------------
   Moved here from a BOARD_CSS constant that was emitted as an inline <style>
   on the two pages that use it. Nothing outside those two pages references
   these selectors.

   It has to sit AFTER the .people / .person block above, not before it.
   `.people--portrait` and `.people` are both one class, so they tie on
   specificity and source order is the whole decision. Pasted higher up the
   file, `.people`'s own grid-template-columns won, the 58rem cap was lost,
   and the portraits went from 180px to 259px on desktop and 308px at
   390px - upscaled past their 256px sources, and one near-full-width
   portrait per phone screen. Do not move this block above .people. */

/* Board name chips (/about) - the chip is now a link to the director's
   LinkedIn profile, so it needs a resting state that does not read as body
   copy and a hover that does. */
.partner[href] { text-decoration: none; transition: color .16s, border-color .16s; }
.partner[href]:hover,
.partner[href]:focus-visible { color: var(--text); border-color: var(--border-strong); }

/* Board portraits (/leadership). The 2.75rem avatar was too small to do the
   only job this page has, which is to show that real, named people are
   accountable. One portrait per column, name underneath.

   180px is the target, not a ceiling reached by accident: the headshots are
   256x256, so 180px still has headroom at 2x DPR and they were already soft
   at that size. The track is flexible so there is never a ragged right edge
   at an in-between width, and the portrait itself carries the cap - a card
   wider than 180px + padding simply centres the picture. */
.people--portrait { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: var(--s-5); max-width: 58rem; }
.people--portrait .person { flex-direction: column; align-items: stretch;
  gap: var(--s-4); padding: var(--s-4); }
.people--portrait .person__photo,
.people--portrait .person__mono { width: 100%; max-width: 180px; height: auto;
  margin-inline: auto; aspect-ratio: 1 / 1; border-radius: var(--r-md); }
/* The monogram stands in for the one director with no headshot, next to seven
   photographs. At a small size in a large frame that reads as a failed load,
   so it takes the same ring and radius as the photographs and fills its tile
   with type - a monogram at portrait scale reads as a decision. */
.people--portrait .person__mono { font-size: clamp(2rem, 4.4vw, 3.2rem);
  letter-spacing: 0.04em; font-weight: 700;
  background: color-mix(in srgb, var(--accent) 13%, var(--surface-2));
  box-shadow: 0 0 0 1px var(--border-strong); }
.people--portrait .person h3 { font-size: 1rem; }
/* The name is the link, so it needs to look like one at rest without shouting
   over eight cards. A hairline in the border colour reads as underlined text
   at a glance; hover and keyboard focus take it to the accent. */
.people--portrait .person h3 a { color: inherit; text-decoration: underline;
  text-decoration-color: var(--border-strong); text-underline-offset: 3px; }
.people--portrait .person h3 a:hover,
.people--portrait .person h3 a:focus-visible { color: var(--accent);
  text-decoration-color: currentColor; }
/* One portrait per column at 390px would run to 308px against a 256px source,
   which is where these crops start to soften. Two up instead. */
@media (max-width: 640px) {
  .people--portrait { grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s-4); }
}

/* A 3.5rem avatar beside a paragraph leaves the writeup a 232px measure at
   390px, which is about 30 characters a line. Below 480px the card stacks and
   the text gets the full column. Only the two grids that carry a writeup -
   the board portraits already stack by their own rule. */
@media (max-width: 480px) {
  .people--mentor .person,
  .people--alum .person {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--s-3);
  }
}

/* ---- Download list (PDFs) ----------------------------------------------- */
.downloads { list-style: none; padding: 0; margin: var(--s-6) 0 0; }
.downloads li { border-bottom: 1px solid var(--border); }
.downloads li:first-child { border-top: 1px solid var(--border); }
/* The meta is pushed right with margin-left:auto and refuses to wrap, so a
   long title plus "PDF - 15.2 MB" overflowed the row and took the whole body
   with it - 41px of horizontal page scroll at 390px. Let the row wrap, and
   below 560px drop the meta onto its own line where nowrap costs nothing. */
.downloads a {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-4);
  padding: var(--s-5) 0;
  text-decoration: none;
  color: var(--text);
}
@media (max-width: 560px) {
  .downloads .dl__meta {
    margin-left: 0;
    flex-basis: 100%;
    white-space: normal;
  }
}
.downloads a:hover { color: var(--accent); }
.downloads .dl__name { font-family: var(--font-display); font-size: var(--fs-h3); font-weight: 500; }
.downloads .dl__meta {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  color: var(--text-muted);
  white-space: nowrap;
}

/* ---- Article list (news, newsletters) ----------------------------------- */
.entries { list-style: none; padding: 0; margin: var(--s-6) 0 0; }
.entries li { border-bottom: 1px solid var(--border); }
.entries li:first-child { border-top: 1px solid var(--border); }
.entries a {
  display: grid;
  grid-template-columns: 7.5rem 1fr;
  gap: var(--s-5);
  padding: var(--s-5) 0;
  text-decoration: none;
  color: var(--text);
  align-items: baseline;
}
.entries a:hover .entry__title { color: var(--accent); }
.entry__date {
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.entry__title {
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: 500;
  letter-spacing: -0.015em;
  transition: color var(--dur-fast) var(--ease-out);
}
.entry__kicker {
  display: block;
  margin-top: var(--s-2);
  font-size: var(--fs-small);
  color: var(--text-muted);
}
@media (max-width: 640px) {
  .entries a { grid-template-columns: 1fr; gap: var(--s-2); }
}

/* ---- Article body ------------------------------------------------------- */
.article { max-width: var(--prose); }
.article > * + * { margin-top: var(--s-5); }
.article h2 { font-size: var(--fs-h2); letter-spacing: -0.025em; margin-top: var(--s-7); }
.article h3 { font-size: var(--fs-h3); margin-top: var(--s-6); }
.article img { border-radius: var(--r-md); display: block; }
.article .embed {
  position: relative;
  padding-bottom: 56.25%;
  height: 0;
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--surface-2);
}
.article .embed iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}
.article__meta {
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  color: var(--text-muted);
  margin-bottom: var(--s-6);
}

/* ==========================================================================
   Primary nav submenus.

   Opens on hover and on :focus-within, so it is reachable by keyboard with no
   JavaScript at all - tab into the parent link and the panel stays open while
   focus is anywhere inside it. The mobile drawer ignores this entirely and
   lists the same links flat, indented under their parent.
   ========================================================================== */
.nav__group {
  position: relative;
  display: inline-flex;
  align-items: center;
}
.nav__group > a::after {
  content: "";
  display: inline-block;
  width: 0.32em;
  height: 0.32em;
  margin-left: 0.45em;
  vertical-align: 0.15em;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translateY(-0.06em);
  opacity: 0.55;
  transition: transform var(--dur-fast) var(--ease-out), opacity var(--dur-fast);
}
.nav__group:hover > a::after,
.nav__group:focus-within > a::after {
  opacity: 1;
  transform: rotate(225deg) translateY(-0.2em);
}

.nav__sub {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translate(-50%, 6px);
  min-width: 13rem;
  margin: 0;
  padding: var(--s-3);
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: 0 12px 32px -12px rgba(0, 0, 0, 0.55);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out),
              visibility var(--dur-fast);
  z-index: 60;
}
/* A gap between the trigger and the panel would drop the hover on the way
   down, so the panel reaches back up under the header instead. */
.nav__sub::before {
  content: "";
  position: absolute;
  inset: -10px 0 100% 0;
}
.nav__group:hover > .nav__sub,
.nav__group:focus-within > .nav__sub {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translate(-50%, 2px);
}
.nav__sub a {
  display: block;
  padding: var(--s-2) var(--s-4);
  border-radius: var(--r-sm);
  font-size: var(--fs-small);
  white-space: nowrap;
  color: var(--text-muted);
  text-decoration: none;
}
.nav__sub a:hover,
.nav__sub a:focus-visible {
  background: var(--surface-2);
  color: var(--text);
}
.nav__sub a[aria-current="page"] {
  color: var(--accent);
}

/* Mobile drawer: same links, indented, no panel. */
.nav-mobile__sub {
  padding-left: var(--s-5) !important;
  font-size: var(--fs-small);
  opacity: 0.82;
}
@media (prefers-reduced-motion: reduce) {
  .nav__sub,
  .nav__group > a::after { transition: none; }
}

/* ==========================================================================
   Embedded document (newsletter PDFs) + its extracted text version.
   ========================================================================== */
.doc { margin-top: var(--s-6); }
.doc__view {
  display: block;
  width: 100%;
  height: min(78vh, 900px);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
}
.doc__fallback {
  padding: var(--s-6);
  margin: 0;
  color: var(--text-muted);
}
/* A PDF plugin is unreliable on phones, so hand them the link instead. */
@media (max-width: 700px) {
  .doc__view { display: none; }
}
.doc__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-4);
  margin-top: var(--s-4);
}
.doc__meta span {
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  color: var(--text-muted);
}

.doc__text {
  margin-top: var(--s-6);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
.doc__text > summary {
  cursor: pointer;
  list-style: none;
  padding: var(--s-5) 0;
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: 500;
  letter-spacing: -0.015em;
  display: flex;
  align-items: center;
  gap: var(--s-4);
}
.doc__text > summary::-webkit-details-marker { display: none; }
.doc__text > summary::after {
  content: "+";
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 1.35rem;
  line-height: 1;
  color: var(--accent);
  transition: transform var(--dur-fast) var(--ease-out);
}
.doc__text[open] > summary::after { transform: rotate(45deg); }
.doc__text > summary:hover { color: var(--accent); }
.doc__text > div { padding-bottom: var(--s-6); max-width: var(--prose); }
.doc__text p { margin: 0 0 var(--s-4); }
.doc__note {
  font-size: var(--fs-small);
  color: var(--text-muted);
  border-left: 2px solid var(--border);
  padding-left: var(--s-4);
}
@media (prefers-reduced-motion: reduce) {
  .doc__text > summary::after { transition: none; }
}

/* Extracted PDF text: the source line breaks are load-bearing, so keep them. */
.doc__raw {
  white-space: pre-wrap;
  word-break: break-word;
  font-family: var(--font-body);
  font-size: var(--fs-small);
  line-height: 1.65;
  color: var(--text-muted);
  margin: 0 0 var(--s-5);
  padding: 0;
  background: none;
  border: 0;
}
.doc__raw + .doc__raw {
  padding-top: var(--s-5);
  border-top: 1px solid var(--border);
}

/* ==========================================================================
   Video with a real caption.

   The old site embedded bare iframes with no title, so a post whose only
   content was a video had nothing for a search engine or a screen reader to
   read. The caption carries the title, the channel and a link out.
   ========================================================================== */
figure.video { margin: var(--s-6) 0; }
figure.video figcaption {
  margin-top: var(--s-4);
  font-size: var(--fs-small);
  line-height: 1.55;
  color: var(--text-muted);
}
figure.video figcaption strong {
  display: block;
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: 500;
  letter-spacing: -0.015em;
  color: var(--text);
  margin-bottom: var(--s-2);
}
.video__by { display: inline-block; margin-right: var(--s-4); }
.video__by::before { content: "Channel: "; opacity: 0.7; }
.video__link { color: var(--accent); text-decoration: none; white-space: nowrap; }
.video__link:hover { text-decoration: underline; }

/* Poster card. First made for the packed preview, where CSP blocks the frame;
   since 1 Oct it is also the picture on every .vidcard below. */
.video-poster {
  position: relative;
  display: block;
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--surface-2);
  line-height: 0;
}
.video-poster img { width: 100%; height: auto; display: block; }
.video-poster::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 72px;
  height: 72px;
  margin: -36px 0 0 -36px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.62) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 24 24' fill='%23fff'%3E%3Cpath d='M8 5v14l11-7z'/%3E%3C/svg%3E") center/28px no-repeat;
  backdrop-filter: blur(2px);
  transition: transform var(--dur-fast) var(--ease-out), background var(--dur-fast);
}
.video-poster:hover::after {
  transform: scale(1.08);
  background-color: rgba(0, 0, 0, 0.78);
}
.video-poster:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) {
  .video-poster::after { transition: none; }
}

/* ---- Video cards (Key Links, /apply) ------------------------------------ */
/* A thumbnail that links out to YouTube, not an embed. Nothing from YouTube
   loads until somebody clicks, so no third-party cookie lands on a page a
   student is filling in. The thumbnails are served from assets/img/video/. */
.vidcards {
  list-style: none;
  padding: 0;
  margin: var(--s-5) 0 0;
  display: grid;
  gap: var(--s-6) var(--s-5);
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
}
.vidcard { display: block; text-decoration: none; color: var(--text); }
.vidcard .video-poster { aspect-ratio: 16 / 9; max-width: 100%; }
.vidcard .video-poster img { height: 100%; object-fit: cover; }
.vidcard .video-poster::after {
  width: 52px;
  height: 52px;
  margin: -26px 0 0 -26px;
  background-size: 22px;
}
.vidcard:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; border-radius: var(--r-md); }
.vidcard__kind {
  display: block;
  margin-top: var(--s-3);
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  color: var(--text-muted);
}
.vidcard__title {
  display: block;
  margin-top: var(--s-1);
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: -0.015em;
  transition: color var(--dur-fast) var(--ease-out);
}
.vidcard:hover .vidcard__title { color: var(--accent); }
.vidcard__note {
  display: block;
  margin-top: var(--s-2);
  font-size: var(--fs-small);
  color: var(--text-muted);
}
/* The one-up version on /apply: picture beside the words, stacked on a phone. */
.vidcard--wide {
  display: grid;
  grid-template-columns: minmax(0, 17rem) minmax(0, 1fr);
  gap: var(--s-5);
  align-items: center;
}
.vidcard--wide .vidcard__kind { margin-top: 0; }
@media (max-width: 640px) {
  .vidcard--wide { grid-template-columns: 1fr; gap: var(--s-3); }
}
@media (prefers-reduced-motion: reduce) {
  .vidcard__title { transition: none; }
}

/* Ported posts contain bare URLs from the old editor, some of them very long.
   Without this they push the page sideways on a phone. */
.article {
  overflow-wrap: break-word;
}
.article a {
  overflow-wrap: anywhere;
}

/* Ported titles contain unbreakable tokens ("TEDxYouth@PalmHarbor") that are
   wider than a phone at display sizes. */
h1, h2, h3, .entry__title {
  overflow-wrap: break-word;
}

/* ---- Partner tiers (homepage) ------------------------------------------
   Principal partners static and centred; the supporting tier keeps the
   marquee below it. Same .partner-logos band as the sponsors page, so the
   two surfaces cannot drift apart. */
.partner-tiers .partner-wall__label {
  text-align: center;
}
.partner-logos--band {
  justify-content: center;
}
.partner-tiers .marquee {
  margin-top: 0;
}

/* ---- Application form ---------------------------------------------------
   One form, two branches. Sized for reading rather than for filling a wide
   screen: a 52rem column with single-column fields, because an application
   answered on a phone is the common case, not the exception. */
.apply-form { margin-top: var(--s-6); }

.form-section {
  padding: var(--s-6) 0;
  border-top: 1px solid var(--border);
}
.form-section:first-child { border-top: 0; padding-top: 0; }
.form-section__title {
  margin: 0 0 var(--s-2);
  font-family: var(--font-display);
  font-size: 1.125rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--text);
}
.form-section__desc {
  margin: 0 0 var(--s-4);
  max-width: 60ch;
  color: var(--text-muted);
  font-size: 0.9375rem;
}

.field { margin-top: var(--s-5); }
.field--group { border: 0; padding: 0; margin-inline: 0; }
.field__label {
  display: block;
  margin-bottom: var(--s-2);
  font-weight: 600;
  font-size: 0.9375rem;
  color: var(--text);
}
.req { color: var(--signal); }
.field__hint {
  margin: 0 0 var(--s-3);
  max-width: 62ch;
  color: var(--text-muted);
  font-size: 0.875rem;
  line-height: 1.5;
}
.field__input {
  width: 100%;
  padding: 0.7rem 0.85rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--text);
  font: 400 0.9375rem/1.5 var(--font-body);
}
.field__input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}
textarea.field__input { resize: vertical; min-height: 6rem; }

/* "At least one of these" groups. The browser has no native rule for it, so
   the page sets a custom validity message on the first box in the group and
   this is the visible half of that - shown only after a failed submit, never
   as a greeting. */
.field__error {
  margin: var(--s-3) 0 0;
  color: var(--signal);
  font-size: 0.875rem;
  font-weight: 600;
}

.opts { display: grid; gap: var(--s-2); }
/* Twenty-one one-line options stacked full width is 700px of government form.
   auto-fit wraps them into columns without a media query and without any of
   them losing its hit area. */
.opts--grid { grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); }
.opt {
  display: flex;
  gap: 0.65rem;
  align-items: flex-start;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  font-size: 0.9375rem;
  line-height: 1.45;
  color: var(--text-muted);
  cursor: pointer;
}
.opt:hover { border-color: var(--border-strong); color: var(--text); }
.opt input { margin-top: 0.28rem; flex: none; accent-color: var(--accent); }
/* Radios are drawn rather than left to the user agent. accent-color only
   paints the SELECTED state; the unselected one stayed the UA's light-mode
   disc, which on a near-black form is a pale filled circle - so every option
   on /apply, /sponsors and /mentors/register read as already answered, in
   both directions at once. An empty ring is the only unselected state that
   is unambiguous on either ground. Checkboxes keep the native control: with
   color-scheme declared (base.css) an unticked box is already an empty
   outline, and the native tick is better than anything drawn here. */
.opt input[type="radio"],
.program-card input[type="radio"] {
  appearance: none;
  -webkit-appearance: none;
  width: 1.1rem;
  height: 1.1rem;
  margin-top: 0.32rem;
  border: 2px solid var(--border-strong);
  border-radius: 50%;
  background: transparent;
  display: grid;
  place-content: center;
  transition: border-color var(--dur-fast) var(--ease-out);
}
.opt input[type="radio"]::before,
.program-card input[type="radio"]::before {
  content: "";
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  background: var(--accent);
  transform: scale(0);
  transition: transform var(--dur-fast) var(--ease-out);
}
.opt:hover input[type="radio"],
.program-card:hover input[type="radio"] { border-color: var(--text-muted); }
.opt input[type="radio"]:checked,
.program-card input[type="radio"]:checked { border-color: var(--accent); }
.opt input[type="radio"]:checked::before,
.program-card input[type="radio"]:checked::before { transform: scale(1); }
/* Windows high contrast throws the background away, so carry the state in
   a property that survives it. */
@media (forced-colors: active) {
  .opt input[type="radio"],
  .program-card input[type="radio"] { border-color: CanvasText; }
  .opt input[type="radio"]::before,
  .program-card input[type="radio"]::before { background: Highlight; }
  .opt input[type="radio"]:checked,
  .program-card input[type="radio"]:checked { border-color: Highlight; }
}
.opt:has(input:checked) {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--text);
}
.opt:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 1px; }

/* The programme choice is the one question everybody answers, so it gets the
   only two-up layout on the page and reads as a decision, not a field. */
.program-cards {
  display: grid;
  gap: var(--s-4);
  margin-top: var(--s-4);
}
@media (min-width: 720px) {
  .program-cards { grid-template-columns: 1fr 1fr; }
}
.program-card {
  display: flex;
  gap: 0.75rem;
  align-items: flex-start;
  padding: var(--s-5);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--surface);
  cursor: pointer;
}
.program-card:hover { border-color: var(--accent); }
.program-card:has(input:checked) {
  border-color: var(--accent);
  background: var(--accent-soft);
}
.program-card:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.program-card input { margin-top: 0.3rem; flex: none; accent-color: var(--accent); }
.program-card input[type="radio"] { width: 1.2rem; height: 1.2rem; margin-top: 0.22rem; }
.program-card input[type="radio"]::before { width: 0.6rem; height: 0.6rem; }
.program-card__body { display: block; }
.program-card__name {
  display: block;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1rem;
  color: var(--text);
}
.program-card__meta {
  display: block;
  margin-top: 0.3rem;
  color: var(--text-muted);
  font-size: 0.875rem;
  line-height: 1.45;
}

.branch__title {
  margin: 0 0 var(--s-5);
  font-size: 1.375rem;
}
.branch__title { width: fit-content; }
/* Focus lands here after choosing a programme, so it must be visible - but a
   full-width ring on a heading reads as a text input. Fit the ring to the
   words. */
.branch__title:focus-visible { outline: 2px solid var(--accent); outline-offset: 6px; }

.form-actions {
  padding-top: var(--s-6);
  border-top: 1px solid var(--border);
}

/* ---- Give form -------------------------------------------------------
   Added 9 Sep for the rebuilt donate page. Four requirements from the
   4 Sep review: a monthly option, a custom amount, an employer-matching
   prompt, and no sponsorship anywhere near the checkout.
   ---------------------------------------------------------------------- */
.give {
  gap: 0;
  padding: var(--s-7);
}
.give-block {
  border: 0;
  padding: 0;
  margin: 0 0 var(--s-7);
}
.give-block:last-of-type { margin-bottom: var(--s-6); }
.give-legend {
  font-family: var(--font-mono);
  font-size: var(--fs-eyebrow);
  font-weight: 500;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-muted);
  padding: 0;
  margin-bottom: var(--s-4);
}

/* Frequency - a two-up segmented control. */
.give-freq {
  display: inline-flex;
  gap: var(--s-1);
  padding: var(--s-1);
  background: var(--bg-deep);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
}
.give-freq__opt input { position: absolute; opacity: 0; pointer-events: none; }
.give-freq__opt span {
  display: block;
  padding: var(--s-2) var(--s-5);
  border-radius: var(--r-pill);
  font-size: var(--fs-small);
  color: var(--text-muted);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.give-freq__opt input:checked + span {
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 500;
}
.give-freq__opt input:focus-visible + span {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The fold's amount pills. Was a style string built in pages_more.py and
   cloned into that page's script, which then re-applied it with
   setAttribute('style', ...) on every render - an inline style that outranks
   any rule written here. The selected state hangs off aria-current, which the
   script sets anyway, so there is one source of truth for "which amount". */
.give-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0.6rem 1.15rem;
  border-radius: var(--r-pill);
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text);
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  letter-spacing: 0.04em;
  text-decoration: none;
  transition: border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.give-pill:hover { border-color: var(--text-muted); }
.give-pill[aria-current="true"] {
  border-color: var(--signal);
  color: var(--signal);
}

/* Amounts. auto-fit keeps six tiles honest from 320px up without a
   media query, and the tiles stay the same height so the grid reads
   as a row rather than a ransom note.

   The floor is 18rem, not 11rem. At 11rem the six tiles resolved to five
   columns at desktop, which left "Other" orphaned on a second row and
   stopped the row short of the card's right edge while the checkbox rows
   underneath ran its full width. 18rem resolves to three - two full rows,
   one flush edge - and still collapses to one column on a phone. This was
   an inline style on the two amount grids in pages_more.py; it is the
   component's own measurement, so it belongs here. */
.give-amounts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  gap: var(--s-3);
}
/* [hidden] is handled globally in base.css. */
.give-amt {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  padding: var(--s-4) var(--s-5);
  background: var(--bg-deep);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out);
}
.give-amt input { position: absolute; opacity: 0; pointer-events: none; }
/* The tile's control is visually hidden, so a disabled one would otherwise
   look exactly like a live one and simply not respond. Both the "Other" tile
   and the monthly frequency option ship disabled for the no-JavaScript case
   (see pages_more.py), and they have to look it. */
.give-amt:has(input:disabled),
.give-freq__opt:has(input:disabled) {
  opacity: 0.45;
  cursor: not-allowed;
}
.give-amt:has(input:disabled):hover { border-color: var(--border); }
.give-amt:hover { border-color: var(--border-strong); }
.give-amt:has(input:checked) {
  border-color: var(--accent);
  background: var(--accent-soft);
}
.give-amt:has(input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.give-amt__fig {
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  line-height: 1;
  color: var(--signal);
}
/* "Other" is a word, not a sum. The money colour has to mean money or it
   means nothing. */
.give-amt--other .give-amt__fig {
  color: var(--text);
}
.give-amt__note {
  font-size: var(--fs-small);
  line-height: 1.4;
  color: var(--text-muted);
}

.give-custom-input { display: flex; align-items: center; gap: var(--s-2); }
.give-custom-input > span {
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  color: var(--text-muted);
}

.give-actions {
  border-top: 1px solid var(--border);
  padding-top: var(--s-6);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s-3);
}
.give-summary {
  font-size: var(--fs-lead);
  color: var(--text);
  margin: 0;
}
/* Every figure in the running total is money. */
.give-summary strong { color: var(--signal); }

/* ---- Special thanks --------------------------------------------------
   Sitewide band above the footer. Names render as chips until a logo file
   with the matching slug lands in assets/logos/partners/, at which point
   they upgrade automatically - see build.special_thanks().
   ---------------------------------------------------------------------- */
.thanks {
  padding: var(--s-9) 0;
  background: var(--bg-deep);
  border-top: 1px solid var(--border);
}
.thanks__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3) var(--s-5);
  margin-top: var(--s-5);
}
.thanks__name {
  font-size: var(--fs-small);
  color: var(--text-muted);
  padding: var(--s-2) var(--s-4);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  white-space: nowrap;
}
/* filter gated on [data-logos="mono"], matching .partner-logo. It used to be
   unconditional here and on .community__logo, so flipping LOGO_TREATMENT to
   colour would have shipped one tier in colour and two still mono. */
.thanks__logo img {
  height: var(--cap, 26px);
  width: auto;
  max-width: 132px;
  object-fit: contain;
  opacity: 0.7;
  transition: opacity var(--dur-fast) var(--ease-out);
}
[data-logos="mono"] .thanks__logo img {
  filter: var(--logo-filter);
}
.thanks__logo img:hover { opacity: 1; }
@media (max-width: 640px) {
  .thanks__row { gap: var(--s-3) var(--s-4); }
  .thanks__logo img {
    height: calc(var(--cap, 26px) * 0.82);
    max-width: 104px;
  }
}

/* ---- Photo-backed program card ---------------------------------------
   First pass ran the photo full-bleed behind the copy under a scrim. It
   read badly both ways: the picture was muddy and the text sat on noise.
   The photo now gets its own band at the top of the card and the copy sits
   on solid surface underneath, so each one is clean.

   21/10 on a ~363px card gives a ~172px band. The masters are square with
   the group low in frame, so a centre crop lands on faces.
   ---------------------------------------------------------------------- */
.card--photo {
  padding-top: 0;
  overflow: hidden;
}
.card__media {
  margin: 0 calc(-1 * var(--s-6)) var(--s-2);
  aspect-ratio: 21 / 10;
  overflow: hidden;
  background: #0a1016;
}
.card__media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur-slow) var(--ease-out);
}
.card--photo:hover .card__media img { transform: scale(1.04); }
@media (prefers-reduced-motion: reduce) {
  .card__media img, .card--photo:hover .card__media img {
    transition: none; transform: none;
  }
}

/* Honeypot. Off-screen rather than display:none - some bots skip fields that
   are display:none, and this one only works if they fill it in. aria-hidden
   and tabindex="-1" keep it away from screen readers and the tab order. */
.hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* ---- Multi-step application ---------------------------------------------
   The steps are plain sections until JavaScript adds [data-on]. Everything
   below is scoped to that attribute, so a no-JS applicant gets the long form
   with no half-styled wizard chrome around it.
   ------------------------------------------------------------------------ */
/* The high school branch opens with a status callout, and .callout carries no
   margin of its own, so the step list sat flush against it. The collegiate
   branch never showed this: its wizard is the first thing in the branch. */
.callout + .wiz { margin-top: var(--s-6); }

.wiz__steps {
  display: none;
  list-style: none;
  margin: 0 0 var(--s-6);
  padding: 0;
  gap: var(--s-2);
}
.wiz[data-on] .wiz__steps { display: flex; flex-wrap: wrap; }

.wiz__dot {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  flex: 1 1 9rem;
  min-width: 0;
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
}
.wiz__num {
  flex: 0 0 auto;
  width: 1.6rem; height: 1.6rem;
  border-radius: 50%;
  display: grid; place-items: center;
  font-family: var(--font-mono);
  font-size: 0.75rem; font-weight: 600;
  background: var(--surface-2, var(--bg-deep));
  color: var(--text-muted);
  border: 1px solid var(--border);
}
.wiz__label { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
.wiz__label strong { font-size: 0.85rem; }
.wiz__label span { font-size: 0.75rem; color: var(--text-faint); }

.wiz__dot[data-state="now"] { border-color: var(--accent); background: var(--accent-soft); }
.wiz__dot[data-state="now"] .wiz__num { background: var(--accent); color: #fff; border-color: var(--accent); }
.wiz__dot[data-state="done"] .wiz__num { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
.wiz__dot[data-state="todo"] { opacity: 0.62; }
.brand-sharkmakers .wiz__dot[data-state="now"] { border-color: var(--accent-sm); background: var(--accent-sm-soft); }
.brand-sharkmakers .wiz__dot[data-state="now"] .wiz__num { background: var(--accent-sm); border-color: var(--accent-sm); }

.wiz__nav {
  display: none;
  align-items: center;
  gap: var(--s-4);
  margin-top: var(--s-6);
  padding-top: var(--s-5);
  border-top: 1px solid var(--border);
}
.wiz[data-on] .wiz__nav { display: flex; flex-wrap: wrap; }
/* The count sits between Back and Next and pushes them apart, so it holds its
   place whether or not Back is on screen. */
.wiz__count {
  margin: 0 auto;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

@media (max-width: 560px) {
  .wiz__label span { display: none; }
  .wiz__dot { flex: 1 1 auto; gap: var(--s-2); padding: var(--s-3); }
}

/* ---- Agreement gate ---- */
.agree-gate[data-locked] .opt { opacity: 0.55; }
.agree-gate[data-locked] .opt input { cursor: not-allowed; }
.agree-gate__hint {
  margin: var(--s-3) 0 0;
  font-size: var(--fs-small);
  color: var(--text-muted);
}

/* ---- Calendar ----------------------------------------------------------- */
.cal th[scope="row"] { white-space: nowrap; vertical-align: top; }
.cal__date { display: block; font-weight: 600; }
.cal__time {
  display: block;
  font-size: var(--fs-small);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  font-weight: 400;
}
/* A missing time is stated, not left blank - a blank cell reads as an
   oversight, and half these rows genuinely have no time yet. */
.cal__time--tbc { font-style: italic; color: var(--text-faint); }
.cal__mode {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  padding: 0.2rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  color: var(--text-muted);
  white-space: nowrap;
}

/* ---- Business Community board -------------------------------------------
   The second tier of the partner wall. Smaller than the principal row and
   denser, because there are 22 of them - they read as a community, not as a
   list of headline sponsors. Same mono ink treatment so the whole wall is one
   colour rather than a ransom note of brand palettes.
   ------------------------------------------------------------------------ */
.community {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-5) var(--s-6);
}
/* 0.85, not 0.65. Measured on the rendered pixels rather than the source
   files: every mark in this tier is already pure white with a transparent
   ground, so brightness(0) invert(1) is a no-op and the tier's opacity is the
   only thing setting how much ink survives. At 0.65 the two finest-line
   lockups - BC forward and The Hive - render at 4.1% and 11.8% ink coverage
   against a 22-mark median of 27%; at 0.85 they come up and peak contrast
   goes from 8.4:1 to 14:1 across the wall.
   --cap height is NOT a lever here and was tried: doubling it moves coverage
   by hundredths of a point, because coverage is scale-invariant. Raising the
   cap floor only flattens the optical normalisation this tier exists to do,
   so the heights are left exactly as they are. */
.community__logo img {
  height: var(--cap, 22px);
  width: auto;
  max-width: 150px;
  object-fit: contain;
  opacity: 0.85;
  transition: opacity var(--dur-fast) var(--ease-out);
}
[data-logos="mono"] .community__logo img {
  filter: var(--logo-filter);
}
.community__logo img:hover { opacity: 1; }
@media (max-width: 640px) {
  .community { gap: var(--s-4) var(--s-5); }
  .community__logo img { height: calc(var(--cap, 22px) * 0.82); max-width: 112px; }
}

/* ---- Empowering All Abilities constellation ---------------------------
   Two photo clusters with the program's own words between them - the layout
   says what the section says. Each cluster is its own 400x432 box and its
   circles are placed from the same coordinates its wire SVG uses, so the
   wires meet the circles at every column width and survive the stack to one
   column on mobile.

   The resting state is the FINISHED diagram: circles at full size, wires
   drawn. v2.js adds .is-armed to hold them back and .is-on to play them in,
   and only when the visitor has not asked for reduced motion. A script-less
   visitor sees the diagram, not an empty box.
   ---------------------------------------------------------------------- */
/* Its own vertical rhythm: the diagram already carries a lot of air inside
   the box, so the section's default padding read as a hole above it. */
.section--abil { padding-block: var(--s-7); }

.abil {
  display: grid;
  grid-template-columns: 1.05fr minmax(20rem, 1.1fr) 1.05fr;
  align-items: center;
  gap: clamp(0.5rem, 2vw, 2rem);
  margin-top: var(--s-6);
}
/* Source order is copy, left, right - so the h2 precedes the two h3s that sit
   under it. These three put the copy back between the clusters on screen. */
.abil__cluster--left  { order: 1; }
.abil__copy           { order: 2; }
.abil__cluster--right { order: 3; }
.abil__cluster {
  position: relative;
  aspect-ratio: 400 / 404;
}
.abil__wires {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  stroke: var(--accent-sm, var(--accent));
  stroke-width: 3;
  opacity: 0.5;
}
.abil__wires--narrow { display: none; }
.abil__wire { transition: stroke-width 0.25s ease, opacity 0.25s ease; }
.abil__wire[data-lit] { stroke-width: 5; opacity: 1; }

.abil__node {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: var(--d);
  aspect-ratio: 1;
  margin: 0;
  border-radius: 50%;
  overflow: hidden;
  border: 3px solid var(--accent-sm, var(--accent));
  background: var(--bg-deep);
  /* The centring translate lives in the same transform as the hover scale,
     or one cancels the other. */
  transform: translate(-50%, -50%) scale(var(--s, 1));
  transition: transform 0.38s cubic-bezier(0.2, 0.8, 0.2, 1),
              opacity 0.3s ease, box-shadow 0.25s ease;
}
.abil__node img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* These name the two halves of the section's argument, so they carry the
   site's own h3 - Bricolage, 700, --fs-h3 - and inherit it rather than
   restating it. They used to be faint uppercase mono at 0.72rem, which made
   the one phrase a reader needs the quietest thing in the section. Mono is
   also the wrong voice: the pack reserves it for figures a donor might
   verify, and this is a heading, not data. */
.abil__label {
  position: absolute;
  left: 50%;
  bottom: -2.4rem;
  transform: translateX(-50%);
  margin: 0;
  white-space: nowrap;
  color: var(--text);
}

/* ---- The copy, between the clusters ---- */
.abil__copy {
  position: relative;
  text-align: center;
  padding: 0 clamp(0.5rem, 1.5vw, 1.5rem);
}
.abil__mark {
  display: block;
  margin: 0 auto var(--s-4);
  width: 2.1rem;
}
.abil__mark img { width: 100%; height: auto; display: block; }
.abil__copy .eyebrow { justify-content: center; }
.abil__copy h2 {
  font-size: clamp(1.35rem, 2.1vw, 2.05rem);
  line-height: 1.14;
  margin: var(--s-3) 0 0;
  text-wrap: balance;
  /* The column is narrow by design; never let a long word break mid-word,
     which is what turned "entrepreneurship" into "entrepreneursh ip". */
  overflow-wrap: normal;
  hyphens: none;
}
.abil__lead {
  margin: var(--s-4) auto 0;
  max-width: 34ch;
  color: var(--text-muted);
  font-size: 0.975rem;
}
.abil__note {
  margin: var(--s-4) auto 0;
  max-width: 40ch;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  line-height: 1.6;
  letter-spacing: 0.04em;
  color: var(--text-faint);
}
/* The arrows flank the copy and point out at the clusters, which is the
   sentence the layout is making. Absolute, so they sit on the hub line
   rather than being pushed around by the length of the copy. */
.abil__arrows svg {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: clamp(2.5rem, 4vw, 4.5rem);
  height: 12px;
  fill: none;
  stroke: var(--accent-sm, var(--accent));
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.8;
}
.abil__arrows svg:first-child { left: calc(-1 * clamp(2.5rem, 4vw, 4.5rem) - 0.25rem); }
.abil__arrows svg:last-child { right: calc(-1 * clamp(2.5rem, 4vw, 4.5rem) - 0.25rem); }

/* Hover: the circle lifts, its wire lights, the rest steps back. Pointer
   devices only - a touch screen gets the resting state. */
@media (hover: hover) {
  .abil:has(.abil__node:hover) .abil__node:not(:hover) { opacity: 0.58; }
  .abil__node:hover {
    --s: 1.06;
    box-shadow: 0 0 0 4px color-mix(in srgb,
                var(--accent-sm, var(--accent)) 28%, transparent);
  }
  .abil__node:hover { animation-play-state: paused; }
}

/* ---- Entrance, added by v2.js only ----------------------------------
   Two scopes, on purpose. The copy sits between the clusters on desktop and
   above them on mobile, so it belongs to neither sequence and plays off the
   wrapper. Each CLUSTER then arms and plays on its own observer, so a
   visitor reads "Every ability" and watches it build before "Every founder"
   starts. Within a cluster the label lands first, on its own; the circles
   and their wires then cascade together, because a wire runs rim to rim and
   has nothing to wait for. Measured at 390px: label alone to 380ms, circles
   220-630ms, wires 380-880ms, the whole cluster done inside a second.
   ---------------------------------------------------------------------- */
.abil.is-armed .abil__copy { opacity: 0; transform: translateY(12px); }

/* How long a cluster waits after IT is switched on before the circles move -
   just long enough for its own label to land. The spacing BETWEEN the two
   clusters is not here: v2.js holds a cluster's `is-on` back so it never
   starts within 700ms of the one before it. That has to be JS, because a CSS
   delay cannot tell "both clusters entered the viewport together" (side by
   side, and on a tall phone, where the pair is only 48px apart and both fit
   on screen) from "the second one arrived a minute of scrolling later". A
   fixed delay is right in the first case and a dead pause in the second.
   JS reads this property for the wire delays - change it here, not in both. */
.abil__cluster { --t0: 220ms; }

.abil__cluster.is-armed .abil__node { opacity: 0; --s: 0.82; }
.abil__cluster.is-armed .abil__wires { opacity: 0; }
.abil__cluster.is-armed .abil__label {
  opacity: 0;
  /* The centring translate has to be carried through both states or the
     label jumps half its own width as it lands. */
  transform: translateX(-50%) translateY(6px);
}

.abil__cluster.is-on .abil__label {
  opacity: 1;
  transform: translateX(-50%);
  /* No delay: the label is the first thing to move in its own cluster, and
     the gap to the cluster before it is handled by when `is-on` lands. */
  transition: opacity 0.34s ease,
              transform 0.34s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.abil__cluster.is-on .abil__wires { opacity: 0.5; transition: opacity 0.4s ease; }
.abil__cluster.is-on .abil__node {
  opacity: 1;
  --s: 1;
  /* --t0 holds the circles back until the label has landed. Then short and
     overlapping: at 70ms a step the last circle landed most of a second
     after the first, which read as lag rather than sequence. */
  transition-delay: calc(var(--t0, 220ms) + (var(--i, 0) - var(--i0, 0)) * 55ms);
  /* A slow drift once everything has landed, so the diagram is alive rather
     than a frozen poster. It writes `translate`, not `transform`, which is
     already carrying the centring and the hover scale. */
  animation: abil-float calc(7s + var(--i, 0) * 0.5s) ease-in-out infinite;
  animation-delay: calc(var(--i, 0) * -1.3s);
}
.abil.is-on .abil__copy {
  opacity: 1;
  transform: none;
  transition: opacity 0.4s ease 60ms,
              transform 0.45s cubic-bezier(0.2, 0.8, 0.2, 1) 60ms;
}

@keyframes abil-float {
  0%, 100% { translate: 0 0; }
  50%      { translate: 0 -5px; }
}

@media (prefers-reduced-motion: reduce) {
  .abil__cluster.is-on .abil__node { animation: none; }
  .abil__node { transition: none; }
  .abil__label { transition: none; }
}

@media (max-width: 900px) {
  .abil {
    grid-template-columns: 1fr;
    gap: var(--s-7);
    justify-items: center;
  }
  .abil__cluster { width: min(100%, 20rem); }
  /* Stacked, the bottom satellite sits where the caption used to, so the
     label moves above the cluster and heads it instead - which is also the
     right reading order for a heading. */
  .abil__label { top: -2.4rem; bottom: auto; }
  /* The stacked layout has its own circle positions and its own wire set. */
  .abil__wires--wide { display: none; }
  .abil__wires--narrow { display: block; }
  .abil__node {
    left: var(--xm);
    top: var(--ym);
    width: var(--dm);
  }
  .abil__mark { width: 1.6rem; }
  .abil__note { margin-top: var(--s-3); }
  /* Stacked, the source order is already the reading order: copy, then the
     two clusters. Drop the desktop reordering entirely. */
  .abil__cluster--left,
  .abil__copy,
  .abil__cluster--right { order: 0; }
  .abil__copy h2 { font-size: clamp(1.6rem, 6vw, 2.1rem); }
  /* Stacked, the arrows have nothing to point at. */
  .abil__arrows { display: none; }
}

/* ---- Founder block (/legal/our-structure) -------------------------------
   One portrait and three paragraphs, inside a 68ch .prose column. A float
   rather than a grid: at this measure a two-column grid leaves the text
   track around 40ch, which is too narrow to read, whereas a float gives the
   text the full width back as soon as it clears the photo.

   The source is a cutout on a near-black ground (#080c10), within a shade of
   the dark skin's --bg, so it sits almost flush there. The hairline and the
   radius are what stop it reading as a hole in the page on the LIGHT skin,
   where that same ground is a dark rectangle. Do not remove them on the
   strength of how it looks in dark. */
.founder__photo {
  float: right;
  width: 200px;
  height: auto;
  margin: 0 0 var(--s-5) var(--s-5);
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  background: var(--bg-deep);
}
.founder::after { content: ""; display: block; clear: both; }

@media (max-width: 560px) {
  /* 200px floated out of a ~330px column leaves a 130px text gutter. Stack. */
  .founder__photo {
    float: none;
    display: block;
    width: 160px;
    margin: 0 0 var(--s-4);
  }
}

/* ---- Cut-out group photo ----------------------------------------------- */
/* Deliberately NOT .figure. A .figure is a photograph in a frame: it gets a
   hairline, a radius and a plate, because a rectangle of image needs an edge
   to sit against the page. A cut-out has no rectangle. Give it the same
   border and radius and you draw a box around empty alpha, which reads as a
   broken image rather than a deliberate one.
   The photo-treatment filter still applies. That is a brightness and contrast
   lift, not a frame, and the whole point of it is that every photograph on
   the site is graded the same way. */
.cutout {
  margin: 0;
}
.cutout img {
  display: block;
  width: 100%;
  height: auto;
  filter: var(--photo-treatment);
}
/* Bottom-align the subjects to the band's floor so they stand on it rather
   than float in it, and cap the height so a wide group does not swallow the
   viewport on a large screen. */
.cutout--band {
  display: flex;
  justify-content: center;
}
.cutout--band img {
  width: auto;
  max-width: 100%;
  max-height: 30rem;
}
