/* ==========================================================================
   HJ Digital Solutions — motion layer
   Loaded after styles.css. Everything here is additive.

   Rules this file holds itself to:
   - transform / opacity / filter only. Never width, height, top, left.
   - custom cubic-bezier or linear() easing. Never a bare `ease`.
   - enter = opacity + translateY + blur ("materialising"), exits subtler.
   - no looping attention-seeking motion (no pulsing CTAs, no glowing rings).
   - every effect has a prefers-reduced-motion path at the bottom of the file.
   ========================================================================== */

/* Typed custom properties so these can actually interpolate rather than
   snapping between string values. */
@property --sheen {
  syntax: '<percentage>';
  initial-value: -40%;
  inherits: false;
}
@property --ring {
  syntax: '<number>';
  initial-value: 0;
  inherits: false;
}

:root {
  /* Easings. Named by feel, not by curve. */
  --ease-out-soft:  cubic-bezier(0.22, 1, 0.36, 1);
  --ease-out-firm:  cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out-ui: cubic-bezier(0.65, 0, 0.35, 1);
  /* A spring with no overshoot — professional, not bouncy. */
  --ease-spring: linear(
    0, 0.006, 0.025, 0.055, 0.096, 0.147, 0.207, 0.276, 0.351, 0.433,
    0.519, 0.607, 0.695, 0.78, 0.86, 0.932, 0.995, 1.048, 1.09, 1.121,
    1.142, 1.153, 1.156, 1.152, 1.144, 1.132, 1.118, 1.104, 1.09, 1.077,
    1.066, 1.057, 1.049, 1.043, 1.038, 1.034, 1.031, 1.028, 1.02, 1.014,
    1.009, 1.005, 1.002, 1
  );

  --dur-fast: 180ms;
  --dur-base: 340ms;
  --dur-slow: 620ms;
}

/* ---------------------------------------------------- scroll progress -- */

.scroll-progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  z-index: 60;
  transform-origin: 0 50%;
  transform: scaleX(0);
  background: linear-gradient(90deg, var(--accent-deep), var(--accent));
  pointer-events: none;
}

/* Native scroll-driven where supported — no JS, no scroll listener. */
@supports (animation-timeline: scroll()) {
  .scroll-progress {
    animation: progress-grow linear;
    animation-timeline: scroll(root block);
  }
}
@keyframes progress-grow { to { transform: scaleX(1); } }

/* ------------------------------------------------------ reveal system -- */
/* Enter recipe: opacity + translateY + blur. Staggered by --i, set in JS. */

.m-reveal {
  opacity: 0;
  transform: translateY(14px);
  filter: blur(6px);
  transition:
    opacity var(--dur-slow) var(--ease-out-soft),
    transform var(--dur-slow) var(--ease-out-soft),
    filter var(--dur-slow) var(--ease-out-soft);
  transition-delay: calc(var(--i, 0) * 70ms);
}
.m-reveal.is-in {
  opacity: 1;
  transform: none;
  filter: blur(0);
}

/* Headline words rise out of their own line box. */
.m-line { display: block; overflow: hidden; padding-bottom: 0.04em; }
.m-word {
  display: inline-block;
  transform: translateY(105%) rotate(2deg);
  opacity: 0;
  transition:
    transform 780ms var(--ease-out-firm),
    opacity 520ms var(--ease-out-soft);
  transition-delay: calc(var(--i, 0) * 55ms);
  will-change: transform;
}
.is-in .m-word { transform: none; opacity: 1; }
/* Drop the hint once the entrance is done — will-change is a temporary hint,
   not a permanent decoration. */
.is-settled .m-word { will-change: auto; }

/* ------------------------------------------------------------ buttons -- */

.btn { position: relative; overflow: hidden; transform: translate3d(0,0,0); }

/* Tactile press. Fast, because it is feedback rather than decoration. */
.btn:active { transform: scale(0.975); transition-duration: 90ms; }

/* A sheen that crosses the button once on hover. --sheen is @property-typed
   above, which is the only reason it can interpolate at all. */
.btn::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    100deg,
    transparent 0%,
    rgba(255, 255, 255, 0.22) 45%,
    transparent 70%
  );
  transform: translateX(var(--sheen));
  transition: --sheen 620ms var(--ease-out-firm);
  pointer-events: none;
}
.btn:hover::after { --sheen: 140%; }
.btn--secondary::after { background: linear-gradient(100deg, transparent 0%, rgba(16,34,47,0.07) 45%, transparent 70%); }

/* Magnetic pull. JS writes --mx/--my directly; kept off the main transform
   so :active scale and the magnet never fight each other. */
.btn[data-magnetic] { transition: transform 420ms var(--ease-spring); }
.btn[data-magnetic] > span { display: inline-block; transition: transform 420ms var(--ease-spring); }

/* ------------------------------------------------------------- cards -- */
/* Shadows rather than borders: they sit correctly on any background and
   transition smoothly, which a solid border cannot. */

.card {
  position: relative;
  border-color: transparent;
  box-shadow:
    0 0 0 1px rgba(16, 34, 47, 0.07),
    0 1px 2px -1px rgba(16, 34, 47, 0.06),
    0 2px 6px 0 rgba(16, 34, 47, 0.03);
  transform-style: preserve-3d;
  transition:
    box-shadow var(--dur-base) var(--ease-out-soft),
    transform var(--dur-base) var(--ease-spring);
}

.card:hover {
  box-shadow:
    0 0 0 1px rgba(16, 34, 47, 0.10),
    0 8px 18px -8px rgba(16, 34, 47, 0.18),
    0 18px 40px -20px rgba(16, 34, 47, 0.22);
}

/* Pointer-tracked spotlight. Coordinates come from JS as raw pixel values. */
.card[data-tilt]::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out-soft);
  background: radial-gradient(
    380px circle at var(--px, 50%) var(--py, 50%),
    rgba(180, 101, 15, 0.10),
    transparent 65%
  );
  pointer-events: none;
}
.card[data-tilt]:hover::before { opacity: 1; }

.card > * { position: relative; z-index: 1; }

/* ------------------------------------------------- process / lifecycle -- */
/* The rail fills as the reader moves through the steps. This is functional:
   it encodes progress through a real sequence, so it earns its place. */

.steps { position: relative; }

.steps-rail {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 2px;
  background: var(--rule);
  overflow: hidden;
}
.steps-rail::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--accent);
  transform: scaleX(var(--fill, 0));
  transform-origin: 0 50%;
  transition: transform 900ms var(--ease-out-soft);
}

.steps li { border-top-color: transparent; }

.steps li::before { transition: color var(--dur-base) var(--ease-out-soft); }

.step-dot {
  position: absolute;
  top: -5px;
  left: 0;
  width: 12px;
  height: 12px;
  border-radius: 3px;
  background: var(--surface);
  box-shadow: 0 0 0 2px var(--rule);
  transform: scale(0.9);
  transition:
    box-shadow var(--dur-base) var(--ease-out-soft),
    transform var(--dur-base) var(--ease-spring),
    background-color var(--dur-base) var(--ease-out-soft);
}
.steps li.is-active .step-dot {
  background: var(--accent);
  box-shadow: 0 0 0 2px var(--accent);
  transform: scale(1);
}

/* --------------------------------------------------------- hero mesh -- */
/* The device mesh draws itself once on load, then holds. */

.hero-visual { position: relative; }

.mesh-link {
  stroke-dasharray: var(--len, 200);
  stroke-dashoffset: var(--len, 200);
  animation: mesh-draw 1100ms var(--ease-out-firm) forwards;
  animation-delay: calc(320ms + var(--i, 0) * 110ms);
}
@keyframes mesh-draw { to { stroke-dashoffset: 0; } }

.mesh-node {
  opacity: 0;
  transform-box: fill-box;
  transform-origin: center;
  transform: scale(0.92);
  animation: mesh-pop 520ms var(--ease-spring) forwards;
  animation-delay: calc(760ms + var(--i, 0) * 90ms);
}
@keyframes mesh-pop { to { opacity: 1; transform: none; } }

.mesh-label {
  opacity: 0;
  animation: mesh-fade 460ms var(--ease-out-soft) forwards;
  animation-delay: calc(1000ms + var(--i, 0) * 90ms);
}
@keyframes mesh-fade { to { opacity: 1; } }

/* A single packet travelling the path — ambient, low contrast, and slow
   enough that it reads as atmosphere rather than as something demanding
   to be looked at. */
.mesh-packet {
  offset-path: var(--path);
  offset-rotate: 0deg;
  animation: mesh-travel 4.5s linear infinite;
  animation-delay: calc(1600ms + var(--i, 0) * 1.5s);
  opacity: 0.7;
}
@keyframes mesh-travel {
  0%   { offset-distance: 0%;   opacity: 0; }
  12%  { opacity: 0.7; }
  88%  { opacity: 0.7; }
  100% { offset-distance: 100%; opacity: 0; }
}

/* ------------------------------------------------------- nav / header -- */
/* Emil's territory: navigation is used constantly, so it stays quick. */

.site-header {
  transition: box-shadow var(--dur-base) var(--ease-out-soft),
              background-color var(--dur-base) var(--ease-out-soft);
}
.site-header.is-stuck {
  box-shadow: 0 1px 0 0 rgba(16,34,47,0.08), 0 6px 20px -12px rgba(16,34,47,0.28);
  background: rgba(255, 255, 255, 0.88);
  backdrop-filter: saturate(1.6) blur(10px);
  -webkit-backdrop-filter: saturate(1.6) blur(10px);
}

.nav a:not(.btn) { position: relative; }
.nav a:not(.btn)::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -2px;
  height: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform var(--dur-fast) var(--ease-out-firm);
}
.nav a:not(.btn):hover::after,
.nav a:not(.btn)[aria-current="page"]::after { transform: scaleX(1); }

/* --------------------------------------------------------- scope rows -- */
/* Rows arrive in sequence so the two columns read as a comparison being
   laid out, rather than appearing all at once. */

.scope li {
  opacity: 0;
  transform: translateX(-6px);
  transition:
    opacity 480ms var(--ease-out-soft),
    transform 480ms var(--ease-out-soft);
  transition-delay: calc(var(--i, 0) * 45ms);
}
.scope.is-in li { opacity: 1; transform: none; }
.scope-col + .scope-col li { transform: translateX(6px); }
.scope.is-in .scope-col + .scope-col li { transform: none; }

/* ------------------------------------------------------- ink CTA band -- */

.band--ink { position: relative; overflow: hidden; }
.band--ink::before {
  content: "";
  position: absolute;
  inset: -40% -10% auto -10%;
  height: 140%;
  background:
    radial-gradient(60% 55% at 20% 0%, rgba(180, 101, 15, 0.20), transparent 70%),
    radial-gradient(50% 50% at 85% 20%, rgba(52, 96, 130, 0.28), transparent 70%);
  pointer-events: none;
  opacity: 0;
  transform: scale(1.04);
  transition: opacity 1200ms var(--ease-out-soft), transform 1200ms var(--ease-out-soft);
}
.band--ink.is-in::before { opacity: 1; transform: none; }
.band--ink > .container { position: relative; z-index: 1; }

/* ============================================================ cinema ==== */
/* One pinned beat. The stage holds still while the reader scrolls "through"
   it, and a single line resolves letter by letter as they go.

   Progress is one number, --p (0 → 1), written to the section once per frame
   by JS. Every character derives its own state from --p with calc(), so a
   frame costs one style write rather than one per character. */

.cinema {
  --p: 0;
  position: relative;
  height: 240vh;             /* the scroll track — deliberately short */
  background: var(--ink);
}

.cinema-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
  display: grid;
  place-items: center;
  background: var(--ink);
}

/* Light thrown from off-stage. Replaces the photography this brand
   does not have, rather than reaching for stock imagery. */
.cinema-stage::before {
  content: "";
  position: absolute;
  inset: -20%;
  background:
    radial-gradient(48% 42% at 50% 42%, rgba(180, 101, 15, 0.30), transparent 68%),
    radial-gradient(60% 50% at 82% 88%, rgba(46, 92, 128, 0.34), transparent 70%),
    radial-gradient(40% 40% at 12% 82%, rgba(180, 101, 15, 0.13), transparent 70%);
  transform: scale(calc(1 + var(--p) * 0.16)) translateY(calc(var(--p) * -2%));
  opacity: calc(0.5 + var(--p) * 0.5);
  pointer-events: none;
}

/* A hairline grid, so the dark panel reads as engineered rather than empty. */
.cinema-grid {
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(to right, rgba(255,255,255,0.045) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(255,255,255,0.045) 1px, transparent 1px);
  background-size: 68px 68px;
  mask-image: radial-gradient(70% 60% at 50% 50%, #000 30%, transparent 78%);
  -webkit-mask-image: radial-gradient(70% 60% at 50% 50%, #000 30%, transparent 78%);
  transform: translateY(calc(var(--p) * -40px));
  pointer-events: none;
}

/* The oversized wordmark sitting behind the line. */
.cinema-mark {
  position: absolute;
  left: 50%;
  top: 50%;
  transform:
    translate(-50%, -50%)
    scale(calc(1.02 + var(--p) * 0.1));
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(4rem, 15vw, 13rem);
  letter-spacing: -0.04em;
  white-space: nowrap;
  color: rgba(255, 255, 255, 0.045);
  opacity: calc(0.35 + var(--p) * 0.65);
  pointer-events: none;
  user-select: none;
}

.cinema-copy {
  position: relative;
  z-index: 2;
  max-width: min(90vw, 880px);
  text-align: center;
  padding-inline: var(--s-5);
}

.cinema-eyebrow {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: #E0A867;
  margin: 0 0 var(--s-5);
  opacity: clamp(0, calc(var(--p) * 8), 1);
}

.cinema-line {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.9rem, 5.2vw, 3.6rem);
  line-height: 1.12;
  letter-spacing: 0.01em;
  color: #fff;
  margin: 0;
}

/* Each character resolves as --p passes its own position in the line.
   The multiplier controls how much of the scroll each character occupies. */
/* Words are the unbreakable unit; characters animate inside them. */
.cin-word { display: inline-block; white-space: nowrap; }

.cin-char {
  --lit: clamp(0, calc((var(--p) - var(--n)) * 9), 1);
  display: inline-block;
  opacity: var(--lit);
  transform: translateY(calc((1 - var(--lit)) * 0.34em));
  filter: blur(calc((1 - var(--lit)) * 7px));
  will-change: opacity, transform, filter;
}
.cin-space { display: inline-block; width: 0.28em; }

.cinema-tail {
  margin-top: var(--s-6);
  color: #B9C8D2;
  font-size: 1.02rem;
  line-height: 1.6;
  opacity: clamp(0, calc((var(--p) - 0.72) * 5), 1);
  transform: translateY(calc((1 - clamp(0, calc((var(--p) - 0.72) * 5), 1)) * 12px));
}

.cinema-tail .btn { margin-top: var(--s-5); }

/* Section index, borrowed from the reference — it tells the reader this is a
   deliberate chapter with a beginning and an end, not an endless scroll. */
.cinema-rail {
  position: absolute;
  right: clamp(16px, 4vw, 48px);
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-4);
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: #6E8494;
  pointer-events: none;
}
.cinema-rail-track {
  width: 2px;
  height: 120px;
  background: rgba(255,255,255,0.14);
  position: relative;
  overflow: hidden;
}
.cinema-rail-track::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--accent);
  transform: scaleY(var(--p));
  transform-origin: 50% 0;
}
.cinema-rail span { writing-mode: vertical-rl; }

@media (max-width: 720px) {
  .cinema { height: 200vh; }
  .cinema-rail { display: none; }
}

/* ------------------------------------------------- cross-page continuity */

/* Cross-document view transitions were removed deliberately.
   `@view-transition { navigation: auto }` gives a pleasant fade between
   pages, but the browser rejects its internal promise with
   AbortError("Transition was skipped") whenever a navigation interrupts a
   running transition. That rejection fires while the old document is being
   torn down and the new one has not run its scripts yet, so no page-level
   handler can catch it — it always reaches the console.

   Console errors on a site whose whole point is looking technically
   competent are not worth a page fade. The per-page entrance animations
   already provide the sense of continuity. */

/* ====================================================== reduced motion == */
/* Everything above collapses to its final state. Nothing moves, nothing
   is hidden, and every page remains fully usable. */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  .m-reveal { opacity: 1; transform: none; filter: none; }
  .m-word   { opacity: 1; transform: none; }
  .m-line   { overflow: visible; }
  .scope li { opacity: 1; transform: none; }
  .band--ink::before { opacity: 1; transform: none; }

  .mesh-link   { stroke-dashoffset: 0; }
  .mesh-node,
  .mesh-label  { opacity: 1; transform: none; }
  .mesh-packet { display: none; }

  .scroll-progress { display: none; }
  .btn::after { display: none; }
  .card[data-tilt]::before { display: none; }

  .steps-rail::after { transform: scaleX(1); }

  /* The cinema beat un-pins entirely: no sticky viewport, no scroll-linked
     state. Forcing --p to 1 lights every character at once, so the line is
     simply readable and the section becomes an ordinary dark band. */
  .cinema { --p: 1; height: auto; }
  .cinema-stage { position: static; height: auto; padding-block: var(--s-9); }
  .cinema-stage::before { transform: none; }
  .cinema-grid { transform: none; }
  .cinema-mark { transform: translate(-50%, -50%); }
  .cin-char { filter: none; transform: none; will-change: auto; }
  .cinema-tail { transform: none; }
  .cinema-rail { display: none; }
}
