Quick Start
Upload the folder.
index.html,assets/style.css, andassets/script.js(orassets/script.min.js) are all you need. It is static: no bundler, no framework.Keep the three script tags just before
</body>and leave them as plain tags (nointegrityattribute, so a version bump never breaks your deployed copy):
Code snippet omitted: it ships with the download.
- Make it yours. Change the palette in the
:rootblock ofstyle.css(see Palette), rewrite the copy inindex.html, and edit the itinerary (see The itinerary map). Point the enquiry form at your own inbox or booking system.
Sections & options
Every section is initialised behind a guard, so you can delete any section in index.html and the rest keeps working. No script edit needed.
| Section | What it is | To change |
|---|---|---|
| Nav | Fixed, blurred bar with a compass-diamond mark | Edit links in .nav-links; the mark is .brand-mark (pure CSS) |
| Hero | Trip name, masked-line headline, key facts, framed trip photo | Headline lines are .line inside .mask; the photo is .hero-img (swap in Images) |
| Trail strip | Row of village names | Plain text in .trail-inner |
| Featured trips | Three poster tiles, each a real trip photo under a teal scrim | Add/remove .trip cards; swap .trip-img sources (see Images) |
| Itinerary map | The signature (see below) | Edit the day list and pins |
| Included + dates | What's covered and departures | Edit .included and .date-card blocks; .date-card--full is the waitlist state |
| Testimonial | A dinner photo over one quote on a sand card | Photo is .voice-photo; text in .voice-quote / .voice-by |
| Enquiry | Styled form | Set the <form action>; it is inert until you wire it |
| Footer | Four-column | Standard links |
The itinerary map (signature)
The map lives in the #route section. Two things must stay in sync: the route path and the day-pins.
- The coast and route are hand-drawn SVG
<path>s in.map-svg(viewBox="0 0 820 620"). The route is the<path data-route>. Draw whatever coastline you like; the route just needs a real length. - Each day-pin is a
<button class="pin">positioned with two custom properties in itsstyle:
Code snippet omitted: it ships with the download.
--x and --y are percentages of the map box. To place a pin on the route, take a point (x, y) on your path and set --x: x/820 * 100%, --y: y/620 * 100%.
- Each day is one
<li>in.daysholding a.day-tabbutton and a.dayarticle. They are tied together by number:data-pin,data-day-tabanddata-day-panelsharing the same value, plus matchingid/aria-controls. Add or remove a whole day by adding/removing its pin and its<li>with a new matching number — the script pairs them by number, not by position, so nothing shifts out of step.
Selecting a day (click a pin, click a rail button, or arrow-key between rail buttons) lights the pin, marks the button and fades in that day's card. The route line draws once when the section scrolls into view.
Palette
The whole system is four painted colours plus two supporting inks. Change these in :root and the site reskins; every translucent value resolves from a --*-rgb triple, so no rgba() literal is left mixed from the old hue.
Code snippet omitted: it ships with the download.
Measured contrast (as used, not as declared)
| Pair | Ratio | Verdict |
|---|---|---|
| Cream on teal ground | 4.49:1 | AA for text, at the floor — see note |
Ink #0C2523 on sand card |
12.7:1 | AAA |
Ink-soft #3A5F5A on sand |
5.58:1 | AA, secondary text |
| Ink on a coral fill (buttons, pin numerals) | 4.9:1 | AA |
| Ink focus ring on teal | 3.13:1 | Passes 3:1 for non-text UI |
| Sand surface vs teal ground | 4.06:1 | Passes 3:1 for a UI boundary |
Two rules hold this palette together, and a reskin must keep them:
- Cream on teal is at the AA floor (4.49:1), so it is never dimmed. Hierarchy on the teal ground comes from size and weight, not opacity: a muted cream drops below the floor. Sustained reading lives on the sand cards, where ink is 12.7:1. Darkening the teal or lightening the cream buys headroom if you want a dim variant on the ground.
- Coral is never text. It measures 1.57:1 on teal, 2.59:1 on sand and 2.87:1 on cream — all failing. It is used only as fills, the route line, the day-pins, rules and marks. The route line is decorative: the full itinerary is carried by the numbered day list and the pins' screen-reader labels, so nothing depends on seeing the coral line.
Images
The five photographs in assets/img/ are bundled, optimised WebP derivatives, not stock files passed through. Each has the template's art direction baked into the pixels: saturation pulled to 0.9, a +4° hue rotation that warms the greens towards olive, a slight gamma lift to open the shadows, and a per-image brightness so a bright vineyard and a warm evening dinner read as one sunny campaign against the teal ground. Total weight is about 800KB across five images, largest (the hero) 314KB.
- Hero (
hero-walkers.webp) — walkers on the coast path, framed beside the headline. The headline itself stays on the solid teal ground, so its cream keeps 4.49:1; only the small caption sits over the photo, held above 4.5:1 by a bottom scrim. - Trip posters (
trip-alentejo,trip-douro,trip-azores) — one real photo per tile, under a teal-deep scrim tuned against the baked pixels so the cream tag (top) and name/price (bottom) clear 4.5:1 even over a bright sky, with a clear window in the middle where the subject reads. - Testimonial (
voice-dinner.webp) — sits above the quote, never behind it, so the quote keeps its 12.7:1 on the sand card.
Every image has real alt, intrinsic width/height (no layout shift), loading="lazy" (the hero is fetchpriority="high" instead), and a background-color fallback on its container for the load/decode gap. With images off, every section is still complete: all copy is live text, nothing is baked into a picture.
Regenerating
assets/img-manifest.json lists every image: source URL, output name, width, WebP quality and grade. To re-bake after editing it:
Code snippet omitted: it ships with the download.
The script downloads each source, bakes the grade into the pixels, and writes assets/img/*.webp. Shipped files are altered, art-directed derivatives, never redistributed originals.
Swapping in your own photographs
- Drop your files in
assets/img/and repoint the<img src>inindex.html, or - Edit the
srcvalues inimg-manifest.json(a remote URL or a local path), tune each image'sbrightnessso they look equally bright rather than being nominally equal, and re-run the command above. This is what keeps a mixed set looking like one campaign.
Then update the alt text in index.html: it is written for the photographs that ship. If you change how bright a trip photo is, re-check that the cream tag and price still clear 4.5:1 over it (the scrim is on .trip::after).
Credits
Original photographs from Unsplash, used under the Unsplash licence and shipped here as altered, art-directed derivatives:
- Hero, coastal walkers — Josh Hild — https://unsplash.com/photos/a-couple-of-people-walking-down-a-dirt-road-AlphUdhXAW4
- Alentejo, walker in the vines — Mathew Benoit — https://unsplash.com/photos/man-in-white-shirt-walking-on-green-grass-field-during-daytime-_us4S66qEh4
- Douro, passenger boat — Joshua Katt — https://unsplash.com/photos/a-passenger-boat-sails-on-a-wide-river-7wrpKdPlBlk
- Azores, Sete Cidades crater lakes — Marco Fernandes — https://unsplash.com/photos/green-volcanic-crater-lakes-with-blue-ocean-background-mk1f_Ajdqf8
- Testimonial, the welcome dinner — Bohdan (undiabler) — https://unsplash.com/photos/a-group-of-people-sitting-around-a-table-eating-food-F2lsSOd2DS8
- Day 1, cliffs above the harbour beach — Unsplash — https://images.unsplash.com/photo-1751711157233-651fc0d1806e
- Day 3, sandstone coastal cliffs — Unsplash — https://images.unsplash.com/photo-1699726232720-402e00259c6c
- Day 5, the beach at Odeceixe — Unsplash — https://images.unsplash.com/photo-1652092521226-3e03a716ae86
(The three day photos are credited by source URL; look each up on unsplash.com for the photographer, or replace them with your own trip photos before launch.)
Accessibility
- Keyboard: day-pins and rail buttons are real
<button>s. The rail is a roving-tabindex group:Tabreaches the active day, then arrow keys,HomeandEndmove between days and shift focus. Every pin carries avisually-hiddenlabel naming its day and village. - Without JavaScript: the seven days render as a complete ordered list (title, activities, where you sleep), and the map is a decorative aside. The interactive card, the rail buttons and the pin behaviour only appear once JavaScript adds the
has-jsclass, so no content is ever hidden behind a control that cannot run. - Reduced motion: via both the CSS media query and a
matchMediabranch, the page is fully static — the route is painted in full, day one is the open card, the headline is settled, and nothing scrubs. - Touch: pins are 2.1rem tap targets and the whole interaction is tap/click driven; there is no hover-only behaviour.
Customising further
- Add a trip tile: copy a
.tripblock, point its.trip-imgat another photo (add it toimg-manifest.jsonand re-bake, see Images) and edit the label. Cards reveal on scroll automatically. - Change the hero image: it is
.hero-imginside.hero-motif; swap the source in the manifest and re-bake. The.hero-motifbox keeps a teal-deep fill as the load fallback. - Lenis: smooth scroll is optional. Delete the
lenisblock inscript.jsand its CDN tag to drop it.
Dependencies
- GSAP 3.15.0 + ScrollTrigger
- Lenis 1.3.17 (optional smooth scroll)
- Google Fonts: DM Serif Display (headings) and Sen (body/UI)
No DrawSVG plugin: the route line uses the stroke-dasharray / stroke-dashoffset technique.
Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.