CSS reference
Generated from the doc comments in the site's two stylesheets. The homepage's is assets/home.css, inlined into index.html by tools/minify.py and holds only what an inline style cannot say; the inner pages share assets/pages.css. Each entry is tagged with what it is: a rule, a modifier or state of one, a hook that script sets, a component-scoped custom property (var), or a breakpoint.
Tokens
The custom properties on :root. assets/home.css and assets/pages.css each carry the set and must agree; the values here are read from the source, and the descriptions 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 | |
assets/home.css
This file is the source and is never served. tools/minify.py copies it, with the comments and spacing taken out, into the one <style> block in index.html, so the first paint does not wait for a second request (ARCHITECTURE.md's *Loading* section has the reasoning). Edit this file, run that script, and commit both. Never write a closing style tag in a comment here: the HTML parser would end the element there once this is inlined.
Most of the homepage's styling is not here. The page began as an export from a design tool, and each element carries its own style attribute. This block holds what an inline style cannot say: the tokens, media queries, pseudo-elements, hover and focus states, keyframes, the elements that assets/home.js writes (thumbnails, calendar days), and rules keyed on the state that home.js sets in classes and data attributes. Because these rules have to beat an inline style, many of them carry !important; that is the mechanism here, not a smell.
Elements are addressed by data attributes (data-tile, data-rate-row, data-hero-stage), not classes: the attribute names the element's job on the page. Classes are kept for the few things script toggles (.scrolled, .menu-open, .active).
The tokens in :root are the site's one set. assets/pages.css mirrors them for the inner pages and documents each one; the two blocks must be kept in step (tools/build-docs.py reports any difference).
Doc comments use the tags described at the top of assets/pages.css (@section, @rule, @hook, @var,
Typefaces
The three families, served from this site (assets/fonts/, with each family's SIL Open Font License beside it), so a page load contacts nobody else for its type. They are the Latin subsets Google Fonts served, which is every file any page ever asked for; a character outside that range falls to the stand-ins below. Playfair Display is one variable file, italic only, because the serif is only ever italic (The Three Voices Rule). font-display is swap, and the stand-ins are metric-matched, so text paints at once and the swap does not re-wrap it. The files never change in place (a new cut gets a new name), which is what lets _headers cache them for a year.
| rule | @font-face 'Playfair Display' | Italic, 400 to 900 in one variable file. Headlines and figures. |
| rule | @font-face 'Barlow' | 300, 400 and 500. Everything read at length. |
| rule | @font-face 'IBM Plex Mono' | 400 and 500. The label voice. |
See DESIGN.md, Typography
Font fallbacks
Metric-matched stand-ins, measured against the webfonts, so the swap does not re-wrap the page.
| rule | @font-face 'Playfair Fallback' | Georgia at 85.2%, measured against Playfair italic 700, the headline weight. |
| rule | @font-face 'Barlow Fallback' | Arial at 94.4%. Body copy. |
| rule | @font-face 'Plex Mono Fallback' | Courier New at 100%. The label voice. |
Base
The reset, link colors per surface, focus, and the accessibility helpers.
| rule | * | Border-box sizing and no default margins. |
| rule | body | Deep Water behind everything, so an overscroll never flashes white; lining figures site-wide (The Plain Figures Rule). |
| rule | h1 to h4; p, li | Balanced headings, and paragraphs that do not end on a lone word. |
| rule | a | the tint at 0.78, turning to --link-hover. |
| var | --link-hover | The hover color for links on a surface. Unset, it is Instrument White. |
| rule | [data-dest-link], the footer's mail link | Small targets keep their drawn size and grow an invisible 44px hit area from an ::after with a negative inset (The Hit Area Rule). |
| rule | :focus-visible | The site-wide ring: 3px Depth Cyan, 3px offset; Instrument White on the ink surfaces and over the hero. |
| rule | [hidden] | Hidden means not displayed, whatever display an inline style asks for: the menu, the two dialogs and the open windows all wait behind this attribute. |
| rule | [data-action]:hover | Call, Email and Book (under the calendar, in the floating bar, in the menu) turn Instrument White. |
| rule | [data-theme-pick] | A button of the footer's theme switch, and its enlarged hit area. The switch ships hidden; home.js shows it. |
| state | [data-theme-pick][aria-pressed="true"] | The theme that is on: Instrument White over a hairline, so the state is not carried by color alone. |
| rule | [data-theme-label] | The switch's caption, "Theme", in the micro-label role: 10px, tracked wider, in the tint's mute step, with a hairline after it. In the buttons' own voice it read as a third choice. |
| rule | [data-outline]:hover | The outlined buttons on navy fields draw their border in full and take a 10% Instrument White wash. |
| rule | .skip-link | "Skip to content", off screen until it takes focus. |
| rule | .vh | Visually hidden, still read aloud: calendar day states, photo counts. |
Heading rhythm
The Heading Leads Rule: a heading takes its space from above, not below, so it binds to the block it introduces instead of floating between two. Written with :where(), so it carries no specificity and every authored heading margin on the page still wins; it is the floor, not an override.
The lead is only on a heading that follows a sibling in the same flow. A heading that opens a container already has the container's padding over it, and adding to that would double-count. In block flow the lead collapses with the previous element's own bottom margin and the larger wins, so it raises a short gap and leaves a long one alone.
| rule | :where(h1..h4) | The trail: 0.4em under every heading. |
| rule | :where(* + h2..h4) | The lead: 1.25em over a heading that follows content. |
State hooks
Small rules keyed on what assets/home.js writes. Each value is the string "yes" or "no".
| hook | [data-weather][data-wx-faded="yes"] | The footer weather is between two readings. |
| hook | [data-hero-img][data-loaded="yes"] | The hero photograph has loaded and fades in over 0.6s, the short end of the reveal range: Chrome times the largest paint at a fade's end, so each second here is a second of LCP. Set by the image's own onload attribute, so a photograph already there is marked before the first frame. |
| hook | [data-hero-img][data-fade="no"] | The photograph loaded before the browser had reported a first paint (cached, or preloaded in time), so it arrives with the page and does not fade. The same onload attribute says so, from the paint timeline, because a transition starts from the last computed style, not the last painted one. Important, to outrank reduced motion's 0.2s. |
| hook | [data-bg-wait][data-bg-on="yes"] | A section's background image is wanted: the section is within two screens or so. Until then the image is withheld, so the five of them, a megabyte together, are not fetched with the page. Only where script runs: without script, or where (scripting: enabled) is unknown, they load with the page, and a printed page gets them all. |
| hook | [data-float-cta][data-cta-shown="yes"] | The floating Call / Email / Book bar is shown. Hidden, it is visibility: hidden as well as transparent, because pointer-events alone leaves its links in the tab order. |
| rule | ::selection, html | Depth Cyan Muted selection, and a scrollbar in the palette. |
| hook | html[data-settled="yes"] | The page has loaded, and in-page links scroll smoothly from here on. Not before: a page opened on a fragment is scrolled by the browser as soon as it finds the target, and a smooth scroll goes on chasing where the target was before the calendar was written above it. |
| rule | section[id], #book, #availability | Anchored sections stop clear of the fixed nav: 89px, 58px on a phone. |
Phone layout
One block of overrides per breakpoint, each rule beating an inline desktop style, which is why they are all !important. At 768px: every grid goes to one column, the gallery stage runs edge to edge, the floating bar becomes a full-width row along the bottom, and the footer stacks and centers. The hero has a phone shape of its own; it is in the Hero section below.
| breakpoint | 768px | The phone layout. |
| breakpoint | 430px | The calendar drops from two months across to one. |
| var | --cta-vv | How far a phone's toolbar or keyboard has pushed the visible bottom edge up. Written by home.js. |
Reveal
Slow, soft, once. A block starts low and transparent and rises over 0.8s when home.js marks it as entered. The variants are further down: the slide-ins, the polaroid plates, and the gallery stage, which sharpens from a blur instead of moving.
Entrances fail open. Nothing is hidden unless the root element carries data-entrances, which the one-line script in the head sets and takes off again if home.js has not started within four seconds or fails to load. So a visitor without script, or behind a blocker, reads the whole page without its entrances. The attribute is matched inside :where(), so these rules keep the specificity they had.
| rule | .reveal | The resting state, while entrances are armed. |
| hook | [data-entrances] | On the root element: "armed" by the head script, "live" once home.js is watching. Absent, nothing waits. |
| hook | .reveal[data-revealed="yes"] | Entered. Written once by the reveal observer in home.js, never removed. |
Hero
Hero photo (Moonstruck under sail, from overhead), anchored 75% across and 54% down: object-position pins that point of the plate to the same point of the box, so the boat holds the right of the hero at every size while the sides and the empty water crop away. The headline reads on the left, over open water, under a haze in the page field that runs from the left edge to the end of the headline's first line (--hero-copy-right) and is gone by 62vw. Under 600px the copy leaves the photograph, so the haze is dropped and the band's cut is pinned at 50%, which puts the boat in its middle.
The stage has a floor, not a height: 80vh and never under 500px (67vh and 470px on a tablet, 62vh between 520px and 760px held upright). Where a short window leaves too little room for the headline and the badge, the stage grows to hold them, by up to 78px, and the plate is drawn wider than --hero-plate-w allows for, so the bow lands up to 12px further left, inside the copy's 24px margin.
The copy stops short of the boat rather than trusting the eye: --hero-boat-left works out where the bow sits in the box from --hero-plate-w (which the sizes attribute in index.html repeats) and the boat's reach of 5.2% of the plate's width. Change either number and remeasure the photograph.
| var | --hero-copy-right | Where the headline's first line ends: the page margin plus 6.8 times the display size. |
| var | --hero-plate-w | How wide cover draws the 3:1 plate over the stage. |
| var | --hero-boat-left | Where the boat's bow falls in the box; the copy ends 24px short of it. |
| var | --hero-band | The height of the photograph's band on a phone. |
| rule | [data-hero-haze] | The haze: Deep Water at 90%, 72% at --hero-copy-right, clear by 62vw. |
| rule | [data-hero-img] | The photograph, anchored on the boat: 75% across, 54% down; 50% across under 600px. |
| breakpoint | 600px | The phone shape: the photograph becomes a band, centred on the boat, and the copy moves below it onto the page field. |
| breakpoint | portrait, 769px and up | A tablet held upright gets a shorter stage floor: 62vh, between 520px and 760px. |
See DESIGN.md, Hero Haze
The boat: header
A three-column round portrait against six columns of headline and lede, each centered on the other. The portrait leads on the left, which turns the hero's arrangement around: there the type is on the left and the boat on the right, and the eye crosses the page the other way as it comes down. The order is set here rather than in the markup, because on a phone the portrait belongs under the headline it illustrates, which is the order the source is written in. The nine columns reach full size at 1152px; under that the same split goes fluid.
| breakpoint | 769px | The portrait moves to the left of the copy. |
| breakpoint | 769px to 1151px | The fluid split. The portrait is three columns of the width there is, and gives way first when the headline needs the room. |
| breakpoint | 1152px | Derived, not chosen: --grid-max plus twice --section-x. |
CAPTAIN360
The cards carry a Deep Water panel: the tint at 0.66 cannot clear 4.5:1 on a photographic field at any wash alpha. Measured, not guessed. Three equal cards: the captain, who is still to be named, then what the program puts in every rate and what the captain does.
| rule | [data-crew-grid] | The row. position and z-index lift it above the section's Deep Water wash. |
| rule | [data-crew-grid] > div | A flat Deep Water panel, 2px radius, no shadow. |
| rule | [data-crew-photo] | The round portrait, and the disc that stands in for one not taken yet. |
| rule | [data-crew-soon] | The waiting disc: Night Watch under an inset tint ring, its label in the tint at 0.66 (6.6:1). |
| breakpoint | 920px | One column, capped at 440px. |
See DESIGN.md, The Photographic Field Rule
The sailing school
ASA instruction, on the sea field the destinations use. Two columns: what it is and how to ask, then the four ASA courses taught aboard as a ruled list, number first in the label voice.
| rule | [data-asa-grid] | The two columns. One under 768px (set with the other phone rules above). |
| rule | [data-asa-courses] | The list: a hairline over each course, and one under the last. |
| rule | [data-asa-courses] > div | One course: its number in a fixed column, its name beside it. |
| rule | [data-asa-courses] dt | The number, in the label voice, in Depth Cyan. |
| rule | [data-asa-courses] dd | The course's name, in the serif. |
| rule | [data-asa-logo] | The link over the list that carries the American Sailing Association's logo to its site. It is the size of the logo, so the focus ring hugs the mark and not the column. |
| rule | [data-asa-logo] img | The logo, 46px tall like the footer's marks. Its white-text variant in ASA's own colors: the red is theirs, so it is not recolored to Instrument White by mask. |
Gallery and plate zoom
The stage, the viewer and the zoom dialog carry their own inline styles. These are the parts that change.
| rule | [data-thumbs] > button | A thumbnail, written by home.js: half strength, with a clear 3px foot. |
| state | [data-thumbs] > button[aria-current="true"] | The photograph on the stage: full strength, and the foot in --accent. |
| rule | [data-zoom-img] | The plate in the zoom dialog, fitted whole inside the window: a window-sized box, with object-fit: contain doing the letterboxing. |
| hook | [data-zoomed="yes"] > [data-zoom-img] | Enlarged: 260% of the window, but never past the file's own pixels. |
| var | --zoom-natural | The file's width, which home.js reads from its name (-2052.webp). |
Entrances
The reveal's variants. All of them key on data-revealed="yes", and all of them are undone in the reduced-motion block at the foot of this stylesheet.
| rule | [data-gallery-stage].reveal | The gallery stage does not move: it sharpens from gray and blurred over 1.2s. |
| rule | [data-slide-in] | A block that arrives from the left or the right, 90px, over 1s. |
| rule | [data-polaroid] | A boat plate arriving from 70px away. |
| rule | [data-layout-zoom] | The magnifier badge on the layout drawings, which fades in half a second after its drawing. |
The boat: feature field
A continuous two-up field of captioned plates between two full-measure plates. The alley is derived: --measure-full - 2 * --span-4 = 152px, so the tiles sit on column lines 1-4 and 6-9 and the wide plates run the full nine.
| rule | [data-boat-field] | The grid: fluid halves with a 14% alley, exact spans from 1152px. |
| rule | [data-tile] | The polaroid: a Night Watch mat carrying the plate, with the caption printed on its lip. |
| modifier | [data-tile="wide"] | A full-measure plate: a wider mat and the Lift shadow. |
| modifier | [data-tile="right"] | A right-hand tile: it arrives from the right, a beat after its neighbor. |
| var | --mat | The mat's width: 16px on tiles, 22px on wide plates, 14px on a phone. |
| var | --plate-ar | The plate's proportion, set inline per tile: 16/9, 4/3 or 8/5. |
| rule | [data-plate] > img | The photograph. A role="button" that opens the plate zoom, hence the zoom-in cursor. |
| rule | [data-zoom-hint] | The round magnifier badge on a plate's corner, darkening on hover and focus. |
| rule | [data-lip] | The lip: title and description read as one unit, tight, with the air above them. |
| rule | [data-lip] p | The description. It runs to a couple of sentences, which is a paragraph and so the sans, not the serif accent line the titles take (DESIGN.md, Serif text). |
| breakpoint | 1152px | Exact spans; the wide lip splits three columns of title against the rest. |
| breakpoint | 768px | One column; every plate arrives from the left; wide plates go 4:3. |
See DESIGN.md, Layout; Shapes
Rate table
A rate table pinned to the chartroom wall: party size down the side, season across the top. It is built from ARIA table roles on a CSS grid, so that at 600px a row can become a header over two cells. Another party size is another data-rate-row.
| rule | [data-rate-table] | The Deep Water panel. isolation keeps its two pseudo-element layers behind its content. |
| rule | [data-rate-table]::before | The mist: four soft radial gradients, blurred, drifting on a 34s loop (rateMist). Stilled under reduced motion, and held while the table is off screen. |
| hook | [data-rate-table][data-mist="on"] | The table is on screen, so the mist drifts. Written by home.js; "off", or absent, holds it where it is. |
| rule | [data-rate-table]::after | A fine paper grain from an inline SVG turbulence filter, multiplied at 16%. |
| rule | [data-rate-head], [data-rate-row] | One grid for the header and every row: the week, the boat's rate, the rate per person. |
| rule | [data-rate-label] | Season labels in the label voice, in --accent-deep with the tint at 0.66 as the fallback. |
| rule | [data-rate-pp] | The per-person figure: Playfair 48px, lining tabular figures. Title large (34px) at 600px and below, capped by the column's width on the narrowest phones and under larger browser text. |
| rule | [data-rate-total] | The weekly total, in Barlow at Body size with tabular figures. Small (14px) at 600px and below. |
| rule | [data-rate-inline] | A season label repeated inside each cell. Hidden until the header row goes at 600px. |
| rule | [data-rate-foot] | The footnote under the table, in serif text. |
| rule | [data-rate-terms] | "Included in every rate" and "Not included", four columns against the rest. |
| breakpoint | 600px | Each row becomes its party-size header over two season cells that carry their own label. The header row is clipped, not removed, so the table keeps its column headers for a screen reader. |
See DESIGN.md, Rate Table
Calendar and booking actions
The floating bar hides over the calendar (it would cover the third column below ~1300px), so the same three actions sit under the grid. One Book button in view still holds.
home.js writes the days and gives each its status in data-cal-day; the colors are here. Every numeral clears 4.5:1 on Deep Water, and no status is carried by color alone: each day also says it in hidden text.
| rule | [data-cal-day] | A day: a square, its numeral centered, answering a change of color in 0.2s. A day that has passed keeps this rule alone: a quiet numeral and no field. |
| modifier | [data-cal-day="open"] | Depth Cyan. |
| modifier | [data-cal-day="booked"] | A Night Watch panel and a struck-through numeral. |
| modifier | [data-cal-day="hold"] | A stripe, so that a hold does not rely on its color. |
| modifier | [data-cal-day="unavailable"], [data-cal-day="turnaround"] | A Midnight Hull well. A turnaround day is an open day beside a charter, kept clear for the crew. |
| hook | [data-cal-day][data-lit="yes"] | The day belongs to the stay that is chosen: Depth Cyan, Deep Water numeral. |
| hook | [data-cal-day][data-lit="maybe"] | The day belongs to a stay being pointed at or focused: Depth Cyan at 0.35 under a full hairline, Instrument White numeral: 8.3:1 in Night Watch and 5.0:1 in Ensign, whose lighter field is what sets the alpha. |
| rule | button[data-cal-day] | A day a charter can start on is a button; every other day is a plain cell. Its focus ring is Instrument White and sits inside the cell, because Depth Cyan cannot be seen on a lit day. |
| rule | [data-cal-readout] | What is chosen, and the one action: a Midnight Hull bar that stays at the foot of the screen while the calendar is in view, so it is never a scroll away from the day just chosen. It floats over the grid, so it takes the Float shadow. No price appears in it. |
| hook | [data-cal-readout][data-chosen="yes"] | Dates are chosen: the bar shows them, the nights, the email action and Clear, in place of the prompt. Written by home.js. |
| hook | [data-cal-grid][data-more="no"] | At 430px and below all but the first three months are folded away, and the button after the grid unfolds them. Written by home.js. |
| rule | [data-cal-actions] | Call, Email and Book under the grid; a full-bleed row on a phone. |
| rule | [data-cal-grid] | The months: three across, two at 768px, one at 430px (see Phone layout). |
| rule | [data-book-grid] | The closing section: copy and actions set left, the bow photograph in the right four columns; one centered column on a phone, photograph first. |
| rule | [data-book-actions] > a, [data-book-primary] > a | The closing section's buttons share a 236px minimum width, so no action reads as the runt. The primary comes first, alone; Email and the phone number follow as ghosts; the broker list is a line of text under them ([data-book-brokers]), because it leaves the site and is not the page's action. |
See DESIGN.md, The Calendar Legibility Rule
Tokens
The site's one set of custom properties. Each is described in assets/pages.css, which mirrors this block for the inner pages; the comments below record why a value is what it is. Four exist only here: --ink-page-rgb, --mist, --mist-clear and --grid-cols.
| var | --accent | Set on the page wrapper, not here (Depth Cyan): the foot of the gallery's current thumbnail. |
| var | --accent-deep | Set beside it (the tint at 0.78): crew roles and the rate table's season labels. |
| breakpoint | 768px | Margins, vertical rhythm and gutter step down together. |
The tokens themselves are in the table above.
See DESIGN.md, Colors; Typography; Layout
Themes
The tokens above are the site's first theme, Night Watch. Ensign is the second: the same tokens re-pinned on a five-color scheme, described in the same section of assets/pages.css, which this block mirrors by hand. A theme is token values and nothing else, and no rule here knows which one is on.
| hook | [data-theme="ensign"] | Ensign, for the element that carries it and everything inside it. On the root element it themes the page: set before first paint by the inline script in the head, from the visitor's stored choice, and by home.js when the footer's switch is pressed. On any other element it themes that element alone, which is how the style guide draws Ensign on a Night Watch page. No attribute is Night Watch. |
See DESIGN.md, Themes
Reduced motion
Every animation's counterpart. Nothing slides, flies or sharpens: entrances become a 0.4s fade, the mist holds still, the nav's hairline appears at once. The weather widget's rotation is stopped in home.js. A new animation anywhere above needs a line here.
Without script
The page is ordinary HTML, so it reads without home.js; these rules undo the few states that wait for it. The hero photograph is shown. The calendar says to call or email (its grid is computed in script), and the footer weather, which would never load, is removed. Entrances need no line here: they are armed by script, so without it they were never hidden (see Reveal).
assets/pages.css
The markup it styles is written by tools/chart-pages.py; the behavior it responds to comes from assets/pages.js. The homepage does not load this file: index.html carries its own styles inline, and the two share one set of tokens. The :root block below mirrors index.html's, and the two must be kept in step (tools/build-docs.py reports any token that differs).
How the file is organized: tokens, font fallbacks and base rules first, then the shared voice and components (buttons, nav, reveal, charts), then one block per page section in page order, then the footer and floating bar, and last the global switches (reduced motion, ?static).
Conventions:
- Color, type size, measure and section spacing come from tokens. A raw hex or a hand-picked width in a rule is a bug (
DESIGN.md: The Column Span Rule, The One Margin Rule). - Every section is one of the three ink steps; Moonstruck Navy is kept for the chart field and the sea plate (The One Field Rule, The Chart Field Rule).
- State is carried by classes and data attributes that
pages.jssets (.is-in,.active,.split-on,.bg-on,data-open,data-shown,data-active). Script never writes a color or a transition. - A single-column grid track is minmax(0, 1fr), never a bare 1fr (The Shrinkable Track Rule).
- Every animation has a counterpart in the reduced-motion block at the foot of the file.
Doc comments: a block like this one documents what follows it. Tags, modeled on JSDoc:
@section Name opens a section of the reference; prose after it describes the section @token --name a global custom property set on :root @var --name a custom property scoped to a component, set inline by the generator or by script @rule selector what a selector, or a group of them, is for @modifier selector a variant of the rule above it @state selector a state of the rule above it, and what puts it there @hook selector a class or attribute that script sets or reads @breakpoint width what changes at a width @see where the reasoning lives, usually a named rule in DESIGN.md
tools/build-docs.py reads these into docs/css.html and into the style guide's token table. Plain comments beside a declaration are left as they are: they explain one value, in place.
Tokens
Every color, type size, measure and spacing step on the inner pages. The palette is three ink steps (Deep Water, Night Watch, Midnight Hull), two creams (Lamp Cream, Instrument White), one blue-white tint at documented alphas, and two accents: Depth Cyan for the action and the data, and Anchor Light in reserve. Moonstruck Navy is kept for the chart field. The -rgb tokens are channel triplets for translucent steps: rgb(var(--tint-rgb) / 0.72).
| breakpoint | 768px | Margins, vertical rhythm, gutter and nav height step down together. Vertical rhythm goes lower on a phone, not higher. |
The tokens themselves are in the table above.
See DESIGN.md, Colors; The One Field Rule
Themes
The tokens above are the site's first theme, Night Watch. Ensign is the second: the same tokens re-pinned on a five-color scheme, Ensign Navy #1d3557, Steel Blue #457b9d, Shoal Blue #a8dadc, Sailcloth #f1faee and Signal Red #e63946. A theme is token values and nothing else; no rule below knows which one is on. Ensign is still one dark field in three steps (The One Field Rule), so every wash and scrim solved against the field keeps its shape; what it leaves alone is asset-bound (--chart-navy) or held in reserve (--amber). This block mirrors the one in assets/home.css, and the two are kept in step by hand.
| hook | [data-theme="ensign"] | Ensign, for the element that carries it and everything inside it. On the root element it themes the page: set before first paint by the inline script in the head, from the visitor's stored choice, and by pages.js when the footer's switch is pressed. On any other element it themes that element alone, which is how the style guide draws Ensign on a Night Watch page. No attribute is Night Watch. |
See DESIGN.md, Themes
Typefaces
The same three families as assets/home.css, served from this site (assets/fonts/), so a page load contacts nobody else for its type. Playfair Display is one variable file, italic only, because the serif is only ever italic (The Three Voices Rule). font-display is swap and the stand-ins below are metric-matched, so the swap does not re-wrap the text.
| rule | @font-face 'Playfair Display' | Italic, 400 to 900 in one variable file. Headlines and figures. |
| rule | @font-face 'Barlow' | 300, 400 and 500. Everything read at length. |
| rule | @font-face 'IBM Plex Mono' | 400 and 500. The label voice. |
See DESIGN.md, Typography
Font fallbacks
Metric-matched stand-ins, measured against the webfonts, so the swap does not re-wrap the page: until a webfont arrives, Georgia, Arial or Courier New stands in, scaled and with its ascent and descent overridden so every line break stays where it was.
| rule | @font-face 'Playfair Fallback' | Georgia at 85.2%, measured against Playfair italic 700, the headline weight. |
| rule | @font-face 'Barlow Fallback' | Arial at 94.4%. Body copy. |
| rule | @font-face 'Plex Mono Fallback' | Courier New at 100%. The label voice. |
See DESIGN.md, Typography: Roles are tokens
Base
The reset, the page defaults and the accessibility helpers.
| rule | * | Border-box sizing and no default margins: every gap on the page is set on purpose. |
| rule | html | Smooth in-page scrolling (made instant under reduced motion) and a scrollbar in the palette. |
| rule | body | Deep Water ground, Instrument White text, Barlow 400 at Body size. lining-nums site-wide, because figures here are data and are pinned rather than left to the face (The Plain Figures Rule). |
| rule | ::selection | Depth Cyan Muted with Midnight Hull text. |
| rule | a | the tint at 0.78, Instrument White on hover. The Moonstruck Navy sections name their own link color. |
| rule | :focus-visible | The site-wide focus ring: 3px Depth Cyan, 3px offset. Instrument White in the footer, the nav and the drawer. |
| rule | p, li, dd | text-wrap: pretty, so no paragraph ends on a lone word. |
| rule | :where(h1..h4) | The Heading Leads Rule: 0.4em under every heading, 1.25em over one that follows content. |
| rule | h1, h2, h3 | Playfair italic 700 (The Three Voices Rule). |
| rule | .vh | Visually hidden, still read aloud. |
| rule | .skip-link | "Skip to content", off screen until it takes focus. |
Voice
Italic serif for things you would say, tracked uppercase IBM Plex Mono for things you would annotate, Barlow for everything you read or count.
| rule | .label | The annotation voice: IBM Plex Mono 400, 12px, uppercase, 0.16em tracking. |
| rule | .lede | The introducing paragraph: Barlow 400 at Lead size. |
| rule | (the two weight groups) | One weight per voice. body is Barlow 400, so every label and every serif fragment that is not a heading is named here and set to 400, and the label group is also where labels take --font-label. A new label or serif class joins the matching group. |
| rule | .between a:not(.btn), .close a:not(.btn) | Prose links on the navy sections inherit Instrument White. |
| rule | section[id] | Anchored sections stop clear of the fixed nav. |
See DESIGN.md, The Three Voices Rule
Buttons
Square, tracked, quiet color swaps. Corners have no radius; pills are for icon-only controls. Every button is a link: the site has no forms, and every action leaves for a call, an email or the Charter Smarter booking form.
| rule | .btn | The base: label voice at 12px with 0.18em tracking, 18px by 26px padding, 132px minimum width, 0.25s color swaps. |
| modifier | .btn-primary | The action pair, --action with --action-ink text: Depth Cyan, or Signal Red in Ensign. One per view, and only for booking. On hover it keeps its color and lights a 2px Lamp Cream bezel. |
| modifier | .btn-dark | Midnight Hull at 92% with Instrument White text: Call and Email in the floating bar. |
| modifier | .btn-sky | A 1px tint outline at 45% with Instrument White text, in the larger 14px size. No page uses it now. |
| modifier | .btn-ghost | A 1px tint outline at 50% with Instrument White text, 14px: navy fields. |
| state | :hover | Dark swaps to an Instrument White fill (Midnight Hull text), and Sky to one with Deep Water text; the ghost firms its outline and takes a 10% wash. |
See DESIGN.md, Buttons; The One Action Rule
Reveal
Same grammar as the homepage: slow, soft, once. A block starts 36px low and transparent, and rises over 0.8s when pages.js marks it as entered.
Entrances fail open, here and for the chart pins, the route mask and the gauges further down. Nothing is hidden unless the root element carries data-entrances, which the one-line script in the head sets and takes off again if pages.js has not started within four seconds, or fails to load at all. Without script, behind a blocker or on a connection that drops the script, the page reads in full. The attribute is matched inside :where(), so these rules keep the specificity they had.
| rule | .reveal | The resting state, while entrances are armed. |
| hook | [data-entrances] | On the root element: "armed" by the head script, "live" once pages.js is watching. Absent, nothing waits. |
| hook | .reveal.reveal-visible | Entered. Added once by pages.js, never removed. |
Charts
Every chart is a viewBox crop of one drawing, assets/chart/vi-land.svg, on a Moonstruck Navy field. The SVG carries the geography and the furniture (graticule, compass rose, scale bar, title block); pins and place labels are HTML laid over it, positioned in percent, so they stay legible at any chart scale and can take focus. Chart type is set in SVG user units, so it scales with the drawing.
| rule | .chart | The positioning context for everything laid over the drawing. |
| rule | .chart .shoal | The coastline once as a wide, faint halo: Instrument White at 7%, 11px. |
| rule | .chart .land | The coastline again as land: Instrument White fill at 16% under a 1px Instrument White line. |
| rule | .chart .grat | Graticule lines every tenth of a degree. Non-scaling, so always 1px. |
| rule | .chart .grat-label | Graticule edge labels in the label voice at chart scale. |
| rule | .chart .rose, .rose-fill, .rose-text | The four-point compass rose and its N. |
| rule | .chart .scale, .scale-text | The nautical-mile scale bar. |
| rule | .chart .chart-title | The title block: a caps line, and with .serif the chart's name in Playfair. |
| rule | .chart .route | One leg of a passage: a dashed Instrument White line. |
| rule | .chart .draw | The mask that draws the routes on: one unbroken path with pathLength 1, so a dash offset of 1 hides the passage and 0 shows it. |
| hook | .chart.is-in | Entered (pages.js): the mask's offset eases to 0 over 2.6s, then pins and labels fade in, staggered by --i. The field is the sea itself: assets/opt/chart-sea-784.webp, Moonstruck's own open water seen from above and carried onto Moonstruck Navy as one hue (tools/chart-sea.py). The plate averages to exactly #061e45 and tiles seamlessly, so it lies where flat navy lay and the sea runs continuous across the satellite divider. It sits on .chart rather than the surround, so below 900px the swell pans with the land, and its 784px repeat is never scaled to the chart. |
| var | --sea | Where this chart's window falls on the one sea: its viewBox origin, and a chart unit to a pixel. Set inline by the generator. Charts over the same water show the same water, and each inset shows its own patch. |
| var | --i | A pin's place in the entrance order. Set inline by the generator. |
See DESIGN.md, Charts; The Chart Truth Rule
Chart pins and linking
Pins are real HTML, so they stay legible at any chart scale and can be focused. Pointing at or focusing a log entry or key row lights its leg and pin, and the rest of the chart steps back.
| rule | .pin | A 24px Depth Cyan disc with a Midnight Hull numeral and a Moonstruck Navy ring. |
| state | .pin:hover, .pin:focus-visible, .pin.active | Instrument White, 1.25 times the size, with a wider ring and the Control shadow. |
| rule | .pin-label | The place name beside a pin, in the label voice, with a Moonstruck Navy halo so it holds over coastline and imagery. Hidden from assistive technology: the pin is named. |
| modifier | .pin-label.left, .above, .below, .belowr | Which side of the pin the label sits on. The one thing on a chart chosen by hand. |
| rule | .chart .mark | A small ring for a place a leg passes through without stopping. |
| hook | .chart[data-active] | A key is lit (pages.js): routes other than .active fall to 28%, and pins other than .active to 55%. |
| modifier | .pin-hold | A pin that keeps full strength while another is lit. Available to the generator (hold=True); unused today. |
| breakpoint | 900px | Pins and labels step down to the size their places were chosen at. |
| breakpoint | 768px | Graticule labels and the title block are dropped. |
Chart / satellite split
Every chart but the itineraries hero carries a satellite plate of the same window, in register with the drawing (tools/fetch-satellite.py): chart to the left of the divider, satellite to its right. The plate lies over the drawing and under the routes, pins and furniture (.chart-over), so the passage stays plotted on both. Nothing shows until pages.js marks the chart .split-on; without script it is simply the chart.
| var | --split | The divider's position in percent from the left. Written by pages.js, which measures where it rests against what is on screen; defaults to 100% (chart only). |
| var | --knob-y | How high the knob rides on the divider, chosen per chart to clear its pins. Set inline by the generator. |
| var | --split-delay | How long the entrance waits, so the route has drawn first. Set inline by the generator. |
| rule | .chart > .chart-over | A second SVG above the plate for whatever must stay visible on both views: the routes, and on the index charts the rose, scale bar, title block and graticule labels. |
| rule | .chart .sat | The plate, clipped from the left by --split. |
| rule | .chart .sat img | color: transparent keeps the alt text from being drawn across the chart while a plate loads. |
| rule | .split-line | A 22px-wide grab strip drawing a 1px Instrument White line. touch-action: pan-y, so the page still scrolls past it. |
| rule | .split-knob | A 40px round control in the Button Dark vocabulary, with the Control shadow since it sits on imagery. Its ::after grows the hit area by 5px; it is clamped 26px inside either edge. |
| state | .split-knob:hover, .chart.is-dragging .split-knob | Solid Instrument White with Midnight Hull chevrons. |
| hook | .chart.split-on | Script is running: the plate, line and knob are displayed. |
| hook | .chart.split-live | The line and knob are visible. |
| hook | .chart.split-intro | The entrance is in flight: --split changes ease over 1.5s, after --split-delay. |
| hook | .chart.is-dragging | A pointer holds the divider: the resize cursor everywhere, and pins stop taking the pointer. |
See DESIGN.md, The chart / satellite split
Chart panning and key
On a phone and a tablet a chart keeps a minimum width and pans sideways inside its scroller, opening on its subject (pages.js, data-focus-x) with its divider resting in the middle of what that leaves on screen. Its labels are placed by hand against the width it is drawn at, so squeezing it below that width runs them into each other and off its right edge: it pans instead.
| rule | .chart-scroll | The scroller. Clipped on desktop, horizontally scrollable at 900px and below, where pages.js makes it a tab stop so the arrow keys pan it. |
| var | --chart-min | The width a chart keeps at 900px and below. Set inline by the generator, per chart. |
| rule | .chart-hint | "Drag the chart to pan": below 900px only. |
| rule | .chart-key | The key under the hero chart: a dashed sample line, and .fine for the "Not for navigation" line. |
| breakpoint | 900px | Where the labels come clear on their own: the itineraries hero is drawn 870 units wide, and its two closest names ("Cane Garden Bay" and "The Baths") stop touching just above that. Measured, not chosen. |
| breakpoint | 768px | The graticule labels and the title block are dropped. The pan itself starts at 900px. |
Itineraries: hero
The itineraries page opens on a full-width chart of the seven-night passage, with the headline set over its open sea. This is the one chart with no satellite plate, because type sits on it.
| rule | .hero-chart | The Moonstruck Navy field. Its top padding clears the fixed nav. |
| rule | .hero-chart .hero-copy | The headline block, laid over the chart's empty north-west with a navy halo on its type. |
| rule | .hero-chart .hero-actions | Two ghost buttons to the plans. |
| rule | .hero-chart .chart | On a phone, cropped by the scroller to a band: 230 of the chart's 560 viewBox units come off the top, because the copy has left the open sea and a screen of empty water stood between the buttons and the islands. The cut takes Anegada, the compass rose and the passage north; nothing comes off the foot, where land reaches to within 5 units of the edge. |
| breakpoint | 768px | The chart is 870px wide whatever the window is, so the cut is flat pixels. |
| breakpoint | 900px | The chart holds its 870px and pans, so the hint under it shows on a tablet as well as a phone. |
| breakpoint | 1100px | The copy leaves the chart and sits above it, without the halo. |
Itineraries: passage plans
One section per sample passage: a head (title, lede, four facts), then a sticky chart beside the day-by-day log, then "how the week leans". Both plans sit on the page field.
| rule | .plan | The section. |
| modifier | .plan.frost | The page field as the ground, written out. |
| rule | .plan-inner | The full nine-column measure, centered. |
| rule | .plan-head | Five columns of title and lede against the facts. |
| rule | .plan-facts | Four figures in Playfair, each over a label-voice caption, above a hairline. |
| rule | .plan-body | Four columns of chart against five of log, with a derived alley, so both sit on column lines. |
| rule | .plan-chart | The chart column. Sticky below the nav while the log scrolls past. |
| rule | .plan-chart .mat | The chart's frame. A Moonstruck Navy plate on the page field, naming its own ink as .hero-chart and .index do. |
| rule | .plan-chart .chart-cap | The caption under the mat: "Numbered by leg" and "Not for navigation". |
| breakpoint | 1151px | Below the full grid the two columns go fluid: 5fr to 6fr with an 8% alley. |
| breakpoint | 900px | One column; the chart stops being sticky. |
| breakpoint | 768px | Section padding drops to --section-y; facts and titles step down a size. |
Itineraries: the log
One entry per leg: a numbered day beside a title ("from, arrow, to"), the time under sail, a paragraph, and the day's planning tags.
| rule | .leg | An entry: a 96px day column and the text, between hairlines. |
| hook | .leg.active | Its leg is lit on the chart (pages.js): an Instrument White wash from the left, and the numeral grows. |
| rule | .leg::before | The lit wash: the row's own layer, not its background, so it can lead the day column by --leg-lead without moving the hairline or the day column off the log's grid line. |
| var | --leg-lead | How far the wash leads the day column: 20px into the alley beside the chart, and the page margin (--section-x) once the log spans the measure, so the lit row breaks the margin. |
| rule | .leg-num | The leg's number in a 30px Moonstruck Navy disc, matching its pin on the chart. |
| rule | .leg-when | "Day 3, Monday" in the label voice. |
| rule | .leg-title | Title size. .to is the arrow between the two places; a visually hidden " to " is read in its place. |
| rule | .leg-sail | Time under sail, with the sail glyph. |
| rule | .tags | Planning tags: the crew's own pictograms from assets/icons/tags.svg at 24px, filled with currentColor (the tint at 0.66 on a panel, Depth Cyan on a chart field). Each is named by its title and an accessible label. |
| modifier | .tags.legend | The key to the set, where each pictogram carries a visible label. |
Itineraries: how the week leans
The mix: relative emphasis, from the charter's own planning sheet. Five gauges on a scale ruled in quarters.
| rule | .mix | The block: a title and lede against the gauges. |
| rule | .gauge | One row: a label and its track. |
| rule | .gauge-track | The ruled scale: a tick every 25%. |
| rule | .gauge-fill | The reading. Scales in from the left over 1.2s, each row 0.1s after the one above. |
| var | --v | The reading in percent. Set inline by the generator. |
| hook | .gauges.is-in | Entered (pages.js): the fills run. |
Itineraries: reading the plan
The notes block under the two plans. A hairline marks the seam, as it does above .more.
| rule | .notes | The section: a heading and lede against a definition list. |
| rule | .notes dt, dd | A term in Playfair at Title S, and its explanation at 50ch. The term leads by --head-lead and trails by --head-trail (The Heading Leads Rule), not on a grid gap. |
| rule | .notes .tags | The full legend of pictograms in three columns, two at 480px, one at 360px. |
Destinations: photo hero
The two destination pages open on a photograph under a scrim that deepens toward the foot, where the headline sits.
| rule | .hero-photo | The field: at least 72vh (capped at 820px), its copy at the bottom left. |
| rule | .hero-photo::after | The scrim: Deep Water at 94% at the foot, 70% two fifths of the way up and 36% at the top. The top is what holds the headline over a pale sky; solve it against the brightest pixels behind the copy in both photographs, and again whenever either changes. |
| rule | .hero-photo .hero-copy | The headline, lede and two ghost buttons, on the mid measure. |
Destinations: chart index
A Moonstruck Navy section holding the country's chart with every featured anchorage numbered, and a key beside it. Key rows and pins light each other (pages.js).
| rule | .index | The Moonstruck Navy section. |
| rule | .index-head | Five columns of heading against the lede. |
| rule | .index-body | Six columns of chart against three of key. |
| modifier | .index-body.wide | A chart too wide to share a row (the USVI): the chart takes the full measure and the key runs in three columns under it. |
| rule | .index .mat | The chart's frame: a 1px Instrument White line at 20%. |
| rule | .key | The numbered list of anchorages. Each row is one link to its entry. |
| rule | .key .k | The numeral, in the pin's colors. |
| rule | .key .name, .where | The anchorage in Playfair, and its island in the label voice. |
| hook | .key a.active | Its pin is lit on the chart. |
Destinations: pilot entries
One entry per anchorage, written like a pilot book: a chart inset beside a number, a name, a place line, a paragraph or two, and what the anchorage is good for. Entries alternate sides.
| rule | .entries | The Deep Water section. |
| rule | .entry | Four columns of inset against five of text, with a derived alley. |
| rule | .entry:nth-child(even) | The mirror image: text first, inset second. |
| rule | .entry-num | The anchorage's number, matching its pin on the index chart. |
| rule | .entry .where | The place line, in the label voice. |
| rule | .entries-credit | The imagery credit, composed per page from the sources its plates came from. |
| breakpoint | 1151px | The columns go fluid. |
| breakpoint | 800px | One column, inset first. |
Chart insets
The inset: the boat section's panel mat, carrying a chart plate instead of a photograph. A 4:3 crop of the one drawing at anchorage scale, with the pin where the boat lies, a north arrow, a one-mile bar, and a caption lip giving the coordinates. It can be swapped for a photograph in the same mat without changing the page.
| rule | .inset | The Night Watch mat, with the Float shadow. It wraps the plate on three sides. |
| var | --mat | The mat's width: 16px, 14px at 800px and below. |
| rule | .inset figcaption | The lip: "Chart inset" and the coordinates, in the label voice. |
| rule | .inset .north | The north arrow, with a soft Midnight Hull cast so it holds over reef and sand. |
| rule | .inset .scalebar | The one-mile bar. |
| var | --nm | One nautical mile as a fraction of the inset's width. Set inline by the generator. |
Anchorage map opener
The inset opens Apple's satellite map at its pin: the whole plate is the target, and this round control is its keyboard and screen-reader handle, in the divider knob's vocabulary. A link until a MapKit token is set.
| rule | .inset .chart | The zoom-in cursor over the whole plate. |
| rule | .inset-open | A 34px round control at the plate's top left. Its ::after grows the hit area by 6px all round. |
Map dialog
The homepage lightbox's field and close button, holding a live map instead of a photograph. Emitted only when a MapKit token is set, so dormant today.
| rule | .mapbox | The Midnight Hull field at 96%. Hidden with visibility as well as opacity, so nothing inside it can take focus while it is closed. |
| hook | .mapbox[data-open="yes"] | Open. Written by pages.js. |
| rule | .mapbox-frame | Three rows: heading, map, foot. |
| rule | .mapbox-num | The anchorage's number, in the pin's colors. |
| rule | .mapbox-map | The box MapKit draws into. |
| rule | .mapbox-fail | The fallback line laid over the map when MapKit has not initialized. |
| rule | .mapbox-foot | "Open in Apple Maps", always present, beside "Not for navigation". |
| rule | .mapbox-close | A 48px solid Instrument White close button with the Control shadow, fixed to the corner. |
Photo band
A quiet full-bleed plate between the entries and the notes.
| rule | .band | The plate: between 320px and 640px tall, the photograph covering it. |
| rule | .band figcaption | A square Midnight Hull caption plate at the bottom left: the badge vocabulary. |
Between the anchorages
Practical notes on the sea: the same plate that lies under every chart, at the same size, so the field here is the water the charts are drawn on rather than a second kind of texture. It averages to Moonstruck Navy exactly, so the type keeps the contrast it had on the flat colour and needs no wash over it.
| rule | .between | The field. |
| rule | .between-inner | A heading and lede against a definition list, as in .notes. |
See DESIGN.md, The Sea Field Rule
Also on the chart
Cross links between the three inner pages: the other two, each a serif title with an arrow over a one-line description.
| rule | .more | The Deep Water section, under a hairline. |
| rule | .more a | One link. .t is the title and arrow, .d the description. |
Closing
Same field as the homepage's booking section: the sea plate, centered type, and the page's one booking button above a row of secondary actions.
| rule | .close | The field, on the emphasis step --section-y-xl. |
| rule | .close h2 | The page's last word, and the one place the headline scale steps up (to 68px). |
| rule | .close .lede | Set in the body sans here, not Playfair: a closing paragraph, not an introduction. |
| rule | .close .row | A centered row of buttons. |
Page not found
404.html: one sea section the height of the window, with a lifebuoy riding three lines of swell above the headline. The host serves it at whatever address was asked for, at any depth, so tools/chart-pages.py writes every reference in it from the site root.
| rule | .lost | The field: the sea plate at its one size, clearing the fixed nav, its copy centered. |
| rule | .lost-sea | The drawing. Its sides are masked away so the swell has no edge to run off. |
| rule | .lost-buoy | The lifebuoy: it rides up and down and rolls a few degrees about its own center. |
| rule | .lost-ring | The ring, in Instrument White. |
| rule | .lost-band | Its four bands, in Signal Red (--band): a lifering is red and white in either theme. |
| rule | .lost-rope | The grab line around it, in the tint. |
| rule | .lost-wave | One line of swell, drawn two wavelengths wider than the window and sliding one wavelength (--run) per cycle, so the loop has no seam. |
| rule | .lost-water | The water in front of the buoy: the chart field's navy, fading out below, so the ring sits in the sea. |
| rule | .lost h1 | The headline, at the closing section's scale. |
| rule | .lost .lede | The message, in the body sans on the four-column measure. |
| rule | .lost .row | The one way home: a ghost button, because the filled action is kept for booking. |
| var | --band | Signal Red, Ensign's action color, pinned here so the bands are as red in Night Watch, which has no red of its own. |
| var | --run | The wavelength of a line of swell, which is how far it slides per cycle. |
See DESIGN.md, Sea Sections
Floating action bar
Call, Email and Book, mirroring index.html: a column on the right edge, a full-width row along the bottom on a phone.
| rule | .float-cta | Hidden: transparent, 16px low, out of the pointer's way, and out of the tab order (visibility, not opacity alone). |
| hook | .float-cta[data-shown="yes"] | Shown. Written by pages.js once the hero has passed, and cleared over the closing section. |
| var | --cta-vv | How far a phone's toolbar or keyboard has pushed the visible bottom edge up. Written by pages.js. |
Reduced motion
Every animation's counterpart. Nothing slides, draws or fills: reveals become a short fade, routes and pins are simply there, gauges are full, the split and the dialog change at once. A new animation anywhere in this file needs a line here.
Static capture
Static capture switch (?static in the URL): every entrance resolved, for screenshots and print. pages.js sets the class before anything else runs.
| hook | html.static | Every entrance is forced to its end state, with !important, and no transition. |
Generated by tools/build-docs.py from the comments in the source. To change this page, change the comment and run it again.