/* Static pages (About / Examples / Pricing / Terms / Privacy, and the two
   /pro pages). Deliberately standalone — no bundle, no App.css: these must
   render even if the app never boots, and crawlers should get plain HTML.
   /tokens.css is linked first on every page and holds the brand primitives
   (and the wordmark @font-face) the app shares; everything below is page
   semantics built out of them.

   Two registers share this sheet:
   · document (Terms, Privacy) — one measure, generous leading, a contents
     list, no decoration. Body class `doc`.
   · product (About, Examples, Pricing, /pro/*) — the same type, plus live
     embeds of Voyarc itself and the pricing columns. Body class `product`. */

:root {
  color-scheme: light dark;
  --paper: var(--voyarc-paper, #ffffff);
  --ink: var(--voyarc-ink, #2c2c2c);
  --muted: var(--voyarc-muted-light, #5c6270);
  --rule: rgba(44, 44, 44, 0.14);
  --accent: var(--voyarc-pink, #d6336c);
  /* The accent, for small text that sits on a plate rather than on the page:
     --accent on --surface is 4.23:1 and on the tinted Pro column 3.83:1, both
     under AA. Buttons, and accent text on paper, keep --accent — it is 4.62:1
     there and it is the brand colour. */
  --accent-on-surface: var(--voyarc-pink-deep, #c6285f);
  /* Text on an --accent fill. Paper in the light register; in the dark one
     the fill is the lift, and white on it is 2.9:1, so the ink is the page
     itself — midnight on the lift is 6.0:1. */
  --on-accent: var(--voyarc-paper, #ffffff);
  /* Plates: contents lists, code blocks, the frame an embed sits in. */
  --surface: #f4f5f8;
  --surface-line: rgba(44, 44, 44, 0.1);
  /* The reading measure, 65–70ch: one number, so both registers agree. */
  --measure: 68ch;
  /* The route log's four changes, each a text/background pair for the chip
     and a lit line for the map. The map colours are read on Midnight only,
     so they do not turn over in dark mode; the chips sit on paper and do. */
  --chip-new: #1f7a4d;
  --chip-new-bg: #e3f4ea;
  --chip-back: #1c5fa8;
  --chip-back-bg: #e2edf9;
  --chip-more: #6a4fb3;
  --chip-more-bg: #ece7f8;
  --chip-cut: #b3261e;
  --chip-cut-bg: #fbe5e3;
  --map-new: #4cd48a;
  --map-back: #6fb3ff;
  --map-more: #c9a8ff;
  --map-cut: #ff6b61;
  /* The ground those maps are drawn on: the app's own Dark look, sea, earth
     and borders. Read on the map plate only, so they do not turn over in dark
     mode either. Every generated page sets these in its head straight from
     HOUSE_COLORS.dark (src/lib/houseStyle.ts); these are what anything else
     using the sheet falls back to. */
  --log-water: #0d1119;
  --log-land: #1f2632;
  --log-border: #303a48;
  /* The hub maps' ground and routes: the app's Light look, and the house
     neutral with the farthest route in amber — the colours "Open this map in
     Voyarc" opens with. Set in each hub page's head from HOUSE_COLORS and
     src/lib/hubs.ts by scripts/build-hubs.mjs; like the log's, they do not
     turn over in dark mode. */
  --hub-water: #cfdbe3;
  --hub-land: #f3efe6;
  --hub-border: #d3cbbd;
  --hub-route: #343a40;
  --hub-far: #f08c00;
}

@media (prefers-color-scheme: dark) {
  :root {
    --paper: var(--voyarc-midnight, #060913);
    --ink: #e9eaf0;
    --muted: var(--voyarc-muted-dark, #a6abbc);
    --rule: rgba(255, 255, 255, 0.14);
    --accent: var(--voyarc-pink-lift, #f06595);
    /* The dark register's plate is near-black, so the lift already clears AA
       on it — 6.3:1 on --surface, 5.8:1 on the tinted Pro column — and the
       two are the same colour again. */
    --accent-on-surface: var(--voyarc-pink-lift, #f06595);
    --on-accent: var(--voyarc-midnight, #060913);
    --surface: #0d1120;
    --surface-line: rgba(255, 255, 255, 0.09);
    --chip-new: #7fd6a6;
    --chip-new-bg: #143424;
    --chip-back: #8fbdf2;
    --chip-back-bg: #142a45;
    --chip-more: #c3b2f2;
    --chip-more-bg: #2a2246;
    --chip-cut: #f29a94;
    --chip-cut-bg: #46201d;
  }
}

* {
  box-sizing: border-box;
}

/* Several controls here ship hidden and are unhidden by script (the billing
   toggle, "Manage plan", the staging restore link). An author
   `display` rule outranks the UA sheet's [hidden] whatever its specificity,
   so the sheet has to say it itself — otherwise a page without JavaScript
   shows every one of them, dead. */
[hidden] {
  display: none !important;
}

/* Said to a screen reader and to nothing else — the comparison table's
   caption, which the columns already say in type. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The first thing in every page's tab order, and the first child of every
   body: one key past the header's six nav links and into the page itself.
   Parked off the top rather than `display: none` — a link nothing can focus
   is a link nothing can use — and it comes down over the header when it is. */
.skip-link {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 20;
  padding: 10px 18px;
  border-radius: 0 0 10px 0;
  background: var(--accent);
  color: var(--voyarc-paper, #fff);
  font: 600 0.92rem/1.3 var(--font-ui, system-ui, sans-serif);
  text-decoration: none;
  transform: translateY(-120%);
}

.skip-link:focus {
  transform: translateY(0);
}

/* <main> is the target, so it takes focus (tabindex="-1") and the next Tab
   carries on inside it. The ring would be a box round the whole page and says
   nothing the jump has not already said. */
main:focus {
  outline: none;
}

/* Chrome on macOS can leave the page stuck rubber-banded above its top after
   a hard trackpad flick (blank band above the header, nav unclickable until
   reload). The doc pages have nothing to pull down to, so refuse to overscroll.
   Vertical only: a `pre` block or the plan table still scrolls sideways. */
html {
  overscroll-behavior-y: none;
}

body {
  margin: 0;
  padding: 28px 20px 64px;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-ui, system-ui, sans-serif);
  /* ~17px: these are pages you read, not a panel you work in. */
  font-size: 1.0625rem;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
}

/* One width for every page: the route log's, which is also the width Examples
   needs for its 16:9 embeds. Header, nav and footer line up across the set. */
.wrap {
  max-width: 62rem;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 28px;
}

/* The container is that width everywhere, so a block that is meant to be read
   says so and keeps the reading measure. Every public page wraps its copy in
   one, and a page may hold several: a figure, a table or an embed sits between
   them at the container's full width. */
.wrap .prose {
  max-width: var(--measure);
}

/* ---------- shared header / footer ---------- */

.page-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 6px 20px;
  padding-bottom: 18px;
  border-bottom: 1px solid var(--rule);
}

.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font: 700 1.05rem/1.2 var(--font-brand, system-ui, sans-serif);
  letter-spacing: -0.01em;
  color: var(--ink);
  text-decoration: none;
}

.wordmark svg {
  color: var(--accent);
}

.head-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 16px;
  font-size: 0.9rem;
}

.head-nav a {
  color: var(--muted);
  text-decoration: none;
}

.head-nav a:hover {
  color: var(--accent);
  text-decoration: underline;
}

.head-nav a[aria-current='page'] {
  color: var(--ink);
  font-weight: 600;
}

/* The way into the thing every one of these pages is about. It is the last
   item of the nav rather than a floating button, so on a phone — where the
   six links wrap to two rows — it wraps with them and is on screen at 360px
   like everything else; nothing here scrolls sideways. The look is the plan
   buttons' (.cta.primary), at nav size, and it never takes aria-current: the
   map is not one of the pages. */
.head-nav .nav-cta {
  padding: 4px 12px;
  border: 1px solid var(--accent);
  border-radius: 10px;
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
  white-space: nowrap;
}

.head-nav .nav-cta:hover {
  color: var(--on-accent);
  text-decoration: none;
  filter: brightness(1.06);
}

.page-foot {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-top: 18px;
  border-top: 1px solid var(--rule);
  font-size: 0.85rem;
  line-height: 1.6;
  color: var(--muted);
}

.page-foot p {
  margin: 0;
  text-wrap: pretty;
}

.foot-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 16px;
}

/* The two accounts that exist, icon-only like the app panel: the muted
   footer grey at rest, the accent on hover, glyphs the size of the text. */
.foot-social {
  display: flex;
  align-items: center;
  gap: 14px;
}

.foot-social a {
  display: inline-flex;
  align-items: center;
  color: var(--muted);
  transition: color 0.15s;
}

.foot-social a:hover {
  color: var(--accent);
}

.foot-social svg {
  width: 14px;
  height: 14px;
  flex: none;
}

/* ---------- back to top ---------- */

/* The way back up a long page, floating clear of the measure. It ships
   `hidden` and the page's own script unhides it, so a reader without
   JavaScript never gets a button that cannot scroll; past the first screen
   the script adds `show` and it fades in. Above the pricing table's sticky
   row heads (z-index 1) and nothing else on these pages. */
.to-top {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 5;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border: 1px solid var(--surface-line);
  border-radius: 50%;
  background: var(--paper);
  color: var(--ink);
  box-shadow: 0 4px 16px rgba(6, 9, 19, 0.18);
  /* Out of the tab order and out of the accessibility tree until it is worth
     reaching: `visibility` cannot tween, so it is held to the end of the fade
     on the way out and dropped at once on the way in. */
  visibility: hidden;
  opacity: 0;
  transform: translateY(8px);
  pointer-events: none;
  cursor: pointer;
  transition: opacity 0.18s ease, transform 0.18s ease,
    visibility 0s linear 0.18s;
}

.to-top.show {
  visibility: visible;
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
  transition-delay: 0s;
}

.to-top:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.to-top:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .to-top {
    transition: none;
  }
}

/* A phone has less room beside the text, and a thumb reaches the corner. */
@media (max-width: 760px) {
  .to-top {
    right: 14px;
    bottom: 14px;
  }
}

/* ---------- text ---------- */

/* Block layout, not flex: adjacent heading/paragraph margins should collapse
   rather than stack into airy gaps between sections. */
main {
  display: block;
}

h1 {
  margin: 0 0 6px;
  font: 700 clamp(1.75rem, 1.3rem + 2vw, 2.3rem) / 1.15 var(--font-brand, system-ui, sans-serif);
  letter-spacing: -0.02em;
  text-wrap: balance;
}

h2 {
  margin: 34px 0 8px;
  font: 700 1.15rem/1.3 var(--font-brand, system-ui, sans-serif);
  letter-spacing: -0.01em;
  text-wrap: balance;
}

h3 {
  margin: 22px 0 6px;
  font: 700 0.98rem/1.35 var(--font-brand, system-ui, sans-serif);
}

p,
ul,
ol {
  margin: 0 0 16px;
}

ul,
ol {
  padding-left: 22px;
}

li {
  margin-bottom: 6px;
}

.lede {
  font-size: 1.15rem;
  line-height: 1.6;
  color: var(--muted);
  text-wrap: pretty;
}

.meta {
  font-size: 0.85rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

a {
  color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}

code {
  padding: 1px 5px;
  border-radius: 5px;
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  font-family: var(--font-mono, monospace);
  font-size: 0.9em;
  white-space: nowrap;
}

/* File samples (the FAQ's format previews). A CSV row that wraps stops being
   a CSV row, so these scroll sideways inside their own box rather than
   pushing the page wider than the phone it is read on. */
pre {
  margin: 0 0 18px;
  padding: 12px 14px;
  overflow-x: auto;
  border: 1px solid var(--surface-line);
  border-radius: 10px;
  background: var(--surface);
  font-family: var(--font-mono, monospace);
  font-size: 0.82rem;
  line-height: 1.55;
}

/* ---------- document register (Terms, Privacy) ---------- */

.doc main {
  counter-reset: section;
}

.doc .lede {
  margin-bottom: 26px;
}

/* Numbers come from a counter, not the copy: the legal wording is untouched
   and the contents list below can't drift out of step with it. The sections
   are the reading block's own headings, so the contents list's own “On this
   page” heading, nested in the .toc, is not one of them and takes no number. */
.doc main .prose > h2 {
  counter-increment: section;
  scroll-margin-top: 18px;
}

.doc main .prose > h2::before {
  content: counter(section) '. ';
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.toc {
  margin: 0 0 6px;
  padding: 16px 20px 6px;
  border: 1px solid var(--surface-line);
  border-radius: 10px;
  background: var(--surface);
}

.toc h2 {
  margin: 0 0 10px;
  font: 700 0.75rem/1.3 var(--font-brand, system-ui, sans-serif);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
}

.toc ol {
  margin: 0 0 10px;
  padding-left: 22px;
  columns: 2;
  column-gap: 28px;
  font-size: 0.94rem;
  line-height: 1.55;
  font-variant-numeric: tabular-nums;
}

.toc li {
  margin-bottom: 4px;
  break-inside: avoid;
}

.toc a {
  color: var(--ink);
  text-decoration-color: var(--rule);
}

/* The box is a plate (--surface), so a hovered entry takes the deeper pink:
   the accent clears 4.5:1 on white and not on the plate. */
.toc a:hover {
  color: var(--accent-on-surface);
}

@media (max-width: 640px) {
  .toc ol {
    columns: 1;
  }
}

/* ---------- product register (About, Examples) ---------- */

/* The jump list under the Examples intro. Ten 16:9 maps and their copy is ten
   thousand pixels of page, so the names go in a row at the top and every
   section is one click rather than six scrolls. Plain anchors: no script, and
   a reader without JavaScript loses nothing. */
.jump {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 40px;
}

.jump a {
  padding: 5px 11px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  color: var(--ink);
  font-size: 0.85rem;
  text-decoration: none;
}

.jump a:hover {
  border-color: var(--ink);
}

/* Landing on an anchor puts the heading hard against the top of the screen;
   a little air above it reads as a section rather than a jump cut. */
.product main section[id] {
  scroll-margin-top: 16px;
}

.example {
  margin: 0 0 52px;
  padding-top: 8px;
}

.example > h2 {
  margin-top: 0;
  font-size: 1.35rem;
}

/* The hero: Black Marble on the globe, full width of the wide wrap, on its
   own dark plate whichever way the OS is leaning. */
.example.hero {
  margin-bottom: 60px;
  padding: 22px 22px 6px;
  /* A hairline, because in dark mode the plate and the page are the same
     midnight and the hero would otherwise have no edge at all. */
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 14px;
  background: var(--voyarc-midnight, #060913);
  color: #e9eaf0;
}

.example.hero h2,
.example.hero .story {
  color: #e9eaf0;
}

.example.hero .numbers,
.example.hero .caveat {
  color: #a6abbc;
}

.example.hero a {
  color: var(--voyarc-pink-lift, #f06595);
}

.example.hero .route-code code {
  background: rgba(255, 255, 255, 0.08);
  color: #e9eaf0;
}

.example.hero .copy {
  border-color: rgba(255, 255, 255, 0.18);
  color: #e9eaf0;
}

/* The numbers sentence: figures line up, so tabular. */
.numbers {
  margin: 0 0 14px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  text-wrap: pretty;
}

/* Route text is monospace (typography doctrine) and must wrap: a 24-route
   fan-out is 200 characters, and nothing on these pages scrolls sideways. */
.route-code {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 8px;
  margin: 0 0 16px;
}

.route-code code {
  /* Shrink-to-fit until the text is long enough to need the width — a
     two-route example shouldn't sit in a 900px box. */
  flex: 0 1 auto;
  max-width: 100%;
  min-width: 0;
  padding: 8px 10px;
  border-radius: 8px;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-size: 0.86rem;
  line-height: 1.5;
}

.copy {
  flex: 0 0 auto;
  padding: 7px 12px;
  border: 1px solid var(--rule);
  border-radius: 8px;
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: 0.82rem;
  cursor: pointer;
}

.copy:hover {
  border-color: var(--accent);
  color: var(--accent);
}

/* 16:9, sized by the box rather than the content, so the page doesn't reflow
   when a section swaps its poster for the live map. */
.embed {
  position: relative;
  aspect-ratio: 16 / 9;
  margin: 0 0 14px;
  border: 1px solid var(--surface-line);
  border-radius: 10px;
  overflow: hidden;
  background: var(--surface);
}

.embed iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

/* The still each frame wears until the section is scrolled to — a capture of
   the very map the iframe boots (npm run build:examples -- --posters), so the
   swap lands on the picture that was already there. Without JS it stays, and
   it is a link to the same map in Voyarc. */
.poster {
  position: absolute;
  inset: 0;
  display: block;
  color: #fff;
}

.poster img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Said before it is live: this map moves. */
.poster-play {
  position: absolute;
  top: 50%;
  left: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 54px;
  height: 54px;
  /* The triangle's mass sits left of its box; nudge it back to centre. */
  padding-left: 4px;
  border-radius: 50%;
  background: rgba(12, 14, 22, 0.55);
  box-shadow: 0 2px 14px rgba(0, 0, 0, 0.3);
  /* Its own colour, not the link's: the hero's pink is for text. */
  color: #fff;
  transform: translate(-50%, -50%);
}

.poster:hover .poster-play {
  background: var(--accent);
}

.embed.live .poster {
  display: none;
}

/* The comparison under the Examples hero. Every embed on our own page is the
   licensed one (our origin is always licensed to itself), so the free result
   has to be shown beside it rather than described — two 16:9 frames while
   there is room for them, stacked on a phone. */
.compare-embeds {
  margin: 0 0 52px;
}

.compare-embeds > h2 {
  margin-top: 0;
  font-size: 1.35rem;
}

.compare {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
  margin: 0 0 12px;
}

.compare figure {
  /* min-width: a grid track otherwise takes its floor from the iframe. */
  min-width: 0;
  margin: 0;
}

.compare .embed {
  margin: 0;
}

.compare figcaption {
  margin-top: 8px;
  color: var(--muted);
  font-size: 0.9rem;
  text-wrap: pretty;
}

@media (max-width: 820px) {
  .compare {
    grid-template-columns: minmax(0, 1fr);
  }
}

.example .story {
  margin: 0 0 12px;
  text-wrap: pretty;
}

/* The Race section reads like an example but cannot be one: no URL carries a
   flown track, so it is a still and its copy, never a live embed. Its own
   class keeps it out of every ".example" rule and assertion. */
.race {
  margin: 0 0 52px;
  padding-top: 8px;
}

.race > h2 {
  margin-top: 0;
  font-size: 1.35rem;
}

.race .story {
  margin: 0 0 12px;
  text-wrap: pretty;
}

.caveat {
  font-size: 0.9rem;
  color: var(--muted);
}

.open-in-app {
  display: inline-block;
  margin: 0 0 14px;
  font-weight: 600;
  text-decoration: none;
}

.open-in-app:hover {
  text-decoration: underline;
}

figure {
  margin: 0 0 22px;
}

figcaption {
  margin-top: 8px;
  font-size: 0.9rem;
  color: var(--muted);
  text-wrap: pretty;
}

/* ---------- pricing (product register) ---------- */

/* One comparison table, three columns: the plans are read across, so they are
   one table rather than three lists that have to be kept in step. It never
   makes the page scroll sideways — the wrapper scrolls instead, and the
   feature column stays put while it does. */
/* Said in words at the width where it is true — a phone shows the labels and
   most of Free, and nothing about the page says the other two plans are a
   swipe away. Above the table, because a caption under it is read after the
   reader has already decided there is nothing else. */
.swipe-hint {
  display: none;
  margin: 0 0 8px;
  font-size: 0.85rem;
  color: var(--muted);
}

@media (max-width: 700px) {
  .swipe-hint {
    display: block;
  }
}

.plan-matrix-wrap {
  /* Flex so the fade below can be a flex item of its own: a pseudo-element in
     a scroll container is laid out with the content and scrolls away with it,
     and the cue has to stay at the edge of the screen. */
  display: flex;
  margin-bottom: 20px;
  overflow-x: auto;
  /* Room for the sticky column's shadow, and for a focus ring on a CTA. */
  padding-bottom: 4px;
}

/* The cue: a fade at the right edge for as long as there is more table to the
   right of it. Sticky, so it rides the scrollport rather than the table, and
   pulled back over the last 44px by its own negative margin so it adds
   nothing to the scroll width. Off until the page's own script turns it on
   (build-pricing.mjs) — without JavaScript there is nothing keeping it honest,
   and a fade that never moves is worse than no fade. */
.plan-matrix-wrap::after {
  content: '';
  position: sticky;
  right: 0;
  flex: none;
  align-self: stretch;
  width: 44px;
  margin-left: -44px;
  background: linear-gradient(to right, transparent, var(--paper));
  opacity: 0;
  transition: opacity 0.18s ease;
  pointer-events: none;
}

.plan-matrix-wrap.more::after {
  opacity: 1;
}

.plan-matrix {
  width: 100%;
  /* Separate, not collapsed: a collapsed table drops border-radius, and the
     column heads are plates like the cards they replace. */
  border-collapse: separate;
  border-spacing: 0;
  /* Fixed, so the three plans are three columns of exactly the same width and
     the labels take what is left: the heads are compared across, and columns
     sized by their own copy would put the prices at three widths. */
  table-layout: fixed;
  /* Below this the columns would squeeze the blurbs into slivers; the wrapper
     scrolls instead. A plan column bottoms out at 8.88rem — the width four of
     them had at 48rem — so three of them and the label column come to 36rem,
     and a phone still scrolls the table sideways (.swipe-hint says so). */
  min-width: 36rem;
  text-align: left;
}

/* The column heads: what the plan is, what it costs, its button. The 1px
   height is the old table trick, not a size: a cell never shrinks below its
   content, but naming a height at all is what lets the grid inside it ask
   for 100% of the row (below, `.plan-head`). */
.plan-matrix th.plan {
  /* What the label column leaves, split three ways. */
  width: calc(74% / 3);
  height: 1px;
  padding: 18px 16px 20px;
  border: 1px solid var(--surface-line);
  border-radius: 14px 14px 0 0;
  background: var(--surface);
  vertical-align: top;
  font-weight: 400;
}

/* One head, in slots: name, pitch, price, the line under it, button. The
   pitch row is the only flexible one, so it swallows every difference in
   blurb length (and the "Pro is active until…" line the script fills in)
   while the price and the button stay level across the three columns. A
   percentage height in a table cell resolves against the row, which is what
   lets the grid fill the head; where it does not, the head simply stacks. */
.plan-head {
  display: grid;
  /* One column, allowed to be narrower than its widest child: without the
     minmax the billing toggle's own minimum would push Pro's head wider than
     its cell on a phone. */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto 1fr;
  gap: 8px;
  height: 100%;
}

/* Pro is the recommendation, so it is the one with an edge. */
.plan-matrix th.plan.featured,
.plan-matrix td.featured {
  background: color-mix(in srgb, var(--accent) 7%, var(--surface));
}

.plan-matrix th.plan.featured {
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
}

.plan h2 {
  margin: 0;
  font-size: 1.05rem;
}

/* The labels take what they need at this width and no more — half of the
   eleven wrap to two lines either way — so the three plans get the rest,
   in three equal columns. */
.plan-matrix .corner {
  width: 26%;
}

/* The empty corner and the feature labels stay legible while the table is
   scrolled sideways. */
.plan-matrix .corner,
.plan-matrix tbody th {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--paper);
}

.plan-matrix tbody th {
  padding: 12px 14px 12px 0;
  border-top: 1px solid var(--rule);
  font-size: 0.92rem;
  font-weight: 500;
  text-wrap: pretty;
}

.plan-matrix tbody td {
  padding: 12px 16px;
  border-top: 1px solid var(--rule);
  vertical-align: middle;
}

/* In or out, then the qualifier: "1080p", "first 25 legs". */
.mark {
  display: block;
}

.mark.yes {
  color: var(--accent);
}

.mark.no {
  color: var(--muted);
  opacity: 0.65;
}

.cell-note {
  display: block;
  margin-top: 2px;
  color: var(--muted);
  font-size: 0.78rem;
  line-height: 1.3;
}

/* Every column has one, in dollars or in words ("Per domain"). */
.price {
  margin: 4px 0 0;
  font: 700 1.5rem/1.2 var(--font-brand, system-ui, sans-serif);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

/* The line under the price — how it is billed, or what the price covers.
   Three lines' worth of room whether it needs them or not, so the prices
   above it stay level: Pro's line names the period it is not showing ("or
   US$39 a year — US$3.25 a month, save 46%"), and that runs to three lines
   in a column this width anywhere from 360 to 1920. */
.billed,
.price-note {
  min-height: 4.05em;
  margin: 0;
  font-size: 0.82rem;
  line-height: 1.35;
  color: var(--muted);
}

/* The period Pro is not showing, offered where the reader is already looking:
   a link in the run of the line that clicks the toggle above for them. It has
   to read as text — the type is inherited, so the two states of `.billed` are
   the same three lines and the prices stay level.

   A button is an atomic inline box whatever its `display` (Blink forces
   inline-block), so it cannot break across lines like the words around it:
   left at 100% it would never fit after "or" and would drop a line, making
   this note four deep. Held a little narrower than its slot it sits on the
   first line and wraps inside itself instead, and `vertical-align: top` keeps
   that first line's top where the text's would be. */
.switch-plan {
  display: inline;
  max-width: calc(100% - 1.5em);
  padding: 0;
  border: 0;
  background: none;
  /* Small type (0.82rem) on the head's plate, and on the Pro column its
     tinted version — accent text that is not on paper, so it takes the
     deeper pink (the same reason .toc a:hover does). */
  color: var(--accent-on-surface);
  font: inherit;
  text-align: left;
  vertical-align: top;
  cursor: pointer;
}

.switch-plan:hover {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}

.switch-plan:focus-visible {
  outline: 2px solid var(--accent-on-surface);
  outline-offset: 1px;
  border-radius: 4px;
  text-decoration: underline;
}

/* Disabled with the toggle once this browser has Pro: there is nothing left
   to switch to, so the offer stops looking like something to click. */
.switch-plan:disabled {
  color: inherit;
  cursor: default;
  text-decoration: none;
}

.plan-state {
  margin: 8px 0 0;
  font-size: 0.82rem;
  color: var(--ink);
  font-weight: 600;
}

.plan .blurb {
  margin: 0;
  font-size: 0.94rem;
  line-height: 1.45;
  text-wrap: pretty;
}

/* Buttons only work with JavaScript (they call /api/checkout), so they ship
   as links to the restore page and are upgraded in place — a reader without
   it still lands somewhere useful. */
.cta {
  display: inline-block;
  margin-top: 14px;
  padding: 10px 14px;
  border: 1px solid var(--accent);
  border-radius: 10px;
  background: transparent;
  color: var(--accent);
  font: 600 0.95rem/1.3 var(--font-ui, system-ui, sans-serif);
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}

/* One button per column, the width of the column it belongs to and the last
   row of its grid — the copy is short enough to stay on one line. */
.plan-matrix th.plan .cta {
  display: block;
  width: 100%;
  margin-top: 2px;
  padding: 10px 8px;
}

.cta.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}

.cta:hover {
  filter: brightness(1.06);
}

.cta.manage {
  margin-top: 8px;
}

.plan-toggle {
  display: flex;
  gap: 4px;
  margin: 0;
  padding: 3px;
  border: 1px solid var(--surface-line);
  border-radius: 10px;
}

.plan-toggle button {
  flex: 1;
  padding: 5px 8px;
  border: none;
  border-radius: 7px;
  background: transparent;
  color: var(--muted);
  font: 600 0.8rem/1.3 var(--font-ui, system-ui, sans-serif);
  cursor: pointer;
}

.plan-toggle button.active {
  background: var(--paper);
  color: var(--ink);
}

/* Only ever filled by the script, and only when something went wrong. */
.plan-note:empty {
  display: none;
}

.plan-note {
  color: var(--accent);
}

.faq h3 {
  margin-top: 20px;
}

/* Where the plan columns stop being wide enough for the price and the button
   copy to hold one line each, the heads give back some of their padding and
   the price comes down a size. Below 820px or so the wrapper is scrolling the
   table sideways as well, and the columns are at their narrowest. */
@media (max-width: 1024px) {
  .plan-matrix th.plan {
    padding: 14px 10px 16px;
  }

  /* A price is one unbreakable word: at this width the big size would run
     out of the column rather than wrap. */
  .price {
    font-size: 1.2rem;
  }

  .plan-matrix th.plan .cta {
    padding: 9px 4px;
    font-size: 0.88rem;
  }

  /* The two toggle buttons want more room than the column has; their own
     padding is the part that can go. */
  .plan-toggle button {
    padding: 5px 4px;
  }

  .plan-matrix tbody td {
    padding: 12px 10px;
  }
}

/* Where the columns bottom out (the table is at its min-width and scrolling
   sideways), Pro's monthly note runs to four lines rather than three: the
   offer inside it is a button, and a button cannot break across lines, so it
   wraps a word or so earlier than the sentence around it would. Every column
   reserves the fourth line, not just Pro's — the reservation is what keeps
   the three prices and the three buttons level. */
@media (max-width: 860px) {
  .billed,
  .price-note {
    min-height: 5.4em;
  }
}

/* ---------- the /pro pages ---------- */

.pro-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin: 18px 0 16px;
}

.pro-actions .cta {
  margin-top: 0;
}

/* /pro/activate holds its way back shut until the token is in localStorage:
   clicking it while /api/activate is still in flight aborts the call that
   writes it, and the map boots free. Muted, and not clickable, until then. */
.cta[aria-disabled='true'] {
  border-color: var(--rule);
  background: transparent;
  color: var(--muted);
  cursor: default;
  pointer-events: none;
}

/* "Email me a link": one field, one button, on one line where there is room. */
.restore-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin: 18px 0 14px;
}

.restore-form label {
  flex: 1 0 100%;
  font-size: 0.82rem;
  color: var(--muted);
}

.restore-form input {
  flex: 1 1 16rem;
  min-width: 0;
  padding: 10px 12px;
  border: 1px solid var(--rule);
  border-radius: 10px;
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  font-size: 0.95rem;
}

.restore-form input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.restore-form .cta {
  flex: 0 0 auto;
  margin-top: 0;
}

/* ---------- the route log (/routes and everything under it) ---------- */

/* Generated by scripts/build-route-log.mjs, so every class here is written by
   that script and nothing else on the site uses them. Body class `routes`
   scopes the lot — `.embed` above already belongs to the Examples iframes,
   and the log's map plate would otherwise land on top of it. */

/* ---- the week on one map ---- */

.routes .hero {
  margin: 0 0 32px;
}

.routes .frame {
  position: relative;
}

/* The plate every map sits on is the sea; the ground over it is the app's own
   basemap, cut out of the same Protomaps archive the app renders and written
   into the page by scripts/route-log-basemap.mjs. Every colour comes from the
   app's Dark look, because these are the app's own route colours and they are
   drawn for that ground. */
.routes .map,
.routes .thumb,
.routes .mini,
.routes .embed {
  background: var(--log-water);
  overflow: hidden;
}

.routes .map {
  position: relative;
  aspect-ratio: 2 / 1;
  border-radius: 8px;
}

.routes .map svg,
.routes .thumb svg,
.routes .mini svg,
.routes .embed svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* A week whose plate is the app's own 16:9 export rather than a drawing
   (scripts/route-log-week-png.mjs). The picture brings its own shape, so the
   2:1 plate gives way to it and the image sets the height — anything else
   letterboxes it or crops the routes off the bottom. */
.routes .map.shot {
  aspect-ratio: auto;
}

.routes .map.shot img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 8px;
}

/* The SVG's own ink. Every colour a route is drawn in comes from a class on
   the path, so the palette stays in this file with the rest of the tokens
   and the generated HTML carries no hex at all. */
.routes .map text,
.routes .thumb text,
.routes .embed text {
  font-family: var(--font-mono, monospace);
  font-size: 12px;
  fill: #e8eaf0;
}

.routes .thumb text {
  font-size: 13px;
}

/* The basemap: the sea the frame is painted, the land on it, the water on the
   land, and the borders across both. `.land` is the fallback drawing — the
   Natural Earth coastline path, for a build that could reach no tiles. */
.routes .sea,
.routes .water {
  fill: var(--log-water);
}

.routes .earth,
.routes .land {
  fill: var(--log-land);
}

.routes .border {
  fill: none;
  stroke: var(--log-border);
  stroke-width: 0.75;
}

/* A hero and an entry's embed are 960 units across, a thumbnail 320 and a
   past-week chip 56: a pixel of border on the big ones, and the floor on the
   rest, so a line never falls under half a pixel. */
.routes .map .border,
.routes .embed .border {
  stroke-width: 1;
}

/* State lines only appear close in, and never at the weight of a country: the
   app fades them in over z4–z6 and a static map has no ramp to do it with. */
.routes .border.region {
  stroke-opacity: 0.5;
}

/* The coastline path itself, once a page: a box with no size, above every map
   that draws it. Block rather than the inline default — an inline SVG here
   would open a line's worth of space at the top of the page. */
.routes .defs {
  display: block;
}

.routes .arc {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linecap: round;
}

.routes .glow {
  fill: none;
  stroke: currentColor;
  stroke-opacity: 0.22;
  stroke-width: 8;
  stroke-linecap: round;
}

.routes .dot {
  fill: currentColor;
  stroke: var(--voyarc-midnight, #060913);
  stroke-width: 1.5;
}

/* Every map says what changed, so a route is drawn in the colour of its
   change wherever it appears — on its week's map, on its own page and on the
   thumbnail in the list. Each class sets `color` alone — the arc strokes it,
   the dot fills with it, the legend's bullet is a block of it — so no rule
   here has to know which of the four it is painting. */
.routes .new {
  color: var(--map-new);
}

.routes .back {
  color: var(--map-back);
}

.routes .more {
  color: var(--map-more);
}

.routes .cut {
  color: var(--map-cut);
}

.routes .brand {
  position: absolute;
  left: 14px;
  top: 12px;
  padding: 3px 10px;
  border-radius: 4px;
  background: rgba(6, 9, 19, 0.72);
  font: 700 15px/1.4 var(--font-brand, system-ui, sans-serif);
  color: #e8eaf0;
}

.routes .credit {
  position: absolute;
  right: 12px;
  bottom: 12px;
  padding: 2px 7px;
  border-radius: 4px;
  background: rgba(6, 9, 19, 0.72);
  font-size: 11px;
  color: #a6abbc;
}

.routes .legend {
  position: absolute;
  left: 14px;
  bottom: 12px;
  display: grid;
  gap: 4px;
  padding: 8px 10px;
  border-radius: 6px;
  background: rgba(6, 9, 19, 0.78);
  font-size: 12px;
  line-height: 1.45;
  color: #e8eaf0;
}

.routes .legend b {
  margin-bottom: 2px;
  font-weight: 600;
}

.routes .legend span {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}

.routes .legend i {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: currentColor;
}

/* The title over an exported plate, which is all that plate carries: the
   picture has its own legend in the bottom-left corner, its own wordmark in
   the top-left and the basemap's credit in the bottom-right, so the week's
   name takes the one corner the app leaves empty. On a phone the rule below
   lays every legend out under the map and this has nothing left to say. */
.routes .legend.alone {
  top: 12px;
  right: 14px;
  bottom: auto;
  left: auto;
}

/* The line under the hero: what the week is, then the two ways into it. */
.routes .under {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 14px;
  margin-top: 10px;
  font-size: 0.88rem;
  color: var(--muted);
}

.routes .under .title {
  font: 700 1.05rem/1.35 var(--font-brand, system-ui, sans-serif);
  letter-spacing: -0.01em;
  color: var(--ink);
}

.routes .under .acts {
  display: flex;
  gap: 8px;
  margin-left: auto;
}

/* ---- past weeks ---- */

.routes .past {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px;
  margin-top: 14px;
  font-size: 0.82rem;
  color: var(--muted);
}

.routes .past .k {
  font-size: 0.75rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.routes .past a {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 4px 10px 4px 4px;
  border: 1px solid var(--rule);
  border-radius: 6px;
  color: var(--ink);
  text-decoration: none;
}

.routes .past a:hover {
  border-color: var(--ink);
}

/* Its own 56×28 drawing, not the hero shrunk: at this size a hero's strokes
   would be a tenth of a pixel wide and the codes unreadable, so the build
   draws the minis without labels or glow and they need only be sized. */
.routes .mini {
  width: 56px;
  height: 28px;
  border-radius: 3px;
}

.routes .mini .arc {
  stroke-width: 1.5;
}

.routes .past a small {
  color: var(--muted);
}

/* ---- the filter bar ---- */

.routes .toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 4px;
  padding: 10px 0;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}

.routes .toolbar .result {
  margin: 0 0 0 auto;
  font-size: 0.88rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.routes .seg {
  display: inline-flex;
  gap: 4px;
  margin-right: 4px;
  padding-right: 8px;
  border-right: 1px solid var(--rule);
}

.routes .seg button {
  padding: 6px 11px;
  border: 1px solid transparent;
  border-radius: 6px;
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: 0.88rem;
  cursor: pointer;
}

.routes .seg button:hover:not(:disabled):not([aria-pressed='true']) {
  background: var(--surface);
}

/* The chosen chip wears its change's own colours — the same pair the badge
   on every matching entry wears, so the bar and the cards agree at a glance.
   "All" has no colour of its own and takes the ink. */
.routes .seg button[aria-pressed='true'] {
  background: var(--ink);
  color: var(--paper);
  font-weight: 600;
}

.routes .seg button[aria-pressed='true'][data-key='new'] {
  background: var(--chip-new-bg);
  color: var(--chip-new);
}

.routes .seg button[aria-pressed='true'][data-key='back'] {
  background: var(--chip-back-bg);
  color: var(--chip-back);
}

.routes .seg button[aria-pressed='true'][data-key='more'] {
  background: var(--chip-more-bg);
  color: var(--chip-more);
}

.routes .seg button[aria-pressed='true'][data-key='cut'] {
  background: var(--chip-cut-bg);
  color: var(--chip-cut);
}

/* Greyed, never hidden: an option that would empty the page still says the
   log has nothing of that kind under the filters already set. */
.routes .seg button:disabled,
.routes .menu-list button:disabled {
  color: var(--muted);
  opacity: 0.4;
  cursor: default;
}

.routes .menu {
  position: relative;
}

.routes .menu summary {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px 6px 12px;
  border: 1px solid var(--rule);
  border-radius: 6px;
  background: var(--paper);
  color: var(--ink);
  font-size: 0.88rem;
  list-style: none;
  cursor: pointer;
  user-select: none;
}

.routes .menu summary::-webkit-details-marker {
  display: none;
}

.routes .menu summary .v {
  color: var(--muted);
}

.routes .menu summary .v b {
  color: var(--ink);
  font-weight: 600;
}

.routes .menu summary svg {
  color: var(--muted);
}

.routes .menu[open] summary,
.routes .menu summary:hover,
.routes .menu.active summary {
  border-color: var(--ink);
}

.routes .menu-list {
  position: absolute;
  z-index: 5;
  top: calc(100% + 6px);
  left: 0;
  min-width: 210px;
  max-height: 320px;
  overflow-y: auto;
  padding: 6px;
  border: 1px solid var(--rule);
  border-radius: 8px;
  background: var(--paper);
  box-shadow: 0 8px 28px rgba(6, 9, 19, 0.18);
}

.routes .menu-list button {
  display: flex;
  width: 100%;
  align-items: center;
  gap: 9px;
  padding: 7px 10px;
  border: 0;
  border-radius: 5px;
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: 0.88rem;
  text-align: left;
  cursor: pointer;
}

.routes .menu-list button:hover:not(:disabled) {
  background: var(--surface);
}

.routes .menu-list button[aria-pressed='true'] {
  background: var(--ink);
  color: var(--paper);
}

.routes .menu-list .none {
  padding: 8px 10px;
  font-size: 0.82rem;
  color: var(--muted);
}

.routes .search {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 2px 2px 6px;
  padding: 5px 9px;
  border: 1px solid var(--rule);
  border-radius: 6px;
  background: var(--paper);
}

.routes .search svg {
  flex: none;
  color: var(--muted);
}

.routes .search input {
  width: 100%;
  min-width: 0;
  border: 0;
  outline: 0;
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: 0.88rem;
}

.routes .search input::placeholder {
  color: var(--muted);
}

.routes .search:focus-within {
  border-color: var(--ink);
}

/* What is filtering the page right now, with a way out of each of them. */
.routes .active-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  min-height: 30px;
  padding: 4px 0;
  font-size: 0.82rem;
  color: var(--muted);
}

.routes .active-row .pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 6px 2px 10px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink);
}

.routes .active-row .pill button {
  padding: 0 3px;
  border: 0;
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: 0.95rem;
  line-height: 1;
  cursor: pointer;
}

.routes .active-row .pill button:hover {
  color: var(--ink);
}

.routes .active-row .clear {
  padding: 0;
  border: 0;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 0.82rem;
  text-decoration: underline;
  text-decoration-color: var(--rule);
  cursor: pointer;
}

/* ---- the airline's code as a tile ---- */

.routes .mono {
  display: inline-grid;
  flex: none;
  place-items: center;
  width: 22px;
  height: 22px;
  border: 1px solid var(--rule);
  border-radius: 5px;
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font-mono, monospace);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.02em;
}

.routes .menu-list .mono {
  width: 24px;
  height: 24px;
}

.routes .menu-list button[aria-pressed='true'] .mono {
  border-color: rgba(255, 255, 255, 0.35);
  background: transparent;
  color: var(--paper);
}

/* ---- weeks and entries ---- */

.routes .week {
  margin: 0 0 8px;
  padding-top: 22px;
}

.routes .week h2 {
  display: flex;
  align-items: center;
  gap: 14px;
  margin: 0 0 4px;
  font-size: 1.05rem;
  color: var(--ink);
}

.routes .week h2 a {
  color: var(--ink);
  text-decoration: none;
}

.routes .week h2 a:hover {
  text-decoration: underline;
}

/* A day on a week page: the sections one week is really in, so the page reads
   h1 → h2 → h3 rather than an h1 and a stack of entries. The week heading's
   treatment one size down and in the muted ink — these are days inside a week
   that the h1 has already named, not weeks. */
.routes .day {
  padding-top: 18px;
}

.routes .day h2 {
  display: flex;
  align-items: center;
  gap: 14px;
  margin: 0 0 4px;
  font: 600 0.95rem/1.6 var(--font-ui, system-ui, sans-serif);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.routes .day h2::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--rule);
}

.routes .day h2 .n {
  order: 3;
  font-weight: 400;
}

/* Every week the index does not carry in full, in the hub index's own shape:
   under the cards rather than instead of them. */
.routes .earlier {
  padding-top: 26px;
  border-top: 1px solid var(--rule);
}

.routes .earlier h2 {
  margin: 0 0 4px;
  font-size: 1.05rem;
}

.routes .earlier > p {
  margin: 0 0 14px;
  font-size: 0.88rem;
  color: var(--muted);
}

/* The rule runs from the end of the title to the count and no further. */
.routes .week h2::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--rule);
}

.routes .week h2 .n {
  order: 3;
  font: 400 0.82rem/1.5 var(--font-ui, system-ui, sans-serif);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.routes .entry {
  display: grid;
  grid-template-columns: 200px 1fr;
  gap: 20px;
  padding: 20px 0;
  border-top: 1px solid var(--rule);
  /* A hundred-odd cards on the index, each one an inline map: off screen they
     are worth their height and nothing else. `auto` means the browser keeps a
     card's real height once it has rendered it once, so the figure below is
     only ever the first guess — the mean card's content box, measured (the
     padding and the rule are on top of it, hence 220 for a 261px card). */
  content-visibility: auto;
  contain-intrinsic-size: auto 220px;
}

.routes .entry:first-of-type {
  border-top: 0;
}

.routes .thumb {
  display: block;
  aspect-ratio: 16 / 10;
  border-radius: 6px;
}

.routes .eyebrow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-bottom: 6px;
  font-size: 0.82rem;
  color: var(--muted);
}

.routes .eyebrow .who {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  color: var(--ink);
}

.routes .eyebrow time {
  font-variant-numeric: tabular-nums;
}

.routes .chip {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 4px;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.routes .chip.new {
  background: var(--chip-new-bg);
  color: var(--chip-new);
}

.routes .chip.back {
  background: var(--chip-back-bg);
  color: var(--chip-back);
}

.routes .chip.more {
  background: var(--chip-more-bg);
  color: var(--chip-more);
}

.routes .chip.cut {
  background: var(--chip-cut-bg);
  color: var(--chip-cut);
}

.routes .tag {
  display: inline-block;
  padding: 1px 7px;
  border: 1px solid var(--rule);
  border-radius: 4px;
  font-size: 0.75rem;
  color: var(--muted);
}

.routes .entry h3 {
  margin: 0 0 6px;
  font: 600 1.2rem/1.2 var(--font-ui, system-ui, sans-serif);
  letter-spacing: -0.005em;
  text-wrap: balance;
}

.routes .entry h3 a {
  color: var(--ink);
  text-decoration: none;
}

.routes .entry h3 a:hover {
  text-decoration: underline;
}

.routes .code {
  font-family: var(--font-mono, monospace);
  letter-spacing: 0.02em;
}

/* Route, distance, aircraft, date, frequency — separated by middots that
   belong to the layout rather than the copy. */
.routes .facts {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 0;
  margin: 0 0 8px;
  font-size: 0.88rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.routes .facts span + span::before {
  content: '·';
  margin: 0 8px;
  color: var(--rule);
}

.routes .facts b {
  color: var(--ink);
  font-weight: 600;
}

/* The one line of context an entry may carry, bulleted in the route gold. */
.routes .context {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin: 0 0 8px;
  font-size: 0.88rem;
  color: var(--muted);
}

.routes .context::before {
  content: '';
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--voyarc-gold, #ffb703);
  transform: translateY(-1px);
}

.routes .note {
  margin: 0 0 10px;
  max-width: var(--measure);
  text-wrap: pretty;
}

.routes .links {
  font-size: 0.88rem;
}

.routes .links .open {
  font-weight: 600;
}

.routes .empty {
  padding: 40px 0;
  border-top: 1px solid var(--rule);
  color: var(--muted);
}

.routes .log-foot {
  margin: 40px 0 0;
  font-size: 0.82rem;
  color: var(--muted);
}

/* ---- a week's page, and an entry's ---- */

.routes .crumb {
  margin-bottom: 20px;
  font-size: 0.88rem;
}

.routes .crumb a {
  color: var(--muted);
}

.routes .week-page .hero {
  margin-top: 20px;
}

.routes .entry-page .code {
  margin-bottom: 12px;
  font-size: 1rem;
  color: var(--muted);
}

.routes .embed {
  position: relative;
  margin: 22px 0 8px;
  border: 0;
}

.routes .embed .tagline {
  position: absolute;
  left: 12px;
  bottom: 10px;
  padding: 3px 8px;
  border-radius: 4px;
  background: rgba(6, 9, 19, 0.7);
  font-family: var(--font-mono, monospace);
  font-size: 12px;
  color: #a6abbc;
}

/* The two ways on from a page: into the app, or into a clipboard. */
.routes .actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin: 12px 0 28px;
}

.routes .actions a,
.routes .under .acts a {
  display: inline-block;
  padding: 8px 14px;
  border: 1px solid var(--rule);
  border-radius: 6px;
  color: var(--ink);
  font-size: 0.88rem;
  text-decoration: none;
}

.routes .under .acts a {
  padding: 6px 12px;
}

.routes .actions a.primary,
.routes .under .acts a.primary {
  border-color: var(--accent);
  background: var(--accent);
  color: #fff;
  font-weight: 600;
}

.routes .actions a:hover,
.routes .under .acts a:hover {
  border-color: var(--ink);
}

.routes .actions a.primary:hover,
.routes .under .acts a.primary:hover {
  border-color: var(--accent);
  filter: brightness(1.05);
}

.routes .actions .copy {
  padding: 8px 14px;
  border-radius: 6px;
  font-size: 0.88rem;
}

/* ---- the owner's post box ---- */

/* What to post when a week goes up, beside the picture to post it with.
   Hidden in the markup and unhidden by the page's own script on a dev server
   and on a Pages preview — the hosts previewAllowedOn allows — so a reader on
   voyarc.app never meets it. Dashed, because it is not part of the page. */
.routes .post {
  display: grid;
  grid-template-columns: 1fr minmax(0, 240px);
  align-items: start;
  gap: 16px;
  margin: 0 0 28px;
  padding: 14px 16px;
  border: 1px dashed var(--rule);
  border-radius: 8px;
  background: var(--surface);
}

.routes .post b {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
  font-size: 0.88rem;
}

.routes .post > div > code {
  display: block;
  margin-bottom: 10px;
  font-family: var(--font-mono, monospace);
  font-size: 0.82rem;
  line-height: 1.55;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.routes .post img {
  width: 100%;
  height: auto;
  border-radius: 6px;
}

.routes .post .none {
  margin: 0;
  font-size: 0.82rem;
  color: var(--muted);
}

.routes .facts-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 14px 24px;
  margin-bottom: 22px;
  padding: 16px 0;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}

.routes .facts-grid div {
  font-size: 0.75rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

.routes .facts-grid div b {
  display: block;
  margin-top: 3px;
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.routes .entry-page .context {
  font-size: 1rem;
  color: var(--ink);
}

.routes .entry-page .note {
  font-size: 1.05rem;
}

.routes .entry-page .src {
  font-size: 0.88rem;
  color: var(--muted);
}

.routes .entry-page .src a {
  color: var(--muted);
  text-decoration-color: var(--rule);
}

.routes .also {
  margin-top: 36px;
  padding-top: 18px;
  border-top: 1px solid var(--rule);
  font-size: 0.88rem;
  color: var(--muted);
}

.routes .also a {
  margin-right: 14px;
}

/* ---- the hubs: an airline's page, an airport's, and an index of each ---- */

/* The line every page of the log carries, under the crumb or under the lede:
   two hops from anywhere to any hub. */
.routes .hub-nav {
  margin: -10px 0 22px;
  font-size: 0.88rem;
  color: var(--muted);
}

/* The links threaded through a card — the airline's name, the route's codes —
   read as text until they are hovered: on these pages a colour means a change,
   and a card of gold links would say four things it does not mean. */
.routes .eyebrow .who a,
.routes .code a {
  color: inherit;
  text-decoration: none;
}

.routes .eyebrow .who a:hover,
.routes .code a:hover {
  text-decoration: underline;
}

.routes .hub-page .hero {
  margin-top: 20px;
}

/* The way across at the foot of a hub: an airline's airports, an airport's
   airlines. A row of names, wrapping, with room between the lines. */
.routes .hub-links {
  margin: 0 0 30px;
  font-size: 0.88rem;
  line-height: 1.9;
  color: var(--muted);
}

/* The airport's hub map (scripts/build-hubs.mjs), one line straight above
   the airlines it links across to. */
.routes .hub-map {
  margin: 0;
  font-size: 0.88rem;
  line-height: 1.9;
  color: var(--muted);
}

/* Every hub of one kind, each with how much of the log is in it. Columns
   rather than one long list: 59 airports is four screens in a single file. */
.routes .hub-index {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 0 28px;
  margin: 0 0 30px;
  padding: 0;
  list-style: none;
}

.routes .hub-index li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 8px 0;
  border-bottom: 1px solid var(--rule);
}

.routes .hub-index li span {
  font-size: 0.8rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

@media (max-width: 760px) {
  /* The chips scroll rather than wrap: four of them plus All is one swipe,
     and the count moves under the row where it has space to say "3 of 8". */
  .routes .toolbar .result {
    order: 9;
    width: 100%;
    margin: 0;
  }

  .routes .seg {
    width: 100%;
    padding-right: 0;
    border-right: 0;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }

  .routes .seg::-webkit-scrollbar {
    display: none;
  }

  .routes .seg button {
    flex: none;
    white-space: nowrap;
  }

  /* A legend over a phone-sized map covers the map; below it, it does not. */
  .routes .legend {
    position: static;
    display: flex;
    flex-wrap: wrap;
    gap: 6px 14px;
    margin-top: 8px;
    padding: 8px 10px;
    background: var(--surface);
    color: var(--ink);
  }

  .routes .legend b {
    width: 100%;
    margin: 0;
  }

  .routes .map {
    aspect-ratio: 16 / 10;
  }

  .routes .entry {
    grid-template-columns: 1fr;
    gap: 14px;
    /* The map goes above the copy, so a card is more than twice as tall. */
    contain-intrinsic-size: auto 570px;
  }

  .routes .thumb {
    max-width: 320px;
  }

  .routes .under .acts {
    margin-left: 0;
  }

  .routes .post {
    grid-template-columns: 1fr;
  }
}

/* ---------- hub maps (/hubs, generated by scripts/build-hubs.mjs) ---------- */

.hubs .crumb {
  margin-bottom: 18px;
  font-size: 0.88rem;
}

.hubs .crumb a {
  color: var(--muted);
}

.hubs .kicker {
  margin: 0 0 8px;
  font: 700 0.75rem/1.3 var(--font-brand, system-ui, sans-serif);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
}

.hubs main > .prose {
  margin-bottom: 36px;
}

.hubs .lede {
  margin-bottom: 0;
}

.hubs .list,
.hubs .cross {
  margin: 0 0 52px;
}

/* One list: heading, stamp, map, the way into the app, then the words and
   the table. */
.hubs .list h2 {
  margin: 0 0 4px;
  font-size: 1.35rem;
}

.hubs .stamp {
  margin: 0 0 14px;
  font-size: 0.85rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* A draft says so before anything else is read. */
.hubs .stamp.draft::before {
  content: '';
  display: inline-block;
  width: 7px;
  height: 7px;
  margin-right: 7px;
  border: 1.5px solid currentColor;
  border-radius: 50%;
  vertical-align: 1px;
}

/* The map is the app's Light plate on either register: the ground and the
   route colours are the ones the linked map opens in, so they stay put. */
.hubs .map {
  aspect-ratio: 2 / 1;
  overflow: hidden;
  border: 1px solid var(--surface-line);
  border-radius: 8px;
  background: var(--hub-water);
}

.hubs .map svg {
  display: block;
  width: 100%;
  height: 100%;
}

.hubs .sea,
.hubs .water {
  fill: var(--hub-water);
}

.hubs .earth {
  fill: var(--hub-land);
}

.hubs .border {
  fill: none;
  stroke: var(--hub-border);
  stroke-width: 1;
}

.hubs .border.region {
  stroke-opacity: 0.5;
}

.hubs .arcs path {
  fill: none;
  stroke: var(--hub-route);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-opacity: 0.85;
}

.hubs .arcs.far path {
  stroke: var(--hub-far);
  stroke-width: 2.8;
  stroke-opacity: 1;
}

.hubs .map .dot {
  fill: var(--hub-route);
  stroke: #fff;
  stroke-width: 1.2;
}

.hubs .map .dot.far {
  fill: var(--hub-far);
}

.hubs .map .hub {
  fill: #fff;
  stroke: var(--hub-route);
  stroke-width: 2.5;
}

.hubs .map text {
  font: 700 15px var(--font-mono, monospace);
  fill: #2c2c2c;
  paint-order: stroke;
  stroke: rgba(255, 255, 255, 0.85);
  stroke-width: 3.5px;
  stroke-linejoin: round;
}

.hubs .actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin: 12px 0 16px;
}

.hubs .actions a {
  display: inline-block;
  padding: 8px 14px;
  border: 1px solid var(--rule);
  border-radius: 6px;
  color: var(--ink);
  font-size: 0.88rem;
  text-decoration: none;
}

.hubs .actions a.primary {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
}

.hubs .actions a:hover {
  border-color: var(--ink);
}

.hubs .actions a.primary:hover {
  border-color: var(--accent);
  filter: brightness(1.05);
}

.hubs .change,
.hubs .notes,
.hubs .history {
  max-width: var(--measure);
  text-wrap: pretty;
}

.hubs .change,
.hubs .history {
  font-size: 0.92rem;
  color: var(--muted);
}

.hubs .history {
  margin: 12px 0 0;
  font-variant-numeric: tabular-nums;
}

/* The list itself, nearest first. It scrolls inside its own box rather than
   push a phone's page sideways. */
.hubs .table {
  overflow-x: auto;
}

.hubs table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.92rem;
  line-height: 1.45;
  text-align: left;
}

.hubs th,
.hubs td {
  padding: 7px 12px 7px 0;
  border-bottom: 1px solid var(--rule);
  vertical-align: baseline;
}

.hubs th {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--muted);
}

.hubs th:last-child,
.hubs td:last-child {
  padding-right: 0;
}

.hubs .num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.hubs td.num:first-child {
  width: 2.5em;
  color: var(--muted);
}

/* The farthest: the row the amber route on the map is. */
.hubs tr.far td {
  background: color-mix(in srgb, var(--hub-far) 12%, transparent);
}

.hubs tr.far td:first-child {
  box-shadow: inset 3px 0 var(--hub-far);
}

.hubs .far-tag {
  display: inline-block;
  margin-left: 6px;
  padding: 0 6px;
  border-radius: 4px;
  background: var(--hub-far);
  color: #2c2c2c;
  font-size: 0.72rem;
  font-weight: 700;
  line-height: 1.6;
  vertical-align: 1px;
}

.hubs .cross h2 {
  margin-top: 0;
}

.hubs .cross p {
  margin: 0;
}

/* /hubs: every hub, what it holds and how current it is. */
.hubs .hub-list {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--rule);
}

.hubs .hub-list li {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: baseline;
  gap: 4px 20px;
  margin: 0;
  padding: 10px 0;
  border-bottom: 1px solid var(--rule);
}

.hubs .hub-list a {
  font-weight: 600;
}

.hubs .hub-list .counts,
.hubs .hub-list .status {
  font-size: 0.85rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

@media (max-width: 640px) {
  .hubs .hub-list li {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  .hubs .hub-list .counts {
    grid-row: 2;
  }

  .hubs th,
  .hubs td {
    padding-right: 8px;
  }
}
