/* Display typography: hollow stroked letterforms against the dark field.
   This is the design's signature — see the 50-jahre-hitparade reference. */

.outlined {
  display: block;
}

.outlined text {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1;
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  font-weight: 800;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.outlined--thin text { stroke-width: 0.75; }

/* UI chrome shares one typographic register: uppercase, wide, small. */
.chrome {
  text-transform: uppercase;
  letter-spacing: 0.3em;
  word-spacing: 0.075em;
  font-size: 0.85rem;
  line-height: 1.4;
  color: var(--accent);
}

.stage {
  position: fixed;
  inset: 0;
  z-index: 0;
  width: 100vw;
  height: 100vh;
  display: block;
  pointer-events: none;
  opacity: 0;
  will-change: opacity;
}

.stage.is-active {
  transition: opacity 1.5s linear;
  will-change: auto;
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .stage { display: none; }
}

/* Labels are DOM, projected from 3D each frame by label.js — real text,
   sharper and cheaper than sprites, and selectable. Position is set inline
   per frame; do not fight it from CSS. */
.labels {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}

.lab {
  position: absolute;
  /* Anchored at the origin — label.js folds the projected screen position
     into `transform: translate(...)` on every write instead of left/top,
     since only transform/opacity/filter may be animated here. A left/top
     write on an absolutely-positioned element forces synchronous layout;
     translate() does not. */
  left: 0;
  top: 0;
  white-space: nowrap;
  /* Left-centre origin: label.js computes the final left edge in pixels
     (layoutX) and scales must not shift it — with the default 50% 50%
     origin a scaled label's left edge would drift right of where the
     overflow math put it. */
  transform-origin: 0 50%;
  transition: opacity 0.25s linear;
  /* Default, pre-first-frame state: invisible and at the prototype's base
     offset. label.js overwrites opacity/transform inline once place() runs;
     these are only what a label looks like in the gap before that first
     call. */
  opacity: 0;
  transform: translate(14px, -50%);
}

.lab .kicker {
  display: block;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  opacity: 0.85;
  margin-bottom: 2px;
}

.lab .title {
  display: block;
  font-size: 19px;
  font-weight: 400;
  letter-spacing: 0.01em;
  color: #cbd3ff;
  line-height: 1.15;
  margin-bottom: 5px;
}

.lab .meta {
  display: flex;
  align-items: center;
  gap: 8px;
}

.lab .badge {
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--space-0);
  font-size: 10px;
  font-weight: 800;
}

.lab .date {
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  opacity: 0.8;
}

.year-ghost {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 0;
  font-size: 22vmin;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: #fff;
  opacity: 0.055;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .labels,
  .year-ghost {
    display: none;
  }
}

/* Arrival photograph: fades in on approach, releases on departure. src is
   swapped in journey.js only when the nearest moment changes; .is-arrived
   is toggled independently as the camera moves in and out of the threshold
   around it. */
.arrival-photo {
  position: fixed;
  right: 5vw;
  top: 50%;
  z-index: 2;
  width: min(30vw, 380px);
  pointer-events: none;
  opacity: 0;
  transform: translateY(-50%) scale(0.98);
  transition: opacity 0.5s ease-out, transform 0.5s ease-out;
  border: 1px solid rgba(255, 255, 255, 0.14);
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.55);
  /* The card is taller than its photo now that a caption sits below it;
     the backdrop is what keeps the caption legible over the starfield. */
  background: rgba(7, 7, 26, 0.82);
}

.arrival-photo.is-arrived {
  opacity: 1;
  transform: translateY(-50%) scale(1);
}

.arrival-photo img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  display: block;
}

/* The moment's own story line, swapped alongside the photograph in
   journey.js whenever the nearest moment changes. */
.arrival-photo-caption {
  padding: 10px 12px 12px;
  font-size: 12px;
  line-height: 1.5;
  color: rgba(245, 245, 247, 0.82);
}

.arrival-photo-caption:empty {
  display: none;
}

/* Fixed chrome: the wordmark and scroll hint sit above everything else.
   Scoped to body.is-immersive, same as .journey/.arrival above — on the
   no-WebGL and no-JS paths is-immersive is never added, and without this
   scope both would sit position: fixed on top of the plain photo essay
   that renders instead. */
body.is-immersive .wordmark-fixed {
  position: fixed;
  top: 22px;
  left: 28px;
  z-index: 3;
  pointer-events: none;
}

body.is-immersive .scroll-hint {
  position: fixed;
  bottom: 28px;
  left: 0;
  right: 0;
  text-align: center;
  z-index: 3;
  pointer-events: none;
  opacity: 0.75;
  animation: glow 8s ease-in-out infinite;
}

@keyframes glow {
  0%, 100% { filter: none; }
  50% { filter: drop-shadow(0 0 20px currentColor); }
}

/* The ending: the destination the journey was travelling toward. Revealed
   by journey.js as End.reveal climbs from 0 to 1 over the last band of
   scroll, written straight to this element's inline opacity every frame —
   do not transition it here, or the fade will lag the scroll it tracks.
   It duplicates the accessible .arrival section for sighted users once the
   scene is immersive; .arrival itself stays in the a11y tree. */
.journey-end {
  position: fixed;
  inset: 0;
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 24px;
  pointer-events: none;
  opacity: 0;
  will-change: opacity;
}

/* Explicit width — .outlined itself carries none, on purpose (see the
   fixed wordmark, which learned this the hard way: width: 100% inside a
   position: fixed ancestor with no width of its own fills half the
   viewport). min() keeps it legible without overflowing a narrow screen. */
.journey-end-mark {
  width: min(300px, 70vw);
  height: auto;
}

.journey-end-tagline {
  font-size: clamp(1.1rem, 3.4vw, 1.5rem);
  font-weight: 500;
  color: var(--fg);
  text-align: center;
}

.journey-end-sub {
  font-size: clamp(0.92rem, 2.6vw, 1.05rem);
  color: var(--muted);
  line-height: 1.55;
  max-width: 40ch;
  text-align: center;
}

/* Below 750px the photograph would collide with the labels, which can
   appear anywhere on the right side of the screen. Move it out of their
   way to the bottom of the viewport instead. */
@media (max-width: 749px) {
  .arrival-photo {
    /* Top-right corner, beside the fixed wordmark (which ends around
       200px in) — the bottom is the scroll hint's territory. */
    top: 16px;
    left: auto;
    right: 16px;
    bottom: auto;
    /* 42vw keeps the card's left edge clear of the wordmark and of the
       label band — labels clamp inside the viewport and can reach ~half
       its width. */
    width: min(42vw, 220px);
    transform: scale(0.98);
  }

  .arrival-photo-caption {
    padding: 8px 10px 10px;
    font-size: 11px;
  }

  .arrival-photo.is-arrived {
    transform: scale(1);
  }

  body.is-immersive .wordmark-fixed {
    left: 15px;
  }

  /* Labels sized for desktop run up to 240px wide — on a phone that is
     over half the viewport, which forces layoutX to clamp constantly and
     labels end up pinned to the edges. Smaller type keeps them anchored
     to their nodes instead. */
  .lab .title {
    font-size: 15px;
  }

  .lab .badge {
    width: 17px;
    height: 17px;
    font-size: 9px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .arrival-photo,
  body.is-immersive .wordmark-fixed,
  body.is-immersive .scroll-hint,
  .journey-end {
    display: none;
  }

  body.is-immersive .scroll-hint {
    animation: none;
  }
}
