/* =============================================================================
   STARS DELIGHTFUL — CINEMATIC MOTION LAYER
   Loads after main.css. Purely additive: delete this file and the <script> for
   motion.js and the site returns to its previous behaviour untouched.

   Principles
   - Every animated element is VISIBLE BY DEFAULT. Motion classes are added by
     JS only once it has confirmed it can run, so a JS failure can never leave
     the page blank.
   - One shared rAF loop drives scroll work (motion.js); CSS only declares the
     resting and target states.
   - prefers-reduced-motion disables transforms and blur, keeping opacity fades.
   ========================================================================== */

:root {
  --m-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --m-ease-soft: cubic-bezier(0.33, 1, 0.68, 1);
  --m-dur: 900ms;
  --m-dur-fast: 520ms;
}

/* Only once JS is driving things do we allow start-states to hide anything. */
.js-motion [data-m] { will-change: transform, opacity; }

/* ---------------------------------------------------------------------------
   0. SMOOTH SCROLL — the few rules Lenis needs

   Lenis adds .lenis / .lenis-smooth to <html> itself, and only while it is
   really running, so none of this applies when it is off (reduced motion,
   touch, or a load failure) and the page scrolls natively.
   ------------------------------------------------------------------------ */
html.lenis,
html.lenis body { height: auto; }
/* main.css sets scroll-behavior: smooth. The browser's own easing and Lenis's
   easing both try to own the scroll position, and the result visibly tugs.
   Keyed to .smooth-scroll, which motion.js sets once the instance is
   built. The name deliberately contains no "lenis": Lenis rewrites the root
   className through /lenis(-\w+)?/g and would eat it. */
html.smooth-scroll { scroll-behavior: auto !important; }
/* Panels with their own scrollbar opt out via data-lenis-prevent (motion.js
   tags them); contain stops the page behind them taking over at the ends. */
html.smooth-scroll [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-stopped { overflow: hidden; }
/* Lenis's own guidance is to kill pointer events on every iframe, because an
   iframe swallows wheel events and strands the eased scroll mid-flight. That
   is wrong here: the only iframe this site would ever embed is the Square
   booking calendar (_square_embed in pages_b.py), and a calendar nobody can
   click is worse than a scroll that stalls over it. Opt an iframe out by hand
   instead -- wrap it in [data-lenis-prevent] -- so the failure is visible at
   the time you add it rather than silent. */
html.smooth-scroll iframe[data-decorative] { pointer-events: none; }

/* ---------------------------------------------------------------------------
   1. MASKED LINE REVEAL  — the editorial signature
   Each line sits inside an overflow:hidden wrapper and rises into place.
   ------------------------------------------------------------------------ */
/* Headings run at line-height 1.04, which is tighter than the font's own glyph
   box — so this mask was slicing the tails off g, y, p and q. "Eight services."
   and "strategy." were both losing 6px. The padding gives the mask room to
   show a descender; the matching negative margin gives that room straight back
   to the layout, so nothing moves. */
.m-line {
  display: block;
  overflow: hidden;
  padding-bottom: 0.18em;
  margin-bottom: -0.18em;
}
.m-line > span {
  display: block;
  transform: translate3d(0, 0, 0);
}
.js-motion .m-line > span {
  /* 130%, not 110%: the mask is now taller than the line it hides, and at 110%
     the text would still be peeking out of the bottom before it rises. */
  transform: translate3d(0, 130%, 0);
  opacity: 0;
  transition: transform var(--m-dur) var(--m-ease),
              opacity calc(var(--m-dur) * 0.6) linear;
  transition-delay: var(--m-delay, 0ms);
}
.js-motion .is-revealed .m-line > span,
.js-motion .m-line.is-revealed > span {
  transform: translate3d(0, 0, 0);
  opacity: 1;
}

/* ---------------------------------------------------------------------------
   2. SCROLL REVEALS
   Richer than the base data-reveal: direction, scale and a touch of blur.
   ------------------------------------------------------------------------ */
.js-motion [data-m="up"],
.js-motion [data-m="fade"],
.js-motion [data-m="left"],
.js-motion [data-m="right"],
.js-motion [data-m="scale"] {
  opacity: 0;
  transition: opacity var(--m-dur-fast) var(--m-ease-soft),
              transform var(--m-dur) var(--m-ease),
              filter var(--m-dur) var(--m-ease);
  transition-delay: var(--m-delay, 0ms);
}
.js-motion [data-m="up"]    { transform: translate3d(0, 34px, 0); }
.js-motion [data-m="left"]  { transform: translate3d(-34px, 0, 0); }
.js-motion [data-m="right"] { transform: translate3d(34px, 0, 0); }
.js-motion [data-m="scale"] { transform: scale(0.94); filter: blur(6px); }

.js-motion [data-m].is-in {
  opacity: 1;
  transform: none;
  filter: none;
}

/* ---------------------------------------------------------------------------
   3. CINEMATIC IMAGE REVEAL
   A curtain wipes back while the image itself settles from a slight zoom —
   the thing that reads as "expensive" more than any other single effect.
   ------------------------------------------------------------------------ */
.m-frame { position: relative; overflow: hidden; }
.m-frame::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  background: var(--ink-900, #04070F);
  transform: scaleY(1);
  transform-origin: top;
  pointer-events: none;
  opacity: 0;
}
.js-motion .m-frame::after {
  opacity: 1;
  transition: transform 1100ms var(--m-ease);
  transition-delay: var(--m-delay, 0ms);
}
.js-motion .m-frame.is-in::after { transform: scaleY(0); transform-origin: bottom; }

/* .zoomable is in this list because most framed images are wrapped in a
   button that opens the lightbox. Without it the chain broke at the button
   and the settle-from-zoom silently did nothing -- the curtain still wiped,
   so the effect looked merely weak rather than broken.

   The entrance zoom deliberately lives on this wrapper while the scroll-linked
   scale (section 14) lives on the <img> inside it. Two transforms on one
   element would overwrite each other; on nested elements they compose. */
.js-motion .m-frame > picture,
.js-motion .m-frame > img,
.js-motion .m-frame > .zoomable {
  display: block;
  transform: scale(1.12);
  transition: transform 1400ms var(--m-ease);
  transition-delay: var(--m-delay, 0ms);
}
.js-motion .m-frame.is-in > picture,
.js-motion .m-frame.is-in > img,
.js-motion .m-frame.is-in > .zoomable { transform: scale(1); }

/* ---------------------------------------------------------------------------
   4. HERO — depth through parallax
   motion.js writes --m-y / --m-scale; CSS just consumes them.
   ------------------------------------------------------------------------ */
.hero__bg { will-change: transform; }
.js-motion .hero__bg {
  transform: translate3d(0, var(--m-y, 0px), 0) scale(var(--m-scale, 1.04));
}
.js-motion .hero__content { transform: translate3d(0, var(--m-content-y, 0px), 0); }
.js-motion .hero__content { opacity: var(--m-content-o, 1); }

/* the eyebrow, lead, buttons and badge step in after the headline */
.js-motion .hero__eyebrow,
.js-motion .hero__lead,
.js-motion .hero .btn-row,
.js-motion .hero__note,
.js-motion .hero__badge {
  opacity: 0;
  transform: translate3d(0, 22px, 0);
  transition: opacity 700ms var(--m-ease-soft), transform 900ms var(--m-ease);
}
.js-motion .hero.is-ready .hero__eyebrow { opacity: 1; transform: none; transition-delay: 120ms; }
.js-motion .hero.is-ready .hero__lead    { opacity: 1; transform: none; transition-delay: 620ms; }
.js-motion .hero.is-ready .btn-row       { opacity: 1; transform: none; transition-delay: 760ms; }
.js-motion .hero.is-ready .hero__note    { opacity: 1; transform: none; transition-delay: 860ms; }
.js-motion .hero.is-ready .hero__badge   { opacity: 1; transform: none; transition-delay: 960ms; }

/* ---------------------------------------------------------------------------
   5. PARALLAX for any element marked data-parallax
   ------------------------------------------------------------------------ */
.js-motion [data-parallax] {
  transform: translate3d(0, var(--m-y, 0px), 0);
  will-change: transform;
}

/* ---------------------------------------------------------------------------
   6. MAGNETIC BUTTONS — pointer attraction, fine pointers only
   ------------------------------------------------------------------------ */
@media (hover: hover) and (pointer: fine) {
  /* Not scoped to .btn any more -- nav links use the same pull, at a smaller
     amplitude set in motion.js. */
  .js-motion [data-magnetic] {
    transform: translate3d(var(--m-mx, 0px), var(--m-my, 0px), 0);
    transition: transform 420ms var(--m-ease), box-shadow var(--m-dur-fast) var(--m-ease);
  }
  .js-motion .btn[data-magnetic]:active { transition-duration: 80ms; }
  /* The underline is a child of the link, so it rides the same transform --
     it must not also ease on its own or it lags behind the letters. */
  .js-motion .nav__link[data-magnetic]::after { transition: transform 260ms var(--m-ease); }
}

/* ---------------------------------------------------------------------------
   7. HEADER — recede on scroll down, return on scroll up
   ------------------------------------------------------------------------ */
.header { transition: transform 520ms var(--m-ease), background-color 420ms var(--m-ease),
                      backdrop-filter 420ms var(--m-ease), box-shadow 420ms var(--m-ease),
                      border-color 420ms var(--m-ease); }
.js-motion .header.is-tucked { transform: translate3d(0, -102%, 0); }

/* ---------------------------------------------------------------------------
   8. SECTION HAIRLINE — a gold rule that draws itself
   ------------------------------------------------------------------------ */
.js-motion .sec-head .kicker::before {
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 720ms var(--m-ease) 120ms;
}
.js-motion .sec-head.is-in .kicker::before,
.js-motion .is-in .kicker::before { transform: scaleX(1); }

/* ---------------------------------------------------------------------------
   9. CARD LIFT — refined, with a light sweep across the surface
   ------------------------------------------------------------------------ */
@media (hover: hover) and (pointer: fine) {
  .card, .pkg, .case-card { position: relative; }
  .card::after, .pkg::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    opacity: 0;
    background: radial-gradient(
      420px circle at var(--m-px, 50%) var(--m-py, 50%),
      rgba(255, 255, 255, 0.07), transparent 62%);
    transition: opacity 420ms var(--m-ease);
  }
  .card:hover::after, .pkg:hover::after { opacity: 1; }
}

/* ---------------------------------------------------------------------------
   10. PAGE TRANSITION — a brief curtain between pages
   ------------------------------------------------------------------------ */
.m-curtain {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: var(--ink-900, #04070F);
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
}
.js-motion .m-curtain {
  transition: opacity 420ms var(--m-ease), visibility 420ms;
}
.js-motion .m-curtain.is-out { opacity: 1; visibility: visible; }

/* ---------------------------------------------------------------------------
   12. NAVIGATION — the menu arrives, it does not simply exist

   Staggered by --m-delay, written by motion.js so the order survives any
   change to the nav in common.py. Start-states live under .js-motion, so the
   menu is fully visible if this layer never runs.
   ------------------------------------------------------------------------ */
.js-motion .header .nav__link,
.js-motion .header .nav__cta,
.js-motion .header .burger {
  opacity: 0;
  transform: translate3d(0, -10px, 0);
  transition: opacity 620ms var(--m-ease), transform 620ms var(--m-ease);
}
.js-motion .header.is-ready .nav__link,
.js-motion .header.is-ready .nav__cta,
.js-motion .header.is-ready .burger {
  opacity: 1;
  transform: none;
  transition-delay: var(--m-delay, 0ms);
}

/* ---------------------------------------------------------------------------
   13. MOBILE MENU — full-screen, and the rows arrive in sequence

   The panel was already full-screen below the header; what it lacked was any
   sense of arrival. Rows are staggered on open and reset on close, so a second
   open replays rather than showing an already-finished list.
   ------------------------------------------------------------------------ */
.js-motion .mobile-nav a,
.js-motion .mobile-nav__cta > * {
  opacity: 0;
  transform: translate3d(0, 14px, 0);
  transition: opacity 520ms var(--m-ease), transform 520ms var(--m-ease);
}
.js-motion .mobile-nav.is-open a,
.js-motion .mobile-nav.is-open .mobile-nav__cta > * {
  opacity: 1;
  transform: none;
  transition-delay: var(--m-delay, 0ms);
}
/* A hairline that wipes across each row as it lands. */
.js-motion .mobile-nav a { background-size: 0% 1px; background-repeat: no-repeat;
  background-position: 0 100%;
  background-image: linear-gradient(to right, var(--brand-gold), transparent);
  transition: opacity 520ms var(--m-ease), transform 520ms var(--m-ease),
              background-size 700ms var(--m-ease), color 240ms var(--m-ease); }
.js-motion .mobile-nav.is-open a { background-size: 100% 1px; }

/* ---------------------------------------------------------------------------
   14. SCROLL-LINKED IMAGE SCALE — the frame breathes as it passes

   motion.js writes --m-img (1 → ~1.06) from the element's position in the
   viewport. Paired with overflow:hidden on the frame, the picture drifts
   inside a fixed crop instead of the whole block moving, which is what makes
   it read as depth rather than as a jump.
   ------------------------------------------------------------------------ */
.js-motion [data-img-scale] { overflow: hidden; }
/* Descendant, not child: these frames wrap the picture in a button.zoomable
   that opens the lightbox, so `> img` and `> picture > img` both miss. */
.js-motion [data-img-scale] img {
  transform: scale(var(--m-img, 1));
  transform-origin: center;
  will-change: transform;
}

/* ---------------------------------------------------------------------------
   15. AMBIENT GRADIENT — a very slow drift behind dark sections

   Long duration and tiny travel on purpose: at this speed it is never caught
   moving, it only stops the large flat areas from looking dead. It sits on a
   ::before so no section's own background has to change.
   ------------------------------------------------------------------------ */
/* overflow:hidden is load-bearing, not tidiness. The wash below bleeds -20%
   on every side and the drift scales it a further 6%, so without a clip it
   sticks ~75px past each edge of a 375px phone. The body then grows to
   contain it and the WHOLE PAGE slides left and right -- every full-width
   element gets measured against the wider body and overflows in turn, which
   makes it look like a dozen separate layout bugs instead of one. */
.js-motion .sec--ambient { position: relative; isolation: isolate; overflow: hidden; }
.js-motion .sec--ambient::before {
  content: "";
  position: absolute;
  inset: -20%;
  z-index: -1;
  pointer-events: none;
  opacity: 0.55;
  background:
    radial-gradient(38% 44% at 22% 30%, rgba(1, 89, 168, 0.18), transparent 70%),
    radial-gradient(34% 40% at 78% 68%, rgba(229, 139, 1, 0.12), transparent 70%);
  animation: m-drift 38s ease-in-out infinite alternate;
}
@keyframes m-drift {
  from { transform: translate3d(-2%, -1%, 0) scale(1); }
  to   { transform: translate3d(2%, 1.5%, 0) scale(1.06); }
}

/* ---------------------------------------------------------------------------
   16. STATE ROTATOR — the names behind the number

   "5 States served" is a figure with nothing to look at. The five names cycle
   through a single masked line instead, each rising into place as the last one
   leaves, so the cell reads as a place rather than a count.

   The height is locked to one line: the stats row is a grid, and a cell that
   grew or shrank as the longest name came round would nudge its neighbours.
   ------------------------------------------------------------------------ */
.js-motion .stat__rotator {
  position: relative;
  height: 1.3em;
  overflow: hidden;
}
.js-motion .stat__state {
  position: absolute;
  inset: 0;
  display: block;
  opacity: 0;
  transform: translate3d(0, 110%, 0);
  transition: opacity 560ms var(--m-ease), transform 560ms var(--m-ease);
  /* One line, so the masked slide stays crisp. The longest name today
     ("Pennsylvania", 92px) clears the narrowest cell comfortably; the ellipsis
     is only there so a longer name added later degrades into a readable
     truncation instead of being sliced through a letter. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The comma belongs to the inline fallback only. */
.js-motion .stat__state + .stat__state::before { content: none; }
.js-motion .stat__state.is-active { opacity: 1; transform: none; }
.js-motion .stat__state.is-out { opacity: 0; transform: translate3d(0, -110%, 0); }

/* ---------------------------------------------------------------------------
   17. WORD REVEAL — the intro sentence arrives a word at a time

   26ms apart, which is fast enough to read as one movement rather than as
   words queueing up. Each word rises a third of its own height, so the line
   never looks like it is being typed.
   ------------------------------------------------------------------------ */
.js-motion .m-w {
  display: inline-block;
  opacity: 0;
  transform: translate3d(0, 0.34em, 0);
  transition: opacity 560ms var(--m-ease), transform 560ms var(--m-ease);
  transition-delay: var(--m-delay, 0ms);
  /* Without this an inline-block word with a descender can push the line box
     taller than the plain text did, and the paragraph shifts as it reveals. */
  vertical-align: baseline;
}
.js-motion [data-m-words].is-in .m-w { opacity: 1; transform: none; }

/* The hero lead otherwise fades in as a whole from .hero.is-ready, which would
   run underneath the word stagger. Pinned open so only the words animate. */
.js-motion .hero__lead.has-words {
  opacity: 1 !important;
  transform: none !important;
  transition: none !important;
}

/* ---------------------------------------------------------------------------
   18. CALL-TO-ACTION BAND

   The aurora drifts slowly and continuously; everything else happens once, in
   order: rule, kicker, headline (masked lines, section 1), sentence, buttons.
   A single sheen crosses the panel as it lands, which is what makes the glass
   read as glass rather than as a grey box.
   ------------------------------------------------------------------------ */
.js-motion .cta-band__aurora {
  animation: m-aurora 26s ease-in-out infinite alternate;
}
@keyframes m-aurora {
  from { transform: translate3d(-3%, -2%, 0) scale(1); }
  to   { transform: translate3d(3%, 2%, 0) scale(1.1); }
}

.js-motion .cta-band__kicker,
.js-motion .cta-band__text,
.js-motion .cta-band__actions > * {
  opacity: 0;
  transform: translate3d(0, 14px, 0);
  transition: opacity 680ms var(--m-ease), transform 680ms var(--m-ease);
}
.js-motion .cta-band__kicker span { transform: scaleX(0); transform-origin: left;
  transition: transform 520ms var(--m-ease); }

.js-motion .cta-band__panel.is-in .cta-band__kicker { opacity: 1; transform: none; }
.js-motion .cta-band__panel.is-in .cta-band__kicker span { transform: scaleX(1); transition-delay: 120ms; }
/* After the headline's own line reveal, not during it. */
.js-motion .cta-band__panel.is-in .cta-band__text { opacity: 1; transform: none; transition-delay: 420ms; }
.js-motion .cta-band__panel.is-in .cta-band__actions > *:nth-child(1) { opacity: 1; transform: none; transition-delay: 560ms; }
.js-motion .cta-band__panel.is-in .cta-band__actions > *:nth-child(2) { opacity: 1; transform: none; transition-delay: 660ms; }

/* One pass of light across the glass, on arrival only. */
.js-motion .cta-band__panel::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(105deg, transparent 38%,
              rgba(255, 255, 255, 0.07) 48%, transparent 58%);
  transform: translateX(-120%);
}
.js-motion .cta-band__panel.is-in::after {
  transition: transform 1500ms cubic-bezier(0.22, 1, 0.36, 1) 300ms;
  transform: translateX(120%);
}

/* ---------------------------------------------------------------------------
   19. HERO EYEBROW — a comet running around the shape

   CSS motion path, so the light actually follows the perimeter at a constant
   speed. The obvious approach, rotating a conic gradient inside the border,
   looks wrong on a pill this wide: angle is not distance, so the two short
   ends take ~150deg of the sweep each and the light crawls there and flicks
   across the long top edge.

   offset-path rides the border box; the 2px inset keeps the head inside the
   pill's clip-path. offset-rotate: auto turns the tail to follow the curve
   round the ends instead of sliding sideways through them.
   ------------------------------------------------------------------------ */
@supports (offset-path: inset(0 round 999px)) {
  .js-motion .hero__eyebrow::before {
    content: "";
    position: absolute;
    width: 54px;
    height: 2px;
    border-radius: 999px;
    /* Head at the leading end, tail fading out behind it. */
    background: linear-gradient(90deg,
      rgba(255, 186, 66, 0) 0%,
      rgba(229, 139, 1, 0.55) 55%,
      rgba(255, 226, 170, 1) 100%);
    offset-path: inset(2px round 999px);
    offset-rotate: auto;
    offset-distance: 0%;
    animation: m-comet 2.2s linear infinite;
    pointer-events: none;
  }
  /* Linear: any easing slows the head at the same two points every lap, which
     reads as a stutter rather than as travel. */
  @keyframes m-comet {
    to { offset-distance: 100%; }
  }
}

/* A single pass of light across the glass as the hero settles, picking up the
   same choreography as the rest of the entrance. */
.js-motion .hero__eyebrow::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(110deg, transparent 40%,
              rgba(255, 255, 255, 0.16) 50%, transparent 60%);
  transform: translateX(-130%);
}
.js-motion .hero.is-ready .hero__eyebrow::after {
  transition: transform 1400ms cubic-bezier(0.22, 1, 0.36, 1) 700ms;
  transform: translateX(130%);
}

/* ---------------------------------------------------------------------------
   11. REDUCED MOTION — keep the meaning, drop the movement
   ------------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .js-motion .m-line > span,
  .js-motion [data-m],
  .js-motion .hero__eyebrow,
  .js-motion .hero__lead,
  .js-motion .hero .btn-row,
  .js-motion .hero__note,
  .js-motion .hero__badge {
    transform: none !important;
    filter: none !important;
    opacity: 1 !important;
    transition: none !important;
  }
  .js-motion .hero__bg,
  .js-motion .hero__content,
  .js-motion [data-parallax] { transform: none !important; }
  .js-motion .m-frame::after { display: none; }
  .js-motion .m-frame > picture,
  .js-motion .m-frame > img,
  .js-motion .m-frame > picture,
  .js-motion .m-frame > .zoomable { transform: none !important; }
  .js-motion .header.is-tucked { transform: none !important; }
  .js-motion .sec-head .kicker::before { transform: scaleX(1) !important; }
  .m-curtain { display: none; }

  /* The menu still arrives, it just does not travel; the scale and the drift
     stop entirely. Opacity is kept because it carries sequence, not movement. */
  .js-motion .header .nav__link,
  .js-motion .header .nav__cta,
  .js-motion .header .burger,
  .js-motion .mobile-nav a,
  .js-motion .mobile-nav__cta > * { transform: none !important; }
  .js-motion [data-img-scale] img { transform: none !important; }
  .js-motion .sec--ambient::before { animation: none !important; transform: none !important; }

  /* Cycling text is motion whichever way it is dressed up, and a line that
     changes under you is exactly what this setting asks us not to do. Fall
     all the way back to the comma list: nothing moves, nothing is lost. */
  .js-motion .stat__rotator { position: static; height: auto; overflow: visible; }
  .js-motion .stat__state {
    position: static;
    display: inline;
    opacity: 1 !important;
    transform: none !important;
    white-space: normal;
  }
  .js-motion .stat__state + .stat__state::before { content: ", "; }

  /* Words stay put and stay visible: splitting is harmless, moving is not. */
  .js-motion .m-w {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }

  /* The CTA keeps its look and loses its motion: the aurora stops where it is,
     the panel's contents are simply present, and the sheen never runs. */
  .js-motion .cta-band__aurora { animation: none !important; transform: none !important; }
  .js-motion .cta-band__kicker,
  .js-motion .cta-band__text,
  .js-motion .cta-band__actions > * {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
  .js-motion .cta-band__kicker span { transform: scaleX(1) !important; }
  .js-motion .cta-band__panel::after { display: none; }

  /* The pill keeps its plain lit edge; the comet, the sheen and the dot's
     pulse all stop. */
  .js-motion .hero__eyebrow::before { display: none; }
  .js-motion .hero__eyebrow::after { display: none; }
  .hero__eyebrow .dot { animation: none !important; }
}

/* ---------- 20. BOOKING CHOOSER ----------
   Three routes, three arrows. Each row rises in on its own beat (--i), and
   its arrow keeps a slow nudge so the row reads as something you go through
   rather than a static list. The nudge stops on hover, where the arrow makes
   one decisive move instead. */
.booksheet .bookopt {
  opacity: 0;
  transform: translateY(14px);
}
.booksheet.is-open .bookopt {
  animation: bookopt-in 0.5s cubic-bezier(.22,1,.36,1) forwards;
  animation-delay: calc(0.12s + var(--i, 0) * 0.075s);
}
@keyframes bookopt-in {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* A light sweep that crosses the row once as it arrives. */
.booksheet .bookopt::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 -60%;
  width: 55%;
  background: linear-gradient(100deg, transparent,
              rgba(255, 255, 255, 0.07), transparent);
  pointer-events: none;
}
.booksheet.is-open .bookopt::before {
  animation: bookopt-sweep 1.1s var(--ease) forwards;
  animation-delay: calc(0.26s + var(--i, 0) * 0.075s);
}
@keyframes bookopt-sweep {
  to { left: 115%; }
}

/* The arrow's own pulse, offset per row so the three are never in lockstep. */
.booksheet.is-open .bookopt__arrow svg {
  animation: bookarrow-nudge 2.1s var(--ease) infinite;
  animation-delay: calc(0.6s + var(--i, 0) * 0.26s);
}
@keyframes bookarrow-nudge {
  0%, 62%, 100% { transform: translateX(0); }
  74%           { transform: translateX(4px); }
  86%           { transform: translateX(-1px); }
}
.bookopt:hover .bookopt__arrow svg,
.bookopt:focus-visible .bookopt__arrow svg {
  animation: none;
  transform: translateX(3px);
  transition: transform 0.22s var(--ease);
}

/* A tail that draws behind the head on hover, so the arrow reads as motion
   towards the page rather than a decoration. */
.bookopt__arrow::after {
  content: "";
  position: absolute;
  top: 50%;
  right: calc(100% + 3px);
  width: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--brand-gold));
  transition: width 0.26s var(--ease);
}
.bookopt:hover .bookopt__arrow::after,
.bookopt:focus-visible .bookopt__arrow::after { width: 16px; }

/* Reduced motion: the rows must still be visible, which an animation that
   never runs would not guarantee -- the from-state leaves them at opacity 0. */
@media (prefers-reduced-motion: reduce) {
  .booksheet .bookopt { opacity: 1; transform: none; }
  .booksheet.is-open .bookopt,
  .booksheet.is-open .bookopt::before,
  .booksheet.is-open .bookopt__arrow svg { animation: none; }
  .booksheet .bookopt::before { display: none; }
}
