/* ============================================
   Lettiva V2 Site Interactions — Shared CSS
   Companion styles for site-v2-interactions.js
   ============================================ */

/* --- Horizontal rails ---
   `contain`, deliberately, and ONLY on the rails.

   MDN: `contain` observes the element's own bounce and blocks scroll chaining;
   `none` blocks chaining AND suppresses the bounce. The bounce is an affordance
   on touch — it is how a thumb learns the row scrolls sideways — so `none` is
   the wrong value here and shipping it cost the rails their discoverability.

   There is deliberately NO html/body rule. MDN warns that these values disable
   native browser navigation, including horizontal swipe navigation (swipe to go
   back). Setting it at the document would trade a working back gesture for
   nothing: once every rail contains its own overscroll, no chaining reaches the
   page anyway. Audited 2026-08-14: no generated rail touches the left screen
   edge, so none of them sits in the back-gesture zone either.

   This covers the Tailwind utility before JS runs; initRails() catches block and
   bespoke rails, which declare overflow-x in their own CSS under names this
   stylesheet cannot know. */
.overflow-x-auto,
.overflow-x-scroll {
  overscroll-behavior-x: contain;
}

/* --- Nav items: never wrap mid-label ---
   Flex items in the nav <ul> shrink by default, so long labels like
   "Get in Touch" get squeezed below their natural width and break onto
   two lines. nowrap forces flex to keep each label whole and wrap the
   row instead, which is the right behaviour. */
nav li a {
  white-space: nowrap;
}

/* --- Active nav state ---
   `aria-current="page"` is injected on the matching anchor by V2SiteRenderer
   when it pre-processes navHtml. Scoped to nav <li> anchors so the logo
   link (no <li> wrapping) and the CTA button (also outside <li>) are not
   restyled. !important is used because Tailwind utilities for text colour
   (text-muted-foreground / hover:text-foreground) live in @layer utilities
   and at certain cascade points can outrank our un-layered rule on these
   anchors — we want the active marker to be unmistakable, so brute-force
   the colour and add a subtle underline. */
nav li a[aria-current="page"] {
  color: var(--color-accent, #009933) !important;
  font-weight: 600 !important;
  text-decoration: underline;
  text-decoration-color: var(--color-accent, #009933);
  text-decoration-thickness: 2px;
  text-underline-offset: 6px;
}

/* --- Base --- */
/* Prevent horizontal page scroll from overflowing buttons/content.
   overflow-x: clip (not hidden) avoids the fixed-positioning containment
   side-effect that overflow-x: hidden creates in some browsers. */
html, body {
  overflow-x: clip;
  max-width: 100%;
}

/* Override Tailwind preflight "img { height: auto }" — without this,
   images inside aspect-ratio containers don't fill vertically. */
figure img,
[style*="aspect-ratio"] img,
[class*="aspect-"] > img {
  height: 100% !important;
  object-fit: cover;
}

/* --- Scroll Reveal ---
   Only applied when .animations-ready is on <html> (added by JS).
   Without JS, all content is fully visible (progressive enhancement). */
.animations-ready [data-reveal] {
  opacity: 0;
  will-change: opacity, transform;
  transition: opacity 0.6s cubic-bezier(0.22, 1, 0.36, 1),
              transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}

.animations-ready [data-reveal="fade-up"] {
  transform: translateY(30px);
}

.animations-ready [data-reveal="fade-in"] {
  transform: none;
}

.animations-ready [data-reveal="slide-left"] {
  transform: translateX(-40px);
}

.animations-ready [data-reveal="slide-right"] {
  transform: translateX(40px);
}

.animations-ready [data-reveal="scale-up"] {
  transform: scale(0.95);
}

[data-reveal].revealed {
  opacity: 1;
  transform: none;
}

/* --- Stagger Reveal ---
   AI agents sometimes write [data-reveal-stagger] > * { opacity: 0 } to
   hide children for a staggered entrance. Our JS adds .revealed to each
   child when the parent enters the viewport. This rule provides a safety
   net so .revealed always wins, even if the agent forgot the reveal state. */
[data-reveal-stagger] > *.revealed {
  opacity: 1 !important;
  transform: none !important;
}

/* --- Lightbox --- */
.lightbox-overlay {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0);
  backdrop-filter: blur(0);
  opacity: 0;
  visibility: hidden;
  transition: background 0.35s ease, backdrop-filter 0.35s ease, opacity 0.35s ease, visibility 0.35s ease;
}

.lightbox-overlay.active {
  background: rgba(0, 0, 0, 0.92);
  backdrop-filter: blur(8px);
  opacity: 1;
  visibility: visible;
}

.lightbox-image-wrapper {
  position: relative;
  max-width: 90vw;
  max-height: 85vh;
  display: flex;
  align-items: center;
  justify-content: center;
  transform: scale(0.92);
  opacity: 0;
  transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.35s ease;
}

.lightbox-overlay.active .lightbox-image-wrapper {
  transform: scale(1);
  opacity: 1;
}

.lightbox-image-wrapper img {
  max-width: 90vw;
  max-height: 85vh;
  object-fit: contain;
  border-radius: 4px;
  user-select: none;
  -webkit-user-drag: none;
}

.lightbox-close {
  position: absolute;
  top: 16px;
  right: 16px;
  z-index: 10000;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 50%;
  color: #fff;
  font-size: 20px;
  cursor: pointer;
  transition: background 0.2s ease, transform 0.2s ease;
  line-height: 1;
}

.lightbox-close:hover {
  background: rgba(255, 255, 255, 0.2);
  transform: scale(1.1);
}

.lightbox-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 10000;
  width: 48px;
  height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 50%;
  color: #fff;
  font-size: 22px;
  cursor: pointer;
  transition: background 0.2s ease, transform 0.2s ease;
  line-height: 1;
}

.lightbox-nav:hover {
  background: rgba(255, 255, 255, 0.2);
  transform: translateY(-50%) scale(1.1);
}

.lightbox-nav--prev {
  left: 16px;
}

.lightbox-nav--next {
  right: 16px;
}

.lightbox-counter {
  position: absolute;
  bottom: 16px;
  left: 50%;
  transform: translateX(-50%);
  color: rgba(255, 255, 255, 0.7);
  font-size: 14px;
  font-family: inherit;
  user-select: none;
}

/* Cursor hint on lightbox-enabled images */
[data-lightbox] img,
img[data-lightbox] {
  cursor: zoom-in;
}

/* --- Accordion --- */
.accordion-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 1rem 0;
  background: none;
  border: none;
  border-bottom: 1px solid rgba(0, 0, 0, 0.08);
  font: inherit;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
  color: inherit;
  transition: color 0.2s ease;
}

.accordion-trigger:hover {
  opacity: 0.8;
}

.accordion-trigger::after {
  content: '+';
  flex-shrink: 0;
  margin-left: 1rem;
  font-size: 1.25em;
  font-weight: 300;
  transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}

.accordion-trigger[aria-expanded="true"]::after {
  content: '\2212'; /* minus sign */
  transform: rotate(180deg);
}

.accordion-content {
  overflow: hidden;
  max-height: 0;
  opacity: 0;
  transition: max-height 0.4s cubic-bezier(0.22, 1, 0.36, 1),
              opacity 0.3s ease,
              padding 0.3s ease;
  padding: 0;
}

.accordion-content.open {
  opacity: 1;
  padding-bottom: 1rem;
}

/* --- Carousel --- */
.carousel-wrapper {
  position: relative;
  overflow: hidden;
}

.carousel-track {
  position: relative;
  min-height: 100px;
}

.carousel-slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.5s ease, visibility 0.5s ease;
}

.carousel-slide.active {
  position: relative;
  opacity: 1;
  visibility: visible;
}

.carousel-dots {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin-top: 1.25rem;
  padding: 0;
  list-style: none;
}

.carousel-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: none;
  background: rgba(0, 0, 0, 0.15);
  cursor: pointer;
  padding: 0;
  transition: background 0.25s ease, transform 0.25s ease;
}

.carousel-dot:hover {
  background: rgba(0, 0, 0, 0.3);
}

.carousel-dot.active {
  background: rgba(0, 0, 0, 0.6);
  transform: scale(1.2);
}

/* --- Preview banner clearance ---
   The preview banner and the site nav are both `sticky top-0`, so they landed
   on the same line and the banner (z-100) simply covered the nav — the brand
   and half the links sat behind it on every unpublished site, which is exactly
   the state a customer is shown their demo in. The banner keeps a FIXED height
   (h-9, no wrapping) so this offset is exact at every width; both bars stay
   pinned and neither hides the other. Applies to already-generated sites
   without a rebuild, because the nav's `top` is overridden here rather than in
   its baked markup. */
:root {
  --preview-banner-h: 2.25rem;
}
/* !important is required, not lazy: each site's own generated CSS pins its nav
   through an id selector (#site-nav), which outranks any attribute-based rule
   this shared stylesheet can write. The declaration only shifts the pin down by
   the banner's height, so it cannot fight whatever else that CSS is doing. */
body:has([data-preview-banner]) [data-sticky-nav],
body:has([data-preview-banner]) #site-nav {
  top: var(--preview-banner-h) !important;
}

/* --- Sticky Nav --- */
[data-sticky-nav] {
  transition: background-color 0.35s cubic-bezier(0.4, 0, 0.2, 1),
              background 0.35s cubic-bezier(0.4, 0, 0.2, 1),
              box-shadow 0.35s cubic-bezier(0.4, 0, 0.2, 1),
              border-color 0.35s cubic-bezier(0.4, 0, 0.2, 1),
              backdrop-filter 0.35s cubic-bezier(0.4, 0, 0.2, 1),
              padding 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Nav background fallback: provides a sensible default for sites where the
   agent-generated CSS omits a nav background entirely. Wrapped in :where()
   so specificity is 0-0-0 — any site-specific #site-nav rule wins regardless
   of source order (this <link> stylesheet loads after inline <style> blocks).
   build-shared-layout.ts already sets background: var(--color-background)
   on new sites; this is a safety net for older agent-generated sites. */
:where(#site-nav),
:where([data-sticky-nav]) {
  background: var(--color-background);
}

[data-sticky-nav].nav-scrolled {
  background: color-mix(in srgb, var(--color-background) 80%, transparent) !important;
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  box-shadow: 0 1px 0 color-mix(in srgb, var(--color-primary) 6%, transparent);
}

/* --- Transparent overlay nav (2026-07-13 benchmark look) ---
   Applies ONLY when BOTH are true:
   1. The nav was generated with data-nav-overlay (build-time opt-in —
      definitions baked before this feature lack the attribute, so live
      sites are untouched by this stylesheet shipping), AND
   2. The current page opens with a full-bleed hero — V2SiteRenderer stamps
      data-nav-overlay-page on <main> when the page fragment's first section
      is data-bleed="full".
   The nav rides fixed and transparent over the hero (white ink), then the
   existing .nav-scrolled toggle (80px, initStickyNav) returns it to the
   solid treatment. Positioning is keyed WITHOUT :not(.nav-scrolled) so the
   bar stays fixed after the transition — swapping fixed→sticky mid-scroll
   would reflow the whole page. Colours are keyed WITH :not(.nav-scrolled)
   so the standard utilities resume the moment the bar goes solid. */
body:has(main[data-nav-overlay-page]) [data-sticky-nav][data-nav-overlay] {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
}
body:has(main[data-nav-overlay-page]) [data-sticky-nav][data-nav-overlay]:not(.nav-scrolled) {
  background: transparent !important;
  border-color: transparent;
  box-shadow: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
body:has(main[data-nav-overlay-page]) [data-sticky-nav][data-nav-overlay]:not(.nav-scrolled) a:not(.nav-cta),
body:has(main[data-nav-overlay-page]) [data-sticky-nav][data-nav-overlay]:not(.nav-scrolled) button {
  color: rgba(255, 255, 255, 0.86);
}
body:has(main[data-nav-overlay-page]) [data-sticky-nav][data-nav-overlay]:not(.nav-scrolled) a:not(.nav-cta):hover,
body:has(main[data-nav-overlay-page]) [data-sticky-nav][data-nav-overlay]:not(.nav-scrolled) button:hover {
  color: #fff;
}
body:has(main[data-nav-overlay-page]) [data-sticky-nav][data-nav-overlay]:not(.nav-scrolled) [data-editable="nav-logo-text"] {
  color: #fff;
}
/* The nav's hairline DIVIDER is a bare decorative span, so it needs a
   translucent white ground to stay visible against a photographic hero. The
   selector has to exclude the living wordmark, which is also an
   aria-hidden span: without the :not() it inherited the divider's ground and
   painted a grey slab behind the brand name on every overlay hero (found on
   the first prospect demo, 2026-08-14). Styling by a bare attribute selector
   catches any decorative span the nav ever gains — keep this list honest. */
body:has(main[data-nav-overlay-page]) [data-sticky-nav][data-nav-overlay]:not(.nav-scrolled) span[aria-hidden="true"]:not(.nav-wordmark-group):not(.nav-wordmark-group *) {
  background: rgba(255, 255, 255, 0.28) !important;
}
/* !important on color is required here (diagnosed 2026-07-14): worker/build-shared-layout.ts
   ships an unlayered "BUTTON TEXT-COLOUR SAFETY NET" —
   a[href][class*="bg-primary"] { color: var(--primary-foreground) !important; } — that force-sets
   white text on ANY anchor whose class list still contains bg-primary, regardless of what actually
   wins the background cascade. The nav-cta pill keeps its bg-primary Tailwind class even though this
   overlay rule visually overrides its background to white, so without !important here the safety net
   wins and paints white text on the white pill (confirmed via computed style: color and background
   both rgb(255,255,255) before this fix). --color-primary itself is NOT the problem — it correctly
   resolves to #1a2740 (verified against the baked theme CSS) — the token was never the bug.
   Scoped to :not(:hover) so the :hover rule below is untouched: on hover the safety net's
   !important still wins and happens to want the same white the hover rule wants, so hover was
   never broken and needs no change. */
body:has(main[data-nav-overlay-page]) [data-sticky-nav][data-nav-overlay]:not(.nav-scrolled) .nav-cta {
  background: #fff;
}
body:has(main[data-nav-overlay-page]) [data-sticky-nav][data-nav-overlay]:not(.nav-scrolled) .nav-cta:not(:hover) {
  color: var(--color-primary, #1a2740) !important;
}
body:has(main[data-nav-overlay-page]) [data-sticky-nav][data-nav-overlay]:not(.nav-scrolled) .nav-cta:hover {
  background: var(--color-accent, var(--color-primary, #1a2740));
  color: #fff;
}
body:has(main[data-nav-overlay-page]) [data-sticky-nav][data-nav-overlay]:not(.nav-scrolled) [data-mobile-menu-toggle] {
  border-color: rgba(255, 255, 255, 0.45);
}

/* --- Living wordmark (round 1 emitter port, 2026-07-16) ---
   generateNavHtml (build-shared-layout.ts) wraps the org-logo <img> with a
   crossfading serif wordmark + location tag, both inside the same brand
   anchor, whenever the org has a logo. Reuses the SAME overlay-state gate
   as the Transparent overlay nav rules directly above (body:has(main
   [data-nav-overlay-page]) [data-sticky-nav][data-nav-overlay]:not(.nav-
   scrolled)) rather than inventing a parallel one — a definition baked
   before nav-brand-slot existed carries neither class, so nothing below
   ever matches it and the plain logo renders exactly as before this
   change (the no-JS/print/reduced-motion floor needs nothing extra: this
   is a crossfade between two already-static marks, not new motion).
   .nav-brand-slot / .nav-wordmark-group / .nav-wordmark / .nav-wordmark-tag
   are declared unconditionally (not just under the overlay gate) so the
   crossfade geometry and easing hold in every nav state, including once
   .nav-scrolled has solidified the bar.
   aria-hidden lives on the markup (build-shared-layout.ts); pointer-events
   is set here so the real link (href, focus ring) stays on the single
   anchor and the wordmark is never a second hit-target.

   STAGGER, asymmetric by direction (measured cosmetic, lab: a plain
   symmetric crossfade briefly shows both marks at full opacity at once).
   CSS transitions take their duration/delay from the computed style of the
   STATE BEING TRANSITIONED INTO, not the one being left — so declaring the
   delay only on one side of the pair makes it direction-specific:
     • Overlay → solid (scrolling down past the hero, the first transition
       every visitor sees): the wordmark's transition-into-hidden has no
       delay (fades out immediately); the logo's transition-into-visible
       (the unconditional rule below) carries the 0.15s delay — "the logo
       waits" — so by the time it starts appearing the wordmark is already
       most of the way through fading out. No double-exposure.
     • Solid → overlay (scrolling back up): the wordmark's transition-into-
       visible has no delay (fades in immediately); the logo's transition-
       into-hidden (the state-scoped rule below) explicitly resets
       transition-delay to 0s, so it does not linger at full opacity
       waiting on a delay that only makes sense for the other direction —
       both simply crossfade over the same 0.4s window, ordinary overlap,
       not a double-exposure stall. */
.nav-brand-slot {
  position: relative;
  display: inline-flex;
  align-items: center;
}
.nav-brand-slot img {
  transition: opacity 0.4s ease 0.15s;
}
.nav-wordmark-group {
  position: absolute;
  left: 0;
  top: 50%;
  translate: 0 -50%;
  display: flex;
  flex-direction: column;
  gap: 3px;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.4s ease;
}
.nav-wordmark {
  font-family: var(--font-display), serif;
  font-weight: 300;
  font-size: 22px;
  line-height: 1;
  letter-spacing: -0.01em;
  color: #fff;
}
.nav-wordmark-tag {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.72);
}
/* Overlay state: wordmark in, logo out — the logo's fade-OUT is immediate
   (transition-delay reset to 0s; see the direction note above). */
body:has(main[data-nav-overlay-page]) [data-sticky-nav][data-nav-overlay]:not(.nav-scrolled) .nav-brand-slot img {
  opacity: 0;
  transition-delay: 0s;
}
body:has(main[data-nav-overlay-page]) [data-sticky-nav][data-nav-overlay]:not(.nav-scrolled) .nav-wordmark-group {
  opacity: 1;
}

/* --- Active Page Indicator --- */
/* Animated underline for nav links (safety net for pre-existing sites
   generated before build-shared-layout added these rules). */
.nav-links a {
  position: relative;
}

.nav-links a::after {
  content: '';
  position: absolute;
  bottom: -4px; left: 0;
  width: 0; height: 2px;
  background: var(--color-accent);
  transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.nav-links a:hover::after { width: 100%; }
.nav-links a[aria-current="page"] { color: var(--color-accent); }
.nav-links a[aria-current="page"]::after { width: 100%; }

.mobile-menu-links a[aria-current="page"] { color: var(--color-accent); }

/* --- Mobile Menu --- */

/* Ensure nav sits above hero / page content so the hamburger is always tappable */
[data-sticky-nav] {
  z-index: 100;
}

/* Prevent double-tap-to-zoom on interactive elements */
[data-mobile-menu-toggle],
[data-lightbox] img,
img[data-lightbox] {
  touch-action: manipulation;
}

.mobile-menu-open {
  overflow: hidden;
}

/* Hide mobile menu from document flow before JS adds .mobile-menu-panel.
   Without this, the menu links render in normal flow on initial paint and
   cause horizontal overflow (body.scrollWidth > viewport width).
   Once JS runs initMobileMenu(), .mobile-menu-panel is added and this
   rule no longer applies — the element becomes a fixed-positioned panel. */
[data-mobile-menu]:not(.mobile-menu-panel) {
  display: none;
}

[data-mobile-menu] {
  transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1),
              opacity 0.35s ease,
              visibility 0.35s ease;
}

/* Default: hidden off-screen right for mobile menu panels */
@media (max-width: 768px) {
  [data-mobile-menu].mobile-menu-panel {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: auto; /* Override inset: 0 from buildSharedLayout — anchor to right edge */
    width: min(320px, 85vw);
    z-index: 9990;
    transform: translateX(100%);
    opacity: 0;
    visibility: hidden;
    overflow-y: auto;
  }

  [data-mobile-menu].mobile-menu-panel.menu-open {
    transform: translateX(0);
    opacity: 1;
    visibility: visible;
  }
}

/* Agent CSS correction: agent sometimes generates
   [data-mobile-menu].mobile-menu-panel { opacity: 1 } as the open state.
   But mobile-menu-panel is a PERMANENT setup class added at DOMContentLoaded —
   using it as an open trigger makes the panel always-visible after JS init.
   The JS always uses .menu-open for the toggled open state.
   Force our rules to win over agent CSS with !important. */
@media (max-width: 768px) {
  [data-mobile-menu].mobile-menu-panel:not(.menu-open) {
    opacity: 0 !important;
    visibility: hidden !important;
    pointer-events: none !important;
    transform: translateX(100%) !important;
  }

  [data-mobile-menu].mobile-menu-panel.menu-open {
    opacity: 1 !important;
    visibility: visible !important;
    pointer-events: auto !important;
    transform: translateX(0) !important;
  }
}

/* Body overlay when mobile menu is open */
.mobile-menu-backdrop {
  position: fixed;
  inset: 0;
  z-index: 9989;
  background: rgba(0, 0, 0, 0);
  visibility: hidden;
  transition: background 0.35s ease, visibility 0.35s ease;
}

.mobile-menu-backdrop.active {
  background: rgba(0, 0, 0, 0.5);
  visibility: visible;
}

/* --- Mobile Menu Close Button --- */
/* Make the X button prominent and tappable (44px minimum touch target) */
.mobile-menu-close,
[data-mobile-menu] [data-mobile-menu-toggle][aria-label*="Close"] {
  min-width: 44px;
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.75rem;
  cursor: pointer;
  touch-action: manipulation;
}

/* --- Mobile Menu "More" Section --- */
/* Improve the visual treatment of the overflow section in mobile nav */
.mobile-menu-divider {
  height: 1px !important;
  background: rgba(0, 0, 0, 0.1) !important;
  margin: 1rem 0 0.5rem !important;
}

.mobile-menu-heading {
  font-size: 0.7rem !important;
  font-weight: 700 !important;
  text-transform: uppercase !important;
  letter-spacing: 0.12em !important;
  color: rgba(0, 0, 0, 0.35) !important;
  padding-bottom: 0.25rem !important;
}

/* Overflow links are slightly smaller than primary nav links */
.mobile-menu-heading ~ li a {
  font-size: 1.15rem !important;
  font-weight: 500 !important;
  color: rgba(0, 0, 0, 0.55) !important;
}

.mobile-menu-heading ~ li a:hover {
  color: var(--color-accent) !important;
}

/* --- Lightbox Mobile Improvements --- */
@media (max-width: 768px) {
  .lightbox-close {
    width: 48px;
    height: 48px;
    font-size: 24px;
    top: 12px;
    right: 12px;
    background: rgba(255, 255, 255, 0.25);
  }

  .lightbox-nav {
    width: 40px;
    height: 40px;
  }

  .lightbox-nav--prev { left: 8px; }
  .lightbox-nav--next { right: 8px; }
}

/* --- Back to Top --- */
[data-back-to-top] {
  position: fixed;
  bottom: 2rem;
  right: 2rem;
  z-index: 999;
  opacity: 0;
  visibility: hidden;
  transform: translateY(12px);
  transition: opacity 0.3s ease, visibility 0.3s ease, transform 0.3s ease;
}

[data-back-to-top].visible {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

[data-back-to-top]:hover {
  transform: translateY(-2px);
}

/* --- Empty Section Guards --- */
/* Hide sections whose React island returns null (no data in DB).
   The agent writes static headings + data-component placeholder; when the
   component has no data, it returns null — leaving an orphan section heading.
   CSS :has() checks whether the rendered component class is present in the DOM.
   Supported: Chrome 105+, Firefox 121+, Safari 15.4+

   IMPORTANT: The class names here MUST match what each React wrapper actually
   renders. If you change a wrapper's output class, update the guard here too.
   Validated by: public/site-v2-interactions.test.ts */
/* Hero layout fix: the hero is display:flex (row), so extra wrappers placed
   outside the main z-10 content div become flex siblings and steal width on mobile.
   Fix: force the hero to flex-wrap so on narrow screens the extra wrappers
   (trustpilot, discount) stack below the main content instead of beside it. */
[data-section="hero"] {
  flex-wrap: wrap;
}
[data-section="hero"] > div:not([class*="absolute"]) {
  position: relative;
  z-index: 10;
  width: 100%;
}

/* Empty section guards: hide component-only sections when the React component
   returns empty (data-component-empty). Only targets sections explicitly marked
   as component-only by the skeleton builder (data-component-section).
   Sections with mixed content (hero + optional discount-display) are never hidden.

   Macro-safe: the :not(:has(.mt-6 > *:not([data-component-empty]))) clause
   prevents collateral hiding of multi-component content-section-1 macros
   (e.g. "FAQ + Booking Policies + House Rules" bundled in one wrapper) when
   only some of the bundled components return empty. The macro hides only if
   EVERY .mt-6 component slot contains only an empty marker — i.e. there is no
   substantive content left. Single-component sections (where placeholders are
   direct children, no .mt-6 wrappers) still hide on the first empty marker
   because the negated :has() is vacuously true. */
/* The second :not() spares a wrapper whose OTHER component actually rendered.
   Macro-safety used to rest entirely on component slots being wrapped in
   .mt-6 — and when a wrapper has no .mt-6 at all, `:has(.mt-6 > ...)` is
   vacuously false, so the negation is vacuously true and the section hides on
   the first empty marker. That is fine for a genuinely single-component
   section, and wrong the moment a wrapper bundles two components without the
   .mt-6 shape: "bedroom-config + floor-plans" rendered a real bedroom, a real
   bed and three photographs, and hid all of it because floor-plans was empty
   (Greers Ferry, 2026-08-14).
   A rendered data-connected component emits its own [data-section]; an empty
   one emits only the marker. So "contains a rendered component section" is the
   structural test for substantive content that does not depend on the
   surrounding markup shape. */
section[data-component-section]:has([data-component-empty]):not(:has(.mt-6 > *:not([data-component-empty]))):not(:has([data-section])) { display: none; }

/* --- Booking-critical stat block omit --- */
/* An inline booking-critical stat (deposit %, security bond) that resolves EMPTY
   emits a hidden [data-stat-empty] marker (PropertyInlineStatWrapper.omitStatBlock).
   A blank sentence ("A % deposit secures your dates", "  bond is held for the stay")
   must never ship, but hiding the whole section (the data-component-empty rule above)
   is too coarse — deposit/bond sit mid-prose in the booking-policies section. So omit
   only the CONTAINING block: the paragraph / list item / definition, plus a paired <dt>
   label. Common when a source conflict (e.g. bond £500 vs £750) is routed to the PM and
   the value is intentionally still null at generation time. */
p:has([data-stat-empty]),
li:has([data-stat-empty]),
dd:has([data-stat-empty]),
dt:has(+ dd [data-stat-empty]) { display: none; }

/* --- Mobile Responsive Overrides --- */

/* Buttons: allow text to wrap on narrow screens.
   Generated .btn CSS always sets white-space:nowrap, which causes overflow
   when button text is long (e.g. "Celebrating Something Special?"). */
@media (max-width: 768px) {
  .btn, a.btn, button.btn {
    white-space: normal;
    max-width: 100%;
  }
}

/* Inline-style grids: collapse multi-column grids to 1 column on mobile.
   The agent sometimes uses inline style="grid-template-columns:repeat(N,1fr)"
   instead of the .grid-N helper classes, which bypasses class-based media queries.
   Covers 2-, 3- and 4-column variants. */
@media (max-width: 768px) {
  [style*="grid-template-columns:repeat(3"],
  [style*="grid-template-columns: repeat(3"],
  [style*="grid-template-columns:repeat(4"],
  [style*="grid-template-columns: repeat(4"] {
    grid-template-columns: 1fr !important;
  }
}

@media (max-width: 480px) {
  [style*="grid-template-columns:repeat(2"],
  [style*="grid-template-columns: repeat(2"] {
    grid-template-columns: 1fr !important;
  }
}

/* ============================================
   Trust Bar — component layout overrides
   ============================================ */
/* .sb-trust-badges has width:100% by default (correct when standalone).
   When placed as a flex item inside a trust-bar section alongside sibling
   text, that 100% width pushes the text to the next row. Override it to
   auto so the two flex children sit side by side.
   NOTE: trust-strip is a standalone badges-only section (gallery, booking pages)
   where badges should remain centered. Only trust-bar has the side-by-side layout. */
[data-section="trust-bar"] .sb-trust-badges,
[data-section="trust"] .sb-trust-badges {
  width: auto;
}

/* Align badges to the left within the trust-bar (badges sit alongside text).
   trust-strip is excluded — standalone badge sections should stay centered. */
[data-section="trust-bar"] .sb-trust-badges__list--horizontal,
[data-section="trust"] .sb-trust-badges__list--horizontal {
  justify-content: flex-start;
}

/* Legacy trust-bar text override removed — TrustBadgeStripWrapper now
   handles its own light/dark contrast via isDark prop + Preline tokens.
   The old rule force-applied dark text with !important, which made the
   new banner variant invisible on dark brand sections. */

/* ============================================
   Reduced Motion — respect user preferences
   ============================================ */
@media (prefers-reduced-motion: reduce) {
  .animations-ready [data-reveal] {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }

  [data-reveal-stagger] > * {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }

  .lightbox-overlay,
  .lightbox-image-wrapper,
  .accordion-content,
  .carousel-slide,
  [data-sticky-nav],
  [data-mobile-menu],
  [data-back-to-top],
  .lightbox-close,
  .lightbox-nav,
  .carousel-dot,
  .mobile-menu-backdrop {
    transition: none !important;
  }

  [data-parallax] {
    transform: none !important;
  }
}

/* ============================================
   Atmosphere Canvas — WebGL harness base styling
   ============================================ */
/* canvas[data-atmos] is created by a block/emitter that wants an ambient
   WebGL layer (mist / fireflies / caustics — see initAtmosphere() in
   site-v2-interactions.js, the shared makeAtmos() harness ported from the
   frontier-holly exemplar, 2026-07-17). This rule only POSITIONS the
   canvas; it never decides whether one exists.

   The canvas fills its containing block and is always click-through
   (pointer-events: none) — atmosphere is decorative, never interactive.

   STACKING CONTRACT — INJECTED PATH (worker/generation/post-process.ts's
   injectAtmosphereCanvases, the normal way a canvas[data-atmos] reaches
   the page): a bare positioning context on the section was NOT sufficient
   on its own. A position:absolute canvas with z-index:auto is a
   "positioned descendant", and per CSS2.1 Appendix E ALL positioned
   descendants paint as a group AFTER every ordinary in-flow, non-positioned
   sibling regardless of DOM order — so a fully-opaque recipe (caustics)
   could paint over, and fully hide, a section's own static text. The
   decided fix asks NOTHING of the section's content: the injector stamps
   the `[data-section]` root with `isolation: isolate` (a new stacking
   context, independent of — and never in conflict with — whatever
   position value the root carries, inline or via a stylesheet) plus a
   `position` when the root has none of its own, and gives the canvas
   itself an inline `z-index: -1`. Once the section isolates its own
   stacking context, that negative z-index can only sink the canvas below
   the section's OTHER children: per the standard stacking-order algorithm
   it still paints above the section's own background/borders but below
   every in-flow, non-positioned descendant — it can never escape below the
   page or an ancestor section. Ordinary content needs no position/z-index
   of its own to sit above it; that requirement only ever applied to
   beating a z-index:auto POSITIONED sibling (the old, now-obsolete
   guidance this comment replaced), not a negative-z-index child of an
   isolated ancestor.

   STACKING CONTRACT — HAND-AUTHORED PATH: a block that hand-writes its own
   canvas[data-atmos] (rather than going through the injector) may still
   manage its own stacking; its own inline z-index wins over anything
   implied here, since this rule never sets z-index itself — it only
   positions/sizes/click-throughs the canvas. */
canvas[data-atmos] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* ─── Prose labels ─────────────────────────────────────────────────────
   Header-only paragraphs in scraped/owner source (e.g. "**Kitchen**" in
   an access statement) are emitted by the worker's markdown-ish paragraph
   renderer (block-handlers.ts stripInlineMarkdown) as
   <strong data-prose-label>. Style them as quiet section labels rather
   than stray bold lines. Em-based so they scale with the host block's
   body size; colours inherit — the brand layer owns them. */
strong[data-prose-label] {
  display: block;
  font-size: 0.8125em;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.85;
  margin-top: 0.75em;
}
