/* ==========================================================================
   howardtaylor.net — Step 3: texture and asset pass
   Real images, real videos, painted textures, torn paper edges, cream bg.
   No motion yet (Step 4 adds grain, drift, hover, header scroll).
   ========================================================================== */

/* Fonts ===================================================================== */

@font-face {
  font-family: 'Cabinet Grotesk';
  src: url('fonts/CabinetGrotesk-Light.woff2') format('woff2');
  font-weight: 300; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Cabinet Grotesk';
  src: url('fonts/CabinetGrotesk-Regular.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Cabinet Grotesk';
  src: url('fonts/CabinetGrotesk-Medium.woff2') format('woff2');
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Cabinet Grotesk';
  src: url('fonts/CabinetGrotesk-Bold.woff2') format('woff2');
  font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Cabinet Grotesk';
  src: url('fonts/CabinetGrotesk-Extrabold.woff2') format('woff2');
  font-weight: 800; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Cabinet Grotesk';
  src: url('fonts/CabinetGrotesk-Black.woff2') format('woff2');
  font-weight: 900; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Zodiak';
  src: url('fonts/Zodiak-Regular.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Zodiak';
  src: url('fonts/Zodiak-Italic.woff2') format('woff2');
  font-weight: 400; font-style: italic; font-display: swap;
}
@font-face {
  font-family: 'Zodiak';
  src: url('fonts/Zodiak-Bold.woff2') format('woff2');
  font-weight: 700; font-style: normal; font-display: swap;
}

/* Tokens ==================================================================== */

:root {
  --teal-dark: #173943;
  --teal-mid: #326b5d;       /* Heading green on cream backgrounds */
  --teal-light: #a2ddd2;
  --terracotta-mid: #de6655;
  --terracotta-dark: #a53d32;
  --cream: #f2eddf;          /* Type colour on dark backgrounds (matches bg) */
  --cream-bg: #f2eddf;        /* Page background */

  /* Fluid wordmark: scales smoothly between 56px (very narrow) and 177px
     (1310px+ viewport). 13.5vw hits ~104px @ 768, ~138px @ 1024, ~176px @ 1300.
     Steeper than 11vw so the wordmark fills its column at mid widths instead
     of looking marooned in empty teal. Caps at 177px on big desktops. */
  --display-1: clamp(56px, 13.5vw, 177px);
  --display-2: 100px;
  --display-3: 80px;
  --heading-a: 59px;
  --heading-b: 47px;
  --sub-heading: 36px;
  --sub-label: 29px;
  --body: 18px;
  --micro: 14px;

  --font-display: 'Cabinet Grotesk', sans-serif;
  --font-editorial: 'Zodiak', Georgia, serif;
  --font-body: 'Cabinet Grotesk', sans-serif;

  --torn-height: 70px;
}

/* Reset and base ============================================================ */

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: 16px; }
body {
  font-family: var(--font-body);
  font-size: var(--body);
  line-height: 1.5;
  color: var(--teal-dark);
  background: var(--cream-bg);
}

/* Animated film grain overlay across the entire viewport.
   Sits on top of everything (z-index 9999), pointer-events none so it
   doesn't block clicks. Subtle opacity + blend mode for the "alive" feel. */
body::after {
  content: '';
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  background-image: url('images/grain-noise.jpg');
  background-size: 200px 200px;
  background-repeat: repeat;
  pointer-events: none;
  opacity: 0.08;
  z-index: 9999;
  mix-blend-mode: multiply;
  animation: grain-shift 4s steps(4) infinite;
}

@keyframes grain-shift {
  0% { transform: translate(0, 0); }
  25% { transform: translate(-1%, 0); }
  50% { transform: translate(0, 1%); }
  75% { transform: translate(1%, 0); }
  100% { transform: translate(0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  body::after { animation: none; }
}
p { margin-bottom: 1rem; }
a { color: inherit; text-decoration: underline; }
em { font-family: var(--font-editorial); font-style: italic; }
img { max-width: 100%; height: auto; display: block; }

/* Layout containers ========================================================= */

.container { max-width: 1200px; margin: 0 auto; padding: 0 32px; }
.container.medium { max-width: 880px; }
.container.narrow { max-width: 720px; }

section { padding: 24px 0; position: relative; }

/* ==========================================================================
   Header — over hero, transparent
   ========================================================================== */
.site-header {
  position: absolute;
  top: 0; left: 0; right: 0;
  padding: 20px 0;
  z-index: 20;
}
.header-inner { display: flex; justify-content: space-between; align-items: center; }
.wordmark {
  font-family: var(--font-body); font-weight: 900;
  font-size: 30px; letter-spacing: 0.02em;
  color: var(--cream);
}

/* ==========================================================================
   Email button (used in header SAY HELLO and closer mailto)
   ========================================================================== */
.email-button {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  padding: 14px 24px;
  background: rgba(0, 0, 0, 0.25);
  border-radius: 6px;
  text-decoration: none;
  color: var(--cream);
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 16px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  transition: background-color 0.2s ease-out;
}
.email-button:hover { background: rgba(0, 0, 0, 0.4); }

.email-button-large {
  font-size: 18px;
  padding: 16px 28px;
  margin-top: 12px;
  letter-spacing: 0.02em;
  text-transform: none;
}

.email-icon {
  width: 22px;
  height: auto;
  display: block;
  filter: brightness(0) invert(1) opacity(0.95);
}

.email-button-large .email-icon {
  width: 26px;
}

/* ==========================================================================
   Coloured band sections (background textures + torn edges)
   ========================================================================== */

/* Dark teal sections: hero, beat-3, footer.
   z-index: 2 lifts the section above sibling cream sections so its
   pseudo-elements (torn edges) can extend into them without being covered.
   Texture image lives on .parallax-bg child, not on the section itself. */
.hero,
.beat-3 {
  background-color: var(--teal-dark);
  color: var(--cream);
  position: relative;
  z-index: 2;
}

/* Terracotta sections: beat-4-numbers, closer */
.beat-4-numbers,
.closer {
  background-color: var(--terracotta-dark);
  color: var(--cream);
  position: relative;
  z-index: 2;
}

/* Parallax background layer
   .parallax-wrap clips the bg to the section (overflow: hidden).
   .parallax-bg is sized 30% taller than its wrap and translated by JS
   on scroll. The translation rate is slower than scroll, so foreground
   appears to lift past the bg — the dominiquesire-style parallax. */
.parallax-wrap {
  position: absolute;
  inset: 0;
  overflow: hidden;
  z-index: 0;
  pointer-events: none;
}

.parallax-bg {
  position: absolute;
  left: 0; right: 0;
  /* Generous overhang so the JS translate never reveals an edge.
     The bg layer is taller than the section by 50vh top + 50vh bottom. */
  top: -50vh;
  bottom: -50vh;
  will-change: transform;
  /* JS sets transform: translate3d(0, Ypx, 0) on scroll */
}

/* Inner drift layer carries the actual texture image. It sits inside the
   parallax-bg and runs its own slow ambient drift via CSS keyframes.
   The outer .parallax-bg gets the JS-driven scroll transform; this inner
   .parallax-drift gets the breathing animation. The two transforms compose.
   inset: -80px gives buffer for the translate so no edge ever shows. */
.parallax-drift {
  position: absolute;
  inset: -80px;
  background-size: cover;
  background-position: center center;
  background-repeat: no-repeat;
  animation: texture-drift 60s ease-in-out infinite alternate;
}

.parallax-drift-teal {
  background-image: url('images/texture-darkteal.webp');
}

.parallax-drift-terracotta {
  background-image: url('images/texture-terracotta.webp');
}

/* Slow breathing drift: 60px translation + 1.04 scale over 60s.
   Clearly visible when stationary, swamped slightly when scrolling
   (which is fine — drift is for moments of pause). */
@keyframes texture-drift {
  0%   { transform: translate3d(0, -60px, 0) scale(1.04); }
  100% { transform: translate3d(0,  60px, 0) scale(1.04); }
}

/* Stagger so sections don't drift in lockstep */
.hero .parallax-drift { animation-delay: 0s; }
.beat-3 .parallax-drift { animation-delay: -22s; }
.beat-4-numbers .parallax-drift { animation-delay: -45s; }
.closer .parallax-drift { animation-delay: -67s; }

@media (prefers-reduced-motion: reduce) {
  .parallax-drift { animation: none; }
}

/* Lift content above the parallax bg.
   Note: .spark-layer already has its own position: absolute; inset: 0; z-index: 1
   from its own rule — don't touch it here, just lift the .container blocks. */
.hero > .container,
.beat-3 > .container,
.beat-4-numbers > .container,
.closer > .container {
  position: relative;
  z-index: 1;
}

/* Cream sections (default) sit on body cream-bg, no extra background needed */

/* ==========================================================================
   Torn paper edges
   The SVGs are filled with the cream colour and visually represent cream
   paper tearing into the coloured section. Top SVG goes at the top of a
   coloured section, bottom SVG at the bottom.
   ========================================================================== */

.hero::after,
.beat-3::before,
.beat-3::after,
.beat-4-numbers::before,
.beat-4-numbers::after,
.closer::before {
  content: '';
  position: absolute;
  left: 0; right: 0;
  height: var(--torn-height);
  pointer-events: none;
  z-index: 50;
  background-repeat: no-repeat;
  background-size: 100% 100%;
  display: block;
}

/* Top torn edge: SVG extends partially above the section into the cream
   section above. Cream parts of SVG are invisible against cream bg, only the
   torn shape (where SVG transitions to transparent) is visible inside the
   colored section, creating the torn boundary at the section top. */
.beat-3::before,
.beat-4-numbers::before,
.closer::before {
  top: -30px;
  background-image: url('images/torn-edge-bottom.svg');
  background-position: top center;
}

/* Bottom torn edge: SVG extends partially below the section into the cream
   section below. Same principle: cream parts invisible against cream bg,
   torn shape visible inside the colored section at the bottom boundary. */
.hero::after,
.beat-3::after,
.beat-4-numbers::after {
  bottom: -30px;
  background-image: url('images/torn-edge-top.svg');
  background-position: bottom center;
}

/* Closer's bottom transitions to dark teal footer (no cream torn edge there) */

/* ==========================================================================
   Hero — Display 1 wordmark on dark teal, with halftone portrait
   ========================================================================== */
.hero {
  padding: 96px 0 0;
  display: flex;
  align-items: flex-end;
  min-height: 70vh;
}

.hero-grid {
  display: grid;
  grid-template-columns: 1.5fr 1fr;
  gap: 0;
  align-items: end;
  position: relative;
  width: 100%;
}

.hero-wordmark {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: var(--display-1);
  line-height: 0.72;
  letter-spacing: -0.025em;
  color: var(--cream);
  text-transform: uppercase;
  position: relative;
  z-index: 1;
  margin: 0;
  /* Nudge down to compensate for Cabinet Grotesk's internal leading (descender space).
     The font's line box sits below the visible cap baseline by ~25px at this size,
     so we shift the wordmark down to align with the portrait's visible bottom. */
  transform: translateY(12px);
}

.hero-portrait {
  width: 100%;
  height: auto;
  margin-left: -8%;
  z-index: 2;
  position: relative;
}

/* ==========================================================================
   Tagline — Heading A Zodiak
   ========================================================================== */
.tagline { text-align: center; padding-bottom: 8px; }
.tagline-text {
  font-family: var(--font-editorial);
  font-weight: 400;
  font-size: var(--heading-a);
  line-height: 1.2;
  letter-spacing: -0.05em;
  color: var(--teal-mid);
  margin-bottom: 0;
}
.tagline-text em { color: var(--teal-mid); }

/* ==========================================================================
   Personal opener
   ========================================================================== */
.opener { text-align: center; padding-top: 8px; }
.opener-body { font-size: var(--body); line-height: 1.5; }

.transition {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: var(--heading-b);
  line-height: 1.15;
  letter-spacing: 0.02em;
  color: var(--teal-mid);
  text-transform: uppercase;
  margin-top: 32px;
}

/* ==========================================================================
   Beats — sentence-case headlines (Heading A) and punch lines (Heading B)
   ========================================================================== */
.beat { text-align: left; }
.beat .punch { text-align: center; }

/* Centered beats */
.beat-3, .beat-3 .beat-headline,
.beat-6, .beat-6 .beat-headline { text-align: center; }

/* Beat 4 setup keeps headline+punch left-aligned in the text column.
   Explicit font-size needed because .beat-body p rule overrides .punch otherwise. */
.beat-4-setup .punch {
  text-align: left;
  margin-top: 16px;
  font-size: var(--heading-b);
  line-height: 1.15;
}

.beat-headline {
  font-family: var(--font-editorial);
  font-weight: 400;
  font-size: 48px;
  line-height: 1.2;
  letter-spacing: -0.03em;
  color: var(--teal-mid);
  margin-bottom: 24px;
  text-align: left;
}
.beat-headline-on-dark { color: var(--cream); }

.beat-body { text-align: left; }
.beat-body p {
  font-size: var(--body); line-height: 1.5; margin-bottom: 1rem;
}
.body-on-dark { color: var(--cream); }

/* Asymmetric grids for beats with inline images */
.beat-grid {
  display: grid;
  grid-template-columns: 1.6fr 1fr;
  gap: 48px;
  align-items: start;
  margin: 0;
}
.beat-grid-reverse { grid-template-columns: 1fr 1.6fr; }

.beat-image,
.beat-video {
  width: 100%;
  height: auto;
  display: block;
  /* No rounded corners on inline images */
}

/* ==========================================================================
   Beat 2 — CRT boot-up sequence
   Three stacked images: blank screen (base), noise (mid), UNDO (top).
   On scroll-in, two parallel keyframe animations fire — noise strobes
   chaotically while UNDO crystallizes through the static. About 1.2s total,
   one-shot, then UNDO holds steady forever.
   ========================================================================== */

.beat-image-crt {
  position: relative;
  width: 68%;
  margin: 0 auto;
}

/* ==========================================================================
   Beat 4 — Three-layer parallax on the bored secretary
   bored_1: backdrop (static, sets layout)
   bored_2: shadow (scales 1.10→1.20 + translateX 0px→8px on scroll)
   bored_3: foreground figure (scales 1.15→1.30 on scroll, "looming closer")
   The differential scale + shadow's horizontal shift creates depth — the
   shadow appears to swing into place beneath the figure as she advances.
   ========================================================================== */
.beat-image-bored {
  position: relative;
}

.beat-image-bored > img:nth-child(1) {
  width: 100%;
  height: auto;
  display: block;
}

.beat-image-bored > img:nth-child(2),
.beat-image-bored > img:nth-child(3) {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  transform-origin: center center;
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  .beat-image-bored > img:nth-child(2),
  .beat-image-bored > img:nth-child(3) {
    transform: none !important;
  }
}

/* ==========================================================================
   Beat 5 — Three-layer parallax on the engaged audience
   engaged_1: backdrop (static, sets layout)
   engaged_2: shadow (scales 1.20→1.10 + translateX 8px→0px on scroll)
   engaged_3: foreground figures (scales 1.30→1.15 on scroll)
   Same parallax structure as Beat 4, but scale direction inverted: the
   audience starts oversized and "settles" into focus as you scroll —
   like the moment of unanimous engagement crystallizing.
   ========================================================================== */
.beat-image-engaged {
  position: relative;
}

.beat-image-engaged > img:nth-child(1) {
  width: 100%;
  height: auto;
  display: block;
}

.beat-image-engaged > img:nth-child(2),
.beat-image-engaged > img:nth-child(3) {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  transform-origin: center center;
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  .beat-image-engaged > img:nth-child(2),
  .beat-image-engaged > img:nth-child(3) {
    transform: none !important;
  }
}

/* ==========================================================================
   Beat 1 — Scroll-driven zoom on the lawnmower
   Two stacked images: brush stroke / paint splat (base, never moves) and
   the lawnmower itself (top layer, scales from ~1.10 down to 1.0 as the
   user scrolls into view). JS sets transform: scale() based on scroll
   position via requestAnimationFrame.
   ========================================================================== */
.beat-image-zoom {
  position: relative;
}

/* Base image (brush stroke bg) sets the layout height naturally, fixed scale */
.beat-image-zoom > img:nth-child(1) {
  width: 100%;
  height: auto;
  display: block;
}

/* Lawnmower stacks on top, scales via JS-set transform.
   transform-origin: center keeps the lawnmower centered as it scales. */
.beat-image-zoom > img:nth-child(2) {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  transform-origin: center center;
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  .beat-image-zoom > img:nth-child(2) {
    transform: none !important;
  }
}

/* Base image (blank screen) sets the layout height naturally */
.beat-image-crt > img:nth-child(1) {
  width: 100%;
  height: auto;
  display: block;
}

/* Noise (image 2) and UNDO (image 3) stack on top, hidden by default */
.beat-image-crt > img:nth-child(2),
.beat-image-crt > img:nth-child(3) {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  opacity: 0;
  pointer-events: none;
}

/* Trigger animations when .is-on lands on .beat-2.
   forwards = hold the final state of the keyframe. */
.beat-2.is-on .beat-image-crt > img:nth-child(2) {
  animation: crt-noise 1.2s linear 1 forwards;
}
.beat-2.is-on .beat-image-crt > img:nth-child(3) {
  animation: crt-undo 1.2s ease-out 1 forwards;
}

/* Noise: chaotic strobe in phase 2, fades out in phase 3 as UNDO rises.
   The brief return at 50% is the overlap with UNDO emergence —
   that's what sells "UNDO resolving from the static" rather than cross-fade. */
@keyframes crt-noise {
  0%   { opacity: 0; }
  16%  { opacity: 1; }
  20%  { opacity: 0; }
  28%  { opacity: 1; }
  32%  { opacity: 0; }
  40%  { opacity: 1; }
  46%  { opacity: 0; }
  52%  { opacity: 0.6; }
  62%  { opacity: 0; }
  100% { opacity: 0; }
}

/* UNDO: silent through phase 1 and most of phase 2, then a faint ghost
   shows through the final noise flashes, micro-flickers, and locks in. */
@keyframes crt-undo {
  0%   { opacity: 0; }
  50%  { opacity: 0; }
  56%  { opacity: 0.4; }
  62%  { opacity: 0; }
  72%  { opacity: 1; }
  78%  { opacity: 0.3; }
  86%  { opacity: 1; }
  100% { opacity: 1; }
}

/* Reduced motion: skip the boot-up, show UNDO immediately. */
@media (prefers-reduced-motion: reduce) {
  .beat-image-crt > img:nth-child(2) { display: none; }
  .beat-image-crt > img:nth-child(3) { opacity: 1; }
  .beat-2.is-on .beat-image-crt > img:nth-child(2),
  .beat-2.is-on .beat-image-crt > img:nth-child(3) {
    animation: none;
  }
}

/* ==========================================================================
   Beat 1 / Beat 2 layout: headline + image side-by-side, body centered narrow below
   ========================================================================== */

.beat-row {
  display: grid;
  grid-template-columns: 1.6fr 1fr;
  gap: 48px;
  align-items: center;
  margin-bottom: 32px;
}

.beat-row-image-left {
  grid-template-columns: 1fr 1.6fr;
}

.beat-row > .beat-headline {
  text-align: left;
  margin-bottom: 0;
  align-self: end;
}

.beat-narrow-body {
  max-width: 600px;
  margin: 0 auto;
  text-align: center;
}

.beat-narrow-body p {
  font-size: var(--body);
  line-height: 1.5;
  margin-bottom: 1rem;
}

/* ==========================================================================
   Beat 5 layout: asymmetric two-column, headline + body left, video right
   ========================================================================== */

.beat-asymmetric {
  display: grid;
  grid-template-columns: 1.6fr 1fr;
  gap: 48px;
  align-items: start;
}

/* Reverse: image on the left, text on the right */
.beat-asymmetric-reverse {
  grid-template-columns: 1fr 1.6fr;
}

.beat-text {
  text-align: left;
}

.beat-text .beat-headline {
  text-align: left;
  margin-bottom: 16px;
}

.beat-text p {
  font-size: var(--body);
  line-height: 1.5;
  margin-bottom: 1rem;
}

/* Punch lines (Cabinet Grotesk Black caps, equivalent visual weight to Heading A) */
.punch {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: var(--heading-b);
  line-height: 1.15;
  letter-spacing: 0.02em;
  color: var(--teal-mid);
  text-transform: uppercase;
  margin-top: 16px;
}
.punch-on-dark { color: var(--cream); }

.punch-small {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: 24px;
  letter-spacing: 0.02em;
  color: var(--teal-mid);
  text-transform: uppercase;
  margin-top: 24px;
}

/* ==========================================================================
   Beat 3 — dark teal, logos
   ========================================================================== */
.beat-3 {
  padding: 80px 0 80px;
}
.beat-3 .beat-headline {
  color: var(--cream);
}

.logo-row {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 48px;
  margin: 32px 0;
  align-items: center;
}
.logo {
  width: 100%;
  height: auto;
  max-height: 60px;
  object-fit: contain;
  filter: brightness(0) invert(1);
}

/* ==========================================================================
   Beat 4 setup — cream background, with dark teal video block
   ========================================================================== */
.beat-4-setup {
  background: var(--cream-bg);
  color: var(--teal-dark);
  /* Extra breathing room below the bored-secretary image before the
     numbers section begins. Lets the looming-figure parallax resolve
     and the eye reset before "Twenty years in..." lands. */
  padding-bottom: 88px;
}

.beat-4-setup .beat-video {
  background: var(--teal-dark);
  padding: 16px;
}

/* ==========================================================================
   Beat 4 numbers — terracotta, monumental numbers
   ========================================================================== */
.beat-4-numbers {
  padding: 56px 0 56px;
}

.numbers-intro { text-align: center; margin-bottom: 32px; }

.numbers-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 32px;
}

.number-cell {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  /* Initial state: lightly blurred and lifted, awaiting scroll reveal.
     Each cell reveals at a different scroll threshold via JS so they
     don't all appear at once — cell 2 and 3 wait for more scrolling. */
  opacity: 0.15;
  transform: translateY(20px);
  filter: blur(6px);
  transition:
    opacity 0.7s ease-out,
    transform 0.7s ease-out,
    filter 0.7s ease-out;
}

.number-cell.is-revealed {
  opacity: 1;
  transform: translateY(0);
  filter: blur(0);
}

@media (prefers-reduced-motion: reduce) {
  .number-cell {
    opacity: 1;
    transform: none;
    filter: none;
    transition: none;
  }
}
.number-label,
.number-sub {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: var(--sub-label);
  line-height: 1.3;
  letter-spacing: -0.025em;
  color: var(--cream);
  text-transform: uppercase;
}
.number-sub { text-align: center; }
.number-value {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: var(--display-3);
  line-height: 1.0;
  color: var(--cream);
  text-transform: uppercase;
  margin: 4px 0;
}

/* ==========================================================================
   Transition section between terracotta numbers and Beat 5 (on cream)
   ========================================================================== */
.transition-section { text-align: center; padding: 16px 0; }
.transition-setup {
  font-family: var(--font-editorial); font-weight: 400;
  font-size: var(--heading-a); line-height: 1.2;
  letter-spacing: -0.05em;
  color: var(--teal-mid);
  margin-bottom: 16px;
}

/* ==========================================================================
   Beat 5 — dark teal video on cream background
   ========================================================================== */
.beat-5 .beat-video {
  background: var(--teal-dark);
  padding: 16px;
}

/* ==========================================================================
   Beat 6 — spark pyramid
   ========================================================================== */
.beat-6 .beat-grid { align-items: center; }

/* Stack wrapper holds four layered SVGs for the build sequence.
   DOM order = paint order (back → front):
   1. spark-pyramid-gradient (BEHIND, absolute) — reveals bottom-up
   2. spark-pyramid-base (in flow, sets dimensions) — fades in first
   3. spark-pyramid-mark (absolute, on top) — scales + fades, the payoff
   4. spark-pyramid-text (absolute, on top) — SPARK label fades in
   The pyramid base is the only element in normal flow, so it dictates the
   stack's height. Everything else absolute-positions to fill the same box. */
.spark-pyramid-stack {
  position: relative;
  width: 55%;
  margin: 0 auto;
  display: block;
}

.spark-pyramid {
  width: 100%;
  height: auto;
  display: block;
  position: relative; /* sit above the absolute-positioned gradient */
}

/* Gradient sits BEHIND the pyramid — first in DOM, absolute-positioned
   so it doesn't take layout space. Pyramid (later in DOM) paints on top. */
.spark-pyramid-gradient {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  /* Hidden initially via clip-path, then revealed bottom-up */
  clip-path: inset(100% 0 0 0);
  transition: clip-path 1.5s ease-out 0.6s;
}

.spark-pyramid-base {
  /* Pyramid is the foundation — fades in first to set the structure.
     The pyramid SVG has transparent interior so the gradient behind it
     shows through the open triangle as a glow/fill. */
  opacity: 0;
  transition: opacity 0.6s ease-out;
}

/* Spark mark: just the spark graphic at the apex. Scales + fades in
   from where the spark actually sits, not the canvas centre.
   The SVG is sized to match the pyramid canvas (so positioning aligns),
   which means transform-origin defaults would scale from canvas centre.
   Override to point at the apex where the spark lives — roughly top-centre. */
.spark-pyramid-mark {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: 0;
  transform: scale(0.8);
  /* Anchor the scale where the spark actually sits in the canvas.
     Tweak both values to nudge the origin point — lower x = scale from
     further left, lower y = scale from higher up. */
  transform-origin: 35% 16%;
  transition:
    opacity 0.6s ease-out 1.8s,
    transform 0.6s ease-out 1.8s;
}

/* SPARK text label: fades in only, no scale. Stays in its proper layout
   position. Slight delay (1.95s) puts it just behind the mark, so SPARK
   reads as punctuation arriving after the spark graphic. */
.spark-pyramid-text {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.6s ease-out 1.95s;
}

/* Trigger: .is-revealed lands on .beat-6 via IntersectionObserver */
.beat-6.is-revealed .spark-pyramid-base {
  opacity: 1;
}
.beat-6.is-revealed .spark-pyramid-gradient {
  clip-path: inset(0 0 0 0);
}
.beat-6.is-revealed .spark-pyramid-mark {
  opacity: 1;
  transform: scale(1);
}
.beat-6.is-revealed .spark-pyramid-text {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .spark-pyramid-base,
  .spark-pyramid-gradient,
  .spark-pyramid-mark,
  .spark-pyramid-text {
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
    transition: none !important;
  }
}

/* ==========================================================================
   Project cards
   ========================================================================== */
/* Beat 7 (WHAT I DO NOW) tightens its bottom padding so the project cards
   sit closer to "I'm driven by play and curiosity..." line. */
.beat-7 { padding-bottom: 0; }
.beat-7 .opener-body { margin-bottom: 0; }

.project-cards { text-align: center; padding-top: 16px; }

.cards-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 32px;
  text-align: left;
}

.card {
  display: flex; flex-direction: column; gap: 16px;
  text-decoration: none; color: inherit;
}

/* Card thumbnail wraps image and the title overlaid on top */
.card-thumb {
  position: relative;
  display: block;
  overflow: hidden;
}

.card-image {
  width: 100%;
  height: auto;
  display: block;
}

.card-title {
  position: absolute;
  top: 0; left: 0;
  width: 100%;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 24px 16px 16px;
  font-family: var(--font-display); font-weight: 900;
  font-size: var(--sub-heading); line-height: 1.15;
  letter-spacing: 0.02em;
  color: var(--cream);
  text-transform: uppercase;
  text-align: center;
  z-index: 2;
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
}

.card-body {
  color: var(--teal-dark);
  font-size: var(--body);
  line-height: 1.5;
}

/* ==========================================================================
   Closer — terracotta, Display 2 thesis
   ========================================================================== */
.closer {
  text-align: center;
  padding: 80px 0 64px;
}
.closer-setup {
  font-family: var(--font-editorial); font-weight: 400;
  font-size: var(--heading-a); line-height: 1.2;
  letter-spacing: -0.05em;
  color: var(--cream);
  margin-bottom: 16px;
}
.closer-setup em { color: var(--cream); }

/* Spark twinkle layer in hero and closer.
   Multiple spark.svg instances scattered as bg decoration.
   Each twinkles with scale + opacity, subtle and on-brand. */
.spark-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  z-index: 1;
}

.spark {
  position: absolute;
  height: auto;
  opacity: 0;
  transform: scale(0.3);
  transform-origin: center;
  animation: spark-twinkle 7s ease-in-out infinite;
  animation-play-state: paused;
}

/* Animation runs only when the parent section is visible (.is-visible class
   added by IntersectionObserver), so off-screen sparks don't burn cycles. */
.is-visible .spark {
  animation-play-state: running;
}

@keyframes spark-twinkle {
  0%, 100% {
    opacity: 0;
    transform: scale(0.3);
  }
  20% {
    opacity: 0.42;
    transform: scale(1);
  }
  40% {
    opacity: 0.18;
    transform: scale(1.15);
  }
  60% {
    opacity: 0;
    transform: scale(1.3);
  }
}

@media (prefers-reduced-motion: reduce) {
  .spark { animation: none; opacity: 0.2; transform: scale(1); }
}

/* Scroll-triggered fade + blur on "disappears" word in the closer.
   IntersectionObserver adds .is-faded when the span enters the viewport. */
.fade-on-scroll {
  opacity: 1;
  filter: blur(0);
  display: inline-block;
  transition: opacity 1.2s ease-out, filter 1.2s ease-out;
}

.fade-on-scroll.is-faded {
  opacity: 0.4;
  filter: blur(2px);
}

@media (prefers-reduced-motion: reduce) {
  .fade-on-scroll {
    transition: none;
  }
}

/* ==========================================================================
   Punch-line reveals — two variations
   .reveal-words: word-by-word stagger with blur + slide. Active feel.
                  JS splits text into <span class="word"> on load and
                  sets per-word transition-delay inline.
   .reveal-line:  whole-line single fade-up. Passive feel.
                  No word splitting — line transitions as one unit.
   ========================================================================== */

/* Variation A — word stagger (the "spoken" punches) */
.reveal-words .word {
  display: inline-block;
  opacity: 0;
  transform: translateY(12px);
  filter: blur(4px);
  transition:
    opacity 0.5s ease-out,
    transform 0.5s ease-out,
    filter 0.5s ease-out;
}

.reveal-words.is-revealed .word {
  opacity: 1;
  transform: translateY(0);
  filter: blur(0);
}

/* Variation B — whole-line fade-up (the gentle connectors) */
.reveal-line {
  opacity: 0;
  transform: translateY(8px);
  transition:
    opacity 0.6s ease-out,
    transform 0.6s ease-out;
}

.reveal-line.is-revealed {
  opacity: 1;
  transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  .reveal-words .word,
  .reveal-line {
    opacity: 1;
    transform: none;
    filter: none;
    transition: none;
  }
}

.closer-thesis {
  font-family: var(--font-display); font-weight: 900;
  font-size: var(--display-2); line-height: 1.0;
  letter-spacing: -0.01em;
  color: var(--cream);
  text-transform: uppercase;
  margin-bottom: 24px;
}

.closer-mailto { font-size: var(--body); color: var(--cream); }
.closer-mailto a { color: var(--cream); text-decoration: underline; }

/* ==========================================================================
   Footer — dark teal thin strip (no torn edge, butts up against closer)
   ========================================================================== */
.site-footer {
  background-color: var(--teal-dark);
  background-image: url('images/texture-darkteal.webp');
  background-size: cover;
  background-position: center;
  color: var(--cream);
  padding: 24px 0;
}

.site-footer .container {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 24px;
}

.footer-credit, .footer-copyright {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: var(--micro);
  line-height: 1.5;
  letter-spacing: 0.04em;
  color: var(--cream);
  margin: 0;
}

.footer-credit { text-align: left; }
.footer-copyright { text-align: right; }

/* ==========================================================================
   Mobile (below 768px)
   ========================================================================== */
@media (max-width: 768px) {
  :root {
    /* On mobile (stacked layout) the wordmark sits below the portrait
       and wants real visual weight. 16vw gives ~83px floor on narrow
       phones and ~123px at the 768px breakpoint, another ~15% bump on
       the previous 14vw / 114px cap. */
    --display-1: clamp(83px, 16vw, 128px);
    --display-2: 56px;
    --display-3: 48px;
    --heading-a: 40px;
    --heading-b: 32px;
    --sub-heading: 28px;
    --sub-label: 22px;
    --body: 16px;
    --micro: 13px;
    /* --torn-height stays at 70px on mobile for consistent torn-edge visibility */
  }

  body { font-size: var(--body); line-height: 1.5; }

  section { padding: 32px 0; }

  .container { padding: 0 20px; }

  /* Hero on mobile: explicit flex column with content pushed to the bottom
     of the section. Tiny 5px bottom padding lifts the wordmark a touch off
     the absolute bottom so the translateY(12px) doesn't dip the visible
     cap line too deeply into the torn-paper zone. */
  .hero {
    position: relative;
    flex-direction: column;
    justify-content: flex-end;
    align-items: center;
    padding: 48px 0 5px;
  }

  .hero-grid {
    display: flex;
    flex-direction: column;
    align-items: center;
    grid-template-columns: 1fr;
    gap: 0;
    text-align: center;
    position: static;
    width: 100%;
  }

  .hero-portrait {
    order: 1;
    display: block;
    width: 70%;
    max-width: 480px;
    height: auto;
    margin: 0;
    position: relative;
    z-index: 1;
  }

  .hero-wordmark {
    order: 2;
    position: relative;
    /* Negative top margin pulls the wordmark up over the bottom of the
       portrait. Adjust this value to control how much overlap there is. */
    margin: -64px 0 0;
    padding: 0 16px;
    text-align: center;
    z-index: 2;
    /* translateY(12px) compensates for Cabinet Grotesk's internal leading —
       the line box bottom sits ~12px below the visible cap baseline. Without
       this, the text bottom appears short of the section's bottom edge. */
    transform: translateY(12px);
    font-size: var(--display-1);
    line-height: 0.85;
    width: 100%;
    box-sizing: border-box;
  }

  /* Coloured sections tighten on mobile */
  .beat-3, .beat-4-numbers, .closer { padding: 56px 0; }

  /* All beats centered text on mobile */
  .beat,
  .beat .beat-headline,
  .beat .beat-text,
  .beat .beat-text p,
  .beat .punch {
    text-align: center;
  }

  /* Beat 1 & 2 row collapses: image and headline stack, then body below */
  .beat-row,
  .beat-row-image-left {
    grid-template-columns: 1fr;
    gap: 20px;
    text-align: center;
    margin-bottom: 20px;
  }

  /* Mobile: headline first, image second (override image-left order) */
  .beat-row-image-left .beat-image { order: 2; }
  .beat-row-image-left .beat-headline { order: 1; }

  .beat-row > .beat-headline { text-align: center; align-self: stretch; }

  /* Beat 5 asymmetric collapses: text first, video below */
  .beat-asymmetric {
    grid-template-columns: 1fr;
    gap: 20px;
    text-align: center;
  }
  .beat-asymmetric .beat-text,
  .beat-asymmetric .beat-text .beat-headline {
    text-align: center;
  }

  /* Beat 4 setup: headline → punch → video on mobile */
  .beat-4-setup .beat-grid {
    grid-template-columns: 1fr;
    gap: 20px;
    text-align: center;
  }
  .beat-4-setup .punch { text-align: center; margin-top: 0; }

  .beat-image,
  .beat-video {
    max-width: 70%;
    margin: 0 auto;
  }

  .spark-pyramid-stack {
    width: 60%;
  }

  .tagline-text, .closer-setup { font-size: var(--heading-a); }
  .beat-headline { font-size: var(--heading-a); }
  .transition, .punch { font-size: var(--heading-b); }
  .closer-thesis { font-size: var(--display-2); }
  .number-value { font-size: var(--display-3); }
  .card-title { font-size: var(--sub-heading); }
  .number-label, .number-sub { font-size: var(--sub-label); }

  /* Mobile logos: 2x2 grid */
  .logo-row {
    grid-template-columns: 1fr 1fr;
    gap: 32px 24px;
    max-width: 320px;
    margin-left: auto;
    margin-right: auto;
  }

  .numbers-row {
    grid-template-columns: 1fr;
    gap: 24px;
  }

  .cards-row {
    grid-template-columns: 1fr;
    gap: 24px;
  }

  /* Card title overlay tightens on mobile */
  .card-title { font-size: var(--sub-heading); padding: 12px; }

  .footer-credit, .footer-copyright { font-size: var(--micro); }

  /* Footer stacks on mobile */
  .site-footer .container {
    flex-direction: column;
    gap: 8px;
    text-align: center;
  }
  .footer-credit, .footer-copyright { text-align: center; }

  /* Spark pyramid also adjusts */
  .beat-6 .beat-grid {
    grid-template-columns: 1fr;
    gap: 24px;
    text-align: center;
  }
}

/* ==========================================================================
   Accessibility: skip-link + focus styles
   ========================================================================== */

/* Skip to content — hidden until tab-focused */
.skip-link {
  position: absolute;
  top: 8px;
  left: 8px;
  padding: 10px 16px;
  background: var(--teal-dark);
  color: var(--cream);
  text-decoration: none;
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-size: 14px;
  border-radius: 6px;
  z-index: 1000;
  transform: translateY(-200%);
  transition: transform 120ms ease-out;
}

.skip-link:focus {
  transform: translateY(0);
  outline: 3px solid var(--terracotta-mid);
  outline-offset: 2px;
}

/* Visible focus rings on interactive elements (keyboard users) */
a:focus-visible,
button:focus-visible {
  outline: 3px solid var(--terracotta-mid);
  outline-offset: 3px;
  border-radius: 4px;
}

/* Email button gets a softer ring that respects its pill shape */
.email-button:focus-visible {
  outline: 3px solid var(--terracotta-mid);
  outline-offset: 4px;
  border-radius: 999px;
}

/* Project cards get the ring on the link, not the inner image */
.card:focus-visible {
  outline: 3px solid var(--terracotta-mid);
  outline-offset: 4px;
  border-radius: 8px;
}

