S/Y Celestine logoCode reference

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.

TokenValueAt 768px and belowWhat it is for
Color
--navy-core#061e45Celestine Navy. The pinned anchor: every navy field, the chart field, photo wells, the theme color.
--navy-shade#041530Night Watch. The darker navy step: the field behind the homepage's destination card photos.
--navy-deep#020c1dMidnight Hull. Text on frost, dark buttons, badges, overlays and the footer.
--navy-core-rgb6 30 69Celestine Navy as channels, for navy washes and legibility halos.
--navy-deep-rgb2 12 29Midnight Hull as channels, for overlays (0.72 to 0.96) and muted calendar days (0.62).
--steel-ink#36454fChart Ink. The pinned charcoal: body and secondary text on frost, fine linework.
--steel-shade#4a5f6dHarbor Steel. Supporting text and the base link color. A Canvas-surface color: never on a photograph or on navy.
--steel-blue#5b7d90Steel Blue. The focus ring, chosen to clear 3:1 against navy, Canvas and Sea Glass at once; small accents.
--slate-muted#566771Slate Muted. Muted small print. A Canvas-surface color.
--sky-tint#9dc4d2Shallow Water. Highlights on navy: chart pins, key numerals, the weather reading; open days in the calendar.
--harbor-sky#7fa8b8Harbor Sky. Secondary action fill (with Midnight Hull text), gauge fills, the on-hold stripe, text selection.
--harbor-sky-rgb127 168 184Harbor Sky as channels.
--pale-sky#d6ecf1Pale Sky. Quiet panels, and the base of the calendar's on-hold stripe.
--seaglass#c3e3ebSea Glass. The pinned pale: the text color on every navy field, small chrome on navy, the hover fill of dark buttons.
--seaglass-rgb195 227 235Sea Glass as channels: borders, ghost outlines and secondary text on navy (0.12 to 0.95).
--canvas#eaf3f6Canvas. The one light section surface. A background only, never a text color.
--canvas-rgb234 243 246Canvas as channels: the wash over the homepage's photographic light sections. Homepage only. home.css only
--seaglass-light#f4fafcSea Glass Light. The polaroid mat around plates and chart insets, and caption panels.
--seaglass-deep#cfdfe5Sea Glass Deep. What shows behind a photograph while it loads.
--seaglass-deep-rgb207 223 229Sea Glass Deep as channels.
--seaglass-mistrgb(180 214 224 / 0.55)Sea Glass Mist. The slow-drifting haze inside the rate table. Homepage only. home.css only
--seaglass-mist-clearrgb(180 214 224 / 0)The same color at zero alpha, so the haze's gradients fade without graying. Homepage only. home.css only
--chart-rule#a9cbd6Chart Rule. Hairlines between log entries, facts and notes. Quiet on purpose: it marks an edge, not a boundary anyone reads.
--chart-label#3d6b7dChart Label. Tracked uppercase annotations on Canvas: places, days, captions. A Canvas-surface color.
--harbor-coral#ff5a5fHarbor Coral. The one warm color. The primary booking action and nothing else, always with Midnight Hull text.
--nav-glassrgb(205 224 231 / 0.82)Nav Glass. Once the translucent nav bar over the homepage hero; unused now that the bar has no background.
--nav-glass-rgb205 224 231Nav Glass as channels. The plate behind the homepage's hero headline on a phone.
Blueprint gains
--blueprint-gain-1#4096aeMultiply gain that rescales the blueprint1 plate so its field lands on Celestine Navy. Asset-bound: re-measure if the plate is redrawn.
--blueprint-gain-2#3d90acThe same for the blueprint2 plate, behind the closing section.
Type
--font-display'Cormorant Garamond', 'Cormorant Fallback', Georgia, serifCormorant Garamond, then its metric-matched fallback. The voice: headings, ledes, names.
--font-body'Jost', 'Jost Fallback', system-ui, sans-serifJost, then its metric-matched fallback. Everything you read or count: paragraphs, figures, links in prose.
--font-label'Jost', 'Jost Fallback', system-ui, sans-serifJost again, under its own name. The tracked uppercase label voice: navigation, buttons, captions, chart furniture.
--type-displayclamp(48px, 8vw, 92px)Display: the homepage hero headline only.
--type-headlineclamp(38px, 9vw, 58px)Headline: section titles.
--type-subheadclamp(30px, 7vw, 44px)Subhead: a second heading inside a section.
--type-title-lg34pxTitle L: closing index headings and the homepage's wide plate names.
--type-title28pxTitle: every card and entry heading.
--type-title-sm24pxTitle S: small titles, and every tile title on a phone.
--type-lead22pxLead: the one serif paragraph that introduces a section.
--type-serif20pxSerif text: serif running text inside a component.
--type-body17pxBody: Jost 300 paragraphs.
--type-body-navy18pxBody on navy: one step up, because light text on a dark field reads smaller. The closing paragraph; the homepage's destination blurbs.
--type-small14pxSmall: secondary facts and credits.
--type-label12pxLabel: the floor for tracked uppercase annotations.
--leading-serif1.5Line height for serif running text.
--leading-body1.65Line height for body copy.
--leading-title1.15Line height for titles.
--head-lead1.25emSpace over a heading that follows content in the same flow (The Heading Leads Rule).
--head-trail0.4emSpace under a heading, binding it to what it introduces.
Motion
--ease-revealcubic-bezier(0.22, 0.61, 0.36, 1)The one easing curve for entrances: soft, decelerating.
Grid and measure
--grid-cols9Columns in the page grid. Informational; the spans below are what rules use. Homepage only. home.css only
--grid-max1040pxFull width of the nine-column page grid.
--gutter32px24pxGutter between columns.
--colcalc((var(--grid-max) - 8 * var(--gutter)) / 9)One column, derived from the two above.
--span-3calc(3 * var(--col) + 2 * var(--gutter))Three columns and their two gutters. Spans 4 to 8 follow the same pattern.
--span-4calc(4 * var(--col) + 3 * var(--gutter))Four columns.
--span-5calc(5 * var(--col) + 4 * var(--gutter))Five columns: the measure for a lede.
--span-6calc(6 * var(--col) + 5 * var(--gutter))Six columns.
--span-7calc(7 * var(--col) + 6 * var(--gutter))Seven columns.
--span-8calc(8 * var(--col) + 7 * var(--gutter))Eight columns.
--measure-readvar(--span-6)Reading container (span 6).
--measure-midvar(--span-7)Mid container (span 7).
--measure-widevar(--span-8)Wide container (span 8).
--measure-fullvar(--grid-max)The whole grid (span 9).
Spacing
--section-xclamp(72px, 7.5vw, 120px)24pxHorizontal page margin. The only source of it: anything that breaks the margin computes from this. Above a phone it grows with the window, 7.5% of it, from 72px to 120px, so the column has air around it on a tablet and a laptop, not only once the grid stops growing.
--section-y56px64pxDefault vertical section padding.
--section-y-lg96px64pxEmphasis step: the homepage's availability section, and the main sections of these pages.
--section-y-xl128px80pxEmphasis step for the closing booking section.
--nav-h89px70pxHeight 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 the 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-stats, data-tile, data-rate-row), 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,

Webfonts

assets/home.css:29

The two faces, served from this origin. Until 2026-09-19 they came from Google's two, which cost a visitor a DNS lookup and a TLS handshake to fonts.googleapis.com for the stylesheet, then the same again to fonts.gstatic.com before a single letter was fetched: about 0.36s of that chain on the homepage before the fonts had even been named.

Both families ship as variable fonts, so one file covers every weight a family is set at. Google's stylesheet declared Jost once per weight, all three naming the same file, and the browser fetched that file three times; one @font-face over the 300-500 range fetches it once.

Latin and latin-ext are split as Google split them: the site's copy is US English, so latin-ext is fetched only by a page that actually sets a character outside it.

Deliberately not preloaded. A preload asks for a font at the highest priority there is, ahead of this stylesheet and ahead of the photograph that is the largest contentful paint on three of the five pages, and it was measured costing about 0.15s of first contentful paint on every page for nothing: the fallbacks below are metric-matched, so display=swap draws the real page in the meantime and the substitution does not move a line when the face arrives.

rule@font-face 'Cormorant Garamond'400 upright and italic. Headings, leads and figures.
rule@font-face 'Jost'300 to 500 in one file. Body copy and the label voice.

See ARCHITECTURE.md, Loading

Font fallbacks

assets/home.css:59

Metric-matched stand-ins, measured against the webfonts, so the swap does not re-wrap the page.

rule@font-face 'Cormorant Fallback'Georgia at 88.4%, ascent and descent overridden.
rule@font-face 'Jost Fallback'Arial at 92.8%. Body copy and the label voice.

Base

assets/home.css:67

The reset, link colors per surface, focus, and the accessibility helpers.

rule*Border-box sizing and no default margins.
rulebodyNavy behind everything, so an overscroll never flashes white; lining figures site-wide (The Plain Figures Rule).
ruleh1 to h4; p, liBalanced headings, and paragraphs that do not end on a lone word.
ruleaHarbor Steel, turning to --link-hover.
var--link-hoverThe hover color for links on a surface. Unset on Canvas (falls back to Celestine Navy); Sea Glass on the footer, the menu, #destinations, #log, #book and the scrolled nav.
rule[data-dest-link], footer social, mail and brochure linksSmall targets keep their drawn size and grow an invisible 44px hit area from an ::after with a negative inset (The Hit Area Rule).
rule[data-brochures]The two brochure PDFs, the contact block's third column: a link each, with an icon, a name and the file size that tools/stamp-brochure.py keeps true. The size sits at 85% of the link's own 70% Sea Glass: 5.1:1 on Celestine Navy and 5.6:1 on the footer (75% measured 4.2:1 and failed).
rule[data-pdf-size]The chip after a brochure's name: the kind and the size, PDF ยท 1.1 MB, in a 1px currentColor outline at 0.8em and never under 11px (the particulars' 12px button would take it to 9.6px), so that a link to a file reads as one. It never breaks, so on a phone it drops to its own line whole. tools/stamp-brochure.py keeps the figure true.
rule[data-reach]The contact block's middle column: the booking address, the phone and the website.
rule[data-book-brochures]The same pair again, centered at the foot of the closing section, under the one coral button.
rulea[target="_blank"]::afterEvery link that leaves the site carries an up-right arrow after its words, so that a new tab is never a surprise. Drawn as a mask in currentColor, so it takes the link's own color and its hover. Not on a link that is a mark or a photograph, nor on the two buttons that draw the arrow themselves; the footer's website link keeps its globe instead, since its ::after is a hit area.
rule:focus-visibleThe site-wide ring: 3px Steel Blue, 3px offset; Sea Glass on the navy surfaces. Over the hero photograph it is Celestine Navy: the bar's links and the badge sit on pale sky and sand, where Steel Blue measured 2.2 to 2.7:1 and Sea Glass would be less (WCAG 1.4.11 asks 3:1). Once the bar turns navy, or the drawer is open under it, its ring is Sea Glass, which also survives the links' 85% opacity.
rule[hidden]Hidden means not displayed, whatever display an inline style asks for: the menu, the two dialogs, the log and the open windows all wait behind this attribute.
rule[data-action]:hoverCall, Email and Book (under the calendar, in the floating bar, in the menu) turn Sea Glass.
rule[data-outline]:hoverThe outlined buttons on navy draw their border in full and take a 10% Sea Glass wash; in the log, on Harbor Sky, the same wash is Midnight Hull instead (#log override).
rule.skip-link"Skip to content", off screen until it takes focus.
rule.vhVisually hidden, still read aloud: calendar day states, photo counts.

Heading rhythm

assets/home.css:110

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

assets/home.css:193

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, not by home.js, so a photograph that is 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. Said outright by the same onload attribute, from the paint timeline: a transition starts from the last computed style, not the last painted one, so being early was not enough to skip it. The timeline trails the paint itself by up to a tenth of a second on a slow phone; a photograph inside that gap appears at once as well. 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 and the section shows its background color, so the five of them, a megabyte together, are not fetched with the page. Only where script runs, as on the inner pages: with script off, or in a browser that does not know (scripting: enabled), they load with the page. Screen only: a printed page gets them all.
hook[data-plane-run="yes"] [data-plane]The seaplane flies once across the closing section; planeFly is its path.
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.
breakpoint769px to 1303px (the bar)The rail. 1304px is derived: --grid-max plus twice the bar's 132px, the first width at which the full bar clears the measure. Below it the bar used to lie on the right-hand captions, so with a mouse it is three 56px buttons inside the page margin (72px at its narrowest), each an icon over its label at 1px tracking ("Email" is the widest, at 46px). Never icons alone: a calendar beside an availability calendar does not say Book. On a touch screen the bar docks along the bottom, as on a phone.
breakpoint768px and below (the bar)A phone: docked across the foot of the window, three equal buttons, over the browser's toolbar where there is one (--cta-vv).
rule::selection, htmlHarbor Sky selection, and a scrollbar in the palette.
hookhtml[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 log and the calendar were written above it.
rulesection[id], #book, #availabilityAnchored sections stop clear of the fixed nav: 89px, 58px on a phone.

Navigation

assets/home.css:278

A fixed bar with two states. Over the hero it has no plate: Chart Ink links and the mark sit on the photograph itself, so their contrast depends on what the photograph puts behind them. Past 100px of scroll home.js adds .scrolled and it becomes the navy bar with Sea Glass links, the form the inner pages always use.

rulenavThe bare state, over the hero: no background and no blur.
hooknav.scrolledThe navy state. The logo turns Sea Glass: its file becomes a mask over a --seaglass fill.
rule[data-footer-logo]The footer's mark, in Sea Glass by the same mask, at 90%.
hooknav.menu-openThe drawer is open: the bar goes transparent and hides its logo and links, leaving the burger as the close control. It also stops taking pointer events, or the raised bar would eat taps on the drawer links beneath it; the burger takes them back.
rule.nav-burgerA 44px target holding three wave-shaped bars (an SVG mask over a fill).
hook.nav-burger.activeThe bars cross into an X.
rule.nav-links a::afterWhere you are: a 1px hairline under the link for the section being read, drawn in from the left over 0.35s. Drawn, not text-decoration, so it stops at the last letter instead of running under the tracking.
state.nav-links a[aria-current]The hairline is drawn. home.js sets aria-current="location" from the section on the reading line.
rule[data-menu] a[aria-current]The same mark in the drawer: a Sea Glass underline at 60%.
rule[data-menu]The drawer is the inner pages' one (#mobile-menu in assets/pages.css), value for value: two groups of 30px links, On the chart under a hairline, then Book. Its styles are inline; only its phone padding is here.
breakpoint920pxLinks hide, the burger shows.
breakpoint768pxThe drawer's padding drops from 56px to 40px 32px, which is what lets nine links fit a short phone.

See DESIGN.md, Navigation

Phone layout

assets/home.css:343

One block of overrides per breakpoint, each rule beating an inline desktop style, which is why they are all !important. At 768px: the stats strip goes two by two, 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. At 500px the hero headline takes its own Nav Glass plate, since the photograph behind it can no longer be relied on.

breakpoint768pxThe phone layout.
breakpoint430pxThe calendar drops from two months across to one.
var--cta-vvHow far a phone's toolbar or keyboard has pushed the visible bottom edge up. Written by home.js.

Reveal

assets/home.css:411

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.

rule.revealThe resting state.
hook.reveal[data-revealed="yes"]Entered. Written once by the reveal observer in home.js, never removed.

Hero

assets/home.css:427

Hero photo (Magens Bay), anchored bottom-up: the sand and surf always show and spare sky crops off the top; 32% across keeps the anchored yacht in frame when the sides crop.

The headline sits in the sky, straight under the nav, at every size, and nothing is laid over the photograph to make it legible. A landscape window takes the whole viewport to get that sky: at the old 80vh cap the crop threw away enough of it that the headline met hilltops at 2.5:1. The exception is a window 2:1 and wider, where there is no sky above the hills at any height, and the Sea Glass haze stays.

var--hero-copy-rightWhere the headline's first line ends: the page margin plus 6.8 times the display size.
rule[data-hero-haze]The haze: Sea Glass at 78%, 60% at --hero-copy-right, clear 38vw further on.
breakpointaspect ratio 2:1 and widerThe photograph is anchored to its foot, so on a wide, short window the hillside rises behind the bar: "The yacht" measured 2.9:1 at 1920 by 900, and every link close to 1:1 at 21:9. A second haze runs across the top there, Sea Glass at 80% behind the bar and clear by 230px, the same material as the one behind the headline. A 16:9 window never sees it: its links are on sky.
rule[data-hero-img]The photograph, anchored 32% across and to its bottom edge.
breakpointportrait, 769px and upA tablet held upright gets a shorter stage: 62vh, between 520px and 760px.

See DESIGN.md, Hero Haze; Stats Band

The yacht: header

assets/home.css:461

Five columns of headline and lede against a four-column print of the yacht under sail, each centered on the other. The section opens on the word "Sailboat", and this is the photograph that answers it: a 4:5 cut, because a yacht under sail is a tall subject. The print is a tile from the field below (the same mat, radius and shadow) with no lip, so [data-sail-plate] mats it on all four sides. It does not zoom: the cut is centered on the yacht (mast to waterline, a tenth of the frame clear all round), so an enlargement would show nothing more than the plate already does. The nine columns reach full size at 1224px; under that the same split goes fluid.

breakpoint768pxThe print goes under the copy, centered, no wider than a tile.
breakpoint769px to 999pxThe headline steps down with the window (38px to 58px), so the print keeps a useful width beside it.
breakpoint769px to 1223pxThe fluid split. The print is four columns of the width there is, and gives way first when the headline needs the room, down to 232px.
breakpoint1224pxDerived, not chosen: the first width at which --grid-max plus twice --section-x fits.

Crew

assets/home.css:493

Crew cards carry a Canvas panel: Chart Label is a Canvas-surface color and cannot clear 4.5:1 on a photographic field at any wash alpha. Measured, not guessed. Three equal cards, one per post aboard: the captain, then the two roles still to be named.

rule[data-crew-grid]The row. position and z-index lift it above the section's Canvas wash.
rule[data-crew-grid] > divA flat Canvas panel, 16px 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: Steel Ink on Sea Glass is 7.3:1, so the label is legible.
breakpoint920pxOne column, capped at 440px.

See DESIGN.md, The Photographic Field Rule

Guest reviews

assets/home.css:521

What guests wrote, as speech bubbles on a Sea Glass field, read by scrolling sideways. The words and the photographs are their own (tools/sync-reviews.py); home.js writes the cards from assets/reviews.js, and the section stays hidden if that file has nothing to show.

A rail on the left holds the mark and the heading, and the track runs off the right edge of the page, which is the one honest way to say there is more to the right before anything is scrolled. It is why this section alone is unpadded on the right: the rail starts on the page's left measure line, worked out from --measure-wide rather than inherited, and nothing closes the row.

The row has no controls of its own: it is swiped, or scrolled sideways, or moved with the arrow keys once the track has focus.

rule#guestsThe field: Sea Glass, between two Canvas sections, and the one section with no right padding.
rule[data-guests]Rail against track. The rail is a fixed 264px so the stars and the heading stay on one axis whatever the track is doing.
rule[data-guests-pause]Stops the row sliding on by itself (WCAG 2.2.2): a 36px round in a 1px Chart Rule outline with a 44px hit area from ::after, filled Celestine Navy while sliding is stopped. It holds both glyphs and shows the one that says what pressing it does: pause while the row slides, play while it is stopped. The name is on the button (aria-label, rewritten with the glyph), since neither glyph is text. Shown by home.js only where the row slides.
rule[data-guests-stars]Five stars in --accent (Steel Blue, 3.2:1 on Sea Glass, past 1.4.11). A mark and not a rating: CYA collects no score with a review, so they are aria-hidden and are not marked up as one. Not Harbor Coral, which is the booking action's alone and measures 2.3:1 here.
rule[data-guests-track]The scroller: one row, scroll-snapped, its own scrollbar hidden. It takes a tabindex, so the row can be reached and scrolled from the keyboard (WCAG 2.1.1), and keeps its list role, so how many reviews there are is spoken with its name. home.js writes it out three times so that it has no end (loopGuests); nothing here depends on that, and the row is the same one-row snapping scroller without it.
rule[data-guest]One card: the bubble, then the guest under it. Each is the height of its own review, and the row centers them on one axis: the quotations are between three lines and thirteen, and a row stretched to the longest would leave most bubbles two thirds empty.
rule[data-guest-bubble]Sea Glass Light, 20px round except at the corner the tail leaves, which is square. The tail is the ::after: a triangle hanging from that corner.
rule[data-guest-words][data-guest-folding]Set by home.js for the length of an unfold, so the taller words are clipped to the height of the moment instead of spilling past the bubble. The tail is drawn outside this element, so nothing of it is cut.
rule[data-guest-words]The quotation, in the serif. A long review is cut to a pull-quote by home.js and the rest is a press away, so nothing is clipped by the box: the card is always the height of the words it is showing.
rule[data-guest-more]Read more: a quiet underlined word in the body face, no bigger than the caption under the bubble, so it reads as the card's own aside and not as a second call to action beside Book. Its 44px hit area is an ::after.
rule[data-guest-shots]The photographs the guest sent with the review, up to three across the foot of the bubble. They are cut square at 288px by tools/sync-reviews.py and draw at about 90px, and squares because these are snapshots of a week aboard: a group photograph in a tall frame crops down to nothing. A review that sent one or two gets one or two, at the same size and not stretched.
rule[data-guest-shot][data-zoom]A plate that opens. 90px is too small to see a week in, so pressing one opens the whole uncropped frame in the same zoom dialog the yacht's plates use. The zoom-in cursor and the hover lift are the only thing that says so, as on those plates; it is a role="button", so it also takes Enter and Space, and the focus ring is the site-wide one.
breakpoint1023pxThe rail sits above the track, and the track runs edge to edge under it.

The log

assets/home.css:620

The nine latest Instagram posts, as polaroids on a Harbor Sky field: the yacht plates' Sea Glass Light mat, with the date and place as the annotation and the caption in the serif. The whole mat is one link to the post. The photographs are local copies (tools/sync-instagram.py), so nothing here contacts Instagram until a link is followed.

rule[data-log-grid]Three columns on the wide measure. Rows stretch, so the mats in a row end level.
var--iAn entry's column, from zero, written by home.js: each column enters a tenth of a second after the last.
rule[data-log-entry]The mat, and the link.
var--matThe mat's width: 12px, 8px on a phone.
rule[data-log-plate]The photograph's window: 4/5 whatever the post's own shape, and clipped, so the hover zoom stays inside it.
rule[data-log-mark]The round Instagram badge on the plate's corner. It says where the link goes before it is followed.
rule[data-log-label]Date and place, in the annotation voice. Harbor Steel, which the mat (lighter than Canvas) carries at 6.3:1.
rule[data-log-caption]At most three lines of the caption, four on a phone; the post has the rest.
breakpoint768pxTwo columns, a narrower mat and gap.

See DESIGN.md, The Log

Entrances

assets/home.css:680

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].revealThe 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 yacht 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 yacht: feature field

assets/home.css:701

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-yacht-field]The grid: fluid halves with a 14% alley, exact spans from 1224px.
rule[data-tile]The polaroid: a Sea Glass Light 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.
modifier[data-sail-plate]The print in the section's header: a tile with no lip, so the mat runs under the plate too.
var--matThe mat's width: 16px on tiles, 22px on wide plates, 14px on a phone.
var--plate-arThe plate's proportion, set inline per tile: 16/9, 4/3 or 8/5.
rule[data-plate] > imgThe photograph. A role="button" that opens the plate zoom, hence the zoom-in cursor; one without data-zoom (the sail print) keeps the plain 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.
breakpoint1224pxExact spans; the wide lip splits three columns of title against the rest.
breakpoint768pxOne column; every plate arrives from the left; wide plates go 4:3.

See DESIGN.md, Layout; Shapes

Rate table

assets/home.css:763

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 Canvas panel. isolation keeps its two pseudo-element layers behind its content.
rule[data-rate-table]::beforeThe mist: four soft radial gradients, blurred, drifting on a 34s loop (rateMist). Stilled under reduced motion.
rule[data-rate-table]::afterA 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: party, then the rate. One rate column while the listing prices the year as one season.
rule[data-rate-label]Season labels in the label voice, in --accent-deep with Chart Label as the fallback.
rule[data-rate-pp]The per-person figure: Cormorant 48px, lining tabular figures. Title large (34px) at 600px and below.
rule[data-rate-total]The weekly total, in Jost 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.
rule[data-dining]Optional dining plans, a details element: closed, one row between two Chart Rule hairlines that says what the plans are worth; open, the two plans. It was a subhead, a paragraph and two panels for a discount of 2 to 3 percent of the rate, a screen of a phone.
rule[data-dining] > summaryThe row: the title and its sentence on the left, the cue on the right; stacked at 600px.
rule[data-dining-cue]What pressing does, in the label voice, with a chevron that turns over when open. Hidden from assistive technology: the summary already says collapsed or expanded.
breakpoint600pxEach 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

The particulars

assets/home.css:839

The yacht's figures, under the layout drawings: what a broker quotes from and a guest checks. They are the brochure's own (brochure/build.py, PARTICULARS and TOYS), and the brochure itself is the button under them.

rule[data-particulars]A Canvas panel under a 3px Chart Rule edge, the rate table's vocabulary, because it is the same kind of thing: figures to quote from. A panel, so its Harbor Steel labels sit on Canvas and not on the drawing behind the section (The Photographic Field Rule).
rule[data-particulars-plate]Voyage Yachts' sail plan (hull, rig, mainsail and genoa) as Steel Blue hairlines: the drawing floats to the left of the figures and reads as a drafting key, not a photograph. The lines are non-scaling strokes, so they stay one pixel at any width. Decorative, so it leaves below 1024px, where the column it stands in would be gone anyway.
rule[data-particulars-grid]Three groups of four facts: three columns, one at 768px.
rule[data-particulars-group] h4The group's name at Title S, over a Chart Rule hairline.
rule[data-particulars-group] dt, ddA tracked label over its value in the body sans, with lining tabular figures.

Calendar and booking actions

assets/home.css:871

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 coral 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 Canvas, 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 or hairline in 0.2s. A day that has passed keeps this rule alone: a quiet numeral and no field.
modifier[data-cal-day="open"]Shallow Water.
modifier[data-cal-day="booked"]A 6% navy wash 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 3% navy wash. A turnaround day is an open day beside a charter, kept clear for the crew.
hook[data-cal-day][data-lit="yes"]The day is one of the chosen dates: navy, Sea Glass numeral.
hook[data-cal-day][data-lit="maybe"]The day is in the week or the window being pointed at or focused: Harbor Sky under a navy hairline, so that what is being looked at is a step short of what is chosen, and is not told apart by color alone. Only open days are ever lit, so the numeral stays navy (7.6:1).
rulebutton[data-cal-day]A day a charter can start on is a button; every other day is a plain cell. Its focus ring sits inside the cell, because the site's 3px offset would cover the days either side, and turns Sea Glass on a chosen day, where navy on navy could not be seen.
rule[data-cal-readout]What is chosen, and the one action, under the grid: a Sea Glass Light panel inside a Chart Rule hairline, full-bleed on a phone like the trio under it. Never a price (the rate table says what a week costs), and never sticky: the floating bar already holds the bottom edge over this section.
state[data-cal-readout][data-chosen="yes"]Dates are chosen: the bar shows them, how long they are, the Email action and Clear, in place of the prompt, and the Email in the trio below it stands down rather than repeat itself.
rule[data-cal-book-note]One quiet line under the chosen dates saying that Charter Smarter's form asks for them again, so the Book button below does not look as though it carries them.
hook[data-window-group] a[data-chosen="yes"]The window whose dates are the chosen ones: the label at 500 over a 2px rule.
rule[data-cal-actions]Call, Email and Book under the grid; a full-bleed row on a phone.
hook[data-cal-grid][data-cal-all="no"]At 440px and below, only the first three months show. Written by home.js; wider windows ignore it.
rule[data-cal-more]The button that opens the rest, under the grid: the label voice in a navy outline, the width of the column. Displayed at 440px and below only.
rule[data-window-group]The open windows of one rate season, under that season's name and lowest per-person figure in the rate table's own label voice, so the price does not have to be carried down the page.
rule[data-cal-grid]The months: three across, two from 768px down, one at 440px (see Phone layout). Three across needs about 700px to keep a day square legible, so the middle width takes two.
rule[data-book-actions] > a, [data-book-primary] > aThe closing section's buttons share a 254px minimum width (Broker partners with its arrow), so no action reads as the runt. The two that leave the site (Broker partners, the booking form) carry an up-right arrow after the label.
rule[data-book-note]One line under the buttons that says where those two go, because neither label does.

See DESIGN.md, The Calendar Legibility Rule

How booking works

assets/home.css:967

The four steps between the rate table and the open weeks, and the one section on the homepage that argues for something rather than describing it: the first card is the broker, and two links go to the accredited list. A navy field with Sea Glass Light cards, the one place the two worlds meet inside a section, which is why it sits between two light ones and not beside the navy close.

rule[data-steps]Four cards. Two at 1223px and one at 768px: at 1040px the row gives each card 236px, and a third step down would take it under 200.
rule[data-step]A card in the polaroid tiles' vocabulary (Sea Glass Light, 2px, the same shadow). Text only, by decision: a line drawing per card was built and cut.
rule[data-step-num]The count as a tracked Harbor Steel label over the name (6.3:1 on Frost).
rule[data-broker-link]Every mention of an external charter broker on a navy field is a link to the accredited list, here and in the closing section, which the inner pages have linked all along. Sea Glass underlined, because Sea Glass is the text color those fields already use.
rule[data-step-link]Only the first card carries one, in its own block so the card can space it. Underlined Midnight Hull with the closing section's external arrow, inline so the arrow follows the last word rather than orphaning beside it.

See DESIGN.md, The Two Worlds Rule

Tokens

assets/home.css:1005

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: --canvas-rgb, --seaglass-mist, --seaglass-mist-clear and --grid-cols.

var--accentSet on the page wrapper, not here (Steel Blue): the foot of the gallery's current thumbnail.
var--accent-deepSet beside it (Harbor Steel): crew roles and the rate table's season labels.
breakpoint768pxMargins, vertical rhythm and gutter step down together.

The tokens themselves are in the table above.

See DESIGN.md, Colors; Typography; Layout

Reduced motion

assets/home.css:1093

Every animation's counterpart. Nothing slides, flies or sharpens: entrances become a 0.4s fade, the seaplane and the mist hold still, the nav's hairline appears at once. The weather widget's rotation is stopped in home.js, as are the guests' row (it does not slide by itself, and a card's whole review arrives at its height at once rather than unfolding to it). A new animation anywhere above needs a line here.

Without script

assets/home.css:1117

The page is ordinary HTML, so it reads without home.js; these rules undo the few states that wait for it. Entrances are resolved and the hero photograph is shown. The calendar says to call or email (its grid is computed in script), the log stays hidden, and the footer weather, which would never load, is removed.

Print

assets/home.css:1130

The homepage on paper: a charter sheet, not a screenshot. Modeled on the Coastal site's print block. Paper is light, so each navy band gives up its fill and its type turns to ink; photographs that carry facts (the cabins, the drawings, the two destinations, the crew) print, and those that are only atmosphere (the hero, the blueprint plates, the section backdrops) do not. Anything that only works on a screen goes: the nav, the gallery carousel, the tour's embeds, the calendar grid, the floating bar, the log and the weather.

Most of the page's styling is inline, so nearly every rule here needs !important to count.

rule@pageLetter, with a running foot: the yacht's name, the site's address and the folio.
rule:rootType and rhythm tokens stepped down, so 17px body copy lands at about 10.5pt.
rule#tour, #destinations, #booking, #book, footer, [data-stats]The navy bands. Their light tokens are redefined as ink inside them, so every inline var(--seaglass) turns dark on its own.
rulea[href^="http"]::afterAn outside link prints its address after it, as Coastal's does.

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 exactly, and the two must be kept in step (tools/build-docs.py reports any token whose value differs between them).

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 two 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 Three Pins Rule, The Column Span Rule, The One Margin Rule).
  • A section is navy (text in Sea Glass) or Canvas (text in navy or Chart Ink). A component used on both grounds follows its Canvas rules and re-inks them in Sea Glass steps (The Two Worlds Rule). Nothing does today: .between and .close are navy throughout, and the plans are Canvas.
  • State is carried by classes and data attributes that pages.js sets (.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

assets/pages.css:38

Every color, type size, measure and spacing step on the inner pages. The palette is three pinned colors (Celestine Navy, Chart Ink, Sea Glass) plus Harbor Coral; every other color is a lightness step inside one of those families, computed to hit a contrast target. The -rgb tokens are channel triplets for translucent steps: rgb(var(--seaglass-rgb) / 0.72).

breakpoint1224pxDerived, not chosen: the first width at which --grid-max plus twice --section-x fits, so the nine-column grid is full size.
breakpoint768pxMargins, 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 Three Pins Rule

Webfonts

assets/pages.css:166

The two faces, served from this origin. Until 2026-09-19 they came from Google's two, which cost a visitor a DNS lookup and a TLS handshake to fonts.googleapis.com for the stylesheet, then the same again to fonts.gstatic.com before a single letter was fetched: about 0.36s of that chain on the homepage before the fonts had even been named.

Both families ship as variable fonts, so one file covers every weight a family is set at. Google's stylesheet declared Jost once per weight, all three naming the same file, and the browser fetched that file three times; one @font-face over the 300-500 range fetches it once.

Latin and latin-ext are split as Google split them: the site's copy is US English, so latin-ext is fetched only by a page that actually sets a character outside it.

Deliberately not preloaded. A preload asks for a font at the highest priority there is, ahead of this stylesheet and ahead of the photograph that is the largest contentful paint on three of the five pages, and it was measured costing about 0.15s of first contentful paint on every page for nothing: the fallbacks below are metric-matched, so display=swap draws the real page in the meantime and the substitution does not move a line when the face arrives.

rule@font-face 'Cormorant Garamond'400 upright and italic. Headings, leads and figures.
rule@font-face 'Jost'300 to 500 in one file. Body copy and the label voice.

See ARCHITECTURE.md, Loading

Font fallbacks

assets/pages.css:196

Metric-matched stand-ins, measured against the webfonts, so the swap does not re-wrap the page. The webfonts load asynchronously with display=swap; until they arrive Georgia and Arial stand in, scaled and with their ascent and descent overridden so every line break stays where it was.

rule@font-face 'Cormorant Fallback'Georgia at 88.4%.
rule@font-face 'Jost Fallback'Arial at 92.8%. Body copy and the label voice.

See DESIGN.md, Typography: Roles are tokens

Base

assets/pages.css:209

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.
rulehtmlSmooth in-page scrolling (made instant under reduced motion) and a scrollbar in the palette.
rulebodyCanvas ground, Midnight Hull text, Jost 300 at Body size. lining-nums site-wide, because figures here are data and Cormorant's old-style 1 and 0 read as letters (The Plain Figures Rule).
rule::selectionHarbor Sky with Midnight Hull text.
ruleaHarbor Steel, Celestine Navy on hover. Right for Canvas only: a navy surface names its own link color.
rulea[target="_blank"]::afterEvery link that leaves the site carries an up-right arrow after its words, so that a new tab is never a surprise. Drawn as a mask in currentColor, so it takes the link's own color and its hover. Not on a link that is a mark or a photograph; the footer's website link keeps its globe instead, since its ::after is a hit area.
rule:focus-visibleThe site-wide focus ring: 3px Steel Blue, 3px offset. Sea Glass on the navy surfaces, where Steel Blue is too quiet.
rulep, li, ddtext-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.
ruleh1, h2, h3Cormorant 400. Bold is not part of the system (The Light Hand Rule).
rule.vhVisually hidden, still read aloud.
rule.skip-link"Skip to content", off screen until it takes focus.

Voice

assets/pages.css:283

Serif for things you would say, tracked uppercase Jost for things you would annotate, light Jost for everything you read or count.

rule.labelThe annotation voice: Jost 400, 12px, uppercase, 3px tracking.
rule.ledeThe introducing paragraph: Cormorant 400 at Lead size.
rule(the two weight groups)One weight per voice. body is Jost 300, 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 navy inherit the field's Sea Glass.
rulesection[id]Anchored sections stop clear of the fixed nav.

See DESIGN.md, The Logbook Voice Rule; The One Weight Per Voice Rule

Buttons

assets/pages.css:309

Square, tracked, quiet color swaps. Corners have no radius; pills are for icon-only controls. Every button is a link: there are no forms on the site, and every action leaves for a phone call, an email or the Charter Smarter booking form.

rule.btnThe base: label voice at 12px with 4px tracking, 18px by 26px padding, 132px minimum width, 0.25s color swaps.
modifier.btn-primaryHarbor Coral with Midnight Hull text. One per view, and only for booking.
modifier.btn-darkMidnight Hull at 92% with Sea Glass text: Call and Email in the floating bar.
modifier.btn-skyHarbor Sky with Midnight Hull text, in the larger 14px size: secondary actions on navy.
modifier.btn-ghostA 1px Sea Glass outline at 50% with Sea Glass text, 14px: navy fields only.
state:hoverEvery filled button swaps to a Sea Glass fill with Midnight Hull text; the ghost firms its outline and takes a 10% fill.

See DESIGN.md, Buttons; The One Coral Rule

Navigation

assets/pages.css:333

Inner pages open in the scrolled navy state; there is no light hero to sit over. A fixed navy bar with the logo and six uppercase links back to the homepage's sections. At 920px and below the links give way to the burger, which opens #mobile-menu: a navy drawer from the right, marked up as a modal dialog, with a second group linking the three inner pages. The homepage's drawer is the same one, value for value, written there as inline styles; a change to either is made to both.

rule.site-navThe fixed bar. Its height is --nav-h, which other rules clear.
rule.site-nav imgThe logo in Sea Glass. Its file is used as a mask over a --seaglass fill, not tinted by filter.
ruleimg.mark-seaglassThe same recolor for the mark anywhere else on navy (the style guide uses it).
rule.nav-links a13px label voice at 85%, full strength on hover.
state.nav-links a[aria-current]Where you are: full strength, with a 1px hairline drawn 8px below the label. Drawn as a pseudo-element because text-decoration would run on under the trailing tracking.
rule.nav-burgerA 44px target holding three wave-shaped bars (an SVG mask over a Sea Glass fill).
state.nav-burger.activeThe bars cross into an X. Set by pages.js while the drawer is open.
hook.site-nav.menu-openThe drawer is open: the bar rises above it, goes transparent and hides its logo and links, leaving the burger as the close control. Written by pages.js. A z-index on the burger cannot do this: the bar is its own stacking context, so the drawer covered the burger whatever it said. Raised, it stops taking pointer events, or its invisible strip would eat taps on the drawer links beneath it; the burger takes them back.
rule#mobile-menuThe drawer: full width up to 24rem, 30px Cormorant links in Sea Glass. It scrolls when a screen is too short for it (a phone on its side).
hook#mobile-menu[data-open="yes"]Displays the drawer, and the backdrop after it. Written by pages.js.
rule#mobile-menu > :first-childWith the rule after it, centers the column by auto margins, not justify-content: centered overflow cannot be scrolled back to, and auto margins fall to zero instead.
rule#mobile-menu .menu-groupA group of links. The second group sits under a hairline and a .label heading.
rule#mobile-menu a.btnThe drawer's Book button. It restates its ink because the id rule above outranks .btn-primary, and its leading (1.5, so 54px tall as on the homepage) because the links' 1.1 would reach it too.
rule#mobile-menu .btn svgIts calendar, 15px like the floating bar's. Unsized, it grew to fill the line.
rule.menu-backdropMidnight Hull at 60% over the page while the drawer is open. A click on it closes the drawer.
breakpoint920pxLinks hide, the burger shows.
breakpoint768pxThe bar and the drawer tighten their padding.

See DESIGN.md, Navigation; Inner Page Chrome

Reveal

assets/pages.css:418

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. Without script it stays hidden.

rule.revealThe resting state.
hook.reveal.reveal-visibleEntered. Added once by pages.js, never removed.

Charts

assets/pages.css:429

Every chart is a viewBox crop of one drawing, assets/chart/vi-land.svg, on a Celestine 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.chartThe positioning context for everything laid over the drawing.
rule.chart .shoalThe coastline once as a wide, faint halo: Sea Glass at 7%, 11px.
rule.chart .landThe coastline again as land: Sea Glass fill at 16% under a 1px Sea Glass line.
rule.chart .gratGraticule lines every tenth of a degree. Non-scaling, so always 1px.
rule.chart .grat-labelGraticule edge labels in the label voice at chart scale.
rule.chart .rose, .rose-fill, .rose-textThe four-point compass rose and its N.
rule.chart .scale, .scale-textThe nautical-mile scale bar.
rule.chart .chart-titleThe title block: a caps line, and with .serif the chart's name in Cormorant.
rule.chart .routeOne leg of a passage: a dashed Sea Glass line.
rule.chart .drawThe mask that draws the routes on. One unbroken path with pathLength 1, so a dash offset of 1 hides the whole passage and 0 shows it.
hook.chart.is-inEntered (pages.js): the mask's offset eases to 0 over 2.6s, then pins and labels fade in, staggered by --i.
var--iA pin's place in the entrance order. Set inline by the generator.

See DESIGN.md, Charts; The Chart Truth Rule

Chart pins and linking

assets/pages.css:469

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.pinA 24px Shallow Water disc with a Midnight Hull numeral and a navy ring.
state.pin:hover, .pin:focus-visible, .pin.activeSea Glass, 1.25 times the size, with a wider ring and the Control shadow.
rule.pin-labelThe place name beside a pin, in the label voice, with a navy halo so it holds over coastline and satellite imagery. Hidden from assistive technology: the pin is named.
modifier.pin-label.left, .above, .below, .belowrWhich side of the pin the label sits on. The one thing on a chart chosen by hand.
rule.chart .markA 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 fall to 55%.
modifier.pin-holdA pin that keeps full strength while another is lit. Available to the generator (hold=True); unused today.
breakpoint900pxPins and labels step down a size, the size their places were chosen at on a chart held to its drawn width. They take no padded hit area (The Hit Area Rule): the Indians and Norman Island lie 23px apart on a phone's chart, so a wider target would take taps off the disc beside it, and pins are never moved to make room (coordinates are data). WCAG 2.5.8 is met through its equivalent-control exception: every pin has a full-height row in the key beside the chart that is the same link.
breakpoint768pxGraticule labels and the title block are dropped.

Chart / satellite split

assets/pages.css:507

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--splitThe 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-yHow high the knob rides on the divider, chosen per chart to clear its pins. Set inline by the generator.
var--split-delayHow long the entrance waits, so the route has drawn first. Set inline by the generator.
rule.chart > .chart-overA 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 .satThe plate, clipped from the left by --split.
rule.chart .sat imgcolor: transparent keeps the alt text (still read aloud) from being drawn across the chart while a plate loads.
rule.split-lineA 22px-wide grab strip drawing a 1px Sea Glass line. touch-action: pan-y, so the page still scrolls past it.
rule.split-knobA 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 all round. Clamped 26px inside either edge.
state.split-knob:hover, .chart.is-dragging .split-knobSolid Sea Glass with Midnight Hull chevrons.
hook.chart.split-onScript is running: the plate, line and knob are displayed.
hook.chart.split-liveThe line and knob are visible.
hook.chart.split-introThe entrance is in flight: --split changes ease over 1.5s, after --split-delay.
hook.chart.is-draggingA 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

assets/pages.css:553

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. A chart's 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, at the label size the placement was made for.

rule.chart-scrollThe scroller. Clipped on desktop, horizontally scrollable at 900px and below.
state.chart-scroll[tabindex]:focus-visibleWhile its chart is wider than it is, pages.js makes the scroller a named region that takes focus, so the arrow keys pan it (WCAG 2.1.1). The ring is drawn inside the box, in Sea Glass, because the mat around a chart clips anything outside it. The style guide's table and code scrollers get the same treatment and keep the site-wide ring.
var--chart-minThe 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-keyThe key under the hero chart: a dashed sample line, and .fine for the "Not for navigation" line.
breakpoint900pxWhere 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.
breakpoint768pxThe graticule labels and the title block are dropped. The pan itself starts at 900px.

Itineraries: hero

assets/pages.css:593

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-chartThe navy field. Its top padding clears the fixed nav.
rule.hero-chart .hero-copyThe headline block, laid over the chart's empty north-west with a navy halo on its type.
rule.hero-chart .hero-actionsTwo ghost buttons to the plans.
rule.hero-chart .chartOn 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 the chart keeps for it and a screen of empty water was all that stood between the buttons and the islands. The cut takes Anegada, the compass rose and the passage north, which runs off the top edge; nothing comes off the foot, where land reaches to within 5 units of the edge.
breakpoint768pxThe chart is 870px wide whatever the window is, so the cut is flat pixels.
breakpoint900pxThe chart holds its 870px and pans, so the hint under it shows on a small tablet as well as a phone, and the copy leaves the chart and sits above it, without the halo. A larger tablet keeps the laid-over headline: the chart's north-west is open sea at that width too.

Itineraries: passage plans

assets/pages.css:635

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 are Canvas today; the navy variant is kept working beside every rule.

rule.planThe section.
modifier.plan.frostCanvas ground.
rule.plan-innerThe full nine-column measure, centered.
rule.plan-headFive columns of title and lede against the facts.
rule.plan-factsFour figures in Cormorant, each over a label-voice caption, above a Chart Rule hairline.
rule.plan-bodyFour columns of chart against five of log, with a derived alley, so both sit on column lines.
rule.plan-chartThe chart column. Sticky below the nav while the log scrolls past.
rule.plan-chart .matThe chart's frame. A navy plate inside a Canvas plan: it names its own ink, as .hero-chart and .index do, so nothing in it inherits the plan's Midnight Hull.
rule.plan-chart .chart-capThe caption under the mat: "Numbered by leg" and "Not for navigation".
breakpoint1223pxBelow the full grid the two columns go fluid: 5fr to 6fr with an 8% alley.
breakpoint900pxOne column; the chart stops being sticky.
breakpoint768pxSection padding drops to --section-y; facts and titles step down a size.

Itineraries: the log

assets/pages.css:675

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.legAn entry: a 96px day column and the text, between Chart Rule hairlines.
hook.leg.activeIts leg is lit on the chart (pages.js): a Sea Glass wash from the left, and the numeral grows.
rule.leg-numThe leg's number in a 30px navy disc, matching its pin on the chart.
rule.leg-when"Day 3, Monday" in the label voice.
rule.leg-titleTitle size. .to is the arrow between the two places; a visually hidden " to " is read in its place.
rule.leg-sailTime under sail, with the sail glyph.
rule.tagsPlanning tags: the crew's own pictograms from assets/icons/tags.svg at 24px, filled with currentColor (Chart Label on Canvas, Shallow Water on navy). Each is named by its title and an accessible label.
modifier.tags.legendThe key to the set, where each pictogram carries a visible label.

Itineraries: how the week leans

assets/pages.css:711

The mix: relative emphasis, from the charter's own planning sheet. Five gauges on a scale ruled in quarters.

rule.mixThe block: a title and lede against the gauges.
rule.gaugeOne row: a label and its track.
rule.gauge-trackThe ruled scale: a tick every 25%.
rule.gauge-fillThe reading. Scales in from the left over 1.2s, each row 0.1s after the one above.
var--vThe reading in percent. Set inline by the generator.
hook.gauges.is-inEntered (pages.js): the fills run.

Itineraries: reading the plan

assets/pages.css:754

The notes block under the two plans. Both plans are Canvas now, so a Chart Rule hairline marks the seam, as it does above .more.

rule.notesThe section: a heading and lede against a definition list.
rule.notes dt, ddA term in Cormorant 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 .tagsThe full legend of pictograms in three columns, two at 480px, one at 360px.
modifier.notes dd.wideThe dd that holds that legend: a second definition under its term, so the list stays a valid dl, and free of the 50ch measure, which is for prose.

Destinations: photo hero

assets/pages.css:789

The two destination pages open on a photograph under a navy scrim that deepens toward the foot, where the headline sits.

rule.hero-photoThe field: at least 72vh (capped at 820px), its copy at the bottom left.
rule.hero-photo::afterThe scrim: navy at 92% at the foot, 10% at the top.
rule.hero-photo .hero-copyThe headline, lede and two ghost buttons, on the mid measure.

Destinations: chart index

assets/pages.css:807

A 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.indexThe navy section.
rule.index-headFive columns of heading against the lede.
rule.index-bodySix columns of chart against three of key.
modifier.index-body.wideA 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 .matThe chart's frame: a 1px Sea Glass line at 20%.
rule.keyThe numbered list of anchorages. Each row is one link to its entry.
rule.key .kThe numeral, in the pin's colors.
rule.key .name, .whereThe anchorage in Cormorant, and its island in the label voice.
hook.key a.activeIts pin is lit on the chart.

Destinations: pilot entries

assets/pages.css:844

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.entriesThe Canvas section.
rule.entryFour 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-numThe anchorage's number, matching its pin on the index chart.
rule.entry .whereThe place line, in the label voice.
rule.entries-creditThe imagery credit, composed per page from the sources its plates came from.
breakpoint1223pxThe columns go fluid.
breakpoint800pxOne column, inset first.

Chart insets

assets/pages.css:870

The inset: the yacht section's frost 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 yacht 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.insetThe Sea Glass Light mat, with the Float shadow. It wraps the plate on three sides.
var--matThe mat's width: 16px, 14px at 800px and below.
rule.inset figcaptionThe lip: "Chart inset" and the coordinates, in the label voice.
rule.inset .northThe north arrow, with a soft navy cast so it holds over reef and sand.
rule.inset .scalebarThe one-mile bar.
var--nmOne nautical mile as a fraction of the inset's width. Set inline by the generator.

Anchorage map opener

assets/pages.css:895

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 .chartThe zoom-in cursor over the whole plate.
rule.inset-openA 34px round control at the plate's top left. Its ::after grows the hit area by 6px all round.

Map dialog

assets/pages.css:911

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.mapboxThe 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-frameThree rows: heading, map, foot.
rule.mapbox-numThe anchorage's number, in the pin's colors.
rule.mapbox-mapThe box MapKit draws into.
rule.mapbox-failThe 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-closeA 48px solid Sea Glass close button with the Control shadow, fixed to the corner.

Photo band

assets/pages.css:947

A quiet full-bleed plate between the entries and the notes.

rule.bandThe plate: between 320px and 640px tall, the photograph covering it. It carries no caption: the photograph is the point, and a plate over it covered the yacht. What it shows is said in the figure's aria-label and the image's alt text, where it does not sit on the picture.

Between the anchorages

assets/pages.css:958

Practical notes on a navy field: the blueprint plate, multiplied by its gain so it lands on Celestine Navy, under a navy wash at 78%.

rule.betweenThe field.
rule.between::beforeThe wash that quiets the blueprint under the type.
rule.between-innerA heading and lede against a definition list, as in .notes.

See DESIGN.md, The Multiply Gain Rule

Also on the chart

assets/pages.css:983

Cross links between the three inner pages: the other two, each a serif title with an arrow over a one-line description.

rule.moreThe Canvas section, under a Chart Rule hairline.
rule.more aOne link. .t is the title and arrow, .d the description.

Closing

assets/pages.css:1002

Same field as the homepage's booking section: the second blueprint plate under a navy wash at 85%, centered type, and the page's one coral button above a row of secondary actions.

rule.closeThe field, on the emphasis step --section-y-xl.
rule.close h2The page's last word, and the one place the headline scale steps up (to 68px). As on the homepage it stacks a voice line over the task line, which takes 0.6 of the size in Shallow Water, inside the one h2.
rule.close .ledeSet in the body sans here, not Cormorant: a closing paragraph, not an introduction.
rule.close .rowA centered row of buttons, brokers first by the owner's word: Broker partners, the phone and Email, then the one coral button under them. All four share the homepage's 254px minimum width, so no action reads as the runt, and at 768px and below they stack in one column. The two that leave the site carry an up-right arrow (.ext) after the label.
rule.close .brochuresThe letter brochure PDF under the buttons, one link in Sea Glass at 70%.

Deferred plates

assets/pages.css:1030

The two blueprint plates lie at the foot of every inner page and weigh 370KB together, and CSS has no lazy loading for a background image. Each is withheld until pages.js marks its section as within reach (two screens or so); until then the section shows its navy field, which is the color the plate is toned to. Only where script runs: (scripting: enabled) is false with script off, and a browser that does not know the feature skips the block, so both still fetch the plates with the page, as they always did. Screen only, so a printed page has them.

state.between.bg-on, .close.bg-onThe plate may load. Set once by pages.js, and at once under ?static.

Fine print

assets/pages.css:1045

A page's small print, laid on the footer's own Midnight Hull so it reads as the footer's first line rather than as a section of the page. The itineraries page carries the imagery credit here: it is a license credit before it is copy, and belongs where a credit belongs. Inside main, after .close, whose Deep Navy is a shade lighter, so the tone change marks the seam and only a hairline is needed below.

rule.fine-printThe strip. Weight 400, not body's 300: every line here is 14px or smaller on Midnight Hull.
rule.fine-print pOne paragraph, capped at 90ch so a wide screen does not set it in one long line.
rule.fine-print aProse links inherit the field's color, as they do on .between and .close.

See DESIGN.md, The Dark Field Rule

Floating action bar

assets/pages.css:1150

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-ctaHidden: transparent, 16px low, out of the pointer's way, and visibility: hidden, so its three links leave the tab order too (pointer-events alone leaves them in it). The visibility flips at the end of the fade out and at the start of the fade in, as on index.html.
hook.float-cta[data-shown="yes"]Shown. Written by pages.js once the hero has passed, and cleared over the closing section.
var--cta-vvHow far a phone's toolbar or keyboard has pushed the visible bottom edge up. Written by pages.js.
breakpoint769px to 1303pxThe rail, as on index.html. 1304px is --grid-max plus twice the bar's 132px, the first width at which the full bar clears the measure. With a mouse it is three 56px buttons, inside the page margin (72px at its narrowest), each an icon over its label at 1px tracking, never icons alone; on a touch screen it docks along the bottom, as on a phone.
breakpoint768pxA full-width row along the bottom. Call and Email go solid, so the page does not show through them.

Not found

assets/pages.css:1185

404.html, which Cloudflare Pages serves for any path that matches nothing. One navy field the height of the window: a lifebuoy riding three lines of swell, the headline, a paragraph, and the way home. The drawing is inline SVG and everything that moves in it is a transform, so it costs no request and no layout.

rule.lostThe field. Centered both ways, and cleared of the fixed nav.
rule.lost .buoyThe drawing, faded out at both sides so the swell has no ends.
rule.lost .buoy .swellA line of swell, one wavelength (120 units) wider than the drawing, sliding by exactly that.
rule.lost .buoy .foreThe line in front of the buoy. Filled with the field's navy, which is what sinks the buoy into it.
rule.lost .buoy .floatThe buoy, rocking about its own center.
rule.lost .ledeSet in the body sans, as the closing section's is.

Reduced motion

assets/pages.css:1215

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

assets/pages.css:1235

Static capture switch (?static in the URL): every entrance resolved, for screenshots and print. pages.js sets the class before anything else runs.

hookhtml.staticEvery entrance is forced to its end state, with !important, and no transition.

Print

assets/pages.css:1249

The inner pages on paper, to the homepage's rules (assets/home.css, Print): Letter, a running foot, ink on white, and only what reads without a screen. The charts are the exception. Their navy is the content, so every chart plate prints as drawn, with print-color-adjust; the sections around them give up their fills, and their light tokens are redefined as ink so the type turns dark on its own. Inside a chart the tokens are put back.

rule@pageLetter, with the yacht's name, the site's address and the folio in the foot.
rule.hero-photo, .index, .between, .close, .fine-print, footerThe navy surfaces, turned to ink on white.
rule.chart, .matThe plates keep their navy and their Sea Glass linework.
rule.entry, .leg, .insetNever split across a page.
rulea[href^="http"]::afterAn outside link prints its address after it.

Generated by tools/build-docs.py from the comments in the source. To change this page, change the comment and run it again.