/* Halcyon motion system — scroll choreography, staged entrances, ambient loops.
   Every hidden state is scoped to .js-motion so content is never stranded if JS fails. */

:root {
  --ease-expo: cubic-bezier(.19, 1, .22, 1);
  --ease-quad: cubic-bezier(.25, .46, .45, .94);
  --ease-snap: cubic-bezier(.32, .72, 0, 1);
  --reveal-shift: 46px;
  --reveal-dur: 1.05s;
}

/* App screens use a shorter throw than the marketing page — dashboards should feel quick. */
.js-motion .app-shell,
.js-motion .staff-login {
  --reveal-shift: 22px;
  --reveal-dur: .62s;
}

/* ---------- Scroll reveals ---------- */

.js-motion [data-reveal] {
  opacity: 0;
  transform: translate3d(0, var(--reveal-shift), 0);
  transition:
    transform var(--reveal-dur) var(--ease-expo) var(--reveal-delay, 0ms),
    opacity .45s var(--ease-quad) var(--reveal-delay, 0ms);
}

.js-motion [data-reveal="fade"] { transform: none; }
.js-motion [data-reveal="scale"] { transform: translate3d(0, var(--reveal-shift), 0) scale(.94); }
.js-motion [data-reveal="tilt"] { transform: translate3d(0, var(--reveal-shift), 0) rotate(1.1deg); }
.js-motion [data-reveal="left"] { transform: translate3d(-42px, 0, 0); }
.js-motion [data-reveal="right"] { transform: translate3d(42px, 0, 0); }

.js-motion [data-reveal].is-visible {
  opacity: 1;
  transform: none;
}

/* ---------- Masked line reveals for display headings ---------- */

.js-motion .line-mask {
  display: block;
  overflow: hidden;
  padding-bottom: .1em;
  margin-bottom: -.1em;
}

.js-motion .line-mask > i {
  display: block;
  font-style: inherit;
  transform: translate3d(0, 110%, 0);
  transition: transform 1.15s var(--ease-expo) var(--line-delay, 0ms);
}

.js-motion .is-visible .line-mask > i { transform: none; }

/* A masked heading drives its own lines, so it opts out of any wrapper entrance. */
.js-motion .hero-copy > .has-lines {
  opacity: 1;
  animation: none;
}

/* Give the grid overlay slack so parallax never exposes an edge. */
.js-motion .hero-grid { inset: -7% 0; }

/* ---------- Page intro wipe ---------- */

.intro-wipe {
  position: fixed;
  inset: 0;
  z-index: 300;
  background: var(--black);
  pointer-events: none;
  transform-origin: top center;
  animation: wipe-away .95s var(--ease-expo) .1s both;
}

.intro-wipe i {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 46px;
  height: 2px;
  margin: -1px 0 0 -23px;
  background: var(--red);
  transform-origin: center;
  animation: wipe-bar .75s var(--ease-quad) both;
}

@keyframes wipe-away { to { transform: translate3d(0, -101%, 0); } }
@keyframes wipe-bar {
  0% { transform: scaleX(0); opacity: 1; }
  70% { transform: scaleX(1); opacity: 1; }
  100% { transform: scaleX(1); opacity: 0; }
}

/* ---------- Hero staged entrance ---------- */

.js-motion .hero-copy > * { animation: rise-in .95s var(--ease-expo) both; }
.js-motion .hero-copy > :nth-child(1) { animation-delay: .55s; }
.js-motion .hero-copy > :nth-child(2) { animation-delay: .64s; }
.js-motion .hero-copy > :nth-child(3) { animation-delay: .76s; }
.js-motion .hero-copy > :nth-child(4) { animation-delay: .86s; }
.js-motion .hero-copy > :nth-child(5) { animation-delay: .96s; }
.js-motion .hero-scene { animation: scene-in 1.4s var(--ease-expo) .5s both; }
.js-motion .hero-bottom { animation: rise-in .9s var(--ease-expo) 1s both; }

@keyframes rise-in {
  from { opacity: 0; transform: translate3d(0, 34px, 0); }
  to { opacity: 1; transform: none; }
}

@keyframes scene-in {
  from { opacity: 0; transform: translate3d(38px, 0, 0); }
  to { opacity: 1; transform: none; }
}

/* ---------- Scroll progress ---------- */

.scroll-progress {
  position: fixed;
  left: 0;
  top: 0;
  right: 0;
  height: 2px;
  z-index: 40;
  pointer-events: none;
  background: transparent;
}

.scroll-progress i {
  display: block;
  height: 100%;
  background: var(--red);
  transform: scaleX(var(--progress, 0));
  transform-origin: left center;
}

/* ---------- Header behaviour ---------- */

/* No background transition: header text snaps to the new theme colour, so a fading
   background leaves it briefly invisible against the old one. */
.site-header {
  transition:
    transform .55s var(--ease-snap),
    height .3s var(--ease-quad),
    border-color .3s ease;
}

.site-header.header-hidden { transform: translate3d(0, -104%, 0); }

/* Applied for the instant the theme flips: otherwise text colours fade while
   backgrounds snap, leaving text briefly invisible against the old background. */
.theme-switching,
.theme-switching * { transition: none !important; }

/* ---------- Route ticker ---------- */

.ticker {
  background: var(--black);
  color: #fff;
  overflow: hidden;
  padding: 20px 0;
  border-top: 1px solid #222;
  border-bottom: 1px solid #222;
  position: relative;
  z-index: 2;
}

.ticker-track {
  display: flex;
  width: max-content;
  animation: ticker-slide 38s linear infinite;
}

.ticker:hover .ticker-track { animation-play-state: paused; }

.ticker-track span {
  display: inline-flex;
  align-items: center;
  gap: 26px;
  padding-right: 26px;
  font: 500 clamp(19px, 2.1vw, 30px) var(--display);
  letter-spacing: -.03em;
  white-space: nowrap;
}

.ticker-track span::after {
  content: "";
  width: 7px;
  height: 7px;
  background: var(--red);
  border-radius: 50%;
  flex: 0 0 auto;
}

.ticker-track span:nth-child(even) { color: #6f6f70; }

@keyframes ticker-slide { to { transform: translate3d(-50%, 0, 0); } }

/* ---------- Micro-interactions ---------- */

.button span,
.text-link span {
  display: inline-block;
  transition: transform .45s var(--ease-snap);
}

.button:hover span,
.text-link:hover span { transform: translate3d(4px, -4px, 0); }

.button { position: relative; overflow: hidden; }
.button > * { position: relative; z-index: 1; }

.button::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(255, 255, 255, .16);
  transform: scaleX(0);
  transform-origin: right center;
  transition: transform .55s var(--ease-snap);
}

.button:hover::after {
  transform: scaleX(1);
  transform-origin: left center;
}

/* Service rows wipe in from the left rather than snapping to a flat fill. */
.service-row { position: relative; overflow: hidden; }
.service-row > * { position: relative; z-index: 1; }

.service-row::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background: var(--red);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform .6s var(--ease-snap);
}

.service-row:hover { background: transparent; }
.service-row:hover::before,
.service-row:focus-visible::before,
.service-row.active::before { transform: scaleX(1); }
.service-row:focus-visible .service-no,
.service-row:focus-visible .service-detail { color: #ffe3e5; }

.service-name {
  display: inline-block;
  transition: transform .55s var(--ease-snap);
}

.service-row:hover .service-name { transform: translate3d(14px, 0, 0); }

.intro-stats div { transition: border-color .4s ease; }
.intro-stats div:hover { border-color: var(--red); }

/* ---------- Ambient map sweep ---------- */

.map-panel::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(100deg, transparent 42%, rgba(227, 6, 19, .07) 50%, transparent 58%);
  animation: map-sweep 7s var(--ease-quad) infinite;
}

@keyframes map-sweep {
  0% { transform: translate3d(-55%, 0, 0); }
  55%, 100% { transform: translate3d(55%, 0, 0); }
}

/* ---------- Wave background (wave.js) ---------- */

/* Fixed behind everything. Sections marked data-wave go transparent to let it through;
   the rest keep their own backgrounds and simply cover it. */
.page-wave {
  position: fixed;
  inset: 0;
  z-index: -1;
  width: 100%;
  height: 100%;
  pointer-events: none;
  transition: opacity .9s var(--ease-quad);
  /* Lighter behind the left-hand copy, full strength over the open right side. */
  mask-image: linear-gradient(90deg, rgba(0, 0, 0, .38) 0%, rgba(0, 0, 0, .7) 38%, #000 62%);
  -webkit-mask-image: linear-gradient(90deg, rgba(0, 0, 0, .38) 0%, rgba(0, 0, 0, .7) 38%, #000 62%);
}

.page-wave.is-soft { opacity: .42; }

body.has-wave [data-wave] { background: transparent; }

/* The red glow from the sign-in screens, in the top-left corner behind the dots, so
   every page with the wave carries the same accent. Softer on the light background. */
body.has-wave::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  background: radial-gradient(ellipse at var(--glow-at, 18% 12%), rgba(227, 6, 19, .1), transparent 40%);
}
.dark body.has-wave::before { background: radial-gradient(ellipse at var(--glow-at, 18% 12%), rgba(227, 6, 19, .18), transparent 36%); }
/* The website's bar would cover a glow in the very corner; sit it behind the headline. */
body.has-wave:has(.site-header) { --glow-at: 16% 34%; }
body.has-wave .hero-grid { display: none; }

@media (max-width: 800px) {
  .page-wave { mask-image: none; -webkit-mask-image: none; opacity: .7; }
  .page-wave.is-soft { opacity: .35; }
}

/* Sign-in screens: fills the dark panel behind the card, under the brand and footer. */
.panel-wave {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* Client portal and staff dashboard: dimmed evenly behind the work area, since there is
   no headline to fade around. The sidebar and the cards cover it; it shows in between. */
.app-wave,
.app-wave.is-soft { mask-image: none; -webkit-mask-image: none; opacity: .6; }
body.has-wave .app-shell { background: transparent; }

/* ---------- Quote section freight scene ---------- */

.quote { position: relative; overflow: hidden; isolation: isolate; }
.quote-copy,
.quote-form { position: relative; z-index: 1; }
/* The quote section stays red in dark mode, so its label keeps its pale pink rather
   than the grey dark mode gives eyebrows elsewhere. */
/* Held to the same 4.5:1 as the rest of the text on the red band: #ffd4d7
   measured 3.64 against it. The band is red in both themes, so there is
   nothing for dark mode to soften here. */
.dark .quote .eyebrow { color: #fff4f4; }

.quote-scene {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

/* A faint chart grid that fades out towards the form, so the fields stay clean. */
.quote-scene::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(rgba(255, 255, 255, .07) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, .07) 1px, transparent 1px);
  background-size: 64px 64px;
  mask-image: radial-gradient(ellipse at 18% 72%, #000 0%, transparent 62%);
  -webkit-mask-image: radial-gradient(ellipse at 18% 72%, #000 0%, transparent 62%);
}

.quote-chart { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }

.quote-rings circle { fill: none; stroke: rgba(255, 255, 255, .13); stroke-width: 1; stroke-dasharray: 3 7; }
.quote-rings .quote-ping {
  stroke: rgba(255, 255, 255, .5);
  stroke-dasharray: none;
  vector-effect: non-scaling-stroke;
  transform-box: fill-box;
  transform-origin: center;
  opacity: 0;
  animation: quote-ping 6s var(--ease-quad) infinite;
}

.quote-arc { fill: none; stroke: rgba(255, 255, 255, .24); stroke-width: 1.5; stroke-dasharray: 6 9; animation: quote-dash 2.4s linear infinite; }
.quote-lane { fill: none; stroke: rgba(11, 11, 12, .28); stroke-width: 2; stroke-dasharray: 2 10; stroke-linecap: round; animation: quote-dash 1.8s linear infinite; }

/* The plane sits at the top of a circle centred at (800, 1700); turning the group
   carries it along the arc, nose always on the tangent. */
.quote-flight {
  transform-box: view-box;
  transform-origin: 800px 1700px;
  animation: quote-flight 26s linear infinite;
}

.quote-plane { fill: #fff; }

.quote-port circle { fill: #fff; }
.quote-port text { fill: rgba(255, 255, 255, .85); font: 500 15px var(--mono); letter-spacing: .12em; }
.quote-port .quote-coords { fill: rgba(255, 255, 255, .55); font-size: 12px; letter-spacing: .08em; }

.quote-sea {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 118px;
  overflow: hidden;
}

.quote-waves {
  position: absolute;
  left: 0;
  width: calc(100% + 120px);
  height: 26px;
  animation: quote-swell 7s linear infinite;
}

.quote-waves.back { top: 44px; opacity: .22; animation-duration: 11s; }
.quote-waves.front { bottom: 10px; opacity: .55; }

.quote-voyage {
  --ship-w: clamp(190px, 17vw, 280px);
  position: absolute;
  left: 0;
  right: 0;
  bottom: 14px;
  animation: quote-voyage 48s linear -15s infinite;
}

.quote-voyage.far {
  --ship-w: clamp(84px, 7vw, 118px);
  bottom: 52px;
  opacity: .32;
  animation-duration: 84s;
  animation-delay: -38s;
  animation-direction: reverse;
}

.quote-ship {
  display: block;
  width: var(--ship-w);
  height: auto;
  overflow: visible;
  transform-origin: 50% 85%;
  animation: quote-bob 5.5s ease-in-out infinite;
}

.quote-voyage.far .quote-ship { animation-duration: 7s; }
#halcyon-ship { overflow: visible; }
.ship-wake { fill: none; stroke: rgba(255, 255, 255, .6); stroke-width: 1.5; stroke-dasharray: 10 6; animation: quote-dash 1.2s linear infinite; }

/* Paused while scrolled out of view (motion.js), so the page isn't animating unseen. */
.quote-scene.is-paused,
.quote-scene.is-paused * { animation-play-state: paused !important; }

@keyframes quote-dash { to { stroke-dashoffset: -15; } }
@keyframes quote-swell { to { transform: translate3d(-120px, 0, 0); } }
@keyframes quote-voyage {
  from { transform: translate3d(calc(-1 * var(--ship-w)), 0, 0); }
  to { transform: translate3d(100%, 0, 0); }
}
@keyframes quote-bob {
  0%, 100% { transform: translate3d(0, 0, 0) rotate(-.6deg); }
  50% { transform: translate3d(0, -3px, 0) rotate(.6deg); }
}
@keyframes quote-flight {
  0% { transform: rotate(-34deg); opacity: 0; }
  8% { opacity: 1; }
  92% { opacity: 1; }
  100% { transform: rotate(34deg); opacity: 0; }
}
@keyframes quote-ping {
  0% { transform: scale(.12); opacity: .9; }
  100% { transform: scale(1); opacity: 0; }
}

@media (max-width: 800px) {
  .quote-scene::before { mask-image: linear-gradient(to top, #000, transparent 45%); -webkit-mask-image: linear-gradient(to top, #000, transparent 45%); }
  /* The stacked layout is tall, so a full-height chart would blow the plane up across the
     heading. Keep the flight to a band above it and leave the port markings to desktop. */
  .quote-chart { height: 300px; }
  .quote-rings,
  .quote-port,
  .quote-lane { display: none; }
  .quote-sea { height: 92px; }
  .quote-waves.back { top: 26px; }
  .quote-voyage { --ship-w: 170px; bottom: 10px; }
  .quote-voyage.far { display: none; }
}

/* ---------- Reduced motion ---------- */

@media (prefers-reduced-motion: reduce) {
  .js-motion [data-reveal],
  .js-motion [data-reveal].is-visible,
  .js-motion .line-mask > i,
  .js-motion .hero-copy > *,
  .js-motion .hero-scene,
  .js-motion .hero-bottom {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
    transition: none !important;
  }

  .intro-wipe,
  .map-panel::after { display: none; }
  .ticker-track { animation: none; }
  .site-header.header-hidden { transform: none; }

  /* The freight scene holds still: plane at the top of its arc, ship moored off the left. */
  .quote-scene * { animation: none !important; }
  .quote-voyage { transform: translate3d(6vw, 0, 0); }
  .quote-voyage.far { transform: translate3d(58vw, 0, 0); }
}

