/* ============================================================
   motion.css — the public site's motion layer (2026-10-14,
   ported from Trades feature/public-motion + feature/welcome-motion)
   ------------------------------------------------------------
   Loaded by the tenant-facing pages (the marketing pages, the
   client portal my-*, booking/availability/signup/reset-password)
   and by welcome.html (the product's own front door — the gate
   only, no tenant nav; it uses [data-mo-header] + [data-mo-drift]).
   Never by admin or the legal pages. Works with assets/js/motion.js
   and two inline snippets every tenant page carries:
     - the GATE in <head> adds html.mo-js (and html.mo-still for
       reduced motion or a framed page) BEFORE first paint. Set any
       later, the browser has already painted the page, and every
       below-the-fold element would animate visible -> hidden on load;
     - the NAV PRE-PAINT after #nav-placeholder paints the header this
       session last built, so it is in the very first frame (what lets
       a page transition keep the header steady).
   Ground rules:
     - nothing is hidden without html.mo-js, and the failsafe below
       shows everything after 4s if motion.js never runs;
     - html.mo-still = final states, no movement: the device asks for
       reduced motion, or the page is framed (the admin Website
       editor's preview measures regions and must not see them move);
     - only opacity/transform animate; hover effects only for
       hover-capable pointers.
   One motion level for every tenant (operator ruling, Trades
   2026-10-13, ported whole) — there is deliberately no per-tenant
   switch. Design reference: Trades design/motion-demo.html.
   ============================================================ */

:root {
  --mo-ease-out:    cubic-bezier(.22, 1, .36, 1);
  --mo-ease-spring: cubic-bezier(.34, 1.56, .64, 1);
}

/* In-page anchors land below the sticky header, not under it. */
section[id] { scroll-margin-top: 84px; }

/* A column waiting to slide in sits up to 36px outside its section; clip
   it so the page never scrolls sideways. clip, not hidden: it creates no
   scroll container, so sticky/fixed descendants keep working. */
body > section { overflow-x: clip; }


/* ── 1. Scroll reveals ──────────────────────────────────────────────────────
   [data-reveal] = rise (bare / "up") | fade | zoom | left | right. motion.js
   adds .mo-in on arrival and staggers whatever arrives together via --mo-d,
   then SETTLES the element (strips these hooks) once it has landed, so a
   card's own hover transition is never stuck with the slow reveal one.
   ⚠ Mark the COLUMN, not a .card inside it, for the same reason.
   A [data-stagger] container reveals each child as it is inserted — the hook
   for every JS-rendered grid (motion.js tags the children).
   ONLY the waiting state is styled, and only while motion is on
   (html.mo-js:not(.mo-still)): arriving just drops it, so an element always
   lands on ITS OWN opacity/transform (a .opacity-75 line stays at .75), and a
   reduced-motion visitor never sees anything hidden in the first place. */
html.mo-js [data-reveal] {
  transition: opacity .7s var(--mo-ease-out), transform .7s var(--mo-ease-out);
  transition-delay: var(--mo-d, 0ms);
}
html.mo-js:not(.mo-still) [data-reveal]:not(.mo-in)                { opacity: 0; }
html.mo-js:not(.mo-still) :is([data-reveal=""], [data-reveal="up"]):not(.mo-in) { transform: translate3d(0, 28px, 0); }
html.mo-js:not(.mo-still) [data-reveal="zoom"]:not(.mo-in)         { transform: scale(.94); }
html.mo-js:not(.mo-still) [data-reveal="left"]:not(.mo-in)         { transform: translate3d(-36px, 0, 0); }
html.mo-js:not(.mo-still) [data-reveal="right"]:not(.mo-in)        { transform: translate3d(36px, 0, 0); }
/* Once columns stack, sliding sideways in a single column reads as a glitch. */
@media (max-width: 991.98px) {
  html.mo-js:not(.mo-still) :is([data-reveal="left"], [data-reveal="right"]):not(.mo-in) { transform: translate3d(0, 28px, 0); }
}

/* The section rule draws itself out once its heading has landed. */
html.mo-js [data-reveal] hr.divider {
  transition: width .8s var(--mo-ease-out);
  transition-delay: calc(var(--mo-d, 0ms) + 300ms);
}
html.mo-js:not(.mo-still) [data-reveal]:not(.mo-in) hr.divider { width: 0; }

/* Failsafe: the gate is set pre-paint, so if motion.js is blocked or fails to
   load, everything it would have revealed shows itself after 4s anyway.
   motion.js adds html.mo-ready the moment it runs, which cancels this. */
@keyframes mo-failsafe { to { opacity: 1; transform: none; stroke-dashoffset: 0; } }
html.mo-js:not(.mo-ready) :is([data-reveal], .mo-pop, .mo-count, .feature-icon, .step-circle, .mo-draw path) {
  animation: mo-failsafe 0s 4s forwards;
}


/* ── 2. Parallax ────────────────────────────────────────────────────────────
   The hero photo drifts at ~1/3 of the scroll speed and the hero copy eases
   up and fades — all set inline by motion.js (one rAF-throttled handler, off
   below 768px and under mo-still). Nothing to declare here. */


/* ── 3. Page transitions ────────────────────────────────────────────────────
   Cross-document view transitions: both pages opt in, the header is captured
   on its own so it stays put while the page cross-fades. Pure CSS; a browser
   without support simply changes page as before. Admin pages never load this
   file, so public ⇄ admin navigation is never animated. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
}
#mainNav { view-transition-name: mo-nav; }
::view-transition-old(root) { animation: mo-vt-out .18s ease-in both; }
::view-transition-new(root) { animation: mo-vt-in .4s var(--mo-ease-out) both; }
@keyframes mo-vt-out { to   { opacity: 0; } }
@keyframes mo-vt-in  { from { opacity: 0; transform: translateY(14px); } }


/* ── 4. Smooth scrolling ────────────────────────────────────────────────────
   The browser's own — no scroll-hijacking library. */
@media (prefers-reduced-motion: no-preference) {
  html.mo-js:not(.mo-still) { scroll-behavior: smooth; }
}
.mo-top {
  position: fixed; right: 16px; bottom: 16px; z-index: 1040;
  width: 44px; height: 44px; border-radius: 50%; border: 0;
  background: var(--sage-600); color: var(--on-accent);
  box-shadow: 0 6px 18px rgb(0 0 0 / .2);
  display: flex; align-items: center; justify-content: center;
  opacity: 0; transform: translateY(12px); pointer-events: none;
  transition: opacity .25s, transform .25s var(--mo-ease-out);
}
.mo-top.mo-shown { opacity: 1; transform: none; pointer-events: auto; }
.mo-top:focus-visible { outline: 3px solid var(--sage-200); outline-offset: 2px; }


/* ── 5. Sliders & galleries ─────────────────────────────────────────────── */
/* Home gallery strip tiles — each opens the photo viewer. */
.mo-tile {
  position: relative; display: block; height: 260px; overflow: hidden;
  border-radius: 1rem; box-shadow: 0 2px 14px rgb(0 0 0 / .13); cursor: zoom-in;
}
.mo-tile img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform .6s var(--mo-ease-out);
}
.mo-tile:focus-visible { outline: 3px solid var(--sage-500); outline-offset: 3px; }

/* Slow zoom ("Ken Burns") on the home About photos. */
@keyframes mo-kenburns { from { transform: scale(1); } to { transform: scale(1.09) translate(-1.5%, -1%); } }
#aboutCarousel .carousel-item.active img { animation: mo-kenburns 6.5s ease-out both; }

/* Photo viewer (motion.js builds it on first use). */
.mo-lb {
  position: fixed; inset: 0; z-index: 1090; background: rgb(10 12 14 / .92);
  display: flex; align-items: center; justify-content: center; padding: 64px 16px;
}
.mo-lb[hidden] { display: none; }
.mo-lb figure { margin: 0; max-width: min(1200px, 100%); text-align: center; }
.mo-lb img {
  max-width: 100%; max-height: calc(100vh - 170px); border-radius: .6rem;
  box-shadow: 0 20px 60px rgb(0 0 0 / .5); user-select: none; -webkit-user-drag: none;
}
.mo-lb figcaption { color: rgb(255 255 255 / .8); font-size: .85rem; margin-top: .75rem; }
.mo-lb.mo-open figure { animation: mo-lb-in .35s var(--mo-ease-out) both; }
.mo-lb img.mo-swap { animation: mo-lb-swap .3s var(--mo-ease-out) both; }
@keyframes mo-lb-in   { from { opacity: 0; transform: scale(.92); } }
@keyframes mo-lb-swap { from { opacity: .2; transform: translateX(var(--mo-dir, 0)); } }
.mo-lb-btn {
  position: absolute; border: 0; width: 44px; height: 44px; border-radius: 50%;
  background: rgb(255 255 255 / .12); color: #fff;
  display: flex; align-items: center; justify-content: center; transition: background .2s;
}
.mo-lb-btn:hover, .mo-lb-btn:focus-visible { background: rgb(255 255 255 / .28); }
.mo-lb-btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.mo-lb-close { top: 12px; right: 12px; }
.mo-lb-prev  { left: 12px;  top: 50%; margin-top: -22px; }
.mo-lb-next  { right: 12px; top: 50%; margin-top: -22px; }
.mo-lb-count { position: absolute; top: 22px; left: 50%; transform: translateX(-50%); color: rgb(255 255 255 / .7); font-size: .8rem; }
.mo-lb.mo-single .mo-lb-prev, .mo-lb.mo-single .mo-lb-next, .mo-lb.mo-single .mo-lb-count { display: none; }


/* ── 6. Animated icons & illustrations ──────────────────────────────────── */
/* Same contract as the reveals: only the waiting state is styled, only while
   motion is on; arriving drops it. */

/* Trust-bar icons pop in ([data-anim="pop"] on the item, .mo-pop on the icon). */
.mo-pop { display: inline-block; }
html.mo-js [data-anim="pop"] .mo-pop {
  transition: opacity .3s, transform .7s var(--mo-ease-spring);
  transition-delay: var(--mo-d, 0ms);
}
html.mo-js:not(.mo-still) [data-anim="pop"]:not(.mo-in) .mo-pop { opacity: 0; transform: scale(.3) rotate(-20deg); }

/* A number that counts up ("{years} years") — hidden until it starts counting. */
html.mo-js:not(.mo-still) [data-anim]:not(.mo-in) .mo-count { opacity: 0; }

/* Feature icons (about values, careers, policies) pop as their card lands. */
html.mo-js [data-reveal] .feature-icon {
  transition: opacity .3s, transform .6s var(--mo-ease-spring);
  transition-delay: calc(var(--mo-d, 0ms) + 200ms);
}
html.mo-js:not(.mo-still) [data-reveal]:not(.mo-in) .feature-icon { opacity: 0; transform: scale(.5); }

/* Process steps (.mo-steps on the row): numbers pop in order, each sends out
   one soft ring. */
.step-circle { position: relative; }
html.mo-js .mo-steps > [data-reveal] .step-circle {
  transition: opacity .3s, transform .6s var(--mo-ease-spring);
  transition-delay: calc(var(--mo-d, 0ms) + 150ms);
}
html.mo-js:not(.mo-still) .mo-steps > [data-reveal]:not(.mo-in) .step-circle { opacity: 0; transform: scale(.4); }
html.mo-js:not(.mo-still) .mo-steps > [data-reveal].mo-in .step-circle::after {
  content: ""; position: absolute; inset: 0; border-radius: 50%;
  border: 2px solid var(--sage-500);
  animation: mo-ring 1s ease-out both;
  animation-delay: calc(var(--mo-d, 0ms) + 450ms);
}
@keyframes mo-ring { from { opacity: .8; transform: scale(1); } to { opacity: 0; transform: scale(1.75); } }

/* Line drawing that sketches itself ([data-anim="draw"]; pathLength="1" on
   every stroke, --mo-p = that stroke's own delay). */
.mo-draw path { fill: none; stroke: currentColor; stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }
html.mo-js:not(.mo-still) [data-anim="draw"] .mo-draw path {
  stroke-dasharray: 1;
  transition: stroke-dashoffset 1.1s var(--mo-ease-out), opacity .15s;
  transition-delay: calc(var(--mo-d, 0ms) + var(--mo-p, 0ms));
}
html.mo-js:not(.mo-still) [data-anim="draw"]:not(.mo-in) .mo-draw path { stroke-dashoffset: 1; opacity: 0; }

/* The check mark motion.js draws into a success message ([data-mo-success]). */
.mo-check { width: 1.35em; height: 1.35em; vertical-align: -.3em; margin-right: .4em; flex-shrink: 0; }
.mo-check path { fill: none; stroke: currentColor; stroke-width: 3.5; stroke-linecap: round; stroke-linejoin: round; }
html.mo-js:not(.mo-still) .mo-check path {
  stroke-dasharray: 1; stroke-dashoffset: 1;
  animation: mo-draw .6s var(--mo-ease-out) forwards;
}
html.mo-js:not(.mo-still) .mo-check .mo-check-mark { animation-delay: .4s; }
@keyframes mo-draw { to { stroke-dashoffset: 0; } }


/* ── 7. Hover & micro-interactions ──────────────────────────────────────── */
/* Header shrinks once the page scrolls. The bottom margin grows by exactly
   what the padding loses (2 × .425rem), so the page underneath never jumps. */
#mainNav { transition: padding .3s var(--mo-ease-out), margin .3s var(--mo-ease-out), box-shadow .3s; }
#mainNav .navbar-brand { transition: transform .3s var(--mo-ease-out); transform-origin: left center; }
#mainNav.mo-compact {
  padding-top: .45rem; padding-bottom: .45rem; margin-bottom: .85rem;
  box-shadow: 0 6px 24px rgb(0 0 0 / .09) !important;   /* beats .shadow-sm's !important */
}
#mainNav.mo-compact .navbar-brand { transform: scale(.92); }

/* Primary call-to-action arrow. Drawn in CSS so a translated label — which
   replaces the button's textContent — can never wipe it. Scoped to html.mo-js
   (any gated page: the tenant pages AND welcome), never to admin. */
html.mo-js .mo-arrow::after {
  content: "\f138";                       /* bi-arrow-right */
  font-family: "bootstrap-icons";
  font-weight: normal; font-style: normal; line-height: 1;
  display: inline-block; margin-left: .45em; vertical-align: -.125em;
  transition: transform .25s var(--mo-ease-out);
}
html.mo-js .btn:not(:disabled):active { transform: scale(.97); }

/* Inputs: a sage line grows across the underline on focus. (No page carries
   .input-underline yet — kept for parity with the upstream Trades layer.) */
.input-underline {
  background-image: linear-gradient(var(--sage-500), var(--sage-500));
  background-repeat: no-repeat; background-position: 0 100%; background-size: 0 2px;
  transition: background-size .35s var(--mo-ease-out);
}
.input-underline:focus { background-size: 100% 2px; }

/* An error message shakes the moment it appears (a CSS animation restarts
   whenever an element goes from display:none to shown). */
@keyframes mo-shake {
  10%, 90% { transform: translateX(-1px); }
  20%, 80% { transform: translateX(3px); }
  30%, 50%, 70% { transform: translateX(-6px); }
  40%, 60% { transform: translateX(6px); }
}
html.mo-js .alert-danger:not(.d-none),
html.mo-js [data-mo-feedback].text-danger:not(.d-none) {
  animation: mo-shake .42s cubic-bezier(.36, .07, .19, .97) both;
}

@keyframes mo-wiggle { 0%, 100% { transform: rotate(0); } 25% { transform: rotate(-12deg); } 50% { transform: rotate(10deg); } 75% { transform: rotate(-6deg); } }

@media (hover: hover) {
  /* Nav: an underline slides in under the link you point at. */
  #mainNav .nav-link:not(.active) { position: relative; }
  #mainNav .nav-link:not(.active)::after {
    content: ""; position: absolute; left: .875rem; right: .875rem; bottom: .2rem; height: 2px;
    border-radius: 2px; background: var(--sage-500);
    transform: scaleX(0); transform-origin: left; transition: transform .3s var(--mo-ease-out);
  }
  #mainNav .nav-link:not(.active):hover::after { transform: scaleX(1); }
  #mainNav .nav-phone:hover .bi { display: inline-block; animation: mo-wiggle .5s ease; }

  html.mo-js .mo-arrow:hover::after { transform: translateX(4px); }
  .mo-tile:hover img { transform: scale(1.07); }
  html.mo-js [data-anim="pop"]:hover .mo-pop { animation: mo-wiggle .5s ease; }

  /* Pricing cards: a soft glow follows the pointer (--mo-x/--mo-y from motion.js). */
  .pricing-card { position: relative; overflow: hidden; }
  .pricing-card > * { position: relative; z-index: 1; }
  .pricing-card::before {
    content: ""; position: absolute; inset: 0; z-index: 0; border-radius: inherit; pointer-events: none;
    background: radial-gradient(260px circle at var(--mo-x, 50%) var(--mo-y, 50%), rgb(var(--sage-overlay-light) / .13), transparent 65%);
    opacity: 0; transition: opacity .3s;
  }
  .pricing-card:hover::before { opacity: 1; }

  #site-footer .social-icon-link { display: inline-block; transition: transform .2s var(--mo-ease-spring); }
  #site-footer .social-icon-link:hover { transform: translateY(-3px); }
  .mo-top.mo-shown:hover { transform: translateY(-3px); }
}


/* ── Reduced motion (the device setting) and framed pages ───────────────────
   The gate mirrors both onto html.mo-still. Every waiting state above is
   gated on :not(.mo-still), so nothing is ever hidden; this block makes the
   rest (Ken Burns, the viewer, the header, Bootstrap's own fades) instant. */
html.mo-still *, html.mo-still *::before, html.mo-still *::after {
  animation-duration: .01ms !important; animation-delay: 0s !important; animation-iteration-count: 1 !important;
  transition-duration: .01ms !important; transition-delay: 0s !important;
  scroll-behavior: auto !important;
}
html.mo-still #aboutCarousel img { animation: none !important; }

@media print {
  html.mo-js [data-reveal], html.mo-js .mo-pop, html.mo-js .mo-count,
  html.mo-js .feature-icon, html.mo-js .step-circle { opacity: 1 !important; transform: none !important; }
  html.mo-js .mo-draw path { stroke-dashoffset: 0 !important; opacity: 1 !important; }
  .mo-top, .mo-lb { display: none !important; }
}
