/* Just Mercy · chrome
   Split out of the single components.css so concurrent work on different
   areas cannot collide in one file, and so a page can eventually load only
   the CSS its elements need. Tokens come from tokens.css; no literal brand
   hex belongs in here (the page-quality gate's D2 criterion enforces it). */

/* =========================================================================
   JM Header — Figma "Organisms / Header / Desktop", "Header / Mobile",
   "Menu / Mobile"

   Breakpoint 60rem. Media queries resolve rem against the INITIAL 16px, not the
   fluid root, so 60rem is a dependable 960px — which is where the logo, four nav
   links and the CTA stop fitting on one line.
   ========================================================================= */

/* No background of its own, and it overlays the page.

   On every board the header is the first child of the hero frame, so the hero's
   fill runs behind the logo and nav. Painting the header Purple/500 put a hard
   seam across the homepage, whose hero is Purple/700. Transparent + overlaid
   reproduces the boards on every page at once, and the first section reserves the
   room with calc(var(--header-h) + var(--page-top)).

   Every hero on the site is a dark ground, so the white/gold header type keeps
   its contrast. A light hero would need a tone modifier on the header. */
.jm-header {
  position: absolute;
  inset-inline: 0;
  top: 0;
  z-index: 20;
  background-color: transparent;
}

/* ---- Sticky (scroll.js) ----
   Two states only. Below the engage point (the header's own height, so the
   swap happens exactly when the natural header has scrolled out of view and
   is invisible in both directions) the header is the transparent overlay it
   always was. Past it, a compact painted bar slides in and STAYS — always
   reachable, per Phil. The admin-bar variable keeps it clear of the toolbar
   for logged-in users. is-anim arrives one frame after is-stuck so the
   entrance slides instead of snapping; menu-open forces transform:none so
   the fixed menu overlay never gets a transformed containing block. */
.jm-header.is-stuck {
  position: fixed;
  top: var(--wp-admin--admin-bar--height, 0px);
  background-color: var(--purple-700);
  /* No shadow (Phil, 2026-09-03): the painted bar sits flat on the page. */
  /* The entrance is a keyframe, not a class dance: the slide plays the moment
     is-stuck lands and needs no second frame from script (the two-class
     version left the bar stranded off screen when that frame never came). */
  animation: jm-stick-in var(--motion-base) var(--motion-ease) backwards;
}
@keyframes jm-stick-in {
  from { transform: translateY(-102%); }
}
/* While the menu is open the header must carry NO transform at any moment —
   a transformed ancestor would become the fixed overlay's containing block. */
.jm-header.is-stuck.jm-header--menu-open { animation: none; }
.jm-header.is-stuck .jm-header__inner {
  padding-top: 1rem;
  padding-bottom: 1.2rem;              /* 20% more under the bar (Phil, 2026-09-03) */
}
.jm-header.is-stuck .jm-header__logo .jm-logo__svg { width: 9rem; }
/* The stuck MOBILE bar breathes: a third more logo and real padding under it
   (the 64px bar read cramped once painted). */
.jm-header.is-stuck .jm-header__bar {
  height: auto;
  padding-block: 0.875rem 1.05rem;     /* 20% more under the bar (Phil, 2026-09-03) */
}
.jm-header.is-stuck .jm-header__logo--small .jm-logo__svg { width: 11.08rem; }
/* The exit: for the moment the bar disengages it keeps its painted, fixed
   self and rides the same track back out (scroll.js holds is-leaving for
   the animation's length, then drops it). Reduced motion never gets the
   class, so the swap stays instant there. */
.jm-header.is-leaving {
  position: fixed;
  top: var(--wp-admin--admin-bar--height, 0px);
  background-color: var(--purple-700);
  pointer-events: none;
  animation: jm-stick-out var(--motion-base) var(--motion-ease) forwards;
}
@keyframes jm-stick-out {
  to { transform: translateY(-102%); }
}
.jm-header.is-leaving .jm-header__inner {
  padding-top: 1rem;
  padding-bottom: 1.2rem;
}
.jm-header.is-leaving .jm-header__logo .jm-logo__svg { width: 9rem; }
.jm-header.is-leaving .jm-header__bar {
  height: auto;
  padding-block: 0.875rem 1.05rem;
}

@media (prefers-reduced-motion: reduce) {
  .jm-header.is-stuck { animation: none; }
}

/* ---- The two bars ----
   MOBILE FIRST, and the desktop override comes last.

   The previous version used an exclusive pair — (min-width: 60rem) for the desktop
   bar and (max-width: 59.999rem) for the mobile bar — which leaves a hole: a
   viewport in (959.984, 960), reachable at fractional browser zoom, matched
   NEITHER query and the header rendered as an empty purple strip with no logo, nav
   or CTA. Mobile-first has no hole by construction, and putting the override last
   means source order works with the cascade rather than against it. */

.jm-header__inner {
  display: none;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  /* Measure and side gutter come from THE CONTAINER rule at the top of this file.
     Only the bar's vertical padding belongs to the header.

     80 above, 0 below. On every board the header row is the first child of the
     hero frame, which carries `p-80` — so the logo's top edge sits at y=80 and
     the row's bottom IS the start of the page's own top spacing (--page-top).
     This was a symmetric 24/24 band, which put the logo 56px higher than drawn
     on every page. Desktop only: the mobile bar (611:3338) is a 64-tall bar with
     no such inset. */
  padding-top: var(--page-top);        /* 80 */
  padding-bottom: 0;
}



.jm-header__nav {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 2rem;                           /* 32 */
}

.jm-header__link {
  color: rgb(255 255 255 / 0.82);
  font: 500 var(--text-label)/1.2 var(--font-sans);
  text-decoration: none;
  transition: color var(--motion-fast) var(--motion-ease-out);
}
.jm-header__link:hover,
.jm-header__link--current { color: var(--text-inverse); }
.jm-header__link:focus-visible { outline: 3px solid var(--gold-500); outline-offset: 4px; }

.jm-header__logo .jm-logo__svg { width: 12.875rem; }   /* 206 */

/* ---- Mobile bar ---- */

.jm-header__bar {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  /* Was the board's fixed 64px bar, but the logo grew a third (Phil,
     2026-08-30) and was touching the top of the page. Same rhythm as the
     overlay's top bar and the stuck bar: real padding plus the safe area. */
  padding-block: 0.875rem;
  padding-top: calc(env(safe-area-inset-top, 0px) + 0.875rem);
  padding-inline: var(--gutter);       /* 24 at the mobile board, via the gutter step */
}

/* The one switch between the two bars. No hole, no order dependence. */
@media (min-width: 60rem) {
  .jm-header__inner { display: flex; }
  .jm-header__bar { display: none; }
  .jm-mobile-menu { display: none; }
}

.jm-header__logo--small .jm-logo__svg { width: 11.08rem; } /* 133.294 up a third (Phil, 2026-08-30) — matches the stuck bar */

.jm-burger {
  display: flex;
  align-items: center;
  justify-content: center;
  /* 44 — but the tap minimum is an accessibility floor in device px, not a
     design length, so it must not shrink with the fluid root (41.9 at 390). */
  width: max(2.75rem, 44px);
  height: max(2.75rem, 44px);
  margin-right: -0.625rem;             /* pull the 44px target back to the 24px edge */
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
}
.jm-burger:focus-visible { outline: 3px solid var(--gold-500); outline-offset: 2px; }

.jm-burger__bars {
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* Phil, 2026-08-30: longer, thinner bars than the drawn 24x2 — his call
     overrides the nav spec board here. */
  gap: 0.4375rem;                      /* 7 */
  width: 1.875rem;                     /* 30 */
}
.jm-burger__bars span {
  display: block;
  height: 1.5px;
  border-radius: 1px;
  background-color: var(--text-inverse);
}

/* ---- Mobile overlay ----

   NAMED `jm-mobile-menu`, NOT `jm-menu`. It used to be `.jm-menu`, which is also
   the select dropdown's listbox in menus.css. Same specificity, and chrome.css
   loads after menus.css, so the header's rules won every time: every jm-dropdown
   and jm-multi-select menu rendered as a fixed, full-viewport purple sheet
   instead of a panel under its trigger, and above 60rem the header's
   `.jm-menu { display: none }` meant a select menu could not open on desktop at
   all. The header's `data-jm-menu*` hooks were renamed with it, since menus.js
   initialises every `[data-jm-menu]` and was matching the header panel too (its
   guard bailed, so that half was latent rather than broken).

   Both components own a `__`-namespace, so keep the roots distinct. */

.jm-mobile-menu {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  background-color: var(--purple-500);
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Slides in from the right as a full sheet (Phil, 2026-08-30 — replaced the
     250ms top-fade). The enter bezier is the tokenised organic curve: fast
     out of the gate, a soft settle at the end. Closing runs the same track
     back out to the right. */
  transform: translateX(100%);
  transition: transform var(--motion-slow) var(--motion-ease-enter);
}
.jm-mobile-menu[hidden] { display: none; }
.jm-mobile-menu.is-open { transform: none; }
@media (prefers-reduced-motion: reduce) {
  .jm-mobile-menu { transition: none; }
}

.jm-mobile-menu__top {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  flex: 0 0 auto;
  /* Was a fixed 64px bar, which pinned the logo a few px under the phone's
     status-bar band when the menu opened from a scrolled page (Phil,
     2026-08-30). Real padding instead — matching the stuck bar's rhythm —
     plus the device safe area, so the logo always breathes at the top. */
  padding-block: 0.875rem;
  padding-top: calc(env(safe-area-inset-top, 0px) + 0.875rem);
  padding-inline: var(--gutter);
}

.jm-mobile-menu__close {
  display: flex;
  align-items: center;
  justify-content: center;
  width: max(2.75rem, 44px);           /* same 44px floor as the burger */
  height: max(2.75rem, 44px);
  margin-right: -0.625rem;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text-inverse);
  font-size: 1.625rem;                 /* 26 */
  line-height: 1;
  cursor: pointer;
}
.jm-mobile-menu__close:focus-visible { outline: 3px solid var(--gold-500); outline-offset: 2px; }

/* The morph: the close control draws the burger's own bars, and once the
   overlay is open they fold into a cross — same position, same geometry, so
   pressing the burger reads as the icon transforming in place rather than
   swapping for a different glyph. Reduced motion lands on the end state
   instantly (the transition is the only thing dropped). */
.jm-burger__bars--close { pointer-events: none; }
.jm-burger__bars--close span {
  transition: transform var(--motion-base) var(--motion-ease),
              opacity var(--motion-base) var(--motion-ease);
}
.jm-mobile-menu.is-open .jm-burger__bars--close span:nth-child(1) {
  transform: translateY(0.53125rem) rotate(45deg);   /* down onto the centre line */
}
.jm-mobile-menu.is-open .jm-burger__bars--close span:nth-child(2) {
  opacity: 0;
  transform: scaleX(0.4);
}
.jm-mobile-menu.is-open .jm-burger__bars--close span:nth-child(3) {
  transform: translateY(-0.53125rem) rotate(-45deg); /* up onto the centre line */
}
@media (prefers-reduced-motion: reduce) {
  .jm-burger__bars--close span { transition: none; }
}

.jm-mobile-menu__nav {
  display: flex;
  flex-direction: column;
  flex: 0 0 auto;
  padding-block: 1.5rem 0;             /* 24 top */
  padding-inline: var(--gutter);
}

.jm-mobile-menu__link {
  display: flex;
  align-items: center;
  padding: 1.125rem 0;                 /* 18 */
  border-bottom: 1px solid rgb(255 255 255 / 0.16);
  color: var(--text-inverse);
  /* 28 at the 390 board, as its own size rather than the shared heading-l clamp.
     `--text-heading-l` bottoms out at 24px at 390 — correct for a section heading
     that has to shrink, wrong for the mobile menu, whose links the board draws at
     a flat 28 (611:4825). 28 / 15.255 (the root at 390) = 1.8355rem, so it still
     rides the fluid root rather than being pinned in px. */
  font: 600 1.8355rem/1.2 var(--font-sans);
  text-decoration: none;
  transition: color var(--motion-fast) var(--motion-ease-out);
}
.jm-mobile-menu__link--current { color: var(--gold-500); }
.jm-mobile-menu__link:hover { color: var(--gold-500); }
.jm-mobile-menu__link:focus-visible { outline: 3px solid var(--gold-500); outline-offset: -3px; }

.jm-mobile-menu__bottom {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 1rem;                           /* 16 */
  margin-top: auto;                    /* the Figma spacer */
  padding-block: 2rem;                 /* 32 */
  padding-inline: var(--gutter);
}

.jm-mobile-menu__email {
  font: 500 var(--text-label)/1.3 var(--font-sans);
  color: var(--text-inverse);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}
.jm-mobile-menu__email:focus-visible { outline: 3px solid var(--gold-500); outline-offset: 2px; }

.jm-mobile-menu__values {
  margin: 0;
  font: 400 var(--text-label-s)/1.4 var(--font-sans);
  color: rgb(255 255 255 / 0.7);
}

@media (prefers-reduced-motion: reduce) {
  /* The spec keeps the fade and drops only the transform. */
  .jm-mobile-menu { transform: none; transition: opacity var(--motion-base) var(--motion-ease); }
}


/* =========================================================================
   JM Footer — Figma "Organisms / Footer / Desktop" (304:523) and
   "Organisms / Footer / Mobile" (611:4921)

   Every number is read off those nodes. rem values are the Figma px ÷ 16, which
   is the fluid root at the 1440 anchor, so the render matches the board there and
   scales either side of it.
   ========================================================================= */

.jm-footer {
  background-color: var(--purple-500);
  color: var(--text-inverse);
}

/* Footer links inherit the footer's colour — but NOT buttons, which carry their
   own label colour. `.jm-footer a` (0,1,1) out-specifies `.jm-button` (0,1,0), so
   without the exclusion the gold CTA rendered a white label on gold. */
.jm-footer a:not(.jm-button) { color: inherit; }

.jm-footer__inner {
  display: flex;
  flex-direction: column;
  gap: 2.5rem;                         /* 40 */
  /* Measure and side gutter come from THE CONTAINER rule. Figma's footer padding
     is 80 all round, and 80 is also the gutter, so the two agree at the board
     width — but they are different things and step independently below it. */
  padding-block: 5rem;                 /* 80 */
}

.jm-footer__columns {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  justify-content: space-between;
  gap: 2.0625rem;                      /* 33 */
}

/* ---- Identity ---- */

/* Figma marks the identity column and the right-hand group `shrink-0`, so neither
   may be squeezed. Without this the statement box compressed from 388 to 252 and
   wrapped to three lines instead of two. */
.jm-footer__identity {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  flex: 0 0 auto;
  gap: 1.6875rem;                      /* 27 */
}

/* The right-hand group: Explore, Get on board and the CTA, gap 33. */
.jm-footer__right {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  flex: 0 0 auto;
  gap: 2.0625rem;                      /* 33 */
}

.jm-footer__logo .jm-logo__svg { width: 11.6875rem; }   /* 187 */

.jm-footer__statement {
  margin: 0;
  width: 24.25rem;                     /* 388 */
  max-width: 100%;
  font: 600 var(--text-body-m)/1.2 var(--font-sans);    /* 18; the board's leading-none read too tight in the wild (Phil, 2026-08-30) */
}

.jm-footer__blurb {
  margin: 0;
  width: 28.3044rem;                   /* 452.87 */
  max-width: 100%;
  font: 400 var(--text-caption)/1.2 var(--font-sans);   /* 14 */
  opacity: 0.82;
}

/* ---- Link columns ---- */

.jm-footer__links {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  flex: 0 0 auto;
  gap: 2.0625rem;                      /* 33 */
}

.jm-footer__group { min-width: 0; }

.jm-footer__summary {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0.75rem;                        /* 12 */
  margin: 0;                           /* it is a <p> on desktop, see footer.js */
  list-style: none;
  cursor: pointer;
}
/* Safari still paints the disclosure triangle without this. */
.jm-footer__summary::-webkit-details-marker { display: none; }
.jm-footer__summary::marker { content: ''; }

/* On desktop the columns never collapse, so the titles are plain labels, not
   controls (Phil, 2026-08-31): no pointer, no click. footer.js pairs this by
   swapping each <details> for a plain <div> and its <summary> for a <p>, so
   nothing in a wide-screen column can take focus. (It used to leave the
   <details> in place with tabindex="-1" on the summary; Chrome reports that as
   an accessibility issue, "interactive element inside of a <summary>".) */
@media (min-width: 48rem) {
  .jm-footer__summary {
    pointer-events: none;
    cursor: default;
  }
}

.jm-footer__heading {
  flex: 1 0 0;
  min-width: 0;
  font: 500 var(--text-overline)/1.2 var(--font-sans);  /* 11 Medium */
  color: var(--gold-500);
  text-transform: uppercase;
}

.jm-footer__caret {
  display: none;                       /* desktop board shows no caret */
  flex: 0 0 auto;
  width: 1rem;                         /* 16 */
  height: 1rem;
  color: var(--gold-500);
  transition: transform var(--motion-fast) var(--motion-ease-out);
}

.jm-footer__group[open] .jm-footer__caret { transform: rotate(180deg); }

.jm-footer__summary:focus-visible {
  outline: 3px solid var(--gold-500);
  outline-offset: 2px;
}

.jm-footer__list {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;                        /* 12 */
  margin: 1.125rem 0 0;                /* 18 — the Figma column gap */
  padding: 0;
  list-style: none;
}
/* Hug the content, do not pin a width.

   These were 158 and 170, read off the board. But the board's columns are
   hug-width, and the measured text is 158.0 and 170.2 once the trailing letter
   space of the 0.375px tracking is counted — so "Coordinated response" and
   "Organisational alliances" each wrapped to two lines by a fraction of a pixel.
   That pushed the footer from 425 to 445.7 tall on every page. max-content
   reproduces Figma's hug exactly and cannot round the wrong way. */
.jm-footer__list--explore,
.jm-footer__list--onboard {
  width: max-content;
  max-width: 100%;
}

/* A title with no destination is not a link. It still has to sit on the same
   baseline and rhythm as the ones that are. */
.jm-footer__item {
  display: inline-block;
  font: 500 var(--text-label)/1.2 var(--font-sans);
  letter-spacing: 0.025em;
  opacity: 0.92;
}

.jm-footer__list a {
  display: inline-block;
  font: 500 var(--text-label)/1.2 var(--font-sans);   /* 15 Medium */
  letter-spacing: 0.025em;             /* 0.375px at 15 */
  opacity: 0.92;
  text-decoration: none;
  transition: opacity var(--motion-fast) var(--motion-ease-out), color var(--motion-fast) var(--motion-ease-out);
}
.jm-footer__list a:hover { opacity: 1; color: var(--gold-500); }
.jm-footer__list a:focus-visible { outline: 3px solid var(--gold-500); outline-offset: 2px; }

/* ---- CTA column ---- */

.jm-footer__cta {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  flex: 0 0 auto;
  gap: 1rem;                           /* 16 */
}

.jm-footer__cta-blurb {
  margin: 0;
  width: 14.0776rem;                   /* 225.242 */
  max-width: 100%;
  font: 400 var(--text-caption)/1.2 var(--font-sans);   /* 14 */
  opacity: 0.84;
}

.jm-footer__cta-tags {
  margin: 0;
  font: 500 var(--text-overline)/1.2 var(--font-sans);  /* 11 Medium */
  color: var(--gold-500);
  text-transform: uppercase;
}

/* ---- Legal row ---- */

.jm-footer__legal {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  gap: 1.25rem;                        /* 20 */
  padding-top: 1.25rem;                /* 20 */
  border-top: 1px solid var(--stone-500);
}

.jm-footer__legal-values {
  margin: 0;
  width: 28.125rem;                    /* 450 */
  max-width: 100%;
  font: 500 var(--text-label)/1.2 var(--font-sans);     /* 15 Medium */
  opacity: 0.85;
}

.jm-footer__legal-meta {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 1.25rem;                        /* 20 */
}
.jm-footer__legal-meta p {
  margin: 0;
  font: 400 var(--text-label-s)/1.3 var(--font-sans);   /* 13 */
  opacity: 0.75;
  white-space: nowrap;
}


/* =========================================================================
   Footer, mobile — Figma "Organisms / Footer / Mobile" (611:4921)

   padding 48 top / 40 bottom / 24 sides, gap 32. Brand gap 20, logo 157x52.
   The two link groups become accordions: a 1px rgba(255,255,255,0.16) rule above
   each, an 18-padded header row with a 12 gap and a 16px caret, and a rule after
   the last. Fine print stacked at gap 10.
   ========================================================================= */

@media (max-width: 47.99rem) {
  .jm-footer__inner {
    gap: 2rem;                         /* 32 */
    padding-block: 3rem 2.5rem;        /* 48 top, 40 bottom — the mobile board */
  }

  .jm-footer__columns {
    flex-direction: column;
    align-items: stretch;
    gap: 2rem;                         /* 32 */
  }

  .jm-footer__identity { gap: 1.25rem; }              /* 20 */
  .jm-footer__logo .jm-logo__svg { width: 9.8024rem; } /* 156.839 */
  .jm-footer__statement,
  .jm-footer__blurb { width: auto; }

  .jm-footer__right {
    flex-direction: column;
    align-items: stretch;
    gap: 2rem;                         /* 32 */
  }

  .jm-footer__links {
    flex-direction: column;
    /* The desktop rule aligns these to flex-start, which makes each <details>
       shrink to its content and leaves the accordion rules short of the edges.
       Mobile wants them full width. */
    align-items: stretch;
    gap: 0;
    border-top: 1px solid rgb(255 255 255 / 0.16);
  }

  .jm-footer__group {
    width: 100%;
    border-bottom: 1px solid rgb(255 255 255 / 0.16);
  }

  .jm-footer__summary { padding: 1.125rem 0; }        /* 18 */
  .jm-footer__caret { display: block; }

  .jm-footer__list {
    width: auto;
    margin: 0 0 1.125rem;              /* the 14 gap under the header, then 18 */
  }

  /* The footer CTA hugs its label; it does NOT go full width.

     The board is deliberate about the difference: the footer's button (611:4911)
     is `px-16 py-12` with no fill directive, so it hugs at about 197, while the
     mobile MENU's CTA (611:4844) genuinely does carry `w-full`. Stretching this
     one made the two identical and lost that distinction. */
  .jm-footer__cta { align-items: flex-start; }
  .jm-footer__cta-blurb { width: auto; }

  .jm-footer__legal {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.625rem;                     /* 10 */
    padding-top: 2rem;
    border-top-color: rgb(255 255 255 / 0.16);
  }
  .jm-footer__legal-values { width: auto; }
  .jm-footer__legal-meta {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.625rem;                     /* 10 */
  }
  .jm-footer__legal-meta p { white-space: normal; }
}

/* Between the mobile board and the desktop board Figma draws nothing, so these
   two steps are our decision, not measurements.

   The three ranges below are MUTUALLY EXCLUSIVE on purpose. An open-ended
   `max-width` query here also matches phone widths, and being later in the file it
   silently overrode the mobile padding — the footer columns came out 314 wide
   inside a 390 viewport. Bounding both ends makes the ranges independent of source
   order.

   First: between 1152 and 1440 the 80px side padding is what runs out, not the
   columns, so it tightens before anything is allowed to reflow. */


/* Then, when the columns genuinely stop fitting side by side, the right-hand
   group wraps under the identity block before anything stacks. */
/* One continuous range, so no width can fall between the two footer queries. The
   previous pair left holes at (1152.000, 1152.016) and (767.984, 768.000) where
   neither matched, the base padding applied, and the shrink-0 columns overflowed
   by about 66px. */
@media (min-width: 48rem) and (max-width: 72rem) {
  .jm-footer__columns { flex-wrap: wrap; gap: 3rem 2.0625rem; }
  .jm-footer__statement,
  .jm-footer__blurb { width: auto; max-width: 30rem; }
}

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

/* =========================================================================
   Section photography — homepage (7:276, 7:359, 7:450)

   The boards composite these; they are not plain background images.
   Bricks writes `background-image` on the section from its own background
   control, so these classes only add the compositing on top of it.
   ========================================================================= */

/* Hero (7:276). The board lays the photograph over a radial gradient in
   luminosity blend, so the picture reads as tone rather than as colour and the
   white headline stays legible across all of it. `background-blend-mode` blends
   the section's image with its background-color, which is already Purple/700 —
   so the brand colour IS the gradient's dominant stop and the photo tints into
   it. The board's multi-stop radial (cream through greys to near-black, anchored
   bottom-right) is approximated by that single colour: worth revisiting with Phil
   if he wants the exact ramp. */
.jm-section-photo--hero {
  /* Two layers: the photograph in `luminosity` over the board's own gradient.
     Luminosity keeps the picture's tone and takes hue and saturation from what
     is behind it, so the gradient below has to BE the board's gradient — against
     a flat purple the whole hero came out purple.

     The photo's URL arrives as `--jm-hero-photo` on the element, set by the page
     script from the media library, so nothing is hardcoded here.

     The photo lives on its OWN ::before layer (isolation keeps the blend
     inside the section) so its arrival can be graceful: the gradient paints
     with the page, and once the image has DECODED (the inline script in
     functions.php flips jm-hero-pending to jm-hero-ready) the photo eases in
     over it, instead of popping a split second after load (Phil,
     2026-08-31). Preloaded with fetchpriority high, so on most visits it is
     ready at first paint and the ease is barely there. Without JS the
     pending class never exists and the photo simply shows. */
  position: relative;
  isolation: isolate;
  background-image: var(--jm-hero-gradient);
  background-size: cover;
  background-position: center center;
  background-repeat: no-repeat;
}
.jm-section-photo--hero::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: var(--jm-hero-photo) center center / cover no-repeat;
  mix-blend-mode: luminosity;
}
.jm-section-photo--hero > * { position: relative; z-index: 1; }

html.jm-hero-pending .jm-section-photo--hero::before { opacity: 0; }
html.jm-hero-ready .jm-section-photo--hero::before {
  opacity: 1;
  transition: opacity var(--motion-slow) var(--motion-ease-out);
}
@media (prefers-reduced-motion: reduce) {
  html.jm-hero-ready .jm-section-photo--hero::before { transition: none; }
}

/* The opportunity (7:359). The dotted Oceania map sits to the right and a scrim
   fades the section's own Stone ground across it so the copy column stays clean.
   The board's angle is 97.29deg with the stone opaque to 17.92% and clear by
   46.862%. */
.jm-section-photo--map {
  position: relative;
  isolation: isolate;
  /* clip, NOT hidden: overflow:hidden makes the section a scroll container,
     and every view() timeline inside it (the stat cards, the still's drift)
     then resolves against the SECTION instead of the viewport and finishes
     instantly (found 2026-08-31). clip clips the globe bleed identically
     without becoming a scroll container. */
  overflow: clip;
}

/* ---- The map, as a scroll-scrubbed scene (Phil, 2026-08-30) ----

   The dotted Oceania map lives on its own layer now (::before), fed by the
   --jm-map-photo variable the build script sets, and on browsers with CSS
   scroll-driven animations it drifts and rotates with the section's travel
   through the viewport — locked to scroll by construction, pivoting around
   Aotearoa (the transform-origin sits on it), pulling NZ toward centre at
   mid-section. Everything else gets the static crop, which matches the old
   drawn placement.

   The layer's box bleeds past the section (more on the right, which is where
   the leftward drift uncovers) so no edge ever shows. The sizing re-derives
   the drawn crop for the bigger box: art was 84.4% x 127.8% of the section;
   the layer is 138% x 120% of it, so 61.2% x 106.5%, re-anchored. */
.jm-section-photo--map::before {
  content: '';
  position: absolute;
  z-index: 0;
  inset: -10% -28% -10% -10%;
  pointer-events: none;
  background-image: var(--jm-map-photo);
  background-size: 61.2% 106.5%;
  background-position: 74% 63%;
  background-repeat: no-repeat;
}

@supports (animation-timeline: view()) {
  .jm-section-photo--map::before {
    transform-origin: 66% 62%;           /* the pivot sits on Aotearoa */
    animation: jm-map-drift linear both;
    animation-timeline: view();
    animation-range: cover 0% cover 100%;
  }
}
@keyframes jm-map-drift {
  from { transform: translate3d(2%, -2%, 0) rotate(-4deg); }
  to   { transform: translate3d(-17%, 2%, 0) rotate(4deg); }
}
@media (prefers-reduced-motion: reduce) {
  .jm-section-photo--map::before { animation: none; }
}

/* The legibility scrim rides ABOVE the map layer (::after paints after
   ::before at the same z), below the raised content. */
.jm-section-photo--map::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  /* Stopped explicitly, and fading to stone-500 at zero alpha rather than to
     `transparent`. `transparent` is rgba(0,0,0,0), so the ramp is asked to
     travel to black as well as to nothing, and the middle of a two-stop fade
     goes grey and coarse over a photograph.

     The opaque plateau runs to 26%, not 17.92%. The map artwork's own left edge
     falls at about 22.5% of the section, and measured on the live page it was a
     five-level step in an otherwise flat cream field: subtle in the browser, but
     any compression latches onto it and draws a hard vertical line. Holding the
     scrim opaque past the artwork's edge removes the step at its source. */
  background: linear-gradient(
    97.29deg,
    rgba(244, 242, 235, 1)     26%,
    rgba(244, 242, 235, 0.94)  29.5%,
    rgba(244, 242, 235, 0.85)  32.5%,
    rgba(244, 242, 235, 0.74)  35.5%,
    rgba(244, 242, 235, 0.61)  38%,
    rgba(244, 242, 235, 0.47)  40.5%,
    rgba(244, 242, 235, 0.33)  42.5%,
    rgba(244, 242, 235, 0.19)  44.5%,
    rgba(244, 242, 235, 0.08)  45.9%,
    rgba(244, 242, 235, 0)     46.862%
  );
}
.jm-section-photo--map > * { position: relative; z-index: 1; }

/* On mobile the copy sits directly over the map with no clear column, so the
   angled scrim is not enough: a light sand wash across the whole scene keeps
   the text legible while the globe stays present underneath (Phil,
   2026-08-30). */
@media (max-width: 47.99rem) {
  .jm-section-photo--map::after {
    background: color-mix(in srgb, var(--stone-500) 75%, transparent);
  }
}

/* Our role (7:450). A full-bleed pencil sketch behind the copy, no blend. */
.jm-section-photo--sketch {
  background-size: cover;
  background-position: center center;
  background-repeat: no-repeat;
}

/* ---- The live globe (jm-globe.js) ----
   A canvas layer between the static map still (::before) and the scrim
   (::after). Until the first frame renders, only the still shows; then the
   section gains is-globe-live, the canvas fades in and the still hands over.
   Every fallback path — reduced motion, no WebGL, Save-Data, any error —
   simply never adds the class. */
.jm-section-photo--map > .jm-globe {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--motion-slow) var(--motion-ease-out);
}
.jm-section-photo--map.is-globe-live > .jm-globe { opacity: 1; }
.jm-section-photo--map.is-globe-live::before {
  opacity: 0;
  transition: opacity var(--motion-slow) var(--motion-ease-out);
  animation: none;
}
.jm-globe canvas { display: block; width: 100%; height: 100%; }

/* ---- The Opportunity stat cards ride the scroll like the map does
   (Phil, 2026-08-31): each card slides in from the right, scrubbed to its
   own travel into the viewport — position in the stack IS the stagger, and
   scrolling back runs it in reverse, exactly like the globe. Desktop only:
   below 48rem these cards live in the horizontal swipe track, where a
   vertical-scroll-driven slide would fight the scroller. Browsers without
   scroll-driven animations simply show the cards, and reduced motion turns
   it off. */
@supports (animation-timeline: view()) {
  @media (min-width: 48rem) {
    .jm-opp-stat {
      animation: jm-opp-stat-in linear both;
      animation-timeline: view();
      /* Starts once the card is a third of the way in and completes as it
         fully enters (Phil, 2026-08-31: later and more obvious) — the
         shorter scroll window makes the same travel read faster. */
      animation-range: entry 35% entry 100%;
    }
  }
  @media (prefers-reduced-motion: reduce) {
    .jm-opp-stat { animation: none; }
  }
}
@keyframes jm-opp-stat-in {
  from { opacity: 0; transform: translateX(6.5rem); }
}

/* ---- The pin (Phil, 2026-08-31): the section must be FULLY in view and
   hold its scroll position until every stat card has arrived, then release.
   No scroll-jacking: the track is 130vh taller than the viewport, the
   section is sticky inside it, and the cards' animations are driven by the
   TRACK's named view-timeline with staggered ranges across the pinned
   stretch — native scrolling all the way, reversible, and the scrollbar
   stays honest. The globe reads its progress from the track too
   (jm-globe.js), so the orbit rides the pin. Desktop only; without
   named-timeline support the track is an inert div and the cards keep
   their own entry animation from the block above. */
@media (min-width: 48rem) {
  @supports (view-timeline-name: --jm-probe) {
    .jm-opp-pin {
      height: 230vh;
      view-timeline-name: --jm-opp;
    }
    .jm-opp-pin > .jm-section-photo--map {
      position: sticky;
      top: 0;
      box-sizing: border-box;
      min-height: 100dvh;
      justify-content: center;
      /* The drawn 140px breathing would push content past a short viewport
         while pinned; compress toward the viewport instead. */
      padding-block: clamp(2rem, 6vh, 8.75rem);
    }
    .jm-opp-pin .jm-opp-stat { animation-timeline: --jm-opp; }
    .jm-opp-pin .jm-opp-stat:nth-child(1) { animation-range: contain 0% contain 30%; }
    .jm-opp-pin .jm-opp-stat:nth-child(2) { animation-range: contain 18% contain 48%; }
    .jm-opp-pin .jm-opp-stat:nth-child(3) { animation-range: contain 36% contain 66%; }
    .jm-opp-pin .jm-opp-stat:nth-child(4) { animation-range: contain 54% contain 84%; }
  }
}

/* The facts sub-text scroll-collapse lived here 2026-08-31 and was removed
   the same day at Phil's call once the card gained its fold-rise. */

/* The footer's legal line, now real links (Policies page, 2026-08-30). */
.jm-footer__legal-links a {
  color: inherit;
  text-underline-offset: 0.2em;
}
.jm-footer__legal-links a:hover { color: var(--text-inverse); }

/* Static crossed variant of the burger bars: the SAME geometry the mobile
   menu's morph lands on, reusable anywhere a close control is needed (the
   conversation modal uses it), so every X on the site is one component. */
.jm-burger__bars--x span:nth-child(1) { transform: translateY(0.53125rem) rotate(45deg); }
.jm-burger__bars--x span:nth-child(2) { opacity: 0; transform: scaleX(0.4); }
.jm-burger__bars--x span:nth-child(3) { transform: translateY(-0.53125rem) rotate(-45deg); }

/* The open menu panel takes programmatic focus (header.js); a container's
   own focus never paints a ring — rings belong to the controls inside. */
.jm-mobile-menu:focus { outline: none; }

/* Below 48rem the hero paints the phone-sized photo (--jm-hero-photo-m,
   set beside the master by the build script; falls back to the master if
   the small file is ever missing). */
@media (max-width: 47.99rem) {
  .jm-section-photo--hero::before {
    background-image: var(--jm-hero-photo-m, var(--jm-hero-photo));
  }

  /* Phones get the banner heading 20% larger (Phil, 2026-09-01) — the
     fluid display size reads small on a phone-width hero. */
  .jm-hero-copy h1 {
    font-size: calc(var(--text-display-l) * 1.2);
  }
}

/* ---- The hero Facts card pops out of the fold (Phil, 2026-08-31) ----
   Hidden at load; the FIRST scroll movement raises it into place, scrubbed
   over the opening 12rem of page travel so it reads as emerging from the
   edge below — the eye lands on the headline first, the card arrives when
   the reader moves. Scroll(root), so the scroll-container trap cannot bite;
   no support or reduced motion simply shows the card. */
@supports (animation-timeline: scroll()) {
  .jm-hero-fact {
    animation: jm-hero-fact-rise linear both;
    animation-timeline: scroll(root);
    animation-range: 0 14rem;
  }
  /* Desktop: the hero PINS while the card rises (Phil, 2026-08-31: "page
     should be pinned until the facts banner card is fully visible") — the
     track is 24rem taller than the viewport, the hero sticks inside it, the
     card completes its rise at 14rem and the page holds a 10rem beat with
     the card settled before releasing (Phil, 2026-09-01: slightly longer). */
  @media (min-width: 48rem) {
    .jm-hero-pin { height: calc(100vh + 24rem); }
    .jm-hero-pin > .jm-section-photo--hero {
      position: sticky;
      top: 0;
      box-sizing: border-box;
      min-height: 100dvh;
      /* The section is a flex column and its container grows, so the card
         row's margin-top:auto lands the card ON the banner's bottom edge
         (the board's placement; the 100dvh stretch had left a gap under it
         — Phil, 2026-08-31). */
      display: flex;
      flex-direction: column;
    }
    .jm-hero-pin > .jm-section-photo--hero > .brxe-container { flex: 1 1 auto; }
    .jm-hero-pin .jm-hero-fact-row { margin-top: auto; }

    /* After the pin releases, the copy (headline, standfirst, CTA) stays
       put while the hero slides out beneath it, and only leaves when the
       section's bottom edge — less a tidy 3.5rem — reaches it (Phil,
       2026-09-01/02). `top` is the hero's own padding-top, so the copy sits
       at exactly its natural position throughout the pin and there is no
       jump when the stick engages. Its containing block is the flex:1
       container, whose bottom IS the section's bottom. The margin is the
       release gap and ALSO eats the flex auto-space above the card: at a
       900px-tall viewport that space is ~34px, so 3.5rem (56px) lets the
       pinned hero run ~22px past the fold there and shortens the hold by
       the same. Accepted (Phil, 2026-09-02: "a little more bottom
       padding"); taller viewports absorb it entirely. */
    .jm-hero-pin .jm-hero-copy {
      position: sticky;
      top: calc(var(--header-h) + var(--page-top));
      margin-bottom: 3.5rem;
    }
    /* The Facts card stays in flow at the section's foot and rides out
       with it, passing IN FRONT of the frozen copy where the headline's
       first line overlaps its column (Phil, 2026-09-02: no fade). The
       sticky copy is positioned and would otherwise paint over the static
       row. */
    .jm-hero-pin .jm-hero-fact-row {
      position: relative;
      z-index: 1;
    }
  }
  @media (prefers-reduced-motion: reduce) {
    .jm-hero-fact { animation: none; }
    .jm-hero-pin { height: auto; }
    .jm-hero-pin > .jm-section-photo--hero { position: static; min-height: 0; }
  }
}
@keyframes jm-hero-fact-rise {
  from { opacity: 0; transform: translateY(3rem); }
}

/* ---- The Facts card minimises to a tab (Phil, 2026-09-02) ----
   fact-min.js wraps everything above the link in .jm-hero-fact__body and
   adds the gold line control. Open, the card is a two-row grid (body 1fr,
   link auto); minimised, the body row goes to 0fr and the padding tightens,
   so the card slides DOWN to a tab (it is anchored to the hero's bottom
   edge) holding just the link, with the line — a hamburger bar's 1.875rem —
   shortened to half. #brx-content scoping beats the element's #brxe- rule,
   which sets the flex column this replaces. The width/height of the bar
   and its 44px target mirror .jm-burger exactly. */
#brx-content .jm-hero-fact.jm-hero-fact--ready {
  position: relative;
  display: grid;
  grid-template-rows: 1fr auto;
  row-gap: 0.75rem;
  transition: grid-template-rows 0.45s ease, row-gap 0.45s ease, padding 0.45s ease;
}
#brx-content .jm-hero-fact.is-min {
  grid-template-rows: 0fr auto;
  row-gap: 0;
  /* Narrower sides so the one-line link clears the control at the card's
     24.8rem max width (the label runs ~302px at 1440; inner width becomes
     349px, the shortened bar takes 15px). */
  padding: 1.25rem 1.5rem;
}
.jm-hero-fact__body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  row-gap: 0.75rem;
  min-height: 0;
  overflow: hidden;
  transition: opacity 0.3s ease;
}
.is-min > .jm-hero-fact__body { opacity: 0; }
.jm-hero-fact__min {
  position: absolute;
  /* Centred on the eyebrow's line box: padding-top plus half a caption
     line, minus half the 44px target. Right edge of the BAR sits on the
     padding edge (the target overhangs by its own inset). */
  top: calc(2.5rem + 0.5rem - 1.375rem);
  right: calc(2.5rem - 0.4375rem);
  transform: translateY(0);
  display: flex;
  align-items: center;
  justify-content: flex-end;
  width: max(2.75rem, 44px);
  height: max(2.75rem, 44px);
  padding: 0 0.4375rem 0 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  transition: top 0.45s ease, right 0.45s ease, transform 0.45s ease;
}
/* The choreography (Phil, 2026-09-02, revised): the line rides DOWN with
   the collapsing card at its full length — nothing about it changes —
   and only once it has arrived (the 0.45s card transition) does it turn
   clockwise on its right end while a second, identical line appears and
   turns anticlockwise, the two meeting in a `>` whose arms are the whole
   1.875rem bar. The link's own arrow fades out beneath (the chevron is
   now the tab's arrow). */
.jm-hero-fact__min span {
  grid-area: 1 / 1;
  display: block;
  width: 1.875rem;
  height: 1.5px;
  border-radius: 1px;
  background-color: var(--gold-500);
  transform-origin: 100% 50%;
  transform: rotate(0deg);
  transition: transform 0.35s ease, opacity 0.15s ease;
}
.jm-hero-fact__min span:last-child { opacity: 0; }
.jm-hero-fact__min { display: grid; justify-content: end; align-content: center; }
.jm-hero-fact__min:hover span:first-child,
.is-min > .jm-hero-fact__min:hover span { opacity: 0.75; }
.jm-hero-fact__min:focus-visible { outline: 3px solid var(--gold-500); outline-offset: 2px; }
.is-min > .jm-hero-fact__min {
  top: 50%;
  right: calc(1.5rem - 0.4375rem);
  transform: translateY(-50%);
}
.is-min > .jm-hero-fact__min span {
  /* Hold until the card and the line have settled, then turn — and, as
     they turn, shorten to 72% (1.35rem arms; Phil, 2026-09-02: 80%, then
     "an extra 10% shorter"). */
  width: 1.35rem;
  transition: transform 0.35s ease 0.45s, width 0.35s ease 0.45s, opacity 0.15s ease 0.45s;
}
.is-min > .jm-hero-fact__min span:first-child { transform: rotate(45deg); }
.is-min > .jm-hero-fact__min span:last-child { opacity: 1; transform: rotate(-45deg); }
/* The tab's label reads "Trafficking in New Zealand" (fact-min.js swaps
   it), a touch larger and in gold, with no underline at rest or on hover. */
.jm-hero-fact .jm-link-arrow__label { transition: color 0.3s ease, font-size 0.3s ease; }
.is-min .jm-link-arrow,
.is-min .jm-link-arrow:hover,
.is-min .jm-link-arrow__label,
.is-min .jm-link-arrow:hover .jm-link-arrow__label {
  text-decoration: none;
  color: var(--gold-500);
}
.is-min .jm-link-arrow__label { font-size: 1.125em; }
/* The closed tab is one button: pointer everywhere, and a hover that
   darkens the ground (the design system's tinted-hover rule: darker,
   never lighter). */
#brx-content .jm-hero-fact.is-min {
  cursor: pointer;
  transition: grid-template-rows 0.45s ease, row-gap 0.45s ease, padding 0.45s ease, background-color 0.2s ease;
}
#brx-content .jm-hero-fact.is-min:hover { background-color: var(--purple-700); }
.is-min > .jm-hero-fact__min span { transition-property: transform, width, opacity; }
.jm-hero-fact .jm-link-arrow__arrow { transition: opacity 0.3s ease; }
.is-min .jm-link-arrow__arrow { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  #brx-content .jm-hero-fact.jm-hero-fact--ready,
  .jm-hero-fact__body,
  .jm-hero-fact__min,
  .jm-hero-fact__min span { transition: none; }
}


/* ---- The home load choreography (Phil, 2026-08-31) ----
   Gated on the hero image lifecycle classes, which exist ONLY on the home
   page and ONLY with JS running (functions.php): the banner photograph
   fades in first (the ::before fade above), the logo catches its tail, the
   menu drops in item by item, then the headline, its standfirst and the
   CTA rise. No JS or reduced motion: nothing is ever hidden. The 2.5s
   decode timeout in functions.php caps how long anything can wait. */

html.jm-hero-pending .jm-header .jm-logo,
html.jm-hero-pending .jm-header__link,
html.jm-hero-pending .jm-header__inner .jm-button,
html.jm-hero-pending .jm-burger,
html.jm-hero-pending .jm-hero-copy h1,
html.jm-hero-pending .jm-hero-copy h1 + *,
html.jm-hero-pending .jm-hero-copy > :nth-child(2) {
  opacity: 0;
}

html.jm-hero-ready .jm-header .jm-logo {
  animation: jm-seq-fade var(--motion-slow) var(--motion-ease-out) both;
  animation-delay: 0.25s;
}
html.jm-hero-ready .jm-header__link {
  animation: jm-seq-drop var(--motion-base) var(--motion-ease-out) both;
}
html.jm-hero-ready .jm-header__link:nth-child(1) { animation-delay: 0.5s; }
html.jm-hero-ready .jm-header__link:nth-child(2) { animation-delay: 0.58s; }
html.jm-hero-ready .jm-header__link:nth-child(3) { animation-delay: 0.66s; }
html.jm-hero-ready .jm-header__link:nth-child(4) { animation-delay: 0.74s; }
html.jm-hero-ready .jm-header__inner .jm-button {
  animation: jm-seq-drop var(--motion-base) var(--motion-ease-out) both;
  animation-delay: 0.82s;
}
html.jm-hero-ready .jm-burger {
  animation: jm-seq-drop var(--motion-base) var(--motion-ease-out) both;
  animation-delay: 0.5s;
}
html.jm-hero-ready .jm-hero-copy h1,
html.jm-hero-ready .jm-hero-copy h1 + *,
html.jm-hero-ready .jm-hero-copy > :nth-child(2) {
  animation: jm-seq-rise var(--motion-slow) var(--motion-ease-enter) both;
}
html.jm-hero-ready .jm-hero-copy h1 { animation-delay: 0.9s; }
html.jm-hero-ready .jm-hero-copy h1 + * { animation-delay: 1.05s; }
html.jm-hero-ready .jm-hero-copy > :nth-child(2) { animation-delay: 1.2s; }

/* ---- The living lockup (Phil, 2026-09-02) ----
   The header logo animates on a 13-second loop, the same choreography as
   the showcase film's opener: the gold ring draws itself CLOCKWISE along its
   own curve (jm-lockup.svg's stroked path carries pathLength="1", so the
   dash maths is unit-free), the wordmark rises in, then the tagline; the
   lockup then holds for 7 seconds, fades over 3, and the cycle restarts.
   Every part is hidden at 0% so the restart never flashes. On the home page
   the loop waits (paused) until the hero photograph has decoded, so it
   starts with the rest of the load choreography. Reduced motion: none of
   this, the lockup simply shows. Header lockups only; the footer's stays
   still. */
.jm-header .jm-logo__svg {
  animation: jm-lockup-cycle 13s linear infinite;
}
.jm-header .jm-logo__svg path[stroke] {
  animation: jm-lockup-ring 13s linear infinite;
}
.jm-header .jm-logo__svg path[fill="var(--jm-logo-mark)"] {
  animation: jm-lockup-mark 13s linear infinite;
}
.jm-header .jm-logo__svg path[fill="var(--jm-logo-tag)"] {
  animation: jm-lockup-tag 13s linear infinite;
}
html.jm-hero-pending .jm-header .jm-logo__svg,
html.jm-hero-pending .jm-header .jm-logo__svg path {
  animation-play-state: paused;
}
/* 13s = 100%. Draw 0.25s to 1.85s (1.9% to 14.2%); wordmark 1.25s to 2.15s
   (9.6% to 16.5%); tagline 1.7s to 2.6s (13.1% to 20%); hold to 10s
   (76.9%); fade to 13s. */
/* The whole lockup is held invisible for the first 1.5% of the cycle (0.2s,
   before the ring's own draw begins at 1.9%): at the exact restart the four
   animations can land a frame apart, and a fully drawn ring or risen
   wordmark from the end of the old cycle would flash at full strength
   against the reset (Phil, 2026-09-02). */
@keyframes jm-lockup-cycle {
  0%, 1.5% { opacity: 0; }
  3%, 76.9% { opacity: 1; }
  100% { opacity: 0; }
}
@keyframes jm-lockup-ring {
  0%, 1.9% { stroke-dasharray: 1 2; stroke-dashoffset: 1; animation-timing-function: cubic-bezier(0.6, 0.02, 0.25, 1); }
  14.2%, 100% { stroke-dasharray: 1 2; stroke-dashoffset: 0; }
}
@keyframes jm-lockup-mark {
  0%, 9.6% { opacity: 0; transform: translateY(3px); animation-timing-function: cubic-bezier(0.2, 0.7, 0.2, 1); }
  16.5%, 100% { opacity: 1; transform: translateY(0); }
}
@keyframes jm-lockup-tag {
  0%, 13.1% { opacity: 0; transform: translateY(3px); animation-timing-function: cubic-bezier(0.2, 0.7, 0.2, 1); }
  20%, 100% { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .jm-header .jm-logo__svg,
  .jm-header .jm-logo__svg path { animation: none; }
}

@keyframes jm-seq-fade {
  from { opacity: 0; }
}
@keyframes jm-seq-drop {
  from { opacity: 0; transform: translateY(-0.5rem); }
}
@keyframes jm-seq-rise {
  from { opacity: 0; transform: translateY(0.75rem); }
}

@media (prefers-reduced-motion: reduce) {
  html.jm-hero-pending .jm-header .jm-logo,
  html.jm-hero-pending .jm-header__link,
  html.jm-hero-pending .jm-header__inner .jm-button,
  html.jm-hero-pending .jm-burger,
  html.jm-hero-pending .jm-hero-copy h1,
  html.jm-hero-pending .jm-hero-copy h1 + *,
  html.jm-hero-pending .jm-hero-copy > :nth-child(2) { opacity: 1; }
  html.jm-hero-ready .jm-header .jm-logo,
  html.jm-hero-ready .jm-header__link,
  html.jm-hero-ready .jm-header__inner .jm-button,
  html.jm-hero-ready .jm-burger,
  html.jm-hero-ready .jm-hero-copy h1,
  html.jm-hero-ready .jm-hero-copy h1 + *,
  html.jm-hero-ready .jm-hero-copy > :nth-child(2) { animation: none; }
}

/* The held header: while the home hero pin owns the page, the natural
   transparent header stays fixed exactly where it was — the banner is never
   headerless (Phil, 2026-08-31). No repaint, no animation: same look, just
   pinned with the hero. The painted bar slides in over it at release. */
.jm-header.is-held {
  position: fixed;
  top: var(--wp-admin--admin-bar--height, 0px);
}

/* ---- The rotating headline tail (rotate.js; Phil, 2026-08-31) ----
   Gold, because the rotation IS the emphasis — the same gold-span-in-a-
   headline pattern the About and Donate heroes already use. The out swap
   lifts away, the next phrase fades up in; reduced motion never rotates
   (rotate.js guards) so no keyframe needs a guard here. */
.jm-rotate {
  display: inline-block;
  color: var(--gold-500);
}
.jm-rotate.is-out { animation: jm-rotate-out 320ms var(--motion-ease) forwards; }
.jm-rotate.is-in { animation: jm-rotate-in 560ms var(--motion-ease-enter); }
@keyframes jm-rotate-out {
  to { opacity: 0; transform: translateY(-0.35em); }
}
@keyframes jm-rotate-in {
  from { opacity: 0; transform: translateY(0.4em); }
}
