/* ==========================================================================
   Bearing Planet — motion layer.
   Additive only. Every rule here is scoped to .motion, which motion.js adds
   to <html> and removes if the visitor prefers reduced motion. With this
   file absent, or JS disabled, the page is the approved static design.
   ========================================================================== */

/* ------------------------------------------------------------- bearings
   These replace the comp's decorative concentric circles, so the circles
   themselves are hidden once a live bearing is mounted in their place. */
.motion .hero__ring,
.motion .industries__ring { display: none; }

.bearing-wrap {
  position: absolute;
  pointer-events: none;
  z-index: 0;
  line-height: 0;
}
.bearing { display: block; width: 100%; height: 100%; overflow: visible; }
/* Sub-pixel rotation on 1px strokes shimmers without this. */
.bearing * { vector-effect: non-scaling-stroke; }

/* Sits where the comp's 640px circle pair sat, top-right of the hero. */
.bearing-wrap--hero {
  right: -150px;
  top: -170px;
  width: 620px;
  height: 620px;
}
/* And where the industries pair sat, bottom-left. */
.bearing-wrap--ind {
  left: -190px;
  bottom: -190px;
  width: 540px;
  height: 540px;
  z-index: 0;
}

/* At phone width a 620px bearing stops being a corner detail and sprawls
   across the headline. Same correction the comp's rings needed. */
@media (max-width: 700px) {
  .bearing-wrap--hero {
    right: -130px;
    top: -120px;
    width: 330px;
    height: 330px;
    opacity: 0.75;
  }
  .bearing-wrap--ind {
    left: -140px;
    bottom: -140px;
    width: 320px;
    height: 320px;
    opacity: 0.8;
  }
}

/* ------------------------------------------------------------- marquee
   JS drives the transform; kill the keyframe so they do not fight. The
   CSS animation stays in style.css as the no-JS fallback. */
.motion .marquee__track { animation: none !important; will-change: transform; }

/* Logo pills: uniform 26px logo height, centred, consistent padding, so the
   wall reads as one aligned system rather than mixed lockup sizes. */
.marquee__pill--logo {
  display: inline-flex;
  align-items: center;
  padding: 8px 16px;
  background: #fff;
}
.marquee__pill--logo img { display: block; height: 21px; width: auto; }

/* Brand wall, take two (his 9 Aug screenshot pointed at this beat; 19 Jul
   ask "make the logos section more appealing"): larger tiles, a real logo
   height, and every tile is now a link to that brand's page. */
.heroB__brandgrid a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 50px;
  min-width: 96px;
  padding: 10px 16px;
}
/* 3 Oct, his voice note: Fenner, NSK, Nachi read too small. Taller logos,
   capped width so the wide marks do not dominate. */
.heroB__brandgrid a img { display: block; height: 30px; width: auto; max-width: 120px; object-fit: contain; }
.heroB__brandgrid a:hover { border-color: var(--blue); background: #fff; }

/* --------------------------------------------------------- industries
   The fill clone sits exactly over the outlined word and is revealed
   left to right, like stock being machined. */
.motion .ind-row__word { position: relative; display: inline-block; }
.ind-row__fill {
  position: absolute;
  left: 0;
  top: 0;
  color: #fff;
  -webkit-text-stroke: 1px transparent;
  clip-path: inset(0 100% 0 0);
  pointer-events: none;
  white-space: nowrap;
}
/* Hover still wins outright, as in the comp. */
.motion .ind-row:hover .ind-row__fill { clip-path: inset(0 0 0 0) !important; }

/* ---------------------------------------------------------- conveyor
   Desktop: the section is pinned and the track is mapped to page scroll.
   The page itself never stops scrolling normally. */
/* `overflow: hidden` on the sheet makes it a scroll container, which silently
   disables `position: sticky` for everything inside it — the conveyor pin just
   scrolls away. `clip` clips identically (border-radius included) WITHOUT
   creating that container, so sticky works. Same distinction that fixed the
   Instagram in-app-browser header. Scoped to .motion so the base design keeps
   the exact declaration it was verified with. */
.motion .sheet { overflow: clip; }

@media (min-width: 901px) {
  /* The pin needs to own the full section box, so the section's own vertical
     padding moves onto the sticky stage. JS sets the section height. */
  .motion [data-conveyor] {
    position: relative;
    padding-top: 0;
    padding-bottom: 0;
  }
  /* Full-bleed while pinned, so cards run off the screen edge rather than
     stopping at the 1280px gutter. The base container constraint lives on
     .conveyor__stage in style.css for the no-motion path. */
  .motion [data-conveyor-viewport] {
    position: sticky;
    top: 0;
    height: 100vh;
    max-width: none;
    display: flex;
    flex-direction: column;
    justify-content: center;
    overflow: hidden;
    padding-block: 96px;
    box-sizing: border-box;
  }
  /* The head stays on the 1280 grid even though the stage is full-bleed. */
  .motion [data-conveyor-viewport] > .container {
    width: 100%;
    padding-inline: 28px;
    box-sizing: border-box;
  }
  .motion [data-conveyor-track] { padding-inline: 28px; }
  .motion [data-conveyor-track] {
    display: flex;
    gap: 20px;
    width: max-content;
    will-change: transform;
    margin-top: 44px;
  }
  .motion [data-conveyor-track] .card {
    width: 380px;
    flex: none;
  }
  .motion .conveyor__rail { display: block; }
}

/* The rail is a progress readout for the pinned section. It is a flex child
   of the stage, so it needs an explicit width or it collapses to its content
   and the two labels collide. */
.conveyor__rail {
  display: none;
  width: 100%;
  max-width: 1280px;
  margin: 34px auto 0;
  padding: 0 28px;
  box-sizing: border-box;
  flex: none;
}
.conveyor__rail-line {
  height: 2px;
  background: var(--border);
  position: relative;
  overflow: hidden;
}
.conveyor__rail-line::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--blue);
  transform: scaleX(0);
  transform-origin: 0 50%;
}
[data-conveyor-bar] {
  position: absolute;
  inset: 0;
  background: var(--blue);
  transform: scaleX(0);
  transform-origin: 0 50%;
}
.conveyor__hint {
  display: flex;
  justify-content: space-between;
  margin-top: 12px;
  font-family: var(--sans);
  font-weight: 600;
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: none;
  color: var(--faint);
}

/* Below the pin threshold: a native swipe carousel. No hijacking, no JS. */
@media (max-width: 900px) {
  [data-conveyor-viewport] {
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    margin-inline: -20px;
    padding-inline: 20px;
  }
  [data-conveyor-viewport]::-webkit-scrollbar { display: none; }
  [data-conveyor-track] {
    display: flex;
    gap: 16px;
    width: max-content;
  }
  [data-conveyor-track] .card {
    width: min(78vw, 340px);
    flex: none;
    scroll-snap-align: start;
  }
  .conveyor__rail { display: none; }
}

/* ------------------------------------------------------------- reveals */
.motion .rv {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.7s cubic-bezier(0.22, 1, 0.36, 1),
              transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
}
.motion .rv--in { opacity: 1; transform: none; }

/* Word-split headings: each word rises out of its own mask. */
.motion .rv .rv-w {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
}
.motion .rv .rv-i {
  display: inline-block;
  transform: translateY(105%);
  transition: transform 0.75s cubic-bezier(0.22, 1, 0.36, 1);
}
/* The heading itself must not fade, or the mask effect is wasted. */
.motion .h2.rv,
.motion .enquiry__h2.rv { opacity: 1; transform: none; }
.motion .rv--in .rv-i { transform: none; }
.motion .rv .rv-w:nth-child(1) .rv-i { transition-delay: 0.00s; }
.motion .rv .rv-w:nth-child(3) .rv-i { transition-delay: 0.04s; }
.motion .rv .rv-w:nth-child(5) .rv-i { transition-delay: 0.08s; }
.motion .rv .rv-w:nth-child(7) .rv-i { transition-delay: 0.12s; }
.motion .rv .rv-w:nth-child(9) .rv-i { transition-delay: 0.16s; }
.motion .rv .rv-w:nth-child(11) .rv-i { transition-delay: 0.20s; }
.motion .rv .rv-w:nth-child(13) .rv-i { transition-delay: 0.24s; }
.motion .rv .rv-w:nth-child(15) .rv-i { transition-delay: 0.28s; }

/* Stagger cards within a row without needing per-card JS. */
.motion .trust__card:nth-child(2),
.motion .step:nth-child(2) { transition-delay: 0.08s; }
.motion .trust__card:nth-child(3),
.motion .step:nth-child(3) { transition-delay: 0.16s; }
.motion .trust__card:nth-child(4) { transition-delay: 0.24s; }

/* ------------------------------------------------------- step connector
   A line drawn between 01 / 02 / 03 as the row arrives. */
@media (min-width: 901px) {
  .motion .how__grid { position: relative; }
  .motion .how__grid::before {
    content: "";
    position: absolute;
    left: 6%;
    right: 6%;
    top: 52px;
    height: 1px;
    background: repeating-linear-gradient(90deg, var(--border-strong) 0 6px, transparent 6px 12px);
    transform: scaleX(0);
    transform-origin: 0 50%;
    transition: transform 1.1s cubic-bezier(0.22, 1, 0.36, 1) 0.15s;
    z-index: 0;
  }
  .motion .how__grid:has(.rv--in)::before { transform: scaleX(1); }
  .motion .step { z-index: 1; }
}

/* --------------------------------------------------- card tilt + glow
   The tilt transform lives here so it composes with the lift. style.css
   already animates `transform` on .card, so the transition is retuned to
   snap while tracking and settle on the way out. */
.motion .tilt {
  transform:
    perspective(900px)
    rotateX(var(--rx, 0deg))
    rotateY(var(--ry, 0deg))
    translateY(var(--lift, 0px));
  transform-style: preserve-3d;
  transition: transform 0.45s cubic-bezier(0.22, 1, 0.36, 1),
              border-color 0.25s, box-shadow 0.35s;
  will-change: transform;
}
.motion .tilt--on {
  transition: transform 0.09s linear, border-color 0.25s, box-shadow 0.35s;
}
/* style.css lifts .card on hover; the tilt owns transform now, so that
   rule has to stand down or the two fight. */
.motion .card:hover,
.motion .trust__card:hover,
.motion .step:hover { transform: perspective(900px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)) translateY(var(--lift, 0px)); }

/* The glow itself. A soft blue pool that follows the cursor, plus a
   brighter rim so the card's border catches the light. Both are painted
   from the SAME --mx/--my, which is what makes the grid read as one
   surface. Restrained on purpose: this sells 58 years of trust, not a
   gaming keyboard. */
.motion .tilt {
  position: relative;
  isolation: isolate;
}
.motion .tilt::before,
.motion .tilt::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.35s ease;
}
/* Pool of light across the card face. */
.motion .tilt::before {
  z-index: -1;
  background: radial-gradient(
    380px circle at var(--mx, 50%) var(--my, 50%),
    rgba(15, 88, 214, 0.13),
    rgba(15, 88, 214, 0.05) 42%,
    transparent 68%);
}
/* Rim light: a gradient clipped to the border box only. */
.motion .tilt::after {
  padding: 1px;
  background: radial-gradient(
    320px circle at var(--mx, 50%) var(--my, 50%),
    rgba(15, 88, 214, 0.85),
    rgba(15, 88, 214, 0.28) 40%,
    transparent 65%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
}
/* Lit whenever the cursor is anywhere over the grid, not just this card. */
.motion .glow-grid:hover .tilt::before { opacity: 1; }
.motion .glow-grid:hover .tilt::after { opacity: 0.9; }
.motion .tilt--on { z-index: 2; }
.motion .tilt--on::before { opacity: 1; }

/* The dark section needs a different light temperature. */
.motion .how__grid .tilt::before {
  background: radial-gradient(380px circle at var(--mx, 50%) var(--my, 50%),
    rgba(15, 88, 214, 0.10), transparent 68%);
}

/* ----------------------------------------------------------- magnetic */
.mag { transition: transform 0.32s cubic-bezier(0.22, 1, 0.36, 1), background 0.2s, box-shadow 0.2s; }
.mag:active { transition-duration: 0.05s; }

/* The pill is fixed-positioned, so a transform on it must not fight the
   hover lift already defined in style.css. */
.motion .pill:hover { transform: translateY(-2px); }

/* ==================================================================== HERO B
   Pinned hero. The section is tall; the stage sticks and its content is
   swapped beat by beat as you scroll through it. Only present when the
   page was loaded with ?hero=b.
   ==================================================================== */
.heroB {
  position: relative;
  /* Dark hero (7 Aug, we: "ours look very white", ref = the SKF
     machine-tools page). FLAT #0A1220, never a gradient: the 2D bearing
     frames (assets/3d/shine-dark) are baked on this exact colour, and any
     gradient behind them ghosts the frame square. Depth comes from the
     vignette wash instead, which sits ABOVE the bearing layer so the square
     and its surroundings are graded identically. */
  /* White again (29 Sep, his 23 Sep ask "home page white Background").
     Flat #FFF: the 2D frames in assets/3d/shine are baked on white. */
  background: #fff;
  /* Height is set by JS from the beat count. This is the pre-JS fallback. */
  min-height: 100vh;
}
.heroB__stage {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* The sticky stage paints over the section, so it must carry the SAME flat
     white as .heroB. The 2D frames are baked on this exact colour. */
  background: #fff;
}
/* The bearing frames are baked on flat #0A1220 (assets/3d/shine-dark) rather
   than with an alpha channel, a third of the file size. That only works while
   the area behind them is EXACTLY that colour, so the
   gradient is held at #FFF until well below the bearing and only falls away
   at the very bottom, where it meets the grey section. Move the bearing or
   shorten this ramp and its square will show. */
.heroB__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: none;
  pointer-events: none;
}
.heroB__bg img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  will-change: opacity, transform;
}
/* Static and reduced-motion fallback: chapter one, held. JS drives the
   crossfade and the slow drift under .motion. */
.heroB__bg img:first-child { opacity: 0.44; }
/* Any time the 2D frame fallback mounts, the background must return to the
   flat bake colour or the frame square ghosts. */
.heroB.has-shine .heroB__bg { display: none; }

.heroB__wash {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Was a solid white sheet hiding the white-baked frame square; now a
     vignette for depth. z-index 1: above the bearing (z0), below the beats
     (z2), so the dark-baked frame square and the flat background around it
     receive the same grade and can never seam. */
  background: none;
  z-index: 1;
}

/* The rendered bearing: a 16-frame shine loop, swapped by JS, plus a
   sub-frame CSS rotation so the geometry turns smoothly between frames. */
.bearing-shine__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  will-change: transform;
  transform-origin: 50% 50%;
}

/* In B the bearing is a small object at the centre of the stage, and the copy
   is arranged in the space AROUND it. Nothing overlaps it: it reads as a part
   sitting on the page with text orbiting, not as a watermark behind text. */
.motion .heroB .bearing-wrap--hero {
  right: auto;
  /* Scaled up 21 Jul (we wanted HERO B bigger, not A). motion.js morphs
     left/top/width/height every frame: beat 1 parks the bearing small inside
     the beat's own slot, then it grows to this stage position as the story
     starts, and steps back for the brand-wall finale. These values are only
     the pre-JS initial state; JS_bsz/asz in motion.js are the real sizes and
     must be changed together with these. */
  top: 56%;
  left: 50%;
  width: clamp(230px, 30vw, 400px);
  height: clamp(230px, 30vw, 400px);
  transform: translate(-50%, -50%);
  opacity: 1;
}
.heroB__gap {
  width: 100%;
  height: clamp(180px, 28vh, 280px);
  /* 7 Aug: we wanted the headline block and the animation to breathe. */
  margin: 22px 0 14px;
  flex: none;
}
/* The 3D canvas needs headroom: as the part tilts it sweeps a bigger box than
   its face-on silhouette, and a tight wrapper would clip the corners. */
.motion .heroB .bearing-3d {
  /* Initial = stage size, so the WebGL buffer is allocated at full res and
     the beat-1 shrink only CSS-scales it down. Model fills 82% of the box. */
  width: clamp(300px, 40vw, 560px);
  height: clamp(300px, 40vw, 560px);
}
.bearing-3d canvas { display: block; }
/* On the white hero the live render read pale (29 Sep QC); a touch more
   contrast keeps the steel reading as steel. The 2D frames are unaffected. */
.heroB .bearing-3d canvas { filter: contrast(1.3) brightness(0.8); }
.bearing-3d { opacity: 0; transition: opacity 0.6s ease; }
.bearing-3d.is-ready { opacity: 1; }
@media (max-width: 900px) {
  .motion .heroB .bearing-wrap--hero {
    width: clamp(170px, 40vw, 260px);
    height: clamp(170px, 40vw, 260px);
    /* Sits above the copy on a phone rather than in the middle of it. */
    top: 27%;
  }
}
.heroB__inner {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
}
/* Every beat occupies the same grid cell, so they cross-fade in place
   instead of pushing each other around.

   Option (b): the text travels AROUND the bearing rather than through it.
   Each beat arrives along an arc from one side and leaves along the far
   side, so across the five beats the copy circles the bearing. Text is
   always in front, and every beat comes to rest on a straight horizontal
   so it is read square, never on a slant. */
/* Each beat is a block parked at a point AROUND the bearing, never on top of
   it. --ox/--oy place it (px from stage centre); consecutive beats step
   clockwise around the part, so as you scroll the copy travels around it.
   Text itself stays square to the page: legibility beats novelty. */
.heroB__beat {
  position: absolute;
  /* Above the vignette wash (z1); the bearing stays at z0 underneath both. */
  z-index: 2;
  left: 50%;
  top: 50%;
  width: min(38ch, 27vw);
  display: flex;
  flex-direction: column;
  align-items: var(--align, flex-start);
  text-align: var(--talign, left);
  opacity: 0;
  transform:
    translate(-50%, -50%)
    translate(calc(var(--ox, 0px) + var(--dx, 0px)), calc(var(--oy, 0px) + var(--dy, 22px)));
  filter: blur(5px);
  transition: opacity 0.5s ease, transform 0.72s cubic-bezier(0.22, 1, 0.36, 1), filter 0.5s ease;
  pointer-events: none;
  will-change: opacity, transform;
}
.heroB__beat--on {
  opacity: 1;
  --dx: 0px; --dy: 0px;
  filter: none;
  pointer-events: auto;
}

/* Five stops, going clockwise from the left. The horizontal offset clears the
   bearing; the vertical one keeps every block inside the stage. */
/* Beat 1 sits ABOVE the part and runs wider, so the headline stays two or
   three lines and the whole block clears the bearing vertically. The rest
   step clockwise around it: right-up, right-down, left-down, left-up. */
.heroB__beat:nth-child(1) {
  --ox: 0vw; --oy: -6vh; --align: center; --talign: center;
  width: min(56ch, 60vw);
}
.heroB__beat:nth-child(1) .hero__ctas { margin-top: 22px; }
/* Offsets pushed out to clear the bigger stage-size bearing (JS bsz up to
   560px) plus the wider text columns above. */
.heroB__beat:nth-child(2) { --ox:  33vw; --oy:  -8vh; --align: flex-start; --talign: left;  }
.heroB__beat:nth-child(3) { --ox:  33vw; --oy:  18vh; --align: flex-start; --talign: left;  }
.heroB__beat:nth-child(4) { --ox: -33vw; --oy:  18vh; --align: flex-end;   --talign: right; }
.heroB__beat:nth-child(5) { --ox: -33vw; --oy:  -8vh; --align: flex-end;   --talign: right; }

/* Beat 1 arrives from above rather than the side, since that is where it
   lives. */
.heroB__beat:nth-child(1) { --dx: 0px; --dy: -30px; }
.heroB__beat--past:nth-child(1) { --dx: 0px; --dy: -34px; }

/* Beat 6: brand index, centred above the part like beat 1 but wider. */
.heroB__beat:nth-child(6) {
  --ox: 0vw; --oy: -14vh; --align: center; --talign: center;
  --dx: 0px; --dy: -30px;
  width: min(1180px, 90vw);
}
.heroB__beat--past:nth-child(6) { --dx: 0px; --dy: -34px; }
.heroB__brandgrid {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
  justify-content: center;
  margin-top: 18px;
}
.heroB__brandgrid a {
  font-family: var(--sans);
  font-weight: 600;
  font-size: 11.5px;
  letter-spacing: 0.06em;
  color: var(--body-muted);
  border: 1px solid var(--border-input);
  border-radius: 999px;
  padding: 7px 14px;
  background: rgba(255, 255, 255, 0.8);
}
.heroB__brandgrid a:hover { color: var(--blue); border-color: var(--blue); }
.heroB__hint {
  margin: 14px 0 0;
  font-family: var(--sans);
  font-weight: 600;
  font-size: 11.5px;
  letter-spacing: 0.06em;
  color: var(--faint);
}

/* Arrive from just outside the stop and leave the far side, so the movement
   continues around the bearing rather than bouncing back. */
.heroB__beat:nth-child(odd)  { --dx: -34px; }
.heroB__beat:nth-child(even) { --dx:  34px; }
.heroB__beat--past:nth-child(odd)  { --dx:  34px; --dy: -22px; }
.heroB__beat--past:nth-child(even) { --dx: -34px; --dy: -22px; }

/* flex-start / flex-end are valid for justify-content too, so the CTA row can
   reuse --align and stay on the same edge as the copy even when it wraps. */
/* MUST STAY LAST among the --dx/--dy rules: the entry-side offsets above tie
   on specificity, so the active beat's reset only wins by file order. Without
   this every beat rests ~34px toward its entry side, visibly off centre. */
.heroB__beat.heroB__beat--on { --dx: 0px; --dy: 0px; }

.heroB__beat .hero__ctas {
  justify-content: var(--align, flex-start);
  width: 100%;
  gap: 12px;
  margin-top: 24px;
}
.heroB__beat .btn-primary,
.heroB__beat .btn-secondary {
  font-size: 15.5px;
  padding: 15px 24px;
}
.heroB__beat .btn-secondary {
  color: var(--ink);
  border-color: var(--border-strong);
}
.heroB__beat .btn-secondary:hover { color: var(--blue); border-color: var(--blue); }
/* The full badge string does not fit a column this narrow at the comp's
   tracking, and a two-line pill looks broken. */
.heroB .hero__badge {
  font-size: 11px;
  letter-spacing: 0.09em;
  padding: 8px 15px;
  gap: 9px;
  white-space: nowrap;
  border-color: var(--border-strong);
  color: var(--body-muted);
  background: #fff;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}

/* Sized for a column beside the bearing, not for the full stage width.
   Scaled up 21 Jul: Hero B read small next to the enlarged SKF-style bands
   and Hero A. Widened columns (above) gave this room to grow into. */
.heroB__title {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(28px, 2.9vw, 46px);
  line-height: 1.04;
  letter-spacing: -0.018em;
  color: var(--ink);
  margin: 26px 0 0;
  text-transform: none;
}
.heroB__title em { font-style: normal; color: var(--blue); }
/* Beat 1 headline a size up and heavier (29 Sep ask: "Authorized Distributor
   for JK Fenner", bigger and bold). Stays lowercase: sitewide rule, 22 Jul. */
.heroB__title--lead {
  font-weight: 800;
  font-size: clamp(34px, 3.9vw, 62px);
  line-height: 1.02;
}
.heroB__title--sm { font-size: clamp(28px, 2.7vw, 42px); }
.heroB__sub {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(25px, 2.4vw, 37px);
  line-height: 1.1;
  letter-spacing: -0.012em;
  color: var(--ink);
  margin: 14px 0 0;
}
.heroB__lede {
  max-width: 52ch;
  color: var(--body-muted);
  font-size: 18.5px;
  line-height: 1.65;
  margin: 24px 0 0;
}
/* Both of these sit over MOVING footage, so they are a step darker than the
   equivalents on the static hero. At the comp's --faint the brand list
   measured 2.82:1 against the worst video frame; AA needs 4.5. */
.heroB__kicker {
  font-family: var(--sans);
  font-weight: 600;
  font-size: 13px;
  letter-spacing: 0.16em;
  text-transform: none;
  color: var(--blue);
}
.heroB__figure {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(68px, 7.8vw, 128px);
  line-height: 0.92;
  letter-spacing: -0.03em;
  color: var(--ink);
  margin-top: 10px;
  display: flex;
  align-items: baseline;
  gap: 10px;
}
.heroB__figure em {
  font-style: normal;
  font-size: clamp(18px, 1.6vw, 26px);
  letter-spacing: 0;
  color: var(--blue);
}
.heroB__lede { font-size: 16.5px; margin-top: 18px; }
.heroB__brands {
  font-family: var(--sans);
  font-weight: 600;
  font-size: 13px;
  letter-spacing: 0.08em;
  color: var(--body-muted);
  margin: 18px 0 0;
  line-height: 1.9;
}

/* Beat progress rail */
.heroB__rail {
  position: absolute;
  right: 34px;
  top: 50%;
  transform: translateY(-50%);
  z-index: 3;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.heroB__rail span {
  width: 2px;
  height: 26px;
  background: var(--border-strong);
  transition: background 0.35s ease, height 0.35s ease;
}
.heroB__rail span.on { background: var(--blue); height: 40px; }

.heroB__cue {
  position: absolute;
  left: 50%;
  bottom: 26px;
  transform: translateX(-50%);
  z-index: 3;
  font-family: var(--sans);
  font-weight: 600;
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: none;
  color: var(--muted);
  transition: opacity 0.4s ease;
}
.heroB__cue::after {
  content: "";
  display: block;
  width: 1px;
  height: 20px;
  margin: 8px auto 0;
  background: linear-gradient(var(--faint), transparent);
}

/* Short desktop windows (his laptop Chrome is ~830px tall): the vh offsets
   shrink but the text stack does not, so beat 1 ends up under the sticky
   header. Everything steps down one size and the bearing drops lower. */
@media (min-width: 901px) and (max-height: 860px) {
  .heroB__title { font-size: clamp(25px, 2.5vw, 37px); }
  .heroB__title--lead { font-size: clamp(30px, 3.3vw, 50px); }
  .heroB__figure { font-size: clamp(58px, 6.7vw, 100px); }
}


/* Half-screen windows. */
@media (min-width: 901px) and (max-height: 740px) {
  .heroB__title { font-size: clamp(22px, 2.2vw, 32px); }
  .heroB__lede { font-size: 15.5px; }
}


/* Under 900px there is no room to orbit: the bearing sits high and every beat
   stacks centred beneath it, still clear of the part. */
@media (max-width: 900px) {
  .heroB__beat {
    width: min(92vw, 520px);
    --ox: 0px !important;
    /* Bumped from 15vh: the bigger mobile bearing (up to 320px stage size,
       was 260px) reached into text sitting at the old fixed offset. */
    --oy: 21vh !important;
    --align: center !important;
    --talign: center !important;
  }
  .heroB__beat:nth-child(odd),
  .heroB__beat:nth-child(even) { --dx: 0px; }
  .heroB__beat--past:nth-child(odd),
  .heroB__beat--past:nth-child(even) { --dx: 0px; --dy: -22px; }
  .heroB__lede { font-size: 15.5px; }
  .heroB__brands { font-size: 11.5px; letter-spacing: 0.06em; }
  .heroB__figure { justify-content: center; }
  .heroB__sub { max-width: none; }
  .heroB__rail { right: 12px; }

  /* Beat 6's 24-pill grid wraps to ~570px tall at this width, which fills
     nearly the whole viewport and defeats any --oy offset (the block's own
     height dominates the -50%-self-centre transform math no matter what).
     A horizontal swipe strip bounds the height to one row regardless of pill
     count. Natural on a phone; the desktop grid is unaffected. */
  .heroB__brandgrid {
    flex-wrap: nowrap;
    justify-content: flex-start;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding: 2px 4px;
    margin: 14px -4px 0;
    mask-image: linear-gradient(90deg, transparent, #000 16px, #000 calc(100% - 16px), transparent);
  }
  .heroB__brandgrid::-webkit-scrollbar { display: none; }
  .heroB__brandgrid a { flex: none; }
  /* The strip has to be NARROWER than its content or there is nothing to
     scroll: without an explicit width it shrink-wrapped to 3647px, so 24 of
     the 28 brands sat off-screen with scrollLeft pinned at 0. The negative
     margin re-centres the 92vw box on a parent that is itself centred. */
  .heroB__beat:has(.heroB__brandgrid) { width: min(92vw, 560px); }
  .heroB__brandgrid { width: 100%; }
}
/* Beat 1 carries the bearing slot inside it, so the 21vh drop meant for the
   later beats left a blank band under the header and pushed its buttons
   below the fold on a phone (29 Sep). Full width also keeps the badge on
   one line. */
@media (max-width: 900px) and (min-height: 481px) {
  .heroB__beat:nth-child(1) { width: min(92vw, 520px); --oy: 3vh !important; }
}

/* Landscape phones: barely any vertical room at all. Same tier pattern as
   the desktop short-window overrides above, mirrored for mobile. */
@media (max-width: 900px) and (max-height: 480px) {
  .heroB__beat { --oy: 30vh !important; }
  .heroB__title { font-size: clamp(22px, 3.4vw, 30px); }
  .heroB__figure { font-size: clamp(40px, 8vw, 60px); }
  .heroB__lede { font-size: 14px; margin-top: 8px; }
  .motion .heroB .bearing-wrap--hero {
    width: clamp(120px, 26vw, 190px);
    height: clamp(120px, 26vw, 190px);
    top: 22%;
  }
}

/* Preview-only A/B switch. */
.heroSwitch {
  position: fixed;
  left: 18px;
  bottom: 18px;
  z-index: 80;
  display: flex;
  align-items: center;
  gap: 8px;
  background: rgba(11, 21, 38, 0.92);
  color: #fff;
  border-radius: 999px;
  padding: 8px 10px 8px 14px;
  font-family: var(--sans);
  font-weight: 600;
  font-size: 10.5px;
  letter-spacing: 0.14em;
  box-shadow: 0 10px 26px rgba(11, 21, 38, 0.3);
}
.heroSwitch a {
  color: rgba(255, 255, 255, 0.62);
  padding: 5px 10px;
  border-radius: 999px;
  letter-spacing: 0.1em;
}
.heroSwitch a:hover { color: #fff; }
.heroSwitch a.on { background: var(--blue); color: #fff; }

/* Hero B is a scroll narrative; without motion it reads as a stacked list.
   The beats are position:absolute at the stage centre for the pinned story,
   so the static layout MUST reset position too — transform/opacity alone
   leaves all six superimposed in a pile. (That latent bug shipped invisibly
   for weeks while hero B hid behind ?hero=b; it surfaced the day hero B
   became the default.) */
@media (prefers-reduced-motion: reduce) {
  .heroB { min-height: 0 !important; height: auto !important; }
  .heroB__stage { position: static !important; height: auto !important; padding: 72px 28px; overflow: visible !important; }
  .heroB__beat {
    position: static !important;
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    grid-area: auto !important;
    width: min(60ch, 100%) !important;
    margin: 0 auto !important;
    visibility: visible !important;
  }
  /* The beats' parent is position:absolute inset:0 in the pinned layout, so
     its static children can never grow the stage. It must join the flow. */
  .heroB__inner { display: block; position: static !important; inset: auto !important; pointer-events: auto !important; }
  .heroB__beat + .heroB__beat { margin-top: 64px !important; }
  .heroB__rail, .heroB__cue { display: none; }
  /* The bearing is hidden here (.bearing-wrap above), so its parking slot
     must not keep reserving a bearing-sized hole in beat one. */
  [data-bearing-slot] { display: none !important; }
}

/* ------------------------------------------------- safety net
   If the OS flips to reduced motion, drop everything immediately even
   before JS reacts. */
@media (prefers-reduced-motion: reduce) {
  .bearing-wrap { display: none; }
  .hero__ring, .industries__ring { display: block !important; }
  .rv, .rv-i { opacity: 1 !important; transform: none !important; transition: none !important; }
  [data-conveyor] { height: auto !important; }
  [data-conveyor-viewport] { position: static !important; height: auto !important; overflow: visible !important; }
  [data-conveyor-track] { transform: none !important; display: grid !important; grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr)); width: auto !important; }
  [data-conveyor-track] .card { width: auto !important; }
  .ind-row__fill { display: none; }
  .conveyor__rail { display: none !important; }
}

/* ---------------------------------------------------------- anatomy pin
   Same pattern as the conveyor: the section is pinned, page scroll is mapped
   to progress, and the page never stops scrolling at its normal rate. Scroll
   advances which range is shown ("with each scrolls it can change") and pulls
   that range's parts apart and back together.

   Everything here is inside .motion, so the static exploded diagram in
   style.css is what reduced-motion and no-JS visitors get. */
@media (min-width: 901px) {
  .motion [data-anat] {
    position: relative;
    padding-top: 0;
    padding-bottom: 0;
  }
  .motion [data-anat-viewport] {
    position: sticky;
    top: 0;
    height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding-block: 52px;
    box-sizing: border-box;
  }
  /* The five ranges collapse onto one another and cross-fade, so the pin only
     ever shows the active one. grid-area 1/1 stacks them without absolute
     positioning, which keeps the tallest one setting the height. */
  .motion .anat__deck {
    display: grid;
    gap: 0;
    margin-top: 26px;
  }
  .motion .anat__item {
    grid-area: 1 / 1;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.32s ease;
  }
  .motion .anat__item.is-on {
    opacity: 1;
    pointer-events: auto;
  }
  /* JS drives per-part opacity during the separation; give it something to
     interpolate from so the first frame is not a pop. */
  .motion .anat__li { transition: opacity 0.18s linear; }

  /* Inside a 100vh pin the diagram has to be bounded by HEIGHT, not width.
     Left to `width:100%; height:auto` it scales off the column width, so on a
     wide-but-short window (1440x720) the drawing alone came to ~480px and blew
     the pin even though the labels fitted fine. Giving the svg both dimensions
     lets preserveAspectRatio letterbox it: it shrinks to fit and stays
     centred, and the parts keep their proportions. */
  .motion .anat__svg {
    width: 100%;
    height: min(46vh, 440px);
    overflow: visible;
  }
}

/* Under 901px the pin is dropped entirely and the static stacked deck from
   style.css stands, matching how the conveyor degrades to a swipe carousel. */

/* ------------------------------------------------------- brand carousel
   Arrows and the progress rail are wired in motion.js but bound before its
   reduced-motion return, because they are controls rather than decoration.
   Only the smooth scrolling is a motion concern. */
@media (prefers-reduced-motion: reduce) {
  .brandc__viewport { scroll-behavior: auto; }
}

/* The pinned stage is a fixed 100vh but its content is not, so the section
   head has to earn its space. Inside the pin the intro paragraph goes (it is
   read in the static/mobile deck, where there is room) and the switcher loses
   its rule, which buys back roughly 90px before any type is touched.

   The five-part bearing item is the tallest thing here, and its label list is
   what sets the height, so the short tiers below tighten the list rather than
   the diagram. Same step-down approach as the hero B beats above: spacing
   first, then type, rather than letting anything clip. */
@media (min-width: 901px) {
  .motion .anat__intro { display: none; }
  /* With the intro gone the two-column head would leave the heading sitting
     next to dead space, so it goes full width and stops wrapping. */
  .motion .anat__head { grid-template-columns: 1fr; }
  .motion .anat__tabs {
    margin-top: 20px;
    padding-bottom: 0;
    border-bottom: 0;
  }
}
@media (min-width: 901px) and (max-height: 860px) {
  .motion [data-anat-viewport] { padding-block: 40px; }
  .motion .anat__deck { margin-top: 16px; }
  .motion .anat__li { padding: 8px 0; }
}
@media (min-width: 901px) and (max-height: 760px) {
  .motion [data-anat-viewport] { padding-block: 28px; }
  .motion .anat__tabs { margin-top: 14px; }
  .motion .anat__title { font-size: clamp(22px, 2.1vw, 30px); }
  .motion .anat__li { padding: 6px 0; }
  .motion .anat__li b { font-size: 13.5px; }
  .motion .anat__li span { font-size: 13px; line-height: 1.45; }
  /* A short window is almost always a WIDE one, and the label list is what
     sets this item's height. Two columns costs nothing horizontally here and
     takes roughly 40% off the stack, which beats shrinking the type again. */
  .motion .anat__parts {
    grid-template-columns: 1fr 1fr;
    column-gap: 26px;
  }
}
@media (min-width: 901px) and (max-height: 680px) {
  .motion [data-anat-viewport] { padding-block: 20px; }
  .motion .anat__deck { margin-top: 10px; }
  .motion .anat__li { padding: 4px 0; }
  .motion .anat__li span { font-size: 12.5px; line-height: 1.4; }
  .motion .anat__cta { margin-top: 12px; }
  .motion .anat__svg { height: 40vh; }
}

/* Brand wall on short desktop windows (3 Oct): the larger logos need the
   beat to sit lower and pack tighter so it clears the header and the part. */
@media (min-width: 901px) and (max-height: 820px) {
  .heroB__beat:nth-child(6) { --oy: -10vh; }
  .heroB__brandgrid { gap: 6px; margin-top: 12px; }
  .heroB__brandgrid a { min-height: 40px; min-width: 84px; padding: 6px 12px; }
  .heroB__brandgrid a img { height: 24px; max-width: 100px; }
}
@media (min-width: 901px) and (max-width: 1180px) {
  .heroB__beat:nth-child(6) { width: 94vw; }
  .heroB__brandgrid a { min-width: 80px; padding: 6px 10px; }
  .heroB__brandgrid a img { height: 24px; max-width: 96px; }
}


/* ------------------------------------------- hero B background video (6 Oct)
   His ask: the white theme with the robotic footage from the first hero.
   The footage sits at the back under a white wash, so the copy stays on a
   near-white ground. The 2D bearing frames are baked on white, so they
   multiply onto the wash and never show their square. */
.heroB__video {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; z-index: 0; pointer-events: none;
}
.heroB__videowash {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: linear-gradient(180deg, rgba(255,255,255,0.90) 0%, rgba(255,255,255,0.84) 55%, rgba(255,255,255,0.78) 100%);
}
.bearing-shine, .bearing-shine__img { mix-blend-mode: multiply; }
.heroB__bg { display: none !important; }
@media (prefers-reduced-motion: reduce) {
  .heroB__video { display: none; }
  .heroB__videowash { background: #fff url(/assets/video/hero-poster.jpg) center / cover; background-blend-mode: lighten; opacity: 0.9; }
}
