Moonstruck reads like the boat's own instrument panel on a night watch: a near-black blue field, lit readings on it, an italic serif that speaks and small tracked capitals that annotate.
This page documents what index.html, assets/pages.css and tools/chart-pages.py ship. DESIGN.md holds the reasoning behind each rule named here; where the two disagree, the source is the truth. tools/build-docs.py writes the token values and contrast ratios into this page and lifts several specimens straight out of the generated pages.
Palette
Three ink steps carry the page, two creams carry the type, one tint carries everything quiet, and two accents divide the work between acting and measuring.
Every value was measured against the field it sits on, not picked by eye. A new step inside a family is a design decision; a new hue is one to take to the client.
Field
#040c16#07101d#010509#061e45Type
#f5efd9#eef3ffrgb(200 225 255 / 0.78)rgb(200 225 255 / 0.66)rgb(200 225 255 / 0.55)Accents
#00c8e8#00b2d8#faa80f#c0572aWhat each color is for
| Color | Token | Role |
|---|---|---|
| Deep Water | --ink-page | The page field: the reading sections, and what shows behind an overscroll. The browser's theme color. |
| Night Watch | --ink-panel | The lifted step: panels, cards, the navigation bar, photo mats, a booked day. |
| Midnight Hull | --ink-well | The recessed step: wells, the footer, badges, a turnaround day, and what shows behind a photograph while it loads. |
| Moonstruck Navy | --chart-navy | The chart field and the sea plate. Asset-bound: every satellite plate is toned to it. |
| Lamp Cream | --cream | Headlines and every large figure. Warm on purpose, like a lit filament. |
| Instrument White | --cream-bright | Body copy, lists and UI text. |
| the tint at 0.78 | --text-soft | Supporting text, section leads and the base link color. |
| the tint at 0.66 | --text-mute | Muted small print and the tracked label voice. |
| the tint at 0.55 | --text-faint | The text floor: a booked or turnaround day. |
| the rule tint | --line-rule | Hairlines at 0.12. The quietest edge is 0.06 (--line-soft). |
| Depth Cyan | --cyan | The booking action, and the measurements: chart pins, key numerals, an open day, the focus ring. |
| Depth Cyan Muted | --cyan-muted | Text selection, gauge fills, and the sea at the foot of the mark. |
| Anchor Light | --amber | Held in reserve. Nothing is set in it; if it returns, it takes Deep Water text. |
| Rust | --rust | A stale reading, as a wash and a border. Never type: it is under the text floor. |
Translucent steps are written from the channel tokens, as in rgb(var(--tint-rgb) / 0.72). No type is set below the text floor.
- The One Field RuleEvery section is one of the three ink steps, told from its neighbor by how far it sits above or below the page. There are no light, white or gray surfaces. A panel that wants to stand out steps up to Night Watch; one that wants to recede drops to Midnight Hull.
- The One Action RuleDepth Cyan fills the single most important action in a view, the Charter Smarter booking link, and nothing else. The floating bar hides itself while the closing section's button is on screen, so two never share a viewport. The rule is about there being one action, not about its hue: the fill is
--action. - The Cyan Is Data RuleBeside that button, Depth Cyan is reserved for what carries a measurement: chart pins and routes, key numerals, gauge fills, an open day. Never a heading, a border or a flourish.
- The Lit and Unlit RuleA state that is off is drawn, not dimmed. A booked day sits on its own Night Watch panel and a turnaround day drops into a Midnight Hull well, each still numbered at the text floor.
Fields and contrast
One dark world in three steps. Depth comes from how far a surface sits above or below the page, never from a change of temperature.
There is no light section, and the site does not follow the device's light or dark setting (color-scheme: dark).
On Deep Water
A week has shape and generosity.
Body copy takes Instrument White. Prose links take the tint at 0.78, and Instrument White on hover.
Annotation in the tint at 0.66
On Night Watch
A week has shape and generosity.
The lifted step: panels, cards, the navigation bar. Type and links are the same.
Annotation in the tint at 0.66
Measured contrast
Computed from the shipped token values by tools/build-docs.py, with a translucent ink composited over its field first and rounded down to one decimal, so no pair is ever rounded up over a threshold. Text needs 4.5:1; large text and non-text marks such as the focus ring need 3:1.
| Sample | Pair | Ratio | Used for |
|---|---|---|---|
| Aa Instrument White | Instrument White on Deep Water | 17.6:1 | Body copy, lists and UI text on the page field. |
| Aa Lamp Cream | Lamp Cream on Deep Water | 17.0:1 | Headlines and every large figure. |
| Aa Instrument White | Instrument White on Night Watch | 17.1:1 | Text on panels, cards and the navigation bar. |
| Aa Instrument White | Instrument White on Midnight Hull | 18.4:1 | Text in wells and the footer. |
| Aa Instrument White | Instrument White on Moonstruck Navy | 14.7:1 | Type over a chart field. |
| Aa at 0.78 | Tint at 0.78 on Deep Water | 9.0:1 | Supporting text and section leads. |
| Aa at 0.66 | Tint at 0.66 on Deep Water | 6.6:1 | Muted small print and the tracked label voice. |
| 12 at 0.55 | Tint at 0.55 on Deep Water | 4.9:1 | The text floor: a booked or turnaround day. |
| Aa Depth Cyan | Depth Cyan on Deep Water | 9.7:1 | Data, chart furniture and the focus ring. |
| Aa Book | Deep Water on Depth Cyan | 9.7:1 | The one booking action. |
| 7 pin | Midnight Hull on Depth Cyan | 10.1:1 | Chart pins and key numerals. |
| Aa chosen | Deep Water on Instrument White | 17.6:1 | A hovered dark button. |
| Aa Book | Deep Water on Anchor Light | 9.9:1 | The only ink Anchor Light takes, held in reserve. |
Pairs that are never used
Recorded so that nobody reaches for them. Each has cost a bug before.
| Sample | Pair | Ratio | Instead |
|---|---|---|---|
| Instrument White on Anchor Light | 1.7:1 | Anchor Light takes Deep Water. | |
| Instrument White on Depth Cyan | 1.8:1 | A cyan fill takes a dark ink. | |
| Rust on Deep Water | 4.3:1 | Under the floor. Rust is a wash and a border, never type. | |
| Rule tint on Deep Water | 1.2:1 | A rule color. Text takes the 0.55 step or better. |
- The Photographic Field RuleA section that carries a photograph is washed with
rgb(var(--ink-page-rgb) / α)so the image settles back into the night field: 0.88 on CAPTAIN360, 0.86 on rates. α is solved so cream type clears 4.5:1 over the photograph's brightest pixel. The tint's soft and mute steps never sit on the wash; they get a Night Watch panel. Re-measure when a photograph changes. - The Calendar Legibility RuleEvery calendar day state clears 4.5:1, and state is never carried by color alone: each day has hidden text, and on-hold days keep their stripe.
Themes
Everything else in this guide is Night Watch, the theme a visitor arrives in. Ensign is the second: the same tokens, pinned again on red, white and two blues over navy.
A visitor chooses with the switch in the footer of every page, and the choice stays in their browser. A theme is one block of token values, [data-theme="ensign"], and nothing else: no rule asks which theme is on. On the root element it themes the page; on any other it themes what is inside it, as the plate below does. Ensign is still one dark world: its darkest color is the page field, so every section is one of three dark steps and every wash solved for light type keeps its shape. Values and ratios are filled from that block by tools/build-docs.py.
Field
#1d3557#234062#132339#061e45Type
#f1faeergb(241 250 238 / 0.82)rgb(241 250 238 / 0.7)rgb(241 250 238 / 0.6)rgb(241 250 238 / 0.16)Data and action
#a8dadc#8abec9#e63946#09101brgb(69 123 157 / 0.25)On Ensign Navy: the one booking action in Signal Red with its own label ink, beside .btn-dark and .btn-ghost. The same markup as under Buttons and controls; only the tokens differ.
Measured contrast
Measured on Ensign's own values. The tint steps sit higher than Night Watch's (0.82, 0.7 and 0.6 against 0.78, 0.66 and 0.55) because the field is lighter, and are measured on the panel step, the worst of the three fields. The booking label's ink is the navy taken down to 7% lightness, because nothing in the scheme clears 4.5:1 on the red.
| Sample | Pair | Ratio |
|---|---|---|
| Aa Sailcloth | Sailcloth on Ensign Navy | 11.5:1 |
| Aa Sailcloth | Sailcloth on the panel step | 9.9:1 |
| Aa Sailcloth | Sailcloth in the well | 14.8:1 |
| Aa Sailcloth | Sailcloth on Moonstruck Navy | 15.3:1 |
| Aa at 0.82 | Tint at 0.82 on the panel step | 7.2:1 |
| Aa at 0.7 | Tint at 0.7 on the panel step | 5.7:1 |
| 12 at 0.6 | Tint at 0.6 on the panel step | 4.7:1 |
| Aa Shoal Blue | Shoal Blue on Ensign Navy | 8.0:1 |
| 7 pin | The well on Shoal Blue | 10.3:1 |
| Aa Book | The label ink on Signal Red | 4.5:1 |
Pairs that are never used
| Sample | Pair | Ratio | Instead |
|---|---|---|---|
| Sailcloth on Signal Red | 3.8:1 | The red always takes its own label ink. | |
| Signal Red on Ensign Navy | 2.9:1 | Under the floor for type and for a thin mark. Signal Red is a fill: the booking action, and a stale reading's wash and border. |
What a theme leaves alone, and what it solves again
Whatever is bound to an asset stays: Moonstruck Navy and the sea plate are matched to 26 satellite plates, so in Ensign a chart sits a step below the page where in Night Watch it sits above it, and Anchor Light stays in reserve. Whatever was solved against the field is solved again: in Ensign the photographic wash, --ink-page-rgb, is 9 16 27 rather than the page field, because with Ensign Navy in the wash the US Virgin Islands headline fell to 2.76:1 on a phone; and --wash-drawing comes down from 0.45 to 0.32, because the layout drawing's lines composite brighter on a lighter field. The token table lists every value Ensign pins.
- The Theme Is Tokens RuleA color theme is one block of token values, mirrored in both stylesheets, and nothing else. Whatever has to differ between themes becomes a token first:
--actionand--action-inkexist so that the booking action can take a color the data does not share. A new theme is measured like a new photograph, every line of type against the worst pixel behind it, before it ships. - The One Action Rule, in any themeThe fill is Depth Cyan in Night Watch and Signal Red in Ensign, and in both it is the only thing that takes it.
Type system
Three faces, three jobs: an italic serif for what is said, a roman sans for what is read, and tracked monospace capitals for what is annotated or pressed.
Aa
Playfair Display
Italic only, one variable file from 400 to 900. Headlines at 700, large figures at 900, and short accent lines inside components.
A week aboard Moonstruck 0123456789
Aa
Barlow
400 for everything read at length: body, leads, prose links. 500 for the drawer's links. The 300 cut is loaded but not used for running text, since a light weight thins out against a near-black field.
Short sails, long afternoons 0123456789
Aa
IBM Plex Mono
The label voice, always uppercase and tracked: 400 for navigation, buttons, captions, chart furniture and calendar days; 500 for short standout labels and pin numbers.
Under sail · Book · 1 h 30 min
Six files carry all of it, served from this site (assets/fonts/, each family's license beside it); CSS may ask for nothing else, because a weight with no file is drawn at the nearest one. The families are reached through --font-display, --font-body and --font-label, whose stacks lead with a metric-matched stand-in (Georgia at 85.2% for Playfair, Arial at 94.4% for Barlow, Courier New for Plex Mono), so the swap to the webfont moves no line break.
- The Three Voices RulePlayfair is italic, Barlow is roman, and IBM Plex Mono is small capitals. A roman Playfair heading, an italic Barlow paragraph or a lowercase mono label all read as mistakes. No uppercase eyebrow sits above a heading: the heading carries the section. Uppercase strings are written in sentence case in the source and transformed by CSS, so assistive technology reads words, not letters.
- The Figures Are Readings RuleAny number a guest acts on (a rate, a length, a year, a day count) is set in Playfair italic 900 with lining tabular figures. Numbers inside running prose stay in Barlow.
Type scale
Roles are tokens, named for the job and not the value. Every size below is set live from its token.
Set sail.
A captain, and a good deal more
Open weeks at a glance
Also on the chart
Norman Island to the Baths
Clearing in and out
Every charter aboard Moonstruck is planned around the people on board, so no two weeks are alike.
The balance this charter was planned around, from its planning sheet. Yours is drawn to your own taste.
We head for the Baths for light swimming and a hike through the granite boulders, then on to Spanish Town for dinner ashore.
Connect with a knowledgeable broker who can get you on board Moonstruck right away.
per person, for the week
1 h 30 min under sail
Most popular
- The Optical Size RulePlayfair's x-height is 0.386em to Barlow's 0.460em, so the serif reads about a sixth smaller at the same pixel size. Serif running text is never set below Serif text (1.25rem), which is what sits level with 1.0625rem Barlow: Side by side: Barlow 17 · Playfair 20 · and Playfair 17, which is lost
- The Heading Leads RuleA heading takes its space from above, not below, so it binds to the block it introduces instead of floating between two.
--head-lead(1.25em) sits over a heading that follows content in the same flow;--head-trail(0.4em) sits under every heading. Both are em, so a subhead leads by more than a title. The lead only matches a heading that follows a sibling, because one that opens a container already has the container's padding over it. A definition list is a heading and its paragraph too, so it cannot sit on a gridgap, which would put a term as far from its own explanation as from the next entry. The rule Clearing in The term hugs its own explanation and keeps its distance from the entry above. On a grid gap Clearing in One gap between every child, so the term floats and reads as the tail of what came before. - The Dark Field RuleLight text on a dark field reads thinner than the same text on a light one. Body copy on the deepest fields steps up to Body on navy (1.125rem), and running text of 14px or smaller never drops below the tint's 0.66 step. Every body paragraph takes
--leading-body; a list of one-line items may open to 1.8, where the leading is the space between items. - The Plain Figures RuleFigures here are data. Playfair's default old-style 1 and 0 read as letters, so
bodysetslining-numssite-wide and anything columnar addstabular-numsbeside it. The property is not additive: a rule that setstabular-numsalone turns the old-style figures back on. $4,100 lining$4,100 old-style, never
The mark
One mark, in two inks: its own colors on a light ground, and Instrument White on the site's dark fields, with its water in two bands: a light blue grey over the sea, which is Depth Cyan Muted in Night Watch and the hull's Moonstruck Navy in Ensign.
The file is assets/Moonstruck-Logo.svg. It follows the name as it is lettered on the hulls: MOON cut out of a moon disc, then STRUCK beside it in light Jost capitals, with the two O's drawn as a pair of wayfarer sunglasses seen straight on. The cut-out letters and the lenses are real holes, which is what lets the file work as a mask. The water under the disc is two bands, as on the hull: a light blue grey, then Moonstruck Navy. The disc's navy (#1f2b5b) and the band's blue grey (#9db2cc) belong to the asset and sit outside the token set.
On a dark field the same file becomes a mask over a fill: Instrument White down to the gap above the water, the tint at its text-soft step from there, and --mark-sea for the sea. There is one file to maintain, and every ink is a token. The sea has a token of its own because the field decides it: the hull's navy is too dark to show on Night Watch (1.20:1), so there it is Depth Cyan Muted, the sea on the app icon, and Ensign takes Moonstruck Navy. The first stop sits at 75% of the height, inside the gap; the second, at 86.2%, falls inside the water, which the mask draws solid. It is not a CSS filter chain: iOS Safari does not clamp after every step, and drew the mark cyan.
background: linear-gradient(var(--cream-bright) 75%, var(--text-soft) 75% 86.2%, var(--mark-sea) 86.2%); /* the water is below 75%, the sea below 86.2% */
mask: url("Moonstruck-Logo.svg") center / contain no-repeat; /* with the -webkit- twin */
object-position: -999px 0; /* the img's own pixels, out of the box */
| Where | Height | Ink |
|---|---|---|
| Homepage nav, over the hero | 60px | Instrument White and cyan water, by mask, on the photograph |
| Homepage nav once scrolled; every inner page's nav | 46px | Instrument White and cyan water, by mask |
| Footer | 46px | Instrument White and cyan water, by mask, at 90% |
| This guide's bar | 40px | Instrument White and cyan water, by mask |
Beside it
The favicon (assets/icon.svg) sets the mark in Lamp Cream on a rounded field of Deep Water, the --ink-page that the pages' theme-color and the web manifest carry. tools/build-icons.py rasterizes that drawing into the rest: a 32px PNG, a 180px touch icon, the manifest's 192 and 512, a maskable 512 drawn smaller to sit inside Android's safe circle, and a flat black mask-icon.svg for a pinned Safari tab. Paradise Yacht Management's mark sits beside Moonstruck's in the footer at the same 46px; the Charter Smarter mark appears once, on the homepage's booking card, where it says where the booking button leads.
Grid and measure
A nine-column page grid: 1040px at most, a 32px gutter, and everything else derived from those two numbers.
Nine is chosen for the editorial 3 + 6 and 4 + 5 splits it allows; it is not a twelve-column utility grid. The grid governs page-level placement only: components subdivide freely inside their span, because the page's natural groupings (two destinations, seven days) do not divide by nine.
One column is 87px. The widths above are worked out from the tokens by tools/build-docs.py.
Margins and rhythm
| Token | Desktop | 768px and below | Use |
|---|---|---|---|
--section-x | 56px | 24px | The horizontal page margin, everywhere: sections, nav, hero and footer. |
--section-y | 56px | 64px | Default vertical section padding. |
--section-y-lg | 96px | 64px | Emphasis: availability, and the inner pages' main sections. |
--section-y-xl | 128px | 80px | The closing booking section. |
--gutter | 32px | 24px | Between columns, and between most paired blocks. |
Vertical rhythm goes lower on a phone, not higher.
Breakpoints
| Width | What changes |
|---|---|
| 1180px | The seven nav links give way to the burger and its drawer. Written 73.75em, so a visitor with larger text gets the burger sooner. |
| 1152px and up | The nine-column line exists: --grid-max plus twice --section-x, derived, not chosen. Compositions that need it engage here and no earlier, or their bases overflow and wrap. |
| 1151px and below | Two-column compositions go fluid: 5fr to 6fr with an 8% alley. |
| 1100px | The itineraries hero copy leaves the chart and sits above it. |
| 920px | The CAPTAIN360 cards drop to one column. |
| 900px, 800px | Passage plans and notes, then pilot entries, drop to one column. |
| 768px | The phone layout: margins and rhythm step down, grids go to one column, charts pan sideways, the floating bar becomes a row along the bottom. |
| 600px | The rate table turns each row into a party-size header over two season cells. |
| 480px, 430px, 380px, 360px | Small repairs: the hero headline's glass plate, the calendar to one month across, the footer address, the pictogram legend. |
- The Column Span RuleA new container takes a span token, never a hand-picked pixel width. If a block needs a width the scale does not offer, the scale is wrong: fix the tokens, not the block.
- The One Margin RuleThe horizontal page margin comes from
--section-xand nothing else. A full-bleed escape is writtencalc(-1 * var(--section-x)), so it tracks the margin. - The Shrinkable Track RuleA single-column grid is written
minmax(0, 1fr), never a bare1fr, which cannot shrink below its content. With a pannable chart inside, a bare track once ran a 390px phone page to 734px. When checking a phone layout, confirm thatinnerWidthequals the width you asked for.
Layouts
The compositions the pages are built from, drawn on the nine columns. Every split sums to nine, so edges land on the same column lines from section to section.
Blue blocks are photographs, navy blocks are charts, dark blocks are type, outlined blocks are panels. Below 1152px these go fluid, and at 768px each is a single column in source order, which is also the order a screen reader gets.
The boat: header
Six columns of headline and lede against a three-column round portrait.
The boat: feature field
Two four-column tiles with a derived one-column alley (dashed), opened and closed by full-measure plates.
CAPTAIN360
Three equal cards across the wide measure: the captain, what every rate includes, and what the captain does.
Passage plan
Four columns of chart, sticky below the nav, against five of log.
Chart index
On navy: six columns of chart against a three-column key; a chart too wide for that takes all nine, with the key beneath.
Pilot entries
Four columns of chart inset against five of text, mirrored on every second entry.
Notes
Reading the plan, and Between the anchorages: a heading and lede on four columns against a definition list on five.
Also on the chart
A three-column heading against the two cross links.
Components
Sections sit flat. Only what floats over content gets a shadow, and only contained cards get a radius.
| Depth | Shadow | On |
|---|---|---|
| Float | 0 8px 24px rgba(0,0,0,0.15) | Polaroid tiles, chart insets, the plan chart's mat. |
| Lift | 0 8px 32px rgba(0,0,0,0.2) | Wide plates, the menu drawer. |
| Control | 0 4px 18px rgba(0,0,0,0.35) | Any control on imagery: close buttons, the divider knob, the map opener. |
| Stage | 0 30px 80px rgba(0,0,0,0.5) | The photograph in the lightbox. |
Corners: none on buttons and section edges; 2px on mats, frames and the CAPTAIN360 cards; 14 to 16px on the destination and booking cards; fully round for icon-only controls and portraits.
The polaroid mat, carrying a chart inset
The boat section's mat: Night Watch wrapping the plate on three sides and deepening into a lip that carries the caption, so the caption belongs to the picture. Photographs sit square, never tilted. On the destination pages it carries a chart inset, which a photograph can replace without changing the page. This one is lifted from usvi.html and is live: drag the divider, or focus it and use the arrow keys.
Badge
A short Standout label on a square Midnight Hull plate: one vocabulary for the hero's line, the “Most popular” tag and the photo band's caption. Never a pill.
Navigation
Uppercase IBM Plex Mono at 13px, tracked. The link for where you are carries a 1px hairline 8px below the label, drawn as a pseudo-element so that it stops at the last letter; text-decoration would run on under the trailing tracking. On the homepage it follows a reading line two fifths of the way down the viewport and draws in over 0.35s; inner pages mark Destinations, their parent.
Planning pictograms
The thirteen pictograms that printed charter itineraries use, lifted as vector paths into assets/icons/tags.svg. They fill with currentColor: the tint at 0.66 on a panel, Depth Cyan on a chart field. In a log they stand alone at 24px, named by their title and an accessible label; only the legend shows names. They are the only icon set on the site besides the button glyphs.
Gauges
“How the week leans”: relative emphasis from a charter's planning sheet, on a scale ruled in quarters. These are the seven-night passage's. Each fill runs once, from the left, when the block enters view.
- Snorkeling
- Beach time
- Sailing time
Calendar days
The availability grid's seven states, with the fills and inks the homepage's stylesheet gives data-cal-day. An open day that touches a booking or a hold is a turnaround day: computed, never entered, and drawn exactly as unavailable. A day a charter can start on (five nights open from it) is a button, and every other day is a plain cell. Pointing at or focusing one previews its stay of up to seven nights; choosing it lights the stay and fills the readout under the grid with the dates, the nights and one action, an email with the dates in its subject. No price is shown there: what a week costs is the rate table's to say.
The months drawn run only as far as the blocks are maintained. A date with no block reads as open, so an unmaintained month would advertise weeks nobody has checked.
Charts
The inner pages draw real nautical charts; they do not decorate with them. One asset carries the geography, and every chart on the site is a viewBox crop of that one drawing.
This is the seven-night plan chart and the first two entries of its log, lifted from itineraries.html. Point at an entry, or tab to a pin, and its leg lights while the others fall to 28%. The satellite view is the same window, in register; the passage stays plotted on both.
-
Day 1
SaturdayCharlotte Amalie to Norman Island
1 h under sailWelcome to the Virgin Islands. After everyone is aboard, we hold a short safety briefing and set sail for Norman Island: snorkeling at the Caves, then an evening at the Willy T, the floating bar in the Bight.
-
Day 2
SundayNorman Island to The Baths, Virgin Gorda
3 h under sailWe head for the Baths for light swimming and a hike through the granite boulders, then on to Spanish Town for dinner ashore at CocoMaya.
Anatomy
| Part | How it is drawn |
|---|---|
| Field | Always Moonstruck Navy, laid with the sea plate (assets/opt/chart-sea-784.webp) at its one size, so every chart is a window onto one body of water. |
| Shoal halo, land | assets/chart/vi-land.svg referenced twice through <use>: once as a wide faint line (Instrument White at 7%, 11px), once as the land (Instrument White fill at 16% under a 1px line). Coastline from OpenStreetMap. |
| Furniture | In the label voice at chart scale: a graticule every tenth of a degree with edge labels, a four-point compass rose, a nautical-mile scale bar, a serif title block, and the line every chart carries: Not for navigation. |
| Routes | Dashed Instrument White, one passage to a chart, drawn on once through a mask. A leg is the track a boat sails: a smooth spline through waypoints in open water, turning a corner only where the boat stops. |
| Pins, marks | HTML, so they hold their size at any chart scale and take focus: a 24px Depth Cyan disc with a Midnight Hull numeral for a stop, a small ring for a place a leg passes through. |
| Satellite plate | Every chart but the itineraries hero carries one under a draggable divider. Open water in every plate is toned to Moonstruck Navy, so the sea is continuous across the divider and only land, reef and shallows change. |
x = (lon + 65.12) × 1000 y = (18.80 − lat) × 1054.35 1 nautical mile = 17.57 units
The divider
A role="slider" whose value text names the share of satellite. Only the knob and the line take a drag, never the whole plate, because on a phone the chart already pans sideways and the page has to scroll past it. Where it rests and how high the knob rides are chosen by hand per chart, to clear its pins and labels.
| Input | Effect |
|---|---|
| Arrow keys | 2%; 10% with Shift |
| Page Up, Page Down | 10% |
| Home, End | Satellite only, chart only |
| Double-click the knob | Back to rest |
- The Chart Truth RuleChart geometry is data, never drawn by eye: coastlines come from the asset, positions from coordinates, the scale bar from the projection. A pin is where the boat lies (the mooring field, not the beach or the town), and the generator's
check_afloat()names any pin or track that touches land. A satellite plate is emitted only at the chart's exactviewBox, so imagery is never shown out of register.
Overlays and dialogs
Four overlays, one behavior. Each is a real dialog, and each gives the keyboard back exactly where it took it.
| Dialog | Field | Opens with focus on | Also |
|---|---|---|---|
| Photo viewer (homepage) | Midnight Hull at 94% | Close | Arrow keys and a horizontal swipe turn the photo; a counter chip; a polite “Photo 7 of 17”. |
| Plate zoom (homepage) | Midnight Hull at 96% | Close | Click toggles fit and enlarged; never past the source's own pixels; its hint sits on a Midnight Hull plate so it survives white cabinetry. |
| Menu drawer (every page) | Night Watch, from the right, up to 20rem, over a Midnight Hull backdrop at 60%. The section links, an On the chart group, then Book, the same on every page | Its first link | The burger stays on screen as the close control and is part of the focus trap. A tap on the backdrop closes it too. |
| Anchorage map (inner pages) | Midnight Hull at 96% | Close | Dormant until a MapKit token is set. Until then the opener is a link to Apple Maps. |
All four share this contract: role="dialog" and aria-modal; the element that had focus is remembered; the page stops scrolling behind; Tab wraps inside; Escape closes; focus returns to where it came from. A closed overlay is visibility: hidden as well as transparent, because pointer-events: none alone leaves its links in the tab order.
The implementations are openOverlay(), closeOverlay() and onOverlayKeys() on the homepage, and toggleMenu(), onMenuKeys(), openMap() and mapKeys() in pages.js; see the JavaScript reference.
Motion
Slow and calm. Things arrive once, over most of a second, on one soft curve, and then stay put.
--ease-reveal: cubic-bezier(0.22, 0.61, 0.36, 1)
| What moves | How | Under reduced motion |
|---|---|---|
| Hero photograph | Fades in once loaded, 0.6s, the shortest reveal because its length counts toward load time; none if ready for the first frame | 0.2s fade |
Reveal (.reveal) | 36px rise and fade, 0.8s | 0.4s fade, no movement |
| Polaroid plates, slide-ins | From 70px or 90px to the side, 0.9 to 1s | 0.4s fade |
| Gallery stage | Sharpens from gray and blurred, 1.2s; it does not move | None |
| A chart's route | Draws itself on through a mask, 2.6s; pins follow 0.14s apart | Already drawn |
| Satellite plate | Wipes in from the east, 1.5s, after the route has drawn | Already at rest |
| Gauges | Fill from the left, 1.2s, 0.1s apart | Already full |
| Nav hairline | Draws in from the left, 0.35s | Appears at once |
| Buttons, calendar days | Color swaps, 0.2 to 0.25s | Unchanged |
| Rate table mist | A 34s drift | Still |
| Footer weather | Changes harbor every ten seconds behind a 0.5s fade, only while the footer is on screen | Holds on the first harbor |
Every animation has its counterpart in the prefers-reduced-motion block at the foot of each stylesheet, and a new one needs a line there. Adding ?static to an inner page's URL resolves every entrance at once, for screenshots and print.
CSS tokens
Every custom property on :root, with its value read from the source.
The set is written twice: at the top of assets/home.css, which tools/minify.py inlines into index.html, and at the top of assets/pages.css. The two must agree, and tools/build-docs.py reports any that do not. Descriptions come from the @token tags in pages.css.
| Token | Value | In Ensign | At 768px and below | What it is for |
|---|---|---|---|---|
| Color | ||||
--ink-page | #040c16 | #1d3557 | Deep Water. The page field: the reading sections, and what shows behind an overscroll. | |
--ink-panel | #07101d | #234062 | Night Watch. The lifted step: panels, cards, the navigation bar, photo mats. | |
--ink-well | #010509 | #132339 | Midnight Hull. The recessed step: wells, the footer, badges, and what shows behind a photograph while it loads. | |
--ink-page-rgb | 4 12 22 | 9 16 27 | Deep Water as channels: the wash over a photographic section, and the hero haze. | |
--ink-well-rgb | 1 5 9 | 19 35 57 | Midnight Hull as channels, for overlays (0.72 to 0.96). | |
--chart-navy | #061e45 | Moonstruck Navy, kept for the chart field alone. Asset-bound: every satellite plate has its open water toned to it. | ||
--chart-navy-rgb | 6 30 69 | Moonstruck Navy as channels, for the legibility halo on type set over a chart. | ||
--cream | #f5efd9 | #f1faee | Lamp Cream. The warm cream: headlines and every large figure. | |
--cream-bright | #eef3ff | #f1faee | Instrument White. The cool cream: body copy, lists and UI text. | |
--tint-rgb | 200 225 255 | 241 250 238 | The tint as channels. One hue carries every rule and every secondary voice (0.06 to 0.78). | |
--text-soft | rgb(200 225 255 / 0.78) | rgb(241 250 238 / 0.82) | The tint at 0.78. Supporting text and section leads. | |
--text-mute | rgb(200 225 255 / 0.66) | rgb(241 250 238 / 0.7) | The tint at 0.66. Muted small print and the tracked label voice. | |
--text-faint | rgb(200 225 255 / 0.55) | rgb(241 250 238 / 0.6) | The tint at 0.55. The text floor: a booked or turnaround day in the calendar. | |
--line-rule | rgb(200 225 255 / 0.12) | rgb(241 250 238 / 0.16) | The tint at 0.12. Hairlines between rows, facts and notes. | |
--line-soft | rgb(200 225 255 / 0.06) | rgb(241 250 238 / 0.08) | The tint at 0.06. The quietest edge, where a rule would otherwise read as a boundary. | |
--amber | #faa80f | Anchor Light. Held in reserve; the booking action takes Depth Cyan. | ||
--amber-rgb | 250 168 15 | Anchor Light as channels, for its washes and borders. | ||
--cyan | #00c8e8 | #a8dadc | Depth Cyan. Data and nothing else: chart pins, key numerals, open days, the focus ring. | |
--cyan-rgb | 0 200 232 | 168 218 220 | Depth Cyan as channels: an open day's wash, the on-hold stripe, the rate table's glow. | |
--cyan-muted | #00b2d8 | #8abec9 | The darker cyan step. Text selection and gauge fills. | |
--action | #00c8e8 | #e63946 | The fill of the one booking action. Depth Cyan here; a theme may give it a color of its own (Ensign: Signal Red). | |
--action-ink | #040c16 | #09101b | The label on that fill, solved to clear 4.5:1 on it. Deep Water here. | |
--mark-sea | #00b2d8 | #061e45 | The sea at the foot of the mark, under its blue grey band. Depth Cyan Muted here; Ensign takes the hull's own Moonstruck Navy. | |
--rust | #c0572a | #e63946 | Rust. A stale reading. It measures 4.34:1 on the page field, so it is a wash and a border, never type. | |
--rust-rgb | 192 87 42 | 230 57 70 | Rust as channels, for that wash. | |
--mist | rgb(200 225 255 / 0.06) | rgb(69 123 157 / 0.25) | The cyan haze inside the rate table. Homepage only. | |
--mist-clear | rgb(200 225 255 / 0) | rgb(69 123 157 / 0) | The same color at zero alpha, so the haze's gradients fade without graying. Homepage only. | |
--nav-glass | rgb(4 12 22 / 0.82) | rgb(29 53 87 / 0.82) | The plate behind the hero headline on a phone. | |
--nav-glass-rgb | 4 12 22 | 29 53 87 | The same as channels. | |
| Type | ||||
--font-display | 'Playfair Display', 'Playfair Fallback', Georgia, serif | Playfair Display, then its metric-matched fallback. Set italic: headings, and every large figure. | ||
--font-body | 'Barlow', 'Barlow Fallback', system-ui, sans-serif | Barlow, then its metric-matched fallback. Everything you read: paragraphs, leads, links in prose. | ||
--font-label | 'IBM Plex Mono', 'Plex Mono Fallback', ui-monospace, monospace | IBM Plex Mono, then its metric-matched fallback. The tracked uppercase label voice: navigation, buttons, captions, chart furniture, calendar days. | ||
--type-display | clamp(3rem, 8vw, 5.75rem) | Display: the homepage hero headline only. | ||
--type-headline | clamp(2.375rem, 9vw, 3.625rem) | Headline: section titles. | ||
--type-subhead | clamp(1.875rem, 7vw, 2.75rem) | Subhead: a second heading inside a section. | ||
--type-title-lg | 2.125rem | Title L: closing index headings and the homepage's wide plate names. | ||
--type-title | 1.75rem | Title: every card and entry heading. | ||
--type-title-sm | 1.5rem | Title S: small titles, and every tile title on a phone. | ||
--type-lead | 1.1875rem | Lead: the one paragraph that introduces a section, set in the body face. | ||
--type-serif | 1.25rem | Serif text: serif running text inside a component. | ||
--type-body | 1.0625rem | Body: Barlow 400 paragraphs. | ||
--type-body-navy | 1.125rem | Body on navy: one step up, because light text on a dark field reads smaller. The closing paragraph; the homepage's destination blurbs. | ||
--type-small | 0.875rem | Small: secondary facts and credits. | ||
--type-label | 0.75rem | Label: the floor for tracked uppercase annotations. | ||
--leading-serif | 1.5 | Line height for serif running text. | ||
--leading-body | 1.65 | Line height for body copy. | ||
--leading-title | 1.15 | Line height for titles. | ||
--head-lead | 1.25em | Space over a heading that follows content in the same flow (The Heading Leads Rule). | ||
--head-trail | 0.4em | Space under a heading, binding it to what it introduces. | ||
| Motion | ||||
--ease-reveal | cubic-bezier(0.22, 0.61, 0.36, 1) | The one easing curve for entrances: soft, decelerating. | ||
| Washes | ||||
--wash-drawing | 0.45 | 0.32 | The opacity of the layout drawing behind the homepage's layout section, solved so the lead clears 4.5:1 over its brightest line. Homepage only. | |
| Grid and measure | ||||
--grid-cols | 9 | Columns in the page grid. Informational; the spans below are what rules use. Homepage only. home.css only | ||
--grid-max | 1040px | Full width of the nine-column page grid. | ||
--gutter | 32px | 24px | Gutter between columns. | |
--col | calc((var(--grid-max) - 8 * var(--gutter)) / 9) | One column, derived from the two above. | ||
--span-3 | calc(3 * var(--col) + 2 * var(--gutter)) | Three columns and their two gutters. Spans 4 to 8 follow the same pattern. | ||
--span-4 | calc(4 * var(--col) + 3 * var(--gutter)) | Four columns. | ||
--span-5 | calc(5 * var(--col) + 4 * var(--gutter)) | Five columns: the measure for a lede. | ||
--span-6 | calc(6 * var(--col) + 5 * var(--gutter)) | Six columns. | ||
--span-7 | calc(7 * var(--col) + 6 * var(--gutter)) | Seven columns. | ||
--span-8 | calc(8 * var(--col) + 7 * var(--gutter)) | Eight columns. | ||
--measure-read | var(--span-6) | Reading container (span 6). | ||
--measure-mid | var(--span-7) | Mid container (span 7). | ||
--measure-wide | var(--span-8) | Wide container (span 8). | ||
--measure-full | var(--grid-max) | The whole grid (span 9). | ||
| Spacing | ||||
--section-x | 56px | 24px | Horizontal page margin. The only source of it: anything that breaks the margin computes from this. | |
--section-y | 56px | 64px | Default vertical section padding. | |
--section-y-lg | 96px | 64px | Emphasis step: the homepage's availability section, and the main sections of these pages. | |
--section-y-xl | 128px | 80px | Emphasis step for the closing booking section. | |
--nav-h | 89px | 70px | Height of the fixed nav bar: hero top padding, sticky offsets and scroll margins clear it. These pages only. pages.css only | |
Component properties
A few custom properties are scoped to a component. They carry a measurement from the generator or from script, never a design decision.
| Property | Set by | Meaning |
|---|---|---|
--split | pages.js | The chart / satellite divider's position, in percent from the left. |
--knob-y, --split-delay | the generator, inline | How high the knob rides, and how long the satellite's entrance waits for the route. |
--chart-min | the generator, inline | The width a chart keeps on a phone, where it pans. |
--sea | the generator, inline | Where a chart's window falls on the one sea plate. |
--i | the generator, inline | A pin's place in the entrance order. |
--v, --nm | the generator, inline | A gauge's reading; one nautical mile as a share of an inset's width. |
--mat, --plate-ar | the stylesheets; inline per tile | The polaroid mat's width, and a plate's proportion. |
--cta-vv | both scripts | How far a phone's toolbar or keyboard has pushed the visible bottom edge up. |
--link-hover, --hero-copy-right | index.html | The link hover ink for a surface; where the hero headline's first line ends, for the haze. |
Code reference
The source comments are the documentation. The reference pages are generated from them.
| Page | Generated from |
|---|---|
| JavaScript reference | The JSDoc in assets/home.js and assets/pages.js: every function, its parameters, the events it listens for, and the shapes of the data it handles. |
| CSS reference | The doc comments in assets/home.css and assets/pages.css: every section, selector, modifier, state, script hook, custom property and breakpoint. |
ARCHITECTURE.md | Written by hand: how the site is put together, where its data comes from, and what to touch when something has to change. |
DESIGN.md | Written by hand: the reasoning behind every rule on this page. |
After changing a doc comment, a token or this page, run the build and commit what it writes. tools/build-docs.py alone rewrites just docs/ and this page.
python3 tools/build.py # the whole build, in the order that works python3 tools/build.py --check # writes nothing; exits 1 if anything is out of date
An internal page, marked noindex and linked only from About this site. Values here are written by tools/build-docs.py.

