/*
  The landing shares the client's palette, not its stylesheet.

  Both pages are the same product and must look it, so every token below is the
  same value as web/src/styles.css. What they do not share is a build: this page
  is one document with no application state, and pulling a framework in to lay
  out four sections would cost more than it saves. Two copies of a palette is a
  real risk, and the comment at the top of each is the mitigation — the tokens
  move together or not at all.
*/

:root {
  --ink: light-dark(#ffffff, #0f1115);
  --surface: light-dark(#f4f6fa, #171a21);
  --raised: light-dark(#e9edf4, #1e222b);
  --line: light-dark(#dde3ec, #272c37);
  --muted: light-dark(#5c6779, #8b93a5);
  --text: light-dark(#12161d, #e8eaf0);

  /* One green cannot carry both on paper and on near-black; see the client. */
  --accent: light-dark(#0b7a66, #3cd3ad);
  --on-accent: light-dark(#ffffff, #0f1115);

  /* The mark is the mark. A logo does not change colour with the room. */
  --brand-from: #3cd3ad;
  --brand-to: #55b9c4;

  --font-sans:
    ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;

  --radius: 14px;
  --radius-lg: 22px;

  /* The page never touches the edge of a phone, notch or no notch. */
  --gutter: max(1.25rem, env(safe-area-inset-left));
  --measure: 68rem;
}

/*
  Dark is the product default on the first paint, before the synchronous theme
  script runs. A remembered choice narrows this to light or dark.
*/
html {
  color-scheme: dark;
}
html[data-theme='light'] {
  color-scheme: light;
}
html[data-theme='dark'] {
  color-scheme: dark;
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Anchors land below the sticky header rather than under it. */
  scroll-padding-top: 5.5rem;
}

body {
  margin: 0;
  background: var(--ink);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 1rem;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  /* A landing that scrolls sideways on a phone is a broken landing. */
  overflow-x: hidden;
}

* {
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
}

h1,
h2,
h3 {
  margin: 0;
  line-height: 1.12;
  letter-spacing: -0.022em;
  font-weight: 600;
  text-wrap: balance;
}

p {
  margin: 0;
  text-wrap: pretty;
}

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

ul,
ol {
  margin: 0;
  padding: 0;
  list-style: none;
}

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

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 0.75rem 1rem;
  background: var(--accent);
  color: var(--on-accent);
  border-radius: 0 0 var(--radius) 0;
}
.skip:focus {
  left: 0;
}

/* ---------------------------------------------------------------- primitives */

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  /*
    Height is set rather than floored. A min-height of 44px with padding and a
    line-height on top of it produces 48.4px, so the icon button beside it —
    which really is 44px — sat visibly short of it. One number, both controls.
  */
  height: 44px;
  padding: 0 1.15rem;
  border: 1px solid transparent;
  border-radius: 12px;
  font: inherit;
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
  transition:
    filter 140ms ease,
    background-color 140ms ease,
    border-color 140ms ease;
}

.button-accent {
  background: var(--accent);
  color: var(--on-accent);
}
.button-accent:hover {
  filter: brightness(1.08);
}

.button-ghost {
  background: transparent;
  border-color: var(--line);
  color: var(--text);
}
.button-ghost:hover {
  background: var(--surface);
  border-color: var(--muted);
}

.button-lg {
  height: 52px;
  padding: 0 1.6rem;
  font-size: 1.0625rem;
}

.icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  transition:
    color 140ms ease,
    border-color 140ms ease;
}
.icon-button:hover {
  color: var(--text);
  border-color: var(--muted);
}
.icon-button svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/*
  A control that cannot work should not be on screen: the toggle appears only
  once the script that drives it has run.
*/
.icon-button {
  display: none;
}
html.has-js .icon-button {
  display: inline-flex;
}

/*
  The toggle shows the theme you would switch to, not the one you are in: the
  control is a promise about what happens next. The script says that destination
  outright so the stylesheet does not need to infer state from storage.
*/
.icon-sun,
.icon-moon {
  display: none;
}
.icon-button[data-icon='sun'] .icon-sun,
.icon-button[data-icon='moon'] .icon-moon {
  display: block;
}

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: clamp(1.25rem, 3vw, 1.75rem);
}

.eyebrow {
  margin-bottom: 0.75rem;
  /* On a narrow phone this wraps, and it should break at the separator. */
  text-wrap: balance;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--accent);
}

.gradient-text {
  background: linear-gradient(100deg, var(--brand-from), var(--brand-to));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
}
.wordmark-name {
  font-size: 1.35rem;
  font-weight: 600;
  letter-spacing: -0.02em;
}
.tile {
  display: block;
  flex: none;
}

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

.nav {
  position: sticky;
  top: 0;
  z-index: 40;
  /*
    Opaque, and no backdrop-filter.

    A translucent blurred header has to re-blur everything behind it on every
    scrolled frame, across the full width of the window. It is the single most
    expensive thing a sticky header can do, and it buys an effect nobody came
    here to look at.
  */
  background: var(--ink);
  border-bottom: 1px solid transparent;
  transition: border-color 200ms ease;
}
.nav[data-scrolled='true'] {
  border-bottom-color: var(--line);
}

.nav-inner {
  width: min(100%, var(--measure));
  margin-inline: auto;
  padding: 0.7rem var(--gutter);
  display: flex;
  align-items: center;
  gap: 1rem;
}

.nav-links {
  display: none;
  margin-inline: auto;
  gap: 1.75rem;
  font-size: 0.9375rem;
  color: var(--muted);
}
.nav-links a:hover {
  color: var(--text);
}

.nav-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-left: auto;
}

@media (min-width: 900px) {
  .nav-links {
    display: flex;
  }
  .nav-actions {
    margin-left: 0;
  }
}

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

.hero {
  position: relative;
  padding: 1.25rem var(--gutter) clamp(2.5rem, 6.5vw, 4.5rem);
  overflow: hidden;
}
@media (min-width: 640px) {
  .hero {
    padding-top: clamp(2.25rem, 6vw, 4.5rem);
  }
}

/*
  One soft brand glow behind the fold. It is the only decoration on the page
  that is not information, and it earns its place by keeping a very plain
  layout from reading as a document.
*/
.hero-glow {
  position: absolute;
  inset: -30% -20% auto -20%;
  height: 70vh;
  max-height: 620px;
  /*
    No filter here, on purpose.

    This element is 1792x620 on a desktop, and blurring it meant painting more
    than a megapixel through a 28px kernel. A radial gradient is already soft,
    and it already fades to transparent well inside its own box, so the blur
    was paying a large price to soften something that had no edges to soften.
    The stops below do the same job for nothing.
  */
  background:
    radial-gradient(45% 55% at 70% 35%, color-mix(in srgb, var(--brand-from) 26%, transparent), transparent 72%),
    radial-gradient(40% 50% at 25% 20%, color-mix(in srgb, var(--brand-to) 20%, transparent), transparent 74%);
  pointer-events: none;
  opacity: 0.75;
}
html[data-theme='light'] .hero-glow {
  opacity: 0.5;
}

.hero-inner {
  position: relative;
  width: min(100%, var(--measure));
  margin-inline: auto;
  display: grid;
  gap: clamp(2.5rem, 6vw, 3.5rem);
  align-items: center;
}

.hero h1 {
  font-size: clamp(2.35rem, 7.2vw, 4rem);
  letter-spacing: -0.03em;
}

.lede {
  margin-top: 1.25rem;
  max-width: 34rem;
  font-size: clamp(1.0625rem, 2.2vw, 1.1875rem);
  color: var(--muted);
}

.hero-actions {
  margin-top: 2rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

/*
  Not twice on one screen.

  The header is sticky and carries the same button, so on a phone this one was
  a second copy of a control that never leaves — 116px of the first screen
  spent saying the same thing again. It comes back where the header CTA has to
  share the row with the navigation.
*/
.hero-actions .button-accent {
  display: none;
}
@media (min-width: 640px) {
  .hero-actions .button-accent {
    display: inline-flex;
  }
}

.hero-note {
  margin-top: 1.25rem;
  font-size: 0.875rem;
  color: var(--muted);
}

/*
  On a phone the picture leads.

  Measured on the old order, the wave began 150px before the fold and the card
  in it was invisible: a demonstration nobody scrolled to is a decoration. The
  copy still comes first in the markup, because the heading is what a reader
  and a search engine should meet first; only the painting order changes.
*/
.hero-visual {
  order: -1;
  /* Edge to edge: the hero's own gutter is undone so the outermost faces can
     reach the sides of the screen. The hero clips, so nothing scrolls. */
  margin-inline: calc(var(--gutter) * -1);
}

/*
  Only on a phone. From here up the hero is still one column, but a wide one,
  and a 26rem ring centred in 768px of it is an island rather than a picture —
  so the original order comes back and the wave waits its turn again.
*/
@media (min-width: 640px) {
  .hero-visual {
    order: 0;
    margin-inline: 0;
  }
}

@media (min-width: 960px) {
  .hero-inner {
    grid-template-columns: 1.05fr 1fr;
  }
}

/* ------------------------------------------------------------- the wave art */

.hero-visual {
  display: flex;
  justify-content: center;
}

.wave {
  position: relative;
  margin: 0;
  width: min(100%, 26rem);
  aspect-ratio: 1;
}

#wave-svg {
  width: 100%;
  height: 100%;
  overflow: visible;
}

.wave-node {
  opacity: 0;
  transform: scale(0.4);
  transform-box: fill-box;
  transform-origin: center;
  transition:
    opacity 320ms ease,
    transform 320ms cubic-bezier(0.34, 1.4, 0.64, 1);
}
.wave-node[data-state='pending'] {
  opacity: 0.42;
  transform: scale(1);
}
.wave-node[data-state='accepted'] {
  opacity: 1;
  transform: scale(1);
}
.wave-node[data-state='left'] {
  opacity: 0;
  transform: scale(0.75);
}

/* The accent ring is what "stayed" looks like; a pending disc has none. */
.wave-ring {
  fill: none;
  stroke: var(--brand-from);
  stroke-width: 2.5;
  opacity: 0;
  transition: opacity 320ms ease;
}
.wave-node[data-state='accepted'] .wave-ring {
  opacity: 1;
}

/*
  Seventeen of these change state during one cycle, and opacity is the only
  property among them the compositor can animate on its own. Transitioning the
  stroke colour as well meant repainting every line on every frame of the fade,
  for a colour change nobody can follow at that speed anyway.
*/
.wave-link {
  stroke: var(--line);
  stroke-width: 1.5;
  fill: none;
  opacity: 0;
  transition: opacity 320ms ease;
}
.wave-link[data-state='pending'] {
  opacity: 0.7;
  stroke-dasharray: 3 4;
}
.wave-link[data-state='accepted'] {
  opacity: 0.9;
  stroke: color-mix(in srgb, var(--brand-from) 55%, transparent);
  stroke-dasharray: none;
}

/*
  The throw itself. It expands out of the author and is gone before the first
  recipients land, so the eye is already moving outward when they arrive.
*/
.wave-ripple {
  fill: none;
  stroke: var(--brand-from);
  stroke-width: 2;
  /* The ring widens; the line it is drawn with does not. */
  vector-effect: non-scaling-stroke;
  transform-box: fill-box;
  transform-origin: center;
  animation: ripple 1300ms ease-out forwards;
}

@keyframes ripple {
  from {
    transform: scale(1);
    opacity: 0.55;
  }
  to {
    transform: scale(4.4);
    opacity: 0;
  }
}

/* The cut-out that keeps two overlapping discs legible where they meet. */
.pair-ring {
  fill: none;
  stroke: var(--surface);
  stroke-width: 3;
}

.wave-meter {
  position: absolute;
  left: 50%;
  bottom: -0.25rem;
  transform: translateX(-50%);
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.4rem 0.85rem;
  border-radius: 999px;
  border: 1px solid var(--line);
  /* Solid: this one sits over the wave, so a blur behind it would be redone
     every time a node changes state. */
  background: var(--ink);
  font-size: 0.8125rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
#wave-count {
  font-weight: 600;
  color: var(--text);
}
.wave-meter-label {
  letter-spacing: 0.02em;
}

/*
  The meter counts something. Before the throw there is nothing to count, so
  it is not there — a "1 kept" sitting under an empty ring is a score for a
  thread nobody has written.
*/
.wave-meter[hidden] {
  display: none;
}

/* ------------------------------------------------------------ the composer */

/*
  What the visitor throws.

  It sits over the middle of the ring rather than beside it: the wave opens
  out of the same place the card was, so the card has to be where the author
  is. Once thrown it collapses into that point and does not come back — the
  loop behind it is a demonstration, and a button that re-arms every six
  seconds is a nag.
*/
.composer {
  position: absolute;
  left: 50%;
  /* Just under the author, whose disc ends at 57% of the square on every
     screen — the ring is drawn in proportion, so one number serves all. */
  top: 60%;
  translate: -50% 0;
  width: min(100%, 20rem);
  padding: 0.9rem 1rem 0.8rem;
  border-radius: var(--radius-lg);
  border: 1px solid var(--line);
  background: var(--ink);
  box-shadow: 0 18px 40px -28px light-dark(rgba(18, 22, 29, 0.5), rgba(0, 0, 0, 0.8));
  transition:
    opacity 320ms ease,
    scale 420ms cubic-bezier(0.4, 0, 0.2, 1);
}
.composer[data-thrown='true'] {
  opacity: 0;
  scale: 0.55;
  pointer-events: none;
}

.composer-said {
  margin: 0 0 0.7rem;
  font-size: 0.9375rem;
  line-height: 1.45;
  color: var(--text);
}

.composer-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}

.composer-reach {
  font-size: 0.8125rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/*
  A pulse on the one thing to press. It is the only animation on the page that
  asks for something rather than showing something, so it stops as soon as it
  is obeyed.
*/
.composer .button-accent {
  animation: waiting 2.2s ease-in-out infinite;
}

@keyframes waiting {
  0%,
  100% {
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 0%, transparent);
  }
  45% {
    box-shadow: 0 0 0 9px color-mix(in srgb, var(--accent) 14%, transparent);
  }
}

/* ----------------------------------------------------------------- sections */

.section {
  width: min(100%, var(--measure));
  margin-inline: auto;
  padding: clamp(2.5rem, 6.5vw, 4.25rem) var(--gutter);
}
.section-tight {
  padding-block: clamp(1rem, 3vw, 2rem);
}

.section-head {
  max-width: 42rem;
  margin-bottom: clamp(1.5rem, 3.5vw, 2.25rem);
}
.section-head h2 {
  font-size: clamp(1.65rem, 4.4vw, 2.4rem);
}

/* -------------------------------------------------------------------- steps */

.steps {
  display: grid;
  gap: 1rem;
}
@media (min-width: 640px) {
  .steps {
    grid-template-columns: repeat(2, 1fr);
  }
}
@media (min-width: 1024px) {
  .steps {
    grid-template-columns: repeat(4, 1fr);
  }
}

.step {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
.step-number {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent);
  font-size: 0.875rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.step h3 {
  font-size: 1.125rem;
}
.step p {
  font-size: 0.9375rem;
  color: var(--muted);
}

/* ----------------------------------------------------------------- features */

.features {
  display: grid;
  gap: 1rem;
}
@media (min-width: 720px) {
  .features {
    grid-template-columns: repeat(2, 1fr);
  }
}

.feature h3 {
  font-size: 1.25rem;
  margin-bottom: 0.5rem;
}
.feature p {
  color: var(--muted);
  font-size: 0.9375rem;
}

/*
  A floor, not a fixed height: the vignettes are different shapes and the
  pills wrap to a second row on a narrow phone. What matters is that the four
  cards start their headings at the same place when they sit side by side.
*/
.feature-art {
  min-height: 76px;
  margin-bottom: 1.1rem;
  display: flex;
  align-items: center;
}

/* Overlapping discs: the roster of a thread, compressed into one mark. */
.stack,
.faces {
  display: flex;
  align-items: center;
}
.stack svg {
  margin-right: -12px;
  border-radius: 999px;
  box-shadow: 0 0 0 2px var(--surface);
}
.stack .stack-more {
  margin-left: 22px;
  font-size: 0.875rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.faces {
  gap: 0.6rem;
  flex-wrap: wrap;
}

/*
  With one face on show the row becomes a stack: both the outgoing and the
  incoming pill occupy the same cell, so nothing shifts sideways as a longer
  name replaces a shorter one.
*/
.faces[data-live='true'] {
  display: grid;
  justify-items: start;
}
.faces[data-live='true'] .face {
  grid-area: 1 / 1;
  transition: opacity 260ms ease;
}
.faces[data-live='true'] .face[data-on='false'] {
  opacity: 0;
}
.face {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.25rem 0.6rem 0.25rem 0.25rem;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--ink);
  font-size: 0.75rem;
  color: var(--muted);
  white-space: nowrap;
}

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}
.tag {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.3rem 0.65rem;
  border-radius: 999px;
  border: 1px solid var(--line);
  font-size: 0.8125rem;
  color: var(--muted);
}
.tag-accent {
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  color: var(--accent);
}
/* Drawn, not an emoji: the rest of the page is one stroked family. */
.tag-icon {
  width: 13px;
  height: 13px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.3;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.code-boxes {
  display: flex;
  gap: 0.4rem;
}
.code-boxes span {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.75rem;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: var(--ink);
  color: var(--muted);
  font-size: 1.125rem;
  font-variant-numeric: tabular-nums;
}
/*
  Every box carries a digit, so none of them is the one being typed into. The
  accent used to mark the first as a caret, which only made sense while the
  code read 000000 and looked like a placeholder rather than a code.
*/
.code-boxes span {
  color: var(--text);
}

/* ------------------------------------------------------------------- absent */

.absent {
  text-align: center;
}
.absent-title {
  font-size: clamp(1.35rem, 3.4vw, 1.75rem);
  margin-bottom: 1.25rem;
}
.absent-list {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem;
}
.absent-list li {
  padding: 0.35rem 0.8rem;
  border-radius: 999px;
  border: 1px dashed var(--line);
  color: var(--muted);
  font-size: 0.875rem;
  /* Struck through, because the point is the absence. */
  text-decoration: line-through;
  text-decoration-color: color-mix(in srgb, var(--muted) 55%, transparent);
}
.absent-note {
  margin-top: 1.25rem;
  margin-inline: auto;
  max-width: 34rem;
  font-size: 0.9375rem;
  color: var(--muted);
}

/* ------------------------------------------------------------------- places */

.places {
  display: grid;
  gap: 1rem;
}
@media (min-width: 720px) {
  .places {
    grid-template-columns: repeat(2, 1fr);
  }
}
.place h3 {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  font-size: 1.25rem;
  margin-bottom: 0.6rem;
}
.place p {
  color: var(--muted);
  font-size: 0.9375rem;
}

/*
  A square where people are circles: in the client that shape is what says at a
  glance that a row is a place you walked into rather than someone who wrote.
*/
.place-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  flex: none;
}
.place-mark-room {
  border-radius: 12px;
  background: color-mix(in srgb, var(--accent) 15%, transparent);
  color: var(--accent);
  font-weight: 600;
}

/* ------------------------------------------------------------------- closer */

.closer {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-lg);
  border: 1px solid var(--line);
  background: var(--surface);
  padding: clamp(2.25rem, 6vw, 3.75rem) clamp(1.25rem, 4vw, 2.5rem);
  text-align: center;
}
.closer-glow {
  position: absolute;
  inset: auto -10% -60% -10%;
  height: 260px;
  background: radial-gradient(
    50% 100% at 50% 100%,
    color-mix(in srgb, var(--brand-from) 30%, transparent),
    transparent 74%
  );
  pointer-events: none;
}
.closer h2 {
  position: relative;
  font-size: clamp(1.6rem, 4.6vw, 2.35rem);
}
.closer > p {
  position: relative;
  margin-top: 0.6rem;
  color: var(--muted);
  font-size: clamp(1rem, 2.4vw, 1.125rem);
}
.closer .button {
  position: relative;
  margin-top: 1.75rem;
}
.closer-note {
  position: relative;
  margin-top: 1rem;
  font-size: 0.8125rem;
  color: var(--muted);
}

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

.footer {
  border-top: 1px solid var(--line);
  margin-top: clamp(2rem, 6vw, 4rem);
}
.footer-inner {
  width: min(100%, var(--measure));
  margin-inline: auto;
  padding: 2rem var(--gutter) max(2rem, env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.75rem;
  text-align: center;
}
.footer-name {
  font-size: 1.1rem;
}
.footer-line {
  font-size: 0.875rem;
  color: var(--muted);
  max-width: 30rem;
}
.footer-cta {
  font-size: 0.875rem;
  color: var(--accent);
}
.footer-cta:hover {
  text-decoration: underline;
}

@media (min-width: 720px) {
  .footer-inner {
    flex-direction: row;
    text-align: left;
  }
  .footer-line {
    margin-inline: auto;
  }
}

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

/*
  Content is visible by default and hidden only where something is running that
  can bring it back. A section that starts at opacity 0 and waits for a script
  is a section that disappears the moment the script does not arrive, and the
  class that gates this is set in the head, before the first paint.
*/
html.has-js .reveal {
  opacity: 0;
  transform: translateY(14px);
  transition:
    opacity 420ms ease-out,
    transform 420ms ease-out;
}
html.has-js .reveal[data-shown='true'] {
  opacity: 1;
  transform: none;
}

/*
  Anyone who has asked for less movement gets the finished page, not a slower
  version of the same animation. Nothing here is load-bearing: every state the
  motion passes through is also legible standing still.
*/
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  html.has-js .reveal {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .wave-node,
  .wave-link,
  .wave-ring {
    transition: none;
  }
}
