/* Homepage app variant; the atmosphere and resin animation stay shared. */
.hero--app { text-align: left; padding-block: 54px 100px; }
.hero--app .shell { grid-template-columns: 1.15fr 1fr; align-items: center; gap: clamp(32px, 5vw, 76px); }
.hero__copy { min-width: 0; }
.hero--app .wordmark--hero { width: 148px; margin: 0 0 20px -18px; }
.hero--app h1 { font-size: clamp(2.6rem, 4.5vw, 4.1rem); line-height: 1.08; letter-spacing: -.045em; text-wrap: initial; margin-bottom: 26px; }
.hero--app .lede { margin: 0; max-width: 40ch; font-size: 18px; }
.hero--app .hero__actions { justify-content: flex-start; margin-top: 30px; }
.hero--app .fineprint { font-size: 12px; }
.hero__preview { position: relative; width: 100%; margin: 26px 0 0; padding: 0 22px; }
/* ── the device ────────────────────────────────────────────────
   The frame is a photograph of the real hardware (iPhone 16 Pro),
   not a box drawn in CSS: the titanium rim, the button cuts and the
   Dynamic Island are all in the PNG, and its interior is transparent.
   The screenshot sits UNDER it, inset to the exact interior the rim
   leaves — measured off the asset's own alpha channel:

       canvas   1350 x 2760
       interior 1206 x 2622  at (72, 69)   ->  5.333% / 2.5%
       corner radius 222px                 ->  18.41% of interior width

   1206/2622 is the iPhone 16 Pro's real display, and min-chat-day.jpg
   is 804/1748 — the same 0.46, so the shot fills the hole exactly with
   nothing cropped. The frame keeps that ratio; everything else follows
   from its height. A box-shadow would trace the element's square box,
   so the drop is a filter, which follows the rim's actual silhouette. */
.app-phone { position: relative; aspect-ratio: 1350 / 2760; width: auto; margin: auto; transform: rotate(3deg); filter: drop-shadow(16px 26px 40px #110c1440); }
/* height:auto is load-bearing: the <img> carries height="1748", a
   presentational hint that `width: 100%` overrides but the height does
   not — so the shot rendered 290px wide by its full 1748px tall, the hero
   grew to 1976px and the headline went below the fold. With height auto
   the aspect-ratio takes over, which is what it is there for. */
.app-phone__shot { position: absolute; left: 5.333%; top: 2.5%; width: 89.333%; height: 95%; display: block; object-fit: cover; border-radius: 18.41% / 8.47%; transform: scale(1.015); }
/* min-ui/*.png screens (e.g. matchv2.png) are exported as a sheet with its
   own smaller baked-in rounded top corner, below a band of background art
   that already accounts for the status bar / Dynamic Island — real content,
   so it can't be cropped off, and this radius can't be tightened to the
   sheet's own either: that undershoots the rim's true hole and opens a gap
   at each corner where the page background shows through. The corner seam
   this leaves is fixed in the asset itself: matchv2-corners.png has the two
   top corners pre-darkened to bridge into the bezel, so the sheet's own
   corner no longer reads as a second, lighter curve inside the frame's.

   The tiny scale(1.015) above is a separate fix, for a separate seam: once
   .app-phone's rotate(deg) is applied, this layer's own rounded-corner clip
   and the rim's transparent hole above it are each anti-aliased on their
   own, and don't quite agree on the curve at rotated (non-axis-aligned)
   angles — a hairline of whatever sits behind both (the page background)
   peeks through right on the seam. Scaling the shot up by 1.5% bleeds its
   edge out past the rim's opening, where the rim's opaque bezel hides the
   overlap, so there's no gap left for the page background to show through.
   Confirmed by rendering rotated at 3x device-pixel-ratio: no scale still
   showed a hairline; 1.015 closed it. A wrapper + ::after (to paint a
   corner-darkening gradient instead of baking it into the asset) has this
   same seam even worse, since the ::after is a second independently
   clipped/rotated layer on top of the first — hence baking into pixels. */
.app-phone__rim { position: absolute; inset: 0; width: 100%; height: 100%; display: block; pointer-events: none; }
.app-phone__status { display: flex; justify-content: space-between; align-items: center; font-size: 11px; padding: 0 8px 20px; }
.app-phone__island { width: 88px; height: 23px; border-radius: 20px; background: #211c1b; }
.app-phone__header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px; }
.app-location { font-size: 11px; color: #e0d4c9; padding: 7px 10px; border: 1px solid #fff3; border-radius: 20px; }
.app-eyebrow { font-size: 8px; letter-spacing: .14em; color: #d5c1df; }
.hero--app .app-phone h2 { font-size: 25px; line-height: 1.15; margin: 9px 0; letter-spacing: -.035em; }
.app-phone__intro > p { font-size: 12px; color: #d0c4bd; }
.app-radar { position: relative; height: 192px; margin: 8px -10px 0; }
.app-radar__ring { position: absolute; left: 50%; top: 46%; translate: -50% -50%; border: 1px solid #f8e7db16; border-radius: 50%; }
.app-radar__ring--outer { width: 258px; height: 158px; transform: rotate(-18deg); }
.app-radar__ring--inner { width: 150px; height: 94px; transform: rotate(-18deg); background: radial-gradient(ellipse, #d1b0e412, transparent); }
.app-person { position: absolute; text-align: center; }
.app-person > span, .app-avatar { display: grid; place-items: center; width: 40px; height: 43px; border-radius: 45% 50% 42% 48%; background: radial-gradient(circle at 35% 10%, #eee1d6ad, transparent 60%), linear-gradient(150deg, #a58cad, #574757); box-shadow: inset 0 1px 1px #fff9, 0 5px 15px #1711184d; font-size: 14px; }
.app-person small { display: block; margin-top: 5px; font-size: 10px; color: #e1d6d0; }
.app-person--one { left: 18%; top: 13px; }
.app-person--two { right: 12%; top: 48px; }
.app-person--two > span { background-color: #a9b7a1; background-blend-mode: luminosity; }
.app-person--three { left: 22%; bottom: 29px; }
.app-person--three > span { width: 31px; height: 33px; background-color: #bba07c; background-blend-mode: luminosity; }
.app-radar__you { position: absolute; left: 50%; top: 45%; translate: -50% -50%; display: grid; justify-items: center; gap: 7px; font-size: 9px; color: #dbccd9; }
.app-radar__you i { width: 11px; height: 11px; border-radius: 50%; background: #e3c9f0; box-shadow: 0 0 0 7px #dec0f015, 0 0 24px #dfb7f080; }
.app-radar__caption { position: absolute; bottom: 3px; width: 100%; text-align: center; font-size: 9px; color: #cabbc7; }
.app-invite { position: relative; margin-top: 12px; padding: 15px; border: 1px solid #f4e7d733; border-radius: 23px; background: linear-gradient(145deg, #ffedda15, #d8b8eb09); box-shadow: inset 0 1px 0 #fffae91a, 0 8px 24px #120c1714; }
.app-invite__eyebrow { display: flex; align-items: center; gap: 6px; font-size: 9px; color: #e3d3e9; }
.app-invite__eyebrow > span:last-child { margin-left: auto; color: #bdb0b6; }
.app-live-dot { display: inline-block; width: 5px; height: 5px; border-radius: 50%; background: #d2dfb9; box-shadow: 0 0 9px #d2dfb955; }
.app-invite__person { display: flex; align-items: center; gap: 10px; margin: 16px 0 12px; }
.app-invite__person strong { display: block; font-size: 16px; font-weight: 600; }
.app-invite__person div > span { display: block; font-size: 9px; margin-top: 3px; color: #d0c4bd; }
.app-invite__spark { margin-left: auto; font-size: 26px; color: #dac4e5; }
.app-invite > p { font-size: 11px; line-height: 1.6; color: #e0d4cd; }
.app-invite__cta { display: flex; align-items: center; justify-content: space-between; margin-top: 14px; padding: 11px 16px; border-radius: 24px; background: linear-gradient(135deg, #ecdbf3, #bfa7d0); color: #392b40; box-shadow: inset 0 1px 1px #fff9, 0 3px 14px #c9a6dc1a; font-size: 12px; font-weight: 700; }
.app-phone__footer { display: flex; align-items: center; justify-content: center; gap: 7px; margin: 17px 0; font-size: 9px; color: #d0c4bd; }
.app-phone__home { width: 100px; height: 4px; margin: 0 auto; border-radius: 5px; background: #f9ecdf70; }
.hero__preview-note { position: absolute; bottom: 114px; left: -30px; padding: 15px 22px; border-radius: 20px; border: 1px solid #fff3; background: #605052b8; backdrop-filter: blur(20px); box-shadow: 0 12px 35px #150e1926, inset 0 1px 0 #ffffff26; transform: rotate(-7deg); font-size: 14px; line-height: 1.45; }
.hero__preview-note > span { float: left; font-size: 30px; margin-right: 12px; color: #dec9e9; }
@media (max-width: 800px) {
  .hero--app { padding-top: 28px; }
  .hero--app .shell { grid-template-columns: 1fr; gap: 36px; }
  .hero__copy { width: 100%; max-width: 560px; }
  .hero--app h1 { font-size: clamp(2.65rem, 7.5vw, 4rem); }
  .hero--app .wordmark--hero { width: 128px; margin-bottom: 16px; }
  .hero__preview { max-width: 430px; margin-top: 0; }
  .hero__preview-note { left: 0; }
}
@media (max-width: 400px) {
  .hero--app .lede { font-size: 16px; }
  .hero__preview { padding-inline: 8px; }
  .app-phone { transform: rotate(1deg); }
  .hero__preview-note { left: -4px; bottom: 110px; font-size: 12px; padding: 12px 16px; }
}
/* Keep the floating aside clear of the invitation and its action. */
@media (min-width: 801px) {
  .hero__preview-note { left: -65px; bottom: 142px; width: 180px; padding: 14px 16px; }
}
@media (max-width: 800px) {
  .hero__preview-note { display: none; }
}

/* min's sunlit home: the room belongs to the hero only. */
.hero--app {
  isolation: isolate;
  color: #3A3733;
  background: #eee7de url('/assets/img/min-home.jpg') center center / cover no-repeat;
  padding-block: 40px 100px;
  overflow: hidden;
  /* One neutral ink family for the whole hero — no hue, only value.
     The room behind is a warm near-white, so a warm ink read as brown
     the moment it was put on type this large. */
  --ink: #3A3733;
  --ink-soft: #565250;
  --ink-faint: #6B6764;
}
.hero--app::before { content: ''; position: absolute; inset: 0; z-index: -1; background: linear-gradient(90deg, #f1ebe4b3, #f1ebe42b 55%, transparent); pointer-events: none; }
/* The copy column takes the larger share: the headline's three lines are
   authored breaks, and the longest of them has to fit on one line at every
   desktop width or the break stops being a decision. Measured: the widest
   line runs 9.97em, so the size below leaves ~10% of headroom at the
   tightest width this layout is used at (801px). */
.hero--app .shell { grid-template-columns: 1.18fr 0.82fr; gap: 60px; }
.hero--app .lede, .hero--app .fineprint { color: var(--ink-soft); }
.hero--app .app-phone { margin-right: 30px; transform: rotate(4deg); filter: drop-shadow(18px 30px 44px #66504147); }
.hero__preview { padding-bottom: 28px; }
.hero--app .scroll-cta { color: var(--ink-faint); }

/* ── Desktop: the phone is capped by the viewport, not by the art ──
   .hero already carries min-height: 100svh (landing.css); what pushed it
   PAST one screen was the phone, which at 355px wide is 680px tall and
   ignores how much room the window actually has. On a 720px-tall window
   that plus the section's own padding came to 854px — a hero you had to
   scroll to finish.

   So its height is the budget and its width follows: `width: auto` hands
   the sizing to the shot's aspect-ratio, exactly as on mobile. 160px is
   the section's own padding (40 + 100) plus 20px of air; the preview's
   margin and padding are zeroed here because both were reserving room for
   things this layout no longer has (the caption, and the Pause min
   button), and 26px of stale margin is what kept it 18px over. */
@media (min-width: 801px) {
  .hero__preview {
    display: flex; justify-content: center; align-items: center;
    min-height: 0;
    margin: 0;
    padding-bottom: 0;
  }
  .hero--app .app-phone {
    width: auto;
    height: min(680px, calc(var(--vh-fixed, 100svh) - 160px));
    max-width: 100%;
    margin-right: 30px;
  }
}

/* ── the hero's words ──────────────────────────────────────────
   The headline is three authored lines, each its own block, so the
   break is a design decision and not a consequence of the viewport.
   The emphasis runs one way down the block: the middle line is the
   loudest (it is the promise), the first sets it up, the third is
   the aside that softens it. */
.hero--app h1 {
  font-size: clamp(2.4rem, 4.1vw, 3.7rem);
  line-height: 1.02;
  letter-spacing: -0.048em;
  margin-bottom: 24px;
}
.hero__line { display: block; }
/* All three lines share ONE ink. The emphasis is carried by weight
   alone — the gradient that used to run brown-to-violet across the
   middle line is gone, and with it the only place two palettes met. */
.hero__line:first-child {
  font-weight: var(--font-weight-semibold);
}
/* The loudest line — the promise. Bold, same ink as its neighbours. */
.hero__line--lit {
  font-weight: var(--font-weight-bold);
  /* tight tracking overhangs the glyph box on the last letter */
  padding-right: 0.06em;
}
.hero--app .hero__headline-soft {
  font-weight: var(--font-weight-semibold);
}

/* The lede carries one emphasised clause — the thing min actually
   gives you. Not an italic, and no highlighter: a weight step and the
   full-strength ink against the softer ink around it. */
.hero--app .lede {
  max-width: 42ch;
  font-size: 19px;
  line-height: 1.55;
}
.hero--app .lede em {
  font-style: normal;
  font-weight: var(--font-weight-bold);
  color: var(--ink);
}

/* The launch note: plain text. The only emphasis is on the two facts in
   it — who it opens for, and when. */
.hero--app .fineprint {
  margin-top: 18px;
  font-size: 13px;
  color: var(--ink-soft);
}
.hero--app .fineprint b { font-weight: var(--font-weight-bold); color: var(--ink); }

@media (max-width: 800px) {
  .hero--app h1 { font-size: clamp(1.6rem, 8.4vw, 3.2rem); }
  .hero--app .lede { font-size: 17px; }
}
@media (max-width: 400px) {
  .hero--app .fineprint { font-size: 12px; }
}

/* ── Mobile: one screen, nothing below the fold ───────────────
   The section is a flex column pinned to the viewport, and .hero__copy is
   `display: contents` so the wordmark, headline, lede, form and fineprint
   become flex items of .shell directly — which is what lets the phone be
   ordered BETWEEN the lede and the form rather than after everything:

     min · headline · lede · [ phone, takes the slack ] · form · fineprint

   The phone is the only thing here sized by height rather than width: it
   gets whatever the words and the form leave over and scales to it, so the
   column ends on the fineprint at every screen size instead of running a
   third of a page past the fold. */
@media (max-width: 800px) {
 /* A PORTRAIT cut of the room, not a crop of the landscape master. `cover`
    on a 390x844 hero scales to the taller side, so a 1.50-wide source had to
    grow to 1267px to fill 844px of height: the phone saw 31% of the scene,
    upscaled. This cut is 860x1560 — the room sits along the bottom at full
    width and the light above it is the scene's own top band carried upward,
    so the same 390x844 box now shows 84% of it, downscaled. Anchored to the
    bottom so a phone held sideways keeps the room rather than the empty air. */
 .hero--app { background-image: url('/assets/img/min-home-tall.jpg'); background-position: center bottom; }

 /* ── the shade under the mark ─────────────────────────────────
    The portrait cut is a near-white wall from the top of the frame down
    to the room, and the wordmark is cream resin — on mobile the mark was
    sitting on its own colour. So the top of the section gets a shade that
    feathers down from the top of the screen: full strength behind the
    mark, carrying it, and nothing left by the headline.

    The PLATEAU is the whole trick, and the first version got it wrong by
    putting the peak at 0% and letting it fall immediately. Measured, the
    letterforms do not sit at the top of this box — there is padding and
    the artwork's own margin above them:

        390x844   shade 206px   glyphs at  69- 96px  =  33-47%
        590x760   shade 183px   glyphs at  47- 68px  =  26-37%
        780x900   shade 221px   glyphs at  69- 96px  =  31-43%

    So the shade holds near peak to ~40% and only then feathers: the band
    the mark actually occupies gets 0.335-0.27 instead of the 0.215-0.135
    it was getting when the ramp started falling from 0%. Stops are
    hand-eased rather than a two-stop ramp, which bands visibly across
    200px of near-white.

    It is a pseudo-element on the section, not a box around the wordmark:
    a box would move with the flex column on every screen height, and the
    shade belongs to the room's light, not to the logo's layout.

    Painted at FULL strength in frame one — it is the ground the resin
    emerges against, not part of the emergence, so it is already there
    before the mark has drawn a single edge. Fading it IN would mean the
    logo forms over the bright wall (the exact problem this solves) and
    only gets its contrast once the animation is over.

    Then it relaxes. The emergence needs ~13% more shade than the parked
    mark does — an empty wall behind a half-drawn outline is the worst
    moment for contrast, and the formed mark carries its own weight. So
    the stops below are the LOUD value and the element eases back to
    0.885 of them (= the resting shade) once the resin has finished
    forming. 2.2s of a 13% opacity step is below the threshold where it
    reads as motion; it reads as the room settling. */
 .hero--app::after {
   content: '';
   position: absolute;
   inset: 0 0 auto;
   z-index: -1;
   height: clamp(180px, 27vh, 268px);
   background: linear-gradient(to bottom,
     rgba(70, 66, 62, 0.340) 0%,
     rgba(70, 66, 62, 0.335) 24%,
     rgba(70, 66, 62, 0.300) 40%,
     rgba(70, 66, 62, 0.225) 54%,
     rgba(70, 66, 62, 0.145) 68%,
     rgba(70, 66, 62, 0.075) 82%,
     rgba(70, 66, 62, 0.025) 92%,
     rgba(70, 66, 62, 0) 100%);
   pointer-events: none;
   /* 1250ms is --T, the length of the whole emerge track (landing.css,
      the wmx-* keyframes: outline 80–460, fill 340–700, glow lands at
      1040, and min is still morphing limbs and feet past that to the end).
      Nothing under the mark gives anything back until the last of it has
      landed. `both` is what holds frame one at the loud value through
      the delay — without it the element would paint at its own opacity of
      1 anyway, but the intent should be in the declaration. */
   animation: hero-shade-relax 1200ms cubic-bezier(0.4, 0, 0.6, 1) 1250ms both;
 }
 /* Symmetric ease, not an ease-out: an ease-out front-loads the change,
    which is the one thing that would make 13% visible. */
 @keyframes hero-shade-relax {
   from { opacity: 1; }
   to   { opacity: 0.885; }
 }
 /* No settle — the resting shade, from the first frame. */
 @media (prefers-reduced-motion: reduce) {
   .hero--app::after { animation: none; opacity: 0.885; }
 }

 /* --vh-fixed is one screen, in pixels, written once (see the inline
    script in the page head). The browser's toolbars retract as you scroll
    and svh moves with them — which grew the phone and pushed the form down
    the screen mid-scroll. A number that is measured once cannot do that;
    svh stays as the no-JS fallback. */
 .hero--app {
   display: flex;
   flex-direction: column;
   min-height: var(--vh-fixed, 100svh);
   text-align: center;
   padding-block: 26px 22px;
 }
 .hero--app .shell {
   display: flex;
   flex-direction: column;
   align-items: center;
   flex: 1;
   min-height: 0;
   gap: 0;
 }
 .hero__copy { display: contents; }

 .hero--app .wordmark--hero { order: 1; width: 112px; margin: 0 auto 10px; }
 .hero--app h1 { order: 2; margin: 0; }
 .hero--app .lede { order: 3; margin: 14px auto 0; max-width: 34ch; font-size: 16px; }
 .hero--app .hero__actions { order: 5; width: 100%; max-width: 420px; margin: 0; justify-content: center; }
 .hero--app .fineprint { order: 6; margin-top: 12px; }

 /* flex: 1 takes the slack and nothing more — an auto min-height would let
    the phone's intrinsic 674px push the column past the fold, which is what
    it used to do. But a floor is needed at the other end too: on a short
    screen the slack goes to nothing and the phone was collapsing to 17px
    wide. Below the floor the section is simply allowed to be taller than
    one screen, which is the honest answer on a 568px-tall phone. */
 .hero__preview {
   order: 4;
   flex: 1 1 0;
   min-height: min(calc(var(--vh-fixed, 100svh) * 0.38), 320px);
   display: flex;
   justify-content: center;
   align-items: center;
   width: 100%;
   max-width: none;
   margin: 12px 0 14px;
   padding: 0;
 }
 /* Height-driven: `width: auto` hands the sizing to the shot's own
    aspect-ratio, so the frame is always the right shape for whatever
    height it was given.

    That height is stated in svh rather than taken as `height: 100%` of the
    preview, and the difference is the whole bug this once had on real
    phones. A percentage height only resolves against an ancestor whose own
    height is DEFINITE, and nothing above here is: .hero--app carries
    min-height, not height, so the preview's height is an outcome of flex
    layout. Blink resolves it anyway — which is why device emulation in
    desktop devtools looked correct — but WebKit, and so every browser on
    iOS, drops the percentage to `auto`. The shot then fell back to its own
    aspect-ratio at full width and the frame rendered ~840px tall, running
    off the fold. An svh length is definite in both engines, so the frame is
    the same size everywhere and the shot's `height: 100%` below now has a
    definite box to resolve against. */
 .hero--app .app-phone {
   width: auto;
   height: min(calc(var(--vh-fixed, 100svh) * 0.44), 420px);
   max-width: 100%;
   margin: 0;
   transform: rotate(2deg);
 }

 /* The column already ends on the fineprint; a scroll nudge under it would
    only push the fold it is promising there isn't one past. */
 .hero--app .scroll-cta { display: none; }
}

/* Short screens — small phones, and any phone in landscape. Everything
   that isn't the headline, the phone or the form gives up its room, and
   the lede steps aside entirely: the screenshot is saying the same thing
   and there is only one screen to say it on. */
/* Gated on a CLASS, not on (max-height: 700px). A live height query flips
   the moment the browser's toolbar retracts past the breakpoint, and the
   lede would pop back in mid-scroll — the same shift --vh-fixed exists to
   stop. The class is set from the height measured once, so what the page
   decided on arrival is what it keeps. */
@media (max-width: 800px) {
 :root.is-short .hero--app { padding-block: 16px 14px; }
 :root.is-short .hero--app .wordmark--hero { width: 92px; margin-bottom: 6px; }
 /* The lede STAYS on a short screen — it is the only line that says what
    the app actually is, and dropping it left the headline unexplained.
    It pays for itself out of the phone: condensed type here, and the frame
    below gives up the difference. The column is ordered so that if anything
    does fall past the fold it is the fineprint, never the form. */
 :root.is-short .hero--app .lede {
   font-size: 14.5px;
   line-height: 1.42;
   max-width: 34ch;
   margin-top: 8px;
 }
 :root.is-short .hero--app h1 { font-size: clamp(1.62rem, 7.3vw, 1.95rem); }
 :root.is-short .hero--app .wordmark--hero { width: 84px; }
 :root.is-short .hero__preview { margin: 8px 0 10px; min-height: 0; }
 :root.is-short .hero--app .app-phone { height: min(calc(var(--vh-fixed, 100svh) * 0.35), 340px); }
 :root.is-short .hero__preview { margin: 10px 0 12px; }
 :root.is-short .hero--app .fineprint { margin-top: 10px; }
}

/* ── Mobile, homepage only: the phone bleeds under the signup ──
   Sized to fit between the headline and the form, the phone came out
   ~170px wide on a 390px screen — too small to read the screen it is
   there to show. So it is sized to the SCREEN instead, and simply allowed
   to run past its own box: the form and fineprint stack over its lower
   part (mostly empty background in the match shot), and .hero--app's
   overflow: hidden crops it at the section edge — a phone rising out of
   the fold, which also says there is more below.

   Opt-in by class: the Helsinki page shares this file but shows the
   chat shot, whose reply field sits at the very
   bottom, exactly where the form would land. */
@media (max-width: 800px) {
 /* The box runs 72px on under the input, and its bottom edge is where the
    phone dissolves into the room: seen through the frosted input, the
    timer, the slider and the bezel edge were noise behind the one thing
    to tap. The mask sits on this box, not on .app-phone — on the phone it
    would also clip the drop-shadow to the phone's own rectangle, which
    showed as a faint square edge. So this box spans the full screen width
    (past the column's gutters) and the shadow keeps its soft sides; the
    24px of top padding is its headroom. */
 .hero--bleed .hero__preview {
   position: relative;
   z-index: 0;
   align-items: flex-start;
   min-height: 0;
   width: 100vw;
   margin: -8px calc(50% - 50vw) -72px;
   padding-top: 24px;
   -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 128px), transparent calc(100% - 20px));
           mask-image: linear-gradient(to bottom, #000 calc(100% - 128px), transparent calc(100% - 20px));
 }
 /* Sized from the room it actually has, not from the screen: how much
    space the headline leaves above the form changes phone to phone (how
    the lines and the fineprint wrap), so a fixed share of the height left
    the avatar clear on one phone and buried under the input on the next.
    The box above is a size container whose height is exactly that room
    (flex slack, never content), and the phone is scaled until the match's
    avatar — ~66% down the frame — lands just above the input:

      phone top → input top = 100cqh − 74px   (padding and margins, measured)
      height = (that − 10px of air) ÷ 0.66

    cqh rather than a % height for the reason spelled out on the base rule:
    WebKit drops a % height here to auto; a container unit is definite in
    both engines. The floor keeps the smallest phones from shrinking the
    frame back to a thumbnail (there the avatar is allowed under the glass);
    the ceiling stops it outgrowing the screen's width. The :root in the
    selector is only there to outrank the :root.is-short size above. */
 .hero--bleed .hero__preview { container-type: size; }
 :root .hero--app.hero--bleed .app-phone {
   height: clamp(300px, (100cqh - 84px) * 1.515, 175vw);
   max-width: none;
 }
 .hero--bleed .hero__actions,
 .hero--bleed .fineprint { position: relative; z-index: 1; }

 :root.is-short .hero--bleed .hero__preview { margin: -14px calc(50% - 50vw) -72px; }
}

/* ── the signup, inline ────────────────────────────────────────
   The homepage asks for the address where the wanting happens — the hero
   and the closer — instead of linking to /waitlist and spending a tap on
   it. Built from --ink so one set of rules serves both contexts: the
   sunlit room (dark ink on light) and the closer (cream ink on dark).
   The <form> contract and the fetch live in waitlist-form.js. */
.signup-block { width: 100%; max-width: 460px; }
.hero--app .signup-block { margin: 0; }
.signup { display: grid; gap: 10px; }
.signup__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 10px;
  align-items: stretch;
}
/* 16px, not 15: under 16 iOS zooms the whole page the moment the field is
   tapped, and there is nowhere for a full-bleed hero to zoom to. */
.signup__input {
  width: 100%;
  min-width: 0;
  padding: 17px 22px;
  border: 1px solid color-mix(in srgb, var(--ink) 18%, transparent);
  border-radius: 999px;
  background: color-mix(in srgb, var(--ink) 7%, transparent);
  box-shadow: inset 0 2px 5px color-mix(in srgb, var(--ink) 10%, transparent);
  color: var(--ink);
  font-family: inherit;
  font-size: 16px;
  letter-spacing: -0.01em;
  outline: none;
  transition: border-color var(--duration-quick) ease, background var(--duration-quick) ease, box-shadow var(--duration-quick) ease;
}
.signup__input::placeholder { color: color-mix(in srgb, var(--ink) 44%, transparent); }
.signup__input:focus {
  border-color: color-mix(in srgb, var(--color-accent) 55%, transparent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent) 20%, transparent);
}
.signup__input[aria-invalid="true"] { border-color: color-mix(in srgb, var(--color-error) 60%, transparent); }
/* Chrome paints autofilled fields a flat yellow, which would punch a hole
   straight through the material. */
.signup__input:-webkit-autofill {
  -webkit-text-fill-color: var(--ink);
  -webkit-box-shadow: inset 0 0 0 100px color-mix(in srgb, var(--ink) 7%, var(--bg));
  caret-color: var(--ink);
}
/* The room is bright enough that a tinted field disappears into it. */
.hero--app .signup__input {
  border-color: #493d3626;
  background: #fffdf9a6;
  box-shadow: inset 0 2px 5px #493d3614, inset 0 -1px 0 #ffffffb3;
}
.signup .btn { white-space: nowrap; }
.signup .field-error,
.signup .status {
  margin: 0;
  padding-inline: 6px;
  font-size: var(--text-sm);
  font-weight: 500;
  line-height: 1.4;
}
.signup .field-error { color: var(--color-error); }
.signup .status.error { color: var(--color-error); }
/* Neither message takes any room until it has something to say. */
.signup .field-error:empty,
.signup .status:empty { display: none; }

.signup__done { display: flex; align-items: center; gap: 12px; text-align: left; }
/* A class rule beats the UA's [hidden] { display: none }, so say it here. */
.signup__done[hidden] { display: none; }
.signup__done strong { display: block; font-size: var(--text-md); font-weight: 600; }
.signup__done div > span { font-size: var(--text-sm); color: color-mix(in srgb, var(--ink) 68%, transparent); }
.signup__tick {
  flex: 0 0 auto;
  display: grid; place-items: center;
  width: 34px; height: 34px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--color-accent) 22%, transparent);
  color: var(--ink);
}
.signup__tick svg { width: 16px; height: 16px; display: block; }

/* One column below the width where the field and the button stop both
   being comfortable — the button keeps its full tap target. */
@media (max-width: 560px) {
  .signup-block { max-width: 420px; }
  .signup__row { grid-template-columns: 1fr; }
  .signup .btn { justify-content: center; }
  .signup__done { justify-content: center; text-align: center; }
}


/* ── "here it is" ──────────────────────────────────────────────
   Added by dock() when the floating CTA jumps you back up here. A field
   that has just been focused from the other end of the page needs to say
   so — two rings breathing out of its border, then gone. Only the ring
   changes: the field keeps its own inset shading throughout (restated in
   every frame, or the pulse would flatten it) and its geometry never
   moves, so nothing under it reflows.

   --violet-ink, not the plain accent: the hero's ground is a bright cream
   room, and --violet at 55% on it is invisible. This is the same hue at
   the darkness the token set already reserves for marks that have to read
   on light.

   Last in the file on purpose — it has to outrank `.hero--app
   .signup__input`, which is the same specificity and sets its own
   box-shadow. */
@keyframes signup-called {
  0% {
    box-shadow: var(--signup-bed), 0 0 0 0 color-mix(in srgb, var(--violet-ink) 85%, transparent);
  }
  55% {
    box-shadow: var(--signup-bed), 0 0 0 11px color-mix(in srgb, var(--violet-ink) 0%, transparent);
  }
  56% {
    box-shadow: var(--signup-bed), 0 0 0 0 color-mix(in srgb, var(--violet-ink) 60%, transparent);
  }
  100% {
    box-shadow: var(--signup-bed), 0 0 0 11px color-mix(in srgb, var(--violet-ink) 0%, transparent);
  }
}
.signup__input.is-called {
  --signup-bed: inset 0 2px 5px #493d3614, inset 0 -1px 0 #ffffffb3;
  animation: signup-called 1200ms var(--ease-emergence) both;
  border-color: var(--violet-ink);
  background: #fffdf9;
}
@media (prefers-reduced-motion: reduce) {
  /* No pulse, but the field still has to be identifiable — it holds one
     ring for the same beat instead of breathing it out. dock() takes the
     class off on a timer, since there is no animationend to wait for. */
  .signup__input.is-called {
    animation: none;
    box-shadow: var(--signup-bed), 0 0 0 4px color-mix(in srgb, var(--violet-ink) 45%, transparent);
  }
}
