/* ==========================================================================
   PROJECTS — 3D carousel
   Colors come from the shared .room palette (css/palette.css) — same room
   as Services/Contact/Footer, not its own separate theme. The section's
   background is the shared .site-canvas image (css/palette.css) that spans
   every post-Hero section — nothing here sets a background on .projects
   itself, so there's no specificity fight and no visible seam at this
   section's edges.

   --font-display/--font-body stay scoped here (NOT :root) since Hero's
   Bricolage Grotesque token lives on :root and must not be overridden
   sitewide — this only affects descendants of .projects.
   js/projects.js reads --card-w/--card-gap from .projects itself (not
   documentElement) to match.
   ========================================================================== */
.projects{
  /* Type */
  --font-display: 'Space Grotesk', 'Helvetica Neue', Arial, sans-serif;
  --font-body:    'Inter', 'Helvetica Neue', Arial, sans-serif;

  /* Card geometry — overridden per breakpoint below.
     script.js reads --card-w / --card-gap to compute drag & autoplay math,
     so changing a breakpoint here is enough to reflow the whole carousel. */
  --card-w: 610px;
  --card-h: 408px;
  --card-gap: 48px;

  /* 3D falloff tuning */
  --carousel-perspective: 1400px;

  font-family: var(--font-body);
  /* Asymmetric top/bottom padding (instead of the old even "8vh 0") shifts
     the whole centered header+carousel+controls block up by ~56px: since
     the block is vertically centered via justify-content:center below, its
     landing position is the *average* of the top and bottom padding, so
     trimming the top by 56px and growing the bottom by 56px moves the
     visual center up by 56px without touching any padding/margins inside
     the header or carousel themselves. max() keeps a sane floor on short
     viewports so the top edge never collapses to nothing. */
  padding: max(16px, calc(8vh - 56px)) 0 calc(8vh + 56px);
  overflow: hidden;
  position: relative;
  isolation: isolate;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  /* Centers the whole header+carousel+controls block vertically in the
     section, landing it in the blueprint's own clean middle area. */
  justify-content: center;
  box-sizing: border-box;
}

/* No background-image of its own anymore — the shared .site-canvas
   backdrop (css/palette.css) now spans Services/Projects/About/Contact/
   Footer as one continuous image, so this section stays transparent. */

/* Soft radial highlight for depth — brightest at dead center, right where
   the cards sit, gently falling off toward the edges. Pure CSS gradient,
   no blur filter, so it's effectively free to paint. */
.projects::after{
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse 70% 60% at 50% 50%, rgba(255,255,255,0.30) 0%, rgba(255,255,255,0) 70%);
  pointer-events: none;
  z-index: 0;
}

/* ==========================================================================
   HEADER
   ========================================================================== */
.projects__header{
  position: relative;
  z-index: 1;
  max-width: 720px;
  margin: 0 auto 24px;
  padding: 0 24px;
  text-align: center;
}

/* Only applied by JS right before the ScrollTrigger entrance fires — see
   script.js applyEntranceHiddenState(). If GSAP fails to load, this class
   is never added, so the header stays visible by default (no FOUC risk). */
.projects__header.js-pre-entrance{
  opacity: 0;
  transform: translateY(28px);
}

/* Eyebrow + title now match the same Fraunces-serif-heading /
   Inter-uppercase-label system used by Services and About, instead of
   this section's leftover Space Grotesk sans treatment (which read as
   a visibly different, smaller, weaker typeface next to the rest of
   the site). --font-display stays Space Grotesk at the .projects
   scope on purpose — it's still used for the carousel's photo-overlay
   card labels, which are a different, intentionally sans caption
   context — so these two rules set their own font-family explicitly
   rather than touching that shared variable. */
.projects__eyebrow{
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  font-family: var(--font-body);
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--room-text-muted);
  margin: 0 0 16px;
}
.projects__eyebrow::before,
.projects__eyebrow::after{
  content: "";
  width: 22px;
  height: 1px;
  background: var(--room-accent);
  opacity: 0.9;
}

.projects__title{
  font-family: 'Fraunces', serif;
  font-size: clamp(28px, 3.6vw, 44px);
  font-weight: 400;
  line-height: 1.15;
  letter-spacing: -0.01em;
  color: var(--room-text);
  margin: 0;
}

/* ==========================================================================
   CAROUSEL SHELL
   ========================================================================== */
.carousel{
  position: relative;
  z-index: 1;
}

.carousel__viewport{
  width: 100%;
  overflow: hidden;                       /* clip cards outside the frame */
  padding: 12px 0 16px;
  cursor: grab;
  touch-action: pan-y;                    /* let vertical page scroll pass through on touch */
  -webkit-user-select: none;
  user-select: none;
}

/* Only applied by JS right before the ScrollTrigger entrance fires. If GSAP
   fails to load, this class is never added, so the carousel (and its cards)
   stay visible by default — the section degrades gracefully instead of
   disappearing. */
.carousel__viewport.js-pre-entrance{
  opacity: 0;
  transform: translateY(70px);
}

/* Fallback: if GSAP/ScrollTrigger couldn't load at all, projects.js adds
   this to <body> so the carousel still works as a plain native-scroll
   strip instead of a blank section.

   .carousel-lite is the SAME native-scroll strip, deliberately reused
   (not a separate visual treatment) — but chosen on purpose for every
   tablet/mobile viewport (<1024px), not just as an error fallback. The
   full 3D version runs a GSAP ticker every frame, forever, animating
   `filter` (blur+brightness) and 3D transforms across all 12 cards
   simultaneously — a genuinely heavy, continuous cost that isn't worth
   paying on a phone/tablet where the effect is smaller anyway and native
   touch-scroll already feels better than a JS-driven substitute. See
   projects.js initLiteCarousel(). */
.no-gsap .carousel__viewport,
.carousel-lite.carousel__viewport{
  overflow-x: auto;
  scroll-snap-type: x proximity;
  cursor: default;
  /* The base .carousel__viewport rule (above) sets touch-action:pan-y
     for the desktop drag-carousel, where GSAP owns horizontal movement
     itself and only wants vertical page-scroll gestures to pass through
     untouched. That value is inherited here too, and on a touch device
     it actively BLOCKS the browser's native horizontal swipe on this
     now-native-scrolling strip — every version of "swipe to browse
     projects" would have been silently eaten. pan-x pan-y restores
     both axes so a horizontal drag scrolls the carousel and a vertical
     drag still passes straight through to the page, exactly matching
     plain overflow-x:auto's native, unhijacked behavior. */
  touch-action: pan-x pan-y;
  /* Hide the native scrollbar track — this is a swipeable card strip,
     not a document to scroll, and a visible horizontal scrollbar is
     the one thing that breaks the "native app" illusion on a touch
     device. Touch/trackpad swipe and momentum are entirely unaffected;
     only the visible track+thumb chrome is removed. */
  scrollbar-width: none;
}
.no-gsap .carousel__viewport::-webkit-scrollbar,
.carousel-lite.carousel__viewport::-webkit-scrollbar{
  display: none;
}
.no-gsap .carousel__track,
.carousel-lite .carousel__track{
  display: flex;
  gap: var(--card-gap);
  position: static;
  height: auto;
  perspective: none;
}
.no-gsap .card-slot,
.carousel-lite .card-slot{
  position: static;
  margin: 0;
  flex: 0 0 auto;
  scroll-snap-align: center;
}

/* Lightweight entrance for the tablet/mobile carousel: a single
   IntersectionObserver-triggered class toggle (projects.js), animating
   only opacity + transform via a plain CSS transition — no GSAP tween,
   no scroll-scrubbed timeline. Mirrors the same .in-view pattern already
   used by Services' mobile cards. */
.carousel-lite-pending{
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}
.projects-in-view .carousel-lite-pending{
  opacity: 1;
  transform: translateY(0);
}
@media (prefers-reduced-motion: reduce){
  .carousel-lite-pending{ transition-duration: 0.01ms; }
}

/* The progress-dot fill is only ever updated inside the full 3D
   ticker loop (updateCardPositions() in projects.js), which never
   runs in lite mode — hide it there instead of leaving a static,
   non-functional bar on screen. Sibling combinator: .carousel-lite
   lives on the viewport, .carousel__progress is a sibling further
   down inside .carousel__controls. */
.carousel__viewport.carousel-lite ~ .carousel__controls .carousel__progress{
  display: none;
}

.carousel__viewport.is-dragging{
  cursor: grabbing;
}

/* The track itself never physically translates — every card is placed
   individually via transform in script.js so we can wrap indices for a
   truly seamless infinite loop. This element only supplies perspective. */
.carousel__track{
  position: relative;
  height: var(--card-h);
  width: 100%;
  perspective: var(--carousel-perspective);
  perspective-origin: 50% 50%;
  transform-style: preserve-3d;
}

/* ==========================================================================
   CARD SLOT
   One per project, permanently mounted in the DOM (no cloning). script.js
   sets its transform every frame based on wrapped distance-from-center —
   this is what makes the loop infinite without a visible seam.
   Positioning transforms live HERE; hover/tilt transforms live on the
   nested .card so the two never fight over the same `transform` property.
   ========================================================================== */
.card-slot{
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--card-w);
  height: var(--card-h);
  margin-left: calc(var(--card-w) / -2);
  margin-top: calc(var(--card-h) / -2);
  transform-style: preserve-3d;
  /* filter/opacity only ever get touched by the desktop 3D ticker
     (projects.js, gated to >=1024px) — below that, the carousel runs
     in lite/native-scroll mode and this element's filter/opacity are
     never written to at all. Hinting the browser to keep a layer
     ready for properties that will never change is wasted GPU memory
     on a phone, so the wider hint is desktop-only (below). */
  will-change: transform;
}
@media (min-width: 1024px){
  .card-slot{ will-change: transform, filter, opacity; }
}

/* ==========================================================================
   CARD
   ========================================================================== */
/* Modern architectural gallery frame — reads as a floating premium
   print mounted on a presentation board, not a decorated picture
   frame. No gold, no corner ornament, no second visible ring: depth
   comes from one soft warm-ivory edge, a barely-there inner line, a
   top-left highlight and a large ambient shadow, the same way a
   photograph looks "expensive" in a Foster + Partners or Porsche
   brochure layout — through material and light, not decoration. */
.card{
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: 20px;
  overflow: hidden;
  background: rgba(255,255,255,0.6);
  border: 2px solid #F5EFE5;
  box-shadow: 0 20px 60px rgba(60,40,20,0.12);
  transform-style: preserve-3d;
  /* Same reasoning as .card-slot above: filter (the blur/brightness
     falloff) is only ever set by the desktop-only ticker. */
  will-change: transform;
  transition:
    border-color 0.35s cubic-bezier(0.22,1,0.36,1),
    box-shadow 0.35s cubic-bezier(0.22,1,0.36,1);
}
@media (min-width: 1024px){
  .card{ will-change: transform, filter; }
}

/* Sits flush just inside the outer border: a very subtle second line
   (barely distinguishable as its own ring), a 1px highlight along only
   the top and left inner edges (offsetting an inset shadow down-right
   by 1px with zero blur is what confines it to those two edges), and
   a soft inset shadow for quiet depth. z-index lifts it above the
   photo/scrim/label (all z-index:auto) without any extra DOM or
   layout changes. */
.card::before{
  content: "";
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  border-radius: 18px;
  border: 1px solid #E7D8C5;
  box-shadow:
    inset 1px 1px 0 rgba(255,255,255,0.55),
    inset 0 0 26px rgba(60,40,20,0.07);
  transition: border-color 0.35s cubic-bezier(0.22,1,0.36,1);
}

/* Hover: understated on purpose — a small lift (done in js/projects.js
   via the existing GSAP tilt tween, since GSAP already owns .card's
   transform every frame), a slightly deeper shadow and a border that's
   only marginally brighter. No glow, no scale past 1.01. */
.card:hover{
  border-color: #F8F3EA;
  box-shadow: 0 26px 72px rgba(60,40,20,0.16);
}
.card:hover::before{
  border-color: #ECDFCC;
}

/* Floating contact shadow beneath each card — scales with slot via the
   slot's own transform, giving cheap parallax without extra JS math.
   Desktop only: filter:blur() is one of the more expensive paint
   operations a browser does (a full offscreen render pass + gaussian
   convolution), and this runs it across all 12 cards. .card's own
   plain box-shadow (above, not a filter) already gives every card a
   normal elevation shadow on mobile/tablet, so nothing looks flat
   without this extra glow — it's a small additional flourish that
   isn't worth the paint cost there. */
@media (min-width: 1024px){
  .card::after{
    content: "";
    position: absolute;
    left: 8%;
    right: 8%;
    bottom: -18px;
    height: 24px;
    background: radial-gradient(50% 100% at 50% 0%, rgba(47,36,27,0.20), transparent 75%);
    filter: blur(6px);
    z-index: -1;
    pointer-events: none;
    transition: background 0.35s cubic-bezier(0.22,1,0.36,1);
  }
  .card:hover::after{
    background: radial-gradient(50% 100% at 50% 0%, rgba(47,36,27,0.26), transparent 75%);
  }
}

.card__media{
  position: absolute;
  inset: 0;
  background:
    linear-gradient(160deg, var(--grad-a, #DCE6F5) 0%, var(--grad-b, #F7F9FC) 100%);
  background-size: cover;
  background-position: center;
}

/* Fine grain texture over the gradient so placeholder media doesn't read
   flat. Only shown when there's no real photo — see .card__media--photo. */
.card__media::before{
  content: "";
  position: absolute;
  inset: 0;
  background-image: radial-gradient(rgba(47,36,27,0.05) 1px, transparent 1px);
  background-size: 4px 4px;
  opacity: 0.6;
}
.card__media--photo::before{
  display: none;
}

/* Real photos: show the WHOLE image, never cropped. "contain" shrinks the
   photo to fit inside the card at its own aspect ratio; whatever gets left
   over (top/bottom or left/right) is filled by the project's own soft
   color tint — set as background-color per project in buildCards() —
   so it reads as a deliberate "matted" frame rather than empty space. */
.card__media--photo{
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
}

/* Very subtle dark gradient at the bottom of every card — 15-20% at its
   darkest, not a heavy scrim, so the photo itself stays the focus. The
   label text below carries a soft text-shadow (a standard, understated
   legibility technique, not a decorative effect) as a safety net for
   bright/light-toned photos where an 18% overlay alone isn't quite
   enough contrast for white text. */
.card__scrim{
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(24,18,12,0) 55%, rgba(24,18,12,0.18) 100%);
  pointer-events: none;
}

.card__label{
  position: absolute;
  left: 24px;
  bottom: 24px;
  right: 24px;
}
.card__label-name{
  font-family: var(--font-display);
  font-size: 19px;
  font-weight: 600;
  color: #FFFFFF;
  margin: 0 0 4px;
  text-shadow: 0 1px 8px rgba(0,0,0,0.35);
}
.card__label-meta{
  font-size: 13px;
  color: rgba(255,255,255,0.75);
  margin: 0;
  text-shadow: 0 1px 6px rgba(0,0,0,0.3);
}

/* ==========================================================================
   CONTROLS
   ========================================================================== */
.carousel__controls{
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 24px;
  /* Was -12px, which (combined with the viewport's own 16px bottom
     padding) left only ~4px of breathing room above the prev/next
     buttons — they read as glued to the carousel. Positive margin
     here instead gives a comfortable, Apple-style gap without
     touching the viewport's own padding or anything inside it. */
  margin-top: 20px;
}

.carousel__nav{
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--room-border);
  background: rgba(255,255,255,0.55);
  color: var(--room-text-muted);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: border-color 0.25s ease, color 0.25s ease, transform 0.25s ease;
}
.carousel__nav svg{ width: 18px; height: 18px; }
.carousel__nav:hover{
  border-color: var(--room-accent);
  color: var(--room-accent);
  transform: translateY(-1px);
}
.carousel__nav:active{ transform: translateY(0); }

.carousel__progress{
  position: relative;
  width: 160px;
  height: 2px;
  background: var(--room-border);
  border-radius: 2px;
  overflow: hidden;
}
.carousel__progress-fill{
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: 8.333%;                 /* recalculated precisely (1/TOTAL) in JS */
  background: var(--room-accent);
  border-radius: 2px;
}

/* ==========================================================================
   RESPONSIVE CARD SIZING
   Desktop: ~5 cards visible · Laptop: ~4 · Tablet: ~3 · Mobile: ~1.3
   Only --card-w / --card-gap change — all carousel math derives from these.
   ========================================================================== */
@media (max-width: 1199px){
  .projects{
    --card-w: 553px;
    --card-h: 370px;
    --card-gap: 40px;
  }
}

@media (max-width: 1023px){
  .projects{
    --card-w: 466px;
    --card-h: 312px;
    --card-gap: 32px;
    /* Same asymmetric-shift trick as the base rule, scaled to ~48px for
       this tablet breakpoint's shorter available height. */
    padding: max(16px, calc(9vh - 48px)) 0 calc(9vh + 48px);
  }
}

@media (max-width: 640px){
  .projects{
    /* Card footprint trimmed a further ~6-8% (96vw/64vw -> 90vw/58vw)
       as part of this density pass — still comfortably the dominant
       element on screen, just not edge-to-edge. */
    --card-w: 90vw;
    --card-h: 58vw;
    --card-gap: 15px;
    --carousel-perspective: 900px;
    /* min-height was a flat 100vh (same as desktop) regardless of how
       much room the header+carousel+controls actually needed — on a
       short phone that forced extra empty scroll space just to reach
       the full screen height. auto lets the section size to its own
       content instead. */
    min-height:auto;
    /* Smaller ~22px shift (was 28px, originally 40px), then ~16px this
       pass — part of the mobile density pass, tightening the vertical
       rhythm (this is the "Services -> Projects" and "Projects ->
       About" transition gap) without losing the asymmetric "shifted
       up" feel. */
    padding: max(6px, calc(4.2vh - 16px)) 0 calc(4.2vh + 16px);
  }
  .projects__eyebrow{ font-size:11.5px; margin:0 0 10px; }
  .projects__title{ font-size:clamp(24px, 7vw, 32px); }
  .projects__header{ margin-bottom: 20px; }
  .carousel__viewport{ padding: 8px 0 10px; }
  .carousel__controls{ margin-top:9px; gap:18px; }
  .carousel__progress{ width: 100px; }
  .card__label{ left:20px; bottom:20px; right:20px; }
  .card__label-name{ font-size:17px; margin:0 0 3px; }
  .card__label-meta{ font-size:12px; }
}

/* ==========================================================================
   ACCESSIBILITY — respect reduced motion
   ========================================================================== */
@media (prefers-reduced-motion: reduce){
  .card, .card-slot, .card__label, .carousel__nav{
    transition-duration: 0.01ms !important;
  }
}
