/* Resona, on the web.
 *
 * The tokens are lifted from the app's own Palette.swift rather than invented here, because
 * the landing page and the product have to be recognisably the same object: warm paper, warm
 * near-black ink, two pigments, and a slanted serif for anything a person would have written
 * by hand.
 *
 * Type is deliberately not a downloaded webfont. The app's headings are the system serif in
 * italic, which on every iPhone and Mac is New York, so asking for New York here is closer to
 * the product than any font we could ship - and it costs no request, so the first paint is the
 * page rather than a flash of something else.
 *
 * One accent (sage). One radius scale (12 / 18 / 26). One theme at a time, light or dark, all
 * the way down the page.
 */

:root {
  color-scheme: light dark;

  --paper: #faf8f5;
  --raised: #f2efea;
  --deep: #e9e5df;
  --ink: #1c1a17;
  --dim: rgb(28 26 23 / .64);
  --faint: rgb(28 26 23 / .40);
  --rule: rgb(28 26 23 / .13);
  --rule-faint: rgb(28 26 23 / .07);

  --accent: #5f8f7d;          /* sage, darkened until it passes AA on paper */
  --accent-soft: #a2c1b7;
  --terracotta: #efb39a;

  --on-ink: #faf8f5;
  --shadow: 0 24px 60px rgb(28 26 23 / .10);

  --r-sm: 12px;
  --r-md: 18px;
  --r-lg: 26px;

  --serif: ui-serif, "New York", "Iowan Old Style", Georgia, "Times New Roman", serif;
  --sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, system-ui, sans-serif;

  --gutter: clamp(20px, 5vw, 44px);
  --measure: 62ch;
}

@media (prefers-color-scheme: dark) {
  :root {
    --paper: #14130f;
    --raised: #1c1a16;
    --deep: #23201b;
    --ink: #f2efe8;
    --dim: rgb(242 239 232 / .68);
    --faint: rgb(242 239 232 / .44);
    --rule: rgb(242 239 232 / .16);
    --rule-faint: rgb(242 239 232 / .09);

    --accent: #9ec5b4;
    --accent-soft: #6d9484;
    --on-ink: #14130f;
    --shadow: 0 24px 60px rgb(0 0 0 / .45);
  }
}

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

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* The paper's topography, the same idea as the app's shader: a very quiet contour under
 * everything. Fixed and pointer-events-none so it never repaints on scroll. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: .5;
  background-image:
    radial-gradient(ellipse 80% 55% at 12% -10%, rgb(162 193 183 / .20), transparent 60%),
    radial-gradient(ellipse 70% 50% at 92% 8%, rgb(239 179 154 / .17), transparent 62%);
}
@media (prefers-color-scheme: dark) {
  body::before { opacity: .34; }
}

main, header, footer, nav { position: relative; z-index: 1; }

img, video { max-width: 100%; height: auto; display: block; }

a { color: inherit; }

::selection { background: var(--accent-soft); color: var(--ink); }

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

/* Type ------------------------------------------------------------------- */

h1, h2, h3 {
  font-family: var(--serif);
  font-style: italic;
  font-weight: 500;
  letter-spacing: -.015em;
  /* Italic descenders in y, g, p need the room; leading-none clips them. */
  line-height: 1.12;
  margin: 0;
}

h1 { font-size: clamp(2.35rem, 5.4vw, 4rem); padding-bottom: .06em; }
h2 { font-size: clamp(1.85rem, 3.6vw, 2.8rem); padding-bottom: .06em; }
h3 { font-size: 1.25rem; padding-bottom: .04em; }

p { margin: 0; max-width: var(--measure); }

.lede { font-size: clamp(1.05rem, 1.6vw, 1.24rem); color: var(--dim); }
.small { font-size: .875rem; color: var(--dim); }
.fine { font-size: .78rem; color: var(--faint); }

strong { font-weight: 600; }

/* Layout ----------------------------------------------------------------- */

.shell { width: min(1180px, 100%); margin-inline: auto; padding-inline: var(--gutter); }

section { padding-block: clamp(56px, 6.5vw, 96px); }

.rule { border: 0; border-top: 1px solid var(--rule); margin: 0; }

/* Navigation ------------------------------------------------------------- */

.nav {
  height: 68px;
  display: flex;
  align-items: center;
  gap: 28px;
  border-bottom: 1px solid var(--rule-faint);
}

.brand {
  display: flex;
  align-items: center;
  padding-block: 6px;
  gap: 10px;
  font-family: var(--serif);
  font-style: italic;
  font-size: 1.32rem;
  text-decoration: none;
  margin-right: auto;
}
.brand img { width: 28px; height: 28px; border-radius: 7px; }

.nav-links { display: flex; gap: 26px; }
.nav-links a {
  font-size: .93rem;
  color: var(--dim);
  text-decoration: none;
  padding-block: 10px;
  transition: color .18s ease;
}
.nav-links a:hover { color: var(--ink); }

@media (max-width: 760px) {
  .nav-links { display: none; }
  .nav .btn { padding: 10px 16px; font-size: .88rem; }
}
/* The bar is sticky, so this button is the whole conversion path on a phone and is not
 * hidden. It is shortened instead: "Get it on the App Store" plus the wordmark does not
 * fit 390px without the two touching. */
.nav .btn .short { display: none; }
@media (max-width: 560px) {
  .nav .btn .full { display: none; }
  .nav .btn .short { display: inline; }
}

/* Buttons ---------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  white-space: nowrap;
  padding: 15px 26px;
  border-radius: var(--r-sm);
  font-size: 1rem;
  font-weight: 600;
  text-decoration: none;
  border: 1px solid transparent;
  transition: transform .18s cubic-bezier(.16, 1, .3, 1), background-color .18s ease, border-color .18s ease;
}

.btn-primary { background: var(--ink); color: var(--on-ink); }
.btn-primary:hover { transform: translateY(-1px); }
.btn-primary:active { transform: translateY(1px); }

.btn-quiet { color: var(--ink); border-color: var(--rule); }
.btn-quiet:hover { border-color: var(--ink); transform: translateY(-1px); }
.btn-quiet:active { transform: translateY(1px); }

@media (prefers-reduced-motion: reduce) {
  .btn { transition: none; }
  .btn:hover, .btn:active { transform: none; }
}

.btn svg { width: 17px; height: 17px; flex: none; }

/* Hero ------------------------------------------------------------------- */

.hero {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr);
  gap: clamp(32px, 6vw, 88px);
  align-items: center;
  padding-block: clamp(48px, 7vw, 92px) clamp(56px, 8vw, 104px);
}

.hero-copy { display: grid; gap: 26px; justify-items: start; }
.hero-copy .lede { max-width: 34ch; }
.hero-actions { display: flex; flex-wrap: wrap; gap: 12px; }
/* Once they stack they are two different lengths, which reads as a mistake rather
 * than a hierarchy. Below 560 they match. */
@media (max-width: 560px) {
  .hero-actions { display: grid; grid-template-columns: 1fr; }
  .hero-actions .btn { justify-content: center; }
}

/* One column: the claim first, then the proof. Putting the phone above the headline is the
 * tempting arrangement and it pushes the one sentence this page exists for below the fold. */
@media (max-width: 900px) {
  .hero { grid-template-columns: 1fr; gap: 40px; }
  .hero-figure { justify-content: flex-start; }
  .hero-figure .screen { width: min(300px, 68vw); }
  .hero-copy .lede { max-width: 40ch; }
}

/* Screens ---------------------------------------------------------------- */

.screen {
  border-radius: var(--r-lg);
  border: 1px solid var(--rule);
  box-shadow: var(--shadow);
  background: var(--raised);
  overflow: hidden;
  width: min(340px, 78vw);
}
.screen video, .screen img { width: 100%; display: block; }

.hero-figure { display: flex; justify-content: center; }
.hero-figure .screen { width: min(360px, 82vw); }

figure { margin: 0; }
figcaption { margin-top: 14px; font-size: .82rem; color: var(--faint); text-align: center; }

/* Facts strip ------------------------------------------------------------ */

.facts {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 4px 40px;
  padding-block: 28px;
  border-block: 1px solid var(--rule);
}
.facts p { font-size: .95rem; color: var(--dim); max-width: 28ch; }
.facts b { color: var(--ink); font-weight: 600; }

@media (max-width: 760px) {
  .facts { grid-template-columns: 1fr; gap: 14px; }
}

/* Steps ------------------------------------------------------------------ */

.split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, .82fr);
  gap: clamp(32px, 6vw, 80px);
  align-items: center;
}
.split.flip { grid-template-columns: minmax(0, .82fr) minmax(0, 1fr); }
.split.flip .split-media { order: -1; }
.split-media { display: flex; justify-content: center; }

@media (max-width: 900px) {
  .split, .split.flip { grid-template-columns: 1fr; }
  .split-media, .split.flip .split-media { order: 0; }
}

.steps { display: grid; gap: 0; margin-top: 34px; }
.step { display: grid; gap: 5px; padding-block: 22px; border-top: 1px solid var(--rule-faint); }
.step:last-child { border-bottom: 1px solid var(--rule-faint); }
.step h3 { font-style: normal; font-family: var(--sans); font-size: 1.02rem; font-weight: 650; letter-spacing: 0; }
.step p { font-size: .96rem; color: var(--dim); }

/* Centred media feature -------------------------------------------------- */

.feature { display: grid; justify-items: center; text-align: center; gap: 18px; }
.feature p { text-align: center; }
.feature .screen { margin-top: 18px; }

.bands {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin-top: 4px;
}
.band {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 14px;
  border-radius: var(--r-sm);
  border: 1px solid var(--rule);
  font-size: .84rem;
  color: var(--dim);
}
.band i { width: 9px; height: 9px; border-radius: 50%; display: block; }

/* Tinted band ------------------------------------------------------------ */

.tinted { background: var(--raised); border-block: 1px solid var(--rule); }

/* Price ------------------------------------------------------------------ */

.plans {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(20px, 4vw, 48px);
  margin-top: 40px;
  align-items: start;
}
@media (max-width: 760px) { .plans { grid-template-columns: 1fr; } }

.plan { display: grid; gap: 16px; padding: 30px; border-radius: var(--r-md); }
.plan-free { border: 1px solid var(--rule); }
.plan-plus { background: var(--ink); color: var(--on-ink); }

.plan ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 9px; }
.plan li { font-size: .95rem; color: var(--dim); padding-left: 20px; position: relative; }
.plan li::before {
  content: "";
  position: absolute;
  left: 2px; top: .62em;
  width: 7px; height: 2px;
  background: currentColor;
  opacity: .45;
}
.price { font-family: var(--serif); font-size: 2.1rem; line-height: 1.1; }
.price small { font-family: var(--sans); font-style: normal; font-size: .95rem; opacity: .7; }
.note { font-size: .84rem; color: var(--faint); }

/* After `.plan li`, not before it. These carry the same specificity, so declared any earlier
 * the generic rule wins and the paid card's list is ink on ink: invisible, and the one card
 * on the page that has to be legible. */
.plan-plus h3, .plan-plus .price { color: var(--on-ink); }
.plan-plus li { color: rgb(250 248 245 / .78); }
.plan-plus .note { color: rgb(250 248 245 / .60); }
@media (prefers-color-scheme: dark) {
  .plan-plus li { color: rgb(20 19 15 / .78); }
  .plan-plus .note { color: rgb(20 19 15 / .62); }
}

/* FAQ -------------------------------------------------------------------- */

/* Two columns from 900px up: the heading holds the left, the questions the right.
 * A single 780px column inside a 1092px shell left a third of the section as bare
 * paper with the word "Questions" floating in it. */
#faq { display: grid; gap: 10px 56px; align-items: start; }
@media (min-width: 900px) {
  #faq { grid-template-columns: minmax(0, 22ch) minmax(0, 1fr); }
  #faq > h2 { position: sticky; top: 100px; }
}
.faq { margin-top: 0; max-width: 720px; }
.faq details { border-top: 1px solid var(--rule-faint); }
.faq details:last-of-type { border-bottom: 1px solid var(--rule-faint); }
.faq summary {
  cursor: pointer;
  list-style: none;
  padding: 20px 40px 20px 0;
  font-size: 1.02rem;
  font-weight: 600;
  position: relative;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: "";
  position: absolute;
  right: 8px; top: 50%;
  width: 11px; height: 11px;
  margin-top: -7px;
  border-right: 1.6px solid var(--faint);
  border-bottom: 1.6px solid var(--faint);
  transform: rotate(45deg);
  transition: transform .2s ease;
}
.faq details[open] summary::after { transform: rotate(-135deg); margin-top: -3px; }
.faq p { padding-bottom: 22px; color: var(--dim); font-size: .98rem; }
@media (prefers-reduced-motion: reduce) { .faq summary::after { transition: none; } }

/* Closing ---------------------------------------------------------------- */

.closing { display: grid; justify-items: center; text-align: center; gap: 24px; }
.closing p { text-align: center; }

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

footer { border-top: 1px solid var(--rule); padding-block: 40px 56px; }
.foot { display: flex; flex-wrap: wrap; gap: 16px 28px; align-items: baseline; }
.foot a {
  color: var(--dim);
  text-decoration: none;
  font-size: .92rem;
  /* 24px of text in a 44px box: the row keeps its rhythm, the thumb gets a target. */
  padding-block: 10px;
  display: inline-block;
}
.foot a:hover { color: var(--ink); text-decoration: underline; }
.foot .fine { margin-left: auto; }
.disclaimer { margin-top: 26px; max-width: 78ch; }

/* There is deliberately no fade-in-on-scroll here.
 *
 * It was built, and then taken out. Every section began at opacity 0 and waited for an
 * observer to notice it, which meant that with the script slow, blocked or simply not run -
 * the state a crawler is often in - the page below the fold was blank. The motion on this
 * page is the two product loops, the buttons under a cursor, and the questions opening. That
 * is enough for something whose subject is settling down.
 */

/* Legal pages ------------------------------------------------------------ */

.doc { width: min(760px, 100%); margin-inline: auto; padding-block: 56px 96px; }
.doc h1 { margin-bottom: 8px; }
.doc h2 { font-size: 1.5rem; margin-top: 44px; margin-bottom: 10px; }
.doc p, .doc li { color: var(--dim); font-size: 1rem; }
.doc ul { padding-left: 20px; display: grid; gap: 8px; }
.doc a { color: var(--accent); }
.doc .updated { color: var(--faint); font-size: .85rem; }

/* ==========================================================================
   Motion
   ==========================================================================

   The page moves now, and it moves on the scrollbar rather than on a timer.
   Everything below is CSS scroll-driven animation: no library, no scroll
   listener, no JavaScript holding content hostage. The reveal keyframes live
   inside `@supports (animation-timeline: view())`, so a browser that cannot
   run them never applies the opacity-0 start and simply shows the page. That
   is not a nicety - the first version of this site hid half its sections
   behind an observer and shipped them invisible.

   One rule this page will not break, whatever the brief asks for: **nothing
   here strobes.** Resona is a brainwave-entrainment app whose own shader
   rules forbid luminance change in the 3-30 Hz band, and a landing page that
   flickers at its own product's hazard frequency is not a landing page for
   this product. Every animation below moves transform and opacity, slowly.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {

    /* The base reveal: rise and resolve as the block enters. */
    /* Finishes on *entry*, never on cover.
     *
     * A range that ends at `cover 32%` needs the element to travel a third of the way up the
     * viewport before it is fully opaque, and the last section on the page can never do that:
     * the document runs out of scroll first, so it sits at forty per cent opacity for good.
     * Ending inside `entry` means anything fully on screen is finished, whatever the height
     * of the screen or the length of the page. */
    .rise {
      animation: rise-in linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 88%;
    }
    @keyframes rise-in {
      from { opacity: 0; transform: translateY(38px); }
      to   { opacity: 1; transform: none; }
    }

    /* Staggered children, driven by an index the markup sets. */
    .rise-group > * {
      animation: rise-in linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 88%;
      animation-delay: calc(var(--i, 0) * -0.06s);
    }

    /* The hero phone drifts up and settles as you leave the fold. */
    .drift {
      animation: drift-up linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 90%;
    }
    @keyframes drift-up {
      from { transform: translateY(4%) scale(1.015); }
      to   { transform: translateY(-9%) scale(1); }
    }

    /* The contour ground moves at a different rate to everything on it. */
    .parallax-ground {
      animation: ground-shift linear both;
      animation-timeline: scroll(root);
    }
    @keyframes ground-shift {
      from { transform: translate3d(0, -2%, 0) scale(1.06); }
      to   { transform: translate3d(0, 6%, 0) scale(1.06); }
    }

    /* The sixty words, pulled sideways by the page rather than by a clock, so
       it is never moving when the reader is not. */
    .wordband-track {
      animation: pull-left linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }
    @keyframes pull-left {
      from { transform: translateX(2%); }
      to   { transform: translateX(-32%); }
    }

    /* The five bands, panned horizontally by vertical scroll. */
    .pan-track {
      animation: pan-across linear both;
      animation-timeline: view(block);
      /* Nothing moves until the strip is fully in the viewport. */
      animation-range: contain 0% contain 100%;
    }
    /* Starts at zero, never at a positive offset.
     *
     * The strip used to begin at +3% and end at -30%, which meant that by the time the
     * section was properly on screen Delta was already half off the left edge - the first
     * card of five, clipped, before the reader had done anything. It now begins flush and
     * only ever travels left, so the section arrives complete and the pan is something the
     * reader causes rather than something that has already happened to them. */
    @keyframes pan-across {
      from { transform: translateX(0); }
      to   { transform: translateX(-26%); }
    }

    /* The navigation condenses once the hero is behind you. */
    .nav-shell {
      animation: nav-settle linear both;
      animation-timeline: scroll(root);
      animation-range: 0 220px;
    }
    @keyframes nav-settle {
      from { background: transparent; border-color: transparent; backdrop-filter: blur(0px); }
      to   { background: var(--nav-veil); border-color: var(--rule-faint); backdrop-filter: blur(14px); }
    }
  }
}

/* Sticky stack ------------------------------------------------------------
   Plain `position: sticky`, which needs neither a library nor a browser that
   has scroll-driven animation. Each card pins at the top and the next one
   arrives over it. */

.stack { display: grid; gap: 0; }
.stack-card {
  position: sticky;
  top: 88px;
  border-radius: var(--r-lg);
  border: 1px solid var(--rule);
  background: var(--raised);
  padding: clamp(24px, 3vw, 40px);
  margin-bottom: 22px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, .62fr);
  gap: clamp(24px, 4vw, 48px);
  align-items: center;
  box-shadow: var(--shadow);
}
.stack-card:last-child { margin-bottom: 0; }
/* The device is cropped to its top, not shown whole.
 *
 * A phone at its real aspect is 2.17 times as tall as it is wide, which forced every card to
 * seven hundred pixels and left the three lines of copy floating in the middle of a void. The
 * informative half of any of these screens is the top half, so that is the half that shows. */
.stack-card .screen {
  width: min(260px, 66vw);
  height: 340px;
  box-shadow: none;
}
.stack-card .screen img,
.stack-card .screen video {
  height: 100%;
  object-fit: cover;
  object-position: top center;
  /* The cut lands wherever the screen's own layout puts it - on the profile card it fell
   * through the middle of "WHAT RESONA FOUND", which reads as a half-loaded image rather than
   * as a crop. Fading the last fifth turns the edge into a horizon. */
  -webkit-mask-image: linear-gradient(180deg, #000 78%, transparent 100%);
  mask-image: linear-gradient(180deg, #000 78%, transparent 100%);
}
.stack-card h3 { font-size: clamp(1.5rem, 2.8vw, 2.1rem); }
.stack-card p { margin-top: 14px; color: var(--dim); }
.stack-index {
  font-family: var(--sans);
  font-size: .75rem;
  font-weight: 700;
  letter-spacing: .14em;
  color: var(--accent);
}

@media (max-width: 860px) {
  .stack-card { grid-template-columns: 1fr; position: static; }
  .stack-card .split-media { order: -1; }
}

/* The ground ---------------------------------------------------------------
   One fixed layer, pointer-events off, so it never repaints on scroll. */

.ground {
  position: fixed;
  inset: -8% -4%;
  z-index: 0;
  pointer-events: none;
  background: url("img/contours.svg") center / cover no-repeat;
  opacity: .5;
}
@media (prefers-color-scheme: dark) {
  .ground { opacity: .16; filter: invert(1); }
}

/* The word band -----------------------------------------------------------*/

.wordband {
  overflow: hidden;
  border-block: 1px solid var(--rule);
  padding-block: 30px;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.wordband-track {
  display: flex;
  gap: 30px;
  width: max-content;
  will-change: transform;
}
.wordband span {
  font-family: var(--serif);
  font-style: italic;
  font-size: clamp(1.4rem, 3vw, 2.3rem);
  color: var(--faint);
  white-space: nowrap;
}
.wordband span:nth-child(4n) { color: var(--accent); }

/* The band strip ----------------------------------------------------------*/

.bandgrid {
  display: grid;
  align-items: stretch;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 14px;
  margin-top: 32px;
}
@media (max-width: 1000px) { .bandgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px)  { .bandgrid { grid-template-columns: 1fr; } }

.pan-card {
  border-radius: var(--r-md);
  border: 1px solid var(--rule);
  padding: 20px;
  background: var(--paper);
  display: flex;
  flex-direction: column;
}
.pan-card .hz { font-family: var(--serif); font-size: 1.6rem; line-height: 1; }
.pan-card h3 { font-family: var(--sans); font-style: normal; font-size: 1rem; font-weight: 650; margin-top: 12px; }
.pan-card p { font-size: .92rem; color: var(--dim); margin-top: 6px; }
/* Pushed to the floor, so five cards of different copy lengths still line up. */
.pan-card .bar { height: 3px; border-radius: 2px; margin-top: auto; padding-top: 0; }
.pan-card p:last-of-type { margin-bottom: 18px; }

/* Spotlight ---------------------------------------------------------------
   A pointer-tracked highlight, set from two custom properties so the hover
   costs one composited paint rather than a React render. */

.spot { position: relative; isolation: isolate; overflow: hidden; }
.spot::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  opacity: 0;
  transition: opacity .35s ease;
  background: radial-gradient(
    260px circle at var(--mx, 50%) var(--my, 50%),
    color-mix(in srgb, var(--accent) 22%, transparent),
    transparent 68%
  );
}
.spot:hover::after { opacity: 1; }
@media (hover: none) { .spot::after { display: none; } }

/* The count-up ------------------------------------------------------------*/

.figure-big {
  font-family: var(--serif);
  font-size: clamp(3.4rem, 10vw, 7rem);
  line-height: 1;
  font-style: italic;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

/* Nav veil ----------------------------------------------------------------*/

:root { --nav-veil: rgb(250 248 245 / .82); }
@media (prefers-color-scheme: dark) { :root { --nav-veil: rgb(20 19 15 / .78); } }

.nav-shell {
  position: sticky;
  top: 0;
  z-index: 40;
  border-bottom: 1px solid transparent;
}

/* The eight, as chips on the chart section. */
.chip {
  display: inline-flex;
  align-items: center;
  padding: 7px 14px;
  border-radius: var(--r-sm);
  border: 1px solid var(--rule);
  font-family: var(--serif);
  font-style: italic;
  font-size: .95rem;
  color: var(--dim);
}

/* The plate ---------------------------------------------------------------
   One full-bleed printed sheet behind the page's single boldest claim.
   
   This is where the generated risograph goes, and it is the only place it
   goes: the rest of the page is drawn from the app itself, so one imported
   material reads as a deliberate change of surface rather than as texture
   sprinkled about. A scrim sits between the image and the type so the ink
   stays at AA whatever the image does underneath, and the image is masked to
   nothing at its edges so the plate arrives and leaves rather than being cut
   off by a hard line. */

.plate {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  border-block: 1px solid var(--rule);
}
.plate-art {
  position: absolute;
  inset: -12% 0 -12% 0;
  z-index: -2;
  background: center / cover no-repeat;
  opacity: .55;
}
.plate::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(
    180deg,
    var(--paper) 0%,
    color-mix(in srgb, var(--paper) 62%, transparent) 26%,
    color-mix(in srgb, var(--paper) 62%, transparent) 74%,
    var(--paper) 100%
  );
}
@media (prefers-color-scheme: dark) {
  .plate-art { opacity: .3; }
}

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .plate-art {
      animation: plate-drift linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }
    @keyframes plate-drift {
      from { transform: translateY(-5%) scale(1.04); }
      to   { transform: translateY(5%) scale(1.04); }
    }
  }
}

/* Hero, third cut --------------------------------------------------------
   Two columns, both complete, both centred.

   The first version was text left, screenshot right at full aspect. The second
   moved the copy low-left over the paper and let the device bleed off the right
   edge, which read well in a comp and cut straight through "+63%" and "Save
   this" on a real window. Capping the crop only moved the cut onto the button.

   So: the device is never cropped and never bled. It is sized from its own
   aspect ratio so it is always whole, the two columns are centred against each
   other, and the section is as tall as its contents rather than a fixed share
   of the viewport - which is what was leaving two hundred pixels of dead paper
   above the fold on a short window.
   ------------------------------------------------------------------------ */

.hero-canvas {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: clamp(32px, 5vw, 72px);
  align-items: center;
  padding-block: clamp(48px, 6vw, 88px) clamp(56px, 7vw, 100px);
  border-bottom: 1px solid var(--rule);
}
.hero-canvas > .shell { width: 100%; padding-inline: 0; }

.hero-lead {
  display: grid;
  gap: 22px;
  justify-items: start;
  /* No max-width in ch here, and none on the headline either. The grid's 1fr already
     decides this column, and ch is the width of the font's zero - a useless ruler for an
     italic display serif. Capping it that way put the headline on six lines at 1000px. */
}
.hero-lead .lede { max-width: 42ch; }

.hero-phone {
  aspect-ratio: 720 / 1565;
  height: min(560px, 66vh);
  border-radius: var(--r-lg);
  border: 1px solid var(--rule);
  box-shadow: var(--shadow);
  overflow: hidden;
  background: var(--raised);
}
.hero-phone img { width: 100%; height: 100%; object-fit: contain; display: block; }

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .hero-phone {
      animation: hero-lift linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }
    @keyframes hero-lift {
      from { transform: translateY(2%); }
      to   { transform: translateY(-6%); }
    }
  }
}

@media (max-width: 900px) {
  .hero-canvas { grid-template-columns: 1fr; gap: 36px; }
  .hero-lead { max-width: none; }
  .hero-lead h1, .hero-lead .lede { max-width: none; }
  .hero-phone { height: auto; aspect-ratio: auto; width: min(300px, 68vw); }
  .hero-phone img { height: auto; object-fit: fill; }
}

/* The live chart -----------------------------------------------------------
   Not a picture of the app's chart: the same layout code, running here. */

.demo {
  position: relative;
  border-radius: var(--r-lg);
  border: 1px solid var(--rule);
  background: var(--paper);
  overflow: hidden;
  box-shadow: var(--shadow);
}
.demo canvas {
  display: block;
  width: 100%;
  height: clamp(400px, 56vh, 620px);
  touch-action: none;          /* the canvas owns drag and pinch */
  cursor: grab;
}
.demo canvas:active { cursor: grabbing; }

.demo-bar {
  display: flex;
  align-items: center;
  gap: var(--gutter);
  padding: 14px 20px;
  border-top: 1px solid var(--rule-faint);
  background: var(--raised);
}
.demo-bar p { font-size: .86rem; color: var(--dim); margin: 0; max-width: none; }
.demo-bar button {
  margin-left: auto;
  font: inherit;
  font-size: .82rem;
  color: var(--ink);
  background: none;
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  padding: 7px 14px;
  cursor: pointer;
}
.demo-bar button:hover { border-color: var(--ink); }

.demo-note {
  position: absolute;
  top: 14px; left: 16px;
  font-size: .72rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--faint);
  pointer-events: none;
}

/* A pair: the live chart, and the same thing on a phone. */
.chart-pair {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 300px);
  gap: clamp(24px, 4vw, 48px);
  /* Centred, not top-aligned. The word-picker card is about half the height of the phone
   * beside it, and starting them both at the top left a rectangle of nothing under the card
   * that read as a missing element rather than as space. */
  align-items: center;
  margin-top: 34px;
}
/* Both columns end where the other one does. A phone at its full aspect is 650px tall next
   to a 490px card, which leaves one column hanging past the other and past the section. */
.chart-pair .screen { height: clamp(360px, 46vh, 500px); }
.chart-pair .screen img,
.chart-pair .screen video {
  height: 100%;
  object-fit: cover;
  object-position: top center;
  -webkit-mask-image: linear-gradient(180deg, #000 78%, transparent 100%);
  mask-image: linear-gradient(180deg, #000 78%, transparent 100%);
}

@media (max-width: 900px) {
  .chart-pair { grid-template-columns: 1fr; }
  .chart-pair .screen { width: min(280px, 70vw); height: auto; margin-inline: auto; }
  .chart-pair .screen img, .chart-pair .screen video { height: auto; object-fit: fill; }
}

/* Recreated app fragments --------------------------------------------------
   Real markup, not a screenshot of markup: these are the app's own components
   rebuilt in HTML so they stay sharp, selectable and legible at any size. */

.uiframe {
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  background: var(--paper);
  padding: 22px 24px;
}
.uiframe-label {
  font-size: .7rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--faint);
  margin-bottom: 16px;
}

.statrow { display: flex; gap: clamp(24px, 5vw, 56px); }
.statrow div { display: grid; gap: 3px; }
.statrow b {
  font-family: var(--serif);
  font-size: clamp(1.7rem, 3.4vw, 2.4rem);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.statrow span {
  font-size: .68rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--faint);
}
.statrow .lit { color: var(--accent); }

.wordpick { display: flex; flex-wrap: wrap; gap: 8px; }
.wordpick span {
  font-family: var(--serif);
  font-style: italic;
  font-size: .98rem;
  color: var(--dim);
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  padding: 6px 12px;
}
.wordpick span.picked {
  background: var(--ink);
  color: var(--on-ink);
  border-color: var(--ink);
}

/* Contact form -----------------------------------------------------------
   Netlify Forms: the markup is the whole back end, so the only thing here is
   making the fields look like the rest of the page rather than like a browser
   default. */

.contact { display: grid; gap: 8px; margin-top: 22px; max-width: 520px; }
.contact label {
  font-size: .82rem;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--faint);
  margin-top: 12px;
}
.contact input[type="text"],
.contact input[type="email"],
.contact textarea {
  font: inherit;
  font-size: 1rem;
  color: var(--ink);
  background: var(--raised);
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  padding: 12px 14px;
  width: 100%;
}
.contact textarea { resize: vertical; min-height: 130px; }
.contact input::placeholder { color: var(--faint); }
.contact input:focus-visible,
.contact textarea:focus-visible { border-color: var(--accent); outline-offset: 1px; }
.contact button { margin-top: 20px; justify-self: start; }

/* The honeypot. Off-screen rather than display:none, because some bots skip
   anything a browser would not render at all. */
.contact .hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
