Grid system
Two grids cover the full range of Latency materials. Digital uses a 12-column structure on an 8px base unit. Print uses a 6-column structure on a 7mm base unit. Full column-width calculations, margin specifications, and responsive behaviour are in development.
Specification in progress
12 columns on an 8px base unit. Gutters scale from 16px (mobile) to 40px (desktop). Maximum content width 1280px. All component widths and spacing in the Albufera system are derived from this grid.
12-column · 40px gutters · 1280px max-width
6 columns on a 7mm base unit. Outer margins 20mm, inner margins 15mm on A4. A 4-column variant covers narrow-format print (DL, A5). All print materials — decks, reports, one-pagers — derive from this structure.
6-column · 7mm base · A4
Five breakpoints. Column count drops at smaller viewports; gutters narrow proportionally. The 1280px breakpoint is the primary design target for desktop marketing materials.
Locomotive Scroll
Locomotive Scroll v5 — built on Lenis. 9.4kB gzipped. Smooth easing, viewport detection, per-element parallax, progress tracking as CSS variables or JS events, custom scroll calls, and anchor navigation. Every attribute is demonstrated live on this page — scroll through each section to see it working.
One script tag, one constructor call. All options have working defaults — the minimal init is sufficient for most use cases. Pass a configuration object to override. This page uses the CDN approach.
CDN
<script src="https://cdn.jsdelivr.net/npm/locomotive-scroll/bundled/locomotive-scroll.min.js"></script>Full configuration — all options shown
const scroller = new LocomotiveScroll({
lenisOptions: {
lerp: 0.1, // smoothing intensity 0–1
duration: 1.2, // animation duration in seconds
orientation: 'vertical', // 'vertical' | 'horizontal'
gestureOrientation:'vertical', // 'vertical' | 'horizontal' | 'both'
smoothWheel: true,
smoothTouch: false, // keep false for native mobile feel
wheelMultiplier: 1,
touchMultiplier: 2,
normalizeWheel: true, // cross-browser delta normalisation
easing: (t) => 1 - Math.pow(1 - t, 3), // cubic-out
},
triggerRootMargin: '-1px -1px -1px -1px', // IntersectionObserver margin
rafRootMargin: '100% 100% 100% 100%', // RAF activation zone
autoStart: true,
scrollCallback: ({ scroll, limit, velocity, direction, progress }) => {
// fires every RAF frame while scrolling
},
});Called on every RAF frame while the page is scrolling. Returns scroll (px), limit (max scroll px), velocity (signed, peaks ~3 on fast flicks), direction (1 down / −1 up), and progress (0–1 page fraction). The readout below is driven by this page's own scrollCallback.
Live scroll state — scroll to update
Velocity magnitude bar (0–3 normalised)
lerp is the primary feel control — lower values feel more fluid, higher values snappier. smoothTouch defaults off to preserve native momentum on iOS and Android. Do not change without testing on real devices.
lenisOptions: {
lerp: 0.1, // ← fluid ···· snappy → 1.0
duration: 1.2, // affects inertia tail
smoothWheel: true,
smoothTouch: false, // keep false on mobile
wheelMultiplier: 1, // scroll speed multiplier
touchMultiplier: 2,
normalizeWheel: true, // unifies Chrome/Firefox delta
}This page runs at lerp: 0.08 — slightly softer than the library default. Adjust per context; landing pages typically want 0.06–0.10, documentation 0.10–0.15.
In-view detection
Five attributes govern element detection. data-scroll registers the element. data-scroll-class names the added class (default is-inview). data-scroll-repeat enables toggle on exit. data-scroll-position sets the trigger point. data-scroll-offset shifts it. data-scroll-ignore-fold disables automatic in-fold adjustment.
Enables Locomotive Scroll viewport detection. When the element enters the viewport the class is-inview is added and never removed — a one-shot trigger. No value required; presence of the attribute is sufficient. The four stat cards below use this to animate in.
Scroll through — cards reveal individually
Coverage
52M+
Financials
12M
Taxonomy
1,200
Integrations
80+
<div data-scroll>
<!-- receives class "is-inview" on entry -->
</div>
/* CSS: base hidden state → revealed state */
[data-scroll] { opacity:0; transform:translateY(24px); transition: ... }
[data-scroll].is-inview { opacity:1; transform:none; }Replaces the default is-inview with a custom class name. Use when the trigger class needs to match an existing CSS state machine or when different entry animations are required on the same page.
Gains is-branded on entry
Class state
Scroll here to trigger is-branded — the border and label colour change via CSS alone.
<div
data-scroll
data-scroll-class="is-branded"
></div>Without this attribute, is-inview is added once and stays. With it, the class toggles: added on entry, removed on exit. The badge below scales up each time you scroll it into view.
Scroll away and back — re-triggers each time
data-scroll-repeat ← no value neededTwo comma-separated values specifying which part of the element (start / middle / end) must cross which part of the viewport (start / middle / end) to trigger. Default: start,end — element top crosses viewport bottom.
start,end
Top edge crosses viewport bottom — triggers first
middle,end
Centre crosses viewport bottom — triggers when half visible
end,end
Bottom edge crosses viewport bottom — triggers last
data-scroll-position="start,end" ← default
data-scroll-position="middle,end"
data-scroll-position="end,end"Shifts the trigger threshold. Two comma-separated values: entry offset and exit offset. Accepts pixels or viewport-relative percentages. Positive delays trigger; negative brings it earlier.
0,0
No offset — triggers at natural edge
150,0
+150px — must travel 150px further into viewport
-80,0
−80px — triggers 80px before the natural edge
data-scroll-offset="0,0" ← default
data-scroll-offset="150,0" ← delay px
data-scroll-offset="-80,0" ← early px
data-scroll-offset="25%,0" ← % of viewportLocomotive Scroll automatically adjusts offsets for elements visible on initial page load (in-fold) to prevent them triggering immediately. data-scroll-ignore-fold disables this — the element uses raw offset values regardless of its initial position. Use when a hero element should animate in even though it starts in the viewport.
<!-- Default: offset auto-corrected for in-fold elements -->
<div data-scroll data-scroll-offset="100,0"></div>
<!-- Ignore fold: offset applied as specified, no adjustment -->
<div
data-scroll
data-scroll-offset="100,0"
data-scroll-ignore-fold
></div>Parallax
data-scroll-speed displaces an element relative to the scroll container. Positive values move counter to scroll (element rises as page scrolls down). Negative values amplify downward movement. Formula: displacement = progress × containerSize × speed × −1. Disabled on touch devices by default.
Decimal. Start at 0.1–0.5 for subtle depth. The three labelled layers in the demo below run at 0.5, 0, and −0.4 — scroll through slowly to feel the separation.
Common speed values and their practical application. Keep foreground copy at or near 0. Use larger values only for decorative background elements.
Parallax is suppressed on touch devices by default, preserving native scroll. Adding this attribute re-enables it per element. Test on target devices before using — lower-end hardware may not sustain smooth RAF under continuous parallax load.
<!-- Desktop-only parallax (default) -->
<div
data-scroll
data-scroll-speed="0.5"
></div>
<!-- Parallax on touch too -->
<div
data-scroll
data-scroll-speed="0.5"
data-scroll-enable-touch-speed
></div>Touch parallax can cause jank on lower-end devices. The default off state is the safer production choice. Enable only after performance testing.
Progress tracking
Two attributes expose per-element scroll progress as a 0–1 value. data-scroll-css-progress sets a --progress inline CSS variable — updated every RAF frame, drives animations with zero JS. data-scroll-event-progress fires a scroll-event-progress CustomEvent for JS-driven effects.
Sets --progress as an inline CSS custom property on the element, updated every animation frame as it moves through the rafRootMargin. CSS custom properties inherit — child elements can use var(--progress) directly. The fills below are driven purely by CSS.
Scroll through — fills track element progress
<div data-scroll data-scroll-css-progress>
<!-- --progress available to all descendants -->
<div class="fill"></div>
</div>
/* Pure-CSS animation */
.fill { width: calc(var(--progress, 0) * 100%); }Fires a scroll-event-progress CustomEvent on window every frame, with detail: { target, progress }. The ring gauge below listens for this event. Use for GSAP scrubs, canvas renders, or any JS-driven scroll animation.
Scroll through — ring tracks element progress
<div
data-scroll
data-scroll-event-progress="myEl"
></div>
window.addEventListener('scroll-event-progress', (e) => {
const { target, progress } = e.detail;
// progress: 0 → 1
});Custom events
data-scroll-call fires a scroll-call CustomEvent when an element enters or exits the viewport. The event detail returns the call ID, the direction (enter / exit), and the axis the element was crossed from. Use for analytics, video autoplay, lazy loading, or staged animation sequences.
Three trigger elements below each fire a different call ID. Scroll through them — each entry and exit is logged. data-scroll-repeat is required to receive both enter and exit events.
Listen on window. The detail object always contains the call ID, the entry/exit state, and the crossing direction. Filter by ID when multiple elements share the same page.
// HTML
<div
data-scroll
data-scroll-call="hero"
data-scroll-repeat ← required for exit events
></div>
// JS
window.addEventListener('scroll-call', (e) => {
const { id, way, from } = e.detail;
// id → value of data-scroll-call
// way → 'enter' | 'exit'
// from → 'top' | 'bottom'
if (id === 'hero' && way === 'enter') {
startHeroAnimation();
}
});Latency · latencydata.com
Decoding private markets.