:root {
  color-scheme: light;
  --paper: #f4f1ea;
  --ink: #121a23;
  --muted: #667078;
  --accent: #0b668f;
  --focus: #ef5a3c;

  /* Mirrors the used height of .field (height: min(860px, 96svh) floored by
     min-height: 660px). The orbit the destinations and the stamp share is
     derived from it, so both track the field at every window size. */
  --field-h: max(660px, min(860px, 96svh));
  --field-w: min(1000px, 96vw);

  /* Uniform destination box. Fixing both axes is what makes a single shared
     radius reproduce all four of the old edge-anchored positions: with the old
     shrink-to-fit box every link had a different half-width and the description
     font-size tracks 1.5vw, so no one radius could land them all. The box is a
     positioning frame, not a container: .destination__description is nowrap and
     is allowed to spill past it (outwards, away from the mark, since east is
     left-aligned and west right-aligned), which is what keeps the frame a
     constant while the type inside it scales. */
  --dest-w: 12rem;
  --dest-h: 4.25rem;

  /* .destination--north used to be top: 3.5%, so its centre sat
     (50% - 3.5%)*H - h/2 = 0.465*H - h/2 above the field centre. That distance
     is the orbit radius, and on a landscape window it is the binding one:
     north/south then reproduce their old position exactly and east/west pull in
     from the field edge by however much the field is wider than it is tall.
     On a portrait window the width binds instead, capping the radius at the old
     right:0 / left:0 position so nothing can be pushed off the field. */
  --orbit: min(
    calc(0.465 * var(--field-h) - var(--dest-h) / 2),
    calc(var(--field-w) / 2 - var(--dest-w) / 2)
  );

  /* The stamp sits one half-box inside the orbit, i.e. tangent to the inner
     edge of the north and south boxes, so the labels ring the rim instead of
     being struck through by it. */
  --ring-r: calc(var(--orbit) - var(--dest-h) / 2);

  /* 36.4885% of 13.10s = 4.780s exactly: the earliest bloom disc (bl2, seeded
     at the knot) starts growing here -- the instant the ink itself begins.
     The seal appears with the bloom rather than waiting for the whole mark to
     finish settling. */
  --stamp-at: 4.78s;

  --reveal-dur: 900ms;
  --reveal-ease: cubic-bezier(0.2, 0.7, 0.2, 1);

  /* The mark's settle/breathe begins at 68.3206% of its 13.10s timeline:
     8.95s. The dial starts there and completes one clockwise turn per minute. */
  --settle-start: 8.95s;
  --stamp-turn: 60s;

  /* The destinations begin as soon as the stamp's 900ms entrance completes.
     Mobile skips this animation entirely and shows the links from first paint. */
  --reveal-at: calc(var(--stamp-at) + var(--reveal-dur));
}

* {
  box-sizing: border-box;
}

html,
body {
  min-height: 100%;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: ui-monospace, "SFMono-Regular", "Cascadia Mono", "Roboto Mono", monospace;
  text-rendering: geometricPrecision;
}

.field {
  position: relative;
  width: min(1000px, 96vw);
  height: min(860px, 96svh);
  min-height: 660px;
  margin: 0 auto;
}

.mark {
  position: absolute;
  top: 50%;
  left: 50%;
  /* Height is the binding constraint: the mark is centred in .field and the
     north/south blocks sit at 3.5% inset, so 58vh keeps a gap at every field
     height (min-height 660px through the 860px cap). 52vw guards the
     east/west blocks on narrow-but-tall windows. */
  width: clamp(340px, min(58vh, 52vw), 560px);
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  transform: translate(-50%, -50%);
  /* The button is still the thing that carries focus and the click listener,
     but it is not a pointer target: only .mark__hit re-enables pointer-events,
     so a pointer click has to land on the ink. pointer-events does not affect
     event propagation, so a click on .mark__hit still bubbles to this button,
     and it does not affect focusability, so Tab + Enter/Space still activate
     it for keyboard users. */
  pointer-events: none;
}

.mark__art {
  display: block;
  width: 100%;
  height: auto;
}

.mark__hit {
  pointer-events: all;
  cursor: pointer;
}

.mark:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 8px;
}

/* The stamp: the two ring &lt;circle&gt;s from logos/04-badge.svg, inlined verbatim
   in index.html inside that file's own 400x400 viewBox (r=175/160,
   stroke-width=6/2, #3b2a1a) — the actual asset, not a CSS reinterpretation of
   its ratios. This rule only sizes and positions that SVG; the ring geometry
   itself lives in the markup. The badge's outer circle (r=175) sits inside a
   400-wide viewBox, i.e. at 350/400 = 87.5% of the SVG's own box — so to make
   that circle land at the shared --ring-r on screen, the SVG element itself
   is sized 2*ring-r / 0.875 = 2*ring-r * 8/7. stroke-opacity in the markup is
   the only thing tuned for "slightly visible"; nothing else about the badge's
   proportions is touched. */
.stamp {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 0;
  width: calc(2 * var(--ring-r) * 8 / 7);
  height: calc(2 * var(--ring-r) * 8 / 7);
  transform: translate(-50%, -50%);
  pointer-events: none;
  animation: stamp-in var(--reveal-dur) var(--reveal-ease) var(--stamp-at) both;
}

.stamp__dial {
  transform-box: view-box;
  transform-origin: center;
  animation: stamp-spin var(--stamp-turn) linear var(--settle-start) infinite;
}

@keyframes stamp-spin {
  to {
    transform: rotate(360deg);
  }
}

.destination {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 1;
  display: grid;
  align-content: center;
  gap: 0.4rem;
  width: var(--dest-w);
  height: var(--dest-h);
  padding: 0.7rem;
  color: inherit;
  text-decoration: none;
  /* Shared angular placement: centre the box on the field centre, swing it out
     to --angle, then unswing the box itself so the text stays upright. The two
     rotations always cancel, including mid-animation, so the box orbits without
     ever tipping. This is also the resting state the entrance lands on, and the
     state reduced-motion falls back to. */
  transform:
    translate(-50%, -50%)
    rotate(var(--angle))
    translateY(calc(-1 * var(--orbit)))
    rotate(calc(-1 * var(--angle)));
  animation: orbit-in var(--reveal-dur) var(--reveal-ease)
    calc(var(--reveal-at) + var(--stagger)) both;
}

@keyframes stamp-in {
  from {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.965);
  }

  to {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1);
  }
}

/* A quarter-turn clockwise into place. pointer-events is animated discretely so
   the links are not phantom click targets while they are invisible;
   it flips to auto halfway through the swing. */
@keyframes orbit-in {
  from {
    opacity: 0;
    pointer-events: none;
    transform:
      translate(-50%, -50%)
      rotate(calc(var(--angle) - 90deg))
      translateY(calc(-1 * var(--orbit)))
      rotate(calc(90deg - var(--angle)));
  }

  to {
    opacity: 1;
    pointer-events: auto;
    transform:
      translate(-50%, -50%)
      rotate(var(--angle))
      translateY(calc(-1 * var(--orbit)))
      rotate(calc(-1 * var(--angle)));
  }
}

.destination__name {
  color: var(--muted);
  white-space: nowrap;
  font-size: 0.68rem;
  font-weight: 650;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

.destination__description {
  /* nowrap keeps every box exactly two lines tall, which is what makes --dest-h
     a constant the orbit can be computed from. The ::after arrow is in flow at
     all times (opacity, not display), so this also stops hover from reflowing
     anything. */
  white-space: nowrap;
  font-family: Georgia, "Times New Roman", serif;
  font-size: clamp(1.05rem, 1.5vw, 1.28rem);
  letter-spacing: -0.012em;
}

.destination__description::after {
  content: "  ↗";
  color: var(--accent);
  opacity: 0;
  transition: opacity 120ms ease;
}

.destination:hover .destination__description,
.destination:focus-visible .destination__description {
  color: var(--accent);
}

.destination:hover .destination__description::after,
.destination:focus-visible .destination__description::after {
  opacity: 1;
}

.destination:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 4px;
}

/* rotate(a) maps (0, -R) to (R sin a, -R cos a) with y running down, so
   0deg is up, 90deg right, 180deg down, 270deg left. The 90ms stagger runs the
   same way round, ready for additional public destinations later. */
.destination--north {
  --angle: 0deg;
  --stagger: 0ms;
  text-align: center;
}

.destination--east {
  --angle: 90deg;
  --stagger: 90ms;
  text-align: left;
}

.destination--south {
  --angle: 180deg;
  --stagger: 180ms;
  text-align: center;
}

.destination--west {
  --angle: 270deg;
  --stagger: 0ms;
  text-align: right;
}

.not-found {
  display: grid;
  place-items: center;
  align-content: center;
  min-height: 100svh;
  padding: 2rem;
  text-align: center;
}

.not-found__mark {
  width: clamp(140px, 22vw, 220px);
  height: auto;
  margin-bottom: 1.5rem;
}

.not-found h1 {
  margin: 0 0 2rem;
  font-family: Georgia, "Times New Roman", serif;
  font-size: clamp(2.2rem, 7vw, 4.8rem);
  font-weight: 400;
  letter-spacing: -0.04em;
}

.not-found a {
  color: var(--accent);
  font-size: 0.82rem;
  text-underline-offset: 0.3em;
}

@media (max-width: 700px) {
  body {
    overflow: auto;
  }

  .field {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-content: center;
    gap: 1rem;
    width: min(34rem, 100%);
    height: auto;
    min-height: 100svh;
    padding: 3rem 1.25rem;
  }

  .mark {
    position: static;
    grid-column: 1 / -1;
    grid-row: 1;
    width: min(86vw, 440px);
    margin: 0 auto 1.5rem;
    transform: none;
  }

  /* A ring is meaningless around a stacked grid, and there is no orbit for the
     links to swing along, so both the stamp and the entrance are dropped here.
     The links stay visible from load, exactly as before: withholding them for
     11.65s on a phone costs a real arrival-and-tap with nothing bought back. */
  .stamp {
    display: none;
  }

  .destination {
    position: static;
    width: auto;
    height: auto;
    min-width: 0;
    padding: 0.85rem 0;
    text-align: left;
    transform: none;
    animation: none;
    opacity: 1;
    pointer-events: auto;
  }

  /* No orbit to keep a constant box for, and a phone column is narrower than
     the longest description, so let it wrap again. */
  .destination__description {
    white-space: normal;
  }

  /* Phones have no persistent hover state. Keep the outbound cue visible so
     each destination reads as a touch target before it is tapped. */
  .destination__description::after {
    opacity: 0.65;
  }

  .destination:only-of-type {
    grid-column: 1 / -1;
    text-align: center;
  }

  .destination--north,
  .destination--east,
  .destination--south,
  .destination--west {
    transform: none;
  }
}

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

  /* Falls back to the base rules: the resting transform, opacity 1, pointer
     events on. Matches the inline SVG's own reduced-motion block, which pins
     the mark to its settled frame. */
  .stamp,
  .stamp__dial,
  .destination {
    animation: none;
    opacity: 1;
  }
}
