/* =====================================================================
   StopDown — marketing site
   =====================================================================

   Tokens are lifted verbatim from the app's globals.css so the site and
   the product cannot drift apart. If a colour changes there, change it
   here; there is no build step joining them.

   PERFORMANCE RULES OBSERVED THROUGHOUT — the brief asked for heavy
   motion without lag, and these are the four things that buy it:

     1. Only `transform` and `opacity` are ever animated. Both are
        composited on the GPU, so they never trigger layout or paint.
        Animating `top`, `height`, `margin` or `filter` on scroll is what
        makes sites like this stutter.
     2. Nothing animates that is not on screen. Reveals are driven by
        IntersectionObserver and unobserved once fired.
     3. Parallax reads scroll position ONCE per frame, in one
        requestAnimationFrame loop, and writes a CSS custom property. No
        element has its own scroll listener.
     4. `will-change` is applied narrowly and never to more than a
        handful of elements — it reserves a compositor layer, and a page
        that reserves fifty is slower than one that reserves none.

   ===================================================================== */

:root {
  color-scheme: dark;

  /* --- app tokens, dark ------------------------------------------- */
  --bg: #0b0b0b;
  --card: #121212;
  --surface-2: #191919;
  --border: #1f1f1f;
  --primary: #ff6a00;
  --on-primary: #000000;
  --primary-text: #ff6a00;
  --text: #f5f5f5;
  --text-dim: #a0a0a0;

  /* --- site-only ------------------------------------------------- */
  --max: 1180px;
  --gutter: clamp(1.25rem, 5vw, 3rem);

  /* Motion. One curve, three durations — same discipline as the app. */
  --ease: cubic-bezier(0.2, 0, 0, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  --tap: 120ms;
  --move: 260ms;
  --enter: 720ms;

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

  /*
   * PHOTOGRAPH SLOTS.
   *
   * Drop your own frames into assets/img/ with these names and they
   * appear. Until then each slot falls back to a gradient, so the site
   * looks finished rather than broken while it is empty. See README.md
   * for the recommended dimensions.
   */
  --img-hero: url('../img/hero.jpg');
  --img-exif: url('../img/scene-exif.jpg');
  --img-credit: url('../img/scene-credit.jpg');
  --img-album-1: url('../img/album-1.jpg');
  --img-album-2: url('../img/album-2.jpg');
  --img-album-3: url('../img/album-3.jpg');
  --img-album-4: url('../img/album-4.jpg');
  --img-album-5: url('../img/album-5.jpg');
  --img-album-6: url('../img/album-6.jpg');
}

:root[data-theme='light'] {
  color-scheme: light;

  --bg: #faf9f7;
  --card: #ffffff;
  --surface-2: #f2efec;
  --border: #e2ded8;
  --primary: #ff6a00;
  --on-primary: #000000;
  /*
   * Orange as TEXT is a different problem from orange as a fill.
   * #ff6a00 on white is about 2.9:1 — under AA — and this colour carries
   * every eyebrow and inline link on the page. The deeper orange still
   * reads as the brand and clears 4.5:1. Same reasoning, same values as
   * the app.
   */
  --primary-text: #c24a00;
  --text: #17161a;
  --text-dim: #63605c;
}

/* --------------------------------------------------------------- reset */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: 17px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img {
  display: block;
  max-width: 100%;
  height: auto;
}

a {
  color: inherit;
  text-decoration: none;
}

h1,
h2,
h3,
p,
figure,
blockquote {
  margin: 0;
}

::selection {
  background: var(--primary);
  color: var(--on-primary);
}

:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 3px;
  border-radius: 4px;
}

/* Screen-reader-only, for the skip link and icon labels. */
.sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.skip {
  position: fixed;
  top: 0.5rem;
  left: 0.5rem;
  z-index: 200;
  padding: 0.6rem 1rem;
  border-radius: 999px;
  background: var(--primary);
  color: var(--on-primary);
  font: 600 0.75rem/1 var(--font-mono);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  transform: translateY(-200%);
  transition: transform var(--move) var(--ease);
}

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

/* ------------------------------------------------------ type + layout */

.wrap {
  width: 100%;
  max-width: var(--max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.eyebrow {
  font: 600 clamp(0.62rem, 1.5vw, 0.7rem) / 1 var(--font-mono);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--primary-text);
}

.display {
  font-size: clamp(2.4rem, 7.5vw, 5.5rem);
  line-height: 1.02;
  letter-spacing: -0.03em;
  font-weight: 700;
  text-wrap: balance;
}

.h2 {
  font-size: clamp(1.9rem, 4.6vw, 3.2rem);
  line-height: 1.06;
  letter-spacing: -0.025em;
  font-weight: 700;
  text-wrap: balance;
}

.h3 {
  font-size: clamp(1.15rem, 2.2vw, 1.4rem);
  line-height: 1.25;
  letter-spacing: -0.015em;
  font-weight: 650;
}

.lede {
  font-size: clamp(1.02rem, 1.9vw, 1.3rem);
  line-height: 1.55;
  color: var(--text-dim);
  max-width: 34ch;
  text-wrap: pretty;
}

.lede--wide {
  max-width: 52ch;
}

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

section {
  position: relative;
}

.pad {
  padding-block: clamp(5rem, 14vh, 10rem);
}

.rule {
  height: 1px;
  background: var(--border);
  border: 0;
  margin: 0;
}

/* -------------------------------------------------------------- buttons */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  padding: 0.95rem 1.6rem;
  border-radius: 999px;
  border: 1px solid transparent;
  font: 700 0.72rem/1 var(--font-mono);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  cursor: pointer;
  white-space: nowrap;
  transition:
    transform var(--tap) var(--ease),
    opacity var(--tap) var(--ease),
    border-color var(--tap) var(--ease),
    color var(--tap) var(--ease),
    background-color var(--tap) var(--ease);
}

.btn:active {
  transform: scale(0.97);
}

.btn--primary {
  background: var(--primary);
  color: var(--on-primary);
}

.btn--primary:hover {
  opacity: 0.9;
}

.btn--ghost {
  border-color: var(--border);
  color: var(--text);
  background: transparent;
}

.btn--ghost:hover {
  border-color: var(--primary);
  color: var(--primary-text);
}

.btn svg {
  width: 1.05em;
  height: 1.05em;
  flex: none;
}

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

/* ----------------------------------------------------------------- nav */

.nav {
  position: fixed;
  inset-inline: 0;
  top: 0;
  z-index: 100;
  transition: background-color var(--move) var(--ease), border-color var(--move) var(--ease);
  border-bottom: 1px solid transparent;
}

/*
 * The bar only gains a background once you have scrolled past the hero.
 * A solid bar over a full-bleed photograph from the first pixel wastes
 * the one moment the photograph has the whole screen.
 */
.nav[data-stuck='true'] {
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  border-bottom-color: var(--border);
}

.nav__inner {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  height: 66px;
  max-width: var(--max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  font-size: 1.05rem;
  margin-right: auto;
}

.brand__mark {
  width: 26px;
  height: 26px;
  flex: none;
  color: var(--primary);
}

.nav__links {
  display: none;
  gap: 1.75rem;
}

.nav__links a {
  font: 600 0.72rem/1 var(--font-mono);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-dim);
  transition: color var(--tap) var(--ease);
}

.nav__links a:hover {
  color: var(--text);
}

.nav__actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.icon-btn {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  transition: color var(--tap) var(--ease), border-color var(--tap) var(--ease);
}

.icon-btn:hover {
  color: var(--primary-text);
  border-color: var(--primary);
}

.icon-btn svg {
  width: 17px;
  height: 17px;
}

@media (min-width: 900px) {
  .nav__links {
    display: flex;
  }
}

/* ---------------------------------------------------------------- hero */

.hero {
  position: relative;
  min-height: 100svh;
  display: grid;
  align-items: end;
  overflow: clip;
  isolation: isolate;
}

/*
 * KEN BURNS.
 *
 * A pure CSS keyframe on transform only — 28 seconds, alternating, so it
 * never snaps back to the start. The layer is scaled up 12% at rest so
 * the drift can never expose an edge.
 *
 * The gradient sits UNDER the photograph in the same declaration, so the
 * slot degrades gracefully: no hero.jpg means a deep charcoal wash with a
 * warm cast rather than an empty black box.
 */
.hero__media {
  position: absolute;
  inset: -6%;
  z-index: -2;
  background-image: var(--img-hero),
    radial-gradient(120% 90% at 22% 12%, #3a2415 0%, transparent 55%),
    radial-gradient(100% 80% at 82% 88%, #1a2430 0%, transparent 60%),
    linear-gradient(160deg, #16130f 0%, #0b0b0b 70%);
  background-size: cover;
  background-position: center;
  will-change: transform;
  animation: ken-burns 28s var(--ease) infinite alternate;
}

@keyframes ken-burns {
  from {
    transform: scale(1.06) translate3d(-1.2%, 0.6%, 0);
  }
  to {
    transform: scale(1.16) translate3d(1.4%, -1.4%, 0);
  }
}

/* Legibility scrim. Two stops, so the top of the frame stays open. */
.hero__scrim {
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    linear-gradient(to top, var(--bg) 2%, color-mix(in srgb, var(--bg) 55%, transparent) 34%, transparent 72%),
    linear-gradient(to bottom, color-mix(in srgb, var(--bg) 70%, transparent) 0%, transparent 30%);
}

.hero__body {
  padding-block: clamp(6rem, 16vh, 9rem) clamp(3.5rem, 9vh, 6rem);
  max-width: var(--max);
  margin-inline: auto;
  padding-inline: var(--gutter);
  width: 100%;
}

.hero .display {
  margin-top: 1.1rem;
  max-width: 20ch;
}

.hero .lede {
  margin-top: 1.4rem;
  max-width: 44ch;
}

.hero .btn-row {
  margin-top: 2.2rem;
}

.hero__meta {
  margin-top: 2.6rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem 1.4rem;
}

/* The camera-data line, used as a signature flourish. */
.exif-line {
  font: 400 0.68rem/1.7 var(--font-mono);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.scroll-cue {
  position: absolute;
  left: 50%;
  bottom: 1.4rem;
  translate: -50% 0;
  width: 1px;
  height: 46px;
  background: linear-gradient(to bottom, transparent, var(--primary));
  transform-origin: top;
  animation: cue 2.4s var(--ease) infinite;
}

@keyframes cue {
  0%,
  100% {
    transform: scaleY(0.3);
    opacity: 0.25;
  }
  50% {
    transform: scaleY(1);
    opacity: 1;
  }
}

/* ------------------------------------------------------------- reveals */

/*
 * The one reveal primitive.
 *
 * Elements start shifted and transparent; JS adds .in when they cross
 * into view and then stops watching them. `--d` staggers siblings
 * without a class per position.
 */
.rise {
  opacity: 0;
  transform: translate3d(0, 26px, 0);
  transition:
    opacity var(--enter) var(--ease),
    transform var(--enter) var(--ease);
  transition-delay: var(--d, 0ms);
}

.rise.in {
  opacity: 1;
  transform: none;
}

.rise--left {
  transform: translate3d(-26px, 0, 0);
}

.rise--scale {
  transform: scale(0.94);
}

/* A word-by-word headline reveal. Spans are wrapped by JS. */
.word {
  display: inline-block;
  opacity: 0;
  transform: translate3d(0, 0.5em, 0);
  transition:
    opacity 620ms var(--ease),
    transform 620ms var(--ease);
}

.in > .word,
.in .word {
  opacity: 1;
  transform: none;
}

/* =================================================== interest + ideas */

.interest-band {
  padding-block: clamp(3.5rem, 9vh, 6rem);
  text-align: center;
  background:
    radial-gradient(70% 120% at 50% 0%, color-mix(in srgb, var(--primary) 9%, transparent), transparent 65%),
    var(--bg);
  border-block: 1px solid var(--border);
}

.interest__tally {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.1rem;
  margin-bottom: 1.6rem;
}

.interest__num {
  font: 700 clamp(2.6rem, 8vw, 4.6rem) / 1 var(--font-mono);
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  color: var(--primary-text);
}

/*
 * Until the real figure arrives the placeholder is dimmed, so a dash is
 * legible as "loading" rather than as a number. [data-loaded] is set by
 * the fetch callback.
 */
.interest:not([data-loaded]) .interest__num {
  color: var(--text-dim);
  opacity: 0.5;
}

.interest__label {
  font: 600 0.68rem/1 var(--font-mono);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.interest-cta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.85rem;
  justify-content: center;
  align-items: stretch;
}

.interest-cta--tight {
  margin-top: 2rem;
}

/*
 * The big one.
 *
 * It is the largest control on the page because it is the only thing
 * being asked of a visitor who is not ready to sign up for anything —
 * one tap, no form, no account. Everything about it should say that it
 * costs nothing.
 */
.btn-big {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  padding: 1.25rem 2.4rem;
  border: 0;
  border-radius: 999px;
  background: var(--primary);
  color: var(--on-primary);
  font: 700 clamp(0.8rem, 1.6vw, 0.95rem) / 1 var(--font-mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  cursor: pointer;
  box-shadow: 0 14px 40px -12px color-mix(in srgb, var(--primary) 60%, transparent);
  transition:
    transform var(--tap) var(--ease),
    box-shadow var(--move) var(--ease),
    opacity var(--tap) var(--ease),
    background-color var(--move) var(--ease),
    color var(--move) var(--ease);
}

.btn-big svg {
  width: 1.2em;
  height: 1.2em;
  flex: none;
}

.btn-big:hover {
  transform: translateY(-2px);
  box-shadow: 0 20px 50px -12px color-mix(in srgb, var(--primary) 70%, transparent);
}

.btn-big:active {
  transform: scale(0.975);
}

.btn-big--sm {
  padding: 1rem 1.9rem;
  font-size: 0.8rem;
}

.btn-big[data-busy='true'] {
  opacity: 0.65;
  cursor: progress;
}

/*
 * Already counted.
 *
 * Deliberately not just a disabled grey button — it becomes a quiet
 * confirmation with a filled heart, so the state reads as "done" rather
 * than "unavailable".
 */
.btn-big[data-voted='true'] {
  background: transparent;
  color: var(--primary-text);
  border: 1px solid color-mix(in srgb, var(--primary) 55%, transparent);
  box-shadow: none;
  cursor: default;
  transform: none;
}

.btn-big[data-voted='true'] svg {
  fill: currentColor;
}

/* Matches the big button's height so the pair sit level. */
.btn--tall {
  padding-block: 1.25rem;
}

.interest-cta--tight .btn--tall {
  padding-block: 1rem;
}

.interest__note {
  margin-top: 1.25rem;
  color: var(--text-dim);
}

.interest--inline {
  margin-top: 1.4rem;
}

.interest__num--sm {
  font-size: 1rem;
  letter-spacing: 0;
  color: var(--primary-text);
}

/* --------------------------------------------------------- the dialog */

.sheet {
  padding: 0;
  border: 0;
  background: transparent;
  max-width: min(560px, calc(100vw - 2rem));
  width: 100%;
}

.sheet::backdrop {
  background: rgb(0 0 0 / 0.72);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

.sheet__inner {
  border: 1px solid var(--border);
  border-radius: 20px;
  background: var(--card);
  padding: clamp(1.4rem, 4vw, 2rem);
  color: var(--text);
  max-height: 88svh;
  overflow-y: auto;
}

.sheet[open] .sheet__inner {
  animation: sheet-in var(--move) var(--ease) both;
}

@keyframes sheet-in {
  from {
    opacity: 0;
    transform: translate3d(0, 14px, 0) scale(0.985);
  }
}

.sheet__head {
  display: flex;
  align-items: start;
  justify-content: space-between;
  gap: 1rem;
}

.sheet__blurb {
  margin-top: 0.9rem;
  color: var(--text-dim);
  font-size: 0.95rem;
}

.field {
  display: block;
  margin-top: 1.25rem;
}

.field__label {
  display: block;
  margin-bottom: 0.45rem;
  font: 600 0.64rem/1.4 var(--font-mono);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.field textarea,
.field input {
  width: 100%;
  border-radius: 0.7rem;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  padding: 0.8rem 0.9rem;
  /* 16px exactly. Anything smaller and iOS Safari zooms the whole page on
     focus and never zooms back out — the app documents the same rule. */
  font-size: 16px;
  font-family: inherit;
  line-height: 1.5;
  outline: none;
  transition: border-color var(--move) var(--ease);
  -webkit-appearance: none;
  appearance: none;
}

.field textarea {
  resize: vertical;
  min-height: 120px;
}

.field textarea:focus,
.field input:focus {
  border-color: var(--primary);
}

.field__hint {
  display: block;
  margin-top: 0.4rem;
  text-align: right;
  font: 400 0.64rem/1 var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
}

.sheet__status {
  min-height: 1.4rem;
  margin-top: 1rem;
  font-size: 0.9rem;
  color: var(--primary-text);
}

.sheet__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  justify-content: flex-end;
  margin-top: 0.6rem;
}

/* A link that has no destination yet renders as this instead. */
.is-soon {
  opacity: 0.55;
  cursor: default;
  pointer-events: none;
}

/* --------------------------------------------------------- manifesto */

.manifesto {
  text-align: center;
}

.manifesto .h2 {
  max-width: 24ch;
  margin-inline: auto;
}

.manifesto .lede {
  margin: 1.6rem auto 0;
  max-width: 46ch;
}

/* The struck-through word — video, which this app does not do. */
.struck {
  position: relative;
  color: var(--text-dim);
  white-space: nowrap;
}

.struck::after {
  content: '';
  position: absolute;
  left: -0.04em;
  right: -0.04em;
  top: 55%;
  height: 0.07em;
  background: var(--primary);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 720ms var(--ease) 320ms;
}

.in .struck::after {
  transform: scaleX(1);
}

/* ------------------------------------------------------- sticky scenes */

/*
 * THE IPHONE-AD EFFECT.
 *
 * A tall outer section holds a sticky viewport-height stage. As the outer
 * section scrolls past, JS writes a 0→1 progress value onto it as `--p`,
 * and the layers inside read that value. Because the stage is `sticky`
 * rather than `fixed`, it never fights the page's own scrolling and it
 * needs no scroll-jacking — the wheel behaves exactly as the reader
 * expects, which is the difference between this and the versions of this
 * effect that feel broken.
 */
/*
 * 200svh: one viewport of pinned dwell, plus one of travel.
 *
 * This was 260, which gave the reveal far more scroll than it needs and
 * cost about 1500px of page for nothing. Below roughly 180 the layers
 * have not finished arriving by the time the stage unpins, which reads as
 * the scene being cut off.
 */
.scene {
  height: 200svh;
}

.scene__stage {
  position: sticky;
  top: 0;
  height: 100svh;
  display: grid;
  place-items: center;
  overflow: clip;
  isolation: isolate;
}

.scene__grid {
  display: grid;
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
  width: 100%;
  max-width: var(--max);
  padding-inline: var(--gutter);
}

@media (min-width: 900px) {
  .scene__grid {
    grid-template-columns: 1fr 1fr;
  }

  .scene__grid--flip .scene__copy {
    order: 2;
  }
}

/* A photograph that drifts as you scroll past it. */
.scene__photo {
  position: relative;
  aspect-ratio: 4 / 5;
  border-radius: 18px;
  overflow: hidden;
  background-image: var(--scene-img),
    linear-gradient(150deg, #2b2118 0%, #12161b 60%, #0d0d0d 100%);
  background-size: cover;
  background-position: center;
  border: 1px solid var(--border);
  /* clamp keeps the drift from ever pulling the image off its own frame */
  transform: translate3d(0, calc((var(--p, 0) - 0.5) * -60px), 0) scale(1.04);
  will-change: transform;
}

.scene__photo::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgb(0 0 0 / 0.55), transparent 55%);
}

/*
 * The EXIF strip, revealed under the photograph as the scene plays.
 *
 * This mirrors what the app itself does — the camera data sits directly
 * under the frame, not at the bottom of the card — so the site is
 * demonstrating the product rather than describing it.
 */
.exif-card {
  position: relative;
  z-index: 1;
  margin-top: -1px;
  padding: 1rem 1.15rem;
  border: 1px solid var(--border);
  border-top: 0;
  border-radius: 0 0 18px 18px;
  background: var(--card);
  opacity: clamp(0, calc((var(--p, 0) - 0.22) * 4), 1);
  transform: translate3d(0, calc((1 - clamp(0, (var(--p, 0) - 0.22) * 4, 1)) * 14px), 0);
}

.exif-card__row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1rem;
}

.exif-card__row span {
  font: 400 0.66rem/1.7 var(--font-mono);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.exif-card__row b {
  color: var(--text);
  font-weight: 500;
}

/* The credit chip that travels with a photograph. */
.credit-chip {
  position: absolute;
  left: 0.9rem;
  top: 0.9rem;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.45rem 0.8rem;
  border-radius: 999px;
  background: rgb(0 0 0 / 0.55);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  /* White on a dark scrim over a photograph is correct in both themes —
     the app makes the same call and documents it. Not themed. */
  color: #fff;
  font: 500 0.64rem/1 var(--font-mono);
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

.credit-chip svg {
  width: 13px;
  height: 13px;
  opacity: 0.8;
}

/* Restop banner that slides in on the credit scene. */
.restop-flag {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.75rem;
  opacity: clamp(0, calc((var(--p, 0) - 0.35) * 5), 1);
  transform: translate3d(calc((1 - clamp(0, (var(--p, 0) - 0.35) * 5, 1)) * -18px), 0, 0);
}

.restop-flag svg {
  width: 15px;
  height: 15px;
  color: var(--primary);
}

/* --------------------------------------------------------- feature grid */

.grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
}

.card {
  padding: 1.6rem;
  border: 1px solid var(--border);
  border-radius: 16px;
  background: var(--card);
  transition: border-color var(--move) var(--ease), transform var(--move) var(--ease);
}

.card:hover {
  border-color: color-mix(in srgb, var(--primary) 45%, var(--border));
  transform: translateY(-3px);
}

.card__icon {
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border-radius: 10px;
  background: color-mix(in srgb, var(--primary) 14%, transparent);
  color: var(--primary-text);
  margin-bottom: 1rem;
}

.card__icon svg {
  width: 18px;
  height: 18px;
}

.card p {
  margin-top: 0.55rem;
  color: var(--text-dim);
  font-size: 0.95rem;
}

/* --------------------------------------------------------- album strip */

/*
 * Six frames, each drifting at a slightly different rate.
 *
 * The varying `--r` is what makes this read as depth rather than as one
 * sheet sliding — it is the whole trick, and it costs one multiply per
 * frame per rAF tick.
 */
.albums {
  display: grid;
  gap: 0.75rem;
  grid-template-columns: repeat(2, 1fr);
}

@media (min-width: 760px) {
  .albums {
    grid-template-columns: repeat(3, 1fr);
  }
}

.albums__frame {
  position: relative;
  aspect-ratio: 1;
  border-radius: 12px;
  overflow: hidden;
  border: 1px solid var(--border);
  background-image: var(--frame-img), linear-gradient(140deg, #241c15, #101418);
  background-size: cover;
  background-position: center;
  transform: translate3d(0, calc((var(--p, 0) - 0.5) * var(--r, -40px)), 0);
  will-change: transform;
}

.albums__frame::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgb(0 0 0 / 0.5), transparent 60%);
  opacity: 0;
  transition: opacity var(--move) var(--ease);
}

.albums__frame:hover::after {
  opacity: 1;
}

/* -------------------------------------------------------------- phone */

/*
 * The app, drawn in HTML and CSS rather than screenshotted.
 *
 * A screenshot goes stale the day the UI changes and costs a 400KB
 * request; this is a few hundred bytes of markup, stays sharp at any
 * density, and animates.
 */
.phone {
  width: min(300px, 78vw);
  margin-inline: auto;
  border-radius: 42px;
  border: 1px solid var(--border);
  background: var(--bg);
  padding: 10px;
  box-shadow: 0 40px 90px -30px rgb(0 0 0 / 0.75), 0 0 0 1px rgb(255 255 255 / 0.03) inset;
}

.phone__screen {
  border-radius: 34px;
  overflow: hidden;
  background: var(--bg);
  border: 1px solid var(--border);
}

.phone__bar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.7rem 0.85rem;
  border-bottom: 1px solid var(--border);
}

.phone__bar span {
  font: 600 0.58rem/1 var(--font-mono);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.phone__notch {
  width: 44px;
  height: 4px;
  border-radius: 999px;
  background: var(--border);
  margin: 0 auto 0.55rem;
}

.phone__photo {
  aspect-ratio: 4 / 3;
  background-image: var(--img-exif), linear-gradient(150deg, #2c2318, #101317);
  background-size: cover;
  background-position: center;
}

.phone__exif {
  padding: 0.6rem 0.8rem;
  border-bottom: 1px solid var(--border);
  font: 400 0.55rem/1.7 var(--font-mono);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.phone__actions {
  display: flex;
  gap: 1.1rem;
  padding: 0.65rem 0.8rem 0.9rem;
  color: var(--text-dim);
}

.phone__actions svg {
  width: 17px;
  height: 17px;
}

.phone__actions em {
  font: 500 0.6rem/1 var(--font-mono);
  font-style: normal;
  align-self: center;
}

.phone__act {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}

.phone__act--live {
  color: var(--primary-text);
}

/* -------------------------------------------------------------- genres */

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 1.6rem;
}

.chip {
  padding: 0.45rem 0.85rem;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  font-size: 0.82rem;
  color: var(--text-dim);
  transition: border-color var(--tap) var(--ease), color var(--tap) var(--ease);
}

.chip:hover {
  border-color: var(--primary);
  color: var(--primary-text);
}

/* -------------------------------------------------------------- ethics */

.ethics {
  background: var(--card);
  border-block: 1px solid var(--border);
}

.ledger {
  display: grid;
  gap: 0;
  margin-top: 2.4rem;
  border: 1px solid var(--border);
  border-radius: 16px;
  overflow: hidden;
  background: var(--bg);
}

.ledger__row {
  display: grid;
  grid-template-columns: 26px 1fr;
  gap: 0.9rem;
  align-items: start;
  padding: 1.1rem 1.25rem;
  border-bottom: 1px solid var(--border);
}

.ledger__row:last-child {
  border-bottom: 0;
}

.ledger__row svg {
  width: 18px;
  height: 18px;
  margin-top: 0.15rem;
  color: var(--primary-text);
}

.ledger__row b {
  font-weight: 600;
}

.ledger__row p {
  color: var(--text-dim);
  font-size: 0.93rem;
  margin-top: 0.2rem;
}

/* ------------------------------------------------------------- roadmap */

.roadmap {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr));
  margin-top: 2.6rem;
}

.roadmap__item {
  padding: 1.5rem;
  border: 1px dashed var(--border);
  border-radius: 16px;
}

.roadmap__tag {
  display: inline-block;
  padding: 0.28rem 0.7rem;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--primary) 50%, transparent);
  color: var(--primary-text);
  font: 600 0.58rem/1 var(--font-mono);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  margin-bottom: 0.9rem;
}

.roadmap__item p {
  color: var(--text-dim);
  font-size: 0.93rem;
  margin-top: 0.5rem;
}

/* ----------------------------------------------------------- community */

.community {
  display: grid;
  gap: 1rem;
}

@media (min-width: 860px) {
  .community {
    grid-template-columns: 1.35fr 1fr;
    align-items: stretch;
  }

  /* No social links filled in yet: the Discord card takes the full row
     rather than leaving half of one empty. Set by wireLinks(). */
  .community[data-collapsed='true'] {
    grid-template-columns: 1fr;
  }
}

/* The Discord card carries Discord's own blurple as an accent so it is
   recognisable at a glance, on top of our own surface. */
.discord {
  position: relative;
  overflow: hidden;
  padding: clamp(1.6rem, 4vw, 2.4rem);
  border-radius: 20px;
  border: 1px solid color-mix(in srgb, #5865f2 40%, var(--border));
  background:
    radial-gradient(120% 130% at 88% 8%, color-mix(in srgb, #5865f2 26%, transparent), transparent 62%),
    var(--card);
}

.discord__mark {
  width: 46px;
  height: 46px;
  color: #5865f2;
  margin-bottom: 1.1rem;
}

.discord .h3 {
  max-width: 26ch;
}

.discord p {
  margin-top: 0.7rem;
  color: var(--text-dim);
  max-width: 42ch;
}

.discord .btn {
  margin-top: 1.5rem;
  background: #5865f2;
  color: #fff;
}

.discord .btn:hover {
  opacity: 0.9;
}

.socials {
  display: grid;
  gap: 0.7rem;
  grid-template-columns: repeat(2, 1fr);
  align-content: start;
}

.social {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 1rem;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--card);
  font: 600 0.68rem/1 var(--font-mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
  transition: color var(--tap) var(--ease), border-color var(--tap) var(--ease), transform var(--tap) var(--ease);
}

.social:hover {
  color: var(--text);
  border-color: var(--brand, var(--primary));
  transform: translateY(-2px);
}

.social svg {
  width: 19px;
  height: 19px;
  flex: none;
  color: var(--brand, var(--primary-text));
}

/* ---------------------------------------------------------------- CTA */

.cta {
  text-align: center;
  overflow: clip;
  isolation: isolate;
}

.cta__glow {
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(60% 60% at 50% 108%, color-mix(in srgb, var(--primary) 22%, transparent), transparent 70%);
}

.cta .btn-row {
  justify-content: center;
  margin-top: 2.2rem;
}

/* -------------------------------------------------------------- footer */

.footer {
  border-top: 1px solid var(--border);
  padding-block: 3.5rem 2.5rem;
}

.footer__grid {
  display: grid;
  gap: 2.4rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr));
}

.footer h4 {
  margin: 0 0 0.9rem;
  font: 600 0.62rem/1 var(--font-mono);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.footer ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.55rem;
}

.footer a {
  color: var(--text);
  font-size: 0.93rem;
  transition: color var(--tap) var(--ease);
}

.footer a:hover {
  color: var(--primary-text);
}

.footer__base {
  margin-top: 3rem;
  padding-top: 1.6rem;
  border-top: 1px solid var(--border);
  display: flex;
  flex-wrap: wrap;
  gap: 0.8rem 1.5rem;
  align-items: center;
  justify-content: space-between;
}

.footer__base p {
  color: var(--text-dim);
  font-size: 0.85rem;
}

/* ================================================================
   REDUCED MOTION

   Everything above is decoration. Somebody who has asked their
   operating system for less movement gets the finished state of every
   animation immediately — nothing drifts, nothing pans, nothing waits
   to be scrolled into existence. The site must be complete without
   any of it, and this block is the proof that it is.
   ================================================================ */

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }

  .rise,
  .word {
    opacity: 1 !important;
    transform: none !important;
  }

  .struck::after {
    transform: scaleX(1) !important;
  }

  .hero__media {
    animation: none !important;
    transform: scale(1.06) !important;
  }

  .scene {
    height: auto !important;
  }

  .scene__stage {
    position: static !important;
    height: auto !important;
    padding-block: clamp(4rem, 10vh, 7rem);
  }

  .scene__photo,
  .albums__frame {
    transform: none !important;
    will-change: auto !important;
  }

  .exif-card,
  .restop-flag {
    opacity: 1 !important;
    transform: none !important;
  }

  .scroll-cue {
    display: none;
  }
}

/*
 * Narrow screens get the scenes as ordinary stacked sections too.
 *
 * A 260svh sticky stage on a phone means three screens of scrolling for
 * one idea, and the parallax it buys is invisible at that width.
 */
@media (max-width: 899px) {
  .scene {
    height: auto;
  }

  .scene__stage {
    position: static;
    height: auto;
    padding-block: clamp(4rem, 12vh, 6rem);
  }

  .scene__photo {
    transform: none;
    will-change: auto;
    aspect-ratio: 3 / 4;
  }

  .exif-card,
  .restop-flag {
    opacity: 1;
    transform: none;
  }
}
