/* ==========================================================================
   Zelofreight — motion & depth layer
   --------------------------------------------------------------------------
   Loaded AFTER theme.css. theme.css owns colour and layout; this file owns
   light, depth and movement. Keeping them apart means the palette can be
   retuned without touching a single animation, and the whole motion system
   can be disabled by removing one <link>.

   Rules this file holds itself to:

   1. Animate only `transform`, `opacity` and `filter`. Anything that moves
      `width`, `top` or `margin` forces layout on every frame and janks on a
      mid-range phone — which is most of the traffic a freight site gets.
   2. Nothing important is revealed by animation alone. Every `[data-reveal]`
      element is fully visible with JavaScript off (see the `.js` guard) and
      under reduced motion.
   3. Motion is a response, not decoration on a timer. Continuous loops are
      limited to ambient light; everything else is triggered by scroll, hover
      or pointer.
   ========================================================================== */

:root {
  /* Easing. Expo-out is the "arrives with authority, settles instantly"
     curve — the one that makes an entrance feel expensive rather than slow. */
  --ease-expo:   cubic-bezier(0.16, 1, 0.3, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  --ease-soft:   cubic-bezier(0.4, 0, 0.2, 1);

  /* Glow. Gold at low alpha, layered, rather than one hard shadow. */
  /* Ambient wash colours. Tokens because the light theme needs a far gentler
     version of each — a 90px-blurred saturated green that reads as depth on a
     near-black ground reads as a stain on paper. */
  --aurora-1: rgba(31, 86, 66, .85);
  --aurora-2: rgba(140, 111, 22, .45);
  --aurora-3: rgba(22, 60, 48, .8);
  --aurora-opacity: .5;
  --aurora-blur: 90px;
  --spotlight-tint: rgba(201, 162, 39, .07);

  --glow-gold-sm: 0 0 24px -4px rgba(201, 162, 39, .35);
  --glow-gold-md: 0 18px 48px -18px rgba(201, 162, 39, .55);
  --glow-gold-lg: 0 30px 90px -30px rgba(201, 162, 39, .65);
  --glow-green:   0 24px 70px -26px rgba(10, 60, 44, .9);

  /* Depth shadows tuned for a dark ground: a dark surface needs a *darker*
     shadow plus a light rim, otherwise the card reads as a flat rectangle. */
  --shadow-1: 0 2px 8px rgba(0, 0, 0, .28);
  --shadow-2: 0 12px 34px -12px rgba(0, 0, 0, .55);
  --shadow-3: 0 30px 80px -24px rgba(0, 0, 0, .7);

  /* A light rim along the top edge reads as a lit surface against a dark
     ground. On paper there is nothing to catch that light, so the light theme
     replaces it with a hairline border instead. */
  --rim: inset 0 1px 0 rgba(244, 241, 233, .06);
}

/* --------------------------------------------------------------------------
   Light theme — the depth values only
   theme.css owns the palette; these are the ambient and shadow values that
   belong to this file and cannot simply carry over to a bright ground.
   -------------------------------------------------------------------------- */
:root[data-theme="light"] {
  /* Barely-there wash. On white the aurora is a suggestion of colour in the
     corners, not a light source. */
  --aurora-1: rgba(31, 122, 90, .10);
  --aurora-2: rgba(201, 162, 39, .10);
  --aurora-3: rgba(31, 122, 90, .07);
  --aurora-opacity: .8;
  --aurora-blur: 120px;
  --spotlight-tint: rgba(125, 100, 24, .04);

  /* Shadows on white are short, soft and green-tinted rather than black. */
  --shadow-1: 0 1px 2px rgba(15, 42, 32, .05), 0 2px 8px rgba(15, 42, 32, .05);
  --shadow-2: 0 8px 22px -8px rgba(15, 42, 32, .12);
  --shadow-3: 0 20px 48px -18px rgba(15, 42, 32, .16);

  /* A pale rim simulates light catching a raised edge on a dark ground. On
     white there is no such highlight, so it becomes a hairline. */
  --rim: inset 0 0 0 1px rgba(15, 42, 32, .05);

  --glow-gold-sm: 0 0 18px -6px rgba(201, 162, 39, .5);
  --glow-gold-md: 0 12px 28px -14px rgba(201, 162, 39, .55);
  --glow-gold-lg: 0 20px 50px -26px rgba(201, 162, 39, .55);
}


/* The film grain is drawn with black noise, which on a white page reads as
   dirt rather than texture. theme.css already drops its opacity; this turns
   it to a soft-light blend so what remains is tone, not speckle. */
:root[data-theme="light"] body::after { display: none; }

/* The lane canvas sits over a bright photograph in light mode; it needs less
   presence, not more. */
/* On a dark ground the lane network is a faint glow behind the type. On white
   the same lines read as scratches across the headline, so it drops back to a
   suggestion and is masked away from the left edge where the text begins. */
:root[data-theme="light"] .hero__lanes {
  opacity: .32;
  -webkit-mask-image: linear-gradient(90deg, transparent 6%, #000 55%);
          mask-image: linear-gradient(90deg, transparent 6%, #000 55%);
}


/* --------------------------------------------------------------------------
   No-JS guard
   The reveal system starts elements at opacity 0. If JavaScript never runs,
   nothing would ever turn them back on — the page would be blank. motion.js
   adds `js-motion` to <html> as its first act, so these rules only take
   effect once something is alive to undo them.
   -------------------------------------------------------------------------- */
html:not(.js-motion) [data-reveal] { opacity: 1 !important; transform: none !important; filter: none !important; }
html:not(.js-motion) [data-reveal="clip"] > * { clip-path: none !important; }


/* ==========================================================================
   1. Ambient light — the "aurora"
   Two slow-drifting radial washes behind the whole page. This is what stops
   a large dark field reading as flat paint: the green is never quite the
   same green twice across the viewport, so the eye finds depth in it.
   Fixed-position and pointer-events:none, so it costs nothing but paint.
   ========================================================================== */
.aurora {
  position: fixed;
  inset: 0;
  z-index: -3;
  pointer-events: none;
  overflow: hidden;
  contain: strict;
}
.aurora span {
  position: absolute;
  display: block;
  border-radius: 50%;
  opacity: var(--aurora-opacity);
  filter: blur(var(--aurora-blur));
  will-change: transform;
}
.aurora span:nth-child(1) {
  width: 46vw; height: 46vw;
  top: -12vw; left: -8vw;
  background: radial-gradient(circle, var(--aurora-1), transparent 70%);
  animation: drift-a 26s var(--ease-soft) infinite alternate;
}
.aurora span:nth-child(2) {
  width: 38vw; height: 38vw;
  top: 30vh; right: -10vw;
  background: radial-gradient(circle, var(--aurora-2), transparent 70%);
  animation: drift-b 34s var(--ease-soft) infinite alternate;
}
.aurora span:nth-child(3) {
  width: 52vw; height: 52vw;
  bottom: -20vw; left: 20vw;
  background: radial-gradient(circle, var(--aurora-3), transparent 72%);
  animation: drift-c 40s var(--ease-soft) infinite alternate;
}
@keyframes drift-a { to { transform: translate3d(14vw, 10vh, 0) scale(1.2); } }
@keyframes drift-b { to { transform: translate3d(-12vw, -8vh, 0) scale(1.15); } }
@keyframes drift-c { to { transform: translate3d(8vw, -12vh, 0) scale(.85); } }


/* ==========================================================================
   2. Scroll progress
   A hairline of gold across the top, width driven from JS via --progress.
   On a long page this is the cheapest possible "how much is left" signal.
   ========================================================================== */
.scroll-progress {
  position: fixed;
  top: 0; left: 0;
  height: 2px;
  width: 100%;
  z-index: 1200;
  pointer-events: none;
  transform-origin: 0 50%;
  transform: scaleX(var(--progress, 0));
  background: linear-gradient(90deg, var(--gold-deep), var(--gold-bright), var(--gold));
  box-shadow: var(--glow-gold-sm);
}


/* ==========================================================================
   3. Pointer spotlight
   A soft gold halo that trails the cursor. --mx/--my are set by motion.js in
   viewport pixels. Hidden on touch, where there is no cursor to follow.
   ========================================================================== */
.spotlight {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  transition: opacity .6s var(--ease-soft);
  background: radial-gradient(
    380px circle at var(--mx, 50%) var(--my, 50%),
    var(--spotlight-tint),
    transparent 65%
  );
}
html.js-motion.has-pointer .spotlight { opacity: 1; }
@media (hover: none) { .spotlight { display: none; } }


/* ==========================================================================
   4. Scroll reveals
   One attribute, several entrances. The element declares its own motion so
   markup stays readable: data-reveal="up", "left", "scale", "blur", "clip".
   data-reveal-delay adds a stagger in milliseconds; motion.js also assigns
   an automatic stagger to children of [data-stagger].
   ========================================================================== */
[data-reveal] {
  opacity: 0;
  transition:
    opacity   1s var(--ease-expo),
    transform 1.05s var(--ease-expo),
    filter    1s var(--ease-expo);
  transition-delay: var(--reveal-delay, 0ms);
  will-change: transform, opacity;
}
/* Travel distances are deliberately long. animate.css's fadeInUp — which is
   what the reference site uses on 37 elements — moves an element by 100% of
   its own height, so a card travels roughly 300px. A 38px nudge reads as a
   page settling; a long rise reads as content arriving. These are fixed rather
   than percentage-based so a tall element does not fly in from off-screen. */
[data-reveal="up"]    { transform: translate3d(0, 72px, 0); }
[data-reveal="down"]  { transform: translate3d(0, -48px, 0); }
[data-reveal="left"]  { transform: translate3d(-64px, 0, 0); }
[data-reveal="right"] { transform: translate3d(64px, 0, 0); }
[data-reveal="scale"] { transform: scale(.88); }
[data-reveal="blur"]  { filter: blur(14px); transform: translate3d(0, 40px, 0); }
/* Note the indirection on "clip": the wipe is applied to the child, never to
   the observed element itself. An IntersectionObserver clips a target's
   intersection rect by that target's own clip-path, so an element clipped to
   zero height reports zero intersection, never receives .is-in, and stays
   clipped permanently. Clipping the child sidesteps the catch-22. */
[data-reveal="clip"]  { transform: none; }
[data-reveal="clip"] > * {
  clip-path: inset(0 0 100% 0);
  transition: clip-path 1.1s var(--ease-expo);
  transition-delay: var(--reveal-delay, 0ms);
}
[data-reveal="clip"].is-in > * { clip-path: inset(0 0 0 0); }
[data-reveal="rise"]  { transform: translate3d(0, 110px, 0) scale(.95); }

/* Below the stacking breakpoint the split layouts collapse to a single
   column, so a sideways entrance no longer describes anything — the two
   halves are above and below each other, not left and right. It is also
   actively harmful: holding a full-width element 44px to the right pushes the
   document into horizontal overflow while it waits to be revealed. */
/* 900px, not 760px: this must match the breakpoint at which `.split` and the
   grids collapse to a single column in theme.css. Between the two values the
   layout is already stacked while the sideways offset still applies, which is
   both meaningless and enough to overflow the page. */
@media (max-width: 900px) {
  [data-reveal="left"],
  [data-reveal="right"] { transform: translate3d(0, 32px, 0); }
}

[data-reveal].is-in {
  opacity: 1;
  transform: none;
  filter: none;
  will-change: auto;
}

/* Keep the existing .reveal class working — the old pages still use it. */
.reveal { transition-timing-function: var(--ease-expo); }


/* ==========================================================================
   5. Split-text headline entrance
   motion.js wraps each word of a [data-split] heading in a span with a
   clipping parent. The words rise out of their own mask, one after another,
   which reads as typography arriving rather than a box fading in.
   ========================================================================== */
[data-split] .word {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  padding-bottom: .08em;   /* room for descenders, else 'g' and 'y' clip */
  margin-bottom: -.08em;
}
[data-split] .word > span {
  display: inline-block;
  transform: translate3d(0, 115%, 0);
  transition: transform 1.05s var(--ease-expo);
  transition-delay: var(--word-delay, 0ms);
}
[data-split].is-in .word > span { transform: none; }


/* ==========================================================================
   6. Buttons — shimmer and magnetism
   The sweep is a single skewed highlight travelling across the face. It runs
   on hover only; a button that shimmers on a timer is a slot machine.
   ========================================================================== */
.btn {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  /* No border-radius here. theme.css owns shape and sets the pill; this file
     loads after it, so restating a radius here silently overrides the pill and
     the buttons render as rectangles. Shape belongs to theme.css, movement to
     this one — the split only works if both files respect it. */
  transition:
    transform .4s var(--ease-expo),
    box-shadow .4s var(--ease-expo),
    background .4s var(--ease-soft),
    color .3s var(--ease-soft),
    border-color .3s var(--ease-soft);
}
.btn::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0;
  left: -140%;
  width: 60%;
  z-index: -1;
  transform: skewX(-22deg);
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .28), transparent);
  transition: left .75s var(--ease-expo);
}
.btn:hover::after, .btn:focus-visible::after { left: 140%; }

.btn--gold:hover, .btn--gold:focus-visible {
  transform: translateY(-3px);
  box-shadow: var(--glow-gold-md);
}
.btn--gold:active { transform: translateY(-1px) scale(.985); }

.btn--ghost { backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); }
.btn--ghost:hover, .btn--ghost:focus-visible {
  transform: translateY(-3px);
  box-shadow: 0 14px 40px -18px rgba(0, 0, 0, .8), inset 0 0 0 1px var(--gold-line);
}

/* Magnetic pull. --mag-x/--mag-y come from motion.js on pointer proximity;
   the element keeps its own hover transform by composing with translate. */
[data-magnetic] {
  transform: translate3d(var(--mag-x, 0), var(--mag-y, 0), 0);
  transition: transform .45s var(--ease-expo);
}
[data-magnetic].is-pulling { transition: transform .12s linear; }


/* ==========================================================================
   7. Cards — depth, gradient rim, and a light that follows the cursor
   The rim is a conic gradient masked to the border box, so it is a true
   1px gradient outline rather than a fake inset shadow. It rotates on hover.
   ========================================================================== */
.card,
.panel {
  /* Radius comes from --radius-card in theme.css. See the note on .btn above:
     this file must not restate shape, or it silently wins by load order. */
  box-shadow: var(--shadow-1), var(--rim);
  transition:
    transform .5s var(--ease-expo),
    box-shadow .5s var(--ease-expo),
    border-color .4s var(--ease-soft),
    background .4s var(--ease-soft);
}

.card {
  background:
    radial-gradient(
      420px circle at var(--cx, 50%) var(--cy, 0%),
      rgba(201, 162, 39, .1),
      transparent 60%
    ),
    var(--surface-1);
}
:root[data-theme="light"] .card {
  background:
    radial-gradient(420px circle at var(--cx, 50%) var(--cy, 0%),
      rgba(31, 122, 90, .05), transparent 60%),
    var(--surface-1);
}

.card:hover {
  transform: translateY(-6px);
  box-shadow: var(--shadow-3), var(--rim), 0 0 0 1px var(--gold-line);
}

/* Rotating conic rim, revealed on hover. Uses mask-composite so only the
   1px border band is painted — the middle stays transparent. */
.card--rim::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  opacity: 0;
  pointer-events: none;
  transition: opacity .5s var(--ease-soft);
  background: conic-gradient(
    from var(--rim-angle, 0deg),
    transparent 0deg,
    var(--gold) 70deg,
    var(--gold-bright) 110deg,
    transparent 190deg,
    transparent 360deg
  );
  -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;
}
.card--rim:hover::after { opacity: 1; animation: rim-spin 4s linear infinite; }

/* @property makes the angle a real animatable number. Without it the conic
   gradient jumps instead of sweeping, because a bare custom property
   interpolates as a string. */
@property --rim-angle {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}
@keyframes rim-spin { to { --rim-angle: 360deg; } }


/* ==========================================================================
   8. 3D tilt
   Applied to [data-tilt]. The parent supplies perspective so the child can
   rotate in depth without the whole page inheriting a 3D context.
   ========================================================================== */
[data-tilt] {
  transform-style: preserve-3d;
  transform:
    perspective(900px)
    rotateX(var(--tilt-x, 0deg))
    rotateY(var(--tilt-y, 0deg))
    translateZ(0);
  transition: transform .5s var(--ease-expo);
}
[data-tilt].is-tilting { transition: transform .1s linear; }
[data-tilt] > * { transform: translateZ(28px); }


/* ==========================================================================
   9. Images — masked wipe on entry, slow drift on hover
   ========================================================================== */
.img-frame,
.media {
  border-radius: var(--radius-card);
  overflow: hidden;
}
.media {
  position: relative;
  display: block;
  box-shadow: var(--shadow-2);
}
.media img {
  width: 100%;
  /* The same duotone theme.css applies to .img-frame. Photography is pushed
     into the brand green so a bright stock photograph cannot sit on top of
     the palette shouting; grayscale first, sepia to reintroduce saturation
     (a grayscale image has none to rotate), then hue-rotate that warm tone
     round to green. */
  filter: var(--photo-filter);
  transition: transform 1.2s var(--ease-expo), filter .8s var(--ease-soft);
}
/* On hover the treatment lifts and the photograph returns to something near
   its own colour — the image comes alive under the cursor rather than merely
   zooming. */
.media:hover img {
  transform: scale(1.06);
  filter: var(--photo-filter-hover);
}

/* Gold edge that draws itself across the bottom of a media block on reveal */
.media::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 2px;
  transform: scaleX(0);
  transform-origin: 0 50%;
  background: linear-gradient(90deg, var(--gold), transparent);
  transition: transform 1.2s var(--ease-expo) .3s;
}
.media.is-in::after { transform: scaleX(1); }


/* ==========================================================================
   10. Parallax
   [data-parallax="0.2"] moves at 20% of scroll. motion.js writes --py.
   ========================================================================== */
[data-parallax] {
  transform: translate3d(0, var(--py, 0px), 0);
  will-change: transform;
}


/* The interior page headers drift against the scroll. A parallaxed background
   MUST be larger than the box it fills: the transform moves it inside a
   container that still clips at its own edges, so a flush-fitting image would
   drag an uncovered strip into view at the bottom. The extra 25% top and
   bottom is sized against the largest drift the 0.12 factor can produce over
   a header of this height. */
.page-head { overflow: hidden; }
.page-head__bg[data-parallax] {
  inset: -25% 0;
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  .page-head__bg[data-parallax] { inset: 0; transform: none !important; }
}


/* ==========================================================================
   11. Marquee ribbon — the lane ticker
   Duplicated content scrolls left forever. The track holds two identical
   copies, so the reset at -50% is invisible.
   ========================================================================== */
.ticker {
  position: relative;
  overflow: hidden;
  border-block: 1px solid var(--line);
  background: linear-gradient(90deg, var(--surface-1), var(--base), var(--surface-1));
  padding-block: 1.15rem;
  /* Fade both ends so words do not collide with the viewport edge */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.ticker__track {
  display: flex;
  width: max-content;
  gap: 3.5rem;
  animation: ticker-run var(--ticker-duration, 48s) linear infinite;
}
.ticker:hover .ticker__track { animation-play-state: paused; }
.ticker__item {
  display: inline-flex;
  align-items: center;
  gap: .9rem;
  font-size: .74rem;
  font-weight: 500;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--text-muted);
  white-space: nowrap;
}
.ticker__item svg { width: 14px; height: 14px; color: var(--gold); flex-shrink: 0; }
.ticker__item b { color: var(--ivory); font-weight: 500; }
@keyframes ticker-run { to { transform: translate3d(-50%, 0, 0); } }


/* ==========================================================================
   12. Hero
   ========================================================================== */
/* The lane canvas: animated freight routes behind the headline. Sits above
   the photograph but below the scrim, so the text contrast is unaffected. */
.hero__lanes {
  position: absolute;
  inset: 0;
  /* A canvas is a replaced element, so `inset: 0` with an auto width resolves
     to its intrinsic 300x150 rather than stretching to the containing block —
     the network would draw into the top-left corner of the hero and stop.
     The explicit 100%/100% is what actually sizes it. */
  width: 100%;
  height: 100%;
  z-index: -1;
  pointer-events: none;
  opacity: .85;
}

.hero h1 em {
  position: relative;
  display: inline-block;
}
/* Gold underline that draws itself under the emphasised word after the
   headline has landed. */
.hero h1 em::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: .06em;
  height: 2px;
  transform: scaleX(0);
  transform-origin: 0 50%;
  background: linear-gradient(90deg, var(--gold-bright), transparent);
  transition: transform 1.1s var(--ease-expo) 1.1s;
}
.hero.is-ready h1 em::after { transform: scaleX(1); }

/* Scroll cue */
.scroll-cue {
  position: absolute;
  left: 50%;
  /* Clear of the stat strip pinned to the bottom of the hero. */
  bottom: clamp(10rem, 19vh, 13.5rem);
  transform: translateX(-50%);
  display: grid;
  justify-items: center;
  gap: .6rem;
  font-size: .6rem;
  letter-spacing: .24em;
  text-transform: uppercase;
  color: var(--text-faint);
  z-index: 2;
}
.scroll-cue__rail {
  width: 1px;
  height: 52px;
  background: var(--line-strong);
  position: relative;
  overflow: hidden;
}
.scroll-cue__rail::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 40%;
  background: linear-gradient(to bottom, transparent, var(--gold));
  animation: cue-fall 2.4s var(--ease-soft) infinite;
}
@keyframes cue-fall {
  0%   { transform: translateY(-100%); opacity: 0; }
  35%  { opacity: 1; }
  100% { transform: translateY(250%); opacity: 0; }
}
@media (max-width: 760px) { .scroll-cue { display: none; } }

/* Stat figures get a gold rule that grows in under each number */
.hero__stats .wrap > div { position: relative; padding-left: 1rem; }
.hero__stats .wrap > div::before {
  content: "";
  position: absolute;
  left: 0; top: .15rem; bottom: .15rem;
  width: 2px;
  transform: scaleY(0);
  transform-origin: 50% 0;
  background: linear-gradient(to bottom, var(--gold), transparent);
  transition: transform .9s var(--ease-expo);
  transition-delay: calc(var(--i, 0) * 120ms);
}
.hero.is-ready .hero__stats .wrap > div::before { transform: scaleY(1); }


/* ==========================================================================
   13. Section headings — the eyebrow mark
   --------------------------------------------------------------------------
   This used to scaleX a 2rem rule into place. The mark is an asterisk glyph
   now, and scaleX(0) on a glyph simply makes it invisible — including on every
   eyebrow that never sits inside a [data-reveal] container, where nothing ever
   adds .is-in to turn it back on. It spins and fades in instead.
   ========================================================================== */
.eyebrow::before {
  opacity: 0;
  transform: rotate(-90deg) scale(.4);
  transition: opacity .6s var(--ease-expo), transform .7s var(--ease-spring);
}
[data-reveal].is-in .eyebrow::before,
.eyebrow.is-in::before,
.is-in .eyebrow::before { opacity: 1; transform: rotate(0deg) scale(1); }

/* An eyebrow outside any revealed container must not stay hidden waiting for a
   class that will never arrive. */
.eyebrow:not(.is-in):not([data-reveal] .eyebrow)::before { opacity: 1; transform: none; }


/* ==========================================================================
   14. Numbers
   The counter itself lives in site.js. This just stops the figure reflowing
   its neighbours as the digit count grows during the count-up.
   ========================================================================== */
.stat__num { font-variant-numeric: tabular-nums; }


/* ==========================================================================
   15. Header — shrink and frost on scroll
   ========================================================================== */
.site-header {
  transition:
    padding .45s var(--ease-expo),
    background .45s var(--ease-soft),
    box-shadow .45s var(--ease-soft),
    transform .5s var(--ease-expo);
}
.site-header.is-stuck { box-shadow: var(--shadow-2); }

/* Hide the header while scrolling down, bring it back on the way up. On a
   phone this returns a whole header's worth of vertical space to the page. */
.site-header.is-hidden { transform: translateY(-105%); }

.brand svg { transition: transform .6s var(--ease-spring); }
.brand:hover svg { transform: rotate(-12deg) scale(1.08); }


/* ==========================================================================
   16. Links — underline that wipes from the left
   ========================================================================== */
.link-arrow { position: relative; }
.link-arrow::after {
  content: "";
  position: absolute;
  left: 0; bottom: -4px;
  height: 1px; width: 100%;
  transform: scaleX(0);
  transform-origin: 100% 50%;
  background: currentColor;
  transition: transform .5s var(--ease-expo);
}
.link-arrow:hover::after { transform: scaleX(1); transform-origin: 0 50%; }


/* ==========================================================================
   17. Form fields — focus ring that blooms
   ========================================================================== */
.input, .select, .textarea {
  border-radius: 12px;
  transition:
    border-color .3s var(--ease-soft),
    background .3s var(--ease-soft),
    box-shadow .3s var(--ease-soft);
}
.input:focus, .select:focus, .textarea:focus {
  box-shadow: 0 0 0 3px var(--gold-veil);
}


/* ==========================================================================
   18. Skeleton shimmer, for content awaiting the API
   ========================================================================== */
.skeleton {
  position: relative;
  overflow: hidden;
  background: var(--surface-2);
  border-radius: 6px;
  color: transparent !important;
}
.skeleton::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, rgba(244, 241, 233, .07), transparent);
  animation: shimmer 1.4s linear infinite;
}
@keyframes shimmer { to { transform: translateX(100%); } }


/* ==========================================================================
   19. Page transition veil
   A gold-tinted panel wipes up over the page on navigation and away on
   arrival. Purely additive: if JS is off, links behave normally.
   ========================================================================== */
.veil {
  position: fixed;
  inset: 0;
  z-index: 2000;
  pointer-events: none;
  transform: scaleY(0);
  transform-origin: 50% 100%;
  background: linear-gradient(to top, var(--base), var(--surface-2));
  transition: transform .5s var(--ease-expo);
}
.veil.is-out { transform: scaleY(1); transform-origin: 50% 100%; }
.veil.is-in  { transform: scaleY(0); transform-origin: 50% 0; transition-duration: .7s; }


/* ==========================================================================
   20. Reduced motion
   One block, at the end, deliberately heavy-handed. Someone who has asked
   their operating system for less movement has usually done so because
   movement makes them ill — a partial honouring of that is not honouring it.
   Everything still *arrives*, it simply arrives already in place.
   ========================================================================== */
/* ==========================================================================
   Route map — the journey drawing itself
   Shape and colour stay in theme.css; only movement is described here.
   The blanket reduced-motion reset below switches all of it off, and the map
   module skips its JavaScript sequence under the same query.
   ========================================================================== */
@keyframes map-halo {
  from { opacity: .55; scale: .65; }
  to   { opacity: 0;   scale: 2.5; }
}
@keyframes map-flow  { to { stroke-dashoffset: -22; } }
@keyframes map-drop {
  from { opacity: 0; translate: 0 -14px; scale: .5; }
  to   { opacity: 1; translate: 0 0;     scale: 1; }
}

/* The leg still to come flows towards the destination. */
.route-remaining { animation: map-flow 1.1s linear infinite; }

/* Pins fall in along the route, each a beat after the one before it. */
.map-pin {
  animation: map-drop .5s var(--ease-soft) both;
  animation-delay: var(--drop, 0ms);
}

/* Only the current position and the vehicle pulse — a halo on every pin would
   be noise rather than emphasis. */
.map-pin--now .map-pin__halo,
.map-vehicle__halo { animation: map-halo 2.4s var(--ease-soft) infinite; }
.map-pin--origin .map-pin__halo,
.map-pin--dest .map-pin__halo,
.map-pin--event .map-pin__halo { display: none; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
  [data-reveal],
  [data-reveal="clip"] > *,
  [data-split] .word > span,
  .reveal {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    clip-path: none !important;
  }
  .aurora, .spotlight, .hero__lanes, .scroll-cue { display: none !important; }
  .ticker__track { animation: none !important; transform: none !important; }
  .media::after, .hero h1 em::after { transform: scaleX(1) !important; }
  .eyebrow::before { opacity: 1 !important; transform: none !important; }
  .hero__stats .wrap > div::before { transform: scaleY(1) !important; }
}


/* ==========================================================================
   Hero badge — a card that bounces gently left and right
   --------------------------------------------------------------------------
   Animates the individual `translate` property, not `transform`. The badge
   already makes its entrance through `transform` (data-reveal="scale" takes it
   from scale(.88) to none), and a keyframe animation on `transform` would
   override that outright — the card would never scale in. `translate` composes
   with `transform`, so the entrance and the bounce both run.

   The bounce comes from the spring easing on an alternating loop: the curve
   overshoots past each end before settling back, once per swing. The delay
   holds it still until the entrance has finished.
   ========================================================================== */
@keyframes badge-bounce {
  from { translate: -14px 0; }
  to   { translate:  14px 0; }
}
.hero__badge {
  animation: badge-bounce 1.6s var(--ease-spring) 1.8s infinite alternate both;
  will-change: translate;
}
/* Hold still while someone is reading it or reaching for it. */
.hero__badge:hover,
.hero__badge:focus-within { animation-play-state: paused; }

@media (prefers-reduced-motion: reduce) {
  /* Explicitly none. The blanket rule above only shortens animations to
     .01ms, which would leave the card parked at one end of its swing. */
  .hero__badge { animation: none !important; translate: none !important; }
}


/* ==========================================================================
   Sign-in page — the vault panel
   --------------------------------------------------------------------------
   Every movement uses the individual `rotate`, `translate` and `scale`
   properties rather than `transform`, for the same reason as the hero badge:
   the pieces make their entrance through `transform` (data-reveal), and a
   keyframe animation on `transform` would override that entrance outright.

   The two dial rings turn in opposite directions at different speeds, so the
   pattern never visibly repeats. The chips bob on staggered periods for the
   same reason. The photograph drifts slowly enough to register as depth
   rather than motion.
   ========================================================================== */
@keyframes dial-turn   { to   { rotate: 360deg; } }
@keyframes chip-float  { from { translate: 0 -8px; } to { translate: 0 8px; } }
@keyframes vault-drift { from { scale: 1; }          to { scale: 1.08; } }

.auth-dial__ring--outer { animation: dial-turn 60s linear infinite; }
.auth-dial__ring--inner { animation: dial-turn 90s linear infinite reverse; }

.auth-chip--one { animation: chip-float 4.2s var(--ease-soft) 1.4s infinite alternate both; }
.auth-chip--two { animation: chip-float 5.1s var(--ease-soft) 2.1s infinite alternate both; }
.auth-chip:hover { animation-play-state: paused; }

.auth-visual__arch img { animation: vault-drift 26s ease-in-out infinite alternate; }

@media (prefers-reduced-motion: reduce) {
  /* Explicitly off. The blanket rule above only shortens animations, which
     would leave the rings, chips and photograph parked mid-movement. */
  .auth-dial__ring,
  .auth-chip,
  .auth-visual__arch img {
    animation: none !important;
    rotate: none !important;
    translate: none !important;
    scale: none !important;
  }
}
