/* =====================================================================
   JMK Studios — shared motion layer (linked on every page)
   Hover/entrance/ambient animation utilities used across components.
   All motion is disabled under prefers-reduced-motion.
   ===================================================================== */
html { scroll-behavior: smooth; }
/* keep anchored sections clear of the fixed nav when scrolled to via a hash link */
section[id] { scroll-margin-top: 88px; }

/* awards / kinetic marquees (used on home + the dedicated Our Awards page) */
@keyframes awardsMarquee { from { transform: translateX(0); } to { transform: translateX(-50%); } }
.awards-marquee:hover .awards-marquee-track { animation-play-state: paused; }

/* --- entrance: rise + fade (runs on load, no observer needed) --- */
@keyframes jmkRise { from { opacity: 0; transform: translateY(42px); } to { opacity: 1; transform: translateY(0); } }
.jmk-rise { opacity: 0; animation: jmkRise 900ms var(--ease-confident, cubic-bezier(.2,.7,.2,1)) both; }

@keyframes jmkFadeIn { from { opacity: 0; } to { opacity: 1; } }
.jmk-fade { opacity: 0; animation: jmkFadeIn 1100ms ease both; }

/* --- ambient loops --- */
@keyframes jmkFloat { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-10px); } }
.jmk-float { animation: jmkFloat 6.5s ease-in-out infinite; }

@keyframes jmkSway { 0%,100% { transform: translateX(0) rotate(0deg); } 50% { transform: translateX(12px) rotate(0.5deg); } }
.jmk-sway { animation: jmkSway 9s ease-in-out infinite; }

/* --- hand-drawn "paint on" reveal (hero "Box." on first load) --- */
@keyframes jmkDrawOn {
  from { -webkit-clip-path: inset(0 100% 0 0); clip-path: inset(0 100% 0 0); }
  to   { -webkit-clip-path: inset(0 0 0 0);    clip-path: inset(0 0 0 0); }
}
.jmk-draw-on { animation: jmkDrawOn 1100ms cubic-bezier(.6,.02,.28,1) both; }

/* --- "Box." being written, letter by letter ---------------------------
   The old version wiped the whole word left to right in one pass, which
   reads as a reveal rather than as drawing — the giveaway is that every
   letter appears at the same rate and finishes together.

   This strokes each letter in turn instead. Three things sell it:

   1. The wipe edge is SLANTED, not vertical. A marker held in a hand
      leaves a diagonal leading edge; a vertical one looks like a curtain.
   2. Each letter is fast (260ms) with a gap between them, so the eye
      reads four separate strokes rather than one long sweep.
   3. Each letter lands — a couple of pixels of drop and a degree of
      rotation settling out — because a hand does not stop dead.

   The nib is the pink dot that travels the word ahead of the ink. It is
   what makes the word look drawn BY something rather than just appearing.
   -------------------------------------------------------------------- */
@keyframes jmkInkOn {
  from {
    -webkit-clip-path: polygon(-30% -20%, -30% -20%, -50% 120%, -50% 120%);
            clip-path: polygon(-30% -20%, -30% -20%, -50% 120%, -50% 120%);
    transform: translateY(3px) rotate(-2deg);
  }
  70% { transform: translateY(0) rotate(0.6deg); }
  to {
    -webkit-clip-path: polygon(-30% -20%, 150% -20%, 130% 120%, -50% 120%);
            clip-path: polygon(-30% -20%, 150% -20%, 130% 120%, -50% 120%);
    transform: translateY(0) rotate(0);
  }
}
.jmk-ink {
  display: inline-block;
  animation: jmkInkOn 260ms cubic-bezier(.55,.06,.4,1) both;
}

/* The nib: a marker tip crossing the word, gone by the time the last
   letter lands. Pointer-events off — it sits over the easter egg. */
@keyframes jmkNib {
  0%   { transform: translate(-4%, 14%) scale(0.7); opacity: 0; }
  12%  { opacity: 1; }
  85%  { opacity: 1; }
  100% { transform: translate(104%, -6%) scale(1); opacity: 0; }
}
.jmk-nib {
  position: absolute; left: 0; top: 38%;
  width: 0.13em; height: 0.13em; border-radius: 50%;
  background: var(--color-primary);
  box-shadow: 0 0 0.14em 0.03em var(--color-primary);
  pointer-events: none;
  animation: jmkNib 980ms cubic-bezier(.5,.02,.42,1) both;
}

/* --- animated success tick (/thankyou) ---

   Six things happen in just under a second and a half, in an order that
   matters: two halo rings ping outward, the disc lands with a squash, the
   check is drawn on by hand, and the whole thing settles with one small
   wobble. Staggered rather than simultaneous, because everything arriving
   at once reads as a page loading, and one thing after another reads as
   something happening. All of it is off under prefers-reduced-motion at
   the foot of this block — the tick is then simply there, which is the
   point of it. */
@keyframes jmkTickPop { 0% { transform: scale(0.5); opacity: 0; } 55% { transform: scale(1.08); opacity: 1; } 100% { transform: scale(1); opacity: 1; } }
@keyframes jmkTickDraw { to { stroke-dashoffset: 0; } }

/* The landing. Overshoot, a squash on the way back — a disc with weight
   rather than a disc that was scaled. */
@keyframes jmkTickDisc {
  0%   { transform: scale(0.2); opacity: 0; }
  45%  { transform: scale(1.14); opacity: 1; }
  62%  { transform: scale(0.94); }
  78%  { transform: scale(1.04); }
  100% { transform: scale(1); opacity: 1; }
}

/* The halo. Starts at the disc's edge and leaves. */
@keyframes jmkTickRing {
  0%   { transform: scale(0.62); opacity: 0; }
  18%  { opacity: 0.55; }
  100% { transform: scale(1.55); opacity: 0; }
}

/* The settle. One wobble, not a loop — a success mark that keeps moving
   is a success mark somebody scrolls away from. */
@keyframes jmkTickSettle {
  0%   { transform: rotate(0deg) scale(1); }
  30%  { transform: rotate(-4.5deg) scale(1.04); }
  60%  { transform: rotate(3deg) scale(0.99); }
  100% { transform: rotate(0deg) scale(1); }
}

.jmk-tick { animation: jmkTickPop 700ms cubic-bezier(.34,1.56,.64,1) both; }

/* The wobble waits for the check to finish drawing, so the mark settles
   once it is whole rather than mid-stroke. The root is a replaced element,
   so its origin is a percentage of the rendered box; the parts inside are
   SVG geometry, whose percentages resolve against the view box. Both land
   on the middle, by two different routes. */
.jmk-tick__settle {
  animation: jmkTickSettle 620ms cubic-bezier(.34,1.3,.5,1) 1080ms both;
  transform-origin: 50% 50%;
}
.jmk-tick__disc,
.jmk-tick__ring { transform-box: view-box; transform-origin: 50% 50%; }
.jmk-tick__disc { animation: jmkTickDisc 760ms cubic-bezier(.34,1.4,.5,1) 60ms both; }
.jmk-tick__ring { animation: jmkTickRing 1100ms cubic-bezier(.2,.7,.3,1) both; }
.jmk-tick__ring--2 { animation-delay: 280ms; }
.jmk-tick__check { animation: jmkTickDraw 520ms cubic-bezier(.65,0,.35,1) 620ms both; }

@media (prefers-reduced-motion: reduce) {
  .jmk-tick,
  .jmk-tick__settle,
  .jmk-tick__disc,
  .jmk-tick__check { animation: none !important; opacity: 1 !important; transform: none !important; stroke-dashoffset: 0 !important; }
  /* A ring whose whole job is to expand and vanish has nothing to show
     when it cannot move, so it does not show at all. */
  .jmk-tick__ring { display: none; }
}

/* --- hover lift for cards --- */
.jmk-lift { transition: transform 440ms var(--ease-confident, cubic-bezier(.2,.7,.2,1)), box-shadow 440ms var(--ease-confident, cubic-bezier(.2,.7,.2,1)); will-change: transform; }
.jmk-lift:hover { transform: translateY(-9px); box-shadow: 0 34px 70px -30px rgba(17,17,17,0.5); }

/* --- image zoom on hover (wrap the clipping container) --- */
.jmk-zoom { overflow: hidden; }
.jmk-zoom img { transition: transform 800ms var(--ease-confident, cubic-bezier(.2,.7,.2,1)); }
.jmk-lift:hover .jmk-zoom img, .jmk-zoom:hover img { transform: scale(1.07); }

/* --- shimmer sweep for placeholder / loading surfaces --- */
.jmk-shimmer::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(115deg, transparent 32%, rgba(255,255,255,0.35) 50%, transparent 68%);
  transform: translateX(-130%);
  animation: jmkShimmer 3s ease-in-out infinite;
}
@keyframes jmkShimmer { to { transform: translateX(130%); } }

/* --- animated underline that grows on hover --- */
.jmk-underline { position: relative; }
.jmk-underline::after {
  content: ""; position: absolute; left: 0; bottom: -4px; height: 2px; width: 100%;
  background: var(--color-primary); transform: scaleX(0); transform-origin: left center;
  transition: transform 380ms var(--ease-confident, cubic-bezier(.2,.7,.2,1));
}
.jmk-underline:hover::after { transform: scaleX(1); }

/* --- blog post typography (rendered Markdown) --- */
.jmk-prose { font-family: var(--font-ui); color: var(--ink-700); font-size: var(--text-lg); line-height: 1.7; }
.jmk-prose > :first-child { margin-top: 0; }
.jmk-prose h2 { font-family: var(--font-editorial); color: var(--ink-900); font-size: clamp(26px,3.4vw,40px); line-height: 1.1; margin: 44px 0 16px; }
.jmk-prose h3 { font-family: var(--font-editorial); color: var(--ink-900); font-size: var(--text-2xl); margin: 32px 0 12px; }
.jmk-prose p { margin: 0 0 20px; }
.jmk-prose a { color: var(--color-secondary); }
.jmk-prose a:hover { color: var(--color-primary); }
.jmk-prose ul, .jmk-prose ol { margin: 0 0 20px; padding-left: 22px; }
.jmk-prose li { margin: 0 0 8px; }
.jmk-prose img { max-width: 100%; height: auto; border-radius: var(--radius-md); margin: 24px 0; }
.jmk-prose blockquote { border-left: 3px solid var(--color-primary); margin: 28px 0; padding: 4px 0 4px 22px; font-family: var(--font-editorial); font-style: italic; font-size: var(--text-xl); color: var(--ink-900); }
.jmk-prose code { background: var(--color-surface-sunken); padding: 2px 6px; border-radius: 4px; font-size: 0.9em; }
.jmk-prose hr { border: none; border-top: 1px solid var(--color-border); margin: 40px 0; }

/* --- animated splash --- */
@keyframes jmkSplashBar { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes jmkSplashUp { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: translateY(0); } }

/* --- full-screen "stage" sections: each fills the viewport, content centred.
   Safe for tall content too — the section simply grows past 100dvh.
   Ambient drifting brand glows behind the content add depth + motion. --- */
.jmk-stage { min-height: 100dvh; display: flex; flex-direction: column; justify-content: center; position: relative; overflow: hidden; }
.jmk-stage::before {
  content: ""; position: absolute; top: -12%; right: -8%;
  width: 44vw; height: 44vw; max-width: 640px; max-height: 640px;
  border-radius: 50%; background: var(--color-primary);
  opacity: 0.1; filter: blur(72px); pointer-events: none; z-index: 0;
  animation: heroDrift1 18s ease-in-out infinite;
}
.jmk-stage::after {
  content: ""; position: absolute; bottom: -12%; left: -8%;
  width: 36vw; height: 36vw; max-width: 540px; max-height: 540px;
  border-radius: 50%; background: var(--color-secondary);
  opacity: 0.1; filter: blur(72px); pointer-events: none; z-index: 0;
  animation: heroDrift2 15s ease-in-out infinite;
}
.jmk-stage > * { position: relative; z-index: 1; }

/* --- sections that hold and are covered ------------------------------
   Paired with the Curtain component in Motion.jsx, which does the
   measuring — see the note above it for why the sticky offset cannot
   live in this file. Everything here is the look; nothing here is the
   mechanism.

   The section arriving carries a rounded top edge and a shadow that
   deepens as it comes, so what pulls over reads as a card rather than a
   colour change. Full width, because the rest of the site is: a gutter
   down both sides would make these three sections a different kind of
   page from the ones under them. --- */
.jmk-curtain { transform-origin: 50% 0%; }
.jmk-curtain--over {
  border-top-left-radius: clamp(20px, 2.6vw, 34px);
  border-top-right-radius: clamp(20px, 2.6vw, 34px);
  box-shadow:
    0 calc(-10px - var(--in, 0) * 14px) calc(30px + var(--in, 0) * 36px) -14px
    rgba(0, 0, 0, 0.42);
}
/* Only while it is actually handing over. A filter of brightness(1) is
   still a filter, and a filter builds a containing block around every
   fixed-position descendant — which on a section holding a cookie
   banner or a floating button is a bug waiting to happen. */
.jmk-curtain--dim { filter: brightness(calc(1 - var(--out, 0) * 0.42)); }

@media (prefers-reduced-motion: reduce) {
  /* Still held, still layered — nothing dims. */
  .jmk-curtain { filter: none !important; }
}
@media print {
  .jmk-curtain {
    position: static !important; filter: none !important;
    box-shadow: none !important; border-radius: 0 !important;
  }
}

/* --- page-to-page dissolve (exit fade only) ---
   #root stays visible by default so a paused/throttled animation can never
   leave a page blank. On internal navigation boot.js adds .jmk-leaving to fade
   the current page out, then loads the next (which shows immediately). */
#root.jmk-leaving { opacity: 0; transition: opacity 320ms var(--ease-confident, ease); }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  #root { animation: none !important; opacity: 1 !important; }
  #root.jmk-leaving { transition: none !important; opacity: 1 !important; }
  .jmk-stage::before, .jmk-stage::after { animation: none !important; }
  .awards-marquee-track { animation: none !important; }
  .jmk-rise, .jmk-fade { opacity: 1 !important; animation: none !important; }
  .jmk-draw-on, .jmk-ink { animation: none !important; -webkit-clip-path: none !important; clip-path: none !important; transform: none !important; }
  .jmk-nib { display: none !important; }
  .jmk-float, .jmk-sway { animation: none !important; }
  .jmk-tick { animation: none !important; opacity: 1 !important; transform: none !important; }
  .jmk-tick circle, .jmk-tick path { animation: none !important; stroke-dashoffset: 0 !important; }
  .jmk-shimmer::after { display: none !important; }
  .jmk-lift:hover { transform: none !important; }
}

/* ======================================================================
   Gallery hover overlay
   Titles sit under a scrim that fades in on hover. On touch there is no
   hover to reveal it, so it is simply always visible there.
   ====================================================================== */
.jmk-gal-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-end;
  padding: 16px;
  text-align: left;
  background: linear-gradient(180deg, rgba(17, 17, 17, 0) 45%, rgba(17, 17, 17, 0.82) 100%);
  opacity: 0;
  transition: opacity 260ms var(--ease-confident);
}

.jmk-gal-overlay__title {
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  line-height: 1.35;
  color: #ffffff;
  transform: translateY(8px);
  transition: transform 260ms var(--ease-confident);
}

button:hover > .jmk-gal-overlay,
button:focus-visible > .jmk-gal-overlay {
  opacity: 1;
}

button:hover > .jmk-gal-overlay .jmk-gal-overlay__title,
button:focus-visible > .jmk-gal-overlay .jmk-gal-overlay__title {
  transform: translateY(0);
}

@media (hover: none) {
  .jmk-gal-overlay {
    opacity: 1;
  }
  .jmk-gal-overlay__title {
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .jmk-gal-overlay,
  .jmk-gal-overlay__title {
    transition: none;
  }
}

/* ======================================================================
   Related-posts marquee
   The track holds two copies of the list and slides by exactly half, so the
   loop is seamless. Duration scales with content via --jmk-marquee-seconds.
   ====================================================================== */
.jmk-marquee {
  position: relative;
  /* Fade the rail into the background at both edges rather than cutting it. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}

.jmk-marquee__track {
  width: max-content;
  animation: jmkMarquee var(--jmk-marquee-seconds, 46s) linear infinite;
}

.jmk-marquee:hover .jmk-marquee__track,
.jmk-marquee:focus-within .jmk-marquee__track {
  animation-play-state: paused;
}

@keyframes jmkMarquee {
  from { transform: translateX(0); }
  /* Half, because the track is the list twice over. */
  to { transform: translateX(-50%); }
}

@media (prefers-reduced-motion: reduce) {
  .jmk-marquee__track {
    animation: none;
  }
  .jmk-marquee {
    overflow-x: auto;
    -webkit-mask-image: none;
    mask-image: none;
  }
}

/* The phone inset on web-design cards would cover the desktop shot it sits
   over once the card itself is phone-width. */
@media (max-width: 640px) {
  .jmk-webdesign-phone {
    display: none !important;
  }
}

/* ======================================================================
   Our Work — editorial tile grid
   ----------------------------------------------------------------------
   The portfolio hub carries more motion than the rest of the site on
   purpose: it is the page people browse rather than read. Everything here
   is transform/opacity only so it stays on the compositor, and all of it
   is switched off under prefers-reduced-motion at the foot of this block.
   ====================================================================== */

/* Media wipes in from the bottom rather than fading — reads as editorial
   rather than as a page still loading. */
@keyframes jmkWipeUp {
  from { clip-path: inset(100% 0 0 0); transform: scale(1.06); }
  to   { clip-path: inset(0 0 0 0);    transform: scale(1); }
}
.jmk-wipe { animation: jmkWipeUp 1000ms cubic-bezier(.62,.05,.2,1) both; }

.jmk-tile {
  position: relative;
  display: block;
  text-decoration: none;
  isolation: isolate;
}

.jmk-tile__media {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-md);
  background: #111111;
  transition: border-radius 620ms var(--ease-confident, cubic-bezier(.2,.7,.2,1));
}
.jmk-tile:hover .jmk-tile__media { border-radius: 4px; }

.jmk-tile__media img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform 1100ms var(--ease-confident, cubic-bezier(.2,.7,.2,1)),
              filter 620ms ease;
  filter: saturate(0.82) contrast(1.02);
}
.jmk-tile:hover .jmk-tile__media img {
  transform: scale(1.09);
  filter: saturate(1.06) contrast(1.04);
}

/* A colour wash that sweeps up from the bottom edge on hover. */
.jmk-tile__wash {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(0deg,
    rgba(17,17,17,0.86) 0%, rgba(17,17,17,0.34) 46%, rgba(17,17,17,0) 78%);
  opacity: 0;
  transform: translateY(28%);
  transition: opacity 520ms ease, transform 620ms var(--ease-confident, cubic-bezier(.2,.7,.2,1));
}
.jmk-tile:hover .jmk-tile__wash { opacity: 1; transform: translateY(0); }

/* The "View" cue, which rises into the wash. */
.jmk-tile__cue {
  position: absolute; left: 20px; bottom: 18px; z-index: 2;
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--font-ui); font-size: 12px; font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase; color: #ffffff;
  opacity: 0; transform: translateY(14px);
  transition: opacity 420ms ease 60ms, transform 520ms var(--ease-confident, cubic-bezier(.2,.7,.2,1)) 60ms;
}
.jmk-tile:hover .jmk-tile__cue { opacity: 1; transform: translateY(0); }
.jmk-tile__cue span { transition: transform 420ms var(--ease-confident, cubic-bezier(.2,.7,.2,1)); }
.jmk-tile:hover .jmk-tile__cue span { transform: translateX(5px); }

/* Index numeral behind the title. */
.jmk-tile__index {
  position: absolute; top: 14px; right: 18px; z-index: 2;
  font-family: var(--font-editorial); font-size: 13px; letter-spacing: 0.08em;
  color: rgba(255,255,255,0.62);
  opacity: 0; transform: translateY(-8px);
  transition: opacity 380ms ease, transform 420ms var(--ease-confident, cubic-bezier(.2,.7,.2,1));
}
.jmk-tile:hover .jmk-tile__index { opacity: 1; transform: translateY(0); }

/* Title slides a touch and the rule under it grows. */
.jmk-tile__title { transition: transform 520ms var(--ease-confident, cubic-bezier(.2,.7,.2,1)); }
.jmk-tile:hover .jmk-tile__title { transform: translateX(6px); }

.jmk-tile__rule {
  height: 2px; width: 100%; margin-top: 12px;
  background: var(--color-primary);
  transform: scaleX(0); transform-origin: left center;
  transition: transform 560ms var(--ease-confident, cubic-bezier(.2,.7,.2,1));
}
.jmk-tile:hover .jmk-tile__rule { transform: scaleX(1); }

/* --- discipline ticker under the hero --- */
@keyframes jmkTicker { from { transform: translateX(0); } to { transform: translateX(-50%); } }
.jmk-ticker { overflow: hidden; position: relative; }
.jmk-ticker__track { display: flex; width: max-content; animation: jmkTicker 42s linear infinite; }
.jmk-ticker:hover .jmk-ticker__track { animation-play-state: paused; }
.jmk-ticker::before, .jmk-ticker::after {
  content: ""; position: absolute; top: 0; bottom: 0; width: 12vw; z-index: 2; pointer-events: none;
}
.jmk-ticker::before { left: 0;  background: linear-gradient(90deg, var(--ink-900), transparent); }
.jmk-ticker::after  { right: 0; background: linear-gradient(270deg, var(--ink-900), transparent); }

/* --- craft / service switch --- */
.jmk-switch { position: relative; display: inline-flex; padding: 4px; border-radius: 999px;
              border: 1px solid var(--color-border); background: transparent; }
.jmk-switch__thumb {
  position: absolute; top: 4px; bottom: 4px; z-index: 0;
  border-radius: 999px; background: var(--ink-900);
  transition: transform 520ms var(--ease-confident, cubic-bezier(.2,.7,.2,1)),
              width 520ms var(--ease-confident, cubic-bezier(.2,.7,.2,1));
}
.jmk-switch button {
  position: relative; z-index: 1; border: 0; background: none; cursor: none;
  font-family: var(--font-ui); font-size: 14px; font-weight: 600;
  padding: 10px 22px; border-radius: 999px; color: var(--ink-700);
  transition: color 380ms ease;
}
.jmk-switch button[aria-pressed="true"] { color: #ffffff; }

/* Crossfade when the grid swaps axis. */
@keyframes jmkSwapIn { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: translateY(0); } }
.jmk-swap { animation: jmkSwapIn 560ms var(--ease-confident, cubic-bezier(.2,.7,.2,1)) both; }

@media (prefers-reduced-motion: reduce) {
  .jmk-wipe, .jmk-swap { animation: none !important; clip-path: none !important;
                         opacity: 1 !important; transform: none !important; }
  .jmk-ticker__track { animation: none !important; }
  .jmk-tile__media img, .jmk-tile__title, .jmk-tile__cue, .jmk-tile__index,
  .jmk-tile__wash, .jmk-tile__rule, .jmk-tile__media, .jmk-switch__thumb {
    transition: none !important; transform: none !important;
  }
  .jmk-tile:hover .jmk-tile__media img { transform: none !important; }
  .jmk-tile__wash, .jmk-tile__cue, .jmk-tile__index { opacity: 1 !important; }
  .jmk-tile__rule { transform: scaleX(1) !important; }
}

/* ======================================================================
   Client logo marquee
   ----------------------------------------------------------------------
   Replaces a 7.8MB animated GIF of the whole strip with individual square
   logos scrolled in CSS. Greyscale at rest so a wall of competing brand
   colours does not fight the page, full colour on hover.
   ====================================================================== */
@keyframes jmkLogoScroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }

.jmk-logos { overflow: hidden; position: relative; margin: 0 auto; max-width: 1100px; }
.jmk-logos__track { display: flex; width: max-content; animation: jmkLogoScroll 38s linear infinite; }
.jmk-logos:hover .jmk-logos__track { animation-play-state: paused; }
.jmk-logos__run { display: flex; flex-shrink: 0; }

.jmk-logos__item {
  width: clamp(88px, 11vw, 132px);
  aspect-ratio: 1 / 1;                 /* square, enforced on upload too */
  margin: 0 clamp(12px, 2vw, 26px);
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.jmk-logos__item img,
.jmk-logos__item a {
  display: block; width: 100%; height: 100%;
}
.jmk-logos__item img {
  object-fit: contain;
  filter: grayscale(1);
  opacity: 0.62;
  transition: filter 420ms ease, opacity 420ms ease, transform 420ms var(--ease-confident, cubic-bezier(.2,.7,.2,1));
}
.jmk-logos__item:hover img { filter: grayscale(0); opacity: 1; transform: scale(1.06); }

/* Fade the ends so logos enter and leave rather than snapping. */
.jmk-logos::before, .jmk-logos::after {
  content: ""; position: absolute; top: 0; bottom: 0; width: 10%; z-index: 2; pointer-events: none;
}
.jmk-logos::before { left: 0;  background: linear-gradient(90deg, var(--color-surface-sunken), transparent); }
.jmk-logos::after  { right: 0; background: linear-gradient(270deg, var(--color-surface-sunken), transparent); }

@media (prefers-reduced-motion: reduce) {
  .jmk-logos__track { animation: none !important; flex-wrap: wrap; justify-content: center; width: 100%; }
  .jmk-logos__run:nth-child(2) { display: none; }   /* one run is enough when static */
  .jmk-logos__item img { filter: grayscale(0); opacity: 1; transition: none !important; }
}

/* ======================================================================
   Editorial figures — hover and ambient motion
   ----------------------------------------------------------------------
   Wrapper > inner > img. The scale lives on the inner element and the
   parallax offset stays inline on the img, so the two never fight over the
   same transform property — which is what happens if you put a hover scale
   on an image a scroll handler is already translating.

   Ambient drift is deliberately tiny (6px over 14s) and stops on hover.
   It should read as the page breathing, not as something moving.
   ====================================================================== */
.jmk-figure {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-md);
  isolation: isolate;
}

.jmk-figure__inner {
  position: absolute;
  inset: 0;
  transform: scale(1);
  transition: transform 1100ms var(--ease-confident, cubic-bezier(.2,.7,.2,1));
  will-change: transform;
  animation: jmkFigureDrift 14s ease-in-out infinite;
}
.jmk-figure:hover .jmk-figure__inner {
  transform: scale(1.055);
  animation-play-state: paused;
}

@keyframes jmkFigureDrift {
  0%, 100% { transform: scale(1) translateY(0); }
  50%      { transform: scale(1.015) translateY(-6px); }
}

.jmk-figure img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  transition: filter 800ms ease;
  filter: saturate(0.94);
}
.jmk-figure:hover img { filter: saturate(1.06) contrast(1.03); }

/* A light sweep that crosses once on hover — the "it's alive" cue, kept
   faint enough that it reads as light rather than as an effect. */
.jmk-figure::after {
  content: "";
  position: absolute; inset: 0; z-index: 2; pointer-events: none;
  background: linear-gradient(115deg, transparent 38%, rgba(255,255,255,0.16) 50%, transparent 62%);
  transform: translateX(-120%);
  transition: transform 1100ms var(--ease-confident, cubic-bezier(.2,.7,.2,1));
}
.jmk-figure:hover::after { transform: translateX(120%); }

/* An optional caption that rises out of the bottom edge. */
.jmk-figure__cap {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 3;
  padding: 40px 20px 18px;
  background: linear-gradient(0deg, rgba(17,17,17,0.82), transparent);
  color: #fff;
  font-family: var(--font-ui); font-size: 13px; letter-spacing: 0.02em;
  opacity: 0; transform: translateY(12px);
  transition: opacity 420ms ease, transform 560ms var(--ease-confident, cubic-bezier(.2,.7,.2,1));
}
.jmk-figure:hover .jmk-figure__cap { opacity: 1; transform: translateY(0); }

/* ---------------------------------------------------------------------
   A figure that keeps its own shape
   ---------------------------------------------------------------------
   The default .jmk-figure is a fixed-aspect box with an absolutely
   positioned inner, which is what lets it crop to a grid. In a masonry
   column there is no grid to crop to and the crop is the thing being
   avoided — so this variant lets the image set the height and the inner
   sit in normal flow. Everything that makes the figure a figure survives:
   the slow drift, the zoom on hover, the sheen sweep and the caption. */
.jmk-figure--flow { height: auto; }
.jmk-figure--flow .jmk-figure__inner { position: static; inset: auto; }
.jmk-figure--flow img { height: auto; }

/* ---------------------------------------------------------------------
   Masonry
   ---------------------------------------------------------------------
   CSS columns rather than a grid, because the whole point is that the
   frames are NOT the same height. break-inside is what stops a column
   splitting a picture across its foot and the next column's head.
   --------------------------------------------------------------------- */
.jmk-masonry {
  columns: 2;
  column-gap: 12px;
}
.jmk-masonry__cell {
  display: block;
  width: 100%;
  margin: 0 0 12px;
  /* All three, because the property is still prefixed in places and a
     picture cut in half is not a cosmetic failure. */
  break-inside: avoid;
  -webkit-column-break-inside: avoid;
  page-break-inside: avoid;
}
.jmk-masonry__video {
  display: flex; align-items: center; justify-content: center;
  aspect-ratio: 4 / 3;
  background: var(--ink-900); color: rgba(255,255,255,0.7);
  font-family: var(--font-ui); font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wide); text-transform: uppercase;
}
@media (max-width: 640px) {
  .jmk-masonry { columns: 1; }
}

/* The craft wall on /our-work/creative. Three columns of masonry rather
   than the two the work strips use — there are eight tiles here, not
   five, and at two columns the wall runs a screen and a half deep. */
.jmk-craft__wall { columns: 3; column-gap: clamp(16px, 1.8vw, 24px); }
.jmk-craft__wall .jmk-masonry__cell { margin-bottom: clamp(16px, 1.8vw, 24px); }
@media (max-width: 1024px) { .jmk-craft__wall { columns: 2; } }
@media (max-width: 640px)  { .jmk-craft__wall { columns: 1; } }

@media (prefers-reduced-motion: reduce) {
  .jmk-figure__inner { animation: none !important; transform: none !important; transition: none !important; }
  .jmk-figure:hover .jmk-figure__inner { transform: none !important; }
  .jmk-figure::after { display: none !important; }
  .jmk-figure img { filter: none !important; transition: none !important; }
  .jmk-figure__cap { opacity: 1; transform: none !important; transition: none !important; }
}

/* =====================================================================
   Craft showcase — the nine ways into Our Work, from a service page.
   ---------------------------------------------------------------------
   Every moving part is transform or opacity, so none of it touches
   layout, and all of it is switched off under prefers-reduced-motion at
   the bottom of this block. The image scales inside a clipped frame
   rather than the tile growing, so neighbouring tiles never shift.
   ===================================================================== */
.jmk-craft__tile {
  display: block;
  text-decoration: none;
  color: inherit;
  outline-offset: 4px;
}

.jmk-craft__media {
  position: relative;
  overflow: hidden;
  border-radius: 4px;
  background: #14110f;
  transform: translateZ(0);
  transition: transform 620ms cubic-bezier(0.16, 1, 0.3, 1);
}

.jmk-craft__media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(1.02);
  transition: transform 900ms cubic-bezier(0.16, 1, 0.3, 1),
              filter 620ms ease;
  filter: saturate(0.82) contrast(1.02);
}

.jmk-craft__blank {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(120% 80% at 30% 0%, rgba(255, 255, 255, 0.07), transparent 60%),
    #14110f;
}

/* Sits under the type so a light photo never eats the name. Deepens on
   hover in step with the image scaling up behind it. */
.jmk-craft__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to top,
    /* Deeper and taller than it was. It only had a title to carry
       before; it now has to hold three lines of description legible over
       whatever the cover happens to be, including a pale one. */
    rgba(0, 0, 0, 0.90) 0%,
    rgba(0, 0, 0, 0.72) 26%,
    rgba(0, 0, 0, 0.34) 52%,
    rgba(0, 0, 0, 0.06) 78%,
    rgba(0, 0, 0, 0.22) 100%
  );
  transition: opacity 620ms ease;
}


.jmk-craft__body {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: clamp(18px, 2.2vw, 30px);
}

.jmk-craft__name {
  font-family: var(--font-editorial);
  font-size: clamp(24px, 2.6vw, 36px);
  line-height: 1.04;
  color: #ffffff;
}

/* Held back until hover on a pointer device — the name and the count are
   what you scan, the description is what you read once you have stopped. */
.jmk-craft__blurb {
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
  color: rgba(255, 255, 255, 0.82);
  max-width: 34ch;
  /* Three lines. The descriptions are a sentence each and a tile is not
     a paragraph — a fourth line starts eating the picture it is written
     about. Anything longer is a sign the sentence wants cutting. */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.jmk-craft__meta {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--color-primary);
}

.jmk-craft__arrow {
  display: inline-block;
  transition: transform 480ms cubic-bezier(0.16, 1, 0.3, 1);
}

@media (hover: hover) and (pointer: fine) {
  /* THE BLURB IS NOT A HOVER REVEAL ANY MORE. It used to slide open on
     hover, which was right while it held a dot-joined list of what the
     craft contains — a bonus, and no loss if you never found it. It now
     holds the description of the discipline, which is the thing the card
     is for and cannot be hidden behind a gesture that does not exist on
     a phone and is not obvious on a desktop. It is simply there. */

  .jmk-craft__tile:hover .jmk-craft__media,
  .jmk-craft__tile:focus-visible .jmk-craft__media {
    transform: translateY(-6px);
  }

  .jmk-craft__tile:hover .jmk-craft__media img,
  .jmk-craft__tile:focus-visible .jmk-craft__media img {
    transform: scale(1.09);
    filter: saturate(1) contrast(1.04);
  }

  .jmk-craft__tile:hover .jmk-craft__scrim,
  .jmk-craft__tile:focus-visible .jmk-craft__scrim {
    opacity: 1.15;
  }


  .jmk-craft__tile:hover .jmk-craft__arrow,
  .jmk-craft__tile:focus-visible .jmk-craft__arrow {
    transform: translateX(6px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .jmk-craft__media,
  .jmk-craft__media img,
  .jmk-craft__scrim,
  .jmk-craft__blurb,
  .jmk-craft__arrow {
    transition: none !important;
  }
  .jmk-craft__tile:hover .jmk-craft__media,
  .jmk-craft__tile:focus-visible .jmk-craft__media {
    transform: none;
  }
  .jmk-craft__tile:hover .jmk-craft__media img,
  .jmk-craft__tile:focus-visible .jmk-craft__media img {
    transform: scale(1.02);
  }
  /* Nothing is hidden behind a hover when motion is off. */
  .jmk-craft__blurb {
    max-height: none;
    opacity: 1;
    transform: none;
  }
}


/* =====================================================================
   The house grammar
   ---------------------------------------------------------------------
   The craft pages were rebuilt around three rules, and this is what makes
   the rest of the site obey the same ones rather than being eight good
   pages attached to twenty-two ordinary ones.

     1. ONE THING PER SCREEN. A section owns the viewport. At anything less
        the next section peeks in at the bottom and the eye leaves before
        it has finished with the one it is on.
     2. THE CONTENT SITS IN THE MIDDLE OF IT. A full-height section with
        everything jammed to the top is a short section with a hole under
        it.
     3. IT ARRIVES. Handled per component by Reveal, which every section
        already uses.

   svh, not vh. On a phone vh is the height of the window with the address
   bar retracted — which it is not, on arrival — so 100vh is reliably
   taller than what can be seen and the bottom of every section is cut off.

   `:where()` keeps the specificity at zero, so any component that needs to
   disagree can, with a plain inline style and no !important arms race.

   OPT OUT with .jmk-flow. Some sections genuinely should not be a screen:
   a running marquee, a divider, a band that is two lines of type by
   design. Those say so rather than being fought with overrides.
   ===================================================================== */
:where(main > section, main > div > section, .jmk-screen) {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* Deliberately not centred and not full height. */
:where(.jmk-flow) {
  min-height: 0;
  display: block;
}

/* A section taller than the screen — a gallery of a hundred and forty
   photographs, a page of terms — stops centring and runs from the top,
   because centring content that overflows pushes its first line off the
   top of the viewport where nobody will look for it. */
@supports (height: 100svh) {
  :where(main > section, main > div > section, .jmk-screen) {
    justify-content: safe center;
  }
}

/* =====================================================================
   Ken Burns, for the slideshow
   ---------------------------------------------------------------------
   A still photograph held perfectly still for six seconds reads as a
   page that has stopped responding rather than as a photograph being
   shown. A slow push across the frame is the difference between a
   slideshow and a stack of file previews.

   Deliberately small: 6% over the whole dwell, and the drift is a
   fraction of that. Enough that the eye registers the frame is alive,
   not enough to notice it moving — anything more and the movement
   becomes the subject instead of the picture.

   transform only, so it composites and never repaints. The alternate
   direction means each slide pushes in and the next one pulls back, so
   a run of them does not feel like a conveyor belt.
   ===================================================================== */
@keyframes jmkKen {
  from { transform: scale(1) translate3d(0, 0, 0); }
  to   { transform: scale(1.06) translate3d(-1.2%, -0.8%, 0); }
}
.jmk-ken {
  animation: jmkKen 7s ease-in-out alternate infinite;
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  .jmk-ken { animation: none; }
}

/* =====================================================================
   Gallery grid — layout and hover, chosen in the dashboard
   ---------------------------------------------------------------------
   Settings -> Galleries picks a layout and a hover style; Shared.jsx puts
   the matching classes on the grid container.

   ALL OF IT IS CSS. Dimming twenty tiles because the cursor moved must
   not re-render twenty React components on a page carrying four hundred
   photographs, and a sibling selector does it for nothing.

   EVERY HOVER RULE SITS INSIDE @media (hover: hover). On a touch screen
   there is no pointer to leave, so a :hover rule latches on tap and the
   tile stays dimmed until something else is tapped.
   ===================================================================== */

.jmk-gal-grid { display: grid; }

/* --- masonry ---------------------------------------------------------
   Columns rather than a grid, filled shortest-first from the real
   proportions of each photograph — which build.js reads out of the file
   header and stores on the item, so the packing is decided before a
   single image has loaded. Nothing reflows as they arrive, which on a
   long archive is the difference between scrolling and losing your place.

   The measuring alternative is what most masonry libraries do and it is
   precisely the behaviour worth avoiding.
   --------------------------------------------------------------------- */
.jmk-gal-grid--masonry { display: flex; align-items: flex-start; }
.jmk-gal-col { display: flex; flex-direction: column; flex: 1 1 0; min-width: 0; }

/* --- editorial -------------------------------------------------------
   A uniform grid with a rhythm broken into it: every fifth tile takes two
   columns and a wider crop, so a page reads like a spread rather than a
   contact sheet. No packing algorithm — just a bigger cell.
   --------------------------------------------------------------------- */
.jmk-gal-grid--editorial .jmk-gal-wide { grid-column: span 2; }

/* --- hover -----------------------------------------------------------
   The tile frame never changes size. Only the image inside it moves, so
   neighbours cannot be nudged and no layout is recalculated mid-scroll:
   transform, opacity and filter only.
   --------------------------------------------------------------------- */
.jmk-gal-tile img { transition: transform 620ms cubic-bezier(.2,.7,.2,1), filter 400ms ease; }
.jmk-gal-tile { transition: opacity 400ms ease, filter 400ms ease, transform 440ms cubic-bezier(.2,.7,.2,1); }

@media (hover: hover) {
  /* zoom — the image breathes inside a fixed frame. */
  .jmk-gal-grid--zoom .jmk-gal-tile:hover img,
  .jmk-gal-grid--zoom .jmk-gal-tile:focus-within img { transform: scale(1.06); }

  /* focus — the one you are looking at stays, the rest step back. */
  .jmk-gal-grid--focus:hover .jmk-gal-tile:not(:hover) { opacity: 0.45; }
  .jmk-gal-grid--focus .jmk-gal-tile:hover img,
  .jmk-gal-grid--focus .jmk-gal-tile:focus-within img { transform: scale(1.05); }

  /* spotlight — the same idea pushed: the rest go soft as well as dim. */
  .jmk-gal-grid--spotlight:hover .jmk-gal-tile:not(:hover) { opacity: 0.3; filter: blur(1.5px) saturate(0.5); }
  .jmk-gal-grid--spotlight .jmk-gal-tile:hover { transform: translateY(-8px); z-index: 2; }
  .jmk-gal-grid--spotlight .jmk-gal-tile:hover img,
  .jmk-gal-grid--spotlight .jmk-gal-tile:focus-within img { transform: scale(1.08); }

  /* mono — a contact sheet until you point at one. */
  .jmk-gal-grid--mono .jmk-gal-tile img { filter: grayscale(1) contrast(1.04); }
  .jmk-gal-grid--mono .jmk-gal-tile:hover img,
  .jmk-gal-grid--mono .jmk-gal-tile:focus-within img { filter: grayscale(0); transform: scale(1.04); }

  /* Keyboard gets what the mouse gets, or tabbing a gallery shows nothing. */
  .jmk-gal-grid--focus:focus-within .jmk-gal-tile:not(:focus-within),
  .jmk-gal-grid--spotlight:focus-within .jmk-gal-tile:not(:focus-within) { opacity: 0.45; }
}

@media (prefers-reduced-motion: reduce) {
  .jmk-gal-tile, .jmk-gal-tile img { transition: none !important; }
  .jmk-gal-grid .jmk-gal-tile:hover img,
  .jmk-gal-grid .jmk-gal-tile:focus-within img { transform: none !important; }
  .jmk-gal-grid--spotlight .jmk-gal-tile:hover { transform: none !important; }
  /* Dimming stays: it is not motion, and it is the part that says which
     tile is live. The blur goes — decoration, and it costs a repaint. */
  .jmk-gal-grid--spotlight:hover .jmk-gal-tile:not(:hover) { filter: none; }
}


/* =====================================================================
   Page layout, ordered from blocks.js
   ---------------------------------------------------------------------
   A flex column only on pages that have a layout, so an ordinary page is
   untouched. Sections keep their own heights and full width — the flex
   is doing one job, which is making `order` mean something.
   ===================================================================== */
main[data-blocks="on"] { display: flex; flex-direction: column; }
main[data-blocks="on"] > * { flex: 0 0 auto; }

/* --- 9:16, the shape it was shot in ----------------------------------
   Vertical video is made for a feed, and a feed scrolls sideways in the
   hand. A static grid shows it in the one shape it was never made for,
   so it runs — and stops under the pointer, because a thing that moves
   while you are trying to look at it is a thing you give up on. */
.jmk-reels .jmk-marquee__track { display: flex; align-items: stretch; gap: 14px; }
.jmk-reels .jmk-reel {
  transition: transform 420ms var(--jmk-ease, cubic-bezier(.22,1,.36,1)),
              box-shadow 420ms var(--jmk-ease, cubic-bezier(.22,1,.36,1));
}
@media (hover: hover) {
  .jmk-reels[data-hover="pause"]:hover .jmk-marquee__track,
  .jmk-reels[data-hover="pause"]:focus-within .jmk-marquee__track {
    animation-play-state: paused;
  }
  .jmk-reels .jmk-reel:hover {
    transform: translateY(-6px) scale(1.015);
    box-shadow: 0 26px 50px -24px rgba(0,0,0,.75);
  }
}
.jmk-reels .jmk-reel:focus-visible {
  outline: 2px solid var(--pink-500, #FF62B1); outline-offset: 3px;
}
/* A row too short to loop scrolls by hand instead. */
.jmk-reels > div:not(.jmk-marquee) { scrollbar-width: none; }
.jmk-reels > div:not(.jmk-marquee)::-webkit-scrollbar { display: none; }
.jmk-reels > div:not(.jmk-marquee) > .jmk-reel { scroll-snap-align: start; }

@media (prefers-reduced-motion: reduce) {
  .jmk-reels .jmk-reel { transition: none; }
  .jmk-reels .jmk-reel:hover { transform: none; }
}

/* --- kinetic marquee: one service per line ---------------------------
   Full height rather than a band, because the rows are the section: there
   is nothing else in it to give it a size. Each row clips its own strip,
   so a row travelling left and the row under it travelling right do not
   need to agree about anything. */
.jmk-kin {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: clamp(2px, 0.6vh, 10px);
  overflow: hidden;
  padding: clamp(24px, 4vh, 56px) 0;
}
.jmk-kin__row { overflow: hidden; display: flex; align-items: center; }
.jmk-kin__track {
  display: flex;
  align-items: center;
  width: max-content;
  animation-name: jmkKin;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  will-change: transform;
}
/* The strip is the run twice over, so half of it is one full cycle. */
@keyframes jmkKin { from { transform: translate3d(0,0,0); } to { transform: translate3d(-50%,0,0); } }

@media (prefers-reduced-motion: reduce) {
  /* Still says all five things — it just says them still. */
  .jmk-kin__track { animation: none; }
}

/* --- tile chrome: where it came from, and how to pass it on -----------
   Sits over the picture, appears on hover. On a touch screen there is no
   hover to wait for, so it is simply always there — a control you cannot
   reveal is a control you do not have. */
.jmk-gal-tile { position: relative; }
.jmk-gal-meta {
  position: absolute;
  left: 10px; right: 10px; bottom: 10px;
  display: flex; align-items: flex-end; justify-content: space-between; gap: 8px;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 260ms ease, transform 260ms cubic-bezier(.2,.7,.2,1);
  pointer-events: none;
}
.jmk-gal-tile:hover .jmk-gal-meta,
.jmk-gal-tile:focus-within .jmk-gal-meta { opacity: 1; transform: translateY(0); }
/* The two controls take pointer events back; the bar between them does
   not, or it would swallow clicks meant for the picture underneath. */
.jmk-gal-chip, .jmk-gal-shareBtn, .jmk-gal-pop { pointer-events: auto; }

.jmk-gal-chip {
  display: inline-flex; align-items: center;
  max-width: 70%;
  padding: 6px 12px; border-radius: var(--radius-pill, 999px);
  background: rgba(17,17,17,.78);
  backdrop-filter: blur(6px);
  font-family: var(--font-ui); font-size: var(--text-2xs, 11px);
  font-weight: 600; letter-spacing: var(--tracking-wide, .04em);
  color: #fff; text-decoration: none; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
  transition: background 200ms ease;
}
.jmk-gal-chip:hover { background: var(--color-primary, #ff62b1); color: #111; }

/* The client chip. Outlined rather than filled, so at a glance the two
   chips on a tile are obviously different kinds of thing — one is a place
   you can go, the other is a fact about the work. It is not a link
   because there is nowhere honest to send somebody: a client is not a
   page on this site. */
.jmk-gal-chip--client {
  background: rgba(17,17,17,.34);
  border: 1px solid rgba(255,255,255,.42);
  cursor: default;
}
.jmk-gal-chip--client:hover { background: rgba(17,17,17,.34); color: #fff; }

.jmk-gal-shareBtn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; flex: 0 0 auto;
  border: 0; border-radius: 50%;
  background: rgba(17,17,17,.78);
  backdrop-filter: blur(6px);
  color: #fff; cursor: pointer;
  transition: background 200ms ease, color 200ms ease;
}
.jmk-gal-shareBtn:hover { background: var(--color-primary, #ff62b1); color: #111; }

/* Opens upward: a tile near the foot of a long gallery has nothing below
   it to open into. */
.jmk-gal-pop {
  position: absolute; right: 0; bottom: calc(100% + 8px);
  z-index: 5; width: max-content; max-width: min(320px, 78vw);
  padding: 12px 14px; border-radius: var(--radius-md, 12px);
  background: #fff; border: 1px solid var(--color-border, #e4e4e4);
  box-shadow: 0 24px 50px -22px rgba(17,17,17,.5);
  animation: jmkPopIn 200ms cubic-bezier(.2,.7,.2,1) both;
}
@keyframes jmkPopIn { from { opacity: 0; transform: translateY(6px) scale(.98); } }

/* The home page's cards carry their badge and title UNDER the picture, so
   a bar pinned to the bottom of the tile would land on the caption rather
   than on the photograph. There, it rides the top edge instead — which is
   over the picture on a card whose picture starts at the top. */
.jmk-gal-tile--card .jmk-gal-meta { top: 10px; bottom: auto; align-items: flex-start; }
/* And opens downward, having nothing above it to open into. */
.jmk-gal-tile--card .jmk-gal-pop { bottom: auto; top: calc(100% + 8px); }
.jmk-gal-tile--card .jmk-gal-meta { transform: translateY(-6px); }
.jmk-gal-tile--card:hover .jmk-gal-meta,
.jmk-gal-tile--card:focus-within .jmk-gal-meta { transform: translateY(0); }

@media (hover: none) {
  .jmk-gal-meta { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .jmk-gal-meta { transition: none; }
  .jmk-gal-pop { animation: none; }
}

/* --- a set shows itself on hover -------------------------------------
   A shoot of sixty photographs is one tile, and the tile shows one frame.
   Hovering cycles the rest of the set underneath it: each frame fades up
   over the one before while drifting and growing very slightly, so the
   tile reads as a shoot rather than as a picture with a number on it.
   Stacked in one frame rather than swapped in place, so a cross-fade has
   two things to fade between. */
.jmk-gal-cycle { position: absolute; inset: 0; }
.jmk-gal-cycle img {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover; display: block;
  opacity: 0;
  transform: scale(1.04);
  transition: opacity 620ms ease, transform 4200ms cubic-bezier(.3,.5,.3,1);
}
.jmk-gal-cycle img[data-on="true"] {
  opacity: 1;
  transform: scale(1.11) translate3d(0, -1.2%, 0);
}
@media (prefers-reduced-motion: reduce) {
  /* The set is still a set; it just does not perform. The count on the
     tile already says how much of it there is. */
  .jmk-gal-cycle { display: none; }
}

/* --- drawn illustrations and looping glyphs ---------------------------
   Everything below animates transform, opacity or stroke-dashoffset and
   nothing else. The whole block stops at the foot of it for anybody who
   asked motion to stop — which is the reason these are SVG and CSS and
   not the GIFs they look like. */

.jmk-illo { display: block; width: 100%; height: 100%; }

/* Web design: the page deals itself out, then the pointer arrives. */
.jmk-illo__b { animation: jmkIlloDeal 5.6s cubic-bezier(.2,.7,.2,1) infinite; transform-box: fill-box; transform-origin: 50% 50%; }
.jmk-illo__b--1 { animation-delay: .00s; }
.jmk-illo__b--2 { animation-delay: .18s; }
.jmk-illo__b--3 { animation-delay: .30s; }
.jmk-illo__b--4 { animation-delay: .42s; }
.jmk-illo__b--5 { animation-delay: .56s; }
.jmk-illo__b--6 { animation-delay: .64s; }
.jmk-illo__b--7 { animation-delay: .78s; }
@keyframes jmkIlloDeal {
  0%   { opacity: 0; transform: translateY(10px) scale(.96); }
  9%   { opacity: 1; transform: translateY(0) scale(1); }
  82%  { opacity: 1; transform: translateY(0) scale(1); }
  95%, 100% { opacity: 0; transform: translateY(-6px) scale(.98); }
}
.jmk-illo__ptr { animation: jmkIlloPoint 5.6s cubic-bezier(.4,0,.2,1) infinite; }
@keyframes jmkIlloPoint {
  0%, 14%  { transform: translate(300px, 430px); opacity: 0; }
  22%      { opacity: 1; }
  46%      { transform: translate(282px, 352px); opacity: 1; }
  /* the click */
  52%      { transform: translate(282px, 356px) scale(.88); }
  58%, 80% { transform: translate(282px, 352px) scale(1); opacity: 1; }
  92%, 100%{ transform: translate(300px, 430px); opacity: 0; }
}

/* Graphic design: shapes turning, the curve drawing itself. */
.jmk-illo__spin { animation: jmkIlloTurn 18s linear infinite; transform-box: fill-box; transform-origin: 50% 50%; }
.jmk-illo__spin--slow { animation-duration: 26s; animation-direction: reverse; }
@keyframes jmkIlloTurn { to { transform: rotate(360deg); } }
.jmk-illo__tri { animation: jmkIlloBob 6s ease-in-out infinite; transform-box: fill-box; transform-origin: 50% 50%; }
@keyframes jmkIlloBob { 0%,100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-12px) rotate(8deg); } }
.jmk-illo__curve { stroke-dasharray: 420; stroke-dashoffset: 420; animation: jmkIlloTrace 7s ease-in-out infinite; }
@keyframes jmkIlloTrace {
  0%   { stroke-dashoffset: 420; }
  38%  { stroke-dashoffset: 0; }
  78%  { stroke-dashoffset: 0; }
  100% { stroke-dashoffset: -420; }
}
.jmk-illo__anchors { animation: jmkIlloFade 7s ease-in-out infinite; }
@keyframes jmkIlloFade { 0%, 22% { opacity: 0; } 42%, 74% { opacity: 1; } 100% { opacity: 0; } }
.jmk-illo__grid { animation: jmkIlloBreathe 9s ease-in-out infinite; }
@keyframes jmkIlloBreathe { 0%,100% { opacity: .5; } 50% { opacity: 1; } }

/* Strategy & Consulting: ways not taken, drifting; the route taken,
   drawn; something going down it. */
.jmk-illo__ghost { stroke-dasharray: 5 11; animation: jmkIlloDrift 11s linear infinite; opacity: .3; }
@keyframes jmkIlloDrift { to { stroke-dashoffset: -160; } }
.jmk-illo__route { stroke-dasharray: 560; stroke-dashoffset: 560; animation: jmkIlloRoute 8.4s ease-in-out infinite; }
@keyframes jmkIlloRoute {
  0%, 5%    { stroke-dashoffset: 560; }
  46%, 88%  { stroke-dashoffset: 0; }
  100%      { stroke-dashoffset: -560; }
}
/* Waypoints off the same curve the route is drawn from, so the marker
   stays on the line instead of cutting the corners of it. */
.jmk-illo__runner { animation: jmkIlloRun 8.4s cubic-bezier(.4,0,.2,1) infinite; }
@keyframes jmkIlloRun {
  0%, 5%    { transform: translate(70px, 410px);  opacity: 0; }
  11%       { transform: translate(104px, 386px); opacity: 1; }
  20%       { transform: translate(150px, 330px); opacity: 1; }
  29%       { transform: translate(205px, 278px); opacity: 1; }
  38%       { transform: translate(270px, 215px); opacity: 1; }
  46%, 88%  { transform: translate(330px, 120px); opacity: 1; }
  100%      { transform: translate(330px, 120px); opacity: 0; }
}
.jmk-illo__flag { transform-box: fill-box; transform-origin: 50% 50%; animation: jmkIlloArrive 8.4s ease-in-out infinite; }
@keyframes jmkIlloArrive {
  0%, 40%   { transform: scale(.62); opacity: .28; }
  50%       { transform: scale(1.14); opacity: 1; }
  58%, 88%  { transform: scale(1); opacity: 1; }
  100%      { transform: scale(.62); opacity: .28; }
}

/* Paid Media: rings out, bars up, cost down. */
.jmk-illo__ring { transform-box: fill-box; transform-origin: 50% 50%; animation: jmkIlloRing 4.6s ease-out infinite; }
.jmk-illo__ring--2 { animation-delay: 1.15s; }
.jmk-illo__ring--3 { animation-delay: 2.30s; }
.jmk-illo__ring--4 { animation-delay: 3.45s; }
@keyframes jmkIlloRing {
  0%   { transform: scale(.24); opacity: 0; }
  14%  { opacity: .95; }
  100% { transform: scale(1); opacity: 0; }
}
.jmk-illo__bar { transform-box: fill-box; transform-origin: 50% 100%; animation: jmkIlloBar 6.2s cubic-bezier(.2,.7,.2,1) infinite; }
.jmk-illo__bar--1 { animation-delay: .00s; }
.jmk-illo__bar--2 { animation-delay: .14s; }
.jmk-illo__bar--3 { animation-delay: .28s; }
.jmk-illo__bar--4 { animation-delay: .42s; }
@keyframes jmkIlloBar {
  0%, 6%    { transform: scaleY(.05); }
  36%, 86%  { transform: scaleY(1); }
  100%      { transform: scaleY(.05); }
}
.jmk-illo__cost { stroke-dasharray: 280; stroke-dashoffset: 280; animation: jmkIlloCost 6.2s ease-in-out infinite; }
@keyframes jmkIlloCost {
  0%, 22%   { stroke-dashoffset: 280; }
  58%, 86%  { stroke-dashoffset: 0; }
  100%      { stroke-dashoffset: -280; }
}

/* Clyr: the wire holds still and the signal moves along it. A short dash
   in a very long gap is one pulse per line rather than a marching ant. */
.jmk-illo__wire { stroke-dasharray: 10 240; animation: jmkIlloWire 4.6s linear infinite; }
.jmk-illo__wire--2 { animation-delay: .5s; }
.jmk-illo__wire--3 { animation-delay: 1.0s; }
.jmk-illo__wire--4 { animation-delay: 1.5s; }
.jmk-illo__wire--5 { animation-delay: 2.0s; }
@keyframes jmkIlloWire { to { stroke-dashoffset: -250; } }
.jmk-illo__node { transform-box: fill-box; transform-origin: 50% 50%; animation: jmkIlloNode 4.6s ease-in-out infinite; }
.jmk-illo__node--2 { animation-delay: .6s; }
.jmk-illo__node--3 { animation-delay: 1.2s; }
.jmk-illo__node--4 { animation-delay: 1.8s; }
@keyframes jmkIlloNode {
  0%, 100% { transform: scale(1); opacity: .82; }
  50%      { transform: scale(1.13); opacity: 1; }
}

/* The illustration standing in for a band's wall on /our-work. The
   craft cards get their 4:5 from the tile; a band has no tile, so the
   shape is set here. */
.jmk-band__illo {
  aspect-ratio: 4 / 5;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: #141414;
}

/* The glyphs. Six behaviours, mixed across a set so a row of cards is
   never six copies of the same tick. */
.jmk-glyph { width: 100%; height: 100%; display: block; overflow: visible; }
.jmk-g-spin   { animation: jmkGSpin 9s linear infinite; transform-box: fill-box; transform-origin: 50% 50%; }
.jmk-g-pulse  { animation: jmkGPulse 2.8s ease-in-out infinite; transform-box: fill-box; transform-origin: 50% 50%; }
.jmk-g-float  { animation: jmkGFloat 4.2s ease-in-out infinite; transform-box: fill-box; transform-origin: 50% 50%; }
.jmk-g-slide  { animation: jmkGSlide 3.6s cubic-bezier(.4,0,.2,1) infinite; transform-box: fill-box; transform-origin: 50% 50%; }
.jmk-g-lift   { animation: jmkGLift 3.4s cubic-bezier(.3,.7,.2,1) infinite; transform-box: fill-box; transform-origin: 50% 50%; }
.jmk-g-blink  { animation: jmkGBlink 3.2s ease-in-out infinite; }
.jmk-g-draw   { stroke-dasharray: 120; stroke-dashoffset: 120; animation: jmkGDraw 4.6s ease-in-out infinite; }
@keyframes jmkGSpin  { to { transform: rotate(360deg); } }
@keyframes jmkGPulse { 0%,100% { transform: scale(1); opacity: .85; } 50% { transform: scale(1.22); opacity: 1; } }
@keyframes jmkGFloat { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-3px); } }
@keyframes jmkGSlide { 0%,100% { transform: translateX(0); } 50% { transform: translateX(9px); } }
@keyframes jmkGLift  { 0%,100% { transform: translateY(2px); opacity: .8; } 50% { transform: translateY(-3px); opacity: 1; } }
@keyframes jmkGBlink { 0%,100% { opacity: .35; } 50% { opacity: 1; } }
@keyframes jmkGDraw  { 0% { stroke-dashoffset: 120; } 45%, 80% { stroke-dashoffset: 0; } 100% { stroke-dashoffset: -120; } }

@media (prefers-reduced-motion: reduce) {
  .jmk-illo__b, .jmk-illo__ptr, .jmk-illo__spin, .jmk-illo__tri,
  .jmk-illo__curve, .jmk-illo__anchors, .jmk-illo__grid,
  .jmk-illo__ghost, .jmk-illo__route, .jmk-illo__runner, .jmk-illo__flag,
  .jmk-illo__ring, .jmk-illo__bar, .jmk-illo__cost,
  .jmk-illo__wire, .jmk-illo__node,
  .jmk-g-spin, .jmk-g-pulse, .jmk-g-float, .jmk-g-slide,
  .jmk-g-lift, .jmk-g-blink, .jmk-g-draw {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
    stroke-dashoffset: 0 !important;
  }
  /* The pointer's whole content is its arrival, so with nothing to arrive
     from it simply waits where it lands. */
  .jmk-illo__ptr { transform: translate(282px, 352px) !important; }
  /* Same idea for the three service drawings: each holds the state it
     spends most of its loop in, which is the state that says the thing.
     The route's marker waits at the end of the route it chose; the
     expanding rings would be four circles on top of each other, so only
     the outermost is kept; and the wires' travelling pulse becomes a
     plain drawn line. */
  .jmk-illo__runner { transform: translate(330px, 120px) !important; }
  .jmk-illo__ring { opacity: .5 !important; }
  .jmk-illo__ring--2, .jmk-illo__ring--3, .jmk-illo__ring--4 { opacity: 0 !important; }
  .jmk-illo__wire { stroke-dasharray: none !important; opacity: .55 !important; }
}

/* --- explainer cards --------------------------------------------------
   Three sections of the service pages used to be three different shapes:
   a two-column numbered list, a row of rules with text under them, and a
   bulleted list. Same job in all three — here is a thing, here is what it
   means — told three ways, so the page read as three pages.

   One card, three densities. The mark in the corner is what makes them
   scannable: you find the step you want by its shape, not by counting. */
.jmk-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: clamp(14px, 1.6vw, 22px);
}
.jmk-cards--steps { grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }
.jmk-cards--takeaway { grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); }

.jmk-card {
  position: relative;
  height: 100%;
  padding: clamp(22px, 2.4vw, 30px);
  border-radius: var(--radius-md, 14px);
  border: 1px solid var(--color-border, #e4e4e4);
  background: var(--color-surface, #fff);
  overflow: hidden;
  transition:
    transform 460ms cubic-bezier(.2,.7,.2,1),
    box-shadow 460ms cubic-bezier(.2,.7,.2,1),
    border-color 300ms ease;
}
/* The wash that arrives from the bottom. A pseudo-element rather than a
   background transition, so the card's own background is never animated
   and the text never sits on a repainting surface. */
.jmk-card::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(255,98,177,0) 0%, rgba(255,98,177,.10) 100%);
  opacity: 0;
  transition: opacity 420ms ease;
  pointer-events: none;
}
.jmk-card:hover, .jmk-card:focus-within {
  transform: translateY(-6px);
  border-color: var(--color-primary, #ff62b1);
  box-shadow: 0 30px 60px -34px rgba(17,17,17,.45);
}
.jmk-card:hover::after, .jmk-card:focus-within::after { opacity: 1; }

.jmk-card__mark {
  display: block;
  width: clamp(34px, 3.4vw, 44px);
  height: clamp(34px, 3.4vw, 44px);
  margin-bottom: clamp(16px, 1.8vw, 22px);
  color: var(--color-primary, #ff62b1);
  transition: color 300ms ease, transform 520ms cubic-bezier(.34,1.3,.5,1);
}
.jmk-card:hover .jmk-card__mark { transform: scale(1.12) rotate(-4deg); }

.jmk-card__num, .jmk-card__step {
  display: block;
  font-family: var(--font-ui);
  font-size: var(--text-2xs, 11px);
  font-weight: 700;
  letter-spacing: var(--tracking-wider, .08em);
  text-transform: uppercase;
  color: var(--color-primary, #ff62b1);
  margin-bottom: 10px;
}
.jmk-card__t {
  font-family: var(--font-editorial);
  font-size: var(--text-2xl, 26px);
  line-height: 1.14;
  color: var(--ink-900, #111);
  margin: 0 0 10px;
}
.jmk-card__d {
  font-family: var(--font-ui);
  font-size: var(--text-base, 16px);
  line-height: var(--leading-relaxed, 1.65);
  color: var(--ink-500, #6b6b6b);
  margin: 0;
  position: relative;
}
/* No heading of its own, so the line itself carries the weight. */
.jmk-card--takeaway .jmk-card__d { color: var(--ink-700, #3a3a3a); font-size: var(--text-lg, 18px); }
.jmk-card--step { border-top: 2px solid var(--ink-900, #111); }

/* Above the wash. */
.jmk-card__mark, .jmk-card__num, .jmk-card__step,
.jmk-card__t, .jmk-card__d { position: relative; z-index: 1; }

@media (prefers-reduced-motion: reduce) {
  .jmk-card, .jmk-card__mark, .jmk-card::after { transition: none; }
  .jmk-card:hover, .jmk-card:focus-within { transform: none; }
  .jmk-card:hover .jmk-card__mark { transform: none; }
}

/* The mark on an accordion row. Glyph only — the number that used
   to sit under it was counting a list nobody reads in order. */
.jmk-area__mark {
  flex-shrink: 0;
  width: clamp(34px, 3vw, 42px);
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  color: var(--ink-300, #a6a6a6);
  transition: color 240ms ease, transform 460ms cubic-bezier(.34,1.3,.5,1);
}
.jmk-area__mark svg { width: 100%; height: auto; }
.jmk-area:hover .jmk-area__mark { color: var(--color-primary, #ff62b1); transform: scale(1.08); }
.jmk-area__mark[data-open="true"] { color: var(--color-primary, #ff62b1); }
@media (prefers-reduced-motion: reduce) {
  .jmk-area__mark { transition: none; }
  .jmk-area:hover .jmk-area__mark { transform: none; }
}

/* "Load more", carrying how much more there is. */
.jmk-more {
  display: inline-flex; align-items: center; gap: 12px;
  padding: 14px 26px; border-radius: var(--radius-pill, 999px);
  border: 1px solid var(--ink-900, #111);
  background: transparent; color: var(--ink-900, #111);
  font-family: var(--font-ui); font-size: var(--text-sm, 14px); font-weight: 600;
  cursor: pointer;
  transition: background 260ms ease, color 260ms ease, transform 320ms cubic-bezier(.2,.7,.2,1);
}
.jmk-more:hover { background: var(--ink-900, #111); color: #fff; transform: translateY(-2px); }
.jmk-more__n {
  font-variant-numeric: tabular-nums;
  font-size: var(--text-xs, 12px); font-weight: 700;
  padding: 3px 9px; border-radius: var(--radius-pill, 999px);
  background: var(--color-primary, #ff62b1); color: #111;
}
@media (prefers-reduced-motion: reduce) {
  .jmk-more { transition: background 260ms ease, color 260ms ease; }
  .jmk-more:hover { transform: none; }
}

/* --- cookie banner: focus you can see ---------------------------------
   The site hides the cursor and draws its own, which makes a keyboard
   ring easy to forget. On the one control a visitor is legally entitled
   to operate without a mouse, it is not optional. */
.jmk-cc :is(button, a, input, [tabindex]):focus-visible {
  outline: 2px solid var(--color-primary, #ff62b1);
  outline-offset: 3px;
  border-radius: 4px;
}
/* The heading is focused on open so the dialog announces itself, but it
   is not a control and must not be ringed like one. Written long enough
   to out-specify the rule above rather than relying on source order,
   which :is() does not respect. */
.jmk-cc h2[tabindex]:focus,
.jmk-cc h2[tabindex]:focus-visible { outline: none; }
.jmk-cc label { cursor: pointer; }

/* ---------------------------------------------------------------------
   Wallpaper mode
   ---------------------------------------------------------------------
   The way out, said once and then left alone. A screensaver with a
   permanent label on it is a dialog; one with no label at all is a page
   that has apparently broken. So: hold it long enough to be read, then
   let it go, and never bring it back — by the time it has faded the
   visitor has either moved (in which case wallpaper is already gone) or
   settled in to watch, and telling a person watching a slideshow how to
   stop watching it is the page talking over the work.
   --------------------------------------------------------------------- */
@keyframes jmkWallHint {
  0%   { opacity: 0; }
  12%  { opacity: 1; }
  62%  { opacity: 1; }
  100% { opacity: 0; }
}
.jmk-wall__hint { animation: jmkWallHint 7s var(--ease-confident, ease) forwards; }
@media (prefers-reduced-motion: reduce) {
  .jmk-wall__hint { animation: none; opacity: 0; }
}

/* The wall itself.
   ---------------------------------------------------------------------
   Black first, then the work behind it. The container fades in; the
   stage inside it fades in a beat later, so the two arrivals read as
   one movement with a pause in it rather than as two overlays.

   Fixed and `inset: 0` rather than any height unit: on a phone with a
   retracting address bar, `100vh` is the TALLEST the viewport ever gets,
   so a layer measured in it hangs below the fold until the bar has gone.
   The viewport is the right answer here and the viewport is what a fixed
   layer already fills.

   Every lane is one composited transform. `will-change` is on the track
   and nowhere else — put it on forty images and the browser is asked to
   hold forty layers for a page nobody is looking at.
   --------------------------------------------------------------------- */
.jmk-wall {
  position: fixed;
  inset: 0;
  z-index: 9000;
  overflow: hidden;
  background: #000;
  /* Slow, and slower still on the way in than the way out: a page going
     dark should feel like a light being turned down, and a page coming
     back should feel like an answer. */
  transition: opacity 1.2s var(--ease-confident, cubic-bezier(.2,.7,.2,1));
  --wall-gap: 10px;
  /* Promoted from the moment it exists, not when it starts leaving.
     -------------------------------------------------------------------
     The layer holds several hundred photographs on a plane that is being
     transformed continuously. Asking the browser to fade THAT by
     repainting it is asking for a frame it does not have: the fade-out
     transition was created, reported a currentTime of zero, and stayed
     there — the wall sat at full strength for three quarters of a second
     and then vanished, which is the jump this was supposed to replace.

     On its own compositor layer the fade is one property on one texture
     and costs nothing. will-change is normally a thing to use sparingly
     and this is exactly the case it is for: one element, short-lived,
     whose opacity is certain to be animated. */
  will-change: opacity;
}

.jmk-wall__stage {
  position: absolute;
  inset: 0;
  display: flex;
  transition: opacity 1.6s var(--ease-confident, cubic-bezier(.2,.7,.2,1));
}

.jmk-wall__lane { overflow: hidden; }

.jmk-wall__track {
  display: flex;
  will-change: transform;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

.jmk-wall__track img {
  flex: 0 0 auto;
  display: block;
  background: #111;
  /* The same corner every other gallery on the site uses. The wall is
     the work shown at its largest, and it was the one place the pieces
     were square-cornered — which read as a different kind of surface
     from the grids and the lightbox rather than as the same work bigger.
     Set on the base so all four drifts get it; the isometric plane used
     to carry its own copy of this. */
  border-radius: var(--radius-md, 4px);
  /* Rounding an <img> leaves the corners of the bitmap showing through
     on some compositors when the layer is transformed, which is exactly
     what every one of these is. Clipping to the border box is what makes
     the corner actually cut. */
  overflow: clip;
  /* No pointer work of any kind: the first thing any pointer does here
     is close the wall, and a drag-started image ghost would survive it. */
  pointer-events: none;
  user-select: none;
}

/* Vertical — lanes side by side, travelling up and down. */
.jmk-wall__stage[data-mode="vertical"] { flex-direction: row; gap: var(--wall-gap); }
.jmk-wall__stage[data-mode="vertical"] .jmk-wall__lane { flex: 1 1 0; height: 100%; }
.jmk-wall__stage[data-mode="vertical"] .jmk-wall__track { flex-direction: column; height: max-content; }
.jmk-wall__stage[data-mode="vertical"] .jmk-wall__track img {
  width: 100%;
  height: auto;
  margin-bottom: var(--wall-gap);
}
.jmk-wall__stage[data-mode="vertical"] .jmk-wall__track[data-dir="fwd"] { animation-name: jmkWallUp; }
.jmk-wall__stage[data-mode="vertical"] .jmk-wall__track[data-dir="rev"] { animation-name: jmkWallDown; }

/* Horizontal and diagonal — lanes stacked, travelling left and right.
   Fixing the HEIGHT and letting the width fall out of it is what gives
   the row its uneven rhythm: a panorama takes four times the space of a
   square, exactly as it would on a wall, instead of every picture being
   cropped to the same tile. */
.jmk-wall__stage[data-mode="horizontal"],
.jmk-wall__stage[data-mode="diagonal"] { flex-direction: column; gap: var(--wall-gap); }
.jmk-wall__stage[data-mode="horizontal"] .jmk-wall__lane,
.jmk-wall__stage[data-mode="diagonal"] .jmk-wall__lane { flex: 1 1 0; width: 100%; }
.jmk-wall__stage[data-mode="horizontal"] .jmk-wall__track,
.jmk-wall__stage[data-mode="diagonal"] .jmk-wall__track { flex-direction: row; width: max-content; height: 100%; }
.jmk-wall__stage[data-mode="horizontal"] .jmk-wall__track img,
.jmk-wall__stage[data-mode="diagonal"] .jmk-wall__track img {
  height: 100%;
  width: auto;
  margin-right: var(--wall-gap);
}
.jmk-wall__stage[data-mode="horizontal"] .jmk-wall__track[data-dir="fwd"],
.jmk-wall__stage[data-mode="diagonal"] .jmk-wall__track[data-dir="fwd"] { animation-name: jmkWallLeft; }
.jmk-wall__stage[data-mode="horizontal"] .jmk-wall__track[data-dir="rev"],
.jmk-wall__stage[data-mode="diagonal"] .jmk-wall__track[data-dir="rev"] { animation-name: jmkWallRight; }

/* ---------------------------------------------------------------------
   Isometric — the wall as a surface, twice, going opposite ways
   ---------------------------------------------------------------------
   The plane is turned away from the viewer on two axes, so what is on it
   reads as a floor of work rather than as a slideshow. One surface
   sliding one way is a conveyor belt; a second grid of the same work
   behind it travelling the OTHER way is what makes it depth — the two
   never agree, so the eye cannot lock onto either and there is always
   something crossing something else.

   preserve-3d has to be on the stage AND on each plate, or the plates
   flatten into the stage's plane and translateZ does nothing — which
   looks like the far grid simply failed to offset.

   The stage is grown to 190% and pulled back by 45%, because rotating a
   layer the size of the window on two axes leaves far more corner to
   cover than the single-axis diagonal does.
   --------------------------------------------------------------------- */
/* The perspective lives on the layer ABOVE the rotated one. Put it on
   the element you are rotating and it applies to that element's
   children, not to the element itself, and the tilt comes out flat. */
.jmk-wall:has(.jmk-wall__stage[data-mode="isometric"]) { perspective: 1500px; }
.jmk-wall__stage[data-mode="isometric"] {
  /* Big enough that the NEAR edge is off screen too. Perspective makes
     the bottom of a tilted plane the widest part of it, so a stage sized
     for the far edge leaves black wedges in the two near corners — which
     is exactly what 190% did. */
  inset: -80%;
  width: 260%;
  height: 260%;
  display: block;
  transform-style: preserve-3d;
  transform: rotateX(48deg) rotateZ(-34deg);
}
.jmk-wall__plate {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: row;
  gap: var(--wall-gap);
  transform-style: preserve-3d;
}
/* The far plate sits back and off to one side, and is knocked down so
   the near one still reads as the front. Offset as well as pushed back:
   two grids on the same footprint at different depths line up column
   for column, which is the one arrangement that reads as a mistake. */
.jmk-wall__plate[data-plate="far"] {
  transform: translateZ(-340px) translate(9%, 11%);
  opacity: 0.5;
  filter: saturate(0.75);
}
.jmk-wall__stage[data-mode="isometric"] .jmk-wall__lane { flex: 1 1 0; height: 100%; }
.jmk-wall__stage[data-mode="isometric"] .jmk-wall__track {
  flex-direction: column;
  height: max-content;
}
.jmk-wall__stage[data-mode="isometric"] .jmk-wall__track img {
  width: 100%;
  height: auto;
  margin-bottom: var(--wall-gap);
}
.jmk-wall__stage[data-mode="isometric"] .jmk-wall__track[data-dir="fwd"] { animation-name: jmkWallUp; }
.jmk-wall__stage[data-mode="isometric"] .jmk-wall__track[data-dir="rev"] { animation-name: jmkWallDown; }

/* Diagonal is the horizontal wall on a tilted stage. Rotating a layer
   the size of the window leaves four triangles of black in the corners,
   so the stage is grown by half in both directions and pulled back by a
   quarter — at 16 degrees that puts every corner comfortably outside
   the screen. */
.jmk-wall__stage[data-mode="diagonal"] {
  inset: -25%;
  width: 150%;
  height: 150%;
  transform: rotate(-16deg);
}

/* Half the track, exactly — the second copy of the pictures arrives
   where the first one started, so there is no seam to see. */
@keyframes jmkWallUp    { from { transform: translate3d(0, 0, 0); }      to { transform: translate3d(0, -50%, 0); } }
@keyframes jmkWallDown  { from { transform: translate3d(0, -50%, 0); }   to { transform: translate3d(0, 0, 0); } }
@keyframes jmkWallLeft  { from { transform: translate3d(0, 0, 0); }      to { transform: translate3d(-50%, 0, 0); } }
@keyframes jmkWallRight { from { transform: translate3d(-50%, 0, 0); }   to { transform: translate3d(0, 0, 0); } }

/* The line, over the black. It is gone by the time the wall is up, so
   it never has to compete with a picture for contrast. */
.jmk-wall__said {
  position: absolute;
  inset: 0;
  margin: 0;
  display: grid;
  place-items: center;
  padding: 0 8vw;
  text-align: center;
  color: #fff;
  font-size: clamp(1.5rem, 4.5vw, 3rem);
  font-weight: 300;
  letter-spacing: -0.01em;
  line-height: 1.2;
  text-shadow: 0 2px 40px rgba(0,0,0,.9);
  transition: opacity 1.6s var(--ease-confident, cubic-bezier(.2,.7,.2,1));
}

/* The caret on the typed line. It is not a cursor anybody can put text
   into, so it never blinks after the line is finished — the component
   stops rendering it rather than the stylesheet hiding it. */
.jmk-wall__caret {
  display: inline-block;
  width: 0.06em;
  height: 1em;
  margin-left: 0.08em;
  vertical-align: -0.08em;
  background: currentColor;
  animation: jmkWallCaret 1s steps(1, end) infinite;
}
@keyframes jmkWallCaret { 0%, 55% { opacity: 1; } 56%, 100% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .jmk-wall__caret { animation: none; opacity: 1; }
}

/* Going, rather than gone.
   The wall arrives over about two and a half seconds and used to be
   removed in a single frame, which reads as a crash rather than as the
   page coming back. Leaving is quicker than arriving — an answer should
   be faster than a question — but it is not instant. */
/* No transition here on purpose: the fade is run from script with
   element.animate(), because a CSS transition on this layer is created
   and then never advances. See the note in Collage.

   THE LANES STOP FIRST, and that is what makes the fade possible at all.
   A dozen infinite marquees on a 3D-transformed plane of photographs
   keep the compositor busy enough that a newly created animation never
   gets a frame to start on — the fade was created and sat at a
   currentTime of zero until the layer was deleted out from under it.
   Freezing the lanes at the moment of leaving hands that budget back,
   and the fade then runs in the frame it asks for. Nobody sees the
   lanes stop: they stop and the whole layer goes in the same beat. */
.jmk-wall--out { pointer-events: none; }
.jmk-wall--out .jmk-wall__track { animation-play-state: paused; }
.jmk-wall--out .jmk-wall__veil,
.jmk-wall--out .jmk-wall__quote { animation-play-state: paused; }

.jmk-wall__hint {
  position: absolute;
  left: 0;
  right: 0;
  bottom: max(28px, env(safe-area-inset-bottom, 0px));
  text-align: center;
  color: rgba(255,255,255,.72);
  font-size: .78rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  text-shadow: 0 1px 20px rgba(0,0,0,.9);
}

/* Wallpaper never arms for anyone who has asked for reduced motion, so
   this is only ever reached by a preference changed while it is up.
   Stop the lanes and leave the pictures where they stand. */
@media (prefers-reduced-motion: reduce) {
  .jmk-wall,
  .jmk-wall__stage,
  .jmk-wall__said { transition: none; }
  .jmk-wall__track { animation: none; }
}


/* ---------------------------------------------------------------------
   The closing ask
   ---------------------------------------------------------------------
   The last screen on every page, and until now a flat #111 rectangle
   with three lines of type on it — which is a fine place to put a
   button and a poor place to end an argument about a studio that makes
   things look good.

   Four soft fields of brand colour, drifting on their own clocks. Every
   period is a prime-ish number of seconds and none of them divide into
   each other, so the four never line back up into a loop you can see —
   the pattern under the type is different every time somebody arrives
   at it. Transform and opacity only, on four elements, so the whole
   thing is composited and costs nothing to run.

   The overlay is the point, not an afterthought. Underneath it this is
   fairly loud pink and blue; over it the colour is a suggestion behind
   the words and the type keeps every bit of its contrast. Turning the
   overlay off is how a background like this ruins a page.
   --------------------------------------------------------------------- */
.jmk-cta { position: relative; isolation: isolate; overflow: hidden; }
.jmk-cta > * { position: relative; z-index: 2; }

.jmk-cta__field {
  position: absolute; inset: -30%; z-index: 0; pointer-events: none;
  /* Not `filter: blur()`. A blur on a full-screen layer is a full-screen
     repaint every frame; a radial gradient with a soft stop is already
     a soft edge, and it composites. */
}
.jmk-cta__blob {
  position: absolute; border-radius: 50%;
  will-change: transform;
  mix-blend-mode: screen;
}
.jmk-cta__blob:nth-child(1) {
  width: 62vmax; height: 62vmax; left: -8%; top: -6%;
  background: radial-gradient(circle at 50% 50%, rgba(255,98,177,.86), rgba(255,98,177,0) 62%);
  animation: jmkCtaDriftA 37s ease-in-out infinite;
}
.jmk-cta__blob:nth-child(2) {
  width: 54vmax; height: 54vmax; right: -10%; top: 8%;
  background: radial-gradient(circle at 50% 50%, rgba(41,156,223,.82), rgba(41,156,223,0) 64%);
  animation: jmkCtaDriftB 43s ease-in-out infinite;
}
.jmk-cta__blob:nth-child(3) {
  width: 48vmax; height: 48vmax; left: 22%; bottom: -18%;
  background: radial-gradient(circle at 50% 50%, rgba(255,98,177,.6), rgba(255,98,177,0) 66%);
  animation: jmkCtaDriftC 53s ease-in-out infinite;
}
.jmk-cta__blob:nth-child(4) {
  width: 40vmax; height: 40vmax; right: 14%; bottom: -8%;
  background: radial-gradient(circle at 50% 50%, rgba(18,161,80,.5), rgba(18,161,80,0) 68%);
  animation: jmkCtaDriftD 61s ease-in-out infinite;
}

@keyframes jmkCtaDriftA {
  0%, 100% { transform: translate3d(0,0,0) scale(1); }
  50%      { transform: translate3d(14%, 9%, 0) scale(1.18); }
}
@keyframes jmkCtaDriftB {
  0%, 100% { transform: translate3d(0,0,0) scale(1.08); }
  50%      { transform: translate3d(-12%, 13%, 0) scale(.92); }
}
@keyframes jmkCtaDriftC {
  0%, 100% { transform: translate3d(0,0,0) scale(.95); }
  50%      { transform: translate3d(9%, -14%, 0) scale(1.22); }
}
@keyframes jmkCtaDriftD {
  0%, 100% { transform: translate3d(0,0,0) scale(1.12); }
  50%      { transform: translate3d(-16%, -8%, 0) scale(.9); }
}

/* The overlay. Dark enough that white type on it clears AAA at every
   size used here, whatever the colour underneath happens to be doing at
   that moment — which is the whole difficulty with a moving background
   and the reason the contrast cannot be checked once and trusted. */
.jmk-cta__veil {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  /* Heaviest exactly where the words are and easing off towards the
     edges, so the colour is allowed to be colour in the corners and the
     headline still sits on something close to flat black. One layer, not
     a stack: the first version added a flat 50% wash on top of this and
     the result was a black rectangle with a rumour of pink in it. */
  background: radial-gradient(115% 85% at 50% 46%, rgba(9,9,11,.74), rgba(9,9,11,.40) 78%);
}

/* Grain, so the gradients do not band on a wide dark screen. An SVG
   turbulence as a data URI: no request, and it tiles. */
.jmk-cta__veil::after {
  content: ""; position: absolute; inset: 0; opacity: .16;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.5'/%3E%3C/svg%3E");
}

@media (prefers-reduced-motion: reduce) {
  .jmk-cta__blob { animation: none; }
}

/* ---------------------------------------------------------------------
   Pinned sequences
   ---------------------------------------------------------------------
   The scroll grammar the Clyr page is built on, made house so the rest
   of the site can speak it. A tall parent buys the scroll distance; a
   sticky child is the one screen the sequence happens on; the statements
   are stacked in a single grid cell so swapping them reflows nothing.

   Scroll-linked, never timed — so scrolling back runs it backwards and
   the page feels attached to the reader rather than played at them.

   Two things that are easy to get wrong and silent when you do:

     · overflow lives on the PINNED element, never on the tall parent.
       An ancestor with overflow becomes the scrolling context and
       position:sticky stops working with no error at all.
     · 100svh, not 100vh. On a phone vh is the height with the browser
       chrome retracted, so a 100vh pin is always taller than what can
       actually be seen.
   --------------------------------------------------------------------- */
.jmk-stage--pin { position: relative; }

.jmk-pin {
  position: sticky; top: 0;
  height: 100svh;
  overflow: hidden;
  display: flex; align-items: center;
  padding: clamp(48px, 8vh, 104px) clamp(20px, 5vw, 72px);
}

.jmk-swap { display: grid; width: 100%; max-width: min(1240px, 92vw); margin: 0 auto; }
.jmk-swap > * { grid-area: 1 / 1; }

.jmk-swap__line {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 640ms cubic-bezier(.2,.7,.2,1),
              transform 780ms cubic-bezier(.16,.84,.28,1);
  will-change: opacity, transform;
}
.jmk-swap__line[data-on="true"]   { opacity: 1; transform: none; }
/* The one leaving clears faster than the one arriving lands. These
   statements carry a paragraph under them — longer than the single lines
   on the Clyr page this came from — and two paragraphs at half opacity
   over each other is unreadable in a way two short lines never were. */
.jmk-swap__line[data-past="true"] {
  opacity: 0;
  transform: translateY(-28px);
  transition: opacity 260ms ease, transform 620ms cubic-bezier(.16,.84,.28,1);
}

/* A rule that draws itself as the statement lands — the same mark the
   big pinned quotes already carry, so a sequence and a statement read as
   the same family. */
.jmk-swap__rule {
  display: block; height: 2px; width: 0;
  transition: width 720ms cubic-bezier(.16,.84,.28,1) 120ms;
  margin-bottom: clamp(22px, 3vw, 38px);
}
.jmk-swap__line[data-on="true"] .jmk-swap__rule { width: 72px; }

/* How far through, for somebody who wants to know how much is left.
   Deliberately faint: it is a reassurance, not a control. */
.jmk-pin__ticks { position: absolute; bottom: clamp(20px, 4vh, 40px); left: 50%;
  transform: translateX(-50%); display: flex; gap: 7px; }
.jmk-pin__ticks i { display: block; width: 22px; height: 2px; border-radius: 2px;
  background: currentColor; opacity: .18; transition: opacity 360ms ease; }
.jmk-pin__ticks i[data-on="true"] { opacity: .75; }

@media (prefers-reduced-motion: reduce) {
  /* A pinned section whose whole content is a sequence has nothing to
     show when the sequence cannot run, so it stops pinning and lets the
     statements stack as ordinary text. Not a broken version of the
     effect — a page without it. */
  .jmk-stage--pin { height: auto !important; }
  .jmk-pin { position: static; height: auto; padding-top: 0; padding-bottom: 0; display: block; }
  .jmk-swap { display: block; }
  .jmk-swap > * { margin-bottom: clamp(30px, 5vh, 60px); }
  .jmk-swap__line { transition: none; opacity: 1 !important; transform: none !important; }
  .jmk-swap__rule { width: 72px; transition: none; }
  .jmk-pin__ticks { display: none; }
}

/* ---------------------------------------------------------------------
   The typing caret on the home page headline
   ---------------------------------------------------------------------
   A bar the height of the capitals, sitting inside the black box with the
   letters it is producing. It blinks only while it is moving; once the
   line is typed the caret is gone entirely rather than left winking at
   somebody who has finished reading.
   --------------------------------------------------------------------- */
.jmk-type-caret {
  display: inline-block;
  width: 0.06em;
  height: 0.78em;
  margin-left: 0.04em;
  vertical-align: baseline;
  background: currentColor;
  animation: jmkTypeCaret 900ms steps(1, end) infinite;
}
@keyframes jmkTypeCaret {
  0%, 55%  { opacity: 1; }
  56%, 99% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .jmk-type-caret { display: none; }
}

/* ---------------------------------------------------------------------
   The phone
   ---------------------------------------------------------------------
   Portrait work shown at the size it was made for, rather than
   letterboxed into a 16:9 slot as two black bars and a sliver of the
   actual thing. Drawn in CSS: a picture of a phone is a fixed resolution,
   a fixed colour and one more request, and it goes soft the moment it is
   scaled. This takes the site's own tokens, so it belongs here rather
   than looking like a stock mockup somebody pasted in.
   --------------------------------------------------------------------- */
.jmk-phone {
  position: relative;
  width: 100%;
  max-width: 300px;
  margin: 0 auto;
  padding: 10px;
  border-radius: 40px;
  background: linear-gradient(160deg, #2b2b2f, #141416 45%, #232327);
  box-shadow:
    0 0 0 1px rgba(255,255,255,.09),
    0 30px 60px -28px rgba(0,0,0,.75);
}
.jmk-phone__screen {
  position: relative;
  border-radius: 30px;
  overflow: hidden;
  background: #000;
  /* The child carries its own 9:16, so this only has to clip. */
}
/* The earpiece slot. One small asymmetric detail is what stops a rounded
   rectangle reading as a rounded rectangle. */
.jmk-phone__ear {
  position: absolute; z-index: 2;
  top: 20px; left: 50%; transform: translateX(-50%);
  width: 54px; height: 5px; border-radius: 3px;
  background: rgba(255,255,255,.16);
}
.jmk-phone .jmk-lift { border-radius: 30px; }
@media (max-width: 640px) { .jmk-phone { max-width: 240px; padding: 8px; border-radius: 32px; } }

/* =====================================================================
   What we're for — three claims on a rail
   =====================================================================
   A tall section, a sticky stage inside it, and one transform on a track
   of cards. The scroll is the page's own; nothing here intercepts a
   wheel, a trackpad or a swipe, so the browser's scrolling stays the
   browser's and this is only reading a number off it.

   The overflow is on the STAGE, which is the sticky element. Putting it
   on the section — the obvious place — kills the stick outright, and the
   symptom is not an error, it is the effect quietly not happening.
   ===================================================================== */
.jmk-pil {
  position: relative;
  background: var(--color-surface-sunken, #FAFAFA);
  border-top: 1px solid var(--color-border, #E4E4E4);
}

.jmk-pil__stage {
  position: sticky; top: 0;
  height: 100svh;
  overflow: hidden;                 /* here, and nowhere above it */
  display: flex; flex-direction: column;
  justify-content: center;
  gap: clamp(20px, 3.5vh, 40px);
  padding: clamp(28px, 5vh, 56px) 0 clamp(28px, 5vh, 52px);
}

/* --- the heading, at the smaller scale ------------------------------- */
.jmk-pil__head { padding: 0 clamp(20px, 5vw, 64px); flex: 0 0 auto; }
.jmk-pil__kicker {
  font-family: var(--font-ui); font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wider); text-transform: uppercase;
  color: var(--color-secondary, #299CDF); font-weight: 600;
  margin-bottom: 10px;
}
/* Down from clamp(32px, 4.6vw, 58px). At the old size the heading and the
   card titles under it were within a few pixels of each other, so the
   section read as four headings rather than as one heading over three
   claims. */
.jmk-pil__heading {
  font-family: var(--font-editorial);
  font-size: clamp(26px, 3.1vw, 42px);
  line-height: 1.04; margin: 0;
  color: var(--ink-900, #111);
}

/* --- the rail -------------------------------------------------------- */
.jmk-pil__track {
  display: flex; align-items: center;
  gap: clamp(16px, 2vw, 28px);
  padding: 0 clamp(20px, 5vw, 64px);
  width: max-content;
  min-height: 0;
  /* Not 1 1 auto. Allowed to grow, the track absorbed every spare pixel in
     the stage and the three children spread to the corners — a heading at
     the very top, a rail at the very bottom and a band of nothing between
     them. Sized to its cards, the group sits together in the middle. */
  flex: 0 0 auto;
  will-change: transform;
}

.jmk-pil__card {
  position: relative; isolation: isolate; overflow: hidden;
  flex: 0 0 auto;
  /* Wide enough that the next one is only ever peeking. At 46vw all three
     fitted on a laptop at once, which turned a rail into a grid that
     happened to slide — and the whole reason for the rail is that a claim
     should have to be read before the next one is available. */
  width: min(66vw, 660px);
  /* Given a height rather than stretched to the stage. Stretched, a card
     with four lines on it was seven hundred pixels tall and two thirds of
     it was nothing — which is the opposite of tightening the scale. */
  height: min(54svh, 460px);
  border-radius: clamp(18px, 1.6vw, 26px);
  background: var(--color-surface, #fff);
  border: 1px solid var(--color-border, #E4E4E4);
  display: flex;
}

/* The abstract, per card. Same grammar as the closing CTA's field —
   soft radial blobs that drift and composite — but at a tint rather than
   a saturation, because the type on these stays ink. A card whose colour
   is strong enough to need white text is a card whose contrast has to be
   re-checked every frame it moves. */
.jmk-pil__field { position: absolute; inset: -25%; z-index: 0; pointer-events: none; }
.jmk-pil__blob { position: absolute; border-radius: 50%; will-change: transform; }
.jmk-pil__blob:nth-child(1) {
  width: 78%; height: 78%; left: -14%; top: -10%;
  background: radial-gradient(circle at 50% 50%, rgba(var(--hue), .30), rgba(var(--hue), 0) 66%);
  animation: jmkPilDriftA 31s ease-in-out infinite;
}
.jmk-pil__blob:nth-child(2) {
  width: 64%; height: 64%; right: -12%; top: 18%;
  background: radial-gradient(circle at 50% 50%, rgba(var(--hue), .22), rgba(var(--hue), 0) 68%);
  animation: jmkPilDriftB 43s ease-in-out infinite;
}
.jmk-pil__blob:nth-child(3) {
  width: 58%; height: 58%; left: 16%; bottom: -18%;
  background: radial-gradient(circle at 50% 50%, rgba(var(--hue), .16), rgba(var(--hue), 0) 70%);
  animation: jmkPilDriftC 53s ease-in-out infinite;
}
@keyframes jmkPilDriftA {
  0%, 100% { transform: translate3d(0,0,0) scale(1); }
  50%      { transform: translate3d(9%, 7%, 0) scale(1.16); }
}
@keyframes jmkPilDriftB {
  0%, 100% { transform: translate3d(0,0,0) scale(1.1); }
  50%      { transform: translate3d(-8%, 10%, 0) scale(.92); }
}
@keyframes jmkPilDriftC {
  0%, 100% { transform: translate3d(0,0,0) scale(.96); }
  50%      { transform: translate3d(7%, -11%, 0) scale(1.2); }
}

.jmk-pil__body {
  position: relative; z-index: 1;
  display: flex; flex-direction: column;
  padding: clamp(24px, 2.6vw, 40px);
  width: 100%;
}

/* The mark. Its own tinted disc so it reads as a mark rather than as a
   drawing that happens to be there, and in the card's colour so the
   three are obviously three of a kind. */
.jmk-pil__mark {
  width: clamp(44px, 4vw, 58px); height: clamp(44px, 4vw, 58px);
  display: grid; place-items: center;
  padding: clamp(9px, .9vw, 13px);
  border-radius: 14px;
  background: rgba(var(--hue), .12);
  color: rgb(var(--hue));
  margin-bottom: clamp(16px, 2vh, 24px);
  flex: 0 0 auto;
}

/* The mark is drawn once and then left alone.
   The glyph set was built for cards where the line drawing itself is the
   effect: `jmk-g-draw` starts fully dash-offset, so the shape is ABSENT
   for part of every cycle. At card size that reads as an animation; at
   the size of a mark on a claim it reads as a broken icon, and half the
   time there is nothing there at all. So the outline is simply on, and
   only the quiet motion — the float, the pulse — is kept. */
.jmk-pil__mark .jmk-g-draw { animation: none; stroke-dashoffset: 0; }
.jmk-pil__mark .jmk-g-blink { animation: none; opacity: 1; }
.jmk-pil__mark .jmk-glyph { stroke-width: 2.4; }

.jmk-pil__eyebrow {
  font-family: var(--font-ui); font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wider); text-transform: uppercase;
  font-weight: 600; color: rgb(var(--hue));
  padding-top: 14px; border-top: 2px solid rgb(var(--hue));
  align-self: flex-start;
  margin-bottom: 12px;
}
.jmk-pil__title {
  font-family: var(--font-editorial);
  font-size: clamp(21px, 2vw, 30px);
  line-height: 1.08; margin: 0 0 12px;
  color: var(--ink-900, #111);
}
.jmk-pil__text {
  font-family: var(--font-ui);
  font-size: clamp(14px, 1.05vw, 16px);
  line-height: 1.62; margin: 0;
  color: var(--ink-500, #6B6B6B);
}
/* The number, so a card taken out of the row still says where it sat. */
.jmk-pil__count {
  margin-top: auto; padding-top: 20px;
  font-family: var(--font-editorial);
  font-size: clamp(28px, 2.6vw, 40px); line-height: 1;
  color: rgba(var(--hue), .28);
}

.jmk-pil__rail {
  flex: 0 0 auto;
  margin: 0 clamp(20px, 5vw, 64px);
  height: 2px; background: var(--color-border, #E4E4E4);
  border-radius: 2px; overflow: hidden;
}
.jmk-pil__rail span {
  display: block; height: 100%; width: 100%;
  background: var(--ink-900, #111);
  transform-origin: left center;
}

/* --- phone, and reduced motion --------------------------------------- */
.jmk-pil--flat { padding: clamp(48px, 7vh, 90px) 0; }
.jmk-pil--flat .jmk-pil__head { margin-bottom: clamp(26px, 4vh, 44px); }
.jmk-pil__strip {
  display: flex; gap: 16px;
  padding: 4px clamp(20px, 5vw, 64px);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}
.jmk-pil__strip::-webkit-scrollbar { display: none; }
.jmk-pil__strip .jmk-pil__card { width: min(84vw, 420px); scroll-snap-align: center; }

/* Stacked, not a strip: under reduced motion a row you have to scroll is
   still movement, and the whole reason this section became a rail was
   movement. */
.jmk-pil--still .jmk-pil__strip {
  display: grid; grid-template-columns: 1fr; gap: 20px; overflow: visible;
}
.jmk-pil--still .jmk-pil__card { width: auto; }

@media (prefers-reduced-motion: reduce) {
  .jmk-pil__blob { animation: none; }
  .jmk-pil__track { transition: none; }
}


/* =====================================================================
   Before you go
   ---------------------------------------------------------------------
   The one prompt on the way out. See ExitIntent.jsx for the five rules
   that decide whether it is allowed to appear at all — this is only what
   it looks like once it has earned the right.
   ===================================================================== */
.jmk-bye { position: fixed; inset: 0; z-index: 620; display: grid; place-items: center; padding: 20px; }
.jmk-bye__scrim { position: absolute; inset: 0; background: rgba(10,10,12,.72); animation: jmkByeIn 300ms ease both; }
.jmk-bye__card {
  position: relative; z-index: 1;
  width: min(520px, 100%);
  padding: clamp(30px, 5vw, 48px);
  border-radius: 22px;
  background: var(--white, #fff); color: var(--ink-900, #111);
  box-shadow: 0 50px 110px -40px rgba(0,0,0,.7);
  text-align: center;
  animation: jmkByeUp 420ms cubic-bezier(.16,.84,.28,1) both;
}
.jmk-bye__lbl {
  margin: 0; font-size: 11.5px; font-weight: 600; letter-spacing: .14em;
  text-transform: uppercase; opacity: .5;
}
.jmk-bye__h {
  margin: 12px 0 0; font-size: clamp(25px, 3.2vw, 34px);
  font-weight: 700; letter-spacing: -0.035em; line-height: 1.1;
}
.jmk-bye__p { margin: 14px 0 0; font-size: 15px; line-height: 1.6; opacity: .72; }
/* The pill does the rest. This carried its own copy of the pill's
   padding, radius, fill and hover, which is a second pill to keep in
   step with the first — and it had already drifted to a different hover
   colour. All that is left here is where it sits. */
.jmk-bye__go { margin-top: 24px; }
.jmk-bye__x {
  position: absolute; top: 14px; right: 16px;
  width: 34px; height: 34px; border-radius: 999px;
  border: 0; background: rgba(17,17,17,.06); color: inherit;
  font-size: 13px; cursor: pointer; line-height: 1;
}
.jmk-bye__x:hover { background: rgba(17,17,17,.12); }

@keyframes jmkByeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes jmkByeUp { from { opacity: 0; transform: translateY(18px) scale(.97); } to { opacity: 1; transform: none; } }

@media (prefers-reduced-motion: reduce) {
  .jmk-bye__scrim, .jmk-bye__card { animation: none; }
}

/* ====================================================================
   Partnerships, dealt as a deck
   ====================================================================
   Paired with PartnerStack.jsx, which does the moving. Everything here
   is the stage the cards move on.

   The deck is one grid cell with every card stacked into it, so the
   stack takes the height of its tallest card and nothing below it moves
   as the cards change. Absolute positioning would have worked too and
   would have needed a height picked by hand.

   preserve-3d on the deck and the perspective on its parent: the depth
   has to be real, because the cards behind are turned as well as scaled
   and a 2D fake of that reads as a drop shadow.
   ==================================================================== */
.pstack { position: relative; }
/* The deck sits BESIDE the claim, not under it.
   Centred below a left-aligned heading it looked like a card that had
   come loose, with the whole right half of the section empty. The stage
   is a two-column grid now: the sentence on one side, the evidence on
   the other, both on the one screen the section is pinned to. */
.pstack--live { perspective: 1500px; }
/* The grid goes on the REVEAL WRAPPER, not on the section.
   Everything in this section — eyebrow, heading, standfirst and deck —
   is inside one Reveal, so the section has a single child and a grid on
   it has nothing to arrange. The wrapper is the thing whose children
   need columns. */
.pstack-host > * {
  display: grid;
  grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr);
  column-gap: clamp(28px, 5vw, 80px);
  align-items: center;
}
/* The words stack down the first column, the deck holds the second and
   spans whatever number of rows they happen to make. */
.pstack-host > * > *:not(.pstack) { grid-column: 1; }
.pstack-host > * > .pstack {
  grid-column: 2;
  /* `1 / -1` does not work here and never did.
     -1 counts back from the end of the EXPLICIT grid, and this grid
     declares columns only — every row is implicit — so `1 / -1`
     resolved to a single row. The deck therefore sat in row one, which
     is the eyebrow's row, and stretched it to the deck's own height:
     that is why the cards floated level with a two-word kicker while
     the sentence they belong to sat two hundred pixels below them.
     A span counts implicit rows, which is what this needs. */
  grid-row: 1 / span 12;
  align-self: center;
}

@media (max-width: 900px) {
  .pstack-host > * {
    grid-template-columns: minmax(0, 1fr);
    row-gap: clamp(18px, 3.5vh, 34px);
  }
  .pstack-host > * > *:not(.pstack),
  .pstack-host > * > .pstack { grid-column: 1; grid-row: auto; }
}
.pstack--live .pstack__deck {
  display: grid;
  transform-style: preserve-3d;
  /* Wider than it was. This section is pinned, so it is only ever seen
     as a whole screen, and a 460px card in the right half of a 1440px
     screen is a stamp in a field. */
  width: min(100%, 560px);
  margin-inline: auto;
}
.pstack--live .pstack__deck > .pstack__card {
  grid-area: 1 / 1;
  transform-style: preserve-3d;
  will-change: transform, opacity;
  backface-visibility: hidden;
}

/* Before GSAP lands — and on a phone, and under reduced motion — the
   cards are the grid they always were. */
.pstack:not(.pstack--live) .pstack__deck {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(16px, 2.2vw, 24px);
}
@media (max-width: 768px) {
  .pstack:not(.pstack--live) .pstack__deck { grid-template-columns: 1fr 1fr; }
}

.pstack__dots {
  display: flex; gap: 10px; justify-content: center;
  margin-top: clamp(22px, 3.5vh, 36px);
}
.pstack__hint { display: block; text-align: center; }
.pstack__dot {
  width: 9px; height: 9px; padding: 0; border: 0; border-radius: 999px;
  background: var(--ink-900); opacity: 0.22; cursor: pointer;
  transition: opacity 220ms var(--ease-confident, ease),
              transform 220ms var(--ease-confident, ease),
              background-color 220ms ease;
}
.pstack__dot:hover { opacity: 0.5; }
.pstack__dot.on { opacity: 1; background: var(--color-primary); transform: scale(1.35); }
.pstack__dot:focus-visible { outline: 2px solid var(--ink-900); outline-offset: 3px; }

.pstack__hint {
  margin-top: 14px;
  font-family: var(--font-ui); font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--ink-300);
}

@media print {
  /* On paper a deck is one card. The grid is the honest version. */
  .pstack--live { perspective: none; }
  .pstack--live .pstack__deck {
    grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px;
  }
  .pstack--live .pstack__deck > .pstack__card {
    grid-area: auto; transform: none !important; opacity: 1 !important;
  }
  .pstack__dots, .pstack__hint { display: none; }
}

/* The section while it is holding a live deck. It needs the screen it is
   pinned to: pinned to the top of the window at its natural height, a
   section shorter than the window leaves the page below it showing under
   the pin, and one taller than the window has its foot cut off. */
.pstack-host {
  min-height: 100svh;
  display: flex; flex-direction: column; justify-content: center;
  /* Tighter than the section's resting padding. Pinned, this has to fit
     the window exactly: the heading, the line under it and the deck all
     on one screen, because anything past the fold of a pinned section is
     somewhere the reader can never scroll to. */
  padding-top: clamp(48px, 6vh, 80px);
  padding-bottom: clamp(48px, 6vh, 80px);
}

/* ======================================================================
   The client wall — rows rather than a strip
   ----------------------------------------------------------------------
   One band of logos across the middle of a full-screen section leaves
   most of the screen empty. Three rows running alternate ways is the
   same list filling the space it was given, and directions that never
   resolve into a pattern are what keep it being looked at.

   Each row holds its list TWICE and travels exactly half its own width,
   so the second copy lands where the first began and the loop has no
   seam. That only holds if every logo carries the same trailing gap —
   which is why the spacing is a margin on the item and not a `gap` on
   the row. A gap is laid between items and not after the last one, so
   half the track would be one gap short and the wall would tick sideways
   once a minute forever.
   ====================================================================== */
.jmk-brands { overflow: hidden; position: relative; margin: 0 auto; }
.jmk-brands__row {
  display: flex; width: max-content; align-items: center;
  animation: jmkBrandsLeft var(--reel-secs, 44s) linear infinite;
}
.jmk-brands__row + .jmk-brands__row { margin-top: clamp(14px, 2.2vh, 30px); }
/* Alternate, rather than "every row after the first" — with two rows
   those are the same rule, with three the second version sends rows two
   and three the same way and the alternation stops. */
.jmk-brands__row:nth-child(even) { animation-name: jmkBrandsRight; }
@keyframes jmkBrandsLeft  { from { transform: translateX(0); }    to { transform: translateX(-50%); } }
@keyframes jmkBrandsRight { from { transform: translateX(-50%); } to { transform: translateX(0); } }

.jmk-brands:hover .jmk-brands__row,
.jmk-brands:focus-within .jmk-brands__row { animation-play-state: paused; }

.jmk-brands__item {
  width: clamp(104px, 13vw, 168px);
  aspect-ratio: 1 / 1;
  margin: 0 clamp(14px, 2.2vw, 30px);
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.jmk-brands__item a, .jmk-brands__item img { display: block; width: 100%; height: 100%; }
.jmk-brands__item img {
  object-fit: contain;
  /* The wall is one colour; twenty-five logos in twenty-five palettes on
     top of it is a jumble. Knocked back so they read as one set, and back
     to their own colours on hover for anybody checking they read it right. */
  filter: grayscale(1); opacity: 0.62;
  transition: filter 420ms ease, opacity 420ms ease,
              transform 420ms var(--ease-confident, cubic-bezier(.2,.7,.2,1));
}
.jmk-brands__item:hover img { filter: grayscale(0); opacity: 1; transform: scale(1.06); }

/* Fade the ends so logos enter and leave rather than snapping. */
.jmk-brands::before, .jmk-brands::after {
  content: ""; position: absolute; top: 0; bottom: 0; width: 9%; z-index: 2; pointer-events: none;
}
.jmk-brands::before { left: 0;  background: linear-gradient(90deg, var(--color-surface-sunken), transparent); }
.jmk-brands::after  { right: 0; background: linear-gradient(270deg, var(--color-surface-sunken), transparent); }

@media (prefers-reduced-motion: reduce) {
  .jmk-brands__row { animation: none !important; flex-wrap: wrap; justify-content: center; width: 100%; }
  .jmk-brands__item[aria-hidden="true"] { display: none; }   /* one copy is enough when still */
  .jmk-brands__item img { filter: grayscale(0); opacity: 1; transition: none !important; }
}

/* --- the house pill, as a link ---------------------------------------
   Everything else on this site that takes you somewhere is a pill. A
   text link at the foot of the wall made the wall look like it ended
   rather than led anywhere. */
.jmk-pill {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 15px 30px; border-radius: var(--radius-button);
  background: var(--ink-900); color: var(--white);
  font-family: var(--font-ui); font-weight: var(--weight-semibold);
  font-size: var(--text-sm); text-decoration: none;
  transition: background-color 220ms ease, color 220ms ease,
              transform 220ms var(--ease-confident, ease);
}
.jmk-pill:hover { background: var(--color-primary); color: var(--white); transform: translateY(-2px); }
.jmk-pill:focus-visible { outline: 2px solid var(--ink-900); outline-offset: 3px; }
.jmk-pill span { transition: transform 220ms var(--ease-confident, ease); }
.jmk-pill:hover span { transform: translateX(4px); }
@media (prefers-reduced-motion: reduce) {
  .jmk-pill, .jmk-pill span { transition: none !important; transform: none !important; }
}

/* --- the dark half of the wall, and what goes in it -------------------
   Type laid straight over photographs is a fight neither side wins.
   The top of the wall is faded down so there is somewhere to put words,
   and the fade is drawn whether or not there is a quote in it — it is
   part of the composition, and a band that appeared only when a quote
   did would make the whole screen change every nine seconds. */
.jmk-wall__veil {
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 58%;
  pointer-events: none;
  background: linear-gradient(
    180deg,
    rgba(0,0,0,0.97) 0%,
    rgba(0,0,0,0.92) 34%,
    rgba(0,0,0,0.66) 62%,
    rgba(0,0,0,0) 100%
  );
  /* Arrives with the wall behind it, not before — the black screen at
     the start is already black and does not need a gradient on it. */
  animation: jmkWallVeil 1.6s var(--ease-confident, ease) both;
}
@keyframes jmkWallVeil { from { opacity: 0; } to { opacity: 1; } }

.jmk-wall__quote {
  position: absolute;
  left: 50%; top: 13%;
  transform: translateX(-50%);
  width: min(90vw, 60ch);
  margin: 0;
  text-align: center;
  color: #fff;
  pointer-events: none;
  animation: jmkWallQuote 900ms var(--ease-confident, ease) both;
}
@keyframes jmkWallQuote {
  from { opacity: 0; transform: translateX(-50%) translateY(10px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}
.jmk-wall__quote blockquote {
  margin: 0;
  font-family: var(--font-editorial);
  font-size: clamp(1.35rem, 3.2vw, 2.6rem);
  line-height: 1.22;
  letter-spacing: -0.015em;
  text-wrap: balance;
  text-shadow: 0 2px 40px rgba(0,0,0,.9);
}
.jmk-wall__quote figcaption {
  margin-top: clamp(12px, 2vh, 20px);
  font-family: var(--font-ui);
  font-size: .78rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: rgba(255,255,255,.7);
}
@media (prefers-reduced-motion: reduce) {
  .jmk-wall__veil, .jmk-wall__quote { animation: none; }
}

/* ======================================================================
   Layout morphing — see morph.js
   ----------------------------------------------------------------------
   Naming an element makes the browser treat it as ONE thing across two
   documents rather than as something that vanished and something else
   that appeared. The furniture that is on every page is named so it
   stays put while the pages change underneath it; the stage name is the
   card-into-hero move.

   A name has to be UNIQUE in a document. Every selector here is either an
   id or a first-of-its-kind, and the stage name is attached to exactly
   one element at a time from script.
   ====================================================================== */
:root[data-morph="on"] #main { view-transition-name: jmk-morph-main; }
/* Hooked by an attribute the components carry rather than by guessing at
   their markup — a selector that silently stops matching after a refactor
   is a transition that silently stops working. */
:root[data-morph="on"] [data-morph-fixed="nav"] { view-transition-name: jmk-morph-nav; }
:root[data-morph="on"] [data-morph-fixed="foot"] { view-transition-name: jmk-morph-foot; }
:root[data-morph="on"] button[aria-label="Back to top"] { view-transition-name: jmk-morph-top; }

/* The furniture does not fade — it is the same nav, so it holds still
   while everything else changes. Giving it a name but leaving the default
   cross-fade group animation on would dissolve it into itself, which
   looks like a flicker rather than like nothing happening. */
::view-transition-group(jmk-morph-nav),
::view-transition-group(jmk-morph-top) { animation-duration: 0.01ms; }
::view-transition-old(jmk-morph-nav),
::view-transition-new(jmk-morph-nav),
::view-transition-old(jmk-morph-top),
::view-transition-new(jmk-morph-top) { animation: none; mix-blend-mode: normal; }

/* The body of the page. Long enough to read as a movement, short enough
   that it never sits between two pages. */
::view-transition-group(jmk-morph-main) { animation-duration: 420ms; }
::view-transition-old(jmk-morph-main) { animation: jmkMorphOut 320ms var(--ease-confident, ease) both; }
::view-transition-new(jmk-morph-main) { animation: jmkMorphIn 420ms var(--ease-confident, ease) both; }
@keyframes jmkMorphOut { to { opacity: 0; transform: translateY(-10px); } }
@keyframes jmkMorphIn { from { opacity: 0; transform: translateY(14px); } }

/* A card growing into the hero it opened. The picture is the thing that
   moves, so it holds its shape all the way rather than cross-fading
   halfway through the journey. */
:root[data-morph="on"] [data-morph-hero] { view-transition-name: jmk-morph-stage; }
::view-transition-group(jmk-morph-stage) { animation-duration: 520ms; }
::view-transition-old(jmk-morph-stage),
::view-transition-new(jmk-morph-stage) {
  animation: none;
  height: 100%;
  object-fit: cover;
  mix-blend-mode: normal;
}

/* A card that is about to become a page says so under the pointer. */
:root[data-morph="on"] [data-morph] { will-change: auto; }

@media (prefers-reduced-motion: reduce) {
  /* morph.js never goes live under this, so these can only be reached by
     a preference changed mid-navigation. Nothing moves. */
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

/* ======================================================================
   The corner contact button — see ContactDot.jsx
   ----------------------------------------------------------------------
   Sits under the back-to-top button, which is moved up to make room. The
   bottom-right corner is the one every visitor already checks, so the
   thing that earns money goes in it and the convenience goes above.
   ====================================================================== */
.jmk-cdot {
  position: fixed;
  right: 28px;
  bottom: 28px;
  z-index: 485;                     /* above the page, under every dialog */
  width: 56px; height: 56px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--color-primary);
  color: var(--white);
  text-decoration: none;
  box-shadow: 0 10px 30px -8px rgba(0,0,0,.45);
  /* Arrives rather than being there: see the note about competing with
     first paint. */
  opacity: 0;
  transform: translateY(16px) scale(.85);
  transition:
    opacity 320ms var(--ease-confident, ease),
    transform 320ms var(--ease-confident, ease),
    background-color 220ms ease;
}
.jmk-cdot.is-in { opacity: 1; transform: translateY(0) scale(1); }
.jmk-cdot:hover, .jmk-cdot:focus-visible { background: var(--ink-900); }
.jmk-cdot:focus-visible { outline: 2px solid var(--ink-900); outline-offset: 3px; }

/* It keeps bouncing, but rarely.
   -------------------------------------------------------------------
   The first version bounced three times and then settled, on the
   argument that a permanent animation in the corner of every page is a
   fly in the room. That argument holds for a CONTINUOUS one; it does
   not hold for this. The cycle is six seconds long and the button is
   still for five of them — it catches the eye once in a while and then
   gets out of the way, which is what a nudge is supposed to do.

   Faster and twice over on hover, where movement is answering something
   the visitor just did rather than interrupting them. */
.jmk-cdot.is-in { animation: jmkCdotBounce 6s var(--ease-confident, ease) 1.2s infinite; }
.jmk-cdot.is-in.is-hover { animation: jmkCdotHop 1.6s var(--ease-confident, ease) infinite; }
@keyframes jmkCdotBounce {
  0%, 82%, 100% { transform: translateY(0) scale(1); }
  87%           { transform: translateY(-10px) scale(1.04); }
  92%           { transform: translateY(0) scale(1); }
  96%           { transform: translateY(-4px) scale(1.01); }
}
@keyframes jmkCdotHop {
  0%, 55%, 100% { transform: translateY(0) scale(1.04); }
  70%           { transform: translateY(-8px) scale(1.07); }
  85%           { transform: translateY(0) scale(1.04); }
}

/* The marker. Persistent on purpose, and never claiming a number. */
.jmk-cdot__dot {
  position: absolute;
  top: 3px; right: 3px;
  width: 13px; height: 13px;
  border-radius: 50%;
  background: #ef4444;
  border: 2px solid var(--color-surface, #fff);
  pointer-events: none;
}

/* The speech bubble, with the little tail that makes it one. */
.jmk-cdot__say {
  position: absolute;
  right: calc(100% + 14px);
  top: 50%;
  transform: translateY(-50%) translateX(6px);
  white-space: nowrap;
  background: var(--ink-900);
  color: var(--white);
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: var(--text-sm);
  padding: 9px 15px;
  border-radius: 12px;
  box-shadow: 0 8px 24px -10px rgba(0,0,0,.5);
  opacity: 0;
  pointer-events: none;
  transition: opacity 200ms ease, transform 200ms var(--ease-confident, ease);
}
.jmk-cdot__say::after {
  content: "";
  position: absolute;
  left: 100%; top: 50%;
  transform: translateY(-50%);
  border: 6px solid transparent;
  border-left-color: var(--ink-900);
}
.jmk-cdot.is-hover .jmk-cdot__say { opacity: 1; transform: translateY(-50%) translateX(0); }

/* The back-to-top button moves up so the two are stacked rather than
   sharing a corner. Its own inline styles set `bottom`, so this has to
   win on specificity rather than on politeness. */
body .jmk-cdot ~ button[aria-label="Back to top"],
body button[aria-label="Back to top"].totop--lifted { bottom: 96px; }
button[aria-label="Back to top"] { bottom: 96px !important; }

@media (max-width: 640px) {
  .jmk-cdot { right: 18px; bottom: 18px; width: 52px; height: 52px; }
  button[aria-label="Back to top"] { bottom: 82px !important; right: 18px !important; }
  /* No hover on a touch screen, so the bubble would never be seen and
     the label lives on the link. */
  .jmk-cdot__say { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .jmk-cdot { transition: none; animation: none !important; }
  .jmk-cdot.is-in { opacity: 1; transform: none; }
  .jmk-cdot__say { transition: none; }
}
@media print { .jmk-cdot { display: none; } }

/* --- sparkles on hover — see Sparkles in Motion.jsx -------------------
   The wrapper is inline-block and positioned so the stars can be placed
   against the thing it wraps; it must not otherwise change the layout of
   whatever is inside it, which is why it takes no padding and no size of
   its own. */
.jmk-spark { position: relative; display: inline-block; }
.jmk-spark__star {
  position: absolute;
  fill: var(--color-primary);
  pointer-events: none;
  z-index: 3;
  transform: translate(-50%, -50%) scale(0);
  animation: jmkSparkPop 900ms var(--ease-confident, cubic-bezier(.2,.7,.2,1)) both;
  /* Drawn over a solid button, so it needs to read on both the pink and
     the ink it might sit against. */
  filter: drop-shadow(0 0 3px rgba(255,255,255,.55));
}
@keyframes jmkSparkPop {
  0%   { transform: translate(-50%, -50%) scale(0) rotate(0deg); opacity: 0; }
  22%  { transform: translate(-50%, -50%) scale(1) rotate(calc(var(--spark-spin) * .4)); opacity: 1; }
  70%  { opacity: 1; }
  100% {
    transform: translate(calc(-50% + var(--spark-drift)), calc(-50% - 18px))
               scale(.25) rotate(var(--spark-spin));
    opacity: 0;
  }
}
@media (prefers-reduced-motion: reduce) { .jmk-spark__star { display: none; } }

/* --- the contact section, held to one screen — see Contact.jsx --------
   Only where `fit` is asked for, which is the home page. The section is
   exactly the window and the form column scrolls inside it; the page
   underneath keeps its rhythm and the form keeps all of its fields. */
.jmk-fit { height: 100svh; min-height: 100svh; }
/* Centred, not stretched.
   Stretching made both columns start at the top of the screen and leave
   whatever was left over as a band of pink underneath — which on the
   first step of a three-step form is most of the section. Centred, the
   block sits in the middle of its own screen whichever step is showing
   and the section reads as composed rather than as top-loaded. */
.jmk-fit .jmk-fit__grid { height: 100%; min-height: 0; align-items: center; }
/* THE INNER SCROLLER HAS GONE.
   ----------------------------------------------------------------------
   `.jmk-fit__scroll` gave the form column its own overflow and a button
   that scrolled it. It fit the section to a screen and broke the page
   doing it: a scrolling box inside a scrolling page means one wheel
   gesture does two different things depending on where the pointer is,
   and over the form the page looks stuck. The form pages now — see
   .jmk-pgform below — and the section is simply as tall as its tallest
   page, which is shorter than a screen.
   ---------------------------------------------------------------------- */

/* The paged form.
   ----------------------------------------------------------------------
   One step visible at a time, and every field still in the document — the
   steps are hidden, never unmounted, so the submission carries the whole
   form however it is sent. `hidden` is respected by [hidden] below rather
   than left to the browser's default, because the step containers are
   flex/grid parents and `display: flex` on a hidden element wins over the
   UA's `display: none`. */
.jmk-pgform__step { display: flex; flex-direction: column; gap: 28px; }
/* !important, and it is earning it.
   `hidden` is a UA rule (`[hidden] { display: none }`) and loses to any
   author rule that sets display — including an inline `display: flex` on
   the element itself, which is how the submit row is written. Without
   this the submit button sat on every step of the form. */
.jmk-pgform [hidden],
.jmk-pgform__step[hidden] { display: none !important; }

/* Enough room for the tallest step, so the panel does not resize under
   the reader as they page through it — a form that jumps two hundred
   pixels every time you press Next reads as the layout breaking. */
.jmk-pgform { min-height: min(60svh, 460px); }

.jmk-pgform__bar {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding-top: 18px;
  border-top: 1px solid rgba(17, 17, 17, 0.18);
}
.jmk-pgform__dots { display: flex; gap: 6px; }
.jmk-pgform__dot {
  width: 7px; height: 7px; border-radius: 999px;
  background: rgba(17, 17, 17, 0.28);
  transition: width 320ms var(--ease-confident, ease), background 240ms ease;
}
.jmk-pgform__dot.on { width: 20px; background: var(--ink-900); }
.jmk-pgform__where {
  font-family: var(--font-ui); font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wide); text-transform: uppercase;
  font-weight: 600; color: rgba(17, 17, 17, 0.62);
}
.jmk-pgform__next,
.jmk-pgform__back {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 11px 20px; border-radius: var(--radius-button);
  font-family: var(--font-ui); font-weight: 600; font-size: var(--text-sm);
  cursor: pointer;
  transition: background 220ms ease, transform 260ms var(--ease-confident, ease);
}
.jmk-pgform__next {
  border: 0; background: var(--ink-900); color: var(--white);
  box-shadow: 0 10px 26px -12px rgba(0, 0, 0, 0.55);
}
.jmk-pgform__next:hover { transform: translateY(-2px); }
.jmk-pgform__back {
  border: 1px solid rgba(17, 17, 17, 0.28); background: transparent;
  color: var(--ink-900);
}
.jmk-pgform__back:hover { background: rgba(255, 255, 255, 0.5); }
.jmk-pgform__next:focus-visible,
.jmk-pgform__back:focus-visible { outline: 2px solid var(--ink-900); outline-offset: 3px; }

@media (prefers-reduced-motion: reduce) {
  .jmk-pgform__dot { transition: none; }
  .jmk-pgform__next:hover { transform: none; }
}

/* A short window has no room to scroll a form in. Below this the section
   goes back to being as tall as it needs to be — a 380px-high browser is
   somebody with a keyboard open, and a form in a 380px box is unusable. */
@media (max-height: 560px) {
  .jmk-fit { height: auto; min-height: 0; }
  .jmk-fit .jmk-fit__grid { height: auto; }
  .jmk-pgform { min-height: 0; }
}

/* --- the marquee holds, and the next section covers it ----------------
   A full screen of moving type, second on the page. Left to scroll away
   it reads as a band that went past; held still while the section after
   it pulls over the top, it reads as a card being dealt onto it.

   top: 0 is right HERE and nowhere else on this site: the section is
   exactly one screen, so pinning it by the top pins the whole of it and
   there is nothing below the fold to be stranded. Sections taller than
   the window need the measured offset the Curtain uses instead.

   The section that follows needs a stacking order to ride over this one.
   `position: relative` is what gives it one — without it z-index does
   nothing and the marquee, painted later in the flow, stays on top. */
.jmk-kin--hold {
  position: sticky;
  top: 0;
  z-index: 0;
  height: 100svh;
  min-height: 100svh;
  overflow: hidden;
}
/* EVERY section after it, not just the next one.
   The marquee is sticky for the whole length of the page, so anything
   below it that has no stacking order of its own paints UNDERNEATH it —
   and the marquee showed through at the edges of every section further
   down, giant letters drifting behind the copy. Only the section
   immediately after it had been given a z-index, which fixed exactly one
   handover and left the other ten broken. */
.jmk-kin--hold ~ * {
  position: relative;
  z-index: 1;
}
/* The edge of the thing coming over belongs to the one that covers it,
   so the handover is visible rather than the next section simply being
   there. Only the first: the rest are handing over to each other, not to
   the marquee. */
.jmk-kin--hold + * {
  box-shadow: 0 -18px 44px -18px rgba(0,0,0,.42);
}

@media (prefers-reduced-motion: reduce) {
  /* Still stacked, nothing pinned: a section that holds while another
     slides over it is exactly the effect this request is about. */
  .jmk-kin--hold { position: relative; }
}
@media print { .jmk-kin--hold { position: static; height: auto; min-height: 0; } }

/* ======================================================================
   Method Behind the Madness — stacked, stuck, and tilting
   ----------------------------------------------------------------------
   Overrides the rail this section used to be. It was a pinned stage with
   cards travelling sideways, which is a good pattern and the wrong one
   here: a step of a method is something you finish before the next one
   starts, and sideways travel says "there is more of this" rather than
   "that was one, here is two".

   Each card is a screen that sticks, and the next comes over the top of
   it. Later cards carry higher z-indexes from script, so the arriving one
   paints over the held one for nothing.

   top: 0 is safe here where it is not elsewhere on this site: every card
   is exactly one screen, so pinning by the top pins the whole of it and
   there is nothing below the fold to be stranded.
   ====================================================================== */
.jmk-pil--stack { display: block; }

.jmk-pil--stack .jmk-pil__intro {
  position: sticky; top: 0;
  height: 100svh;
  display: flex; flex-direction: column; justify-content: center;
  z-index: 0;
}
.jmk-pil--stack .jmk-pil__head { padding: 0 clamp(20px, 5vw, 64px); }
.jmk-pil--stack .jmk-pil__heading {
  /* It is the only thing on its screen now, so it can be the size the
     claim deserves rather than the size that fitted above a rail. */
  font-size: clamp(34px, 6vw, 86px);
  max-width: 14ch;
}

.jmk-pil--stack .jmk-pil__card {
  position: sticky; top: 0;
  width: auto;
  height: 100svh;
  border-radius: 0;
  border: 0;
  border-top: 1px solid var(--color-border, #E4E4E4);
  align-items: center;
  justify-content: center;
  /* The tilt needs somewhere to happen. perspective on the card itself
     would apply to its children and leave the card flat, so it goes on
     the section and the card is what rotates inside it. */
  transform-style: preserve-3d;
}
.jmk-pil--stack { perspective: 1400px; }

.jmk-pil--stack .jmk-pil__body {
  transform: rotateX(var(--tx, 0deg)) rotateY(var(--ty, 0deg));
  transform-style: preserve-3d;
  transition: transform 320ms var(--ease-confident, cubic-bezier(.2,.7,.2,1));
  width: min(92vw, 900px);
  margin-inline: auto;
  padding: clamp(28px, 5vh, 64px) clamp(24px, 5vw, 64px);
  border-radius: clamp(18px, 1.6vw, 28px);
  background: var(--color-surface, #fff);
  border: 1px solid var(--color-border, #E4E4E4);
  box-shadow: 0 30px 80px -40px rgba(0,0,0,.4);
  position: relative;
}
/* The sheen, following the pointer. Behind the words, never over them. */
.jmk-pil--stack .jmk-pil__body::before {
  content: "";
  position: absolute; inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: radial-gradient(
    460px circle at var(--lx, 50%) var(--ly, 50%),
    rgba(var(--hue), 0.14),
    transparent 62%
  );
  opacity: 0;
  transition: opacity 300ms ease;
}
.jmk-pil--stack .jmk-pil__card:hover .jmk-pil__body::before { opacity: 1; }

.jmk-pil--stack .jmk-pil__title {
  font-size: clamp(26px, 3.6vw, 50px);
}
.jmk-pil--stack .jmk-pil__text { max-width: 62ch; }

/* --- the working, one animated mark per line ------------------------- */
.jmk-pil__how {
  list-style: none;
  margin: clamp(20px, 3vh, 34px) 0 0;
  padding: 0;
  display: grid;
  gap: clamp(10px, 1.6vh, 16px);
}
.jmk-pil__how li {
  display: flex; align-items: center; gap: 12px;
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  color: var(--ink-700, #444);
  /* Dealt in rather than arriving together, so the three read as a list
     being written rather than as a block appearing. */
  animation: jmkHowIn 520ms var(--ease-confident, ease) both;
  animation-delay: calc(var(--k, 0) * 110ms + 160ms);
}
@keyframes jmkHowIn { from { opacity: 0; transform: translateX(-10px); } }
.jmk-pil__howmark {
  flex: 0 0 auto;
  width: 30px; height: 30px;
  display: grid; place-items: center;
  border-radius: 9px;
  background: rgba(var(--hue), 0.12);
  color: rgb(var(--hue));
}
.jmk-pil__howmark .jmk-glyph { width: 17px; height: 17px; }

@media (max-width: 768px) {
  .jmk-pil--stack .jmk-pil__body { padding: clamp(22px, 4vh, 40px) 20px; }
  .jmk-pil__how li { font-size: var(--text-xs); }
}
@media (prefers-reduced-motion: reduce) {
  .jmk-pil--stack .jmk-pil__body { transform: none !important; transition: none; }
  .jmk-pil--stack .jmk-pil__body::before { display: none; }
  .jmk-pil__how li { animation: none; }
}
@media print {
  .jmk-pil--stack .jmk-pil__card,
  .jmk-pil--stack .jmk-pil__intro { position: static; height: auto; }
}

/* ======================================================================
   Method Behind the Madness — one deck on one screen
   ----------------------------------------------------------------------
   Replaces the three-screens-in-a-row version. Each step used to be a
   section of its own that stuck while the next came over it, which spent
   four screens of a twenty-screen page on three short claims. The
   heading and the cards share one pinned screen now, the cards sit in
   depth behind one another, and the one in front changes as you scroll
   through the pin or on its own every five seconds.
   ====================================================================== */
.jmk-pil--deck { display: block; perspective: 1500px; }
/* The claim on the left, the evidence on the right, both on one screen.
   Stacked vertically the heading sat on top of the deck and the whole
   right half of the screen was empty — which on a section this short
   read as something having failed to load. Side by side, the title has
   somewhere to be and the deck has the room it needs. */
.jmk-pil--deck .jmk-pil__stage {
  position: sticky; top: 0;
  height: 100svh;
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  grid-template-areas: "head deck" "head dots";
  align-content: center; align-items: center;
  column-gap: clamp(28px, 5vw, 80px);
  row-gap: clamp(14px, 2.4vh, 26px);
  padding: clamp(24px, 5vh, 56px) clamp(20px, 5vw, 64px);
  overflow: hidden;
}
.jmk-pil--deck .jmk-pil__head { grid-area: head; padding: 0; align-self: center; }
.jmk-pil--deck .jmk-pil__deck { grid-area: deck; }
.jmk-pil--deck .jmk-pil__dots { grid-area: dots; justify-self: center; }
.jmk-pil--deck .jmk-pil__heading {
  font-size: clamp(30px, 4.6vw, 68px);
  max-width: 12ch;
}

/* One column below the fold of a laptop — two columns on a phone is two
   half-columns, and the deck needs the width more than the title does. */
@media (max-width: 900px) {
  .jmk-pil--deck .jmk-pil__stage {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "head" "deck" "dots";
    align-content: center;
    row-gap: clamp(16px, 3vh, 30px);
  }
  .jmk-pil--deck .jmk-pil__head { text-align: center; }
  .jmk-pil--deck .jmk-pil__heading { max-width: 18ch; margin-inline: auto; }
}

/* The deck is one grid cell with every card stacked into it, so it takes
   the height of the tallest and nothing below it moves as they change. */
.jmk-pil--deck .jmk-pil__deck {
  display: grid;
  width: 100%;
  transform-style: preserve-3d;
  flex: 0 1 auto;
  min-height: 0;
}
.jmk-pil--deck .jmk-pil__slot {
  grid-area: 1 / 1;
  transform-origin: 50% 100%;
  transition:
    transform 620ms var(--ease-confident, cubic-bezier(.2,.7,.2,1)),
    opacity 620ms var(--ease-confident, cubic-bezier(.2,.7,.2,1));
  will-change: transform, opacity;
}
.jmk-pil--deck .jmk-pil__card {
  position: relative; height: auto; width: auto;
  border: 0; border-radius: 0; background: none;
  display: block;
}
.jmk-pil--deck .jmk-pil__body {
  transform: rotateX(var(--tx, 0deg)) rotateY(var(--ty, 0deg));
  transform-style: preserve-3d;
  transition: transform 320ms var(--ease-confident, cubic-bezier(.2,.7,.2,1));
  padding: clamp(24px, 4vh, 48px) clamp(22px, 4vw, 52px);
  border-radius: clamp(18px, 1.6vw, 28px);
  background: var(--color-surface, #fff);
  border: 1px solid var(--color-border, #E4E4E4);
  box-shadow: 0 30px 80px -40px rgba(0,0,0,.42);
  position: relative;
}
.jmk-pil--deck .jmk-pil__body::before {
  content: "";
  position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  background: radial-gradient(420px circle at var(--lx,50%) var(--ly,50%),
              rgba(var(--hue), 0.14), transparent 62%);
  opacity: 0; transition: opacity 300ms ease;
}
.jmk-pil--deck .jmk-pil__card:hover .jmk-pil__body::before { opacity: 1; }
.jmk-pil--deck .jmk-pil__title { font-size: clamp(24px, 3.2vw, 44px); }
.jmk-pil--deck .jmk-pil__text { max-width: 60ch; }

.jmk-pil__dots { display: flex; gap: 10px; flex: 0 0 auto; }
.jmk-pil__dot {
  width: 9px; height: 9px; padding: 0; border: 0; border-radius: 999px;
  background: var(--ink-900); opacity: .2; cursor: pointer;
  transition: opacity 220ms ease, transform 220ms ease, background-color 220ms ease;
}
.jmk-pil__dot:hover { opacity: .5; }
.jmk-pil__dot.on { opacity: 1; background: var(--color-primary); transform: scale(1.35); }
.jmk-pil__dot:focus-visible { outline: 2px solid var(--ink-900); outline-offset: 3px; }

@media (prefers-reduced-motion: reduce) {
  .jmk-pil--deck .jmk-pil__slot,
  .jmk-pil--deck .jmk-pil__body { transition: none; transform: none !important; }
  .jmk-pil--deck .jmk-pil__body::before { display: none; }
}
@media print {
  .jmk-pil--deck .jmk-pil__stage { position: static; height: auto; }
  .jmk-pil--deck .jmk-pil__slot { grid-area: auto; transform: none !important; opacity: 1 !important; }
  .jmk-pil__dots { display: none; }
}

/* ======================================================================
   Nothing shows through — every section on the home page fills a screen
   ----------------------------------------------------------------------
   The kinetic marquee is sticky for the whole length of the page, so any
   section shorter than the window, or with a transparent strip between
   it and the next, is a window onto giant drifting letters behind the
   copy. The paint dividers were exactly that and have gone; this closes
   the other half of it.

   min-height rather than height: a section with more in it than fits is
   still allowed to be taller. What it may not be is shorter.

   Scoped to the home page's own sections. The inner pages set their own
   heights, and the CV has a whole grammar of its own.
   ====================================================================== */
body.jmk-home #main > section {
  min-height: 100svh;
  /* An opaque ground of its own. A section that inherits the page's is
     transparent to anything painted underneath it, which is the same
     hole by another route. */
  background-color: var(--color-surface, #fff);
  position: relative;
}
/* The ones that set their own colour keep it — this only fills in for
   the sections that were relying on the page behind them. */
body.jmk-home #main > section[style*="background"] { background-color: unset; }

/* NOT `> * { position: relative }`.
   ----------------------------------------------------------------------
   That rule used to be here, to lift each section's contents above the
   sticky marquee. It was never needed — the section itself carries the
   z-index (see `.jmk-kin--hold ~ *`), which makes it a stacking context,
   and everything inside a stacking context paints inside it. What the
   rule DID do was overwrite `position: sticky` on any direct child that
   had it, at a specificity nothing in a component could win back. That
   silently unpinned the Method deck's stage: the pin stopped happening,
   the cards dealt themselves over three thousand pixels of scroll, and
   what was left was a very tall blue section with a card in the top
   third of it. Which is the dead space. */

/* EXCEPT the marquee, which is the whole reason this block exists.
   ----------------------------------------------------------------------
   The rule above sets `position: relative` on every section so its
   z-index and its opaque ground both take, and it is more specific than
   `.jmk-kin--hold` — so it silently turned the marquee's `sticky` into
   `relative` and the section it was meant to hold under simply scrolled
   past it. The hold stopped working the day the fullscreen block landed,
   which is exactly what "the marquee isn't revealing as static" is.
   It has to be re-stated here, at the same specificity, to win it back. */
body.jmk-home #main > section.jmk-kin--hold {
  position: sticky;
  top: 0;
  z-index: 0;
}
@media (prefers-reduced-motion: reduce) {
  body.jmk-home #main > section.jmk-kin--hold { position: relative; }
}

/* And edge to edge. Ten of the home page's sections were carrying a 20px
   side margin from somewhere no stylesheet in this repo admits to — no
   author rule matches them and their inline styles set no margin — and
   that gutter was a strip of the sticky marquee showing down both sides
   of every one of them. Rather than keep hunting for a rule that does not
   appear to exist, it is closed here where the sections are already being
   held to a screen. */
/* ----------------------------------------------------------------------
   One rounded corner rule for the whole page, and no gaps at all.
   ----------------------------------------------------------------------
   The sheet system rounds and insets `.jmk-stage`, which five of this
   page's fifteen sections do not carry — so a third of the handovers had
   a square edge and the rest had a curved one, which is not a rhythm,
   it is an oversight. And the sheets kept a 20px top margin here, which
   on a page with a sticky marquee behind everything meant that gap was a
   pink band showing between two white sections.

   So: no margins, and the same curve on every section. A section at rest
   fills the screen edge to edge — no gutter, no band, nothing of the
   thing behind it. The curve is only ever read during the handover,
   where it is the point: one sheet pulling over the one before it, which
   is the same grammar the CV uses and the reason the corners are there.

   Top corners only. A section's foot is always covered by the next one
   arriving over it, so rounding it is 34 pixels of curve that nobody can
   ever see — except at the very bottom of the page, where a curve above
   the footer reads as the page having come unstuck.
   ---------------------------------------------------------------------- */
body.jmk-home #main > section {
  margin: 0;
  border-radius: var(--sheet-radius) var(--sheet-radius) 0 0;
}
/* The hero is the top of the document. There is nothing above it for a
   curve to reveal, and a rounded top under a fixed nav is a gap. */
body.jmk-home #main > section:first-child {
  border-radius: 0;
}
/* And every section overlaps the one before it by exactly its own curve.
   ----------------------------------------------------------------------
   Without this the curve cuts a notch out of the top corners and what
   shows through the notch is whatever is furthest back — which on this
   page is the sticky pink marquee, three sections up and still pinned.
   A slice of pink in the corner of a lime section is not a handover, it
   is a hole.

   Pulled up by the radius, the notch sits over the previous section
   instead, so the corner reveals the thing it is actually being handed
   from. That is the whole point of the curve, and it is the only reading
   of it that makes sense at any scroll position. */
body.jmk-home #main > section + section {
  margin-top: calc(-1 * var(--sheet-radius));
}

/* ======================================================================
   The home grid's tiles — hover plays the rest of the category
   ----------------------------------------------------------------------
   See WorkCycle in Work.jsx. The frames stack over the tile's own
   picture and cross-fade; the one on screen pushes in slowly while it is
   there, so a sequence of stills reads as footage rather than as a
   slideshow clicking over.
   ====================================================================== */
.jmk-workcycle {
  position: absolute; inset: 0;
  display: block;
  pointer-events: none;
  overflow: hidden;
  border-radius: inherit;
}
.jmk-workcycle img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
  transform: scale(1.02);
  transition: opacity 520ms var(--ease-confident, cubic-bezier(.2,.7,.2,1));
}
/* The slow push in, on the frame that is showing. The animation runs a
   touch longer than the frame is held, so it is never seen to stop and
   restart — what you get is continuous drift for as long as you hover. */
.jmk-workcycle img[data-on="true"] {
  opacity: 1;
  animation: jmkKen 4.2s linear both;
}
@keyframes jmkKen {
  from { transform: scale(1.02); }
  to   { transform: scale(1.16); }
}

/* The emboss: the tile lifts off the page while it is playing, and the
   picture under the frames drifts too, so the whole tile is moving
   rather than a rectangle inside a rectangle. */
.jmk-worktile .jmk-figure {
  transition:
    transform 420ms var(--ease-confident, cubic-bezier(.2,.7,.2,1)),
    box-shadow 420ms var(--ease-confident, cubic-bezier(.2,.7,.2,1));
  will-change: transform;
}
.jmk-worktile:hover .jmk-figure,
.jmk-worktile:focus-visible .jmk-figure {
  transform: translateY(-6px) scale(1.015);
  box-shadow: 0 26px 60px -26px rgba(0,0,0,.55);
}
.jmk-worktile:hover .jmk-figure__inner > img,
.jmk-worktile:focus-visible .jmk-figure__inner > img {
  animation: jmkKen 6s linear both;
}

@media (prefers-reduced-motion: reduce) {
  .jmk-workcycle { display: none; }
  .jmk-worktile .jmk-figure { transition: none; }
  .jmk-worktile:hover .jmk-figure { transform: none; }
  .jmk-worktile:hover .jmk-figure__inner > img { animation: none; }
}

/* ====================================================================
   The moving still
   ====================================================================
   A short preview file — a GIF, or a muted clip — laid over a video
   tile's poster while the pointer rests on it. Over rather than instead
   of, so a preview that fails to load leaves the poster where it was,
   and faded in rather than cut to, because a hard swap on hover reads as
   the image changing rather than as the work starting to play.
   ==================================================================== */
.jmk-vt__peek {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  border: 0;
  pointer-events: none;
  z-index: 1;
  opacity: 0;
  animation: jmkPeekIn 0.3s ease forwards;
}

@keyframes jmkPeekIn {
  to { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .jmk-vt__peek { animation: none; opacity: 1; }
}

/* ====================================================================
   The word that noticed you
   ====================================================================
   "Box." leans towards the pointer and bobs on its own. Two nested
   elements because a CSS animation and an inline transform are the same
   property and the animation wins — one node leans, the other bobs.

   The lean is written from script as --bx/--by/--br so a pointermove
   never re-renders React; the transition is what turns those discrete
   jumps into a follow rather than a twitch.
   ==================================================================== */
.jmk-boxlean {
  transform: translate3d(var(--bx, 0px), var(--by, 0px), 0) rotate(var(--br, 0deg));
  transition: transform 420ms cubic-bezier(.22, 1, .36, 1);
  will-change: transform;
}

.jmk-boxbob {
  animation: jmkBoxBob 5.5s ease-in-out infinite;
}

@keyframes jmkBoxBob {
  0%, 100% { transform: translate3d(0, 0, 0) rotate(0deg); }
  33%      { transform: translate3d(0, -9px, 0) rotate(0.7deg); }
  66%      { transform: translate3d(0, 5px, 0) rotate(-0.5deg); }
}

@media (prefers-reduced-motion: reduce) {
  .jmk-boxlean { transition: none; transform: none; }
  .jmk-boxbob { animation: none; }
}

/* ====================================================================
   Our Expertise — a mark where the number was
   ====================================================================
   A small chip that holds the drawn glyph, sized to the 60px column the
   list already reserves. It picks up the row's accent on hover so the
   mark, the discipline name and the row's indent all answer the pointer
   together rather than as three separate reactions.
   ==================================================================== */
.jmk-exp__mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: clamp(38px, 4vw, 48px);
  height: clamp(38px, 4vw, 48px);
  padding: 9px;
  border-radius: 50%;
  border: 1px solid var(--color-border, #e6e6e6);
  color: var(--ink-500, #666);
  background: var(--color-surface, #fff);
  flex: 0 0 auto;
  transition: color 240ms var(--ease-confident, ease),
              border-color 240ms var(--ease-confident, ease),
              transform 320ms var(--ease-confident, ease);
}
.jmk-exp__mark.is-on {
  color: var(--color-primary, #ff62b1);
  border-color: var(--color-primary, #ff62b1);
  transform: scale(1.08);
}
.jmk-exp__mark .jmk-glyph { stroke-width: 2.2; }

/* ====================================================================
   The Method — its own room
   ====================================================================
   White cards on a white ground with a white section above and a white
   section below is four screens of the same page. The deck stands on
   blue now: the cards read as objects laid on something rather than as
   panels cut out of the background, and the section announces itself as
   a different room the moment its top edge appears.

   Blue rather than any of the other grounds because the section either
   side of it on both pages it appears on is a warm off-white, and this
   is the one hue in the house palette that is unmistakably not that.
   ==================================================================== */
.jmk-pil,
body.jmk-home #main > section.jmk-pil {
  background-color: var(--blue-500, #299CDF);
}
/* The heading is on the blue, not on a card, so it takes the ink that
   reads against it. */
.jmk-pil .jmk-pil__heading { color: var(--ink-900, #111); }
.jmk-pil .jmk-pil__dot { background: rgba(17, 17, 17, 0.28); }
.jmk-pil .jmk-pil__dot.on { background: var(--ink-900, #111); }

/* ====================================================================
   The home grid's tiles say what they are, on the picture
   ====================================================================
   The same move the CV's gallery makes: the label belongs inside the
   frame, over a gradient dark enough to carry white type on any
   photograph, rather than in a caption under it. A caption under a tile
   is a second element to align, a second thing to pack the masonry
   around, and — on a tile that shuffles a whole category through itself
   on hover — a name for something that is no longer on screen.

   Always visible rather than on hover. On a phone there is no hover and
   the label is the only thing telling you what the picture is.
   ==================================================================== */
.jmk-worktile__tag {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 3;
  padding: 42px 16px 14px;
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--white, #fff);
  background: linear-gradient(0deg, rgba(0, 0, 0, 0.72) 0%, rgba(0, 0, 0, 0.34) 46%, transparent 100%);
  pointer-events: none;
}

/* ====================================================================
   The partner deck, levelled
   ====================================================================
   This section is pinned for its whole run, so it is only ever seen as
   one full screen — and it was laying itself out as though it were a
   band in a scrolling page: content packed against the top, a couple of
   hundred pixels of nothing under it, the deck floating clear of the
   sentence it belongs to.

   Filling the pinned screen and centring what is in it fixes both at
   once. The two columns then share a middle line, which is what makes
   the claim and the evidence read as one statement rather than as two
   things that happen to be on the same screen.
   ==================================================================== */
.pstack-host {
  display: grid;
  align-content: center;
  min-height: 100svh;
  /* The deck fans. On a narrow screen the outermost card in the fan
     reaches past the section's edge, and with nothing clipping it that
     is eighteen pixels of horizontal scroll on the whole document — a
     phone that rocks sideways for a card nobody can see. The section is
     the frame the deck is dealt inside, so it is the thing that clips. */
  overflow: hidden;
}
.pstack-host > * {
  align-content: center;
  width: 100%;
}
/* The deck's own middle, against the words' own middle. `align-items:
   center` centres each row's contents; this centres the whole spanned
   cell, which is the one that has to line up. */
.pstack-host > * > .pstack {
  align-self: center;
  justify-self: stretch;
}
@media (max-width: 900px) {
  .pstack-host { align-content: center; }
}

/* =====================================================================
   The media mix network — /our-work/paid-media
   =====================================================================
   Twelve nodes and twenty lines in one SVG. Everything that moves is a
   transform or an opacity, so none of it touches layout, and the pointer
   pull comes in as two custom properties written by rAF rather than as
   React state — see MediaMix.jsx.
   ===================================================================== */
/* Copy left, board right, above 1024. See the note in MediaMix.jsx. */
.jmk-mix__grid {
  display: grid;
  gap: clamp(32px, 4vw, 64px);
  align-items: center;
  max-width: 1400px;
  margin: 0 auto;
}
@media (min-width: 1025px) {
  .jmk-mix__grid { grid-template-columns: minmax(0, 0.85fr) minmax(0, 1fr); }
}
.jmk-mix__grid > :first-child { max-width: 640px; }

.jmk-mix__board {
  --mx: 0;
  --my: 0;
  position: relative;
  width: 100%;
  max-width: 1080px;
  /* Real clearance above, not the 22 pixels the heading block's own
     margin left. The board enters on a Reveal that travels upward, so a
     gap smaller than that travel puts the top row of nodes through the
     last line of the paragraph for the length of the animation — settled
     correctly, wrong the whole way there. */
  margin: 0 auto;
  /* SQUARE, and capped. The viewBox is 100x100 and preserveAspectRatio
     meets it — so in a 16/11 box the drawing was letterboxed into the
     middle square and the two side strips were dead board, which is why
     the network looked cramped and the labels ran into each other. A
     square box means the positions in media-platforms.js mean what they
     say. Capped at 820 because a square that fills a desktop is a screen
     and a half of one diagram. */
  max-width: min(720px, 100%);
  aspect-ratio: 1 / 1;
}
.jmk-mix__board svg { width: 100%; height: 100%; overflow: visible; }

/* --- the wires --------------------------------------------------- */
.jmk-mix__wire {
  /* Quieter than they were. The board carries sixteen tools and sixty-odd
     joins now rather than twelve and twenty, and at the old weight that
     many lines read as a mesh with logos caught in it rather than as
     tools that happen to be connected. The lines are the argument, but
     they are the argument in the background. */
  stroke: rgba(255, 255, 255, 0.11);
  stroke-width: 0.16;
  transition: stroke 420ms ease, stroke-width 420ms ease, opacity 420ms ease;
}
.jmk-mix__wire.is-on {
  stroke: var(--color-primary, #ff62b1);
  stroke-width: 0.45;
}
.jmk-mix__wire.is-dim { opacity: 0.22; }

/* --- the nodes ---------------------------------------------------- */
.jmk-mix__node {
  cursor: pointer;
  /* NO transform-box or transform-origin here. This element is positioned
     by its `transform` attribute; anything that touches the transform
     property on it moves the node off its own coordinates. The lean
     below uses `translate`, which is a separate property and composes
     with the attribute rather than replacing it. */
  transition: opacity 420ms ease;
}
.jmk-mix__drift * {
  transition: transform 520ms cubic-bezier(.16,1,.3,1), opacity 420ms ease,
              fill 420ms ease, stroke 420ms ease;
}
.jmk-mix__node.is-dim { opacity: 0.32; }
.jmk-mix__node:focus { outline: none; }
.jmk-mix__node:focus-visible .jmk-mix__disc {
  stroke: var(--white, #fff);
  stroke-width: 0.6;
}

.jmk-mix__halo {
  fill: var(--color-primary, #ff62b1);
  opacity: 0;
  transition: opacity 420ms ease, transform 520ms cubic-bezier(.16,1,.3,1);
}
.jmk-mix__node.is-on .jmk-mix__halo { opacity: 0.18; }

.jmk-mix__disc {
  fill: #141416;
  stroke-width: 0.3;
  transition: transform 520ms cubic-bezier(.16,1,.3,1), fill 420ms ease;
}
.jmk-mix__node.is-on .jmk-mix__disc { fill: #1e1e22; }

.jmk-mix__short {
  fill: rgba(255, 255, 255, 0.9);
  font-family: var(--font-ui, system-ui);
  font-size: 3.4px;
  font-weight: 600;
}
.jmk-mix__name {
  fill: rgba(255, 255, 255, 0.62);
  font-family: var(--font-ui, system-ui);
  font-size: 2.5px;
  letter-spacing: 0.04em;
  transition: fill 420ms ease;
}
.jmk-mix__node.is-on .jmk-mix__name { fill: var(--white, #fff); }

/* --- a node that carries a logo --------------------------------------
   A WHITE DISC, and only where there is a mark to put on it. Brand
   artwork is drawn for a light ground: half of these are dark or nearly
   black, and recolouring them all to one flat white throws away the
   thing that makes a logo recognisable. A white circle with the group's
   colour as its ring lets every mark be its own colours, which is also
   what most of these trademark guidelines actually ask for.

   Scoped to .has-logo rather than applied to every disc, because the
   paid-media board draws lettered discs — white type on near-black —
   and a white fill there would erase them. Both boards are this one
   component; the difference is per node, so the rule is too. */
.jmk-mix__node.has-logo .jmk-mix__disc { fill: #ffffff; }
.jmk-mix__node.has-logo.is-on .jmk-mix__disc { fill: #ffffff; }
/* The lit ring, which is what the glow in the reference is doing. */
.jmk-mix__node.has-logo .jmk-mix__halo { fill: #ffffff; }

/* --- the drift, and the pull ------------------------------------- */
@keyframes jmkMixFloat {
  0%,  100% { transform: translate(0, 0); }
  50%       { transform: translate(0.5px, -0.9px); }
}
/* The drift lives on the inner group, whose transform is free. */
.jmk-mix__board[data-still="false"] .jmk-mix__drift {
  animation: jmkMixFloat calc(7s + var(--n) * 0.65s) ease-in-out infinite;
  animation-delay: calc(var(--n) * -0.9s);
}
/* The lean lives on the positioned node, as `translate` — a different
   property from `transform`, so the position attribute survives it. */
.jmk-mix__board[data-still="false"] .jmk-mix__node {
  translate: calc(var(--mx) * 1.1px) calc(var(--my) * 1.1px);
  transition: translate 900ms cubic-bezier(.16,1,.3,1), opacity 420ms ease;
}
.jmk-mix__board[data-still="false"] .jmk-mix__node.is-on {
  translate: calc(var(--mx) * 2.2px) calc(var(--my) * 2.2px);
}
/* Both circles are centred on the node's own origin, so scaling about
   0 0 IS scaling about their middle. `transform-origin: center` would
   resolve against the whole viewBox and fling them across the board. */
.jmk-mix__node.is-on .jmk-mix__disc,
.jmk-mix__node.is-on .jmk-mix__halo { transform: scale(1.18); transform-origin: 0 0; }

/* --- the readout -------------------------------------------------- */
.jmk-mix__read {
  max-width: 640px;
  margin: clamp(20px, 2.4vw, 32px) auto 0;
  /* Held open so the section does not jump every time the pointer
     crosses a node. Two lines is the longest note in the file. */
  min-height: 5.2em;
  text-align: center;
}
.jmk-mix__read p {
  font-family: var(--font-ui);
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
  color: rgba(255, 255, 255, 0.74);
  margin: 0;
  animation: jmkMixSay 460ms cubic-bezier(.16,1,.3,1) both;
}
.jmk-mix__read b { color: var(--white); font-weight: 600; }
.jmk-mix__prompt { color: rgba(255, 255, 255, 0.38) !important; font-style: italic; }
@keyframes jmkMixSay {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

@media (max-width: 720px) {
  .jmk-mix__short { font-size: 4.2px; }
  .jmk-mix__name { font-size: 3.1px; }
  .jmk-mix__read { min-height: 7em; }
}

@media (prefers-reduced-motion: reduce) {
  .jmk-mix__node,
  .jmk-mix__drift,
  .jmk-mix__drift *,
  .jmk-mix__wire { animation: none !important; transition: none !important; }
  .jmk-mix__read p { animation: none; }
}

/* =====================================================================
   Flat scenes — see Scenes.jsx
   =====================================================================
   Everything here animates transform or opacity and nothing else, and
   every one of them ENDS ON ITS LAST FRAME under reduced motion rather
   than on its first. A story that stops at the beginning is not a still
   illustration, it is a broken one — the feed scene in particular would
   hold on four grey placeholder cards and never show the ad, which is
   the only thing it exists to show.
   ===================================================================== */
.jmk-scene { display: block; width: 100%; height: auto; }
.jmk-scene__tag {
  font-family: var(--font-ui, system-ui);
  font-size: 7px;
  font-weight: 600;
  letter-spacing: 0.04em;
}

/* --- the feed scroll ---------------------------------------------- */
@keyframes jmkFeedScroll {
  /* A thumb, not a conveyor. Four flicks with a rest between each, then
     the long hold on the card that stopped it. The percentages are
     uneven on purpose: even ones read as a machine scrolling. */
  0%,   7%   { transform: translateY(0); }
  14%,  21%  { transform: translateY(-150px); }
  28%,  34%  { transform: translateY(-300px); }
  41%,  47%  { transform: translateY(-450px); }
  56%, 100%  { transform: translateY(-624px); }
}
.jmk-scene--feed .jmk-scene__track {
  animation: jmkFeedScroll 11s cubic-bezier(.3,.9,.25,1) infinite;
}

/* The ad arrives at full strength; the posts above it never do. */
@keyframes jmkFeedAd {
  0%,  50%  { opacity: 0.55; }
  58%, 100% { opacity: 1; }
}
.jmk-scene--feed .jmk-scene__ad { animation: jmkFeedAd 11s ease-out infinite; }

/* The button is the last thing to land, after the card has settled. */
@keyframes jmkFeedCta {
  0%,  60%  { transform: scale(0.92); opacity: 0; }
  68%, 94%  { transform: scale(1);    opacity: 1; }
  100%      { transform: scale(1);    opacity: 1; }
}
.jmk-scene--feed .jmk-scene__cta {
  transform-origin: 224px 195px;
  animation: jmkFeedCta 11s cubic-bezier(.16,1,.3,1) infinite;
}

/* The thumb comes in for the flicks and leaves once the reader stops. */
@keyframes jmkFeedThumb {
  0%        { transform: translateY(120px); opacity: 0; }
  8%,  46%  { transform: translateY(0);     opacity: 1; }
  56%, 100% { transform: translateY(120px); opacity: 0; }
}
.jmk-scene--feed .jmk-scene__thumb {
  animation: jmkFeedThumb 11s cubic-bezier(.3,.9,.25,1) infinite;
}

/* Small life in the creative itself, so the ad is not a static rectangle
   the moment it arrives. */
@keyframes jmkSceneBlob {
  0%, 100% { transform: translate(0, 0) scale(1); }
  50%      { transform: translate(-4px, 5px) scale(1.05); }
}
.jmk-scene__blob { transform-origin: center; animation: jmkSceneBlob 9s ease-in-out infinite; }
@keyframes jmkSceneFigure {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-3px); }
}
.jmk-scene__figure { animation: jmkSceneFigure 6s ease-in-out infinite; }

@media (prefers-reduced-motion: reduce) {
  /* Held on the END of the story, not the start. */
  .jmk-scene *,
  .jmk-scene { animation: none !important; transition: none !important; }
  .jmk-scene--feed .jmk-scene__track { transform: translateY(-624px); }
  .jmk-scene--feed .jmk-scene__ad { opacity: 1; }
  .jmk-scene--feed .jmk-scene__cta { opacity: 1; transform: none; }
  .jmk-scene--feed .jmk-scene__thumb { opacity: 0; }
}

/* --- the site scene: heavy page, trimmed ---------------------------- */
@keyframes jmkSiteLoad {
  /* Slow, stalling, then instant. The stall in the middle is the point:
     a bar that fills smoothly reads as "loading", and a bar that hangs
     at seventy per cent reads as the thing everybody has actually sat
     through. */
  0%        { width: 0; }
  22%       { width: 96px; }
  40%       { width: 118px; }
  52%       { width: 130px; }
  58%       { width: 0; }
  74%, 100% { width: 248px; }
}
.jmk-scene--site .jmk-scene__load { animation: jmkSiteLoad 9s cubic-bezier(.5,0,.2,1) infinite; }

@keyframes jmkSiteDrop {
  0%,  56%  { opacity: 1; transform: translateY(0); }
  66%, 100% { opacity: 0; transform: translateY(14px); }
}
.jmk-scene--site .jmk-scene__field { animation: jmkSiteDrop 9s cubic-bezier(.5,0,.2,1) infinite; }
/* Staggered, so the form loses its fields one at a time rather than
   blinking out — three leaving together is a glitch, three leaving in
   sequence is a decision. */
.jmk-scene--site .jmk-scene__field[data-f="2"] { animation-delay: 0.18s; }
.jmk-scene--site .jmk-scene__field[data-f="3"] { animation-delay: 0.36s; }
.jmk-scene--site .jmk-scene__field[data-f="4"] { animation-delay: 0.54s; }
.jmk-scene--site .jmk-scene__field[data-f="5"] { animation-delay: 0.72s; }

@keyframes jmkSiteGo {
  0%,  66%  { opacity: 0; transform: translateY(10px); }
  76%, 100% { opacity: 1; transform: translateY(0); }
}
.jmk-scene--site .jmk-scene__go { animation: jmkSiteGo 9s cubic-bezier(.16,1,.3,1) infinite; }

@keyframes jmkSiteNumA { 0%, 62% { opacity: 1; } 70%, 100% { opacity: 0; } }
@keyframes jmkSiteNumB { 0%, 66% { opacity: 0; } 74%, 100% { opacity: 1; } }
.jmk-scene__num { font-family: var(--font-ui, system-ui); font-size: 20px; font-weight: 600; }
.jmk-scene__numlbl { font-family: var(--font-ui, system-ui); font-size: 8px; letter-spacing: 0.14em; }
.jmk-scene--site .jmk-scene__numA { animation: jmkSiteNumA 9s ease infinite; }
.jmk-scene--site .jmk-scene__numB { animation: jmkSiteNumB 9s ease infinite; }

/* --- the system scene: by hand, then not --------------------------- */
@keyframes jmkSysPipe {
  /* Drawn, not faded in. The line being made is the moment the section
     is describing. */
  0%,  46%  { stroke-dashoffset: 128; opacity: 0.35; }
  60%, 100% { stroke-dashoffset: 0;   opacity: 1; }
}
.jmk-scene--system .jmk-scene__pipe {
  stroke-dasharray: 128;
  animation: jmkSysPipe 10s cubic-bezier(.16,1,.3,1) infinite;
}

@keyframes jmkSysCarry {
  /* Two slow trips carried by hand — up, across, down — and then four
     quick ones straight along the line once it exists. */
  0%        { transform: translate(96px, 158px); opacity: 0; }
  4%        { transform: translate(96px, 158px); opacity: 1; }
  12%       { transform: translate(150px, 108px); }
  20%       { transform: translate(286px, 158px); }
  24%       { transform: translate(286px, 158px); opacity: 0; }
  25%       { transform: translate(96px, 158px); opacity: 0; }
  28%       { opacity: 1; }
  36%       { transform: translate(150px, 108px); }
  44%       { transform: translate(286px, 158px); }
  48%       { transform: translate(286px, 158px); opacity: 0; }
  /* the line lands here */
  62%       { transform: translate(120px, 163px); opacity: 0; }
  65%       { opacity: 1; }
  76%       { transform: translate(286px, 163px); opacity: 1; }
  79%       { transform: translate(286px, 163px); opacity: 0; }
  100%      { transform: translate(286px, 163px); opacity: 0; }
}
.jmk-scene--system .jmk-scene__rec { animation: jmkSysCarry 10s linear infinite; }
.jmk-scene--system .jmk-scene__rec[data-r="2"] { animation-delay: -1.4s; }
.jmk-scene--system .jmk-scene__rec[data-r="3"] { animation-delay: -2.8s; }

@keyframes jmkSysWorker {
  /* Steps back once the line exists. Does not leave — see the note in
     Scenes.jsx. */
  0%,  50%  { transform: translate(0, 0); }
  64%, 100% { transform: translate(-14px, 8px) scale(0.94); }
}
.jmk-scene--system .jmk-scene__worker {
  transform-origin: 211px 240px;
  animation: jmkSysWorker 10s cubic-bezier(.16,1,.3,1) infinite;
}

@media (prefers-reduced-motion: reduce) {
  /* Both held on the resolved end, same rule as the feed. */
  .jmk-scene--site .jmk-scene__load { width: 248px; }
  .jmk-scene--site .jmk-scene__field { opacity: 0; }
  .jmk-scene--site .jmk-scene__go { opacity: 1; transform: none; }
  .jmk-scene--site .jmk-scene__numA { opacity: 0; }
  .jmk-scene--site .jmk-scene__numB { opacity: 1; }
  .jmk-scene--system .jmk-scene__pipe { stroke-dashoffset: 0; opacity: 1; }
  .jmk-scene--system .jmk-scene__rec { opacity: 0; }
  .jmk-scene--system .jmk-scene__worker { transform: translate(-14px, 8px) scale(0.94); }
}


/* =====================================================================
   The web design cover flow — see WebFlow in PageWebDesign.jsx
   =====================================================================
   One rail, one card facing front, the rest angled behind it. Everything
   here is a transform or an opacity; the perspective lives on the rail
   so every card shares one vanishing point rather than each having its
   own, which is what stops the angled cards looking like they belong to
   different pictures.
   ===================================================================== */
/* The stage the rail turns inside.
   ---------------------------------------------------------------------
   A rail of absolutely-placed cards has no width of its own to be bounded
   by — only transforms — so the cards behind the front one went wherever
   the percentages sent them: over the description at desktop, and 731px
   past the right of the document in the tablet band, which gave the whole
   page horizontal scroll. This is the box they are not allowed to leave.

   It fades to the section's own colour on both sides before it clips, so
   a neighbour dissolves rather than being cut off — and the words beside
   it are read against #FAFAFA instead of through somebody's homepage.

   The padding and the matching negative margin are for the front card's
   drop shadow, which hangs 30px below it and would otherwise be sliced
   flat along the bottom edge. */
.jmk-flow__stage {
  position: relative;
  overflow: hidden;
  padding-bottom: 36px;
  margin-bottom: -36px;
}
.jmk-flow__stage::before,
.jmk-flow__stage::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 36px;
  width: clamp(48px, 10%, 150px);
  pointer-events: none;
  z-index: 5;
}
.jmk-flow__stage::before {
  left: 0;
  background: linear-gradient(to right, var(--color-surface-sunken), rgba(250,250,250,0));
}
.jmk-flow__stage::after {
  right: 0;
  background: linear-gradient(to left, var(--color-surface-sunken), rgba(250,250,250,0));
}
.jmk-flow {
  position: relative;
  perspective: 1600px;
  transform-style: preserve-3d;
  /* The cards are absolutely placed, so the rail needs a height of its
     own. 16/10 for the shot plus the browser chrome above it. */
  aspect-ratio: 16 / 11;
  outline: none;
}
.jmk-flow:focus-visible { outline: 2px solid var(--ink-900); outline-offset: 12px; border-radius: 4px; }

.jmk-flow__card {
  position: absolute;
  /* NOT inset: 0. The front card used to be the full width of its column,
     which on a wide screen is a browser screenshot the better part of a
     metre across — bigger than the site it is a picture of, and leaving
     the description to fight it for attention. Inset from both sides it
     is a preview again, and the room it gives back is what the
     neighbours turn into. */
  inset: 0 8%;
  transform-origin: center;
  transition: transform 780ms cubic-bezier(.22,1,.28,1),
              opacity 520ms ease,
              filter 520ms ease;
  will-change: transform, opacity;
}
.jmk-flow__card > a,
.jmk-flow__card > button { display: block; width: 100%; }
.jmk-flow__pick {
  padding: 0; border: 0; background: none; cursor: pointer; width: 100%;
  font: inherit; color: inherit; text-align: left;
}
.jmk-flow__pick:focus-visible { outline: 2px solid var(--ink-900); outline-offset: 8px; }

/* The copy crossfades rather than swapping, because the frame beside it
   takes three quarters of a second to turn and text that changes
   instantly reads as belonging to the card that is still leaving. */
@keyframes jmkFlowCopy {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}
.jmk-flow__copy { animation: jmkFlowCopy 520ms cubic-bezier(.16,1,.3,1) both; }

.jmk-flow__tag {
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  padding: 7px 14px;
  border-radius: var(--radius-pill);
}
.jmk-flow__tag[data-tone="pink"] { background: rgba(255,98,177,.16); color: #a3175f; }
.jmk-flow__tag[data-tone="blue"] { background: rgba(41,156,223,.16); color: #16628f; }
.jmk-flow__tag[data-tone="dark"] { background: var(--ink-900); color: var(--white); }

.jmk-flow__nav {
  display: flex; align-items: center;
  gap: 16px; margin-top: clamp(24px, 3vh, 36px);
}
.jmk-flow__nav > button {
  width: 44px; height: 44px; border-radius: 50%;
  border: 1px solid var(--color-border);
  background: var(--white); color: var(--ink-900);
  font-size: 16px; line-height: 1; cursor: pointer;
  transition: background 240ms ease, color 240ms ease, border-color 240ms ease;
}
.jmk-flow__nav > button:hover { background: var(--ink-900); color: var(--white); border-color: var(--ink-900); }

.jmk-flow__dots { display: flex; gap: 8px; }
.jmk-flow__dots button {
  width: 9px; height: 9px; padding: 0; border-radius: 50%;
  border: 0; cursor: pointer;
  background: rgba(17,17,17,.22);
  transition: background 240ms ease, transform 240ms ease;
}
.jmk-flow__dots button[data-on="true"] {
  background: var(--color-primary);
  transform: scale(1.35);
}

@media (max-width: 1024px) {
  /* Narrower stage, shallower turn — at tablet width a 34-degree card
     reaches past the column it lives in. */
  .jmk-flow { aspect-ratio: 16 / 12; perspective: 1100px; }
  /* One column here, so the rail has the full width and the front card
     can have more of it back. */
  .jmk-flow__card { inset: 0 5%; }
}

/* Below the phone breakpoint the rail is a single flat card and the copy
   sits under it. The gap wants to be a real one: a description that
   starts 20px under a screenshot reads as a caption on it. */
@media (max-width: 700px) {
  .jmk-flow__card { inset: 0; }
  /* One card, no neighbours, nothing to fade — and a gradient laid over a
     single flat screenshot is just a smudge on it. */
  .jmk-flow__stage::before,
  .jmk-flow__stage::after { display: none; }
  .jmk-flow__stage { padding-bottom: 28px; margin-bottom: -12px; }
  .jmk-flow__nav { margin-top: 20px; }
}

@media (prefers-reduced-motion: reduce) {
  .jmk-flow__card { transition: none; }
  .jmk-flow__copy { animation: none; }
}

/* One card, flat, on a phone — see the note in WebFlow. The card still
   fades between builds so the change is legible; it simply does not turn. */
.jmk-flow--flat { perspective: none; transform-style: flat; aspect-ratio: 16 / 11; }
.jmk-flow--flat .jmk-flow__card { animation: jmkFlowCopy 460ms cubic-bezier(.16,1,.3,1) both; }

/* The craft archive reel — see CraftReel.jsx. The masonry itself is the
   shared .jmk-masonry; this only sets the column count and the note. */
/* The reel packs its own columns in JS rather than using CSS `columns`.
   The browser balances those to a height nothing can predict, and this
   wall has to reserve room for its tallest page — a reservation you
   cannot calculate is a guess. See the note in CraftReel.jsx. */
.jmk-reel__cell { transition: opacity 700ms ease; }

/* A frame is a button now rather than an anchor — it opens the viewer
   rather than going anywhere — so it needs a button's defaults undone. */
.jmk-reel__open {
  display: block; width: 100%; padding: 0; border: 0;
  background: none; text-align: left; font: inherit; color: inherit;
  cursor: none;
}

/* The page dots. Small, because they are a position indicator that
   happens to be operable, not a control bar; large enough to hit,
   because they are operable. The tap target is the whole 22px square;
   the dot itself is 7. */
.jmk-reel__dots {
  display: flex; justify-content: center; align-items: center;
  gap: 4px; margin: clamp(24px, 3.4vh, 40px) 0 0;
}
.jmk-reel__dot {
  width: 22px; height: 22px; padding: 0; border: 0;
  background: none; cursor: none; position: relative;
}
.jmk-reel__dot::after {
  content: ""; position: absolute; inset: 0; margin: auto;
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--ink-900);
  opacity: .18;
  transition: opacity 320ms ease, transform 320ms cubic-bezier(.34,1.3,.5,1), background-color 320ms ease;
}
.jmk-reel__dot:hover::after { opacity: .45; }
.jmk-reel__dot[data-on="true"]::after {
  opacity: 1; background: var(--color-primary, #ff62b1); transform: scale(1.35);
}
.jmk-reel__dot:focus-visible::after { outline: 2px solid var(--color-primary, #ff62b1); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) {
  .jmk-reel__dot::after { transition: none; }
}

/* How many frames are behind this one. Same badge as the gallery walls
   carry, because it means the same thing on both. */
.jmk-reel__count {
  position: absolute; top: 12px; right: 12px; z-index: 4;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 11px; border-radius: var(--radius-pill);
  background: rgba(17, 17, 17, 0.78);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  font-family: var(--font-ui); font-size: var(--text-2xs);
  font-weight: 600; letter-spacing: var(--tracking-wide);
  color: #fff; pointer-events: none;
}

/* The caption on a grouped tile is two things, not one: what the shoot
   is called, and who it was for. Stacked, with the client set smaller
   and quieter — it is an attribution, not a title. */
.jmk-reel__cell .jmk-figure__cap { display: grid; gap: 3px; }
.jmk-reel__setname {
  font-family: var(--font-editorial);
  font-size: clamp(15px, 1.3vw, 19px);
  line-height: 1.15;
}
.jmk-reel__client {
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.62);
}

/* --- the fullscreen one-pager variant ------------------------------
   /clyr asks for the stack as a screen of its own: "a fullscreen one
   pager hence why i wanted the logos in circles that float and move
   while connected to each other in a web that reacts to mouse
   movement". Same component, same data, same drift and pointer pull —
   the only difference is that the section claims the viewport and the
   board takes as much of it as the copy leaves.

   min-height rather than height, and dvh rather than vh: a fixed 100vh
   is a scroll trap on a phone the moment the copy is longer than the
   address bar lets it be, and the copy here is four lines plus a
   reader. */
.jmk-mix--full {
  min-height: 100dvh;
  display: grid;
  align-content: center;
}
.jmk-mix--full .jmk-mix__grid { width: 100%; }
@media (min-width: 1025px) {
  /* The board leads on this one. On Paid Media the network illustrates
     an argument that is already made in the copy; here the network IS
     the argument — six tools, joined up — so it gets the larger half. */
  .jmk-mix--full .jmk-mix__grid { grid-template-columns: minmax(0, 0.72fr) minmax(0, 1fr); }
  .jmk-mix--full .jmk-mix__board { max-width: min(760px, 100%); }
}

/* =====================================================================
   The last screen on a craft page — "And there's more."
   =====================================================================
   The one moving ground on the site, and deliberately the only one.
   Every other section stands on a flat field, which is what makes
   scrolling read as moving from room to room. This is the last screen
   before the footer: the page has ended and the studio has not, and a
   ground that will not quite settle is the difference between a full
   stop and a door.

   HOW IT MOVES. One element, one gradient, one animated
   background-position — no canvas, no image, no per-frame JavaScript.
   The gradient is drawn at three times the box so there is somewhere
   for it to travel to, and the whole thing is a composited paint the
   GPU can do without touching layout.

   THE TWO HOUSE COLOURS, NOT THREE. The first pass put the green in
   as well; pink, blue and green overlapping at low opacity average out
   to a murky olive, which is the trouble with mixing a full palette in
   light rather than in ink. Pink and blue over black stay pink and
   blue wherever they cross, and they are the pair the brand is
   actually built on. */
.jmk-onward {
  position: relative;
  isolation: isolate;
  background: var(--ink-900);
  overflow: hidden;
}
.jmk-onward::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    radial-gradient(46% 58% at 18% 26%, rgba(255, 98, 177, 0.78), transparent 64%),
    radial-gradient(44% 56% at 84% 24%, rgba(41, 156, 223, 0.72), transparent 64%),
    radial-gradient(52% 60% at 62% 92%, rgba(255, 98, 177, 0.44), transparent 66%),
    radial-gradient(48% 58% at 12% 88%, rgba(41, 156, 223, 0.40), transparent 66%),
    var(--ink-900);
  background-size: 300% 300%, 300% 300%, 300% 300%, 300% 300%, 100% 100%;
  background-position: 0% 50%, 100% 50%, 50% 100%, 50% 0%, 0 0;
  animation: jmkMoreDrift 26s ease-in-out infinite alternate;
}
@keyframes jmkMoreDrift {
  0%   { background-position:   0% 50%, 100% 50%,  50% 100%,  50%   0%, 0 0; }
  50%  { background-position:  70% 18%,  26% 82%,  90%  34%,  10%  66%, 0 0; }
  100% { background-position: 100% 62%,   0% 24%,  14%  70%,  84%  22%, 0 0; }
}

/* A pill on a dark ground. The house pill is ink on white and inverts to
   pink on hover; on this ground the ink version is invisible, so it runs
   the other way round — white first, and the brand colour is what the
   hover reveals. */
.jmk-pill--light { background: var(--white); color: var(--ink-900); }
.jmk-pill--light:hover { background: var(--color-primary); color: var(--white); }
.jmk-pill--light:focus-visible { outline-color: var(--white); }

@media (prefers-reduced-motion: reduce) {
  /* Still, not absent. The colour is part of the composition; only the
     travel is the thing somebody asked to stop. */
  .jmk-onward::before { animation: none; }
}

/* =====================================================================
   The craft card turns over — /our-work/creative
   =====================================================================
   Front: the cover, the name, the blurb. Back: four frames from that
   craft's own gallery, what it is called, how much of it there is, and
   the way through to the page.

   A REAL ROTATION, not a cross-fade. Two absolutely positioned faces on
   a wrapper with preserve-3d, the back pre-rotated half a turn and both
   backface-hidden — so what you see is one object turning, which is the
   whole reason to do this rather than swap two divs. transform and
   nothing else, so it composites and never touches layout.

   THE WRAPPER OWNS THE SHAPE. Both faces are inset:0 inside it, so the
   card keeps the packed masonry's proportions on both sides and the
   wall does not reflow when one turns.
   ===================================================================== */
.jmk-craft__flip {
  position: relative;
  perspective: 1400px;
  transform-style: preserve-3d;
}
.jmk-craft__face {
  position: absolute;
  inset: 0;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  text-align: left;
  font: inherit;
  color: inherit;
  cursor: none;
  border-radius: 4px;
  overflow: hidden;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  transform-style: preserve-3d;
  transition: transform 720ms cubic-bezier(0.16, 1, 0.3, 1);
}
/* The front turns away; the back starts turned away and comes to meet
   you. Half a turn each, in the same direction, so the card never
   appears to change its mind halfway. */
.jmk-craft__flip .jmk-craft__face:not(.jmk-craft__back) { transform: rotateY(0deg); }
.jmk-craft__back { transform: rotateY(-180deg); }
.jmk-craft__flip[data-flipped="true"] .jmk-craft__face:not(.jmk-craft__back) { transform: rotateY(180deg); }
.jmk-craft__flip[data-flipped="true"] .jmk-craft__back { transform: rotateY(0deg); }

/* The face fills the wrapper, so .jmk-craft__media has to as well —
   it sets its own aspect-ratio on the unflipped card and that is the
   wrapper's job now. */
.jmk-craft__flip .jmk-craft__media {
  position: absolute;
  inset: 0;
  aspect-ratio: auto;
}

/* --- the back ------------------------------------------------------ */
.jmk-craft__back {
  display: grid;
  /* minmax(0,1fr) rather than 1fr on the first row: a card with
     nothing to preview drops that row entirely, and a bare 1fr
     would hold the hole open where it would have been. */
  grid-template-rows: minmax(0, 1fr) auto auto;
  gap: 14px;
  padding: 14px;
  background: #17171a;
  color: var(--white);
}
/* Two by two. Four frames is a taste of the craft; a fifth would be the
   gallery, and the gallery has a page. */
.jmk-craft__peek {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-auto-rows: 1fr;
  gap: 8px;
  min-height: 0;
}
.jmk-craft__peekcell {
  position: relative;
  overflow: hidden;
  border-radius: 3px;
  background: #232327;
}
.jmk-craft__peekcell img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.jmk-craft__backfoot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  border-top: 1px solid rgba(255, 255, 255, 0.14);
  padding-top: 12px;
}
.jmk-craft__backname {
  font-family: var(--font-editorial);
  font-size: clamp(17px, 1.5vw, 22px);
  line-height: 1.1;
}
.jmk-craft__backcount {
  font-family: var(--font-ui);
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.5);
  white-space: nowrap;
}
.jmk-craft__backrow { display: flex; align-items: center; gap: 10px; }
.jmk-craft__go {
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 11px 18px;
  border-radius: var(--radius-pill);
  background: var(--white);
  color: var(--ink-900);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: 600;
  text-decoration: none;
  transition: background-color 220ms ease, color 220ms ease;
}
.jmk-craft__go:hover { background: var(--color-primary); color: var(--white); }
.jmk-craft__go span { transition: transform 220ms ease; }
.jmk-craft__go:hover span { transform: translateX(4px); }
.jmk-craft__flipback {
  padding: 11px 16px;
  border-radius: var(--radius-pill);
  border: 1px solid rgba(255, 255, 255, 0.24);
  background: none;
  color: rgba(255, 255, 255, 0.72);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: none;
  transition: border-color 220ms ease, color 220ms ease;
}
.jmk-craft__flipback:hover { border-color: var(--white); color: var(--white); }

@media (prefers-reduced-motion: reduce) {
  /* Still swaps, still reversible — it simply arrives rather than
     turning. A rotation is the one part of this somebody asked to
     stop; the card having two sides is the feature. */
  .jmk-craft__face { transition: none; }
  .jmk-craft__flip .jmk-craft__face:not(.jmk-craft__back),
  .jmk-craft__flip[data-flipped="true"] .jmk-craft__back { transform: none; }
  .jmk-craft__back,
  .jmk-craft__flip[data-flipped="true"] .jmk-craft__face:not(.jmk-craft__back) {
    transform: none;
    opacity: 0;
    pointer-events: none;
  }
  .jmk-craft__flip[data-flipped="true"] .jmk-craft__back { opacity: 1; pointer-events: auto; }
}

/* =====================================================================
   The process, as a carousel — see StepCarousel.jsx
   =====================================================================
   One track, translated by whole slides. The slide width is a custom
   property so the same markup is one card on a phone and three across
   on a desktop, with the two either side of the current one held back
   rather than cropped off.

   The stage clips; the track does not animate anything but transform.
   ===================================================================== */
.jmk-steps {
  background: var(--color-surface-sunken, #fafafa);
  padding: clamp(64px, 9vh, 110px) clamp(20px, 5vw, 64px);
}
.jmk-steps__h {
  font-family: var(--font-editorial);
  font-weight: 500;
  font-size: clamp(30px, 4.4vw, 56px);
  line-height: 1.05;
  color: var(--ink-900);
  margin: 0 0 clamp(28px, 4vh, 48px);
}
.jmk-steps__stage {
  --slide: 100%;
  overflow: hidden;
  /* Room for the card's own hover lift and shadow, which would
     otherwise be clipped flat by the stage. */
  padding: 10px 0 34px;
  margin: -10px 0 -34px;
}
@media (min-width: 721px)  { .jmk-steps__stage { --slide: 50%; } }
@media (min-width: 1025px) { .jmk-steps__stage { --slide: 33.3333%; } }

.jmk-steps__track {
  display: flex;
  align-items: stretch;
  transition: transform 720ms cubic-bezier(0.16, 1, 0.3, 1);
  will-change: transform;
}
.jmk-steps__slide {
  flex: 0 0 var(--slide);
  min-width: 0;
  padding-right: clamp(14px, 1.6vw, 22px);
  /* The ones either side are present and readable — they are the reason
     to keep watching — but they are not the thing being said. */
  opacity: 0.42;
  transition: opacity 620ms ease;
}
.jmk-steps__slide[data-on="true"] { opacity: 1; }
.jmk-steps__card { height: 100%; }

.jmk-steps__dots {
  display: flex;
  align-items: center;
  gap: 4px;
  margin: clamp(20px, 3vh, 32px) 0 0;
}
.jmk-steps__dot {
  width: 26px; height: 22px; padding: 0; border: 0;
  background: none; cursor: none; position: relative;
}
/* A bar rather than a circle. These are four steps in an order, and a
   short rule reads as a position on a line where a dot reads as one of
   a set — the archive's dots are a set, this is a line. */
.jmk-steps__dot::after {
  content: "";
  position: absolute; inset: 0; margin: auto;
  width: 18px; height: 3px; border-radius: 2px;
  background: var(--ink-900);
  opacity: .16;
  transition: opacity 320ms ease, background-color 320ms ease, width 320ms cubic-bezier(.34,1.3,.5,1);
}
.jmk-steps__dot:hover::after { opacity: .4; }
.jmk-steps__dot[data-on="true"]::after {
  opacity: 1; width: 26px; background: var(--color-primary, #ff62b1);
}
.jmk-steps__dot:focus-visible::after { outline: 2px solid var(--color-primary, #ff62b1); outline-offset: 4px; }

@media (prefers-reduced-motion: reduce) {
  .jmk-steps__track, .jmk-steps__slide, .jmk-steps__dot::after { transition: none; }
  /* Still, and all four legible — the carousel does not run, so nothing
     should be dimmed waiting for a turn that will not come. */
  .jmk-steps__slide { opacity: 1; }
}

/* The deck's own controls — see PartnerStack.jsx.
   Small, either side of the dots. This is a way to take hold of
   something that is already moving, not a control bar the section is
   built around; and being real buttons is what makes the deck operable
   from a keyboard, which the pinned version never was. */
.pstack__arrow {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; padding: 0;
  border: 1px solid var(--color-border, #e4e4e4); border-radius: 999px;
  background: none; color: var(--ink-500, #6b6b6b);
  cursor: pointer;
  transition: color 220ms ease, border-color 220ms ease, transform 220ms var(--ease-confident, ease);
}
.pstack__arrow:hover { color: var(--ink-900); border-color: var(--ink-900); }
.pstack__arrow:active { transform: scale(0.92); }
.pstack__arrow:focus-visible { outline: 2px solid var(--ink-900); outline-offset: 3px; }
/* A little more air around the dots now they sit between two arrows. */
.pstack__dots { align-items: center; }
@media (prefers-reduced-motion: reduce) {
  .pstack__arrow { transition: none; }
  .pstack__arrow:active { transform: none; }
}
/* Dragging the deck should feel like dragging it, not like selecting
   four paragraphs of partner copy. */
.pstack__deck { touch-action: pan-y; }
.pstack--live .pstack__deck { user-select: none; -webkit-user-select: none; }
