/**
 * Motion layer — scroll-driven reveals and load-time entrances.
 *
 * Plain CSS on purpose, outside the Tailwind pipeline: everything here is
 * @keyframes, animation-timeline and transform, none of which needs a utility
 * class. Two practical consequences — tweaking an animation doesn't require
 * recompiling Tailwind, and tailwind-input.css doesn't grow into the file
 * where fonts, focus rings, filter pills, the contact form and this all live
 * together.
 *
 * See docs/plans/2026-08-02-fase-3.md.
 *
 * ---------------------------------------------------------------------------
 * THE ONE RULE THIS FILE EXISTS TO ENFORCE
 * ---------------------------------------------------------------------------
 * Every declaration that can hide content lives inside BOTH gates:
 *
 *     @media ( prefers-reduced-motion: no-preference )
 *     @supports ( animation-timeline: view() )
 *
 * and `opacity: 0` appears ONLY inside @keyframes — never as a base state on
 * a selector. That is what makes the classic reveal failure impossible here:
 * an element parked at opacity 0 waiting for an observer that never fires,
 * and a visitor looking at a blank page.
 *
 * If the browser lacks scroll-driven animations, if the visitor asked for
 * reduced motion, or if this stylesheet fails to load outright, there is no
 * rule anywhere that hides anything. The page renders complete and visible.
 *
 * Adding a base-state `opacity: 0` or `visibility: hidden` outside the gates
 * breaks that guarantee. tests/e2e/motion.spec.js asserts it doesn't happen.
 * ---------------------------------------------------------------------------
 */

/*
 * Scroll reveal: fade up as the element enters the viewport.
 *
 * Only transform and opacity — the two properties the compositor resolves
 * without recalculating layout or repainting. Animating top/left/width/height
 * is what makes scroll effects stutter on a modest phone.
 */
@keyframes gazano-reveal {
	from {
		opacity: 0;
		transform: translateY( 24px );
	}

	to {
		opacity: 1;
		transform: none;
	}
}

/*
 * The factory parallax used to live here and was removed on 2026-08-02.
 *
 * It is worth knowing why, because the same trap is waiting for the next
 * parallax anyone adds: `scale()` on a photo is not a free effect, it is a
 * permanent crop. 1.14 hid 7% of the band at every edge and the 6% drift hid
 * up to 6% more — about 91px on a 756px band, which was most of the slack
 * that composition had left after the building and the pavement took their
 * share. The result was a cut roofline and no sky, on the establishing shot
 * of the client's factory.
 *
 * Halving it to 3% / 1.08 was tried first and still cost ~49px, and it left
 * two numbers in two files (this scale and object-position in
 * blocks/fabrica/render.php) that had to be moved together or break in
 * silence. A subtle 45px drift does not buy that.
 *
 * If a parallax comes back: budget its scale as crop before choosing the
 * framing, not after.
 */

/*
 * The gold rule that connects steps 01→02→03→04 draws itself left to right.
 * The element already exists in the markup and is already measured — this
 * animates it, it doesn't add anything.
 */
@keyframes gazano-draw {
	from {
		transform: scaleX( 0 );
	}

	to {
		transform: scaleX( 1 );
	}
}

/*
 * Polaroids settle into place with a little extra tilt.
 *
 * This uses the INDEPENDENT transform properties — translate, rotate and
 * scale — rather than the `transform` shorthand, and that is the whole
 * trick. Each polaroid already carries a Tailwind rotation
 * (`-rotate-6`, `rotate-6`, `-rotate-3`, `rotate-3`) that compiles to the
 * `transform` property, and those four angles are measured off the PDF and
 * live in the fidelity table. Animating `transform` here would overwrite
 * them and the photos would land flat.
 *
 * The independent properties compose with `transform` instead of replacing
 * it, so `rotate: -4deg` reads as four degrees ON TOP of whatever Tailwind
 * set, and `rotate: none` at the end returns the element to exactly its
 * designed angle. The animation cannot land anywhere else.
 */
@keyframes gazano-polaroid {
	from {
		opacity: 0;
		translate: 0 18px;
		rotate: -4deg;
		scale: 0.94;
	}

	to {
		opacity: 1;
		translate: none;
		rotate: none;
		scale: none;
	}
}

/*
 * Product grid, after a category filter.
 */
@keyframes gazano-filter-in {
	from {
		opacity: 0;
		transform: translateY( 10px );
	}

	to {
		opacity: 1;
		transform: none;
	}
}

/*
 * Lightbox opening. Deliberately an animation rather than a transition off a
 * base `opacity: 0`: a base state would leave a full-screen, fully
 * interactive, completely invisible overlay on top of the page if the class
 * that reveals it ever failed to land. Driven from a keyframe, the worst case
 * is an overlay that appears without animating.
 */
@keyframes gazano-lightbox-in {
	from {
		opacity: 0;
	}

	to {
		opacity: 1;
	}
}

@keyframes gazano-lightbox-figure-in {
	from {
		transform: scale( 0.97 );
	}

	to {
		transform: none;
	}
}

/*
 * The signature writing itself on.
 *
 * A clip rectangle whose right edge travels across the line. The other three
 * insets are NEGATIVE and that is load-bearing: Florestta's swashes leave the
 * element's box on every side, and a clip that ended at `inset(0)` would trim
 * them for good — silently cropping the one thing on the page that is
 * supposed to look hand-drawn.
 *
 * 120px, a LENGTH and not a percentage, and that is the part that was wrong
 * first: percentage insets resolve against the box, and these lines are ~55px
 * tall, so a generous-looking −40% is 22px. Measured against the same
 * signature under reduced motion, the tail of "honestidad" runs about 46px
 * below its own box — the clip was cutting 24px off the finished state, on
 * the one glyph nobody would think to check. A fixed value cannot be
 * out-scaled by a face whose ink has nothing to do with its metrics.
 *
 * Used twice: the signature, and the hero ornament — which is the same gesture
 * (a stroke of the same script face drawing itself on) at a different size.
 * The ornament's ink was measured too and sits INSIDE its box on all four
 * sides, so the 120px costs it nothing.
 */
@keyframes gazano-write {
	from {
		clip-path: inset( -120px 100% -120px -120px );
	}

	to {
		clip-path: inset( -120px -120px -120px -120px );
	}
}

/*
 * Load-time entrance, hero only.
 *
 * `translate`, not `transform: translateY()`, for the reason spelled out at
 * gazano-polaroid: the independent property composes with whatever `transform`
 * an element already carries instead of replacing it. Nothing in the hero
 * column needs that today, but it is what lets the rise be applied to anything
 * in the hero — the ornament's measured rotation included — without having to
 * check first whether it is safe.
 */
@keyframes gazano-rise {
	from {
		opacity: 0;
		translate: 0 24px;
	}

	to {
		opacity: 1;
		translate: none;
	}
}

@media ( prefers-reduced-motion: no-preference ) {
	@supports ( animation-timeline: view() ) {
		/*
		 * `animation-duration: auto` — with a progress-based timeline the
		 * animation spans its range rather than a wall-clock duration. It is
		 * set explicitly rather than left to the initial value because the
		 * initial value of animation-duration changed with the scroll-driven
		 * spec, and the `animation` shorthand would reset it to 0s. Every
		 * engine inside this @supports block understands `auto`.
		 */
		[data-reveal],
		[data-reveal-stack] > *,
		[data-reveal-row] > * {
			animation-name: gazano-reveal;
			animation-duration: auto;
			animation-timing-function: linear;
			animation-fill-mode: both;
			animation-timeline: view();
			animation-range: entry 10% entry 45%;
		}

		/*
		 * Stagger comes from geometry, not from delays — and which mechanism
		 * applies depends on how the children sit.
		 *
		 * `data-reveal-stack`: children are stacked vertically (the five
		 * marquee words), so each one has its own view() timeline and enters
		 * at its own moment. The stagger is already there; adding offsets on
		 * top would over-stagger it into feeling slow.
		 *
		 * `data-reveal-row`: children share a row (the four process cards,
		 * the product grid, the gallery tiles), so their timelines are
		 * identical and they would all fire at once. These are the ones that
		 * need the range shifted per child.
		 */
		[data-reveal-row] > *:nth-child( 2 ) {
			animation-range: entry 16% entry 51%;
		}

		[data-reveal-row] > *:nth-child( 3 ) {
			animation-range: entry 22% entry 57%;
		}

		[data-reveal-row] > *:nth-child( 4 ) {
			animation-range: entry 28% entry 63%;
		}

		[data-reveal-row] > *:nth-child( 5 ) {
			animation-range: entry 34% entry 69%;
		}

		[data-reveal-row] > *:nth-child( 6 ) {
			animation-range: entry 40% entry 75%;
		}

		/*
		 * Past the sixth the offsets stop growing. The product grid takes
		 * whatever the client loads, and an eleventh card waiting 60% of its
		 * own entry range reads as broken, not as choreographed.
		 */
		[data-reveal-row] > *:nth-child( n + 7 ) {
			animation-range: entry 40% entry 75%;
		}

		/*
		 * `cover`, not `entry`: the drift should run for the whole time the
		 * section is crossing the viewport, not just while it arrives.
		 *
		 * NOTE — why the factory and valores sections use `overflow-clip`
		 * rather than `overflow-hidden`, and why changing them back silently
		 * breaks this file:
		 *
		 * `view()` resolves against the nearest ancestor scroll container that
		 * has scrollable overflow. `overflow: hidden` CREATES a scroll
		 * container (it just can't be scrolled by hand); `overflow: clip` does
		 * not. A section that is overflow-hidden AND has overflowing content
		 * therefore captures every view() timeline created inside it, and
		 * because that section never scrolls, those timelines never progress.
		 *
		 * It fails silently in the worst way. The animation reports
		 * `playState: 'running'`, the reveals simply never fire and the
		 * content stays fully visible, and the parallax sits at a constant 50%
		 * progress — which for a -6%→+6% range resolves to exactly 0, a
		 * perfectly static photo that still gets its scale applied and so
		 * looks implemented. Measured here: 7 elements (the factory caption
		 * and CTA, the five valores words) animated on paper and not at all in
		 * practice.
		 */
		/*
		 * `cover`, not `entry`, and this one is not a preference — with `entry`
		 * this animation could not run at all.
		 *
		 * A view() timeline is measured against ITS OWN SUBJECT, and `entry`
		 * spans exactly the subject's length: the phase from its leading edge
		 * crossing the viewport to its trailing edge doing the same. These
		 * connectors are `h-px`. Their entry phase is therefore ONE PIXEL of
		 * scroll — the draw completed in a single scroll step and there was no
		 * position from which it could be seen part-drawn. Measured at 1px
		 * increments across the trigger point: progress read 1 at every one.
		 *
		 * This predates the segments. The single rule they replaced was `h-px`
		 * too, so the "gold rule draws itself left to right" from Fase 3 has
		 * never actually run on screen, and nothing caught it: the animation is
		 * attached, its timeline is a real ViewTimeline, playState reports
		 * running, and the element ends up at its final state — which for a
		 * line that is supposed to finish full width is indistinguishable from
		 * working.
		 *
		 * `cover` spans the whole time the subject overlaps the viewport, so
		 * for a 1px subject it is about a viewport height of scroll instead of
		 * a pixel. Measured after the change: 0 → 7.9 → 18.2 → 28.4 → 36px.
		 *
		 * RULE OF THUMB for anything added to this file: view() ranges scale
		 * with the animated element's own size, so `entry` on a thin or short
		 * element is a no-op that looks implemented. Reach for `cover` there.
		 */
		[data-reveal-line] {
			animation-name: gazano-draw;
			animation-duration: auto;
			animation-timing-function: linear;
			animation-fill-mode: both;
			animation-timeline: view();
			animation-range: cover 5% cover 40%;
			transform-origin: left center;
		}

		/*
		 * The process connectors, one per gap, drawn in sequence.
		 *
		 * They used to be a single rule across the whole row, which could only
		 * ever draw as one gesture. Now that there is a segment per gap the
		 * animation can actually trace the route the section is describing.
		 *
		 * On a scroll timeline the range IS the speed: there is no duration to
		 * set, so a wider range means the same draw spread over more
		 * scrolling. 35% of `cover` is roughly 315px at a 900px viewport —
		 * slow enough to read as a stroke being drawn rather than a line
		 * switching on.
		 *
		 * The 7% offsets are what makes the sequence legible; below about 5%
		 * the three overlap enough to look simultaneous, which is the thing
		 * having separate segments was for.
		 *
		 * Keyed off the parent card's position rather than the segment's,
		 * because the segments live inside the cards and each is an only
		 * child — nth-child on the segment itself would match all of them.
		 */
		[data-reveal-row] > *:nth-child( 1 ) [data-reveal-line] {
			animation-range: cover 5% cover 40%;
		}

		[data-reveal-row] > *:nth-child( 2 ) [data-reveal-line] {
			animation-range: cover 12% cover 47%;
		}

		[data-reveal-row] > *:nth-child( 3 ) [data-reveal-line] {
			animation-range: cover 19% cover 54%;
		}

		/*
		 * `cover`, not `entry`, for the reason spelled out at the connectors:
		 * a view() range is measured against its own subject, and these lines
		 * are ~55px tall. Their whole entry phase is 55px of scroll, so the
		 * writing would be over before it could be seen — the same no-op that
		 * looked implemented for a year on the gold rules.
		 *
		 * EVERY PERCENTAGE HERE IS IN A DIFFERENT COORDINATE SYSTEM, and the
		 * version this replaces was written as if they shared one. Each span
		 * carries its own view() timeline, measured against its own subject:
		 * line 2 sits 55px lower on the page, so its `cover 0%` happens 55px
		 * of scroll AFTER line 1's. The old ranges (38→52%, 48→62%) look like
		 * they overlap by 4% and do not: once the 55px offset is paid, line 2
		 * started 28px of scroll AFTER line 1 finished. Measured, not derived.
		 * The pen stopped dead between the lines — the exact failure the old
		 * comment claimed the overlap was there to prevent. That stop-and-
		 * restart is most of what read as mechanical; it is a carriage
		 * returning, not a hand moving.
		 *
		 * THE BOX IS NOT THE INK, and that is where most of the stalling came
		 * from. These spans are `block` inside a `text-align: center`
		 * paragraph, so each box is the full 320px column while the writing
		 * is 171px ("con respeto") and 200px ("y honestidad"), centred in it.
		 * The clip sweeps the BOX. Measured on line 1, the pen crossed 75px
		 * of empty column before reaching the first letter and 194px of empty
		 * column plus overtravel after the last one: the ink occupied 39% of
		 * the window, sandwiched between 17% of nothing and 44% of nothing.
		 *
		 * That is the robot. Wait, write fast, wait — and widening the range
		 * alone only makes both waits longer, which is why this could not be
		 * fixed by slowing it down.
		 *
		 * So the box is shrunk onto the ink: `fit-content` for the width,
		 * `margin-inline: auto` to keep it centred (without it the signature
		 * jumps 75px left, because centring the TEXT in a wide box and
		 * centring a NARROW box are the same result only by accident), and
		 * 48px of inline padding because the script's swashes leave the text
		 * box — measured at 39px left / 30px right on line 1, 24px / 16px on
		 * line 2. Without that padding the box would start flush with the
		 * ink and a sliver of the first swash would sit on screen before the
		 * animation ever ran. Verified: the glyphs do not move by a pixel.
		 *
		 * Ink now occupies ~60% of each window instead of 39%.
		 *
		 * The ranges are then set in a COMMON frame (px of scroll, line 1's
		 * cover start as zero, cover = 955px at a 900px viewport) against
		 * where the ink actually is inside each sweep, not where the window
		 * edges are:
		 *
		 *   line 1  24→48%  (24%)         writes 188 → 312px
		 *   line 2  29→55%  (26%), +55px  writes 292 → 416px
		 *
		 * Line 2's start percentage is LOWER than the gap between them
		 * suggests because it is paying the 55px offset first. Read the
		 * right-hand column, not the percentages.
		 *
		 * The overlap is roughly `0.089 × cover − 55`, and it is the reason
		 * the two ranges are only 5 points apart. Measured end to end: 20px
		 * of overlap at a 900px viewport, 36px at 1080px, and just touching
		 * at 700px — no dead scroll at any of them, which is the property
		 * that matters. Sizing this for 1440×900 alone would put the stop
		 * back on any short laptop, because the 55px offset is fixed while
		 * `cover` shrinks with the viewport.
		 *
		 * Windows are 24% and 26% rather than equal: the two sweeps are
		 * 387px and 416px of travel, so equal windows would run the pen 7%
		 * faster over the longer line. A hand takes longer over more letters.
		 *
		 * And it is slower. Each line's visible stroke used to take 44px of
		 * scroll — well under half a wheel notch, over before it registered
		 * as writing at all. Now 124px each, and the two lines finally take
		 * the same time as each other rather than the longer one being
		 * written faster.
		 *
		 * LINEAR, where the ornament that shares this keyframe keeps its
		 * ease-in-out, and the change is a consequence of shrinking the box.
		 * `cubic-bezier( 0.4, 0, 0.7, 1 )` spends its slow ends and its fast
		 * middle somewhere, and while the box was mostly empty column those
		 * slow ends fell on the padding where nothing was happening. Against
		 * a box that is now mostly ink they land on the first and last
		 * letters instead: the pen hesitated, dashed through the middle of
		 * the word, then crawled. It also cost ~40px of the intended stroke
		 * length, because the ink sits in the steep part of the curve. A hand
		 * crossing letters moves at roughly one speed.
		 *
		 * The n+3 rule is defensive, not dead: the signature is a client-
		 * editable field split on newlines, so a third line is one return
		 * away. Same cadence, against its own +110px offset.
		 */
		/*
		 * THE WINDOWS ARE WIDER THAN THE MEASUREMENT ASKED FOR, on purpose.
		 *
		 * Fixing the box was what made the writing legible; 24% of cover
		 * (~124px of stroke per line) was where it stopped reading as a switch
		 * being flicked. These are 40% (~207px), chosen by eye against the
		 * measured one running beside it.
		 *
		 * What that costs is the sequence: the second line starts 5 points
		 * after the first and now runs 40 points, so the two are writing
		 * together for almost all of it instead of handing over. A hand does
		 * not write two lines at once — this reads less like writing and more
		 * like the whole signature appearing slowly. It is a deliberate trade,
		 * not an oversight, and moving the offset out to ~35 points is what
		 * buys the hand back if it ever needs to be.
		 *
		 * Line 2 keeps its extra 2 points for the reason it always had: its ink
		 * is 416px against line 1's 387px, so equal windows would run the pen
		 * faster over the longer line.
		 */
		[data-reveal-write] > span {
			animation-name: gazano-write;
			animation-duration: auto;
			animation-timing-function: linear;
			animation-fill-mode: both;
			animation-timeline: view();
			animation-range: cover 24% cover 64%;
			width: fit-content;
			margin-inline: auto;
			padding-inline: 48px;
		}

		[data-reveal-write] > span:nth-child( 2 ) {
			animation-range: cover 29% cover 71%;
		}

		[data-reveal-write] > span:nth-child( n + 3 ) {
			animation-range: cover 35% cover 77%;
		}

		[data-reveal-polaroid] {
			animation-name: gazano-polaroid;
			animation-duration: auto;
			animation-timing-function: linear;
			animation-fill-mode: both;
			animation-timeline: view();
			animation-range: entry 15% entry 55%;
		}
	}

	/*
	 * Hero, on load. No @supports gate: this is an ordinary time-based
	 * animation, and no scroll timeline is involved.
	 *
	 * THE FADE WAS ONCE FORBIDDEN HERE, AND THE REASON TURNED OUT NOT TO
	 * HOLD. The rule used to be transform-only, 12px of travel and no
	 * opacity, on the grounds that the headline is a candidate for the LCP
	 * element and an element at opacity 0 does not count as painted — so a
	 * fade would add its full duration straight onto the metric.
	 *
	 * Sound reasoning, false premise. Measured, the LCP is the hero PHOTO:
	 * 999,045px² against 21,574 for the largest run of text, painted at
	 * 176ms on desktop and 128ms on mobile. The photo is an `<img>` outside
	 * [data-reveal-load] that nothing here animates, so the text can fade in
	 * for free. What the old rule bought was not a faster LCP, it was 12px
	 * of unaccompanied movement — which the inspection recorded, accurately,
	 * as the hero barely animating at all.
	 *
	 * The premise is now pinned by a test rather than by a comment: a hero
	 * published without a photo would promote the headline back to LCP
	 * element and make the old objection true again, and nothing in this
	 * file would know. See `the hero entrance still costs nothing` in
	 * tests/e2e/motion.spec.js, and docs/plans/2026-08-02-fase-3.md §3.4 for
	 * the original decision.
	 */
	[data-reveal-load] > * {
		animation: gazano-rise 600ms cubic-bezier( 0.16, 1, 0.3, 1 ) both;
	}

	/*
	 * The calligraphic stroke laid over the headline, drawn on last.
	 *
	 * It sat outside the entrance entirely until now, and the exclusion was
	 * well founded: the span carries an inline
	 * `transform: rotate(-4.53deg)` read straight off the PDF's content
	 * stream, and an entrance that animated `transform` would overwrite it
	 * and land the stroke flat. But that only rules out `transform`. Clipping
	 * composes with it, so the stroke can be wiped on left to right along its
	 * own rotated axis — which is both the gesture the shape is asking for
	 * and the one the signature already uses further down the page.
	 *
	 * It is the largest thing in the hero after the photo, so it appearing
	 * instantly was a large share of why the load read as static.
	 *
	 * Deliberately slower than the text and starting after it: the stroke is
	 * laid OVER a headline, so it has to arrive over something already
	 * there. 900ms across ~464px of ink is about the speed of a hand.
	 *
	 * The same easing as the signature, and NOT the expo-out the text uses.
	 * `cubic-bezier( 0.16, 1, 0.3, 1 )` is 87% resolved a third of the way
	 * through — which is what you want for something arriving at a position,
	 * and exactly what you don't want for something being drawn: the stroke
	 * flicked on and then crawled the last few pixels for half a second. A
	 * pen accelerates into the line and decelerates out of it.
	 */
	[data-reveal-load-item] {
		animation: gazano-write 900ms cubic-bezier( 0.4, 0, 0.7, 1 ) 420ms both;
	}

	[data-reveal-load] > *:nth-child( 2 ) {
		animation-delay: 90ms;
	}

	[data-reveal-load] > *:nth-child( 3 ) {
		animation-delay: 180ms;
	}

	[data-reveal-load] > *:nth-child( 4 ) {
		animation-delay: 270ms;
	}

	[data-reveal-load] > *:nth-child( n + 5 ) {
		animation-delay: 360ms;
	}

	/*
	 * Filtering the product grid.
	 *
	 * Only the cards that ARRIVE animate; the ones being filtered out go at
	 * once. That asymmetry is the point — waiting for products you just said
	 * you don't want to see to fade away is dead time, and the grid reflows
	 * once instead of twice. It also sidesteps `display` not being
	 * transitionable without reaching for @starting-style and a second
	 * @supports gate.
	 *
	 * `animation-timeline: auto` puts these cards back on the document
	 * timeline: they are also `[data-reveal-row]` children, so without this
	 * they would still be bound to the scroll timeline that revealed them and
	 * a filter click would produce nothing. Deliberately outside the
	 * @supports block so the filter still animates in browsers with no
	 * scroll-driven support at all.
	 */
	[data-gazano-grid].is-filtering > * {
		animation-name: gazano-filter-in;
		animation-duration: 280ms;
		animation-timing-function: cubic-bezier( 0.16, 1, 0.3, 1 );
		animation-fill-mode: both;
		animation-timeline: auto;
	}

	/* Opening the lightbox. Closing stays instant — see lightbox.js. */
	[data-lightbox-overlay].is-open {
		animation: gazano-lightbox-in 200ms ease both;
	}

	[data-lightbox-overlay].is-open [data-lightbox-figure] {
		animation: gazano-lightbox-figure-in 240ms cubic-bezier( 0.16, 1, 0.3, 1 ) both;
	}
}
