/* Hero — interactive color-effect headline, ported from
   color.joonasvirtanen.com. The canvas sits behind the line and draws
   cursor-following color blobs; each character animates lift / scale /
   blur / color in response to cursor proximity (or auto patterns). */

/* Extra typefaces (Selecta, Kalice) hotlinked from color.joonasvirtanen.com.
   Edict Display is declared globally in styles.css. */
@font-face {
  font-family: 'Selecta';
  src: url('https://color.joonasvirtanen.com/fonts/Selecta-Light.otf') format('opentype');
  font-weight: 300; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Selecta';
  src: url('https://color.joonasvirtanen.com/fonts/Selecta-LightItalic.otf') format('opentype');
  font-weight: 300; font-style: italic; font-display: swap;
}
@font-face {
  font-family: 'Selecta';
  src: url('https://color.joonasvirtanen.com/fonts/Selecta-Regular.otf') format('opentype');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Selecta';
  src: url('https://color.joonasvirtanen.com/fonts/Selecta-Bold.otf') format('opentype');
  font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Kalice';
  src: url('https://color.joonasvirtanen.com/fonts/Kalice-Trial-Regular.otf') format('opentype');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Kalice';
  src: url('https://color.joonasvirtanen.com/fonts/Kalice-Trial-Italic.otf') format('opentype');
  font-weight: 400; font-style: italic; font-display: swap;
}
@font-face {
  font-family: 'Kalice';
  src: url('https://color.joonasvirtanen.com/fonts/Kalice-Trial-Bold.otf') format('opentype');
  font-weight: 700; font-style: normal; font-display: swap;
}

/* Hero stage — generous breathing room above + below the unified block */
.hero-stage {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  /* Vh-anchored top padding so the headline floats at roughly the same
     proportion of viewport on every screen size. min-height: 100vh so
     the hero claims the full first screen — keeps the cards stack
     anchored near the BOTTOM of the viewport regardless of monitor size. */
  padding: clamp(60px, 16vh, 220px) 24px 0;
  min-height: 100vh;
  overflow: visible;
}

/* Paragraph separation inside the engine — vh-based so the gap between
   the intro paragraph and the cards stack scales with viewport height.
   Each paragraph fades in as ONE BLOCK (not letter-by-letter) — a single
   opacity + blur + lift transition gives the headline a smoother arrival
   than the previous per-char waterfall. */
.hero-para {
  margin-bottom: clamp(50px, 14vh, 200px);
  /* Avoid a lone word wrapping onto the final line. text-wrap:
     pretty is the modern orphan-aware line breaker; browsers that
     don't support it (older Firefox) fall back to normal wrapping
     with no harm. */
  text-wrap: pretty;
  opacity: 0;
  filter: blur(10px);
  transform: translateY(8px);
  transition:
    opacity 700ms cubic-bezier(.22, 1, 0.36, 1),
    filter 700ms cubic-bezier(.22, 1, 0.36, 1),
    transform 700ms cubic-bezier(.22, 1, 0.36, 1);
}
.hero-para.is-revealed {
  opacity: 1;
  filter: blur(0);
  transform: translateY(0);
}

/* Live-data paragraph (always the 2nd paragraph). Reserves space for
   the eventual live sentence (which is ~5–8 wrapped lines depending
   on viewport) so the short "Loading…" placeholder doesn't cause a
   massive reflow when live data arrives. Without this min-height the
   page would shift downward by hundreds of px when the placeholder
   swaps for the real sentence, snapping the experiments-fan section
   into a different viewport position mid-unfurl.
   margin-bottom overrides the inherited .hero-para value by −20% so
   the combined live-para-margin + experiments-margin-top reduces by
   ~20% in total. */
.hero-para--live {
  min-height: clamp(240px, 30vh, 480px);
  margin-bottom: calc(clamp(50px, 14vh, 200px) * 0.8);
}
.hero-para:last-child { margin-bottom: 0; }

/* Intro paragraph (the first, non-live one): narrow it to 656px on
   desktop (820px → −20%) so the bio text wraps with more words on the
   last line. text-wrap: balance equalizes line lengths — ensures "NYC."
   shares its line with at least some preceding words instead of sitting
   alone. Centred within the wider hero-line. (The live paragraph stays
   full width and keeps text-wrap: pretty.) */
@media (min-width: 801px) {
  .hero-para:not(.hero-para--live) {
    max-width: 689px;
    margin-left: auto;
    margin-right: auto;
    text-wrap: balance;
  }
}

/* Italic accent inside the intro (Moi!, Joonas Virtanen, &, New York) */
.hero-italic { font-style: italic; }

/* Hoverable intro words — show cursor pointer, get cursor color effect */
.hero-intro-link {
  cursor: pointer;
  font-style: italic;
}
/* The licensed Edict Display Light renders `!` and `&` at the correct
   weight, so the previous Thin-weight workaround is unnecessary. Keeping
   the class as a no-op so the markup doesn't need to change. */
.hero-thin { font-weight: inherit; }

/* (intro mobile font-size now inherits from .hero-c clamp rule) */
.hero-wrap {
  position: relative;
  cursor: default;
  overflow: visible;
  padding: 0 2rem;
  text-align: center;
  max-width: 960px;
  margin-top: 50px;
}
.hero-wrap canvas {
  position: absolute;
  pointer-events: none;
  z-index: 1;
}
.hero-line {
  position: relative;
  z-index: 3;
  white-space: normal;
  user-select: none;
  font-family: 'Edict Display', Georgia, serif;
  line-height: 1.18;
  letter-spacing: -0.01em;
}
/* Each word is an atomic unit for line-breaking; letters animate inside */
.hero-word {
  display: inline-block;
  white-space: nowrap;
}
.hero-c {
  display: inline-block;
  font-family: inherit;
  /* −25% from clamp(32px, 4.4vw, 64px). Applies to the intro +
     live-data paragraphs (both built from .hero-c chars). */
  font-size: clamp(24px, 3.3vw, 48px);
  font-weight: 300;
  letter-spacing: 0.002em;
  color: #1a1a2e;
  line-height: 1.15;
  /* No per-char reveal — the parent .hero-para handles the block fade.
     Chars carry their cursor-effect transforms during runtime, so we
     leave the transform/opacity/filter slots free for the engine to
     drive. */
}
.hero-c.s { width: 0.28em; }

/* Once the entrance animation has completed, lock the hero stage so a
   subsequent re-render (the live-data fetch rebuilds the DOM) skips the
   block fade entirely and the new paragraph is painted instantly. */
.hero-stage.fully-revealed .hero-para {
  opacity: 1;
  filter: blur(0);
  transform: none;
  transition: none;
}

/* Inline links inside the headline — italic, underlined.
   Underline fades to transparent on hover (reveals the cursor effect). */
.hero-link {
  display: inline;
  color: inherit;
  font-style: italic;
  text-decoration-line: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.16em;
  text-decoration-color: currentColor;
  cursor: pointer;
  transition: text-decoration-color 280ms ease;
}
.hero-link:hover {
  text-decoration-color: transparent;
}
.hero-link .hero-c {
  color: inherit;
  font-style: italic;
}

/* Hover preview popup — iframe of the linked project's embed view.
   Position is set in JS to follow the cursor; we only fade in / out here. */
.hero-preview {
  position: fixed;
  z-index: 200;
  /* Portrait 3:4 frame — taller than wide so the subdomain widgets
     (painting, timeforms, subway, surf) actually fit inside the popup
     instead of being squashed into a square. */
  width: 360px;
  height: 480px;
  background: #f4f4f4;
  border-radius: 14px;
  box-shadow:
    0 1px 2px rgba(15, 17, 22, 0.06),
    0 8px 24px rgba(15, 17, 22, 0.12),
    0 32px 80px rgba(15, 17, 22, 0.18);
  overflow: hidden;
  pointer-events: none;
  opacity: 0;
  transform: scale(0.96);
  transform-origin: top left;
  /* Slower, smoother fade in/out — felt too snappy at 160ms. */
  transition:
    opacity 380ms cubic-bezier(0.22, 1, 0.36, 1),
    transform 460ms cubic-bezier(0.22, 1, 0.36, 1);
}
.hero-preview.show {
  opacity: 1;
  transform: scale(1);
}
.hero-preview iframe,
.hero-preview-img {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
  object-fit: cover;
  object-position: center;
}
/* Show iframe or image depending on what the popup is currently rendering */
.hero-preview .hero-preview-img { display: none; }
.hero-preview iframe { display: block; }
.hero-preview.kind-image .hero-preview-img { display: block; }
.hero-preview.kind-image iframe { display: none; }

/* Two image layers stacked so the cycle can crossfade between photos.
   Both layers occupy the same space; opacity controls which is the
   visible "front" and which is the fading "trace" behind. */
.hero-preview.kind-image .hero-preview-img--a,
.hero-preview.kind-image .hero-preview-img--b {
  position: absolute;
  inset: 0;
  opacity: 0;
  /* 420 ms crossfade — slow enough to register as a "trace" of the
     previous photo lingering, fast enough to keep the cycle lively. */
  transition: opacity 420ms ease;
  pointer-events: none;
}
.hero-preview.kind-image .hero-preview-img--a.is-active,
.hero-preview.kind-image .hero-preview-img--b.is-active {
  opacity: 1;
}

/* Some embeds (timeforms, subway) have content that sits very tight to
   their own edges — when scaled into the small 360×480 preview the
   composition crops uncomfortably. For those, inset the iframe inside
   the preview frame so the popup's #f4f4f4 background acts as a soft
   safe-area mat around the embed.
   The iframe also gets a smaller corner radius and is bumped to a
   neutral white inside the mat so the embed reads as a framed picture
   rather than a flush-bleed crop. */
.hero-preview.preview-padded iframe {
  width: calc(100% - 32px);
  height: calc(100% - 32px);
  margin: 16px;
  border-radius: 8px;
  background: #ffffff;
}

/* ── Hover image trail ─────────────────────────────────────────
   Cursor-following layer that spawns small rounded photo cards as
   the cursor moves over the "Joonas Virtanen" link. Older cards
   linger and fade + blur out behind newer ones, so the trail reads
   as a wake of portrait photos dropping behind the cursor. */
.hero-trail {
  position: fixed;
  inset: 0;
  z-index: 180;
  pointer-events: none;
  /* No backdrop — just the cards. */
}
.hero-trail-card {
  position: absolute;
  /* `left`/`top` set inline to cursor + TRAIL_OFFSET (down-right) so
     the card's top-left corner sits just below+right of the cursor and
     the name copy stays visible. Rotation jitter (--rot) is applied
     via transform, around the card's natural centre. */
  transform: rotate(var(--rot, 0deg)) scale(0.7);
  width: var(--w, 180px);
  height: auto;
  border-radius: 14px;
  overflow: hidden;
  background: #fafafa;
  box-shadow:
    0 10px 28px rgba(15, 17, 22, 0.20),
    0 2px 6px  rgba(15, 17, 22, 0.10);
  opacity: 0;
  /* Pop-in: opacity 0 → 1, scale 0.7 → 1, with a slight overshoot. */
  transition:
    opacity 220ms ease,
    transform 320ms cubic-bezier(0.18, 0.89, 0.32, 1.28),
    filter 420ms ease;
  will-change: transform, opacity, filter;
}
.hero-trail-card.show {
  opacity: 1;
  transform: rotate(var(--rot, 0deg)) scale(1);
}
.hero-trail-card.fade {
  opacity: 0;
  filter: blur(12px);
  transform: rotate(var(--rot, 0deg)) scale(0.92);
  /* Override base transition so the EXIT plays as one smooth fast curve.
     Tail cards demote as soon as the next card spawns, so this needs to
     read as a quick, clean disappearance — long enough to be smooth, not
     so long that tail cards pile up while the user moves the cursor. */
  transition:
    opacity   380ms cubic-bezier(0.4, 0, 0.2, 1),
    filter    380ms cubic-bezier(0.4, 0, 0.2, 1),
    transform 380ms cubic-bezier(0.4, 0, 0.2, 1);
}
.hero-trail-card img {
  display: block;
  width: 100%;
  height: auto;
  user-select: none;
  -webkit-user-drag: none;
}

/* Mobile single-image cycler — instead of the desktop trail (a stream
   of cards following the cursor), a tap on the name shows ONE fixed-
   size card whose image swaps every second. Two stacked <img> layers
   crossfade so the container never resizes or jumps; position is
   clamped in JS so it never spills off the screen edge. */
.hero-trail-card--mobile {
  width: 240px;
  height: 300px;
}
.hero-trail-card--mobile img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 440ms ease;
}
.hero-trail-card--mobile img.is-active {
  opacity: 1;
}

/* Text hint pill — liquid-glass: highly transparent, heavy blur, subtle
   top highlight + soft sheen gradient for a refractive feel. */
.hero-hint {
  position: fixed;
  z-index: 200;
  background:
    linear-gradient(135deg,
      rgba(255, 255, 255, 0.30) 0%,
      rgba(255, 255, 255, 0.08) 100%);
  color: #0a0a0a;
  border-radius: 999px;
  padding: 14px 24px;
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 18px;
  font-weight: 500;
  letter-spacing: -0.005em;
  white-space: nowrap;
  pointer-events: none;
  backdrop-filter: blur(40px) saturate(1.9) brightness(1.04);
  -webkit-backdrop-filter: blur(40px) saturate(1.9) brightness(1.04);
  border: 1px solid rgba(255, 255, 255, 0.45);
  box-shadow:
    /* top edge highlight (specular sheen) */
    inset 0 1px 0 rgba(255, 255, 255, 0.7),
    /* bottom soft inner edge */
    inset 0 -1px 0 rgba(255, 255, 255, 0.08),
    /* thin contact shadow */
    0 1px 2px rgba(15, 17, 22, 0.04),
    /* main floating shadow */
    0 14px 36px rgba(15, 17, 22, 0.10),
    /* ambient lift */
    0 32px 72px rgba(15, 17, 22, 0.10);
  opacity: 0;
  filter: blur(10px);
  transform: scale(0.96);
  transform-origin: top left;
  /* Longer, more cushioned fade so the pill drifts in / out instead of
     popping. */
  transition:
    opacity 460ms cubic-bezier(0.22, 1, 0.36, 1),
    filter 540ms cubic-bezier(0.22, 1, 0.36, 1),
    transform 500ms cubic-bezier(0.22, 1, 0.36, 1);
}
.hero-hint.show {
  opacity: 1;
  filter: blur(0);
  transform: scale(1);
}

/* Preview popup stays at its desktop 360×480 footprint on every screen
   size. On mobile we don't fire hover popups anyway, so there's no
   shrink-rule for narrow viewports. */

/* ── Effects panel (collapsible left drawer) ─────────────────────── */
.fx-panel {
  position: fixed;
  left: 0; top: 0; bottom: 0;
  width: 280px;
  background: #f5f5f5;
  border-right: 1px solid #ddd;
  overflow-y: auto;
  padding: 16px 14px;
  z-index: 100;
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 11px;
  color: #333;
  scrollbar-width: thin;
  transform: translateX(-100%);
  transition: transform 240ms cubic-bezier(0.22, 1, 0.36, 1);
}
.fx-panel.open { transform: translateX(0); }
.fx-panel h2 {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: #999;
  margin: 16px 0 8px;
}
.fx-panel h2:first-child { margin-top: 0; }

.fx-ctrl {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 6px;
  gap: 6px;
}
.fx-ctrl label {
  flex: 0 0 auto;
  max-width: 100px;
  font-size: 11px;
}
.fx-ctrl input[type="range"] {
  flex: 1;
  margin: 0 4px;
  height: 3px;
  -webkit-appearance: none;
  appearance: none;
  background: #ccc;
  border-radius: 2px;
  outline: none;
}
.fx-ctrl input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 12px; height: 12px;
  border-radius: 50%;
  background: #333;
  cursor: pointer;
}
.fx-ctrl .val {
  flex: 0 0 36px;
  text-align: right;
  font-variant-numeric: tabular-nums;
  color: #666;
  font-size: 10px;
}
.fx-btn {
  width: 100%;
  padding: 7px 10px;
  margin-bottom: 5px;
  border: 1px solid #ccc;
  border-radius: 6px;
  background: #fff;
  font-family: inherit;
  font-size: 11px;
  color: #333;
  cursor: pointer;
  text-align: left;
  transition: background 0.15s;
}
.fx-btn:hover { background: #eee; }
.fx-btn.active { background: #111; color: #fff; border-color: #111; }
.fx-cycle {
  width: 100%;
  padding: 8px 10px;
  margin-bottom: 5px;
  border: 1px solid #111;
  border-radius: 6px;
  background: #111;
  color: #fff;
  font-family: inherit;
  font-size: 11px;
  cursor: pointer;
  text-align: center;
}
.fx-cycle:hover { background: #333; }

/* The "FX" toggle button in the topbar */
.fx-toggle {
  font-family: 'Inter', sans-serif;
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
  background: rgba(15,17,22,0.05);
  border: 0;
  border-radius: 999px;
  padding: 6px 12px;
  cursor: pointer;
  letter-spacing: -0.005em;
  transition: background 160ms ease, color 160ms ease;
}
.fx-toggle:hover { color: var(--ink); }
.fx-toggle.open {
  background: #111;
  color: #fff;
}

@media (max-width: 800px) {
  /* Generous top padding on mobile so the headline dominates the
     fold; combined with the bigger margin below .hero-para (next
     rule), the first work card barely peeks at the bottom of the
     initial viewport, inviting a scroll without showing the whole
     thing up front. */
  .hero-stage { padding: 48px 12px 8px; }
  /* +20% over the prior clamp(21px, 6.75vw, 36px) — the paragraph copy
     reads a touch small on phones, so bump the per-char size on mobile
     only (desktop .hero-c is unchanged). */
  .hero-c { font-size: clamp(25px, 8.1vw, 43px); }
  .hero-subtitle { font-size: 15px; padding: 0 24px; }
  .fx-panel { width: 240px; }
  /* Push the bottom of the intro paragraph further down so the
     work stack starts well below the fold on initial load. The
     paragraph itself sits high in the viewport; the cards just
     peek up from the bottom edge. (+22% on mobile — this is the gap
     ABOVE the 2nd/live paragraph, half of "padding around" it.) */
  .hero-para:not(.hero-para--live) { margin-bottom: calc(clamp(140px, 28vh, 280px) * 1.22); }
  /* +22% on the gap BELOW the 2nd/live paragraph too (the desktop
     value is calc(clamp(50px, 14vh, 200px) * 0.8); this bumps it on
     mobile only), so the second paragraph gets more breathing room
     above and below. */
  .hero-para--live { margin-bottom: calc(clamp(50px, 14vh, 200px) * 0.8 * 1.22); }

  /* Underline the interactive italic phrases on mobile — the lift/blur
     cursor effect that signals interactivity on desktop is killed on
     touch, so a static underline is the clearest cue that these phrases
     are tappable. Covers the intro links (.hero-intro-link — "Moi!",
     "Joonas Virtanen") and the live-paragraph links (.hero-link —
     "this Rothko", subway status, surf status, time-of-day, the
     temperature easter egg). The standalone "&" accent (.hero-italic) is
     deliberately NOT underlined — it isn't tappable. */
  .hero-intro-link,
  .hero-link {
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 0.16em;
    text-decoration-color: currentColor;
  }
  /* The per-char .hero-c spans and their .hero-word wrappers are
     display:inline-block on desktop so the cursor effect can transform
     each glyph. That atomicity has two side effects we don't want on
     touch (where there's no cursor effect):
       (a) it blocks the link's underline from painting across the
           word/space gaps, leaving a broken, per-letter line; and
       (b) it creates a soft-wrap opportunity before EVERY word, which
           lets a lone "," or "." wrap to the start of the next line.
     Flipping them back to plain inline on mobile fixes both:
       • the link underline now flows continuously across the whole
         phrase ("this Rothko", "Joonas Virtanen"), spaces included;
       • line-breaking reverts to the Unicode algorithm, which never
         breaks before a comma/period — so no line starts with one.
     Word integrity is preserved by .hero-word's own white-space:nowrap
     (kept from the base rule), so words still never split mid-letter. */
  .hero-word,
  .hero-c {
    display: inline;
  }
}
