Orrery Air
Boards09:40
LDNLondon
LSBLisbon
- Passenger
- Ines Moreau
- Cabin
- Economy
- Flight
- OR 214
- Date
- 14 Jun
- Departs
- 10:15
- Gate
- B12
- Seat
- 14A
- Group
- 2
Gate closes at 10:00
Orrery Air
Boards13:05
LSBLisbon
MRKMarrakesh
- Passenger
- Ines Moreau
- Cabin
- Economy
- Flight
- OR 351
- Date
- 18 Jun
- Departs
- 13:40
- Gate
- A04
- Seat
- 07F
- Group
- 1
Gate closes at 13:25
Orrery Air
Boards06:50
MRKMarrakesh
RKVReykjavik
- Passenger
- Ines Moreau
- Cabin
- Economy
- Flight
- OR 1180
- Date
- 22 Jun
- Departs
- 07:30
- Gate
- C21
- Seat
- 22C
- Group
- 3
Gate closes at 07:10
Orrery Air
Boards23:10
RKVReykjavik
KIXOsaka
- Passenger
- Ines Moreau
- Cabin
- Premium
- Flight
- OR 88
- Date
- 27 Jun
- Departs
- 23:55
- Gate
- D02
- Seat
- 31K
- Group
- 4
Gate closes at 23:35
Orrery Air
Boards11:25
KIXOsaka
SELSeoul
- Passenger
- Ines Moreau
- Cabin
- Business
- Flight
- OR 730
- Date
- 02 Jul
- Departs
- 12:00
- Gate
- E15
- Seat
- 03A
- Group
- 1
Gate closes at 11:45
Drag the front pass down, or use the arrow keys.
01
Boarding passes
Five legs of one trip, stacked top to bottom. The stack stops at the last leg.
axis: 'y'bounds: 'clamp'fan({ offset: 60 })on('change')
Show codeHide code
import { createRiffle, fan } from '@rpxl/riffle'
const passes = Array.from(stack.querySelectorAll<HTMLElement>('.pass'))
const riffle = createRiffle(stack, {
count: passes.length,
axis: 'y',
bounds: 'clamp',
cardWidth: 'auto',
cardHeight: 'auto',
maxVisible: 4,
layout: fan({ offset: 60, scaleStep: 0.07 }),
getLabel: (i) => passes[i]!.dataset.label!,
})
passes.forEach((pass, i) => riffle.registerNode(i, pass))
// The whole scene takes on the colour of wherever you fly next.
riffle.on('change', ({ index }) => {
scene.style.setProperty('--bg', passes[index]!.dataset.tint!)
})
// Clamped at both ends, so the buttons say when there is nowhere to go.
const sync = () => {
const { activeIndex, canPrev, canNext } = riffle.getSnapshot()
prevButton.setAttribute('aria-disabled', String(!canPrev))
nextButton.setAttribute('aria-disabled', String(!canNext))
counter.textContent = `${activeIndex + 1} / ${passes.length}`
}
riffle.subscribe(sync)
sync()
prevButton.addEventListener('click', () => riffle.prev())
nextButton.addEventListener('click', () => riffle.next())
Concrete Bloom
Mira Solano

Night Bus Hymns
The Low Lanterns

Tangerine Static
Odalys and the Wires

Parallax
Kofi Mensah-Dahl

Soft Machines
Ines Varga Quartet

Moss Hours
Heron Coast
Now playing: Concrete Bloom by Mira Solano
Drag sideways. It loops.
02
Record crate
Sleeves loop like a shop bin. When the stack settles, the record slides out.
bounds: 'loop'on('settle')on('dragstart')
Show codeHide code
import { createRiffle, fan } from '@rpxl/riffle'
const sleeves = Array.from(stack.querySelectorAll<HTMLElement>('.sleeve'))
// bounds defaults to 'loop': the crate never runs out.
const riffle = createRiffle(stack, {
count: sleeves.length,
cardWidth: 'auto',
cardHeight: 'auto',
maxVisible: 4,
layout: fan({ offset: 30, scaleStep: 0.06 }),
getLabel: (i) => sleeves[i]!.dataset.label!,
})
sleeves.forEach((sleeve, i) => riffle.registerNode(i, sleeve))
// At rest, the record slides out of the front sleeve and spins.
riffle.on('settle', ({ index }) => sleeves[index]!.classList.add('is-playing'))
// Grab the stack, or move to another sleeve, and it slides back in.
const tuckAway = () => sleeves.forEach((sleeve) => sleeve.classList.remove('is-playing'))
riffle.on('dragstart', tuckAway)
riffle.on('change', ({ index }) => {
tuckAway()
nowPlaying.textContent = sleeves[index]!.dataset.label!
})
// The first sleeve never settled into place, so it starts out playing.
sleeves[0]!.classList.add('is-playing')Stage 1
Emberfox
HP120

Ember type Shardling, stage 1
- Cinder Flick30
- Tail Bonfire90
- Weakness
- Tide x2
- Resistance
- None
- Retreat
- 1
Leaves warm footprints in fresh snow. Nobody has ever caught one asleep.
Stage 2
Tidewyrm
HP140

Tide type Shardling, stage 2
- Undertow50
- King Swell130
- Weakness
- Spark x2
- Resistance
- None
- Retreat
- 2
Sailors blame it for every wave that arrives a second too early.
Basic
Voltmoth
HP80

Spark type Shardling, basic
- Static Dust20
- Porch Light60
- Weakness
- Grove x2
- Resistance
- None
- Retreat
- 1
Drawn to streetlights, which tend to flicker for a week afterwards.
Stage 2
Thornback
HP160

Grove type Shardling, stage 2
- Bramble Wall40
- Old Growth120
- Weakness
- Ember x2
- Resistance
- None
- Retreat
- 2
Moves one field a year. Moss grows on the shell faster than it walks.
Stage 1
Gloomowl
HP100

Dusk type Shardling, stage 1
- Hush20
- Midnight Stare100
- Weakness
- Spark x2
- Resistance
- None
- Retreat
- 1
Asks one question each night. Nobody remembers the answer by morning.
Grab a corner and drag.
03
Trading cards
Grab a corner and the card tilts on it. The foil follows the drag.
rotation: { maxRotation: 24 }on('drag')progressoffset
Show codeHide code
import { createRiffle } from '@rpxl/riffle'
const cards = Array.from(stack.querySelectorAll<HTMLElement>('.holo'))
const riffle = createRiffle(stack, {
count: cards.length,
cardWidth: 'auto',
cardHeight: 'auto',
// A big tilt, mostly from where you grab: corners swing hardest.
rotation: { maxRotation: 24, leverFactor: 0.9 },
getLabel: (i) => cards[i]!.dataset.label!,
})
cards.forEach((card, i) => riffle.registerNode(i, card))
let held: HTMLElement | null = null
let width = 1
riffle.on('dragstart', ({ index }) => {
held = cards[index]!
width = held.offsetWidth
held.classList.add('is-held')
})
// Every frame of the drag: no allocation, just two numbers for the CSS.
riffle.on('drag', ({ progress, offset }) => {
if (!held) return
held.style.setProperty('--shift', (offset / width).toFixed(3))
held.style.setProperty('--glow', Math.min(1, Math.abs(progress)).toFixed(3))
})
// Let go and the light settles back with a CSS transition.
riffle.on('dragend', () => {
if (!held) return
held.classList.remove('is-held')
held.style.setProperty('--shift', '0')
held.style.setProperty('--glow', '0')
held = null
})花火
はなびhanabi
駅
えきeki
猫
ねこneko
雨
あめame
朝ご飯
あさごはんasagohan
友達
ともだちtomodachi
Press Enter or Space to flip this card.
←→ move, Enter or Space flips
04
Flashcards
Arrow keys move, Enter flips. Nothing here needs a mouse.
bounds: 'clamp'on('change')getLabel
Show codeHide code
import { createRiffle } from '@rpxl/riffle'
const cards = Array.from(stack.querySelectorAll<HTMLElement>('.flashcard'))
const riffle = createRiffle(stack, {
count: cards.length,
bounds: 'clamp',
cardWidth: 'auto',
cardHeight: 'auto',
maxVisible: 3,
getLabel: (i) => cards[i]!.dataset.label!,
})
cards.forEach((card, i) => riffle.registerNode(i, card))
// Only the face you can see is in the accessibility tree.
const setFlipped = (card: HTMLElement, flipped: boolean) => {
card.classList.toggle('is-flipped', flipped)
card.querySelector('.front')!.setAttribute('aria-hidden', String(flipped))
card.querySelector('.back')!.setAttribute('aria-hidden', String(!flipped))
live.textContent = flipped ? card.dataset.answer! : ''
}
// A tap flips, but a drag that started on the card does not.
let dragged = false
stack.addEventListener('pointerdown', () => (dragged = false))
riffle.on('dragstart', () => (dragged = true))
for (const card of cards) {
const flip = () => setFlipped(card, !card.classList.contains('is-flipped'))
card.addEventListener('click', () => dragged || flip())
card.addEventListener('keydown', (event) => {
if (event.target !== card || (event.key !== 'Enter' && event.key !== ' ')) return
event.preventDefault()
flip()
})
}
// Arrow keys, Home and End are built in. Moving on turns the last card back.
riffle.on('change', ({ index, previousIndex }) => {
setFlipped(cards[previousIndex]!, false)
dots.forEach((dot, i) => dot.classList.toggle('is-active', i === index))
})Fernwick
Ethiopia
Yirgacheffe, washed
- Jasmine
- Bergamot
- White peach
- Altitude
- 2,000 m
- Variety
- Heirloom
250 g, whole bean
Fernwick
Colombia
Huila, washed
- Red apple
- Panela
- Cocoa
- Altitude
- 1,700 m
- Variety
- Caturra
250 g, whole bean
Fernwick
Kenya
Nyeri, washed
- Blackcurrant
- Grapefruit
- Cane sugar
- Altitude
- 1,800 m
- Variety
- SL28
250 g, whole bean
Fernwick
Guatemala
Huehuetenango, washed
- Milk chocolate
- Almond
- Orange
- Altitude
- 1,750 m
- Variety
- Bourbon
250 g, whole bean
Fernwick
Sumatra
Gayo, wet hulled
- Cedar
- Dark chocolate
- Clove
- Altitude
- 1,400 m
- Variety
- Typica
250 g, whole bean
Fernwick
Brazil
Mogiana, natural
- Hazelnut
- Caramel
- Raisin
- Altitude
- 1,100 m
- Variety
- Mundo Novo
250 g, whole bean
Drag a bag or tap the arrows.
05
Coffee origins
The page takes the colour of whichever bag is in front.
on('change')style.setProperty('--bg')style.setProperty('--ink')
Show codeHide code
import { createRiffle, fan } from '@rpxl/riffle'
const bags = Array.from(stack.querySelectorAll<HTMLElement>('.bag'))
const riffle = createRiffle(stack, {
count: bags.length,
cardWidth: 'auto',
cardHeight: 'auto',
maxVisible: 4,
layout: fan({ offset: 36, scaleStep: 0.06 }),
getLabel: (i) => bags[i]!.dataset.label!,
})
bags.forEach((bag, i) => riffle.registerNode(i, bag))
// Your brand, your colours: the scene floods to the bag in front, and
// the headline, copy and buttons follow through the --ink property.
riffle.on('change', ({ index }) => {
const { flood, ink } = bags[index]!.dataset
scene.style.setProperty('--bg', flood!)
scene.style.setProperty('--ink', ink!)
})
prevButton.addEventListener('click', () => riffle.prev())
nextButton.addEventListener('click', () => riffle.next())
Two Days InMexico City, Sat 08:30
Saturday 08:30, Breakfast
Chilaquiles verdes and a coffee strong enough to argue with.

Two Days InMexico City, Sat 11:00
Saturday 11:00, Walk
Roma Norte under the jacarandas. Look up. The purple is the point.

Two Days InMexico City, Sat 14:00
Saturday 14:00, Lunch
A market counter, a plastic stool, and whatever the cook says is good today.

Two Days InMexico City, Sat 19:30
Saturday 19:30, Rooftop
Golden hour over the old town. Bring a jacket, it cools fast.

Two Days InMexico City, Sun 10:00
Sunday 10:00, Park
Chapultepec by pedal boat. Go slowly and call it exercise.

Two Days InMexico City, Sun 13:00
Sunday 13:00, Tacos
Al pastor off the spit. Order two more than you planned.
Hover, focus or grab a story to hold it.
06
City stories
Advances on its own every five seconds and holds while you touch it.
next()on('dragstart')prefers-reduced-motion
Show codeHide code
import { createRiffle } from '@rpxl/riffle'
const stories = Array.from(stack.querySelectorAll<HTMLElement>('.story'))
const riffle = createRiffle(stack, {
count: stories.length,
cardWidth: 'auto',
cardHeight: 'auto',
maxVisible: 3,
getLabel: (i) => stories[i]!.dataset.label!,
})
stories.forEach((story, i) => riffle.registerNode(i, story))
prevButton.addEventListener('click', () => riffle.prev())
nextButton.addEventListener('click', () => riffle.next())
riffle.on('change', ({ index }) => {
segments.forEach((segment, i) => {
segment.classList.toggle('is-done', i < index)
segment.classList.toggle('is-active', i === index)
})
})
if (prefersReducedMotion()) return // No auto-advance, ever.
bar.addEventListener('animationend', () => riffle.next())
// Any one reason to wait holds the bar, and with it the timer.
const holds = new Set<string>()
const hold = (reason: string, on: boolean) => {
holds[on ? 'add' : 'delete'](reason)
bar.classList.toggle('is-paused', holds.size > 0)
}
riffle.on('dragstart', () => hold('drag', true))
riffle.on('dragend', () => hold('drag', false))
stack.addEventListener('pointerenter', (e) => hold('hover', e.pointerType === 'mouse'))
stack.addEventListener('pointerleave', () => hold('hover', false))
stack.addEventListener('focusin', () => hold('focus', true))
stack.addEventListener('focusout', () => hold('focus', false))
pauseButton.addEventListener('click', () => {
const paused = pauseButton.getAttribute('aria-pressed') !== 'true'
pauseButton.setAttribute('aria-pressed', String(paused))
hold('button', paused)
})