How this site was made
The travel company is invented. The decisions are not. Here are the ones that mattered — and the ones that were wrong before they were fixed.
Last updated 27 September 2026
One rule, and everything follows
Light first. The site is set like a film at dusk: the canvas is night, and the only warmth is the light in the photographs and one golden accent. The opening screen is a pirogue against the sun off Saly, full height, with the title at the top where the sky has been darkened.
Three sentences are the whole rule. Night carries everything, and no surface is brighter than the photographs. Gold is used only where something should be read first or pressed. And the type has two voices: Cormorant, a tall, light serif, set large and thin like a film title, and Jost, a geometric sans, for body text and the letter-spaced labels.
A rule like this is worth more than the taste of whoever is drawing. It answers questions nobody has asked yet, and it answers them the same way every time.
The site has been five other things before this: a season catalogue printed in two solid inks, then frosted glass over a photograph, then flat fields alternating between sand and ink, then a single dark canvas with one lime accent, then a light canvas in sand and terracotta. The points below were rewritten each time the surfaces changed. What remains, remains because it is still true.
The accent has moved five times
Each time for the same reason, and none of them was taste: the colour was never chosen for itself, it was chosen by the ground it had to stand on.
The first time: the ochre was a light, pleasant orange that gave 2.83:1 against the paper, below the 4.5:1 requirement. It was darkened to 4.85:1 and stayed there while the ground was light.
The second time: the ground went dark. The same dark ochre gave 2.70:1 against the glass — it had been chosen to hold against light paper — and had to be lifted back roughly to where it started, to a sun that held 6.4:1 against the ink.
The third time: the ground went darker again. The lime held 16.5:1 against the dark canvas and was the first accent that could be both surface and type.
The fourth time: the canvas went light, and the lime fell to 1.3:1 against sand. The terracotta held 5.4:1 both ways.
The fifth time: the canvas went back to night, and the terracotta fell with it — it had been chosen to hold against sand, and against night it gives 3.4:1. The gold holds 11.3:1 against the night, and the night holds the same back on the gold surface, so it carries both the buttons and the one italic word in the title.
- Light ochre on paper, rejected
- 2.83:1
- Dark ochre on paper, the print edition
- 4.85:1
- Dark ochre on glass, below requirement
- 2.70:1
- Sun on ink, flat fields
- 6.40:1
- Lime on dark canvas
- 16.5:1
- Lime on sand, fell
- 1.30:1
- Terracotta on sand
- 5.40:1
- Terracotta on night, fell
- 3.43:1
- Gold on night, now
- 11.3:1
The point is not which colour won. It is that the colour moved every time the ground did, and that the arithmetic decided where it landed.
- Type on the canvas
- 17.0:1
- Accent on the canvas
- 11.3:1
- Canvas as type on the accent
- 11.3:1
- Muted type on the canvas
- 10.1:1
Against a photograph nothing can be worked out from the palette, so there the figures are measured on the finished page, pixel by pixel, by kontrast.py. The title stands at 15.3:1 against the sky on a phone and 16.7:1 on a desktop. The intro beneath it stands at 14.8:1, the button text on the gold surface at 10.5:1, and the credit down in the corner at 7.6:1. The weakest thing on the whole front page is the small labels meant to step back, at 5.7:1 — the requirement is 4.5.
The swatch sits next to the figure because a table about contrast that only gives numbers asks the reader to take them on trust. Do the sums before you touch any of the colours.
Light, and a little motion
Dusk allowed something the previous edition did not: a little motion. The photograph in the hero drifts slowly into place over sixteen seconds, the title fades up, and whatever comes into view further down rises a few pixels. If your system asks for less motion, everything stands still from the first frame.
The depth comes from light. The veil over the photograph is multiplied into the picture rather than laid on top, so the orange sky turns deep amber towards the top instead of grey. The glow around the buttons, the sun on the thermometer and the route on the globe are what shine.
- The phone gets its own crop. A landscape picture covering a portrait screen shows a third of its width, and the browser picks the file by screen width — the result was a picture blown up more than twice. Now the portrait crop is cut from the original at 1290 × 2796 pixels and served to portrait screens.
- The photographs got higher quality and one more width, 2400 pixels. On a dark ground, block artefacts in the shadows show far more than they did on sand.
- Every image and every font has a content hash in its file name. They are cached for a year; when a file gets new content, it gets a new address too.
- One picture went. The one at the top of the previous edition, labelled as the coast of the Petite Côte, was taken in Ada, Ghana, according to Unsplash. The coast pictures now are from Saly, and the place is stated on the Unsplash page of each.
The fonts have changed: Fraunces, Inter and JetBrains Mono are out, Cormorant and Jost are in. There is no monospace on the site any more — the figures are set in Cormorant, and the labels are letter-spaced Jost.
Two built pages, not a button that swaps the text
The first version switched language with JavaScript on the same URL. That creates three problems you cannot fix without building: the English text does not exist for search engines, nobody can link to it, and the choice is gone on the next page load.
Now Norwegian lives at «/» and English at «/en/», with hreflang between them. It costs one build step and solves all three.
So NO and EN in the top right are plain links between the two addresses, not a toggle. On the terms page, NO takes you to the terms in Norwegian, not to the front page.
Sniffing the browser language is gone too, and it is worth a warning: a Norwegian system reports «nb-NO», not «no». The old test compared the first two letters against «no» and so sent every single Norwegian visitor to the English version.
The share image: 507 kB nobody saw
The picture that shows up when someone shares the link is drawn in plain Python, with random grain laid over it to look like print. It looked right. It weighed 507 kB — more than the rest of the site put together.
Noise has no repetition, so compression cannot get a grip on it. And WhatsApp will not fetch share images much over 300 kB, so the preview quietly failed there. No error; just no picture.
- With random grain
- 507 kB
- With row filtering as well
- 656 kB — worse
- With a printer's screen instead
- 16 kB
- The same screen, drawn on night
- 22 kB
- The photograph as JPEG, now
- 76 kB
Row filtering is the trick you read about, and here it made things worse: the filter stores the difference from the neighbouring pixel, and the difference between two random numbers is just as random.
The fix was a screen instead: a pattern that repeats by definition and therefore compresses for almost nothing.
The screen is gone too. In a chat it read as clip art, not as the place. The share image is now a crop of the hero photograph, with the name in Cormorant and the gold hairline, saved as JPEG. As a PNG the same picture weighed 840 kB — over the limit again.
What never loads
No npm, no build tooling, no framework, no CDNs, no cookies, no analytics, no tracking. The fonts live in the repository. Your browser talks to one domain while you are here.
- Dependencies
- 0
- Python source
- 5 500 lines, stdlib only
- Front page over the wire, without images
- 38 kB compressed
- Fonts, two families in three files
- 76 kB
None of that is something you should have to take on trust. A Content Security Policy blocks everything external, so the claim cannot become false without somebody switching it off first.
The traps that cost the most time
- «1fr» has min-content as its floor. One long word in capitals was wider than its column, and the whole grid grew off the page at 280 px. «minmax(0,1fr)» fixes it, and it is worth looking for whenever something sticks out on a narrow screen.
- Geometry in CSS is not geometry in SVG. The pins on the map scale with transform rather than by setting «r» — geometry attributes as CSS are unsupported in older WebKit.
- The coastline had to be drawn as an open stroke, separate from the filled landmass. Stroke the closed shape and you stroke the straight viewBox edges too, and the map ends up in a box.
- requestAnimationFrame is throttled in background tabs, so a counting animation can stop halfway. The correct value is already in the HTML, and a timeout falls back to it. A half-finished number must never be left standing.
- On iPhone, a fixed blending layer, a blur in the header and an opacity write every frame produced stutter while scrolling. Nothing fixed has blending or blur any more — the grain and the header are plain layers — and opacity is quantised to steps of 0.02.
Everything works without JavaScript
All the content is in the HTML: the rooms, the places, the weekends, the questions and the months the house is open read in full with JavaScript switched off. The photos are plain images until the script makes them tappable, and the films have a plain link instead of the player. The maps are drawings, not applications.
The globe below the opening screen is the one exception, and it is built as an addition: the flight map sits in the HTML the whole time, and the globe only lays itself over it once it has managed to draw a frame. Without JavaScript, without canvas, or if the 33 kB of point data and drawing code never arrives, the map stays. If your system asks for less motion, the globe holds still.
The form is the only thing that does not work without script, and it stores nothing — it opens your email client with the text filled in, and you send it yourself.
The dates date themselves
Departures are given as month and day, with no year. The build adds the year based on when it runs, and shows only departures still ahead. The season counts as November to March, so a build in February picks up the rest of the current season and carries on into the next.
It is the kind of detail nobody notices when it works, and everybody notices when a site sits there in March showing last year's dates.