/* ─── COMPONENTS ────────────────────────────────────────────────── */

/* ─── PARTICLE CANVAS ───────────────────────────────────────────── */

/* width/height:100% instead of 100vw/100vh — vw/vh includes the
   scrollbar gutter, which on a position:fixed element was adding ~27px
   of phantom horizontal overflow to every page that uses this canvas. */
#particle-canvas {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
}

main, header {
  position: relative;
  z-index: 1;
}

/* ─── LAYOUT ────────────────────────────────────────────────────── */

.container {
  width: 100%;
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 0 var(--container-pad);
}

/* ─── BUTTONS ───────────────────────────────────────────────────── */

.btn-primary {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 14px 28px;
  background: var(--color-text-soft);
  color: var(--color-text-dark);
  font-size: var(--fs-cta);
  font-weight: 500;
  letter-spacing: 0;
  border-radius: var(--radius-pill);
  transition:
    background var(--duration-base) var(--ease-smooth),
    color var(--duration-base) var(--ease-smooth),
    transform var(--duration-fast) var(--ease-smooth);
  will-change: transform;
}

.btn-primary:hover {
  background: var(--color-highlight);
  transform: translateY(-2px);
}

.btn-primary:active {
  transform: translateY(0);
  background: var(--color-accent);
  transition-duration: 0.08s;
}

.btn-primary:focus-visible {
  outline: 2px solid var(--color-highlight);
  outline-offset: 4px;
}

.btn-nav {
  position: relative;
  z-index: 0;
  box-sizing: border-box;
  height: var(--nav-btn-height);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  padding: 0 32px;
  border: 1px solid var(--color-text-dark);
  border-radius: var(--radius-pill);
  font-size: var(--fs-btn);
  font-weight: 700;
  line-height: var(--lh-uppercase);
  letter-spacing: var(--tracking-cta);
  text-transform: uppercase;
  color: var(--color-text-dark);
  transition: color var(--duration-base) var(--ease-smooth);
}

.btn-nav::before {
  content: '';
  position: absolute;
  z-index: -1;
  inset: 0;
  background: var(--color-text-dark);
  transform: translateY(100%);
  transition: transform 0.5s var(--ease-snap);
}

.btn-nav:hover {
  color: var(--color-bg-light);
}

.btn-nav:hover::before {
  transform: translateY(0);
}

.btn-nav:focus-visible {
  outline: 2px solid var(--color-text-dark);
  outline-offset: 4px;
}

/* Reuses .btn-nav's pill/outline/fill-on-hover look for the case-study
   "ver caso completo" link (Behance), placed under .cs-tags — only the
   spacing above it is specific to that context. */
.cs-cta-btn {
  margin-top: 40px;
}

/* ─── NAVIGATION ────────────────────────────────────────────────── */

.nav-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: var(--z-nav);
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  transition: background var(--duration-slow) var(--ease-smooth),
              backdrop-filter var(--duration-slow) var(--ease-smooth);
}

/* The header hides almost immediately on any scroll (see js/scroll.js's
   THRESHOLD) and .nav-fab takes over instead — it's only ever visible
   near the very top, sitting over the full-bleed dark hero photo. No JS
   ever toggles a `.scrolled` state on it, so the nav-on-dark treatment
   below is unconditional (there used to be a `.scrolled`-driven ink
   reversion here and a redundant `.nav-header.scrolled` background
   rule; both were dead code — `.nav-header`'s own base background is
   already transparent — and got removed). */
.nav-header--on-dark .nav-logo-name,
.nav-header--on-dark .nav-logo-title,
.nav-header--on-dark .nav-links a,
.nav-header--on-dark .nav-lang-btn,
.nav-header--on-dark .nav-lang-chevron,
.nav-header--on-dark .btn-nav {
  color: var(--color-text-pink);
}

.nav-header--on-dark .nav-logo-sep {
  background: var(--color-text-pink);
}

.nav-header--on-dark .nav-lang-btn,
.nav-header--on-dark .btn-nav {
  border-color: var(--color-text-pink);
}

.nav-header--on-dark .btn-nav::before {
  background: var(--color-text-pink);
}

.nav-header--on-dark .btn-nav:hover {
  color: var(--color-bg-dark);
}

/* .nav-fab-bars (the icon wrapper) is itself a span too — scoping one
   level deeper than ".nav-burger span" so only the two inner bar spans
   get recolored, not the 18x12 wrapper box (which painted as a solid
   pink rectangle when this matched it as well). */
.nav-header--on-dark .nav-burger .nav-fab-bars span {
  background: var(--color-text-pink);
}

.nav-container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: var(--nav-height);
  padding: 0 var(--container-pad);
  max-width: var(--container-max);
  margin: 0 auto;
}

.nav-logo {
  will-change: transform;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0;
  line-height: 1;
  flex-shrink: 0;
}

.nav-logo-sep {
  display: block;
  width: 1px;
  height: 13px;
  background: var(--color-text-dark);
  opacity: 0.25;
  margin: 0 16px;
  flex-shrink: 0;
}

/* ─── NAV RIGHT GROUP ───────────────────────────────────────────── */

.nav-right {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.nav-links {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  margin-right: var(--space-xs);
}

.nav-links a {
  will-change: transform;
  color: var(--color-text-dark);
  transition: color var(--duration-fast) var(--ease-smooth),
              transform var(--duration-fast) var(--ease-smooth);
  position: relative;
}

/* Small dot below the link instead of a full-width underline — matches
   the active/hover dot treatment on the off-canvas drawer's own links
   (audited from dennissnellenberg.com). */
.nav-links a::after {
  content: '';
  position: absolute;
  bottom: -23px; /* 16px gap from the link's own bottom edge to the dot's top */
  left: 50%;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--color-text-pink);
  transform: translateX(-50%) scale(0);
  transition: transform var(--duration-base) var(--ease-smooth);
}

.nav-links a:hover,
.nav-links a:focus-visible {
  transform: translateY(-1px);
}

.nav-links a:focus-visible {
  outline: 1px solid currentColor;
  outline-offset: 4px;
}

.nav-links a:hover::after,
.nav-links a.active::after {
  transform: translateX(-50%) scale(1);
}


/* ─── LANGUAGE DROPDOWN ─────────────────────────────────────────── */

.nav-lang {
  position: relative;
}

.nav-lang-btn {
  box-sizing: border-box;
  height: var(--nav-btn-height);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 0 30px;
  border: 1px solid var(--color-text-dark);
  border-radius: var(--radius-pill);
  font-family: var(--font-family);
  font-size: 13px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: var(--tracking-tight);
  color: var(--color-text-dark);
  background: transparent;
}

.nav-lang-chevron {
  transition: transform 0.25s var(--ease-smooth);
  color: var(--color-text-dark);
  flex-shrink: 0;
}

.nav-lang.open .nav-lang-chevron {
  transform: rotate(180deg);
}

.nav-lang-dropdown {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  min-width: 76px;
  background: var(--color-bg-light);
  border: 1px solid var(--color-text-dark);
  border-radius: var(--radius-md);
  overflow: hidden;
  z-index: calc(var(--z-nav) + 1);
  opacity: 0;
  pointer-events: none;
  transform: translateY(-6px);
  transition: opacity 0.2s var(--ease-smooth),
              transform 0.2s var(--ease-smooth);
}

.nav-lang.open .nav-lang-dropdown {
  opacity: 1;
  pointer-events: all;
  transform: translateY(0);
}

.nav-lang-dropdown li {
  padding: 10px 18px;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: var(--tracking-tight);
  color: var(--color-text-dark);
  transition: color var(--duration-fast) var(--ease-smooth),
              background var(--duration-fast) var(--ease-smooth);
}

.nav-lang-dropdown li[aria-selected="true"] {
  font-weight: 700;
}

/* Same two-bar icon as .nav-fab (below) — this is the header's plain/inline
   trigger shown at the top of the page; .nav-fab is the pink-circle version
   that takes over once js/scroll.js reveals it past the scroll threshold.
   Sharing the .nav-fab-bars icon between both keeps the trigger visually
   identical across that state change, only the container differs. */
.nav-burger {
  display: none;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  z-index: calc(var(--z-nav) + 10);
  position: relative;
}

.nav-burger:focus-visible {
  outline: 1px solid currentColor;
  outline-offset: 2px;
}

/* The drawer's own .mobile-menu-close takes over once it's open — hide
   the header burger so there aren't two overlapping close controls. */
body.nav-open .nav-burger {
  opacity: 0;
  pointer-events: none;
}

/* Mobile menu — off-canvas drawer, audited from dennissnellenberg.com's
   own nav (position:fixed, right:0, translateX(calc(100% + 6vw)) closed).
   Adapted to our own palette/type instead of copying his dark theme:
   light drawer (--color-bg-lighter, same bg as .footer-cta), pink accent
   for the close button + active-link dot.

   The leading (left) edge uses a genuinely oversized circle clipped
   inside a narrow strip (.mobile-menu-curve), not the border-radius
   shortcut on .footer-reveal-cap/.preloader — those are wide, short
   shapes where rounding just the corners reads as a curve, but this
   drawer is tall and narrow, so only a huge arc (775%/150% of the strip,
   Dennis's own ratio) keeps the bulge readable along the full height
   instead of flattening out in the middle. */
.mobile-menu {
  position: fixed;
  inset: 0;
  z-index: calc(var(--z-nav) + 5);
  pointer-events: none;
}

.mobile-menu.open {
  pointer-events: all;
}

.mobile-menu-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(10, 10, 11, 0.4);
  opacity: 0;
  transition: opacity var(--duration-slow) var(--ease-smooth);
}

.mobile-menu.open .mobile-menu-backdrop {
  opacity: 1;
}

/* Audited directly from dennissnellenberg.com's own CSS: the curve is
   NOT a static shape that just rides along with the panel's slide — its
   WRAP WIDTH itself animates (6vw → 0), on its own slightly longer
   transition than the panel's translate. `right: 100%` keeps the wrap's
   right edge pinned to the panel's left edge as that width shrinks, so
   the curve visually dissolves into a flat edge right as the panel
   finishes sliding in (and regrows as it slides back out on close) —
   that's what was missing before: a curve that just sat there instead of
   resolving away once open. */
.mobile-menu-curve {
  position: absolute;
  top: 0;
  right: 100%;
  width: var(--nav-curve-w);
  height: 100%;
  overflow: hidden;
  pointer-events: none;
  /* Same "dissolves into a flat edge" reveal as before, but via
     clip-path instead of animating width: the box's own geometry (and
     the oversized child circle's) never changes, so there's no layout
     recalculation and no risk of the circle getting squashed the way a
     transform: scaleX() on this wrapper would. inset(0 0 0 X%) clips
     from the left inward — at 0% the full circle shows through the
     strip, at 100% nothing does, exactly mirroring width: var(...) → 0
     with the right edge pinned. */
  clip-path: inset(0 0 0 0%);
  transition: clip-path 0.85s cubic-bezier(0.7, 0, 0.2, 1);
}

.mobile-menu.open .mobile-menu-curve {
  clip-path: inset(0 0 0 100%);
}

.mobile-menu-curve-circle {
  position: absolute;
  top: -25%;
  left: 0;
  width: 775%;
  height: 150%;
  border-radius: 50%;
  background: var(--color-bg-light);
}

.mobile-menu-panel {
  --nav-curve-w: clamp(20px, 6vw, 40px);
  position: absolute;
  top: 0;
  right: 0;
  width: min(90vw, 560px);
  height: 100vh;
  height: 100dvh;
  background: var(--color-bg-light);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: clamp(56px, 15vh, 160px) clamp(24px, 7.5vw, 108px) clamp(40px, 10vh, 100px) clamp(24px, 7.5vw, 108px);
  /* Matches dennissnellenberg.com's own .fixed-nav transition exactly —
     a plain cubic-bezier slide, not an elastic/spring ease. What reads as
     "springy" on his site is really the curve (above) resolving on its
     own, slightly longer timeline underneath this same motion. */
  transform: translateX(calc(100% + var(--nav-curve-w)));
  transition: transform 0.8s cubic-bezier(0.7, 0, 0.2, 1);
}

.mobile-menu.open .mobile-menu-panel {
  transform: translateX(0);
}

/* Floating trigger — lives outside .nav-header (which slides away on
   scroll-down, see js/scroll.js) so it stays reachable once the header
   is hidden. Hidden at the very top of the page; js/scroll.js reveals it
   past the same 80px threshold the header hide/reveal uses, and keeps it
   visible whenever the drawer is open regardless of scroll position.
   Doubles as the drawer's close button (single circle, morphing icon —
   matches dennissnellenberg.com's own floating nav trigger) instead of a
   separate close button inside the panel, so there's only ever one
   control in that corner. */
.nav-fab {
  will-change: transform;
  position: fixed;
  top: clamp(20px, 5vh, 32px);
  right: clamp(20px, 5vw, 32px);
  width: 56px;
  height: 56px;
  border-radius: 50%;
  /* Solid pink at rest — enough contrast on its own against the page,
     no border needed (the dark-circle + inset-ring version this replaced
     needed the ring to read against dark backgrounds like the hero). */
  background: var(--color-text-pink);
  box-shadow: none;
  outline: none;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: calc(var(--z-nav) + 20);
  /* Opacity/scale are driven by GSAP in js/scroll.js (a back.out overshoot
     pop, not a flat fade) — only `background` still transitions via CSS,
     for the pink/ink swap between closed and open. */
  opacity: 0;
  pointer-events: none;
  transition: background var(--duration-base) var(--ease-smooth);
}

.nav-fab.visible {
  pointer-events: all;
}

.nav-fab.open {
  background: var(--color-text-dark);
}

.nav-fab:focus-visible {
  outline: 2px solid var(--color-text-dark);
  outline-offset: 3px;
}

.nav-fab-bars {
  position: relative;
  display: block;
  width: 18px;
  height: 9px;
}

.nav-fab-bars span {
  position: absolute;
  left: 0;
  width: 18px;
  height: 1.5px;
  background: var(--color-text-dark);
  transition: transform var(--duration-base) var(--ease-smooth),
              background var(--duration-base) var(--ease-smooth),
              opacity var(--duration-base) var(--ease-smooth);
}

.nav-fab-bars span:nth-child(1) { top: 0; }
.nav-fab-bars span:nth-child(2) { top: 7.5px; }

.nav-fab.open .nav-fab-bars span:nth-child(1) {
  top: 3.75px;
  transform: rotate(45deg);
}

.nav-fab.open .nav-fab-bars span:nth-child(2) {
  top: 3.75px;
  transform: rotate(-45deg);
}

/* Ink icon reads fine on the pink resting state; once open the circle
   itself turns ink, so the X switches to light for contrast. */
.nav-fab.open .nav-fab-bars span {
  background: var(--color-bg-light);
}

/* Matches .footer-label exactly — same uppercase/bold/-0.03em eyebrow
   treatment already used elsewhere on the light theme. */
.mobile-menu-label {
  display: block;
  font-weight: 700;
  font-size: 14px;
  line-height: 1.05;
  letter-spacing: -0.03em;
  text-transform: uppercase;
  color: var(--color-text-dark);
  margin-bottom: 16px;
}

.mobile-menu-divider {
  height: 1px;
  background: var(--color-text-dark);
  margin-bottom: var(--space-md);
}

.mobile-menu-links {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.mobile-menu-links a {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  font-family: var(--font-headline);
  font-size: clamp(32px, 10vw, 40px);
  font-weight: 400;
  letter-spacing: 0;
  line-height: var(--lh-heading);
  text-transform: uppercase;
  color: var(--color-text-dark);
  opacity: 0.55;
  transform: translateX(24px);
  transition: opacity 0.5s var(--ease-smooth),
              transform 0.5s var(--ease-smooth),
              color var(--duration-fast) var(--ease-smooth);
}

.mobile-menu.open .mobile-menu-links a {
  opacity: 1;
  transform: translateX(0);
}

.mobile-menu-links a.active {
  opacity: 1;
}

.mobile-menu-links a:nth-child(1) { transition-delay: 0.08s; }
.mobile-menu-links a:nth-child(2) { transition-delay: 0.13s; }
.mobile-menu-links a:nth-child(3) { transition-delay: 0.18s; }
.mobile-menu-links a:nth-child(4) { transition-delay: 0.23s; }

.mobile-menu-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-text-dark);
  opacity: 0;
  flex-shrink: 0;
  transition: opacity var(--duration-fast) var(--ease-smooth);
}

/* The dot's flex slot (with its `gap`) is always reserved, even at
   opacity:0 — so revealing it on hover never shifts the link text. */
.mobile-menu-links a.active .mobile-menu-dot,
.mobile-menu-links a:hover .mobile-menu-dot {
  opacity: 1;
}

.mobile-menu-social {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

/* Matches .footer-mail/.footer-social a exactly — same 16px/-0.02em body
   treatment already used elsewhere on the light theme. */
.mobile-menu-social a {
  font-size: 16px;
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: var(--tracking-tight);
  color: var(--color-text-dark);
}

/* ─── HERO SECTION ──────────────────────────────────────────────── */

/* Matches dennissnellenberg.com's own hero: `min-height: 115vh`, tied to
   viewport HEIGHT, not width — confirmed via devtools (same computed height
   at 1920 and 1440 wide, since only the window's height moved the number).
   115vh is intentionally taller than the viewport so the hero reads as
   "cut off", inviting scroll. The 800px floor is a hard safety net: the
   descriptor column (padding + arrow + 7 lines of copy at 350px wide)
   needs ~771px, so anything shorter would clip the copy on very short
   windows. `height` (not `min-height`) is used so `height: 100%` on the
   children below resolves correctly — a parent with only min-height and
   no explicit height leaves percentage heights ambiguous. */
.hero {
  position: sticky;
  top: 0;
  height: max(800px, 115vh);
  overflow: hidden;
  background: var(--color-bg-dark);
}

/* Same capped-container pattern as every other section (--container-max /
   --container-pad, see variables.css) — the hero was the one section built
   with raw viewport-edge pixel offsets instead, which is why it only
   looked right at the one width it was eyeballed against. */
.hero-inner {
  position: relative;
  max-width: var(--container-max);
  height: 100%;
  margin: 0 auto;
  padding: 0 var(--container-pad);
}

/* FULL-BLEED: the photo (3840×2400, ratio 1.6 — same subject scale/crop
   as the column version, canvas just extended sideways) now covers the
   entire .hero instead of a 50% column. Moved out of .hero-inner (which
   is capped at --container-max + padding) to be a direct child of .hero,
   so inset:0 reaches the true section edges on wide viewports. */
.hero-image-wrap {
  position: absolute;
  inset: 0;
  overflow: hidden;
  z-index: 0;
}

.hero-image-inner {
  position: relative;
  width: 100%;
  height: 100%;
  will-change: transform;
}

.hero-image-inner img {
  display: block;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Scrim so the nav, descriptor text and marquee (all sitting directly on
   top of the full-bleed photo instead of bare background) keep enough
   contrast — darkens the left edge (descriptor column) and the bottom
   (marquee) without flattening the whole photo. */
.hero-image-wrap::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    linear-gradient(to right, rgba(26, 26, 26, 0.55) 0%, rgba(26, 26, 26, 0) 40%),
    linear-gradient(to top, rgba(26, 26, 26, 0.65) 0%, rgba(26, 26, 26, 0) 30%);
}

/* Hero-specific: unlike every other section (grain sits behind images,
   which stay sharp), here the photo IS the section background, so the
   grain needs to sit on top of it — but still under the type (nav,
   descriptor, marquee all use --z-above). Same noise pattern as
   .hero::before, but with mix-blend-mode:overlay instead of the normal
   blend that pattern uses elsewhere: the noise itself is pure black
   (only its alpha varies), so normal blend darkens the photo everywhere
   it lands — measured ~6% at opacity 0.18. Overlay only meaningfully
   darkens areas that are already dark (shadows), leaving mid/highlight
   skin tones essentially unaffected, which is what "adds texture
   without darkening the photo" requires here. Only one animated grain
   layer is visible at a time (.hero::before itself is hidden behind the
   now-opaque photo), so there's no risk of the earlier
   two-layers-out-of-phase flicker. */
.hero-image-inner::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 220px 220px;
  opacity: 0.18;
  /* Static, not animated: grain-shift's steps(2) jump reads as a strobe
     over an area this large (the whole photo) — fine for the smaller
     exposed-background strips it's used for elsewhere, too much here. */
}

.hero-descriptor-wrap {
  position: relative;
  z-index: var(--z-above);
  padding-top: 258px;
  /* .hero-image-wrap went from a fixed 584px to 50% (~720px at 1440) to
     stop cropping the right side of the photo — that ate back into this
     column's clearance, so padding-left came down again (276→140) rather
     than shrinking .hero-descriptor's max-width and re-fragmenting the
     paragraph back into 7 short lines. */
  padding-left: 140px;
  padding-bottom: 250px;
}

.hero-arrow {
  display: block;
  margin-bottom: 24px;
  color: var(--color-text-pink);
}

.hero-marquee {
  position: absolute;
  left: 0;
  right: 0;
  /* .hero is max(800px, 115vh) — 15vh taller than the viewport by design
     (see .hero above). bottom:0 sat the marquee at the very bottom of
     that taller box, i.e. inside the 15vh that's below the fold — cut off
     entirely. bottom:15vh pulls it up by exactly that excess, matching
     dennissnellenberg.com's own `.big-name { bottom: 15vh }` (confirmed
     via devtools), so the name stays fully visible; only the photo is
     meant to bleed past the fold. */
  bottom: 15vh;
  padding-top: 80px;
  padding-bottom: 72px;
  z-index: var(--z-above);
  overflow: hidden;
  pointer-events: none;
}

.hero-marquee-track {
  display: flex;
  width: max-content;
  animation: marqueeScroll 30s linear infinite reverse;
}

.hero-marquee-group {
  display: flex;
  flex-shrink: 0;
  align-items: baseline;
}

.hero-marquee-group span {
  font-family: var(--font-headline);
  font-weight: 400;
  font-size: var(--fs-headline-0);
  line-height: 1.05;
  letter-spacing: 0;
  text-transform: uppercase;
  color: var(--color-text-pink);
  white-space: nowrap;
}

.hero-marquee-sep {
  margin: 0 24px;
}

/* ─── MARQUEE ───────────────────────────────────────────────────── */

.marquee-double {
  width: 100%;
  overflow: hidden;
  padding: var(--space-xl) 0;
  margin: 0;
  background: var(--color-bg-light);
}

.marquee-double-row {
  display: flex;
  gap: 16px;
  width: calc(100% + 160px);
  margin-left: -80px;
  will-change: transform;
}

.marquee-double-row + .marquee-double-row {
  margin-top: 16px;
}

.marquee-double-item {
  flex: 1 1 0;
  min-width: 0;
  aspect-ratio: 4 / 3;
  overflow: hidden;
}

.marquee-double-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  pointer-events: none;
  user-select: none;
}

@keyframes marqueeScroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

@media (max-width: 768px) {
  /* Removed on mobile per design decision — was previously a 2-row
     autoscrolling image marquee below the work list. */
  .marquee-double {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .marquee-double-row {
    transform: none !important;
  }
}

/* ─── SELECTED WORK ─────────────────────────────────────────────── */

.work-section {
  background: var(--color-bg-light);
  padding-top: 0;
  padding-bottom: var(--space-xl);
}

.work-inner {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 0 var(--container-pad);
}

.work-heading {
  font-family: var(--font-family);
  font-weight: 700;
  font-size: 16px;
  line-height: 1.1;
  letter-spacing: -0.03em;
  text-transform: uppercase;
  color: var(--color-text-dark);
  margin: 0;
}

.work-list {
  list-style: none;
  margin: 32px 0 0;
  padding: 0;
}

.work-list li {
  border-top: 1px solid var(--color-text-dark);
}

.project-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 40px;
  padding: 60px 109px;
  text-decoration: none;
  color: var(--color-text-dark);
}

/* Desktop shows the project photo via the cursor-follow .project-preview
   panel instead (mouse-only). This <img> exists purely for the mobile
   card layout, which has no hover — see the mobile override below. */
.project-row-image {
  display: none;
}

.project-row-image--placeholder {
  background: var(--color-bg-light);
}

.project-row-text {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.project-row-title {
  font-family: var(--font-headline);
  font-weight: 400;
  font-size: var(--fs-headline-3);
  line-height: 1.05;
  text-transform: uppercase;
  color: var(--color-text-dark);
}

.project-row-desc {
  font-family: var(--font-family);
  font-weight: 400;
  font-size: var(--fs-body);
  line-height: 1.2;
  letter-spacing: -0.03em;
  color: var(--color-text-dark);
}

.project-row-arrow {
  flex-shrink: 0;
  display: flex;
}

.project-row-arrow svg {
  display: block;
  transition: transform var(--duration-base) var(--ease-smooth);
}

.project-row:hover .project-row-arrow svg,
.project-row:focus-visible .project-row-arrow svg {
  transform: translateX(4px);
}

.project-row:focus-visible {
  outline: 1px solid var(--color-text-dark);
  outline-offset: -8px;
}

.project-row--placeholder {
  cursor: default;
  opacity: 0.4;
}

/* Neutralize the dark-grid .project-card look (shared with work.html)
   inside the new text-row list */
.work-list .project-card {
  position: static;
  overflow: visible;
  background: transparent;
}

.work-list .project-card--placeholder {
  opacity: 1;
}

/* ─── PROJECT PREVIEW — cursor-follow thumbnail ──────────────────── */

.project-preview {
  /* Fixed at its full open size — js/project-preview.js scales it via
     GSAP instead of animating width/height (same reasoning as
     .cursor-ring above: this already carries a GSAP-driven x/y
     transform for cursor-tracking, so the open/close grow is folded
     into that same transform via `scale` rather than fought over with
     a separate CSS width/height transition). */
  --preview-size: clamp(220px, 20vw, 380px);
  position: fixed;
  top: 0;
  left: 0;
  width: var(--preview-size);
  height: var(--preview-size);
  overflow: hidden;
  z-index: calc(var(--z-cursor) - 1);
  pointer-events: none;
  will-change: transform;
}

/* Track/item heights assume exactly 5 stacked .project-preview-item
   frames (100% / 5 = 20% each, track = 5x the mask's own height) — keep
   both in sync with the actual count in index.html; js/project-preview.js
   itself already computes yPercent dynamically from that count. */
.project-preview-track {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 500%;
}

.project-preview-item {
  width: 100%;
  height: 20%;
  background-size: cover;
  background-position: center;
}

.project-preview-item--placeholder {
  background-color: var(--color-surface);
}

@media (max-width: 640px) {
  .project-preview {
    display: none;
  }
}

.project-card {
  position: relative;
  overflow: hidden;
  background: var(--color-bg-dark);
}

.project-card--placeholder {
  pointer-events: none;
  opacity: 0.35;
}

.project-card--placeholder .project-img-wrap {
  background: var(--color-surface);
  display: flex;
  align-items: center;
  justify-content: center;
}

.project-card--placeholder .placeholder-label {
  font-size: var(--fs-label);
  color: var(--color-muted);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}

.project-card-link {
  display: block;
  position: relative;
  overflow: hidden;
}

.project-img-wrap {
  position: relative;
  overflow: hidden;
  aspect-ratio: 16/10;
}

.project-img-inner {
  width: 100%;
  height: 100%;
  transition: transform 0.8s var(--ease-smooth);
}

.project-img-inner img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

.project-card-link:hover .project-img-inner,
.project-card-link:focus-visible .project-img-inner {
  transform: scale(1.06);
}

.project-card-link:focus-visible {
  outline: 1px solid var(--color-text-dark);
  outline-offset: -4px;
}

.project-info {
  padding: var(--space-sm) 0;
  background: var(--color-bg-dark);
}

.project-meta {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-sm);
  margin-top: var(--space-xs);
}

.project-title {
  margin-top: 0;
}

.project-card-desc {
  margin-top: var(--space-xs);
  font-family: var(--font-family);
  font-weight: 400;
  font-size: var(--fs-body);
  line-height: 1.2;
  letter-spacing: var(--tracking-h1);
}

/* work.html — grid de cards sobre fondo claro (body.page-light), a
   diferencia del uso original de .project-card en fondo oscuro. */
body.page-light .project-card,
body.page-light .project-info {
  background: transparent;
}


/* ─── CTA SHOWCASE ──────────────────────────────────────────────── */

.cta-showcase {
  background: var(--color-bg-dark);
  padding-top: var(--space-3xl);
  padding-bottom: var(--space-3xl);
  overflow: hidden;
}

.cta-showcase-inner {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 0 var(--container-pad);
}

.cta-showcase-media-grid {
  position: relative;
  height: 965px;
}

.cta-showcase-media-item {
  position: absolute;
  overflow: hidden;
  will-change: transform;
}

.cta-showcase-media-item img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.cta-media-1 { left: 0;     top: 260px; width: 505px; height: 284px; }
.cta-media-2 { left: 390px; top: 0;     width: 499px; height: 852px; }
.cta-media-3 { left: 860px; top: 60px;  width: 232px; height: 329px; }
.cta-media-4 { left: 0;     top: 704px; width: 505px; height: 339px; }
.cta-media-5 { left: 775px; top: 656px; width: 505px; height: 505px; }

.cta-showcase-content {
  max-width: 1220px;
  margin: 80px auto 0;
  text-align: center;
}

.cta-showcase-heading {
  font-family: var(--font-headline);
  font-size: var(--fs-headline-1);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: uppercase;
  line-height: 1.05;
  color: var(--color-text-pink);
}

.cta-showcase-text {
  margin-top: 56px;
  font-family: var(--font-family);
  font-size: 20px;
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: var(--tracking-tight);
  color: var(--color-text-pink);
}

.cta-showcase-btn {
  position: relative;
  z-index: 0;
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  margin-top: 56px;
  height: var(--nav-btn-height);
  padding: 0 32px;
  border: 1px solid var(--color-text-pink);
  border-radius: var(--radius-pill);
  font-size: var(--fs-btn);
  font-weight: 700;
  line-height: var(--lh-uppercase);
  letter-spacing: var(--tracking-cta);
  text-transform: uppercase;
  color: var(--color-text-pink);
  transition: color var(--duration-base) var(--ease-smooth);
}

.cta-showcase-btn::before {
  content: '';
  position: absolute;
  z-index: -1;
  inset: 0;
  background: var(--color-text-pink);
  transform: translateY(100%);
  transition: transform 0.5s var(--ease-snap);
}

.cta-showcase-btn:hover {
  color: var(--color-text-dark);
}

.cta-showcase-btn:hover::before {
  transform: translateY(0);
}

/* ─── FOOTER ────────────────────────────────────────────────────── */

.footer-cta {
  background: var(--color-bg-light);
  padding-top: 80px;
  padding-bottom: 24px;
}

/* overflow:hidden on .footer-cta (set below, for the grain effect) doubles
   as the mask for this: .footer-parallax gets translated down on load and
   eases to 0 as the section scrolls into view, so the footer content reads
   as rising up from beneath the section above it. See js/scroll.js. */
.footer-parallax {
  will-change: transform;
}

/* Elliptical bottom edge (huge border-radius on a short rectangle) reads as
   a smooth curve — matches .cta-showcase's dark bg. Scales down to 0 from
   the top as the section scrolls into view (see js/scroll.js), so the dark
   curve appears to retreat upward and reveal the light footer beneath it,
   like a curtain lifting rather than a flat wipe. Sits directly on
   .footer-cta (not inside .footer-parallax) so it stays flush with the
   section's own top edge instead of trailing the rise animation. */
.footer-reveal-cap {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 5;
  width: 100%;
  height: 110px;
  background: var(--color-bg-dark);
  border-radius: 0 0 50% 50% / 0 0 55% 55%;
  transform-origin: top center;
  pointer-events: none;
}

.footer-cta-inner {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 0 var(--container-pad);
  display: grid;
  grid-template-columns: minmax(0, 570px) 1fr;
  gap: var(--space-lg);
}

.footer-col {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.footer-name {
  font-family: var(--font-family);
  font-weight: 700;
  font-size: 24px;
  line-height: 1.05;
  letter-spacing: -0.03em;
  text-transform: uppercase;
  color: var(--color-text-dark);
  margin-bottom: 56px;
}

.footer-group + .footer-group {
  margin-top: 56px;
}

.footer-label {
  display: block;
  font-family: var(--font-family);
  font-weight: 700;
  font-size: 14px;
  line-height: 1.05;
  letter-spacing: -0.03em;
  text-transform: uppercase;
  color: var(--color-text-dark);
  margin-bottom: 16px;
}

.footer-mail,
.footer-social a {
  position: relative;
  display: inline-block;
  font-size: 16px;
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: var(--tracking-tight);
  color: var(--color-text-dark);
}

.footer-mail::after,
.footer-social a::after {
  content: '';
  position: absolute;
  bottom: 1px;
  left: 0;
  width: 100%;
  height: 1px;
  background: var(--color-text-dark);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--duration-base) var(--ease-smooth);
}

.footer-mail:hover::after,
.footer-social a:hover::after {
  transform: scaleX(1);
}

.footer-social {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.footer-social a + a {
  margin-top: 8px;
}

.footer-cta-heading {
  font-family: var(--font-family);
  font-weight: 500;
  font-size: 32px;
  line-height: 1.2;
  letter-spacing: var(--tracking-tight);
  color: var(--color-text-dark);
}

.footer-cta-btn {
  position: relative;
  z-index: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  height: var(--nav-btn-height);
  padding: 0 32px;
  margin-top: 40px;
  border: 1px solid var(--color-text-dark);
  border-radius: var(--radius-pill);
  font-size: var(--fs-btn);
  font-weight: 700;
  line-height: var(--lh-uppercase);
  letter-spacing: var(--tracking-cta);
  text-transform: uppercase;
  color: var(--color-text-dark);
  transition: color var(--duration-base) var(--ease-smooth);
}

.footer-cta-btn::before {
  content: '';
  position: absolute;
  z-index: -1;
  inset: 0;
  background: var(--color-text-dark);
  transform: translateY(100%);
  transition: transform 0.5s var(--ease-snap);
}

.footer-cta-btn:hover {
  color: var(--color-bg-light);
}

.footer-cta-btn:hover::before {
  transform: translateY(0);
}

.site-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  max-width: var(--container-max);
  margin: 80px auto 0;
  padding: 24px var(--container-pad) 0;
  position: relative;
  font-size: var(--fs-btn);
  font-weight: 400;
  letter-spacing: var(--tracking-tight);
  color: var(--color-text-dark);
}

/* Standalone usage (Contact, no .footer-cta wrapper) has no parent
   padding-bottom to breathe against — everywhere else .site-footer sits
   inside .footer-cta, which supplies its own 24px padding-bottom below
   it. */
main > .site-footer {
  padding-bottom: 48px;
}

.site-footer::before {
  content: '';
  position: absolute;
  top: 0;
  left: var(--container-pad);
  right: var(--container-pad);
  height: 1px;
  background: var(--color-border);
  pointer-events: none;
}

/* ─── PAGE TRANSITION OVERLAY ───────────────────────────────────── */
/*
 * CSS-only overlay on html::before.
 * Inline script in <head> adds .is-transitioning before first paint,
 * so the overlay covers the page immediately — no JS timing gap.
 * Exit uses .is-leaving; enter fades out when .is-transitioning is removed.
 */

html::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 9998;
  background: var(--color-bg-dark);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.25s ease;
}

/* ENTER: class set by inline script → snap to covered, no CSS transition */
html.is-transitioning::before {
  opacity: 1;
  transition: none;
}

/* EXIT: class set by click handler → fade in using base transition */
html.is-leaving::before {
  opacity: 1;
}

/* Hide preloader so it can't appear above the overlay during transition */
html.is-transitioning .preloader {
  display: none !important;
}

/* ─── PRELOADER ─────────────────────────────────────────────────── */

/* dennissnellenberg.com's own preloader (confirmed via devtools) wipes
   away with a curved edge instead of a flat one — same rounded-div
   technique as .footer-reveal-cap (border-radius ellipse), just applied
   to the full-screen curtain itself instead of a separate cap element,
   since here the whole box slides off rather than shrinking in place.
   The vertical radius is intentionally shallow (4%) — kept far subtler
   than the footer's curve per feedback that the footer's read as too
   pronounced at first. */
.preloader {
  position: fixed;
  inset: 0;
  z-index: var(--z-preloader);
  background: var(--color-bg-light);
  overflow: hidden;
}

.preloader::before {
  content: '';
  position: absolute;
  top: -50%;
  left: -50%;
  width: 200%;
  height: 200%;
  z-index: -1;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 220px 220px;
  opacity: 0.18;
  animation: grain-shift 1s steps(2) infinite;
  will-change: transform;
}

/* Curved bottom edge only while the preloader is sliding away — applied
   here instead of on the base rule so the static loading state (counter
   at 0-100%) stays flat. The curve otherwise sits there the whole time,
   exposing html's dark background as a fixed dark line under the counter
   instead of only appearing as a trailing edge during the exit wipe. */
.preloader.is-exiting {
  border-radius: 0 0 50% 50% / 0 0 4% 4%;
}

.preloader-media {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: clamp(260px, 32vw, 500px);
  aspect-ratio: 16 / 10;
  overflow: hidden;
}

.preloader-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
}

.preloader-count {
  position: absolute;
  bottom: clamp(32px, 4vw, 56px);
  right:  clamp(32px, 4vw, 56px);
  font-family: var(--font-headline);
  font-size: var(--fs-headline-4);
  font-weight: 400;
  letter-spacing: 0;
  color: var(--color-text-dark);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

/* ─── GRAIN OVERLAY ─────────────────────────────────────────────── */
/*
 * Applied per-section (not as a full-page overlay) so the noise sits
 * behind each section's own content: the pseudo-element carries a
 * negative z-index inside the section's own stacking context (position:
 * relative + z-index:0 on the section), which paints it above the
 * section's background but below every normal child — text and images
 * stay fully sharp.
 */

.hero,
.marquee-double,
.promesa,
.work-section,
.services-section,
.cta-showcase,
.footer-cta {
  position: relative;
  z-index: 0;
  overflow: hidden;
}

.hero::before,
.marquee-double::before,
.promesa::before,
.work-section::before,
.services-section::before,
.cta-showcase::before,
.footer-cta::before {
  content: '';
  position: absolute;
  top: -50%;
  left: -50%;
  width: 200%;
  height: 200%;
  z-index: -1;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 220px 220px;
  opacity: 0.18;
  animation: grain-shift 1s steps(2) infinite;
  will-change: transform;
}

/* ─── PAGE — LIGHT VARIANT (páginas internas de fondo claro, ej. work.html) ── */
/* La textura de grano de arriba está pensada para secciones puntuales sobre
   fondo alternante. Acá la página entera es un solo tono claro, así que el
   grano se aplica una vez a nivel body, fixed para cubrir todo el scroll. */
body.page-light {
  position: relative;
  background-color: var(--color-bg-light);
  color: var(--color-text-dark);
}

/* Nav's active-link dot defaults to pink (built for the dark hero nav) —
   on a light-hero page it needs the dark ink instead, same logic as the
   rest of this page-light override block. */
body.page-light .nav-links a::after {
  background: var(--color-text-dark);
}

/* Case study shared classes (.cs-*) default to --color-text-soft, a light
   pink built for dark backgrounds — near-invisible (~1.3:1) against
   --color-bg-light. Scoped fix for case-study pages going page-light. */
body.page-light .cs-client,
body.page-light .cs-section-label .eyebrow,
body.page-light .cs-related > .eyebrow,
body.page-light .cs-meta-value,
body.page-light .cs-overview-text,
body.page-light .cs-text,
body.page-light .cs-hero-eyebrow-label,
body.page-light .cs-hero-eyebrow-value {
  color: var(--color-text-dark);
}

body.page-light .cs-headline em {
  color: var(--color-highlight-on-light);
}

/* Spec calls for "Body large, 20/150%" — .cs-overview-text's default
   line-height (--lh-body-lg, 1.45) is tuned for the not-yet-migrated case
   studies; this page needs the exact 150% (--lh-body, 1.5) instead. */
body.page-light .cs-overview-text {
  line-height: var(--lh-body);
}

/* This page standardizes its section-level vertical gaps to 160px
   (title -> eyebrow, headline -> tags, paragraph -> images all use it). */
body.page-light .cs-section-images {
  margin-top: 160px;
}

/* --color-border is a light-grey mix tuned to show up against a dark
   background (~15:1) — it's nearly invisible (~1.25:1) on bg-light. The
   meta grid's dividers need a page-light-appropriate value instead. */
body.page-light .cs-meta-grid,
body.page-light .cs-meta-item {
  border-color: color-mix(in srgb, var(--color-text-dark) 15%, transparent);
}

body.page-light::before {
  content: '';
  position: fixed;
  inset: -50%;
  z-index: -1;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 220px 220px;
  opacity: 0.18;
  animation: grain-shift 1s steps(2) infinite;
  will-change: transform;
}

/* El grano de .footer-cta::before (compartido con el resto del sitio) se
   suma al de body.page-light::before de arriba y duplica la densidad —
   se desactiva acá porque el grano a nivel body ya cubre esta sección. */
body.page-light .footer-cta::before {
  content: none;
}

/* .footer-reveal-cap está pensado para camuflarse con la sección anterior
   (oscura en el homepage). En páginas body.page-light la sección anterior
   es clara, así que el cap tiene que matchear ese tono en vez del oscuro
   hardcodeado. */
body.page-light .footer-reveal-cap {
  background: var(--color-bg-light);
}

/* Footer dark variant — páginas body.page-light (work, contact) terminan en
   una sección entera clara, así que el footer (siempre claro por defecto)
   se pierde sin ningún borde visual. Se invierte a dark acá para que el
   .footer-reveal-cap de arriba (claro, matchea la sección anterior) siga
   funcionando como telón: ahora revela un footer oscuro en vez de uno claro,
   el mismo efecto que en el home pero en la dirección opuesta. Texto en
   var(--color-bg-light) (FAFAFA), no en el pink de acento — registro más
   neutro/gráfico para el cierre de página, distinto del resto de las
   secciones dark del sitio. */
body.page-light .footer-cta {
  background: var(--color-bg-dark);
}

/* Scoped to .footer-cta specifically (not just body.page-light) — this
   flips text to light because .footer-cta's own background inverts to
   dark on page-light pages (see above). Without that scope it also hit
   .footer-label/.footer-mail/.footer-social reused outside the footer
   (Contact's "00 Formulario" info block), turning them white-on-white
   against the section's own light background. */
body.page-light .footer-cta .footer-name,
body.page-light .footer-cta .footer-label,
body.page-light .footer-cta .footer-mail,
body.page-light .footer-cta .footer-social a,
body.page-light .footer-cta-heading,
body.page-light .footer-cta .site-footer {
  color: var(--color-bg-light);
}

body.page-light .footer-cta .footer-mail::after,
body.page-light .footer-cta .footer-social a::after {
  background: var(--color-bg-light);
}

body.page-light .footer-cta-btn {
  border-color: var(--color-bg-light);
  color: var(--color-bg-light);
}

body.page-light .footer-cta-btn::before {
  background: var(--color-bg-light);
}

body.page-light .footer-cta-btn:hover {
  color: var(--color-bg-dark);
}

body.page-light .site-footer::before {
  background: color-mix(in srgb, var(--color-bg-light) 15%, transparent);
}

body.page-light .project-tag,
body.page-light .project-year {
  color: var(--color-text-dark);
}

body.page-light .project-title {
  font-family: var(--font-headline);
  font-size: var(--fs-headline-4);
  font-weight: 400;
  letter-spacing: 0;
  line-height: var(--lh-heading);
  text-transform: uppercase;
}

body.page-light .project-info {
  padding: 20px 0;
}

@keyframes grain-shift {
  0%   { transform: translate(0, 0); }
  10%  { transform: translate(-2%, -4%); }
  20%  { transform: translate(-8%, 2%); }
  30%  { transform: translate(6%, -6%); }
  40%  { transform: translate(-4%, 8%); }
  50%  { transform: translate(-6%, -2%); }
  60%  { transform: translate(8%, 4%); }
  70%  { transform: translate(2%, -8%); }
  80%  { transform: translate(-8%, 6%); }
  90%  { transform: translate(6%, 2%); }
  100% { transform: translate(0, 0); }
}

/* ─── CUSTOM CURSOR ─────────────────────────────────────────────── */

.cursor-dot,
.cursor-ring {
  position: fixed;
  pointer-events: none;
  z-index: var(--z-cursor);
  border-radius: var(--radius-pill);
  /* Highest-frequency animation on the site (updates on every
     mousemove) — the one place will-change matters most. */
  will-change: transform;
  /* Hidden by default in CSS, not just via the initial gsap.set() —
     js/cursor.js now exits early on touch devices AND on
     prefers-reduced-motion, so that gsap.set() never runs in either
     case. Without a CSS default these would sit fully opaque and
     pinned at (0,0) instead of properly absent. */
  opacity: 0;
}

.cursor-dot {
  width: 8px;
  height: 8px;
  background: var(--color-text-dark);
  /* NO transform here — GSAP handles centering via xPercent/yPercent */
}

.cursor-ring {
  /* Fixed at its largest state (116px) — GSAP scales it down for the
     smaller states via `transform: scale()` instead of animating
     width/height directly (layout thrash on every mousemove/hover).
     Scale factors are computed in js/cursor.js relative to this size. */
  width: 116px;
  height: 116px;
  border: none;
  opacity: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  transition: border-radius 0.35s var(--ease-smooth),
              background 0.35s var(--ease-smooth),
              opacity 0.35s var(--ease-smooth);
}

.cursor-ring.is-hovering {
  opacity: 0.5;
}

.cursor-ring.is-project,
.cursor-ring.is-drag,
.cursor-ring.is-scroll {
  background: var(--color-text-pink);
  opacity: 1;
  transition: border-radius 0.45s var(--ease-smooth),
              background 0.3s var(--ease-smooth),
              opacity 0.3s var(--ease-smooth);
}

.cursor-ring-label {
  font-family: var(--font-family);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
  text-transform: uppercase;
  color: var(--color-text-dark);
  white-space: nowrap;
  opacity: 0;
  transition: opacity 0.2s;
  pointer-events: none;
}

.cursor-ring.is-project .cursor-ring-label,
.cursor-ring.is-drag .cursor-ring-label,
.cursor-ring.is-scroll .cursor-ring-label {
  opacity: 1;
}

@media (pointer: coarse) {
  .cursor-dot,
  .cursor-ring {
    display: none;
  }
}

/* ─── PAGE HERO ─────────────────────────────────────────────────── */

.page-hero {
  padding: calc(var(--nav-height) + var(--space-xl)) var(--container-pad) var(--space-xl);
  max-width: var(--container-max);
  margin: 0 auto;
  position: relative;
}
.page-hero::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: var(--container-pad);
  right: var(--container-pad);
  height: 1px;
  background: var(--color-border);
  pointer-events: none;
}

/* work.html tiene su propia línea divisoria en .work-counter — la de
   .page-hero queda redundante y se saca solo para esta página. */
body.page-light .page-hero::after {
  content: none;
}

.page-hero .eyebrow {
  display: block;
  margin-bottom: var(--space-sm);
}

.page-hero h1 {
  margin-bottom: var(--space-md);
  max-width: 900px;
}

/* work.html hero — shared with .about-page-hero (same grid/typography,
   About just runs it on the site's default dark theme instead of
   page-light, so colors are split into their own rules below). */
/* Grid de 2 columnas: headline a la izquierda, párrafo empujado a la
   derecha por la columna, no por un margin fijo en px. */
body.page-light .page-hero,
.about-page-hero {
  display: grid;
  grid-template-columns: 1fr 396px;
  align-items: start;
  padding-top: calc(var(--nav-height) + var(--space-lg));
  padding-bottom: 160px;
}

/* Named exception to the headline scale (CLAUDE.md): work.html's hero goes
   a level up (--fs-headline-1, 80px) vs the --fs-headline-2 every other
   .page-hero h1 uses — spec'd by the Figma reference for this page. About
   reuses the same exception (own Figma spec, confirmed same scale). */
body.page-light .page-hero h1,
.about-page-hero h1 {
  grid-column: 1 / -1;
  margin-bottom: var(--space-lg);
  font-family: var(--font-headline);
  font-size: var(--fs-headline-1);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: uppercase;
  line-height: 1.05;
}

body.page-light .page-hero h1 {
  color: var(--color-text-dark);
}

.about-page-hero h1 {
  color: var(--color-text-pink);
}

body.page-light .page-hero p,
.about-page-hero p {
  font-size: 20px;
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: var(--tracking-tight);
  text-wrap: pretty;
}

body.page-light .page-hero p {
  color: var(--color-text-dark);
}

.about-page-hero p {
  color: var(--color-text-soft);
}

/* Wider box for the headline than the shared .page-hero h1 cap (900px)
   gives — that fits work.html's shorter copy fine, but About's headline
   wraps into more lines than needed at that width. Scoped to About only. */
.about-page-hero h1 {
  max-width: 1200px;
}

/* .page-hero's default ::after divider (var(--color-border), tuned for a
   light bg) has no dark-theme equivalent here — About's Figma has no
   line under the hero, same as work.html's page-light suppression. */
.about-page-hero::after {
  content: none;
}

/* Hero -> "01" index-row gap, same 160px/80px (desktop/mobile) rhythm
   already used between major blocks on the case studies. Overrides the
   160px padding-bottom shared with work.html's hero above — About's
   .cs-section/.cs-index-row underneath it adds its own padding on top of
   whatever's left here, so this alone isn't the full visible gap. */
.about-page-hero {
  padding-bottom: 120px;
}

/* Contact hero — different arrangement from work.html/About's "h1 spans
   both columns, image/paragraph share row 2" pattern (inherited above
   via the shared body.page-light .page-hero rule): here the headline
   stays in column 1 only, the image sits beside it in column 2 (top-
   aligned, own aspect-ratio — not stretched to match the text block),
   and the paragraph continues below the headline in column 1. Also its
   own (smaller) headline scale — --fs-headline-2 (64px), not the
   work.html/About --fs-headline-1 exception. */
body.page-light .contact-page-hero h1 {
  grid-column: 1;
  font-size: var(--fs-headline-2);
}

body.page-light .contact-page-hero p {
  grid-column: 1;
}

/* Widens the shared grid's 396px image column to 550px — h1/p sit in
   column 1 (1fr, shrinks to make room) and are left-aligned within it,
   so text position is untouched; only the column split moves, growing
   the image leftward and closing the empty gap that column 1's unused
   width left between the text and the image. Wrapped in its own
   min-width query rather than relying on cascade order against the
   mobile grid-template-columns:1fr override below — same specificity
   (0,2,1) either way, and this is more robust than trusting source
   order to keep winning here. */
@media (min-width: 769px) {
  body.page-light .contact-page-hero {
    grid-template-columns: 1fr 550px;
  }
}

.contact-hero-image {
  grid-column: 2;
  grid-row: 1 / 3;
  align-self: start;
  aspect-ratio: 628 / 355;
}

/* Same hero -> "00" index-row 160px/80px rhythm as About. */
body.page-light .contact-page-hero {
  padding-bottom: 132px;
}

@media (max-width: 768px) {
  body.page-light .page-hero,
  .about-page-hero {
    grid-template-columns: 1fr;
  }

  /* .contact-hero-image's grid-column:2 has nothing to sit in once the
     grid above collapses to a single explicit column — grid auto-
     creates an implicit second column instead, sized by grid-auto-
     columns (default "auto", i.e. by the image's own intrinsic size),
     which forced real overflow wide enough to inflate the whole page's
     layout viewport. Column 1 stacks it under the text like About's/
     work.html's image already does on mobile. */
  .contact-hero-image {
    grid-column: 1;
    grid-row: auto;
    width: 100%;
    /* Grid items default to min-width:auto (their content's min-content
       size) — for an aspect-ratio box with display:flex and no explicit
       width, that min-content was wide enough to stop the "1fr" track
       from ever shrinking to the actual 390px viewport, the real source
       of the residual overflow above (the classic min-width:auto grid/
       flexbox sizing gotcha). */
    min-width: 0;
    margin-top: var(--space-lg);
  }

  /* Desktop's --fs-headline-1 exception (see the named-exception comment
     above) is tuned for an 80px desktop headline — at mobile's clamp floor
     it still reads as oversized against a 375px-wide column (4 stacked
     lines dominating the whole viewport). Drops one more level, to the
     --fs-headline-3 (56px) an H3 gets elsewhere on the site. */
  body.page-light .page-hero h1,
  .about-page-hero h1 {
    font-size: var(--fs-headline-3);
    /* "RESULTADOS." alone measures ~394px at 56px PP Fragment uppercase —
       wider than the 390px viewport even before the column's own
       padding, so it can never fit on one line. `break-word` (used on
       .cs-hero-title for "MyFitnessPal") looked like the same fix but
       doesn't apply here: per spec, break-word only breaks a word to
       avoid overflowing an ALREADY-ESTABLISHED width — it's excluded
       from the min-content/intrinsic-size calculation mobile browsers
       use to decide the layout viewport's width, so that calculation
       still saw one unbreakable 394px word and widened the whole page's
       layout viewport to fit it (clipping everything on the right,
       nav-burger included). `anywhere` is the value that's actually
       counted in that intrinsic-size pass. */
    overflow-wrap: anywhere;
  }

  /* Own (smaller) mobile size — work.html's headline ("Producto real,
     para problemas reales.") has "PROBLEMAS" alone measuring ~360px at
     the shared 56px, wider than the ~333-346px column across mobile
     widths — same mid-word-break risk as the about-page-hero case below,
     just triggered by a different word. 44px leaves ~50-60px of margin.
     Matches the shared rule's own specificity (body.page-light .page-hero
     h1) so it actually wins instead of losing to it silently. */
  body.page-light .page-hero h1 {
    font-size: 48px;
  }

  /* Own (smaller) mobile size — "resultados." alone still measures ~325px
     at --fs-headline-3, comfortably under the ~346px column, whereas at
     the shared 56px it was ~394px, wider than the column and forcing an
     ugly mid-word break even with overflow-wrap:anywhere as a backstop. */
  .about-page-hero h1 {
    font-size: 46px;
  }

  body.page-light .page-hero p,
  .about-page-hero p {
    grid-column: 1;
    font-size: 18px;
  }

  .about-page-hero {
    padding-bottom: 40px;
  }
}

.page-hero p {
  max-width: 560px;
}

/* Wider bio box for About specifically — the shared 560px cap above fits
   work.html's shorter subhead fine, but About's longer bio wraps into far
   more lines than needed at that width. Placed after the shared rule
   (equal specificity) so it actually wins instead of losing to it. */
.about-page-hero p {
  max-width: 920px;
}

/* ─── FILTER CHIPS ──────────────────────────────────────────────── */

.filter-bar {
  display: flex;
  gap: var(--space-md);
  padding: var(--space-lg) var(--container-pad);
  max-width: var(--container-max);
  margin: 0 auto;
  flex-wrap: wrap;
}

@media (max-width: 768px) {
  /* Wrapping to a 2nd line reflows the whole filter-bar height depending on
     which chip is active; a horizontal scroll strip keeps the row's height
     fixed and matches the swipeable-chips pattern mobile users expect. */
  .filter-bar {
    flex-wrap: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }

  .filter-bar::-webkit-scrollbar {
    display: none;
  }

  .filter-chip {
    flex-shrink: 0;
    white-space: nowrap;
  }
}

/* work.html — .page-hero ya termina con --space-xl (120px) de padding
   inferior; el filter-bar no debe sumar otro padding-top encima, si no
   el gap real hasta las pills queda en 200px en vez de los 120 pedidos.
   padding-bottom:40px es también el gap filtros→línea horizontal de abajo
   (.work-counter::before), ahora que el filter-bar quedó arriba de todo. */
body.page-light .filter-bar {
  padding-top: 0;
  padding-bottom: 40px;
}

/* Solo work.html usa .filter-bar-wrap — seguro escalar acá sin afectar
   about.html, que usa .filter-bar suelto. Aloja los chips + el toggle de
   vista lado a lado; flex-start (no center) para que el toggle se alinee
   con el texto de los chips y no con la caja completa del filter-bar
   (que es más alta por su padding-bottom de 40px). */
.filter-bar-wrap {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-md);
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 0 var(--container-pad);
}

.filter-bar-wrap .filter-bar {
  max-width: none;
  margin: 0;
  padding-left: 0;
  padding-right: 0;
}

/* View toggle — lista/grilla, reference: dennissnellenberg.com/work.
   js/animations.js swaps [data-work-view] on <main> between "list"/"grid"
   on click; the [data-work-view] rules further down show/hide
   .work-index-list vs .work-grid accordingly. Sin sentido en mobile — ahí
   no hay opción de vista, siempre es la grilla con imágenes. */
.view-toggle {
  display: none;
  gap: 8px;
  flex-shrink: 0;
}

.view-toggle-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--color-text-dark);
  background: transparent;
  color: var(--color-text-dark);
  transition: background var(--duration-fast) var(--ease-smooth),
              color var(--duration-fast) var(--ease-smooth);
}

.view-toggle-btn.active {
  background: var(--color-text-dark);
  color: var(--color-bg-light);
}

.view-toggle-btn svg {
  display: block;
}

/* ─── WORK INDEX LIST (list view) ──────────────────────────────────
   Reference: dennissnellenberg.com/work. Reuses .work-list (border-top
   dividers, list reset) and .project-row (link reset, focus outline)
   from the home page's list, with its own row layout — title / tags /
   year in one line instead of home's title+desc stacked with an arrow —
   and narrower vertical padding, since this list shows every project
   with filtering instead of home's short curated set. */
/* No border-top here — .work-list li already puts one on every <li>,
   the first row included, so adding another on the list itself just
   doubled the line right above it. Desktop-only (≥1025px) — mobile keeps
   the image grid instead, see .work-grid's own media query below. */
.work-index-list {
  display: none;
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 0 var(--container-pad) var(--space-xl);
}

/* Desktop only: .work-index-list starts hidden here, shown when
   [data-work-view="list"] on <main> (toggled by js/animations.js,
   defaults to "list" — see work.html). .work-grid's own hide-on-desktop
   rule lives with its base rule further down (ordering reasons, see the
   comment there); main[data-work-view="grid"] .work-grid below outranks
   that on specificity (2 selectors vs 1) to show it when picked. */
@media (min-width: 1025px) {
  .view-toggle {
    display: flex;
  }

  .work-index-list {
    display: none;
  }

  main[data-work-view="list"] .work-index-list {
    display: block;
  }

  main[data-work-view="grid"] .work-grid {
    display: grid;
  }
}

/* Title: fixed 410px (the literal Figma spec), not auto or a % — every
   row is its own independent grid (they're separate <a> elements, not
   rows of one shared grid), so "auto" sizes each title column to that
   row's own text and "%" still varies with nothing to lock it to, and
   either way tags/year drift out of alignment between rows (Dennis
   Snellenberg's reference keeps them locked). A fixed px is also the
   *correct* pairing here, not just the simplest: .work-index-title's
   font-size is a flat 32px, not a clamp(), so its rendered width never
   changes with viewport — a fluid column width against fixed-width
   content just guarantees a wrap/overflow at some size. Fixed here,
   fluid everywhere else that actually scales (padding, gaps) is the
   right split, not an inconsistency. */
.work-index-row {
  display: grid;
  grid-template-columns: 410px 1fr auto;
  align-items: center;
  gap: var(--space-lg);
  padding: 44px 64px;
}

.work-index-title {
  font-family: var(--font-headline);
  font-weight: 400;
  font-size: var(--fs-work-index-title);
  line-height: 1.05;
  text-transform: uppercase;
  color: var(--color-text-dark);
  white-space: nowrap;
}

.work-index-tags,
.work-index-year {
  font-family: var(--font-family);
  font-size: 16px;
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: var(--tracking-cta);
  text-transform: uppercase;
  color: var(--color-text-dark);
}

.work-index-tags {
  text-align: left;
}

.work-index-year {
  text-align: right;
}

.work-index-list .project-card.filtered {
  opacity: 0.2;
  pointer-events: none;
  transition: opacity var(--duration-base) var(--ease-smooth);
}

.work-counter {
  position: relative;
  display: flex;
  justify-content: space-between;
  align-items: center;
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 16px var(--container-pad) var(--space-lg);
  font-family: var(--font-family);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: var(--tracking-tight);
  color: var(--color-text-dark);
}

.work-counter::before {
  content: '';
  position: absolute;
  top: 0;
  left: var(--container-pad);
  right: var(--container-pad);
  height: 1px;
  background: var(--color-text-dark);
}

.filter-chip {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  border: none;
  font-family: var(--font-family);
  font-size: 16px;
  font-weight: 700;
  letter-spacing: var(--tracking-cta);
  line-height: var(--lh-uppercase);
  text-transform: uppercase;
  color: var(--color-text-dark);
  background: transparent;
  transition: color var(--duration-fast) var(--ease-smooth);
}

.filter-chip:hover,
.filter-chip.active {
  color: var(--color-highlight-on-light);
}

/* ─── WORK PAGE GRID (mobile only, <1025px) ───────────────────────── */

.work-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  column-gap: var(--space-md);
  row-gap: 120px;
  padding: 0 var(--container-pad) var(--space-xl);
  max-width: var(--container-max);
  margin: 0 auto;
}

/* Must come after the base rule above — same specificity, so source
   order decides, and this needs to win at ≥1025px. */
@media (min-width: 1025px) {
  .work-grid {
    display: none;
  }
}

@media (max-width: 768px) {
  .work-grid {
    row-gap: var(--space-md);
  }

  /* Same fixed-to-mobile downscale .project-row-title gets (--fs-headline-4
     -> --fs-headline-4-mobile); .project-title otherwise stays pinned at
     the desktop 40px card-title size all the way down to phone widths. */
  body.page-light .project-title {
    font-size: var(--fs-headline-4-mobile);
  }

  /* View toggle wraps below the (already-wrapping) filter chips instead of
     fighting them for horizontal space on a narrow screen. */
  .filter-bar-wrap {
    flex-wrap: wrap;
    row-gap: 16px;
  }

  /* Work index list: title/tags/year stack instead of 3 cramped columns. */
  .work-index-row {
    grid-template-columns: 1fr;
    justify-items: start;
    gap: 8px;
    padding: 24px 0;
  }

  .work-index-title {
    font-size: var(--fs-headline-4-mobile);
    white-space: normal;
  }

  .work-index-year {
    text-align: left;
  }
}

.work-grid .project-card {
  transition: opacity var(--duration-base) var(--ease-smooth),
              transform var(--duration-base) var(--ease-smooth);
}

.work-grid .project-card.filtered {
  opacity: 0.2;
  transform: scale(0.97);
  pointer-events: none;
}

.work-grid .project-img-wrap {
  aspect-ratio: 1/1;
}

/* ─── CASE STUDY ────────────────────────────────────────────────── */

.cs-hero {
  padding: calc(var(--nav-height) + var(--space-xl)) 0 120px;
}


/* BoxPoint rebuild spec: nav -> 80px -> title, an exact px gap rather than
   the fluid --space-xl the other (not-yet-migrated) case studies use. */
body.page-light .cs-hero {
  padding-top: calc(var(--nav-height) + 80px);
  padding-bottom: 0;
}

/* Switches off .cs-section's old 1fr/2fr grid + built-in horizontal
   padding — the new .cs-index-row/.cs-block-grid children are
   self-contained (own max-width + container-pad) like .cs-hero-title. */
body.page-light .cs-section {
  display: block;
  padding: 40px 0 120px;
}

.cs-hero-meta {
  display: flex;
  gap: var(--space-md);
  align-items: center;
  padding: 0 var(--container-pad);
  max-width: var(--container-max);
  margin: 0 auto var(--space-sm);
  flex-wrap: wrap;
}

.cs-client {
  font-size: var(--fs-h2);
  font-weight: 500;
  color: var(--color-text-soft);
}

.cs-year {
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: var(--tracking-label);
  color: var(--color-muted);
}

.cs-headline {
  padding: 0 var(--container-pad);
  max-width: var(--container-max);
  margin: 0 auto var(--space-lg);
}

/* New centered case-study hero (BoxPoint rebuild) — additive, new class
   names rather than restyling .cs-hero/.eyebrow/.cs-headline, since those
   are still used unscoped by the case studies not yet migrated to this
   layout. Order: title -> 160px -> eyebrow -> 20px -> image (the 80px
   nav->title gap lives on .cs-hero's own top padding, below). */

/* Centered display title — same H0 treatment (130px, PP Fragment, weight
   400, line-height 1.05) as the homepage hero marquee. Plain centered text
   with the font's own natural word-spacing (justify-stretch was tried
   first but spread "BOX"/"POINT" apart too far, reading as two unrelated
   words instead of one title). */
.cs-hero-title {
  display: block;
  width: 100%;
  max-width: var(--container-max);
  margin: 0 auto 160px;
  padding: 0 var(--container-pad);
  text-align: center;
  white-space: nowrap;
  font-family: var(--font-headline);
  font-size: var(--fs-headline-0);
  font-weight: 400;
  text-transform: uppercase;
  line-height: 1.05;
  color: var(--color-text-dark);
}

/* "STARBUCKS RESERVE" at the shared 130px measures ~1419px, wider than
   the ~1280px available at desktop container-max — every other
   .cs-hero-title (Box Point, MyFitnessPal, Atipikal) is short enough to
   fit as-is, so this scales down just for this one instead of touching
   the shared size. Mobile's own clamp(40px,14vw,72px) override below
   still wins under 768px regardless. */
.cs-hero-title--compact {
  font-size: 110px;
}

.cs-hero-eyebrow {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  margin-bottom: 20px;
  font-family: var(--font-family);
  text-transform: uppercase;
}

.cs-hero-eyebrow-label {
  font-size: 14px;
  font-weight: 500;
  letter-spacing: var(--tracking-h1);
  color: var(--color-muted);
}

.cs-hero-eyebrow-value {
  font-size: 14px;
  font-weight: 700;
  letter-spacing: var(--tracking-h1);
  color: var(--color-muted);
}

.cs-hero-image {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 0 var(--container-pad);
  overflow: hidden;
}

.cs-hero-image img,
.cs-hero-image .img-placeholder {
  width: 100%;
  height: auto;
  display: block;
}

.cs-overview {
  padding: var(--space-xl) var(--container-pad);
  max-width: var(--container-max);
  margin: 0 auto;
  display: grid;
  grid-template-columns: 1fr 2fr;
  gap: var(--space-xl);
  position: relative;
}
.cs-overview::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: var(--container-pad);
  right: var(--container-pad);
  height: 1px;
  background: var(--color-border);
  pointer-events: none;
}

.cs-overview-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-md);
}

.cs-stat {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.stat-value {
  font-size: var(--fs-body-lg);
  font-weight: 500;
  color: var(--color-text-soft);
}

.cs-section {
  display: grid;
  grid-template-columns: 1fr 2fr;
  gap: 0 var(--space-xl);
  align-items: start;
  padding: 120px var(--container-pad);
  max-width: var(--container-max);
  margin: 0 auto;
}

.cs-section-label {
  padding-top: 6px;
}

.cs-section-label .eyebrow,
.cs-related > .eyebrow {
  font-size: 30px;
  font-weight: 500;
  text-transform: none;
  letter-spacing: 0;
  color: var(--color-text-soft);
}

.cs-section-content {
  /* right column — h2 + text */
}

.cs-section-images {
  grid-column: 1 / -1;
  margin-top: 120px;
}

.cs-section--gallery {
  display: block;
  padding-top: 0;
  padding-bottom: 0;
}

.cs-section:not(.cs-section--gallery) + .cs-section:not(.cs-section--gallery) {
  padding-top: 0;
}

.cs-hero + .cs-section {
  padding-top: 0;
}

.cs-section-title {
  font-family: var(--font-family);
  font-size: var(--fs-h2);
  font-weight: 500;
  letter-spacing: 0;
  margin: 0 0 var(--space-lg);
}

.cs-content-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-xl);
  align-items: center;
}

/* BoxPoint rebuild — numbered section-index divider + 2-col content block.
   New classes rather than restyling .cs-section/.cs-section-label/
   .cs-content-grid, since those keep their 1:2-ratio layout on the case
   studies not yet migrated. Scoped to body.page-light: .cs-section
   switches to plain block flow (see the override further down) so these
   self-contained, self-padded children aren't fighting its old grid. */
/* border-top would sit at the box's outer edge (the full 1440px), not
   inset by container-pad — same "wider than the content" gotcha .work-
   counter had. The divider needs to match .cs-hero-image's own visible
   width (1440 - 2*container-pad = 1280px at full desktop), so it's a
   pseudo-element inset by container-pad instead of a native border. */
.cs-index-row {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-xl);
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 16px var(--container-pad) 0;
  font-family: var(--font-family);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: var(--tracking-tight);
  text-transform: uppercase;
  color: var(--color-text-dark);
}

.cs-index-row::before {
  content: '';
  position: absolute;
  top: 0;
  left: var(--container-pad);
  right: var(--container-pad);
  height: 1px;
  background: var(--color-text-dark);
}

.cs-index-label::before {
  content: '/';
}

.cs-block-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-xl);
  align-items: start;
  max-width: var(--container-max);
  margin: 160px auto 0;
  padding: 0 var(--container-pad);
}

/* Gallery that follows a .cs-block-grid (sections 02/03 pattern) — same
   1280px visible width as .cs-hero-image/.cs-index-row, 160px gap up
   from the content block above it. */
.cs-gallery-block {
  max-width: var(--container-max);
  margin: 160px auto 0;
  padding: 0 var(--container-pad);
}

.cs-block-title {
  margin-bottom: 40px;
  font-family: var(--font-headline);
  font-size: var(--fs-headline-4);
  font-weight: 400;
  line-height: var(--lh-heading);
  text-transform: uppercase;
  color: var(--color-text-dark);
}

/* Per-breakpoint manual line breaks for .cs-block-title, where the site's
   blanket "h1 br, h2 br: none on mobile" rule (natural wrap) produces an
   orphan line at 40px. Hidden by default (desktop uses the plain <br>
   already in the markup); the mobile media query flips both. A single
   class beats the two-type-selector "h2 br" rule on specificity, so this
   wins even inside that same query. */
.cs-break-mobile {
  display: none;
}

/* Break point shared by both breakpoints (same position desktop and
   mobile) — needs no media query, a plain class always outranks the
   "h1 br, h2 br: none" element-selector rule on specificity. */
.cs-break-always {
  display: initial;
}

/* Test: unify with the Section 01 display headline (PP Fragment,
   fs-headline-4, uppercase) instead of the smaller Scto Grotesk
   treatment. No overrides needed — inherits .cs-block-title as-is. */
.cs-block-title--h2 {
}

/* ─── PAGE — ABOUT (dark reuse of the case-study cs-* structure) ──── */
/* About runs the same cs-section/cs-index-row/cs-block-grid structure as
   the case studies, but on the site's default dark theme instead of
   page-light — these are the pieces built assuming a light bg
   (.cs-section's display switch, and the hardcoded --color-text-dark /
   --color-muted on .cs-index-row/.cs-block-title/.cs-overview-text).
   Everything else (.cs-block-grid, .cs-gallery-block, .cs-two-images)
   already works unscoped on either background. */
body.page-about .cs-section {
  display: block;
  padding: 40px 0 120px;
}

body.page-about .cs-index-row {
  color: var(--color-text-pink);
}

body.page-about .cs-index-row::before {
  background: var(--color-text-pink);
}

body.page-about .cs-block-title {
  color: var(--color-text-pink);
}

/* Body copy matches the nav's pink exactly (not --color-muted's gray,
   nor --color-text-soft's softer tint) — per explicit request. */
body.page-about .cs-overview-text {
  color: var(--color-text-pink);
}

.cs-tags {
  display: flex;
  flex-direction: column;
  gap: 8px;
  list-style: none;
  margin: 0;
  padding: 0;
}

.cs-tags li {
  font-family: var(--font-family);
  font-size: 14px;
  font-weight: 400;
  line-height: 1.15;
  letter-spacing: var(--tracking-h1);
  text-transform: uppercase;
  color: var(--color-text-dark);
}

.cs-block-col p.cs-overview-text + p.cs-overview-text {
  margin-top: 1.5em;
}

.cs-text {
  font-size: var(--fs-body-lg);
  color: var(--color-muted);
  line-height: var(--lh-body-lg);
  max-width: 680px;
  margin-bottom: 0;
}

.cs-image-wrap {
  overflow: hidden;
}

.cs-image-wrap img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.8s var(--ease-smooth);
}

.cs-image-wrap:hover img {
  transform: scale(1.04);
}

/* Logo wordmark card — .cs-image-wrap's cover/16:10 pairing is built for
   photos; a wide wordmark (e.g. atk-logo.svg, ~10.5:1) needs contain +
   its own frame instead of the crop/zoom that pairing produces.
   Background: static light-pink wash + a soft blurred blob (::before)
   that drifts slowly back and forth (ref: wearemotto.com/portfolio/
   cloudbees). Built from the site's own --color-text-pink via
   color-mix() instead of a new hardcoded hue — atk-logo.svg is fill
   #333333 (dark), drawn for a light backdrop like this one, not the flat
   dark card this replaces. */
.cs-brand-card {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-lg);
  background: color-mix(in srgb, var(--color-text-pink) 30%, var(--color-bg-light) 70%);
}

.cs-brand-card::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 40%;
  width: 65%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle,
    color-mix(in srgb, var(--color-text-pink) 75%, var(--color-text-dark) 25%) 0%,
    transparent 70%);
  filter: blur(60px);
  opacity: 0.85;
  pointer-events: none;
  transform: translate(-50%, -50%);
  animation: cs-brand-blob-drift 13s ease-in-out infinite alternate;
}

.cs-brand-card img {
  position: relative;
  width: 100%;
  height: auto;
  object-fit: contain;
  transition: none;
}

.cs-brand-card:hover img {
  transform: none;
}

@keyframes cs-brand-blob-drift {
  from { transform: translate(-65%, -58%); }
  to   { transform: translate(-35%, -42%); }
}

@media (prefers-reduced-motion: reduce) {
  .cs-brand-card::before { animation: none; }
}

/* ─── Overview Deck Panel ────────────────────────────────────────────── */
.cs-deck-panel {
  background: var(--color-bg-dark);
  cursor: default;
  overflow: visible;
  clip-path: inset(0 0 0 -100vw);
}

.cs-deck-panel:hover .cs-deck-card img {
  transform: none;
}

.cs-deck-stage {
  position: relative;
  width: 100%;
  height: 100%;
  perspective: 1200px;
  perspective-origin: 50% 50%;
  overflow: visible;
}

.cs-deck-card {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 62%;
  aspect-ratio: 16/10;
  overflow: hidden;
  transform-origin: center center;
  will-change: transform, opacity;
}

.cs-deck-card img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: none;
}

/* ─── Scroll Simulation ──────────────────────────────────────────────── */
/* background: var(--color-bg-dark) stays as the fallback/backdrop — some
   instances (starbucks-reserve.html) add a background-image inline per
   case, sized/positioned here so it actually fills the frame instead of
   tiling at native size. */
.cs-scroll-sim {
  background-color: var(--color-bg-dark);
  background-size: cover;
  background-position: center;
  display: flex;
  align-items: center;
  justify-content: center;
}

.cs-scroll-sim:hover img {
  transform: none;
}

.cs-scroll-sim-window {
  width: 75%;
  aspect-ratio: 16/9;
  overflow: hidden;
  position: relative;
}

.cs-scroll-sim-track {
  width: 100%;
}

.cs-scroll-sim-track img {
  width: 100%;
  height: auto;
  display: block;
  transition: none;
}

/* ─── Drag Simulation (Projects UI) ─────────────────────────────────── */
.cs-drag-sim {
  background: var(--color-bg-dark);
  display: flex;
  align-items: center;
  justify-content: center;
}

.cs-drag-sim:hover img { transform: none; }

.cs-drag-sim-wrap {
  width: 75%;
  aspect-ratio: 900/506;
  position: relative;
  overflow: hidden;
}

.cs-drag-sim-window {
  position: absolute;
  top: 0; left: 0;
  width: 900px;
  height: 506px;
  transform-origin: top left;
  overflow: hidden;
}

.ds-chrome {
  position: absolute;
  top: 0; left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
  user-select: none;
  z-index: 0;
  transition: none;
}

.ds-carousel {
  position: absolute;
  top: 58px;
  bottom: 46px;
  left: 0; right: 0;
  overflow: hidden;
  display: flex;
  align-items: center;
  z-index: 1;
}

.ds-track {
  display: flex;
  gap: 16px;
  padding: 0 36px;
  align-items: center;
  will-change: transform;
}

.ds-card {
  flex-shrink: 0;
  width: 180px;
  height: 220px;
  overflow: hidden;
  background: var(--color-border);
}

.ds-card img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
  user-select: none;
  transition: none;
}

.cs-phases {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 2px;
  position: relative;
  margin-top: var(--space-lg);
}

.cs-phases::before {
  content: '';
  position: absolute;
  top: 24px;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--color-border);
  z-index: 0;
}

.cs-phase {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  padding: var(--space-md);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
}

.phase-name {
  font-size: var(--fs-body-lg);
  font-weight: 500;
  color: var(--color-text-soft);
}

.cs-gallery {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-sm);
  margin-top: var(--space-lg);
}

.cs-gallery-item {
  overflow: hidden;
  aspect-ratio: 4/3;
  background: var(--color-surface);
}

.cs-gallery-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.8s var(--ease-smooth);
}

.cs-gallery-item:hover img {
  transform: scale(1.05);
}

.cs-metrics-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-md);
  margin-top: var(--space-lg);
}

.cs-metric-card {
  padding: var(--space-md);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.cs-next {
  padding: var(--space-xl) var(--container-pad);
  max-width: var(--container-max);
  margin: 0 auto;
}

.cs-next-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-lg) 0;
  border-bottom: 1px solid var(--color-border);
  border-top: 1px solid var(--color-border);
}

.cs-next-title {
  font-family: var(--font-headline);
  font-size: var(--fs-headline-2);
  font-weight: 500;
  letter-spacing: var(--tracking-h1);
  color: var(--color-text-soft);
  transition: color var(--duration-base) var(--ease-smooth);
}

.cs-next-arrow {
  font-size: clamp(32px, 4vw, 52px);
  color: var(--color-muted);
  transition: color var(--duration-fast) var(--ease-smooth),
              transform var(--duration-fast) var(--ease-smooth);
}

.cs-next-link:hover .cs-next-arrow {
  color: var(--color-highlight);
  transform: translateX(8px);
}

.cs-next-link:hover .cs-next-title {
  color: var(--color-highlight);
}

/* ─── CASE STUDY — ULRYCH STRUCTURE ─────────────────────────────── */

.cs-hero .eyebrow {
  display: block;
  padding: 0 var(--container-pad);
  max-width: var(--container-max);
  margin: 0 auto var(--space-sm);
}

.cs-tagline {
  padding: 0 var(--container-pad);
  max-width: var(--container-max);
  margin: 0 auto var(--space-xl);
  font-size: var(--fs-body-lg);
  color: var(--color-muted);
  line-height: var(--lh-body-lg);
}

.cs-meta-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  max-width: calc(var(--container-max) - 2 * var(--container-pad));
  margin: 0 auto var(--space-xl);
  border-top: 1px solid var(--color-border);
}

.cs-meta-item {
  padding: var(--space-md);
  border-right: 1px solid var(--color-border);
}

.cs-meta-item:last-child { border-right: none; }

.cs-meta-label {
  display: block;
  font-size: var(--fs-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
  margin-bottom: 8px;
}

.cs-meta-value {
  font-size: var(--fs-body);
  font-weight: 500;
  color: var(--color-text-soft);
}

.cs-overview-text {
  font-size: var(--fs-body-lg);
  color: var(--color-muted);
  line-height: var(--lh-body-lg);
  max-width: 540px;
  margin-bottom: 0;
}

/* Numbered list variant of .cs-overview-text — same body typography,
   with a bold dark number standing in for the bullet (matches the
   weight of .cs-index-num elsewhere). First used by Hotel Berna's "El
   desafío" (5 concrete friction points read better as a list than
   fused into prose). */
.cs-overview-list {
  list-style: none;
  counter-reset: cs-overview-list;
  display: flex;
  flex-direction: column;
  gap: 1em;
  margin-top: 1.5em;
  max-width: 540px;
}

.cs-overview-list li {
  counter-increment: cs-overview-list;
  position: relative;
  padding-left: 2em;
  font-size: var(--fs-body-lg);
  font-weight: 400;
  color: var(--color-text-dark);
  line-height: var(--lh-body-lg);
}

.cs-overview-list li::before {
  content: counter(cs-overview-list) '.';
  position: absolute;
  left: 0;
  font-weight: 400;
  color: var(--color-text-dark);
}

/* Sub-phases within a single .cs-block-col (Hotel Berna's "Proceso":
   Descubrir/Definir/Idear/Entregar) — a small subheading per phase
   (20px Medium, same family/weight recipe as .cs-service-item h3),
   followed by its own paragraph(s), for process narratives too long
   for the single-title pattern the other case studies use. */
.cs-process-phases {
  display: flex;
  flex-direction: column;
  gap: 3em;
}

/* Hotel Berna's Proceso leads with an intro paragraph before the phases,
   so the block needs its own top margin to separate from it. Starbucks
   Reserve's Proceso has no intro line — .cs-process-phases is the column's
   first child there, and this margin would only push it out of alignment
   with the h2 in the column next to it. Scoped to the adjacent-sibling
   case so each page gets the right spacing without a page-specific class. */
.cs-overview-text + .cs-process-phases {
  margin-top: 3em;
}

.cs-process-phase h3 {
  margin-bottom: 0.5em;
  font-family: var(--font-family);
  font-weight: 500;
  font-size: 20px;
  line-height: var(--lh-subhead);
  letter-spacing: var(--tracking-h1);
  color: var(--color-text-dark);
}

.cs-process-phase p {
  font-size: var(--fs-body-lg);
  font-weight: 400;
  color: var(--color-text-dark);
  line-height: var(--lh-body-lg);
  max-width: 540px;
}

.cs-process-phase p + p {
  margin-top: 1.5em;
}

/* About's "02 Servicios" — a repeating title+paragraph pair, one per
   service. Sits in a .cs-block-grid's right column same as
   .cs-overview-text does elsewhere, just structured as a list instead of
   plain paragraphs. */
.cs-service-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
}

/* Same treatment as .service-card-title (home page) — its --fs-h3 token
   is literally commented "service-card-title", clamping to 24px. */
.cs-service-item h3 {
  margin-bottom: 8px;
  font-family: var(--font-family);
  font-size: var(--fs-h3);
  font-weight: 500;
  letter-spacing: var(--tracking-h1);
  line-height: var(--lh-subhead);
  color: var(--color-text-pink);
}

.cs-service-item p {
  font-size: var(--fs-body-lg);
  color: var(--color-text-pink);
  line-height: var(--lh-body-lg);
  max-width: 680px;
}

.cs-two-images {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  overflow: hidden;
}

/* Square crop — validated on BoxPoint, now the shared treatment for every
   two-image gallery block (case studies + About). */
.cs-two-images .cs-image-wrap { aspect-ratio: 1/1; }

/* ─── MFP OUTCOME CYCLE — "resultado 1" and "resultado 2" (Resultados) ──
   Stacked screenshots cycling in place, phase-shifted per image via
   negative animation-delay against one shared keyframe instead of a
   separate keyframe per image. Default (no modifier) is the 3-frame/15s
   version (resultado 2); .mfp-outcome-cycle--2 below overrides just the
   timing pieces for the 2-frame/10s version (resultado 1) — same base
   positioning either way. .cs-image-wrap needs position:relative here
   since its absolutely-positioned children (this component only) would
   otherwise anchor to the nearest positioned ancestor instead. */
.mfp-outcome-cycle {
  position: relative;
  background: #F9F4EE;
}

.mfp-outcome-cycle-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  animation: mfpOutcomeCycle 15s step-end infinite;
}

.mfp-outcome-cycle-img:nth-child(1) { animation-delay: 0s; }
.mfp-outcome-cycle-img:nth-child(2) { animation-delay: -5s; }
.mfp-outcome-cycle-img:nth-child(3) { animation-delay: -10s; }

/* Each image "owns" one 5s third of the loop (0–33.333%, then off until
   its turn comes back around at 100%/0%) — hard cut, no crossfade. The
   cutoff has to land exactly on 33.3333% (always 1/3, regardless of the
   15s total), matching the -5s/-10s delays exactly — an early cutoff (33%
   flat) left a gap where all 3 images were opacity:0 at once, flashing
   the wrap's background through. */
@keyframes mfpOutcomeCycle {
  0%, 33.3333%      { opacity: 1; }
  33.3334%, 100%    { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .mfp-outcome-cycle-img,
  .mfp-toggle-state {
    animation: none;
  }

  .mfp-outcome-cycle-img:first-child,
  .mfp-toggle-state--1 {
    opacity: 1;
  }
}

/* ─── MFP TOGGLE PILL — "resultado 1" (Desayuno/Almuerzo) ──────────────
   GSAP-driven per spec — no CSS transitions on the indicator, JS
   (js/animations.js, "MFP TOGGLE PILL" section) computes and animates its
   x/width. container-type lets the 40px-at-a-1280-wide-reference spec
   scale as cqw instead of a fixed px, same approach as .mfp-marquee. */
.mfp-toggle-wrap {
  container-type: inline-size;
  position: relative;
}

.mfp-toggle-photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
}

.mfp-toggle-photo.is-active {
  opacity: 1;
}

.toggle-pill {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 2;
  display: flex;
  background: rgba(255, 255, 255, 0.7);
  border-radius: 100px;
  padding: 0.469cqw;
}

.toggle-option {
  position: relative;
  z-index: 1;
  padding: 1.6cqw 3.1cqw;
  border: none;
  background: none;
  border-radius: 100px;
  font-family: 'DM Sans', var(--font-family);
  font-size: 3.125cqw;
  font-weight: 400;
  letter-spacing: -0.02em;
  color: rgba(56, 56, 56, 0.6);
  cursor: pointer;
  white-space: nowrap;
}

.toggle-option.active {
  font-weight: 500;
  color: #fff;
}

.toggle-option.active::after {
  content: '';
  display: inline-block;
  width: 4cqw;
  height: 4cqw;
  margin-left: 0.8cqw;
  vertical-align: -0.7cqw;
  background: url('/assets/images/myfitnesspal/icon-check.svg') no-repeat center / contain;
}

/* No transition here — GSAP owns every bit of this element's movement. */
.toggle-indicator {
  position: absolute;
  top: 0.469cqw;
  bottom: 0.469cqw;
  left: 0.469cqw;
  z-index: 0;
  background: #2D2D2D;
  border-radius: 100px;
}

/* ─── MFP MARQUEE — "resultado 2" (myfitnesspal.html only) ─────────────
   Two text+avatar rows scrolling opposite directions, reusing the same
   marqueeScroll keyframe as .hero-marquee/.marquee-double. container-type
   lets every size (text, avatars, veg cutouts) scale in cqw off this
   element's own width instead of a fixed px tuned to one viewport — the
   spec's "200px" was against a 1280px-wide reference frame (200/1280 =
   15.625cqw), same ratio for the rest. Lives inside .cs-image-wrap, whose
   own 1:1 aspect-ratio and overflow:hidden (see .cs-two-images above and
   .cs-image-wrap below) already give it the right box + clipping — this
   only needs to override that base rule's img sizing/hover-zoom, which
   would otherwise stretch the nested avatar/veg <img> tags to fill it. */
.mfp-marquee {
  container-type: inline-size;
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0;
  background: #F9F4EE;
}

/* Two classes deep (.mfp-marquee .mfp-marquee-veg--x), not one — has to
   outrank .cs-image-wrap img's width:100%/height:100% (a class+type
   selector, same specificity weight as two plain classes but earlier in
   the file loses ties): a single .mfp-marquee-veg--brocoli class alone
   sits *below* that rule and would render at the source file's raw pixel
   size instead of this scaled one. */
.mfp-marquee .mfp-marquee-veg {
  position: absolute;
  z-index: 2;
  height: auto;
  pointer-events: none;
  filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.18));
}

.mfp-marquee .mfp-marquee-veg--brocoli {
  top: 8cqw;
  left: 9cqw;
  width: 24cqw;
  transform: rotate(-6deg);
}

.mfp-marquee .mfp-marquee-veg--tomate {
  bottom: 2cqw;
  right: 9cqw;
  width: 34cqw;
}

/* height must be >= the avatar's own 17cqw — otherwise overflow:hidden
   (needed to mask the horizontal scroll loop) also clips the avatar
   top/bottom, since the row's height would default to the shorter text
   line instead. */
.mfp-marquee-row {
  position: relative;
  z-index: 1;
  overflow: hidden;
  width: 100%;
  height: 17cqw;
}

.mfp-marquee-track {
  display: flex;
  align-items: center;
  width: max-content;
}

.mfp-marquee-row--1 .mfp-marquee-track {
  animation: marqueeScroll 22s linear infinite;
}

.mfp-marquee-row--2 .mfp-marquee-track {
  animation: marqueeScroll 22s linear infinite reverse;
}

@media (prefers-reduced-motion: reduce) {
  .mfp-marquee-track {
    animation: none;
  }
}

.mfp-marquee-item {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  gap: 3cqw;
  padding-right: 3cqw;
}

.mfp-marquee-text {
  font-family: 'DM Sans', var(--font-family);
  font-weight: 600;
  font-size: 15.625cqw;
  line-height: 1;
  letter-spacing: -0.05em;
  white-space: nowrap;
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
}

.mfp-marquee-text--1 {
  background-image: linear-gradient(90deg, #EFA12F, #FF6835);
}

.mfp-marquee-text--2 {
  background-image: linear-gradient(90deg, #FF6835, #8754B7);
}

.mfp-marquee .mfp-marquee-avatar {
  width: 17cqw;
  height: 17cqw;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
}

.cs-showcase-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}

/* A big hero-style image stacked below a 3-image showcase row —
   validated on BoxPoint's "El desafío"/"Proceso" galleries, now the
   shared treatment. .cs-hero-image already gives the full-bleed/
   natural-ratio treatment; this just adds the gap needed when it
   follows another gallery element in the same block instead of
   standing alone, and locks it to the same 2:1 crop as the real hero. */
.cs-hero-image--inline {
  margin-top: 8px;
  padding: 0;
  max-width: none;
  aspect-ratio: 2/1;
}

.cs-hero-image--inline img {
  height: 100%;
  object-fit: cover;
}

.cs-hero-image--inline .img-placeholder {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}


.cs-section-images .cs-showcase-grid {
  margin-top: 0;
}

.cs-showcase-item {
  overflow: hidden;
  aspect-ratio: 3/4;
  background: var(--color-surface);
}

.cs-showcase-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.8s var(--ease-smooth);
}

.cs-showcase-item:hover img { transform: scale(1.04); }

.cs-related {
  padding: var(--space-xl) var(--container-pad);
  max-width: var(--container-max);
  margin: 0 auto;
  position: relative;
}
.cs-related::before {
  content: '';
  position: absolute;
  top: 0;
  left: var(--container-pad);
  right: var(--container-pad);
  height: 1px;
  background: var(--color-border);
  pointer-events: none;
}

.cs-related-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-sm);
  margin-top: var(--space-lg);
}

.cs-related-grid .project-img-wrap { aspect-ratio: 16/10; }

/* ─── CONTACT PAGE ──────────────────────────────────────────────── */
/* page-light like work.html/case studies — reuses .page-hero (grid,
   headline-1 exception) plus the cs-section/cs-index-row/cs-block-grid
   structure from About/case studies, adding only what neither already
   covers: the hero's image column and the form itself. */

.contact-form {
  display: flex;
  flex-direction: column;
  gap: 56px;
}

.contact-form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 var(--space-xl);
}

.contact-submit {
  align-self: flex-start;
  margin-top: var(--space-sm);
}

.form-group {
  display: flex;
  flex-direction: column;
  gap: 24px;
}

/* Matches .footer-label exactly, per request — same eyebrow treatment
   reused across the form's field labels. */
.form-group label {
  font-family: var(--font-family);
  font-weight: 700;
  font-size: 14px;
  line-height: 1.05;
  letter-spacing: -0.03em;
  text-transform: uppercase;
  color: var(--color-text-dark);
}

.form-group input,
.form-group textarea {
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--color-text-dark);
  color: var(--color-muted);
  padding: 0 0 24px;
  font-size: 20px;
  font-family: var(--font-family);
  letter-spacing: -0.02em;
  transition: border-color var(--duration-fast) var(--ease-smooth);
  resize: none;
  width: 100%;
}

.form-group input:focus,
.form-group textarea:focus {
  outline: none;
  border-bottom-color: var(--color-text-dark);
}

.form-group input::placeholder,
.form-group textarea::placeholder {
  color: var(--color-muted);
  opacity: 0.7;
}

.form-group textarea {
  min-height: 140px;
}

/* CONTACTO/SOCIAL block under the "¿Y ahora qué?" title — reuses
   .footer-col/.footer-group/.footer-label/.footer-mail/.footer-social
   as-is (already styled for a light bg, since .footer-cta is always
   light regardless of page theme). */
.contact-info {
  margin-top: var(--space-lg);
}

/* ─── PLACEHOLDER IMAGES ────────────────────────────────────────── */

.img-placeholder {
  background: var(--color-surface);
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-muted);
  font-size: var(--fs-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  border: 1px dashed var(--color-border);
}

/* ─── RESPONSIVE — TABLET ───────────────────────────────────────── */

@media (max-width: 1024px) {
  .cs-overview {
    grid-template-columns: 1fr;
    gap: var(--space-lg);
  }

  .cs-phases {
    grid-template-columns: repeat(2, 1fr);
  }

  .cs-metrics-grid {
    grid-template-columns: repeat(3, 1fr);
  }

  .cs-meta-grid    { grid-template-columns: repeat(2, 1fr); }
  .cs-showcase-grid { grid-template-columns: repeat(2, 1fr); }
}

/* ─── RESPONSIVE — MOBILE ───────────────────────────────────────── */

@media (max-width: 768px) {
  /* Nav */
  .nav-right {
    display: none;
  }

  .nav-burger {
    display: flex;
  }

  /* Logo: hide title on small screens */
  .nav-logo-sep,
  .nav-logo-title {
    display: none;
  }

  .nav-container {
    height: 68px;
  }

  .nav-logo-name {
    letter-spacing: -0.03em;
  }

  /* Hero — matches the Figma mobile frame (375×666, see mockup) but keeps
     height fluid (min-height, not a hard height) so it doesn't leave a
     bare strip of background on taller phones (iPhone 14/15 etc. are
     ~812-932px, well past 666). The desktop layout uses absolute
     positioning for the image/text/marquee stack because they overlap in
     specific ways at that size; on mobile the Figma spec has them stack
     top-to-bottom instead, so the three pieces switch to normal flow
     here. `.hero-inner` uses `display:contents` so its two children
     (.hero-image-wrap, .hero-descriptor-wrap) become direct flex items of
     .hero, alongside the sibling .hero-marquee — letting `order` interleave
     the marquee between the image and the arrow/body text despite it
     living outside .hero-inner in the DOM (needed on desktop so it can
     span the full section width, unconstrained by .hero-inner's padding). */
  .hero {
    display: flex;
    flex-direction: column;
    align-items: center;
    height: auto;
    min-height: 100vh;
    padding-bottom: 80px;
  }

  .hero-inner {
    display: contents;
  }

  /* FULL-BLEED: photo covers the whole mobile hero instead of the old
     256×350 box (Figma's original stacked spec) — same treatment as
     desktop. .hero-marquee/.hero-descriptor-wrap switch from static to
     relative below so their z-index:var(--z-above) (set in the shared
     desktop rules above) actually applies again and lifts them above
     the photo; `static` would silently drop it. */
  .hero-image-wrap {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    margin-top: 0;
  }

  /* margin-top (not a fixed px) roughly reproduces where the marquee sat
     against the old 256×350 image box (~330px into a 666px Figma frame,
     ≈48%) — now there's no image box to anchor against, so this scales
     with the viewport instead of assuming one phone height. */
  .hero-marquee {
    order: 2;
    position: relative;
    /* Neutralizes the desktop rule's bottom:15vh, which still applies to
       relative positioning (shifting the box up by that amount from its
       flow position) now that position is no longer static — was
       silently pulling the marquee 126.6px higher than its margin-top
       actually specified. */
    bottom: auto;
    width: 100%;
    margin-top: 52vh;
    padding: 0;
  }

  .hero-marquee-group span {
    font-size: 80px;
  }

  .hero-descriptor-wrap {
    order: 3;
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    width: 100%;
    padding: 0 var(--container-pad);
  }

  .hero-arrow {
    margin-top: 32px;
    margin-bottom: 32px;
  }

  .hero-descriptor {
    max-width: 100%;
    font-size: 18px;
    line-height: 1.5;
    letter-spacing: var(--tracking-tight);
  }

  /* .marquee-double (which used to separate .work-section from
     .services-section) is hidden on mobile — see below — leaving them
     directly adjacent. Both sides carrying their own var(--space-xl)
     summed to a 128px gap that read as an outlier next to the 56px
     rhythm between cards. padding-bottom:0 here means only
     .services-section's own top padding provides the gap now, same
     "one side only" pattern used for .footer-cta below. */
  .work-section {
    padding-bottom: 0;
  }

  .work-inner {
    padding: 0 20px;
  }

  /* Stacks image → title → body text instead of desktop's row layout
     (title/desc on the left, arrow on the right). padding-top:20px is the
     line-to-image gap (the divider itself is .work-list li's border-top);
     padding-bottom:56px is the space-between-cards gap, since each card's
     own divider line (the next li's border-top) immediately follows. */
  .project-row {
    flex-direction: column;
    align-items: flex-start;
    padding: 20px 0 56px;
    gap: 0;
  }

  .project-row-image {
    display: block;
    width: 100%;
    aspect-ratio: 335 / 335;
    object-fit: cover;
    margin-bottom: 32px;
  }

  .project-row-text {
    gap: 4px;
  }

  .project-row-title {
    font-size: var(--fs-headline-4-mobile);
    letter-spacing: 0;
  }

  .project-row-desc {
    line-height: 1.2;
    letter-spacing: var(--tracking-tight);
  }

  .project-row-arrow {
    display: none;
  }

  .project-img-wrap {
    aspect-ratio: 335 / 207;
  }

  .work-grid {
    grid-template-columns: 1fr;
  }

  /* Footer email line break */
  .footer-email {
    word-break: break-all;
  }

  /* Footer — single column, CTA block first, name hidden. A full
     --space-xl top here would fully double up with .cta-showcase's own
     --space-xl bottom right above it, but 0 read as cramped — the heading
     started right at the curve with no breathing room at all. --space-lg
     gives real air without quite matching --space-xl, so it still reads
     as more connected to the CTA above than a standalone section gap. */
  .footer-cta {
    padding-top: var(--space-lg);
    padding-bottom: 24px;
  }

  /* The border-radius ellipse is sized in absolute px, so the same 110px
     height reads as a much steeper arc against a 375px-wide viewport than
     it does on a 1440px+ desktop — flatten both the height and the
     vertical radius fraction so the curve stays subtle here too. */
  .footer-reveal-cap {
    height: 48px;
    border-radius: 0 0 50% 50% / 0 0 26% 26%;
  }

  .footer-cta-inner {
    padding: 0 20px;
    display: flex;
    flex-direction: column;
    gap: 56px;
  }

  .footer-col:first-of-type {
    order: 2;
  }

  .footer-col:last-of-type {
    order: 1;
  }

  .footer-name {
    display: none;
  }

  .footer-label {
    font-size: 16px;
    line-height: 1.05;
    margin-bottom: 16px;
  }

  .footer-social a + a {
    margin-top: 8px;
  }

  .footer-cta-heading {
    font-size: 24px;
    line-height: 1.3;
  }

  .footer-cta-btn {
    height: 34px;
  }

  .site-footer {
    margin-top: 56px;
    padding: 24px 20px 0;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
  }

  /* CTA Showcase — media collage scaled down (~26%) from desktop,
     same relative positions/proportions and parallax mechanism, just
     recalculated for a 335px content width instead of 1280px.
     Steps down from --space-3xl (desktop's biggest tier, would resolve
     to a 160px floor here) to --space-xl — mobile screens don't have the
     room to spare for the extra emphasis, and it kept this section from
     matching the rhythm of everything around it. */
  .cta-showcase {
    /* padding-top can't shrink to --space-xl's ~64px floor here: .cta-media-2
       ("ancla" piece, top:0) travels data-y-mobile="-86px" on scroll, and
       .cta-showcase has overflow:hidden — anything short of 86px of
       clearance turns its intended "peeking above" residual into a hard
       clip mid-shape. 96px covers the worst case (item-2) with a small
       margin, still on the 4px grid. */
    padding-top: 96px;
    padding-bottom: var(--space-xl);
  }

  .cta-showcase-media-grid {
    height: 253px;
  }

  .cta-media-1 { left: 0;    top: 68px;  width: 132px; height: 74px; }
  .cta-media-2 { left: 102px; top: 0;    width: 131px; height: 223px; }
  .cta-media-3 { left: 225px; top: 16px; width: 61px;  height: 86px; }
  .cta-media-4 { left: 0;    top: 184px; width: 132px; height: 89px; }
  .cta-media-5 { left: 203px; top: 172px; width: 132px; height: 132px; }

  .cta-showcase-content {
    max-width: 100%;
    margin-top: 32px;
  }

  .cta-showcase-heading {
    font-size: 40px;
    line-height: 1.03;
  }

  .cta-showcase-text {
    margin-top: 40px;
    font-size: 20px;
    line-height: 1.5;
  }

  .cta-showcase-btn {
    margin-top: 40px;
    height: 34px;
  }

  /* Case study */

  /* .cs-hero-title has no mobile size at all otherwise — fixed 130px +
     white-space:nowrap (needed on desktop so the name reads as one
     word, not wrapped) forces an overflow/cutoff on any narrow
     viewport. Fluid size + allow wrapping as the safety net for long
     names (MyFitnessPal) that still won't fit on one line down here.
     But "MyFitnessPal" is a single unbroken token — white-space:normal
     alone has no space to wrap at, so it still overflowed. overflow-wrap
     lets it break mid-word only when nothing else fits. */
  .cs-hero-title {
    font-size: clamp(40px, 14vw, 72px);
    white-space: normal;
    overflow-wrap: break-word;
    margin-bottom: 80px;
  }

  /* Figma spec (BoxPoint) keeps this at the desktop 40px (H4) size on
     mobile too, relying on the site's existing "h1 br, h2 br: none on
     mobile" rule for natural wrap instead of a smaller font. Note this
     is shared with .cs-block-title--h2 (challenge/process/outcome
     titles) — revisit as its own class if those don't read well at 40px
     without manual breaks. */
  .cs-block-title {
    font-size: var(--fs-headline-4);
  }

  /* Hotel Berna's outcome title ("El diseño / fue la / consecuencia.") —
     "consecuencia." alone on its own line reads too heavy at the shared
     40px in this narrow a column. Scoped to this one title only, same
     pattern as .cs-hero-title--compact, so the other case studies' titles
     (shorter words, same 40px) are untouched. */
  .cs-block-title--compact {
    font-size: 34px;
  }

  /* Contact-specific: "¿Y ahora qué? La primera conversación." reads too
     big at the shared 40px on a narrow column — own smaller size,
     scoped so it doesn't touch the case studies'/About's identical
     titles elsewhere. */
  .contact-form-title {
    font-size: 38px;
  }

  /* Body copy: pinned at the desktop 20px ceiling instead of --fs-body-lg's
     mobile-resolved 16px floor. Shared class — affects every .cs-overview-text
     paragraph in the case study (overview + challenge/process/outcome), not
     just section 01. Hotel Berna's numbered list and Proceso phase paragraphs
     use --fs-body-lg too but aren't .cs-overview-text, so without their own
     line here they'd sit at the 16px floor while every other paragraph on
     the same page reads at 20px. */
  .cs-overview-text,
  .cs-overview-list li,
  .cs-process-phase p {
    font-size: 20px;
  }

  /* Hero image: square crop on mobile only (matches home's
     .project-row-image 1:1 treatment) — desktop and every other image
     in the case study body keep their own ratio. */
  .cs-hero-image img {
    aspect-ratio: 1;
    object-fit: cover;
  }

  .cs-break-mobile {
    display: initial;
  }

  .cs-break-desktop {
    display: none;
  }

  /* Index row ("01 /Descripción general"): equal 1fr/1fr columns left
     "/DESCRIPCIÓN GENERAL" too narrow to fit one line at 335px. Shrink
     column 1 to just "01"'s width and right-align the label in the rest
     — same visual position as before, just with room to not wrap. */
  .cs-index-row {
    grid-template-columns: auto 1fr;
    gap: var(--space-sm);
  }

  .cs-index-label {
    justify-self: end;
    text-align: right;
    white-space: nowrap;
  }

  /* Column gap = headline -> body text for the 3 sections with no tags
     list (challenge/process/outcome). Section 01 has .cs-tags between
     its headline and this gap, so it needs the smaller 40px instead —
     :has() targets it without a modifier class per section. */
  .cs-block-grid {
    grid-template-columns: 1fr;
    margin-top: 80px;
    gap: 80px;
  }

  .cs-block-grid:has(.cs-tags) {
    gap: 40px;
  }

  /* .cs-block-title's own margin-bottom (40px, desktop's headline->tags
     gap) still applies in sections with no .cs-tags, stacking with the
     grid gap above for 120px instead of the intended 80px. */
  .cs-block-grid:not(:has(.cs-tags)) .cs-block-title {
    margin-bottom: 0;
  }

  .cs-gallery-block {
    margin-top: 80px;
    /* 40px here + the next .cs-section's own 40px top-padding = 80px
       total gap before "02" starts. */
    margin-bottom: 40px;
  }

  /* body.page-light .cs-section's desktop padding (40px 0 120px) left its
     120px bottom untouched here — stacking with the next section's own
     40px top padding for 160px between sections instead of the intended
     80px. Symmetric 40px both sides; the "gap before next section" math
     above already accounts for it. */
  body.page-light .cs-section,
  body.page-about .cs-section {
    padding: 40px 0;
  }

  /* footer-cta overlaps the section above it by 40px (its own rounded
     reveal-cap curve) — the 40px padding-bottom above lands exactly
     underneath that overlap and cancels out, leaving 0px visible gap.
     80px here nets the 40px actually asked for once the overlap eats
     the rest. */
  body.page-about .cs-section:has(+ .footer-cta) {
    padding-bottom: 80px;
  }

  .cs-hero-meta {
    flex-direction: column;
    gap: 8px;
  }

  .cs-section {
    grid-template-columns: 1fr;
  }

  .cs-section-images {
    margin-top: var(--space-lg);
  }

  .cs-content-grid {
    grid-template-columns: 1fr;
    gap: var(--space-lg);
  }

  .cs-phases {
    grid-template-columns: 1fr;
    position: relative;
    padding-left: 32px;
  }

  .cs-phases::before {
    top: 0;
    bottom: 0;
    left: 12px;
    right: auto;
    width: 1px;
    height: auto;
  }

  .cs-metrics-grid {
    grid-template-columns: 1fr;
  }

  .cs-gallery {
    grid-template-columns: 1fr;
  }

  .cs-overview {
    grid-template-columns: 1fr;
  }

  .cs-overview-grid {
    grid-template-columns: 1fr 1fr;
  }

  /* ─── Case study — Ulrych structure ────────────────────────────── */

  /* All forced breaks in headings → let text wrap naturally on mobile */
  h1 br, h2 br { display: none; }

  /* Reduce hero and section vertical spacing */
  .cs-hero    { padding-top: calc(var(--nav-height) + var(--space-sm)); }
  .cs-section { padding: var(--space-lg) var(--container-pad); }
  .cs-related { padding: var(--space-lg) var(--container-pad); }

  /* Meta grid: 2×2, tighter vertical padding */
  .cs-meta-grid { grid-template-columns: 1fr 1fr; }
  .cs-meta-item { padding-top: var(--space-xs); padding-bottom: var(--space-xs); }

  /* Overview images: stack vertically */
  .cs-two-images { grid-template-columns: 1fr; }

  /* Showcase: 1 col. Ratio stays 3/4 (same as desktop, set on
     .cs-showcase-item itself) — these are portrait phone-screenshot
     mockups; a landscape 16/9 crop here was slicing the middle out of
     the screen and clipping the app UI. */
  .cs-showcase-grid { grid-template-columns: 1fr; }

  /* Related: stack vertically */
  .cs-related-grid  { grid-template-columns: 1fr; }

  /* Contact */
  /* Desktop's row-gap:0 assumes Nombre/Email sit side by side — once
     this collapses to a single column, that 0 became the gap between
     the two stacked fields. Matches .contact-form's own 56px so every
     field-to-field gap in the form is the same, whether or not the
     fields started out in the same .contact-form-row. */
  .contact-form-row {
    grid-template-columns: 1fr;
    gap: 56px;
  }


  /* Footer: stack to avoid forcing viewport width on narrow phones */
  .site-footer {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-sm);
  }

  .site-footer nav {
    flex-wrap: wrap;
  }
}

/* ─── READING PROGRESS BAR ──────────────────────────────────────── */

.cs-progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 2px;
  background: var(--color-highlight);
  transform-origin: left center;
  transform: scaleX(0);
  z-index: calc(var(--z-nav) + 2);
  pointer-events: none;
}

/* ─── CS BREADCRUMB ─────────────────────────────────────────────── */

.cs-breadcrumb {
  position: sticky;
  top: var(--nav-height);
  z-index: calc(var(--z-nav) - 1);
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px var(--container-pad);
  background: rgba(10, 10, 11, 0.92);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--color-border);
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
}

.cs-breadcrumb a {
  color: var(--color-muted);
  transition: color var(--duration-fast) var(--ease-smooth);
}

.cs-breadcrumb a:hover {
  color: var(--color-text-soft);
}

.cs-breadcrumb-sep {
  font-size: 10px;
  opacity: 0.35;
}

.cs-breadcrumb-current {
  color: var(--color-text-soft);
}

/* ─── CS PULL QUOTE ─────────────────────────────────────────────── */

.cs-pull-quote {
  padding: var(--space-xl) var(--container-pad);
  max-width: var(--container-max);
  margin: 0 auto;
  position: relative;
}
.cs-pull-quote::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: var(--container-pad);
  right: var(--container-pad);
  height: 1px;
  background: var(--color-border);
  pointer-events: none;
}

.cs-pull-quote blockquote {
  font-family: var(--font-family);
  font-size: clamp(22px, 2.8vw, 36px);
  font-weight: 400;
  color: var(--color-text-soft);
  letter-spacing: var(--tracking-tight);
  line-height: 1.3;
  max-width: 800px;
  padding-left: var(--space-lg);
  position: relative;
  margin: 0;
}

.cs-pull-quote blockquote::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.1em;
  bottom: 0.1em;
  width: 2px;
  background: var(--color-highlight);
}

/* ─── CS METRICS BAND ───────────────────────────────────────────── */

.cs-metrics-band {
  background: var(--color-surface);
  padding: var(--space-xl) var(--container-pad);
  position: relative;
}
.cs-metrics-band::before,
.cs-metrics-band::after {
  content: '';
  position: absolute;
  left: var(--container-pad);
  right: var(--container-pad);
  height: 1px;
  background: var(--color-border);
  pointer-events: none;
}
.cs-metrics-band::before { top: 0; }
.cs-metrics-band::after  { bottom: 0; }

.cs-metrics-band-inner {
  max-width: var(--container-max);
  margin: 0 auto;
}

.cs-metrics-grid--large {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px;
  background: var(--color-border);
  border: 1px solid var(--color-border);
  margin-top: var(--space-lg);
}

.cs-metric-card--large {
  background: var(--color-surface);
  padding: var(--space-lg) var(--space-md);
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.metric-num--xl {
  font-size: clamp(56px, 7vw, 96px);
  font-weight: 500;
  letter-spacing: -0.05em;
  line-height: 1;
  color: var(--color-text-soft);
  font-variant-numeric: tabular-nums;
}

/* ─── WORK PAGE HERO STATS ──────────────────────────────────────── */

.work-hero-stats {
  display: flex;
  gap: var(--space-xl);
  padding-top: var(--space-lg);
  border-top: 1px solid var(--color-border);
  margin-top: var(--space-lg);
}

.work-hero-stat {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.work-hero-stat-num {
  font-size: clamp(32px, 4vw, 52px);
  font-weight: 500;
  letter-spacing: -0.03em;
  color: var(--color-text-soft);
  line-height: 1;
}

.work-hero-stat-label {
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
}

/* ─── WORK GRID EDITORIAL ───────────────────────────────────────── */

.work-grid--editorial {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2px;
  padding: 0 var(--container-pad) var(--space-xl);
  max-width: var(--container-max);
  margin: 0 auto;
}

.work-grid--editorial .project-card {
  transition: opacity var(--duration-base) var(--ease-smooth),
              transform var(--duration-base) var(--ease-smooth);
}

.work-grid--editorial .project-card.filtered {
  opacity: 0.15;
  transform: scale(0.97);
  pointer-events: none;
}

.work-grid--editorial .project-card:first-child {
  grid-column: 1 / -1;
}

.work-grid--editorial .project-card:first-child .project-img-wrap {
  aspect-ratio: 21 / 9;
}

.work-grid--editorial .project-card:not(:first-child) .project-img-wrap {
  aspect-ratio: 4 / 3;
}

/* ─── CS GALLERY VARIED ─────────────────────────────────────────── */

.cs-gallery--varied .cs-gallery-item:first-child {
  grid-column: 1 / -1;
  aspect-ratio: 16 / 7;
}

/* ─── SITE FOOTER — fix container padding ───────────────────────── */

.site-footer {
  padding-left: var(--container-pad);
  padding-right: var(--container-pad);
}

/* ─── RESPONSIVE — new components ──────────────────────────────── */

@media (max-width: 1024px) {
  .cs-metrics-grid--large {
    grid-template-columns: repeat(3, 1fr);
  }
}

@media (max-width: 768px) {
  .work-grid--editorial {
    grid-template-columns: 1fr;
  }

  .work-grid--editorial .project-card:first-child {
    grid-column: 1;
  }

  .work-grid--editorial .project-card:first-child .project-img-wrap,
  .work-grid--editorial .project-card:not(:first-child) .project-img-wrap {
    aspect-ratio: 4 / 3;
  }

  .work-hero-stats {
    gap: var(--space-md);
    flex-wrap: wrap;
  }

  .cs-metrics-grid--large {
    grid-template-columns: 1fr;
  }

  .cs-pull-quote blockquote {
    padding-left: var(--space-md);
  }

  .cs-breadcrumb {
    position: relative;
    top: 0;
  }

  .cs-gallery--varied .cs-gallery-item:first-child {
    grid-column: 1;
    aspect-ratio: 4 / 3;
  }
}

/* ─── AVAILABILITY DOT ──────────────────────────────────────────── */

.available-dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  background: var(--color-available);
  border-radius: 50%;
  margin-right: 8px;
  vertical-align: middle;
  animation: pulseDot 2.4s ease-in-out infinite;
}

@keyframes pulseDot {
  0%, 100% { opacity: 1;   transform: scale(1); }
  50%       { opacity: 0.4; transform: scale(0.7); }
}

/* ─── FILTER CHIP FOCUS ─────────────────────────────────────────── */

.filter-chip:focus-visible {
  outline: 2px solid var(--color-highlight);
  outline-offset: 3px;
}


/* ─── PAGE TRANSITION ───────────────────────────────────────────── */

.page-transitioning main {
  pointer-events: none;
}

/* ─── SERVICES — draggable strip, fixed heading ──────────────────── */

.services-section {
  background: var(--color-bg-light);
  padding-top: var(--space-xl);
  padding-bottom: var(--space-xl);
}

.services-inner {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 0 var(--container-pad);
}

.services-topline {
  height: 1px;
  background: var(--color-text-dark);
}

.services-counter {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 16px;
  font-family: var(--font-family);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: var(--tracking-tight);
  color: var(--color-text-dark);
}

.services-heading {
  margin-top: 80px;
  font-family: var(--font-headline);
  font-size: var(--fs-headline-3);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: uppercase;
  line-height: 1.05;
  color: var(--color-text-dark);
}

.services-track-viewport {
  margin-top: 120px;
  overflow: hidden;
}

.services-track {
  display: flex;
  gap: 24px;
  cursor: grab;
  user-select: none;
}

.services-track:active {
  cursor: grabbing;
}

.service-card {
  flex: 0 0 calc((100% - 48px) / 3);
  padding-right: 32px;
}

.service-card-num {
  display: none;
}

.service-card-title {
  font-family: var(--font-family);
  font-size: var(--fs-h3);
  font-weight: 500;
  letter-spacing: var(--tracking-h1);
  text-transform: none;
  line-height: var(--lh-subhead);
  color: var(--color-text-dark);
  margin-bottom: 24px;
  pointer-events: none;
}

.service-card-desc {
  font-family: var(--font-family);
  font-size: var(--fs-body);
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: var(--tracking-tight);
  color: var(--color-text-dark);
  pointer-events: none;
}

.services-scrollline {
  position: relative;
  margin-top: 120px;
  height: 1px;
  background: rgba(45, 45, 45, 0.15);
}

.services-scrollline-thumb {
  position: absolute;
  top: 0;
  left: 0;
  height: 1px;
  width: 50%;
  background: var(--color-text-dark);
}

@media (max-width: 1024px) {
  .services-counter {
    gap: 120px;
  }

  .service-card {
    flex: 0 0 calc((100% - 24px) / 2);
  }

  .services-scrollline-thumb {
    width: 66.666%;
  }
}

@media (max-width: 640px) {
  .services-inner {
    padding: 0 20px;
  }

  /* padding-top/bottom: var(--space-xl) (desktop rule above) already
     resolves to a consistent 64px here — no override needed. The old
     hardcoded 80px bottom made this section's rhythm drift from its own
     top padding for no reason. */

  .services-counter {
    gap: 0;
    justify-content: space-between;
  }

  .services-heading {
    margin-top: 64px;
    font-size: 32px;
    line-height: 1.1;
  }

  .services-track-viewport {
    margin-top: 80px;
  }

  .services-track {
    gap: 20px;
  }

  .service-card {
    flex: 0 0 246px;
    padding-right: 0;
  }

  .service-card-title {
    line-height: 1.2;
    letter-spacing: var(--tracking-tight);
    margin-bottom: 24px;
  }

  .services-scrollline-thumb {
    width: 33.333%;
  }
}

/* ─── PROMESA ───────────────────────────────────────────────────── */

/* box-shadow with a negative y-offset casts upward, over whatever's behind
   it (the sticky .hero, mid-cover) — reads as a physical sheet sliding up
   over the hero instead of just a flat background change. */
.promesa {
  background: var(--color-bg-light);
  padding: var(--space-2xl) 0;
  position: relative;
  box-shadow: 0 -30px 60px -20px rgba(0, 0, 0, 0.35);
}

.promesa-inner {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 0 var(--container-pad);
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 40px;
}

.promesa-headline {
  max-width: 737px;
  font-family: var(--font-family);
  font-size: var(--fs-h2);
  font-weight: 500;
  line-height: var(--lh-subhead);
  letter-spacing: var(--tracking-h1);
  color: var(--color-text-dark);
  margin: 0;
}

.promesa-side {
  width: 411px;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  gap: 40px;
}

.promesa-text {
  font-family: var(--font-family);
  font-size: 20px;
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: -0.03em;
  color: var(--color-text-dark);
  margin: 0;
}

.promesa-cta {
  align-self: flex-start;
  height: 34px;
}

@media (max-width: 1024px) {
  .promesa-inner {
    flex-direction: column;
    gap: 40px;
  }

  .promesa-headline {
    max-width: none;
  }

  .promesa-side {
    width: 100%;
  }
}

@media (max-width: 768px) {
  /* Steps down from --space-2xl (would resolve to a 100px floor here) to
     --space-xl, matching the rest of the mobile section rhythm instead of
     an unrelated hardcoded 80px. */
  .promesa {
    padding: var(--space-xl) 0;
  }

  .promesa-inner {
    padding: 0 20px;
  }

  .promesa-headline {
    font-size: 24px;
    line-height: 1.3;
  }
}

/* ─── PAGE TRANSITION ────────────────────────────────────────────── */
/* Overlay built and styled entirely via transitions.js (inline styles) */
