:root {
  --teal: #00c3d0;
  --cyan: #00c0e8;
  --ink: #000000;
  --paper: #ffffff;
  --copy: clamp(0.85rem, 1.5vw, 1.05rem); /* body copy on the text screens */
  --mono: 'Necto Mono', 'Space Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace;
  --edge: clamp(14px, 3.2vw, 36px); /* viewport margin for the logo + tagline */
}

* { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  scroll-snap-type: y mandatory;
}

html, body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--mono);
}

body { overflow-x: hidden; }

/* Each screen is exactly one viewport; the arrow moves you between them. */
.screen {
  position: relative;
  width: 100%;
  height: 100vh;
  height: 100dvh;
  background: var(--paper);
  overflow: hidden;
  scroll-snap-align: start;
}

/* Square field — the whirlpool, scaled from the 300-wide Figma frame to span the
   whole page. Every measurement here comes from layout() in index.html as plain
   pixels: an earlier version did the scaling in CSS with
   `scale(calc(100vw / 300px))`, but length-divided-by-length is CSS Values 4, and
   browsers that don't support it drop the whole transform and leave the squares
   sitting unscaled in the corner. Keep the arithmetic in JS. */
.squares {
  position: absolute;
  left: 0;
  width: 100%;
  /* top and height are set by layout() */
}

.sq {
  position: absolute;
  background: var(--teal);
  /* left, top, width and height are set by layout(); transform is driven
     per-frame by the swirl */
}

.sq--cyan { background: var(--cyan); }

/* Site chrome — logo left, actions right, fixed to the viewport so it rides all
   four screens and carries onto login.html. Body-level for the same reason
   .cursor is: #hero clips its overflow. */
.chrome {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 10;            /* over the page, under .cursor's 20 */
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--edge);   /* the same viewport margin the tagline and arrow use */
  pointer-events: none;   /* the bar is invisible and full-width; it must not
                             swallow clicks or hover on the hero beneath it */
}

.chrome > * { pointer-events: auto; }

.chrome__actions {
  display: flex;
  align-items: center;
  gap: .6em;
}

/* Logo — omnipresent, and a link home from every page. */
.wordmark {
  color: inherit;
  text-decoration: none;
  font-size: clamp(0.85rem, 2.2vw, 1.9rem);
  line-height: 1;
}

/* Chrome buttons. The site is otherwise borderless and hard-edged, so these
   introduce its only radius: 6px, matching the rectangle convention the app in
   ../public uses (999px there is reserved for pills). */
.btn {
  padding: .5em .9em;
  border: 1px solid var(--ink);
  border-radius: 6px;
  background: none;
  color: var(--ink);
  text-decoration: none;
  font-family: var(--mono);
  font-size: clamp(0.75rem, 1.6vw, 1rem);
  line-height: 1;
  white-space: nowrap;    /* labels must not wrap at narrow widths */
  transition: background-color .25s ease, border-color .25s ease, color .25s ease,
              opacity .35s ease;
}

/* Hover fills with the lighter square blue. Black text, not white — the fill is
   light and the site is ink-on-paper throughout. :focus-visible rides along so
   the state is reachable from the keyboard, not mouse-only. */
.btn:hover,
.btn:focus-visible {
  background: var(--cyan);
  color: var(--ink);
  border-color: transparent;  /* the fill carries the state; transparent rather
                                 than border:0 so the box doesn't resize */
}

/* Screens that carry text rather than artwork: centred on both axes. */
.screen--text {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1.4em;
  padding: var(--edge);
  text-align: center;
}

/* Same scale as the hero headline, so the page reads as one voice. */
.screen__title {
  margin: 0;
  font-weight: 400;
  font-size: min(5.2vw, 8vh);
  line-height: 1.25;
}

/* Content screens carry real copy, so give them a safety valve: .screen is a
   fixed 100dvh with overflow: hidden, which was fine when each held one heading.
   A short or landscape viewport must not silently clip a paragraph. */
.screen--text { overflow-y: auto; }

/* --- why ------------------------------------------------------------------
   Laid out from a 300x200 Figma frame. The frame's font sizes are frame units,
   not pixels — scaling them literally would give ~48px body copy — so the frame
   is read for hierarchy and expressed in the site's clamp() idiom. Widths do
   carry over: the copy block is 183 of 300 and the art 89, which is why they sit
   side by side. */
.why__lead {
  margin: 0;
  max-width: 30ch;      /* wider than the frame's 93-of-300 block, because the
                           type is now heading-sized and needs the measure */
  font-size: clamp(1.4rem, 3.4vw, 2.75rem);  /* ~44px at 1440 */
  font-weight: 400;
  line-height: 1.25;    /* heading leading, not the body's 1.45 */
}

.why__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 2em;
}

.why__copy {
  flex: 1 1 22em;
  max-width: 46ch;
  text-align: left;
}

.why__copy p {
  margin: 0;
  /* The frame sets this a step below the lead (8 against 10); keep that gap. */
  font-size: calc(var(--copy) * 0.92);
  line-height: 1.55;
}

/* A real heading, but well under .screen__title: it anchors the column rather
   than competing with the screen titles on either side of this one. */
.why__title {
  margin: 0 0 .6em;
  font-weight: 400;
  font-size: clamp(1.1rem, 2.4vw, 1.8rem);
  line-height: 1.2;
}

/* The ratio comes from the img's width/height ATTRIBUTES, not aspect-ratio —
   attributes are how browsers have always derived intrinsic ratio, and newer CSS
   has been dropped silently by this user's browser before. The file is 518x400,
   so the 440 cap renders at ~1.2x native at its widest and ~1.5x at its usual
   size; pixelated stops a smooth upscale from muddying the dither.

   flex: 0 0 auto, not 0 1 auto — while it could shrink, the stated width was
   overridden by the row's own sizing and 30% came out as 198px. */
.why__art {
  flex: 0 0 auto;
  width: 37%;   /* measured: yields ~340px on a desktop viewport */
  min-width: 220px;
  max-width: 440px;
  height: auto;
  border-radius: 8px;
  image-rendering: pixelated;
}

/* Flex basis alone kept these side by side down to ~400px, which is too tight
   for a 46ch column next to the art. Stack them explicitly instead. */
@media (max-width: 720px) {
  .why__row {
    flex-direction: column;
    flex-wrap: nowrap;  /* with direction: column, `wrap` breaks into a second
                           column and undoes the stacking entirely */
  }

  /* .why__copy's `flex: 1 1 22em` is a width hint for the row layout. In a
     column, flex-basis is the HEIGHT, so it was pinning this block to a flat
     352px whatever the copy actually needed. Let it size to its content. */
  .why__copy { flex: 0 0 auto; }
  .why__art { width: 55%; min-width: 0; max-width: 320px; }
}

/* Short viewports — landscape phones — are height-starved, and this screen has
   the most to fit. Tighten the rhythm rather than let it clip. */
@media (max-height: 560px) {
  .screen--text { gap: .8em; }
  .why__row { gap: 1.2em; }
  .why__art { max-width: 260px; }
  .why__title { margin-bottom: .3em; }
}

/* Narrow phones. A 300-character paragraph at 320px wide runs to ~352px of
   text on its own, and the screen overflowed by 51px. Reclaim it from the
   rhythm rather than from the content: tighter gaps, tighter leading, slightly
   smaller art. 375x667 previously fit by exactly 0px, so this buys it headroom
   too. */
@media (max-width: 420px) {
  .screen--text { gap: .8em; }
  .why__row { gap: 1.2em; }
  .why__title { margin-bottom: .3em; }
  .why__copy p { line-height: 1.45; }
  .why__art { width: 48%; }
}

/* --- join now -------------------------------------------------------------
   The frame runs the steps together as one block of text. Breaking them into a
   real list is the point of the ask; counters keep the user's (1) (2) (3). */
.steps {
  counter-reset: step;
  list-style: none;
  margin: 0;
  padding: 0;
  max-width: 64ch;
  text-align: left;
  font-size: var(--copy);
  line-height: 1.5;
}

.steps li {
  counter-increment: step;
  display: grid;
  grid-template-columns: 2.8em 1fr;
  gap: .3em;
}

.steps li + li { margin-top: .85em; }

.steps li::before { content: "(" counter(step) ")"; }

/* The only inline link in the copy, and it is the one that starts the whole
   product, so it is underlined rather than left to the cursor to reveal. */
.steps__link {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}

.steps__link:hover,
.steps__link:focus-visible {
  background: var(--cyan);
}

/* Carrier disclosure under the login button. Small and grey: it is required
   on the page, not part of the pitch. */
.fineprint {
  margin: 0;
  max-width: 48ch;
  font-size: clamp(0.62rem, 1.1vw, 0.72rem);
  line-height: 1.6;
  opacity: .6;
  text-align: center;
}

.fineprint a { color: inherit; }

/* A page that is deliberately blank still has to fill the viewport in white. */
.page {
  min-height: 100vh;
  min-height: 100dvh;
}

/* Headline — centred in the page, both axes. */
.headline {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 2;
  margin: 0;
  width: min(92vw, 12em);
  text-align: center;
  font-weight: 400;
  font-size: min(5.2vw, 8vh);
  line-height: 1.25;
}

/* Typewriter. Both lines' height is reserved up front so the headline never
   shifts as characters come and go; the caret trails the last one typed. */
.type {
  display: block;
  white-space: pre-wrap; /* renders the \A in TYPE_TEXT as the line break */
  min-height: 2.5em;     /* 2 lines x 1.25 line-height */
}

.type::after {
  content: '';
  display: inline-block;
  width: 0.6em;
  height: 1em;
  margin-left: 0.06em;
  vertical-align: -0.12em;
  background: var(--ink);
  animation: caret-blink 1s steps(1, end) infinite;
}

@keyframes caret-blink {
  0%, 50%      { opacity: 1; }
  50.01%, 100% { opacity: 0; }
}

/* The animated text is aria-hidden, so carry the real headline for screen
   readers here. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Tagline — one word per line, stacked directly, bottom-left of the page. */
.tagline {
  position: absolute;
  left: var(--edge);
  bottom: var(--edge);
  z-index: 3;
  margin: 0;
  font-size: clamp(0.8rem, 2.1vw, 1.4rem);
  line-height: 1.35;
}

.tagline span { display: block; }

/* Arrow — a rotated -> that points down, bobs, and is a real click target.
   Fixed rather than pinned inside #hero: it rides every screen so there's
   always a cue that more is below, and it hides itself on the last one.

   The glyph is a separate span so the bob and the click nudge can own a
   transform without fighting the button's opacity. The button's reveal uses
   `backwards` fill, not `both`: backwards covers the pre-delay period, then
   hands opacity back to the declared value, which is what lets .is-hidden
   transition it away later. `both` would hold opacity forever and win. */
.arrow {
  position: fixed;
  right: var(--edge);
  bottom: var(--edge);
  z-index: 10;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  font-family: var(--mono);
  font-size: clamp(1.4rem, 3.5vw, 2.6rem);
  line-height: 1;
  cursor: pointer;
  opacity: 1;
  transition: opacity .35s ease;
  animation: fade-in .55s ease .75s backwards;
}

.arrow__glyph {
  display: inline-block;
  /* Rotated 90deg, the arrow's local +X points down the screen. */
  transform: rotate(90deg);
  animation: arrow-bob 1.4s ease-in-out infinite;
}

@keyframes arrow-bob {
  0%, 100% { transform: rotate(90deg) translateX(0); }
  50%      { transform: rotate(90deg) translateX(6px); }
}

/* Click: a nudge in the direction of travel, then back. It replaces the bob
   for its duration rather than layering on it — both drive transform. */
.arrow--nudge .arrow__glyph {
  animation: arrow-nudge .45s ease-out;
}

@keyframes arrow-nudge {
  0%, 100% { transform: rotate(90deg) translateX(0); }
  45%      { transform: rotate(90deg) translateX(20px); }
}

/* Nothing below: the cue would be a lie. Also used for "learn more" once you
   leave the hero. */
.arrow.is-hidden,
.chrome__actions .btn.is-hidden {
  opacity: 0;
  pointer-events: none;
}

/* Custom cursor. A real cursor can't be animated — cursor: url() takes a static
   image only — so the native one is switched off site-wide and this trails the
   pointer instead. Fixed, and a body-level child, so #hero's overflow:hidden
   can't clip it and it carries onto the second screen. Never a hit target. */
.cursor {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 20;
  display: none;
  pointer-events: none;
}

/* Set by main.js on the first real mousemove, so a touch-only device is never
   left without a cursor. The `*` is needed because .arrow sets cursor: pointer
   and would otherwise win over a bare html rule. */
.has-cursor,
.has-cursor * { cursor: none; }

.cursor__dot {
  position: absolute;
  left: 0;
  top: 0;
  background: var(--ink);
  /* size and transform are set per-frame */
}

/* Load-in reveal: each element fades up in turn — wordmark, tagline, arrow,
   headline. `both` fill holds them at opacity 0 before their delay, so there's
   no flash of fully-styled text on a slow load.

   .arrow and .headline animate opacity ONLY: both already own a transform
   (rotate(90deg) and translate(-50%,-50%)), which a translateY here would
   clobber. They get their motion from the bob and the typewriter instead. */
@keyframes rise-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

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

.wordmark { animation: rise-in .55s ease .15s both; }
.chrome__actions .btn { animation: rise-in .55s ease .28s backwards; }
.tagline  { animation: rise-in .55s ease .45s both; }
/* .arrow's fade is declared with its bob, above. */
.headline { animation: fade-in .55s ease 1.05s both; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .arrow,
  .arrow__glyph,
  .arrow--nudge .arrow__glyph { animation: none; }
  .type::after { animation: none; opacity: 0; }
  .cursor { display: none !important; }
  .wordmark,
  .tagline,
  .btn,
  .headline { animation: none; opacity: 1; }
}

/* ------------------------------------------------------------- doc pages */

/* Privacy and anything else that is a page of prose rather than a screen.
   Nothing is needed to disable the landing page's scroll snapping here:
   `scroll-snap-type` on html only does anything when the page contains
   elements with `scroll-snap-align`, and only `.screen` carries that. Doing
   it with `html:has(.doc-page)` would be the obvious move and is exactly the
   kind of newer selector this project avoids. */
.doc {
  max-width: 68ch;
  margin: 0 auto;
  padding-inline: var(--edge);
  padding-block: calc(var(--edge) * 4) calc(var(--edge) * 3);
  font-size: var(--copy);
  line-height: 1.65;
}

.doc h1 {
  font-size: clamp(1.4rem, 4vw, 2rem);
  letter-spacing: -0.02em;
  margin: 0 0 .2em;
}

.doc h2 {
  font-size: clamp(1rem, 2.2vw, 1.2rem);
  margin: 2em 0 .5em;
}

.doc__meta {
  margin: 0 0 2em;
  opacity: .55;
  font-size: .8em;
}

.doc ul {
  padding-left: 1.2em;
}

.doc li + li { margin-top: .5em; }

.doc a {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.doc__back {
  margin-top: 3em;
  opacity: .6;
}
