/* Activate cross-document view transitions (Chrome 126+, Safari 18+, Firefox 131+) */
@view-transition {
  navigation: auto;
}

/* ==================================================================
   Fonts
================================================================== */
@font-face {
  font-family: "Inter";
  src: url("../assets/fonts/inter/Inter-Regular.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: "Inter";
  src: url("../assets/fonts/inter/Inter-Medium.woff2") format("woff2");
  font-weight: 500;
  font-display: swap;
}
@font-face {
  font-family: "Inter";
  src: url("../assets/fonts/inter/Inter-SemiBold.woff2") format("woff2");
  font-weight: 600;
  font-display: swap;
}
@font-face {
  font-family: "Inter";
  src: url("../assets/fonts/inter/Inter-Bold.woff2") format("woff2");
  font-weight: 700;
  font-display: swap;
}
@font-face {
  font-family: "Inter";
  src: url("../assets/fonts/inter/Inter-ExtraBold.woff2") format("woff2");
  font-weight: 800;
  font-display: swap;
}
@font-face {
  font-family: "Bebas Neue";
  src: url("../assets/fonts/bebas-neue/BebasNeue-Regular.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}

/* ==================================================================
   Tokens
   --fg-rgb drives every text/border/dash color as a single source of
   truth. In scene mode JS crossfades it on <html> every frame; in the
   fallback (mobile/reduced-motion) path each section pins its own
   value so light and dark project "worlds" still read correctly with
   no JS-driven animation loop.
================================================================== */
:root {
  --bg: #faf8f4;
  --fg-rgb: 21, 20, 15;
  --font-display: "Bebas Neue", "Inter", sans-serif;
  --font-body: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  /* Figma reference frame is 1512px wide with a fixed 60px side margin
     (60 / 1512 = 3.97vw). Expressed as vw so it holds true at the
     reference width and scales proportionally elsewhere. */
  --side-pad: clamp(1.75rem, 4vw, 4rem);
}

/* ==================================================================
   Reset / base
================================================================== */
* { margin: 0; padding: 0; box-sizing: border-box; }

html { color-scheme: light; }

body {
  background: var(--bg);
  color: rgb(var(--fg-rgb));
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

img, video { display: block; max-width: 100%; }

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: rgb(var(--fg-rgb));
  color: var(--bg);
  padding: 0.75rem 1.25rem;
  z-index: 200;
  font-weight: 600;
}
.skip-link:focus { left: 1rem; top: 1rem; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
  }
}

/* ==================================================================
   Page Transitions (back: case study → homepage)
   The S&A media card is a named element — browser morphs it back to
   its homepage position automatically. Root crossfades.
================================================================== */

@media (prefers-reduced-motion: no-preference) {
  ::view-transition-old(root) { animation: 380ms ease both vt-hp-fade-out; }
  ::view-transition-new(root) { animation: 380ms ease both vt-hp-fade-in; }
}

@keyframes vt-hp-fade-out { to   { opacity: 0; } }
@keyframes vt-hp-fade-in  { from { opacity: 0; } }

/* Shared nav/label type — Bebas Neue, matches the Figma header/CTA spec */
.nav-label {
  font-family: "Bebas Neue", sans-serif;
  font-size: 18px;
  letter-spacing: 1.8px;
  color: rgb(var(--fg-rgb));
}

/* ==================================================================
   Header — fixed, minimal, always on top of the scene
================================================================== */
.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* Figma: logo/About sit exactly 32px from the top edge. */
  padding: clamp(1.25rem, 3.2vh, 2rem) var(--side-pad);
}

.brand {
  font-family: "Bebas Neue", sans-serif;
  font-size: 18px;
  letter-spacing: 0.1em;
  color: rgb(var(--fg-rgb));
}

a.brand {
  transition: opacity 0.2s;
}
a.brand:hover { opacity: 0.5; }

.brand--name {
  display: inline-flex;
  align-items: center;
  position: relative;
}

.brand-avatar-wrap {
  position: absolute;
  left: 100%;
  top: 50%;
  transform: translateY(-50%);
  margin-left: 10px;
  width: 38px;
  height: 38px;
}

.brand-avatar {
  display: block;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  object-fit: cover;
  object-position: top;
  opacity: 0;
  transform: translateX(-10px) scale(0.8);
  transition: opacity 0.3s ease, transform 0.3s ease;
}

.brand--name:hover .brand-avatar,
.brand--name.brand--open .brand-avatar {
  opacity: 1;
  transform: translateX(0) scale(1);
}

/* Same specificity as the hover rule above, placed after so it wins when both match.
   Clicking at the last message adds this class to hide avatar until next re-enter. */
.brand--name.brand--exhausted .brand-avatar {
  opacity: 0;
  transform: translateX(-10px) scale(0.8);
  pointer-events: none;
}

/* Touch: avatar is absolutely positioned, no layout adjustment needed when open */

@media (pointer: coarse) {
  .brand--name { cursor: pointer; }
}

/* ── Desktop: JS sets left to clamp within viewport, Y-only transform for animation ── */
.brand-bubble {
  position: absolute;
  top: calc(100% + 12px);
  left: 0;                         /* JS overrides per-show */
  transform: translateY(-6px);
  white-space: normal;
  width: max-content;
  max-width: calc(100vw - 2 * var(--side-pad));
  background: rgba(10, 10, 10, 0.72);
  backdrop-filter: blur(16px) saturate(1.4);
  -webkit-backdrop-filter: blur(16px) saturate(1.4);
  color: #fff;
  font-family: "Inter", sans-serif;
  font-size: 12px;
  font-weight: 400;
  letter-spacing: 0.015em;
  line-height: 1.45;
  padding: 8px 14px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.22s ease, transform 0.22s ease;
  z-index: 200;
}

/* Two-layer arrow: ::before is the border ring, ::after is the fill.
   Both track --arrow-x so the tip aligns with the avatar center on mobile. */
.brand-bubble::before {
  content: '';
  position: absolute;
  bottom: calc(100% + 1px); /* outer border edge of the bubble */
  left: var(--arrow-x, 50%);
  transform: translateX(-50%);
  border: 6px solid transparent;
  border-bottom-color: rgba(255, 255, 255, 0.12); /* matches bubble border */
}

.brand-bubble::after {
  content: '';
  position: absolute;
  bottom: 100%; /* padding-box top, 1px inside ::before */
  left: var(--arrow-x, 50%);
  transform: translateX(-50%);
  border: 6px solid transparent;
  border-bottom-color: rgba(10, 10, 10, 0.72); /* matches bubble fill */
}

.brand-bubble.is-visible {
  opacity: 1;
  transform: translateY(0);
}

/* ── Mobile: fixed, JS measures and sets width + position ── */
@media (pointer: coarse) {
  .brand-bubble {
    position: fixed;
    left: auto;         /* JS sets this */
    right: auto;
    top: auto;          /* JS sets this */
    width: auto;        /* JS overrides with explicit px after measuring */
    max-width: min(260px, calc(100vw - 2 * var(--side-pad)));
    transform: none;
    transition: opacity 0.15s ease;
  }

  .brand-bubble.is-visible {
    transform: none;
  }
}

/* Fallback/mobile: header sits above several differently-themed
   sections at once, so it gets a neutral legible backdrop instead of
   chasing whichever section happens to be behind it. */
html:not(.js-scene) .site-header {
  background: rgba(250, 248, 244, 0.82);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
html:not(.js-scene) .site-header .brand { color: #15140f; }

/* ==================================================================
   Scene — shared structure (mobile-first: plain scroll document)
================================================================== */
main { display: block; }
.scene { position: relative; }
.scene__bg { display: none; } /* only animated/shown in desktop scene mode */

.scene__rail { list-style: none; }

.scene__project {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2rem;
  min-height: 100vh;
  padding: clamp(6rem, 14vh, 9rem) clamp(1.25rem, 6vw, 3rem) clamp(3rem, 8vh, 5rem);
}

/* Mobile/fallback: no JS-driven --fg-rgb lerp, so each section pins
   its own background + foreground pair directly. Scoped away from
   js-scene so it never fights the per-frame root override there. */
html:not(.js-scene):not(.js-mobile-scene) #sackcloth {
  background: #faf8f4;
  --fg-rgb: 21, 20, 15;
}

html:not(.js-scene):not(.js-mobile-scene) #locl {
  background: #181f2b;
  --fg-rgb: 255, 255, 255;
}

html:not(.js-scene):not(.js-mobile-scene) #change-agents {
  background: #fff8f2;
  --fg-rgb: 21, 20, 15;
}

html:not(.js-scene):not(.js-mobile-scene) #egg-path {
  background: #111111;
  --fg-rgb: 255, 255, 255;
}

/* ==================================================================
   Media group — single large primary visual per project
================================================================== */
.media-group { width: 100%; }

/* Compositor layers only when the JS scene is active — avoids 8 idle GPU layers */
html.js-scene .media-travel,
html.js-mobile-scene .media-travel { will-change: transform, opacity; }
html.js-scene .media-drift,
html.js-mobile-scene .media-drift { will-change: transform; }

.media-primary {
  display: block; /* keeps layout correct when element is an <a> */
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: 20px;
  overflow: hidden;
  border: 1px solid rgba(var(--fg-rgb), 0.16);
  box-shadow: 0 30px 70px -28px rgba(0, 0, 0, 0.35);
  cursor: pointer;
}
.media-primary img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

/* Small video preview, sits under the text column per the design.
   Figma: exactly 341x192, 18px below the CTA, 16px corner radius. */
.scene__preview {
  display: block; /* keeps layout correct when element is an <a> */
  position: relative;
  width: min(341px, 100%);
  aspect-ratio: 341 / 192;
  margin-top: 1.125rem;
  border-radius: 16px;
  overflow: hidden;
  border: 1px solid rgba(var(--fg-rgb), 0.16);
  background: rgba(var(--fg-rgb), 0.08);
  cursor: pointer;
  text-decoration: none;
}
.layer__video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Minimal "spotlight" glow that tracks the cursor inside each case-study
   tile — both tiles will share the same case-study link once it exists, so
   the hover affordance previews that they're both clickable. JS keeps
   --mx/--my (set in px from the tile's own top-left) in sync on mousemove;
   the radial gradient itself is pure CSS, opacity-gated on :hover so there
   is zero cost while idle. */
.media-primary::after,
.scene__preview::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(
    circle 220px at var(--mx, 50%) var(--my, 50%),
    rgba(255, 255, 255, 0.16),
    transparent 70%
  );
  opacity: 0;
  transition: opacity 0.35s var(--ease);
  pointer-events: none;
}
.media-primary:hover::after,
.scene__preview:hover::after {
  opacity: 1;
}

/* Continuous idle motion — alive even with no input. All four tiles share
   the exact same duration/timing so they float up and down in sync rather
   than drifting independently. */
@media (prefers-reduced-motion: no-preference) {
  .media-drift { animation: drift 8s ease-in-out infinite alternate; }
}
@keyframes drift {
  from { transform: translateY(0); }
  to   { transform: translateY(-8px); }
}

/* ==================================================================
   Text column
================================================================== */
.scene__text { text-align: left; }

.scene__title {
  font-family: var(--font-display);
  font-weight: 400;
  text-transform: uppercase;
  /* Grows past the 1512px reference on bigger screens (no upper plateau
     until 8rem) so the hero type gets "big and epic" on wide desktops,
     while the 3rem floor keeps it legible as the viewport narrows. */
  font-size: clamp(3rem, 6.2vw, 8rem);
  line-height: 0.95;
  color: var(--accent);
}

.scene__description {
  /* Figma: 10px gap between description and the CTA below it. */
  margin-top: 0.9rem;
  max-width: 100%;
  font-size: clamp(1rem, 1.3vw, 1.25rem);
  line-height: 1.5;
  color: rgb(var(--fg-rgb));
}

.scene__cta {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 1.5rem;
  text-decoration: none;
  cursor: pointer;
  line-height: 1;
  transition: opacity 0.2s;
}
.scene__cta:hover { opacity: 0.55; }
.cta-label {
  font-family: "Bebas Neue", sans-serif;
  font-size: 20px;
  letter-spacing: 1.8px;
  color: rgb(var(--fg-rgb));
}
.cta-arrow {
  display: inline-block;
  font-family: "Bebas Neue", sans-serif;
  font-size: 24px;
  color: rgb(var(--fg-rgb));
  transition: transform 0.3s var(--ease);
}
.scene__cta:hover .cta-arrow { transform: translateX(4px); }

/* ==================================================================
   Pagination — vertical dashes, right edge
================================================================== */
.scene__pagination {
  display: flex;
  gap: 1rem;
  padding: 0 clamp(1.25rem, 6vw, 3rem) 2rem;
  justify-content: center;
}

.scene__dot {
  width: 6px;
  height: 24px;
  border-radius: 999px;
  border: none;
  padding: 0;
  cursor: pointer;
  background: rgba(var(--fg-rgb), 0.16);
  transition: background 0.3s var(--ease);
}
html:not(.js-scene):not(.js-mobile-scene) .scene__dot {
  background: rgba(0, 0, 0, 0.25);
}

html.js-mobile-scene .scene__dot {
  background: rgba(var(--fg-rgb), 0.16);
}
html:not(.js-scene):not(.js-mobile-scene) .scene__dot.is-active {
  background: #15140f;
}

/* ==================================================================
   Desktop immersive scene — applied only once JS confirms capability
================================================================== */
html.js-scene,
html.js-scene body {
  height: 100%;
  overflow: hidden;
}

.js-scene .scene { position: fixed; inset: 0; overflow: hidden; }

.js-scene .scene__bg {
  display: block;
  position: absolute;
  inset: 0;
  z-index: 0;
}

.js-scene .scene__rail { height: 100%; position: relative; z-index: 1; }

.js-scene .scene__project {
  /* Both children become position:fixed below, so this element
     contributes no layout box of its own. */
  display: block;
  min-height: 0;
  padding: 0;
  gap: 0;
}

.js-scene .scene__text {
  position: fixed;
  top: 50%;
  left: var(--side-pad);
  transform: translateY(-50%);
  /* Figma: 558px description column inside a 1512px frame (~37vw). The
     description/CTA wrap to this width; the title (below) deliberately
     escapes it. */
  width: min(560px, 37vw);
  z-index: 5;
}

.js-scene .scene__title {
  /* Sized to its own text exactly, never to the 560px column above — this
     is what keeps "Sackcloth & Ashes" on one line at any desktop width:
     the box is always precisely as wide as the rendered glyphs at the
     current clamp()'d font-size, so it can never wrap itself. (It's free
     to overflow .scene__text's edge since nothing clips it, and the media
     card to its right is positioned with enough clearance at every width
     from the 861px scene-mode floor on up — verified by hand against the
     box's own font-size curve.) */
  white-space: nowrap;
  width: max-content;
  max-width: none;
}

.js-scene .media-group {
  position: fixed;
  top: 50%;
  /* Figma: primary card's right edge sits 122px from the frame edge
     (~8vw at the 1512px reference width). */
  right: clamp(3.5rem, 8vw, 8rem);
  transform: translateY(-50%);
  /* No plateau until 1180px — on wide desktop monitors the card keeps
     growing into the otherwise-empty middle of the screen instead of
     stalling at the 1512px-reference size. */
  width: clamp(420px, 48.5vw, 1180px);
  z-index: 4;
}

.js-scene .media-primary { box-shadow: 0 40px 90px -30px rgba(0, 0, 0, 0.4); }

.js-scene .scene__pagination {
  position: fixed;
  /* Same 60px margin as the header/text column. */
  right: var(--side-pad);
  top: 50%;
  transform: translateY(-50%);
  flex-direction: column;
  gap: 1.5rem;
  padding: 0;
  z-index: 90;
}

.js-scene .scene__preview {
  position: fixed;
  left: var(--side-pad);
  /* Figma: video tile pinned 40px from the frame's bottom edge, scaled
     the same way the header's 32px top offset is (vh-based clamp so it
     holds at the 982px reference and stays sane at other heights). */
  bottom: clamp(1.25rem, 4.1vh, 2.5rem);
  /* Scale with viewport width so it doesn't crowd the CTA text on small laptops */
  width: clamp(180px, 20vw, 341px);
  margin-top: 0;
  z-index: 6;
}

/* Before JS runs, all four projects sit at the same fixed position —
   they'd flash as a stack on first paint (and during View Transitions).
   Default to opacity:0 so only project 0 is visible; JS render(0)
   immediately overrides these with inline styles. */
.js-scene .media-travel  { opacity: 0; }
.js-scene .scene__text   { opacity: 0; }
.js-scene .scene__preview { opacity: 0; }
.js-scene [data-index="0"] .media-travel,
.js-scene [data-index="0"] .scene__text,
.js-scene [data-index="0"] .scene__preview { opacity: 1; }

/* ==================================================================
   Desktop scene — small laptop responsive adjustments (861–1200px).
   Shrinks the primary image card and preview tile so neither crowds
   the text column or the CTA at 13-14" laptop viewport sizes.
================================================================== */
@media (max-width: 1200px) {
  .js-scene .media-group {
    width: clamp(300px, 41vw, 600px);
    right: clamp(2.5rem, 7vw, 5rem);
  }
  .js-scene .scene__preview {
    width: clamp(150px, 16vw, 230px);
  }
}

/* ==================================================================
   Fallback (769-860px fine-pointer landscape tablets only).
   Phones and portrait tablets (≤768px) now run the mobile swipe
   scene — this range is only ever reached by a mouse-driven browser
   resized into the gap between mobile-scene and desktop-scene.
================================================================== */
@media (min-width: 769px) and (max-width: 860px) {
  html:not(.js-scene):not(.js-mobile-scene) .scene__preview {
    width: min(300px, 100%);
  }
}

/* ==================================================================
   Mobile / tablet scene (≤768px) — pinned viewport, horizontal
   scroll-snap swipe. Covers phones AND portrait tablets.
================================================================== */
html.js-mobile-scene,
html.js-mobile-scene body {
  height: 100%;
  overflow: hidden;
}

.js-mobile-scene .scene { position: fixed; inset: 0; overflow: hidden; }

.js-mobile-scene .scene__bg {
  display: block;
  position: absolute;
  inset: 0;
  z-index: 0;
  transition: background-color 0.3s var(--ease);
}

.js-mobile-scene .scene__rail {
  position: relative;
  z-index: 1;
  height: 100%;
  display: flex;
  flex-direction: row;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* This inset is what makes slide 0 sit flush at x=20 with zero scrolling
     needed — scroll-snap-align:center can't reach it on its own, since
     centering the first/last slide would require scrolling past the start
     of the content (an invalid, clamped-to-0 position). The slide below
     fills this content-box exactly (no further subtraction), so the next
     slide's edge peeks in by the same 20px past this box's far edge. */
  padding-inline: 20px;
}
.js-mobile-scene .scene__rail::-webkit-scrollbar { display: none; }

.js-mobile-scene .scene__project {
  flex: 0 0 100%;
  scroll-snap-align: center;
  min-height: 0;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  /* Top/bottom safe-zones: clears the fixed header + pagination row above,
     and gives the CTA breathing room above the viewport edge below. */
  padding: 7.25rem 0 2.5rem;
  gap: 0;
}

.js-mobile-scene .media-group { order: 0; }

.js-mobile-scene .scene__preview {
  order: 1;
  align-self: center;
  /* Scales with viewport: phone ~180px, tablet up to 260px */
  width: clamp(180px, 38%, 260px);
  margin-top: -50.5px;
  margin-bottom: 40px;
  z-index: 2;
}

.js-mobile-scene .scene__text {
  order: 2;
}

.js-mobile-scene .scene__title {
  white-space: nowrap;
  /* Tuned for 320–768px: grows with viewport but caps before overflowing
     "Sackcloth & Ashes" (the longest title) at any width in the range. */
  font-size: clamp(2.1rem, 13.5vw, 5rem);
}

.js-mobile-scene .scene__description {
  font-size: clamp(1rem, 4.5vw, 1.25rem);
  /* Figma: 10px gap from the title above. */
  margin-top: 10px;
}

.js-mobile-scene .scene__cta {
  /* Figma: 16px gap from the description above. */
  margin-top: 16px;
}

.js-mobile-scene .cta-label {
  font-size: clamp(1rem, 4.5vw, 1.25rem);
}

.js-mobile-scene .scene__pagination {
  position: absolute;
  /* top is set in JS, measured against the hero image so the gap below
     stays exactly the Figma-spec ~14px at any viewport height. */
  left: 50%;
  transform: translateX(-50%);
  flex-direction: row;
  gap: 0.5rem;
  padding: 0;
  z-index: 50;
}
.js-mobile-scene .scene__dot { width: 24px; height: 6px; }

/* Header matches the active project's own colours, with no pill/backdrop
   behind it — same bare treatment the desktop scene uses. */
html.js-mobile-scene .site-header {
  padding-inline: 20px;
  /* viewport-fit=cover extends the scene behind the Dynamic Island/notch;
     push header content below the safe area, with the existing visual gap
     as a minimum on devices with no notch (env() returns 0 there). */
  padding-top: max(env(safe-area-inset-top, 0px), clamp(1.25rem, 3.2vh, 2rem));
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
html.js-mobile-scene .site-header .brand {
  color: rgb(var(--fg-rgb));
  transition: color 0.3s var(--ease);
}

/* ────────────────────────────────────────────────────────────────────────
   Page loader
   Covers the window between first paint and the page being ready, so a slow
   connection sees an intentional state rather than blank white.

   Added by a small inline head script and cleared on window load, so a no-JS
   visitor is never trapped behind it. It only becomes visible after 250ms —
   a fast or cached load clears it first and it is never painted at all.
   Opacity is driven by a transition (not the sweep keyframe) so the fade-out
   is smooth no matter where the fade-in had got to.
   ──────────────────────────────────────────────────────────────────────── */
html.is-loading::before,
html.is-loading::after {
  content: "";
  position: fixed;
  opacity: 0;
  transition: opacity 0.3s ease;
  pointer-events: none;
}

html.is-loading::before {
  inset: 0;
  background: #faf8f4;
  z-index: 9998;
}

html.is-loading::after {
  top: 0;
  left: 0;
  width: 100%;
  height: 2px;
  z-index: 9999;
  background: linear-gradient(90deg, transparent, #15140f, transparent);
  animation: loaderSweep 1.15s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite;
}

html.is-loading.ldr-show::before,
html.is-loading.ldr-show::after {
  opacity: 1;
}

@keyframes loaderSweep {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
}

/* Reduced motion: keep the covering panel, drop the travelling bar. */
@media (prefers-reduced-motion: reduce) {
  html.is-loading::after { animation: none; }
}
