/* ============================================================
   site.css - The Bridge Up Project

   One stylesheet, no build step. Read this first.

   HOW THIS FILE IS ORGANISED

   TIER 0 holds the base styles and MUST STAY FIRST: everything
   below is written to override it at equal specificity. Later
   tiers are corrections layered on top rather than edits in
   place, so source order is load-bearing. Do not reorder.

   All design tokens live in a single :root at the top - colour,
   type scale, leading, tracking, spacing, radii, shadows,
   z-index. Change them there, not at the point of use.

   TWO CONTRACTS THAT WILL BREAK THINGS IF IGNORED

   1. Anything hidden for animation must be gated behind .js on
      <html> AND resolved by the reduced-motion block in TIER 7.
      CSS that hides what the engine no longer reveals strands
      content invisible. See TIER 5 and TIER 7.

   2. The nav and footer are duplicated across four HTML files.
      scripts/check-chrome.js blocks commits where they drift.

   CONTENTS
     FONTS - self-hosted                                  L58
     TIER 0 — BASE                                        L113
     The Bridge Up Project — polish & motion layer        L510
     SCROLL REVEAL                                        L590
     TEXT APPEARANCE — word-by-word headline reveal       L624
     PARALLAX LAYERS                                      L648
     NAV                                                  L723
     BUTTONS — press feedback was missing everywhere      L790
     CARDS — resting depth so they stop reading as flat o L808
     MISSION — a gold hairline to anchor an otherwise bar L843
     PROGRAM VISUAL — the bridge builds itself.           L859
     FOOTER                                               L906
     MOBILE — scroll-linked work now runs here too, at an L928
     TIER 2 — depth, material, and alignment              L943
     TIER 3 — adaptive layout                             L1121
     NAV — every destination visible on desktop, one cont L1139
     STATS — lead figure + stacked pair, replacing 3 equa L1182
     CONNECT PATHS — two primary routes, one secondary.   L1393
     PROGRAMS — let the card grid choose its own column c L1423
     TABLET                                               L1432
     PHONE                                                L1447
     TIER 4 — cohesion                                    L1536
     TIER 5 — GSAP engine handoff                         L1646
     CTA CHOREOGRAPHY — "Span" (primary) and "Circuit" (s L1715
     TIER 6 — CROSS-DEVICE FIDELITY                       L1849
     TIER 7 — REDUCED MOTION IS NOT HONOURED              L2046
     TIER 8 — SCROLL-DRIVEN MOTION RUNS AT EVERY WIDTH    L2096
   ============================================================ */

/* ============================================================
   FONTS - self-hosted

   Google Fonts was the last third-party runtime dependency on a
   site that self-hosts everything else. It cost more than a
   request: the <link> is a render-blocking stylesheet from
   another origin, and motion.js gates every text animation on
   document.fonts.ready, so a slow font response stalled the whole
   choreography up to its 2.5s cap.

   Both families are SIL Open Font License 1.1, which permits
   redistribution; see fonts/OFL.txt. Variable woff2, latin and
   latin-ext only - the site is lang="en" and the other subsets
   were never going to be requested.

   font-display: swap is kept: text renders immediately in the
   fallback and reflows once, which beats invisible text.
   ============================================================ */

@font-face{
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/fonts/fraunces-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face{
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/fonts/fraunces-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face{
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/fonts/inter-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face{
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/fonts/inter-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ============================================================
 TIER 0 — BASE
 Was duplicated inline in all four HTML files. Consolidated here.

 MUST STAY FIRST IN THIS FILE. It occupies the cascade position
 the inline <style> blocks used to: everything below is written
 to override it at equal specificity, and it wins only where
 nothing below speaks. Move it after any tier and that tier
 silently stops applying.

 The four copies had drifted, which is what made a fixed 2rem in
 .program h3 need overriding in TIER 6 instead of simply being
 corrected. 89 distinct selectors across the copies: 84 agreed or
 were already overridden downstream, 2 conflicted only in values
 that get replaced below anyway, and 3 genuinely differed - those
 are scoped by page class at the end of this tier.
 ============================================================ */


/* ------------------------------------------------------------
   TOKENS

   Consolidated from four separate :root blocks that had
   accumulated at lines 20, 231, 623 and 1039. A designer looking
   for "the tokens" would find the palette and stop, then hard-code
   the rest - which is exactly what happened: nine text colours and
   twenty-five rgba() tuples were written by hand elsewhere in this
   file. They are all named here now.
   ------------------------------------------------------------ */
:root{
  /* --- Brand. Unchanged, and not up for negotiation. --- */
  --ink:        #1161A8;   /* cobalt: headings, primary text */
  --steel:      #2B5291;   /* accent blue: figures, mid-tone */
  --gold:       #E9BE3F;   /* accent: CTAs, eyebrows, keystone */
  /* Gold is a surface colour, not a text colour. #E9BE3F reads at
     5.9:1 on the navy hero and 1.66:1 on cream - the same token on
     the two backgrounds this site actually uses, one passing
     comfortably and one failing badly. Eyebrows set on light
     surfaces use this darker member of the same family instead
     (4.7:1 on cream, 5.0:1 on white). */
  --gold-ink:   #8C6B0E;
  --paper:      #FAF8F4;   /* page background */
  --paper-dark: #1D3F73;   /* deep navy: hero, footer */
  --line:       #DCD5C8;   /* hairline */
  --surface:    #FFFFFF;   /* card fill */

  /* --- Text. Every value here was previously a literal hex
         repeated across TIER 0. Two were below AA and have been
         darkened; see the contrast note in TIER 9. --- */
  --text-strong:    var(--ink);
  --text-body:      #3D4657;   /* long-form reading copy */
  --text-muted:     #545D6E;   /* card body, list items (was #565f6e) */
  --text-faint:     #666E7C;   /* source attributions (was #9aa2ae at 2.6:1) */
  --text-on-dark:   #F4EFE4;
  --text-on-dark-muted: #C6D0E2;
  --text-on-dark-faint: #A3B0C8;   /* footer (was #8792A8 at 3.6:1) */
  --border-on-dark: #5A6B87;

  /* --- Typography ---
         Families first. Fraunces carries an opsz axis, so the
         display roles opt into optical sizing rather than
         rendering one master at every size. */
  --font-body:    'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-display: 'Fraunces', Georgia, 'Times New Roman', serif;

  --weight-normal:   400;
  --weight-medium:   500;
  --weight-semibold: 600;
  --weight-bold:     700;

  /* Line heights. Previously FOUR declarations covered ~40 text
     roles, so every display heading inherited body's 1.55 - which
     is why .page-header h1 at 44.8px ran looser than .hero h1 at
     54.4px. Leading now tightens as size grows, which is the way
     round it should have been. */
  --leading-display: 1.08;
  --leading-heading: 1.2;
  --leading-title:   1.32;
  --leading-body:    1.6;
  --leading-tight:   1.45;

  /* Tracking ramp: bigger is tighter. */
  --track-display: -0.018em;
  --track-heading: -0.012em;
  --track-title:   -0.005em;
  --track-body:     0;
  --track-label:    0.16em;

  /* --- Spacing. An 8px base with two half-steps for control
         detail. Replaces a near-continuous run of magic numbers
         from 8 to 34 where nearly every even value appeared. --- */
  --space-3xs: 4px;
  --space-2xs: 8px;
  --space-xs:  12px;
  --space-sm:  16px;
  --space-md:  24px;
  --space-lg:  32px;
  --space-xl:  48px;
  --space-2xl: 64px;
  --space-3xl: 96px;

  /* Card padding, one pair, applied to all three card types.
     They previously used 34/30, 32/30 and 34/28. */
  --pad-card: clamp(26px, 2.6vw, 34px) clamp(24px, 2.4vw, 32px);

  /* --- Motion --- */
  --ease-out:    cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);

  --dur-press:  140ms;
  --dur-hover:  200ms;
  --dur-menu:   200ms;
  --dur-reveal: 620ms;

  /* --- Depth. Derived from the navy, never a new hue. --- */
  --shadow-rest: 0 12px 20px -8px rgba(29, 63, 115, 0.10);
  --shadow-lift: 0 18px 34px -10px rgba(29, 63, 115, 0.18);
  --shadow-nav:  0 8px 24px -12px rgba(29, 63, 115, 0.22);

  /* --- Layout --- */
  --container: 1100px;
  --gutter: clamp(20px, 6vw, 72px);
  /* One shared edge for every full-bleed block */
  --edge: max(var(--gutter), (100% - var(--container)) / 2);

  /* --- Shape. Four values, concentric: a 4px control sits inside
         an 8px surface sits inside a 16px tray. --- */
  --r-control: 4px;
  --r-surface: 8px;
  --r-tray:   16px;

  /* --- Layers --- */
  --z-nav:   10;
  --z-menu:  20;
  --z-grain: 60;
  --z-rail:  90;
  --z-skip:  80;
}

*{ box-sizing: border-box; margin:0; padding:0; }

/* Sticky footer. /about is shorter than a tall viewport, which left
   a strip of cream body background showing below the navy footer -
   it read as the footer having come loose rather than as a short
   page. dvh, not vh, so mobile browser chrome does not cause a jump. */
html{ min-height: 100%; }
body{ min-height: 100dvh; display: flex; flex-direction: column; }
body > main{ flex: 1 0 auto; }
body > footer{ flex-shrink: 0; }
body{
  font-family: var(--font-body);
  background: var(--paper);
  color: var(--ink);
  line-height: var(--leading-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
a{ color: inherit; text-decoration: none; }

/* NAV */
body > nav{
  display:flex; align-items:center; justify-content:space-between;
  padding: 22px 6%;
  background: var(--paper);
  border-bottom: 1px solid var(--line);
  position: sticky; top:0; z-index: var(--z-nav);
}
.nav-logo{ display:flex; align-items:center; gap:10px; font-family: var(--font-display); font-weight:700; font-size: 1.15rem; }
.nav-logo svg{ height: 34px; width: auto; }
.nav-links{ display:flex; align-items:center; gap: 34px; font-size: 0.92rem; font-weight:500; letter-spacing:0.02em; }
.nav-links a{ position:relative; padding-bottom:4px; }
@media (hover: hover) and (pointer: fine){
  .nav-links a:hover{ color: var(--gold); }
}
.nav-menu{ position:relative; }
.nav-menu summary{
  list-style:none; cursor:pointer; padding: 8px 16px;
  border: 1px solid var(--line); font-weight:600;
}
.nav-menu summary::-webkit-details-marker{ display:none; }
.nav-menu summary:after{ content:" \25BE"; }
.nav-menu[open] summary{ background: var(--ink); color:var(--surface); border-color:var(--ink); }
.nav-menu-panel{
  position:absolute; right:0; top: calc(100% + 8px); background:var(--surface);
  border:1px solid var(--line); min-width:180px;
  box-shadow: var(--shadow-nav); overflow:hidden; z-index: var(--z-menu);
}
.nav-menu-panel a{ display:block; padding: 13px 18px; font-weight:500; }
@media (hover: hover) and (pointer: fine){
  .nav-menu-panel a:hover{ background: var(--paper); color: var(--ink); }
}

/* HERO */
.hero{
  background: var(--paper-dark);
  color: var(--text-on-dark);
  padding: 100px 6% 130px;
  position: relative;
  overflow: hidden;
}
.hero-arc{
  position:absolute; left:0; right:0; bottom:-2px; height:140px;
  background: linear-gradient(180deg, transparent, var(--paper) 92%);
}
.hero-inner{ max-width: 760px; position:relative; }
.eyebrow{
  font-size: 0.8rem; letter-spacing: 0.18em; text-transform:uppercase;
  color: var(--gold); font-weight:600; margin-bottom: 18px; display:block;
}
/* Home only. This was a bare `h1` in index.html's block and in no
   other, so subpage headings never received it: they took only
   `.page-header h1`, which sets no line-height, and inherited 1.55
   from body. Left unscoped here it goes site-wide and silently
   pulls every subpage heading to 1.12 - measured, the /programs h1
   went from 42.2px tall to 30.5px and shifted the entire page up.
   :where() keeps the original (0,0,1) weight. */
:where(.page-home) h1{
  font-family: var(--font-display); font-weight:600; font-size: clamp(2.3rem, 4.2vw, 3.4rem);
  line-height:1.12; margin-bottom: 26px; letter-spacing:-0.01em;
}
.hero-buttons{ display:flex; gap:16px; flex-wrap:wrap; }
.btn-primary{
  background: var(--gold); color: var(--paper-dark); padding: 14px 30px;
  border-radius:3px; font-weight:700; font-size:0.95rem;
}
.btn-secondary{
  border: 1px solid var(--border-on-dark); color:var(--text-on-dark); padding: 14px 30px;
  border-radius:3px; font-weight:600; font-size:0.95rem;
}

/* STATS */
.stats{
  display:grid; grid-template-columns: repeat(3, 1fr); gap: 0;
  max-width: 1100px; margin: -70px auto 0; position:relative; z-index:2;
  padding: 0 6%;
}
.stat-card{
  background:var(--surface); border:1px solid var(--line); padding: 34px 30px;
  border-right:none;
}
.stat-card:last-child{ border-right:1px solid var(--line); }
.stat-number{ font-family: var(--font-display); font-size:2.6rem; font-weight:700; color:var(--steel); }
.stat-label{ font-size:0.92rem; color:var(--text-muted); margin-top:8px; }
.stat-source{ font-size:0.72rem; color:var(--text-faint); margin-top:10px; letter-spacing:0.02em; }

section{ padding: 100px 6%; }
.section-inner{ max-width: 1100px; margin: 0 auto; }

/* MISSION */
.mission{ text-align:center; }
.mission h2{
  font-family: var(--font-display); font-weight:600;
  margin: 0 auto; color: var(--ink);
  text-wrap: balance;
}

/* PROGRAM */
.program{ background:var(--surface); border-top:1px solid var(--line); border-bottom:1px solid var(--line); }
.program-grid{ display:grid; grid-template-columns: 1fr 1fr; gap: 60px; align-items:center; }
.program-tag{ font-size:0.78rem; letter-spacing:0.14em; text-transform:uppercase; color:var(--gold); font-weight:700; margin-bottom:16px; display:block; }
.program h3{ font-family: var(--font-display); font-size:2rem; font-weight:600; margin-bottom:18px; }
.program p{ color:var(--text-muted); margin-bottom: 22px; }
.session-list{ list-style:none; display:grid; grid-template-columns: 1fr 1fr; gap: 10px 22px; }
.session-list li{ font-size:0.9rem; color: var(--steel); padding-left:20px; position:relative; }
.session-list li:before{ content: attr(data-n); position:absolute; left:0; top:0; color:var(--gold); font-weight:700; font-size:0.78rem; }
.program-visual{
  background: var(--paper-dark); border-radius:6px; height: 340px;
  display:flex; align-items:center; justify-content:center; position:relative; overflow:hidden;
}
.program-visual svg{ width:70%; height:auto; }

/* ---------- Footer ----------

   Previously a single grey line of type. It was also the only
   place on the site whose links differed per page - each footer
   dropped the link to the page you were standing on, and none of
   the four linked home, so the footer was a partial dead end. It
   now carries the same four destinations everywhere, with
   aria-current marking where you are instead of removing it. */
footer{
  background: var(--paper-dark);
  color: var(--text-on-dark-faint);
  padding: var(--space-xl) 6% var(--space-lg);
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: var(--space-md);
  font-size: 0.85rem;
}
.footer-mark{ display: grid; gap: var(--space-3xs); }
.footer-name{
  font-family: var(--font-display);
  font-weight: var(--weight-semibold);
  font-size: 1.05rem;
  line-height: var(--leading-title);
  color: var(--text-on-dark);
}
.footer-legal{ font-size: 0.8rem; }
.footer-links{ display: flex; gap: var(--space-md); flex-wrap: wrap; }
.footer-links a[aria-current="page"]{ color: var(--text-on-dark); }
.footer-links a[aria-current="page"]::after{ transform: scaleX(1); }

@media (max-width: 760px){
  .stats{ grid-template-columns: 1fr; margin-top:0; }
  .program-grid{ grid-template-columns: 1fr; }
  .nav-links{ gap:16px; }
  .hero{ padding: 60px 6% 26px; }
  .hero-arc{ height:36px; }
  .program-visual{ height:200px; }
  footer{ padding: 36px 6% 24px; }
}

/* ---------- SUBPAGE HEADER — programs, about, connect ---------- */
.page-header{ background: var(--paper-dark); color:var(--text-on-dark); padding: 80px 6% 70px; }
.page-header h1{ font-family: var(--font-display); font-weight:600; font-size: clamp(2rem, 4vw, 2.8rem); max-width:640px; }
.page-header p{ color:var(--text-on-dark-muted); max-width:580px; margin-top:18px; font-size:1.05rem; }

/* ---------- PROGRAMS — session cards ---------- */
.session-grid{ display:grid; grid-template-columns: 1fr 1fr; gap: 26px; }
.session-card{ background:var(--surface); border:1px solid var(--line); padding: var(--pad-card); }
.session-head{ display:flex; align-items:center; gap:14px; margin-bottom:18px; }
.session-num{ width:34px; height:34px; border-radius:50%; background: var(--gold); color: var(--paper-dark); font-weight:700; font-size:0.95rem; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
.session-card h3{ font-family: var(--font-display); font-size:1.25rem; font-weight:600; }
.session-card ul{ list-style:none; }
.session-card li{ position:relative; padding-left:18px; color:var(--text-muted); font-size:0.94rem; margin-bottom:9px; }
.session-card li:before{ content:"•"; position:absolute; left:0; color: var(--gold); font-weight:700; }
.session-card li:last-child{ margin-bottom:0; }

/* ---------- CONNECT — paths ---------- */
.paths{ display:grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-md); max-width: 1080px; margin: 0 auto; }
.path-card{ background:var(--surface); border:1px solid var(--line); padding: var(--pad-card); }
.path-card h3{ font-family: var(--font-display); font-weight:600; margin-bottom: var(--space-xs); }
.path-card p{ color:var(--text-muted); margin-bottom:0; font-size:0.98rem; }

/* ---------- ABOUT — bio ---------- */
.bio p{ color:var(--text-body); font-size:1.15rem; margin-bottom: 26px; }
/* .name and .role became <p> elements (they were bare <div>s), which
   means they now inherit `.bio p{ margin-bottom }` and were sitting
   26px apart. They are one attribution block, not two paragraphs. */
.signoff{
  margin-top: var(--space-lg);
  padding-top: var(--space-md);
  border-top: 1px solid var(--line);
}
.signoff .name{
  font-family: var(--font-display);
  font-weight: var(--weight-semibold);
  font-size: 1.05rem;
  color: var(--ink);
  margin-bottom: 0;
}
.signoff .role{
  font-size: 0.9rem;
  color: var(--text-faint);
  margin-top: 2px;
  margin-bottom: 0;
}

/* ---------- The three values that genuinely disagreed ----------
   Of 89 distinct selectors across the four copies, 84 either
   matched exactly or were already overridden by the tiers below.
   Two more looked like conflicts but were moot: the `section`
   padding and the `.section-inner` max-width are both replaced
   further down
   (by the fluid clamp and by max-width:none), so whichever copy
   won made no difference to what rendered. These three are the
   only ones that reached the screen differently.

   :where() carries ZERO specificity, so each still weighs exactly
   what the original single-class rule weighed. A plain
   `.page-connect .btn-primary` would be (0,2,0) and would quietly
   start beating any future (0,1,0) rule in the tiers below - on
   one page only, which is the kind of bug that takes an afternoon
   to find. */
:where(.page-home) .eyebrow{ margin-bottom: 22px; }
:where(.page-connect) .page-header p{ max-width:560px; }
/* The hard 150px width was set when all three buttons read "Get in
   touch". Now that each names its own destination, a fixed width
   wraps every label onto two lines. Buttons size to their label and
   are told not to break; a wrapped CTA is a broken CTA. */
:where(.page-connect) .btn-primary{
  padding: 12px 26px;
  font-size: 0.92rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  white-space: nowrap;
}

@media (max-width: 760px){
  .page-header{ padding: 56px 6% 28px; }
  .paths{ grid-template-columns: 1fr; }
}


/* ============================================================
   The Bridge Up Project — polish & motion layer
   Loads AFTER each page's inline <style>. Adds depth, states,
   and motion only. Does not redefine palette or type tokens.

   Motion parameters follow the ui-ux-pro-max motion DB:
   parallax on decorative layers only (never reading copy),
   small deltas, <=3 layers, no pinning, reduced-motion gated.
   ============================================================ */

/* Tokens live in one block at the top of this file. */

/* ---------- Typographic settling (no type-scale changes) ---------- */
h1, h2, h3{ text-wrap: balance; }
p{ text-wrap: pretty; }

::selection{ background: var(--gold); color: var(--paper-dark); }

section[id]{ scroll-margin-top: 96px; }
[tabindex="-1"]{ outline: none; }   /* focus targets: focusable, not ringed */

/* No `html{ scroll-behavior: smooth }`. Anchor travel is driven from
   JS (site.js, scrollIntoView) so the focus move can be sequenced
   with it. Declaring it here as well would hand two engines the
   same scrollTop, which is what produced the stutter on desktop. */

/* ---------- Focus visibility ----------

   A single gold ring failed WCAG 2.2 SC 1.4.11 on most of the
   site. Gold #E9BE3F against cream #FAF8F4 is about 1.9:1, well
   under the 3:1 minimum for a non-text indicator - and it was
   worst on .btn-primary, which is itself gold, so the ring was
   effectively invisible on the page's main call to action.

   The fix is a two-tone ring rather than a different colour: a
   navy core carries the contrast on light surfaces (navy on cream
   is 9.4:1), and a gold outer halo carries it on the navy hero
   and footer (gold on navy is 7.1:1). One rule works everywhere,
   so nothing depends on remembering which background a control
   happens to sit on.

   box-shadow rather than a second outline so the ring follows
   each control's own border-radius. */
:where(a, summary, button, [tabindex]):focus-visible{
  outline: 2px solid var(--paper-dark);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px var(--gold);
  border-radius: var(--r-control);
}

/* Windows High Contrast strips both outline colour and shadow;
   the system keyword restores a ring it controls. */
@media (forced-colors: active){
  :where(a, summary, button, [tabindex]):focus-visible{
    outline: 3px solid Highlight;
    box-shadow: none;
  }
}

/* ---------- Focus parity for hover-only affordances ----------

   Eight components animated on :hover and did nothing at all on
   :focus-visible, so a keyboard user got the generic ring and
   none of the component's own feedback. Each pairing below gives
   focus the same treatment hover already had.

   .path-card is the important one: it is a <div>, so it cannot
   take focus itself. Tabbing to the button inside it produced no
   card-level response whatsoever. :focus-within fixes that. */
.nav-logo:focus-visible svg{ transform: translateY(-2px) rotate(-2deg); }
.nav-links > a:focus-visible::after{ transform: scaleX(1); }
.footer-links a:focus-visible::after{ transform: scaleX(1); }
.btn-arrow:focus-visible .btn-icon{
  transform: translate(3px, -1px) scale(1.06);
  background: color-mix(in srgb, var(--paper-dark) 22%, transparent);
}
.link-rule:focus-visible .link-arrow{ transform: translateX(4px); }
.nav-menu-panel a:focus-visible{ background: var(--paper); color: var(--ink); }

/* ============================================================
   SCROLL REVEAL
   Transitions (not keyframes) so a re-entering element retargets
   instead of restarting. Hidden state is gated behind .js so the
   page stays fully readable if the script never runs.
   ============================================================ */
.js [data-reveal]{
  --drift: 0px;             /* scroll-linked column offset, composed below */
  opacity: 0;
  transform: translate3d(0, calc(14px + var(--drift)), 0);
  transition:
    opacity   var(--dur-reveal) var(--ease-out),
    transform var(--dur-reveal) var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}
.js [data-reveal].is-visible{
  opacity: 1;
  transform: translate3d(0, var(--drift), 0);
}

/* Grid items get a whisper of scale so a wave of cards reads as a wave */
.js [data-reveal].reveal-card{
  transform: translate3d(0, calc(22px + var(--drift)), 0) scale(0.985);
}
.js [data-reveal].reveal-card.is-visible{
  transform: translate3d(0, var(--drift), 0) scale(1);
}

/* Once the reveal has landed, stop transitioning transform so the
   scroll-linked drift tracks the scrollbar instead of lagging it. */
.js [data-reveal].drift-live{
  transition: opacity var(--dur-reveal) var(--ease-out);
}

/* ============================================================
   TEXT APPEARANCE — word-by-word headline reveal
   Split happens in JS, so no-JS gets clean, unsplit text.
   Blur masks the crossfade so words resolve rather than slide.
   ============================================================ */
.js .word-reveal .word{
  display: inline-block;
  opacity: 0;
  transform: translateY(0.42em);
  filter: blur(6px);
  transition:
    opacity   520ms var(--ease-out),
    transform 640ms var(--ease-out),
    filter    520ms var(--ease-out);
  transition-delay: var(--word-delay, 0ms);
  will-change: transform, opacity, filter;
}
.js .word-reveal.is-visible .word{
  opacity: 1;
  transform: none;
  filter: blur(0);
}
.js .word-reveal.is-settled .word{ will-change: auto; }

/* ============================================================
   PARALLAX LAYERS
   Decorative only. Every element carrying data-parallax-* is
   aria-hidden and non-interactive.
   ============================================================ */
[data-parallax-y],
[data-parallax-x]{
  will-change: transform;
  transform: translate3d(0, 0, 0);
}
.parallax-idle[data-parallax-y],
.parallax-idle[data-parallax-x]{ will-change: auto; }

.hero-bg,
.header-bg{
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}

/* Softness comes from the gradient's own falloff rather than a
   filter: blur(). A blurred 600px layer is one of the most expensive
   things you can promote to the compositor, and the visual difference
   against a multi-stop radial gradient is negligible. */
.hero-glow{
  position: absolute;
  display: block;
  border-radius: 50%;
}
.hero-glow-a{
  width: 46vw; height: 46vw;
  max-width: 620px; max-height: 620px;
  top: -18%; right: -8%;
  background: radial-gradient(circle closest-side,
    rgba(233, 190, 63, 0.20),
    rgba(233, 190, 63, 0.11) 38%,
    rgba(233, 190, 63, 0.04) 64%,
    rgba(233, 190, 63, 0) 82%);
}
.hero-glow-b{
  width: 38vw; height: 38vw;
  max-width: 520px; max-height: 520px;
  bottom: -22%; left: -10%;
  background: radial-gradient(circle closest-side,
    rgba(17, 97, 168, 0.42),
    rgba(17, 97, 168, 0.24) 38%,
    rgba(17, 97, 168, 0.09) 64%,
    rgba(17, 97, 168, 0) 82%);
}

/* Horizontal-parallax bridge line — the only element that drifts sideways */
.hero-arcline{
  position: absolute;
  bottom: 4%;
  left: -14%;
  width: 128%;
  height: auto;
  opacity: 0.16;
}
.hero-arcline path{
  fill: none;
  stroke: var(--gold);
  stroke-width: 1.5;
  stroke-linecap: round;
}

/* Hero and page-header need to clip their layers and stack above them */
.hero-inner{ position: relative; z-index: 2; }
.hero-arc{ z-index: 1; }

.page-header{ position: relative; overflow: hidden; }
.page-header > *:not(.header-bg){ position: relative; z-index: 2; }

/* ============================================================
   NAV
   ============================================================ */
body > nav{ transition: box-shadow var(--dur-hover) ease; }
body > nav.is-scrolled{ box-shadow: var(--shadow-nav); }

.nav-logo svg{ transition: transform var(--dur-hover) var(--ease-out); }
@media (hover: hover) and (pointer: fine){
  .nav-logo:hover svg{ transform: translateY(-2px) rotate(-2deg); }
}

/* Animated underline replaces the bare colour swap.
   The link box is 44px tall for the touch target, so the rule is
   pinned just under the text rather than to the bottom of the box. */
.nav-links > a::after{
  content: "";
  position: absolute;
  left: 0; right: 0;
  bottom: calc(50% - 0.86em);
  height: 2px;
  background: var(--gold);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-hover) var(--ease-out);
}
@media (hover: hover) and (pointer: fine){
  .nav-links > a:hover::after{ transform: scaleX(1); }
}
.nav-links > a[aria-current="page"]::after{ transform: scaleX(1); }

.nav-menu summary{
  transition:
    transform        var(--dur-press) var(--ease-out),
    background-color var(--dur-hover) ease,
    border-color     var(--dur-hover) ease;
}
.nav-menu summary:active{ transform: scale(0.97); }
.nav-menu summary:after{
  display: inline-block;
  transition: transform var(--dur-menu) var(--ease-out);
}
.nav-menu[open] summary:after{ transform: rotate(180deg); }

/* Dropdown kept in the box model so it can transition, then scaled
   in from the trigger corner rather than from centre. */
.nav-menu-panel{
  display: block;
  opacity: 0;
  visibility: hidden;
  transform: scale(0.97) translateY(-6px);
  transform-origin: top right;
  transition:
    opacity    var(--dur-menu) var(--ease-out),
    transform  var(--dur-menu) var(--ease-out),
    visibility 0s linear var(--dur-menu);
}
.nav-menu[open] .nav-menu-panel{
  opacity: 1;
  visibility: visible;
  transform: none;
  transition:
    opacity    var(--dur-menu) var(--ease-out),
    transform  var(--dur-menu) var(--ease-out),
    visibility 0s;
}
.nav-menu-panel a{ transition: background-color var(--dur-hover) ease; }

/* ============================================================
   BUTTONS — press feedback was missing everywhere
   ============================================================ */
.btn-primary,
.btn-secondary{
  display: inline-block;
  transition:
    transform    var(--dur-press) var(--ease-out),
    box-shadow   var(--dur-hover) var(--ease-out),
    border-color var(--dur-hover) ease;
}
@media (hover: hover) and (pointer: fine){
  .btn-primary:hover{ transform: translateY(-2px); box-shadow: var(--shadow-lift); }
  .btn-secondary:hover{ transform: translateY(-2px); border-color: var(--gold); }
}
.btn-primary:active,
.btn-secondary:active{ transform: scale(0.97); box-shadow: none; }

/* ============================================================
   CARDS — resting depth so they stop reading as flat outlines.
   No hover lift on non-interactive cards: false affordance.
   Only .path-card (which wraps a CTA) lifts.
   ============================================================ */
.stat-card,
.session-card,
.path-card{ box-shadow: var(--shadow-rest); }

.path-card{
  transition:
    transform  var(--dur-hover) var(--ease-out),
    box-shadow var(--dur-hover) var(--ease-out);
}
@media (hover: hover) and (pointer: fine){
  .path-card:hover{ transform: translateY(-4px); box-shadow: var(--shadow-lift); }
}

/* Outside the pointer query on purpose: keyboard focus is not a
   pointer capability, so this must apply on touch devices with an
   attached keyboard too. */
.path-card:focus-within{
  transform: translateY(-4px);
  box-shadow: var(--shadow-lift);
}

.session-num{ box-shadow: 0 4px 10px -3px rgba(29, 63, 115, 0.35); }

/* Column counter-drift: adjacent columns offset slightly against each
   other as the grid crosses the viewport. Desktop only, tiny delta. */
.drift-col{
  --drift: 0px;
  transform: translate3d(0, var(--drift), 0);
}

/* ============================================================
   MISSION — a gold hairline to anchor an otherwise bare section
   ============================================================ */
.mission .section-inner::before{
  content: "";
  display: block;
  width: 46px;
  height: 3px;
  background: var(--gold);
  margin: 0 auto 30px;
  transform: scaleX(0);
  transform-origin: center;
  transition: transform 520ms var(--ease-out) 120ms;
}
.mission .section-inner.is-visible::before{ transform: scaleX(1); }

/* ============================================================
   PROGRAM VISUAL — the bridge builds itself.
   Pillars rise, deck settles, span draws, keystone lands.
   ============================================================ */
.program-visual svg [class^="arc-"]{ transform-box: fill-box; }

.js:not(.gsap) .program-visual .arc-pillar{
  transform-origin: bottom center;
  transform: scaleY(0.2);
  opacity: 0;
  transition:
    transform 520ms var(--ease-out),
    opacity   320ms var(--ease-out);
}
.js:not(.gsap) .program-visual .arc-pillar-l{ transition-delay: 120ms; }
.js:not(.gsap) .program-visual .arc-pillar-r{ transition-delay: 200ms; }

.js:not(.gsap) .program-visual .arc-deck{
  transform-origin: center;
  transform: scaleX(0.25);
  opacity: 0;
  transition:
    transform 560ms var(--ease-out) 300ms,
    opacity   320ms var(--ease-out) 300ms;
}

.js:not(.gsap) .program-visual .arc-span{
  /* --arc-len is measured at runtime via getTotalLength() */
  stroke-dasharray: var(--arc-len, 240);
  stroke-dashoffset: var(--arc-len, 240);
  transition: stroke-dashoffset 1100ms var(--ease-in-out) 560ms;
}

.js:not(.gsap) .program-visual .arc-key{
  transform-origin: center;
  transform: scale(0.6);
  opacity: 0;
  transition:
    transform 420ms var(--ease-out) 1450ms,
    opacity   300ms var(--ease-out) 1450ms;
}

.js:not(.gsap) .program-visual.is-visible .arc-pillar{ transform: scaleY(1); opacity: 1; }
.js:not(.gsap) .program-visual.is-visible .arc-deck{   transform: scaleX(1); opacity: 1; }
.js:not(.gsap) .program-visual.is-visible .arc-span{   stroke-dashoffset: 0; }
.js:not(.gsap) .program-visual.is-visible .arc-key{    transform: scale(1); opacity: 1; }

/* ============================================================
   FOOTER
   ============================================================ */
.footer-links a{
  position: relative;
  transition: color var(--dur-hover) ease;
}
.footer-links a::after{
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -3px;
  height: 1px;
  background: var(--gold);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-hover) var(--ease-out);
}
@media (hover: hover) and (pointer: fine){
  .footer-links a:hover{ color: var(--paper); }
  .footer-links a:hover::after{ transform: scaleX(1); }
}

/* ============================================================
   MOBILE — scroll-linked work now runs here too, at an amplitude
   scaled to the viewport. See the note below.
   ============================================================ */
/* The arcline and the column drift used to be switched off here.
   Both are scroll-scrubbed and need no pointer, so "phone" was
   never the real reason - cost was. The particle count is derived
   now (20 nodes on a phone rather than 44, and far fewer link
   calculations per frame), which buys back the budget these need,
   and motion.js scales their amplitude to the viewport instead of
   shipping desktop pixel values to a small screen. */

/* Reduced motion is implemented in TIER 7, at the end of this file. */


/* ============================================================
   TIER 2 — depth, material, and alignment
   Driven by the 8/10 skills: redesign-existing-projects (texture,
   tinted shadows, bottom-aligned CTAs, semantic gaps),
   high-end-visual-design (double-bezel, nested CTA icon, blur-in
   scroll entry, backdrop-blur only on sticky), design-review
   (component consistency, alignment, tabular figures).
   ============================================================ */

/* Tokens live in one block at the top of this file. */

/* ---------- Film grain: breaks digital flatness ----------
   Fixed and pointer-events-none so it never repaints on scroll. */
body::after{
  content: "";
  position: fixed;
  inset: 0;
  z-index: var(--z-grain);
  pointer-events: none;
  opacity: 0.038;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 120px 120px;
}

/* ---------- Skip link ---------- */
.skip-link{
  position: absolute;
  left: 12px;
  top: -60px;
  z-index: var(--z-skip);
  background: var(--gold);
  color: var(--paper-dark);
  padding: 10px 18px;
  border-radius: 3px;
  font-weight: 700;
  font-size: 0.9rem;
  transition: top var(--dur-hover) var(--ease-out);
}
.skip-link:focus{ top: 12px; }

/* ---------- Nav as a real material ----------
   backdrop-filter is confined to the sticky element, never to
   scrolling content, per the performance guardrail. */
@supports ((backdrop-filter: blur(12px)) or (-webkit-backdrop-filter: blur(12px))){
  body > nav{
    background: color-mix(in srgb, var(--paper) 78%, transparent);
    -webkit-backdrop-filter: blur(14px) saturate(150%);
    backdrop-filter: blur(14px) saturate(150%);
  }
}

/* ---------- Tabular figures on data ---------- */
.stat-number{ font-variant-numeric: tabular-nums; }

/* ---------- Card interiors: edge refraction ----------
   A hairline inner highlight reads as a lit top edge and stops
   the cards looking like flat rectangles cut out of the page. */
.stat-card,
.session-card,
.path-card{
  box-shadow:
    var(--shadow-rest),
    inset 0 1px 0 rgba(255, 255, 255, 0.85);
}
@media (hover: hover) and (pointer: fine){
  .path-card:hover{
    box-shadow:
      var(--shadow-lift),
      inset 0 1px 0 rgba(255, 255, 255, 0.9);
  }
}
.path-card:focus-within{
  box-shadow:
    var(--shadow-lift),
    inset 0 1px 0 rgba(255, 255, 255, 0.9);
}

/* ---------- Alignment: CTAs form one clean line ----------
   Path cards carry different copy lengths, so their buttons were
   landing at three different heights. */
.path-card{
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
.path-card .btn-primary{ margin-top: auto; }

/* Session card lists must start at the same Y across the row,
   whatever the title wraps to. */
.session-head{ min-height: 64px; }

/* ---------- Double-bezel around the bridge panel ----------
   Outer tray + inner plate, with concentric radii, so the panel
   reads as machined hardware rather than a pasted-in rectangle. */
.visual-shell{
  padding: 10px;
  background: rgba(29, 63, 115, 0.055);
  box-shadow: inset 0 0 0 1px rgba(29, 63, 115, 0.10);
}
.visual-shell .program-visual{
  box-shadow:
    inset 0 1px 0 rgba(244, 239, 228, 0.16),
    0 10px 24px -12px rgba(29, 63, 115, 0.55);
}

/* ---------- Nested-icon CTA with internal kinetic tension ---------- */
.btn-arrow{
  display: inline-flex;
  align-items: center;
  gap: 14px;
  padding-right: 10px;
}
.btn-arrow .btn-icon{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: rgba(29, 63, 115, 0.14);
  font-size: 0.95rem;
  line-height: 1;
  transition:
    transform var(--dur-hover) var(--ease-out),
    background-color var(--dur-hover) ease;
}
@media (hover: hover) and (pointer: fine){
  .btn-arrow:hover .btn-icon{
    transform: translate(3px, -1px) scale(1.06);
    background: rgba(29, 63, 115, 0.22);
  }
}

/* ---------- Scroll entry resolves out of blur ---------- */
.js [data-reveal].reveal-card{
  filter: blur(7px);
  transition:
    opacity   var(--dur-reveal) var(--ease-out),
    transform var(--dur-reveal) var(--ease-out),
    filter    var(--dur-reveal) var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}
.js [data-reveal].reveal-card.is-visible{ filter: blur(0); }
.js [data-reveal].reveal-card.drift-live{
  filter: none;
  transition: opacity var(--dur-reveal) var(--ease-out);
}



/* ---------- Extracted from inline styles (code-quality audit) ---------- */
.program-more{ margin-top: 20px; }

.link-rule{
  color: var(--steel);
  font-weight: 600;
  font-size: 0.9rem;
  border-bottom: 1px solid var(--gold);
  padding-bottom: 1px;
}
.link-rule .link-arrow{
  display: inline-block;
  transition: transform var(--dur-hover) var(--ease-out);
}
@media (hover: hover) and (pointer: fine){
  .link-rule:hover .link-arrow{ transform: translateX(4px); }
}

.stats-note{
  max-width: 1100px;
  margin: 14px auto 0;
  padding: 0 6%;
  font-size: 0.78rem;
  color: var(--text-faint);
}



/* ============================================================
   TIER 3 — adaptive layout
   The original stylesheet had a single breakpoint (760px), so
   everything between phone and desktop was unhandled. This adds
   fluid scaling plus real tablet/phone behaviour, and replaces
   the two "three equal columns" rows with asymmetric layouts.
   ============================================================ */

/* ---------- Fluid rhythm: no hard jumps between breakpoints ---------- */
section{ padding: clamp(56px, 7.5vw, 104px) 6%; }
.program-grid{ gap: clamp(32px, 5vw, 60px); }

/* Headline scale that actually works on a phone.
   The original clamp floor (2.3rem) never engaged below ~820px,
   so the hero ran to five lines on a 375px screen. */
.hero h1{ font-size: clamp(1.85rem, 6.6vw, 3.4rem); }
.page-header h1{ font-size: clamp(1.7rem, 5.4vw, 2.8rem); }

/* ============================================================
   NAV — every destination visible on desktop, one control on
   small screens (never two wrapped rows).

   Links and the menu button are different shapes (text vs.
   bordered pill), so centring their *boxes* is not enough: the
   link carried a 4px bottom pad that pushed its text 2px above
   the button's. Both now use the same 44px inline-flex box, so
   their text sits on one optical line.
   ============================================================ */
body > nav{ gap: 16px; }
.nav-logo{ min-width: 0; }
.nav-logo svg{ flex-shrink: 0; }
.nav-links{ flex-shrink: 0; gap: 30px; }

.nav-links > a{
  display: inline-flex;
  align-items: center;
  min-height: 44px;          /* touch target */
  padding-bottom: 0;         /* was 4px — the source of the 2px offset */
  white-space: nowrap;
}

.nav-menu summary{
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  white-space: nowrap;       /* "Menu" and its caret stay on one line */
}

/* Desktop: the three destinations stand on their own. The menu
   button would be a second, redundant route to the same places. */
.nav-menu{ display: none; }

/* Below 900px three labels plus the wordmark stop fitting on one
   row, so they collapse into the menu. 900 rather than 760: the
   squeeze starts well before phone widths. */
@media (max-width: 900px){
  .nav-links > a.nav-link{ display: none; }
  .nav-menu{ display: block; }
  .nav-links{ gap: 0; }
}

/* ============================================================
   STATS — lead figure + stacked pair, replacing 3 equal columns.
   Hairlines come from a 1px grid gap over a line-coloured
   background, so the seams survive any grid shape.
   ============================================================ */
.stats-wrap{
  max-width: 1100px;
  margin: -70px auto 0;
  padding: 0 6%;
  position: relative;
  z-index: 2;
}
.stats{
  display: grid;
  grid-template-columns: 1.45fr 1fr;
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  max-width: none;
  margin: 0;
  padding: 0;
  box-shadow: var(--shadow-rest);
}
.stat-card{
  border: 0 !important;
  box-shadow: none;
  background: var(--surface);
}
.stat-lead{
  grid-row: span 2;
  display: flex;
  flex-direction: column;
  /* Figure and label sit at the top, attribution pins to the bottom.
     Centring left ~270px of space floating in the middle of the cell,
     which reads as a layout accident rather than breathing room. */
  justify-content: flex-start;
  padding: clamp(30px, 3.4vw, 46px) clamp(26px, 3vw, 40px);
}
.stat-lead .stat-number{
  font-size: clamp(3rem, 5vw, 4.4rem);
  line-height: 1.02;
}
.stat-lead .stat-label{
  font-size: clamp(1rem, 1.25vw, 1.15rem);
  max-width: 34ch;
}
.stat-lead .stat-source{ margin-top: auto; padding-top: var(--space-md); }


/* ---------- About: the statement, and a way onward ----------

   The bio is one paragraph. That is the entire page, so it is set
   as a lead rather than as body copy - at body size it read as a
   caption for a heading rather than as the thing the page is for.
   The words are unchanged.

   The page also linked to nothing. In a three-page site that made
   About a dead end: you arrived, read four sentences, and had to
   go back to the nav. These two cards are the routes that were
   already implied and never offered. */
.bio-lead{
  font-size: clamp(1.15rem, 1.05rem + 0.42vw, 1.4rem);
  line-height: var(--leading-tight);
  color: var(--text-body);
  text-wrap: pretty;
}

/* Eyebrows and kickers sitting on a light surface take the darker
   gold. The hero and page-header keep the bright one - it is on
   navy there, where it passes at 5.9:1 and is the brand's voice. */
.mission .eyebrow,
.program-tag{
  color: var(--gold-ink);
}

/* ---------- Mission emphasis, and its fallback ----------

   The two phrases the mission statement exists to stress are marked
   by rough-notation at runtime, and they had NO css at all. If that
   11 KB script failed to load - or if GSAP failed earlier, which
   returns before the annotation code is ever reached - the emphasis
   silently vanished. The sentence stayed readable, so nothing looked
   broken; it just quietly stopped saying what it was written to say.

   So the fallback is the DEFAULT state, and motion.js switches it
   off by adding .rn-live once it has actually constructed the
   annotations. Failure needs no detection: every path that ends
   without a drawn annotation simply never adds the class.

   Both fallbacks are layout-neutral on purpose. A background
   gradient and a box-shadow occupy no space, so the swap to the
   hand-drawn marks cannot shift the sentence. Padding here would
   have cost a layout shift on every load that succeeded. */
.note-mark{
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;   /* underline each wrapped fragment */
}
.note-mark[data-note="underline"]{
  background: linear-gradient(var(--gold), var(--gold))
              0 92% / 100% 0.085em no-repeat;
}
.note-mark[data-note="box"]{
  /* Inner paper ring supplies the breathing room rough-notation
     gets from padding: 5, without occupying layout. */
  box-shadow: 0 0 0 3px var(--paper), 0 0 0 5px var(--ink);
  border-radius: 4px;
}

/* rough-notation is live and will draw these itself. */
.rn-live .note-mark[data-note="underline"]{ background: none; }
.rn-live .note-mark[data-note="box"]{ box-shadow: none; }

/* ---------- Screen-reader-only utility ---------- */
.sr-only{
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------- Contact routes ----------

   Three cards previously carried three buttons reading "Get in
   touch", "Get in touch" and "Email Us" - two links with
   identical text pointing at different inboxes, which is
   ambiguous out of context and exactly what a screen reader
   reads out when listing links.

   Each button now names its own destination, and the address is
   printed underneath. That last part is not decoration: a mailto:
   link is silent on a device with no mail client, so without a
   visible address those visitors get a button that does nothing
   and no way to reach anyone.
   ---------------------------------------------------------- */
.path-action{
  margin-top: auto;
  padding-top: var(--space-md);
  display: grid;
  gap: var(--space-xs);
  justify-items: start;
}
.path-address{
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  flex-wrap: wrap;
  font-size: 0.84rem;
  color: var(--text-faint);
}
.path-address .address{
  color: var(--steel);
  font-weight: var(--weight-medium);
  border-bottom: 1px solid color-mix(in srgb, var(--steel) 34%, transparent);
  word-break: break-word;
}

/* The site's first real <button>. Every state is declared, because
   a copy control that gives no feedback is worse than no copy
   control - the user cannot tell whether it worked. */
.copy-btn{
  --copy-fg: var(--text-muted);
  font: inherit;
  font-size: 0.78rem;
  font-weight: var(--weight-semibold);
  letter-spacing: 0.02em;
  color: var(--copy-fg);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--r-control);
  padding: 5px 10px;
  cursor: pointer;
  /* Pinned so the card cannot reflow when the label changes from
     "Copy" to "Copied" mid-interaction. */
  min-width: 7ch;
  text-align: center;
  transition:
    color var(--dur-hover) var(--ease-out),
    border-color var(--dur-hover) var(--ease-out),
    background var(--dur-hover) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}
@media (hover: hover) and (pointer: fine){
  .copy-btn:hover{
    --copy-fg: var(--ink);
    border-color: color-mix(in srgb, var(--ink) 45%, transparent);
    background: color-mix(in srgb, var(--ink) 5%, transparent);
  }
}
.copy-btn:active{ transform: scale(0.96); }
.copy-btn[data-state="done"]{
  --copy-fg: var(--paper-dark);
  background: var(--gold);
  border-color: var(--gold);
}
.copy-btn[data-state="error"]{
  --copy-fg: #8A3324;
  border-color: color-mix(in srgb, #8A3324 45%, transparent);
  background: color-mix(in srgb, #8A3324 7%, transparent);
}

/* Only the longest of the three addresses wrapped its Copy button
   onto a second line, so one card differed from its two siblings
   for no reason a reader could see. Below 560px all three stack,
   which is predictable rather than dependent on how many
   characters happen to be in an inbox name. */
@media (max-width: 560px){
  .path-address{
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-2xs);
  }
}

.paths-note{
  margin-top: var(--space-lg);
  font-size: 0.84rem;
  color: var(--text-faint);
  max-width: 62ch;
}

/* ---------- Section heads ----------

   Both /programs and /connect ran h1 straight to h3 - eight times
   over on programs - and the wrapping <section> had no accessible
   name, which makes it worth less than a <div> in a landmark tree.
   Each now has a real h2 that names what follows. */
.section-head{
  max-width: 62ch;
  margin-bottom: var(--space-xl);
}
.section-head h2{
  font-family: var(--font-display);
  font-weight: var(--weight-semibold);
  font-size: clamp(1.5rem, 1.1656rem + 1.371vw, 2.4rem);
  line-height: var(--leading-heading);
  letter-spacing: var(--track-heading);
  font-optical-sizing: auto;
}
.section-head p{
  margin-top: var(--space-sm);
  color: var(--text-muted);
  max-width: 56ch;
}


/* ============================================================
   CONNECT PATHS — two primary routes, one secondary.
   The page copy says "there are two clear paths"; three identical
   cards contradicted it. Contact Us is the stated catch-all, so
   it becomes a lighter full-width strip instead of a third peer.
   ============================================================ */
.paths{ grid-template-columns: 1fr 1fr; }

.path-card--wide{
  grid-column: 1 / -1;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  gap: clamp(20px, 3vw, 40px);
  padding: 24px clamp(22px, 2.6vw, 32px);
  background: rgba(255, 255, 255, 0.55);
  box-shadow: none;
}
.path-card--wide .path-card-body{ min-width: 0; }
.path-card--wide h3{ margin-bottom: 4px; }   /* size stays the shared .path-card h3 role */
.path-card--wide p{ margin-bottom: 0; max-width: 62ch; }
.path-card--wide .btn-primary{ margin-top: 0; flex-shrink: 0; }
@media (hover: hover) and (pointer: fine){
  .path-card--wide:hover{
    transform: none;
    background: var(--surface);
    box-shadow: var(--shadow-rest);
  }
}

/* ============================================================
   PROGRAMS — let the card grid choose its own column count
   instead of snapping from 2 straight to 1
   ============================================================ */
.session-grid{
  grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
  gap: clamp(16px, 2vw, 26px);
}

/* ============================================================
   TABLET
   ============================================================ */
@media (max-width: 1000px){
  .program-grid{ grid-template-columns: 1fr; }
  .program-grid .visual-shell{ order: -1; }
  .program-visual{ height: clamp(220px, 34vw, 300px); }
  .session-list{ grid-template-columns: 1fr 1fr; }
}

@media (max-width: 860px){
  .stats{ grid-template-columns: 1fr 1fr; }
  .stat-lead{ grid-row: auto; grid-column: 1 / -1; }
}

/* ============================================================
   PHONE
   ============================================================ */
@media (max-width: 760px){
  body > nav{ padding: 12px 5%; }
  .nav-logo{ font-size: 1rem; gap: 8px; }
  .nav-logo svg{ height: 28px; }
  .nav-menu-panel{ min-width: 216px; }

  .stats-wrap{ margin-top: 0; padding: 0 5%; }
  .stats{ grid-template-columns: 1fr; }
  .stat-lead{ grid-column: auto; }

  .paths{ grid-template-columns: 1fr; }
  .path-card--wide{
    flex-direction: column;
    align-items: flex-start;
    gap: 18px;
  }
  .path-card--wide .btn-primary{ margin-top: 0; }

  .session-list{ grid-template-columns: 1fr; }
  .hero-buttons{ gap: 12px; }
}

@media (max-width: 480px){
  .nav-logo{ font-size: 0.94rem; }
  .nav-logo svg{ height: 25px; }
  .nav-menu summary{ padding: 8px 13px; font-size: 0.86rem; }

  /* Full-width CTAs read better than two cramped pills */
  .hero-buttons{ flex-direction: column; align-items: stretch; }
  .hero-buttons .btn-primary,
  .hero-buttons .btn-secondary{ width: 100%; text-align: center; }
  .btn-arrow{ justify-content: center; }

  /* Column layout inherits align-items: flex-end from the row rule,
     which pushes the wordmark to the right while the link row stays
     left. Both go left. */
  footer{
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-md);
  }
}

/* The hero's bottom fade is 36px tall on phones, but the original
   mobile padding-bottom was 26px — sized for CTAs sitting on one
   row. Once they stack, the last button runs into the fade. */
@media (max-width: 760px){
  .hero{ padding-bottom: clamp(58px, 13vw, 84px); }
}

/* Touch targets: the footer links are visually small text, but on a
   tablet they still need a 44px-tall hit area. Expanding with an
   absolutely-positioned ::before keeps layout and the ::after
   underline exactly where they are. (Nav links get their height
   from the 44px inline-flex box instead.) */
.nav-links > a::before,
.footer-links a::before{
  content: "";
  position: absolute;
  inset: -12px -8px;
}
.nav-links > a::before{ inset: 0 -8px; }

/* Same treatment for the controls added or missed since:
   - .link-rule is 20px tall (its own text height) and is the only
     route from the homepage into the full curriculum
   - .address and .copy-btn are the contact page's fallback path,
     which is the one people reach for when mailto: does nothing */
.link-rule,
.path-address .address{ position: relative; }
.link-rule::before,
.path-address .address::before{
  content: "";
  position: absolute;
  inset: -12px -6px;
}
.copy-btn{ position: relative; }
.copy-btn::before{
  content: "";
  position: absolute;
  inset: -7px -4px;
}

/* The wordmark is a link and was under target on phones. */
.nav-logo{ min-height: 44px; }

/* ============================================================
   TIER 4 — cohesion
   Two problems this fixes:

   1. ALIGNMENT. The site was running two competing container
      systems. Full-bleed blocks (nav, hero, page-header, footer)
      inset by a 6% gutter, while `section > .section-inner`
      centred a 1100px box inside that already-padded area. At
      1512px that put the nav at 90.7px, section content at 206px
      and the stat cards at 297.7px — three different left edges
      on one page. The about page was worse: h1 at 90.7, bio at 406.

      Everything now shares one edge: a centred container measure
      expressed as padding, so full-bleed backgrounds keep working
      while their contents land on the same vertical line.

   2. TYPE AND RADIUS DRIFT. Same roles had drifted apart across
      pages (eyebrow vs program-tag, hero h1 vs page-header h1,
      session list sizes), and radii had spread to 3/4/6/10/18px.
   ============================================================ */

/* Tokens live in one block at the top of this file. */

/* ---------- One container edge ---------- */
body > nav,
.hero,
.page-header,
footer,
section,
.stats-wrap,
.stats-note{
  padding-left: var(--edge);
  padding-right: var(--edge);
}

/* Inner wrappers no longer centre — the edge already places them */
.section-inner,
.stats,
.paths,
.stats-wrap,
.stats-note{
  max-width: none;
  margin-left: 0;
  margin-right: 0;
}

/* The mission block is deliberately centred; keep it that way */
.mission .section-inner{ text-align: center; }
.mission h2{ margin-left: auto; margin-right: auto; }

/* Optical, not equal. The gold hairline and eyebrow sit above the
   statement, so the visual mass is top-heavy; matching the top and
   bottom padding by the numbers makes it look like it has slipped
   upward. The bottom carries a little more. */
.mission{
  padding-top: clamp(56px, 6vw, 88px);
  padding-bottom: clamp(64px, 7vw, 104px);
}

/* The bio is a reading column, so it keeps a measure — but it is
   left-aligned to the same edge as its own h1 instead of floating
   315px to the right of it. */
.bio .section-inner{ max-width: 66ch; }

/* ---------- Type roles ---------- */

/* Eyebrow and program tag are the same role: a small uppercase
   gold label above a heading. They had drifted to different
   sizes, weights and tracking. */
.eyebrow,
.program-tag{
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

/* Optical tracking scale for the serif display sizes. The hero h1
   carried -0.01em while the page-header h1 sat at normal. */
.hero h1,
.page-header h1{ letter-spacing: -0.015em; }
.mission h2{ letter-spacing: -0.01em; }
.program h3,
.stat-number{ letter-spacing: -0.005em; }

/* One card-title role across pages (was 1.25rem here, 1.4rem there) */
.session-card h3,
.path-card h3{ font-size: 1.3rem; }

/* One session-list item size (was 0.9rem here, 0.94rem there) */
.session-list li,

/* ---------- Radius scale ---------- */
.btn-primary,
.btn-secondary,
.nav-menu summary,
.skip-link{ border-radius: var(--r-control); }

.nav-menu-panel,
.session-card,
.path-card,
.stats,
.visual-shell .program-visual{ border-radius: var(--r-surface); }

.stats{ overflow: hidden; }          /* clip the seam-joined cells */
.visual-shell{
  border-radius: var(--r-tray);
  padding: 8px;                       /* 16 - 8 = 8, truly concentric */
}

/* ============================================================
   TIER 5 — GSAP engine handoff
   html.gsap is set inline in <head> and confirmed by motion.js
   (site.js removes it if the engine never initialised). While
   active, GSAP owns every animated property, so the CSS
   transition system stands down: initial hidden states stay
   (they prevent FOUC before the deferred scripts run), but
   nothing transitions — GSAP tweens from those states instead.
   ============================================================ */

/* Reveal targets keep their hidden state, lose their transitions */
.gsap [data-reveal]{ transition: none !important; }

/* GSAP owns the card blur-in: the CSS resting blur must not
   resurface when the tween clears its inline filter on complete */
.gsap [data-reveal].reveal-card{ filter: none; }

/* Headlines hide untouched until SplitText has rebuilt them */
.gsap [data-words]{ visibility: hidden; }

/* Mission hairline: driven by a CSS variable GSAP animates */
.gsap .mission .section-inner::before{
  transition: none;
  transform: scaleX(var(--rule, 0));
}

/* Bridge visual: in gsap mode the legacy .js rules above are
   scoped out with :not(.gsap) rather than overridden here —
   GSAP animates these SVG nodes via the transform ATTRIBUTE,
   and any CSS transform declaration (even `none`) would win
   over the attribute and freeze the build. */

/* Buttons: on fine pointers the magnetic system owns transform,
   so the CSS hover lift retires to avoid a first-hover jump.
   Width-gated to match the matchMedia condition in motion.js —
   below 761px the magnet is off and CSS keeps hover feedback. */
@media (min-width: 761px) and (hover: hover) and (pointer: fine){
  .gsap .btn-primary:hover,
  .gsap .btn-secondary:hover,
  .gsap .path-card:hover{ transform: none; }
}

/* Gold progress rail, scrubbed to page scroll by motion.js */
.scroll-progress{
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 3px;
  background: linear-gradient(90deg, var(--gold), color-mix(in srgb, var(--gold) 62%, var(--surface)));
  transform: scaleX(0);
  transform-origin: 0 50%;
  z-index: var(--z-rail);
  pointer-events: none;
}


/* Kill switch: if the visitor turns on reduced motion while the
   GSAP engine is live, motion.js adds gsap-off after clearing its
   inline styles, and this forces everything readable. */
.gsap.gsap-off [data-reveal],
.gsap.gsap-off .hero-inner{
  opacity: 1 !important;
  transform: none !important;
  filter: none !important;
  visibility: visible !important;
}
.gsap.gsap-off [data-words]{ visibility: visible !important; }
/* clearProps strips the inline --rule, so force the hairline in */
.gsap.gsap-off .mission .section-inner::before{ transform: scaleX(1); }

/* ============================================================
   CTA CHOREOGRAPHY — "Span" (primary) and "Circuit" (secondary)
   Replaces the magnetic-button system.

   One rule governs the whole thing: THE HIT TARGET NEVER MOVES.
   The old magnet translated the <a> toward the cursor, so near an
   edge the element slid out from under the pointer, fired
   mouseleave, started an elastic snap-back, slid under the
   pointer again and fired mouseenter mid-flight. That oscillation
   was the "glitch". Every layer below is injected INSIDE the
   anchor, is pointer-events:none, and only ever touches transform
   or opacity — so the anchor's box is stable to the pixel and the
   compositor does the work.
   ============================================================ */

.cta{
  position: relative;
  isolation: isolate;
  overflow: hidden;
  -webkit-tap-highlight-color: transparent;
}

/* Decorative stack. Out of flow, so it cannot affect the flex
   layout (or the `gap`) of .btn-arrow. */
.cta-fx{
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  border-radius: inherit;
  pointer-events: none;
}

/* Label + icon ride above the effects and own the press scale,
   so the anchor itself is never scaled out from under the cursor. */
.cta-inner{
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  transform: translateZ(0);
}
.btn-arrow .cta-inner{ gap: 14px; }

/* The split target. Kept out of the flex context so SplitText's
   characters never become flex items and reflow the label. */
.cta-label{ display: inline-block; }

/* Per-character wrapper produced by SplitText. anime.js staggers a
   translateY across these on hover, which is what reads as a wave
   travelling out from the letter the cursor arrived over. Only
   transform is touched, so each character stays on the compositor. */
/* No standing will-change. This promoted one compositor layer per
   CHARACTER for the life of the page - roughly 27 layers on the
   homepage - to serve a hover effect most visitors never trigger.
   The hint is applied by JS on pointerenter and released on leave,
   which is what will-change is actually for. */
.cta-char{ display: inline-block; }
.cta:hover .cta-char,
.cta:focus-within .cta-char{ will-change: transform; }

/* Once the engine claims a button, JS owns the icon outright: a CSS
   transition here would re-interpolate every value WAAPI commits,
   which is the exact double-smoothing that made the old magnet lag. */
.cta .btn-icon{ transition: none; }
.cta.btn-arrow:hover .btn-icon{ transform: none; }

/* ---------- Cursor-tracked light (GSAP, transform only) ---------- */
.cta-glow{
  position: absolute;
  top: 50%;
  left: 0;
  width: 150px;
  height: 150px;
  margin: -75px 0 0 -75px;
  border-radius: 50%;
  opacity: 0;
  transform: translate3d(0, 0, 0) scale(0.6);
}
.btn-primary .cta-glow{
  background: radial-gradient(circle closest-side, rgba(255,255,255,0.55), rgba(255,255,255,0) 72%);
}
.btn-secondary .cta-glow{
  background: radial-gradient(circle closest-side, rgba(233,190,63,0.42), rgba(233,190,63,0) 70%);
}

/* ---------- SECONDARY: the circuit completes ---------- */
.cta-circuit{
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.cta-trace{
  fill: none;
  stroke: var(--gold, #E9BE3F);
  /* The path sits on the viewBox edge and .cta-fx clips the outer
     half of the stroke, so 3 renders as a 1.5px inner tracer. */
  stroke-width: 3;
  vector-effect: non-scaling-stroke;
  /* pathLength=100 normalises the perimeter, so --trace reads as a
     percentage of half the border regardless of button width.
     The initial value must be declared, not defaulted, or anime.js
     has no current value to tween from. */
  --trace: 0;
  stroke-dasharray: var(--trace) 100;
}
.cta-wash{
  position: absolute;
  inset: 0;
  background: rgba(233, 190, 63, 0.16);
  transform: scaleX(0);
  transform-origin: 0% 50%;   /* JS flips this to the pointer's entry edge */
}

/* Resting state for the buttons the engine has claimed. The old
   translateY(-2px) hover is replaced by the choreography above;
   crucially the `transform` transition is gone, because the
   browser re-interpolating GSAP's per-frame transform writes over
   another 140ms was what made the magnet swim behind the cursor. */
.cta{
  transition:
    box-shadow   var(--dur-hover) var(--ease-out),
    border-color var(--dur-hover) ease;
}
@media (hover: hover) and (pointer: fine){
  .cta.btn-primary:hover{ transform: none; box-shadow: var(--shadow-lift); }
  .cta.btn-secondary:hover{ transform: none; border-color: var(--gold); }
}
.cta.btn-primary:active,
.cta.btn-secondary:active{ transform: none; }



/* ============================================================
   TIER 6 — CROSS-DEVICE FIDELITY
   One design, every screen.

   THE BUG THIS FIXES. The display scale was half fluid and half
   fixed. These shrink with the viewport:

     .hero h1        clamp(1.85rem, 6.6vw, 3.4rem)   54.4 → 29.6
     .page-header h1 clamp(1.7rem,  5.4vw, 2.8rem)   44.8 → 27.2
     .mission h2     clamp(1.8rem,  3vw,   2.4rem)   38.4 → 28.8

   while these are plain rem in the per-page <style> blocks and
   never move at all:

     .stat-number    2.6rem   41.6 at every width
     .program h3     2rem     32   at every width

   On a laptop that reads correctly, because the fixed sizes
   happen to land below the hero. On a 390px phone the hero has
   dropped to 29.6 while they have not moved, so the ordering
   INVERTS: the section heading (32) outranks the page headline
   (29.6), and the stat figures (41.6) outrank both. Nothing is
   broken or overflowing — the hierarchy is simply upside down,
   which is what made the phone read as a different site.

   THE FIX. Every display role gets one continuous ramp between
   a phone size at 390px and its CURRENT laptop size at 1440px.
   Two consequences worth stating:

   1. At >=1440px every value below equals the size already
      shipping, so the laptop rendering is unchanged. This tier
      only ever acts below 1440.
   2. The scale is COMPRESSED on phones, not scaled. Scaling the
      laptop ratios linearly would put card titles at 11px and
      the eyebrow at 7px — type has a legibility floor that does
      not scale. Compressing keeps the ORDER identical and just
      narrows the gaps between steps:

        role              390px     1440px
        stat lead #        38        70.4
        hero h1            29.6      54.4
        page-header h1     27.2      44.8
        stat other #       26        41.6
        mission h2         24        38.4
        program h3         22        32
        card h3            18        20.8
        eyebrow            11.7      12.8

   Each ramp is clamp(phone, intercept + coefficient*vw, laptop),
   solved so the middle term passes through both anchor points.
   ============================================================ */

/* ---------- Display scale ----------

   Every role below ramps between the SAME two anchors, 390px and
   1440px. That is what makes the ordering safe: straight lines
   sharing endpoints, with both endpoint sets strictly ordered,
   interpolate in parallel and cannot cross at any width in
   between. Ordering stops being something to spot-check at a few
   sample widths and becomes a property of the maths.

   This is also why the two headline roles are re-expressed here
   rather than left alone. Their old ramps — 6.6vw and 5.4vw —
   run through the ORIGIN, not through these anchors, which makes
   them piecewise: flat below ~448px, very steep through the
   middle, flat again above ~824px. A ramp of a different shape
   crosses the straight ones even when both of its endpoints are
   correctly ordered, and at 768px it did exactly that — the hero
   headline reached 50.7px while the lead figure that is supposed
   to tower over it sat at 49.7px. Re-anchoring keeps both of
   their endpoints exactly where they already were (29.6/54.4 and
   27.2/44.8) and only changes how they travel between them. */

/* Stated in rem, not px.

   These ramps shipped in px, which silently opted every display
   role out of the reader's browser font-size setting: someone who
   sets 20px because they need to gets body copy that scales and
   headlines that do not, which inverts the hierarchy for exactly
   the reader who most needs it intact. Each rem figure is the old
   px figure over 16, so nothing moves at the default root size.
   The vw term stays vw - it is viewport-relative by design and is
   what makes these fluid. */

.stat-lead .stat-number{ font-size: clamp(2.375rem, 1.6231rem + 3.086vw, 4.4rem); }
.hero h1{ font-size: clamp(1.85rem, 1.2744rem + 2.362vw, 3.4rem); }
.page-header h1{ font-size: clamp(1.7rem, 1.2913rem + 1.676vw, 2.8rem); }

/* Supporting figures — must sit below the headline, not above it */
.stat-number{ font-size: clamp(1.625rem, 1.2631rem + 1.486vw, 2.6rem); }

/* Up from a 38.4px ceiling. It still sits below .hero h1 (54.4px)
   and well below the lead figure (70.4px), so the ramp's
   guarantee that the hierarchy cannot invert is intact - but it
   now has enough presence to justify the room it is standing in.
   Below ~46ch the line breaks read as deliberate rather than
   accidental. */
.mission h2{
  font-size: clamp(1.6rem, 1.2119rem + 1.5924vw, 3rem);
  max-width: 20ch;
}
.program h3{ font-size: clamp(1.375rem, 1.1431rem + 0.952vw, 2rem); }

.session-card h3,
.path-card h3{ font-size: clamp(1.125rem, 1.06rem + 0.267vw, 1.3rem); }

.eyebrow,
.program-tag{ font-size: clamp(0.7313rem, 0.7056rem + 0.105vw, 0.8rem); }

/* ---------- Leading, matched to the size ramp above ----------

   This is the part that was missing. Four line-height
   declarations covered roughly forty text roles, so everything
   that did not explicitly opt out inherited body's 1.55. At
   44.8px that is 69px of leading — .page-header h1 was running
   LOOSER than .hero h1 at 54.4px, which reads as a mistake
   because it is one. Leading now tightens as size grows. */

.hero h1,
.page-header h1,
.stat-lead .stat-number{
  line-height: var(--leading-display);
}

.mission h2,
.program h3,
.stat-number{
  line-height: var(--leading-heading);
}

.session-card h3,
.path-card h3,
.signoff .name{
  line-height: var(--leading-title);
}

/* Optical sizing: Fraunces ships a real opsz axis and this file
   loads it. Without opting in, one master renders at every size
   and the small card titles carry display-weight contrast they
   are too small to survive. */
.hero h1,
.page-header h1,
.mission h2,
.program h3,
.stat-number,
.session-card h3,
.path-card h3,
.nav-logo{
  font-optical-sizing: auto;
}

/* The 480px override predates the ramp above and fought it: at
   2.6rem the lead figure collapsed onto the same 41.6px as its
   two supporting figures, flattening the one size relationship
   the stats block is built on. The ramp handles this width, so
   the override is simply gone rather than restated. */

/* ---------- Layout fidelity ----------

   CONTACT US, ONCE THE COLUMNS ARE GONE. On laptop .path-card--wide
   spans both columns as a deliberately lighter strip: tighter
   padding, translucent surface, no shadow. Against two solid peer
   cards beside it, that reads as exactly what it is — the
   secondary, catch-all route.

   Below 760px .paths is a single column, so all three cards are
   already the same width and the strip-vs-card contrast has
   nothing left to express. What survives is only the leftovers of
   it: a third card whose padding does not match the two above it,
   which reads as a mistake rather than a hierarchy signal. The
   padding returns to the peer value; the lighter surface stays,
   since that still says "secondary" in a single column. */
@media (max-width: 760px){
  .path-card--wide{ padding: var(--pad-card); }
}

/* ---------- Mobile text inflation guard ----------

   Android Chrome, and iOS Safari under some conditions, apply
   automatic text inflation to pages that never opt out: font
   sizes in text blocks get scaled up based on the block's width
   against the viewport. It is applied PER BLOCK, not uniformly —
   a long paragraph is boosted harder than a short heading — so
   it does not shift the page up by some constant, it reorders
   the type scale. That is exactly the relationship the ramps
   above exist to hold, and it is invisible in a desktop browser
   at a phone width, because desktop engines do no boosting.

   100% (rather than none) keeps the user's own pinch-zoom and
   accessibility text-size settings working; it only refuses the
   automatic, per-block adjustment. */
html{
  -webkit-text-size-adjust: 100%;
     -moz-text-size-adjust: 100%;
          text-size-adjust: 100%;
}

/* ============================================================
   TIER 7 — REDUCED MOTION IS NOT HONOURED

   The @media (prefers-reduced-motion: reduce) block that lived
   here has been removed, and motion.js pins honoursReduce to
   false. Every visitor gets the full animated site regardless of
   their system setting.

   Requested, and the cost belongs in the code rather than only in
   a chat log: Reduce Motion is switched on for vestibular
   disorders, migraine and seizure triggers, where motion produces
   physical symptoms - nausea, vertigo, headache - rather than
   mild annoyance. Those visitors asked their browser for less
   motion and this site declines. Nothing here degrades for them.

   To restore it, put back a block that resolves every hidden
   state to visible BEFORE it touches any duration:

     @media (prefers-reduced-motion: reduce){
       .js [data-reveal],
       .js [data-reveal].reveal-card,
       .js .word-reveal .word,
       .js [data-words],
       .gsap [data-words],
       .js .hero-inner,
       .js:not(.gsap) .program-visual [class^="arc-"]{
         opacity: 1 !important;
         visibility: visible !important;
         transform: none !important;
         filter: none !important;
       }
       .js:not(.gsap) .program-visual .arc-span{
         stroke-dasharray: none !important;
         stroke-dashoffset: 0 !important;
       }
       *, *::before, *::after{
         animation-duration: 1ms !important;
         animation-iteration-count: 1 !important;
         transition-duration: 1ms !important;
         transition-delay: 0ms !important;
       }
       .hero-glow, .hero-arcline, .scroll-progress{ display: none; }
     }

   ...and flip honoursReduce in motion.js in the same commit. The
   two layers must always agree: CSS that suppresses motion while
   the engine is still driving it leaves elements frozen part-way
   through a tween, which is worse than either choice alone.
   ============================================================ */

/* ============================================================
   TIER 8 — SCROLL-DRIVEN MOTION RUNS AT EVERY WIDTH

   The arcline draw and the column counter-drift were once
   switched off below 760px. Neither needs a pointer — both are
   scrubbed to scroll — so the gate was about frame cost, not
   capability. Deriving the particle count freed that budget back.

   What remains desktop-only is the genuinely pointer-dependent
   layer: the CTA choreography and the card tilt read a cursor
   position. There is no cursor on a touchscreen, so these are not
   switched off on phones so much as undefined there.
   ============================================================ */
