/* Reset */
*,
*::before,
*::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
  -moz-text-size-adjust: none;
  -webkit-text-size-adjust: none;
  text-size-adjust: none;
  scroll-behavior: smooth;
}

body {
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  min-height: 100svh;
  background-color: var(--color-dark);
  overflow-x: hidden;
  /* Hoist the view-timelines so the hipster (a non-descendant) can drive
     its scroll-driven parallax (--main) and hue shift (--panel-one). */
  timeline-scope: --main, --panel-one;
}

h1,
h2,
h3,
h4,
h5,
h6,
button,
input,
label,
select,
textarea {
  line-height: 1.1;
  font: inherit;
}

/* @property registration for the interactive foreground layer.
   Per mask, two stacked layers: --hipster-image-set (static base) on the
   bottom, --expression-image-set (neutral 1x1 transparent overlay slot) on
   top. Expressions are composited via masks + opacity-faded ::after/::before
   overlays, so the base is never repainted. --expression-image is the typed
   @property url() fallback for the untyped image-set upgrade. */
@property --expression-image {
  syntax: "<image>";
  inherits: true;
  initial-value: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=");
}

/* Variables & assets */
:root {
  /* Colors */
  --color-dark: oklch(0 0 0);
  --color-light: oklch(1 0 0);
  --color-primary: oklch(68.8% 0.26 341.1);

  /* Expression timing FALLBACKS ONLY. Live values live in the JS CONFIG and
     are written onto .hipster inline at startup (applyTimingVars). These keep
     transitions sane if the script fails to load. Tune in the JS. */
  --easing: cubic-bezier(0.175, 0.885, 0.32, 1.275);
  --expression-fade: 220ms;
  /* Blink fade: same easing as smile/wink, shorter duration so it stays snappy. */
  --blink-fade: 180ms;
  /* Grin (figure rest-intent): its own in/out, unlike the shared fade —
     quick cheeky onset, lingering release. Live values from JS CONFIG.grin. */
  --grin-in: 1200ms;
  --grin-out: 1000ms;
  /* uuuh fade: kept independent of --expression-fade so the blend stays
     deterministic while JS times it to uuuh.mp3 (~1.358s). */
  /* uuuh timing (all tunable). Delayed + eased in slower than the wink so it
     ramps up with the sustained body of the audio. JS keeps delay+in+hold+out
     == audio duration. */
  --uhhh-in-delay: 120ms; /* wait before the morph starts */
  --uhhh-in: 450ms; /* slow, gradual fade-in */
  --uhhh-out: 350ms; /* fade-out (matches the audio's decay tail) */
  /* Total uuuh on-screen length as a MULTIPLE of the mp3 duration (1 = audio
     length). This is the duration knob; --uhhh-in/out only shape the fade edges. */
  --uhhh-length-scale: 1.37;
  --uhhh-fade: var(--uhhh-in); /* live per-phase duration, set by JS */
  /* Reactive sadness (no audio). Fires on upward viewport exit or focus loss.
     Same in/hold/out model as uuuh, slightly slower and softer. */
  --sad-in-delay: 60ms;
  --sad-in: 650ms;
  --sad-hold: 900ms;
  --sad-out: 700ms;
  --sad-fade: var(--sad-in); /* live per-phase duration, set by JS */

  /* Frame thickness for panel one. */
  --frame: 1.6vw;

  /* Camera distance, in viewport units ON PURPOSE: the layer depths (--t)
     are in vh, so a px perspective made the depth-to-distance ratio — i.e.
     the whole 3D intensity — grow with window height. In vh the ratio is
     constant: the effect looks proportionally identical at every resolution.
     240vh ≈ the old 2000px at an 833px-tall window. ONE knob: raise to relax
     all depth/tilt foreshortening, lower to exaggerate. The figure's own
     lens (projecting the mask depths) is half of this, as before (1000px). */
  --camera: 240vh;

  /* Scroll-driven parallax as .main enters (see @keyframes hipster-parallax).
     POSITIVE shift = the figure drifts DOWN against the sticky exit, so it
     moves slower than the scroll and the eyes stay in the viewport as long
     as possible (until the incoming main sheet covers them). The bow
     rotation that used to ride along was removed — too much motion during
     the exit, UX-wise. */
  --parallax-shift: 72vh;

  /* Scroll-driven hue shift: the figure's hue rotates from 0 to this value
     as panel-one exits the viewport (a quick flip between 80% and 90%
     exited — see @keyframes hipster-hue). The artwork is monochrome except the beanie,
     so this is effectively the beanie-colour knob. */
  --hue-to: 180deg;

  /* Image sets (local assets) */
  --hipster-image-set: image-set(
    url("assets/hipster-default_set-1.avif") 1x type("image/avif"),
    url("assets/hipster-default_set-1.webp") 1x type("image/webp"),
    url("assets/hipster-default_set-1.png") 1x type("image/png"),
    url("assets/hipster-default_set-2.avif") 2x type("image/avif"),
    url("assets/hipster-default_set-2.webp") 2x type("image/webp"),
    url("assets/hipster-default_set-2.png") 2x type("image/png")
  );

  --glasses-image-set: image-set(
    url("assets/hipster-glasses_set-1.avif") 1x type("image/avif"),
    url("assets/hipster-glasses_set-1.webp") 1x type("image/webp"),
    url("assets/hipster-glasses_set-1.png") 1x type("image/png"),
    url("assets/hipster-glasses_set-2.avif") 2x type("image/avif"),
    url("assets/hipster-glasses_set-2.webp") 2x type("image/webp"),
    url("assets/hipster-glasses_set-2.png") 2x type("image/png")
  );

  --blink-image-set: image-set(
    url("assets/hipster-blink_set-1.avif") 1x type("image/avif"),
    url("assets/hipster-blink_set-1.webp") 1x type("image/webp"),
    url("assets/hipster-blink_set-1.png") 1x type("image/png"),
    url("assets/hipster-blink_set-2.avif") 2x type("image/avif"),
    url("assets/hipster-blink_set-2.webp") 2x type("image/webp"),
    url("assets/hipster-blink_set-2.png") 2x type("image/png")
  );

  --smile-image-set: image-set(
    url("assets/hipster-smile_set-1.avif") 1x type("image/avif"),
    url("assets/hipster-smile_set-1.webp") 1x type("image/webp"),
    url("assets/hipster-smile_set-1.png") 1x type("image/png"),
    url("assets/hipster-smile_set-2.avif") 2x type("image/avif"),
    url("assets/hipster-smile_set-2.webp") 2x type("image/webp"),
    url("assets/hipster-smile_set-2.png") 2x type("image/png")
  );

  --wink-image-set: image-set(
    url("assets/hipster-wink_set-1.avif") 1x type("image/avif"),
    url("assets/hipster-wink_set-1.webp") 1x type("image/webp"),
    url("assets/hipster-wink_set-1.png") 1x type("image/png"),
    url("assets/hipster-wink_set-2.avif") 2x type("image/avif"),
    url("assets/hipster-wink_set-2.webp") 2x type("image/webp"),
    url("assets/hipster-wink_set-2.png") 2x type("image/png")
  );

  --grin-image-set: image-set(
    url("assets/hipster-grin_set-1.avif") 1x type("image/avif"),
    url("assets/hipster-grin_set-1.webp") 1x type("image/webp"),
    url("assets/hipster-grin_set-1.png") 1x type("image/png"),
    url("assets/hipster-grin_set-2.avif") 2x type("image/avif"),
    url("assets/hipster-grin_set-2.webp") 2x type("image/webp"),
    url("assets/hipster-grin_set-2.png") 2x type("image/png")
  );

  --uuuh-image-set: image-set(
    url("assets/hipster-uuuh_set-1.avif") 1x type("image/avif"),
    url("assets/hipster-uuuh_set-1.webp") 1x type("image/webp"),
    url("assets/hipster-uuuh_set-1.png") 1x type("image/png"),
    url("assets/hipster-uuuh_set-2.avif") 2x type("image/avif"),
    url("assets/hipster-uuuh_set-2.webp") 2x type("image/webp"),
    url("assets/hipster-uuuh_set-2.png") 2x type("image/png")
  );

  --sad-image-set: image-set(
    url("assets/hipster-sad_set-1.avif") 1x type("image/avif"),
    url("assets/hipster-sad_set-1.webp") 1x type("image/webp"),
    url("assets/hipster-sad_set-1.png") 1x type("image/png"),
    url("assets/hipster-sad_set-2.avif") 2x type("image/avif"),
    url("assets/hipster-sad_set-2.webp") 2x type("image/webp"),
    url("assets/hipster-sad_set-2.png") 2x type("image/png")
  );

  /* Foreground defaults: a 1x1 transparent PNG (neutral overlay slot). Visible
     expressions come from the mask/overlays, not these. --expression-image is
     the typed @property fallback; --expression-image-set the image-set upgrade. */
  --expression-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=");
  --expression-image-set: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=");

  /* Fallback images */
  --hipster: url("assets/hipster-default_set-1.png");
  --glasses: url("assets/hipster-glasses_set-1.png");
  --blink: url("assets/hipster-blink_set-1.png");
  --smile: url("assets/hipster-smile_set-1.png");
  --wink: url("assets/hipster-wink_set-1.png");
  --grin: url("assets/hipster-grin_set-1.png");
  --uuuh: url("assets/hipster-uuuh_set-1.png");
  --sad: url("assets/hipster-sad_set-1.png");

  /* Layered masks. These SVGs contain feGaussianBlur filters, which Firefox
     re-renders on every composited frame — script.js rasterizes them into PNG
     textures at load and overrides these vars (measured: 28fps -> 60fps). */
  --mask-01: url("assets/hipster-mask-01.svg");
  --mask-02: url("assets/hipster-mask-02.svg");
  --mask-03: url("assets/hipster-mask-03.svg");
  --mask-04: url("assets/hipster-mask-04.svg");
  --mask-05: url("assets/hipster-mask-05.svg");
  --mask-06: url("assets/hipster-mask-06.svg");
  --mask-07: url("assets/hipster-mask-07.svg");
  --mask-08: url("assets/hipster-mask-08.svg");
  --mask-09: url("assets/hipster-mask-09.svg");
}

/* Layout */
.hipster-section {
  position: relative;
  min-height: 200vh;
  background-color: var(--color-dark);
}

/* Pinned overlay: an ABSOLUTE full-section column with a sticky child.
   (Not margin-bottom: -100vh on the sticky element: Firefox clamps sticky
   against the margin box, which made the figure over-stick by ~100vh, linger
   over <main>, and drift against the scroll.) Absolute takes no flow space
   by definition — interop-safe. */
.hipster-overlay {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none; /* panels stay interactive; the figure re-enables itself */
}

.hipster-viewport {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  perspective: var(--camera); /* vh-based — see --camera in :root */
  overflow: hidden;
}

/* Keep the figure clickable wherever visible without covering .main */
.hipster-perspective {
  position: relative;
  z-index: 5;
  pointer-events: auto;
  height: 100%;
  aspect-ratio: 4 / 5;
  transform-style: preserve-3d;
  /* `translate` carries the whole-figure dolly z (JS, per pointermove) with
     its own inertia, while `transform` (tilt) keeps situational durations.
     Keep the translate duration in sync with CONFIG.depth.inertia. */
  transition: all 0.4s var(--easing), translate 0.45s ease-out;
}

/* panel two — pinned behind, revealed as panel one scrolls off. No background,
   so the section colour shows through. */
.panel-two {
  position: sticky;
  top: 0;
  height: 100vh;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}
.panel-two__text {
  color: var(--color-light);
  font-size: 3vw;
  max-width: 60ch;
  text-align: center;
  padding: 0 8vw;
}

/* panel one — the framed cover; scrolls off to reveal panel two. */
.panel-one {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 100vh;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  /* Its exit progress drives the figure's hue shift (see hipster-hue). */
  view-timeline-name: --panel-one;
  view-timeline-axis: block;
}

/* The growing cover/frame — animated on its own layer so the text above it
   stays put while the frame grows in. */
.panel-one::before {
  content: "";
  position: absolute;
  z-index: -1;
  background-color: var(--color-light);
  animation: frame-grow 1.2s var(--easing) both;
}
.panel-one__text {
  color: var(--color-dark);
  font-size: 3vw;
  max-width: 60ch;
  text-align: center;
  padding: 0 8vw;
  animation: panel-one-fade 1.2s var(--easing) both;
}

@keyframes panel-one-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* The frame grows on load: viewport starts full, then the section colour
   shows around the panel. */
@keyframes frame-grow {
  from { inset: 0; }
  to   { inset: var(--frame); }
}

.hipster {
  top: 10vh;
  width: 100%;
  aspect-ratio: 4 / 5;
  position: relative;
  transform-style: preserve-3d;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  perspective: calc(var(--camera) / 2); /* projects the masks' vh depths */
  will-change: transform;
  --depth-multiplier: 1.25; /* Normal expansion */
  cursor: pointer;
}

/* Scroll-driven parallax: the figure drifts down against the sticky exit as
   .main enters, so it leaves slower than the scroll (translate only — the
   bow rotation was removed as UX noise). The .scroll-fx class is added by
   script.js when scroll-driven animations are supported AND the browser is
   not Firefox (Gecko re-renders the masked surfaces per animated frame) —
   Firefox degrades to a stationary figure. */
@keyframes hipster-parallax {
  from { translate: 0 0; }
  to   { translate: 0 var(--parallax-shift); }
}
html.scroll-fx .hipster {
  animation: hipster-parallax linear both;
  animation-timeline: --main;
  animation-range: entry 0% entry 100%;
}

/* Scroll-driven hue shift: hue 0 while panel-one is fully visible, rotating
   to var(--hue-to) by the time panel-one is three quarters out of the
   viewport (exit 75%). Applied PER MASK LAYER, never on .hipster or above:
   filter is a grouping property — on an ancestor it forces transform-style
   to flat and collapses the whole translateZ depth stack. hue-rotate is a
   paint-free colour-matrix, compositor-animated in Chrome; Firefox is gated
   out by .scroll-fx (Gecko would re-raster the masked surfaces per frame). */
@keyframes hipster-hue {
  from { filter: hue-rotate(0deg); }
  to   { filter: hue-rotate(var(--hue-to)); }
}
html.scroll-fx .mask {
  animation: hipster-hue linear both;
  animation-timeline: --panel-one;
  animation-range: exit 80% exit 90%;
}

/* Shadow. --shadow-img is a rasterized PNG texture set by script.js (the SVG
   contains a gaussian blur that Firefox would re-render per frame); the SVG
   url is the fallback until it lands. */
.hipster::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: var(--shadow-img, url("assets/hipster-shadow.svg"));
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  transform: translateZ(calc(-4vh * var(--depth-multiplier)));
  transition: transform 0.6s var(--easing);
  pointer-events: none;
  opacity: 0.78;
  z-index: -1;
}

/* Glitch prevention, forces preload of images before interaction */
.hipster::after {
  content: "";
  position: fixed;
  inset: auto auto 0 0;
  width: 1px;
  height: 1px;
  opacity: 0.001;
  pointer-events: none;
  z-index: -1;
  background-image: var(--blink), var(--smile), var(--wink), var(--grin);
  background-image: var(--blink-image-set), var(--smile-image-set),
    var(--wink-image-set), var(--grin-image-set);
  background-repeat: no-repeat;
  background-size: 1px 1px;
}

/* Depth trigger: the old :hover rules are gone — CSS :hover can't express
   the proximity effect and doesn't even latch reliably under the transforming
   figure. The dolly is driven continuously from JS (see "Proximity dolly" in
   script.js): a whole-figure translate-z on .hipster-perspective. The layer
   stack keeps its resting micro-spacing at all times. */

/* Mask Styles */
.mask {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-position: center;
  mask-position: center;
  pointer-events: none;
  transform-style: preserve-3d;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  overflow: hidden;
  /* GPU acceleration. Only `transform` — the pen also listed
     background-image (never animated now) and 9+ promoted layers blow
     Firefox's will-change memory budget. */
  will-change: transform;
  /* Default idle depth */
  --active-depth: calc(var(--z) * 1px);
  /* Calculation: Multiplier only increases --t depth */
  transform: translateZ(calc(var(--active-depth) * var(--depth-multiplier)));
  transition: transform 0.6s var(--easing);
}

/* Staggered transition delays: each layer's depth changes ripple by its own
   --d while the section is hovered (today this mainly shapes the emergence
   handover; per-layer depth no longer changes on hover). */
.hipster-section:hover .mask {
  transition-delay: var(--d);
}

/* ---- Load emergence: the figure grows out of panel one's surface ----
   On load every mask layer starts flat at the surface (depth 0). Removing
   .is-emerging lets each layer ease its depth in, staggered by its own --d,
   so the figure appears to grow out of the cover one layer at a time.
   The frame-grow + text fade are held (paused) until that has finished. */
.hipster-section.is-emerging .mask,
.hipster-section.has-emerged .mask {
  transition: transform 0.6s var(--easing), opacity 0.6s var(--easing);
  transition-delay: var(--d);
}
.hipster-section.is-emerging .mask {
  --depth-multiplier: 0; /* collapsed onto the surface */
  opacity: 0; /* invisible on the surface, fades in with the depth */
}
/* The cast shadow appears last: hold it transparent, then fade it in 0.8s
   after emergence starts (once the figure has grown out of the surface). */
.hipster-section.is-emerging .hipster::before,
.hipster-section.has-emerged .hipster::before {
  transition: transform 0.6s var(--easing), opacity 0.6s var(--easing) 0.8s;
}
.hipster-section.is-emerging .hipster::before {
  opacity: 0; /* no shadow until the figure has emerged */
}
.hipster-section.is-emerging .panel-one::before,
.hipster-section.has-emerged .panel-one::before,
.hipster-section.is-emerging .panel-one__text,
.hipster-section.has-emerged .panel-one__text {
  animation-play-state: paused; /* delay the visual load until the figure emerges */
}

/* Layer variables and masks */
.mask:nth-child(1) {
  --z: 1;
  --t: 0vh;
  --d: 0.75s;
  -webkit-mask-image: var(--mask-01);
  mask-image: var(--mask-01);
}
.mask:nth-child(2) {
  --z: 2;
  --t: 1.2vh;
  --d: 0.7s;
  -webkit-mask-image: var(--mask-02);
  mask-image: var(--mask-02);
}
.mask:nth-child(3) {
  --z: 3;
  --t: 2.4vh;
  --d: 0.65s;
  -webkit-mask-image: var(--mask-03);
  mask-image: var(--mask-03);
}
.mask:nth-child(4) {
  --z: 4;
  --t: 3.6vh;
  --d: 0.6s;
  -webkit-mask-image: var(--mask-04);
  mask-image: var(--mask-04);
}
.mask:nth-child(5) {
  --z: 5;
  --t: 4.8vh;
  --d: 0.55s;
  -webkit-mask-image: var(--mask-05);
  mask-image: var(--mask-05);
}
.mask:nth-child(6) {
  --z: 6;
  --t: 6vh;
  --d: 0.5s;
  -webkit-mask-image: var(--mask-06);
  mask-image: var(--mask-06);
}
.mask:nth-child(7) {
  --z: 7.2;
  --t: 6vh;
  --d: 0.45s;
  /* Glasses, glasses bitmap above glass SVG. No mask on this layer.
     --glass-img = rasterized glass texture from script.js (SVG fallback). */
  background-image: var(--glasses), var(--glass-img, url("assets/hipster-glass.svg"));
  background-image: var(--glasses-image-set), var(--glass-img, url("assets/hipster-glass.svg"));
  background-repeat: no-repeat, no-repeat;
  background-position: center, center;
  background-size: contain, contain;
}
.mask:nth-child(8) {
  --z: 8;
  --t: 8vh;
  --d: 0.005s;
  -webkit-mask-image: var(--mask-08);
  mask-image: var(--mask-08);
}
.mask:nth-child(9) {
  --z: 9;
  --t: 8.8vh;
  --d: 0s;
  -webkit-mask-image: var(--mask-09);
  mask-image: var(--mask-09);
}

/* Background assignment: two stacked layers per mask. TOP = --expression-image
   (neutral overlay slot), BOTTOM = --hipster (static base, body + head). Each is
   declared twice: url() fallback, then image-set() upgrade for AVIF/WebP. */
/* Base character image (body + head) on layers 1-6. Expression art is painted
   by the ::before/::after overlays, scoped to layers 3-6 below. */
.mask:nth-child(-n + 6) {
  /* Fallback: typed @property url() on top of the raw hipster url */
  background-image: var(--expression-image), var(--hipster);
  /* Upgrade: image-set() on top of the hipster image-set */
  background-image: var(--expression-image-set), var(--hipster-image-set);
}

/* Smile overlay: painted on ::after above the neutral layer and opacity-faded
   (compositor-only, cheap). Base stays neutral so wink/blink keep their snap. */

/* Only expression-carrying masks get the smile overlay. ::after inherits the
   parent mask's clipping, so no need to redeclare mask-image. */
.mask:nth-child(n + 3):nth-child(-n + 6)::after {
  content: "";
  position: absolute;
  inset: 0;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  background-image: var(--smile);
  background-image: var(--smile-image-set);
  opacity: 0;
  transition: opacity var(--expression-fade) var(--easing);
  pointer-events: none;
  will-change: opacity;
}

.hipster.smile-animation .mask:nth-child(n + 3):nth-child(-n + 6)::after {
  opacity: 1;
}
/* Sad overlay on ::after (independent of wink on ::before) so sad<->wink cross-fades
   like smile<->wink. Declared after smile so its image wins while sad classes are on. */
.hipster.sad-animation .mask:nth-child(n + 3):nth-child(-n + 6)::after,
.hipster.sad-fading .mask:nth-child(n + 3):nth-child(-n + 6)::after {
  background-image: var(--sad);
  background-image: var(--sad-image-set);
  transition-duration: var(--sad-fade);
}
.hipster.sad-animation .mask:nth-child(n + 3):nth-child(-n + 6)::after {
  opacity: 1;
}
/* Wink/blink overlay: same fade pattern as smile but on ::before so they fade
   independently. Image set per state class; -fading keeps it rendered while
   opacity eases back to 0. */
.mask:nth-child(n + 3):nth-child(-n + 6)::before {
  content: "";
  position: absolute;
  inset: 0;
  background-repeat: no-repeat;
  background-size: contain;
  background-position: center;
  opacity: 0;
  /* Match the smile's eased fade by default. Blink overrides this below. */
  transition: opacity var(--expression-fade) var(--easing);
  pointer-events: none;
  will-change: opacity;
  z-index: 1;
}

/* Blink: same easing as smile/wink, shorter duration only. */
.hipster.blink-animation .mask:nth-child(n + 3):nth-child(-n + 6)::before,
.hipster.blink-fading .mask:nth-child(n + 3):nth-child(-n + 6)::before {
  transition-duration: var(--blink-fade);
}

/* Wink: fade in. Image stays declared during -fading so opacity can ease back to 0. */
.hipster.wink-animation .mask:nth-child(n + 3):nth-child(-n + 6)::before,
.hipster.wink-fading .mask:nth-child(n + 3):nth-child(-n + 6)::before {
  background-image: var(--wink);
  background-image: var(--wink-image-set);
}
.hipster.wink-animation .mask:nth-child(n + 3):nth-child(-n + 6)::before {
  opacity: 1;
}

/* Grin: the figure's rest-intent expression (the wink stays on the button).
   Same ::before fade pattern as the wink, but with its own asymmetric
   timing: --grin-in while appearing, --grin-out while releasing. */
.hipster.grin-animation .mask:nth-child(n + 3):nth-child(-n + 6)::before,
.hipster.grin-fading .mask:nth-child(n + 3):nth-child(-n + 6)::before {
  background-image: var(--grin);
  background-image: var(--grin-image-set);
}
.hipster.grin-animation .mask:nth-child(n + 3):nth-child(-n + 6)::before {
  opacity: 1;
  transition-duration: var(--grin-in);
}
.hipster.grin-fading .mask:nth-child(n + 3):nth-child(-n + 6)::before {
  transition-duration: var(--grin-out);
}

/* uuuh overlay: like the wink/blink ::before but with its own --uhhh-fade so
   JS can time the in/hold/out blend to the audio. -fading avoids a snap-out. */
.hipster.uhhh-animation .mask:nth-child(n + 3):nth-child(-n + 6)::before,
.hipster.uhhh-fading .mask:nth-child(n + 3):nth-child(-n + 6)::before {
  /* url() fallback first, then the image-set() upgrade (AVIF/WebP) */
  background-image: var(--uuuh);
  background-image: var(--uuuh-image-set);
  transition-duration: var(--uhhh-fade);
}
.hipster.uhhh-animation .mask:nth-child(n + 3):nth-child(-n + 6)::before {
  opacity: 1;
}
/* While uhhh is firing, hide the smile overlay so the expression reads cleanly. */
.hipster.uhhh-animation .mask:nth-child(n + 3):nth-child(-n + 6)::after {
  opacity: 0;
  transition-duration: 0ms;
}


/* Blink: same pattern, faster fade. */
.hipster.blink-animation .mask:nth-child(n + 3):nth-child(-n + 6)::before,
.hipster.blink-fading .mask:nth-child(n + 3):nth-child(-n + 6)::before {
  background-image: var(--blink);
  background-image: var(--blink-image-set);
}
.hipster.blink-animation .mask:nth-child(n + 3):nth-child(-n + 6)::before {
  opacity: 1;
}

/* While wink/grin/blink fires, hide the smile overlay so the expression
   reads cleanly; it fades back in when the class is removed. */
.hipster.wink-animation .mask:nth-child(n + 3):nth-child(-n + 6)::after,
.hipster.grin-animation .mask:nth-child(n + 3):nth-child(-n + 6)::after,
.hipster.blink-animation .mask:nth-child(n + 3):nth-child(-n + 6)::after {
  opacity: 0;
  transition-duration: 0ms;
}

/* Capture touch for finger-tracking on the hipster; empty section area stays
   scrollable (sticky preserved). */
.hipster-perspective,
.hipster {
  touch-action: none;
}

/* Classes for testing */
.button {
  position: absolute;
  top: 1vw;
  left: 2vw;
  background-color: rgba(51, 51, 51, 0.05);
  border-radius: 999px;
  border-width: 0;
  color: #333333;
  cursor: pointer;
  display: inline-block;
  font-family: "Haas Grot Text R Web", "Helvetica Neue", Helvetica, Arial,
    sans-serif;
  font-size: 1em;
  font-weight: 500;
  line-height: 20px;
  list-style: none;
  margin: 0;
  padding: 0.8em 1.35em;
  text-align: center;
  transition: all 200ms;
  vertical-align: baseline;
  white-space: nowrap;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  -webkit-user-select: none;
  touch-action: manipulation;
  z-index: 1000;
}

.button:hover {
  background-color: rgba(51, 51, 51, 0.15);
}

/* Main content, outside the section — drives the parallax as it enters. */
.main {
  position: relative;
  z-index: 10;
  min-height: 200vh;
  background-color: var(--color-light);
  view-timeline-name: --main;
  view-timeline-axis: block;
  display: flex;
  align-items: flex-start;
  justify-content: center;
}
.main__text {
  color: var(--color-dark);
  font-size: 3vw;
  padding: 12vh 8vw;
}
