/* The public site. Every colour, size and radius below comes from the project's own
   design tokens — the files in assets/tokens/ are copies of mobile/design/tokens/,
   not a re-transcription, so the site and the app cannot drift apart.

   Two things this file may not do, and both are load-bearing rather than stylistic:

   - No script. The site chain answers `script-src 'none'`, so every piece of motion
     here is a CSS animation. Scroll reveals use view() timelines behind @supports;
     a browser without them shows the content outright rather than nothing at all.
   - No edit to tokens/. The dark values live behind a [data-theme="dark"] attribute
     the app sets at runtime and nothing here can set. The dark block near the bottom
     therefore restates the handful of roles this page uses, inside a
     prefers-color-scheme query — it is a copy of those values on purpose, and the
     token files stay verbatim copies of the app's. */

@import url("tokens/palette.css?v=2026-08-10");
@import url("tokens/color-roles.css?v=2026-08-10");
@import url("tokens/typography.css?v=2026-08-10");
@import url("tokens/spacing.css?v=2026-08-10");
@import url("tokens/radii.css?v=2026-08-10");

/* Bundled rather than fetched, for the same reason the app bundles them: loading a
   font from someone else's server at render time contradicts the product's own
   promise that nothing sits with a third party. */
@font-face {
  font-family: "Figtree";
  src: url("fonts/Figtree-Regular.ttf?v=2026-08-10") format("truetype");
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: "Figtree";
  src: url("fonts/Figtree-SemiBold.ttf?v=2026-08-10") format("truetype");
  font-weight: 600;
  font-display: swap;
}

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

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

body {
  margin: 0;
  position: relative;
  font-family: "Figtree", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: var(--text-body-lg-size);
  line-height: var(--text-body-lg-line);
  font-weight: var(--weight-regular);
  color: var(--color-on-surface);
  background: var(--color-surface);
  /* The hero is a full-bleed gradient and the device mockup sits partly outside its
     column at narrow widths; without this the page scrolls sideways on a phone. */
  overflow-x: hidden;
}

.wrap {
  width: 100%;
  max-width: 1120px;
  margin: 0 auto;
  padding: 0 var(--gutter-screen);
}

/* ---------- header ---------- */

.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-inline);
  padding: var(--space-5) 0;
  flex-wrap: wrap;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  text-decoration: none;
  color: inherit;
}

.logo-mark {
  display: block;
  width: 40px;
  height: 40px;
  border-radius: 12px;
  flex: none;
}

.wordmark {
  font-size: var(--text-title-lg-size);
  line-height: var(--text-title-lg-line);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
  color: var(--color-primary);
}

.lang {
  display: flex;
  gap: var(--space-2);
  font-size: var(--text-label-lg-size);
}

.lang a,
.lang span {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-target-min);
  padding: 0 var(--space-4);
  border-radius: var(--radius-pill);
  text-decoration: none;
}

.lang a {
  color: var(--color-on-surface-variant);
}

.lang a:hover {
  color: var(--color-primary);
  background: var(--color-primary-container);
}

.lang .current {
  color: var(--color-on-primary-container);
  background: var(--color-primary-container);
  font-weight: var(--weight-semibold);
}

/* On the landing pages the header lies over the hero gradient rather than inside it.
   Nesting it in the hero would have been simpler to style and wrong for assistive
   technology: a <header> inside a sectioning element is not exposed as the page's
   banner landmark, and putting the hero inside <main> to compensate would have put
   the banner inside the main landmark instead. Taken out of the flow here, the header
   is a direct child of <body> — a banner — and <main> still starts at the hero, which
   is where the h1 and the download buttons are. */
.site-header--hero {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 2;
}

.site-header--hero .wordmark {
  color: #ffffff;
}

.site-header--hero .lang a {
  color: rgb(255 255 255 / 0.78);
}

.site-header--hero .lang a:hover {
  color: #ffffff;
  background: rgb(255 255 255 / 0.16);
}

.site-header--hero .lang .current {
  color: #ffffff;
  background: rgb(255 255 255 / 0.22);
}

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

/* The gradient stops at --blue-500 rather than the lighter --blue-400. White on
   blue-400 is 3.4:1 — under AA for the 12px hero note and the button captions, which
   is precisely the text that sits at the light end of a 158deg gradient. blue-400
   survives as the top-corner blob, where nothing is read over it.
   The top padding is what the absolutely-positioned header would otherwise cover. */
.hero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  color: #ffffff;
  background: linear-gradient(158deg, var(--blue-800) 0%, var(--blue-600) 52%, var(--blue-500) 100%);
  padding-top: var(--space-11);
  /* Room for the wave, which is absolutely positioned along the bottom edge and up to 96px tall.
     Its crest reaches about two thirds of the way up, so clearing the whole height would leave a
     visible gap under the copy — 64px puts the content above the crest and not above the box. */
  padding-bottom: calc(var(--space-9) + 64px);
}

/* Two blurred blobs drifting behind the content. Pseudo-elements rather than markup:
   they are decoration and a screen reader has nothing to say about them. */
.hero::before,
.hero::after {
  content: "";
  position: absolute;
  z-index: -1;
  border-radius: 50%;
  filter: blur(70px);
  opacity: 0.55;
  pointer-events: none;
}

.hero::before {
  width: 460px;
  height: 460px;
  top: -140px;
  right: -80px;
  background: radial-gradient(circle, #8fc4ff 0%, rgb(143 196 255 / 0) 70%);
  animation: drift-a 22s ease-in-out infinite alternate;
}

.hero::after {
  width: 520px;
  height: 520px;
  bottom: -220px;
  left: -120px;
  background: radial-gradient(circle, #2f6bff 0%, rgb(47 107 255 / 0) 70%);
  animation: drift-b 26s ease-in-out infinite alternate;
}

@keyframes drift-a {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to { transform: translate3d(-60px, 50px, 0) scale(1.12); }
}

@keyframes drift-b {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to { transform: translate3d(70px, -40px, 0) scale(1.08); }
}

/* Chat bubbles drifting up behind the hero copy. Empty spans rather than text: they are the
   suggestion of a conversation, not a conversation — anything readable in them would be a
   second, unattributed claim on a page whose every sentence is checked against /privacy. */
.hero-bubbles {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
}

.float-bubbles {
  position: absolute;
  display: block;
  border-radius: 18px;
  background: rgb(255 255 255 / 0.1);
  border: 1px solid rgb(255 255 255 / 0.14);
  opacity: 0;
  animation: float-up 19s ease-in-out infinite;
}

/* Three sizes, three lanes, three offsets into one loop — so they never rise in step. */
.float-bubbles:nth-child(1) {
  width: 132px;
  height: 46px;
  left: 6%;
  bottom: -60px;
}

.float-bubbles:nth-child(2) {
  width: 92px;
  height: 38px;
  left: 44%;
  bottom: -60px;
  animation-delay: -7s;
}

.float-bubbles:nth-child(3) {
  width: 158px;
  height: 52px;
  left: 74%;
  bottom: -60px;
  animation-delay: -13s;
}

@keyframes float-up {
  0% { opacity: 0; transform: translate3d(0, 0, 0) rotate(-3deg); }
  12% { opacity: 1; }
  78% { opacity: 1; }
  100% { opacity: 0; transform: translate3d(28px, -560px, 0) rotate(4deg); }
}

/* The seam between the blue hero and the white page under it. The path is drawn wider than the
   viewport and slid sideways, which reads as water rather than as a shape that moves. */
.hero-wave {
  position: absolute;
  left: -20%;
  bottom: -1px;
  width: 140%;
  height: clamp(48px, 7vw, 96px);
  fill: var(--color-surface);
  animation: wave-drift 16s ease-in-out infinite alternate;
}

@keyframes wave-drift {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(-9%, 0, 0); }
}

.hero-inner {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-9);
  align-items: center;
  padding: var(--space-8) 0 var(--space-6);
}

@media (min-width: 900px) {
  .hero-inner {
    grid-template-columns: 1.05fr 0.95fr;
    gap: var(--space-10);
    padding: var(--space-10) 0 var(--space-9);
  }
}

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0 0 var(--space-5);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-pill);
  background: rgb(255 255 255 / 0.16);
  border: 1px solid rgb(255 255 255 / 0.24);
  font-size: var(--text-label-md-size);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

.hero h1 {
  margin: 0 0 var(--space-5);
  font-size: var(--text-display-sm-size);
  line-height: var(--text-display-sm-line);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
}

@media (min-width: 720px) {
  .hero h1 {
    font-size: var(--text-display-md-size);
    line-height: var(--text-display-md-line);
  }
}

@media (min-width: 1024px) {
  .hero h1 {
    font-size: var(--text-display-lg-size);
    line-height: var(--text-display-lg-line);
  }
}

/* The translucent whites below are set by contrast, not by taste: against the
   gradient's lightest stop they land at 5.5:1 and 4.9:1, both over AA. */
.lead {
  margin: 0 0 var(--space-7);
  max-width: 32em;
  font-size: var(--text-title-sm-size);
  line-height: var(--text-title-sm-line);
  color: rgb(255 255 255 / 0.94);
}

.hero-note {
  margin: var(--space-5) 0 0;
  font-size: var(--text-body-sm-size);
  color: rgb(255 255 255 / 0.88);
}

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

.store-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
}

/* Deliberately not an <a>: there is no listing to link to yet, and a button that
   navigated would land on a 404 on Google's or Apple's own domain. It also is not the
   official badge artwork — those terms cover published listings. Both are swapped on
   the day the listings go live. */
.store-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-5);
  min-height: var(--touch-target-min);
  border: 1px solid rgb(255 255 255 / 0.3);
  border-radius: var(--radius-lg);
  /* Dark rather than a white wash: it is what a store badge looks like, and it is the
     one treatment whose contrast does not depend on where the gradient happens to be
     behind it. A backdrop-filter here would have re-rasterised the animating blurred
     blobs behind it on every frame — expensive on exactly the low-end phones this
     product is for, in exchange for an effect a flat fill already gives. */
  background: rgb(6 19 54 / 0.42);
  color: #ffffff;
  text-align: left;
  cursor: default;
}

.store-btn svg {
  width: 26px;
  height: 26px;
  flex: none;
}

.store-btn-text {
  display: flex;
  flex-direction: column;
  line-height: 1.15;
}

.store-btn-text small {
  font-size: var(--text-label-sm-size);
  color: rgb(255 255 255 / 0.88);
}

.store-btn-text strong {
  font-size: var(--text-title-sm-size);
  font-weight: var(--weight-semibold);
}

/* The one live button in the pair's company: a real link to the current Android build.
   White like the badge, so it reads as the enabled half of the row on the gradient. */
.store-btn--apk {
  border-color: transparent;
  background: #ffffff;
  color: var(--blue-700);
  cursor: pointer;
  text-decoration: none;
  margin-top: var(--space-4);
}

.store-btn--apk:hover {
  box-shadow: 0 4px 14px rgb(6 19 54 / 0.25);
}

.store-btn--apk .store-btn-text small {
  color: rgb(6 19 54 / 0.62);
}

.badge {
  margin-left: var(--space-3);
  padding: 2px var(--space-3);
  border-radius: var(--radius-pill);
  background: #ffffff;
  color: var(--blue-700);
  font-size: var(--text-label-sm-size);
  font-weight: var(--weight-semibold);
  white-space: nowrap;
}

/* ---------- the phone mockup ---------- */

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

.phone {
  position: relative;
  width: 272px;
  max-width: 100%;
  aspect-ratio: 9 / 17;
  padding: 12px 10px;
  border-radius: 42px;
  background: #0a1230;
  box-shadow:
    0 2px 0 rgb(255 255 255 / 0.18) inset,
    0 40px 80px rgb(6 19 54 / 0.45);
  animation: float 9s ease-in-out infinite;
}

@keyframes float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-14px); }
}

.phone-screen {
  display: flex;
  flex-direction: column;
  height: 100%;
  border-radius: 32px;
  overflow: hidden;
  background: var(--neutral-150);
}

.phone-bar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-4) var(--space-3);
  background: var(--blue-500);
  color: #ffffff;
}

.phone-avatar {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: rgb(255 255 255 / 0.28);
  flex: none;
}

.phone-who {
  display: flex;
  flex-direction: column;
  line-height: 1.2;
}

.phone-who b {
  font-size: var(--text-label-lg-size);
  font-weight: var(--weight-semibold);
}

.phone-who span {
  font-size: var(--text-label-sm-size);
  color: rgb(255 255 255 / 0.8);
}

/* Bottom-aligned, the way a real conversation sits: the newest message rests just
   above the input and the empty space is above it, not below. */
.phone-chat {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: var(--space-3);
  padding: var(--space-4);
  overflow: hidden;
}

.bubble {
  max-width: 78%;
  padding: var(--space-3) var(--space-4);
  border-radius: 16px;
  font-size: var(--text-body-sm-size);
  line-height: 1.35;
  opacity: 0;
  animation-duration: 14s;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
  animation-fill-mode: both;
}

.bubble--in {
  align-self: flex-start;
  background: var(--color-other-bubble);
  color: var(--color-on-other-bubble);
  border-bottom-left-radius: 4px;
}

.bubble--out {
  align-self: flex-end;
  background: var(--color-own-bubble);
  color: var(--color-on-own-bubble);
  border-bottom-right-radius: 4px;
}

/* One 14-second loop, four entrances. Separate keyframes rather than one set with
   animation-delay: a delayed animation whose iteration is as long as the loop still
   plays across the loop boundary, so the bubbles would fade out at four different
   moments instead of resetting together. */
.bubble--1 { animation-name: enter-1; }
.bubble--2 { animation-name: enter-2; }
.bubble--3 { animation-name: enter-3; }
.bubble--4 { animation-name: enter-4; }

@keyframes enter-1 {
  0%, 3% { opacity: 0; transform: translateY(10px) scale(0.96); }
  8%, 92% { opacity: 1; transform: none; }
  97%, 100% { opacity: 0; transform: none; }
}

@keyframes enter-2 {
  0%, 22% { opacity: 0; transform: translateY(10px) scale(0.96); }
  27%, 92% { opacity: 1; transform: none; }
  97%, 100% { opacity: 0; transform: none; }
}

@keyframes enter-3 {
  0%, 52% { opacity: 0; transform: translateY(10px) scale(0.96); }
  57%, 92% { opacity: 1; transform: none; }
  97%, 100% { opacity: 0; transform: none; }
}

@keyframes enter-4 {
  0%, 72% { opacity: 0; transform: translateY(10px) scale(0.96); }
  77%, 92% { opacity: 1; transform: none; }
  97%, 100% { opacity: 0; transform: none; }
}

/* The three dots, shown only while the other person is "writing" the third bubble. */
.typing {
  align-self: flex-start;
  display: inline-flex;
  gap: 4px;
  padding: var(--space-3) var(--space-4);
  border-radius: 16px;
  border-bottom-left-radius: 4px;
  background: var(--color-other-bubble);
  opacity: 0;
  animation: typing-window 14s linear infinite both;
}

.typing i {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--neutral-400);
  animation: blink 1.2s ease-in-out infinite;
}

.typing i:nth-child(2) { animation-delay: 0.18s; }
.typing i:nth-child(3) { animation-delay: 0.36s; }

@keyframes typing-window {
  0%, 38% { opacity: 0; }
  41%, 52% { opacity: 1; }
  55%, 100% { opacity: 0; }
}

@keyframes blink {
  0%, 100% { opacity: 0.35; transform: translateY(0); }
  50% { opacity: 1; transform: translateY(-3px); }
}

/* Sent, delivered, read: the ticks recolour partway through the loop, which is the
   product feature this whole mockup exists to show. */
.ticks {
  display: inline-block;
  margin-left: var(--space-2);
  font-size: var(--text-label-sm-size);
  color: var(--color-on-own-bubble-meta);
  animation: ticks-read 14s linear infinite both;
}

@keyframes ticks-read {
  0%, 62% { color: rgb(255 255 255 / 0.7); }
  66%, 100% { color: #8fd8ff; }
}

.phone-input {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4) var(--space-4);
  background: var(--neutral-150);
}

.phone-field {
  flex: 1;
  height: 34px;
  border-radius: var(--radius-pill);
  background: var(--neutral-0);
}

.phone-send {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--blue-500);
  flex: none;
}

/* ---------- trust strip ---------- */

.trust {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--gap-stack);
  padding: var(--space-9) 0 var(--space-8);
}

@media (min-width: 720px) {
  .trust {
    grid-template-columns: repeat(4, 1fr);
  }
}

.trust div {
  padding: var(--space-5) var(--space-6);
  border-radius: var(--radius-lg);
  background: var(--color-surface-dim);
}

.trust b {
  display: block;
  margin-bottom: var(--space-2);
  font-size: var(--text-title-sm-size);
  font-weight: var(--weight-semibold);
}

.trust span {
  font-size: var(--text-body-sm-size);
  color: var(--color-on-surface-variant);
}

/* ---------- how to start ---------- */

.steps {
  padding: var(--space-8) 0 var(--space-9);
}

.steps h2 {
  margin: 0 0 var(--space-7);
  font-size: var(--text-headline-md-size);
  line-height: var(--text-headline-md-line);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
}

.steps-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--gap-stack);
}

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

.step {
  position: relative;
  padding: var(--space-7) var(--space-6) var(--space-6);
  border: 1px solid var(--color-outline);
  border-radius: var(--radius-lg);
  background: var(--color-surface-raised);
}

.step h3 {
  margin: var(--space-4) 0 var(--space-3);
  font-size: var(--text-title-md-size);
  line-height: var(--text-title-md-line);
  font-weight: var(--weight-semibold);
}

.step p {
  margin: 0;
  font-size: var(--text-body-md-size);
  line-height: var(--text-body-md-line);
  color: var(--color-on-surface-variant);
}

.step-number {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  background: var(--color-primary);
  color: var(--neutral-0);
  font-size: var(--text-title-lg-size);
  font-weight: var(--weight-semibold);
  line-height: 1;
}

@keyframes steps-in {
  from { transform: scale(0.6) rotate(-24deg); opacity: 0; }
  to { transform: none; opacity: 1; }
}

/* ---------- section headings ---------- */

.section-head {
  max-width: 44em;
  padding: var(--space-9) 0 var(--space-6);
}

.section-head h2 {
  margin: 0 0 var(--space-4);
  font-size: var(--text-headline-lg-size);
  line-height: var(--text-headline-lg-line);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
}

.section-head p {
  margin: 0;
  font-size: var(--text-title-sm-size);
  line-height: var(--text-title-sm-line);
  color: var(--color-on-surface-variant);
}

/* ---------- the chat-list mockup beside the feature list ---------- */

/* The heading and the second device share a row on a wide screen and stack on a phone, where a
   272px mockup above a heading would push the list itself below the fold. */
.features-intro {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-7);
  align-items: center;
}

@media (min-width: 900px) {
  .features-intro {
    grid-template-columns: 1.15fr 0.85fr;
    gap: var(--space-9);
  }
}

.device-aside {
  display: flex;
  justify-content: center;
}

/* The same frame as the hero's, one screen further back in the app: no floating — two phones
   bobbing out of step on one page reads as a fault rather than as motion. Shorter than the
   conversation screen because a list of four ends where the fourth row ends; the hero's 9/17
   would leave a third of the screen empty. */
.phone--list {
  width: 240px;
  aspect-ratio: 9 / 15;
  animation: none;
}

.phone-bar--list {
  padding: var(--space-4);
}

.chat-list {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
  padding: var(--space-3);
  list-style: none;
  overflow: hidden;
}

.chat-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3);
  border-radius: 14px;
  background: var(--neutral-0);
}

/* The mockup is a picture of a phone screen, so everything inside it is a fixed colour rather
   than a theme role — the same rule the hero's mockup already follows. A role here would repaint
   the inside of the phone when the visitor's system is dark, which is not what their phone would
   look like. */
.chat-row .phone-avatar {
  background: var(--blue-100);
}

.chat-text {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.chat-text b {
  font-size: var(--text-label-lg-size);
  font-weight: var(--weight-semibold);
  color: var(--neutral-900);
}

.chat-text span,
.chat-time {
  font-size: var(--text-label-sm-size);
  color: var(--neutral-600);
}

/* The preview line is the one thing here that can be longer than its box. */
.chat-text span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-meta {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
  flex: none;
}

.chat-unread {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: var(--radius-pill);
  background: var(--blue-500);
  color: var(--neutral-0);
  font-size: var(--text-label-sm-size);
  font-weight: var(--weight-semibold);
}

/* ---------- feature groups ---------- */

/* align-items: start, so a group with five items is five items tall instead of being
   stretched to match the sixteen-item one beside it. */
.feature-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
  align-items: start;
  gap: var(--gap-stack);
  padding-bottom: var(--space-9);
}

.feature-group {
  padding: var(--space-7);
  border: 1px solid var(--color-outline);
  border-radius: var(--radius-lg);
  background: var(--color-surface-raised);
  transition: translate 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
}

/* The lift is `translate`, not `transform`. These cards are also `.reveal` targets,
   and an animation's declarations outrank the author's — with `fill-mode: both` the
   reveal's final `transform: none` keeps applying after the scroll range closes, so a
   hover written as `transform` silently did nothing in every browser that supports
   view() timelines. `translate` is a separate property and is not overridden. */
.feature-group:hover {
  translate: 0 -4px;
  border-color: var(--color-primary);
  box-shadow: 0 18px 40px rgb(16 19 34 / 0.08);
}

.feature-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 46px;
  height: 46px;
  margin-bottom: var(--space-5);
  border-radius: 14px;
  background: var(--color-primary-container);
  color: var(--color-primary);
}

.feature-icon svg {
  width: 24px;
  height: 24px;
}

/* Idle motion, not a hover reward: on a phone there is no hover, and these cards are most of
   the page. Eight glyphs on one long loop, each offset so the grid never pulses together. */
.feature-icon--animated svg {
  animation: glyph-drift 6s ease-in-out infinite;
}

.feature-group:nth-child(2n) .feature-icon--animated svg { animation-delay: -1.5s; }
.feature-group:nth-child(3n) .feature-icon--animated svg { animation-delay: -3s; }
.feature-group:nth-child(4n) .feature-icon--animated svg { animation-delay: -4.5s; }

@keyframes glyph-drift {
  0%, 100% { transform: translate3d(0, 0, 0) rotate(0deg); }
  35% { transform: translate3d(0, -3px, 0) rotate(-5deg); }
  70% { transform: translate3d(0, 2px, 0) rotate(4deg); }
}

/* The voice message the group is about, drawn as the app draws it. Twelve bars, each with its
   own height and delay — a shared animation with one delay would be a graphic equaliser. */
.waveform {
  display: flex;
  align-items: center;
  gap: 3px;
  height: 34px;
  margin-bottom: var(--space-4);
}

.waveform i {
  display: block;
  width: 4px;
  height: 30%;
  border-radius: var(--radius-pill);
  background: var(--color-primary);
  opacity: 0.55;
  animation: bar-dance 1.4s ease-in-out infinite;
}

.waveform i:nth-child(1) { animation-delay: -0.1s; }
.waveform i:nth-child(2) { animation-delay: -0.9s; }
.waveform i:nth-child(3) { animation-delay: -0.4s; }
.waveform i:nth-child(4) { animation-delay: -1.2s; }
.waveform i:nth-child(5) { animation-delay: -0.7s; }
.waveform i:nth-child(6) { animation-delay: -0.2s; }
.waveform i:nth-child(7) { animation-delay: -1s; }
.waveform i:nth-child(8) { animation-delay: -0.5s; }
.waveform i:nth-child(9) { animation-delay: -1.3s; }
.waveform i:nth-child(10) { animation-delay: -0.3s; }
.waveform i:nth-child(11) { animation-delay: -0.8s; }
.waveform i:nth-child(12) { animation-delay: -0.6s; }

@keyframes bar-dance {
  0%, 100% { height: 28%; opacity: 0.5; }
  50% { height: 100%; opacity: 1; }
}

/* A padlock that closes as the group scrolls into view. Resting state is shut, so a browser
   that cannot drive a view() timeline shows a closed lock rather than an open one — the
   opposite default would illustrate the wrong thing on exactly the group about privacy. */
.lock-glyph {
  display: block;
  width: 44px;
  height: 44px;
  margin-bottom: var(--space-4);
  color: var(--color-primary);
}

.lock-shackle {
  transform-origin: 24px 22px;
}

@keyframes lock-close {
  from { transform: translateY(-5px) rotate(38deg); }
  to { transform: none; }
}

.feature-group h3 {
  margin: 0 0 var(--space-4);
  font-size: var(--text-title-md-size);
  line-height: var(--text-title-md-line);
  font-weight: var(--weight-semibold);
}

.feature-group ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

.feature {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-top: 1px solid var(--color-outline);
  font-size: var(--text-body-md-size);
  line-height: var(--text-body-md-line);
}

.feature:first-child {
  border-top: none;
}

.chip {
  flex: none;
  padding: 1px var(--space-3);
  border-radius: var(--radius-pill);
  font-size: var(--text-label-sm-size);
  font-weight: var(--weight-semibold);
  white-space: nowrap;
}

.chip--now {
  background: var(--color-success-container);
  color: var(--color-success);
}

.chip--soon {
  background: var(--color-primary-container);
  color: var(--color-on-primary-container);
}

.chip--planned {
  background: var(--color-surface-dim);
  color: var(--color-on-surface-variant);
}

.legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  padding-bottom: var(--space-8);
  font-size: var(--text-body-sm-size);
  color: var(--color-on-surface-variant);
}

.legend span {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

/* ---------- the closing band ---------- */

.band {
  margin: var(--space-6) 0 var(--space-10);
  padding: var(--space-9);
  border-radius: var(--radius-xl);
  background: linear-gradient(140deg, var(--blue-900) 0%, var(--blue-700) 60%, var(--blue-500) 100%);
  color: #ffffff;
}

.band h2 {
  margin: 0 0 var(--space-4);
  font-size: var(--text-headline-md-size);
  line-height: var(--text-headline-md-line);
  font-weight: var(--weight-bold);
}

.band p {
  margin: 0 0 var(--space-6);
  max-width: 46em;
  color: rgb(255 255 255 / 0.88);
}

.band ul {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-5);
  margin: 0;
  padding: 0;
  list-style: none;
}

.band li {
  padding-left: var(--space-6);
  position: relative;
  font-size: var(--text-body-md-size);
  color: rgb(255 255 255 / 0.92);
}

.band li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.55em;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #8fd8ff;
}

/* ---------- closing call to action ---------- */

.cta {
  padding: var(--space-9);
  margin-bottom: var(--space-10);
  border-radius: var(--radius-xl);
  border: 1px solid var(--color-outline);
  background: var(--color-surface-dim);
  text-align: center;
}

.cta h2 {
  margin: 0 0 var(--space-4);
  font-size: var(--text-headline-md-size);
  line-height: var(--text-headline-md-line);
  font-weight: var(--weight-bold);
}

.cta p {
  margin: 0 auto var(--space-7);
  max-width: 40em;
  color: var(--color-on-surface-variant);
}

.cta .store-buttons {
  justify-content: center;
}

/* On white the translucent hero styling would disappear, so the same button gets a
   solid treatment here. */
.cta .store-btn {
  border-color: var(--color-outline-strong);
  background: var(--neutral-900);
  color: var(--neutral-0);
}

.cta .store-btn-text small {
  color: rgb(255 255 255 / 0.72);
}

.cta .badge {
  background: var(--blue-400);
  color: var(--blue-950);
}

/* On the light surface the white treatment would disappear the way the translucent one
   does, so here the live button is the brand fill instead. */
.cta .store-btn--apk {
  border-color: transparent;
  background: var(--blue-700);
  color: #ffffff;
}

.cta .store-btn--apk .store-btn-text small {
  color: rgb(255 255 255 / 0.8);
}

/* ---------- policy pages ---------- */

.policy {
  padding: var(--space-8) 0 var(--space-10);
  max-width: 44em;
}

.policy h1 {
  margin: 0 0 var(--space-3);
  font-size: var(--text-headline-lg-size);
  line-height: var(--text-headline-lg-line);
  font-weight: var(--weight-bold);
}

.policy .updated {
  margin: 0 0 var(--space-8);
  font-size: var(--text-body-sm-size);
  color: var(--color-on-surface-faint);
}

.policy h2 {
  margin: var(--space-8) 0 var(--space-3);
  font-size: var(--text-title-md-size);
  line-height: var(--text-title-md-line);
  font-weight: var(--weight-semibold);
}

.policy p,
.policy li {
  font-size: var(--text-body-md-size);
  line-height: var(--text-body-md-line);
}

.policy ul {
  padding-left: var(--space-6);
}

.policy li {
  margin-bottom: var(--space-3);
}

.notice {
  padding: var(--space-6);
  border-left: 4px solid var(--color-warning);
  border-radius: var(--radius-sm);
  background: var(--color-warning-container);
}

.notice p {
  margin: 0;
}

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

.site-footer {
  padding: var(--space-8) 0;
  border-top: 1px solid var(--color-outline);
  font-size: var(--text-body-sm-size);
  color: var(--color-on-surface-variant);
}

.site-footer .brand {
  margin-bottom: var(--space-4);
}

.site-footer .logo-mark {
  width: 32px;
  height: 32px;
}

.site-footer .wordmark {
  font-size: var(--text-title-md-size);
}

.footer-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5);
  margin: 0 0 var(--space-4);
  padding: 0;
  list-style: none;
}

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

.site-footer p {
  margin: 0 0 var(--space-2);
}

a:focus-visible,
.lang a:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

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

   view() timelines are progressive enhancement in the strictest sense: without the
   @supports gate an unsupporting browser would apply the animation's initial keyframe
   and never advance it, leaving the whole page at opacity 0. Inside the gate, the
   default state stays visible and only browsers that can drive the timeline animate. */

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .reveal {
      animation: reveal linear both;
      animation-timeline: view();
      animation-range: entry 5% cover 26%;
    }

    /* Both of these are driven by the same scroll rather than by a clock, for the same reason
       the reveals are: they say something about the element arriving, and an element that has
       been on screen for a minute has nothing left to announce. */
    .step-number {
      animation: steps-in linear both;
      animation-timeline: view();
      animation-range: entry 10% cover 30%;
    }

    .lock-glyph .lock-shackle {
      animation: lock-close linear both;
      animation-timeline: view();
      animation-range: entry 15% cover 38%;
    }
  }
}

@keyframes reveal {
  from { opacity: 0; transform: translateY(26px); }
  to { opacity: 1; transform: none; }
}

/* ---------- motion preference ----------

   Everything above is decoration. A visitor who has asked their system to stop moving
   things has asked this page too — the mockup keeps its finished state rather than
   disappearing, because the bubbles' resting keyframe is opacity 0. */

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

  .hero::before,
  .hero::after,
  .phone,
  .bubble,
  .typing,
  .typing i,
  .ticks,
  .reveal,
  .feature-group,
  .hero-wave,
  .float-bubbles,
  .step-number,
  .waveform i,
  .lock-glyph .lock-shackle,
  .feature-icon--animated svg {
    animation: none;
    transition: none;
  }

  .bubble,
  .typing {
    opacity: 1;
    transform: none;
  }

  /* The bubbles' own resting keyframe is opacity 0, so stopping the animation would leave three
     invisible boxes drifting nowhere. Hidden outright is what "no motion" means for decoration
     that exists only to move. */
  .hero-bubbles {
    display: none;
  }

  /* Every bar at one height: a frozen waveform mid-dance reads as a rendering fault. */
  .waveform i {
    height: 55%;
    opacity: 0.8;
  }

  .ticks {
    color: #8fd8ff;
  }
}

/* ---------- print ----------

   Printing resolves a view() timeline the same way a full-page screenshot does: the
   whole document is inside one "viewport", the reveal animations never advance, and
   sixteen elements a page — the entire feature list, the band and the closing call to
   action — come out blank. A printed page is also not the place for a device mockup
   whose bubbles are mid-animation. */

@media print {
  /* Browsers disagree about whether prefers-color-scheme still evaluates as dark while
     printing. Pinning the scheme here decides it, and it is also what keeps the roles
     below from resolving to the dark block's near-white on paper. */
  :root {
    color-scheme: light;
    --color-surface: var(--neutral-0);
    --color-on-surface: var(--neutral-900);
    --color-on-surface-variant: var(--neutral-600);
  }

  .reveal,
  .step-number,
  .lock-glyph .lock-shackle {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }

  /* Motion that loops rather than reveals prints as whatever frame it happened to be on, which
     for the bubbles is a set of empty boxes and for the wave is a blue band across the page. */
  .hero-bubbles,
  .hero-wave,
  .waveform,
  .device-aside {
    display: none;
  }

  .feature-icon--animated svg {
    animation: none !important;
    transform: none !important;
  }

  .hero {
    background: none;
    color: var(--neutral-900);
    padding-top: var(--space-6);
  }

  .site-header--hero {
    position: static;
  }

  .site-header--hero .wordmark {
    color: var(--blue-700);
  }

  /* Flattening the hero's background is not enough on its own: every one of these sets
     white explicitly, and an explicit declaration beats the inherited colour — they
     would print white on white. */
  .lead,
  .hero-note,
  .eyebrow,
  .site-header--hero .lang a,
  .site-header--hero .lang .current {
    color: var(--neutral-900);
    background: none;
    border-color: var(--neutral-300);
  }

  .hero-device,
  .store-buttons {
    display: none;
  }

  .feature-group,
  .step,
  .band,
  .cta {
    break-inside: avoid;
    page-break-inside: avoid;
  }

  .band {
    background: none;
    color: var(--neutral-900);
    border: 1px solid var(--neutral-300);
  }

  .band p,
  .band li {
    color: var(--neutral-900);
  }
}

/* ---------- dark mode ----------

   The roles this page uses, restated for a phone set to dark. Values are the ones the
   app's own [data-theme="dark"] block carries; they are copied here because setting
   that attribute needs script and the site has none. The hero keeps its brand gradient
   in both schemes — it is the brand, not a surface. */

/* `screen and` is not decoration: this block and the print block above both redefine
   :root at the same specificity, and this one comes later — so if a print rendering
   ever also matched prefers-color-scheme: dark, it would win and undo the pin. */
@media screen and (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;

    --color-surface: var(--neutral-900);
    --color-surface-dim: var(--neutral-950);
    --color-surface-variant: var(--neutral-800);
    --color-surface-raised: var(--neutral-800);
    --color-on-surface: var(--neutral-50);
    --color-on-surface-variant: #a8b0c8;
    --color-on-surface-faint: var(--neutral-400);

    --color-primary: #7fa6ff;
    --color-primary-container: #16234a;
    --color-on-primary-container: #b9cdfc;

    --color-outline: #2c3247;
    --color-outline-strong: #3d445c;

    --color-success: var(--green-300);
    --color-success-container: #103b2c;
    --color-warning: var(--amber-300);
    --color-warning-container: #3d2a0b;
  }

  .feature-group:hover {
    box-shadow: 0 18px 40px rgb(4 6 14 / 0.5);
  }

  /* The dark theme's primary is a pale blue — white on it is 2.4:1. The number takes the deep
     navy instead, the same inversion the closing call to action's buttons make. */
  .step-number {
    color: var(--blue-950);
  }

  /* The phone's own bezel is nearly the dark surface colour, so the frame disappears and the
     screen looks like it is floating unattached. The hero's mockup sits on the brand gradient
     and has no such problem; this one needs the outline drawn. */
  .phone--list {
    box-shadow:
      0 2px 0 rgb(255 255 255 / 0.18) inset,
      0 0 0 1px rgb(255 255 255 / 0.14),
      0 30px 60px rgb(4 6 14 / 0.6);
  }

  .cta .store-btn {
    background: var(--neutral-0);
    color: var(--neutral-900);
  }

  .cta .store-btn-text small {
    color: var(--neutral-600);
  }
}
