<!-- ==========================================================================
     people-page  —  WHOLE PAGE, ONE FILE
     ==========================================================================
     Everything this page needs, in one place: the markup, every stylesheet it
     uses, and every script. Paste the lot into ONE custom module's HTML pane.

     No <!doctype>, <html>, <head> or <body> — HubSpot supplies those. This is
     page content only.

     The <style> block at the end already includes cmha-base.css (the design
     tokens) as well as every module stylesheet this page needs, so there is
     nothing else to load anywhere.

     ⚠ ONE PAGE, ONE FILE. Paste two of these into the same HubSpot page and you
     get two copies of the tokens and of every module stylesheet, plus duplicate
     element ids — which breaks the forms, the FAQ accordion and every label
     that points at a field by id.
     ========================================================================== -->

<!-- ==========================================================================
     PHOTO HERO — added 27 Aug 2026. The BMHC 2024 team photo sits behind the
     purple, the same treatment as the home page.

     RETUNED 1 Sep 2026, and this time against the actual file. Every earlier
     number here was a guess, because the build sandbox cannot load remote
     images. It was measured in a real browser on Brian's machine: the image
     was decoded to a canvas and the crop, the veil and the text contrast were
     read off the pixels. What follows is measurement, not estimate.

     THE FILE IS 4084 × 4480 — PORTRAIT, and taller than it is wide. An earlier
     note in cmha-page-hero.css reasoned about this photo as "closer to a
     normal 3:2". It is not, and that mattered: a portrait forced into a wide
     band throws away even more height than a landscape one does. At the old
     520px band, roughly two thirds of the picture was never on screen.

     WHERE THE PEOPLE ACTUALLY ARE, as fractions of the image height:
       hair tops   ~15%
       faces       ~19% – 33%
       bodies      33% – 96%
     Four women, full length, in front of a step-and-repeat banner.

     ⚠ THE ONE THING STILL WORTH A HUMAN EYE: whether the file 404s in
     production. Everything else below has been seen rendered.
     ========================================================================== -->

<!-- ==========================================================================
     WHY THIS HERO IS TALLER AND BOTTOM-ANCHORED THAN EVERY OTHER ONE
     ==========================================================================
     Brian, 1 Sep 2026: "make sure you can see their faces."

     Three things were stopping that, and all three had to change together.

     1. THE VEIL WAS OPAQUE. At 0.82/0.91 the photo was texture, not a
        photograph — the faces were present and unreadable. Now 0.62/0.74.
        Worst-case white-text contrast at the new setting, measured pixel by
        pixel across the whole text region: 7.3:1 at 2560px, 7.7:1 at 1440px,
        8.3:1 on a phone. AA wants 4.5:1, so there is real headroom left; it
        was not lightened further because past about 0.54 the brand purple
        stops reading as a treatment and starts looking like a mistake.

     2. THE BAND WAS TOO SHORT TO HOLD FOUR HEADS. Heads occupy image rows
        15%–33%, an 18-point span. At 520px tall and 2560px wide, only about
        18.5 points of the image survive the crop at all — so there was no
        focus value anywhere that fit four heads with any margin. Solved by
        `cmha-hero--tall` (620px, 480px on mobile), which is an existing
        modifier, not a new one. Verified at 900 / 1200 / 1440 / 1920 / 2560
        and at phone width: heads fully in frame at every one.

     3. THE HEADLINE LANDED ACROSS THEIR FACES. With text vertically centred,
        "The People Behind the Work" sat straight over the second and third
        women. `cmha-hero--anchor-bottom` drops the text to the foot of the
        band, so the faces sit clear above the words. It also happens to
        improve contrast — the text now falls over clothing and floor rather
        than over the white banner — which is where the headroom in (1) came
        from.

     FOCUS IS 20%, not the 18% default. Raising it clips the tops of heads on
     wide monitors — the ceiling is 23.7%, set by 2560px; lowering it wastes
     the band on empty banner above their heads. 20% sits just under the
     ceiling with margin, and keeps image rows 15%–33% fully in frame at 768,
     900, 1200, 1440, 1920 and 2560px and at phone width.

     ⚠ ONE TRAP IF YOU RECALCULATE ANY OF THIS. `.cmha-base` sets border-box on
     `.cmha *` — the descendants — but not on `.cmha` itself, so the hero
     section is CONTENT-BOX. `min-height: 620px` plus `padding: 80px` renders
     as a 780px band, not 620px. I got this wrong the first time round and
     tuned the focus against a band 160px shorter than the real one. Measure
     the rendered height; do not read it off the min-height.

     ==========================================================================
     ⚠ THE BACKDROP IS THE OLD BRAND, AND YOU CAN NOW READ IT
     ==========================================================================
     The photo was taken at BMHC 2024 in front of a step-and-repeat banner for
     the CENTER FOR BLACK MATERNAL HEALTH & REPRODUCTIVE JUSTICE. At the old
     veil that was illegible mush. At a veil light enough to show faces, it is
     legible — across the top of the frame and again in the gaps between
     people, because a step-and-repeat repeats.

     That is not a bug in this build and it cannot be tuned away: the two
     things are the same knob. Any veil that shows the faces shows the banner.
     A tighter crop removes the top instance and not the ones between people,
     and costs the tops of their heads.

     ⚠ SO IT IS A DECISION FOR JUDITH AND ARIEL, on the People page of a site
     whose whole purpose is the new name:
       (a) ship it — it is a real photo of the team at a real conference, and
           an old banner in a 2024 event photo reads as history, not as a
           branding error;
       (b) go back to the heavy veil and accept that the faces do not read;
       (c) use a different photo without old-brand signage in it.
     Shipping (a) for now because it is the option that answers the request;
     (b) is one line, and (c) needs a file nobody has yet.

     PERFORMANCE: this is the full-size original — 4084 × 4480 is a large JPEG
     to send on every visit to People. HubSpot's file manager can serve a
     resized copy by appending ?width=2000 to the URL. Worth doing, but test it
     rather than assuming, since a bad parameter gives you no image at all.

     IN A REAL HUBSPOT MODULE, make this an image field so the team can swap
     the photo without editing code:
       style="--cmha-hero-photo: url('')"
     ⚠ If the photo is ever swapped, all four numbers below (focus, both veil
     stops, and the --tall / --anchor-bottom pair) were tuned to THIS picture
     and none of them carries over.
     ========================================================================== -->

<section class="cmha cmha-hero cmha-hero--image cmha-hero--photo cmha-hero--tall cmha-hero--anchor-bottom"
         data-cmha-module="page-hero">

  <!-- Background layers. Delete this block if you never use an image hero. -->
  <div class="cmha-hero__bg" aria-hidden="true"
       style="--cmha-hero-photo: url('https://46233500.fs1.hubspotusercontent-na1.net/hubfs/46233500/bmhc-2024-%20Team%20Pic-1.jpg');
              --cmha-hero-focus: center 20%;
              --cmha-hero-veil-top: 0.62;
              --cmha-hero-veil-bottom: 0.74;">
    <div class="cmha-hero__texture"></div>
    <div class="cmha-hero__overlay"></div>
  </div>

  <div class="cmha-hero__content" data-cmha-reveal-group="fade">

    <p class="cmha-hero__eyebrow">Our Team</p>

    <h1 class="cmha-hero__title">The People Behind the Work</h1>

    <p class="cmha-hero__sub">
      Meet the dedicated researchers, educators, and advocates working to advance
      women&rsquo;s health equity.
    </p>

    <!-- Optional CTA row. Delete if the page has no hero buttons. -->
    <!--
    <div class="cmha-hero__ctas">
      <a class="cmha-btn cmha-btn--primary" href="#">Primary action</a>
      <a class="cmha-btn cmha-btn--ghost"   href="#">Secondary action</a>
    </div>
    -->

  </div>
</section>


<!-- ==========================================================================
     Arms this module's scroll animation before the first paint, so content
     never flashes in at full opacity and then fades. Keep it directly after
     the markup above.

     Self-healing: if the module JS never runs (blocked, 404, editor preview),
     the armed state is dropped after 2.5s and everything is shown. Content can
     never be left invisible.
     ========================================================================== -->
<script>
(function () {
  var roots = document.querySelectorAll('[data-cmha-module="page-hero"]');
  for (var i = 0; i < roots.length; i++) {
    roots[i].classList.add('cmha-anim-ready', 'cmha-entrance');
  }
  setTimeout(function () {
    for (var i = 0; i < roots.length; i++) {
      if (!roots[i].__cmhaAnim) {
        roots[i].classList.remove('cmha-anim-ready', 'cmha-entrance');
      }
    }
  }, 2500);
})();
</script>

<section class="cmha cmha-section" data-cmha-module="featured-person">
  <div class="cmha-inner">

    <div data-cmha-reveal="fade">
      <p class="cmha-eyebrow">Senior Leadership</p>
      <h2 class="cmha-title">Founder &amp; Executive Director</h2>
      <p class="cmha-desc">Select the card to read the full profile.</p>
    </div>

    <!-- ---------------------------------------------------------------
         THE CARD
         --------------------------------------------------------------- -->
    <article class="cmha-featured" data-cmha-reveal>

      <div class="cmha-featured__media">
        <img class="cmha-featured__img"
             src="https://maternalhealthadvancement.tufts.edu/hubfs/AO%20headshot%20round%202.jpg"
             alt=""
             width="240" height="240" loading="lazy" decoding="async">
      </div>

      <div class="cmha-featured__body">
        <h3 class="cmha-featured__name">
          Dr. Ndidiamaka Amutah-Onukagha
          <span class="cmha-featured__creds">PhD, MPH</span>
        </h3>

        <p class="cmha-featured__role">
          Founder &amp; Executive Director<span class="cmha-featured__sep">·</span>Julia A. Okoro Professor of Black Maternal Health<span class="cmha-featured__sep">·</span>Assistant Dean of Diversity, Equity &amp; Inclusion
        </p>

        <p class="cmha-featured__preview">
          Founder and Director of the Center for Maternal Health Advancement and of the
          Maternal Outcomes of Translational Health Equity Research (MOTHER) Lab. Her
          scholarship examines maternal health disparities, infant mortality, reproductive
          health and social justice, and HIV/AIDS among Black women.
        </p>

        <!-- This button is the real trigger. Clicking anywhere on the card
             activates it, but the button is what keyboards and screen
             readers find. -->
        <button type="button" class="cmha-featured__trigger" data-cmha-panel-open="founder-panel">
          View full profile
          <span class="cmha-featured__arrow" aria-hidden="true">&rarr;</span>
        </button>
      </div>
    </article>

  </div>

  <!-- ---------------------------------------------------------------
       THE SLIDE-IN PANEL
       --------------------------------------------------------------- -->
  <div class="cmha-panel-root" data-cmha-panel-root>

    <div class="cmha-panel__scrim" data-cmha-panel-close hidden></div>

    <aside class="cmha-panel"
           id="founder-panel"
           role="dialog"
           aria-modal="true"
           aria-labelledby="founder-panel-name"
           hidden>

      <button type="button" class="cmha-panel__close" data-cmha-panel-close aria-label="Close profile">
        <svg viewBox="0 0 20 20" width="18" height="18" aria-hidden="true" focusable="false">
          <path d="M4 4 L16 16 M16 4 L4 16" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" fill="none"/>
        </svg>
      </button>

      <div class="cmha-panel__inner">

        <img class="cmha-panel__avatar"
             src="https://maternalhealthadvancement.tufts.edu/hubfs/AO%20headshot%20round%202.jpg"
             alt="" width="208" height="208" loading="lazy" decoding="async">

        <h2 class="cmha-panel__name" id="founder-panel-name">Dr. Ndidiamaka Amutah-Onukagha</h2>

        <p class="cmha-panel__role">
          PhD, MPH &middot; Founder &amp; Executive Director<br>
          Julia A. Okoro Professor of Black Maternal Health<br>
          Assistant Dean of Diversity, Equity &amp; Inclusion,<br>
          Public Health and Professional Degree Programs
        </p>

        <hr class="cmha-divider">

        <h3 class="cmha-panel__label">About</h3>
        <div class="cmha-panel__text">
          <p>
            Dr. Amutah-Onukagha is the Founder and Executive Director of the Center for
            Maternal Health Advancement (CMHA) and of the Maternal Outcomes of Translational
            Health Equity Research (MOTHER) Lab. She holds the Julia A. Okoro Professorship
            in Black Maternal Health in the Department of Public Health &amp; Community
            Medicine at Tufts University School of Medicine, and serves as the inaugural
            Assistant Dean of Diversity, Equity, and Inclusion for the university&rsquo;s Public
            Health and Professional Degree Programs.
          </p>
          <p>
            She founded the nation&rsquo;s largest Black maternal health conference, which draws
            participants from more than 46 states and 10 countries. She directs diversity,
            equity, and inclusion training for the KL2 and BIRCWH fellowship programs at
            Tufts CTSI, and serves on the boards of Birth Equity Justice MA and the
            Neighborhood Birth Center.
          </p>
          <p>
            Dr. Amutah-Onukagha earned her MPH from the George Washington University School
            of Public Health and Health Services and her PhD in Public Health from the
            University of Maryland, and completed a Kellogg Health Scholars postdoctoral
            fellowship in community-based participatory research and health disparities.
          </p>
        </div>

        <h3 class="cmha-panel__label">Research Focus</h3>
        <div class="cmha-panel__text">
          <p>
            Her scholarship examines maternal health disparities, infant mortality,
            reproductive health and social justice, and the experiences of Black women
            living with HIV/AIDS. She leads two multi-year studies: an NIH R01-funded
            investigation of maternal mortality and morbidity, and a Robert Wood Johnson
            Foundation interdisciplinary maternal health equity grant.
          </p>
          <p>
            Her work spans more than 80 manuscripts, six book chapters, a bestselling book,
            and a textbook on culturally responsive evaluation, and has been featured in
            <em>The Lancet</em>, TEDx, <em>USA Today</em>, MSNBC, and <em>The New York
            Times</em>. She serves on the editorial board of <em>Women&rsquo;s Health Issues</em>.
          </p>
        </div>

        <h3 class="cmha-panel__label">Recognition</h3>
        <div class="cmha-panel__text">
          <p>
            2022 John MacQueen Lecture Award, Association of Maternal and Child Health
            Programs &middot; 2020 Top 40 Under 40 Minority Leaders in Healthcare &middot;
            2019 Young Professional of the Year, American Public Health Association
            Maternal and Child Health Section.
          </p>
        </div>

        <!-- ==================================================================
             "CONTACT & LINKS" REMOVED — 27 Aug 2026, at Brian's direction.
             ==================================================================
             This panel held three buttons: Email, Publications, LinkedIn. All
             three pointed at "#" and "mailto:#" — placeholders that were never
             filled in. They rendered as real buttons and did nothing.

             They are gone rather than fixed, on purpose. The Center wants
             every route in through its own forms, so a personal mailto on the
             founder's panel works against that even once it points somewhere
             real: it routes round the contact form, lands in one person's
             inbox, and is nobody's job to answer.

             The page still has a contact route — the form on Get Involved and
             the enquiry form on Press. Those go to a monitored inbox and get
             logged in HubSpot, which a mailto never does.

             IF THIS EVER COMES BACK: a Publications link is the one with a
             real case for it, since it points outward at her work rather than
             inward at her inbox. It would need a genuine URL, and the panel
             label should say "Publications" rather than "Contact & Links".
             ================================================================== -->

      </div>
    </aside>
  </div>
</section>


<!-- ==========================================================================
     Arms this module's scroll animation before the first paint, so content
     never flashes in at full opacity and then fades. Keep it directly after
     the markup above.

     Self-healing: if the module JS never runs (blocked, 404, editor preview),
     the armed state is dropped after 2.5s and everything is shown. Content can
     never be left invisible.
     ========================================================================== -->
<script>
(function () {
  var roots = document.querySelectorAll('[data-cmha-module="featured-person"]');
  for (var i = 0; i < roots.length; i++) {
    roots[i].classList.add('cmha-anim-ready', 'cmha-entrance');
  }
  setTimeout(function () {
    for (var i = 0; i < roots.length; i++) {
      if (!roots[i].__cmhaAnim) {
        roots[i].classList.remove('cmha-anim-ready', 'cmha-entrance');
      }
    }
  }, 2500);
})();
</script>

<hr class="cmha-divider">

<!-- ==========================================================================
     TIER 2 — UNIT LEADS
     ========================================================================== -->
<section class="cmha cmha-section cmha-section--alt" data-cmha-module="people-grid">
  <div class="cmha-inner">

    <div data-cmha-reveal="fade">
      <p class="cmha-eyebrow">Unit Leads</p>
      <h2 class="cmha-title">Program Area Leadership</h2>
      <p class="cmha-desc">Select any card to read that lead&rsquo;s biography.</p>
    </div>

    <div class="cmha-grid cmha-grid--2"
         data-cmha-accordion="single"
         data-cmha-accent="on"
         data-cmha-reveal-group>

      <!-- ============ CARD — duplicate this whole <article> per person ==== -->
      <article class="cmha-person">
        <img class="cmha-person__avatar"
             src="https://maternalhealthadvancement.tufts.edu/hs-fs/hubfs/New%20Nina%20Circle%20(1).png"
             alt="" width="152" height="152" loading="lazy" decoding="async">
        <div class="cmha-person__body">
          <h3 class="cmha-person__name">Dr. Nina Ashford</h3>
          <p class="cmha-person__role">Policy Advisor<br>Clinical Assistant Professor</p>
          <span class="cmha-tag">Unit Lead</span>

          <button type="button"
                  class="cmha-person__toggle"
                  aria-expanded="false"
                  aria-controls="bio-lead-1">
            <span class="cmha-person__toggle-label">Read bio</span>
            <svg class="cmha-person__chev" viewBox="0 0 12 8" width="10" height="7" aria-hidden="true" focusable="false">
              <path d="M1 1.5 L6 6.5 L11 1.5" stroke="currentColor" stroke-width="1.5"
                    stroke-linecap="round" stroke-linejoin="round" fill="none"/>
            </svg>
          </button>

        </div>
        <div class="cmha-person__bio" id="bio-lead-1">
          <div class="cmha-person__bio-inner">
            <p>Dr. Ashford is committed to health equity through innovative person-centered
            policy, public health programming, and evaluation, with over 16 years of
            experience across government, academia, and community-based non-profits.</p>
            <p>She serves as Deputy Director for the Program Alignment Group at the CMS
            Medicare-Medicaid Coordination Office, and previously spent seven years leading
            the State and Population Health portfolio at the CMS Innovation Center. Her
            dissertation examined the influence of patient activation on the use of clinical
            preventive services among Medicare beneficiaries.</p>
          </div>
        </div>
      </article>
      <!-- ============ /CARD ============================================== -->

      <article class="cmha-person">
        <img class="cmha-person__avatar"
             src="https://maternalhealthadvancement.tufts.edu/hs-fs/hubfs/people-unit-lead-linda-hudson%20(1).png"
             alt="" width="152" height="152" loading="lazy" decoding="async">
        <div class="cmha-person__body">
          <h3 class="cmha-person__name">Dr. Linda Hudson</h3>
          <p class="cmha-person__role">Community Engaged Research Lead<br>Assistant Professor</p>
          <span class="cmha-tag">Unit Lead</span>
          <button type="button" class="cmha-person__toggle" aria-expanded="false" aria-controls="bio-lead-2">
            <span class="cmha-person__toggle-label">Read bio</span>
            <svg class="cmha-person__chev" viewBox="0 0 12 8" width="10" height="7" aria-hidden="true" focusable="false"><path d="M1 1.5 L6 6.5 L11 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>
          </button>
        </div>
        <div class="cmha-person__bio" id="bio-lead-2">
          <div class="cmha-person__bio-inner">
            <p>Dr. Hudson is an Assistant Professor at Tufts University School of Medicine.
            The strength of her contribution has been in community-based practice,
            implementation, management, and evaluation of programs designed to facilitate
            health-related behavior change by addressing individual and community level
            determinants of health.</p>
            <p>Her doctoral work examined chronic disease prevention among women, including
            protective behaviors related to lupus and obesity prevention in urban
            environments. Her recent research adapts a nationally tested cardiovascular
            disease prevention curriculum specifically for African-American women.</p>
          </div>
        </div>
      </article>

      <article class="cmha-person">
        <img class="cmha-person__avatar"
             src="https://maternalhealthadvancement.tufts.edu/hubfs/Shikhar%203-modified.png"
             alt="" width="152" height="152" loading="lazy" decoding="async">
        <div class="cmha-person__body">
          <h3 class="cmha-person__name">Dr. Shikhar Shrestha</h3>
          <p class="cmha-person__role">Maternal &amp; Child Health Epidemiological Data Lead<br>Assistant Professor</p>
          <span class="cmha-tag">Unit Lead</span>
          <button type="button" class="cmha-person__toggle" aria-expanded="false" aria-controls="bio-lead-3">
            <span class="cmha-person__toggle-label">Read bio</span>
            <svg class="cmha-person__chev" viewBox="0 0 12 8" width="10" height="7" aria-hidden="true" focusable="false"><path d="M1 1.5 L6 6.5 L11 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>
          </button>
        </div>
        <div class="cmha-person__bio" id="bio-lead-3">
          <div class="cmha-person__bio-inner">
            <p>Dr. Shrestha is an Assistant Professor in the Department of Public Health and
            Community Medicine at Tufts University School of Medicine. He earned his PhD from
            the University of New Mexico in 2019, where his dissertation examined
            post-discharge healthcare utilization in infants with neonatal opioid withdrawal
            syndrome.</p>
            <p>His research studies the opioid crisis, emphasizing prenatal opioid exposure
            and associated health outcomes, opioid overdose events, and harm reduction. He
            uses Geographic Information Systems to examine the accessibility and
            acceptability of treatment.</p>
          </div>
        </div>
      </article>

      <article class="cmha-person">
        <img class="cmha-person__avatar"
             src="https://maternalhealthadvancement.tufts.edu/hs-fs/hubfs/Stewart%2c%20Candace-modified%20(1).png"
             alt="" width="152" height="152" loading="lazy" decoding="async">
        <div class="cmha-person__body">
          <h3 class="cmha-person__name">Candace Stewart, MPH</h3>
          <p class="cmha-person__role">MOTHER Lab Lead</p>
          <span class="cmha-tag">Unit Lead</span>
          <button type="button" class="cmha-person__toggle" aria-expanded="false" aria-controls="bio-lead-4">
            <span class="cmha-person__toggle-label">Read bio</span>
            <svg class="cmha-person__chev" viewBox="0 0 12 8" width="10" height="7" aria-hidden="true" focusable="false"><path d="M1 1.5 L6 6.5 L11 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>
          </button>
          <!-- TODO: the live page lists conflicting alma maters for Candace Stewart
               (Philadelphia University / Thomas Jefferson University, and Northeastern
               University School of Law). Confirm with her before launch — omitted below. -->
        </div>
        <div class="cmha-person__bio" id="bio-lead-4">
          <div class="cmha-person__bio-inner">
            <p>Candace Stewart completed her Master of Public Health with a concentration in
            Health Policy and Management at Stony Brook University, and holds a Juris
            Doctorate with a concentration in Health Law and Policy.</p>
            <p>She has previously served as a Quality Improvement Research Associate at the
            Yale-New Haven Stroke Center and as Research Program Coordinator for the World
            Trade Center Health and Wellness Program.</p>
          </div>
        </div>
      </article>

    </div>
  </div>
</section>

<hr class="cmha-divider">


<!-- ==========================================================================
     TIER 3 — STAFF
     ========================================================================== -->
<section class="cmha cmha-section" data-cmha-module="people-grid">
  <div class="cmha-inner">

    <div data-cmha-reveal="fade">
      <p class="cmha-eyebrow">Team</p>
      <h2 class="cmha-title">Staff</h2>
      <p class="cmha-desc">Select any card to read that person&rsquo;s biography.</p>
    </div>

    <div class="cmha-grid cmha-grid--2"
         data-cmha-accordion="single"
         data-cmha-accent="on"
         data-cmha-reveal-group>

      <article class="cmha-person">
        <img class="cmha-person__avatar"
             src="https://maternalhealthadvancement.tufts.edu/hs-fs/hubfs/people-team-alysha-noel%20(1).png"
             alt="" width="152" height="152" loading="lazy" decoding="async">
        <div class="cmha-person__body">
          <h3 class="cmha-person__name">Alysha Noel</h3>
          <p class="cmha-person__role">Senior Administrative Coordinator</p>
          <span class="cmha-tag">Staff</span>
          <button type="button" class="cmha-person__toggle" aria-expanded="false" aria-controls="bio-staff-1">
            <span class="cmha-person__toggle-label">Read bio</span>
            <svg class="cmha-person__chev" viewBox="0 0 12 8" width="10" height="7" aria-hidden="true" focusable="false"><path d="M1 1.5 L6 6.5 L11 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>
          </button>
          <!-- TODO: this is the full bio currently on the live site — one sentence.
               Worth asking Alysha for two or three more. -->
        </div>
        <div class="cmha-person__bio" id="bio-staff-1">
          <div class="cmha-person__bio-inner">
            <p>Alysha Noel is a results-driven professional with a diverse background in
            operations management and community engagement.</p>
          </div>
        </div>
      </article>

      <article class="cmha-person">
        <img class="cmha-person__avatar"
             src="https://maternalhealthadvancement.tufts.edu/hs-fs/hubfs/people-team-judith-jeanty%20(1).png"
             alt="" width="152" height="152" loading="lazy" decoding="async">
        <div class="cmha-person__body">
          <h3 class="cmha-person__name">Judith Jeanty, MPH</h3>
          <p class="cmha-person__role">Center Research Project Manager<br>Annual BMHC Conference Coordinator</p>
          <span class="cmha-tag">Staff</span>
          <button type="button" class="cmha-person__toggle" aria-expanded="false" aria-controls="bio-staff-2">
            <span class="cmha-person__toggle-label">Read bio</span>
            <svg class="cmha-person__chev" viewBox="0 0 12 8" width="10" height="7" aria-hidden="true" focusable="false"><path d="M1 1.5 L6 6.5 L11 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>
          </button>
        </div>
        <div class="cmha-person__bio" id="bio-staff-2">
          <div class="cmha-person__bio-inner">
            <p>Judith Jeanty is from New Jersey, where she received her Bachelor of Science
            from Montclair State University and her Master&rsquo;s in Public Health with a
            concentration in Community Health and Prevention from Drexel University.</p>
            <p>She was previously a program management officer at the New Jersey Department
            of Health, where she managed statewide doula-funded programs. Her research
            interests include maternal and child health, health disparities, and reproductive
            health, specifically among Black women and low-income communities. She is
            passionate about increasing access to doula services and achieving health equity.</p>
          </div>
        </div>
      </article>

      <!-- Two open staff slots. Duplicate a card above when the roles are filled. -->

    </div>
  </div>
</section>

<hr class="cmha-divider">


<!-- ==========================================================================
     TIER 4 — RESEARCHERS & STUDENTS
     Per the kickoff notes, the student roster rotates by season. The two
     postdoctoral fellows below are from the live site; the student cards are
     placeholders awaiting the current roster.
     ========================================================================== -->
<section class="cmha cmha-section cmha-section--alt" data-cmha-module="people-grid">
  <div class="cmha-inner">

    <div data-cmha-reveal="fade">
      <p class="cmha-eyebrow">Team</p>
      <h2 class="cmha-title">Researchers &amp; Students</h2>
      <p class="cmha-desc">Student roster rotates by season.</p>
    </div>

    <div class="cmha-grid cmha-grid--2"
         data-cmha-accordion="single"
         data-cmha-accent="on"
         data-cmha-reveal-group>

      <article class="cmha-person">
        <img class="cmha-person__avatar"
             src="https://maternalhealthadvancement.tufts.edu/hs-fs/hubfs/Farida%20Circle%20(1).png"
             alt="" width="152" height="152" loading="lazy" decoding="async">
        <div class="cmha-person__body">
          <h3 class="cmha-person__name">Dr. Farida N. Yada</h3>
          <p class="cmha-person__role">Postdoctoral Research Fellow</p>
          <span class="cmha-tag">Researcher</span>
          <button type="button" class="cmha-person__toggle" aria-expanded="false" aria-controls="bio-res-1">
            <span class="cmha-person__toggle-label">Read bio</span>
            <svg class="cmha-person__chev" viewBox="0 0 12 8" width="10" height="7" aria-hidden="true" focusable="false"><path d="M1 1.5 L6 6.5 L11 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>
          </button>
        </div>
        <div class="cmha-person__bio" id="bio-res-1">
          <div class="cmha-person__bio-inner">
            <p>Dr. Yada&rsquo;s background is in Biology, Public Health, and Health Services
            Research. She is passionate about reproductive health, access, utilization, and
            quality of maternity services across the Black Diaspora, with a focus on
            immigrant Black women.</p>
            <p>She plans to use her research skills to leverage current knowledge and
            contribute to the growing body of evidence on birth equity. Dr. Yada is
            particularly interested in making information about respectful maternity care,
            disrupting obstetric racism, and culturally appropriate maternity care accessible
            to families and providers alike.</p>
          </div>
        </div>
      </article>

      <article class="cmha-person">
        <img class="cmha-person__avatar"
             src="https://maternalhealthadvancement.tufts.edu/hs-fs/hubfs/Heather%20Circle%20(1).png"
             alt="" width="152" height="152" loading="lazy" decoding="async">
        <div class="cmha-person__body">
          <h3 class="cmha-person__name">Dr. Heather Olden</h3>
          <p class="cmha-person__role">Postdoctoral Research Fellow</p>
          <span class="cmha-tag">Researcher</span>
          <button type="button" class="cmha-person__toggle" aria-expanded="false" aria-controls="bio-res-2">
            <span class="cmha-person__toggle-label">Read bio</span>
            <svg class="cmha-person__chev" viewBox="0 0 12 8" width="10" height="7" aria-hidden="true" focusable="false"><path d="M1 1.5 L6 6.5 L11 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>
          </button>
        </div>
        <div class="cmha-person__bio" id="bio-res-2">
          <div class="cmha-person__bio-inner">
            <p>Dr. Olden is a Postdoctoral Research Fellow tackling racial disparities in
            severe maternal mortality. She matriculated from the Harvard T.H. Chan School of
            Public Health, with a dissertation focused on hospital readiness for Medicaid
            expansion covering doula services in Massachusetts.</p>
            <p>She specializes in qualitative data collection and analysis, organizational
            strategic planning, and community and patient engagement.</p>
          </div>
        </div>
      </article>

      <!-- ==================================================================
           STUDENT PLACEHOLDER CARDS - HELD BACK, NOT DELETED
           ==================================================================
           Two "[Student Name] / [Program / Year] / [Short bio placeholder.]"
           cards rendered here until 27 Aug 2026. They came out together with
           the build note under the page, which was the thing explaining them.

           Removing the note and leaving these would have been the worst of
           both: a client sees bracketed placeholder names with nothing to say
           why. The page now simply ends after Dr. Olden, which reads finished.

           TO ADD THE ROSTER: uncomment ONE card, fill in name, program and
           bio, then duplicate it per student. Give each a UNIQUE id -
           bio-res-3, bio-res-4, bio-res-5 ... - and make each button's
           aria-controls match its own bio's id. Duplicate ids silently break
           the toggle for every card sharing them.

           (Inner "- -" sequences below are escaped so this comment does not
           close early. Restore them to double hyphens when you uncomment.)

           <!- - PLACEHOLDER — awaiting the current student roster. Duplicate per student. - ->
           <article class="cmha-person">
           <div class="cmha-person__avatar" aria-hidden="true"></div>
           <div class="cmha-person__body">
           <h3 class="cmha-person__name">[Student Name]</h3>
           <p class="cmha-person__role">[Program / Year]</p>
           <span class="cmha-tag">Student</span>
           <button type="button" class="cmha-person__toggle" aria-expanded="false" aria-controls="bio-res-3">
           <span class="cmha-person__toggle-label">Read bio</span>
           <svg class="cmha-person__chev" viewBox="0 0 12 8" width="10" height="7" aria-hidden="true" focusable="false"><path d="M1 1.5 L6 6.5 L11 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>
           </button>
           </div>
           <div class="cmha-person__bio" id="bio-res-3">
           <div class="cmha-person__bio-inner"><p>[Short bio placeholder.]</p></div>
           </div>
           </article>
           
           <article class="cmha-person">
           <div class="cmha-person__avatar" aria-hidden="true"></div>
           <div class="cmha-person__body">
           <h3 class="cmha-person__name">[Student Name]</h3>
           <p class="cmha-person__role">[Program / Year]</p>
           <span class="cmha-tag">Student</span>
           <button type="button" class="cmha-person__toggle" aria-expanded="false" aria-controls="bio-res-4">
           <span class="cmha-person__toggle-label">Read bio</span>
           <svg class="cmha-person__chev" viewBox="0 0 12 8" width="10" height="7" aria-hidden="true" focusable="false"><path d="M1 1.5 L6 6.5 L11 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>
           </button>
           </div>
           <div class="cmha-person__bio" id="bio-res-4">
           <div class="cmha-person__bio-inner"><p>[Short bio placeholder.]</p></div>
           </div>
           </article>
           ================================================================== -->

    </div>
  </div>
</section>


<!-- ==========================================================================
     Arms this module's scroll animation before the first paint, so content
     never flashes in at full opacity and then fades. Keep it directly after
     the markup above.

     Self-healing: if the module JS never runs (blocked, 404, editor preview),
     the armed state is dropped after 2.5s and everything is shown. Content can
     never be left invisible.
     ========================================================================== -->
<script>
(function () {
  var roots = document.querySelectorAll('[data-cmha-module="people-grid"]');
  for (var i = 0; i < roots.length; i++) {
    roots[i].classList.add('cmha-anim-ready', 'cmha-entrance');
  }
  setTimeout(function () {
    for (var i = 0; i < roots.length; i++) {
      if (!roots[i].__cmhaAnim) {
        roots[i].classList.remove('cmha-anim-ready', 'cmha-entrance');
      }
    }
  }, 2500);
})();
</script>

<script>
/* ==========================================================================
   CMHA — OUTBOUND CLICK TRACKING
   ==========================================================================
   WHAT PROBLEM THIS SOLVES

   HubSpot counts page views on your own pages automatically, and it records
   form submissions. It records NOTHING when a visitor clicks a link that
   leaves the site. The feature that would do it for you — AutoCapture — is
   Enterprise-only on every hub.

   That matters here more than on most sites, because the actions that count
   are nearly all off-site:

     Doula Toolkit   → the walkthrough form (share.hsforms.com)
                     → the "which toolkit" survey (Qualtrics)
     Get Involved    → the donate button (GiveCampus)
     Press, Reports  → 40-odd articles and PDFs elsewhere

   Without this, "how many people actually started enrolment from our page"
   is a number nobody can produce.

   ==========================================================================
   HOW IT WORKS

   On an outbound click it records a virtual page view with a made-up path:

       /clicked/doula-toolkit/open-the-walkthrough-guide

   That shows up in Reports → Analytics Tools → Traffic Analytics → Pages,
   alongside real pages, so clicks become countable next to the views that
   produced them. setPath + trackPageView is HubSpot's documented API, not a
   trick — but the paths ARE synthetic, which is why every one starts with
   /clicked/. Anyone reading the report later can tell at a glance which rows
   are pages and which are clicks, and can filter on that prefix.

   ==========================================================================
   WHERE TO PUT IT

   BEST: once, in Settings → Website → Pages → site footer HTML, wrapped in
   <script> tags. One copy, every page, nothing to keep in sync.

   ALSO FINE: paste into any module's JS pane. It guards against running twice
   (window.__cmhaTrackReady), so having it in several modules is harmless —
   the second and third copies do nothing.

   ==========================================================================
   THINGS WORTH KNOWING BEFORE YOU TRUST THE NUMBERS

   1. IT COUNTS CLICKS, NOT ARRIVALS. Someone can click and close the tab
      before the page loads. Treat these as intent, not conversions. The real
      conversion for the walkthrough is the form submission, which HubSpot
      already records properly.

   2. LINKS THAT OPEN IN THE SAME TAB ARE LESS RELIABLE. If the browser starts
      unloading before HubSpot's queue flushes, the hit can be lost. Every
      outbound link in these modules carries target="_blank", so this is not
      currently an issue — but if anyone removes that, the tracking gets
      quietly worse rather than visibly broken.

   3. IT RESPECTS COOKIE CONSENT AUTOMATICALLY. It goes through the same
      HubSpot queue as everything else, so if the portal has a consent banner
      and a visitor declines, nothing is recorded. That is correct behaviour,
      and it means these counts are a floor, not a total.

   4. TO EXCLUDE A LINK: add data-cmha-no-track to it.
   ========================================================================== */
(function () {
  'use strict';

  /* One instance per page, however many modules this gets pasted into. */
  if (window.__cmhaTrackReady) return;
  window.__cmhaTrackReady = true;

  var PREFIX = '/clicked';
  var MAX_SEGMENT = 60;

  /* Turns link text into something readable in a report. Falls back through
     aria-label and title before giving up and using the destination host —
     an icon-only link still produces a row you can identify. */
  function slug(text) {
    return String(text || '')
      .replace(/\s+/g, ' ')
      .trim()
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/^-+|-+$/g, '')
      .slice(0, MAX_SEGMENT) || '';
  }

  function labelFor(a) {
    /* Screen-reader-only spans like "(opens in a new tab)" are inside the
       link text, and would be in every single label. Strip them by reading
       only the parts a sighted visitor sees. */
    var clone = a.cloneNode(true);
    Array.prototype.forEach.call(clone.querySelectorAll('.cmha-sr-only'),
      function (el) { el.parentNode.removeChild(el); });

    return slug(clone.textContent) ||
           slug(a.getAttribute('aria-label')) ||
           slug(a.getAttribute('title')) ||
           slug(a.hostname) ||
           'link';
  }

  function pageSlug() {
    var path = (window.location.pathname || '/')
      .replace(/^\/+|\/+$/g, '')
      .replace(/\.html?$/i, '');
    return slug(path) || 'home';
  }

  /* Outbound means: goes somewhere this site does not control. mailto: and
     tel: count — an email to doulatoolkit@tufts.edu is a real action and is
     otherwise as invisible as any other off-site click. */
  function destinationOf(a) {
    var href = a.getAttribute('href') || '';
    if (!href || href.charAt(0) === '#') return null;

    var proto = (a.protocol || '').toLowerCase();
    if (proto === 'mailto:' || proto === 'tel:') {
      return proto.replace(':', '');
    }
    if (proto !== 'http:' && proto !== 'https:') return null;
    if (a.hostname && a.hostname !== window.location.hostname) return 'external';
    return null;   // same host — HubSpot already counts the page view
  }

  function record(path) {
    try {
      var hsq = window._hsq = window._hsq || [];
      var real = window.location.pathname + window.location.search;

      hsq.push(['setPath', path]);
      hsq.push(['trackPageView']);
      /* Put the path back. setPath is sticky — anything tracked afterwards
         would otherwise be attributed to the virtual path, so a visitor who
         clicks an outbound link and later submits a form would have that
         submission filed under /clicked/... instead of the real page. */
      hsq.push(['setPath', real]);
    } catch (e) {
      /* Analytics must never break a page. If HubSpot's queue is not there,
         or is something unexpected, the click still goes through. */
    }
  }

  document.addEventListener('click', function (e) {
    /* Not e.target — the click usually lands on a <span> inside the link. */
    var a = e.target && e.target.closest && e.target.closest('a[href]');
    if (!a) return;
    if (a.hasAttribute('data-cmha-no-track')) return;

    var kind = destinationOf(a);
    if (!kind) return;

    record(PREFIX + '/' + pageSlug() + '/' + labelFor(a));
  }, true);   /* capture: still fires if something later calls stopPropagation */
})();


/* ==========================================================================
   MODULE: CMHA Page Hero — JS
   Files for this module:
     html/cmha-page-hero.html          → paste into the module's HTML + HubL pane
     css/cmha-page-hero.css            → paste into the module's CSS pane
     js/cmha-page-hero.js              → paste into the module's JS pane

   Self-contained scroll animation. No site-wide script, no library.
   The animation plays in BOTH directions: content fades up as it enters and
   fades back out as it leaves, so scrolling up re-runs it.
   ========================================================================== */
(function () {
  'use strict';

  var SELECTOR = '[data-cmha-module="page-hero"]';
  var STAGGER_CAP = 10;
  var ENTRANCE_MS = 850;      // keep in sync with --cmha-entrance-dur

  function reveal(root) {
    if (root.__cmhaAnim) return;
    root.__cmhaAnim = true;

    var reduced = window.matchMedia &&
                  window.matchMedia('(prefers-reduced-motion: reduce)').matches;

    // Give each child of a [data-cmha-reveal-group] an incrementing delay.
    var groups = root.querySelectorAll('[data-cmha-reveal-group]');
    Array.prototype.forEach.call(groups, function (group) {
      var variant = group.getAttribute('data-cmha-reveal-group') || '';
      Array.prototype.forEach.call(group.children, function (kid, i) {
        if (!kid.hasAttribute('data-cmha-reveal')) {
          kid.setAttribute('data-cmha-reveal', variant);
        }
        kid.style.setProperty('--cmha-reveal-i', String(Math.min(i, STAGGER_CAP)));
      });
    });

    var els = root.querySelectorAll('[data-cmha-reveal]');
    if (!els.length) return;

    // No observer support, or the visitor asked for reduced motion:
    // show everything and drop the armed state entirely.
    if (reduced || !('IntersectionObserver' in window)) {
      root.classList.remove('cmha-anim-ready', 'cmha-entrance');
      Array.prototype.forEach.call(els, function (el) { el.classList.add('is-revealed'); });
      return;
    }

    /* ---- INITIAL PAGE ENTRANCE ----------------------------------------
       The inline script in this module's HTML added .cmha-entrance before the
       first paint, so the module is already hidden and nothing flashes.

       If the module is on screen at load, it fades in as ONE unit and its
       children skip their individual reveal for this first pass — a fade
       inside a fade reads as mush. If the module is below the fold, drop the
       entrance now and let the per-element scroll reveal handle it. */
    if (root.classList.contains('cmha-entrance')) {
      var box = root.getBoundingClientRect();
      var onScreenAtLoad = box.top < window.innerHeight && box.bottom > 0;
      /* "unit" (default) fades the whole module in at once, with its children
         already revealed — a fade inside a fade reads as mush.
         "sequence" instead drops the module's own hidden state immediately and
         lets the children arrive one after another on their own stagger. Worth
         it on a sparse page where the entrance carries the weight; too much on
         a dense one. */
      var mode = root.getAttribute('data-cmha-entrance') || 'unit';

      if (!onScreenAtLoad) {
        root.classList.remove('cmha-entrance');
      } else if (mode === 'sequence') {
        // The children are still armed hidden, so nothing flashes — only the
        // module wrapper stops being hidden.
        root.classList.remove('cmha-entrance');
      } else {
        /* Mark as revealed only what is actually inside the observer's band —
           the same band the IntersectionObserver below uses. Marking every
           element revealed showed the ones past the fold at full opacity for a
           frame, and the observer's first callback then faded them back out.
           Below the fold that is invisible, but an element peeking a few pixels
           over the bottom edge visibly appeared and then dissolved. */
        var bandTop = window.innerHeight * 0.06;
        var bandBottom = window.innerHeight * 0.90;
        Array.prototype.forEach.call(els, function (el) {
          var r = el.getBoundingClientRect();
          if (r.top < bandBottom && r.bottom > bandTop) el.classList.add('is-revealed');
        });
        // Two frames: the first lets the browser commit the hidden state, the
        // second starts the transition from it.
        requestAnimationFrame(function () {
          requestAnimationFrame(function () { root.classList.add('is-entered'); });
        });
        // Clean up afterwards so nothing is left holding an opacity or a
        // transform on the module root.
        setTimeout(function () {
          root.classList.remove('cmha-entrance', 'is-entered');
        }, ENTRANCE_MS + 150);
      }
    }

    var io = new IntersectionObserver(function (entries) {
      entries.forEach(function (entry) {
        var el = entry.target;
        if (entry.isIntersecting) {
          el.classList.add('is-revealed');
          return;
        }
        // Left the viewport. Remember which edge, so it animates back in from
        // the side it went out — the motion follows the scroll direction.
        var topEdge = entry.rootBounds ? entry.rootBounds.top : 0;
        var wentUp = entry.boundingClientRect.top < topEdge;
        el.style.setProperty('--cmha-reveal-dir', wentUp ? '-1' : '1');
        el.classList.remove('is-revealed');
      });
    }, {
      // Start a little inside the viewport so the fade completes as the
      // element settles rather than finishing off-screen.
      rootMargin: '-6% 0px -10% 0px',
      threshold: 0
    });

    Array.prototype.forEach.call(els, function (el) { io.observe(el); });
  }

  /* ======================================================================
     BACKGROUND VIDEO
     ----------------------------------------------------------------------
     Three jobs: decide whether this visitor should get the video at all,
     keep it silent, and give them a way to stop it.

     The src lives in data-src so that "decide" happens BEFORE the download,
     not after. A hero video that a visitor never sees but has already paid
     for is the worst of both.
     ====================================================================== */
  var MIN_WIDTH = 700;   // below this the video is mostly cropped away

  function wantsVideo() {
    // 1. Reduce-motion is an explicit request. A looping reel is exactly what
    //    it is asking us not to do.
    if (window.matchMedia &&
        window.matchMedia('(prefers-reduced-motion: reduce)').matches) return false;

    // 2. Data Saver, where supported. Not universal, so treated as a bonus
    //    signal rather than something to rely on.
    var c = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
    if (c) {
      if (c.saveData) return false;
      if (/^(slow-)?2g$/.test(c.effectiveType || '')) return false;
    }

    // 3. Narrow screens: cover-cropping a 16:9 reel into a phone-width hero
    //    throws most of it away, and phones are likeliest to be metered.
    if (window.innerWidth < MIN_WIDTH) return false;

    return true;
  }

  function silence(video) {
    /* Belt and braces. Autoplay is gated on muted everywhere, so an unmuted
       video would not just make noise, it would refuse to start — but Safari
       has shipped versions that ignore the attribute, so set the property
       too, and zero the volume so an unmuting elsewhere still makes no sound. */
    video.muted = true;
    video.defaultMuted = true;
    video.volume = 0;
  }

  function initVideo(root) {
    var video = root.querySelector('.cmha-hero__video');
    if (!video || video.__cmhaVideoReady) return;
    video.__cmhaVideoReady = true;

    var toggle = root.querySelector('[data-cmha-motion-toggle]');
    var src = video.getAttribute('data-src');
    if (!src) return;

    if (!wantsVideo()) {
      /* Never assign src. Nothing is fetched, the gradient stands, and the
         pause button stays hidden because there is nothing to pause. */
      return;
    }

    silence(video);
    video.setAttribute('preload', 'auto');
    video.src = src;

    /* ====================================================================
       WHY THIS IS MORE STUBBORN THAN IT LOOKS
       --------------------------------------------------------------------
       Reported 27 Aug 2026: the video disappeared on refresh. The first
       version treated a single play() rejection as final — it stripped the
       class and hid the button forever. That was wrong, because the common
       reasons play() fails on a reload are all TEMPORARY:

         - play() is called the instant src is set, before a byte of a remote
           file has arrived. On a cold load the browser waits; on a reload it
           can reject outright while it revalidates the cache.
         - the browser restores your scroll position, so the hero is off
           screen when the script runs, the observer pauses it, and the
           'playing' event that would have revealed it never fires.
         - an autoplay block that a later user gesture would lift.
         - a bfcache restore (back button), which hands back a paused video
           and no events at all.

       So nothing here is treated as terminal except a genuine decode/network
       error on the file itself. `ensure()` is safe to call as often as we
       like, and it is called from every event that could mean "now you can
       try again". A retry costs nothing when the video is already playing.
       ==================================================================== */
    var wanted = true;      // what the visitor last asked for, via the button
    var dead = false;       // set only by a real media error
    var tries = 0;

    function markAvailable() {
      if (!toggle) return;
      toggle.classList.add('is-available');
      /* Sync on reveal, not only on play/pause events. A video that never
         started never fires 'pause' — it is BORN paused — so without this the
         button appears reading "Pause background video" over a still frame,
         which is the one state a blocked autoplay actually produces. */
      syncToggle();
    }

    /* The button must describe the VIDEO, not our intention for it. If autoplay
       is blocked, or a retry has not landed yet, the video is stopped — and a
       button reading "Pause background video" over a still frame is a lie a
       screen reader will read out. Driven off the media events so it can never
       drift from reality. */
    function syncToggle() {
      if (!toggle) return;
      var stopped = video.paused;
      toggle.classList.toggle('is-paused', stopped);
      toggle.setAttribute('aria-label',
        stopped ? 'Play background video' : 'Pause background video');
    }
    video.addEventListener('play', syncToggle);
    video.addEventListener('pause', syncToggle);

    function ensure() {
      /* NOTE: no document.hidden check here. An earlier version had one and it
         was a mistake — a page opened in a background tab, or in a preview
         panel that reports itself hidden, never got its first play() at all
         and the hero stayed blank for good. Browsers already throttle muted
         background video, and the visibilitychange handler below pauses it
         properly, so blocking the attempt bought nothing and cost everything. */
      if (dead || !wanted) return;
      silence(video);
      var p = video.play();
      if (p && typeof p.catch === 'function') {
        p.catch(function () {
          /* Do NOT hide anything. Back off briefly and try again — the first
             few rejections after a reload usually clear on their own. */
          if (dead || !wanted) return;
          if (tries++ < 8) setTimeout(ensure, 250 * tries);
        });
      }
    }

    /* Last resort. If nothing is playing a few seconds in and no real error
       has been reported, the element is wedged rather than broken — a request
       aborted by a reload leaves it in exactly that state. Re-load the source
       once and try again. Cheap, and it costs nothing when all is well. */
    setTimeout(function () {
      if (dead || !wanted || !video.paused) return;
      try { video.load(); } catch (e) {}
      tries = 0;
      ensure();
    }, 4000);

    /* 'playing' without {once} so recovery re-reveals it. 'canplay' reveals
       the control as soon as there is something to control, which no longer
       waits on playback actually starting. */
    video.addEventListener('playing', function () {
      tries = 0;
      video.classList.add('is-playing');
      markAvailable();
    });
    video.addEventListener('canplay', function () { markAvailable(); ensure(); });
    video.addEventListener('loadeddata', ensure);

    /* RETRY BY ATTEMPT COUNT, NOT BY ERROR CODE. I tried the clever version
       first and it was wrong.

       The plan was: treat MEDIA_ERR_ABORTED (1) and MEDIA_ERR_NETWORK (2) as
       recoverable, and DECODE (3) / SRC_NOT_SUPPORTED (4) as fatal. Sound in
       theory. In practice, a request the browser cancels — which is precisely
       what a reload does to an in-flight video — reports code **4**, the same
       code as a URL that does not exist. Measured, not assumed. So the one
       failure this most needed to survive was the one the classification threw
       away.

       Since the codes cannot separate "temporarily failed" from "permanently
       wrong", attempts do. Three tries with a growing gap, then stop. A dead
       URL costs three requests and goes quiet; a transient failure recovers.
       The code still goes in the log, because it is the first thing anyone
       debugging this will want. */
    var errorRetries = 0;
    video.addEventListener('error', function () {
      var code = (video.error && video.error.code) || 0;

      if (errorRetries < 3 && wanted) {
        errorRetries++;
        if (window.console) {
          console.warn('[CMHA] Hero video error, code ' + code +
            ' — retry ' + errorRetries + ' of 3. src: ' + video.currentSrc);
        }
        setTimeout(function () {
          try { video.load(); } catch (e) {}
          tries = 0;
          ensure();
        }, 500 * errorRetries);
        return;
      }

      if (window.console) {
        console.warn('[CMHA] Hero video error, code ' + code +
          ' — giving up after 3 retries. The gradient hero stands. src: ' +
          video.currentSrc);
      }
      dead = true;
      video.classList.remove('is-playing');
      if (toggle) toggle.classList.remove('is-available');
    });

    /* Autoplay blocks lift after any user gesture. One cheap listener, then
       it takes itself off. */
    ['pointerdown', 'keydown', 'touchstart'].forEach(function (evt) {
      document.addEventListener(evt, function once() {
        document.removeEventListener(evt, once);
        tries = 0;
        ensure();
      }, { passive: true });
    });

    /* Back/forward navigation restores the page from bfcache with the video
       paused and no media events fired. pageshow is the only signal. */
    window.addEventListener('pageshow', function () { tries = 0; ensure(); });

    ensure();

    if (toggle) {
      toggle.addEventListener('click', function () {
        wanted = video.paused;
        tries = 0;
        if (wanted) { ensure(); } else { video.pause(); }
        /* A click is also a user gesture, so a play() that autoplay policy was
           refusing will now be allowed — which is why the button works as a
           manual rescue even when nothing else has. syncToggle runs off the
           resulting play/pause event rather than here, so the label follows
           what actually happened rather than what we asked for. */
      });
    }

    /* Stop paying for frames nobody is looking at — scrolled past, or the tab
       in the background. Never overrides an explicit pause. */
    if ('IntersectionObserver' in window) {
      new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!wanted) return;
          if (e.isIntersecting) { tries = 0; ensure(); } else { video.pause(); }
        });
      }, { threshold: 0 }).observe(root);
    }

    document.addEventListener('visibilitychange', function () {
      if (!wanted) return;
      if (document.hidden) { video.pause(); } else { tries = 0; ensure(); }
    });
  }

  function boot() {
    Array.prototype.forEach.call(document.querySelectorAll(SELECTOR), function (root) {
      reveal(root);
      initVideo(root);
    });
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', boot);
  } else {
    boot();
  }

  // Re-init if the HubSpot editor injects or reorders modules.
  if ('MutationObserver' in window) {
    var pending = null;
    new MutationObserver(function () {
      clearTimeout(pending);
      pending = setTimeout(boot, 150);
    }).observe(document.documentElement, { childList: true, subtree: true });
  }
})();


/* ==========================================================================
   MODULE: CMHA Featured Person
   Files for this module:
     html/cmha-featured-person.html    → paste into the module's HTML + HubL pane
     css/cmha-featured-person.css      → paste into the module's CSS pane
     js/cmha-featured-person.js        → paste into the module's JS pane
   Uses tokens from css/cmha-base.css. The scroll animation is self-contained
   in this module's own CSS and JS — there is no site-wide animation script.

   Handles: this module's own scroll animation, plus panel open / close,
   click-anywhere-on-card, Esc, click-outside, focus trap, focus restore,
   and background scroll lock.

   Safe to have several instances on one page — every listener is scoped to
   its own module root, and panels are matched by id.
   ========================================================================== */
(function () {
  'use strict';

  var FOCUSABLE = [
    'a[href]', 'button:not([disabled])', 'input:not([disabled])',
    'select:not([disabled])', 'textarea:not([disabled])', '[tabindex]:not([tabindex="-1"])'
  ].join(',');

  var STAGGER_CAP = 10;
  var ENTRANCE_MS = 850;      // keep in sync with --cmha-entrance-dur
  var reduced = window.matchMedia &&
                window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  /* ------------------------------------------------------- scroll reveal  */
  /* Self-contained: this module animates on its own, with no site-wide
     script. Plays in both directions so scrolling up re-runs it. */
  function initReveal(root) {
    var groups = root.querySelectorAll('[data-cmha-reveal-group]');
    Array.prototype.forEach.call(groups, function (group) {
      var variant = group.getAttribute('data-cmha-reveal-group') || '';
      Array.prototype.forEach.call(group.children, function (kid, i) {
        if (!kid.hasAttribute('data-cmha-reveal')) {
          kid.setAttribute('data-cmha-reveal', variant);
        }
        kid.style.setProperty('--cmha-reveal-i', String(Math.min(i, STAGGER_CAP)));
      });
    });

    var els = root.querySelectorAll('[data-cmha-reveal]');
    if (!els.length) return;

    if (reduced || !('IntersectionObserver' in window)) {
      root.classList.remove('cmha-anim-ready', 'cmha-entrance');
      Array.prototype.forEach.call(els, function (el) { el.classList.add('is-revealed'); });
      return;
    }

    /* ---- INITIAL PAGE ENTRANCE ----------------------------------------
       The inline script in this module's HTML added .cmha-entrance before the
       first paint, so the module is already hidden and nothing flashes.

       If the module is on screen at load, it fades in as ONE unit and its
       children skip their individual reveal for this first pass — a fade
       inside a fade reads as mush. If the module is below the fold, drop the
       entrance now and let the per-element scroll reveal handle it. */
    if (root.classList.contains('cmha-entrance')) {
      var box = root.getBoundingClientRect();
      var onScreenAtLoad = box.top < window.innerHeight && box.bottom > 0;

      if (!onScreenAtLoad) {
        root.classList.remove('cmha-entrance');
      } else {
        /* Mark as revealed only what is actually inside the observer's band —
           the same band the IntersectionObserver below uses. Marking every
           element revealed showed the ones past the fold at full opacity for a
           frame, and the observer's first callback then faded them back out.
           Below the fold that is invisible, but an element peeking a few pixels
           over the bottom edge visibly appeared and then dissolved. */
        var bandTop = window.innerHeight * 0.06;
        var bandBottom = window.innerHeight * 0.90;
        Array.prototype.forEach.call(els, function (el) {
          var r = el.getBoundingClientRect();
          if (r.top < bandBottom && r.bottom > bandTop) el.classList.add('is-revealed');
        });
        // Two frames: the first lets the browser commit the hidden state, the
        // second starts the transition from it.
        requestAnimationFrame(function () {
          requestAnimationFrame(function () { root.classList.add('is-entered'); });
        });
        // Clean up afterwards so nothing is left holding an opacity or a
        // transform on the module root.
        setTimeout(function () {
          root.classList.remove('cmha-entrance', 'is-entered');
        }, ENTRANCE_MS + 150);
      }
    }

    var io = new IntersectionObserver(function (entries) {
      entries.forEach(function (entry) {
        var el = entry.target;
        if (entry.isIntersecting) {
          el.classList.add('is-revealed');
          return;
        }
        var topEdge = entry.rootBounds ? entry.rootBounds.top : 0;
        var wentUp = entry.boundingClientRect.top < topEdge;
        el.style.setProperty('--cmha-reveal-dir', wentUp ? '-1' : '1');
        el.classList.remove('is-revealed');
      });
    }, { rootMargin: '-6% 0px -10% 0px', threshold: 0 });

    Array.prototype.forEach.call(els, function (el) { io.observe(el); });
  }

  var openPanel = null;      // currently open <aside>
  var lastFocused = null;    // element to return focus to on close

  /* ---------------------------------------------------------------- open  */
  function open(panel) {
    if (!panel || openPanel === panel) return;
    if (openPanel) close();

    var scrim = panel.parentNode.querySelector('[data-cmha-panel-close].cmha-panel__scrim');

    lastFocused = document.activeElement;

    panel.hidden = false;
    if (scrim) scrim.hidden = false;

    // Lock the page behind, compensating for the scrollbar so nothing shifts.
    var barWidth = window.innerWidth - document.documentElement.clientWidth;
    document.body.classList.add('cmha-scroll-lock');
    if (barWidth > 0) document.body.style.paddingRight = barWidth + 'px';

    // Next frame so the browser paints the closed position before animating.
    requestAnimationFrame(function () {
      panel.classList.add('is-open');
      if (scrim) scrim.classList.add('is-open');
    });

    openPanel = panel;

    // Move focus into the panel.
    var first = panel.querySelector('.cmha-panel__close') || panel;
    if (first === panel && !panel.hasAttribute('tabindex')) panel.setAttribute('tabindex', '-1');
    first.focus();
  }

  /* --------------------------------------------------------------- close  */
  function close() {
    if (!openPanel) return;

    var panel = openPanel;
    var scrim = panel.parentNode.querySelector('[data-cmha-panel-close].cmha-panel__scrim');

    panel.classList.remove('is-open');
    if (scrim) scrim.classList.remove('is-open');

    document.body.classList.remove('cmha-scroll-lock');
    document.body.style.paddingRight = '';

    openPanel = null;

    var finish = function () {
      panel.hidden = true;
      if (scrim) scrim.hidden = true;
    };

    var reduced = window.matchMedia &&
                  window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reduced) {
      finish();
    } else {
      var done = false;
      var once = function () { if (!done) { done = true; finish(); } };
      panel.addEventListener('transitionend', once, { once: true });
      setTimeout(once, 450); // fallback if transitionend never fires
    }

    if (lastFocused && typeof lastFocused.focus === 'function') lastFocused.focus();
    lastFocused = null;
  }

  /* ------------------------------------------------------------- focus trap */
  function trapFocus(e) {
    if (!openPanel || e.key !== 'Tab') return;

    var items = Array.prototype.filter.call(
      openPanel.querySelectorAll(FOCUSABLE),
      function (el) { return el.offsetParent !== null || el === document.activeElement; }
    );
    if (!items.length) { e.preventDefault(); return; }

    var first = items[0];
    var last  = items[items.length - 1];

    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault();
      last.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault();
      first.focus();
    }
  }

  /* ------------------------------------------------------------------ init */
  function init(root) {
    if (root.__cmhaFeaturedReady) return;
    root.__cmhaFeaturedReady = true;
    root.__cmhaAnim = true;   // the inline arming script checks this flag

    initReveal(root);

    // Lift the panel out of the module and onto <body>, so no theme wrapper
    // with overflow:hidden, transform, or a low z-index can clip it.
    var panelRoot = root.querySelector('[data-cmha-panel-root]');
    if (panelRoot && panelRoot.parentNode !== document.body) {
      document.body.appendChild(panelRoot);
    }

    // Explicit trigger buttons.
    root.addEventListener('click', function (e) {
      var trigger = e.target.closest('[data-cmha-panel-open]');
      if (!trigger) return;
      e.preventDefault();
      open(document.getElementById(trigger.getAttribute('data-cmha-panel-open')));
    });

    // Clicking anywhere on the card opens it too — but never hijack a real
    // link, button, or a text selection the user is making.
    root.addEventListener('click', function (e) {
      var card = e.target.closest('.cmha-featured');
      if (!card || !root.contains(card)) return;
      if (e.target.closest('a, button, input, select, textarea, label')) return;
      if (window.getSelection && String(window.getSelection()).length > 0) return;

      var trigger = card.querySelector('[data-cmha-panel-open]');
      if (trigger) open(document.getElementById(trigger.getAttribute('data-cmha-panel-open')));
    });
  }

  function boot() {
    var roots = document.querySelectorAll('[data-cmha-module="featured-person"]');
    Array.prototype.forEach.call(roots, init);
  }

  /* Global listeners — attached once, no matter how many instances exist. */
  if (!window.__cmhaPanelGlobals) {
    window.__cmhaPanelGlobals = true;

    document.addEventListener('click', function (e) {
      if (e.target.closest('[data-cmha-panel-close]')) {
        e.preventDefault();
        close();
      }
    });

    document.addEventListener('keydown', function (e) {
      if (e.key === 'Escape' || e.key === 'Esc') close();
      trapFocus(e);
    });
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', boot);
  } else {
    boot();
  }

  // Re-init if the HubSpot editor swaps modules around.
  if ('MutationObserver' in window) {
    var pending = null;
    new MutationObserver(function () {
      clearTimeout(pending);
      pending = setTimeout(boot, 150);
    }).observe(document.documentElement, { childList: true, subtree: true });
  }
})();


/* ==========================================================================
   MODULE: CMHA People Grid — JS
   Files for this module:
     html/cmha-people-grid.html        → paste into the module's HTML + HubL pane
     css/cmha-people-grid.css          → paste into the module's CSS pane
     js/cmha-people-grid.js            → paste into the module's JS pane

   Two self-contained behaviors, both scoped to this module. No library, no
   site-wide script.

   1. SCROLL REVEAL — cards fade up as they enter and fade back out as they
      leave, so scrolling up re-runs the animation.

   2. EMPHASIS ON CLICK — clicking anywhere on a card marks it active and
      opens its bio in place. The card stays exactly where it is; the rest of
      the section fades back behind it.

      The bio's height is measured at runtime rather than guessed, so a bio of
      any length opens cleanly. One card at a time per grid.
   ========================================================================== */
(function () {
  'use strict';

  var SELECTOR = '[data-cmha-module="people-grid"]';
  var STAGGER_CAP = 10;
  var ENTRANCE_MS = 850;      // keep in sync with --cmha-entrance-dur
  var BIO_MS = 380;           // keep in sync with --cmha-bio-dur

  var OPEN_LABEL = 'Hide bio';
  var SHUT_LABEL = 'Read bio';

  var reduced = window.matchMedia &&
                window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  /* ======================================================================
     1. SCROLL REVEAL
     ====================================================================== */
  function initReveal(root) {
    var groups = root.querySelectorAll('[data-cmha-reveal-group]');
    Array.prototype.forEach.call(groups, function (group) {
      var variant = group.getAttribute('data-cmha-reveal-group') || '';
      Array.prototype.forEach.call(group.children, function (kid, i) {
        if (!kid.hasAttribute('data-cmha-reveal')) {
          kid.setAttribute('data-cmha-reveal', variant);
        }
        kid.style.setProperty('--cmha-reveal-i', String(Math.min(i, STAGGER_CAP)));
      });
    });

    var els = root.querySelectorAll('[data-cmha-reveal]');
    if (!els.length) return;

    if (reduced || !('IntersectionObserver' in window)) {
      root.classList.remove('cmha-anim-ready', 'cmha-entrance');
      Array.prototype.forEach.call(els, function (el) { el.classList.add('is-revealed'); });
      return;
    }

    /* ---- INITIAL PAGE ENTRANCE ----------------------------------------
       The inline script in this module's HTML added .cmha-entrance before the
       first paint, so the module is already hidden and nothing flashes.

       If the module is on screen at load, it fades in as ONE unit and its
       children skip their individual reveal for this first pass — a fade
       inside a fade reads as mush. If the module is below the fold, drop the
       entrance now and let the per-element scroll reveal handle it. */
    if (root.classList.contains('cmha-entrance')) {
      var box = root.getBoundingClientRect();
      var onScreenAtLoad = box.top < window.innerHeight && box.bottom > 0;

      if (!onScreenAtLoad) {
        root.classList.remove('cmha-entrance');
      } else {
        /* Mark as revealed only what is actually inside the observer's band —
           the same band the IntersectionObserver below uses. Marking every
           element revealed showed the ones past the fold at full opacity for a
           frame, and the observer's first callback then faded them back out.
           Below the fold that is invisible, but an element peeking a few pixels
           over the bottom edge visibly appeared and then dissolved. */
        var bandTop = window.innerHeight * 0.06;
        var bandBottom = window.innerHeight * 0.90;
        Array.prototype.forEach.call(els, function (el) {
          var r = el.getBoundingClientRect();
          if (r.top < bandBottom && r.bottom > bandTop) el.classList.add('is-revealed');
        });
        // Two frames: the first lets the browser commit the hidden state, the
        // second starts the transition from it.
        requestAnimationFrame(function () {
          requestAnimationFrame(function () { root.classList.add('is-entered'); });
        });
        // Clean up afterwards so nothing is left holding an opacity or a
        // transform on the module root.
        setTimeout(function () {
          root.classList.remove('cmha-entrance', 'is-entered');
        }, ENTRANCE_MS + 150);
      }
    }

    var io = new IntersectionObserver(function (entries) {
      entries.forEach(function (entry) {
        var el = entry.target;
        if (entry.isIntersecting) {
          el.classList.add('is-revealed');
          return;
        }
        // Remember which edge it left by, so it animates back in from the
        // side it went out — the motion follows the scroll direction.
        var topEdge = entry.rootBounds ? entry.rootBounds.top : 0;
        var wentUp = entry.boundingClientRect.top < topEdge;
        el.style.setProperty('--cmha-reveal-dir', wentUp ? '-1' : '1');
        el.classList.remove('is-revealed');
      });
    }, { rootMargin: '-6% 0px -10% 0px', threshold: 0 });

    Array.prototype.forEach.call(els, function (el) { io.observe(el); });
  }

  /* ======================================================================
     2. EMPHASIS ON CLICK
     ====================================================================== */

  function setLabel(card, isOpen) {
    var label = card.querySelector('.cmha-person__toggle-label');
    if (label) label.textContent = isOpen ? OPEN_LABEL : SHUT_LABEL;
  }

  function openBio(card) {
    var bio = card.querySelector('.cmha-person__bio');
    var toggle = card.querySelector('.cmha-person__toggle');
    if (!bio) return;

    if (toggle) toggle.setAttribute('aria-expanded', 'true');
    setLabel(card, true);

    bio.style.maxHeight = bio.scrollHeight + 'px';

    // Once open, release the cap so the card reflows correctly if the window
    // is resized or a webfont lands late.
    var release = function () {
      if (card.classList.contains('is-active')) bio.style.maxHeight = 'none';
    };
    bio.addEventListener('transitionend', release, { once: true });
    setTimeout(release, BIO_MS + 60);
  }

  function shutBio(card) {
    var bio = card.querySelector('.cmha-person__bio');
    var toggle = card.querySelector('.cmha-person__toggle');
    if (!bio) return;

    if (toggle) toggle.setAttribute('aria-expanded', 'false');
    setLabel(card, false);

    // Re-pin the measured height before collapsing, or the transition has no
    // start value to animate from.
    bio.style.maxHeight = bio.scrollHeight + 'px';
    void bio.offsetHeight;
    requestAnimationFrame(function () { bio.style.maxHeight = '0px'; });
  }

  function deactivate(grid) {
    if (!grid) return;
    var active = grid.querySelector('.cmha-person.is-active');
    if (!active) return;
    active.classList.remove('is-active');
    shutBio(active);
    grid.classList.remove('has-active');
  }

  function activate(card) {
    var grid = card.closest('.cmha-grid');
    if (!grid) return;

    /* A card with no bio has nothing to reveal. Without this guard it would
       still take .is-active and dim every sibling — a click that dims the page
       and shows nothing. The MOTHER Lab roster is built this way: name and
       committee role only, no bios on the source site. */
    if (!card.querySelector('.cmha-person__bio')) return;

    // Already the active one — a second click closes it.
    if (card.classList.contains('is-active')) {
      deactivate(grid);
      return;
    }

    deactivate(grid);           // one at a time, per grid
    grid.classList.add('has-active');
    card.classList.add('is-active');
    openBio(card);
  }

  /* ======================================================================
     WIRING
     ====================================================================== */
  function init(root) {
    if (root.__cmhaPeopleReady) return;
    root.__cmhaPeopleReady = true;
    root.__cmhaAnim = true;   // the inline arming script checks this flag

    // Tells the CSS that JS is live, which is what collapses the bios. Without
    // it every bio just reads inline — never hidden with no way to open it.
    root.classList.add('cmha-js-ready');

    initReveal(root);

    root.addEventListener('click', function (e) {
      // Never hijack a real link or form control inside a card.
      if (e.target.closest('a, input, select, textarea, label')) return;

      var card = e.target.closest('.cmha-person');
      if (!card || !root.contains(card)) return;

      // Don't fire when the user is selecting text.
      if (window.getSelection && String(window.getSelection()).length > 0) return;

      e.preventDefault();
      activate(card);
    });

    // The "Read bio" button is the accessible trigger. It sits inside the
    // card, so a click already reaches the handler above — this is for
    // Space, which does not fire click on all browsers.
    root.addEventListener('keydown', function (e) {
      if (e.key !== ' ' && e.key !== 'Spacebar') return;
      var btn = e.target.closest('.cmha-person__toggle');
      if (!btn) return;
      e.preventDefault();
      var card = btn.closest('.cmha-person');
      if (card) activate(card);
    });

    // Esc clears the emphasis in whichever grid holds it.
    root.addEventListener('keydown', function (e) {
      if (e.key !== 'Escape' && e.key !== 'Esc') return;
      Array.prototype.forEach.call(root.querySelectorAll('.cmha-grid.has-active'), deactivate);
    });

    // Keep an open bio correctly sized when the layout reflows.
    var rz = null;
    window.addEventListener('resize', function () {
      clearTimeout(rz);
      rz = setTimeout(function () {
        var open = root.querySelectorAll('.cmha-person.is-active .cmha-person__bio');
        Array.prototype.forEach.call(open, function (bio) { bio.style.maxHeight = 'none'; });
      }, 150);
    });
  }

  function boot() {
    Array.prototype.forEach.call(document.querySelectorAll(SELECTOR), init);
  }

  // Clicking anywhere outside a grid clears the emphasis. Attached once.
  if (!window.__cmhaPeopleGlobals) {
    window.__cmhaPeopleGlobals = true;

    document.addEventListener('click', function (e) {
      if (e.target.closest('.cmha-person')) return;
      Array.prototype.forEach.call(
        document.querySelectorAll(SELECTOR + ' .cmha-grid.has-active'), deactivate);
    });

    document.addEventListener('keydown', function (e) {
      if (e.key !== 'Escape' && e.key !== 'Esc') return;
      Array.prototype.forEach.call(
        document.querySelectorAll(SELECTOR + ' .cmha-grid.has-active'), deactivate);
    });
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', boot);
  } else {
    boot();
  }

  if ('MutationObserver' in window) {
    var pending = null;
    new MutationObserver(function () {
      clearTimeout(pending);
      pending = setTimeout(boot, 150);
    }).observe(document.documentElement, { childList: true, subtree: true });
  }
})();

</script>

<style>
html, body { margin: 0; padding: 0; }
body { background: var(--cmha-white); }

/* ==========================================================================
   CMHA — Global base
   Center for Maternal Health Advancement
   --------------------------------------------------------------------------
   WHERE THIS GOES IN HUBSPOT:
   Design Manager > your theme's main stylesheet, OR a new coded file
   named cmha-base.css that you attach to the template. Load ONCE per site.

   This file is TOKENS AND SHARED PRIMITIVES ONLY — no animation.
   Every module carries its own scroll animation in its own CSS and JS, so a
   module animates correctly the moment it is pasted in, with no site-wide
   script to install and nothing to break if a page doesn't load it.

   The motion tokens below are still defined here so all modules share one
   set of timings. Each module also declares its own fallback values, so it
   still animates correctly if this file is missing.

   Everything is prefixed .cmha- / --cmha- so it cannot collide with
   HubSpot theme styles or other modules.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. TOKENS
   Change a color here and it changes everywhere. Do not hardcode hex values
   inside modules.
   -------------------------------------------------------------------------- */
:root {
  /* Brand palette (from the wireframes) */
  --cmha-purple-deep:  #2E063D;
  --cmha-purple-mid:   #877097;
  --cmha-purple-light: #AC9CB4;
  --cmha-purple-pale:  #D5C9D5;
  --cmha-lola:         #F7F4F9;
  --cmha-brown:        #3E2931;
  --cmha-white:        #FBFBFB;
  --cmha-ink:          #4A4048;   /* body copy — softer than pure black */

  /* Surfaces */
  --cmha-surface:      #FFFFFF;
  --cmha-border:       #D5C9D5;
  --cmha-border-hover: #877097;

  /* Type */
  --cmha-font: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;

  /* ------------------------------------------------------------------------
     MATCHED TO THE LIVE HOME PAGE — 27 Aug 2026
     ------------------------------------------------------------------------
     Brian sent the home page's own HTML/CSS/JS (the `ob-` module). These
     values are taken from it rather than invented, so the pages we build read
     as the same site rather than a near miss.

     What was actually different, and why the pages "felt" unlike theirs:

       ·  OUR TYPE RAN MUCH LARGER AT DESKTOP. Our h1 scaled to 56px against
          their fixed 44px, and our h2 to 40px against their 28px. That alone
          is most of it — the same words at a third again the size read as a
          different, louder site.
       ·  ROUNDER CORNERS. 6px against their 2px on buttons and 3px on cards.
       ·  HEAVIER RULES. 1px borders against their 0.5px hairlines.
       ·  PILL-SHAPED TAGS at 10px against their 8px rectangles.
       ·  TIGHTER EYEBROW TRACKING. 0.18em against their 2.5px (≈0.227em).
       ·  FLUID PADDING (clamp) against their fixed 80px / 56px steps.

     Sizes are px here, as theirs are. Normally I would keep rem for the type
     scale so it honours a reader's browser font setting — but a scale that is
     half rem and half px drifts apart the moment anyone changes anything, and
     matching the existing site is the job.
     ------------------------------------------------------------------------ */
  --cmha-fs-eyebrow: 11px;    /* .ob-eyebrow */
  --cmha-fs-tag:      8px;    /* .ob-press-tag / .ob-prog-tag */
  --cmha-fs-micro:   10px;    /* .ob-press-link, .ob-form-label */
  --cmha-fs-small:   12px;    /* card body — see the note below */
  --cmha-fs-body:    14px;    /* .ob-programs-sub, .ob-contact-sub */
  --cmha-fs-lead:    15px;    /* .ob-hero-sub */
  --cmha-fs-h3:      13px;    /* .ob-prog-name */
  --cmha-fs-h2:      28px;    /* .ob-section-title */
  --cmha-fs-h2-lg:   32px;    /* .ob-programs-title, .ob-contact-title */
  --cmha-fs-h1:      44px;    /* .ob-hero-title */
  --cmha-fs-h1-sm:   28px;    /* their 768px breakpoint */
  --cmha-fs-stat:    32px;    /* .ob-stat-num */

  /* THE ONE PLACE I DID NOT MATCH EXACTLY. Their card copy runs 10–11px
     (.ob-press-snippet is 10px, .ob-prog-desc 11px). That works in a carousel
     of eight cards a visitor skims; our pages carry paragraphs of bios,
     abstracts and answers that people actually read, and 10px is below what
     most guidance considers readable for sustained text. 12px is the
     compromise — much closer to theirs than the 14px we had, still legible.
     One token, so if you want it at 11px to match precisely, change it here
     and the whole site follows. */

  /* Spacing — fixed steps, as theirs are, not fluid clamps */
  --cmha-gutter:      48px;
  --cmha-gutter-sm:   24px;
  --cmha-section-y:   80px;   /* .ob-programs-content / .ob-contact */
  --cmha-section-y-sm: 56px;
  --cmha-maxw:        1200px;
  --cmha-maxw-narrow: 720px;  /* .ob-hero-content */

  /* Corners and rules */
  --cmha-radius:     3px;     /* cards */
  --cmha-radius-sm:  2px;     /* buttons, tags, inputs */
  --cmha-radius-lg:  3px;     /* nothing on their site is rounder than this */
  --cmha-hairline:   0.5px;   /* every border on their site */
  --cmha-shadow:     0 2px 14px rgba(46, 6, 61, 0.08);
  --cmha-shadow-lg:  0 8px 34px rgba(46, 6, 61, 0.16);

  /* Motion — shared timings. Modules reference these but also carry their
     own fallbacks, so they never depend on this file being present. */
  --cmha-ease:            cubic-bezier(0.22, 0.61, 0.36, 1);
  --cmha-dur-fast:        180ms;
  --cmha-dur:             260ms;

  /* Scroll reveal — deliberately slow and gentle, and it plays in BOTH
     directions: elements fade out as they leave and fade back in on return. */
  --cmha-reveal-dur:      700ms;   /* .ob-scroll-fade is 0.7s */
  --cmha-reveal-shift:    28px;    /* .ob-scroll-fade translateY(28px) */
  --cmha-reveal-stagger:  50ms;    /* see the note below */

  /* THE SECOND PLACE I DID NOT MATCH EXACTLY. Their sections fade as one
     block with no stagger at all. Ours walks through a grid's children. On a
     row of three that difference is invisible; on the 39-card press grid,
     removing it makes every card arrive at once, which reads as a flash
     rather than an entrance. Cut from 120ms to 50ms — close enough that the
     two feel the same, small enough that a long grid still lands quickly.
     Set to 0 for an exact match. */

  /* Page entrance — a module already on screen at load fades in as ONE unit,
     rather than its elements trickling in separately. Slower and gentler than
     the scroll reveal, because it is the first thing a visitor sees. */
  --cmha-entrance-dur:    850ms;
  --cmha-entrance-shift:  14px;
  --cmha-entrance-ease:   cubic-bezier(0.22, 0.61, 0.36, 1);

  /* Click-to-emphasise on a card, and the bio opening beneath it */
  --cmha-emphasis-dur:    300ms;
  --cmha-bio-dur:         380ms;

  /* FAQ answers. Opening is slower than closing on purpose: an answer should
     unfold, but getting out of the way should feel immediate. The ease is
     gentler at both ends than --cmha-ease, which is tuned for travel rather
     than for something growing in place. */
  --cmha-faq-dur:         520ms;
  --cmha-faq-dur-out:     380ms;
  --cmha-faq-ease:        cubic-bezier(0.33, 0.02, 0.22, 1);
}

/* --------------------------------------------------------------------------
   2. SHARED PRIMITIVES
   -------------------------------------------------------------------------- */
.cmha { font-family: var(--cmha-font); color: var(--cmha-ink); }
.cmha *, .cmha *::before, .cmha *::after { box-sizing: border-box; }

.cmha-section {
  padding: var(--cmha-section-y) var(--cmha-gutter);
  background: var(--cmha-white);
}
/* Their single breakpoint, and the same two numbers behind it. */
@media (max-width: 768px) {
  .cmha-section { padding: var(--cmha-section-y-sm) var(--cmha-gutter-sm); }
}
.cmha-section--alt  { background: var(--cmha-lola); }
.cmha-section--dark { background: var(--cmha-purple-deep); }

.cmha-inner        { max-width: var(--cmha-maxw); margin-inline: auto; }
.cmha-inner--narrow{ max-width: var(--cmha-maxw-narrow); margin-inline: auto; }

.cmha-eyebrow {
  font-size: var(--cmha-fs-eyebrow);
  letter-spacing: 2.5px;   /* .ob-eyebrow — wider than the 0.18em we had */
  text-transform: uppercase;
  color: var(--cmha-purple-mid);
  font-weight: 500;
  margin: 0 0 8px;
}
.cmha-section--dark .cmha-eyebrow { color: var(--cmha-purple-light); }

.cmha-title {
  font-size: var(--cmha-fs-h2);
  font-weight: 300;
  line-height: 1.25;              /* .ob-hero-title */
  color: var(--cmha-purple-deep);
  margin: 0 0 12px;
  /* No negative tracking. Theirs sets none, and at 28px the tightening we had
     was visible as a slightly condensed, different-typeface impression. */
}
/* 500, not 600 — .ob-hero-title strong. On dark it also takes the pale
   purple rather than staying white, which is what gives their headline its
   two-tone look. */
.cmha-title strong { font-weight: 500; }
.cmha-section--dark .cmha-title strong { color: var(--cmha-purple-pale); }
.cmha-section--dark .cmha-title { color: var(--cmha-white); }

.cmha-desc {
  font-size: var(--cmha-fs-body);
  line-height: 1.8;               /* .ob-programs-sub / .ob-hero-sub */
  font-weight: 300;
  color: var(--cmha-purple-mid);
  margin: 0 0 36px;
  max-width: 62ch;
}
.cmha-section--dark .cmha-desc { color: var(--cmha-purple-pale); }

.cmha-divider {
  height: var(--cmha-hairline);
  border: 0;
  background: var(--cmha-border);
  margin: 0;
}

/* .ob-press-tag / .ob-prog-tag. Was a 10px pill with a 1px border and a
   999px radius; theirs is an 8px rectangle with a hairline and 2px corners.
   Of everything on this page this was the loudest mismatch — a row of pills
   next to their rectangles reads as a different design system entirely. */
.cmha-tag {
  display: inline-block;
  font-size: var(--cmha-fs-tag);
  letter-spacing: 1px;
  text-transform: uppercase;
  font-weight: 400;
  color: var(--cmha-purple-mid);
  border: var(--cmha-hairline) solid var(--cmha-border);
  border-radius: var(--cmha-radius-sm);
  padding: 2px 8px;
  white-space: nowrap;
}

/* Editorial note strip — remove before launch, useful during content collection */
.cmha-note {
  font-size: var(--cmha-fs-micro);
  color: var(--cmha-purple-mid);
  background: var(--cmha-lola);
  border-top: var(--cmha-hairline) solid var(--cmha-border);
  padding: 1rem var(--cmha-gutter);
  font-style: italic;
}

/* Visible focus ring — keyboard users and HubSpot accessibility audits */
.cmha :focus-visible {
  outline: 2px solid var(--cmha-purple-mid);
  outline-offset: 3px;
  border-radius: 3px;
}

.cmha-sr-only {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* Set by the Featured Person module's JS while its slide-in panel is open, to
   stop the page behind from scrolling. Lives here because it applies to
   <body>, which no single module owns. */
body.cmha-scroll-lock { overflow: hidden; }

/* --------------------------------------------------------------------------
   3. REDUCED MOTION
   Modules disable their own animations too; this is the site-wide backstop.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .cmha *,
  .cmha *::before,
  .cmha *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}


/* ==========================================================================
   MODULE: CMHA Page Hero
   Files for this module:
     html/cmha-page-hero.html          → paste into the module's HTML + HubL pane
     css/cmha-page-hero.css            → paste into the module's CSS pane
     js/cmha-page-hero.js              → paste into the module's JS pane
   Uses tokens from css/cmha-base.css. The scroll animation is self-contained
   in this module's own CSS and JS — there is no site-wide animation script.
   ========================================================================== */

/* Matched to .ob-hero on the live home page: a 520px minimum with the content
   centred inside it, rather than the fluid padding we had. Their hero is a
   fixed stage; ours grew and shrank with the viewport, which is a large part
   of why the pages felt unrelated at desktop widths. */
.cmha-hero {
  position: relative;
  overflow: hidden;
  min-height: 520px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 80px var(--cmha-gutter, 48px);
  text-align: center;
  background: var(--cmha-purple-deep);
  isolation: isolate;
}

.cmha-hero--tall { min-height: 620px; }

/* --- bottom-anchored text -------------------------------------------------
   ONE PAGE USES THIS: People. It exists because that hero carries a full-
   length group photograph, and with the text centred the headline landed
   directly across two of the four faces.

   Dropping the block to the foot of the band puts the faces above the words
   instead of behind them. Two things follow from that, both wanted:

     - the picture gets to be a picture. The top two thirds of the band are
       photograph with nothing written on them.
     - contrast improves. The text now sits over clothing and floor, which are
       dark, instead of over the white event banner behind their heads. That
       is what bought the room to lighten the veil far enough to see faces at
       all — measured worst case went from needing 0.82 to holding 7.3:1 at
       0.62. The two changes are not independent; do not undo one alone.

   `align-items` is the vertical axis here because .cmha-hero is a row-
   direction flex container. The 80px padding still applies, so the text keeps
   its margin off the bottom edge — it is anchored, not jammed against it.

   ⚠ NOT A GENERAL-PURPOSE MODIFIER. It is right when the subject of the photo
   is in the upper half of the frame and wrong otherwise, which is why it is
   opt-in per page rather than a property of --photo.
   -------------------------------------------------------------------------- */
.cmha-hero--anchor-bottom { align-items: flex-end; }

/* --- and why its height is tied to the viewport width ---------------------
   A fixed-height hero gets WORSE the wider the monitor, which is the opposite
   of what anyone expects. The band's width grows and its height does not, so
   the slice of photograph that survives `cover` keeps thinning. Measured on
   the People photo, as a share of the image's height:

     1440 × 620   →  39 points of image visible
     1920 × 620   →  30
     2560 × 620   →  22      ← four heads with hair need 18 of those 22

   At 2560 the heads still fit, but nothing else does: the headline is forced
   back on top of their faces, which is the exact problem --anchor-bottom was
   added to solve. Seen rendered, not inferred.

   `34vw` holds the band's proportions roughly constant instead, so a wide
   monitor gets a bigger hero rather than a thinner one. The clamp keeps it
   from doing anything on ordinary laptops — 34vw does not reach 620px until
   about 1825px wide — and caps it so an ultrawide does not get a hero the
   height of the screen.

   Guarded to ≥769px so it can never collide with the mobile min-height below;
   the two rules have the same specificity and would otherwise be decided by
   source order, which is how three earlier bugs on this project happened.
   -------------------------------------------------------------------------- */
@media (min-width: 769px) {
  .cmha-hero--anchor-bottom { min-height: clamp(620px, 34vw, 880px); }
}

@media (max-width: 768px) {
  .cmha-hero { min-height: 420px; padding: 60px var(--cmha-gutter-sm, 24px); }
  .cmha-hero--tall { min-height: 480px; }
}

/* --- background layers ---------------------------------------------------- */
.cmha-hero__bg {
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    linear-gradient(135deg,
      #2E063D 0%,
      #3E2931 35%,
      #4A3550 65%,
      #2E063D 100%);
  background-size: cover;
  background-position: center;
}

/* Fine diagonal weave, straight from the wireframe. Kept very low opacity
   so it reads as texture rather than pattern. */
.cmha-hero__texture {
  position: absolute;
  inset: 0;
  opacity: 0.09;
  background-image:
    repeating-linear-gradient(45deg,  #877097 0 1px, transparent 1px 16px),
    repeating-linear-gradient(-45deg, #AC9CB4 0 1px, transparent 1px 16px);
}

/* Only does real work once a photo is set on .cmha-hero__bg — keeps text
   at an accessible contrast ratio over any image. */
.cmha-hero__overlay {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to bottom,
    rgba(46, 6, 61, 0.62) 0%,
    rgba(46, 6, 61, 0.78) 100%
  );
}
.cmha-hero:not(.cmha-hero--image) .cmha-hero__overlay { opacity: 0.35; }

/* --- photo hero ----------------------------------------------------------- */
/* Add cmha-hero--photo to the <section> and set --cmha-hero-photo on the
   .cmha-hero__bg div:

     <section class="cmha cmha-hero cmha-hero--image cmha-hero--photo" …>
       <div class="cmha-hero__bg" style="--cmha-hero-photo: url('…')" …>

   TWO CLASSES, TWO JOBS:
     --image  run the overlay at full strength (without it, line 61 dims the
              overlay to 0.35, which is right over the flat gradient)
     --photo  there is an actual photograph here

   They are separate so a page can be tuned without touching the others: the
   photo pages need their own overlay depth and their own focus point, and
   those must not leak onto the gradient heroes.

   ⚠ CORRECTING MY OWN NOTE, 27 Aug 2026. An earlier version of this comment
   claimed --image "already sits on EVERY page's hero". It does not, and never
   did. I had grepped the built single-file pages for the class name and got a
   hit on all thirteen — but the stylesheet is inlined into every one of those
   files, so I was matching THIS comment, not the markup. The correct check is
   to read the class off the DOM. Only the photo heroes carry --image.

   The brand gradient stays underneath the photo on purpose. A hero whose
   background is a single remote JPEG shows flat purple while that file is in
   flight, and shows flat purple forever if the URL ever breaks. Layered this
   way, the gradient is what a visitor sees in both cases — the page degrades
   to the design rather than to a blank panel. Same reason for the `none`
   fallback: a missing custom property leaves the gradient, not an empty rule. */
.cmha-hero--photo .cmha-hero__bg {
  background-image:
    var(--cmha-hero-photo, none),
    linear-gradient(135deg,
      #2E063D 0%,
      #3E2931 35%,
      #4A3550 65%,
      #2E063D 100%);
  background-position: var(--cmha-hero-focus, center 18%), center;
  background-size: cover, cover;
  background-repeat: no-repeat, no-repeat;
}

/* --- why the focus sits so high ------------------------------------------
   WAS `center 35%`, which cut the tops of heads off on wide screens.

   The mechanism is worth understanding, because it is counter-intuitive. The
   hero is a wide, short band. With `cover`, the photo is scaled until its
   WIDTH fills the band, which makes it far taller than the band — so most of
   its height is thrown away, and the vertical position picks which slice
   survives. Widen the browser and the band gets proportionally shorter still,
   so the surviving slice gets thinner. That is why the problem appears at
   large widths and not small ones: it is the container's aspect ratio, not
   its size.

   The default here is 18%, which keeps the slice near the top of the frame
   where heads usually are. Lower the number to show more of the top, raise it
   to move down. Pages override it per photo on .cmha-hero__bg.

   ⚠ CORRECTING MY OWN NOTE, 1 Sep 2026. This paragraph used to say the People
   photo was "closer to a normal 3:2" and reason from that. It is 4084 × 4480
   — PORTRAIT. I had never seen the file; the sandbox cannot load remote
   images, so the aspect ratio was an assumption dressed up as a fact. It was
   measured in a real browser on 1 Sep. A portrait source makes the crop worse,
   not better: at 2560px wide and a 520px band, about 18.5 points of the
   image's height survive, and four heads with hair need 18. There was no
   focus value that fit. Anyone tuning a photo hero here should read the real
   dimensions off the file first rather than trusting a comment.

   IF IT STILL CROPS ON A VERY WIDE MONITOR, the other lever is the band
   itself — a taller hero is less of a letterbox and throws less of the photo
   away. `cmha-hero--tall` is that lever, and the People page now uses it for
   exactly this reason. It changes the page's proportions, so it stays a
   per-page decision rather than a global default.
   -------------------------------------------------------------------------- */

/* --- pushing the photo further back --------------------------------------
   Scoped to --photo ON PURPOSE. The base .cmha-hero__overlay is shared by
   every hero on the site. Deepening the base values to calm one photograph
   would have reached all of them — the gradient heroes run the same gradient
   at 0.35 opacity, so raising its alphas would darken those too. */
.cmha-hero--photo .cmha-hero__overlay {
  background: linear-gradient(
    to bottom,
    rgba(46, 6, 61, var(--cmha-hero-veil-top, 0.82)) 0%,
    rgba(46, 6, 61, var(--cmha-hero-veil-bottom, 0.91)) 100%
  );
}

/* --- why the veil is a knob, and why the text got lighter -----------------
   Two photos needed opposite things, which is the whole reason these are
   custom properties now.

   A busy team photo has to be pushed back hard or it fights the words. A
   silhouette is a simple graphic on a light ground — push it that hard and
   there is nothing left to see, which is exactly what happened on the FAQ
   page at 0.82/0.91. Set --cmha-hero-veil-top / --cmha-hero-veil-bottom on
   the .cmha-hero__bg div per page.

   THE CATCH, and why you cannot just turn the veil down: the eyebrow and
   subtitle were purple-light and purple-pale, which are chosen to sit on
   deep purple. Over a LIGHT part of a photo, at a light veil, they collapse.
   Measured against a pure-white patch — the realistic worst case for a
   silhouette on a pale ground:

                       veil 0.62      veil 0.82
     eyebrow  #AC9CB4       1.9:1         4.1:1     ← fails AA at both
     subtitle #D5C9D5       3.0:1         6.5:1     ← fails at 0.62
     white    #FBFBFB       5.0:1        10.1:1     ← passes at both

   So on photo heroes only, the eyebrow and subtitle go white. That buys the
   room to lighten the veil until the picture actually reads, and it also
   fixes something that was already wrong: at 0.82 the eyebrow was 4.06:1,
   under the 4.5:1 line, on a page that had already shipped to review.

   Gradient heroes keep the purple tints — over flat deep purple they have
   plenty of contrast and the tint is doing real work there. */
.cmha-hero--photo .cmha-hero__eyebrow,
.cmha-hero--photo .cmha-hero__sub {
  color: var(--cmha-white);
}

/* --- video hero ----------------------------------------------------------
   Same layering as a photo hero — gradient at the bottom, then the moving
   picture, then texture, then the veil — so every failure mode lands on the
   gradient rather than on nothing. The video element sits INSIDE
   .cmha-hero__bg so it inherits that stacking without needing its own
   z-index games.
   -------------------------------------------------------------------------- */
.cmha-hero__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--cmha-hero-focus, center);
  /* Hidden until the JS has it playing. A <video> that has not started paints
     a black rectangle over the gradient, which is worse than the gradient. */
  opacity: 0;
  transition: opacity 700ms var(--cmha-ease, ease);
}
.cmha-hero__video.is-playing { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .cmha-hero__video { transition: none; }
}

/* --- the pause control ---------------------------------------------------
   Not decoration, and not optional. WCAG 2.2.2 (Pause, Stop, Hide) says any
   motion that starts on its own, runs more than five seconds and sits
   alongside other content needs a way to stop it. A looping video under a
   headline is exactly that. Tufts is a university and will be held to AA, so
   this ships with the feature rather than after someone files a complaint.

   Hidden until JS confirms the video is actually running — a control that
   pauses nothing is worse than no control.
   -------------------------------------------------------------------------- */
.cmha-hero__motion {
  position: absolute;
  right: clamp(0.75rem, 2vw, 1.5rem);
  bottom: clamp(0.75rem, 2vw, 1.5rem);
  z-index: 2;
  display: none;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border: var(--cmha-hairline, 0.5px) solid rgba(251, 251, 251, 0.35);
  border-radius: 50%;
  background: rgba(46, 6, 61, 0.55);
  color: var(--cmha-white);
  cursor: pointer;
  backdrop-filter: blur(6px);
  transition: background var(--cmha-dur-fast, 150ms) var(--cmha-ease, ease),
              border-color var(--cmha-dur-fast, 150ms) var(--cmha-ease, ease),
              opacity var(--cmha-dur-fast, 150ms) var(--cmha-ease, ease);
  opacity: 0.55;
}
.cmha-hero__motion.is-available { display: flex; }
.cmha-hero__motion:hover,
.cmha-hero__motion:focus-visible {
  opacity: 1;
  background: rgba(46, 6, 61, 0.8);
  border-color: rgba(251, 251, 251, 0.7);
}
.cmha-hero__motion svg { display: block; }
/* One icon is shown at a time; the JS flips this class. */
.cmha-hero__motion .cmha-hero__icon-play { display: none; }
.cmha-hero__motion.is-paused .cmha-hero__icon-play { display: block; }
.cmha-hero__motion.is-paused .cmha-hero__icon-pause { display: none; }

/* The diagonal weave is brand texture over flat colour; over a photograph it
   reads as noise across people's faces. Kept, but taken well down. */
.cmha-hero--photo .cmha-hero__texture { opacity: 0.045; }

/* --- content -------------------------------------------------------------- */
.cmha-hero__content {
  position: relative;
  z-index: 2;
  max-width: var(--cmha-maxw-narrow, 720px);   /* .ob-hero-content */
  margin-inline: auto;
}

.cmha-hero__eyebrow {
  font-size: var(--cmha-fs-eyebrow, 11px);
  letter-spacing: 2.5px;          /* .ob-eyebrow */
  text-transform: uppercase;
  color: var(--cmha-purple-light);
  font-weight: 500;
  margin: 0 0 8px;
}

/* .ob-hero-title: 44px / 300 / 1.25, dropping to 28px at their 768px
   breakpoint. We were at 34–56px with -0.02em tracking and a 1.08 line-height
   — larger, tighter and more condensed than theirs at every width, which was
   the single biggest reason these pages read as a different site. */
.cmha-hero__title {
  font-size: var(--cmha-fs-h1, 44px);
  font-weight: 300;
  line-height: 1.25;
  color: var(--cmha-white);
  margin: 0 0 20px;
  text-wrap: balance;
}
.cmha-hero__title strong {
  font-weight: 500;
  color: var(--cmha-purple-pale);
}
@media (max-width: 768px) {
  .cmha-hero__title { font-size: var(--cmha-fs-h1-sm, 28px); }
}

.cmha-hero__sub {
  font-size: var(--cmha-fs-lead, 15px);
  font-weight: 300;
  line-height: 1.8;               /* .ob-hero-sub */
  color: var(--cmha-purple-pale);
  margin: 0 auto 36px;
  max-width: 540px;               /* .ob-hero-sub */
  text-wrap: pretty;
}

/* --- optional meta row (date / location / host) --------------------------- */
/* Used by the conference hero. A list, not a row of spans, because it is a
   list of facts — and the separators are drawn in CSS so they are never read
   aloud as content. */
.cmha-hero__meta {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem 1.5rem;
  list-style: none;
  margin: 0 0 1.25rem;
  padding: 0;
}
.cmha-hero__meta-item {
  position: relative;
  font-size: var(--cmha-fs-micro, 10px);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-weight: 500;
  color: var(--cmha-purple-light, #AC9CB4);
}
.cmha-hero__meta-item + .cmha-hero__meta-item::before {
  content: '';
  position: absolute;
  inset-inline-start: -0.85rem;
  top: 50%;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--cmha-purple-mid, #877097);
  transform: translateY(-50%);
}

/* --- buttons (shared by other modules too) -------------------------------- */
.cmha-hero__ctas {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;                     /* .ob-hero-ctas */
  justify-content: center;
}

.cmha-btn {
  display: inline-block;
  font-family: var(--cmha-font);
  font-size: var(--cmha-fs-micro);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-decoration: none;
  padding: 0.95em 1.9em;
  border-radius: 3px;
  border: var(--cmha-hairline, 0.5px) solid transparent;
  cursor: pointer;
  transition: background var(--cmha-dur) var(--cmha-ease),
              color      var(--cmha-dur) var(--cmha-ease),
              border     var(--cmha-dur) var(--cmha-ease),
              transform  var(--cmha-dur-fast) var(--cmha-ease);
}
.cmha-btn:hover  { transform: translateY(-1px); }
.cmha-btn:active { transform: translateY(0); }

.cmha-btn--primary {
  background: var(--cmha-purple-mid);
  color: var(--cmha-white);
}
.cmha-btn--primary:hover { background: var(--cmha-purple-light); }

/* --------------------------------------------------------------------------
   SAVE-THE-DATE
   --------------------------------------------------------------------------
   Treated as a date card rather than a button. A solid filled rectangle was
   flat here because it is the only element in the lower half of the hero —
   nothing else gives it scale or depth, so it has to carry its own.

   What does the work: a translucent panel over the hero rather than a flat
   fill, a hairline splitting the date from the label so it reads as a card,
   a drop shadow to lift it off the background, and a hover that fills in
   rather than just changing shade.
   -------------------------------------------------------------------------- */
.cmha-btn.cmha-btn--date {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
  padding: 1.15rem 2.5rem 1rem;
  line-height: 1.15;
  border-radius: var(--cmha-radius, 3px);
  background: rgba(251, 251, 251, 0.07);
  border: var(--cmha-hairline, 0.5px) solid rgba(213, 201, 213, 0.32);
  box-shadow: 0 14px 40px rgba(20, 2, 28, 0.35);
  backdrop-filter: blur(3px);
  transition: background   var(--cmha-dur, 260ms) var(--cmha-ease, ease),
              border-color var(--cmha-dur, 260ms) var(--cmha-ease, ease),
              box-shadow   var(--cmha-dur, 260ms) var(--cmha-ease, ease),
              transform    var(--cmha-dur, 260ms) var(--cmha-ease, ease);
}

.cmha-btn.cmha-btn--date .cmha-btn__date {
  font-size: clamp(1.4rem, 1.2rem + 0.8vw, 1.85rem);
  font-weight: 300;
  letter-spacing: 0.01em;
  text-transform: none;
  color: var(--cmha-white, #FBFBFB);
  padding-bottom: 0.6rem;
}

/* The hairline. A pseudo-element rather than a border on the label, so it can
   be inset from the edges and read as a rule inside a card. */
.cmha-btn.cmha-btn--date .cmha-btn__label {
  position: relative;
  font-size: 0.625rem;
  letter-spacing: 0.24em;
  font-weight: 500;
  color: var(--cmha-purple-light, #AC9CB4);
  padding-top: 0.7rem;
}
.cmha-btn.cmha-btn--date .cmha-btn__label::before {
  content: '';
  position: absolute;
  top: 0;
  left: 50%;
  width: 2.5rem;
  height: 1px;
  background: rgba(213, 201, 213, 0.4);
  transform: translateX(-50%);
  transition: width var(--cmha-dur, 260ms) var(--cmha-ease, ease),
              background var(--cmha-dur, 260ms) var(--cmha-ease, ease);
}

.cmha-btn.cmha-btn--date:hover {
  background: var(--cmha-purple-mid, #877097);
  border-color: var(--cmha-purple-light, #AC9CB4);
  box-shadow: 0 20px 50px rgba(20, 2, 28, 0.45);
  transform: translateY(-3px);
}
.cmha-btn.cmha-btn--date:hover .cmha-btn__label { color: var(--cmha-white, #FBFBFB); }
.cmha-btn.cmha-btn--date:hover .cmha-btn__label::before {
  width: 4.5rem;
  background: rgba(251, 251, 251, 0.75);
}

/* --------------------------------------------------------------------------
   THE SAME CARD, WITH THE CLICK TAKEN OUT
   --------------------------------------------------------------------------
   WHY IT EXISTS. The conference hero now carries the interest form, so the
   date card no longer has anywhere to send anyone. Brian liked the card, so
   it stays — as a plaque. What has to go is every affordance that says
   "click me": the lift, the purple fill, the widening rule, and the pointer.
   An element that hovers like a button and does nothing is worse than a plain
   label, and worse still for anyone on a keyboard or a screen reader, who
   gets no signal at all that it is inert.

   ⚠ EVERY SELECTOR HERE IS DOUBLED UP (.cmha-btn.cmha-btn--plaque) ON
   PURPOSE. DO NOT "TIDY" IT TO A SINGLE CLASS.

   The first version used single classes and was silently beaten. The reason
   is not in this file: css/cmha-conf-interest.css carries its own copy of the
   .cmha-btn block, so that the interest form still works on a page without
   the hero module. That copy loads AFTER this file and sets cursor: pointer
   and a :hover transform, so at equal specificity it won — and the plaque
   went back to lifting under the cursor. Caught by measuring the computed
   style, not by looking at it.

   Doubling the class takes the rules to (0,2,0) and (0,3,0), which beats
   .cmha-btn (0,1,0) and .cmha-btn:hover (0,2,0) no matter what order the
   stylesheets load in. That is the durable fix: the two files can be
   concatenated in either order and the plaque still behaves.
   -------------------------------------------------------------------------- */
.cmha-btn.cmha-btn--plaque { cursor: default; }
.cmha-btn.cmha-btn--plaque:hover,
.cmha-btn.cmha-btn--plaque:focus {
  transform: none;
  background: rgba(251, 251, 251, 0.07);
  border-color: rgba(213, 201, 213, 0.32);
  box-shadow: 0 14px 40px rgba(20, 2, 28, 0.35);
}
.cmha-btn.cmha-btn--plaque:hover .cmha-btn__label { color: var(--cmha-purple-light, #AC9CB4); }
.cmha-btn.cmha-btn--plaque:hover .cmha-btn__label::before {
  width: 2.5rem;
  background: rgba(213, 201, 213, 0.4);
}

/* The row exists only to centre the plaque; the hero centres text, not
   inline-flex boxes. */
.cmha-hero__plaque-row {
  display: flex;
  justify-content: center;
  margin: 0 0 24px;
}

/* --------------------------------------------------------------------------
   A FORM INSIDE THE HERO
   --------------------------------------------------------------------------
   Narrower than the hero's 720px content column on purpose. Full-width input
   rows under a centred headline read as a different section that has drifted
   up into the hero; at 460px the form sits as one object beneath the words.

   The fields themselves are NOT restyled here — they use the same
   .cmha-interest__* rules as everywhere else, which render white inputs on a
   dark ground. Over video that is the right call twice over: white is the
   highest-contrast surface available for text someone is typing, and it is
   unambiguously an input, which a translucent panel over moving footage is
   not.
   -------------------------------------------------------------------------- */
.cmha-hero__form {
  max-width: 460px;
  margin: 0 auto;
  text-align: left;
}

/* The submit spans the form. A hero has no adjacent content to size against,
   and a small button under two full-width rows looks like an afterthought. */
.cmha-hero__form .cmha-interest__submit {
  width: 100%;
  margin-top: 4px;
}

/* Status messages go back to centred — they are addressed to the reader, not
   attached to a field. */
.cmha-hero__form .cmha-interest__status { text-align: center; }

@media (max-width: 768px) {
  .cmha-hero__form { max-width: 100%; }
}

/* --------------------------------------------------------------------------
   CONFERENCE HERO — slower, longer-travelled motion
   Scoped to this one hero. A sparse page can afford a more deliberate
   entrance; the People and FAQ heroes keep the site defaults.
   -------------------------------------------------------------------------- */
.cmha-hero--conference {
  /* Was 1150/44/170 — a deliberately grander entrance for the conference
     hero. Pulled back toward the site default now that the whole build
     matches the live pages' quicker 0.7s fade; a hero three times slower than
     every other section was part of what read as "a different site". */
  --cmha-reveal-dur: 850ms;
  --cmha-reveal-shift: 32px;
  --cmha-reveal-stagger: 70ms;
}

/* .ob-btn-o. Theirs takes a solid purple-mid border rather than a translucent
   pale one, and fills faintly on hover instead of only brightening its edge. */
.cmha-btn--ghost {
  background: transparent;
  color: var(--cmha-purple-pale);
  border-color: var(--cmha-purple-mid);
}
.cmha-btn--ghost:hover {
  background: rgba(135, 112, 151, 0.15);
  border-color: var(--cmha-purple-mid);
  color: var(--cmha-white);
}

/* --------------------------------------------------------------------------
   PAGE ENTRANCE
   --------------------------------------------------------------------------
   The inline script in this module's HTML adds .cmha-entrance before the first
   paint, so the module is already hidden when the page renders — no flash of
   content that then disappears to fade back in.

   The module JS decides what to do with it: if the module is on screen at
   load, it fades in as one unit and its children skip their individual reveal
   for that first pass (a fade inside a fade reads as mush). If the module is
   below the fold, the JS drops this class immediately and the normal
   per-element scroll reveal takes over.

   Both classes are removed once the entrance finishes, so nothing is left
   holding an opacity or a transform on the module root.
   -------------------------------------------------------------------------- */
/* NOTE: the hidden state carries NO transition, on purpose. If it did, adding
   the class would animate the module from visible to hidden over 850ms — and
   since the JS adds .is-entered a frame or two later, the two would cancel and
   the module would simply appear. The transition lives only on the entered
   state, so arming snaps instantly and only the reveal animates. */
[data-cmha-module="page-hero"].cmha-entrance {
  opacity: 0;
  transform: translate3d(0, var(--cmha-entrance-shift, 14px), 0);
  will-change: opacity, transform;
}
[data-cmha-module="page-hero"].cmha-entrance.is-entered {
  opacity: 1;
  transform: none;
  transition:
    opacity   var(--cmha-entrance-dur, 850ms) var(--cmha-entrance-ease, ease),
    transform var(--cmha-entrance-dur, 850ms) var(--cmha-entrance-ease, ease);
}

@media (prefers-reduced-motion: reduce) {
  [data-cmha-module="page-hero"].cmha-entrance {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
}

/* --------------------------------------------------------------------------
   SCROLL ANIMATION — scoped to this module only
   --------------------------------------------------------------------------
   Nothing here leaks outside [data-cmha-module="page-hero"], so this module
   animates on its own with no site-wide stylesheet or script.

   The hidden state applies only once the module root carries .cmha-anim-ready,
   which the inline script in the module HTML adds and then removes again after
   2.5s if the module JS never ran. Content can never be left invisible.

   Fallback values are inlined after each var() so the animation is still
   correct if cmha-base.css is not loaded.
   -------------------------------------------------------------------------- */
/* --------------------------------------------------------------------------
   WHY THE GROUP-CHILD SELECTORS ARE HERE
   --------------------------------------------------------------------------
   data-cmha-reveal-group is a convenience: the JS copies the group's variant
   down onto each child as data-cmha-reveal and assigns the stagger index.

   But that copy happens after the first paint. Until it lands, a child matches
   none of the rules below and sits at opacity 1 — so the moment the attribute
   arrives, the element transitions from visible to hidden over the full reveal
   duration. Anything at the edge of the viewport visibly appeared and then
   dissolved, which is exactly what arming before paint exists to prevent.

   So every rule below matches the group's children directly as well. The
   hidden state is then in force from the first paint, and the JS only fills in
   the attribute and the stagger index — it never causes a transition.

   The rule stands on its own: a hidden state must never be reachable by a
   transition. When an animation "just doesn't happen," this is usually why.
   -------------------------------------------------------------------------- */
[data-cmha-module="page-hero"].cmha-anim-ready [data-cmha-reveal],
[data-cmha-module="page-hero"].cmha-anim-ready [data-cmha-reveal-group] > * {
  opacity: 0;
  transform: translate3d(
    0,
    calc(var(--cmha-reveal-shift, 34px) * var(--cmha-reveal-dir, 1)),
    0
  );
  transition:
    opacity   var(--cmha-reveal-dur, 1100ms) var(--cmha-ease, cubic-bezier(0.22, 0.61, 0.36, 1)),
    transform var(--cmha-reveal-dur, 1100ms) var(--cmha-ease, cubic-bezier(0.22, 0.61, 0.36, 1));
  transition-delay: 0ms;          /* leaving is immediate */
  will-change: opacity, transform;
}

/* Variants */
[data-cmha-module="page-hero"].cmha-anim-ready [data-cmha-reveal="fade"],
[data-cmha-module="page-hero"].cmha-anim-ready [data-cmha-reveal-group="fade"] > *  { transform: none; }
[data-cmha-module="page-hero"].cmha-anim-ready [data-cmha-reveal="left"],
[data-cmha-module="page-hero"].cmha-anim-ready [data-cmha-reveal-group="left"] > *  { transform: translate3d(calc(var(--cmha-reveal-shift, 34px) * -1.4), 0, 0); }
[data-cmha-module="page-hero"].cmha-anim-ready [data-cmha-reveal="right"],
[data-cmha-module="page-hero"].cmha-anim-ready [data-cmha-reveal-group="right"] > * { transform: translate3d(calc(var(--cmha-reveal-shift, 34px) *  1.4), 0, 0); }
[data-cmha-module="page-hero"].cmha-anim-ready [data-cmha-reveal="scale"],
[data-cmha-module="page-hero"].cmha-anim-ready [data-cmha-reveal-group="scale"] > * { transform: scale(0.96); }

[data-cmha-module="page-hero"].cmha-anim-ready [data-cmha-reveal].is-revealed,
[data-cmha-module="page-hero"].cmha-anim-ready [data-cmha-reveal-group] > .is-revealed {
  opacity: 1;
  transform: none;
  /* entering is staggered */
  transition-delay: calc(var(--cmha-reveal-i, 0) * var(--cmha-reveal-stagger, 120ms));
  will-change: auto;
}

@media (prefers-reduced-motion: reduce) {
  [data-cmha-module="page-hero"].cmha-anim-ready [data-cmha-reveal],
[data-cmha-module="page-hero"].cmha-anim-ready [data-cmha-reveal-group] > *,
  [data-cmha-module="page-hero"].cmha-anim-ready [data-cmha-reveal].is-revealed,
[data-cmha-module="page-hero"].cmha-anim-ready [data-cmha-reveal-group] > .is-revealed {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
}


/* ==========================================================================
   MODULE: CMHA Featured Person
   Files for this module:
     html/cmha-featured-person.html    → paste into the module's HTML + HubL pane
     css/cmha-featured-person.css      → paste into the module's CSS pane
     js/cmha-featured-person.js        → paste into the module's JS pane
   Uses tokens from css/cmha-base.css. The scroll animation is self-contained
   in this module's own CSS and JS — there is no site-wide animation script.
   ========================================================================== */

/* --------------------------------------------------------------------------
   CARD
   -------------------------------------------------------------------------- */
.cmha-featured {
  position: relative;
  display: flex;
  gap: clamp(1.25rem, 3vw, 2.25rem);
  align-items: flex-start;
  background: var(--cmha-surface);
  border: var(--cmha-hairline, 0.5px) solid var(--cmha-border);
  border-radius: var(--cmha-radius-lg);
  padding: clamp(1.5rem, 3vw, 2.25rem);
  overflow: hidden;
  cursor: pointer;
  transition: border-color var(--cmha-dur) var(--cmha-ease),
              box-shadow   var(--cmha-dur) var(--cmha-ease),
              transform    var(--cmha-dur) var(--cmha-ease);
}

/* Deep purple spine on the left edge */
.cmha-featured::before {
  content: '';
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: 4px;
  background: var(--cmha-purple-deep);
}

.cmha-featured:hover,
.cmha-featured:focus-within {
  border-color: var(--cmha-border-hover);
  box-shadow: var(--cmha-shadow);
  transform: translateY(-2px);
}

.cmha-featured__media { flex-shrink: 0; }

.cmha-featured__avatar,
.cmha-featured__img {
  width: clamp(76px, 11vw, 120px);
  /* See note in cmha-people-grid.css — height:auto stops the img's height
     attribute from overriding the CSS and stretching the headshot. */
  height: auto;
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--cmha-purple-pale);
  object-fit: cover;
  display: block;
}

.cmha-featured__body { min-width: 0; }

.cmha-featured__name {
  font-size: var(--cmha-fs-h3);
  font-weight: 500;
  line-height: 1.3;
  color: var(--cmha-purple-deep);
  margin: 0 0 0.25rem;
}
.cmha-featured__creds {
  display: block;
  font-size: var(--cmha-fs-micro);
  font-weight: 400;
  letter-spacing: 0.04em;
  color: var(--cmha-purple-mid);
  margin-top: 0.2rem;
}

.cmha-featured__role {
  font-size: var(--cmha-fs-small);
  line-height: 1.55;
  color: var(--cmha-purple-mid);
  margin: 0 0 0.75rem;
}
.cmha-featured__sep { padding: 0 0.5em; opacity: 0.6; }

.cmha-featured__preview {
  font-size: var(--cmha-fs-body);
  line-height: 1.65;
  color: var(--cmha-ink);
  margin: 0 0 1rem;
  max-width: 58ch;
}

/* Trigger reads as a link, behaves as a button */
.cmha-featured__trigger {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  background: none;
  border: 0;
  padding: 0;
  font-family: var(--cmha-font);
  font-size: var(--cmha-fs-small);
  font-weight: 500;
  color: var(--cmha-purple-mid);
  cursor: pointer;
  transition: color var(--cmha-dur) var(--cmha-ease);
}
.cmha-featured__trigger:hover { color: var(--cmha-purple-deep); }

.cmha-featured__arrow {
  transition: transform var(--cmha-dur) var(--cmha-ease);
}
.cmha-featured:hover .cmha-featured__arrow { transform: translateX(4px); }

@media (max-width: 560px) {
  .cmha-featured {
    flex-direction: column;
    align-items: center;
    text-align: center;
  }
  .cmha-featured__preview { max-width: none; }
}

/* --------------------------------------------------------------------------
   SLIDE-IN PANEL
   cmha-featured-person.js relocates .cmha-panel-root to the end of <body>, so these rules
   must not depend on any ancestor from the module.
   -------------------------------------------------------------------------- */
.cmha-panel__scrim {
  position: fixed;
  inset: 0;
  z-index: 9998;
  background: rgba(46, 6, 61, 0.42);
  backdrop-filter: blur(2px);
  opacity: 0;
  transition: opacity var(--cmha-dur) var(--cmha-ease);
}
.cmha-panel__scrim.is-open { opacity: 1; }

.cmha-panel {
  position: fixed;
  inset-block: 0;
  inset-inline-end: 0;
  z-index: 9999;
  width: min(440px, 100vw);
  background: var(--cmha-white);
  font-family: var(--cmha-font);
  box-shadow: var(--cmha-shadow-lg);
  overflow-y: auto;
  overscroll-behavior: contain;
  transform: translateX(102%);
  transition: transform 340ms var(--cmha-ease);
}
.cmha-panel.is-open { transform: translateX(0); }

/* [hidden] must win over the display created by position:fixed */
.cmha-panel[hidden],
.cmha-panel__scrim[hidden] { display: none; }

.cmha-panel__inner {
  padding: clamp(2.5rem, 5vw, 3.25rem) clamp(1.5rem, 4vw, 2.5rem) 3rem;
}

.cmha-panel__close {
  position: absolute;
  top: 1rem;
  inset-inline-end: 1rem;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: var(--cmha-hairline, 0.5px) solid transparent;
  background: none;
  color: var(--cmha-purple-mid);
  cursor: pointer;
  transition: background var(--cmha-dur) var(--cmha-ease),
              color      var(--cmha-dur) var(--cmha-ease);
}
.cmha-panel__close:hover {
  background: var(--cmha-lola);
  color: var(--cmha-purple-deep);
}

.cmha-panel__avatar {
  width: 104px;
  height: auto;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: 50%;
  background: var(--cmha-purple-pale);
  margin: 0 auto 1.25rem;
}

.cmha-panel__name {
  font-size: 1.375rem;
  font-weight: 500;
  line-height: 1.25;
  color: var(--cmha-purple-deep);
  text-align: center;
  margin: 0 0 0.5rem;
}

.cmha-panel__role {
  font-size: var(--cmha-fs-micro);
  line-height: 1.7;
  color: var(--cmha-purple-mid);
  text-align: center;
  margin: 0 0 1.5rem;
}

.cmha-panel .cmha-divider { margin: 1.5rem 0; }

.cmha-panel__label {
  font-size: var(--cmha-fs-eyebrow);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--cmha-purple-deep);
  margin: 1.75rem 0 0.6rem;
}
.cmha-panel__label:first-of-type { margin-top: 0; }

.cmha-panel__text p {
  font-size: var(--cmha-fs-small);
  line-height: 1.75;
  color: var(--cmha-ink);
  margin: 0 0 0.85rem;
}

.cmha-panel__links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.75rem;
}
.cmha-panel__link {
  font-size: var(--cmha-fs-micro);
  color: var(--cmha-purple-mid);
  text-decoration: none;
  border: var(--cmha-hairline, 0.5px) solid var(--cmha-border);
  border-radius: 3px;
  padding: 0.5em 1em;
  transition: background var(--cmha-dur) var(--cmha-ease),
              color      var(--cmha-dur) var(--cmha-ease),
              border     var(--cmha-dur) var(--cmha-ease);
}
.cmha-panel__link:hover {
  background: var(--cmha-purple-deep);
  border-color: var(--cmha-purple-deep);
  color: var(--cmha-white);
}

/* Scroll lock while a panel is open. The class itself is defined in cmha-base.css,
   which owns rules that target <body>; the padding-right is applied inline by
   the JS to prevent a scrollbar jump */
body.cmha-scroll-lock {
  overflow: hidden;
  /* padding-right is applied inline by the JS to prevent scrollbar jump */
}

@media (prefers-reduced-motion: reduce) {
  .cmha-panel,
  .cmha-panel__scrim { transition: none; }
}

/* --------------------------------------------------------------------------
   PAGE ENTRANCE
   --------------------------------------------------------------------------
   The inline script in this module's HTML adds .cmha-entrance before the first
   paint, so the module is already hidden when the page renders — no flash of
   content that then disappears to fade back in.

   The module JS decides what to do with it: if the module is on screen at
   load, it fades in as one unit and its children skip their individual reveal
   for that first pass (a fade inside a fade reads as mush). If the module is
   below the fold, the JS drops this class immediately and the normal
   per-element scroll reveal takes over.

   Both classes are removed once the entrance finishes, so nothing is left
   holding an opacity or a transform on the module root.
   -------------------------------------------------------------------------- */
/* NOTE: the hidden state carries NO transition, on purpose. If it did, adding
   the class would animate the module from visible to hidden over 850ms — and
   since the JS adds .is-entered a frame or two later, the two would cancel and
   the module would simply appear. The transition lives only on the entered
   state, so arming snaps instantly and only the reveal animates. */
[data-cmha-module="featured-person"].cmha-entrance {
  opacity: 0;
  transform: translate3d(0, var(--cmha-entrance-shift, 14px), 0);
  will-change: opacity, transform;
}
[data-cmha-module="featured-person"].cmha-entrance.is-entered {
  opacity: 1;
  transform: none;
  transition:
    opacity   var(--cmha-entrance-dur, 850ms) var(--cmha-entrance-ease, ease),
    transform var(--cmha-entrance-dur, 850ms) var(--cmha-entrance-ease, ease);
}

@media (prefers-reduced-motion: reduce) {
  [data-cmha-module="featured-person"].cmha-entrance {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
}

/* --------------------------------------------------------------------------
   SCROLL ANIMATION — scoped to this module only
   --------------------------------------------------------------------------
   Nothing here leaks outside [data-cmha-module="featured-person"], so this module
   animates on its own with no site-wide stylesheet or script.

   The hidden state applies only once the module root carries .cmha-anim-ready,
   which the inline script in the module HTML adds and then removes again after
   2.5s if the module JS never ran. Content can never be left invisible.

   Fallback values are inlined after each var() so the animation is still
   correct if cmha-base.css is not loaded.
   -------------------------------------------------------------------------- */
/* --------------------------------------------------------------------------
   WHY THE GROUP-CHILD SELECTORS ARE HERE
   --------------------------------------------------------------------------
   data-cmha-reveal-group is a convenience: the JS copies the group's variant
   down onto each child as data-cmha-reveal and assigns the stagger index.

   But that copy happens after the first paint. Until it lands, a child matches
   none of the rules below and sits at opacity 1 — so the moment the attribute
   arrives, the element transitions from visible to hidden over the full reveal
   duration. Anything at the edge of the viewport visibly appeared and then
   dissolved, which is exactly what arming before paint exists to prevent.

   So every rule below matches the group's children directly as well. The
   hidden state is then in force from the first paint, and the JS only fills in
   the attribute and the stagger index — it never causes a transition.

   The rule stands on its own: a hidden state must never be reachable by a
   transition. When an animation "just doesn't happen," this is usually why.
   -------------------------------------------------------------------------- */
[data-cmha-module="featured-person"].cmha-anim-ready [data-cmha-reveal],
[data-cmha-module="featured-person"].cmha-anim-ready [data-cmha-reveal-group] > * {
  opacity: 0;
  transform: translate3d(
    0,
    calc(var(--cmha-reveal-shift, 34px) * var(--cmha-reveal-dir, 1)),
    0
  );
  transition:
    opacity   var(--cmha-reveal-dur, 1100ms) var(--cmha-ease, cubic-bezier(0.22, 0.61, 0.36, 1)),
    transform var(--cmha-reveal-dur, 1100ms) var(--cmha-ease, cubic-bezier(0.22, 0.61, 0.36, 1));
  transition-delay: 0ms;          /* leaving is immediate */
  will-change: opacity, transform;
}

/* Variants */
[data-cmha-module="featured-person"].cmha-anim-ready [data-cmha-reveal="fade"],
[data-cmha-module="featured-person"].cmha-anim-ready [data-cmha-reveal-group="fade"] > *  { transform: none; }
[data-cmha-module="featured-person"].cmha-anim-ready [data-cmha-reveal="left"],
[data-cmha-module="featured-person"].cmha-anim-ready [data-cmha-reveal-group="left"] > *  { transform: translate3d(calc(var(--cmha-reveal-shift, 34px) * -1.4), 0, 0); }
[data-cmha-module="featured-person"].cmha-anim-ready [data-cmha-reveal="right"],
[data-cmha-module="featured-person"].cmha-anim-ready [data-cmha-reveal-group="right"] > * { transform: translate3d(calc(var(--cmha-reveal-shift, 34px) *  1.4), 0, 0); }
[data-cmha-module="featured-person"].cmha-anim-ready [data-cmha-reveal="scale"],
[data-cmha-module="featured-person"].cmha-anim-ready [data-cmha-reveal-group="scale"] > * { transform: scale(0.96); }

[data-cmha-module="featured-person"].cmha-anim-ready [data-cmha-reveal].is-revealed,
[data-cmha-module="featured-person"].cmha-anim-ready [data-cmha-reveal-group] > .is-revealed {
  opacity: 1;
  transform: none;
  /* entering is staggered */
  transition-delay: calc(var(--cmha-reveal-i, 0) * var(--cmha-reveal-stagger, 120ms));
  will-change: auto;
}

@media (prefers-reduced-motion: reduce) {
  [data-cmha-module="featured-person"].cmha-anim-ready [data-cmha-reveal],
[data-cmha-module="featured-person"].cmha-anim-ready [data-cmha-reveal-group] > *,
  [data-cmha-module="featured-person"].cmha-anim-ready [data-cmha-reveal].is-revealed,
[data-cmha-module="featured-person"].cmha-anim-ready [data-cmha-reveal-group] > .is-revealed {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
}


/* ==========================================================================
   MODULE: CMHA People Grid
   Files for this module:
     html/cmha-people-grid.html        → paste into the module's HTML + HubL pane
     css/cmha-people-grid.css          → paste into the module's CSS pane
     js/cmha-people-grid.js            → paste into the module's JS pane
   Uses tokens from css/cmha-base.css, but every value has an inline fallback
   so the module still works if that file is missing.
   ========================================================================== */

/* --------------------------------------------------------------------------
   GRID
   Cards are deliberately wide — these tiers hold 2–4 people, and narrow cards
   in a short row read as an unfinished grid. Raise the 300px floor below to
   make them wider still.
   -------------------------------------------------------------------------- */
.cmha-grid {
  display: grid;
  gap: clamp(1rem, 1.8vw, 1.5rem);
  grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
  align-items: start;   /* an open card must not stretch its row-mates */
}

@media (min-width: 700px) {
  .cmha-grid--2 { grid-template-columns: repeat(2, 1fr); }
  .cmha-grid--3,
  .cmha-grid--4 { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1000px) {
  .cmha-grid--3 { grid-template-columns: repeat(3, 1fr); }
  .cmha-grid--4 { grid-template-columns: repeat(4, 1fr); }
}

/* --------------------------------------------------------------------------
   DENSE GRID CADENCE
   --------------------------------------------------------------------------
   The default reveal is tuned for a tier of 2–4 people: a 120ms stagger and a
   1100ms fade read as unhurried. At twelve cards the same numbers add up to a
   2.3-second wait for the last card in the group — long enough that a visitor
   scrolling at a normal pace watches the bottom row arrive after they have
   already read the top one.

   These three overrides ride down through inheritance to the cards, so nothing
   about the animation itself changes — only its pace. Put --dense on any grid
   holding more than about six cards.
   -------------------------------------------------------------------------- */
.cmha-grid--dense {
  --cmha-reveal-stagger: 45ms;
  --cmha-reveal-dur: 700ms;
  --cmha-reveal-shift: 24px;
}

/* --------------------------------------------------------------------------
   CARD
   -------------------------------------------------------------------------- */
.cmha-person {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  background: var(--cmha-surface, #fff);
  border: var(--cmha-hairline, 0.5px) solid var(--cmha-border, #D5C9D5);
  border-radius: var(--cmha-radius, 3px);
  padding: clamp(1.5rem, 2.4vw, 2rem);
  overflow: hidden;
  cursor: pointer;
  transition: border-color var(--cmha-dur, 260ms) var(--cmha-ease, ease),
              box-shadow   var(--cmha-dur, 260ms) var(--cmha-ease, ease),
              transform    var(--cmha-dur, 260ms) var(--cmha-ease, ease),
              opacity      var(--cmha-dur, 260ms) var(--cmha-ease, ease),
              filter       var(--cmha-dur, 260ms) var(--cmha-ease, ease);
}

.cmha-person:hover {
  border-color: var(--cmha-border-hover, #877097);
  transform: translateY(-3px);
  box-shadow: var(--cmha-shadow, 0 2px 14px rgba(46, 6, 61, 0.08));
}

/* Accent bar across the top, cycling through the palette.
   The cycle length matches the column count so the colors line up into
   vertical stripes instead of drifting diagonally down the grid. */
[data-cmha-accent="on"] .cmha-person::before {
  content: '';
  position: absolute;
  inset-inline: 0;
  top: 0;
  height: 3px;
  background: var(--cmha-purple-mid, #877097);
}

[data-cmha-accent="on"].cmha-grid--2 .cmha-person:nth-child(2n)::before     { background: var(--cmha-purple-light, #AC9CB4); }

[data-cmha-accent="on"].cmha-grid--3 .cmha-person:nth-child(3n + 2)::before { background: var(--cmha-purple-light, #AC9CB4); }
[data-cmha-accent="on"].cmha-grid--3 .cmha-person:nth-child(3n + 3)::before { background: var(--cmha-purple-deep,  #2E063D); }

[data-cmha-accent="on"].cmha-grid--4 .cmha-person:nth-child(4n + 2)::before { background: var(--cmha-purple-light, #AC9CB4); }
[data-cmha-accent="on"].cmha-grid--4 .cmha-person:nth-child(4n + 3)::before { background: var(--cmha-purple-deep,  #2E063D); }
[data-cmha-accent="on"].cmha-grid--4 .cmha-person:nth-child(4n + 4)::before { background: var(--cmha-purple-light, #AC9CB4); }

.cmha-person__avatar {
  width: clamp(72px, 8vw, 96px);
  /* height:auto is required: the img's width/height attributes (which are there
     to reserve layout space and avoid CLS) otherwise pin the height and render
     the headshot stretched. */
  height: auto;
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--cmha-purple-pale, #D5C9D5);
  object-fit: cover;
  margin-bottom: 1rem;
  flex-shrink: 0;
}

/* A roster card carries a name and a role and nothing more — no bio to open,
   so it is not a control and must not say it is. The JS refuses to activate
   these; this stops the pointer cursor promising otherwise. */
.cmha-person--static { cursor: default; }

/* The role line normally reserves two lines so that the tag and the "Read bio"
   button line up across a row. A roster card has neither, so the reservation
   is just dead space under every name. */
.cmha-person--static .cmha-person__role {
  min-height: 0;
  margin-bottom: 0;
}

/* Fallback for a person with no headshot yet. A row of identical blank circles
   reads as broken; initials read as deliberate, and each card looks different
   from its neighbours. The element stays aria-hidden — the full name is in the
   heading directly below, so a screen reader gains nothing from hearing the
   initials first.

   Nothing in the shipped markup uses this today: every real person on People
   and on the Community Advisory Board has a photo, and the two student cards
   are unnamed placeholders where initials would be meaningless. It is here for
   the HubL fields version at the bottom of html/cmha-people-grid.html, which
   falls back to it when an editor adds a person before their photo exists. */
.cmha-person__avatar--initials {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: clamp(1.125rem, 1.6vw, 1.5rem);
  font-weight: 400;
  letter-spacing: 0.04em;
  color: var(--cmha-purple-deep, #2E063D);
  background: var(--cmha-purple-pale, #D5C9D5);
  user-select: none;
}
.cmha-section--dark .cmha-person__avatar--initials {
  color: var(--cmha-purple-deep, #2E063D);
  background: var(--cmha-purple-light, #AC9CB4);
}

.cmha-person__name {
  font-size: var(--cmha-fs-body, 14px);
  font-weight: 500;
  line-height: 1.35;
  color: var(--cmha-brown, #3E2931);
  margin: 0 0 0.25rem;
}

.cmha-person__role {
  font-size: var(--cmha-fs-small, 12px);
  line-height: 1.45;
  color: var(--cmha-purple-mid, #877097);
  margin: 0 0 0.7rem;
  /* Reserves two lines so the tag and button line up across a row even when
     one person's title wraps and another's doesn't. Raise to 4.35em if a tier
     has lots of three-line titles; the grid uses align-items:start, so cards
     can't simply stretch to match. */
  min-height: 2.9em;
}

.cmha-person__body { width: 100%; min-width: 0; }

/* Two-across cards are wide enough that a centered column leaves a lot of
   dead space — put the headshot beside the text instead.

   This switches the card from flex to CSS grid so the bio can span BOTH
   columns when it opens. In flex the bio would be stuck in the right-hand
   column, wrapping in a narrow measure with a tall empty gap beside it.
   That is also why the bio markup sits outside .cmha-person__body. */
@media (min-width: 700px) {
  .cmha-grid--2 .cmha-person {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: start;
    column-gap: clamp(1.25rem, 2.5vw, 1.75rem);
    text-align: left;
  }
  .cmha-grid--2 .cmha-person__avatar {
    grid-column: 1;
    grid-row: 1;
    margin-bottom: 0;
  }
  .cmha-grid--2 .cmha-person__body { grid-column: 2; grid-row: 1; }
  .cmha-grid--2 .cmha-person__bio  { grid-column: 1 / -1; grid-row: 2; }
  /* The reserved two lines still matter here: without it a one-line title
     leaves its card shorter than the two-line card beside it. */
}

.cmha-person__toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  margin-top: 0.75rem;
  background: none;
  border: 0;
  padding: 0.3em 0;
  font-family: var(--cmha-font, inherit);
  font-size: var(--cmha-fs-micro, 10px);
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--cmha-purple-mid, #877097);
  cursor: pointer;
  transition: color var(--cmha-dur, 260ms) var(--cmha-ease, ease);
}
.cmha-person:hover .cmha-person__toggle { color: var(--cmha-purple-deep, #2E063D); }

.cmha-person__chev { transition: transform var(--cmha-dur, 260ms) var(--cmha-ease, ease); }
.cmha-person:hover .cmha-person__chev { transform: translateY(2px); }

/* --------------------------------------------------------------------------
   BIO
   Visible by default. The JS adds .cmha-js-ready to the module root and only
   THEN are bios collapsed — so with JS blocked or broken, every bio simply
   reads inline instead of being permanently hidden.
   -------------------------------------------------------------------------- */
.cmha-person__bio {
  width: 100%;
  text-align: left;
}

[data-cmha-module="people-grid"].cmha-js-ready .cmha-person__bio {
  max-height: 0;
  overflow: hidden;
}
[data-cmha-module="people-grid"]:not(.cmha-js-ready) .cmha-person__toggle {
  display: none;
}

.cmha-person__bio-inner {
  border-top: var(--cmha-hairline, 0.5px) solid var(--cmha-border, #D5C9D5);
  margin-top: 1rem;
  padding-top: 1rem;
}
.cmha-person__bio-inner p {
  font-size: var(--cmha-fs-small, 12px);
  line-height: 1.75;
  color: var(--cmha-ink, #4A4048);
  margin: 0 0 0.8rem;
}
.cmha-person__bio-inner p:last-child { margin-bottom: 0; }

/* ==========================================================================
   EMPHASIS ON CLICK
   --------------------------------------------------------------------------
   Clicking a card keeps it exactly where it is. It gains a ring, a deeper
   shadow and a heavier accent bar, its bio opens beneath it, and the rest of
   the section fades back to imply the emphasis.

   Deliberately NO transform on the active card. These cards already carry a
   1100ms `transform` transition from the scroll reveal below, and anything
   that animates transform here would have to fight it. Ring plus shadow reads
   as "bolder" without touching a property the reveal already owns.

   The two rules that DO overlap with the reveal — opacity on the faded
   siblings, and the ring on the active card — are scoped with the module
   attribute so they outrank it, and restate their own timing so they respond
   at click speed rather than on the 1100ms scroll curve.
   ========================================================================== */

/* Everything else in the section fades back. */
[data-cmha-module="people-grid"] .cmha-grid.has-active .cmha-person:not(.is-active) {
  opacity: 0.38;
  transition: opacity var(--cmha-emphasis-dur, 300ms) var(--cmha-ease, ease),
              border-color var(--cmha-emphasis-dur, 300ms) var(--cmha-ease, ease),
              box-shadow   var(--cmha-emphasis-dur, 300ms) var(--cmha-ease, ease);
  transition-delay: 0ms;
}
/* Still clickable — switching straight to another card should just work. */
[data-cmha-module="people-grid"] .cmha-grid.has-active .cmha-person:not(.is-active):hover {
  opacity: 0.62;
}

/* The active card. The ring is a box-shadow rather than a border so the card
   does not change size and shove its neighbours around. */
[data-cmha-module="people-grid"] .cmha-person.is-active {
  opacity: 1;
  border-color: var(--cmha-purple-deep, #2E063D);
  box-shadow: 0 0 0 2px var(--cmha-purple-deep, #2E063D),
              0 10px 34px rgba(46, 6, 61, 0.16);
  background: var(--cmha-surface, #fff);
  z-index: 2;
  transition: box-shadow  var(--cmha-emphasis-dur, 300ms) var(--cmha-ease, ease),
              border-color var(--cmha-emphasis-dur, 300ms) var(--cmha-ease, ease),
              opacity      var(--cmha-emphasis-dur, 300ms) var(--cmha-ease, ease);
  transition-delay: 0ms;
}

/* The accent bar thickens along with it. */
[data-cmha-accent="on"] .cmha-person.is-active::before {
  height: 6px;
  transition: height var(--cmha-emphasis-dur, 300ms) var(--cmha-ease, ease);
}

[data-cmha-module="people-grid"] .cmha-person.is-active .cmha-person__name {
  color: var(--cmha-purple-deep, #2E063D);
}

/* Bio opens in place. max-height is set inline by the JS from the measured
   height, so a bio of any length opens cleanly — no magic numbers. */
[data-cmha-module="people-grid"].cmha-js-ready .cmha-person__bio {
  transition: max-height var(--cmha-bio-dur, 380ms) var(--cmha-ease, ease);
}

.cmha-person__toggle[aria-expanded="true"] .cmha-person__chev {
  transform: rotate(180deg);
}
.cmha-person.is-active:hover .cmha-person__chev { transform: rotate(180deg); }

/* --------------------------------------------------------------------------
   DARK SECTION VARIANT (Community Advisory Board, and similar)
   -------------------------------------------------------------------------- */
.cmha-section--dark .cmha-person {
  background: rgba(251, 251, 251, 0.05);
  border-color: rgba(213, 201, 213, 0.22);
}
.cmha-section--dark .cmha-person:hover { border-color: var(--cmha-purple-light, #AC9CB4); }
.cmha-section--dark .cmha-person__name { color: var(--cmha-white, #FBFBFB); }
.cmha-section--dark .cmha-person__role,
.cmha-section--dark .cmha-person__toggle { color: var(--cmha-purple-light, #AC9CB4); }
.cmha-section--dark .cmha-person__bio-inner   { border-top-color: rgba(213, 201, 213, 0.22); }
.cmha-section--dark .cmha-person__bio-inner p { color: var(--cmha-purple-pale, #D5C9D5); }
.cmha-section--dark .cmha-tag {
  color: var(--cmha-purple-light, #AC9CB4);
  border-color: rgba(213, 201, 213, 0.3);
}
/* The active ring reads correctly on a dark section too. The scoped selector
   is written with the two on the SAME element, because that's how the markup
   has it — the dark class sits on the module root, not on a wrapper inside
   it. Written as a descendant it would never match. */
[data-cmha-module="people-grid"].cmha-section--dark .cmha-person.is-active,
.cmha-section--dark .cmha-person.is-active {
  border-color: var(--cmha-purple-light, #AC9CB4);
  box-shadow: 0 0 0 2px var(--cmha-purple-light, #AC9CB4),
              0 10px 34px rgba(0, 0, 0, 0.28);

  /* ⚠ THE LINE THAT WAS MISSING — added 27 Aug 2026. Reported as "the bios are
     hard to read"; it was worse than that.

     The light-section rule for an open card sets background:#fff, and its
     selector — [data-cmha-module="people-grid"] .cmha-person.is-active — is
     0,3,0. The dark variant's card background lives on
     .cmha-section--dark .cmha-person, which is only 0,2,0. So on a dark
     section, OPENING a card turned it white while every text colour stayed
     set for dark. Measured on the CAB page:

         name  #FBFBFB on white   1.03:1     invisible
         bio   #D5C9D5 on white   1.60:1     invisible
         role  #AC9CB4 on white   2.57:1     fails AA

     A card that becomes unreadable at the exact moment someone clicks it to
     read it. It only appeared on click, which is why it survived the earlier
     sweeps — those checked the resting grid.

     0.10 rather than the resting 0.05: an open card should read as lifted,
     and the ring alone was carrying that job. Against it the bio is 8.53:1.

     The first selector is 0,4,0, which is what beats the light rule. Do not
     "simplify" it to the second one alone. */
  background: rgba(251, 251, 251, 0.10);
}
.cmha-section--dark .cmha-person.is-active .cmha-person__name { color: var(--cmha-white, #FBFBFB); }

@media (prefers-reduced-motion: reduce) {
  .cmha-person,
  .cmha-person__avatar,
  .cmha-person__bio { transition: none !important; }
}

/* --------------------------------------------------------------------------
   PAGE ENTRANCE
   --------------------------------------------------------------------------
   The inline script in this module's HTML adds .cmha-entrance before the first
   paint, so the module is already hidden when the page renders — no flash of
   content that then disappears to fade back in.

   The module JS decides what to do with it: if the module is on screen at
   load, it fades in as one unit and its children skip their individual reveal
   for that first pass (a fade inside a fade reads as mush). If the module is
   below the fold, the JS drops this class immediately and the normal
   per-element scroll reveal takes over.

   Both classes are removed once the entrance finishes, so nothing is left
   holding an opacity or a transform on the module root.
   -------------------------------------------------------------------------- */
/* NOTE: the hidden state carries NO transition, on purpose. If it did, adding
   the class would animate the module from visible to hidden over 850ms — and
   since the JS adds .is-entered a frame or two later, the two would cancel and
   the module would simply appear. The transition lives only on the entered
   state, so arming snaps instantly and only the reveal animates. */
[data-cmha-module="people-grid"].cmha-entrance {
  opacity: 0;
  transform: translate3d(0, var(--cmha-entrance-shift, 14px), 0);
  will-change: opacity, transform;
}
[data-cmha-module="people-grid"].cmha-entrance.is-entered {
  opacity: 1;
  transform: none;
  transition:
    opacity   var(--cmha-entrance-dur, 850ms) var(--cmha-entrance-ease, ease),
    transform var(--cmha-entrance-dur, 850ms) var(--cmha-entrance-ease, ease);
}

@media (prefers-reduced-motion: reduce) {
  [data-cmha-module="people-grid"].cmha-entrance {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
}

/* --------------------------------------------------------------------------
   SCROLL ANIMATION — scoped to this module only
   --------------------------------------------------------------------------
   Nothing here leaks outside [data-cmha-module="people-grid"], so this module
   animates on its own with no site-wide stylesheet or script.

   The hidden state applies only once the module root carries .cmha-anim-ready,
   which the inline script in the module HTML adds and then removes again after
   2.5s if the module JS never ran. Content can never be left invisible.

   Fallback values are inlined after each var() so the animation is still
   correct if cmha-base.css is not loaded.
   -------------------------------------------------------------------------- */
/* --------------------------------------------------------------------------
   WHY THE GROUP-CHILD SELECTORS ARE HERE
   --------------------------------------------------------------------------
   data-cmha-reveal-group is a convenience: the JS copies the group's variant
   down onto each child as data-cmha-reveal and assigns the stagger index.

   But that copy happens after the first paint. Until it lands, a child matches
   none of the rules below and sits at opacity 1 — so the moment the attribute
   arrives, the element transitions from visible to hidden over the full reveal
   duration. Anything at the edge of the viewport visibly appeared and then
   dissolved, which is exactly what arming before paint exists to prevent.

   So every rule below matches the group's children directly as well. The
   hidden state is then in force from the first paint, and the JS only fills in
   the attribute and the stagger index — it never causes a transition.

   The rule stands on its own: a hidden state must never be reachable by a
   transition. When an animation "just doesn't happen," this is usually why.
   -------------------------------------------------------------------------- */
[data-cmha-module="people-grid"].cmha-anim-ready [data-cmha-reveal],
[data-cmha-module="people-grid"].cmha-anim-ready [data-cmha-reveal-group] > * {
  opacity: 0;
  transform: translate3d(
    0,
    calc(var(--cmha-reveal-shift, 34px) * var(--cmha-reveal-dir, 1)),
    0
  );
  transition:
    opacity   var(--cmha-reveal-dur, 1100ms) var(--cmha-ease, cubic-bezier(0.22, 0.61, 0.36, 1)),
    transform var(--cmha-reveal-dur, 1100ms) var(--cmha-ease, cubic-bezier(0.22, 0.61, 0.36, 1));
  transition-delay: 0ms;          /* leaving is immediate */
  will-change: opacity, transform;
}

/* Variants */
[data-cmha-module="people-grid"].cmha-anim-ready [data-cmha-reveal="fade"],
[data-cmha-module="people-grid"].cmha-anim-ready [data-cmha-reveal-group="fade"] > *  { transform: none; }
[data-cmha-module="people-grid"].cmha-anim-ready [data-cmha-reveal="left"],
[data-cmha-module="people-grid"].cmha-anim-ready [data-cmha-reveal-group="left"] > *  { transform: translate3d(calc(var(--cmha-reveal-shift, 34px) * -1.4), 0, 0); }
[data-cmha-module="people-grid"].cmha-anim-ready [data-cmha-reveal="right"],
[data-cmha-module="people-grid"].cmha-anim-ready [data-cmha-reveal-group="right"] > * { transform: translate3d(calc(var(--cmha-reveal-shift, 34px) *  1.4), 0, 0); }
[data-cmha-module="people-grid"].cmha-anim-ready [data-cmha-reveal="scale"],
[data-cmha-module="people-grid"].cmha-anim-ready [data-cmha-reveal-group="scale"] > * { transform: scale(0.96); }

[data-cmha-module="people-grid"].cmha-anim-ready [data-cmha-reveal].is-revealed,
[data-cmha-module="people-grid"].cmha-anim-ready [data-cmha-reveal-group] > .is-revealed {
  opacity: 1;
  transform: none;
  /* entering is staggered */
  transition-delay: calc(var(--cmha-reveal-i, 0) * var(--cmha-reveal-stagger, 120ms));
  will-change: auto;
}

@media (prefers-reduced-motion: reduce) {
  [data-cmha-module="people-grid"].cmha-anim-ready [data-cmha-reveal],
[data-cmha-module="people-grid"].cmha-anim-ready [data-cmha-reveal-group] > *,
  [data-cmha-module="people-grid"].cmha-anim-ready [data-cmha-reveal].is-revealed,
[data-cmha-module="people-grid"].cmha-anim-ready [data-cmha-reveal-group] > .is-revealed {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
}
</style>
