# experience-boost Specification

## Requirements

### Requirement: Lenis smooth scrolling integrated with ScrollTrigger

The frontend MUST provide a `resources/js/smooth-scroll.js` module that initializes Lenis, keeps
ScrollTrigger in sync (`lenis.on('scroll', ScrollTrigger.update)`), drives Lenis from the GSAP ticker
(`gsap.ticker.add`) with `gsap.ticker.lagSmoothing(0)`, and exposes `start`/`stop`/`destroy` functions.
The module MUST NOT initialize Lenis when `prefers-reduced-motion: reduce` matches, MUST stop Lenis
while the mobile drawer is open and restart it when the drawer closes, and MUST re-initialize after
`livewire:navigated`. Anchor links to in-page targets MUST still scroll to the target without breaking
the fixed header.

#### Scenario: Module wires Lenis to ScrollTrigger and the ticker

- GIVEN the built JavaScript bundle
- WHEN `smooth-scroll.js` is inspected
- THEN it imports `lenis`
- AND it registers `lenis.on('scroll', ScrollTrigger.update)`
- AND it drives Lenis from `gsap.ticker`
- AND it calls `gsap.ticker.lagSmoothing(0)`

#### Scenario: Reduced motion keeps native scrolling

- GIVEN the user prefers reduced motion
- WHEN the page initializes
- THEN no Lenis instance is created
- AND native scrolling is used

#### Scenario: Drawer open stops smooth scroll and close restarts it

- GIVEN Lenis is active
- WHEN the mobile drawer opens
- THEN smooth scrolling stops
- WHEN the drawer closes
- THEN smooth scrolling restarts

### Requirement: Configurable hero video background

The system MUST expose the settings `hero_video` (URL or uploaded path) and `hero_video_enabled`
(default `false`). `<x-hero>` MUST render a background `<video autoplay muted loop playsinline
preload="metadata">` with the configured source and the existing image as `poster`/fallback ONLY when
`hero_video_enabled` is true AND a resolvable source exists. When disabled or without a source it MUST
render the current image. Under reduced motion the video MUST be hidden and MUST NOT autoplay, leaving
the poster/image visible. The full-height hero, overlay and LCP image handling MUST be preserved.

#### Scenario: Enabled hero with a source renders the video

- GIVEN `hero_video_enabled` is true and `hero_video` points to a resolvable video
- WHEN the home page renders
- THEN the hero contains a `<video>` element with the source
- AND the video is muted, looped and plays inline

#### Scenario: Disabled hero renders only the image

- GIVEN `hero_video_enabled` is false
- WHEN the home page renders
- THEN the hero contains no `<video>` element

#### Scenario: Reduced motion hides the video

- GIVEN a hero video is configured
- WHEN the hero markup is rendered
- THEN the video element carries a reduced-motion class that hides it
- AND the poster image remains the visible background

### Requirement: Scroll storytelling section

The home page MUST include a "De la cantera a tu proyecto" section with four structured steps
(Extracción, Corte, Selección, Despacho). The section MUST expose `data-journey` and
`data-journey-step` hooks so the motion system can build a pinned/scrubbed GSAP ScrollTrigger
sequence on desktop. On mobile and under reduced motion the section MUST render as a normal stacked
list (no pin). The copy MUST be editable from the admin through settings, with defaults shipped by the
seeder.

#### Scenario: Journey section renders the four steps

- GIVEN the home page renders
- WHEN the journey section is inspected
- THEN it contains the title "De la cantera a tu proyecto"
- AND it contains the four step labels in order
- AND each step carries a `data-journey-step` hook

#### Scenario: Journey copy is editable from the admin

- GIVEN an administrator sets a custom journey title
- WHEN the home page renders
- THEN the custom title is shown instead of the default

### Requirement: Before/after comparison component

A reusable `<x-before-after>` Blade component MUST render two images with a draggable divider. It MUST
support pointer and touch dragging and keyboard interaction (arrow keys) through an element with
`role="slider"`, `aria-valuemin`, `aria-valuemax` and `aria-valuenow`, plus accessible labels for the
images. The component MUST NOT render when either image is missing. Project detail pages MUST use it
when a `before`/`after` media pair exists.

#### Scenario: Component renders both images with an accessible slider

- GIVEN a before image and an after image
- WHEN `<x-before-after>` is rendered
- THEN both images are present
- AND an element with `role="slider"` and the ARIA value attributes is present
- AND the keyboard handler is wired

#### Scenario: Missing pair renders nothing

- GIVEN only a before image (or only an after image)
- WHEN `<x-before-after>` is rendered
- THEN no comparison markup is produced

### Requirement: Testimonials

The system MUST provide a `Testimonial` model with `name`, `company`, `role`, `quote`, `logo`,
`rating` (1-5, nullable), `sort_order` and `is_published`, backed by a migration, factory, repository,
service and DTO. A thin Filament resource MUST manage them with Spanish labels. A home section
`<x-testimonials>` MUST render ONLY published testimonials ordered by `sort_order` and emit
`AggregateRating` and `Review` JSON-LD when ratings exist. A seeder MUST provide realistic Peruvian
testimonials.

#### Scenario: Only published testimonials are rendered

- GIVEN two published and one unpublished testimonial
- WHEN the home page renders
- THEN the two published quotes are present
- AND the unpublished quote is absent

#### Scenario: Ratings emit JSON-LD

- GIVEN published testimonials with ratings
- WHEN the home page renders
- THEN an `AggregateRating` schema is present
- AND at least one `Review` schema is present

#### Scenario: Admins manage testimonials

- GIVEN an authenticated administrator
- WHEN they open the testimonials resource
- THEN the Spanish-labelled listing is available

### Requirement: Blur-up image component

A reusable `<x-image>` component MUST render lazy-loaded images with explicit `width`/`height`
attributes when provided, an optional `srcset`/`sizes`, a small blurred placeholder that fades into
the real image, and a graceful fallback that keeps the real image visible when JavaScript is
unavailable. It MUST be used by product cards, galleries, project media and blog covers.

#### Scenario: Component emits lazy loading, dimensions and srcset

- GIVEN a source, dimensions and a srcset
- WHEN `<x-image>` is rendered
- THEN the image has `loading="lazy"`
- AND the `width` and `height` attributes
- AND the `srcset` attribute
- AND a decorative placeholder element

#### Scenario: Missing source renders nothing

- GIVEN no source
- WHEN `<x-image>` is rendered
- THEN no image markup is produced

### Requirement: Sticky mobile CTA bar

The public layout MUST include a bottom sticky bar, hidden on desktop, with a "Cotizar" button and a
WhatsApp button. It MUST appear after the user scrolls past the hero and carry `data-track`
attributes. It MUST NOT overlap the floating WhatsApp button or the mobile drawer.

#### Scenario: Bar is present in the layout with tracked CTAs

- GIVEN a public page renders
- WHEN the layout is inspected
- THEN a mobile-only bar is present
- AND it contains a Cotizar link with `data-track`
- AND it contains a WhatsApp link with `data-track`

#### Scenario: Bar is hidden on desktop

- GIVEN the bar markup
- WHEN it is inspected
- THEN it carries a desktop-hiding utility class
