/* Navology — cool ground, cobalt route.

   The palette, carried across both themes by role tokens.

   THE UI ACCENT IS COBALT BLUE, hue 223. It replaced a violet, and the reason
   for the swap is worth recording because it is not what you would expect:
   research across 21 sources found NO measured evidence that any hue is more
   appealing, more trustworthy, calmer or faster than any other, on either
   ground. So the hue was not chosen on preference — there is nothing to choose
   on. It was chosen by elimination and arithmetic: 223 is 48 degrees from the
   teal step, 133 from olive and 204 from rust, the widest simultaneous
   separation available; teal/cyan, olive/green and rust/amber all collide with
   a step identity; red and rose collide with destructive and error states;
   yellow cannot reach 4.5:1 as a light-theme fill at all.

   THE TWO THEMES ARE AUTHORED SEPARATELY, not inverted from one another, and
   the dark values sit at ~65% saturation against the light values' 70% — the
   one concrete change the desaturate-don't-just-lighten finding supports. The
   on-accent label FLIPS: white on the light fill, near-black on the dark one.

   THE GROUNDS DID NOT CHANGE, deliberately. #121415 measures L* 6.17, landing
   almost exactly on Material 3's dark Surface tone 6, and #191E20 at L* 10.85
   sits inside its container band. The evidence supported changing the accent
   and nothing else, so nothing else changed. Both grounds stay low-chroma and
   below the step sheets in saturation: the background must never be the most
   colourful large surface on the page. Earlier passes learned that twice.

   ONE DELIBERATE EXCEPTION: --map-accent stays violet. Map ink is chosen to
   survive Apple's basemap, which paints tan land, green parks and BLUE water —
   a cobalt flight arc would vanish into the sea. Map ink has never followed
   the brand and does not start now.

   A RULE THE ARITHMETIC SURFACED: the accent and the three step hues sit at
   similar luminance (1.02:1 to 2.02:1 apart), so accent and step colour must
   never be paired as foreground on background. That was equally true of the
   violet, so it is not a regression — but it is now written down.

   Every pair measured, not estimated: 50 pairs across both themes and the map
   ink, plus 27 more for the step sheets. Lowest text pair 4.77:1, lowest
   non-text 3.50:1. Map ink is checked against the basemap's own fills, not
   against the page, because the basemap stays light in both themes. */

:root {
  color-scheme: light;

  /* Planes. --surface is the page; --surface-2 is anything raised above it —
     panels, cards, controls. It rises in both themes, which is why one token
     can serve both. A full page of #FFFFFF glares under a long session, so
     light puts the soft tint on the ground and keeps the white for the
     surfaces you actually read from. */
  /* The desk the step sheets lie on, 1.098:1 below them in both themes.
     Its saturation is held BELOW the sheets' on purpose. It ran at 20% while
     the sheets sat at 8-10%, which made the background the most colourful
     large surface on the page — the mint read as a wash over everything and
     fought the neutral cards on top of it. Same luminance as before, so every
     measured pair is unchanged; only the chroma came out. */
  --surface: #F0F2F2;         /* the ground — paper with a mint cast */
  /* THE HEADER VEIL. The nav carries a photograph now, and one image has to
     sit under dark ink here and light ink in the other theme, so the veil is
     not a "dim" -- it is the ground itself, laid over the picture at whatever
     opacity keeps the type legible.

     RESOLVED for the Shinkansen band, and the two themes are no longer near
     each other because the artwork is DARK. Under a white veil a dark
     photograph drags the light ground down and costs the dark type contrast,
     so light needs MORE veil than before; under a near-black veil the same
     photograph costs nothing, so dark needs far LESS. Measured across 2 pages
     x 3 widths x both themes:

       light 0.92 -> weakest pair 4.63:1   (0.90 = 4.45, misses AA)
       dark  0.60 -> weakest pair 5.64:1   (clears AA all the way down to 0.44)

     Dark is NOT set at its floor; it is set where the band's surviving
     luminance range matches light's -- 0.164 against 0.160.

     THAT MATCHED RANGE IS NOT THE CLAIM IT LOOKS LIKE, and the first version
     of this comment made the claim anyway. Range is a property of the whole
     band; it says nothing about whether the SUBJECT reads. Measured
     separately, by the train's own row against the band's median row:

       dark  2.75:1 at 1440px, 3.03:1 at 500px   -- the train reads
       light 1.12:1 at 1440px, 1.14:1 at 500px   -- it does not

     The cause is structural, not a tuning error: THE TRAIN IS WHITE AND THE
     LIGHT VEIL IS WHITE. Compositing a near-240 subject under a near-241 veil
     leaves the subject essentially untouched at any opacity, while the dark
     landscape around it lifts -- so lowering the veil separates them only
     slowly (0.68 buys just 1.73:1) and costs the type its contrast long
     before the picture arrives. Only a light-theme image whose subject is
     DARK against a light ground fixes it properly.

     So light currently carries the landscape and not the train, deliberately
     and on the record. tools/probes/nav-band-contrast.mjs re-measures the
     contrast numbers from the rendered page rather than trusting this note. */
  --nav-veil: rgba(240, 242, 242, 0.92);
  --nav-veil-weak: rgba(240, 242, 242, 0.62);   /* the middle of the bar carries no text, so the picture is barely veiled there */
  --nav-art: url("/brand/nav-ridges-2026-08b-9000.webp");   /* the light band: a long grey range, tones floored at 96 so type still reads over it */
  --surface-2: #FFFFFF;       /* panels, cards and controls, raised on it */
  --border: #E0E5E5;          /* separators and card edges */
  --border-strong: #6E8583;   /* control edges — 3.50:1 on the ground */

  --ink: #0E2A2C;             /* teal-slate — 13.48:1 on the ground */
  --muted: #4F6664;           /* 5.47:1 on the ground, 6.15:1 on a panel */

  --accent: #2350C4;          /* cobalt, as a fill — 6.96:1 under a white label */
  --accent-ink: #FFFFFF;      /* label on the fill — 6.96:1 */
  --accent-text: #1C4099;     /* the same cobalt darkened until it reads —
                                 9.36:1 on a panel, 8.33:1 on the ground */
  --brand-mark: #0E2A2C;      /* the wordmark's fill — ink on a light ground */
  /* The monogram is a raster wearing one gold on both grounds, so the only
     lever on it is a filter. Undimmed it measures 1.35:1 against the worst
     band pixel behind its box -- not a colour, an absence.

     SOLVED, NOT PICKED, and the earlier note in this repo saying 0.75 clears
     3:1 was wrong: 0.75 measures 2.37:1. Three instruments agree the pure
     brightness threshold is 0.65 (3.06:1), which is a hair over the line and
     drops the mark to an olive bronze.

     brightness(0.6) saturate(1.6) instead: 3.44:1 with real margin, and the
     saturate buys back what the darkening spends -- the ink measures 0.66
     saturation against the original gold's 0.42, so it reads MORE gold, not
     less. Measured by rendering the mark over black and white and solving
     alpha per pixel, so the number is the browser's arithmetic, not mine. */
  --mark-dim: brightness(0.6) saturate(1.6);
  /* The header graticule. Two weights, like a chart's degrees and minutes.
     A muted teal rather than the accent violet: violet is what the wordmark,
     the buttons and the heat band are made of, and spending it on the
     background too leaves nothing for those to be. Structure you feel before
     you see it. */
  /* The header artwork: a street grid crossed by transit lines, dropped pins,
     a GPS accuracy ring and a flight arc trailing a dashed track. Modern
     wayfinding, because that is what this site does -- it routes you across a
     city by rail and on foot. The first version was a portolan chart and a
     compass rose, which said "navigation" beautifully and said it in 1590; a
     product about trains, streets and flights should not open with a sextant. */
  --accent-soft: rgba(35, 80, 196, 0.1);

  /* Alpha washes composite over whatever plane they land on, so a chip inside
     a panel inside the page keeps its separation without a third and fourth
     surface token. */
  --wash: rgba(14, 42, 44, 0.05);
  --wash-strong: rgba(14, 42, 44, 0.10);

  --danger: #B3261E;
  --danger-soft: rgba(179, 38, 30, 0.10);

  --scrim: rgba(6, 32, 35, 0.55);
  /* Light lifts with air rather than weight: two soft, wide, very low-opacity
     layers instead of one dark one. */
  --shadow: 0 1px 2px rgba(14, 42, 44, 0.05), 0 4px 12px rgba(14, 42, 44, 0.07);
  --shadow-lg: 0 8px 30px rgba(14, 42, 44, 0.14);
  /* A lit top edge is how a dark surface reads as raised. In light there is
     nothing to catch, so it stays off. */
  --edge: 0 0 0 0 transparent;

  /* Measured, not counted: 107px is what the nav actually renders at with the
     wordmark and tagline. The Where to Stay page no longer depends on this
     (it fills the remainder with flex instead), but planner.css still does. */
  --nav-h: 123px;
  /* Sections used to run one uniform clamp(84,9vw,124) on BOTH edges, which
     put 124px above and 124px below every band — 248px per section, 744px of
     the page's 2,875, measured. That is not air, it is absence. The rhythm is
     now asymmetric on purpose: a section opens with more room than it closes
     with, so the gap READS as belonging to the heading below it rather than as
     a uniform gutter between two things. */
  --space-section: clamp(60px, 6vw, 88px);
  --space-section-end: clamp(44px, 4.4vw, 64px);
  --space-gutter: clamp(22px, 4vw, 34px);

  /* The basemap is a light raster in both themes, so map ink never flips.
     Measured against OSM's lightest and darkest common tile fills. */
  --map-road: #123032;
  --map-transit: #4F6668;
  --map-accent: #5A38D0;      /* flight arcs and must-do stars — the accent
                                 violet, 4.71:1 on water, 6.29:1 on tan land */
  --map-hotel: #C22A11;       /* hotel pins are a signal red: nothing Apple
                                 paints is red, so it can never be lost in the
                                 basemap. Clears 3:1 on every tile unaided
                                 (worst 3.78 on water) as well as inside its
                                 own white ring, so a thin ring at small sizes
                                 is not load-bearing. */
  --map-hotel-ink: #FFFFFF;   /* glyph on the pin — 5.78:1 */
  /* The "where a hotel would serve" heat. Violet, third attempt at this hue:
     the brand gold sat a few degrees from Apple's tan basemap and vanished,
     and the green that replaced it vanished the same way over Japan -- parks,
     bamboo and forested hills paint half of a Kyoto tile green (seen live on
     the Japan 2026 trip, 2026-08-15). Violet collides with nothing Apple
     paints: not the tan land, not the green parks, not the blue water, and
     not the site's own red hotel pins. That finding is why the whole palette
     is now built around violet rather than the reverse: the map chose the
     accent, and the interface followed it.
     --map-heat-line stays in the same hue rather than going to a near-black
     outline: the band should look like a tint of one colour, not a filled
     outline. */
  --map-heat: #8F7FDE;        /* light violet fill, strongest band still light */
  --map-heat-line: #5646A5;   /* the same violet, dark enough to read on a light
                                 tile */
  /* The mask over everywhere the trip does not reach. Neutral on purpose:
     it must read as "dimmed", not as a colour with a meaning of its own. */
  --map-dim: #5A6A6B;
  --map-pin: #123032;
  --map-pin-ink: #FFFFFF;

  --max: 1080px;
  --wordmark-size: 30px;
  /* The tagline's size is a token because the mark's height is computed from
     it. See --brand-block-h. */
  --brand-tag-size: 0.75rem;
  /* THE HEIGHT OF THE WHOLE BRAND BLOCK, and therefore of the mark beside it:
     Chris asked for a mark as tall as the wordmark plus the line under it.
     This is arithmetic, not a measurement, and every term is a real one:
       wordmark line box  = --wordmark-size exactly, because .brand .wordmark
                            is set at `/1`
       + the grid row gap = 1px
       + tagline line box = --brand-tag-size * 1.2, because .brand-tag is set
                            at `/1.2`
     30 + 1 + 14.4 = 45.4px on desktop and 23 + 1 + 13.2 = 37.2px below 620px,
     which is what the block measured before the mark existed. A probe asserts
     the rendered mark and the rendered block agree to the pixel, so if anyone
     changes a line-height here this fails loudly rather than drifting. */
  --brand-block-h: calc(var(--wordmark-size) + 1px + var(--brand-tag-size) * 1.2);
  /* The wordmark's own face, and deliberately not the interface font: a mark
     that matches the nav links reads as a heading that happens to come first.
     Playfair Display, at Chris's direction, so the wordmark shares a face with
     the Sweet D's Jerky hero and the two Stack Studios properties look related.
     Georgia is the fallback because it is the nearest thing to a Didone that
     ships on every machine -- if the webfont fails the mark degrades to a
     serif rather than snapping to a sans and changing character entirely. */
  --wordmark-font: "Playfair Display", Georgia, serif;
  --display: "Archivo", system-ui, sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, monospace;
}

/* System preference, for the moment before the toggle script runs — at that
   point there is no data-theme attribute at all. Once the script owns the
   attribute this rule can never match again, so a toggle only ever flips
   plain attribute selectors and the two sources can't disagree. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    color-scheme: dark;
    --surface: #121415;
    --nav-veil: rgba(18, 20, 21, 0.84);   /* the TEXT ZONES: 0.84 is what the coastline needs for AA */
    --nav-veil-weak: rgba(18, 20, 21, 0.30);   /* same, on the dark ground */
    --nav-art: url("/brand/nav-ridges-dark-2026-08b-9000.webp");   /* the SAME range, inverted: sky near the ground, ridges rising out of it */
    --surface-2: #141F21;
    --border: #262E30;
    --border-strong: #7C8F91;
    --ink: #E9F0EF;
    --muted: #9BADAE;
    --accent: #5E86DC;
    --accent-ink: #121415;
    --accent-text: #A6BEF0;   /* 9.89:1 on the ground, 9.01:1 on a panel */
    --brand-mark: #8AA9EA;    /* a step lighter than the accent fill: unfilled
                                 type carries no background of its own — 7.87:1 */
    --mark-dim: none;   /* the full gold: 3.09:1 at worst on this ground, so nothing to buy */
    --accent-soft: rgba(94, 134, 220, 0.16);
    --wash: rgba(255, 255, 255, 0.05);
    --wash-strong: rgba(255, 255, 255, 0.1);
    --danger: #F2837B;
    --danger-soft: rgba(242, 131, 123, 0.16);
    --scrim: rgba(2, 8, 9, 0.74);
    --shadow: 0 1px 2px rgba(0, 0, 0, 0.55), 0 6px 18px rgba(0, 0, 0, 0.45);
    --shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.7);
    --edge: inset 0 1px 0 rgba(255, 255, 255, 0.05);
  }
}

/* An explicit choice wins in both directions. */
:root[data-theme="dark"] {
  color-scheme: dark;
  --surface: #121415;
  --nav-veil: rgba(18, 20, 21, 0.84);   /* the TEXT ZONES: 0.84 is what the coastline needs for AA */
  --nav-veil-weak: rgba(18, 20, 21, 0.30);   /* same, on the dark ground */
  --nav-art: url("/brand/nav-ridges-dark-2026-08b-9000.webp");   /* the SAME range, inverted: sky near the ground, ridges rising out of it */
  --surface-2: #141F21;
  --border: #262E30;
  --border-strong: #7C8F91;
  --ink: #E9F0EF;
  --muted: #9BADAE;
  --accent: #5E86DC;
  --accent-ink: #121415;
  --accent-text: #A6BEF0;   /* 9.89:1 on the ground, 9.01:1 on a panel */
  --brand-mark: #8AA9EA;    /* a step lighter than the accent fill: unfilled
                               type carries no background of its own — 7.87:1 */
  --mark-dim: none;   /* the full gold: 3.09:1 at worst on this ground, so nothing to buy */
  --accent-soft: rgba(94, 134, 220, 0.16);
  --wash: rgba(255, 255, 255, 0.05);
  --wash-strong: rgba(255, 255, 255, 0.1);
  --danger: #F2837B;
  --danger-soft: rgba(242, 131, 123, 0.16);
  --scrim: rgba(2, 8, 9, 0.74);
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.55), 0 6px 18px rgba(0, 0, 0, 0.45);
  --shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.7);
  --edge: inset 0 1px 0 rgba(255, 255, 255, 0.05);
}

* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

body {
  overflow-x: clip;   /* full-bleed header cannot make a horizontal scrollbar */
  font-family: var(--display);
  background: var(--surface);
  color: var(--ink);
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
}

/* CORNER RADIUS, as a stated rule rather than a habit. Three tiers, and every
   radius on the page is one of them:
     999px  interactive pills   - buttons, tags, chips
     14-16px  surfaces          - cards, the nav card, panels
     8-10px  small controls     - the theme toggle, the skip link
   Anything outside these three is a mistake, not a fourth tier. */
a { color: var(--accent-text); text-decoration: none; }
a:hover { text-decoration: underline; }
a:focus-visible, button:focus-visible, input:focus-visible,
select:focus-visible, textarea:focus-visible, summary:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--accent-text); outline-offset: 2px;
}

/* Surfaces the browser draws, which otherwise ship in nobody's palette.
   Scrollbars are deliberately NOT set here: `color-scheme` is already declared
   per theme, so the platform draws its own correct dark or light bar, and a
   hand-rolled one would be worse than the native it replaced. */
::selection { background: var(--accent-soft); color: var(--ink); }
:root { caret-color: var(--accent-text); }

/* ── Skip link ───────────────────────────────────────── */
/* Keyboard users shouldn't tab the whole nav on every page. */
.skip {
  position: absolute; left: 8px; top: 8px; z-index: 999;
  transform: translateY(-200%);
  background: var(--accent); color: var(--accent-ink);
  box-shadow: inset 0 0 0 1px var(--accent-text);
  font-weight: 700; font-size: 0.9rem;
  padding: 10px 18px; border-radius: 8px;
  transition: transform 0.15s ease-out;
}
.skip:focus { transform: translateY(0); text-decoration: none; }
main:focus { outline: none; }

/* Announced, not shown. */
.vh {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

.wrap { max-width: var(--max); margin: 0 auto; padding: 0 var(--space-gutter); }

/* ── Nav ─────────────────────────────────────────────── */
nav.site {
  display: flex; align-items: center; gap: 26px;
  /* FULL WIDTH, not the --max content column.
     The nav used to be capped at --max and centred, so on any screen wider
     than 1080px the wordmark sat wherever the centred column began -- left
     within the nav, but hundreds of pixels in from the left of the header. The
     header now carries a full-bleed background, and a centred bar of content
     floating on a full-bleed ground is the mismatch you can see. The brand goes
     hard left against the page gutter and the links go hard right, which is
     also what makes the wordmark's own left edge the page's left edge. */
  padding: 14px var(--space-gutter);
  max-width: none;
  margin: 0;
  position: relative;
  isolation: isolate;      /* the artwork stays behind this nav, not the page */
}

/* ── The header band ──────────────────────────────────────────────────────
   A photograph, veiled down until the type on top of it is comfortably
   readable. It replaced a coordinate graticule -- 56px cells with every fourth
   line heavier -- which was a system rather than a decoration and defensible on
   those terms, but a grid is a DIAGRAM of navigation and this is a PICTURE of
   it: one train crossing a viaduct out of a valley of mist at first light,
   ridges fading back into haze. Generated for this slot, at Chris's direction.

   THE VEIL IS THE GROUND, NOT A DIMMER. One image has to sit under near-black
   ink in light and near-white ink in dark, so the layer over it is --surface at
   an opacity solved for the weakest text pair in each theme rather than picked
   by eye. Both numbers are in the token's own comment, and a probe re-derives
   them from the rendered page.

   background-color under everything, so a failed image request leaves the plain
   ground rather than a transparent nav over page content. */
nav.site::before {
  content: "";
  position: absolute;
  /* FULL BLEED. The nav is capped at --max and centred, so an inset:0
     background stops at the content column. body carries overflow-x: clip, so
     100vw cannot make a scrollbar. */
  inset: 0 calc(50% - 50vw);
  z-index: -1;
  pointer-events: none;
  background-color: var(--surface);
  /* THE VEIL IS HORIZONTAL, AND THE STOPS ARE WHERE THE TEXT IS.
     A flat veil has to be strong enough for the worst pixel that lands behind
     a label, which means the whole picture pays for the two ends of the bar.
     But this nav is brand-left, links-right, and EMPTY IN THE MIDDLE -- so the
     middle does not need protecting and can carry the photograph nearly
     unveiled. Measured positions at 1440px: the brand ends at 19% and the
     links begin at 80%; at 900px, 29% and 67%. The strong band therefore runs
     to 32% and from 64%, which covers both with margin, and the transition is
     wide enough (10%) that no edge is visible.
     Below 620px the nav wraps to two lines and text crosses the whole width,
     so this rule is overridden back to a flat veil at that breakpoint. */
  background-image:
    linear-gradient(to right,
      var(--nav-veil) 0 32%,
      var(--nav-veil-weak) 42% 58%,
      var(--nav-veil) 64% 100%),
    var(--nav-art);
  /* The veil must cover the whole band whatever the photograph does under it,
     so it is sized on its own rather than sharing the image's `cover`. */
  background-size: 100% 100%, cover;
  background-position: center, center;
  background-repeat: no-repeat, no-repeat;
}

/* No fade across the wordmark: the grid runs the full width as one even
   field. A system that stops partway is two treatments meeting rather than
   one composition, and the mark reads perfectly well on it -- gold at full
   strength over rules at 8% was never a contrast problem. */

/* The band STAYS on phones. The rule that stood here hid it, because a MOTIF
   under wrapped text is noise -- but a photograph veiled to the point where it
   reads as a ground is not a motif, and hiding it left the phone header a flat
   rectangle. The contrast probe measures the wrapped two-line layout at 500px
   as its own case rather than assuming the desktop numbers carry over.
   (nav.site::after was in that selector and has never had a rule of its own.) */
/* A GRID, not a column, because the mark now stands beside BOTH lines rather
   than inside the first one. The markup is unchanged -- one anchor holding two
   spans -- so the mark is still a ::before and the second column is placed by
   hand. justify-content keeps the whole thing at its content width inside the
   nav's flex row. */
.brand {
  display: grid;
  grid-template-columns: auto auto;
  grid-template-rows: auto auto;
  justify-content: start;
  align-content: start;
  justify-items: start;
  /* Tracks the type rather than sitting at a fixed px, so the gap shrinks with
     the wordmark at the phone breakpoint. 0.34em of 30px = 10.2px. */
  column-gap: calc(var(--wordmark-size) * 0.34);
  row-gap: 1px;
}
.brand .wordmark { grid-column: 2; grid-row: 1; }
.brand .brand-tag { grid-column: 2; grid-row: 2; }

/* THE MARK: Chris's gold N monogram -- the letter, a compass rose through it
   and an aircraft leaving on a bearing -- lifted off the dark mockup he had
   rendered and keyed to transparency.

   IT IS A RASTER, and that is a real trade against the arrowhead it replaces.
   The arrowhead was an alpha mask filled with a token, so it recoloured per
   theme and cost nothing. This has brushed metal, a bevel and baked lighting;
   none of that survives being reduced to one flat colour, so it ships as
   pixels and wears the same gold on both grounds.

   HEIGHT IS DRIVEN, NOT GUESSED: --brand-block-h is the wordmark plus the
   tagline, computed from the same tokens those two lines are set in. Width
   follows the shipped bitmap's own 194x192, so `contain` has nothing to
   letterbox.

   The asset is 192px tall for a box that renders at 45.4px -- 4x, which covers
   every Retina case with room over, at 17KB. */
.brand::before {
  content: "";
  grid-column: 1;
  grid-row: 1 / 3;
  align-self: start;
  height: var(--brand-block-h);
  width: calc(var(--brand-block-h) * 194 / 192);
  background: url("/brand/navology-mark-gold-192.webp") center / contain no-repeat;
  filter: var(--mark-dim);
}
.brand:hover { text-decoration: none; }
/* Says what the site is for, to someone who has arrived on it for the first
   time and has only the mark to go on. Small and muted deliberately: it is a
   caption to the logo, not a second logo, and the nav is not where a page
   makes its argument. */
.brand-tag {
  font: 500 var(--brand-tag-size)/1.2 var(--display);
  letter-spacing: 0.01em;
  color: var(--muted);
  white-space: nowrap;
}
@media (max-width: 620px) {
  /* Both as tokens: --brand-block-h reads them, so a bare font-size override
     here would leave the mark sized for the desktop tagline. */
  :root { --wordmark-size: 23px; --brand-tag-size: 0.6875rem; }
}
/* The wordmark is set in type: all caps, with the N raised above the rest.
   Archivo, which the site already loads, so it costs no request and stays sharp
   at any size -- and unlike an image of a word, it is a word: selectable, and
   readable without a parallel aria-label.
   The N is 1.22em, not a different weight or colour. Scale alone is the quiet
   version of this move; making it heavier or gold as well would be three
   signals doing one job. Its extra height is absorbed by aligning on the
   baseline, so the cap line steps up and the feet stay level.
   The compass sits before it, drawn at the weight of the type rather than as a
   heavier mark, so the two read as the same hand. */
.brand .wordmark {
  display: inline-flex;
  align-items: baseline;
  /* NO flex gap. The N is its own element so it can be sized, which makes it a
     flex ITEM -- and a gap on the container therefore inserted itself between
     "N" and "avology" as well as after the star. Measured: 9px after the N
     against ~0 between every other pair, which is the uneven spacing, and no
     amount of letter-spacing tuning would have touched it. The star gets a
     margin instead, so only the mark is spaced and the word is set purely by
     its own tracking. */
  gap: 0;
  /* WEIGHT 900, and it has to be. Playfair is a Didone: high contrast between
     thick strokes and hair-thin ones. At 500 the thins are already fragile at
     30px and they break at the 15px mobile size; at 900 the letter carries
     enough mass that they survive both. Measured at both sizes before choosing.
     THE SWAP COST NOTHING. The previous face was requested at three weights and
     used by exactly one rule -- this one -- so trading it for a single Playfair
     weight makes the font request smaller than it was, not larger. */
  font: 900 var(--wordmark-size) / 1 var(--wordmark-font);
  /* Tighter and even. 0.11em was set for a heavier grotesque; a geometric face
     at 500 has more air inside the letters already, so the same tracking pulls
     the word apart. */
  /* Slightly tighter than the 0.165em the geometric face wanted. Playfair's
     caps are wider and heavier, so the same tracking pushed the word past the
     nav's comfortable width and left it looking spaced rather than set. */
  letter-spacing: 0.145em;
  color: var(--brand-mark);
  text-transform: uppercase;
  white-space: nowrap;
  /* NO VERTICAL SCALE. The rule here used to squash the word to 84% of its
     height, and the comment that set it already knew the risk: a vertical
     squeeze thins the horizontal strokes and leaves the verticals alone. At 4x
     it was plainly visible. The O's had gone oval and the crossbars had gone
     light, which is exactly the quality that made a geometric face the right
     choice in the first place. The word is drawn as its designer drew it. */
}

/* The N is no longer set apart. It was enlarged to 1.22em, which bought almost
   nothing at 30px and cost a hand-tuned letter-spacing exception to stop the
   tracking scaling up with it. With a real mark beside the word, an enlarged
   initial has no job left to do. */

/* The bearing arrowhead that stood here is gone -- Chris replaced it with the
   gold monogram, which is drawn on .brand instead because it spans both lines.
   It was a good mark and it did its job for one afternoon. */


nav.site .links { margin-left: auto; display: flex; gap: 22px; align-items: center; font-size: 0.92rem; }
nav.site .links a { color: var(--muted); font-weight: 600; }
nav.site .links a[aria-current="page"], nav.site .links a:hover { color: var(--accent-text); text-decoration: none; }

/* The toggle is one more item in a row that was already full at phone width.
   Let the links drop to their own line rather than push the page sideways. */
@media (max-width: 900px) {
  /* Not enough empty middle to spend on the picture: flat veil at the strong
     value, which is what protects the type. See the note on --nav-veil-weak. */
  nav.site::before { background-image:
    linear-gradient(var(--nav-veil), var(--nav-veil)),
    var(--nav-art); }
}

@media (max-width: 620px) {
  nav.site { flex-wrap: wrap; gap: 10px 14px; padding: 14px 18px; }

  nav.site .links { gap: 16px; font-size: 0.88rem; }
}

/* ── Theme toggle ────────────────────────────────────── */
/* Hidden until the script that powers it has run, so the page never shows a
   control that cannot do anything. */
.theme-toggle { display: none; }
.js .theme-toggle {
  display: inline-grid; place-items: center;
  width: 38px; height: 38px; flex: none;
  background: transparent; color: var(--muted);
  border: 1px solid var(--border-strong); border-radius: 10px;
  cursor: pointer;
  transition: color 0.15s ease-out, border-color 0.15s ease-out, background 0.15s ease-out;
}
.theme-toggle:hover { color: var(--accent-text); border-color: var(--accent-text); background: var(--wash); }
.theme-toggle svg { width: 17px; height: 17px; }
/* Show the destination, not the current state: a moon means "go dark". */
.theme-toggle .i-sun { display: none; }
:root[data-theme="dark"] .theme-toggle .i-sun { display: block; }
:root[data-theme="dark"] .theme-toggle .i-moon { display: none; }

/* ── Hero ────────────────────────────────────────────── */
/* The hero is the claim, not a description of it. A full-bleed photograph of a
   family already on the trip: the planning is finished, the phone is face down
   on the boards, and nobody is looking at a screen. That is the product's whole
   argument, made before a word is read.

   It replaced a generated drawing of Tokyo's real rail network. That drawing was
   accurate, honest, and derived from the app's own track data — and it read as
   scribbles to anyone not already interested in rail data. The generator is
   still in the repo (tools/build-hero-rail.mjs) and still passes its assertions;
   it is simply not what a family arriving on this page needs to see.

   Text sits BELOW the image, never over it. An earlier pass measured a coloured
   rail line crossing the h1's own box at 178/255 deviation from the ground; the
   lesson generalises — a headline over a bright, busy photograph is a contrast
   problem you tune forever instead of a layout you get right once. */
.hero { padding: 0 0 clamp(56px, 5.5vw, 84px); position: relative; }
.hero .wrap { position: relative; z-index: 1; }

.herofig {
  /* Full-bleed without the 100vw trick — that one overflows by the scrollbar's
     width and hands the page a sideways scroll. */
  width: 100%;
  margin-bottom: clamp(26px, 2.8vw, 44px);
  overflow: hidden;
  background: var(--wash);        /* holds the band's colour before paint */
  position: relative;             /* the ambient video stacks on the still */
}
.heroimg {
  display: block; width: 100%;
  /* Sized against the FOLD. The picture is the second thing this page has to
     do; getting someone to the planner is the first, so the band is capped
     rather than running to its full 21:9 height on a wide screen. */
  height: clamp(200px, 26vw, 370px);
  object-fit: cover;
  /* The family sits low-left and the lake fills the right. Holding the crop a
     little above centre keeps the eaves and the far shore in frame when the
     band is short, instead of cropping to bare water. */
  object-position: 50% 42%;
}

/* The ambient hero loop. Every geometry value here is COPIED from .heroimg
   above and must move with it: the video is cropped by the same rules as the
   still, or the fade between them slides the picture sideways.

   It starts at opacity 0 with no source attached. js/hero-video.js gives it a
   source after the load event and reveals it on `playing`, so the still keeps
   the largest paint and the swap only ever happens on a frame that is ready.
   Only opacity animates — no layout, no paint, no CLS. */
.herovid {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: 50% 42%;
  opacity: 0;
  pointer-events: none;
}
.herovid.ready { opacity: 1; }

@media (prefers-reduced-motion: no-preference) {
  /* The transition lives inside the no-preference guard rather than relying on
     the global reduce block to strip it. Under `reduce` the element is removed
     outright below, so there is nothing left to animate either way. */
  .herovid { transition: opacity 0.9s ease-out; }
}
@media (prefers-reduced-motion: reduce) {
  /* Not merely hidden: `display: none` keeps it out of the layer tree, and the
     script checks the same query before it ever requests the file. */
  .herovid { display: none; }
}

h1 {
  /* Two lines at desktop, not three. 15ch at 4.6rem forced a third line and
     pushed the call to action to y=996, below the fold on a 1440x900 laptop. */
  font-size: clamp(2.2rem, 5.4vw, 3.9rem); line-height: 1.05;
  font-weight: 900; letter-spacing: -0.03em; margin-bottom: 20px;
  text-wrap: balance; max-width: 21ch;
}
h1 em { color: var(--accent-text); font-style: normal; }
.lede { color: var(--muted); font-size: 1.12rem; line-height: 1.7; max-width: 46ch; margin-bottom: 30px; text-wrap: pretty; }

.cta-row { display: flex; gap: 14px; flex-wrap: wrap; align-items: center; }
.btn {
  display: inline-block; font-weight: 800; font-size: 0.98rem;
  padding: 13px 26px; border-radius: 999px; border: 0; cursor: pointer;
}
.btn-primary { background: var(--accent); color: var(--accent-ink); box-shadow: inset 0 0 0 1px var(--accent-text); }
.btn-primary:hover { text-decoration: none; filter: brightness(1.08); }
.btn-ghost { background: transparent; color: var(--ink); border: 1.5px solid var(--border-strong); }
.btn-ghost:hover { text-decoration: none; border-color: var(--accent-text); color: var(--accent-text); }
.cta-note { font-family: var(--mono); font-size: 0.78rem; color: var(--muted); }
/* The notify link replaced a dead "coming soon" label — it does something. */
.cta-note a { font-family: var(--mono); color: var(--accent-text); }

/* The GO card, rebuilt in HTML — the app's most characteristic artifact,
   and a live demo rather than a claim. */
.gocard {
  background: var(--surface-2); color: var(--ink);
  border: 1px solid var(--border);
  border-radius: 16px; padding: 28px 28px 22px;
  box-shadow: var(--edge), var(--shadow);
  max-width: 340px; margin: 0 auto; text-align: center;
}
.gocard .banner {
  display: flex; align-items: center; gap: 8px; justify-content: flex-start;
  font-size: 0.82rem; font-weight: 700; color: var(--muted); margin-bottom: 18px;
}
.gocard .banner svg { width: 15px; height: 15px; flex: none; color: var(--accent-text); }
.gocard .countdown {
  font-family: var(--mono); font-size: 3.4rem; font-weight: 700;
  letter-spacing: -0.03em; color: var(--ink); line-height: 1;
  font-variant-numeric: tabular-nums;
}
.gocard .instruction { font-size: 1.25rem; font-weight: 800; margin: 10px 0 4px; text-wrap: balance; }
.gocard .then { font-size: 0.85rem; color: var(--muted); margin-bottom: 18px; }
.gocard .end {
  background: var(--accent); color: var(--accent-ink); border-radius: 999px;
  box-shadow: inset 0 0 0 1px var(--accent-text);
  font-weight: 800; padding: 12px 0; width: 100%; font-size: 0.95rem;
}
/* Two lines are reserved because the spoken line changes length as the demo
   cycles — without the floor the whole card grows and shrinks under it. */
.gocard .voice {
  display: flex; align-items: flex-start; justify-content: center; gap: 7px;
  font-family: var(--mono); font-size: 0.72rem; color: var(--muted);
  margin-top: 12px; min-height: 2.7em;
}
.gocard .voice svg { width: 13px; height: 13px; flex: none; }

/* ── Sections ────────────────────────────────────────── */
section.block { padding: var(--space-section) 0 var(--space-section-end); position: relative; }

/* The last section is nothing but the closing ask, so it is centred: a single
   left-aligned column of three lines and two buttons left the right 60% of the
   band empty and read as an unfinished row rather than as a conclusion. */
section.block.closing { text-align: center; }
section.block.closing .intro { margin-left: auto; margin-right: auto; }
section.block.closing .cta-row { justify-content: center; }
section.block.alt { background: var(--surface-2); }
h2 { font-size: clamp(1.5rem, 3vw, 2.1rem); font-weight: 900; letter-spacing: -0.01em; margin-bottom: 16px; text-wrap: balance; }
.block .intro { color: var(--muted); max-width: 58ch; margin-bottom: 36px; text-wrap: pretty; }

/* Two claims carry the product; the rest are a list. Six identical cards
   let the reader assume everything here weighs the same. */
/* min() on the floor, or the track keeps its 290px on a narrower screen and
   pushes the page sideways. */
.features-lead {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(290px, 100%), 1fr));
  gap: 22px; margin-bottom: 34px;
}
.feature-lead {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 14px; padding: 32px;
  box-shadow: var(--edge), var(--shadow);
}
section.block.alt .feature-lead { background: var(--surface); }
.feature-lead .glyph { display: block; color: var(--accent-text); margin-bottom: 14px; }
.feature-lead .glyph svg { width: 26px; height: 26px; }
.feature-lead h3 { font-size: 1.18rem; font-weight: 800; margin-bottom: 10px; letter-spacing: -0.01em; }
.feature-lead p { color: var(--muted); max-width: 46ch; }
.feature-lead .tag {
  display: inline-block; margin-top: 14px;
  font-size: 0.78rem; font-weight: 700; color: var(--accent-text);
  background: var(--accent-soft); border-radius: 999px; padding: 3px 12px;
}

/* THREE items now that turn-by-turn was promoted into its own band, so the
   floor drops to land them 3 across in one clean row instead of 2 + orphan. */
.feature-list {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
  column-gap: 40px;
}
.feature-item {
  display: grid; grid-template-columns: auto 1fr; gap: 14px;
  padding: 22px 0; border-top: 1px solid var(--border);
}
.feature-item .glyph { color: var(--accent-text); padding-top: 2px; }
.feature-item .glyph svg { width: 18px; height: 18px; display: block; }
.feature-item h3 { font-size: 0.98rem; font-weight: 800; margin-bottom: 4px; }
.feature-item p { font-size: 0.92rem; color: var(--muted); }

/* The navigation card moved here from the hero, and the move is the point.
   In the hero it was a 340px box whose "End" button rendered 282px wide in
   saturated cobalt — measured WIDER than the page's actual call to action, so
   the loudest thing above the fold was a fake button reading "End". Beside the
   claim it illustrates it is evidence rather than decoration, and the End
   button is gone because on a landing page it was never a control.

   Its own band rather than a cell in the feature grid: that grid is `auto-fit`,
   so an explicitly placed item fights the auto-placement of everything after
   it, and the section reads better as two claims, one demonstration, three
   notes than as five things of equal weight. */
.showcase {
  display: grid; grid-template-columns: 1fr auto;
  gap: clamp(28px, 4vw, 64px); align-items: center;
  padding: 38px 0 34px;
  border-top: 1px solid var(--border);
}
.showcase-text h3 { font-size: 1.18rem; font-weight: 800; margin-bottom: 10px; letter-spacing: -0.01em; }
.showcase-text p { color: var(--muted); max-width: 48ch; }
.showcase-text .tag {
  display: inline-block; margin-top: 14px;
  font-size: 0.78rem; font-weight: 700; color: var(--accent-text);
  background: var(--accent-soft); border-radius: 999px; padding: 3px 12px;
}
.gocard-figure { max-width: 340px; }
.gocard-figure figcaption {
  margin-top: 14px; font-size: 0.82rem; line-height: 1.55;
  color: var(--muted); text-wrap: pretty; text-align: center;
}
/* One column once the pair would squeeze the card below its own measure. */
@media (max-width: 820px) {
  .showcase { grid-template-columns: 1fr; }
  .gocard-figure { justify-self: center; }
}


/* ── Motion ──────────────────────────────────────────────
   Two moments, and each one has a reason it could be defended out loud:

   1. THE HERO SETTLES. Headline, then subtext, then the call to action, each
      rising a few pixels as it fades in. It establishes reading order at first
      paint, which is the one moment the page gets to say what to look at first.
   2. CLAIMS ARRIVE AS THEY BECOME RELEVANT. Each card, the demonstration, and
      each supporting note fades up as it enters the viewport, so the reader
      meets one claim at a time instead of a wall.

   Everything else stays still on purpose. This is a product whose pitch is that
   it does not oversell, and a page that fidgets contradicts that.

   THE PERFORMANCE STORY, because it is the whole reason this is built this way:

   - No JavaScript and no library. The scroll reveals use the native
     `animation-timeline: view()`, so the browser drives them on the compositor.
     There is no scroll listener, no IntersectionObserver, no rAF loop, and no
     main-thread work per frame. `window.addEventListener("scroll")` would have
     been the obvious way to do this and it is exactly the wrong one.
   - Only `opacity` and `transform` are animated. Both composite; neither
     triggers layout or paint, so none of this can cause CLS.
   - The hero PHOTOGRAPH is deliberately not animated. It is the LCP element,
     and putting an entrance on it would delay the largest paint to win a flourish.

   TWO GUARDS, AND THE ORDER MATTERS:

   - The hidden start state lives INSIDE `prefers-reduced-motion: no-preference`.
     It cannot live outside it: the global reduce block below kills animations
     with `animation: none !important`, so an element parked at `opacity: 0`
     outside this guard would never come back. That is not hypothetical; it is
     the same failure the hero's previous draw-in animation was built to avoid.
   - `@supports (animation-timeline: view())` gates the scroll reveals, so a
     browser without scroll-driven animations (Firefox today) simply renders the
     finished page. Degrading to "already visible" is the only acceptable
     fallback for a reveal. */

@keyframes rise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: no-preference) {
  /* 1. The hero settles, once, on load. Plain keyframes: no timeline needed,
        so this works in every browser and costs one composited animation. */
  .hero .wrap > * { animation: rise 0.7s cubic-bezier(0.16, 1, 0.3, 1) both; }
  .hero h1       { animation-delay: 0.04s; }
  .hero .lede    { animation-delay: 0.13s; }
  .hero .cta-row { animation-delay: 0.21s; }

  /* 2. Scroll reveals, driven by the viewport rather than by a listener. The
        stagger comes from each element's own scroll position, not from a delay
        chain, so it stays correct however fast the reader scrolls. */
  @supports (animation-timeline: view()) {
    .feature-lead,
    .showcase-text,
    .gocard-figure,
    .feature-item,
    .closing .wrap > * {
      animation: rise 1s linear both;
      animation-timeline: view();
      animation-range: entry 8% cover 26%;
    }
    /* Side-by-side siblings share a scroll position, so they need a nudge or
       they arrive in unison and read as one block sliding in. */
    .features-lead > :nth-child(2) { animation-range: entry 12% cover 31%; }
    .feature-list  > :nth-child(2) { animation-range: entry 11% cover 29%; }
    .feature-list  > :nth-child(3) { animation-range: entry 14% cover 33%; }
  }
}

/* Tactile feedback: the button moves under the press. One frame of transform,
   no guard needed beyond the global reduce block. */
.btn { transition: transform 0.12s ease-out, filter 0.15s ease-out; }
.btn:active { transform: translateY(1px); }

/* ── Footer ──────────────────────────────────────────── */
footer.site {
  background: var(--surface-2); border-top: 1px solid var(--border);
  padding: 56px 22px 42px;
  font-size: 0.85rem; color: var(--muted);
}
footer.site .cols {
  max-width: var(--max); margin: 0 auto; display: grid;
  grid-template-columns: 2fr 1fr 1fr; gap: 30px;
}
@media (max-width: 760px) { footer.site .cols { grid-template-columns: 1fr; } }
/* h3, not h4: the page's last section heading is an h2, so an h4 here skipped
   a level in the document outline on every page. Appearance is unchanged. */
footer.site h3 { color: var(--ink); font-size: 0.8rem; letter-spacing: 0.12em; text-transform: uppercase; margin-bottom: 10px; }
/* Scoped to the link COLUMNS. Unscoped, `display: block` also caught the
   OpenStreetMap attribution link inside the legal paragraph and broke it onto
   a line of its own, so the credit read as three ragged fragments. */
footer.site .cols a { display: block; color: var(--muted); margin-bottom: 6px; }
footer.site a { color: var(--muted); }
footer.site a:hover { color: var(--accent-text); }
footer.site .legal a { text-decoration: underline; text-underline-offset: 2px; }
footer.site .legal { max-width: var(--max); margin: 26px auto 0; padding-top: 20px; border-top: 1px solid var(--border); font-size: 0.76rem; }
/* Fine print is still print: unbounded, the attribution ran 178 characters to
   the line at 1440px. */
footer.site .legal p { margin-bottom: 4px; max-width: 92ch; }

/* ── Shared form + card vocabulary ───────────────────── */
.planner { max-width: 900px; margin: 0 auto; padding: 30px 22px 90px; }
.card {
  background: var(--surface-2); color: var(--ink);
  border: 1px solid var(--border);
  border-radius: 14px; padding: 22px; margin-bottom: 18px;
  box-shadow: var(--edge), var(--shadow);
}
.card h3 { font-size: 1rem; font-weight: 800; margin-bottom: 14px; }
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
@media (max-width: 640px) { .grid2 { grid-template-columns: 1fr; } }
label { font-size: 0.78rem; font-weight: 700; color: var(--muted); display: block; margin-bottom: 4px; }
input, select, textarea {
  width: 100%; font: inherit; font-size: 0.95rem; padding: 10px 12px;
  border: 1.5px solid var(--border-strong); border-radius: 10px;
  background: var(--surface-2); color: var(--ink);
}
input::placeholder, textarea::placeholder { color: var(--muted); opacity: 1; }
textarea { resize: vertical; min-height: 60px; }
.daytabs { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
.daytab {
  font-family: var(--mono); font-size: 0.8rem; font-weight: 600;
  padding: 7px 14px; border-radius: 999px; border: 1.5px solid var(--border-strong);
  background: var(--surface-2); color: var(--ink); cursor: pointer;
}
.daytab[aria-selected="true"] { background: var(--accent); color: var(--accent-ink); border-color: var(--accent-text); }
.item-row {
  display: grid; grid-template-columns: auto 1fr auto; gap: 12px;
  padding: 12px; border: 1px solid var(--border); border-radius: 12px;
  margin-bottom: 10px; background: var(--surface); align-items: start;
}
.item-kind { font-size: 1.2rem; padding-top: 2px; }
.item-main strong { display: block; font-size: 0.95rem; }
.item-main span { font-size: 0.8rem; color: var(--muted); }
.item-main .time { font-family: var(--mono); color: var(--ink); font-weight: 600; margin-right: 8px; }
.item-actions { display: flex; gap: 6px; }
.mini {
  font-size: 0.78rem; font-weight: 700; padding: 6px 10px;
  border: 1px solid var(--border-strong); border-radius: 8px; background: var(--surface-2);
  color: var(--ink); cursor: pointer;
}
.mini:hover { border-color: var(--accent-text); }
.mini.danger { color: var(--danger); }
.toolbar { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; margin-top: 8px; }
.empty-note { font-size: 0.88rem; color: var(--muted); padding: 8px 2px; }
.export-hint {
  background: var(--accent-soft); border: 1px solid var(--accent-text);
  border-radius: 12px; padding: 14px 16px; font-size: 0.88rem; color: var(--ink);
  margin-top: 16px;
}
dialog.item-editor { border: 0; border-radius: 14px; padding: 0; max-width: 480px; width: calc(100vw - 40px); }
dialog.item-editor::backdrop { background: var(--scrim); }

/* ── Data page ───────────────────────────────────────── */
/* The measure is centred so a desktop screen doesn't leave 40% of itself
   empty to the right of the text. */
.prose { max-width: 68ch; margin-inline: auto; }
.prose p { margin-bottom: 1em; color: var(--muted); text-wrap: pretty; }
.prose h2 { margin-top: 2em; }
.prose h2:first-of-type { margin-top: 0; }
.prose ul { padding-left: 1.3em; margin-bottom: 1em; color: var(--muted); }
.prose li { margin-bottom: 0.4em; }
.prose strong { color: var(--ink); }
.page-head { max-width: 68ch; margin-inline: auto; }
code { font-family: var(--mono); font-size: 0.88em; background: var(--wash); padding: 2px 6px; border-radius: 6px; }

/* The two downloads are the point of the page, so they get a panel rather
   than three columns of a table. */
.downloads { display: grid; gap: 12px; margin: 22px 0 28px; }
.download {
  display: grid; grid-template-columns: auto 1fr auto; gap: 16px; align-items: start;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 12px; padding: 18px 20px;
  box-shadow: var(--edge), var(--shadow);
}
.download .dl-icon { color: var(--accent-text); padding-top: 3px; }
.download .dl-icon svg { width: 20px; height: 20px; display: block; }
.download .dl-name {
  font-family: var(--mono); font-size: 0.92rem; font-weight: 700;
  white-space: nowrap;   /* a filename that breaks mid-word reads as a typo */
}
.download p { font-size: 0.88rem; color: var(--muted); margin-top: 4px; }
.download .dl-size { font-family: var(--mono); font-size: 0.8rem; color: var(--muted); white-space: nowrap; }
@media (max-width: 560px) {
  .download { grid-template-columns: auto 1fr; }
  .download .dl-size { grid-column: 2; }
}

.data-table { width: 100%; border-collapse: collapse; margin: 20px 0; font-size: 0.9rem; }
.data-table th, .data-table td { text-align: left; padding: 12px 14px; border-bottom: 1px solid var(--border); }
.data-table th { font-family: var(--mono); font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); }
.data-table td { color: var(--muted); }
.data-table td:first-child { color: var(--ink); font-weight: 600; white-space: nowrap; }

/* ── Contents list (privacy) ─────────────────────────── */
/* Fourteen flat h2s are a wall; this gives the page a map. */
.contents {
  max-width: 68ch; margin: 0 auto 36px;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 12px; padding: 18px 22px;
}
.contents-title {
  font-size: 0.78rem; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--muted); margin: 0 0 10px; font-weight: 700;
}
.contents ol {
  list-style: none; padding: 0; margin: 0;
  columns: 2; column-gap: 30px;
}
@media (max-width: 560px) { .contents ol { columns: 1; } }
.contents li { margin-bottom: 5px; break-inside: avoid; }
.contents a { font-size: 0.88rem; color: var(--ink); }
.contents a:hover { color: var(--accent-text); }
