/* The site is a night navigation chart, and only that. There is no light
   palette: the one that used to live here was an inversion of this one rather
   than a design of its own — the amber went to rust (#a35c00), the chart-navy
   identity disappeared into near-white, and the grid all but vanished. The
   owner's call is that the page commits to dark, so every colour below is
   defined unconditionally and no colour has its only definition inside a
   media query.

   `color-scheme: dark` is what makes that hold up on a light-set OS: the
   browser paints form controls, scrollbars and the canvas underneath the page
   to match, so a viewer who prefers light gets the intended design rather than
   a half-themed page. */
:root {
  color-scheme: dark;

  --ink: #0a1a2f;
  /* --rule is for hairlines and the background grid, which are meant to read
     as texture and are deliberately near-invisible (1.52:1). --rule-strong is
     the same family pushed past WCAG 1.4.11's 3:1 for meaningful non-text
     content (3.64:1), for the one place the faint version was doing real
     work: the dashed planned course on the route chart, where "where we're
     still going" is half the information the chart carries. Still well below
     --haze, so it stays subordinate to the amber sailed track. */
  --rule: #1d3b5c;
  --rule-strong: #52759a;
  /* Raised surface, for the one element that sits *over* the page rather than
     on it (the ship-note tooltip). Only a shade off --ink — the hairline
     border does the separating, so the card reads as an annotation rather
     than a callout. */
  --surface: #12263f;
  --foam: #e8eff5;
  /* The second voice. The page used to step straight from 144px --foam
     numerals to a 20px --haze caption, with nothing between: the only
     sentence on the page whispered. --mist sits between --haze and --foam so
     the caption can be clearly subordinate to the timer without being
     incidental. 10.51:1 on --ink. */
  --mist: #b9cbdb;
  --haze: #7fa0bc;
  /* One step below --haze, for the footer disclaimer only. On mobile that
     paragraph is the densest block of text on an otherwise sparse page and
     was pulling the eye off the countdown. 4.72:1 on --ink — dimmer, but
     still clear of the 4.5:1 floor for body text, which matters for a notice
     whose whole job is being readable. */
  --faint: #6b88a2;
  --beacon: #ffb23f;
  --sea: #4fc3ba;

  --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas,
    "Liberation Mono", monospace;
  --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
    "Helvetica Neue", sans-serif;

  --gutter: clamp(1.25rem, 5vw, 4rem);
  /* The measured column. Two full-width hairlines (masthead, clock) used to
     promise a wide canvas that nothing below filled: at 1440 the rules ran
     1312px while the caption was 582 and the chart 416, so ~60% of the screen
     was empty grid and the page read as a stretched phone layout. Capping the
     whole block — masthead, main and footer together — and centring it makes
     the rules end where the content ends, so they belong to the same object.
     Below 64rem + two gutters the cap is not in play and the gutter alone
     governs, exactly as before. */
  --measure: 64rem;
  /* The route chart's own cap. It grew from 26rem with the column: 416px was
     small under a 1024px measure. It stays an inset, not a poster — the timer
     is the hero — so it caps at roughly half the column. */
  --chart-max: 34rem;
  /* The strip of column kept clear to the chart's right, for the label that
     overhangs it (St. Thomas sits at 80.5% across, so on a phone its label
     runs past the chart's edge). See the port-label placement rule below. */
  --chart-label-gutter: 2.25rem;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  padding: var(--gutter);
  /* One break value for both the masthead-to-main and main-to-footer gaps, so
     the page's top and bottom breathing room match. It used to be
     clamp(2rem, 6vh, 4rem) plus a stray 1em <p> margin under the disclaimer,
     which made the space below the colophon larger than the space above the
     clock and read as slack rather than as a break. */
  gap: clamp(1.75rem, 4vh, 2.75rem);
  background-color: var(--ink);
  /* Chart grid: hairlines every 48px, quiet enough to read as texture. */
  background-image:
    linear-gradient(var(--rule) 1px, transparent 1px),
    linear-gradient(90deg, var(--rule) 1px, transparent 1px);
  background-size: 48px 48px;
  background-position: -1px -1px;
  color: var(--foam);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.5;
}

/* The grid must not overwhelm the type; fade it toward the edges. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  background: radial-gradient(ellipse at 50% 40%, transparent 0%, var(--ink) 78%);
  pointer-events: none;
}

a {
  color: var(--sea);
  text-underline-offset: 0.2em;
}

a:focus-visible,
:focus-visible {
  outline: 2px solid var(--beacon);
  outline-offset: 3px;
}

/* The measured column: the three top-level bands are the same object, so they
   share one width and one centre line. Everything inside them still starts at
   the column's left edge — this is a narrow document, not a centred one. */
.masthead,
main,
footer {
  width: 100%;
  max-width: var(--measure);
  margin-inline: auto;
}

.masthead {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--haze);
}

main {
  flex: 1;
  display: flex;
  flex-direction: column;
  /* The floor is 2.5rem rather than 2rem because two chart labels are
     translated outside the chart's box — Fort Lauderdale's above it, Antigua's
     below — so this gap is also the clearance between the caption and FLL, and
     between ANU and the colophon. At 2rem on a 320x568 phone those boxes met. */
  gap: clamp(2.5rem, 6vh, 3.5rem);
  justify-content: center;
}

/* ---- Readout ---- */

.clock {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: clamp(0.75rem, 4vw, 2.5rem);
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--rule);
}

/* The label is centred under its numeral rather than flush with the flex
   item's left edge. Flush left aligned the label to the *advance* box, but a
   numeral's ink starts inside that box by its left side bearing, so every
   label floated left of the number it names — and by a different amount each
   time, because the bearing is per-glyph: measured at 144px, "1" carries a
   14.63px left bearing, "0" 9.35px and "4" 7.17px, so the days label drifted
   twice as far as the seconds label and the drift changed as the count
   ticked. Tabular figures have near-symmetric bearings, so centring is a
   single rule that lands within ~3px of true optical centre for every value
   without measuring anything at runtime. */
.unit {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
}

.unit__value {
  font-family: var(--mono);
  font-size: clamp(3.25rem, 14vw, 9rem);
  font-weight: 500;
  line-height: 0.9;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}

.unit--live .unit__value {
  color: var(--beacon);
}

.unit__label {
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--haze);
}

/* The page's only sentence, and the one step between the 144px numerals and
   the 11px labels. At 20px in --haze it was a caption on a photograph; it is
   sized and toned here to read as the second voice. */
/* The <h1>'s default 0.67em margin would set the block off-centre in main. */
.notfound {
  margin: 0;
}

.caption,
.notfound__detail {
  margin: 0.9rem 0 0;
  font-size: clamp(1.125rem, 0.9rem + 1.2vw, 1.75rem);
  line-height: 1.35;
  color: var(--mist);
  max-width: 34ch;
  /* Two lines of a short sentence look better even than ragged. At 390 the
     natural wrap left "Lauderdale" alone on line two; balanced it reads
     "until JoCo Cruise 2027" / "departs Fort Lauderdale". A no-op from 500px
     up, where the sentence sets on one line. */
  text-wrap: balance;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.nojs {
  display: none;
  margin: 0;
  font-family: var(--mono);
  font-size: 1.25rem;
}

.no-js .clock {
  display: none;
}

.no-js .nojs {
  display: block;
}

/* The caption is the tail of a sentence whose subject is the clock ("...
   until the cruise departs Fort Lauderdale"). With no clock above it, it
   reads as a dangling fragment after the .nojs paragraph, which already
   states the departure as a complete sentence. */
.no-js .caption {
  display: none;
}

/* Phases: the clock is only meaningful while counting. */
.is-aboard .clock,
.is-home .clock,
.is-over .clock {
  display: none;
}

/* .notfound is 404.html's heading. It borrows the phase captions' voice by
   joining their selector list rather than a phase class, which would drag
   along behavior that page has no use for. */
.is-aboard .caption,
.is-home .caption,
.is-over .caption,
.notfound {
  font-size: clamp(1.75rem, 6vw, 3rem);
  color: var(--foam);
  line-height: 1.15;
  /* No measure of their own: "Almost home!", "Welcome back to land!" and
     "It's over. See you next year?" are 12–29 characters and set on one line
     in the column at every width from 390 up (at 320 the column itself is the
     only cap they need). Only the aboard caption is long enough to need one. */
  max-width: none;
}

/* The aboard caption is the one long sentence, and the only one whose wrap
   has to be steered: "🚢 You're on the boat.* Day 5 of 8."
   The old flat 20ch cap was tuned for a phone and followed the page onto the
   desktop, where it measured 582px against a 685px sentence and orphaned
   "of 8." on a line of its own.
   The rule now has two states, because the good outcomes are discontinuous —
   there is no single measure that both holds the break after the asterisk on
   a phone and lets the whole sentence set on one line on a desktop:
     - up to 46rem, cap the measure so the line breaks after the asterisk
       ("🚢 You're on the boat.*" / "Day 5 of 8."). 17ch is wide enough for
       the first half at every size in that range and too narrow for the
       following "Day", which is what pins the break;
     - from 46rem up, the column is wider than the whole sentence (630px of
       text in a 662px column at the breakpoint itself), so no cap at all —
       one line, no orphan. */
.is-aboard .caption {
  max-width: 17ch;
  /* Not balanced: balancing this sentence picks the most even split, which is
     "🚢 You're on the" / "boat.* Day 5 of 8." — even, and wrong. The measure
     above puts the break where the sentence wants it. */
  text-wrap: wrap;
}

@media (min-width: 46rem) {
  .is-aboard .caption {
    max-width: none;
  }
}

/* The caption block is the tooltip's positioning context, and the area the
   pointer may move within while the tooltip stays open. The asterisk sits
   inline in a caption that wraps, so anchoring the card to the asterisk
   itself would put it mid-line and overflow narrow viewports; anchoring to
   the block instead keeps it inside the gutter at every width. */
.caption-block {
  position: relative;
}

/* The "you're on the boat" footnote marker: small and superscript so it
   reads as an aside, not part of the sentence, and muted so it doesn't
   compete with the caption's beacon-adjacent prominence. It is a <button>,
   not a link — it opens a tooltip and has nowhere to navigate to — so the
   button chrome is reset away to leave just the glyph. */
.ship-note-ref {
  font: inherit;
  font-size: 0.45em;
  vertical-align: super;
  color: var(--haze);
  background: none;
  border: 0;
  padding: 0;
  margin-left: 0.05em;
  cursor: pointer;
  position: relative;
}

.ship-note-ref:hover {
  text-decoration: underline;
}

/* The glyph alone is an 8.9 x 24.5px tap target — legal under WCAG 2.2's
   inline exception, but a genuinely hard thing to hit with a thumb. This
   expands the hit box past 24px in both axes. It is an absolutely positioned
   pseudo-element rather than padding on the button because a <button> is
   inline-block, so vertical padding on it *would* grow the caption's line
   box; an out-of-flow pseudo-element cannot affect layout at all, and hit
   testing still resolves to the button. */
.ship-note-ref::after {
  content: "";
  position: absolute;
  inset: -0.7rem;
}

/* A quiet chart-annotation card, in the manner of the route labels: raised
   surface, hairline in the chart stroke token, small muted text. Absolutely
   positioned so opening it never shifts the page. */
.ship-note {
  /* The tooltip exists only where the caption's asterisk does: the aboard
     phase, excluding debarkation morning, whose caption says nothing about a
     boat and so raises no question to answer. */
  display: none;
  position: absolute;
  /* Above the caption, not below it. Below is the obvious placement and was
     the first attempt, but the chart's Fort Lauderdale label is translated up
     out of the chart box into exactly that band, and the card covered it
     completely at all four test widths (89.3 x 24.6px at 1440, 89.3 x 36.1px
     at 320 — the whole label). There is no room between the caption and that
     label to fit the card: 29px of gap at 320px against a 57px card. Above
     the caption is empty space at every size tested except a very short
     phone. See the report for both sets of measurements. */
  bottom: calc(100% + 0.5rem);
  left: 0;
  z-index: 1;
  /* min() so the card can never be wider than the column it sits in, at any
     viewport — this is what keeps it inside the gutter at 320px. */
  max-width: min(27rem, 100%);
  margin: 0;
  padding: 0.6rem 0.75rem;
  background: var(--surface);
  border: 1px solid var(--rule-strong);
  border-radius: 2px;
  font-family: var(--mono);
  font-size: 0.75rem;
  font-style: italic;
  line-height: 1.5;
  color: var(--haze);
  /* Closed state. visibility (not display) so the transition can run, and so
     the box is measurable; aria-describedby still resolves against a hidden
     element, which is why the description works in either state. */
  visibility: hidden;
  opacity: 0;
  transition:
    opacity 120ms ease,
    visibility 120ms;
}

/* main.js owns this attribute — a single source of truth covering hover,
   keyboard focus, tap, Escape-to-dismiss and outside-click. See the comment
   above the ship-note state machine there for why it is not CSS :hover. */
.ship-note[data-open="true"] {
  visibility: visible;
  opacity: 1;
}

/* Bridges the 0.5rem gap between the card and the caption block, so a pointer
   travelling from the asterisk up to the card never crosses dead space. It is
   a descendant of .ship-note and therefore of .caption-block, so crossing it
   does not fire the block's pointerleave — which is what would otherwise
   close the card mid-journey, the exact failure WCAG 1.4.13's Hoverable
   clause is about. Inert when the card is hidden, since visibility: hidden
   suppresses hit testing on the pseudo-element too. */
.ship-note::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  height: 0.5rem;
}

.is-aboard:not(.is-debarkation-day) .ship-note {
  display: block;
}

/* ---- Route strip (signature) ----
   A small equirectangular chart: an SVG draws the planned course, the
   amber-progress course sailed, and the four waypoint dots; an absolutely
   positioned <ol> overlays the code/name labels at the same coordinates.
   Labels are ordinary HTML text (not SVG <text>) on purpose — SVG content
   scales with the viewBox, so text baked into the SVG would shrink below
   legible sizes on narrow viewports. Keeping labels as HTML sized in rem
   keeps them legible at any width while the chart itself scales freely. */

/* No `width: 100%`: the chart is a plain block, so `margin-right` genuinely
   shortens it. That reserved strip is the chart's label gutter — the space
   St. Thomas's label overhangs into on a phone. Once the column is wide
   enough for --chart-max the cap takes over and the reservation costs
   nothing. */
.route__chart {
  position: relative;
  max-width: var(--chart-max);
  margin-right: var(--chart-label-gutter);
  /* Makes 1cqw == 1% of the chart's rendered width, so the port labels'
     horizontal offsets below can be expressed relative to the chart instead
     of to the root font size. See the .port--* rules for why that matters. */
  container-type: inline-size;
}

.route__map {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 2 / 1;
  overflow: visible;
}

.route__course {
  fill: none;
  stroke: var(--rule-strong);
  stroke-width: 0.6;
  stroke-linecap: round;
  stroke-dasharray: 0.01 0.014;
}

/* The course actually sailed, drawn over the planned course in beacon
   amber. pathLength="1" on both paths normalizes distance-along-path to
   0..1 regardless of the curve's real rendered length, so main.js can set
   stroke-dashoffset directly from the measured progress fraction. */
.route__progress {
  fill: none;
  stroke: var(--beacon);
  stroke-width: 0.9;
  stroke-linecap: round;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  transition: stroke-dashoffset 600ms ease;
}

.route__dot {
  fill: var(--ink);
  stroke: var(--haze);
  stroke-width: 0.5;
  transition:
    fill 300ms ease,
    stroke 300ms ease;
}

.route__dot--reached {
  fill: var(--beacon);
  stroke: var(--beacon);
}

/* Measurement-only paths (see index.html); never rendered. */
.route__leg {
  fill: none;
  stroke: none;
}

.route__list {
  position: absolute;
  inset: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

.port {
  position: absolute;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  width: max-content;
  white-space: nowrap;
  /* Defaults for the placement rule below; .port--hmc overrides the first. */
  --label-gap-x: 2.3cqw;
  --label-gap-y: 0.6rem;
}

.port__code {
  font-family: var(--mono);
  font-size: 0.8125rem;
  letter-spacing: 0.12em;
  color: var(--foam);
}

.port__name {
  font-size: 0.75rem;
  line-height: 1.25;
  color: var(--haze);
}

.port--reached .port__code,
.port--reached .port__name {
  color: var(--beacon);
}

/* ---- Port label placement rule ----
   Read this before moving a label. The placements below are a rule with two
   named exceptions, not four independent decisions; the previous set was the
   latter and looked improvised (Fort Lauderdale flush above its dot, Half Moon
   Cay and St. Thomas above-right at two different offsets, Antigua out to the
   left and a little below).

   THE RULE. A label occupies one quadrant of its dot, offset by
   --label-gap-x horizontally and --label-gap-y vertically, so the dot reads
   as the label's inner corner. Quadrants are tried in this fixed order and
   the first free one wins:

     1. above-right   — the default. FLL, HMC, STT.
     2. above-left    — when the label's right edge would pass the chart's
                        right edge. The exact mirror: same two gaps, and
                        text-align flips with it.
     3. below-left    — when the flipped label would still collide with a
                        neighbour's. ANU only, whose above-left box overlaps
                        St. Thomas's by 5px at --chart-max.

   below-right is deliberately never used: the outbound leg leaves every dot
   heading down and to the right, so that quadrant is always on the route.

   Within the chosen quadrant a label may be pushed further along the
   horizontal gap to clear a neighbour's label — a bigger offset, never a new
   direction. HMC is the only one that needs it; see below.

   THE UNITS. A gap between a label and its own dot is in cqw (percent of the
   chart's rendered width) so it stays the same fraction of the chart at every
   size. A rem gap drifts as the chart scales, which leaves a label looking
   adrift from the dot it names, with no leader line to connect the two.
   Vertical gaps stay in rem: they separate lines of type from a dot, and
   should track the type, which is rem-sized.

   ST. THOMAS no longer flips below 400px. It used to, because its right-hand
   label ran within 0.9px of the screen edge at 320px — but its left is where
   the return leg to Half Moon Cay departs, so the flipped label sat directly
   on the dashed route, which is the collision this rule was rewritten to fix.
   Instead the chart reserves --chart-label-gutter of column to its right (see
   .route__chart) and St. Thomas stays above-right at every width.

   HALF MOON CAY needs the pushed-out horizontal gap. Its dot is 21.13% of the
   chart's width from Fort Lauderdale's, while "Fort Lauderdale" is a fixed
   89.3px of rem-sized text at every viewport — so on a phone the two dots are
   closer together than one label is wide. The offset it needs is therefore
   FLL's label width, plus 10px of clearance, minus the dot separation:

     6.21rem - 18.83cqw   ==   (89.3px + 10px + 0.063 * W) - 0.2513 * W

   which shrinks as the chart grows and falls below the ordinary gap past
   ~470px of chart, where max() hands that gap back. Re-measure FLL against
   HMC at 320px if you touch either label's text.

   Measured clearances after this rewrite, worst case across 320/375/390/768/
   1024/1440/1920: label to route 7.6px, label to label 10.0px (FLL/HMC at
   320), and no label past either edge of the column. */

/* 1. above-right */
.port--fll,
.port--hmc,
.port--stt {
  transform: translate(var(--label-gap-x), calc(-100% - var(--label-gap-y)));
  text-align: left;
}

/* ...with Half Moon Cay pushed clear of Fort Lauderdale's label. */
.port--hmc {
  --label-gap-x: max(2.3cqw, 6.21rem - 18.83cqw);
}

/* 3. below-left */
.port--anu {
  transform: translate(calc(-100% - var(--label-gap-x)), var(--label-gap-y));
  text-align: right;
}

/* ---- Footer matter ---- */

.colophon {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.vessel,
.official {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.875rem;
  color: var(--haze);
}

/* The closing rule belongs to the column, not to the paragraph. It used to be
   a border on .disclaimer, so it stopped at the 68ch measure — a 514px
   hairline sitting under a 1312px one, which was half of why the page read as
   two unrelated widths. The rule now brackets the page against the masthead's;
   the 68ch measure still governs the text inside it. */
footer {
  padding-top: 1rem;
  border-top: 1px solid var(--rule);
}

.disclaimer {
  /* Explicit: the default 1em <p> margin was adding an unowned 12px on top of
     the body gap above this rule. */
  margin: 0;
  font-size: 0.6875rem;
  line-height: 1.65;
  color: var(--faint);
  max-width: 68ch;
}

/* Once the paragraph dimmed, the teal link was the brightest thing in the
   footer and the eye went straight back to it. Here the underline carries the
   affordance and the whole block recedes together. The link is still
   distinguishable without relying on colour. */
.disclaimer a {
  color: inherit;
  text-decoration-color: var(--rule-strong);
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
}
