/* Inter, served from here rather than Google Fonts, so text doesn't wait on two more origins.
   One variable file, Google's latin subset (fonts.gstatic.com, Inter v20), preloaded by both pages.
   Its weights are cut to the 400 to 700 the pages and the videos' pages use (bold <b> is 700),
   which takes a quarter off it (README). */
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("assets/fonts/inter-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Tokens: dark is taken from the Fleo product (Figma variables), light is its shadcn
   neutral counterpart. Each colour is light-dark(light, dark): the page follows the system's
   theme, unless the visitor picked one with the sun and moon (theme.js sets data-theme). Light's
   --fg-muted is a step darker than shadcn's, which is under 4.5:1 on --surface. Dark's
   --fg-subtle is a step lighter than #737373 for the same reason: it's 4.2:1 on --bg. */
:root {
  color-scheme: light dark;
  --bg: light-dark(#ffffff, #0a0a0a);
  --surface: light-dark(#f5f5f5, #141414);
  --surface-2: light-dark(#e5e5e5, #262626);
  --fg: light-dark(#0a0a0a, #fafafa);
  --fg-muted: light-dark(#525252, #a1a1a1);
  --fg-subtle: light-dark(#737373, #808080);
  --border: light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.1));
  --border-soft: light-dark(rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.06));
  --border-strong: light-dark(rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.15));
  --primary: light-dark(#171717, #e5e5e5);
  --primary-fg: light-dark(#fafafa, #171717);
  --primary-hover: light-dark(#404040, #ffffff);
  /* The email field's focus border and glow */
  --ring: light-dark(rgba(0, 0, 0, 0.35), rgba(255, 255, 255, 0.35));
  --ring-soft: light-dark(rgba(0, 0, 0, 0.08), rgba(255, 255, 255, 0.08));
  --danger: light-dark(#dc2626, #f87171);
  --success: light-dark(#16a34a, #4ade80);
  --claude: #d97757;
  --codex: light-dark(#262626, #e5e5e5);
  --radius: 8px;
  --radius-full: 9999px;
  --font: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --nav-h: 56px;
  --section: 224px;
  /* The page's side margin, outside the container */
  --gutter: 24px;
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }

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

html {
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--nav-h) + 16px);
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3, p, ul, ol { margin: 0; }
ul, ol { padding: 0; list-style: none; }
a { color: inherit; text-decoration: none; }
img, svg { display: block; }
img { max-width: 100%; height: auto; }

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

.container {
  width: min(1200px, 100% - 2 * var(--gutter));
  margin-inline: auto;
}

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

.skip-link {
  position: absolute;
  top: -48px;
  left: 16px;
  z-index: 20;
  padding: 8px 12px;
  border-radius: var(--radius);
  background: var(--primary);
  color: var(--primary-fg);
  font-size: 14px;
}
.skip-link:focus { top: 12px; }

/* Buttons */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 40px;
  padding: 0 20px;
  border-radius: var(--radius-full);
  font-size: 15px;
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  transition: background-color 0.15s ease;
}
.btn--primary { background: var(--primary); color: var(--primary-fg); }
.btn--primary:hover { background: var(--primary-hover); }
.btn--sm { height: 32px; padding: 0 16px; font-size: 14px; }

/* Small links and buttons take taps on a box of at least 44×44, centred on them, so a finger
   doesn't have to land on the text. The box is empty, so nothing looks different. */
.brand,
.nav__links a,
.nav__actions .btn,
.footer a,
.signup__button { position: relative; }
.brand::after,
.nav__links a::after,
.nav__actions .btn::after,
.footer a::after,
.signup__button::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(100%, 44px);
  height: max(100%, 44px);
  transform: translate(-50%, -50%);
}

/* Waitlist form: one email field with an inline submit button */
.signup { width: 100%; max-width: 460px; }

.signup__field {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 48px;
  /* The field fills the pill's height, so a tap anywhere on its side of the pill lands in it */
  padding: 0 4px 0 0;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-full);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.signup__field:focus-within {
  border-color: var(--ring);
  box-shadow: 0 0 0 3px var(--ring-soft);
}
.signup.is-error .signup__field { border-color: color-mix(in srgb, var(--danger) 60%, transparent); }

.signup__input {
  flex: 1;
  min-width: 0;
  height: 100%;
  padding: 0 0 0 20px;
  background: transparent;
  border: 0;
  /* Rounded like the pill's end it fills, so a browser's autofill colour keeps to its curve */
  border-radius: var(--radius-full) 0 0 var(--radius-full);
  outline: 0;
  color: var(--fg);
  font: inherit;
  /* 16px, not the page's 15: iOS zooms the page into a field whose text is smaller */
  font-size: 16px;
}
.signup__input::placeholder { color: var(--fg-subtle); }
.signup__input:focus-visible { outline: none; }

.signup__button {
  flex: none;
  height: 38px;
  border: 0;
  cursor: pointer;
  font-family: inherit;
}
.signup__button:disabled { opacity: 0.6; cursor: progress; }

.signup__trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.signup__message {
  min-height: 20px;
  margin-top: 8px;
  font-size: 13px;
  color: var(--fg-subtle);
}
.signup.is-error .signup__message { color: var(--danger); }
/* Notice replaces the field at the same height, so nothing below jumps */
.signup.is-done .signup__field { display: none; }
/* A check, then the text: when the text wraps, the check stays by its first line, and the
   text lines up under it, even in the centred CTA */
.signup.is-done .signup__message {
  display: grid;
  grid-template-columns: 16px minmax(0, max-content);
  column-gap: 8px;
  align-content: center;
  text-align: start;
  min-height: 48px;
  margin: 0 0 32px;
  font-size: 15px;
  color: var(--fg);
  animation: signup-notice 0.25s ease;
}
/* Lucide's check, drawn as a mask so it takes the colour */
.signup.is-done .signup__message::before {
  content: "";
  height: 1lh;
  background: var(--success);
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / 16px no-repeat;
}
.cta__signup .signup__message { justify-content: center; }

/* Fine print (the closing form's), in the message's place: a message (an error, or the notice)
   takes over at the same height, so nothing below moves. It's outside the message, which
   script.js rewrites as text, and the empty message stays in the page for screen readers. A form
   without it keeps the message's line free instead. */
.signup__message:empty:has(+ .signup__privacy) {
  min-height: 0;
  margin-top: 0;
}
.signup__message:not(:empty) + .signup__privacy { display: none; }
.signup__privacy {
  min-height: 20px;
  margin-top: 8px;
  font-size: 12px;
  color: var(--fg-subtle);
}
.signup__privacy a {
  text-decoration: underline;
  text-decoration-color: var(--border-strong);
  text-underline-offset: 2px;
  transition: color 0.15s ease, text-decoration-color 0.15s ease;
}
.signup__privacy a:hover {
  color: var(--fg-muted);
  text-decoration-color: currentColor;
}
/* The field comes back the way the notice came in. The whole form animates: the message
   already ran signup-notice as the notice, so it wouldn't run again on its own. */
.signup.is-reset { animation: signup-notice 0.25s ease; }

@keyframes signup-notice {
  from { opacity: 0; transform: translateY(4px); }
}

/* Nav: sticky, over a frosted band that fades out below it rather than ending in a line, as
   iOS's bars do. The band (.nav__glass) runs --nav-fade past the nav, and fades from 16px above
   the nav's foot to the band's end. Its blur steps down an octave at a time, from 20px under the nav
   to half a pixel, over seven layers: each shows in its own stretch of the fade (an eighth of
   it) and overlaps its neighbours, so the blur tapers off evenly rather than in a step or two.
   The page's tint over them (::after) eases out over the same stretch, as a straight fade
   shows where it starts and ends. None takes a click. */
.nav {
  --nav-fade: 80px;
  position: sticky;
  top: 0;
  z-index: 10;
}
.nav__glass {
  --glass-top: calc(var(--nav-h) - 16px);
  --glass-step: calc((var(--nav-fade) + 16px) / 8);
  position: absolute;
  inset: 0 0 calc(-1 * var(--nav-fade));
  z-index: -1;
  pointer-events: none;
}
.nav__glass > span,
.nav__glass::after {
  position: absolute;
  inset: 0;
  -webkit-mask-image: var(--mask);
  mask-image: var(--mask);
}
/* Layer k shows from an eighth k − 1 of the fade, fully over k and k + 1, and is gone by k + 2 */
.nav__glass > span {
  --at: calc(var(--glass-top) + var(--glass-step) * var(--k));
  --mask: linear-gradient(transparent calc(var(--at) - var(--glass-step)), #000 var(--at), #000 calc(var(--at) + var(--glass-step)), transparent calc(var(--at) + 2 * var(--glass-step)));
  -webkit-backdrop-filter: blur(var(--blur));
  backdrop-filter: blur(var(--blur));
}
/* The first runs from the top, under the nav itself */
.nav__glass > :nth-child(1) {
  --mask: linear-gradient(#000 calc(var(--glass-top) + var(--glass-step)), transparent calc(var(--glass-top) + 2 * var(--glass-step)));
  -webkit-backdrop-filter: saturate(140%) blur(20px);
  backdrop-filter: saturate(140%) blur(20px);
}
.nav__glass > :nth-child(2) { --k: 1; --blur: 12px; }
.nav__glass > :nth-child(3) { --k: 2; --blur: 8px; }
.nav__glass > :nth-child(4) { --k: 3; --blur: 4px; }
.nav__glass > :nth-child(5) { --k: 4; --blur: 2px; }
.nav__glass > :nth-child(6) { --k: 5; --blur: 1px; }
.nav__glass > :nth-child(7) { --k: 6; --blur: 0.5px; }
/* The tint, along a cosine: 1 − (1 − cos πt) / 2 at each eighth t of the fade */
.nav__glass::after {
  content: "";
  --mask: linear-gradient(
    #000 var(--glass-top),
    rgba(0, 0, 0, 0.962) calc(var(--glass-top) + var(--glass-step)),
    rgba(0, 0, 0, 0.854) calc(var(--glass-top) + 2 * var(--glass-step)),
    rgba(0, 0, 0, 0.691) calc(var(--glass-top) + 3 * var(--glass-step)),
    rgba(0, 0, 0, 0.5) calc(var(--glass-top) + 4 * var(--glass-step)),
    rgba(0, 0, 0, 0.309) calc(var(--glass-top) + 5 * var(--glass-step)),
    rgba(0, 0, 0, 0.146) calc(var(--glass-top) + 6 * var(--glass-step)),
    rgba(0, 0, 0, 0.038) calc(var(--glass-top) + 7 * var(--glass-step)),
    transparent calc(var(--glass-top) + 8 * var(--glass-step))
  );
  background: color-mix(in srgb, var(--bg) 72%, transparent);
}

.nav__inner {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  height: var(--nav-h);
}

.brand { justify-self: start; color: var(--fg); }
/* The icon and the wordmark. The icon's glyph takes the page colour, so in dark mode the icon turns
   white with a dark glyph. */
.brand__logo { width: 93px; height: 24px; }
.brand__glyph { fill: var(--bg); }

.nav__links {
  display: flex;
  gap: 32px;
  font-size: 14px;
  color: var(--fg-muted);
}
.nav__links a { transition: color 0.15s ease; }
.nav__links a:hover { color: var(--fg); }

.nav__actions { justify-self: end; }

/* Painterly art panel (CSS only) */
.art {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  border-radius: var(--radius);
  background:
    radial-gradient(40% 35% at 16% 74%, rgba(186, 162, 128, 0.75) 0%, transparent 70%),
    radial-gradient(55% 40% at 82% 28%, rgba(158, 176, 208, 0.8) 0%, transparent 70%),
    radial-gradient(60% 45% at 24% 16%, rgba(216, 204, 230, 0.9) 0%, transparent 70%),
    radial-gradient(70% 40% at 66% 90%, rgba(104, 120, 138, 0.85) 0%, transparent 70%),
    linear-gradient(180deg, #c3bccd 0%, #a4a6b8 42%, #8b93a0 58%, #9aa0b2 100%);
}
.art::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background-image: var(--grain);
  opacity: 0.35;
  mix-blend-mode: overlay;
  pointer-events: none;
}
.art > * { position: relative; z-index: 1; }

/* App screenshot sitting on an art panel. Its <picture> (a light and a dark one) steps out of
   the way, so the img is the panel's child as far as layout goes, and stays above the grain.
   Its <source> would step into the panel too, with a row of its own that pushes the img below
   centre, so it's hidden. */
.hero__art picture { display: contents; }
.hero__art source { display: none; }
.hero__art img {
  position: relative;
  z-index: 1;
  border-radius: 12px;
  box-shadow:
    0 0 0 1px light-dark(rgba(0, 0, 0, 0.08), rgba(255, 255, 255, 0.08)),
    0 10px 30px rgba(0, 0, 0, 0.35),
    0 30px 80px -20px rgba(0, 0, 0, 0.6);
}

/* Hero */
.hero { padding-top: 112px; }

.hero__title {
  max-width: 640px;
  font-size: clamp(30px, 3.4vw, 40px);
  font-weight: 500;
  line-height: 1.15;
  letter-spacing: -0.025em;
}

.hero__lede {
  max-width: 600px;
  margin-top: 16px;
  font-size: 18px;
  color: var(--fg-muted);
}

.hero__signup { margin-top: 32px; }

.hero__art {
  display: grid;
  place-items: center;
  aspect-ratio: 5 / 3;
  margin-top: 56px;
}
.hero__art img {
  width: 78%;
  animation: hero-rise 1.6s cubic-bezier(0.33, 1, 0.68, 1) 0.2s both;
}

/* The .art light, split up: the base stays, and its four patches of colour (.hero__glow) drift
   on their own loops, mostly along the edges the screenshot leaves showing. They start where
   the .art gradients sit, so at rest the panel looks the same. */
.hero__art {
  background: linear-gradient(180deg, #c3bccd 0%, #a4a6b8 42%, #8b93a0 58%, #9aa0b2 100%);
}
.hero__glow {
  position: absolute;
  z-index: -1;
  border-radius: 50%;
  animation: 30s ease-in-out infinite alternate;
}
.hero__glow--slate {
  top: 62%;
  left: 17%;
  width: 98%;
  height: 56%;
  background: radial-gradient(closest-side, rgba(104, 120, 138, 0.85), transparent);
  animation-name: glow-slate;
  animation-duration: 54s;
}
.hero__glow--lilac {
  top: -15.5%;
  left: -18%;
  width: 84%;
  height: 63%;
  background: radial-gradient(closest-side, rgba(216, 204, 230, 0.9), transparent);
  animation-name: glow-lilac;
}
.hero__glow--blue {
  top: 0;
  left: 43.5%;
  width: 77%;
  height: 56%;
  background: radial-gradient(closest-side, rgba(158, 176, 208, 0.8), transparent);
  animation-name: glow-blue;
  animation-duration: 38s;
}
.hero__glow--sand {
  top: 49.5%;
  left: -12%;
  width: 56%;
  height: 49%;
  background: radial-gradient(closest-side, rgba(186, 162, 128, 0.75), transparent);
  animation-name: glow-sand;
  animation-duration: 46s;
}
/* Scrolled out of view (script.js): stop the drift rather than keep the GPU busy */
.hero__art.is-paused .hero__glow { animation-play-state: paused; }

@keyframes hero-rise {
  from { opacity: 0; transform: translateY(20px); }
}
/* Lilac along the top, blue down the right edge and back across the top,
   sand and slate crossing each other along the bottom */
@keyframes glow-lilac {
  50% { transform: translate(28%, -12%) scale(1.08); }
  to { transform: translate(48%, -6%) scale(0.96); }
}
@keyframes glow-blue {
  50% { transform: translate(-4%, 36%) scale(1.1); }
  to { transform: translate(-32%, -18%) scale(1); }
}
@keyframes glow-sand {
  50% { transform: translate(45%, 26%) scale(1.12); }
  to { transform: translate(95%, 20%) scale(1.04); }
}
@keyframes glow-slate {
  50% { transform: translate(-25%, 2%) scale(1.08); }
  to { transform: translate(-48%, -4%) scale(0.96); }
}

/* Sections */
.section { padding-top: var(--section); }

.section__title {
  max-width: 720px;
  margin-bottom: 40px;
  font-size: clamp(24px, 2.6vw, 32px);
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: -0.02em;
}

/* Problem statement: one paragraph, the second sentence runs on in grey */
.statement__text {
  max-width: 820px;
  font-size: clamp(26px, 3.2vw, 36px);
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: -0.025em;
  text-wrap: pretty;
}
.statement__more { color: var(--fg-muted); }

/* What Fleo connects, under the problem statement: four columns, no cards, each its icon over
   its title and a line */
.tiles {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 40px;
  margin-top: 80px;
}

.tile__icon {
  width: 24px;
  height: 24px;
  fill: none;
  stroke: var(--fg);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.tile__title {
  margin-top: 24px;
  font-size: 16px;
  font-weight: 500;
}

.tile__text {
  margin-top: 8px;
  color: var(--fg-muted);
  line-height: 1.45;
  text-wrap: pretty;
}

/* Apps: the heading on the left, and on the right a plain list of the apps, each its icon
   beside its name and a line on what Fleo takes from it. The icons carry macOS's own margin
   around the shape, so they're pulled out by it to line up with the text. */
.apps {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
  gap: 96px;
  align-items: start;
}
.apps .section__title { margin-bottom: 0; }
.apps__body {
  margin-top: 16px;
  font-size: 17px;
  color: var(--fg-muted);
  text-wrap: pretty;
}

.apps__list {
  display: grid;
  gap: 32px;
  padding-top: 8px;
}

.app {
  display: flex;
  align-items: flex-start;
  gap: 16px;
}

/* As in the hero: the <picture> steps out of the row, and its <source> stays out of it */
.app picture { display: contents; }
.app source { display: none; }

.app__icon {
  flex: none;
  width: 48px;
  height: 48px;
  margin: -4px;
}

.app__title {
  font-size: 16px;
  font-weight: 500;
}

.app__text {
  margin-top: 4px;
  color: var(--fg-muted);
}

/* How it works: no panel, the copy and its steps on one side, the video on the other */
.feature {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr);
  gap: 80px;
  align-items: center;
}

.feature__copy {
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.feature__title {
  font-size: clamp(22px, 2vw, 26px);
  font-weight: 500;
  line-height: 1.25;
  letter-spacing: -0.015em;
}

.feature__body {
  margin-top: 8px;
  font-size: 17px;
  color: var(--fg-muted);
}

.feature__art {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-items: center;
}

/* Each video carries its own light and keeps its shape, so the window in it keeps its margins */
.feature__art video {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1544 / 1080;
  object-fit: cover;
}

/* A video asked to play before it has loaded (script.js) shows a short line in its middle,
   filled to how much has come in (--progress), until it plays. It fades in after a moment, so a
   video that's ready at once doesn't flash it. */
.scene::after,
.feature__art--session::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 2;
  width: 100px;
  height: 2px;
  margin: -1px 0 0 -50px;
  border-radius: 1px;
  background:
    linear-gradient(var(--fg), var(--fg)) left / calc(var(--progress, 0) * 100%) 100% no-repeat,
    var(--border);
  opacity: 0;
  transition: opacity 0.2s ease, background-size 0.2s linear;
  pointer-events: none;
}
.is-waiting::after {
  opacity: 1;
  transition-delay: 0.15s, 0s;
}

/* Steps */
.steps { margin-top: 40px; }

.step {
  position: relative;
  display: grid;
  grid-template-columns: 32px 1fr;
  column-gap: 16px;
  padding: 16px 0;
}
.step:last-child { padding-bottom: 0; }

/* The active step's line: under its name, as wide as the name, on a faint track. It's the
   trigger's background, as the trigger's ::after is the step's click target. */
.step.is-active .step__trigger {
  background:
    linear-gradient(var(--fg), var(--fg)) left bottom / 100% 2px no-repeat,
    linear-gradient(var(--border), var(--border)) left bottom / 100% 2px no-repeat;
}

/* While the tour auto-advances (script.js), the line fills, then the next step shows. It waits
   while the step's video loads. */
[data-tour-auto] .step.is-active .step__trigger {
  animation: step-progress 6s linear;
  animation-play-state: paused;
}
[data-tour-auto].is-playing:not(.is-loading):not(:hover):not(:focus-within) .step.is-active .step__trigger {
  animation-play-state: running;
}

@keyframes step-progress {
  from { background-size: 0 2px, 100% 2px; }
}
@keyframes step-in-next {
  from { opacity: 0; transform: translateX(12px); }
}
@keyframes step-in-prev {
  from { opacity: 0; transform: translateX(-12px); }
}

.step__num {
  grid-row: span 2;
  padding-top: 2px;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--fg-subtle);
}
.step__title {
  font-size: 15px;
  font-weight: 500;
  color: var(--fg-muted);
  transition: color 0.15s ease;
}
.step.is-active .step__title,
.step:hover .step__title { color: var(--fg); }
.step__text {
  margin-top: 4px;
  font-size: 14px;
  line-height: 1.45;
  color: var(--fg-muted);
}

.step__trigger {
  padding: 0 0 8px;
  background: none;
  border: 0;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
/* The whole step is the click target */
.step__trigger::after {
  content: "";
  position: absolute;
  inset: 0 -8px;
  border-radius: 6px;
}
.step__trigger:focus-visible { outline: none; }
.step__trigger:focus-visible::after { outline: 2px solid var(--fg); }

/* Agents: no panel, the video on the page beside the copy, and the two agents side by side
   under it, each its logo over its name: Claude's spark in its orange, from the Claude Code
   extension, and the OpenAI mark for Codex in the text's colour. */
.agents {
  display: grid;
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
  gap: 80px;
  align-items: center;
}

.agents__pair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 24px;
  margin-top: 48px;
}

.agent__logo {
  width: 28px;
  height: 28px;
  fill: currentColor;
}
.agent__logo--claude { color: var(--claude); }
.agent__logo--codex { color: var(--codex); }

.agent__name {
  margin-top: 16px;
  font-size: 16px;
  font-weight: 500;
}

.agent__text {
  margin-top: 8px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--fg-muted);
}

/* How it works: one video per step, stacked so switching never moves the layout. They only
   fade: sliding would show the panel's edge. */
.scene {
  grid-area: 1 / 1;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.4s ease, visibility 0s linear 0.4s;
}
.scene.is-active {
  opacity: 1;
  visibility: visible;
  transition-delay: 0s;
}

/* Photo panel: a darkened photo with white text, in both themes. Dark until the photo loads. */
.photo {
  position: relative;
  background: #141414;
  color: #fafafa;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--radius);
  isolation: isolate;
}

/* As the photo comes into view, from 40% in until it reaches the middle of the screen, its frame
   draws in around it and the photo settles back inside it, toward focus. Both follow the
   frame's own scroll (--photo, so the photo's scale doesn't move its own timeline), back as
   well as forward. Where scroll-driven animations aren't supported, and with reduced motion,
   the frame and photo stay as they are. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .photo {
      view-timeline: --photo;
      animation: photo-focus linear both;
      animation-timeline: --photo;
      animation-range: entry 40% cover 50%;
    }
    .photo__img {
      animation: photo-settle linear both;
      animation-timeline: --photo;
      animation-range: entry 40% cover 50%;
    }
  }
}
@keyframes photo-focus {
  from { clip-path: inset(0 round var(--radius)); }
  to { clip-path: inset(3% 4.5% round 20px); }
}
@keyframes photo-settle {
  from { transform: scale(1.08); }
}
.photo__img {
  position: absolute;
  inset: 0;
  z-index: -2;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.photo::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    linear-gradient(180deg, rgba(10, 10, 10, 0) 35%, rgba(10, 10, 10, 0.82) 100%),
    rgba(10, 10, 10, 0.18);
}

.photo__copy {
  padding: 0 24px 56px;
  text-align: center;
}
.photo__title {
  font-size: clamp(26px, 3.2vw, 40px);
  font-weight: 500;
  line-height: 1.15;
  letter-spacing: -0.025em;
}
.photo__body {
  margin-top: 16px;
  font-size: 18px;
  color: rgba(250, 250, 250, 0.78);
}

.pills {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
  margin-top: 24px;
}
.pills li {
  padding: 8px 12px;
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: var(--radius-full);
  background: rgba(255, 255, 255, 0.08);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  font-size: 13px;
  font-weight: 500;
}

/* Final CTA: the same room above as every section, and as much again below, before the footer */
.cta { padding-bottom: var(--section); }

.cta__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

/* Fleo's app icon above the title. The image has a clear margin around the icon, so the gap
   below looks a little wider than its margin. Its shadow lifts it off the dark page; on the light
   one, where a dark icon already stands out, the wide one goes and the close one is faint. */
.cta__mark {
  position: relative;
  margin-bottom: 28px;
}
/* A dark icon and a light one, which follows the theme (theme.js) */
.cta__mark picture { display: contents; }
.cta__icon,
.cta__canvas {
  display: block;
  filter:
    drop-shadow(0 24px 40px light-dark(transparent, rgba(0, 0, 0, 0.45)))
    drop-shadow(0 6px 12px light-dark(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.35)));
}

/* 3D, with a mouse (public/3d/icon.js): the icon as a tile that turns towards the mouse, on a
   canvas that reaches past it, so its edge isn't cut off as it turns. It fades in once it has
   drawn (is-3d), and the <img> goes once it has. The shadow follows the tile's outline. */
.cta__stage {
  position: absolute;
  inset: -30%;
  pointer-events: none;
}
.cta__canvas {
  position: absolute;
  top: 0;
  left: 0;
  opacity: 0;
  transition: opacity 0.2s ease;
}
.cta__mark.is-3d .cta__canvas { opacity: 1; }
.cta__mark.is-3d .cta__icon {
  opacity: 0;
  transition: opacity 0s 0.2s;
}
/* A finger down on it turns it sideways; up and down still scroll the page. No callout to save
   the hidden <img> on a long press. */
.cta__mark.is-3d {
  touch-action: pan-y pinch-zoom;
  -webkit-touch-callout: none;
  user-select: none;
}

.cta__title {
  font-size: clamp(28px, 3.2vw, 40px);
  font-weight: 500;
  line-height: 1.15;
  letter-spacing: -0.025em;
}

.cta__body {
  max-width: 520px;
  margin-top: 16px;
  font-size: 18px;
  color: var(--fg-muted);
}

.cta__signup { margin-top: 32px; }

/* Turnstile bot check: invisible unless Cloudflare wants a click (is-checking) */
.signup.is-checking .signup__turnstile { margin-top: 8px; }
.cta__signup .signup__turnstile { display: flex; justify-content: center; }

/* Confirm and 404 pages: one centred message under the logo, footer at the bottom */
.confirm-page {
  display: grid;
  grid-template-rows: 1fr auto;
  min-height: 100vh;
  min-height: 100dvh;
}
.confirm {
  display: grid;
  place-items: center;
  padding-block: 48px;
}
.confirm [hidden] { display: none; }
.confirm__brand { justify-self: auto; margin-bottom: 32px; }
.confirm__body { overflow-wrap: anywhere; white-space: pre-line; }
.confirm__actions { margin-top: 32px; }
.confirm__button { border: 0; cursor: pointer; font-family: inherit; }
.confirm__button:disabled { opacity: 0.6; cursor: progress; }
.confirm__message {
  min-height: 20px;
  margin-top: 8px;
  font-size: 13px;
  color: var(--danger);
}
/* A short screen (a phone on its side, or a small one): less room around the message and the
   footer, so both fit without scrolling */
@media (max-height: 640px) {
  .confirm { padding-block: 24px; }
  .confirm__brand { margin-bottom: 16px; }
  .confirm-page .footer__inner { padding-block: 20px; }
}

/* Privacy policy: one column of reading text */
.legal {
  max-width: 680px;
  padding-block: 112px 160px;
}
.legal__title {
  font-size: clamp(28px, 3.2vw, 40px);
  font-weight: 500;
  line-height: 1.15;
  letter-spacing: -0.025em;
}
.legal__updated {
  margin-top: 16px;
  font-size: 14px;
  color: var(--fg-subtle);
}
.legal h2 {
  margin: 48px 0 16px;
  font-size: 19px;
  font-weight: 500;
  letter-spacing: -0.01em;
}
.legal__updated + p { margin-top: 48px; }
.legal p,
.legal li {
  font-size: 16px;
  line-height: 1.65;
  color: var(--fg-muted);
}
.legal p + p,
.legal ul + p { margin-top: 16px; }
.legal ul {
  margin-top: 8px;
  padding-left: 24px;
  list-style: disc;
}
.legal li + li { margin-top: 8px; }
.legal li::marker { color: var(--fg-subtle); }
.legal a {
  color: var(--fg);
  text-decoration: underline;
  text-decoration-color: var(--border-strong);
  text-underline-offset: 3px;
  transition: text-decoration-color 0.15s ease;
}
.legal a:hover { text-decoration-color: currentColor; }

/* The privacy policy over the page (privacy-dialog.js): the same text as /privacy, scrolling
   inside the dialog, with the close button pinned in its corner. */
html:has(.policy[open]) { overflow: hidden; }
.policy {
  width: min(720px, 100% - 2 * var(--gutter));
  max-width: none;
  max-height: min(800px, 100dvh - 64px);
  padding: 0;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--fg);
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.24);
}
.policy[open] {
  display: flex;
  flex-direction: column;
  animation: signup-notice 0.2s ease;
}
.policy::backdrop {
  background: rgba(0, 0, 0, 0.48);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
}
.policy[open]::backdrop { animation: policy-backdrop 0.2s ease; }
@keyframes policy-backdrop {
  from { opacity: 0; }
}
/* Two classes, to win over the page's padding for .legal at every width */
.policy .policy__body {
  min-height: 0;
  max-width: none;
  padding: 48px;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.policy__body .legal__title {
  padding-right: 32px;
  font-size: 32px;
}
.policy__close {
  position: absolute;
  top: 16px;
  right: 16px;
  z-index: 1;
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  background: var(--bg);
  color: var(--fg-muted);
  cursor: pointer;
  transition: color 0.15s ease, background-color 0.15s ease;
}
.policy__close:hover {
  background: var(--surface);
  color: var(--fg);
}
.policy__close svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
}

/* Footer */
.footer {
  font-size: 13px;
  color: var(--fg-subtle);
}
/* The copyright, email and privacy policy, centred. The theme switch ends the row, but on a
   desktop it's fixed in the corner, out of it. */
.footer__inner {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 8px;
  padding-block: 48px;
}
.footer a { transition: color 0.15s ease; }
.footer a:hover { color: var(--fg); }

/* Theme switch (theme.js): a sun and a moon, the current theme's on a faint disc, fixed in the
   viewport's bottom-right corner (on a tablet or phone, in the footer). It floats over the page,
   so it sits on a frosted pill like the nav's, which reads over the hero's light and the photo
   alike. The buttons are 24px, the smallest comfortable target. */
.theme-switch {
  position: fixed;
  right: 16px;
  bottom: 16px;
  z-index: 10;
  display: inline-flex;
  align-items: center;
  padding: 4px;
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--bg) 72%, transparent);
  -webkit-backdrop-filter: saturate(140%) blur(12px);
  backdrop-filter: saturate(140%) blur(12px);
}
.theme-switch[hidden] { display: none; }
.theme-switch__button {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  padding: 0;
  background: none;
  border: 0;
  border-radius: var(--radius-full);
  color: var(--fg-subtle);
  cursor: pointer;
  transition: color 0.15s ease;
}
.theme-switch__button[aria-pressed="true"] {
  background: var(--border);
  color: var(--fg-muted);
  cursor: default;
}
.theme-switch__button:hover { color: var(--fg); }
.theme-switch__icon {
  width: 14px;
  height: 14px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Scroll reveal (only when script.js opts in) */
.js .reveal {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}
.js .reveal.is-visible {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .signup.is-done .signup__message,
  .signup.is-reset,
  .policy[open],
  .policy[open]::backdrop { animation: none; }
  .scene { transition: none; }
  .hero__art img,
  .hero__glow { animation: none; }
}

/* Responsive */
@media (max-width: 960px) {
  .tiles {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 40px 24px;
  }
  .apps {
    grid-template-columns: minmax(0, 1fr);
    gap: 48px;
  }
  .feature { grid-template-columns: minmax(0, 1fr); }
  /* Agents: the copy, then the video */
  .agents {
    grid-template-columns: minmax(0, 1fr);
    gap: 40px;
  }
  .agents .feature__art { order: 1; }
  .hero__art img { width: 86%; }
  .photo { aspect-ratio: 4 / 3; }

  /* How it works, narrow: the title, then the cards, then the step the card in front is
     showing. The copy holds the title and the steps, so it steps out of the way (display:
     contents) and the three take their own places in the section's column. */
  [data-tour] { display: flex; flex-direction: column; align-items: stretch; gap: 0; }
  [data-tour] .feature__copy { display: contents; }
  [data-tour] .feature__title { order: 1; margin-bottom: 32px; }
  [data-tour] .feature__art--tour { order: 2; }
  [data-tour] .steps { order: 3; }
  /* A step sliding in from the right would poke past the screen's edge, and the page would
     scroll sideways under the swipe. Clipped at the section, which spans the screen. */
  .section:has([data-tour]) { overflow-x: clip; }

  /* The scenes become a row of cards, the next one peeking so it's clear it can be swiped.
     Each is an .art panel of its own (index.html), the light its video starts on, so the panel
     behind them doesn't show its own; wider, they stack on it exactly. The row runs
     out through the page's margins to the screen's edges, so a swiped card leaves by the
     screen's edge, not the margin's. A card comes to rest in line with the page, and the next
     one shows --peek of itself at the right edge: 16px on a tablet, 12px on a phone. */
  .feature__art--tour {
    --peek: 16px;
    display: flex;
    gap: 8px;
    margin-inline: calc(-1 * var(--gutter));
    padding-left: var(--gutter);
    scroll-padding-left: var(--gutter);
    overflow-x: auto;
    background: none;
    border-radius: 0;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
  }
  .feature__art--tour::before { content: none; }
  .feature__art--tour::-webkit-scrollbar { display: none; }
  /* Room past the last card, so it too can come to rest against the left edge. As tall as the
     cards: Firefox leaves an empty box out of the row's scroll width, and the last card stops short */
  .feature__art--tour::after { content: ""; flex: 0 0 var(--peek); align-self: stretch; }
  .feature__art--tour .scene {
    grid-area: auto;
    flex: 0 0 calc(100% - var(--peek) - 8px);
    overflow: hidden;
    border-radius: var(--radius);
    opacity: 1;
    visibility: visible;
    scroll-snap-align: start;
    /* A flick moves one card, not past it */
    scroll-snap-stop: always;
    transition: none;
  }
  /* A card's video starts on the panel's light, as the Agents video does, with the loading line
     over it until it plays. One that won't play has its poster up, its last frame, which goes
     behind it too: WebKit (Safari, and every browser on iOS) stops showing the poster as soon as
     a video is played or seeked, before it has a frame to show. */
  .feature__art--tour video[poster] { background: var(--poster) center / cover no-repeat; }

  /* One step at a time, under its card. They sit in one cell, so the block is as tall as the
     longest and swiping doesn't jog the page. The ones not showing stay in the page for screen
     readers and crawlers rather than being hidden from them, and tabbing to one brings its
     card over (script.js). */
  .steps { display: grid; margin-top: 24px; }
  .step { grid-area: 1 / 1; padding: 0; }
  .step:not(.is-active) { opacity: 0; pointer-events: none; }
  /* A swipe's step fades out, and the next one comes in once it's gone, from the side its card
     came from (script.js sets the direction) */
  @media (prefers-reduced-motion: no-preference) {
    .step { transition: opacity 0.15s ease; }
    .step.is-active { animation: step-in-next 0.3s ease-out 0.1s backwards; }
    [data-step-direction="prev"] .step.is-active { animation-name: step-in-prev; }
  }
  /* The card in front is the step, so there's no line to fill */
  .step .step__trigger,
  .step.is-active .step__trigger { padding-bottom: 0; background: none; }

  /* Short of a desktop, the theme switch doesn't float over the page: it ends the footer's row,
     at the right edge, and the text starts at the left. Without the pill, and pulled in so the
     footer keeps its height. */
  .theme-switch {
    position: static;
    margin: -4px 0 -4px auto;
    padding: 0;
    border: 0;
    background: none;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

@media (max-width: 768px) {
  :root { --section: 152px; }
  .nav__inner { grid-template-columns: 1fr auto; }
  .nav__links { display: none; }
  .hero { padding-top: 72px; }
  .legal { padding-block: 72px 120px; }
}

@media (max-width: 560px) {
  :root { --section: 120px; --gutter: 16px; }
  .feature__art--tour { --peek: 12px; }
  /* The nav's band fades out sooner, so it covers less of a phone's screen */
  .nav { --nav-fade: 60px; }
  .hero__lede,
  .cta__body,
  .photo__body { font-size: 16px; }

  /* The app's top-left corner pinned in the frame and the rest running off the right, its whole
     height in view, with as much light below it as above and beside it. Taller than the cards
     below, so it shows bigger. */
  .hero__art {
    place-items: start;
    aspect-ratio: 3 / 4;
    margin-top: 32px;
    padding: 8% 0 0 8%;
  }
  .hero__art img {
    width: 204%;
    max-width: none;
  }

  /* Waitlist form stacks: full-width field, then full-width button */
  .signup__field {
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
    height: auto;
    padding: 0;
    background: none;
    border: 0;
    border-radius: 0;
  }
  .signup__field:focus-within { box-shadow: none; }
  .signup__input {
    flex: none;
    height: 48px;
    padding: 0 20px;
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-full);
  }
  .signup__input:focus { border-color: var(--ring); }
  .signup.is-error .signup__input { border-color: color-mix(in srgb, var(--danger) 60%, transparent); }
  .signup__button {
    height: 48px;
    justify-content: center;
  }
  .signup.is-done .signup__message { min-height: 104px; }

  .policy { max-height: calc(100dvh - 32px); }
  .policy .policy__body { padding: 32px 24px; }
  .policy__body .legal__title { font-size: 28px; }
  .policy__close { top: 8px; right: 8px; }

  /* The footer's text and the theme switch don't fit on one row, so they stack, centred under
     the waitlist's centred form: the copyright, the email and the privacy policy, one to a line,
     then the switch, set apart and back on its pill so it reads as a control. The dots go, since
     nothing sits beside them. The lines are over 44px apart, so one link's tap area never
     reaches the next. */
  .footer__inner { flex-direction: column; gap: 26px; }
  .footer__inner > [aria-hidden] { display: none; }
  .theme-switch {
    margin: 0;
    padding: 4px;
    border: 1px solid var(--border);
  }

  /* One column, each tile as far from the next as the first is from the statement, so they
     read as four, not one list */
  .tiles {
    grid-template-columns: minmax(0, 1fr);
    gap: 56px;
    margin-top: 56px;
  }
  .apps__body { font-size: 16px; }
  /* Stacked, the agents sit as far apart as the first is from the copy, as the tiles do */
  .agents__pair {
    grid-template-columns: minmax(0, 1fr);
    gap: 48px;
  }

  /* 4:5 cards. Each video has a narrow version for them (script.js), with its window laid out to
     fit, whole, at the same size in every card. */
  .feature__art--tour video,
  .feature__art--session video { aspect-ratio: 4 / 5; }

  .photo { aspect-ratio: 3 / 5; }
  .photo__img { object-position: 62% center; }
  .photo__copy { padding-bottom: 32px; }
}
