/* ============================================================
   Homepage — the loader, the pinned hero, the lockup, the marquee strip, the card hover
   takeover and viewer. Links after core.css.
   Split out of the former style.css on 2026-10-18; rules unchanged, in their
   original order. ============================================================ */

/* ============================================================
   Loader — black intro overlay (text reads white on it)
   ============================================================ */

.loader {
  position: fixed;
  inset: 0;
  background: var(--bg-inverse);   /* the intro starts on the opposite ground; dissolves as the page inverts to the theme's */
  z-index: 100;
  pointer-events: auto;
}
/* The intro's short cut (a return within the session — the head script's
   html.intro-quick, hero.js): no loader at all, so it is never painted. */
html.intro-quick .loader { display: none; }

/* Load counter — 0→100 in the loader's top-right (14px Epilogue Medium).
   Inherits the intro's inverse --fg (set on <body> in hero.js) and fades out with
   the loader's opacity tween since it's a child. */
.loader-count {
  position: absolute;
  top: calc(var(--pad-inline) + var(--safe-top));
  right: var(--pad-inline);
  font-family: var(--ff-sans);
  font-weight: 500;
  font-size: 14px;
  line-height: 1;
  color: var(--fg);
  font-variant-numeric: tabular-nums;   /* fixed-width digits — no jitter while counting */
}

/* Load progress bar — 1px along the loader's top edge; the black fill grows
   left→right in step with the counter (scaleX driven in hero.js). */
.loader-bar {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 1px;
  overflow: hidden;
}
.loader-bar-fill {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--fg-inverse);         /* the loader's ink (stays put through the intro) */
  transform: scaleX(0);
  transform-origin: left center;
  will-change: transform;
}


/* ============================================================
   Pinned hero — the hero stays fixed at the top while page content
   (.page-flow) scrolls up and overlaps it.
   ============================================================ */

/* Everything above the pinned footer, as one opaque layer.
   Two jobs, both required for the footer reveal to work:
   1. It is the containing block for .hero-pin's sticky, so the hero unpins at
      the end of this wrapper instead of riding to the end of the document —
      otherwise the carousel paints over the revealed footer.
   2. It is opaque and sits above .footer-pin (z-index 0), so the footer stays
      hidden at the top of the page, where .page-flow hasn't reached the bottom
      of the viewport yet and .hero-pin itself is transparent. */
.page-stack {
  position: relative;
  z-index: 1;
  background: var(--bg);   /* follows the loader's --bg inversion */
}

.hero-pin {
  position: sticky;
  top: 0;
  height: 100svh;
  z-index: 1;
  overflow: hidden;   /* clip to 100svh so the marquee can't overflow into the footer */
}

/* Inside the pinned hero the header rides along (and is covered on scroll)
   rather than floating fixed above everything. */
/* (The header used to be `position: absolute` inside .hero-pin so it scrolled
   away with the hero. It's now a body-level sibling and stays fixed.) */

.page-flow {
  position: relative;
  z-index: 2;
  background: var(--bg);   /* opaque — covers the hero as it scrolls over */
}


.hero {
  position: relative;
  z-index: 101;                    /* above the loader so words fade in on the black */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding-inline: var(--pad-inline);
  padding-top: var(--nav-h);       /* clear the fixed nav */
  padding-bottom: 0;               /* row + justify-content:center handles vertical centering */
  min-height: 0;                   /* let the row cap hero — never push main past 100svh */
}

.hero-title {
  font-size: var(--hero-title-size);
  line-height: 0.9;                /* 90% */
  letter-spacing: -0.03em;         /* -3% on the lockup only */
  margin: 0;
  text-align: center;
}

.hero-title .line {
  display: block;
}

.hero-title .line-sans {
  font-family: var(--ff-sans);
  font-weight: 600;
}

.hero-title .line-serif {
  font-family: var(--ff-serif);
  font-weight: 400;
}

/* word-level animation targets — leaf spans/ems only.
   Start invisible AND blurred; GSAP fades + racks them into focus. */
.hero-title[data-animated-words] :is(span, em):not(:has(*)) {
  opacity: 0;
  filter: blur(var(--blur-reveal));
  display: inline-block;
  will-change: opacity, filter;
}

/* Sub-nav — WORK / SOCIAL / POST, directly under the lockup */
.subnav {
  margin-top: clamp(24px, 2.5vw, 44px);
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: 12px;
  line-height: var(--lh-link);
}


/* ============================================================
   Marquee — auto-scroll image strip
   ============================================================ */

.marquee {
  width: 100%;
  /* The clip box runs from the strip's top down to the VIEWPORT's bottom edge,
     not the strip's own: main's padding-bottom below this row is exactly
     --furniture-clear, so adding it here lands the clip on the screen edge and
     the negative margin hands the space straight back — layout is unchanged.

     Why not `overflow-y: visible`: unclipping this box makes the compositor
     drop the sticky hero's paint and the lockup vanishes entirely (the same
     "black hero" failure mode as the iOS layer-budget one). Keeping `overflow:
     hidden` and moving its edge gets the effect without touching the layer. */
  height: calc(var(--marquee-h) + var(--furniture-clear));
  margin-bottom: calc(-1 * var(--furniture-clear));
  overflow: hidden;
  position: relative;
  z-index: 60;                     /* hovered card sits above the card-viewer */
}

.marquee-track {
  display: flex;
  /* Explicit, not 100%: .marquee is taller than the strip now (see above), and
     the track must stay the strip's own height or every card grows with it. */
  height: var(--marquee-h);
  width: max-content;
  will-change: transform;
}

.marquee-set {
  display: flex;
  height: 100%;
  gap: var(--marquee-gap);
  padding-inline-end: var(--marquee-gap);
}

.marquee-card {
  position: relative;                /* the in-card loop is absolute over the still */
  height: 100%;
  border-radius: 4px;
  /* Width computed explicitly rather than left to `aspect-ratio`. The card's
     height is a percentage, and iOS Safari won't reliably resolve
     aspect-ratio into a definite width from that — it falls back to sizing
     from the child <img>, and the card blows out horizontally while still
     matching the marquee height. --ar is authored as e.g. `520/360`, so this
     substitutes to calc(<marquee height> * 520/360). */
  width: calc(var(--marquee-h) * var(--ar));
  flex: 0 0 auto;
  overflow: hidden;
  will-change: transform;
}

.marquee-card :is(img, video) {
  width: 101%;                       /* +1% covers sub-pixel rounding gaps at the card edges */
  height: 101%;
  object-fit: cover;
}
/* The in-card loop ("Play the loop as the thumbnail"), over the still: fades
   up once marquee.js confirms playback, so the still holds until then. */
.marquee-card-video {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.4s var(--ease-out-expo);
}
.marquee-card-video.is-playing { opacity: 1; }

/* Desktop: size cards off the viewport, not the strip height — landscape
   ≈ 50vw, portrait ≈ 25vw (each including its trailing gap, so a landscape +
   gap == 50vw and two portraits + gaps == 50vw: a clean 50/25 rhythm). The
   strip keeps its original height (max 240px), so the wider cards render as
   slim horizontal crops — object-fit: cover does the cropping. Gated to
   > 48em so the mobile 80vw rule (max-width: 48em) is untouched — media
   queries add no specificity, and this selector (0,2,0) would otherwise
   outrank the mobile one (0,1,0). Landscape is picked off its inline --ar. */
@media (min-width: 48.0625em) {
  /* Strip height scales with the viewport but is capped at 256px; card widths
     derive from that height × each card's aspect, so the orientations hold
     (verticals portrait, horizontals landscape) even at the cap. */
  .marquee { --marquee-h: min((22.5vw - var(--marquee-gap)) * 360 / 520, 256px); }
  /* Each card is as wide as its own thumbnail: marquee.js writes the image's
     aspect to the inline --ar (vertical slots default to 246/360, horizontal
     to 520/360 when the image has no dimensions). */
  .marquee-card { width: calc(var(--marquee-h) * var(--ar, 246 / 360)); }
}


/* ============================================================
   Card hover — fullscreen image takeover (after the intro only)
   ============================================================ */

/* Pointer-only. iOS emulates :hover on tap, so without this gate a single tap
   on a card triggers the whole takeover — strip fades out, lockup and header
   disappear — with no way to dismiss it. `(hover: hover)` is the right test
   rather than a width query: it tracks the input device, not the screen. */
@media (hover: hover) {
  /* Hide ALL cards (including the hovered one) — the fullscreen viewer
     replaces the strip entirely */
  body.intro-done .marquee:has(.marquee-card:hover) .marquee-card {
    opacity: 0;
  }

  /* Hide the hero lockup, nav, sub-nav and corners behind the takeover */
  body.intro-done:has(.marquee-card:hover) .hero-title,
  body.intro-done:has(.marquee-card:hover) .site-header {
    opacity: 0;
  }
  body.intro-done:has(.marquee-card:hover) .subnav,
  body.intro-done:has(.marquee-card:hover) .corner {
    opacity: 0 !important;
  }

  /* Hovered card — hide the real cursor; the VIEW cursor stands in for it */
  body.intro-done .marquee-card:hover {
    cursor: none;
  }
}


/* Project + Director cursor — small horizontal label following the pointer
   over a hovered card, replacing the page cursor. Project (sans uppercase) on the
   left, Director (italic serif) on the right, 40px gap. Container positioned by
   card-viewer.js; children styled below. */
.card-cursor {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 500;                    /* above the marquee + viewer */
  pointer-events: none;
  opacity: 0;
  display: flex;
  align-items: flex-start;
  gap: 20px;                       /* project · play · director */
  color: #fff;                     /* sits over fullscreen viewer image (dark scrim handles contrast) */
  white-space: nowrap;
  will-change: transform;
  /* Runs on hover off; the .is-visible rule below overrides it on hover on, so
     the label racks in slower than it drops out. Only opacity is transitioned
     here — this element's transform is the pointer position, written inline by
     JS on every mousemove, and transitioning it would lag the cursor. */
  transition: opacity 0.225s ease;
}

.card-cursor.is-visible {
  opacity: 1;
  transition: opacity 0.375s var(--ease-out-expo);
}

/* Blur reveal: the label rises into focus from below on hover on, and blurs
   back out upwards on hover off. Keyframes rather than a transition because
   the two directions need different start points — a transition would have to
   share one resting state, which would send it back down the way it came.
   var(--ease-out-expo) is the CSS equivalent of GSAP's expo.out,
   matching the rest of the page. */
@keyframes card-cursor-in {
  from { filter: blur(var(--blur-reveal)); transform: translateY(16px); }
  to   { filter: blur(0px); transform: translateY(0); }
}

@keyframes card-cursor-out {
  from { filter: blur(0px); transform: translateY(0); }
  to   { filter: blur(var(--blur-reveal)); transform: translateY(-8px); }
}

/* Hover off. Also runs once at page load, harmlessly — the container is at
   opacity 0 until the first hover, so it plays out of sight. */
.card-cursor-client,
.card-cursor-play,
.card-cursor-director {
  animation: card-cursor-out 0.225s ease forwards;
}

/* Promote only where hover can actually happen — on touch these can never
   animate, so the layers were pure cost. */
@media (hover: hover) {
  .card-cursor-client,
  .card-cursor-play,
  .card-cursor-director {
    will-change: filter, transform;   /* blurred text re-rasterizes without it */
  }
}

/* Hover on. Swapping animation-name restarts it, so the rise always begins
   from below regardless of where the leave animation left off. The 16px start
   is deliberately further than the 8px the leave travels — the rise reads as
   the deliberate move, the exit as a smaller lift. */
.card-cursor.is-visible :is(.card-cursor-client, .card-cursor-play, .card-cursor-director) {
  animation: card-cursor-in 0.375s var(--ease-out-expo) forwards;
}

@media (prefers-reduced-motion: reduce) {
  /* Container: kill the fade only. Its transform is the pointer position, set
     inline by JS — leave it alone. */
  .card-cursor,
  .card-cursor.is-visible {
    transition: none;
  }

  /* Children: show them in their settled state with no motion or blur. */
  .card-cursor-client,
  .card-cursor-play,
  .card-cursor-director,
  .card-cursor.is-visible :is(.card-cursor-client, .card-cursor-play, .card-cursor-director) {
    animation: none;
    filter: none;
    transform: none;
    will-change: auto;
  }
}

.card-cursor-client {
  margin: 0;
  font-family: var(--ff-sans);
  font-weight: 600;
  font-size: 12px;
  line-height: 1.2;
  text-transform: uppercase;
}

.card-cursor-director {
  margin: 0;
  font-family: var(--ff-serif-italic);
  font-style: italic;
  font-weight: 400;
  font-size: 12px;
  line-height: 1.2;
}

/* Mini play triangle between project + director (work-grid cursor) */
.card-cursor-play {
  display: inline-flex;
  align-items: center;
  align-self: center;   /* vertically centre the icon against the labels */
}

/* The fullscreen viewer's big top/bottom client+director labels are hidden in v2 —
   the small cursor label above is the only on-hover text. JS still populates the
   underlying elements, harmlessly. */
.card-viewer-client,
.card-viewer-director {
  display: none;
}


/* ============================================================
   Card viewer — fixed fullscreen image with client + director names
   ============================================================ */

.card-viewer {
  position: fixed;
  inset: 0;
  z-index: 55;                     /* above hero + nav, below the marquee */
  opacity: 0;
  pointer-events: none;            /* never intercepts the card hover */
}

.card-viewer.is-visible {
  opacity: 1;
}

/* ---- Work-page state (hero-transition.js) ------------------------------
   Once a card is clicked the viewer stops being a passive hover overlay and
   becomes the work page: it takes pointer events and sits above the marquee. */
.card-viewer.is-work {
  pointer-events: auto;
  z-index: 100;                    /* above the marquee (60), below the cursor */
}

/* Record light — a small dot, top-right, that blinks during the ~2s "loading"
   beat. mix-blend-mode: exclusion inverts it against whatever image it's over,
   so it reads on light or dark stills. Hidden until JS adds .is-blinking. */
.record-dot {
  position: fixed;
  top: calc(var(--pad-inline) + var(--safe-top));
  right: var(--pad-inline);
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: #fff;
  opacity: 0;
  pointer-events: none;
  z-index: 110;
}
.record-dot.is-blinking {
  animation: record-blink 1.1s ease-in-out infinite;
}
@keyframes record-blink {
  0%, 100% { opacity: 0; }
  50% { opacity: 1; }
}


.card-viewer-bg {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
}


/* The film, over the still. Held back for a beat by card-viewer.js and only
   faded up once playback actually starts, so the still is never replaced by a
   black rectangle if the file is slow or autoplay is refused. */
.card-viewer-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 0.3s var(--ease-out-expo);
}
.card-viewer-video.is-playing { opacity: 1; }

/* faint scrim keeps the white names legible on light images */
.card-viewer-bg::after {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.16);
}

.card-viewer-client,
.card-viewer-director {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  margin: 0;
  color: #fff;
  line-height: 1;                  /* 100% */
  white-space: nowrap;
}

/* Client — Epilogue SemiBold, 10px from the top edge */
.card-viewer-client {
  top: calc(var(--pad-inline) + var(--safe-top));
  font-family: var(--ff-sans);
  font-weight: 600;
  font-size: 3.125vw;              /* 60px @ 1920 */
  text-transform: uppercase;
}

/* Director — Libre Caslon Text Italic, 10px from the bottom edge */
.card-viewer-director {
  bottom: var(--pad-inline);
  font-family: var(--ff-serif-italic);
  font-style: italic;
  font-weight: 400;
  font-size: 3.125vw;              /* 60px @ 1920 */
}


/* ---- Mobile layout (≤48em) — MUST stay last in this sheet: media queries
   add no specificity, so source order decides. ---- */
@media (max-width: 48em) {
  /* Every card the same width, whatever its --ar. Simpler than deriving the
     width from the strip height, and it gives a predictable one-card-plus-a-
     peek. The img is object-fit: cover, so the differing source ratios just
     crop. Desktop keeps calc(--marquee-h * --ar) and its alternating shapes. */
  .marquee-card {
    width: calc(var(--marquee-h) * var(--ar, 246 / 360));   /* the card's own shape, like desktop (QA 2026-10-08) */
  }
  /* "all work" is underlined permanently on touch — it only ever picked up the
     shared hover rule, which can't fire here. */
  .work-cta a {
    text-decoration: underline;
    text-underline-offset: var(--ul-offset);
    text-decoration-thickness: var(--ul-thickness);
  }
}
