/* ===================================================================
   AdMart Technology — Motion System (Phase 1: Global Motion Foundation)
   Loaded site-wide from includes/header.php, after app.css so these
   tokens/utilities can reference and layer on top of the existing
   design system (var(--royal), var(--shadow-md), etc.) without
   duplicating any of it.

   Almost everything below is opt-in classes (.animate-on-scroll,
   .is-visible, .btn-loading, .skeleton, ...) that only affect an
   element once a later phase applies them to page markup. The one
   exception is the `scroll-behavior: smooth` rule directly below,
   which is itself the active Phase 2 deliverable - it changes
   same-page anchor-link/back-to-top scrolling site-wide immediately.
   =================================================================== */

/* Phase 2: Smooth Scrolling. Native CSS handles every same-document
   anchor jump (category nav, "jump to reviews" links, the back-to-top
   button below) with no JS required, and — critically — this never
   touches real page-to-page navigation or browser history; it only
   changes how the browser animates the scroll position for an anchor
   that already exists on the current page. Overridden back to `auto`
   under prefers-reduced-motion (see the media query further down). */
html {
  scroll-behavior: smooth;
}

:root {
  /* Shared timing/easing tokens so every future animation (this file,
     scroll-animations.js, page-transitions.js, carousels.js) draws from
     the same small set of values instead of inventing new ones per
     component. Keeps the "smooth, subtle, premium" feel consistent. */
  --motion-fast: 200ms;
  --motion-base: 300ms;
  --motion-slow: 500ms;
  --motion-ease: cubic-bezier(0.22, 0.61, 0.36, 1); /* smooth "ease-out" - decelerates into place, no overshoot/bounce */
  --motion-ease-in-out: cubic-bezier(0.45, 0, 0.15, 1);
  --motion-distance: 18px; /* small reveal travel distance - see Phase 3 "small movement distances" */
}

/* -------------------------------------------------------------------
   Reduced motion (Phase 12 groundwork, applied now rather than left
   for later — every rule below already respects it from day one so no
   later pass has to retrofit accessibility onto animations that
   shipped without it).

   Effect: reveal/hover/scale transitions collapse to nearly-instant
   and any transform-based movement is neutralized, but opacity-based
   fades are LEFT ENABLED at a fast speed — full removal of the
   .is-visible fade would mean content that starts at opacity:0 could
   stay invisible if a browser oddity ever stops JS from adding the
   class, and a fast fade is not a "large transition" the way a
   400ms+ slide/scale is. Essential state changes (menus, alerts,
   drawers) stay immediate either way since they use separate,
   non-decorative transitions already gated at a fast duration.
   ------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .animate-on-scroll {
    transition-duration: 1ms !important;
    transform: none !important;
  }
  .mkt-card:hover .mkt-card__media img,
  .mkt-card:hover,
  .mkt-card:focus-within,
  .mkt-card:hover .mkt-card__price,
  .mkt-card__wishlist:hover,
  .mkt-card__wishlist:active,
  .mkt-card__btn:hover,
  .mkt-card__btn:active,
  .btn:active,
  .service-card:hover,
  .vendor-card:hover,
  .hover-lift:hover,
  .card:hover,
  .mkt-card:hover .mkt-card__quickview {
    transform: none !important;
  }
  .alert,
  .modal.fade .modal-dialog,
  .modal.show .modal-dialog {
    transition-duration: 1ms !important;
  }
  .modal.fade .modal-dialog,
  .modal.show .modal-dialog {
    transform: none !important;
  }
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
}

/* ---------------------------------------------------------------------
   Scroll-reveal utility classes (engine: assets/js/scroll-animations.js)

   Usage: add `animate-on-scroll` plus one variant class
   (`fade-up` | `fade-in` | `slide-left` | `slide-right` | `scale-in`)
   to any element. The JS observer adds `.is-visible` the first time it
   enters the viewport, then unobserves it (animate-once by default).

   No-JS / pre-hydration behavior: the base `.animate-on-scroll` rule
   only defines the *transition*, not a hidden starting state - the
   starting (invisible/offset) state is applied by
   `.js-animations-ready .animate-on-scroll`, a class scroll-animations.js
   adds to <html> once it has actually attached its observers. If
   JavaScript never runs, `.js-animations-ready` never appears and every
   element simply renders in its final, fully-visible position - this is
   what Phase 3's "keep content visible if JavaScript is disabled"
   requirement means in practice.
   --------------------------------------------------------------------- */
.animate-on-scroll {
  transition: opacity var(--motion-base) var(--motion-ease), transform var(--motion-base) var(--motion-ease);
}

/* Phase 13 (performance): `will-change` only while an element is still
   in its pre-reveal state, not as a permanent `.animate-on-scroll`
   property - a page can have dozens of these (the homepage alone has
   50+), and leaving every one perpetually promoted to its own
   compositor layer after it has already settled wastes GPU memory for
   no benefit once `.is-visible` lands. Scoping this to `:not(.is-visible)`
   means the browser stops reserving a layer for each element the
   moment it reveals, without needing a JS `transitionend` listener to
   clean it up. */
.js-animations-ready .animate-on-scroll:not(.is-visible) {
  will-change: opacity, transform;
}

.js-animations-ready .animate-on-scroll.fade-up {
  opacity: 0;
  transform: translateY(var(--motion-distance));
}
.js-animations-ready .animate-on-scroll.fade-in {
  opacity: 0;
}
.js-animations-ready .animate-on-scroll.slide-left {
  opacity: 0;
  transform: translateX(var(--motion-distance));
}
.js-animations-ready .animate-on-scroll.slide-right {
  opacity: 0;
  transform: translateX(calc(-1 * var(--motion-distance)));
}
.js-animations-ready .animate-on-scroll.scale-in {
  opacity: 0;
  transform: scale(0.96);
}

.animate-on-scroll.is-visible {
  opacity: 1;
  transform: none;
}

/* Ensure the JavaScript-visible state always beats the pre-reveal hidden
   state even when the element has a more specific variant selector. */
.js-animations-ready .animate-on-scroll.fade-up.is-visible,
.js-animations-ready .animate-on-scroll.fade-in.is-visible,
.js-animations-ready .animate-on-scroll.slide-left.is-visible,
.js-animations-ready .animate-on-scroll.slide-right.is-visible,
.js-animations-ready .animate-on-scroll.scale-in.is-visible {
  opacity: 1;
  transform: none;
}

/* Staggered reveal - pair with .animate-on-scroll on sibling cards in a
   grid so they settle in slightly after one another instead of all at
   once. Small, capped delays only (Phase 3: "do not delay important
   content excessively"). */
.animate-delay-1 { transition-delay: 60ms; }
.animate-delay-2 { transition-delay: 120ms; }
.animate-delay-3 { transition-delay: 180ms; }
.animate-delay-4 { transition-delay: 240ms; }
.animate-delay-5 { transition-delay: 300ms; }
.animate-delay-6 { transition-delay: 360ms; }

@media (prefers-reduced-motion: reduce) {
  .js-animations-ready .animate-on-scroll.fade-up,
  .js-animations-ready .animate-on-scroll.fade-in,
  .js-animations-ready .animate-on-scroll.slide-left,
  .js-animations-ready .animate-on-scroll.slide-right,
  .js-animations-ready .animate-on-scroll.scale-in {
    opacity: 1;
    transform: none;
    transition-delay: 0ms !important;
  }
}

/* ---------------------------------------------------------------------
   Generic hover-lift (Phase 4 groundwork) - an opt-in alternative to
   hand-writing `transform: translateY(...)` per component. `.card` and
   `.mkt-card` already have their own hover treatment in app.css; this
   class is for any *other* card-like element that wants the same
   restrained lift without a bespoke rule.
   --------------------------------------------------------------------- */
.hover-lift {
  transition: transform var(--motion-fast) var(--motion-ease), box-shadow var(--motion-fast) var(--motion-ease);
}
.hover-lift:hover {
  transform: translateY(-6px);
  box-shadow: var(--shadow-md);
}

/* ---------------------------------------------------------------------
   Phase 4: Product & Service Card Motion.

   `.mkt-card` (includes/marketplace-cards.php) is the ONE shared
   component behind both product and service cards - service cards are
   rendered by admart_render_service_card() using the exact same
   `.card mkt-card` markup and `.mkt-card__*` child classes as
   admart_render_product_card(), so every rule below applies to both
   without duplication.

   app.css already handles: the base `.card:hover` lift (kept, just
   strengthened below for the `.mkt-card` case specifically), the image
   zoom (`.mkt-card__media img`, already scale(1.06) - within the
   250-350ms/1.03-1.06 spec, untouched), and the Quick View fade-in
   (already opacity+translateY on a 150ms transition, untouched). What
   was missing - and is added here - is: a stronger/more premium card
   lift, and *any* transition at all on the wishlist button, action
   buttons, price, and title, all of which previously changed state
   instantly (no `transition` property existed on them in app.css).
   --------------------------------------------------------------------- */

/* Card lift: overrides the plain `.card:hover` (translateY(-2px),
   shadow-md) that `.mkt-card` would otherwise inherit, with a slightly
   more pronounced lift + shadow, per the "-4px to -8px" spec. Defining
   the transition on the base (non-hover) rule too, not just `:hover`,
   so the return-to-rest motion on mouse-out is equally smooth instead
   of snapping back via app.css's faster 150ms `.card` transition. */
.mkt-card {
  transition: transform var(--motion-base) var(--motion-ease), box-shadow var(--motion-base) var(--motion-ease);
}
.mkt-card:hover,
.mkt-card:focus-within {
  transform: translateY(-6px);
  box-shadow: var(--shadow-lg);
}

/* Wishlist / favorite icon response - a light scale on hover and a
   press-down on click/tap, layered on top of app.css's existing
   background/color hover swap (now actually smooth, since neither had
   a `transition` before this). */
.mkt-card__wishlist {
  transition: background var(--motion-fast) ease, color var(--motion-fast) ease, transform 150ms var(--motion-ease);
}
.mkt-card__wishlist:hover {
  transform: scale(1.1);
}
.mkt-card__wishlist:active {
  transform: scale(0.9);
}

/* Quick View is only ever visible on hover (opacity:0 otherwise per
   app.css) - a keyboard user who Tabs to it needs the same reveal a
   mouse hover gives, or they'd be focused on an invisible button. */
.mkt-card__quickview:focus-visible {
  opacity: 1;
  transform: translateY(0);
}

/* Add to Cart / View / Get Quote button response. */
.mkt-card__btn {
  transition: background var(--motion-fast) ease, transform 150ms var(--motion-ease), box-shadow 150ms ease;
}
.mkt-card__btn:hover {
  transform: translateY(-1px);
  box-shadow: 0 4px 10px rgba(21, 101, 192, 0.25);
}
.mkt-card__btn:active {
  transform: translateY(0) scale(0.97);
}
.mkt-card__btn--outline:hover {
  box-shadow: none; /* outline variant stays flat - only the filled button gets the colored glow */
}

/* Smooth price/badge/title appearance - mostly matters once these
   values start updating dynamically (Phase 9's AJAX filtering), but
   also gives the price a very subtle "comes to attention" scale when
   its card is hovered, tying the whole card together. */
.mkt-card__price {
  transition: color var(--motion-fast) ease, transform var(--motion-fast) var(--motion-ease);
  display: inline-block;
}
.mkt-card:hover .mkt-card__price {
  transform: scale(1.03);
}
.mkt-card__badge {
  transition: transform var(--motion-fast) var(--motion-ease);
}
.mkt-card__title {
  transition: color var(--motion-fast) ease;
}
/* Reduced-motion overrides for all of the above live in the single
   consolidated `@media (prefers-reduced-motion: reduce)` block near the
   top of this file, not repeated here. */

/* ---------------------------------------------------------------------
   Phase 5: Button & Link Interactions.

   app.css's `.btn` already had hover states for `.btn-primary`/
   `.btn-outline` and a generic `transition: all .15s ease`. Pagination
   and every "View All" link already reuse `.btn`/`.btn-outline`
   directly (marketplace/index.php, includes/marketplace-cards.php), so
   everything below applies to them automatically with no separate
   work. What was missing - added here - is :active press feedback, a
   real :disabled treatment (previously just whatever the browser
   default happened to be), and the loading-state wiring (JS in
   assets/js/page-transitions.js toggles these classes on form submit).
   --------------------------------------------------------------------- */
.btn {
  transition: background var(--motion-fast) ease, border-color var(--motion-fast) ease, color var(--motion-fast) ease, box-shadow var(--motion-fast) ease, transform 150ms var(--motion-ease);
}
.btn:active:not(:disabled):not(.btn-loading) {
  transform: scale(0.97);
}
.btn:disabled,
.btn[disabled] {
  opacity: 0.55;
  cursor: not-allowed;
  pointer-events: none;
}

/* Filter chips / view toggle - same "instant before, smooth now" gap
   as the card buttons in Phase 4: neither had its own `transition`. */
.view-toggle__btn,
.location-scope-filters__chip {
  transition: background var(--motion-fast) ease, color var(--motion-fast) ease, border-color var(--motion-fast) ease;
}

/* Loading state. Default (no `data-loading-text`): the button's own
   label is hidden via `color:transparent` (not `display:none`, so its
   layout box - and the button's width - never changes) and a centered
   spinner overlays it. Used automatically for every form submit button
   site-wide (see page-transitions.js) unless it opts out via
   `data-no-loading`. */
.btn-loading {
  position: relative;
  color: transparent !important;
  pointer-events: none;
}
.btn-loading::after {
  content: '';
  position: absolute;
  width: 16px;
  height: 16px;
  top: 50%;
  left: 50%;
  margin: -8px 0 0 -8px;
  border: 2px solid rgba(255, 255, 255, 0.4);
  border-top-color: #fff;
  border-radius: 50%;
  animation: btn-spin 700ms linear infinite;
}
.btn-outline.btn-loading::after,
.btn-loading.btn-outline::after {
  border-color: rgba(21, 101, 192, 0.25);
  border-top-color: var(--royal);
}
@keyframes btn-spin {
  to { transform: rotate(360deg); }
}

/* Opt-in `data-loading-text` variant (Login/Register/Place Order -
   the highest-stakes actions, where "Logging in…" communicates more
   clearly than a bare spinner). page-transitions.js swaps the button's
   text to that attribute's value before adding `.btn-loading`, so
   here - unlike the default case - the label must stay VISIBLE, and
   the spinner moves beside it instead of centering over blank space.
   Only ever applied via JS to plain-text buttons with no icon, so
   there's no risk of this clobbering an SVG the way a blind
   `textContent` swap would on an icon+label button. */
.btn-loading[data-loading-text] {
  color: inherit !important;
  pointer-events: none;
}
.btn-loading[data-loading-text]::after {
  left: 20px;
  margin: -8px 0 0 0;
}

/* ---------------------------------------------------------------------
   Phase 6: Header, Menu & Mobile Drawer Motion.

   The mobile drawer's slide-in, backdrop fade, and background-scroll
   lock already existed and worked (app.css's `.mobile-drawer`/
   `.mobile-drawer-backdrop`/`body.drawer-open`) - only re-tokenized
   below (250ms plain `ease` -> the shared 300ms `--motion-ease`) for
   consistency with the rest of this file, not rebuilt. Two real gaps
   are filled: Bootstrap's dropdowns (`.dropdown-menu` - the desktop
   account menu AND the location selector, both use it) had no entrance
   animation at all, and the mobile hamburger icon never visually
   became an "X" when the drawer opened, despite already tracking that
   state via `aria-expanded` (app.js already toggles it - this is pure
   CSS keyed off the existing attribute, no JS changes needed).

   Not covered (documented, not an oversight): a "search suggestions"
   dropdown and an "accordion submenu" are listed in the brief, but
   neither exists as a UI element anywhere in this codebase to animate
   - the header search bar has no live-suggestions feature, and the
   mobile drawer's sections are flat lists, not collapsible accordions.
   Building either from scratch is a new interactive feature, not a
   motion pass on an existing one, so both are left for a future phase
   that's scoped to build the feature itself.
   --------------------------------------------------------------------- */

/* Bootstrap dropdown entrance (desktop account menu, location
   selector - both already use `.dropdown-menu`, so this is one rule
   for both, no per-component work). Deliberately an `animation` on the
   `.show` class rather than a `transition`: Bootstrap's own CSS toggles
   `display: none` -> `display: block` via that same class, and a
   `transition` cannot animate across a `display` change, but a CSS
   `animation` plays immediately once the element *is* rendered - so
   this only needs to hook the moment `.show` appears, with zero changes
   to Bootstrap's own show/hide logic (safer than overriding its
   `display` handling to force a transition to work). This animates the
   entrance only, not the close - closing already happens instantly via
   Bootstrap's own logic, unchanged. */
.dropdown-menu.show {
  animation: dropdown-in var(--motion-fast) var(--motion-ease) both;
}
@keyframes dropdown-in {
  from { opacity: 0; transform: translateY(-8px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Sticky header elevation - `.site-header--scrolled` is toggled by
   assets/js/page-transitions.js once the page has scrolled past the
   header's natural position, giving the now-floating header a subtle
   shadow it doesn't have at the very top of the page. */
.site-header__main {
  transition: box-shadow var(--motion-fast) ease;
}
.site-header--scrolled .site-header__main {
  box-shadow: var(--shadow-md);
}

/* Category browse strip - previously changed color/background
   instantly on hover (no `transition` defined in app.css). */
.site-header__category-link {
  transition: color var(--motion-fast) ease, background var(--motion-fast) ease, border-color var(--motion-fast) ease;
}

/* Hamburger -> X. Keyed entirely off `aria-expanded`, which app.js's
   openDrawer()/closeDrawer() already sets - no new JS needed. Values
   are tuned to this button's existing markup (3 spans, 2px tall, 5px
   vertical margin each, from app.css's `.nav-toggle span` rule). */
.nav-toggle span {
  transition: transform var(--motion-fast) var(--motion-ease), opacity var(--motion-fast) ease;
}
.nav-toggle[aria-expanded="true"] span:nth-child(1) {
  transform: translateY(7px) rotate(45deg);
}
.nav-toggle[aria-expanded="true"] span:nth-child(2) {
  opacity: 0;
}
.nav-toggle[aria-expanded="true"] span:nth-child(3) {
  transform: translateY(-7px) rotate(-45deg);
}

/* Re-tokenized mobile drawer / backdrop transitions - same behavior
   app.css already shipped, just drawing from the shared motion tokens
   instead of standalone hardcoded values, for consistency. */
.mobile-drawer {
  transition: transform var(--motion-base) var(--motion-ease);
}
.mobile-drawer-backdrop {
  transition: opacity var(--motion-base) ease;
}

@media (prefers-reduced-motion: reduce) {
  /* The hamburger's X-shape is a real state indicator, not decoration
     - it should still change shape under reduced motion, just without
     an animated transition getting there (unlike the transform:none
     overrides above, which correctly remove decorative movement
     entirely, removing this one would leave the icon stuck looking
     like "closed" while the drawer is actually open). */
  .nav-toggle span {
    transition-duration: 1ms;
  }
}

/* ---------------------------------------------------------------------
   Phase 7: Horizontal Carousel Enhancements.

   app.css's `.hz-carousel__viewport` already has `scroll-behavior:
   smooth`, `scroll-snap-type: x proximity`, and native touch scrolling
   for free. assets/js/carousels.js adds mouse-drag-to-scroll on top of
   it - these two rules give that drag its visual affordance (a "grab"
   cursor at rest, a "grabbing" cursor while active) and temporarily
   suspend scroll-snap during an active drag, so the snap points don't
   fight the pointer's direct 1:1 movement - snapping resumes the
   instant the drag ends and the class is removed, landing on the
   nearest card exactly as `scroll-snap-type` intends.
   --------------------------------------------------------------------- */
.hz-carousel__viewport {
  cursor: grab;
}
.hz-carousel__viewport--dragging {
  cursor: grabbing;
  scroll-snap-type: none;
  user-select: none;
}

/* ---------------------------------------------------------------------
   Phase 8: Page Loading & Content States.

   `.skeleton` (the base shimmering block) shipped in Phase 1. This
   phase adds the shaped variants Phase 8 names specifically -
   `.skeleton-card`/`.skeleton-vendor-card` (sized to match `.mkt-card`/
   `.vendor-card`'s real proportions, so a loading grid doesn't jump
   when real cards replace it) and `.skeleton-table-row` - plus a safe,
   JS-gated image fade-in, and a slim top-of-page loading bar for real
   navigations. None of the skeleton classes are wired to a live
   trigger yet (there is no AJAX-refreshed grid/table on the site
   today, that's Phase 9) - they're ready for Phase 9 to render into
   the DOM while a fetch is in flight, the same "build the reusable
   piece now, wire it up when the consumer exists" approach already
   used for scroll-animations.js in Phase 1.
   --------------------------------------------------------------------- */
.skeleton {
  position: relative;
  overflow: hidden;
  background: #EEF2F6;
  border-radius: 6px;
}
.skeleton::after {
  content: '';
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.65), transparent);
  animation: skeleton-shimmer 1.4s ease-in-out infinite;
}
@keyframes skeleton-shimmer {
  100% { transform: translateX(100%); }
}
@media (prefers-reduced-motion: reduce) {
  .skeleton::after { animation: none; display: none; }
}

/* Skeleton product/service card - same box shape as `.card.mkt-card`
   (app.css) so a grid of these placeholders holds the exact layout a
   grid of real cards will occupy a moment later, with no reflow. */
.skeleton-card {
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}
.skeleton-card__media {
  aspect-ratio: 4/3;
}
.skeleton-card__body {
  padding: 14px 16px 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.skeleton-card__line {
  height: 12px;
}
.skeleton-card__line--title {
  height: 14px;
  width: 85%;
}
.skeleton-card__line--price {
  height: 18px;
  width: 45%;
  margin-top: 4px;
}

/* Skeleton vendor card - matches `.vendor-card`'s centered logo +
   text layout (includes/marketplace-cards.php's admart_render_vendor_card()). */
.skeleton-vendor-card {
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 20px;
  text-align: center;
}
.skeleton-vendor-card__logo {
  width: 64px;
  height: 64px;
  border-radius: 14px;
  margin: 0 auto 14px;
}
.skeleton-vendor-card__line {
  height: 12px;
  width: 70%;
  margin: 0 auto 8px;
}

/* Skeleton table row - one <tr> of shimmering cells, for a dashboard/
   admin table's loading state. Column count/widths are set by
   whatever markup uses this (colspan, or one .skeleton per <td>). */
.skeleton-table-row td {
  padding: 10px 8px;
}
.skeleton-table-row .skeleton {
  height: 14px;
  width: 100%;
}

/* ---------------------------------------------------------------------
   Image loading fade-in. Safe by construction: `.img-fade-loading`
   (the opacity:0 starting state) is ONLY ever added by JS
   (assets/js/page-transitions.js), and only to an <img> JS has already
   confirmed is still loading (`!img.complete`) - if JavaScript never
   runs at all, this class never gets applied and every image simply
   renders normally at full opacity, exactly as before this phase. JS
   also guarantees removal on both `load` and `error`, so a broken
   image URL can never get stuck invisible.
   --------------------------------------------------------------------- */
.img-fade-loading {
  opacity: 0;
}
.img-fade-loaded {
  animation: img-fade-in var(--motion-base) var(--motion-ease);
}
@keyframes img-fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* ---------------------------------------------------------------------
   Slim top-of-page loading bar for real navigations (link clicks, form
   submits, back/forward) - `.page-loading` is toggled on <html> by
   assets/js/page-transitions.js via the `beforeunload`/`pageshow`
   events, not by intercepting individual link clicks, so there is no
   risk of this ever delaying or breaking a click (no preventDefault
   anywhere in that code path). Deliberately stops at 70%, never
   reaches 100% - the page unloads before that would matter, same
   pattern as GitHub/YouTube's own top-of-page loading bar.
   --------------------------------------------------------------------- */
#page-loader {
  position: fixed;
  top: 0;
  left: 0;
  height: 3px;
  width: 0;
  background: linear-gradient(90deg, var(--royal), var(--sky));
  z-index: 2000;
  transition: width 400ms ease;
  pointer-events: none;
}
.page-loading #page-loader {
  width: 70%;
}
@media (prefers-reduced-motion: reduce) {
  #page-loader {
    transition: none;
  }
}

/* ---------------------------------------------------------------------
   AJAX search/filter results (Phase 9). marketplace/search.php's
   #search-results is swapped in place by assets/js/search-ajax.js for
   filter/sort/pagination/view/scope changes instead of a full page
   navigation. `--loading` only ever gets added right before a fetch
   this same script just started, and is always removed in that fetch's
   own `finally` - so a script error can dim results but can never
   leave them stuck non-interactive, since pointer-events is scoped to
   that same class. `min-height` keeps the region from collapsing to
   near-0 during the swap, giving the centered spinner room and
   avoiding a layout jump.
   --------------------------------------------------------------------- */
.search-results {
  position: relative;
  min-height: 240px;
  transition: opacity var(--motion-base) var(--motion-ease);
}
.search-results--loading {
  opacity: 0.45;
  pointer-events: none;
}
.search-results__spinner {
  position: absolute;
  top: 96px;
  left: 50%;
  width: 34px;
  height: 34px;
  margin-left: -17px;
  border: 3px solid rgba(21, 101, 192, 0.15);
  border-top-color: var(--royal, #1565c0);
  border-radius: 50%;
  animation: btn-spin 700ms linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .search-results {
    transition: none;
  }
  .search-results__spinner {
    animation-duration: 1ms;
    animation-iteration-count: 1;
  }
}

/* ---------------------------------------------------------------------
   Flash message alerts (Phase 10). Markup: includes/alerts.php (always
   plain server-rendered HTML, never JS-inserted), close behavior:
   assets/js/app.js. Entrance is a pure CSS `animation` - zero JS
   dependency, so a flash message is never invisible if a script fails
   to load. Exit needs JS (`.alert--dismissing`, added on close-button
   click) since a removal can't be triggered by CSS alone; see app.js's
   `transitionend` + timeout-fallback pairing for why this can never
   leave a dismissed alert stuck half-visible.
   --------------------------------------------------------------------- */
.alert {
  animation: alert-in var(--motion-base) var(--motion-ease) both;
  transition: opacity var(--motion-fast) var(--motion-ease), transform var(--motion-fast) var(--motion-ease), max-height var(--motion-base) var(--motion-ease), margin var(--motion-base) var(--motion-ease), padding var(--motion-base) var(--motion-ease);
  overflow: hidden;
  max-height: 200px;
}
@keyframes alert-in {
  from { opacity: 0; transform: translateY(-8px); }
  to { opacity: 1; transform: translateY(0); }
}
.alert--dismissing {
  opacity: 0;
  transform: translateY(-6px);
  max-height: 0;
  margin: 0;
  padding-top: 0;
  padding-bottom: 0;
}

/* ---------------------------------------------------------------------
   Bootstrap modal polish (Phase 10). Applies to every `.modal.fade`
   site-wide (Quick View on the homepage/search/product/service pages
   today, and any future one) via Bootstrap's own selectors - one
   override, not a per-modal change. This only replaces the
   *transition/transform values* Bootstrap's default modal CSS already
   uses; the `.show` class toggle, backdrop creation/removal, and focus
   trapping are entirely Bootstrap's own bundled JS, untouched here -
   same reasoning as the Phase 6 dropdown, but here `transition` is
   safe (unlike a raw display:none swap) because Bootstrap's modal JS
   already sets `display:block` and forces a reflow one frame before
   adding `.show`, which is precisely what makes its own default
   transition work today.
   --------------------------------------------------------------------- */
.modal-backdrop {
  transition: opacity var(--motion-base) var(--motion-ease);
}
.modal.fade .modal-dialog {
  transition: transform var(--motion-base) var(--motion-ease), opacity var(--motion-base) var(--motion-ease);
  transform: scale(0.95) translateY(-12px);
  opacity: 0;
}
.modal.show .modal-dialog {
  transform: scale(1) translateY(0);
  opacity: 1;
}

/* ---------------------------------------------------------------------
   Mobile filters drawer (Phase 10). marketplace/search.php's sidebar
   filters collapse behind a checkbox toggle below 900px (app.css) with
   no JS at all - `:checked ~` is a pure CSS show/hide. A `transition`
   can't animate across that display:none -> block flip, so (same
   technique as the Phase 6 dropdown) this uses a one-shot `animation`
   instead, which plays immediately once the sibling selector makes the
   element part of layout. Requires no change to the existing
   checkbox/label markup or any JS.
   --------------------------------------------------------------------- */
@media (max-width: 900px) {
  .search-filters-toggle-checkbox:checked ~ .search-filters {
    animation: filter-drawer-in var(--motion-base) var(--motion-ease) both;
  }
}
@keyframes filter-drawer-in {
  from { opacity: 0; transform: translateY(-10px); }
  to { opacity: 1; transform: translateY(0); }
}

/* ---------------------------------------------------------------------
   Dashboard chrome (Phase 11). Scoped to `.dashboard-layout`, the one
   wrapper class every customer/vendor-portal/admin/messages/affiliate
   page already shares (includes/sidebar.php's container) - one edit
   here reaches every dashboard table and sidebar nav link site-wide
   instead of touching each page individually. app.css's `.card`
   (stat cards, panels) already had its own hover lift before this
   phase started; nothing to add there.
   --------------------------------------------------------------------- */
.dashboard-sidebar__link {
  transition: background var(--motion-fast) var(--motion-ease), color var(--motion-fast) var(--motion-ease);
}

/* Dashboard tables ship with fully inline per-cell styles and no
   shared row/table class (see e.g. customer/orders.php,
   admin/dashboard.php's Recent Activity) - rather than retrofit a
   class onto every table cell across dozens of pages, this targets
   plain <table><tbody><tr> wherever one appears inside a dashboard,
   which is exactly the set of tables this phase is about. The badge
   micro-lift only fires together with its own row's hover, tying the
   "status badges" and "tables" asks in the original spec into one
   real, interaction-driven piece of motion rather than two unused
   decorative rules. */
.dashboard-layout table tbody tr {
  transition: background var(--motion-fast) var(--motion-ease);
}
.dashboard-layout table tbody tr:hover {
  background: rgba(21, 101, 192, 0.035);
}
.badge {
  transition: transform var(--motion-fast) var(--motion-ease);
}
.dashboard-layout table tbody tr:hover .badge {
  transform: scale(1.04);
}
@media (prefers-reduced-motion: reduce) {
  .dashboard-sidebar__link,
  .dashboard-layout table tbody tr,
  .badge {
    transition-duration: 1ms;
  }
  .dashboard-layout table tbody tr:hover .badge {
    transform: none;
  }
}

/* ---------------------------------------------------------------------
   Back to Top button (Phase 2). Markup lives in
   includes/back-to-top.php (required from includes/footer.php);
   visibility/scroll behavior in assets/js/page-transitions.js.
   --------------------------------------------------------------------- */
.back-to-top {
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: 1040; /* below the AI support widget (1050) so the two never overlap-fight when both are visible */
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--white);
  color: var(--navy);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: var(--shadow-md);
  opacity: 0;
  visibility: hidden;
  transform: translateY(8px);
  transition: opacity var(--motion-fast) var(--motion-ease), transform var(--motion-fast) var(--motion-ease), visibility 0s linear var(--motion-fast), background var(--motion-fast) ease, color var(--motion-fast) ease;
}
.back-to-top.is-visible {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition: opacity var(--motion-fast) var(--motion-ease), transform var(--motion-fast) var(--motion-ease), background var(--motion-fast) ease, color var(--motion-fast) ease;
}
.back-to-top:hover {
  background: var(--royal);
  color: var(--white);
  border-color: var(--royal);
}

/* The AI support widget already claims the bottom-right corner at
   right:20px/bottom:20px (see app.css). When both it and Back to Top
   are visible at once, stack Back to Top above it rather than letting
   them overlap. */
.back-to-top--stacked {
  bottom: 84px;
}

@media (max-width: 480px) {
  .back-to-top { right: 14px; bottom: 14px; width: 40px; height: 40px; }
  .back-to-top--stacked { bottom: 72px; }
}

/* ---------------------------------------------------------------------
   Subtle initial-paint smoothing ("page content does not suddenly
   appear"). Deliberately a pure CSS @keyframes animation rather than a
   JS-driven class toggle: it runs the instant the stylesheet is
   parsed, with no dependency on JavaScript executing at all, so there
   is no flash-of-invisible-content failure mode to guard against - a
   no-JS visitor gets exactly the same fade a JS visitor does. The
   `both` fill-mode holds the 0%/from state until the animation starts
   and the 100%/to state after it ends, so main-content is never stuck
   showing an intermediate frame.

   Scoped to <main id="main-content"> (present on every page via
   includes/header.php/footer.php) rather than <body>, so the header/
   footer chrome around it is never part of the fade.
   --------------------------------------------------------------------- */
@keyframes page-fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
main#main-content {
  animation: page-fade-in var(--motion-fast) var(--motion-ease) both;
}
