Skip to content
Sahel & Fjord Oslo
NO EN
Colophon · Portfolio piece

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 15 August 2026

One rule, and everything follows

A photograph sits behind the whole site, in colour, and all the content stands on panes of frosted glass above it. This is not a styling exercise. It is a rule that settles every decision below it.

Three sentences are the whole rule. Nothing is opaque without a reason: if a surface holds content, it lets the ground through. The separation between two layers is light, not a line — a white edge at low coverage, never a black rule. And the blur is always the same amount: one glass, not seven different ones.

A rule like that 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 used to be something else: a season catalogue printed in two opaque colours on uncoated stock, with no transparency anywhere. The sections below were rewritten because the surfaces changed. What survives from that version survives because it is still true.

The accent has moved twice

Both times for the same reason, and neither was taste.

First time: the ochre was a light, pretty orange giving 2.83:1 against the paper, under the 4.5:1 requirement. It was darkened to 4.85:1 and stayed there as long as the ground was light.

Second time: the ground went dark. That same dark ochre gives 2.70:1 against the glass and falls under the bar there — it had been chosen to hold against light paper, after all. The accent had to be lifted back to roughly where it started.

Light ochre on paper, rejected
2.83:1
Dark ochre on paper, the print version
4.85:1
Dark ochre on glass, under the bar
2.70:1
The accent now, on glass
6.13:1

The point is not which colour won. It is that the colour was never chosen for itself — it was chosen by the ground it had to stand on, and when the ground changed, it had to move again.

One rule flipped with the ground. On a solid accent field the paper was the type in the print version; now it has to be the dark field. Light type on accent gives 2.52:1, and the last row below is there to show exactly how bad that is.

Light type on glass
15.44:1
Accent on glass
6.13:1
The field as type on accent
6.20:1
Light type on accent
2.52:1

The swatch sits next to the number because a table about contrast that only gives numbers is asking the reader to take them on faith. Do the arithmetic before touching any of the colours.

Glass needs something to refract

That is the one technical condition the whole redesign rests on. «backdrop-filter» does nothing visible over a flat surface — it blurs whatever is behind, and if what is behind is one solid colour, the blur looks exactly like that colour. Glass on a sheet of paper is just a lighter sheet of paper.

So the photograph sits fixed behind the whole site, in colour. The duotone the images used to carry was a printing rule, and this is not a print any more.

Over the photograph lies a dark scrim, and it is not decoration: without it the type stands on the subject, and the contrast is whatever the picture happens to give at that spot. The scrim turns the ground into a field of known value, and only then can the numbers in the section above be worked out at all.

If the browser has no «backdrop-filter», the panes close up instead and become solid fields in the ground colour. Half-transparent without blur is the one thing that does not work: then the type stands on the photograph with nothing in between.

Two built pages, no language toggle

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.

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

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 share image is the one surface on the whole site still drawn by the printing rule, and that is deliberate — a share image gets scaled down to a thumbnail in a chat, and a screen survives that scaling better than glass and gradients do.

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
3 300 lines, stdlib only
Front page over the wire
27 kB compressed
Fonts, three families
104 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. The blending is gone, and opacity is quantised to steps of 0.02.

Everything works without JavaScript

The tabs over the day-by-day plans are an enhancement, not a requirement: turn JavaScript off and all four plans stack up and read fine. The map is a drawing, not an application. The departures are written into the HTML.

The form is the only thing that needs 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.