# Gasser homepage intro, hero background, and Astro route-transition notes

Session-derived notes for Gasser Bauunternehmen website work. Use this when tuning homepage intro, hero sketch background, or internal page-transition animation.

## Approved visual baseline

- Main homepage intro is approved/good.
- Hero background is approved/good with exactly these aligned assets:
  - `/images/hero/Bleistiftzeichnung_beton_neu.png`
  - `/images/hero/überlagerung_realer.png` for mouseover reveal.
- Do **not** revive old hero layers such as:
  - `bleistiftzeichnung.png`
  - `bleistiftzeichnung_bw.png`
  - `bleistiftzeichnung_weiss.png`
  - `bleistiftzeichnung_beton.png`
  - `bleistiftzeichnung_ueberlagerung.png`
- The two active hero images must lie pixel-exactly over each other. Keep identical `object-fit`, `object-position`, transform, transform origin, and dimensions. QA with `naturalWidth/naturalHeight`, DOM rects, computed transform, and loaded resource names.

## Homepage intro policy

- Every homepage visit (`/`) should run the large architectural GSAP intro, not only the first visit of a browser session.
- Direct homepage load/reload and internal navigation back to `/` should both trigger the large homepage intro.
- Internal navigation between non-home pages should use only the smaller route-transition animation, not the large homepage intro.
- Do not gate the homepage intro behind `sessionStorage.gasserIntroSeen`; set `has-pending-gasser-intro` whenever `isHome` is true.
- `.site-intro` should be default-hidden and only visible through an explicit pending class.
- Rejected fix: do **not** add `has-gsap-intro` in the inline layout script on first home paint, and do **not** use the earlier full quick fix with active-timeline guard plus timeline `undefined` resets. That attempt (`fix: prevent duplicate homepage intro`, later reverted) made the approved page-transition animation worse.
- Accepted fix for doubled iPhone first-load intro: add a separate JS-only class `has-gasser-js-intro` in the inline layout script, use it only to disable CSS keyframe fallback on `.site-intro`/children and keep the pending intro hidden until GSAP starts. Add only a minimal `if (window.__gasserHomeIntroTimeline?.isActive()) return;` guard in `maybePlayDirectHomepageIntro()`. Do **not** alter route-transition motion, route overlay sizing, or timeline cleanup.
- Astro sequencing pitfall: on `astro:after-swap` for `/`, only mark the document as pending (`has-pending-gasser-intro`, remove seen/completed). Start the GSAP homepage intro from `astro:page-load`, when the swapped DOM and page scripts are ready. Starting it directly in `after-swap` can race with the new home DOM and route overlay timing.
- Respect `prefers-reduced-motion`.
- Header and hero content can be hidden during the large intro and revealed after completion.
- The hero background must not be transformed by the intro; animate only logo/depth/aperture layers.

## Route-transition pitfall and durable fix

Observed failure: the route transition existed and technically ran, but during Astro `ClientRouter` swaps it became invisible. Browser sampling showed the overlay starting with a valid high z-index, then after swap becoming `z-index: auto`; the animation continued but was visually behind the new page or effectively absent.

Durable fix:

1. Create/reuse the route overlay outside the swapped page content, e.g. append to `document.documentElement`.
2. Start the route animation immediately on captured internal link click, before Astro swap hooks.
3. In `runRouteLogoTransition()`, set critical visibility styles inline with GSAP at runtime, not only via injected CSS:
   - `position: fixed`
   - `inset: 0`
   - very high `zIndex` (e.g. `2147483000`)
   - `display: grid`
   - `placeItems: center`
   - `pointerEvents: none`
   - `overflow: hidden`
   - background/depth/perspective
   - mark width/aspect/filter/transform origin
4. Keep CSS for defaults and reduced-motion, but do not depend on CSS alone for the live overlay during Astro swaps.
5. On completion, remove the running class and set `display: none`.

## Route-transition timing preference

The route transition must be visible across Astro swaps, but the user rejected the larger/slow version as too much like a second intro. Current approved direction:

- Latest user correction: for **all internal page switches**, reuse the mobile homepage first-load animation style and make it **40% faster**. Remove the old separate mobile route animation.
- Route transition timing is shared but the visual config is now explicitly split between mobile and desktop.
- Mobile route transition: `markWidth: clamp(13.5rem, 68.4vw, 18rem)`, `maxWidth: calc(100vw - 4rem)`, `maxHeight: calc(100svh - 4rem)`, opacity-only, no paint containment, `overflow: visible`, image centered. This is 80% larger than the prior separated mobile route mark while preserving iPhone viewport safety.
- Desktop route transition: `markWidth: clamp(19.8rem, 82.8vw, 27rem)` but 50% less dense than the earlier shared route (`overlayPeakAlpha: 0.5`, `logoPeakAlpha: 0.31`, halved background/depth alpha). This is 80% larger than the prior separated desktop route mark.
- Timing remains mobile-homepage × 0.6: overlay fade-in `0.252s`, logo fade-in starts `0.072s`, logo/depth fade-in `0.348s`, fade-out starts `0.588s`, fade-out duration `0.372s`.
- It must be a real page crossfade/Überblendung: fade a full-screen overlay in before the Astro swap, keep it visible through the swap, then fade overlay + static centered logo out after `astro:page-load`.
- Do **not** end the route timeline on click before the swap completes. Start fade-in on captured link click; call `finishRouteLogoTransition()` from `astro:page-load` when the new page exists.
- Logo must stay centered and never be clipped/offscreen. Use static transform values: `scale: 1`, `filter: none`, `rotateX/Y: 0`, `transformPerspective: 0`; animate only opacity for overlay/depth/mark.
- Preload `/images/brand/Gasserlogo3d.webp` globally, not only on the homepage, to prevent iPhone/Safari image-load pop on the first internal route transition.
- Avoid switching technology to the native View Transitions API for now: Astro `ClientRouter` + GSAP overlay is more predictable across iOS/Safari, especially with `prefers-reduced-motion` and high z-index requirements.
- QA target: route overlay visible for roughly **0.95–1.05s** in browser sampling, ending with `display: none`; mark peak alpha around `0.62`, overlay peak `1`, no transform/filter.

Current preferred route-transition GSAP shape:

```ts
// Runtime inline size and opacity, because Astro swaps can discard CSS context.
gsap.set(mark, {
  width: 'clamp(2.5rem, 5.75vw, 4.5rem)',
  autoAlpha: 0,
  scale: 0.82,
  rotateX: 3,
  rotateY: -4,
  filter: 'blur(5px)',
});

// One continuous flow: appear/focus and enlargement overlap, no separate hard hold.
.to(depth, { autoAlpha: 0.287, scale: 1.08, duration: 0.26, ease: 'sine.out' }, 0)
.to(mark, { autoAlpha: 0.35, scale: 1.08, rotateX: 0, rotateY: 0, filter: 'blur(0px)', duration: 0.36, ease: 'sine.out' }, 0)
.to(mark, { autoAlpha: 0, scale: 1.84, rotateX: -2, rotateY: 3, filter: 'blur(7px)', duration: 0.42, ease: 'sine.inOut' }, 0.32)
.to(depth, { autoAlpha: 0, scale: 1.16, duration: 0.42, ease: 'sine.inOut' }, 0.28)
.to(overlay, { autoAlpha: 0, duration: 0.18, ease: 'sine.out' }, 0.58)
```

QA target from browser sampling: `.gasser-route-intro` should be `display: grid` for about 600ms, retain `position: fixed` and `z-index: 2147483000`, then end with `display: none`.

## Mobile animation policy

Use the same brand animation system on mobile, but not the desktop values 1:1. Mobile should feel premium and quick, never like a loading barrier.

Current implemented approach:

- Keep both animations available on mobile:
  - Direct homepage `/` visit/reload: mobile homepage intro.
  - All internal page switches, including non-home → `/`: use the same mobile homepage intro style as a route crossfade, but 40% faster.
- Do **not** keep a separate mobile route animation variant. The old tiny logo/fast accent route animation was rejected.
- Use viewport-aware motion config in `src/scripts/page-transition.ts` with `matchMedia('(max-width: 767px)')` plus `prefers-reduced-motion` for direct homepage only; the route transition uses the mobile homepage style for all route switches.
- Homepage intro mobile variant:
  - Roughly **1.2s** active animation / cleanup around **1.5s**.
  - Runtime mark width: `clamp(11rem, 46vw, 15rem)`.
  - CSS fallback/mobile keyframe duration: `1200ms` under `@media (max-width: 767px)` in `BaseLayout.astro`.
  - Opacity-only: no zoom/scale-out/3D/blur.
- Route transition variant:
  - Uses the same mark width/background/opacity-only motion as the mobile homepage intro.
  - 40% faster than homepage intro timings, so browser sampling target is about **0.95–1.05s** visible through the swap.
  - Keep `position: fixed` and `zIndex: 2147483000` inline during the live route overlay.
- Desktop values should not accidentally reintroduce a separate route animation; route transitions intentionally use the mobile homepage style on all breakpoints.
- For `prefers-reduced-motion: reduce`, do **not** hide the brand overlay entirely on iPhone. Avoid 3D/zoom, but keep a short opacity-only logo fade so users with iOS “Reduce Motion” still see a visible brand transition.
- iPhone pitfall: if CSS uses `@media (prefers-reduced-motion: reduce) { .site-intro { display: none !important; } }` or route code returns early on `reducedMotion()`, the user will see no animation at all. Replace with no-transform/no-keyframe fallback and JS fade-out cleanup.
- UX principle: Desktop can be cinematic; mobile should be functional-elegant. Preserve the brand impression without adding perceived wait time.

Mobile QA notes:

- If the browser tool cannot resize the viewport directly, a useful code-path smoke test is to temporarily override `window.matchMedia` in the page context for `max-width: 767px`, then re-import `/src/scripts/page-transition.ts?mobileqa=<timestamp>` after setting `window.__gasserTransitionInitialized = false`.
- Verify mobile non-home → non-home samples show route mark inline width `clamp(1.75rem, 8vw, 2.75rem)` and high z-index.
- Verify mobile non-home → `/` samples show `routeEverGrid: false` and `introEverGrid: true`.

## QA recipe

- Build with `npm run build`.
- Search source and `dist/` for stale assets and rejected zoom values:
  - `bleistiftzeichnung_weiss`, `bleistiftzeichnung_bw`, `/images/hero/bleistiftzeichnung.png`, `bleistiftzeichnung_beton.png`, `bleistiftzeichnung_ueberlagerung`
  - `scale(74)`, `scale(54)`, `scale(32)`, other old extreme zooms.
- Direct `/` load or reload: large intro should run every time and then `.site-intro` should be `display: none`.
- Internal navigation from a non-home page back to `/`: do **not** start the route transition. Only the large homepage intro should run after the home page swap.
- Internal navigation across all main pages (`/leistungen/`, `/referenzen/`, `/ueber-uns/`, `/aktuelles/`, `/kontakt/`, `/projekt-anfrage/`, `/`): route overlay should become `display: grid`, `position: fixed`, and retain high z-index through the swap.
- Sample route overlay during navigation with `getComputedStyle()` to confirm `z-index` does not fall to `auto`.
- Check that the route overlay ends with `display: none` and no console errors.
- Check loaded hero resources only include the approved new background/overlay assets.
