/* ── Trip Showcase — homepage carousel ─────────────────────────────────────
   Sits between the hero and the About section (index.html). The outer
   #tripShowcase section stays inside the normal page padding (so its
   "TRIP MOMENTS" eyebrow pill lines up with "WHO WE ARE" below it); the
   inner .ts-viewport is further inset by its own 2rem side margins with
   rounded corners, rather than breaking full-bleed like .hero-section. */

#tripShowcase {
  padding: 2rem 0 2.5rem;
  /* Single source of truth for the caption gradient, referenced by both
     .ts-caption and (since a CSS var, unlike `background: inherit`, keeps
     resolving through any number of intervening elements) the nested
     .ts-marquee-item spans that need their own copy of it — see the
     comment on that rule below for why they can't just inherit it. */
  --ts-caption-gradient: linear-gradient(to right, #22d3ee, #60a5fa, #c084fc);
}

.ts-eyebrow-wrap {
  text-align: center;
  margin-bottom: 1.25rem;
}
/* Reuses .ts-caption's exact font-size/weight/gradient (same as the slide
   captions, e.g. "Weekend getaways made easy") — only the box's margin/
   max-width are adjusted so it centers here instead of sitting left-aligned
   like it does inside a slide's scrim. */
.ts-eyebrow-wrap .ts-caption {
  margin: 0 auto;
  max-width: 44rem;
}

.ts-viewport {
  position: relative;
  overflow: hidden;
  margin: 0 1rem;
  border-radius: 1.25rem;
  min-height: 60vh;
  background-color: #0f172a;
}
@media (min-width: 640px) {
  .ts-viewport { min-height: 75vh; }
}
.ts-viewport:focus-visible {
  outline: 2px solid #22d3ee;
  outline-offset: -2px;
}

/* ── Loading skeleton — shown from first paint (no layout shift) while the
   slide data (which embeds several MB of base64 photos) is still being
   fetched; swapped out once trip-showcase.js has real slides to render. ── */
.ts-skeleton {
  position: absolute;
  inset: 0;
  background: linear-gradient(120deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
  background-size: 200% 100%;
  animation: ts-skeleton-shimmer 1.6s ease-in-out infinite;
}
.ts-viewport:not(.ts-loading) .ts-skeleton {
  display: none;
}
.ts-viewport.ts-loading .ts-arrow,
.ts-viewport.ts-loading .ts-controls {
  display: none;
}
@keyframes ts-skeleton-shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .ts-skeleton { animation: none; }
}

/* ── Slides: stacked, crossfading (not sliding) ───────────────────────── */
.ts-slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 900ms ease;
  pointer-events: none;
}
.ts-slide.ts-active {
  opacity: 1;
  pointer-events: auto;
  z-index: 1;
}
.ts-slide img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  transform: scale(1);
  transform-origin: center;
  will-change: transform;
}
.ts-slide.ts-active img.ts-kenburns {
  animation: ts-ken-burns var(--ts-duration, 6000ms) ease-out forwards;
}
@keyframes ts-ken-burns {
  from { transform: scale(1) translate(0, 0); }
  to   { transform: scale(1.08) translate(-1.5%, -1.5%); }
}

.ts-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(2, 6, 23, .92) 0%, rgba(2, 6, 23, .55) 42%, transparent 78%);
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: flex-start;
  /* Bottom padding clears .ts-controls (position:absolute, bottom:1.25rem)
     further down this file. 6.25rem (reported as leaving a large empty
     gap between the CTA button and the dots on real phones — measured at
     ~59px clearance, well past the ~20px this value was originally
     tuned for) was pulled back to 3.75rem, which measures ~18px
     clearance — enough that a slightly taller CTA render (font metrics,
     line-height rounding) can't tip it into actually overlapping the
     dots/progress bar, without the button reading as stranded far above
     them. */
  padding: 1.5rem 1.25rem 3.75rem;
}
@media (min-width: 640px) {
  /* Requested tighter than the original 5.5rem (felt too floaty/spaced
     from the dots below) — 1.5rem was tested first but measured a real
     17px overlap with .ts-controls at every width from 640px up to
     1800px+ (verified: the CTA button's own bottom edge extended past
     the dots row's top edge). 3.5rem is the tightest value that still
     keeps a safe positive gap (~15px) at the narrowest end of this
     query's range (640-768px, the highest-risk width, same reasoning as
     the mobile-only rule above) — noticeably closer than 5.5rem without
     reintroducing the overlap. */
  .ts-scrim { padding: 2.5rem 2.5rem 3.5rem; }
}

.ts-caption {
  font-size: 1.375rem;
  font-weight: 900;
  line-height: 1.25;
  background: var(--ts-caption-gradient);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  max-width: 38rem;
  margin: 0 0 1rem;
  text-shadow: 0 2px 24px rgba(0, 0, 0, 0.25); /* keeps the gradient text legible over bright photo areas */
}
@media (min-width: 640px) {
  .ts-caption { font-size: 2.25rem; }
}

/* ── Per-slide caption/eyebrow (inside .ts-scrim) ─────────────────────────
   Smaller, lighter than the section heading above so the carousel photo
   stays the focus. The eyebrow <p> is always emitted by trip-showcase.js
   (an nbsp placeholder when a slide has none) purely to reserve its line's
   height — same reason the caption box below gets a fixed `height` instead
   of just a max-height — so the CTA button beneath both sits at one
   constant vertical offset no matter which slide is active or how long its
   text is. Scoped under .ts-scrim so the section-level heading above the
   carousel (.ts-eyebrow-wrap .ts-caption, a different, unrelated use of
   this same class) keeps its original larger/bold/wrapping treatment
   untouched. */
.ts-scrim .ts-eyebrow {
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(226, 232, 240, 0.72);
  margin: 0 0 0.375rem;
}
@media (min-width: 640px) {
  .ts-scrim .ts-eyebrow { font-size: 0.75rem; }
}

.ts-scrim .ts-caption {
  font-size: 0.9rem;
  font-weight: 600;
  font-style: italic;
  line-height: 1.4;
  margin: 0 0 0.75rem;
  white-space: normal;
}
@media (min-width: 640px) {
  .ts-scrim .ts-caption { font-size: 1.5rem; }
}

/* ── Ticker (default, motion-allowed) ──────────────────────────────────────
   trip-showcase.js only emits the .ts-marquee markup — including the
   duplicated .ts-marquee-item pair the seamless loop depends on — when
   prefers-reduced-motion is off, so this whole block is simply absent from
   the DOM for reduced-motion visitors; they get the plain static rule
   below instead. A fixed `height` (not max-height) is what makes the
   strip's height identical across every slide regardless of caption
   length, which is what keeps the CTA button below it at one constant
   vertical position — a variable-height wrapped caption pushing the button
   around was clipping it on short mobile viewports. */
.ts-scrim .ts-caption.ts-marquee {
  display: block;
  overflow: hidden;
  white-space: nowrap;
  width: 100%;
  /* The generic .ts-caption rule above (shared with the section heading —
     see the comment on .ts-scrim .ts-eyebrow above) caps max-width at
     38rem/608px. That's fine for the heading's own wrapped, centered text,
     but this per-slide ticker never redeclared max-width to override it,
     so on any viewport wider than ~608px (every laptop/desktop size) the
     ticker's own visible window was stuck at 608px regardless of how much
     wider .ts-scrim actually had available — reported as the caption only
     covering roughly the left half of the photo instead of its full
     width. max-width:100% here lets width:100% above actually take over. */
  max-width: 100%;
  min-width: 0;
  height: 1.4em;
}
.ts-scrim .ts-caption .ts-marquee-track {
  display: inline-flex;
  white-space: nowrap;
  will-change: transform;
  animation-duration: 14s; /* fallback only — trip-showcase.js sets each slide's real duration inline so every caption scrolls at the same ~40-60px/s regardless of text length */
}
/* animation-name only applies while the slide is active — mirrors Ken
   Burns just above. Applying it unconditionally to every slide's track
   (the original version) meant EVERY slide's ticker started scrolling at
   page load and kept running in the background the whole time it sat
   hidden behind opacity:0 — several mobile browsers throttle or fully
   pause CSS animations on off-screen/non-visible content to save battery,
   so by the time a slide was later activated its animation clock could
   be stuck wherever it got throttled, rendering as a caption that never
   moves at all instead of a slow scroll (reported repeatedly on real
   Android devices; never reproduced in desktop devtools mobile emulation,
   which doesn't throttle background animations the same way). Scoping the
   animation-name to only apply under .ts-active means an inactive slide's
   track has no animation at all (nothing to throttle), and activating a
   slide is a fresh application of the animation — not a resume — so it
   always starts cleanly from 0%. */
.ts-slide.ts-active .ts-caption .ts-marquee-track {
  animation-name: ts-marquee-scroll;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
/* Text is duplicated end-to-end in the markup (two .ts-marquee-item
   copies); translating exactly one item-width (-50% of the two-item track)
   lands the second copy exactly where the first started, so the loop
   restart is imperceptible instead of the visible snap a single-copy
   marquee has. */
.ts-scrim .ts-caption .ts-marquee-item {
  display: inline-block;
  white-space: nowrap;
  padding-right: 3rem;
  /* Being a flex item (parent .ts-marquee-track is inline-flex) forces this
     span's own box — its gradient/clip-to-text on the ancestor .ts-caption
     (the shared rule with .ts-eyebrow-wrap's heading, further up this
     file) never reaches glyphs painted inside it; only the inherited
     `color: transparent` does, which by itself is just invisible text. Has
     to be `var(--ts-caption-gradient)`, not `background: inherit` — inherit
     only pulls from this element's immediate parent (.ts-marquee-track,
     which has no background of its own), not the .ts-caption ancestor two
     levels up. */
  background: var(--ts-caption-gradient);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
@keyframes ts-marquee-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}
/* Paused via JS (trip-showcase.js) on pointer hover AND touch, with a
   short delay before resuming — plain CSS :hover can't cover touch devices
   and has no way to express a resume delay, which is why this is a class
   toggle instead of a :hover selector. */
.ts-scrim .ts-caption.ts-marquee.ts-marquee-paused .ts-marquee-track {
  animation-play-state: paused;
}

/* ── Static fallback (prefers-reduced-motion — the .ts-marquee class is
   never added, see trip-showcase.js) — wrapped, capped at 2 lines with
   ellipsis truncation instead of scrolling. -webkit-line-clamp has
   universal support in all current evergreen browsers despite the prefix. */
.ts-scrim .ts-caption:not(.ts-marquee) {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  text-overflow: ellipsis;
  max-height: calc(1.4em * 2);
  /* Same 38rem cap from the generic .ts-caption rule as the ticker above —
     this static (reduced-motion) fallback wraps at whatever width it's
     given, so left uncapped it wrapped to a narrow column near the left
     edge on wide screens instead of using the photo's full width. */
  max-width: 100%;
  width: 100%;
}

/* Inactive slide's eyebrow/caption default to invisible, fading in/out on
   their own fast 150ms transition — independent of .ts-slide's own much
   slower 900ms photo crossfade. Without this, an outgoing slide's caption
   had no opacity rule of its own at all, so it just sat at its normal
   opacity:1 for the entire 900ms the photo took to fade out underneath
   it — during that whole window, the outgoing caption (static/frozen,
   since deactivating a slide also stops its ticker's animation — see the
   .ts-active-scoped animation-name rule above) was fully visible layered
   on top of the incoming slide's caption, which was simultaneously fading
   in AND already scrolling. Reported as "static text with scrolling text
   on top of it" — both were real, just from two different slides
   overlapping far longer than intended. 150ms clears the outgoing
   caption well before the photo crossfade is even a third done. */
.ts-scrim .ts-eyebrow,
.ts-scrim .ts-caption {
  opacity: 0;
  transition: opacity 150ms ease;
}
.ts-slide.ts-active .ts-scrim .ts-eyebrow,
.ts-slide.ts-active .ts-scrim .ts-caption {
  opacity: 1;
}

/* ── Eyebrow entrance — replayed on every slide activation by
   trip-showcase.js's _goTo() (remove/reflow/re-add, same pattern as Ken
   Burns above). Skipped entirely under prefers-reduced-motion. Layers on
   top of the plain opacity transition above (same property) — an active
   CSS animation takes priority over a transition for the duration it
   runs, so this still drives the entrance while active; once it finishes
   ('both' fill mode holds opacity:1) the plain transition above is what
   handles fading back out on deactivation.

   Deliberately only ever applied to .ts-eyebrow, never to .ts-caption —
   trip-showcase.js used to add this class to both. .ts-caption is the
   marquee ticker's own overflow:hidden clipping box, with a
   .ts-marquee-track descendant separately animating transform for the
   scroll; giving that same clipping ancestor a second, independent
   transform animation at once caused a genuine Chromium compositor bug
   (confirmed via isolated repro) where the clipped, scrolling caption
   painted as a garbled, doubled-looking smear for the animation's 600ms.
   The caption still fades in fine on its own via the plain opacity
   transition above — it just skips this extra translateY entrance
   polish. ─────────────────────────────────────────────────────────────── */
@keyframes ts-caption-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
.ts-caption-in {
  animation: ts-caption-in 600ms ease-out both;
}

.ts-cta {
  width: fit-content;
  max-width: 100%;
  /* The <button> variant (used for in-app "#..." links, see
     trip-showcase.js) is a real <button> element, so it silently
     inherited white-space:nowrap from the app-wide button reset
     (custom.css) — same bug class as the earlier .cd-faq-question fix.
     slide.ctaLabel is a free-text admin field with no length limit
     (admin.js's Trip Showcase editor), so a long label had nowhere to
     wrap and could force this pill wider than the hero slide on mobile.
     Explicit here (rather than relying on the <a> variant's own
     unaffected default) so both variants behave identically regardless
     of which element the CTA renders as. */
  white-space: normal;
}
.ts-cta:focus-visible {
  outline: 2px solid #22d3ee;
  outline-offset: 2px;
}

/* ── Prev/Next arrows — just the icon, no button chrome ────────────────────
   Vertically centered on the slide at sm+ (below), but bottom-anchored
   on phones (max-width: 639.98px block further down) — the slide's
   min-height (60vh mobile, 75vh sm+) is tall enough that on a narrow,
   tall phone viewport a true top:50% center lands well above the
   caption/CTA/dots, which are themselves anchored to the bottom via
   .ts-scrim's justify-content:flex-end, leaving the arrows floating
   alone in the middle of the photo (reported on a Samsung S23-size
   screen specifically — the taller/narrower the viewport, the bigger
   that gap). Kept centered at sm+ instead of also moving there: the
   CTA button and left arrow sit close enough at those wider widths
   (.ts-scrim's own left-aligned content) that bottom-anchoring the
   arrow there measurably overlapped it. */
.ts-arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 3;
  width: 2.75rem;
  height: 2.75rem;
  /* The same global `button, .btn` rule (custom.css) that stretched
     .ts-dot also affects this one, worse: its padding (0.75rem 1.5rem,
     0.875rem 1.75rem from 640px up) is 48-56px total — MORE than this
     button's own 44px width — and with box-sizing:border-box, padding
     that exceeds a box's specified width wins: the browser can't shrink
     content below 0, so it expands the rendered box to fit the padding
     instead of honoring the smaller width/height. Result was a
     44x48-56px oval instead of the intended 44x44 circle. Resetting both
     properties the global rule sets is what actually restores the plain
     round icon button this was designed as. */
  min-height: 0;
  padding: 0;
  border-radius: 9999px;
  background: transparent;
  border: none;
  color: #fff;
  font-size: 1.5rem;
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.65);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: transform 150ms ease, color 150ms ease;
}
.ts-arrow:hover {
  background: transparent;
  color: #22d3ee;
  transform: translateY(-50%) scale(1.15);
}
.ts-arrow:focus-visible {
  outline: 2px solid #22d3ee;
  outline-offset: 2px;
}
.ts-arrow-prev { left: 0.75rem; }
.ts-arrow-next { right: 0.75rem; }
@media (min-width: 640px) {
  .ts-arrow-prev { left: 1.5rem; }
  .ts-arrow-next { right: 1.5rem; }
}
/* Phones only — see the comment on .ts-arrow above. Matches
   .ts-controls' own bottom:1.25rem so the arrows read as part of the
   same bottom control row as the dots, instead of floating mid-photo. */
@media (max-width: 639.98px) {
  .ts-arrow {
    top: auto;
    bottom: 1.25rem;
    transform: none;
  }
  .ts-arrow:hover {
    transform: scale(1.15);
  }
}

/* ── Dots + progress bar ───────────────────────────────────────────────── */
.ts-controls {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 1.25rem;
  z-index: 3;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.625rem;
}
.ts-dots {
  display: flex;
  gap: 0.5rem;
}
.ts-dot {
  position: relative;
  width: 0.5rem;
  height: 0.5rem;
  /* custom.css has a global `button, .btn { min-height: 48px }` rule for
     normal CTA-sized buttons at 640px+ — .ts-dot is a <button> too (for
     tab-role a11y semantics), so it inherited that floor with nothing to
     override it: width stayed 8px but height got forced up to 48px,
     turning every pagination dot into a tall, disproportionate vertical
     bar instead of a small round dot. min-height:0 here is what actually
     lets the 0.5rem height above take effect. */
  min-height: 0;
  border-radius: 9999px;
  background: rgba(255, 255, 255, 0.35);
  border: none;
  cursor: pointer;
  padding: 0;
  transition: background 200ms ease, transform 200ms ease;
}
.ts-dot:hover { background: rgba(255, 255, 255, 0.6); }
.ts-dot.ts-dot-active {
  background: linear-gradient(to right, #22d3ee, #a855f7);
  transform: scale(1.35);
}
.ts-dot:focus-visible {
  outline: 2px solid #22d3ee;
  outline-offset: 2px;
}
/* The dot stays visually small (a compact pagination indicator, not a
   CTA) but gets an invisible tap-area expansion on touch devices — an
   8x8px target is too small to reliably hit with a finger even once the
   48px min-height bug above is fixed. Same (pointer: coarse) convention
   as the icon-button touch targets in responsive-primitives.css. Gap
   between dots widens a little in the same breakpoint so adjacent hit
   areas don't swallow each other. */
@media (pointer: coarse) {
  .ts-dots { gap: 0.875rem; }
  .ts-dot::before {
    content: '';
    position: absolute;
    inset: -10px;
  }
}

.ts-progress-track {
  width: 8rem;
  height: 3px;
  border-radius: 9999px;
  background: rgba(255, 255, 255, 0.2);
  overflow: hidden;
}
.ts-progress-fill {
  height: 100%;
  width: 0%;
  background: linear-gradient(to right, #22d3ee, #a855f7);
  border-radius: 9999px;
}
.ts-progress-fill.ts-progress-animating {
  transition: width linear;
}

.ts-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ── Reduced motion: kill the pan/zoom + shorten the crossfade; autoplay is
   separately skipped in JS under the same media query, but arrows/dots/
   keyboard nav stay fully functional either way. ─────────────────────── */
@media (prefers-reduced-motion: reduce) {
  .ts-slide { transition: opacity 150ms linear; }
  .ts-slide.ts-active img.ts-kenburns { animation: none; transform: none; }
  .ts-caption-in { animation: none; }
}

/* ── Admin slide manager (Admin Panel → Branding) ─────────────────────── */
.ts-admin-card {
  cursor: default;
}
.ts-admin-card.ts-dragging {
  opacity: 0.4;
}
.ts-admin-card.ts-drop-target {
  border-color: #22d3ee !important;
}
.ts-admin-handle {
  cursor: grab;
}
.ts-admin-handle:active {
  cursor: grabbing;
}
