/* ==========================================================================
   Motion — parallax-v1

   Animation layer for the BH theme. Everything that hides content is gated
   on `html.js` (set by motion.js on its first line), so a blocked or failed
   script degrades to the plain static site instead of a blank page.

   Timing language (shared with the easings already used on the site):
     --ease-out   expo out — reveals, hovers
     --ease-mask  curtain  — clip wipes, the page curtain
   ========================================================================== */

:root {
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-mask: cubic-bezier(0.77, 0, 0.175, 1);
  --dur-reveal: 900ms;
  --dur-mask: 1200ms;
}

/* ==========================================================================
   1. Page curtain — the single transition language between pages.

   Arrival:   curtain starts covering, slides up and away.
   Departure: curtain rises from below to cover, navigation proceeds.

   The element replaces the old #preloader div (same position in header.php,
   same z-index role). A subtle border-radius on the leading edge keeps the
   wipe from feeling like a hard cut.
   ========================================================================== */

.curtain {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: #292f38; /* matches the old preloader + line footer tone */
  transform: translate3d(0, 0, 0);
  pointer-events: none;
  visibility: hidden;
}

/* Arrival state — set by JS before first paint via the inline class. */
html.js .curtain {
  visibility: visible;
}

html.js .curtain.is-uncovering {
  transform: translate3d(0, -102%, 0);
  border-radius: 0 0 6% 6% / 0 0 3% 3%;
  transition:
    transform 1000ms var(--ease-mask) 120ms,
    border-radius 1000ms var(--ease-mask) 120ms;
}

html.js .curtain.is-covering {
  transform: translate3d(0, 0, 0);
  border-radius: 6% 6% 0 0 / 3% 3% 0 0;
  transition:
    transform 700ms var(--ease-mask),
    border-radius 700ms var(--ease-mask);
}

/* No-JS: the curtain must never sit over the page. */
html:not(.js) .curtain {
  display: none;
}

/* --- Preloader counter ---------------------------------------------------
   First visit per session only (JS adds .is-counting). Big tabular figure
   bottom-left, hairline progress bar along the bottom edge. On completion
   (.is-done) both exit upward just before the curtain itself lifts. */

.curtain__count {
  position: absolute;
  left: 4vw;
  bottom: 48px;
  font-size: clamp(64px, 9vw, 132px);
  line-height: 1;
  font-weight: 500;
  letter-spacing: -0.02em;
  color: #e4e7ec;
  font-variant-numeric: tabular-nums; /* no width jitter as digits change */
  opacity: 0;
}

.curtain__bar {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 2px;
  background: rgba(228, 231, 236, 0.14);
  opacity: 0;
}

.curtain__bar-fill {
  width: 100%;
  height: 100%;
  background: #e4e7ec;
  transform: scaleX(0);
  transform-origin: left center;
}

.curtain.is-counting .curtain__count,
.curtain.is-counting .curtain__bar {
  opacity: 1;
  transition: opacity 400ms ease 120ms;
}

.curtain.is-done .curtain__count {
  opacity: 0;
  transform: translate3d(0, -24px, 0);
  transition:
    opacity 350ms ease,
    transform 500ms var(--ease-out);
}

.curtain.is-done .curtain__bar {
  opacity: 0;
  transition: opacity 300ms ease 100ms;
}

/* ==========================================================================
   2. Reveal primitives — JS adds .is-in when the element crosses ~12% of the
   viewport. All variants share the same duration/easing so the page speaks
   one motion language.
   ========================================================================== */

html.js [data-reveal] {
  opacity: 0;
  transform: translate3d(0, 32px, 0);
  transition:
    opacity var(--dur-reveal) var(--ease-out) var(--reveal-delay, 0ms),
    transform var(--dur-reveal) var(--ease-out) var(--reveal-delay, 0ms);
  will-change: opacity, transform;
}

html.js [data-reveal].is-in {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}

/* Added by JS ~1.5s after is-in: the reveal has played, so the transition
   is dead weight. Magnetic elements write transform every frame and a
   lingering 900ms transition smears each write into a trail. */
html.js [data-reveal].is-settled {
  transition: none;
  will-change: auto;
}

/* The footer CTAs are the one exception: they carry an authored colour
   hover of their own (redesign.css). Killing the transition wholesale left
   that colour change snapping with no fade, where the live site eases it,
   so the colour transition is restored here. Colour ONLY — the transform
   side stays dead so the magnetic writes keep their clean lerp. */
html.js .page-footer__link.is-settled,
html.js .page-footer__link-wrapper a.is-settled {
  transition: color 450ms var(--ease-out);
}

/* --- 2b. Gallery photos — the one decorated image variant --------------
   A masonry grid reads as a set, so the generic 32px rise is too polite:
   these tiles get three moves on one beat.

     .gallery__item      a mask wipes up off the tile while it rises and
                         settles back from 96% — the frame opens.
     .gallery__item img  the photo sits at 112% inside that frame and eases
                         back to 100%, so the subject pulls out through the
                         opening mask. Deliberately slower than the tile
                         (700ms vs 450ms) — the photo is still settling
                         when the frame has landed, which is what makes the
                         wipe read as depth rather than a resize.
     --reveal-delay      a wider 55ms per-tile stagger, written by JS.

   Every timing here was halved on 2026-11-11 (was 900/1000/1150ms for the
   tile, 1400ms for the photo, a 110ms stagger) at the client's request:
   the reveal now plays at 2x. Nothing downstream needed retuning — the
   longest move is still the photo at 700ms, comfortably inside the 1500ms
   JS settle that kills the transitions, and the relative order of the
   three tile moves (fade, rise, wipe) is unchanged.

   clip-path is safe on these nodes: no blend modes, and Masonry owns
   left/top on the tiles (never transform), so the stacking context it
   creates costs nothing and the layout engine's reflows are untouched.
   The 16% inset is measured from the top edge only, so the mask never
   shaves a neighbour's gutter.

   Specificity: (0,4,1) here beats the (0,2,1) of the generic rule above,
   so the generic transform never fights this one. */
html.js .gallery .gallery__item[data-reveal] {
  opacity: 0;
  transform: translate3d(0, 46px, 0) scale(0.96);
  clip-path: inset(16% 0 0 0);
  transition:
    opacity 450ms var(--ease-out) var(--reveal-delay, 0ms),
    transform 500ms var(--ease-out) var(--reveal-delay, 0ms),
    clip-path 575ms var(--ease-out) var(--reveal-delay, 0ms);
  will-change: opacity, transform, clip-path;
}

html.js .gallery .gallery__item[data-reveal] img {
  transform: scale(1.12);
  transform-origin: 50% 60%;
  transition: transform 700ms var(--ease-out) var(--reveal-delay, 0ms);
  will-change: transform;
}

html.js .gallery .gallery__item[data-reveal].is-in {
  opacity: 1;
  transform: translate3d(0, 0, 0) scale(1);
  clip-path: inset(0 0 0 0);
}

html.js .gallery .gallery__item[data-reveal].is-in img {
  transform: scale(1);
}

/* Settled — same rationale as above, extended to the inner photo, which
   carries its own transition the generic rule knows nothing about. */
html.js .gallery .gallery__item[data-reveal].is-settled,
html.js .gallery .gallery__item[data-reveal].is-settled img {
  transition: none;
  will-change: auto;
}

/* ==========================================================================
   3. Split-line hero title — each line rides up out of an overflow mask
   with a trace of rotation. The signature text move; used on
   .page-header__title only.
   ========================================================================== */

html.js .split-line {
  display: block;
  overflow: hidden;
}

html.js .split-line-inner {
  display: block;
  transform: translate3d(0, 110%, 0) rotate(2.5deg);
  transform-origin: 0 100%;
  will-change: transform;
}

html.js .is-in .split-line-inner,
html.js .split-lines.is-in .split-line-inner {
  transform: translate3d(0, 0, 0) rotate(0deg);
  transition: transform 1000ms var(--ease-out) calc(var(--i, 0) * 90ms);
}

/* Per-character variant — the hero title. Each char rides out of the line
   mask with a trace of rotation, staggered 26ms across the whole title. */
html.js .split-char {
  display: inline-block;
  transform: translate3d(0, 118%, 0) rotate(9deg);
  transform-origin: 0 100%;
  will-change: transform;
}

html.js .is-in .split-char,
html.js .split-lines.is-in .split-char {
  transform: translate3d(0, 0, 0) rotate(0deg);
  transition: transform 1150ms var(--ease-out) calc(var(--i, 0) * 26ms);
}

/* ==========================================================================
   4. Line decorations — OPACITY ONLY.

   The pseudo-elements carry two properties that any fancier reveal breaks:

     transform: translateX(-50%)   — the horizontal centering. Any transform
                                     written by the reveal REPLACES it and
                                     the artwork jumps half a width right.
     mix-blend-mode: multiply      — what dims the white SVG arcs into the
                                     navy sections and blends them across
                                     the hero seam. clip-path / transform /
                                     will-change isolate the element and the
                                     arcs render bright white.

   A slow fade is the only animation that preserves both, and against the
   multiply blending it reads as the artwork materialising out of the
   section — which suits these decorations better than a wipe anyway.
   ========================================================================== */

html.js .line:after,
html.js .line:before {
  opacity: 0;
  /* The artwork starts 56px low and rises as it fades. background-position
     is a paint-level property: no stacking context, so multiply blending
     survives, and no transform, so the translateX(-50%) centering survives.
     Every variant's authored Y is `top` (0), so returning to 0 is exact.
     Browsers without background-position-y support just get the fade. */
  background-position-y: 56px;
  transition:
    opacity 1500ms var(--ease-out) var(--reveal-delay, 0ms),
    background-position-y 1800ms var(--ease-out) var(--reveal-delay, 0ms);
}

/* :before and :after reveal simultaneously — the footer pair reads as one
   composition, so it materialises as one. */
html.js .line.is-in:after,
html.js .line.is-in:before {
  opacity: 0.1;
  background-position-y: 0;
}

/* Keep the password page's dimmed lines at their authored opacity. */
.protected .line.is-in:after,
.protected .line.is-in:before {
  opacity: 0.3;
}

/* Full-bleed dividers land fully opaque once the reveal has settled — the
   fade above is the entrance, not the resting state. (0,5,2) outranks the
   .is-in fade (0,3,2) so the landing rides the fade's own 1500ms easing to 1
   rather than snapping, and it also outranks the authored faint dividers
   (.companies-logos-block / .img-box_full / .img-box_bg), which therefore
   read 1 too. The two page-specific !important rules still win as before:
   .page-id-3965 .line.line_full::after and .single-post .line_box-2::after
   stay at 0.5.

   Light theme ONLY — the landing is scoped to html.light-theme so the dark
   theme is never forced: this selector used to be plain `html.js`, which
   meant a full divider in dark was yanked to 1 the moment it settled
   instead of resting on the fade above. Keep it light-only. */
html.js.light-theme .line.line_full.is-in.is-settled:after {
  opacity: 1;
}

/* --------------------------------------------------------------------------
   4b. Owner overrides (2026-11-09) — key line art: no rise.

   [1] `.line_footer` — the closing pair of arcs in footer.php. It is a
       zero-height div at the very end of the document, so its `is-in`
       arrives only from the page-bottom failsafe in motion.js: the moment
       the visitor reaches the last pixel the artwork used to fade in from
       56px below its resting position. Both beats are gone — the arcs sit
       at the authored Y from first paint and hold the resting opacity the
       fade used to land on (0.1 dark; light already pins 0.5 !important in
       overrides.css, so it never faded here anyway). `transition: none`
       also stops the opacity from animating if a class lands later.

       Scoped with `body:not(.protected)` so the password gate keeps the
       placement it was tuned with (.protected .line_footer, bottom -40vh).

   [2] Case-study line art (`.line_box-1` / `.line_box-2` — the pair the
       black-info-block opens every case study with) keeps its 1500ms fade
       and loses the rise: only background-position-y is pinned, so the
       authored X and every breakpoint's `top` still apply, and the opacity
       transition runs as before.

   background-position-y is the only property that can carry this motion —
   see the note above: a transform here destroys translateX(-50%) and the
   multiply blending, which is why the rise is painted, not transformed.
   -------------------------------------------------------------------------- */

html.js body:not(.protected) .line.line_footer:before,
html.js body:not(.protected) .line.line_footer:after {
  background-position-y: 0 !important;
  transition: none !important;
}

html.js:not(.light-theme) body:not(.protected) .line.line_footer:before,
html.js:not(.light-theme) body:not(.protected) .line.line_footer:after {
  opacity: 0.1 !important;
}

html.js .line.line_box-1:before,
html.js .line.line_box-1:after,
html.js .line.line_box-2:before,
html.js .line.line_box-2:after {
  background-position-y: 0 !important;
}

/* ==========================================================================
   5. Hero — pointer warp + scroll drift.

   Two elements move on separate axes so they never fight:
     .page-header__bg-image   translate from pointer + scroll (JS, lerped)
     .page-header__circle     scroll drift only (JS owns transform)

   The bg is NOT pre-scaled: a fixed overscale magnifies the photo's circular
   pattern about the image centre, so the disc stops registering with it. The
   plate rests at scale 1 (matching the live site) and JS derives the cover
   scale per frame from the pointer travel actually in play.
   ========================================================================== */

html.js .page-header__bg-image {
  will-change: transform;
}

html.js .page-header__circle {
  will-change: transform;
}

/* Disc interactivity — REMOVED at request. The :before pseudo used to
   lean toward the pointer via a lerped --pull-x/--pull-y pair, and before
   that a :hover "breathe" (a 4% grow on width/height with countering
   margins). Both are gone, along with every transition that fed them.
   The pseudo now keeps the plain `left: 0; top: 0` from main.css, so the
   disc holds a fixed centre. Its mix-blend-mode: multiply is why the pull
   was done at paint level with left/top in the first place — if a lean is
   ever wanted again, keep it off `transform`, which isolates the blend.

   The title sits on .page-header__circle-inner and does NOT move with
   the disc. */

/* Opacity-only entrance, matching production. The live site fades the disc in
   with AOS `fade-in` — 1500ms on the default `ease` — and only from 1200px up,
   because AOS is initialised with `disable: () => innerWidth < 1200`, so on
   smaller viewports the disc is simply there with no entrance at all.

   Deliberate deviation, kept on purpose: measured on live at 375/768/991px the
   disc does drift up over ~1000ms, but that is the #preloader fade clearing on
   top of it, not a disc entrance of its own. Copying it would mean hiding the
   disc below 1200px behind an is-ready gate — a JS/font failure would then
   leave the hero with no disc at all on phones, which is a worse trade than a
   snap-in the preloader already masks. So: entrance >=1200px only.

   The is-ready gate stays: it starts the fade as the curtain lifts, which is
   where production's #preloader fade sits. The circle's transform belongs to
   the scroll-drift tick in motion.js, so this must never animate transform —
   that conflict is what stuttered v0. fill:both keeps it visible afterwards. */
@media (min-width: 1200px) {
  html.js .page-header__circle.circle-enter {
    opacity: 0;
  }

  html.js.is-ready .page-header__circle.circle-enter {
    animation: circle-in 1500ms ease both;
  }
}

@keyframes circle-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* ==========================================================================
   6. Hover polish — small, consistent, and only where the design already
   invites interaction.
   ========================================================================== */

/* Work list rows: the existing color-bar wipe stays; add a quiet text
   shift so the row feels alive rather than just recolored. */
@media (hover: hover) and (pointer: fine) {
  .posts__item-inner {
    transition: padding-left 500ms var(--ease-out);
  }

  .posts__item:hover .posts__item-inner {
    padding-left: 12px;
  }

  .posts__item-color {
    transition: width 500ms var(--ease-out);
  }

  /* Social icons + the generic footer CTA. These are magnetic (JS writes
     transform per-frame), so the transition must cover opacity ONLY — a
     transform transition here would smear every lerped write.
     "Next case ›" (.page-footer__link-wrapper a) is deliberately absent:
     it keeps its authored color hover from main.css, nothing more. */
  .page-footer__link,
  .page-footer__s-item {
    transition: opacity 400ms var(--ease-out);
  }

  .page-footer__s-item:hover {
    opacity: 0.85;
  }
}

/* Magnetic elements get their transform written by JS; transitions must
   stay off transform or the lerp stutters. */
[data-magnetic] {
  will-change: transform;
}

/* ==========================================================================
   7. Password-protected page — the form is the whole experience, so it gets
   a staged entrance and a real focus state instead of appearing fully
   formed.
   ========================================================================== */

/* The v2 gate markup (.pw-form__*) gets a staged entrance — each line
   lands a beat after the last. An earlier rule animated
   `.page-container > *` with fill:both — animation fill overrides the
   `opacity: 0` that hides the header/footer on protected pages, which
   made the hidden chrome visible. Do not bring that selector back. */

html.js .protected .pw-form__title {
  animation: protected-in 1000ms var(--ease-out) 220ms both;
}

html.js .protected .pw-form__text {
  animation: protected-in 900ms var(--ease-out) 340ms both;
}

html.js .protected .pw-form__field {
  animation: protected-in 900ms var(--ease-out) 470ms both;
}

html.js .protected .pw-form__skip {
  animation: protected-in 900ms var(--ease-out) 600ms both;
}

@keyframes protected-in {
  from {
    opacity: 0;
    transform: translate3d(0, 28px, 0);
  }
  to {
    opacity: 1;
    transform: translate3d(0, 0, 0);
  }
}

/* Wrong-password feedback — the sessionStorage handshake in motion.js
   adds .is-error after a failed attempt: the field shakes and the error
   line + red underline appear (redesign.css §6f). */
.protected form.is-error .pw-form__field {
  animation: protected-shake 500ms var(--ease-out);
}

@keyframes protected-shake {
  0%, 100% { transform: translate3d(0, 0, 0); }
  20% { transform: translate3d(-8px, 0, 0); }
  40% { transform: translate3d(8px, 0, 0); }
  60% { transform: translate3d(-5px, 0, 0); }
  80% { transform: translate3d(5px, 0, 0); }
}

/* ==========================================================================
   8. Custom cursor — lerped ring + dot. The site alternates navy and white
   sections, so a fixed-colour cursor disappears on half the page:
   `difference` inverts against whatever is underneath. The `cursor: none`
   rules are gated on `html.has-cursor`, which JS only sets once the cursor
   is mounted and tracking — a script failure never leaves the page with no
   visible pointer.
   ========================================================================== */

.bh-cursor {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 10001;
  pointer-events: none;
  opacity: 0;
  transition: opacity 300ms ease;
  mix-blend-mode: difference;
}

.bh-cursor.is-active {
  opacity: 1;
}

/* Over text fields the native I-beam takes over — the password gate needs
   a visible caret target. */
.bh-cursor.is-text {
  opacity: 0;
}

.bh-cursor__ring {
  position: fixed;
  top: -19px;
  left: -19px;
  width: 38px;
  height: 38px;
  border: 1px solid rgba(255, 255, 255, 0.6);
  border-radius: 50%;
  will-change: transform;
  transition:
    top 260ms var(--ease-out),
    left 260ms var(--ease-out),
    width 260ms var(--ease-out),
    height 260ms var(--ease-out),
    background-color 260ms ease;
}

.bh-cursor__dot {
  position: fixed;
  top: -3px;
  left: -3px;
  width: 6px;
  height: 6px;
  background: #fff;
  border-radius: 50%;
  will-change: transform;
}

.bh-cursor.is-link .bh-cursor__ring {
  top: -29px;
  left: -29px;
  width: 58px;
  height: 58px;
  background: rgba(255, 255, 255, 0.08);
}

@media (hover: hover) and (pointer: fine) {
  html.has-cursor body,
  html.has-cursor a,
  html.has-cursor button,
  html.has-cursor .posts__item-inner {
    cursor: none;
  }

  html.has-cursor input,
  html.has-cursor textarea,
  html.has-cursor select,
  html.has-cursor #wpadminbar,
  html.has-cursor #wpadminbar * {
    cursor: auto;
  }
}

/* ==========================================================================
   9. Reduced motion — every animation collapses to instant visibility.
   The JS side checks the same media query and skips observers entirely.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  html.js [data-reveal],
  html.js .split-line-inner,
  html.js .split-char,
  html.js .line:after,
  html.js .line:before {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }

  html.js .page-header__circle.circle-enter {
    animation: none !important;
    opacity: 1 !important;
  }

  /* Gallery tiles stack clip-path and an inner photo scale on top of the
     reveal transform — neither is covered by the rule above, and an
     un-reset mask would leave the tiles permanently cropped. */
  html.js .gallery .gallery__item[data-reveal],
  html.js .gallery .gallery__item[data-reveal] img {
    clip-path: none !important;
    transform: none !important;
    transition: none !important;
  }

  .bh-cursor {
    display: none !important;
  }

  .curtain__count,
  .curtain__bar {
    display: none !important;
  }

  .curtain {
    display: none !important;
  }

  html.js .protected .pw-form__title,
  html.js .protected .pw-form__text,
  html.js .protected .pw-form__field,
  html.js .protected .pw-form__skip {
    animation: none !important;
  }
}
