/*
  Orby site styles. One file for every page; sections follow the page anatomy:
  tokens → base → backdrop → landing → headline → waitlist → footer → sheet → transitions.

  Motion rules that apply throughout:
  - Only opacity and transform are animated (the one exception is the waitlist button's width,
    which is a deliberate morph).
  - Anything with `backdrop-filter` (the waitlist pill) must not sit under an ancestor whose
    opacity animates: that ancestor becomes the blur's backdrop root and the blur pops in late.
    So the waitlist block only moves, and its pill and note fade themselves.
  - Each section carries its own `prefers-reduced-motion` rules.
*/

/* ---------- Tokens ---------- */

:root {
  --color-canvas: #050507;
  --color-ink: #fff;
  --color-ink-strong: rgb(255 255 255 / 0.9);
  --color-ink-soft: rgb(255 255 255 / 0.8);
  --color-ink-muted: rgb(255 255 255 / 0.75);
  --color-ink-faint: rgb(255 255 255 / 0.6);
  --color-danger: #ff8a8a;
  --color-scrim: rgb(3 6 16 / 0.55);

  --color-paper: #fff;
  --color-paper-ink: #16171c;
  --color-paper-body: rgb(22 23 28 / 0.78);
  --color-paper-muted: rgb(0 0 0 / 0.45);
  --color-paper-link: #1c4fb8;

  --font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro", "Helvetica Neue", Arial,
    sans-serif;
  --font-serif: "Instrument Serif", Georgia, serif;

  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-swap: cubic-bezier(0.2, 0.7, 0.2, 1);
  --ease-accelerate: cubic-bezier(0.4, 0, 1, 1);
  --ease-morph: cubic-bezier(0.65, 0, 0.35, 1);
  --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);

  color-scheme: dark;
}

/* Self-hosted (SIL OFL, see /assets/fonts/OFL.txt). Only the latin subset of the italic is used. */
@font-face {
  font-family: "Instrument Serif";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/fonts/instrument-serif-italic-latin.woff2") format("woff2");
}

/* ---------- Base ---------- */

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

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

body {
  margin: 0;
  min-height: 100svh;
  background: var(--color-canvas);
  color: var(--color-ink);
  font-family: var(--font-sans);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1,
h2,
p,
ul {
  margin: 0;
}

img,
video,
svg {
  display: block;
}

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

button,
input {
  margin: 0;
  border: 0;
  font: inherit;
  color: inherit;
}

:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The italic serif accent, used for the rotating headline word and "Singapore". */
.accent {
  font-family: var(--font-serif);
  font-style: italic;
  font-weight: 400;
  letter-spacing: -0.01em;
}

/* Entrance: blocks rise in a stagger; each block sets its own --delay. */
.rise {
  animation: rise 1000ms var(--ease-out-expo) var(--delay, 0ms) backwards;
}

/* Rise without fading, for blocks that contain backdrop-filter (see the note at the top). */
.rise-move {
  animation: rise-move 1000ms var(--ease-out-expo) var(--delay, 0ms) backwards;
}

@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(18px);
  }
}

@keyframes rise-move {
  from {
    transform: translateY(18px);
  }
}

@keyframes fade-in {
  from {
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .rise,
  .rise-move {
    animation: none;
  }
}

/* ---------- Backdrop: star-trails still + looping video + scrim, behind every page ---------- */

.backdrop {
  position: fixed;
  inset: 0;
  overflow: hidden;
}

/* The still is the loop's first frame, so the video can fade in over it without a jump. */
.backdrop__media {
  position: absolute;
  inset: 0;
  animation: backdrop-in 2600ms cubic-bezier(0.45, 0, 0.25, 1) backwards;
}

.backdrop__still,
.backdrop__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.backdrop__video {
  opacity: 0;
  transition: opacity 700ms var(--ease-standard);
}

.backdrop__video.is-playing {
  opacity: 1;
}

/* Soft darkening behind the copy, extra shade where the bright horizon meets the form note, and
   top/bottom bands that keep the header and footer legible. */
.backdrop__scrim {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 45% 22% at 50% 68%, rgb(3 8 30 / 0.45), rgb(3 8 30 / 0) 100%),
    radial-gradient(ellipse 55% 50% at 50% 52%, rgb(3 8 30 / 0.3), rgb(3 8 30 / 0) 75%),
    linear-gradient(
      to bottom,
      rgb(3 6 16 / 0.4),
      rgb(3 6 16 / 0) 22%,
      rgb(3 6 16 / 0) 70%,
      rgb(3 6 16 / 0.65)
    );
}

@keyframes backdrop-in {
  from {
    opacity: 0;
    transform: scale(1.06);
  }
}

/* Arriving from another page of the site through a view transition: the backdrop is already on
   screen, so it must not fade in from black again (set by /js/page-lifecycle.js). */
[data-intro="skip"] .backdrop__media {
  animation: none;
}

[data-intro="skip"] .backdrop__video {
  transition: none;
}

@media (prefers-reduced-motion: reduce) {
  .backdrop__media {
    animation: none;
  }

  .backdrop__video {
    display: none;
  }
}

/* ---------- Landing ---------- */

.landing {
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 100svh;
}

.landing__header {
  --delay: 100ms;

  display: flex;
  justify-content: center;
  padding: 20px 20px 0;
}

.wordmark {
  width: auto;
  height: 36px;
}

.hero {
  display: flex;
  flex: 1;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  max-width: 680px;
  margin-inline: auto;
  padding: 40px 16px;
  text-align: center;
  text-shadow: 0 2px 24px rgb(0 0 0 / 0.35);
}

.hero .headline {
  --delay: 200ms;
}

.hero__pitch {
  --delay: 320ms;

  max-width: 460px;
  margin-top: 16px;
  color: var(--color-ink-soft);
  font-size: 16px;
  line-height: 1.5;
  text-wrap: pretty;
}

.hero__link {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.hero .waitlist {
  --delay: 440ms;

  margin-top: 28px;
}

@media (min-width: 640px) {
  .landing__header {
    justify-content: flex-start;
    padding: 24px 28px 0;
  }

  .hero__pitch {
    font-size: 17px;
  }
}

/* ---------- Headline with the rotating word ---------- */

.headline {
  font-size: 32px;
  font-weight: 500;
  line-height: 1.08;
  letter-spacing: -0.025em;
}

@media (min-width: 400px) {
  .headline {
    font-size: 40px;
  }
}

@media (min-width: 640px) {
  .headline {
    font-size: 56px;
  }
}

@media (min-width: 768px) {
  .headline {
    font-size: 64px;
  }
}

/* The slot eases to each word's width (set by /js/headline.js). The padding/negative margin pair
   keeps descenders inside the overflow clip without shifting the baseline. */
.headline__slot {
  position: relative;
  display: inline-block;
  overflow: hidden;
  margin-bottom: -0.14em;
  padding-bottom: 0.14em;
  text-align: left;
  vertical-align: bottom;
  transition: width 500ms var(--ease-swap);
}

.headline__word {
  display: inline-block;
  white-space: nowrap;
}

/* Invisible copies of every word, measured to size the slot. */
.headline__sizer {
  position: absolute;
  top: 0;
  left: 0;
  visibility: hidden;
}

.headline__word.is-entering {
  animation: word-in 560ms var(--ease-swap) both;
}

.headline__word.is-leaving {
  position: absolute;
  top: 0;
  left: 0;
  animation: word-out 380ms var(--ease-accelerate) both;
}

@keyframes word-in {
  from {
    opacity: 0;
    transform: translateY(0.45em);
  }
}

@keyframes word-out {
  to {
    opacity: 0;
    transform: translateY(-0.45em);
  }
}

/* Words still change, just without the slide. A duration rather than `none`, so animationend still
   fires and the outgoing word is removed. */
@media (prefers-reduced-motion: reduce) {
  .headline__word.is-entering,
  .headline__word.is-leaving {
    animation-duration: 1ms;
  }
}

/* ---------- Waitlist form ----------
   States live in `data-state` on the form (set by /js/waitlist.js):
   idle → submitting → done, or back to invalid / failed. On submit the white button grows over the
   whole pill while the email fades, a spinner appears, then the check draws in. */

.waitlist {
  width: 100%;
  max-width: 440px;
  text-shadow: none;
}

/* Light white glass. Promoted to its own layer (translateZ): without it Chrome re-tiles the
   backdrop blur whenever the headline word animates and flashes a rectangular patch here. */
.waitlist__pill {
  position: relative;
  height: 56px;
  border-radius: 999px;
  background-color: rgb(255 255 255 / 0.2);
  box-shadow: inset 0 1.5px 2px rgb(255 255 255 / 0.3);
  -webkit-backdrop-filter: blur(24px);
  backdrop-filter: blur(24px);
  transform: translateZ(0);
  will-change: transform;
  transition:
    background-color 300ms ease-out,
    box-shadow 300ms ease-out;
}

.waitlist__pill:hover {
  background-color: rgb(255 255 255 / 0.23);
}

.waitlist__pill:focus-within {
  background-color: rgb(255 255 255 / 0.25);
}

.waitlist__pill:active {
  background-color: rgb(255 255 255 / 0.27);
}

.waitlist[data-state="invalid"] .waitlist__pill {
  box-shadow:
    inset 0 1.5px 2px rgb(255 255 255 / 0.3),
    inset 0 0 0 1px rgb(255 138 138 / 0.8);
}

.waitlist__input {
  width: 100%;
  height: 100%;
  padding: 0 150px 0 22px;
  border-radius: 999px;
  background: transparent;
  font-size: 16px; /* 16px or more stops iOS Safari zooming in on focus */
  outline: none;
  transition:
    opacity 300ms ease-out,
    translate 300ms ease-out;
}

.waitlist__input::placeholder {
  color: var(--color-ink-muted);
  opacity: 1;
}

.waitlist__button {
  position: absolute;
  top: 5px;
  right: 5px;
  bottom: 5px;
  width: 134px;
  overflow: hidden;
  border-radius: 999px;
  background-color: rgb(255 255 255 / 0.9);
  box-shadow: inset 0 1.5px 2px rgb(255 255 255 / 0.95);
  color: #000;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  transition:
    width 560ms var(--ease-morph),
    background-color 560ms var(--ease-morph),
    scale 560ms var(--ease-morph);
}

.waitlist__button:focus-visible {
  outline-color: var(--color-ink);
}

@media (hover: hover) {
  .waitlist__button:hover {
    scale: 0.96;
    background-color: rgb(255 255 255 / 0.85);
  }
}

.waitlist__button:active {
  scale: 0.93;
  background-color: rgb(255 255 255 / 0.75);
}

/* Label, spinner and success share the button's box. */
.waitlist__label,
.waitlist__spinner,
.waitlist__done {
  position: absolute;
  inset: 0;
  align-items: center;
  justify-content: center;
}

.waitlist__label {
  display: flex;
  transition: opacity 150ms var(--ease-standard);
}

.waitlist__spinner,
.waitlist__done {
  display: none;
}

.waitlist__spinner::before {
  content: "";
  width: 18px;
  height: 18px;
  border: 2px solid rgb(0 0 0 / 0.15);
  border-top-color: #000;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

.waitlist__done {
  gap: 10px;
  font-size: 16px;
}

.waitlist__check {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #000;
  color: #fff;
}

.waitlist__check path {
  stroke-dasharray: 24;
  stroke-dashoffset: 24;
  animation: check-draw 420ms cubic-bezier(0.3, 0.7, 0.3, 1) 180ms forwards;
}

/* Submitting and done: the button covers the pill, the email slides away. */
.waitlist:is([data-state="submitting"], [data-state="done"]) .waitlist__input {
  opacity: 0;
  translate: -8px 0;
}

.waitlist:is([data-state="submitting"], [data-state="done"]) .waitlist__button {
  width: calc(100% - 10px);
  scale: 1;
  background-color: rgb(255 255 255 / 0.9);
  cursor: default;
}

.waitlist:is([data-state="submitting"], [data-state="done"]) .waitlist__label {
  opacity: 0;
}

.waitlist[data-state="submitting"] .waitlist__spinner {
  display: flex;
  animation: pop-in 220ms ease-out 250ms backwards;
}

.waitlist[data-state="done"] .waitlist__done {
  display: flex;
  animation: done-in 420ms cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
}

.waitlist[data-state="done"] .waitlist__button {
  animation: pop 460ms cubic-bezier(0.3, 1.4, 0.5, 1);
}

/* The hint and the confirmation share one grid cell, so swapping them never changes the height. */
.waitlist__notes {
  display: grid;
  margin-top: 12px;
  font-size: 13px;
}

.waitlist__note,
.waitlist__confirmation {
  grid-area: 1 / 1;
}

.waitlist__note {
  color: var(--color-ink-faint);
  transition:
    opacity 300ms var(--ease-standard),
    visibility 0s;
}

.waitlist:is([data-state="invalid"], [data-state="failed"]) .waitlist__note {
  color: var(--color-danger);
}

.waitlist__confirmation {
  color: var(--color-ink-muted);
  opacity: 0;
  translate: 0 4px;
  visibility: hidden;
  transition:
    opacity 500ms var(--ease-standard) 200ms,
    translate 500ms var(--ease-standard) 200ms,
    visibility 0s 700ms;
}

.waitlist[data-state="done"] .waitlist__note {
  opacity: 0;
  visibility: hidden;
  transition:
    opacity 300ms var(--ease-standard),
    visibility 0s 300ms;
}

.waitlist[data-state="done"] .waitlist__confirmation {
  opacity: 1;
  translate: 0 0;
  visibility: visible;
  transition-delay: 200ms, 200ms, 0s;
}

.waitlist__email {
  display: inline-block;
  max-width: 220px;
  overflow: hidden;
  color: var(--color-ink);
  font-weight: 500;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}

/* Glass must fade itself rather than inherit its parent's fade (see the note at the top). */
.rise-move .waitlist__pill,
.rise-move .waitlist__notes {
  animation: fade-in 700ms ease-out var(--delay, 0ms) backwards;
}

@keyframes spin {
  to {
    rotate: 1turn;
  }
}

@keyframes check-draw {
  to {
    stroke-dashoffset: 0;
  }
}

@keyframes pop-in {
  from {
    opacity: 0;
    transform: scale(0.8);
  }
}

@keyframes done-in {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
}

@keyframes pop {
  40% {
    transform: scale(1.025);
  }
}

@media (prefers-reduced-motion: reduce) {
  .rise-move .waitlist__pill,
  .rise-move .waitlist__notes,
  .waitlist[data-state="submitting"] .waitlist__spinner,
  .waitlist[data-state="done"] .waitlist__done,
  .waitlist[data-state="done"] .waitlist__button {
    animation: none;
  }

  .waitlist__check path {
    animation: none;
    stroke-dashoffset: 0;
  }
}

/* ---------- Footer ---------- */

.site-footer {
  --delay: 580ms;

  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 0 20px 22px;
  color: var(--color-ink-muted);
  font-size: 13px;
}

.site-footer__tagline {
  color: var(--color-ink-strong);
}

.site-footer__tagline .accent {
  font-size: 16px;
}

/* Mobile: the links row sits above the copyright line. */
.site-footer__meta {
  display: flex;
  flex-direction: column-reverse;
  align-items: center;
  gap: 8px;
}

.site-footer__links {
  display: flex;
  align-items: center;
  column-gap: 20px;
}

.site-footer__links a {
  transition: color 150ms var(--ease-standard);
}

.site-footer__links a:hover {
  color: var(--color-ink);
}

@media (min-width: 640px) {
  .site-footer {
    flex-direction: row;
    justify-content: space-between;
    padding-inline: 28px;
  }

  .site-footer__meta {
    flex-direction: row;
    column-gap: 20px;
  }
}

/* ---------- Sheet: legal and support pages ----------
   A white sheet of paper over the backdrop. It slides up from below and its text scrolls inside.
   A bottom sheet on phones, a centred card from 640px up. */

.sheet-layer {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

/* Dims the backdrop so the sheet reads as the focus. Also a click target that closes the sheet. */
.sheet-dim {
  position: absolute;
  inset: 0;
  background: var(--color-scrim);
  animation: fade-in 600ms ease-out backwards;
}

.sheet {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 720px;
  height: calc(100svh - 40px);
  overflow: hidden;
  border-radius: 24px 24px 0 0;
  background: var(--color-paper);
  box-shadow: 0 30px 80px -20px rgb(0 0 0 / 0.6);
  color: var(--color-paper-ink);
  color-scheme: light;
  animation: sheet-in 900ms var(--ease-out-expo) 80ms backwards;
}

.sheet__header {
  display: flex;
  flex-shrink: 0;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  padding: 22px 24px 18px;
  border-bottom: 1px solid rgb(0 0 0 / 0.06);
}

.sheet__title {
  font-size: 28px;
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.02em;
}

.sheet__meta {
  margin-top: 6px;
  color: var(--color-paper-muted);
  font-size: 13px;
}

.sheet__close {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  margin-right: -6px;
  border-radius: 50%;
  background-color: rgb(0 0 0 / 0.05);
  color: rgb(0 0 0 / 0.6);
  transition:
    background-color 200ms var(--ease-standard),
    color 200ms var(--ease-standard),
    scale 200ms var(--ease-standard);
}

.sheet__close:hover {
  scale: 0.94;
  background-color: rgb(0 0 0 / 0.09);
  color: #000;
}

.sheet__body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 20px 24px 40px;
  color: var(--color-paper-body);
  font-size: 15px;
  line-height: 1.65;
}

.sheet__body:focus-visible {
  outline-color: var(--color-paper-link);
  outline-offset: -2px;
}

.sheet__body h2 {
  margin: 28px 0 8px;
  color: var(--color-paper-ink);
  font-size: 17px;
  font-weight: 600;
  line-height: 1.3;
}

.sheet__body p + p,
.sheet__body ul + p {
  margin-top: 10px;
}

.sheet__body ul {
  margin-top: 8px;
  padding-left: 20px;
  list-style: disc;
}

.sheet__body li + li {
  margin-top: 6px;
}

.sheet__body strong {
  color: var(--color-paper-ink);
  font-weight: 600;
}

.sheet__body a {
  color: var(--color-paper-link);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.sheet__legal {
  margin-top: 32px;
  color: var(--color-paper-muted);
  font-size: 13px;
}

@keyframes sheet-in {
  from {
    transform: translateY(100svh);
  }
}

@media (min-width: 640px) {
  .sheet-layer {
    align-items: center;
    padding: 32px;
  }

  .sheet {
    height: auto;
    max-height: calc(100svh - 64px);
    border-radius: 24px;
  }

  .sheet__header {
    padding: 32px 40px 18px;
  }

  .sheet__title {
    font-size: 34px;
  }

  .sheet__body {
    padding-inline: 40px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sheet-dim,
  .sheet {
    animation: none;
  }
}

/* ---------- Page transitions ----------
   Navigating between pages crossfades the whole page (cross-document View Transitions; browsers
   without them simply navigate). The backdrop is identical on both sides and /js/backdrop.js hands
   the video's position over, so in practice only the foreground changes: the landing fades while
   the sheet slides up, and the other way round. Old and new use the same timing so the backdrop,
   present in both, never dips toward black mid-fade. */

@view-transition {
  navigation: auto;
}

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 400ms;
  animation-timing-function: var(--ease-standard);
}

@media (prefers-reduced-motion: reduce) {
  @view-transition {
    navigation: none;
  }
}
